4. CMS32 CAN外设驱动开发:从寄存器到中断处理
好,咱们进入第四章节。CAN外设驱动,说白了就是让芯片的CAN模块真正跑起来。很多初学者一上来就对着寄存器手册发呆,其实没必要。我个人的习惯是:先理清硬件框图,再动手写代码。今天我们就一步步把CMS32的CAN模块给“唤醒”。
4.1 CAN模块寄存器映射:你得知道门牌号
CMS32的CAN外设挂在APB总线上,基地址是固定的。每个功能寄存器都像房间的门牌号,找错了门,数据就写到别处去了。
我刚开始做项目时,就犯过这种低级错误——寄存器地址偏移算错了,结果CAN怎么都初始化不成功。后来我养成了一个习惯:每次写驱动前,先把寄存器映射结构体定义好。
下面是我常用的寄存器映射结构体,直接对应CMS32参考手册的地址偏移:
typedef struct {
volatile uint32_t CAN_CTRL; // 0x00 控制寄存器
volatile uint32_t CAN_STATUS; // 0x04 状态寄存器
volatile uint32_t CAN_BTR; // 0x08 位时序寄存器
volatile uint32_t CAN_ERR; // 0x0C 错误计数器
volatile uint32_t CAN_INTEN; // 0x10 中断使能寄存器
volatile uint32_t CAN_TXREQ; // 0x14 发送请求寄存器
volatile uint32_t CAN_RXMSG; // 0x18 接收消息寄存器
volatile uint32_t CAN_TXMSG; // 0x1C 发送消息寄存器
volatile uint32_t CAN_FILTER; // 0x20 滤波器配置寄存器
volatile uint32_t CAN_FILTER_MASK; // 0x24 滤波器掩码寄存器
} CAN_TypeDef;
#define CMS32_CAN_BASE 0x40005000UL
#define CMS32_CAN ((CAN_TypeDef *)CMS32_CAN_BASE)
volatile 关键字。编译器优化会“吃掉”你的寄存器读写操作,到时候调试起来很头疼。
4.2 初始化流程:模式、波特率、滤波器
初始化CAN外设,说白了就是三件事:定模式、配速率、设滤波。顺序不能乱,否则模块可能进不了正常工作状态。
4.2.1 模式选择
CMS32的CAN模块支持三种工作模式:
| 模式 | 寄存器配置 | 适用场景 |
|---|---|---|
| 正常模式 | CTRL.MODE = 0b00 | 正常收发通信 |
| 环回模式 | CTRL.MODE = 0b01 | 自测、调试 |
| 静默模式 | CTRL.MODE = 0b10 | 只监听不发送 |
我个人习惯在开发阶段先用环回模式。为什么?因为环回模式下,发送的数据会自己收回来,不需要接CAN收发器就能验证驱动对不对。等环回测试通过了,再切到正常模式接总线。
4.2.2 波特率配置
波特率是CAN通信的命根子。CMS32的CAN模块采用经典的位时序结构:同步段 + 传播段 + 相位缓冲段1 + 相位缓冲段2。
波特率计算公式很简单:
波特率 = 系统时钟 / (BRP + 1) / (SyncSeg + PropSeg + PS1 + PS2)
其中BRP是波特率预分频器,SyncSeg固定为1个时间量子(Tq)。
举个例子,假设系统时钟是48MHz,目标波特率500kbps:
// 48MHz / (BRP+1) / (1 + PropSeg + PS1 + PS2) = 500k
// 取 BRP=5, PropSeg=2, PS1=3, PS2=2
// 48M / 6 / (1+2+3+2) = 8M / 8 = 1M → 不对,重算
// 48M / 6 / (1+2+3+2) = 8M / 8 = 1Mbps → 不对
// 试试 BRP=3, PropSeg=1, PS1=2, PS2=1
// 48M / 4 / (1+1+2+1) = 12M / 5 = 2.4M → 还是不对
// 最终取 BRP=5, PropSeg=1, PS1=2, PS2=2
// 48M / 6 / (1+1+2+2) = 8M / 6 ≈ 1.333M → 不对
// 正确计算:BRP=5, 则 Tq = 6/48M = 125ns
// 位时间 = (1+1+2+2) * 125ns = 6 * 125ns = 750ns
// 波特率 = 1/750ns ≈ 1.333Mbps → 还是不对
// 重新算:目标500kbps,位时间=2us
// Tq = 2us / (1+1+2+2) = 2us/6 ≈ 333ns
// BRP = 333ns * 48M - 1 ≈ 15
// 取 BRP=15, PropSeg=1, PS1=2, PS2=2
// 实际Tq = 16/48M = 333.33ns
// 位时间 = 6 * 333.33ns = 2us
// 波特率 = 500kbps ✓
代码实现如下:
void CAN_Init(uint32_t baudrate) {
uint32_t brp, prop, ps1, ps2;
uint32_t tq_num = 6; // 位时间 = 6个Tq
// 计算BRP
brp = (SystemCoreClock / (baudrate * tq_num)) - 1;
// 配置位时序寄存器
CMS32_CAN->CAN_BTR = (brp << 0) | // BRP[9:0]
(1 << 10) | // SyncSeg固定为1
(1 << 12) | // PropSeg=1
(2 << 14) | // PS1=2
(2 << 17); // PS2=2
// 设置采样点位置(80%)
// 采样点 = (SyncSeg + PropSeg + PS1) / 总Tq数
// = (1+1+2)/6 = 66.7% → 偏低,调整PS1=3, PS2=1
// 重新配置
CMS32_CAN->CAN_BTR = (brp << 0) |
(1 << 10) |
(1 << 12) |
(3 << 14) | // PS1=3
(1 << 17); // PS2=1
// 采样点 = (1+1+3)/6 = 83.3% ✓
}
4.2.3 滤波器配置
滤波器的作用是“挑食”——只接收你关心的CAN ID,其他的统统丢掉。CMS32的CAN模块支持两种滤波器模式:
- 标识符列表模式:精确匹配,只收指定的ID
- 掩码模式:范围匹配,收一组ID
我在项目中遇到过一个问题:总线负载高了以后,CPU频繁被CAN中断打断,导致其他任务卡死。后来发现是滤波器没配好,把不需要的报文也收进来了。所以滤波器一定要“窄”,只收你真正需要的。
void CAN_FilterConfig(uint32_t id, uint32_t mask) {
// 配置滤波器为掩码模式
CMS32_CAN->CAN_FILTER = id & 0x7FF; // 11位标准ID
CMS32_CAN->CAN_FILTER_MASK = mask & 0x7FF;
// 使能滤波器
CMS32_CAN->CAN_CTRL |= (1 << 8); // FILTER_EN位
}
4.3 发送与接收中断处理
CAN通信的核心就是收发。我习惯用中断方式处理接收,用轮询或中断方式处理发送。为什么?接收是异步的,你不知道报文什么时候来,中断最合适。发送嘛,你可以控制节奏。
4.3.1 发送流程
发送一个CAN帧的步骤:
- 检查发送缓冲区是否空闲
- 填充ID、数据长度、数据内容
- 设置发送请求位
- 等待发送完成(中断或轮询)
uint8_t CAN_SendMsg(uint32_t id, uint8_t *data, uint8_t len) {
// 检查发送缓冲区状态
if (CMS32_CAN->CAN_STATUS & (1 << 3)) { // TX_BUSY位
return 0; // 发送忙,返回失败
}
// 填充发送寄存器
CMS32_CAN->CAN_TXMSG = (id & 0x7FF) << 21; // 标准ID
CMS32_CAN->CAN_TXMSG |= (len & 0x0F) << 16; // DLC
for (int i = 0; i < len; i++) {
CMS32_CAN->CAN_TXMSG |= data[i] << (8 * i);
}
// 触发发送
CMS32_CAN->CAN_TXREQ = 1;
return 1;
}
4.3.2 接收中断处理
接收中断是CAN驱动的灵魂。配置好中断后,每次收到报文都会触发中断服务函数。我在中断里只做一件事:把数据搬到环形缓冲区,然后置一个标志位。真正的处理在主循环里做,这样不会阻塞中断。
// 中断服务函数
void CAN_IRQHandler(void) {
uint32_t status = CMS32_CAN->CAN_STATUS;
if (status & (1 << 0)) { // RX_READY位
// 读取接收到的数据
uint32_t rx_data = CMS32_CAN->CAN_RXMSG;
uint8_t id = (rx_data >> 21) & 0x7FF;
uint8_t dlc = (rx_data >> 16) & 0x0F;
// 存入环形缓冲区(伪代码)
// ring_buf_push(id, rx_data, dlc);
// 清除中断标志
CMS32_CAN->CAN_STATUS &= ~(1 << 0);
}
if (status & (1 << 1)) { // TX_DONE位
// 发送完成,可以发下一帧
// tx_complete_flag = 1;
CMS32_CAN->CAN_STATUS &= ~(1 << 1);
}
if (status & (1 << 2)) { // ERR位
// 错误处理
// error_handler();
CMS32_CAN->CAN_STATUS &= ~(1 << 2);
}
}
4.4 整体初始化流程
把上面几块拼起来,就是完整的初始化函数:
void CAN_ModuleInit(void) {
// 1. 使能CAN时钟
RCC_APBPeriphClockCmd(RCC_APBPeriph_CAN, ENABLE);
// 2. 配置GPIO(CAN_TX和CAN_RX引脚)
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13; // 假设用PA12/PA13
gpio.GPIO_Mode = GPIO_Mode_AF;
GPIO_Init(GPIOA, &gpio);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource12, GPIO_AF_CAN);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource13, GPIO_AF_CAN);
// 3. 复位CAN模块
CMS32_CAN->CAN_CTRL |= (1 << 15); // RESET位
CMS32_CAN->CAN_CTRL &= ~(1 << 15);
// 4. 配置波特率(500kbps)
CAN_Init(500000);
// 5. 配置滤波器(只收ID=0x123)
CAN_FilterConfig(0x123, 0x7FF);
// 6. 使能中断
CMS32_CAN->CAN_INTEN = (1 << 0) | // RX中断使能
(1 << 1) | // TX中断使能
(1 << 2); // ERR中断使能
NVIC_EnableIRQ(CAN_IRQn);
// 7. 进入正常模式
CMS32_CAN->CAN_CTRL &= ~(0x03 << 0); // MODE=0b00
}
嗯,到这里CAN外设驱动的基本骨架就搭好了。你想想看,其实核心就三块:寄存器映射、初始化配置、中断处理。把这三点吃透了,不管换什么芯片,CAN驱动的思路都是一样的。