文章目錄
- 一、 引言
- 二、 環境準備
- 三、編寫簡單的RISC-V程序
- 四、 編譯步驟詳解
- 五、使用QEMU運行程序
- 六、程序詳解
- 七、退出QEMU
- 八、總結
- 附錄:QEMU中通過UTRA顯示字符工作原理
- 1、內存映射I/O原理
- 2、add.s程序工作流程
- 3、關鍵指令解析
- 4、QEMU模擬的UART控制器
- 5、為什么不需要初始化UART?
- 6、字符如何顯示到終端?
- 7、擴展知識:處理UART狀態
- 總結
一、 引言
RISC-V作為一種開源指令集架構,近年來在嵌入式系統和高性能計算領域備受關注。借助QEMU模擬器,我們可以在Ubuntu主機上輕松測試和運行RISC-V程序,無需真實硬件。本文將詳細介紹如何使用riscv64-unknown-elf-gcc
工具鏈編譯一個簡單的RISC-V程序,并通過QEMU模擬器啟動它。
二、 環境準備
首先需要在Ubuntu系統上安裝必要的工具鏈和依賴:
# 安裝RISC-V交叉編譯工具鏈
sudo apt-get update
sudo apt-get install gcc-riscv64-unknown-elf binutils-riscv64-unknown-elf# 安裝QEMU模擬器
sudo apt-get install qemu-system-riscv64# 驗證安裝結果
riscv64-unknown-elf-gcc --version
qemu-system-riscv64 --version
三、編寫簡單的RISC-V程序
下面是一個簡單的RISC-V匯編程序,它將兩個數相加并通過串口輸出結果:
# add.s - 使用直接UART操作的版本.section .text.globl _start_start:# 設置棧指針la sp, stack_top# 初始化兩個數li a0, 10li a1, 20# 相加add a2, a0, a1# 直接操作UART輸出結果la a0, msgcall print_stringmv a0, a2call print_intla a0, newlinecall print_string# 無限循環
loop:j loop# 打印字符串函數
print_string:li t0, 0x10000000 # UART基地址
ps_loop:lb t1, 0(a0) # 加載字符beqz t1, ps_done # 如果是NULL,結束sb t1, 0(t0) # 寫入UART數據寄存器addi a0, a0, 1 # 指向下一個字符j ps_loop
ps_done:ret# 打印整數函數(簡化版)
print_int:li t0, 0x10000000 # UART基地址li t1, 10 # 除數# 將數字轉換為ASCII并輸出# 此處為簡化實現,實際需要更復雜的轉換邏輯li t2, '0'add t2, t2, a0sb t2, 0(t0)ret.section .rodata
msg:.string "計算結果: "
newline:.string "\n".section .bss.align 3
stack:.space 4096
stack_top:
四、 編譯步驟詳解
接下來我們使用RISC-V交叉編譯工具鏈編譯這個程序:
# 1. 編譯匯編代碼為目標文件
riscv64-unknown-elf-as -march=rv64g -mabi=lp64 add.s -o add.o# 2. 創建鏈接腳本link.ld
cat > link.ld << EOF
ENTRY(_start)SECTIONS {.text 0x80000000 : {*(.text)}.data : {*(.data)}.bss : {*(.bss)}
}
EOF# 3. 鏈接目標文件
riscv64-unknown-elf-ld -T link.ld add.o -o add.elf# 4. 轉換為二進制格式
riscv64-unknown-elf-objcopy -O binary add.elf add.bin# 5. 生成可執行文件
riscv64-unknown-elf-objcopy -O elf64-littleriscv add.elf add
五、使用QEMU運行程序
編譯完成后,我們可以使用QEMU模擬器運行生成的RISC-V程序:
qemu-system-riscv64 \-machine virt \-cpu rv64 \-m 128M \-nographic \-bios none \-kernel add.elf \
如果一切正常,你將在終端看到以下輸出:
計算結果: 3
六、程序詳解
這個簡單的RISC-V程序包含幾個關鍵部分:
- 初始化部分:設置棧指針并初始化要相加的兩個數
- 計算部分:執行加法運算
- 輸出部分:通過系統調用將結果輸出到UTRA
- 退出部分:調用exit系統調用結束程序
值得注意的是,我們使用了QEMU虛擬平臺提供的系統調用接口來實現輸出功能。在真實硬件上,可能需要通過操作UART寄存器來實現相同的功能。
七、退出QEMU
退出qemu-system-riscv64
通常可以使用快捷鍵或通過監視器界面來操作,具體方法如下:
- 使用快捷鍵:按下
Ctrl + a
,然后松開這兩個鍵,再按下x
,即可直接終止QEMU進程,回到shell界面。 - 通過監視器界面:首先按下
Ctrl + a
,然后松開,再按下c
,這將退出當前操作系統的shell界面,進入QEMU的監視器界面。接著在監視器界面中,輸入q
并按回車鍵,即可完全退出QEMU。
八、總結
通過本文的步驟,你已經學會了如何在Ubuntu上使用RISC-V交叉編譯工具鏈編寫、編譯一個簡單的匯編程序,并通過QEMU模擬器運行它。這為進一步開發更復雜的RISC-V應用程序奠定了基礎。后續你可以嘗試添加更復雜的功能,如C語言支持、設備驅動等。
附錄:QEMU中通過UTRA顯示字符工作原理
本附錄是QEMU系統中,UTRA顯示的工作原理。供理解上面add.s程序是如何輸出的。
在嵌入式系統中,與外部設備(如屏幕、串口)通信通常通過**內存映射I/O(Memory-Mapped I/O)**實現。在RISC-V架構的QEMU模擬環境中,向特定內存地址寫入數據實際上是向模擬的UART(通用異步收發傳輸器)控制器發送字符,最終顯示在終端上。以下是詳細的工作原理解析:
1、內存映射I/O原理
在計算機系統中,外設(如串口、硬盤)的控制寄存器被映射到特定的內存地址空間。CPU可以像訪問內存一樣訪問這些地址,從而控制外設的行為。
在QEMU模擬的RISC-V virt
平臺中:
- UART基地址:
0x10000000
- 向該地址寫入一個字節數據,相當于通過串口發送一個字符
- 讀取該地址,則獲取接收到的字符
2、add.s程序工作流程
以下是 add.s
程序的打印關鍵部分:
# 打印字符串函數
print_string:li t0, 0x10000000 # UART基地址
ps_loop:lb t1, 0(a0) # 加載字符,a0是調用print_string函數的時候,輸入字符串的地址beqz t1, ps_done # 如果是NULL,結束sb t1, 0(t0) # 寫入UART數據寄存器addi a0, a0, 1 # 指向下一個字符j ps_loop
ps_done:
3、關鍵指令解析
-
li(Load Immediate):
li t0, 0x10000000
- 將立即數(常量)
0x10000000
加載到寄存器t0
中 - 相當于
t0 = 0x10000000;
- 將立即數(常量)
-
sb(Store Byte):
sb t1, 0(t0)
- 將寄存器
t1
的低8位(一個字節)存儲到地址t0 + 0
- 相當于
*(uint8_t*)t0 = t1 & 0xFF;
- 將寄存器
4、QEMU模擬的UART控制器
QEMU的 virt
平臺模擬了一個 16550兼容UART控制器,其簡化結構如下:
偏移地址 | 寄存器名稱 | 功能 |
---|---|---|
0x00 | RBR/THR/DLL | 接收/發送緩沖區 |
0x01 | IER/DLM | 中斷使能寄存器 |
0x02 | IIR/FCR | 中斷標識/FIFO控制 |
0x03 | LCR | 線路控制寄存器 |
在 test.s
中,我們直接操作的是 THR(Transmitter Holding Register):
- 當向
0x10000000
寫入數據時,數據被放入發送緩沖區 - UART控制器會自動將緩沖區中的數據轉換為串行信號發送
- QEMU捕獲這些模擬的串行信號,并將其轉換為終端輸出
5、為什么不需要初始化UART?
在QEMU的 virt
平臺中:
- UART控制器默認已配置為 8數據位、無校驗、1停止位(8N1)
- 波特率設置為 115200bps
- 這些默認配置適用于大多數簡單應用,因此無需額外初始化
在真實硬件上,通常需要先配置LCR(線路控制寄存器)、IER(中斷使能寄存器)等:
# 真實硬件上的UART初始化示例
li t0, 0x10000000 # UART基地址# 設置波特率為115200
li t1, 0x00 # 除數寄存器值(對于115200bps)
sw t1, 0(t0) # DLL (除數鎖存器低位)
sw t1, 1(t0) # DLM (除數鎖存器高位)# 配置為8N1模式
li t1, 0x03 # 8數據位, 1停止位, 無校驗
sw t1, 3(t0) # LCR (線路控制寄存器)
6、字符如何顯示到終端?
整個數據流向如下:
- CPU執行
sb t1, 0(t0)
指令 - 數據被寫入內存地址
0x10000000
- QEMU檢測到對該地址的寫操作
- QEMU模擬UART控制器的行為,將數據轉換為字符
- QEMU將字符輸出到宿主系統的終端
7、擴展知識:處理UART狀態
在更復雜的應用中,需要檢查UART狀態以確保數據成功發送:
# 帶狀態檢查的UART發送函數
uart_putc:li t0, 0x10000000 # UART基地址li t1, 0x10000005 # LSR (線路狀態寄存器)地址wait_tx_ready:lb t2, 0(t1) # 讀取LSRandi t2, t2, 0x20 # 檢查THRE位(bit 5)beqz t2, wait_tx_ready # 如果THRE=0,繼續等待sb a0, 0(t0) # 發送字符ret
總結
在 add.s
程序中,通過向 0x10000000
地址寫入數據,實際上是利用了QEMU模擬的UART控制器的內存映射I/O特性。這種方式直接、高效,適用于簡單的輸出需求。在實際開發中,根據硬件平臺的不同,可能需要更復雜的初始化和錯誤處理邏輯。