RISC-V嵌入式開發準備篇2:嵌入式開發的特點介紹
原文出處:https://mp.weixin.qq.com/s/ljYZwMj3JaPN29dTAXA3bQ
隨著國內第一本RISC-V中文書籍《手把手教你設計CPU——RISC-V處理器篇》 正式上市,越來越多的愛好者開始使用開源的蜂鳥E203 RISC-V處理核,很多初學者留言詢問有關RISC-V工具鏈使用的問題,因此本公眾號將開始陸續發表若干篇有關RISC-V軟體工具鏈使用的文章,包括:
- RISC-V嵌入式開發準備篇1:編譯過程簡介
- RISC-V嵌入式開發準備篇2:嵌入式開發的特點介紹
- RISC-V嵌入式開發入門篇1:RISC-V GCC工具鏈的介紹
- RISC-V嵌入式開發入門篇2:RISC-V組合語言程式設計
- RISC-V嵌入式開發上手篇:基於HBird-E-SDK平臺的軟體開發與執行
- RISC-V嵌入式開發實踐篇:執行開源蜂鳥E200 MCU更多示例程式
- RISC-V嵌入式開發新奇篇:基於Windows Eclipse IDE的軟體開發與執行
- RISC-V嵌入式開發昇華篇:基於開源蜂鳥E200 MCU移植RTOS
本文為RISC-V嵌入式開發準備篇2:嵌入式開發的特點介紹。
本文的目的是對嵌入式開發的特點進行簡單的科普與回顧,為後續詳細介紹“RISC-V GCC工具鏈”和“RISC-V組合語言程式設計”打下基礎。注:本文力求通俗易懂,主要面向初學者,對嵌入式開發有所瞭解的讀者可以忽略此文。
在本號上次發表的文章《編譯過程簡介》中介紹過,嵌入式系統的程式編譯過程和開發有其特殊性,譬如:
- 嵌入式系統需要使用交叉編譯與遠端除錯的方法進行開發。
- 需要自己定義載入程式。
- 需要注意減少程式碼體積(Code Size)。
- 需要移植printf從而使得嵌入式系統也能夠列印輸入。
- 使用Newlib作為C執行庫。
- 每個特定的嵌入式系統都需要配套的板級支援包。
下文將分別予以介紹。
1 交叉編譯和遠端除錯
在本號上次發表的文章《編譯過程簡介》中介紹瞭如何在Linux系統的PC電腦上開發一個Hello World程式,對其進行編譯,然後執行在此電腦上。在這種方式下,我們使用PC電腦上的編譯器編譯出該PC電腦本身可執行的程式,這種編譯方式稱之為本地編譯。
嵌入式平臺上往往資源有限,嵌入式系統(譬如常見ARM MCU或8051微控制器)的儲存器容量通常只在幾KB到幾MB之間,且只有快閃記憶體而沒有硬碟這種大容量儲存裝置,在這種資源有限的環境中,不可能將編譯器等開發工具安裝在嵌入式裝置中,所以無法直接在嵌入式裝置中進行軟體開發。因此,嵌入式平臺的軟體一般在主機PC上進行開發和編譯,然後將編譯好的二進位制程式碼下載至目標嵌入式系統平臺上執行,這種編譯方式屬於交叉編譯。
交叉編譯可以簡單理解為,在當前編譯平臺下,編譯出來的程式能執行在體系結構不同的另一種目標平臺上,但是編譯平臺本身卻不能執行該程式,譬如,在x86平臺的PC電腦上編寫程式並編譯成能執行在ARM平臺的程式,編譯得到的程式在x86平臺上不能執行,必須放到ARM平臺上才能執行。
與交叉編譯同理,在嵌入式平臺上往往也無法執行完整的偵錯程式,因此當運行於嵌入式平臺上的程式出現問題時,需要藉助主機PC平臺上的偵錯程式來對嵌入式平臺進行除錯。這種除錯方式屬於遠端除錯。
常見的交叉編譯和遠端除錯工具是GCC和GDB。在本號上次發表的文章《編譯過程簡介》中介紹瞭如何使用Linux自帶的GCC本地編譯一個Hello World程式並執行。但是,GCC不僅能作為本地編譯器,還能作為交叉編譯器;同理GDB不僅可以作為本地偵錯程式,還可以作為遠端偵錯程式。
當作為交叉編譯器之時,GCC通常有不同的命名,譬如:
- arm-none-eabi-gcc和arm-none-eabi-gdb是面向裸機(Bare-Metal)ARM平臺的交叉編譯器和遠端偵錯程式。
- 所謂裸機(Bare-Metal)是嵌入式領域的一個常見形態,表示不執行作業系統的系統
- 而riscv-none-embed-gcc和riscv-none-embed-gdb是面向裸機RISC-V平臺的交叉編譯器和遠端偵錯程式。
- 本號後續發文《RISC-V GCC工具鏈的介紹》將介紹RISC-V GCC工具鏈的更多資訊。
2 移植newlib或newlib-nano作為C執行庫
newlib是一個面向嵌入式系統的C執行庫。相對於本號上次發表的文章《編譯過程簡介》中介紹的glibc,newlib實現了大部分的功能函式,但體積卻小很多。newlib獨特的體系結構將功能實現與具體的作業系統分層,使之能夠很好地進行配置以滿足嵌入式系統的要求。由於專為嵌入式系統設計,newlib具有可移植性強、輕量級、速度快、功能完備等特點,已廣泛應用於各種嵌入式系統中。
由於嵌入式作業系統和底層硬體的多樣性,為了能夠將C/C++語言所需要的庫函式實現與具體的作業系統和底層硬體進行分層,newlib的所有庫函式都建立在20個樁函式的基礎上,這20個樁函式完成具體作業系統和底層硬體相關的功能:
- I/O和檔案系統訪問(open、close、read、write、lseek、stat、fstat、fcntl、link、unlink、rename);
- 擴大記憶體堆的需求(sbrk);
- 獲得當前系統的日期和時間(gettimeofday、times);
- 各種型別的任務管理函式(execve、fork、getpid、kill、wait、_exit);
這20個樁函式在語義、語法上與POSIX(Portable Operating System Interface of UNIX)標準下對應的20個同名系統呼叫完全相容。
所以,如果需要移植newlib至某個目標嵌入式平臺,成功移植的關鍵是在目標平臺下找到能夠與newlib樁函式銜接的功能函式或者實現這些樁函式。本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將介紹蜂鳥E200的HBird-E-SDK平臺如何實現移植實現newlib的樁函式。
注意:newlib的一個特殊版本newlib-nano版本進一步為嵌入式平臺減少了程式碼體積(Code Size),因為newlib-nano提供了更加精簡版本的malloc和printf函式的實現,並且對庫函式使用GCC的-Os(側重程式碼體積的優化)選項進行編譯優化。
3 嵌入式載入程式和中斷異常處理
在本號上次發表的文章《編譯過程簡介》中介紹瞭如何在Linux系統的PC電腦上開發一個Hello World程式,對其進行編譯,然後執行在此電腦上。在這種方式下,程式設計師僅僅只需要關注Hello World程式本身,程式的主體由main函式組織而成,程式設計師可以無需關注Linux作業系統在執行該程式的main函式之前和之後需要做什麼。事實上,在Linux作業系統中執行應用程式(譬如簡單的Hello World)時,作業系統需要動態地建立一個程序、為其分配記憶體空間、建立並執行該程序的載入程式,然後才會開始執行該程式的main函式,待其執行結束之後,作業系統還要清除並釋放其記憶體空間、登出該程序等。
從上述過程中可以看出,程式的引導和清除這些“髒活累活”都是由Linux這樣的作業系統來負責進行。但是在嵌入式系統中,程式設計師除了開發以main函式為主體的功能程式之外,還需要關注如下兩個方面:
- 載入程式:
- 嵌入式系統上電後需要對系統硬體和軟體執行環境進行初始化,這些工作往往由用匯編語言編寫的載入程式完成。
- 載入程式是嵌入式系統上電後執行的第一段軟體程式碼。載入程式對於嵌入式系統非常關鍵,載入程式所執行的操作依賴於所開發的嵌入式系統的軟硬體特性,一般流程包括:初始化硬體、設定異常和中斷向量表、把程式拷貝到片上SRAM中、完成程式碼的重對映等,最後跳轉到main函式入口。
- 本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將結合HBird-E-SDK平臺的載入程式例項瞭解載入程式的更多細節。
- 中斷異常處理
- 中斷和異常是嵌入式系統非常重要的一個環節,因此,嵌入式系統軟體還必須正確地配置中斷和異常處理函式。有關RISC-V架構的中斷和異常的詳細資訊,請參見RISC-V中文書籍《手把手教你設計CPU——RISC-V處理器篇》 中第13章內容《不得不說的故事——中斷和異常》。
- 本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將結合HBird-E-SDK程式例項瞭解如何配置中斷和異常處理函式。
4 嵌入式系統連結指令碼
在本號上次發表的文章《編譯過程簡介》中介紹瞭如何在Linux系統的PC電腦上開發一個Hello World程式,對其進行編譯,然後執行在此電腦上。在這種方式下,程式設計師也無需關心編譯過程中的“連結”這一步驟所使用的連結指令碼,無需為程式分配具體的記憶體空間。
但是在嵌入式系統中,程式設計師除了開發以main函式為主體的功能程式之外,還需要關注“連結指令碼”為程式分配合適的儲存器空間,譬如程式段放在什麼區間、資料段放在什麼區間等等。
本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將結合HBird-E-SDK的“連結指令碼”例項瞭解更多細節。
5 減少程式碼體積
嵌入式平臺上往往儲存器資源有限,嵌入式系統(譬如常見的ARM MCU或8051微控制器)的儲存器容量通常只在幾KB到幾MB之間,且只有快閃記憶體而沒有硬碟這種大容量儲存裝置,在這種資源有限的環境中,程式的程式碼體積(Code Size)顯得尤其重要,因此,有效地降低降低程式碼體積(Code Size)是嵌入式軟體開發必須要考慮的問題,常見的方法如:
- 使用newlib-nano作為C執行庫以取得較小程式碼體積(Code Size)的C庫函式。
- 儘量少使用C語言的大型庫函式,譬如在正式發行版本的程式中避免使用printf和scanf等函式。
- 如果在開發的過程中一定需要使用printf函式,可以使用某些自己實現的閹割版printf函式(而不是C執行庫中提供的printf函式)以生成較小的程式碼體積。
- 除此之外,在C/C++語言的語法和程式開發方面也有眾多技巧以取得更小的程式碼體積(Code Size)。
- 本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將結合HBird-E-SDK平臺例項瞭解更多“減少程式碼體積”的實現細節。
減小程式碼體積(Code Size)的方法很多,本文在此不做一一贅述,請初學的讀者自行查閱相關資料進行學習。
6 支援printf函式
在本號上次發表的文章《編譯過程簡介 》中介紹瞭如何在Linux系統的PC電腦上開發一個Hello World程式,程式中使用C語言的標準庫函式printf列印了一個“Hello World”字串。該程式在Linux系統裡面執行的時候字串被成功的輸出到了Linux的終端介面上。在這個過程中,程式設計師無需關心Linux系統到底是如何將printf函式的字串輸出到Linux終端上的。事實上,如《編譯過程簡介》中所述,在Linux本地編譯的程式會連結使用Linux系統的C執行庫glibc,而glibc充當了應用程式和Linux作業系統之間的介面,glibc提供的 printf 函式就會呼叫如sys_write等作業系統的底層系統呼叫函式,從而能夠將“字串”輸出到Linux終端上。
從上述過程中可以看出,由於有glibc的支援,所以printf函式能夠在Linux系統中正確的進行輸出。但是在嵌入式系統中,printf的輸出卻不那麼容易了,基於如下幾個原因:
- 嵌入式系統使用newlib作為C執行庫,而newlib的C執行庫所提供的printf函式最終依賴於如本文中所介紹的newlib樁函式write,因此必須實現此write函式才能夠正確的執行printf函式。
- 嵌入式系統往往沒有“顯示終端”存在,譬如常見的微控制器其作為一個黑盒子一般的晶片,根本沒有顯示終端。因此,為了能夠支援顯示輸出,通常需要藉助微控制器晶片的UART介面將printf函式的輸出重新定向到主機PC的COM口上,然後藉助主機PC的串列埠除錯助手顯示出輸出資訊。同理,對於scanf輸入函式,也需要通過主機PC的串列埠除錯助手獲取輸入然後通過主機PC的COM口傳送給微控制器晶片的UART介面。
- 從以上兩點可以看出,嵌入式平臺的UART介面非常重要,往往扮演了輸出管道的角色,為了能夠將printf函式的輸出定向到UART介面,需要實現newlib的樁函式write,使其通過程式設計UART的相關暫存器將字元通過UART介面輸出。
本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將結合HBird-E-SDK平臺移植printf函式的例項瞭解更多細節。
7 提供板級支援包
對於特定的嵌入式硬體平臺,為了方便使用者在硬體平臺上開發嵌入式程式,硬體平臺一般會提供板級支援包(Board Support Package,BSP)。板級支援包所包含的內容沒有絕對的標準,通常說來,其必須包含如下內容:
- 底層硬體裝置的地址分配資訊
- 底層硬體裝置的驅動函式
- 系統的載入程式
- 中斷和異常處理服務程式
- 系統的連結指令碼
- 如果使用newlib作為C執行庫,一般還提供newlib樁函式的實現。
由於板級支援包往往會將很多底層的基礎設施和移植工作搭建好,因此應用程式開發人員通常都無需關心本文第1.2節至第1.6節中描述的內容,能夠從底層細節中被解放出來避免重複建設而出錯。本號後續發文《基於HBird-E-SDK平臺的軟體開發與執行》將結合HBird-E-SDK平臺的BSP例項瞭解更多細節。
更多資訊
感興趣的讀者可以通過下面二維碼關注公眾號“矽農亞歷山大”,瞭解Verilog、IC設計、CPU、RISC-V和人工智慧AI相關的更多設計技巧和經驗分享,注意:由於乾貨太多,請自備茶水。