资讯动态

STM32 LIN总线实战:从帧结构到主机从机完整实现

发布时间:2026/10/13 20:30:01 来源:尧图企业网站定制
简介这份资源面向嵌入式开发初学者与汽车电子方向工程师提供在STM32F103C8上实现LIN总线主机与从机通信的完整工程代码。LIN作为低成本低速串行协议常用于车身传感器与执行器组网资源围绕USART的LIN模式配置、时钟同步中断、帧结构与CRC校验、主机调度与从机响应、错误检测等核心环节展开适合作为协议入门与项目移植的参考。压缩包共111个文件约2.84MB包含c与h源码、s启动汇编、uvprojx与uvoptx工程配置、axf与hex可执行产物、map与lst编译映射文件以及crf、o、d等编译中间文件工程结构完整可直接用Keil打开编译调试。目前已有3769人学习下载。通过阅读主函数、初始化、中断服务与帧处理代码读者能掌握LIN通信的底层驱动实现与调试方法并可按需扩展从机节点与数据种类。1. 从一次总线调试翻车说起这套 STM32 LIN 程序到底能干什么如果你手头有一块 STM32 开发板想接一个车用 LIN 节点——比如车窗升降、雨量传感器、座椅调节这类低速执行器——大概率会遇到一个尴尬局面CAN 的例程满网都是LIN 的完整可跑工程却少得可怜。要么是只有一段初始化代码要么是拿串口 USART 硬凑中断一开就丢帧。这套 STM32 LIN 程序解决的正是这个断层它把 LIN 2.x 协议里的主机/从机角色、帧头与响应分离、PID 校验、调度表这些概念落成一套能在 STM32 上直接编译烧录的工程。它适合两类人一类是刚接触车载总线的嵌入式新手想拿一个能跑通的最小系统理解 LIN 的帧结构另一类是已经在做车身控制模块的工程师需要一个可裁剪的协议栈底座把精力放在应用层逻辑而不是底层时序上。整套程序围绕 USART 的 LIN 模式或普通 USART 定时器模拟断场展开核心是中断收发加状态机不依赖复杂 RTOS裸机就能跑。2. LIN 帧结构与 STM32 外设映射先搞懂再动手2.1 LIN 帧的四个字段与 STM32 的对应关系LIN 的一帧由帧头Header和响应Response组成帧头又拆成断场Break、同步场Sync、标识符场PID。断场是至少 13 个显性位的连续低电平作用是告诉总线上所有从机“新的一帧要开始了”。同步场固定是 0x55从机靠它测量主机波特率做时钟校准。PID 是 6 位帧 ID 加 2 位奇偶校验决定这一帧是主机发还是从机发、数据长度是 2/4/8 字节。在 STM32 上断场有两种生成方式。第一种是用 USART 的 LIN 模式置位 LINEN 后写一次数据寄存器硬件自动发 13 位低电平这是最省事的路子。第二种是普通 USART 模式把 TX 脚临时切成 GPIO 拉低用定时器延时再切回 USART常见于那些 LIN 模式被占用或者波特率特殊的芯片。我一般优先用硬件 LIN 模式因为断场长度精确不受中断延迟影响。同步场和 PID 就是普通字节发送没什么特殊。真正需要留意的是响应阶段的收发方向切换主机发帧头后如果是主机作为发布者紧接着发数据如果是从机发布主机要立刻把 USART 从发送切到接收等待从机在响应空间内回数据。这个切换窗口很窄晚了就丢帧。2.2 波特率与同步机制的参数计算LIN 标准波特率最高 20kbps常见 19200 和 9600。STM32 的 USART 波特率寄存器按fCK / (16 * USARTDIV)算以 72MHz 主频、19200 波特率为例USARTDIV 约等于 234.375整数部分 234小数部分 0.375 对应 6因为 16 分频下小数寄存器是 4 位0.375*166。写进去就是 BRR (234 4) | 6。从机的时钟校准靠同步场。从机在检测到断场后用输入捕获或者定时器记录同步场 0x55 的下降沿间隔算出实际位时间再反过来修正自己的波特率寄存器。这一步不做晶振偏差大一点就会在几个字节后累积出错。常见做法是用一个定时器通道捕获第一个下降沿和最后一个下降沿中间跨了 8 个位时间0x55 是 01010101有 4 个下降沿首尾间隔 8 位用总时间除以 8 得到位时间。提示如果从机晶振是内部 RC温漂可能到 2% 以上同步场校准基本是必须的别省这一步。2.3 调度表与帧 ID 分配LIN 主机要维护一张调度表决定每一帧什么时候发。调度表本质是一个数组每个元素包含帧 ID、该帧是发布还是订阅、以及相对于上一帧的延时。主机按表轮询从机被动响应。帧 ID 的分配有惯例0x00 到 0x3B 是信号帧0x3C 和 0x3D 是诊断帧0x3E 是用户自定义0x3F 保留。实际项目里我会把 0x10 到 0x1F 留给周期性传感器数据0x20 到 0x2F 留给执行器命令诊断帧固定用 0x3C。调度表的周期计算要注意每帧的传输时间等于帧头加响应加响应间隔。19200 波特率下一个字节 10 位含起始停止8 字节数据帧大约 1118 字节共 11 字节约 5.7ms加上帧间间隔一帧按 6 到 7ms 估。如果调度表有 10 帧一轮就是 60 到 70ms这个周期决定了信号的刷新率做闭环控制时要算进去。3. 主机节点实现断场生成、PID 校验与调度循环3.1 初始化与断场发送主机初始化的关键是 USART 配置和 LIN 模式使能。下面这段是 19200 波特率、8 数据位、无校验的配置注意 LIN 模式要求字长设成 8 位校验由软件在 PID 里做不用硬件校验。// 主机 USART 初始化以 72MHz 主频、19200 波特率为例 void LIN_Master_Init(USART_TypeDef* USARTx) { // 1. 使能时钟略按具体芯片补 RCC 配置 // 2. 配置波特率72e6 / (16 * 19200) 234.375 USARTx-BRR (234 4) | 6; // 整数234小数6 // 3. 8 数据位无校验1 停止位 USARTx-CR1 ~USART_CR1_M; // M0 表示 8 位 USARTx-CR2 ~USART_CR2_STOP; // 1 个停止位 // 4. 使能 LIN 模式断场长度 13 位LBDL0 USARTx-CR2 | USART_CR2_LINEN; USARTx-CR2 ~USART_CR2_LBDL; // 5. 使能发送和 USART USARTx-CR1 | USART_CR1_TE | USART_CR1_UE; } // 发送断场写一次 DR 即可硬件自动发 13 位低电平 void LIN_SendBreak(USART_TypeDef* USARTx) { USARTx-CR1 | USART_CR1_SBK; // 置位 SBK发送断场 while (!(USARTx-SR USART_SR_TXE)); // 等待发送完成 }逻辑说明BRR 的计算必须按实际主频重算上面是 72MHz 的例子换成 64MHz 或 48MHz 要重新算否则波特率偏差会导致从机同步失败。SBK 位置位后硬件会在当前字节发完后插入断场不需要手动拉 GPIO。参数上LBDL 决定断场是 10 位还是 11 位LIN 2.x 要求至少 13 位所以 LBDL 保持 0对应 13 位。3.2 PID 计算与校验PID 的 6 位 ID 加 2 位奇偶校验校验位 P0 和 P1 的计算公式是固定的。P0 是 ID0 到 ID3 的奇校验P1 是 ID1、ID3、ID4、ID5 的奇校验。写错这两位从机会直接忽略这一帧而且不报错排查起来很费时间。// 根据 6 位帧 ID 计算带校验的 PID uint8_t LIN_CalcPID(uint8_t id) { uint8_t pid id 0x3F; // 只取低 6 位 uint8_t p0 ((pid 0) ^ (pid 1) ^ (pid 2) ^ (pid 4)) 0x01; uint8_t p1 ~((pid 1) ^ (pid 3) ^ (pid 4) ^ (pid 5)) 0x01; return pid | (p0 6) | (p1 7); }逻辑说明P0 是 ID 第 0、1、2、4 位的异或P1 是第 1、3、4、5 位异或后取反。这两个公式来自 LIN 规范不能改。参数上传入的 id 必须是 0 到 0x3F超过范围要先截断。返回值的 bit6 是 P0bit7 是 P1直接写进 USART 的 DR 发送即可。3.3 调度循环与收发方向切换主机调度循环的核心是发帧头判断该帧方向发布则发数据订阅则切接收。切换时机要卡在帧头发完、响应空间开始之前。// 调度表项结构 typedef struct { uint8_t pid; // 带校验的 PID uint8_t dir; // 0主机发布1主机订阅 uint8_t len; // 数据长度 2/4/8 uint8_t data[8]; // 发布时的数据缓冲 uint16_t delay_ms; // 相对上一帧的延时 } LIN_ScheduleItem; void LIN_Master_Run(LIN_ScheduleItem* table, uint8_t count) { for (uint8_t i 0; i count; i) { LIN_SendBreak(USARTx); USARTx-DR 0x55; // 同步场 while (!(USARTx-SR USART_SR_TC)); USARTx-DR table[i].pid; // 标识符场 while (!(USARTx-SR USART_SR_TC)); if (table[i].dir 0) { // 主机发布紧接着发数据 for (uint8_t j 0; j table[i].len; j) { USARTx-DR table[i].data[j]; while (!(USARTx-SR USART_SR_TC)); } } else { // 主机订阅切接收等从机响应 USARTx-CR1 ~USART_CR1_TE; USARTx-CR1 | USART_CR1_RE; // 接收逻辑放在中断里这里只等超时 LIN_WaitResponse(table[i].len, 10); // 10ms 超时 USARTx-CR1 | USART_CR1_TE; USARTx-CR1 ~USART_CR1_RE; } Delay_ms(table[i].delay_ms); } }逻辑说明发送同步场和 PID 后要等 TC发送完成标志不能只等 TXE否则最后一个位还没发完就切方向会截断。订阅帧切接收后接收中断负责把数据填进缓冲主循环用超时等待。参数上超时时间按帧长算8 字节帧在 19200 下约 5ms给 10ms 余量足够。delay_ms 是帧间间隔最小要大于 0实际项目里给 2 到 5ms。4. 从机节点实现同步场校准与响应中断4.1 同步场测量与波特率修正从机的难点在同步。断场检测到后要立刻启动定时器捕获同步场的边沿。0x55 的波形是 0-1-0-1-0-1-0-1有 4 个下降沿第一个下降沿到最后一个下降沿跨 8 个位时间。// 用 TIM 输入捕获测同步场假设捕获通道已配置 volatile uint16_t cap_first 0, cap_last 0; volatile uint8_t cap_count 0; void TIM_IC_IRQHandler(void) { if (TIM_GetITStatus(TIMx, TIM_IT_CC1)) { uint16_t now TIM_GetCapture1(TIMx); if (cap_count 0) { cap_first now; } else if (cap_count 3) { cap_last now; // 8 个位时间 cap_last - cap_first注意定时器溢出 uint16_t bit_time (cap_last - cap_first) / 8; LIN_Slave_AdjustBaud(bit_time); } cap_count; TIM_ClearITPendingBit(TIMx, TIM_IT_CC1); } }逻辑说明cap_count 计到 3 表示收到第 4 个下降沿此时首尾差就是 8 位时间。定时器要设成足够高的分辨率比如 1MHz 计数19200 波特率下位时间是 52us8 位是 416us16 位定时器不会溢出。参数上bit_time 算出来后反推 BRR公式和主机初始化一样只是用实测位时间代替理论值。4.2 响应中断与数据收发从机收到 PID 后要判断这个 ID 是不是自己负责的。如果是自己发布的帧就在响应空间内发数据如果是订阅的帧就准备接收。// 从机接收中断处理 PID 和后续数据 void USART_IRQHandler(void) { if (USART_GetITStatus(USARTx, USART_IT_RXNE)) { uint8_t byte USART_ReceiveData(USARTx); switch (slave_state) { case WAIT_PID: slave_pid byte; if (LIN_IsMyFrame(byte)) { if (LIN_IsPublish(byte)) { // 本从机发布切发送 USARTx-CR1 | USART_CR1_TE; USARTx-CR1 ~USART_CR1_RE; LIN_SendResponse(byte); } else { // 本从机订阅准备收数据 slave_state RECV_DATA; recv_idx 0; } } else { slave_state IDLE; // 不是自己的帧忽略 } break; case RECV_DATA: slave_buf[recv_idx] byte; if (recv_idx LIN_GetLen(slave_pid)) { slave_state IDLE; LIN_OnDataReceived(slave_pid, slave_buf); } break; } USART_ClearITPendingBit(USARTx, USART_IT_RXNE); } }逻辑说明状态机只有两个有效状态WAIT_PID 和 RECV_DATA简单但够用。切发送的时机是在确认 PID 属于自己且是发布帧之后立刻置 TE 清 RE然后发数据。参数上LIN_GetLen 根据 PID 的低 6 位查表返回 2/4/8这个表要和应用层约定一致。注意接收中断里不要做耗时操作数据处理丢到主循环。4.3 校验和计算LIN 有两种校验和经典校验和Classic和增强校验和Enhanced。经典只对数据字节求和取反增强还要把 PID 加进去。LIN 2.x 规定诊断帧和 0x3C、0x3D 用经典其他用增强。算错校验和主机收到数据也会丢弃。// 计算校验和enhanced1 用增强校验 uint8_t LIN_CalcChecksum(uint8_t pid, uint8_t* data, uint8_t len, uint8_t enhanced) { uint16_t sum 0; if (enhanced) sum pid; for (uint8_t i 0; i len; i) { sum data[i]; if (sum 0xFF) sum - 0xFF; // 进位回卷 } return (uint8_t)(~sum); }逻辑说明进位回卷是 LIN 校验和的特点不是简单截断sum 超过 0xFF 要减 0xFF 而不是减 0x100。参数上enhanced 标志由帧 ID 决定0x3C 和 0x3D 传 0其余传 1。返回的是取反后的值直接作为最后一个字节发送。5. 避坑与排查那些让 LIN 帧莫名丢失的细节5.1 断场长度不够导致从机不响应现象主机发了帧头从机毫无反应示波器看断场只有 10 个位宽。原因USART 的 LBDL 位设成了 1或者用了普通 USART 模式但 GPIO 拉低时间不够。解决确认 CR2 的 LINEN 置位且 LBDL 清零用硬件 LIN 模式如果必须用 GPIO 模拟拉低时间按 13 位算19200 下约 677us留 10% 余量。5.2 同步场校准后波特率反而偏了现象从机加了同步校准通信反而比不加还不稳定。原因捕获的边沿数错了把断场末尾的上升沿或者 PID 的边沿也算进去导致位时间算错。解决捕获只在断场结束后使能收到第 4 个下降沿就关闭捕获cap_count 严格从 0 计到 3。用示波器确认同步场确实是 0x55 的波形。5.3 收发切换时机不对丢响应现象主机订阅帧收不到从机数据或者收到半截。原因主机在 PID 发完后没有及时切接收或者切接收前从机已经开始发。解决PID 写入 DR 后等 TC 标志再切切换动作放在中断里比主循环轮询更稳。从机侧则要在确认 PID 后立刻切发送不能等主循环调度。5.4 校验和类型用错现象数据内容明明对主机就是丢弃。原因诊断帧用了增强校验或者普通帧用了经典校验。解决按帧 ID 判断0x3C 和 0x3D 用经典其余用增强。抓包时把校验和字节单独看一眼和手算结果比对。5.5 调度表周期过长导致信号滞后现象执行器响应慢半拍或者闭环控制振荡。原因调度表帧数太多一轮周期超过应用层容忍度。解决把高频信号放前面低频的往后排能合并的帧合并比如同方向的多个 2 字节信号拼成一个 8 字节帧。算一下最坏情况下的轮询时间留 30% 余量。6. 进阶技巧用状态机把收发揉进一个定时器中断裸机跑 LIN最容易翻车的地方是主循环里 Delay 阻塞导致帧间间隔不准、响应超时。我后来改成一个定时器中断驱动状态机把断场、同步场、PID、数据、校验和拆成状态每个状态在定时器 tick 里推进一步主循环只负责填数据和读结果。这样帧间时序由硬件定时器保证不受主循环里其他任务影响。具体做法定时器设成位时间的若干分之一比如位时间的 1/419200 下约 13us 一个 tick。状态机在 tick 里数位发断场数 13 个位时间发同步场按 0x55 的位模式输出PID 和数据同理。接收侧在 tick 里采样 RX 脚按位拼字节。这套写法代码量比中断收发大但时序确定性好适合对抖动敏感的场景。验证方法拿示波器抓 TX 和 RX看断场宽度、同步场位宽、帧间间隔是否和计算值一致。再用一个 LIN 分析仪或者第二块板子做从机跑一轮调度表统计丢帧率。连续跑 10 万帧不丢基本就稳了。从那以后我每次做 LIN 节点都强制先用示波器把断场和同步场量一遍再写应用逻辑省得后面在协议层和硬件层之间来回猜。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑