與傳統的4/8位單片機相比,ARM的性能和處理能力當然是遙遙領先的,但與之相應,ARM的系統設計復雜度和難度,較之傳統的設計方法也大大提升了。本文旨在通過討論系統程序設計中的幾個基本方面,來說明基于ARM的嵌入式系統程序開發的一些特點,并提出和解決了一些常見的問題。 本文分成幾個相對獨立的專題陸續刊載。 (一) 嵌入式程序開發基本概念 (二) 系統的初始化過程 (三) 如何滿足嵌入式系統的靈活需求 (四) 異常處理機制的設計 (五) ARM/Thumb的交互工作 (六) 開發高效程序的技巧 1 嵌入式程序開發過程 不同于通用計算機和工作站上的軟件開發工程,一個嵌入式程序的開發過程具有很多特點和不確定性。其中最重要的一點是軟件跟硬件的緊密耦合特性。 圖1是兩類簡化的嵌入式系統層次結構圖。由于嵌入式系統的靈活性和多樣性,圖1中各個層次之間缺乏統一的標準,幾乎每一個獨立的系統都不一樣。這樣就給上層的軟件設計人員帶來了極大地困難。第一,在軟件設計過程中過多地考慮硬件,給開發和調試都帶來了很多不便;第二,如果所有的軟件工作都需要在硬件平臺就緒之后進行,自然就延長了整個的系統開發周期。這些都是應該從方法上加以改進和避免的問題。 ![]() 圖1 兩類不同的嵌入式系統結構模型 為了解決這個問題,工程和設計人員提出了許多對策。首先在應用與驅動(或API)這一層接口,可以設計成相對統一的一些接口函數,這對于具體的某一個開發平臺或在某個公司內部,是完全做得到的。這樣一來,就大大提高了應用層軟件設計的標準化程度,方便了應用程序在跨平臺之間的復用和移植。 對于驅動/硬件抽象這一層,因為直接驅動硬件,其標準化變得非常困難甚至不太可能。但是為了簡化程序的調試和縮短開發周期,我們可以在特定的EDA工具環境下面進行開發,通過后再移植到硬件平臺上。這樣既可以保證程序邏輯設計的正確性,同時使得軟件開發可行甚至超前于硬件開發進程。 我們把脫離于硬件的嵌入式軟件開發階段稱之為“PC軟件”的開發,可以用圖2來示意一個嵌入式系統程序的開發過程。 ![]() 圖2 嵌入式系統程序的開發過程 在“PC軟件”開發階段,可以用軟件仿真,即指令集模擬的方法,來對用戶程序進行驗證。在ARM公司的開發工具中,ADS內嵌的ARMulator和 RealView開發工具中的ISS,都提供了這項功能。在模擬環境下,用戶可以設置ARM處理器的型號、時鐘頻率等,同時還可以配置存儲器訪問接口的時序參數。程序在模擬環境下運行,不但能夠進行程序的運行流程和邏輯測試,還能夠統計系統運行的時鐘周期數、存儲器訪問周期數、處理器運行時的流水線狀態(有效周期、等待周期、連續和非連續訪問周期)等信息。這些寶貴的信息是在硬件調試階段都無法取得的,對于程序的性能評估非常有價值。 為了更加完整和真實地模擬一個目標系統,ARMulator和ISS還提供了一個開放的API編程環境。用戶可以用標準C來描述各種各樣的硬件模塊,連同工具提供的內核模塊一起,組成一個完整的“軟”硬件環境。在這個環境下面開發的軟件,可以更大程度地接近最終的目標。 利用這種先進的EDA工具環境,極大地方便了程序開發人員進行嵌入式開發的工作。當完成一個“PC軟件”的開發之后,只要進行正確的移植,一個真正的嵌入式軟件就開發成功了。而移植過程是相對比較容易形成一套規范的流程的,其中三個最重要的方面是: ◆ 考慮硬件對庫函數的支持 ◆ 符合目標系統上的存儲器資源分布 ◆ 應用程序運行環境的初始化 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; } 請注意上面的函數體并不完全遵循標準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系列平臺。 2.3 裁減使用半主機機制實現的函數 庫函數里有一大部分函數是涉及到輸入/輸出流設備的,比如文件操作函數需要訪問磁盤I/O,打印函數需要訪問字符輸出設備等。在嵌入式調試環境下,所有的標準C庫函數都是有效且有其缺省行為的,很多目標系統硬件不能支持的操作,都通過調試工具來完成了。比如printf() 函數,缺省的輸出設備是調試器里面的信息輸出窗口。 但是一個真實的系統是需要脫離調試工具而獨立運行的,所以在程序的移植過程當中,需先對這些庫函數的運行機制作一了解。 圖3說明了在ADS下面這類C庫函數的結構。 ![]() 圖3 C庫函數實現過程中的層次調用 如圖4中例子所示,函數printf() 最終是調用了底層的輸入/輸出函數_sys_write() 來實現輸出操作的,而_sys_write() 使用了調試工具的內部機制來把信息輸出到調試器。 ![]() 圖4 printf() 的調試過程 顯然這樣的函數調用過程在一個真實的嵌入式系統里是無法實現的,因為獨立運行的嵌入式系統將不會有調試器的參與。如果在最終系統中仍然要保留 printf() 函數,而且在系統硬件中具備正確的輸出設備(如LCD等),則在移植過程中,需要把printf() 調用的輸出設備進行重新定向。 單純考慮printf() 的輸出重新定向,可以有三種途徑實現: ◆ 改寫printf() 本身 ◆ 改寫 fput() ◆ 改寫 _sys_write() 需要注意的是,越底層的函數,被其他上層函數調用的可能性越大,改變了一個底層函數的實現,則所有調用該函數的上層函數的行為都被改變了。 以fputc() 的重新實現為例,下面是改變fputc() 輸出設備到系統串行通信端口的實例: int fputc(int ch, FILE *f) { /* 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 操作來實現,如圖5所示。庫函數調用相應的SWI(軟件中斷),然后調試代理程序處理SWI 異常,并提供所需的與主機之間的通訊。多數情況下,半主機SWI 是由庫函數內的代碼調用的。但是應用程序也可以直接調用半主機SWI。半主機SWI 的接口函數是通用的。當半主機操作在硬件仿真器、指令集仿真器、RealMonitor或Angel下執行時,不需要進行移植處理。 ![]() 圖5 semihosting的實現過程 使用單個SWI 編號請求半主機操作。其它的SWI 編號可供應用程序或操作系統使用。用于半主機的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 編號。 4 應用環境的初始化和根據目標系統資源進行的移植 在下一期中介紹應用環境和目標系統的初始化。(更多關于ARM的詳細技術資料,請訪問:http://www.arm.com/arm/documentation?OpenDocument) 引證文獻 1. 譚云福.程志剛.孫會珺 基于ARM的嵌入式系統的初始化引導程序 [期刊論文] -河北省科學院學報2006(04) 2. 孟凌凌 基于嵌入式系統技術的樁基礎檢測儀的研制 [學位論文] 碩士2006 3. 連迅 基于ARM微處理器的玻璃缺陷在線檢測系統 [學位論文] 碩士2006 4. 楊志強 嵌入式系統設計與發展 [期刊論文] -青海師范大學學報(自然科學版)2005(03) 5. 劉志勇 基于ARM的無線視頻傳輸硬件系統的初步研究與開發 [學位論文] 碩士2005 6. 田勁華 基于USB Host和ARM技術的大容量數據采集系統的研究 [學位論文] 碩士2005 7. 金志強 基于ARM的嵌入式控制系統硬件平臺設計 [學位論文] 碩士2005 8. 王雷 基于ARM微處理器的應用研究 [學位論文] 碩士2005 9. 信俊昌 基于ARM的具有網絡功能的嵌入式平臺的設計與實現 [學位論文] 碩士2004 作 者:ARM中國 費浙平 刊 名:單片機與嵌入式系統應用 2003(8) |