4. UDS协议栈架构:分层设计与状态机
好,咱们今天聊聊UDS协议栈的架构。说实话,很多初学者一上来就扎进代码里,结果越看越懵。我个人习惯,先搞清楚分层,再动手写代码。你想想看,一个完整的UDS协议栈,如果没分层,那维护起来简直是噩梦。
我在项目中遇到过好几次,客户要求增加一个新功能,如果协议栈分层清晰,我只需要在应用层加个处理函数就行。要是没分层,那得从底层翻到顶层,改得心惊胆战。
4.1 协议栈为什么要分层?
说白了,分层就是为了解耦。每一层只管自己的事,不越界。UDS协议栈通常分为四层:
- 传输层:负责数据收发,比如CAN驱动、CAN FD驱动
- 网络层:处理多帧传输、流控制、时间参数
- 会话层:管理诊断会话、安全访问、服务状态
- 应用层:解析UDS服务,比如0x22读数据、0x2E写数据
嗯,这里要注意,ISO 14229-1定义的是应用层和会话层,ISO 15765-2定义的是网络层。传输层其实跟硬件相关,STM32上我们通常用CAN外设或者CAN FD外设。
4.2 传输层与网络层
传输层,说白了就是最底层的硬件驱动。在STM32上,我一般用HAL库的CAN驱动,或者直接用LL库,效率更高。传输层只做一件事:把数据发出去,把数据收进来。
网络层就复杂一些了。它要处理单帧、多帧、流控制帧。我记得第一次做多帧传输时,踩了个坑——流控制帧的超时时间没算对,导致ECU一直重发,总线都快堵死了。
网络层核心功能:
- 单帧(SF):最多7字节数据(CAN标准帧)
- 首帧(FF):多帧传输的第一帧,包含总长度
- 连续帧(CF):后续数据帧
- 流控制帧(FC):接收方告诉发送方“慢点发”或“继续发”
这里我贴一段网络层状态机的伪代码,大家感受一下:
// 网络层接收状态机
typedef enum {
NW_IDLE, // 空闲
NW_WAIT_FF, // 等待首帧
NW_WAIT_CF, // 等待连续帧
NW_WAIT_FC // 等待流控制
} NwState_t;
void Nw_ReceiveFrame(CAN_Frame_t *frame) {
switch (nwState) {
case NW_IDLE:
if (frame->type == SINGLE_FRAME) {
// 直接交给上层
App_ProcessSingleFrame(frame->data);
} else if (frame->type == FIRST_FRAME) {
// 记录总长度,准备接收连续帧
nwState = NW_WAIT_CF;
Nw_SendFlowControl(FC_CONTINUE);
}
break;
case NW_WAIT_CF:
// 接收连续帧,拼装数据
// 拼完了就交给上层
break;
// 其他状态...
}
}
我的经验:网络层的超时处理一定要做。我曾经遇到过ECU发送首帧后,工具端没及时回复流控制,结果ECU一直卡在等待状态。加个超时定时器,超时了就复位状态机,这是保底操作。
4.3 会话层与应用层
会话层,管理的是诊断会话。UDS定义了三种默认会话:
| 会话类型 | 会话ID | 说明 |
|---|---|---|
| 默认会话 | 0x01 | 上电后的初始状态,功能受限 |
| 编程会话 | 0x02 | 用于刷写程序,通常需要安全访问 |
| 扩展会话 | 0x03 | 用于诊断、标定,功能最全 |
应用层呢,就是处理具体的UDS服务。比如0x22(读取数据标识符),0x2E(写入数据标识符),0x31(例程控制)等等。每个服务都有固定的请求格式和响应格式。
我建议把应用层的服务处理做成函数指针表。这样加一个新服务,只需要在表里注册一个函数就行,不用改主流程。我在项目里就是这么干的,维护起来特别爽。
4.4 状态机设计
状态机是UDS协议栈的灵魂。没有状态机,协议栈就是一盘散沙。UDS的状态机分好几个维度:
- 会话状态机:默认会话 ↔ 编程会话 ↔ 扩展会话
- 安全访问状态机:锁定 → 请求种子 → 发送密钥 → 解锁
- 服务处理状态机:空闲 → 处理中 → 等待响应 → 完成
我个人习惯用枚举+switch来实现状态机,简单直观,调试也方便。别整那些花里胡哨的状态模式,嵌入式系统讲究的是确定性。
注意:状态机一定要考虑异常情况。比如客户端发送了0x10 0x02(切换到编程会话),但ECU正在执行一个耗时操作,这时候怎么办?我的做法是:先回复待处理响应(0x78),等操作完成再切换会话。
这里给一个会话状态机的简化示例:
typedef enum {
SESSION_DEFAULT = 0x01,
SESSION_PROGRAM = 0x02,
SESSION_EXTENDED = 0x03
} Session_t;
Session_t currentSession = SESSION_DEFAULT;
void Session_HandleRequest(uint8_t sessionId) {
switch (currentSession) {
case SESSION_DEFAULT:
if (sessionId == SESSION_PROGRAM) {
// 检查条件,比如是否允许切换
currentSession = SESSION_PROGRAM;
SendPositiveResponse(0x10, sessionId);
}
break;
case SESSION_PROGRAM:
// 编程会话下,只能切回默认或扩展
if (sessionId == SESSION_DEFAULT || sessionId == SESSION_EXTENDED) {
currentSession = sessionId;
SendPositiveResponse(0x10, sessionId);
}
break;
// 扩展会话类似...
}
}
嗯,这里要注意,会话切换时,有些资源要重新初始化。比如在编程会话下,你可能禁用了某些通信,切回默认会话时要恢复。
4.5 我的架构建议
说了这么多,总结一下我的个人建议:
- 分层要彻底:每一层只通过接口函数通信,不要跨层调用。比如网络层不要直接调用CAN驱动,而是通过回调函数。
- 状态机要完整:每个状态都要考虑超时、错误、异常。我曾经漏掉了“等待响应超时”这个状态,结果ECU死锁了,只能断电重启。
- 日志要详细:每个状态切换、每帧收发,都打日志。调试的时候你就知道这有多重要了。
- 预留扩展点:比如在应用层预留一个“自定义服务”的钩子函数,方便客户加私有协议。
核心要点:UDS协议栈的架构设计,说白了就是“分而治之”。传输层管收发,网络层管拼装,会话层管状态,应用层管业务。各司其职,出了问题也好定位。
好了,这一章就到这里。下一章咱们聊聊具体的UDS服务实现,比如0x22读数据、0x2E写数据,我会带着大家一步步写代码。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。