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

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
 
Ligne 1 254 : Ligne 1 254 :
</ul>
</ul>


== Historique des scéances ==
== Historique des séances ==


<div style="font-size: 1.2em; font-weight: bold; margin-top: 1em; margin-bottom: 0.5em;">03/03 : soudure</div>
<div style="font-size: 1.2em; font-weight: bold; margin-top: 1em; margin-bottom: 0.5em;">03/03 : soudure</div>

Version actuelle datée du 18 juin 2026 à 06:36

Programmation des systèmes embarqués

Fabrication et programmation d'un programmeur AVR. But : programmer d'autres µP via le protocole ISP.

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

    Historique des séances

    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(#Archive 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(#Archive GIT), on retrouve le contenu nécessaire dans Programmeur/. L'utilisation du code est décris dans le README.

    Premier système embarqué

    On retrouve dans cette sections les détails du code pour notre premier système embarqué. Tout n'est pas indiqué car cela aurait trop de contenu pas forcément intéressant à lire. Le README de notre git(#Archive GIT) indique l'utilisation de notre programme ainsi qu'un résumé très bref de chaque fichiers C. La section OBJECTIFS résume ce que nous avons réussi à faire et à ne pas faire.

    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.

    Programmation

    Il se peut que certaines fonctions on été modifiée sur le git(#Archive GIT) depuis la création de ce wiki. Notamment à propos de la gestion du temps ou de l'utilisation de fonctions externes. Mais le principe de ces fonctions reste le même, c'est ce qui est important d'indiquer dans ce wiki.

    La gestion d'état du robot est dirigée par un enum Etat_robot ce qui permet l'activation de fonctions selon l'état du robot. Cette mécanique ne sera pas expliquée dans le wiki.

    fonction test

    Fonction led.c qui fait clignoter les 2 LEDS :

    #define BLINK_DELAY 100 // en milli secondes
    void leds()
    {
        // configurer les led en sortie :
        DDRB |= (1 << 4); //  LED1
        DDRF |= (1 << 0); //  LED2
    
        // led on
        PORTB ^= (1 << 4);
        PORTF ^= (1 << 0);
        _delay_ms(BLINK_DELAY);
        PORTB ^= (1 << 4);
        PORTF ^= (1 << 0);
        _delay_ms(BLINK_DELAY);
    }
    

    Capteur ultra son

    On utilise un capteur ultra son HC-SR04. Il a les pins 5V, GND, trigger et echo.
    Le trigger : c'est le signal qu'envoit le uP au capteur. Il passe à 1 -> le capteur envoi l'onde, il retombe à 0 -> le capteur se "rendors".

    #define RC7 (1 << 7)
    void trigger(void) 
    {
        PORTC |= RC7;
        _delay_us(10);
        PORTC &= ~RC7;
    }
    
  • L'echo : c'est l'onde qu’envoi le capteur. Une fois l'onde envoyée, on lance un timer. Quand l'onde revient sur le capteur après avoir rebondit sur l'objet, le timer s'arrête.
    TCR1B configure le timer1 de l'AVR soit TCNT1. Mettre CS11 à 1 fait que le timer incrémente de 1, une fois tout les 8 coups d'horloge. On a la formule . Or ici on a un quartz de 8MHz, ce qui nous donne .
    La vitesse du son dans l'air à 20°C est d'environ 340 m/s. Cela correspond à 0.034 cm/µs. Soit (car aller-retour) donc .
  • #define RB7 (1 << 7)
    uint16_t echo(void)
    {
        uint16_t time;
        trigger();
        while (!(PINB & RB7)) // On attend que l'onde s'envoie.
            ;
        // timer
        TCNT1 = 0;             // reset
        TCCR1B |= (1 << CS11); // timer on (uS)
        while (PINB & (RB7))   // On attend que l'onde revienne.
            ;
        time = TCNT1;
        return ((uint32_t)time * 10) / 59; // distance en mm
    }
    

    Ecran

    Datasheets/1602A.pdf
    On utilise un écran RC1602A qu'on commande sur 4 bits (car manque de ports sur l'arduino.)

    Pour commander l'écran sur 4 bits il faut envoyer un section d'octets, comme indiqué dans la data sheet :
    4bit config.png
    void ecran_init(void)
    {
        DDRC |= RS;
        DDRF |= (E | DB4 | DB5 | DB6 | DB7);
    
        PORTC &= ~RS;
        // Init 4 bit mode
        _delay_ms(20);
        send_nibble(DB4 | DB5);
        _delay_ms(5);
        send_nibble(DB4 | DB5);
        _delay_us(500);
        send_nibble(DB4 | DB5);
        _delay_us(50);
        send_nibble(DB5);
        _delay_us(50);
        // config : 0x28 = 40 = 5*8 caractères (par cases)
        lcd_write(0x28, 0);
        // Display ON/OFF Control : Allume l'écran, curseur caché, pas de clignotement
        lcd_write(0x0C, 0);
        // Clear Display : Nettoie la mémoire (DDRAM)
        clear_display();
        // Entry Mode Set : Écriture de gauche à droite (incrémentation automatique)
        lcd_write(0x06, 0);
    }
    
    En revanche cette gestion de l'écran demande 2 configuration de registres dans le main.c avant de compiler :
    • La gestion du Watchdog Timer (WDT) post-Bootloader
    • Sur l'ATmega32U4, le passage du mode de programmation (Bootloader DFU) à l'application utilisateur s'effectue via un redémarrage logiciel provoqué par le Watchdog Timer. Cependant, le bootloader ne désactive pas toujours ce timer avant de céder le contrôle. Comme l'initialisation de l'écran ici prend plusieur dizaine de millisecondes à s'executer, elle dépasse le délai d'expiration du Watchdog (souvent 15 ou 30 ms), le microcontrôleur subit un redémarrage d'urgence en boucle. Il est donc impératif d'effacer le drapeau de redémarrage dans le registre d'état (MCUSR = 0;) et de désactiver le timer (wdt_disable();) dès la toute première ligne du main() pour garantir un démarrage stable du système.
    • La libération de l'interface de débogage JTAG
    • Par défaut, en sortie d'usine, le microcontrôleur réserve les broches PF4 à PF7 au port de débogage matériel JTAG. Toute tentative logicielle de configurer ces broches en entrées/sorties standards (GPIO) via les registres DDRF et PORTF est matériellement ignorée. Pour récupérer le contrôle de ce port, l'interface JTAG doit être désactivée. Cela s'effectue en écrivant un "1" logique sur le bit JTD du registre de contrôle MCUCR.
    Il faut le faire de manière consécutive de telle sorte à ce que cela dure moins de 4 coups d'horlorge (MCUCR |= (1 << JTD); MCUCR |= (1 << JTD);

    Affichage de caractères standars (CGROM et DDRAM)

    Une fois l'écran initialisé, il est prêt à recevoir des caractères. Le contrôleur de l'écran possède une mémoire morte interne appelée CGROM (Character Generator ROM) qui contient l'alphabet et divers symboles pré-enregistrés.
    Pour afficher un caractère, il suffit d'envoyer son code binaire correspondant à la table de la datasheet :

    HD44780 Character Table.png

    Pour faciliter la lecture du code, tous les caractères utiles ont été mappés dans une énumération C (enum Symbole).

    Cependant, il faut dire à l'écran afficher ce caractère. L'écran possède une mémoire d'affichage (la DDRAM). Une particularité technique importante de cet écran est que les adresses mémoires ne sont pas continues entre les deux lignes :
    • La Ligne 1 s'étend de l'adresse 0x00 à 0x0F.
    • La Ligne 2 commence à l'adresse 0x40 et va jusqu'à 0x4F.
    DDRAM Memory Map.png

    L'envoi d'un caractère se fait donc en deux étapes : positionner le curseur (instruction), puis envoyer le dessin (data).

    • On envoi un quartet soit 4 bits sur nos fameuses 4 entrées de l'écran.
    • void send_nibble(uint8_t quartet)
      {
          PORTF &= ~(RegB4 | RegB5 | RegB6 | RegB7);
          PORTF |= ((RegB4 | RegB5 | RegB6 | RegB7) & quartet);
          pulse_enable();
      }
      
    • On autorise l'écriture sur l'écran
    • void pulse_enable(void)
      {
          PORTF |= RegE;
          _delay_us(1);
          PORTF &= ~RegE;
      }
      
    • On envoi le quartet de poids fort puis de poids faible
    • void lcd_write(uint8_t data, uint8_t is_char)
      {
          if (is_char) // DATA
          {
              PORTC |= RegRS;
          }
          else // INSTRUCTION
          {
              PORTC &= ~RegRS;
          }
          // high
          send_nibble(data & 0xF0);
          _delay_us(1);
          // low
          send_nibble((data & 0x0F) << 4);
      
          _delay_us(50);
      }
      
    • On se place sur la case qu'on veut écrire. (0x80 : cf datasheet page 12, table des commandes)
    • void set_address(uint8_t address)
      {
          lcd_write(0x80 + address, 0);
      }
      
    • On écrit la data sur cette case.
    • void afficher_char(uint8_t data, uint8_t address)
      {
          set_address(address);
          lcd_write(data, 1);
      }
      

    Création d'animations et limitation matérielle (CGRAM)

    Pour réaliser des expressions faciales pour le robot, les symboles d'usine ne suffisent pas. Le HD44780 dispose d'une petite mémoire vive modifiable appelée CGRAM (Character Generator RAM). Cette mémoire est très limitée : elle ne peut stocker que 8 caractères personnalisés simultanément (de 5x8 pixels), indexés de 0x00 à 0x07.

    Exemple de caractère fabriqué :
    Grand zero.png
    CGRAM Address Structure.png
    • L'astuce de l'animation :
    • Pour créer une animation fluide (comme un clignement d'œil) sans subir le scintillement causé par l'instruction clear_display(), la stratégie consiste à positionner les blocs 0x00 à 0x07 de manière permanente sur l'écran LCD, puis à écraser les données dans la CGRAM en arrière-plan. L'écran mettra automatiquement à jour l'image affichée.
      void charger_frame(const uint8_t matrice_complete[8][8])
      {
          // On place le curseur tout au début de la mémoire CGRAM
          lcd_write(0x40, 0);
      
          // On envoie les 64 octets (8 caractères * 8 lignes) à la suite
          for (uint8_t bloc = 0; bloc < 8; bloc++)
          {
              for (uint8_t ligne = 0; ligne < 8; ligne++)
              {
                  lcd_write(pgm_read_byte(&(matrice_complete[bloc][ligne])), 1);
              }
          }
          // On remet le curseur sur l'écran normal
          set_address(case1);
      }
      
    • Gestion de la mémoire Flash (PROGMEM) :
    • Stocker de nombreuses "frames" d'animation (tableaux à deux dimensions) sature immédiatement la mémoire RAM du microcontrôleur (qui ne possède que 2.5 Ko). Il faut forcer le compilateur à stocker ces données dans la mémoire Flash (32 Ko) en utilisant la directive PROGMEM de la bibliothèque <avr/pgmspace.h>.
      La lecture de ces tableaux nécessite alors une macro spécifique pgm_read_byte() (vu précédemment dans la fonction charger_frame).
      Exemple d'une frame :
      const uint8_t yeux_ouverts[8][8] = {
          {0x00, 0x07, 0x0F, 0x1C, 0x18, 0x18, 0x18, 0x18}, // Oeil Gauche - Haut Gauche (0x00)
          {0x00, 0x1C, 0x1E, 0x07, 0x03, 0x03, 0x03, 0x03}, // Oeil Gauche - Haut Droit  (0x01)
          {0x18, 0x18, 0x18, 0x1C, 0x0F, 0x07, 0x00, 0x00}, // Oeil Gauche - Bas Gauche  (0x02)
          {0x03, 0x03, 0x03, 0x07, 0x1E, 0x1C, 0x00, 0x00}, // Oeil Gauche - Bas Droit   (0x03)
      
          {0x00, 0x07, 0x0F, 0x1C, 0x18, 0x18, 0x18, 0x18}, // Oeil Droit - Haut Gauche  (0x04)
          {0x00, 0x1C, 0x1E, 0x07, 0x03, 0x03, 0x03, 0x03}, // Oeil Droit - Haut Droit   (0x05)
          {0x18, 0x18, 0x18, 0x1C, 0x0F, 0x07, 0x00, 0x00}, // Oeil Droit - Bas Gauche   (0x06)
          {0x03, 0x03, 0x03, 0x07, 0x1E, 0x1C, 0x00, 0x00}  // Oeil Droit - Bas Droit    (0x07)
      };
      

    C'est détaillé dans L'#Historique des scéances 2 mais je le reprécise ici : pour fabriquer une animation entière, on fabrique nos frames sur un site de pixel art (ex : https://www.piskelapp.com/p/create/sprite/) et on fait une conversion PNG -> tableau à 3 dimensions en C grâce à un programme python (Programmation/pixel_art_to_C/convertisseur.py).

    Synchronisation Écran et Capteur

    Maintenant qu'on a un capteur qui renvoi capte une distance en mm et un écran capable d'afficher des caractères et des frames on peut les synchronisées ensemble et faire des animations. Par exemple, une fonction qui affiche une barre de progression en fonction de la distance de l'objet du capteur et accélère la cadence des leds en fonction de celle-ci.

    void distance_barre(uint16_t distance)
    {
        uint8_t nb_carres = 0;
        uint16_t delay = 500;
    
        if (distance >= 20 && distance <= 340)
        {
            nb_carres = (340 - distance) / 10;
            delay = (distance * 3) / 2;
        }
        else if (distance < 20)
        {
            nb_carres = 32;
            delay = 30;
        }
        else
        {
            nb_carres = 0;
            delay = 500;
        }
        for (uint8_t i = 0; i < NBR_CASES; i++)
        {
            uint8_t address = ordre_cases[i];
            if (i < nb_carres)
            {
                afficher_char(plein, address);
            }
            else
            {
                clear_case(address);
            }
        }
        blink(delay);
    }
    



    Autre exemple, une fonction qui affiche la distance de l'objet en cm.

    void afficher_distance(uint16_t distance_mm)
    {
        uint8_t dizaines_cm = (distance_mm / 100) % 10;
        uint8_t unites_cm = (distance_mm / 10) % 10;
        uint8_t millimetres = distance_mm % 10;
    
        if (distance_mm >= 100)
        {
            afficher_char(dizaines_cm + '0', case1); // + '0' convertit le chiffre en caractère ASCII
            afficher_char(unites_cm + '0', case2);
        }
        else
        {
            clear_case(case1);
            afficher_char(unites_cm + '0', case2);
        }
        afficher_char(virgule, case3);
        afficher_char(millimetres + '0', case4);
        afficher_char(c, case15);
        afficher_char(m, case16);
    }
    

    Gestion du Temps (Timers AVR)

    La gestion du temps est l'un des facteur les plus important de ce projet. En effet, on doit avoir un timer différent pour chaque composants. L'écran prend à lui tout seule un certains temps pour afficher des choses. Le capteur ultrason demande lui aussi un temps précis pour fonctioner...

    Le problème des délais bloquants

    Initialement, les pauses (entre les mesures ou lors des changements de frames d'animation sur l'écran LCD) étaient gérées par la macro _delay_ms(). Cette fonction est dite "bloquante" car elle monopolise intégralement le processeur (l'ATmega32U4 tourne dans le vide en comptant des cycles d'horloge). Pendant ce temps, le robot est complètement paralysé : il ne peut plus mesurer de distance, ni lire les données entrantes via USB, ni jouer de l'audio de manière fluide.

    Le Chronomètre Système avec le Timer 0

    Pour pallier ce problème de paralysie, nous avons implémenté un chronomètre système absolu (SysTick) basé sur le Timer 0 de l'ATmega32U4, fonctionnant en tâche de fond de manière complètement transparente pour la boucle principale grâce aux interruptions matérielles. L'objectif technique est de déclencher une interruption très exactement toutes les millisecondes (soit 1000 Hz).

    • Choix du Timer :
    • Le Timer 0 est un compteur sur 8 bits. Il est parfait pour gérer des interruptions régulières à basse résolution, ce qui permet de libérer les Timers 16 bits de la puce pour des tâches nécessitant une plus grande finesse (comme l'audio dans notre cas).
    • Mode de fonctionnement :
    • Nous configurons ce Timer en mode CTC (Clear Timer on Compare Match). Dans ce mode, le Timer s'incrémente à partir de zéro et compare sa valeur en continu à la limite fixée dans le registre OCR0A. Dès que la limite est atteinte, il déclenche une interruption matérielle et retourne automatiquement à 0 au cycle d'horloge suivant, sans aucune intervention logicielle.
      Tableau de configuration du mode CTC
    • Calcul de la fréquence et du Prescaler :
    • La fréquence d'horloge de notre horloge externe est configurée sur 8 MHz. La formule mathématique dictée par la datasheet pour un Timer en mode CTC est la suivante : f = \frac{f_{CPU}}{N \times (1 + OCR0A)} Pour obtenir notre fréquence cible de 1000 Hz, nous divisons d'abord l'horloge système à l'aide d'un prédiviseur (Prescaler) N = 64. Le calcul de la valeur plafond de OCR0A devient alors une simple équation : OCR0A = \frac{8000000}{64 \times 1000} - 1 = 124
      Tableau de sélection du Prescaler à 64

    time.c

    Fonction d'initialisation expliqué ci-desuss :

    static volatile uint32_t temps_systeme = 0;
    void temps_init(void) // TIMER 0
    {
        // Mode CTC (Clear Timer on Compare Match)
        TCCR0A = (1 << WGM01);
    
        // Prescaler à 64 (Horloge à 8 MHz / 64 = 125 000 ticks par seconde)
        TCCR0B = (1 << CS01) | (1 << CS00);
    
        // 125 000 ticks par seconde = 125 ticks par milliseconde.
        // On compte de 0 à 124 (soit 125 étapes)
        OCR0A = 124;
    
        // Activer l'interruption sur la comparaison A
        TIMSK0 |= (1 << OCIE0A);
    
        // Activer les interruptions globales du microcontrôleur
        sei();
    }
    

    Fonction d'interruption qui permet d'augmenter le compteur de temps :

    // Interuption déclenchée toutes les 1 milliseconde
    ISR(TIMER0_COMPA_vect)
    {
        millis_compteur++;
    }
    

    La fonction qui est utilisée partout dans notre code qui utilise cette logique du TIMER0 qui permet de générer des compteurs de temps sans mettre en pause notre système :

    uint32_t get_millis(void)
    {
        uint32_t temps_actuel;
    
        // ATOMIC_BLOCK fige l'alarme pendant 1 microseconde pour éviter qu'elle ne modifie la variable pile au moment où on la lit
        ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
        {
            temps_actuel = millis_compteur;
        }
        return temps_actuel;
    }
    

    Le mot-clé volatile : Il s'agit d'une instruction vitale donnée au compilateur GCC pour lui signifier que la variable temps_systeme peut être modifiée en arrière-plan à tout instant de manière "invisible" par le matériel (l'ISR). Sans ce mot-clé, le flag d'optimisation -Os la traiterait comme une constante et paralyserait la machine à états.

    Le principe de ATOMIC_BLOCK : Le cœur de l'ATmega32U4 étant sur 8 bits, il doit physiquement effectuer 4 opérations de lecture successives pour copier une valeur de 32 bits (la variable temps). Si l'interruption matérielle du Timer 0 vient à s'exécuter au beau milieu de ces opérations et modifie la variable source, la valeur finale retournée sera corrompue. Le bloc ATOMIC_BLOCK neutralise temporairement toutes les interruptions globales pendant la lecture pour garantir l'intégrité absolue du temps.

    Répartition globale des Timers de l'architecture

    Grâce à la fonction get_millis(), toute l'architecture de la boucle principale repose désormais sur des conditions de delta temporel fluides de type if (get_millis() - temps_precedent > DELAI), rendant le multitâche possible. Pour avoir un audio précis on a séparé le timer réservé pour celui-ci du timer 0. Ce qui nous donne en résumé :

    • Timer 0 (8 bits) : Réservé exclusivement au Chronomètre Système (SysTick) tournant à 1 kHz.
    • Timer 3 (16 bits) : Réservé au Moteur Audio pour piloter le convertisseur DAC R-2R à une fréquence d'échantillonnage très précise (8 kHz).

    Note : plusieurs fonctions d'interruptions sont commentées dans time.c car on a fait des test pour vérifier le hardware (tests qui suggèrent un problème sur le hardware)

    Le Module Audio (DAC R-2R et Timer 3)

    L'objectif de cette section est de générer des sons (voix, bruitages) pour donner vie à BMO. N'utilisant pas de composant décodeur MP3 dédié, nous devons générer le signal analogique nous-mêmes.

    Bien que nos tests finaux n'aient pas permis d'entendre le son attendu en sortie du haut-parleur, ce code de l'audio (et du timer) nous ont apportés la certitude que nos sons on été joués sur le buzzer. Ce détail et les hyposthèses d'erreures sont sugérées à la fin de cette section.

    Principe de l'échantillonnage

    Pour jouer un son, nous utilisons le format audio le plus brut possible : le PCM (Pulse Code Modulation) 8-bits Mono non-signé.

    • 8-bits : Le niveau de tension de l'onde sonore est défini sur 256 valeurs.
    • Fréquence de 8 kHz : Nous devons envoyer exactement 8000 valeurs par seconde pour reconstituer correctement l'onde sonore.

    Pour convertir ces valeurs numériques en tensions électriques, notre carte électronique utilise un DAC (Convertisseur Numérique-Analogique) R-2R. Il s'agit d'un simple réseau de résistances soudées directement sur les 8 broches du PORTD de l'ATmega32U4. Envoyer une valeur audio se résume donc à une simple assignation : PORTD = valeur_audio;.

    Configuration du Timer 3

    Pour respecter le rythme exacte de 8000 Hz, la fonction bloquante _delay_ms() est impossible. Exactement comme nous l'avons fait pour le Chronomètre Système (cf. section précédente sur le Timer 0), nous allons utiliser une interruption matérielle, mais cette fois-ci adossée au Timer 3.

    La configuration globale de ce timer est très similaire au 0. Mais forcément des caluls changent :

    Calcul du registre OCR3A : Notre horloge système (F_CPU) tourne à 8 MHz. Nous visons une fréquence d'interruption f = 8000 Hz. Étant donné que 8 MHz est facilement divisible pour obtenir 8 kHz, nous n'avons pas besoin de ralentir le Timer. Nous fixons donc le Prescaler à 1 (horloge directe). L'équation de la datasheet est la suivante : OCR3A = \frac{f_{CPU}}{Prescaler \times f} - 1 OCR3A = \frac{8000000}{1 \times 8000} - 1 = 1000 - 1 = 999

    Datasheet : Configuration du Prescaler à 1 via le bit CS30

    Architecture de flux : Le Ring Buffer

    Si le processeur doit aller chercher chaque octet audio dans une mémoire externe (Flash ou via USB) 8000 fois par seconde, il risque de "manquer" des cycles. Pour éviter les coupures audio, nous avons implémenté un Ring Buffer (Tampon circulaire) de 256 octets.

    Le Timer 3 vide ce tableau à la vitesse stricte de 8 kHz pour l'envoyer sur le PORTD.

    La boucle principale remplit ce tableau avec de nouvelles données dès qu'il y a de la place, de manière asynchrone.

    Implémentation en C

    Définition du Ring Buffer

    #define BUFFER_SIZE 256
    volatile uint8_t audio_buffer[BUFFER_SIZE];
    volatile uint8_t index_lecture = 0;  // Géré par le Timer 3
    volatile uint8_t index_ecriture = 0; // Géré par la Super-Boucle
    

    Initialisation du timer audio

    void timer_audio_init(void)
    {
        // 1. Configuration des broches matérielles
        // Le DAC R-2R est connecté sur tout le PORTD (broches D0 à D7)
        // On met tout le port D en sortie.
        DDRD = 0xFF;
        PORTD = 128; // On initialise au milieu de l'échelle (silence analogique)
    
        // 2. Configuration du Timer 3
        // On s'assure que les registres sont à 0 avant de les configurer
        TCCR3A = 0;
        TCCR3B = 0;
        TCNT3 = 0;
    
        // Mode CTC (Clear Timer on Compare Match) pour le Timer 3 (Mode 4)
        // Le bit WGM32 se trouve dans TCCR3B
        TCCR3B |= (1 << WGM32);
    
        // Prédiviseur (Prescaler) à 1
        // Le bit CS30 active l'horloge sans division (8 MHz directs)
        TCCR3B |= (1 << CS30);
    
        // Valeur de comparaison pour atteindre exactement 8 kHz
        // (8 000 000 / 8000) - 1 = 999
        OCR3A = 999;
    
        // Activer l'interruption sur la comparaison A du Timer 3
        TIMSK3 |= (1 << OCIE3A);
    }
    

    Le timer qui dirige le flux audio.

    ISR(TIMER3_COMPA_vect)
    {
        // Si un son doit être joué
        if (index_lecture != index_ecriture)
        {
    
            // L'envoyer directement sur les 8 broches du convertisseur DAC
            PORTD = ring_buffer[index_lecture];
    
            index_lecture++;
        }
        else
            PORTD = 128;
    }
    

    Bilan du du module audio

    Deux points font que l'ont suspecte l'hardware au software, testé et approuvé sur plusieurs audio différents :

    • La longueur de l'audio entre le mp3, et le rendu final est exactement la même
    • On peut différencier 2 audio différents qui se distingue par de grosses variations d'intensité sonores. Malgrés le son de mauvaise qualitée qui sort

    Plusieurs tests on soulevé des hypothèses de la défaillance de l'audio :

    • On a mesuré au multimètre, une présence anormale de 0.88V en courant continu (DC) directement aux bornes du haut-parleur. Notre circuit audio comporte un amplificateur LM386 couplé à un grand condensateur cylindrique de 470µF (C11). Le rôle critique de ce condensateur est de filtrer la tension continue et de ne laisser passer que les variations du signal alternatif (le son). Le passage de ces 0.88V peut indiquer que le condensateur C11 n'a pas fait son traval (court-circuit interne ou inversion de polarité). Ce courant continu a poussé la membrane du haut-parleur vers l'avant de manière permanente ce qui donc a dù perturber le flux audio.
    • Un test pour envoyer un voltage très précis a échoué. Avec un timer on envoyait (normalement) 0V puis 2.5V puis 5V sur le haut parleur. Résultat : 0V et parfois 5V à des manière aléatoires.
    • La soudure des composants est peut être liée à une perturbation audio. Un test d'inversion des bits sur le PORTD n'a rien donné. Mais la disposition des résistances peut jouer sur la qualité sonore finale.
    • Quand on a fait le schéma Kicad on a pas fait atention mais la igne d'alimentation du LM386 est d'une largeur similaire à un fil de données classique. Mais je pense que cela ne joue seulement sur la chaleur créée mais je n'écarte pas l'hypothèse.

    Communication USB et Streaming Audio (LUFA & PC Streamer)

    Pour alimenter en continu le réseau de résistances de notre DAC R-2R à la fréquence stricte de 8 kHz (cf. section précédente), le robot BMO a besoin d'une source de données constante. Avant d'envisager le stockage autonome, nous avons développé un système de streaming direct depuis un ordinateur hôte via le bus USB. Cette section détaille l'infrastructure logicielle double : le côté de la bibliothèque LUFA et le programmme pc qui envoit le bon fichier audio.

    Rapprochement avec le Programmeur AVR

    Lors de notre premier projet d'étude concernant la fabrication d'un programmeur AVR (cf. section détaillée : #LUFA), nous avions déjà utilisé la blibliothèque LUFA. On l'avait utilisée pour faire une classe de type CDC (Communication Device Class), transformant le microcontrôleur en un port série virtuel (dev_ttyACM0) capable de dialoguer de manière transparente avec avrdude.

    Pour le streaming audio de BMO, nous réutilisons exactement ce même système, mais avec une modification de l'architecture des descripteurs USB. Au lieu d'émuler un port série (CDC), nous configurons un canal de données brut de type Périphérique Générique (Vendor-Class) avec un Endpoint de type OUT (Bulk ou Interrupt) optimisé pour le transfert massif de données binaires directes vers notre mémoire tampon.

    Implémentation en c de la LUFA (audio_USB.c)

    Le fichier audio_USB.c s'appuie sur les fonctions de bas niveau de LUFA pour écouter le canal de communication (Endpoint 1, configuré en mode OUT de 64 octets) et déverser les octets reçus dans notre Ring Buffer de lecture audio.

    Déclaration externe des index du Ring Buffer définis dans time.c

    extern volatile uint8_t audio_buffer[256];
    extern volatile uint8_t index_ecriture;
    extern volatile uint8_t index_lecture;
    

    Fonction qui s'occupe de remplir le tuyau USB à chaque tours de boucle :

    void task_module_usb(void)
    {
    	// On anticipe la prochaine case mémoire
    	uint8_t octet_suivant = (uint8_t)(index_ecriture + 1);
    
    	// TANT QU'il y a des octets dans le tuyau USB ET qu'il reste de la place dans notre Ring Buffer (octet_suivant != index_lecture)
    	while ((CDC_Device_BytesReceived(&VirtualSerial_CDC_Interface) > 0) && (octet_suivant != index_lecture))
    	{
    		int16_t octet_recu = CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface);
    
    		if (octet_recu >= 0)
    		{
    			ring_buffer[index_ecriture] = (uint8_t)octet_recu;
    			index_ecriture = octet_suivant;
    			octet_suivant = (uint8_t)(index_ecriture + 1);
    		}
    	}
    
    	CDC_Device_USBTask(&VirtualSerial_CDC_Interface);
    	USB_USBTask();
    }
    

    Programme PC (pc_audio_streamer.c)

    Du côté du PC, le code pc_audio_streamer.c utilise la bibliothèque système libusb-1.0 pour ouvrir le périphérique BMO via son identifiant unique de vendeur (VID) et de produit (PID). Son rôle est d'ouvrir un fichier audio brut (RAW, 8 kHz, 8 bits) présent sur le disque dur, de le découper en segments de 64 octets, et de les propulser sur le bus USB.

    On convertit nos fichiers mp3 en .raw grâce à ffmpeg. Exemple d'une conversion :

    ffmpeg -i audio/son.mp3 -ac 1 -ar 8000 -f u8 -acodec pcm_u8 audio/son.raw
    

    Plus de détails dans le README du git.

    Pour les détails de cette partie il est judicieux d'aller directement voir le code sur le git(#Archive GIT) dans Programmation/audio/C_files car cela utilise du programme libusb vu dans la partie #Programme PC avec libusb Pour résumer : le robot envoi un caractère en fonction de son état (dans audio.c) et en fonction de ce caractère le PC lit le son.raw et l’envoi par paquets de 64 octets dans le tuyau USB. Voici la fonction principale de lecture audio :

    void read_audio(char *audio, int fd)
    {
        FILE *audio_file = fopen(audio, "rb");
        if (!audio_file)
        {
            perror("\033[31mErreur : Fichier audio introuvable\033[0m");
            return;
        }
    
        unsigned char tx_buffer[CHUNK_SIZE];
        size_t bytes_read;
    
        // On lit le fichier par blocs de 64 octets
        while ((bytes_read = fread(tx_buffer, 1, CHUNK_SIZE, audio_file)) > 0)
        {
            // On envoie le bloc au robot
            write(fd, tx_buffer, bytes_read);
        }
    
        fclose(audio_file);
        printf("\033[32mStreaming terminé. Retour à l'écoute.\033[0m\n");
    }
    

    Mémoire Flash SPI (AT45DB641E)

    Afin de rendre notre robot interactif totalement autonome et de l'affranchir d'une connexion permanente à un PC Linux via USB, nous avons intégré une mémoire sur notre carte électronique : la puce Flash AT45DB641E. L'objectif est d'y stocker nos sons. Tout les codes C qui suivent proviennent de flash.c. La fonction audio_flash.c qui permet la lecture audio de la flash, utilise le même principe que audio_USB.c mais en plus simple car elle n'utilise pas de modules LUFA pour l'USB. On ne redétailleras donc pas cette fonction ici.

    Le Bus SPI : Rapprochement avec le Programmeur AVR

    Pour dialoguer avec cette mémoire Flash, le microcontrôleur utilise son interface matérielle SPI (Serial Peripheral Interface).

    Comme vu précédemment dans la section détaillée de la Programmation AVR, nous utilisons les broches MISO, MOSI et SCK pour transmettre nos octets, ainsi que le principe de l'octet factice (0x00) pour générer l'horloge et forcer la cible à répondre. Le principe de communication est ici rigoureusement identique à celui utilisé pour flasher un autre microcontrôleur. cf : #Lecture ISP.

    Configuration ATmega32U4 et le "Piège du SS"

    L'initialisation du bus SPI matériel nécessite une lecture attentive de la datasheet de l'ATmega32U4 pour éviter un blocage critique :

    Sélection des broches : Le bus SPI matériel est physiquement câblé sur le port B. Nous utilisons PB1 (SCK - Horloge), PB2 (MOSI - Données sortantes) et PB3 (MISO - Données entrantes).

    Le piège de la broche SS (Slave Select) : Sur l'ATmega32U4, la broche par défaut pour le SS matériel est PB0. La datasheet indique que si le microcontrôleur est configuré en mode "Maître" (Master) mais que cette broche PB0 est laissée en entrée et passe à l'état bas (0V), le microcontrôleur panique, abandonne son rôle de Maître et coupe l'horloge SPI. Donc on fait dans l'initialisation DDRB |= (1 << PB0). On configure également FLASH_CS (câblé sur PE6) en sortie. Remarque: j'avais mis FLASH_CS sur PE6 car on utilisais PB0 pour autre mais au final on la modifié et j'aurais finalement pu utiliser PB0.

    master mode

    Registre SPCR (SPI Control Register) : Nous activons le module matériel en mettant le bit SPE à 1, et nous déclarons le microcontrôleur comme Maître avec le bit MSTR à 1. Sans ce dernier, l'ATmega ne générera jamais le signal d'horloge.

    Registre SPCR détaillant l'activation du SPI et le mode Maître

    Configuration flash AT45DB641E

    Du côté de la puce Flash, la datasheet nous impose deux règles simples :

    Polarité du CS (Chip Select) : La puce s'active sur un front descendant (état bas). Au repos, nous devons donc maintenir la broche FLASH_CS à l'état haut (1 logique).

    Polarité du CS (Chip Select)

    Mode SPI : La puce supporte les modes SPI 0 (CPOL=0, CPHA=0) et 3. Ces paramètres de polarité et de phase d'horloge à zéro correspondent très exactement aux valeurs par défaut de l'ATmega au démarrage. Aucune configuration supplémentaire n'est donc requise dans le registre SPCR.

    mode SPI

    On a donc notre fonction d'initialisation de la flash qui répond aux critères énoncés ci-dessus :

    #define FLASH_CS PE6 // Chip Select (SS)
    #define SPI_SCK PB1  // Horloge
    #define SPI_MOSI PB2 // Sortie (Master Out)
    #define SPI_MISO PB3 // Entrée (Master In)
    
    void flash_init(void)
    {
        DDRE |= (1 << FLASH_CS);
        DDRB |= (1 << PB0) | (1 << SPI_SCK) | (1 << SPI_MOSI);
        DDRB &= ~(1 << SPI_MISO);
    
        spi_OFF();
    
        // SPE  : SPI Enable
        // MSTR : Master (L'ATmega dirige l'horloge)
        SPCR = (1 << SPE) | (1 << MSTR);
    }
    

    Le "Hello World" de la Flash : Lecture de l'ID

    Avant d'écrire ou de lire des données audio complexes, la première étape logicielle consiste à valider l'intégrité des soudures et du bus SPI en demandant à la puce son identifiant constructeur (Device ID).

    Selon la datasheet de la Flash, l'envoi de l'Opcode 0x9F provoque la réponse immédiate de la puce sur 3 octets.

    flash read ids

    Pour le transfert SPI on utilise ecxactement la même fonction que pour le programmeur (cf: #Lecture ISP)

    Pour la lecture de l'id on utilise le protocole indiqué par la datasheet :

    void flash_read_device_id(uint8_t *manufacturer_id, uint8_t *device_id_1, uint8_t *device_id_2)
    {
        // Activer Flash
        spi_ON();
    
        // Envoyer le code pour lire l'ID
        spi_transfer(0x9F);
    
        // Lire les 3 octets de réponse (on envoie des octets bidons '0x00' pour générer l'horloge)
        *manufacturer_id = spi_transfer(0x00);
        *device_id_1 = spi_transfer(0x00);
        *device_id_2 = spi_transfer(0x00);
    
        spi_OFF();
    }
    

    On peut ensuite afficher ces données directement sur l'écran LCD, plutot que de sembeter à le faire passer par l'USB ! Dans ecran.c on a cette fonction qui s'en charge :

    void display_flash_ids(void)
    {
        uint8_t id1, id2, id3;
        char buffer_lcd[16];
        char sentence[26] = "This come from  the flash";
        flash_read_device_id(&id1, &id2, &id3);
    
        // Copie de la chaine dans une variable, pour la lire ensuite
        sprintf(buffer_lcd, "ID: %02X %02X %02X", id1, id2, id3);
    
        while (1)
        {
            clear_display();
            for (int i = 0; buffer_lcd[i] != '\0'; i++)
            {
                afficher_char(buffer_lcd[i], ordre_cases[i]);
            }
            _delay_ms(3000);
            clear_display();
            for (int i = 0; sentence[i] != '\0'; i++)
            {
                afficher_char(sentence[i], ordre_cases[i]);
            }
            _delay_ms(2000);
        }
    }
    

    On affiche bien les valeurs 1F 28 00 (1F correspondant au fabricant Adesto, et 28-00 définissant la famille et la densité de 64Mbits). Cela nous a permis de valider définitivement la couche de communication matérielle de notre architecture !
    English is not for everyone'c

    Effacement et Écriture (Gravure Audio)

    Une fois la communication validée, il a fallu pouvoir injecter les sons depuis le PC vers cette puce. Il est peut être plus judicieux de le faire en ISP, mais le faire via l'USB reste bien plus pratique.

    L'écriture avec Auto-Effacement : Pour graver les données, nous utilisons l'Opcode 0x82 (Main Memory Page Program through Buffer 1). L'avantage majeur de cette instruction mis en évidence par la datasheet est sa fonction "Built-In Erase" : elle efface automatiquement la page mémoire avant d'y écrire les nouvelles données de notre son.

    Datasheet_Flash_opcode_0x82

    Pour écrire dans la flash il faut conpremdre cela :

    • Elle possède 32 768 "Pages". (Il faut 15 bits pour compter jusqu'à 32768).
    • Chaque page contient nativement 264 "Octets" (ou 256 en mode binaire). Pour désigner un octet précis parmi 264, il faut 9 bits (car 8 bits ne vont que jusqu'à 255).

    Comme on veut envoyer une page est son adresse il nous faut au moins 24bits (15+9). Comme dans stdint.h le type uint24_t n'existe pas on utilise le type uint32_t. Donc on va se retrouver avec une adresse de ce style [ 00000000 ] [ Bits de Page ] [ Bits de Page ] [ Offset (0) ]. Comme on ne peut que envoyer un octet à la fois via le protocole ISP on envoit les bits de poids fort puis de poids faible. On a donc la fonction ci-dessous qui résume tout ça :

    void flash_write_page(uint16_t page, uint8_t *data, uint16_t length)
    {
        flash_wait_ready(); // S'assurer que la puce est prête
    
        spi_ON();
        spi_transfer(0x82); // Opcode d'écriture via Buffer 1
    
        // Adresse de la page (décalée de 9 bits, offset interne à 0)
        uint32_t adresse_complete = ((uint32_t)page << 9) | 0;
        // RAPPEL ->le premier octet est un octet null
        spi_transfer((adresse_complete >> 16) & 0xFF);
        spi_transfer((adresse_complete >> 8) & 0xFF);
        spi_transfer(adresse_complete & 0xFF);
    
        // Envoi des octets
        for (uint16_t i = 0; i < length; i++)
        {
            spi_transfer(data[i]);
        }
    
        spi_OFF(); // Remonter CS déclenche la gravure interne dans la puce !
    }
    

    L'effacement complet (Chip Erase) : Pour réinitialiser le robot de zéro, la Flash propose une commande de formatage global. Pour éviter une suppression accidentelle par du bruit sur le bus SPI, l'effacement nécessite l'envoi d'une séquence de sécurité stricte composée de 4 octets : 0xC7, 0x94, 0x80, 0x9A

    Datasheet_Flash_erase
    void display_flash_ids(void)
    void flash_erase_all(void)
    {
        spi_ON();
        spi_transfer(0xC7);
        spi_transfer(0x94);
        spi_transfer(0x80);
        spi_transfer(0x9A);
        spi_OFF();
    
        // On bloque le programme tant que la puce n'a pas fini son formatage
        flash_wait_ready();
    }
    // Attendre que la puce ait fini de graver la page précédente (Datasheet Table 15-4, Opcode 0xD7)
    void flash_wait_ready(void)
    {
        spi_ON();
        spi_transfer(0xD7); // Demande du registre de statut
    
        // On boucle tant que le bit 7 (0x80) vaut 0 (Busy)
        while ((spi_transfer(0x00) & 0x80) == 0)
            ;
    
        spi_OFF();
    }
    

    Test de lecture audio

    Pour vérifier que ce qu'on a gravé juste avant est réelement dans la flash, il faut faire une fonction test. Or peut avant la finition du code de la flash (et du code global pour une base stable de notre robot) l'encodeur audio s'est cassé, ce qui rend le buzzer completment inutilisable (même avant je pouvais quand même faire des test audios). Comme je ne peux pas resouder la pièce avant le rendu du projet, j'ai fait une fonction qui affiche sur l'ecran LCD les octets qui transitent en live par l'USB et la flash. Donc grâce au fichier audio_USB.c l'audio transite par paquet via l'USB. Et de même pour audio_flash.c mais via la flash. Donc on fait une fonction dans ecran.c qui récéptionne ces paquets et qui les affiche à l'écran (fonctions externes en plus qui permettent le fonctionnenet de celle-ci (voir git(#Archive GIT))) :

    void test_affichage_audio(void)
    {
        char ligne1[17];
        char ligne2[17];
    
        // % spéciale pour l'affichage sur l'écran
        sprintf(ligne1, "USB:%-12lu", debug_compteur_usb);
    
        sprintf(ligne2, "FLS:%-5lu", debug_compteur_flash);
    
        // Affichage Ligne 1 (Cases 0 à 15)
        for (int i = 0; i < 16; i++)
        {
            if (ligne1[i] == '\0')
                break;
            afficher_char(ligne1[i], ordre_cases[i]);
        }
    
        // Affichage Ligne 2 (Cases 16 à 31)
        for (int i = 0; i < 16; i++)
        {
            if (ligne2[i] == '\0')
                break;
            afficher_char(ligne2[i], ordre_cases[i + 16]);
        }
    }
    

    Cette vidéo montre exactement le comportement attendu : 1) On lance la transmission audio via l'USB. On fait le mouvement qui active l'audio -> l'audio de 33110 Ko (triste.raw) s'affiche à l'écran. 2) On coupe la transmission USB. On fait le mouvement et on récupère le même montant que pour l'USB, donc le même audio (gravé dans la flash précédemment).



    Historique des Scéances 2

    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
    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.
    18/05 : Problème complation
    • DFU récupéré.
    • Modification de la fonction test led.c.
    • Problème : Cela ne fonctionne pas.
      Soudure vérifiée patte par patte. La LED s'allume avec le multimètre connecté au pin de l'AVR et au GND.
      Code led.c vérifié plusieurs fois pour les PINS de la LED.
      Le makefile compile le .hex et le DFU indique une validation de transfert.

    • => TODO :
      • demander de l'aide au prof.
      • commencer à préparer le code pour l'écran pour le tester à la prochaine séance.
    20/05 : Soudure nouvelle carte
    • Impossible de savoir le problème -> soudure sur une nouvelle PCB.
    • Le DFU fonctionne et le code led.c compile bien sur la carte -> soudure de l'écran et du capteur ultra son.
    • Diffcultée : le code d'initalisation de l'écran reboot la carte quand on essaye de le téléverser -> Watchdog?

    • => TODO : Fixer le code de l'écran et faire des affichages avec le capteur ultra son.
    21/05 : Codes écran + capteur
    • Le watchdog était effectivement un des problèmes pour le fonctionnement de l'avr, et aussi la désactivation du JTAG pour l'utilisation des PORTF (explications aprofondis dans Programmation/ecran.
    • On avait aussi un problème de rétroéclairage qui etait tout simplement causé par l'inversion des fils LED+ et LED -.
    • Des simples codes de tests donc une parfaite fonctionnalité et synchronisation entre le capteur ultra son et l'écran.

    • => TODO :
      - Coder des animation sur l'écran.
      - Coder des interactions avec le capteurs.
      - Faire le lien entre les 2.
      - Commencer le code pour chercher un fichier MP3 (ou autre fichier audio) sur le PC -> LUFA + LIBUSB.
      - Demander à BOE, uP buzzer, booster, regulateur, level shifter et broche batterie.
    23/05 : Codes écran + capteur
      Avancement du code de l'écran et du capteur -> création de la fonction void afficher_distance(uint16_t distance_mm) qui affiche sur l'écran la distance en live d'un objet du capteur au milimètre près.
      Début des animation personnalisées -> création de frames.
    25/05 : animations + interactions
    • On créé des frames sur un site de pixel art (https://www.piskelapp.com/p/create/sprite/) ensuite on les convertit grâce à un script python, en tableaux à 3 dimension (nombre de frames * nombre de cases * nombre de lignes(FRAMES*4*8 pour un oeil)). On les range donc par animation et on affiche frame par frame dans le tableau d'animation associé.
    • Création d'un enum qui différencie les états des yeux : par défaut le mode normal tourne, ce qui correspond à une animation complète qui boucle (frame 1 -> frame max -> frame 1 ...)
    • Fonction avec le capteur ultra son pour détécter un mouvement brusque -> si mouvement brusque alors le robot pleur, sinon mode normal.
10/06 : Code servos
    Comme on a pas été fournit en servos moteurs on a pris nos propre servos moteurs. On a que à disposition des SG90 360° donc le code ne fonctionne pas comme on aurait voulu. Plutôt que d'indiquer une valeur pour orienter le ras du servos on indique une vitesse. Et on gère les distance avec le timer1 de l'AVR. Pendant le mouvement de base (quand il ne se passe rien) les servos font de petits mouvements haut /bas. QUand un mouvement est détecté, il se mettents à faire un gros mouvement (frustration).
11/06 : Tentative de réparation audio
    Après beaucoup de tentatives différents niveau software. Le buzzer faisait toujours le même son désagréable. On a fait des test aberrant en changeant une fréquence, en inversant les bits poids fort et poids faible etc. Donc aujourd'hui j'ai essayé de refaire les soudures, tester chaque connexion vérifier les voltages. Mais après beaucoup de tests également je n'ai rien trouvé de bizarre. L'erreur provient peut être du routage qui aurait dut respecter une disposition très précise des résistances ? Ou peut être de l'envoie des octets par le code qui n'est pas bon (peut probable car le .raw est correct et on peut entendre la disctinction sonore entre 2 son. C'est "juste" le son qui n'est pas bon ce qui ressemble beaucoup à du hardware.
13/06 : finitions matériels avant écriture wiki
  • On a arrété d'essayer de fixer le booster sur la carte : la footprint est trop petite
  • On mise en place de la batterie : chargement OK, Utilisation OK, mais comme attendu sans booster notre circuit fonctionne mal sur batterie.
  • Ajouts de CC entre le VCC et les alimentations (USB et batterie) pour utiliser la flash (car le booster faisant la transition entre VCC et tout le reste du circuit) -> Code pour utilisation de la flash.
14/06 : finitions écriture wiki

Rush final pour écrire le wiki. Les dernières sections de la partie programmation (audio à flash) sont faites dans un temps plus restreins. Donc moins détaillées / travaillées.

Carte électronique/Hardware

Kicad

Schéma électronique
Routage

3d front
3d back

Soudure front
Soudure back

Batterie

On connecte les fils de la batterie aux pins J7 (noir sur GND et rouge sur VBAT)

sur J2 pour charger la batterie on viendra court-circuiter le bus ALIM avec VUSB en utilisant un jumper sur les pin de J2. Et lorsqu'on la branche en usb, on alimente directement l'ALIM qui charge la batterie. Lors de la charge de la batterie, la led d'alimentation de la charge s'allume quand l'ALIM est alimentée. En mesurant la tension de la batterie pendent qu'elle charge on mesure bien 3.3V sur VBAT ce qui indique qu'elle se charge sans soucis.

Batterie charge 3.3V.jpeg

Batterie en charge, tension 3.3V


Problème rencontré : Comme nous n'avons pas réussit à mettre le booster de tension sur notre carte du à des conflits de footprint. Notre circuit principal n'est pas alilmenté par celui ci. Nous devons donc procéder à des court-circuits sur J3. sur J3 : On à d'abord court-circuité VBUS au test pointer 5V lorsque l'on est alimenté par l'USB. Puis, on court-circuite VBAT au test pointer 5V lorsque l'on utilise la batterie.

Pour pouvoir utiliser la flash on doit faire fonctionner le regulateur de tension. Comme nous n'avons pas de booster, pour que le régulateur délivre 3.3V on doit court-circuiter VCC avec VUSB lorsque l'USB est branché.

Regulateur 3.3V.jpeg

Regulateur de tension, 3.3V

Pour que la flash fonctionne sur batterie, on court-circuite VBAT au 3.3V photo 3.3V - regulateur -batterie puis USB (2 photos)

Flash sur batterie.jpg

Flash en fonctionnement sur batterie, 3.3V

Flash sur USB.jpeg

Flash en fonctionnement par USB, 3.3V

Booster de tension

Nous nous étions trompé sur la footprint de notre booster de tension. Cependant, le booster fourni utilise les même pins que notre booster voulu, donc nous avons décidé de souder chaque pin du booster de tension puis de l'envelopper de colle chaude pour éviter que nos pattes faites maison ne s'enlèvent (c'était le cas lorsque j'ai tenté de les souder sur la PCB car les pattes étaient trop fines pour être manipulées).

Booster de tension reçu
Footprint utilisé pour le booster de tension

Comme tout les pins coincide, le booster peut en théorie fonctionner. Alors nous avons tenté de le souder tout de même, d'une manière ou d'une autre.

Booster de tension + extensions de pattes
Booster de tension étendu et renforcé

Malheureusement je n'ai jamais réussit à dépasser les 4/6 pattes soudées car la pièce était si fragile que la déplacer d'un peu trop cassait la soudure. Une soudure d'ailleurs étroite du à la proximité des pattes sur la footprint.

Booster final.jpeg

Dernière tentative, 4 pattes sur 6 soudées

Tout ça aurait pu être éviter si j'avais lu la datasheet et la footprint correctement...

Necéssitée du booster de tension

Un test de la carte sur batterie sans les servos moteur montre que ça fonctionne parfaitement (même si l'écran est légèrmeent plus sombre) :


Un autre test de la carte sur batterie mais cette fois-ci avec les servos moteur montre que l'écran et le capteur ultra son ne fontionnent plus (ou mal) et que les servos moteurs tourent dans le vide sans suivre le code qui leurs est attribué. L'ajout de l'audio sur ce test aurait encore empiré les choses. Ce qui prouve bien l'utilité du booster de tension ici :


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.

Vidéo de présentation BMO

Vidéo d'utilisation de notre robot pour le mode "triste" lors d'un transfert audio via le PC : Média:2025-PSE-02-systeme-video.mp4

Vidéo du terminal en même temps que le test précédent qui montre bien le flux audio en temps réél : Média:BMO_lecture_pc_terminal.mp4

Objectifs

Réalisés

Programmeur AVR :

  • Capacité de lecture d'une flash cible
  • Capacité d'écriture sur une flash cible

PSE(BMO) :

  • Affichage dynamique, et création d'animations sur un écran LCD.
  • Fonctionnalités d'un capteur ultrason.
  • Fonctionnalités de servos moteurs 360 (code sur le git(#Archive GIT).
  • Lecture et écriture dans une flash de 8Mo.
  • Mise en place de 2 timer globaux permettant une gestion de temps ciblée en fonction des composants.
  • Mise en place d'une connexion USB avec un PC linux afin d’effectuer une transmission audio (en se servant du PC commme disk dur) ainsi que l'écriture de la flash.
  • Recharge d'une batterie LIPO et alimentation de la carte avec celle-ci.

Non réalisés

PSE(BMO) :

  • Utilisation de la batterie non réalisable sans booster de tension
  • Système audio défaillant
  • Mise en place de plusieurs Etats du robots (actuellement 2 états) faisant ainsi un projet plus complets (manque de temps + capteur ultra son seul est limité pour switch d'états)
  • Boîte en 3D pour donner vie à notre projet (optionnel)