简介STM32F103 的 LIN 总线通信 C 语言示例工程面向嵌入式与汽车电子开发者。工程基于 USART3 中断接收 LIN 报文通过同步标志与标识符判断完成帧识别在标识符校验通过后发送 8 字节数据并计算 CRC搭配 TPIC1021AQDRQ1 收发器以 19200bps 运行可帮助解决 LIN 从机节点开发中的帧解析、校验与中断处理问题。资源包共 38 个文件以 C 源文件与 H 头文件为主头文件多用于外设寄存器、报文字段与接口声明源文件则包含初始化、中断处理及 CRC 计算实现另有 IAR 工程的链接配置、启动汇编与工程管理文件整体仅 226KB结构精炼便于快速导入与阅读。已有 1995 人学习下载。获取后可对照完整工程梳理 LIN 收发流程理解各标志位的检查顺序也可将外设初始化、中断服务与 CRC 校验逻辑迁移到其他 STM32F1 系列项目中适合有一定 STM32 基础、正在开展 LIN 通信或车身控制开发的工程师参考。1. STM32F103 LINBUS 示例代码从拿到源码到跑通一帧通信汽车电子项目里CAN 总线经常被拿来讲架构真正落到车门控制、车窗电机这类低速节点时LIN 总线反而更常见。STM32F103 LINBUS 示例代码就是一份把 LIN 通信做成最小可运行工程的下载资源里面包含 USART 的 LIN 模式初始化、主节点发送一帧、从节点中断接收处理以及配套的状态机代码。它解决的问题很直接让你在一两天内把 LIN 总线跑通而不是先花两周啃协议规范。这份代码适合三类人正在做汽车电子课程设计的学生、刚接触 LIN 的嵌入式软件开发者以及需要验证一款 LIN 收发器芯片是否正常的硬件工程师。整个工程基于 C 语言编写不依赖 RTOS找一块 F103 最小系统板就能编译。拿到代码后你首先会发现LIN 在硬件层面就是复用普通串口只不过加了一个收发器芯片再用状态机来处理帧结构。如果只想看现象烧录后把 TX 引脚接一个 LIN 收发器再接逻辑分析仪很快就能抓到一帧完整的 LIN 波形。想进一步理解协议细节接下来就得从帧结构讲起。2. LIN 协议基础与 STM32F103 硬件映射先搞懂帧结构再改代码2.1 LIN 帧结构同步间隔场、同步场、标识符、数据场与校验和LIN 总线上的一个最小通信单位可以划分成五个部分。同步间隔场是一段明显的显性电平持续时间至少 13 位后面跟着同步场 0x55再往后是标识符场、数据场和校验和场。在 STM32F103 的代码里这五个部分直接对应五次USART_SendData调用只不过第一部分用了硬件产生的同步间隔剩下的字节都是普通串口发送。同步场固定为 0x55接收端依靠 0x55 边沿来做波特率对齐。标识符是一个字节低 6 位是实际 ID高 2 位是奇偶校验位发送前必须计算好。数据场长度从 1 字节到 8 字节不等LIN 规范里 ID 与长度有固定对应关系不是用像 UART 那样靠帧头解析出来的。校验和是最后 1 个字节经典校验只对数据字节累加后取反增强校验则要把带校验位的 PID 字节也累加进去诊断帧强制使用增强校验。网络上很多资料把 LIN 叫做低成本版 CAN这个说法其实不准确。LIN 是单主多从结构从节点之间不能直接通信所有帧都由主节点调度。这意味着代码的重心不在仲裁和退避而在调度表的正确性。理解了这一点再回头看 STM32F103 的 USART 配置思路就会清晰很多。帧间间隙和字节间间隙在规范里有严格的时序要求但在示例代码层面只需要满足一个原则前一帧发送完成后不要立刻把下一帧的同步间隔打出去最好留几个毫秒或至少留出 1 位时间以上的空闲。这个间隙在逻辑分析仪上看起来就是一段隐性电平如果被忽略接收端可能把两帧误判成一帧。2.2 STM32F103 的 USART 在 LIN 模式下替你做了哪几件硬件活F103 的 USART1、USART2、USART3 都支持 LIN 模式硬件层面会帮你完成三件关键事情。第一发送端可以硬件产生同步间隔场也就是把 TX 引脚拉低一段时间持续时间由 USART_CR2 里的 LBDL 位决定可取 11 位或 13 位。LIN 规范要求显性电平至少 13 位所以工程里通常配置为 13 位。第二接收端有 LBD 中断也就是同步间隔检测中断。当 RX 引脚出现一段持续低电平并且超过阈值时硬件会置起 LBD 标志并触发中断这个中断就是一帧 LIN 数据开始接收的锚点。第三硬件支持用 0x55 同步场做波特率校准但 F103 的 USART 并不是像某些新 MCU 那样能全自动改写波特率寄存器它的校准能力有限所以工程实现上主节点通常固定波特率从节点也在初始化时按固定波特率配置。换句话说F103 的 LIN 模式解决的是同步间隔的产生和检测这两个最麻烦的问题而帧解析、校验和计算、超时处理这些协议逻辑仍然要写在软件里。这正是示例代码的核心价值所在它帮你把硬件寄存器配置和协议状态机的边界划分清楚让你知道哪些行为是外设完成的哪些必须自己写。很多人在入门时犯的错是把 USART 的 LINEN 位置位之后就以为整个协议栈都有了结果发现数据过来全是乱码。实际原因往往是状态机没有写或者把 RXNE 中断和 LBD 中断的优先级搞混了导致同步间隔没被识别。2.3 引脚选择与收发器电路为什么普通串口不能直连 LIN 总线示例代码默认使用 USART1PA9 是 TXPA10 是 RX。如果把 PA9 直接接到另一块板子的 PA10得到的顶多是一个 TTL 串口和 LIN 总线差得很远。原因有两个LIN 总线电平是相对 12V 电源的单线传输并且采用线与结构不能像普通 UART 那样推挽输出。标准做法是增加一个 LIN 收发器常见型号如 TJA1020。收发器芯片把 MCU 的 3.3V 逻辑电平转换成总线上的电平同时承担总线保护和唤醒检测。MCU 的 TX 接收发器 TXD收发器 RXD 接 MCU 的 RXLIN 引脚通过二极管和上拉电阻接入总线。主机节点要额外接一个 1kΩ 终端电阻到 VBAT从机节点一般用 30kΩ 上拉。下面是最简接线的对照表收发器引脚接到哪里说明TXDMCU TX 引脚推挽输出3.3V 逻辑RXDMCU RX 引脚浮空或上拉输入LIN总线经二极管与上拉电阻接 VBATVBAT12V 电源部分型号支持 5V 供电如果买的是现成的 LIN 收发模块接线基本就是 TX、RX、VBAT、GND 四根线终端电阻模块已经处理好了。实验时示波器探头接 LIN 引脚地线夹板子地即可。注意总线空闲时的隐性电平并不是 0V而是接近 VBAT 的高电平显性电平才是接近地的低电平和 UART 的空闲高电平逻辑正好相反这也是很多初次调试者困惑的点。3. 示例代码拆解主节点发送一帧与从节点接收状态机验证 LIN 是否跑通最直接的办法是让主节点定时发布一帧再用逻辑分析仪抓 0x55 和标识符。下面按主节点发送、从节点接收两条线拆解代码。3.1 初始化时钟、GPIO 与 USART LIN 模式配置主节点和从节点的初始化几乎相同差别只在是否发送帧。以 USART1 为例先配置 GPIOstatic void LIN_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOA 和 USART1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // PA9 作为 USART1_TX必须配置成复用推挽 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA10 作为 USART1_RX浮空输入即可 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }PA9 必须配置为复用推挽而不是普通推挽。普通推挽模式下引脚由 GPIO 数据寄存器控制不会跟随 USART 信号翻转示波器上自然看不到波形。PA10 平时是浮空输入如果板子上没有加收发器的 RXD 上拉可以在初始化里改成上拉输入减小噪声干扰。紧接着配置 USARTstatic void LIN_USART_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 8 数据位、无校验、1 停止位避免硬件校验干扰 LIN 帧 USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStructure); // 把 USART1 切换到 LIN 模式 USART_LINCmd(USART1, ENABLE); // 打开接收数据中断和同步间隔检测中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_ITConfig(USART1, USART_IT_LBD, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_Cmd(USART1, ENABLE); }这段配置本身没有太多特殊之处。关键在最后几行USART_LINCmd打开 LIN 模式RXNE 中断负责逐字节接收LBD 中断负责识别同步间隔场。中断优先级方面建议让 LBD 的抢占优先级不低于 RXNE否则在极端时序下可能会先处理数据字节而错过同步间隔标志。注意 USART1 和 USART2 的时钟源不同。USART1 挂在 APB2 上当 SYSCLK 为 72MHz 且 APB2 预分频为 1 时外设时钟是 72MHz。USART2 和 USART3 挂在 APB1 上默认 36MHz。标准外设库在USART_Init内部会根据 RCC 时钟树自动计算 BRR但前提是你在工程里正确调用了SystemInit()并且不要在初始化之前手动改 APB1 的预分频值。3.2 主节点发送一帧同步间隔、帧内容与校验和主节点发送一帧 LIN 数据的核心操作如下void LIN_Master_SendFrame(uint8_t id, uint8_t *data, uint8_t len) { uint8_t sum 0; uint8_t pid, i; // 1. 发送同步间隔场硬件自动拉低 TX 一段时间 USART_SendBreak(USART1); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 2. 同步场固定为 0x55 USART_SendData(USART1, 0x55); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 3. 标识符场低 6 位是 ID高 2 位是奇偶校验位 pid LIN_CalcPID(id); USART_SendData(USART1, pid); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 4. 数据场同时累加数据字节 for (i 0; i len; i) { USART_SendData(USART1, data[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); sum data[i]; } // 5. 经典校验和对累加和取反 USART_SendData(USART1, (uint8_t)(0xFF - sum)); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); }逻辑说明USART_SendBreak让硬件产生一段连续显性电平这就是帧起始的同步间隔。0x55 是同步场给接收端一个固定的边沿序列。LIN_CalcPID负责计算标识符中的两个奇偶校验位这一步不能省否则接收端算出的 ID 会出错。数据场循环里同时累加 sum最后用0xFF - sum产生校验和。这里必须使用TC标志而不是TXE标志。TXE只表示发送数据寄存器已空数据可能还在移位寄存器里而TC表示整个字节已经从引脚上输出完成。如果以TXE作为判断条件同步间隔后紧跟的 0x55 会被硬件提前塞入发送逻辑导致波形变形接收端误判帧结构。LIN_CalcPID的计算逻辑是PID 的 bit6 是 ID 的 0、1、2、4 位异或结果bit7 是 1、3、4、5 位异或结果再把裸 ID 放入低 6 位。代码里可以直接用查表法因为整个 LIN 的 ID 空间只有 64 个。3.3 从节点接收中断状态机逐字节解析一帧从节点接收更依赖中断状态机核心处理逻辑如下void LIN_RX_ProcessByte(uint8_t byte) { switch (state) { case RX_SYNC: if (byte 0x55) { state RX_ID; } else { state RX_IDLE; // 同步场不符回到初始等待 } break; case RX_ID: rx_id byte 0x3F; // 去掉奇偶校验位只保留低 6 位 sum 0; count 0; state RX_DATA; break; case RX_DATA: if (count expected_len) { rx_data[count] byte; sum byte; // 数据字节累加校验和不累加 } else { rx_checksum byte; state RX_IDLE; // 经典校验和检查 if ((uint8_t)(0xFF - sum) rx_checksum) { rx_done 1; // 标志由主循环消费 } else { rx_error 1; } } break; default: state RX_IDLE; break; } }参数说明expected_len是当前 ID 对应的数据场长度由上层函数根据 ID 查表得到不是由帧头解析出来的。如果只想响应某个从节点地址应该在RX_ID分支里判断rx_id是否匹配不匹配就直接把状态置回RX_IDLE避免浪费内存处理无关帧。中断服务函数可以这样写void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_LBD) ! RESET) { // 检测到本帧的同步间隔场状态机准备等待 0x55 state RX_SYNC; USART_ClearITPendingBit(USART1, USART_IT_LBD); } if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte (uint8_t)USART_ReceiveData(USART1); LIN_RX_ProcessByte(byte); } if (USART_GetITStatus(USART1, USART_IT_ORE) ! RESET) { // 溢出标志会导致后续字节全部丢失 USART_ClearITPendingBit(USART1, USART_IT_ORE); } }ORE 分支是一个容易漏掉的地方。如果接收缓冲区里的数据还没有被取走新字节已经到达硬件会置起溢出标志RXNE 不再触发后面的字节全部丢失。清 ORE 的标准做法是先读状态寄存器再做一次读数据寄存器标准外设库的ClearITPendingBit在多数版本里也能完成等效操作。实际抓逻辑分析仪波形正常但数据连续乱码时多半就是 ORE 没处理。3.4 波特率误差与时钟源的影响LIN 常见的波特率是 19200 和 10400。以 USART1 在 APB2 上 72MHz 时钟计算19200 波特率时 72MHz 除以 19200 等于 3750恰好整除。10400 波特率时72MHz 除以 10400 得 6923.08BRR 寄存器只能填 6923实际输出约 10400Hz误差极小完全满足 LIN 规范 2% 的容限。真正容易出错的地方是手工计算 BRR 时使用的时钟频率不对。比如在一款板子上把 RCC 配成了 SYSCLK 72MHz、APB1 36MHzUSART2 的波特率计算就必须以 36MHz 为基准。如果直接把 72MHz 代入到 USART2 的 BRR 计算里波特率就偏了接近一倍接收端会频繁丢帧。另一个常见问题是两块开发板的晶振精度差异。廉价的贴片晶振误差可能在 100ppm 以上对短帧影响不大但累积到一帧 8 字节加校验和的长度再加上接收端的采样误差就可能出现偶发丢位。遇到这类情况先用示波器测 RXD 引脚的实际位宽再按第 6 章的思路做一次校准不要靠猜。4. 调度表与总线生命周期让多个从节点稳定工作的关键设计LIN 总线上主节点是唯一的调度者从节点永远不主动占用总线只能等主节点发布帧。这个特性决定了工程核心是调度表而不是数据竞争处理。4.1 调度表的数据结构与发送轮询常见做法是预先定义一个表列出每个时隙要发送的 ID、数据长度和内容。示例工程里可以做成结构体数组typedef struct { uint8_t id; // 6 位有效 ID不含校验位 uint8_t data[8]; uint8_t len; } LIN_ScheduleEntry; const LIN_ScheduleEntry schedule[] { { 0x01, { 0x21, 0x00, 0x00, 0x00 }, 4 }, // 查询节点1状态 { 0x02, { 0x00 }, 1 }, // 查询节点2状态 { 0x03, { 0x00 }, 1 }, // 查询节点3状态 { 0x3C, { 0x00 }, 1 }, // 诊断帧主机睡眠命令 };配合一个定时器周期触发void LIN_Master_SchedulePoll(void) { static uint8_t slot 0; uint32_t now GetTickMs(); if (now - last_slot_time 10) // 每 10ms 发送一个时隙 { const LIN_ScheduleEntry *entry schedule[slot]; LIN_Master_SendFrame(entry-id, entry-data, entry-len); last_slot_time now; slot; if (slot sizeof(schedule) / sizeof(schedule[0])) { slot 0; } } }逻辑说明主节点在每个时隙只发一帧从节点收到后只做一件事要么消费数据号样本要么在收到匹配 ID 后进入发送响应流程。调度表顺序和周期决定了整个系统的实时性。10ms 只是一个示例值实际项目中要根据每条信号的更新频率来设定比如车窗电机控制可能需要 5ms而温度传感器信号 100ms 更新一次就够。调度表的另一层作用是把“谁在什么时候说话”固定下来这对调试很有用。逻辑分析仪抓包时只要知道当前时隙到了哪一帧就能立刻判断是从节点没回应还是主节点根本没发出这帧。4.2 唤醒与休眠总线管理模式LIN 规范提供两种总线级管理操作休眠和唤醒。主节点可以发送一条诊断帧让整个总线进入低功耗状态也可以发送一段唤醒信号让从节点恢复工作。示例代码中通常用两个函数void LIN_Master_SendSleepCmd(void) { uint8_t sleep_cmd 0x00; LIN_Master_SendFrame(0x3C, sleep_cmd, 1); // 主机睡眠命令 } void LIN_Master_WakeUp(void) { // 唤醒信号一段持续 250us 以上的显性脉冲 USART_SendBreak(USART1); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); delay_ms(5); }逻辑说明唤醒信号要求显性电平持续时间大于 250us 且小于 5ms。USART_SendBreak产生的同步间隔在 19200 波特率下 13 位约 677us正好落在规范区间内所以可以直接复用来产生唤醒信号。从节点收到睡眠命令后通常会把收发器切换到休眠模式的 GPIO 控制引脚拉低同时保留 LBD 中断。这样总线上有人唤醒时LBD 中断能把 MCU 从低功耗模式拉起来而普通数据帧不会打扰它。示例代码一般不会写完整的低功耗切换流程但把睡眠命令的识别和标志位置写好后续扩展就很容易。4.3 超时、节点离线与错误恢复中断状态机最大的风险是卡在半路。比如同步间隔已经收到但 0x55 迟迟不来或者数据场传了一半总线噪声介入。如果没有超时机制状态机会一直停在错误分支下一帧来了也无法正确解析。常见做法是引入字节超时定时器void LIN_CheckTimeout(void) { if (state ! RX_IDLE) { if (GetTickMs() - last_byte_time 5) // 5ms 内没有新字节判超时 { state RX_IDLE; rx_error 1; } } }在中断收到每个字节时刷新last_byte_time。5ms 这个阈值对 19200 波特率的一帧通信来说足够宽一帧完整数据在 19200 波特率下最多也就约 1ms 左右不会误判。主节点在等待从节点回复时也需要做类似超时判断超时后通常把这个节点标记为离线。节点离线的恢复策略很重要不要第一次收不到响应就立刻改变调度表可能是总线上恰好出现了一个噪声帧。连续失败 3 到 5 次再判定离线并把该节点对应的时隙从调度表中跳过同时上报错误标志是比较稳妥的做法。示例代码一般只提供错误标志不包含完整的离线管理但模块划分是清楚的能自己扩展。5. LIN 调试避坑从波特率误差到中断标志位的五个真实问题这五个问题按出现频率排列基本覆盖 STM32F103 加 LIN 收发器联调时会遇到的绝大部分坑。每条按现象、原因、解决三段来说明。5.1 现象偶发丢帧逻辑分析仪波形看起来齐整但数据偶尔多一位或少一位原因波特率换算误差超过 LIN 规范 2% 容限或者主从两侧用了不同基准计算 BRR。常见于主节点板使用 USART1 挂在 APB2从节点板使用 USART2 挂在 APB1两侧如果手工计算波特率寄存器就容易差 1% 到 2%。数据流总长度越长累计的时间误差越明显所以 8 字节数据帧比 1 字节帧更容易出错。解决统一使用标准外设库的USART_Init不要手工填写 BRR。调试时先跑 19200不要一上来用 10400因为低波特率对时钟误差更敏感。如果切换时钟配置后再现就用示波器量 RXD 的实际位宽按第 6 章的校准流程反推。5.2 现象从节点进入一次中断后不再收到任何后续数据原因LBD 中断标志没有被及时清除。LBD 是电平检测标志中断处理函数里只置了状态但没清标志或者清标志的顺序不对下次同步间隔到来时状态机根本不会进入同步场等待分支。解决在 LBD 中断分支里先清标志再修改状态变量。用标准外设库的朋友注意USART_ClearITPendingBit必须在读取状态寄存器之后执行直接操作寄存器时需要先读 SR 再写 CR否则清不掉。5.3 现象TX 引脚完全没有波形或者只有短暂跳到低又立刻回高原因GPIO 没有配置为复用推挽配成了普通推挽输出或复用开漏。另一个常见问题是 AFIO 时钟没有使能特别是用了引脚重映射时。还有一种情况是程序里在初始化 USART 前不小心把 PA9 当作普通 GPIO 用了后面又没有恢复成复用模式。解决检查四步。第一RCC_APB2PeriphClockCmd使能 GPIOA 和 USART1第二PA9 配置为GPIO_Mode_AF_PP第三PA10 配置为GPIO_Mode_IN_FLOATING第四不需要对 USART1 使能重映射它默认就在 PA9 和 PA10 上。如果想快速定位可以先把 PA9 改成普通推挽输出并写 1看示波器是否有高电平再改回复用配置看是否翻转。5.4 现象接收字节内容正确但校验和计算一直失败原因经典校验和与增强校验和没区分。经典校验只对数据场累加增强校验要在累加前先加入 PID 字节。LIN 诊断帧强制使用增强校验普通应用帧协议两端可以约定用经典或增强多平台联调时经常出现一端用增强校验另一端却按经典校验计算。解决在工程的配置结构体里增加一个use_enhanced_checksum字段发送和接收都读取这个字段。校验和计算函数独立封装统一用LIN_CalcChecksum(id, data, len, enhanced)来生成而不是在发送和接收两处各自写一遍求和逻辑。这样即使后续改动校验类型也只改一个地方。5.5 现象板上电后总线被拉死所有节点都发不出数据原因MCU 复位期间 GPIO 输出为低如果 TX 引脚默认状态直接驱动收发器收发器会把 LIN 总线持续拉成显性电平其他节点就无法再发起通信。另一种常见情况是收发器的 VBAT 供电异常或者模块上的 12V 和 5V 接反。解决电路设计上给收发器 TXD 引脚加一个 10kΩ 上拉到 3.3V确保 MCU 复位期间总线处于隐性状态。如果板子已经做好了软件上可以在 GPIO 配置前先把 PA9 设为输入上拉避免默认低电平驱动收发器等初始化函数执行到 GPIO 复用配置时再切回去。这个问题胜在隐蔽很多人查协议找半天最后发现是复位时序的问题。6. 给示例代码加一套自动波特率校准用输入捕获量同步间隔宽度很多 LIN 节点在更换晶振或者板级时钟有偏差时固定波特率配置就会失效。一个很实用的技巧是用定时器输入捕获去测量同步间隔场的宽度从而反推实际波特率然后把新的 BRR 写回 USART。原理很简单。同步间隔场是一段显性低电平宽度等于 13 个位时间。如果测得这段低电平的时间是 T那么实际波特率就等于 13 除以 T。测量过程不影响正常通信因为同步间隔场是整帧里最长的一段定时器有充足时间完成捕获。示例使用 TIM2 通道 1 的输入捕获void LIN_Measure_Baudrate_Init(void) { TIM_ICInitTypeDef TIM_ICInitStructure; // 预分频设为 72-1定时器时钟 1MHz计数值直接对应微秒 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_PrescalerConfig(TIM2, 72 - 1, TIM_PSCReloadMode_Update); // TIM2 通道1 下降沿捕获测量低电平时间 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); }在捕获中断里测量两个下降沿之间的间隔void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { static uint16_t last_edge 0; uint16_t now_edge TIM_GetCapture1(TIM2); uint16_t diff now_edge - last_edge; last_edge now_edge; // 连续两个下降沿之间的时间就是完整位时间单位 us if (diff 20 diff 2000) { g_bit_time_us diff; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }得到位时间后把它换算回波特率并更新 USARTvoid LIN_ApplyCalibratedBaudrate(void) { if (g_bit_time_us 20) { return; } uint32_t baud 1000000UL / (uint32_t)g_bit_time_us; // 位时间 us 换算为波特率 uint32_t brr 72000000UL / baud; // 关闭 USART 再写 BRR否则配置不生效 USART_Cmd(USART1, DISABLE); USART1-BRR brr; USART_Cmd(USART1, ENABLE); }这段代码适用于主节点固定波特率、从节点上电后做一次性校准的场景。19200 波特率下位时间约为 52us10400 波特率下约为 96us算出的波特率误差都能控制在很小范围内。需要说明的是这里直接把整数写入 BRR 寄存器对 19200 和 10400 这类常用波特率来说精度足够如果要覆盖更多非标波特率还是建议把计算出的波特率重新传给USART_Init走标准外设库的分数分频逻辑。我在验证新板子时每次都先跑这套测量流程上电后主节点发一帧从节点记录同步间隔宽度如果算出的波特率和预期偏差超过 1%就先查晶振和时钟配置而不是查协议解析代码。这个方法帮助我排除过至少三个看起来像又不像波特率问题的故障。从那以后每调一个新的 LIN 工程我都会强制自己先走一遍抓同步间隔、算位时间、核对波的特点的流程再往下查数据场和校验和。希望帮到你。本文还有配套的精品资源点击获取