跳转到主要内容

打通MCU与ROS2世界:Micro-ROS在MCXN947上的落地实践

概述

随着机器人技术与嵌入式系统的不断融合,如何让资源受限的微控制器也能接入强大的ROS2生态,成为开发者关注的重点。Micro-ROS正是在这样的背景下诞生,它将ROS2的通信能力和架构优势“下沉”到MCU级设备,让嵌入式系统也具备分布式、可扩展的能力。

NXP推出的MCXN947作为一款性能强劲、资源丰富的Cortex-M33微控制器,加上官方FRDM-MCXN947开发板,为开发者提供了理想的实验平台。那么问题来了:如何让这颗MCU成功运行Micro-ROS,并与ROS2系统打通?

本文将从原理到实践,手把手带你完成Micro-ROS在MCXN947上的移植,包括环境搭建、静态库生成、通信接口适配,以及最终运行验证。即便你是第一次接触Micro-ROS,也可以跟随本文顺利跑通整套流程。

在本文中,我们将探讨如何在MCXN947板移植Micro-ROS。

硬件环境:

  • 开发板:FRDM-MCXN947

软件环境:

  • IDE:MCUXpresso IDE v25.6
  • SDK:SDK Builder | MCUXpresso SDK Builder (nxp.com)

ROS架构

在深入探讨MicroROS的移植过程之前,让我们先对全新的ROS2架构进行简要介绍。这是因为MicroROS在其设计和实现中大量借鉴了ROS2的抽象层源码。ROS2与它的前身ROS1在核心通信机制上有着显著的不同。最突出的变化是ROS2采用了DDS(数据分发服务)作为其通信和节点发现的协议,而ROS1则依赖于XML-RPC协议,并且需要在主节点启动后其他节点才能加入。

ROS1对于嵌入式设备的支持受限主要是因为XML-RPC机制在这些环境下需要引入大量的软件包依赖。为了解决这个问题,像rosserial这样的项目为MCU和PC上的主节点之间的通信定制了更轻量级的通信协议。相比之下,ROS2引入了工业界成熟的DDS协议,该协议已经在军工、航天和金融系统领域证明了自己的稳定性。

值得一提的是,DDS只是一个标准协议,不同的公司可能会有不同的实现,例如Cyclone DDS(由Eclipse提供)、Fast DDS和Micro XRCE-DDS(两者都是由eProsima提供)。为了确保在使用不同厂商的DDS实现时上层的ROS2代码不需要变动,ROS2定义了一系列的抽象层接口,如RCL(ROS客户端库)和RMW(ROS中间件)。

这些底层的DDS实现都提供了一致的RMW接口。在RMW之上,则是C语言实现的RCL,这允许它进一步向上提供对不同编程语言的支持,包括C++、Python和Java等。因此,要想成功移植ROS2到一个实时操作系统(RTOS),关键在于该RTOS需要支持一个具体的DDS实现及其对应的RMW接口。只有这样,上层的RCL和ROS应用程序代码才能够无需任何修改地运行。

现在我们了解到,ROS2主要由RCL(ROS客户端库)、RMW(ROS中间件接口)以及DDS(数据分发服务)组成。要将ROS2移植到新的平台或系统上,核心工作便在于实现一套兼容的RMW和DDS组合,而上层的RCL则可以保持原样,无需修改。在这个框架下,MicroROS正是提供了这样的RMW和DDS组合。

具体来说,MicroROS采用的DDS是专门为嵌入式设备设计的Micro XRCE DDS(针对极端资源受限环境的DDS标准),并且配有与之适配的RMW实现。这使得MicroROS不仅兼容ROS2的架构,还能有效地运行在资源有限的嵌入式设备上。

因此,移植MicroROS到不同的硬件平台,本质上就是需要将其通信协议如UART(通用异步收发传输器)和UDP(用户数据报协议)与MicroROS的RMW+DDS组合进行对接。这一过程涉及确保MicroROS能够通过这些通信协议与平台上的其他节点或外部系统有效交互,同时保持其轻量级和高效性,以适应嵌入式设备的特殊需求。

通过上述介绍,我们可以看出MicroROS的移植主要有2个部分:

对接UART/UDP通信和时钟,也就是实现default_transport.cpp里面的5个函数。打包DDS+RMW+RCL生成libmicroros.a静态库。

Micro-ROS静态库工具链和环境配置

3.1 工具链配置

首先在Ubuntu 22.04 LTS上安装ROS 2。并运行如下命令。

sudo apt update&&sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
sudo apt update && sudo apt install curl gnupg lsb-releasesudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg

(1)sudo apt update: 使用管理员权限(sudo)更新系统软件包列表,确保获取最新的软件包信息。

(2)sudo apt install locales: 使用管理员权限安装locales软件包,它提供了管理和配置本地化的工具。

(3)sudo locale-gen en_US en_US.UTF-8: 使用管理员权限生成英文(美国)的本地化环境,包括en_US和en_US.UTF-8。

(4)sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8: 使用管理员权限更新系统的本地化设置,将LC_ALL和LANG环境变量设置为en_US.UTF-8,即英语(美国)的UTF-8编码。

(5)export LANG=en_US.UTF-8: 将当前用户的LANG环境变量设置为en_US.UTF-8,以便在终端中使用英语(美国)的UTF-8编码显示文本。

(6)sudo apt update && sudo apt install curl gnupg lsb-release: 使用管理员权限更新系统软件包列表,并安装curl、gnupg和lsb-release软件包。这些软件包分别用于在终端中执行网络请求、处理GPG加密和获取发行版信息。

(7)sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg: 使用管理员权限从GitHub仓库下载ROS的公钥文件(ros.key),并将其保存到系统的密钥环目录(/usr/share/keyrings/)下,命名为ros-archive-keyring.gpg。这个公钥用于验证ROS软件包的完整性和来源。

3.2 Ubuntu安装ROS

配置和构建Micro-ROS的开发环境的。运行如下命令:

sudo apt update && sudo apt upgrade && sudo apt install ros-humble-desktop
source /opt/ros/humble/setup.bash && echo “source /opt/ros/humble/setup.bash” >>~/.bashrc
source /opt/ros/$ROS_DISTRO/setup.bash
mkdir uros_ws && cd uros_ws
git clone -b iron https://github.com/micro-ROS/micro_ros_setup.git src/micro_ros_setup
rosdep update && rosdep install --from-paths src --ignore-src -y
colcon build
source install/local_setup.bash 
ros2 run micro_ros_setup create_firmware_ws.sh generate_lib

(1)更新软件和安装ROS(Robot Operating System);

(2)设置和保存ROS环境的配置;

(3) 这两个命令创建一个新的目录uros_ws(工作空间),然后切换到该目录;

(4) 从GitHub仓库克隆名为micro_ros_setup的项目,并将其放在工作空间的src目录下;

(5) 这两个命令用于安装项目所需的依赖项;

(6) 这个命令使用colcon构建系统来编译和安装项目。Colcon是一个通用的构建工具,适用于多种编程语言和平台;

(7) create_firmware_ws.sh:这是要运行的可执行文件的名称。它可能是一个用于创建固件工作空间(firmware workspace)的脚本。这条命令的目的是运行micro_ros_setup包中的create_firmware_ws.sh脚本,并传递参数generate_lib来生成一个包含Micro-ROS库的固件工作空间。这一步需要从github下载大量的源码。失败后下次生成会提示firmware已经存在,要rm这个文件夹。如果出现以下图片,证明代码下载成功。

Micro-ROS静态库生成

如果工具链和环境配置成功,回到工作空间下,现在我们应该有五个文件夹,分别为build、 fireware、 install、 log、 src。其中fireware是我们生成micro-ros的空间: cd fireware.

4.1 根据目标处理器配置toolchain.cmake文件

进入fireware目录中,这里面的colcon.meta和toolchain.cmake就是我们生成静态库需要指定的配置文件了,其中colcon.meta描述了我们micro-ros的配置,toolchain.cmake描述了单片机的平台。 

我们可以打开MCUXpresso查找MCXN947相关配置。并创建toolchain.cmake。

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_CROSSCOMPILING 1)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# 
SET HERE THE PATH TO YOUR C99 AND C++ COMPILERS
# 在这里添加编译器路径
set(PIX /opt/gcc-arm-none-eabi-10.3-2021.10/bin)
set(CMAKE_C_COMPILER ${PIX}/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER ${PIX}/arm-none-eabi-g++)
set(CMAKE_C_COMPILER_WORKS 1 CACHE INTERNAL "")
set(CMAKE_CXX_COMPILER_WORKS 1 CACHE INTERNAL "")
# SET HERE YOUR BUILDING FLAGS
set(FLAGS "-O2 -ffunction-sections -fdata-sections -fno-exceptions -mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard -nostdlib -mthumb --param max-inline-insns-single=500 -D'RCUTILS_LOG_MIN_SEVERITY=RCUTILS_LOG_MIN_SEVERITY_NONE'" CACHE STRING "" FORCE)
#-mcpu=cortex-m3 
改成 
-mcpu=cortex-m33
# 
加入 
mfpu=fpv5-d16 -mfloat-abi=hard 
支持硬件浮点编译
set(CMAKE_C_FLAGS_INIT "-std=c11 ${FLAGS} -DCLOCK_MONOTONIC=0 -D'__attribute__(x)='" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_INIT "-std=c++11 ${FLAGS} -fno-rtti -DCLOCK_MONOTONIC=0 -D'__attribute__(x)='" CACHE STRING "" FORCE)
set(__BIG_ENDIAN__ 0)

上面就是cmake的写法。

其中,"-mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard":这些选项指定了目标处理器的架构和浮点单元特性。

我们可以查看MCXN947的目标处理器的架构和浮点单元特性。

我们可以打开MCUXpresso,打开一个MCXN947工程,在settings->MCU Linker 中的All options中查看mcpu=cortex-m33 -mfpu=fpv5-sp-d16 mfloat-abi=hard。

所以MCXN947系列的是M33内核,我们需要将-mcpu=cortex-m7修改成 -mcpu=cortex-m33,其他基本不需要改动。从这块我们也可以看出来,生产的静态库一个系列应该都是通用的!

4.2 根据Micro-ROS需求配置colcon.meta文件

touch colcon.meta
{
"names": {
"tracetools": {
"cmake-args": [
"-DTRACETOOLS_DISABLED=ON",
"-DTRACETOOLS_STATUS_CHECKING_TOOL=OFF" 
] 
},
"rosidl_typesupport": {
"cmake-args": [
"-DROSIDL_TYPESUPPORT_SINGLE_TYPESUPPORT=ON" 
] 
},
"rcl": {
"cmake-args": [
"-DBUILD_TESTING=OFF",
"-DRCL_COMMAND_LINE_ENABLED=OFF",
"-DRCL_LOGGING_ENABLED=OFF" 
] 
},
"rcutils": {
"cmake-args": [
"-DENABLE_TESTING=OFF",
"-DRCUTILS_NO_FILESYSTEM=ON",
"-DRCUTILS_NO_THREAD_SUPPORT=ON",
"-DRCUTILS_NO_64_ATOMIC=ON",
"-DRCUTILS_AVOID_DYNAMIC_ALLOCATION=ON" 
] 
},
"microxrcedds_client": {
"cmake-args": [
"-DUCLIENT_PIC=OFF",
"-DUCLIENT_PROFILE_UDP=OFF",
"-DUCLIENT_PROFILE_TCP=OFF",
"-DUCLIENT_PROFILE_DISCOVERY=OFF",
"-DUCLIENT_PROFILE_SERIAL=OFF",
"-UCLIENT_PROFILE_STREAM_FRAMING=ON",
"-DUCLIENT_PROFILE_CUSTOM_TRANSPORT=ON",
"-DUCLIENT_PROFILE_SHARED_MEMORY=ON",   #允许内存共享
"-DUCLIENT_SHARED_MEMORY_MAX_ENTITIES=20" 
] 
},
"rmw_microxrcedds": {
"cmake-args": [
"-DRMW_UXRCE_MAX_NODES=5", #最大节点数
"-DRMW_UXRCE_MAX_PUBLISHERS=6", #最大发布者数量
"-DRMW_UXRCE_MAX_SUBSCRIPTIONS=4",   #最大订阅者数量
"-DRMW_UXRCE_MAX_SERVICES=6",             #最大服务端数量
"-DRMW_UXRCE_MAX_CLIENTS=1",              #最大客户端数量
"-DRMW_UXRCE_MAX_HISTORY=4",             
"-DRMW_UXRCE_TRANSPORT=custom"                    #自定义传输接口 
] 
} 
}
}

4.3 生成Micro-ROS静态库

运行如下两条命令生产静态库。

source install/local_setup.bash

ros2 run micro_ros_setup build_firmware.sh $(pwd)/firmware/toolchain.cmake $(pwd)/firmware/colcon.meta

如果成功生成会静态库,你会在firmware/build文件中看到libmicroros.a

整合本地工程并测试

5.1 将生成的静态库加入本地工程

Folder name起名为bsp_include。

将include所有文件夹加入工程中。

将include文件中所有路径加入include path中。

将生成好的libmicroros.a静态库加入工程中,例如加入bsp_include中。

将libmicrorots.a静态库和名字路径加入Libraries(-l)和Library search path(-L).

加入静态库和相关头文件后,可以编译。

5.2 实现串口通信接口函数

为了将Micro-ROS移植到MCU,我们必须提供传输层的功能,通过通信接口进行读写的功能。可以在Micro-ROS GitHub中找到的其他示例实现,来自一些预制插件和端口。在这里,我将展示每个所需传输函数的示例代码。

rmw_ret_t
rmw_uros_set_custom_transport
( 
bool framing, 
void 
* args, open_custom_func 
open_cb, 
close_custom_func 
close_cb, 
write_custom_func 
write_cb, 
read_custom_func 
read_cb);

通过该结构体,可以看出通信接口函数组主要包含open,close,write,read。

open:此函数负责初始化(打开)传输层使用的外围设备。如果外围设备在其他地方初始化,并且在调用Micro-ROS函数之前,此函数为空。

close:此功能负责取消初始化(关闭)传输层使用的外围设备。因为不需要取消初始化函数。此函数也为空。

write:此函数负责在外围设备上写入数据(字节)。要写入的字节数和字节本身分别作为参数“len”和“buf”给出。

read:此功能负责从外围设备读取数据(字节)。要读取的字节数在函数参数“len”中指定,字节应通过函数参数“buf”返回。

写入数据和读取数据通过:

bool 
transport_close
(
struct
uxrCustomTransport * transport) { 
return 
true;
}
bool 
transport_open
(
struct
uxrCustomTransport * transport) { 
return 
true;
}
size_t 
transport_write
(
struct
uxrCustomTransport* transport, 
const 
uint8_t * buf, size_t len, uint8_t * err) { 
LPUART_WriteBlocking(DEMO_LPUART, buf, len); 
return len;}
size_t 
transport_read
(
struct
uxrCustomTransport* transport, uint8_t* buf, size_t len, 
int 
timeout, uint8_t* err) { 
LPUART_ReadBlocking(DEMO_LPUART, buf, len); 
return len;}

此外,还需要一个返回系统时基的函数。这个时基不一定是实时的,也就是正确的世界时间,它可以是自启动以来的时间或类似的时间。

此函数不必传递给Micro-ROS,只需使用如上所示的正确名称和参数创建即可。

int clock_gettime(clock_t unused, struct timespec *tp) { 
(void)unused; 
IRTC_GetDatetime(RTC, &datetimeGet); 
tp->tv_sec = datetimeGet.second; 
tp->tv_nsec = (long)(datetimeGet.second) * 1000000; 
return 0;
}

5.3 测试

使Micro-ROS实际运行。这由两部分组成:在MCU上运行的客户端和在主机PC上运行的代理。

基本微ROS客户端(MCU)

从客户端开始,创建和运行Micro-ROS客户端所需的最小步骤(和代码)如下。这些说明基于创建节点的Micro-ROS文档。

下面是实现前面所有步骤的代码,创建和启动Micro-ROS客户端:

//Required global variables
rcl_allocator_t allocator;
rclc_support_t support;
rcl_node_t node;
rclc_executor_t executor;
rmw_ret_t error;
//Set Communication functions
rmw_uros_set_custom_transport( 
true, 
NULL, 
rtt_transport_open, 
rtt_transport_close, 
rtt_transport_write, 
rtt_transport_read
);
//Set Allocation functions (Optional)
rcl_allocator_t allocator = rcutils_get_zero_initialized_allocator();
allocator.allocate = rtt_allocate;
allocator.deallocate = rtt_deallocate;
allocator.reallocate = rtt_reallocate;
allocator.zero_allocate = rtt_zero_allocate;
(void)!rcutils_set_default_allocator(&allocator);
//Get allocator
allocator = rcl_get_default_allocator();
//Create init_options
error = rclc_support_init(&support, 0, NULL, &allocator);
//Create node
error = rclc_node_init_default(&node, "uROS_Terminal", "", &support);
//Create executor
error = rclc_executor_init(&executor, &support.context, 1, &allocator);
//Call the executor periodically e.g. in the while(1) loop or a thread:
while(1) { 
error = rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
}

基本Micro-ROS设置的另一部分是代理,它在主机PC上运行。代理在MCU上运行的Micro-ROS和主机PC上的ROS 2之间建立接口,它是Micro-ROS(和DDS)代理。代理通过定义的自定义接口连接到MCU上运行的Micro-ROS。对于这个代理,不需要编写代码,只需要构建代理,就像任何其他ROS 2包一样,并且以与构建Micro-ROS静态库类似的方式构建。此处显示的说明基于Micro-ROS文档,用于创建代理。

# Go to the Micro-ROS workspace folder
cd microros_ws
# Source ROS 2
source /opt/ros/humble/setup.bash
# Source local packages
source install/local_setup.bash
# Create Agent
ros2 run micro_ros_setup create_agent_ws.sh
# Build Agent
ros2 run micro_ros_setup build_agent.sh
# Possible ROS Update
sudo rosdep init
rosdep update
成功构建Micro-ROS代理后,以下bash命令启动/运行代理:
# Go to the Micro-ROS workspace folder
cd microros_ws# Source ROS 2
source /opt/ros/humble/setup.bash
# Source local packages
source install/local_setup.bash
# Run Agent (serial connection)
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyACM0

在这些bash命令中,最后一个是实际启动Micro-ROS代理的命令。它需要几个参数,第一个是要使用的连接类型,这里是串行连接。然后是两个仅适用于串行连接的参数:“–dev/ttyACM0”,用于设置要使用的串行接口(要列出linux中的所有串行接口,可以使用以下bash命令:“dmesg | grep tty”)。

使用Micro-ROS Agent进行测试和运行

启动Micro-ROS代理后,MCU可以通过串行接口连接到主机PC,并重置以建立新的连接。如果以下Micro-ROS代理终端输出:

至此,我们已经完整走通了Micro-ROS 在 MCXN947平台上的移植流程。从ROS2架构理解,到静态库构建,再到通信接口适配与运行验证,每一步都是嵌入式系统接入ROS2生态的关键。

可以看到,Micro-ROS并不是一个“黑盒”,而是一套清晰的架构组合——RCL+RMW+Micro XRCE-DDS+自定义传输层。一旦掌握了这套思路,将其移植到其他MCU平台也将变得得心应手。

未来,你可以在此基础上扩展更多功能,比如:

  • 发布/订阅传感器数据
  • 构建嵌入式ROS2节点网络
  • 实现与PC、机器人系统的实时协同

Micro-ROS的价值,不只是“让MCU能跑ROS2”,更重要的是:

它为嵌入式设备打开了通往机器人生态的大门;

如果你正在从事机器人开发、智能设备、边缘计算,那么这项技术非常值得掌握。

如果你后续想继续深入(比如 FreeRTOS 适配、UDP 通信、性能优化),也欢迎继续交流探讨。

作者:Hang Zhang

来源:恩智浦MCU加油站

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。