简介:这份资源面向嵌入式开发初学者与汽车电子方向工程师,提供在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*16=6)。写进去就是 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 字节数据帧大约 1+1+1+8 字节共 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; // M=0 表示 8 位 USARTx->CR2 &= ~USART_CR2_STOP; // 1 个停止位 // 4. 使能 LIN 模式,断场长度 13 位(LBDL=0) 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 是 P0,bit7 是 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 波特率下位时间是 52us,8 位是 416us,16 位定时器不会溢出。参数上,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 用经典,其他用增强。算错校验和,主机收到数据也会丢弃。
// 计算校验和,enhanced=1 用增强校验 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/4,19200 下约 13us 一个 tick。状态机在 tick 里数位,发断场数 13 个位时间,发同步场按 0x55 的位模式输出,PID 和数据同理。接收侧在 tick 里采样 RX 脚,按位拼字节。这套写法代码量比中断收发大,但时序确定性好,适合对抖动敏感的场景。
验证方法:拿示波器抓 TX 和 RX,看断场宽度、同步场位宽、帧间间隔是否和计算值一致。再用一个 LIN 分析仪或者第二块板子做从机,跑一轮调度表,统计丢帧率。连续跑 10 万帧不丢,基本就稳了。
从那以后我每次做 LIN 节点,都强制先用示波器把断场和同步场量一遍再写应用逻辑,省得后面在协议层和硬件层之间来回猜。希望帮到你。
本文还有配套的精品资源,点击获取