3. CAN总线基础

好,咱们今天来啃 CAN 驱动模块最核心的三个接口:Can_Init、Can_Write 和 Can_Read。这三个函数,说白了就是 CAN 通信的“三板斧”——初始化、发、收。你把这几个搞透了,CAN 驱动的基本骨架就有了。

我个人习惯,在讲代码之前,先理清楚它们在整个 BSW 里的位置。你想想看,Can_Init 是入口,Can_Write 是出口,Can_Read 是入口的回调。它们之间通过硬件寄存器、Mailbox 和中断服务程序串联起来。

核心逻辑: Can_Init 配置硬件并启动;Can_Write 将 L-PDU 写入硬件发送缓冲区;Can_Read 从中断或轮询中取出接收到的 L-PDU。
CAN 驱动模块核心接口与数据流 CAN 驱动模块:Can_Init / Can_Write / Can_Read 数据流 Can_Init 配置 CAN 控制器寄存器 设置波特率、采样点、中断 CAN 控制器运行中 Can_Write 写入 Mailbox / Tx Buffer Can_Read 从 Mailbox / Rx Buffer 读取 CAN Tx 硬件发送 CAN Rx 硬件接收 + 中断 发送 接收

3.1 Can_Init:一锤定音的初始化

Can_Init 是 CAN 驱动的第一个入口。它负责把硬件从“混沌”状态拉入“就绪”状态。我记得刚入行那会儿,总觉得初始化就是写几个寄存器,后来踩了坑才明白——初始化的顺序和时机,直接决定了后续通信的稳定性。

Can_Init 主要干这几件事:

  • 硬件复位与时钟使能:把 CAN 控制器从复位状态释放,打开外设时钟。
  • 配置通信参数:波特率、采样点、同步跳转宽度(SJW)。这些参数必须和总线上的其他节点一致,否则连不上。
  • 配置 Mailbox / 硬件对象:分配发送和接收缓冲区,设置过滤规则。
  • 中断使能:打开发送完成、接收、错误等中断。
  • 设置控制器模式:从初始化模式切换到正常模式。
避坑指南: 我曾经在一个项目里,因为波特率计算时忽略了晶振误差,导致 CAN 总线在高温下频繁丢帧。后来我养成了一个习惯——初始化完成后,主动发送一个 Dummy 帧并等待 ACK,验证总线是否真的通了。

下面是一个简化的 Can_Init 实现示例(基于 AUTOSAR 风格):

/* Can_Init 简化实现 */
void Can_Init(const Can_ConfigType* Config)
{
    /* 1. 硬件复位 */
    CAN_REG_RESET = 0x01;
    while(CAN_REG_RESET & 0x01);  /* 等待复位完成 */

    /* 2. 配置波特率 */
    CAN_REG_BTR = Config->BaudRateConfig;  /* 包含 BRP, TSEG1, TSEG2, SJW */

    /* 3. 配置 Mailbox */
    for (uint8 i = 0; i < Config->HwObjectCount; i++)
    {
        Can_HwObjectType* obj = &Config->HwObjects[i];
        CAN_REG_MB[i].ID   = obj->CanId;
        CAN_REG_MB[i].CTRL = obj->ObjectType;  /* 发送/接收/FIFO */
        CAN_REG_MB[i].MASK = obj->FilterMask;
    }

    /* 4. 使能中断 */
    CAN_REG_IER = (CAN_IER_TX | CAN_IER_RX | CAN_IER_ERR);

    /* 5. 切换到正常模式 */
    CAN_REG_MCR &= ~CAN_MCR_INIT;
    while(CAN_REG_MSR & CAN_MSR_INIT);  /* 等待模式切换完成 */

    /* 6. 设置驱动状态 */
    Can_GlobalState = CAN_STATE_READY;
}

3.2 Can_Write:把数据“塞”进硬件

Can_Write 的职责很明确:把上层传下来的 L-PDU(数据链路层协议数据单元)写入硬件发送缓冲区。但这里有个关键点——不是所有硬件都支持随时写入。有些控制器要求 Mailbox 必须先配置为“空闲”状态,才能写入新数据。

Can_Write 的标准流程:

  1. 检查硬件对象状态:确认目标 Mailbox 是否空闲(没有正在发送的数据)。
  2. 写入 ID 和数据:将 Can_PduType 中的 ID、数据长度(DLC)和数据内容写入寄存器。
  3. 触发发送请求:设置发送请求位,硬件会自动处理总线仲裁和发送。
  4. 返回状态:成功返回 E_OK,失败返回 E_NOT_OK。
注意: 如果硬件没有空闲的发送 Mailbox,Can_Write 应该返回 CAN_BUSY,而不是阻塞等待。上层模块(CanIf)会负责重传或排队。我曾经见过一个项目,因为驱动层在 Can_Write 里加了 while 等待,导致整个任务卡死——驱动层永远不要阻塞。
/* Can_Write 简化实现 */
Std_ReturnType Can_Write(uint8 Hth, const Can_PduType* PduInfo)
{
    /* 1. 检查硬件对象是否空闲 */
    if (CAN_REG_MB[Hth].CTRL & CAN_MB_TX_PENDING)
    {
        return CAN_BUSY;  /* 硬件正忙,上层会处理 */
    }

    /* 2. 写入 ID 和 DLC */
    CAN_REG_MB[Hth].ID  = PduInfo->id;
    CAN_REG_MB[Hth].DLC = PduInfo->length;

    /* 3. 写入数据 */
    for (uint8 i = 0; i < PduInfo->length; i++)
    {
        CAN_REG_MB[Hth].DATA[i] = PduInfo->sdu[i];
    }

    /* 4. 触发发送请求 */
    CAN_REG_MB[Hth].CTRL |= CAN_MB_TX_REQ;

    return E_OK;
}

3.3 Can_Read:从硬件“捞”数据

Can_Read 的实现方式取决于硬件架构。常见的有两种:

  • 中断驱动:硬件收到帧后触发中断,ISR 中调用 Can_Read 将数据拷贝到上层缓冲区。
  • 轮询模式:主循环或定时任务中主动检查 Mailbox 是否有新数据。

我个人更推荐中断方式——效率高,响应快。但要注意,中断服务程序里不要做太多事。Can_Read 只负责把数据从硬件寄存器拷贝到内存缓冲区,然后通知 CanIf 层。真正的协议处理交给上层。

关键设计原则: Can_Read 是“数据搬运工”,不是“数据消费者”。它只负责把硬件里的帧搬到软件缓冲区,然后通过回调函数通知上层来取。
/* Can_Read 简化实现(中断上下文调用) */
void Can_Read(uint8 Hrh, Can_PduType* PduInfo)
{
    /* 1. 读取 ID */
    PduInfo->id = CAN_REG_MB[Hrh].ID;

    /* 2. 读取 DLC */
    PduInfo->length = CAN_REG_MB[Hrh].DLC;

    /* 3. 读取数据 */
    for (uint8 i = 0; i < PduInfo->length; i++)
    {
        PduInfo->sdu[i] = CAN_REG_MB[Hrh].DATA[i];
    }

    /* 4. 清除接收标志,使能下一次接收 */
    CAN_REG_MB[Hrh].CTRL &= ~CAN_MB_RX_PENDING;
}

3.4 配置参数对比表

这三个接口的配置参数,我整理了一个表格,方便你对照理解:

接口 关键配置参数 典型配置值 说明
Can_Init BaudRate, SamplePoint, SJW, Mode 500kbps, 87.5%, 1Tq, Normal 波特率必须与总线一致;采样点推荐 87.5%
Can_Write Hth, CanId, DLC, Data, ObjectType Hth=0, ID=0x100, DLC=8, TX Hth 是硬件发送句柄,需提前配置
Can_Read Hrh, CanId, DLC, Data, FilterMask Hrh=1, Mask=0x7FF, RX Hrh 是硬件接收句柄;FilterMask 决定哪些帧能进来

3.5 我踩过的几个坑

嗯,这里再分享几个实战中容易翻车的地方:

  • 初始化顺序反了:先配 Mailbox 再配波特率?有些硬件会报错。我建议严格按照数据手册的初始化序列来,别想当然。
  • 中断优先级没配好:CAN 接收中断优先级太低,被其他中断频繁打断,导致溢出。我曾经在一个多 CAN 项目里,因为中断优先级配置不当,高速 CAN 的帧被低速 CAN 的中断堵住,丢帧率高达 30%。
  • Can_Write 返回 BUSY 后没有重试机制:上层 CanIf 模块会处理重试,但如果你自己写应用层直接调 Can_Write,记得加重试逻辑。
  • 过滤配置太松:把 Mask 设成 0x000,结果所有帧都进来了,CPU 负载飙升。建议先配成严格过滤,调试时再放宽。
一个小技巧: 调试 CAN 驱动时,我习惯在 Can_Init 完成后,用示波器抓一下 CAN_H 和 CAN_L 的波形。如果能看到一个显性位(约 3.5V vs 1.5V),说明硬件初始化基本没问题。然后再用 Can_Write 发一个固定 ID 的帧,看总线上有没有 ACK 波形——有 ACK 说明至少有一个节点在监听。