15. 从手写报文到 CANopenNode
前面几课里,我们亲手完成过这些工作:
- 用 TWAI 接收 CAN 帧;
- 根据 Node-ID 计算
0x701、0x601和0x581; - 拼出 SDO 读取请求;
- 检查响应中的 Index、Sub-index、命令字和 Abort Code;
- 处理超时,避免程序一直等下去。
这些代码非常值得写一遍。只有亲手拆过报文,后面看到协议栈函数时,才知道它替我们做了什么。
但如果继续加入分段 SDO、PDO、NMT 状态机和多个节点,应用程序会越来越像一份协议实现。此时需要把“执行 CANopen 规则”和“决定设备做什么”分开。
CANopenNode 是什么
CANopenNode 是一个开源 CANopen 协议栈。协议栈可以理解为一组已经实现 CANopen 规则的 C 源码。
它不会替我们决定电机要转多少,也不知道设备应该在什么时候使能。它负责的是:
- 按规则收发 NMT、Heartbeat、SDO 和 PDO;
- 维护协议通信状态;
- 检查超时和 Abort;
- 把对象字典中的变量与 CANopen 服务连接起来。
应用程序仍然负责业务决定。

从上到下看这张图:
| 部分 | 本项目中的实例 | 它负责什么 |
|---|---|---|
| 应用代码 | app_main.c、电机命令处理 | 决定读哪个对象、何时执行操作 |
| CANopen 协议栈 | CANopenNode | 实现 CANopen 通信规则 |
| CAN 控制器驱动 | ESP-IDF TWAI | 收发 Classical CAN 帧 |
| 物理层 | TJA1050、CANH、CANL | 在电线上传输差分信号 |
CANopenNode 和 TWAI 不是两套 CAN 控制器。CANopenNode 最终仍通过 ESP32-C3 内部的同一个 TWAI 外设发送和接收。
用一次 SDO 读取看分工
读取电机 0x6064:00 时,应用代码只想表达一件事:
从 Node
0x01读取对象0x6064:00。
使用手写客户端时,我们还要亲自完成:
- 计算请求 CAN-ID
0x601; - 构造
40 64 60 00 00 00 00 00; - 等待
0x581; - 核对响应对象地址;
- 识别成功命令字或
0x80Abort; - 处理小端序和超时。
使用 CANopenNode 后,应用代码调用 SDO Client API,协议栈负责上述通信过程。CAN 总线上真实出现的仍是同样的 0x601 请求和 0x581 响应。
这点很重要:协议栈没有改变 CANopen 协议,只是把已经学过的规则集中实现了。
Espressif 官方组件做了什么
本课程使用 Espressif 组件注册表中的:
| 项目 | 固定值 |
|---|---|
| 组件 | espressif/canopennode |
| 版本 | 0.1.0 |
| ESP-IDF | v5.5.4 |
| 芯片 | ESP32-C3 |
这个组件包含 CANopenNode 源码和 ESP32 TWAI 适配层。工程通过 idf_component.yml 声明依赖,idf.py build 会解析并下载它。
官方资料:
不要同时初始化两套 TWAI
加入协议栈后,常见错误是保留原来的 twai_new_node_onchip() 初始化,又让另一份代码再初始化一次控制器。
本课程采用的关系是:
- 应用创建一个 TWAI 节点句柄;
- 将这个句柄交给
CO_CANinit(); - CANopenNode 通过该句柄使用 TWAI;
- 所有 CANopen 服务共享这一条接收和发送通道。
所以后续看到 Heartbeat、SDO 和 PDO 同时工作,不代表创建了三套驱动。
这次为什么还不访问电机
切换协议栈后的第一个实验,只让 ESP32 自己成为 CANopen Node 0x20,并发送:
| 报文 | CAN-ID | DATA |
|---|---|---|
| Boot-up | 0x720 | 00 |
| Heartbeat | 0x720 | 当前 NMT 状态 |
这样可以先证明:组件依赖、TWAI 适配、Node-ID、对象字典和主循环都能启动。若第一步就混入电机读取,失败时很难判断问题来自本地节点还是远程设备。
本课回顾
手写报文适合学习协议和制作受控调试工具;CANopenNode 适合继续构建完整节点。后续应用代码负责“想做什么”,协议栈负责“按 CANopen 规则怎样完成”。
下一课从标准 ESP-IDF 工程开始,创建第一个 CANopenNode Heartbeat 节点。