1. 程式人生 > >Stay Hungry, Stay Stupid

Stay Hungry, Stay Stupid

基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
基於ARM的嵌入式系統程式開發要點(一)
—— 嵌入式程式開發過程
ARM 系列微處理器作為全球16/32位RISC處理器市場的領先者,在許多領
域內得到了成功的應用.近年來,ARM在國內的應用也得到了飛速的發展,越
來越多的公司和工程師在基於ARM的平臺上面開發自己的產品.
與傳統的4/8位微控制器相比,ARM的效能和處理能力當然是遙遙領先的,但
與之相應,ARM的系統設計複雜度和難度,較之傳統的設計方法也大大提升了.
本文旨在通過討論系統程式設計中的幾個基本方面,來說明基於ARM的嵌入式
系統程式開發的一些特點,並提出和解決了一些常見的問題.
文章分成幾個相對獨立的章節刊載.第一部分討論基於ARM的嵌入式程式
開發和移植過程中的一些基本概念.
1.嵌入式程式開發過程
不同於通用計算機和工作站上的軟體開發工程,一個嵌入式程式的開發過程
具有很多特點和不確定性.其中最重要的一點是軟體跟硬體的緊密耦合特性.
(不帶作業系統支援) (帶作業系統支援)
圖-1:兩類不同的嵌入式系統結構模型
這是兩類簡化的嵌入式系統層次結構圖.由於嵌入式系統的靈活性和多樣
性,上面圖中各個層次之間缺乏統一的標準,幾乎每一個獨立的系統都不一樣.
這樣就給上層的軟體設計人員帶來了極大地困難.第一,在軟體設計過程中過多
地考慮硬體,給開發和除錯都帶來了很多不便;第二,如果所有的軟體工作都需
要在硬體平臺就緒之後進行,自然就延長了整個的系統開發週期.這些都是應該
從方法上加以改進和避免的問題.
為了解決這個問題,工程和設計人員提出了許多對策.首先在應用與驅動(或
API)這一層介面,可以設計成相對統一的一些介面函式,這對於具體的某一個
開發平臺或在某個公司內部,是完全做得到的.這樣一來,就大大提高了應用層
應用(Application)
驅動/板級支援包
(Driver/BSP)
硬體(Hardware)
應用(Application)
硬體抽象層(HAL)
硬體(Hardware)
作業系統(OS)
標準介面函式(API)
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
軟體設計的標準化程度,方便了應用程式在跨平臺之間的複用和移植.
對於驅動/硬體抽象這一層,因為直接驅動硬體,其標準化變得非常困難甚至
不太可能.但是為了簡化程式的除錯和縮短開發週期,我們可以在特定的EDA
工具環境下面進行開發,通過後再進行移植到硬體平臺的工作.這樣既可以保證
程式邏輯設計的正確性,同時使得軟體開發可平行甚至超前於硬體開發程序.
我們把脫離於硬體的嵌入式軟體開發階段稱之為"PC軟體"的開發,可以
用下面的圖來示意一個嵌入式系統程式的開發過程.
"PC軟體"開發 移植,測試 產品釋出
圖-2:嵌入式系統產品的開發過程
在"PC軟體"開發階段,可以用軟體模擬,即指令集模擬的方法,來對用
戶程式進行驗證.在ARM公司的開發工具中,ADS 內嵌的ARMulator和
RealView 開發工具中的ISS,都提供了這項功能.在模擬環境下,使用者可以設
置ARM處理器的型號,時鐘頻率等,同時還可以配置儲存器訪問介面的時序參
數.程式在模擬環境下執行,不但能夠進行程式的執行流程和邏輯測試,還能夠
統計系統執行的時鐘週期數,儲存器訪問週期數,處理器執行時的流水線狀態(有
效週期,等待週期,連續和非連續訪問週期)等資訊.這些寶貴的資訊是在硬體
除錯階段都無法取得的,對於程式的效能評估非常有價值.
為了更加完整和真實地模擬一個目標系統,ARMulator和ISS還提供了一個
開放的API程式設計環境.使用者可以用標準C來描述各種各樣的硬體模組,連同工
具提供的核心模組一起,組成一個完整的"軟"硬體環境.在這個環境下面開發
的軟體,可以更大程度地接近最終的目標.
利用這種先進的EDA工具環境,極大地方便了程式開發人員進行嵌入式開
發的工作.當完成一個"PC軟體"的開發之後,只要進行正確的移植,一個真
正的嵌入式軟體就開發成功了.而移植過程是相對比較容易形成一套規範的流程
的,其中三個最重要的方面是:
考慮硬體對庫函式的支援
移植
移植
開發/實驗/
測試平臺
最終產品
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
符合目標系統上的儲存器資源分佈
應用程式執行環境的初始化
2.開發工具環境裡面的庫函式
如果使用者程式裡呼叫了跟目標相關的一些庫函式,則在應用前需要裁剪這些
函式以適合在目標上允許的要求.主要需要考慮以下三類函式:
訪問靜態資料的函式
訪問目標儲存器的函式
使用semihosting(半主機)機制實現的函式
這裡所指的C庫函式,除了ISO C標準裡面定義的函式以外,還包括由編
譯工具提供的另外一些擴充套件函式和編譯輔助函式.
2.1 裁剪訪問靜態資料的函式
庫函式裡面的靜態資料,基本上都是在標頭檔案裡面加以定義的.比如CTYPE
類庫函式,其返回值都是通過預定義好的CTYPE屬性表來獲得的.比如,想要
改變isalpha() 函式的預設判斷,則需要修改對應CTYPE屬性表裡對字元屬性的
定義.
2.2 裁減訪問目標儲存器的函式
有一類動態記憶體管理函式,如malloc() 等,其本身是獨立於目標系統而執行
的;但是它所使用的儲存器空間需要根據目標來確定.所以malloc() 函式本身
並不需要裁剪或移植,但那些設定動態記憶體區(地址和空間)的函式則是跟目標
系統的儲存器分佈直接相關的,需要進行移植.例如堆疊的初始化函式
__user_initial_stackheap(),是用來設定堆(heap)和棧(stack)地址的函式,顯
然針對每一個具體的目標平臺,該函式都需要根據具體的目標儲存器資源進行正
確移植.
下面是對示例函式__user_initial_stackheap() 進行移植的一個例子:
__value_in_regs struct __initial_stackheap __user_initial_stackheap(
unsigned R0, unsigned SP, unsigned R2, unsigned SL)
{
struct __initial_stackheap config;
config.heap_base = (unsigned int) 0x11110000;
// config.stack_base = SP; // optional
return config;
}
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
請注意上面的函式體並不完全遵循標準C的關鍵字和語法規範,使用了ARM
公司編譯器(ADS 或RealView Compilation tool) 裡的C語言擴充套件特性.關於編譯
器特定的C語言擴充套件,請參考相關的編譯器說明,這裡簡單介紹函式
__user_initial_stackheap() 的功能,它主要是返回堆和棧的基地址.上面的程式中
只對堆(heap) 的基地址進行了設定(設成了0x11110000),也就是說使用者把
0x11110000開始的儲存器地址用作了動態記憶體分配區(heap區).具體地址的確
定是要由使用者根據自己的目標系統和應用情況來確定的,至少要滿足以下條件:
0x11110000開始的地址空間有效且可寫(是RAM)
該儲存器空間不與其它功能區衝突(比如程式碼區,資料區,stack區等)
因為__user_initial_stackheap() 函式的全部執行效果就是返回一些數值,所
以只要符合介面的呼叫標準,直接用匯編來實現看起來更加直觀一些:
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR r0,0x11110000
MOV pc,lr
如果不對這個函式進行移植,編譯過程中將使用預設的設定,這個設定適用
於ARM公司的Integrator系列平臺.
(注意:ARM的編譯/連線工具鏈也提供了繞過庫函式來設定執行時儲存器模型
的方法,請參閱ARM公司其他的相關文件.)
2.3 裁剪使用semihosting(半主機)機制實現的函式
庫函式裡有一大部分函式是涉及到輸入/輸出流裝置的,比如檔案操作函式需
要訪問磁碟I/O,列印函式需要訪問字元輸出裝置等.在嵌入式除錯環境下,所
有的標準C庫函式都是有效且有其預設行為的,很多目標系統硬體不能支援的
操作,都通過除錯工具來完成了.比如printf() 函式,預設的輸出裝置是偵錯程式
裡面的資訊輸出視窗.
但是一個真實的系統是需要脫離除錯工具而獨立執行的,所以在程式的移植
過程當中,需先對這些庫函式的執行機制作一瞭解.
下圖說明了在ADS下面這類C庫函式的結構.
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
圖-3:C庫函式實現過程中的層次呼叫
如圖中例子所示,函式printf() 最終是呼叫了底層的輸入/輸出函式
_sys_write() 來實現輸出操作的,而_sys_write() 使用了除錯工具的內部機制來把
資訊輸出到偵錯程式.
顯然這樣的函式呼叫過程在一個真實的嵌入式系統裡是無法實現的,因為獨
立執行的嵌入式系統將不會有偵錯程式的參與.如果在最終系統中仍然要保留
printf() 函式,而且在系統硬體中具備正確的輸出裝置(如LCD等),則在移植
過程中,需要把printf() 呼叫的輸出裝置進行重新定向.
考察printf() 函式的完整呼叫過程:
圖-4:printf() 的呼叫過程
單純考慮printf() 的輸出重新定向,可以有三種途徑實現:
改寫printf() 本身
改寫 fput()
改寫 _sys_write()
需要注意的是,越底層的函式,被其他上層函式呼叫的可能性越大,改變了
一個底層函式的實現,則所有呼叫該函式的上層函式的行為都被改變了.
以fputc() 的重新實現為例,下面是改變fputc() 輸出裝置到系統序列通訊端
口的例項:
int fputc(int ch, FILE *f)
ANSI C
Input/
output
Error
handling
Stack &
heap setup
Other
Semihosting Support
應用程式呼叫的
函式,如printf()
裝置驅動程式級
使用semihosting
機制
如_sys_write()
由除錯系統執行
printf() fput() _sys_wite()輸出裝置
其他函式 其他函式
C 庫函式
除錯輔助環境
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
{ /* e.g. write a character to an UART */
char tempch = ch;
sendchar(&tempch); // UART driver
return ch;
}
程式碼中的函式sendchar() 假定是系統的串列埠裝置驅動函式.只要新建函式
fput() 的介面符合標準,經過編譯連線後,該函式實現就覆蓋了原來預設的函式
體,所有對該函式的呼叫,其行為都被新實現的函式所重新定向了.
具體哪些庫函式是跟目標相關的,這些函式之間的相互呼叫關係等,請參考
具體的編譯器說明.
3.Semihosting (半主機) 機制
上面提到許多庫函式在除錯環境下的實現都呼叫了一種叫semihosting的機
制.Semihosting具體來講是指一種讓程式碼在ARM 目標上執行,但使用運行了
ARM 偵錯程式的主機上I/O 裝置的方法;也就是讓ARM 目標將輸入/ 輸出請求
從應用程式程式碼傳遞到執行偵錯程式的主機的一種機制.通常這些輸入/輸出裝置
包括鍵盤,螢幕和磁碟I/O.
半主機由一組已定義的SWI 操作來實現.庫函式呼叫相應的SWI(軟體中
斷),然後除錯代理程式處理SWI 異常,並提供所需的與主機之間的通訊.
圖-5:Semihosting的實現過程
多數情況下,半主機SWI 是由庫函式內的程式碼呼叫的.但是應用程式也可
以直接呼叫半主機SWI.半主機SWI 的介面函式是通用的.當半主機操作在硬
件模擬器,指令集模擬器,RealMonitor或Angel下執行時,不需要進行移植處
理.
使用單個SWI 編號請求半主機操作.其它的SWI 編號可供應用程式或操
printf()
printf("Hello world! ");
SWI
偵錯程式
Hello world!
C 庫程式碼
應用程式程式碼
與執行在主機上的偵錯程式通訊
主機螢幕上顯示的文字
目標
主機
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
作系統使用.用於半主機的SWI號是:
在ARM 狀態下:0x123456
在Thumb 狀態下:0xAB
SWI 編號向除錯代理程式指示該SWI 請求是半主機請求.要辨別具體的操
作型別,用暫存器r0 作為引數傳遞.r0 傳遞的可用半主機操作編號分配如下:
0x00-0x31:這些編號由ARM 公司使用,分別對應32個具體的執行函
數.
0x32-0xFF:這些編號由ARM 公司保留,以備將來用作函式擴充套件.
0x100-0x1FF:這些編號保留給使用者應用程式.但是,如果編寫自己的
SWI 操作,建議直接使用SWI指令和SWI編號,而不要使用半主機
SWI 編號加這些操作型別編號的方法.
0x200-0xFFFFFFFF:這些編號未定義.當前未使用並且不推薦使用這
些編號.
半主機SWI使用的軟體中斷編號也可以由使用者自定義,但若是改變了預設
的軟中斷編號,需要:
更改系統中所有程式碼(包括庫程式碼)的半主機SWI 呼叫
重新配置偵錯程式對半主機請求的捕捉與相應
這樣才能使用新的SWI 編號.
有關半主機SWI處理函式實現的更詳細資訊,請參考ARM編譯器的相關
文件.
4.應用環境的初始化和根據目標系統資源進行的移植
在下一期中介紹應用環境和目標系統的初始化.
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
基於ARM的嵌入式系統程式開發要點(二)
—— 系統的初始化過程
基於ARM的晶片多數為複雜的片上系統整合(SoC),這種複雜的系統裡多
數的硬體模組都是可配置的,需要由軟體來設定其需要的工作狀態.因此在使用者
的應用程式啟動之前,需要有專門的一段啟動程式碼來完成對系統的初始化.由於
這類程式碼直接面對處理器核心和硬體控制器進行程式設計,一般都使用匯編語言.系
統啟動程式所執行的操作跟具體的目標系統和開發系統相關,一般通用的內容包
括:
中斷向量表
初始化儲存器系統
初始化堆疊
初始化有特殊要求的埠,裝置
初始化應用程式執行環境
改變處理器模式
呼叫主應用程式
1.中斷向量表
ARM要求中斷向量表必須放置在從0地址開始,連續8×4位元組的空間內
(ARM720T和ARM9/10及以後的ARM處理器也支援從0xFFFF0000開始的高
地址向量表,在本文的其他地方對此不再另加說明).各個中斷向量在向量表中
的位置分配如下圖:
圖1:中斷向量表
每當一箇中斷髮生以後,ARM處理器便強制把PC指標置為向量表中對應中
Reset 復位中斷 0x00
Undef 未定義指令中斷 0x04
Software Interrupt 軟體中斷 0x08
Prefetch Abort 指令預取異常 0x0C
Data Abort 資料異常 0x10
(Reserved) 保留 0x14
IRQ 普通外部中斷 0x18
FIQ 外部快速中斷 0x1C
… …
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
斷型別的地址值.因為每個中斷只佔據向量表中1個字的儲存器空間,只能放置
1條ARM指令,所以通常在向量表中放的是跳轉指令,使程式能從向量表裡跳
轉到儲存器裡的其他地方,再執行中斷處理.
中斷向量表的程式實現通常如下所示:
AREA Boot, CODE, READONLY
ENTRY
B Reset_Handler ; Reset_Handler is a label
B Undef_Handler
B SWI_Handler
B PreAbort_Handler
B DataAbort_Handler
B . ; for reserved interrupt, stop here
B IRQ_Handler
B FIQ_Handler
其中的關鍵字ENTRY是指定編譯器保留這段程式碼,因為編譯器可能會認為
這是一段冗餘程式碼而加以優化.連線的時候要確保這段程式碼被連結在0地址處,
並且作為整個程式的入口點(關鍵字ENTRY並非總是用來設定程式入口點,所
以通常需要在連線選項裡顯式地指定程式入口點).
2.初始化儲存器系統
初始化儲存器系統的程式設計物件是系統的儲存器控制器.儲存器控制器並不是
ARM核心的一部分,不同的系統其設計不盡相同,所以應該針對具體的要求來
完成這部分的程式設計.一般來說,下面這兩個方面是比較通用的.
2.1.儲存器型別和時序配置
一個複雜的系統可能存在多種儲存器型別的介面,需要根據實際的系統設計
對此加以正確配置.對同一種儲存器型別來說,也因為訪問速度的差異,需要不
同的時序設定.
通常Flash 和SRAM同屬於靜態儲存器型別,可以合用同一個儲存器埠;
而DRAM 因為動態重新整理和地址線複用等特性,通常配有專用的儲存器埠.
儲存器埠的介面時序優化是非常重要的,影響到整個系統的效能.因為一
般系統執行的速度瓶頸都存在於儲存器訪問,所以儲存器訪問時序應儘可能地
快;但同時又要考慮由此帶來的穩定性問題.只有根據具體選定的晶片,進行多
次的測試之後,才能確定最佳的時序配置.
2.2.儲存器地址分佈(memory map)
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
有些系統具有非常靈活的儲存器地址分配特性,進行儲存器初始化設計的時
候一定要根據應用程式的具體要求來完成地址分配.
一種典型的情況是啟動ROM的地址重對映(remap).如前面第1節所述,
當一個系統上電後程序將自動從0地址處開始執行,因此在系統的初始狀態,必
須保證在0地址處存在正確的程式碼,即要求0地址開始處的儲存器是非易性的
ROM或Flash等.但是因為ROM或Flash的訪問速度相對較慢,每次中斷髮生
後都要從讀取ROM或Flash上面的向量表開始,影響了中斷響應速度.因此有
的系統便提供一種靈活的地址重對映方法,可以把0地址重新指向到RAM中去.
在這種地址對映的變化過程當中,程式設計師需要仔細考慮的是程式的執行流程不能
被這種變化所打斷.比如下面這種情況:
圖2:啟動ROM的地址重對映對程式執行流程的影響
系統上電後從Flash內的0地址開始執行,啟動程式碼位於地址0x100開始的
空間,當執行到地址0x200時,完成了一次地址的重對映,把原來0開始的地址
空間由Flash轉給了RAM.接下去執行的指令(這裡為了簡化起見,忽略流水
線指令預取的模型)將來自從0x204開始的RAM空間.如果預先沒有對RAM
內容進行正確的設定,則裡面的資料都是隨機的,這樣處理器在執行完0x200
地址處的指令之後,再往下取指執行就會出錯.解決的方法就是要使RAM在使
用之前準備好正確的內容,包括開頭的向量表部分.
有的系統不具備儲存器地址重對映的功能,所有的空間地址就相對簡單一
些,不需要考慮這方面的問題.
3.初始化堆疊
因為ARM處理器有7種執行狀態,每一種狀態的堆疊指標暫存器(SP)都
Flash
0x0100
(Reset_Handler)
B Reset_Handler
… …
.
.
.
(boot code)
.
.
(remap)
0x0000
0x0200
RAM
0x0200
remap 0x0204
Vector Table
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
是獨立的(System和User模式使用相同的SP暫存器).因此對程式中需要用到
的每一種模式都要給SP暫存器定義一個堆疊地址.方法是改變狀態暫存器CPSR
內的狀態位,使處理器切換到不同的狀態,然後給SP賦值.注意不要切換到User
模式進行User模式的堆疊設定,因為進入User模式後就不能再操作CPSR回到
別的模式了.可能會對接下去的程式執行造成影響.
一般堆疊的大小要根據需要而定,但是要儘可能給堆疊分配快速和高頻寬的
儲存器.堆疊效能的提高對系統整體效能的影響是非常明顯的.
這是一段堆疊初始化的程式碼示例,其中只定義了三種模式的SP指標:
MRS R0, CPSR ; CPSR -> R0
BIC R0, R0, #MODEMASK ; 安全起見,遮蔽模式位以外的其它位
ORR R1, R0, #IRQMODE ; 把設定模式位設定成需要的模式
MSR CPSR_cxsf, R1 ; 轉到IRQ模式
LDR SP, =UndefStack ; 設定 SP_irq
ORR R1,R0,#FIQMODE
MSR CPSR_cxsf, R1 ; FIQMode
LDR SP, =FIQStack
ORR R1, R0, #SVCMODE
MSR CPSR_cxsf, R1 ; SVCMode
LDR SP, =SVCStack
注意上面的程式中使用到的3個SP暫存器是不同的物理暫存器:SP_irq,
SP_fiq和SP_svc.引用的幾個標號假設已經正確定義.
4.初始化有特殊要求的埠,裝置
這要由具體的系統和使用者需求而定.一般的外設初始化可以在系統初始化之
後進行.
比較典型的應用是驅動一些簡單的輸出裝置,如LED等,來指示系統啟動
的程序和狀態.
5.初始化應用程式執行環境
一個簡單的可執行程式的映像結構通常如下:
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
圖3:程式映像的結構
映像一開始總是儲存在ROM/Flash裡面的,其RO部分既可以在ROM/Flash
裡面執行,也可以轉移到速度更快的RAM中去;而RW和ZI這兩部分必須是
需要轉移到可寫的RAM裡去的.所謂應用程式執行環境的初始化,就是完成必
要的從ROM到RAM的資料傳輸和內容清零.
不同的工具鏈會提供一些不同的機制和方法幫助使用者完成這一步操作,主要
是跟連結器(Linker)相關.下面是在ARM開發工具環境(ADS或RVCT)下,
一種常用儲存器模型的直接實現:
LDR r0, =|Image$$RO$$Limit ; Get pointer to ROM data
LDR r1, =|Image$$RW$$Base| ; RAM copy address
LDR r3, =|Image$$ZI$$Base| ; Zero init base => top of initialised data
CMP r0, r1 ; Check that they are different
BEQ %F1
0
CMP r1, r3 ; Copy init data
LDRCC r2, [r0], #4 ; ([r0] -> r2) and (r0+4)
STRCC r2, [r1], #4 ; (r2 -> [r1]) and (r1+4)
BCC %B0
1
LDR r1, =|Image$$ZI$$Limit| ; Top of zero init segment
MOV r2, #0
2
CMP r3, r1
STRCC r2, [r3], #4 ; (0 -> [r3]) and (r3+4)
BCC %B2
程式實現了RW資料的拷貝和ZI區域的清零功能.其中引用到的4個符號
是由聯結器(linker)定義輸出的:
|Image$$RO$$Limit|:表示RO區末地址後面的地址,即RW資料來源的起始地址.
|Image$$RW$$Base|:RW區在RAM裡的執行區起始地址,也就是編譯選項
RW_Base指定的地址;程式裡是RW資料拷貝的目標地址.
ZI (Zero initialized R/W Data)
RW (R/W Data)
RO (Code + RO Data)
編譯結果
定義時帶初始值的全域性變數
只定義了變數名的全域性變數
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
|Image$$ZI$$Base|:ZI區在RAM裡面的起始地址.
|Image$$ZI$$Limit|:ZI區在RAM裡面的結束地址後面的一個地址.
程式先把ROM裡 |Image$$RO$$Limit| 開始的RW初始資料拷貝到RAM裡
|Image$$RW$$Base| 開始的地址,當RAM這邊的目標地址到達
|Image$$ZI$$Base| 後就表示RW區的結束和ZI區的開始,接下去就對這片ZI
區進行清零操作,直到遇到結束地址 |Image$$ZI$$Limit|.
6.改變處理器模式
ARM處理器(V4架構以後的版本)一共有7種執行模式:
User: 使用者模式
FIQ: 快速中斷響應模式
IRQ: 一般中斷響應模式
Supervisor:超級模式
Abort: 出錯處理模式
Undef: 未定義模式
System: 系統模式
除使用者模式以外,其他6種模式都是特權模式.因為在初始化過程中許多操
作需要在特權模式下才能進行(比如CPSR的修改),所以要特別注意不能過早
地進入使用者模式.一般地,在初始化過程中會經歷以下一些模式變化:
圖4:處理器模式變換過程
在最後階段才把模式轉換到最終應用程式執行所需的模式,一般是使用者模
式.
核心級的中斷使能(CPSR的I,F位狀態)也可以考慮在這一步進行.如果
系統中另外存在一個專門的中斷控制器(多數情況下是這樣的),這麼做總是安
全的,否則就需要考慮過早地開啟中斷可能帶來的問題,比如在系統初始化完成
之前就觸發了有效中斷,導致系統的宕機.
7.呼叫主應用程式
當所有的系統初始化工作完成之後,就需要把程式流程轉入主應用程式.最
簡單的一種情況是:
復位後的預設模式 注意不要進入使用者模式 使用者選擇
(堆疊初始化階段)
超級模式
(Supervisor)
多種特權模式
變化
設定成使用者程
序執行模式
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
IMPORT main ; get the label main if main() is defined in other files
B man ; jump to main()
直接從啟動程式碼跳入應用程式主函式入口,主函式名字可由使用者自己定義.
在ARM ADS環境中,還另外提供了一套系統級的呼叫機制.
IMPORT __main
B __main
__main()
圖5:在應用程式主函式之前插入__main
__main() 是編譯系統提供的一個函式,負責完成庫函式的初始化和第5節中
所描述的功能,最後自動跳向main() 函式.這種情況下使用者程式的主函式名字
必須得是main.
使用者可以根據需要選擇是否使用__main().如果想讓系統自動完成系統呼叫
(如庫函式)的初始化過程,可以直接使用__main();如果所有的初始化步驟都
是由使用者自己顯式地完成,則可以跳過__main().
當然,使用__main() 的時候,可能會涉及到一些庫函式的移植和重定向問
題.在__main() 裡面的程式執行流程如下圖所示:
圖6:有系統呼叫參與的程式執行流程
關於在__main() 裡面呼叫到的庫函式說明,可以參閱相關的編譯器文件,
庫函式移植和重定向的方法,可以參考上一期文章裡面的相關章節.
Image Entry Point
__main
·copy code and data
·zero initialize
__rt_entry
·initialize library functions
·call top-level constructors
(C++)
·Exit from application
·
Reset handler
·user boot code
User application
·main
__User_initial_stackheap
·set up stack & heap
啟動程式碼 應用程式初始化使用者應用程式main()
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
基於ARM的嵌入式系統程式開發要點(三)
—— 如何滿足嵌入式系統的靈活需求
因為嵌入式應用領域的多樣性,每一個系統都具有各自的特點.在進行系統
程式設計的時候,一定要進行具體分析,充分利用這些特點,揚長避短.
結合ARM架構本身的一些特點,在這裡討論幾個常見的要點.
1.ARM還是Thumb
在討論ARM還是Thumb之前,先說明ARM核心型號和ARM結構體系之
間的區別和聯絡.
圖-1 ARM結構體系和處理器家族的演變發展
如圖-1所示,ARM的結構體系主要從版本4開始,發展到了現在的版本6,
結構體系的變化,對程式設計師而言最直接的影響就是指令集的變化.結構體系的演
變意味著指令集的不斷擴充套件,值得慶幸的是ARM結構體系的發展一直保持了向
上相容,不會造成老版本程式在新結構體系上的不相容.
在圖中的橫座標上,顯示了每一個體繫結構上都含有眾多的處理器型號,這
是在同一體系結構下根據硬體配置和儲存器系統的不同而作的進一步細分.需要
注意的是通常我們用來區分ARM處理器家族的ARM7,ARM9或ARM10,可
能跨越不同的體系結構.
在ARM的體系結構版本4與5中,還可以再細分出幾個小的擴充套件版本:V4T,
V5TE和V5TEJ,其區別如圖-2中所示,這些字尾名也反映在各自擁有的處理器
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
型號上面,可以進行直觀的分辨.V6結構體系因為包含了以前版本的所有特性,
所以不需要再進行分類.
圖-2 結構體系特徵
上面介紹了整個ARM處理器家族的分佈,主要是說明在一個特定的平臺上
編寫程式的時候,一定要先弄清楚目標的特性和一些細微的差別,特別是需要具
體優化特徵的時候.
從ARM體系結構V4T以後,最大的變化是增加了一套16位的指令集——
Thumb.到底在一個具體應用中要否採用Thumb呢 首先我們來分析一下ARM
和Thumb各自的特點和優勢.先看下面一張效能分析圖:
圖-3 ARM和Thumb指令集的比較
圖中的縱座標是測試向量Dhrystone在20MHz頻率下執行1秒鐘的結果,其
值越大表明效能越好;橫座標是系統儲存器系統的資料匯流排寬度.結果表明:
(a) 當系統具有32位的資料匯流排寬度時,ARM比Thumb有更好的效能表現.
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
(b) 當系統的資料匯流排寬度小於32位時,Thumb比ARM的效能更好.
由此可見,並不是32位的ARM指令集效能一定強於16位的Thumb指令集,
要具體情況具體分析.考察箇中的原因,其實不難發現,因為當在一個16位存
儲器系統裡面取1條32位指令的時候,需要耗費2個儲存器訪問週期;比之32
位的系統,其速度正好大概下降一半左右.而16位指令在32位儲存器系統或
16位儲存器系統裡的表現基本相同.正是儲存器造成的系統瓶頸導致了這個有
趣的差別.
除了在窄頻寬系統裡面的效能優勢外,Thumb指令的另外一個好處是程式碼尺
寸.同樣一段C程式碼,用Thumb指令編譯的結果,其長度大約只佔ARM編譯
結果的65%左右,可以明顯地節省儲存器空間.在大多數情況下,緊湊的程式碼和
窄頻寬的儲存器系統,還會帶來功耗上的優勢.
當然,如果在32位的系統上面,並且對系統性能要求很高的情況下,ARM
是一個更好的選擇.畢竟在這種情況下,只有32位的指令集才能完全發揮32
位處理器的優勢來.
因此,選擇ARM還是Thumb,需要從儲存器開銷和效能要求兩方面加以權
衡考慮.
2.堆疊的分配
在圖-3中,橫座標上還有一種情況,就是16位的儲存器寬度,但是堆疊空
間是32位的.這種情況下無論ARM還是Thumb,其效能表現都比單純的16位
儲存器系統情況下要好.這是因為ARM和Thumb其指令集雖然分32位和16
位,但是堆疊全部是採用32位的.因此在16位堆疊和32位堆疊的不同環境下,
其效能當然都會相差很多.這種差別還跟具體的應用程式密切相關,如果一個程
序堆疊的使用頻率相當高,則這種效能差異很大;反之則要小一些.
在基於ARM的系統中,堆疊不僅僅被用來進行諸如函式呼叫,中斷響應等
時候的現場保護,還是程式區域性變數和函式引數傳遞(如果大於4個)的儲存空
間.所以出於系統整體效能考慮,要給堆疊分配相對訪問速度最快,資料寬度最
大的儲存器空間.
一個嵌入式系統通常存在多種多樣的儲存器型別.設計的時候一定要先清楚
每一種儲存器的訪問速度,地址分配和資料線寬度.然後根據不同程式和目標模
塊對儲存器的不同要求進行合理分配,以期達到最佳配置狀態.
3.ROM還是RAM在0地址處
顯然當系統剛啟動的時候,0地址處肯定是某種型別的ROM,裡面儲存了系
統的啟動程式碼.但是很多靈活的系統設計中,0地址處的儲存器型別是可對映的.
也就是說,可以通過軟體的方法,把別的儲存器(主要是快速的RAM)分配以
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
0起始的地址.
這種做法的最主要目的之一是提高系統對中斷的反應速度.因為每一箇中斷
發生的時候,ARM都需要從0地址處的中斷向量表開始其中斷響應流程,顯然
把中斷向量表放在RAM裡,比放在ROM裡有更快的訪問速度.因此,如果系
統提供了這一類的地址重對映功能,軟體設計者一定要加以利用.
下面是一個典型的經過0地址重對映之後的儲存空間分佈圖,注意儘可能把
速度要求最高的部分放置在系統裡面訪問速度最快,頻寬最寬的RAM裡面.
圖-4 系統儲存器分佈的例項
4.儲存器地址重對映(memory remap)
儲存器地址重對映是當前很多先進控制器所具有的功能.在上一節中已經提
到了0地址處儲存器重對映的例子,簡而言之,地址重對映就是可以通過軟體配
置來改變一塊儲存器實體地址的一種機制或方法.
當一段程式對執行自己的儲存器進行重對映的時候,需要特別注意保證程式
執行流程在重對映前後的承接關係.下面是一種典型的儲存器地址重對映情況:
Peripherals
RO
Reset Handler
Heap
RW/ZI
Stack
Exception Handlers
Vector Table
Fast32-bit RAM
16-bit RAM
Flash
0x0000 0000
0x0000 4000
0x0001 0000
0x0001 8000
0x2400 0000
0x2800 0000
0x4000 0000
可以在ROM 裡執行的程式碼
外設暫存器
變數區和動態記憶體分配區
需要快速響應的程式碼和資料
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
圖-5 儲存器重對映舉例1
系統上電後的預設狀態是0地址上放有ROM,這塊ROM有兩個地址:從0
起始和從0x10000起始,裡面儲存了初始化程式碼.當進行地址remap以後,從0
起始的地址被定向到了RAM上,ROM則只保留有唯一的從0x10000起始的地
址了.
如果儲存在ROM 裡的Reset_Handler一直在0 - 0x4000的地址上執行,則
當執行完remap以後,下面的指令將從RAM裡預取,必然會導致程式執行流程
的中斷.根據系統特點,可以用下面的辦法來解決這個問題:
(1) 上電後系統從0地址開始自動執行,設計跳轉指令在remap發生前使PC
指標指向0x10000開始的ROM地址中去,因為不同地址指向的是同一塊
ROM,所以程式能夠順利執行.
(2) 這時候0 - 0x4000的地址空間空閒,不被程式引用,執行remap後把RAM
引進.因為程式一直在0x10000起始的ROM空間裡執行,remap對執行
流程沒有任何影響.
(3) 通過在ROM裡執行的程式,對RAM進行相應的程式碼和資料拷貝,完成
應用程式執行的初始化.
下面是一段實現上述步驟的例程:
-------------------------------------------------------------------------------------------------------
ENTRY
;啟動時,從0開始,設法跳轉到"真"的ROM地址(0x10000開始的空間裡)
LDR pc, =start
;insert vector table here

Start ;Begin of Reset_Handler
; 進行remap設定
remap
0x10000
0x4000
=
0x4000
0x0000
Reset Handler
Vectors
0x4000
0x0000
RAMROM
0x10000
0x10400
ROM ROM
0x10400
Vectors
Reset Handler
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
LDR r1, =Ctrl_reg ;假定控制remap的暫存器
LDR r0, [r1]
ORR r0, r0, #Remap_bit ;假定對控制暫存器進行remap設定
STR r0, [r1]
;接下去可以進行從ROM到RAM的程式碼和資料拷貝
-------------------------------------------------------------------------------------------------------
除此之外,還有另外一種常見的remap方式,如下圖:
圖-6 儲存器重對映舉例2
原來RAM和ROM各有自己的地址,進行重對映以後RAM和ROM的地址
都發生了變化,這種情況下,可以採用以下的方案:
(1) 上電後,從0地址的ROM開始往下執行.
(2) 根據對映前的地址,對RAM進行必要的程式碼和資料拷貝.
(3) 拷貝完成後,進行remap操作.
(4) 因為RAM在remap前準備好了內容,使得PC指標能繼續在RAM裡取
到正確的指令.
不同的系統可能會有多種靈活的remap方案,根據上面提到的兩個例子,可
以總結出最根本的考慮是:要使程式指標在remap以後能繼續往下得到正確的指
令.
5. 根據目標儲存器系統分散載入映像(scatterloading)
Scatterloading檔案是ARM的工具鏈裡面的一個特性,作為程式編譯過程中
給聯結器使用的一個引數,用來指定最終生成的目標映像檔案執行時的分佈狀
態.如果使用者程式映像只是如圖7所示的最簡狀態,所有的可執行程式碼都集合放
置在一起,那麼可以不使用Scatterloading檔案,直接用聯結器的命令列選項就
remap
0x20000
0x4000
=
0x4000
0x0000
Reset Handler
Vectors
0x4000
0x0000
RAMROM
0x10000
0x10400
RAM ROM
0x20400
Vectors
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
能夠完成設定:
RO = 0x00000:表示映像的第一條指令開始地址;
RW = 0x10000:表示變數區的起始地址,變數區一定要位於RAM區.
圖-7 簡單的映像分佈舉例
但是一個複雜的系統可能會把映像分割成幾個部分.如圖8,系統中存在多
種類型的儲存器,不能的程式碼部分根據執行效能優化的考慮分佈與不同的地方.
圖-8 複雜的映像分佈舉例
這時候不能通過簡單的RO,RW引數來完成實現上述配置,就要用到
scatterloading檔案了.在scatterloading檔案裡,可以給編譯出來的各個目標模組
RO
RW
ZI
Stack
Heap
RAM
Flash 程式碼區
變數區
0x00000
0x10000
Exception Handler
RO
Reset Handler
Heap
RW & ZI
Stack
Vector table
0x0000
0x4000
0x10000
0x18000
0x20000
0x28000
32-bit fast RAM
16-bit RAM
Flash
效能要求最苛刻的部分
變數區和動態記憶體分配區
普通程式區
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
指定執行地址,下面的例子是針對圖8的.
FLASH 0x20000 0x8000
{
FLASH 0x20000 0x8000
{
init.o (Init, +First)
* (+RO)
}
32bitRAM 0x0000
{
vectors.o (Vect, +First)
handlers.o (+RO)
}
STACK 0x1000 UNINIT
{
stackheap.o (stack)
}
:
:
16bitRAM 0x10000
{
* (+RW,+ZI)
}
HEAP 0x15000 UNINIT
{
stackheap.o (heap)
}
}
關於scatterloading檔案的詳細語法,請參閱ARM公司的相關手冊.
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
基於ARM的嵌入式系統程式開發要點(四)
—— 異常處理機制的設計
異常或中斷是使用者程式中最基本的一種執行流程或形態,這部分對ARM架
構下異常處理程式的編寫作一個全面的介紹.
ARM一共有7種類型的異常,按優先順序從高到低排列如下:
Reset
Data Abort
FIQ
IRQ
Prefetch Abort
SWI
Undefined instruction
請注意在ARM的文件中,使用術語exception 來描述異常.Exception主要
是從處理器被動接受異常的角度出發描述,而interrupt帶有向處理器主動申請的
色彩.在本文中,對"異常"和"中斷"不作嚴格區分,都是指請求處理器打斷
正常的程式執行流程,進入特定程式迴圈的一種機制.
1.異常響應流程
如以前介紹異常向量表時所提到過的,每一個異常發生時,總是從異常向量
表開始起跳的,最簡單的一種情況是:
圖-1 異常向量表
B
B
(Reserved)
B B
B
B
B
B
0x1C
0x18
0x14
0x10
0x0C
0x08
0x04
0x00
FIQ_Handler()
IRQ_Handler()
DataAbt_Handler()
PreAbt_Handler()
SWI_Handler()
Undef_Handler()
Reset_Handler()
中斷處理函式
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
向量表裡面的每一條指令直接跳向對應的異常處理函式.其中FIQ_Handler()
可以直接從地址0x1C處開始,省下一條跳轉指令.
但是當執行跳轉的時候有2個問題需要討論:跳轉範圍和異常分支.
1.1 跳轉範圍
我們知道ARM的跳轉指令(B)是有範圍限制的(±32MB),但很多情況
下不能保證所有的異常處理函式都定位在向量表的32MB範圍內,需要大於
32MB的長跳轉,而且因為向量表空間的限制只能由一條指令完成.這可以通過
下面二種方法實現.
(a) MOV PC, #imme_value
把目標地址直接賦給PC暫存器.
但是這條指令受格式限制並不能處理任意立即數,只有當這個立即數能夠
表示為一個8-bit數值通過迴圈右移偶數位而得到,才是合法的.例如:
MOV PC, #0x30000000 是合法的,因為0x300000000可以通過0x03循
環右移4位而得到.
而 MOV PC, #30003000 就是非法指令.
(b) LDR PC, [PC+offset]
把目標地址先儲存在某一個合適的地址空間,然後把這個儲存器單元上的32
位資料傳送給PC來實現跳轉.
這種方法對目標地址值沒有要求,可以是任意有效地址.但是儲存目標地址
的儲存器單元必須在當前指令的±4KB空間範圍內.
注意在計算指令中引用的offset數值的時候,要考慮處理器流水線中指令預
取對PC值的影響,以圖-2的情況為例:
offset = address location - vector address - pipeline effect
= 0xFFC - 0x4 - 0x8
= 0xFF0
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
圖-2 利Literal pool實現跳轉
1.2 異常分支
ARM核心只有二個外部中斷輸入訊號nFIQ和nIRQ,但對於一個系統來說,
中斷源可能多達幾十個.為此,在系統整合的時候,一般都會有一個異常控制器
來處理異常訊號.
圖-3 中斷系統
這時候,使用者程式可能存在多個IRQ/FIQ的中斷處理函式,為了從向量表
開始的跳轉最終能找到正確的處理函式入口,需要設計一套處理機制和方法.
圖-4 中斷分支
LDR PC, [PC, 0xFF0]
0x30003000
Undef_Handler()
0x00
0x04
0xFFC
32MB
0x30003000
n
1
2 多
中斷源
中斷
控制器
ARM
核心
nIRQ
nFIQ
外設通訊
配置/獲取資訊
IRQ 0x14
IRQ_Handler_1()
IRQ_Handler_2()
...
...
IRQ_Handler_n()

基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
(a) 硬體處理
有的系統在ARM的異常向量表之外,又增加了一張由中斷控制器控制的特
殊向量表.當由外設觸發一箇中斷以後,PC能夠自動跳到這張特殊向量表中去,
特殊向量表中的每個向量空間對應一個具體的中斷源.
舉例來說,下面的系統一共有20個外設中斷源,特殊向量表被直接放置在
普通向量表後面.
圖-5 額外的硬體異常向量表
當某個外部中斷觸發之後,首先觸發ARM的核心異常,中斷控制器檢測到
ARM的這種狀態變化,再通過識別具體的中斷源,使PC自動跳轉到特殊向量
表中的對應地址,從而開始一次異常響應.需要檢查具體的晶片說明,是否支援
這類特性.
(b) 軟體處理
多數情況下是用軟體來處理異常分支.因為軟體可以通過讀取中斷控制器來
獲得中斷源的詳細資訊.
圖-6 軟體控制中斷分支
Int_20
.
.
.
Int_2
Int_1
FIQ
IRQ
.
.
Reset
0x70
0x6C
.
.
.
0x24
0x20
0x1C
0x18
.
.
0x00
Int_20_Handler()



Int_2_Handler()
Int_1_Handler()
(獲取狀態資訊)
IRQ
… …

中斷控制器
IRQ_Handler:
Switch(int_source)
{
case 1:
case 2:

}
Int_1_Handler()
Int_2_Handler()
… …
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
因為軟體設計的靈活性,使用者可以設計出比上圖更好的流程控制方法來.下
面是一個例子:
圖-7 靈活的軟體分支設計
Int_vector_table是使用者自己開闢的一塊儲存器空間,裡面按次序存放異常處
理函式的地址.IRQ_Handler()從中斷控制器獲取中斷源資訊,然後再從
Int_verctor_table中的對應地址單元得到異常處理函式的入口地址,完成一次異
常響應的跳轉.這種方法的好處是使用者程式在執行過程中,能夠很方便地動態改
變異常服務內容.
2.異常處理函式的設計
2.1 異常發生時處理器的動作
當任何一個異常發生並得到響應時,ARM核心自動完成以下動作:
拷貝 CPSR 到 SPSR_
Address of Int_n_Handler()
.
.
.
Address of Int_2_Handler()
Address of Int_1_Handler()
(獲取狀態資訊)
IRQ
… …

中斷控制器
IRQ_Handler():
Switch(int_source)
{
case 1:
case 2:

case n:
}
Int_1_Handler()
Int_2_Handler()
… …
Int_vector_table
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
設定適當的 CPSR 位:
改變處理器狀態進入 ARM 狀態
改變處理器模式進入相應的異常模式
設定中斷禁止位禁止相應中斷
更新 LR_
設定 PC 到相應的異常向量
注意當響應異常後,不管異常發生在ARM還是Thumb狀態下,處理器都將
自動進入ARM狀態.另一個需要注意的地方是中斷使能被自動關閉,也就是說
預設情況下中斷是不可重入的.單純的把中斷使能位開啟接受重入的中斷會帶來
新的問題,在第3部分中對此會有詳細介紹.
除這些自動完成的動作之外,如果在彙編級進行手動程式設計,還需要注意儲存
必要的通用暫存器.
2.2 進入異常處理迴圈後軟體的任務
進入異常處理程式以後,使用者可以完全按照自己的意願來進行程式設計,包
括呼叫Thumb狀態的函式,等等.但是對於絕大多數的系統來說,有一個步驟
必須處理,就是要把中斷控制器中對應的中斷狀態標識清掉,表明該中斷請求已
經得到響應.否則等退出中斷函式以後,又馬上會被再一次觸發,從而進入周而
復始的死迴圈.
2.3 異常的返回
當一個異常處理返回時,一共有3件事情需要處理:通用暫存器的恢復,狀
態暫存器的恢復以及PC指標的恢復.
通用暫存器的恢復採用一般的堆疊操作指令,而PC和CPSR的恢復可以通
過一條指令來實現,下面是3個例子:
MOVS pc, lr 或 SUBS pc, lr, #4 或LDMFD sp!, {pc}^
這幾條指令都是普通的資料處理指令,特殊之處就是把PC暫存器作為了目
標暫存器,並且帶了特殊的字尾"S"或"^",在特權模式下,"S"或"^"的作
用就是使指令在執行時,同時完成從SPSR到CPSR的拷貝,達到恢復狀態寄存
器的目的.
異常返回時另一個非常重要的問題是返回地址的確定.在2.1節中提到進入
異常時處理器會有一個儲存LR的動作,但是該儲存值並不一定是正確中斷的返
回地址.下面以一個簡單的指令執行流水狀態圖來對此加以說明.
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
圖-8 ARM狀態下3級指令流水線執行示例
我們知道在ARM架構裡,PC值指向當前執行指令的地址加8處.也就是說,
當執行指令A(地址0x8000)時,PC等於指令C的地址(0x8008).假如指令
A是"BL"指令,則當執行時,會把PC(=0x8008)儲存到LR暫存器裡面,但
是接下去處理器會馬上對LR進行一個自動的調整動作:LR=LR-0x4.這樣,最
終儲存在LR裡面的是B指令的地址,所以當從BL返回時,LR裡面正好是正
確的返回地址.
同樣的調整機制在所有LR自動儲存操作中都存在,比如進入中斷響應時處
理器所做的LR儲存中,也進行了一次自動調整,並且調整動作都是LR=LR-0x4.
由此我們來對不同異常型別的返回地址進行依次比較:
假設在指令B處(地址0x8004)發生了中斷響應,進入中斷響應後LR上經
過調整儲存的地址值應該是C的地址0x8008.
(a) 如果發生的是軟體中斷,即B是"SWI"指令
從SWI中斷返回後下一條執行指令就是C,正好是LR暫存器儲存的地址,
所以只要直接把LR恢復給PC.
(b) 如果發生的是"IRQ"或"FIQ"等指令
因為外部中斷請求中斷了B指令的執行,當中斷返回後,需要重新回到B
指令的執行,也就是返回地址應該是B(0x8004),需要把LR減4.
(c) 如果發生的是"Data Abort"
在B上進入資料異常的響應,但導致資料異常的原因卻應該是上一條指令A.
當中斷處理程式修復資料異常以後,要回到A上重新執行導致資料異常的指令,
因此返回地址應該是LR減8.
如果原來的指令執行狀態是Thumb,異常返回地址的分析與此類似,對LR
的調整正好與ARM狀態完全一致.
2.4 ARM編譯器對異常處理函式編寫的擴充套件
F D E
F D E
F D E
F D E
0x8000 A
0x8004 B
0x8008 C
0x800C D
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
考慮到異常處理函式在現場保護和返回地址的處理上與普通函式的不同之
處,不能直接把普通函式體連線到異常向量表上,需要在上面加一層封裝,下面
是一個例子:
IRQ_Handler ;中斷響應,從向量表直接跳來
STMFD SP!, {R0-R12, LR} ;保護現場,一般只需保護{r0-r3,lr}即可
BL IrqHandler ;進入普通處理函式,C或彙編均可
LDMFD SP!, {R0-R12, LR} ;恢復現場
SUBS PC, LR, #4 ;中斷返回,注意返回地址
為了方便使用高階語言直接編寫異常處理函式,ARM編譯器對此作了特定
的擴充套件,可以使用函式宣告關鍵字__irq,這樣編譯出來的函式就滿足異常響應
對現場保護和恢復的需要,並且自動加入對LR進行減4的處理,符合IRQ和
FIQ中斷處理的要求.
__irq void IRQ_Handler (void)
{…}
2.5 軟體中斷處理
軟體中斷由專門的軟中斷指令SWI觸發,SWI指令後面跟一箇中斷編號,
以標識可能共存的多個軟體中斷程式.
圖-9 軟體中斷處理流程
在C程式中呼叫軟體中斷需要用到編譯器的擴充套件功能,使用關鍵字"__swi"
來宣告中斷函式.注意軟中斷號碼同時在函式定義時指定.
__swi(0x24) void my_swi (void);
這樣當呼叫函式my_swi的時候,就會用"SWI 0x24"來代替普通的函式調
用"BL my_swi".
分析圖9的流程,可以發現軟體中斷同樣存在著中斷分支的問題,即需要根
據中斷號碼來決定呼叫不同的處理程式.軟中斷號碼只存在於SWI指令碼當中,
因此需要在中斷處理程式中讀取觸發中斷的指令程式碼,然後提取中斷號資訊,再


SWI 0x01


使用者程式(C或彙編)

CMP swi_num
BEQ …
(Optional)
異常向量表 SWI處理程式(彙編)
SWI處理程式(C)
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
進行進一步處理.下面是軟中斷指令的編碼格式:
ARM狀態下的SWI指令編碼格式,32位長度,其中低24位是中斷編號.
Thumb狀態下的SWI指令編碼格式,16位長度,其中低8位是中斷編號.
圖-10 SWI指令編碼格式
為了在中斷處理程式裡面得到SWI 指令的地址,可以利用LR暫存器.每
當響應一次SWI的時候,處理器都會自動儲存並調整LR暫存器,使裡面的內
容指向SWI下一條指令的地址,所以把LR裡面的地址內容上溯一條指令就是
所需的SWI指令地址.需要注意的一點是當SWI指令的執行狀態不同時,其指
令地址間隔不一樣,如果進入SWI執行前是在ARM狀態下,需要通過LR-4來
獲得SWI指令地址,如果是在Thumb狀態下進入,則只要LR-2就可以了.
下面是一段提取SWI中斷號碼的例程:
MRS R0, SPSR ;檢查進入SWI響應前的狀態
TST R0, #T_bit ;是ARM還是Thumb #T_bit=0x20
LDRNEH R0, [LR, #-2] ;是Thumb,讀回SWI指令碼
BICNE R0, R0, #0xff00 ;提取低8位
LDREQ R0, [LR, #-4] ;是ARM,讀回SWI指令碼
BICEQ R0, R0, #0xff000000 ;提取低24位
;暫存器R0中的內容是正確的軟中斷編號了
3.可重入中斷設計
如2.1節所述,預設情況下ARM中斷是不可重入的,因為一旦進入異常響
應狀態,ARM自動關閉中斷使能.如果在異常處理過程中簡單地開啟中斷使能
而發生中斷巢狀,顯然新的異常處理將破壞原來的中斷現場而導致出錯.但有時
候中斷的可重入又是需要的,因此要能夠通過程式設計來解決這個問題.其中有
二點是這個問題的關鍵:
(a) 新中斷使能之前必須要保護好前一箇中斷的現場資訊,比如LR_irq和
SPSR_irq等,這一點容易想到也容易做到.
(b) 中斷處理過程中對BL的保護
28 24 27
SWI number
23
15 8 7 0
1 1 0 1 1 1 1 1 SWI number
31
Cond 1 1 1 1
0
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
在中斷處理函式中發生函式呼叫(BL)是很常見的,假設有下面一種情況:
IRQ_Handler:

BL Foo -----------> Foo:
ADD … STMFD SP!, {R0-R3, LR}
… …
LDMFD SP!, {R0-R3, PC}
上述程式,在IRQ處理函式IRQ_Handler() 中呼叫了函式Foo(),這是一個
普通的異常處理函式.但若是在IRQ_Handler() 裡面中斷可重入的話,則可能發
生問題,考察下面的情況:
當新的中斷請求恰好在"BL Foo"指令執行完成後發生.
這時候LR暫存器(因在IRQ模式下,是LR_irq)的值將調整為BL指令的
下一條指令(ADD)地址,以期能從Foo() 正確返回;但是因為這時候發生了
中斷請求,接下去要進行新中斷的響應,處理器為了能使新中斷處理完成後能正
確返回,也將進行LR_irq儲存.因為新中斷是在指令流
BL Foo --> STMFD SP!, {R0-R3, LR}
執行過程中插入的,完成跳轉操作後,進行流水線重新整理,最後LR_irq儲存的是
STMFD後面一條指令的地址;這樣當新中斷利用(PC = LR - 4)操作返回時,
正好可以繼續原來的流程執行STMFD指令.這二次對LR_irq的操作發生了衝
突,當新中斷返回後往下執行STMFD指令,這時候壓棧的LR已不是原來需要
的ADD指令的地址,從而使子程式Foo() 無法正確返回.
這個問題無法通過增加額外的現場保護指令來解決.一個巧妙的辦法是在重
新使能中斷之前改變處理器的模式,也就是使上面程式中的"BL Foo"指令不
要執行在IRQ模式下.這樣當新中斷髮生時就不會造成LR暫存器的衝突了.考
慮ARM的所有執行模式,採用System模式是最恰當的,因為它既是特權模式,
又與中斷響應無關.
所以一個完整的可重入中斷應該遵循以下流程:
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
圖-11 可重入中斷處理流程
下面是一段實現的例程:
保護暫存器:LR,SPSR等
與中斷控制器通訊(需要的話)
切換到System狀態,開中斷使能
中斷處理(現在中斷可重入)
關閉中斷使能,切換回IRQ狀態
恢復暫存器:PC,CPSR等
進入普通不可重入中斷處理
結束一次可重入中斷處理
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
基於ARM的嵌入式系統程式開發要點(五)
—— ARM/Thumb的互動工作
在前面的文章中提到過,很多情況下應用程式需要在ARM跟Thumb狀態之
間相互切換,這部分就討論互動工作的實現方法和一些注意問題.
1. 需要互動的原因
前面提到過Thumb指令在某些特殊情況下具有比ARM指令更為出色的表
現,主要是在程式碼長度和窄頻寬儲存器系統性能兩方面.正因為Thumb指令在
特定環境下面的優勢,它在很多方面得到了廣泛的應用.但是因為下面一些原因,
Thumb又不可能獨立地組成一個應用系統,所以不可避免地會產生ARM與
Thumb之間互動的問題.
Thumb指令集在功能上只是ARM指令集的一個子集,某些功能只能在
ARM狀態下執行,如CPSR和協處理器的訪問.
進行異常響應時,處理器會自動進入ARM狀態.
從系統優化考慮,在寬頻儲存器上不應該放置Thumb程式碼,很多窄帶
系統具有寬頻的內部儲存器.
即使是一個單純的Thumb應用系統,也必須加一個彙編的互動頭程式,
因為系統總是自動從ARM開始啟動.
2. 狀態切換的實現
處理器在ARM/Thumb之間的狀態切換是通過一條專用的跳轉交換指令BX
來實現的.BX指令以通用暫存器(R0-R15)為運算元,通過拷貝Rn到PC來
實現4GB空間範圍內的一個絕對跳轉. BX利用Rn暫存器中儲存的目標地址值
的最後一位來判斷跳轉後的狀態.
圖-1 BX指令實現狀態切換
0 31
Rn
PC
BX
ARM/Thumb選擇位:
0 - ARM
1 - Thumb
BX Rn
當前狀態是Thumb時
BX{Cond.} Rn
當前狀態是ARM時
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
無論ARM還是Thumb,其指令儲存在儲存器中都是邊界對齊的(4-Byte或
2-Byte對齊),所以在執行跳轉過程中,PC暫存器中的最低位肯定被捨棄,不起
作用.在BX指令的執行過程中,最低位正好被用作狀態判斷的標識,不會造成
儲存器訪問不對齊的錯誤.
圖2中是一段直接進行狀態切換的例程:
圖-2 ARM/Thumb互動工作的例子
我們知道ARM的狀態暫存器CPSR中,bit-5是狀態控制位T-bit,決定當前
處理器的執行狀態.如果直接修改CPSR的狀態位,也能夠達到改變處理器執行
狀態的目的,但是會帶來一個問題.因為ARM採用了多級流水線的結構,所以
在程式執行過程中指令流水線上會存在幾條預取指令(具體數目視流水線級數而
不同).當修改CPSR的T-bit以後,狀態的轉變會造成流水線上預取指令執行的
錯誤.而如果用BX指令,則執行後會進行流水線重新整理動作,清除流水線上的殘
餘指令,在新的狀態下重新開始指令預取,從而保證狀態轉變時候指令流的正確
銜接.
3. ARM/Thumb之間的函式呼叫
在無互動的子程式呼叫中,其過程比較簡單.實現呼叫通常只需要一條指
令:
BL function
實現返回也只需要從LR恢復PC即可:
MOV PC, LR
;從ARM狀態開始
CODE32 ;彙編關鍵字
ADR R0, Into_Thumb+1 ;得到目標地址,末位置1,轉向Thumb
BX R0 ;執行
… ;其他程式碼
CODE16 ;彙編關鍵字
Into_Thumb ;Thumb程式碼段起始地址
… ;Thumb程式碼
ADR R5, Back_to_ARM ;得到目標地址,末位預設為0,轉向ARM
BX R5 ;執行
… ;其他程式碼
CODE32 ;彙編關鍵字
Back_to_ARM ;ARM程式碼段起始地址

基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
如下圖所示:
圖-3 普通函式呼叫
如果子函式和父函式不是在同一種狀態下執行的,因為狀態切換,需要對
函式呼叫作更多的考慮.
(a) BL不能完成狀態切換,需要由BX來切換狀態.
(b) BX不能自動儲存返回地址到LR,需要在BX之前先儲存好LR.
(c) 用"BX LR"來返回,不能使用"MOV PC, LR",因為這條指令同
樣不能實現狀態切換.返回時要仔細考慮儲存的LR中最低位內容是否
正確.
假如使用者直接使用匯編進行狀態互動跳轉,上述的幾個問題都需要用手工
編碼加以處理.如果使用者使用高階語言進行開發,不需要為ARM/Thumb之間的
相互呼叫增加額外的編碼,但是最好要對其呼叫過程加以瞭解.下面以ARM ADS
中的編譯工具為例進行說明(圖4).
(a) 兩個函式func1()和func2()被編譯成了不同的指令集(ARM或Thumb).
注意func1()和func2()在這裡位於二個不同的原始檔.
(b) 編譯時必須告訴編譯器和聯結器足夠的資訊,一方面讓編譯器能夠使用
正確的指令碼進行編譯,另一方面這樣當在不同的狀態之間發生函式調
用時,聯結器將插入一段連線程式碼(veneers)來實現狀態轉換.
圖-4 不同狀態間函式呼叫的示例
func1
聯結器生成
連線程式碼
File2.c File1.c
Void func1(void)
{

func2();

}
.
.
.
BL
.
.
.
.
.
BX
func2
. .
.
BX
Void func2(void)
{


}
func2
func1
Void func1(void)
{

func2();

}
.
.
.
BL func2
.
.
.
.
.
MOV PC, LR
基於ARM的嵌入式程式開發要點
ARM-CHINA-040415A
上述過程中的一個特點是func1還是使用通常的BL指令來進行子程式呼叫,
而func2返回時則直接使用"BX LR",沒有對LR進行判斷和最低位的設定.
這是因為當執行BL指令對LR進行儲存時,其最低位會被自動設定,以滿足返
回時狀態切扼/td>