1. Overview
The MCXN947 chip is a highly integrated microcontroller featuring robust processing capabilities, extensive peripheral support, and advanced security features. It is suitable for a wide range of complex applications.
The NXP FRDM-MCXN947 board is a low-cost design and evaluation board based on the MCXN947 device. NXP provides a comprehensive set of tools and software support for the MCXN947, including hardware evaluation boards, an integrated development environment (IDE), example applications, and drivers.
Micro-ROS is an embedded version of ROS 2 specifically designed to operate on embedded systems, enabling real-time control and communication for robotics and embedded devices. It extends the powerful features of ROS 2 to resource-constrained embedded platforms like microcontrollers and embedded systems. The introduction of Micro-ROS facilitates tighter integration between embedded systems and the ROS 2 ecosystem, enabling advanced robotic automation and control.
This document explores the process of porting Micro-ROS to the MCXN947 board.
This document explores the process of porting Micro-ROS to the MCXN947 board.
Hardware Environment:
FRDM-MCXN947
Software Environment:
Ubuntu 22.04
IDE:MCUXpresso IDE v11.9.0
SDK:SDK Builder | MCUXpresso SDK Builder (nxp.com)
2. ROS 2 Architecture
Before delving into the Micro-ROS porting process, let us briefly introduce the new ROS 2 architecture. This is necessary because Micro-ROS heavily leverages ROS 2's abstraction layer source code in its design and implementation.
Compared to its predecessor ROS 1, ROS 2 employs a distinct communication mechanism, most notably adopting the DDS (Data Distribution Service) protocol for communication and node discovery. ROS 1, by contrast, relies on the XML-RPC protocol and requires the master node to launch before other nodes can join.
ROS 1's limited support for embedded devices stems from the XML-RPC mechanism, which introduces significant software dependencies in these environments. To address this issue, projects like rosserial developed lightweight communication protocols tailored for MCU-to-PC master node communication. ROS 2, on the other hand, integrates the industry-proven DDS protocol, which has demonstrated its stability in domains like military, aerospace, and financial systems.
It is important to note that DDS is a standard protocol with various implementations, such as Cyclone DDS (Eclipse), Fast DDS, and Micro XRCE-DDS (eProsima). To ensure that upper-layer ROS 2 code remains unaffected by different DDS implementations, ROS 2 defines a series of abstract layer interfaces, such as RCL (ROS Client Library) and RMW (ROS Middleware).
These DDS implementations provide a uniform RMW interface. On top of RMW is the RCL, implemented in C, which supports various programming languages, including C++, Python, and Java. Thus, successfully porting ROS 2 to a real-time operating system (RTOS) hinges on the RTOS supporting a specific DDS implementation and its corresponding RMW interface. Only then can upper-layer RCL and ROS application code run without modification.
ROS 2 comprises RCL (ROS Client Library), RMW (ROS Middleware Interface), and DDS (Data Distribution Service). Porting ROS 2 to a new platform essentially involves creating a compatible RMW-DDS combination, leaving the RCL layer unaltered. Micro-ROS provides such an RMW-DDS combination.
Specifically, Micro-ROS employs Micro XRCE-DDS (a DDS standard designed for extremely resource-constrained environments) with a corresponding RMW implementation. This enables Micro-ROS to be compatible with ROS 2's architecture while efficiently running on resource-limited embedded devices.
Thus, porting Micro-ROS to different hardware platforms fundamentally involves interfacing communication protocols like UART (Universal Asynchronous Receiver-Transmitter) and UDP (User Datagram Protocol) with the Micro-ROS RMW-DDS combination. This process ensures effective communication between Micro-ROS and other nodes or external systems while maintaining the lightweight and efficient characteristics suitable for embedded devices.
From the above introduction, we can see that MicroROS transplantation mainly has two parts:
Connect UART/UDP communication and clock to implement the 5 functions in default_transport.cpp. Folder DDS+RMW+RCL generates libmicroros.a static library.
3. Micro-ROS Static Library Toolchain and Environment Configuration
3.1 Micro-ROS Static Library Toolchain and Environment Configuration
First, install ROS 2 on Ubuntu 22.04 LTS and run the following commands:
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-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
(1) sudo apt update: Updates the system’s package list to fetch the latest package information.
(2) sudo apt install locales: Installs the locales package, which provides tools for managing and configuring localization.
(3) sudo locale-gen en_US en_US.UTF-8: Generates the localization settings for US English, including UTF-8 encoding.
(4) sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8: Updates the system’s localization settings to use UTF-8 encoding for US English.
(5) export LANG=en_US.UTF-8: Sets the current terminal’s language encoding to US English UTF-8.
(6) sudo apt install curl gnupg lsb-release: Installs additional tools (curl for network requests, gnupg for encryption, lsb-release for distribution information).
(7) sudo curl: Downloads the ROS public key for verifying package integrity.
3.2 Installing ROS on Ubuntu
Set up and build the Micro-ROS development environment with the following commands:
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) Update the software and install ROS (Robot Operating System)
(2) Set and save the configuration of the ROS environment.
(3) These two commands create a new directory uros_ws (workspace), and then switch to this directory.
(4) Clone the project named micro_ros_setup from the GitHub repository and place it in the src directory of the workspace.
(5) These two commands are used to install the dependencies required by the project.
(6) This command uses the colcon build system to compile and install the project. Colcon is a general-purpose build tool suitable for multiple programming languages and platforms.
(7) create_firmware_ws.sh: This is the name of the executable file to be run. It might be a script that creates a firmware workspace. The purpose of this command is to run the create_firmware_ws.sh script in the micro_ros_setup package and pass the parameter generate_lib to generate a firmware workspace containing the Micro-ROS library. This step requires downloading a large amount of source code from github. After failure, the next generation will prompt that the firmware already exists and you need to rm this folder. If the following picture appears, it proves that the code download is successful.
4. Generating the Micro-ROS Static Library
If the toolchain and environment setup are successful, you should have five folders in your workspace: build, firmware, install, log, and src.
The firmware folder contains the Micro-ROS workspace.
4.1 Configure the toolchain.cmake file according to the target processor
Enter the fireware directory. The colcon.meta and toolchain.cmake inside are the configuration files we need to specify to generate the static library. Colcon.meta describes the configuration of our micro-ros, and toolchain.cmake describes the microcontroller platform.
We can open MCUXpresso to find MCXN947 related configuration. and create 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)
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)
The above is how cmake is written.
Among them, "-mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard": These options specify the architecture and floating point unit characteristics of the target processor.
We can view the architecture and floating point unit characteristics of the MCXN947's target processor.
We can open MCUXpresso IDE, open a MCXN947 project, and check mcpu=cortex-m33 -mfpu=fpv5-sp-d16 mfloat-abi=hard in All options in settings->MCU Linker.
Therefore, the MCXN947 series is an M33 core. We need to change -mcpu=cortex-m7 to -mcpu=cortex-m33, and basically no other changes are needed. From this we can also see that a series of static libraries produced should be universal!
4.2 Configure the colcon.meta file according to Micro-ROS requirements
The other file created sets the Micro-ROS library configurations. To set the transport layer type, here we want a custom transport layer, the configuration “-DUCLIENT_PROFILE_CUSTOM_TRANSPORT=ON” is set to “ON” and the configuration “-DRMW_UXRCE_TRANSPORT=custom” is set to “custom”. The other interesting settings are under the “rmw_microxrcedds” section. Here we can set the maximum number of nodes (1), publisher (5), subscribers (5), services (1) and clients (1) that should be supported, as well as the maximum message history (4) used for a reliable QoC mode. A more detailed description of all settings under this section, can be found in the eProsima Micro XRCE-DDS GitHub. An example configuration, the one I used, is shown bellow:
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 Building the Micro-ROS Static Library
Run the following commands:
source install/local_setup.bash
ros2 run micro_ros_setup build_firmware.sh $(pwd)/firmware/toolchain.cmake $(pwd)/firmware/colcon.meta
If the static library is successfully generated, you will see libmicroros.a in the firmware/build file.
5. Integrating and Testing the Static Library Locally
5.1 Adding the Generated Micro-ros Static Library to the Local Project
(1) Create a folder named bsp_include.
(2) Add all files from the include directory into your project under bsp_include.
(3) Add all paths in the include file to the include path.
(4) Add the generated libmicroros.a static library to the project, for example, add it to bsp_include.
(5) Add the libmicrorots.a static library and name path to Libraries(-l) and Library search path(-L).
5.2 Implementing the Serial Communication Interface Functionsrmw_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);
Through this structure, we can see that the communication interface function group mainly includes open, close, write, and read.
open: This function is responsible for initializing (opening) peripheral devices used by the transport layer. This function is empty if the peripheral is initialized elsewhere and before the Micro-ROS function is called.
close: This function is responsible for deinitializing (turning off) peripherals used by the transport layer. Because there is no need to deinitialize the function. This function is also empty.
write: This function is responsible for writing data (bytes) on the peripheral device. The number of bytes to be written and the bytes themselves are given as parameters "len" and "buf" respectively.
read: This function is responsible for reading data (bytes) from the peripheral device. The number of bytes to read is specified in the function parameter "len" and bytes should be returned via the function parameter "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;
}
Additionally, you need to implement a function to provide the system’s clock time. This clock does not have to provide real-time or world time, but rather elapsed time since startup or similar. For example:
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 Testing
To run Micro-ROS in practice, the setup includes two components:
1. A client running on the MCU.
2. An agent running on the host PC.
Basic Micro-ROS Client (MCU)
Below are the minimal steps and code needed to create and run a Micro-ROS client. These instructions are based on the Micro-ROS documentation for node creation.
//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));
}
Setting up the Micro-ROS Agent (PC)
The agent establishes an interface between the Micro-ROS client on the MCU and ROS 2 on the host PC. To set up the agent:
# 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
After successfully building the Micro ROS agent, the following bash command starts/runs the agent:
# 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
Of these bash commands, the last one is the one that actually starts the Micro-ROS agent. It takes several parameters, the first is the type of connection to use, in this case a serial connection. Then there are two parameters that only apply to serial connections: "--dev/ttyACM0", which sets the serial interface to be used (to list all serial interfaces in linux, you can use the following bash command: "dmesg | grep tty".)
After starting the Micro-ROS agent, the MCU can connect to the host PC via the serial interface and reset to establish a new connection. If the following Micro-ROS agent terminal output:
This proves the success of transplanting Micro-Ros On Frdm-mcxm947.