執行人: HenryChaing
解說錄影
未來會再更新第二版
yinghuaxia
ADB 擁有兩項特點,第一個是它的傳輸功能,可以選擇使用 TCP 亦或是 USB 作為傳輸媒介 ; 第二是健全的功能設計,例如 adb shell 可以對目標裝置的 shell 下達命令,adb push/pull 可以雙向傳輸檔案。
ADB 可以使用 TCP 或是 USB 作為傳輸媒介,是因為 TCP 和 USB 之間有什麼共通點嗎?
\(\to\) TCP/IP 是網路架構之一同時也是電腦之間的通訊方式,而 USB 是實體媒介,實際傳輸資訊需要靠 USB Host Driver 以及 USB Gadget Driver 去設定行為。
adbd 的全名為 android debug bridge deamon ,是一支在目標裝置背景執行的程式。不過在 Milk-v Duo 上執行前會遇到一個問題,也就是 Milk-v Duo 的大核 C906B 是 RISC-V 64 位元的架構,因此若要獲得 adbd 執行檔就必須先經過交叉編譯。
請問交叉編譯的概念是什麼?是因為架構不同所以才需要使用交叉編譯嗎?交叉編譯與一般的編譯差異為何?
\(\to\) 編譯這項行為與平台相依,會因為執行檔與自身平台而有不同的編譯環境(包含 編譯器、連結器、標頭檔、靜態函式庫等)。而交叉編譯是編譯的一種,不過是指編譯出的執行檔所運行的平台與執行編譯的電腦平台相異的情形。而需要交叉編譯的原因以本專題來說是因為 Milk-v duo 的運算資源有限。
hugo0406
接著介紹 ADB 的運作機制,這也與實作內容相關。 ADB 共分成 3 個部分:在主機端的 adb 和 adb server 以及運行在目標裝置的 adbd 。其中 adb 負責擔當與使用者互動的 end point ,可以在參數列調整命令內容, adb server 則是擔當 adb 與 adbd 之間的溝通程式, adbd 則會呼叫副程式將命令下達到 shell ,再將副程式之結果回傳到 adb server 。
文章中提到 adb server 是 adb 與 adbd 之間溝通的橋樑,是否能詳細說明一下是如何進行溝通的呢?以及溝通的主要內容有哪些?
\(\to\) 其中, adb server 與 adb 之間採用的是 TCP socket 的方式進行 IPC ,而 adbd 與 adb server 之間可以選擇採用 USB 或是 TCP socket 的方式溝通,本專題使用 USB 方式。至於詳細的溝通內容,我以 adb shell ls
這個命令為例,首先 adb 與 adb server 會先進行交握,包括確認 adb 版本以及目標裝置、虛擬機器,再來將 ls
這個命令從 adb 經由 adb server 送到 adbd ,而 adbd 經由子行程執行 shell 得到的結果會再經過 adb server 回傳到 adb 當中輸出。
本專題嘗試在物美價廉的 Milk-V Duo 硬體上面,利用其異質多核的 RISC-V,運作 Linux 核心和 ThreadX,並利用 OpenAMP 規範的 RPMsg,建立異質多核間的通訊。
針對 Milk-V Duo (及其 256M 的硬體型號)
移植 Eclipse ThreadX RTOS 到 RISC-V64 架構,驗證平台為 Milk-V Duo,ThreadX 執行無 MMU 的小核 C906L,Linux 運行於大核 C906B。
\(\to\) 驗證 saicogn/threadx
避免非必要的 backtick,後者用於程式碼、特別的數值,或有工程意義的位元組序列。反之,保持行文的流暢。
了解,下次會慎用 backtick
在 Milk-v Duo 當中,小核 C906L 預設運行的作業系統為 FreeRTOS , 但是基於我們隨後要加上的 RPMsg-lite 以及基於兩個核通訊而誕生的應用程式,在 Milk-v Duo 只有 64MB 的主記憶體之情況下,我們必須使用佔據記憶體空間較少並且利於通訊的作業系統,而這就是我們採用 Eclipse Threadx RTOS 作為我們小核作業系統的原因。
在實作方面,我們只須將 ThreadX-to-RISC-V64 這個 repository 使用命令 git clone 到我們 duo-buildroot-sdk 當中即可 , 接著是改動 build 目錄底下的 milkvsetup.sh 檔案 ,這個是 SDK 在建置環境時會使用到的腳本,其中 bash 變數 FREERTOS_PATH
要將其改為 SDK 當中 threadx 專案目錄的位置。
這樣的改動會讓最後映像檔包含的二進位檔從 FreeRTOS 轉變為 ThreadX ,至於二進位檔的生成, ThreadX-to-RISC-V64 專案採用的是 CMake 的方式, CMake 可以在 CMakeLists.txt 檔案透過簡單的指令引入 Source file 、 Header file 、 Static library 以及 Flag 、 Options 。 生成 Makefile 並完成編譯。
- FREERTOS_PATH="$TOP_DIR"/freertos
+ # FREERTOS_PATH="$TOP_DIR"/freertos
+ FREERTOS_PATH="$TOP_DIR"/threadx
接著是驗證 threadx 是否成功運行,在 SDK 中的 threadx 專案中有幾個任務會並行執行,分別是 thread_0
、 thread_1
。 它們的內容分別是每 4 、 8 秒進行一次計數,並印出計數的內容,但是一般的連線方式如 ssh 亦或是本章的 ADB 都無法直接看到 2 個 RISC-V 核所印出來的資訊,所以我們改用 neocon 這項 utility software 觀察 Milk-v Duo 運行時印出的資訊。
neocon 是一個好用的 serial console utility ,它會嘗試不斷開啟 tty device 直到連線成功,並且建立連線來傳輸 terminal 的輸入及輸出。 我們也就用這個方法來觀察 tty device 接收到的字元訊息,其中也包含了 threadx 在小核上列印的字元輸出結果。
以下為部份輸出:
(開機時)
RT: [9.315555]threadx 1 running: 11
RT: [9.320556]threadx 0 running: 12
RT: [9.323703]float cal: 54
(執行約 1200 秒)
RT: [2505.490374]threadx 1 running: 323
RT: [2508.615374]threadx 0 running: 636
RT: [2508.618879]float cal: 304722
RT: [2512.620374]threadx 0 running: 637
RT: [2512.623879]float cal: 305679
RT: [2513.490374]threadx 1 running: 324
RT: [2516.625374]threadx 0 running: 638
RT: [2516.628880]float cal: 306637
RT: [2520.630374]threadx 0 running: 639
RT: [2520.633880]float cal: 307597
RT: [2521.490374]threadx 1 running: 325
RT: [2524.635374]threadx 0 running: 640
RT: [2524.638880]float cal: 308559
RT: [2528.640374]threadx 0 running: 641
RT: [2528.643880]float cal: 309522
RT: [2529.490374]threadx 1 running: 326
RT: [2532.645374]threadx 0 running: 642
RT: [2532.648879]float cal: 310486
其中經由計數來觀察執行緒的執行狀況,會發現兩者皆呈現從第一次列印之後經過了 1248
秒。
因此 ThreadX 的運行在 Milk-v Duo 上算是穩定。
用語要精準,什麼「穩定」?
Linux 運行於大核 C906B,在其上移植 (Android) ADB,確保 USB gadget driver 運作符合預期,並在 Microsoft Windows 和 GNU/Linux 上驗證
改進漢語描述
ADB 的全名為 Android Debug Bridge,原先是針對 android 作業系統為目標裝置設計的基礎建設,但是其功能便利因此以 Linux 為核心的嵌入式裝置也會需要它的協助。 ADB 擁有兩項特點,第一個是它的傳輸功能,可以選擇使用 TCP 亦或是 USB 作為傳輸媒介 ; 第二是健全的功能設計,例如 adb shell
可以對目標裝置的 shell 下達命令,adb push/pull
可以雙向傳輸檔案。
接著介紹 ADB 的運作機制,這也與實作內容相關。 ADB 共分成 3 個部分:在主機端的 adb 和 adb server 以及運行在目標裝置的 adbd 。其中 adb 負責擔當與使用者互動的 end point ,可以在參數列調整命令內容, adb server 則是擔當 adb 與 adbd 之間的溝通程式, adbd 則會呼叫副程式將命令下達到 shell ,再將副程式之結果回傳到 adb server 。
adbd 的全名為 android debug bridge deamon ,是一支在目標裝置背景執行的程式。不過在 Milk-v Duo 上執行前會遇到一個問題,也就是 Milk-v Duo 的大核 C906B 是 RISC-V 64 位元的架構,因此若要獲得 adbd 執行檔就必須先經過交叉編譯。
首先我採用的是 riscv-gnu-toolchain 這個工具鏈,並且也成功編譯出了執行檔,但是交叉編譯出來的執行檔卻無法在大核 C906B 上執行。後來歸納出原因在於沒有使用 Milk-v Duo 所提供的 toolchain ,因為這個工具鏈當中有包含選項 (option) 專門用於編譯 C906B 的執行檔,然而 riscv-gnu-toolchain 卻沒有支援這個編譯選項。
編譯及連結階段所使用的選項:
export CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d"
# -Os
export LDFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64"
因此我後來採用 Milk-v Duo 所提供的工具鏈 riscv64-linux-musl-x86_64 來進行交叉編譯,與第一次編譯較為不同的地方在於,因為採用的是 musl (讀作: muscle) 而非 GNU 的工具鏈,所以有一些函式庫內容並沒有支援,其中 musl libc 同 glibc 也是標準 C 函式庫的實作,但它更注重於執行檔的大小、靜態連結鏈結 的支援 以及 Race condition 等並行程式問題的排除。
強調相互結合的意境時,譯作「連結」,例如「動態連結函式庫」(dynamic-link library),而強調資料因結合而呈現鏈狀樣貌,用「鏈結」,如鏈結串列 (linked list)。本例要說「連結」
參見 https://hackmd.io/@l10n-tw/glossaries
感謝糾正 !
例如我在編譯過程中遇到 adbd 專案中有使用 GNU extension 的程式片段,後來經過老師提醒發現可以在編譯選項加上 -std = gnu++2a
,這樣編譯器可以認得 GNU dialect of -std=c++ 20 規範的 C++ 語言。 詳見 GCC manual 。再來還有 musl libc 未支援的 C 標準函式庫函式,也在 adbd 的編譯過程中遇到。而解決辦法是聽照老師從 newlib/glibc 中找到該方法的實作,並將其加入到專案當中,解決這項難題。
接著還面臨與 libc 無關的 OpenSSL 函式庫使用,也當然 musl libc 沒有支援這項函式庫。因此我後來在 OpenSSL 官方網站找到了 Git repository 並先對該函式庫進行交叉編譯。原先以為將標頭檔及相關的 C 程式實作放入對應的目錄即可,但是到了連結階段發現缺乏對應的 lib , 例如 libcrypto
、 libssl
,原來這些 lib 也是出自 OpenSSL 並且就是最後編譯出來的靜態函式庫 (static library),附檔名為 .a
並由 linker 與其他目標檔案組成執行檔。
再來還使用到了 GLib 函式庫, Glib 結合了多項平台底層函式的實作,因此 Glib 可以被不同的作業系統引用並編譯,但是在編譯函式庫的過程中遇到了不少問題,因此後來決定將使用到 Glib 函式庫的部份改為 Linux 標準 C 函式庫的方法實作。在此除了需要查詢 GLib manual 關於函式的用途,也需要使用到 man
命令查詢系統的標準 C 函式庫實作。
不過之所以改為 Linux 所支援的 C 實作,原因是在下達以下命令後,發現僅有一個檔案使用到這個關鍵的 Glib 標頭檔。關於 grep
的介紹,在 man grep
的手冊資訊當中可以得知 -r
是遞迴找對應目錄中所有檔案的內容,而 -v
則是排除在該目錄搜尋到的結果(以此例來說 sysroot 的交叉編譯工具鏈的環境目錄,與 adbd 無關)。
$ grep -r "#include <glib.h>" * | grep -v sysroot
最後得到了交叉編譯得到的 adbd 執行檔,但是與第一次結果不同,這個是可以在 Milk-v Duo 上執行的檔案,並且透過 strace
命令可以看到這個執行檔所用到的系統呼叫,以下為 adbd 所用到的系統呼叫部份展示:
execve("/root/adbd", ["/root/adbd"], 0x3fffdaabe0 /* 18 vars */) = 0
set_tid_address(0x3fd852aed8) = 1864
brk(NULL) = 0x316000
brk(0x318000) = 0x318000
mmap(0x316000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x316000
openat(AT_FDCWD, "/mnt/system/lib/libstdc++.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/mnt/system/usr/lib/libstdc++.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld-musl-riscv64.path", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/libstdc++.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
這個是 USB Peripheral 專用的 API ,如果是主機則是使用 USB Host API , USB 使用類似網路堆疊架構的方式傳輸資料,因此也會有實體層、資料連結層 及 網路層等等。至於 USB 所採用的是拓樸階梯型星狀拓樸 (tiered-star topologies) ,以 USB root-hub 為中心向外連結 hub 或是 USB Peripheral 。
上圖中的 Func 可以視為 USB 週邊裝置,而我們現在要觀察的是下圖的 USB 的網路堆疊,可以視為 OSI 網路堆疊的 Layer 1
~ Layer 3
。其中可以看到 Layer 1
是 USB 的實際硬體裝置,也就是我們的 USB 匯流排介面 ; Layer 2
則是會因為主機端跟裝置端有所不同,主機端位在這層的是 Host Controller Driver 而裝置端則是 USB Controller Driver 在運行,邏輯上連結它們的是 USB core 以及 Endpoint 0 ,其中兩者間可以互相傳輸封包資訊讓 USB core 對裝置進行設定 ; Layer 3
則是主機端的應用程式以及裝置端的 Gadget Driver ,這兩邊邏輯上是讓應用程式使用裝置端正常運行的 function 。
以我們 ADB 的例子來說, adb server
就是我們主機端的 Client SW , 而 functionfs
則是裝置端的 Function , 而 Pipe Bundle 則是維持 Endpoints 與應用程式的資訊傳輸。
在開始介紹實作前,先把一些重要的概念補充完。首先是 Gadget Driver ,這個是專門設定 USB 屬性以及運行方法的 driver , 這裡的方法指的是 Linux 核心已經內建好提供給 USB 裝置使用的功能,例如 Mass_Storage , RNDIS , FunctionFs 。
其中 functionfs 這項功能的目的為,將原先位在核心空間的 Endpoints 轉移到使用者空間使用,如此一來使用者空間的程式也可以透過 Endpoints 接收或傳輸資料。
最後是 Configfs , 它可以在使用者空間使用命令的方式告訴核心要新增、刪除、更動的核心物件,我們實作時的 gadget driver 就是透過 Configfs 來新增並改動設定並配置 functionfs 方法。
在原先 Milk-v Duo SDK 所建置產生的環境中, Linux 會在初始化時使用到與 Configfs 相關的腳本來配置 Gadget Driver , 但是配置的方法是 RNDIS , 因此我所要做的任務即是將方法改為 functionfs 以及將剛才交叉編譯完成的 adbd 放到 /bin
目錄並改寫腳本讓其運行。 (以下為型號 256m
的示範,僅有與型號相關的目錄及檔案有差異)
.config
資訊的檔案,在預設當中 Linux 使用者環境無法使用 Configfs 配置 functionfs 方法,因此我們必須增加以下配置。 CONFIG_USB_CONFIGFS_ACM=y
CONFIG_USB_CONFIGFS_F_UAC1=y
+CONFIG_USB_CONFIGFS_F_FS=y
/etc/uhubon.sh device >> /tmp/rndis.log 2>&1
-/etc/run_usb.sh probe rndis >> /tmp/rndis.log 2>&1
-/etc/run_usb.sh start rndis >> /tmp/rndis.log 2>&1
+/etc/run_usb.sh probe adb >> /tmp/rndis.log 2>&1
+/etc/run_usb.sh start adb >> /tmp/rndis.log 2>&1
if [ -f $ADBD_PATH/adbd ]; then
$ADBD_PATH/adbd &
fi
+ UDC=`ls /sys/class/udc/ | awk '{print $1}'`
+ echo ${UDC} >$CVI_GADGET/UDC
+ echo ${UDC} >$CVI_GADGET/UDC
$ sudo apt-get install android-sdk-platform-tools
$ adb devices
List of devices attached
0123456789 device
$ adb shell
/ #
確認異質的二個 RISC-V 處理器核可藉由 mailbox 搭配 RPMsg-Lite,進行通訊
RPMsg 是基於 AMP (Asymmetric Multi-Processing) 建立的通訊框架,與 OSI 七層網路模型相符,在 RPMsg 之下還有實體層的 IPC 機制例如 Mailbox 、 Shared Memory 以及資料連結層的 Virtio 機制。
Mailbox 是 2 個 RISC-V 核之間以 Linux 以及另一支遠端燒錄程式進行溝通的框架,由於燒錄程式沒有限定使用的框架,因此 Mailbox 會誕生出各種不同的實作方式。以我們 Milk-v Duo 來說,它所設計用來與 Linux 以及 FreeRTOS 溝通的框架被稱為 cvi_mailbox ,以下我們會以範例進行說明。
Milk-v Duo 核與週邊裝置互動的方式是採取 Memory-mapped I/O ,而 Mailbox 所使用的裝置也是如此,在 datasheet 中可以看到記憶體位址 0x01900000
~ 0x01900FFF
是提供給 Mailbox 裝置的暫存器使用。
其中最常使用到單元有兩個,分別是 mailbox_set_register 暫存器以及 mailbox_context buffer ,第一個 mailbox_set_register 是用來設定一些 flag ,例如觸發中斷。而 mailbox_context 則是用來存放傳遞資料的區域,例如命令以及變數的參數。
mailbox_test.c
這個是使用者空間的程式,它透過 ioctl 的方法對核心模組進行讀寫,這支程式會寫入一個給 mailbox 傳遞的結構變數 cmdqu_t
,而在核心模組接收倒寫入時,會將結構變數的成員放到對應的 Mailbox buffer 當中。
rtos_cmdqu.c
這個是使用 Mailbox 與 ThreadX 溝通的核心模組,同時它也是個 platform driver ,而配對的裝置即是 Mailbox 所使用的裝置,而這個裝置也有註冊 IRQ 所對應到的 ISR ,中斷處理常式的內容是接收 ThreadX 所傳送過來的 Mailbox 訊息,處理的有兩個部份分別是 Mailbox 裝置的暫存器以及 buffer 。
mbox_reg = (struct mailbox_set_register *) reg_base;
/* mailbox buffer context is send from rtos, clear mailbox interrupt */
mbox_reg->cpu_mbox_set[RECEIVE_CPU].cpu_mbox_int_clr.mbox_int_clr = valid_val;
// need to disable enable bit
mbox_reg->cpu_mbox_en[RECEIVE_CPU].mbox_info &= ~valid_val;
\({\uparrow}\) 裝置暫存器欄位,解除暫存器中斷。
\({\downarrow}\) 將 buffer 欄位清空,暫存到令一個結構變數中。
mailbox_context = (unsigned long *) (reg_base + MAILBOX_CONTEXT_OFFSET);//MAILBOX_CONTEXT;
cmdqu_t *cmdq, linux_cmdq;
cmdq = (cmdqu_t *)(mailbox_context) + i;
// copy cmdq context (8 bytes) to buffer ASAP ??
*((unsigned long long *) &linux_cmdq) = *((unsigned long long *)cmdq);
/* need to clear mailbox interrupt before clear mailbox buffer ??*/
*((unsigned long long *) cmdq) = 0;
再來是這個模組的 send 函式,它會將 ioctl 函式得到的 Mailbox buffer 內容傳送到 ThreadX 上,因此要對 Mailbox 進行一些設定。
// clear mailbox
mbox_reg->cpu_mbox_set[SEND_TO_CPU].cpu_mbox_int_clr.mbox_int_clr = (1 << valid);
// trigger mailbox valid to rtos
mbox_reg->cpu_mbox_en[SEND_TO_CPU].mbox_info |= (1 << valid);
mbox_reg->mbox_set.mbox_set = (1 << valid);
\({\uparrow}\) 裝置暫存器欄位,觸發中斷。
\({\downarrow}\) 將 buffer 欄位,改為我們需要的結構變數。
cmdqu_t *linux_cmdqu_t;
linux_cmdqu_t = (cmdqu_t *) mailbox_context;
int *ptr = (int *)linux_cmdqu_t;
*ptr = ((cmdq->ip_id << 0) | (cmdq->cmd_id << 8) | (cmdq->block << 15) |
(linux_cmdqu_t->resv.valid.linux_valid << 16) |
(linux_cmdqu_t->resv.valid.rtos_valid << 24));