news 2026/9/8 12:37:34

CAN与UDS车载诊断协议开发实战:从底层通信到上层刷写全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CAN与UDS车载诊断协议开发实战:从底层通信到上层刷写全解析

车载底层 CAN 和上层 UDS,这两块东西拆开看都不算难,但把它们串起来做成一整套嵌入式诊断方案,坑是真的不少。这份文档不是教科书搬运,是我从实际项目里抠出来的东西,面向正在做 ECU 底层软件开发、车载测试、或者刚入门想快速建立完整知识体系的朋友。你如果已经把 CAN 收发调通了,但一碰到诊断服务、刷写流程就发怵,那这篇文章应该能帮你省掉不少弯路。

先说清楚整条链路:CAN 总线是汽车电子里的“神经系统”,负责把各个 ECU 的报文可靠地传出去;UDS 则是跑在这套神经系统上的“业务语言”,诊断仪发一个请求,ECU 解析完回一个响应,整个过程从上到下分了好几层。很多初学者最大的问题,是只盯着某一个服务去背协议,结果真上了板子,报文发不出去、响应回不过来、NRC 码乱飞,完全不知道从哪里查起。所以我会从底层通信一直讲到上层协议,再把常见故障和排查手段串一遍。

1. 整体架构与技术栈梳理

1.1 CAN 和 UDS 在车载软件栈中的位置

车载 ECU 之间的数据交换,最底层就是 CAN 控制器和 CAN 收发器。CAN 控制器负责协议解析、错误检测、仲裁这些脏活累活,收发器负责把逻辑电平转换成总线上的差分信号。再往上,是 ISO 15765-2 定义的 CAN 传输层(CAN TP),它把超过单帧容量的诊断数据拆成多帧发送,接收端再拼回来。最上面才是 ISO 14229 定义的 UDS 应用层,也就是我们常说的 0x10、0x19、0x22、0x27、0x31、0x34 这些服务。

理解这个分层极其重要。我遇到过不少同事,UDS 服务明明写得挺对,但响应就是不对,最后查来查去发现是 CAN TP 拆包没对齐,或者单帧和流控帧的时序错了。可以把这个模型理解成寄快递:CAN 是物流卡车,CAN TP 是打包分拣中心,UDS 是你写在包裹上的商品说明。商品本身没错,但卡车坏了、分拣错了,收件人照样收不到货。

1.2 为什么诊断协议要独立于底层通信

把 UDS 和 CAN 剥离开设计,核心原因是可移植性和可测试性。同一个诊断栈,今天可能跑在 STM32 上,明天可能就要移植到瑞萨或者英飞凌的芯片上,甚至底层换成 CAN FD 或者以太网 DoIP,应用层服务逻辑不应该大改。只要抽象好收发接口,比如提供一个uds_send_message(uint8_t* data, uint16_t len)uds_receive_indication(),上层就完全不关心底层是 CAN 还是别的。

另外一个好处是单元测试好写。你在 PC 上模拟一个 CAN TP 的收发环境,然后把 UDS 服务层放进去跑,逻辑对不对几秒钟就能验出来。如果服务和通信搅在一起,每次测一条服务都得搭硬件环境,效率低得没法看。所以我的建议是,哪怕项目很小,也把can_drvcan_tpuds_service分成三个目录建工程。

1.3 常用开发语言与硬件平台参考

传统车载底层开发里,C 语言仍然是绝对主力。原因很直接:编译结果可控、内存占用可预估、实时性强,而且芯片厂商的 SDK 和 AUTOSAR MCAL 基本都是 C 接口。现在也有用 Rust 做原型验证的,但量产项目里还是少见,建议一门心思把 C 撸熟。

硬件平台方面,常见的几类可以列一下做个参考:

平台特点常用场景
STM32F103/F407生态成熟、资料多、价格友好原型验证、教学、小批量工具链
S32K1xx车规级、CAN FD 支持好车身控制器、网关
TC2xx/TC3xx高性能、多核、ASIL-D动力域、自动驾驶域控制器
TJA1043 / TJA1145经典 CAN 收发器,带唤醒节点通信、低功耗场景

2. 底层 CAN 通信开发核心要点

2.1 CAN 帧格式、仲裁机制与 DLC 的理解

CAN 报文有标准帧和扩展帧两种格式,标准帧 ID 是 11 位,扩展帧是 29 位。实际项目里诊断报文一般用扩展帧的居多,因为 29 位地址空间里可以编码更多信息,比如源地址、目标地址、报文类型。普通控制报文像车速、转速这些,很多时候标准帧就够了。

仲裁机制是 CAN 通信里最容易被忽视但又最迷人的部分。多个节点同时发送时,总线通过 ID 的逐位仲裁决定谁先发,ID 数值越小优先级越高。这个过程没有中央调度器,完全是分布式的,靠 CAN 控制器的硬件实现。为什么 ID 小的优先级高?因为显性电平(逻辑 0)会覆盖隐性电平(逻辑 1),仲裁时先发 0 的节点自然就赢下了总线。这也是为什么 ECU 之间的关键报文会把 ID 设计得比较小,保证关键时刻抢得到总线。

DLC 表示数据长度,经典 CAN 是 0 到 8 字节。这里有个非常实战的细节:发送时 DLC 代表实际发送长度,但接收时不一定要完全匹配,很多控制器会填充 0xAA 之类的垃圾字节,不是说有效数据就有 8 个字节。处理逻辑里千万要做字节长度判断,不要想当然地全量解析。

2.2 波特率、采样点与 SJW 怎么配置才靠谱

CAN 通信质量的好坏,一大半取决于波特率和采样点配置。波特率决定 1 秒能发多少位,这个好理解,真正容易翻车的是采样点。CAN 总线上每一位时间由同步段、传播段、相位缓冲段 1、相位缓冲段 2 组成,采样点必须落在位时间的某个位置,通常建议配置在 75% 到 87.5% 之间。太靠前抗干扰差,太靠后又容易采到下一帧的信号边沿。

常见的配置可以参照下面这个表:

波特率总线长度参考适用场景
125 kbps500 m 左右车身舒适系统
250 kbps250 m 左右诊断、动力部分节点
500 kbps100 m 左右动力传动、网关
1 Mbps40 m 以内高速实时控制

开发时用示波器或者 CAN 分析仪抓波形,观察一位时间的长度、显隐电平的跳变沿是否干净,基本就能判断采样点合不合理。改采样点不只是改一个寄存器那么简单,不同芯片的位定时段长不一样,要根据芯片手册的公式反推预分频、同步段和相位缓冲段参数。

2.3 STM32 平台 CAN 初始化和滤波配置实操

用 STM32 HAL 库做 CAN 初始化,核心代码量其实不大,但配置项不能漏。下面是一个我在项目中常用的初始化片段:

CAN_HandleTypeDef hcan; hcan.Instance = CAN1; hcan.Init.Prescaler = 6; hcan.Init.Mode = CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_13TQ; hcan.Init.TimeSeg2 = CAN_BS2_2TQ; hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.AutoBusOff = ENABLE; hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE; hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); }

这里的预分频和数据能算出来:如果 APB1 外设时钟是 42 MHz,预分频 6 得到 CAN 时钟 7 MHz,一个时间量子是 1/7 MHz ≈ 143 ns。TimeSeg1 + TimeSeg2 + SyncSeg = 13 + 2 + 1 = 16 TQ,所以波特率是 7 MHz / 16 = 437.5 kbps,接近 500 kbps 需要微调分频系数,实际项目以协议要求为准。

滤波配置是新手最容易懵的地方。CAN 滤波器的作用是只接收关心 ID 的报文,减少 CPU 中断负担。STM32 的滤波模式有掩码模式和列表模式,掩码模式用 ACCMASK 指定哪些位必须匹配,哪些位不管。比如只接收诊断物理寻址报文 0x18DA10F1,掩码设为 0x1FFFFFFF,表示全部匹配。如果还想接收功能寻址 0x18DB10F1,可以把掩码最后几位放开,让不同源地址的报文都能进来。

CAN_FilterTypeDef filter; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x18DA >> 16; filter.FilterIdLow = 0x18DA & 0xFFFF; filter.FilterMaskIdHigh = 0x1FFF >> 16; filter.FilterMaskIdLow = 0x1FFF & 0xFFFF; filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan, &filter);

注意不同芯片的滤波器位宽不一样,不是所有芯片都把 32 位直接拆成高 16 位和低 16 位,具体要看参考手册。动手前先画一张滤波位映射图,比盲写代码要稳。

2.4 CAN FD 和经典 CAN 的差异,以及升级注意点

CAN FD(CAN with Flexible Data-rate)和经典 CAN 最大的区别有两个:一是数据段最长可以到 64 字节,二是数据段可以用更高的比特率传输。仲裁段为了保证总线兼容性,还是用标准波特率,但数据段可以用 2 Mbps、5 Mbps 这样更高的速率。

升级到 CAN FD 不是改个寄存器就完事。第一,总线上所有节点都得支持 CAN FD,否则兼容性协议只允许在经典 CAN 模式下通信。第二,CAN FD 的位定时和采样点可能需要独立配置,收发器的转换速率也要考虑。第三,如果你的诊断栈是基于 ISO 15765-2 经典 CAN TP 写的,要做 CAN FD 支持,就得处理更长包的分包策略,比如原来超过 7 字节就要多帧,现在单帧能塞 64 字节,很多分支判断逻辑会变。

3. 上层 UDS 诊断协议开发拆解

3.1 UDS 协议栈的基础:服务 ID、寻址方式和 CAN TP 分包

UDS 互不通信的核心是一个 8 位的服务 ID(SID),请求和响应用同一个 SID,只是响应的最高位置 1。比如请求读取数据是 0x22,肯定响应就是 0x62;请求会话控制是 0x10,肯定响应是 0x50。这条规则记牢,看日志时一眼就能认出哪条是请求哪条是响应。

寻址方式分物理寻址和功能寻址。物理寻址是 1 对 1,诊断仪发给某个特定 ECU,ECU 必须回复;功能寻址是 1 对多,比如 0x18DBxxxx 这样的 ID 广播给一组 ECU,这些 ECU 收到以后只执行不回复,否则总线上会同时出现一堆响应,冲突到没法看。

CAN TP 分包逻辑在诊断协议里非常关键。经典 CAN 单帧最多 8 字节,去掉 CAN TP 的头部信息(PCI),真正能放数据的就 7 字节。超过 7 字节的诊断请求和响应就必须多帧传输:第一帧叫单帧,直接带数据;多帧场景下第一帧是首帧(FF),通知接收方总共要传多少字节,接收方如果准备好了就回一个流控帧(FC),然后发送方按照块大小把后续数据拆成连续帧(CF)发过去。

请求数据: 02 10 03 CAN TP 单帧: [0x02] 0x10 0x03

这个0x02就是 PCI 字节,高 4 位 0 表示单帧,低 4 位 2 表示后续数据长度 2 字节。多帧首帧的 PCI 又是另一种编码,C 语言实现里要分清楚,最好封装一个can_tp_receive()状态机。

3.2 常用 UDS 服务逐个拆开讲:10、22、2E、19、14、31

UDS 服务里面最常用的几个,建议达到随手能背的程度。

0x10 服务是会话控制,子功能有 01 默认会话、02 编程会话、03 扩展会话。ECU 上电默认通常停在默认会话,很多诊断服务必须切到扩展会话或者编程会话才能用。切会话的同时往往要配合 0x27 安全访问,否则就是白搭。

0x22 和 0x2E 是一对,读数据和写数据。它们用 DID(数据标识符)区分要访问的具体数据,比如 0xF190 是 VIN 号,0xF187 是 ECU 软件版本号。读取请求的格式是22 F1 90,正常响应就是62 F1 90后面跟数据。写数据的流程类似,但要注意很多 DID 是只读的,写不了就得回 NRC。

0x19 是读取 DTC 信息,子功能很多,比如 01 按状态掩码读取 DTC,02 按掩码读取 DTC 快照,04 读取 DTC 扩展数据。DTC 本身是 3 字节,比如动力系统故障码 P0100 对应到 UDS 里的编码是00 01 00。这个服务在售后诊断、产线测试里用得非常频繁。

0x14 是清除 DTC。清除 DTC 的触发条件比较苛刻,一般要求先进入扩展会话,而且车辆状态要满足一定条件,比如车速为 0、发动机转速在安全范围内。如果条件不满足,ECU 会回 NRC 0x22(条件不满足)。

0x31 是例程控制,子功能有 01 启动例程、02 停止例程、03 查询例程结果。例程可以理解成 ECU 内部的一段可触发程序,比如擦除 Flash、复位学习值、执行传感器自测。例程控制请求里要带例程标识符(RID),不同 RID 代表不同的功能。

0x34、0x36、0x37 是刷写三件套:请求下载、传输数据、请求退出传输。后面单独讲刷写流程。

下面这个表可以把服务统一收一下:

SID名称常见子功能 / 用途
0x10DiagnosticSessionControl01 默认、02 编程、03 扩展
0x27SecurityAccess01/02 请求种子和发送密钥
0x22ReadDataByIdentifier按 DID 读取数据
0x2EWriteDataByIdentifier按 DID 写入数据
0x19ReadDTCInformation01/02/04 等读取 DTC
0x14ClearDiagnosticInformation清除 DTC
0x31RoutineControl01 启动、02 停止、03 查询
0x34RequestDownload请求刷写下载
0x36TransferData传输固件数据
0x37RequestTransferExit结束传输
0x11ECUReset01 硬复位、02 软复位、03 快断电

3.3 安全访问 27 服务的种子与密钥机制

安全访问是 ECU 的一道锁。很多关键操作,比如刷写、写配置、执行特殊例程,都要求先通过 0x27 服务完成安全校验。基本流程是:诊断仪先发27 01请求种子,ECU 收到后回67 01加一个种子数据;诊断仪拿到种子以后按算法生成密钥,发27 02带密钥,ECU 校验正确则回67 02

种子通常是 4 字节或 2 字节,密钥算法完全是 OEM 和供应商自己定的,协议规范里不会给你。有的是简单异或,有的是 CRC 查表,有的是 AES 的某个变体。开发时经常要跟后端或安全团队对算法,先在 PC 工具里验证对了,再烧到 ECU 上联调。

这里有几个实战注意点。第一,安全访问有尝试次数限制,错误次数太多 ECU 会锁定一段时间,锁定时间从 10 秒到几十分钟都有,调试时别手滑发太多错误请求。第二,种子往往和 ECU 内部的随机数或者时间戳绑定,同一个 ECU 每次请求种子都不一样,不能拿上次算好的密钥硬发。第三,实现时要把安全状态变量做成全局,不能每次在服务回调里临时算一下,否则会被绕过。

static uint8_t security_level = 0; static uint8_t security_seed[4]; void uds_27_handler(uint8_t* data, uint8_t len) { if (data[0] == 0x01) // request seed { generate_seed(security_seed); security_level = 1; send_response(0x67, 0x01, security_seed, 4); } else if (data[0] == 0x02) // send key { uint8_t computed_key[4]; calculate_key(security_seed, computed_key); if (memcmp(computed_key, &data[1], 4) == 0) { security_level = 2; send_response(0x67, 0x02, NULL, 0); } else { send_nrc(0x27, 0x35); security_attempt_count++; } } }

3.4 UDS 刷写全流程:从编程会话到检查点

刷写流程是诊断开发里最完整也最容易出问题的一环。标准刷写步骤大概是这样:

  1. 进入编程会话:发送10 02,ECU 切到编程模式,关闭应用层功能。
  2. 安全访问:发送27 0127 02,通过安全校验。
  3. 写入指纹(有的 OEM 要求):用2E F1 0C之类写入刷写者信息。
  4. 请求下载:发送34 00 44加地址和长度信息,ECU 回一个最大块长度。
  5. 传输数据:循环发送36 0136 02加数据块,块大小以 34 服务返回的值为准。
  6. 请求退出传输:发送37,ECU 做完整性检查,可能回77表示等待,之后再确认。
  7. 检查点与复位:有些流程要求做例程控制擦除、写应用有效性标志,最后发11 01复位 ECU,让新程序运行起来。

检查点流程是 OEM 厂特别看重的东西。刷写过程中每完成一个阶段,ECU 就要把状态记录到非易失存储区,防止中途断电后变砖。比如我遇到过的一个项目:刷写到 70% 的时候测试人员直接断电,重新上电后 ECU 还能通过 bootloader 接着刷,这就是检查点做得好。检查点实现要把握好时机,不能每写一块 Flash 就存一次状态,否则寿命和性能都扛不住。

3.5 UDS 的 NRC 错误码到底怎么读

NRC(Negative Response Code)是 ECU 拒绝请求时的回复码,藏在7F响应里。比如请求22 F1 90但 DID 不存在,ECU 回7F 22 31,意思是服务 0x22 拒绝,原因 NRC 0x31(请求超出范围)。

常用 NRC 列一张表:

NRC名称常见触发场景
0x11serviceNotSupportedSID 不存在
0x12subFunctionNotSupported子功能不支持
0x13incorrectMessageLengthOrInvalidFormat报文长度错误
0x22conditionsNotCorrect条件不满足
0x24requestSequenceError顺序错误,比如没进会话直接发安全访问
0x31requestOutOfRange参数超出范围
0x33securityAccessDenied未通过安全访问
0x35invalidKey密钥不匹配
0x72generalProgrammingFailure编程/刷写失败
0x78requestCorrectlyReceivedResponsePendingECU 还在处理,请稍等

NRC 0x31 是出现频率最高的一个。它不一定是参数写错了,很可能是条件不满足。比如地址长度和实际 Flash 区域不匹配、刷写地址没按对齐要求、数据块长度不对等。排查 NRC 0x31 的正确姿势,是先看日志里请求帧的完整字节,再对照 ECU 配置表,千万别只盯着最后一个字节猜。

4. 常见问题排查与实战技巧汇总

4.1 典型故障速查表

下面这个表是我做项目时遇到最多的问题,按症状整理了一下排查路径:

症状可能原因排查建议
总线上抓不到任何报文波特率不匹配、收发器供电异常、CAN_H/CAN_L接反先用分析仪监听,对比波形确认电平
能收到别人的报文,自己发不出去未进入 Normal 模式、滤波器配置错误、总线忙检查控制器状态寄存器,关滤波试一下
报文间歇性丢失采样点偏移、总线过长、终端电阻缺失抓波形看边沿和毛刺,检查 120 欧终端
UDS 请求超时无响应物理寻址 ID 不对、CAN TP 解析异常、ECU 没进对会话抓 CAN 层日志,看请求是否到达 TP 层
ECU 回 7F 31参数越界、地址不对、条件不满足核对 DID/RID/地址长度配置,翻 OEM 规范
刷写中途失败块长度超过 ECU 限制、Flash 擦除超时、检查点状态错乱先测小数据块,再逐步增加,核对 34 服务返回值
安全访问一直回 0x35密钥算法错误、种子有效期太短用 PC 工具离线算一遍,确认 seed 变化逻辑

4.2 CAN 波形怎么看,通信质量好坏怎么判断

波形分析不是示波器厂商的玄学,它是定位物理层问题的直接武器。用示波器双通道抓 CAN_H 和 CAN_L,正常情况下显性电平时 CAN_H 大概 3.5 V,CAN_L 大概 1.5 V,差分约 2 V;隐性电平时两个都约 2.5 V,差分约 0 V。

判断通信质量主要看这几处:信号边沿是否陡峭、是否有回勾和毛刺、位时间的长度是否均匀、采样点附近的电平是否稳定。如果边沿缓、上升时间长,就要怀疑终端电阻或者线缆分布电容;如果显性电平幅度不够,可能是收发器驱动能力不行或总线节点太多;如果波形上出现明显的台阶,通常是多个节点同时发送导致位冲突,这在仲裁阶段是正常现象,但要是数据段也出现就要查一查。

我曾经排查过一个棘手问题:当整车电压掉到 9 V 以下时,个别 ECU 诊断响应时好时坏。示波器抓过去,发现低电压下收发器的隐性电平从 2.5 V 掉到了 2.0 V 左右,导致远端的接收节点识别不了。这不是软件能解决的,最后换了低压性能更好的收发器型号才解决。

4.3 一次 UDS 刷写失败排查实录

有一次客户反馈刷写程序到 80% 左右必定失败,复现率非常高。第一反应先抓 CAN 总线日志,看到第 2000 个左右的数据帧之后,ECU 回了一个7F 36 72(generalProgrammingFailure)。

排查过程分几步走。第一步,确认失败位置是不是固定的。结果发现每次失败时的总数据量不同,但都集中在某个 Flash 扇区附近。第二步,查看 Flash 驱动擦写逻辑,发现该扇区是 bootloader 用来存检查点信息的保留区域,应用刷写禁止覆盖。第三步,回到上层,看 34 服务请求下载的地址范围,发现上位机把整个 Flash 区域都当成应用区了,没有排除保留扇区。第四步,修正上位机刷写地址范围后,问题消失。

这个案例说明,UDS 刷写的问题往往不是协议栈本身,而是地址空间划分、上位机和 ECU 双方对内存布局的理解不一致。排查时一定要把 34 服务里的地址和长度字段解码出来,和 ECU 的内存映射表逐项核对。

4.4 SocketCAN 和 CAN 分析仪配合使用的调试心得

Linux 环境下用 SocketCAN 做验证是效率最高的方式之一。起一个虚拟 CAN 接口,配好波特率,再用 candump 监听,几分钟就能把协议栈跑起来。常用命令顺手列一下:

sudo ip link set can0 up type can bitrate 500000 candump can0 cansend can0 18DA10F1#021003

如果要用 PC 上的 CAN 卡,周立功、PCAN 这些都各有特点。周立功的驱动在部分 Windows 11 上会有兼容问题,表现为设备管理里显示感叹号或者软件打不开。解决方法一般是去官网下最新的驱动安装包,关掉驱动强制签名校验,或者干脆换一台老的测试机器。这类工具问题说起来不大,但卡一下午也确实让人头大。

调试诊断协议时,我会同时开两个视角:一个是 CAN 层的原始报文窗口,看帧 ID、DLC、数据是否正常;另一个是 UDS 层的解码窗口,把 PCI 去掉后直接看 SID 和 NRC。两边对照着看,能很快定位问题是在通信层还是在应用层。

4.5 DTC 状态位的含义到底怎么理解

DTC 状态字节是一个 8 位的状态掩码,每一位都代表一个独立的故障状态。最常用的几个位是:Bit 0 表示测试失败,Bit 1 表示当前故障,Bit 2 表示待处理故障(pending),Bit 3 表示已确认故障(confirmed),Bit 4 表示上次测试未完成,Bit 5 表示上次测试失败,Bit 6 表示警告指示灯请求,Bit 7 表示已请求更新。

比如收到的状态字节是0x19,二进制是00011001,说明 Bit 0、Bit 3、Bit 4 置位:这个故障当前测试失败,而且已经确认过,但上一次测试没跑完。开发 DTC 逻辑的时候,对每一位的置位和清位时机要定义清楚,特别是 confirmed 和 pending 的切换规则,很多项目就是在这个地方返工。

地址和状态位的配合也很关键:19 服务子功能 02 按状态掩码读取 DTC 时,你传的掩码直接决定返回哪些故障。掩码传 0xFF 是返回全部,传 0x0A 就只返回当前故障和已确认故障。这些细节在排查售后数据时特别有用。

5. 工具链、代码结构和项目落地的几点建议

5.1 CAN ISO TP 状态机实现建议

如果你的项目不用现成的 AUTOSAR 协议栈,而是自己写 CAN TP,建议把状态机按状态划分好。经典 CAN 的接收状态机至少要有:等待单帧、接收首帧、发送流控、等待连续帧、重组完成这几个状态。每个状态要有超时处理,超时时间一般取 200 ms 到 1 s 不等,不能无限等下去。

我自己实现过一个简化版本的收发结构:

typedef struct { uint8_t state; uint8_t buffer[4096]; uint16_t total_len; uint16_t received_len; uint8_t current_sn; uint8_t block_counter; } CanTpRx; typedef struct { uint8_t buffer[4096]; uint16_t total_len; uint16_t offset; uint8_t sn; } CanTpTx;

多帧接收时,连续帧的序号只占 4 位,范围是 0 到 15,如果中间漏了一帧,序号对不上,接收状态机就要立刻停止。调试时给我最大的教训是,不要把重组缓冲区和 UDS 服务层的数据区共用,否则一个多帧重组到一半,另一个诊断请求插进来,缓冲区直接被踩了。

5.2 诊断服务代码的组织方式

代码结构上,我用过回调表驱动的方式,效果很好。把每一个 SID 的服务函数指针放在一个静态表里,请求进来以后用 SID 查表,找到就调用,找不到就回 0x11。

typedef void (*uds_service_handler)(uint8_t* data, uint16_t len); const uds_service_handler service_table[256] = { [0x10] = uds_10_session_control, [0x22] = uds_22_read_data, [0x27] = uds_27_security_access, [0x2E] = uds_2E_write_data, [0x31] = uds_31_routine_control, // ... };

这样新增服务不用改主流程,在表里加一行加上实现函数就行。同时要注意,请求数据里的子功能有些需要抑制响应位(suppressPosRspMsgIndicationBit),即子功能最高位为 1 时 ECU 只执行不回复。这个位很多人会漏处理,导致诊断仪那边一直等待响应,最后超时。

5.3 对 AUTOSAR 和非 AUTOSAR 项目的看法

AUTOSAR 没你想象的那么可怕,但也不是万能的。在 AUTOSAR 架构里,CAN 驱动、CAN 接口、CAN TP、诊断服务(Dcm)都有一层层标准化的模块,配置工具生成代码后直接集成。优点是可配置性和 CA 一致性做得很好,缺点是学习曲线陡峭、调试门槛高,出了问题往往要翻一堆工具生成的代码。

对于中小团队或者原型项目,手写一个轻量级的 CAN TP + UDS 栈完全可行,稳定性和可维护性都能做到不错。关键是接口要清晰,日志要完善。无论用哪种方案,我建议把诊断日志做成统一的入口,每一帧请求和响应都带时间戳打出来,这样以后线上问题复现会轻松很多。

6. 最后再说两句实在话

做车载底层通信和诊断协议,说难也难,说简单也简单。难在它跨了物理层、数据链路层、传输层和应用层,每一层都可能出问题;简单在你只要把每一层的核心逻辑吃透、把工具链用熟,大部分问题都有规律可循。

我个人经验里最值钱的一条是:先看现象,再抓日志,最后才动代码。很多开发人员遇到问题直接改代码,改完发现现象还在,一查才发现是波特率不对或者 ID 配错了。正确的流程是先把 CAN 层的原始报文全部抓出来,确认请求有没有发出、ECU 有没有响应、NRC 是多少,再决定是改应用逻辑还是改驱动配置。

另外想说的是,诊断协议和底层通信的参数一定要文档化。项目的波特率、采样点、诊断物理寻址 ID、功能寻址 ID、安全访问算法版本、DID 映射表、刷写地址范围,这些信息散落在每个工程师脑子里,出一次人员变动就丢一半。我一般会用一个简单的 Markdown 或者 Excel 维护一份配置基线,每次改参数就更新,出了事故以后这是你最好的救命稻草。

这个项目后续如果要扩展,可以往 DoIP 以太网诊断方向走,UDS 服务层的逻辑几乎是完全复用的,只是底层从 CAN TP 换成 TCP/UDP,诊断服务的开发经验照样值钱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 12:37:28

Arm Trusted Firmware(ATF)架构解析与平台移植实战

做嵌入式底层的人,迟早得跟Arm Trusted Firmware(ATF)打交道。不管你是在调OP-TEE、看U-Boot从EL2跳转,还是排查Linux内核在EL1启动前的异常,ATF始终是绕不开的那一环。我最早接触它的时候还叫ARM Trusted Firmware&am…

作者头像 李华
网站建设 2026/9/8 12:37:22

LabVIEW RT目标上DLL与INI配置文件的部署与调用指南

把自定义 DLL 和 INI 配置文件部署到 LabVIEW RT(实时)目标,这个需求我遇到太多次了。很多工程师在 Windows 端把上位机跑通了,程序写得贼顺,一到要往 CompactRIO、PXI 或者工控机实时目标上部署时,就开始踩…

作者头像 李华
网站建设 2026/9/8 12:36:59

AI代理上下文工程:从生命周期到治理的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:36:41

轨道交通线网指挥中心:核心引擎、建设难点与工程实践

凌晨一点多,最后几班列车还在隧道里跑,线网指挥中心的大屏一片深蓝。值班长盯着晚点指标,手边的即时通信窗口里,几条线路的行车调度员几乎同时发来同样的问题:末班车延误能否顺延换乘等待?这一刻&#xff0…

作者头像 李华
网站建设 2026/9/8 12:36:05

多AGV调度系统架构设计与路径规划避碰策略实战

简介:多AGV调度系统软件是一套基于JAVA的自动化物流解决方案,适用于智能仓储与智能制造场景,面向需要研究多机器人协同调度、路径规划与任务分配的开发者、方案工程师及高校师生。软件包内含1260个文件,主要包括923个java源码、16…

作者头像 李华
网站建设 2026/9/8 12:35:31

Java对接NTP服务器实现高精度时间同步的完整指南

1. 项目概述与NTP接入的整体思路1.1 为什么要独立对接NTP服务器做Java开发时间久了,你会发现一个特别容易被忽略但又特别要命的问题:服务器时间不准。我最早是在一次日志排查中踩到坑的,两台应用服务器日志时间差了将近40秒,联调接…

作者头像 李华