? ?????我們在學習 STM32 的時候幾乎沒有用到過匯編,可能在學習 UCOS 、 FreeRTOS 等 RTOS
類操作系統移植的時候可能會接觸到一點匯編。但是我們在進行嵌入式 Linux 開發的時候是絕
對要掌握基本的 ARM 匯編,因為 Cortex-A 芯片一上電 SP 指針還沒初始化, C 環境還沒準備
好,所以肯定不能運行 C 代碼,必須先用匯編語言設置好 C 環境,比如初始化 DDR 、設置 SP
指針等等,當匯編把 C 環境設置好了以后才可以運行 C 代碼。所以 Cortex-A 一開始肯定是匯
編代碼,其實 STM32 也一樣的,一開始也是匯編,以 STM32F103 為例,啟動文件
startup_stm32f10x_hd.s 就是匯編文件,只是這個文件 ST 已經寫好了,我們根本不用去修改,所
以大部分學習者都沒有深入的去研究。匯編的知識很龐大,本章我們只講解最常用的一些指令,
滿足我們后續學習即可。
一.GNU 匯編語法
如果大家使用過 STM32 的話就會知道 MDK 和 IAR 下的啟動文件 startup_stm32f10x_hd.s
其中的匯編語法是有所不同的,將 MDK 下的匯編文件直接復制到 IAR 下去編譯就會出錯,因
為 MDK 和 IAR 的編譯器不同,因此對于匯編的語法就有一些小區別。我們要編寫的是 ARM
匯編,編譯使用的 GCC 交叉編譯器,所以我們的匯編代碼要符合 GNU 語法。
GNU 匯編語法適用于所有的架構,并不是 ARM 獨享的, GNU 匯編由一系列的語句組成,
每行一條語句,每條語句有三個可選部分,如下:
label : instruction @ comment
label 即標號,表示地址位置,有些指令前面可能會有標號,這樣就可以通過這個標號得到
指令的地址,標號也可以用來表示數據地址。注意 label 后面的“:”,任何以“:”結尾的標識
符都會被識別為一個標號。
instruction 即指令,也就是匯編指令或偽指令。
@ 符號,表示后面的是注釋,就跟 C 語言里面的“ /* ”和“ */ ”一樣,其實在 GNU 匯編文
件中我們也可以使用“ /* ”和“ */ ”來注釋。
comment 就是注釋內容。
比如如下代碼:
add:
MOVS R0, #0X12 @ 設置 R0=0X12
上面代碼中“ add: ”就是標號,“ MOVS R0,#0X12 ”就是指令,最后的“ @ 設置 R0=0X12 ”就是
注釋。
.data 初始化的數據段。
.bss 未初始化的數據段。
.rodata 只讀數據段。
我們當然可以自己使用 .section 來定義一個段,每個段以段名開始,以下一段名或者文件結
尾結束,比如:
.section .testsection @ 定義一個 testsetcion 段
匯編程序的默認入口標號是 _start ,不過我們也可以在鏈接腳本中使用 ENTRY 來指明其它
的入口點,下面的代碼就是使用 _start 作為入口標號:
.global _start
_start:
ldr r0, =0x12 @r0=0x12
上面代碼中 .global 是偽操作,表示 _start 是一個全局標號,類似 C 語言里面的全局變量一
樣,常見的偽操作有:
.byte
定義單字節數據,比如 .byte 0x12 。
.short
定義雙字節數據,比如 .short 0x1234 。
.long
定義一個 4 字節數據,比如 .long 0x12345678 。
.equ
賦值語句,格式為: .equ 變量名,表達式,比如 .equ num, 0x12 ,表示 num=0x12 。
.align 數據字節對齊,比如: .align 4 表示 4 字節對齊。
.end
表示源文件結束。
.global 定義一個全局符號,格式為: .global symbol ,比如: .global _start 。
GNU 匯編還有其它的偽操作,但是最常見的就是上面這些,如果想詳細的了解全部的偽操
作,可以參考《 ARM Cortex-A(armV7) 編程手冊 V4.0.pdf 》的 57 頁。
GNU 匯編同樣也支持函數,函數格式如下:
函數名 :
函數體
返回語句
GNU 匯編函數返回語句不是必須的,如下代碼就是用匯編寫的 Cortex-A7 中斷服務函數:
示例代碼 7.1.1.1 匯編函數定義
/* 未定義中斷 */
Undefined_Handler :
ldr r0 , = Undefined_Handler
bx r0
/* SVC 中斷 */
SVC_Handler :
ldr r0 , = SVC_Handler
bx r0
/* 預取終止中斷 */
PrefAbort_Handler :
ldr r0 , = PrefAbort_Handler
bx r0
上述代碼中定義了三個匯編函數: Undefined_Handler 、 SVC_Handler 和
PrefAbort_Handler 。以函數 Undefined_Handler 為例我們來看一下匯編函數組成,
“ Undefined_Handler ”就是函數名,“ ldr r0, =Undefined_Handler ”是函數體,“ bx r0 ”是函數
返回語句,“ bx ”指令是返回指令,函數返回語句不是必須的。
二.處理器內部數據傳輸指令
三個指令:
1 、 MOV 指令
MOV 指令用于將數據從一個寄存器拷貝到另外一個寄存器,或者將一個立即數傳遞到寄
存器里面,使用示例如下:
MOV R0 , R1
@ 將寄存器 R1 中的數據傳遞給 R0 ,即 R0=R1
MOV R0, #0X12
@ 將立即數 0X12 傳遞給 R0 寄存器,即 R0=0X12
2 、 MRS 指令
MRS 指令用于將特殊寄存器 ( 如 CPSR 和 SPSR) 中的數據傳遞給通用寄存器,要讀取特殊
寄存器的數據只能使用 MRS 指令!使用示例如下:
MRS R0, CPSR
@ 將特殊寄存器 CPSR 里面的數據傳遞給 R0 ,即 R0=CPSR
3 、 MSR 指令
MSR 指令和 MRS 剛好相反, MSR 指令用來將普通寄存器的數據傳遞給特殊寄存器,也就
是寫特殊寄存器,寫特殊寄存器只能使用 MSR ,使用示例如下:
MSR CPSR, R0
@ 將 R0 中的數據復制到 CPSR 中,即 CPSR=R0
三.存儲器訪問指令
1 、 LDR 指令
LDR 主要用于從存儲加載數據到寄存器 Rx 中, LDR 也可以將一個立即數加載到寄存器 Rx
中, LDR 加載立即數的時候要使用“ = ”,而不是“ # ”。在嵌入式開發中, LDR 最常用的就是讀
取 CPU 的寄存器值,比如 I.MX6UL 有個寄存器 GPIO1_GDIR ,其地址為 0X0209C004 ,我們
現在要讀取這個寄存器中的數據,示例代碼如下:
示例代碼 7.2.2.1 LDR 指令使用
1 LDR R0 , = 0X0209C004 @ 將寄存器地址 0X0209C004 加載到 R0 中,即 R0 = 0X0209C004
2 LDR R1 , [ R0 ] @ 讀取地址 0X0209C004 中的數據到 R1 寄存器中
上述代碼就是讀取寄存器 GPIO1_GDIR 中的值,讀取到的寄存器值保存在 R1 寄存器中,
上面代碼中 offset 是 0 ,也就是沒有用到 offset 。
2 、 STR 指令
LDR 是從存儲器讀取數據, STR 就是將數據寫入到存儲器中,同樣以 I.MX6UL 寄存器
GPIO1_GDIR 為例,現在我們要配置寄存器 GPIO1_GDIR 的值為 0X20000002 ,示例代碼如下:
示例代碼 7.2.2.2 STR 指令使用
1 LDR R0 , = 0X0209C004 @ 將寄存器地址 0X0209C004 加載到 R0 中,即 R0 = 0X0209C004
2 LDR R1 , = 0X20000002 @R1 保存要寫入到寄存器的值,即 R1 = 0X20000002
3 STR R1 , [ R0 ] @ 將 R1 中的值寫入到 R0 中所保存的地址中
LDR 和 STR 都是按照字進行讀取和寫入的,也就是操作的 32 位數據,如果要按照字節、
半字進行操作的話可以在指令“ LDR ”后面加上 B 或 H ,比如按字節操作的指令就是 LDRB 和
STRB ,按半字操作的指令就是 LDRH 和 STRH 。
四.?壓棧和出棧指令
????????通常會在 A 函數中調用 B 函數,當 B 函數執行完以后再回到 A 函數繼續執行。要想
再跳回 A 函數以后代碼能夠接著正常運行,那就必須在跳到 B 函數之前將當前處理器狀態保存
起來 ( 就是保存 R0~R15 這些寄存器值 ) ,當 B 函數執行完成以后再用前面保存的寄存器值恢復
R0~R15 即可。保存 R0~R15 寄存器的操作就叫做現場保護,恢復 R0~R15 寄存器的操作就叫做
恢復現場。在進行現場保護的時候需要進行壓棧 ( 入棧 ) 操作,恢復現場就要進行出棧操作。壓棧
的指令為 PUSH ,出棧的指令為 POP , PUSH 和 POP 是一種多存儲和多加載指令,即可以一次
操作多個寄存器數據