本文档说明如何在 Linux 上用 虚拟 CAN 总线(vcan) 搭建、编译并运行两个 CANopen 电机 demo:
motor402_slave— CiA 402 模拟伺服驱动器(从站,实现 402 状态机 + 位置/速度模式)motor402_master— CANopen 主站(NMT 主站 + SDO 客户端 + 心跳监控),自动控制从站往复运动
不需要任何 CAN 硬件,全部在虚拟总线上完成收发调试。
note:files at demos/
| 项目 | 要求 |
|---|---|
| 操作系统 | Linux(Debian/Ubuntu 系最方便,本文以 Ubuntu 为例) |
| 内核 | 支持 SocketCAN / CAN 子系统(主流发行版默认支持) |
| 工具链 | gcc、make、cmake(≥3.10) |
| 权限 | root(仅创建 vcan 接口时需要) |
| 可选 | can-utils(candump/cansend 等,强烈建议安装,方便调试) |
检查内核 CAN 支持:
# 应能正常加载(或提示已加载)
sudo modprobe can
sudo modprobe vcan如果 modprobe vcan 报错,说明内核未编译 vcan 模块(罕见,可 sudo apt install linux-modules-extra-$(uname -r) 或换内核)。
sudo apt update
sudo apt install build-essential cmake can-utilssudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
# 验证
ip link show vcan0 # 应看到 vcan0: <NOARP,UP,LOWER_UP> ... link/can注意:
vcan0重启后消失,每次开机需重新执行(可选:见 2.3 开机自动创建)。
创建 systemd 服务 /etc/systemd/system/vcan0.service:
[Unit]
Description=Virtual CAN interface vcan0
After=network.target
[Service]
Type=oneshot
ExecStart=/sbin/ip link add dev vcan0 type vcan
ExecStart=/sbin/ip link set up vcan0
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now vcan0在项目根目录:
cd CANopenLinux
cmake -S . -B build -G "Unix Makefiles"
cmake --build build -j生成三个可执行文件(build/ 目录下):
| 目标 | 说明 |
|---|---|
build/canopend |
标准 CANopen 设备 + commander(与 make 一致,OD 为示例 DS301) |
build/motor402_slave |
CiA 402 模拟电机从站(自定义 402 对象字典 + 电机仿真) |
build/motor402_master |
CANopen 主站,自动控制 402 从站往复运动 |
语言标准:C11(-std=gnu11)、C++14(-std=gnu++14,工程已启用 C/C++ 双语言,
可自行添加 .cpp 源文件复用 libcanopenlinux_core.a 栈库)。
常用构建选项(配置时传入):
cmake -S . -B build -DCO_SINGLE_THREAD=ON # 单线程模式(默认开,匹配 Makefile)
cmake -S . -B build -DCO_CONFIG_DEBUG=ON # 开启栈调试日志
make原生命令也可用(构建canopend),但两个 402 demo 只能通过 CMake 构建。
./build/motor402_slave vcan0 -i 1参数(继承自 canopend):
| 参数 | 说明 |
|---|---|
<can device> |
CAN 接口名,如 vcan0 |
-i <NodeId> |
CANopen 节点号 1..127(默认 LSS 未配置 0xFF,会等 LSS 分配) |
-c <接口> |
CiA 309 ASCII 网关:stdio / local-<路径> / tcp-<端口>(调试用) |
-T <ms> |
网关 socket 超时 |
-r |
允许 NMT reset 后重启系统 |
启动后输出:
[slave] motor_app: communication reset
... (syslog: CANopen device, Node ID = 0x01, running ...)
说明:demo 从站已禁用非易失存储(
CO_CONFIG_STORAGE=0),因此首次启动 不会出现*.persist文件缺失的 EMCY 告警,NMT 稳定在 operational。
./build/motor402_master vcan0 -i 2 -s 1 -m pp --amp 80000| 参数 | 说明 | 默认 |
|---|---|---|
<can device> |
CAN 接口名 | — |
-i <NodeId> |
主站节点号 | 2 |
-s <NodeId> |
从站节点号 | 1 |
-m <mode> |
pp 位置模式 / pv 速度模式 |
pp |
-n <moves> |
往复次数 | 4 |
--amp <v> |
运动幅度(increments) | 100000 |
--pdo |
改用 RPDO1 发送控制字与目标(默认 SDO) | 关 |
运行流程(自动):NMT 启动从站 → 等心跳 → 设模式 → 0x6040=06/07/0F 使能 → 写目标位置 → 轮询状态字等 target reached → 读回实际位置 → 反向 → 循环。
# 1) 位置模式(pp):往复 2 次,幅度 60000
./build/motor402_master vcan0 -i 2 -s 1 -m pp -n 2 --amp 60000
# 2) 速度模式(pv):正反转各保持 2.5s
./build/motor402_master vcan0 -i 2 -s 1 -m pv -n 2 --amp 30000
# 3) PDO 控制:控制字+目标经 RPDO1(0x201) 发送
./build/motor402_master vcan0 -i 2 -s 1 -m pp --pdo -n 2 --amp 60000从站(命令接收 + 每秒位置变化):
[slave] received controlword 0x0006 ← Shutdown
[slave] received controlword 0x0007 ← Switch on
[slave] received controlword 0x000F ← Enable operation
[slave] received modes of operation 1 ← 0x6060 = pp
[slave] received target position 80000 ← 0x607A
[slave] DRIVE state: 1 -> 2 -> 3 -> 4 ← 402 状态机迁移
[slave] pp: target 80000 reached
[slave] pos=80000 (demand 80000), vel=0, state=4, sw=0x0637 ← 每秒
主站(SDO 收发,读值变化时才打印):
[master] SDO TX write 0x6060:0 = 01
[master] SDO TX write 0x6040:0 = 06 00
[master] SDO RX ack write 0x6040:0
[master] SDO TX write 0x607A:0 = 80 38 01 00 ← 80000 (小端 0x00013880)
[master] SDO TX read 0x6041:0
[master] SDO RX read 0x6041:0 = 37 02 ← 状态字变化
[master] moving to 80000
[master] target reached: statusword=0x0637 (operation enabled) target-reached
[master] position actual = 80000
从站(终端 1):
[slave] motor_app: communication reset
[slave] received controlword 0x0006 ← shutdown
[slave] DRIVE state: 1 -> 2 ← switch on disabled -> ready to switch on
[slave] received controlword 0x0007
[slave] DRIVE state: 2 -> 3 ← switched on
[slave] received controlword 0x000F
[slave] DRIVE state: 3 -> 4 ← operation enabled
[slave] received modes of operation 1 ← pp 模式
[slave] received target position 80000
[slave] pos=12500 (demand 12500), vel=50000, state=4, sw=0x0237 ← 正在加速/匀速
[slave] pos=80000 (demand 80000), vel=0, state=4, sw=0x0637 ← 到位(bit10 置位)
[slave] pp: target 80000 reached
[slave] received target position -80000
[slave] pos=45000 (demand 45000), vel=-50000, state=4, sw=0x0237 ← 反向运动中
[slave] pos=0 (demand 0), vel=0, state=4, sw=0x0637 ← 回到 0
[slave] pp: target -80000 reached
主站(终端 2):
NMT 'enter operational' sent to node 1
slave 1 is operational
[master] SDO TX write 0x6060:0 = 01
[master] SDO RX ack write 0x6060:0
[master] SDO TX write 0x6040:0 = 06 00
[master] SDO RX ack write 0x6040:0
[master] SDO TX write 0x6040:0 = 07 00
[master] SDO RX ack write 0x6040:0
[master] SDO TX write 0x6040:0 = 0F 00
[master] SDO RX ack write 0x6040:0
[master] drive enabled, mode pp, starting moves
[master] SDO TX write 0x607A:0 = 80 38 01 00 ← +80000
[master] SDO TX read 0x6041:0
[master] SDO RX read 0x6041:0 = 37 02 ← 状态字变化
[master] moving to 80000
[master] target reached: statusword=0x0637 (operation enabled) target-reached
[master] SDO TX read 0x6064:0
[master] SDO RX read 0x6064:0 = 80 38 01 00
[master] position actual = 80000
[master] SDO TX write 0x607A:0 = 80 C7 FE FF ← -80000 (补码 0xFFFEC780)
[master] moving to -80000
[master] target reached: statusword=0x0637 (operation enabled) target-reached
[master] SDO TX read 0x6064:0
[master] position actual = 0
[master] all 1 moves completed
主站内部相位机:NMT启动 → 等心跳 → 设模式 → cw=06→07→0F → 写目标 → 轮询状态字(bit10) → 读回位置 → 反向 → 往复。
candump -td -a vcan0 # 全部报文(带时间戳)
candump vcan0,181:7FF,281:7FF,701:7FF # 只看从站 TPDO1/TPDO2/心跳
candump vcan0,601:7FF,581:7FF # 只看 SDO 请求/响应常见 COB-ID 速查(节点 1 = 从站,节点 2 = 主站):
| COB-ID | 含义 |
|---|---|
0x000 |
NMT 命令 |
0x081 |
从站 EMCY |
0x181 |
从站 TPDO1:状态字(u16)+位置(i32),每 100ms |
0x281 |
从站 TPDO2:模式显示(i8)+速度(i32),每 100ms |
0x201 |
主站 RPDO1→从站:控制字(u16)+目标位置(i32) |
0x581/0x601 |
从站 SDO 响应/请求 |
0x701 |
从站心跳(0x05=operational),每 500ms |
# 向节点 1 写 0x6040:0 = 0x000F(enable operation,SDO 快速下载)
cansend vcan0 601 2F 40 60 00 0F 00 00 00
# 向节点 1 写 0x607A:0 = 80000(目标位置,小端 0x13880 → 80 38 01 00)
cansend vcan0 601 23 7A 60 00 80 38 01 00./build/motor402_slave vcan0 -i 1 -c "stdio"
1 w 0x6060 0 u8 1 # pp 模式
1 w 0x6040 0 u16 0x06 # shutdown
1 w 0x6040 0 u16 0x07 # switch on
1 w 0x6040 0 u16 0x0F # enable operation
1 w 0x607A 0 i32 80000 # 运动到 80000
1 r 0x6064 0 i32 # 读实际位置
1 r 0x6041 0 u16 # 读状态字或本地 socket 接口配合 cocomm:
./build/motor402_slave vcan0 -i 1 -c "local-/tmp/CO_command_socket"
cd cocomm && make && ./cocomm "1 r 0x6064 0 i32"graph TD
A[Switch on disabled] -->|Shutdown 0x06| B[Ready to switch on]
B -->|Switch on 0x07| C[Switched on]
C -->|Enable operation 0x0F| D[Operation enabled]
D -->|bit2=0 快速停止| E[Quick stop active]
E -->|bit2=1 且 bit3=1| D
D -->|bit1=0 禁用电压| A
B -->|bit1=0 或 bit2=0| A
C -->|bit1=0 或 bit2=0| A
E -->|bit1=0| A
D -.->|故障 跟随误差超限| F[Fault reaction active]
F -->|100ms| G[Fault]
G -->|bit7 上升沿 故障复位| A
| bit | 名称 | 说明 |
|---|---|---|
| 0 | Switch on | 1 = 接通 |
| 1 | Enable voltage | 1 = 使能电压 |
| 2 | Quick stop | 1 = 允许运行(0 触发快速停止) |
| 3 | Enable operation | 1 = 使能运行 |
| 4-6 | 保留 | 置 0 |
| 7 | Fault reset | 0→1 上升沿复位故障 |
| 8 | Halt | 1 = 暂停(按 0x6084 减速停止) |
常用命令值:0x06 Shutdown、0x07 Switch on、0x0F Enable operation、0x80 Fault reset。
| bit | 名称 | 说明 |
|---|---|---|
| 0 | Ready to switch on | 就绪 |
| 1 | Switched on | 已接通 |
| 2 | Operation enabled | 运行使能 |
| 3 | Fault | 故障(需 bit7 复位) |
| 4 | Voltage enabled | 电压使能 |
| 5 | Quick stop | 快速停止激活 |
| 6 | Switch on disabled | 禁止接通 |
| 7 | Warning | 警告 |
| 9 | Remote | 远程控制(恒 1) |
| 10 | Target reached | 到位(窗口 0x6065 内持续 0x6067) |
| 12 | Set-point acknowledge | pp 模式目标已接收 |
典型值:0x0250 禁止接通 / 0x0221 就绪 / 0x0223 已接通 / 0x0237 运行使能 / 0x0208 故障。
| 索引 | 名称 | 类型 | 访问 | 默认值 |
|---|---|---|---|---|
| 0x603F | Error code | u16 | RO | 0 |
| 0x6040 | Controlword | u16 | RW | 0 |
| 0x6041 | Statusword | u16 | RO | 0x0250 |
| 0x605A | Quick stop option code | u16 | RW | 1 |
| 0x605B | Shutdown option code | u16 | RW | 0 |
| 0x605C | Disable operation option code | u16 | RW | 1 |
| 0x605D | Halt option code | u16 | RW | 1 |
| 0x605E | Fault reaction option code | u16 | RW | 0 |
| 0x6060 | Modes of operation(1=pp, 3=pv) | i8 | RW | 0 |
| 0x6061 | Modes of operation display | i8 | RO | 0 |
| 0x6062 | Position demand value | i32 | RO | 0 |
| 0x6063 | Position actual (internal) | i32 | RO | 0 |
| 0x6064 | Position actual value | i32 | RO | 0 |
| 0x6065 | Position window(到位判据) | u32 | RW | 1 |
| 0x6067 | Position window time | u16 | RW | 50 ms |
| 0x606B | Velocity demand value | i32 | RO | 0 |
| 0x606C | Velocity actual value | i32 | RO | 0 |
| 0x607A | Target position | i32 | RW | 0 |
| 0x6080 | Max motor speed | u32 | RW | 100000 |
| 0x6081 | Profile velocity | u32 | RW | 50000 |
| 0x6083 | Profile acceleration | u32 | RW | 100000 |
| 0x6084 | Profile deceleration | u32 | RW | 100000 |
| 0x6085 | Quick stop deceleration | u32 | RW | 200000 |
| 0x6086 | Motion profile type(0=线性) | i16 | RW | 0 |
| 0x6098 | Homing method(未实现,置 0) | i8 | RW | 0 |
| 0x6099 | Homing speeds(sub1/2) | rec | RW | - |
| 0x609A | Homing acceleration | u32 | RW | 10000 |
| 0x60F4 | Following error actual | i32 | RO | 0 |
| 0x60FF | Target velocity | i32 | RW | 0 |
| 0x6502 | Supported drive modes(pp+pv) | u32 | RO | 0x03 |
| 0x2000 | Encoder increments/rev(仿真) | u32 | RW | 10000 |
| 0x2001 | Motor inertia(仿真,tau=值×1ms) | u32 | RW | 100 |
| 0x2002 | Friction(仿真) | u32 | RW | 0 |
| 0x2003 | Rated torque(仿真) | u32 | RW | 100 mNm |
| 0x2004 | Load torque(仿真) | i32 | RW | 0 |
| 0x2005 | Max velocity(仿真限速) | u32 | RW | 100000 |
| 0x2006 | Following error limit(超限→故障) | u32 | RW | 20000 |
单位约定:位置 = increments,速度 = inc/s,加速度 = inc/s²。
| PDO | COB-ID | 方向 | 映射 | 触发 |
|---|---|---|---|---|
| RPDO1 | 0x200+从站号(0x201) | 主站→从站 | 0x6040:0:16 + 0x607A:0:32(6 字节) | 异步(type 0xFE) |
| TPDO1 | 0x180+从站号(0x181) | 从站→主站 | 0x6041:0:16 + 0x6064:0:32(6 字节) | 事件定时 100ms |
| TPDO2 | 0x280+从站号(0x281) | 从站→主站 | 0x6061:0:8 + 0x606C:0:32(5 字节) | 事件定时 100ms |
SDO 写 0x6040:0 = 0x000F(enable operation):
601 2F 40 60 00 0F 00 00 00
│ │ │ │ │ └─────── 数据区 8 字节,有效 1 字节:0x0F
│ │ │ │ └────────── subindex = 0x00
│ │ │ └───────────── index = 0x6040(小端 40 60)
│ │ └──────────────── 命令 0x2F = 快速下载(1 字节数据)
└──── 请求 COB-ID = 0x600 + 节点号 1
581 60 40 60 00 00 00 00 00
│ └ 0x60 = 下载响应(成功)
└ 响应 COB-ID = 0x580 + 节点号 1
其他典型帧:
| 帧 | 含义 |
|---|---|
000 01 01 |
NMT:节点 1 进入 operational |
701 05 |
节点 1 心跳,operational |
181 37 02 80 38 01 00 |
TPDO1:状态字 0x0237 + 位置 80000 |
081 00 FF 01 2F 04 00 00 00 |
节点 1 EMCY(0xFF00 设备特定,错误位 0x2F=非易失存储,info=0x04) |
Q1:Error: CAN device 'vcan0' not found
A:vcan0 不存在。先执行第 2.2 节创建接口(重启后需重建)。
Q2:主站 ERROR: heartbeat of slave 1 not seen
A:依次检查:
- 从站是否在运行且带
-i 1(不带则节点未配置,不发心跳); candump vcan0,701:7FF是否能看到701 [1] 05(05=operational);- 主从是否在同一网络命名空间(
unshare/容器场景)。
Q3:SDO error 0x0000
A:SDO 请求无响应。确认从站在运行;用 candump 观察 0x601/0x581 是否有往返。
Q4:位置只增不减 / 往复数值不对
A:旧版本 profilePositionStep 有速度方向 bug(已修复)。重新 cmake --build build。
Q5:启动时 EMCY errorCode=0x5000, errorBit=0x2F
A:这是"非易失存储文件缺失"告警。demo 从站已禁用存储,正常不会出现;若出现说明构建时未带 CO_CONFIG_STORAGE=0。
Q6:想恢复从站参数持久化
A:去掉 CMakeLists.txt 中 motor402_slave 目标的 CO_CONFIG_STORAGE=0 重新构建即可(注意首次运行会产生 EMCY 且 NMT 可能降级为 pre-operational)。
Q7:两个 demo 的 CiA 402 对象字典在哪?
A:demos/motor402_slave/OD.c / OD.h(DS301 + 402 + 厂商仿真参数,手写,格式同 CANopenEditor V4 生成文件)。
CANopenLinux/
├── CMakeLists.txt # C11/C++14,Unix Makefiles 生成器
├── CO_driver.c ... # Linux SocketCAN 驱动
├── CO_main_basic.c # 通用主程序(canopend 与从站共用)
├── CANopenNode/ # CANopenNode 栈(submodule)
├── cocomm/ # CiA309 命令行工具
└── demos/
├── README.md # 本文档
├── motor402_slave/ # 402 从站:OD.c/h、motor_app.c/h、README
└── motor402_master/ # 402 主站:main.c、README
参考实现(开源电机项目):
- CANopenNode CANopenDemo
MotorDemo:https://github.com/CANopenNode/CANopenDemo - ODrive 运动控制器:https://github.com/odriverobotics/ODrive
- CiA 402 标准(IEC 61800-7-201):https://www.can-cia.org/can-knowledge/canopen/cia402/