4. CAN驱动层设计:底层硬件抽象层设计、寄存器操作封装、中断服务程序编写

驱动层是整个CAN软件栈的基石。说白了,它直接跟硬件寄存器打交道。我见过不少项目,应用层代码写得漂漂亮亮,结果驱动层一塌糊涂,最后整个系统跑起来各种诡异问题。

这一章,我们就来聊聊怎么把驱动层做扎实。核心就三件事:硬件抽象层(HAL)设计、寄存器操作封装、中断服务程序(ISR)编写。

4.1 硬件抽象层设计

为什么要搞一个硬件抽象层?你想想看,今天用STM32的FDCAN,明天可能换成NXP的FlexCAN,后天又可能是TI的DCAN。如果驱动代码跟寄存器直接耦合,换一次芯片就得重写一遍驱动,那不得累死?

硬件抽象层的作用,就是在芯片相关的寄存器操作和上层协议栈之间,加一层“翻译官”。

4.1.1 抽象层接口定义

我个人习惯,先把CAN控制器的核心操作抽象成一组接口。不管底层是什么芯片,上层调用的函数签名不变。

/* can_hw_interface.h */
#ifndef CAN_HW_INTERFACE_H
#define CAN_HW_INTERFACE_H

#include <stdint.h>
#include <stdbool.h>

/* CAN控制器状态 */
typedef struct {
    uint32_t    core_id;        /* CAN内核ID */
    uint32_t    base_addr;      /* 寄存器基地址 */
    uint8_t     irq_num;        /* 中断号 */
    bool        is_initialized; /* 初始化标志 */
} can_hw_t;

/* 抽象接口:初始化 */
int32_t can_hw_init(can_hw_t *hw, uint32_t base_addr, uint8_t irq_num);

/* 抽象接口:配置波特率 */
int32_t can_hw_set_bitrate(can_hw_t *hw, uint32_t bitrate_kbps);

/* 抽象接口:发送报文 */
int32_t can_hw_send(can_hw_t *hw, uint32_t id, uint8_t *data, uint8_t len);

/* 抽象接口:接收报文 */
int32_t can_hw_receive(can_hw_t *hw, uint32_t *id, uint8_t *data, uint8_t *len);

/* 抽象接口:使能中断 */
void can_hw_enable_irq(can_hw_t *hw, uint32_t irq_mask);

/* 抽象接口:清除中断标志 */
void can_hw_clear_irq(can_hw_t *hw, uint32_t irq_flag);

#endif /* CAN_HW_INTERFACE_H */

这套接口,就是驱动层的“契约”。上层协议栈只认这些函数,不关心底层是哪个厂家的CAN控制器。

4.1.2 分层结构

我建议把驱动层拆成三层:

层级 名称 职责 可移植性
顶层 CAN抽象接口层 定义统一API,屏蔽芯片差异 完全可移植
中间层 CAN芯片适配层 实现具体芯片的寄存器操作 芯片相关
底层 寄存器操作层 直接读写寄存器地址 MCU相关

这样做的好处很明显:换芯片时,只需要重写中间层和底层,顶层接口纹丝不动。

经验之谈: 我在一个车载网关项目里,用这套分层结构,三天内就把CAN驱动从STM32F4移植到了S32K144上。上层协议栈一行代码没改,全靠抽象层隔离得好。

4.2 寄存器操作封装

寄存器操作是驱动层最底层的活。直接写*(volatile uint32_t *)0x40006400 |= 0x01这种代码,不仅可读性差,还容易写错位。

4.2.1 寄存器地址映射

我习惯用结构体来映射寄存器组。这样既清晰,又方便编译器做地址计算。

/* 以STM32 bxCAN为例 */
typedef struct {
    volatile uint32_t MCR;      /* 主控制寄存器 */
    volatile uint32_t MSR;      /* 主状态寄存器 */
    volatile uint32_t TSR;      /* 发送状态寄存器 */
    volatile uint32_t RF0R;     /* 接收FIFO0寄存器 */
    volatile uint32_t RF1R;     /* 接收FIFO1寄存器 */
    volatile uint32_t IER;      /* 中断使能寄存器 */
    volatile uint32_t ESR;      /* 错误状态寄存器 */
    volatile uint32_t BTR;      /* 位时序寄存器 */
    /* ... 省略其他寄存器 */
} CAN_TypeDef;

/* 基地址定义 */
#define CAN1_BASE   0x40006400UL
#define CAN2_BASE   0x40006800UL

/* 访问宏 */
#define CAN1        ((CAN_TypeDef *)CAN1_BASE)
#define CAN2        ((CAN_TypeDef *)CAN2_BASE)

这样,操作寄存器就变成了结构体成员访问:CAN1->MCR = 0x01;。编译器会自动计算偏移地址,不容易出错。

4.2.2 位操作封装

直接写CAN1->MCR |= (1 << 0);还是不够友好。我建议把常用的位操作封装成内联函数或宏。

/* 位操作封装 */
#define CAN_MCR_INRQ      ((uint32_t)0x00000001)  /* 初始化请求 */
#define CAN_MCR_TTCM      ((uint32_t)0x00000080)  /* 时间触发通信模式 */
#define CAN_MCR_ABOM      ((uint32_t)0x00000040)  /* 自动离线管理 */
#define CAN_MCR_AWUM      ((uint32_t)0x00000020)  /* 自动唤醒模式 */
#define CAN_MCR_NART      ((uint32_t)0x00000010)  /* 禁止自动重传 */
#define CAN_MCR_RFLM      ((uint32_t)0x00000008)  /* 接收FIFO锁定模式 */
#define CAN_MCR_TXFP      ((uint32_t)0x00000004)  /* 发送FIFO优先级 */

/* 设置位 */
static inline void CAN_SetBit(volatile uint32_t *reg, uint32_t mask) {
    *reg |= mask;
}

/* 清除位 */
static inline void CAN_ClrBit(volatile uint32_t *reg, uint32_t mask) {
    *reg &= ~mask;
}

/* 读取位 */
static inline uint32_t CAN_GetBit(volatile uint32_t *reg, uint32_t mask) {
    return (*reg & mask);
}

/* 使用示例:进入初始化模式 */
void CAN_EnterInitMode(CAN_TypeDef *CANx) {
    CAN_SetBit(&CANx->MCR, CAN_MCR_INRQ);
    /* 等待硬件确认 */
    while (!CAN_GetBit(&CANx->MSR, CAN_MCR_INRQ)) {
        /* 超时处理 */
    }
}
注意: 寄存器操作必须使用 volatile 关键字。我曾经在一个项目里忘了加 volatile,结果编译器优化后,寄存器读出来永远是同一个值,排查了整整两天才发现问题。

4.2.3 寄存器操作的安全检查

写寄存器时,我习惯加一些防御性检查。比如写之前先读一下,确认硬件状态允许写。

/* 安全写寄存器 */
int32_t CAN_SafeWriteReg(volatile uint32_t *reg, uint32_t value, uint32_t mask) {
    uint32_t readback;
    
    /* 写寄存器 */
    *reg = value;
    
    /* 回读验证 */
    readback = *reg;
    if ((readback & mask) != (value & mask)) {
        return -1;  /* 写入失败 */
    }
    
    return 0;
}

这种回读验证,在调试阶段特别有用。能帮你快速发现硬件问题或配置错误。

4.3 中断服务程序编写

CAN中断服务程序,是驱动层最考验功力的地方。写得好,系统稳如泰山;写得不好,各种死锁、丢帧、优先级反转。

4.3.1 中断优先级设计

我建议把CAN中断优先级设得高一些,但不能高过系统时钟和紧急错误处理。一般给个中等偏上的优先级就够了。

/* 中断优先级分组 */
#define CAN_IRQ_PREEMPT_PRIORITY    2   /* 抢占优先级 */
#define CAN_IRQ_SUB_PRIORITY        0   /* 子优先级 */

/* 初始化中断 */
void CAN_IRQ_Init(can_hw_t *hw) {
    /* 设置中断优先级 */
    NVIC_SetPriority((IRQn_Type)hw->irq_num, 
                     NVIC_EncodePriority(NVIC_GetPriorityGrouping(),
                                         CAN_IRQ_PREEMPT_PRIORITY,
                                         CAN_IRQ_SUB_PRIORITY));
    /* 使能中断 */
    NVIC_EnableIRQ((IRQn_Type)hw->irq_num);
}

4.3.2 ISR的核心原则

写ISR,记住三个字:短、快、稳。

  • 短:ISR里只做最必要的事。读取数据、清除标志、通知任务。别在ISR里做复杂运算。
  • 快:ISR执行时间要短。CAN总线速度高,ISR太长会导致丢帧。
  • 稳:ISR里不能调用可能阻塞的函数。比如printf、malloc、信号量等待等。
/* CAN接收中断服务程序 */
void CAN1_RX0_IRQHandler(void) {
    uint32_t id;
    uint8_t  data[8];
    uint8_t  len;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    /* 1. 读取接收报文 */
    if (CAN_ReceiveFrame(CAN1, &id, data, &len) == 0) {
        /* 2. 将数据放入队列(不阻塞) */
        xQueueSendFromISR(g_can_rx_queue, &id, &xHigherPriorityTaskWoken);
        xQueueSendFromISR(g_can_data_queue, data, &xHigherPriorityTaskWoken);
    }
    
    /* 3. 清除中断标志 */
    CAN_ClearIRQ(CAN1, CAN_IRQ_RF0NE);
    
    /* 4. 触发任务切换(如果需要) */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
关键点: ISR里使用队列或信号量时,必须用 FromISR 后缀的API。普通API会在ISR里引起任务调度,导致系统崩溃。这个坑我踩过,一次CAN总线风暴,ISR里调用了普通队列发送,结果系统直接死锁。

4.3.3 中断标志处理

CAN控制器的中断标志,处理起来有讲究。我见过有人一进ISR就把所有中断标志全清了,结果漏掉了同时发生的其他中断事件。

正确的做法是:先读中断状态,再按需清除。

/* 正确的中断处理流程 */
void CAN1_SCE_IRQHandler(void) {
    uint32_t status;
    
    /* 1. 读取中断状态寄存器 */
    status = CAN1->RF0R;
    
    /* 2. 根据状态位处理 */
    if (status & CAN_RF0R_FMP0) {
        /* FIFO0有报文,处理接收 */
        CAN_HandleReceive(CAN1, 0);
    }
    
    if (status & CAN_RF0R_FULL0) {
        /* FIFO0溢出,处理溢出 */
        CAN_HandleOverflow(CAN1, 0);
        /* 清除溢出标志 */
        CAN1->RF0R |= CAN_RF0R_FULL0;
    }
    
    /* 3. 最后清除FMP0标志(由硬件自动清除,或手动清除) */
    CAN1->RF0R |= CAN_RF0R_FMP0;
}

4.4 驱动层整体结构图

下面这张图,展示了驱动层的整体架构和数据流。你可以看到,上层协议栈只跟抽象接口层打交道,完全不碰寄存器。

CAN驱动层整体架构 应用层 / CAN协议栈 CAN抽象接口层 (can_hw_interface.h) CAN芯片适配层 (bxCAN / FlexCAN / DCAN) 寄存器操作层 (位封装 / 地址映射 / 安全检查) CAN控制器硬件 (寄存器) 数据流方向 应用层 → 抽象接口 抽象接口 → 芯片适配 芯片适配 → 寄存器操作 寄存器操作 → 硬件 中断从硬件向上传递 ISR → 队列 → 任务

4.5 驱动层初始化流程

驱动层初始化,顺序很重要。我总结了一个标准流程:

  1. 时钟使能:打开CAN控制器的时钟和GPIO时钟。
  2. GPIO配置:将CAN_TX和CAN_RX引脚配置为复用功能。
  3. CAN复位:将CAN控制器置于初始化模式。
  4. 配置参数:设置波特率、工作模式、过滤器等。
  5. 退出初始化:将CAN控制器切换到正常模式。
  6. 中断配置:设置中断优先级,使能中断。
  7. 启动CAN:使能CAN控制器,开始通信。
/* 驱动层初始化函数 */
int32_t CAN_DriverInit(can_hw_t *hw, uint32_t base_addr, uint8_t irq_num) {
    int32_t ret;
    
    /* 1. 初始化硬件抽象层 */
    ret = can_hw_init(hw, base_addr, irq_num);
    if (ret != 0) return ret;
    
    /* 2. 配置波特率(500kbps) */
    ret = can_hw_set_bitrate(hw, 500);
    if (ret != 0) return ret;
    
    /* 3. 配置过滤器(接收所有报文) */
    CAN_SetFilter(CAN1, CAN_FILTER_MODE_MASK, CAN_FILTER_SCALE_32BIT,
                  0x00000000, 0x00000000);
    
    /* 4. 使能中断 */
    can_hw_enable_irq(hw, CAN_IRQ_RX0NE | CAN_IRQ_ERROR);
    
    /* 5. 启动CAN */
    CAN_Start(CAN1);
    
    return 0;
}
调试技巧: 初始化完成后,建议读一下CAN控制器的状态寄存器,确认是否真的进入了正常模式。我习惯在初始化函数末尾加一段自检代码,打印当前状态,方便调试。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 寄存器写后不回读:有些CAN控制器对寄存器写入有时序要求,不回读可能导致配置不生效。
  • ISR里调用阻塞函数:这是新手最容易犯的错误。ISR里不能调用vTaskDelay、printf等阻塞函数。
  • 中断标志清除顺序:先处理数据,再清除标志。顺序反了,可能丢失中断事件。
  • 忽略超时处理:等待硬件状态时,一定要加超时。否则硬件异常时,系统会卡死。

嗯,驱动层的内容就这些。记住一个原则:抽象隔离变化,封装隐藏细节。把这三件事做好,你的CAN驱动层就稳了。