PIC16F73 做 LIN 从机:一张表算清 20 kbps 波特率与 4 个超时值

PIC16F73 用 USART 做 LIN 从机时,20 kbps 在 4 MHz 下根本配不出合规的 SPBRG。按 AN237 的 16(X+1) 关系、X>25 下限、25000/39/128 三个超时常量与 1.4 倍帧时间,拆开主从时序与七步初始化。

2026-11-09
PIC16F73LIN 从机AN237SPBRG帧时间超时判定

一台拖拉机座椅控制器的 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 MHz9600259615 bps+0.16%可用
4 MHz200001120833 bps+4.17%超差
4 MHz200001219231 bps−3.85%超差
8 MHz9600519615 bps+0.16%可用
8 MHz200002420000 bps0可用

SPBRG 还有个下限。AN237 用 2% 误差推交叠边界,结论是 X 应大于 25;若按 ±5% 包络估,X 低于 10 时某些时钟频率会出现“没有任何 SPBRG 可用”的空档。所以 8 MHz / 20 kbps 的 X=24 只是理论零误差,晶振精度必须自己兜住;用陶瓷谐振器或内部 RC 时,这颗器件不适合跑 20 kbps,退到 9600 更稳。

二、一个 2 字节帧,为什么按 82 位做预算

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 后起算)394.06 ms1.95 ms
2 字节数据 + 校验和(×1.4)434.48 ms2.15 ms
整帧合计828.54 ms4.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_TIME250002.60 s总线长时间无活动,从机应进入睡眠
MAX_HEADER_TIME394.06 ms帧头没收全,置帧超时错误
帧时间累加每 2 字节 434.48 ms数据或校验和没收全
MAX_TIME_OUT12813.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_PARID 奇偶校验错误核对主机 ID 表与从机表驱动是否一致
LE_SYNC同步容差超限回查 SPBRG 与晶振精度

六、表驱动:每个 ID 只填三个变量

收到 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小时内响应