
简介该资源是面向STM32F103开发者的LIN总线通信示例工程重点演示如何利用USART3中断检测LIN报文、解析同步字段与标识符并在校验正确后完成数据帧及CRC的发送。工程配合TPIC1021AQDRQ1收发器以19200bps速率运行适合正在学习车载总线协议或需要快速搭建LIN通信验证平台的嵌入式工程师参考。资源共包含38个文件其中主要涉及18个h头文件、11个c源文件以及链接脚本、工程配置文件等压缩包整体约226KB代码结构紧凑便于按模块阅读和移植。目前已有1995人学习说明该示例在同类入门与验证场景中具备一定参考热度。读者可通过阅读HAL库驱动、中断处理与CRC计算逻辑理清LIN主从通信的基本流程同时借助IAR工程文件快速编译测试从而减少从零调试总线时序的工作量。1. 一个被忽视的事实STM32F103跑LIN总线难点不在波特率而在帧头识别很多人拿到LIN项目的第一反应是“这不就是串口吗改个波特率就能收发”。实际烧进去跑一遍就发现同一个字节发出去从节点就是不回应逻辑分析仪上总线的波形跟协议手册对不上。这套STM32F103_LINBUS示例代码解决的核心问题正是这层“黑匣子”同步间隔场怎么检测、同步场怎么校准波特率、PID怎么过滤、校验和怎么算、调度表怎么走全部拆成了可读的C代码主从角色都覆盖。适合正在做车身电子、传感器采集、低成本分布式节点的人尤其是想让LIN节点在STM32F103上快速落地、而不是把时间浪费在协议细节上的开发者。2. 协议关与技术选型LIN总线为什么不能当普通串口写2.1 LIN一帧的完整结构从同步间隔场到校验和LIN一帧并不是“起始位8个数据位停止位”那样一次发完。它由主机发送的帧头Header和主/从机发送的响应Response组成。帧头固定包含三部分同步间隔场Break、同步场0x55、受保护IDPID。Break是一个持续至少13位时间的显性电平也就是拉低总线同步场固定为0x55它的下降沿是连续且均匀的从机可以据此测量总线实际波特率PID由6位真正ID和2位奇偶校验位拼成总线上传的是这8位不是裸ID。响应部分由1到8字节数据加一个校验和组成。校验和有两种经典校验和只覆盖数据字节增强校验和覆盖PID加数据字节。LIN 1.x规范用经典校验和LIN 2.x之后统一用增强校验和示例代码里用的是增强校验和因为兼容性和报文过滤都更好。很多串口习惯的人在这里踩坑以为发出去的过程是一气呵成的字符流实际上LIN在字符层面有时间约束在帧层面还要区分帧头由谁发、响应由谁发。这套结构给底层带来的直接问题就是“普通串口协议里没有超长低电平”。标准串口如果连续收到0x00也只是正常的地址或数据但LIN里的Break比一个最长数据帧还要长必须单独识别不能用读数据寄存器的方式来解释它。这就是LIN不能直接套串口收发代码的本质原因。2.2 STM32F103的USART外设能不能直接干这件事F103的USART没有硬件LIN控制器也没有后续新系列才配的break检测中断。所以工程上通常把USART当普通数据通道Break检测交给定时器输入捕获去完成。示例代码采用的方案是从节点RX引脚同时接到USART接收脚和TIM的输入捕获通道先测RX上低电平持续时间大于阈值就判定Break到来然后使能USART接收接下来的0x55和PID。同步场的存在本来是为了做自动波特率校准但自动校准要重新计算并重写USART_BRR寄存器代码量不小。很多量产工程在波特率固定时直接跳过这一大步把同步场当成帧同步的起始标记用。示例代码给了两条路径一条是固定波特率快速验证另一条是测量0x55两个下降沿间隔来重算BRR。我一般建议先把固定波特率跑通能收到数据再往自动校准迁否则第一次上总线就同时面对波形、点频和应答三个问题很难定位。USART本身的配置也不复杂8位数据、无校验、1停止位开启RXNE接收中断和IDLE空闲中断。RXNE负责收字符IDLE用来判断响应帧结束二者配合才能知道“一帧收完了”。如果只开RXNE不开IDLE从机响应是变长的你永远不知道数据从哪里结束。2.3 硬件连接MCU的3.3V逻辑电平与LIN总线物理层LIN物理层是单线总线总线空闲时是12V高电平显性电平被拉低到接近地。MCU的3.3V电平不能直接驱动总线中间必须加LIN收发器。收发器把MCU的TXD/RXD逻辑电平转成总线电平自己需要一路12V供电。现在市面上的LIN收发器模块引脚基本是TXD、RXD、BUS、12V、GND接法非常规整。最小系统需要两块STM32F103板子一块配成主节点一块配成从节点。主节点USART1的TX接主收发器的TXDRX接主收发器的RXD从节点USART2的TX接从收发器的TXDRX接从收发器的RXD。两个收发器的BUS引脚互连就是同一条LIN总线。主从开发板的GND要跟12V电源的GND共地否则总线电平参考点不一致波形会乱飘。连接项接法主节点USART1_TX接主收发器TXD主节点USART1_RX接主收发器RXD从节点USART2_TX接从收发器TXD从节点USART2_RXPA3接从收发器RXD同时接TIM2_CH4输入捕获BUS接口两个收发器BUS脚互联供电收发器VSUP接12VMCU板用各自供电GND共地这里有一个容易忽略的细节从节点RX引脚要同时进USART2和定时器输入捕获通道。F103的引脚复用刚好支持PA3同时作为USART2_RX和TIM2_CH4输入工程里就是按这个映射配置的。如果你换了板子需要重新查引脚复用表不能照抄接线。主节点和从节点的区分不是硬件差异而是协议角色。总线上必须有且仅有一个主机节点负责发帧头、维护调度表从节点只能被点名后应答。所以两块板子烧同一个固件、只在配置表里区分角色就能工作这也是示例代码把角色做成枚举写在配置表里的原因。3. 核心代码拆解一帧从主机发出到从机应答的完整链路3.1 目录结构与模块划分示例代码按“配置、协议、应用”三层拆开目录结构大致如下stm32f103_linbus/ ├── lin_cfg.h # 节点角色、应答表、调度表配置 ├── lin_if.h # 协议层对外接口 ├── lin_if.c # 帧头状态机、PID解析、校验和 ├── lin_master.c # 主机调度表轮询、帧头发送 ├── lin_slave.c # 从机应答、应用回调 ├── tim_break.c # 定时器输入捕获检测Break └── main.c # 中断入口、主循环lin_cfg.h是业务改动的入口新增一个信号量几乎只改这里lin_if.c是协议核心处理Break识别后的同步场和PID以及数据收发状态机lin_master.c和lin_slave.c分别对应主从角色逻辑tim_break.c处理最底层的物理信号测量。main.c里把定时器中断和USART中断接进协议层。从代码运行路径看主节点上电后进入主循环lin_master_poll按调度表周期调用到点就通过lin_if_send_header发出帧头。从节点那边TIM2输入捕获先测到Break低电平置位break标志USART2紧接着收到0x55和PIDlin_if_rx_byte进入协议状态机。这一进一出正好覆盖完整的一帧交互调试时只要盯住这两条路径就能快速定位问题出在谁身上。3.2 配置表节点角色、PID表与调度表先看lin_cfg.h里最核心的几个定义typedef enum { LIN_NODE_MASTER 0, LIN_NODE_SLAVE } LinNodeRole; typedef struct { uint8_t pid; /* 受保护ID含奇偶校验位 */ uint8_t dataLen; /* 该帧数据长度最大8 */ uint8_t checksumType; /* 0经典校验和 1增强校验和 */ } LinFrameCfg; /* 从机模式下本节点需要应答的帧表 */ static const LinFrameCfg slaveRespTable[] { { 0x18, 4, 1 }, /* PID 0x184字节数据增强校验 */ { 0x2A, 1, 1 }, /* PID 0x2A1字节数据增强校验 */ }; /* 主机模式下调度表每帧的间隔单位ms */ static const LinScheduleTable masterSchedule[] { { 0x18, 10 }, /* 每10ms发一次0x18的帧头 */ { 0x2A, 10 }, };这里需要特别说明的是pid字段。它必须包含bit6和bit7的奇偶校验位不能把裸ID填进去。从机收到的PID要经过校验校验通过后再与应答表比对比对时用的是解析出来的6位ID。dataLen不要超过8LIN协议最大数据长度就是8字节。checksumType建议全部用增强校验和只有对接老的LIN 1.x节点时才需要改成经典。调度表的间隔也不是随便填的。一次最小交互至少包含13位Break加10位0x55加10位PID再加上响应数据19200波特率下一帧需要2到4ms。调度间隔填1ms会让总线上连续拥堵常见取值是5ms以上。示例里默认10ms就是为了让逻辑分析仪看得清。3.3 Break检测与帧头接收Break是LIN帧最特殊的物理特征。固定波特率模式下从机用定时器输入捕获测量RX引脚低电平宽度/* 19200bps位时间约52usBreak要求至少13位约676us */ #define BREAK_MIN_US 700 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { uint16_t width TIM_GetCapture1(TIM2); uint32_t low_time_us width / 72; /* 72MHz计数换算成us */ if (low_time_us BREAK_MIN_US) { lin_if_signal_break_detected(); } } }输入捕获测的是低电平持续时间这里的72是72MHz主频下每微秒的计数个数。如果你把系统时钟改成48MHz除数要同步改成48否则阈值全部漂移。捕获到Break后协议层把状态从“等待Break”切到“接收同步场”然后USART中断开始收字节。USART接收的状态机是lin_if.c里最值得读的一段static uint8_t lin_rx_state LIN_RX_WAIT_BREAK; void lin_if_rx_byte(uint8_t byte) { switch (lin_rx_state) { case LIN_RX_WAIT_BREAK: /* break检测由定时器置位这里不处理字节 */ break; case LIN_RX_SYNC: if (byte 0x55) lin_rx_state LIN_RX_PID; else lin_rx_state LIN_RX_WAIT_BREAK; /* 同步场不正确重新等待 */ break; case LIN_RX_PID: lin_if_on_pid_received(byte); lin_rx_state LIN_RX_WAIT_BREAK; break; } }同步场判定失败就回到等待Break这是容错的关键。实际总线上难免有干扰脉冲如果收到不是0x55的字节还硬往前走PID就会错位后面的数据全部白收。主节点发送帧头时Break部分也有一个常见做法先把TX引脚切换成GPIO输出拉低至少13位时间再切回USART发送0x55和PID。GPIO操作和USART复用切换之间要留一点时间否则第一帧的0x55会丢半个字节。3.4 PID校验与校验和计算PID由6位ID加2位奇偶校验构成。校验位公式在协议里是固定的P0由ID的bit0、bit1、bit2、bit4异或得到P1由bit1、bit3、bit4、bit5异或后取反得到。示例代码里有一个生成函数static uint8_t lin_make_pid(uint8_t id) { uint8_t p0 ((id 0) ^ (id 1) ^ (id 2) ^ (id 4)) 0x01; uint8_t p1 ~((id 1) ^ (id 3) ^ (id 4) ^ (id 5)) 0x01; return (id 0x3F) | (p0 6) | (p1 7); }我建议在工程里把64个ID对应的PID提前生成好存成表运行时不调用这个公式直接用id做下标查表。因为公式里面位运算多手工心算几乎必错查表法一次算完永久使用。从节点收到PID后先做校验再比对校验不过的直接丢弃不进入应答流程。校验和的计算也要按LIN规范来不能直接取反uint8_t lin_checksum(uint8_t pid, const uint8_t *data, uint8_t len, uint8_t type) { uint16_t sum 0; uint8_t i; if (type 1) sum pid; /* 增强校验和覆盖PID */ for (i 0; i len; i) { sum data[i]; if (sum 256) sum - 255; /* 进位回卷 */ } return (uint8_t)(~sum); }这段算法的关键在于“进位回卷”每累加一个数据字节如果和大于等于256就减255而不是把进位丢掉。最后按位取反才是总线上发送的校验字节。很多从串口代码移植过来的工程在这里翻车因为普通串口校验和直接把低位字节取反就发出去LIN从节点会一直报校验错误波形看起来又完全正常。3.5 主机调度表与从机应答的配合主节点的调度逻辑在lin_master.c里非常直白void lin_master_poll(void) { static uint32_t last_tick 0; static uint8_t idx 0; uint32_t now get_ms(); if (now - last_tick masterSchedule[idx].intervalMs) { lin_if_send_header(masterSchedule[idx].pid); last_tick now; idx (idx 1) % (sizeof(masterSchedule) / sizeof(masterSchedule[0])); } }这是一个周期轮询的循环链表主节点按时间片依次发不同PID的帧头。从机识别出帧头PID属于自己后并不会立刻把数据扔到总线上而是等一小段帧内空闲时间再切换USART为发送模式。这段等待时间在协议里叫响应空间示例代码用一个宏控制#define LIN_TURNAROUND_TIME_US 200 /* 帧头结束到响应开始的最小间隔 */这个值是调试中最常动的参数之一。太短主机还没切回接收状态从机就开始发数据总线冲突太长调度周期变长整个网络的实时性下降。示例默认值在19200波特率下足够稳定如果你的总线节点多了建议用示波器实测后再调。从机响应结束后主机的USART空闲中断会触发标志当前这帧接收完成上层应用就可以从缓冲区里读数据了。整个链路从主机发帧头到从机返回数据和校验和再到主机回收数据全部靠状态机串起来。4. 跑通工程与排查把两个节点拉到一条总线上4.1 最小硬件清单与接线跑通这套示例不需要专门的LIN开发板。两块STM32F103最小系统板、两个LIN收发器模块、一个12V电源、一个逻辑分析仪就够了。接线按第2.3节的表格来这里补几个容易出问题的点收发器模块的TXD和RXD有方向性TXD是MCU到收发器RXD是收发器到MCU接反现象是“自己的串口能看到发出的数据但总线上没有波形”BUS脚直接互联不分正负但GND必须共地。如果手上只有逻辑分析仪挂在BUS脚和GND之间就能看到完整的帧波形。12V供电确认好收发器VSUP引脚不能接5V接5V时有些模块也能工作但总线电平到不了12V接远距离从节点时会丢帧。4.2 编译、烧录与波形验证示例工程用标准外设库写成打开集成开发环境直接编译即可。烧录时注意两块板子烧不同配置固件主节点板配LIN_NODE_MASTER从节点板配LIN_NODE_SLAVE。烧完上电主节点板加一句循环打印当前调度表状态从逻辑分析仪上应该能看到这样的周期波形12V高电平一个1ms左右的长低电平Break然后是0x55、PID、数据、校验和再回到12V高电平。没有逻辑分析仪的话可以临时把主节点的USART3接一个串口调试器波特率设成115200把lin_if接收完成的标志和数据打印出来。注意主节点的USART1已经被LIN占用所以调试串口要用USART3F103的USART3默认引脚是PB10和PB11。从节点单独上电时总线上只有它一个设备不会主动发任何数据。这是正常现象LIN从节点本身就是“被点名才发言”如果一上电总线就忙说明从节点被配成了主节点或者代码里开了自治发送。4.3 避坑记录我在这套代码上踩过的五个问题现象1Break能被定时器识别但同步场0x55经常收错。原因两个板子一个用外部晶振一个默认用内部RC振荡器实际波特率偏差超过LIN规定。解决统一改用外部8MHz晶振并把系统时钟配到72MHz固定波特率工程里主从时钟必须同源等级不能用内部RC凑合。现象2从节点收到PID后不响应逻辑分析仪上只有帧头。原因从机应答表里配的是6位裸ID而总线上传的是含校验位的PID比对时直接用整个字节比较永远对不上。解决先对PID做奇偶校验和ID解析解析出的6位ID再与应答表比对。现象3收发器模块发烫总线一直是0V或12V不动。原因12V供电正负接反或BUS脚接错导致收发器内部限流。解决断电检查VSUP和GND再量BUS对GND的静态电压正常应该是12V。现象4主节点收到从节点响应全是0xFF偶尔一帧乱码。原因从节点在帧头结束瞬间就开始发响应主节点还没从发帧头切换回接收模式两个方向冲突。解决加大LIN_TURNAROUND_TIME_US让从节点晚一点切换发送。现象5两个节点单板调试正常一接入其他从设备就频繁丢帧。原因每个板子都焊了终端电阻并联后总线负载过大。解决LIN规范要求主节点1kΩ终端电阻、从节点30kΩ把板载终端电阻按角色调整主节点保留1k从节点换30k或直接拔掉。5. 二次开发技巧三步把示例改成自己的从节点5.1 新增一个信号量的三步改法假设你要新增一路温度采集数据分配裸ID为0x25。第一步用lin_make_pid(0x25)生成对应PID把结果写进lin_cfg.h的slaveRespTable{ pid, 2, 1 }数据长度2字节。第二步在主节点的masterSchedule里加上一行让主机周期性发这个PID的帧头。第三步在从节点应用层回调里填数据void lin_app_process(uint8_t id, uint8_t *tx_data, uint8_t *len) { if (id 0x25) { int16_t temp read_temperature(); tx_data[0] temp 0xFF; tx_data[1] (temp 8) 0xFF; *len 2; } }这里传入的id是解析后的6位ID不是总线上带校验位的PID。tx_data是协议层准备好的发送缓冲区len返回实际数据长度最大8。协议栈会把校验和自动加在数据后面应用层不用管。5.2 验证方法主节点把收到的2字节温度值通过调试串口打印到电脑正常应该周期性出现对应的数值变化。再用逻辑分析仪对照调度表检查确认0x25的帧头按预期间隔出现从节点也确实在帧头之后返回了数据。调试时建议把调度间隔临时改成100ms肉眼跟踪方便链路稳定后再改回实际周期。5.3 改造时最容易漏的两处配置第一处是从机应答表加了PID但主机调度表没加导致主机永远不发这个帧头从节点没有机会开口。第二处是把裸ID直接填进PID字段线上广播的PID和本地表对不上。这两个问题现象相似都是“从节点不响应”但一个是主机的问题一个是从机的问题排查路径完全不同。我每次改配置表都会强制走一遍“查PID表、改应答表、改调度表、上逻辑分析仪”的流程防止PID算错。后来再接过几个LIN项目第一件事一定是先看对方的配置表和调度表再看协议栈顺序反了容易白费半天。这套示例把协议层和应用层分得很干净改动几乎全部集中在lin_cfg.h和一个回调函数里希望帮到你。本文还有配套的精品资源点击获取