星期一, 9月 03, 2012

為 Das U-Boot 新增網路服務

最近被交代要在 u-boot 上實作 DHCP server。雖然單純的寫一個簡單的 DHCP server 其實蠻容易的,只要完成 "DORA" (Discover, Offer, Request, Ack) 即可,sourceforge 上也有一些程式碼可以參考,例如 Simple Embedded DHCP 等等,但是要在 U-Boot 上實作就不是那麼直接了,因為在 U-Boot 環境中沒有 OS 的協助,所以必須依照 U-Boot 的架構從零開始。

今天完成了可運作的 DHCPD 實作,雖然還有一些細節需要再檢驗 U-Boot 原始碼來做些確認和釐清,但大致上的重點應該都有抓到,所以紀錄一下以供以後參考。不過因為我的目標 U-Boot 版本是大約 2005 年釋出的 v1.1.4,雖然大原則相同,但一些細節可能會有所不同,例如定義一些函式、字串、巨集的檔案可能會有所不同等等。


U-Boot 網路相關的核心功能與函式,主要是在 <U-BOOT>/net/net.c 中實作,並且在 <U-BOOT>/include/net.h 中定義許多相關全域變數。基本上要實作任何網路相關功能,大概至少要引入 net.h 和 common.h:

#include <net.h>
#include <common.h>
此外,若實作的服務是原本 U-Boot 中沒有的服務,那多半還需要在 net.h 的 proto_t 中定義新的項目。

U-Boot 中的網路功能,主要是透過 net.c 中的 int NetLoop(proto_t protocol) 來執行。當 NetLoop() 函式開始執行時,會根據傳入的 protocol 作該協定相關的初始化工作,然後再呼叫 net_check_prereq(protocol) 來做該服務相關的先決條件檢查,如果檢查通過的話,通常就會執行該相關服務的起始函式,接著進入無限迴圈,在每次環圈執行中檢查全域變數 NetState 來決定接下來是繼續執行迴圈還是結束 NetLoop() 的執行。

在這樣的架構下,要實作一個新的服務(EX. MYSERVICE),需要至少實作以下的部分:
  • 至少需要實作新服務的起始函式 MyServiceStart()、封包處理回呼函式 MyServiceHandler()、逾時處理回呼函式 MyServiceTimeout() 等,其中除了 MyServiceHandler() 的原型會有輸入參數以外,其餘兩個函式都可以沒有輸出輸入參數。另外,通常還會實作一個封包建構和傳送函式 MyServiceSend(),把封包建構的程式碼由 MyServiceHandler() 中分離出來,方便修改除錯。此外,因為大部分的函式都沒有使用輸入參數來做參數傳遞,因此通常還會建立一個該服務的狀態全域變數 MyServiceState 來追蹤新服務的執行階段。
  • 在 <U-BOOT>/include/net.h 的 proto_t enum 中新增該服務
  • 修改 <U-BOOT>/net/net.c 的 NetLoop() 中協定初始化的 switch() {...},加入新服務的初始化程式碼,例如由環境變數中把 local IP 填入 NetOurIP 全域變數等等。
  • 修改 net_check_prereq() 函式,加入新服務的檢查。
  • 修改 NetLoop() 中的 swtich(net_check_prereq(protocol)) {...},在 "case 0:" 的部分,加上新服務在通過檢查後的後續初始化程式碼,並在最後呼叫新服務的 MyServiceStart() 實際起始新服務。

至於新服務的3個基本函式要包含怎樣的內容呢?大致上來說,MyServiceStart() 需要處理以下工作:
  • 開始接收封包前的其他初始化工作。
  • 使用 NetSetTimeout(TIMEOUT, MyServiceTimeout()) 來註冊新服務的逾時時間門檻以及逾時處理回呼函式。
  • 使用 NetSetHandler(MyServiceHandler()) 來註冊新服務的封包接收處理回呼函式。

MyServiceHandler() 則需要負責接收到封包後的處理,例如封包內容的解讀、根據收到的封包更新 MyServiceState 狀態變數、呼叫 MyServiceSend() 來建構和發送回應封包、根據服務的執行階段設定 NetState 全域變數來結束 NetLoop() 的執行或呼叫 NetStartAgain() 重新初始化 NetLoop() 的執行等等。

至於 MyServiceTimeout() 中,則是處理逾時的狀況,例如顯示除錯訊息後,把 NetState 設定為 NETLOOP_FAIL 讓 NetLoop() 結束執行等等。不過要注意一點的是,如果在接收到有效封包後沒有在 MyServiceHandler() 中呼叫 NetSetTimeout(0, (thand_f *)0) 取消逾時回呼的註冊,或者是重新設定 Timeout 時限值 (即 NetSetTimeout() 的第一個參數)的話,逾時的計算會繼續執行,所以如果一開始的 Timeout 值設定得很短,然後又忘了在之後重設 Timeout 的話,很可能會發生服務執行到一半莫名其妙被中斷的 bug。

除了實作這3個基本函式以外,通常還會把封包建構和傳送的程式碼,集中在 MyServiceSend() 函式中。在 U-Boot 中要建構封包,幾乎是必須從零開始。net.c 中有提供兩個封包傳送函式,發送網路層封包的 NetSendPacket() 函式,以及發送傳輸層 UDP 封包的 NetSendUDPPacket() 函式,需要視新服務所用協定的封包種類做選擇。封包的建構通常是宣告一個 uchar *pkt 指標後,由 pkt=NetTxPacket 取得外送封包的指標,接著就是逐步加上 ethernet header、IP header、負載協定封包的檔頭與資料等等,填值時需注意適當使用 htons()、htonl() 等函式來處理 byte order 的問題。

到此為止,新服務的實作大致完成了,但是如果想要在 U-Boot 的互動式命令列介面中能夠用指令來執行這個新服務,則還需要實作一個新的 U-Boot 命令列指令。

U-Boot 的命令列指令的實作,都集中在 <U-BOOT>/common/cmd_*.c 中,並且要在 <U-BOOT>/include/cmd_confdefs.h 中定義該指令的設定用旗標值的巨集,以及修改相關的 Makefile 中把新指令和新服務的 .o 檔加入連結的目標中。

要實作一個新的命令列指令 myservice,大致上需要修改以下幾個地方:
  • 在 <U-BOOT>/include/cmd_confdefs.h 中定義 CFG_CMD_MYSERVICE 巨集。U-Boot 在設定命令列介面要包含哪些指令的 CONFIG_COMMAND 巨集時,會使用這個巨集的旗標值作 OR 運算,來決定要包含的指令有哪些。如果找不到適當的值的話,就只好取代已經被定義但應該用不到的指令的旗標值。
  • 在 <U-BOOT>/common/cmd_net.c 中,實作 do_myservice() 函式,並使用 U_BOOT_CMD(myservice, 1, 1, do_myservice, "description", "Help Message") 巨集在指令表中註冊 myservice 指令與對應的 do_myservice() 處理函式。至於 do_myservice() 處理函式的定義,最簡單的方式就是直接呼叫 cmd_net.c 中定義的 netboot_common() 函式,該函式會在完成參數分析之後,呼叫 NetLoop() 函式開始執行新服務。

假如所有實作都正確,且 CONFIG_COMMAND 巨集中有啟用 CFG_CMD_MYSERVICE 的話,在進入 U-Boot 的命令列模式後,使用 help 指令,應該會看到 myservice 指令也在其中。

大致上要實作一個新的網路服務,並且要新增一個命令列指令來啟動該服務的注意事項就是這些,剩下的就是一些實際實作時隨情況而定的細節,以及不斷的除錯直到新服務可以順利執行了。過幾天如果時間許可,也許會寫一個簡單的 echo service 來紀錄整個實作的細節。

繼續閱讀全文