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相关 |
这样做的好处很明显:换芯片时,只需要重写中间层和底层,顶层接口纹丝不动。
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);
}
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 驱动层整体结构图
下面这张图,展示了驱动层的整体架构和数据流。你可以看到,上层协议栈只跟抽象接口层打交道,完全不碰寄存器。
4.5 驱动层初始化流程
驱动层初始化,顺序很重要。我总结了一个标准流程:
- 时钟使能:打开CAN控制器的时钟和GPIO时钟。
- GPIO配置:将CAN_TX和CAN_RX引脚配置为复用功能。
- CAN复位:将CAN控制器置于初始化模式。
- 配置参数:设置波特率、工作模式、过滤器等。
- 退出初始化:将CAN控制器切换到正常模式。
- 中断配置:设置中断优先级,使能中断。
- 启动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;
}
4.6 避坑指南
最后,分享几个我踩过的坑:
- 寄存器写后不回读:有些CAN控制器对寄存器写入有时序要求,不回读可能导致配置不生效。
- ISR里调用阻塞函数:这是新手最容易犯的错误。ISR里不能调用
vTaskDelay、printf等阻塞函数。 - 中断标志清除顺序:先处理数据,再清除标志。顺序反了,可能丢失中断事件。
- 忽略超时处理:等待硬件状态时,一定要加超时。否则硬件异常时,系统会卡死。
嗯,驱动层的内容就这些。记住一个原则:抽象隔离变化,封装隐藏细节。把这三件事做好,你的CAN驱动层就稳了。