PIC16F73 用 USART 做 LIN 从机时,20 kbps 在 4 MHz 下根本配不出合规的 SPBRG。按 AN237 的 16(X+1) 关系、X>25 下限、25000/39/128 三个超时常量与 1.4 倍帧时间,拆开主从时序与七步初始化。
一台拖拉机座椅控制器的 LIN 子节点用 PIC16F73,主机每 10 ms 发一次帧头,从机按 ID 回 2 字节开关状态。台架上一切正常,装车后偶发“座椅不动”:LIN 分析仪抓到的校验和错误不到千分之一,整帧丢失却明显更多。结论先给:这类问题很少是校验算法写错,多数是波特率误差吃掉了采样窗口,再叠加超时账没算清,从机把“还没轮到我”误判成“总线睡眠”。AN237 把边界写成了 4 个位时间常量:25000 位空闲、39 位帧头、128 位唤醒等待,以及每 2 字节按 43 位累加的帧计时;20 kbps 用 8 MHz 晶振 SPBRG=24,4 MHz 配不出合规值。
本文按 Microchip 应用笔记 AN237《Implementing a LIN Slave Node on a PIC16F73》DS00237A 整理。波特率关系式、SPBRG 下限、39/128/25000 三个常量、2 字节 43 位帧时间与 333 字程序空间均取自该文档,毫秒数由位时间换算得到。
LIN 1.2 给主从通信的容差是 ±2%,而 SPBRG 决定从机自己的采样节奏。AN237 指出,USART 对输入做三采样多数判决,等效于在位中心采一次;频率误差与比特率误差近似 1:1,所以 ±2% 的时钟误差几乎就是 ±2% 的位宽误差。9 个位时间内累计偏移超过半个位宽,一帧里最后一个字节就会被判错——现象正是“校验偶发失败、整帧偶尔消失”。
PIC16F73 的异步高速档满足 B = FOSC / (16 × (X+1)),X 是 SPBRG 的 8 位值。把常用组合代进去,问题立刻现形:4 MHz 想把 20 kbps 配准,X 只能取 11 或 12,一个偏 +4.17%,一个偏 −3.85%,都超出 ±2%;8 MHz 时 X=24 恰好得到 20000 bps。
| FOSC | 目标波特率 | SPBRG(X) | 实际波特率 | 误差 | 结论 |
|---|---|---|---|---|---|
| 4 MHz | 9600 | 25 | 9615 bps | +0.16% | 可用 |
| 4 MHz | 20000 | 11 | 20833 bps | +4.17% | 超差 |
| 4 MHz | 20000 | 12 | 19231 bps | −3.85% | 超差 |
| 8 MHz | 9600 | 51 | 9615 bps | +0.16% | 可用 |
| 8 MHz | 20000 | 24 | 20000 bps | 0 | 可用 |
SPBRG 还有个下限。AN237 用 2% 误差推交叠边界,结论是 X 应大于 25;若按 ±5% 包络估,X 低于 10 时某些时钟频率会出现“没有任何 SPBRG 可用”的空档。所以 8 MHz / 20 kbps 的 X=24 只是理论零误差,晶振精度必须自己兜住;用陶瓷谐振器或内部 RC 时,这颗器件不适合跑 20 kbps,退到 9600 更稳。
AN237 的帧计时不是“数够字节就完事”,而是按位时间累加。2 个数据字节加 1 个校验和字节,每个字节含起始位与停止位共 10 位,合计 30 位;再乘 1.4 的容差系数,得到 43 位。这就是 ID 事件里要往 FRAME_TIME 寄存器加的数值。
帧头的时间定义更容易踩坑。LIN 规范写 49 位,AN237 的 MAX_HEADER_TIME 却填 39,注释说得很清楚:计时从 Break 之后才开始,Break 本身占 10 位不计入。39 加 43,一帧 2 字节报文的完整预算就是 82 位。
| 帧组成 | 位数 | @9600 bps | @20000 bps |
|---|---|---|---|
| 帧头(Break 后起算) | 39 | 4.06 ms | 1.95 ms |
| 2 字节数据 + 校验和(×1.4) | 43 | 4.48 ms | 2.15 ms |
| 整帧合计 | 82 | 8.54 ms | 4.10 ms |
这张表能直接回答调度问题:主机若按 10 ms 周期发帧,9600 bps 下一帧 8.54 ms,余量只剩 1.46 ms,从机中断里再塞长任务就会顶到下一帧;20000 bps 下同样帧只占 4.1 ms,余量翻倍。反过来说,主机周期小于 4.5 ms 时,2 字节帧在 9600 bps 下物理上就排不开。
AN237 的计时函数与驱动解耦,只要求“每位时间至少调用一次”,示例用 Timer0 产生位率中断。帧时间与总线时间都按位计数,好处是换波特率只改 SPBRG,这几个常量不用重算,坏处是新手容易把“128”当成毫秒。
| 常量 | 值(位时间) | @9600 换算 | 防的故障 |
|---|---|---|---|
| MAX_IDLE_TIME | 25000 | 2.60 s | 总线长时间无活动,从机应进入睡眠 |
| MAX_HEADER_TIME | 39 | 4.06 ms | 帧头没收全,置帧超时错误 |
| 帧时间累加 | 每 2 字节 43 | 4.48 ms | 数据或校验和没收全 |
| MAX_TIME_OUT | 128 | 13.33 ms | 发出唤醒请求后等不到总线活动 |
四个值对应四个状态边界。把 MAX_IDLE_TIME 当成 25 ms 写进应用层,从机会在每次正常帧间隙里反复“入睡—唤醒”,表现出来就是响应时快时慢;把 128 当成 128 µs,唤醒重试会密到把总线占满。按位时间理解,这类错配基本可以一次排掉。
LIN 是单线总线,收发器把总线电平回送到 MCU 的 RX 脚,所以节点自己发送时,USART 接收中断照样会进。AN237 正是利用这一点:驱动只挂 USART 接收中断,发送和接收走同一个入口,再用状态位判断当前在等 Break、Sync、ID 还是数据。这也是它能做到“后台常驻”的原因——不需要额外的发送完成中断。
中断预算并不紧。AN237 给出 4 MHz 下总线空闲时约 98% 的处理时间仍归应用;核心驱动只占 333 字程序存储器与 12 字节数据存储器,对应 PIC16F73 的 5% 程序空间和 10% 数据空间。换句话说,剩下的大把头时间可以留给电机控制、采样滤波这类主任务。
状态标志由驱动自己推进:收到 Break 置 LS_BRK,收到同步字节置 LS_SYNC,收到 ID 置 LS_ID,数据收齐置 LS_DATA,校验通过置 LS_CHKSM。应用层通常只看 LS_BUSY 和两个完成标志 LF_RX、LF_TX,处理完必须手动清零,否则下一次完成事件读不到。
错误标志则全部集中在 LIN_STATUS_FLAGS 里,AN237 明确写了“错误在被处理并清零前视为致命”,也就是说不清标志,驱动会忽略后续所有数据。这一条在现场最容易漏:校验错误发生一次后从机再也不应答,看着像死机,其实只是标志没清。
| 标志 | 含义 | 现场处理 |
|---|---|---|
| LE_BIT | 回读位错误(发送数据与总线不一致) | 检查收发器与总线短路,重发由主机调度决定 |
| LE_BTO | 总线活动超时 | 进入睡眠流程,等待唤醒 |
| LE_CHKSM | 校验和错误 | 丢弃本帧,记录计数,勿立即回写 |
| LE_FTO | 帧超时(头或数据未收全) | 复位状态位,等下一个 Break |
| LE_PAR | ID 奇偶校验错误 | 核对主机 ID 表与从机表驱动是否一致 |
| LE_SYNC | 同步容差超限 | 回查 SPBRG 与晶振精度 |
收到 ID 后,驱动调用 l_txrx_table 拿“方向 + 长度”,再把三个变量填好:数据指针 LIN_POINTER、任务计数 LIN_COUNT、帧时间 FRAME_TIME。AN237 特意提醒,count 必须初始化为大于零,否则驱动不会进入收发流程;2 字节帧要往 FRAME_TIME 里加 43,因为此时寄存器里通常还剩帧头用剩的时间。
// 事件表:ID -> 方向 / 字节数 / 缓冲
typedef struct {
unsigned char dir; // 0=接收 1=发送
unsigned char len; // 数据字节数,必须 > 0
unsigned char *buf;
} lin_id_entry;
static unsigned char seat_status[2];
static const lin_id_entry id_table[64] = {
[0x21] = { 1, 2, seat_status }, // 从机上报座椅开关
[0x22] = { 0, 2, seat_status }, // 主机下发指令
};
void lin_id_event(unsigned char id)
{
const lin_id_entry *e = &id_table[id & 0x3F];
if (e->len == 0) return; // 本节点不处理该 ID
LIN_POINTER = e->buf;
LIN_COUNT = e->len;
FRAME_TIME += (unsigned char)((e->len * 10 + 10) * 1.4 + 0.5); // 2 字节 -> 43
if (e->dir) LIN_STATE_FLAGS |= 0x01; // 置发送方向
}
void lin_poll(void)
{
if (LIN_STATUS_FLAGS) { // 任一错误都必须清,否则驱动停摆
if (LIN_STATUS_FLAGS & LE_BTO) enter_sleep();
LIN_STATUS_FLAGS = 0;
}
if (LIN_FINISH_FLAGS & LF_RX) {
LIN_FINISH_FLAGS &= ~LF_RX;
apply_command(seat_status);
}
}
按 AN237 的项目结构,lin.asm 负责协议、timer.asm 负责位计时、linevent.asm 负责 ID 分派,main.asm 只做初始化和应用。落地顺序建议固定成七步:①按目标波特率选晶振,20 kbps 用 8 MHz、9600 用 4 MHz;②按 16(X+1) 算 SPBRG,并核对误差在 ±2% 内;③Timer0 配成位率中断,中断里调用计时更新;④USART 接收中断里调用收发驱动;⑤填 ID 表,三个变量一个都不能少;⑥主循环集中处理错误标志;⑦需要从机唤醒主机时再调用唤醒函数。
| 现象 | 根因 | 改法 |
|---|---|---|
| 校验偶发错、波形看着正常 | 波特率误差超过 ±2% | 换 8 MHz 或降到 9600,重算 SPBRG |
| 某些 ID 永远不应答 | ID 表 len=0 或方向填反 | 核对表驱动与主机调度表 |
| 发生一次错误后彻底静默 | LIN_STATUS_FLAGS 未清零 | 主循环统一清错误标志 |
| 帧收到一半卡住 | FRAME_TIME 没按 43 累加 | 按 (字节数×10+10)×1.4 补齐 |
| 总线空闲后不响应唤醒 | 25000 位空闲判定未实现 | 用位时间计数,勿按毫秒写死 |
一句话总结:LIN 从机的稳定性先由波特率误差决定,再由超时账决定。先用 16(X+1) 把 SPBRG 和 ±2% 误差算平,再把 39 位帧头、43 位帧时间、128 位唤醒等待与 25000 位空闲按位时间落进定时器,最后记住错误标志不清驱动就停摆——这三步做完,剩下才是应用逻辑。
还想接着看:LIN 总线 TJA1020 网关子节点设计、PIC18F2480 EUSART 自动波特率与错误恢复、KT8341 电力线扩频载波通信设计。
本文基于 Microchip 应用笔记 AN237《Implementing a LIN Slave Node on a PIC16F73》DS00237A 技术资料整理优化。
沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付
电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应