简介:PIC18系列单片机指令中文讲解是一份面向单片机初学者和嵌入式开发者的指令集学习资料,聚焦PIC18家族指令的快速入门与日常查阅。文档对算术运算、位元运算、程式流程控制、数据传输、逻辑运算及移位等常用指令进行了分类整理,并给出助记符、操作数、指令功能和影响旗号等关键信息,便于读者对照学习与编程时检索。资源包内仅包含1个doc文档,大小约722KB,内容结构紧凑,适合作为数据手册之外的中文辅助参考。该资源已有253人浏览学习,对于正在基于PIC18开展实验或项目开发的人员,可借助这份资料快速理清指令用法并减少查阅英文手册的时间。 我最早是从51单片机的汇编入门的。后来有个项目要用到硬件乘法、多个串口,我把目光转到PIC18系列,拿到数据手册翻到指令集那一章,说实话第一反应有点懵:同样写MOV、ADD,为什么后面要跟W、F、A一串操作数?GOTO和CALL为什么在指令里占两个字?想从Flash读个常量表,还要折腾TBLRD这种陌生指令?等真正把PIC18指令系统过了一遍,又踩过几次坑之后,我才意识到这套体系在8位机里属于设计得相当讲究的:指令总线16位、数据总线8位,指令能装下更多信息,运行效率也比想象中高。这篇文章就用中文把PIC18系列单片机的指令系统拆开讲一遍,讲每条常用指令怎么记、怎么用、有哪些坑,给从51转过来或者准备做产品的朋友一条能直接用的路。
1. 从51汇编转过来,第一道要破的思维坎:16位指令字的PIC18
1.1 PIC18的真实面目:指令宽、数据窄的哈佛结构
51单片机是8位指令字,一条MOV指令从操作码到地址都得挤在8位里,很多寄存器操作要靠累加器A中转。PIC18不是这个玩法。它的指令字固定16位,数据存储器和程序存储器相互独立,内部各走各的总线。也就是说,一条指令里可以同时塞下"目标RAM地址"、"结果该写到哪去"、"访问哪个Bank"这些信息,CPU取到一条16位的指令后,译码阶段就知道该怎么动。
这也是PIC18叫"8位机"却又不像传统8位机的原因:数据总线是8位的,但指令总线已经做成16位了。我当初从51切过来,最大的不适应就是这个——51的寄存器就是内部RAM,你要操作一个标志位,脑子里先想"这地址在哪一页";PIC18里则要先想"这个地址是不是Access区,要不要带BSR"。想通"指令字宽度决定了一条指令能表达多少信息"之后,再看PIC18的指令表,很多语法就顺了。
再一个打破惯性的是硬件栈。51的堆栈在RAM里,SP可以随便改,PIC18不一样,它的返回地址栈是独立的31层硬件栈,CALL自动压栈、RETURN自动弹栈,不需要软件维护SP。好处是子程序和中断切换极快,坏处是递归深度有限,11层以下是安全区,超过30层就可能触发堆栈溢出复位。这个设计对实时性要求高的应用非常友好,但对新手写递归函数是个大坑。
1.2 为什么Microchip要这么设计:编译友好与低延迟
PIC18的指令集目标是明确的:让C编译器好生成代码,同时保持中断响应快。如果你用过PIC16,会知道它的指令字是14位,但PIC16的很多操作要两步才能完成,间接寻址也只有一对FSR。PIC18把指令字拉宽到16位,增加FSR0/FSR1/FSR2三个间接寻址指针,还加了硬件乘法器,这些改动很直接地提升了C语言代码的密度。
我理解这套设计可以类比成"给8位核心配了一条16位的高速指挥通道"。数据总是8位搬运,但命令可以描述得更具体,比如一条ADDWF某RAM地址,同时决定结果写到WREG还是写回RAM。这种能力是51汇编里没有的,也是为什么PIC18的C编译器能生成比经典51更紧凑的汇编代码。理解了这些,你就明白后续要讲的f、d、a操作数不是故意复杂,而是为了把"数据流向"在一条指令里说清楚。
2. 指令里那个“W、F、A”到底是什么:字节操作类指令的通用语法
2.1 一条ADDWF指令拆开看:源地址、目标方向和访问区
PIC18的字节操作类指令大多数写成这种格式:
助记符 f, d, a比如:
MOVF PORTA, W, A ; 把PORTA读入WREG ADDWF count, F, A ; count = count + WREG INCF count, F, A ; count = count + 1三个操作数各管一件事:
- f:文件寄存器地址,也就是要操作的RAM单元或SFR,可以是0x00~0xFF这样的地址。
- d:结果去向,d=W代表结果写入WREG,d=F代表结果写回f本身。这一位是PIC18让我最头疼、也最爱的一点:你可以在一条算术指令里自由选择结果放哪,不用像51那样先把数据搬进累加器再搬出来。
- a:访问区选择,a=A表示访问Access区,和当前BSR(Bank Select Register)无关;a=B表示访问BSR指定的Bank区。刚接触时我经常忘写a,结果指令跑到错误的Bank里,修了半天才发现是访问区选错了。
举个例子,我想把地址0x20的变量加1,同时不影响WREG里的值,可以写INCF 0x20, F, A;如果想让结果进WREG,就写INCF 0x20, W, A。这种"结果可落两边"的设计,在实际写状态机和数据处理时非常省事,C编译器也很喜欢在这种语法上生成短指令。
2.2 常用字节操作指令速查,以及两个方向陷阱
下面这些是PIC18字节操作指令里我用得最频繁的,整理成表方便对着翻:
| 指令 | 功能 | 典型写法 |
|---|---|---|
| MOVF f, d, a | 读f到WREG,或测试f本身 | MOVF PORTA, W, A |
| MOVWF f, a | WREG写入f | MOVWF LATA, A |
| MOVFF src, dst | 任意RAM/SFR间直接传送,不经WREG,双字指令 | MOVFF 0x100, lat_buf |
| CLRF f, a | 清0 | CLRF count, A |
| INCF/DECF f, d, a | 加1/减1 | INCF count, F, A |
| ADDWF/SUBWF f, d, a | f与WREG做加法/减法 | SUBWF count, W, A |
| ANDWF/IORWF/XORWF f, d, a | 与/或/异或 | XORWF temp, F, A |
| COMF f, d, a | 按位取反 | COMF temp, F, A |
| NEGF f, a | 取二进制补码 | NEGF temp, A |
| RLNCF/RRNCF f, d, a | 不带进位循环移位 | RLNCF temp, F, A |
| RLCF/RRCF f, d, a | 带进位循环移位 | RLCF temp, F, A |
| SWAPF f, d, a | 高低半字节交换 | SWAPF temp, W, A |
| MULWF f | WREG乘以f,结果送PRODH:PRODL | MULWF mul_buf |
| MOVLB k | 立即数写入BSR,切换Bank | MOVLB 0x1 |
这里必须提醒两个方向陷阱。第一个是SUBWF和SUBLW的方向完全不同:SUBWF f, W, A执行的是"f - WREG",结果放WREG;而SUBLW k执行的是"立即数k - WREG",结果也放WREG。如果脑子里还保留51汇编"SUB B,A"那种"被减数在前"的习惯,很容易把减法算反。我早期写校准程序时,就用错了一次SUBLW,导致补偿值方向完全反了,设备实测数据偏到离谱。
第二个陷阱是乘法指令的结果在PRODH:PRODL,而且不改变STATUS里的Z、C、DC、OV、N标志。也就是说,你不能在MULWF之后直接BZ判断乘积是否为0,必须去读PRODL和PRODH判断。很多人把乘法当成普通算术指令,误以为会影响Z标志,这是PIC18指令系统里一个非常隐蔽的认知坑。
3. 程序拐弯的方式不止GOTO:跳过机制、相对跳转与双字指令
3.1 “跳过下一条”的机制:BTFSC/BTFSS与CPFSxx
PIC18里有一类指令不直接跳转,而是"跳过下一条指令",这是和51、STM32指令风格差异最大的地方。位测试类指令BTFSC和BTFSS就属于这类:
BTFSC f, b, a:测试f的第b位,如果该位为0,则跳过下一条指令。BTFSS f, b, a:测试f的第b位,如果该位为1,则跳过下一条指令。
实际使用中,我经常这样配合BRA实现短分支:
BTFSS PORTB, 0, A ; 如果RB0引脚=1,跳过下一行 BRA key_not_pressed ; RB0=0时执行这一行 BSF LATA, 0, A ; RB0=1时点亮LED key_not_pressed:这类指令省去了读位、比较、再跳转的多条指令,尤其适合按键扫描、状态标志判断。和位测试类似的还有比较跳过指令:CPFSEQ(相等时跳过)、CPFSGT(大于W时跳过)、CPFSLT(小于W时跳过)、TSTFSZ(f为0时跳过)。它们的好处是不影响STATUS标志,判断完直接分支,很多C编译器生成的代码就是这类指令的组合。
3.2 相对跳转BRA家族、双字GOTO/CALL与RETLW状态机
PIC18的跳转分成两个层次:近距离用BRA,远距离用GOTO。BRA指令带11位有符号偏移,可以在当前PC位置上下约1024条指令的范围内跳转,配合BC、BNC、BZ、BNZ、BN、BNN、BOV、BNOV这些条件跳转指令,构成了局部循环和短分支的主干。早期的51汇编里,短跳转和长跳转要靠不同的指令助记符区分,PIC18直接由汇编器算偏移,写起来要轻松很多。
而GOTO和CALL是双字指令,这也是PIC18新手最容易困惑的地方:明明16位指令很短,为什么GOTO要占两个指令字?因为GOTO需要携带20位目标地址,一个16位指令字装不下,所以第一字放部分地址,第二字放剩余的低16位。执行时CPU会把两个字作为一个整体,占用2个指令周期。因为是双字,代码里如果必须精确计算跳转表偏移,就要留意这一点。
状态机场景里RETLW非常好用:它等价于"把立即数放入WREG,然后RETURN返回"。配合调用一个查表子程序,一个按键码、一段显示段码,就能用连续RETLW搭出来:
get_seg_code: ADDWF PCL, F, A ; WREG是索引,把它加到PC低位 RETLW 0x3F ; 索引0:数字0段码 RETLW 0x06 ; 索引1:数字1段码 RETLW 0x5B ; 索引2:数字2段码但这里有个限制:PCL只是8位,这种跳转只能在一个256字节的指令块内完成,如果表很长、跨过了页面边界,就必须配合PCLATH/PCLATU设置页地址。我自己通常只在小表(几十项以内)上用RETLW,大常量表老老实实用TBLRD,后面会专门讲。
3.3 双字指令旁边的经典大坑:条件跳过可能踩到半条指令
用"跳过下一条"的指令时,如果下一条是GOTO、CALL、MOVFF、LFSR这类双字指令,就要非常小心。因为条件跳过的语义通常是"跳过一条指令",有的流水线实现会只让PC加一个字,结果跳到双字指令的第二字上,把第二字当成一条新指令执行,最终程序跑飞。虽然不同型号的PIC18对这种边界情况的处理有细微差异,但我的习惯是:在BTFSC/BTFSS/CPFSEQ等跳过指令后面,尽量只放单字指令,如果逻辑需要跳过一个远跳转,就在中间补一个NOP,把风险隔离掉。这个习惯帮我避免过不止一次莫名其妙的跑飞。
4. 查表、乘法与BCD:进阶指令里最容易被忽视的几个
4.1 TBLRD/TBLWT:程序存储器不是只能放代码
PIC18的常量表放在Flash程序空间,但CPU不能像读RAM一样直接用MOVFF去读,必须通过表格读写指令TBLRD/TBLWT。查表时要用到三个特殊寄存器:TBLPTRU、TBLPTRH、TBLPTRL(合起来是TBLPTR指针),以及TABLAT(读出的数据会放在这里)。TBLRD有四种指针操作模式:
TBLRD*:读取当前地址指向的字节。TBLRD*+:读取后TBLPTR自动加1。TBLRD+*:TBLPTR先加1再读取。TBLRD*-:读取后TBLPTR自动减1。
我实际写正弦表、温度补偿表时,最常用的是TBLRD*+,连续读完一个表后指针自动走,配合DECFSZ控制循环次数,代码很紧凑:
MOVLW HIGH(sin_table) MOVWF TBLPTRH, A MOVLW LOW(sin_table) MOVWF TBLPTRL, A MOVLW 64 ; 要读64个点 MOVWF cnt, A read_next: TBLRD*+ ; 读一个字节,指针后移 MOVFF TABLAT, buf, A?注意我上面只是示意,实际GTBLRD把结果放在TABLAT里,再用MOVFF TABLAT, target把数据搬到RAM缓冲区。如果你用XC8写C代码,定义const数组时编译器会自动生成TBLRD访问代码,这也是为什么PIC18的const数组比静态RAM数组慢一点,因为它本质上是按字节读Flash。
TBLWT则是向Flash写缓冲区写数据,配合EECON1寄存器和解锁序列才能真正擦写Flash,通常只有做Bootloader或数据存储功能时才用。建议新手不要随便玩写Flash,一个解锁序列写错,很可能把整个程序空间擦掉。
4.2 MULLW/MULWF、DAW,以及"不是所有指令都改STATUS"
PIC18加入硬件乘法器之后,MULWF f(WREG乘以RAM地址里的数)和MULLW k(WREG乘以立即数)用起来非常爽。乘积固定是16位,结果放在PRODH(高8位)和PRODL(低8位)。做8位无符号乘法完全不需要软件循环累加,一条指令搞定。但前面说过,乘法不改变STATUS的标志位,所以判断结果是否为0要自己读PRODH/PRODL。
另外有个冷门但实用的指令是DAW,十进制调整指令。做BCD码加法时,两个BCD码加出来可能得到一个非法的十六进制结果,比如0x09+0x01=0x0A,DAW会自动把它修正成0x10。做电子钟、计价器这些需要精确十进制的应用,DAW是必备的。我见过不少人在PIC18上硬写BCD修正逻辑,实际一条DAW就够。
还有一类容易被忽略的控制类指令:CLRWDT(清看门狗)、SLEEP(进入休眠)、RESET(软复位)。如果一个应用开了看门狗,主循环里忘了放CLRWDT,系统会被强制复位,排查起来往往以为是电源问题。把这些指令归类记清楚,写底层驱动时会省很多调试时间。
5. 中断现场靠硬件:31层硬件栈、快速寄存器和RETFIE的配合
5.1 中断向量与硬件栈的配合方式
PIC18经典系列的中断入口不是从某个中断源直接跳转,而是统一跳到固定向量:高优先级中断入口在0x0008,低优先级中断入口在0x0018。CPU响应中断时,会自动把当前PC压入硬件栈,再跳到对应向量地址执行。中断服务程序最后必须用RETFIE返回,它会从硬件栈弹出返回地址并恢复全局中断使能状态。
因为返回地址栈是硬件实现的,所以中断响应时不需要软件保存PC,速度比51那种"人肉压栈"快很多。但代价是你不能像访问普通内存一样随意操作这31层硬件栈。如果子程序嵌套层级很深,再加上中断嵌套,很可能把硬件栈顶破掉。PIC18默认配置下,硬件栈溢出/下溢会直接导致芯片复位(STVR使能时)。我调试过的一个工程就是因为递归调用把一个查找函数写深了,导致每运行一段时间就复位,最后定位到是硬件栈溢出。
这里建议:PIC18的程序设计要尽量用"状态机+短子程序"代替深层递归。如果确实需要复杂调用,先算好最大嵌套深度,给硬件栈留足余量。
5.2 Fast Register Set和中断现场恢复清单
PIC18有个很不"8位机"的福利:FAST Register Set,快速寄存器镜像。当你写CALL 子程序, FAST或者RETFIE FAST时,硬件会自动把WREG、STATUS、BSR三个关键寄存器的当前值存入影子寄存器,返回时再自动恢复。这相当于把中断现场保存中最麻烦的"WREG+STATUS+BSR"三条保存指令省掉了。
但fast不是万能的。它只镜像了WREG、STATUS、BSR,不等于整个现场就完整了。如果中断服务程序里会用到FSR、TBLPTR、TABLAT、PRODH/PRODL,这些寄存器依然要手动保存恢复。我常用的中断现场保存清单是这样的:
- WREG、STATUS、BSR:如果不用fast,就自己压栈;用fast则由硬件管。
- FSR0/FSR1/FSR2:中断里用到哪个就保存哪个,尤其是用
POSTINC这类间接寻址时。 - TBLPTR、TABLAT:如果中断里做了查表或Flash操作,这两个必须保护,否则中断返回后主程序查表指针乱掉。
- PRODH/PRODL:乘法结果寄存器,中断里碰过乘法就要保护。
写中断服务程序最忌讳的是"反正主程序没用到"这种想法。中断随时可能发生,保护现场永远是宁多勿缺。
6. 在MPLAB X里把PIC18指令“看透”:模拟器单步、反汇编与混合编程
6.1 用Simulator把每条指令摊开看
如果你手里暂时没有开发板,Microchip的MPLAB X IDE自带Simulator,可以不用硬件就能单步执行PIC18汇编指令。我个人强烈建议学习指令系统时把Simulator用起来,因为它能直接看到每条指令执行前后的WREG、STATUS、BSR、FSR、TBLPTR变化,比看数据手册的指令表直观得多。
在MPLAB X里新建工程,芯片选PIC18F4520或者你实际用的型号,写一段最简单的汇编,比如:
MOVLW 0x12 MOVWF count, A INCF count, W, A NOP END然后选择Simulator作为工具,点击单步执行,观察窗口里count从0x00变成0x12,再看WREG从0x12变成0x13,这样一个小的数据流就完整呈现出来了。很多你觉得"诶这个指令到底干嘛的"的疑问,单步两三次就全通了。Simulator还能模拟中断时序,调试中断向量和RETFIE尤其方便。
6.2 反汇编窗口和内联汇编:看懂编译器怎么"翻"PIC18指令
用C写PIC18工程时,MPLAB X里有个Disassembly窗口可以显示C语句对应的反汇编指令。我每次优化代码都会打开这个窗口看几眼,它对我理解PIC18指令系统帮助很大。比如你写a += b;,实际可能生成一条MOVF b, W, A加ADDWF a, F, A,看到这几条指令你就明白C编译器是在哪个寄存器上做临时变量的。
如果C里需要精确控制时序,可以用XC8的内联汇编:
__asm("BSF LATA, 0, A");老一点的工程也常见__asm ... __endasm块的形式。内联汇编的好处是能精准控制某条指令,缺点是可移植性变差。我建议只在中断关开、精确延时、特殊外设时序要求高的地方用。
6.3 我踩过几次之后沉淀下来的五个避坑点
最后把这几年用PIC18踩过的坑浓缩成几句话,无论汇编还是C写底层都适用:
- 条件跳过指令后面不要紧跟双字指令,必要时补NOP隔离,防止跳到第二字上。
- 访问RAM一定要想清楚用A还是B。跨Bank操作先用MOVLB设BSR,别指望编译器替你收拾所有烂摊子。
- 中断服务程序里即使不用fast,也要保护TBLPTR/TABLAT和FSR。查表指针在中断里被打断,是很多随机性bug的根源。
- 不要写深递归。PIC18不是不能递归,但31层硬件栈是硬约束,递归层数深了就是堆栈溢出复位。
- 同一条指令在不同PIC18子系列上可能有细微差异,比如带K或Q的新型型号指令集有扩展,装新芯片前一定查对应Data Sheet的指令集章节。
我自己现在的习惯是:所有SFR操作都显式写访问区后缀,所有跨Bank变量都统一先MOVLB再操作,所有中断服务程序固定先压FSR再干活。这些习惯看起来很笨,但能让你从指令级就开始控制程序的确定性。PIC18这套指令系统,值得你花一周时间去啃,啃完之后再回头看51、STM32的汇编,你会对整个单片机世界多一层理解。
本文还有配套的精品资源,点击获取