Option SIN - Systèmes d'Information et Numérique

Programmation Embarquée Avancée

Niveau Terminale — Cours avancé

Objectifs du cours

  • Au-delà du simple contrôle des entrées/sorties, les microcontrôleurs modernes intègrent des périphériques sophistiqués gérés directement par...
  • Pour connecter un microcontrôleur à plusieurs capteurs ou à d'autres processeurs, on utilise des bus de communication série. I2C (Inter...
  • Pour des systèmes complexes gérant plusieurs tâches simultanées (acquisition capteur, commande moteur, communication réseau), la programmati...
  • Le Raspberry Pi est un nano-ordinateur basé sur un processeur ARM exécutant un système d'exploitation Linux (comme Raspberry Pi OS). Co...
  • Les microcontrôleurs peuvent effectuer du traitement numérique du signal (TNS) sur des données acquises. Un besoin courant est le filtrage p...

Introduction

En classe de Première, vous avez acquis les bases de la programmation embarquée : architecture simple d'un microcontrôleur, utilisation de capteurs et d'actionneurs, et programmation basique avec l'environnement Arduino. En Terminale, nous allons approfondir ces connaissances pour concevoir des systèmes embarqués plus complexes, performants et connectés. Ce cours couvrira les architectures avancées, les protocoles de communication, les systèmes d'exploitation temps réel, le traitement du signal, la sécurité et des études de cas intégratrices.

1. Architecture avancée des microcontrôleurs

Au-delà du simple contrôle des entrées/sorties, les microcontrôleurs modernes intègrent des périphériques sophistiqués gérés directement par le programme. Leur cœur est un processeur qui exécute des instructions lues en mémoire. Pour interagir avec les périphériques, le programmeur manipule des registres, qui sont des emplacements mémoire spéciaux liés à une fonction matérielle spécifique (configuration d'un timer, lecture d'une conversion ADC, etc.). Par exemple, sur un microcontrôleur AVR (Arduino Uno), configurer la broche 13 en sortie se fait en écrivant dans les registres DDRB et PORTB.

Les interruptions sont un mécanisme essentiel. Au lieu de scruter en permanence l'état d'un périphérique (attente active), le microcontrôleur peut exécuter son programme principal et être "interrompu" par un événement (front sur une broche, fin de conversion ADC, débordement de timer). L'exécution saute alors vers une routine de service d'interruption (ISR) avant de reprendre là où elle s'était arrêtée. Cela permet une réaction rapide et une gestion efficace du temps processeur. Un timer est un compteur incrémenté par l'horloge système. Il peut générer des interruptions à intervalle régulier pour exécuter une tâche périodique (échantillonnage, clignotement LED) sans bloquer le programme principal. L'ADC (Convertisseur Analogique-Numérique) transforme une tension en une valeur numérique. Sa configuration avancée implique de régler la fréquence d'échantillonnage, la référence de tension et le mode de déclenchement (continu, par interruption). Le DAC (Convertisseur Numérique-Analogique), plus rare, fait l'opération inverse.

Exemple de code : Utilisation d'une interruption externe et d'un timer sur Arduino (AVR).

volatile int compteur = 0; // 'volatile' indique que la variable peut être modifiée dans une ISR

// Routine de Service d'Interruption pour l'interruption externe sur la broche 2 (INT0)

compteur++; // Incrémente à chaque appui sur un bouton connecté à la broche 2

attachInterrupt(digitalPinToInterrupt(2), interruptionExterne, FALLING); // Associe la fonction à l'interruption sur front descendant

// Configuration du Timer1 pour une interruption toutes les secondes

noInterrupts(); // Désactive temporairement les interruptions

TCCR1A = 0; // Régistre de contrôle A du Timer1 à 0

TCCR1B = 0; // Idem pour le contrôle B

TCNT1 = 0; // Initialise le compteur du timer à 0

// On veut une interruption chaque seconde. Horloge à 16MHz, prédiviseur à 1024.

// Compteur à incrémenter : 16e6 / 1024 = 15625 incréments par seconde.

OCR1A = 15625; // Définit la valeur de comparaison

TCCR1B |= (1 << WGM12); // Mode CTC (Clear Timer on Compare match)

TCCR1B |= (1 << CS12) | (1 << CS10); // Prédiviseur à 1024

TIMSK1 |= (1 << OCIE1A); // Active l'interruption sur comparaison A

interrupts(); // Réactive les interruptions globales

ISR(TIMER1_COMPA_vect) { // ISR du Timer1

Serial.print("Compteur bouton : ");

// Le programme principal est libre de faire autre chose

delay(100); // Simule une autre tâche

À retenir

Au-delà du simple contrôle des entrées/sorties, les microcontrôleurs modernes intègrent des périphériques sophistiqués gérés directement par le programme. Leur cœur est un processeur qui exécute des instructions lues en mémoire. Pour interagir avec l...

2. Protocoles de communication avancés

Pour connecter un microcontrôleur à plusieurs capteurs ou à d'autres processeurs, on utilise des bus de communication série. I2C (Inter-Integrated Circuit) est un bus bidirectionnel à deux fils (SDA : données, SCL : horloge). Il permet de connecter plusieurs esclaves (capteurs, mémoires) sur le même bus, chacun ayant une adresse unique sur 7 ou 10 bits. Le protocole est géré par un ou plusieurs maîtres qui génèrent l'horloge. Il supporte le mode multi-maîtres avec arbitrage. La communication est relativement lente (100 kbit/s standard, 400 kbit/s fast mode). Exemple : lecture d'un capteur de température LM75 (adresse 0x48) avec la bibliothèque Wire d'Arduino.

Wire.begin(); // Rejoint le bus I2C en tant que maître

Wire.beginTransmission(ADRESSE_LM75); // Début transmission vers l'adresse

Wire.write(0x00); // Pointe vers le registre de température

Wire.endTransmission(); // Fin transmission (pas de stop pour répété start)

Wire.requestFrom(ADRESSE_LM75, 2); // Demande 2 octets à l'esclave

int donnee = (Wire.read() << 8) | Wire.read(); // Lecture MSB puis LSB

float temperature = donnee / 256.0; // Résolution 0.5°C par bit

Serial.print("Température : ");

SPI (Serial Peripheral Interface) est un bus full-duplex (données envoyées et reçues simultanément) à 4 fils (MISO : Maître In Esclave Out, MOSI : Maître Out Esclave In, SCK : Horloge, SS : Sélection d'Esclave). Il est plus rapide qu'I2C (plusieurs Mbit/s) mais nécessite une ligne de sélection par esclave. Le maître génère toujours l'horloge. UART (Universal Asynchronous Receiver-Transmitter) est une communication série asynchrone point-à-point (pas de bus). Il n'y a pas d'horloge partagée ; les deux périphériques doivent être configurés sur la même vitesse (baud rate). Les niveaux logiques sont souvent convertis en RS232 (niveaux ±12V pour une liaison longue distance) ou RS485 (bus différentiel multi-points très robuste). Sur Arduino, la communication série avec Serial.begin() utilise l'UART.

À retenir

Pour connecter un microcontrôleur à plusieurs capteurs ou à d'autres processeurs, on utilise des bus de communication série. I2C (Inter-Integrated Circuit) est un bus bidirectionnel à deux fils (SDA : données, SCL : horloge). Il permet de connec...

3. Introduction aux RTOS (Real-Time Operating System)

Pour des systèmes complexes gérant plusieurs tâches simultanées (acquisition capteur, commande moteur, communication réseau), la programmation par super-loop (une grande boucle loop()) devient difficile à maintenir et peu réactive. Un RTOS comme FreeRTOS permet de découper le programme en tâches (threads) indépendantes, chacune ayant sa propre fonction et priorité. Le noyau du RTOS planifie l'exécution des tâches, donnant l'illusion du parallélisme sur un seul cœur. L'ESP32, basé sur un double cœur, supporte nativement FreeRTOS. Les tâches peuvent communiquer entre elles via des mécanismes de synchronisation : les sémaphores (pour gérer l'accès à une ressource partagée) et les files de messages (pour échanger des données).

Exemple de code : Création de deux tâches et utilisation d'un sémaphore sur ESP32 avec l'Arduino Core.

SemaphoreHandle_t semaphore; // Déclaration du sémaphore

void tacheCapteur(void * parameter) {

// Simulation d'acquisition capteur (longue)

vTaskDelay(pdMS_TO_TICKS(100)); // Délai de 100ms

Serial.println("Capteur : Acquisition terminée.");

xSemaphoreGive(semaphore); // Donne (libère) le sémaphore

void tacheAffichage(void * parameter) {

// Attend le sémaphore. Si non disponible, la tâche se met en attente.

if(xSemaphoreTake(semaphore, portMAX_DELAY) == pdTRUE) {

Serial.println("Affichage : Donnée traitée.");

semaphore = xSemaphoreCreateBinary(); // Crée un sémaphore binaire initialement à 0

tacheCapteur, // Fonction de la tâche

10000, // Taille de la pile (mots)

1, // Priorité (1 basse)

0 // Cœur (0 ou 1)

// La tâche setup() se termine, le scheduler FreeRTOS démarre.

// La boucle loop() est une tâche FreeRTOS de priorité 1.

vTaskDelay(pdMS_TO_TICKS(1000)); // Mise en sommeil pour laisser les autres tâches s'exécuter

À retenir

Pour des systèmes complexes gérant plusieurs tâches simultanées (acquisition capteur, commande moteur, communication réseau), la programmation par super-loop (une grande boucle loop()) devient difficile à maintenir et peu réactive. Un RTOS comme Free...

4. Programmation Raspberry Pi et Linux embarqué

Le Raspberry Pi est un nano-ordinateur basé sur un processeur ARM exécutant un système d'exploitation Linux (comme Raspberry Pi OS). Contrairement aux microcontrôleurs, il permet de faire tourner plusieurs processus, d'avoir un système de fichiers, et de se connecter facilement à Internet. La programmation embarquée sous Linux consiste souvent à écrire des programmes en C ou Python qui interagissent avec le matériel via des interfaces système. Les broches GPIO (General Purpose Input/Output) sont accessibles via la bibliothèque wiringPi (en C) ou RPi.GPIO (en Python). Le Linux embarqué introduit des concepts comme les pilotes (drivers) noyau, les appels système, et la gestion des processus.

Exemple de code : Contrôle GPIO en Python sur Raspberry Pi.

GPIO.setmode(GPIO.BCM) # Utilisation des numéros BCM des broches

GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Activation résistance pull-up interne

button_state = GPIO.input(BUTTON_PIN)

if button_state == GPIO.LOW: # Bouton pressé (car pull-up)

GPIO.output(LED_PIN, GPIO.HIGH)

time.sleep(0.1) # Petite pause pour éviter la surcharge CPU

print("Arrêt par l'utilisateur")

GPIO.cleanup() # Réinitialisation des GPIO

Pour des applications temps réel critiques, il est possible de combiner un microcontrôleur (pour les tâches temps réel bas niveau) et un Raspberry Pi (pour la logique haut niveau, interface utilisateur, cloud).

À retenir

Le Raspberry Pi est un nano-ordinateur basé sur un processeur ARM exécutant un système d'exploitation Linux (comme Raspberry Pi OS). Contrairement aux microcontrôleurs, il permet de faire tourner plusieurs processus, d'avoir un système de f...

5. Traitement du signal embarqué

Les microcontrôleurs peuvent effectuer du traitement numérique du signal (TNS) sur des données acquises. Un besoin courant est le filtrage pour supprimer le bruit d'un signal de capteur. Un filtre numérique implémente un algorithme (comme un filtre à réponse impulsionnelle finie, RIF) qui calcule chaque échantillon de sortie comme une combinaison linéaire des échantillons d'entrée actuels et passés. La Transformée de Fourier Rapide (FFT) est un algorithme pour passer du domaine temporel au domaine fréquentiel, permettant d'analyser les différentes fréquences composant un signal. Sur microcontrôleur, on utilise des bibliothèques optimisées (comme la FFT fixe-point) pour des calculs rapides.

Exemple de code : Filtrage RIF simple (moyenne glissante) et acquisition ADC sur Arduino.

const int N = 10; // Taille du filtre

float buffer[N]; // Buffer circulaire

for(int i=0; i<N; i++) buffer[i] = 0;

int lecture = analogRead(A0); // Lecture brute du capteur (ex: potentiomètre)

float tension = lecture * (5.0 / 1023.0); // Conversion en tension (si ref 5V)

// Mise à jour du filtre moyenneur

somme = somme - buffer[index]; // Soustrait la valeur la plus ancienne

buffer[index] = tension; // Ajoute la nouvelle valeur

somme = somme + buffer[index]; // Ajoute la nouvelle valeur à la somme

index = (index + 1) % N; // Incrémente l'index circulaire

float moyenne = somme / N; // Calcule la moyenne (filtre passe-bas)

delay(50); // Période d'échantillonnage de 50ms

À retenir

Les microcontrôleurs peuvent effectuer du traitement numérique du signal (TNS) sur des données acquises. Un besoin courant est le filtrage pour supprimer le bruit d'un signal de capteur. Un filtre numérique implémente un algorithme (comme un fil...

6. Sécurité des systèmes embarqués et études de cas

La connexion des objets à Internet (IoT) expose les systèmes embarqués à des menaces. La sécurité doit être intégrée dès la conception. Le chiffrement des données (avec des algorithmes comme AES) protège la confidentialité. L'authentification (par certificats ou clés) assure que seul un appareil autorisé peut se connecter. Le Secure Boot est un mécanisme matériel/logiciel qui vérifie la signature numérique du firmware au démarrage, empêchant l'exécution de code malveillant. Sur ESP32, il est possible d'activer le Secure Boot et le chiffrement de la flash.

Étude de cas 1 : Station météo IoT avec ESP32 et MQTT.

L'ESP32 lit un capteur de température/humidité (DHT22 ou BME280 via I2C) et un capteur de pression. Il se connecte en Wi-Fi à un broker MQTT (comme Mosquitto) et publie périodiquement les mesures sur un topic (ex: "maison/cuisin/temperature"). Un serveur distant (Node-RED, application mobile) peut s'abonner à ces topics pour afficher les données. Le protocole MQTT est léger et adapté aux objets connectés. On peut ajouter une sécurisation avec TLS (chiffrement) et une authentification par mot de passe.

Exemple de code simplifié (sans gestion d'erreur complète) :

const char* password = "MonMotDePasse";

const char* mqtt_server = "192.168.1.100"; // Adresse du broker MQTT

PubSubClient client(espClient);

Adafruit_BME280 bme; // Objet capteur

while (WiFi.status() != WL_CONNECTED) {

if (client.connect("ESP32ClientMeteo")) {

client.setServer(mqtt_server, 1883); // Port MQTT standard

bme.begin(0x76); // Initialisation du BME280 à l'adresse I2C 0x76

if (!client.connected()) reconnect();

float temperature = bme.readTemperature();

float humidite = bme.readHumidity();

// Conversion en chaîne de caractères

dtostrf(temperature, 6, 2, tempString);

client.publish("maison/jardin/temperature", tempString);

dtostrf(humidite, 6, 2, humString);

client.publish("maison/jardin/humidite", humString);

delay(30000); // Publication toutes les 30 secondes

Étude de cas 2 : Robot autonome avec capteurs ultrason et PID.

Un robot à deux roues (avec moteurs à courant continu contrôlés par un pont en H) doit suivre un mur à une distance fixe. Un capteur à ultrasons (HC-SR04) mesure la distance au mur. Un régulateur PID (Proportionnel, Intégral, Dérivé) calcule la commande pour les moteurs afin de corriger l'erreur entre la distance mesurée et la distance consigne. Le PID est un algorithme de contrôle classique en automatique. La sortie du PID peut ajuster la vitesse d'une roue par rapport à l'autre pour tourner vers ou loin du mur. Le code doit gérer l'acquisition précise du capteur (via interruptions sur les fronts d'écho), le calcul du PID à période fixe (via un timer), et la commande PWM des moteurs.

Exemple de code simplifié (structure) :

// Pseudocode pour l'idée générale

float consigne = 20.0; // Distance souhaitée en cm

float erreur, erreur_precedente, integral, derive;

float Kp = 1.0, Ki = 0.01, Kd = 0.1; // Gains à régler

void ISR_Timer_PID() { // Appelée périodiquement par un timer

float distance = mesurerDistanceUltrason(); // Fonction à écrire

derive = erreur - erreur_precedente;

commande = Kp erreur + Ki integral + Kd * derive;

// Commande des moteurs : vitesse de base +/- commande

int vitesseGauche = VITESSEMAX - commande;

int vitesseDroite = VITESSEMAX + commande;

analogWrite(PIN_MOTEUR_GAUCHE, constrain(vitesseGauche, 0, 255));

analogWrite(PIN_MOTEUR_DROITE, constrain(vitesseDroite, 0, 255));

À retenir

La connexion des objets à Internet (IoT) expose les systèmes embarqués à des menaces. La sécurité doit être intégrée dès la conception. Le chiffrement des données (avec des algorithmes comme AES) protège la confidentialité. L'authentification (p...

Points clés à retenir

  • Ce cours couvre les concepts avancés de Terminale avec des applications concrètes.

Exercices d'application

Exercice 1

Un afficheur OLED SSD1306 (128x64 pixels) est connecté en I2C à un microcontrôleur. Son adresse est 0x3C. Il possède une mémoire graphique (GDDRAM) qu'il faut mettre à jour pour afficher du texte ou des formes. Expliquez brièvement le rôle des lignes SDA et SCL. Écrivez la séquence I2C (en pseudo-code ou avec la bibliothèque Wire) pour initialiser l'afficheur (commande 0xAE pour éteindre l'affichage, 0xAF pour l'allumer, parmi d'autres). Proposez une fonction pour afficher un caractère à une position donnée (x, y) en utilisant une police définie dans un tableau de bits

Exercice 2

On a un système avec trois tâches : T1 (acquisition capteur, priorité 2), T2 (traitement des données, priorité 1), T3 (affichage, priorité 1). T1 produit des données que T2 doit traiter, et T2 produit un résultat pour T3. Quel mécanisme de synchronisation utiliser pour que T2 attende les données de T1 ? Justifiez. Même question pour la communication entre T2 et T3. Écrivez le code de T1 et T2 en utilisant une file de messages (Queue) pour transmettre un entier (la donnée du capteur). T1 envoie, T2 reçoit. Corrigé Exercice 2 : Une file de messages (Queue) est adaptée. T1 peut envoyer (xQueueSen

Exercice 3

On souhaite implémenter un filtre passe-bas RIF d'ordre 4. Les coefficients du filtre sont : h[0]=0.1, h[1]=0.2, h[2]=0.4, h[3]=0.2, h[4]=0.1. Donnez l'équation de récurrence du filtre (y[n] = ...). En supposant un signal d'entrée x[n] échantillonné périodiquement, écrivez une fonction en C/C++ qui calcule un échantillon de sortie y. Utilisez un buffer circulaire pour stocker les 5 derniers échantillons d'entrée. Quel est l'effet de ce filtre sur un signal ? Comment pourrait-on modifier les coefficients pour obtenir un filtre passe-haut ? Corrigé Exercice 3 : y[n] = 0.

Scientia