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。
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 的标准流程:
- 检查硬件对象状态:确认目标 Mailbox 是否空闲(没有正在发送的数据)。
- 写入 ID 和数据:将 Can_PduType 中的 ID、数据长度(DLC)和数据内容写入寄存器。
- 触发发送请求:设置发送请求位,硬件会自动处理总线仲裁和发送。
- 返回状态:成功返回 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 说明至少有一个节点在监听。