Skip to main content
🔌 micro-ROS

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

BoardRAMTransportRecommended Setup
ESP32520 KBUART / UDP (WiFi)Arduino + micro_ros_arduino library
STM32128 KB+UART / USB-CDCmicro_ros_stm32cubemx_utils or CMake + FreeRTOS
Raspberry Pi Pico264 KBUART / USB-CDCmicro_ros_raspberrypi_pico_sdk
Arduino Nano RP2040264 KBUDP (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