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

/* 注释掉AVR头文件 */
#else // GCC
// #include <inttypes.h>
// #include <avr/io.h>
// #include <avr/interrupt.h>
// #include <avr/pgmspace.h>
// #include <avr/sleep.h>
// #include <avr/wdt.h>
#endif // GCC

步骤 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
// 初始化定时器(在main中调用)
void CANopen_TimerInit(void);

// 获取距上次setTimer()的经过时间(ms)
// CANopen库通过此值修正软件定时器
unsigned long getElapsedTime(void);

// 设置定时器中断周期
// CANopen库动态调用此函数改变中断周期
void setTimer(TIMEVAL value);

CAN 函数:

1
2
3
4
5
6
7
// 发送CAN消息(CANopen库的所有发送都调用此函数)
// 将CANopen Message结构转换为HAL CAN_TxHeaderTypeDef格式
UNS8 canSend(Message *m);

// 处理接收的CAN消息
// 在CAN中断中调用,将HAL格式转换为Message后调用canDispatch()
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  // 修改此值设置节点ID(1-127)

CO_Data ObjDict_Data; // CANopen数据结构(库管理)
UNS8 Node_ID = NODE_ID; // 节点ID变量
static UNS8 iam_slave = 1; // 从站标志

// 对象字典初始化(在main中调用)
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
// TIM7定时器中断(每1ms触发)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM7) {
TimeDispatch(); // CANopen内部定时器调度
}
}

// CAN接收中断(收到CAN消息时触发)
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
/* USER CODE BEGIN 2 */

// CAN初始化(由CubeMX自动调用 MX_CAN_Init())
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);

// TIM7初始化(由CubeMX自动调用 MX_TIM7_Init())
HAL_TIM_Base_Start_IT(&htim7);

// CANopen初始化
ObjDict_Init(); // 初始化对象字典
CANopen_TimerInit(); // 初始化定时器
setNodeId(&ObjDict_Data, NODE_ID); // 设置节点ID
setState(&ObjDict_Data, Pre_operational); // 进入预运行状态

printf("CANopen initialized, Node ID: %d\r\n", NODE_ID);

/* USER CODE END 2 */

2. 基本操作

状态转换:

1
2
3
4
5
6
7
8
9
10
11
// 切换到运行状态(支持PDO通信)
setState(&ObjDict_Data, Operational);

// 获取当前状态
e_nodeState currentState = getState(&ObjDict_Data);

// 切换到停止状态
setState(&ObjDict_Data, Stopped);

// 切换到预运行状态
setState(&ObjDict_Data, Pre_operational);

节点 ID 设置:

1
2
// 修改节点ID(需要在Pre_operational状态执行)
setNodeId(&ObjDict_Data, 0x02);

3. PDO 通信(需完整对象字典)

1
2
3
4
5
// 发送PDO事件
sendPDOevent(&ObjDict_Data);

// 启用/禁用PDO
PDOEnable(&ObjDict_Data, pdoNum);

4. SDO 通信(需完整对象字典)

1
2
// 同步SDO读取(从站)
// 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)

1
#define NODE_ID 0x01  // 节点ID(1-127)

第五部分:故障排除

程序崩溃(HardFault)

原因: 全局对象字典结构过大,超过可用 RAM

解决:config.h 中减小以下参数:

1
2
3
#define NMT_MAX_NODE_ID 8           // 默认128,改为8
#define SDO_MAX_LENGTH_TRANSFER 32 // 默认256,改为32
#define MAX_NB_TIMER 8 // 默认16,改为8

CAN 消息收不到

检查列表:

  • CAN 过滤器是否正确配置
  • CAN 接收中断是否启用
  • HAL_CAN_ActivateNotification() 是否调用
  • 回调函数是否实现
  • CANopen 消息格式是否正确

状态切换失败

可能原因:

  • 对象字典未初始化 → 调用 ObjDict_Init()
  • 节点 ID 超出范围(1-127) → 检查 NODE_ID
  • 状态转换非法 → 检查 CANopen 状态机规则

第六部分:下一步工作

1. 生成完整对象字典

当前 ObjDict.c/h 是最小化实现,实际应用需要:

1
2
# 使用objdictgen工具
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) {
// 例:启动传感器采集
// 配置PDO以定期传输数据
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 # 本文档