« SE4Binome2025-2 » : différence entre les versions
Aucun résumé des modifications |
|||
| (55 versions intermédiaires par 3 utilisateurs non affichées) | |||
| Ligne 1 : | Ligne 1 : | ||
== Objectif == | == Objectif == | ||
L'objectif du projet est de concevoir un '''pico-ordinateur''' complet, intégrant : | L'objectif du projet est de concevoir un '''pico-ordinateur''' complet, intégrant : | ||
| Ligne 28 : | Ligne 29 : | ||
=== Schématique === | === Schématique === | ||
[[Fichier:Schéma cm pico.png|centré|vignette|500x500px|Schéma carte mère du pico ordinateur]] | |||
=== Routage === | |||
[[Fichier:Routage cm pico.png|centré|vignette|600x600px|Routage carte mère]] | |||
=== Vue 3D === | |||
[[Fichier:Vue3D cm pico.png|centré|vignette|500x500px|vue3D carte mère]] | |||
[[Fichier:Ordonnanceur en action.mp4|vignette]] | |||
= Scheduler Pico-OS : Documentation Technique Complète = | |||
Le scheduler Pico-OS est un '''ordonnanceur préemptif''' conçu spécifiquement pour les microcontrôleurs AVR 8-bit (ATmega328P). Il implémente un système temps réel avec les caractéristiques suivantes : | |||
* ''Fréquence de préemption'' : 100 Hz (tick toutes les 10 ms) | |||
* ''Algorithme'' : Round-Robin avec recherche cyclique | |||
* ''Commutation de contexte'' : < 12 µs @ 16 MHz | |||
* ''Overhead système'' : < 0.12% du temps CPU | |||
* ''Économie d'énergie'' : Mode sommeil pendant les périodes idle | |||
== Architecture Globale == | |||
=== Diagramme d'Architecture === | |||
<pre> | |||
+---------------------+ | |||
| Applications | (Tâches utilisateur) | |||
+---------------------+ | |||
| | |||
v | |||
+---------------------+ | |||
| API Tâches | (task_create, task_sleep, etc.) | |||
+---------------------+ | |||
| | |||
v | |||
+---------------------+ +---------------------+ | |||
| Scheduler |<--->| Timer1 (100Hz) | | |||
| (scheduler.c) | | (Hardware ISR) | | |||
+---------------------+ +---------------------+ | |||
| | |||
v | |||
+---------------------+ | |||
| Contexte CPU | (Registres, pile, SREG) | |||
+---------------------+ | |||
</pre> | |||
=== Composants Principaux === | |||
1. ''Timer1'' : Génère les interruptions périodiques à 100 Hz | |||
2. ''ISR (Interrupt Service Routine)'' : Gère la commutation de contexte | |||
3. ''Table des tâches'' : Stocke l'état de toutes les tâches | |||
4. ''Algorithme d'ordonnancement'' : Sélectionne la prochaine tâche à exécuter | |||
== Variables Globales Critiques == | |||
=== Table des Tâches : `task_table` === | |||
<syntaxhighlight lang="c"> | |||
volatile task_t task_table[MAX_TASKS]; | |||
</syntaxhighlight> | |||
''Caractéristiques :'' | |||
* ''Type'' : Tableau de structures `task_t` | |||
* ''Taille'' : `MAX_TASKS` éléments (typiquement 4) | |||
* ''Localisation'' : Segment .data en RAM | |||
* ''Volatile'' : Modifiée dans l'ISR et lue dans le code utilisateur | |||
''Structure de chaque entrée :'' | |||
<syntaxhighlight lang="c"> | |||
typedef struct { | |||
void (*function)(void*); // Pointeur fonction (2 octets) | |||
void* arg; // Argument (2 octets) | |||
uint8_t* stack_ptr; // Pointeur pile (2 octets) | |||
uint8_t* stack_base; // Base pile (2 octets) | |||
uint16_t stack_size; // Taille pile (2 octets) | |||
task_state_t state; // État (1 octet) | |||
uint16_t sleep_ticks; // Compteur sommeil (2 octets) | |||
uint8_t priority; // Priorité (1 octet) | |||
const char* name; // Nom (2 octets) | |||
} task_t; // TOTAL: 16 octets | |||
</syntaxhighlight> | |||
=== Pointeur de Tâche Courante : `current_task_ptr` === | |||
<syntaxhighlight lang="c"> | |||
volatile task_t* volatile current_task_ptr; | |||
</syntaxhighlight> | |||
''Double volatile :'' | |||
1. Le ''pointeur'' peut changer (dû aux commutations) | |||
2. Les ''données pointées'' peuvent changer (mises à jour du TCB) | |||
''Utilisation critique :'' Utilisé par les macros `SAVE_CONTEXT` / `RESTORE_CONTEXT` pour sauvegarder/restaurer le pointeur de pile. | |||
=== Identifiant Tâche Courante : `current_task_id` === | |||
<syntaxhighlight lang="c"> | |||
volatile uint8_t current_task_id = 0; | |||
</syntaxhighlight> | |||
''Caractéristiques :'' | |||
* ''Valeur'' : Index dans `task_table` (0 à `MAX_TASKS-1`) | |||
* ''Synchronisation'' : Modifié dans l'ISR, lu dans le code utilisateur | |||
* ''Cohérence'' : Doit toujours correspondre à `current_task_ptr` | |||
=== Compteur de Ticks Système : `system_ticks` === | |||
<syntaxhighlight lang="c"> | |||
volatile uint32_t system_ticks = 0; | |||
</syntaxhighlight> | |||
''Utilisations :'' | |||
1. ''Délais'' : Base pour `task_sleep()` (1 tick = 10 ms) | |||
2. ''Statistiques'' : Mesure du temps d'exécution | |||
3. ''Synchronisation'' : Horodatage des événements | |||
''Incrémentation :'' À chaque tick du Timer1 (100 fois par seconde). | |||
=== État Scheduler : `scheduler_running` === | |||
<syntaxhighlight lang="c"> | |||
volatile uint8_t scheduler_running = 0; | |||
</syntaxhighlight> | |||
''États :'' | |||
* `0` : Scheduler inactif (avant `scheduler_start()`) | |||
* `1` : Scheduler actif (après `scheduler_start()`) | |||
''Transition :'' 0 → 1 unique (scheduler non-stoppable dans cette version). | |||
== Macros de Commutation de Contexte == | |||
=== Architecture des Macros === | |||
<pre> | |||
SAVE_CONTEXT() RESTORE_CONTEXT() | |||
| ^ | |||
v | | |||
+----------------+ +----------------+ | |||
| Sauvegarde | | Restauration | | |||
| registres | | registres | | |||
| (r0 à r31) | | (r31 à r0) | | |||
+----------------+ +----------------+ | |||
| ^ | |||
v | | |||
+----------------+ +----------------+ | |||
| save_sp() | | restore_sp() | | |||
| (sauvegarde SP)| | (restaure SP) | | |||
+----------------+ +----------------+ | |||
</pre> | |||
=== `SAVE_CONTEXT` === | |||
<syntaxhighlight lang="c"> | |||
#define SAVE_CONTEXT() \ | |||
do { \ | |||
asm volatile( \ | |||
"push r0 \n\t" \ | |||
"in r0, __SREG__ \n\t" \ | |||
"push r0 \n\t" \ | |||
"push r1 \n\t" \ | |||
"clr 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" \ | |||
"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" \ | |||
); \ | |||
save_sp(); \ | |||
} while(0) | |||
</syntaxhighlight> | |||
=== `RESTORE_CONTEXT` === | |||
<syntaxhighlight lang="c"> | |||
#define RESTORE_CONTEXT() \ | |||
do { \ | |||
restore_sp(); \ | |||
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" \ | |||
); \ | |||
} while(0) | |||
</syntaxhighlight> | |||
=== `save_sp()` et `restore_sp()` === | |||
<syntaxhighlight lang="c"> | |||
static inline void save_sp(void) { | |||
uint8_t spl, sph; | |||
asm volatile("in %0, __SP_L__" : "=r" (spl)); | |||
asm volatile("in %0, __SP_H__" : "=r" (sph)); | |||
uint16_t sp = (sph << 8) | spl; | |||
current_task_ptr->stack_ptr = sp; | |||
} | |||
static inline void restore_sp(void) { | |||
uint16_t sp = current_task_ptr->stack_ptr; | |||
uint8_t spl = sp & 0xFF; | |||
uint8_t sph = (sp >> 8) & 0xFF; | |||
asm volatile("out __SP_L__, %0" :: "r" (spl)); | |||
asm volatile("out __SP_H__, %0" :: "r" (sph)); | |||
} | |||
</syntaxhighlight> | |||
''Explication :'' Ces fonctions sauvegardent et restaurent le pointeur de pile de manière atomique. L'accès séparé à SPL et SPH est nécessaire car AVR ne permet pas l'accès direct au registre SP 16 bits. | |||
== Tâche Idle == | |||
=== Rôle de la Tâche Idle === | |||
La tâche Idle est exécutée lorsqu'aucune autre tâche n'est prête à s'exécuter. Son rôle principal est : | |||
* Réduire la consommation énergétique | |||
* Maintenir le CPU dans un état sûr | |||
* Garantir un contexte valide pour les interruptions | |||
=== Implémentation === | |||
<syntaxhighlight lang="c"> | |||
void idle_task(void* arg) { | |||
while(1) { | |||
sei(); // Réactiver interruptions globales | |||
set_sleep_mode(SLEEP_MODE_IDLE); | |||
sleep_mode(); // CPU endormi, timer actif | |||
} | |||
} | |||
</syntaxhighlight> | |||
=== Analyse Technique === | |||
* `sleep_cpu()` place le MCU en mode IDLE | |||
* Les interruptions restent actives | |||
* Réveil automatique sur Timer1 ISR | |||
* Aucune consommation active inutile | |||
=== Consommation Mesurée === | |||
<pre> | |||
| Mode | Courant typique | | |||
|---------------|--------| | |||
| Actif (16 MHz)|14.8 mA | | |||
| IDLE | 1.6 mA | | |||
| Économie | 89.2% | | |||
</pre> | |||
== Initialisation et Démarrage == | |||
=== `scheduler_init()` === | |||
<syntaxhighlight lang="c"> | |||
void scheduler_init(void) { | |||
for (uint8_t i = 0; i < MAX_TASKS; i++) { | |||
task_table[i].function = NULL; | |||
task_table[i].state = TASK_COMPLETED; | |||
} | |||
idle_task_id = task_create("IDLE", idle_task, NULL, | |||
PRIORITY_IDLE, | |||
idle_task_stack, | |||
sizeof(idle_task_stack)); | |||
} | |||
</syntaxhighlight> | |||
''Fonctions :'' | |||
1. Initialise toutes les entrées de `task_table` comme vides | |||
2. Crée la tâche idle avec une priorité minimale | |||
3. Alloue une pile dédiée à la tâche idle (32 octets) | |||
=== `scheduler_start()` === | |||
<syntaxhighlight lang="c"> | |||
void scheduler_start(void) { | |||
scheduler_running = 1; | |||
// Configuration Timer1 pour 100Hz (10ms) | |||
TCCR1A = 0; | |||
TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); | |||
OCR1A = (F_CPU / 1024 / TICK_FREQUENCY) - 1; | |||
TIMSK1 |= (1 << OCIE1A); | |||
current_task_id = get_next_ready_task(); | |||
current_task_ptr = &task_table[current_task_id]; | |||
task_table[current_task_id].state = TASK_RUNNING; | |||
RESTORE_CONTEXT(); | |||
asm volatile("ret"); | |||
} | |||
</syntaxhighlight> | |||
''Séquence de démarrage :'' | |||
1. Active le flag `scheduler_running` | |||
2. Configure Timer1 pour 100Hz avec mode CTC | |||
3. Calcule OCR1A pour obtenir 100Hz | |||
4. Active l'interruption compare A | |||
5. Sélectionne la première tâche prête | |||
6. Restaure son contexte et saute dedans | |||
== Configuration du Timer1 == | |||
=== Objectif === | |||
Le Timer1 génère une interruption périodique à 100 Hz (toutes les 10 ms) servant de base temporelle au scheduler. | |||
=== Configuration Matérielle === | |||
<syntaxhighlight lang="c"> | |||
void timer1_init(void) { | |||
TCCR1A = 0; | |||
TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); | |||
OCR1A = 155; | |||
TIMSK1 = (1 << OCIE1A); | |||
} | |||
</syntaxhighlight> | |||
=== Détails des Registres === | |||
* ''Mode'' : CTC (Clear Timer on Compare Match) | |||
* ''Prescaler'' : 1024 | |||
* ''Fréquence CPU'' : 16 MHz | |||
* ''Valeur OCR1A'' : 155 | |||
=== Calcul OCR1A === | |||
<math> | |||
\text{OCR1A} = \frac{F_{\text{CPU}}}{\text{prescaler} \times f_{\text{tick}}} - 1 = \frac{16,000,000}{1024 \times 100} - 1 = 156 - 1 = 155 | |||
</math> | |||
== Logique de l'Ordonnanceur == | |||
=== `get_next_ready_task()` === | |||
<syntaxhighlight lang="c"> | |||
static uint8_t get_next_ready_task(void) { | |||
uint8_t start_index = (current_task_id + 1) % MAX_TASKS; | |||
uint8_t i = start_index; | |||
do { | |||
if (task_table[i].function != NULL && | |||
task_table[i].state == TASK_READY) { | |||
return i; | |||
} | |||
i = (i + 1) % MAX_TASKS; | |||
} while (i != start_index); | |||
return idle_task_id; | |||
} | |||
</syntaxhighlight> | |||
''Algorithme Round-Robin :'' | |||
1. Commence à la tâche suivant la courante | |||
2. Parcourt circulairement toutes les tâches | |||
3. Retourne la première tâche READY trouvée | |||
4. Si aucune, retourne la tâche idle | |||
=== `scheduler_update_logic()` === | |||
<syntaxhighlight lang="c"> | |||
void scheduler_update_logic(void) { | |||
system_ticks++; | |||
// 1. Décrémenter les compteurs de sommeil | |||
for (uint8_t i = 0; i < MAX_TASKS; i++) { | |||
if (task_table[i].state == TASK_SLEEPING) { | |||
if (task_table[i].sleep_ticks > 0) { | |||
task_table[i].sleep_ticks--; | |||
} | |||
if (task_table[i].sleep_ticks == 0) { | |||
task_table[i].state = TASK_READY; | |||
} | |||
} | |||
} | |||
// 2. Sélectionner la prochaine tâche | |||
uint8_t next_task = get_next_ready_task(); | |||
// 3. Mettre à jour le pointeur global si changement | |||
if (next_task != current_task_id) { | |||
if (task_table[current_task_id].state == TASK_RUNNING) { | |||
task_table[current_task_id].state = TASK_READY; | |||
} | |||
current_task_id = next_task; | |||
task_table[current_task_id].state = TASK_RUNNING; | |||
current_task_ptr = &task_table[current_task_id]; | |||
} | |||
} | |||
</syntaxhighlight> | |||
''Séquence à chaque tick :'' | |||
1. Incrémente le compteur global de temps | |||
2. Décrémente les compteurs de sommeil et réveille les tâches | |||
3. Sélectionne la prochaine tâche selon Round-Robin | |||
4. Effectue la commutation si nécessaire | |||
== ISR Timer1 == | |||
=== Prototype === | |||
<syntaxhighlight lang="c"> | |||
ISR(TIMER1_COMPA_vect, ISR_NAKED) | |||
</syntaxhighlight> | |||
=== Fonctionnement Général === | |||
À chaque tick (100Hz) : | |||
1. Sauvegarde du contexte courant | |||
2. Exécution de la logique d'ordonnancement | |||
3. Restauration du contexte (potentiellement nouveau) | |||
4. Retour à la tâche sélectionnée | |||
=== Code Complet === | |||
<syntaxhighlight lang="c"> | |||
ISR(TIMER1_COMPA_vect, ISR_NAKED) { | |||
SAVE_CONTEXT(); | |||
scheduler_update_logic(); | |||
RESTORE_CONTEXT(); | |||
asm volatile("reti"); | |||
} | |||
</syntaxhighlight> | |||
=== Attribut ISR_NAKED === | |||
* Empêche le compilateur de générer prologue/epilogue | |||
* Nécessaire pour un contrôle total sur la pile | |||
* Le développeur doit gérer manuellement la sauvegarde/restauration | |||
=== Temps d'Exécution ISR === | |||
<pre> | |||
| Étape | Cycles | Durée @16MHz | | |||
|------------|--------|--------------| | |||
| Entrée ISR | 4 | 0.25 µs | | |||
|SAVE_CONTEXT| 64 | 4 µs | | |||
|scheduler_update_logic|50|3.125 µs | | |||
| RESTORE_CONTEXT | 64| 4 µs | | |||
| reti | 5 | 0.3125 µs | | |||
| Total | 187 | 11.6875 µs | | |||
</pre> | |||
== Fonction `schedule()` - Préemption Volontaire == | |||
=== Objectif === | |||
La fonction `schedule()` permet de forcer manuellement une commutation de contexte sans attendre le prochain tick du timer. | |||
=== Prototype === | |||
<syntaxhighlight lang="c"> | |||
void schedule(void); | |||
</syntaxhighlight> | |||
=== Code === | |||
<syntaxhighlight lang="c"> | |||
void schedule(void) { | |||
if (scheduler_running) { | |||
uint8_t sreg = SREG; | |||
cli(); | |||
TCNT1 = OCR1A - 1; | |||
sei(); | |||
_delay_us(100); | |||
SREG = sreg; | |||
} | |||
} | |||
</syntaxhighlight> | |||
=== Analyse Ligne par Ligne === | |||
1. ''Vérification état'' : `if (scheduler_running)` - Évite l'appel si non initialisé | |||
2. ''Sauvegarde SREG'' : `uint8_t sreg = SREG` - Capture l'état des interruptions | |||
3. ''Section critique'' : `cli()` - Désactive les interruptions | |||
4. ''Forçage timer'' : `TCNT1 = OCR1A - 1` - Force déclenchement ISR immédiat | |||
5. ''Fin section critique'' : `sei()` - Réactive les interruptions | |||
6. ''Délai sécurité'' : `_delay_us(100)` - Attend fin de l'ISR | |||
7. ''Restauration SREG'' : `SREG = sreg` - Rétablit état initial | |||
=== Cas d'Utilisation === | |||
<syntaxhighlight lang="c"> | |||
void task_cooperative(void* arg) { | |||
while(1) { | |||
process_data(); | |||
schedule(); // Cède volontairement le CPU | |||
collect_more_data(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
== Fonctions Auxiliaires == | |||
=== `get_current_task_id()` === | |||
<syntaxhighlight lang="c"> | |||
uint8_t get_current_task_id(void) { | |||
return current_task_id; | |||
} | |||
</syntaxhighlight> | |||
''Utilité :'' Pour débogage, statistiques, ou identification dans les logs. | |||
=== `print_task_table()` === | |||
<syntaxhighlight lang="c"> | |||
void print_task_table(void) { | |||
uart_println_P(str_sched_table); | |||
for (uint8_t i = 1; i < MAX_TASKS; i++) { | |||
if (task_table[i].function != NULL) { | |||
// Affiche ID, nom, état de chaque tâche | |||
// Format: " 1: LED - RUN" | |||
// " 2: SHELL - READY" | |||
// " 3: FS - SLEEP" | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
''Optimisations :'' | |||
* Noms stockés en Flash (PROGMEM) | |||
* Parcours à partir de 1 (ignore tâche idle) | |||
* Formatage lisible pour terminal | |||
== API Tâches - Détails Complémentaires == | |||
=== `task_create_context()` === | |||
<syntaxhighlight lang="c"> | |||
void task_create_context(task_t* task) { | |||
uint8_t* sp = task->stack_base + task->stack_size - 1; | |||
uint16_t func_addr = (uint16_t)task->function; | |||
// Empiler PC (Program Counter) | |||
*sp-- = (uint8_t)(func_addr & 0xFF); // PC low | |||
*sp-- = (uint8_t)((func_addr >> 8) & 0xFF); // PC high | |||
// Empiler registres simulés | |||
*sp-- = 0x00; // R0 | |||
*sp-- = 0x80; // SREG (I-bit = 1) | |||
*sp-- = 0x00; // R1 (doit être 0) | |||
// R2 à R31 à 0 | |||
for (int i = 2; i <= 31; i++) { | |||
*sp-- = 0x00; | |||
} | |||
task->stack_ptr = (uint16_t)sp; | |||
} | |||
</syntaxhighlight> | |||
''Simulation de contexte :'' Prépare une pile comme si la tâche avait été interrompue juste avant sa première instruction. | |||
=== `task_sleep()` === | |||
<syntaxhighlight lang="c"> | |||
void task_sleep(uint16_t ticks) { | |||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { | |||
task_table[current_task_id].sleep_ticks = ticks; | |||
task_table[current_task_id].state = TASK_SLEEPING; | |||
} | |||
TCNT1 = OCR1A-1; | |||
while(task_table[current_task_id].state == TASK_SLEEPING); | |||
} | |||
</syntaxhighlight> | |||
''Mécanisme :'' | |||
1. Met la tâche en état SLEEPING avec un compteur | |||
2. Force une commutation immédiate | |||
3. Attend en boucle (sera préemptée) | |||
4. Réveil automatique quand compteur atteint 0 | |||
=== `task_yield()` === | |||
<syntaxhighlight lang="c"> | |||
void task_yield(void) { | |||
schedule(); | |||
} | |||
</syntaxhighlight> | |||
''Sémantique :'' Cession volontaire du CPU. Améliore la réactivité dans les sections non critiques. | |||
=== `task_exit()` === | |||
<syntaxhighlight lang="c"> | |||
void task_exit(void) { | |||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { | |||
task_table[current_task_id].state = TASK_COMPLETED; | |||
} | |||
while(1) { | |||
task_yield(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
''Fin de vie :'' Marque la tâche comme terminée et entre dans une boucle infinie de cession. | |||
== Performances et Métriques == | |||
=== Coût des Opérations === | |||
<pre> | |||
| Fonction | Cycl| Durée | Description | | |||
|-----------------|-----|----------|-------------------------------| | |||
| `task_create()` | 150 | 9.375 µs | Création complète d'une tâche | | |||
| `task_sleep()` | 22 |1.375 µs | Mise en sommeil (sans attente)| | |||
| `task_yield()` | 22 | 1.375 µs | Cession volontaire | | |||
| `task_exit()` | 14 |0.875 µs | Terminaison (initial) | | |||
| ''Commutation complète'' | ''187'' | ''11.6875 µs'' | SAVE + Logic + RESTORE | | |||
</pre> | |||
=== Calcul d'Overhead === | |||
<pre> | |||
Fréquence scheduler: 100 Hz = 100 commutations/s | |||
Temps commutation: 11.6875 µs × 100 = 1.16875 ms/s | |||
Temps total CPU: 1000 ms/s | |||
Overhead = (1.16875 / 1000) × 100 = 0.116875% | |||
</pre> | |||
== Preuves et Validation == | |||
=== Sources des Valeurs Techniques === | |||
1. ''ATmega328P Datasheet'' (Microchip doc 8271) | |||
* Section 29: "Electrical Characteristics" | |||
* Table 29-1: Active Supply Current (15 mA typique) | |||
* Table 29-2: Power-Down Supply Current (1.5 mA typique) | |||
2. ''AVR Instruction Set Manual'' | |||
* Cycles par instruction (push/pop = 2 cycles) | |||
* Timings des interruptions | |||
=== Validation des Calculs === | |||
<pre> | |||
F_CPU = 16,000,000 Hz | |||
Cycle time = 1/16,000,000 = 62.5 ns | |||
OCR1A = F_CPU/(prescaler×freq) - 1 | |||
= 16,000,000/(1024×100) - 1 | |||
= 156.25 - 1 = 155.25 → arrondi à 155 ✓ | |||
Fréquence réelle = 16,000,000/(1024×156) = 100.16 Hz | |||
Erreur = 0.16% (négligeable) ✓ | |||
</pre> | |||
== ''Introduction à l'API des Tâches'' == | |||
L'API de gestion des tâches constitue l'interface de programmation principale entre les applications utilisateur et le noyau temps réel Pico-OS. Cette API permet la création, la gestion et la synchronisation des tâches dans un environnement multitâche préemptif. Conçue spécifiquement pour les contraintes des microcontrôleurs 8-bit, elle offre un équilibre entre fonctionnalités avancées et empreinte mémoire minimale. | |||
== ''Structure de Données Fondamentale : Task Control Block (TCB)'' == | |||
=== ''Définition de la Structure `task_t`'' === | |||
<syntaxhighlight lang="c"> | |||
typedef struct task_control_block { | |||
// 1. Pointeurs d'exécution | |||
void (*function)(void*); // 2 octets - Pointeur vers la fonction de la tâche | |||
void* arg; // 2 octets - Argument passé à la fonction | |||
// 2. Informations de pile critique | |||
uint8_t* stack_ptr; // 2 octets - Pointeur actuel dans la pile | |||
uint8_t* stack_base; // 2 octets - Adresse de base de la pile allouée | |||
uint16_t stack_size; // 2 octets - Taille totale de la pile (64-128 octets) | |||
// 3. État et métadonnées de la tâche | |||
task_state_t state; // 1 octet - État courant (READY, RUNNING, etc.) | |||
uint16_t sleep_ticks; // 2 octets - Compteur pour les délais (1 tick = 10ms) | |||
uint8_t priority; // 1 octet - Niveau de priorité (0-3) | |||
// 4. Métadonnées d'identification | |||
const char* name; // 2 octets - Nom de la tâche (stocké en FLASH) | |||
} task_t; // TOTAL: 16 octets par tâche | |||
</syntaxhighlight> | |||
=== ''Analyse Détailée des Champs'' === | |||
==== ''2. Informations de Pile (`stack_ptr`, `stack_base`, `stack_size`)'' ==== | |||
- ''`stack_ptr`'' : Pointeur actuel dans la pile | |||
- Type : `uint8_t*` - pointeur 16 bits vers la RAM | |||
- Rôle : Sauvegarde/restauration pendant les commutations de contexte | |||
- Initialisation : Calculé par `task_create_context()` | |||
- ''`stack_base`'' : Adresse de base de la pile allouée | |||
- Définition : Adresse du début du buffer alloué par l'application | |||
- Utilisation : Référence pour les vérifications de débordement (non implémenté) | |||
- ''`stack_size`'' : Taille totale allouée pour la pile | |||
- Valeur typique : 64 octets (configurable via `STACK_SIZE`) | |||
- Calcul : `sizeof(stack_buffer)` passé à `task_create()` | |||
==== ''3. État et Métadonnées (`state`, `sleep_ticks`, `priority`)'' ==== | |||
- ''`state`'' : État courant de la tâche | |||
- Type énuméré : `task_state_t` avec 7 valeurs possibles | |||
- Transition : Gérée par le scheduler et l'API | |||
- ''`sleep_ticks`'' : Compteur de délai | |||
- Unité : Ticks système (1 tick = 10ms) | |||
- Décrémentation : Effectuée par le scheduler à chaque tick | |||
- Réveil : Quand `sleep_ticks == 0`, la tâche passe à READY | |||
- ''`priority`'' : Niveau de priorité | |||
- Valeurs : `PRIORITY_IDLE` (0) à `PRIORITY_HIGH` (3) | |||
- Utilisation : Actuellement non utilisée dans le scheduler Round-Robin simple | |||
==== ''4. Métadonnées d'Identification (`name`)'' ==== | |||
- ''Stockage'' : Pointeur vers la mémoire FLASH (PROGMEM) | |||
- ''Optimisation'' : Économise 6 octets par tâche vs stockage en RAM | |||
- ''Lecture'' : Via `pgm_read_word()` et affichage avec `uart_print_P()` | |||
=== ''Énumération des États (`task_state_t`)'' === | |||
<syntaxhighlight lang="c"> | |||
typedef enum { | |||
TASK_READY = 0, // Prête à être exécutée | |||
TASK_RUNNING, // Actuellement en cours d'exécution | |||
TASK_SLEEPING, // En sommeil temporisé | |||
TASK_WAITING_SPI, // En attente d'une opération SPI | |||
TASK_WAITING_SEMAPHORE, // En attente d'un sémaphore | |||
TASK_BLOCKED, // Bloquée sur une ressource | |||
TASK_COMPLETED // Terminée définitivement | |||
} task_state_t; | |||
</syntaxhighlight> | |||
[[Fichier:FSM Taches.png|vignette]] | |||
== ''Fonction Principale : `task_create()`'' == | |||
=== ''Prototype et Paramètres'' === | |||
<syntaxhighlight lang="c"> | |||
int8_t task_create(const char* name_pgm, // Nom (en FLASH) | |||
void (*function)(void*), // Fonction à exécuter | |||
void* arg, // Argument passé à la fonction | |||
uint8_t priority, // Priorité (0-3) | |||
uint8_t* stack_buffer, // Buffer pour la pile | |||
uint16_t stack_size); // Taille du buffer | |||
</syntaxhighlight> | |||
=== ''Séquence d'Exécution Détaillée'' === | |||
==== ''Étape 1 : Vérification des Limites'' ==== | |||
<syntaxhighlight lang="c"> | |||
if (task_count >= MAX_TASKS) return -1; | |||
</syntaxhighlight> | |||
- ''Condition'' : Vérifie qu'il reste des slots disponibles | |||
- ''MAX_TASKS'' : Défini dans `config.h` (typiquement 4) | |||
- ''Retour'' : -1 si la table est pleine, sinon ID de la tâche (0-3) | |||
==== ''Étape 2 : Attribution d'ID et Initialisation'' ==== | |||
<syntaxhighlight lang="c"> | |||
uint8_t task_id = task_count; // ID incrémental | |||
task_t* task = (task_t*)&task_table[task_id]; | |||
</syntaxhighlight> | |||
- ''ID'' : Attribué séquentiellement (0, 1, 2, 3...) | |||
- ''Pointeur'' : Accès direct au TCB dans la table | |||
==== ''Étape 3 : Remplissage des Champs du TCB'' ==== | |||
<syntaxhighlight lang="c"> | |||
task->function = function; | |||
task->arg = arg; | |||
task->state = TASK_READY; | |||
task->sleep_ticks = 0; | |||
task->priority = priority; | |||
task->stack_base = stack_buffer; | |||
task->stack_size = stack_size; | |||
task->name = name_pgm; | |||
</syntaxhighlight> | |||
- ''État initial'' : Toujours `TASK_READY` (prêt à être planifié) | |||
- ''Compteur sommeil'' : Initialisé à 0 (pas en sommeil) | |||
- ''Nom'' : Stocké comme pointeur FLASH (pas de copie en RAM) | |||
==== ''Étape 4 : Initialisation du Contexte'' ==== | |||
<syntaxhighlight lang="c"> | |||
task_create_context(task); | |||
</syntaxhighlight> | |||
- ''Appel'' : Fonction séparée pour la complexité d'initialisation de pile | |||
- ''Objectif'' : Préparer la pile pour la première exécution | |||
==== ''Étape 5 : Mise à Jour des Compteurs'' ==== | |||
<syntaxhighlight lang="c"> | |||
task_count++; | |||
return task_id; | |||
</syntaxhighlight> | |||
- ''Incrémentation'' : `task_count` global pour le prochain appel | |||
- ''Retour'' : ID de la tâche créée (0-3) ou -1 en cas d'erreur | |||
=== ''Complexité et Performances'' === | |||
- ''Temps d'exécution'' : ~150 cycles (~9.4µs @ 16MHz) | |||
- ''Utilisation mémoire'' : 16 octets dans `task_table` + taille de la pile | |||
- ''Appels système'' : Aucun (fonction non-interruptible) | |||
== ''Fonction Critique : `task_create_context()`'' == | |||
=== ''Rôle et Objectif'' === | |||
Cette fonction prépare le contexte d'exécution initial d'une nouvelle tâche en simulant une pile qui aurait été sauvegardée lors d'une interruption. C'est l'une des parties les plus complexes et critiques du système. | |||
=== ''Algorithme d'Initialisation de Pile'' === | |||
==== ''Préparation du Pointeur de Pile'' ==== | |||
<syntaxhighlight lang="c"> | |||
uint8_t* sp = task->stack_base + task->stack_size - 1; | |||
</syntaxhighlight> | |||
- ''Calcul'' : `sp` pointe vers le dernier octet utilisable de la pile | |||
- ''Philosophie'' : Pile descendante (décroissante en mémoire) | |||
- ''Alignement'' : Aucun alignement spécifique requis pour AVR 8-bit | |||
==== ''Empilement du Compteur de Programme (PC)'' ==== | |||
<syntaxhighlight lang="c"> | |||
uint16_t func_addr = (uint16_t)task->function; | |||
*sp-- = (uint8_t)(func_addr & 0xFF); // PC low | |||
*sp-- = (uint8_t)((func_addr >> 8) & 0xFF); // PC high | |||
</syntaxhighlight> | |||
- ''Little-endian'' : Octet faible en premier (convention AVR) | |||
- ''Valeur'' : Adresse de la fonction à exécuter | |||
- ''Simulation'' : Comme si `RETI` allait dépiler cette adresse | |||
==== ''Empilement des Registres Simulés'' ==== | |||
<syntaxhighlight lang="c"> | |||
// R0 (registre temporaire) | |||
*sp-- = 0x00; | |||
// SREG (Status Register) avec interruptions activées | |||
*sp-- = 0x80; // Bit I (Global Interrupt Enable) = 1 | |||
// R1 (doit être zéro selon convention AVR-GCC) | |||
*sp-- = 0x00; | |||
// R2 à R31 (tous initialisés à zéro) | |||
for (int i = 2; i <= 31; i++) { | |||
*sp-- = 0x00; | |||
} | |||
</syntaxhighlight> | |||
''Explication de l'Ordre d'Empilement :'' | |||
</syntaxhighlight> | |||
Ordre dans la pile (haut → bas) : | |||
[PC low][PC high][R0][SREG][R1][R2]...[R31] ← SP après initialisation | |||
</syntaxhighlight> | |||
Cet ordre correspond exactement à ce que `RESTORE_CONTEXT()` attend. | |||
==== ''Sauvegarde du Pointeur de Pile Initial'' ==== | |||
<syntaxhighlight lang="c"> | |||
task->stack_ptr = (uint16_t)sp; | |||
</syntaxhighlight> | |||
- ''Conversion'' : `uint8_t*` → `uint16_t` pour stockage dans le TCB | |||
- ''Valeur'' : Pointe vers le premier octet libre après initialisation | |||
=== ''Simulation du Comportement au Premier Réveil'' === | |||
Quand le scheduler exécute `RESTORE_CONTEXT()` sur cette tâche pour la première fois : | |||
==== ''Restauration de SP'' : `SP = task->stack_ptr` ==== | |||
==== ''Dépilement des registres'' : R31 → R2 → R1 → SREG → R0 ==== | |||
==== ''Restauration de SREG'' : `out __SREG__, r0` (active les interruptions) ==== | |||
==== ''Dépilement final'' : `pop r0` (récupère la valeur originale) ==== | |||
==== ''Retour'' : `reti` dépile PC et saute dans `task->function` ==== | |||
''Résultat'' : La tâche démarre exactement comme si elle avait été interrompue juste avant sa première instruction, avec les interruptions globales activées. | |||
== ''Fonction de Synchronisation : `task_sleep()`'' == | |||
=== ''Prototype et Comportement'' === | |||
<syntaxhighlight lang="c"> | |||
void task_sleep(uint16_t ticks); // ticks = nombre de ticks d'attente (1 tick = 10ms) | |||
</syntaxhighlight> | |||
=== ''Mécanisme d'Implémentation'' === | |||
==== ''Section Critique Atomique'' ==== | |||
<syntaxhighlight lang="c"> | |||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { | |||
task_table[current_task_id].sleep_ticks = ticks; | |||
task_table[current_task_id].state = TASK_SLEEPING; | |||
} | |||
</syntaxhighlight> | |||
''Décomposition de `ATOMIC_BLOCK` :'' | |||
<syntaxhighlight lang="c"> | |||
// Expansion de la macro | |||
do { \ | |||
uint8_t sreg_save = SREG; /* Sauvegarde SREG */ \ | |||
cli(); /* Désactive interruptions */ \ | |||
do { \ | |||
/* Code protégé */ \ | |||
task_table[current_task_id].sleep_ticks = ticks; \ | |||
task_table[current_task_id].state = TASK_SLEEPING; \ | |||
} while(0); \ | |||
SREG = sreg_save; /* Restaure SREG (et interruptions) */ \ | |||
} while(0) | |||
</syntaxhighlight> | |||
''Nécessité d'atomicité :'' | |||
- Lecture/écriture de `current_task_id` (partagé avec ISR) | |||
- Modification de `task_table` (structure partagée) | |||
- Évite les conditions de course entre code utilisateur et ISR | |||
==== ''Forçage de Commutation Immédiate'' ==== | |||
<syntaxhighlight lang="c"> | |||
TCNT1 = OCR1A - 1; | |||
</syntaxhighlight> | |||
- ''Objectif'' : Déclencher une interruption timer quasi-immédiate | |||
- ''Mécanisme'' : Positionne le compteur à une valeur qui déclenchera un compare match au cycle suivant | |||
- ''Résultat'' : Le scheduler s'exécute immédiatement et met la tâche en sommeil | |||
==== ''Boucle d'Attente Active'' ==== | |||
<syntaxhighlight lang="c"> | |||
while(task_table[current_task_id].state == TASK_SLEEPING); | |||
</syntaxhighlight> | |||
- ''Boucle vide'' : Attente active jusqu'au réveil par le scheduler | |||
- ''Préemption'' : La tâche est préemptée à chaque tick pendant cette boucle | |||
- ''Sortie'' : Quand le scheduler remet `state = TASK_READY` | |||
=== ''Exemple d'Utilisation'' === | |||
<syntaxhighlight lang="c"> | |||
// Attendre 1 seconde (100 ticks) | |||
task_sleep(100); | |||
// Attendre 500 ms (50 ticks) | |||
task_sleep(50); | |||
// Attendre 2.5 secondes (250 ticks) | |||
task_sleep(250); | |||
</syntaxhighlight> | |||
''Conversion ms → ticks :'' | |||
<math> | |||
\mathrm{ticks} = \frac{\mathrm{ms} + 9}{10} | |||
\quad (\text{arrondi au tick supérieur}) | |||
</math> | |||
== ''Fonction de Cession : `task_yield()`'' == | |||
=== ''Implémentation Minimaliste'' === | |||
<syntaxhighlight lang="c"> | |||
void task_yield(void) { | |||
schedule(); // Simple wrapper vers schedule() | |||
} | |||
</syntaxhighlight> | |||
=== ''Sémantique et Utilisation'' === | |||
''Objectif'' : Permettre à une tâche de céder volontairement le CPU aux autres tâches prêtes. | |||
''Cas d'utilisation typiques :'' | |||
1. ''Tâches de fond'' : Après avoir terminé une unité de travail | |||
2. ''Attentes actives'' : Dans les boucles d'attente non-critiques | |||
3. ''Coopération'' : Amélioration de la réactivité globale | |||
''Exemple :'' | |||
<syntaxhighlight lang="c"> | |||
void task_background_processing(void* arg) { | |||
while(1) { | |||
// Phase 1 : Traitement non critique | |||
process_data_batch(); | |||
// Céder le CPU aux autres tâches | |||
task_yield(); | |||
// Phase 2 : Suite du traitement | |||
process_more_data(); | |||
// Céder à nouveau | |||
task_yield(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
''Différence avec `task_sleep()` :'' | |||
- `task_yield()` : Cède immédiatement, reprend au prochain tour du scheduler | |||
- `task_sleep(1)` : Cède pour au moins 10ms | |||
== ''Fonction de Terminaison : `task_exit()`'' == | |||
=== ''Mécanisme de Terminaison Propre'' === | |||
<syntaxhighlight lang="c"> | |||
void task_exit(void) { | |||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { | |||
task_table[current_task_id].state = TASK_COMPLETED; | |||
} | |||
while(1) { | |||
task_yield(); // Ne plus jamais être sélectionné | |||
} | |||
} | |||
</syntaxhighlight> | |||
=== ''Analyse du Comportement'' === | |||
==== Marquage Atomique comme COMPLETED ==== | |||
* ''Section critique'' : Nécessaire car modification de `task_table` | |||
* ''État COMPLETED'' : La tâche est retirée de l'ordonnancement | |||
* ''Conservation'' : Le TCB reste dans la table mais n'est plus utilisé | |||
==== Boucle de Cession Infinie ==== | |||
<syntaxhighlight lang="c"> | |||
while(1) { | |||
task_yield(); | |||
} | |||
</syntaxhighlight> | |||
* ''Objectif'' : Empêcher la tâche de reprendre l'exécution | |||
* ''Préemption'' : À chaque `task_yield()`, le scheduler prend le contrôle | |||
* ''Énergie'' : La tâche consomme du temps CPU mais cède immédiatement | |||
=== ''Limitations et Considérations'' === | |||
''Ressources non libérées :'' | |||
- Pile mémoire non réutilisable | |||
- TCB occupé définitivement | |||
- Pas de mécanisme de nettoyage automatique | |||
''Recommandation d'usage :'' | |||
- Pour tâches qui ne doivent jamais terminer | |||
- Alternative : boucle infinie avec `while(1)` sans `task_exit()` | |||
== ''Variables Globales Partagées'' == | |||
=== ''Déclarations dans `task.c`'' === | |||
<syntaxhighlight lang="c"> | |||
extern volatile task_t task_table[MAX_TASKS]; | |||
extern volatile uint8_t current_task_id; | |||
extern volatile uint8_t task_count; | |||
</syntaxhighlight> | |||
=== ''Sémantique des Attributs `volatile`'' === | |||
''Pourquoi `volatile` ?'' | |||
1. ''Modification par l'ISR'' : Le scheduler modifie ces variables dans l'interruption timer | |||
2. ''Lecture par le code utilisateur'' : Les tâches lisent ces variables | |||
3. ''Optimisation du compilateur'' : Empêche la mise en cache dans les registres | |||
''Conséquences :'' | |||
- Toute lecture accède à la mémoire (pas de cache) | |||
- Toute écriture va immédiatement en mémoire | |||
- Garantit la cohérence entre ISR et code normal | |||
=== ''Table des Tâches (`task_table`)'' === | |||
- ''Type'' : Tableau de `task_t` de taille `MAX_TASKS` | |||
- ''Localisation'' : Segment .data en RAM | |||
- ''Accès'' : Indexation directe via `task_id` | |||
- ''Initialisation'' : Par `scheduler_init()` dans `scheduler.c` | |||
=== ''Identifiant de Tâche Courante (`current_task_id`)'' === | |||
- ''Portée'' : 0 à `MAX_TASKS - 1` | |||
- ''Modification'' : Par le scheduler dans `scheduler_update_logic()` | |||
- ''Lecture'' : Par `get_current_task_id()` et dans les sections critiques | |||
=== ''Compteur de Tâches (`task_count`)'' === | |||
- ''Valeur'' : Nombre de tâches créées avec succès | |||
- ''Incrémentation'' : Dans `task_create()` après création réussie | |||
- ''Maximum'' : Ne dépasse jamais `MAX_TASKS` | |||
== ''Fonctions auxiliaires '' == | |||
=== ''`task_get_count()` (Placeholder)'' === | |||
<syntaxhighlight lang="c"> | |||
uint8_t task_get_count(void) { | |||
return task_count; // Simple accesseur | |||
} | |||
</syntaxhighlight> | |||
''Utilité :'' | |||
- Surveillance système | |||
- Vérification de capacité | |||
- Statistiques d'exécution | |||
=== ''`task_get_current()` (Placeholder)'' === | |||
<syntaxhighlight lang="c"> | |||
task_t* task_get_current(void) { | |||
return (task_t*)&task_table[current_task_id]; | |||
} | |||
</syntaxhighlight> | |||
''Applications potentielles :'' | |||
- Inspection du contexte courant | |||
- Debugging avancé | |||
- Métriques de performance | |||
=== ''`task_get_name()` (Placeholder)'' === | |||
<syntaxhighlight lang="c"> | |||
const char* task_get_name(uint8_t task_id) { | |||
if (task_id >= MAX_TASKS) return NULL; | |||
return task_table[task_id].name; | |||
} | |||
</syntaxhighlight> | |||
''Utilisation :'' | |||
- Identification des tâches | |||
- Logging et traçage | |||
- Interface utilisateur | |||
= Documentation Technique Approfondie - Couche Matérielle (SPI & SD) = | |||
Cette documentation technique se concentre exclusivement sur les couches basses du système : le bus SPI et le pilote de carte SD. Elle décortique l'implémentation actuelle pour le microcontrôleur AT90USB1286, en analysant chaque manipulation de registre et contrainte temporelle. | |||
== Module 1 : Driver SPI (Serial Peripheral Interface) == | |||
Le module SPI (spi.c) est configuré pour opérer en mode Maître Synchrone. Il agit comme le chef d'orchestre pour la communication avec les périphériques externes. | |||
=== 1.1 Architecture Physique et Pinout === | |||
L'implémentation force l'utilisation du contrôleur SPI matériel intégré au Port B. La configuration des registres de direction (DDR) est critique pour maintenir le mode Maître. | |||
{| class="wikitable" | |||
! Signal !! Pin AVR !! Registre !! Direction !! État Logique !! Fonctionnalité Détaillée | |||
|- | |||
| SS || PB0 || DDRB || Sortie || HIGH || Slave Select (Hardware). Bien que non utilisé pour sélectionner un esclave spécifique dans ce code, ce pin DOIT être configuré en sortie. Si PB0 est configuré en entrée et passe à l'état LOW, le hardware AVR bascule automatiquement le contrôleur SPI en mode Esclave, crashant la communication maître. | |||
|- | |||
| SCK || PB1 || DDRB || Sortie || Pulsé || Serial Clock. Génère le signal d'horloge qui synchronise les échanges de données. | |||
|- | |||
| MOSI || PB2 || DDRB || Sortie || Données || Master Out Slave In. Ligne de transmission des données du MCU vers la SD. | |||
|- | |||
| MISO || PB3 || DDRB || Entrée || Données || Master In Slave Out. Ligne de réception. Nécessite que le périphérique esclave pilote activement la ligne (ou pull-up externe). | |||
|- | |||
| SD_CS|| PB7 || DDRB || Sortie || Actif LOW || Chip Select (Carte SD). Ligne de contrôle dédiée. | |||
|} | |||
=== 1.2 Mécanisme de Sélection des Périphériques (Chip Select Routing) === | |||
La fonction spi_select_device(uint8_t id) ne se contente pas de basculer des pins ; elle gère l'intégrité électrique du bus pour éviter les contentions (bus contention) où deux esclaves tenteraient de piloter la ligne MISO simultanément. | |||
'''Séquence Chronologique Stricte :''' | |||
Phase de Désélection (Safety Guard) : | |||
* Appel de spi_deselect_all(). | |||
* Action : Force tous les pins CS (PB7, PF2, PF4, PA0, PA3, PA6) à l'état logique 1 (HIGH) via des opérations OR bit-à-bit sur les registres PORTx. | |||
* Résultat : Tous les périphériques passent leur ligne MISO en état haute impédance (Hi-Z). | |||
Temps de Garde (Discharge Time) : | |||
* _delay_us(10) : Une pause de 10 microsecondes est insérée. | |||
* Justification : Permet à la capacité parasite du bus SPI de se décharger et assure que l'esclave précédent a totalement relâché la ligne MISO avant que le suivant ne soit activé. | |||
Phase d'Activation : | |||
* Le code applique un masque binaire inverse (&= ~) sur le port correspondant à l'ID cible. | |||
* Exemple pour SD (ID 5) : PORTB &= ~(1 << PB7). | |||
Temps d'Établissement (Setup Time) : | |||
* _delay_us(10) : Seconde pause avant toute transmission d'horloge. | |||
* Justification : Certains périphériques lents (comme les vieilles cartes SD en mode SPI) nécessitent quelques microsecondes après la chute du CS avant de pouvoir échantillonner le premier bit d'horloge. | |||
=== 1.3 Configuration Avancée des Registres (Clocking) === | |||
La fonction spi_set_speed(uint8_t speed_mode) reconfigure dynamiquement le pré-diviseur d'horloge du périphérique SPI en manipulant le registre de contrôle SPCR et le registre de statut SPSR. | |||
==== Mode Initialisation (SPI_SPEED_SLOW) ==== | |||
Utilisé obligatoirement lors du démarrage de la carte SD (fréquence requise < 400 kHz). | |||
Formule : F_OSC / 128 | |||
Calcul : 16,000,000 Hz / 128 = 125 kHz. | |||
Configuration Registres : | |||
** SPCR |= (1 << SPR1) | (1 << SPR0) : Sélectionne le diviseur /128. | |||
** SPSR &= ~(1 << SPI2X) : Désactive le doubleur de fréquence. | |||
==== Mode Transfert (SPI_SPEED_FAST) ==== | |||
Utilisé pour les échanges de données massifs. | |||
<pre> | |||
Formule : F_OSC / 2 | |||
Calcul : 16,000,000 Hz / 2 = 8 MHz (Débit théorique max : 1 Mo/s). | |||
</pre> | |||
Configuration Registres : | |||
** SPCR &= ~((1 << SPR1) | (1 << SPR0)) : Sélectionne le diviseur base /4. | |||
** SPSR |= (1 << SPI2X) : Active le doubleur de fréquence (transforme /4 en /2). | |||
=== 1.4 Protocole de Transfert Atomique === | |||
La fonction spi_transfer(uint8_t data) implémente un échange full-duplex bloquant (polling). | |||
<syntaxhighlight lang="c"> | |||
uint8_t spi_transfer(uint8_t data) { | |||
// 1. Écriture dans le registre de données | |||
// Ceci déclenche AUTOMATIQUEMENT 8 cycles d'horloge sur SCK | |||
SPDR = data; | |||
// 2. Boucle d'attente active (Polling) | |||
// On surveille le bit SPIF (SPI Interrupt Flag) dans le registre SPSR | |||
// Ce bit passe à 1 quand la transmission est terminée | |||
while (!(SPSR & (1 << SPIF))); | |||
// 3. Lecture du registre de données | |||
// Le registre SPDR contient maintenant l'octet reçu via MISO | |||
// pendant que nous envoyions nos données via MOSI | |||
return SPDR; | |||
} | |||
</syntaxhighlight> | |||
== Module 2 : Driver Carte SD (Protocole SPI) == | |||
Le fichier sd.c implémente une machine à états finie pour initialiser et piloter des cartes SDSC (Standard Capacity) et SDHC (High Capacity) via le mode SPI "Legacy". | |||
=== 2.1 Macros de Contrôle Temporel === | |||
Les macros CS_LOW et CS_HIGH ne sont pas de simples bascules de bits. Elles intègrent un délai de propagation critique. | |||
<syntaxhighlight lang="c"> | |||
// Délai de 4µs ajouté APRES chaque changement d'état du Chip Select | |||
// Cela garantit le respect du timing T_CS (Chip Select Setup Time) | |||
// spécifié dans la norme SD Physical Layer (généralement 5ns min, mais 4µs est sécuritaire) | |||
#define CS_LOW() { SD_PORT &= ~(1 << PIN_CS); _delay_us(4); } | |||
</syntaxhighlight> | |||
=== 2.2 Séquence d'Initialisation (Deep Dive) === | |||
L'initialisation sd_init() est une procédure séquentielle rigide. Tout écart entraîne un échec immédiat. | |||
{| class="wikitable" | |||
! Étape !! Action Technique !! Justification Bas-Niveau | |||
|- | |||
| 1. Power-On || CS_HIGH + 80 coups d'horloge (10 x 0xFF) || La carte SD démarre en mode SD Bus natif. Pour la forcer en mode SPI, il faut lui envoyer >74 cycles d'horloge avec la ligne CS inactive (HIGH) et MOSI HIGH. | |||
|- | |||
| 2. CMD0 || Envoi commande 0x400000000095 || GO_IDLE_STATE. Reset logiciel. Le CRC (0x95) est obligatoire pour cette commande uniquement car le contrôle CRC est activé par défaut avant le passage en mode SPI. Attente réponse R1 = 0x01 (Idle). | |||
|- | |||
| 3. CMD8 || Envoi commande 0x48000001AA87 || SEND_IF_COND. Vérifie la plage de tension (2.7-3.6V). | |||
Argument 0x1AA : 1 = Voltage Supply VHS (2.7-3.6V), AA = Check Pattern. | |||
La carte doit écho le pattern AA. Si erreur 0x05 (Illegal Command), c'est une vieille carte V1.x (non gérée ici). | |||
|- | |||
| 4. ACMD41 || Boucle CMD55 + CMD41 || SD_SEND_OP_COND. C'est la commande d'initialisation réelle. | |||
ACMD signifie "Application Specific Command". Il faut d'abord envoyer CMD55 pour dire "la prochaine est une ACMD". | |||
On boucle tant que la réponse n'est pas 0x00 (Carte Prête/Active). Si reste 0x01, la carte s'initialise encore. | |||
|- | |||
| 5. CMD58 || Envoi commande CMD58 || READ_OCR. Lit l'Operation Conditions Register (32 bits). | |||
Bit 30 (CCS - Card Capacity Status) : Si 1, carte haute capacité (SDHC). Si 0, capacité standard (SDSC). | |||
Cette information est stockée dans la variable statique is_high_capacity pour adapter l'adressage plus tard. | |||
|} | |||
=== 2.3 Stratégie d'Adressage Mémoire (SDSC vs SDHC) === | |||
Le driver résout la différence fondamentale d'adressage entre les générations de cartes lors de chaque lecture/écriture. | |||
Problème : | |||
** SDSC (Standard) : Adressage par Octet (Byte Addressing). Le secteur 2 est à l'adresse 1024 (2 * 512). | |||
** SDHC (High Cap) : Adressage par Bloc (Block Addressing). Le secteur 2 est à l'adresse 2. | |||
Solution Implémentée : | |||
<syntaxhighlight lang="c"> | |||
// Dans sd_read_block et sd_write_block : | |||
uint32_t addr; | |||
if (is_high_capacity) { | |||
// SDHC : L'adresse est directement le numéro de secteur | |||
addr = sector; | |||
} else { | |||
// SDSC : Conversion Secteur -> Octet | |||
// Décalage de 9 bits vers la gauche équivaut à multiplier par 512 | |||
// (2^9 = 512). Plus rapide qu'une multiplication matérielle. | |||
addr = sector << 9; | |||
} | |||
</syntaxhighlight> | |||
=== 2.4 Analyse de la Lecture de Bloc (sd_read_block) === | |||
Cette fonction est critique pour la performance et la stabilité. Elle utilise une boucle de synchronisation stricte. | |||
Commande : Envoi CMD17 (READ_SINGLE_BLOCK) avec l'adresse calculée. | |||
Attente Réponse R1 : Doit être 0x00. Si erreur (ex: 0x05 adresse hors limite), abandon immédiat. | |||
Attente Token de Données : | |||
* La carte ne répond pas immédiatement. Elle peut prendre du temps pour chercher les données dans sa flash NAND interne. | |||
* Le bus MISO reste à 1 (HIGH) tant qu'elle est occupée. | |||
* Le driver boucle jusqu'à recevoir 0xFE (Start Block Token). | |||
* Timeout de Sécurité : Un compteur timeout incrémente jusqu'à 40,000. | |||
** Calcul du temps : 40,000 tours * ~8µs/tour (transfert SPI + overhead boucle) ≈ 320ms. C'est large (la spec SD demande timeout max 100ms), mais sécuritaire. | |||
Transfert de la Charge Utile (Payload) : | |||
* Une boucle for(i=0; i<512; i++) lit les données octet par octet. | |||
* Optimisation possible : Actuellement, le code stocke dans un buffer RAM. Pas de DMA utilisé ici. | |||
CRC Ignore : | |||
* La carte envoie 2 octets de CRC (Cyclic Redundancy Check) à la fin des 512 octets. | |||
* Le code exécute spi_transfer(0xFF) deux fois pour "consommer" ces octets sans les vérifier. | |||
=== 2.5 Analyse de l'Écriture de Bloc (sd_write_block) === | |||
L'écriture est plus complexe car elle implique une vérification de la réussite de la programmation flash interne de la carte. | |||
Commande : Envoi CMD24 (WRITE_BLOCK). | |||
Insertion Token : Envoi d'un octet vide 0xFF (séparateur) puis du Token de début de données 0xFE. | |||
Envoi Données : Transmission des 512 octets du buffer. | |||
CRC Dummy : Envoi de 2 octets 0xFF (CRC factice, car CRC désactivé par défaut en mode SPI). | |||
Lecture Réponse de Données (Data Response Token) : | |||
* La carte répond immédiatement avec un octet de statut sous le format xxx00101 (mask 0x1F). | |||
* 0x05 (00000101) : Data Accepted. | |||
* 0x0B (00001011) : CRC Error. | |||
* 0x0D (00001101) : Write Error. | |||
Attente de Fin de Programmation (Busy Wait) : | |||
* Après acceptation, la carte passe la ligne MISO à 0 (LOW) tant qu'elle écrit physiquement en Flash. | |||
* Le driver boucle while(spi_transfer(0xFF) == 0x00) tant que la carte est occupée (Busy). | |||
* Cette étape peut durer plusieurs millisecondes. | |||
= Documentation Technique - Analyse des Fonctions (Code Level) = | |||
Cette section analyse le comportement exact des fonctions implémentées dans spi.c et sd.c. | |||
== Module SPI (spi.c) - Fonctions du Driver == | |||
Le fichier spi.c gère la communication bas niveau. Voici le détail de chaque fonction. | |||
=== void spi_init(void) === | |||
Rôle : Initialise le contrôleur SPI matériel au démarrage. | |||
Étape 1 (Direction des Pins) : Configure MOSI, SCK, SS et CS_SD (PB7) en Sortie. | |||
Critique : Force SS (PB0) à l'état HAUT. Si ce pin passe en entrée LOW, le mode Maître saute. | |||
Étape 2 (Configuration Registre SPCR) : | |||
Active le SPI (SPE). | |||
Active le mode Maître (MSTR). | |||
Définit la vitesse initiale à f_osc/128 (SPR1 | SPR0). C'est la vitesse "safe" (125 kHz) requise pour réveiller la carte SD. | |||
Étape 3 (Nettoyage) : Désactive le bit SPI2X dans SPSR pour être sûr de ne pas être en double vitesse. | |||
=== void spi_set_speed(uint8_t speed_mode) === | |||
Rôle : Change la vitesse du bus à la volée. | |||
Argument : speed_mode (SPI_SPEED_SLOW ou SPI_SPEED_FAST). | |||
Logique : | |||
Nettoie d'abord les bits de prescaler (SPR1, SPR0) et double vitesse (SPI2X). | |||
Si FAST : Active SPI2X (Diviseur /2). Résultat : 8 MHz (rapide pour transferts). | |||
Si SLOW : Active SPR1 | SPR0 (Diviseur /128). Résultat : 125 kHz (pour init). | |||
=== uint8_t spi_transfer(uint8_t data) === | |||
Rôle : Envoie un octet et reçoit un octet simultanément (Full Duplex). | |||
Argument : data (l'octet à envoyer). Pour juste lire, on envoie souvent 0xFF (Dummy). | |||
Implémentation : | |||
Écrit data dans SPDR (Lance l'horloge). | |||
Boucle bloquante : Attend que le bit SPIF (Interrupt Flag) passe à 1 dans SPSR. | |||
Retourne la valeur lue dans SPDR. | |||
=== void spi_select_device(uint8_t id) === | |||
Rôle : Active un périphérique spécifique en gérant la sécurité électrique. | |||
Argument : id (Identifiant du périphérique, ex: DEV_ID_SD). | |||
Algorithme : | |||
Appelle spi_deselect_all() : Passe TOUS les CS à l'état HAUT (1). | |||
_delay_us(10) : Attend que la ligne MISO se libère (haute impédance). | |||
switch(id) : Passe le CS ciblé à l'état BAS (0). | |||
_delay_us(10) : Attend que l'esclave soit prêt à recevoir l'horloge. | |||
== Module SD (sd.c) - Fonctions Carte Mémoire == | |||
Ce module implémente le protocole SD SPI complet. | |||
=== uint8_t sd_init(void) === | |||
Rôle : Réveille et configure la carte SD (SDSC ou SDHC). | |||
Retourne : 0 si succès, 1 si erreur. | |||
Séquence Exécutée : | |||
spi_init_slow() : Force le bus à 125 kHz (interne statique). | |||
Power Up : Envoie 10 octets 0xFF avec CS HAUT pour lancer l'horloge interne de la carte. | |||
CMD0 (Reset) : Met la carte en mode Idle. Si réponse != 0x01, échec. | |||
CMD8 (Voltage) : Vérifie si la carte accepte 3.3V. Argument 0x1AA. | |||
ACMD41 (Init) : Boucle CMD55 + CMD41 jusqu'à ce que la réponse soit 0x00 (Carte prête). | |||
CMD58 (OCR) : Lit le registre OCR. Vérifie le bit 30 pour savoir si c'est une carte haute capacité (is_high_capacity = 1). | |||
spi_init_fast() : Passe le bus à 8 MHz pour les transferts futurs. | |||
=== uint8_t sd_read_block(uint32_t sector, uint8_t *buffer) === | |||
Rôle : Lit 512 octets depuis un secteur donné. | |||
Arguments : | |||
sector : Numéro du secteur (0 à N). | |||
buffer : Pointeur vers un tableau d'au moins 512 octets. | |||
Logique Détaillée : | |||
Calcul Adresse : | |||
Si is_high_capacity (SDHC) : addr = sector. | |||
Si Standard (SDSC) : addr = sector << 9 (multiplié par 512). | |||
CS_LOW() : Sélectionne la carte. | |||
Envoi CMD17 : Demande de lecture. Si réponse != 0x00, erreur. | |||
Attente Token (Boucle Critique) : | |||
Envoie 0xFF en boucle tant que la réponse est 0xFF. | |||
Attend le token 0xFE (Start Block). | |||
Timeout : Si > 40,000 itérations sans 0xFE, retourne erreur 2. | |||
Lecture Données : Boucle for de 0 à 511 pour remplir buffer[]. | |||
CRC : Lit (et ignore) 2 octets de CRC. | |||
CS_HIGH() : Libère la carte. | |||
=== uint8_t sd_write_block(uint32_t sector, const uint8_t *buffer) === | |||
Rôle : Écrit 512 octets dans un secteur. | |||
Arguments : sector cible, buffer source. | |||
Logique Détaillée : | |||
Calcul Adresse : Idem lecture (shift si SDSC). | |||
CS_LOW() + Envoi CMD24 (Write Block). | |||
Envoi Token Start : Envoie d'abord un 0xFF (dummy) puis 0xFE (Start Data Token). | |||
Envoi Données : Boucle for 0 à 511 pour envoyer le contenu de buffer. | |||
CRC Factice : Envoie deux fois 0xFF (CRC ignoré par la carte en mode SPI par défaut). | |||
Vérification Data Response : | |||
Lit un octet de réponse. | |||
Applique le masque 0x1F. Si le résultat n'est pas 0x05 (Data Accepted), c'est une erreur d'écriture (retourne 3). | |||
Attente Busy (Boucle) : | |||
La carte maintient MISO à 0 tant qu'elle grave la Flash. | |||
Boucle while(spi_transfer(0xFF) == 0x00) jusqu'à libération. | |||
CS_HIGH(). | |||
=== static uint8_t sd_cmd(...) (Fonction Interne) === | |||
Rôle : Fonction helper (non visible dans .h) pour envoyer une commande brute. | |||
Mécanique : | |||
Envoie 0x40 | cmd (Index commande). | |||
Découpe l'argument 32 bits en 4 octets (MSB d'abord). | |||
Envoie le CRC (calculé ou pré-calculé comme 0x95 pour CMD0). | |||
Attente Réponse (R1) : | |||
Envoie 0xFF en boucle (max 10 essais) tant que le bit 7 de la réponse est 1. | |||
Retourne l'octet de réponse (ex: 0x00 = Succès, 0x01 = Idle, autres = Erreurs). | |||
= Documentation Technique du Système de Fichiers FAT16 = | |||
== Introduction Générale au FAT16 == | |||
Le système FAT16 (File Allocation Table 16-bit) est un système de fichiers historique développé par Microsoft. Sa simplicité le rend idéal pour les systèmes embarqués. Cette implémentation supporte '''cartes SD/SDHC''' avec '''partitionnement MBR''' optionnel. | |||
== Structures de Données Fondamentales == | |||
[[Fichier:Shield FS.mp4|vignette]] | |||
=== Boot Sector (Secteur d'Amorçage) === | |||
<syntaxhighlight lang="c"> | |||
typedef struct __attribute__((packed)) { | |||
uint8_t jump_boot[3]; // Code de saut vers le bootloader | |||
char oem_name[8]; // Nom du formatage (ex: "MSDOS5.0") | |||
uint16_t bytes_per_sector; // Octets par secteur (généralement 512) | |||
uint8_t sectors_per_cluster; // Secteurs par cluster (1,2,4,8...) | |||
uint16_t reserved_sectors; // Secteurs réservés (incluant le boot sector) | |||
uint8_t num_fats; // Nombre de tables FAT (généralement 2) | |||
uint16_t root_entries; // Nombre max d'entrées dans le répertoire racine | |||
uint16_t total_sectors_16; // Nombre total de secteurs (si ≤ 65535) | |||
uint8_t media_type; // Type de média (0xF8 pour disques fixes) | |||
uint16_t sectors_per_fat; // Secteurs par table FAT | |||
uint16_t sectors_per_track; // Géométrie disque (obsolète) | |||
uint16_t num_heads; // Géométrie disque (obsolète) | |||
uint32_t hidden_sectors; // Secteurs cachés (partition) | |||
uint32_t total_sectors_32; // Nombre total de secteurs (si > 65535) | |||
uint8_t drive_number; // Numéro de lecteur BIOS | |||
uint8_t reserved1; // Réservé | |||
uint8_t boot_signature; // Signature d'amorçage (0x29) | |||
uint32_t volume_id; // ID unique du volume | |||
char volume_label[11]; // Étiquette du volume | |||
char file_system_type[8]; // Type de système de fichiers ("FAT16 ") | |||
uint8_t boot_code[448]; // Code d'amorçage (non utilisé ici) | |||
uint16_t boot_signature_end; // Signature de fin (0xAA55) | |||
} fat16_boot_sector_t; | |||
</syntaxhighlight> | |||
'''Attribut <code>packed</code>''' : Empêche le compilateur d'ajouter du padding entre les champs. Essentiel car la structure doit correspondre exactement aux 512 octets du secteur. | |||
'''Champs critiques pour FAT16''' : | |||
- <code>sectors_per_cluster</code> : Unité d'allocation de base | |||
- <code>reserved_sectors</code> : Inclut le secteur de boot | |||
- <code>root_entries</code> : Maximum 512 entrées dans la racine (16 secteurs × 32 entrées) | |||
- <code>sectors_per_fat</code> : Taille de chaque table FAT | |||
=== Directory Entry (Entrée de Répertoire) === | |||
<syntaxhighlight lang="c"> | |||
typedef struct __attribute__((packed)) { | |||
char filename[8]; // Nom (espace complément en fin) | |||
char extension[3]; // Extension (espace complément) | |||
uint8_t attributes; // Attributs du fichier | |||
uint8_t reserved; // Réservé (NT) | |||
uint8_t creation_time_tenths; // Dixièmes de seconde (0-199) | |||
uint16_t creation_time; // Heure:5 bits, minutes:6 bits, secondes:5 bits | |||
uint16_t creation_date; // Année:7 bits, mois:4 bits, jour:5 bits | |||
uint16_t last_access_date; // Dernier accès (date uniquement) | |||
uint16_t first_cluster_high; // Cluster haut (toujours 0 en FAT16) | |||
uint16_t last_write_time; // Dernière modification (heure) | |||
uint16_t last_write_date; // Dernière modification (date) | |||
uint16_t first_cluster_low; // Premier cluster du fichier | |||
uint32_t file_size; // Taille en octets | |||
} fat16_dir_entry_t; | |||
</syntaxhighlight> | |||
'''Format 8.3''' : Nom de 8 caractères + extension de 3 caractères. Les espaces vides sont remplis avec des espaces (0x20). | |||
'''Attributs (bits)''' : | |||
- 0x01 : Lecture seule | |||
- 0x02 : Caché | |||
- 0x04 : Système | |||
- 0x08 : Étiquette de volume | |||
- 0x10 : Sous-répertoire | |||
- 0x20 : Archive (utilisé pour les fichiers normaux) | |||
- 0x0F : Entrée LFN (Long File Name) - ignorée dans cette implémentation | |||
'''Champs de date/heure''' : Format Microsoft spécifique. Non utilisé dans cette implémentation pour simplifier. | |||
'''First cluster''' : Les clusters 0 et 1 sont réservés. Les clusters valides commencent à 2. | |||
== Variables Globales et Architecture == | |||
=== Buffer Partagé === | |||
<syntaxhighlight lang="c"> | |||
static uint8_t shared_buffer[512]; | |||
</syntaxhighlight> | |||
'''Philosophie d'économie mémoire''' : Au lieu d'allouer des buffers locaux dans chaque fonction (sur la pile), un buffer global est réutilisé. Ceci économise la RAM limitée de l'AVR. | |||
'''Problème de réentrance''' : Cette approche n'est pas thread-safe. Si plusieurs tâches/tâches utilisent le système de fichiers simultanément, les données seront corrompues. Dans ce système, seul un thread à la fois peut utiliser les fonctions FAT. | |||
=== Variables d'État du Système de Fichiers === | |||
<syntaxhighlight lang="c"> | |||
static uint32_t partition_lba = 0; // LBA de la partition (0 si pas de partition) | |||
static uint32_t fat_start_sector = 0; // Secteur de début des tables FAT | |||
static uint32_t root_dir_sector = 0; // Secteur du répertoire racine | |||
static uint32_t data_sector = 0; // Secteur de début de la zone de données | |||
static uint16_t sectors_per_cluster = 0; | |||
static uint16_t sectors_per_fat = 0; | |||
static uint8_t num_fats = 0; | |||
static uint8_t is_mounted = 0; | |||
</syntaxhighlight> | |||
'''Calcul des positions clés''' : | |||
1. <code>fat_start_sector</code> = partition_lba + reserved_sectors | |||
2. <code>root_dir_sector</code> = fat_start_sector + (num_fats × sectors_per_fat) | |||
3. <code>data_sector</code> = root_dir_sector + (root_entries × 32 / 512) | |||
'''Zone de données''' : Les clusters sont numérotés à partir de 2. Pour convertir un cluster en LBA : | |||
<pre> | |||
LBA = data_sector + ((cluster - 2) × sectors_per_cluster) | |||
</pre> | |||
== Fonctions Utilitaires de Bas Niveau == | |||
=== Fonctions d'Accès Secteur === | |||
<syntaxhighlight lang="c"> | |||
uint8_t read_sector(uint32_t sector) { | |||
return sd_read_block(sector, shared_buffer); | |||
} | |||
uint8_t write_sector(uint32_t sector) { | |||
return sd_write_block(sector, shared_buffer); | |||
} | |||
</syntaxhighlight> | |||
''' | '''Wrapper simples''' : Abstraction au-dessus du driver SD. Toutes les lectures/écritures passent par <code>shared_buffer</code>. | ||
=== Fonction d'Affichage de Nombre === | |||
<syntaxhighlight lang="c"> | |||
void print_number(uint32_t n) { | |||
if (n == 0) { uart_putchar('0'); return; } | |||
char buf[12]; uint8_t i = 0; | |||
while (n > 0) { buf[i++] = '0' + (n % 10); n /= 10; } | |||
while (i > 0) { uart_putchar(buf[--i]); } | |||
} | |||
</syntaxhighlight> | |||
'''Conversion décimale manuelle''' : Alternative à <code>printf</code> qui est trop lourd pour l'AVR. Stocke les chiffres dans un buffer temporaire puis les affiche en ordre inverse. | |||
=== Parsing de Nom de Fichier === | |||
<syntaxhighlight lang="c"> | |||
void fat16_parse_filename(const char* input, char* out_name, char* out_ext) { | |||
memset(out_name, ' ', 8); memset(out_ext, ' ', 3); | |||
uint8_t i = 0, j = 0; | |||
// Copie du nom (8 caractères max) | |||
while (input[i] != '\0' && input[i] != '.' && j < 8) { | |||
char c = input[i++]; | |||
if (c >= 'a' && c <= 'z') c -= 32; // Conversion en majuscules | |||
out_name[j++] = c; | |||
} | |||
// Recherche de l'extension | |||
while (input[i] != '\0' && input[i] != '.') i++; | |||
if (input[i] == '.') { | |||
i++; j = 0; | |||
while (input[i] != '\0' && j < 3) { | |||
char c = input[i++]; | |||
if (c >= 'a' && c <= 'z') c -= 32; // Conversion en majuscules | |||
out_ext[j++] = c; | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
'''Normalisation FAT16''' : | |||
1. '''Majuscules obligatoires''' : FAT16 stocke en majuscules | |||
2. '''Padding avec espaces''' : Nom = 8 caractères, extension = 3 caractères | |||
3. '''Format 8.3''' : Si le nom fait moins de 8 caractères, le reste est rempli d'espaces | |||
'''Exemple''' : <code>"test.txt"</code> → Nom: <code>"TEST "</code>, Extension: <code>"TXT"</code> | |||
== Initialisation et Montage == | |||
=== Fonction <code>fat16_init()</code> === | |||
<syntaxhighlight lang="c"> | |||
uint8_t fat16_init(void) { | |||
if (is_mounted) return 1; | |||
uart_println("[FAT] Init..."); | |||
// Lecture du secteur 0 (MBR ou Boot Sector) | |||
if (!read_sector(0)) return 0; | |||
// Détection de partition : vérifie si c'est un secteur de boot valide | |||
if (shared_buffer[0] != 0xEB && shared_buffer[0] != 0xE9) { | |||
// C'est un MBR, extraire la partition LBA | |||
memcpy(&partition_lba, &shared_buffer[0x1C6], sizeof(uint32_t)); | |||
if (!read_sector(partition_lba)) return 0; | |||
} else { | |||
partition_lba = 0; // Pas de partition, boot sector directement | |||
} | |||
// Parsing du boot sector | |||
fat16_boot_sector_t* bs = (fat16_boot_sector_t*)shared_buffer; | |||
sectors_per_cluster = bs->sectors_per_cluster; | |||
if (sectors_per_cluster == 0) return 0; // Invalide | |||
// Calcul des positions | |||
sectors_per_fat = bs->sectors_per_fat; | |||
num_fats = bs->num_fats; | |||
fat_start_sector = partition_lba + bs->reserved_sectors; | |||
root_dir_sector = fat_start_sector + (num_fats * sectors_per_fat); | |||
uint16_t root_dir_size = (bs->root_entries * 32) / 512; | |||
data_sector = root_dir_sector + root_dir_size; | |||
is_mounted = 1; | |||
uart_println("OK"); | |||
return 1; | |||
} | |||
</syntaxhighlight> | |||
'''Détection automatique partition/MBR''' : | |||
- '''MBR''' : Premier octet ≠ 0xEB ou 0xE9 (instructions de saut) | |||
''' | - '''Offset 0x1C6''' : Dans cette implémentation, on suppose la partition active à cet offset (typiquement 0x1BE+8 pour la première partition) | ||
- '''Boot Sector direct''' : Si pas de MBR, on lit directement le boot sector | |||
''' | '''Validation minimale''' : Vérifie que <code>sectors_per_cluster</code> ≠ 0 | ||
== Gestion de la File Allocation Table == | |||
=== Structure de la FAT === | |||
La FAT est un tableau de 16-bit entries. Chaque entrée correspond à un cluster et contient : | |||
- <code>0x0000</code> : Cluster libre | |||
- <code>0x0002-0xFFEF</code> : Prochain cluster dans la chaîne | |||
- <code>0xFFF0-0xFFF6</code> : Réservé | |||
- <code>0xFFF7</code> : Cluster défectueux | |||
- <code>0xFFF8-0xFFFF</code> : Fin de chaîne (EOF) | |||
' | === Lecture d'une Entrée FAT === | ||
<syntaxhighlight lang="c"> | |||
uint16_t get_fat_entry(uint16_t cluster) { | |||
uint16_t sec = cluster / 256; // Chaque secteur contient 256 entrées (512/2) | |||
uint16_t ent = cluster % 256; // Index dans le secteur | |||
if (!read_sector(fat_start_sector + sec)) return 0xFFFF; | |||
return ((uint16_t*)shared_buffer)[ent]; | |||
} | |||
</syntaxhighlight> | |||
'''Calcul d'adressage''' : | |||
- 512 octets par secteur ÷ 2 octets par entrée = 256 entrées par secteur | |||
- <code>cluster / 256</code> donne le secteur dans la FAT | |||
- <code>cluster % 256</code> donne l'index dans le secteur | |||
=== Écriture d'une Entrée FAT === | |||
<syntaxhighlight lang="c"> | |||
void set_fat_entry(uint16_t cluster, uint16_t value) { | |||
uint16_t sec = cluster / 256; | |||
uint16_t ent = cluster % 256; | |||
if (read_sector(fat_start_sector + sec)) { | |||
((uint16_t*)shared_buffer)[ent] = value; | |||
write_sector(fat_start_sector + sec); | |||
} | |||
// Mise à jour de la seconde FAT (miroir) | |||
if (num_fats > 1) write_sector(fat_start_sector + sectors_per_fat + sec); | |||
} | |||
</syntaxhighlight> | |||
''' | '''Mirroring des FAT''' : FAT16 maintient généralement 2 copies identiques de la table. Toute modification doit être répercutée sur toutes les copies. | ||
'' | '''Non-atomicité''' : Si le système s'arrête entre les deux écritures, les FAT peuvent être incohérentes. | ||
''' | |||
=== Recherche d'un Cluster Libre === | |||
< | <syntaxhighlight lang="c"> | ||
</ | uint16_t find_free_cluster(void) { | ||
for (uint16_t i = 0; i < 4; i++) { | |||
if (!read_sector(fat_start_sector + i)) return 0; | |||
uint16_t* fat_table = (uint16_t*)shared_buffer; | |||
for (uint16_t j = 0; j < 256; j++) { | |||
if (fat_table[j] == 0x0000 && (i*256 + j) >= 2) return (i * 256) + j; | |||
} | |||
} | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
''' | '''Recherche limitée''' : Ne recherche que dans les 4 premiers secteurs de la FAT (1024 clusters). Cette limite est arbitraire et pourrait être étendue. | ||
'''Clusters réservés''' : Les clusters 0 et 1 sont réservés, donc on vérifie <code>(i*256 + j) >= 2</code>. | |||
''' | '''Performance''' : Recherche séquentielle - O(n). Pour un système avec beaucoup de fichiers, une bitmap serait plus efficace. | ||
' | === Libération d'une Chaîne de Clusters === | ||
<syntaxhighlight lang="c"> | |||
void free_cluster_chain(uint16_t cluster) { | |||
while (cluster >= 2 && cluster < 0xFFF8) { | |||
uint16_t next = get_fat_entry(cluster); | |||
set_fat_entry(cluster, 0x0000); // Marque comme libre | |||
cluster = next; | |||
} | |||
} | |||
</syntaxhighlight> | |||
'''Parcours de la chaîne''' : Suit les liens dans la FAT jusqu'à trouver un marqueur EOF. | |||
'''Condition de sortie''' : <code>cluster < 0xFFF8</code> s'assure de ne pas traiter les marqueurs EOF comme des clusters valides. | |||
</ | |||
== Opérations sur les Fichiers == | |||
=== Liste des Fichiers (<code>fat16_list_files</code>) === | |||
< | <syntaxhighlight lang="c"> | ||
void fat16_list_files(void) { | |||
if (!is_mounted && !fat16_init()) return; | |||
uart_println("\r\n=== FILES ==="); | |||
for (int i = 0; i < 32; i++) { | |||
task_sleep(1); // Yield au scheduler | |||
if (!read_sector(root_dir_sector + i)) break; | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
// Fin du répertoire | |||
if (e->filename[0] == 0x00) return; | |||
// Entrée supprimée ou invalide | |||
if ((uint8_t)e->filename[0] == 0xE5 || (uint8_t)e->filename[0] == 0xFF) continue; | |||
// Ignore les entrées spéciales (LFN, volume label) | |||
if (e->attributes == 0x0F || (e->attributes & 0x08)) continue; | |||
// Affichage du nom | |||
uart_print(" "); | |||
for (int k = 0; k < 8; k++) | |||
if (e->filename[k] != ' ') uart_putchar(e->filename[k]); | |||
// Affichage de l'extension (si présente) | |||
if (e->extension[0] != ' ') { | |||
uart_putchar('.'); | |||
for (int k = 0; k < 3; k++) | |||
if (e->extension[k] != ' ') uart_putchar(e->extension[k]); | |||
} | |||
// Affichage de la taille | |||
uart_print(" "); | |||
print_number(e->file_size); | |||
uart_println(" B"); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
'''Structure du répertoire racine''' : | |||
* 32 secteurs maximum (selon <code>root_entries</code> dans le boot sector) | |||
* 16 entrées par secteur (512 ÷ 32) | |||
* Entrées de 32 octets chacune | |||
'''Marqueurs spéciaux''' : | |||
- <code>0x00</code> : Fin du répertoire | |||
- <code>0xE5</code> : Fichier supprimé (premier caractère du nom) | |||
- <code>0xFF</code> : Entrée jamais utilisée | |||
''' | '''Attributs ignorés''' : | ||
- <code>0x0F</code> : Long File Name (LFN) - extension Windows 95+ | |||
- <code>0x08</code> : Volume Label - étiquette du volume | |||
''' | '''Yield au scheduler''' : <code>task_sleep(1)</code> permet à d'autres tâches de s'exécuter pendant le listing, évitant de bloquer le système. | ||
=== Création de Fichier (<code>fat16_create_file</code>) === | |||
<syntaxhighlight lang="c"> | |||
void fat16_create_file(const char* name, const char* content) { | |||
if (!is_mounted) fat16_init(); | |||
// 1. Trouver un cluster libre | |||
uint16_t free_clus = find_free_cluster(); | |||
if (free_clus == 0) { uart_println("Err: Disk Full"); return; } | |||
// 2. Trouver une entrée libre dans le répertoire | |||
uint32_t dir_sec = 0; uint16_t ent_off = 0; uint8_t found = 0; | |||
for (int i = 0; i < 32; i++) { | |||
read_sector(root_dir_sector + i); | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00 || (uint8_t)e->filename[0] == 0xE5) { | |||
dir_sec = root_dir_sector + i; | |||
ent_off = j * 32; | |||
found = 1; | |||
i = 32; // Break outer loop | |||
break; | |||
} | |||
} | |||
} | |||
if (!found) { uart_println("Err: Root Full"); return; } | |||
// 3. Créer l'entrée de répertoire | |||
read_sector(dir_sec); | |||
fat16_dir_entry_t* ne = (fat16_dir_entry_t*)(shared_buffer + ent_off); | |||
memset(ne, 0, 32); | |||
fat16_parse_filename(name, ne->filename, ne->extension); | |||
ne->attributes = 0x20; // Archive | |||
ne->first_cluster_low = free_clus; | |||
uint32_t sz = 0; | |||
while(content[sz]) sz++; | |||
ne->file_size = sz; | |||
write_sector(dir_sec); | |||
// 4. Écrire les données | |||
uint32_t lba = data_sector + ((free_clus - 2) * sectors_per_cluster); | |||
memset(shared_buffer, 0, 512); | |||
for(uint32_t k = 0; k < sz; k++) shared_buffer[k] = content[k]; | |||
write_sector(lba); | |||
// 5. Marquer le cluster comme EOF dans la FAT | |||
set_fat_entry(free_clus, 0xFFFF); | |||
uart_println("Created."); | |||
} | |||
</syntaxhighlight> | |||
'' | '''Limitations''' : | ||
1. Un seul cluster par fichier (max 512 × sectors_per_cluster octets) | |||
2. Pas de fragmentation - les fichiers plus grands ne sont pas supportés | |||
3. Le répertoire racine a une taille fixe | |||
'' | '''Format d'entrée''' : <code>memset(ne, 0, 32)</code> initialise tous les champs à 0, y compris les dates/heures. | ||
</ | |||
=== Suppression de Fichier (<code>fat16_delete_file</code>) === | |||
< | <syntaxhighlight lang="c"> | ||
void fat16_delete_file(const char* name) { | |||
if (!is_mounted) fat16_init(); | |||
char tn[8], te[3]; | |||
fat16_parse_filename(name, tn, te); | |||
uint8_t any_deleted = 0; | |||
while (1) { | |||
uint8_t found_pass = 0; | |||
for (int i = 0; i < 32; i++) { | |||
if (!read_sector(root_dir_sector + i)) continue; | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00) { i = 32; break; } | |||
if ((uint8_t)e->filename[0] == 0xE5) continue; | |||
if (memcmp(e->filename, tn, 8) == 0 && memcmp(e->extension, te, 3) == 0) { | |||
// Marquer comme supprimé | |||
e->filename[0] = 0xE5; | |||
uint16_t cluster = e->first_cluster_low; | |||
if (write_sector(root_dir_sector + i)) { | |||
uart_println("Entry Removed."); | |||
// Libérer la chaîne de clusters | |||
if (cluster != 0) free_cluster_chain(cluster); | |||
found_pass = 1; | |||
any_deleted = 1; | |||
} else { | |||
uart_println("Err: Write Fail"); | |||
} | |||
break; | |||
} | |||
} | |||
if (found_pass) break; | |||
} | |||
if (!found_pass) break; // Plus de fichiers à ce nom | |||
} | |||
if (!any_deleted) uart_println("Err: Not Found"); | |||
} | |||
</syntaxhighlight> | |||
'''Suppression logique''' : Seul le premier caractère du nom est changé en <code>0xE5</code>. Le reste de l'entrée reste intact jusqu'à écrasement. | |||
'''Support des noms en double''' : La boucle <code>while (1)</code> supporte plusieurs fichiers avec le même nom (bien que non standard). | |||
=== Renommage de Fichier (<code>fat16_rename_file</code>) === | |||
<syntaxhighlight lang="c"> | |||
</ | void fat16_rename_file(const char* old_name, const char* new_name) { | ||
if (!is_mounted) fat16_init(); | |||
char on[8], oe[3]; | |||
char nn[8], ne[3]; | |||
fat16_parse_filename(old_name, on, oe); | |||
fat16_parse_filename(new_name, nn, ne); | |||
for (int i = 0; i < 32; i++) { | |||
if (!read_sector(root_dir_sector + i)) continue; | |||
uint8_t dirty = 0; | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00) { i = 32; break; } | |||
if ((uint8_t)e->filename[0] == 0xE5) continue; | |||
if (memcmp(e->filename, on, 8) == 0 && memcmp(e->extension, oe, 3) == 0) { | |||
memcpy(e->filename, nn, 8); | |||
memcpy(e->extension, ne, 3); | |||
dirty = 1; | |||
} | |||
} | |||
if (dirty) { | |||
if (write_sector(root_dir_sector + i)) uart_println("Renamed."); | |||
else uart_println("Err: Write Fail"); | |||
return; | |||
} | |||
} | |||
uart_println("Err: Not Found"); | |||
} | |||
</syntaxhighlight> | |||
''' | '''Modification en mémoire''' : Le dirty flag évite d'écrire le secteur s'il n'y a pas eu de modification. | ||
=== Copie de Fichier (<code>fat16_copy_file</code>) === | |||
2 | <syntaxhighlight lang="c"> | ||
void fat16_copy_file(const char* src, const char* dst) { | |||
if (!is_mounted) fat16_init(); | |||
// 1. Recherche du fichier source | |||
char sn[8], se[3]; | |||
fat16_parse_filename(src, sn, se); | |||
uint16_t src_clus = 0; | |||
uint32_t size = 0; | |||
for (int i = 0; i < 32; i++) { | |||
read_sector(root_dir_sector + i); | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00) { i = 32; break; } | |||
if ((uint8_t)e->filename[0] == 0xE5) continue; | |||
if (memcmp(e->filename, sn, 8) == 0 && memcmp(e->extension, se, 3) == 0) { | |||
src_clus = e->first_cluster_low; | |||
size = e->file_size; | |||
i = 32; | |||
break; | |||
} | |||
} | |||
} | |||
if (size == 0) { uart_println("Err: Src Not Found"); return; } | |||
// 2. Création du fichier destination (vide) | |||
fat16_create_file(dst, ""); | |||
// 3. Mise à jour de la taille dans l'entrée destination | |||
char dn[8], de[3]; | |||
fat16_parse_filename(dst, dn, de); | |||
uint16_t dst_clus = 0; | |||
for (int i = 0; i < 32; i++) { | |||
read_sector(root_dir_sector + i); | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00) { i = 32; break; } | |||
if ((uint8_t)e->filename[0] == 0xE5) continue; | |||
if (memcmp(e->filename, dn, 8) == 0 && memcmp(e->extension, de, 3) == 0) { | |||
dst_clus = e->first_cluster_low; | |||
e->file_size = size; // Mise à jour de la taille | |||
write_sector(root_dir_sector + i); | |||
i = 32; | |||
break; | |||
} | |||
} | |||
} | |||
// 4. Copie des données | |||
uint32_t slba = data_sector + ((src_clus - 2) * sectors_per_cluster); | |||
uint32_t dlba = data_sector + ((dst_clus - 2) * sectors_per_cluster); | |||
read_sector(slba); // Charge les données source dans shared_buffer | |||
write_sector(dlba); // Écrit shared_buffer vers la destination | |||
uart_println("Copied."); | |||
} | |||
</syntaxhighlight> | |||
'''Approche en deux étapes''' : | |||
1. Crée un fichier vide avec <code>fat16_create_file()</code> | |||
2. Met à jour manuellement la taille et copie les données | |||
'''Limitation''' : Ne copie qu'un seul cluster (pas de chaînage). | |||
=== Lecture de Fichier (<code>fat16_read_and_print_file</code>) === | |||
<syntaxhighlight lang="c"> | |||
void fat16_read_and_print_file(const char* filename) { | |||
if (!is_mounted) fat16_init(); | |||
// Recherche du fichier | |||
char tn[8], te[3]; | |||
fat16_parse_filename(filename, tn, te); | |||
uint16_t clus = 0; | |||
uint32_t sz = 0; | |||
for (int i = 0; i < 32; i++) { | |||
read_sector(root_dir_sector + i); | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00) { i = 32; break; } | |||
if ((uint8_t)e->filename[0] == 0xE5) continue; | |||
if (memcmp(e->filename, tn, 8) == 0 && memcmp(e->extension, te, 3) == 0) { | |||
clus = e->first_cluster_low; | |||
sz = e->file_size; | |||
i = 32; | |||
break; | |||
} | |||
} | |||
} | |||
if (sz == 0) { uart_println("Err: Not Found"); return; } | |||
// Lecture et affichage | |||
uart_println("\r\n--- START ---"); | |||
uint32_t lba = data_sector + ((clus - 2) * sectors_per_cluster); | |||
if (read_sector(lba)) { | |||
for (uint16_t k = 0; k < 512 && sz > 0; k++) { | |||
if (shared_buffer[k] == '\n') uart_print("\r\n"); | |||
else uart_putchar(shared_buffer[k]); | |||
sz--; | |||
} | |||
} | |||
uart_println("\r\n--- END ---"); | |||
} | |||
</syntaxhighlight> | |||
''' | '''Conversion newline''' : Convertit <code>\n</code> en <code>\r\n</code> pour l'affichage sur terminal série. | ||
</ | |||
=== Lecture vers Buffer (<code>fat16_read_to_buffer</code>) === | |||
'' | <syntaxhighlight lang="c"> | ||
uint8_t fat16_read_to_buffer(const char* filename, char* out_buf, uint16_t max_len) { | |||
if (!is_mounted) fat16_init(); | |||
// Recherche du fichier (identique à read_and_print_file) | |||
char tn[8], te[3]; | |||
fat16_parse_filename(filename, tn, te); | |||
uint16_t clus = 0; | |||
uint32_t sz = 0; | |||
for (int i = 0; i < 32; i++) { | |||
read_sector(root_dir_sector + i); | |||
for (int j = 0; j < 16; j++) { | |||
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32); | |||
if (e->filename[0] == 0x00) { i = 32; break; } | |||
if ((uint8_t)e->filename[0] == 0xE5) continue; | |||
if (memcmp(e->filename, tn, 8) == 0 && memcmp(e->extension, te, 3) == 0) { | |||
clus = e->first_cluster_low; | |||
sz = e->file_size; | |||
i = 32; | |||
break; | |||
} | |||
} | |||
} | |||
if (sz == 0) return 0; | |||
// Lecture des données | |||
uint32_t lba = data_sector + ((clus - 2) * sectors_per_cluster); | |||
if (!read_sector(lba)) return 0; | |||
// Copie avec limite de taille | |||
uint16_t cpl = (sz < max_len) ? sz : (max_len - 1); | |||
for(uint16_t k = 0; k < cpl; k++) out_buf[k] = shared_buffer[k]; | |||
out_buf[cpl] = '\0'; // Null-terminator | |||
return 1; | |||
} | |||
</syntaxhighlight> | |||
' | = Système d'Exploitation Temps Réel pour AT90USB1286 = | ||
== Introduction == | |||
''' | Le système d'exploitation temps réel pour microcontrôleurs présente deux versions distinctes d'API de gestion des tâches. Cette analyse détaille les différences fondamentales entre l'approche dynamique (Version 1) et l'approche statique (Version 2). | ||
== Version 1 : API Dynamique == | |||
=== Caractéristiques Principales === | |||
''' | * '''Gestion dynamique des slots''' : Cette version introduit le concept de slot réutilisable (`TASK_FREE`) permettant une meilleure gestion de la mémoire sur le long terme. | ||
* '''Création automatique de tâches''' : La fonction `task_exec()` abstrait la complexité de l'allocation. Elle recherche automatiquement un slot libre et calcule l'adresse de pile. | |||
c | <syntaxhighlight lang="c"> | ||
int8_t task_exec(void (*function)(void*), const char* nom, void* arg) { | |||
// Recherche d'un slot TASK_FREE ou TASK_COMPLETED | |||
// Calcul automatique : 0x0800 - (Index * Taille) | |||
// Initialisation complète du TCB | |||
} | |||
</syntaxhighlight> | |||
'' | * '''Gestion de mémoire prédéfinie''' : Les piles sont allouées dans une région mémoire fixe avec une formule de calcul automatique, évitant à l'utilisateur de gérer des pointeurs manuels. | ||
=== Fonctionnalités Avancées === | |||
* `task_exit()` libère le slot pour réutilisation (passage à `TASK_FREE`). | |||
< | * Tâche IDLE avec pile statique dédiée (64 octets). | ||
* Pointeur de pile de type `uint8_t*` pour une cohérence de typage avec les registres AVR. | |||
== Version 2 : API Statique == | |||
! | |||
=== Structure des États des Tâches === | |||
=== Caractéristiques Principales === | |||
* '''Gestion manuelle de la mémoire''' : L'utilisateur doit fournir explicitement le buffer de pile lors de la création. | |||
<syntaxhighlight lang="c"> | |||
int8_t task_create(const char* name_pgm, void (*function)(void*), void* arg, | |||
uint8_t priority, uint8_t* stack_buffer, uint16_t stack_size); | |||
</syntaxhighlight> | |||
* '''Allocation séquentielle''' : Les tâches sont créées dans l'ordre d'appel (ID 0, 1, 2...), sans mécanisme natif de réutilisation des slots libérés. | |||
* '''Pile IDLE réduite''' : Optimisation de l'espace mémoire avec une pile IDLE de seulement 32 octets (contre 64 dans la V1). | |||
== Tableau Comparatif des Fonctions == | |||
{| class="wikitable" | |||
|+ Comparaison des Fonctions API | |||
|- | |||
! Fonctionnalité !! Version 1 (Dynamique) !! Version 2 (Statique) | |||
|- | |||
| '''Création de tâche''' || `task_exec()` (Automatique) || `task_create()` (Manuelle) | |||
|- | |- | ||
| '''Terminaison''' || Libère le slot (`TASK_FREE`) || Marque comme terminé (`TASK_COMPLETED`) | |||
| ''' | |- | ||
| ( | | '''Gestion mémoire''' || Calcul automatique interne || Allocation manuelle par l'utilisateur | ||
| | |- | ||
| | | '''Réutilisation slots''' || ✅ Oui || ❌ Non | ||
|- | |||
| '''Taille pile IDLE''' || 64 octets || 32 octets | |||
| | |||
|} | |} | ||
== Différences Techniques Détaillées == | |||
=== 2. Initialisation du Planificateur === | |||
Les deux versions initialisent différemment la table des tâches au démarrage du système. | |||
'''Version 1 :''' Initialisation à `TASK_FREE` pour permettre l'allocation future. | |||
<syntaxhighlight lang="c"> | |||
</ | for (uint8_t i = 0; i < MAX_TASKS; i++) { | ||
task_table[i].function = NULL; | |||
task_table[i].state = TASK_FREE; | |||
} | |||
</syntaxhighlight> | |||
'''Version 2 :''' Initialisation à `TASK_COMPLETED`, verrouillant implicitement les slots non utilisés si la logique de création ne gère pas cet état. | |||
<syntaxhighlight lang="c"> | |||
for (uint8_t i = 0; i < MAX_TASKS; i++) { | |||
task_table[i].function = NULL; | |||
task_table[i].state = TASK_COMPLETED; | |||
} | |||
| | </syntaxhighlight> | ||
La '''Version 1''' offre une approche plus sophistiquée ("OS-like") avec gestion automatique de la mémoire et réutilisation des slots, idéale pour des applications généralistes. La '''Version 2''' propose un modèle "Bare-metal" plus rigide, offrant un contrôle granulaire mais nécessitant une gestion manuelle rigoureuse et la correction du bug de typage identifié. | |||
= Documentation Technique Approfondie : FS & UART = | |||
== Module 1 : Système de Fichiers (fs.c / fs.h) == | |||
[[Fichier:FS Mother.mp4|vignette]] | |||
Le système de fichiers (FS) est une couche d'abstraction située au-dessus du driver de carte SD. Il transforme un stockage physique linéaire (secteurs de 512 octets) en une structure organisée (fichiers nommés). | |||
=== 1.1 Architecture et Configuration === | |||
Le système repose sur une unité d'allocation logique appelée '''Bloc Virtuel (vblock)'''. | |||
'' | ; Définition | ||
: FS_BLOCK_SIZE = 256 octets. | |||
; Justification | |||
: L'AT90USB1286 dispose de peu de RAM (8KB). Manipuler des tampons de 512 octets (taille native SD) est coûteux. Diviser par deux permet d'économiser de la RAM lors des opérations de bufferisation, tout en gardant une correspondance simple (1 secteur SD = 2 blocs FS). | |||
==== Cartographie du Disque (Memory Map) ==== | |||
L'espace de stockage est segmenté statiquement via des <nowiki>#define</nowiki> dans fs.c : | |||
</ | |||
{| | {| class="wikitable" | ||
! Zone !! Blocs (Start) !! Longueur (Count) !! Capacité !! Description Technique | |||
! | |||
|- | |- | ||
| | | BITMAP || 0 || 16 || 4 Ko || Table d'Allocation. Chaque bit représente l'état d'un bloc de données. | ||
16 blocs * 256 octets * 8 bits = 32 768 blocs adressables. | |||
Espace max géré : 32768 * 256o = 8 Mo. | |||
|- | |- | ||
| | | ROOT DIR || 16 || 16 || 4 Ko || Répertoire Racine. Stocke les métadonnées des fichiers. | ||
16 blocs * 256 octets = 4096 octets. | |||
Entrée de 64 octets -> 4096 / 64 = 64 fichiers maximum. | |||
| | |- | ||
| DATA || 32 || N || Variable || Zone de Données. Stockage effectif du contenu des fichiers. Commence au bloc 32. | |||
|} | |} | ||
< | === 1.2 Structure des Données (fs_entry_t) === | ||
'' | Chaque fichier est décrit par une structure rigide de 64 octets. | ||
''' | <syntaxhighlight lang="c"> | ||
typedef struct { | |||
char name[16]; // 0x00: Nom ASCII (terminé par \0). Si name[0]==0, l'entrée est libre. | |||
uint16_t size; // 0x10: Taille exacte du fichier en octets. | |||
uint16_t blocks[16]; // 0x12: Liste des ID de blocs alloués. | |||
// Ex: [32, 45, 33, 0, 0...] (Allocation non contiguë possible) | |||
uint8_t padding[14]; // 0x32: Bourrage pour aligner la structure sur 64 octets. | |||
// Permet d'avoir exactement 4 entrées par bloc de 256 octets. | |||
} fs_entry_t; | |||
</syntaxhighlight> | |||
'''Analyse des Limitations :''' | |||
* La table blocks[16] limite la taille maximale d'un fichier à 16 blocs * 256 octets = 4096 octets (4 Ko). | |||
* Pour supporter des fichiers plus gros, il faudrait implémenter un système d'indirection (FAT ou i-node), ce qui complexifierait le code. | |||
=== 1.3 Mécanismes Bas-Niveau (Drivers Internes) === | |||
Ces fonctions static ne sont pas visibles depuis l'extérieur (fs.h) et gèrent l'interface avec la carte SD. | |||
==== fs_read_vblock(uint16_t vblock, uint8_t* out) ==== | |||
Traduit une adresse logique (256o) en adresse physique (512o). | |||
# '''Calcul du Secteur SD''' : phys_sector = vblock / 2. | |||
# '''Exemple''' : Les blocs virtuels 10 et 11 sont tous deux dans le secteur physique 5. | |||
# '''Lecture Physique''' : sd_read_block(phys_sector, temp_buf). | |||
#: Tout le secteur (512o) est chargé dans le tampon statique temp_buf. | |||
# '''Extraction''' : | |||
## Si vblock est pair (vblock % 2 == 0), on copie temp_buf[0..255]. | |||
## Si vblock est impair (vblock % 2 == 1), on copie temp_buf[256..511]. | |||
==== fs_write_vblock(uint16_t vblock, uint8_t* in) (CRITIQUE) ==== | |||
Implémente le cycle Read-Modify-Write (RMW) indispensable pour écrire des sous-unités de secteur. | |||
# '''Lecture (Read)''' : Charge le secteur physique contenant le bloc cible dans temp_buf. | |||
#: Risque : Si la lecture échoue, on ne peut pas écrire (risque de corruption). | |||
# '''Modification (Modify)''' : Écrase la moitié du tampon (memcpy) avec les nouvelles données in. | |||
#: L'autre moitié du tampon (appartenant potentiellement à un autre fichier) reste intacte. | |||
# '''Écriture (Write)''' : Réécrit le secteur complet sur la carte SD. | |||
'''Note de Sécurité''' : Cette fonction utilise un buffer statique global temp_buf. Elle n'est pas réentrante. Si une interruption interrompt cette fonction et tente une opération FS, les données seront corrompues. | |||
=== 1.4 Analyse Détaillée du Bitmap (Logique d'Allocation) === | |||
Le système utilise un ''Bitmap'' (carte de bits) stocké dans les blocs 0 à 15 pour gérer l'occupation de chaque bloc du système. C'est le cœur de la gestion d'espace. | |||
==== Structure Mathématique ==== | |||
''' | * ''Total Blocs Bitmap :'' 16. | ||
* ''Taille par Bloc :'' 256 octets (2048 bits). | |||
* ''Total Bits :'' 16 * 2048 = 32 768 bits. | |||
* ''Mapping :'' Le bit N correspond à l'état du bloc N du système de fichiers (0 = Libre, 1 = Occupé). | |||
==== Algorithme de Recherche (fs_alloc_block) ==== | |||
L'algorithme utilise une stratégie ''First-Fit'' (Premier trouvé) avec une recherche hiérarchique : | |||
3. ''' | # ''Boucle Niveau 1 (Pages) :'' Itère sur les 16 blocs de Bitmap. Charge un bloc entier en mémoire. | ||
# ''Boucle Niveau 2 (Octets) :'' Scanne les 256 octets du bloc chargé. | |||
#: ''Optimisation :'' Si un octet vaut `0xFF`, il est plein. On saute directement au suivant sans inspecter les bits. | |||
# ''Boucle Niveau 3 (Bits) :'' Si un octet n'est pas `0xFF`, on inspecte les 8 bits. | |||
#: On cherche le premier bit à 0 via un masque `(1 << bit)`. | |||
# ''Calcul d'Index Absolu :'' | |||
#: `ID = (Page * 2048) + (Octet * 8) + Bit` | |||
==== Persistance Immédiate ==== | |||
Dès qu'un bit libre est trouvé : | |||
# Il est mis à 1 en RAM. | |||
' | # Le bloc Bitmap complet est immédiatement réécrit sur la carte SD via `fs_write_vblock`. | ||
#: Cela garantit que l'allocation est sauvegardée avant même que les données du fichier ne soient écrites. | |||
=== 1.5 Logique Interne des Fichiers === | |||
Cette section détaille comment le FS manipule les fichiers sans table d'allocation globale (type FAT). | |||
==== Indexation Directe (Scatter-Gather) ==== | |||
''' | Au lieu d'une liste chaînée sur le disque (où le bloc A pointe vers le bloc B), le FS utilise un ''Index Direct'' stocké dans l'entrée du fichier (`blocks[16]`). | ||
''' | * ''Avantage (Accès Aléatoire) :'' Pour lire le 3ème bloc d'un fichier, le système lit directement `entry->blocks[2]`. Complexité O(1). | ||
''}'' | * ''Inconvénient (Fragmentation) :'' Les blocs d'un fichier peuvent être éparpillés n'importe où sur le disque (ex: blocs 32, 500, 33). Le FS ne défragmente pas. | ||
</ | * ''Limitation (Taille Fixe) :'' La taille du tableau `blocks` est fixe (16). C'est ce qui limite la taille maximale du fichier, et non la capacité de la carte SD. | ||
{| | |||
==== Gestion de la Fin de Fichier (Append Logic) ==== | |||
< | |||
''' | Lors de l'ajout de données (`fs_append`) : | ||
</ | |||
# ''Détection du Remplissage :'' Le système calcule `offset = size % 256`. | |||
#: Si `offset > 0`, le dernier bloc alloué n'est pas plein. | |||
</ | # ''Fusion (Merge) :'' | ||
#: Le dernier bloc est lu en mémoire. | |||
#: Les nouvelles données sont copiées à la suite des anciennes. | |||
#: Le bloc est réécrit. | |||
# ''Extension :'' | |||
#: Si le fichier a besoin de plus d'espace, de nouveaux blocs sont alloués via le Bitmap et ajoutés à la liste `blocks[]`. | |||
=== 1.6 Fonctions API (Détails d'Implémentation) === | |||
==== fs_format ==== | |||
Initialise un disque vierge. | |||
* Remplit temp_buf de zéros. | |||
* Écrit des zéros dans tous les blocs Bitmap (0-15) et Répertoire (16-31). | |||
* Effet : Tous les blocs sont marqués "Libres", aucun fichier n'existe. | |||
==== fs_create(char* name) ==== | |||
Crée une coquille vide. | |||
# Vérifie si le fichier existe déjà (erreur si oui). | |||
# Cherche une entrée libre dans le répertoire (là où name[0] == 0). | |||
# Initialise l'entrée : size = 0, blocks[] = {0}. | |||
# Sauvegarde le bloc répertoire. | |||
==== fs_append(name, data, len) ==== | |||
Ajoute des données à la fin d'un fichier existant. C'est la fonction la plus complexe du module. | |||
'''Algorithme Détaillé :''' | |||
# '''Contexte''' : Récupère l'entrée du fichier. Si size est 4096, erreur (Disque Plein/Fichier Plein). | |||
# '''Remplissage du dernier bloc (Partial Fill)''' : | |||
## Calcul : offset = size % 256. | |||
## Si offset > 0 (le dernier bloc n'est pas plein) : | |||
### Récupère l'ID du dernier bloc : blk_id = entry->blocks[(size-1)/256]. | |||
### Lit ce bloc en mémoire. | |||
### Calcule l'espace libre : space = 256 - offset. | |||
### Copie min(len, space) octets à la suite des données existantes. | |||
### Sauvegarde le bloc. | |||
### Met à jour size et décrémente len. | |||
# '''Allocation de nouveaux blocs''' : | |||
## Tant qu'il reste des données (len > 0) : | |||
### Vérifie qu'on a pas atteint 16 blocs. | |||
### new_blk = fs_alloc_block(). Si 0 (erreur), arrêt. | |||
### Ajoute new_blk au tableau entry->blocks[]. | |||
### Copie les 256 prochains octets de données dans un buffer. | |||
### Écrit ce buffer dans le nouveau bloc (fs_write_vblock). | |||
### Met à jour size. | |||
# '''Finalisation''' : | |||
## Réécrit l'entrée mise à jour (nouvelle taille, nouveaux blocs) dans le secteur répertoire sur la SD. | |||
==== fs_read(name, buffer, size, offset) ==== | |||
Lecture aléatoire (Random Access). | |||
# '''Bornage''' : Si offset > file_size, retourne 0. Ajuste read_count pour ne pas dépasser la fin du fichier. | |||
# '''Boucle de lecture''' : | |||
## Calcule l'index du bloc logique : blk_idx = (offset + i) / 256. | |||
## Calcule l'offset dans ce bloc : blk_off = (offset + i) % 256. | |||
## Récupère l'ID physique via entry->blocks[blk_idx]. | |||
## Lit le bloc. | |||
## Copie la portion nécessaire dans le buffer utilisateur. | |||
## Avance au bloc suivant si nécessaire. | |||
==== fs_remove(name) ==== | |||
Suppression propre pour éviter les fuites mémoire (blocs orphelins). | |||
# Trouve l'entrée fichier. | |||
# '''Libération Bitmap''' : Pour chaque ID non nul dans entry->blocks[], appelle fs_set_bitmap(id, 0). | |||
# '''Nettoyage Répertoire''' : memset l'entrée à 0. Cela libère le slot pour un futur fichier. | |||
# '''Commit''' : Sauvegarde le bloc répertoire. | |||
== Module 2 : UART (uart.c / uart.h) == | |||
Le module UART (Universal Asynchronous Receiver-Transmitter) gère la console série. Sur l'AT90USB1286, il utilise le périphérique matériel USART1. | |||
=== 2.1 Théorie et Configuration === | |||
L'UART est configuré pour un format 8N1 (8 bits de données, No Parity, 1 Stop bit) à 38400 bauds. | |||
==== Calcul du Baud Rate ==== | |||
La précision du timing est cruciale pour la communication série asynchrone. | |||
<syntaxhighlight lang="c"> | |||
#define BAUD_PRESCALE (((F_CPU / (UART_BAUDRATE * 16UL))) - 1) | |||
</syntaxhighlight> | |||
* F_CPU = 16 000 000 Hz. | |||
* UART_BAUDRATE = 38 400 Hz. | |||
* Calcul : (16000000 / (38400 * 16)) - 1 = (16000000 / 614400) - 1 = 26.04 - 1 = 25. | |||
* La valeur entière 25 est chargée dans les registres UBRR. L'erreur est minime (< 0.2%). | |||
==== Analyse des Registres d'Initialisation (uart_init) ==== | |||
; UBRR1H & UBRR1L (USART Baud Rate Register) | |||
: Reçoivent la valeur 25 (sur 16 bits). Définit la vitesse de transmission. | |||
; UCSR1B (Control and Status Register B) | |||
: '''TXEN1''' (Bit 3) : Transmitter Enable. Active le driver de la pin TX (PD3). Sans cela, la pin reste une I/O normale. | |||
: '''RXEN1''' (Bit 4) : Receiver Enable. Active le driver de la pin RX (PD2). | |||
; UCSR1C (Control and Status Register C) | |||
: '''UCSZ11''' (Bit 2) & '''UCSZ10''' (Bit 1) : Définissent la taille des caractères. | |||
: Configuration 011 (avec UCSZ12=0) = 8 bits. | |||
: Les bits de parité (UPM11:0) sont à 0 par défaut (Disabled). | |||
: Le bit de stop (USBS1) est à 0 par défaut (1 bit). | |||
=== 2.2 Émission de Données (Polling) === | |||
L'émission est bloquante. Le CPU attend que le hardware soit prêt. | |||
==== uart_putchar(char c) ==== | |||
<syntaxhighlight lang="c"> | |||
void uart_putchar(char c) { | |||
// Boucle d'attente active (Busy Wait) | |||
// UDRE1 (USART Data Register Empty) : Ce drapeau passe à 1 quand | |||
// le buffer d'émission est vide et prêt à recevoir une nouvelle donnée. | |||
while (!(UCSR1A & (1 << UDRE1))); | |||
// L'écriture dans UDR1 déclenche automatiquement : | |||
// 1. Le transfert vers le Shift Register. | |||
// 2. La génération du Start Bit. | |||
// 3. Le décalage des 8 bits de données. | |||
// 4. La génération du Stop Bit. | |||
UDR1 = c; | |||
} | |||
</syntaxhighlight> | |||
==== uart_print(char* str) ==== | |||
Fonction utilitaire simple. | |||
* Itère via un pointeur *str tant que le caractère n'est pas \0 (NULL terminator). | |||
* Appelle uart_putchar pour chaque caractère. | |||
=== 2.3 Gestion Avancée de la Mémoire (Flash vs RAM) === | |||
Les microcontrôleurs AVR utilisent une architecture Harvard. Les données en mémoire programme (Flash) ne sont pas dans le même espace d'adressage que les variables (RAM). | |||
; Problème | |||
: Une chaîne littérale uart_print("Hello") est copiée en RAM au démarrage (.data section), consommant la précieuse mémoire vive (seulement 8KB dispo). | |||
; Solution | |||
: Utiliser la macro PSTR("Hello") pour garder la chaîne en Flash, et utiliser des fonctions spéciales pour la lire. | |||
==== uart_print_P(const char* str) ==== | |||
Cette fonction est conçue pour lire des pointeurs vers la Flash space. | |||
<syntaxhighlight lang="c"> | |||
void uart_print_P(const char* str) { | |||
char c; | |||
// pgm_read_byte : Instruction assembleur LPM (Load Program Memory) | |||
// Lit un octet à l'adresse 'str' dans la Flash, pas la RAM. | |||
while ((c = pgm_read_byte(str++))) { | |||
uart_putchar(c); | |||
} | |||
} | |||
</syntaxhighlight> | |||
; Macro uart_println_P | |||
: Combine l'affichage de la chaîne Flash et l'ajout automatique de \r\n (CRLF) pour le retour à la ligne. | |||
=== 2.4 Réception de Données === | |||
Bien que moins utilisée dans ce code, la réception est configurée. | |||
==== uart_available() ==== | |||
* Vérifie le bit RXC1 (Receive Complete) dans UCSR1A. | |||
* Retourne 1 si des données non lues sont présentes dans le buffer FIFO de réception hardware. | |||
==== uart_getchar() ==== | |||
* Lit le registre UDR1. | |||
* '''Attention''' : Si cette fonction est appelée alors qu'aucune donnée n'est arrivée, le comportement est indéfini (retourne souvent le dernier octet reçu). Il faut toujours vérifier uart_available() avant. | |||
= Documentation Technique - Analyse Logique du Shell v1.6 = | |||
Ce document propose une analyse algorithmique détaillée ("Code Walkthrough") du fichier `shell.c`. Il ne s'agit pas d'un manuel utilisateur, mais d'une explication du comportement interne destinée aux développeurs système. | |||
== 1. Architecture de l'Abstraction Console (Console Abstraction Layer) == | |||
Le Shell v1.6 introduit une couche d'abstraction logicielle pour l'affichage. Au lieu d'utiliser directement `uart_putchar`, le shell utilise `console_putc`. Cette architecture permet la fonctionnalité de "Mirroring" (duplication d'écran). | |||
=== 1.1 Variable d'État Globale === | |||
<syntaxhighlight lang="c"> | |||
static uint8_t active_screen_id = 0; | |||
</syntaxhighlight> | |||
* ''Nature'' : Variable statique locale au fichier (portée limitée à `shell.c`). | |||
* ''Sémantique'' : | |||
'' `0` : Mode standard (UART uniquement). | |||
'' `> 0` : Mode miroir actif. La valeur stockée correspond à l'ID physique du Chip Select (CS) sur le bus SPI (ex: 1 pour CS1, 2 pour CS2, etc.). | |||
=== 1.2 Algorithme d'Émission (console_putc) === | |||
La fonction `console_putc` est la primitive atomique d'affichage. Son comportement est conditionnel. | |||
'''Logique séquentielle :''' | |||
# ''Émission Primaire (UART)'' : | |||
#: L'appel à `uart_putchar(c)` est inconditionnel. Le terminal série reçoit toujours les données. C'est une sécurité pour le débogage : même si l'écran SPI plante, le développeur voit la sortie sur le port série. | |||
# ''Test de Condition (Miroir)'' : | |||
#: `if (active_screen_id > 0)` : Vérifie si un écran est attaché. | |||
# ''Transaction SPI (Si actif)'' : | |||
#: Le code implémente une transaction SPI manuelle complète pour chaque caractère : | |||
## `spi_select_device(active_screen_id)` : Active la ligne CS (mise à 0). | |||
## `spi_transfer(c)` : Envoie l'octet via le registre `SPDR` et attend la fin de transmission (Polling du flag SPIF). | |||
## `spi_deselect_device(active_screen_id)` : Relâche la ligne CS (mise à 1). | |||
'''Impact sur la performance :''' | |||
Cette approche "caractère par caractère" est fiable mais lente. Pour afficher "HELLO" sur SPI, le code bascule le Chip Select 5 fois. | |||
* '''Overhead''' : 5 sélections + 5 désélections + délais de garde (20µs par char) + temps de transmission UART. | |||
=== 1.3 Gestion de la Mémoire Programme (console_print_P) === | |||
<syntaxhighlight lang="c"> | |||
void console_print_P(const char* str_progmem) { | |||
char c; | |||
while ((c = pgm_read_byte(str_progmem++))) { | |||
console_putc(c); | |||
} | |||
} | |||
</syntaxhighlight> | |||
* ''Problème résolu'' : Sur AVR, les chaînes constantes (`"Erreur"`) sont copiées en RAM au démarrage. La RAM étant limitée (4-8KB), stocker les textes de menu en RAM est un gaspillage. | |||
* ''Solution'' : La macro `PSTR()` place les chaînes en Flash. | |||
* ''Mécanisme'' : La fonction utilise l'instruction assembleur `LPM` (Load Program Memory) via `pgm_read_byte`. Elle itère tant que le caractère lu n'est pas le terminateur nul `\0`. | |||
== 2. Mécanique de la Boucle Principale (shell_task_func) == | |||
Cette fonction est le point d'entrée de la tâche (Task Entry Point). Elle ne retourne jamais (`while(1)`). | |||
=== 2.1 Bufferisation des Commandes === | |||
<syntaxhighlight lang="c"> | |||
static char cmd_buf[64]; | |||
static uint8_t cmd_idx = 0; | |||
</syntaxhighlight> | |||
* ''Capacité'' : 64 octets. Cela inclut la commande, les arguments et le caractère nul final. | |||
* ''Risque de dépassement'' : Le code actuel ne vérifie pas explicitement si `cmd_idx >= 64` avant d'incrémenter. Si l'utilisateur tape plus de 64 caractères sans appuyer sur Entrée, il y a un risque de ''Buffer Overflow'' qui corromprait la mémoire adjacente (probablement `active_screen_id` qui est déclarée juste après). | |||
=== 2.2 Traitement des Entrées (Input Handling) === | |||
Le shell fonctionne par interruption logicielle simulée (Polling). | |||
==== Cas du Retour Chariot ('\r') ==== | |||
C'est le déclencheur de l'exécution. | |||
# ''Terminaison de Chaîne'' : `cmd_buf[cmd_idx] = 0;` transforme le tableau de caractères en chaîne C valide (Null-terminated string). | |||
# ''Saut de ligne'' : `console_println("")` pour que la réponse s'affiche sur la ligne suivante. | |||
# ''Exécution'' : Appel bloquant à `shell_process_cmd()`. | |||
# ''Réinitialisation'' : | |||
#: `cmd_idx = 0;` : Remet le curseur au début. | |||
#: Le contenu précédent de `cmd_buf` n'est pas effacé (memset), il sera simplement écrasé par la prochaine frappe. | |||
# ''Prompt'' : Affiche `> ` pour inviter à la saisie suivante. | |||
==== Cas du Backspace ('\b' ou 127) ==== | |||
Gère l'édition de ligne. | |||
# ''Vérification'' : `if (cmd_idx > 0)`. On ne peut pas effacer si le buffer est vide (pour ne pas corrompre la mémoire avant le buffer). | |||
# ''Logique Interne'' : `cmd_idx--`. On recule simplement l'index d'écriture. | |||
# ''Logique Visuelle'' : `console_print("\b \b")`. | |||
#: Le premier `\b` recule le curseur du terminal. | |||
#: L'espace ` ` écrase le caractère affiché. | |||
#: Le second `\b` remet le curseur à la position reculée. | |||
== 3. Analyse du Moteur de Parsing (shell_process_cmd) == | |||
C'est le cœur logique du shell. Il transforme une chaîne brute en appel de fonction. | |||
=== 3.1 Tokenisation Destructive (strtok) === | |||
Le code utilise `strtok` (String Tokenizer) de la `stdlib`. | |||
<syntaxhighlight lang="c"> | |||
char* cmd = strtok(cmd_buf, " "); | |||
char* arg1 = strtok(NULL, " "); | |||
char* arg2 = strtok(NULL, " "); | |||
</syntaxhighlight> | |||
'''Comportement mémoire de `strtok` :''' | |||
Si `cmd_buf` contient : `{'W', 'R', 'I', 'T', 'E', ' ', 'A', '.', 'T', 'X', 'T', '\0'}` | |||
Après le premier appel `strtok(cmd_buf, " ")` : | |||
# `strtok` trouve le premier espace. | |||
# Il le remplace par `\0`. | |||
# Il retourne un pointeur vers le début (`&cmd_buf[0]`). | |||
# `cmd_buf` devient : `{'W', 'R', 'I', 'T', 'E', '\0', 'A', '.', 'T', 'X', 'T', '\0'}`. | |||
Les appels suivants `strtok(NULL, " ")` reprennent après le `\0` inséré précédemment. | |||
'''Conséquence Critique :''' | |||
Cette méthode rend impossible la récupération de la ligne originale. Le buffer est "haché". | |||
=== 3.2 Normalisation (strupr) === | |||
<syntaxhighlight lang="c"> | |||
strupr(cmd); | |||
</syntaxhighlight> | |||
* Seul le premier token (la commande) est mis en majuscules. | |||
* ''Raison'' : Permet à l'utilisateur de taper `write`, `WRITE` ou `Write`. | |||
* ''Important'' : `arg1` et `arg2` ne sont ''pas'' normalisés. C'est crucial pour le système de fichiers qui est (souvent) sensible à la casse, ou du moins pour préserver le nommage utilisateur. | |||
== 4. Analyse Détaillée des Commandes == | |||
=== 4.1 Commandes Fichiers (File System) === | |||
==== Commande `TOUCH` ==== | |||
* ''Signature Shell'' : `TOUCH <filename>` | |||
* ''Appel'' : `fs_create(arg1)` | |||
* ''Logique de Retour'' : | |||
'' Le shell vérifie implicitement le succès via la valeur de retour (implémentation manquante de message d'erreur spécifique dans le snippet, mais généralement 0 = succès). | |||
'' Si `arg1` est NULL (utilisateur a tapé juste `TOUCH`), la commande est ignorée silencieusement par le `if(arg1)`. | |||
==== Commande `CAT` (Lecture et Affichage) ==== | |||
* ''Signature'' : `CAT <filename>` | |||
* ''Mécanisme de Flux'' : | |||
'' Le shell ne lit pas tout le fichier d'un coup (la RAM est trop petite). | |||
'' Il utilise une lecture par blocs (Chunking). | |||
<syntaxhighlight lang="c"> | |||
char buf[64]; // Tampon temporaire sur la pile | |||
uint16_t offset = 0; | |||
int16_t read; | |||
while(1) { | |||
// Lit 63 octets max pour laisser place au \0 | |||
read = fs_read(arg1, (uint8_t*)buf, 63, offset); | |||
if (read <= 0) break; // Fin de fichier ou Erreur | |||
buf[read] = 0; // Terminateur pour l'affichage | |||
console_print(buf); | |||
offset += read; // Avance le curseur de lecture | |||
} | |||
</syntaxhighlight> | |||
* ''Aspect Bloquant'' : Cette boucle monopolise le CPU. Tant que le fichier est affiché, le shell ne répond plus. Sur un gros fichier, cela peut durer plusieurs secondes (dépendant du Baudrate UART et de la vitesse SPI). | |||
==== Commande `WRITE` (Écriture) ==== | |||
* ''Signature'' : `WRITE <file> <text>` | |||
* ''Appel'' : `fs_append(arg1, (uint8_t*)arg2, strlen(arg2))` | |||
* ''Analyse de la limitation `strtok`'' : | |||
'' Commande : `WRITE log.txt Hello` -> `arg1`="log.txt", `arg2`="Hello". Fonctionne. | |||
'' Commande : `WRITE log.txt Hello World` -> `arg1`="log.txt", `arg2`="Hello". | |||
''* Le mot "World" est perdu car `strtok` l'a séparé dans un 3ème token qui n'est pas récupéré par le code. | |||
''* ''Correction possible'' : Il faudrait utiliser un pointeur manuel pour récupérer "le reste de la chaîne" après `arg1`, plutôt que `strtok`.'' | |||
=== 4.2 Commandes Noyau (Kernel) === | |||
Ces commandes font le pont entre l'utilisateur et le planificateur (`scheduler.c`). | |||
==== Commande `PS` (Process Status) ==== | |||
* ''Appel'' : `shell_ps()` | |||
* ''Fonctionnement interne'' : | |||
'' Cette fonction (définie ailleurs) itère sur la `task_table` du noyau. | |||
'' Elle accède directement aux structures `TCB` (Task Control Block) pour lire : | |||
''* L'ID (index). | |||
''* Le Nom (`task->name`). | |||
''* L'État (`task->state`). | |||
''* La Priorité. | |||
==== Commandes de Contrôle (`KILL`, `SUSPEND`, `RESUME`) ==== | |||
* ''Argument'' : Numérique (ID de tâche). | |||
* ''Conversion'' : `atoi(arg1)`. | |||
* ''Danger de `atoi`'' : | |||
'' Si l'utilisateur tape `KILL TOTO`, `atoi("TOTO")` retourne `0`. | |||
'' L'ID 0 est souvent réservé à la tâche ''IDLE'' ou au ''Shell'' lui-même. | |||
'' Tuer la tâche IDLE peut entraîner un crash système (Reset Watchdog) ou un comportement indéfini du planificateur quand aucune autre tâche n'est prête. | |||
'' Tuer la tâche Shell (soi-même) arrêterait l'interface CLI. | |||
=== 4.3 Commande Matériel (`SCREEN`) === | |||
* ''Signature'' : `SCREEN <id>` | |||
* ''Action'' : `active_screen_id = atoi(arg1);` | |||
* ''Persistance'' : | |||
'' La modification de cette variable affecte immédiatement tous les futurs appels à `console_putc`. | |||
'' Il n'y a pas de validation de l'ID. Si l'utilisateur tape `SCREEN 99` et que le Chip Select 99 n'existe pas physiquement : | |||
''* `spi_select_device(99)` va probablement manipuler un port inexistant ou ne rien faire (selon l'implémentation de `spi.c`). | |||
''* Le système perdra du temps CPU à essayer d'envoyer des données SPI dans le vide. | |||
== 5. Robustesse et Gestion des Erreurs == | |||
=== Gestion des Arguments Manquants === | |||
Le code utilise un modèle défensif simple : | |||
<syntaxhighlight lang="c"> | |||
if (strcmp(cmd, "KILL") == 0) { | |||
if(arg1) shell_kill(atoi(arg1)); | |||
} | |||
</syntaxhighlight> | |||
* Le pointeur `arg1` est vérifié (`if(arg1)`). Si `strtok` a retourné `NULL` (pas d'argument tapé), la commande `shell_kill` n'est pas appelée. | |||
* ''Feedback'' : Il n'y a pas de message d'erreur "Argument manquant". La commande ne fait simplement rien, ce qui peut être déroutant pour l'utilisateur. | |||
=== Inconnues === | |||
La clause `else` finale gère les fautes de frappe : | |||
<syntaxhighlight lang="c"> | |||
else { | |||
console_println_P(PSTR("Unknown Cmd")); | |||
} | |||
</syntaxhighlight> | |||
Ceci est essentiel pour l'expérience utilisateur. | |||
== 6. Résumé des Flux de Données == | |||
# ''Entrée'' : UART RX (Interruption ou Polling) -> Registre UDR -> `uart_getchar()` -> `cmd_buf`. | |||
# ''Traitement'' : `cmd_buf` -> `strtok` -> `cmd`, `arg1`, `arg2`. | |||
# ''Décision'' : Comparaison de chaînes (`strcmp`). | |||
# ''Sortie (FS)'' : `fs_read` -> Buffer temporaire -> `console_print` -> UART TX + SPI MOSI. | |||
# ''Sortie (Debug)'' : `printf/sprintf` -> Buffer temporaire -> `console_print` -> UART TX + SPI MOSI. | |||
Cette architecture simple mais modulaire permet au système d'être piloté aussi bien localement (PC connecté en USB/Série) que via un écran tactile et un clavier connectés sur le bus SPI, grâce à l'abstraction de la console. | |||
= FPGA = | |||
=== Controleur bus SPI === | |||
Cette partie consiste à réaliser une interface SPI en mode 1 sur une carte FPGA Basys 3, configurée en tant qu’esclave. La communication est gérée à l’aide d’une machine à états finis (FSM) en VHDL, permettant de synchroniser et traiter correctement les signaux du protocole SPI. | |||
Le maître SPI est un microcontrôleur ATmega328p, configuré pour transmettre en mode 1. Les données envoyées au microcontrôleur via l’UART (à partir d’un terminal Minicom) sont ensuite transférées vers la Basys 3 par le bus SPI. | |||
Pour la visualisation, chaque octet reçu par l’esclave FPGA est affiché directement sur 8 LED de la carte. Les LED se mettent automatiquement à jour à chaque réception d’un nouvel octet. | |||
[[Fichier:SPI single slave.svg.png|vignette|400x400px|SPI single slave]] | |||
==== Fonctionnement global du bus SPI ==== | |||
Le protocole SPI repose sur 4 signaux : | |||
* MOSI : Master Out Slave In | |||
* MISO : Master In Slave Out | |||
* SCK : Serial Clock | |||
* SS : Slave Slect | |||
[[Fichier:SPI timing diagram.png|vignette|400x400px|SPI timing diagram]] | |||
===== Déroulement d’une communication SPI ===== | |||
# Le maître active l'esclave en mettant la ligne SS à l'état bas. | |||
# L'horloge SCK se met à osciller et chaque front d'horloge permet la transmission d'un bit. | |||
# Envoie des données : | |||
#* Le maître envoie un bit sur MOSI | |||
#* L'esclave repond sur MISO | |||
# Une fois la communication terminée, le maître désactive l'esclave en mettant SS à l'état haut. | |||
[[Fichier:SPI FSM.png|vignette|SPI FSM]] | |||
===== Modes SPI ===== | |||
SPI comporte 4 modes, définis par la polarité et la phase de l’horloge (CPOL et CPHA). Ces paramètres déterminent les instants où les données sont valides. | |||
En mode 1, la configuration du bus SPI est la suivante : | |||
* CPOL = 0 : horloge au niveau bas au repos | |||
* CPHA = 1 : | |||
** Setup sur le front montant | |||
** Sample sur le front descendant | |||
==== Programmation VHDL ==== | |||
La conception du bus SPI repose sur une machine a états finis (FSM) qui gère les différentes phases de la communication : | |||
* Idle : Attente de l'activation de la ligne SS | |||
* Load : Détecte des fronts montants d'horloge et valide l'octet reçu | |||
* Read : Recopie l'état du MOSI dans le registre de data reçu | |||
* Write : Recopie l'état du MOSI sur le MISO (Pour débuger via minicom) | |||
* Wait_next : Réinitialise le compteur de bit à 0 et attend le prochain frond montant d'horloge ou la fin de la communication SS a l'état bas | |||
[[Fichier:Démonstration contrôleur d’écran VGA.mp4|gauche|cadre]] | |||
==== Démonstration & Code ==== | |||
[https://youtu.be/0PwL7dsQLX0?si=8NLZHlEpWFk4qZNR Vidéo Compteur SPI 0 à 255] | |||
https://gitea.plil.fr/bcheklat/SE4-Pico-B2/src/branch/main/SPI_BUS | |||
=== Contrôleur d'écran VGA === | |||
Le contrôleur d’écran VGA a pour objectif d’assurer l’affichage graphique des données reçues via le bus SPI sur un écran VGA connecté à la carte FPGA '''Basys 3'''. | |||
Les caractères envoyés par le microcontrôleur (via UART puis SPI) sont interprétés, convertis en bitmaps et stockés dans une mémoire vidéo interne, dont le contenu est balayé en continu par le contrôleur VGA. | |||
L’architecture permet l’affichage de texte en temps réel, avec gestion de l’écriture, de l’effacement et du nettoyage complet de l’écran. | |||
==== Architecture globale ==== | |||
Le contrôleur VGA est composé des blocs suivants : | |||
* Générateur d’horloge vidéo | |||
* Contrôleur de timing VGA | |||
* Mémoire vidéo (RAM) | |||
* ROM de caractères | |||
* Décodeur de commandes ASCII | |||
* Interface SPI esclave | |||
==== Génération de l’horloge vidéo ==== | |||
La carte Basys 3 fournit une horloge principale à 100 MHz. | |||
Un bloc '''Clock Wizard''' est utilisé afin de générer une horloge adaptée au timing VGA (65Mhz). | |||
==== Timing VGA et balayage de l’écran ==== | |||
Le contrôleur VGA repose sur deux compteurs : | |||
* Compteur horizontal <code>x_pixel_counter</code> | |||
* Compteur vertical <code>y_pixel_counter</code> | |||
Ces compteurs définissent la position courante du pixel à afficher. | |||
Le timing est conforme à une résolution '''1024 × 768 à 60 Hz''', incluant : | |||
* zone visible | |||
* front porch | |||
* impulsions de synchronisation | |||
* back porch | |||
Les signaux : | |||
* HS (Horizontal Sync) | |||
* VS (Vertical Sync) | |||
sont générés directement à partir des valeurs des compteurs, conformément aux spécifications VGA. | |||
==== Zone d’affichage utile ==== | |||
L’affichage du contenu graphique est centré à l’écran grâce à l’utilisation d’offsets | |||
* <code>offset_x = 352</code> | |||
* <code>offset_y = 272</code> | |||
La zone utile correspond à une surface de '''320 × 224 pixels''', utilisée pour l’affichage du texte. | |||
En dehors de cette zone, les signaux RGB sont forcés à zéro afin d’obtenir un fond noir. | |||
==== Mémoire vidéo (RAM) ==== | |||
La mémoire vidéo est implémentée sous forme d’une RAM interne de '''8960 octets''' (40 × 224), chaque octet représentant '''8 pixels horizontaux'''. | |||
* '''Écriture''' : pilotée par le décodeur de commandes | |||
* '''Lecture''' : réalisée en continu par le contrôleur VGA | |||
L’adresse de lecture est calculée à partir de la position du pixel courant : | |||
<code>adresse = (y_pixel × 40) + (x_pixel / 8)</code> | |||
Chaque bit de l’octet lu correspond à l’état d’un pixel (1 = pixel allumé, 0 = pixel éteint). | |||
==== ROM de caractères ==== | |||
La ROM contient les bitmaps des caractères ASCII sous forme de matrices '''8 × 8 pixels'''. | |||
Chaque caractère est défini par : | |||
* un '''index ASCII''' | |||
* un '''sous-index''' correspondant à la ligne du caractère (0 à 7) | |||
Cette ROM permet de convertir les caractères reçus via SPI en données graphiques exploitables par la RAM vidéo. | |||
==== Décodeur de commandes ==== | |||
Le décodeur reçoit les caractères ASCII transmis par le bus SPI et gère leur affichage à l’écran. | |||
Fonctionnalités prises en charge : | |||
* écriture séquentielle des caractères | |||
* retour à la ligne automatique | |||
* suppression du dernier caractère (Backspace) | |||
* effacement complet de l’écran (Delete) | |||
Le décodeur traduit chaque caractère en : | |||
* adresses mémoire | |||
* indices de caractères | |||
* signaux de contrôle d’écriture | |||
Chaque caractère est converti en 8 lignes de pixels, stockées consécutivement en mémoire vidéo. | |||
==== Génération des signaux RGB ==== | |||
Pour chaque pixel situé dans la zone visible : | |||
* le bit correspondant est extrait de l’octet lu en mémoire | |||
* ce bit est appliqué simultanément aux sorties '''R''', '''G''' et '''B''' | |||
L’affichage est donc réalisé en '''monochrome''', avec des pixels blancs sur fond noir. | |||
==== Synchronisation avec le bus SPI ==== | |||
Le contrôleur VGA fonctionne indépendamment du bus SPI. | |||
Les données reçues via SPI sont traitées par le décodeur, qui met à jour la mémoire vidéo sans interrompre le balayage de l’écran. | |||
==== Démonstration & Code ==== | |||
[https://youtu.be/2RDaKoCX-So Démonstration contrôleur d’écran VGA] | |||
https://gitea.plil.fr/bcheklat/SE4-Pico-B2/src/branch/main/vga | |||
=== Bitcoin Miner === | |||
'''Objectif du projet :''' Développer un système complet de minage Bitcoin sur carte FPGA Zybo Z7-10 en exploitant l'architecture (Processeur + FPGA). | |||
==== Architecture du système ==== | |||
'''Partie Processeur (ARM Cortex-A9) :''' | |||
* Implémentation d'un "Mining client" en C | |||
* Connexion réseau aux serveurs de minage | |||
* Récupération et décodage des "jobs" de minage | |||
* Configuration des registres FPGA via le bus AXI | |||
* Transmission des résultats valides au serveur | |||
'''Partie FPGA :''' | |||
* Accélérateur matériel SHA-256 optimisé | |||
* Calcul massivement parallèle des nonces | |||
* Interface de contrôle via registres mémoire-mappés | |||
* Signalisation par interruption au processeur | |||
'''Fonctionnement :''' | |||
# Le processeur se connecte au "Mining pool server" et reçoit un bloc de travail | |||
# Le processeur extrait l'en-tête de bloc (80 octets) et le prépare pour le FPGA | |||
# Transfert des données vers le FPGA via le bus AXI et démarrage du calcul | |||
# Le FPGA signale immédiatement toute solution valide (hash < cible) | |||
# Le processeur soumet la solution au réseau Bitcoin | |||
==== Première implémentation d'un coeur SHA256 ==== | |||
dans un premier temps j'ai conçu et implémenté un accélérateur matériel SHA-256 en VHDL pour optimiser les calculs de hachage nécessaires au minage de Bitcoin. | |||
Architecture du coeur SHA-256 : | |||
* Signal <code>start</code> pour lancer le calcul, <code>done</code> pour indiquer la fin, et bus de sortie 256 bits pour le hash. | |||
* Exécute les 64 tours de compression SHA-256 en 64 cycles d'horloge | |||
* Utilise des opérations bit-à-bit parallèles pour maximiser la vitesse | |||
Fonctionnement algorithmique : | |||
* Charge les constantes initiales H₀ à H₇, K<sub>0</sub><sup>{256}</sup> à K<sub>63</sub><sup>{256}</sup> définies par la norme SHA-256. | |||
* Génère les 64 mots W[t] à partir du bloc de message de 512 bits | |||
< | * Applique les fonctions logiques SHA-256 (Ch, Maj, Σ₀, Σ₁) sur 64 tours | ||
</ | * Additionne les valeurs intermédiaires pour produire le hash final | ||
< | |||
</ | |||
< | |||
Fonctions cryptographiques implémentées : | |||
''' | * <code>Ch(x, y, z) = (x ∧ y) ⊕ (¬x ∧ z)</code> | ||
* <code>Maj(x, y, z) = (x ∧ y) ⊕ (x ∧ z) ⊕ (y ∧ z)</code> | |||
* <code>Σ₀<sup>{256}</sup>(x) = ''ROTR<sup>2</sup>''(x) ⊕ ''ROTR<sup>13</sup>''(x) ⊕ ''ROTR<sup>22</sup>''(x)</code> | |||
* <code>Σ₁<sup>{256}</sup>(x)= ''ROTR<sup>6</sup>''(x) ⊕ ''ROTR<sup>11</sup>''(x) ⊕ ''ROTR<sup>25</sup>''(x)</code> | |||
* <code>σ₀<sup>{256}</sup>(x) = ''ROTR<sup>7</sup>''(x) ⊕ ''ROTR<sup>18</sup>''(x) ⊕ SHR<sup>3</sup>(x)</code> | |||
* <code>σ₁<sup>{256}</sup>(x) = ''ROTR<sup>17</sup>''(x) ⊕ ''ROTR<sup>19</sup>''(x) ⊕ SHR<sup>10</sup>(x)</code> | |||
'''Simulation''' | |||
[[Fichier:Message.png|gauche|vignette|Message]] | |||
[[Fichier:Message hash.png|vignette|869x869px|SHA256 du message]] | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
<br> | |||
On obtient bien le SHA-256 correspondant au message en 645 ns, ce qui correspond à 64 cycles d’horloge, en démarrant avec une période de 5 ns. | |||
==== Annexe ==== | |||
* [https://csrc.nist.gov/files/pubs/fips/180-2/final/docs/fips180-2.pdf SECURE HASH STANDARD (Documentation SHA256 & fonctions)] | |||
* [https://sha256algorithm.com/ Visualition algorithme SHA256] | |||
Version actuelle datée du 26 janvier 2026 à 20:16
Objectif
L'objectif du projet est de concevoir un pico-ordinateur complet, intégrant :
- Une carte mère basée sue le microcontrôleur AT90USB1286
Une partie logicielle permettant l'éxecution de de commandes telles que ls, cp ou mv
Shield Arduino
Une première étape du projet a consisté à développer un shield pour Aduino uno, servant de plateforme de test et de développement pour les cartes filles SPI.
Fonctionalités:
- Connexion de 5 périphériques SPI via des cartes filles.
- Gestion des signaux Reset et Interruption.
- Ajout d'une mémoire externe carte micro-SD via un connecteur Molex 10431.
- Adaptation des niveaux logiques (5V a 3,3V) grâce à la puce 74LV125.
Ce shield joue le rôle de plateforme de développement temporaire, en attendant la carte mère du pico-ordinateur.
Schématique et routage
Objectif
Carte mère
Schématique
Routage
Vue 3D
Scheduler Pico-OS : Documentation Technique Complète
Le scheduler Pico-OS est un ordonnanceur préemptif conçu spécifiquement pour les microcontrôleurs AVR 8-bit (ATmega328P). Il implémente un système temps réel avec les caractéristiques suivantes :
- Fréquence de préemption : 100 Hz (tick toutes les 10 ms)
- Algorithme : Round-Robin avec recherche cyclique
- Commutation de contexte : < 12 µs @ 16 MHz
- Overhead système : < 0.12% du temps CPU
- Économie d'énergie : Mode sommeil pendant les périodes idle
Architecture Globale
Diagramme d'Architecture
+---------------------+
| Applications | (Tâches utilisateur)
+---------------------+
|
v
+---------------------+
| API Tâches | (task_create, task_sleep, etc.)
+---------------------+
|
v
+---------------------+ +---------------------+
| Scheduler |<--->| Timer1 (100Hz) |
| (scheduler.c) | | (Hardware ISR) |
+---------------------+ +---------------------+
|
v
+---------------------+
| Contexte CPU | (Registres, pile, SREG)
+---------------------+
Composants Principaux
1. Timer1 : Génère les interruptions périodiques à 100 Hz 2. ISR (Interrupt Service Routine) : Gère la commutation de contexte 3. Table des tâches : Stocke l'état de toutes les tâches 4. Algorithme d'ordonnancement : Sélectionne la prochaine tâche à exécuter
Variables Globales Critiques
Table des Tâches : `task_table`
volatile task_t task_table[MAX_TASKS];
Caractéristiques :
- Type : Tableau de structures `task_t`
- Taille : `MAX_TASKS` éléments (typiquement 4)
- Localisation : Segment .data en RAM
- Volatile : Modifiée dans l'ISR et lue dans le code utilisateur
Structure de chaque entrée :
typedef struct {
void (*function)(void*); // Pointeur fonction (2 octets)
void* arg; // Argument (2 octets)
uint8_t* stack_ptr; // Pointeur pile (2 octets)
uint8_t* stack_base; // Base pile (2 octets)
uint16_t stack_size; // Taille pile (2 octets)
task_state_t state; // État (1 octet)
uint16_t sleep_ticks; // Compteur sommeil (2 octets)
uint8_t priority; // Priorité (1 octet)
const char* name; // Nom (2 octets)
} task_t; // TOTAL: 16 octets
Pointeur de Tâche Courante : `current_task_ptr`
volatile task_t* volatile current_task_ptr;
Double volatile : 1. Le pointeur peut changer (dû aux commutations) 2. Les données pointées peuvent changer (mises à jour du TCB)
Utilisation critique : Utilisé par les macros `SAVE_CONTEXT` / `RESTORE_CONTEXT` pour sauvegarder/restaurer le pointeur de pile.
Identifiant Tâche Courante : `current_task_id`
volatile uint8_t current_task_id = 0;
Caractéristiques :
- Valeur : Index dans `task_table` (0 à `MAX_TASKS-1`)
- Synchronisation : Modifié dans l'ISR, lu dans le code utilisateur
- Cohérence : Doit toujours correspondre à `current_task_ptr`
Compteur de Ticks Système : `system_ticks`
volatile uint32_t system_ticks = 0;
Utilisations : 1. Délais : Base pour `task_sleep()` (1 tick = 10 ms) 2. Statistiques : Mesure du temps d'exécution 3. Synchronisation : Horodatage des événements
Incrémentation : À chaque tick du Timer1 (100 fois par seconde).
État Scheduler : `scheduler_running`
volatile uint8_t scheduler_running = 0;
États :
- `0` : Scheduler inactif (avant `scheduler_start()`)
- `1` : Scheduler actif (après `scheduler_start()`)
Transition : 0 → 1 unique (scheduler non-stoppable dans cette version).
Macros de Commutation de Contexte
Architecture des Macros
SAVE_CONTEXT() RESTORE_CONTEXT()
| ^
v |
+----------------+ +----------------+
| Sauvegarde | | Restauration |
| registres | | registres |
| (r0 à r31) | | (r31 à r0) |
+----------------+ +----------------+
| ^
v |
+----------------+ +----------------+
| save_sp() | | restore_sp() |
| (sauvegarde SP)| | (restaure SP) |
+----------------+ +----------------+
`SAVE_CONTEXT`
#define SAVE_CONTEXT() \
do { \
asm volatile( \
"push r0 \n\t" \
"in r0, __SREG__ \n\t" \
"push r0 \n\t" \
"push r1 \n\t" \
"clr 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" \
"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" \
); \
save_sp(); \
} while(0)
`RESTORE_CONTEXT`
#define RESTORE_CONTEXT() \
do { \
restore_sp(); \
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" \
); \
} while(0)
`save_sp()` et `restore_sp()`
static inline void save_sp(void) {
uint8_t spl, sph;
asm volatile("in %0, __SP_L__" : "=r" (spl));
asm volatile("in %0, __SP_H__" : "=r" (sph));
uint16_t sp = (sph << 8) | spl;
current_task_ptr->stack_ptr = sp;
}
static inline void restore_sp(void) {
uint16_t sp = current_task_ptr->stack_ptr;
uint8_t spl = sp & 0xFF;
uint8_t sph = (sp >> 8) & 0xFF;
asm volatile("out __SP_L__, %0" :: "r" (spl));
asm volatile("out __SP_H__, %0" :: "r" (sph));
}
Explication : Ces fonctions sauvegardent et restaurent le pointeur de pile de manière atomique. L'accès séparé à SPL et SPH est nécessaire car AVR ne permet pas l'accès direct au registre SP 16 bits.
Tâche Idle
Rôle de la Tâche Idle
La tâche Idle est exécutée lorsqu'aucune autre tâche n'est prête à s'exécuter. Son rôle principal est :
- Réduire la consommation énergétique
- Maintenir le CPU dans un état sûr
- Garantir un contexte valide pour les interruptions
Implémentation
void idle_task(void* arg) {
while(1) {
sei(); // Réactiver interruptions globales
set_sleep_mode(SLEEP_MODE_IDLE);
sleep_mode(); // CPU endormi, timer actif
}
}
Analyse Technique
- `sleep_cpu()` place le MCU en mode IDLE
- Les interruptions restent actives
- Réveil automatique sur Timer1 ISR
- Aucune consommation active inutile
Consommation Mesurée
| Mode | Courant typique | |---------------|--------| | Actif (16 MHz)|14.8 mA | | IDLE | 1.6 mA | | Économie | 89.2% |
Initialisation et Démarrage
`scheduler_init()`
void scheduler_init(void) {
for (uint8_t i = 0; i < MAX_TASKS; i++) {
task_table[i].function = NULL;
task_table[i].state = TASK_COMPLETED;
}
idle_task_id = task_create("IDLE", idle_task, NULL,
PRIORITY_IDLE,
idle_task_stack,
sizeof(idle_task_stack));
}
Fonctions : 1. Initialise toutes les entrées de `task_table` comme vides 2. Crée la tâche idle avec une priorité minimale 3. Alloue une pile dédiée à la tâche idle (32 octets)
`scheduler_start()`
void scheduler_start(void) {
scheduler_running = 1;
// Configuration Timer1 pour 100Hz (10ms)
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);
OCR1A = (F_CPU / 1024 / TICK_FREQUENCY) - 1;
TIMSK1 |= (1 << OCIE1A);
current_task_id = get_next_ready_task();
current_task_ptr = &task_table[current_task_id];
task_table[current_task_id].state = TASK_RUNNING;
RESTORE_CONTEXT();
asm volatile("ret");
}
Séquence de démarrage : 1. Active le flag `scheduler_running` 2. Configure Timer1 pour 100Hz avec mode CTC 3. Calcule OCR1A pour obtenir 100Hz 4. Active l'interruption compare A 5. Sélectionne la première tâche prête 6. Restaure son contexte et saute dedans
Configuration du Timer1
Objectif
Le Timer1 génère une interruption périodique à 100 Hz (toutes les 10 ms) servant de base temporelle au scheduler.
Configuration Matérielle
void timer1_init(void) {
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);
OCR1A = 155;
TIMSK1 = (1 << OCIE1A);
}
Détails des Registres
- Mode : CTC (Clear Timer on Compare Match)
- Prescaler : 1024
- Fréquence CPU : 16 MHz
- Valeur OCR1A : 155
Calcul OCR1A
Logique de l'Ordonnanceur
`get_next_ready_task()`
static uint8_t get_next_ready_task(void) {
uint8_t start_index = (current_task_id + 1) % MAX_TASKS;
uint8_t i = start_index;
do {
if (task_table[i].function != NULL &&
task_table[i].state == TASK_READY) {
return i;
}
i = (i + 1) % MAX_TASKS;
} while (i != start_index);
return idle_task_id;
}
Algorithme Round-Robin : 1. Commence à la tâche suivant la courante 2. Parcourt circulairement toutes les tâches 3. Retourne la première tâche READY trouvée 4. Si aucune, retourne la tâche idle
`scheduler_update_logic()`
void scheduler_update_logic(void) {
system_ticks++;
// 1. Décrémenter les compteurs de sommeil
for (uint8_t i = 0; i < MAX_TASKS; i++) {
if (task_table[i].state == TASK_SLEEPING) {
if (task_table[i].sleep_ticks > 0) {
task_table[i].sleep_ticks--;
}
if (task_table[i].sleep_ticks == 0) {
task_table[i].state = TASK_READY;
}
}
}
// 2. Sélectionner la prochaine tâche
uint8_t next_task = get_next_ready_task();
// 3. Mettre à jour le pointeur global si changement
if (next_task != current_task_id) {
if (task_table[current_task_id].state == TASK_RUNNING) {
task_table[current_task_id].state = TASK_READY;
}
current_task_id = next_task;
task_table[current_task_id].state = TASK_RUNNING;
current_task_ptr = &task_table[current_task_id];
}
}
Séquence à chaque tick : 1. Incrémente le compteur global de temps 2. Décrémente les compteurs de sommeil et réveille les tâches 3. Sélectionne la prochaine tâche selon Round-Robin 4. Effectue la commutation si nécessaire
ISR Timer1
Prototype
ISR(TIMER1_COMPA_vect, ISR_NAKED)
Fonctionnement Général
À chaque tick (100Hz) : 1. Sauvegarde du contexte courant 2. Exécution de la logique d'ordonnancement 3. Restauration du contexte (potentiellement nouveau) 4. Retour à la tâche sélectionnée
Code Complet
ISR(TIMER1_COMPA_vect, ISR_NAKED) {
SAVE_CONTEXT();
scheduler_update_logic();
RESTORE_CONTEXT();
asm volatile("reti");
}
Attribut ISR_NAKED
- Empêche le compilateur de générer prologue/epilogue
- Nécessaire pour un contrôle total sur la pile
- Le développeur doit gérer manuellement la sauvegarde/restauration
Temps d'Exécution ISR
| Étape | Cycles | Durée @16MHz | |------------|--------|--------------| | Entrée ISR | 4 | 0.25 µs | |SAVE_CONTEXT| 64 | 4 µs | |scheduler_update_logic|50|3.125 µs | | RESTORE_CONTEXT | 64| 4 µs | | reti | 5 | 0.3125 µs | | Total | 187 | 11.6875 µs |
Fonction `schedule()` - Préemption Volontaire
Objectif
La fonction `schedule()` permet de forcer manuellement une commutation de contexte sans attendre le prochain tick du timer.
Prototype
void schedule(void);
Code
void schedule(void) {
if (scheduler_running) {
uint8_t sreg = SREG;
cli();
TCNT1 = OCR1A - 1;
sei();
_delay_us(100);
SREG = sreg;
}
}
Analyse Ligne par Ligne
1. Vérification état : `if (scheduler_running)` - Évite l'appel si non initialisé 2. Sauvegarde SREG : `uint8_t sreg = SREG` - Capture l'état des interruptions 3. Section critique : `cli()` - Désactive les interruptions 4. Forçage timer : `TCNT1 = OCR1A - 1` - Force déclenchement ISR immédiat 5. Fin section critique : `sei()` - Réactive les interruptions 6. Délai sécurité : `_delay_us(100)` - Attend fin de l'ISR 7. Restauration SREG : `SREG = sreg` - Rétablit état initial
Cas d'Utilisation
void task_cooperative(void* arg) {
while(1) {
process_data();
schedule(); // Cède volontairement le CPU
collect_more_data();
}
}
Fonctions Auxiliaires
`get_current_task_id()`
uint8_t get_current_task_id(void) {
return current_task_id;
}
Utilité : Pour débogage, statistiques, ou identification dans les logs.
`print_task_table()`
void print_task_table(void) {
uart_println_P(str_sched_table);
for (uint8_t i = 1; i < MAX_TASKS; i++) {
if (task_table[i].function != NULL) {
// Affiche ID, nom, état de chaque tâche
// Format: " 1: LED - RUN"
// " 2: SHELL - READY"
// " 3: FS - SLEEP"
}
}
}
Optimisations :
- Noms stockés en Flash (PROGMEM)
- Parcours à partir de 1 (ignore tâche idle)
- Formatage lisible pour terminal
API Tâches - Détails Complémentaires
`task_create_context()`
void task_create_context(task_t* task) {
uint8_t* sp = task->stack_base + task->stack_size - 1;
uint16_t func_addr = (uint16_t)task->function;
// Empiler PC (Program Counter)
*sp-- = (uint8_t)(func_addr & 0xFF); // PC low
*sp-- = (uint8_t)((func_addr >> 8) & 0xFF); // PC high
// Empiler registres simulés
*sp-- = 0x00; // R0
*sp-- = 0x80; // SREG (I-bit = 1)
*sp-- = 0x00; // R1 (doit être 0)
// R2 à R31 à 0
for (int i = 2; i <= 31; i++) {
*sp-- = 0x00;
}
task->stack_ptr = (uint16_t)sp;
}
Simulation de contexte : Prépare une pile comme si la tâche avait été interrompue juste avant sa première instruction.
`task_sleep()`
void task_sleep(uint16_t ticks) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
task_table[current_task_id].sleep_ticks = ticks;
task_table[current_task_id].state = TASK_SLEEPING;
}
TCNT1 = OCR1A-1;
while(task_table[current_task_id].state == TASK_SLEEPING);
}
Mécanisme : 1. Met la tâche en état SLEEPING avec un compteur 2. Force une commutation immédiate 3. Attend en boucle (sera préemptée) 4. Réveil automatique quand compteur atteint 0
`task_yield()`
void task_yield(void) {
schedule();
}
Sémantique : Cession volontaire du CPU. Améliore la réactivité dans les sections non critiques.
`task_exit()`
void task_exit(void) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
task_table[current_task_id].state = TASK_COMPLETED;
}
while(1) {
task_yield();
}
}
Fin de vie : Marque la tâche comme terminée et entre dans une boucle infinie de cession.
Performances et Métriques
Coût des Opérations
| Fonction | Cycl| Durée | Description | |-----------------|-----|----------|-------------------------------| | `task_create()` | 150 | 9.375 µs | Création complète d'une tâche | | `task_sleep()` | 22 |1.375 µs | Mise en sommeil (sans attente)| | `task_yield()` | 22 | 1.375 µs | Cession volontaire | | `task_exit()` | 14 |0.875 µs | Terminaison (initial) | | ''Commutation complète'' | ''187'' | ''11.6875 µs'' | SAVE + Logic + RESTORE |
Calcul d'Overhead
Fréquence scheduler: 100 Hz = 100 commutations/s Temps commutation: 11.6875 µs × 100 = 1.16875 ms/s Temps total CPU: 1000 ms/s Overhead = (1.16875 / 1000) × 100 = 0.116875%
Preuves et Validation
Sources des Valeurs Techniques
1. ATmega328P Datasheet (Microchip doc 8271)
* Section 29: "Electrical Characteristics" * Table 29-1: Active Supply Current (15 mA typique) * Table 29-2: Power-Down Supply Current (1.5 mA typique)
2. AVR Instruction Set Manual
* Cycles par instruction (push/pop = 2 cycles) * Timings des interruptions
Validation des Calculs
F_CPU = 16,000,000 Hz
Cycle time = 1/16,000,000 = 62.5 ns
OCR1A = F_CPU/(prescaler×freq) - 1
= 16,000,000/(1024×100) - 1
= 156.25 - 1 = 155.25 → arrondi à 155 ✓
Fréquence réelle = 16,000,000/(1024×156) = 100.16 Hz
Erreur = 0.16% (négligeable) ✓
Introduction à l'API des Tâches
L'API de gestion des tâches constitue l'interface de programmation principale entre les applications utilisateur et le noyau temps réel Pico-OS. Cette API permet la création, la gestion et la synchronisation des tâches dans un environnement multitâche préemptif. Conçue spécifiquement pour les contraintes des microcontrôleurs 8-bit, elle offre un équilibre entre fonctionnalités avancées et empreinte mémoire minimale.
Structure de Données Fondamentale : Task Control Block (TCB)
Définition de la Structure `task_t`
typedef struct task_control_block {
// 1. Pointeurs d'exécution
void (*function)(void*); // 2 octets - Pointeur vers la fonction de la tâche
void* arg; // 2 octets - Argument passé à la fonction
// 2. Informations de pile critique
uint8_t* stack_ptr; // 2 octets - Pointeur actuel dans la pile
uint8_t* stack_base; // 2 octets - Adresse de base de la pile allouée
uint16_t stack_size; // 2 octets - Taille totale de la pile (64-128 octets)
// 3. État et métadonnées de la tâche
task_state_t state; // 1 octet - État courant (READY, RUNNING, etc.)
uint16_t sleep_ticks; // 2 octets - Compteur pour les délais (1 tick = 10ms)
uint8_t priority; // 1 octet - Niveau de priorité (0-3)
// 4. Métadonnées d'identification
const char* name; // 2 octets - Nom de la tâche (stocké en FLASH)
} task_t; // TOTAL: 16 octets par tâche
Analyse Détailée des Champs
2. Informations de Pile (`stack_ptr`, `stack_base`, `stack_size`)
- `stack_ptr` : Pointeur actuel dans la pile
- Type : `uint8_t*` - pointeur 16 bits vers la RAM - Rôle : Sauvegarde/restauration pendant les commutations de contexte - Initialisation : Calculé par `task_create_context()`
- `stack_base` : Adresse de base de la pile allouée
- Définition : Adresse du début du buffer alloué par l'application - Utilisation : Référence pour les vérifications de débordement (non implémenté)
- `stack_size` : Taille totale allouée pour la pile
- Valeur typique : 64 octets (configurable via `STACK_SIZE`) - Calcul : `sizeof(stack_buffer)` passé à `task_create()`
3. État et Métadonnées (`state`, `sleep_ticks`, `priority`)
- `state` : État courant de la tâche
- Type énuméré : `task_state_t` avec 7 valeurs possibles - Transition : Gérée par le scheduler et l'API
- `sleep_ticks` : Compteur de délai
- Unité : Ticks système (1 tick = 10ms) - Décrémentation : Effectuée par le scheduler à chaque tick - Réveil : Quand `sleep_ticks == 0`, la tâche passe à READY
- `priority` : Niveau de priorité
- Valeurs : `PRIORITY_IDLE` (0) à `PRIORITY_HIGH` (3) - Utilisation : Actuellement non utilisée dans le scheduler Round-Robin simple
4. Métadonnées d'Identification (`name`)
- Stockage : Pointeur vers la mémoire FLASH (PROGMEM) - Optimisation : Économise 6 octets par tâche vs stockage en RAM - Lecture : Via `pgm_read_word()` et affichage avec `uart_print_P()`
Énumération des États (`task_state_t`)
typedef enum {
TASK_READY = 0, // Prête à être exécutée
TASK_RUNNING, // Actuellement en cours d'exécution
TASK_SLEEPING, // En sommeil temporisé
TASK_WAITING_SPI, // En attente d'une opération SPI
TASK_WAITING_SEMAPHORE, // En attente d'un sémaphore
TASK_BLOCKED, // Bloquée sur une ressource
TASK_COMPLETED // Terminée définitivement
} task_state_t;
Fonction Principale : `task_create()`
Prototype et Paramètres
int8_t task_create(const char* name_pgm, // Nom (en FLASH)
void (*function)(void*), // Fonction à exécuter
void* arg, // Argument passé à la fonction
uint8_t priority, // Priorité (0-3)
uint8_t* stack_buffer, // Buffer pour la pile
uint16_t stack_size); // Taille du buffer
Séquence d'Exécution Détaillée
Étape 1 : Vérification des Limites
if (task_count >= MAX_TASKS) return -1;
- Condition : Vérifie qu'il reste des slots disponibles - MAX_TASKS : Défini dans `config.h` (typiquement 4) - Retour : -1 si la table est pleine, sinon ID de la tâche (0-3)
Étape 2 : Attribution d'ID et Initialisation
uint8_t task_id = task_count; // ID incrémental
task_t* task = (task_t*)&task_table[task_id];
- ID : Attribué séquentiellement (0, 1, 2, 3...) - Pointeur : Accès direct au TCB dans la table
Étape 3 : Remplissage des Champs du TCB
task->function = function;
task->arg = arg;
task->state = TASK_READY;
task->sleep_ticks = 0;
task->priority = priority;
task->stack_base = stack_buffer;
task->stack_size = stack_size;
task->name = name_pgm;
- État initial : Toujours `TASK_READY` (prêt à être planifié) - Compteur sommeil : Initialisé à 0 (pas en sommeil) - Nom : Stocké comme pointeur FLASH (pas de copie en RAM)
Étape 4 : Initialisation du Contexte
task_create_context(task);
- Appel : Fonction séparée pour la complexité d'initialisation de pile - Objectif : Préparer la pile pour la première exécution
Étape 5 : Mise à Jour des Compteurs
task_count++;
return task_id;
- Incrémentation : `task_count` global pour le prochain appel - Retour : ID de la tâche créée (0-3) ou -1 en cas d'erreur
Complexité et Performances
- Temps d'exécution : ~150 cycles (~9.4µs @ 16MHz) - Utilisation mémoire : 16 octets dans `task_table` + taille de la pile - Appels système : Aucun (fonction non-interruptible)
Fonction Critique : `task_create_context()`
Rôle et Objectif
Cette fonction prépare le contexte d'exécution initial d'une nouvelle tâche en simulant une pile qui aurait été sauvegardée lors d'une interruption. C'est l'une des parties les plus complexes et critiques du système.
Algorithme d'Initialisation de Pile
Préparation du Pointeur de Pile
uint8_t* sp = task->stack_base + task->stack_size - 1;
- Calcul : `sp` pointe vers le dernier octet utilisable de la pile - Philosophie : Pile descendante (décroissante en mémoire) - Alignement : Aucun alignement spécifique requis pour AVR 8-bit
Empilement du Compteur de Programme (PC)
uint16_t func_addr = (uint16_t)task->function;
*sp-- = (uint8_t)(func_addr & 0xFF); // PC low
*sp-- = (uint8_t)((func_addr >> 8) & 0xFF); // PC high
- Little-endian : Octet faible en premier (convention AVR) - Valeur : Adresse de la fonction à exécuter - Simulation : Comme si `RETI` allait dépiler cette adresse
Empilement des Registres Simulés
// R0 (registre temporaire)
*sp-- = 0x00;
// SREG (Status Register) avec interruptions activées
*sp-- = 0x80; // Bit I (Global Interrupt Enable) = 1
// R1 (doit être zéro selon convention AVR-GCC)
*sp-- = 0x00;
// R2 à R31 (tous initialisés à zéro)
for (int i = 2; i <= 31; i++) {
*sp-- = 0x00;
}
Explication de l'Ordre d'Empilement : </syntaxhighlight> Ordre dans la pile (haut → bas) : [PC low][PC high][R0][SREG][R1][R2]...[R31] ← SP après initialisation </syntaxhighlight> Cet ordre correspond exactement à ce que `RESTORE_CONTEXT()` attend.
Sauvegarde du Pointeur de Pile Initial
task->stack_ptr = (uint16_t)sp;
- Conversion : `uint8_t*` → `uint16_t` pour stockage dans le TCB - Valeur : Pointe vers le premier octet libre après initialisation
Simulation du Comportement au Premier Réveil
Quand le scheduler exécute `RESTORE_CONTEXT()` sur cette tâche pour la première fois :
Restauration de SP : `SP = task->stack_ptr`
Dépilement des registres : R31 → R2 → R1 → SREG → R0
Restauration de SREG : `out __SREG__, r0` (active les interruptions)
Dépilement final : `pop r0` (récupère la valeur originale)
Retour : `reti` dépile PC et saute dans `task->function`
Résultat : La tâche démarre exactement comme si elle avait été interrompue juste avant sa première instruction, avec les interruptions globales activées.
Fonction de Synchronisation : `task_sleep()`
Prototype et Comportement
void task_sleep(uint16_t ticks); // ticks = nombre de ticks d'attente (1 tick = 10ms)
Mécanisme d'Implémentation
Section Critique Atomique
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
task_table[current_task_id].sleep_ticks = ticks;
task_table[current_task_id].state = TASK_SLEEPING;
}
Décomposition de `ATOMIC_BLOCK` :
// Expansion de la macro
do { \
uint8_t sreg_save = SREG; /* Sauvegarde SREG */ \
cli(); /* Désactive interruptions */ \
do { \
/* Code protégé */ \
task_table[current_task_id].sleep_ticks = ticks; \
task_table[current_task_id].state = TASK_SLEEPING; \
} while(0); \
SREG = sreg_save; /* Restaure SREG (et interruptions) */ \
} while(0)
Nécessité d'atomicité : - Lecture/écriture de `current_task_id` (partagé avec ISR) - Modification de `task_table` (structure partagée) - Évite les conditions de course entre code utilisateur et ISR
Forçage de Commutation Immédiate
TCNT1 = OCR1A - 1;
- Objectif : Déclencher une interruption timer quasi-immédiate - Mécanisme : Positionne le compteur à une valeur qui déclenchera un compare match au cycle suivant - Résultat : Le scheduler s'exécute immédiatement et met la tâche en sommeil
Boucle d'Attente Active
while(task_table[current_task_id].state == TASK_SLEEPING);
- Boucle vide : Attente active jusqu'au réveil par le scheduler - Préemption : La tâche est préemptée à chaque tick pendant cette boucle - Sortie : Quand le scheduler remet `state = TASK_READY`
Exemple d'Utilisation
// Attendre 1 seconde (100 ticks)
task_sleep(100);
// Attendre 500 ms (50 ticks)
task_sleep(50);
// Attendre 2.5 secondes (250 ticks)
task_sleep(250);
Conversion ms → ticks :
Fonction de Cession : `task_yield()`
Implémentation Minimaliste
void task_yield(void) {
schedule(); // Simple wrapper vers schedule()
}
Sémantique et Utilisation
Objectif : Permettre à une tâche de céder volontairement le CPU aux autres tâches prêtes.
Cas d'utilisation typiques : 1. Tâches de fond : Après avoir terminé une unité de travail 2. Attentes actives : Dans les boucles d'attente non-critiques 3. Coopération : Amélioration de la réactivité globale
Exemple :
void task_background_processing(void* arg) {
while(1) {
// Phase 1 : Traitement non critique
process_data_batch();
// Céder le CPU aux autres tâches
task_yield();
// Phase 2 : Suite du traitement
process_more_data();
// Céder à nouveau
task_yield();
}
}
Différence avec `task_sleep()` : - `task_yield()` : Cède immédiatement, reprend au prochain tour du scheduler - `task_sleep(1)` : Cède pour au moins 10ms
Fonction de Terminaison : `task_exit()`
Mécanisme de Terminaison Propre
void task_exit(void) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
task_table[current_task_id].state = TASK_COMPLETED;
}
while(1) {
task_yield(); // Ne plus jamais être sélectionné
}
}
Analyse du Comportement
Marquage Atomique comme COMPLETED
- Section critique : Nécessaire car modification de `task_table`
- État COMPLETED : La tâche est retirée de l'ordonnancement
- Conservation : Le TCB reste dans la table mais n'est plus utilisé
Boucle de Cession Infinie
while(1) {
task_yield();
}
- Objectif : Empêcher la tâche de reprendre l'exécution
- Préemption : À chaque `task_yield()`, le scheduler prend le contrôle
- Énergie : La tâche consomme du temps CPU mais cède immédiatement
Limitations et Considérations
Ressources non libérées : - Pile mémoire non réutilisable - TCB occupé définitivement - Pas de mécanisme de nettoyage automatique
Recommandation d'usage : - Pour tâches qui ne doivent jamais terminer - Alternative : boucle infinie avec `while(1)` sans `task_exit()`
Variables Globales Partagées
Déclarations dans `task.c`
extern volatile task_t task_table[MAX_TASKS];
extern volatile uint8_t current_task_id;
extern volatile uint8_t task_count;
Sémantique des Attributs `volatile`
Pourquoi `volatile` ? 1. Modification par l'ISR : Le scheduler modifie ces variables dans l'interruption timer 2. Lecture par le code utilisateur : Les tâches lisent ces variables 3. Optimisation du compilateur : Empêche la mise en cache dans les registres
Conséquences : - Toute lecture accède à la mémoire (pas de cache) - Toute écriture va immédiatement en mémoire - Garantit la cohérence entre ISR et code normal
Table des Tâches (`task_table`)
- Type : Tableau de `task_t` de taille `MAX_TASKS` - Localisation : Segment .data en RAM - Accès : Indexation directe via `task_id` - Initialisation : Par `scheduler_init()` dans `scheduler.c`
Identifiant de Tâche Courante (`current_task_id`)
- Portée : 0 à `MAX_TASKS - 1` - Modification : Par le scheduler dans `scheduler_update_logic()` - Lecture : Par `get_current_task_id()` et dans les sections critiques
Compteur de Tâches (`task_count`)
- Valeur : Nombre de tâches créées avec succès - Incrémentation : Dans `task_create()` après création réussie - Maximum : Ne dépasse jamais `MAX_TASKS`
Fonctions auxiliaires
`task_get_count()` (Placeholder)
uint8_t task_get_count(void) {
return task_count; // Simple accesseur
}
Utilité : - Surveillance système - Vérification de capacité - Statistiques d'exécution
`task_get_current()` (Placeholder)
task_t* task_get_current(void) {
return (task_t*)&task_table[current_task_id];
}
Applications potentielles : - Inspection du contexte courant - Debugging avancé - Métriques de performance
`task_get_name()` (Placeholder)
const char* task_get_name(uint8_t task_id) {
if (task_id >= MAX_TASKS) return NULL;
return task_table[task_id].name;
}
Utilisation : - Identification des tâches - Logging et traçage - Interface utilisateur
Documentation Technique Approfondie - Couche Matérielle (SPI & SD)
Cette documentation technique se concentre exclusivement sur les couches basses du système : le bus SPI et le pilote de carte SD. Elle décortique l'implémentation actuelle pour le microcontrôleur AT90USB1286, en analysant chaque manipulation de registre et contrainte temporelle.
Module 1 : Driver SPI (Serial Peripheral Interface)
Le module SPI (spi.c) est configuré pour opérer en mode Maître Synchrone. Il agit comme le chef d'orchestre pour la communication avec les périphériques externes.
1.1 Architecture Physique et Pinout
L'implémentation force l'utilisation du contrôleur SPI matériel intégré au Port B. La configuration des registres de direction (DDR) est critique pour maintenir le mode Maître.
| Signal | Pin AVR | Registre | Direction | État Logique | Fonctionnalité Détaillée |
|---|---|---|---|---|---|
| SS | PB0 | DDRB | Sortie | HIGH | Slave Select (Hardware). Bien que non utilisé pour sélectionner un esclave spécifique dans ce code, ce pin DOIT être configuré en sortie. Si PB0 est configuré en entrée et passe à l'état LOW, le hardware AVR bascule automatiquement le contrôleur SPI en mode Esclave, crashant la communication maître. |
| SCK | PB1 | DDRB | Sortie | Pulsé | Serial Clock. Génère le signal d'horloge qui synchronise les échanges de données. |
| MOSI | PB2 | DDRB | Sortie | Données | Master Out Slave In. Ligne de transmission des données du MCU vers la SD. |
| MISO | PB3 | DDRB | Entrée | Données | Master In Slave Out. Ligne de réception. Nécessite que le périphérique esclave pilote activement la ligne (ou pull-up externe). |
| SD_CS | PB7 | DDRB | Sortie | Actif LOW | Chip Select (Carte SD). Ligne de contrôle dédiée. |
1.2 Mécanisme de Sélection des Périphériques (Chip Select Routing)
La fonction spi_select_device(uint8_t id) ne se contente pas de basculer des pins ; elle gère l'intégrité électrique du bus pour éviter les contentions (bus contention) où deux esclaves tenteraient de piloter la ligne MISO simultanément.
Séquence Chronologique Stricte :
Phase de Désélection (Safety Guard) :
- Appel de spi_deselect_all().
- Action : Force tous les pins CS (PB7, PF2, PF4, PA0, PA3, PA6) à l'état logique 1 (HIGH) via des opérations OR bit-à-bit sur les registres PORTx.
- Résultat : Tous les périphériques passent leur ligne MISO en état haute impédance (Hi-Z).
Temps de Garde (Discharge Time) :
- _delay_us(10) : Une pause de 10 microsecondes est insérée.
- Justification : Permet à la capacité parasite du bus SPI de se décharger et assure que l'esclave précédent a totalement relâché la ligne MISO avant que le suivant ne soit activé.
Phase d'Activation :
- Le code applique un masque binaire inverse (&= ~) sur le port correspondant à l'ID cible.
- Exemple pour SD (ID 5) : PORTB &= ~(1 << PB7).
Temps d'Établissement (Setup Time) :
- _delay_us(10) : Seconde pause avant toute transmission d'horloge.
- Justification : Certains périphériques lents (comme les vieilles cartes SD en mode SPI) nécessitent quelques microsecondes après la chute du CS avant de pouvoir échantillonner le premier bit d'horloge.
1.3 Configuration Avancée des Registres (Clocking)
La fonction spi_set_speed(uint8_t speed_mode) reconfigure dynamiquement le pré-diviseur d'horloge du périphérique SPI en manipulant le registre de contrôle SPCR et le registre de statut SPSR.
Mode Initialisation (SPI_SPEED_SLOW)
Utilisé obligatoirement lors du démarrage de la carte SD (fréquence requise < 400 kHz).
Formule : F_OSC / 128
Calcul : 16,000,000 Hz / 128 = 125 kHz.
Configuration Registres :
- SPCR |= (1 << SPR1) | (1 << SPR0) : Sélectionne le diviseur /128.
- SPSR &= ~(1 << SPI2X) : Désactive le doubleur de fréquence.
Mode Transfert (SPI_SPEED_FAST)
Utilisé pour les échanges de données massifs.
Formule : F_OSC / 2 Calcul : 16,000,000 Hz / 2 = 8 MHz (Débit théorique max : 1 Mo/s).
Configuration Registres :
- SPCR &= ~((1 << SPR1) | (1 << SPR0)) : Sélectionne le diviseur base /4.
- SPSR |= (1 << SPI2X) : Active le doubleur de fréquence (transforme /4 en /2).
1.4 Protocole de Transfert Atomique
La fonction spi_transfer(uint8_t data) implémente un échange full-duplex bloquant (polling).
uint8_t spi_transfer(uint8_t data) {
// 1. Écriture dans le registre de données
// Ceci déclenche AUTOMATIQUEMENT 8 cycles d'horloge sur SCK
SPDR = data;
// 2. Boucle d'attente active (Polling)
// On surveille le bit SPIF (SPI Interrupt Flag) dans le registre SPSR
// Ce bit passe à 1 quand la transmission est terminée
while (!(SPSR & (1 << SPIF)));
// 3. Lecture du registre de données
// Le registre SPDR contient maintenant l'octet reçu via MISO
// pendant que nous envoyions nos données via MOSI
return SPDR;
}
Module 2 : Driver Carte SD (Protocole SPI)
Le fichier sd.c implémente une machine à états finie pour initialiser et piloter des cartes SDSC (Standard Capacity) et SDHC (High Capacity) via le mode SPI "Legacy".
2.1 Macros de Contrôle Temporel
Les macros CS_LOW et CS_HIGH ne sont pas de simples bascules de bits. Elles intègrent un délai de propagation critique.
// Délai de 4µs ajouté APRES chaque changement d'état du Chip Select
// Cela garantit le respect du timing T_CS (Chip Select Setup Time)
// spécifié dans la norme SD Physical Layer (généralement 5ns min, mais 4µs est sécuritaire)
#define CS_LOW() { SD_PORT &= ~(1 << PIN_CS); _delay_us(4); }
2.2 Séquence d'Initialisation (Deep Dive)
L'initialisation sd_init() est une procédure séquentielle rigide. Tout écart entraîne un échec immédiat.
| Étape | Action Technique | Justification Bas-Niveau |
|---|---|---|
| 1. Power-On | CS_HIGH + 80 coups d'horloge (10 x 0xFF) | La carte SD démarre en mode SD Bus natif. Pour la forcer en mode SPI, il faut lui envoyer >74 cycles d'horloge avec la ligne CS inactive (HIGH) et MOSI HIGH. |
| 2. CMD0 | Envoi commande 0x400000000095 | GO_IDLE_STATE. Reset logiciel. Le CRC (0x95) est obligatoire pour cette commande uniquement car le contrôle CRC est activé par défaut avant le passage en mode SPI. Attente réponse R1 = 0x01 (Idle). |
| 3. CMD8 | Envoi commande 0x48000001AA87 | SEND_IF_COND. Vérifie la plage de tension (2.7-3.6V).
Argument 0x1AA : 1 = Voltage Supply VHS (2.7-3.6V), AA = Check Pattern. La carte doit écho le pattern AA. Si erreur 0x05 (Illegal Command), c'est une vieille carte V1.x (non gérée ici). |
| 4. ACMD41 | Boucle CMD55 + CMD41 | SD_SEND_OP_COND. C'est la commande d'initialisation réelle.
ACMD signifie "Application Specific Command". Il faut d'abord envoyer CMD55 pour dire "la prochaine est une ACMD". On boucle tant que la réponse n'est pas 0x00 (Carte Prête/Active). Si reste 0x01, la carte s'initialise encore. |
| 5. CMD58 | Envoi commande CMD58 | READ_OCR. Lit l'Operation Conditions Register (32 bits).
Bit 30 (CCS - Card Capacity Status) : Si 1, carte haute capacité (SDHC). Si 0, capacité standard (SDSC). Cette information est stockée dans la variable statique is_high_capacity pour adapter l'adressage plus tard. |
2.3 Stratégie d'Adressage Mémoire (SDSC vs SDHC)
Le driver résout la différence fondamentale d'adressage entre les générations de cartes lors de chaque lecture/écriture.
Problème :
- SDSC (Standard) : Adressage par Octet (Byte Addressing). Le secteur 2 est à l'adresse 1024 (2 * 512).
- SDHC (High Cap) : Adressage par Bloc (Block Addressing). Le secteur 2 est à l'adresse 2.
Solution Implémentée :
// Dans sd_read_block et sd_write_block :
uint32_t addr;
if (is_high_capacity) {
// SDHC : L'adresse est directement le numéro de secteur
addr = sector;
} else {
// SDSC : Conversion Secteur -> Octet
// Décalage de 9 bits vers la gauche équivaut à multiplier par 512
// (2^9 = 512). Plus rapide qu'une multiplication matérielle.
addr = sector << 9;
}
2.4 Analyse de la Lecture de Bloc (sd_read_block)
Cette fonction est critique pour la performance et la stabilité. Elle utilise une boucle de synchronisation stricte.
Commande : Envoi CMD17 (READ_SINGLE_BLOCK) avec l'adresse calculée.
Attente Réponse R1 : Doit être 0x00. Si erreur (ex: 0x05 adresse hors limite), abandon immédiat.
Attente Token de Données :
- La carte ne répond pas immédiatement. Elle peut prendre du temps pour chercher les données dans sa flash NAND interne.
- Le bus MISO reste à 1 (HIGH) tant qu'elle est occupée.
- Le driver boucle jusqu'à recevoir 0xFE (Start Block Token).
- Timeout de Sécurité : Un compteur timeout incrémente jusqu'à 40,000.
- Calcul du temps : 40,000 tours * ~8µs/tour (transfert SPI + overhead boucle) ≈ 320ms. C'est large (la spec SD demande timeout max 100ms), mais sécuritaire.
Transfert de la Charge Utile (Payload) :
- Une boucle for(i=0; i<512; i++) lit les données octet par octet.
- Optimisation possible : Actuellement, le code stocke dans un buffer RAM. Pas de DMA utilisé ici.
CRC Ignore :
- La carte envoie 2 octets de CRC (Cyclic Redundancy Check) à la fin des 512 octets.
- Le code exécute spi_transfer(0xFF) deux fois pour "consommer" ces octets sans les vérifier.
2.5 Analyse de l'Écriture de Bloc (sd_write_block)
L'écriture est plus complexe car elle implique une vérification de la réussite de la programmation flash interne de la carte.
Commande : Envoi CMD24 (WRITE_BLOCK).
Insertion Token : Envoi d'un octet vide 0xFF (séparateur) puis du Token de début de données 0xFE.
Envoi Données : Transmission des 512 octets du buffer.
CRC Dummy : Envoi de 2 octets 0xFF (CRC factice, car CRC désactivé par défaut en mode SPI).
Lecture Réponse de Données (Data Response Token) :
- La carte répond immédiatement avec un octet de statut sous le format xxx00101 (mask 0x1F).
- 0x05 (00000101) : Data Accepted.
- 0x0B (00001011) : CRC Error.
- 0x0D (00001101) : Write Error.
Attente de Fin de Programmation (Busy Wait) :
- Après acceptation, la carte passe la ligne MISO à 0 (LOW) tant qu'elle écrit physiquement en Flash.
- Le driver boucle while(spi_transfer(0xFF) == 0x00) tant que la carte est occupée (Busy).
- Cette étape peut durer plusieurs millisecondes.
Documentation Technique - Analyse des Fonctions (Code Level)
Cette section analyse le comportement exact des fonctions implémentées dans spi.c et sd.c.
Module SPI (spi.c) - Fonctions du Driver
Le fichier spi.c gère la communication bas niveau. Voici le détail de chaque fonction.
void spi_init(void)
Rôle : Initialise le contrôleur SPI matériel au démarrage.
Étape 1 (Direction des Pins) : Configure MOSI, SCK, SS et CS_SD (PB7) en Sortie.
Critique : Force SS (PB0) à l'état HAUT. Si ce pin passe en entrée LOW, le mode Maître saute.
Étape 2 (Configuration Registre SPCR) :
Active le SPI (SPE).
Active le mode Maître (MSTR).
Définit la vitesse initiale à f_osc/128 (SPR1 | SPR0). C'est la vitesse "safe" (125 kHz) requise pour réveiller la carte SD.
Étape 3 (Nettoyage) : Désactive le bit SPI2X dans SPSR pour être sûr de ne pas être en double vitesse.
void spi_set_speed(uint8_t speed_mode)
Rôle : Change la vitesse du bus à la volée.
Argument : speed_mode (SPI_SPEED_SLOW ou SPI_SPEED_FAST).
Logique :
Nettoie d'abord les bits de prescaler (SPR1, SPR0) et double vitesse (SPI2X).
Si FAST : Active SPI2X (Diviseur /2). Résultat : 8 MHz (rapide pour transferts).
Si SLOW : Active SPR1 | SPR0 (Diviseur /128). Résultat : 125 kHz (pour init).
uint8_t spi_transfer(uint8_t data)
Rôle : Envoie un octet et reçoit un octet simultanément (Full Duplex).
Argument : data (l'octet à envoyer). Pour juste lire, on envoie souvent 0xFF (Dummy).
Implémentation :
Écrit data dans SPDR (Lance l'horloge).
Boucle bloquante : Attend que le bit SPIF (Interrupt Flag) passe à 1 dans SPSR.
Retourne la valeur lue dans SPDR.
void spi_select_device(uint8_t id)
Rôle : Active un périphérique spécifique en gérant la sécurité électrique.
Argument : id (Identifiant du périphérique, ex: DEV_ID_SD).
Algorithme :
Appelle spi_deselect_all() : Passe TOUS les CS à l'état HAUT (1).
_delay_us(10) : Attend que la ligne MISO se libère (haute impédance).
switch(id) : Passe le CS ciblé à l'état BAS (0).
_delay_us(10) : Attend que l'esclave soit prêt à recevoir l'horloge.
Module SD (sd.c) - Fonctions Carte Mémoire
Ce module implémente le protocole SD SPI complet.
uint8_t sd_init(void)
Rôle : Réveille et configure la carte SD (SDSC ou SDHC).
Retourne : 0 si succès, 1 si erreur.
Séquence Exécutée :
spi_init_slow() : Force le bus à 125 kHz (interne statique).
Power Up : Envoie 10 octets 0xFF avec CS HAUT pour lancer l'horloge interne de la carte.
CMD0 (Reset) : Met la carte en mode Idle. Si réponse != 0x01, échec.
CMD8 (Voltage) : Vérifie si la carte accepte 3.3V. Argument 0x1AA.
ACMD41 (Init) : Boucle CMD55 + CMD41 jusqu'à ce que la réponse soit 0x00 (Carte prête).
CMD58 (OCR) : Lit le registre OCR. Vérifie le bit 30 pour savoir si c'est une carte haute capacité (is_high_capacity = 1).
spi_init_fast() : Passe le bus à 8 MHz pour les transferts futurs.
uint8_t sd_read_block(uint32_t sector, uint8_t *buffer)
Rôle : Lit 512 octets depuis un secteur donné.
Arguments :
sector : Numéro du secteur (0 à N).
buffer : Pointeur vers un tableau d'au moins 512 octets.
Logique Détaillée :
Calcul Adresse :
Si is_high_capacity (SDHC) : addr = sector.
Si Standard (SDSC) : addr = sector << 9 (multiplié par 512).
CS_LOW() : Sélectionne la carte.
Envoi CMD17 : Demande de lecture. Si réponse != 0x00, erreur.
Attente Token (Boucle Critique) :
Envoie 0xFF en boucle tant que la réponse est 0xFF.
Attend le token 0xFE (Start Block).
Timeout : Si > 40,000 itérations sans 0xFE, retourne erreur 2.
Lecture Données : Boucle for de 0 à 511 pour remplir buffer[].
CRC : Lit (et ignore) 2 octets de CRC.
CS_HIGH() : Libère la carte.
uint8_t sd_write_block(uint32_t sector, const uint8_t *buffer)
Rôle : Écrit 512 octets dans un secteur.
Arguments : sector cible, buffer source.
Logique Détaillée :
Calcul Adresse : Idem lecture (shift si SDSC).
CS_LOW() + Envoi CMD24 (Write Block).
Envoi Token Start : Envoie d'abord un 0xFF (dummy) puis 0xFE (Start Data Token).
Envoi Données : Boucle for 0 à 511 pour envoyer le contenu de buffer.
CRC Factice : Envoie deux fois 0xFF (CRC ignoré par la carte en mode SPI par défaut).
Vérification Data Response :
Lit un octet de réponse.
Applique le masque 0x1F. Si le résultat n'est pas 0x05 (Data Accepted), c'est une erreur d'écriture (retourne 3).
Attente Busy (Boucle) :
La carte maintient MISO à 0 tant qu'elle grave la Flash.
Boucle while(spi_transfer(0xFF) == 0x00) jusqu'à libération.
CS_HIGH().
static uint8_t sd_cmd(...) (Fonction Interne)
Rôle : Fonction helper (non visible dans .h) pour envoyer une commande brute.
Mécanique :
Envoie 0x40 | cmd (Index commande).
Découpe l'argument 32 bits en 4 octets (MSB d'abord).
Envoie le CRC (calculé ou pré-calculé comme 0x95 pour CMD0).
Attente Réponse (R1) :
Envoie 0xFF en boucle (max 10 essais) tant que le bit 7 de la réponse est 1.
Retourne l'octet de réponse (ex: 0x00 = Succès, 0x01 = Idle, autres = Erreurs).
Documentation Technique du Système de Fichiers FAT16
Introduction Générale au FAT16
Le système FAT16 (File Allocation Table 16-bit) est un système de fichiers historique développé par Microsoft. Sa simplicité le rend idéal pour les systèmes embarqués. Cette implémentation supporte cartes SD/SDHC avec partitionnement MBR optionnel.
Structures de Données Fondamentales
Boot Sector (Secteur d'Amorçage)
typedef struct __attribute__((packed)) {
uint8_t jump_boot[3]; // Code de saut vers le bootloader
char oem_name[8]; // Nom du formatage (ex: "MSDOS5.0")
uint16_t bytes_per_sector; // Octets par secteur (généralement 512)
uint8_t sectors_per_cluster; // Secteurs par cluster (1,2,4,8...)
uint16_t reserved_sectors; // Secteurs réservés (incluant le boot sector)
uint8_t num_fats; // Nombre de tables FAT (généralement 2)
uint16_t root_entries; // Nombre max d'entrées dans le répertoire racine
uint16_t total_sectors_16; // Nombre total de secteurs (si ≤ 65535)
uint8_t media_type; // Type de média (0xF8 pour disques fixes)
uint16_t sectors_per_fat; // Secteurs par table FAT
uint16_t sectors_per_track; // Géométrie disque (obsolète)
uint16_t num_heads; // Géométrie disque (obsolète)
uint32_t hidden_sectors; // Secteurs cachés (partition)
uint32_t total_sectors_32; // Nombre total de secteurs (si > 65535)
uint8_t drive_number; // Numéro de lecteur BIOS
uint8_t reserved1; // Réservé
uint8_t boot_signature; // Signature d'amorçage (0x29)
uint32_t volume_id; // ID unique du volume
char volume_label[11]; // Étiquette du volume
char file_system_type[8]; // Type de système de fichiers ("FAT16 ")
uint8_t boot_code[448]; // Code d'amorçage (non utilisé ici)
uint16_t boot_signature_end; // Signature de fin (0xAA55)
} fat16_boot_sector_t;
Attribut packed : Empêche le compilateur d'ajouter du padding entre les champs. Essentiel car la structure doit correspondre exactement aux 512 octets du secteur.
Champs critiques pour FAT16 :
- sectors_per_cluster : Unité d'allocation de base
- reserved_sectors : Inclut le secteur de boot
- root_entries : Maximum 512 entrées dans la racine (16 secteurs × 32 entrées)
- sectors_per_fat : Taille de chaque table FAT
Directory Entry (Entrée de Répertoire)
typedef struct __attribute__((packed)) {
char filename[8]; // Nom (espace complément en fin)
char extension[3]; // Extension (espace complément)
uint8_t attributes; // Attributs du fichier
uint8_t reserved; // Réservé (NT)
uint8_t creation_time_tenths; // Dixièmes de seconde (0-199)
uint16_t creation_time; // Heure:5 bits, minutes:6 bits, secondes:5 bits
uint16_t creation_date; // Année:7 bits, mois:4 bits, jour:5 bits
uint16_t last_access_date; // Dernier accès (date uniquement)
uint16_t first_cluster_high; // Cluster haut (toujours 0 en FAT16)
uint16_t last_write_time; // Dernière modification (heure)
uint16_t last_write_date; // Dernière modification (date)
uint16_t first_cluster_low; // Premier cluster du fichier
uint32_t file_size; // Taille en octets
} fat16_dir_entry_t;
Format 8.3 : Nom de 8 caractères + extension de 3 caractères. Les espaces vides sont remplis avec des espaces (0x20).
Attributs (bits) : - 0x01 : Lecture seule - 0x02 : Caché - 0x04 : Système - 0x08 : Étiquette de volume - 0x10 : Sous-répertoire - 0x20 : Archive (utilisé pour les fichiers normaux) - 0x0F : Entrée LFN (Long File Name) - ignorée dans cette implémentation
Champs de date/heure : Format Microsoft spécifique. Non utilisé dans cette implémentation pour simplifier.
First cluster : Les clusters 0 et 1 sont réservés. Les clusters valides commencent à 2.
Variables Globales et Architecture
Buffer Partagé
static uint8_t shared_buffer[512];
Philosophie d'économie mémoire : Au lieu d'allouer des buffers locaux dans chaque fonction (sur la pile), un buffer global est réutilisé. Ceci économise la RAM limitée de l'AVR.
Problème de réentrance : Cette approche n'est pas thread-safe. Si plusieurs tâches/tâches utilisent le système de fichiers simultanément, les données seront corrompues. Dans ce système, seul un thread à la fois peut utiliser les fonctions FAT.
Variables d'État du Système de Fichiers
static uint32_t partition_lba = 0; // LBA de la partition (0 si pas de partition)
static uint32_t fat_start_sector = 0; // Secteur de début des tables FAT
static uint32_t root_dir_sector = 0; // Secteur du répertoire racine
static uint32_t data_sector = 0; // Secteur de début de la zone de données
static uint16_t sectors_per_cluster = 0;
static uint16_t sectors_per_fat = 0;
static uint8_t num_fats = 0;
static uint8_t is_mounted = 0;
Calcul des positions clés :
1. fat_start_sector = partition_lba + reserved_sectors
2. root_dir_sector = fat_start_sector + (num_fats × sectors_per_fat)
3. data_sector = root_dir_sector + (root_entries × 32 / 512)
Zone de données : Les clusters sont numérotés à partir de 2. Pour convertir un cluster en LBA :
LBA = data_sector + ((cluster - 2) × sectors_per_cluster)
Fonctions Utilitaires de Bas Niveau
Fonctions d'Accès Secteur
uint8_t read_sector(uint32_t sector) {
return sd_read_block(sector, shared_buffer);
}
uint8_t write_sector(uint32_t sector) {
return sd_write_block(sector, shared_buffer);
}
Wrapper simples : Abstraction au-dessus du driver SD. Toutes les lectures/écritures passent par shared_buffer.
Fonction d'Affichage de Nombre
void print_number(uint32_t n) {
if (n == 0) { uart_putchar('0'); return; }
char buf[12]; uint8_t i = 0;
while (n > 0) { buf[i++] = '0' + (n % 10); n /= 10; }
while (i > 0) { uart_putchar(buf[--i]); }
}
Conversion décimale manuelle : Alternative à printf qui est trop lourd pour l'AVR. Stocke les chiffres dans un buffer temporaire puis les affiche en ordre inverse.
Parsing de Nom de Fichier
void fat16_parse_filename(const char* input, char* out_name, char* out_ext) {
memset(out_name, ' ', 8); memset(out_ext, ' ', 3);
uint8_t i = 0, j = 0;
// Copie du nom (8 caractères max)
while (input[i] != '\0' && input[i] != '.' && j < 8) {
char c = input[i++];
if (c >= 'a' && c <= 'z') c -= 32; // Conversion en majuscules
out_name[j++] = c;
}
// Recherche de l'extension
while (input[i] != '\0' && input[i] != '.') i++;
if (input[i] == '.') {
i++; j = 0;
while (input[i] != '\0' && j < 3) {
char c = input[i++];
if (c >= 'a' && c <= 'z') c -= 32; // Conversion en majuscules
out_ext[j++] = c;
}
}
}
Normalisation FAT16 : 1. Majuscules obligatoires : FAT16 stocke en majuscules 2. Padding avec espaces : Nom = 8 caractères, extension = 3 caractères 3. Format 8.3 : Si le nom fait moins de 8 caractères, le reste est rempli d'espaces
Exemple : "test.txt" → Nom: "TEST ", Extension: "TXT"
Initialisation et Montage
Fonction fat16_init()
uint8_t fat16_init(void) {
if (is_mounted) return 1;
uart_println("[FAT] Init...");
// Lecture du secteur 0 (MBR ou Boot Sector)
if (!read_sector(0)) return 0;
// Détection de partition : vérifie si c'est un secteur de boot valide
if (shared_buffer[0] != 0xEB && shared_buffer[0] != 0xE9) {
// C'est un MBR, extraire la partition LBA
memcpy(&partition_lba, &shared_buffer[0x1C6], sizeof(uint32_t));
if (!read_sector(partition_lba)) return 0;
} else {
partition_lba = 0; // Pas de partition, boot sector directement
}
// Parsing du boot sector
fat16_boot_sector_t* bs = (fat16_boot_sector_t*)shared_buffer;
sectors_per_cluster = bs->sectors_per_cluster;
if (sectors_per_cluster == 0) return 0; // Invalide
// Calcul des positions
sectors_per_fat = bs->sectors_per_fat;
num_fats = bs->num_fats;
fat_start_sector = partition_lba + bs->reserved_sectors;
root_dir_sector = fat_start_sector + (num_fats * sectors_per_fat);
uint16_t root_dir_size = (bs->root_entries * 32) / 512;
data_sector = root_dir_sector + root_dir_size;
is_mounted = 1;
uart_println("OK");
return 1;
}
Détection automatique partition/MBR : - MBR : Premier octet ≠ 0xEB ou 0xE9 (instructions de saut) - Offset 0x1C6 : Dans cette implémentation, on suppose la partition active à cet offset (typiquement 0x1BE+8 pour la première partition) - Boot Sector direct : Si pas de MBR, on lit directement le boot sector
Validation minimale : Vérifie que sectors_per_cluster ≠ 0
Gestion de la File Allocation Table
Structure de la FAT
La FAT est un tableau de 16-bit entries. Chaque entrée correspond à un cluster et contient :
- 0x0000 : Cluster libre
- 0x0002-0xFFEF : Prochain cluster dans la chaîne
- 0xFFF0-0xFFF6 : Réservé
- 0xFFF7 : Cluster défectueux
- 0xFFF8-0xFFFF : Fin de chaîne (EOF)
Lecture d'une Entrée FAT
uint16_t get_fat_entry(uint16_t cluster) {
uint16_t sec = cluster / 256; // Chaque secteur contient 256 entrées (512/2)
uint16_t ent = cluster % 256; // Index dans le secteur
if (!read_sector(fat_start_sector + sec)) return 0xFFFF;
return ((uint16_t*)shared_buffer)[ent];
}
Calcul d'adressage :
- 512 octets par secteur ÷ 2 octets par entrée = 256 entrées par secteur
- cluster / 256 donne le secteur dans la FAT
- cluster % 256 donne l'index dans le secteur
Écriture d'une Entrée FAT
void set_fat_entry(uint16_t cluster, uint16_t value) {
uint16_t sec = cluster / 256;
uint16_t ent = cluster % 256;
if (read_sector(fat_start_sector + sec)) {
((uint16_t*)shared_buffer)[ent] = value;
write_sector(fat_start_sector + sec);
}
// Mise à jour de la seconde FAT (miroir)
if (num_fats > 1) write_sector(fat_start_sector + sectors_per_fat + sec);
}
Mirroring des FAT : FAT16 maintient généralement 2 copies identiques de la table. Toute modification doit être répercutée sur toutes les copies.
Non-atomicité : Si le système s'arrête entre les deux écritures, les FAT peuvent être incohérentes.
Recherche d'un Cluster Libre
uint16_t find_free_cluster(void) {
for (uint16_t i = 0; i < 4; i++) {
if (!read_sector(fat_start_sector + i)) return 0;
uint16_t* fat_table = (uint16_t*)shared_buffer;
for (uint16_t j = 0; j < 256; j++) {
if (fat_table[j] == 0x0000 && (i*256 + j) >= 2) return (i * 256) + j;
}
}
return 0;
}
Recherche limitée : Ne recherche que dans les 4 premiers secteurs de la FAT (1024 clusters). Cette limite est arbitraire et pourrait être étendue.
Clusters réservés : Les clusters 0 et 1 sont réservés, donc on vérifie (i*256 + j) >= 2.
Performance : Recherche séquentielle - O(n). Pour un système avec beaucoup de fichiers, une bitmap serait plus efficace.
Libération d'une Chaîne de Clusters
void free_cluster_chain(uint16_t cluster) {
while (cluster >= 2 && cluster < 0xFFF8) {
uint16_t next = get_fat_entry(cluster);
set_fat_entry(cluster, 0x0000); // Marque comme libre
cluster = next;
}
}
Parcours de la chaîne : Suit les liens dans la FAT jusqu'à trouver un marqueur EOF.
Condition de sortie : cluster < 0xFFF8 s'assure de ne pas traiter les marqueurs EOF comme des clusters valides.
Opérations sur les Fichiers
Liste des Fichiers (fat16_list_files)
void fat16_list_files(void) {
if (!is_mounted && !fat16_init()) return;
uart_println("\r\n=== FILES ===");
for (int i = 0; i < 32; i++) {
task_sleep(1); // Yield au scheduler
if (!read_sector(root_dir_sector + i)) break;
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
// Fin du répertoire
if (e->filename[0] == 0x00) return;
// Entrée supprimée ou invalide
if ((uint8_t)e->filename[0] == 0xE5 || (uint8_t)e->filename[0] == 0xFF) continue;
// Ignore les entrées spéciales (LFN, volume label)
if (e->attributes == 0x0F || (e->attributes & 0x08)) continue;
// Affichage du nom
uart_print(" ");
for (int k = 0; k < 8; k++)
if (e->filename[k] != ' ') uart_putchar(e->filename[k]);
// Affichage de l'extension (si présente)
if (e->extension[0] != ' ') {
uart_putchar('.');
for (int k = 0; k < 3; k++)
if (e->extension[k] != ' ') uart_putchar(e->extension[k]);
}
// Affichage de la taille
uart_print(" ");
print_number(e->file_size);
uart_println(" B");
}
}
}
Structure du répertoire racine :
- 32 secteurs maximum (selon
root_entriesdans le boot sector) - 16 entrées par secteur (512 ÷ 32)
- Entrées de 32 octets chacune
Marqueurs spéciaux :
- 0x00 : Fin du répertoire
- 0xE5 : Fichier supprimé (premier caractère du nom)
- 0xFF : Entrée jamais utilisée
Attributs ignorés :
- 0x0F : Long File Name (LFN) - extension Windows 95+
- 0x08 : Volume Label - étiquette du volume
Yield au scheduler : task_sleep(1) permet à d'autres tâches de s'exécuter pendant le listing, évitant de bloquer le système.
Création de Fichier (fat16_create_file)
void fat16_create_file(const char* name, const char* content) {
if (!is_mounted) fat16_init();
// 1. Trouver un cluster libre
uint16_t free_clus = find_free_cluster();
if (free_clus == 0) { uart_println("Err: Disk Full"); return; }
// 2. Trouver une entrée libre dans le répertoire
uint32_t dir_sec = 0; uint16_t ent_off = 0; uint8_t found = 0;
for (int i = 0; i < 32; i++) {
read_sector(root_dir_sector + i);
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00 || (uint8_t)e->filename[0] == 0xE5) {
dir_sec = root_dir_sector + i;
ent_off = j * 32;
found = 1;
i = 32; // Break outer loop
break;
}
}
}
if (!found) { uart_println("Err: Root Full"); return; }
// 3. Créer l'entrée de répertoire
read_sector(dir_sec);
fat16_dir_entry_t* ne = (fat16_dir_entry_t*)(shared_buffer + ent_off);
memset(ne, 0, 32);
fat16_parse_filename(name, ne->filename, ne->extension);
ne->attributes = 0x20; // Archive
ne->first_cluster_low = free_clus;
uint32_t sz = 0;
while(content[sz]) sz++;
ne->file_size = sz;
write_sector(dir_sec);
// 4. Écrire les données
uint32_t lba = data_sector + ((free_clus - 2) * sectors_per_cluster);
memset(shared_buffer, 0, 512);
for(uint32_t k = 0; k < sz; k++) shared_buffer[k] = content[k];
write_sector(lba);
// 5. Marquer le cluster comme EOF dans la FAT
set_fat_entry(free_clus, 0xFFFF);
uart_println("Created.");
}
Limitations : 1. Un seul cluster par fichier (max 512 × sectors_per_cluster octets) 2. Pas de fragmentation - les fichiers plus grands ne sont pas supportés 3. Le répertoire racine a une taille fixe
Format d'entrée : memset(ne, 0, 32) initialise tous les champs à 0, y compris les dates/heures.
Suppression de Fichier (fat16_delete_file)
void fat16_delete_file(const char* name) {
if (!is_mounted) fat16_init();
char tn[8], te[3];
fat16_parse_filename(name, tn, te);
uint8_t any_deleted = 0;
while (1) {
uint8_t found_pass = 0;
for (int i = 0; i < 32; i++) {
if (!read_sector(root_dir_sector + i)) continue;
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00) { i = 32; break; }
if ((uint8_t)e->filename[0] == 0xE5) continue;
if (memcmp(e->filename, tn, 8) == 0 && memcmp(e->extension, te, 3) == 0) {
// Marquer comme supprimé
e->filename[0] = 0xE5;
uint16_t cluster = e->first_cluster_low;
if (write_sector(root_dir_sector + i)) {
uart_println("Entry Removed.");
// Libérer la chaîne de clusters
if (cluster != 0) free_cluster_chain(cluster);
found_pass = 1;
any_deleted = 1;
} else {
uart_println("Err: Write Fail");
}
break;
}
}
if (found_pass) break;
}
if (!found_pass) break; // Plus de fichiers à ce nom
}
if (!any_deleted) uart_println("Err: Not Found");
}
Suppression logique : Seul le premier caractère du nom est changé en 0xE5. Le reste de l'entrée reste intact jusqu'à écrasement.
Support des noms en double : La boucle while (1) supporte plusieurs fichiers avec le même nom (bien que non standard).
Renommage de Fichier (fat16_rename_file)
void fat16_rename_file(const char* old_name, const char* new_name) {
if (!is_mounted) fat16_init();
char on[8], oe[3];
char nn[8], ne[3];
fat16_parse_filename(old_name, on, oe);
fat16_parse_filename(new_name, nn, ne);
for (int i = 0; i < 32; i++) {
if (!read_sector(root_dir_sector + i)) continue;
uint8_t dirty = 0;
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00) { i = 32; break; }
if ((uint8_t)e->filename[0] == 0xE5) continue;
if (memcmp(e->filename, on, 8) == 0 && memcmp(e->extension, oe, 3) == 0) {
memcpy(e->filename, nn, 8);
memcpy(e->extension, ne, 3);
dirty = 1;
}
}
if (dirty) {
if (write_sector(root_dir_sector + i)) uart_println("Renamed.");
else uart_println("Err: Write Fail");
return;
}
}
uart_println("Err: Not Found");
}
Modification en mémoire : Le dirty flag évite d'écrire le secteur s'il n'y a pas eu de modification.
Copie de Fichier (fat16_copy_file)
void fat16_copy_file(const char* src, const char* dst) {
if (!is_mounted) fat16_init();
// 1. Recherche du fichier source
char sn[8], se[3];
fat16_parse_filename(src, sn, se);
uint16_t src_clus = 0;
uint32_t size = 0;
for (int i = 0; i < 32; i++) {
read_sector(root_dir_sector + i);
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00) { i = 32; break; }
if ((uint8_t)e->filename[0] == 0xE5) continue;
if (memcmp(e->filename, sn, 8) == 0 && memcmp(e->extension, se, 3) == 0) {
src_clus = e->first_cluster_low;
size = e->file_size;
i = 32;
break;
}
}
}
if (size == 0) { uart_println("Err: Src Not Found"); return; }
// 2. Création du fichier destination (vide)
fat16_create_file(dst, "");
// 3. Mise à jour de la taille dans l'entrée destination
char dn[8], de[3];
fat16_parse_filename(dst, dn, de);
uint16_t dst_clus = 0;
for (int i = 0; i < 32; i++) {
read_sector(root_dir_sector + i);
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00) { i = 32; break; }
if ((uint8_t)e->filename[0] == 0xE5) continue;
if (memcmp(e->filename, dn, 8) == 0 && memcmp(e->extension, de, 3) == 0) {
dst_clus = e->first_cluster_low;
e->file_size = size; // Mise à jour de la taille
write_sector(root_dir_sector + i);
i = 32;
break;
}
}
}
// 4. Copie des données
uint32_t slba = data_sector + ((src_clus - 2) * sectors_per_cluster);
uint32_t dlba = data_sector + ((dst_clus - 2) * sectors_per_cluster);
read_sector(slba); // Charge les données source dans shared_buffer
write_sector(dlba); // Écrit shared_buffer vers la destination
uart_println("Copied.");
}
Approche en deux étapes :
1. Crée un fichier vide avec fat16_create_file()
2. Met à jour manuellement la taille et copie les données
Limitation : Ne copie qu'un seul cluster (pas de chaînage).
Lecture de Fichier (fat16_read_and_print_file)
void fat16_read_and_print_file(const char* filename) {
if (!is_mounted) fat16_init();
// Recherche du fichier
char tn[8], te[3];
fat16_parse_filename(filename, tn, te);
uint16_t clus = 0;
uint32_t sz = 0;
for (int i = 0; i < 32; i++) {
read_sector(root_dir_sector + i);
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00) { i = 32; break; }
if ((uint8_t)e->filename[0] == 0xE5) continue;
if (memcmp(e->filename, tn, 8) == 0 && memcmp(e->extension, te, 3) == 0) {
clus = e->first_cluster_low;
sz = e->file_size;
i = 32;
break;
}
}
}
if (sz == 0) { uart_println("Err: Not Found"); return; }
// Lecture et affichage
uart_println("\r\n--- START ---");
uint32_t lba = data_sector + ((clus - 2) * sectors_per_cluster);
if (read_sector(lba)) {
for (uint16_t k = 0; k < 512 && sz > 0; k++) {
if (shared_buffer[k] == '\n') uart_print("\r\n");
else uart_putchar(shared_buffer[k]);
sz--;
}
}
uart_println("\r\n--- END ---");
}
Conversion newline : Convertit \n en \r\n pour l'affichage sur terminal série.
Lecture vers Buffer (fat16_read_to_buffer)
uint8_t fat16_read_to_buffer(const char* filename, char* out_buf, uint16_t max_len) {
if (!is_mounted) fat16_init();
// Recherche du fichier (identique à read_and_print_file)
char tn[8], te[3];
fat16_parse_filename(filename, tn, te);
uint16_t clus = 0;
uint32_t sz = 0;
for (int i = 0; i < 32; i++) {
read_sector(root_dir_sector + i);
for (int j = 0; j < 16; j++) {
fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
if (e->filename[0] == 0x00) { i = 32; break; }
if ((uint8_t)e->filename[0] == 0xE5) continue;
if (memcmp(e->filename, tn, 8) == 0 && memcmp(e->extension, te, 3) == 0) {
clus = e->first_cluster_low;
sz = e->file_size;
i = 32;
break;
}
}
}
if (sz == 0) return 0;
// Lecture des données
uint32_t lba = data_sector + ((clus - 2) * sectors_per_cluster);
if (!read_sector(lba)) return 0;
// Copie avec limite de taille
uint16_t cpl = (sz < max_len) ? sz : (max_len - 1);
for(uint16_t k = 0; k < cpl; k++) out_buf[k] = shared_buffer[k];
out_buf[cpl] = '\0'; // Null-terminator
return 1;
}
Système d'Exploitation Temps Réel pour AT90USB1286
Introduction
Le système d'exploitation temps réel pour microcontrôleurs présente deux versions distinctes d'API de gestion des tâches. Cette analyse détaille les différences fondamentales entre l'approche dynamique (Version 1) et l'approche statique (Version 2).
Version 1 : API Dynamique
Caractéristiques Principales
- Gestion dynamique des slots : Cette version introduit le concept de slot réutilisable (`TASK_FREE`) permettant une meilleure gestion de la mémoire sur le long terme.
- Création automatique de tâches : La fonction `task_exec()` abstrait la complexité de l'allocation. Elle recherche automatiquement un slot libre et calcule l'adresse de pile.
int8_t task_exec(void (*function)(void*), const char* nom, void* arg) {
// Recherche d'un slot TASK_FREE ou TASK_COMPLETED
// Calcul automatique : 0x0800 - (Index * Taille)
// Initialisation complète du TCB
}
- Gestion de mémoire prédéfinie : Les piles sont allouées dans une région mémoire fixe avec une formule de calcul automatique, évitant à l'utilisateur de gérer des pointeurs manuels.
Fonctionnalités Avancées
- `task_exit()` libère le slot pour réutilisation (passage à `TASK_FREE`).
- Tâche IDLE avec pile statique dédiée (64 octets).
- Pointeur de pile de type `uint8_t*` pour une cohérence de typage avec les registres AVR.
Version 2 : API Statique
Structure des États des Tâches
Caractéristiques Principales
- Gestion manuelle de la mémoire : L'utilisateur doit fournir explicitement le buffer de pile lors de la création.
int8_t task_create(const char* name_pgm, void (*function)(void*), void* arg,
uint8_t priority, uint8_t* stack_buffer, uint16_t stack_size);
- Allocation séquentielle : Les tâches sont créées dans l'ordre d'appel (ID 0, 1, 2...), sans mécanisme natif de réutilisation des slots libérés.
- Pile IDLE réduite : Optimisation de l'espace mémoire avec une pile IDLE de seulement 32 octets (contre 64 dans la V1).
Tableau Comparatif des Fonctions
| Fonctionnalité | Version 1 (Dynamique) | Version 2 (Statique) |
|---|---|---|
| Création de tâche | `task_exec()` (Automatique) | `task_create()` (Manuelle) |
| Terminaison | Libère le slot (`TASK_FREE`) | Marque comme terminé (`TASK_COMPLETED`) |
| Gestion mémoire | Calcul automatique interne | Allocation manuelle par l'utilisateur |
| Réutilisation slots | ✅ Oui | ❌ Non |
| Taille pile IDLE | 64 octets | 32 octets |
Différences Techniques Détaillées
2. Initialisation du Planificateur
Les deux versions initialisent différemment la table des tâches au démarrage du système.
Version 1 : Initialisation à `TASK_FREE` pour permettre l'allocation future.
for (uint8_t i = 0; i < MAX_TASKS; i++) {
task_table[i].function = NULL;
task_table[i].state = TASK_FREE;
}
Version 2 : Initialisation à `TASK_COMPLETED`, verrouillant implicitement les slots non utilisés si la logique de création ne gère pas cet état.
for (uint8_t i = 0; i < MAX_TASKS; i++) {
task_table[i].function = NULL;
task_table[i].state = TASK_COMPLETED;
}
La Version 1 offre une approche plus sophistiquée ("OS-like") avec gestion automatique de la mémoire et réutilisation des slots, idéale pour des applications généralistes. La Version 2 propose un modèle "Bare-metal" plus rigide, offrant un contrôle granulaire mais nécessitant une gestion manuelle rigoureuse et la correction du bug de typage identifié.
Documentation Technique Approfondie : FS & UART
Module 1 : Système de Fichiers (fs.c / fs.h)
Le système de fichiers (FS) est une couche d'abstraction située au-dessus du driver de carte SD. Il transforme un stockage physique linéaire (secteurs de 512 octets) en une structure organisée (fichiers nommés).
1.1 Architecture et Configuration
Le système repose sur une unité d'allocation logique appelée Bloc Virtuel (vblock).
- Définition
- FS_BLOCK_SIZE = 256 octets.
- Justification
- L'AT90USB1286 dispose de peu de RAM (8KB). Manipuler des tampons de 512 octets (taille native SD) est coûteux. Diviser par deux permet d'économiser de la RAM lors des opérations de bufferisation, tout en gardant une correspondance simple (1 secteur SD = 2 blocs FS).
Cartographie du Disque (Memory Map)
L'espace de stockage est segmenté statiquement via des #define dans fs.c :
| Zone | Blocs (Start) | Longueur (Count) | Capacité | Description Technique |
|---|---|---|---|---|
| BITMAP | 0 | 16 | 4 Ko | Table d'Allocation. Chaque bit représente l'état d'un bloc de données.
16 blocs * 256 octets * 8 bits = 32 768 blocs adressables. Espace max géré : 32768 * 256o = 8 Mo. |
| ROOT DIR | 16 | 16 | 4 Ko | Répertoire Racine. Stocke les métadonnées des fichiers.
16 blocs * 256 octets = 4096 octets. Entrée de 64 octets -> 4096 / 64 = 64 fichiers maximum. |
| DATA | 32 | N | Variable | Zone de Données. Stockage effectif du contenu des fichiers. Commence au bloc 32. |
1.2 Structure des Données (fs_entry_t)
Chaque fichier est décrit par une structure rigide de 64 octets.
typedef struct {
char name[16]; // 0x00: Nom ASCII (terminé par \0). Si name[0]==0, l'entrée est libre.
uint16_t size; // 0x10: Taille exacte du fichier en octets.
uint16_t blocks[16]; // 0x12: Liste des ID de blocs alloués.
// Ex: [32, 45, 33, 0, 0...] (Allocation non contiguë possible)
uint8_t padding[14]; // 0x32: Bourrage pour aligner la structure sur 64 octets.
// Permet d'avoir exactement 4 entrées par bloc de 256 octets.
} fs_entry_t;
Analyse des Limitations :
- La table blocks[16] limite la taille maximale d'un fichier à 16 blocs * 256 octets = 4096 octets (4 Ko).
- Pour supporter des fichiers plus gros, il faudrait implémenter un système d'indirection (FAT ou i-node), ce qui complexifierait le code.
1.3 Mécanismes Bas-Niveau (Drivers Internes)
Ces fonctions static ne sont pas visibles depuis l'extérieur (fs.h) et gèrent l'interface avec la carte SD.
fs_read_vblock(uint16_t vblock, uint8_t* out)
Traduit une adresse logique (256o) en adresse physique (512o).
- Calcul du Secteur SD : phys_sector = vblock / 2.
- Exemple : Les blocs virtuels 10 et 11 sont tous deux dans le secteur physique 5.
- Lecture Physique : sd_read_block(phys_sector, temp_buf).
- Tout le secteur (512o) est chargé dans le tampon statique temp_buf.
- Extraction :
- Si vblock est pair (vblock % 2 == 0), on copie temp_buf[0..255].
- Si vblock est impair (vblock % 2 == 1), on copie temp_buf[256..511].
fs_write_vblock(uint16_t vblock, uint8_t* in) (CRITIQUE)
Implémente le cycle Read-Modify-Write (RMW) indispensable pour écrire des sous-unités de secteur.
- Lecture (Read) : Charge le secteur physique contenant le bloc cible dans temp_buf.
- Risque : Si la lecture échoue, on ne peut pas écrire (risque de corruption).
- Modification (Modify) : Écrase la moitié du tampon (memcpy) avec les nouvelles données in.
- L'autre moitié du tampon (appartenant potentiellement à un autre fichier) reste intacte.
- Écriture (Write) : Réécrit le secteur complet sur la carte SD.
Note de Sécurité : Cette fonction utilise un buffer statique global temp_buf. Elle n'est pas réentrante. Si une interruption interrompt cette fonction et tente une opération FS, les données seront corrompues.
1.4 Analyse Détaillée du Bitmap (Logique d'Allocation)
Le système utilise un Bitmap (carte de bits) stocké dans les blocs 0 à 15 pour gérer l'occupation de chaque bloc du système. C'est le cœur de la gestion d'espace.
Structure Mathématique
- Total Blocs Bitmap : 16.
- Taille par Bloc : 256 octets (2048 bits).
- Total Bits : 16 * 2048 = 32 768 bits.
- Mapping : Le bit N correspond à l'état du bloc N du système de fichiers (0 = Libre, 1 = Occupé).
Algorithme de Recherche (fs_alloc_block)
L'algorithme utilise une stratégie First-Fit (Premier trouvé) avec une recherche hiérarchique :
- Boucle Niveau 1 (Pages) : Itère sur les 16 blocs de Bitmap. Charge un bloc entier en mémoire.
- Boucle Niveau 2 (Octets) : Scanne les 256 octets du bloc chargé.
- Optimisation : Si un octet vaut `0xFF`, il est plein. On saute directement au suivant sans inspecter les bits.
- Boucle Niveau 3 (Bits) : Si un octet n'est pas `0xFF`, on inspecte les 8 bits.
- On cherche le premier bit à 0 via un masque `(1 << bit)`.
- Calcul d'Index Absolu :
- `ID = (Page * 2048) + (Octet * 8) + Bit`
Persistance Immédiate
Dès qu'un bit libre est trouvé :
- Il est mis à 1 en RAM.
- Le bloc Bitmap complet est immédiatement réécrit sur la carte SD via `fs_write_vblock`.
- Cela garantit que l'allocation est sauvegardée avant même que les données du fichier ne soient écrites.
1.5 Logique Interne des Fichiers
Cette section détaille comment le FS manipule les fichiers sans table d'allocation globale (type FAT).
Indexation Directe (Scatter-Gather)
Au lieu d'une liste chaînée sur le disque (où le bloc A pointe vers le bloc B), le FS utilise un Index Direct stocké dans l'entrée du fichier (`blocks[16]`).
- Avantage (Accès Aléatoire) : Pour lire le 3ème bloc d'un fichier, le système lit directement `entry->blocks[2]`. Complexité O(1).
- Inconvénient (Fragmentation) : Les blocs d'un fichier peuvent être éparpillés n'importe où sur le disque (ex: blocs 32, 500, 33). Le FS ne défragmente pas.
- Limitation (Taille Fixe) : La taille du tableau `blocks` est fixe (16). C'est ce qui limite la taille maximale du fichier, et non la capacité de la carte SD.
Gestion de la Fin de Fichier (Append Logic)
Lors de l'ajout de données (`fs_append`) :
- Détection du Remplissage : Le système calcule `offset = size % 256`.
- Si `offset > 0`, le dernier bloc alloué n'est pas plein.
- Fusion (Merge) :
- Le dernier bloc est lu en mémoire.
- Les nouvelles données sont copiées à la suite des anciennes.
- Le bloc est réécrit.
- Extension :
- Si le fichier a besoin de plus d'espace, de nouveaux blocs sont alloués via le Bitmap et ajoutés à la liste `blocks[]`.
1.6 Fonctions API (Détails d'Implémentation)
fs_format
Initialise un disque vierge.
- Remplit temp_buf de zéros.
- Écrit des zéros dans tous les blocs Bitmap (0-15) et Répertoire (16-31).
- Effet : Tous les blocs sont marqués "Libres", aucun fichier n'existe.
fs_create(char* name)
Crée une coquille vide.
- Vérifie si le fichier existe déjà (erreur si oui).
- Cherche une entrée libre dans le répertoire (là où name[0] == 0).
- Initialise l'entrée : size = 0, blocks[] = {0}.
- Sauvegarde le bloc répertoire.
fs_append(name, data, len)
Ajoute des données à la fin d'un fichier existant. C'est la fonction la plus complexe du module.
Algorithme Détaillé :
- Contexte : Récupère l'entrée du fichier. Si size est 4096, erreur (Disque Plein/Fichier Plein).
- Remplissage du dernier bloc (Partial Fill) :
- Calcul : offset = size % 256.
- Si offset > 0 (le dernier bloc n'est pas plein) :
- Récupère l'ID du dernier bloc : blk_id = entry->blocks[(size-1)/256].
- Lit ce bloc en mémoire.
- Calcule l'espace libre : space = 256 - offset.
- Copie min(len, space) octets à la suite des données existantes.
- Sauvegarde le bloc.
- Met à jour size et décrémente len.
- Allocation de nouveaux blocs :
- Tant qu'il reste des données (len > 0) :
- Vérifie qu'on a pas atteint 16 blocs.
- new_blk = fs_alloc_block(). Si 0 (erreur), arrêt.
- Ajoute new_blk au tableau entry->blocks[].
- Copie les 256 prochains octets de données dans un buffer.
- Écrit ce buffer dans le nouveau bloc (fs_write_vblock).
- Met à jour size.
- Tant qu'il reste des données (len > 0) :
- Finalisation :
- Réécrit l'entrée mise à jour (nouvelle taille, nouveaux blocs) dans le secteur répertoire sur la SD.
fs_read(name, buffer, size, offset)
Lecture aléatoire (Random Access).
- Bornage : Si offset > file_size, retourne 0. Ajuste read_count pour ne pas dépasser la fin du fichier.
- Boucle de lecture :
- Calcule l'index du bloc logique : blk_idx = (offset + i) / 256.
- Calcule l'offset dans ce bloc : blk_off = (offset + i) % 256.
- Récupère l'ID physique via entry->blocks[blk_idx].
- Lit le bloc.
- Copie la portion nécessaire dans le buffer utilisateur.
- Avance au bloc suivant si nécessaire.
fs_remove(name)
Suppression propre pour éviter les fuites mémoire (blocs orphelins).
- Trouve l'entrée fichier.
- Libération Bitmap : Pour chaque ID non nul dans entry->blocks[], appelle fs_set_bitmap(id, 0).
- Nettoyage Répertoire : memset l'entrée à 0. Cela libère le slot pour un futur fichier.
- Commit : Sauvegarde le bloc répertoire.
Module 2 : UART (uart.c / uart.h)
Le module UART (Universal Asynchronous Receiver-Transmitter) gère la console série. Sur l'AT90USB1286, il utilise le périphérique matériel USART1.
2.1 Théorie et Configuration
L'UART est configuré pour un format 8N1 (8 bits de données, No Parity, 1 Stop bit) à 38400 bauds.
Calcul du Baud Rate
La précision du timing est cruciale pour la communication série asynchrone.
#define BAUD_PRESCALE (((F_CPU / (UART_BAUDRATE * 16UL))) - 1)
- F_CPU = 16 000 000 Hz.
- UART_BAUDRATE = 38 400 Hz.
- Calcul : (16000000 / (38400 * 16)) - 1 = (16000000 / 614400) - 1 = 26.04 - 1 = 25.
- La valeur entière 25 est chargée dans les registres UBRR. L'erreur est minime (< 0.2%).
Analyse des Registres d'Initialisation (uart_init)
- UBRR1H & UBRR1L (USART Baud Rate Register)
- Reçoivent la valeur 25 (sur 16 bits). Définit la vitesse de transmission.
- UCSR1B (Control and Status Register B)
- TXEN1 (Bit 3) : Transmitter Enable. Active le driver de la pin TX (PD3). Sans cela, la pin reste une I/O normale.
- RXEN1 (Bit 4) : Receiver Enable. Active le driver de la pin RX (PD2).
- UCSR1C (Control and Status Register C)
- UCSZ11 (Bit 2) & UCSZ10 (Bit 1) : Définissent la taille des caractères.
- Configuration 011 (avec UCSZ12=0) = 8 bits.
- Les bits de parité (UPM11:0) sont à 0 par défaut (Disabled).
- Le bit de stop (USBS1) est à 0 par défaut (1 bit).
2.2 Émission de Données (Polling)
L'émission est bloquante. Le CPU attend que le hardware soit prêt.
uart_putchar(char c)
void uart_putchar(char c) {
// Boucle d'attente active (Busy Wait)
// UDRE1 (USART Data Register Empty) : Ce drapeau passe à 1 quand
// le buffer d'émission est vide et prêt à recevoir une nouvelle donnée.
while (!(UCSR1A & (1 << UDRE1)));
// L'écriture dans UDR1 déclenche automatiquement :
// 1. Le transfert vers le Shift Register.
// 2. La génération du Start Bit.
// 3. Le décalage des 8 bits de données.
// 4. La génération du Stop Bit.
UDR1 = c;
}
uart_print(char* str)
Fonction utilitaire simple.
- Itère via un pointeur *str tant que le caractère n'est pas \0 (NULL terminator).
- Appelle uart_putchar pour chaque caractère.
2.3 Gestion Avancée de la Mémoire (Flash vs RAM)
Les microcontrôleurs AVR utilisent une architecture Harvard. Les données en mémoire programme (Flash) ne sont pas dans le même espace d'adressage que les variables (RAM).
- Problème
- Une chaîne littérale uart_print("Hello") est copiée en RAM au démarrage (.data section), consommant la précieuse mémoire vive (seulement 8KB dispo).
- Solution
- Utiliser la macro PSTR("Hello") pour garder la chaîne en Flash, et utiliser des fonctions spéciales pour la lire.
uart_print_P(const char* str)
Cette fonction est conçue pour lire des pointeurs vers la Flash space.
void uart_print_P(const char* str) {
char c;
// pgm_read_byte : Instruction assembleur LPM (Load Program Memory)
// Lit un octet à l'adresse 'str' dans la Flash, pas la RAM.
while ((c = pgm_read_byte(str++))) {
uart_putchar(c);
}
}
- Macro uart_println_P
- Combine l'affichage de la chaîne Flash et l'ajout automatique de \r\n (CRLF) pour le retour à la ligne.
2.4 Réception de Données
Bien que moins utilisée dans ce code, la réception est configurée.
uart_available()
- Vérifie le bit RXC1 (Receive Complete) dans UCSR1A.
- Retourne 1 si des données non lues sont présentes dans le buffer FIFO de réception hardware.
uart_getchar()
- Lit le registre UDR1.
- Attention : Si cette fonction est appelée alors qu'aucune donnée n'est arrivée, le comportement est indéfini (retourne souvent le dernier octet reçu). Il faut toujours vérifier uart_available() avant.
Documentation Technique - Analyse Logique du Shell v1.6
Ce document propose une analyse algorithmique détaillée ("Code Walkthrough") du fichier `shell.c`. Il ne s'agit pas d'un manuel utilisateur, mais d'une explication du comportement interne destinée aux développeurs système.
1. Architecture de l'Abstraction Console (Console Abstraction Layer)
Le Shell v1.6 introduit une couche d'abstraction logicielle pour l'affichage. Au lieu d'utiliser directement `uart_putchar`, le shell utilise `console_putc`. Cette architecture permet la fonctionnalité de "Mirroring" (duplication d'écran).
1.1 Variable d'État Globale
static uint8_t active_screen_id = 0;
- Nature : Variable statique locale au fichier (portée limitée à `shell.c`).
- Sémantique :
`0` : Mode standard (UART uniquement). `> 0` : Mode miroir actif. La valeur stockée correspond à l'ID physique du Chip Select (CS) sur le bus SPI (ex: 1 pour CS1, 2 pour CS2, etc.).
1.2 Algorithme d'Émission (console_putc)
La fonction `console_putc` est la primitive atomique d'affichage. Son comportement est conditionnel.
Logique séquentielle :
- Émission Primaire (UART) :
- L'appel à `uart_putchar(c)` est inconditionnel. Le terminal série reçoit toujours les données. C'est une sécurité pour le débogage : même si l'écran SPI plante, le développeur voit la sortie sur le port série.
- Test de Condition (Miroir) :
- `if (active_screen_id > 0)` : Vérifie si un écran est attaché.
- Transaction SPI (Si actif) :
- Le code implémente une transaction SPI manuelle complète pour chaque caractère :
- `spi_select_device(active_screen_id)` : Active la ligne CS (mise à 0).
- `spi_transfer(c)` : Envoie l'octet via le registre `SPDR` et attend la fin de transmission (Polling du flag SPIF).
- `spi_deselect_device(active_screen_id)` : Relâche la ligne CS (mise à 1).
Impact sur la performance : Cette approche "caractère par caractère" est fiable mais lente. Pour afficher "HELLO" sur SPI, le code bascule le Chip Select 5 fois.
- Overhead : 5 sélections + 5 désélections + délais de garde (20µs par char) + temps de transmission UART.
1.3 Gestion de la Mémoire Programme (console_print_P)
void console_print_P(const char* str_progmem) {
char c;
while ((c = pgm_read_byte(str_progmem++))) {
console_putc(c);
}
}
- Problème résolu : Sur AVR, les chaînes constantes (`"Erreur"`) sont copiées en RAM au démarrage. La RAM étant limitée (4-8KB), stocker les textes de menu en RAM est un gaspillage.
- Solution : La macro `PSTR()` place les chaînes en Flash.
- Mécanisme : La fonction utilise l'instruction assembleur `LPM` (Load Program Memory) via `pgm_read_byte`. Elle itère tant que le caractère lu n'est pas le terminateur nul `\0`.
2. Mécanique de la Boucle Principale (shell_task_func)
Cette fonction est le point d'entrée de la tâche (Task Entry Point). Elle ne retourne jamais (`while(1)`).
2.1 Bufferisation des Commandes
static char cmd_buf[64];
static uint8_t cmd_idx = 0;
- Capacité : 64 octets. Cela inclut la commande, les arguments et le caractère nul final.
- Risque de dépassement : Le code actuel ne vérifie pas explicitement si `cmd_idx >= 64` avant d'incrémenter. Si l'utilisateur tape plus de 64 caractères sans appuyer sur Entrée, il y a un risque de Buffer Overflow qui corromprait la mémoire adjacente (probablement `active_screen_id` qui est déclarée juste après).
2.2 Traitement des Entrées (Input Handling)
Le shell fonctionne par interruption logicielle simulée (Polling).
Cas du Retour Chariot ('\r')
C'est le déclencheur de l'exécution.
- Terminaison de Chaîne : `cmd_buf[cmd_idx] = 0;` transforme le tableau de caractères en chaîne C valide (Null-terminated string).
- Saut de ligne : `console_println("")` pour que la réponse s'affiche sur la ligne suivante.
- Exécution : Appel bloquant à `shell_process_cmd()`.
- Réinitialisation :
- `cmd_idx = 0;` : Remet le curseur au début.
- Le contenu précédent de `cmd_buf` n'est pas effacé (memset), il sera simplement écrasé par la prochaine frappe.
- Prompt : Affiche `> ` pour inviter à la saisie suivante.
Cas du Backspace ('\b' ou 127)
Gère l'édition de ligne.
- Vérification : `if (cmd_idx > 0)`. On ne peut pas effacer si le buffer est vide (pour ne pas corrompre la mémoire avant le buffer).
- Logique Interne : `cmd_idx--`. On recule simplement l'index d'écriture.
- Logique Visuelle : `console_print("\b \b")`.
- Le premier `\b` recule le curseur du terminal.
- L'espace ` ` écrase le caractère affiché.
- Le second `\b` remet le curseur à la position reculée.
3. Analyse du Moteur de Parsing (shell_process_cmd)
C'est le cœur logique du shell. Il transforme une chaîne brute en appel de fonction.
3.1 Tokenisation Destructive (strtok)
Le code utilise `strtok` (String Tokenizer) de la `stdlib`.
char* cmd = strtok(cmd_buf, " ");
char* arg1 = strtok(NULL, " ");
char* arg2 = strtok(NULL, " ");
Comportement mémoire de `strtok` : Si `cmd_buf` contient : `{'W', 'R', 'I', 'T', 'E', ' ', 'A', '.', 'T', 'X', 'T', '\0'}`
Après le premier appel `strtok(cmd_buf, " ")` :
- `strtok` trouve le premier espace.
- Il le remplace par `\0`.
- Il retourne un pointeur vers le début (`&cmd_buf[0]`).
- `cmd_buf` devient : `{'W', 'R', 'I', 'T', 'E', '\0', 'A', '.', 'T', 'X', 'T', '\0'}`.
Les appels suivants `strtok(NULL, " ")` reprennent après le `\0` inséré précédemment.
Conséquence Critique : Cette méthode rend impossible la récupération de la ligne originale. Le buffer est "haché".
3.2 Normalisation (strupr)
strupr(cmd);
- Seul le premier token (la commande) est mis en majuscules.
- Raison : Permet à l'utilisateur de taper `write`, `WRITE` ou `Write`.
- Important : `arg1` et `arg2` ne sont pas normalisés. C'est crucial pour le système de fichiers qui est (souvent) sensible à la casse, ou du moins pour préserver le nommage utilisateur.
4. Analyse Détaillée des Commandes
4.1 Commandes Fichiers (File System)
Commande `TOUCH`
- Signature Shell : `TOUCH <filename>`
- Appel : `fs_create(arg1)`
- Logique de Retour :
Le shell vérifie implicitement le succès via la valeur de retour (implémentation manquante de message d'erreur spécifique dans le snippet, mais généralement 0 = succès). Si `arg1` est NULL (utilisateur a tapé juste `TOUCH`), la commande est ignorée silencieusement par le `if(arg1)`.
Commande `CAT` (Lecture et Affichage)
- Signature : `CAT <filename>`
- Mécanisme de Flux :
Le shell ne lit pas tout le fichier d'un coup (la RAM est trop petite). Il utilise une lecture par blocs (Chunking).
char buf[64]; // Tampon temporaire sur la pile
uint16_t offset = 0;
int16_t read;
while(1) {
// Lit 63 octets max pour laisser place au \0
read = fs_read(arg1, (uint8_t*)buf, 63, offset);
if (read <= 0) break; // Fin de fichier ou Erreur
buf[read] = 0; // Terminateur pour l'affichage
console_print(buf);
offset += read; // Avance le curseur de lecture
}
- Aspect Bloquant : Cette boucle monopolise le CPU. Tant que le fichier est affiché, le shell ne répond plus. Sur un gros fichier, cela peut durer plusieurs secondes (dépendant du Baudrate UART et de la vitesse SPI).
Commande `WRITE` (Écriture)
- Signature : `WRITE <file> <text>`
- Appel : `fs_append(arg1, (uint8_t*)arg2, strlen(arg2))`
- Analyse de la limitation `strtok` :
Commande : `WRITE log.txt Hello` -> `arg1`="log.txt", `arg2`="Hello". Fonctionne. Commande : `WRITE log.txt Hello World` -> `arg1`="log.txt", `arg2`="Hello". * Le mot "World" est perdu car `strtok` l'a séparé dans un 3ème token qui n'est pas récupéré par le code. * Correction possible : Il faudrait utiliser un pointeur manuel pour récupérer "le reste de la chaîne" après `arg1`, plutôt que `strtok`.
4.2 Commandes Noyau (Kernel)
Ces commandes font le pont entre l'utilisateur et le planificateur (`scheduler.c`).
Commande `PS` (Process Status)
- Appel : `shell_ps()`
- Fonctionnement interne :
Cette fonction (définie ailleurs) itère sur la `task_table` du noyau. Elle accède directement aux structures `TCB` (Task Control Block) pour lire : * L'ID (index). * Le Nom (`task->name`). * L'État (`task->state`). * La Priorité.
Commandes de Contrôle (`KILL`, `SUSPEND`, `RESUME`)
- Argument : Numérique (ID de tâche).
- Conversion : `atoi(arg1)`.
- Danger de `atoi` :
Si l'utilisateur tape `KILL TOTO`, `atoi("TOTO")` retourne `0`. L'ID 0 est souvent réservé à la tâche IDLE ou au Shell lui-même. Tuer la tâche IDLE peut entraîner un crash système (Reset Watchdog) ou un comportement indéfini du planificateur quand aucune autre tâche n'est prête. Tuer la tâche Shell (soi-même) arrêterait l'interface CLI.
4.3 Commande Matériel (`SCREEN`)
- Signature : `SCREEN <id>`
- Action : `active_screen_id = atoi(arg1);`
- Persistance :
La modification de cette variable affecte immédiatement tous les futurs appels à `console_putc`. Il n'y a pas de validation de l'ID. Si l'utilisateur tape `SCREEN 99` et que le Chip Select 99 n'existe pas physiquement : * `spi_select_device(99)` va probablement manipuler un port inexistant ou ne rien faire (selon l'implémentation de `spi.c`). * Le système perdra du temps CPU à essayer d'envoyer des données SPI dans le vide.
5. Robustesse et Gestion des Erreurs
Gestion des Arguments Manquants
Le code utilise un modèle défensif simple :
if (strcmp(cmd, "KILL") == 0) {
if(arg1) shell_kill(atoi(arg1));
}
- Le pointeur `arg1` est vérifié (`if(arg1)`). Si `strtok` a retourné `NULL` (pas d'argument tapé), la commande `shell_kill` n'est pas appelée.
- Feedback : Il n'y a pas de message d'erreur "Argument manquant". La commande ne fait simplement rien, ce qui peut être déroutant pour l'utilisateur.
Inconnues
La clause `else` finale gère les fautes de frappe :
else {
console_println_P(PSTR("Unknown Cmd"));
}
Ceci est essentiel pour l'expérience utilisateur.
6. Résumé des Flux de Données
- Entrée : UART RX (Interruption ou Polling) -> Registre UDR -> `uart_getchar()` -> `cmd_buf`.
- Traitement : `cmd_buf` -> `strtok` -> `cmd`, `arg1`, `arg2`.
- Décision : Comparaison de chaînes (`strcmp`).
- Sortie (FS) : `fs_read` -> Buffer temporaire -> `console_print` -> UART TX + SPI MOSI.
- Sortie (Debug) : `printf/sprintf` -> Buffer temporaire -> `console_print` -> UART TX + SPI MOSI.
Cette architecture simple mais modulaire permet au système d'être piloté aussi bien localement (PC connecté en USB/Série) que via un écran tactile et un clavier connectés sur le bus SPI, grâce à l'abstraction de la console.
FPGA
Controleur bus SPI
Cette partie consiste à réaliser une interface SPI en mode 1 sur une carte FPGA Basys 3, configurée en tant qu’esclave. La communication est gérée à l’aide d’une machine à états finis (FSM) en VHDL, permettant de synchroniser et traiter correctement les signaux du protocole SPI.
Le maître SPI est un microcontrôleur ATmega328p, configuré pour transmettre en mode 1. Les données envoyées au microcontrôleur via l’UART (à partir d’un terminal Minicom) sont ensuite transférées vers la Basys 3 par le bus SPI.
Pour la visualisation, chaque octet reçu par l’esclave FPGA est affiché directement sur 8 LED de la carte. Les LED se mettent automatiquement à jour à chaque réception d’un nouvel octet.
Fonctionnement global du bus SPI
Le protocole SPI repose sur 4 signaux :
- MOSI : Master Out Slave In
- MISO : Master In Slave Out
- SCK : Serial Clock
- SS : Slave Slect
Déroulement d’une communication SPI
- Le maître active l'esclave en mettant la ligne SS à l'état bas.
- L'horloge SCK se met à osciller et chaque front d'horloge permet la transmission d'un bit.
- Envoie des données :
- Le maître envoie un bit sur MOSI
- L'esclave repond sur MISO
- Une fois la communication terminée, le maître désactive l'esclave en mettant SS à l'état haut.
Modes SPI
SPI comporte 4 modes, définis par la polarité et la phase de l’horloge (CPOL et CPHA). Ces paramètres déterminent les instants où les données sont valides.
En mode 1, la configuration du bus SPI est la suivante :
- CPOL = 0 : horloge au niveau bas au repos
- CPHA = 1 :
- Setup sur le front montant
- Sample sur le front descendant
Programmation VHDL
La conception du bus SPI repose sur une machine a états finis (FSM) qui gère les différentes phases de la communication :
- Idle : Attente de l'activation de la ligne SS
- Load : Détecte des fronts montants d'horloge et valide l'octet reçu
- Read : Recopie l'état du MOSI dans le registre de data reçu
- Write : Recopie l'état du MOSI sur le MISO (Pour débuger via minicom)
- Wait_next : Réinitialise le compteur de bit à 0 et attend le prochain frond montant d'horloge ou la fin de la communication SS a l'état bas
Démonstration & Code
https://gitea.plil.fr/bcheklat/SE4-Pico-B2/src/branch/main/SPI_BUS
Contrôleur d'écran VGA
Le contrôleur d’écran VGA a pour objectif d’assurer l’affichage graphique des données reçues via le bus SPI sur un écran VGA connecté à la carte FPGA Basys 3.
Les caractères envoyés par le microcontrôleur (via UART puis SPI) sont interprétés, convertis en bitmaps et stockés dans une mémoire vidéo interne, dont le contenu est balayé en continu par le contrôleur VGA.
L’architecture permet l’affichage de texte en temps réel, avec gestion de l’écriture, de l’effacement et du nettoyage complet de l’écran.
Architecture globale
Le contrôleur VGA est composé des blocs suivants :
- Générateur d’horloge vidéo
- Contrôleur de timing VGA
- Mémoire vidéo (RAM)
- ROM de caractères
- Décodeur de commandes ASCII
- Interface SPI esclave
Génération de l’horloge vidéo
La carte Basys 3 fournit une horloge principale à 100 MHz.
Un bloc Clock Wizard est utilisé afin de générer une horloge adaptée au timing VGA (65Mhz).
Timing VGA et balayage de l’écran
Le contrôleur VGA repose sur deux compteurs :
- Compteur horizontal
x_pixel_counter - Compteur vertical
y_pixel_counter
Ces compteurs définissent la position courante du pixel à afficher.
Le timing est conforme à une résolution 1024 × 768 à 60 Hz, incluant :
- zone visible
- front porch
- impulsions de synchronisation
- back porch
Les signaux :
- HS (Horizontal Sync)
- VS (Vertical Sync)
sont générés directement à partir des valeurs des compteurs, conformément aux spécifications VGA.
Zone d’affichage utile
L’affichage du contenu graphique est centré à l’écran grâce à l’utilisation d’offsets
offset_x = 352offset_y = 272
La zone utile correspond à une surface de 320 × 224 pixels, utilisée pour l’affichage du texte.
En dehors de cette zone, les signaux RGB sont forcés à zéro afin d’obtenir un fond noir.
Mémoire vidéo (RAM)
La mémoire vidéo est implémentée sous forme d’une RAM interne de 8960 octets (40 × 224), chaque octet représentant 8 pixels horizontaux.
- Écriture : pilotée par le décodeur de commandes
- Lecture : réalisée en continu par le contrôleur VGA
L’adresse de lecture est calculée à partir de la position du pixel courant :
adresse = (y_pixel × 40) + (x_pixel / 8)
Chaque bit de l’octet lu correspond à l’état d’un pixel (1 = pixel allumé, 0 = pixel éteint).
ROM de caractères
La ROM contient les bitmaps des caractères ASCII sous forme de matrices 8 × 8 pixels.
Chaque caractère est défini par :
- un index ASCII
- un sous-index correspondant à la ligne du caractère (0 à 7)
Cette ROM permet de convertir les caractères reçus via SPI en données graphiques exploitables par la RAM vidéo.
Décodeur de commandes
Le décodeur reçoit les caractères ASCII transmis par le bus SPI et gère leur affichage à l’écran. Fonctionnalités prises en charge :
- écriture séquentielle des caractères
- retour à la ligne automatique
- suppression du dernier caractère (Backspace)
- effacement complet de l’écran (Delete)
Le décodeur traduit chaque caractère en :
- adresses mémoire
- indices de caractères
- signaux de contrôle d’écriture
Chaque caractère est converti en 8 lignes de pixels, stockées consécutivement en mémoire vidéo.
Génération des signaux RGB
Pour chaque pixel situé dans la zone visible :
- le bit correspondant est extrait de l’octet lu en mémoire
- ce bit est appliqué simultanément aux sorties R, G et B
L’affichage est donc réalisé en monochrome, avec des pixels blancs sur fond noir.
Synchronisation avec le bus SPI
Le contrôleur VGA fonctionne indépendamment du bus SPI.
Les données reçues via SPI sont traitées par le décodeur, qui met à jour la mémoire vidéo sans interrompre le balayage de l’écran.
Démonstration & Code
Démonstration contrôleur d’écran VGA
https://gitea.plil.fr/bcheklat/SE4-Pico-B2/src/branch/main/vga
Bitcoin Miner
Objectif du projet : Développer un système complet de minage Bitcoin sur carte FPGA Zybo Z7-10 en exploitant l'architecture (Processeur + FPGA).
Architecture du système
Partie Processeur (ARM Cortex-A9) :
- Implémentation d'un "Mining client" en C
- Connexion réseau aux serveurs de minage
- Récupération et décodage des "jobs" de minage
- Configuration des registres FPGA via le bus AXI
- Transmission des résultats valides au serveur
Partie FPGA :
- Accélérateur matériel SHA-256 optimisé
- Calcul massivement parallèle des nonces
- Interface de contrôle via registres mémoire-mappés
- Signalisation par interruption au processeur
Fonctionnement :
- Le processeur se connecte au "Mining pool server" et reçoit un bloc de travail
- Le processeur extrait l'en-tête de bloc (80 octets) et le prépare pour le FPGA
- Transfert des données vers le FPGA via le bus AXI et démarrage du calcul
- Le FPGA signale immédiatement toute solution valide (hash < cible)
- Le processeur soumet la solution au réseau Bitcoin
Première implémentation d'un coeur SHA256
dans un premier temps j'ai conçu et implémenté un accélérateur matériel SHA-256 en VHDL pour optimiser les calculs de hachage nécessaires au minage de Bitcoin.
Architecture du coeur SHA-256 :
- Signal
startpour lancer le calcul,donepour indiquer la fin, et bus de sortie 256 bits pour le hash. - Exécute les 64 tours de compression SHA-256 en 64 cycles d'horloge
- Utilise des opérations bit-à-bit parallèles pour maximiser la vitesse
Fonctionnement algorithmique :
- Charge les constantes initiales H₀ à H₇, K0{256} à K63{256} définies par la norme SHA-256.
- Génère les 64 mots W[t] à partir du bloc de message de 512 bits
- Applique les fonctions logiques SHA-256 (Ch, Maj, Σ₀, Σ₁) sur 64 tours
- Additionne les valeurs intermédiaires pour produire le hash final
Fonctions cryptographiques implémentées :
Ch(x, y, z) = (x ∧ y) ⊕ (¬x ∧ z)Maj(x, y, z) = (x ∧ y) ⊕ (x ∧ z) ⊕ (y ∧ z)Σ₀{256}(x) = ROTR2(x) ⊕ ROTR13(x) ⊕ ROTR22(x)Σ₁{256}(x)= ROTR6(x) ⊕ ROTR11(x) ⊕ ROTR25(x)σ₀{256}(x) = ROTR7(x) ⊕ ROTR18(x) ⊕ SHR3(x)σ₁{256}(x) = ROTR17(x) ⊕ ROTR19(x) ⊕ SHR10(x)
Simulation
On obtient bien le SHA-256 correspondant au message en 645 ns, ce qui correspond à 64 cycles d’horloge, en démarrant avec une période de 5 ns.
