PIC16C74A 中断跑飞复盘:4 个体选、PCLATH 分页与现场保护七步配置

同一份代码从 2K 字型号搬到 8K 字的 PIC16C74A,按键误触发、串口错位、中断返回跑飞接连出现。结论先给:中档内核对程序存储器分 4 页、数据存储器分 4 体,中断只压了 PC,W 与 STATUS 全靠软件保存;换大容量型号后,"原来用不上"的页选位和体选位突然开始生效。

2026-10-30
PIC16C74A体选切换PCLATH中断现场保护中档单片机汇编

一块小家电控制板,从 2K 字程序空间的型号换成 8K 字的 PIC16C74A,代码几乎原样搬过去,功能却开始出问题:按键偶尔误触发、串口收到的数据错位,最严重的一次是中断返回后直接跑飞,看门狗复位才恢复。结论先给:这类"换大容量型号就出问题"的现象,根因基本不在外设,而在中档内核的两套地址空间——程序存储器的 4 页要靠 PCLATH<4:3> 跳,数据存储器的 4 个体要靠 RP1:RP0 和 IRP 切,而中断只压了 PC,W 与 STATUS 必须自己保存。

一、程序存储器 4 页:PCLATH 什么时候参与、什么时候不参与

中档系列单片机有一个 13 位程序计数器,可以寻址 8K × 14 位的程序空间,分成 4 页、每页 2K 字(000h-7FFh、800h-FFFh、1000h-17FFh、1800h-1FFFh)。而 CALL 和 GOTO 指令只有 11 位地址,单条指令只能在 2K 范围内跳,跨页必须靠 PCLATH 的高位补上。理解这张表,比记住"跳转前设 PCLATH"这句口诀有用得多。

PC 装载方式PC 高位来源是否需要提前设 PCLATH
以 PCL 为目标寄存器的指令(如 ADDWF PCL)PCLATH<4:0> → PCH需要,且要保证表不跨 256 字节边界
GOTO 指令PCLATH<4:3> → PCH需要,先写页选择位
CALL 指令PCLATH<4:3> → PCH,同时 PC 压栈需要,返回时由堆栈恢复整个 13 位 PC
RETURN / RETLW / RETFIE从 8 级堆栈弹出完整 13 位 PC不需要

两个容易踩的点。第一,任何复位都会把 PCLATH 清零,所以复位向量 0h 处的跳转一定落在第 0 页;而响应中断时 PC 指向 0004h,PCLATH 不会被修改,也就是说中断进来时 PCLATH 还是主程序当时的值。ISR 里如果要跳转到别的页,必须自己先设好页选择位,并在离开前恢复原值——很多"中断里调用一次函数就回不来"的案例,就是这里漏了。第二,比较省事的写法是把整个 ISR 和它调用的函数都放在同一页,靠链接器布局保证,但这属于运气,不是设计。

手册给出的调用示例很简洁:在 0x500 处调用第 1 页的子程序,先 BSF PCLATH,3 再 CALL SUB1_P1,返回时不需要再动 PCLATH,因为返回指令弹出的是完整 13 位地址。对于程序空间只有 2K 字的器件,页选择位没有意义;2K 到 4K 字的器件 PCLATH<4> 也无意义,但不要把它当普通读写位用,将来换型号会出问题。

二、数据存储器 4 个体:直接寻址看 RP1:RP0,间接寻址看 IRP

数据存储器由 SFR 和 GPR 组成,分成 4 个体,每个体最多 128 字节(7Fh),SFR 排在低地址、GPR 排在高地址。直接寻址时,指令里给出 7 位地址,高位由 STATUS 的 RP1、RP0 补上;间接寻址时,9 位有效地址由 STATUS 的 IRP 与 8 位 FSR 拼成。两套选择位互不相干,这是最容易混的地方。

目标体直接寻址(RP1 : RP0)间接寻址(IRP)
Bank 00 : 00(配合 FSR 的 00h~7Fh)
Bank 10 : 10(配合 FSR 的 80h~FFh)
Bank 21 : 01(配合 FSR 的 00h~7Fh)
Bank 31 : 11(配合 FSR 的 80h~FFh)

为了少切换体,较新的器件把每个体最后 16 字节映射到 Bank 0,也就是常说的公用 RAM,同一个地址在四个体里指向同一块物理存储。这条特性直接决定了中断现场保护怎么写:W_TEMP 就放在这个公用区(例如 0x70~0x7F),四个体同样的偏移都能访问,保存 W 时不必先判断当前在哪个体。老器件没有公用区,就得多一步切体动作。

还有一个初始化陷阱:上电复位并不初始化 GPR,其他复位也不改变 GPR 的值。变量"上电是随机值"不是 bug,是规格。启动代码里必须显式把用到的 GPR 清零,否则第一次判断标志位就可能走错分支。

三、FSR 与 INDF:间接寻址的两个边界条件

间接寻址靠 INDF 和 FSR 配合:任何使用 INDF 的指令,实际操作的是 FSR 指向的单元。手册明确了两条边界:当 FSR = 0 时读 INDF 得到 00h;用间接寻址对 INDF 写,实际执行的是空操作,但可能影响状态位。这两条决定了"用 INDF 清零一片 RAM"的循环必须从有效地址开始,且循环变量不能是 FSR 本身指向的单元。

标准写法是把起始地址装进 FSR,然后循环 CLRF INDF 加 INCF FSR,F,用 FSR 的某一位判断是否越界。手册的例子清的是 20h~2Fh,靠 BTFSS FSR,4 判断是否到 30h 结束。这种写法比逐个直接寻址短得多,也不受当前体影响,前提是 IRP 已经按目标体设好。

四、8 级堆栈:没有溢出标志,越界是静默的

中档内核的硬件堆栈是 8 级深、13 位宽,既不占程序空间也不占数据空间,栈指针不可读写。压栈 8 次之后,第 9 次压栈会覆盖第 1 次的数据,第 10 次覆盖第 2 次,依次类推。关键在于:没有表示堆栈溢出或下溢的状态位。也就是说,嵌套过深导致的返回地址丢失,不会在发生的那一刻报错,而是在某个 RETURN 之后跳到完全无关的地址,表现为"跑飞"。

这条规格对设计的影响很直接:中断里不要再套多层函数调用,函数嵌套层数要人工核算,禁止递归。8 级听起来够用,但中断本身占一级,ISR 里再调三层函数,主程序那边就只剩 4 级余量。

五、中断现场保护:只压了 PC,其余自己来

响应中断时,硬件只把返回地址压进堆栈,W 寄存器和 STATUS 都不保存。W 里可能是主程序正在算的中间结果,STATUS 里带着体选位 RP1:RP0 和运算标志位,任何一个丢了,返回后都会错。手册给了三套写法,区别只在器件的 RAM 布局。

器件类型写法要点代价
有公用 RAM 的器件W_TEMP 定义在 0x70~0x7F,四个体同偏移;直接 MOVWF 保存最短,推荐
无公用 RAM 的器件先保存 W,再 BCF STATUS,RP0 切到 Bank0 存 STATUS_TEMP多两条指令,恢复时靠 STATUS 还原体选
仅 Bank0 有 GPR 的器件先用 BTFSS STATUS,RP0 判断当前体,按分支分别保存代码最长,且要在 STATUS_TEMP 里补回 RP0 位

无论哪一套,都有两个细节必须照做。第一,保存 STATUS 要用 SWAPF STATUS,W 而不是 MOVF,因为 SWAPF 不影响状态标志,MOVF 会改 Z 位,等于把要保护的东西先破坏了;恢复时同理,用两次 SWAPF 把 W 换回来。第二,W_TEMP 必须在每个体都定义、且偏移相同,否则 ISR 从不同体进入时会写进不同的物理单元。

; 有公用 RAM 的器件:PUSH / POP 宏
PUSH_MACRO  MACRO
            MOVWF   W_TEMP          ; 保存 W,不必关心当前在哪个体
            SWAPF   STATUS,W        ; SWAPF 不改变 Z 标志
            MOVWF   STATUS_TEMP     ; STATUS_TEMP 固定在 Bank0
            ENDM

POP_MACRO   MACRO
            SWAPF   STATUS_TEMP,W   ; 先恢复 STATUS,体选位随之还原
            MOVWF   STATUS
            SWAPF   W_TEMP,F
            SWAPF   W_TEMP,W        ; 最后恢复 W,全程不动状态位
            ENDM

; 中断服务程序骨架
            ORG     0x0004
            PUSH_MACRO
            BCF     STATUS,RP0      ; 进入 Bank0 处理标志位
            BTFSC   PIR1,TMR1IF
            GOTO    T1_INT
            BTFSC   PIR1,ADIF
            GOTO    AD_INT
            BTFSC   INTCON,RBIF
            GOTO    PORTB_INT
            GOTO    END_ISR         ; 无匹配中断,直接退出,避免死等
T1_INT
            BCF     PIR1,TMR1IF
            GOTO    END_ISR
AD_INT
            BCF     PIR1,ADIF
            GOTO    END_ISR
PORTB_INT
            BCF     INTCON,RBIF
END_ISR
            POP_MACRO
            RETFIE

顺带说清中断响应延时:同步中断(一般是内部中断)为 3 个指令周期,异步中断(INT 引脚、RB 端口电平变化这类)为 3~3.75 个指令周期,具体取决于中断事件落在指令周期的哪个位置,INTF 标志在每个 Q1 节拍采样。单周期指令和双周期指令的响应延时是一样的,这一点和不少人的直觉相反。做时序敏感的外设时,这 3 个多周期要算进预算里。

六、七步落地

  1. 先确认器件程序空间有多大,2K 字以内的项目可以完全不关心页选择位,但代码里别把 PCLATH 当普通变量用。
  2. 把跨页调用的函数集中到同一页,或统一封装成"设 PCLATH → CALL → 恢复 PCLATH"的宏,避免手写遗漏。
  3. ISR 入口先 PUSH_MACRO,出口最后 POP_MACRO,中间不要提前恢复 STATUS。
  4. W_TEMP 在链接脚本里固定到 0x70~0x7F,并在四个体里都留出同名同偏移的单元。
  5. 启动代码里显式清零所有用到的 GPR,不能指望复位帮你初始化。
  6. 核算最坏嵌套深度:中断占一级,ISR 内部的调用链加主程序调用链合计不超过 7 级。
  7. 把"中断里跳转前设 PCLATH"写进代码评审清单,和 MISRA 一类的规范一起查。

七、排查对照:现象、根因与改法

现象常见根因改法
中断返回后跳到无关地址ISR 里改了 PCLATH 没恢复,或堆栈溢出ISR 内保存/恢复 PCLATH,核算嵌套深度
主程序变量在中断后变成别的值W 或 STATUS 未保存ISR 首尾加 PUSH/POP 宏
某个变量只有中断里改,主程序读到乱值中断里切了体,主程序还按原体读恢复 STATUS 即恢复体选,不要在 ISR 中间写 STATUS
上电第一次判断标志位就走错分支GPR 上电不复位,是随机值启动代码显式清零
用 INDF 清零 RAM 时死循环FSR 起始地址为 0,读到的是 00h从有效 GPR 地址起算,边界判断改用地址位
换到更大容量型号后偶发跑飞新器件有 4 页 4 体,原代码按 2K 单页写补页选择位与体选,检查链接后的函数分布

一句话总结:PIC 中档内核把地址空间拆成程序 4 页、数据 4 体,硬件只帮你压 PC,其余全靠软件。换大容量型号后出问题,本质是原来"用不上"的页选位和体选位突然开始生效;把 PCLATH、RP1:RP0、IRP 三处状态在当前上下文里查清楚,再配合一套正确的现场保护,跑飞和串数据这两类问题会同时消失。

还想接着看:C51 存储模式与代码重构、C51 调试中最常见的几类坑、MISRA C 141 条规则怎么分优先级、七步落地。

本文基于 Microchip《PICmicro 中档单片机系列》参考手册(DS31006A_CN 存储器构成、DS31008A_CN 中断)技术资料整理优化。

需要定制开发?

沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付

立即微信咨询

电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应