« SE4Binome2025-2 » : différence entre les versions
Aucun résumé des modifications |
Aucun résumé des modifications |
||
| Ligne 303 : | Ligne 303 : | ||
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. | 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 ==== | ==== Fonctionnement global du bus SPI ==== | ||
| Ligne 311 : | Ligne 312 : | ||
* SCK : Serial Clock | * SCK : Serial Clock | ||
* SS : Slave Slect | * SS : Slave Slect | ||
[[Fichier:SPI timing diagram.png|vignette|400x400px|SPI timing diagram]] | |||
===== Déroulement d’une communication SPI ===== | ===== Déroulement d’une communication SPI ===== | ||
Version du 7 décembre 2025 à 19:57
Objectif
L'objectif du projet est de concevoir un pico-ordinateur complet, intégrant :
- Une carte mère basée sue le microcontrôleur AT90USB1286
Une partie logicielle permettant l'éxecution de de commandes telles que ls, cp ou mv
Shield Arduino
Une première étape du projet a consisté à développer un shield pour Aduino uno, servant de plateforme de test et de développement pour les cartes filles SPI.
Fonctionalités:
- Connexion de 5 périphériques SPI via des cartes filles.
- Gestion des signaux Reset et Interruption.
- Ajout d'une mémoire externe carte micro-SD via un connecteur Molex 10431.
- Adaptation des niveaux logiques (5V a 3,3V) grâce à la puce 74LV125.
Ce shield joue le rôle de plateforme de développement temporaire, en attendant la carte mère du pico-ordinateur.
Schématique et routage
Objectif
Carte mère
Schématique
Firmware — Pico-OS (RTOS Préemptif)
Modèle d'Ordonnancement
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 :
- **Interruption :** Le Timer 1 se déclenche toutes les 10ms.
- **Sauvegarde (Push) :** L'ISR empile manuellement les 32 registres généraux (`r0` à `r31`) et le registre d'état (`SREG`).
- **Sauvegarde du SP :** Le pointeur de pile matériel (`SP`) est sauvegardé dans la structure de la tâche courante (`task->stack_ptr`).
- **Ordonnancement :** La fonction C `scheduler_tick_preemptive()` choisit la prochaine tâche.
- **Restauration (Pop) :** Le `SP` de la nouvelle tâche est chargé, puis ses registres sont dépilés.
- **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 des Tâches et Ordonnanceur
Le cœur de Pico-OS repose sur un système multitâche préemptif conçu pour fonctionner efficacement sur des microcontrôleurs à ressources limitées comme l'ATmega328P. Cette section détaille les mécanismes de création de tâches, leurs états, et le fonctionnement de l'ordonnanceur.
Les Tâches (Tasks)
Une tâche dans Pico-OS est une unité d'exécution indépendante, représentée par une fonction C standard. Chaque tâche possède son propre contexte d'exécution (registres CPU, pointeur de pile) et sa propre pile (stack).
Structure de Contrôle de Tâche (TCB)
Chaque tâche est décrite par une structure `task_t` (Task Control Block), définie dans `kernel/task.h`. Cette structure contient toutes les informations nécessaires à la gestion de la tâche par le noyau.
typedef struct task_control_block {
void (*function)(void*); // Pointeur vers la fonction de la tâche
void* arg; // Argument passé à la tâche
uint8_t* stack_ptr; // Pointeur de pile courant (sauvegardé lors d'un changement de contexte)
uint8_t* stack_base; // Adresse de base de la pile allouée
uint16_t stack_size; // Taille de la pile en octets
task_state_t state; // État actuel de la tâche (READY, RUNNING, SLEEPING...)
uint16_t sleep_ticks; // Compteur pour le sommeil (en ticks système)
uint8_t priority; // Priorité de la tâche (0 = IDLE, 1 = LOW, ..., 3 = HIGH)
char name[TASK_NAME_LENGTH]; // Nom de la tâche (pour le débogage et l'affichage 'ps')
} task_t;
États d'une Tâche
Une tâche peut se trouver dans l'un des états suivants, définis par l'énumération `task_state_t` :
- TASK_READY (0) : La tâche est prête à être exécutée mais attend que l'ordonnanceur lui alloue le processeur.
- TASK_RUNNING (1) : La tâche est actuellement en cours d'exécution sur le processeur.
- TASK_SLEEPING (2) : La tâche est en attente pour une durée déterminée (appel à `task_sleep()`). Elle ne sera pas sélectionnée par l'ordonnanceur tant que son délai n'est pas écoulé.
- TASK_BLOCKED (5) : La tâche est bloquée en attente d'un événement (non temporel), comme la disponibilité d'une ressource (par ex. sémaphore, non implémenté dans la version de base).
- TASK_COMPLETED (6) : La tâche a terminé son exécution (retour de la fonction ou appel à `task_exit()`). Elle ne sera plus jamais planifiée.
Création d'une Tâche
La création d'une tâche se fait via la fonction `task_create()`. Cette fonction initialise le TCB et prépare la pile de la tâche pour qu'elle puisse démarrer correctement lors de son premier ordonnancement.
Processus de création :
- Allocation du TCB : Le noyau recherche un emplacement libre dans le tableau global `task_table`.
- Initialisation du TCB : Les champs du TCB (fonction, argument, priorité, état, nom) sont remplis. Le nom est copié depuis la mémoire Flash (PROGMEM) vers la RAM du TCB.
- Préparation de la Pile (Stack Frame) : C'est l'étape critique. La fonction `task_create_context()` simule l'état de la pile tel qu'il serait après une interruption. Elle empile :
- L'adresse de retour (PC) pointant vers le début de la fonction de la tâche.
- Le registre d'état `SREG` avec le bit d'interruption globale (I) activé, pour que les interruptions soient autorisées dès le lancement de la tâche.
- Des valeurs initiales (souvent 0) pour tous les registres généraux (R0-R31), afin d'assurer un état connu.
- Enregistrement : La tâche est marquée comme `TASK_READY` et le compteur de tâches `task_count` est incrémenté.
// Exemple de création de tâche dans main.c
task_create(name_blink, blink_task, NULL, 2, blink_task_stack, sizeof(blink_task_stack));
L'Ordonnanceur (Scheduler)
L'ordonnanceur est le composant du noyau responsable de distribuer le temps processeur entre les différentes tâches prêtes (`TASK_READY`). Pico-OS utilise un ordonnanceur préemptif à priorités fixes avec round-robin.
Fonctionnement Préemptif
La préemption est assurée par le Timer 1 de l'ATmega328P, configuré pour générer une interruption périodique (tick système) à une fréquence définie (par défaut 100 Hz, soit toutes les 10 ms).
L'interruption Timer1 déclenche la routine de service d'interruption (ISR) `TIMER1_COMPA_vect`, déclarée avec l'attribut `ISR_NAKED`. Cet attribut indique au compilateur de ne générer aucun code de sauvegarde/restauration automatique (prologue/épilogue), laissant cette responsabilité entièrement au code assembleur intégré dans l'ISR.
Déroulement d'une commutation de contexte (Context Switch) :
- Interruption : Le Timer1 expire, l'exécution courante est suspendue, et le CPU saute au vecteur d'interruption.
- Sauvegarde du Contexte : L'ISR empile manuellement tous les registres (R0-R31) et le registre d'état (SREG) sur la pile de la tâche en cours.
- Sauvegarde du Pointeur de Pile : La valeur actuelle du pointeur de pile matériel (SP) est lue et sauvegardée dans le champ `stack_ptr` du TCB de la tâche courante.
- Appel de l'Ordonnanceur C : La fonction `scheduler_tick_preemptive()` est appelée. Elle :
- Incrémente le temps système (`system_ticks`).
- Met à jour les compteurs de sommeil des tâches `TASK_SLEEPING`. Si un compteur atteint 0, la tâche passe à `TASK_READY`.
- Sélectionne la prochaine tâche à exécuter (`next_task`) en utilisant l'algorithme de choix.
- Met à jour le pointeur global `current_task_ptr` vers le TCB de la nouvelle tâche.
- Restauration du Pointeur de Pile : L'ISR lit la valeur de `stack_ptr` depuis le TCB de la nouvelle tâche et met à jour le pointeur de pile matériel (SP).
- Restauration du Contexte : Les registres (R0-R31, SREG) sont dépilés depuis la pile de la nouvelle tâche.
- Retour d'Interruption (RETI) : L'instruction `reti` dépile le compteur ordinal (PC), transférant ainsi le contrôle à la nouvelle tâche, exactement là où elle s'était arrêtée (ou au début de sa fonction si c'est sa première exécution).
Algorithme de Sélection (Scheduling Policy)
La fonction `get_next_ready_task()` implémente la politique de choix de la prochaine tâche :
- Elle parcourt le tableau des tâches `task_table` de manière circulaire (Round-Robin), en commençant après la tâche courante.
- Elle cherche la première tâche valide (fonction non nulle) dont l'état est `TASK_READY`.
- Si aucune autre tâche n'est prête, elle sélectionne la tâche "Idle" (toujours prête, priorité 0).
- Gestion des Priorités : Bien que l'implémentation actuelle soit un Round-Robin simple, la structure permet d'évoluer vers une sélection stricte par priorité (exécuter la tâche prête de plus haute priorité). Dans la version actuelle, l'ordre dans le tableau et le parcours séquentiel offrent un partage du temps simple.
Tâche Idle
Le système crée toujours une tâche spéciale nommée "idle" au démarrage. Cette tâche a la priorité la plus basse et ne fait rien (boucle infinie `while(1);`). Elle garantit qu'il y a toujours au moins une tâche prête à exécuter, évitant ainsi un plantage de l'ordonnanceur si toutes les tâches utilisateur sont endormies ou bloquées.
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 :
- Scanne la FAT pour trouver un cluster libre (marqué `0x0000`).
- Scanne le répertoire racine pour trouver une entrée libre.
- Écrit les métadonnées du fichier (Nom, Taille, Cluster de départ) dans le répertoire.
- Écrit le contenu du fichier dans le secteur de données correspondant.
- 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` :
- 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`.
- 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)
FPGA
Controleur bus SPI
Cette partie consiste à réaliser une interface SPI en mode 1 sur une carte FPGA Basys 3, configurée en tant qu’esclave. La communication est gérée à l’aide d’une machine à états finis (FSM) en VHDL, permettant de synchroniser et traiter correctement les signaux du protocole SPI.
Le maître SPI est un microcontrôleur ATmega328p, configuré pour transmettre en mode 1. Les données envoyées au microcontrôleur via l’UART (à partir d’un terminal Minicom) sont ensuite transférées vers la Basys 3 par le bus SPI.
Pour la visualisation, chaque octet reçu par l’esclave FPGA est affiché directement sur 8 LED de la carte. Les LED se mettent automatiquement à jour à chaque réception d’un nouvel octet.
Fonctionnement global du bus SPI
Le protocole SPI repose sur 4 signaux :
- MOSI : Master Out Slave In
- MISO : Master In Slave Out
- SCK : Serial Clock
- SS : Slave Slect
Déroulement d’une communication SPI
- Le maître active l'esclave en mettant la ligne SS à l'état bas.
- L'horloge SCK se met à osciller et chaque front d'horloge permet la transmission d'un bit.
- Envoie des données :
- Le maître envoie un bit sur MOSI
- L'esclave repond sur MISO
- Une fois la communication terminée, le maître désactive l'esclave en mettant SS à l'état haut.
Modes SPI
SPI comporte 4 modes, définis par la polarité et la phase de l’horloge (CPOL et CPHA). Ces paramètres déterminent les instants où les données sont valides.
En mode 1, la configuration du bus SPI est la suivante :
- CPOL = 0 : horloge au niveau bas au repos
- CPHA = 1 :
- Setup sur le front montant
- Sample sur le front descendant
