ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PIC18指令系统实战解析:从51汇编到16位指令字的进阶之路

PIC18指令系统实战解析:从51汇编到16位指令字的进阶之路 简介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位指令字的PIC181.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结果去向dW代表结果写入WREGdF代表结果写回f本身。这一位是PIC18让我最头疼、也最爱的一点你可以在一条算术指令里自由选择结果放哪不用像51那样先把数据搬进累加器再搬出来。a访问区选择aA表示访问Access区和当前BSRBank Select Register无关aB表示访问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, AMOVWF f, aWREG写入fMOVWF LATA, AMOVFF src, dst任意RAM/SFR间直接传送不经WREG双字指令MOVFF 0x100, lat_bufCLRF f, a清0CLRF count, AINCF/DECF f, d, a加1/减1INCF count, F, AADDWF/SUBWF f, d, af与WREG做加法/减法SUBWF count, W, AANDWF/IORWF/XORWF f, d, a与/或/异或XORWF temp, F, ACOMF f, d, a按位取反COMF temp, F, ANEGF f, a取二进制补码NEGF temp, ARLNCF/RRNCF f, d, a不带进位循环移位RLNCF temp, F, ARLCF/RRCF f, d, a带进位循环移位RLCF temp, F, ASWAPF f, d, a高低半字节交换SWAPF temp, W, AMULWF fWREG乘以f结果送PRODH:PRODLMULWF mul_bufMOVLB k立即数写入BSR切换BankMOVLB 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与CPFSxxPIC18里有一类指令不直接跳转而是跳过下一条指令这是和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 ; RB00时执行这一行 BSF LATA, 0, A ; RB01时点亮LED key_not_pressed:这类指令省去了读位、比较、再跳转的多条指令尤其适合按键扫描、状态标志判断。和位测试类似的还有比较跳过指令CPFSEQ相等时跳过、CPFSGT大于W时跳过、CPFSLT小于W时跳过、TSTFSZf为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以及不是所有指令都改STATUSPIC18加入硬件乘法器之后MULWF fWREG乘以RAM地址里的数和MULLW kWREG乘以立即数用起来非常爽。乘积固定是16位结果放在PRODH高8位和PRODL低8位。做8位无符号乘法完全不需要软件循环累加一条指令搞定。但前面说过乘法不改变STATUS的标志位所以判断结果是否为0要自己读PRODH/PRODL。另外有个冷门但实用的指令是DAW十进制调整指令。做BCD码加法时两个BCD码加出来可能得到一个非法的十六进制结果比如0x090x010x0ADAW会自动把它修正成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三个关键寄存器的当前值存入影子寄存器返回时再自动恢复。这相当于把中断现场保存中最麻烦的WREGSTATUSBSR三条保存指令省掉了。但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的汇编你会对整个单片机世界多一层理解。本文还有配套的精品资源点击获取
返回列表