4、STM32平台移植:硬件抽象层(HAL)适配、定时器配置、CAN外设初始化

好,咱们进入实战环节。

前面几章把CANopenNode的架构和协议栈讲清楚了。但说真的,协议栈再牛,跑不到硬件上就是纸上谈兵。这一章,我带你手把手把CANopenNode移植到STM32上。

移植的核心就三件事:HAL层适配、定时器配置、CAN外设初始化。这三件事搞定了,剩下的就是应用层的活了。

4.1 硬件抽象层(HAL)适配——别小看这层“胶水”

CANopenNode的代码设计得很巧妙。它把跟硬件相关的操作全部抽象出来了。你打开源码,会看到 CO_driver.hCO_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控制器。配置步骤如下:

  1. 使能时钟:GPIO时钟、CAN时钟
  2. 配置GPIO:CAN_RX设为输入浮空,CAN_TX设为复用推挽输出
  3. 配置CAN参数:模式、波特率、同步跳转宽度等
  4. 配置过滤器:设置为掩码模式,接收所有ID
  5. 使能中断:接收中断、错误中断(可选)
  6. 启动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 把三块拼起来——初始化流程

好,三个模块都配好了。怎么把它们串起来?

我建议的初始化顺序是:

  1. HAL库初始化(HAL_Init()
  2. 系统时钟配置(SystemClock_Config()
  3. CAN外设初始化(CANopen_CAN_Init()
  4. 定时器初始化(CANopen_Timer_Init()
  5. CANopenNode对象初始化(CO_Init()
  6. 启动定时器(如果上一步没启动)

为什么CAN初始化要在定时器之前?因为CANopenNode初始化时,会尝试发送一个Boot-up报文。如果CAN还没配好,这报文就发不出去。虽然协议栈有重发机制,但第一次就失败,总归不优雅。

调试建议:

我第一次移植时,死活收不到报文。后来发现是过滤器配置错了。我把过滤器设成了只接收标准帧,但CANopen用的是扩展帧。所以建议你调试初期,先把过滤器设成“接收所有”,等通信正常了,再根据需要细化过滤规则。

嗯,到这里,STM32平台移植的核心工作就完成了。你可能会问,就这么简单?是的,CANopenNode的设计目标就是“易于移植”。你只要把这三个模块配好,剩下的就是应用层的活了。

下一章,我们聊聊如何验证移植是否成功——也就是让两个节点互相通信。到时候我会展示一个完整的测试用例。