STM32F103裸机移植CanFestival-3全记录:从源码下载到心跳包测试(附对象字典生成工具避坑)
STM32F103裸机移植CanFestival-3全记录:从源码下载到心跳包测试(附对象字典生成工具避坑)
在工业自动化领域,CANopen协议因其高可靠性和灵活性成为设备间通信的首选方案之一。对于嵌入式开发者而言,如何在资源受限的STM32F103平台上实现CANopen从站功能,是一个既具挑战性又充满实践价值的课题。本文将详细记录一个完整的移植过程,从源码获取到最终通信测试,特别针对裸机环境下的特殊问题进行深入探讨。
1. 环境准备与源码获取
1.1 开发环境配置
在开始移植前,需要准备以下基础环境:
- 硬件平台:STM32F103C8T6最小系统板(俗称"蓝板")
- 开发工具:Keil MDK-ARM 5.32
- 调试工具:ST-Link V2仿真器
- CAN分析仪:PCAN-USB或ZLG的CAN盒
提示:虽然STM32F103系列内部时钟精度足够CAN通信使用,但建议外接8MHz晶振以获得更稳定的时钟源。
1.2 CanFestival源码获取与结构分析
CanFestival官方源码托管在Mercurial仓库,获取方式如下:
hg clone https://hg.beremiz.org/CanFestival-3源码目录结构关键部分说明:
CanFestival-3/ ├── drivers/ # 各平台驱动实现 ├── examples/ # 示例代码 ├── include/ # 公共头文件 ├── objdictgen/ # 对象字典生成工具 └── src/ # 核心协议栈源码对于裸机移植,我们需要重点关注src/目录下的核心文件和include/中的头文件。特别需要注意的是,CanFestival默认设计为支持多平台,因此需要针对STM32进行特定配置。
2. 工程搭建与基础移植
2.1 工程目录结构设计
合理的目录结构能显著提升项目管理效率,建议采用如下布局:
Project/ ├── CMSIS/ # ST官方库文件 ├── Drivers/ # 外设驱动 ├── CanFestival/ │ ├── inc/ # 头文件 │ ├── src/ # 源码文件 │ └── driver/ # 平台特定驱动 └── User/ # 用户代码2.2 关键文件移植与修改
从CanFestival源码中需要移植的文件包括:
核心协议栈文件(复制到
CanFestival/src/):dcf.c、emcy.c、lifegrd.c、nmtSlave.c、objacces.c、pdo.c、sdo.c、states.c、sync.c、timer.c
头文件(复制到
CanFestival/inc/):canfestival.h、config.h、timerscfg.h等
在移植过程中,裸机环境下常见的两个编译错误及解决方法:
内联函数报错: 在
dcf.c中找到以下两个函数,添加static修饰符:static inline UNS8 _getSubIndex(UNS16 index, UNS8 subindex) {...} static inline void _setSubIndex(UNS16 index, UNS8 subindex, UNS8 value) {...}定时器相关宏冲突: 修改
timerscfg.h中的宏定义:#define TIMER_HANDLE int #define TIMER_NONE -1
2.3 基础驱动实现
在driver/stm32_canfestival.c中需要实现三个关键函数:
// 定时器设置函数 void setTimer(TIMEVAL value) { NextTime = (TimeCNT + value) % TIMER_MAX_COUNT; } // 获取已过去的时间 TIMEVAL getElapsedTime(void) { int ret = TimeCNT > last_time_set ? TimeCNT - last_time_set : TimeCNT + TIMER_MAX_COUNT - last_time_set; last_time_set = TimeCNT; return ret; } // CAN消息发送函数 unsigned char canSend(CAN_PORT notused, Message *m) { return CAN1_Send_Msg((Message *)m); }3. CAN驱动与定时器配置
3.1 CAN外设初始化
STM32的CAN控制器初始化需要特别注意波特率设置。对于常见的1Mbps速率,配置如下:
CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = ENABLE; CAN_InitStructure.CAN_AWUM = ENABLE; CAN_InitStructure.CAN_NART = DISABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = DISABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_3tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler = 4; // APB1时钟为36MHz时 CAN_Init(CAN1, &CAN_InitStructure);3.2 定时器配置
CanFestival需要1ms精度的定时器来维护协议栈时间基准。使用STM32的TIM2定时器配置示例:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period = 35999; // 72MHz/72000 = 1KHz TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE);定时器中断服务程序中调用timerForCan()函数:
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); timerForCan(); } }4. 对象字典生成与配置
4.1 对象字典生成工具搭建
CanFestival自带的objdictgen工具基于Python开发,搭建步骤如下:
- 安装Python 2.7(工具暂不支持Python 3)
- 安装依赖库:
pip install wxPython pyserial - 运行工具:
python objdictgen/objdictgen.py
注意:在Windows 10上运行时,可能会遇到
wxPython兼容性问题,建议使用兼容模式运行。
4.2 对象字典配置实践
创建从站对象字典时的关键配置项:
| 参数项 | 推荐设置 | 说明 |
|---|---|---|
| 节点ID | 1-127 | 确保网络中唯一 |
| 心跳生产者时间 | 1000ms | 建议初始值 |
| PDO通信参数 | 传输类型0xFE | 异步制造商特定事件 |
| 映射参数 | 0x2000-0x5FFF区域 | 用户自定义变量区域 |
常见问题及解决方案:
工具闪退:
- 检查Python路径是否包含中文
- 尝试以管理员身份运行
生成的代码编译错误:
- 确保选择了正确的目标平台(STM32)
- 检查头文件包含路径
4.3 对象字典集成技巧
生成的对象字典包含两个关键文件:
ObjDict.c:变量存储和对象字典实现ObjDict.h:对象字典声明和从站数据结构
在工程中使用时,可以修改ObjDict.c中的变量指针,指向应用中的实际变量:
/* 原始定义 */ UNS32 Obj2000_00 = 0x0; UNS32 Obj2000_01 = 0x0; /* ... */ /* 修改为指向应用变量 */ UNS32 *Obj2000_00_ptr = &app_var1; UNS32 *Obj2000_01_ptr = &app_var2; /* ... */5. 通信测试与问题排查
5.1 初始化流程
在main()函数中,CANopen协议栈的初始化只需三个关键函数:
/* CAN硬件初始化 */ CAN_Configuration(); /* CanFestival初始化 */ setNodeId(&SLAVE_Data, 0x01); // 设置节点ID setState(&SLAVE_Data, Initialisation); setState(&SLAVE_Data, Pre_operational);5.2 心跳包测试
如果配置了心跳生产者,可以使用CAN分析仪观察心跳报文。典型的心跳报文格式:
| 字段 | 值 | 说明 |
|---|---|---|
| COB-ID | 0x700+NodeID | 节点1为0x701 |
| 数据[0] | 状态字节 | 0x05表示运行状态 |
常见心跳包问题及解决方法:
无心跳包发出:
- 检查
lifegrd.c是否包含在工程中 - 确认
SetHeartbeatTime()函数被正确调用
- 检查
心跳间隔不稳定:
- 检查定时器中断是否正常触发
- 确认没有其他高优先级中断阻塞
5.3 PDO通信测试
测试RPDO接收功能的步骤:
- 配置主站发送RPDO,COB-ID为
0x200+NodeID - 发送包含目标数据的PDO报文
- 在从站中检查对象字典对应变量是否更新
一个典型的RPDO报文示例(通过CAN分析仪发送):
# 使用python-can发送RPDO1 import can bus = can.interface.Bus(channel='can0', bustype='socketcan') msg = can.Message( arbitration_id=0x201, # 节点1的RPDO1 data=[0x01, 0x02, 0x03, 0x04], is_extended_id=False ) bus.send(msg)5.4 常见问题排查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无法接收到任何报文 | CAN过滤器配置错误 | 设置为不过滤模式 |
| 能收不能发 | CAN发送邮箱满 | 检查发送函数返回值 |
| 对象字典访问超时 | SDO服务器未正确初始化 | 确认sdo.c包含在工程中 |
| 心跳包能发但状态不对 | 状态机未正确迁移 | 检查setState()调用顺序 |
在实际项目中,我遇到最棘手的问题是CAN发送偶尔失败,最终发现是PCB布局问题导致CAN信号质量差。通过增加终端电阻和缩短布线长度解决了这一问题。另一个经验是,对象字典生成工具生成的代码可能需要手动调整变量对齐方式,特别是在使用非32位变量时。
