« 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
 
(24 versions intermédiaires par 3 utilisateurs non affichées)
Ligne 28 : Ligne 28 :


=== Schématique ===
=== Schématique ===
[[Fichier:Schéma cm pico.png|centré|vignette|500x500px|Schéma carte mère du pico ordinateur]]


[[Fichier:Schema carte mere.jpg|gauche|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]]


<br clear="all" />
= Firmware — Pico-OS (RTOS Préemptif) =
= Firmware — RTOS =


== 1. Vue d'ensemble du système ==
== Modèle d'Ordonnancement ==
Le firmware implémente un '''système d'exploitation temps réel (RTOS) coopératif''' optimisé pour les microcontrôleurs AVR (ATmega328P/AT90USB1286).


'''Architecture coopérative vs préemptive :'''
* '''Coopératif''' : Les tâches cèdent volontairement le contrôle (via <code>task_yield()</code>, <code>task_sleep()</code>)
* '''Avantage''' : Plus simple, moins de surcharge mémoire, pas besoin de sauvegarde de contexte
* '''Inconvénient''' : Une tâche malveillante peut bloquer le système


'''Optimisations mémoire :'''
[[Fichier:Ordonnanceur en action.mp4|vignette]]
* Allocation statique uniquement (pas de malloc/free)
 
* Taille fixe des structures pour prédictibilité
== Vue d'ensemble du système ==
* Piles partagées entre noyau et applications
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.


== 2. 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
=== Commutation de Contexte (Context Switching) ===
* <code>main.c</code> : Logique métier de l'application
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.


== 3. Paramètres de configuration ==
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
#define MAX_TASKS      4      // Compromis fonctionnalité/mémoire
ISR(TIMER1_COMPA_vect, ISR_NAKED) {
#define STACK_SIZE      96     // Suffisant pour appels de fonctions + variables locales
    // 1. Sauvegarde du contexte (Assembleur inline)
#define TICK_FREQUENCY  100    // 100Hz = résolution 10ms (équilibre précision/charge CPU)
    asm volatile(
#define TASK_NAME_LEN  8      // Noms courts pour économiser la RAM
        "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");
}
</syntaxhighlight>
</syntaxhighlight>


'''Justification des valeurs :'''
== Gestion des Tâches et Ordonnanceur ==
* '''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é
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.
* '''TICK_FREQUENCY=100''' : Temps de réponse <10ms sans surcharger le CPU
 
=== 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.


== 4. Structure de contrôle de tâche (TCB) ==
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
typedef enum {
typedef struct task_control_block {
    TASK_INVALID = 0,
     void (*function)(void*);       // Pointeur vers la fonction de la tâche
    TASK_READY,
     void* arg;                     // Argument passé à la tâche
    TASK_RUNNING,
    uint8_t* stack_ptr;            // Pointeur de pile courant (sauvegardé lors d'un changement de contexte)
    TASK_SLEEPING
     uint8_t* stack_base;           // Adresse de base de la pile allouée
} task_state_t;
    uint16_t stack_size;          // Taille de la pile en octets
 
     task_state_t state;           // État actuel de la tâche (READY, RUNNING, SLEEPING...)
typedef struct {
     uint16_t sleep_ticks;         // Compteur pour le sommeil (en ticks système)
     void (*function)(void *);
     uint8_t priority;             // Priorité de la tâche (0 = IDLE, 1 = LOW, ..., 3 = HIGH)
     void *arg;
     char name[TASK_NAME_LENGTH];   // Nom de la tâche (pour le débogage et l'affichage 'ps')
     uint8_t *stack_base;
     task_state_t state;
     uint16_t sleep_ticks;
     uint8_t priority;
     char name[TASK_NAME_LEN];
} task_t;
} task_t;
</syntaxhighlight>
</syntaxhighlight>


'''Cycle de vie d'une tâche :'''
==== États d'une Tâche ====
<pre>
 
CRÉATION → [READY] ↔ [RUNNING] → [SLEEPING] → [READY]
Une tâche peut se trouver dans l'un des états suivants, définis par l'énumération `task_state_t` :
                    ↓
                [COMPLETED] (si task_exit() appelée)
</pre>


== 5. Gestion des sections critiques (imbriquées) ==
* '''TASK_READY (0)''' : La tâche est prête à être exécutée mais attend que l'ordonnanceur lui alloue le processeur.
<syntaxhighlight lang="c">
* '''TASK_RUNNING (1)''' : La tâche est actuellement en cours d'exécution sur le processeur.
#include <avr/interrupt.h>
* '''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é.
static volatile uint8_t critical_nesting = 0;
* '''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.


static inline void enter_critical(void) {
[[Fichier:state.png|cadre|centré|600px|alt=Diagramme des états des tâches|Cycle de vie d'une tâche dans Pico-OS]]
    cli();
    critical_nesting++;
}


static inline void leave_critical(void) {
==== Création d'une Tâche ====
    if (critical_nesting > 0) {
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.
        critical_nesting--;
        if (critical_nesting == 0) sei();
    }
}
</syntaxhighlight>


'''Protection des ressources partagées :'''
'''Processus de création :'''
* Empêche les accès concurrents aux structures partagées (task_table, variables globales)
#  '''Allocation du TCB''' : Le noyau recherche un emplacement libre dans le tableau global `task_table`.
* '''Imbrication''' : Appels imbriqués autorisés
#  '''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.
* '''Performance''' : Sections critiques très courtes (<1ms)
#  '''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é.


== 6. Création de tâche ==
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
int8_t task_create(const char *name, void (*function)(void *), void *arg,
// Exemple de création de tâche dans main.c
                  uint8_t priority, uint8_t *stack_buffer)
task_create(name_blink, blink_task, NULL, 2, blink_task_stack, sizeof(blink_task_stack));
{
    enter_critical();
   
    for (uint8_t i = 0; i < MAX_TASKS; i++) {
        if (task_table[i].function == NULL) {
            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();
    return -1;
}
</syntaxhighlight>
</syntaxhighlight>


'''Contraintes :'''
=== L'Ordonnanceur (Scheduler) ===
* Appelée avant <code>scheduler_start()</code>
 
* Piles statiques uniquement
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.
* Noms tronqués à TASK_NAME_LEN
 
==== 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).


== 7. Ordonnanceur (round-robin) ==
==== Algorithme de Sélection (Scheduling Policy) ====
<syntaxhighlight lang="c">
 
static uint8_t current_task_id = 0;
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).


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)


== 8. 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.


== 9. 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
Le Shell (`shell_core.c`) permet l'interaction utilisateur. Il supporte l'exécution de scripts via la commande `exec`.
# Création tâche idle
 
# Activation interruptions
'''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.


== 10. Gestion d'erreurs ==
== Guide d'utilisation ==
'''Stratégies de robustesse :'''
* Validation des pointeurs
* Sleep minimal
* Tâche idle toujours active


<syntaxhighlight lang="c">
=== Commandes disponibles ===
if (function == NULL || stack_buffer == NULL) {
{| class="wikitable"
    return -1;
! Commande !! Description
}
|-
</syntaxhighlight>
| `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.
|}


== 11. 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>


== 12. Intégration du build system ==
== FPGA ==
<syntaxhighlight lang="makefile">
 
MCU = atmega328p
=== Controleur bus SPI ===
F_CPU = 16000000UL
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 =====


CFLAGS = -mmcu=$(MCU) \
# Le maître active l'esclave en mettant la ligne SS à l'état bas.
        -DF_CPU=$(F_CPU) \
# L'horloge SCK se met à osciller et chaque front d'horloge permet la transmission d'un bit.
        -Os -Wall -std=c99
# 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]]


all: firmware.hex
===== Modes SPI =====
flash: firmware.hex
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.
clean:
 
</syntaxhighlight>
En mode 1, la configuration du bus SPI est la suivante :
 
* CPOL = 0 : horloge au niveau bas au repos
* CPHA = 1 :
** Setup sur le front montant
** Sample sur le front descendant
 
==== Programmation VHDL ====
La conception du bus SPI repose sur une machine a états finis (FSM) qui gère les différentes phases de la communication :
 
* Idle : Attente de l'activation de la ligne SS
* Load : Détecte des fronts montants d'horloge et valide l'octet reçu
* Read : Recopie l'état du MOSI dans le registre de data reçu
* Write : Recopie l'état du MOSI sur le MISO (Pour débuger via minicom)
* Wait_next : Réinitialise le compteur de bit à 0 et attend le prochain frond montant d'horloge ou la fin de la communication SS a l'état bas
 
==== Démonstration & Code ====
[https://youtu.be/0PwL7dsQLX0?si=8NLZHlEpWFk4qZNR Vidéo Compteur SPI 0 à 255]
 
https://gitea.plil.fr/bcheklat/SE4-Pico-B2/src/branch/main/SPI_BUS
 
=== Contrôleur d'écran VGA ===
Le contrôleur d’écran VGA a pour objectif d’assurer l’affichage graphique des données reçues via le bus SPI sur un écran VGA connecté à la carte FPGA '''Basys 3'''.
 
Les caractères envoyés par le microcontrôleur (via UART puis SPI) sont interprétés, convertis en bitmaps et stockés dans une mémoire vidéo interne, dont le contenu est balayé en continu par le contrôleur VGA.
 
L’architecture permet l’affichage de texte en temps réel, avec gestion de l’écriture, de l’effacement et du nettoyage complet de l’écran.
 
==== Architecture globale ====
Le contrôleur VGA est composé des blocs suivants :
 
* Générateur d’horloge vidéo
* Contrôleur de timing VGA
* Mémoire vidéo (RAM)
* ROM de caractères
* Décodeur de commandes ASCII
* Interface SPI esclave
 
==== Génération de l’horloge vidéo ====
La carte Basys 3 fournit une horloge principale à 100 MHz.
 
Un bloc '''Clock Wizard''' est utilisé afin de générer une horloge adaptée au timing VGA (65Mhz).
 
==== Timing VGA et balayage de l’écran ====
Le contrôleur VGA repose sur deux compteurs :
 
* Compteur horizontal  <code>x_pixel_counter</code>
* Compteur vertical  <code>y_pixel_counter</code>
 
Ces compteurs définissent la position courante du pixel à afficher.
 
Le timing est conforme à une résolution '''1024 × 768 à 60 Hz''', incluant :
 
* zone visible
* front porch
* impulsions de synchronisation
* back porch
 
Les signaux :
 
* HS (Horizontal Sync)
* VS (Vertical Sync)
 
sont générés directement à partir des valeurs des compteurs, conformément aux spécifications VGA.
 
==== Zone d’affichage utile ====
L’affichage du contenu graphique est centré à l’écran grâce à l’utilisation d’offsets
 
* <code>offset_x = 352</code>
* <code>offset_y = 272</code>
 
La zone utile correspond à une surface de '''320 × 224 pixels''', utilisée pour l’affichage du texte.
 
En dehors de cette zone, les signaux RGB sont forcés à zéro afin d’obtenir un fond noir.
 
==== Mémoire vidéo (RAM) ====
La mémoire vidéo est implémentée sous forme d’une RAM interne de '''8960 octets''' (40 × 224), chaque octet représentant '''8 pixels horizontaux'''.
 
* '''Écriture''' : pilotée par le décodeur de commandes
* '''Lecture''' : réalisée en continu par le contrôleur VGA
 
L’adresse de lecture est calculée à partir de la position du pixel courant :
 
<code>adresse = (y_pixel × 40) + (x_pixel / 8)</code>
 
Chaque bit de l’octet lu correspond à l’état d’un pixel (1 = pixel allumé, 0 = pixel éteint).
 
==== ROM de caractères ====
La ROM contient les bitmaps des caractères ASCII sous forme de matrices '''8 × 8 pixels'''.
 
Chaque caractère est défini par :
 
* un '''index ASCII'''
* un '''sous-index''' correspondant à la ligne du caractère (0 à 7)
 
Cette ROM permet de convertir les caractères reçus via SPI en données graphiques exploitables par la RAM vidéo.
 
==== Décodeur de commandes ====
Le décodeur reçoit les caractères ASCII transmis par le bus SPI et gère leur affichage à l’écran.
Fonctionnalités prises en charge :
 
* écriture séquentielle des caractères
* retour à la ligne automatique
* suppression du dernier caractère (Backspace)
* effacement complet de l’écran (Delete)
 
Le décodeur traduit chaque caractère en :
 
* adresses mémoire
* indices de caractères
* signaux de contrôle d’écriture
 
Chaque caractère est converti en 8 lignes de pixels, stockées consécutivement en mémoire vidéo.
 
==== Génération des signaux RGB ====
Pour chaque pixel situé dans la zone visible :
 
* le bit correspondant est extrait de l’octet lu en mémoire
* ce bit est appliqué simultanément aux sorties '''R''', '''G''' et '''B'''
 
L’affichage est donc réalisé en '''monochrome''', avec des pixels blancs sur fond noir.
 
==== Synchronisation avec le bus SPI ====
Le contrôleur VGA fonctionne indépendamment du bus SPI.
 
Les données reçues via SPI sont traitées par le décodeur, qui met à jour la mémoire vidéo sans interrompre le balayage de l’écran.
 
==== Démonstration & Code ====
[https://youtu.be/2RDaKoCX-So Démonstration contrôleur d’écran VGA]
 
https://gitea.plil.fr/bcheklat/SE4-Pico-B2/src/branch/main/vga
 
=== Bitcoin Miner ===
'''Objectif du projet :''' Développer un système complet de minage Bitcoin sur carte FPGA Zybo Z7-10 en exploitant l'architecture (Processeur + FPGA).
 
==== Architecture du système ====
'''Partie Processeur (ARM Cortex-A9) :'''
 
* Implémentation d'un "Mining client" en C
* Connexion réseau aux serveurs de minage
* Récupération et décodage des "jobs" de minage
* Configuration des registres FPGA via le bus AXI
* Transmission des résultats valides au serveur
 
'''Partie FPGA  :'''
 
* Accélérateur matériel SHA-256 optimisé
* Calcul massivement parallèle des nonces
* Interface de contrôle via registres mémoire-mappés
* Signalisation par interruption au processeur
 
'''Fonctionnement :'''
 
# Le processeur se connecte au "Mining pool server" et reçoit un bloc de travail
# Le processeur extrait l'en-tête de bloc (80 octets) et le prépare pour le FPGA
# Transfert des données vers le FPGA via le bus AXI et démarrage du calcul
# Le FPGA signale immédiatement toute solution valide (hash < cible)
# Le processeur soumet la solution au réseau Bitcoin
 
==== Première implémentation d'un coeur SHA256 ====
dans un premier temps j'ai conçu et implémenté un accélérateur matériel SHA-256 en VHDL pour optimiser les calculs de hachage nécessaires au minage de Bitcoin.
 
Architecture du coeur SHA-256 :
 
* Signal <code>start</code> pour lancer le calcul, <code>done</code> pour indiquer la fin, et bus de sortie 256 bits pour le hash.
* Exécute les 64 tours de compression SHA-256 en 64 cycles d'horloge
* Utilise des opérations bit-à-bit parallèles pour maximiser la vitesse
 
Fonctionnement algorithmique :
 
* Charge les constantes initiales H₀ à H₇, K<sub>0</sub><sup>{256}</sup> à K<sub>63</sub><sup>{256}</sup> définies par la norme SHA-256.
* Génère les 64 mots W[t] à partir du bloc de message de 512 bits
* Applique les fonctions logiques SHA-256 (Ch, Maj, Σ₀, Σ₁) sur 64 tours
* Additionne les valeurs intermédiaires pour produire le hash final
 
Fonctions cryptographiques implémentées :
 
* <code>Ch(x, y, z) = (x ∧ y) ⊕ (¬x ∧ z)</code>
* <code>Maj(x, y, z) = (x ∧ y) ⊕ (x ∧ z) ⊕ (y ∧ z)</code>
* <code>Σ₀<sup>{256}</sup>(x) = ''ROTR<sup>2</sup>''(x) ⊕ ''ROTR<sup>13</sup>''(x) ⊕ ''ROTR<sup>22</sup>''(x)</code>
* <code>Σ₁<sup>{256}</sup>(x)= ''ROTR<sup>6</sup>''(x) ⊕ ''ROTR<sup>11</sup>''(x) ⊕ ''ROTR<sup>25</sup>''(x)</code>
* <code>σ₀<sup>{256}</sup>(x) = ''ROTR<sup>7</sup>''(x) ⊕ ''ROTR<sup>18</sup>''(x) ⊕ SHR<sup>3</sup>(x)</code>
* <code>σ₁<sup>{256}</sup>(x) = ''ROTR<sup>17</sup>''(x) ⊕ ''ROTR<sup>19</sup>''(x) ⊕ SHR<sup>10</sup>(x)</code>
'''Simulation'''
[[Fichier:Message.png|gauche|vignette|Message]]
[[Fichier:Message hash.png|vignette|869x869px|SHA256 du message]]


== 13. Comportements observables ==
<br>
<syntaxhighlight lang="c">
<br>
kernel_init();
<br>
task_create("task1", ...);
<br>
task_create("task2", ...);
<br>
kernel_start();
<br>
</syntaxhighlight>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>


<syntaxhighlight lang="c">
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.
void ma_tache(void* arg) {
    while (1) {
        faire_travail();
        task_sleep(100);
    }
}
</syntaxhighlight>


== 14. Extensions possibles ==
==== Annexe ====
* Ordonnancement préemptif
* Sémaphores / mutex
* Files de messages
* Gestion d’énergie


'''Ce RTOS démontre :'''
* [https://csrc.nist.gov/files/pubs/fips/180-2/final/docs/fips180-2.pdf SECURE HASH STANDARD (Documentation SHA256 & fonctions)]
* Les principes fondamentaux des systèmes d’exploitation embarqués
* [https://sha256algorithm.com/ Visualition algorithme SHA256]
* L’optimisation pour contraintes sévères (2KB RAM)
* La coopération entre tâches sans protection matérielle
* La gestion du temps réel à 10ms

Version actuelle datée du 23 décembre 2025 à 23:25

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

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 :

  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 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.
Diagramme des états des tâches
Cycle de vie d'une tâche dans Pico-OS

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 :

  1. Allocation du TCB : Le noyau recherche un emplacement libre dans le tableau global `task_table`.
  2. 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.
  3. 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.
  4. 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) :

  1. Interruption : Le Timer1 expire, l'exécution courante est suspendue, et le CPU saute au vecteur d'interruption.
  2. 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.
  3. 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.
  4. 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.
  5. 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).
  6. Restauration du Contexte : Les registres (R0-R31, SREG) sont dépilés depuis la pile de la nouvelle tâche.
  7. 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 :

  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)

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