32 KiB
TD 1 : Corrigé
Points essentiels
1. Sortie numérique
gpio_set_dir(pin, GPIO_OUT);
gpio_put(pin, value);
Le programme impose un niveau logique Ă une broche.
2. Entrée numérique
gpio_set_dir(pin, GPIO_IN);
bool value = gpio_get(pin);
Le programme observe un niveau électrique externe.
3. Pull-up
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
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
uint16_t value = adc_read();
Une tension analogique est convertie en une valeur numérique.
6. PWM
pwm_set_gpio_level(pin, level);
HypothÚses et remarques générales
Les solutions suivantes utilisent le brochage du sujet :
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 :
bouton relùché \(\rightarrow\) GPIO = 1
bouton appuyé \(\rightarrow\) GPIO = 0
Ainsi, il est souvent pratique d'écrire :
bool pressed = !gpio_get(BUTTON_PIN);
Pour les exercices utilisant printf(), il faut appeler :
stdio_init_all();
et activer la sortie USB ou UART dans CMakeLists.txt selon l'environnement utilisé.
Exercice 1 : Branchements
Exercice 2 : Hello GPIO
Solution minimale
#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 :
T = 1 / f = 1 / 5 = 0,2 s = 200 ms
Avec un rapport cyclique de 50 %, chaque demi-période dure 100 ms :
while (true) {
gpio_put(LED_PIN, true);
sleep_ms(100);
gpio_put(LED_PIN, false);
sleep_ms(100);
}
100 ms ON / 900 ms OFF
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()
void blink(uint pin, uint duration_ms) {
gpio_put(pin, true);
sleep_ms(duration_ms);
gpio_put(pin, false);
sleep_ms(duration_ms);
}
Utilisation :
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.
#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 :
bouton relùché : entrée reliée faiblement au 3,3 V -> 1
bouton appuyé : entrée reliée directement à GND -> 0
D'oĂč :
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
programme
â
â consigne : vitesse = -255 ... +255
âŒ
controleMoteur()
â
âââ signe ââââââââââââș choix du sens
â
âââ valeur absolue âââș rapport cyclique PWM
â
âŒ
GPIO A / GPIO B
â
âŒ
pont en H
â
âŒ
moteur
#define LEFT_MOTOR_PIN_A 26
#define LEFT_MOTOR_PIN_B 27
#define RIGHT_MOTOR_PIN_A 2
#define RIGHT_MOTOR_PIN_B 3
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);
}
}
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);
}
âââ moteur avant gauche
vitesse gauche ââ L298N ââ€
âââ moteur arriĂšre gauche
âââ moteur avant droit
vitesse droite ââ L298N ââ€
âââ moteur arriĂšre droit
Exercice 5 : Missions
- Ăcrire un programme qui fait avancer le robot pendant 1 seconde, puis reculer le robot pendant 1 seconde.
- Ă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.
- Poser le robot sur le sol de la salle, aligné sur un repÚre (bord de dalle, ruban adhésif).
- Lancer le quart de tour à gauche avec la durée courante.
- Relever l'angle obtenu.
- Corriger la durée en proportion : si 600 ms donnent 75°, essayer
600 * 90 / 75 = 720ms. - 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 dePOST /api/command).â: ligne envoyĂ©e par le robot (console USB, ouGET /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
mset deseqsont 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 :
â {"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 :
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 :
â {"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 :
â {"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 :
â {"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 :
â {"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 :
â {"left":-180,"right":180,"duration":600,"seq":5735}
â ⊠ligne done:true avec "seq":5735 âŠ
Avec robot.js, classe Mission.
â {"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.
â {"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
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 :
'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
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 :
'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 :
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 :
if (command.has("mission")) {
_mission = (int)command.integer("mission");
_backStep = 0;
}
Question 1 : avancer 1 s, puis reculer 1 s
// 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 :
{"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.
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 :
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 :
{"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
-
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 ».
-
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.
-
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.
-
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).
-
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.
-
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.
-
Variation d'erreur :
[ e_d = e[3] - e[2] = 1 - 4 = -3 ]
-
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.
-
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
-
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) -
Comment corriger ? Régler les gains un par un, sur le robot :
- Mettre (K_i = 0) et (K_d = 0).
- Augmenter (K_p) jusqu'Ă ce que le robot oscille autour de la cible, puis le diviser par deux.
- Augmenter (K_d) jusqu'Ă ce que les oscillations restantes s'amortissent.
- Si une erreur persiste à l'équilibre, ajouter un petit (K_i).
- 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
-
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}).
-
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.
-
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.
-
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
-
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.
-
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.
-
Historique de télémétrie :
- Un échantillon : 4 + 4 (\times) 2 = 12 octets (pas d'octets de remplissage : le
uint32_test 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
- Un échantillon : 4 + 4 (\times) 2 = 12 octets (pas d'octets de remplissage : le
-
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_tau lieu demssur unuint32_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é
-
Autonomie des moteurs :
- Sans oscillations : 1000 mAh / 100 mA = 10 heures
- Avec oscillations : 1000 mAh / 150 mA â 6,67 heures (6 h 40 min)
-
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))
-
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
-
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
-
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.
-
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/linesdu 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/commandtraverse le Wi-Fi jusqu'au robot.
-
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.
-
Latence, canal encombré :
- Meilleur cas : 0 + 0 + 50 + 1 + 50 = 101 ms
- Pire cas : 20 + 100 + 50 + 1 + 50 = 221 ms
-
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 -
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.
-
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.
-
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
msdate 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.
