使用ADS1.2進行嵌入式軟件開發(上)

 

概述
        嵌入式應用程序通常都是在樣機環境下調試與開發的,這種環境與最終產品之間並不完全相同。因此,在系統調試階段就考慮應用程序在最終目標硬件中的運行情況是非常重要的。
       本文旨在討論如何將一個開發/調試環境下的嵌入式應用程序轉移到最終獨立運行的目標系統中去,並提到了ARM ADS1.2開發工具包的一些功能特性及其在這個過程中所起到的作用。

       使用ADS開發嵌入式程序時,需要着重考慮以下幾個問題:
1.與硬件相關的C語言庫函數的使用;
2.某些C語言庫函數使用了調試環境中的資源,要把這些使用的資源重定向到目標系統中的硬件上來;
3.可執行映象文件的存儲器映射必須根據目標硬件的存儲器分佈進行裁剪;
4.在主程序執行前,嵌入式應用程序必須先完成系統的初始化。一個完整的初始化包括用戶的啓動執行代碼和ADS中C庫函數的初始化過程。

 


圖1 Semihosting的實現舉例


圖2 C語言庫函數結構


圖3 缺省的存儲器映射


圖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。


圖5 缺省的ADS初始化過程


圖6 C庫函數重定向


圖7 scatter文件語法


圖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()重定向的一個例子:
  extern void sendchar( char *ch );
  int fputc( int ch, FILE *f )
  { /* e.g.write a character to an UART */
      char tempch = ch;
      sendchar( &tempch );
      return ch;
  }

這個例子簡單地將輸入字符重新定向到另一個函數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_Region 0x0000 0x1000
  {
      the context of region
  }

每個塊由一個頭標題開始定義,頭中至少包含塊的名字和起始地址,另外還有最大長度和其他一些屬性選項。塊定義的內容包括在緊接的一對花括號內,依賴於具體的系統情況。
一個加載塊必須至少含有一個執行塊;實踐中通常有多個執行塊。
一個執行塊必須至少含有一個代碼或數據段;這些通常來自源文件或庫函數等的目標文件;通配符號*可以匹配指定屬性項中所有沒有在文件中定義的餘下部分。
2.簡單分散加載樣例
圖8所示樣例中,只有一個加載塊,包含了所有的代碼和數據,起始地址爲0。這個加載塊一共對應兩個執行塊。一個包含所有的RO代碼和數據,執行地址與裝載地址相同;同時另一個起始地址爲0x10000的執行塊,包含所有的RW和ZI數據。這樣當系統開始啓動時,從第一個執行塊開始運行(執行地址等於裝載地址),在執行過程中,有一段初始化代碼會把裝載塊中的一部分代碼轉移到另外的執行塊中。
下面是這個scatter描述文件,該文件描述了上述存儲器映射方式。
  LOAD_ROM 0x4000
  {
      EXE_ROM 0x0000 0x4000    ;Rootregion
      {
          *〈+RO〉             ;Allcodeandconstantdata
      }
      RAM 0x10000 0x8000
      {
          *〈+RW,+ZI〉         ;Allnon-constantdata
      }
  }

3.在分散文件中放置對象
在大多數應用中,並不是像前例那樣,簡單地把所有屬性都放在一起,用戶需要控制特定代碼和數據段的放置位置。這可以通過在scatter文件中對單個目標文件進行定義實現,而不是隻簡單地依靠通配符。
爲了覆蓋標準的連接器佈局規則,我們可以使用+FIRST和+LAST分散加載指令。典型的例子是在執行塊的開始處放置中斷向量表格:
  LOAD_ROM 0x0000 0x4000
  {
      EXEC_ROM 0x0000 0x4000
      {
          vectors.o〈Vect,+FIRST〉
          *〈+RO〉
      }
      ;more execregions...
  }

在這個scatter文件中,保證了vextors.o中的Vect域被放置於地址0x0000。
4.RootRegion(根區)
根區是一個執行塊,它的加載地址與執行地址是一致的。每個scatter文件至少有一個根區。分散加載有一個限制:創建執行塊的代碼和數據(即完成複製和清零的代碼和數據)無法自行復制到另一個位置。因此,在根區中必須含有下面的部分:
_main.o,包含複製代碼/數據的代碼;
連接器輸出變量$$Table和ZISection$$Table,包含被複制代碼/數據的地址。
由於上面兩個部分的屬性是隻讀的,因此他們被*〈+RO〉通配符語法匹配。如果*〈+RO〉被用在了非根區中,則在根區中必須顯式地指明另一個RO區域。
下面是一個例子:
  LOAD_ROM 0x0000 0x4000
  {
      EXE_ROM 0x0000 0x4000       ;rootregion
      {
          _main.o〈+RO〉          ;copyingcode
          *〈Region$$Tabl0e〉     ;RO/RWaddressestocopy
          *〈ZISection$$Table〉   ;ZIaddressestozero
      }
      RAM 0x10000 0x8000
      {
          *〈+RO〉                ;allotherROsections
          *〈+RW,+ZI〉            ;allRWandZIsections
      }
  }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章