3.2.3 MPASM 的偽指令
我們在第一章中已經詳細介紹了中檔PIC 單片機的35 條指令,源程序的編寫主要就是
用這些基本的指令實現你的控制任務。但為了增加源程序的可讀性和可維護性,我們引入了
偽指令的概念。偽指令本身不會產生可執行的匯編指令,但它們可以幫組“管理”你編寫的
程序,其實用性和必要性絕不亞于35 條正真的匯編指令。我們在此著重介紹最常用的幾種
偽指令。
#include 或include
#include 偽指令的作用是把另外一個文件的內容全部包含復制到本偽指令所在的位置。
被包含復制的文件可以是任何形式的文本文件,當然文件中的內容和語法結構必須是
MPASM 能夠識別的。最經常被“include”的是針對PIC 單片機內部特殊功能寄存器定義的
包含頭文件, 在MPLAB 安裝后它們全部放在路徑“ C:\Program Files\MPLAB
IDE\MCHIP_Tools”下,每一個型號的PIC 單片機都有一個對應的預定義包含頭文件,擴展
名是“.inc”。除了一些符號預定義文件,你也可以把現有的其它程序文件作為一個代碼模塊
直接“包含”進來作為自己程序的一部分。見例3-01。
#include <p16f877a.inc> ;把預定義的PIC16F877A 寄存器符號包含到此處
#include ”math.asm” ;把現有的程序文件包含進來作為自己代碼的一部分
例3-01
請注意被包含文件的引用方式。一種是<>尖括號引用,這種引用意味著讓編譯器去默
認的路徑下尋找該文件,MPASM 默認的寄存器預定義文件存放路徑即為上面提及的
MPLAB 安裝后的目錄;另一種是””雙引號引用,這種引用方式的意思是指示編譯器從引號
中指定的全程文件路徑下尋找該文件。例3-01 中”math.asm”沒有指定路徑,即意味著在
當前項目路徑下尋找math.asm 文件。如果編譯器找不到被包含的文件,將會有錯誤信息告
知。
請在你的源程序中盡量用MPLAB 標準頭文件定義的寄存器符號。一來這些被定義的寄
存器符號和芯片數據手冊上的描述一一對應,理解起來即直觀又容易;二來如果用你自己定
義符號就缺乏一個大家能一起交流的標準平臺,其他人要解讀你的代碼時將費時費力。故例
3-01 中的首行#include 包含引用偽指令可以說是PIC 單片機程序編寫時的標準必備。
list
list 偽指令可以設定程序編譯時的一些信息,例如所選單片機的型號,編譯時選擇的缺
省數制等。例如:
list p=16f877a, r=DEC ;單片機型號為PIC16F877A,無特別指明的數字為十進制數
例3-02
如果程序開發時使用項目管理的模式,則所有list 偽指令可以描述的參數項都可以在項
目的設定選項中通過對話框的形式設定并保存。在此只需對list 偽指令稍作了解即可。
__config
此偽指令的重要作用是把芯片的配置字設定在源程序中,請參閱2.5 節的詳細說明。建
議大家盡量用此偽指令把芯片的配置字寫在程序中。
__idlocs
PIC 單片機中有一處非常特殊的標記單元。它獨立于任何其它存儲器,唯一的作用就是
作為一個標記。此標記值無法用軟件讀到,讀取和寫入的方法只有通過編程器實現。此標記
值沒有讀保護,你可以利用它存放程序的版本或日期等信息。如果需要,則可以用偽指令
__idloc 在程序中定義具體的值。
__idloc 0x1234 ;設定芯片的標記值為0x1234,注意前面有兩個下劃線符
例3-03
和__config 偽指令定義的配置字一樣,用__idloc 定義的芯片標記值在最后也會存放在
HEX 文件中,這就要求編程器能夠解析它。
errorlevel
errorlevel 的用途是控制編譯信息的輸出顯示。編譯器在編譯你的源程序時會提供很多
信息,有些信息是你必須要處理的,例如錯誤信息(Error),只要有錯誤信息存在,你的程
序將永遠無法完成編譯;有些可能只需要關注,例如警告信息(Warning);也有一些可能你
根本就不感興趣,它們只是一些提示信息(Message)而已。注意出現警告和提示信息時將
不會中止編譯器的編譯工作,你的程序將被編譯并最終產生HEX 文件。圖3-14 中顯示了一
個程序編譯后的各種信息實例,其中既有錯誤信息,也有警告和提示信息。我們可以用
errorlevel 偽指令來控制輸出信息的級別,或刻意關閉/打開一些提示信息。
編譯信息的輸出顯示級別有三種,分別是0、1 和2。級別0 代表顯示所有信息,包括
各種錯誤、警告和提示信息,如圖3-14 所示;級別1 代表顯示錯誤和警告信息,忽略提示
信息;級別3 代表只顯示錯誤信息而忽略警告和提示信息。在任何一個大的級別上還可以對
某些信息單獨設定顯示或關閉。每個信息都有一個識別標號,見圖3-14 中信息項“[]”中的
數字,打開或關閉某類信息只需在errorlevel 偽指令中引用信息識別標號,并在其前面用“+”
或“-”號,即代表打開或關閉這一類信息,例如:
errorlevel 0, -302, -305 ;顯示所有信息,但不需要302 和305 這兩類提示信息
errorlevel 1, +305 ;顯示錯誤和警告信息,但同時還要關注305 類的提示信息
圖3-14
例3-04
#define / #undefine
#define 的作用是定義常數符號,即用一個符號變量替換另一個符號串或變量。被替換
的可以是任意字母數字組成的符號但替換者本身不能是一個純數字。例如:
#define DELAY_TIME 1000 ;定義常數符號,即用DELAY_TIME 符號代替1000
#define KEY1 PORTB,7 ;用KEY1 符號代替端口PORTB 的第7 引腳
例3-05
用#define 偽指令定義符號后,可使程序中的變量或指令變得更具實際意義,也使程序
變得更易維護。指令“btfss PORTB,7”和“btfss KEY1”在事先用了例3-05 中的#define 后
編譯的結果是一樣的,但明顯地后者看起來更容易理解,一看就知道這是在測試編號為
KEY1 的一個按鍵。而且如果你的硬件設計改動了KEY1 所接的單片機引腳,只要改動這一
處#define 重新定義引腳位置,程序的其它部分無需任何修改,再編譯一次即可得到更新后
的軟件代碼。一個好的編程習慣是事先把一些代表實際意義的變量、單片機的輸入輸出引腳
在硬件電路中的實際功能等用#define 偽指令定義成簡單直觀的符號名字,然后在程序中直
接用其符號名字而不用簡單機械的數字形式。替換的工作由編譯器在編譯時自動完成。它會
先掃描你的源程序代碼,把事先#define 的符號名改回成被替換的字符串,然后再繼續編譯
生產機器碼。
equ
equ 顧名思義是“等于”的意思,其作用和#define 偽指令有點類似,也是用一個符號名
字替換其它數字變量,但它只能替換立即數。如果要替換一個符號名字,則此符號名必須事
先用#define 或equ 偽指令已經定義替換了一個立即數。例如:
#define MyCount 0x70 ;定義MyCount 符號替換立即數0x70
w_temp equ 0x20 ;符號名w_temp 等于0x20
count1 equ MyCount ;符號名count1 等同于MyCount
;如果MyCount 沒有事先定義則會產生一個錯誤
例3-06
在絕對定位的編程模式中equ 被經常用于定義用戶自己的變量,即用一個符號名代替一
個固定的存儲單元地址,上例3-06 中的w_temp 定義即屬于此類。用equ 方式定義的符號在
匯編后可以生成相關的調試信息,可以通過各種變量觀察的方式顯示此符號所代表的內存地
址處的數據內容,但用#define 方式定義的符號則不能產生調試信息。要注意equ 偽指令本
身并沒有限定所定義的一定是一個變量地址,它只是一個簡單的符號和數字替換而已,其意
義必須和具體的指令結合才能確定,如下例3-07 中對符號w_temp 的理解。
w_temp equ 0x20 ;符號名w_temp 等于0x20
movlw 0x55 ;W=0x55
movwf w_temp ;把W 的值送給變量w_temp, (0x20 單元內容=0x55)
movf w_temp, w ;把w_temp 單元內容送W, (W=0x55)
movwf FSR ;把W 的內容送FSR, (FSR=0x55)
movlw w_temp ;把w_temp 所代表的立即數即地址值送給W, (W=0x20)
movwf FSR ;讓FSR 指針指向w_temp, (FSR=0x20 而不是0x55)
例3-07
cblock / endc
用equ 偽指令可以給一個符號變量分配一個地址。但在一個程序設計過程中往往需要定
義很多變量,你當然可以給每一個變量逐個用equ 的方法分配一個地址空間。但如果變量很
多,這樣做就顯得非常麻煩,你必須自己安排每個變量的地址,小心不能出現地址重疊;若
要在已定義分配好的變量間插入新的變量,那就必須重新逐個安排隨后變量的地址等等。
cblock/endc 偽指令可以輕松解決有很多變量定義的場合出現的這些問題,我們把它叫作變
量塊連續定義。具體用法如下:
cblock 偽指令聲明變量塊的起始地址,endc 偽指令聲明變量塊定義結束,cblock/endc
中間可以插入任意多的變量聲明。其地址編排由編譯器自動計算:第一個變量地址分配從起
始地址開始,然后按所聲明變量保留的字節數自動分配后面變量的地址,變量所需保留的字
節數用“:”加后面的數字表示,如果只有一個字節“:1”可以省略不寫。以例3-08 來說明:
cblock 0x20 ;變量定義起始地址為0x20
w_temp ;w_temp 地址為0x20,占一個字節
status_temp ;status_temp 地址為0x21,占一個字節
buffer:8 ;buffer 的起始地址為0x22,并保留8 個字節單元
var1 ;var1 的地址為0x2a,占一個字節
var2 ;var2 的地址為0x2b,占一個字節
endc ;結束變量連續定義
例3-08
用cblock 方式定義的變量和用equ 方式定義的變量一樣在匯編后可以生成相關的調試
信息,可以通過各種變量觀察的方式顯示此符號所代表的內存地址和其中的數據內容,所以
實際編程時一般無需關心計算每個變量的具體地址。程序員要注意的用這種方式連續定義很
多變量時不要讓變量塊跨越所處bank 的邊界。你可以在cblock 中隨意插入新定義的變量,
或通過改變起始地址的方式使變量塊整個挪到其它內存地址處,地址的更新由編譯器代勞。
org
org 用以定義程序代碼的起始地址,通過此偽指令你可以把程序定位到任何可用的程序
空間,它實現的是程序代碼絕對定位,如例3-09:
org 0x0000 ;定義復位入口地址,以下指令從地址0x0000 開始
goto main ;
org 0x0004 ;定義中斷入口地址,以下指令從地址0x0004 開始
movwf w_temp ;保存w
;... ;其它中斷服務代碼
org 0x0800 ;定義page1 的起始地址,以下指令代碼放在page1
Sub1 return
例3-09
只要你認為代碼需要確定放在某一特定地址處,在程序的任何地方都可以用org 偽指令
重新定義存放的起始地址,且地址順序可以任意編排。但要注意的是若干個確定起始地址的
代碼塊不能相互重疊,否則編譯器會報錯,無法得到正確結果。若用可重定位方式開發指令
代碼時一般不能用org 偽指令絕對定位代碼。
dt
dt 的作用是定義表格數據。在第一章介紹基本匯編指令時已經提到,PIC 單片機實現表
格定義的最基本指令是“retlw xx”,表格中的每一個字節數據都以指令“retlw”的形式出現。
若表格較大,就需要很多“retlw”指令,比較麻煩,可讀性也差。這時我們可以用此“dt”
偽指令替代“retlw”實現很多數據的表格定義。如例3-10:
Table addwf PCL,f ;PC 相對尋址查表
dt 0 ;retlw 0
dt 1, 2, ’3’ ;retlw 1
;retlw 2
;retlw 0x33 (’3’的ASCII 碼)
dt ”ABC” ;retlw ’A’
;retlw ’B’
;retlw ’C’
例3-10
de
de 偽指令可以讓你在源程序中定義片內EEPROM 的初值。毫無疑問,該條偽指令只適
用于那些內含EEPROM 數據存儲器的單片機,例如:PIC16F87x、PIC16F62x 等等。在中檔
PIC 單片機中,除了PIC16F7x 系列外,其它Flash 型的單片機都有片上EEPROM,只是字
節數多少的問題。你可以編寫代碼在程序運行時來設定片內EEPROM 數據區的初值,但此
EEPROM 區還可以在芯片編程燒寫時通過編程器對其設定初值。對編程器而言EEPROM 數
據區是程序空間的延伸,它有個特別的編程起始地址0x2100。基于這一前提,我們可以在
源程序中利用“org”和“de”偽指令定義片內EEPROM 數據的初值,這樣最后得到的HEX
文件被燒入到單片機內后,EEPROM 區就同時被特定數據所初始化。看例3-11 的實例
org 0x2100 ;特殊的程序空間起始地址
;編程器能識別此地址作為EEPROM 數據區的起始地址
de 0, 1, 2, 3 ;EEPROM 地址單元[0]=0, [1]=1, [2]=2, [3]=3
de ”ABCD” ;[4]=0x41, [5]=0x42, [6]=0x43, [7]=0x44
例3-11
按例3-11 所示的定義,芯片完成編程燒入后,其內部EEPROM 區從0x00 單元開始被
分別初始化成0x00、0x01、0x02、0x03、0x41、0x42、0x43、0x44。其它未被初始化的EEPROM
單元全部是0xff。
要注意并不是所有的編程工具都能支持此法定義的EEPROM 初始值燒入。能直接掛接
在MPLAB 環境下的Microchip 原廠或兼容的編程工具都可以支持“de”偽指令定義的
EEPROM 初值燒入,但其它第三方生產的編程工具就不一定,使用前請咨詢編程器的生產
廠商。
fill
fill 偽指令可以實現對程序空間連續自動填充某一特定的指令數據,被填充的可以是一
個立即數(實際肯定代表某一條指令),也可以是一條形象的匯編指令。基本上在一個設計
中都有一些程序空間沒有寫上具體的指令編碼(空白處),在單片機正常運行時這些地方的
指令是不會被執行到的。但在有干擾的情況下程序跑飛正好落在這些非法指令處時,就有必
要設置軟件陷阱捕捉這些非法跳轉,讓程序恢復正常運行。如果要程序員一個一個地址去分
析哪里有空的指令單元然后又用特殊指令一條一條填入,這是根本行不通的。fill 偽指令在
這時就派上用場了。
fill 0x0000, 5 ;從當前地址處連續5 個程序字填成0x0000(NOP 指令)
fill (goto $), NEXT_BLOCK-$ ;從當前地址開始到標號NEXT_BLOCK 前所有程序空間填上
;goto $ (死循環)指令
org 0x0800
NEXT_BLOCK
例3-12
請大家特別注意上例3-12 中第二行fill 偽指令的用法。在你自己的程序中也可以用同樣
的方法把所有未用到的程序空間填上“goto $”這樣一條死循環的指令。一旦單片機執行過
程中非法跳到這些指令處時指令運行就將被“俘獲”,停在那里直到看門狗復位,然后程序
從頭開始。這是軟件陷阱的最基本處理方法。若填充指令“goto 0x0000”直接跳轉到復位地
址處可能會有問題,因為goto 指令執行時必須和PCLATH 寄存器配合(跨頁跳轉的問題),
若PCLATH[4:3]不為00 就不能跳到復位地址0x0000 處。在程序跑飛非法跳轉到設定的陷阱
處時你又怎能保證PCLATH 中的頁面設定為正好指向第0 頁?
end
end 偽指令告訴匯編編譯器編譯工作到此為止,end 后面所有的信息,不管正確與否,
一概不管。絕大多數情形下你的程序的最后一行應該是“end”。無論如何,end 必須出現在
程序中,不然編譯器會報錯,無法進行編譯工作。
3.2.4 MPASM 內的直接運算符
為了使所編的程序理解更直觀,維護更方便,MPASM 匯編器允許你在程序的編寫過程
中直接以數學表達式的形式在指令中實現一些數字運算的功能。千萬不要誤解成MPASM 可
以替你生成數學運算的指令,那可是其它編譯器(例如C 編譯器)才能完成的工作。這里
講的數字運算前提是所有參與運算的操作數全部是明明白白的立即數,如果是符號名字則必
須事先用#define 或equ 偽指令明確定義了的。整個運算過程是由編譯器在掃描你的源程序
時進行的,運算結果也只能是一個確定的立即數。我們將在這里介紹幾種非常有用的運算符。
取當前指令的地址值:$
你可以在寫程序時給一條指令前加上一個標號,然后直接引用該標而得到此程序字的地
址。如果你的程序經常需要用到指令的當前地址或附近的地址值,這樣的標號就需要寫很多
且不能重復。用“$”運算符讓匯編器替你計算當前指令所處的位置將有效地減輕你的這份
工作量。見例3-12 和3-13。
;用語句標號得到指令地址
Here goto Here ;跳轉到當前地址,程序進入死循環
Delay decfsz count, f ;計數器減1 并判0
goto Delay ;跳轉到上一行重復循環
;用$運算符得到指令地址而無需定義任何語句標號
goto $ ;跳轉到當前地址,程序進入死循環
decfsz count, f ;計數器減1 并判0
goto $-1 ;跳轉到(當前地址-1)處,即上一行,重復循環
例3-13
取16 位立即數的高低字節:high 和low
一個16 位的立即數在8 位單片機中必須被拆解成高8 位一個字節(高字節)和低8 位
一個字節(低字節)才能用指令一條條處理,類似的處理在對兩字節變量賦立即數初值和基
于PC 相對跳轉查表前設定PCLATH 寄存器時經常碰到。MPASM 提供了high 和low 兩個運
算符分別計算一個立即數的高字節和低字節。我們看例3-14 的代碼實例:
;兩字節變量賦立即數初值
#define DELAY_TIME .1000 ;定義一個常數立即數
movlw low(DELAY_TIME) ;取立即數的低字節值,經編譯器計算將得到0xe8
movwf count ;賦給變量的低字節
movlw high(DELAY_TIME) ;取立即數的高字節值,經編譯器計算將得到0x03
movwf count+1 ;賦給變量的高字節
;查表前設定PCLATH 寄存器。關于PC 相對跳轉的概念詳見1.5.2 節
movlw high(Table) ;取查找表入口地址的高字節值
movwf PCLATH ;設定PCLATH 寄存器
movf index,w ;取查表索引值
call Table ;調用查表子程序
例3-14
加減乘除:+ - * /
實際上前面的很多代碼范例中都已經說明了“+”、“-”運算符的使用方法。“*”和“/”
的運算也類似。看下面例3-15 計算異步串行通訊波特率常數的方法。
;高速異步通信波特率BPS=Fosc/(16*(X+1))
;故,波特率常數X = Fosc/(BPS*16) – 1
#define BPS .9600 ;定義工作波特率
#define Fosc .4000000 ;定義單片機工作振蕩頻率4MHz
;... ;其它代碼
movlw Fosc/(BPS*.16) – 1 ;編譯器計算得到.25(10 進制25)
movwf SPBRG ;設定波特率定時寄存器
例3-15
程序中用了統一的計算公式后,在調試時只要簡單地改變前面的#define 語句定義新的
波特率或振蕩頻率值,然后重新編譯一次程序即實現了波特率設定代碼的更新,非常方便。
移位運算:>> 和<<
“>>”運算符把一個立即數算術右移若干位(高位補0),“<<”運算符把一個立即數算
術左移若干位(低位補0)。
#define xxx 0x55
movlw xxx>>1 ;W=0x2a
movlw xxx<<2 ;W=0x54
movlw 1<<7 ;W=0x80
例3-16
立即數邏輯運算: & | ^
“&”運算符把一個立即數和另外一個立即數相“與”;“|”運算符把一個立即數和另外
一個立即數相“或”;“^”運算符把一個立即數和另外一個立即數相“異或”。例3-17 的代
碼利用異或運算符“^”實現類似于C 語言“switch-case”功能的匯編代碼指令,注意例中
的VAL1、VAL2、VAL3 等判別值都是事先已經定義的立即數而不是RAM 中的變量。
;利用異或運算實現類似于C 語言的switch-case 語句
movf switchVal, w ;取分支判斷值. switch (W)
xorlw VAL1 ;W=W ^ VAL1
btfsc STATUS, Z ;判0 標志
goto Case_VAL1 ;case VAL1: (原始W=VAL1)
xorlw VAL1^VAL2 ;W=(W^VAL1)^(VAL1^VAL2) = W^VAL2
btfsc STATUS, Z ;判0 標志
goto Case_VAL2 ;case VAL2: (原始W=VAL2)
xorlw VAL2^VAL3 ;W=(W^VAL2)^(VAL2^VAL3) = W^VAL3
btfsc STATUS, Z ;判0 標志
goto Case_VAL3 ;case VAL3: (原始W=VAL3)
;... ;其它case 情況判別
例3-17
3.2.5 MPASM 的宏指令
引入宏指令的目的也是為了增強程序的可讀性和易維護性。和偽指令不同的是,偽指令
所起的只是輔助性的作用,其本身不會直接產生真正的機器碼;但宏指令是真正的指令,它
實際上是若干條基本匯編指令的集合。為了編程方便,MPASM 已經內含了一些非常好用的
宏指令,用戶也可以自己編寫任意形式的宏指令。
3.2.5.1 MPASM 內含的宏指令
MPASM 內含的宏指令就象擴充了的標準匯編指令一樣,其名字已作為MPLAB 的關鍵
詞而被保留。雖然經過編譯器編譯后最終將變成真正的匯編指令機器碼,但某些宏指令的轉
換過程還是有其獨到之處。
banksel
banksel 和下面的pagesel 宏指令可以說是所有宏指令中最好用最有用的了。banksel 可
以幫助你非常方便地實現寄存器bank 的設定。你只需在banksel 后給它一個變量名或地址,
編譯器會自動按照變量地址所在的bank,自動生成設定STATUS 寄存器RP1:RP0 位的指令。
更聰明的是,編譯器知道你所選的芯片最多有幾個bank,它將用最少的指令完成bank 設定。
例如:
;芯片選擇PIC16F874A,RAM 共有2 個bank
banksel TRISC ;設定TRISC 所在的bank (TRISC 在bank1)
;編譯后的機器碼
bsf STATUS, RP0 ;只生成1 條匯編代碼
;芯片選擇PIC16F877A,RAM 共有4 個bank
banksel TRISC ;設定TRISC 所在的bank (TRISC 在bank1)
;編譯后的機器碼
bsf STATUS, RP0 ;生成2 條匯編代碼
bcf STATUS, RP1 ;
例3-18
同樣的一條“banksel TRISC”指令,針對不同的芯片編譯器生成的匯編代碼可能不同。
兩個bank 的芯片只要用到RP0 一位即可實現bank 選擇,banksel 宏指令會轉換成一條匯編
指令;四個bank 的芯片則必須用RP1:RP0 兩位一起實現bank 選擇,故一條banksel 宏指令
將轉換成兩條匯編指令。用banksel 的好處是顯而易見的,你無需太多關心你準備操作的寄
存器落在哪個bank 內,編譯器會知道這個寄存器的實際地址,然后替你生成相關的匯編代
碼以正確設定bank 位;需要時你可以隨意移動變量的定義地址而無需修改其它代碼,只需
重新編譯一次即可;另外,如果你用代碼可重定位方式進行軟件開發時,在寫指令之時根本
就無法知道自己定義的變量最后會落在哪個bank 中,想自己設定具體的bank 都不行。此時,
只有用banksel 宏指令讓編譯器連接器一起在連接定位后再“自動填入”相關的bank 位設定
指令。
bankisel
和banksel 類似,不過它對付的是用于寄存器相對尋址的STATUS 寄存器中的IRP 位。
它也會用最少的代碼實現IRP 位的設定。如果是只有兩個bank 的芯片,用bankisel 將不產
生任何指令!在代碼可重定位開發方式下,對可重定位的變量作相對尋址需要設定IRP 位
時,也只能用bankisel 交由編譯器連接器來替你實現。
;芯片選擇PIC16F877A,RAM 共有4 個bank
cblock 0x120
buffer:8 ;從地址0x120 起定義8 字節的數據區
endc
bankisel buffer ;用bankisel 自動設定IRP 位
movlw low(buffer) ;取buffer 的地址(只有低8 位)
movwf FSR ;送給FSR
;編譯后的機器碼
bsf STATUS, 7 ;真正的設定IRP 的匯編代碼
movlw 0x20
movwf FSR
例3-19
pagesel
pagesel 可以幫助你設定程序的頁面。使用方式和banksel 相似,只是它改變的是
PCLATH[4:3]兩位。該宏指令也同樣將用最少的代碼實現程序頁面設定:程序空間不超過2K
字(只有1 頁)的將不產生任何匯編代碼;程序空間不超過4K 字(最多2 頁)的芯片將只
生成一條設定PCLATH[3]的匯編代碼;只有超過4K 字(最多4 頁)的芯片才會生成兩條代
碼。同樣,pagesel 在代碼可重定位的開發模式下也是不可或缺的。
;芯片選擇PIC16F877A,RAM 共有4 個頁面
org 0x0100 ;在第0 頁內
main pagesel sub1 ;用宏指令設定被調用子程序的頁面
call sub1 ;隨后調用該子程序
pagesel $ ;用宏指令設定當前地址的頁面
goto main ;循環
org 0x0800 ;第1 頁起始
sub1 return ;子程序返回
;編譯后的機器碼(main 部分)
main bsf PCLATH, 3 ;設定sub1 所在的頁面
bcf PCLATH, 4
call sub1
bcf PCLATH, 3 ;設定當前指令所在的頁面
bcf PCLATH, 4
goto main
例3-20
clrc/setc
clrc/setc 針對的是狀態寄存器STATUS 中的進位標志位。
clrc 等同于bcf STATUS, C ;C=0
setc 等同于bsf STATUS, C ;C=1
clrz/setz
clrz/setz 針對的是狀態寄存器STATUS 中的零標志位。
clrz 等同于bcf STATUS, Z ;Z=0
setz 等同于bsf STATUS, Z ;Z=1
clrdc/setdc
clrdc/setdc 針對的是狀態寄存器STATUS 中的半字節進位標志位。
clrdc 等同于bcf STATUS, DC ;DC=0
setdc 等同于bsf STATUS, DC ;DC=1
skpc/skpnc
skpc/skpnc 是判狀態寄存器STATUS 中的進位標志位,若條件滿足則程序跳過下一條指
令。
skpc 等同于btfss STATUS, C ;若C=1 則程序跳過下一條指令
skpnc 等同于btfsc STATUS, C ;若C=0 則程序跳過下一條指令
skpz/skpnz
skpz/skpnz 是判狀態寄存器STATUS 中的零標志位,若條件滿足則程序跳過下一條指令。
skpz 等同于btfss STATUS, Z ;若Z=1 則程序跳過下一條指令
skpnz 等同于btfsc STATUS, Z ;若Z=0 則程序跳過下一條指令
skpdc/skpndc
skpdc/skpndc 是判狀態寄存器STATUS 中的半字節進位標志位,若條件滿足則程序跳過
下一條指令。
skpdc 等同于btfss STATUS, DC ;若DC=1 則程序跳過下一條指令
skpndc 等同于btfsc STATUS, DC ;若DC=0 則程序跳過下一條指令
bc/bnc
bc/bnc 宏指令的作用有點象51 單片機的“jc/jnc”指令。它判別狀態寄存器STATUS 中
的進位標志位,按進位標志實現程序的分支跳轉。如例3-21。
movlw 0x31 ;W=0x31
addwf sum,f ;sum = sum+W
bc Carry1 ;如果發生進位就跳轉到Carry1 處執行
nop ;如果沒有進位則繼續執行bc 的下一條指令
;...
Carry1 nop
bc XXX ;如果C=1 就跳轉到標號XXX,否則程序執行bc 的下一條
等同于
btfsc STATUS,C
goto XXX
bnc YYY ;如果C=0 就跳轉到標號YYY,否則程序執行bnc 的下一條
等同于
btfss STATUS,C
goto YYY
例3-21
請不要被bc/bnc 這樣“一條”指令所迷惑,它實際上是由兩條匯編指令組成,且用到
了“goto”實現跳轉,故在用此宏指令前注意頁面的設定。
bz/bnz
同bc/bnc 一樣,只不過判別的是狀態寄存器STATUS 中的零標志位。
movlw 0x55 ;W=0x55
xorwf flag,w ;flag = 0x55 ?
bz Match ;Z=1, flag=0x55, 跳轉到Match 處執行
nop ;Z=0,繼續執行bz 的下一條指令
;...
Match nop
bz XXX ;如果Z=1 就跳轉到標號XXX,否則程序執行bz 的下一條
等同于
btfsc STATUS,Z
goto XXX
bnz YYY ;如果Z=0 就跳轉到標號YYY,否則程序執行bnz 的下一條
等同于
btfss STATUS,Z
goto YYY
例3-22
bdc/bndc
同上,判別的是狀態寄存器STATUS 中的半字節進位標志位。
bdc XXX ;如果DC=1 就跳轉到標號XXX,否則程序執行bdc 的下一條
等同于
btfsc STATUS,DC
goto XXX
bndc YYY ;如果DC=0 就跳轉到標號YYY,否則程序執行bndc 的下一條
等同于
btfss STATUS,DC
goto YYY
例3-23
3.2.5.2 用戶自定義宏
除了MPASM 內帶的宏指令外,按實際開發的需要和個人的習慣,程序員可以自己定義
任意形式的宏指令。大量使用定義合理的宏指令可以使程序的可讀性大大提高,也更容易移
植。
自己定義宏指令時須遵循一些語法規則。宏指令的定義由“宏指令名“開始,后跟關鍵
詞“macro”,其后可以帶若干宏參數,也可以不跟任何宏參數;然后從下一行起開始寫基本
的匯編指令或已被認可的其它宏指令(宏嵌套);指令可以是任意多行,最后以關鍵詞“endm”
結束整個宏定義,例如:
;定義宏指令實現一個字(兩字節數)加1 的功能
IncWord macro wordVal ;IncWord 是宏指令名, wordVal 是宏參數
;下面為宏的實體(實際的匯編指令)
incf wordVal,f ;對字的低字節加1,如果結果為0 則為字節計數溢出
skpnz ;如果沒有溢出(上面指令結果不為0)就跳過下一條指令
incf wordVal+1,f ;若低字節加1 后溢出,則字的高字節加1
endm ;結束宏指令定義
;程序中對宏指令的引用
cblock 0x20
counter:2 ;定義一個兩字節的字變量
endc
Loop
IncWord counter ;用宏指令實現變量counter 每次循環加1
;編譯器會把這“一條”宏指令展開成原定義的3 條匯編指令
;并用實際的counter 符號替換宏參數wordVal
goto Loop ;跳轉重復循環。注意一條宏指令展開后可能是多條匯編指令
;故此處用“$-?”時要特別小心。
例3-24
使用宏指令時幾個問題需要注意。
㈠宏指令不同于子程序調
用指令。編譯器在編譯你的代碼
時會用原宏定義中的若干條匯
編指令代替程序中的“一條”宏
指令插入到此宏指令位置處(圖
3-15)。若程序中有很多地方用
了同樣的宏指令,那么相同的匯
編指令集也會被復制成同樣多
份,它不能節省代碼長度。而子
程序調用只有一條指令,若一個
子程序在程序中被多處調用,增加的只是調用指令“call”而子程序只有一個,它可以減少
代碼長度。宏指令最有用的是集成少量且非常相關的代碼實現一個特定任務,例如3-24 中
的字變量加1 這樣的功能。你可以安自己的習慣和項目的需要設計這樣的宏指令,甚至可以
建一個宏指令庫頭文件,以后程序開發時直接用#include 包含進你的程序即可使用。
㈡雖然宏指令定義中允許使用語句標號以便給“goto”指令引用,但最好不要這樣做。
因為若此宏指令將被多處使用的話,相同的宏定義會被重復復制,其中的語句標號也會一樣
復制,這就使得程序有“標號重復定義”的語法錯誤而無法成功編譯。所以在宏指令中需要
用goto 指令跳轉時盡量使用“$”配合“+”、“-”運算,例如:“goto $-3”、“goto $+2”等
等;或者使用宏參數給goto 指令一個特定的語句標號。例如3-25 的宏定義:
;定義宏指令實現寄存器和立即數比較大小
;若寄存器值>=立即數則程序跳到某一位置
FL_JGE macro fileReg, litVal, jumpTo
;fileReg 為寄存器, litVal 為立即數, jumpTo 為跳轉的語句標號
movlw litVal &
0xff
;把立即數送給W (確保0x00~0xff)
subwf fileReg, w ;計算寄存器-W
skpnc ;若C=0 即, 寄存器值<W,程序跳過下一行繼續運行
goto jumpTo ;若寄存器值>=W,則跳到指定標號處繼續運行
源程序編譯后的lst 列表文件(局部)
圖3-15
endm ;結束宏指令定義
;程序中對宏指令的引用
val1 equ 0x20 ;定義一個寄存器變量
;使用宏指令
FL_JGE val1, .100, Val1_Over
;變量val1 和立即數100 比較,
;如果val1>=100 則程序跳到Val1_Over 處運行
nop ;若val1<.100,則程序執行這條語句
;... ;其它代碼
Val1_Over nop ;若val1>=.100,則程序將跳到這里繼續運行
例3-25
㈢程序仿真調試時對用戶自定義的宏指令和MPASM 自己提供的內部宏指令支持度不
盡相同。在用戶自定義的宏指令處無法設置斷點,但MPASM 自己提供的內部宏指令沒有此
限制;在程序單步運行時用戶自定義的宏指令無法“一步”執行完畢,調試器會單步跟蹤進
入宏定義體一步一步執行,MPASM 內部宏指令可以“一步”執行完畢。
|