4、STM32平台移植:硬件抽象层(HAL)适配、定时器配置、CAN外设初始化
好,咱们进入实战环节。
前面几章把CANopenNode的架构和协议栈讲清楚了。但说真的,协议栈再牛,跑不到硬件上就是纸上谈兵。这一章,我带你手把手把CANopenNode移植到STM32上。
移植的核心就三件事:HAL层适配、定时器配置、CAN外设初始化。这三件事搞定了,剩下的就是应用层的活了。
4.1 硬件抽象层(HAL)适配——别小看这层“胶水”
CANopenNode的代码设计得很巧妙。它把跟硬件相关的操作全部抽象出来了。你打开源码,会看到 CO_driver.h 和 CO_driver_target.h 这两个文件。
说白了,你只需要实现里面定义的几个宏和函数,协议栈就能跑起来。
我个人习惯,先把 CO_driver_target.h 复制一份,改成 CO_driver_STM32.h。这样改动都在自己的文件里,以后升级CANopenNode版本也方便。
核心要实现的接口:
CO_LOCK_xxx()/CO_UNLOCK_xxx()—— 临界区保护CO_CANsetConfiguration()—— CAN波特率等参数设置CO_CANsendMsg()—— 发送CAN报文CO_CANrxMsg_t—— 接收报文结构体- 定时器相关的
CO_TMRxx宏
嗯,这里要注意。临界区保护,我建议直接用STM32 HAL库的 __disable_irq() 和 __enable_irq()。简单粗暴,但够用。如果你对实时性要求极高,可以考虑用关中断+挂起调度器的方式。不过对于CANopen应用,关中断那几十个微秒,完全不是问题。
我的小技巧:
我在项目中遇到过,有些工程师把临界区做得特别复杂,用了信号量、互斥锁什么的。其实没必要。CANopenNode的临界区只在几个关键地方使用,比如发送队列操作、定时器链表操作。用最基础的中断开关就够了。搞复杂了反而容易出死锁。
4.2 定时器配置——CANopen的“心跳”
CANopenNode需要一个1ms的定时器。这个定时器是协议栈的“心跳”。所有的定时事件,比如PDO超时、SDO超时、心跳报文发送,都依赖它。
你想想看,如果定时器不准,整个网络的时间同步就乱套了。
在STM32上,我通常用基本定时器TIM6或TIM7。为什么?因为它们最简单,没有复杂的输入捕获、输出比较功能,就是纯粹的定时中断。
配置代码大概长这样:
// 定时器句柄
TIM_HandleTypeDef htim6;
// 定时器初始化函数
void CANopen_Timer_Init(void)
{
htim6.Instance = TIM6;
htim6.Init.Prescaler = 84 - 1; // 84MHz / 84 = 1MHz
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 1000 - 1; // 1MHz / 1000 = 1kHz (1ms)
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
// 使能中断
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
// 启动定时器
HAL_TIM_Base_Start_IT(&htim6);
}
// 中断服务函数
void TIM6_DAC_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim6);
}
// 定时器中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
// 调用CANopenNode的定时器处理函数
CO_TMR_Task(&canopenObj.tmr);
}
}
注意:
定时器的时钟源要确认清楚。STM32F4系列,TIM6挂在APB1上。如果APB1的时钟是84MHz,那预分频器设84-1,自动重装载设1000-1,正好得到1ms中断。我曾经因为时钟树没配对,定时器快了10倍,排查了半天才发现是APB1时钟配置错了。
4.3 CAN外设初始化——通信的“嘴巴”
CAN外设初始化,说白了就是让STM32的CAN控制器能正常收发报文。
CANopenNode对CAN驱动的要求不高。它不依赖CAN硬件FIFO,也不依赖硬件过滤。它自己管理接收队列。所以初始化时,我们只需要把CAN外设配成正常模式,波特率设对,中断开好就行。
我一般用bxCAN,也就是STM32内置的CAN控制器。配置步骤如下:
- 使能时钟:GPIO时钟、CAN时钟
- 配置GPIO:CAN_RX设为输入浮空,CAN_TX设为复用推挽输出
- 配置CAN参数:模式、波特率、同步跳转宽度等
- 配置过滤器:设置为掩码模式,接收所有ID
- 使能中断:接收中断、错误中断(可选)
- 启动CAN:进入正常模式
代码示例:
// CAN句柄
CAN_HandleTypeDef hcan1;
// CAN初始化函数
void CANopen_CAN_Init(void)
{
// 1. 使能时钟
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9; // PB8:CAN_RX, PB9:CAN_TX
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF9_CAN1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 配置CAN参数
hcan1.Instance = CAN1;
hcan1.Init.Prescaler = 6; // 42MHz / 6 = 7MHz
hcan1.Init.Mode = CAN_MODE_NORMAL;
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_12TQ; // 12 + 1 + 2 = 15 TQ
hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;
hcan1.Init.TimeTriggeredMode = DISABLE;
hcan1.Init.AutoBusOff = DISABLE;
hcan1.Init.AutoWakeUp = DISABLE;
hcan1.Init.AutoRetransmission = ENABLE;
hcan1.Init.ReceiveFifoLocked = DISABLE;
hcan1.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan1) != HAL_OK)
{
Error_Handler();
}
// 4. 配置过滤器——接收所有ID
CAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
// 5. 使能中断
HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
// 6. 启动CAN
if (HAL_CAN_Start(&hcan1) != HAL_OK)
{
Error_Handler();
}
// 使能接收中断
if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
Error_Handler();
}
}
// CAN接收中断服务函数
void CAN1_RX0_IRQHandler(void)
{
HAL_CAN_IRQHandler(&hcan1);
}
// CAN接收回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK)
{
// 将接收到的报文传递给CANopenNode
CO_CANrxMsg_t msg;
msg.ident = rxHeader.IDE == CAN_ID_STD ? rxHeader.StdId : rxHeader.ExtId;
msg.DLC = rxHeader.DLC;
msg.IDE = rxHeader.IDE;
msg.RTR = rxHeader.RTR;
memcpy(msg.data, rxData, rxHeader.DLC);
CO_CANrxMsg_t *pMsg = &msg;
CO_CAN_RxMsgHandler(&canopenObj.can, pMsg);
}
}
波特率计算小贴士:
上面代码中,APB1时钟42MHz,预分频器6,得到CAN时钟7MHz。时间份额TQ = 1/7MHz ≈ 143ns。BS1=12TQ,BS2=2TQ,SJW=1TQ,加起来15TQ。所以波特率 = 7MHz / 15 ≈ 466.7kbps。如果你要标准250kbps,把预分频器改成12,BS1=13TQ,BS2=2TQ,总TQ=16,7MHz/16/12 ≈ 250kbps。算清楚再写代码,别像我以前那样,算都不算直接抄例程,结果波特率对不上,调试器死活连不上。
4.4 把三块拼起来——初始化流程
好,三个模块都配好了。怎么把它们串起来?
我建议的初始化顺序是:
- HAL库初始化(
HAL_Init()) - 系统时钟配置(
SystemClock_Config()) - CAN外设初始化(
CANopen_CAN_Init()) - 定时器初始化(
CANopen_Timer_Init()) - CANopenNode对象初始化(
CO_Init()) - 启动定时器(如果上一步没启动)
为什么CAN初始化要在定时器之前?因为CANopenNode初始化时,会尝试发送一个Boot-up报文。如果CAN还没配好,这报文就发不出去。虽然协议栈有重发机制,但第一次就失败,总归不优雅。
调试建议:
我第一次移植时,死活收不到报文。后来发现是过滤器配置错了。我把过滤器设成了只接收标准帧,但CANopen用的是扩展帧。所以建议你调试初期,先把过滤器设成“接收所有”,等通信正常了,再根据需要细化过滤规则。
嗯,到这里,STM32平台移植的核心工作就完成了。你可能会问,就这么简单?是的,CANopenNode的设计目标就是“易于移植”。你只要把这三个模块配好,剩下的就是应用层的活了。
下一章,我们聊聊如何验证移植是否成功——也就是让两个节点互相通信。到时候我会展示一个完整的测试用例。