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 ✓
注意:采样点位置建议设置在75%~85%之间。PS1和PS2的比例决定了采样点,PS1太大会导致采样点偏后,抗干扰能力下降。

代码实现如下:

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位
}
经验之谈:掩码位为0表示“必须匹配”,为1表示“忽略该位”。比如掩码0x7FF表示全匹配,只收一个ID;掩码0x000表示不过滤,所有报文都收。

4.3 发送与接收中断处理

CAN通信的核心就是收发。我习惯用中断方式处理接收,用轮询或中断方式处理发送。为什么?接收是异步的,你不知道报文什么时候来,中断最合适。发送嘛,你可以控制节奏。

4.3.1 发送流程

发送一个CAN帧的步骤:

  1. 检查发送缓冲区是否空闲
  2. 填充ID、数据长度、数据内容
  3. 设置发送请求位
  4. 等待发送完成(中断或轮询)
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);
    }
}
避坑指南:中断服务函数里千万不要做耗时操作,比如打印日志、动态内存分配。我曾经在中断里调了一个printf,结果系统直接死机——中断嵌套把栈撑爆了。

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驱动的思路都是一样的。

CMS32 CAN外设驱动初始化流程 1. 使能CAN时钟 2. 配置CAN_TX/RX引脚 3. 复位CAN模块 4. 配置波特率 5. 配置滤波器 6. 使能中断 关键点 • 顺序不能乱 • 先复位再配置 • 最后才进正常模式 调试建议 • 先用环回模式自测 • 用逻辑分析仪看波形