平台:RDK X5
电机:DJI M2006 、M3508、GM6020
屏:3.5 寸 ST7796S SPI LCD(480×320)+ GT911 触摸
关键词:SocketCAN、tcan4x5x、m_can、PyQt5、PID
打过RoboMaster 的小伙伴都有一个痛点, 就是机械经常需要动电机去测试一些机构是否合理,如弹仓拨轮,发射等,但是嵌入式不一定有空,有了RDK X5 DJI控制上位机软件,这个痛点就会迎刃而解,纯小白,只要接上线,打开软件,就能控制电机,调节PID,甚至还能看曲线,让 RDK X5 像一台普通工控机一样去驱动 DJI 电机。先看效果:
1. RDK X5 硬件介绍
RDK X5集成了TCAN4550 TCAN4550是带有集成 CAN FD 收发器的 CAN FD 控制器,支持高达 8Mbps 的数据速率。此 CAN FD 控制器符合 ISO11898-1:2015 高速控制器局域网 (CAN) 数据链路层的规范,并满足 ISO11898–2:2016 高速CAN 规范的物理层要求。TCAN4550 通过串行外设接口 (SPI) 在 CAN 总线和系统处理器之间提供了一个接口,同时支持经典 CAN 和 CAN FD,并为不支持CAN FD 的处理器实现 端口扩展或 CAN 支持。TCAN4550 提供 CAN FD 收发器功能:传输到总线的差分传输能力和从总线接收的差分接收能力。
这意味着你不需要外挂任何 USB-CAN 适配器或 STM32 中转板,X5 本身就能直接接入 CAN 总线。配合板载的算力,X5 可以成为一台"算法 + 运动控制"一体机——视觉/SLAM 在一颗 SoC 上跑,控制环也在同一颗 SoC 上跑,省掉外挂 MCU + 串口/USB 中转的麻烦。
X5 的 CANFD 接口详细引脚定义和电气规格,可参考官方文档:
https://developer.d-robotics.cc/rdk_doc/Quick_start/hardware_introduction/rdk_x5#canfd-接口
小结:RDK X5 自带 TCAN4x5x CAN 控制器,出厂就有
can0接口,是做"视觉 + 运控"一体化的天然好平台。
2. CAN 基础使用
2.1 接线与终端电阻
DJI C610/C620 电调的 CAN 接口走的是标准 CAN 2.0B 差分总线,接线如下:
RDK X5 DJI C610 电调
┌──────────┐ ┌──────────────┐
│ CAN0_H ├───────────────┤ CAN_H │
│ CAN0_L ├───────────────┤ CAN_L │
│ GND ├───────────────┤ GND │
└──────────┘ └──────────────┘
│ │
[120Ω 终端电阻 跨接在 CAN_H / CAN_L 上]
几个必须注意的点:
- 120Ω 终端电阻必装。整条 CAN 总线两端各需要一颗 120Ω 电阻。保持总线电阻为60欧,这里打开的是rdk x5的can电阻和c610电调的电阻。
- GND 必须共地。CAN 是差分信号但不是悬空的,没有共地会出现莫名其妙的 bus-off。
- 电调供电独立:C610 走 24V,建议用带电流表的稳压电源,方便看堵转电流。
- 不要把 H/L 接反,接反不会烧东西,但
can0会一直收不到任何报文。
提示:先用万用表量一下 CAN_H 和 CAN_L 之间的电阻,应该接近 60Ω(两颗 120Ω 并联)。这是判断终端电阻有没有装对的最快方法。
2.2 安装 CAN 驱动修复包
RDK X5 出厂的 hobot-boot 包对 m_can 的中断处理有个小问题。参考话题:
D-Robotics 后来发布了修复版的 boot 包,装上就好了:
# 下载修复包
wget https://archive.d-robotics.cc/downloads/fix_deb/hobot-boot_3.1.0-20260518154347_arm64.deb
# 安装并重启
sudo dpkg -i hobot-boot_3.1.0-20260518154347_arm64.deb
sudo reboot
重启后 can0 的 RX 就能正常工作了。
2.3 配置 can0 接口
重启完成后,配置 CAN 接口:
sudo ip link set can0 down 2>/dev/null
sudo ip link set can0 type can bitrate 1000000 restart-ms 100
sudo ip link set can0 up
ip -details link show can0
注意三点:
- bitrate 必须设为 1Mbps。DJI 电调的波特率是写死的 1M,不能改。
- restart-ms 100:bus-off 后 100ms 自动恢复,调试期间被电调拔插一下不至于要手动重启 can0。
- 看
ip -details输出里的state ERROR-ACTIVE,如果是BUS-OFF,那基本就是终端电阻或接线有问题。
把它写成 systemd 服务或者放进 /etc/network/interfaces.d/can0 都行,看个人习惯。
2.4 用 candump / cansend 测试
物理层通不通,最快的验证方法不是写 Python,而是 cansend:
# 给 motor1(ID 1)发 1000 正向电流
# 0x03E8 = 1000,big-endian
cansend can0 200#03E8000000000000
如果电机"嗡"一下转起来,恭喜,物理层完全没问题。如果没反应,请按以下顺序排查:
candump can0有没有看到 0x201 周期性反馈帧(电调每 1ms 反馈一次)?没有 → 检查接线 / 终端电阻 / fix 包。- 电调的拨码(C610 上有 4 位拨码盘)有没有正确设置 ID?1 号电调拨到 1。
- 24V 上电没?C610 没有供电是不会动的。
2.5 SocketCAN 编程基础(Python)
Linux SocketCAN 把 CAN 总线抽象成一个网络接口,用标准 socket API 就能收发 CAN 帧,不需要任何额外 SDK。
import socket, struct
# 创建 CAN socket
s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW)
s.bind(('can0',))
# 发送一帧 CAN 数据
# SocketCAN 帧格式:can_id(4B) + dlc(1B) + pad(3B) + data(8B)
data = struct.pack('>hhhh', 1000, 0, 0, 0) # motor1 = 1000mA
frame = struct.pack('=IB3x8s', 0x200, 8, data) # '=' 强制 native byte order
s.send(frame)
# 接收一帧
raw = s.recv(16)
can_id, dlc, _, data = struct.unpack('=IB3x8s', raw)
两个要点:
struct.pack('=IB3x8s', ...)里的=是关键,强制 native byte order,否则在 ARM64 上对齐会错位。- CAN socket 默认会收到总线上所有帧;可以用
s.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FILTER, ...)做硬件级过滤,减少 CPU 开销。
3. SPI LCD 屏幕配置
为了让 GUI 更好用,我在 X5 上接了一块 3.5 寸 ST7796S SPI LCD(480×320)+ GT911 触摸屏。这块小屏可以直接手指触控,比 SSH 命令行操作电机方便太多。
ST7796S + GT911 这一套在 RDK X5 上的完整点亮过程(包括内核模块、设备树 overlay、X11 配置),我之前在 D-Robotics 论坛写过一篇详细教程,可以参考:
点亮之后系统会多出一个 /dev/fb1 和 X11 上的一块新 screen,可以直接跑 Qt/PyQt5 应用。也可以使用 D-Robotics 提供的 Cathay-Launch-UI 仓库一键部署所有驱动和配置。
小结:3.5 寸 SPI 屏点亮后,X5 就有了一块可以手指触控的显示终端,非常适合做电机的现场调试 GUI。
4. DJI RoboMaster 电机控制
这是本文的核心部分。DJI RoboMaster 系列电机(M2006、M3508、GM6020)通过对应电调(C610/C620)与主控通信。M2006 和 M3508 共用同一套 CAN 协议,但 GM6020 的发送帧 ID 和接收帧 ID 与它们不同,混用时需要特别注意。
4.1 DJI CAN 协议
M2006/M3508 和 GM6020 的 CAN 帧结构相同(8 字节、大端序),但 帧 ID 完全不同,混用时必须区分电机类型。
发送帧(主控 → 电调)
M2006 / M3508(电流控制):
| CAN ID | 控制的电机 | 字节 0~1 | 字节 2~3 | 字节 4~5 | 字节 6~7 |
|---|---|---|---|---|---|
0x200 |
motor 1~4 | M1 电流 (i16 BE) | M2 电流 | M3 电流 | M4 电流 |
0x1FF |
motor 5~8 | M5 电流 | M6 电流 | M7 电流 | M8 电流 |
GM6020:
| CAN ID | 控制的电机 | 字节 0~1 | 字节 2~3 | 字节 4~5 | 字节 6~7 |
|---|---|---|---|---|---|
0x1FF |
motor 1~4 | M1 电压 (i16 BE) | M2 电压 | M3 电压 | M4 电压 |
0x2FF |
motor 5~8 | M5 电压 | M6 电压 | M7 电压 | M8 电压 |
GM6020:
| CAN ID | 控制的电机 | 字节 0~1 | 字节 2~3 | 字节 4~5 | 字节 6~7 |
|---|---|---|---|---|---|
0x1FE |
motor 1~4 | M1 电流 (i16 BE) | M2 电流 | M3 电流 | M4 电流 |
0x2FE |
motor 5~8 | M5 电流 | M6 电流 | M7 电流 | M8 电流 |
- DLC 固定 8,即使你只控制一个电机,剩下的位置补 0。
- 大端序(big-endian),Python 里用
struct.pack('>h', value)打包。 - 0x1FF 冲突:
0x1FF同时承载 M3508 ID 5~8 的电流命令和 GM6020 ID 1~4 的电压命令。如果你的系统里同时有这两组电机,它们的命令会在同一帧的同一字节位置冲突,此时需要通过+=合并,或者避免这种配置。
接收帧(电调 → 主控)
M2006 / M3508:
CAN ID = 0x200 + motor_id,例如 1 号电机反馈帧 ID 是 0x201。
GM6020:
CAN ID = 0x204 + motor_id,例如 1 号 GM6020 反馈帧 ID 是 0x205,2 号是 0x206。
混用时的歧义问题:0x205~0x208 这个范围既可能是 M3508 的 ID 5~8(0x200+5~8),也可能是 GM6020 的 ID 1~4(0x204+1~4)。解歧的方法是根据系统中配置的电机类型来判断——如果你在 ID 1 上挂的是 GM6020,那 0x205 就是 GM6020 ID=1 的反馈;如果挂的是 M3508,那 0x205 就是 M3508 ID=5 的反馈。
反馈帧格式三款电机完全一致:
| 字节 | 内容 | 类型 | 单位 |
|---|---|---|---|
| 0~1 | 转子机械角度 ecd |
u16 BE | 0~8191(对应 0~360°) |
| 2~3 | 实际转速 rpm |
i16 BE | rpm |
| 4~5 | 实际转矩电流 cur |
i16 BE | 与发送同尺度 |
| 6 | 电调温度 | u8 | ℃ |
| 7 | 保留 | - | - |
解包示例:
ecd, rpm, cur, temp = struct.unpack('>HhhB', data[:7])
angle_deg = ecd * 360.0 / 8192.0
4.2 电机型号与规格
| 电机 | 配套电调 | 电流范围 | 特点 |
|---|---|---|---|
| M2006 | C610 | ±10000 | 小体积,低扭矩,适合弹舱 |
| M3508 | C620 | ±16384 | RoboMaster 标配驱动轮电机 |
| GM6020 | C620 | ±25000 | 云台电机,空心轴,支持角度直控 |
三款电机的反馈帧格式相同,但 GM6020 的发送帧 ID 和接收帧 ID 与 M2006/M3508 不同(详见 4.1 节),混用时务必注意。
4.3 控制模式
DJI 电调本身只提供电流控制(M2006/M3508)或电压控制(GM6020),更高级的控制环需要主控自己实现:
- 电流模式:直接下发电流值,电机输出对应扭矩。最底层,响应最快,但需要自己做一切闭环。
- 速度模式:在电流模式之上套一层 PI 环,目标 = rpm,输出 = 电流命令。适合做轮速控制。
- 位置模式:在速度模式之上再套一层 P 环(或 PD 环),目标 = 角度,输出 = 目标 rpm,再由速度环输出电流。适合云台/舵轮。
4.4 PID 在线参数整定与曲线观看
5 PyQt5 GUI 应用
光命令行还不够直观,我做了一个跑在 3.5 寸 SPI 屏上的图形界面,可以拨滑条调电流、切换控制模式、看实时曲线。
GUI 架构
┌─────────────────────────────────────────┐
│ GUI 主线程 (PyQt5) │
│ ├─ 滑条/按钮 → 写共享 target_state │
│ └─ QChart ← 读 ring-buffer │
├─────────────────────────────────────────┤
│ 控制线程 (1kHz, time.perf_counter) │
│ ├─ 读反馈 (非阻塞 recv) │
│ ├─ PID 计算 │
│ └─ struct.pack + s.send() │
└─────────────────────────────────────────┘
要点:
- GUI 和控制环必须分线程。Qt 的事件循环卡几十毫秒很正常,但 DJI 看门狗只有约 100ms,主线程一卡电机就停了。
- 反馈帧用非阻塞 recv。
s.setblocking(False),在 1kHz 周期里 try 一次,能读就读,读不到就跳过。 - 窗口全屏:
showFullScreen()+setWindowFlag(Qt.FramelessWindowHint),480×320 全屏没有标题栏才好用手指点。 - 图表:用 QtChart +
QSplineSeries,X 轴用 ring buffer 滑窗,只保留最近 5 秒数据,否则点多了渲染就拖死了。
功能特性
- 多电机支持:最多 8 路电机同时控制,每路独立滑条和模式切换。
- 控制模式切换:电流 / 速度 / 位置三种模式,一键切换。
- 实时曲线:RPM、角度、电流三条曲线实时滚动显示。
- 暗色主题:480×320 小屏上暗色主题对比度更好,手指操作也更舒适。
- 一键停止:紧急停止按钮,所有电机电流立即归零。
仓库
完整代码:
在板子上克隆仓库直接运行下面命令就能跑
cd ~/dji_motor_ctl
./scripts/run.sh
预告
RDK X5 你的下一代CAN逻辑分析仪


