STM32F103 LIN总线通信实战:帧结构、USART配置与调试避坑指南 简介面向STM32F103微控制器及LIN总线通信的示例代码适合嵌入式开发初学者与需要快速实现LIN从机通信的工程师。代码演示了基于USART3中断的完整LIN帧处理流程检测到LIN中断后读取同步标志与标识符当标识符匹配时发送DATA[8]并附加CRC校验物理层采用TPIC1021AQDRQ1收发器波特率设为19200bps覆盖了从中断触发到数据校验的关键环节。资源共38个文件以11个C源文件与18个头文件为主体另有IAR工程配置文件ewp/ewd/ewt/icf、启动文件stm32f103xb.s、HAL驱动及说明文档工程结构清晰便于对照学习。压缩包仅226KB内容紧凑。已有1993人学习下载适合希望掌握LIN协议底层实现、中断编程及CRC计算细节的开发者也可作为车载LIN节点项目的基础移植模板。 前阵子我手里正好有一块 STM32F103 最小系统板要跟一个门控模块通信对方只给了 LIN 接口协议表。F103 严格来说不是做 LIN 最顺手的芯片但它的 USART 是支持 LIN 模式的配合标准外设库 V3.5 完全可以实现主从通信。我从零把整个 LINBUS 的示例代码梳理了一遍从帧结构、寄存器配置到中断状态机再到调试时踩掉的坑一次性整理出来。如果你也在做车身电子、低速单线通信或者单纯想把一个 LIN 从机设备接入自己的 F103 工程这篇文章应该能直接帮你省掉很多翻手册的时间。1. 为什么要在 F103 上硬啃 LIN 总线1.1 LIN 到底适合什么样的通信场景LIN 全称 Local Interconnect Network字面意思就是“本地互联网络”。它最大的特点是单线传输整个总线上一根信号线完成通信加上地线和电源三根线就能把一堆节点串起来。和 CAN 比起来CAN 是双绞线差分信号还要配控制器和收发器成本明显高不少而 LIN 的收发器便宜线束也少非常适合车窗、后视镜、座椅调节、雨刮、车灯这些对速率要求不高的设备。F103 本身带 bxCAN很多人会想既然要做通信用 CAN 不就行了但实际项目中大量场景是“我只想读一个传感器状态或者控制一个马达”通信速率 19200 波特率就够了数据量也就几个字节。这时候上 CAN 属于杀鸡用牛刀车厂愿意为 CAN 付钱是因为安全性和实时性而低速子网用 LIN 是行业默认做法。1.2 F103 实现 LIN 的三条技术路径F103 没有像 F0/G0 部分型号那样带完整的 LIN 控制器但并不代表不能用。我梳理下来实现 LIN 通信有三条路硬件 LIN 模式F103 的 USART 支持 LIN break 的发送和检测这是最常用的方式代码量和实时性都最好。纯软件模拟用 GPIO 拉低一定时间模拟 break后续字节仍然走 UART。适合 USART 资源不够或者需要发 13 位 break 的场景。外接带 LIN 协议的串口芯片比如直接买一个串口转 LIN 模块MCU 只发普通串口命令。这种方式最简单但成本高而且很多场景下你手里根本没有现成模块。我最终用的方案是“硬件 LIN 模式 软件补 13 位 break”这样既利用了 F103 的硬件能力又能满足 LIN 规范对 break 长度的要求。后面会详细说这个取舍的原因。2. LIN 物理层和帧结构把一帧报文拆开看2.1 单线总线的电平定义有一点必须先说清楚LIN 总线不是 TTL 电平它工作在汽车电瓶电压范围。主节点内部要有一个 1kΩ 上拉电阻接到 VBAT从节点则是 30kΩ 上拉。总线空闲时是高电平这个高对应的是“隐性”节点发数据时把总线拉低对应“显性”。从 MCU 的视角来看UART 发送的 0 和 1 经过 LIN 收发器转换后就变成了总线上的显性和隐性电平。很多人第一次调的时候拿逻辑分析仪去抓 MCU 的 TX 引脚看到的是普通 UART 波形其实这是对的。真正上总线之后才需要 LIN 收发器比如 TJA1020、TJA1021、MCP2003。如果没有收发器你只能在 TTL 电平上验证帧格式不能直接挂到真实 LIN 网络里。2.2 一帧 LIN 报文的字节顺序LIN 的报文格式非常规整一帧由这几部分组成字段长度说明Break 同步断开场至少 13 位显性电平表示一帧开始从机靠它判断新帧到来Sync 同步场1 字节 0x55从机用它校准波特率PID 保护标识符1 字节高 2 位是奇偶校验低 6 位是 IDData 数据场通常 2/4/8 字节不同 ID 对应不同长度Checksum 校验和1 字节经典校验和或增强校验和这里可以用一个会议场景来理解主机是会议主持人break 是主持人敲桌子“安静一下”sync 是主持人告诉大家“我说话的速度是每分钟 19200 个字”PID 是点名“编号 0x3C 的节点回答问题”数据就是被点名的节点或主机要传递的内容校验和是防有人听错。2.3 PID 和校验和的算法细节PID 不是简单的 ID 本身它由 6 位 ID 和 2 位校验位组成公式如下P0 ID0 ⊕ ID1 ⊕ ID2 ⊕ ID4P1 ~(ID1 ⊕ ID3 ⊕ ID4 ⊕ ID5)也就是说PID (ID 0x3F) | (P0 6) | (P1 7)。这个公式很容易记错尤其是 P1 的取反。我强烈建议直接用查表法或者写一个函数不要每次手算。校验和有两种经典校验和只累加数据字节增强校验和要先把 PID 也累加进去。LIN 2.x 规范里大多数帧用增强校验和诊断帧必须用经典校验和。如果你的主机和从机不是同一个供应商这里最容易出问题。3. F103 的 USART 在 LIN 模式下帮你做了哪些事3.1 硬件能力清单F103 的 USART 外设确实是为 LIN 留了后门的。打开参考手册的 USART 章节能看到 LIN 模式相关的寄存器位。硬件主要提供了三个能力break 发送通过 USART_CR2 的 LBCL 位选择 break 长度是 10 位还是 11 位设置 SBK 位后硬件自动拉低 TX 引脚指定时长。break 检测通过 LBDL 位选择检测 10 位还是 11 位连续低电平一旦检测到USART_SR 的 LBD 标志置位可以开 USART_IT_LBD 中断。自动波特率检测USART_CR2 的 ABRMOD 字段可以配置为按 0x55 头检测波特率这个在 LIN 同步场场景下正好能用但实际工程中如果两端都用外部晶振固定波特率一般不依赖它。用标准外设库配置起来很简单USART_InitStructure.USART_BaudRate 19200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_LINConfig(USART1, USART_LINBreakDetectLength_11b); USART_LINCmd(USART1, ENABLE); USART_ITConfig(USART1, USART_IT_LBD, ENABLE);要注意的是break 检测的 10/11 位阈值是经过设计的普通 UART 字节最多只有 9 位连续低电平起始位 8 位数据全 0所以 10 位检测不会误触发。实际使用中我更推荐把检测长度配置成 11 位兼容性更好。3.2 F103 没有完整 LIN 控制器状态机还得自己写很多从其他芯片转过来的朋友容易有个误区以为 F103 的 LIN 模式会像完整 LIN 控制器那样自动处理 PID 滤波、帧超时、调度表。实际上 F103 只做了 break 这一层的硬件加速整帧的协议解析仍然要靠软件状态机。也就是说从机收到 break 中断后后续 0x55、PID、数据、校验和是什么含义数据长度是多少要不要响应都是代码里的事情。这也意味着你要在中断服务函数里维护一个解析状态机。这听起来麻烦但好处是很灵活——你可以自己决定支持哪些 ID、数据长度多少、用经典还是增强校验和。对于 F103 这种资源不算大的芯片手写状态机反而比套一个完整的 LIN 协议栈更可控。3.3 硬件连接最小系统板怎么接我用的是一块普通的 F103 最小系统板PA9 和 PA10 分别作为 USART1 的 TX 和 RX外接一个 LIN 收发器模块。收发器的 TXD 接 PA9RXD 接 PA10LIN 引脚接总线再给收发器供 5V 或 3.3V 电源。这里有个常见的坑LIN 收发器一般需要 VBAT 供电才能正常工作如果你只给模块的 VCC 供电而没接 VBAT总线上永远没有正确的隐性电平表现出来就是主机发了数据总线上却抓不到波形。如果你只是做 TTL 级验证那 PA9/PA10 直接接逻辑分析仪就够了。4. 可直接落地的示例代码主机发送与从机接收4.1 PID 和校验和计算函数先把最基础的两个函数写出来后面所有收发逻辑都会调用它们static uint8_t lin_calc_pid(uint8_t id) { uint8_t id0 (id 0) 0x01; uint8_t id1 (id 1) 0x01; uint8_t id2 (id 2) 0x01; uint8_t id3 (id 3) 0x01; uint8_t id4 (id 4) 0x01; uint8_t id5 (id 5) 0x01; uint8_t p0 id0 ^ id1 ^ id2 ^ id4; uint8_t p1 ~(id1 ^ id3 ^ id4 ^ id5) 0x01; return (id 0x3F) | (p0 6) | (p1 7); } static uint8_t lin_checksum(const uint8_t *data, uint8_t len, uint8_t pid, uint8_t enhanced) { uint16_t sum 0; uint8_t i; if (enhanced) { sum pid; } for (i 0; i len; i) { sum data[i]; } while (sum 8) { sum (sum 0xFF) (sum 8); } return (uint8_t)(~sum); }注意增强校验和算法里PID 要在累加数据之前先加进去。有些实现是最后才加 PID那样算出来的结果跟标准不一致主从双方校验和永远对不上。4.2 主机发送一帧 LIN 报文主机发送是 LIN 通信中最常见的操作。流程是发 break → 发 0x55 → 发 PID → 发数据 → 发校验和。使用 F103 硬件 break 发送时最简洁的写法是这样void lin_master_send_frame(uint8_t id, const uint8_t *data, uint8_t len) { uint8_t pid lin_calc_pid(id); uint8_t i; // 1. 发送 break硬件最多发 11 位 USART_SendBreak(USART1); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 2. 同步场 USART_SendData(USART1, 0x55); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); // 3. PID USART_SendData(USART1, pid); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); // 4. 数据场 for (i 0; i len; i) { USART_SendData(USART1, data[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); } // 5. 校验和这里用增强校验和 uint8_t cs lin_checksum(data, len, pid, 1); USART_SendData(USART1, cs); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); }发 break 之后必须等 TC 标志置位再发下一个字节否则 break 还没发完同步场已经进移位寄存器了波形就乱了。很多示例代码没写这个等待实测会在高速率下表现得很诡异。如果对方从机对 break 长度要求严格需要 13 位 break硬件就发不了那么长。这时候可以用软件方式先把 TX 引脚拉低 13 个位时间void lin_send_break_software(uint32_t bit_time_us) { // 先拉高保证总线空闲至少 1 位时间 GPIO_SetBits(GPIOA, GPIO_Pin_9); delay_us(bit_time_us); // 13 位显性电平break GPIO_ResetBits(GPIOA, GPIO_Pin_9); delay_us(13 * bit_time_us); // 至少 1 位隐性作为 break delimiter GPIO_SetBits(GPIOA, GPIO_Pin_9); delay_us(bit_time_us); // 这里把 PA9 重新配置为 USART1_TX 复用功能 }以 19200 波特率为例1 位时间约 52 微秒13 位 break 约 677 微秒。用定时器延时比粗暴的 delay 循环更可靠因为中断会影响循环精度。发送完 break 后记得把引脚切回 USART 复用功能不然后续 0x55 根本发不出去。4.3 从机接收状态机从机的难点是解析状态机。收到 break 中断后后面每个字节都要根据当前状态决定怎么处理typedef enum { LIN_STATE_WAIT_BREAK, LIN_STATE_WAIT_SYNC, LIN_STATE_WAIT_PID, LIN_STATE_WAIT_DATA, LIN_STATE_WAIT_CHECKSUM } lin_state_t; volatile lin_state_t lin_state LIN_STATE_WAIT_BREAK; volatile uint8_t lin_rx_buf[8]; volatile uint8_t lin_rx_index 0; volatile uint8_t lin_rx_pid 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_LBD) ! RESET) { USART_ClearITPendingBit(USART1, USART_IT_LBD); lin_state LIN_STATE_WAIT_SYNC; lin_rx_index 0; return; } if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); switch (lin_state) { case LIN_STATE_WAIT_SYNC: if (byte 0x55) { lin_state LIN_STATE_WAIT_PID; } else { lin_state LIN_STATE_WAIT_BREAK; } break; case LIN_STATE_WAIT_PID: lin_rx_pid byte; lin_rx_index 0; // 根据 PID 判断数据长度这里简化为固定 8 字节 lin_state LIN_STATE_WAIT_DATA; break; case LIN_STATE_WAIT_DATA: lin_rx_buf[lin_rx_index] byte; if (lin_rx_index 8) { lin_state LIN_STATE_WAIT_CHECKSUM; } break; case LIN_STATE_WAIT_CHECKSUM: { uint8_t calc lin_checksum(lin_rx_buf, 8, lin_rx_pid, 1); if (calc byte) { // 帧接收成功可以置标志位让主循环处理 } lin_state LIN_STATE_WAIT_BREAK; break; } default: lin_state LIN_STATE_WAIT_BREAK; break; } } }实际项目中数据长度不能写死要根据 PID 查一个长度表因为不同 ID 的帧长度不同。这里用固定 8 字节只是为了展示状态机的流转逻辑。5. 调试过程中踩过的坑和排查思路5.1 主机发了数据RX 端一直收到 0xFF0xFF 意味着 UART 接收引脚一直处于空闲高电平说明总线根本没有有效的显性电平信号。我当时排查顺序是先看收发器供电再看总线侧上拉最后才发现是收发器的 TXD 和 RXD 接反了。如果只是 MCU 直连不经过收发器那 RX 引脚必须有外部上拉。USART 的空闲状态是高电平如果引脚悬空或者被拉低会不停产生错误帧。解决办法是确认 RX 引脚配置成浮空输入或带上拉输入并且 TX 引脚不能一直输出低。5.2 检测到 break但同步场解析总是失败这个坑十有八九是 break 发送长度和检测长度不匹配。主机用硬件发送 11 位 break从机如果配置成 10 位检测从机在 break 中间就被触发后面同步场的第一个位被当成 break 的尾巴吞掉导致 0x55 变成乱码。我的建议是主机和从机统一使用 11 位检测长度主机的 LBCL 也配置成 11 位。如果严格要求 13 位 break就用前面给的软件发送方式从机仍然配置 11 位检测因为 13 位 11 位检测触发点依然有效。5.3 数据字节全对但对方就是不回帧大概率是校验和版本不一致。经典校验和不算 PID增强校验和算 PID差的就是一个字节的事情。很多国产 LIN 从机模块默认用经典校验和而 F103 示例代码里如果你写了增强校验和数据格式看起来完全正常但对方接收端校验失败直接丢弃整帧表现为“无响应”。调试方法很简单先用逻辑分析仪抓出完整帧手动算一遍校验和然后跟发送的字节对比。我后来在自己的代码里加了配置项通过一个宏切换两种校验和#define LIN_CHECKSUM_ENHANCED 1 // 0 为经典校验和这样在项目里切换起来方便很多。5.4 短帧正常长帧偶尔出错这是我调 LIN 时最隐蔽的一个问题。短数据帧2 字节怎么发都对换成 8 字节数据帧之后帧尾时不时校验失败。用逻辑分析仪一看发现越靠后的字节位宽度偏差越明显。原因很简单主机和从机的波特率都存在微小误差单个字节里误差积累不明显一帧 10 多个字节下来累计偏差就会在最后几个字节处超过采样容错范围。解决办法有两个方向一是两端都用外部晶振特别是 F103 最小系统板上常见的内部 RC 振荡器精度不够必须换外部晶振二是把波特率降到 9600位时间变长容错能力增强。6. 工程落地时的几条实用建议6.1 什么情况下 F103 够用什么情况下建议换芯片F103 做简单的 LIN 主从通信完全够用尤其是只有几个 ID、帧长不长、波特率不超过 19200 的项目。但如果节点数很多、调度表复杂、需要大量帧超时管理或者对实时性要求很高那就别在 F103 上硬扛了。F0/G0 系列的部分型号对 LIN 支持更完整或者直接用带 LIN 控制器的专用芯片开发周期能缩短不少。另外如果项目里已经有 CAN 总线F103 本身自带 bxCAN可以把高速 CAN 和低速 LIN 同时跑一个芯片搞定两层网络这种组合在车身电子里很常见。6.2 波形验证是调 LIN 的第一生产力我每次调 LIN 都会先抓波形再谈协议。逻辑分析仪接到 MCU 的 TX 引脚上先看 break 低电平持续时间是不是接近 13 位时间再看 0x55 是不是清晰的 01010101 交替电平。波形对了协议解析才有意义。如果波形不对花再多时间看寄存器都是白费。一个典型的 19200 波特率 LIN 帧逻辑分析仪上应该能看到一段约 677 微秒的低电平 → 1 位高电平 → 0x55 → PID → 数据 → 校验和。这个特征非常明显一眼就能认出来。6.3 关于中断优先级和缓冲区的一点心得LIN 从机接收时如果主循环业务很重RXNE 中断不及时处理USART 的 overrun 错误标志会置位后续字节全部丢弃。我在工程里一般把 USART1 中断优先级配成最高并且在 overrun 标志置位时手动清除并重新进入等待 break 状态。if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) ! RESET) { USART_ReceiveData(USART1); // 读掉数据清除 ORE lin_state LIN_STATE_WAIT_BREAK; }这个处理看着简单但能避免很多“偶尔卡死”的诡异现象。缓冲区方面如果一帧数据最多 8 字节那就用 8 字节数组不要用链表中断里动态分配内存是大忌。我自己的习惯是每次调 LIN 之前先把主机侧波形完整抓一遍确认 break、sync、PID、校验和都符合协议再开始调从机。这个习惯帮我省了很多时间。如果你也正在 F103 上跑 LINBUS建议先抓波形再谈协议栈几乎所有的玄学问题最后都能在波形上找到答案。本文还有配套的精品资源点击获取