第2章:CANopenNode框架入门:源码结构解析、核心模块说明、移植到STM32平台
好,咱们接着聊。上一章我们把CANopen协议的基本概念捋了一遍,这一章直接上手干活——把CANopenNode这个开源框架拆开看看,再把它移植到STM32上。
说实话,我第一次看CANopenNode源码的时候,也有点懵。代码量不小,结构也挺复杂。但别怕,我带你一步步拆解。你会发现,其实核心就那么几个模块。
2.1 源码结构总览
CANopenNode的源码,说白了就是一套C语言写的CANopen协议栈。它支持CiA 301标准,也兼容CiA 402(电机驱动)和CiA 401(I/O设备)。
我习惯把它的目录结构分成三块:
- 核心协议栈:在
stack/目录下,这是灵魂所在 - 驱动层:在
drivers/目录下,负责跟硬件打交道 - 示例应用:在
example/目录下,给你抄作业用的
你下载源码后,第一眼看到的可能是这样的结构:
CANopenNode/
├── stack/ # 核心协议栈
│ ├── CO_driver.h # 驱动抽象层接口
│ ├── CO_Emergency.c # 紧急报文处理
│ ├── CO_NMT_Heartbeat.c # NMT与心跳
│ ├── CO_PDO.c # PDO处理
│ ├── CO_SDO.c # SDO处理
│ ├── CO_SYNC.c # 同步报文
│ ├── CO_TIME.c # 时间戳
│ └── CO_LSS.c # 层设置服务(可选)
├── drivers/ # 硬件驱动
│ ├── CAN/ # 各平台CAN驱动
│ │ ├── STM32/ # STM32专用
│ │ ├── Linux/ # Linux SocketCAN
│ │ └── ...
│ └── timer/ # 定时器驱动
├── example/ # 示例工程
│ ├── STM32/ # STM32示例
│ └── Linux/ # Linux示例
└── doc/ # 文档
嗯,这里要注意:stack/里的代码是平台无关的,你移植的时候基本不用改。真正要动的是drivers/里的东西。
2.2 核心模块说明
我挑几个最重要的模块讲讲。这些模块,你在做电机控制时几乎天天都要打交道。
2.2.1 NMT与心跳模块
NMT(Network Management)是CANopen的“交警”。它负责节点状态管理:初始化、预操作、操作、停止。
我记得第一次调试时,节点死活进不了操作状态。查了半天,原来是心跳报文没配置对。主站收不到心跳,以为节点挂了,一直发NMT复位命令。
核心函数就这几个:
// NMT状态切换
void CO_NMT_process(CO_NMT_t *nmt, uint32_t timeDifference_us);
// 心跳发送
void CO_Heartbeat_process(CO_Heartbeat_t *hb, uint32_t timeDifference_us);
// 检查节点是否在线
bool CO_NMT_isOperational(CO_NMT_t *nmt);
2.2.2 SDO模块
SDO(Service Data Object)用于读写对象字典。说白了,就是配置参数用的。
SDO是点对点通信,有确认机制。你发一个请求,对方必须回复。所以它可靠,但慢。
我在项目中遇到过一个问题:用SDO批量下载电机参数时,如果连续发太快,从站会丢包。后来加了重传机制才搞定。
关键API:
// SDO服务器处理(从站用)
void CO_SDO_server_process(CO_SDO_t *sdo, uint32_t timeDifference_us);
// SDO客户端发起读请求(主站用)
int16_t CO_SDO_client_upload(CO_SDO_t *sdo, uint16_t index, uint8_t subIndex,
uint8_t *data, uint32_t *dataSize, uint32_t timeout_ms);
// SDO客户端发起写请求
int16_t CO_SDO_client_download(CO_SDO_t *sdo, uint16_t index, uint8_t subIndex,
uint8_t *data, uint32_t dataSize, uint32_t timeout_ms);
2.2.3 PDO模块
PDO(Process Data Object)是CANopen的“快车道”。它没有确认机制,发完就完事,但速度快。
电机控制中,PDO用来实时传输:
- 目标位置/速度/转矩
- 实际位置/速度/电流
- 状态字/控制字
PDO有两种触发方式:
- 事件触发:数据变化了就发。适合位置反馈。
- 同步触发:收到SYNC报文后统一发。适合多轴同步。
你想想看,做多轴联动时,同步PDO几乎是唯一选择。我做过一个四轴机械臂,就是用同步PDO,周期1ms,效果很好。
// PDO处理
void CO_PDO_process(CO_PDO_t *pdo, uint32_t timeDifference_us);
// 发送TPDO(生产者)
void CO_PDO_sendTPDO(CO_PDO_t *pdo, uint8_t tpdoNumber);
// 接收RPDO(消费者)
void CO_PDO_receiveRPDO(CO_PDO_t *pdo, uint8_t rpdoNumber, uint8_t *data);
2.2.4 紧急报文模块
紧急报文(Emergency)是CANopen的“报警器”。当节点检测到错误时,立即发送。
电机控制中常见的紧急错误:
- 过流
- 过温
- 编码器故障
- 通信超时
我曾经调试一台伺服驱动器,电机一启动就报紧急错误。查了半天,发现是电流采样偏置没校准。嗯,从那以后我每次上电都会先做一次自校准。
// 发送紧急报文
void CO_Emergency_send(CO_Emergency_t *emergency, uint16_t errorCode,
uint8_t errorRegister, uint8_t *data, uint8_t dataLength);
2.3 移植到STM32平台
好了,理论说完了,咱们动手移植。我以STM32F407为例,HAL库环境。
移植说白了就三步:
- 配置CAN外设:波特率、过滤器、中断
- 实现驱动接口:CAN发送、CAN接收、定时器
- 集成协议栈:初始化、主循环调用
2.3.1 第一步:配置CAN外设
CAN波特率我一般设500kbps。为什么?因为这是工业现场最常用的速率,兼容性好。
// CAN初始化(HAL库)
void CAN_Init(void)
{
hcan.Instance = CAN1;
hcan.Init.Prescaler = 6; // 42MHz / 6 = 7MHz
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_13TQ; // 13 TQ
hcan.Init.TimeSeg2 = CAN_BS2_2TQ; // 2 TQ
// 总TQ = 1 + 13 + 2 = 16
// 波特率 = 7MHz / 16 = 437.5kbps ≈ 500kbps
HAL_CAN_Init(&hcan);
// 配置过滤器:接收所有CANopen报文(11位ID)
CAN_FilterTypeDef filter;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = 0x0000;
filter.FilterIdLow = 0x0000;
filter.FilterMaskIdHigh = 0x0000;
filter.FilterMaskIdLow = 0x0000;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &filter);
// 使能中断
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_CAN_Start(&hcan);
}
2.3.2 第二步:实现驱动接口
CANopenNode的驱动层定义了一组回调函数。你需要实现它们:
// CAN发送回调
void CO_CANsend(CO_CANmodule_t *CANmodule, CO_CANtx_t *buffer)
{
CAN_TxHeaderTypeDef header;
uint8_t data[8];
header.StdId = buffer->ident;
header.IDE = CAN_ID_STD;
header.RTR = CAN_RTR_DATA;
header.DLC = buffer->DLC;
memcpy(data, buffer->data, 8);
HAL_CAN_AddTxMessage(&hcan, &header, data, &txMailbox);
}
// CAN接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef header;
uint8_t data[8];
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, data);
// 调用协议栈接收处理
CO_CANinterrupt(&canModule, header.StdId, data, header.DLC);
}
// 定时器回调(1ms周期)
void TIM_IRQHandler(void)
{
// 调用协议栈主循环处理
CO_process(&canOpen, 1000); // 1000微秒
}
2.3.3 第三步:集成协议栈
在主循环中,你需要周期调用协议栈的处理函数:
int main(void)
{
HAL_Init();
SystemClock_Config();
CAN_Init();
Timer_Init(); // 1ms定时器
// 初始化CANopen协议栈
CO_ReturnError_t err;
err = CO_init(&canOpen, &canModule, &timerModule,
nodeId, &OD, &SDO, &NMT, &Heartbeat,
&Emergency, &PDO, &SYNC, &TIME);
if (err != CO_ERROR_NO) {
// 初始化失败,处理错误
Error_Handler();
}
while (1)
{
// 主循环中也可以调用,但建议用定时器中断驱动
// CO_process(&canOpen, 0);
// 你的应用代码
Motor_Control_Loop();
}
}
CO_process()最好放在定时器中断里,周期1ms。主循环只跑应用逻辑。这样时序更可控。
2.4 避坑指南
移植过程中,我踩过不少坑。分享几个典型的:
- 对象字典配置:CANopenNode的对象字典是用一个巨大的结构体数组定义的。修改时一定要同步更新
CO_OD.h和CO_OD.c。我见过有人只改了.h忘了改.c,编译不报错,运行就死机。 - 心跳超时:主站和从站的心跳周期要匹配。从站设了100ms,主站超时时间就要大于100ms,一般设150ms。太短了容易误报。
- PDO映射:PDO映射必须在预操作状态下配置。进入操作状态后就不能改了。这个顺序搞错,节点会报错。
- 中断优先级:CAN接收中断优先级要高于定时器中断。否则数据可能丢失。我吃过这个亏。
好了,这一章的内容就到这。下一章我们开始实战——在STM32上跑一个完整的CANopen从站,实现电机的基本控制。到时候你会看到,这些模块是怎么协同工作的。