對於組合語言,最大的迷思在於:「有了編譯器,為什麼我還要學組合語言?」,關於學習組合語言的好處我認為可以分成幾點討論。
由於編譯器會產生一些 bootstrap 程式碼,所以光看編譯器編譯後的程式碼,可能會比想像中的數量還多。為了深入底層,你可能還需要知道編譯器對於特定片段的程式碼是如何編譯的。
一旦了解組合語言的概念,其實它並沒有想像中那麼難理解。因為我們是直接撰寫組合語言,所以也不會有編譯器轉換時的隔閡。你也不需要再去學習各種語言特性,組合語言運作的方式不會因為 chip 不同而有變化。
學習組合語言不代表你必須要精通它,或是從此以後只能用組合語言撰寫,回頭看看人類發展高階語言的原因,正是因為組合語言的表達能力有限,繁瑣的操作也會造成開發人員的心智負擔,不過知道組合語言是如何運作的,能夠幫助你在使用高階語言撰寫程式時,對實際上電腦會如何運作有更深的了解。
因此,如果你真的想更了解 AVR 的架構,認識組合語言就是必經的一段過程。
組合語言通常會由三個部分組成:操作、運算數 1(Operand)、運算數 2。舉例來說,在 AVR 當中若要將數字載入至暫存器當中可以這樣寫:
ldi r16, 00000001
在這段程式碼中,ldi
代表操作,在這裡指得是 load immediately,r16 為暫存器,00000001 則為數字。我們通常會將操作稱為指令(instruction),在 AVR 當中大部分的 instruction 都是一個 cycle。
在這邊要特別注意的是,並不是每個指令都能夠像這樣,後面直接接一個常數,而是要先將數字載入到暫存器之後,再繼續接下來的操作。
例如我希望計算 1 + 1 後將結果放入暫存器中,這樣寫是不對的:
add r16, 1, 1
而是需要先將 1 載入到暫存器中,再進行加法:
ldi r14, 1
ldi r15, 1
add r15, r14
在 AVR 當中有許多定址模式,最主要的目的在於簡化在記憶體(RAM)與暫存器之間的資料交換與操作。
一個指令搭配一個暫存器,例如 inc
指令
inc r16
將暫存器 r16 增加 1。在 AVR 組合語言當中,通用暫存器會以 r 當作前綴加上數字表示,assembler 會對照 AVR 的架構將這些暫存器映射到對應的位址(Register File)。在 AVR 當中有 32 個暫存器,所以 r0 ~ r31 都是可使用的暫存器。
可使用兩個暫存器,如 add
指令
add r16, r17
mov r0, r1
可以直接在暫存器與 I/O Port 間傳輸資料,最常見的就是 in
跟 out
指令:
in r16, PINB ; 將 PINB 的資料傳給暫存器 r16
out PORTC, r16 ; 將 r16 的資料傳給 PORTC
在這邊可以發現 PINB 以及 PORTC 之類看起來很像變數的東西,這是由 assembler 事先定義的變數。在 AVR 當中除了通用暫存器(general purpose register)這類可以讓我們直接操作的暫存器之外,還有控制各種功能、參數的暫存器可以使用。
在 AVR 當中暫存器可以透過特定的位址存取,舉例來說 PORTB 這個暫存器就存在於 0x18 這個記憶體位址當中。不過直接撰寫記憶體位址撰寫時比較麻煩,因此 assembler 通常會事先定義好這些暫存器的位址與名稱,在撰寫時就不需要查表寫記憶體位址了。
將資料寫到 data space 當中。在 AVR 當中 data space 包含 Register file、I/O memory、SRAM。例如 sts
指令
sts 0x1000, r16
將 r16 的資料寫入到 data space 的位址 0x1000
。
AVR 採用的是哈佛架構(Harvard Architecture),在哈佛架構當中,程式指令和資料儲存會分別存放在不同的記憶體空間。目前使用哈佛架構的微控制器與中央處理器的晶片有 AVR、ARM9、ARM10、ARM11。
因此我們將資料儲存的地方稱為 data space,儲存指令的地方稱為 program memory space。將程式指令與資料儲存空間分開存放最大的好處在於,當我們在執行指令時,就可以預先讀取下一條指令,進而提高效能。
間接定址。在 AVR 當中有三個比較特別的暫存器稱為 X, Y, Z 暫存器。我們在前面有講到,AVR 具有 32 個通用暫存器,分別從 r0 ~ r31,其中 r26 ~ r31 在特定指令中會具有定址功能,而當這些暫存器當作定址功能使用時就稱為 X、Y、Z 暫存器。
LD r16, Y
這個指令的意思是以 Y 的值當作記憶體位址,找出此記憶體位址儲存的值,再將值放入 r16 暫存器當中。因為不是直接將值放入暫存器中,而是先找記憶體位址再去找值,因此才有 indriect 之稱。從這個指令多少就能感受出指標在組合語言的表達是什麼,其實就是 indirect 的對應。
除此之外 AVR 還有提供執行後增、減的功能。
LD r16, Y+
當執行完這條指令後,將 Y 暫存器的值 +1,這樣在存取連續的記憶體空間時很方便。
ST Y+, r28
這條指令則是將暫存器 r28 的內容存入到以 Y 暫存器的值為位址的記憶體。
可以在 X、Y、Z 當中自行加入常數的 offset
LDD r16, Y+0x10
將 Y 的值加上 0x10 後當作記憶體位址去尋找對應的值。
可以使用 Z 暫存器當作記憶體位址存取到 program memory。
LPM
使用 ijump
或是 icall
時可根據 z-register 的值改寫 PC 的位址。
PC 指得是 program counter,在 MCU 當中會不斷從 flash memory 當中獲取下一條指令解碼後執行,為了得知目前執行到哪一條指令,通常會使用 program counter 來儲存,每次執行一條指令時就將 PC+1。PC 可透過 ijump、icall 修改,讓程式跳到指定的位置後執行,進而實現像是迴圈、條件式判斷、函數呼叫等功能。
以 ijump 與 icall 都是直接以記憶體位址改動 PC 值,也可以透過 offset 的方式來改變。
rjmp r16
rcall r16
AVR 的指令集雖然不多,但算下來也有 100 多條,文章裡無法一一介紹,不過我會在接下來介紹到特定功能時,一併介紹對應的指令。