STM32F103 OBD诊断仪源码解析:CAN总线通信与转速车速计算 简介一套基于STM32F103的汽车OBD诊断仪软件源码面向嵌入式开发者和汽车电子爱好者解决从整车CAN总线读取发动机转速、车速等实时数据的需求。项目基于标准外设库编写包含CAN收发、数据流PID扫描、串口打印等核心逻辑适合学习CAN总线协议解析和OBD-II诊断流程。压缩包内共83个文件以33个C源文件和34个头文件为主另有启动汇编、Keil工程文件uvproj及hex固件整体仅334KB目录结构清晰便于直接编译和烧录验证。目前已有1151人学习。代码中可通过16位滤波器配置和自定义CAN ID发送请求帧完成对整车数据流的扫描与读取并附有readme说明对理解车载网络通信和单片机外设驱动均有实际参考价值。1. 为什么做 OBD 诊断仪的 STM32F103 软件源码核心都在 CAN 线上很多人拿到“STM32F103 单片机汽车 OBD 诊断仪软件源码”这类压缩包之后第一反应是去翻 UI 代码第二反应是去查 PID 表。真正在整车上跑过一遍的工程师会有完全相反的结论这个项目的技术主线不在菜单不在液晶屏而在 STM32F103 的 CAN 控制器和整车 CAN 线之间那几根线、几组寄存器。发动机转速、车速这两个最常被问到的数据标准 OBD 模式 01 的服务里分别对应 PID 0x0C 和 0x0D请求走 CAN ID 0x7DF响应走 0x7E8 一类 ID。只要 CAN 收发器、波特率、验收滤波、请求帧和响应帧解析这五件事都做对了剩下的事情只是把这堆字节挪到串口屏或者上位机里。这篇文章就从整车 CAN 线讲起直到转速和车速真正被算出来最后给一个整车验证时防止被假数据骗过的方法。适合手里已经有 STM32F103 最小系统板、想把手伸进汽车诊断这个方向的开发者。2. 整车 CAN 线上挂的是 120Ω 电阻STM32F103 却不能直接挂上去2.1 OBD 插座上只有两根线是高速 CANCAN_H 和 CAN_L无论是家用车还是商用车OBD 诊断座按 SAE J1962 定义是 16 针其中跟高速 CAN 有关的只有 6 号针脚 CAN_H 和 14 号针脚 CAN_L。很多第一次做车载诊断的人会把 2、10 号针脚也当成 CAN那是老款 ISO 9141-2 和 ISO 14230-4 的 K 线和标题说的“整车 CAN 线”不是一回事。CAN 总线是差分传输CAN_H 和 CAN_L 之间的电压差决定总线状态。显性位时 CAN_H 标称 3.5V、CAN_L 标称 1.5V差分电压约 2V隐性位时两个信号都接近 2.5V差分电压约 0V。这个特性使得 CAN 的抗干扰能力远强于普通串口但也意味着用单片机 GPIO 直接去读这根线是读不出稳定电平的。另外值得一提闭合点火开关去量 OBD 插座 6 号和 14 号之间的电阻正常应该在 60Ω 左右这是总线上两个 120Ω 终端电阻并联的结果。如果量出来接近 120Ω说明有一端终端电阻丢失如果接近 0Ω则要考虑有节点损坏或者线路短路。这一点放在整车验证章节里还会用上。2.2 PA11 和 PA12 是 TTL 电平必须经过收发器STM32F103 内部集成了 bxCAN 控制器但控制器输出的并不是可以直接上总线的差分电平。F103 的 CAN_RX 引脚默认是 PA11CAN_TX 是 PA12它们输出的是 3.3V 单端信号。把 PA11 和 PA12 直接接到 OBD 插座的 6 和 14 上是新手最容易犯的错误后果通常是 CAN 控制器一直报总线错误发送请求没有任何回应。正确做法是在两者之间增加一个 CAN 收发器芯片常见的有 TJA1050、MCP2551、SN65HVD230 等。收发器一边接 STM32F103 的 3.3V 逻辑电平另一边接 CAN_H 和 CAN_L。选型时主要看三个参数工作电压是否兼容 3.3V 逻辑、最高速率是否能到 1Mbps、是否有总线保护。收发器逻辑电压速率备注TJA10505V可接 3.3V 单片机最高 1Mbps经典款市面上货最多MCP25515V最高 1MbpsMicrochip外围简单SN65HVD2303.3V最高 1Mbps适合纯 3.3V 系统功耗低TJA10425V带待机模式最高 1Mbps低功耗场景多带唤醒注意无论用哪颗收发器CAN_L 和 CAN_H 对地的耐压范围和静电防护都达不到直接户外插拔的级别。批量产品建议在总线侧加 TVS 管至少也要用带内置保护的收发器型号。2.3 RXD 引脚悬空时的隐性电平不要想当然很多 STM32F103 最小系统板没有把 PA11 上拉默认状态下 CAN 控制器读到的电平不确定。加上整车 CAN 是显性优先的机制如果 RXD 在总线空闲时读到低电平会把空闲误判成持续的显性位进而导致发送失败。我一般会在 PA11 上加一个 10kΩ 上拉电阻到 3.3V这不算规范要求但能避免一部分接触不良和上电瞬间的电平抖动被误识别为总线错误。更严格的做法是用收发器的 S 或 STB 引脚控制静默模式调试阶段把 STB 拉低进入正常模式不要悬空。3. 从最小系统到 STM32F103 的 CAN 初始化500k 波特率和 0x7E8 验收滤波3.1 先配置时钟和引脚再操作 CAN 控制器STM32F103 的 CAN1 挂载在 APB1 总线上。常见的 72MHz 主频配置下APB1 被分频到 36MHz。初始化顺序应当是先开 AFIO 时钟和 GPIOA 时钟把 PA11 配成浮空输入、PA12 配成复用推挽输出然后再打开 CAN1 时钟、退出初始化模式。void CAN_GPIO_Init(void) { RCC-APB2ENR | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN; // 注意改完时钟配置后最好加一个空的读操作让写操作完成 GPIOA-CRH ~(GPIO_CRH_CNF11 | GPIO_CRH_MODE11); GPIOA-CRH | GPIO_CRH_CNF11_1; // PA11 浮空输入连 CAN_RX GPIOA-CRH ~(GPIO_CRH_CNF12 | GPIO_CRH_MODE12); GPIOA-CRH | GPIO_CRH_CNF12_0 | GPIO_CRH_MODE12_1; // PA12 复用推挽输出 10MHz }void CAN_Module_Reset(void) { RCC-APB1ENR | RCC_APB1ENR_CAN1EN; CAN1-MCR | CAN_MCR_INRQ; // 请求进入初始化模式 while (!(CAN1-MSR CAN_MSR_INAK)); // 等待初始化确认位 }这里把 GPIO 初始化和 CAN 初始化分成两个函数不是为了好看是因为整车环境里 CAN 收发器可能因为短路进入总线关闭状态后续排错时往往只重新初始化 CAN 控制器而不再动 GPIO。分开以后排查“引脚配置被后来代码覆盖”这类问题时也更方便。CNF11_1 的含义是把这个引脚配成输入模式因为 CAN_RX 是由外部收发器驱动的不需要内部上拉。PA12 必须复用推挽输出因为 CAN_TX 是控制器主动驱动的信号推挽输出才能保证显性位有足够的驱动能力。如果 PA12 被误配成开漏发送时显性电平会偏弱严重时对方节点收不到完整帧。3.2 36MHz APB1 下把位时间凑成 500kCAN 波特率不是直接写一个“500”就能成立的。F103 的 CAN 外设把每一位时间拆成同步段、传播段、相位缓冲段 1、相位缓冲段 2最终波特率等于 APB1 时钟除以预分频系数再除以一个位时间的总时间量子数。整车动力 CAN 的标准速率是 500kbps。整车 OBD 接口上的总线基本上也都遵循这个速率少数车型会用到 250kbps但标题项目只要先守住动力 CAN 这条最常见的主线。常见配置组合如下APB136MHzBRP 预分频TS1 时间量子TS2 时间量子实际波特率4143500k6112500k采样点略靠前TS114、TS23 的采样点约在 83%能兼顾较长的总线长度和较差的边沿质量。代码里写寄存器时要注意STM32F103 的 BTR 寄存器里 TS1 字段和 TS2 字段存的值比实际时间量子数小 1也就是要 14 个时间量子时寄存器里写 13。void CAN_Bitrate_Init(void) { CAN1-MCR | CAN_MCR_INRQ; while (!(CAN1-MSR CAN_MSR_INAK)); CAN1-BTR (CAN_BTR_SJW_1TQ) // 同步跳转宽度 1TQ | (13u 16) // TS1 字段实际 14TQ | (2u 20) // TS2 字段实际 3TQ | (3u); // BRP4 CAN1-MCR ~CAN_MCR_INRQ; while (CAN1-MSR CAN_MSR_INAK); // 等待退出初始化模式 }如果波特率不对表现非常有辨识度CAN 发送函数返回成功但总线上始终收到 ACK 错误寄存器 ESR 里的 LEC 字段会变成 1。整车总线对波特率误差比两个单片机直连更敏感因为总线上挂了十几个节点任何误差都会累积。所以这里不要为了省代码省略 SJW 的配置。3.3 验收滤波只收诊断响应 ID不收听全车报文整车 CAN 线上同时跑着很多业务报文发动机转速或车速如果直接抓广播报文确定 ID 会因车型而异。标题项目走的是 OBD 诊断通道响应统一从 0x7E8 到 0x7EF 之间回来。所以验收滤波要放行 0x7E8 到 0x7EF而不是只放行一个 0x7E8。void CAN_Filter_Init(void) { CAN1-FMR | CAN_FMR_FINIT; // 进入滤波器初始化模式 CAN1-FS1R | 1u; // 过滤器 0 使用 32 位宽度 CAN1-FM1R ~1u; // 掩码模式 CAN1-sFilterRegister[0].FR1 (0x7E8u 21); // 期望 ID CAN1-sFilterRegister[0].FR2 (0x7F8u 21); // 掩码低 3 位不关心 CAN1-FA1R | 1u; // 激活过滤器 0 CAN1-FMR ~CAN_FMR_FINIT; // 退出初始化模式 CAN1-IER | CAN_IER_FMPIE0; // 开启 FIFO0 消息挂起中断 }掩码 0x7F8 的意思是0x7E8 到 0x7EF 这些 ID 都放行而 CAN ID 的高位必须匹配。为什么要放开 8 个 ID同一辆车有发动机 ECU、变速箱 ECU、车身控制器等多个节点不同制造商对响应节点编号的处理并不一样。部分车型会从 0x7E8 开始逐字节增加只放行固定一个 ID 就可能在特定车型上收不到任何响应。4. 请求转速和车速0x7DF 发出去0x7E8 收回来4.1 8 字节请求帧不是随意填的OBD 标准诊断请求里CAN ID 0x7DF 是功能寻址请求 ID所有 ECU 都会接收。发动机转速对应的模式是 01PID 是 0x0C因此数据场应该是字节值含义00x02后续有效字节数10x01服务模式请求当前数据20x0CPID发动机转速3~70x00填充位发送时 DLC 写 8但有效字节只有 3 个。这个长度字节很容易被忽略如果把它写成 0x08部分严格要求 ECU 会直接拒绝响应因为长度和内容不匹配。typedef union { uint32_t Word[2]; uint8_t Byte[8]; } CanData; void CAN_SendOBDRequest(uint8_t pid) { CanData data; data.Byte[0] 0x02; data.Byte[1] 0x01; data.Byte[2] pid; data.Byte[3] 0x00; data.Byte[4] 0x00; data.Byte[5] 0x00; data.Byte[6] 0x00; data.Byte[7] 0x00; CAN1-sTxMailBox[0].TIR (0x7DFu 21); // 标准帧数据帧 CAN1-sTxMailBox[0].TDTR 8u; // DLC8 CAN1-sTxMailBox[0].TDLR data.Word[0]; // 低 4 字节 CAN1-sTxMailBox[0].TDHR data.Word[1]; // 高 4 字节 CAN1-sTxMailBox[0].TIR | 1u; // 请求发送 }使用邮箱 0 而不是邮箱 1 或 2是因为邮箱 0 优先级最高。在总线繁忙时高优先级发送请求可以先占上总线。整车 CAN 的广播报文比较密诊断请求如果一直被插队响应时间会拉长继而影响转速和车速的刷新率。4.2 转速用两字节计算车速只取一字节接收中断里拿到的是 0x7E8 的响应帧。响应数据场的第 0 字节是长度第 1 字节是 0x41表示对模式 01 的响应第 2 字节是 PID 原值之后才是真正要用的数据。转速回应的有效数据占两个字节车速只占一个字节volatile uint16_t engine_rpm 0; volatile uint8_t vehicle_speed 0; volatile uint8_t obd_rx_flag 0; void CAN1_RX0_IRQHandler(void) { uint8_t data[8]; // 把 FIFO0 的数据读出来 data[0] CAN1-sFIFOMailBox[0].RDLR 0xFF; data[1] (CAN1-sFIFOMailBox[0].RDLR 8) 0xFF; data[2] (CAN1-sFIFOMailBox[0].RDLR 16) 0xFF; data[3] (CAN1-sFIFOMailBox[0].RDLR 24) 0xFF; data[4] CAN1-sFIFOMailBox[0].RDHR 0xFF; data[5] (CAN1-sFIFOMailBox[0].RDHR 8) 0xFF; // 判断是不是标准转速响应 if (data[1] 0x41 data[2] 0x0C) { engine_rpm (data[3] 8) | data[4]; engine_rpm / 4; } // 判断是不是标准车速响应 if (data[1] 0x41 data[2] 0x0D) { vehicle_speed data[3]; } CAN1-RF0R | CAN_RF0R_RFOM0; // 释放 FIFO 邮箱 obd_rx_flag 1; }转速公式为什么要除以 4ISO 15031-5 规定转速的计算公式是(A * 256 B) / 4转每分钟。原因在于实际转速超出单个字节范围而车辆内部计算转速时的精度又不需要到 1rpm。除以 4 之后分辨率是 0.25rpm对诊断来说完全够用。车速没有乘除解析出来就是一个字节单位是 km/h。这个字节直接来自 ABS/ESP 或仪表总线真实车速虽然也有一些滤波逻辑但 OBD 层不处理这些细节原样输出即可。4.3 主循环里交替请求漏帧由超时兜底一次请求只能拿一个 PID所以主循环里要交替发送 0x0C 和 0x0D。最朴素的写法是while (1) { CAN_SendOBDRequest(0x0C); HAL_Delay(20); CAN_SendOBDRequest(0x0D); HAL_Delay(20); }间隔 20ms 看起来简单但这里有个隐性要求发送后必须判断是否收到响应。很多整车 ECU 在收到诊断请求后如果总线繁忙会在几十毫秒后才回复。因此只发不读、或者读的时候不做超时判断都会让界面上的转速忽然不动。我一般会用 100ms 作为阈值发完请求后如果 100ms 内没有收到目标 PID 响应就重新发送一次。这样既不会因为一条响应丢失导致数据长时间不刷新也不会让 CAN 总线因为重发太快而被广播报文淹没。提示如果整车环境里同时有多个设备在抢占诊断通道就会出现互相干扰。诊断功能寻址 0x7DF 是广播式的总线上的诊断仪之间需要协调不能两个设备同时发请求。5. 整车验证时怎么确认读到的转速和车速没被波特率坑过5.1 先用万用表确认总线基础状态上车之后不要急着开上位机。第一步是用万用表电阻档量 OBD 插座 6 号和 14 号之间的电阻点火开关关闭正常约 60Ω。如果没有这个阻值说明你插的插座根本不是 CAN 总线或者总线处于掉电断线状态。第二步是万用表直流电压档点火开关打开但不着车CAN_H 和 CAN_L 之间应该能测到约 2V 或 2.5V 的动态直流电压具体数值和总线负载有关但不会是 0V 或 12V。如果万用表量出 12V大概率是接错引脚把信号地 4、5 号以外的电源针脚当成 CAN 了。5.2 用逻辑分析仪确认位流插上逻辑分析仪通道 A 接 CAN_H通道 B 接 CAN_L采样率至少 10MS/s解码选 CAN 协议波特率按 500k。解码成功后能看到 0x7DF 和 0x7E8 的标准帧 ID。这里最容易出现的事故是逻辑分析仪和 STM32F103 的诊断仪同时挂在一条总线上但逻辑分析仪的地线没有和车辆共地导致波形乱跳。先把逻辑分析仪的 GND 和 OBD 插座的 4 号或 5 号针脚连好再去测。确认波形的意义在于它能回答“STM32 发出来的帧到底有没有到总线上”这个问题。软件层面能看到的 ACK 错误和位错误在波形上一眼就能看出来。5.3 用第二路信号做交叉校验转速和车速这类数据整车上有多个独立来源比如转速也可以通过发动机 ECU 发出的广播报文读到车速也可以通过 ABS 轮速计算得到。把 OBD 诊断读到的转速和车速与另一路传感器或者另一台原本就在车内工作的诊断设备做比对如果两边的差值在合理范围内才说明 STM32F103 的解析逻辑没有被字节序问题带偏。我在实际项目里常用一个笨办法让车原地怠速记录 OBD 读到的转速值同时把同轴电缆上诊断仪的读数拍下来连续对比 10 次。怠速转速通常在 700 到 900rpm 之间如果 STM32F103 读到的数值稳定在同一个区间说明 PID 解析对如果数值忽大忽小先检查波特率对不对再检查长度字节和字节序。车速的验证更简单找一段直路让车辆定速 60km/h用手机 GPS 的速度做对照误差在 3km/h 以内基本可以锁定解析无误。本文还有配套的精品资源点击获取