天天看點

使用ADS1.2進行嵌入式軟體開發

概述

      嵌入式應用程式通常都是在樣機環境下調試與開發的,這種環境與最終産品之間并不完全相同。是以,在系統調試階段就考慮應用程式在最終目标硬體中的運作情況是非常重要的。

     本文旨在讨論如何将一個開發/調試環境下的嵌入式應用程式轉移到最終獨立運作的目标系統中去,并提到了ARM ADS1.2開發工具包的一些功能特性及其在這個過程中所起到的作用。

    使用ADS開發嵌入式程式時,需要着重考慮以下幾個問題:

1.與硬體相關的C語言庫函數的使用;

2.某些C語言庫函數使用了調試環境中的資源,要把這些使用的資源重定向到目标系統中的硬體上來;

3.可執行映象檔案的存儲器映射必須根據目标硬體的存儲器分布進行裁剪;

4.在主程式執行前,嵌入式應用程式必須先完成系統的初始化。一個完整的初始化包括使用者的啟動執行代碼和ADS中C庫函數的初始化過程。

使用ADS1.2進行嵌入式軟體開發

圖1 Semihosting的實作舉例

使用ADS1.2進行嵌入式軟體開發

圖2 C語言庫函數結構

使用ADS1.2進行嵌入式軟體開發

圖3 預設的存儲器映射

使用ADS1.2進行嵌入式軟體開發

圖4 連接配接器布局規則

預設的工程項目設定

剛開始一個嵌入式應用軟體開發時,ADS使用者可能并 不完全清楚目标硬體的一些參數名額。比如有關外設、存儲器位址分布,甚至處理器類型等一些細節,可能還沒有最終确定。為了在所有這些細節全部就緒前就能進 行軟體開發,ADS工具有一套程式建構和調試的預設設定。了解這套預設的工程項目設定方法,對于掌握最終的移植步驟非常有好處。

ADS1.2C語言函數庫

Semihosting

在ADS 的C語言函數庫中,某些ANSIC的功能是由主機的調試環境來提供的,這套機制有一個專門術語叫Semihosting。Semihosting通過一組 軟體中斷(SWI)指令來實作。如圖1所示,當一個Semihosting軟中斷被執行時,調試系統先識别這個SWI請求,然後挂起正在運作的程式,調用 Semihosting的服務,完成後再恢複原來的程式執行。是以,主機執行的任務對于程式來說是透明的。

C語言庫函數結構

從概念上來講,C語言庫函數可以被分成兩部分,一是ANSIC語言規範本身的一部分,一是隻受某一特定ANSIC層次支援的函數,如圖2所示。

其 中一些ANSIC的功能是由主機調試環境調用驅動程式級的函數完成的。例如,ADS的庫函數printf()把輸出資訊輸出到調試器的控制台視窗,這個功 能通過調用__sys_write()實作,__sys_write()執行了一個把字元串輸出到主機控制台的Semihosting軟中斷服務程式。

預設的存儲器映射

如果使用者在程式編譯時沒有指定映象的存儲器映射分布,ADS将為生成的目标代碼和資料配置設定一個預設的存儲器映射圖,如圖3所示。

目标印象被連接配接至位址0x8000,存儲和執行區域都位于該位址開始的空間。RO(隻讀)部分放在前面,接着是RW(讀寫)部分,最後是ZI(零初始化)部分。

在ZI部分之上緊跟着HEAP,是以HEAP的确切位址要在連接配接時才能确定。

STACK的基位址是在應用程式啟動時由一個Semihosting操作提供。這項Semihosting操作傳回的位址值視不同調試環境而定:

ARMulator傳回配置檔案peripherals.ami中的設定值;預設為0x08000000。

Multi-ICE傳回的是調試器内部變量$top_of_memory的值;預設為0x00080000。

連接配接器布局規則

連接配接器對代碼和資料在存儲器系統中的配置設定,遵循一套規則,如圖4所示。

映 象首先按照屬性以RO-RW-ZI的次序進行排列,在同一種屬性裡面代碼先于資料。然後連接配接器将輸入段根據名字的字母順序進行排列,輸入段的名字與彙編代 碼裡面的塊名字訓示一緻(在彙程式設計式中用AREA關鍵字)。在輸入段中,來自不同對象的代碼和資料放置次序與在連接配接器指令行中指定的對象檔案次序一緻。

在需要靈活配置設定代碼和資料放置位置的情況下,建議使用者不要簡單地依靠這些規則。後面會介紹一種如何控制代碼和資料布局的機制Scatterloading。

使用ADS1.2進行嵌入式軟體開發

圖5 預設的ADS初始化過程

使用ADS1.2進行嵌入式軟體開發

圖6 C庫函數重定向

使用ADS1.2進行嵌入式軟體開發

圖7 scatter檔案文法

使用ADS1.2進行嵌入式軟體開發

圖8 分散加載的簡單樣例

啟動應用程式

大多數嵌入式系統在進入應用主程式之前有一個初始化的過程,該過程完成系統的啟動和初始化功能。預設的ADS初始化過程如圖5所示。

總體上,初始化過程可以分成兩部分來看:

_main負責設定運作映像存儲器映射;

_rt_entry負責庫函數的初始化。

_main 完成代碼和資料的複制,并把ZI資料區清零。這一步隻有當代碼和資料區在存儲和運作時處于不同的存儲器位置時才有意義。接着_main跳進 _rt_entry,進行STACK和HEAP等的初始化。最後_rt_entry跳進應用程式的入口main()。當應用程式執行完時, _rt_entry又将控制權交還給調試器。

函數main()在ADS中有特殊的意義。當一個程式工程項目中存在main()時,連接配接器會把_main和_rt_entry中的初始化代碼連接配接進來;如果沒有main()函數,初始化過程就不會被連接配接,結果就會導緻一些标準的C庫函數無效。

根據目标環境裁減C庫函數

預設狀态下C庫函數利用Semihotsting機制來實作裝置驅動的功能。但一個真正的嵌入式系統,要使用到具體的外設或硬體獨立于主機環境運作。

C庫函數重定向

使用者可以定義自己的C語言庫函數,連接配接器在連接配接時自動使用這些新的功能函數。這個過程叫做重定向C語言庫函數,如圖6所示。

舉例來說,使用者有一個I/O裝置(如UART)。本來庫函數fputc()是把字元輸出到調試器控制視窗中去的,但使用者把輸出裝置改成了UART端口,這樣一來,所有基于fputc()函數的printf()系列函數輸出都被重定向到UART端口上去了。

下面是實作fputc()重定向的一個例子:

externvoidsendchar(char*ch);

intfputc(intch,FILE*f)

chartempch=ch;

sendchar(&tempch);

returnch;

這個例子簡單地将輸入字元重新定向到另一個函數sendchar(),sendchar()假定是一個另外定義的序列槽輸出函數。在這裡,fputc()就好像目标硬體和标準C庫函數之間的一個抽象層。

在C語言庫函數中禁用Semihosting

在一個獨立的嵌入式應用程式中,應該不存在SemihostingSWI操作。是以,使用者必須确定在所有調用到的庫函數中沒有使用Semihosting。為了保證這一點,在程式中可以引進一個符号關鍵字_use_no_semihosting:

在C代碼中,使用#prgrama #pragmaimport〈_use_no_semihosting_swi〉

在彙程式設計式中,使用IMPORT

IMPORT_use_no_semihosting_swi

這樣,當有使用SWI機制的庫函數被連接配接時,連接配接器會進行報錯:

Error:Symbol_semihosting_swi_guardmultiplydefined

為了确定具體是哪一個函數,連接配接時打開-verbose選項。這樣在結果資訊輸出時,該庫函數上将有一個_I_use_semihosting_swi的标記。

Loadingmembersys_wxit.ofromc_a_un.1.

Definition:_sys_exit

Reference:_I_use_semihosting_swi

使用者必須要把這些函數定義成自己的執行内容。

有一點需要注意,連接配接器隻能報告庫函數中被調用的Semihosting,對使用者自定義函數中使用的Semihosting則不會報錯。

根據目标硬體定制存儲器映射

分散裝載(Scatlerloading)

在實際的嵌入式系統中,ADS提供的預設存儲器映射是不能滿足要求的。使用者的目标硬體通常有多個存儲器裝置位于不同的位置,并且這些存儲器裝置在程式裝載和運作時可能還有不同的配置。

Scattertoading可以通過一個文本檔案來指定一段代碼或資料在加載和運作時在存儲器中的不同位置。這個文本檔案scatterfile在指令行中由-scatter開關指定,例如:

armlink_scatterscat.scffilel.ofile2.0

在scatterfile中可以為每一個代碼或資料區在裝載和執行時指定不同的存儲區域位址,Scatlertoading的存儲區塊可以分成二種類型:

裝載區:當系統啟動或加載時應用程式的存放區。

執行區:系統啟動後,應用程式進行執行和資料通路的存儲器區域,系統在實時運作時可以有一個或多個執行塊。

映像中所有的代碼和資料都有一個裝載位址和運作位址(二者可能相同也可能不同,視具體情況而定)。在系統啟動時,C函數庫中的__main初始化代碼會執行必要的複制及清零操作,使應用程式的相應代碼和資料段從裝載狀态轉入執行狀态。

1.scatter檔案文法

scatter檔案是一個簡單的文本檔案,包含一些簡單的文法。

My_Region0x00000x1000

{

thecontextofregion

}

每個塊由一個頭标題開始定義,頭中至少包含塊的名字和起始位址,另外還有最大長度和其他一些屬性選項。塊定義的内容包括在緊接的一對花括号内,依賴于具體的系統情況。

一個加載塊必須至少含有一個執行塊;實踐中通常有多個執行塊。

一個執行塊必須至少含有一個代碼或資料段;這些通常來自源檔案或庫函數等的目标檔案;通配符号*可以比對指定屬性項中所有沒有在檔案中定義的餘下部分。

2.簡單分散加載樣例

圖8 所示樣例中,隻有一個加載塊,包含了所有的代碼和資料,起始位址為0。這個加載塊一共對應兩個執行塊。一個包含所有的RO代碼和資料,執行位址與裝載位址 相同;同時另一個起始位址為0x10000的執行塊,包含所有的RW和ZI資料。這樣當系統開始啟動時,從第一個執行塊開始運作(執行位址等于裝載地 址),在執行過程中,有一段初始化代碼會把裝載塊中的一部分代碼轉移到另外的執行塊中。

下面是這個scatter描述檔案,該檔案描述了上述存儲器映射方式。

LOAD_ROM0x4000

EXE_ROM0x00000x4000;Rootregion

*〈+RO〉;Allcodeandconstantdata

RAM0x100000x8000

*〈+RW,+ZI〉;Allnon-constantdata

3.在分散檔案中放置對象

在大多數應用中,并不是像前例那樣,簡單地把所有屬性都放在一起,使用者需要控制特定代碼和資料段的放置位置。這可以通過在scatter檔案中對單個目标檔案進行定義實作,而不是隻簡單地依靠通配符。

為了覆寫标準的連接配接器布局規則,我們可以使用+FIRST和+LAST分散加載指令。典型的例子是在執行塊的開始處放置中斷向量表格:

LOAD_ROM0x00000x4000

EXEC_ROM0x00000x4000

vectors.o〈Vect,+FIRST〉

*〈+RO〉

;moreexecregions...

在這個scatter檔案中,保證了vextors.o中的Vect域被放置于位址0x0000。

4.RootRegion(根區)

根區是一個執行塊,它的加載位址與執行位址是一緻的。每個scatter檔案至少有一個根區。分散加載有一個限制:建立執行塊的代碼和資料(即完成複制和清零的代碼和資料)無法自行複制到另一個位置。是以,在根區中必須含有下面的部分:

_main.o,包含複制代碼/資料的代碼;

連接配接器輸出變量$$Table和ZISection$$Table,包含被複制代碼/資料的位址。

由于上面兩個部分的屬性是隻讀的,是以他們被*〈+RO〉通配符文法比對。如果*〈+RO〉被用在了非根區中,則在根區中必須顯式地指明另一個RO區域。

下面是一個例子:

LOAD_ROM0x00000x4000

EXE_ROM0x00000x4000;rootregion

_main.o〈+RO〉;copyingcode

*〈Region$$Tabl0e〉;RO/RWaddressestocopy

*〈ZISection$$Table〉;ZIaddressestozero

RAM0x100000x8000

*〈+RO〉;allotherROsections

*〈+RW,+ZI〉;allRWandZIsections

放置堆棧和heap

Scatterloading機制提供了一種指定代碼和靜态資料布局的方法。下面介紹如何放置應用程式的堆棧和heap。

* _user_initial_stackheap重定向

應用程式的堆棧和heap是在C庫函數初始化過程中建立起來的。可以通過重定向對應的子程式來改變堆棧和heap的位置,在ADS的庫函數中,即_user_initial_stackheap()函數。

_user_initial_stackheap()可以用C或彙編來實作,它必須傳回如下參數:

r0:heap基位址;

r1:堆棧基位址;

r2:heap長度限制值(需要的話);

r3:堆棧長度限制值。

當使用者使用分散裝載功能的時候,必須重調用_user_initial_stackheap(),否則連接配接器會報錯:

Error: L6218E: Undefined symbol Image$$ZI$$Limit (referred from sys_stackheap.o)

*存儲器模型

ADS 提供了兩種實時存儲器模型。預設時為one-region,應用程式的堆棧和heap位于同一個存儲器區塊,使用的時候相向生長,當在heap區配置設定一塊 存儲器空間時需要檢查堆棧指針。另一種情況是堆棧和heap使用兩塊獨立的存儲器區域。對于速度特别快的RAM,可選擇隻用來作堆棧使用。為了使用這種 two-region模型,使用者需要導入符号use_two_region_memory,heap使用需要檢查heap的長度限制值。

對這兩種模型來說,預設情況下對堆棧的生長都不進行檢查。使用者可以在程式編譯時使用 -apcs/swst 編譯器選項來進行軟體堆棧檢查。如果使用two-region模型,必須得在執行_user_initial_stackheap時指定一個堆棧限制值。

使用ADS1.2進行嵌入式軟體開發

圖9 重定向_user_initial_stackheap()

使用ADS1.2進行嵌入式軟體開發

圖10 基本初始化過程

使用ADS1.2進行嵌入式軟體開發

圖11 ROM/RAM重定向和映射

使用ADS1.2進行嵌入式軟體開發

表1

系統複位和初始化

目前情況,一般假設程式從C庫函數的初始化入口_main開始執行。實際上,所有的嵌入式程式在啟動時都要執行一些系統級的初始化操作。在此讨論這方面的内容。

初始化過程

圖10中顯示了一個基于ARM的嵌入式系統的基本初始化過程。可以看到,在_main之前加入了一個複位處理子產品reset handler,它在系統上電複位時立即啟動。辨別為$sub$$main的新代碼塊在進入主程式之前執行。

複 位處理子產品reset handler通常是一小段彙編代碼,在系統複位時執行。它至少完成應用程式中使用到的所有處理器模式的堆棧初始化工作。對于含有本地存儲器系統的核心 (比如含cache的ARM核心),配置工作也必須在這一段初始化過程中完成。當完成系統初始化之後,通常程式會跳向_main,開始C庫函數的初始化過 程。

系統初始化過程一般還包括另外一些内容,中斷使能等,這些大多安排在C庫函數的初始化完成之後執行。$sub$$main()完成這部分功能。

向量表(vector table)

所有的ARM系統都有一張中斷向量表當出現異常需要處理時,必須調用向量表。向量表一般要位于0位址處。

使用ADS1.2進行嵌入式軟體開發

表2

使用ADS1.2進行嵌入式軟體開發

表3

使用ADS1.2進行嵌入式軟體開發

表4

使用ADS1.2進行嵌入式軟體開發

表5

使用ADS1.2進行嵌入式軟體開發

表6

使用ADS1.2進行嵌入式軟體開發

表7

使用ADS1.2進行嵌入式軟體開發

表8

使用ADS1.2進行嵌入式軟體開發

表9

使用ADS1.2進行嵌入式軟體開發

表10

存儲器配置

*ROM/RAM重定向

當系統啟動的時候,為了保證0位址處有正确的啟動代碼存在,需要非易失性的存儲器。

一種簡單的方法,就是把系統0x0000開始的一塊位址配置設定給ROM。其缺點是,由于ROM的通路速度比RAM慢很多,當執行中斷響應需要從中斷向量表跳轉時,會給系統性能帶來損失;同時,在ROM中的向量表内容也不能被使用者程式動态修改。

另 外一種可行的方案如圖11所示。ROM位于位址0x1000開始的地方,但是在系統複位時又被存儲器控制器映射到0x0000位址處。這樣當系統啟動之 後,在位址0x0000看到的是ROM,系統執行這塊ROM中的啟動代碼,啟動代碼跳轉到真正的ROM的位址,并讓存儲器控制器移除對ROM的位址映射。 這時0x0000位址處的存儲器又恢複回了RAM。__main中的代碼把向量表copy到0x0000處的RAM中去,使得異常時能被正确響應。

表1 為ARM彙編中執行ROM/RAM重定向和映射的一個例子。它以ARM公司的Integrator平台為基礎的,該方法适用于類似ROM/RAM重定向方 法的所有平台。第一條指令完成從ROM的映射位址(0x00000)到真實位址的跳轉。位址标号instruct_2是ROM的真實位址 (0x180004)。然後通過設定Integrator平台上的相應控制寄存器,移除ROM的位址映射。代碼在系統一啟動就被執行。所有關于位址重定向 /映射的操作必須在C庫函數初始化之前完成。

*本地存儲器配置

許多ARM處理器都有片上存儲器系統,如cache和緊密耦合存儲器(TCM)、存儲器管理單元(MMU)或存儲器保護單元(MPU)。這些裝置都要在系統初始化過程中正确配置,并且有一些特殊的要求需要考慮。

由前文可知,_main中的C庫函數初始化代碼負責程式運作時的存儲器系統設定。是以,整個存儲器系統本身必須得在__main之前完成初始化工作,如MMU或MPU必須在reset handler裡面完成配置。

緊密耦合存儲器(TCM)的初始化同樣須在_main之前完成(通常在MMU/MPU之前),因為一般程式都需要把代碼和資料分散裝入TCM。需要注意的是當TCM被使能後,不再通路被TCM屏蔽的存儲器。

關 于cache的一緻性問題,如果cache在_main之前使能的話,那麼當_main裡面進行從裝載區到執行區的代碼和資料拷貝時(因為在拷貝過程中指 令和資料在本質上都是被當作資料處理),指令會出現在資料緩沖區。避免此問題的方法是在C庫函數初始化完成後再使能cache。

*Scatter loading與存儲器配置

無論是通過ROM/RAM重定向還是MMU配置的方法,如果系統在啟動和運作時存儲器分布不一緻,scatterloading檔案中的定義就要按照系統重定向後的存儲器分布情況進行。

以上文ROM/RAM重定向為例:

LOAD_ROM 0x10000 0x8000

{

EXE_ROM 0x10000 0x8000

{

reset_handler.o (+RO, +FIRST)

...

}

RAM 0x0000 0x4000

{

vectors.o (+RO, +FIRST)

...

}

}

裝載區LOAD_ROM被放置在0x10000處,代表了重定向之後代碼和資料的裝載位址。

堆棧的初始化

程式中可能用到的處理器模式,都需要定義一個堆棧指針。

在 表2中,堆棧位于stack_base辨別的位址中。這個符号可以是存儲器系統中的一個直接位址,也可以在另外的彙編檔案中定義,由scatter檔案來 定義配置設定位址。表2代碼為FIQ和IRQ模式各配置設定了一個256位元組的堆棧,使用者可以用同樣的方法為其他模式也配置設定堆棧。最簡單的方法就是進入相應的模 式,然後為SP寄存器指定相應的值。如果想使用軟體堆棧檢查,還必須指定一個堆棧長度限制值。

堆棧指針和堆棧限制的數值會作為參數自動傳遞到C庫函數的初始化代碼__user_initial_stackheap中,在__user_initial_stackheap中不應該修改這些值。

硬體初始化 $sub$$main()

一般來說,應該把所有的系統初始化代碼與主應用程式分離開來,但是有幾個例外,比如cache和中斷的使能,需要在C庫函數初始化之後執行。

表3代碼顯示了如何使用 $sub和 $supper 。連接配接器把呼叫main()的函數替換成呼叫$sub$$main(),完成cache和中斷的使能,并最終跳向main()。

執行模式考慮

為主應用程式選擇一個處理器執行模式非常重要,這取決于系統的初始化代碼。

許多在啟動過程中使用到的功能,如MMU/MPU的配置、中斷的使能等,隻能在特權級模式下進行。如果需要在特權極模式下運作自己的應用程式,隻要在退出初始化過程之前改變到相應的模式就行了,沒有其他任何問題。

如 果使用user模式,必須保證所有隻能在特權模式下執行的功能完成之後,才能進入user模式。因為system模式和user模式使用相同的寄存器組, reset handler應該從system模式退出,_user_initial_stackheap在system模式下完成應用程式堆棧的初始化。這樣在處理 器進入user模式後,所有的堆棧空間都已經被正确設定好了。

對存儲器布局的進一步考慮

在scatter檔案中配置設定硬體位址

雖 然可以在一個scatter檔案中描述代碼和資料的分散布局,但是目标硬體中的外設寄存器,堆棧和heap配置仍然直接采用硬體位址在程式源代碼中進行設 置。如果把所有存儲器位址相關的資訊都在scatter檔案中進行定義,避免在源檔案中引用絕對硬體位址,對程式的工程化管理是有大好處的。

*在scatter檔案中定義目标外設位址

通常外設寄存器的位址在程式檔案或頭檔案中定義,也可以聲明一個結構類型指向外設寄存器,結構的位址定位在scatter檔案中完成。

舉例來說,目标定時器上有2個32位的寄存器,可以用表4來映射這些寄存器。為了把結構放置在指定的存儲器位址上面,建立一個新的執行區(見表5)。scatter檔案便把timer_regs結構定位在了位址0x40000000。

注意,在啟動過程當中這些寄存器的内容不需要清零,改變寄存器的内容可能影響系統狀态。在執行區上加UNINIT屬性可以防止ZI資料在初始化過程中被清零。

在scatter檔案中配置設定堆棧和heap

在許多情況下,用scatter檔案來定義堆棧和heap的位址會帶來一些好處,主要有:所有的存儲器配置設定資訊集中在一個檔案裡;改變堆棧和heap的位址隻要重新連接配接就行了,不需要重新編譯。

*顯式地放置符号

在ADS1.2環境下,這是最簡單的方法。在前文中引用過2個符号stack_base和heap_base,這2個符号在彙編子產品中建立,在scatter檔案中各自的執行區裡定位(見表6)。

表7檔案中,heap基位址定位在0x20000上,堆棧基位址位于0x40000。現在heap和堆棧的位置就可以非常友善地進行編輯了。

*使用連接配接器産生的符号

這種方法需要在目标檔案中指定好heap和堆棧的長度。這在一定程度上減弱了本節開頭描述的兩個優點。

首先在彙編源程式中定義heap和堆棧的長度。關鍵詞SPACE用來保留一塊存儲器空間,NOINT則可以阻止清零操作(見表8)。注意在這裡的源檔案中并不需要位址标号。

然 後這些部分就可以在scatter檔案中對應的執行區裡定位了(見表9)。連接配接器産生的符号指向每一個執行區的基位址和長度限制,這些符号可以被 _user_initial_stackheap調用的重定向代碼使用。在代碼中使用DCD來給這些值定義更有意義的名字,可以增強代碼的可讀性(見表 10)。

檔案把heap基位址定位在0x15000,堆棧位址定位在0x4000。Heap和堆棧的位置可以通過編輯對應執行區的位址友善地改變。

繼續閱讀