« SE3Groupe2025-2 » : différence entre les versions
| Ligne 1 254 : | Ligne 1 254 : | ||
</ul> | </ul> | ||
== Historique des | == 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.
Vidéo très courte et en basse résolution de la carte en fonctionnement :
Programmation
Les tests finaux on été réalisé sur une arduino UNO
Teste de la carte
Pour tester notre carte on a fait des codes simples.
led_blink
// Clignote les 3 LEDS
#include <avr/io.h>
#include <util/delay.h>
#define BLINK_DELAY 50 // en milli secondes
// PB 5, 6, 7 correspond respectivement aux leds 1 , 2 , 3
int main()
{
// configurer les led en sortie :
DDRB |= (1 << 5); // PORTB 5e bit pour la LED1
DDRB |= (1 << 6); // PORTB 6e bit pour la LED2
DDRB |= (1 << 7); // PORTB 7e bit pour la LED3
for (int i = 0; i < 50; i++)
{
// led on
PORTB |= (1 << 5); // OR
PORTB |= (1 << 6);
PORTB |= (1 << 7);
_delay_ms(BLINK_DELAY);
// led off
PORTB &= ~(1 << 5); // AND + NOT
PORTB &= ~(1 << 6);
PORTB &= ~(1 << 7);
_delay_ms(BLINK_DELAY);
}
return 0;
}
boutons
#include <avr/io.h>
#include <util/delay.h>
#define b7 0b10000000
#define b6 0b01000000
#define L5 0b11011111
#define L6 0b10111111
#define L7 0b01111111
void config()
{
DDRB |= (1 << 5);
DDRB |= (1 << 6);
DDRB |= (1 << 7);
DDRC &= 0x00;
// Activation des résistances de Pull-Up internes sur le port C
PORTC |= (b6 | b7);
}
int lire_bouton(int bouton)
{
return (PINC & bouton) == 0;
}
void ecrire_LED(int LED, int etat)
{
switch (etat)
{
case 0:
PORTB &= LED; // Applique le masque avec le 0 pour éteindre
break;
case 1:
PORTB |= ~LED; // Inverse le masque pour avoir un 1 et allumer
break;
}
}
int main()
{
config();
while (1)
{
if (lire_bouton(b6) && lire_bouton(b7))
{
ecrire_LED(L7, 1);
ecrire_LED(L6, 0);
ecrire_LED(L5, 0);
}
else if (lire_bouton(b7))
{
ecrire_LED(L5, 1);
}
else if (lire_bouton((b6)))
{
ecrire_LED(L6, 1);
}
else
{
// On éteint tout si aucun bouton n'est pressé
ecrire_LED(L6, 0);
ecrire_LED(L5, 0);
ecrire_LED(L7, 0);
}
}
return 0;
}
LUFA
Pour injecter un nouveau code sur la carte(être sudo):
dfu-programmer atmega16u2 erase --force
dfu-programmer atmega16u2 flash --suppress-bootloader-mem file.hex
dfu-programmer atmega16u2 reset
Pour injecter le même code mais modifié sur la carte(être sudo):
dfu-programmer atmega16u2 erase
dfu-programmer atmega16u2 flash file.hex
dfu-programmer atmega16u2 reset
Connexion avec minicom
On a récupéré le dossier LUFA depuis le wiki du cours. Puis on a copié le fichier VirtualSerial situé dans lufa-LUFA-210130-NSI/Demos/Device/ClassDriver/VirtualSerial.
L'objectif ici est de communiquer grâce à la LUFA en USB sur minicom.
Dans VirtualSerial.c on modifie la fonction CheckJoystickMovement(void) par la fonction :
#define b7 0b10000000
#define b6 0b01000000
int Button_Status(int boutton)
{
return (PINC & boutton) == 0;
}
void CheckButtons(void)
{
char *ReportString = NULL;
static bool ActionSent = false;
if (Button_Status(b6) && Button_Status(b7))
{
ReportString = "both pressed\r\n";
_delay_ms(250);
ActionSent = false;
}
else if (Button_Status(b6))
{
ReportString = "b6 pressed\r\n";
_delay_ms(250);
ActionSent = false;
}
else if (Button_Status(b7))
{
ReportString = "b7 pressed\r\n";
_delay_ms(250);
ActionSent = false;
}
if ((ReportString != NULL) && (ActionSent == false))
{
ActionSent = true;
/* Write the string to the virtual COM port via the created character stream */
fputs(ReportString, &USBSerialStream);
/* Alternatively, without the stream: */
// CDC_Device_SendString(&VirtualSerial_CDC_Interface, ReportString);
}
}
On change VirtualSerial.h en conséquence.
On modifie la Makefile adapté à notre µP :
MCU = atmega16u2
ARCH = AVR8
BOARD = NONE
F_CPU = 16000000
F_USB = $(F_CPU)
OPTIMIZATION = s
TARGET = VirtualSerial
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS)
LUFA_PATH = ../../LUFA
CC_FLAGS = -DUSE_LUFA_CONFIG_HEADER -IConfig/
LD_FLAGS =
On compile le programme :
make
On injecte le code sur la carte:
sudo dfu-programmer atmega16u2 erase
sudo dfu-programmer atmega16u2 flash
sudo dfu-programmer atmega16u2 reset
Erasing flash... Success
Checking memory from 0x0 to 0x2FFF... Empty.
Checking memory from 0x0 to 0xEFF... Empty.
0% 100% Programming 0xF00 bytes...
[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] Success
0% 100% Reading 0x3000 bytes...
[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] Success
Validating... Success
0xF00 bytes written into 0x3000 bytes memory (31.25%).
et on observe bien un changement de nom de notre carte :
lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0408:a061 Quanta Computer, Inc. HD User Facing
Bus 001 Device 003: ID 8087:0026 Intel Corp. AX201 Bluetooth
Bus 001 Device 005: ID 046d:0aba Logitech, Inc. PRO X Wireless Gaming Headset
Bus 001 Device 012: ID 046d:c08b Logitech, Inc. G502 SE HERO Gaming Mouse
Bus 001 Device 016: ID 03eb:2044 Atmel Corp. LUFA CDC Demo Appli
Ensuite on configure minicom via : minicom -os
configuration minicom :
- "Serial port setup" ->
ENTRER A->/dev/ttyACM0->ENTRERE->C->ENTRERF->ENTRER- "Save setup as dfl" ->
ENTRER EchapeouExit
Puis on lance minicom via (être en sudo)
minicom
Pour retéléverser un programme sur la carte, il faut la repasser en DFU. Pour cela il suffit d'appuyer sur le bouton RESET
Resultat
Gestion personnalisée des Endpoints
Configuration IN et OUT
L'objectif est de s'affranchir de la classe CDC pour gérer directement les points d'accès (Endpoints). On définit une interface avec un point d'accès OUT (pour contrôler les LEDs depuis le PC) et un point d'accès IN (pour envoyer l'état de la carte au PC).
On modifie Descriptors.h pour définir les adresses :
/* Adresses des points d'accès */
#define LED_OUT_EPADDR (ENDPOINT_DIR_OUT | 1) // Endpoint 1 OUT
#define DATA_IN_EPADDR (ENDPOINT_DIR_IN | 2) // Endpoint 2 IN
/* Taille des paquets*/
#define LED_EPSIZE 8
Dans main.c, on remplace la gestion CDC par la configuration manuelle des points d'accès lors de l'énumération :
void EVENT_USB_Device_ConfigurationChanged(void)
{
// Configuration du point d'accès pour recevoir les ordres (LEDs)
Endpoint_ConfigureEndpoint(LED_OUT_EPADDR, EP_TYPE_INTERRUPT, LED_EPSIZE, 1);
// Configuration du point d'accès pour envoyer les données (Boutons/ISP)
Endpoint_ConfigureEndpoint(DATA_IN_EPADDR, EP_TYPE_INTERRUPT, LED_EPSIZE, 1);
}
Traitement des données OUT
Pour piloter les LEDs (même si elles ne sont pas encore soudées, le code cible le PORTB), on crée une fonction de traitement:
void ProcessLEDControl(void)
{
Endpoint_SelectEndpoint(LED_OUT_EPADDR);
if (Endpoint_IsOUTReceived())
{
if (Endpoint_IsReadWriteAllowed())
{
// On lit l'octet envoyé par le PC
uint8_t LEDMask = Endpoint_Read_8();
// On l'applique aux LEDs (PORTB)
PORTB = LEDMask;
}
Endpoint_ClearOUT();
}Atmel-0943-In-System-Programming_ApplicationNote_AVR910
}
Traitement des données IN
On récupère l'état des boutons dans le PINC :
void ButtonsStatus(void)
{
Endpoint_SelectEndpoint(DATA_IN_EPADDR);
if (Endpoint_IsReadWriteAllowed())
{
Endpoint_Write_8(PINC & 0xC0); //pour activer les résistances de pull up sur b6 et b7.
Endpoint_ClearIN();
}
}
Modification du Makefile
On retire les sources de la classe CDC (LUFA_SRC_USBCLASS) car nous utilisons désormais les fonctions "Low Level" de la LUFA.
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB)
Programme PC avec libusb
Maintenant que la carte est configurée avec des Endpoints bruts (IN et OUT), nous devons écrire un programme C fonctionnant sous Linux pour communiquer avec elle. L'outil standard pour cela est la bibliothèque libusb-1.0.
Code de test IN/OUT (main_pc.c)
Ce programme a pour but de valider la liaison USB bas niveau. Il va :
Ouvrir la communication avec le périphérique (VID/PID).
Réclamer l'interface.
Envoyer un ordre OUT pour allumer les broches des LEDs (PORTB).
Lire un état IN pour récupérer l'état des boutons (PORTC).
Ouvrir la communication avec le périphérique
libusb_device_handle *init_usb(void)
{
libusb_device_handle *dev_handle = NULL;
if (libusb_init(NULL) < 0)
{
printf("Erreur d'initialisation de libusb\n");
return NULL;
}
dev_handle = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);
if (dev_handle == NULL)
{
printf("Impossible d'ouvrir le périphérique (03EB:2044). Avez-vous utilisé sudo ?\n");
libusb_exit(NULL);
return NULL;
}
if (libusb_kernel_driver_active(dev_handle, 0) == 1)
{
libusb_detach_kernel_driver(dev_handle, 0);
}
return dev_handle;
}
Réclamer l'interface
int reclamer_interface(libusb_device_handle *dev_handle)
{
int r = libusb_claim_interface(dev_handle, 0);
if (r < 0)
{
printf("Erreur de réclamation de l'interface : %s\n", libusb_error_name(r));
libusb_close(dev_handle);
libusb_exit(NULL);
return -1;
}
return 0;
}
Configuration LED en OUT
On veut allumer les LEDS avec les touches du clavier en LIVE. Pour cela on utilise la bibliothèque termios.h utilisé dans la fonction kbhit().
printf("\n=== MODE LIVE ACTIVÉ ===\n");
printf("Touches :\n");
printf(" [A] : Basculer LED 1 (PB5)\n");
printf(" [Z] : Basculer LED 2 (PB6)\n");
printf(" [E] : Basculer LED 3 (PB7)\n");
printf(" [Q] : Quitter\n");
printf("========================\n\n");
unsigned char data_out = 0x00;
unsigned char last_data_in = 0xFF; // Pour mémoriser l'ancien état des boutons
int actual_length;
int running = 1;
while (running)
{
if (kbhit())
{
char c = getchar();
int send_update = 0;
switch (c)
{
case 'a':
data_out ^= (1 << 5);
send_update = 1;
break; // Bascule PB5
case 'z':
data_out ^= (1 << 6);
send_update = 1;
break; // Bascule PB6
case 'e':
data_out ^= (1 << 7);
send_update = 1;
break; // Bascule PB7
case 'q':
running = 0;
break;
}
if (send_update)
{
// Envoi de la nouvelle configuration OUT
int r = libusb_interrupt_transfer(dev_handle, EP_OUT, &data_out, 1, &actual_length, 50);
if (r == 0)
{
printf("[PC] Ordre envoyé : 0x%02X\n", data_out);
}
else
{
printf("[PC] Erreur OUT : %s\n", libusb_error_name(r));
}
}
}
Configuration boutons en IN
Ce bout de code permet d'afficher l'état des boutons.
unsigned char data_in = 0;
// On met un timeout très court (10ms) pour ne pas bloquer la boucle
int r = libusb_interrupt_transfer(dev_handle, EP_IN, &data_in, 1, &actual_length, 10);
if (r == 0 && actual_length == 1)
{
// On n'affiche que si l'état des boutons a changé (pour éviter de spammer la console)
if (data_in != last_data_in)
{
printf("[CARTE] État PORTC : 0x%02X -> ", data_in);
if ((data_in & (1 << 6)) == 0)
printf("B6 pressé ! ");
if ((data_in & (1 << 7)) == 0)
printf("B7 pressé ! ");
if ((data_in & (1 << 6)) != 0 && (data_in & (1 << 7)) != 0)
printf("Relâchés.");
printf("\n");
last_data_in = data_in;
}
}
// Petite pause pour ne pas surcharger le processeur du PC (10 millisecondes)
usleep(10000);
}
Résultat
sudo ./main_pc
Terminal
=== MODE LIVE ACTIVÉ ===
Touches :
[A] : Basculer LED 1 (PB5)
[Z] : Basculer LED 2 (PB6)
[E] : Basculer LED 3 (PB7)
[Q] : Quitter
========================
[CARTE] État PORTC : 0xC0 -> Relâchés.
[CARTE] État PORTC : 0x40 -> B7 pressé !
[CARTE] État PORTC : 0xC0 -> Relâchés.
[CARTE] État PORTC : 0x80 -> B6 pressé !
[CARTE] État PORTC : 0xC0 -> Relâchés.
[CARTE] État PORTC : 0x00 -> B6 pressé ! B7 pressé !
[CARTE] État PORTC : 0x40 -> B7 pressé !
[CARTE] État PORTC : 0xC0 -> Relâchés.
a[PC] Ordre envoyé : 0x20
z[PC] Ordre envoyé : 0x60
e[PC] Ordre envoyé : 0xE0
q
Fermeture du programme...
Vidéo après soudure des LEDS
Lecture ISP
L'objectif est maintenant d'utiliser notre communication IN/OUT pour envoyer des commandes SPI à un microcontrôleur cible afin de lire sa signature (Device ID).
Côté Carte (LUFA)
On modifie notre fonction main.c, afin d'inclure la lecture ISP.
- On initialise le SPI
void spi_init(void) { SPI_DDR |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS); // Définition des sorties SPI_DDR &= ~(1 << SPI_MISO); // Définition de l'entrée SPI_PORT |= (1 << SPI_SS); // Désactivation du périphérique SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0); // Activation SPI (SPE) en état maître (MSTR) SPSR &= ~(1 << SPI2X); // horloge F_CPU/128 (SPI2X=0, SPR1=1,SPR0=1) }
- On fait le transfert l'octet. On place l'octet à envoyer dans
SPDR, on attend que l'octet arrive chez l'esclave, puis on renvoie ce que nous envoie l'esclave.uint8_t spi_transfer(uint8_t data) { SPDR = data; // Octet a envoyer while (!(SPSR & (1 << SPIF))) ; // Attente fin envoi (drapeau SPIF du statut) return SPDR; }
- On réécrit la fonction qui gère la commande OUT du PC pour inclure le paquet SPI. Si l'octet ne vaut pas
0x01alors on ne fait rien, sinon on applique au LED la valeurs du paquet OUT, comme précédemment.uint8_t commande_PC = 0; void ProcessOUT(void) { Endpoint_SelectEndpoint(DATA_OUT_EPADDR); if (Endpoint_IsOUTReceived()) { if (Endpoint_IsReadWriteAllowed()) { // On lit l'octet envoyé par le PC commande_PC = Endpoint_Read_8(); if (commande_PC != 0x01) // On l'applique aux LEDs (PORTB) PORTB = commande_PC; } Endpoint_ClearOUT(); } }
- Pour la gestion du IN : si le paquet n'est pas
0x01alors on lit l'état des boutons comme précemment. Sinon on active le SPI, et on applique le protocole ISP. Ce protocole consiste à envoyer une série de 4 octets dans un ordre bien spécifique, comme indiqué dans la datasheetAtmel-0943-In-System-Programming_ApplicationNote_AVR910qu'on peut retrouver dans le dossierdatasheet.Que se passe-t-il pendant les 4 octets du protocole AVR ISP ?
Le protocole ISP d'Atmel est conçu pour que chaque instruction fasse exactement 32 bits (4 octets).- Octet 1 (0x30) : Le Maître dit "Je veux lire la signature". L'Esclave reçoit ça, mais ne renvoie rien pour le moment.
- Octet 2 (0x00) : Le Maître envoie l'adresse de poids fort (inutile ici). L'Esclave est en train de comprendre la commande
0x30. - Octet 3 (0x00, 0x01 ou 0x02) : Le Maître demande précisément l'adresse 0, 1 ou 2 de la signature. L'Esclave va chercher l'information dans sa mémoire.
- Octet 4 (0x00) : L'Esclave a préparé la réponse (la signature), mais il ne contrôle pas l'horloge ! Il a besoin que le Maître génère 8 coups d'horloge pour que la réponse puisse voyager sur le fil
MISO. Le Maître envoie donc un octet factice (0x00) uniquement pour faire tourner l'horloge. Pendant ce temps, la cible glisse sa réponse sur le filMISO. C'est pour ça qu'on faituint8_t sig_n = spi_transfer(0x00);! Le dernier octet retourné par la cible est la donnée recherchée.
Pourquoi 3 octets de signature et qu'est-ce qu'on récupère ?
Chaque microcontrôleur AVR possède une "Signature" unique gravée en usine, composée de 3 octets (situés aux adresses 0, 1 et 2).- Octet 0 : Identifie le fabricant. Atmel est toujours
0x1E. - Octet 1 : Identifie la famille et la mémoire.
- Octet 2 : Identifie le modèle exact. Par exemple, le AT90S1200 a pour signature 0x1E 0x90 0x01.
Voici le tableau qui résume ce protocole. On retrouve en ligne les 3 octets qu'on reçoit et en colonne les 4 octets nécéssaires à la récupération de chacun de ces 3 octets.
Voici la fonction qui met en place le protocole :
void ProcessIN(void) { Endpoint_SelectEndpoint(DATA_IN_EPADDR); if (Endpoint_IsReadWriteAllowed()) { if (commande_PC != 0x01) Endpoint_Write_8(PINC & 0xC0); else { // Mode ISP : Le PC a demandé les identifiants spi_ON(); // (3.2 datasheet) Enable Memory Access spi_transfer(0xAC); spi_transfer(0x53); spi_transfer(0x00); spi_transfer(0x00); // --- Lecture du 1er octet de signature (Adresse 0x00) --- spi_transfer(0x30); spi_transfer(0x00); spi_transfer(0x00); uint8_t sig_1 = spi_transfer(0x00); // La cible répond pendant cet octet // --- Lecture du 2ème octet de signature (Adresse 0x01) --- spi_transfer(0x30); spi_transfer(0x00); spi_transfer(0x01); uint8_t sig_2 = spi_transfer(0x00); // --- Lecture du 3ème octet de signature (Adresse 0x02) --- spi_transfer(0x30); spi_transfer(0x00); spi_transfer(0x02); uint8_t sig_3 = spi_transfer(0x00); spi_OFF(); // On envoie les 3 octets au PC ! Endpoint_Write_8(sig_1); Endpoint_Write_8(sig_2); Endpoint_Write_8(sig_3); // On efface la commande pour ne pas spammer la cible au prochain tour commande_PC = 0; } Endpoint_ClearIN(); } }
- Read Low Byte : Commande
0x20suivi de l'adresse. - Read High Byte : Commande
0x28suivi de l'adresse.
Le format d'une commande est toujours de 4 octets:
- Octet 1 :
0x20(Low) ou 0x28 (High). - Octets 2 et 3 : L'adresse du mot (poids fort puis poids faible)
- Octet 4 : Donnée fictive (
0x00) pour récupérer la réponse de la cible. - Modification de la fonction
ProcessOUT(): On prépare la demande du codePC. Une fois que celui-ci aura envoyé0X02, le programmeur AVR "sait" qu'il va recevoir 2 octets contenant l'adresse désirée par le PC.uint16_t flash_address = 0; void ProcessOUT(void) { Endpoint_SelectEndpoint(DATA_OUT_EPADDR); if (Endpoint_IsOUTReceived()) { if (Endpoint_IsReadWriteAllowed()) { // On lit l'octet envoyé par le PC commande_PC = Endpoint_Read_8(); if (commande_PC == 0x02) // Demande de lecture Flash { flash_address = ((uint16_t)Endpoint_Read_8() << 8); // Poids fort flash_address |= Endpoint_Read_8(); // Poids faible } else if (commande_PC != 0x01) { PORTB = commande_PC; // Gestion des LEDs } } Endpoint_ClearOUT(); } } } }
- Ajout du
ifdans la fonctionProcessIN(): Comme indiqué précédemment, on va lire la flash à l'adresse demandée par le PC. Comme dansdescriptor.hon a mis#define DATA_SIZE 8(plus lent mais plus intéressant pour comprendre) et qu'un mot fait 2 octets, alors cela veut dire qu'on peut lire jusqu'à 4 mots àflash_address + ipour optimiser la bande passante USB (d'ou le i jusqu'à 4 dans lefor). Ensuite on suit le protocole expliqué au dessus pour la lecture de la flash pour chaque mots lu.if (commande_PC == 0x02) { spi_ON(); // (3.2 datasheet) Enable Memory Access spi_transfer(0xAC); spi_transfer(0x53); spi_transfer(0x00); spi_transfer(0x00); for (uint16_t i = 0; i < 4; i++) { uint16_t current_addr = flash_address + i; // Lecture du Low Byte spi_transfer(0x20); spi_transfer(current_addr >> 8); // On décale de 8 les 2 octets donc on a 0x00HIGH = 0xHIGH spi_transfer(current_addr & 0xFF); // On fait un & à l'adresse avec 0x00FF donc on a 0x00LOW = 0xLOW uint8_t data_low = spi_transfer(0x00); // Lecture du High Byte spi_transfer(0x28); spi_transfer(current_addr >> 8); spi_transfer(current_addr & 0xFF); uint8_t data_hight = spi_transfer(0x00); // envoie au PC Endpoint_Write_8(data_low); Endpoint_Write_8(data_hight); } spi_OFF(); commande_PC = 0; // On a fini d'envoyer le bloc }
- Demander l'adresse
0x0000(la carte renvoie les mots 0, 1, 2, 3). - Demander l'adresse
0x0004(la carte renvoie les mots 4, 5, 6, 7). - Demander l'adresse
0x0008(la carte renvoie les mots 8, 9, 10, 11). - ...
- flash_read.c :
printf("\n=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===\n\n"); int r; int actual_length; unsigned char data_out[3]; unsigned char data_in[8]; // Tampon de 8 octets pour recevoir 4 mots // Boucle pour lire 32 mots (64 octets), par pas de 4 mots for (uint16_t addr = 0; addr < 32; addr += 4) { // Préparation de la commande OUT (0x02 + Addr_High + Addr_Low) data_out[0] = 0x02; data_out[1] = (addr >> 8) & 0xFF; // Poids fort de l'adresse data_out[2] = addr & 0xFF; // Poids faible de l'adresse // Demande de lecture en OUT r = libusb_interrupt_transfer(dev_handle, EP_OUT, data_out, 3, &actual_length, 100); if (r != 0) { printf("Erreur lors de la demande pour l'adresse 0x%04X : %s\n", addr, libusb_error_name(r)); continue; } // Réception IN (avec supression des paquets "boutons" d'1 octet) do { r = libusb_interrupt_transfer(dev_handle, EP_IN, data_in, 8, &actual_length, 100); } while (r == 0 && actual_length == 1); // Ignore les paquets de boutons // Affichage du bloc reçu if (r == 0 && actual_length == 8) { printf("Adresse 0x%04X : ", addr); for (int i = 0; i < 8; i += 2) { printf("%04X ", (data_in[i + 1] << 8) | data_in[i]); } printf(" \n"); } else { printf("Erreur de lecture a l'adresse 0x%04X (reçu %d octets)\n", addr, actual_length); } }
- Activation de la mémoire (Enable Memory Access) : Envoi de la séquence
0xAC 0x53 0x00 0x00obligatoire pour déverrouiller l'accès. - Chargement du Low Byte) : Utilisation de la commande
0x40suivie de l'adresse et de l'octet faible pour le placer dans le tampon. - Chargement du High Byte) : Utilisation de la commande
0x48suivie de l'adresse et de l'octet fort. - Déclenchement du Write Page : Utilisation de la commande
0x4Csuivie de l'adresse pour graver physiquement le tampon dans la Flash. - Attente : Délai de quelques millisecondes (environ 5 ms) pour laisser le matériel terminer l'écriture avant de relâcher le
RESET. :: indique le début d'un ligne10: Nombre d'octets de données sur cette ligne (Ici0x10=16octets).0000: L'adresse de départ en Flash pour cette ligne.00: Type de ligne (00= données à flasher,01= fin du fichier).0C9434000C94...: Les données du programme- écriture
- lecture On lit bien ce qu'on a écrit juste avant.
- Problème : le programmateur n'est pas détecté en USB. Un erreur est détectée ce qui montre qu'il y a une connexion, mais n'apparaît pas dans
lsusb. - Programmable : La connexion en ISP en revanche fonctionne. On pourra donc programmer les leds et les boutons avec une UNO. Voir dans le futur si on peut fixer le problème de l'USB pour rendre le programmateur fonctionnel.
- ajout d'un premier
blink_led.c(brouillon) au git(#Archive GIT) - TESTS : Le code
blinkfonctionne mais pas le codeboutons-> faut contact / pbs de soudure sur les pattes - 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.
TCR1Bconfigure le timer1 de l'AVR soitTCNT1. MettreCS11à 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 de8MHz, ce qui nous donne .
La vitesse du son dans l'air à20°Cest d'environ340 m/s. Cela correspond à0.034 cm/µs. Soit (car aller-retour) donc . - 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 (
- La libération de l'interface de débogage JTAG Par défaut, en sortie d'usine, le microcontrôleur réserve les broches
- La Ligne 1 s'étend de l'adresse
0x00à0x0F. - La Ligne 2 commence à l'adresse
0x40et va jusqu'à0x4F. - On envoi un quartet soit 4 bits sur nos fameuses 4 entrées de l'écran.
- On autorise l'écriture sur l'écran
- On envoi le quartet de poids fort puis de poids faible
- On se place sur la case qu'on veut écrire. (0x80 : cf datasheet page 12, table des commandes)
- On écrit la data sur cette case.
- 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
- 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
- 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
- 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
- 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).
- 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.
- 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
- 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.
- 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).
- Modification initiale du wikicode
- Brainstorming pour se décider sur un projet
- Encodeur: voir si on peut en utiliser un pour régler le son/luminosité écran.
- Connecteur ISP (le même que le programatteur)
- USB-A, condensateurs de découplage, condensateur VUSB.
- Chargeur Lipo (le même que celui du wiki donc à modifier selon notre schéma).
- haut parleur : 8 entrée DAC.
- Ecran : 11 entrée (8 affichage et 3 gestion).
- Cerveaux moteur optionnels si on a pas assez de ports
- Voir comment connecter la flash.
- Correction de la V1, et ajout des connecteurs pour le chargement de la batterie/ choix de l'alimentation / connecteur batterie
- Routage : début, du placement des composants. A FAIRE:
- Demander les composants physique tel que le buzzer et le cerveau moteur
- On a enlevé des pins sur l'AVR qui étaient pris inutilement par la batterie LIPO => voir si on ne peut pas rajouter un cerveau moteur et des boutons à la place.
- Tension : Analyser les composants qui nécéssitent 5V de tension pour ajouter un booster de tension pour garder le 5V en batterie (3,3V). Inversement mettre un régulateur de tension sur les composants fonctionnant en 3,3V, lors de l'alimentation en 5v. Convertisseur : TPS61033-Q1
- Ajout du booster de tension TPS61033-Q1 pour l'écran, le LM386, le capteur ultrason et les 2 cervosmoteur.
- Ajout d'un régulateur de tension pour la flash.
- Ajout de la section "Détail schématique" et première explication du boost et régulateur de tension.
- A FAIRE:
- Demander les composants physique disponibles : buzzer et les cerveaux moteur
- Demander une vérification de la V2 pour avancer sur le routage
- Correction de la V2, changement de booster et du regulateur pour simplifier le comosant et la commande sur Farnell. A FAIRE
- Modifier la section "détail de la schématique" en conséquence et revoir le dimsensionnement des composants.
- Ajout du shifter pour la flash.
- V1 routage : Alimentation en 0.8mm, pas d'angles droits, DRC au maximum, annotation avec du texte en silkscreen.
- Soudures test du uP : ISP OK
- TODO : Faut contact sur l'USB. Mais carte détéctée en DFU donc pas de soucis.
- Le problème du DFU ne vient pas de l'USB, mais du condensateur utilisé sur le port UCAP du uP.
- Problème : Avec les 3 commandes de DFU classiques, le programme ne voulait pas se téléverser. Donc j'ai utilisé
--forcesur leerase. Malheureusement cela a écrasé le code DFU sur la carte.
=> TODO :- remettre le code DFU sur l'atmega
- commencer à préparer le code pour l'écran pour le tester à la prochaine séance.
- 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.
Codeled.cvérifié plusieurs fois pour les PINS de la LED.
Le makefile compile le .hex et le DFU indique une validation de transfert. - demander de l'aide au prof.
- commencer à préparer le code pour l'écran pour le tester à la prochaine séance.
- Impossible de savoir le problème -> soudure sur une nouvelle PCB.
- Le DFU fonctionne et le code
led.ccompile 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?
- 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.
- 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
enumqui 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.
Côté PC (libusb)
La fonction isp_ids.c permet l'envoie d'une commande qui implique la réception des identifiants ISP de l'AVR. Il enverra la commande 0x01, puis écoutera le point d'accès IN pour récupérer et afficher les 3 octets de la signature.
OUT
On envoie simplement l'octet 0x01 pour activer le code ISP sur l'avr (côté LUFA).
// ----OUT----
unsigned char data_out = 0x01; // octet à envoyer pout l'ISP (choix)
int actual_length;
int r = libusb_interrupt_transfer(dev_handle, EP_OUT, &data_out, 1, &actual_length, 50);
if (r == 0)
{
printf("[PC] Ordre envoyé : 0x%02X\n", data_out);
}
else
{
printf("[PC] Erreur OUT : %s\n", libusb_error_name(r));
}
IN
Pour récupérer les 3 octets, on doit d'abord ignorer le premier paquet envoyé par les boutons (do/while). Ensuite on imprime les 3 octets reçu et on indique si c'est un AVR atmel qu'on connecte en ISP.
// ----IN ----
unsigned char data_in[3] = {0, 0, 0};
printf("[PC] Attente de la réponse (Purge des anciens paquets)...\n");
// Boucle pour ignorer les paquets de 1 octet (les boutons)
// On boucle tant que le transfert réussit ET que la taille est de 1
do
{
r = libusb_interrupt_transfer(dev_handle, EP_IN, data_in, 3, &actual_length, 100);
} while (r == 0 && actual_length == 1);
// Sortie de boucle : on a soit une erreur, soit notre paquet de 3 octets !
if (r == 0 && actual_length == 3)
{
printf("Signature reçue avec succès : 0x%02X 0x%02X 0x%02X\n", data_in[0], data_in[1], data_in[2]);
if (data_in[0] == 0x1E)
{
printf("-> Fabricant : Atmel reconnu !\n");
}
}
else
{
printf("Erreur ou Timeout (reçu %d octets) : %s\n", actual_length, libusb_error_name(r));
}
Résultats
sudo ./isp_ids
Résultat avant soudure de l'ISP :
[PC] Ordre envoyé : 0x01
[PC] Attente de la réponse (Purge des anciens paquets)...
Signature reçue avec succès : 0xFF 0xFF 0xFF
Fermeture du programme...
Résultat après soudure de l'ISP : Test sur une Arduino UNO
[PC] Ordre envoyé : 0x01
[PC] Attente de la réponse (Purge des anciens paquets)...
Signature reçue avec succès : 0x1E 0x95 0x0F
-> Fabricant : Atmel reconnu !
Fermeture du programme...
Flash
L'objectif est de récupérer l'intégralité du contenu de la mémoire flash de la cible. Celle-ci étant volumineuse, le transfert se fait par blocs de 8 octets (soit 4 mots de 16 bits).
Envoyer bloc par bloc sur le IN
Modification de la LUFA (main.c)
Du côté de la LUFA, nous modifions le main.c pour gérer un adressage sur 16 bits et utiliser
les commandes de lecture flash définies dans la datasheet (AVR910).
L'adresse de lecture est envoyée par le PC via le point d'accès OUT, puis la carte lit 8 octets en
SPI et les renvoie via le point d'accès IN.
Comprendre la lecture de la Flash via ISP :
-
Selon le document technique (Table 3-5), la lecture de la mémoire flash se fait mot par mot (un mot = 2 octets).
CODE (main.c):
Récéption PC (flash_read.c)
Comme on a programmé notre AVR pour qu'elle récupère 4 mots par adresses données, il faut qu'on lui donne des adresses indentées de 4. Par exemple si on veut lire 64 octets de la flash (soit 32 mots), on boucle de cette manière :
-
Le code ci-dessous effectue cette boucle afin de récupérer 64 octets dans la flash cible.
RESULTATS
sudo ./flash_read
Avant soudure :
=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===
Adresse 0x0000 : FFFF FFFF FFFF FFFF
Adresse 0x0004 : FFFF FFFF FFFF FFFF
Adresse 0x0008 : FFFF FFFF FFFF FFFF
Adresse 0x000C : FFFF FFFF FFFF FFFF
Adresse 0x0010 : FFFF FFFF FFFF FFFF
Adresse 0x0014 : FFFF FFFF FFFF FFFF
Adresse 0x0018 : FFFF FFFF FFFF FFFF
Adresse 0x001C : FFFF FFFF FFFF FFFF
Fermeture du programme...
Après soudure (UNO):
=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===
Adresse 0x0000 : 940C 005D 940C 0085
Adresse 0x0004 : 940C 0085 940C 0085
Adresse 0x0008 : 940C 0085 940C 0085
Adresse 0x000C : 940C 0085 940C 0085
Adresse 0x0010 : 940C 0085 940C 0085
Adresse 0x0014 : 940C 0085 940C 0085
Adresse 0x0018 : 940C 0085 940C 0085
Adresse 0x001C : 940C 0085 940C 0085
Fermeture du programme...
Mise à jour de la flash (Page Mode)
L'objectif est d'écrire de nouvelles données dans la mémoire Flash de la cible. Étant donné que les cibles modernes comme l'ATmega328P (Arduino) ne supportent plus l'écriture octet par octet (Byte Mode), nous nous appuyons sur la Datasheet de l'ATmega328P (Section Serial Downloading) pour utiliser le mode de programmation par page (Page Mode).
Mécanisme d'écriture SPI
L'écriture dans la Flash nécessite désormais de précharger les données dans un tampon temporaire (Page Buffer) avant de déclencher la gravure physique de la page. Pour simplifier notre programme PC, nous chargeons un seul mot (16 bits) dans le tampon, puis nous forçons immédiatement la gravure.
La session se fait en un seul cycle d'activation de la cible (spi_ON) :
Implémentation LUFA (main.c)
Pour pourvoir écrire dans la flash cible il faut effacer la flah. Pour ça on a fait la fonction chip_erase() qui suit le protocole du tableau.
void chip_erase(void)
{
spi_ON();
_delay_ms(5);
// (3.2 datasheet) Enable Memory Access
spi_transfer(0xAC);
spi_transfer(0x53);
spi_transfer(0x00);
spi_transfer(0x00);
// (3.4.3 datasheet) erase
spi_transfer(0xAC);
spi_transfer(0x80);
spi_transfer(0x00);
spi_transfer(0x00);
_delay_ms(5);
spi_OFF();
}
Dans notre point d'accès OUT, nous créons la commande 0x03. Le programme PC enverra un paquet
contenant l'adresse cible et les deux octets à écrire. La carte exécute ensuite la séquence SPI. On crée la commande 0x04 pour erase la flash.
else if (commande_PC == 0x03) // Ecriture de flash (flash_rw.c)
{
flash_address = ((uint16_t)Endpoint_Read_8() << 8); // Poids fort
flash_address |= Endpoint_Read_8(); // Poids faible
uint8_t low = Endpoint_Read_8();
uint8_t high = Endpoint_Read_8();
spi_ON();
_delay_ms(5); // Temps de réveil cible
// (3.2 datasheet) Enable Memory Access
spi_transfer(0xAC);
spi_transfer(0x53);
spi_transfer(0x00);
spi_transfer(0x00);
// Low byte
spi_transfer(0x40);
spi_transfer(flash_address >> 8);
spi_transfer(flash_address & 0xFF);
spi_transfer(low);
// High byte
spi_transfer(0x48);
spi_transfer(flash_address >> 8);
spi_transfer(flash_address & 0xFF);
spi_transfer(high);
// Write
spi_transfer(0x4C);
spi_transfer(flash_address >> 8);
spi_transfer(flash_address & 0xFF);
spi_transfer(0x00);
_delay_ms(5); // Attente de la gravure physique
spi_OFF();
}
else if (commande_PC == 0x04)
{
chip_erase();
}
PC(flash_rw.c)
Pour tester l'écriture, on fait une fonction flash_rw.c qui écrit dans la flash cible puis la lit.
Pour écrire dans la flash cible, on doit d'abord effacer la flash, puis on envoi l’octet 0x03 (choisit par défaut). Ensuite on
envoi l'adresse ou on veut écrire la donnée; en 2 octets HIGH/LOW, et de même pour la donnée à envoyer.
int r;
int actual_length;
// =========================================================
// 0. EFFACEMENT DE LA CIBLE (Chip Erase)
// =========================================================
printf("\n=== EFFACEMENT DE LA PUCE ===\n");
unsigned char cmd_erase = 0x04;
r = libusb_interrupt_transfer(dev_handle, EP_OUT, &cmd_erase, 1, &actual_length, 500);
if (r == 0)
{
printf("Puce effacée avec succès !\n");
}
else
{
printf("Erreur lors de l'effacement : %s\n", libusb_error_name(r));
}
// =========================================================
// 1. TEST D'ÉCRITURE (Byte Mode)
// =========================================================
printf("\n=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===\n\n");
unsigned char data_write[5]; // Paquet de 5 octets pour l'écriture
// On boucle sur les 4 premières adresses de mots (0, 1, 2, 3)
for (uint16_t addr = 0; addr < 5; addr++)
{
// On invente une donnée factice pour le test (ex: 0xA000, 0xA001...)
uint16_t fake_data = 0xA000 + addr;
// Préparation du paquet selon le protocole LUFA
data_write[0] = 0x03; // Commande d'écriture
data_write[1] = (addr >> 8) & 0xFF; // add HIGH
data_write[2] = addr & 0xFF; // add LOW
data_write[3] = fake_data & 0xFF; // fake LOW
data_write[4] = (fake_data >> 8) & 0xFF; // fake HIGH
// Envoi de l'ordre d'écriture en OUT
r = libusb_interrupt_transfer(dev_handle, EP_OUT, data_write, 5, &actual_length, 100);
if (r == 0)
{
printf("Ordre d'écriture envoyé -> Adresse: 0x%04X | Donnée: 0x%04X\n", addr, fake_data);
}
else
{
printf("Erreur d'écriture à l'adresse 0x%04X : %s\n", addr, libusb_error_name(r));
}
}
RESULTATS
sudo ./flash_rw
Avant soudure :
=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===
Ordre d'écriture envoyé -> Adresse: 0x0000 | Donnée: 0xA000
Ordre d'écriture envoyé -> Adresse: 0x0001 | Donnée: 0xA001
Ordre d'écriture envoyé -> Adresse: 0x0002 | Donnée: 0xA002
Ordre d'écriture envoyé -> Adresse: 0x0003 | Donnée: 0xA003
RQ : la lecture renvoie quand même que des mots en FFFF
Après soudure :
=== EFFACEMENT DE LA PUCE ===
Puce effacée avec succès !
=== ECRITURE DE LA FLASH (Test : 4 premiers mots) ===
Ordre d'écriture envoyé -> Adresse: 0x0000 | Donnée: 0xA000
Ordre d'écriture envoyé -> Adresse: 0x0001 | Donnée: 0xA001
Ordre d'écriture envoyé -> Adresse: 0x0002 | Donnée: 0xA002
Ordre d'écriture envoyé -> Adresse: 0x0003 | Donnée: 0xA003
Ordre d'écriture envoyé -> Adresse: 0x0004 | Donnée: 0xA004
=== LECTURE DE LA FLASH (Test : 64 premiers octets) ===
Adresse 0x0000 : A000 A001 A002 A003
Adresse 0x0004 : A004 FFFF FFFF FFFF
Adresse 0x0008 : FFFF FFFF FFFF FFFF
Adresse 0x000C : FFFF FFFF FFFF FFFF
Adresse 0x0010 : FFFF FFFF FFFF FFFF
Adresse 0x0014 : FFFF FFFF FFFF FFFF
Adresse 0x0018 : FFFF FFFF FFFF FFFF
Adresse 0x001C : FFFF FFFF FFFF FFFF
Fermeture du programme...
Assemblage(résultats)
L'objectif est d'utiliser les fonctions test et de les modifier afin de d'avoir un seul programme main qui contrôle tout. On l'appel prog.c
La première chose est de retirer la gestion USB dans flash_read.c et flash_read.c et de la séparer dans une fonction dev_handler.c.
On change la fonction flash_rw.c en flash_write.c pour seulement écrire avec.
Enfin on compile tout avec un Makefile et on exécute le code avec une commande intelligente.
Ici on flashera un code basique blink.c qui fera clignoter la LED PB5 de l'arduino UNO.
Comment flasher un programme C ?
Pour flasher un fichier C il faut écrire le .hex associé. La compilation d'un .hex est détaillée dans "Programateur/libusb/code_a_televerser/Makefile".
Un fichier .hex est composé de lignes d'octets qui ont une fonction bien précise.
On prend par exemple la première ligne dans blink.hex : :100000000C9434000C943E000C943E000C943E0082
flash_write.c
On applique cette logique dans notre nouvelle fonction.
Pour séparer ligne par ligne on utilise la fonction fgets.
Pour sectionner ces ligne on utilise la fonction sscanf. (Ces 2 fonctions on été vues en Structure de Données Avancées).
Le reste est détaillés dans les commentaires du code.
char line[256];
unsigned int length, addr_in, type_in;
while (fgets(line, sizeof(line), file) != NULL)
{
sscanf(line, ":%2x%4x%2x", &length, &addr_in, &type_in);
uint16_t type = (uint16_t)type_in;
if (type == 0x01)
break;
if (type == 0x00)
{
// Du côté du fichier .HEX l'adresse est codé octet par octet. Contrairement à l'AVR qui stock des adresses de mots. Donc pour avoir l'adresse du mot(2 octets) il suffit de diviser l'adresse in par 2.
uint16_t current_word_addr = addr_in / 2;
for (unsigned int i = 0; i < length; i += 2)
{
unsigned int word_data;
// On décale le point de départ du sscanf de 9 (début des octets du code) puis on l'avance d'un facteur de 2 par rapport à i. Comme i aussi avance d'un facteur de 2 alors le tout avance d'un facteur de 4. Comme on lit 4 valeurs ascii à la fois (en effet 1 octet = 2 valeurs ascii => 2 octets = 1 mot = 4 valeurs ascii) le décallage est respécté.
sscanf(line + 9 + (i * 2), "%4x", &word_data);
uint16_t data = ((word_data & 0xFF) << 8) | (word_data >> 8);
// Préparation du paquet selon le protocole LUFA
data_write[0] = 0x03; // Commande d'écriture
data_write[1] = (current_word_addr >> 8) & 0xFF; // add HIGH
data_write[2] = current_word_addr & 0xFF; // add LOW
data_write[3] = data & 0xFF; // LOW
data_write[4] = (data >> 8) & 0xFF; // HIGH
// Envoi de l'ordre d'écriture en OUT
...
Programme principale (prog.c)
Ce programme relie toutes les fonctions vues avant. Il récupère d'abord les identifiants de la cible via le protocole ISP. Ensuite il laisse le choix entre lire ou écrire la flash cible.
Structure cible
Tout d'abord pour bien différencier la cible, on cré une structure cible:
typedef struct
{
unsigned char signature[3];
const char *nom;
uint32_t taille_flash; // Taille totale en octets
uint16_t taille_page; // Taille d'une page en octets
} CibleAVR;
Base de Donnée
On a créé une base de donnée pour avoir les informations exactes des puces si elles sont reconnues via le protocole ISP.
const CibleAVR base_de_donnees[] = {
{{0x1E, 0x95, 0x0F}, "ATmega328P", 32768, 128},
{{0x1E, 0x94, 0x89}, "ATmega16U2", 16384, 128},
{{0x1E, 0x93, 0x0B}, "ATtiny85", 8192, 64}};
On créé ensuite une fonction qui cherche la puce dans la BDD :
#define NB_CIBLES (sizeof(base_de_donnees) / sizeof(CibleAVR))
const CibleAVR *recherche_cible(unsigned char sig[3])
{
for (long unsigned int i = 0; i < NB_CIBLES; i++)
{
if (sig[0] == base_de_donnees[i].signature[0] &&
sig[1] == base_de_donnees[i].signature[1] &&
sig[2] == base_de_donnees[i].signature[2])
{
return &base_de_donnees[i];
}
}
return NULL; // Puce inconnue
}
main
On utilise des variables dans notre fonction main() pour pouvoir rajouter des fonctions : si on écrit -read ou -write après le nom de notre programme on peut soit lire ou écrire la flash cible.
int main(int argc, char *argv[])
{
// Initialisation unique
libusb_device_handle *dev_handle = init_usb();
if (dev_handle == NULL)
return 1;
if (reclamer_interface(dev_handle) < 0)
return 1;
// On récupère les infos de la cible si elle est connue.
unsigned char sig[3] = {0, 0, 0};
isp_ids(dev_handle, sig);
const CibleAVR *cible = recherche_cible(sig);
if (cible == NULL)
printf("\033[31mLa cible est inconnue.\033[0m\n");
else
printf("La cible est une %s, elle a une flash de \033[33m%d\033[0m octets et des pages de \033[33m%d\033[0m octets.\n", cible->nom, cible->taille_flash, cible->taille_page);
// commandes utilisateur
if (argc == 1)
{
printf("\033[31mErreur : Vous devez préciser une action.\033[0m\n");
printf("Usage : make read OU make write <fichier.hex>\n");
}
else if (strcmp(argv[1], "-read") == 0)
{
printf("\nLancement de la lecture...\n");
flash_read(dev_handle, cible->taille_flash);
}
else if (strcmp(argv[1], "-write") == 0)
{
if (argc < 3)
{
printf("\033[31mErreur : Il manque le nom du fichier .hex !\033[0m\n");
}
else
{
FILE *file = fopen(argv[2], "r");
if (file == NULL)
printf("\033[31mErreur : Impossible d'ouvrir le fichier %s.\033[0m\n", argv[2]);
else
{
printf("\nOuverture du fichier %s...\n", argv[2]);
flash_write(dev_handle, file);
printf("\033[32mÉcriture terminée avec succès !\033[0m\n");
fclose(file);
}
}
}
// Fermeture unique et propre
printf("\nFermeture globale du programme...\n");
libusb_release_interface(dev_handle, 0);
libusb_close(dev_handle);
libusb_exit(NULL);
return 0;
}
RESULTATS
Compilation dans libusb/
sudo make clean && make
Compilation dans libusb/code_a_televerser/
make clean && make
Execution du code
sudo ./build/prog -write blink.hex
sudo ./build/prog -read
Historique des séances
Soudure presque terminée. LEDs et résistances associées manquantes, 1 bouton poussoir traversant manquant.
LEDS et boutons fonctionnent -> attente de réparation de notre USB par le prof ou fin de projet pour le programatteur.
Nouvelle carte imprimée et détection de notre carte via lsusb.
Programmation minicom.
Programmation InOut test et ISP.
Programmation flash,ISP, fonctions test OK avant soudure.
Soudure des leds et du composants ISP de la carte. Modification des fonctions test pour faire fonctionner le code sur une atemaga328p(UNO).
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;
}
#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.)
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);
}
main.c avant de compiler : 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.
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.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 :
Pour faciliter la lecture du code, tous les caractères utiles ont été mappés dans une énumération C (enum Symbole).
L'envoi d'un caractère se fait donc en deux étapes : positionner le curseur (instruction), puis envoyer le dessin (data).
void send_nibble(uint8_t quartet)
{
PORTF &= ~(RegB4 | RegB5 | RegB6 | RegB7);
PORTF |= ((RegB4 | RegB5 | RegB6 | RegB7) & quartet);
pulse_enable();
}
void pulse_enable(void)
{
PORTF |= RegE;
_delay_us(1);
PORTF &= ~RegE;
}
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);
}
void set_address(uint8_t address)
{
lcd_write(0x80 + address, 0);
}
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.
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);
}
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).
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.
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é :
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é.
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
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 :
Plusieurs tests on soulevé des hypothèses de la défaillance de l'audio :
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.
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.
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).
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.
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.
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.
Pour écrire dans la flash il faut conpremdre cela :
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
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
-
Push de la V1 sur le git(#Archive GIT).
=> TODO :
=> TODO : Fixer le code de l'écran et faire des affichages avec le capteur ultra son.
=> 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.
-
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.
-
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).
-
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.
- 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.
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
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.
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é.
Pour que la flash fonctionne sur batterie, on court-circuite VBAT au 3.3V photo 3.3V - regulateur -batterie puis USB (2 photos)
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).
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.
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.
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)









