CM 2026
This commit is contained in:
19
.devcontainer/devcontainer-lock.json
Normal file
19
.devcontainer/devcontainer-lock.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/khannurien/devcontainer-features/claude-code:1": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "ghcr.io/khannurien/devcontainer-features/claude-code@sha256:17eb69bfd15ff5206c0bff2945935a4349f7423cabee461a0ba2681d52205db9",
|
||||
"integrity": "sha256:17eb69bfd15ff5206c0bff2945935a4349f7423cabee461a0ba2681d52205db9"
|
||||
},
|
||||
"ghcr.io/khannurien/devcontainer-features/opencode:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/khannurien/devcontainer-features/opencode@sha256:8df3f8c98a104ce7e089ba3e8df39e0d3890753c8dc92e03deb54527d30d3574",
|
||||
"integrity": "sha256:8df3f8c98a104ce7e089ba3e8df39e0d3890753c8dc92e03deb54527d30d3574"
|
||||
},
|
||||
"ghcr.io/khannurien/devcontainer-features/rtk:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/khannurien/devcontainer-features/rtk@sha256:4a60b3a1d1b64904de3d856c61cdf7e5943e3c74d114609787dc88d9db7c788f",
|
||||
"integrity": "sha256:4a60b3a1d1b64904de3d856c61cdf7e5943e3c74d114609787dc88d9db7c788f"
|
||||
}
|
||||
}
|
||||
}
|
||||
38
.devcontainer/devcontainer.json
Normal file
38
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,38 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/markdown
|
||||
{
|
||||
"name": "Markdown Editing",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/base:bookworm",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// typst is installed by .devcontainer/setup.sh, not by the devcontainers-extra
|
||||
// feature: 1.0.6 resolves "latest" to the legacy v23-03-28 tag.
|
||||
"features": {},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"yzhang.markdown-all-in-one",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"streetsidesoftware.code-spell-checker-cspell-bundled-dictionaries",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"shd101wyy.markdown-preview-enhanced",
|
||||
"bierner.github-markdown-preview",
|
||||
"myriad-dreamin.tinymist"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Installs the colour emoji font typst needs: see .devcontainer/setup.sh.
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh"
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
33
.devcontainer/setup.sh
Normal file
33
.devcontainer/setup.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# noto color emoji, colrv1 flavour.
|
||||
#
|
||||
# typst >= 0.14 exports colr colour glyphs to pdf, but it does not load the cbdt
|
||||
# bitmap font that debian's fonts-noto-color-emoji package installs (`typst fonts`
|
||||
# does not even list it), so emoji used in source.md silently disappear from the
|
||||
# pdf. the colrv1 build is picked up by font fallback, no markup needed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# noto-emoji only tags releases sporadically, so pin the font by commit
|
||||
EMOJI_FONT_COMMIT=f3ae03f5e9b3b8516fa151f7168159ca1a3e7515
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
# typst, static musl build: x86_64 releases no longer ship a gnu build
|
||||
TYPST_VERSION=0.15.1
|
||||
case "$(uname -m)" in
|
||||
x86_64 | amd64) typst_arch=x86_64 ;;
|
||||
aarch64 | arm64) typst_arch=aarch64 ;;
|
||||
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
|
||||
esac
|
||||
curl -fsSL "https://github.com/typst/typst/releases/download/v${TYPST_VERSION}/typst-${typst_arch}-unknown-linux-musl.tar.xz" \
|
||||
| tar -xJ -C "$tmp"
|
||||
sudo install -m 755 "$tmp/typst-${typst_arch}-unknown-linux-musl/typst" /usr/local/bin/typst
|
||||
|
||||
curl -fsSL -o "$tmp/Noto-COLRv1.ttf" \
|
||||
"https://raw.githubusercontent.com/googlefonts/noto-emoji/${EMOJI_FONT_COMMIT}/fonts/Noto-COLRv1.ttf"
|
||||
sudo install -D -m 644 "$tmp/Noto-COLRv1.ttf" /usr/local/share/fonts/Noto-COLRv1.ttf
|
||||
command -v fc-cache > /dev/null && fc-cache -f > /dev/null
|
||||
|
||||
typst fonts | grep -i emoji
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cSpell.language": "fr,en"
|
||||
}
|
||||
BIN
images/Robot_Schema_completed_breadboard.png
Normal file
BIN
images/Robot_Schema_completed_breadboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
BIN
images/Robot_Schema_unwired_breadboard.png
Normal file
BIN
images/Robot_Schema_unwired_breadboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 404 KiB |
BIN
images/vscode-devcontainers.png
Normal file
BIN
images/vscode-devcontainers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
828
td-corrige.md
Normal file
828
td-corrige.md
Normal file
@@ -0,0 +1,828 @@
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
8058
td-corrige.pdf
Normal file
8058
td-corrige.pdf
Normal file
File diff suppressed because it is too large
Load Diff
101
td-corrige.typ
Normal file
101
td-corrige.typ
Normal file
@@ -0,0 +1,101 @@
|
||||
#import "@preview/minicise:0.1.0": sheet
|
||||
#import "@preview/mitex:0.2.7": mitex
|
||||
#import "@preview/cmarker:0.1.10"
|
||||
|
||||
// --------------------------------------------------------- configuration ----
|
||||
|
||||
#show: sheet.with(
|
||||
title: [Exercices de cours -- Corrigés],
|
||||
course: [Objets connectés et robotique],
|
||||
semester: [L3 IFA, S5],
|
||||
date: [2026],
|
||||
author: [Vincent Lannurien],
|
||||
)
|
||||
|
||||
// the markdown source rendered as the body of this sheet
|
||||
#let md-source = "td-corrige.md"
|
||||
|
||||
// set to `false` to drop the first page with the table of contents
|
||||
#let show-toc = false
|
||||
|
||||
// ---------------------------------------------------------------- styles ----
|
||||
|
||||
#show figure.caption: set text(size: 0.9em, fill: luma(70))
|
||||
|
||||
// the outline picks up the headings cmarker emits further down
|
||||
#show outline.entry.where(level: 2): set text(weight: "semibold")
|
||||
|
||||
// each `##` heading starts on a fresh page, except the first one, which stays
|
||||
// on the title page — `weak` avoids a blank page when we are already at the top
|
||||
#let md-section = counter("md-section")
|
||||
#show heading.where(level: 2, outlined: true): it => {
|
||||
context if md-section.get().first() > 0 {
|
||||
pagebreak(weak: true)
|
||||
}
|
||||
md-section.step()
|
||||
it
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ math ----
|
||||
|
||||
// hugo goldmark delimits math with `\(...\)` and `\[...\]`, while cmarker
|
||||
// expects the typst/markdown `$...$` and `$$...$$` forms
|
||||
#let goldmark-math(markdown) = {
|
||||
markdown
|
||||
.replace("\\[", "$$")
|
||||
.replace("\\]", "$$")
|
||||
.replace("\\(", "$")
|
||||
.replace("\\)", "$")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- images ----
|
||||
|
||||
// maximum footprint of an image coming from the markdown source
|
||||
#let img-max-height = 7cm
|
||||
// set to `none` to drop the frame around screenshots
|
||||
#let img-stroke = 0.5pt + luma(180)
|
||||
|
||||
// scale an image down so it fits both the available width and `img-max-height`,
|
||||
// without ever blowing it up past its natural size
|
||||
#let fitted-image(source, ..args) = layout(available => {
|
||||
let natural = measure(image(source, ..args))
|
||||
let factor = calc.min(
|
||||
1.0,
|
||||
available.width / natural.width,
|
||||
img-max-height / natural.height,
|
||||
)
|
||||
image(source, width: natural.width * factor, ..args)
|
||||
})
|
||||
|
||||
// handle `` and `<img src=... width=... height=...>`
|
||||
#let md-image(source, alt: none, width: auto, height: auto, ..args) = {
|
||||
// an explicit width/height in the markdown wins over the automatic fitting
|
||||
let body = if width == auto and height == auto {
|
||||
fitted-image(source, alt: alt, ..args)
|
||||
} else {
|
||||
image(source, alt: alt, width: width, height: height, ..args)
|
||||
}
|
||||
|
||||
let framed = block(stroke: img-stroke, radius: 3pt, clip: true, body)
|
||||
|
||||
// alt text, when there is one, becomes a numbered caption
|
||||
if alt == none or alt == "" {
|
||||
align(center, framed)
|
||||
} else {
|
||||
figure(framed, caption: alt)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- toc ----
|
||||
|
||||
// outline's own title is a level-1 heading, which would outrank every heading in
|
||||
// the source (they start at `##`), so we emit it ourselves at the right level
|
||||
#if show-toc {
|
||||
heading(level: 2, outlined: false)[Sommaire]
|
||||
outline(title: none, depth: 3, indent: 1em)
|
||||
pagebreak()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- exercices ----
|
||||
|
||||
#cmarker.render(goldmark-math(read(md-source)), math: mitex, scope: (image: md-image))
|
||||
316
td.md
Normal file
316
td.md
Normal file
@@ -0,0 +1,316 @@
|
||||
## TD 1 : programmation pour Raspberry Pi Pico
|
||||
|
||||
### API du Pico SDK
|
||||
|
||||
```cpp
|
||||
// Initialise un GPIO afin de pouvoir l'utiliser comme entrée ou sortie.
|
||||
// pin : numéro du GPIO (ex. 15 pour GPIO15).
|
||||
gpio_init(pin);
|
||||
// Configure la direction d'un GPIO.
|
||||
// GPIO_IN : le programme lit l'état électrique de la broche.
|
||||
gpio_set_dir(pin, GPIO_IN);
|
||||
// GPIO_OUT : le programme impose l'état électrique de la broche.
|
||||
gpio_set_dir(pin, GPIO_OUT);
|
||||
// Impose un niveau logique sur un GPIO configuré en sortie.
|
||||
// value : 0/false = niveau bas ; 1/true = niveau haut.
|
||||
// Ex. : allumer/éteindre une LED.
|
||||
gpio_put(pin, value);
|
||||
// Lit le niveau logique présent sur un GPIO configuré en entrée.
|
||||
// Retourne false (niveau bas) ou true (niveau haut).
|
||||
// Ex. : lire l'état d'un bouton ou d'un capteur numérique.
|
||||
bool value = gpio_get(pin);
|
||||
|
||||
// Active une résistance interne reliant faiblement le GPIO au 3.3 V.
|
||||
// Permet notamment de fixer l'état d'une entrée lorsqu'aucun composant
|
||||
// externe ne lui impose de niveau.
|
||||
gpio_pull_up(pin);
|
||||
// Même principe, mais la résistance interne relie le GPIO à GND.
|
||||
gpio_pull_down(pin);
|
||||
|
||||
// Suspend l'exécution du programme pendant une durée donnée.
|
||||
// milliseconds : durée en millisecondes.
|
||||
// Ex. : sleep_ms(1000) attend environ une seconde.
|
||||
sleep_ms(milliseconds);
|
||||
|
||||
// Initialise les entrées/sorties standard utilisées notamment par printf().
|
||||
// À appeler une fois au démarrage si le programme utilise printf().
|
||||
stdio_init_all();
|
||||
// Affiche du texte et des valeurs sur la sortie standard.
|
||||
// %d est remplacé ici par la valeur entière passée en argument.
|
||||
// Ex. : observer la valeur renvoyée par un capteur.
|
||||
printf("value = %d\n", value);
|
||||
|
||||
// Fixe le niveau PWM produit sur un GPIO.
|
||||
// value est compris entre 0 et la valeur passée à pwm_set_wrap().
|
||||
// 0 = toujours bas ; max_value = presque/toujours haut selon la configuration.
|
||||
// Les valeurs intermédiaires font varier le rapport cyclique.
|
||||
// Ex. : contrôler la luminosité d'une LED ou la vitesse d'un moteur.
|
||||
pwm_set_gpio_level(pin, value);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exercice 1 : Branchements
|
||||
|
||||

|
||||
|
||||
### Exercice 2 : Hello GPIO
|
||||
|
||||
Une LED est reliée au GPIO 15 via une résistance.
|
||||
|
||||
1. Configurer GPIO 15 comme sortie numérique.
|
||||
2. Allumer la LED pendant 1 seconde.
|
||||
3. Éteindre la LED pendant 1 seconde.
|
||||
4. Répéter indéfiniment.
|
||||
5. Modifier le programme pour obtenir une fréquence de clignotement de 5 Hz avec un rapport cyclique de 50 %.
|
||||
6. Modifier ensuite le clignotement pour obtenir 100 ms allumé et 900 ms éteint.
|
||||
7. Écrire une fonction, qui allume puis éteint une LED, chaque phase durant `duration_ms` millisecondes, telle que :
|
||||
|
||||
```cpp
|
||||
void blink(uint pin, uint duration_ms);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exercice 3 : Lire un bouton
|
||||
|
||||
Un bouton relie GPIO 14 à GND lorsqu'il est appuyé.
|
||||
|
||||
1. Configurer GPIO 14 comme entrée.
|
||||
2. Activer sa résistance interne de pull-up.
|
||||
3. Lire son état toutes les 100 ms.
|
||||
4. Afficher l'état brut sur le terminal série sous la forme :
|
||||
|
||||
```text
|
||||
button = 1
|
||||
button = 1
|
||||
button = 0
|
||||
...
|
||||
```
|
||||
|
||||
5. Ajouter une variable booléenne `pressed` qui vaut `true` lorsque le bouton est physiquement appuyé.
|
||||
|
||||
Questions :
|
||||
|
||||
- Pourquoi utilise-t-on ici une résistance de pull-up ?
|
||||
- Quelle valeur logique est lue lorsque le bouton est relâché ?
|
||||
- Quelle valeur logique est lue lorsqu'il est appuyé ?
|
||||
- Pourquoi `pressed` peut-il être l'inverse de la valeur retournée par `gpio_get()` ?
|
||||
|
||||
---
|
||||
|
||||
### Exercice 4 : Utilisation des moteurs
|
||||
|
||||
Le robot possède quatre moteurs, regroupés deux par deux : les deux moteurs de gauche sont commandés ensemble, de même que les deux moteurs de droite. Chaque groupe est piloté par un pont en H à l'aide de deux broches du GPIO.
|
||||
|
||||
Pour un groupe de moteurs, la vitesse est comprise entre \([-255, 255]\) :
|
||||
|
||||
- le signe indique le sens de rotation ;
|
||||
- la valeur absolue indique la puissance appliquée par PWM ;
|
||||
- 0 correspond à l'arrêt.
|
||||
|
||||
1. Écrire la fonction `void controleMoteurs(int vitesseGauche, int vitesseDroite)` telle que :
|
||||
|
||||
```cpp
|
||||
controleMoteurs(255, 255); // avancer à pleine puissance
|
||||
controleMoteurs(128, 128); // avancer à environ 50 %
|
||||
controleMoteurs(-255, -255); // reculer à pleine puissance
|
||||
controleMoteurs(0, 0); // arrêt
|
||||
```
|
||||
|
||||
On utilisera les constantes suivantes :
|
||||
|
||||
```cpp
|
||||
#define LEFT_MOTOR_PIN_A ...
|
||||
#define LEFT_MOTOR_PIN_B ...
|
||||
#define RIGHT_MOTOR_PIN_A ...
|
||||
#define RIGHT_MOTOR_PIN_B ...
|
||||
```
|
||||
|
||||
Pour chaque groupe de moteurs :
|
||||
|
||||
- si la vitesse est positive, appliquer le PWM sur la broche A et mettre B à 0 ;
|
||||
- si la vitesse est négative, mettre A à 0 et appliquer le PWM sur B ;
|
||||
- si la vitesse est nulle, mettre les deux broches à 0.
|
||||
|
||||
2. Qu'est-ce que produit l'appel suivant ?
|
||||
|
||||
```cpp
|
||||
controleMoteurs(200, -200);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exercice 5 : Missions
|
||||
|
||||
À partir de l'API de commandes décrite dans le cours :
|
||||
|
||||
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.
|
||||
|
||||
## TD 2 : comportement autonome avec un contrôleur PID
|
||||
|
||||
Équation d'un contrôleur PID en temps continu :
|
||||
|
||||
\[
|
||||
u(t) = K_p \cdot e(t) + K_i \cdot \int_0^t e(\tau) \, d\tau + K_d \cdot \frac{de(t)}{dt}
|
||||
\]
|
||||
|
||||
Et en version discrète, pour une implantation numérique :
|
||||
|
||||
\[
|
||||
u[n] = K_p \cdot e[n] + K_i \cdot \sum_{k=0}^{n} e[k] + K_d \cdot (e[n] - e[n-1])
|
||||
\]
|
||||
|
||||
Expliquer le rôle de chacune des composantes de la fonction discrète du PID, sachant que :
|
||||
|
||||
- \(u[n]\) : commande envoyée au système au pas de temps \(n\)
|
||||
- \(e[n]\) : erreur mesurée au pas de temps \(n\), avec \(e[n] = distance_{cible} - distance_{mesurée}\)
|
||||
|
||||
Pour simplifier :
|
||||
|
||||
- erreur proportionnelle \(e_p\) : distance voulue − distance mesurée
|
||||
- erreur intégrale \(e_i\) : somme des \(e_p\) calculées depuis le début
|
||||
- erreur dérivée \(e_d\) : \(e_{p}[n] - e_{p}[n-1]\)
|
||||
|
||||
**Remarque.** La période d'échantillonnage \(\Delta t\) est fixe. Elle est intégrée aux gains : \(K_i\) contient le facteur \(\Delta t\) de l'intégrale, \(K_d\) contient le facteur \(1 / \Delta t\) de la dérivée.
|
||||
|
||||
---
|
||||
|
||||
### Exercice 1 : composante proportionnelle (P)
|
||||
|
||||
*Comment le terme proportionnel influence-t-il la réaction du robot ?*
|
||||
|
||||
> Un robot doit maintenir une distance de 10 cm avec un mur lorsqu'il le longe.
|
||||
> Sa distance actuelle au mur est de 6 cm.
|
||||
|
||||
1. Que signifie \(e[0]\) ? Calculez sa valeur.
|
||||
2. Si le gain proportionnel \(K_p = 0.5\), quelle est la correction appliquée ?
|
||||
3. Que se passe-t-il si \(K_p = 2\) ?
|
||||
|
||||
---
|
||||
|
||||
### Exercice 2 : composante intégrale (I)
|
||||
|
||||
*Comment l'intégrale corrige-t-elle les erreurs persistantes ?*
|
||||
|
||||
> Le robot reste trop près du mur pendant plusieurs pas de temps.
|
||||
> Aux trois premiers pas de temps : \(e[0] = 4, e[1] = 4, e[2] = 4\).
|
||||
> On garde \(K_p = 0.5\) et on prend un gain intégral \(K_i = 0.1\).
|
||||
|
||||
1. Calculer la commande totale \(u[2]\) après 3 mesures (composantes P et I).
|
||||
2. Quelle est la part due à l'intégrale ?
|
||||
3. Que se passe-t-il si \(K_i\) est trop grand ?
|
||||
|
||||
---
|
||||
|
||||
### Exercice 3 : composante dérivée (D)
|
||||
|
||||
*Comment la dérivée anticipe-t-elle les changements rapides ?*
|
||||
|
||||
> Le robot s'éloigne du mur. On poursuit l'exercice 2 :
|
||||
> \(e[2] = 4\), puis \(e[3] = 1\).
|
||||
> On garde \(K_p = 0.5\) et \(K_i = 0.1\), et on prend un gain dérivé \(K_d = 0.2\).
|
||||
|
||||
1. Calculer la variation d'erreur \(e[3] - e[2]\).
|
||||
2. Calculer la correction due à la dérivée.
|
||||
3. Quelle est la commande totale \(u[3]\) ?
|
||||
|
||||
---
|
||||
|
||||
### Exercice 4 : pertinence des composantes
|
||||
|
||||
*Comment diagnostiquer un mauvais réglage du contrôleur ?*
|
||||
|
||||
> - Situation A : le robot oscille autour de la distance cible.
|
||||
> - Situation B : le robot corrige trop lentement sa trajectoire.
|
||||
> - Situation C : le robot dépasse systématiquement la distance cible.
|
||||
|
||||
1. Que dire de \(K_p\), \(K_i\) et \(K_d\) dans chacune de ces trois situations ?
|
||||
2. Comment peut-on corriger ces dysfonctionnements ?
|
||||
|
||||
## TD 3 : distribution du calcul et étude des compromis
|
||||
|
||||
Le robot embarque un Raspberry Pi Pico WH :
|
||||
|
||||
- microcontrôleur RP2040 : deux cœurs Cortex-M0+, sans unité de calcul flottant ;
|
||||
- 264 KB de SRAM, 2 MB de mémoire flash ;
|
||||
- puce radio CYW43439 : Wi-Fi 802.11n dans la bande 2,4 GHz.
|
||||
|
||||
Le robot crée son propre point d'accès Wi-Fi. Le téléphone s'y connecte, lit la télémétrie par `GET /api/lines` et envoie les commandes par `POST /api/command`.
|
||||
|
||||
---
|
||||
|
||||
### Exercice 1 : temps de calcul et fréquence de lecture
|
||||
|
||||
> Le capteur VL53L1X mesure en continu, indépendamment du processeur. Il produit au plus une mesure toutes les 20 ms (**50 Hz**).
|
||||
>
|
||||
> - Pico WH cadencé à 125 MHz
|
||||
> - Lecture d'un résultat sur le bus I2C : \~0,5 ms
|
||||
> - Calcul d'un PID en `float` : \~20 µs
|
||||
|
||||
1. La boucle de contrôle attend chaque nouvelle mesure, la lit, puis calcule le PID. Quelle est la fréquence maximale de cette boucle ? Quel élément la limite ?
|
||||
2. Quelle part du temps processeur la lecture et le calcul occupent-ils à cette fréquence ? Citer d'autres tâches du robot qui utilisent le reste.
|
||||
3. Que se passe-t-il si l'on fait tourner la boucle à 100 Hz ?
|
||||
4. Proposez une fréquence raisonnable pour un robot roulant à 0.5 m/s.
|
||||
|
||||
---
|
||||
|
||||
### Exercice 2 : occupation mémoire
|
||||
|
||||
> On veut stocker un historique de 100 erreurs pour calculer un PID discret.
|
||||
>
|
||||
> - Pico WH : 264 KB de SRAM
|
||||
> - Chaque erreur est un `float` (4 octets)
|
||||
|
||||
1. Quelle quantité de mémoire est utilisée pour stocker 100 erreurs ? Quelle part de la SRAM cela représente-t-il ?
|
||||
2. Quelles valeurs le PID discret du TD 2 doit-il réellement conserver d'un pas de temps au suivant ? Combien d'octets faut-il ?
|
||||
3. On veut garder sur le robot un historique de la télémétrie pour le tracer sur le téléphone. Un échantillon contient `ms` (`uint32_t`), `front_mm`, `side_mm`, `left` et `right` (`int16_t` chacun). À 50 Hz, combien d'octets par seconde faut-il ? Combien de secondes tiennent dans un tampon de 32 KB ?
|
||||
4. Proposer une solution pour réduire cette empreinte mémoire sans perdre l'information utile.
|
||||
|
||||
---
|
||||
|
||||
### Exercice 3 : énergie et efficacité
|
||||
|
||||
> Un robot utilise un PID mal réglé qui provoque des oscillations.
|
||||
>
|
||||
> - Les moteurs consomment 100 mA en ligne droite
|
||||
> - En cas d'oscillations : consommation moyenne = 150 mA
|
||||
> - Batterie des moteurs = 1000 mAh
|
||||
|
||||
1. Quelle est l'autonomie des moteurs avec et sans oscillations ?
|
||||
2. Quel est le gain en autonomie si le PID est bien réglé ?
|
||||
3. Quelle stratégie PID permet de réduire les oscillations ?
|
||||
|
||||
---
|
||||
|
||||
### Exercice 4 : précision et bruit de mesure
|
||||
|
||||
> Le robot est immobile face à un mur situé à 200 mm. Le VL53L1X renvoie des distances entières en millimètres.
|
||||
>
|
||||
> - Mesures successives : 198, 202, 195, 205, 200
|
||||
> - Distance cible du PID : 200 mm
|
||||
|
||||
1. Calculez l'erreur moyenne et l'écart-type des mesures.
|
||||
2. Calculez les termes \(e[n] - e[n-1]\) du PID pour cette série. Quelle est l'influence du bruit sur le terme dérivé ?
|
||||
3. Proposez une méthode pour filtrer les mesures avant le calcul du PID. Quel est son coût en latence à 50 Hz ?
|
||||
|
||||
---
|
||||
|
||||
### Exercice 5 : latence dans une architecture distribuée
|
||||
|
||||
> Pour alléger le Pico, on propose de calculer le PID sur le téléphone, dans `robot.js`. Le robot envoie ses mesures dans la télémétrie ; le téléphone calcule la commande et la renvoie par Wi-Fi.
|
||||
>
|
||||
> - Capteur VL53L1X : une mesure toutes les 20 ms
|
||||
> - Télémétrie : une ligne toutes les 100 ms (`{"telemetry":100}`)
|
||||
> - Le téléphone lit les lignes en attente par `GET /api/lines` toutes les 100 ms
|
||||
> - Trajet d'une requête ou d'une réponse HTTP sur le Wi-Fi : \~5 ms quand le canal est libre, \~50 ms quand il est encombré
|
||||
> - Calcul du PID dans le navigateur : \~1 ms
|
||||
> - Vitesse du robot : 0.5 m/s
|
||||
|
||||
1. Quelle est la latence entre la mesure et l'application de la commande, dans le meilleur et dans le pire cas, quand le canal est libre ?
|
||||
2. Même question quand le canal est encombré.
|
||||
3. Quelle distance le robot parcourt-il pendant ces latences ?
|
||||
4. Est-ce acceptable pour un robot devant éviter des obstacles à 20 cm ?
|
||||
5. Une salle de TP compte une douzaine de robots, chacun avec son point d'accès 2,4 GHz. Combien de canaux de 20 MHz ne se chevauchent pas dans cette bande ? Quel est l'effet sur la latence ?
|
||||
6. Proposez une stratégie pour réduire l'impact de cette latence (*e.g.* prédiction, calcul local, hybridation).
|
||||
101
td.typ
Normal file
101
td.typ
Normal file
@@ -0,0 +1,101 @@
|
||||
#import "@preview/minicise:0.1.0": sheet
|
||||
#import "@preview/mitex:0.2.7": mitex
|
||||
#import "@preview/cmarker:0.1.10"
|
||||
|
||||
// --------------------------------------------------------- configuration ----
|
||||
|
||||
#show: sheet.with(
|
||||
title: [Exercices de cours],
|
||||
course: [Objets connectés et robotique],
|
||||
semester: [L3 IFA, S5],
|
||||
date: [2026],
|
||||
author: [Vincent Lannurien],
|
||||
)
|
||||
|
||||
// the markdown source rendered as the body of this sheet
|
||||
#let md-source = "td.md"
|
||||
|
||||
// set to `false` to drop the first page with the table of contents
|
||||
#let show-toc = false
|
||||
|
||||
// ---------------------------------------------------------------- styles ----
|
||||
|
||||
#show figure.caption: set text(size: 0.9em, fill: luma(70))
|
||||
|
||||
// the outline picks up the headings cmarker emits further down
|
||||
#show outline.entry.where(level: 2): set text(weight: "semibold")
|
||||
|
||||
// each `##` heading starts on a fresh page, except the first one, which stays
|
||||
// on the title page — `weak` avoids a blank page when we are already at the top
|
||||
#let md-section = counter("md-section")
|
||||
#show heading.where(level: 2, outlined: true): it => {
|
||||
context if md-section.get().first() > 0 {
|
||||
pagebreak(weak: true)
|
||||
}
|
||||
md-section.step()
|
||||
it
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ math ----
|
||||
|
||||
// hugo goldmark delimits math with `\(...\)` and `\[...\]`, while cmarker
|
||||
// expects the typst/markdown `$...$` and `$$...$$` forms
|
||||
#let goldmark-math(markdown) = {
|
||||
markdown
|
||||
.replace("\\[", "$$")
|
||||
.replace("\\]", "$$")
|
||||
.replace("\\(", "$")
|
||||
.replace("\\)", "$")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- images ----
|
||||
|
||||
// maximum footprint of an image coming from the markdown source
|
||||
#let img-max-height = 7cm
|
||||
// set to `none` to drop the frame around screenshots
|
||||
#let img-stroke = 0.5pt + luma(180)
|
||||
|
||||
// scale an image down so it fits both the available width and `img-max-height`,
|
||||
// without ever blowing it up past its natural size
|
||||
#let fitted-image(source, ..args) = layout(available => {
|
||||
let natural = measure(image(source, ..args))
|
||||
let factor = calc.min(
|
||||
1.0,
|
||||
available.width / natural.width,
|
||||
img-max-height / natural.height,
|
||||
)
|
||||
image(source, width: natural.width * factor, ..args)
|
||||
})
|
||||
|
||||
// handle `` and `<img src=... width=... height=...>`
|
||||
#let md-image(source, alt: none, width: auto, height: auto, ..args) = {
|
||||
// an explicit width/height in the markdown wins over the automatic fitting
|
||||
let body = if width == auto and height == auto {
|
||||
fitted-image(source, alt: alt, ..args)
|
||||
} else {
|
||||
image(source, alt: alt, width: width, height: height, ..args)
|
||||
}
|
||||
|
||||
let framed = block(stroke: img-stroke, radius: 3pt, clip: true, body)
|
||||
|
||||
// alt text, when there is one, becomes a numbered caption
|
||||
if alt == none or alt == "" {
|
||||
align(center, framed)
|
||||
} else {
|
||||
figure(framed, caption: alt)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- toc ----
|
||||
|
||||
// outline's own title is a level-1 heading, which would outrank every heading in
|
||||
// the source (they start at `##`), so we emit it ourselves at the right level
|
||||
#if show-toc {
|
||||
heading(level: 2, outlined: false)[Sommaire]
|
||||
outline(title: none, depth: 3, indent: 1em)
|
||||
pagebreak()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- exercices ----
|
||||
|
||||
#cmarker.render(goldmark-math(read(md-source)), math: mitex, scope: (image: md-image))
|
||||
136
tp1.md
Normal file
136
tp1.md
Normal file
@@ -0,0 +1,136 @@
|
||||
## TP 1 : Découverte du robot
|
||||
|
||||
### Objectifs
|
||||
|
||||
- Identifier les composants et vérifier le câblage.
|
||||
- Compiler et charger un programme sur la carte.
|
||||
- Piloter le robot par le câble USB.
|
||||
- Ajouter une commande au programme.
|
||||
|
||||
### Matériel
|
||||
|
||||
- [ ] Raspberry Pi Pico WH
|
||||
- [ ] Shield Grove
|
||||
- [ ] Carte moteur L298N
|
||||
- [ ] 4x moteurs à courant continu
|
||||
- [ ] Télémètre à ultrasons Grove
|
||||
- [ ] Télémètre à infrarouges VL53L1X
|
||||
- [ ] Afficheur 16x2 Grove
|
||||
- [ ] 2x batteries (une pour les moteurs, une pour le Pico via un abaisseur de tension)
|
||||
- [ ] Câble micro-USB
|
||||
|
||||
Le schéma de câblage est dans `schematics/sketches/Robot_Schema_completed.fzz`.
|
||||
|
||||
> ⚠️ Vous êtes responsables de l'inventaire de vos boîtes ! Il sera vérifié en fin de cycle.
|
||||
> Merci de signaler aux enseignants tout matériel cassé ou perdu afin de le remplacer.
|
||||
|
||||
### Mise en place
|
||||
|
||||
- On travaille avec une machine sous **Linux** :
|
||||
- ⚙️ Démarrer sous Debian sur les machines du département
|
||||
- On utilise l'environnement de développement [**Visual Studio Code**](https://code.visualstudio.com/) :
|
||||
- ✅ Disponible sur les machines du département
|
||||
- Installer l'extension pour VS Code : "*Dev Containers*"
|
||||
|
||||

|
||||
- On télécharge l'archive du projet depuis Moodle et on la décompresse :
|
||||
|
||||
```sh
|
||||
unzip rubo-smol.zip
|
||||
```
|
||||
|
||||
- On initialise l'environnement logiciel. Ces étapes devront être réalisées **à chaque TP** :
|
||||
|
||||
- On utilisera un conteneur de développement. Sur les machines du département, il faut exécuter une première fois la commande `docker` pour initialiser le *daemon* :
|
||||
|
||||
```sh
|
||||
docker
|
||||
```
|
||||
|
||||
- Ouvrir le projet dans VS Code :
|
||||
|
||||
```sh
|
||||
code rubo-smol/
|
||||
```
|
||||
|
||||
- Rouvrir le projet dans le conteneur (<kbd>Ctrl</kbd> + <kbd>P</kbd> > "*Reopen in Container*")
|
||||
|
||||
### Déroulé
|
||||
|
||||
1. Ouvrir `robot/Config.h`. Le schéma de câblage est en tête de fichier. Vérifier chaque fil sur le robot avant de brancher l'alimentation.
|
||||
|
||||
2. Dans le conteneur, compiler le firmware (le compilateur et le SDK sont déjà installés) :
|
||||
|
||||
```sh
|
||||
python3 tools/build.py
|
||||
```
|
||||
|
||||
3. Brancher la carte en USB, et vérifier qu'elle est bien reconnue :
|
||||
|
||||
```sh
|
||||
python3 tools/pico.py list
|
||||
```
|
||||
|
||||
Puis charger le programme. Enclencher `BOOTSEL` n'est normalement pas nécessaire :
|
||||
|
||||
```sh
|
||||
python3 tools/build.py --flash
|
||||
```
|
||||
|
||||
4. Ouvrir la console :
|
||||
|
||||
```sh
|
||||
python3 tools/pico.py console
|
||||
```
|
||||
|
||||
Quitter par <kbd>Ctrl</kbd> + <kbd>A</kbd> puis <kbd>Ctrl</kbd> + <kbd>X</kbd>. Relever la liste des périphériques détectés au démarrage. Un capteur marqué `ENABLED BUT NOT RESPONDING` signale un problème de câblage.
|
||||
|
||||
5. Tester les moteurs en tapant les commandes dans la console :
|
||||
|
||||
```json
|
||||
{"left": 200, "right": 200}
|
||||
{"left": 200, "right": -200}
|
||||
{"left": 0, "right": 200}
|
||||
```
|
||||
|
||||
Si un moteur tourne à l'envers, inverser la valeur de `LEFT_MOTOR_REVERSED` ou de `RIGHT_MOTOR_REVERSED` dans `Config.h`, puis recompiler et recharger.
|
||||
Si le robot recule au lieu d'avancer, inverser les deux ne suffit pas : il faut aussi échanger les broches du moteur gauche et du moteur droit, sinon les virages sont inversés.
|
||||
|
||||
6. Relever la vitesse minimale à laquelle chaque moteur démarre. En dessous, le moteur bourdonne sans tourner.
|
||||
|
||||
7. Approcher la main du capteur et suivre la valeur `front_mm` dans la télémétrie. N'utiliser cette distance que si `front_ok` est vrai ; sinon `front_mm` vaut `-1`.
|
||||
|
||||
8. Lire la fonction `enforceSafety()` dans `robot/Robot.h` et les constantes `DEFAULT_DURATION_MS` et `MAX_DURATION_MS` dans `Config.h`.
|
||||
|
||||
9. Écrire la mission 1, arrêt devant un obstacle.
|
||||
- Lire d'abord `Robot::update()` dans `robot/Robot.h` : à chaque tour, il actualise les capteurs, lit les commandes, puis appelle `mission()`.
|
||||
- `{"mission":1}` ne fait pas avancer le robot ! La mission arrête seulement un mouvement en cours en cas d'obstacle.
|
||||
- Dans `Robot.h`, remplacer `MISSION_NAMES = ""` par `MISSION_NAMES = "obstacle"`.
|
||||
- Dans le `switch` de `mission()`, ajouter `case 1: stopAtObstacle(); break;` après `case 0`.
|
||||
- Toujours **dans la classe `Robot`**, près de `mission()`, commencer la méthode suivante :
|
||||
|
||||
```cpp
|
||||
void stopAtObstacle() {
|
||||
if (!front.valid()) return; // ne pas utiliser une mesure absente
|
||||
// À compléter : si front.distanceMm() n'est pas sous 150 mm, retourner.
|
||||
// À compléter : si aucune roue n'avance, retourner.
|
||||
// À compléter : arrêter le mouvement et signaler sa fin.
|
||||
}
|
||||
```
|
||||
|
||||
- `front.distanceMm()` donne la distance en millimètres.
|
||||
- `motors.left()` et `motors.right()` donnent des vitesses signées (positive = marche avant).
|
||||
- Pour la dernière ligne, chercher comment `enforceSafety()` arrête les roues, désarme `_hasDeadline` et envoie une télémétrie `done`.
|
||||
- On utilise `drive()` pour **démarrer** un mouvement avec une échéance ; ici on veut arrêter celui qui est en cours. La méthode doit retourner tout de suite, sans boucle d'attente.
|
||||
|
||||
- Compiler avec `python3 tools/build.py` ; corriger les erreurs avant de charger avec `python3 tools/build.py --flash`. Poser d'abord le robot sur un support, roues en l'air. Dans la console, envoyer :
|
||||
|
||||
```json
|
||||
{"mission": 1}
|
||||
{"left": 200, "right": 200, "duration": 3000}
|
||||
```
|
||||
|
||||
- Approcher une main du capteur avant à moins de 150 mm : les roues doivent s'arrêter **avant** les trois secondes, et une ligne de télémétrie doit contenir `"done":true` et `"moving":false` (cette mission envoie elle-même la ligne `done` quand elle arrête les roues).
|
||||
- Avec la main toujours proche, essayer `{"left":-150,"right":-150}` : la marche arrière doit rester
|
||||
possible.
|
||||
- `{"mission":0}` désactive la mission ; `{"stop":true}` arrête aussi le robot et la désactive, mais n'envoie pas de ligne `done`.
|
||||
101
tp1.typ
Normal file
101
tp1.typ
Normal file
@@ -0,0 +1,101 @@
|
||||
#import "@preview/minicise:0.1.0": sheet
|
||||
#import "@preview/mitex:0.2.7": mitex
|
||||
#import "@preview/cmarker:0.1.10"
|
||||
|
||||
// --------------------------------------------------------- configuration ----
|
||||
|
||||
#show: sheet.with(
|
||||
title: [Exercices de TP],
|
||||
course: [Objets connectés et robotique],
|
||||
semester: [L3 IFA, S5],
|
||||
date: [2026],
|
||||
author: [Vincent Lannurien],
|
||||
)
|
||||
|
||||
// the markdown source rendered as the body of this sheet
|
||||
#let md-source = "tp1.md"
|
||||
|
||||
// set to `false` to drop the first page with the table of contents
|
||||
#let show-toc = true
|
||||
|
||||
// ---------------------------------------------------------------- styles ----
|
||||
|
||||
#show figure.caption: set text(size: 0.9em, fill: luma(70))
|
||||
|
||||
// the outline picks up the headings cmarker emits further down
|
||||
#show outline.entry.where(level: 2): set text(weight: "semibold")
|
||||
|
||||
// each `##` heading starts on a fresh page, except the first one, which stays
|
||||
// on the title page — `weak` avoids a blank page when we are already at the top
|
||||
#let md-section = counter("md-section")
|
||||
#show heading.where(level: 2, outlined: true): it => {
|
||||
context if md-section.get().first() > 0 {
|
||||
pagebreak(weak: true)
|
||||
}
|
||||
md-section.step()
|
||||
it
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ math ----
|
||||
|
||||
// hugo goldmark delimits math with `\(...\)` and `\[...\]`, while cmarker
|
||||
// expects the typst/markdown `$...$` and `$$...$$` forms
|
||||
#let goldmark-math(markdown) = {
|
||||
markdown
|
||||
.replace("\\[", "$$")
|
||||
.replace("\\]", "$$")
|
||||
.replace("\\(", "$")
|
||||
.replace("\\)", "$")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- images ----
|
||||
|
||||
// maximum footprint of an image coming from the markdown source
|
||||
#let img-max-height = 6cm
|
||||
// set to `none` to drop the frame around screenshots
|
||||
#let img-stroke = 0.5pt + luma(180)
|
||||
|
||||
// scale an image down so it fits both the available width and `img-max-height`,
|
||||
// without ever blowing it up past its natural size
|
||||
#let fitted-image(source, ..args) = layout(available => {
|
||||
let natural = measure(image(source, ..args))
|
||||
let factor = calc.min(
|
||||
1.0,
|
||||
available.width / natural.width,
|
||||
img-max-height / natural.height,
|
||||
)
|
||||
image(source, width: natural.width * factor, ..args)
|
||||
})
|
||||
|
||||
// handle `` and `<img src=... width=... height=...>`
|
||||
#let md-image(source, alt: none, width: auto, height: auto, ..args) = {
|
||||
// an explicit width/height in the markdown wins over the automatic fitting
|
||||
let body = if width == auto and height == auto {
|
||||
fitted-image(source, alt: alt, ..args)
|
||||
} else {
|
||||
image(source, alt: alt, width: width, height: height, ..args)
|
||||
}
|
||||
|
||||
let framed = block(stroke: img-stroke, radius: 3pt, clip: true, body)
|
||||
|
||||
// alt text, when there is one, becomes a numbered caption
|
||||
if alt == none or alt == "" {
|
||||
align(center, framed)
|
||||
} else {
|
||||
figure(framed, caption: alt)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- toc ----
|
||||
|
||||
// outline's own title is a level-1 heading, which would outrank every heading in
|
||||
// the source (they start at `##`), so we emit it ourselves at the right level
|
||||
#if show-toc {
|
||||
heading(level: 2, outlined: false)[Sommaire]
|
||||
outline(title: none, depth: 3, indent: 1em)
|
||||
pagebreak()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- exercices ----
|
||||
|
||||
#cmarker.render(goldmark-math(read(md-source)), math: mitex, scope: (image: md-image))
|
||||
Reference in New Issue
Block a user