Files
l3-ocr-exercises/td-corrige.md
2026-10-16 17:00:37 +02:00

829 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## TD 1 : Corrigé
### Points essentiels
#### 1. Sortie numérique
```cpp
gpio_set_dir(pin, GPIO_OUT);
gpio_put(pin, value);
```
Le programme impose un niveau logique à une broche.
#### 2. Entrée numérique
```cpp
gpio_set_dir(pin, GPIO_IN);
bool value = gpio_get(pin);
```
Le programme observe un niveau électrique externe.
#### 3. Pull-up
```cpp
gpio_pull_up(pin);
```
Elle évite une entrée flottante et permet un câblage simple du bouton vers GND.
#### 4. Niveau et événement
```text
niveau : « le bouton est actuellement appuyé »
événement : « le bouton vient d'être appuyé »
```
La détection de front nécessite de conserver un état précédent.
#### 5. ADC
```cpp
uint16_t value = adc_read();
```
Une tension analogique est convertie en une valeur numérique.
#### 6. PWM
```cpp
pwm_set_gpio_level(pin, level);
```
### Hypothèses et remarques générales
Les solutions suivantes utilisent le brochage du sujet :
```cpp
constexpr uint LED_PIN = 15;
constexpr uint BUTTON_PIN = 14;
```
Les boutons sont supposés connectés entre le GPIO et GND. On utilise donc une **pull-up interne** :
```text
bouton relâché \(\rightarrow\) GPIO = 1
bouton appuyé \(\rightarrow\) GPIO = 0
```
Ainsi, il est souvent pratique d'écrire :
```cpp
bool pressed = !gpio_get(BUTTON_PIN);
```
Pour les exercices utilisant `printf()`, il faut appeler :
```cpp
stdio_init_all();
```
et activer la sortie USB ou UART dans `CMakeLists.txt` selon l'environnement utilisé.
### Exercice 1 : Branchements
![](./images/Robot_Schema_completed_breadboard.png)
### Exercice 2 : Hello GPIO
#### Solution minimale
```cpp
#include "pico/stdlib.h"
constexpr uint LED_PIN = 15;
int main() {
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
while (true) {
gpio_put(LED_PIN, 1);
sleep_ms(1000);
gpio_put(LED_PIN, 0);
sleep_ms(1000);
}
}
```
`gpio_init()` initialise le GPIO, `gpio_set_dir()` fixe sa direction et `gpio_put()` fixe son niveau logique.
#### Clignotement à 5 Hz
Une fréquence de 5 Hz correspond à une période :
```text
T = 1 / f = 1 / 5 = 0,2 s = 200 ms
```
Avec un rapport cyclique de 50 %, chaque demi-période dure 100 ms :
```cpp
while (true) {
gpio_put(LED_PIN, true);
sleep_ms(100);
gpio_put(LED_PIN, false);
sleep_ms(100);
}
```
#### 100 ms ON / 900 ms OFF
```cpp
while (true) {
gpio_put(LED_PIN, true);
sleep_ms(100);
gpio_put(LED_PIN, false);
sleep_ms(900);
}
```
La période vaut ici 1 s, soit une fréquence de 1 Hz, avec un rapport cyclique de 10 %.
#### Fonction `blink()`
```cpp
void blink(uint pin, uint duration_ms) {
gpio_put(pin, true);
sleep_ms(duration_ms);
gpio_put(pin, false);
sleep_ms(duration_ms);
}
```
Utilisation :
```cpp
while (true) {
blink(LED_PIN, 250);
}
```
---
### Exercice 3 : Lire un bouton
Le **numéro du GPIO** (GPIO14) identifie une broche logique du microcontrôleur ; son **état logique** est la valeur 0 ou 1 sur cette broche.
```cpp
#include <cstdio>
#include "pico/stdlib.h"
constexpr uint BUTTON_PIN = 14;
int main() {
stdio_init_all();
gpio_init(BUTTON_PIN);
gpio_set_dir(BUTTON_PIN, GPIO_IN);
gpio_pull_up(BUTTON_PIN);
while (true) {
bool raw = gpio_get(BUTTON_PIN);
bool pressed = !raw;
printf("button = %d, pressed = %d\n", raw, pressed);
sleep_ms(100);
}
}
```
La résistance de pull-up impose un état connu lorsque le bouton est ouvert. Sans elle, l'entrée pourrait être flottante et donc prendre un niveau indéterminé.
Avec ce câblage :
```text
bouton relâché : entrée reliée faiblement au 3,3 V -> 1
bouton appuyé : entrée reliée directement à GND -> 0
```
D'où :
```cpp
bool pressed = !gpio_get(BUTTON_PIN);
```
La valeur physique « bouton appuyé » est ici représentée par un zéro logique : on parle souvent d'entrée **active à l'état bas**.
---
### Exercice 4 : Utilisation des moteurs
```text
programme
│
│ consigne : vitesse = -255 ... +255
▼
controleMoteur()
│
├── signe ───────────► choix du sens
│
└── valeur absolue ──► rapport cyclique PWM
│
▼
GPIO A / GPIO B
│
▼
pont en H
│
▼
moteur
```
```cpp
#define LEFT_MOTOR_PIN_A 26
#define LEFT_MOTOR_PIN_B 27
#define RIGHT_MOTOR_PIN_A 2
#define RIGHT_MOTOR_PIN_B 3
```
```cpp
void controleMoteur(uint pinA, uint pinB, int vitesse)
{
int puissance = vitesse < 0 ? -vitesse : vitesse;
if (vitesse > 0) {
pwm_set_gpio_level(pinA, puissance);
pwm_set_gpio_level(pinB, 0);
}
else if (vitesse < 0) {
pwm_set_gpio_level(pinA, 0);
pwm_set_gpio_level(pinB, puissance);
}
else {
pwm_set_gpio_level(pinA, 0);
pwm_set_gpio_level(pinB, 0);
}
}
```
```cpp
void controleMoteurs(int gauche, int droite)
{
controleMoteur(LEFT_MOTOR_PIN_A, LEFT_MOTOR_PIN_B, gauche);
controleMoteur(RIGHT_MOTOR_PIN_A, RIGHT_MOTOR_PIN_B, droite);
}
```
```text
┌── moteur avant gauche
vitesse gauche ── L298N ─┤
└── moteur arrière gauche
┌── moteur avant droit
vitesse droite ── L298N ─┤
└── moteur arrière droit
```
### Exercice 5 : Missions
1. Écrire un programme qui fait avancer le robot pendant 1 seconde, puis reculer le robot pendant 1 seconde.
2. Écrire un programme qui fait tourner le robot de 90° vers la gauche.
Chaque exercice est traité avec le protocole JSON, puis avec l'API `robot.js` (téléphone) et l'API `Robot.h` (robot).
#### Tourner de 90°
Le robot n'a ni encodeur ni gyroscope : il ne mesure pas l'angle dont il a tourné. Un virage sur place (roue gauche en arrière, roue droite en avant) dure un temps fixé, et ce temps donne l'angle. La valeur `600` ms à la vitesse `180` est un point de départ, à mesurer sur chaque robot et sur le sol de la salle.
1. Poser le robot sur le sol de la salle, aligné sur un repère (bord de dalle, ruban adhésif).
2. Lancer le quart de tour à gauche avec la durée courante.
3. Relever l'angle obtenu.
4. Corriger la durée en proportion : si 600 ms donnent 75°, essayer `600 * 90 / 75 = 720` ms.
5. Recommencer jusqu'à obtenir 90°, puis vérifier avec quatre quarts de tour d'affilée : le robot doit revenir face au repère.
**Remarque.** `turnLeft()` dans `robot.js` vaut par défaut 500 ms à la vitesse 200.
Cette valeur par défaut n'est pas un quart de tour mesuré.
**Attention.** La durée mesurée ne vaut que pour une vitesse, un sol et un
niveau de batterie. Changer l'un des trois oblige à la mesurer de nouveau.
#### Protocole JSON
Conventions des échanges ci-dessous :
- `→` : ligne envoyée au robot (console USB, ou corps de `POST /api/command`).
- `←` : ligne envoyée par le robot (console USB, ou `GET /api/lines`).
- Les champs des capteurs (`front_mm`, `front_ok`, `front_q`, `side_mm`, `side_ok`, `side_q`) sont omis des lignes de télémétrie pour la lisibilité.
- Les valeurs de `ms` et de `seq` sont des exemples. La télémétrie part toutes les 200 ms par défaut ; seules les lignes utiles sont montrées.
##### Question 1 : avancer 1 s, puis reculer 1 s
**À la main** (console USB ou `curl`). Deux commandes de mouvement, la seconde envoyée après la ligne `done:true` de la première :
```text
→ {"seq":1,"left":200,"right":200,"duration":1000}
← {"seq":1,"ms":41230,"left":200,"right":200,"moving":true,"mission":0}
← {"seq":1,"ms":42230,"left":0,"right":0,"moving":false,"mission":0,"done":true}
→ {"seq":2,"left":-200,"right":-200,"duration":1000}
← {"seq":2,"ms":42430,"left":-200,"right":-200,"moving":true,"mission":0}
← {"seq":2,"ms":43390,"left":0,"right":0,"moving":false,"mission":0,"done":true}
```
`"duration":1000` est la valeur par défaut et peut être omis. `seq` est facultatif ; il permet de savoir à quelle commande correspond une ligne `done`.
Avec `curl`, en remplaçant `<robot-ip>` par l'adresse affichée sur l'écran du robot :
```sh
curl -d '{"seq":1,"left":200,"right":200,"duration":1000}' http://<robot-ip>/api/command
curl -d '{"seq":2,"left":-200,"right":-200,"duration":1000}' http://<robot-ip>/api/command
```
**Remarque.** En salle, les ordinateurs ne sont pas sur le point d'accès du robot. `curl` s'utilise alors contre `client/fakerobot.js`, à l'adresse `http://localhost:8080`.
**Erreur fréquente.** Envoyer les deux commandes d'un coup, dans le même corps de requête ou sans attendre le `done`. Un mouvement remplace le mouvement en cours : le robot exécute le retour presque aussitôt et l'aller n'a pas lieu.
**Avec `robot.js`, classe `Robot`.** `forward()` et `backward()` envoient les mêmes lignes. `send()` ajoute `seq` après les autres champs ; les tags partent d'une valeur aléatoire :
```text
→ {"left":200,"right":200,"duration":1000,"seq":5731}
← … ligne done:true avec "seq":5731 …
→ {"left":-200,"right":-200,"duration":1000,"seq":5732}
← … ligne done:true avec "seq":5732 …
```
**Avec `robot.js`, classe `Mission`.** `Mission.run()` encadre les étapes par deux commandes de réglage :
```text
→ {"telemetry":100,"seq":5731}
→ {"left":200,"right":200,"duration":1000,"seq":5732}
← … ligne done:true avec "seq":5732 …
→ {"left":-200,"right":-200,"duration":1000,"seq":5733}
← … ligne done:true avec "seq":5733 …
→ {"stop":true,"seq":5734}
```
`{"telemetry":100}` fait passer la télémétrie à 100 ms pour les conditions `stopWhen()`. `{"stop":true}` final laisse le robot arrêté ; il ne produit pas de ligne `done`.
**Avec `Robot.h`.** Une seule commande traverse la liaison. Les deux mouvements sont décidés sur le robot et n'apparaissent que dans la télémétrie :
```text
→ {"mission":1}
← {"seq":-1,"ms":41230,"left":200,"right":200,"moving":true,"mission":1}
← {"seq":-1,"ms":42430,"left":-200,"right":-200,"moving":true,"mission":1}
← {"seq":-1,"ms":43230,"left":0,"right":0,"moving":false,"mission":0,"done":true}
```
Il n'y a qu'une ligne `done:true`, à la fin du retour : le retour remplace l'aller sans que les roues s'arrêtent. `seq` reste à `-1` tant qu'aucune commande n'a fourni de tag. Depuis le téléphone, `bot.mission(1)` envoie `{"mission":1,"seq":5731}` et la télémétrie porte alors `"seq":5731`.
##### Question 2 : quart de tour à gauche
**À la main.** Une seule commande de mouvement, roue gauche en arrière, roue droite en avant :
```text
→ {"seq":3,"left":-180,"right":180,"duration":600}
← {"seq":3,"ms":50100,"left":-180,"right":180,"moving":true,"mission":0}
← {"seq":3,"ms":50700,"left":0,"right":0,"moving":false,"mission":0,"done":true}
```
`600` est la durée à mesurer.
**Avec `robot.js`, classe `Robot`.** `turnLeft(180, 600)` envoie :
```text
→ {"left":-180,"right":180,"duration":600,"seq":5735}
← … ligne done:true avec "seq":5735 …
```
**Avec `robot.js`, classe `Mission`.**
```text
→ {"telemetry":100,"seq":5735}
→ {"left":-180,"right":180,"duration":600,"seq":5736}
← … ligne done:true avec "seq":5736 …
→ {"stop":true,"seq":5737}
```
**Avec `Robot.h`.**
```text
→ {"mission":2}
← {"seq":-1,"ms":50100,"left":-180,"right":180,"moving":true,"mission":0}
← {"seq":-1,"ms":50700,"left":0,"right":0,"moving":false,"mission":0,"done":true}
```
`"mission":0` apparaît dès la première ligne : `quarterTurnLeft()` remet `_mission` à 0 sur le tour même où elle lance le virage (voir la remarque de l'exercice 2 côté `Robot.h`).
#### API `robot.js` (téléphone)
Les méthodes de `Robot` envoient une commande et rendent son `seq` sans attendre. `waitUntilDone(seq)` attend la ligne `done:true` du robot. Le code ci-dessous s'exécute dans un module ES (`await` au premier niveau) ou dans une fonction `async`.
##### Question 1 : avancer 1 s, puis reculer 1 s
```js
import { Robot } from './robot.js';
const bot = new Robot();
bot.start(); // lit la télémétrie toutes les 100 ms
const aller = bot.forward(200, 1000);
if (await bot.waitUntilDone(aller) === null) throw new Error('pas de fin d\'aller');
const retour = bot.backward(200, 1000);
if (await bot.waitUntilDone(retour) === null) throw new Error('pas de fin de retour');
```
`waitUntilDone()` rend `null` après 6 s sans ligne `done`. Le test évite d'envoyer le retour alors que l'aller n'a jamais été confirmé.
Même programme sous forme de mission du téléphone, dans l'objet `missions` de `robot/web/missions.js` :
```js
'avancer-reculer': ({ speed = 200 } = {}) =>
new Mission('avancer-reculer')
.forward(speed, 1000)
.backward(speed, 1000),
```
`Mission.run()` envoie chaque étape, attend son `done` et laisse le robot arrêté à la fin.
**Attention.** Entre l'aller et le retour, le robot reste immobile un court moment : il arrête ses roues à l'échéance, puis le téléphone doit recevoir la ligne `done` (jusqu'à 100 ms de scrutation) et envoyer la commande suivante (un aller-retour HTTP). Combien de millisecondes séparent le `ms` de la ligne `done` de l'aller et le `ms` de la première ligne avec `"left":-200` ?
##### Question 2 : quart de tour à gauche
```js
import { Robot } from './robot.js';
const QUARTER_TURN_SPEED = 180;
const QUARTER_TURN_MS = 600; // à mesurer, voir « Tourner de 90° »
const bot = new Robot();
bot.start();
const virage = bot.turnLeft(QUARTER_TURN_SPEED, QUARTER_TURN_MS);
if (await bot.waitUntilDone(virage) === null) throw new Error('pas de fin de virage');
```
`turnLeft(speed, duration)` envoie `drive(-speed, speed, duration)` : la roue gauche recule, la roue droite avance, le robot tourne sur place.
Sous forme de mission du téléphone, avec les deux constantes ci-dessus déclarées en tête de `missions.js` :
```js
'quart-gauche': ({ speed = QUARTER_TURN_SPEED } = {}) =>
new Mission('quart-gauche')
.turnLeft(speed, QUARTER_TURN_MS),
```
**Erreur fréquente.** Passer le curseur de vitesse de la page à cette mission sans recalibrer : `QUARTER_TURN_MS` n'est juste qu'à `QUARTER_TURN_SPEED`.
#### API `Robot.h` (robot)
Un programme du robot est une mission : une méthode de la classe `Robot` appelée à chaque tour de `Robot::update()`. Elle ne doit jamais attendre (`sleep_ms()` est interdit) : elle lit l'heure avec `nowMs()`, bouge avec `drive()` et rend la main tout de suite. Elle garde son état dans des membres déclarés à côté d'elle.
Le code ci-dessous part du firmware de départ, où aucune mission n'existe : il utilise les numéros 1 et 2. Si les missions du programme des TP sont déjà en place (1 à 5), prendre 6 et 7 et ajouter les noms à la fin de
`MISSION_NAMES`.
##### Déclaration des deux missions
Dans `robot/Robot.h` :
```cpp
static constexpr const char* MISSION_NAMES = "avancer-reculer,quart-gauche";
void mission() {
switch (_mission) {
case 0: break;
case 1: forwardThenBack(); break;
case 2: quarterTurnLeft(); break;
default: break;
}
}
```
Dans `execute()`, remettre l'exercice 1 à son début à chaque sélection de mission. Un `{"mission":1}` envoyé après un arrêt en cours de route repart alors de l'aller :
```cpp
if (command.has("mission")) {
_mission = (int)command.integer("mission");
_backStep = 0;
}
```
##### Question 1 : avancer 1 s, puis reculer 1 s
```cpp
// Avancer 1 s, puis reculer 1 s. Étapes : 0 à lancer, 1 aller, 2 retour.
int _backStep = 0;
uint32_t _backUntil = 0;
void forwardThenBack() {
if (_backStep != 0 && (int32_t)(nowMs() - _backUntil) < 0) return; // étape en cours
if (_backStep == 0) {
drive(200, 200, 1000);
_backStep = 1;
} else if (_backStep == 1) {
drive(-200, -200, 1000);
_backStep = 2;
} else {
_backStep = 0;
_mission = 0; // fini : enforceSafety() arrête les roues sur ce tour
return;
}
_backUntil = _moveDeadline; // l'étape finit avec le mouvement
}
```
Lancement depuis la console USB ou par le bouton « avancer-reculer » du téléphone :
```json
{"mission":1}
```
`mission()` s'exécute avant `enforceSafety()` dans `Robot::update()`. Sur le tour où l'aller arrive à échéance, `forwardThenBack()` a déjà lancé le retour avec une nouvelle échéance : les roues passent de +200 à -200 sans arrêt intermédiaire. Sur le tour où le retour arrive à échéance, la mission se désactive, puis `enforceSafety()` arrête les roues et envoie la ligne `done:true`.
**Attention.** La comparaison `(int32_t)(nowMs() - _backUntil) < 0` reste juste quand l'horloge de 32 bits repasse à zéro ; `nowMs() < _backUntil` ne l'est pas. Voir [`API.md` — Time](API.md#time).
**Attention.** Passer directement de +200 à -200 sollicite les moteurs et la batterie. Si le robot redémarre au changement de sens, ajouter une étape d'arrêt de 200 ms entre l'aller et le retour avec `drive(0, 0, 200)`.
##### Question 2 : quart de tour à gauche
Dans la classe `Robot`, à côté de la méthode :
```cpp
static constexpr int QUARTER_TURN_SPEED = 180;
static constexpr uint32_t QUARTER_TURN_MS = 600; // à mesurer, voir « Tourner de 90° »
// Un seul mouvement : enforceSafety() l'arrête à l'échéance.
void quarterTurnLeft() {
drive(-QUARTER_TURN_SPEED, QUARTER_TURN_SPEED, QUARTER_TURN_MS);
_mission = 0;
}
```
Lancement :
```json
{"mission":2}
```
Le quart de tour est un seul mouvement. La mission le lance une fois, puis se désactive pour ne pas le relancer au tour suivant. L'échéance armée par `drive()` arrête les roues et produit la ligne `done:true`, comme pour une commande tapée dans la console.
**Remarque.** Pendant le virage, la télémétrie indique déjà `"mission":0`. Pour garder `"mission":2` jusqu'à la fin, attendre que `motors.isMoving()` soit faux avant de remettre `_mission` à 0, avec un booléen qui retient que le virage a été lancé.
**Erreur fréquente.** Écrire `QUARTER_TURN_MS` dans `Config.h` avec `#define` alors qu'il est aussi déclaré dans `Robot.h` : le préprocesseur remplace le nom dans la déclaration et la compilation échoue. Le déclarer à un seul endroit.
#### Comparaison des deux versions
| Question | `robot.js` | `Robot.h` |
| ---------------------------------- | ------------------------------------ | ----------------------------------- |
| Où s'exécute le programme ? | Dans le navigateur du téléphone | Sur le Pico |
| Qui décide de lancer le retour ? | Le téléphone, après la ligne `done` | La mission, à l'échéance de l'aller |
| Arrêt entre l'aller et le retour ? | Oui, au moins un aller-retour réseau | Non |
| Modifier le programme demande… | Un rechargement de la page¹ | Une recompilation et un flash |
¹ Contre `client/fakerobot.js`. Sur le vrai robot, la page est servie par le firmware : la modifier demande aussi une recompilation et un flash.
## TD 2 : Corrigé
### Rappel de la formule PID discrète
\[
u[n] = K_p \cdot e[n] + K_i \cdot \sum_{k=0}^{n} e[k] + K_d \cdot (e[n] - e[n-1])
\]
avec \(e[n] = distance_{cible} - distance_{mesurée}\).
**Remarque.** Discrétisées avec une période \(\Delta t\), l'intégrale devient \(\sum e[k] \cdot \Delta t\) et la dérivée \(\frac{e[n] - e[n-1]}{\Delta t}\). Si \(\Delta t\) est fixe, on pose \(K_i = K'_i \cdot \Delta t\) et \(K_d = K'_d / \Delta t\) (\(K'_i\), \(K'_d\) : gains du PID continu). À 50 Hz, \(K'_i = 5\) et \(K'_d = 0.004\) donnent \(K_i = 0.1\) et \(K_d = 0.2\). Changer la fréquence de la boucle oblige à recalculer \(K_i\) et \(K_d\).
- \(K_p\) : gain proportionnel \(\rightarrow\) réagit à l'erreur actuelle.
- \(K_i\) : gain intégral \(\rightarrow\) corrige les erreurs accumulées dans le temps.
- \(K_d\) : gain dérivé \(\rightarrow\) réagit à la vitesse de variation de l'erreur et amortit les changements rapides.
### Exercice 1 : composante proportionnelle (P)
> Distance cible = 10 cm, distance mesurée = 6 cm
1. **Que signifie \(e[0]\) ? Calculez sa valeur.**
\[
e[0] = 10 - 6 = 4 \text{ cm}
\]
C'est l'erreur au premier pas de temps : le robot est 4 cm trop près du mur. Une erreur positive signifie « trop près ».
2. **Si \(K_p = 0.5\), quelle est la correction appliquée ?**
\[
u[0] = K_p \cdot e[0] = 0.5 \cdot 4 = 2
\]
Le robot applique une correction de 2 unités de commande (par exemple, un écart de vitesse entre les roues) pour s'écarter du mur.
3. **Que se passe-t-il si \(K_p = 2\) ?**
\[
u[0] = 2 \cdot 4 = 8
\]
La correction est quatre fois plus forte. Le robot réagit plus vite, mais risque de dépasser la cible (sur-correction) puis d'osciller.
### Exercice 2 : composante intégrale (I)
> \(e[0] = 4, e[1] = 4, e[2] = 4\) : l'erreur persiste.
1. **Commande totale après 3 mesures (avec \(K_p = 0.5\) et \(K_i = 0.1\)) :**
- Correction proportionnelle = \(0.5 \cdot 4 = 2\)
- Correction intégrale = \(K_i \cdot \sum e[k] = 0.1 \cdot (4 + 4 + 4) = 1.2\)
- Commande totale \(u[2] = 2 + 1.2 = 3.2\)
**Remarque.** Le terme dérivé est nul ici : \(e[2] - e[1] = 0\).
2. **Quelle est la part due à l'intégrale ?**
\(1.2\) sur \(3.2\), soit 37,5 % de la commande. Tant que l'erreur reste à 4, cette part augmente de \(0.4\) à chaque pas de temps.
3. **Que se passe-t-il si \(K_i\) est trop grand ?**
- La somme accumulée pèse trop dans la commande \(\rightarrow\) sur-correction.
- L'intégrale ne diminue que lorsque l'erreur change de signe : le robot dépasse la cible et oscille, même quand l'erreur actuelle est faible.
### Exercice 3 : composante dérivée (D)
> \(e[2] = 4\), \(e[3] = 1\) : l'erreur diminue rapidement, le robot s'éloigne du mur.
1. **Variation d'erreur :**
\[
e_d = e[3] - e[2] = 1 - 4 = -3
\]
2. **Correction dérivée :**
\[
u_d = K_d \cdot e_d = 0.2 \cdot (-3) = -0.6
\]
Le terme dérivé est négatif : il **freine** la commande car l'erreur diminue rapidement.
3. **Commande totale \(u[3]\) (avec \(K_p = 0.5\), \(K_i = 0.1\) et \(K_d = 0.2\)) :**
\[
u[3] = 0.5 \cdot 1 + 0.1 \cdot (4 + 4 + 4 + 1) + 0.2 \cdot (-3) = 0.5 + 1.3 - 0.6 = 1.2
\]
Le robot **réduit** sa correction pour ne pas dépasser la consigne.
**Erreur fréquente.** Multiplier deux fois par \(K_d\) : \(0.2 \cdot (-0.6)\). La correction dérivée \(-0.6\) contient déjà le gain.
### Exercice 4 : pertinence des composantes
1. **Analyse des comportements :**
| Situation | Interprétation | Réglage probable |
| ------------------------------ | --------------------------------------------------------------- | ----------------------------------------------- |
| A : oscille autour de la cible | \(K_p\) trop grand, \(K_i\) trop grand ou \(K_d\) trop faible | Diminuer \(K_p\) ou \(K_i\), augmenter \(K_d\) |
| B : corrige trop lentement | \(K_p\) trop faible, ou \(K_d\) trop grand (amortissement excessif) | Augmenter \(K_p\), diminuer \(K_d\) |
| C : dépasse systématiquement | \(K_i\) trop grand (somme accumulée pendant l'approche), \(K_p\) trop grand ou \(K_d\) trop faible | Diminuer \(K_i\), augmenter \(K_d\) |
2. **Comment corriger ?** Régler les gains un par un, sur le robot :
1. Mettre \(K_i = 0\) et \(K_d = 0\).
2. Augmenter \(K_p\) jusqu'à ce que le robot oscille autour de la cible, puis le diviser par deux.
3. Augmenter \(K_d\) jusqu'à ce que les oscillations restantes s'amortissent.
4. Si une erreur persiste à l'équilibre, ajouter un petit \(K_i\).
5. Borner la somme \(\sum e[k]\) (*anti-windup*) pour qu'elle ne s'accumule pas quand le robot est bloqué ou loin de la cible.
**Attention.** Ne modifier qu'un gain à la fois, sinon on ne sait pas lequel a produit l'effet observé.
## TD 3 : Corrigé
### Exercice 1 : temps de calcul et fréquence de lecture
1. **Fréquence maximale de la boucle** :
- Le capteur mesure pendant que le processeur lit et calcule : la boucle attend seulement la mesure suivante.
- Période de la boucle = 20 ms, soit **50 Hz**.
- Élément limitant : le capteur.
**Erreur fréquente.** Additionner 20 ms + 0,5 ms + 0,02 ms. Ce calcul vaut pour un capteur qui attend la fin de la lecture avant de lancer la mesure suivante (mode « une seule mesure ») : on obtiendrait alors \(\frac{1}{0.02052} \approx 48.7 \text{ Hz}\).
2. **Part du temps processeur** :
- Lecture + calcul = 0,5 + 0,02 = 0,52 ms toutes les 20 ms
- \(\frac{0.52}{20} = 2.6\,\%\)
- Le reste sert notamment à la pile Wi-Fi (pilote CYW43439 et TCP/IP), au serveur HTTP, à la télémétrie, à l'afficheur et au télémètre à ultrasons.
3. **Boucle à 100 Hz** :
- Le VL53L1X ne produit que 50 mesures par seconde : la boucle lit chaque mesure deux fois.
- Une fois sur deux, \(e[n] - e[n-1] = 0\) ; la fois suivante, toute la variation arrive d'un coup. Le terme dérivé alterne entre 0 et le double de sa valeur.
- Le terme intégral compte chaque mesure deux fois.
- Solution : ne calculer le PID que lorsqu'une nouvelle mesure est disponible.
4. **Fréquence raisonnable à 0.5 m/s** :
- À 50 Hz, le robot avance de \(\frac{0.5}{50} = 1 \text{ cm}\) entre deux mesures ; à 25 Hz, de 2 cm.
- Le coût en calcul étant faible (question 2), **50 Hz** est adapté. 25 Hz reste acceptable si l'on accepte 2 cm entre deux mesures.
### Exercice 2 : occupation mémoire
1. **Mémoire utilisée pour 100 erreurs** :
- \(100 \times 4\) octets = **400 octets**
- SRAM = 264 kio = 270 336 octets, soit \(\frac{400}{270336} \approx 0.15\,\%\)
- Sur le Pico WH, cet historique n'est pas une contrainte.
2. **Ce que le PID doit conserver** :
- La somme des erreurs \(S[n] = S[n-1] + e[n]\), mise à jour à chaque pas
- L'erreur précédente \(e[n-1]\)
- Soit 2 `float` = **8 octets**. L'historique complet n'est nécessaire que pour une intégrale sur fenêtre glissante ou un filtre.
3. **Historique de télémétrie** :
- Un échantillon : 4 + 4 \(\times\) 2 = 12 octets (pas d'octets de remplissage : le `uint32_t` est en tête)
- À 50 Hz : \(12 \times 50 = 600\) octets/s
- Tampon de 32 kio : \(\frac{32768}{600} \approx 54.6\) s, soit **environ 54 s**
4. **Réduire l'empreinte** :
- Tampon circulaire : on garde les \(N\) dernières secondes et on écrase les plus anciennes.
- Sous-échantillonner l'historique à 10 Hz : 5 fois moins de mémoire, le PID continue à 50 Hz.
- Stocker un écart de temps sur un `uint16_t` au lieu de `ms` sur un `uint32_t` : 10 octets au lieu de 12.
- Ne rien stocker sur le robot : le téléphone reçoit déjà la télémétrie et garde l'historique, où la mémoire ne manque pas.
### Exercice 3 : énergie et efficacité
1. **Autonomie des moteurs** :
- Sans oscillations : 1000 mAh / 100 mA = **10 heures**
- Avec oscillations : 1000 mAh / 150 mA ≈ **6,67 heures** (6 h 40 min)
2. **Gain en autonomie** :
- 10 h − 6,67 h = **3,33 heures** (3 h 20 min)
- Soit 50 % d'autonomie en plus (\(\frac{10}{6.67} = 1.5\))
3. **Stratégie PID pour réduire les oscillations** :
- Diminuer \(K_p\) s'il est trop agressif
- Augmenter \(K_d\) pour amortir les variations
- Réduire \(K_i\) et borner la somme si elle s'accumule trop
### Exercice 4 : précision et bruit de mesure
1. **Erreur moyenne** :
- Moyenne = (198 + 202 + 195 + 205 + 200) / 5 = **200 mm**
- Erreur moyenne (biais) = 200 − 200 = **0 mm**
- Erreur absolue moyenne = (2 + 2 + 5 + 5 + 0) / 5 = **2,8 mm**
**Écart-type** :
- Écarts à la moyenne : −2, +2, −5, +5, 0 ; carrés : 4, 4, 25, 25, 0 ; somme = 58
- Écart-type de la population : \(\sqrt{58 / 5} = \sqrt{11.6} \approx\) **3,4 mm**
- Écart-type de l'échantillon : \(\sqrt{58 / 4} = \sqrt{14.5} \approx\) **3,8 mm**
2. **Influence du bruit sur le terme dérivé** :
- Erreurs \(e[n] = 200 - mesure\) : 2, −2, 5, −5, 0
- Variations \(e[n] - e[n-1]\) : −4, +7, −10, +5 mm
- Avec \(K_d = 0.2\) : termes dérivés −0,8, +1,4, −2,0, +1,0, alors que le robot ne bouge pas.
- À 50 Hz, une variation de 10 mm en 20 ms correspond à une vitesse apparente de 0,5 m/s, la vitesse du robot dans les exercices 1 et 5.
- La différence de deux mesures bruitées a un écart-type \(\sqrt{2}\) fois plus grand que celui d'une mesure. Le terme dérivé amplifie le bruit : commandes saccadées, oscillations.
3. **Méthodes de filtrage** :
- Moyenne glissante sur \(N\) mesures : retard de \(\frac{N-1}{2}\) périodes, soit 40 ms pour \(N = 5\) à 50 Hz.
- Médiane sur 3 mesures : élimine une valeur aberrante isolée, retard d'une période (20 ms).
- Filtre exponentiel : \(y[n] = \alpha \cdot x[n] + (1 - \alpha) \cdot y[n-1]\) ; plus \(\alpha\) est petit, plus le filtre lisse et plus il retarde.
- Ne filtrer que le terme dérivé, pour garder la réactivité des termes P et I.
**Attention.** Le retard du filtre s'ajoute à la latence de la boucle (exercice 5).
### Exercice 5 : latence dans une architecture distribuée
La chaîne entre la mesure et la commande :
- La mesure attend la ligne de télémétrie suivante : de 0 à 20 ms (la ligne porte la dernière mesure, prise au plus 20 ms plus tôt).
- La ligne attend la prochaine requête `GET /api/lines` du téléphone : de 0 à 100 ms.
- La réponse traverse le Wi-Fi jusqu'au téléphone.
- Le téléphone calcule le PID : 1 ms.
- Le `POST /api/command` traverse le Wi-Fi jusqu'au robot.
1. **Latence, canal libre** :
- Meilleur cas : 0 + 0 + 5 + 1 + 5 = **11 ms**
- Pire cas : 20 + 100 + 5 + 1 + 5 = **131 ms**
- À comparer avec le PID local : au plus 20 ms + 0,52 ms.
2. **Latence, canal encombré** :
- Meilleur cas : 0 + 0 + 50 + 1 + 50 = **101 ms**
- Pire cas : 20 + 100 + 50 + 1 + 50 = **221 ms**
3. **Distance parcourue à 0.5 m/s** (0,5 mm par milliseconde) :
| Cas | Latence | Distance |
| ----------------------- | ------- | --------- |
| Canal libre, meilleur | 11 ms | 5,5 mm |
| Canal libre, pire | 131 ms | 6,6 cm |
| Canal encombré, meilleur | 101 ms | 5,1 cm |
| Canal encombré, pire | 221 ms | 11,1 cm |
| PID local, pire | 20,5 ms | 1,0 cm |
4. **Est-ce acceptable pour des obstacles à 20 cm ?**
- Dans le pire cas, le robot parcourt plus de la moitié des 20 cm avant que la commande n'arrive, et il lui faut encore une distance de freinage.
- La latence varie d'un facteur 20 entre le meilleur et le pire cas : le réglage du PID ne peut pas en tenir compte.
- Acceptable pour des corrections lentes de trajectoire. Inacceptable pour l'arrêt devant un obstacle.
5. **Canaux Wi-Fi 2,4 GHz** :
- **3 canaux** de 20 MHz sans chevauchement : 1, 6 et 11. En Europe, où les canaux 1 à 13 sont autorisés, on peut en placer 4 : 1, 5, 9 et 13.
- Une douzaine de robots se partagent donc 3 ou 4 canaux, avec le Wi-Fi de l'établissement, le Bluetooth et les autres équipements de la bande.
- Sur un même canal, une seule station émet à la fois : chacune attend que le canal soit libre (CSMA/CA), et les collisions provoquent des retransmissions. La latence augmente et devient irrégulière.
6. **Stratégies pour réduire l'impact de la latence** :
- **Calcul local** : faire tourner le PID sur le Pico à 50 Hz ; il n'occupe que 2,6 % du temps processeur (exercice 1).
- **Hybridation** : le téléphone envoie des consignes lentes (distance au mur, vitesse, mission) ; le robot exécute le PID et l'arrêt devant un obstacle. C'est le rôle de la mission `stopAtObstacle()` du TP 1.
- **Prédiction** : le champ `ms` date chaque ligne sur l'horloge du robot. Le téléphone estime l'âge de la mesure et extrapole la distance : \(d_{prédite} = d_{mesurée} - v \cdot \text{âge}\).
- **Sécurité** : chaque commande porte une `duration`. Si le téléphone n'envoie plus rien, le robot s'arrête de lui-même à l'échéance (`enforceSafety()`).
- **Réseau** : choisir un canal peu occupé pour le point d'accès du robot ; réduire la période de scrutation, au prix de plus de trafic sur le canal.