STM32F103 LIN总线通信实战:从UART平滑过渡到LINBUS 简介面向嵌入式开发者的STM32F103 LIN总线通信示例工程围绕USART3外设实现完整LIN从机节点收发逻辑检测LIN中断、读取同步标志与标识符、校验标识符正确后发送8字节数据并计算CRC同时适配TPIC1021AQDRQ1收发器以19200Bps波特率稳定运行。工程共38个文件以18个h头文件与11个c源文件为主体头文件用于寄存器定义与函数声明源文件实现外设初始化、中断响应、数据发送及CRC校验等核心逻辑另有IAR工程配置文件ewp/ewd/eww、链接脚本icf、启动文件s和README说明直接导入IAR即可编译调试压缩包仅226KB。已有1993人学习下载适合想快速掌握LIN总线通信机制、中断驱动编程或CRC校验实现的中初级嵌入式开发者参考代码结构清晰模块划分便于对照协议规范学习也可作为LIN总线产品开发的基础模板。 只做了一件事的酷代码做好了就是真功夫在STM32F103上把LINBUS跑通。这不是什么炫技项目但是汽车电子、工业控制里绕不开的通信方式。标题里写的是“使用LINBUS的示例代码”实际做下来要搞明白的点一点不比CAN少波特率怎么配、同步间隔场怎么识别、校验算法选经典还是增强、从机节点要不要晶振。这篇文章我直接按自己调试这套东西时的思路来讲把原理、代码、硬件要点和踩过的坑一起说清楚适合手里有F103开发板、想从UART平滑过渡到LIN的朋友。看完你不仅能把这套示例代码跑起来还敢自己往上改。1. 项目背景与选型思路1.1 为什么选LIN而不是CAN或I2CLIN的全称是Local Interconnect Network本地互联网络。它在汽车电子里定位很明确CAN总线太贵对低速、低实时性的节点没必要。车窗、后视镜、雨量传感器、座椅调节这些报文量小、速率低用LIN就够了。它的物理层就是单根线基于UART协议成本可以压到很低。做这个项目之前我手里的板子是STM32F103最小系统板8MHz外部晶振标准库V3.5。最开始考虑过直接用CAN但F103的CAN外设虽然好用板子上还得加收发器而且CAN的报文仲裁、波特率配置对新手来说门槛偏高。后来翻参考手册发现F103的USART外设内置了LIN模式支持。这意味着只要把USART配置成LIN模式配合一个LIN收发器芯片比如TJA1020就能在硬件层面搞定大部分协议细节软件上只需要维护状态机和调度。注意这里的“LIN模式”不是完整协议栈而是USART硬件提供了对LIN帧结构的支持比如同步间隔场检测、同步场自动接收、校验场自动计算。真正的主从调度、报文超时和错误处理还是要自己写。1.2 标准库还是HAL库为什么选了标准库V3.5现在新项目基本都是STM32CubeMXHAL库但网上搜“STM32F103 LIN”出来的资料大量还是标准库时代的。我手里这块板子之前的工程都是标准库V3.5写的寄存器映射、外设配置都熟了加上LIN的示例代码在标准库里能找到相对完整的参考就用标准库了。如果你是用HAL库结构上其实没本质区别HAL_UART_Init()里把Mode配成UART_MODE_LIN然后使能LIN中断回调函数里处理同步间隔场中断和接收中断就行。只是细节API不同。标准库的好处是寄存器看得见摸得着出了bug好查。坏处是串口中断要自己写分发状态管理很容易乱。这个项目我最终决定用标准库还有一个原因LIN从机节点需要精确的波特率校准标准库下可以直接操作USART_BRR寄存器算波特率配合定时器做误差补偿调试手段更直接。2. LIN协议速览读懂帧结构后面代码才不会懵2.1 一帧报文里都有什么LIN的报文帧结构很有规律。主机发送报文头从机响应报文体。一帧完整的报文包含同步间隔场Break Field显性电平持续至少13位时间用于唤醒总线和标志帧起始同步场Sync Field固定为0x55用于从机校准波特率受保护ID场Protected ID6位ID加上2位奇偶校验通过异或实现数据场1到8字节具体长度由ID决定校验场经典校验或增强校验计算方式不同我画了一个简化的帧结构说明主机发送: [同步间隔][同步0x55][PID] - 从机响应: [数据1][数据2]...[数据N][校验]这里的重点在同步间隔场。普通UART是低电平起始位高电平空闲LIN要求显性电平低电平持续至少13位时间接收端要把这13位识别成“新一帧开始”而不是普通的起始位。F103的USART LIN模式有硬件检测功能能自动识别Break Field并产生LBDLIN Break Detection中断这就省去了软件上做超时判定的麻烦。2.2 为什么说LIN是“低成本CAN”从拓扑上看LIN就是单线总线主机一个从机最多15个。速率上限20kbps实际上汽车上常用的就是9600和19200。CAN的速率高、可靠性强但引脚多、收发器贵、协议栈复杂。LIN把协议简化到本地UART即可实现代价是速率和可靠性都低一些。实际选型时我是这样判断的传输距离在2米以内、速率不超过19200、节点数量个位数、不需要多主冗余直接用LIN一点问题没有。如果这些条件任一不满足老老实实上CAN。千万别用LIN做动力系统或者安全相关的通信那是拿生命开玩笑。2.3 LIN的两种校验算法选错了通信就是乱的LIN的校验场分经典校验Classic Checksum和增强校验Enhanced Checksum。经典校验只对数据场计算增强校验对受保护ID场和数据场一起计算。0x3C和0x3D两个ID固定用经典校验其余ID由节点配置决定。我在调试时遇到过主机用增强校验从机用经典校验结果高低温工况下偶发通信失败查了半天才发现两边校验方式没对上。这不是代码bug是协议配置不一致。标准库里LIN_ComputeParity()函数只算了PID的奇偶位校验场是要自己在发送数据时算好放进去的提前做好协议规划表会省掉很多麻烦。3. 核心代码设计与实现流程3.1 初始化USART1复用成LIN模式我的硬件连接是PA9USART1_TX→ TJA1020的TXDPA10USART1_RX→ TJA1020的RXDPA8 → TJA1020的SLP引脚用于休眠/唤醒控制LIN总线 → TJA1020的LIN引脚经过1kΩ电阻和ESD保护后接总线初始化代码里最关键的有两块GPIO配置和USART的LIN模式配置。GPIO要把TX配成复用推挽输出RX配成浮空输入SLP引脚配成通用推挽输出。void LIN_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // TX: PA9, 复用推挽输出 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); // RX: PA10, 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // SLP: PA8, 通用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); }这里的要点是TX脚必须是复用推挽不能用开漏。TJA1020的输入级需要正常的推挽驱动。如果误配成开漏输出示波器上看到的总线波形会很难看。3.2 USART LIN模式寄存器配置深度解析USART的LIN配置不像普通串口那样只写USART_Init()还需要额外启用LIN模式。标准库里的USART_LINConfig()函数接收两个参数USART_LIN_BreakDetectLength_10B或USART_LIN_BreakDetectLength_11B。F103支持10位或11位Break检测长度这一点和很多单片机不太一样——有些芯片只支持固定长度。void LIN_UART_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; USART_ClockInitTypeDef USART_ClockInitStructure; 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); // 使能LIN模式, 11位break检测 USART_LINConfig(USART1, USART_LIN_BreakDetectLength_11B); USART_LINCmd(USART1, ENABLE); USART_ITConfig(USART1, USART_IT_LBD, ENABLE); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }波特率配置上F103的USART是fck / (16 * USARTDIV)。以8MHz外部晶振为例19200波特率时USARTDIV 8000000 / (16 * 19200) 26.0417取整存到BRR寄存器是26 4 | 0实际小数部分是0x0。这里建议用官方提供的USART_Init()它会自动算好不要自己手写BRR除非你需要做波特率校准的自定义逻辑。3.3 主机发送完整报文帧主机发送是LIN节点最核心的功能。报文头由主机发包括同步间隔、同步场和PID。数据场如果从机要回复则从机会在PID后自动发起响应。作为主机发送流程是void LIN_Master_SendFrame(uint8_t id, uint8_t *data, uint8_t len) { uint8_t pid LIN_ComputePID(id); uint8_t checksum LIN_ComputeChecksum(id, data, len); // 1. 发送同步间隔场 USART_SendBreak(USART1); while (!(USART1-SR USART_FLAG_TC)); // 2. 发送同步场 0x55 USART_SendData(USART1, 0x55); while (!(USART1-SR USART_FLAG_TC)); // 3. 发送PID USART_SendData(USART1, pid); while (!(USART1-SR USART_FLAG_TC)); // 4. 发送数据场 for (uint8_t i 0; i len; i) { USART_SendData(USART1, data[i]); while (!(USART1-SR USART_FLAG_TC)); } // 5. 发送校验场 USART_SendData(USART1, checksum); while (!(USART1-SR USART_FLAG_TC)); }这里要注意USART_SendBreak()和USART_SendData()之间要有充分的延时。TJA1020在Break后会进入接收状态如果紧接着发同步场太快接收端可能还没从Break检测中恢复。实测下来在Break后加50μs左右的延时比较稳妥。3.4 从机接收状态机从机的处理思路和主机完全不同。从机不能主动发报文它只能接收、判断PID是否属于自己、然后决定是回复数据还是接收数据。接收侧的核心是LBD中断——硬件检测到同步间隔场后进入再连续接收同步场和PID然后根据PID查表。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_LBD) ! RESET) { USART_ClearITPendingBit(USART1, USART_IT_LBD); lin_state LIN_STATE_SYNC; // 同步间隔场已检测到 } if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); switch (lin_state) { case LIN_STATE_SYNC: if (byte 0x55) // 验证同步场 lin_state LIN_STATE_PID; else lin_state LIN_STATE_IDLE; break; case LIN_STATE_PID: lin_state process_pid(byte); // 解析PID, 判断本节点是否响应 break; case LIN_STATE_DATA: // 接收数据场, 存到缓冲区 lin_rx_buf[lin_rx_cnt] byte; break; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }这个状态机的设计要点是LBD中断是“硬同步点”意味着新一帧开始后续的0x55和PID判断则是“软校验”。如果硬件检测到Break但后续字节不是0x55说明总线质量差或者波特率偏差太大建议直接把状态机复位回IDLE。从机的数据回复不能在中断里做大量计算。尤其是校验场的计算如果报文长度是8字节用经典校验算法要异或9次在8MHz主频下也就几微秒的事但中断里最好只做数据缓存实际响应放在主循环标志位触发避免长时间关中断影响其他外设。4. 硬件电路、波特率误差与调试实录4.1 TJA1020收发器的外围电路LIN的物理层收发器我用的是TJA1020这是NXP的经典型号。外围电路极其简单TXD接STM32的TXRXD接RXSLP接普通GPIO用于休眠控制LIN输出串一个1kΩ电阻再接总线。注意在LIN引脚对地要加TVS管比如PESD1LIN和反压保护二极管这关系到整车的ESD和抛负载测试。关键点TJA1020的TXD引脚内部有上拉平时保持高电平总线就处于隐性状态。如果你的MCU在启动瞬间TX默认输出低电平总线会被拉成显性状态有可能让其他节点认为你在发Break。解决方案是在MCU的TX到TJA1020的TXD之间串一个10kΩ电阻MCU复位时即使为低也不会强拉总线。4.2 波特率误差到底能容忍多少LIN协议要求从机节点的位时间误差控制在±14%以内但两个节点的累计误差不能超过一个位的采样点宽度。我实测下来用8MHz晶振配19200理论误差为0实际示波器测出来在±1%以内非常稳。如果你的从机用的是内部RC振荡器省掉晶振那就要特别注意了。F103的内部RC在全温域下大约有±2%的偏差大多数情况下能工作但在极端温度下可能接近极限。LIN的同步场0x55就是干这个用的——从机收到0x55后可以测量每个位的宽度来重新校准自己的波特率这就是LIN的从机自主同步机制。标准库下要实现这个需要打开USART的OverSampling为16倍在同步场中断里用定时器捕获相邻下降沿的间隔然后动态调整USART_BRR。4.3 调试LIN总线必备工具和手法调试LIN我推荐以下工具组合工具用途示波器至少2通道看总线电平、测量Break宽度、数据位时序USB转LIN调试器模拟主机或从机抓总线报文逻辑分析仪长时间抓帧、分析时序异常实测最有效的调试手法是在USART_SendBreak()后用示波器抓波形确认Break段长度够13位。F103硬件产生的Break长度是11位但LIN协议要求13位这个差异需要注意。如果需要严格满足协议需要在TX引脚上手动拉低延长Break时间或者在硬件上允许11位Break有些从机节点能识别。4.4 常见故障排查速查表实际调试一个周末遇到的问题集中在下面这几类现象可能原因解决办法通信完全没反应TX/RX接反、收发器供电异常、Break长度不够先用示波器看TX、LIN引脚波形确认数据在MCU侧已发出能发不能收RX未配浮空输入、TJA1020 RXD开漏需要上拉检查GPIO配置RXD引脚加10kΩ上拉到3.3V偶发丢帧波特率误差大、中断被其他高优先级抢占用定时器捕获同步场校准波特率中断里只做快速处理从机不响应PID校验不对、从机地址过滤被短路单独用LIN调试器发PID看从机是否回复总线一直显性TX脚被拉低、总线对地短路断开收发器单独测MCU TX波形4.5 从实际项目中总结的4条经验第一先调通自发自收再做双机联调。把USART1的TX直接短接到RX上经1kΩ电阻用主机代码发报文中断里解帧。这样可以排除收发器和线束因素快速验证协议代码逻辑。第二状态机一定要有超时保护。总线上的干扰是不可避免的如果Sync Field收到一个错误的字节就永远卡在某个状态下一帧正常报文就进不来了。我在主循环里加了一个5ms的看门狗定时器超过这个时间没有完成一帧就强制恢复IDLE。没有这个保护整车电磁干扰场景下系统会很脆弱。第三PID的奇偶校验位不能省。有些示例代码在PID计算时报头直接用ID左移两位P0和P1位置置0在纯主从一对一的场景下没问题但多节点时就是灾难。标准库里LIN_ComputeParity()的实现是uint8_t LIN_ComputeParity(uint8_t id) { uint8_t p0 0, p1 0; for (uint8_t i 0; i 4; i) { p0 ^ ((id i) 0x01) (i); p1 ^ ((id (i 1)) 0x01) (i); } return ((id 0x3F) | (p0 6) | (p1 7)); }第四校验场算法要与ID绑定。0x3C和0x3D诊断帧固定用经典校验其他ID用增强校验还是经典校验必须提前在协议设计文档里定义清楚代码里做条件分支选择。5. 样例工程外扩方向这个LINBUS示例代码跑通之后能扩展的地方很多。最简单的再加几个从机节点把门控、灯光、传感器都挂到总线上做一个完整的车身控制演示进阶一点的把波特率改成自动校准模式用自己的板子实时调整BRR寄存器值来适配不同主机的波特率偏差再深入一些可以加入LIN的休眠与唤醒管理用总线信号控制SLP引脚让节点在总线空闲时自动进入低功耗模式。如果是做产品级设计建议进一步考虑MCU的资源占用和调度设计比如从机节点数量多时用独立定时器做报文调度避免在中断里做复杂协议解析把协议栈代码从应用代码中剥离出来。这个项目里的框架可以直接作为起点往里面填充自己的业务逻辑就行。本文还有配套的精品资源点击获取