# Linux 核心專題: 將 Linux 執行於 FPGA 為基礎 RISC-V 處理器 > 執行人: fennecJ > [專題解說錄影](https://youtu.be/IacPuRMGdiU) :::success :question: 提問清單 * ? ::: ## 任務描述 在 Xilinx PYNQ-Z2 硬體上,重現〈[從零開始的 RISC-V SoC 架構設計與 Linux 核心運行](https://hackmd.io/@w4K9apQGS8-NFtsnFXutfg/B1Re5uGa5)〉,並更新 Linux 核心及相關套件的版本。 ### 啟動系統的準備工作 #### 產生 bitstream file 先到 vivado 網站 [下載](https://www.xilinx.com/support/download.html) 並安裝 vivado 軟體,這裡筆者使用的 vivado 版本為 2020.2,可在網頁左側清單的 `Vivado Archive` 找到,注意下載前需要註冊帳號才行。 再來取得 [RISC-V_SoC](https://github.com/yutongshen/RISC-V_SoC) 原始程式碼,之後用 vivado 將其開啟,選擇 File $\to$ Project $\to$ Open... ![](https://hackmd.io/_uploads/S1Yl5vZu2.png) 並移動到剛剛 clone RISC-V_SoC 的目錄,打開目錄下的 `vivado/soc/soc.xpr` 打開後選擇左側選單最下方的 `generate bitsteam` ![](https://hackmd.io/_uploads/rJT_nDWd3.png) 之後一路按 OK 到底,等它全部跑完後( 15~25min )選擇 File $\to$ Export $\to$ Export Hardware... ![](https://hackmd.io/_uploads/Hk8Uaw-O3.png) 按 Next ![](https://hackmd.io/_uploads/SkSDpvbun.png) 選擇 `Include Bitstream` 再按 Next ![](https://hackmd.io/_uploads/HkB9TDZ_n.png) 設定好要輸出的目錄和檔名 然後一路 Next 到底後按下 Finish 即可產生對應的 xsa 檔案(不同版本的檔案名稱可能不同,但不影響後續操作),這個 xsa 檔案可以使用一般的解壓縮軟體開啟,裡面會包含 bit stream file (.bit) 以及 hardware handoff file (.hwh): ![](https://hackmd.io/_uploads/rJ1rHOWO2.png) soc_smartconnect_0_0.hwh 是 vivado 自動產生的檔案,我們不需要用,只需要用到 `soc.hwh` 以及 `soc_xsa.bit` 即可,為了方便後續使用,我們要確保兩個檔案檔名前綴相同,因此將 `soc.hwh` 更名為 `soc_xsa.hwh` 準備好 bit stream file 後,接下來要將所需的各個 periperals 接起來。 若不方便安裝 vivado ,也能從 [這裡](https://drive.google.com/drive/folders/1U_mz91qeFlM4RmhGj2KTosD3d-gEyJ4v?usp=sharing) 下載已準備好的 `soc_xsa.xsa` #### Peripherals 的腳位連接 詳細腳位定義可參考 [PYNQ-Z2 Reference Manual v1.0](https://www.mouser.com/datasheet/2/744/pynqz2_user_manual_v1_0-1525725.pdf) ![](https://hackmd.io/_uploads/SyOwlHvHh.png) ###### img src: [PYNQ Z2 pinout](https://discuss.pynq.io/t/pynq-z2-pinout/4256) 對照 `.xdc` constraint files: ```e set_property -dict {PACKAGE_PIN Y18 IOSTANDARD SSTL18_I} [get_ports riscv_rmii_tx_en] set_property -dict {PACKAGE_PIN Y19 IOSTANDARD SSTL18_I} [get_ports {riscv_rmii_rxd[0]}] ... set_property -dict {PACKAGE_PIN U18 IOSTANDARD SSTL18_I} [get_ports riscv_rmii_refclk] set_property -dict {PACKAGE_PIN Y17 IOSTANDARD SSTL18_I} [get_ports {riscv_rmii_txd[1]}] ... set_property -dict {PACKAGE_PIN Y16 IOSTANDARD SSTL18_I} [get_ports {riscv_rmii_txd[0]}] set_property -dict {PACKAGE_PIN U19 IOSTANDARD SSTL18_I} [get_ports {riscv_rmii_rxd[1]}] set_property -dict {PACKAGE_PIN W18 IOSTANDARD SSTL18_I} [get_ports riscv_rmii_crs_dv] ... set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets riscv_jtag_tck_IBUF_BUFG] set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets riscv_jtag_tck_IBUF] set_property -dict {PACKAGE_PIN W14 IOSTANDARD SSTL18_I} [get_ports riscv_jtag_tck] set_property -dict {PACKAGE_PIN Y14 IOSTANDARD SSTL18_I} [get_ports riscv_jtag_tms] set_property -dict {PACKAGE_PIN T11 IOSTANDARD SSTL18_I} [get_ports riscv_jtag_tdi] set_property -dict {PACKAGE_PIN T10 IOSTANDARD SSTL18_I} [get_ports riscv_jtag_tdo] ... set_property -dict {PACKAGE_PIN V6 IOSTANDARD LVCMOS33} [get_ports sclk_0] set_property -dict {PACKAGE_PIN Y6 IOSTANDARD LVCMOS33} [get_ports nss_0] set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS33} [get_ports miso_0] set_property -dict {PACKAGE_PIN C20 IOSTANDARD LVCMOS33} [get_ports mosi_0] ... set_property -dict {PACKAGE_PIN W6 IOSTANDARD LVCMOS33} [get_ports riscv_uart_rxd] set_property -dict {PACKAGE_PIN Y7 IOSTANDARD LVCMOS33} [get_ports riscv_uart_txd] ``` 可繪製出如下的接線圖: ![](https://hackmd.io/_uploads/H1rGaXoL3.png) * 本接線圖假設兩個 Periphral VCC 皆為 5.0V * 有關 rmii_ 的 port ,根據 [RISC-V_SoC](https://github.com/yutongshen/RISC-V_SoC) 原作者 yutongshen 提供的架構圖是用來連接網路的,這邊暫不列入接線圖。待系統順利啟動後再研究 ![](https://hackmd.io/_uploads/BJKvpIDB2.png) #### JTAG ![](https://hackmd.io/_uploads/rJeCp7VOn.png) JTAG 的全名是 Joint Test Action Group ,主要用來 debug 或驗證硬體設計是否運作正常,其機制為輸入一連串特定的訊號給硬體,並檢查硬體運作是否符合預期,實務上不會直接將訊號連到硬體的 input ,而是透過 DP(Debug Port) 讀寫 DR(Data reg) 或 IR(Inst reg) 將特定的值寫入到硬體中並讀出(後面會解釋 DR、IR 的用途),一個 JTAG 要能運作至少要有 4 個 port : * TDI (Test Data In) 用來將資料寫入 DR/IR * TDO (Test Data Out) 用來讀取 DR/IR 的值 * TMS (Test Mode Select) 用來選擇要(寫入/讀取)哪個 (DR/IR) * TCK (Test clock) 用來將 JTAG 和硬體同步用的 clock 我們可以藉由 JTAG 和 TAP(Test Access Port) 溝通並測試系統, 透過 TAP 可以存取 DR(Data reg) 和 IR(Inst reg) , TAP 由 TAP controller 控制,而 JTAG 可以控制 TAP controller ,硬體剛通電時,我們不知道 TAP controller 的 FSM 目前在哪個狀態,因此我們需要先將 TAP controller 的狀態設為 reset,下圖展示了 TAP controller 的 FSM: ![](https://hackmd.io/_uploads/Hk4Qcp6wh.png) ###### source:[ARM Debug Interface v5 Architecture Specification](https://developer.arm.com/documentation/ihi0031/a/Overview-of-the-ARM-Debug-Interface-and-its-components) 可以看到,無論在何種狀態,只要 TMS 連續給五個 1 就必定會讓狀態回到 Test-logic-reset 。 進到 Test-logic-reset 後,我們只要根據所需傳送不同的 TMS 訊號即可到我們想要的狀態,並透過 TDI 將我們希望寫入的值傳送進 DP 特定的 DR/IR ;透過 TDO 讀取 DP 特定的 DR/IR。 而一個 DP 會有一個 IR 和數個 DR , 要讀寫特定的 DR 時,會透過 IR 目前存的值來決定要選哪個 DR 進行讀寫,[RISC-V_SoC](https://github.com/yutongshen/RISC-V_SoC) 的原作者共開 8 個 DR ,包含 ARM debug interface 中標準的 5 個 DR 以及另外 3 個客製化的 DR | IR value | DR select | | -------- | -------- | | 0x8 | ABORT | | 0xa | DAPCC | | 0xb | APACC | | 0xe | IDCODE | | 0xf | BYPASS | |* 0x10 | APDBUF | |* 0x11 | APRBUF | |* 0x12 | APWBUF | (有打星號的是客製化的 DR) reg 的讀寫主要發生在 shift-IR/shift-DR 的狀態,在這個狀態下, TDI 的值會以 bit stream 的方式一個個 write 進 DUT(Device under test) 的 input shift reg ,同時 DTO 會將 DUT 的 output shift reg 一個個 read 出來。 #### 透過 JTAG access APB/AXI interface * 待補 #### JTAG 連接 這裡使用的 JTAG 模組為 `CJMCU-232H`: ![](https://hackmd.io/_uploads/Bks3ifJd2.png) 參考 FT232H 的[官方文件](https://ftdichip.com/wp-content/uploads/2020/07/DS_FT232H.pdf) ![](https://hackmd.io/_uploads/B1hYiGk_h.png) 可以看到 AD0~AD4 即分別為 JTAG 所需的 `TCK`、`TDI`、`TDO`、`TMS` * JTAG-USB 的局部放大圖: ![](https://hackmd.io/_uploads/Hke_6Xj8n.png) fpga 的 PMODB 上從左到右接線分別為 `AD2` `AD1` `AD3` `AD0` |T10 | T11 | Y14 | W14 | | -------- | -------- | -------- |-| | AD2 | AD1 | AD3 |AD0| #### 建立 sd 分區 聯繫上 yutongshen 後,他提供許多具有參考價值的資源。 下圖是開機流程圖。 ![](https://hackmd.io/_uploads/H1nmXttIh.png) 我們需要在 sd card 上建立兩個分區,第一個分區用來放 bbl 和 vmlinux ,第二個分區則用來放 rootfs 我們可以用 fdisk 命令建立分區,並用 mkfs 命令建立檔案系統: ```bash sudo fdisk /dev/sdc/ n # create new partition p # choose primary partition 1 # Partition num +50M # Set a 50M space for current part # Do you want to remove the signature? [Y]es/[N]o: Y # If an old signature exists, remove it. ``` 如此我們便建立好的第一個 part ,目前 terminal 的輸出資訊應如下: ```bash Command (m for help): n Partition type p primary (0 primary, 0 extended, 4 free) e extended (container for logical partitions) Select (default p): p Partition number (1-4, default 1): 1 First sector (2048-15523839, default 2048): 2048 Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-15523839, default 15523839): +50M Created a new partition 1 of type 'Linux' and of size 50 MiB. Partition #1 contains a vfat signature. Do you want to remove the signature? [Y]es/[N]o: Y The signature will be removed by a write command. ``` 再來依照 yutongshen 圖片,將 part1 設為 W95 FAT32 LBA , 繼續在 fdsik 中使用命令 `t` 來更改 part type: ```bash t # change partition type 1 # choose 1st partition 0c # Hex code for W95 FAT32 LBA part type ``` 這時終端機的輸出如下: ```bash Command (m for help): t Selected partition 1 Hex code or alias (type L to list all): 0c Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'. ``` 如此便完成 part1 的分區,再來切 part2 ,繼續在 fdisk 中輸入以下命令: ```bash n # create new partition p # choose primary partition 2 # part number = 2 # press enter directly ``` 如此我們便建好 part2 的分區。 再來使用命令 `w` 將 partition 資訊寫入 sd card ,並用 `q` 離開 fdisk 。 接著是格式化,前一步驟切完分割表資訊後可以發現使用命令 `ls /dev/ | grep sdc` 的輸出從 `sdc` 變成了 `sdc sdc1 sdc2` ,其中 `sdc1` 就是我們切的 part1 分區, `sdc2` 則是 part2 的分區。 使用命令 ```bash sudo mkfs.fat /dev/sdc1 sudo mkfs.ext3 /dev/sdc2 ``` 分別將兩個分區進行格式化。 再來將檔案分別複製到對應的目錄下即可: 將 yutongshen 提供的 `vmlinux` 和 `bbl` 複製到 `sdc1` 把 `rootfs.tar.gz` 解壓縮之後將所有檔案複製到 `sdc2` 並把 `etc/rcS` 中第 36 行的 `cp /root/index.html /var/www/` 開始到最後一行全部註解起來: ![](https://hackmd.io/_uploads/ryjw8eVdn.png) (以上註解的部份是有關網路配置的命令,但我們尚未將網路模組連接到板子上因此暫時註解起來) `rootfs.tar.gz` 、 `vmlinux` 和 `bbl` 等相關檔案可到 [這裡](https://drive.google.com/drive/folders/1U_mz91qeFlM4RmhGj2KTosD3d-gEyJ4v) 下載。 #### 使用 arm cpu 驗證連接的 peripherals 運作正常 將前述所有 peripherals 依照接線圖連接到 PYNQ-Z2 上之後,我們便能截由 pynq 上的 arm cpu 打開 jupyter notebook 開始測試硬體功能是否運作正常,我們需要另外準備一塊 8gb 以上的 micro sd 並將板子對應的 [image](http://www.pynq.io/board.html) 下載解壓縮後複製到 micro sd 中,之後把 PYNQ-Z2 右上角的跳線設置為 `sd` (短路下圖步驟 1 的藍色區域)以及將左下角的跳線設置為 `USB` (短路下圖步驟 2 的藍色區域) ![](https://hackmd.io/_uploads/HJdF2V-_2.png) 之後將存有 image 的 microsd 放入 PYNQ 下方的插槽(圖中步驟 3 ) ,將 usb 傳輸線(圖中步驟 4 )連上電腦並將網路線(圖中步驟 5 )接上數據機後即可打開電源 接著和剛剛圖中步驟 4 插入的 Uart 連線,這裡使用 putty 作為連線的軟體,使用命令 `sudo dmesg | tail -5 | grep ttyU` 可以看到剛剛接入的 USB 裝置名稱為何: ![](https://hackmd.io/_uploads/BJA-8SZuh.png) 在這裡我先接上了 PYNQ 的傳輸線,再接上了 riscv-soc 接出來的 UART 到電腦上,因此 ttyUSB1 即為 PYNQ 中 arm cpu 的 uart ;而 ttyUSB2 為 riscv-soc 接出來的 UART,將 baud rate 設為 115200,點選 Open 即可建立連線: ![](https://hackmd.io/_uploads/SJ_JMwWO2.png) 若提示 permission denied ,記得先用 `sudo chmod 666 /dev/ttyUSB1` 調整 ttyUSB1 的 權限。 連上之後我們就進入了 arm cpu 執行的作業系統,使用命令 `ifconfig` : ![](https://hackmd.io/_uploads/Bye2GD-u3.png) 可以看到它被掛在 LAN 的 192.168.2.124 這個 ip,打開網頁瀏覽器,在網址中輸入剛剛 `ifconfig` 呈現的 ip 位址,並指定 port = 9090 ( jupyter kernel server 的預設 port ),即輸入 `192.168.2.124:9090` 到網址列中,這時若它提示需要輸入密碼,密碼為 `xilinx` ,如此我們便能進入 PYNQ 上的 jupyter 伺服器。 將 yutongshen 提供 debug 用的 [riscv.ipynb](https://gist.github.com/fennecJ/5f7af384e79c9aa8d201aaebfc40c1ee) 透過 upload 按鈕上傳到板子上我們便完成了初步的準備工作。 再來是將前面產生 bit stream file 時得到的 `soc_xsa.bit` 以及 `soc_xsa.hwh` 兩個檔案上傳到和 `riscv.ipynb` 相同的目錄,之後便能開啟 `riscv.ipynb` 藉由 arm cpu 測試硬體和各 peripherals 是否運作正常。 開啟 `riscv.ipynb` 後,我們可以看到第一個 block 的程式為: ```python from pynq import Overlay riscv = Overlay("./soc_xsa.bit") riscv.ip_dict for i in riscv.ip_dict: print(i) print('------------------------------') for j in riscv.ip_dict[i]: print(j) print('------------------------------') ``` 這裡大致講一下 Overlay 這個 function 的作用為何, FPGA 全稱 Filed Programmable Gating Array ,他的特性是其硬體行為可以藉由 HDL 程式更改,前面我們產生的 bitstream 便包含了可程式化硬體應該如何配置和實現目標設計的邏輯元件的資訊,透過 Overlay 這個 function ,可以將 bitstream 燒到板子上的可程式化硬體中,因此這一步便是將 riscv_soc 部署到板子上。 同時, bitstream file 中也包含目標硬體設計的各種基本資訊,例如可以透過 ip_dict 看到板子上所有可以透過特定 addr access 到的 ip 後面的 ```python for i in riscv.ip_dict: ... ``` 便是列出所有 addressable IPs 接下來我們會透過 MMIO(Memory mapped I/O) 去 access 這些 addressable IPs 並測試其運作符合預期。 大部分的測試就如 jupyter notebook 中的標題一樣,這裡就只挑幾個比較特別的講: 最前面的 ROM test block 除了進行讀寫測試外,也能作為載入 rom.bin 到 ROM 的手段之一,若手邊剛好沒有 Jtag 或 Jtag debugger 無法順利運作也能透過這個方法啟動系統,唯注意啟動系統後 arm cpu 會被關掉無法繼續存取板子上的硬體資訊,因此在啟動系統後若要除錯仍須透過 Jtag 。 在進行 UART test 前需要把 riscv_soc 的 uart-usb 接到電腦上並將其開啟, baud rate 設為 115200 。 之後執行完初始化設定好硬體 addr 的 jupyter notebook block 後,執行 ```python for i in "Hello".encode(): riscv_base.write(UART_TXFIFO, i) ``` 預期可在對應的 putty terminal 中看到 `Hello` 字樣。 在 SPI test 中,主要用來測試 micro sd-SPI 模組是否運作正常,因此需先將前面 `啟動系統的準備工作` 燒好的 micro sd 插入模組中,之後若出現 Read Success 表示透過 micro sd 可正常存取。 若出現 Read Fail ,可嘗試以下步驟除錯: * 檢查 spi-micro sd 腳位是否有接錯,並透過三用電錶檢查連接線是否接觸不良 * 將 sd_init(0) 中的數字改為較大的數字,這裡的數字表示要將 spi-micro sd 的 clk rate 除以 2 的多少次方,當 clk rate 越低我們會有更高的容錯率,另外部份 micro sd card 須在較低的 clk rate 下才能正常運作。 * 嘗試將 spi-micro sd 的 VCC 接到不同的電壓上( 5V <-> 3.3V )不同模組或不同廠牌的 micro sd 所需的工做電壓可能不同 * 換一個 micor-sd card 筆者換了 3 個 micro-sd card 才順利讀取出來 :::warning 請避免使用 Sandisk 這個牌子的 Ultra 系列 microsd,不只筆者有遇到問題,網路上也有多篇文章討論到這個系列,該系列不知為何無法正確被 spi-microsd 模組讀取出來 ::: * 有時候會出現 ipynb 無法 read success 但仍順利啟動系統的情形,也能嘗試直接進行後面啟動系統的步驟。 #### 使用 Jtag debugger 啟動系統 在使用 debugger 前,需要準備一台裝有 windows 系統的電腦,並安裝好 visual studio 2015 以後的版本並安裝好 C# 相關的套件。 確保將所有 perepheral 接到板子上,包含 `Jtag-USB 、 Uart-USB 、 以及 spi-microsd` ,同時將之前放好 `vmlinux` 、 `BBL` 以及 `rootfs` 的 microsd 卡放入 spi-micro sd 模組中。 將 Jtag-USB 接上板子後把 USB 接端接上電腦 再來確保已藉由 `riscv.ipynb` 的 Overlay funct 把 bitstream 燒到板子上。 同時將 riscv 上的 uart-usb 接到電腦上並將其開啟, baud rate 設為 115200 。 到 [這裡](https://drive.google.com/drive/folders/1U_mz91qeFlM4RmhGj2KTosD3d-gEyJ4v?usp=sharing) 下載 yutongshen 提供的 `rom.bin` 備用。 之後下載 yutongshen 提供的 [dubugger](https://github.com/yutongshen/RISC-V-Debugger) ,直接點開裡面的 `debugger.sln` ,再點「開始」按鍵 ![](https://hackmd.io/_uploads/ryehCk4dn.png) 便會看到如下的視窗畫面。 先點選 Setup 按鈕: ![](https://hackmd.io/_uploads/BJzvMg4d2.png) 之後 Jtag 便會將 ROM 中的資料呈現出來: ![](https://hackmd.io/_uploads/ryJe7g4_n.png) 然後按下 「 Download ROM 」 將 yutongshen 提供的 `rom.bin` 載入到 ROM (事實上 yutongshen 為了方便用的是 SRAM 實做 ROM) 中。 之後點下右上的按鈕 `WORD` 並在右上角輸入 `4000000` 然後按下 Goto ,並在圖中左上紅匡處點兩下,輸入 `00000001` 後 cpu 便會從 rom addr 的 `00000000` 開始執行。 ![](https://hackmd.io/_uploads/HkqOjx4uh.png) 我們來看一下編譯 `rom.bin` 的 [main.c](https://github.com/yutongshen/RISC-V_SoC/blob/4ae4ba15a8596bfb67b368e7180e0152b0e094c1/rom/main.c) 內容 ```c ... __U32 main(void) { *PLIC_INT_TYPE_32P = -1; __uart_init(); __puts("[BROM] UART init done"); #ifndef FAKE_SD __puts("[BROM] SD card init"); __sd_init(&__sd_type); #endif __puts("[BROM] FAT BPB init"); __fat_bpb_init(&__bpb); // load bbl __puts("[BROM] load bbl"); __fopen(&__file, "bbl"); __elf_loader(&__file); // load linux __puts("[BROM] load vmlinux"); __fopen(&__file, "vmlinux"); __elf_loader_reloc(&__file, 0x80200000); __puts("[BROM] boot rom init done"); return 0; } ``` 基本上它做事情是 * 把相關的 peripherals 都 Init 好 * 到 micro sd 上找到 `bbl ` 和 `vmlinux` ,將 `bbl` 載入到 sram , `vmlinux` 載入到 dram 中,之後程式執行結束後將控制權交給 `bbl` , `bbl` 再把 `vmlinux` 開起來。 這時可以在剛剛打開的 usb-uart 中看到啟動相關資訊,之後會出現 RVLinux 的歡迎畫面: ![](https://hackmd.io/_uploads/rkaHLWEO3.png) 輸入帳號密碼,帳號密碼皆為 `root` 我們便能成功啟動系統,如此便順利在這顆 cpu 上執行 linux 作業系統 #### 編譯 vmlinux 接下來我們嘗試自己編譯 vmlinux ,先安裝 [riscv-gnu-toolchain](https://github.com/riscv-collab/riscv-gnu-toolchain) ,安裝步驟可參考連結中的 `README.md` ,先將 Prerequisites 都安裝好,之後參考 Installation (Linux) 章節安裝好 cross compiler 。 安裝後記得要將安裝目錄下的 `bin/` 加到 PATH 環境變數中。 到 [這裡](https://drive.google.com/drive/folders/1jvnw7T8S8Pd-WTIBcMbHXMrpvyL0VHo3) 下載裡面的 `drivers/` 目錄下的所有 `.c` 檔、`.sh` 檔以及 `debug/` 目錄 。 接著下載 4.20 的 kernel src code ,筆者是下載 [這裡](http://ftp.ntu.edu.tw/linux/kernel/v4.x/) 的 `linux-4.20.tar.xz` 使用命令 `tar -xvf linux-4.20.tar.xz` 解壓縮,在 cd 到 `linux-4.20` 的目錄裡面。 打開剛剛下載的 `drivers/` 中的 `copy_file.sh` 將第一行的 DIR 改成剛剛下載的 `driver/` 目錄路徑, 這個 script file 是我寫來方便將大部分 driver 複製到特定目錄用的。 ```shell #!/usr/bin/env bash DIR='/home/fennecj/Downloads/drivers' # Change the path to where the drivers dir is ``` 之後將 `copy_file.sh` 複製到 `linux-4.20` 的目錄裡面,並執行該腳本以便複製 driver 到對應的目錄下,同時將 `/drivers/config_log/.config_4.20 `複製到`linux-4.20` 中並將檔名改為 `.config` ```bash $ pwd /home/fennecj/Downloads/linux-4.0 $ cp ../drivers/copy_file.sh . $ chmod +x copy_file.sh $ ./copy_file.sh $ cp ../drivers/config ``` 接著我們仍須手動做一些調整: * 到 `drivers/tty/serial/` 下查看是否存在 `sifive.c` 這個檔案,若不存在則將 drivers/special/sifive.c 複製到該目錄下(這個 driver 在新版 kernel 便有包含,唯較舊版本的 kernel 不含該 driver ) ```bash $ pwd #/home/fennecj/Downloads/linux-4.0 $ ls drivers/tty/serial/ | grep sifive.c # if there exist, do nothing # else : cp ../drivers/special/sifive.c drivers/tty/serial/sifive.c ``` * 到 `include/linux/init.h` 目錄下,在 `#include <linux/types.h>` 這行下面加上: ```c // include/linux/init.h #define dbg_info(fmt, ...) do { \ printk("[DBG] %s:%d:%s: " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); \ } while(0) ``` * 開啟 `script/dtc/dtc-lexer.l` ,若存在 ```c YYLTYPE yylloc; ``` 則將其改為 ```c extern YYLTYPE yylloc; ``` > 較新版核心已移除這行 之後便能使用命令編譯出 vmlinux ```bash $ pwd #/home/fennecj/Downloads/linux-4.0 $ make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- all -j4 ``` 將 vmlinux 複製到 microsd 上,之後照著前面的步驟走一起就能順利啟動自己編譯的系統了。 #### 編譯時可能遇到的問題 以下是我在編譯時遇到的問題和解法。 ##### riscv64-unknown-linux-gnu-xxx not exist 解法: 確認是否有將 riscv-toolchain 目錄的 `bin/` 加到 PATH 中 ##### need extension zicsr terminal 顯示: ```! Err: ... need extension zicsr ``` 解法: 這是由於較新版本的 gnu buildutil 將 zicsr 獨立出來所致,打開 `arch/riscv/Makefile` ,找到出現以下幾行的地方: ``` riscv-march-$(CONFIG_ARCH_RV64I) := rv64ima riscv-march-$(CONFIG_FPU) := $(riscv-march-y)fd riscv-march-$(CONFIG_RISCV_ISA_C) := $(riscv-march-y)c KBUILD_CFLAGS += -march=$(subst fd,,$(riscv-march-y)) KBUILD_AFLAGS += -march=$(riscv-march-y) ``` 將其改為: ``` riscv-march-$(CONFIG_ARCH_RV64I) := rv64ima riscv-march-$(CONFIG_FPU) := $(riscv-march-y)fd riscv-march-$(CONFIG_RISCV_ISA_C) := $(riscv-march-y)c # Newer binutils versions default to ISA spec version 20191213 which moves some # instructions from the I extension to the Zicsr and Zifencei extensions. toolchain-need-zicsr-zifencei := $(call cc-option-yn, -march=$(riscv-march-y)_zicsr_zifencei) riscv-march-$(toolchain-need-zicsr-zifencei) := $(riscv-march-y)_zicsr_zifencei KBUILD_CFLAGS += -march=$(subst fd,,$(riscv-march-y)) KBUILD_AFLAGS += -march=$(riscv-march-y) ``` 即可。 ref: https://lore.kernel.org/lkml/20220126171442.1338740-1-aurelien@aurel32.net/ ##### multiple definition of `yylloc` terminal 顯示: ```! /usr/bin/ld: scripts/dtc/dtc-parser.tab.o:(.bss+0x10): multiple definition of `yylloc'; scripts/dtc/dtc-lexer.lex.o:(.bss+0x0): first defined here collect2: error: ld returned 1 exit status ``` sol: 將 `scripts/dtc/dtc-lexer.l` 中的 ```c YYLTYPE yylloc; ``` 改為 ```c extern YYLTYPE yylloc; ``` ref: https://github.com/BPI-SINOVOIP/BPI-M4-bsp/issues/4 ##### DECLARE_TASKLET() take 2 args but pass 3 在較新版本的 kernel 中, DECLARE_TASKLET 這個 MACRO 有進行更動,我們可以改用 `DECLARE_TASKLET_OLD` 來確保相容性,將所有 ` DECLARE_TASKLET (A, B, C)` 改為 `DECLARE_TASKLET_OLD(A, B)` 即可。 延伸閱讀: 老師在維護的 [lkmpg](https://sysprog21.github.io/lkmpg) 剛好有提到這件事 ## TODO: 確保 [RISC-V_SoC](https://github.com/yutongshen/RISC-V_SoC) 具備足夠的周邊來啟動 Linux 核心 > 盡量避免未授權的 IP [RISC-V_SoC](https://github.com/yutongshen/RISC-V_SoC) 用到的兩個 IP : AXI-Bus 和 ps7system (針對板子上 arm cpu 的 IP ) 都是可直接在 `vivado` 軟體中取得的 IP ,沒有用到未授權的 IP 。 ## TODO: 更新 Linux 核心及相關套件 經過上述步驟後,讀者理應能夠編譯出能執行的 4.20 vmlinux ,接著嘗試將其改為更新版本的 kernel 。 在之前下載的 `/drivers` 目錄下,有我成功升級後的 kernel 對應的 config 檔案,讀者可自行將其複製到對應的 kernel 目錄中並更名為 `.config` ,其餘步驟和編譯 4.20 的 vmlinux 大同小異,若編譯時 terminal 出現下列選項: ``` Initialize kernel stack variables at function entry > 1. no automatic initialization (weakest) (INIT_STACK_NONE) 2. 0xAA-init everything on the stack (strongest) (INIT_STACK_ALL_PATTERN) (NEW) 3. zero-init everything on the stack (strongest and safest) (INIT_STACK_ALL_ZERO) (NEW) choice[1-3?]: 1 ``` 我目前是選 1 ,但選其他選項理應也能順利運作。 目前成功移植的版本有 `v5.0.1` `5.3.1` ~ `5.7.1` ,其中, kernel `5.4` 是 long term support ,我有成功將最新板的 `5.4.248` 啟動,而目前數字最大的版本是 `v5.7.1` ,嘗試移動到 `v5.8` 後系統便會在啟動到一半時出錯,無法順利讀取 `mmcblk0` (micro sd) 的 partition table ,因而無法順利掛載 rootfs 。 推測是 `riscv-spi.c` 在新版的 kernel 需要進一步修改才能正常運作,或是我在 `5.8` 的 `.config` file 設定有誤導致,目前仍在嘗試中。 目前發現 `5.8` 的系統啟動時印出的 irq 數值不同於 [device tree](https://github.com/yutongshen/RISC-V_SoC/blob/master/rom/rv64.dts) 中的數值: ```=120 spi0: spi@10001000 { interrupts = <0x2>; ``` ![](https://hackmd.io/_uploads/HJNrHSEu2.png) 已嘗試的方法: `CONFIG_CMDLINE='root=/dev/mmcblk0p2 rootfs=ext3 rootwait'` 將 `spi-riscv.c` 中的 `RISCV_SPI_CR1_BR_MASK` 改為 `((2 & 0x7) << 3)` (即把 clk rate 調低提高容錯率, 2 表示將 clk rate 除以 $2^2$ ) Update: 日前和 yutongshen 討論了這個問題, yutongshen 便幫忙排除了相關問題並更新了 github 上的 `rom.bin` ,改用該 binary file 可順利啟動 `v5.8` 系統。目前可以開到 `5.8.1` ,但 `5.9.1` 會連 `bbl` 的啟動畫面都進不了,目前尚在研究是哪裡發生問題。 同時他也分享了發現問題的過程: 首先他從開機畫面的 log 中發現 `SPI transfer timed out` : ![](https://hackmd.io/_uploads/ry2-Z7v_n.png) 這個 log 發生的時機為何? -> 當 SPI 之前的 data transfer 後並未 call `spi_finalize_current_transfer` 時, kernel 就會認為 SPI 尚未完成,持續一段時間後就會有 `SPI transfer timed out` 的 log 。 而在 [spi-riscv.c](https://drive.google.com/file/d/1OiXkdY-JLF0hKZYwT4AJJqdcGa4MuhTH/view?usp=sharing) 中只有兩個地方會 call `spi_finalize_current_transfer` ,分別是在 `riscv_spi_transfer_one` 和 `riscv_spi_irq` 這兩個 funct 中,而在 `riscv_spi_transfer_one` 中: ```c=96 static int riscv_spi_transfer_one(struct spi_master *master, struct spi_device *spi, struct spi_transfer *t) { struct riscv_spi *rvspi = spi_master_get_devdata(spi->master); rvspi->tx = t->tx_buf; rvspi->rx = t->rx_buf; if (rvspi->irq && t->len >= 0x80) { while (readl(rvspi->base + RISCV_DMA_CON) >> 31) cpu_relax(); writel(rvspi->tx ? (int) virt_to_phys(rvspi->tx) : -1, rvspi->base + RISCV_DMA_SRC); writel(rvspi->rx ? (int) virt_to_phys(rvspi->rx) : 0, rvspi->base + RISCV_DMA_DEST); writel(t->len, rvspi->base + RISCV_DMA_LEN); writel(1, rvspi->base + RISCV_DMA_IC); writel(1, rvspi->base + RISCV_DMA_IE); writel(SIZE_WORD << 10 | // dest size SIZE_WORD << 8 | // src size (rvspi->rx ? TYPE_INCR : TYPE_CONST) << 6 | // dest type (rvspi->tx ? TYPE_INCR : TYPE_CONST) << 4 | // src type 0 << 1 | // bypass spi 1, rvspi->base + RISCV_DMA_CON); goto done; } for (rvspi->count = 0; rvspi->count < t->len; rvspi->count += 1) { while (!(readl(rvspi->base + RISCV_SPI_SR) & RISCV_SPI_SR_TXE_MASK)) cpu_relax(); riscv_spi_tx_word(rvspi); while (!(readl(rvspi->base + RISCV_SPI_SR) & RISCV_SPI_SR_RXNE_MASK)) cpu_relax(); riscv_spi_rx_word(rvspi); } spi_finalize_current_transfer(master); done: return t->len; } ``` 可以看到上述第 101 行的 `t->len >= 0x80` ,這裡在做的事情是判斷要傳輸的資料長度若超過 0x80 的話則交由 DMA 進行 transfer ,否則會直接讓 cpu 傳輸,除非硬體實做上有錯,不然透過 cpu 傳輸理應不會發生問題。 因此可以著重在 DMA 的部份進行除錯,當 cpu 將 transfer 的工作交給 DMA 執行後, cpu 便會跑去處理其它 routine (而此時尚不會 call `spi_finalize_current_transfer` ) ,直到 DMA 處理完後發出 irq 通知 cpu 要處理後面的事情,因此若走 DMA 這條路的話,會一直到 `riscv_spi_irq` 中 ```c static irqreturn_t riscv_spi_irq(int irq, void *dev) { struct spi_master *master = dev; struct riscv_spi *rvspi = spi_master_get_devdata(master); if (readl(rvspi->base + RISCV_DMA_IP) & readl(rvspi->base + RISCV_DMA_IE)) { writel(0, rvspi->base + RISCV_DMA_IE); spi_finalize_current_transfer(master); } return IRQ_HANDLED; } ``` 才會 call `spi_finalize_current_transfer` ,所以要等到 `spi_finalize_current_transfer` ,順序該會是: cpu 交由 DMA 處理 transfer -> DMA 處理完畢發出 IRQ 訊號 -> cpu 回來 handle DMA 的 IRQ 並 call `riscv_spi_irq` 這個 funct -> 在 funct 中 call `spi_finalize_current_transfer` 。 我們可以逐一檢視上述每個環節哪裡出錯了: * DMA 是否順利被 CPU 交付任務? :::info 成功啟動 6.1.1 了 🎉 ::: 後來和 yutongshen 討論時, yutongshen 指出他留意到 6.1 的 kernel 啟動系統時預設會以 SV57 啟動 ( 最多5層page table) ,而它原本的 MMU 只支援 SV39 。近日在作者更新硬體後,目前能順利啟動 linux 6.1.1 的 kernel ,我把對應的 vmlinux 放在 [這個連結下](https://drive.google.com/file/d/1Pv5gm_MBAwiqwFxaLnVCSjbinb2T6L14/view?usp=drive_link) ## misc backup `5.9.1` 問題紀錄: 不斷重複 rom 中 `main.c` 的 `[BROM] UART init done` 到 `[BROM] load vmlinux` ,而並未進入 bbl 的 INSTRUCTION SETS WANT TO BE FREE 。 但是一但將 sd card 拔出來,就能順利進入 bbl 的對應畫面,表示 bbl 有成功被 load 進去。 yutongshen 提示我可能是 rom code 執行過程中發生 exception 了,但系統剛啟動時 mtvec 尚未初始化,因此發生 exception 時 cpu 無法 handle,可以透過 debugger 看看各個 csr reg 有什麼提示。 雖然能從 csr reg 看到很多數值,但這些 reg 分別代表什麼我尚不清楚,目前正在惡補 riscv64 相關的 spec 。 目前透過 debugger 看到 PC 卡在 0x0CD2 , 透過 objdump trace 後,研判它目前卡在 `__dma_busy()` 的地方,而這個 `__dma_busy()` 又會被 `__dma_spi2buf` 、 `__dma_memcpy` `__dma_memfill` 三個 MACRO 用到,晚點確認一下是卡在哪一個 MACRO 中。 目前成功打開 5.9.1 了,但要用一個很奇怪的作法,由於會 call 到 `__dma_busy()` 的三個 MACRO 都是 spi 相關的,所以我打算先嘗試將 sd 的處理頻率增加容錯率看看。 原先我嘗試將 `spi.c` 中的 init 從 2 改成 4 讓它處理的 freq 變慢以取得更高的容錯空間,但是當我直接用改好的 `rom.bin` 刷上去啟動時會什麼資訊都沒看到(連 `[BROM] UART init done` 都看不到),但是我檢查 PC reg 發現它似乎有執行到 vmlinux 中,所以研判是 uart 沒有順利被初始化導致資料出不來,結果在某次測試時我偶然發現,先用原本未修改過得的 `rom.bin` 讓它初始化 uart 後再改成新的 `rom.bin` 居然能順利進入系統,但為何只是將 spi_init 的 freq 做更改就會讓 uart 無法順利初始化這點尚待研究。 目前要啟動系統要 刷上原本的 `rom.bin` -> 執行到 `rom.bin` 進入不斷重複 rom 中 `main.c` 的 `[BROM] UART init done` 到 `[BROM] load vmlinux` 的死循環後按下 debugger 的 reset 重置 pc -> 改刷調整過 sd 處理頻率的 `rom.bin` -> 順利進入 `5.9.1` 系統。 ### 可能遇到的問題 -- jtag-usb :::spoiler 問題描述 近期我檢視文章中的開機 [影片](https://youtu.be/LUEj9g0ChXA) ,留意到 yutongshen 似乎是利用自製的 debugger 透過 jtag-usb 寫入特定的數值到暫存器後 CPU 才切到 Boot ROM 開始啟動系統的一連串行為。 但 yutongshen 似乎沒有在 repo 中提供 debugger ,因此需要想其他方法寫資料到 0400_0000 的 register 上,目前有兩個想法: 1. 研究可否透過 gpio 經由板子上的 UART (pynq-z2) 自帶的 UART 經由 AXI-BUS 送資料進去,但這樣做會需要 arm cpu 的協同工作,不知道會不會衍生其他問題。 2. 研究 jtag-usb 的 data-sheet 以及他在系統中的詳細用途,嘗試寫一個簡單的 cli 控制他和板子溝通 。 目前我手上的 jtag-usb 模組是 `CJMCU-232H` 這塊(如下圖) ![](https://hackmd.io/_uploads/BJl6hRASh.png) 詳細的腳位定義可參考 [data-sheet](http://www.ftdichip.com/Support/Documents/DataSheets/ICs/DS_FT232H.pdf) 目前偏向嘗試第 `2.` 有找到幾個 repo 可以研究: * [libmpsse](https://github.com/devttys0/libmpsse) 一系列 FT-232H 官方 doc 提供的範例 code 順利聯絡上 yutongshen,他慷慨表示願意提供 [debugger](https://github.com/yutongshen/RISC-V-Debugger) ::: > 問題已解決 ### openSBI ::: spoiler 由於技術問題, risc-v_soc 暫不能使用 openSBI 1. 目前 risc-v_soc 還沒有 FPU ,但作業系統中有部份應用需要用到浮點運算功能,yutongshen 的作法是透過 bbl (Berkeley boot loader) 的 float emulate 功能實現浮點運算,在遇到浮點運算時跳到 bbl 進行處理之後才跳回來,但 openSBI 目前沒有支援相關功能。 2. yutongshen 曾嘗試將所有不必要的功能的關掉,但 openSBI 編譯出來的 binary 檔案還是太大,無法直接載入 soc 上的 sram ,但 yutongshen 希望能保留這個方便性,因此最後選擇使用相對較小的 bbl ![](https://hackmd.io/_uploads/ByKs0M_B2.png) ![](https://hackmd.io/_uploads/HJ02Cz_H2.png) ![](https://hackmd.io/_uploads/rJA00MdSn.png) > 出處: [An Introduction to RISC-V Boot Flow](https://riscv.org/wp-content/uploads/2019/12/Summit_bootflow.pdf) ![](https://hackmd.io/_uploads/r1N1k7_H2.png) > 出處: [OpenSBI 簡介與說明](https://forum.mapleboard.org/topic/%e6%96%87%e4%bb%b6-opensbi/) :::