« SE4Binome2025-4 » : différence entre les versions
(→Shield) |
|||
| (29 versions intermédiaires par 2 utilisateurs non affichées) | |||
| Ligne 1 : | Ligne 1 : | ||
'''<big>Carte Son</big>''' pour un pico ordinateur | '''<big>Carte Son</big>''' pour un pico ordinateur : lien git [https://gitea.plil.fr/vsouopme/SE4-PICO-B4.git SE4-PICO-B4.git] | ||
<big>'''Présentation projet'''</big> | |||
Dans le cadre du cursus Systèmes Embarqués (SE4), ce projet consiste à concevoir un système capable de générer des signaux sonores de manière autonome. L'objectif est de transformer une valeur numérique calculée par un programme en une tension électrique réelle pour faire vibrer un haut-parleur. | |||
Le système repose sur deux éléments principaux : | |||
La carte Shield Arduino(PICO) : Équipée d'un microcontrôleur ATmega32U4, elle assure l'intelligence du système et l'ordonnancement des tâches. | |||
La | La carte fille son : Elle contient le dispositif de conversion numérique-analogique (DAC) et l'étage d'amplification. | ||
== '''Shield''' == | |||
== | === <small>Réalisation du shield arduino</small> === | ||
Nous avons réalisé un bouclier pour Arduino Uno afin d'implémenter un système d'ordonnancement, ce qui nous permettra de simuler le fonctionnement d'une carte mère. | |||
==== | ==== <small>Schématique - Routage et vue 3D</small> ==== | ||
{| class="wikitable mw-collapsible" | {| class="wikitable mw-collapsible" | ||
|+ | |+ | ||
![[Fichier:PicoShield - schema vue3D SE4-2025 G4. | ![[Fichier:PicoShield - schema vue3D SE4-2025 G4.pdf.pdf|alt=PicoShield - schema électrique SE4-2025|centré|vignette|PicoShield - schema vue3D SE4-2025 G4]] | ||
![[Fichier:PicoShield pcb schema routage se4 pico 4 2025.png|vignette|PicoShield - schema routage SE4-2025 G4]] | ![[Fichier:PicoShield pcb schema routage se4 pico 4 2025.png|vignette|PicoShield - schema routage SE4-2025 G4]] | ||
|- | |- | ||
| Ligne 37 : | Ligne 30 : | ||
===== '''< | == '''Ordonnanceur / Système d'exploitation''' == | ||
[[ | Le code implémente un '''mini ordonnanceur préemptif''' sur Arduino, qui alterne deux tâches toutes les 20 ms grâce à un timer et à son ISR. | ||
< | |||
[[Fichier:SoundCard pcb se4 pico 4 2025.png| | Chaque tâche a sa propre pile et son contexte CPU. | ||
< | |||
=== Code de l'ordonnanceur === | |||
Un ordonnanceur sert à gérer l'exécution des tâches dans un système temps réel, en assurant qu'elles s'exécutent dans un ordre optimal et respectent les délais. | |||
On sauvegarde les registres et le registre d'etat SREG, pour ensuite les restaurer dans l'ordre inverse. | |||
le programme définit deux macros assembleur : | |||
* SAVE_REGISTERS (): sauvegarde l’ensemble des registres généraux ainsi que le registre d’état <code>SREG</code>, | |||
* RESTORE_REGISTERS () : restaure ces mêmes registres dans l’ordre inverse. | |||
Cette sauvegarde manuelle est rendue nécessaire par l’utilisation d’une interruption déclarée avec l’attribut <code>ISR_NAKED</code>,<syntaxhighlight lang="c"> | |||
#define SAVE_REGISTERS() \ | |||
asm volatile ( \ | |||
"push r0 \n\t" \ | |||
"in r0,__SREG__ \n\t push r0 \n\t" \ | |||
"push r1 \n\t push r2 \n\t push r3 \n\t push r4 \n\t push r5 \n\t push r6 \n\t push r7 \n\t push r8 \n\t push r9 \n\t" \ | |||
"clr r1 \n\t" \ | |||
"push r10 \n\t push r11 \n\t push r12 \n\t push r13 \n\t push r14 \n\t push r15 \n\t push r16 \n\t push r17 \n\t push r18 \n\t push r19 \n\t" \ | |||
"push r20 \n\t push r21 \n\t push r22 \n\t push r23 \n\t push r24 \n\t push r25 \n\t push r26 \n\t push r27 \n\t push r28 \n\t push r29 \n\t" \ | |||
"push r30 \n\t push r31 \n\t" \ | |||
); | |||
#define RESTORE_REGISTERS() \ | |||
asm volatile ( \ | |||
"pop r31 \n\t pop r30 \n\t" \ | |||
"pop r29 \n\t pop r28 \n\t pop r27 \n\t pop r26 \n\t pop r25 \n\t pop r24 \n\t pop r23 \n\t pop r22 \n\t pop r21 \n\t pop r20 \n\t" \ | |||
"pop r19 \n\t pop r18 \n\t pop r17 \n\t pop r16 \n\t pop r15 \n\t pop r14 \n\t pop r13 \n\t pop r12 \n\t pop r11 \n\t pop r10 \n\t" \ | |||
"pop r9 \n\t pop r8 \n\t pop r7 \n\t pop r6 \n\t pop r5 \n\t pop r4 \n\t pop r3 \n\t pop r2 \n\t pop r1 \n\t" \ | |||
"pop r0 \n\t out __SREG__,r0 \n\t" \ | |||
"pop r0 \n\t" \ | |||
); | |||
</syntaxhighlight>ici on definit l'etat et la structure des taches | |||
Les tâches peuvent se trouver dans deux états distincts : | |||
* '''Ready''' : la tâche est prête à être exécutée | |||
* '''Sleep''' : la tâche est temporairement suspendue | |||
Chaque tâche est représentée par une structure '''Process''' contenant les informations nécessaires à sa gestion : | |||
* un pointeur vers la fonction à exécuter, | |||
* l’adresse du pointeur de pile associé à la tâche, | |||
* l’état courant de la tâche, | |||
* le temps de sommeil restant avant son réveil. | |||
Cette structure constitue le bloc fondamental de l’ordonnanceur.<syntaxhighlight lang="c"> | |||
typedef enum{ | |||
Ready, | |||
Sleep, | |||
}task_state; | |||
#if 0 | |||
struct task { | |||
uint16_t function; | |||
uint16_t stack; | |||
}; | |||
#endif | |||
typedef struct { | |||
void(*function)(void); | |||
uint16_t stack; | |||
volatile task_state state; | |||
uint16_t sleepTime; | |||
}process; | |||
</syntaxhighlight>Tableau des taches, la tache idle qui s'execute si aucune tache n'est prete<syntaxhighlight lang="c"> | |||
void idle (void); | |||
void task1 (void); | |||
void task2 (void); | |||
process Tasks [MAX_TASKS] = { | |||
{idle, STACKTOP-STACKSIZE,Ready,0}, | |||
{task1, STACKTOP-2*STACKSIZE,Ready,0}, | |||
{task2, STACKTOP-3*STACKSIZE,Ready,0}, | |||
{0,0}, | |||
{0,0}, | |||
{0,0}, | |||
{0,0}, | |||
{0,0} | |||
}; | |||
</syntaxhighlight>L'interruption déclenche une fonction de gestion d'interruption (ISR pour Interrupt Service Routine). | |||
Cette interruption est déclenchée par le '''Timer1''' à intervalle régulier. | |||
Elle sert à : | |||
* Sauvegarder le contexte de la tâche courante | |||
* Choisir la tâche suivante ('''scheduler''') | |||
* Restaurer le contexte de la nouvelle tâche | |||
<syntaxhighlight lang="c"> | |||
ISR(TIMER1_COMPA_vect,ISR_NAKED){ // Procédure d'interruption | |||
SAVE_REGISTERS(); | |||
Tasks[current].stack=SP; | |||
LED_PORT ^= (1<<LED1_BIT); | |||
scheduler(); | |||
SP=Tasks[current].stack; | |||
RESTORE_REGISTERS(); | |||
asm volatile ( "reti" ); | |||
} | |||
</syntaxhighlight> | |||
=== Fonction `wait()` === | |||
Comme première amélioration de votre ordonnanceur, il nous a été demandé de mettre en place un état endormi pour nos processus. Pour cela nous avons créer la procédure wait() qui a pour role : | |||
* De mettre une tache en pause pendant une duree donnee (avec Delay), tout ca sans bloquer les autres taches | |||
<syntaxhighlight lang="c"> | |||
void wait(uint16_t delay) { | |||
Tasks[current].sleepTime = delay; | |||
Tasks[current].state = Sleep; | |||
TCNT1=nb_ticks-1; | |||
while(Tasks[current].state == Sleep); | |||
} | |||
</syntaxhighlight>Par la suite, on a ajouter les fonctions | |||
* add_task : Fonction qui sert a ajouter une nouvelle tache au système | |||
* remove_task : Fonction pour supprimer (desactiver) une tâche existante | |||
Ensuite dans la fonction principale, on | |||
* Configure les Leds LED1 et LED2 comme sortie | |||
* Initialise les tâches | |||
* Initialise le minuteur (timer) | |||
* Charge la pile de la tâche courante | |||
* Restaure les registres | |||
* Lance la tâche avec '''reti''' | |||
<syntaxhighlight lang="c"> | |||
// Fonction pour ajouter une tâche | |||
void add_task(void (*task_function)(void)) { | |||
for (int n = 0; n < MAX_TASKS; n++) { | |||
if (Tasks[n].function == 0) { | |||
Tasks[n].function = task_function; | |||
Tasks[n].stack = (uint16_t)(STACKTOP - n * STACKSIZE); | |||
Tasks[n].state = Ready; | |||
sei(); | |||
} | |||
} | |||
} | |||
// Fonction pour supprimer une tâche | |||
void remove_task(int n) { | |||
if (n >= 0 && n < MAX_TASKS) { | |||
Tasks[n].function = 0; // Supprime la tâche en réinitialisant la fonction | |||
Tasks[n].state = Sleep; // Met la tâche en état de sommeil | |||
} | |||
} | |||
int main(void) | |||
{ | |||
LED_DIR |= (1<<LED1_BIT)|(1<<LED2_BIT); | |||
LED_PORT &= ~((1<<LED1_BIT)|(1<<LED2_BIT)); | |||
for(int i=0; i<MAX_TASKS; i++){ | |||
if (Tasks[i].function!=0) init_stack(i); | |||
// Tasks[i].stack = (uint16_t)(STACKTOP - (i + 1) * STACKSIZE); | |||
} | |||
init_minuteur(256,PERIODE); | |||
SP = Tasks[current].stack; | |||
RESTORE_REGISTERS(); | |||
//sei(); | |||
asm volatile ( "reti" ); | |||
while(1); | |||
return 0; | |||
} | |||
</syntaxhighlight>[[Fichier:Clignotement de la led.mov|vignette|gauche]] | |||
=='''Carte Son'''== | |||
=== <small>Description</small> === | |||
Notre carte fille son sera alimentée par la carte mère. | |||
Elle comporte une ATmega328p comme coeur et sa spécificité est un convertisseur numérique vers analogique dit R-2R. | |||
Pour fonctionner, elle a besoin : | |||
* d'un microphone | |||
* d'un haut-parleur pour jouer le son, | |||
* d'un connecteur USB permettant la configuration de l'appareil via une connexion a un ordinateur . | |||
=== <small>Schématique - routage et vue 3D</small> === | |||
{| class="wikitable mw-collapsible" | |||
|+ | |||
![[Fichier:SoundCard pcb electrical se4 pico 4 2025.png|alt=PicoShield - schema électrique SE4-2025|centré|vignette|Carte son - schema electrique SE4-2025 G4]] | |||
![[Fichier:SoundCard pcb schema se4 pico 4 2025.png|alt=Carte son - schema routage SE4-2025 G4|vignette|Carte son - schema routage SE4-2025 G4]] | |||
|- | |||
|[[Fichier:SoundCard pcb se4 pico 4 2025.png|alt=Carte son - schema vue3D recto SE4-2025 G4|vignette|Carte son - schema vue3D recto SE4-2025 G4]] | |||
|[[Fichier:SoundCard pcb verso se4 pico 4 2025.png|alt=Carte son - schema vue3D verso SE4-2025 G4|vignette|Carte son - schema vue3D verso SE4-2025 G4]] | |||
|} | |||
<big>'''Carte empruntée'''</big> | |||
Suite à des défauts de conception sur notre propre PCB, nous utilisons la carte du [https://wiki-se.plil.fr/mediawiki/index.php/I2L_2023_Groupe2 Groupe 2 (I2L 2023/24)]'''. | |||
* '''Adaptation''' : Nous avons dû identifier que cette carte utilise également le **PORTD** pour son réseau R-2R. | |||
* '''Contrainte :''' Cette carte n'était pas prévue pour notre châssis PICO original, ce qui a nécessité une vérification des broches pour éviter tout court-circuit. | |||
'''Architecture logicielle''' | |||
Le code est divisé en 2 modules pour plus de clarté : | |||
* <code>main.c</code> : Point d'entrée, initialise le système et lance les tâches. | |||
* <code>scheduler.c</code> : L'ordonnanceur qui gère le multitâche "souple". | |||
* <code>audio_card.c</code> : Le pilote bas-niveau qui communique avec le matériel. | |||
'''Pilote audio''' | |||
Le pilote utilise le **Timer 2** du microcontrôleur. Ce timer est réglé à '''10 kHz'''. À chaque "tic", il déclenche une fonction qui fait une seule chose : <code>PORTD = échantillon</code>. C'est l'écriture directe, la méthode la plus rapide possible sur AVR. | |||
<blockquote></blockquote> | |||
Version actuelle datée du 15 janvier 2026 à 10:40
Carte Son pour un pico ordinateur : lien git SE4-PICO-B4.git
Présentation projet
Dans le cadre du cursus Systèmes Embarqués (SE4), ce projet consiste à concevoir un système capable de générer des signaux sonores de manière autonome. L'objectif est de transformer une valeur numérique calculée par un programme en une tension électrique réelle pour faire vibrer un haut-parleur.
Le système repose sur deux éléments principaux :
La carte Shield Arduino(PICO) : Équipée d'un microcontrôleur ATmega32U4, elle assure l'intelligence du système et l'ordonnancement des tâches. La carte fille son : Elle contient le dispositif de conversion numérique-analogique (DAC) et l'étage d'amplification.
Shield
Réalisation du shield arduino
Nous avons réalisé un bouclier pour Arduino Uno afin d'implémenter un système d'ordonnancement, ce qui nous permettra de simuler le fonctionnement d'une carte mère.
Schématique - Routage et vue 3D
Ordonnanceur / Système d'exploitation
Le code implémente un mini ordonnanceur préemptif sur Arduino, qui alterne deux tâches toutes les 20 ms grâce à un timer et à son ISR.
Chaque tâche a sa propre pile et son contexte CPU.
Code de l'ordonnanceur
Un ordonnanceur sert à gérer l'exécution des tâches dans un système temps réel, en assurant qu'elles s'exécutent dans un ordre optimal et respectent les délais.
On sauvegarde les registres et le registre d'etat SREG, pour ensuite les restaurer dans l'ordre inverse.
le programme définit deux macros assembleur :
- SAVE_REGISTERS (): sauvegarde l’ensemble des registres généraux ainsi que le registre d’état
SREG, - RESTORE_REGISTERS () : restaure ces mêmes registres dans l’ordre inverse.
Cette sauvegarde manuelle est rendue nécessaire par l’utilisation d’une interruption déclarée avec l’attribut ISR_NAKED,
#define SAVE_REGISTERS() \
asm volatile ( \
"push r0 \n\t" \
"in r0,__SREG__ \n\t push r0 \n\t" \
"push r1 \n\t push r2 \n\t push r3 \n\t push r4 \n\t push r5 \n\t push r6 \n\t push r7 \n\t push r8 \n\t push r9 \n\t" \
"clr r1 \n\t" \
"push r10 \n\t push r11 \n\t push r12 \n\t push r13 \n\t push r14 \n\t push r15 \n\t push r16 \n\t push r17 \n\t push r18 \n\t push r19 \n\t" \
"push r20 \n\t push r21 \n\t push r22 \n\t push r23 \n\t push r24 \n\t push r25 \n\t push r26 \n\t push r27 \n\t push r28 \n\t push r29 \n\t" \
"push r30 \n\t push r31 \n\t" \
);
#define RESTORE_REGISTERS() \
asm volatile ( \
"pop r31 \n\t pop r30 \n\t" \
"pop r29 \n\t pop r28 \n\t pop r27 \n\t pop r26 \n\t pop r25 \n\t pop r24 \n\t pop r23 \n\t pop r22 \n\t pop r21 \n\t pop r20 \n\t" \
"pop r19 \n\t pop r18 \n\t pop r17 \n\t pop r16 \n\t pop r15 \n\t pop r14 \n\t pop r13 \n\t pop r12 \n\t pop r11 \n\t pop r10 \n\t" \
"pop r9 \n\t pop r8 \n\t pop r7 \n\t pop r6 \n\t pop r5 \n\t pop r4 \n\t pop r3 \n\t pop r2 \n\t pop r1 \n\t" \
"pop r0 \n\t out __SREG__,r0 \n\t" \
"pop r0 \n\t" \
);
ici on definit l'etat et la structure des taches
Les tâches peuvent se trouver dans deux états distincts :
- Ready : la tâche est prête à être exécutée
- Sleep : la tâche est temporairement suspendue
Chaque tâche est représentée par une structure Process contenant les informations nécessaires à sa gestion :
- un pointeur vers la fonction à exécuter,
- l’adresse du pointeur de pile associé à la tâche,
- l’état courant de la tâche,
- le temps de sommeil restant avant son réveil.
Cette structure constitue le bloc fondamental de l’ordonnanceur.
typedef enum{
Ready,
Sleep,
}task_state;
#if 0
struct task {
uint16_t function;
uint16_t stack;
};
#endif
typedef struct {
void(*function)(void);
uint16_t stack;
volatile task_state state;
uint16_t sleepTime;
}process;
Tableau des taches, la tache idle qui s'execute si aucune tache n'est prete
void idle (void);
void task1 (void);
void task2 (void);
process Tasks [MAX_TASKS] = {
{idle, STACKTOP-STACKSIZE,Ready,0},
{task1, STACKTOP-2*STACKSIZE,Ready,0},
{task2, STACKTOP-3*STACKSIZE,Ready,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0}
};
L'interruption déclenche une fonction de gestion d'interruption (ISR pour Interrupt Service Routine).
Cette interruption est déclenchée par le Timer1 à intervalle régulier.
Elle sert à :
- Sauvegarder le contexte de la tâche courante
- Choisir la tâche suivante (scheduler)
- Restaurer le contexte de la nouvelle tâche
ISR(TIMER1_COMPA_vect,ISR_NAKED){ // Procédure d'interruption
SAVE_REGISTERS();
Tasks[current].stack=SP;
LED_PORT ^= (1<<LED1_BIT);
scheduler();
SP=Tasks[current].stack;
RESTORE_REGISTERS();
asm volatile ( "reti" );
}
Fonction `wait()`
Comme première amélioration de votre ordonnanceur, il nous a été demandé de mettre en place un état endormi pour nos processus. Pour cela nous avons créer la procédure wait() qui a pour role :
- De mettre une tache en pause pendant une duree donnee (avec Delay), tout ca sans bloquer les autres taches
void wait(uint16_t delay) {
Tasks[current].sleepTime = delay;
Tasks[current].state = Sleep;
TCNT1=nb_ticks-1;
while(Tasks[current].state == Sleep);
}
Par la suite, on a ajouter les fonctions
- add_task : Fonction qui sert a ajouter une nouvelle tache au système
- remove_task : Fonction pour supprimer (desactiver) une tâche existante
Ensuite dans la fonction principale, on
- Configure les Leds LED1 et LED2 comme sortie
- Initialise les tâches
- Initialise le minuteur (timer)
- Charge la pile de la tâche courante
- Restaure les registres
- Lance la tâche avec reti
// Fonction pour ajouter une tâche
void add_task(void (*task_function)(void)) {
for (int n = 0; n < MAX_TASKS; n++) {
if (Tasks[n].function == 0) {
Tasks[n].function = task_function;
Tasks[n].stack = (uint16_t)(STACKTOP - n * STACKSIZE);
Tasks[n].state = Ready;
sei();
}
}
}
// Fonction pour supprimer une tâche
void remove_task(int n) {
if (n >= 0 && n < MAX_TASKS) {
Tasks[n].function = 0; // Supprime la tâche en réinitialisant la fonction
Tasks[n].state = Sleep; // Met la tâche en état de sommeil
}
}
int main(void)
{
LED_DIR |= (1<<LED1_BIT)|(1<<LED2_BIT);
LED_PORT &= ~((1<<LED1_BIT)|(1<<LED2_BIT));
for(int i=0; i<MAX_TASKS; i++){
if (Tasks[i].function!=0) init_stack(i);
// Tasks[i].stack = (uint16_t)(STACKTOP - (i + 1) * STACKSIZE);
}
init_minuteur(256,PERIODE);
SP = Tasks[current].stack;
RESTORE_REGISTERS();
//sei();
asm volatile ( "reti" );
while(1);
return 0;
}
Carte Son
Description
Notre carte fille son sera alimentée par la carte mère.
Elle comporte une ATmega328p comme coeur et sa spécificité est un convertisseur numérique vers analogique dit R-2R.
Pour fonctionner, elle a besoin :
- d'un microphone
- d'un haut-parleur pour jouer le son,
- d'un connecteur USB permettant la configuration de l'appareil via une connexion a un ordinateur .
Schématique - routage et vue 3D
Carte empruntée
Suite à des défauts de conception sur notre propre PCB, nous utilisons la carte du Groupe 2 (I2L 2023/24).
- Adaptation : Nous avons dû identifier que cette carte utilise également le **PORTD** pour son réseau R-2R.
- Contrainte : Cette carte n'était pas prévue pour notre châssis PICO original, ce qui a nécessité une vérification des broches pour éviter tout court-circuit.
Architecture logicielle
Le code est divisé en 2 modules pour plus de clarté :
main.c: Point d'entrée, initialise le système et lance les tâches.scheduler.c: L'ordonnanceur qui gère le multitâche "souple".audio_card.c: Le pilote bas-niveau qui communique avec le matériel.
Pilote audio
Le pilote utilise le **Timer 2** du microcontrôleur. Ce timer est réglé à 10 kHz. À chaque "tic", il déclenche une fonction qui fait une seule chose : PORTD = échantillon. C'est l'écriture directe, la méthode la plus rapide possible sur AVR.