通信协议之CANopen移植CanFestival
CANopen 集成指南
概述
本文档描述如何将 CanFestival CANopen 协议栈集成到 STM32F103RC 项目中,包括源代码适配步骤和使用方法。
第一部分:源代码适配步骤
步骤 1:准备 CanFestival 源码
从 CanFestival 官方获取源码,复制到项目的 User/CANopen 目录:
1 2 3 4 5 6 7
| CANopen/ ├── src/ # CanFestival源代码 ├── include/ # 头文件(需清理) ├── canopen_stm32.c # STM32硬件抽象层(已实现) ├── canopen_stm32.h ├── ObjDict.c # 对象字典(完整版) └── ObjDict.h
|
步骤 2:清理不必要的文件
删除以下文件(非 STM32 平台相关):
src/win32
src/Makefile.in
src/symbols.c
src/canfestival.pc.in
步骤 3:清理 include 目录
保留文件夹:
stm32/(改名自 AVR 或新建)
unix/
- 其他 C 源文件
删除文件夹:
AVR/(已改为 stm32)
CM0/, CM3/(MCU 特定文件)
步骤 4:配置 stm32/config.h
编辑 include/stm32/config.h,注释 AVR 相关头文件:
1 2 3 4 5 6 7 8 9 10 11
|
#else
#endif
|
步骤 5:移除 inline 关键字
编辑 src/dcf.c,删除第 59、98 行前的 inline 关键字:
第二部分:平台适配实现
核心概念
CANopen 库需要三个必须实现的平台相关函数:
| 函数 |
作用 |
实现位置 |
canSend() |
发送 CAN 消息 |
canopen_stm32.c |
getElapsedTime() |
获取经过时间(ms) |
canopen_stm32.c |
setTimer() |
设置定时器周期 |
canopen_stm32.c |
适配工作
1. 硬件抽象层(canopen_stm32.c/h)
定时器函数:
1 2 3 4 5 6 7 8 9 10
| void CANopen_TimerInit(void);
unsigned long getElapsedTime(void);
void setTimer(TIMEVAL value);
|
CAN 函数:
1 2 3 4 5 6 7
|
UNS8 canSend(Message *m);
void CANopen_ProcessRxMessage(CAN_RxHeaderTypeDef *header, uint8_t *data);
|
2. 对象字典(ObjDict.c/h)
节点配置:
1 2 3 4 5 6 7 8
| #define NODE_ID 0x01
CO_Data ObjDict_Data; UNS8 Node_ID = NODE_ID; static UNS8 iam_slave = 1;
void ObjDict_Init(void);
|
回调函数:
1 2 3 4 5 6 7 8 9 10 11
| void _initialisation(CO_Data* d) { ... }
void _preOperational(CO_Data* d) { ... }
void _operational(CO_Data* d) { ... }
void _stopped(CO_Data* d) { ... }
|
3. 中断回调函数
在 Core/Src/main.c 中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM7) { TimeDispatch(); } }
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef canRxHeader; uint8_t canRxData[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &canRxHeader, canRxData) == HAL_OK) { CANopen_ProcessRxMessage(&canRxHeader, canRxData); } }
|
第三部分:使用方法
1. 初始化步骤
在 main() 函数的 USER CODE BEGIN 2 添加初始化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
CAN_FilterTypeDef canFilter = {0}; canFilter.FilterBank = 0; canFilter.FilterMode = CAN_FILTERMODE_IDMASK; canFilter.FilterScale = CAN_FILTERSCALE_32BIT; canFilter.FilterIdHigh = 0x0000; canFilter.FilterIdLow = 0x0000; canFilter.FilterMaskIdHigh = 0x0000; canFilter.FilterMaskIdLow = 0x0000; canFilter.FilterFIFOAssignment = CAN_RX_FIFO0; canFilter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &canFilter); HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_TIM_Base_Start_IT(&htim7);
ObjDict_Init(); CANopen_TimerInit(); setNodeId(&ObjDict_Data, NODE_ID); setState(&ObjDict_Data, Pre_operational);
printf("CANopen initialized, Node ID: %d\r\n", NODE_ID);
|
2. 基本操作
状态转换:
1 2 3 4 5 6 7 8 9 10 11
| setState(&ObjDict_Data, Operational);
e_nodeState currentState = getState(&ObjDict_Data);
setState(&ObjDict_Data, Stopped);
setState(&ObjDict_Data, Pre_operational);
|
节点 ID 设置:
1 2
| setNodeId(&ObjDict_Data, 0x02);
|
3. PDO 通信(需完整对象字典)
1 2 3 4 5
| sendPDOevent(&ObjDict_Data);
PDOEnable(&ObjDict_Data, pdoNum);
|
4. SDO 通信(需完整对象字典)
5. NMT 主站操作(需主站配置)
1 2 3 4 5 6 7 8 9 10 11
| masterSendNMTstateChange(&ObjDict_Data, slaveNodeId, NMT_Start_Node);
masterSendNMTstateChange(&ObjDict_Data, slaveNodeId, NMT_Stop_Node);
masterSendNMTstateChange(&ObjDict_Data, slaveNodeId, NMT_Reset_Node);
masterRequestNodeState(&ObjDict_Data, slaveNodeId);
|
第四部分:配置参数
硬件配置(CubeMX)
CAN 配置:
- 波特率: 500 kbps
- 时间段 1: 8 TQ
- 时间段 2: 3 TQ
- 分频系数: 6
- 接收中断: CAN_IT_RX_FIFO0_MSG_PENDING
TIM7 配置:
- 分频系数: 71(使得 1MHz 时钟)
- 重装载值: 999(产生 1ms 中断)
- 中断: 启用
软件配置(config.h)
| 参数 |
值 |
说明 |
NMT_MAX_NODE_ID |
8 |
节点数量(节省内存) |
SDO_MAX_LENGTH_TRANSFER |
32 |
SDO 最大传输长度 |
MAX_NB_TIMER |
8 |
软件定时器数量 |
EMCY_MAX_ERRORS |
8 |
紧急错误队列大小 |
对象字典配置(ObjDict.h)
第五部分:故障排除
程序崩溃(HardFault)
原因: 全局对象字典结构过大,超过可用 RAM
解决: 在 config.h 中减小以下参数:
1 2 3
| #define NMT_MAX_NODE_ID 8 #define SDO_MAX_LENGTH_TRANSFER 32 #define MAX_NB_TIMER 8
|
CAN 消息收不到
检查列表:
状态切换失败
可能原因:
- 对象字典未初始化 → 调用
ObjDict_Init()
- 节点 ID 超出范围(1-127) → 检查 NODE_ID
- 状态转换非法 → 检查 CANopen 状态机规则
第六部分:下一步工作
1. 生成完整对象字典
当前 ObjDict.c/h 是最小化实现,实际应用需要:
1 2
| objdictgen --export-c application.eds ObjDict.c ObjDict.h
|
生成的文件包含:
- PDO 映射表
- SDO 配置
- 专用对象(0x1000-0x9FFF)
2. 实现应用回调
在 ObjDict.c 中实现具体应用逻辑:
1 2 3 4 5
| void _operational(CO_Data* d) { sendPDOevent(d); }
|
3. 配置 PDO 映射
在对象字典中定义:
- RPDO:接收处理数据对象
- TPDO:传输处理数据对象
- 映射应用变量到 PDO
4. 测试与调试
使用 CANopen 工具:
- CANopen Magic: 可视化网络调试
- CANalyzer: CAN 总线分析
- Node Configuration Tool: 节点参数配置
参考资源
项目文件结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| User/CANopen/ ├── src/ # CanFestival源代码 │ ├── dcf.c │ ├── emcy.c │ ├── lifeguard.c │ ├── lss.c │ ├── nmt.c │ ├── objacces.c │ ├── pdo.c │ ├── sdo.c │ ├── states.c │ ├── sync.c │ └── timer.c ├── include/ │ ├── stm32/ │ │ ├── config.h # STM32特定配置 │ │ └── sysdep.h │ ├── can.h │ ├── data.h │ ├── def.h │ ├── dcf.h │ ├── emcy.h │ ├── lifeguard.h │ ├── lss.h │ ├── nmt.h │ ├── objacces.h │ ├── objdictdef.h │ ├── pdo.h │ ├── sdo.h │ ├── states.h │ ├── sync.h │ ├── timer.h │ ├── canfestival.h │ └── ... ├── canopen_stm32.c # STM32硬件抽象层(已实现) ├── canopen_stm32.h ├── ObjDict.c # 对象字典实现 ├── ObjDict.h └── can.md # 本文档
|