TP 1 — Premier nœud micro-ROS
Objectifs
comprendre ce que sont un nœud, un topic et un message ROS 2 ;
intégrer micro-ROS à un projet CubeIDE avec FreeRTOS ;
faire publier par la carte l’état d’un bouton et un compteur, et les lire sur un ordinateur avec les outils de ROS 2.
Durée indicative : deux séances de 3 h. Prérequis : la partie 2, en particulier TP 6 — Le DMA et TP 7 — Un programme en flash externe.
Matériel : la carte, son câble de programmation, un adaptateur USB-série 3,3 V branché sur le connecteur J1 (comme au TP 5 de la partie 1), et un ordinateur sous Linux avec ROS 2 Humble et Docker.
Ce qu’il faut savoir
ROS 2 en quelques mots
Un programme ROS 2 est fait de nœuds (nodes) : des programmes qui
s’échangent des messages sur des topics, des canaux nommés comme
/ratbot_1/base/cmd_vel. Un nœud publie sur un topic, d’autres s’y
abonnent ; aucun ne connaît les autres. Chaque topic a un type de
message, par exemple std_msgs/msg/Bool (un booléen) ou
sensor_msgs/msg/Temperature.
Les nœuds se découvrent seuls sur le réseau, grâce à DDS, la couche de
communication de ROS 2. Tous ceux qui partagent le même numéro de domaine
(ROS_DOMAIN_ID) se voient.
micro-ROS et l’agent
Un microcontrôleur n’a ni la mémoire ni le réseau qu’il faudrait pour parler DDS. micro-ROS lui fournit une version allégée de ROS 2 : la carte parle un protocole compact, XRCE-DDS, sur une simple liaison série, à un programme qui tourne sur l’ordinateur, l”agent. L’agent crée de l’autre côté les vrais nœuds et topics DDS, au nom de la carte :

La carte parle XRCE-DDS à l’agent, qui la représente dans le monde ROS 2.
C’est exactement ce que fait le robot : sa carte est reliée au Raspberry Pi par UART5, à 3 Mbit/s, et l’agent tourne sur le Raspberry Pi.
Un numéro de domaine par poste
Dans une salle de TP, toutes les cartes publieraient le même topic, et chaque
poste verrait celles des voisins. Chaque poste utilise donc son propre
domaine : 10 + le numéro du poste (11 pour le poste 1, etc.).
Avertissement
N’utilisez jamais le domaine 77 : c’est celui des robots. Une carte de TP qui y publierait apparaîtrait au milieu de la flotte.
Le domaine se règle des deux côtés : dans le programme de la carte, et sur
l’ordinateur, avec export ROS_DOMAIN_ID=11.
Étape 1 — Le projet CubeMX
Créez un projet
ros_tp01_noeud: FREERTOS en CMSIS_V2, Timebase Source sur TIM6, LED_1 à LED_4 en sortie, SW1 en entrée sans tirage (partie 2, TP 1).UART5, sur PB12 (RX) et PB13 (TX), en Asynchronous, 115 200 bit/s, 8N1.
Onglet DMA Settings de UART5 : ajoutez
UART5_RXen mode Circular etUART5_TXen mode Normal, toutes deux en priorité Very High. Onglet NVIC Settings : activez UART5 global interrupt.FREERTOS → Config parameters : TOTAL_HEAP_SIZE à 65536. Tasks and Queues : la tâche
defaultTaskreçoit une pile de 3000 mots (12 Kio : micro-ROS en demande plus de 10).CORTEX_M7 : MPU NOT USED, I-cache activé, D-cache désactivé (partie 2, TP 6 et 7).
Générez le code.
Étape 2 — Ajouter micro-ROS au projet
micro-ROS est fourni sous forme d’une bibliothèque statique, compilée pour le Cortex-M7 et pour votre projet, dans un conteneur Docker.
Dans le dossier du projet :
git clone -b humble https://github.com/micro-ROS/micro_ros_stm32cubemx_utils.git
Project → Properties → C/C++ Build → Settings → Build Steps → Pre-build steps, sur une seule ligne :
docker pull microros/micro_ros_static_library_builder:humble && docker run --rm -v ${workspace_loc:/${ProjName}}:/project --env MICROROS_LIBRARY_FOLDER=micro_ros_stm32cubemx_utils/microros_static_library_ide microros/micro_ros_static_library_builder:humbleMCU GCC Compiler → Include paths : ajoutez
micro_ros_stm32cubemx_utils/microros_static_library_ide/libmicroros/include.MCU GCC Linker → Libraries : dans Library search path, le chemin absolu de
micro_ros_stm32cubemx_utils/microros_static_library_ide/libmicroros; dans Libraries,microros.Copiez dans
Core/Srcces quatre fichiers du dossiermicro_ros_stm32cubemx_utils/extra_sources:microros_time.c,microros_allocators.c,custom_memory_manager.c, etmicroros_transports/dma_transport.c.Compilez. Le premier build est long (une dizaine de minutes) : la bibliothèque est construite. Les suivants la réutilisent.
Étape 3 — Faire tenir le programme
La compilation échoue sans doute à l’édition de liens, avec deux messages :
region `FLASH' overflowed by ... bytes
region `DTCMRAM' overflowed by ... bytes
La flash. Le programme dépasse les 128 Kio de la flash interne : il doit vivre en flash externe, exactement comme au TP 7 de la partie 2 (région
FLASHà0x90000000,__enable_irq()au début demain, programmation avec le greffon QSPI,-rst).La RAM. Dans le script d’édition de liens, faites passer
.bss(et._user_heap_stacksi besoin) deDTCMRAMà la grande RAM AXI (RAM_D1ouRAM), en remplaçant>DTCMRAMpar>RAM_D1à la fin de la section.
Questions
Quelle taille fait le programme (
arm-none-eabi-size, ou la console de CubeIDE) ? Combien de fois la flash interne ?Le transport
dma_transport.cdéclare ses tampons de réception et d’émission en variables globales, donc dans.bss. Hormis la place, pourquoi est-il indispensable que.bssquitte la DTCM ? (Partie 2, TP 6.)
Réponses aux questions 1 et 2
1. La taille du programme. Plusieurs centaines de Kio (de l’ordre de
200 à 300 Kio avec deux types de messages), soit deux à trois fois la
flash interne. arm-none-eabi-size affiche trois nombres : text (le
code et les constantes, en flash), data (les variables initialisées,
en flash et en RAM), bss (les variables à zéro, en RAM seulement).
C’est text + data qui doit tenir en flash.
D’où vient tout ce code ? La bibliothèque micro-ROS embarque une partie de
la pile ROS 2 (rcl, rclc), le client XRCE-DDS, la sérialisation des
messages (CDR), et le code de chaque type de message utilisé. Le robot,
avec une vingtaine de types, dépasse 400 Kio.
2. Les tampons du transport hors de la DTCM. Le transport
dma_transport.c reçoit par DMA (en mode circulaire) et émet par DMA :
ses tampons doivent être accessibles aux contrôleurs DMA1 et DMA2, qui
n’ont pas accès à la DTCM (partie 2, TP 6). Si .bss reste en DTCM, la
carte n’envoie ni ne reçoit rien : le DMA s’arrête sur une erreur de bus,
et la carte attend l’agent indéfiniment, LED_1 clignotante, sans autre
message. Déplacer .bss en AXI SRAM résout à la fois le problème de
place et celui d’accès.
Étape 4 — Le nœud et ses deux topics
Dans StartDefaultTask, et seulement là (micro-ROS n’est pas prévu pour être
appelé depuis plusieurs tâches) :
Le transport et la mémoire. Indiquez à micro-ROS d’utiliser UART5, et son propre allocateur :
rmw_uros_set_custom_transport(true, (void *)&huart5, cubemx_transport_open, cubemx_transport_close, cubemx_transport_write, cubemx_transport_read); rcl_allocator_t a = rcutils_get_zero_initialized_allocator(); a.allocate = microros_allocate; a.deallocate = microros_deallocate; a.reallocate = microros_reallocate; a.zero_allocate = microros_zero_allocate; rcutils_set_default_allocator(&a);
Ces fonctions sont définies dans les fichiers copiés à l’étape 2 : déclarez-les
extern(voirsample_main.cdansmicro_ros_stm32cubemx_utils).Attendre l’agent. Tant que
rmw_uros_ping_agent(200, 1)ne renvoie pasRMW_RET_OK, faites clignoter LED_1. Allumez-la fixe ensuite.Le nœud. Créez-le dans votre domaine, avec le nom
ratbot_labet l’espace de nomstp:rcl_allocator_t allocator = rcl_get_default_allocator(); rcl_init_options_t opts = rcl_get_zero_initialized_init_options(); rcl_init_options_init(&opts, allocator); rcl_init_options_set_domain_id(&opts, 11); /* 10 + numéro du poste */ rclc_support_init_with_options(&support, 0, NULL, &opts, &allocator); rclc_node_init_default(&node, "ratbot_lab", "tp", &support);
Deux éditeurs (publishers) :
buttonde typestd_msgs/msg/Bool, etcounterde typestd_msgs/msg/Int32:rclc_publisher_init_default(&pub_button, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Bool), "button");
La boucle : toutes les 100 ms (
osDelayUntil), publiez l’état de SW1 ; toutes les secondes, le compteur, augmenté de 1.
Vérifiez chaque code de retour (RCL_RET_OK) : en cas d’échec, allumez
LED_4 et arrêtez-vous là. C’est votre seul moyen de savoir quelle étape a
échoué.
Étape 5 — Côté ordinateur
Branchez l’adaptateur USB-série sur J1, et repérez son nom (
/dev/ttyUSB0en général :ls /dev/ttyUSB*).Lancez l’agent, dans un terminal :
docker run -it --rm -v /dev:/dev --privileged --net=host --ipc=host \ microros/micro-ros-agent:humble serial --dev /dev/ttyUSB0 -b 115200
Faites redémarrer la carte (bouton reset) : LED_1 cesse de clignoter, et l’agent affiche la création de la session, du nœud et des topics.
Dans un autre terminal :
source /opt/ros/humble/setup.bash export ROS_DOMAIN_ID=11 ros2 node list ros2 topic list ros2 topic echo /tp/button ros2 topic hz /tp/counter
Questions
Quel est le nom complet du nœud ? Celui des deux topics ?
Appuyez sur SW1 : que montre
ros2 topic echo? Mesurez la cadence de/tp/buttonavecros2 topic hz: est-ce celle que vous avez programmée ?Arrêtez l’agent (Ctrl+C), puis relancez-le. La carte reprend-elle ses publications ? Pourquoi ? (On y reviendra au TP 2.)
Changez
ROS_DOMAIN_IDdans le terminal, et relancezros2 topic list. Que voyez-vous ? Que se serait-il passé avec le domaine 77 ?
Solution de l’étape 4
Les inclusions (USER CODE BEGIN Includes) :
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <uxr/client/transport.h>
#include <rmw_microxrcedds_c/config.h>
#include <rmw_microros/rmw_microros.h>
#include <std_msgs/msg/bool.h>
#include <std_msgs/msg/int32.h>
Les fonctions définies dans les fichiers copiés à l’étape 2, déclarées
extern (USER CODE BEGIN 0) :
bool cubemx_transport_open(struct uxrCustomTransport *transport);
bool cubemx_transport_close(struct uxrCustomTransport *transport);
size_t cubemx_transport_write(struct uxrCustomTransport *transport,
const uint8_t *buf, size_t len, uint8_t *err);
size_t cubemx_transport_read(struct uxrCustomTransport *transport,
uint8_t *buf, size_t len, int timeout, uint8_t *err);
void *microros_allocate(size_t size, void *state);
void microros_deallocate(void *pointer, void *state);
void *microros_reallocate(void *pointer, size_t size, void *state);
void *microros_zero_allocate(size_t number_of_elements, size_t size_of_element,
void *state);
La tâche :
#define POSTE_DOMAIN_ID 11u /* 10 + numéro du poste ; JAMAIS 77 */
void StartDefaultTask(void *argument)
{
rcl_allocator_t allocator;
rcl_init_options_t opts;
rclc_support_t support;
rcl_node_t node;
rcl_publisher_t pub_button, pub_counter;
std_msgs__msg__Bool msg_button;
std_msgs__msg__Int32 msg_counter;
uint32_t next, tick = 0u;
(void)argument;
/* 1. le transport et la mémoire */
rmw_uros_set_custom_transport(true, (void *)&huart5,
cubemx_transport_open, cubemx_transport_close,
cubemx_transport_write, cubemx_transport_read);
allocator = rcutils_get_zero_initialized_allocator();
allocator.allocate = microros_allocate;
allocator.deallocate = microros_deallocate;
allocator.reallocate = microros_reallocate;
allocator.zero_allocate = microros_zero_allocate;
if (!rcutils_set_default_allocator(&allocator)) { fail(1u); }
/* 2. attendre l'agent */
while (rmw_uros_ping_agent(200, 1) != RMW_RET_OK)
{
HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin);
osDelay(200);
}
HAL_GPIO_WritePin(LED_1_GPIO_Port, LED_1_Pin, GPIO_PIN_SET);
/* 3. le nœud, dans le domaine du poste */
allocator = rcl_get_default_allocator();
opts = rcl_get_zero_initialized_init_options();
if (rcl_init_options_init(&opts, allocator) != RCL_RET_OK) { fail(2u); }
if (rcl_init_options_set_domain_id(&opts, POSTE_DOMAIN_ID) != RCL_RET_OK) { fail(2u); }
if (rclc_support_init_with_options(&support, 0, NULL, &opts, &allocator)
!= RCL_RET_OK) { fail(3u); }
if (rclc_node_init_default(&node, "ratbot_lab", "tp", &support) != RCL_RET_OK) { fail(4u); }
/* 4. deux éditeurs */
if (rclc_publisher_init_default(&pub_button, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Bool), "button") != RCL_RET_OK) { fail(5u); }
if (rclc_publisher_init_default(&pub_counter, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32), "counter") != RCL_RET_OK) { fail(5u); }
/* 5. la boucle : bouton à 10 Hz, compteur à 1 Hz */
msg_counter.data = 0;
next = osKernelGetTickCount();
for (;;)
{
msg_button.data = (HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin) == GPIO_PIN_SET);
if (rcl_publish(&pub_button, &msg_button, NULL) != RCL_RET_OK)
{
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin); /* échec ponctuel */
}
if (++tick % 10u == 0u)
{
if (rcl_publish(&pub_counter, &msg_counter, NULL) != RCL_RET_OK)
{
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
}
msg_counter.data++;
}
next += 100u;
osDelayUntil(next);
}
}
fail(n) allume LED_4 et fait clignoter LED_1 n fois en boucle (voir le
fichier complet) : sans terminal ni débogueur, c’est le seul moyen de
savoir quelle étape a échoué.
Les deux allocateurs. Le premier (microros_allocate & co) est celui de
la pile micro-ROS : il puise dans un tas qui lui est réservé
(custom_memory_manager.c), séparé du tas de FreeRTOS. Le second
(rcl_get_default_allocator) est l’interface par laquelle rcl demande
de la mémoire : après rcutils_set_default_allocator, il renvoie le
premier.
Le ping avant tout. Sans agent, rclc_support_init échoue, et on ne
saurait pas distinguer « l’agent n’est pas lancé » de « la pile est
cassée ». Le ping lève l’ambiguïté : LED_1 qui clignote veut dire
« j’attends l’agent ».
Ce qui part sur le fil. rcl_publish sérialise le message (format
CDR : un booléen tient en un octet), l’encapsule dans un paquet XRCE, que
le transport encadre (framing : début, longueur, somme de contrôle)
avant de l’envoyer par DMA. L’agent fait le chemin inverse, et publie le
message en DDS au nom de la carte.
ROSIDL_GET_MSG_TYPE_SUPPORT donne la description du type de message
(comment le sérialiser), générée à la compilation de la bibliothèque : on
ne peut publier que les types compilés dans libmicroros.
Réponses aux questions 3 à 6
3. Les noms. Le nœud est /tp/ratbot_lab : son nom, précédé de son
espace de noms. Les topics ont des noms relatifs (sans / au début) :
ils sont placés dans l’espace de noms du nœud, d’où /tp/button et
/tp/counter. Un nom commençant par / serait absolu, et ignorerait
l’espace de noms. C’est ce mécanisme qui permet au même firmware de servir
tous les robots : seul l’espace de noms change (/ratbot_1,
/ratbot_2…).
4. Le bouton. data: true tant que SW1 est enfoncé, data: false
sinon, à 10 Hz, la cadence de la boucle. ros2 topic hz donne une
moyenne très proche de 10,000 Hz grâce à osDelayUntil, avec une petite
gigue due à la liaison série et au réseau. Publier un état toutes les
100 ms, qu’il change ou non, est simple et robuste ; publier seulement
les changements économise la liaison (exercice).
5. L’agent relancé. Non, la carte ne reprend pas ses publications.
La carte et l’agent partagent une session XRCE : l’agent garde, pour
elle, la liste de ses entités (nœud, éditeurs) et les crée en DDS. Un agent
relancé ne connaît plus rien de cette session ; les messages de la carte
font référence à des entités qui n’existent plus, et sont ignorés. Ce
programme ne le détecte pas (rcl_publish échoue, LED_3 clignote). Il
faut redémarrer la carte… ou surveiller l’agent et tout recréer : c’est
l’étape 3 du TP 2.
6. Le domaine. Dans un autre domaine, ros2 topic list ne montre plus
les topics de la carte : DDS ne fait se rencontrer que les participants
d’un même domaine (le numéro de domaine choisit les ports réseau utilisés
pour la découverte). Dans le domaine 77, la carte apparaîtrait parmi
les vrais robots du laboratoire : ses topics se mêleraient aux leurs,
et un nom mal choisi pourrait même entrer en conflit avec ceux d’un robot.
Le domaine se fixe dans le firmware : l’agent crée le participant DDS de
la carte dans le domaine qu’elle demande, quel que soit le ROS_DOMAIN_ID
de la machine où il tourne.
Tout le code de la correction
taches.c — compilé sans
avertissement contre les en-têtes de micro-ROS Humble, la HAL et FreeRTOS
du firmware RatBot (sans édition de liens ni essai sur carte).
Le fichier contient les inclusions, les déclarations et la fonction de la
tâche micro-ROS, qui remplace le squelette StartDefaultTask généré par
CubeMX. La configuration attendue est rappelée en tête du fichier, et les
réponses aux questions à la fin.
Exercices supplémentaires
Ces exercices n’ont pas de correction.
Les quatre boutons : un seul topic, de type
std_msgs/msg/UInt8, dont chaque bit donne l’état d’un bouton ; publié seulement quand il change.La batterie : publiez la tension (TP 6 de la partie 1) en
std_msgs/msg/Float32, une fois par seconde, et tracez-la avecrqt_plotou PlotJuggler.Le débit : passez la liaison à 921 600 bit/s (dans CubeMX et pour l’agent), publiez le compteur aussi vite que possible, et mesurez la cadence maximale avec
ros2 topic hz.
Après le TP
La flash externe contient votre programme : réinstallez celui du robot, voir Remettre la carte en état RatBot.