micro-ROS 與即時系統

2026-07-18
  • ros2
  • micro-ros
  • embedded
  • real-time
  • microcontroller

問題定義

前面所有章節寫的節點,都假設執行環境是一台有完整作業系統(通常是 Linux)、記憶體以 GB 為單位計算的電腦。但機器人系統裡,很多最底層的控制邏輯(馬達驅動、編碼器讀取、電池電量監控)實際跑在資源極度受限的微控制器上——記憶體可能只有幾百 KB,沒有完整的作業系統。這些微控制器要怎麼參與 ROS2 系統的通訊,是 micro-ROS 要解決的問題。

核心概念說明

為什麼需要一個「Agent」中介角色

完整的 DDS 通訊協定,對微控制器的資源需求過重,micro-ROS 的解法是把通訊職責拆成兩邊:微控制器上執行輕量化的 micro-ROS 用戶端,只需要處理相對簡單的序列化與傳輸邏輯;另一台有完整運算資源的機器(可能是機器人上的主控電腦、或第 10.1 節介紹的容器化部署環境)執行 micro-ROS Agent,負責把微控制器傳來的輕量訊息,轉譯成正常的 DDS 通訊,讓 ROS2 系統裡其他一般節點完全不需要知道對方其實是一台資源受限的微控制器——對它們而言,micro-ROS 節點在通訊層面表現得跟一般節點沒有差異。

text
微控制器(micro-ROS 用戶端)
      │  輕量傳輸層(序列埠 / UDP)
      ▼
micro-ROS Agent(跑在有完整資源的機器上)
      │  正常 DDS 通訊
      ▼
其他一般 ROS2 節點

靜態記憶體配置:即時系統設計的核心考量

一般 ROS2 節點開發時很少需要在意記憶體配置的細節,但 micro-ROS 開發時這是核心考量之一——微控制器上通常不使用動態記憶體配置(也就是不用 C 語言的 malloc/C++ 的 new 這類在執行期間才決定要配置多少記憶體的機制),原因是動態配置在資源極度受限、且需要嚴格即時性保證的系統裡,可能因為記憶體碎片化或配置失敗導致不可預期的行為,這在需要穩定、可預測回應時間的即時控制迴路(例如馬達的閉迴路控制,必須嚴格在固定週期內完成計算)裡是不可接受的風險。micro-ROS 因此設計成在編譯期就明確配置好所有需要的記憶體(例如訊息緩衝區的大小),執行期間不再動態增減,用犧牲一些彈性換取行為的可預測性。

實作範例:micro-ROS 韌體的基本結構

以下用簡化過的 C 語言範例,示範一個發布感測器讀數的 micro-ROS 節點結構,聚焦在跟一般 ROS2 節點概念上對應、但實作細節不同的部分(完整可編譯的韌體專案依實際使用的微控制器開發板而有所不同,這裡展示核心邏輯)。

c
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/float32.h>

rcl_publisher_t publisher;
std_msgs__msg__Float32 msg;

void timer_callback(rcl_timer_t *timer, int64_t last_call_time) {
  msg.data = read_sensor_value();
  rcl_publish(&publisher, &msg, NULL);
}

void setup_micro_ros_node() {
  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, "sensor_node", "", &support);

  rclc_publisher_init_default(
    &publisher, &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
    "sensor_reading"
  );

  rcl_timer_t timer;
  const unsigned int timer_period_ms = 100;
  rclc_timer_init_default(
    &timer, &support, RCL_MS_TO_NS(timer_period_ms), timer_callback
  );

  rclc_executor_t executor;
  rclc_executor_init(&executor, &support.context, 1, &allocator);
  rclc_executor_add_timer(&executor, &timer);

  while (1) {
    rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
  }
}

這段程式碼在結構上跟第 2.5 節寫的 TunableNode(用 timer 定期發布資料)概念上是對應的:rclc_timer_init_default 對應 create_timerrclc_publisher_init_default 對應 create_publisherrclc_executor_spin_some 對應 rclpy.spin。差異在於這裡所有物件(publishertimerexecutor)都是在編譯期就宣告好固定大小的結構,沒有任何執行期動態配置的記憶體。

啟動 Agent 端

在有完整運算資源的機器上(呼應第 10.1 節,這通常也會是一個容器化部署的環境):

bash
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0

預期輸出

text
[INFO] [micro_ros_agent]: Serial port not found. Please connect your serial port device

(未連接裝置時的預期錯誤,用來確認 Agent 本身有正確啟動並嘗試監聽)連接微控制器後:

text
[INFO] [micro_ros_agent]: session established
[INFO] [micro_ros_agent]: participant created
[INFO] [micro_ros_agent]: publisher created

在主機端確認能收到微控制器發布的資料,用的是完全一般的 ROS2 CLI 指令,沒有任何特殊之處:

bash
ros2 topic echo /sensor_reading
text
data: 23.5
---
data: 23.7
---

常見錯誤與除錯技巧

錯誤一:Agent 顯示連線成功,但主機端 ros2 topic list 完全看不到微控制器發布的主題

原因:常見是微控制器端程式碼裡 rclc_executor_spin_some 沒有被持續呼叫(例如卡在某個阻塞操作,或韌體邏輯裡有其他任務佔用了執行時間),導致微控制器雖然跟 Agent 建立了連線(session established),但實際的訊息發布邏輯從來沒有真正被執行到。

排除方式:先確認韌體本身的主迴圈確實持續在跑(可以用微控制器開發板上常見的除錯 LED,或序列埠印出的除錯訊息確認主迴圈仍然存活),排除硬體本身卡死的可能性後,再檢查 rclc_executor 的設定是否正確涵蓋了所有需要處理的 timer 與 subscription。

錯誤二:韌體在資源受限的微控制器上編譯時,回報記憶體不足

text
region 'RAM' overflowed by 1024 bytes

原因:micro-ROS 用戶端函式庫本身、加上訊息型別產生的靜態緩衝區,都會佔用編譯期就固定下來的記憶體空間,如果微控制器本身的 RAM 容量較小,又同時訂閱/發布了多種訊息型別、或使用了較大的訊息(例如包含陣列的自訂訊息),很容易超出可用記憶體。

排除方式:檢視實際需要的訊息型別數量與大小,只保留真正必要的 publisher/subscriber/timer,避免為了「以防萬一」而預先配置用不到的通訊物件;必要時考慮拆分成多個各自負責較少通訊職責的微控制器節點,而不是把過多功能塞進單一微控制器的有限資源裡。

小結

micro-ROS 透過用戶端與 Agent 分離的架構,讓資源受限的微控制器能參與正常的 ROS2 通訊,對其他節點而言完全透明;編譯期靜態記憶體配置是即時系統設計換取行為可預測性的核心取捨。下一節要處理另一個系統設計裡容易被忽略、但正式部署前必須認真面對的面向:安全性——當系統連上網路、甚至對外部開放存取時,怎麼確保通訊不被竊聽或竄改。

延伸閱讀

常見問題

faq_01.log
micro-ROS 節點寫的程式碼,跟一般 rclpy/rclcpp 節點很不一樣嗎?
API 設計上刻意做得很相似——micro-ROS 提供的是 rclc(一個 C 語言的用戶端函式庫),概念上的 publisher、subscriber、timer 都有對應的呼叫方式,一個熟悉 rclpy 或 rclcpp 概念的開發者,讀 micro-ROS 的 C 程式碼不會覺得完全陌生。真正的差異在於底層資源管理是手動且明確的(沒有動態記憶體配置這種在微控制器上有風險的機制),以及需要額外設定跟 micro-ROS Agent 之間的傳輸層(序列埠、UDP 等)。
faq_02.log
為什麼不能直接把完整的 ROS2(rclcpp/rclpy)跑在微控制器上?
完整的 ROS2 底層依賴 DDS,DDS 的實作通常需要一般作業系統提供的完整網路堆疊、動態記憶體管理、多執行緒排程等能力,這些對執行 Linux 或 Windows 的一般電腦不是問題,但微控制器(例如常見的 STM32、ESP32)通常沒有完整作業系統、記憶體以 KB 為單位計算、也沒有原生支援這麼重量級的網路堆疊,完全負擔不起完整 DDS 的資源需求。micro-ROS 用更輕量的傳輸層取代直接的 DDS 通訊,並透過 Agent 這個中介角色,讓微控制器不需要自己扛起完整 DDS 的重量。
faq_03.log
micro-ROS Agent 掛掉了,微控制器上的程式會跟著當機嗎?
不會,這是分離架構的好處之一——微控制器上執行的韌體邏輯,跟 Agent 是兩個獨立運作的東西,Agent 只是負責轉譯通訊協定,Agent 斷線或重啟,微控制器上的韌體本身(例如底層的馬達控制迴圈)理論上應該被設計成能繼續獨立運作或安全地進入待機狀態,不應該因為失去跟上層 ROS2 系統的連線就直接故障,這是即時系統設計時需要特別考慮的容錯需求。