« SE3Groupe2025-2 » : différence entre les versions
Aucun résumé des modifications |
|||
| (193 versions intermédiaires par 3 utilisateurs non affichées) | |||
| Ligne 1 : | Ligne 1 : | ||
= Programmation des systèmes embarqués = | |||
Programmeur. | |||
== Carte électronique == | |||
Carte réalisée en utilisant le logiciel <code>KiCAD</code> : [[file:2025-PSE-2-Prog.zip|ma carte électronique]]. | |||
[[File:2025_PSE-02-PROG-schema.pdf|thumb|left|710px|Schéma électronique]] | |||
[[File:2025_PSE-02-PROG-PCB.png|thumb|right|710px|Routage]] | |||
<p style="clear: both;" /> | |||
{| style="margin: auto;" | |||
| [[File:Schema final 3D.png|thumb|500px|3d]] | |||
| [[File:2025 PSE-02-PROG-carte-soudée-front.jpg|thumb|400px|Front]] | |||
| [[File:2025 PSE-02-PROG-carte-soudée-back.jpg|thumb|400px|Back]] | |||
|} | |||
<p style="clear: both;" /> | |||
Vidéo très courte et en basse résolution de la carte en fonctionnement : | |||
== Programmation == | |||
Les tests finaux on été réalisé sur une arduino UNO [[File : PSE2_montage_isp.png|center|400x400px|Montage]] | |||
===Teste de la carte=== | |||
Pour tester notre carte on a fait des codes simples. | |||
====led_blink==== | |||
<syntaxhighlight lang="c" line="1"> | |||
// Clignote les 3 LEDS | |||
#include <avr/io.h> | |||
#include <util/delay.h> | |||
#define BLINK_DELAY 50 // en milli secondes | |||
// PB 5, 6, 7 correspond respectivement aux leds 1 , 2 , 3 | |||
int main() | |||
{ | |||
// configurer les led en sortie : | |||
DDRB |= (1 << 5); // PORTB 5e bit pour la LED1 | |||
DDRB |= (1 << 6); // PORTB 6e bit pour la LED2 | |||
DDRB |= (1 << 7); // PORTB 7e bit pour la LED3 | |||
for (int i = 0; i < 50; i++) | |||
{ | |||
// led on | |||
PORTB |= (1 << 5); // OR | |||
PORTB |= (1 << 6); | |||
PORTB |= (1 << 7); | |||
_delay_ms(BLINK_DELAY); | |||
// led off | |||
PORTB &= ~(1 << 5); // AND + NOT | |||
PORTB &= ~(1 << 6); | |||
PORTB &= ~(1 << 7); | |||
_delay_ms(BLINK_DELAY); | |||
} | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
[[File:2025-PSE-02-PROG-video-blink.mp4|Blink]] | |||
====boutons==== | |||
<syntaxhighlight lang="c" line="1"> | |||
#include <avr/io.h> | |||
#include <util/delay.h> | |||
#define b7 0b10000000 | |||
#define b6 0b01000000 | |||
#define L5 0b11011111 | |||
#define L6 0b10111111 | |||
#define L7 0b01111111 | |||
void config() | |||
{ | |||
DDRB |= (1 << 5); | |||
DDRB |= (1 << 6); | |||
DDRB |= (1 << 7); | |||
DDRC &= 0x00; | |||
// Activation des résistances de Pull-Up internes sur le port C | |||
PORTC |= (b6 | b7); | |||
} | |||
int lire_bouton(int bouton) | |||
{ | |||
return (PINC & bouton) == 0; | |||
} | |||
void ecrire_LED(int LED, int etat) | |||
{ | |||
switch (etat) | |||
{ | |||
case 0: | |||
PORTB &= LED; // Applique le masque avec le 0 pour éteindre | |||
break; | |||
case 1: | |||
PORTB |= ~LED; // Inverse le masque pour avoir un 1 et allumer | |||
break; | |||
} | |||
} | |||
int main() | |||
{ | |||
config(); | |||
while (1) | |||
{ | |||
if (lire_bouton(b6) && lire_bouton(b7)) | |||
{ | |||
ecrire_LED(L7, 1); | |||
ecrire_LED(L6, 0); | |||
ecrire_LED(L5, 0); | |||
} | |||
else if (lire_bouton(b7)) | |||
{ | |||
ecrire_LED(L5, 1); | |||
} | |||
else if (lire_bouton((b6))) | |||
{ | |||
ecrire_LED(L6, 1); | |||
} | |||
else | |||
{ | |||
// On éteint tout si aucun bouton n'est pressé | |||
ecrire_LED(L6, 0); | |||
ecrire_LED(L5, 0); | |||
ecrire_LED(L7, 0); | |||
} | |||
} | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
[[File:2025-PSE-02-PROG-video-boutons.mp4|Boutons]] | |||
===LUFA=== | |||
Pour injecter un nouveau code sur la carte(être sudo): | |||
<syntaxhighlight lang="bash"> | |||
dfu-programmer atmega16u2 erase --force | |||
dfu-programmer atmega16u2 flash --suppress-bootloader-mem file.hex | |||
dfu-programmer atmega16u2 reset | |||
</syntaxhighlight> | |||
Pour injecter le même code mais modifié sur la carte(être sudo): | |||
<syntaxhighlight lang="bash"> | |||
dfu-programmer atmega16u2 erase | |||
dfu-programmer atmega16u2 flash file.hex | |||
dfu-programmer atmega16u2 reset | |||
</syntaxhighlight> | |||
====Connexion avec minicom==== | |||
On a récupéré le dossier LUFA depuis le wiki du cours. Puis on a copié le fichier <code>VirtualSerial</code> situé dans <code>lufa-LUFA-210130-NSI/Demos/Device/ClassDriver/VirtualSerial</code>. | |||
L'objectif ici est de communiquer grâce à la LUFA en USB sur minicom. | |||
Dans <code>VirtualSerial.c</code> on modifie la fonction <code>CheckJoystickMovement(void)</code> par la fonction : | |||
<syntaxhighlight lang="c"> | |||
#define b7 0b10000000 | |||
#define b6 0b01000000 | |||
int Button_Status(int boutton) | |||
{ | |||
return (PINC & boutton) == 0; | |||
} | |||
void CheckButtons(void) | |||
{ | |||
char *ReportString = NULL; | |||
static bool ActionSent = false; | |||
if (Button_Status(b6) && Button_Status(b7)) | |||
{ | |||
ReportString = "both pressed\r\n"; | |||
_delay_ms(250); | |||
ActionSent = false; | |||
} | |||
else if (Button_Status(b6)) | |||
{ | |||
ReportString = "b6 pressed\r\n"; | |||
_delay_ms(250); | |||
ActionSent = false; | |||
} | |||
else if (Button_Status(b7)) | |||
{ | |||
ReportString = "b7 pressed\r\n"; | |||
_delay_ms(250); | |||
ActionSent = false; | |||
} | |||
if ((ReportString != NULL) && (ActionSent == false)) | |||
{ | |||
ActionSent = true; | |||
/* Write the string to the virtual COM port via the created character stream */ | |||
fputs(ReportString, &USBSerialStream); | |||
/* Alternatively, without the stream: */ | |||
// CDC_Device_SendString(&VirtualSerial_CDC_Interface, ReportString); | |||
} | |||
} | |||
</syntaxhighlight> | |||
On change <code>VirtualSerial.h</code> en conséquence. | |||
On modifie la <code>Makefile</code> adapté à notre µP : | |||
<syntaxhighlight lang="Makefile"> | |||
MCU = atmega16u2 | |||
ARCH = AVR8 | |||
BOARD = NONE | |||
F_CPU = 16000000 | |||
F_USB = $(F_CPU) | |||
OPTIMIZATION = s | |||
TARGET = VirtualSerial | |||
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS) | |||
LUFA_PATH = ../../LUFA | |||
CC_FLAGS = -DUSE_LUFA_CONFIG_HEADER -IConfig/ | |||
LD_FLAGS = | |||
</syntaxhighlight> | |||
On compile le programme : | |||
<syntaxhighlight lang="bash"> | |||
make | |||
</syntaxhighlight> | |||
On injecte le code sur la carte: | |||
<syntaxhighlight lang="bash"> | |||
sudo dfu-programmer atmega16u2 erase | |||
sudo dfu-programmer atmega16u2 flash | |||
sudo dfu-programmer atmega16u2 reset | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="bash"> | |||
Erasing flash... Success | |||
Checking memory from 0x0 to 0x2FFF... Empty. | |||
Checking memory from 0x0 to 0xEFF... Empty. | |||
0% 100% Programming 0xF00 bytes... | |||
[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] Success | |||
0% 100% Reading 0x3000 bytes... | |||
[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] Success | |||
Validating... Success | |||
0xF00 bytes written into 0x3000 bytes memory (31.25%). | |||
</syntaxhighlight> | |||
et on observe bien un changement de nom de notre carte : | |||
<syntaxhighlight lang="bash"> | |||
lsusb | |||
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub | |||
Bus 001 Device 002: ID 0408:a061 Quanta Computer, Inc. HD User Facing | |||
Bus 001 Device 003: ID 8087:0026 Intel Corp. AX201 Bluetooth | |||
Bus 001 Device 005: ID 046d:0aba Logitech, Inc. PRO X Wireless Gaming Headset | |||
Bus 001 Device 012: ID 046d:c08b Logitech, Inc. G502 SE HERO Gaming Mouse | |||
Bus 001 Device 016: ID 03eb:2044 Atmel Corp. LUFA CDC Demo Appli | |||
</syntaxhighlight> | |||
Ensuite on configure <code>minicom</code> via : <code>minicom -os</code> | |||
'''configuration minicom''' :<ul> | |||
<li>"Serial port setup" -> <code>ENTRER</code></li> | |||
<li><code>A</code> -> <code>/dev/ttyACM0</code> -> <code>ENTRER</code></li> | |||
<li><code>E</code> -> <code>C</code> -> <code>ENTRER</code></li> | |||
<li><code>F</code> -> <code>ENTRER</code></li> | |||
<li>"Save setup as dfl" -> <code>ENTRER</code></li> | |||
<li><code>Echape</code> ou <code>Exit</code></li> | |||
</ul> | |||
Puis on lance minicom via (être en sudo) <syntaxhighlight lang="bash">minicom</syntaxhighlight> | |||
<br> | |||
Pour retéléverser un programme sur la carte, il faut la repasser en DFU. Pour cela il suffit d'appuyer sur le bouton <code>RESET</code><br> | |||
'''Resultat'''<br> | |||
[[File:BE2_minicom.mp4]] | |||
====Gestion personnalisée des Endpoints==== | |||
=====Configuration IN et OUT===== | |||
L'objectif est de s'affranchir de la classe CDC pour gérer directement les points d'accès (Endpoints). On définit une interface avec un point d'accès OUT (pour contrôler les LEDs depuis le PC) et un point d'accès IN (pour envoyer l'état de la carte au PC). | |||
On modifie <code>Descriptors.h</code> pour définir les adresses : | |||
<syntaxhighlight lang="c"> | |||
/* Adresses des points d'accès */ | |||
#define LED_OUT_EPADDR (ENDPOINT_DIR_OUT | 1) // Endpoint 1 OUT | |||
#define DATA_IN_EPADDR (ENDPOINT_DIR_IN | 2) // Endpoint 2 IN | |||
/* Taille des paquets*/ | |||
#define LED_EPSIZE 8 | |||
</syntaxhighlight> | |||
Dans <code>main.c</code>, on remplace la gestion CDC par la configuration manuelle des points d'accès lors de l'énumération : | |||
<syntaxhighlight lang="c"> | |||
void EVENT_USB_Device_ConfigurationChanged(void) | |||
{ | |||
// Configuration du point d'accès pour recevoir les ordres (LEDs) | |||
Endpoint_ConfigureEndpoint(LED_OUT_EPADDR, EP_TYPE_INTERRUPT, LED_EPSIZE, 1); | |||
// Configuration du point d'accès pour envoyer les données (Boutons/ISP) | |||
Endpoint_ConfigureEndpoint(DATA_IN_EPADDR, EP_TYPE_INTERRUPT, LED_EPSIZE, 1); | |||
} | |||
</syntaxhighlight> | |||
=====Traitement des données OUT===== | |||
Pour piloter les LEDs (même si elles ne sont pas encore soudées, le code cible le <code>PORTB</code>), on crée une fonction de traitement: | |||
<syntaxhighlight lang="c"> | |||
void ProcessLEDControl(void) | |||
{ | |||
Endpoint_SelectEndpoint(LED_OUT_EPADDR); | |||
if (Endpoint_IsOUTReceived()) | |||
{ | |||
if (Endpoint_IsReadWriteAllowed()) | |||
{ | |||
// On lit l'octet envoyé par le PC | |||
uint8_t LEDMask = Endpoint_Read_8(); | |||
// On l'applique aux LEDs (PORTB) | |||
PORTB = LEDMask; | |||
} | |||
Endpoint_ClearOUT(); | |||
}Atmel-0943-In-System-Programming_ApplicationNote_AVR910 | |||
} | |||
</syntaxhighlight> | |||
=====Traitement des données IN===== | |||
On récupère l'état des boutons dans le <code>PINC</code> : | |||
<syntaxhighlight lang="c"> | |||
void ButtonsStatus(void) | |||
{ | |||
Endpoint_SelectEndpoint(DATA_IN_EPADDR); | |||
if (Endpoint_IsReadWriteAllowed()) | |||
{ | |||
Endpoint_Write_8(PINC & 0xC0); //pour activer les résistances de pull up sur b6 et b7. | |||
Endpoint_ClearIN(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
=====Modification du Makefile===== | |||
On retire les sources de la classe CDC (<code>LUFA_SRC_USBCLASS</code>) car nous utilisons désormais les fonctions "Low Level" de la LUFA. | |||
<syntaxhighlight lang="Makefile"> | |||
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB) | |||
</syntaxhighlight> | |||
=== Programme PC avec libusb === | |||
Maintenant que la carte est configurée avec des Endpoints bruts (IN et OUT), nous devons écrire un programme C fonctionnant sous Linux pour communiquer avec elle. L'outil standard pour cela est la bibliothèque <code>libusb-1.0</code>. | |||
==== Code de test IN/OUT (main_pc.c) ==== | |||
Ce programme a pour but de valider la liaison USB bas niveau. Il va : | |||
Ouvrir la communication avec le périphérique (VID/PID). | |||
Réclamer l'interface. | |||
Envoyer un ordre <code>OUT</code> pour allumer les broches des LEDs (PORTB). | |||
Lire un état <code>IN</code> pour récupérer l'état des boutons (PORTC). | |||
=====Ouvrir la communication avec le périphérique===== | |||
<syntaxhighlight lang="c"> | |||
libusb_device_handle *init_usb(void) | |||
{ | |||
libusb_device_handle *dev_handle = NULL; | |||
if (libusb_init(NULL) < 0) | |||
{ | |||
printf("Erreur d'initialisation de libusb\n"); | |||
return NULL; | |||
} | |||
dev_handle = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID); | |||
if (dev_handle == NULL) | |||
{ | |||
printf("Impossible d'ouvrir le périphérique (03EB:2044). Avez-vous utilisé sudo ?\n"); | |||
libusb_exit(NULL); | |||
return NULL; | |||
} | |||
if (libusb_kernel_driver_active(dev_handle, 0) == 1) | |||
{ | |||
libusb_detach_kernel_driver(dev_handle, 0); | |||
} | |||
return dev_handle; | |||
} | |||
</syntaxhighlight> | |||
=====Réclamer l'interface===== | |||
<syntaxhighlight lang="c"> | |||
int reclamer_interface(libusb_device_handle *dev_handle) | |||
{ | |||
int r = libusb_claim_interface(dev_handle, 0); | |||
if (r < 0) | |||
{ | |||
printf("Erreur de réclamation de l'interface : %s\n", libusb_error_name(r)); | |||
libusb_close(dev_handle); | |||
libusb_exit(NULL); | |||
return -1; | |||
} | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
=====Configuration LED en OUT===== | |||
On veut allumer les LEDS avec les touches du clavier en LIVE. Pour cela on utilise la bibliothèque <code>termios.h</code> utilisé dans la fonction <code>kbhit()</code>. | |||
<syntaxhighlight lang="c"> | |||
printf("\n=== MODE LIVE ACTIVÉ ===\n"); | |||
printf("Touches :\n"); | |||
printf(" [A] : Basculer LED 1 (PB5)\n"); | |||
printf(" [Z] : Basculer LED 2 (PB6)\n"); | |||
printf(" [E] : Basculer LED 3 (PB7)\n"); | |||
printf(" [Q] : Quitter\n"); | |||
printf("========================\n\n"); | |||
unsigned char data_out = 0x00; | |||
unsigned char last_data_in = 0xFF; // Pour mémoriser l'ancien état des boutons | |||
int actual_length; | |||
int running = 1; | |||
while (running) | |||
{ | |||
if (kbhit()) | |||
{ | |||
char c = getchar(); | |||
int send_update = 0; | |||
switch (c) | |||
{ | |||
case 'a': | |||
data_out ^= (1 << 5); | |||
send_update = 1; | |||
break; // Bascule PB5 | |||
case 'z': | |||
data_out ^= (1 << 6); | |||
send_update = 1; | |||
break; // Bascule PB6 | |||
case 'e': | |||
data_out ^= (1 << 7); | |||
send_update = 1; | |||
break; // Bascule PB7 | |||
case 'q': | |||
running = 0; | |||
break; | |||
} | |||
if (send_update) | |||
{ | |||
// Envoi de la nouvelle configuration OUT | |||
int r = libusb_interrupt_transfer(dev_handle, EP_OUT, &data_out, 1, &actual_length, 50); | |||
if (r == 0) | |||
{ | |||
printf("[PC] Ordre envoyé : 0x%02X\n", data_out); | |||
} | |||
else | |||
{ | |||
printf("[PC] Erreur OUT : %s\n", libusb_error_name(r)); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
=====Configuration boutons en IN===== | |||
Ce bout de code permet d'afficher l'état des boutons. | |||
<syntaxhighlight lang="c"> | |||
unsigned char data_in = 0; | |||
// On met un timeout très court (10ms) pour ne pas bloquer la boucle | |||
int r = libusb_interrupt_transfer(dev_handle, EP_IN, &data_in, 1, &actual_length, 10); | |||
if (r == 0 && actual_length == 1) | |||
{ | |||
// On n'affiche que si l'état des boutons a changé (pour éviter de spammer la console) | |||
if (data_in != last_data_in) | |||
{ | |||
printf("[CARTE] État PORTC : 0x%02X -> ", data_in); | |||
if ((data_in & (1 << 6)) == 0) | |||
printf("B6 pressé ! "); | |||
if ((data_in & (1 << 7)) == 0) | |||
printf("B7 pressé ! "); | |||
if ((data_in & (1 << 6)) != 0 && (data_in & (1 << 7)) != 0) | |||
printf("Relâchés."); | |||
printf("\n"); | |||
last_data_in = data_in; | |||
} | |||
} | |||
// Petite pause pour ne pas surcharger le processeur du PC (10 millisecondes) | |||
usleep(10000); | |||
} | |||
</syntaxhighlight> | |||
====Résultat==== | |||
<syntaxhighlight lang="bash"> | |||
sudo ./main_pc | |||
</syntaxhighlight> | |||
'''Terminal''' | |||
<syntaxhighlight lang="bash"> | |||
=== MODE LIVE ACTIVÉ === | |||
Touches : | |||
[A] : Basculer LED 1 (PB5) | |||
[Z] : Basculer LED 2 (PB6) | |||
[E] : Basculer LED 3 (PB7) | |||
[Q] : Quitter | |||
======================== | |||
[CARTE] État PORTC : 0xC0 -> Relâchés. | |||
[CARTE] État PORTC : 0x40 -> B7 pressé ! | |||
[CARTE] État PORTC : 0xC0 -> Relâchés. | |||
[CARTE] État PORTC : 0x80 -> B6 pressé ! | |||
[CARTE] État PORTC : 0xC0 -> Relâchés. | |||
[CARTE] État PORTC : 0x00 -> B6 pressé ! B7 pressé ! | |||
[CARTE] État PORTC : 0x40 -> B7 pressé ! | |||
[CARTE] État PORTC : 0xC0 -> Relâchés. | |||
a[PC] Ordre envoyé : 0x20 | |||
z[PC] Ordre envoyé : 0x60 | |||
e[PC] Ordre envoyé : 0xE0 | |||
q | |||
Fermeture du programme... | |||
</syntaxhighlight> | |||
'''Vidéo après soudure des LEDS''' | |||
[[File:PSE2_main_pc_demo.mp4]] | |||
=== Lecture ISP === | |||
L'objectif est maintenant d'utiliser notre communication IN/OUT pour envoyer des commandes SPI à un microcontrôleur cible afin de lire sa signature (Device ID). | |||
==== Côté Carte (LUFA) ==== | |||
On modifie notre fonction <code>main.c</code>, afin d'inclure la lecture ISP. | |||
<ul> | |||
<li> On '''initialise''' le SPI | |||
<syntaxhighlight lang="c"> | |||
void spi_init(void) | |||
{ | |||
SPI_DDR |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS); // Définition des sorties | |||
SPI_DDR &= ~(1 << SPI_MISO); // Définition de l'entrée | |||
SPI_PORT |= (1 << SPI_SS); // Désactivation du périphérique | |||
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0); // Activation SPI (SPE) en état maître (MSTR) | |||
SPSR &= ~(1 << SPI2X); // horloge F_CPU/128 (SPI2X=0, SPR1=1,SPR0=1) | |||
} | |||
</syntaxhighlight></li> | |||
<li> On fait le '''transfert''' l'octet. On place l'octet à envoyer dans <code>SPDR</code>, on attend que l'octet arrive chez l'esclave, puis on renvoie ce que nous envoie l'esclave. | |||
<syntaxhighlight lang="c"> | |||
uint8_t spi_transfer(uint8_t data) | |||
{ | |||
SPDR = data; // Octet a envoyer | |||
while (!(SPSR & (1 << SPIF))) | |||
; // Attente fin envoi (drapeau SPIF du statut) | |||
return SPDR; | |||
} | |||
</syntaxhighlight></li> | |||
<li> On réécrit la fonction qui gère la '''commande OUT''' du PC pour inclure le paquet SPI. Si l'octet ne vaut pas <code>0x01</code> alors on ne fait rien, sinon on applique au LED la valeurs du paquet OUT, comme précédemment. | |||
<syntaxhighlight lang="c"> | |||
uint8_t commande_PC = 0; | |||
void ProcessOUT(void) | |||
{ | |||
Endpoint_SelectEndpoint(DATA_OUT_EPADDR); | |||
if (Endpoint_IsOUTReceived()) | |||
{ | |||
if (Endpoint_IsReadWriteAllowed()) | |||
{ | |||
// On lit l'octet envoyé par le PC | |||
commande_PC = Endpoint_Read_8(); | |||
if (commande_PC != 0x01) | |||
// On l'applique aux LEDs (PORTB) | |||
PORTB = commande_PC; | |||
} | |||
Endpoint_ClearOUT(); | |||
} | |||
} | |||
</syntaxhighlight></li> | |||
<li> Pour la '''gestion du IN :''' si le paquet n'est pas <code>0x01</code> alors on lit l'état des boutons comme précemment. Sinon on active le SPI, et on applique le protocole ISP. Ce protocole consiste à envoyer une série de 4 octets dans un ordre bien spécifique, comme indiqué dans la datasheet <code>Atmel-0943-In-System-Programming_ApplicationNote_AVR910</code> qu'on peut retrouver dans le dossier <code>datasheet</code>. | |||
[[File : PSE2_ISP_command.png|center|1000x1000px]] | |||
'''Que se passe-t-il pendant les 4 octets du protocole AVR ISP ?'''<br> | |||
Le protocole ISP d'Atmel est conçu pour que chaque instruction fasse exactement 32 bits (4 octets). | |||
<ul> | |||
<li>'''Octet 1 (0x30) :''' Le Maître dit "Je veux lire la signature". L'Esclave reçoit ça, mais ne renvoie rien pour le moment.</li> | |||
<li>'''Octet 2 (0x00) :''' Le Maître envoie l'adresse de poids fort (inutile ici). L'Esclave est en train de comprendre la commande <code>0x30</code>.</li> | |||
<li>'''Octet 3 (0x00, 0x01 ou 0x02) :''' Le Maître demande précisément l'adresse 0, 1 ou 2 de la signature. L'Esclave va chercher l'information dans sa mémoire.</li> | |||
<li>'''Octet 4 (0x00) :''' L'Esclave a préparé la réponse (la signature), mais il ne contrôle pas l'horloge ! Il a besoin que le Maître génère 8 coups d'horloge pour que la réponse puisse voyager sur le fil <code>MISO</code>. Le Maître envoie donc un octet factice (0x00) uniquement pour faire tourner l'horloge. Pendant ce temps, la cible glisse sa réponse sur le fil <code>MISO</code>. C'est pour ça qu'on fait <code>uint8_t sig_n = spi_transfer(0x00);</code> ! Le dernier octet retourné par la cible est la '''donnée recherchée'''.</li></ul> | |||
'''Pourquoi 3 octets de signature et qu'est-ce qu'on récupère ?'''<br> | |||
Chaque microcontrôleur AVR possède une "Signature" unique gravée en usine, composée de 3 octets (situés aux adresses 0, 1 et 2). | |||
<ul><li>'''Octet 0 :''' Identifie le fabricant. Atmel est toujours <code>0x1E</code>.</li> | |||
<li>'''Octet 1 :''' Identifie la famille et la mémoire.</li> | |||
<li>'''Octet 2 :''' Identifie le modèle exact. Par exemple, le AT90S1200 a pour signature 0x1E 0x90 0x01.</li></ul> | |||
[[File : PSE2_ISP_allowed_dev.png|left|750x200px]] | |||
Voici le tableau qui résume ce protocole. On retrouve en ligne les 3 octets qu'on reçoit et en colonne les 4 octets nécéssaires à la récupération de chacun de ces 3 octets. | |||
[[File : PSE2_ISP_device_code.png|right|750x200px]] | |||
<p style="clear: both;" /> | |||
Voici la fonction qui met en place le protocole : | |||
<syntaxhighlight lang="c"> | |||
void ProcessIN(void) | |||
{ | |||
Endpoint_SelectEndpoint(DATA_IN_EPADDR); | |||
if (Endpoint_IsReadWriteAllowed()) | |||
{ | |||
if (commande_PC != 0x01) | |||
Endpoint_Write_8(PINC & 0xC0); | |||
else | |||
{ | |||
// Mode ISP : Le PC a demandé les identifiants | |||
spi_ON(); | |||
// (3.2 datasheet) Enable Memory Access | |||
spi_transfer(0xAC); | |||
spi_transfer(0x53); | |||
spi_transfer(0x00); | |||
spi_transfer(0x00); | |||
// --- Lecture du 1er octet de signature (Adresse 0x00) --- | |||
spi_transfer(0x30); | |||
spi_transfer(0x00); | |||
spi_transfer(0x00); | |||
uint8_t sig_1 = spi_transfer(0x00); // La cible répond pendant cet octet | |||
// --- Lecture du 2ème octet de signature (Adresse 0x01) --- | |||
spi_transfer(0x30); | |||
spi_transfer(0x00); | |||
spi_transfer(0x01); | |||
uint8_t sig_2 = spi_transfer(0x00); | |||
// --- Lecture du 3ème octet de signature (Adresse 0x02) --- | |||
spi_transfer(0x30); | |||
spi_transfer(0x00); | |||
spi_transfer(0x02); | |||
uint8_t sig_3 = spi_transfer(0x00); | |||
spi_OFF(); | |||
// On envoie les 3 octets au PC ! | |||
Endpoint_Write_8(sig_1); | |||
Endpoint_Write_8(sig_2); | |||
Endpoint_Write_8(sig_3); | |||
// On efface la commande pour ne pas spammer la cible au prochain tour | |||
commande_PC = 0; | |||
} | |||
Endpoint_ClearIN(); | |||
} | |||
} | |||
</syntaxhighlight></li> | |||
==== Côté PC (libusb) ==== | |||
La fonction <code>isp_ids.c</code> permet l'envoie d'une commande qui implique la réception des identifiants ISP de l'AVR. Il enverra la commande <code>0x01</code>, puis écoutera le point d'accès IN pour récupérer et afficher les 3 octets de la signature. | |||
===== OUT ===== | |||
On envoie simplement l'octet <code>0x01</code> pour activer le code ISP sur l'avr (côté LUFA). | |||
<syntaxhighlight lang="c"> | |||
// ----OUT---- | |||
unsigned char data_out = 0x01; // octet à envoyer pout l'ISP (choix) | |||
int actual_length; | |||
int r = libusb_interrupt_transfer(dev_handle, EP_OUT, &data_out, 1, &actual_length, 50); | |||
if (r == 0) | |||
{ | |||
printf("[PC] Ordre envoyé : 0x%02X\n", data_out); | |||
} | |||
else | |||
{ | |||
printf("[PC] Erreur OUT : %s\n", libusb_error_name(r)); | |||
} | |||
</syntaxhighlight> | |||
===== IN ===== | |||
Pour récupérer les 3 octets, on doit d'abord ignorer le premier paquet envoyé par les boutons (do/while). Ensuite on imprime les 3 octets reçu et on indique si c'est un AVR atmel qu'on connecte en ISP. | |||
<syntaxhighlight lang="c"> | |||
// ----IN ---- | |||
unsigned char data_in[3] = {0, 0, 0}; | |||
printf("[PC] Attente de la réponse (Purge des anciens paquets)...\n"); | |||
// Boucle pour ignorer les paquets de 1 octet (les boutons) | |||
// On boucle tant que le transfert réussit ET que la taille est de 1 | |||
do | |||
{ | |||
r = libusb_interrupt_transfer(dev_handle, EP_IN, data_in, 3, &actual_length, 100); | |||
} while (r == 0 && actual_length == 1); | |||
// Sortie de boucle : on a soit une erreur, soit notre paquet de 3 octets ! | |||
if (r == 0 && actual_length == 3) | |||
{ | |||
printf("Signature reçue avec succès : 0x%02X 0x%02X 0x%02X\n", data_in[0], data_in[1], data_in[2]); | |||
if (data_in[0] == 0x1E) | |||
{ | |||
printf("-> Fabricant : Atmel reconnu !\n"); | |||
} | |||
} | |||
else | |||
{ | |||
printf("Erreur ou Timeout (reçu %d octets) : %s\n", actual_length, libusb_error_name(r)); | |||
} | |||
</syntaxhighlight> | |||
====Résultats==== | |||
<syntaxhighlight lang="bash"> | |||
sudo ./isp_ids | |||
</syntaxhighlight> | |||
'''Résultat avant soudure de l'ISP :''' | |||
<syntaxhighlight lang="bash"> | |||
[PC] Ordre envoyé : 0x01 | |||
[PC] Attente de la réponse (Purge des anciens paquets)... | |||
Signature reçue avec succès : 0xFF 0xFF 0xFF | |||
Fermeture du programme... | |||
</syntaxhighlight> | |||
'''Résultat après soudure de l'ISP :''' | |||
Test sur une Arduino UNO | |||
<syntaxhighlight lang="bash"> | |||
[PC] Ordre envoyé : 0x01 | |||
[PC] Attente de la réponse (Purge des anciens paquets)... | |||
Signature reçue avec succès : 0x1E 0x95 0x0F | |||
-> Fabricant : Atmel reconnu ! | |||
Fermeture du programme... | |||
</syntaxhighlight> | |||
=== Flash === | |||
L'objectif est de récupérer l'intégralité du contenu de la mémoire flash de la cible. Celle-ci étant volumineuse, le | |||
transfert se fait par blocs de 8 octets (soit 4 mots de 16 bits). | |||
==== Envoyer bloc par bloc sur le IN ==== | |||
===== Modification de la LUFA (main.c) ===== | |||
Du côté de la <code>LUFA</code>, nous modifions le <code>main.c</code> pour gérer un adressage sur 16 bits et utiliser | |||
les commandes de lecture flash définies dans la datasheet (AVR910). | |||
L'adresse de lecture est envoyée par le PC via le point d'accès <code>OUT</code>, puis la carte lit 8 octets en | |||
<code>SPI</code> et les renvoie via le point d'accès <code>IN</code>. | |||
'''Comprendre la lecture de la Flash via ISP :''' | |||
<ul> | |||
Selon le document technique (Table 3-5), la lecture de la mémoire flash se fait mot par mot (un mot = 2 octets). | |||
<li>'''Read Low Byte''' : Commande <code>0x20</code> suivi de l'adresse.</li> | |||
<li>'''Read High Byte''' : Commande <code>0x28</code> suivi de l'adresse.</li> | |||
Le format d'une commande est toujours de 4 octets: | |||
<li>'''Octet 1''' : <code>0x20</code> (Low) ou 0x28 (High).</li> | |||
<li>'''Octets 2 et 3''' : L'adresse du mot (poids fort puis poids faible)</li> | |||
<li>'''Octet 4''' : Donnée fictive (<code>0x00</code>) pour récupérer la réponse de la cible.</li> | |||
[[file : PSE2_flash_reading.png|centre|1000x1000px]] | |||
</ul> | |||
'''CODE (main.c):''' | |||
<ul> | |||
<li>'''Modification de la fonction <code>ProcessOUT()</code> :''' | |||
On prépare la demande du code <code>PC</code>. Une fois que celui-ci aura envoyé <code>0X02</code>, le | |||
programmeur AVR "sait" qu'il va recevoir 2 octets contenant l'adresse désirée par le PC. | |||
<syntaxhighlight lang="c"> | |||
uint16_t flash_address = 0; | |||
void ProcessOUT(void) | |||
{ | |||
Endpoint_SelectEndpoint(DATA_OUT_EPADDR); | |||
if (Endpoint_IsOUTReceived()) | |||
{ | |||
if (Endpoint_IsReadWriteAllowed()) | |||
{ | |||
// On lit l'octet envoyé par le PC | |||
commande_PC = Endpoint_Read_8(); | |||
if (commande_PC == 0x02) // Demande de lecture Flash | |||
{ | |||
flash_address = ((uint16_t)Endpoint_Read_8() << 8); // Poids fort | |||
flash_address |= Endpoint_Read_8(); // Poids faible | |||
} | |||
else if (commande_PC != 0x01) | |||
{ | |||
PORTB = commande_PC; // Gestion des LEDs } } | |||
Endpoint_ClearOUT(); | |||
} | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
</li> | |||
<li>'''Ajout du <code>if</code> dans la fonction <code>ProcessIN()</code> :''' | |||
Comme indiqué précédemment, on va lire la flash à l'adresse demandée par le PC. Comme dans | |||
<code>descriptor.h</code> on a mis <code>#define DATA_SIZE 8</code>(plus lent mais plus intéressant pour | |||
comprendre) et qu'un mot fait 2 octets, alors cela veut dire qu'on peut lire jusqu'à 4 mots à | |||
<code>flash_address + i</code> pour optimiser la bande passante USB (d'ou le i jusqu'à 4 dans le | |||
<code>for</code>). | |||
Ensuite on suit le protocole expliqué au dessus pour la lecture de la flash pour chaque mots lu. | |||
<syntaxhighlight lang="c"> | |||
if (commande_PC == 0x02) | |||
{ | |||
spi_ON(); | |||
// (3.2 datasheet) Enable Memory Access | |||
spi_transfer(0xAC); | |||
spi_transfer(0x53); | |||
spi_transfer(0x00); | |||
spi_transfer(0x00); | |||
for (uint16_t i = 0; i < 4; i++) | |||
{ | |||
uint16_t current_addr = flash_address + i; | |||
// Lecture du Low Byte | |||
spi_transfer(0x20); | |||
spi_transfer(current_addr >> 8); // On décale de 8 les 2 octets donc on a 0x00HIGH = 0xHIGH | |||
spi_transfer(current_addr & 0xFF); // On fait un & à l'adresse avec 0x00FF donc on a 0x00LOW = 0xLOW | |||
uint8_t data_low = spi_transfer(0x00); | |||
// Lecture du High Byte | |||
spi_transfer(0x28); | |||
spi_transfer(current_addr >> 8); | |||
spi_transfer(current_addr & 0xFF); | |||
uint8_t data_hight = spi_transfer(0x00); | |||
// envoie au PC | |||
Endpoint_Write_8(data_low); | |||
Endpoint_Write_8(data_hight); | |||
} | |||
spi_OFF(); | |||
commande_PC = 0; // On a fini d'envoyer le bloc | |||
} | |||
</syntaxhighlight> | |||
</li> | |||
</ul> | |||
===== Récéption PC (flash_read.c)===== | |||
Comme on a programmé notre AVR pour qu'elle récupère 4 mots par adresses données, il faut qu'on lui donne des adresses | |||
indentées de 4. | |||
Par exemple si on veut lire 64 octets de la flash (soit 32 mots), on boucle de cette manière : | |||
<ul> | |||
<li>Demander l'adresse <code>0x0000</code> (la carte renvoie les mots 0, 1, 2, 3).</li> | |||
<li>Demander l'adresse <code>0x0004</code> (la carte renvoie les mots 4, 5, 6, 7).</li> | |||
<li>Demander l'adresse <code>0x0008</code> (la carte renvoie les mots 8, 9, 10, 11).</li> | |||
<li>...</li> | |||
</ul> | |||
<ul> | |||
Le code ci-dessous effectue cette boucle afin de récupérer 64 octets dans la flash cible. | |||
<li>'''flash_read.c :''' | |||
<syntaxhighlight lang="c"> | |||
printf("\n=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===\n\n"); | |||
int r; | |||
int actual_length; | |||
unsigned char data_out[3]; | |||
unsigned char data_in[8]; // Tampon de 8 octets pour recevoir 4 mots | |||
// Boucle pour lire 32 mots (64 octets), par pas de 4 mots | |||
for (uint16_t addr = 0; addr < 32; addr += 4) | |||
{ // Préparation de la commande OUT (0x02 + Addr_High + Addr_Low) | |||
data_out[0] = 0x02; | |||
data_out[1] = (addr >> 8) & 0xFF; // Poids fort de l'adresse | |||
data_out[2] = addr & 0xFF; // Poids faible de l'adresse | |||
// Demande de lecture en OUT | |||
r = libusb_interrupt_transfer(dev_handle, EP_OUT, data_out, 3, &actual_length, 100); | |||
if (r != 0) | |||
{ | |||
printf("Erreur lors de la demande pour l'adresse 0x%04X : %s\n", addr, libusb_error_name(r)); | |||
continue; | |||
} | |||
// Réception IN (avec supression des paquets "boutons" d'1 octet) | |||
do | |||
{ | |||
r = libusb_interrupt_transfer(dev_handle, EP_IN, data_in, 8, &actual_length, 100); | |||
} while (r == 0 && actual_length == 1); // Ignore les paquets de boutons | |||
// Affichage du bloc reçu | |||
if (r == 0 && actual_length == 8) | |||
{ | |||
printf("Adresse 0x%04X : ", addr); | |||
for (int i = 0; i < 8; i += 2) | |||
{ | |||
printf("%04X ", (data_in[i + 1] << 8) | data_in[i]); | |||
} | |||
printf(" \n"); | |||
} | |||
else | |||
{ | |||
printf("Erreur de lecture a l'adresse 0x%04X (reçu %d octets)\n", addr, | |||
actual_length); | |||
} | |||
} | |||
</syntaxhighlight> | |||
</li></ul> | |||
=====RESULTATS===== | |||
<syntaxhighlight lang="bash"> | |||
sudo ./flash_read | |||
</syntaxhighlight> | |||
'''Avant soudure :''' | |||
<syntaxhighlight lang="bash"> | |||
=== LECTURE DE LA FLASH (Test : 64 premiers octets) === | |||
Adresse 0x0000 : FFFF FFFF FFFF FFFF | |||
Adresse 0x0004 : FFFF FFFF FFFF FFFF | |||
Adresse 0x0008 : FFFF FFFF FFFF FFFF | |||
Adresse 0x000C : FFFF FFFF FFFF FFFF | |||
Adresse 0x0010 : FFFF FFFF FFFF FFFF | |||
Adresse 0x0014 : FFFF FFFF FFFF FFFF | |||
Adresse 0x0018 : FFFF FFFF FFFF FFFF | |||
Adresse 0x001C : FFFF FFFF FFFF FFFF | |||
Fermeture du programme... | |||
</syntaxhighlight> | |||
'''Après soudure (UNO):''' | |||
<syntaxhighlight lang="bash"> | |||
=== LECTURE DE LA FLASH (Test : 64 premiers octets) === | |||
Adresse 0x0000 : 940C 005D 940C 0085 | |||
Adresse 0x0004 : 940C 0085 940C 0085 | |||
Adresse 0x0008 : 940C 0085 940C 0085 | |||
Adresse 0x000C : 940C 0085 940C 0085 | |||
Adresse 0x0010 : 940C 0085 940C 0085 | |||
Adresse 0x0014 : 940C 0085 940C 0085 | |||
Adresse 0x0018 : 940C 0085 940C 0085 | |||
Adresse 0x001C : 940C 0085 940C 0085 | |||
Fermeture du programme... | |||
</syntaxhighlight> | |||
==== Mise à jour de la flash (Page Mode) ==== | |||
L'objectif est d'écrire de nouvelles données dans la mémoire Flash de la cible. Étant donné que les cibles modernes comme l'ATmega328P (Arduino) ne supportent plus l'écriture octet par octet (Byte Mode), nous nous appuyons sur la Datasheet de l'ATmega328P (Section Serial Downloading) pour utiliser le mode de programmation par page (Page Mode). | |||
===== Mécanisme d'écriture SPI ===== | |||
[[File: PSE2_flash_writting_page_mode.png|right|600x600px]] | |||
L'écriture dans la Flash nécessite désormais de précharger les données dans un tampon temporaire (Page Buffer) avant de déclencher la gravure physique de la page. Pour simplifier notre programme PC, nous chargeons un seul mot (16 bits) dans le tampon, puis nous forçons immédiatement la gravure. | |||
La session se fait en un seul cycle d'activation de la cible (spi_ON) : | |||
<ul> | |||
<li>'''Activation de la mémoire (Enable Memory Access)''' : Envoi de la séquence <code>0xAC 0x53 0x00 0x00</code> obligatoire pour déverrouiller l'accès.</li> | |||
<li>'''Chargement du Low Byte)''' : Utilisation de la commande <code>0x40</code> suivie de l'adresse et de l'octet faible pour le placer dans le tampon.</li> | |||
<li>'''Chargement du High Byte)''' : Utilisation de la commande <code>0x48</code> suivie de l'adresse et de l'octet fort.</li> | |||
<li>'''Déclenchement du Write Page''' : Utilisation de la commande <code>0x4C</code> suivie de l'adresse pour graver physiquement le tampon dans la Flash.</li> | |||
<li>'''Attente''' : Délai de quelques millisecondes (environ 5 ms) pour laisser le matériel terminer l'écriture avant de relâcher le <code>RESET</code>.</li></ul> | |||
===== Implémentation LUFA (main.c) ===== | |||
Pour pourvoir écrire dans la flash cible il faut effacer la flah. Pour ça on a fait la fonction <code>chip_erase()</code> qui suit le protocole du tableau. | |||
<syntaxhighlight lang="c"> | |||
void chip_erase(void) | |||
{ | |||
spi_ON(); | |||
_delay_ms(5); | |||
// (3.2 datasheet) Enable Memory Access | |||
spi_transfer(0xAC); | |||
spi_transfer(0x53); | |||
spi_transfer(0x00); | |||
spi_transfer(0x00); | |||
// (3.4.3 datasheet) erase | |||
spi_transfer(0xAC); | |||
spi_transfer(0x80); | |||
spi_transfer(0x00); | |||
spi_transfer(0x00); | |||
_delay_ms(5); | |||
spi_OFF(); | |||
} | |||
</syntaxhighlight> | |||
Dans notre point d'accès <code>OUT</code>, nous créons la commande <code>0x03</code>. Le programme PC enverra un paquet | |||
contenant l'adresse cible et les deux octets à écrire. La carte exécute ensuite la séquence <code>SPI</code>. On crée la commande <code>0x04</code> pour erase la flash. | |||
<syntaxhighlight lang="c"> | |||
else if (commande_PC == 0x03) // Ecriture de flash (flash_rw.c) | |||
{ | |||
flash_address = ((uint16_t)Endpoint_Read_8() << 8); // Poids fort | |||
flash_address |= Endpoint_Read_8(); // Poids faible | |||
uint8_t low = Endpoint_Read_8(); | |||
uint8_t high = Endpoint_Read_8(); | |||
spi_ON(); | |||
_delay_ms(5); // Temps de réveil cible | |||
// (3.2 datasheet) Enable Memory Access | |||
spi_transfer(0xAC); | |||
spi_transfer(0x53); | |||
spi_transfer(0x00); | |||
spi_transfer(0x00); | |||
// Low byte | |||
spi_transfer(0x40); | |||
spi_transfer(flash_address >> 8); | |||
spi_transfer(flash_address & 0xFF); | |||
spi_transfer(low); | |||
// High byte | |||
spi_transfer(0x48); | |||
spi_transfer(flash_address >> 8); | |||
spi_transfer(flash_address & 0xFF); | |||
spi_transfer(high); | |||
// Write | |||
spi_transfer(0x4C); | |||
spi_transfer(flash_address >> 8); | |||
spi_transfer(flash_address & 0xFF); | |||
spi_transfer(0x00); | |||
_delay_ms(5); // Attente de la gravure physique | |||
spi_OFF(); | |||
} | |||
else if (commande_PC == 0x04) | |||
{ | |||
chip_erase(); | |||
} | |||
</syntaxhighlight> | |||
===== PC(flash_rw.c) ===== | |||
Pour tester l'écriture, on fait une fonction <code>flash_rw.c</code> qui écrit dans la flash cible puis la lit. | |||
Pour écrire dans la flash cible, on doit d'abord effacer la flash, puis on envoi l’octet <code>0x03</code> (choisit par défaut). Ensuite on | |||
envoi l'adresse ou on veut écrire la donnée; en 2 octets HIGH/LOW, et de même pour la donnée à envoyer. | |||
<syntaxhighlight lang="c"> | |||
int r; | |||
int actual_length; | |||
// ========================================================= | |||
// 0. EFFACEMENT DE LA CIBLE (Chip Erase) | |||
// ========================================================= | |||
printf("\n=== EFFACEMENT DE LA PUCE ===\n"); | |||
unsigned char cmd_erase = 0x04; | |||
r = libusb_interrupt_transfer(dev_handle, EP_OUT, &cmd_erase, 1, &actual_length, 500); | |||
if (r == 0) | |||
{ | |||
printf("Puce effacée avec succès !\n"); | |||
} | |||
else | |||
{ | |||
printf("Erreur lors de l'effacement : %s\n", libusb_error_name(r)); | |||
} | |||
// ========================================================= | |||
// 1. TEST D'ÉCRITURE (Byte Mode) | |||
// ========================================================= | |||
printf("\n=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===\n\n"); | |||
unsigned char data_write[5]; // Paquet de 5 octets pour l'écriture | |||
// On boucle sur les 4 premières adresses de mots (0, 1, 2, 3) | |||
for (uint16_t addr = 0; addr < 5; addr++) | |||
{ | |||
// On invente une donnée factice pour le test (ex: 0xA000, 0xA001...) | |||
uint16_t fake_data = 0xA000 + addr; | |||
// Préparation du paquet selon le protocole LUFA | |||
data_write[0] = 0x03; // Commande d'écriture | |||
data_write[1] = (addr >> 8) & 0xFF; // add HIGH | |||
data_write[2] = addr & 0xFF; // add LOW | |||
data_write[3] = fake_data & 0xFF; // fake LOW | |||
data_write[4] = (fake_data >> 8) & 0xFF; // fake HIGH | |||
// Envoi de l'ordre d'écriture en OUT | |||
r = libusb_interrupt_transfer(dev_handle, EP_OUT, data_write, 5, &actual_length, 100); | |||
if (r == 0) | |||
{ | |||
printf("Ordre d'écriture envoyé -> Adresse: 0x%04X | Donnée: 0x%04X\n", addr, fake_data); | |||
} | |||
else | |||
{ | |||
printf("Erreur d'écriture à l'adresse 0x%04X : %s\n", addr, libusb_error_name(r)); | |||
} | |||
} | |||
</syntaxhighlight> | |||
=====RESULTATS===== | |||
<syntaxhighlight lang="bash"> | |||
sudo ./flash_rw | |||
</syntaxhighlight> | |||
'''Avant soudure :''' | |||
<syntaxhighlight lang="bash"> | |||
=== ECRITURE DE LA FLASH (Test : 4 premiers mots) === | |||
Ordre d'écriture envoyé -> Adresse: 0x0000 | Donnée: 0xA000 | |||
Ordre d'écriture envoyé -> Adresse: 0x0001 | Donnée: 0xA001 | |||
Ordre d'écriture envoyé -> Adresse: 0x0002 | Donnée: 0xA002 | |||
Ordre d'écriture envoyé -> Adresse: 0x0003 | Donnée: 0xA003 | |||
</syntaxhighlight> | |||
<i>RQ : la lecture renvoie quand même que des mots en <code>FFFF</code></i> | |||
'''Après soudure :''' | |||
<syntaxhighlight lang="bash"> | |||
=== EFFACEMENT DE LA PUCE === | |||
Puce effacée avec succès ! | |||
=== ECRITURE DE LA FLASH (Test : 4 premiers mots) === | |||
Ordre d'écriture envoyé -> Adresse: 0x0000 | Donnée: 0xA000 | |||
Ordre d'écriture envoyé -> Adresse: 0x0001 | Donnée: 0xA001 | |||
Ordre d'écriture envoyé -> Adresse: 0x0002 | Donnée: 0xA002 | |||
Ordre d'écriture envoyé -> Adresse: 0x0003 | Donnée: 0xA003 | |||
Ordre d'écriture envoyé -> Adresse: 0x0004 | Donnée: 0xA004 | |||
=== LECTURE DE LA FLASH (Test : 64 premiers octets) === | |||
Adresse 0x0000 : A000 A001 A002 A003 | |||
Adresse 0x0004 : A004 FFFF FFFF FFFF | |||
Adresse 0x0008 : FFFF FFFF FFFF FFFF | |||
Adresse 0x000C : FFFF FFFF FFFF FFFF | |||
Adresse 0x0010 : FFFF FFFF FFFF FFFF | |||
Adresse 0x0014 : FFFF FFFF FFFF FFFF | |||
Adresse 0x0018 : FFFF FFFF FFFF FFFF | |||
Adresse 0x001C : FFFF FFFF FFFF FFFF | |||
Fermeture du programme... | |||
</syntaxhighlight> | |||
=== Assemblage(résultats) === | |||
L'objectif est d'utiliser les fonctions test et de les modifier afin de d'avoir un seul programme <code>main</code> qui contrôle tout. On l'appel <code>prog.c</code><br> | |||
La première chose est de retirer la gestion USB dans <code>flash_read.c</code> et <code>flash_read.c</code> et de la séparer dans une fonction <code>dev_handler.c</code>.<br> | |||
On change la fonction <code>flash_rw.c</code> en <code>flash_write.c</code> pour seulement écrire avec.<br> | |||
Enfin on compile tout avec un <code>Makefile</code> et on exécute le code avec une commande intelligente. | |||
Ici on flashera un code basique <code>blink.c</code> qui fera clignoter la LED PB5 de l'arduino UNO. | |||
====Comment flasher un programme C ?==== | |||
Pour flasher un fichier C il faut écrire le <code>.hex</code> associé. La compilation d'un .hex est détaillée dans "Programateur/libusb/code_a_televerser/Makefile".<br> | |||
Un fichier .hex est composé de lignes d'octets qui ont une fonction bien précise. <br> | |||
On prend par exemple la première ligne dans <code>blink.hex</code> : <code>:100000000C9434000C943E000C943E000C943E0082</code> | |||
<ul> | |||
<li><code>:</code> : indique le début d'un ligne</li> | |||
<li><code>10</code> : Nombre d'octets de données sur cette ligne (Ici <code>0x10</code> = <code>16</code> octets).</li> | |||
<li><code>0000</code> : L'adresse de départ en Flash pour cette ligne.</li> | |||
<li><code>00</code> : Type de ligne (<code>00</code> = données à flasher, <code>01</code> = fin du fichier).</li> | |||
<li><code>0C9434000C94...</code> : Les données du programme</li> | |||
</ul> | |||
====flash_write.c==== | |||
On applique cette logique dans notre nouvelle fonction.<br> | |||
Pour séparer ligne par ligne on utilise la fonction <code>fgets</code>.<br> | |||
Pour sectionner ces ligne on utilise la fonction <code>sscanf</code>. (Ces 2 fonctions on été vues en Structure de Données Avancées).<br> | |||
Le reste est détaillés dans les commentaires du code. | |||
<syntaxhighlight lang="c"> | |||
char line[256]; | |||
unsigned int length, addr_in, type_in; | |||
while (fgets(line, sizeof(line), file) != NULL) | |||
{ | |||
sscanf(line, ":%2x%4x%2x", &length, &addr_in, &type_in); | |||
uint16_t type = (uint16_t)type_in; | |||
if (type == 0x01) | |||
break; | |||
if (type == 0x00) | |||
{ | |||
// Du côté du fichier .HEX l'adresse est codé octet par octet. Contrairement à l'AVR qui stock des adresses de mots. Donc pour avoir l'adresse du mot(2 octets) il suffit de diviser l'adresse in par 2. | |||
uint16_t current_word_addr = addr_in / 2; | |||
for (unsigned int i = 0; i < length; i += 2) | |||
{ | |||
unsigned int word_data; | |||
// On décale le point de départ du sscanf de 9 (début des octets du code) puis on l'avance d'un facteur de 2 par rapport à i. Comme i aussi avance d'un facteur de 2 alors le tout avance d'un facteur de 4. Comme on lit 4 valeurs ascii à la fois (en effet 1 octet = 2 valeurs ascii => 2 octets = 1 mot = 4 valeurs ascii) le décallage est respécté. | |||
sscanf(line + 9 + (i * 2), "%4x", &word_data); | |||
uint16_t data = ((word_data & 0xFF) << 8) | (word_data >> 8); | |||
// Préparation du paquet selon le protocole LUFA | |||
data_write[0] = 0x03; // Commande d'écriture | |||
data_write[1] = (current_word_addr >> 8) & 0xFF; // add HIGH | |||
data_write[2] = current_word_addr & 0xFF; // add LOW | |||
data_write[3] = data & 0xFF; // LOW | |||
data_write[4] = (data >> 8) & 0xFF; // HIGH | |||
// Envoi de l'ordre d'écriture en OUT | |||
... | |||
</syntaxhighlight> | |||
====Programme principale (prog.c)==== | |||
Ce programme relie toutes les fonctions vues avant. Il récupère d'abord les identifiants de la cible via le protocole ISP. Ensuite il laisse le choix entre lire ou écrire la flash cible.<br> | |||
=====Structure cible===== | |||
Tout d'abord pour bien différencier la cible, on cré une structure cible: | |||
<syntaxhighlight lang="c"> | |||
typedef struct | |||
{ | |||
unsigned char signature[3]; | |||
const char *nom; | |||
uint32_t taille_flash; // Taille totale en octets | |||
uint16_t taille_page; // Taille d'une page en octets | |||
} CibleAVR; | |||
</syntaxhighlight> | |||
=====Base de Donnée===== | |||
On a créé une base de donnée pour avoir les informations exactes des puces si elles sont reconnues via le protocole ISP. | |||
<syntaxhighlight lang="c"> | |||
const CibleAVR base_de_donnees[] = { | |||
{{0x1E, 0x95, 0x0F}, "ATmega328P", 32768, 128}, | |||
{{0x1E, 0x94, 0x89}, "ATmega16U2", 16384, 128}, | |||
{{0x1E, 0x93, 0x0B}, "ATtiny85", 8192, 64}}; | |||
</syntaxhighlight> | |||
On créé ensuite une fonction qui cherche la puce dans la BDD : | |||
<syntaxhighlight lang="c"> | |||
#define NB_CIBLES (sizeof(base_de_donnees) / sizeof(CibleAVR)) | |||
const CibleAVR *recherche_cible(unsigned char sig[3]) | |||
{ | |||
for (long unsigned int i = 0; i < NB_CIBLES; i++) | |||
{ | |||
if (sig[0] == base_de_donnees[i].signature[0] && | |||
sig[1] == base_de_donnees[i].signature[1] && | |||
sig[2] == base_de_donnees[i].signature[2]) | |||
{ | |||
return &base_de_donnees[i]; | |||
} | |||
} | |||
return NULL; // Puce inconnue | |||
} | |||
</syntaxhighlight> | |||
=====main===== | |||
On utilise des variables dans notre fonction main() pour pouvoir rajouter des fonctions : si on écrit <code>-read</code> ou <code>-write</code> après le nom de notre programme on peut soit lire ou écrire la flash cible. | |||
<syntaxhighlight lang="c"> | |||
int main(int argc, char *argv[]) | |||
{ | |||
// Initialisation unique | |||
libusb_device_handle *dev_handle = init_usb(); | |||
if (dev_handle == NULL) | |||
return 1; | |||
if (reclamer_interface(dev_handle) < 0) | |||
return 1; | |||
// On récupère les infos de la cible si elle est connue. | |||
unsigned char sig[3] = {0, 0, 0}; | |||
isp_ids(dev_handle, sig); | |||
const CibleAVR *cible = recherche_cible(sig); | |||
if (cible == NULL) | |||
printf("\033[31mLa cible est inconnue.\033[0m\n"); | |||
else | |||
printf("La cible est une %s, elle a une flash de \033[33m%d\033[0m octets et des pages de \033[33m%d\033[0m octets.\n", cible->nom, cible->taille_flash, cible->taille_page); | |||
// commandes utilisateur | |||
if (argc == 1) | |||
{ | |||
printf("\033[31mErreur : Vous devez préciser une action.\033[0m\n"); | |||
printf("Usage : make read OU make write <fichier.hex>\n"); | |||
} | |||
else if (strcmp(argv[1], "-read") == 0) | |||
{ | |||
printf("\nLancement de la lecture...\n"); | |||
flash_read(dev_handle, cible->taille_flash); | |||
} | |||
else if (strcmp(argv[1], "-write") == 0) | |||
{ | |||
if (argc < 3) | |||
{ | |||
printf("\033[31mErreur : Il manque le nom du fichier .hex !\033[0m\n"); | |||
} | |||
else | |||
{ | |||
FILE *file = fopen(argv[2], "r"); | |||
if (file == NULL) | |||
printf("\033[31mErreur : Impossible d'ouvrir le fichier %s.\033[0m\n", argv[2]); | |||
else | |||
{ | |||
printf("\nOuverture du fichier %s...\n", argv[2]); | |||
flash_write(dev_handle, file); | |||
printf("\033[32mÉcriture terminée avec succès !\033[0m\n"); | |||
fclose(file); | |||
} | |||
} | |||
} | |||
// Fermeture unique et propre | |||
printf("\nFermeture globale du programme...\n"); | |||
libusb_release_interface(dev_handle, 0); | |||
libusb_close(dev_handle); | |||
libusb_exit(NULL); | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
=====RESULTATS===== | |||
'''Compilation dans libusb/''' | |||
<syntaxhighlight lang="bash"> | |||
sudo make clean && make | |||
</syntaxhighlight> | |||
'''Compilation dans libusb/code_a_televerser/''' | |||
<syntaxhighlight lang="bash"> | |||
make clean && make | |||
</syntaxhighlight> | |||
'''Execution du code''' | |||
<ul> | |||
<li> écriture </li> | |||
<syntaxhighlight lang="bash"> | |||
sudo ./build/prog -write blink.hex | |||
</syntaxhighlight> | |||
[[File : PSE2_write.mp4]] | |||
<li> lecture </li> | |||
On lit bien ce qu'on a écrit juste avant. | |||
<syntaxhighlight lang="bash"> | |||
sudo ./build/prog -read | |||
</syntaxhighlight> | |||
[[File : PSE2_read.mp4]] | |||
</ul> | |||
== Bilan == | |||
<h3>03/03 : soudure</h3> | |||
Soudure presque terminée. LEDs et résistances associées manquantes, 1 bouton poussoir traversant manquant. | |||
<h3>10/03 : Problème ISP -> connexion ISP</h3> | |||
<ul> | |||
<li>'''Problème''' : le programmateur n'est pas détecté en USB. Un erreur est détectée ce qui montre qu'il y a une connexion, mais n'apparaît pas dans <code>lsusb</code>. </li> | |||
<li>Programmable : La connexion en ISP en revanche fonctionne. On pourra donc programmer les leds et les boutons avec une UNO. Voir dans le futur si on peut fixer le problème de l'USB pour rendre le programmateur fonctionnel.</li> | |||
<li>ajout d'un premier <code>blink_led.c</code> (brouillon) au git</li> | |||
<li>TESTS : Le code <code>blink</code> fonctionne mais pas le code <code>boutons</code> -> faut contact / pbs de soudure sur les pattes</li> | |||
</ul> | |||
<h3>11/03 : fonctions test</h3> | |||
LEDS et boutons fonctionnent -> attente de réparation de notre USB par le prof ou fin de projet pour le programatteur.<br> | |||
<h3>08/04 : impression de la nouvelle carte</h3> | |||
Nouvelle carte imprimée et détection de notre carte via <code>lsusb</code>.<br> | |||
[[File:BE2_lsusb.png|left|sans_cadre|712x712px]][[File:BE2_lsusb_details.png|right|sans_cadre|713x792px]] | |||
<p style="clear: both;" /> | |||
<br> | |||
<h3>17/04 : minicom</h3> | |||
Programmation <code>minicom</code>.<br> | |||
<h3>18/04 : codes test vérifiables avant soudure</h3> | |||
Programmation <code>InOut</code> test et ISP.<br> | |||
<h3>(19-26)/04 : suite codes test vérifiables avant soudure</h3> | |||
Programmation <code>flash</code>,<code>ISP</code>, fonctions test OK avant soudure.<br> | |||
<h3>28/04 : Soudure de la nouvelle carte</h3> | |||
Soudure des leds et du composants ISP de la carte. Modification des fonctions test pour faire fonctionner le code sur une atemaga328p(UNO). <br> | |||
<h3>02/05 : finition du projet</h3> | |||
Finition du projet : le programmeur est capable de transférer un code C sur une AVR (3 AVR sont ajoutées dans la BDD du programmeur).<br> | |||
Un code simple <code>blink.c</code> est testé et fonctionnel sur une arduino UNO (atemage328p). <br> | |||
Tout les détails du projets sont indiqués dans la section "Programmation" détaillés étape par étape.<br> | |||
Le code/résultat final est dans la sous section "Assemblage" de la section "Programmation".<br><br> | |||
Sur le '''git''', on retrouve le contenu nécessaire dans <code>Programmeur/</code>. L'utilisation du code est décris dans le <code>README</code>. | |||
= Premier système embarqué = | |||
== Archive GIT == | |||
Notre archive GIT pour le projet KiCAD et pour les programmes : https://gitea.plil.fr/mterrier/2025_PSE_B2_mterrier_jramesh | |||
Structure avec matériel (y compris production - gerber, bill of materials) / logiciel / documentation (e.g. documentation technique). | |||
== Description du système embarqué == | |||
Nous avons décider de réaliser '''BMO''', un système comportant un '''écran''' affichant un visage minimaliste, munis d'un '''détécteur de mouvement''' et d'un '''buzzer'''. | |||
Lorsque notre main s'approche de détécteur, le visage plisse les yeux (ou devient triste). Lorsque notre main s'écarte de celui ci, le visage réouvre les yeux (ou devient heureux). | |||
Le système sera alimenté par une '''batterie''', ou une alimentation '''USB''' en 5V. | |||
Afin de valider l'utilisation du port USB, nous connecterons un ordinateur au système via USB et éffectuerons un transfert de données sonores (divers sons) qu'on ira stocker dans la flash. | |||
On a ajouté 2 servos moteurs pour faire office de bras. On pourra s'en servir pour proposer plus d'interactions. | |||
== Historique des Scéances == | |||
<h3>17/02 : initialisation</h3> | |||
<ul> | |||
<li>Modification initiale du wikicode </li> | |||
<li>Brainstorming pour se décider sur un projet </li> | |||
</ul> | |||
<h3>03/03 : Début de la schématique </h3> | |||
<ul> | |||
<li>Encodeur: voir si on peut en utiliser un pour régler le son/luminosité écran.</li> | |||
<li>Connecteur ISP (le même que le programatteur) </li> | |||
<li>USB-A, condensateurs de découplage, condensateur VUSB. </li> | |||
<li>Chargeur Lipo (le même que celui du wiki donc à modifier selon notre schéma). </li> | |||
<li>haut parleur : 8 entrée DAC.</li> | |||
<li>Ecran : 11 entrée (8 affichage et 3 gestion).</li> | |||
<li>Cerveaux moteur optionnels si on a pas assez de ports</li> | |||
<li>Voir comment connecter la flash.</li> | |||
</ul> | |||
<h3>10/03 : V1 schématique</h3> | |||
<ul> | |||
Push de la V1 sur le git. | |||
</ul> | |||
<h3>24/03 : corrections + modification chargeur LIPO</h3> | |||
<ul> | |||
<li>Correction de la V1, et ajout des connecteurs pour le chargement de la batterie/ choix de l'alimentation / connecteur batterie</li> | |||
<li>'''Routage''' : début, du placement des composants.</li> | |||
'''A FAIRE''': | |||
<li>Demander les composants physique tel que le '''buzzer''' et le '''cerveau moteur'''</li> | |||
<li>On a enlevé des pins sur l'AVR qui étaient pris inutilement par la batterie LIPO => voir si on ne peut pas rajouter un cerveau moteur et des boutons à la place.</li> | |||
<li>'''Tension''' : Analyser les composants qui nécéssitent 5V de tension pour ajouter un '''booster de tension''' pour garder le 5V en batterie (3,3V). Inversement mettre un régulateur de tension sur les composants fonctionnant en 3,3V, lors de l'alimentation en 5v. '''Convertisseur''' : TPS61033-Q1</li> | |||
</ul> | |||
<h3>29/03 : push de la V2 de la schématique</h3> | |||
<ul> | |||
<li>Ajout du booster de tension TPS61033-Q1 pour '''l'écran''', le '''LM386''', le '''capteur ultrason''' et les '''2 cervosmoteur'''.<li> | |||
<li>Ajout d'un régulateur de tension pour la '''flash'''.<li> | |||
<li>Ajout de la section "Détail schématique" et première explication du boost et régulateur de tension.<li> | |||
'''A FAIRE''': | |||
<li> Demander les composants physique disponibles : '''buzzer''' et les '''cerveaux moteur'''</li> | |||
<li> Demander une vérification de la V2 pour avancer sur le '''routage'''</li> | |||
</ul> | |||
<h3>31/03 : Correction de la V2</h3> | |||
<ul> | |||
<li>Correction de la V2, changement de '''booster''' et du '''regulateur''' pour simplifier le comosant et la commande sur Farnell.</li> | |||
'''A FAIRE''' | |||
<li>Modifier la section "détail de la schématique" en conséquence et revoir le dimsensionnement des composants.</li> | |||
</ul> | |||
<h3>(04-05)/04 : V1 Routage</h3> | |||
<ul> | |||
<li>Ajout du shifter pour la flash.</li> | |||
<li>V1 routage : Alimentation en 0.8mm, pas d'angles droits, DRC au maximum, annotation avec du texte en silkscreen.</li> | |||
</ul> | |||
<h3>12/05 : Premières soudures</h3> | |||
<ul> | |||
<li>Soudures test du uP : ISP OK</li> | |||
<li>TODO : Faut contact sur l'USB. Mais carte détéctée en DFU donc pas de soucis.</li> | |||
</ul> | |||
<h3>13/05 : Premières soudures</h3> | |||
<ul> | |||
<li>Le problème du DFU ne vient pas de l'USB, mais du condensateur utilisé sur le port UCAP du uP.</li> | |||
<li>Problème : Avec les 3 commandes de DFU classiques, le programme ne voulait pas se téléverser. Donc j'ai utilisé <code>--force</code> sur le <code>erase</code>. Malheureusement cela a écrasé le code DFU sur la carte. | |||
<br>=> TODO : <ul><li>remettre le code DFU sur l'atmega</li> | |||
<li>commencer à préparer le code pour l'écran pour le tester à la prochaine séance.</li></ul></li> | |||
</ul> | |||
== Carte électronique == | |||
Carte réalisée en utilisant le logiciel <code>KiCAD</code> : [[File:2025-PSE-B2-systeme.zip|ma carte électronique]]. | |||
[[File:2025_PSE-B2-systeme-schema.pdf|thumb|left|710px|Schéma électronique]] | |||
[[File:2025_PSE-02-systeme-PCB.png|thumb|right|710px|Routage]] | |||
<p style="clear: both;" /> | |||
[[File:2025_PSE-B2-systeme-PCB-3D-front.png|thumb|left|710px|3d front]] | |||
[[File:2025_PSE-B2-systeme-PCB-3D-back.png|thumb|right|710px|3d back]] | |||
<p style="clear: both;" /> | |||
[[File:2025_PSE-02-systeme-carte-front.jpg|thumb|left|710px|Soudure front]] | |||
[[File:2025_PSE-02-systeme-carte-back.jpg|thumb|right|710px|Soudure back]] | |||
<p style="clear: both;" /> | |||
Vidéo très courte et en basse résolution de la carte en fonctionnement : | |||
[[Média:2025-PSE-02-systeme-video.mp4]] | |||
== Détail de la schématique == | |||
<h3>Booster de tension</h3> | |||
<ul> | |||
Pour alimenter la logique (écran LCD, capteur ultra son, Haut parleur) et surtout les servomoteurs depuis la batterie de 3.3V, le choix s'est porté sur le '''régulateur boost TPS61033-Q1'''. La broche FB est reliée directement à l'entrée (VIN) pour fixer la tension de sortie à 5.0V en interne, rendant inutile l'usage d'un pont diviseur. La broche EN est maintenue à VIN pour un fonctionnement continu, tandis que la broche MODE est à la masse (GND) pour activer le mode Auto PFM. Ce mode garantit un excellent rendement énergétique à faible charge, ce qui est crucial pour économiser la batterie lorsque les moteurs sont à l'arrêt. <br> | |||
Pour gérer la décharge lors de l'extinction, une résistance de 1kΩ (Rdummy) est placée sur la broche PG. Cela tire seulement 5mA (bien en deçà de la limite de 50mA imposée par le transistor à drain ouvert) et assure une chute de tension propre et rapide en un quart de seconde.<br> | |||
Côté puissance, les servomoteurs SG90 imposent des pics de courant importants (estimés à 1.5A au total au démarrage). Pour éviter les chutes de tension critiques (brownouts), les capacités de filtrage ont été dimensionnées avec une marge de sécurité : 22µF en entrée et 47µF en sortie pour compenser la perte de capacité sous tension continue. Le cœur du convertisseur est une inductance de 0.47µH. En considérant un rendement (η), soit environ 40.6%. Le courant continu moyen demandé à la bobine se calcule via <math>I_{L(DC)} = \frac{V_{OUT} \times I_{OUT}}{V_{IN} \times \eta}</math>, soit environ 2.53A. En y ajoutant la moitié de l'ondulation crête-à-crête <math>\Delta I_{L(P-P)} = \frac{V_{IN} \times D}{L \times f_{SW}}</math>, le courant de crête absolu atteint <math>I_{L(P)} = I_{L(DC)} + \frac{\Delta I_{L(P-P)}}{2}</math>, soit 3.12A. L'inductance choisie doit donc impérativement présenter un courant de saturation supérieur à 3.5A pour garantir la stabilité du système en pleine charge. C'est pour cela qu'on a choisi comme conseillé dans la datasheet l'inductance '''XGL4020-471MEC''' qui peut avoir un courant de saturation allant jusqu'à 6,1A. | |||
</ul> | |||
<h3>Régulateur de tension</h3> | |||
<ul> | |||
<ul> | |||
Pour alimenter la mémoire flash AT45DB641E, qui requiert une tension de fonctionnement < 3,6V, le choix s'est porté sur le '''convertisseur LTC3531-3.3'''. Contrairement à un régulateur linéaire (LDO) qui dissiperait l'excédent de tension sous forme de chaleur, ce composant est un régulateur de type Buck-Boost synchrone. Cette topologie permet de maintenir une sortie de 3,3V parfaitement stable, que la tension d'entrée provienne du 5V ou directement de la batterie. <br> | |||
Le dimensionnement des composants périphériques s'appuie sur les recommandations strictes du constructeur. L'inductance de puissance a été fixée à une valeur standard de 10 µH. Cette valeur est optimisée par le fabricant pour limiter l'ondulation du courant et maximiser le rendement énergétique à faible charge (la mémoire flash consommant au maximum ~25 mA lors des cycles d'écriture/effacement). <br> | |||
En entrée, un condensateur C<sub>IN</sub> = 4.7 µF est chargé d'absorber le bruit haute fréquence et les appels de courant transitoires liés au hachage interne du régulateur. En sortie, un condensateur C<sub>OUT</sub> = 10 µF est implanté au plus près de la broche VCC de la mémoire. Le respect de ces valeurs (L = 10 µH, C<sub>IN</sub> = 4.7 µF, C<sub>OUT</sub> = 10 µF) garantit que les appels de courant soudains de la puce SPI ne provoqueront aucune chute de tension hors des tolérances logiques. | |||
</ul> | |||
</ul> | |||
Version actuelle datée du 13 mai 2026 à 17:25
Programmation des systèmes embarqués
Programmeur.
Carte électronique
Carte réalisée en utilisant le logiciel KiCAD : Fichier:2025-PSE-2-Prog.zip.
Vidéo très courte et en basse résolution de la carte en fonctionnement :
Programmation
Les tests finaux on été réalisé sur une arduino UNO
Teste de la carte
Pour tester notre carte on a fait des codes simples.
led_blink
// Clignote les 3 LEDS
#include <avr/io.h>
#include <util/delay.h>
#define BLINK_DELAY 50 // en milli secondes
// PB 5, 6, 7 correspond respectivement aux leds 1 , 2 , 3
int main()
{
// configurer les led en sortie :
DDRB |= (1 << 5); // PORTB 5e bit pour la LED1
DDRB |= (1 << 6); // PORTB 6e bit pour la LED2
DDRB |= (1 << 7); // PORTB 7e bit pour la LED3
for (int i = 0; i < 50; i++)
{
// led on
PORTB |= (1 << 5); // OR
PORTB |= (1 << 6);
PORTB |= (1 << 7);
_delay_ms(BLINK_DELAY);
// led off
PORTB &= ~(1 << 5); // AND + NOT
PORTB &= ~(1 << 6);
PORTB &= ~(1 << 7);
_delay_ms(BLINK_DELAY);
}
return 0;
}
boutons
#include <avr/io.h>
#include <util/delay.h>
#define b7 0b10000000
#define b6 0b01000000
#define L5 0b11011111
#define L6 0b10111111
#define L7 0b01111111
void config()
{
DDRB |= (1 << 5);
DDRB |= (1 << 6);
DDRB |= (1 << 7);
DDRC &= 0x00;
// Activation des résistances de Pull-Up internes sur le port C
PORTC |= (b6 | b7);
}
int lire_bouton(int bouton)
{
return (PINC & bouton) == 0;
}
void ecrire_LED(int LED, int etat)
{
switch (etat)
{
case 0:
PORTB &= LED; // Applique le masque avec le 0 pour éteindre
break;
case 1:
PORTB |= ~LED; // Inverse le masque pour avoir un 1 et allumer
break;
}
}
int main()
{
config();
while (1)
{
if (lire_bouton(b6) && lire_bouton(b7))
{
ecrire_LED(L7, 1);
ecrire_LED(L6, 0);
ecrire_LED(L5, 0);
}
else if (lire_bouton(b7))
{
ecrire_LED(L5, 1);
}
else if (lire_bouton((b6)))
{
ecrire_LED(L6, 1);
}
else
{
// On éteint tout si aucun bouton n'est pressé
ecrire_LED(L6, 0);
ecrire_LED(L5, 0);
ecrire_LED(L7, 0);
}
}
return 0;
}
LUFA
Pour injecter un nouveau code sur la carte(être sudo):
dfu-programmer atmega16u2 erase --force
dfu-programmer atmega16u2 flash --suppress-bootloader-mem file.hex
dfu-programmer atmega16u2 reset
Pour injecter le même code mais modifié sur la carte(être sudo):
dfu-programmer atmega16u2 erase
dfu-programmer atmega16u2 flash file.hex
dfu-programmer atmega16u2 reset
Connexion avec minicom
On a récupéré le dossier LUFA depuis le wiki du cours. Puis on a copié le fichier VirtualSerial situé dans lufa-LUFA-210130-NSI/Demos/Device/ClassDriver/VirtualSerial.
L'objectif ici est de communiquer grâce à la LUFA en USB sur minicom.
Dans VirtualSerial.c on modifie la fonction CheckJoystickMovement(void) par la fonction :
#define b7 0b10000000
#define b6 0b01000000
int Button_Status(int boutton)
{
return (PINC & boutton) == 0;
}
void CheckButtons(void)
{
char *ReportString = NULL;
static bool ActionSent = false;
if (Button_Status(b6) && Button_Status(b7))
{
ReportString = "both pressed\r\n";
_delay_ms(250);
ActionSent = false;
}
else if (Button_Status(b6))
{
ReportString = "b6 pressed\r\n";
_delay_ms(250);
ActionSent = false;
}
else if (Button_Status(b7))
{
ReportString = "b7 pressed\r\n";
_delay_ms(250);
ActionSent = false;
}
if ((ReportString != NULL) && (ActionSent == false))
{
ActionSent = true;
/* Write the string to the virtual COM port via the created character stream */
fputs(ReportString, &USBSerialStream);
/* Alternatively, without the stream: */
// CDC_Device_SendString(&VirtualSerial_CDC_Interface, ReportString);
}
}
On change VirtualSerial.h en conséquence.
On modifie la Makefile adapté à notre µP :
MCU = atmega16u2
ARCH = AVR8
BOARD = NONE
F_CPU = 16000000
F_USB = $(F_CPU)
OPTIMIZATION = s
TARGET = VirtualSerial
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS)
LUFA_PATH = ../../LUFA
CC_FLAGS = -DUSE_LUFA_CONFIG_HEADER -IConfig/
LD_FLAGS =
On compile le programme :
make
On injecte le code sur la carte:
sudo dfu-programmer atmega16u2 erase
sudo dfu-programmer atmega16u2 flash
sudo dfu-programmer atmega16u2 reset
Erasing flash... Success
Checking memory from 0x0 to 0x2FFF... Empty.
Checking memory from 0x0 to 0xEFF... Empty.
0% 100% Programming 0xF00 bytes...
[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] Success
0% 100% Reading 0x3000 bytes...
[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] Success
Validating... Success
0xF00 bytes written into 0x3000 bytes memory (31.25%).
et on observe bien un changement de nom de notre carte :
lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0408:a061 Quanta Computer, Inc. HD User Facing
Bus 001 Device 003: ID 8087:0026 Intel Corp. AX201 Bluetooth
Bus 001 Device 005: ID 046d:0aba Logitech, Inc. PRO X Wireless Gaming Headset
Bus 001 Device 012: ID 046d:c08b Logitech, Inc. G502 SE HERO Gaming Mouse
Bus 001 Device 016: ID 03eb:2044 Atmel Corp. LUFA CDC Demo Appli
Ensuite on configure minicom via : minicom -os
configuration minicom :
- "Serial port setup" ->
ENTRER A->/dev/ttyACM0->ENTRERE->C->ENTRERF->ENTRER- "Save setup as dfl" ->
ENTRER EchapeouExit
Puis on lance minicom via (être en sudo)
minicom
Pour retéléverser un programme sur la carte, il faut la repasser en DFU. Pour cela il suffit d'appuyer sur le bouton RESET
Resultat
Gestion personnalisée des Endpoints
Configuration IN et OUT
L'objectif est de s'affranchir de la classe CDC pour gérer directement les points d'accès (Endpoints). On définit une interface avec un point d'accès OUT (pour contrôler les LEDs depuis le PC) et un point d'accès IN (pour envoyer l'état de la carte au PC).
On modifie Descriptors.h pour définir les adresses :
/* Adresses des points d'accès */
#define LED_OUT_EPADDR (ENDPOINT_DIR_OUT | 1) // Endpoint 1 OUT
#define DATA_IN_EPADDR (ENDPOINT_DIR_IN | 2) // Endpoint 2 IN
/* Taille des paquets*/
#define LED_EPSIZE 8
Dans main.c, on remplace la gestion CDC par la configuration manuelle des points d'accès lors de l'énumération :
void EVENT_USB_Device_ConfigurationChanged(void)
{
// Configuration du point d'accès pour recevoir les ordres (LEDs)
Endpoint_ConfigureEndpoint(LED_OUT_EPADDR, EP_TYPE_INTERRUPT, LED_EPSIZE, 1);
// Configuration du point d'accès pour envoyer les données (Boutons/ISP)
Endpoint_ConfigureEndpoint(DATA_IN_EPADDR, EP_TYPE_INTERRUPT, LED_EPSIZE, 1);
}
Traitement des données OUT
Pour piloter les LEDs (même si elles ne sont pas encore soudées, le code cible le PORTB), on crée une fonction de traitement:
void ProcessLEDControl(void)
{
Endpoint_SelectEndpoint(LED_OUT_EPADDR);
if (Endpoint_IsOUTReceived())
{
if (Endpoint_IsReadWriteAllowed())
{
// On lit l'octet envoyé par le PC
uint8_t LEDMask = Endpoint_Read_8();
// On l'applique aux LEDs (PORTB)
PORTB = LEDMask;
}
Endpoint_ClearOUT();
}Atmel-0943-In-System-Programming_ApplicationNote_AVR910
}
Traitement des données IN
On récupère l'état des boutons dans le PINC :
void ButtonsStatus(void)
{
Endpoint_SelectEndpoint(DATA_IN_EPADDR);
if (Endpoint_IsReadWriteAllowed())
{
Endpoint_Write_8(PINC & 0xC0); //pour activer les résistances de pull up sur b6 et b7.
Endpoint_ClearIN();
}
}
Modification du Makefile
On retire les sources de la classe CDC (LUFA_SRC_USBCLASS) car nous utilisons désormais les fonctions "Low Level" de la LUFA.
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB)
Programme PC avec libusb
Maintenant que la carte est configurée avec des Endpoints bruts (IN et OUT), nous devons écrire un programme C fonctionnant sous Linux pour communiquer avec elle. L'outil standard pour cela est la bibliothèque libusb-1.0.
Code de test IN/OUT (main_pc.c)
Ce programme a pour but de valider la liaison USB bas niveau. Il va :
Ouvrir la communication avec le périphérique (VID/PID).
Réclamer l'interface.
Envoyer un ordre OUT pour allumer les broches des LEDs (PORTB).
Lire un état IN pour récupérer l'état des boutons (PORTC).
Ouvrir la communication avec le périphérique
libusb_device_handle *init_usb(void)
{
libusb_device_handle *dev_handle = NULL;
if (libusb_init(NULL) < 0)
{
printf("Erreur d'initialisation de libusb\n");
return NULL;
}
dev_handle = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);
if (dev_handle == NULL)
{
printf("Impossible d'ouvrir le périphérique (03EB:2044). Avez-vous utilisé sudo ?\n");
libusb_exit(NULL);
return NULL;
}
if (libusb_kernel_driver_active(dev_handle, 0) == 1)
{
libusb_detach_kernel_driver(dev_handle, 0);
}
return dev_handle;
}
Réclamer l'interface
int reclamer_interface(libusb_device_handle *dev_handle)
{
int r = libusb_claim_interface(dev_handle, 0);
if (r < 0)
{
printf("Erreur de réclamation de l'interface : %s\n", libusb_error_name(r));
libusb_close(dev_handle);
libusb_exit(NULL);
return -1;
}
return 0;
}
Configuration LED en OUT
On veut allumer les LEDS avec les touches du clavier en LIVE. Pour cela on utilise la bibliothèque termios.h utilisé dans la fonction kbhit().
printf("\n=== MODE LIVE ACTIVÉ ===\n");
printf("Touches :\n");
printf(" [A] : Basculer LED 1 (PB5)\n");
printf(" [Z] : Basculer LED 2 (PB6)\n");
printf(" [E] : Basculer LED 3 (PB7)\n");
printf(" [Q] : Quitter\n");
printf("========================\n\n");
unsigned char data_out = 0x00;
unsigned char last_data_in = 0xFF; // Pour mémoriser l'ancien état des boutons
int actual_length;
int running = 1;
while (running)
{
if (kbhit())
{
char c = getchar();
int send_update = 0;
switch (c)
{
case 'a':
data_out ^= (1 << 5);
send_update = 1;
break; // Bascule PB5
case 'z':
data_out ^= (1 << 6);
send_update = 1;
break; // Bascule PB6
case 'e':
data_out ^= (1 << 7);
send_update = 1;
break; // Bascule PB7
case 'q':
running = 0;
break;
}
if (send_update)
{
// Envoi de la nouvelle configuration OUT
int r = libusb_interrupt_transfer(dev_handle, EP_OUT, &data_out, 1, &actual_length, 50);
if (r == 0)
{
printf("[PC] Ordre envoyé : 0x%02X\n", data_out);
}
else
{
printf("[PC] Erreur OUT : %s\n", libusb_error_name(r));
}
}
}
Configuration boutons en IN
Ce bout de code permet d'afficher l'état des boutons.
unsigned char data_in = 0;
// On met un timeout très court (10ms) pour ne pas bloquer la boucle
int r = libusb_interrupt_transfer(dev_handle, EP_IN, &data_in, 1, &actual_length, 10);
if (r == 0 && actual_length == 1)
{
// On n'affiche que si l'état des boutons a changé (pour éviter de spammer la console)
if (data_in != last_data_in)
{
printf("[CARTE] État PORTC : 0x%02X -> ", data_in);
if ((data_in & (1 << 6)) == 0)
printf("B6 pressé ! ");
if ((data_in & (1 << 7)) == 0)
printf("B7 pressé ! ");
if ((data_in & (1 << 6)) != 0 && (data_in & (1 << 7)) != 0)
printf("Relâchés.");
printf("\n");
last_data_in = data_in;
}
}
// Petite pause pour ne pas surcharger le processeur du PC (10 millisecondes)
usleep(10000);
}
Résultat
sudo ./main_pc
Terminal
=== MODE LIVE ACTIVÉ ===
Touches :
[A] : Basculer LED 1 (PB5)
[Z] : Basculer LED 2 (PB6)
[E] : Basculer LED 3 (PB7)
[Q] : Quitter
========================
[CARTE] État PORTC : 0xC0 -> Relâchés.
[CARTE] État PORTC : 0x40 -> B7 pressé !
[CARTE] État PORTC : 0xC0 -> Relâchés.
[CARTE] État PORTC : 0x80 -> B6 pressé !
[CARTE] État PORTC : 0xC0 -> Relâchés.
[CARTE] État PORTC : 0x00 -> B6 pressé ! B7 pressé !
[CARTE] État PORTC : 0x40 -> B7 pressé !
[CARTE] État PORTC : 0xC0 -> Relâchés.
a[PC] Ordre envoyé : 0x20
z[PC] Ordre envoyé : 0x60
e[PC] Ordre envoyé : 0xE0
q
Fermeture du programme...
Vidéo après soudure des LEDS
Lecture ISP
L'objectif est maintenant d'utiliser notre communication IN/OUT pour envoyer des commandes SPI à un microcontrôleur cible afin de lire sa signature (Device ID).
Côté Carte (LUFA)
On modifie notre fonction main.c, afin d'inclure la lecture ISP.
- On initialise le SPI
void spi_init(void) { SPI_DDR |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS); // Définition des sorties SPI_DDR &= ~(1 << SPI_MISO); // Définition de l'entrée SPI_PORT |= (1 << SPI_SS); // Désactivation du périphérique SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0); // Activation SPI (SPE) en état maître (MSTR) SPSR &= ~(1 << SPI2X); // horloge F_CPU/128 (SPI2X=0, SPR1=1,SPR0=1) }
- On fait le transfert l'octet. On place l'octet à envoyer dans
SPDR, on attend que l'octet arrive chez l'esclave, puis on renvoie ce que nous envoie l'esclave.uint8_t spi_transfer(uint8_t data) { SPDR = data; // Octet a envoyer while (!(SPSR & (1 << SPIF))) ; // Attente fin envoi (drapeau SPIF du statut) return SPDR; }
- On réécrit la fonction qui gère la commande OUT du PC pour inclure le paquet SPI. Si l'octet ne vaut pas
0x01alors on ne fait rien, sinon on applique au LED la valeurs du paquet OUT, comme précédemment.uint8_t commande_PC = 0; void ProcessOUT(void) { Endpoint_SelectEndpoint(DATA_OUT_EPADDR); if (Endpoint_IsOUTReceived()) { if (Endpoint_IsReadWriteAllowed()) { // On lit l'octet envoyé par le PC commande_PC = Endpoint_Read_8(); if (commande_PC != 0x01) // On l'applique aux LEDs (PORTB) PORTB = commande_PC; } Endpoint_ClearOUT(); } }
- Pour la gestion du IN : si le paquet n'est pas
0x01alors on lit l'état des boutons comme précemment. Sinon on active le SPI, et on applique le protocole ISP. Ce protocole consiste à envoyer une série de 4 octets dans un ordre bien spécifique, comme indiqué dans la datasheetAtmel-0943-In-System-Programming_ApplicationNote_AVR910qu'on peut retrouver dans le dossierdatasheet.Que se passe-t-il pendant les 4 octets du protocole AVR ISP ?
Le protocole ISP d'Atmel est conçu pour que chaque instruction fasse exactement 32 bits (4 octets).- Octet 1 (0x30) : Le Maître dit "Je veux lire la signature". L'Esclave reçoit ça, mais ne renvoie rien pour le moment.
- Octet 2 (0x00) : Le Maître envoie l'adresse de poids fort (inutile ici). L'Esclave est en train de comprendre la commande
0x30. - Octet 3 (0x00, 0x01 ou 0x02) : Le Maître demande précisément l'adresse 0, 1 ou 2 de la signature. L'Esclave va chercher l'information dans sa mémoire.
- Octet 4 (0x00) : L'Esclave a préparé la réponse (la signature), mais il ne contrôle pas l'horloge ! Il a besoin que le Maître génère 8 coups d'horloge pour que la réponse puisse voyager sur le fil
MISO. Le Maître envoie donc un octet factice (0x00) uniquement pour faire tourner l'horloge. Pendant ce temps, la cible glisse sa réponse sur le filMISO. C'est pour ça qu'on faituint8_t sig_n = spi_transfer(0x00);! Le dernier octet retourné par la cible est la donnée recherchée.
Pourquoi 3 octets de signature et qu'est-ce qu'on récupère ?
Chaque microcontrôleur AVR possède une "Signature" unique gravée en usine, composée de 3 octets (situés aux adresses 0, 1 et 2).- Octet 0 : Identifie le fabricant. Atmel est toujours
0x1E. - Octet 1 : Identifie la famille et la mémoire.
- Octet 2 : Identifie le modèle exact. Par exemple, le AT90S1200 a pour signature 0x1E 0x90 0x01.
Voici le tableau qui résume ce protocole. On retrouve en ligne les 3 octets qu'on reçoit et en colonne les 4 octets nécéssaires à la récupération de chacun de ces 3 octets.
Voici la fonction qui met en place le protocole :
void ProcessIN(void) { Endpoint_SelectEndpoint(DATA_IN_EPADDR); if (Endpoint_IsReadWriteAllowed()) { if (commande_PC != 0x01) Endpoint_Write_8(PINC & 0xC0); else { // Mode ISP : Le PC a demandé les identifiants spi_ON(); // (3.2 datasheet) Enable Memory Access spi_transfer(0xAC); spi_transfer(0x53); spi_transfer(0x00); spi_transfer(0x00); // --- Lecture du 1er octet de signature (Adresse 0x00) --- spi_transfer(0x30); spi_transfer(0x00); spi_transfer(0x00); uint8_t sig_1 = spi_transfer(0x00); // La cible répond pendant cet octet // --- Lecture du 2ème octet de signature (Adresse 0x01) --- spi_transfer(0x30); spi_transfer(0x00); spi_transfer(0x01); uint8_t sig_2 = spi_transfer(0x00); // --- Lecture du 3ème octet de signature (Adresse 0x02) --- spi_transfer(0x30); spi_transfer(0x00); spi_transfer(0x02); uint8_t sig_3 = spi_transfer(0x00); spi_OFF(); // On envoie les 3 octets au PC ! Endpoint_Write_8(sig_1); Endpoint_Write_8(sig_2); Endpoint_Write_8(sig_3); // On efface la commande pour ne pas spammer la cible au prochain tour commande_PC = 0; } Endpoint_ClearIN(); } }
- Read Low Byte : Commande
0x20suivi de l'adresse. - Read High Byte : Commande
0x28suivi de l'adresse.
Le format d'une commande est toujours de 4 octets:
- Octet 1 :
0x20(Low) ou 0x28 (High). - Octets 2 et 3 : L'adresse du mot (poids fort puis poids faible)
- Octet 4 : Donnée fictive (
0x00) pour récupérer la réponse de la cible. - Modification de la fonction
ProcessOUT(): On prépare la demande du codePC. Une fois que celui-ci aura envoyé0X02, le programmeur AVR "sait" qu'il va recevoir 2 octets contenant l'adresse désirée par le PC.uint16_t flash_address = 0; void ProcessOUT(void) { Endpoint_SelectEndpoint(DATA_OUT_EPADDR); if (Endpoint_IsOUTReceived()) { if (Endpoint_IsReadWriteAllowed()) { // On lit l'octet envoyé par le PC commande_PC = Endpoint_Read_8(); if (commande_PC == 0x02) // Demande de lecture Flash { flash_address = ((uint16_t)Endpoint_Read_8() << 8); // Poids fort flash_address |= Endpoint_Read_8(); // Poids faible } else if (commande_PC != 0x01) { PORTB = commande_PC; // Gestion des LEDs } } Endpoint_ClearOUT(); } } } }
- Ajout du
ifdans la fonctionProcessIN(): Comme indiqué précédemment, on va lire la flash à l'adresse demandée par le PC. Comme dansdescriptor.hon a mis#define DATA_SIZE 8(plus lent mais plus intéressant pour comprendre) et qu'un mot fait 2 octets, alors cela veut dire qu'on peut lire jusqu'à 4 mots àflash_address + ipour optimiser la bande passante USB (d'ou le i jusqu'à 4 dans lefor). Ensuite on suit le protocole expliqué au dessus pour la lecture de la flash pour chaque mots lu.if (commande_PC == 0x02) { spi_ON(); // (3.2 datasheet) Enable Memory Access spi_transfer(0xAC); spi_transfer(0x53); spi_transfer(0x00); spi_transfer(0x00); for (uint16_t i = 0; i < 4; i++) { uint16_t current_addr = flash_address + i; // Lecture du Low Byte spi_transfer(0x20); spi_transfer(current_addr >> 8); // On décale de 8 les 2 octets donc on a 0x00HIGH = 0xHIGH spi_transfer(current_addr & 0xFF); // On fait un & à l'adresse avec 0x00FF donc on a 0x00LOW = 0xLOW uint8_t data_low = spi_transfer(0x00); // Lecture du High Byte spi_transfer(0x28); spi_transfer(current_addr >> 8); spi_transfer(current_addr & 0xFF); uint8_t data_hight = spi_transfer(0x00); // envoie au PC Endpoint_Write_8(data_low); Endpoint_Write_8(data_hight); } spi_OFF(); commande_PC = 0; // On a fini d'envoyer le bloc }
- Demander l'adresse
0x0000(la carte renvoie les mots 0, 1, 2, 3). - Demander l'adresse
0x0004(la carte renvoie les mots 4, 5, 6, 7). - Demander l'adresse
0x0008(la carte renvoie les mots 8, 9, 10, 11). - ...
- flash_read.c :
printf("\n=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===\n\n"); int r; int actual_length; unsigned char data_out[3]; unsigned char data_in[8]; // Tampon de 8 octets pour recevoir 4 mots // Boucle pour lire 32 mots (64 octets), par pas de 4 mots for (uint16_t addr = 0; addr < 32; addr += 4) { // Préparation de la commande OUT (0x02 + Addr_High + Addr_Low) data_out[0] = 0x02; data_out[1] = (addr >> 8) & 0xFF; // Poids fort de l'adresse data_out[2] = addr & 0xFF; // Poids faible de l'adresse // Demande de lecture en OUT r = libusb_interrupt_transfer(dev_handle, EP_OUT, data_out, 3, &actual_length, 100); if (r != 0) { printf("Erreur lors de la demande pour l'adresse 0x%04X : %s\n", addr, libusb_error_name(r)); continue; } // Réception IN (avec supression des paquets "boutons" d'1 octet) do { r = libusb_interrupt_transfer(dev_handle, EP_IN, data_in, 8, &actual_length, 100); } while (r == 0 && actual_length == 1); // Ignore les paquets de boutons // Affichage du bloc reçu if (r == 0 && actual_length == 8) { printf("Adresse 0x%04X : ", addr); for (int i = 0; i < 8; i += 2) { printf("%04X ", (data_in[i + 1] << 8) | data_in[i]); } printf(" \n"); } else { printf("Erreur de lecture a l'adresse 0x%04X (reçu %d octets)\n", addr, actual_length); } }
- Activation de la mémoire (Enable Memory Access) : Envoi de la séquence
0xAC 0x53 0x00 0x00obligatoire pour déverrouiller l'accès. - Chargement du Low Byte) : Utilisation de la commande
0x40suivie de l'adresse et de l'octet faible pour le placer dans le tampon. - Chargement du High Byte) : Utilisation de la commande
0x48suivie de l'adresse et de l'octet fort. - Déclenchement du Write Page : Utilisation de la commande
0x4Csuivie de l'adresse pour graver physiquement le tampon dans la Flash. - Attente : Délai de quelques millisecondes (environ 5 ms) pour laisser le matériel terminer l'écriture avant de relâcher le
RESET. :: indique le début d'un ligne10: Nombre d'octets de données sur cette ligne (Ici0x10=16octets).0000: L'adresse de départ en Flash pour cette ligne.00: Type de ligne (00= données à flasher,01= fin du fichier).0C9434000C94...: Les données du programme- écriture
- lecture On lit bien ce qu'on a écrit juste avant.
- Problème : le programmateur n'est pas détecté en USB. Un erreur est détectée ce qui montre qu'il y a une connexion, mais n'apparaît pas dans
lsusb. - Programmable : La connexion en ISP en revanche fonctionne. On pourra donc programmer les leds et les boutons avec une UNO. Voir dans le futur si on peut fixer le problème de l'USB pour rendre le programmateur fonctionnel.
- ajout d'un premier
blink_led.c(brouillon) au git - TESTS : Le code
blinkfonctionne mais pas le codeboutons-> faut contact / pbs de soudure sur les pattes - Modification initiale du wikicode
- Brainstorming pour se décider sur un projet
- Encodeur: voir si on peut en utiliser un pour régler le son/luminosité écran.
- Connecteur ISP (le même que le programatteur)
- USB-A, condensateurs de découplage, condensateur VUSB.
- Chargeur Lipo (le même que celui du wiki donc à modifier selon notre schéma).
- haut parleur : 8 entrée DAC.
- Ecran : 11 entrée (8 affichage et 3 gestion).
- Cerveaux moteur optionnels si on a pas assez de ports
- Voir comment connecter la flash.
- Correction de la V1, et ajout des connecteurs pour le chargement de la batterie/ choix de l'alimentation / connecteur batterie
- Routage : début, du placement des composants. A FAIRE:
- Demander les composants physique tel que le buzzer et le cerveau moteur
- On a enlevé des pins sur l'AVR qui étaient pris inutilement par la batterie LIPO => voir si on ne peut pas rajouter un cerveau moteur et des boutons à la place.
- Tension : Analyser les composants qui nécéssitent 5V de tension pour ajouter un booster de tension pour garder le 5V en batterie (3,3V). Inversement mettre un régulateur de tension sur les composants fonctionnant en 3,3V, lors de l'alimentation en 5v. Convertisseur : TPS61033-Q1
- Ajout du booster de tension TPS61033-Q1 pour l'écran, le LM386, le capteur ultrason et les 2 cervosmoteur.
- Ajout d'un régulateur de tension pour la flash.
- Ajout de la section "Détail schématique" et première explication du boost et régulateur de tension.
- A FAIRE:
- Demander les composants physique disponibles : buzzer et les cerveaux moteur
- Demander une vérification de la V2 pour avancer sur le routage
- Correction de la V2, changement de booster et du regulateur pour simplifier le comosant et la commande sur Farnell. A FAIRE
- Modifier la section "détail de la schématique" en conséquence et revoir le dimsensionnement des composants.
- Ajout du shifter pour la flash.
- V1 routage : Alimentation en 0.8mm, pas d'angles droits, DRC au maximum, annotation avec du texte en silkscreen.
- Soudures test du uP : ISP OK
- TODO : Faut contact sur l'USB. Mais carte détéctée en DFU donc pas de soucis.
- Le problème du DFU ne vient pas de l'USB, mais du condensateur utilisé sur le port UCAP du uP.
- Problème : Avec les 3 commandes de DFU classiques, le programme ne voulait pas se téléverser. Donc j'ai utilisé
--forcesur leerase. Malheureusement cela a écrasé le code DFU sur la carte.
=> TODO :- remettre le code DFU sur l'atmega
- commencer à préparer le code pour l'écran pour le tester à la prochaine séance.
Côté PC (libusb)
La fonction isp_ids.c permet l'envoie d'une commande qui implique la réception des identifiants ISP de l'AVR. Il enverra la commande 0x01, puis écoutera le point d'accès IN pour récupérer et afficher les 3 octets de la signature.
OUT
On envoie simplement l'octet 0x01 pour activer le code ISP sur l'avr (côté LUFA).
// ----OUT----
unsigned char data_out = 0x01; // octet à envoyer pout l'ISP (choix)
int actual_length;
int r = libusb_interrupt_transfer(dev_handle, EP_OUT, &data_out, 1, &actual_length, 50);
if (r == 0)
{
printf("[PC] Ordre envoyé : 0x%02X\n", data_out);
}
else
{
printf("[PC] Erreur OUT : %s\n", libusb_error_name(r));
}
IN
Pour récupérer les 3 octets, on doit d'abord ignorer le premier paquet envoyé par les boutons (do/while). Ensuite on imprime les 3 octets reçu et on indique si c'est un AVR atmel qu'on connecte en ISP.
// ----IN ----
unsigned char data_in[3] = {0, 0, 0};
printf("[PC] Attente de la réponse (Purge des anciens paquets)...\n");
// Boucle pour ignorer les paquets de 1 octet (les boutons)
// On boucle tant que le transfert réussit ET que la taille est de 1
do
{
r = libusb_interrupt_transfer(dev_handle, EP_IN, data_in, 3, &actual_length, 100);
} while (r == 0 && actual_length == 1);
// Sortie de boucle : on a soit une erreur, soit notre paquet de 3 octets !
if (r == 0 && actual_length == 3)
{
printf("Signature reçue avec succès : 0x%02X 0x%02X 0x%02X\n", data_in[0], data_in[1], data_in[2]);
if (data_in[0] == 0x1E)
{
printf("-> Fabricant : Atmel reconnu !\n");
}
}
else
{
printf("Erreur ou Timeout (reçu %d octets) : %s\n", actual_length, libusb_error_name(r));
}
Résultats
sudo ./isp_ids
Résultat avant soudure de l'ISP :
[PC] Ordre envoyé : 0x01
[PC] Attente de la réponse (Purge des anciens paquets)...
Signature reçue avec succès : 0xFF 0xFF 0xFF
Fermeture du programme...
Résultat après soudure de l'ISP : Test sur une Arduino UNO
[PC] Ordre envoyé : 0x01
[PC] Attente de la réponse (Purge des anciens paquets)...
Signature reçue avec succès : 0x1E 0x95 0x0F
-> Fabricant : Atmel reconnu !
Fermeture du programme...
Flash
L'objectif est de récupérer l'intégralité du contenu de la mémoire flash de la cible. Celle-ci étant volumineuse, le transfert se fait par blocs de 8 octets (soit 4 mots de 16 bits).
Envoyer bloc par bloc sur le IN
Modification de la LUFA (main.c)
Du côté de la LUFA, nous modifions le main.c pour gérer un adressage sur 16 bits et utiliser
les commandes de lecture flash définies dans la datasheet (AVR910).
L'adresse de lecture est envoyée par le PC via le point d'accès OUT, puis la carte lit 8 octets en
SPI et les renvoie via le point d'accès IN.
Comprendre la lecture de la Flash via ISP :
-
Selon le document technique (Table 3-5), la lecture de la mémoire flash se fait mot par mot (un mot = 2 octets).
CODE (main.c):
Récéption PC (flash_read.c)
Comme on a programmé notre AVR pour qu'elle récupère 4 mots par adresses données, il faut qu'on lui donne des adresses indentées de 4. Par exemple si on veut lire 64 octets de la flash (soit 32 mots), on boucle de cette manière :
-
Le code ci-dessous effectue cette boucle afin de récupérer 64 octets dans la flash cible.
RESULTATS
sudo ./flash_read
Avant soudure :
=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===
Adresse 0x0000 : FFFF FFFF FFFF FFFF
Adresse 0x0004 : FFFF FFFF FFFF FFFF
Adresse 0x0008 : FFFF FFFF FFFF FFFF
Adresse 0x000C : FFFF FFFF FFFF FFFF
Adresse 0x0010 : FFFF FFFF FFFF FFFF
Adresse 0x0014 : FFFF FFFF FFFF FFFF
Adresse 0x0018 : FFFF FFFF FFFF FFFF
Adresse 0x001C : FFFF FFFF FFFF FFFF
Fermeture du programme...
Après soudure (UNO):
=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===
Adresse 0x0000 : 940C 005D 940C 0085
Adresse 0x0004 : 940C 0085 940C 0085
Adresse 0x0008 : 940C 0085 940C 0085
Adresse 0x000C : 940C 0085 940C 0085
Adresse 0x0010 : 940C 0085 940C 0085
Adresse 0x0014 : 940C 0085 940C 0085
Adresse 0x0018 : 940C 0085 940C 0085
Adresse 0x001C : 940C 0085 940C 0085
Fermeture du programme...
Mise à jour de la flash (Page Mode)
L'objectif est d'écrire de nouvelles données dans la mémoire Flash de la cible. Étant donné que les cibles modernes comme l'ATmega328P (Arduino) ne supportent plus l'écriture octet par octet (Byte Mode), nous nous appuyons sur la Datasheet de l'ATmega328P (Section Serial Downloading) pour utiliser le mode de programmation par page (Page Mode).
Mécanisme d'écriture SPI
L'écriture dans la Flash nécessite désormais de précharger les données dans un tampon temporaire (Page Buffer) avant de déclencher la gravure physique de la page. Pour simplifier notre programme PC, nous chargeons un seul mot (16 bits) dans le tampon, puis nous forçons immédiatement la gravure.
La session se fait en un seul cycle d'activation de la cible (spi_ON) :
Implémentation LUFA (main.c)
Pour pourvoir écrire dans la flash cible il faut effacer la flah. Pour ça on a fait la fonction chip_erase() qui suit le protocole du tableau.
void chip_erase(void)
{
spi_ON();
_delay_ms(5);
// (3.2 datasheet) Enable Memory Access
spi_transfer(0xAC);
spi_transfer(0x53);
spi_transfer(0x00);
spi_transfer(0x00);
// (3.4.3 datasheet) erase
spi_transfer(0xAC);
spi_transfer(0x80);
spi_transfer(0x00);
spi_transfer(0x00);
_delay_ms(5);
spi_OFF();
}
Dans notre point d'accès OUT, nous créons la commande 0x03. Le programme PC enverra un paquet
contenant l'adresse cible et les deux octets à écrire. La carte exécute ensuite la séquence SPI. On crée la commande 0x04 pour erase la flash.
else if (commande_PC == 0x03) // Ecriture de flash (flash_rw.c)
{
flash_address = ((uint16_t)Endpoint_Read_8() << 8); // Poids fort
flash_address |= Endpoint_Read_8(); // Poids faible
uint8_t low = Endpoint_Read_8();
uint8_t high = Endpoint_Read_8();
spi_ON();
_delay_ms(5); // Temps de réveil cible
// (3.2 datasheet) Enable Memory Access
spi_transfer(0xAC);
spi_transfer(0x53);
spi_transfer(0x00);
spi_transfer(0x00);
// Low byte
spi_transfer(0x40);
spi_transfer(flash_address >> 8);
spi_transfer(flash_address & 0xFF);
spi_transfer(low);
// High byte
spi_transfer(0x48);
spi_transfer(flash_address >> 8);
spi_transfer(flash_address & 0xFF);
spi_transfer(high);
// Write
spi_transfer(0x4C);
spi_transfer(flash_address >> 8);
spi_transfer(flash_address & 0xFF);
spi_transfer(0x00);
_delay_ms(5); // Attente de la gravure physique
spi_OFF();
}
else if (commande_PC == 0x04)
{
chip_erase();
}
PC(flash_rw.c)
Pour tester l'écriture, on fait une fonction flash_rw.c qui écrit dans la flash cible puis la lit.
Pour écrire dans la flash cible, on doit d'abord effacer la flash, puis on envoi l’octet 0x03 (choisit par défaut). Ensuite on
envoi l'adresse ou on veut écrire la donnée; en 2 octets HIGH/LOW, et de même pour la donnée à envoyer.
int r;
int actual_length;
// =========================================================
// 0. EFFACEMENT DE LA CIBLE (Chip Erase)
// =========================================================
printf("\n=== EFFACEMENT DE LA PUCE ===\n");
unsigned char cmd_erase = 0x04;
r = libusb_interrupt_transfer(dev_handle, EP_OUT, &cmd_erase, 1, &actual_length, 500);
if (r == 0)
{
printf("Puce effacée avec succès !\n");
}
else
{
printf("Erreur lors de l'effacement : %s\n", libusb_error_name(r));
}
// =========================================================
// 1. TEST D'ÉCRITURE (Byte Mode)
// =========================================================
printf("\n=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===\n\n");
unsigned char data_write[5]; // Paquet de 5 octets pour l'écriture
// On boucle sur les 4 premières adresses de mots (0, 1, 2, 3)
for (uint16_t addr = 0; addr < 5; addr++)
{
// On invente une donnée factice pour le test (ex: 0xA000, 0xA001...)
uint16_t fake_data = 0xA000 + addr;
// Préparation du paquet selon le protocole LUFA
data_write[0] = 0x03; // Commande d'écriture
data_write[1] = (addr >> 8) & 0xFF; // add HIGH
data_write[2] = addr & 0xFF; // add LOW
data_write[3] = fake_data & 0xFF; // fake LOW
data_write[4] = (fake_data >> 8) & 0xFF; // fake HIGH
// Envoi de l'ordre d'écriture en OUT
r = libusb_interrupt_transfer(dev_handle, EP_OUT, data_write, 5, &actual_length, 100);
if (r == 0)
{
printf("Ordre d'écriture envoyé -> Adresse: 0x%04X | Donnée: 0x%04X\n", addr, fake_data);
}
else
{
printf("Erreur d'écriture à l'adresse 0x%04X : %s\n", addr, libusb_error_name(r));
}
}
RESULTATS
sudo ./flash_rw
Avant soudure :
=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===
Ordre d'écriture envoyé -> Adresse: 0x0000 | Donnée: 0xA000
Ordre d'écriture envoyé -> Adresse: 0x0001 | Donnée: 0xA001
Ordre d'écriture envoyé -> Adresse: 0x0002 | Donnée: 0xA002
Ordre d'écriture envoyé -> Adresse: 0x0003 | Donnée: 0xA003
RQ : la lecture renvoie quand même que des mots en FFFF
Après soudure :
=== EFFACEMENT DE LA PUCE ===
Puce effacée avec succès !
=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===
Ordre d'écriture envoyé -> Adresse: 0x0000 | Donnée: 0xA000
Ordre d'écriture envoyé -> Adresse: 0x0001 | Donnée: 0xA001
Ordre d'écriture envoyé -> Adresse: 0x0002 | Donnée: 0xA002
Ordre d'écriture envoyé -> Adresse: 0x0003 | Donnée: 0xA003
Ordre d'écriture envoyé -> Adresse: 0x0004 | Donnée: 0xA004
=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===
Adresse 0x0000 : A000 A001 A002 A003
Adresse 0x0004 : A004 FFFF FFFF FFFF
Adresse 0x0008 : FFFF FFFF FFFF FFFF
Adresse 0x000C : FFFF FFFF FFFF FFFF
Adresse 0x0010 : FFFF FFFF FFFF FFFF
Adresse 0x0014 : FFFF FFFF FFFF FFFF
Adresse 0x0018 : FFFF FFFF FFFF FFFF
Adresse 0x001C : FFFF FFFF FFFF FFFF
Fermeture du programme...
Assemblage(résultats)
L'objectif est d'utiliser les fonctions test et de les modifier afin de d'avoir un seul programme main qui contrôle tout. On l'appel prog.c
La première chose est de retirer la gestion USB dans flash_read.c et flash_read.c et de la séparer dans une fonction dev_handler.c.
On change la fonction flash_rw.c en flash_write.c pour seulement écrire avec.
Enfin on compile tout avec un Makefile et on exécute le code avec une commande intelligente.
Ici on flashera un code basique blink.c qui fera clignoter la LED PB5 de l'arduino UNO.
Comment flasher un programme C ?
Pour flasher un fichier C il faut écrire le .hex associé. La compilation d'un .hex est détaillée dans "Programateur/libusb/code_a_televerser/Makefile".
Un fichier .hex est composé de lignes d'octets qui ont une fonction bien précise.
On prend par exemple la première ligne dans blink.hex : :100000000C9434000C943E000C943E000C943E0082
flash_write.c
On applique cette logique dans notre nouvelle fonction.
Pour séparer ligne par ligne on utilise la fonction fgets.
Pour sectionner ces ligne on utilise la fonction sscanf. (Ces 2 fonctions on été vues en Structure de Données Avancées).
Le reste est détaillés dans les commentaires du code.
char line[256];
unsigned int length, addr_in, type_in;
while (fgets(line, sizeof(line), file) != NULL)
{
sscanf(line, ":%2x%4x%2x", &length, &addr_in, &type_in);
uint16_t type = (uint16_t)type_in;
if (type == 0x01)
break;
if (type == 0x00)
{
// Du côté du fichier .HEX l'adresse est codé octet par octet. Contrairement à l'AVR qui stock des adresses de mots. Donc pour avoir l'adresse du mot(2 octets) il suffit de diviser l'adresse in par 2.
uint16_t current_word_addr = addr_in / 2;
for (unsigned int i = 0; i < length; i += 2)
{
unsigned int word_data;
// On décale le point de départ du sscanf de 9 (début des octets du code) puis on l'avance d'un facteur de 2 par rapport à i. Comme i aussi avance d'un facteur de 2 alors le tout avance d'un facteur de 4. Comme on lit 4 valeurs ascii à la fois (en effet 1 octet = 2 valeurs ascii => 2 octets = 1 mot = 4 valeurs ascii) le décallage est respécté.
sscanf(line + 9 + (i * 2), "%4x", &word_data);
uint16_t data = ((word_data & 0xFF) << 8) | (word_data >> 8);
// Préparation du paquet selon le protocole LUFA
data_write[0] = 0x03; // Commande d'écriture
data_write[1] = (current_word_addr >> 8) & 0xFF; // add HIGH
data_write[2] = current_word_addr & 0xFF; // add LOW
data_write[3] = data & 0xFF; // LOW
data_write[4] = (data >> 8) & 0xFF; // HIGH
// Envoi de l'ordre d'écriture en OUT
...
Programme principale (prog.c)
Ce programme relie toutes les fonctions vues avant. Il récupère d'abord les identifiants de la cible via le protocole ISP. Ensuite il laisse le choix entre lire ou écrire la flash cible.
Structure cible
Tout d'abord pour bien différencier la cible, on cré une structure cible:
typedef struct
{
unsigned char signature[3];
const char *nom;
uint32_t taille_flash; // Taille totale en octets
uint16_t taille_page; // Taille d'une page en octets
} CibleAVR;
Base de Donnée
On a créé une base de donnée pour avoir les informations exactes des puces si elles sont reconnues via le protocole ISP.
const CibleAVR base_de_donnees[] = {
{{0x1E, 0x95, 0x0F}, "ATmega328P", 32768, 128},
{{0x1E, 0x94, 0x89}, "ATmega16U2", 16384, 128},
{{0x1E, 0x93, 0x0B}, "ATtiny85", 8192, 64}};
On créé ensuite une fonction qui cherche la puce dans la BDD :
#define NB_CIBLES (sizeof(base_de_donnees) / sizeof(CibleAVR))
const CibleAVR *recherche_cible(unsigned char sig[3])
{
for (long unsigned int i = 0; i < NB_CIBLES; i++)
{
if (sig[0] == base_de_donnees[i].signature[0] &&
sig[1] == base_de_donnees[i].signature[1] &&
sig[2] == base_de_donnees[i].signature[2])
{
return &base_de_donnees[i];
}
}
return NULL; // Puce inconnue
}
main
On utilise des variables dans notre fonction main() pour pouvoir rajouter des fonctions : si on écrit -read ou -write après le nom de notre programme on peut soit lire ou écrire la flash cible.
int main(int argc, char *argv[])
{
// Initialisation unique
libusb_device_handle *dev_handle = init_usb();
if (dev_handle == NULL)
return 1;
if (reclamer_interface(dev_handle) < 0)
return 1;
// On récupère les infos de la cible si elle est connue.
unsigned char sig[3] = {0, 0, 0};
isp_ids(dev_handle, sig);
const CibleAVR *cible = recherche_cible(sig);
if (cible == NULL)
printf("\033[31mLa cible est inconnue.\033[0m\n");
else
printf("La cible est une %s, elle a une flash de \033[33m%d\033[0m octets et des pages de \033[33m%d\033[0m octets.\n", cible->nom, cible->taille_flash, cible->taille_page);
// commandes utilisateur
if (argc == 1)
{
printf("\033[31mErreur : Vous devez préciser une action.\033[0m\n");
printf("Usage : make read OU make write <fichier.hex>\n");
}
else if (strcmp(argv[1], "-read") == 0)
{
printf("\nLancement de la lecture...\n");
flash_read(dev_handle, cible->taille_flash);
}
else if (strcmp(argv[1], "-write") == 0)
{
if (argc < 3)
{
printf("\033[31mErreur : Il manque le nom du fichier .hex !\033[0m\n");
}
else
{
FILE *file = fopen(argv[2], "r");
if (file == NULL)
printf("\033[31mErreur : Impossible d'ouvrir le fichier %s.\033[0m\n", argv[2]);
else
{
printf("\nOuverture du fichier %s...\n", argv[2]);
flash_write(dev_handle, file);
printf("\033[32mÉcriture terminée avec succès !\033[0m\n");
fclose(file);
}
}
}
// Fermeture unique et propre
printf("\nFermeture globale du programme...\n");
libusb_release_interface(dev_handle, 0);
libusb_close(dev_handle);
libusb_exit(NULL);
return 0;
}
RESULTATS
Compilation dans libusb/
sudo make clean && make
Compilation dans libusb/code_a_televerser/
make clean && make
Execution du code
sudo ./build/prog -write blink.hex
sudo ./build/prog -read
Bilan
03/03 : soudure
Soudure presque terminée. LEDs et résistances associées manquantes, 1 bouton poussoir traversant manquant.
10/03 : Problème ISP -> connexion ISP
11/03 : fonctions test
LEDS et boutons fonctionnent -> attente de réparation de notre USB par le prof ou fin de projet pour le programatteur.
08/04 : impression de la nouvelle carte
Nouvelle carte imprimée et détection de notre carte via lsusb.
17/04 : minicom
Programmation minicom.
18/04 : codes test vérifiables avant soudure
Programmation InOut test et ISP.
(19-26)/04 : suite codes test vérifiables avant soudure
Programmation flash,ISP, fonctions test OK avant soudure.
28/04 : Soudure de la nouvelle carte
Soudure des leds et du composants ISP de la carte. Modification des fonctions test pour faire fonctionner le code sur une atemaga328p(UNO).
02/05 : finition du projet
Finition du projet : le programmeur est capable de transférer un code C sur une AVR (3 AVR sont ajoutées dans la BDD du programmeur).
Un code simple blink.c est testé et fonctionnel sur une arduino UNO (atemage328p).
Tout les détails du projets sont indiqués dans la section "Programmation" détaillés étape par étape.
Le code/résultat final est dans la sous section "Assemblage" de la section "Programmation".
Sur le git, on retrouve le contenu nécessaire dans Programmeur/. L'utilisation du code est décris dans le README.
Premier système embarqué
Archive GIT
Notre archive GIT pour le projet KiCAD et pour les programmes : https://gitea.plil.fr/mterrier/2025_PSE_B2_mterrier_jramesh
Structure avec matériel (y compris production - gerber, bill of materials) / logiciel / documentation (e.g. documentation technique).
Description du système embarqué
Nous avons décider de réaliser BMO, un système comportant un écran affichant un visage minimaliste, munis d'un détécteur de mouvement et d'un buzzer.
Lorsque notre main s'approche de détécteur, le visage plisse les yeux (ou devient triste). Lorsque notre main s'écarte de celui ci, le visage réouvre les yeux (ou devient heureux).
Le système sera alimenté par une batterie, ou une alimentation USB en 5V.
Afin de valider l'utilisation du port USB, nous connecterons un ordinateur au système via USB et éffectuerons un transfert de données sonores (divers sons) qu'on ira stocker dans la flash.
On a ajouté 2 servos moteurs pour faire office de bras. On pourra s'en servir pour proposer plus d'interactions.
Historique des Scéances
17/02 : initialisation
03/03 : Début de la schématique
10/03 : V1 schématique
-
Push de la V1 sur le git.
24/03 : corrections + modification chargeur LIPO
29/03 : push de la V2 de la schématique
31/03 : Correction de la V2
(04-05)/04 : V1 Routage
12/05 : Premières soudures
13/05 : Premières soudures
Carte électronique
Carte réalisée en utilisant le logiciel KiCAD : ma carte électronique.
Vidéo très courte et en basse résolution de la carte en fonctionnement : Média:2025-PSE-02-systeme-video.mp4
Détail de la schématique
Booster de tension
-
Pour alimenter la logique (écran LCD, capteur ultra son, Haut parleur) et surtout les servomoteurs depuis la batterie de 3.3V, le choix s'est porté sur le régulateur boost TPS61033-Q1. La broche FB est reliée directement à l'entrée (VIN) pour fixer la tension de sortie à 5.0V en interne, rendant inutile l'usage d'un pont diviseur. La broche EN est maintenue à VIN pour un fonctionnement continu, tandis que la broche MODE est à la masse (GND) pour activer le mode Auto PFM. Ce mode garantit un excellent rendement énergétique à faible charge, ce qui est crucial pour économiser la batterie lorsque les moteurs sont à l'arrêt.
Pour gérer la décharge lors de l'extinction, une résistance de 1kΩ (Rdummy) est placée sur la broche PG. Cela tire seulement 5mA (bien en deçà de la limite de 50mA imposée par le transistor à drain ouvert) et assure une chute de tension propre et rapide en un quart de seconde.
Côté puissance, les servomoteurs SG90 imposent des pics de courant importants (estimés à 1.5A au total au démarrage). Pour éviter les chutes de tension critiques (brownouts), les capacités de filtrage ont été dimensionnées avec une marge de sécurité : 22µF en entrée et 47µF en sortie pour compenser la perte de capacité sous tension continue. Le cœur du convertisseur est une inductance de 0.47µH. En considérant un rendement (η), soit environ 40.6%. Le courant continu moyen demandé à la bobine se calcule via , soit environ 2.53A. En y ajoutant la moitié de l'ondulation crête-à-crête , le courant de crête absolu atteint , soit 3.12A. L'inductance choisie doit donc impérativement présenter un courant de saturation supérieur à 3.5A pour garantir la stabilité du système en pleine charge. C'est pour cela qu'on a choisi comme conseillé dans la datasheet l'inductance XGL4020-471MEC qui peut avoir un courant de saturation allant jusqu'à 6,1A.
Régulateur de tension
-
Pour alimenter la mémoire flash AT45DB641E, qui requiert une tension de fonctionnement < 3,6V, le choix s'est porté sur le convertisseur LTC3531-3.3. Contrairement à un régulateur linéaire (LDO) qui dissiperait l'excédent de tension sous forme de chaleur, ce composant est un régulateur de type Buck-Boost synchrone. Cette topologie permet de maintenir une sortie de 3,3V parfaitement stable, que la tension d'entrée provienne du 5V ou directement de la batterie.
Le dimensionnement des composants périphériques s'appuie sur les recommandations strictes du constructeur. L'inductance de puissance a été fixée à une valeur standard de 10 µH. Cette valeur est optimisée par le fabricant pour limiter l'ondulation du courant et maximiser le rendement énergétique à faible charge (la mémoire flash consommant au maximum ~25 mA lors des cycles d'écriture/effacement).
En entrée, un condensateur CIN = 4.7 µF est chargé d'absorber le bruit haute fréquence et les appels de courant transitoires liés au hachage interne du régulateur. En sortie, un condensateur COUT = 10 µF est implanté au plus près de la broche VCC de la mémoire. Le respect de ces valeurs (L = 10 µH, CIN = 4.7 µF, COUT = 10 µF) garantit que les appels de courant soudains de la puce SPI ne provoqueront aucune chute de tension hors des tolérances logiques.

