跳到主要内容

15. 从手写报文到 CANopenNode

学习形式架构课
硬件要求不需要
配套 Demo
本课动作不写电机对象

前面几课里,我们亲手完成过这些工作:

  • 用 TWAI 接收 CAN 帧;
  • 根据 Node-ID 计算 0x7010x6010x581
  • 拼出 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

使用手写客户端时,我们还要亲自完成:

  1. 计算请求 CAN-ID 0x601
  2. 构造 40 64 60 00 00 00 00 00
  3. 等待 0x581
  4. 核对响应对象地址;
  5. 识别成功命令字或 0x80 Abort;
  6. 处理小端序和超时。

使用 CANopenNode 后,应用代码调用 SDO Client API,协议栈负责上述通信过程。CAN 总线上真实出现的仍是同样的 0x601 请求和 0x581 响应。

这点很重要:协议栈没有改变 CANopen 协议,只是把已经学过的规则集中实现了。

Espressif 官方组件做了什么

本课程使用 Espressif 组件注册表中的:

项目固定值
组件espressif/canopennode
版本0.1.0
ESP-IDFv5.5.4
芯片ESP32-C3

这个组件包含 CANopenNode 源码和 ESP32 TWAI 适配层。工程通过 idf_component.yml 声明依赖,idf.py build 会解析并下载它。

官方资料:

不要同时初始化两套 TWAI

加入协议栈后,常见错误是保留原来的 twai_new_node_onchip() 初始化,又让另一份代码再初始化一次控制器。

本课程采用的关系是:

  1. 应用创建一个 TWAI 节点句柄;
  2. 将这个句柄交给 CO_CANinit()
  3. CANopenNode 通过该句柄使用 TWAI;
  4. 所有 CANopen 服务共享这一条接收和发送通道。

所以后续看到 Heartbeat、SDO 和 PDO 同时工作,不代表创建了三套驱动。

这次为什么还不访问电机

切换协议栈后的第一个实验,只让 ESP32 自己成为 CANopen Node 0x20,并发送:

报文CAN-IDDATA
Boot-up0x72000
Heartbeat0x720当前 NMT 状态

这样可以先证明:组件依赖、TWAI 适配、Node-ID、对象字典和主循环都能启动。若第一步就混入电机读取,失败时很难判断问题来自本地节点还是远程设备。

本课回顾

手写报文适合学习协议和制作受控调试工具;CANopenNode 适合继续构建完整节点。后续应用代码负责“想做什么”,协议栈负责“按 CANopen 规则怎样完成”。

下一课从标准 ESP-IDF 工程开始,创建第一个 CANopenNode Heartbeat 节点。