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

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
Aucun résumé des modifications
Ligne 34 : Ligne 34 :
<br clear="all" />
<br clear="all" />
= Firmware — RTOS =
= Firmware — RTOS =
= Firmware — Pico-OS (RTOS Préemptif) =


== Vue d'ensemble du système ==
== Vue d'ensemble du système ==
Le firmware implémente un '''système d'exploitation temps réel (RTOS) coopératif''' optimisé pour les microcontrôleurs AVR (ATmega328P/AT90USB1286).
Le firmware implémente un '''système d'exploitation temps réel (RTOS) préemptif''' complet pour l'ATmega328P. Contrairement à la version coopérative précédente, ce noyau utilise des interruptions matérielles pour forcer le changement de contexte entre les tâches, garantissant qu'une tâche lourde (comme lister des fichiers SD) ne bloque jamais les tâches critiques (comme clignoter une LED).


'''Architecture coopérative vs préemptive :'''
'''Caractéristiques principales :'''
* '''Coopératif''' : Les tâches cèdent volontairement le contrôle (via <code>task_yield()</code>, <code>task_sleep()</code>)
* '''Ordonnancement Préemptif''' : Commutation de contexte basée sur le Timer1 (100 Hz).
* '''Avantage''' : Plus simple, moins de surcharge mémoire, pas besoin de sauvegarde de contexte
* '''Priorités''' : Ordonnanceur Round-Robin avec niveaux de priorité (IDLE, LOW, MEDIUM, HIGH).
* '''Inconvénient''' : Une tâche malveillante peut bloquer le système
* '''Système de Fichiers FAT16''' : Lecture/Écriture complète, création/suppression de fichiers, support des scripts.
* '''Shell Interactif''' : Interface ligne de commande via UART (115200 bauds).
* '''Optimisation Extrême''' : Conçu pour fonctionner dans les 2 Ko de RAM de l'ATmega328p.


'''Optimisations mémoire :'''
== Architecture Logicielle ==
* Allocation statique uniquement (pas de malloc/free)
 
* Taille fixe des structures pour prédictibilité
L'arborescence du projet est structurée pour séparer le noyau, les pilotes et le système de fichiers.
* Piles partagées entre noyau et applications


==  Arborescence (extrait) ==
<pre>
<pre>
firmware/
src/
  ├─ kernel/                 # Cœur du système d'exploitation
├─ main.c                # Point d'entrée et définition des tâches utilisateur
  │ ├─ kernel.h           # API publique du noyau
├─ Makefile              # Système de compilation
  │ ├─ kernel.c           # Gestion système (ticks, délais)
  ├─ kernel/               # Cœur de l'OS
  │ ├─ scheduler.h         # Déclarations de l'ordonnanceur  
  │   ├─ kernel.c/h       # Initialisation et boucles principales
  │ ├─ scheduler.c         # Implémentation round-robin
  │   ├─ scheduler.c/h    # Gestionnaire de tâches et ISR
  │ ├─ task.h             # Structures de contrôle des tâches
  │   ├─ task.c/h         # Création de tâches et gestion de la pile
  │ └─ task.c             # Création et gestion des tâches
│  └─ config.h          # Paramètres globaux (Fréquence, Taille piles)
  ├─ config.h               # Configuration matérielle (mémoire, timers)
  ├─ drivers/              # Pilotes Matériels
  └─ main.c                 # Tâches applicatives (horloge binaire)
  │   ├─ uart.c/h          # Gestion du port série (Interruption RX)
  │   ├─ spi_bitbang.c/h   # SPI logiciel atomique
  │   └─ sd_card.c/h      # Protocole SD bas niveau (CMD/Response)
  ├─ fs/                  # Système de fichiers
│  └─ fat16.c/h         # Implémentation FAT16 complète
  └─ shell/                # Interface Utilisateur
    └─ shell_core.c/h    # Interpréteur de commandes
</pre>
</pre>


'''Séparation des responsabilités :'''
== Le Noyau (Kernel) ==
* <code>kernel/</code> : Fonctionnalités système bas niveau
* <code>config.h</code> : Adaptation au matériel spécifique
* <code>main.c</code> : Logique métier de l'application


== Paramètres de configuration ==
=== Commutation de Contexte (Context Switching) ===
<syntaxhighlight lang="c">
Le cœur du système repose sur l'interruption du Timer 1. Nous utilisons une fonction `ISR_NAKED` pour avoir un contrôle total sur la pile (Stack).
#define MAX_TASKS      4      // Compromis fonctionnalité/mémoire
#define STACK_SIZE      96    // Suffisant pour appels de fonctions + variables locales
#define TICK_FREQUENCY  100    // 100Hz = résolution 10ms (équilibre précision/charge CPU)
#define TASK_NAME_LEN  8      // Noms courts pour économiser la RAM
</syntaxhighlight>


'''Justification des valeurs :'''
* '''MAX_TASKS=4''' : Permet tâche système + 3 tâches applicatives dans 2KB RAM
* '''STACK_SIZE=96''' : Empilement typique des appels AVR + marge de sécurité
* '''TICK_FREQUENCY=100''' : Temps de réponse <10ms sans surcharger le CPU


==  Structure de contrôle de tâche (TCB) ==
<syntaxhighlight lang="c">
typedef enum {
    TASK_INVALID = 0,
    TASK_READY,
    TASK_RUNNING,
    TASK_SLEEPING
} task_state_t;


typedef struct {
'''Le mécanisme de préemption :'''
    void (*function)(void *);
1. **Interruption :** Le Timer 1 se déclenche toutes les 10ms.
    void *arg;
2. **Sauvegarde (Push) :** L'ISR empile manuellement les 32 registres généraux (`r0` à `r31`) et le registre d'état (`SREG`).
    uint8_t *stack_base;
3. **Sauvegarde du SP :** Le pointeur de pile matériel (`SP`) est sauvegardé dans la structure de la tâche courante (`task->stack_ptr`).
    task_state_t state;
4. **Ordonnancement :** La fonction C `scheduler_tick_preemptive()` choisit la prochaine tâche.
    uint16_t sleep_ticks;
5. **Restauration (Pop) :** Le `SP` de la nouvelle tâche est chargé, puis ses registres sont dépilés.
    uint8_t priority;
6. **RETI :** L'instruction de retour restaure le compteur ordinal (PC), reprenant l'exécution de la nouvelle tâche exactement là où elle s'était arrêtée.
    char name[TASK_NAME_LEN];
} task_t;
</syntaxhighlight>


'''Cycle de vie d'une tâche :'''
<pre>
CRÉATION → [READY] ↔ [RUNNING] → [SLEEPING] → [READY]
                    ↓
                [COMPLETED] (si task_exit() appelée)
</pre>
==  Gestion des sections critiques (imbriquées) ==
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
#include <avr/interrupt.h>
ISR(TIMER1_COMPA_vect, ISR_NAKED) {
static volatile uint8_t critical_nesting = 0;
     // 1. Sauvegarde du contexte (Assembleur inline)
 
     asm volatile(
static inline void enter_critical(void) {
        "push r0 \n in r0, __SREG__ \n push r0 \n" // Sauve SREG
     cli();
         "push r1 \n push r2 \n ... \n push r31 \n" // Sauve R1-R31
     critical_nesting++;
    );
}
      
 
    // 2. Sauvegarde du Pointeur de Pile (SP) vers la Tâche Courante
static inline void leave_critical(void) {
    asm volatile(
    if (critical_nesting > 0) {
        "lds r28, current_task_ptr \n" // Charge l'adresse du TCB
         critical_nesting--;
        "in r30, 0x3d \n"              // Lit SPL
        if (critical_nesting == 0) sei();
        "std Y+4, r30 \n"              // Stocke dans TCB->stack_ptr
     }
     );
}
</syntaxhighlight>
 
'''Protection des ressources partagées :'''
* Empêche les accès concurrents aux structures partagées (task_table, variables globales)
* '''Imbrication''' : Appels imbriqués autorisés
* '''Performance''' : Sections critiques très courtes (<1ms)
 
==  Création de tâche ==
<syntaxhighlight lang="c">
int8_t task_create(const char *name, void (*function)(void *), void *arg,
                  uint8_t priority, uint8_t *stack_buffer)
{
     enter_critical();
      
      
     for (uint8_t i = 0; i < MAX_TASKS; i++) {
     // 3. Appel de l'Ordonnanceur C
        if (task_table[i].function == NULL) {
    asm volatile("call scheduler_tick_preemptive");
            task_table[i].function = function;
            task_table[i].arg = arg;
            task_table[i].stack_base = stack_buffer;
            task_table[i].state = TASK_READY;
            task_table[i].priority = priority;
            strncpy(task_table[i].name, name, TASK_NAME_LEN);
           
            leave_critical();
            return i;
        }
    }
      
      
     leave_critical();
     // 4. Restauration du Pointeur de Pile depuis la Nouvelle Tâche
     return -1;
    // 5. Restauration du contexte (Pop)
     asm volatile("reti");
}
}
</syntaxhighlight>
</syntaxhighlight>


'''Contraintes :'''
=== Gestion de la Mémoire et PROGMEM ===
* Appelée avant <code>scheduler_start()</code>
L'ATmega328p ne dispose que de **2048 octets de RAM**. Avec un système de fichiers et un shell, la saturation mémoire est le principal danger.
* Piles statiques uniquement
* Noms tronqués à TASK_NAME_LEN


==  Ordonnanceur (round-robin) ==
'''Stratégies d'optimisation :'''
<syntaxhighlight lang="c">
* **Chaînes en Flash (PROGMEM) :** Tous les noms de tâches et les chaînes de caractères du Shell sont stockés en mémoire programme (Flash) pour épargner la RAM. Les fonctions comme `pgm_read_word` sont utilisées pour y accéder.
static uint8_t current_task_id = 0;
* **Buffer Partagé :** Le pilote FAT16 utilise un unique buffer de 512 octets (`shared_buffer`) pour toutes les opérations (lecture MBR, FAT, Répertoire, Données), au lieu d'allouer plusieurs tampons.
* **Piles Ajustées :** Chaque tâche possède une taille de pile spécifique définie à la création (`blink`: 64o, `shell`: 384o).


static inline uint8_t is_task_valid(uint8_t id) {
== Système de Fichiers (FAT16) ==
    return (id < MAX_TASKS) &&
          (task_table[id].function != NULL) &&
          (task_table[id].state == TASK_READY);
}


static uint8_t get_next_task(void) {
Le pilote FAT16 a été écrit à la main pour supporter les opérations de lecture et d'écriture tout en minimisant l'empreinte mémoire.
    uint8_t next = (current_task_id + 1) % MAX_TASKS;
    for (uint8_t i = 0; i < MAX_TASKS; i++) {
        if (is_task_valid(next)) return next;
        next = (next + 1) % MAX_TASKS;
    }
    return current_task_id;
}


void scheduler_run(void) {
    enter_critical();
   
    if (is_task_valid(current_task_id)) {
        task_table[current_task_id].state = TASK_RUNNING;
        leave_critical();
       
        task_table[current_task_id].function(task_table[current_task_id].arg);
       
        enter_critical();
        if (task_table[current_task_id].state == TASK_RUNNING)
            task_table[current_task_id].state = TASK_READY;
    }
   
    current_task_id = get_next_task();
    leave_critical();
}
</syntaxhighlight>


'''Algorithme round-robin :'''
* Parcours circulaire équitable
* Recherche O(n)
* Aucune sauvegarde de contexte (coopératif)


== Mécanismes de sommeil et tick système ==
=== Fonctionnalités ===
<syntaxhighlight lang="c">
* **Montage Dynamique :** Détection automatique du Master Boot Record (MBR) ou du format Superfloppy.
// Configuration du timer hardware pour 100Hz
* **Allocation Réelle :** Recherche de clusters libres dans la FAT pour l'écriture.
void timer1_init_100hz(void) {
* **Mise à jour Miroir :** Écriture simultanée dans `FAT1` et `FAT2` pour assurer la compatibilité avec Linux/Windows (évite les erreurs "Read-only file system").
    TCCR1A = 0;
* **Noms 8.3 :** Conversion automatique des noms de fichiers (ex: `test.txt` → `TEST    TXT`).
    TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);
    OCR1A = 2499;
    TIMSK1 |= (1 << OCIE1A);
}


ISR(TIMER1_COMPA_vect) {
=== Écriture de Fichier (Create) ===
    extern volatile uint32_t system_ticks;
La fonction `fat16_create_file` effectue les opérations suivantes :
    system_ticks++;
# Scanne la FAT pour trouver un cluster libre (marqué `0x0000`).
   
# Scanne le répertoire racine pour trouver une entrée libre.
    for (uint8_t i = 0; i < MAX_TASKS; i++) {
# Écrit les métadonnées du fichier (Nom, Taille, Cluster de départ) dans le répertoire.
        if (task_table[i].state == TASK_SLEEPING && task_table[i].sleep_ticks > 0) {
# Écrit le contenu du fichier dans le secteur de données correspondant.
            task_table[i].sleep_ticks--;
# Met à jour la FAT pour marquer le cluster comme "Fin de fichier" (`0xFFFF`).
            if (task_table[i].sleep_ticks == 0)
                task_table[i].state = TASK_READY;
        }
    }
}


void task_sleep(uint16_t ticks) {
== Pilotes Matériels (Drivers) ==
    if (ticks == 0) ticks = 1;
   
    enter_critical();
    task_table[current_task_id].sleep_ticks = ticks;
    task_table[current_task_id].state = TASK_SLEEPING;
    leave_critical();
   
    scheduler_run();
}
</syntaxhighlight>


'''Gestion du temps :'''
=== SPI Atomique ===
* Timer hardware 100Hz → tick 10ms
Le pilote SPI (`spi_bitbang.c`) est critique dans un système préemptif.
* Pas de busy-waiting
* **Problème :** Si l'ordonnanceur interrompt l'envoi d'un octet SPI, l'horloge (SCK) peut rester à l'état haut/bas pendant 10ms. Certaines cartes SD interprètent cela comme un timeout ou une erreur.
* Réveil automatique via ISR
* **Solution :** Utilisation de blocs atomiques. Les interruptions sont désactivées (`cli()`) juste avant d'envoyer les 8 bits d'un octet, et réactivées (`SREG = sreg`) immédiatement après.


== Initialisation du système ==
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
void kernel_init(void) {
uint8_t spi_transfer(uint8_t data) {
     memset(task_table, 0, sizeof(task_table));
     uint8_t sreg = SREG;
    current_task_id = 0;
     cli(); // DÉBUT SECTION CRITIQUE
     task_count = 0;
    system_ticks = 0;
      
      
     timer1_init_100hz();
     // Bit-banging rapide (quelques microsecondes)
     uart_init();
     for (int i=7; i>=0; i--) { ... }
    io_init();
      
      
     task_create("idle", idle_task, NULL, PRIORITY_IDLE, idle_stack);
     SREG = sreg; // FIN SECTION CRITIQUE
     sei();
     return received;
}
}
</syntaxhighlight>
</syntaxhighlight>


'''Séquence de boot :'''
== Shell et Scripting ==
# Réinitialisation
# Configuration hardware
# Création tâche idle
# Activation interruptions


==  Gestion d'erreurs ==
Le Shell (`shell_core.c`) permet l'interaction utilisateur. Il supporte l'exécution de scripts via la commande `exec`.
'''Stratégies de robustesse :'''
* Validation des pointeurs
* Sleep minimal
* Tâche idle toujours active


<syntaxhighlight lang="c">
'''Commande EXEC :'''
if (function == NULL || stack_buffer == NULL) {
La commande `exec SCRIPT.TXT` :
    return -1;
# Utilise `fat16_read_to_buffer` pour charger le contenu du fichier texte dans la RAM.
}
# Passe ce buffer à l'interpréteur de commandes `shell_process_line`.
</syntaxhighlight>
# Cela permet d'automatiser des séquences de démarrage ou de test.
 
== Guide d'utilisation ==
 
=== Commandes disponibles ===
{| class="wikitable"
! Commande !! Description
|-
| `ps` || Affiche la liste des tâches, leur état et l'utilisation CPU.
|-
| `list` || Affiche les fichiers présents sur la carte SD avec leur taille.
|-
| `type <fichier>` || Affiche le contenu d'un fichier texte.
|-
| `create <nom> <txt>` || Crée un nouveau fichier contenant le texte spécifié.
|-
| `del <nom>` || Supprime un fichier (marque comme supprimé).
|-
| `exec <script>` || Exécute le contenu d'un fichier comme une commande.
|-
| `free` || Affiche une estimation de la RAM libre.
|-
| `reboot` || Redémarre le microcontrôleur.
|}


== Estimations mémoire ==
=== Exemple de Session ===
<pre>
<pre>
Piles tâches      : 4 × 96 octets  = 384 octets
Pico> create auto.bat version
Structures TCB    : 4 × 28 octets  = 112 octets 
Fichier cree.
Variables globales: ~20 octets
Pico> exec auto.bat
TOTAL estimé      : 516 / 2048 (25%)
Exec: auto.bat
Run: version
Pico-OS v1.2 (Dual FAT + Exec)
</pre>
</pre>
==  Intégration du build system ==
<syntaxhighlight lang="makefile">
MCU = atmega328p
F_CPU = 16000000UL
CFLAGS = -mmcu=$(MCU) \
        -DF_CPU=$(F_CPU) \
        -Os -Wall -std=c99
all: firmware.hex
flash: firmware.hex
clean:
</syntaxhighlight>
==  Comportements observables ==
<syntaxhighlight lang="c">
kernel_init();
task_create("task1", ...);
task_create("task2", ...);
kernel_start();
</syntaxhighlight>
<syntaxhighlight lang="c">
void ma_tache(void* arg) {
    while (1) {
        faire_travail();
        task_sleep(100);
    }
}
</syntaxhighlight>
= Démonstration RTOS de Clignotement Double LED - Documentation Technique Détaillée =
== Aperçu Global du Système ==
Ce projet implémente un système d'exploitation temps réel (RTOS) coopératif sur microcontrôleur **ATmega328P**, démontrant le contrôle simultané de deux LEDs avec des fréquences de clignotement distinctes. 
L'architecture logicielle repose sur trois tâches indépendantes s'exécutant concurremment sous la supervision d'un ordonnanceur *round-robin*, illustrant les principes fondamentaux du multitâche dans les systèmes embarqués contraints en ressources.
L'originalité de cette implémentation réside dans son approche **coopérative pure**, où les tâches conservent le contrôle du processeur jusqu'à leur terminaison naturelle, sans mécanisme de préemption. 
Cette conception simplifiée minimise la surcharge système tout en maintenant une précision temporelle acceptable pour des applications de contrôle basique.
== Architecture Matérielle Détaillée ==
=== Microcontrôleur et Configuration ===
Le système cible un ATmega328P fonctionnant à 16 MHz, un choix dicté par son équilibre entre performances, consommation énergétique et disponibilité. 
Cette plateforme offre **32 Ko de mémoire Flash** et **2 Ko de RAM SRAM**, des ressources suffisantes pour héberger le noyau RTOS et les tâches applicatives.
=== Mapping des Broches GPIO ===
<syntaxhighlight lang="c">
#define LED1_PIN PC2    // Port C, Bit 2 - Broche analogique 2 sur Arduino
#define LED2_PIN PC5    // Port C, Bit 5 - Broche analogique 5 sur Arduino
</syntaxhighlight>
La sélection des broches PC2 et PC5 n'est pas arbitraire :
* Elles appartiennent au même port (PORTC), permettant une configuration groupée 
* Aucune fonction spéciale ne leur est associée sur ATmega328P 
* Positionnement favorable sur les cartes de développement standards 
=== Configuration du Sous-système UART ===
Le module UART est configuré pour une communication asynchrone à **115 200 bauds**, optimale pour le débogage temps réel. 
**Paramètres :**
* Format de trame : 8 bits de données, 1 bit de stop, sans parité 
* Buffer d’émission activé, réception désactivée 
* Adaptation des séquences de nouvelle ligne pour terminal standard 
== Analyse Détaillée du Code Source ==
=== Structure de Données et Variables Globales ===
<syntaxhighlight lang="c">
uint8_t led1_stack[STACK_SIZE];  // 96 octets pour la tâche LED1
uint8_t led2_stack[STACK_SIZE];  // 96 octets pour la tâche LED2 
uint8_t status_stack[STACK_SIZE]; // 96 octets pour la tâche Status
</syntaxhighlight>
Cette allocation statique élimine les risques de fragmentation mémoire et garantit un comportement déterministe. 
Chaque pile est dimensionnée empiriquement pour accommoder la profondeur d'appel maximale anticipée.
=== Tâche LED1 – Clignotement 300 ms ===
<syntaxhighlight lang="c">
void blink_led1_task(void* arg) {
    static uint32_t last_toggle = 0;
    static uint8_t led_state = 0;
    if (last_toggle == 0) {
        DDRC |= (1 << LED1_PIN);
        PORTC &= ~(1 << LED1_PIN);
        printf(">>> LED1 Task: Started (300ms blink)\n");
    }
    uint32_t current_time = kernel_get_ticks();
    if (current_time - last_toggle >= 30) {
        led_state = !led_state;
        if (led_state)
            PORTC |= (1 << LED1_PIN);
        else
            PORTC &= ~(1 << LED1_PIN);
        last_toggle = current_time;
    }
}
</syntaxhighlight>
**Analyse du Comportement :**
La tâche LED1 implémente une machine à états simple avec persistance de contexte (`static`). 
Elle est non bloquante et maintient la réactivité globale du système.
=== Tâche LED2 – Clignotement 100 ms ===
<syntaxhighlight lang="c">
void blink_led2_task(void* arg) {
    static uint32_t last_toggle = 0;
    static uint8_t led_state = 0;
    if (last_toggle == 0) {
        DDRC |= (1 << LED2_PIN);
        PORTC &= ~(1 << LED2_PIN);
        printf(">>> LED2 Task: Started (100ms blink)\n");
    }
    uint32_t current_time = kernel_get_ticks();
    if (current_time - last_toggle >= 10) {
        led_state = !led_state;
        if (led_state)
            PORTC |= (1 << LED2_PIN);
        else
            PORTC &= ~(1 << LED2_PIN);
        last_toggle = current_time;
    }
}
</syntaxhighlight>
**Différences clés :**
* Fréquence triplée (100 ms contre 300 ms) 
* Aucune dépendance fonctionnelle vis-à-vis de LED1 
* Même logique algorithmique 
=== Tâche de Surveillance (Status) ===
<syntaxhighlight lang="c">
void status_task(void* arg) {
    static uint32_t last_status = 0;
    static uint32_t status_count = 0;
    if (last_status == 0) {
        last_status = kernel_get_ticks();
        printf(">>> Status Task: Started\n");
        printf("\n=== DUAL LED BLINKING DEMO ===\n");
    }
    uint32_t current_time = kernel_get_ticks();
    if (current_time - last_status >= 10) {
        status_count++;
        uint8_t led1_state = (PORTC & (1 << LED1_PIN)) ? 1 : 0;
        uint8_t led2_state = (PORTC & (1 << LED2_PIN)) ? 1 : 0;
        printf("STATUS #%lu: Uptime: %lus | LED1: %s | LED2: %s\n",
              status_count,
              kernel_get_ticks() / 100,
              led1_state ? "ON " : "OFF",
              led2_state ? "ON " : "OFF");
        last_status = current_time;
    }
}
</syntaxhighlight>
== Séquence d'Initialisation ==
<syntaxhighlight lang="c">
int main(void) {
    MCUSR = 0;
    wdt_disable();
    uart_init();
    stdout = &uart_output;
    _delay_ms(100);
    printf("\n=== RTOS DUAL LED BLINKING DEMO ===\n");
    DDRC = 0x00;
    PORTC = 0x00;
    kernel_init();
    task_create("LED1", blink_led1_task, NULL, PRIORITY_HIGH, led1_stack);
    task_create("LED2", blink_led2_task, NULL, PRIORITY_HIGH, led2_stack);
    task_create("Status", status_task, NULL, PRIORITY_LOW, status_stack);
    kernel_start();
    return 0;
}
</syntaxhighlight>
== Modèle d'Ordonnancement ==
== Modèle d'Ordonnancement ==
L’ordonnanceur utilise un algorithme **round-robin coopératif** sans préemption.   
L’ordonnanceur utilise un algorithme **round-robin coopératif** sans préemption.   

Version du 25 novembre 2025 à 22:55

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



Firmware — RTOS

Firmware — Pico-OS (RTOS Préemptif)

Vue d'ensemble du système

Le firmware implémente un système d'exploitation temps réel (RTOS) préemptif complet pour l'ATmega328P. Contrairement à la version coopérative précédente, ce noyau utilise des interruptions matérielles pour forcer le changement de contexte entre les tâches, garantissant qu'une tâche lourde (comme lister des fichiers SD) ne bloque jamais les tâches critiques (comme clignoter une LED).

Caractéristiques principales :

  • Ordonnancement Préemptif : Commutation de contexte basée sur le Timer1 (100 Hz).
  • Priorités : Ordonnanceur Round-Robin avec niveaux de priorité (IDLE, LOW, MEDIUM, HIGH).
  • Système de Fichiers FAT16 : Lecture/Écriture complète, création/suppression de fichiers, support des scripts.
  • Shell Interactif : Interface ligne de commande via UART (115200 bauds).
  • Optimisation Extrême : Conçu pour fonctionner dans les 2 Ko de RAM de l'ATmega328p.

Architecture Logicielle

L'arborescence du projet est structurée pour séparer le noyau, les pilotes et le système de fichiers.

src/
 ├─ main.c                # Point d'entrée et définition des tâches utilisateur
 ├─ Makefile              # Système de compilation
 ├─ kernel/               # Cœur de l'OS
 │   ├─ kernel.c/h        # Initialisation et boucles principales
 │   ├─ scheduler.c/h     # Gestionnaire de tâches et ISR
 │   ├─ task.c/h          # Création de tâches et gestion de la pile
 │   └─ config.h          # Paramètres globaux (Fréquence, Taille piles)
 ├─ drivers/              # Pilotes Matériels
 │   ├─ uart.c/h          # Gestion du port série (Interruption RX)
 │   ├─ spi_bitbang.c/h   # SPI logiciel atomique
 │   └─ sd_card.c/h       # Protocole SD bas niveau (CMD/Response)
 ├─ fs/                   # Système de fichiers
 │   └─ fat16.c/h         # Implémentation FAT16 complète
 └─ shell/                # Interface Utilisateur
     └─ shell_core.c/h    # Interpréteur de commandes

Le Noyau (Kernel)

Commutation de Contexte (Context Switching)

Le cœur du système repose sur l'interruption du Timer 1. Nous utilisons une fonction `ISR_NAKED` pour avoir un contrôle total sur la pile (Stack).


Le mécanisme de préemption : 1. **Interruption :** Le Timer 1 se déclenche toutes les 10ms. 2. **Sauvegarde (Push) :** L'ISR empile manuellement les 32 registres généraux (`r0` à `r31`) et le registre d'état (`SREG`). 3. **Sauvegarde du SP :** Le pointeur de pile matériel (`SP`) est sauvegardé dans la structure de la tâche courante (`task->stack_ptr`). 4. **Ordonnancement :** La fonction C `scheduler_tick_preemptive()` choisit la prochaine tâche. 5. **Restauration (Pop) :** Le `SP` de la nouvelle tâche est chargé, puis ses registres sont dépilés. 6. **RETI :** L'instruction de retour restaure le compteur ordinal (PC), reprenant l'exécution de la nouvelle tâche exactement là où elle s'était arrêtée.

ISR(TIMER1_COMPA_vect, ISR_NAKED) {
    // 1. Sauvegarde du contexte (Assembleur inline)
    asm volatile(
        "push r0 \n in r0, __SREG__ \n push r0 \n" // Sauve SREG
        "push r1 \n push r2 \n ... \n push r31 \n" // Sauve R1-R31
    );
    
    // 2. Sauvegarde du Pointeur de Pile (SP) vers la Tâche Courante
    asm volatile(
        "lds r28, current_task_ptr \n" // Charge l'adresse du TCB
        "in r30, 0x3d \n"              // Lit SPL
        "std Y+4, r30 \n"              // Stocke dans TCB->stack_ptr
    );
    
    // 3. Appel de l'Ordonnanceur C
    asm volatile("call scheduler_tick_preemptive");
    
    // 4. Restauration du Pointeur de Pile depuis la Nouvelle Tâche
    // 5. Restauration du contexte (Pop)
    asm volatile("reti");
}

Gestion de la Mémoire et PROGMEM

L'ATmega328p ne dispose que de **2048 octets de RAM**. Avec un système de fichiers et un shell, la saturation mémoire est le principal danger.

Stratégies d'optimisation :

  • **Chaînes en Flash (PROGMEM) :** Tous les noms de tâches et les chaînes de caractères du Shell sont stockés en mémoire programme (Flash) pour épargner la RAM. Les fonctions comme `pgm_read_word` sont utilisées pour y accéder.
  • **Buffer Partagé :** Le pilote FAT16 utilise un unique buffer de 512 octets (`shared_buffer`) pour toutes les opérations (lecture MBR, FAT, Répertoire, Données), au lieu d'allouer plusieurs tampons.
  • **Piles Ajustées :** Chaque tâche possède une taille de pile spécifique définie à la création (`blink`: 64o, `shell`: 384o).

Système de Fichiers (FAT16)

Le pilote FAT16 a été écrit à la main pour supporter les opérations de lecture et d'écriture tout en minimisant l'empreinte mémoire.


Fonctionnalités

  • **Montage Dynamique :** Détection automatique du Master Boot Record (MBR) ou du format Superfloppy.
  • **Allocation Réelle :** Recherche de clusters libres dans la FAT pour l'écriture.
  • **Mise à jour Miroir :** Écriture simultanée dans `FAT1` et `FAT2` pour assurer la compatibilité avec Linux/Windows (évite les erreurs "Read-only file system").
  • **Noms 8.3 :** Conversion automatique des noms de fichiers (ex: `test.txt` → `TEST TXT`).

Écriture de Fichier (Create)

La fonction `fat16_create_file` effectue les opérations suivantes :

  1. Scanne la FAT pour trouver un cluster libre (marqué `0x0000`).
  2. Scanne le répertoire racine pour trouver une entrée libre.
  3. Écrit les métadonnées du fichier (Nom, Taille, Cluster de départ) dans le répertoire.
  4. Écrit le contenu du fichier dans le secteur de données correspondant.
  5. Met à jour la FAT pour marquer le cluster comme "Fin de fichier" (`0xFFFF`).

Pilotes Matériels (Drivers)

SPI Atomique

Le pilote SPI (`spi_bitbang.c`) est critique dans un système préemptif.

  • **Problème :** Si l'ordonnanceur interrompt l'envoi d'un octet SPI, l'horloge (SCK) peut rester à l'état haut/bas pendant 10ms. Certaines cartes SD interprètent cela comme un timeout ou une erreur.
  • **Solution :** Utilisation de blocs atomiques. Les interruptions sont désactivées (`cli()`) juste avant d'envoyer les 8 bits d'un octet, et réactivées (`SREG = sreg`) immédiatement après.
uint8_t spi_transfer(uint8_t data) {
    uint8_t sreg = SREG;
    cli(); // DÉBUT SECTION CRITIQUE
    
    // Bit-banging rapide (quelques microsecondes)
    for (int i=7; i>=0; i--) { ... }
    
    SREG = sreg; // FIN SECTION CRITIQUE
    return received;
}

Shell et Scripting

Le Shell (`shell_core.c`) permet l'interaction utilisateur. Il supporte l'exécution de scripts via la commande `exec`.

Commande EXEC : La commande `exec SCRIPT.TXT` :

  1. Utilise `fat16_read_to_buffer` pour charger le contenu du fichier texte dans la RAM.
  2. Passe ce buffer à l'interpréteur de commandes `shell_process_line`.
  3. Cela permet d'automatiser des séquences de démarrage ou de test.

Guide d'utilisation

Commandes disponibles

Commande Description
`ps` Affiche la liste des tâches, leur état et l'utilisation CPU.
`list` Affiche les fichiers présents sur la carte SD avec leur taille.
`type <fichier>` Affiche le contenu d'un fichier texte.
`create <nom> <txt>` Crée un nouveau fichier contenant le texte spécifié.
`del <nom>` Supprime un fichier (marque comme supprimé).
`exec <script>` Exécute le contenu d'un fichier comme une commande.
`free` Affiche une estimation de la RAM libre.
`reboot` Redémarre le microcontrôleur.

Exemple de Session

Pico> create auto.bat version
Fichier cree.
Pico> exec auto.bat
Exec: auto.bat
Run: version
Pico-OS v1.2 (Dual FAT + Exec)

Modèle d'Ordonnancement

L’ordonnanceur utilise un algorithme **round-robin coopératif** sans préemption. Chaque tâche s’exécute jusqu’à son retour, puis la suivante prend le relais.