乐于分享
好东西不私藏

汽车电子嵌入式软件缺口大不大、诊断栈与UDS服务

汽车电子嵌入式软件缺口大不大、诊断栈与UDS服务

   

一、行业缺口:缺的不是写代码的人

缺口大不大?大。 但缺的不是"纯写代码的人"。

大部分公司招人时的真实感受是:简历收一堆,技术面能过的很少。很多人停留在"会用厂商SDK、会改Demo、会配工具"的层面,一旦遇到芯片手册写得不清楚、FAE解决不了、需要自己从实际需求出发设计模块的时候,就搞不定了。所以准确说,缺口大的不是"纯写代码的人",而是能独立解决"非标准问题"的人——比如能把MCAL配稳、把UDS调通、能独立定位常见通信故障。

3年左右经验的是不是还不够? 从我接触的圈子看,是的。因为这个阶段的人大多还在熟悉业务和工具链,能独立负责一个底层模块(比如驱动、通信栈、诊断栈)的人确实不多。这不是能力问题,是这个方向的学习曲线本来就陡,3年时间刚好够入门到熟练,还来不及积累足够多的"非标准问题"处理经验。

那行业里真正公认的稀缺能力有哪些? 根据招聘数据和同行交流,真正供不应求的是以下几类高阶复合型人才:

• 把功能安全落实到代码细节:不仅仅是看过ISO 26262,而是知道一个ASIL B的ADC驱动,变量应该加什么保护?内存分区怎么设?故障响应怎么写?很多项目的功能安全是"文档安全",代码还是老一套。

• 搞定多核异构SoC的底层:现在域控制器越来越多,Cortex-R锁步核、Cortex-A跑Linux、M核做实时。能把启动、核间通信、内存共享、硬件加速器驱动写明白的人很少。

• 软硬结合的调试能力:偶发性CAN错误,能自己去抓波形、看共模电压、定位到收发器电源噪声。这种能力大部分软件工程师不具备,硬件工程师又不熟悉协议栈。

• 车载操作系统与中间件:具备5年以上AUTOSAR(CP/AP)、QNX、Linux RT等经验的人,猎头报告显示供需比低至1:8甚至1:16。

以上这些,我不敢说自己多擅长——有些还在学,有些只在项目里摸过皮毛。但这些方向确实是行业里公认的"稀缺"。而最能体现上述能力交集的战场之一,就是诊断栈


二、AUTOSAR诊断栈的四模块架构

诊断不是一条单线,而是一套精细的分工流水线。AUTOSAR将诊断功能划分成了四个核心模块,各自负责诊断管线的不同阶段:

模块
完整名称
一句话职责
DCM
Diagnostic Communication Manager
收到UDS请求→解析→分发→执行→回复。诊断通信的核心服务器
DEM
Diagnostic Event Manager
存储DTC、管理DTC状态位、Debounce、快照/扩展数据
DLT
Diagnostic Log and Trace
诊断日志记录——将诊断交互写入持久存储或串口输出
FiM
Function Inhibition Manager
函数级抑制——根据DTC严重度临时停用ECU的某些功能块

这四个模块里,DCM是UDS应用层的全部实现,也是我们接下来的焦点。


三、DCM内部的三层架构——ISO 14229-1的直接映射

DCM内部进一步分解为三个子模块,对应ISO标准中"诊断请求受理→路由→服务处理"的三个阶段。可以把它类比成一家医院的门诊体系:

缩写
文件
代码量
核心职责
医院类比
会话层
DSL
Dcm_Dsl.c
1828行
缓冲区管理、Rx/Tx状态机、S3超时、协议抢占
门诊前台——挂号、分配候诊室、到期踢人
调度层
DSD
Dcm_Dsd.c
915行
SID表查找、会话/安全鉴权、子功能分发、正/负响应创建
分诊护士——根据病人的"主诉"判断挂哪个科的号,检查有没有权限
执行层
DSP
Dcm_Dsp.c
6492行
全部22个UDS服务的业务逻辑实现
每个科室的医生——真正执行诊断操作

Dcm是总控——Dcm_InitDcm_MainFunction调用这三个子模块的入口,保证执行顺序是DspPreDsdMain → DsdMain → DspMain → DspTimerMain → DslMain

DSL(会话层)——不仅仅是收发缓冲区

这是DCM的最外层,直接面对传输层(PduR)。DSL的核心是一个缓冲区的六态机:

NOT_IN_USE → 

IN_USE → 

PROVIDED_TO_PDUR → 

DSD_PENDING_RESPONSE_SIGNALED → 

DCM_TRANSMIT_SIGNALED → 

PROVIDED_TO_PDUR (发送中) → 

PENDING_BUFFER_RELEASE → 

NOT_IN_USE

这六种状态分别对应UDS请求从"传输层开始接收"到"DCM处理完成、回传响应、释放缓冲"的全部生命周期。DSL有两个Rx缓冲区和两个Tx缓冲区——一个"外部"(大块数据,给传输层的PDU),一个"内部"(本地小buffer,给响应NRC和responsePending等不需要大缓冲的场景)。DSL还负责:协议启动/停止回调链、会话切换时调用DspResetDiagnosticActivityOnSessionChange、功能寻址和物理寻址的统一管理。

DSD(调度层)——SID表查找器

DSD的核心数据结构是一个SID查找表——DsdServiceTable[]——每一行绑定一个SID、可选的子功能表、会话和安全的鉴权引用表、以及处理函数的函数指针。AUTOSAR栈通常支持三种协议同时运行——CAN、FlexRay、DoIP——每种协议有自己的DsdServiceTable实例。DSD的主函数DsdHandleRequest()流程:

1. 从 Rx buffer 读出 SID (pduRxData->SduDataPtr[0])

2. lookupSid(sid) —— 在 SID 表中线性搜索

3. 找到后调用 DspCheckSessionLevel —— 当前会话是否允许这个SID

4. 再调用 DspCheckSecurityLevel —— 当前安全等级是否允许

5. 如果有子功能码——调用 DsdLookupSubService() 查找子功能配置

6. 最终调用 selectServiceFunction → runInternalService(SID) —— 这是一个巨大的 switch(SID) 分发到 DspUds* 函数

DSP(执行层)——所有UDS服务在这里变成C代码

Dcm_Dsp是DCM最大的文件——它实现了全部22个UDS服务的业务逻辑。Dsp看到的每一个DspUdsXxx()函数,都直接对应一个SID:

// DspUdsDiagnosticSessionControl(0x10) —— 会话切换

// DspUdsEcuReset(0x11) —— ECU复位

// DspUdsSecurityAccess(0x27) —— Seed/Key

// DspUdsReadDataByIdentifier(0x22) —— 按DID读

// DspUdsWriteDataByIdentifier(0x2E) —— 按DID写

// DspUdsReadDtcInformation(0x19) —— 读DTC

// DspUdsRoutineControl(0x31) —— 例行控制

// ... 总计 22+ 个服务处理函数

这就引出了下面这个核心问题:DSP里究竟要处理哪些服务?这些服务又是怎么来的?


四、UDS服务的完整体系

主谓宾结构:C/S模型的固定语法

在UDS通信里,有两句话永远成立:

• 1. 诊断仪发起请求,ECU返回响应。 没有例外。ECU永远不会主动发起一个UDS请求——它只答复。

• 2. 每一个请求对应至多一个响应。 要么正响应(成功+数据),要么负响应(失败+原因码),要么(在功能寻址+suppressPosRsp条件下)无响应。

这在形式上非常类似HTTP的Request-Response模型——但UDS更加严格地遵守"一请求、一响应"。HTTP有Server-Sent Events、WebSocket升级、100 Continue中间响应的概念——UDS没有任何"流式响应"或"推送"机制。ResponseOnEvent(0x86)是ECU"主动发送数据"的唯一方式,但这也严格绑定到诊断仪先在0x86请求里注册事件——是准主动。

全部26个SID一览

SID (hex)
正响应SID
服务名称
子功能
一句话概括
0x10
0x50
诊断会话控制
挂号——切换诊断会话(默认/编程/扩展/安全系统)
0x11
0x51
ECU复位
重启——hardReset/keyOffOnReset/softReset
0x14
0x54
清除诊断信息
删除病历——按groupOfDTC清DTC/快照/扩展数据
0x19
0x59
读DTC信息
查看症状——18种子功能覆盖DTC的所有查询维度
0x22
0x62
按标识符读数据
开化验单——读一个或多个DID的当前值
0x23
0x63
按地址读内存
按物理地址读内存,走地址+长度格式
0x24
0x64
按标识符读缩放数据
读DID的缩放定义(物理单位和范围)
0x27
0x67
安全访问
病历授权——Seed/Key解锁安全等级
0x28
0x68
通信控制
屏蔽噪音——关闭/开启ECU特定类型的通信
0x2A
0x6A
按标识符周期读取
持续监控——让ECU定期自动上报DID值
0x2C
0x6C
动态定义DID
自定义化验单——诊断仪临时定义DID的组成
0x2E
0x6E
按标识符写数据
调整参数——写一个DID的值(覆盖当前值)
0x2F
0x6F
按标识符IO控制
触诊——短期替代某个DID值以观察系统行为
0x31
0x71
例行控制
治疗——启动/停止/获取例行程序的结果
0x34
0x74
请求下载
手术准备——声明数据格式/地址/长度,获取下载授权
0x35
0x75
请求上传
上传准备——声明数据格式/地址/长度,获取上传授权
0x36
0x76
传输数据
手术传送——传输实际的数据块(下载或上传)
0x37
0x77
请求传输终止
手术结束——完成数据传输,验证并退出
0x38
0x78
请求文件传输
文件系统操作——传输文件。实际量产极少使用
0x3D
0x7D
按地址写内存
按物理地址写内存,直接操作非易失存储
0x3E
0x7E
诊断仪保活
有(子功能控制是否回复)
医生还在——诊断仪发心跳防会话超时
0x83
0xC3
访问时序参数
协商时序——动态修改P2/P2*超时
0x84
0xC4
安全数据传输
加密隧道——在逐服务基础上传输加密数据
0x85
0xC5
控制DTC设置
暂停记录——关闭/开启DTC状态位更新
0x86
0xC6
事件响应
事件驱动推送——让ECU在特定事件发生时主动发数据
0x87
0xC7
链路控制
波特率切换——K-Line遗留功能

*注:0x23、0x24、0x83、0x84、0x87这5个SID在量产诊断场景中极少使用。0x23被0x22替代(语义化寻址);0x24依赖ODX文件而非在线查询;0x83的P2/P2*值通常在0x10正响应中一次协商;0x84协议栈复杂且多数ECU未实现;0x87为K-Line波特率切换,在CAN/DoIP时代已基本废弃。*

每一个SID都有"为什么存在"的故事

从历史发展的角度看,这26个SID不是一次性设计的——它们是逐层堆叠的:

• 第一波(OBD-II遗留,1996):0x22读数据、0x19读DTC、0x14清除DTC——这是OBD-II Mode 0x01、0x03和0x04的UDS化。排放诊断必须得有这些。

• 第二波(KWP2000进化,2000):0x10会话控制、0x11 ECU复位、0x27安全访问——KWP2000把"修车不仅限于看故障码"的需求带了进来。要执行主动治疗和安全操作,得先挂号、先授权。

• 第三波(CAN适配,2003-2006):0x34/0x35/0x36/0x37下载上传、0x28通信控制、0x3E保活——CAN总线带来了大块数据分段传输和共享总线通信管理(刷写时关掉应用报文)的需求。0x3E则独立应对S3会话超时。

• 第四波(现代汽车电子,2013+):0x2F IO控制、0x31例行控制、0x2A周期读取、0x86事件响应——控制功能的深化。诊断已从"读故障码修车"变成通过诊断接口重新标定、重新编程、重新校准整辆车。

为什么不合并? 为什么有0x22还要0x23?为什么0x84独立于0x27?为什么0x86不和0x2A合并?

• 0x22 vs 0x23(DID读取 vs 地址读取):0x22面向应用层——DID是语义化命名;0x23面向存储层——直接按物理地址读。前者是"给我发动机转速",后者是"给我0x40024000的内容"。两者面对不同的角色和安全边界。

• 0x27 vs 0x84(安全访问 vs 安全数据传输):0x27是会话级鉴权,解决"你是谁";0x84是消息级加密,解决"你的消息是否被偷看/篡改"。两者正交。

• 0x2A vs 0x86(周期读取 vs 事件响应):0x2A是定时器驱动("每500ms发一次"),0x86是事件驱动("DTC状态变化时发给我")。这是时间轴上两种不同的采样模式。

核心洞察:每一个"为什么不合并"背后都有一个两难权衡——合并可以简化接口,但会混淆语义,导致一个SID承担太多不相关的职责。这26个SID被ISO委员会认定为最小充分集合。

哪些仅能在非默认会话中使用

UDS有一套严格的访问控制矩阵,确保普通OBD-II扫描仪不能意外触发敏感操作:

会话限制
SID
任何会话都可用(包括默认)
0x10 会话控制、0x11 复位、0x3E 保活、0x14 清除DTC、0x19 读DTC、0x22 读DID、0x23 读地址、0x24 读缩放、0x2E 写DID、0x3D 写地址、0x86 事件响应
仅非默认会话(扩展/编程/安全)
0x27 安全访问、0x28 通信控制、0x2A 周期读、0x2C 动态DID、0x2F IO控制、0x31 例行控制(受安全限制)、0x34/35/36/37/38 上下传、0x83/0x84/0x85/0x87

这张表就是UDS"挂号"逻辑的实质:默认会话是一个公共可读空间,但要写、控制、做手术,必须挂到其他号。

SID地图的医院化映射

诊断阶段
UDS服务
医院映射
什么时候调用
挂号
0x10
选择科室
每次诊断会话的第一条请求
心跳
0x3E
医生巡视
防止session过期
授权
0x27
获取病历权限
敏感操作之前
查病史
0x19
读病历
收集DTC及背景数据
开化验
0x22
抽血/拍片
实时读取传感器、配置等
自定义化验
0x2C
自拟化验项目
临时定义DID组合
持续监测
0x2A
24小时心电图
让ECU周期上报数据
触诊
0x2F
按压、叩诊
临时假写参数观察反馈
治疗
0x31
执行治疗方案
触发ABS自检、ADAS标定
调参
0x2E
调整药量
修改标定值、配置参数
手术准备
0x34
手术授权
声明刷写地址和长度
手术进行
0x36
输血/移植
传送每一块固件数据
手术结束
0x37
拆线/缝合
校验完整性,退出传输模式
清除病历
0x14
病历更新
修好后清除已完成症状记录
重置病人
0x11
让病人重启
刷写后复位到正常状态
暂停监控
0x85
暂停信号采集
防止刷写中误报DTC
屏蔽噪音
0x28
关掉多余监护器
刷写期间停掉应用报文
事件推送
0x86
护士按铃
监控到异常自动通知医生

五、贯通:从需求到代码的完整链路

你会发现:

• 稀缺能力的第一条"把功能安全落实到代码细节",在诊断栈里就体现为:DSP服务处理中显式调用E2E保护、DSD的会话/安全鉴权矩阵严格配置、DSL缓冲区管理能正确处理S3超时和响应Pending。

• 第二条"多核异构SoC",则对应着诊断栈在锁步核、高性能核上的不同部署策略,以及跨核诊断通信的安全设计。

• 第三条"软硬结合调试",正是当偶发CAN错误导致诊断响应丢失时,能立刻沿着"物理层波形→DSL状态机→DSD查表→DSP逻辑"这条链一路排查到底的能力。

• 第四条"车载OS与中间件",则直接指向AUTOSAR CP/AP诊断栈本身的深度掌握。

加入群聊可获得

面试

资料

100+企业面试案例

学习

路线

汽车开发学习路线

学习

指导

多名10年+大厂经验

工程师在线指导

学习

交流

众多开发者一起交流

助力你提升技能

王老师的小程序👇👇点击即达!!!
扫码直接进入汽车嵌入式交流群