news 2026/9/8 17:46:21

【瑞萨Micro-ROS评测】+基于 RA6M4 的 micro-ROS 自定义传输:CAN 2.0 + ISO-TP 实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【瑞萨Micro-ROS评测】+基于 RA6M4 的 micro-ROS 自定义传输:CAN 2.0 + ISO-TP 实现

1. 概述

micro-ROS 支持自定义传输层,允许开发者将 ROS 2 节点运行在资源受限的嵌入式设备上。由于标准 CAN 2.0 总线单帧最多只能传输 8 字节,无法直接承载较大的 ROS 消息,因此需要引入 ISO-TP(ISO 15765-2)协议进行拆包与组包。本文将详细介绍如何在 Renesas RA6M4 MCU 上实现基于 CAN 2.0 与 ISO-TP 的 micro-ROS 自定义传输,包括 Agent 的编译修改、传输层代码实现以及硬件配置。

2. 环境准备与 Agent 编译

2.1 获取 Micro-XRCE-DDS-Agent 源码

# 创建工作空间 mkdir -p ~/microros_ws/src cd ~/microros_ws/src 克隆 Micro-XRCE-DDS-Agent(micro-ROS Agent 的底层实现) git clone -b humble https://github.com/eProsima/Micro-XRCE-DDS-Agent.git cd Micro-XRCE-DDS-Agent 安装依赖 sudo apt update sudo apt install python3-rosdep python3-colcon-common-extensions rosdep update rosdep install --from-paths . --ignore-src -y

2.2 修改 Agent 支持自定义传输

Micro-XRCE-DDS-Agent 默认支持串口、UDP、TCP 等传输方式。要添加基于 CAN + ISO-TP 的自定义传输,本方案采用在 Agent 仓库根目录创建custom_agent_can_isotp.cpp的方式,并依赖isotp-c/子模块实现 ISO-TP 协议。最终目录结构如下:

Micro-XRCE-DDS-Agent/ ├── CMakeLists.txt ├── custom_agent_can_isotp.cpp ├── isotp-c/ ├── include/ ├── src/ └── build/

步骤 1:添加 isotp-c 子模块

在 Agent 仓库根目录下添加lishen2/isotp-c作为子模块:

cd ~/microros_ws/src/Micro-XRCE-DDS-Agent git submodule add https://github.com/lishen2/isotp-c.git isotp-c

步骤 2:创建 custom_agent_can_isotp.cpp

在 Agent 仓库根目录创建custom_agent_can_isotp.cpp,实现基于 CAN + ISO-TP 的自定义传输。该文件直接使用isotp-c子模块提供的协议栈,并通过uxrCustomTransport接口对接 micro-ROS Agent:

// custom_agent_can_isotp.cpp #include <uxr/agent/transport/custom/CustomAgent.hpp> #include <uxr/agent/logger/Logger.hpp> #include <uxr/agent/middleware/Middleware.hpp> #include <iostream> #include <iomanip> #include <cstdio> #include <string> #include <cstring> #include <ctime> #include <unistd.h> #include <sys/socket.h> #include <net/if.h> #include <sys/ioctl.h> #include <linux/can.h> #include <linux/can/raw.h> #include <poll.h> extern "C" { #include "isotp.h" } using namespace eprosima::uxr; static int g_sock = -1; #define ISOTP_TX_ID 0x2 #define ISOTP_RX_ID 0x1 #define ISOTP_BUFFER_SIZE 512 static IsoTpLink g_link; static uint8_t g_isotp_send_buf[ISOTP_BUFFER_SIZE]; static uint8_t g_isotp_recv_buf[ISOTP_BUFFER_SIZE]; // 辅助:打印十六进制数据 static void print_hex(const uint8_t* data, size_t len) { std::cout << " "; for (size_t i = 0; i < len; ++i) { std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)data[i] << " "; if ((i+1) % 16 == 0) std::cout << std::endl << " "; } std::cout << std::dec << std::endl; } int isotp_user_send_can(const uint32_t arbitration_id, const uint8_t *data, const uint8_t size) { if (g_sock < 0 || size > 8) return -1; struct can_frame frame; memset(&frame, 0, sizeof(frame)); frame.can_id = arbitration_id & 0x7FF; frame.can_dlc = size; memcpy(frame.data, data, size); printf("[ISOTP] TX CAN id=0x%03X dlc=%u data=", arbitration_id & 0x7FF, size); for (uint8_t i = 0; i < size; ++i) printf("%02x ", data[i]); printf("\n"); fflush(stdout); ssize_t nbytes = write(g_sock, &frame, sizeof(frame)); return (nbytes == sizeof(frame)) ? ISOTP_RET_OK : -1; } uint32_t isotp_user_get_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } void isotp_user_debug(const char *message, ...) { (void)message; } static bool custom_init() { std::cout << "[DEBUG] custom_init() called" << std::endl; return true; } static bool custom_fini() { std::cout << "[DEBUG] custom_fini() called" << std::endl; return true; } static ssize_t custom_recv(CustomEndPoint *endpoint, uint8_t *buffer, size_t buffer_length, int timeout, TransportRc &transport_rc) { (void)timeout; (void)endpoint; while (true) { struct can_frame frame; struct pollfd fds; fds.fd = g_sock; fds.events = POLLIN; int ret = poll(&amp;amp;fds, 1, 100); if (ret &amp;gt; 0 &amp;amp;&amp;amp; (fds.revents &amp;amp; POLLIN)) { ssize_t nbytes = read(g_sock, &amp;amp;frame, sizeof(frame)); if (nbytes == sizeof(frame)) { printf("[ISOTP] RX CAN id=0x%03X dlc=%u data=", frame.can_id &amp;amp; 0x7FF, frame.can_dlc); for (int i = 0; i &amp;lt; frame.can_dlc; ++i) printf("%02x ", frame.data[i]); printf("\n"); fflush(stdout); if ((frame.can_id &amp;amp; 0x7FF) == ISOTP_RX_ID) { isotp_on_can_message(&amp;amp;g_link, frame.data, frame.can_dlc); } } } isotp_poll(&amp;amp;g_link); uint16_t out_size = 0; int rc = isotp_receive(&amp;amp;g_link, buffer, (uint16_t)buffer_length, &amp;amp;out_size); if (rc == ISOTP_RET_OK &amp;amp;&amp;amp; out_size &amp;gt; 0) { transport_rc = TransportRc::ok; std::cout &amp;lt;&amp;lt; "[DEBUG] custom_recv: received complete message, len=" &amp;lt;&amp;lt; out_size &amp;lt;&amp;lt; std::endl; print_hex(buffer, out_size); return out_size; } } } static ssize_t custom_send(const CustomEndPoint *endpoint, uint8_t buffer, size_t message_length, TransportRc &transport_rc) { (void)endpoint; std::cout << "[DEBUG] custom_send called, message_length=" << message_length << std::endl; print_hex(buffer, message_length); / 若上一次多帧发送仍在进行(CF 由接收线程 isotp_poll 异步发出), isotp_send 会返回 INPROGRESS(-2)。等待其完成后重试, 避免被误判为 server error 而反复 fini/init。 */ int ret = isotp_send(&amp;g_link, buffer, (uint16_t)message_length); if (ret == ISOTP_RET_INPROGRESS) { int waited = 0; while (g_link.send_status == ISOTP_SEND_STATUS_INPROGRESS &amp;&amp; waited &lt; 200) { usleep(1000); waited++; } std::cout &lt;&lt; "[DEBUG] custom_send: waited " &lt;&lt; waited &lt;&lt; "ms for previous TX, retry" &lt;&lt; std::endl; ret = isotp_send(&amp;g_link, buffer, (uint16_t)message_length); } if (ret == ISOTP_RET_OK) { transport_rc = TransportRc::ok; std::cout &lt;&lt; "[DEBUG] custom_send: isotp_send succeeded" &lt;&lt; std::endl; return message_length; } else { transport_rc = TransportRc::server_error; std::cout &lt;&lt; "[DEBUG] custom_send: isotp_send failed with ret=" &lt;&lt; ret &lt;&lt; std::endl; return -1; } } int main(int argc, char **argv) { (void)argc; (void)argv; const char *ifname = "can0"; if ((g_sock = socket(PF_CAN, SOCK_RAW, CAN_RAW)) &lt; 0) { perror("socket"); return 1; } struct ifreq ifr; strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); ifr.ifr_name[IFNAMSIZ - 1] = '\0'; if (ioctl(g_sock, SIOCGIFINDEX, &amp;ifr) &lt; 0) { perror("ioctl"); close(g_sock); return 1; } struct sockaddr_can addr; addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(g_sock, (struct sockaddr *)&amp;addr, sizeof(addr)) &lt; 0) { perror("bind"); close(g_sock); return 1; } isotp_init_link(&amp;g_link, ISOTP_TX_ID, g_isotp_send_buf, sizeof(g_isotp_send_buf), g_isotp_recv_buf, sizeof(g_isotp_recv_buf)); auto init_func = std::function&lt;bool()&gt;(custom_init); auto fini_func = std::function&lt;bool()&gt;(custom_fini); auto send_func = std::function&lt;ssize_t(const CustomEndPoint *, uint8_t *, size_t, TransportRc &amp;)&gt;(custom_send); auto recv_func = std::function&lt;ssize_t(CustomEndPoint *, uint8_t *, size_t, int, TransportRc &amp;)&gt;(custom_recv); CustomEndPoint *endpoint = new CustomEndPoint(); CustomAgent agent( "CAN_ISOTP", endpoint, Middleware::Kind::FASTDDS, false, init_func, fini_func, send_func, recv_func ); // 设置更高的日志级别 agent.set_verbose_level(6); // 使用 start 启动(根据官方示例) agent.start(); // 程序会阻塞在 start() 中,直到收到停止信号 // 但为了演示,我们添加一个简单的循环保持运行 // 实际 start() 内部会处理信号,但这里我们简单等待 while (true) { sleep(1); } close(g_sock); delete endpoint; return 0; }

步骤 3:修改 CMakeLists.txt

在 Agent 根目录的CMakeLists.txt中添加对custom_agent_can_isotp.cppisotp-c子模块的编译支持:

# 在 Micro-XRCE-DDS-Agent 根目录的 CMakeLists.txt 中添加 添加 isotp-c 子模块 add_subdirectory(isotp-c) 编译自定义传输源文件 add_library(custom_agent_can_isotp custom_agent_can_isotp.cpp ) target_include_directories(custom_agent_can_isotp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/isotp-c ) target_link_libraries(custom_agent_can_isotp isotp-c ) 将自定义传输库链接到 agent 可执行文件 target_link_libraries(microxrcedds_agent custom_agent_can_isotp ... 其他库 )

步骤 4:在 agent.cpp 中注册自定义传输

src/cpp/agent/Agent.cpp中声明并注册自定义传输的创建函数:

// 在 Agent.cpp 顶部声明 extern "C" { struct uxrCustomTransport *get_custom_can_isotp_transport(void); } // 在 Agent::create_transport 中添加分支 std::unique_ptr<Transport> Agent::create_transport( const TransportKind transport_kind, const std::string& dev) { switch (transport_kind) { case TransportKind::serial: return std::make_unique<SerialTransport>(dev); case TransportKind::udp4: return std::make_unique<UDPv4Transport>(dev); case TransportKind::tcp4: return std::make_unique<TCPv4Transport>(dev); case TransportKind::custom_can_isotp: // 新增 return std::make_unique<CustomCANIsotpTransport>(); default: throw std::runtime_error("Unsupported transport"); } }

2.3 编译 Agent

cd ~/microros_ws colcon build --packages-select microxrcedds_agent source install/setup.bash

3. RA6M4 CAN 传输层实现

硬件接线示意图如下:

以下代码基于 Renesas RA6M4 MCU 的 FSP(Flexible Software Package)和开源 ISO-TP 库lishen2/isotp-c实现。请将代码复制到你的工程中。

3.1 头文件 (microros_transports.h)

#ifndef MICROROS_TRANSPORTS_H_ #define MICROROS_TRANSPORTS_H_ #include <stdbool.h> #include <stddef.h> #include <stdint.h> struct uxrCustomTransport; bool renesas_e2_transport_open(struct uxrCustomTransport * transport); bool renesas_e2_transport_close(struct uxrCustomTransport * transport); size_t renesas_e2_transport_write(struct uxrCustomTransport * transport, const uint8_t * buf, size_t len, uint8_t * error); size_t renesas_e2_transport_read(struct uxrCustomTransport * transport, uint8_t * buf, size_t len, int timeout, uint8_t * error); #endif /* MICROROS_TRANSPORTS_H_ */

3.2 源文件 (microros_transport_can_adapter.c)

/* * micro-ROS custom transport for Renesas RA6M4 using Classic CAN + ISO-TP (lishen2/isotp-c) */ #include "microros_transports.h" #include "hal_data.h" #include <string.h> #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <uxr/client/transport.h> #include <uxr/client/util/time.h> #include "isotp.h" /* ==================== CAN hardware config ==================== */ #define CAN_BAUDRATE 500000U #define CAN_MAILBOX_TX 0U #define CAN_MAILBOX_RX_FIFO 0U /* ISO-TP parameters (must match the agent side) */ #define ISOTP_TX_ID 0x1 // CAN ID used for transmission #define ISOTP_RX_ID 0x2 // CAN ID to accept (RX filter) #define ISOTP_BUFFER_SIZE 512 // RX/TX buffer size /* ==================== ISO-TP global state ==================== */ static IsoTpLink g_link; static uint8_t g_isotp_send_buf[ISOTP_BUFFER_SIZE]; static uint8_t g_isotp_recv_buf[ISOTP_BUFFER_SIZE]; /* ==================== CAN hardware state ==================== */ static volatile bool g_tx_complete = false; static volatile bool g_tx_error = false; /* Raw CAN frame RX queue */ typedef struct { can_frame_t frame; bool valid; } can_rx_item_t; #define RX_QUEUE_SIZE 32 static can_rx_item_t g_rx_queue[RX_QUEUE_SIZE]; static volatile size_t g_rx_head = 0; static volatile size_t g_rx_tail = 0; /* ==================== callbacks required by isotp-c ==================== */ /** @brief Send a single CAN frame (called by isotp-c) @param arbitration_id CAN ID @param data Data pointer, length <= 8 @param size Data length @return ISOTP_RET_OK on success, otherwise an error */ int isotp_user_send_can(const uint32_t arbitration_id, const uint8_t *data, const uint8_t size) { if (size > 8) return -1; can_frame_t tx_frame; memset(&tx_frame, 0, sizeof(tx_frame)); tx_frame.id = arbitration_id & 0x7FF; tx_frame.id_mode = CAN_ID_MODE_STANDARD; tx_frame.type = CAN_FRAME_TYPE_DATA; tx_frame.data_length_code = size; memcpy(tx_frame.data, data, size); g_tx_complete = false; g_tx_error = false; fsp_err_t err = R_CAN_Write(&g_can_ctrl, CAN_MAILBOX_TX, &tx_frame); if (err != FSP_SUCCESS) { return -1; } /* Wait for TX completion (max 100 ms) */ int64_t start = uxr_millis(); while (!g_tx_complete && !g_tx_error && (uxr_millis() - start) < 100) { R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS); } return (g_tx_complete && !g_tx_error) ? ISOTP_RET_OK : -1; } /** @brief System millisecond clock (called by isotp-c) */ uint32_t isotp_user_get_ms(void) { return (uint32_t)uxr_millis(); } /** @brief Optional debug output (called by isotp-c) */ void isotp_user_debug(const char *message, ...) { (void)message; } /* ==================== CAN callback (FSP interrupt) ==================== */ void can_callback(can_callback_args_t *p_args) { if (!p_args) return; switch (p_args-&gt;event) { case CAN_EVENT_TX_COMPLETE: g_tx_complete = true; break; case CAN_EVENT_RX_COMPLETE: { const can_frame_t *p_frame = &amp;amp;p_args-&amp;gt;frame; size_t next_tail = (g_rx_tail + 1) % RX_QUEUE_SIZE; if (next_tail != g_rx_head) { g_rx_queue[g_rx_tail].frame = *p_frame; g_rx_queue[g_rx_tail].valid = true; g_rx_tail = next_tail; } break; } case CAN_EVENT_TX_ABORTED: case CAN_EVENT_ERR_BUS_OFF: default: g_tx_error = true; break; } } /* ==================== micro-ROS transport interface ==================== */ bool renesas_e2_transport_open(struct uxrCustomTransport *transport) { (void)transport; fsp_err_t err = R_CAN_Open(&amp;g_can_ctrl, &amp;g_can_cfg); if (err != FSP_SUCCESS) return false; /* Initialize the ISO-TP link */ isotp_init_link(&amp;g_link, ISOTP_TX_ID, g_isotp_send_buf, sizeof(g_isotp_send_buf), g_isotp_recv_buf, sizeof(g_isotp_recv_buf)); g_rx_head = g_rx_tail = 0; return true; } bool renesas_e2_transport_close(struct uxrCustomTransport *transport) { (void)transport; return (R_CAN_Close(&g_can_ctrl) == FSP_SUCCESS); } size_t renesas_e2_transport_write(struct uxrCustomTransport *transport, const uint8_t *buf, size_t len, uint8_t *error) { (void)transport; if (!buf || len == 0 || len &gt; ISOTP_BUFFER_SIZE) { if (error) *error = 1; return 0; } int ret = isotp_send(&amp;g_link, buf, (uint16_t)len); if (ret == ISOTP_RET_OK) { if (error) *error = 0; return len; } else { if (error) *error = 1; return 0; } } size_t renesas_e2_transport_read(struct uxrCustomTransport *transport, uint8_t *buf, size_t len, int timeout, uint8_t *error) { (void)transport; if (!buf || len == 0) { if (error) *error = 1; return 0; } int64_t start = uxr_millis(); while ((uxr_millis() - start) &lt; timeout) { /* 1. Feed CAN frames from the HW queue into ISO-TP */ if (g_rx_head != g_rx_tail) { can_rx_item_t item = &amp;g_rx_queue[g_rx_head]; / Only accept ISOTP_RX_ID. NOTE: the CANable may report the agent's * standard frame (0x002) as an extended ID (SID shifted to bits 18-28, * e.g. 0x00080000), so compare the SID bits for extended frames. */ uint32_t rx_id = item-&gt;frame.id &amp; 0x7FF; if (item-&gt;frame.id_mode == CAN_ID_MODE_EXTENDED) { rx_id = (item-&gt;frame.id &gt;&gt; 18) &amp; 0x7FF; } if (rx_id == ISOTP_RX_ID) { isotp_on_can_message(&amp;g_link, item-&gt;frame.data, item-&gt;frame.data_length_code); } item-&gt;valid = false; g_rx_head = (g_rx_head + 1) % RX_QUEUE_SIZE; } /* 2. Poll periodically to drive the multi-frame state machine */ isotp_poll(&amp;amp;g_link); /* 3. Try to extract a complete message */ uint16_t out_size = 0; int ret = isotp_receive(&amp;amp;g_link, buf, (uint16_t)len, &amp;amp;out_size); if (ret == ISOTP_RET_OK &amp;amp;&amp;amp; out_size &amp;gt; 0) { if (error) *error = 0; return out_size; } R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS); } if (error) *error = 1; return 0; }

4. 运行效果展示

下面展示 micro-ROS 节点通过 CAN + ISO-TP 自定义传输与 Agent 通信的实际运行效果。

4.1 硬件连接与实物图

RA6M4 开发板通过 CAN 收发器(如 TJA1050)连接 CAN 总线,另一端接入运行 Micro-XRCE-DDS-Agent 的主机(通过 USB-CAN 适配器,如 CANable)。整体连接示意如下:

4.2 Agent 端运行日志

启动 Agent 并指定自定义 CAN 传输后,终端输出如下日志,可以看到 Agent 成功初始化 CAN 套接字并等待客户端连接:

zh@zh:~/work/ros/Micro-XRCE-DDS-Agent/build$ ./custom_agent_can_isotp [1787941708.460342] info | Root.cpp | set_verbose_level | logger setup | verbose_level: 6 [DEBUG] custom_init() called [1787941708.460421] info | CustomAgent.cpp | init | Custom agent status: opened | CAN_ISOTP agent running [ISOTP] RX CAN id=0x001 dlc=8 data=10 18 80 00 00 00 00 01 [ISOTP] TX CAN id=0x002 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=21 10 00 58 52 43 45 01 [ISOTP] RX CAN id=0x001 dlc=8 data=22 00 01 0f 12 34 56 78 [ISOTP] RX CAN id=0x001 dlc=8 data=23 81 00 fc 01 00 00 00 [DEBUG] custom_recv: received complete message, len=24 80 00 00 00 00 01 10 00 58 52 43 45 01 00 01 0f 12 34 56 78 81 00 fc 01 [1787941710.004017] debug | CustomAgent.cpp | recv_message | [==>> CAN_ISOTP <<==] | client_key: 0x00000000, len: 24, data: 0000: 80 00 00 00 00 01 10 00 58 52 43 45 01 00 01 0F 12 34 56 78 81 00 FC 01 [1787941710.004333] info | Root.cpp | create_client | create | client_key: 0x12345678, session_id: 0x81 [1787941710.004368] info | SessionManager.hpp | establish_session | session established | client_key: 0x12345678, address: [DEBUG] custom_send called, message_length=19 81 00 00 00 04 01 0b 00 00 00 58 52 43 45 01 00 01 0f 00 [ISOTP] TX CAN id=0x002 dlc=8 data=10 13 81 00 00 00 04 01 [DEBUG] custom_send: isotp_send succeeded [1787941710.004511] debug | CustomAgent.cpp | send_message | [** <<CAN_ISOTP>> **] | client_key: 0x12345678, len: 19, data: 0000: 81 00 00 00 04 01 0B 00 00 00 58 52 43 45 01 00 01 0F 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] TX CAN id=0x002 dlc=8 data=21 0b 00 00 00 58 52 43 [ISOTP] TX CAN id=0x002 dlc=8 data=22 45 01 00 01 0f 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=10 34 81 80 00 00 01 07 [ISOTP] TX CAN id=0x002 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=21 2c 00 00 0a 00 01 01 [ISOTP] RX CAN id=0x001 dlc=8 data=22 03 00 00 1e 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=23 00 01 00 00 16 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=24 00 72 61 36 6d 34 5f [ISOTP] RX CAN id=0x001 dlc=8 data=25 66 6c 61 73 68 5f 70 [ISOTP] RX CAN id=0x001 dlc=8 data=26 75 62 6c 69 73 68 65 [ISOTP] RX CAN id=0x001 dlc=8 data=27 72 00 00 00 00 00 00 [DEBUG] custom_recv: received complete message, len=52 81 80 00 00 01 07 2c 00 00 0a 00 01 01 03 00 00 1e 00 00 00 00 01 00 00 16 00 00 00 72 61 36 6d 34 5f 66 6c 61 73 68 5f 70 75 62 6c 69 73 68 65 72 00 00 00 [1787941710.122750] debug | CustomAgent.cpp | recv_message | [==>> CAN_ISOTP <<==] | client_key: 0x12345678, len: 52, data: 0000: 81 80 00 00 01 07 2C 00 00 0A 00 01 01 03 00 00 1E 00 00 00 00 01 00 00 16 00 00 00 72 61 36 6D 0020: 34 5F 66 6C 61 73 68 5F 70 75 62 6C 69 73 68 65 72 00 00 00 [1787941710.129614] info | ProxyClient.cpp | create_participant | participant created | client_key: 0x12345678, participant_id: 0x000(1) [DEBUG] custom_send called, message_length=14 81 80 00 00 05 01 06 00 00 0a 00 01 00 00 [ISOTP] TX CAN id=0x002 dlc=8 data=10 0e 81 80 00 00 05 01 [DEBUG] custom_send: isotp_send succeeded [1787941710.130041] debug | CustomAgent.cpp | send_message | [** <<CAN_ISOTP>> **] | client_key: 0x12345678, len: 14, data: 0000: 81 80 00 00 05 01 06 00 00 0A 00 01 00 00 [DEBUG] custom_send called, message_length=13 81 00 00 00 0a 01 05 00 01 00 00 00 80 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] TX CAN id=0x002 dlc=8 data=21 06 00 00 0a 00 01 00 [ISOTP] RX CAN id=0x001 dlc=8 data=10 0d 81 00 00 00 0b 01 [ISOTP] TX CAN id=0x002 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] TX CAN id=0x002 dlc=8 data=22 00 00 00 00 00 00 00 [DEBUG] custom_send: waited 53ms for previous TX, retry [ISOTP] TX CAN id=0x002 dlc=8 data=10 0d 81 00 00 00 0a 01 [DEBUG] custom_send: isotp_send succeeded [1787941710.209888] debug | CustomAgent.cpp | send_message | [** <<CAN_ISOTP>> **] | client_key: 0x12345678, len: 13, data: 0000: 81 00 00 00 0A 01 05 00 01 00 00 00 80 [ISOTP] RX CAN id=0x001 dlc=8 data=21 05 00 00 00 00 00 80 [DEBUG] custom_recv: received complete message, len=13 81 00 00 00 0b 01 05 00 00 00 00 00 80 [1787941710.210877] debug | CustomAgent.cpp | recv_message | [==>> CAN_ISOTP <<==] | client_key: 0x12345678, len: 13, data: 0000: 81 00 00 00 0B 01 05 00 00 00 00 00 80 [DEBUG] custom_send called, message_length=13 81 00 00 00 0a 01 05 00 01 00 00 00 80 [ISOTP] RX CAN id=0x001 dlc=8 data=10 0d 81 00 00 00 0a 01 [ISOTP] TX CAN id=0x002 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] RX CAN id=0x001 dlc=8 data=30 08 00 00 00 00 00 00 [ISOTP] TX CAN id=0x002 dlc=8 data=21 05 00 01 00 00 00 80 [DEBUG] custom_send: waited 3ms for previous TX, retry [ISOTP] TX CAN id=0x002 dlc=8 data=10 0d 81 00 00 00 0a 01 [DEBUG] custom_send: isotp_send succeeded [1787941710.215807] debug | CustomAgent.cpp | send_message | [** <<CAN_ISOTP>> **] | client_key: 0x12345678, len: 13, data:

4.4 视频演示

以下视频演示了从 Agent 启动、节点连接、话题发布到数据接收的完整流程:

微信视频2026-08-29_234547_053

视频中可以看到:

  • Agent 端打印的 CAN 帧收发日志(TX/RX 十六进制数据)。
  • RA6M4 端通过串口输出的 micro-ROS 节点状态信息。
  • 上位机订阅到话题数据并实时刷新显示。

STM32_PY32_CH32_RA8/ra6m4

https://download.csdn.net/download/MakeWorks/93356894

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 15:37:10

LVGL手表UI开发实战:内存、刷新、功耗与页面管理的平衡术

做嵌入式 GUI 有一个特别容易踩的坑&#xff1a;以为用 LVGL 做手表 UI&#xff0c;重点是把界面画得好看。真到上手时会发现&#xff0c;在 MCU 上做手表界面&#xff0c;难点从来不在某个控件怎么用&#xff0c;而在内存、刷新、事件和功耗这四件事怎么平衡。特别是当你决定做…

作者头像 李华
网站建设 2026/9/5 14:38:27

信号与系统必考点:单位冲激函数性质与解题套路

信号与系统这门课&#xff0c;网上讨论度最高的两个考点&#xff0c;一个是卷积&#xff0c;另一个就是单位冲激函数。很多新手看到教材里写“δ(t) 在零点等于无穷大”就直接懵掉&#xff0c;觉得这是一个数学家拿来吓人的概念。实际上&#xff0c;在“做题”这个层面&#xf…

作者头像 李华
网站建设 2026/9/5 18:07:19

网约车抽成比例下调:规则引擎如何支撑计费系统灵活调整

最近不少城市都在讨论网约车平台下调抽成比例的事情。作为开发者&#xff0c;我们看到的可能不只是一个“比例数字变化”&#xff0c;而是一整套计费系统、规则配置、结算链路和数据对账逻辑需要跟着调整。业务侧一句话&#xff0c;技术侧往往要动好几个服务。这篇文章想从工程…

作者头像 李华
网站建设 2026/9/6 0:40:12

上帝视角不是一张图:从无人机航拍到实景三维的空间数据工作流

先从一个真实画面说起。一台多旋翼无人机起飞后&#xff0c;按照规划好的航线采集了几百张影像&#xff1b;另一边&#xff0c;十几个固定在塔吊和围挡上的摄像头正在回传现场画面&#xff1b;调度室里&#xff0c;项目经理盯着大屏&#xff0c;不再需要反复切单路画面&#xf…

作者头像 李华
网站建设 2026/9/5 21:39:25

框架选型不再看标题:从压测数据到生产迁移的评估指南

如果让我对“史上最牛逼框架、吊打 Rust”这种标题做技术评审&#xff0c;我的第一反应是先看评测基准&#xff0c;再看压测脚本&#xff0c;最后才会去打开代码仓库。这类标题的广告属性通常大于工程属性&#xff0c;但它背后确实藏着一个值得认真聊的话题&#xff1a;我们到底…

作者头像 李华