ROS 2 micro-ROS Guide 2026
micro-ROS brings ROS 2 pub/sub to microcontrollers like ESP32, STM32, and Raspberry Pi Pico. An agent process on your Linux host bridges the MCU to full ROS 2 over UART, UDP, or USB.
Architecture Overview
Microcontroller
ESP32 / STM32 / Pico
micro-ROS client library
(C, FreeRTOS or bare-metal)
Transport
UART / USB-CDC / UDP / TCP
micro-ROS Agent
Linux host
bridges to full ROS 2 DDS network
ROS 2 Nodes
Any DDS middleware
| Board | RAM | Transport | Recommended Setup |
|---|---|---|---|
| ESP32 | 520 KB | UART / UDP (WiFi) | Arduino + micro_ros_arduino library |
| STM32 | 128 KB+ | UART / USB-CDC | micro_ros_stm32cubemx_utils or CMake + FreeRTOS |
| Raspberry Pi Pico | 264 KB | UART / USB-CDC | micro_ros_raspberrypi_pico_sdk |
| Arduino Nano RP2040 | 264 KB | UDP (WiFi) | micro_ros_arduino + WiFi transport |
Step 1 — Install the micro-ROS Agent
The agent runs on your Linux host and translates DDS ↔ micro-ROS serial protocol:
# Option A: from apt (Jazzy / Humble) sudo apt install ros-jazzy-micro-ros-agent # Option B: build from source mkdir -p ~/microros_ws/src && cd ~/microros_ws/src git clone https://github.com/micro-ROS/micro-ROS-Agent.git git clone https://github.com/micro-ROS/micro_ros_msgs.git cd ~/microros_ws rosdep install --from-paths src --ignore-src -r -y colcon build source install/setup.bash
Step 2 — ESP32: Arduino micro-ROS Library
The fastest path for ESP32 is the prebuilt micro_ros_arduino library:
// 1. In Arduino IDE: Sketch → Include Library → Add .ZIP Library
// Download: https://github.com/micro-ROS/micro_ros_arduino/releases
// Pick the release matching your ROS 2 version (Humble/Jazzy)
// 2. Board: ESP32 Dev Module, Upload Speed: 921600
// 3. Example: UART publisher (Serial0 at default baud)
#include <micro_ros_arduino.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/int32.h>
rcl_publisher_t publisher;
std_msgs__msg__Int32 msg;
rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
rcl_timer_t timer;
void timer_callback(rcl_timer_t * timer, int64_t last_call_time) {
RCLC_UNUSED(last_call_time);
if (timer != NULL) {
rcl_publish(&publisher, &msg, NULL);
msg.data++;
}
}
void setup() {
// UART transport (Serial at 115200 to the agent)
set_microros_serial_transports(Serial);
delay(2000);
allocator = rcl_get_default_allocator();
rclc_support_init(&support, 0, NULL, &allocator);
rclc_node_init_default(&node, "micro_ros_esp32_node", "", &support);
rclc_publisher_init_default(&publisher, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
"micro_ros_int32_publisher");
// Timer: publish every 1000 ms
rclc_timer_init_default(&timer, &support, RCL_MS_TO_NS(1000), timer_callback);
rclc_executor_init(&executor, &support.context, 1, &allocator);
rclc_executor_add_timer(&executor, &timer);
msg.data = 0;
}
void loop() {
delay(100);
rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
}Step 3 — Run the Agent over UART
# Check which port the ESP32 appears on ls /dev/ttyUSB* /dev/ttyACM* # Grant permission (add yourself to dialout group — log out and back in) sudo usermod -a -G dialout $USER # Start the agent (Jazzy) source /opt/ros/jazzy/setup.bash ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 --baud 115200 # You should see: # [1687654321.123] info | AgentRunner.cpp | run_sync | running in sync mode # [1687654321.456] info | Root.cpp | set_verbose_level | logger: ... # In another terminal — verify the topic appears ros2 topic list # → /micro_ros_int32_publisher ros2 topic echo /micro_ros_int32_publisher
UDP Transport (ESP32 WiFi)
For wireless ESP32 robots, switch to WiFi UDP transport — no USB cable needed:
// Replace set_microros_serial_transports with:
#include <WiFi.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const char* agent_ip = "192.168.1.100"; // your Linux host
const uint16_t agent_port = 8888;
void setup() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
// UDP transport to agent
set_microros_wifi_transports(
(char*)ssid, (char*)password, (char*)agent_ip, agent_port);
delay(2000);
// ... rest of setup() same as UART example above
}# Run agent listening on UDP port 8888 ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888
Subscriber on the Microcontroller
Receive commands from ROS 2 on the ESP32 (e.g., LED on/off from /cmd):
#include <micro_ros_arduino.h>
#include <std_msgs/msg/bool.h>
rcl_subscription_t subscriber;
std_msgs__msg__Bool sub_msg;
void subscription_callback(const void * msgin) {
const std_msgs__msg__Bool * msg = (const std_msgs__msg__Bool *)msgin;
digitalWrite(LED_BUILTIN, msg->data ? HIGH : LOW);
}
// In setup(), after rclc_node_init_default():
rclc_subscription_init_default(&subscriber, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Bool),
"led_cmd");
// Add to executor (2 handles: 1 timer + 1 subscription)
rclc_executor_init(&executor, &support.context, 2, &allocator);
rclc_executor_add_timer(&executor, &timer);
rclc_executor_add_subscription(&executor, &subscriber,
&sub_msg, &subscription_callback, ON_NEW_DATA);
// Publish from ROS 2 side to control the LED:
// ros2 topic pub /led_cmd std_msgs/msg/Bool "data: true"STM32 / FreeRTOS: CMake Build
For production STM32 firmware, use the micro_ros_stm32cubemx_utils CMake integration instead of Arduino:
# 1. Clone the micro-ROS component for STM32CubeIDE/CMake
git clone https://github.com/micro-ROS/micro_ros_stm32cubemx_utils.git
# 2. In your STM32 CMakeLists.txt:
include(micro_ros_stm32cubemx_utils/microros_static_library/libmicroros.cmake)
target_link_libraries(${CMAKE_PROJECT_NAME}.elf microros)
# 3. FreeRTOS task pattern
void micro_ros_task(void * argument) {
rcl_allocator_t allocator = rcl_get_default_allocator();
rclc_support_t support;
rclc_support_init(&support, 0, NULL, &allocator);
rcl_node_t node;
rclc_node_init_default(&node, "stm32_node", "", &support);
// ... create publishers/subscribers as above
rclc_executor_t executor;
rclc_executor_init(&executor, &support.context, 1, &allocator);
rclc_executor_spin(&executor); // blocks forever in task
for(;;) { osDelay(1); }
}Custom Message Types
Generate C headers from a custom .msg file for use in micro-ROS firmware:
# 1. Create your message package (standard ROS 2 package) ros2 pkg create --build-type ament_cmake my_micro_msgs # Add MyData.msg with: float32 voltage\nfloat32 current\nint32 rpm # 2. Generate micro-ROS C headers cd ~/microros_ws git clone https://github.com/micro-ROS/micro_ros_setup.git src/micro_ros_setup rosdep install --from-paths src --ignore-src -r -y ros2 run micro_ros_setup create_firmware_ws.sh generate_libmicroros # Then add --packages-select my_micro_msgs to the colcon build # 3. In firmware code: #include <my_micro_msgs/msg/my_data.h> my_micro_msgs__msg__MyData sensor_msg; sensor_msg.voltage = 3.3f; sensor_msg.rpm = 1200;
Diagnostics & Common Issues
Agent connects but no topics appear in ros2 topic list
Check baud rate matches on both sides (115200 default). Run agent with -v6 for verbose: ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -v6. Look for 'session established' vs 'timeout'.
UDP transport: ESP32 connects but disconnects every few seconds
The agent keepalive timeout is 1000ms by default. Your loop() must call rclc_executor_spin_some every <200ms. Use a hardware timer callback instead of delay() for publishing.
Out of memory on ESP32 (rcl_init returns RCL_RET_BAD_ALLOC)
Reduce the number of entities. Each publisher/subscriber costs ~2-4KB static memory. Use MICRO_ROS_TRANSPORT_ARDUINO_SERIAL to reduce overhead. Set configMINIMAL_STACK_SIZE to 4096 for the micro-ROS task.
Next Steps
- → ros2_control hardware interface guide — bridge micro-ROS sensor data into the ros2_control stack
- → ROS 2 diagnostics guide — publish diagnostic_msgs from MCU to monitor battery, temperature
- → ROS 2 Zenoh guide — replace the DDS transport on the Linux host for better WiFi performance