Communication avec la carte STM32
Principe
La carte STM32 exécute un client micro-ROS (voir le chapitre Monter un projet FreeRTOS + micro-ROS). Elle ne parle pas DDS directement : ses nœuds, publications et abonnements sont proxifiés par l’agent micro-ROS qui tourne sur la Raspberry Pi. Vu du graphe ROS 2, les topics du firmware sont des topics ordinaires.
Le client embarqué, l’agent, et le graphe ROS 2.
Transport
Décidé et validé le 2026-09-06 : UART5 + DMA. Porté à 3 Mbaud le
2026-09-08. L’USB CDC est
abandonné — aucun périphérique USB n’existe dans le .ioc, et la carte n’a
pas de connecteur pour ça.
Côté carte |
UART5, connecteur J1 « SERIAL ROS » (JST PH 3 points) :
|
|---|---|
Côté Pi |
/dev/ttyAMA0 — le PL011, basculé sur GPIO 14/15 par
|
Transport |
|
Débit |
3 Mbaud, exact des deux côtés : noyau UART5 = D2PCLK1 = 60 MHz,
suréchantillonnage 16 → |
Préparer la Raspberry
Par défaut l’UART GPIO est occupé par la console noyau et un getty, et
c’est le mini-UART (ttyS0) qui y est câblé — son débit dérive de
l’horloge du VPU, donc inutilisable à ce débit. Trois corrections :
# 1. liberer l'UART de la console noyau
sudo sed -i "s/console=serial0,115200 //" /boot/firmware/cmdline.txt
# 2. basculer le PL011 (ttyAMA0, debit stable) sur GPIO 14/15
echo "dtoverlay=disable-bt" | sudo tee -a /boot/firmware/config.txt
# 3. couper le getty serie
sudo systemctl disable --now serial-getty@ttyS0.service
sudo reboot
Après reboot, /dev/serial0 pointe sur ttyAMA0, en groupe dialout.
Warning
Le DMA du STM32H7 n’atteint pas la DTCMRAM. Les contrôleurs DMA1/DMA2 vivent dans le domaine D2 ; la TCM est câblée directement au cœur, hors de la matrice de bus qu’ils voient. Tout tampon qu’ils touchent doit être ailleurs — sinon le transfert reste figé, sans aucun drapeau d’erreur.
Trois tampons sont concernés, et les trois ont mordu successivement :
le tampon de réception de
dma_transport.c;le tas de micro-ROS (
custom_memory_manager.c), d’où viennent les messages sérialisés ;le tampon d’émission, qui ne nous appartient pas : micro-ROS passe un pointeur sur son propre tas FreeRTOS. D’où un tampon de transit en RAM_D2, avec recopie avant émission.
Tous sont placés dans la section .dma_buffer (RAM_D2, 0x30000000)
définie par le linker, et les horloges des SRAM D2 doivent être activées
(RCC->AHB2ENR) — sans elles les écritures partent dans le vide.
Symptôme à reconnaître : DMAx_Streamy->NDTR figé et M0AR pointant
dans 0x2000xxxx.
Note
it_transport.c (une interruption par octet) convient à la mise au
point, mais pas au nominal : à 1 Mbaud il faut déjà ~100 000 interruptions
par seconde — trois fois plus à 3 — et depuis la flash QSPI le gestionnaire
n’est plus assez rapide.
La réception part alors en ORE (overrun) et la HAL cesse
définitivement de recevoir. Mesuré : UART5->ISR = 0x006210F8.
Le domaine DDS se fixe dans le firmware
Piège non évident : ROS_DOMAIN_ID côté Raspberry ne configure que les
processus du Pi. Le participant DDS que l’agent crée pour le compte de la
carte suit le domaine demandé par le client, qui vaut 0 par défaut. Sans
l’appel ci-dessous, le robot publie hors de portée du reste de la flotte alors
que tout semble fonctionner côté carte :
rcl_init_options_set_domain_id(&init_options, RATBOT_ROS_DOMAIN_ID);
rclc_support_init_with_options(&support, 0, NULL, &init_options, &allocator);
Pourquoi la cinématique est dans le firmware (et pas ros2_control)
Décision (2026-07) : c’est le firmware qui convertit cmd_vel en
vitesses de roues (cinématique inverse) et la position des roues en odom
(cinématique directe). L’alternative envisagée était le
diff_drive_controller de ros2_control sur la Raspberry Pi, la carte
ne recevant que des consignes de vitesses de roues. Arguments de la
décision :
Sécurité au bon endroit : le watchdog (silence de
cmd_vel> ~500 ms → arrêt) vit dans la carte, au plus près des moteurs, et fonctionne même si la Pi ou le WiFi est hors service.Les dynamixels asservissent déjà la vitesse : il ne reste au firmware que les deux formules de la cinématique différentielle (~30 lignes).
Le passage à des moteurs CC ne change pas la donne (la carte le prévoit) : la boucle d’asservissement PID (~1 kHz) devrait tourner dans le STM32 dans les deux options — ros2_control envoie des consignes de roues et suppose que quelqu’un les réalise en dessous. Le travail firmware serait le même.
La mutualisation avec l’URDF est un faux avantage :
diff_drive_controllerlitwheel_separation/wheel_radiusdans son YAML, pas dans l’URDF — il y aurait deux sources dans les deux cas. Le vrai avantage (régler sans reflasher) est conservé : les paramètres de roues du firmware seront configurables depuis la Pi (paramètres micro-ROS, TODO).Moins de trafic et de fragilité : pas de boucle de commande à 50–100 Hz qui traverse la liaison série, pas de hardware interface micro-ROS à écrire ni de
controller_managerà configurer.
Cette décision est réversible et invisible de l’extérieur : le contrat
public reste base/cmd_vel/base/odom. Si un besoin réel de
ros2_control apparaît (bras manipulateur, contrôleurs avancés…), le pont
pourra être écrit sans rien changer pour les projets hôtes.
Messages échangés
Le firmware est organisé en threads FreeRTOS, chacun responsable d’un sous-système. Le tableau donne la correspondance entre les threads et les topics du contrat (Les topics du robot) :
Thread FreeRTOS |
Topics |
Remarques |
|---|---|---|
Moteurs de traction |
|
Le firmware s’abonne à |
IMU (I2C) |
|
Données brutes ; la fusion se fait sur la Pi. |
Température carte (TMP102) |
|
Capteur entre les deux régulateurs de courant. |
Écran + 6 boutons de navigation |
(aucun) |
Menu local du robot, interne au firmware. |
4 boutons + 4 LEDs utilisateur |
|
|
Buzzer actif |
(aucun — reflet dans |
Alertes de sécurité autonomes (température, batterie, danger), indépendantes de ROS. |
Buzzer passif |
|
Mélodies et alertes sonores commandées depuis ROS. |
Capteurs additionnels |
TODO |
Prévu pour des extensions futures. |
TODO : sensors/battery (quel thread ?), service
reset_odometry, et la politique QoS de chaque topic (best effort
pour les flux périodiques, reliable pour les événements).
Dimensionnement de la liaison
Une liaison série à 3 Mbaud (8N1) transporte au mieux ~300 ko/s. Charge réellement mesurée aujourd’hui :
base/odomà 50 Hz : ~38 ko/s (message d’environ 750 octets, dominé par les deux matrices de covariance) ;sensors/imu/data_rawà 25 Hz : ~8 ko/s ;base/joint_statesà 10 Hz,/diagnosticsà 1 Hz en 5 morceaux,status/*et événements : ~7 ko/s.
Soit ~53 ko/s, moins d’un cinquième du lien — et environ 92 messages par seconde.
Important
La vraie limite n’a jamais été le débit du fil, c’est le nombre de messages par seconde — et ce plafond a été levé deux fois depuis.
Essai de charge, un Int32 publié aussi vite que possible :
Débit du lien |
Cadence obtenue |
Occupation du fil |
|---|---|---|
1 Mbaud |
241,5 Hz |
~12 % |
3 Mbaud |
240,6 Hz |
~4 % |
Tripler le débit ne changeait rien, parce que le plafond était ailleurs :
cubemx_transport_write attendait la fin du DMA avec osDelay(1), soit
un tick FreeRTOS entier (1 ms) pour un transfert de 170 µs. Chaque
message coûtait plusieurs ticks, quel que soit le débit.
Premier correctif (2026-09-08) : attente serrée par taskYIELD() au
lieu du sommeil arrondi au tick. On paie le temps réel du DMA, pas un
multiple du tick.
Second correctif (2026-09-10) : activation du cache de données, qui a divisé par sept le temps processeur d’un tour de boucle micro-ROS (42,0 ms → 6,1 ms). Voir Architecture de l’application firmware.
Conséquence pour la conception : compter en messages par seconde, pas en octets. La charge actuelle est de ~92 messages/s pour une boucle qui tourne à 132 Hz — elle n’est donc plus saturée par la publication.
Note
Le plafond n’a pas été re-mesuré depuis ces deux correctifs. L’ancien chiffre de ~240 messages/s ne vaut plus, et il n’y a pas de raison de le remesurer tant que la marge est celle-là. À refaire le jour où le lidar viendra.
Le levier suivant, s’il en faut un : rclc_executor_spin_some coûte 2,5 ms à
chaque tour de boucle, soit environ un tiers du processeur passé à guetter des
commandes qui arrivent à 20 Hz au plus. Le limiter libérerait beaucoup, au prix
d’un peu de latence sur cmd_vel.
Robustesse de la liaison
Quatre défauts trouvés le 2026-09-24, en cherchant pourquoi une carte parfaitement vivante redémarrait en plein essai. Ils sont liés, et les mesures qui suivent viennent du même enregistrement.
La consigne cmd_vel doit être en best effort
rclc_subscription_init_default crée une souscription fiable. Sur la voie
descendante, une trame perdue bloque alors tout ce qui suit, le temps de la
retransmission.
Ce que ça donnait sur un carré : à l’ordre d’arrêt, le robot continue de tourner
à 28 °/s constants pendant que la consigne descend de 0,5 à 0 en 0,9 s. Il
s’arrête net 0,35 s après le dernier zéro — c’est le délai de garde
cmd_vel_timeout_ms qui tombe, pas la consigne qui arrive. Entre-temps,
12 degrés de trop à chaque virage.
Passée en rclc_subscription_init_best_effort : une consigne perdue est
simplement remplacée par la suivante, 50 ms plus tard.
fiable |
best effort |
|
|---|---|---|
dépassement par virage |
+12° |
+2,7° |
synchronisations d’horloge ratées par essai |
32 |
0 à 3 |
Note
Le second chiffre a résolu un mystère qui traînait : rmw_uros_sync_session
échouait systématiquement dès que le robot roulait. Même cause — la voie
descendante était saturée par les retransmissions.
Un message reçu prouve que la session vit
Le chien de garde considérait la liaison morte si la synchronisation d’horloge
échouait pendant SYNC_DEAD_MS (6 s). Comme elle échouait à chaque trajet, il
relançait une carte en parfait état de marche, en plein essai.
La liveness a donc deux sources, et il suffit de l’une :
synchronisation réussie dans les 6 s (SYNC_DEAD_MS)
OU un message reçu dans les 3 s (RX_ALIVE_MS)
ros_node_note_rx() est appelé depuis chaque callback d’abonnement —
cmd_vel, les LED, le buzzer, l’écran. Un agent qui parle est un agent vivant,
quelle que soit l’opinion de l’horloge.
Une erreur de réception ne doit pas rendre la carte sourde
La HAL traite toute erreur de réception comme bloquante : bruit de ligne, débordement, erreur de trame — le DMA s’arrête et ne redémarre pas tout seul. La carte restait sourde jusqu’au chien de garde.
HAL_UART_ErrorCallback relance donc la réception, et compte. Le bloc
serial link de /diagnostics expose :
framing noise overrun parity les erreurs, par nature
restarts relances de la réception DMA
restart_ko relances qui ont échoué
La relance se synchronise avec le chemin de lecture : les deux ne doivent pas réarmer le DMA en même temps.
Tip
Le PL011 du Raspberry produit 3 à 4 débordements par minute à 3 Mbaud
(visibles dans /proc/tty/driver/ttyAMA, colonne oe). Ce n’est pas la
cause des pertes de liaison, mais ça explique que le compteur ne reste pas
à zéro.
Ne pas attendre 1 seconde sur une publication fiable
Sur un topic fiable, le client attend l’acquittement de l’agent jusqu’à
RMW_UXRCE_PUBLISH_RELIABLE_TIMEOUT, 1000 ms par défaut. Une seule
publication qui traîne fait chuter toutes les cadences.
ros_pub_sans_attente() ramène ce délai à ROS_RELIABLE_WAIT_MS (5 ms) —
mais seulement après l’initialisation, et seulement sur les quatre
publishers périodiques.
Danger
Appliqué pendant l’initialisation, ce réglage provoque une boucle de redémarrage : le chien de garde tombe toutes les 12 secondes. La création des entités a besoin du délai long. Le correctif a dû être annulé une première fois pour cette raison.
Limites du client micro-ROS
Le nombre d’entités micro-ROS est figé à la compilation de la bibliothèque
statique (colcon.meta, section rmw_microxrcedds). Les changer impose
de régénérer la lib, pas seulement de recompiler le firmware.
Valeurs retenues le 2026-09-06, MAX_SERVICES relevé le 2026-09-07 :
Paramètre |
Avant |
Après |
Pourquoi |
|---|---|---|---|
|
10 |
24 |
marge confortable pour tout le contrat |
|
5 |
16 |
le contrat en demande déjà cinq ( |
|
1 |
16 |
le serveur de paramètres en consomme 5 à lui seul ( |
|
1 / 1 |
2 / 2 |
marge |
|
4 |
4 |
délibérément inchangé — voir l’avertissement ci-dessous |
|
512 |
2048 |
un |
Warning
``MAX_HISTORY`` et le MTU se multiplient. La mémoire consommée est de
l’ordre de entités × history × MTU : gonfler les deux en même temps est
la façon classique de faire exploser la RAM. Ici, passer les entités de
16 à 48 au total a fait grimper .bss de 61 à 109 Ko, ne laissant
que 21 Ko de DTCMRAM. D’où la bascule de .bss vers l’AXI SRAM
(512 Ko), décrite dans Bootloader et exécution depuis la flash externe.
L’autre limite, celle qu’on rencontre en premier
Les valeurs ci-dessus ne sont pas les seules bornes. L’exécuteur rclc a la sienne, et elle vit dans le firmware — pas dans la bibliothèque :
/* App/ros/ros_node.c */
#define ROS_EXECUTOR_HANDLES 16u
Un handle est consommé par chaque abonnement, minuterie ou service servi par l’exécuteur. Le serveur de paramètres en prend 5, comme ses services.
Danger
Le dépassement est SILENCIEUX. rclc_executor_add_subscription()
renvoie une erreur que rien n’oblige à lire : si on l’ignore, l’abonnement
est créé, visible dans ros2 topic list, apparié côté DDS — et jamais
servi. Aucun message n’arrive, aucune erreur n’est signalée.
Tous les rclc_executor_add_* du projet testent leur retour. Ne pas
déroger : c’est le genre de panne qu’on cherche pendant des heures du
mauvais côté du lien.
Cette limite-là ne demande pas de régénérer la bibliothèque : c’est un
simple #define, une recompilation du firmware suffit.
Télécharger la bibliothèque précompilée
libmicroros.a et ses en-têtes microros_include/ ne sont pas dans le
dépôt (47 Mio, régénérables). Pour compiler le firmware sans passer par
Docker, la bibliothèque utilisée par les robots du parc est proposée en
téléchargement : libmicroros_ratbot_humble.tar.gz
(9,7 Mo), avec son empreinte SHA-256.
Depuis firmware/ratbot_firmware_card/ :
curl -fLO https://dev.ratbot.org/fichiers/libmicroros_ratbot_humble.tar.gz
curl -fLO https://dev.ratbot.org/fichiers/libmicroros_ratbot_humble.tar.gz.sha256
sha256sum -c libmicroros_ratbot_humble.tar.gz.sha256
tar -xzf libmicroros_ratbot_humble.tar.gz \
-C micro_ros_stm32cubemx_utils/microros_static_library
L’archive contient libmicroros/ tel que le produit l’étape Docker
ci-dessous (ROS 2 Humble, colcon.meta du dépôt), plus le colcon.meta
utilisé et les licences de micro-ROS (Apache-2.0). Elle n’est à reprendre que
si colcon.meta change : la bibliothèque est alors à régénérer, et
l’archive à remplacer sur le serveur (dossier fichiers/ du site, que le
déploiement de la doc ne touche pas).
Régénérer la bibliothèque
Important
colcon.meta existe en deux exemplaires, un par dossier de
génération :
micro_ros_stm32cubemx_utils/microros_static_library/library_generation/colcon.meta
micro_ros_stm32cubemx_utils/microros_static_library_ide/library_generation/colcon.meta
Modifier les deux. N’en changer qu’un donne une bibliothèque dont les limites dépendent du dossier utilisé pour la générer — et l’écart ne se voit qu’à l’exécution, quand une entité de trop échoue sans bruit.
Depuis la racine du projet firmware, après avoir modifié colcon.meta :
docker run -it --rm -v $(pwd):/project \
--env MICROROS_LIBRARY_FOLDER=micro_ros_stm32cubemx_utils/microros_static_library \
microros/micro_ros_static_library_builder:humble
Note
libmicroros.a (36 Mio) et microros_include/ sont dans le
.gitignore : un clone frais ne peut pas compiler le firmware sans
cette étape Docker ou le téléchargement de l’archive précompilée
(ci-dessus).
Si le conteneur répond Permission denied sur
library_generation.sh, c’est que le bit exécutable manque
(chmod +x). Il est désormais versionné.
Après régénération, tout reflasher et revalider : la bibliothèque change intégralement.