« SE3Groupe2025-2 » : différence entre les versions

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
 
(193 versions intermédiaires par 3 utilisateurs non affichées)
Ligne 1 : Ligne 1 :
Selsabil TAZANI & Leïli KACHOUR
 
= 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.

Schéma électronique
Routage

3d
Front
Back

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

Montage

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 -> ENTRER
  • E -> C -> ENTRER
  • F -> ENTRER
  • "Save setup as dfl" -> ENTRER
  • Echape ou Exit

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 0x01 alors 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 0x01 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 Atmel-0943-In-System-Programming_ApplicationNote_AVR910 qu'on peut retrouver dans le dossier datasheet.
    PSE2 ISP command.png

    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 fil MISO. C'est pour ça qu'on fait uint8_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.
    PSE2 ISP allowed dev.png

    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.

    PSE2 ISP device code.png

    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();
    	}
    }
    
  • 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).
    • Read Low Byte : Commande 0x20 suivi de l'adresse.
    • Read High Byte : Commande 0x28 suivi 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.
    • PSE2 flash reading.png

    CODE (main.c):

    • Modification de la fonction ProcessOUT() : On prépare la demande du code PC. 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 if dans la fonction ProcessIN() : Comme indiqué précédemment, on va lire la flash à l'adresse demandée par le PC. Comme dans descriptor.h on 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 + i pour optimiser la bande passante USB (d'ou le i jusqu'à 4 dans le for). 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
      		}
      
    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 :

    • 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).
    • ...
      Le code ci-dessous effectue cette boucle afin de récupérer 64 octets dans la flash cible.
    • 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);
          }
      }
      
    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
    PSE2 flash writting page mode.png

    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) :

    • Activation de la mémoire (Enable Memory Access) : Envoi de la séquence 0xAC 0x53 0x00 0x00 obligatoire pour déverrouiller l'accès.
    • Chargement du Low Byte) : Utilisation de la commande 0x40 suivie de l'adresse et de l'octet faible pour le placer dans le tampon.
    • Chargement du High Byte) : Utilisation de la commande 0x48 suivie de l'adresse et de l'octet fort.
    • Déclenchement du Write Page : Utilisation de la commande 0x4C suivie 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.
    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

    • : : indique le début d'un ligne
    • 10 : Nombre d'octets de données sur cette ligne (Ici 0x10 = 16 octets).
    • 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

    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

    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

    • 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 blink fonctionne mais pas le code boutons -> faut contact / pbs de soudure sur les pattes

    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.

    BE2 lsusb.png
    BE2 lsusb details.png


    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

    • Modification initiale du wikicode
    • Brainstorming pour se décider sur un projet

    03/03 : Début de la schématique

    • 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.

    10/03 : V1 schématique

      Push de la V1 sur le git.

    24/03 : corrections + modification chargeur LIPO

    • 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

    29/03 : push de la V2 de la schématique

    • 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

    31/03 : Correction de la V2

    • 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.

    (04-05)/04 : V1 Routage

    • 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.

    12/05 : Premières soudures

    • Soudures test du uP : ISP OK
    • TODO : Faut contact sur l'USB. Mais carte détéctée en DFU donc pas de soucis.

    13/05 : Premières soudures

    • 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é --force sur le erase. 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.

    Carte électronique

    Carte réalisée en utilisant le logiciel KiCAD : ma carte électronique.

    Schéma électronique
    Routage

    3d front
    3d back

    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.