4. CAN通信栈配置:CAN驱动初始化、CAN报文收发、基于CAN的UDS传输层实现
好,咱们进入第四章。这一章可以说是整个Bootloader的「血管系统」——CAN通信栈。没有它,你后面写的UDS、刷写逻辑全是空中楼阁。
我个人习惯把CAN通信栈拆成三层来看:最底层的CAN驱动、中间的CAN接口层、以及上层的CAN传输层(也就是CAN TP)。说白了,就是让数据从物理总线,一路顺畅地跑到你的应用层手里。
4.1 CAN驱动初始化——别小看这几步
CAN驱动的初始化,很多人觉得就是调个函数完事。嗯,我刚开始也这么想,直到有一次在实车上,ECU死活不上总线……查了两天,发现是初始化顺序搞反了。
在AUTOSAR架构下,CAN驱动的初始化通常包含以下几个关键步骤:
- 硬件复位与时钟使能——先让CAN控制器从复位状态出来,再把时钟打开。这一步漏了,后面全白搭。
- 引脚配置——CAN_TX和CAN_RX引脚要配成复用功能。我见过有人配成GPIO输出,结果总线一直显性电平,整个网络都被他拉死了。
- 波特率设置——这个要跟整车网络一致。通常250kbps或500kbps。设置不对,你发出去的报文没人听得懂。
- 中断使能——接收中断、错误中断、状态改变中断。我个人建议至少把接收中断打开,轮询方式在Bootloader里不太靠谱。
- 启动CAN控制器——从初始化模式切换到正常运行模式。
核心要点:初始化完成后,一定要检查CAN控制器的状态寄存器。确认它真的进入了「正常运行模式」,而不是卡在初始化模式里。我曾经在一个项目里,芯片复位后CAN控制器状态位一直没置起来,结果就是发不出任何报文。
代码示例(简化版,基于AUTOSAR接口风格):
/* CAN驱动初始化函数 */
void Can_Init(const Can_ConfigType* Config)
{
/* 1. 硬件复位 */
Can_HwReset();
/* 2. 配置引脚 */
Can_PinMuxConfig();
/* 3. 设置波特率 - 500kbps */
Can_SetBaudrate(500000U);
/* 4. 配置中断 */
Can_EnableInterrupt(CAN_INTERRUPT_RX);
/* 5. 启动CAN控制器 */
Can_StartController();
/* 6. 检查状态 - 我习惯加个超时等待 */
while (Can_GetState() != CAN_STATE_READY)
{
/* 超时处理,防止死等 */
if (timeout-- == 0U)
{
/* 上报错误 */
Det_ReportError(CAN_MODULE_ID, CAN_INSTANCE_ID, CAN_E_INIT_FAILED);
break;
}
}
}
小技巧:初始化时,我习惯先发一条「空报文」来验证硬件链路。比如发一个ID为0x000的数据帧,看能不能正常发送完成。这招帮我抓出过好几次硬件虚焊的问题。
4.2 CAN报文收发——别让数据卡在路上
CAN报文收发,听起来简单,但Bootloader场景下有几个坑要注意。
发送方面:
- Bootloader里通常用轮询+中断混合模式。发送时先把报文写入发送邮箱,然后等待发送完成中断或轮询状态位。
- 注意发送优先级。UDS诊断请求的响应报文,优先级要高于普通网络管理报文。你想想看,如果ECU响应诊断请求被别的报文堵住了,诊断仪那边就超时报错了。
- 发送缓冲区要管理好。我遇到过发送邮箱满了,新报文写不进去,结果丢帧的情况。
接收方面:
- 一定要用硬件过滤。CAN控制器都有接收过滤寄存器,把不关心的ID直接滤掉。不然CPU会被大量无关中断淹没。
- 接收中断服务程序要短。我见过有人在中断里做UDS协议解析……那实时性简直灾难。正确做法是:中断里只把报文搬到接收队列,解析工作交给后台任务。
- 注意接收超时。如果连续几秒收不到任何报文,可能是总线离线了,需要触发错误恢复机制。
警告:千万不要在CAN中断服务函数里调用printf或者写Flash!中断服务函数必须快进快出。我曾经接手过一个项目,前任工程师在中断里写日志到EEPROM,结果一收报文就卡死,整个ECU直接掉线。
代码示例(报文发送):
/* CAN报文发送函数 */
Std_ReturnType Can_Write(Can_HwHandleType Hth, const Can_PduType* PduInfo)
{
Std_ReturnType ret = E_NOT_OK;
/* 检查发送邮箱是否可用 */
if (Can_CheckTxMailboxFree(Hth) == TRUE)
{
/* 写入硬件发送邮箱 */
Can_WriteTxMailbox(Hth, PduInfo);
/* 触发发送 */
Can_TriggerTx(Hth);
ret = E_OK;
}
else
{
/* 邮箱满,返回忙状态 */
ret = CAN_BUSY;
}
return ret;
}
4.3 基于CAN的UDS传输层实现——这才是重头戏
UDS诊断协议跑在CAN上,靠的是CAN TP(Transport Protocol,传输协议)。ISO 15765-2里定义得清清楚楚。说白了,就是解决一个问题:UDS报文可能很长(比如刷写数据有4096字节),但CAN一帧只能发8字节,怎么办?
答案就是:分包发送,组包接收。
CAN TP的核心机制:
- 单帧(SF):报文长度 ≤ 7字节时,一帧搞定。
- 首帧(FF):报文长度 > 7字节时,第一帧告诉对方「我要发这么多数据」。
- 连续帧(CF):后续数据一帧一帧发,每帧带序号。
- 流控帧(FC):接收方告诉发送方「慢点,我处理不过来」或者「继续发」。
嗯,这里要注意:流控机制是CAN TP里最容易出问题的地方。接收方处理不过来时,会发FC帧要求发送方暂停。如果发送方不理睬,继续猛发,数据就会丢失。
实战经验:我在做Bootloader刷写时,遇到过一个问题:刷写到一半,ECU突然不响应了。查了半天,发现是发送方连续帧发得太快,接收方的缓冲区溢出了。后来在流控帧里加了「STmin(最小间隔时间)」参数,设为10ms,问题就解决了。所以,STmin不要设成0,除非你确定接收方处理能力足够强。
CAN TP的状态机:
| 状态 | 说明 | 常见问题 |
|---|---|---|
| IDLE | 空闲状态,等待接收或发送请求 | 无 |
| SF_RX/TX | 单帧接收/发送中 | 长度字段解析错误 |
| FF_RX/TX | 多帧接收/发送中,已收到/发送首帧 | 首帧长度与实际数据不符 |
| CF_RX/TX | 连续帧接收/发送中 | 序号错乱、丢帧、超时 |
| WAIT_FC | 发送方等待流控帧 | 流控帧丢失导致死等 |
代码示例(CAN TP接收状态机片段):
/* CAN TP接收状态机处理 */
void CanTp_RxStateMachine(CanTp_RxChannelType* channel, Can_PduType* frame)
{
switch (channel->state)
{
case CANTP_RX_IDLE:
/* 判断是单帧还是首帧 */
if (frame->length & 0xF0) /* PCI类型判断 */
{
/* 首帧,进入多帧接收 */
channel->state = CANTP_RX_FF;
channel->expectedLen = ((frame->data[0] & 0x0F) << 8) | frame->data[1];
channel->receivedLen = 0U;
/* 发送流控帧 */
CanTp_SendFlowControl(channel, CANTP_FC_CTS, 0U, 10U);
}
else
{
/* 单帧,直接回调上层 */
channel->state = CANTP_RX_SF;
CanTp_RxIndication(channel->id, frame);
channel->state = CANTP_RX_IDLE;
}
break;
case CANTP_RX_CF:
/* 接收连续帧,检查序号 */
if (frame->data[0] == channel->expectedSn)
{
/* 序号正确,拷贝数据 */
CanTp_CopyData(channel, frame);
channel->expectedSn++;
/* 检查是否收完 */
if (channel->receivedLen >= channel->expectedLen)
{
/* 收完了,回调上层 */
CanTp_RxIndication(channel->id, &channel->pdu);
channel->state = CANTP_RX_IDLE;
}
}
else
{
/* 序号错误,触发错误处理 */
channel->state = CANTP_RX_ERROR;
}
break;
/* 其他状态处理... */
}
}
避坑指南:我曾经在实现CAN TP时,忘记处理「连续帧序号回绕」的情况。连续帧序号只有4位(0-15),发完15后下一帧序号应该是0。如果没处理好,接收方会认为序号错误而丢弃报文。这个bug藏得很深,直到做压力测试时才暴露出来。
4.4 小结一下
CAN通信栈的配置,说白了就是三件事:让CAN控制器能正常工作、让报文能正确收发、让长报文能可靠传输。
我个人觉得,最容易出问题的环节反而是最基础的CAN初始化。很多人觉得简单,随便写写,结果硬件没起来都不知道。而CAN TP的实现,只要严格按照ISO 15765-2的状态机来写,一般不会有大问题。
下一章我们会讲UDS诊断协议栈的实现,到时候这些CAN通信栈的代码就会真正跑起来。你想想看,诊断请求进来,CAN TP拆包,UDS解析,然后执行刷写……整个链路就通了。
好,这一章就到这儿。记住:通信栈是基础,基础不牢,地动山摇。