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

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
 
(58 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 ==


[[Fichier:Schema carte mere.jpg|gauche|vignette|500x500px|Schéma carte mère du pico ordinateur]]
=== `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;


# GitHub Wiki — Dual-LED Binary Clock Firmware
    // Configuration Timer1 pour 100Hz (10ms)
    TCCR1A = 0;
    TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);
    OCR1A = (F_CPU / 1024 / TICK_FREQUENCY) - 1;
    TIMSK1 |= (1 << OCIE1A);


Below is a structured wiki layout with properly formatted Markdown files ready for upload to a GitHub Wiki.
    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>


## Home.md
''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


```markdown
== Configuration du Timer1 ==
# Dual-LED Binary Clock Firmware


## Project Overview
=== Objectif ===
Le Timer1 génère une interruption périodique à 100 Hz (toutes les 10 ms) servant de base temporelle au scheduler.


**Dual-LED Binary Clock Firmware** is a sophisticated real-time operating system (RTOS) implementation for ATmega328p microcontrollers, demonstrating advanced embedded systems concepts with minimal resource utilization.
=== 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>


## Quick Links
=== Détails des Registres ===
* ''Mode'' : CTC (Clear Timer on Compare Match)
* ''Prescaler'' : 1024
* ''Fréquence CPU'' : 16 MHz
* ''Valeur OCR1A'' : 155


- **[System Architecture](System-Architecture)** - High-level design and components
=== Calcul OCR1A ===
- **[Kernel Design](Kernel-Design)** - RTOS kernel implementation details 
<math>
- **[Scheduler](Scheduler)** - Task scheduling algorithms
\text{OCR1A} = \frac{F_{\text{CPU}}}{\text{prescaler} \times f_{\text{tick}}} - 1 = \frac{16,000,000}{1024 \times 100} - 1 = 156 - 1 = 155
- **[Memory Management](Memory-Management)** - Resource allocation and constraints
</math>
- **[API Reference](API-Reference)** - Complete function documentation
- **[Build System](Build-System)** - Compilation and deployment guide
- **[Performance Analysis](Performance-Analysis)** - Timing and resource metrics


## Key Features
== Logique de l'Ordonnanceur ==


✅ **Cooperative RTOS** with round-robin scheduling 
=== `get_next_ready_task()` ===
✅ **Memory-optimized** for ATmega328p (2KB RAM)
<syntaxhighlight lang="c">
✅ **Tick-based timing** with 100Hz resolution 
static uint8_t get_next_ready_task(void) {
✅ **Priority-based** task execution (4 levels)
    uint8_t start_index = (current_task_id + 1) % MAX_TASKS;
✅ **Sleep/wake** functionality for power management 
    uint8_t i = start_index;
✅ **Critical section** protection for shared resources 


## Repository Structure
    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>


firmware/
''Algorithme Round-Robin :''
├── src/
1. Commence à la tâche suivant la courante
│  ├── kernel/
2. Parcourt circulairement toutes les tâches
│  │  ├── kernel.c/h      # Core kernel management
3. Retourne la première tâche READY trouvée
│  │  ├── scheduler.c/h    # Task scheduler
4. Si aucune, retourne la tâche idle
│  │  ├── task.c/h        # Task control blocks
│  │  └── config.h        # System configuration
│  └── main.c              # Application example
├── Makefile
└── README.md


```
=== `scheduler_update_logic()` ===
<syntaxhighlight lang="c">
void scheduler_update_logic(void) {
    system_ticks++;


## Getting Started
    // 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;
            }
        }
    }


1. **Prerequisites**: `avr-gcc`, `avrdude`, `make`
    // 2. Sélectionner la prochaine tâche
2. **Clone**: `git clone <repository-url>`
    uint8_t next_task = get_next_ready_task();
3. **Build**: `make`
4. **Flash**: `make flash`
```


---
    // 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>


## System-Architecture.md
''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


```markdown
== ISR Timer1 ==
# System Architecture


## Component Hierarchy
=== Prototype ===
<syntaxhighlight lang="c">
ISR(TIMER1_COMPA_vect, ISR_NAKED)
</syntaxhighlight>


The firmware is organized into a modular RTOS architecture:
=== 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>


// Core RTOS Components
=== Attribut ISR_NAKED ===
kernel/          // Real-time kernel foundation
* Empêche le compilateur de générer prologue/epilogue
├── kernel.c/h    // System initialization and management
* Nécessaire pour un contrôle total sur la pile
├── scheduler.c/h // Task scheduling algorithms
* Le développeur doit gérer manuellement la sauvegarde/restauration
├── task.c/h      // Task control block implementation
└── config.h      // Resource constraints and tuning


// Application Layer
=== Temps d'Exécution ISR ===
main.c          // User tasks and application logic
<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 ==


## Data Flow
=== 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>


Hardware Interrupts → Scheduler Tick → Task State Update → Task Execution
=== Code ===
↑                    ↓                    ↓              ↓
<syntaxhighlight lang="c">
Timer ISR         Update sleepers      Ready → Running  User code
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


## Key Design Patterns
=== 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>


**Static Allocation Pattern**
== Fonctions Auxiliaires ==
- All memory allocated at compile time
- No dynamic memory management
- Fixed-size arrays for predictable behavior


**Cooperative Multitasking**
=== `get_current_task_id()` ===
- Tasks voluntarily yield control
<syntaxhighlight lang="c">
- No preemptive context switching
uint8_t get_current_task_id(void) {
- Simplified synchronization
    return current_task_id;
}
</syntaxhighlight>


**Layered Architecture**
''Utilité :'' Pour débogage, statistiques, ou identification dans les logs.
- Hardware abstraction through interrupts
- Kernel services for task management
- Application-specific logic in tasks
```


---
=== `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>


## Kernel-Design.md
''Optimisations :''
* Noms stockés en Flash (PROGMEM)
* Parcours à partir de 1 (ignore tâche idle)
* Formatage lisible pour terminal


````markdown
== API Tâches - Détails Complémentaires ==
# Kernel Design


## Core Data Structures
=== `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>


### Task Control Block (TCB)
''Simulation de contexte :'' Prépare une pile comme si la tâche avait été interrompue juste avant sa première instruction.


```c
=== `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 {
typedef struct task_control_block {
     void (*function)(void*);     // Task entry point
    // 1. Pointeurs d'exécution
     void* arg;                   // Task parameters
     void (*function)(void*);     // 2 octets - Pointeur vers la fonction de la tâche
     uint8_t* stack_base;         // 96-byte stack memory
     void* arg;                   // 2 octets - Argument passé à la fonction
     task_state_t state;         // Current execution state
   
     uint16_t sleep_ticks;       // Sleep duration counter
    // 2. Informations de pile critique
     uint8_t priority;           // Execution priority (0-3)
    uint8_t* stack_ptr;          // 2 octets - Pointeur actuel dans la pile
     char name[TASK_NAME_LENGTH]; // Task identifier
     uint8_t* stack_base;         // 2 octets - Adresse de base de la pile allouée
} task_t;
    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`)'' ===


### System Globals
<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]]


```c
== ''Fonction Principale : `task_create()`'' ==
// Shared kernel state
task_t task_table[MAX_TASKS];        // Static task array
volatile uint8_t current_task_id;    // Currently executing task
volatile uint8_t task_count;        // Active task counter
volatile uint32_t system_ticks;      // Global time reference
volatile uint8_t critical_nesting;  // Interrupt disable counter
```


## State Management
=== ''Prototype et Paramètres'' ===


### Task Lifecycle
<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'' ===
CREATED → READY ↔ RUNNING → SLEEPING → READY
    ↓        ↓                  ↓
EXITED ← COMPLETED        (sleep expired)
```


### State Transitions
==== ''É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)


| Operation      | From State | To State  | Conditions                  |
==== ''Étape 2 : Attribution d'ID et Initialisation'' ====
| --------------- | ---------- | --------- | ---------------------------- |
<syntaxhighlight lang="c">
| `task_create()` | -          | READY    | Slot available, valid params |
uint8_t task_id = task_count;  // ID incrémental
| `schedule()`    | RUNNING    | READY    | Voluntary yield              |
task_t* task = (task_t*)&task_table[task_id];
| `task_sleep()`  | RUNNING    | SLEEPING  | ticks > 0                    |
</syntaxhighlight>
| Timer ISR      | SLEEPING  | READY    | sleep_ticks == 0             |
- ''ID'' : Attribué séquentiellement (0, 1, 2, 3...)
| `task_exit()`  | RUNNING    | COMPLETED | Task finished                |
- ''Pointeur'' : Accès direct au TCB dans la table


## Initialization Sequence
==== ''É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)


```c
==== ''Étape 4 : Initialisation du Contexte'' ====
void kernel_init(void) {
<syntaxhighlight lang="c">
     // 1. Clear all system state
task_create_context(task);
     memset(task_table, 0, sizeof(task_table));
</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...");
      
      
     // 2. Configure 100Hz system timer
     // Lecture du secteur 0 (MBR ou Boot Sector)
     TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);
    if (!read_sector(0)) return 0;
     OCR1A = 2499; // 16MHz/64/100Hz - 1
   
     TIMSK1 |= (1 << OCIE1A);
    // 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);
    }
      
      
     // 3. Create idle task
     // Mise à jour de la seconde FAT (miroir)
     task_create("idle", idle_task, NULL, PRIORITY_IDLE, idle_stack);
     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 ===");
      
      
     // 4. Enable global interrupts
     for (int i = 0; i < 32; i++) {
    sei();
        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


## Scheduler.md
'''Attributs ignorés''' :
```markdown
- <code>0x0F</code> : Long File Name (LFN) - extension Windows 95+
# Scheduler
- <code>0x08</code> : Volume Label - étiquette du volume


## Round-Robin Algorithm
'''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.


### Next Task Selection
=== Création de Fichier (<code>fat16_create_file</code>) ===


```c
<syntaxhighlight lang="c">
static uint8_t get_next_task(void) {
void fat16_create_file(const char* name, const char* content) {
     uint8_t next_task = (current_task_id + 1) % MAX_TASKS;
     if (!is_mounted) fat16_init();
      
      
     for (uint8_t i = 0; i < MAX_TASKS; i++) {
    // 1. Trouver un cluster libre
         if (is_task_valid(next_task) &&
    uint16_t free_clus = find_free_cluster();
             task_table[next_task].state == TASK_READY) {
    if (free_clus == 0) { uart_println("Err: Disk Full"); return; }
             return next_task;
   
    // 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;
             }
         }
         }
        next_task = (next_task + 1) % MAX_TASKS;
     }
     }
     return current_task_id;
     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>


### Task Validation
'''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


```c
'''Format d'entrée''' : <code>memset(ne, 0, 32)</code> initialise tous les champs à 0, y compris les dates/heures.
static uint8_t is_task_valid(uint8_t task_id) {
 
    return (task_id < MAX_TASKS &&
=== Suppression de Fichier (<code>fat16_delete_file</code>) ===
            task_table[task_id].function != NULL &&
 
            task_table[task_id].state != TASK_COMPLETED);
<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).


## Execution Loop
=== Renommage de Fichier (<code>fat16_rename_file</code>) ===


```c
<syntaxhighlight lang="c">
void scheduler_start(void) {
void fat16_rename_file(const char* old_name, const char* new_name) {
     scheduler_running = 1;
     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);
      
      
     while (scheduler_running) {
     for (int i = 0; i < 32; i++) {
         enter_critical_section();
         if (!read_sector(root_dir_sector + i)) continue;
        uint8_t dirty = 0;
          
          
         if (is_task_valid(current_task_id)) {
         for (int j = 0; j < 16; j++) {
             leave_critical_section();
             fat16_dir_entry_t* e = (fat16_dir_entry_t*)(shared_buffer + j * 32);
             task_table[current_task_id].function(task_table[current_task_id].arg);
             if (e->filename[0] == 0x00) { i = 32; break; }
             enter_critical_section();
            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;
            }
         }
         }
         current_task_id = get_next_task();
          
         leave_critical_section();
        if (dirty) {
         _delay_us(1000);
            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>) ===
 
<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.
 
<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 :
 
# ''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>


## Priority Handling
* ''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`.


While primarily round-robin, priorities influence task creation order, manual scheduling, and idle task execution.
== 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 ===


## Memory-Management.md
<syntaxhighlight lang="c">
```markdown
static char cmd_buf[64];
# Memory Management
static uint8_t cmd_idx = 0;
</syntaxhighlight>


## Resource Constraints
* ''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).


| Resource | Allocation | Usage |
=== 2.2 Traitement des Entrées (Input Handling) ===
|----------|------------|-------|
| **Total Tasks** | 4 slots | System + application tasks |
| **Stack Size** | 96 bytes/task | Function calls and locals |
| **TCB Size** | 28 bytes/task | Task metadata storage |
| **Total RAM** | ~516 bytes | 25% of ATmega328p capacity |


## Memory Layout
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.


0x0000 +----------------+
==== Cas du Backspace ('\b' ou 127) ====
| .data (init)   |
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).
| .bss (zero)   |
# ''Logique Interne'' : `cmd_idx--`. On recule simplement l'index d'écriture.
+----------------+
# ''Logique Visuelle'' : `console_print("\b \b")`.
| task_table[4]  |
#: Le premier `\b` recule le curseur du terminal.
+----------------+
#: L'espace ` ` écrase le caractère affiché.
| stack_0[96]    |
#: Le second `\b` remet le curseur à la position reculée.
+----------------+
| stack_1[96]    |
+----------------+
| stack_2[96]    |
+----------------+
| stack_3[96]    |
+----------------+
| Heap (unused) |
+----------------+
0x0800 | Stack (down)  |
+----------------+


````
== 3. Analyse du Moteur de Parsing (shell_process_cmd) ==


## Configuration Tuning
C'est le cœur logique du shell. Il transforme une chaîne brute en appel de fonction.


### config.h Parameters
=== 3.1 Tokenisation Destructive (strtok) ===


```c
Le code utilise `strtok` (String Tokenizer) de la `stdlib`.
#define MAX_TASKS 4
#define STACK_SIZE 96
#define TASK_NAME_LENGTH 8
#define TICK_FREQUENCY 100
#define F_CPU 16000000UL
#define PRIORITY_IDLE  0
#define PRIORITY_LOW    1
#define PRIORITY_MEDIUM 2
#define PRIORITY_HIGH  3
````


````
<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'}`


## API-Reference.md
Après le premier appel `strtok(cmd_buf, " ")` :
```markdown
# `strtok` trouve le premier espace.
# API Reference
# 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'}`.


## Kernel API
Les appels suivants `strtok(NULL, " ")` reprennent après le `\0` inséré précédemment.


### `kernel_init()`
'''Conséquence Critique :'''
```c
Cette méthode rend impossible la récupération de la ligne originale. Le buffer est "haché".
void kernel_init(void);
````


Initializes all kernel subsystems, clears task table, sets up timer, and enables interrupts.
=== 3.2 Normalisation (strupr) ===


### `kernel_start()`
<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.


```c
== 4. Analyse Détaillée des Commandes ==
void kernel_start(void);
```


Begins task execution. Requires prior initialization.
=== 4.1 Commandes Fichiers (File System) ===


### `kernel_delay_ms()`
==== 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)`.


```c
==== Commande `CAT` (Lecture et Affichage) ====
void kernel_delay_ms(uint16_t ms);
* ''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).


Cooperative delay without busy-waiting.
<syntaxhighlight lang="c">
char buf[64]; // Tampon temporaire sur la pile
uint16_t offset = 0;
int16_t read;


## Task Management API
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>


### `task_create()`
* ''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).


```c
==== Commande `WRITE` (Écriture) ====
int8_t task_create(const char* name, void (*function)(void*), void* arg, uint8_t priority, uint8_t* stack_buffer);
* ''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`.''


Creates a new task. Returns task ID or -1 on error.
=== 4.2 Commandes Noyau (Kernel) ===


### `task_sleep()`
Ces commandes font le pont entre l'utilisateur et le planificateur (`scheduler.c`).


```c
==== Commande `PS` (Process Status) ====
void task_sleep(uint16_t ticks);
* ''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é.


Transitions running task to sleeping state.
==== 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.


### `task_exit()`
=== 4.3 Commande Matériel (`SCREEN`) ===


```c
* ''Signature'' : `SCREEN <id>`
void task_exit(void);
* ''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.


Marks current task as completed.
== 5. Robustesse et Gestion des Erreurs ==


## Scheduler Control API
=== Gestion des Arguments Manquants ===
Le code utilise un modèle défensif simple :


### `enter_critical_section()`
<syntaxhighlight lang="c">
if (strcmp(cmd, "KILL") == 0) {
    if(arg1) shell_kill(atoi(arg1));
}
</syntaxhighlight>


```c
* 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.
void enter_critical_section(void);
* ''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.
```


Disables interrupts and protects critical resources.
=== 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.


### `schedule()`
== 6. Résumé des Flux de Données ==


```c
# ''Entrée'' : UART RX (Interruption ou Polling) -> Registre UDR -> `uart_getchar()` -> `cmd_buf`.
void schedule(void);
# ''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'''.


Yields control to next ready task.
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.


## Build-System.md
==== Décodeur de commandes ====
```markdown
Le décodeur reçoit les caractères ASCII transmis par le bus SPI et gère leur affichage à l’écran.
# Build System
Fonctionnalités prises en charge :


## Toolchain Requirements
* écriture séquentielle des caractères
* retour à la ligne automatique
* suppression du dernier caractère (Backspace)
* effacement complet de l’écran (Delete)


```bash
Le décodeur traduit chaque caractère en :
sudo apt install avr-gcc avr-libc avrdude make
brew install avr-gcc avrdude
````


## Makefile Targets
* adresses mémoire
* indices de caractères
* signaux de contrôle d’écriture


| Target      | Purpose                | Output          |
Chaque caractère est converti en 8 lignes de pixels, stockées consécutivement en mémoire vidéo.
| ------------ | ---------------------- | --------------- |
| `make`      | Build firmware        | `firmware.hex`  |
| `make flash` | Program device        | Serial upload  |
| `make clean` | Remove build artifacts | Clean directory |
| `make size`  | Display memory usage  | Text report    |


## Compilation Flags
==== Génération des signaux RGB ====
Pour chaque pixel situé dans la zone visible :


```makefile
* le bit correspondant est extrait de l’octet lu en mémoire
CFLAGS = -mmcu=atmega328p \
* ce bit est appliqué simultanément aux sorties '''R''', '''G''' et '''B'''
        -DF_CPU=16000000UL \
        -Os -Wall -std=c99
```


## Programming Configuration
L’affichage est donc réalisé en '''monochrome''', avec des pixels blancs sur fond noir.


```makefile
==== Synchronisation avec le bus SPI ====
PROGRAMMER = arduino
Le contrôleur VGA fonctionne indépendamment du bus SPI.
PORT = /dev/ttyACM0
BAUD = 115200
```


Use `make flash` to upload via Arduino bootloader.
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


## Performance-Analysis.md
=== Bitcoin Miner ===
```markdown
'''Objectif du projet :''' Développer un système complet de minage Bitcoin sur carte FPGA Zybo Z7-10 en exploitant l'architecture (Processeur + FPGA).
# Performance Analysis


## Timing Characteristics
==== Architecture du système ====
'''Partie Processeur (ARM Cortex-A9) :'''


| Operation | CPU Cycles | Real Time @16MHz |
* Implémentation d'un "Mining client" en C
|-----------|------------|------------------|
* Connexion réseau aux serveurs de minage
| Context Switch | ~16000 | 1ms |
* Récupération et décodage des "jobs" de minage
| Tick ISR | ~800 | 50μs |
* Configuration des registres FPGA via le bus AXI
| Task Search | 80-320 | 5-20μs |
* Transmission des résultats valides au serveur
| Sleep Update | 20-80 | 1.25-5μs |


## Memory Utilization
'''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


Program Memory: ~3KB of 32KB (9%)
'''Fonctionnement :'''
Data Memory:    ~516B of 2KB (25%)
Stack Headroom: ~384B per task (conservative)


```
# 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


## Interrupt Timing
==== 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 :


Input Frequency: 16,000,000 Hz
* Signal <code>start</code> pour lancer le calcul, <code>done</code> pour indiquer la fin, et bus de sortie 256 bits pour le hash.
Prescaler: 64
* Exécute les 64 tours de compression SHA-256 en 64 cycles d'horloge
Timer Frequency: 250,000 Hz
* Utilise des opérations bit-à-bit parallèles pour maximiser la vitesse
Compare Match: 2500 counts
Interrupt Frequency: 100 Hz
Interrupt Period: 10 ms


```
Fonctionnement algorithmique :


Critical section disable time <1ms during switches.
* 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 :


## _Sidebar.md
* <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]]


```markdown
<br>
**Navigation**
<br>
- [[Home]]
<br>
- [[System Architecture]]
<br>
- [[Kernel Design]]
<br>
- [[Scheduler]]
<br>
- [[Memory Management]]
<br>
- [[API Reference]]
<br>
- [[Build System]]
<br>
- [[Performance Analysis]]
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>


**External Links**
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.
- [Repository](https://github.com/your-username/your-repo)
- [Issues](https://github.com/your-username/your-repo/issues)
- [Releases](https://github.com/your-username/your-repo/releases)
```


---
==== Annexe ====


These Markdown files can be directly uploaded into the GitHub Wiki via the web interface or the `wiki.git` repository clone.
* [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

Schema shield arduino.jpg

Objectif

Schema shield arduino


Routage shield arduino


Carte mère

Schématique

Schéma carte mère du pico ordinateur

Routage

Routage carte mère

Vue 3D

vue3D carte mère



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;
FSM Taches.png

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_entries dans 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

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.

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

  1. Calcul du Secteur SD : phys_sector = vblock / 2.
  2. Exemple : Les blocs virtuels 10 et 11 sont tous deux dans le secteur physique 5.
  3. Lecture Physique : sd_read_block(phys_sector, temp_buf).
    Tout le secteur (512o) est chargé dans le tampon statique temp_buf.
  4. Extraction :
    1. Si vblock est pair (vblock % 2 == 0), on copie temp_buf[0..255].
    2. 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.

  1. 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).
  2. 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.
  3. É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 :

  1. Boucle Niveau 1 (Pages) : Itère sur les 16 blocs de Bitmap. Charge un bloc entier en mémoire.
  2. 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.
  3. 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)`.
  4. Calcul d'Index Absolu :
    `ID = (Page * 2048) + (Octet * 8) + Bit`

Persistance Immédiate

Dès qu'un bit libre est trouvé :

  1. Il est mis à 1 en RAM.
  2. 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`) :

  1. Détection du Remplissage : Le système calcule `offset = size % 256`.
    Si `offset > 0`, le dernier bloc alloué n'est pas plein.
  2. 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.
  3. 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.

  1. Vérifie si le fichier existe déjà (erreur si oui).
  2. Cherche une entrée libre dans le répertoire (là où name[0] == 0).
  3. Initialise l'entrée : size = 0, blocks[] = {0}.
  4. 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é :

  1. Contexte : Récupère l'entrée du fichier. Si size est 4096, erreur (Disque Plein/Fichier Plein).
  2. Remplissage du dernier bloc (Partial Fill) :
    1. Calcul : offset = size % 256.
    2. Si offset > 0 (le dernier bloc n'est pas plein) :
      1. Récupère l'ID du dernier bloc : blk_id = entry->blocks[(size-1)/256].
      2. Lit ce bloc en mémoire.
      3. Calcule l'espace libre : space = 256 - offset.
      4. Copie min(len, space) octets à la suite des données existantes.
      5. Sauvegarde le bloc.
      6. Met à jour size et décrémente len.
  3. Allocation de nouveaux blocs :
    1. Tant qu'il reste des données (len > 0) :
      1. Vérifie qu'on a pas atteint 16 blocs.
      2. new_blk = fs_alloc_block(). Si 0 (erreur), arrêt.
      3. Ajoute new_blk au tableau entry->blocks[].
      4. Copie les 256 prochains octets de données dans un buffer.
      5. Écrit ce buffer dans le nouveau bloc (fs_write_vblock).
      6. Met à jour size.
  4. Finalisation :
    1. 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).

  1. Bornage : Si offset > file_size, retourne 0. Ajuste read_count pour ne pas dépasser la fin du fichier.
  2. Boucle de lecture :
    1. Calcule l'index du bloc logique : blk_idx = (offset + i) / 256.
    2. Calcule l'offset dans ce bloc : blk_off = (offset + i) % 256.
    3. Récupère l'ID physique via entry->blocks[blk_idx].
    4. Lit le bloc.
    5. Copie la portion nécessaire dans le buffer utilisateur.
    6. Avance au bloc suivant si nécessaire.

fs_remove(name)

Suppression propre pour éviter les fuites mémoire (blocs orphelins).

  1. Trouve l'entrée fichier.
  2. Libération Bitmap : Pour chaque ID non nul dans entry->blocks[], appelle fs_set_bitmap(id, 0).
  3. Nettoyage Répertoire : memset l'entrée à 0. Cela libère le slot pour un futur fichier.
  4. 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 :

  1. É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.
  2. Test de Condition (Miroir) :
    `if (active_screen_id > 0)` : Vérifie si un écran est attaché.
  3. Transaction SPI (Si actif) :
    Le code implémente une transaction SPI manuelle complète pour chaque caractère :
    1. `spi_select_device(active_screen_id)` : Active la ligne CS (mise à 0).
    2. `spi_transfer(c)` : Envoie l'octet via le registre `SPDR` et attend la fin de transmission (Polling du flag SPIF).
    3. `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.

  1. Terminaison de Chaîne : `cmd_buf[cmd_idx] = 0;` transforme le tableau de caractères en chaîne C valide (Null-terminated string).
  2. Saut de ligne : `console_println("")` pour que la réponse s'affiche sur la ligne suivante.
  3. Exécution : Appel bloquant à `shell_process_cmd()`.
  4. 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.
  5. Prompt : Affiche `> ` pour inviter à la saisie suivante.

Cas du Backspace ('\b' ou 127)

Gère l'édition de ligne.

  1. 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).
  2. Logique Interne : `cmd_idx--`. On recule simplement l'index d'écriture.
  3. 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, " ")` :

  1. `strtok` trouve le premier espace.
  2. Il le remplace par `\0`.
  3. Il retourne un pointeur vers le début (`&cmd_buf[0]`).
  4. `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

  1. Entrée : UART RX (Interruption ou Polling) -> Registre UDR -> `uart_getchar()` -> `cmd_buf`.
  2. Traitement : `cmd_buf` -> `strtok` -> `cmd`, `arg1`, `arg2`.
  3. Décision : Comparaison de chaînes (`strcmp`).
  4. Sortie (FS) : `fs_read` -> Buffer temporaire -> `console_print` -> UART TX + SPI MOSI.
  5. 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.

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
SPI timing diagram
Déroulement d’une communication SPI
  1. Le maître active l'esclave en mettant la ligne SS à l'état bas.
  2. L'horloge SCK se met à osciller et chaque front d'horloge permet la transmission d'un bit.
  3. Envoie des données :
    • Le maître envoie un bit sur MOSI
    • L'esclave repond sur MISO
  4. Une fois la communication terminée, le maître désactive l'esclave en mettant SS à l'état haut.
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
Démonstration contrôleur d’écran VGA.mp4

Démonstration & Code

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 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 = 352
  • offset_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 :

  1. Le processeur se connecte au "Mining pool server" et reçoit un bloc de travail
  2. Le processeur extrait l'en-tête de bloc (80 octets) et le prépare pour le FPGA
  3. Transfert des données vers le FPGA via le bus AXI et démarrage du calcul
  4. Le FPGA signale immédiatement toute solution valide (hash < cible)
  5. 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 start pour lancer le calcul, done 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₇, 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

Message
SHA256 du message


















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