同一份代码从 2K 字型号搬到 8K 字的 PIC16C74A,按键误触发、串口错位、中断返回跑飞接连出现。结论先给:中档内核对程序存储器分 4 页、数据存储器分 4 体,中断只压了 PC,W 与 STATUS 全靠软件保存;换大容量型号后,"原来用不上"的页选位和体选位突然开始生效。
一块小家电控制板,从 2K 字程序空间的型号换成 8K 字的 PIC16C74A,代码几乎原样搬过去,功能却开始出问题:按键偶尔误触发、串口收到的数据错位,最严重的一次是中断返回后直接跑飞,看门狗复位才恢复。结论先给:这类"换大容量型号就出问题"的现象,根因基本不在外设,而在中档内核的两套地址空间——程序存储器的 4 页要靠 PCLATH<4:3> 跳,数据存储器的 4 个体要靠 RP1:RP0 和 IRP 切,而中断只压了 PC,W 与 STATUS 必须自己保存。
中档系列单片机有一个 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> 也无意义,但不要把它当普通读写位用,将来换型号会出问题。
数据存储器由 SFR 和 GPR 组成,分成 4 个体,每个体最多 128 字节(7Fh),SFR 排在低地址、GPR 排在高地址。直接寻址时,指令里给出 7 位地址,高位由 STATUS 的 RP1、RP0 补上;间接寻址时,9 位有效地址由 STATUS 的 IRP 与 8 位 FSR 拼成。两套选择位互不相干,这是最容易混的地方。
| 目标体 | 直接寻址(RP1 : RP0) | 间接寻址(IRP) |
|---|---|---|
| Bank 0 | 0 : 0 | 0(配合 FSR 的 00h~7Fh) |
| Bank 1 | 0 : 1 | 0(配合 FSR 的 80h~FFh) |
| Bank 2 | 1 : 0 | 1(配合 FSR 的 00h~7Fh) |
| Bank 3 | 1 : 1 | 1(配合 FSR 的 80h~FFh) |
为了少切换体,较新的器件把每个体最后 16 字节映射到 Bank 0,也就是常说的公用 RAM,同一个地址在四个体里指向同一块物理存储。这条特性直接决定了中断现场保护怎么写:W_TEMP 就放在这个公用区(例如 0x70~0x7F),四个体同样的偏移都能访问,保存 W 时不必先判断当前在哪个体。老器件没有公用区,就得多一步切体动作。
还有一个初始化陷阱:上电复位并不初始化 GPR,其他复位也不改变 GPR 的值。变量"上电是随机值"不是 bug,是规格。启动代码里必须显式把用到的 GPR 清零,否则第一次判断标志位就可能走错分支。
间接寻址靠 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 级深、13 位宽,既不占程序空间也不占数据空间,栈指针不可读写。压栈 8 次之后,第 9 次压栈会覆盖第 1 次的数据,第 10 次覆盖第 2 次,依次类推。关键在于:没有表示堆栈溢出或下溢的状态位。也就是说,嵌套过深导致的返回地址丢失,不会在发生的那一刻报错,而是在某个 RETURN 之后跳到完全无关的地址,表现为"跑飞"。
这条规格对设计的影响很直接:中断里不要再套多层函数调用,函数嵌套层数要人工核算,禁止递归。8 级听起来够用,但中断本身占一级,ISR 里再调三层函数,主程序那边就只剩 4 级余量。
响应中断时,硬件只把返回地址压进堆栈,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 个多周期要算进预算里。
| 现象 | 常见根因 | 改法 |
|---|---|---|
| 中断返回后跳到无关地址 | 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小时内响应