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 :doc:`../rtos/tp06_dma` et :doc:`../rtos/tp07_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 : .. tikz:: La carte parle XRCE-DDS à l'agent, qui la représente dans le monde ROS 2. \tikzset{box/.style={draw, rounded corners, align=center, font=\small, inner sep=5pt, minimum height=12mm}, lnk/.style={{Stealth}-{Stealth}, thick}, lbl/.style={font=\scriptsize, align=center}} \node[box, fill=orange!12, minimum width=34mm] (c) at (0,0) {\textbf{carte}\\nœud micro-ROS\\[-1pt]\scriptsize client XRCE-DDS}; \node[box, fill=green!8, minimum width=30mm] (a) at (5.6,0) {\textbf{agent}\\micro-ROS\\[-1pt]\scriptsize sur le PC}; \node[box, fill=blue!8, minimum width=36mm] (r) at (12.4,0) {\textbf{nœuds ROS 2}\\\scriptsize ros2 topic echo, RViz,\\[-2pt]\scriptsize vos programmes}; \draw[lnk] (c) -- node[above, lbl]{UART5, J1} node[below, lbl]{XRCE-DDS\\(compact)} (a); \draw[lnk] (a) -- node[above, lbl]{réseau / mémoire} node[below, lbl]{DDS\\domaine 10 + poste} (r); \node[lbl, text width=4.2cm] at (5.6,-1.8) {l'agent crée en DDS les nœuds\\et les topics \emph{au nom} de la carte}; 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.). .. warning:: 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_RX`` en mode **Circular** et ``UART5_TX`` en 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 ``defaultTask`` reç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 : .. code-block:: bash 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** : .. Capture d'écran à insérer : CubeIDE, Properties → C/C++ Build → Settings → Build Steps, la commande Docker en Pre-build steps. Déposez _static/captures/ros/tp01-prebuild.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/ros/tp01-prebuild.png .. :align: center .. :width: 90% .. code-block:: text 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:humble #. **MCU 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/Src`` ces quatre fichiers du dossier ``micro_ros_stm32cubemx_utils/extra_sources`` : ``microros_time.c``, ``microros_allocators.c``, ``custom_memory_manager.c``, et ``microros_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 : .. code-block:: text 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 de ``main``, programmation avec le greffon QSPI, ``-rst``). #. **La RAM.** Dans le script d'édition de liens, faites passer ``.bss`` (et ``._user_heap_stack`` si besoin) de ``DTCMRAM`` à la grande RAM AXI (``RAM_D1`` ou ``RAM``), en remplaçant ``>DTCMRAM`` par ``>RAM_D1`` à la fin de la section. .. admonition:: Questions 1. Quelle taille fait le programme (``arm-none-eabi-size``, ou la console de CubeIDE) ? Combien de fois la flash interne ? 2. Le transport ``dma_transport.c`` déclare ses tampons de réception et d'émission en variables globales, donc dans ``.bss``. Hormis la place, pourquoi est-il **indispensable** que ``.bss`` quitte la DTCM ? (Partie 2, TP 6.) .. dropdown:: Réponses aux questions 1 et 2 :icon: light-bulb :color: info **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 : .. code-block:: c 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`` (voir ``sample_main.c`` dans ``micro_ros_stm32cubemx_utils``). #. **Attendre l'agent.** Tant que ``rmw_uros_ping_agent(200, 1)`` ne renvoie pas ``RMW_RET_OK``, faites clignoter LED_1. Allumez-la fixe ensuite. #. **Le nœud.** Créez-le dans **votre domaine**, avec le nom ``ratbot_lab`` et l'espace de noms ``tp`` : .. code-block:: c 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*) : ``button`` de type ``std_msgs/msg/Bool``, et ``counter`` de type ``std_msgs/msg/Int32`` : .. code-block:: c 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/ttyUSB0`` en général : ``ls /dev/ttyUSB*``). #. Lancez l'agent, dans un terminal : .. code-block:: bash 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 : .. Capture d'écran à insérer : le terminal de l'agent micro-ROS : session établie, création du nœud et des topics. Déposez _static/captures/ros/tp01-agent-session.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/ros/tp01-agent-session.png .. :align: center .. :width: 90% .. code-block:: bash 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 .. Capture d'écran à insérer : un terminal avec ros2 node list, ros2 topic list et ros2 topic echo /tp/button pendant un appui sur SW1. Déposez _static/captures/ros/tp01-topic-echo.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/ros/tp01-topic-echo.png .. :align: center .. :width: 90% .. admonition:: Questions 3. Quel est le nom complet du nœud ? Celui des deux topics ? 4. Appuyez sur SW1 : que montre ``ros2 topic echo`` ? Mesurez la cadence de ``/tp/button`` avec ``ros2 topic hz`` : est-ce celle que vous avez programmée ? 5. Arrêtez l'agent (Ctrl+C), puis relancez-le. La carte reprend-elle ses publications ? Pourquoi ? (On y reviendra au TP 2.) 6. Changez ``ROS_DOMAIN_ID`` dans le terminal, et relancez ``ros2 topic list``. Que voyez-vous ? Que se serait-il passé avec le domaine 77 ? .. dropdown:: Solution de l'étape 4 :icon: code :color: secondary Les inclusions (``USER CODE BEGIN Includes``) : .. code-block:: c #include #include #include #include #include #include #include #include Les fonctions définies dans les fichiers copiés à l'étape 2, déclarées ``extern`` (``USER CODE BEGIN 0``) : .. code-block:: c 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 : .. code-block:: c #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``. .. dropdown:: Réponses aux questions 3 à 6 :icon: light-bulb :color: info **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. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/ros/tp01/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 avec ``rqt_plot`` ou 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 :ref:`etat-ratbot`.