# Linux 核心專題: 異質多核通訊機制 > 執行人: HenryChaing > ==[解說錄影](https://youtu.be/J177aLGKtfY)== > 未來會再更新第二版 > ==[解說錄影(二)](https://youtu.be/X2D4UcjAj8c?si=pYC1qcfz8v6UhxdQ)== > 完整專題講解 (包含 OpenAMP, Presentation) > ==[解說錄影(三)](https://youtu.be/iC6wci2ElvI)== ### Reviewed by jserv * 未能詳盡說明 USB gadget driver 和 adbd 之間的互動,應該由 USB stack 的角度去闡述 * 沒有探討「移植 ThreadX」的細節,也就是說中斷處理、計時器中斷、和 RISC-V 相關的硬體初始化程式碼 ### Reviewed by `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 的運算資源有限。 ### Reviewed by `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 當中輸出。 ### Reviewed by `RealBigMickey` 是否考慮過 endian 問題? Linux 和 ThreadX 預設 endian 一樣嗎?如果日後換成不同平台(如 big-endian),封包格式會不會出錯? $\to$ Virtio 會針對週邊裝置在裝置被註冊時,設定這裝置為 big/little edian ,隨後在存取共享記憶體時,就會依照這個屬性去逐位元組讀取內容,函式實作內容可參見 [virtio_config.h](https://elixir.bootlin.com/linux/v6.12.6/source/include/linux/virtio_config.h#L344),這是 virtqueue 在存取共享記憶體時的方法。 ## 專題 Repository 以下是我所用到的 repository ,可以從 git log 看到相關的開發紀錄。 ::: success - [ ] [Linux kernel](https://github.com/HenryChaing/duo-buildroot-sdk-v2) - [ ] [ThreadX](https://github.com/HenryChaing/ThreadX-to-RISC-V64) - [ ] [Linux user space](https://github.com/HenryChaing/duo-examples) ::: ## 任務說明 本專題嘗試在物美價廉的 [Milk-V Duo](https://milkv.io/zh/duo) 硬體上面,利用其異質多核的 RISC-V,運作 Linux 核心和 [ThreadX](https://github.com/eclipse-threadx/threadx),並利用 [OpenAMP](https://github.com/OpenAMP/open-amp) 規範的 [RPMsg](https://github.com/nxp-mcuxpresso/rpmsg-lite),建立異質多核間的通訊。 ## TODO: 移植 ThreadX 到 Milk-V Duo > 針對 [Milk-V Duo](https://milkv.io/zh/duo) (及其 256M 的硬體型號) > [移植 Eclipse ThreadX RTOS 到 RISC-V64 架構](https://github.com/saicogn/ThreadX-to-RISC-V64),驗證平台為 Milk-V Duo,ThreadX 執行無 MMU 的小核 C906L,Linux 運行於大核 C906B。 $\to$ 驗證 [saicogn/threadx](https://github.com/saicogn/threadx) :::danger 避免非必要的 [backtick](https://en.wikipedia.org/wiki/Backtick),後者用於程式碼、特別的數值,或有工程意義的位元組序列。反之,保持行文的流暢。 > 了解,下次會慎用 backtick ::: 在 Milk-v Duo 當中,小核 C906L 預設運行的作業系統為 FreeRTOS , 但是基於我們隨後要加上的 RPMsg-lite 以及基於兩個核通訊而誕生的應用程式,在 Milk-v Duo 只有 64MB 的主記憶體之情況下,我們必須使用佔據記憶體空間較少並且利於通訊的作業系統,而這就是我們採用 Eclipse Threadx RTOS 作為我們小核作業系統的原因。 在實作方面,我們只須將 [ThreadX-to-RISC-V64](https://github.com/saicogn/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 並完成編譯。 * milkvsetup..sh ```diff - 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](https://github.com/sysprog21/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` 秒。 <s>因此 ThreadX 的運行在 Milk-v Duo 上算是穩定。</s> :::danger 用語要精準,什麼「穩定」? ::: ## TODO: 將 ADB 移植到 Linux > Linux 運行於大核 C906B,在其上移植 (Android) ADB,確保 USB gadget driver 運作符合預期,並在 Microsoft Windows 和 GNU/Linux 上驗證 ### ADB 簡介 :::danger 改進漢語描述 ::: ADB 的全名為 [Android Debug Bridge](https://developer.android.com/tools/adb),原先是針對 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 。 ![Screenshot from 2024-06-21 15-37-12](https://hackmd.io/_uploads/HkjEiFcU0.png) ### adbd 交叉編譯 adbd 的全名為 android debug bridge deamon ,是一支在目標裝置背景執行的程式。不過在 Milk-v Duo 上執行前會遇到一個問題,也就是 Milk-v Duo 的大核 C906B 是 RISC-V 64 位元的架構,因此若要獲得 adbd 執行檔就必須先經過交叉編譯。 首先我採用的是 [riscv-gnu-toolchain](https://github.com/riscv-collab/riscv-gnu-toolchain) 這個工具鏈,並且也成功編譯出了執行檔,但是交叉編譯出來的執行檔卻無法在大核 C906B 上執行。後來歸納出原因在於沒有使用 Milk-v Duo 所提供的 toolchain ,因為這個工具鏈當中有包含選項 (option) 專門用於編譯 C906B 的執行檔,然而 riscv-gnu-toolchain 卻沒有支援這個編譯選項。 編譯及連結階段所使用的選項: ```shell 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](https://github.com/milkv-duo/duo-sdk/tree/master/riscv64-linux-musl-x86_64) 來進行交叉編譯,與第一次編譯較為不同的地方在於,因為採用的是 musl (讀作: muscle) 而非 GNU 的工具鏈,所以有一些函式庫內容並沒有支援,其中 musl libc 同 glibc 也是標準 C 函式庫的實作,但它更注重於執行檔的大小、靜態連結<s>鏈結</s> 的支援 以及 Race condition 等並行程式問題的排除。 :::danger 強調相互結合的意境時,譯作「連結」,例如「動態連結函式庫」(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](https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html?fbclid=IwZXh0bgNhZW0CMTEAAR1loIKomefuaar-DrLakivn9iGsenvKLbsuwmpCcGy1htVDk6kMxagd-OY_aem_8Sufg-NtyfP-5MmqmKn_Jg) 。再來還有 musl libc 未支援的 C 標準函式庫函式,也在 adbd 的編譯過程中遇到。而解決辦法是聽照老師從 newlib/glibc 中找到該方法的實作,並將其加入到專案當中,解決這項難題。 接著還面臨與 libc 無關的 OpenSSL 函式庫使用,也當然 musl libc 沒有支援這項函式庫。因此我後來在 OpenSSL 官方網站找到了 [Git repository](https://github.com/openssl/openssl) 並先對該函式庫進行交叉編譯。原先以為將標頭檔及相關的 C 程式實作放入對應的目錄即可,但是到了連結階段發現缺乏對應的 lib , 例如 `libcrypto` 、 `libssl` ,原來這些 lib 也是出自 OpenSSL 並且就是最後編譯出來的靜態函式庫 (static library),附檔名為 `.a` 並由 linker 與其他目標檔案組成執行檔。 再來還使用到了 GLib 函式庫, Glib 結合了多項平台底層函式的實作,因此 Glib 可以被不同的作業系統引用並編譯,但是在編譯函式庫的過程中遇到了不少問題,因此後來決定將使用到 Glib 函式庫的部份改為 Linux 標準 C 函式庫的方法實作。在此除了需要查詢 [GLib manual](https://docs.gtk.org/glib/) 關於函式的用途,也需要使用到 `man` 命令查詢系統的標準 C 函式庫實作。 不過之所以改為 Linux 所支援的 C 實作,原因是在下達以下命令後,發現僅有一個檔案使用到這個關鍵的 Glib 標頭檔。關於 `grep` 的介紹,在 `man grep` 的手冊資訊當中可以得知 `-r` 是遞迴找對應目錄中所有檔案的內容,而 `-v` 則是排除在該目錄搜尋到的結果(以此例來說 sysroot 的交叉編譯工具鏈的環境目錄,與 adbd 無關)。 ```shell $ 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 Gadget API > 參考: [USB Gadget API for Linux](https://docs.kernel.org/driver-api/usb/gadget.html) 、 [USB (Universal Serial Bus)](https://wiki.csie.ncku.edu.tw/embedded/USB) 這個是 USB Peripheral 專用的 API ,如果是主機則是使用 [USB Host API](https://docs.kernel.org/driver-api/usb/usb.html) , USB 使用類似網路堆疊架構的方式傳輸資料,因此也會有實體層、資料連結層 及 網路層等等。至於 USB 所採用的是拓樸階梯型星狀拓樸 (tiered-star topologies) ,以 USB root-hub 為中心向外連結 hub 或是 USB Peripheral 。 ![tiernetwork](https://hackmd.io/_uploads/SJUVAJ2UC.png) 上圖中的 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 與應用程式的資訊傳輸。 ![usb host_device ](https://hackmd.io/_uploads/B1MYFe280.png) #### Gadget Driver 、 Configfs 、 Endpoint 在開始介紹實作前,先把一些重要的概念補充完。首先是 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` 的示範,僅有與型號相關的目錄及檔案有差異) * [cvitek_cv1812cp_milkv_duo256m_sd_defconfig](https://github.com/milkv-duo/duo-buildroot-sdk/blob/develop/build/boards/cv181x/cv1812cp_milkv_duo256m_sd/linux/cvitek_cv1812cp_milkv_duo256m_sd_defconfig) 這個是 SDK 用來管理 Linux 核心 `.config` 資訊的檔案,在預設當中 Linux 使用者環境無法使用 Configfs 配置 functionfs 方法,因此我們必須增加以下配置。 ```diff CONFIG_USB_CONFIGFS_ACM=y CONFIG_USB_CONFIGFS_F_UAC1=y +CONFIG_USB_CONFIGFS_F_FS=y ``` * [usb-rndis.sh](https://github.com/milkv-duo/duo-buildroot-sdk/blob/develop/device/milkv-duo256m/overlay/mnt/system/usb-rndis.sh) 這個檔案會運行使用 Configfs 來新增並設定 Gadget driver 的 shell script , 不過這個檔案預設啟動的功能為 RNDIS ,我們將其改為 adb 參數來啟用 functionfs 。 ```diff /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 ``` * [run-usb.sh](https://github.com/milkv-duo/duo-buildroot-sdk/blob/develop/device/common/br_overlay/etc/run_usb.sh) 這應該是廠商設計 SDK 時的疏失,在啟動 Gadget driver 方法的配置前,我們必須配對一個 USB Controller Driver 模擬的裝置才可以運作,但唯有 functionfs 的配置少了這項動作。 ```diff 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 ``` * android-tools 在 SDK 的 buildroot 工具當中其實有提供關於 adbd 執行檔的安裝選項,若要省去交叉編譯可以選擇這項方法。 ![Screenshot from 2024-06-22 17-51-43](https://hackmd.io/_uploads/HkCWtf2IA.png) #### 在 Windows / Ubuntu 平台上驗證 ADB #### Windows > 參考: [Windows 安裝 adb 的方式](https://home.gamer.com.tw/artwork.php?sn=5106509) #### Ubuntu ```bash $ sudo apt-get install android-sdk-platform-tools ``` #### 展示結果 ```bash $ adb devices List of devices attached 0123456789 device $ adb shell / # ``` ## TODO: 引入 RPMsg-Lite 到 Milk-V Duo > 確認異質的二個 RISC-V 處理器核可藉由 mailbox 搭配 RPMsg-Lite,進行通訊 ![image](https://hackmd.io/_uploads/Sy12-6t8R.png) ### RPMsg RPMsg 是基於 AMP (Asymmetric Multi-Processing) 建立的通訊框架,與 OSI 七層網路模型相符,在 RPMsg 之下還有實體層的 IPC 機制例如 Mailbox 、 Shared Memory 以及資料連結層的 Virtio 機制,最後是對應到傳輸層的 RPMsg Layer。 ![image](https://hackmd.io/_uploads/Bkw1KJzeex.png) ### Mailbox Controller > 參考: [The Common Mailbox Framework](https://docs.kernel.org/driver-api/mailbox.html) Mailbox 是讓多個核可以互相觸發中斷的硬體元件,以 Milk-V 來說可以讓 2 個 RISC-V 的核互相觸發中斷。一般的 Mailbox 會搭配 [mailbox channel ops](https://github.com/torvalds/linux/blob/master/include/linux/mailbox_controller.h#L46) 來設計驅動程式,但是 Cvitek 則是簡化設計成一個字元驅動程式。 #### Milk-v Duo Mailbox > 參考: [Milk-v Dio datasheet](https://github.com/milkv-duo/duo-files/tree/main/duo/datasheet) Milk-v Duo 核與週邊裝置互動的方式是採取 Memory-mapped I/O ,而 Mailbox 所使用的裝置也是如此,在 datasheet 中可以看到記憶體位址 `0x01900000` ~ `0x01900FFF` 是提供給 Mailbox 裝置的暫存器使用。 其中最常使用到單元有兩個,分別是 `mbox_reg` 暫存器以及 `mailbox_context` buffer ,第一個 `mbox_reg` 可以透過設定對應的控制暫存器,來達到對應的硬體操作,例如觸發中斷。而 `mailbox_context` 則是用來存放傳遞資料的區域,例如命令以及變數的參數。 #### Milk-v Duo Mailbox 範例解說 - [ ] **mailbox_test.c** 這個是使用者層級的程式,它透過 ioctl 的方法對核心模組進行讀寫,這支程式會寫入一個給 mailbox 傳遞的結構變數 `cmdqu_t`,並且在 ioctl 系統呼叫之後,將結構變數的成員放到對應的 Mailbox buffer 當中。 - [ ] **rtos_cmdqu.c** 這個是 Milk-V 提供的 mailbox 驅動程式,而這個裝置也有註冊 IRQ 所對應到的 ISR ,中斷服務常式的 top half 是讀取緩衝區內容以及清除中斷 , bottom half 是喚醒在 wait_queue 的任務,讓任務將得到的結構體傳回使用者層級。 * Interrupt Service Routine ```c 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 欄位清空,暫存到令一個結構變數中。 ```c 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; ``` * Mailbox Send ```c // 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 欄位,改為我們需要的結構變數。 ```c 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)); ``` ### Mailbox Client (RPMSg Platform Driver) 首先 RPMsg 這個框架並沒有對應到實際的硬體元件 (只有參照其他硬體元件),因此無法像其他裝置被特定的 bus 探測並初始化,因此我們這裡採用 Linux 提供的 Platform 機制讓 RPMsg 框架也可以參照到裝置樹當中的其他硬體元件,例如共享記憶體以及 mailbox 。 ![image](https://hackmd.io/_uploads/r1PrSkGxxe.png) - [ ] cvi_rpmsg.c 這個是參考 [linux-fslc](https://github.com/Freescale/linux-fslc) 這個由 Freescale 公司 (現為 NXP) 所維護的 Linux 分支所實作的 [imx_rpmsg.c](https://github.com/diegosueiro/linux-fslc/blob/4.9-1.0.x-imx/drivers/rpmsg/imx_rpmsg.c) ,它本質上是一個 platform driver ,除了擁有初始化 RPMsg 框架要使用的硬體元件之函式外,它還會註冊 virtio device 讓 virtio 的功能得以啟用。 原先的 imx_rpmsg.c 會取得裝置樹當中關於 MU 相關暫存器以及緩衝區位置、時脈週期、中斷請求號碼等資訊,而這裡則改成只需要知道 vring 記憶體位址區間 、 mailbox 的中斷請求號碼,關於 vring 的部份會在下一章作介紹。 * 中斷服務常式註冊,需要裝置樹中的中斷請求號碼 ```c np_mu = of_find_compatible_node(NULL, NULL, "cvitek,rtos_cmdqu"); if (!np_mu) pr_info("Cannot find MU-RPMSG entry in device tree\n"); irq = of_irq_get(np_mu, 0); int err = request_irq( irq , imx_mu_rpmsg_isr, 0, "mailbox", dev); if (err) { pr_err("fail to register interrupt handler: %d \n", err); return -1; } ``` * 共享記憶體初始化,存放 vring 相關資料。 ```c resource_size_t size = platform_get_resource(pdev, IORESOURCE_MEM, 0); ``` ### Virtio 這個機制的目的是讓 Host 端可以與不同性質的裝置傳輸訊息,以我們這次專題而言這個裝置就是遠端運行 ThreadX 的處理器。這裡會逐一介紹 virtio 所使用的資料結構,以及它搭配了哪一些函式來傳輸以及接收訊息。 ![image](https://hackmd.io/_uploads/HyEDryGegx.png) - [ ] struct vring_virtqueue 這個資料結構將會包含接下來提到的 *vring* 以及 *virtqueue* ,可以把它想像成是 virtio 的 channel 。這個結構的成員紀錄了這個 channel 的性質,包括是否使用 DMA 傳輸資料、最後一個讀取到的封包編號以及傳輸會呼叫的函式 *notify* 。 - [ ] struct virtqueue 紀錄 channel 的基本資料,例如名字、編號,另外一個重要的是收到封包後的對應函式 *callback* ,這個會與 *notify* 一同介紹。 - [ ] struct vring 這個是 channel 儲存傳輸資料的地方,與前兩個結構變數使用 kzalloc 這個記憶體管理 API 不同, *vring* 的空間是來自 `platform_get_resource` 由 platform device 擁有的記憶體空間,這個空間不會被核心映射到虛擬記憶體區塊,因此可以給這個 device 獨立使用。 - [ ] struct vring_virtqueue 結合 *virtqueue* 以及 *vring* 結構體。 ```graphviz digraph G { subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="virtqueue | {<prev> descripter area | <next> avail ring | <next> used ring }"]; label="vring_virtqueue" } } ``` - [ ] notify / callback 首先是 `virtqueue_notify` 這個函式顧名思義是要通知 virtio 裝置,例如 Linux 將資料放進 avail vring 後,必須讓遠端知道資料已經可以使用,這時就會使用 *notify* 函式讓遠端得知。接著是當對方接收到訊息時, *virtqueue* 會有對應的中斷服務常式 `vring_interrupted` 來處理 bottom half 的內容,在這個函式當中會因為 transfer/recieve 的不同而呼叫不同的 *callback* 函式。 ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Linux |<ht0> tx |<ht1> tx_ack |<ht2> "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="virtqueue | <prev> avail ring | <next> used ring"]; label="vring_virtqueue_0" } map:ht0 -> hn1:prev [label = "notify", color = "red"] hn1:next -> map:ht1 [label = "notified", color = "blue"] // map_1:ht1 -> hn1:next } ``` - [ ] virtqueue_add / virtqueue_get_buf `virtqueue_add` 會將資訊放入 *vring* 當中,以 Linux 來說 `virtqueue_add_inbuf` 會更新 *vring* 當中 desc 以及 avail 的內容,讓遠端知道可以讀取幾個 buffer 。 `virtqueue_get_buf` 則是會把資料取出,並將其傳到傳輸層 rpmsg 對應的 *endpoint* 。 - [ ] Virtio 資料傳遞流程 > 參考: Virtio-spec ([2.6](https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html#x1-270006), [2.7](https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html#x1-350007)) 這裡會先從 Host 端的 Linux 談起,在 Virtio 驅動程式被 probe 之後,它會先建立 *virtqueue* 並且會連同 *vring* 一同初始化。在這之後有個 Linux 必須完成的任務,也就是 rpmsg buffer 的配置,這些 buffer 就是用來存放雙方要互相傳遞的原始資料,可以想成是網路封包要存放的空間。 在前面介紹中有談到重要的 *vring* ,這裡要來繼續探討它。 *vring* 本質上是 Linux 核心當中的 [ring buffer](https://en.wikipedia.org/wiki/Circular_buffer) ,並且會被分成三個區塊作使用,分別是 **descripter area** 、 **avail ring** 以及 **used ring** 。其中第一個區塊 **descripter area** 被用來管理 rpmsg buffer ,可以透過紀錄的 rpmsg buffer 實體位址、長度以及是否與下個區塊連續使用等性質,管理目前 Virtio 對 rpmsg buffer 的使用情形。 再來是 **avail ring** 以及 **used ring** ,這兩個區塊會一同紀錄 transfer / recieve (tx/rx) 對 rpmsg buffer 的使用情況。以 Linux 來說,如果今天 Linux 要傳輸訊息給 remote 端,我們會先將訊息紀錄到 rpmsg buffer 當中並一同更新 desripter area 的管理紀錄,當以上兩者更新後,我們就可以讓 avail ring 紀錄所使用到的 rpmsg buffer ,而紀錄的方式就是儲存 descripter area 對應的 entry ,並且依據現在 avail ring 紀錄到的位置更新 idx 值。 以下我畫了示意圖來呈現以上的說明,先從 descipter area 開始談起,其中 addr 、 length 欄位會用來存放 rpmsg buffer 的實體位址,而 flags 以及 next 欄位則會呈現這個區間是否連續。 再來是 avail ring ,其中 idx 欄位用來紀錄現在環狀佇列 tail 所指定到的位置,而 ring 欄位則是紀錄這個元素所對應到的 desripter entry 。當 avail ring 加入新的 desripter entry 時, idx 所紀錄的計數也會隨之更新。 ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Descripter Area |{<prev> [addr] | <next> [length]} |{<line_0_id> 0x7d..0000 | <next> 2000}|{<prev> 0x7d..2000 | <next> 2000} |{<line_2_id> 0x7d..4000 | <next> 2000} "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_0 { color=lightgrey; node [shape=record]; hn0 [label="<id> idx : z |{ <ring_0> ring[0].idx : 0 | <ring_0_len> ring[0].len : 2000 }|{ <ring_1> ring[1].idx : 6 | <ring_1_len> ring[1].len : 2000 }| ... "]; label="used ring" } subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="<id> idx : x | <ring_0> ring[0] : 2 | <ring_1> ring[1] : 5 | ... "]; label="avail ring" } hn0:ring_0_len -> map:line_0_id[ color = "red"] hn1:ring_0 -> map:line_2_id [color = "blue"] // map_1:ht1 -> hn1:next } ``` - [ ] RPMsg Communication Flow > 規範: [OpenAMP Project Documentation (RPMsg Protocol Layers)](https://openamp.readthedocs.io/en/latest/protocol_details/rpmsg_comms.html) 這裡會從 RPMsg 的層面去探討資料如何傳遞,也會補充上個章節關於 remote 以及 host 使用 avail / used ring 的詳細規範。在 host 傳遞給 remote 的流程詳細可參照下圖或是上方參考連結,首先 host 會去檢查 used ring 當中是否有多的 buffer 可供使用,接著會將這個 buffer 從 used ring 搬移到 avail ring (實際上只是更新兩個 ring 的紀錄, used ring 作 dequeue 而 avail ring 進行 enqueue) ,然後會將資料寫進 rpmsg buffer 並存放到 desc entry 所指定的 addr ,最後會觸發中斷 *notify* 讓 remote 知道有來至 host 的資料。 ![image](https://hackmd.io/_uploads/BJdNaApJeg.png) - [ ] Linux 傳遞封包給 ThreadX * 示意圖: ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Linux |<ht0> tx |<ht1> tx_ack |<ht2> |<ht3> |<ht4> "]; map_1 [label=" threadx |<ht0> rx |<ht1> rx_ack |<ht2> |<ht3> |<ht4> "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="virtqueue | <prev> avail ring | <next> used ring"]; label="vring_virtqueue_0" } subgraph cluster_2 { color=lightgrey; node [shape=record]; hn1 [label="virtqueue | <prev> avail ring | <next> used ring"]; label="vring_virtqueue_1" } map:ht0 -> hn1:prev hn1:next -> map:ht1 hn1:prev -> map_1:ht0 hn1:next -> map_1:ht1 [dir=back] } ``` * (phase 1) 初始狀態: ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Descripter Area |{<prev> [Buffer] | <next> [Length]} |{<line_0_id> 0x7d..0000 | <next> 2000}|{<prev> 0x7d..2000 | <next> 2000} |{<line_2_id> 0x7d..4000 | <next> 2000} "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_0 { color=lightgrey; node [shape=record]; hn0 [label="<id> idx : 1 |{ <ring_0> ring[0].idx : 0 | <ring_0_len> ring[0].len : 2000 }|{ <ring_1> ring[1].idx : 6 | <ring_1_len> ring[1].len : 2000 }| ... "]; label="used ring" } subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="<id> idx : 1 | <ring_0> ring[0] : 2 | <ring_1> ring[1] : 5 | ... "]; label="avail ring" } hn0:ring_0_len -> map:line_0_id[ color = "red"] hn1:ring_1 -> map:line_2_id [color = "blue"] // map_1:ht1 -> hn1:next } ``` * (phase 2) Linux 要將封包寫入到 buffer 當中必須經過下列步驟,先呼叫 *virtqueue_get_buf* 將 used ring 的 buffer 取出並將資料寫進 buffer ,隨後呼叫 *virtqueue_add_buf* 將 buffer 加入 avail ring 當中: ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Descripter Area |{<prev> [Buffer] | <next> [Length]} |{<line_0_id> 0x7d..0000 | <next> 2000}|{<prev> 0x7d..2000 | <next> 2000} |{<line_2_id> 0x7d..4000 | <next> 2000} "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_0 { color=lightgrey; node [shape=record]; hn0 [label="<id> idx : 1 |{ <ring_0> ring[0].idx : 0 | <ring_0_len> ring[0].len : 2000 }|{ <ring_1> ring[1].idx : 6 | <ring_1_len> ring[1].len : 2000 }| ... "]; label="used ring" } subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="<id> idx : 2 | <ring_0> ring[0] : 2 | <ring_1> ring[1] : 5 | <ring_2> ring[2] : 0 | ... "]; label="avail ring" } // hn0:ring_0_len -> map:line_0_id[ color = "red"] hn1:ring_1 -> map:line_2_id [color = "blue"] hn1:ring_2 -> map:line_0_id [color = "red"] // map_1:ht1 -> hn1:next } ``` * (phase 3) Linux 觸發中斷 *virtqueue_kick* 通知 ThreadX 。 * (phase 4) 由於 ThreadX 之 ISR 會觸發 RPMsg 機制接收封包,因此會有下列步驟需要完成,首先 *virtqueue_get_buffer* 會將 avail ring 可取用之 buffer 讀出,並將該 buffer 之資料轉成封包給 *ept callback* 處理。 ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Descripter Area |{<prev> [Buffer] | <next> [Length]} |{<line_0_id> 0x7d..0000 | <next> 2000}|{<prev> 0x7d..2000 | <next> 2000} |{<line_2_id> 0x7d..4000 | <next> 2000} "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_0 { color=lightgrey; node [shape=record]; hn0 [label="<id> idx : 1 |{ <ring_0> ring[0].idx : 0 | <ring_0_len> ring[0].len : 2000 }|{ <ring_1> ring[1].idx : 6 | <ring_1_len> ring[1].len : 2000 }| ... "]; label="used ring" } subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="<id> idx : 2 | <ring_0> ring[0] : 2 | <ring_1> ring[1] : 5 | <ring_2> ring[2] : 0 | ... "]; label="avail ring" } // hn0:ring_0_len -> map:line_0_id[ color = "red"] hn1:ring_1 -> map:line_2_id [color = "blue"] hn1:ring_2 -> map:line_0_id [color = "red"] // map_1:ht1 -> hn1:next } ``` * (phase 5) 隨後 ThreadX 呼叫 *virtqueue_add_buffer* 將位址使用完之 buffer 加入 used ring 。 ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" Descripter Area |{<prev> [Buffer] | <next> [Length]} |{<line_0_id> 0x7d..0000 | <next> 2000}|{<prev> 0x7d..2000 | <next> 2000} |{<line_2_id> 0x7d..4000 | <next> 2000} "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_0 { color=lightgrey; node [shape=record]; hn0 [label="<id> idx : 2 |{ <ring_0> ring[0].idx : 0 | <ring_0_len> ring[0].len : 2000 }|{ <ring_1> ring[1].idx : 6 | <ring_1_len> ring[1].len : 2000 }|{ <ring_2> ring[2].idx : 5 | <ring_2_len> ring[2].len : 2000 }| ... "]; label="used ring" } subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="<id> idx : 2 | <ring_0> ring[0] : 2 | <ring_1> ring[1] : 5 | <ring_2> ring[2] : 0 | ... "]; label="avail ring" } // hn0:ring_0_len -> map:line_0_id[ color = "red"] hn0:ring_2_len -> map:line_2_id [color = "blue"] hn1:ring_2 -> map:line_0_id [color = "red"] // map_1:ht1 -> hn1:next } ``` * (phase 6) Linux 傳送封包至 ThreadX 流程結束。 - [ ] ThreadX 傳遞訊息給 Linux * 示意圖: ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label=" threadx |<ht0> tx |<ht1> tx_ack |<ht2> |<ht3> |<ht4> "]; map_1 [label=" Linux |<ht0> rx |<ht1> rx_ack |<ht2> |<ht3> |<ht4> "]; node[shape=none] // null1 [label=NULL] // null2 [label=NULL] subgraph cluster_1 { color=lightgrey; node [shape=record]; hn1 [label="virtqueue | <prev> used ring | <next> avail ring"]; label="vring_virtqueue_1" } map:ht0 -> hn1:prev hn1:next -> map:ht1 hn1:prev -> map_1:ht0 hn1:next -> map_1:ht1 [dir=back] } ``` * 流程與 Linux 傳送封包至 ThreadX 相同,將 avail ring 與 used ring 之描述對調即可。 ### RPMsg Layer 這個層次會對應到網路堆疊當中的傳輸層 (Transport Layer) ,因此不同的埠號會對應到不同的功能,首先我們會介紹幾個專有名詞,分別是 *rpmsg channel* 以及 *rpmsg endpoint (endpoint)* ,這些概念都與網路習習相關。 - [ ] rpmsg channel 這個對應到主機間在傳輸層的邏輯連線,當有 remote 端與主機端建立連線時,就會形成這個通道,這個通道紀錄著 remote 端的名稱以及兩邊的地址。至於建立連線的方法,這裡會說明到連線時建立的交握,下圖是若 Linux 以及 Zephyr 同時被載入時的交握過程,如果是使用 remoteproc 載入則會略過 Zephyr 等待連線的過程, ThreadX 也適用此流程圖。 其中有個重要的步驟是遠端要發起 *name service* 請求,這個步驟等同是建立傳輸層的邏輯連線,這個宣告會通知主機端關於遠端相關的資訊,包含裝置名稱以及 *endpoint* 的號碼,並且會讓已經註冊的 rpmsg driver 進行 probe 。 ![image](https://hackmd.io/_uploads/H1aWCkAkgl.png) - [ ] rpmsg endpoint *Endpoint* 是這個層次用來接收訊息的個體 (entity) ,每次 RPMsg 傳遞訊息時都必須指定接收端的 *endpoint* 。並且 *endpoint* 也會綁定到特定的服務 (callback) ,每當訊息傳遞到指定的 *endpoint* 時特定的服務就會被觸發,而這樣的模式也方便讓 Linux 去改變 ThreadX 的行為。 ![image](https://hackmd.io/_uploads/H1uHJLJxee.png) - [ ] RPMsg 封包 排除 user payload 的部份,其他區塊屬於 RPMsg Header 。首先可以看到 source / destination address 這個紀錄的是 *endpoint* 的編號,再來 length 則是紀錄 payload 的長度。 flags 在規格書則指出留給開發者自行定義。 ![image](https://hackmd.io/_uploads/SkBM9Wflge.png) ### Demo (ping-pong) 這裡呈現的硬體會由 Milk-V 轉為 KV260 ,在新的硬體延續了之前的異質多核架構,並且即時系統轉為了 Zephyr 。在實驗場景的設計下,兩個系統在傳輸的 Endpoint 會指定為同一者,並且在收到訊息後會立即傳遞給另一個系統,以此來達到乒乓的效果。 實驗設計是將共同變數 X 來回傳遞在 Linux 以及 Zephyr 之間,並且後者會將計數加一再傳回前者。因此結果會同解說錄影(二)的 [31:10](https://youtu.be/X2D4UcjAj8c) 時間段,詳細可看解說錄影。 ::: success 代辦事項: 1. THreadX 藉由 [primary](https://github.com/milkv-duo/duo-buildroot-sdk-v2/blob/develop/fsbl/plat/cv181x/bl2/bl2_opt.c) or secondary stage 載入。 2. 如何判斷 ThreadX 有正常開啟。 3. 有沒有 IPI 的範例。 [(SDK-v2)](https://github.com/milkv-duo/duo-buildroot-sdk-v2/blob/develop/cvi_mpi/modules/isp/cv181x/isp/src/isp_mailbox.c) 4. 範例 IPI 程式碼是否合理,正常運作。 () 5. 同時發 IPI (Multiplexing) 。 6. ThreadX 發送的程式碼。 為什麼會有問題。 7. 至少把 ping-pong 測試出來。 8. 現在 OPenAMP 範例要整合到 Milk-V 要補那一些內容。 9. Remoteproc , fsbl load 流程。 ::: ### 環境建置 step by step 教學 編譯的環境按照官方要求 1.作業系統 `Ubuntu 22.04.5` 2. 處理器架構 `amd64` (俗稱 x86_64) #### ADB & Remoteproc Flow: - [ ] 下載 buildroot-sdk repository 。 ``` $ git clone --recurse-submodules https://github.com/HenryChaing/duo-buildroot-sdk-v2.git ``` - [ ] 啟動 buildroot 編譯流程 。 ``` $ ./build.sh lunch #(choose 3.) ``` - [ ] 使用 shell script 函式編譯單一部件 。 ``` $ ./source step_by_step_compilation.sh $ build_rtos && build_uboot && pack_sd_image ``` - [ ] 使用燒錄器(如 balenaEtcher)燒錄以下檔案。 ``` ./install/soc_sg2002_milkv_duo256m_musl_riscv64_sd/milkv-duo256m-musl-riscv64-sd.img ``` - [ ] 將開發環境所需的小核執行檔以及 Remoteproc shell script 複製到 rootfs。 ``` $ cp ./remoteproc.sh ./threadx/cvitek/install/bin/cvirtos.elf /media/$USER/rootfs/root/ ``` - [ ] 將 Milk-V 開機,以及下載主機端 adb tool ,開機完成即可連線。 ``` $ sudo apt-get -y install android-tools-adb $ adb shell ``` - [ ] 啟用 Rmoteproc 功能, load 執行擋到小核。 ``` [Target]$ sh remoteproc.sh ``` #### RPMsg Flow (based on previous tutorial) - [ ] 下載使用者層級應用程式開發環境之 repository。 ``` $ git clone https://github.com/HenryChaing/duo-examples.git ``` - [ ] 啟動交叉編譯環境。 ``` $ source ./envsetup.sh #(choose 2, and then choose 2) ``` - [ ] 編譯裝置需要的應用程式。 ``` $ cd rpmsg_userspace && make all ``` - [ ] 將編譯後的應用程式轉移到裝置 rootfs。 ``` $ cp mailbox_pingpong mailbox_led /media/$USER/rootfs/root/ ``` - [ ] 將 Milk-V 開機並執行以下應用程式。 ``` [Target]$ sh remoteproc.sh [Target]$ ./mailbox_led & [Target]$ ./mailbox_pingpong ``` #### 教學中的 shell script - [ ] step_by_step_compilation.sh ```sh source device/milkv-duo256m-musl-riscv64-sd/boardconfig.sh source build/envsetup_milkv.sh defconfig sg2002_milkv_duo256m_musl_riscv64_sd ``` - [ ] remoteproc,sh ```sh mkdir /lib/firmware/ cp cvirtos.elf /lib/firmware echo cvirtos.elf > /sys/class/remoteproc/remoteproc0/firmware echo start > /sys/class/remoteproc/remoteproc0/state sleep 1 echo stop > /sys/class/remoteproc/remoteproc0/state echo start > /sys/class/remoteproc/remoteproc0/state ``` ### RPMsg 機制可預期性分析 #### 機制 UML 示意圖 #### ThreadX 機制說明 > 摘錄與可預期性相關的內容,完整 CPU 排程可參照 ThreadX [ch3.](https://github.com/eclipse-threadx/rtos-docs/blob/main/rtos-docs/threadx/chapter3.md) ThreadX 因為要處理硬即時的事件,因此屬於可搶佔且按照優先權進行排程,當最高優先權的執行緒有數個時,會採取 First in First Out (FIFO) 排程策略。當有中斷發生時,會將目前的 context 存入堆疊當中,並切換至中斷模式執行 Interupt Service Routine (ISR) ,當處理完 ISR 後才會再重新排程,挑選最高優先權的執行緒。 (以上有提到的可排程執行緒皆為 *Ready State* ) 在上述的基礎之上,接著要介紹與這次專題應用程式(ThreadX 對 Multi-thread Process 的稱呼)的內容,我們共有兩個執行緒在處理 RPMsg 的內容,執行緒的名稱分別為 Recieve-Thread 以及 Send-Thread ,功能如其名在接收以及發送訊息封包。而使用接收以及發送的數個執行緒,我們統一稱為 Application-Thread ,這些執行緒的優先權會比 Recieve-Thread 以及 Send-Thread 的優先權來的低,因此我們得到了如下執行緒以及優先權列表。 * Recieve-Thread, Send-Thread (priority: high) * Application-Thread (priority: low) 因此我的系統確保了一件關鍵的事,整個應用程式會優先執行 RPMsg 機制的內容,當有執行緒要使用 RPMsg 發送功能,原先處於 *Suspend state* 的發送執行緒會被喚醒到 *Ready state* ,此時發送執行緒會處於可排程且最高優先權,因此會觸發搶佔成為 *Running state* ,完成發送後再回到 *Suspend state* 。 接收執行緒也是相同的道理,首先接收時會優先收到 mailbox 觸發的 ISR , mailbox ISR 的設計當中最後會將接收執行緒轉移到 *Running state* ,因此 ISR 結束時可排程的最高優先全執行緒會是接收執行緒,執行完內容後再回到 *Suspend state* 。 #### 案例說明 ```c typedef struct data_form { uint32_t parameter; uint32_t result; } THE_DATA, *THE_DATA_PTR; /* thread2: standalone*/ static void thread_character_entry(ULONG thread_input) { (void)thread_input; unsigned char cnt = 0; while (1) { printf("%c",max((simrupt_data++) % 0x7f, 0x20)); tx_thread_sleep(TX_MS_TO_TICKS(100)); } } /* thread1: used RPMsg*/ static void accelerator_thread_entry(ULONG thread_input) { (void)thread_input; THE_DATA data; for(;;) { tx_queue_receive(&recv_queue, &data, TX_WAIT_FOREVER); data.result = accelerator_execute(data.parameter); tx_queue_send(&send_queue, &data, TX_NO_WAIT)) != TX_SUCCESS) } } /* RPMsg receive thread callback*/ static void accelerator_callback(void *payload, uint32_t payload_len) { THE_DATA parameter_data; memcpy((void *)&data, payload, payload_len); tx_queue_send(&recv_queue, &data, TX_NO_WAIT)) != TX_SUCCESS) } /* RPMsg send thread */ static void send_thread(ULONG thread_input) { (void)thread_input; THE_DATA send_data; for(;;) { tx_queue_receive(&send_queue, &send_data, TX_WAIT_FOREVER); rpmsg_lite_send(rpmsg_dev_inst, rpmsg_ept, remote_addr, (char *)&send_data, sizeof(THE_MESSAGE),RL_DONT_BLOCK); } } ``` 在我們的應用程式當中,除了 RPMsg 所屬的發送以及接收執行緒 (`accelerator_callback` 為接收執行緒呼叫的對象之一) , 還有兩個執行緒。第一個執行緒依賴 RPMsg 機制來接收與回傳資料,第二個執行緒不使用 RPMsg 機制,其中前者優先權高於後者。這時將產生兩種情形: - [ ] 此時若 Linux 都沒有傳送訊息到 ThreadX ,第二個執行緒會長期持有 CPU 資源,因為第二個執行緒會因為等待佇列訊息而長期處於 *Suspend State* 。 - [ ] 但是若有訊息進到 ThreadX 當中,接收執行緒將會搶先在 ISR 結束後執行,並且會喚醒第一個執行緒,第一個執行緒由於優先權高於第二個執行緒,因此在接收執行緒執行完後會緊接著執行,並且再把發送執行緒喚醒,讓訊息傳回 Linux 。 #### 影響 RPMsg 可預期性的因素 我們可以從三個層面分析影響來源,分別是 Excessive Timers, Starvation, Priority Inversion ,這些指標來自 ThreadX 文件。首先 Excessiver Timers 提到了頻繁的中斷服務常式將會衝擊系統效能,不過目前除了 ThreadX CPU 排程器,我們設計的執行緒皆未直接使用到 Timer 。再來是 Starvation ,若有比 RPMsg 機制更高優先權的執行緒長期佔用 CPU 資源,則通訊機制將受到嚴重影響,不過這在目前的應用程式設計當中也不存在。最後是 Priority Inversion ,在 RPMsg-lite 的函式庫當中,除了發送以及接收函式共用的資料結構,包含 buffer 以及 mutex lock ,其餘的資料皆不與其他執行緒共享,因此可以避開 priority inversion 的問題。 ### Remoteproc Remoteproc 的主要目的是要更換其他處理器的執行檔,它能藉由 system filesystem 的介面操作其他系統的處理器,並且載入指定的執行檔到處理器對應的記憶體位址。當前我的實作可以支援啟動/暫停遠端處理器,並載入位在 `/lib/firmware/` 的執行檔讓遠端處理器執行。 以下是對 `linux_5.10 LTS` 版的改動內容 [commit 969a98d](https://github.com/HenryChaing/duo-buildroot-sdk-v2/commit/969a98d9c1e18276797e53239cea4e0c3d01683f) ,包括追加平台使用的驅動程式,以及更改裝置樹內容,其中驅動程式透過 `rproc_alloc()` 以及 `rproc_add()` 註冊 remoteproc 裝置,並且支援的操作包括啟動/暫停處理器,使用的是 reset 硬體模組。至於裝置樹則是紀錄了遠端處理器的開機位址(用來載入執行檔)以及要使用 reset 模組的指標,reset-value 紀錄的是遠端處理器 reset 的暫存器偏移量。 ### ThreadX RPMsg-lite 移植 [commit 35c7af3](https://github.com/HenryChaing/ThreadX-to-RISC-V64/commit/35c7af3889be85b7766342afdbc759f36f8f8915) 這個移植過程可以想成是將 rpmsg-lite 函式庫轉移到 ThreadX 的驅動程式目錄底下。首先與硬體無關的程式碼都可以重用例如 `rpmsg_lite.c` `virtqueue.c` 這些原先屬於 rpmsg virtio 層的程式碼,需要更改的部份包括 `rpmsg_platform.c` `rpmsg_env.c` 這些屬於硬體層的部份。 不過也有例外,例如對快取資料的寫回 (write through)在早期的版本是沒有實作的,所以必須針對共享記憶體加入相關描述。再者例如 `BUFFER_COUNT` `alignment` 這兩個變數必須依據 Linux 核心的內部實作去決定,以 `linux_5.10 LTS` 版本來說,前者是由 RPMSG 硬體層的驅動程式決定,後者則是由核心規定成 `4096` 。 關於 `rpmsg_platform.c` 的改動包括將原先 disable/enable global interrupt 的實作從 arm 架構指令改為 RISC-V 架構指令。接著 `rpmsg_env.c` 中的處理器 memory barrier 也改成 RISC-V 架構的指令。至於共享記憶體存取涉及對快取的操作,可見以下示意程式片段。 ```diff /* Invalidate avail->idx before it is read */ + inv_dcache_range(&vq->vq_ring.avail->idx, sizeof(vq->vq_ring.avail->idx)); if (vq->vq_available_idx == vq->vq_ring.avail->idx) { return (VQ_NULL); } ``` ```diff dp = &desc[head_idx]; dp->len = length; dp->flags = VRING_DESC_F_WRITE; /* Flush desc after write */ + flush_dcache_range(&desc[head_idx], sizeof(desc[head_idx])); ``` 隨後就可以把使用 RPMsg 機制的執行緒加到 ThreadX 當中,以接收的 callback 來說,我將其加入了 Mailbox Driver 的 ISR 當中,因此接收的動作會與 ThreadX 的 ISR 機制一同處理。再來是初始化以及發送的執行緒,目前他們是被寫在同一條執行緒當中,優先權會略高於其他執行緒。 多組的通訊 (多個 endpoint )。 作到 multiplexing.