SE4Binome2025-2
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 — RTOS
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).
Architecture coopérative vs préemptive :
- Coopératif : Les tâches cèdent volontairement le contrôle (via
task_yield(),task_sleep()) - 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 :
- Allocation statique uniquement (pas de malloc/free)
- Taille fixe des structures pour prédictibilité
- Piles partagées entre noyau et applications
Arborescence (extrait)
firmware/ ├─ kernel/ # Cœur du système d'exploitation │ ├─ kernel.h # API publique du noyau │ ├─ kernel.c # Gestion système (ticks, délais) │ ├─ scheduler.h # Déclarations de l'ordonnanceur │ ├─ scheduler.c # Implémentation round-robin │ ├─ task.h # Structures de contrôle des tâches │ └─ task.c # Création et gestion des tâches ├─ config.h # Configuration matérielle (mémoire, timers) └─ main.c # Tâches applicatives (horloge binaire)
Séparation des responsabilités :
kernel/: Fonctionnalités système bas niveauconfig.h: Adaptation au matériel spécifiquemain.c: Logique métier de l'application
Paramètres de configuration
#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
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)
typedef enum {
TASK_INVALID = 0,
TASK_READY,
TASK_RUNNING,
TASK_SLEEPING
} task_state_t;
typedef struct {
void (*function)(void *);
void *arg;
uint8_t *stack_base;
task_state_t state;
uint16_t sleep_ticks;
uint8_t priority;
char name[TASK_NAME_LEN];
} task_t;
Cycle de vie d'une tâche :
CRÉATION → [READY] ↔ [RUNNING] → [SLEEPING] → [READY]
↓
[COMPLETED] (si task_exit() appelée)
Gestion des sections critiques (imbriquées)
#include <avr/interrupt.h>
static volatile uint8_t critical_nesting = 0;
static inline void enter_critical(void) {
cli();
critical_nesting++;
}
static inline void leave_critical(void) {
if (critical_nesting > 0) {
critical_nesting--;
if (critical_nesting == 0) sei();
}
}
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
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++) {
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;
}
Contraintes :
- Appelée avant
scheduler_start() - Piles statiques uniquement
- Noms tronqués à TASK_NAME_LEN
Ordonnanceur (round-robin)
static uint8_t current_task_id = 0;
static inline uint8_t is_task_valid(uint8_t id) {
return (id < MAX_TASKS) &&
(task_table[id].function != NULL) &&
(task_table[id].state == TASK_READY);
}
static uint8_t get_next_task(void) {
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();
}
Algorithme round-robin :
- Parcours circulaire équitable
- Recherche O(n)
- Aucune sauvegarde de contexte (coopératif)
Mécanismes de sommeil et tick système
// Configuration du timer hardware pour 100Hz
void timer1_init_100hz(void) {
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);
OCR1A = 2499;
TIMSK1 |= (1 << OCIE1A);
}
ISR(TIMER1_COMPA_vect) {
extern volatile uint32_t system_ticks;
system_ticks++;
for (uint8_t i = 0; i < MAX_TASKS; i++) {
if (task_table[i].state == TASK_SLEEPING && task_table[i].sleep_ticks > 0) {
task_table[i].sleep_ticks--;
if (task_table[i].sleep_ticks == 0)
task_table[i].state = TASK_READY;
}
}
}
void task_sleep(uint16_t ticks) {
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();
}
Gestion du temps :
- Timer hardware 100Hz → tick 10ms
- Pas de busy-waiting
- Réveil automatique via ISR
Initialisation du système
void kernel_init(void) {
memset(task_table, 0, sizeof(task_table));
current_task_id = 0;
task_count = 0;
system_ticks = 0;
timer1_init_100hz();
uart_init();
io_init();
task_create("idle", idle_task, NULL, PRIORITY_IDLE, idle_stack);
sei();
}
Séquence de boot :
- Réinitialisation
- Configuration hardware
- Création tâche idle
- Activation interruptions
Gestion d'erreurs
Stratégies de robustesse :
- Validation des pointeurs
- Sleep minimal
- Tâche idle toujours active
if (function == NULL || stack_buffer == NULL) {
return -1;
}
Estimations mémoire
Piles tâches : 4 × 96 octets = 384 octets Structures TCB : 4 × 28 octets = 112 octets Variables globales: ~20 octets TOTAL estimé : 516 / 2048 (25%)
Intégration du build system
MCU = atmega328p
F_CPU = 16000000UL
CFLAGS = -mmcu=$(MCU) \
-DF_CPU=$(F_CPU) \
-Os -Wall -std=c99
all: firmware.hex
flash: firmware.hex
clean:
Comportements observables
kernel_init();
task_create("task1", ...);
task_create("task2", ...);
kernel_start();
void ma_tache(void* arg) {
while (1) {
faire_travail();
task_sleep(100);
}
}
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
#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
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
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
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
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;
}
}
- 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
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;
}
}
- 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)
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;
}
}
Séquence d'Initialisation
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;
}
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.