opensbi下的riscv64裸機系列編程1(串口輸出)
-
1.說明
-
2.opensbi的編譯
-
3.基本環境的準備
-
3.1 準備qemu
-
3.2 準備交叉編譯工具鏈
-
-
4.工程完善
-
5.封裝的sbi接口
-
6.程序運行
-
7.printf函數的實現
-
8.小結
1.說明
前面的文章中已經提到了opensbi的作用不僅僅是一個引導作用,還提供了M模式轉換到S模式的實現,同時在S-Mode下的內核可以通過這一層訪問一些M-Mode的服務。
本文會從最小系統角度出發,利用opensbi的M-Mode的服務在控制臺上輸出Hello
。
2.opensbi的編譯
opensbi提供了三種引導啟動模式
- FW_PAYLOAD
- FW_JUMP
- FW_DYNAMIC
那么這三種模式有什么區別呢?
FW_PAYLOAD
這種模式會直接將Opensbi固件與uboot等綁定在一起。
可以說這種模式是需要bootloader的。
FW_JUMP
這種模式會直接跳轉到bootloader去執行。
這里是通過寄存器a2
傳遞了fw_dynamic_info
結構體信息。
為了簡化模型,目前只通過FW_JUMP
方式進行跳轉。
下載opensbi的代碼
gitclonehttps://github.com/riscv/opensbi.git
進行編譯
exportCROSS_COMPILE=riscv64-unknown-elf-
makePLATFORM=genericclean
makePLATFORM=genericFW_JUMP_ADDR=0x80200000
注意FW_JUMP_ADDR=0x80200000
是指定的跳轉地址。當然可以指定固件跳轉到其他的地址。
生成fw_jump.elf
位于platform/generic/firmware/fw_jump.elf
。
3.基本環境的準備
3.1 準備qemu
可以到官網下載最新的qemu
https://www.qemu.org
解壓后進行安裝與編譯。
tarxvfqemu-5.2.0.tar.xz
./configure--target-list=riscv64-softmmu
make
sudomakeinstall
3.2 準備交叉編譯工具鏈
可以到官網上下載對應的交叉編譯工具鏈
https://www.sifive.com/software
準備交叉編譯工具鏈
exportPATH=$PATH:/opt/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14/bin/
4.工程完善
相關的實驗代碼已經放到倉庫
https://github.com/bigmagic123/riscv64_opensbi_baremetal/tree/master/01_startup
工程的目錄結構如下:
.
├──build.sh##編譯腳本
├──entry.s##入口函數
├──fw_bin##可執行的固件腳本
│├──fw_jump.elf##opensbi
│├──hello.elf##編譯完成的固件
│└──run.sh##直接運行的腳本
├──link.ld##鏈接文件
├──main.c##主函數
├──readme.md
└──sbi.h##sbi調用api
首先是編譯腳本
build.sh
目前為了簡化工程,暫時沒有使用makefile文件。
riscv64-unknown-elf-gcc-nostdlib-centry.s-oentry.o
riscv64-unknown-elf-gcc-nostdlib-cmain.c-omain.o
riscv64-unknown-elf-ld-ofw_bin/hello.elf-Tlink.ldentry.omain.o
編譯了entry.s
和main.c
文件,并通過link.ld
文件進行鏈接。
link.ld
鏈接腳本規定了程序的布局
OUTPUT_ARCH("riscv")
OUTPUT_FORMAT("elf64-littleriscv")
ENTRY(_start)
SECTIONS
{
/*text:testcodesection*/
.=0x80200000;
start=.;
.text:{
stext=.;
*(.text.entry)
*(.text.text.*)
.=ALIGN(4K);
etext=.;
}
.data:{
sdata=.;
*(.data.data.*)
edata=.;
}
.bss:{
sbss=.;
*(.bss.bss.*)
ebss=.;
}
PROVIDE(end=.);
}
整體的鏈接腳本寫在SECTION{ }
包含的結構中。
其中*
代表通配符,而.
則表示當前的地址。當鏈接腳本需要使用的時候,可將其通過-T
進行參數的傳遞。
entry.s
該文件描述了執行的入口函數。
.section.text.entry
.globl_start
_start:
/*setupstack*/
lasp,stack_top#setupstackpointer
callmain
halt:jhalt#entertheinfiniteloop
loop:
jloop
.section.bss.stack
.align12
.globalstack_top
stack_top:
.space4096*4
.globalstack_top
最關鍵的是兩點:
- 設置函數堆地址
- 跳轉到main函數
stack_top:
.space4096*4
.globalstack_top
將棧頂設置,通過call
跳轉到c語言的main函數。
main.c
#include"sbi.h"
voidmain()
{
SBI_PUTCHAR('H');
SBI_PUTCHAR('e');
SBI_PUTCHAR('l');
SBI_PUTCHAR('l');
SBI_PUTCHAR('o');
SBI_PUTCHAR('
');
while(1){}
}
這個程序會調用opensbi的函數,此時可以在S-Mode訪問M-Mode的串口輸出服務。
5.封裝的sbi接口
可以通過下面的官方文檔來了解其使用。
https://github.com/riscv/riscv-sbi-doc/blob/master/riscv-sbi.adoc
在進行M-Mode服務訪問的時候,采用了ECALL進行系統調用。
在系統調用過程中,ecall會使用a0與a7寄存器。其中a7寄存器保留的是系統的調用號,而a0寄存器則保存系統的調用參數。返回值則會保存在a0寄存器中。
需要注意的是在RISCV的設計上,S模式不直接控制時鐘中斷和軟件中斷,而是使用ecall指令請求M模式設置定時器或在代理處理器中斷。
所以opensbi在提供M-Mode服務的時候,到目前為止,opensbi提供的sbi服務接口有如下的表示:
Function Name | FID | EID | Replacement EID |
---|---|---|---|
sbi_set_timer | 0 | 0x00 | 0x54494D45 |
sbi_console_putchar | 0 | 0x01 | N/A |
sbi_console_getchar | 0 | 0x02 | N/A |
sbi_clear_ipi | 0 | 0x03 | N/A |
sbi_send_ipi | 0 | 0x04 | 0x735049 |
sbi_remote_fence_i | 0 | 0x05 | 0x52464E43 |
sbi_remote_sfence_vma | 0 | 0x06 | 0x52464E43 |
sbi_remote_sfence_vma_asid | 0 | 0x07 | 0x52464E43 |
sbi_shutdown | 0 | 0x08 | 0x53525354 |
RESERVED | 0x09-0x0F |
這里只使用了sbi_console_putchar
接口。
接著看看具體的ecall的實現:
#defineSBI_ECALL(__num,__a0,__a1,__a2)
({
registerunsignedlonga0asm("a0")=(unsignedlong)(__a0);
registerunsignedlonga1asm("a1")=(unsignedlong)(__a1);
registerunsignedlonga2asm("a2")=(unsignedlong)(__a2);
registerunsignedlonga7asm("a7")=(unsignedlong)(__num);
asmvolatile("ecall"
:"+r"(a0)
:"r"(a1),"r"(a2),"r"(a7)
:"memory");
a0;
})
根據上述的解釋,ecall采用的是內嵌匯編函數。
ecall
iia0,101
lia1,0
lia2,0
lia7,1
這個內嵌匯編的展開形式如上面所示,a0
、a1
、a2
表示傳遞的參數,a7
表示系統調用號。
而根據內嵌匯編的語法,有著如下的格式
asm(assemblertemplate
:/*outputoperands*/
:/*inputoperands*/
:/*clobberedregisterslist*/
);
對于C語言來說,其函數的調用規則是處理器規定的,而編譯器可以按照這種規則進行翻譯代碼。riscv的函數調用規則可以按照下面的文檔進行操作。
https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf
而對于main函數中的SBI_PUTCHAR
其展開為
#defineSBI_CONSOLE_PUTCHAR1
#defineSBI_PUTCHAR(__a0)SBI_ECALL_1(SBI_CONSOLE_PUTCHAR,__a0)
#defineSBI_ECALL_1(__num,__a0)SBI_ECALL(__num,__a0,0,0)
可以看到通過ecall只傳遞一個參數。
6.程序運行
在fw_bin
文件夾下輸入./run.sh
就可以運行看到效果了。
而這條操作的代碼如下:
qemu-system-riscv64-Msifive_u-biosfw_jump.elf-kernelhello.elf-nographic
對應的machine是sifive_u
。bios是fw_jump.elf
。
7.printf函數的實現
對于printf函數的使用很容易,但是深入了解其實現機制,發現并不簡單,因為可變參數的特性使得其變得復雜起來。
實驗代碼如下:
https://github.com/bigmagic123/riscv64_opensbi_baremetal/tree/master/02_printf
看一個glibc
中的prinf
的實現機制。
#include
#include
#include
/*WriteformattedoutputtostdoutfromtheformatstringFORMAT.*/
/*VARARGS1*/
intprintf(constchar*format,...)
{
va_listarg;
intdone;
va_start(arg,format);
done=vprintf(format,arg);
va_end(arg);
returndone;
}
對于上述的定義
intprintf(constchar*format,...)
format
表示固定的參數,...
表示可變的參數。
主要的實現過程利用三個函數進行
va_start(p,format)//將指針p移到第一個變量參數
var=va_arg(p,變量類型)//已知變量的情況下,移到下個參數變量
va_end(p)//結束參數使用等價于p=NULL
這里為了實現方便,我直接使用開源的tinyprintf
。
https://github.com/cjlano/tinyprintf
移植的過程也很容易,在main.c
文件中作如下的實現:
#include"sbi.h"
#include"tinyprintf.h"
#defineUNUSED(x)(void)(x)
staticvoidstdout_putc(void*unused,char*ch)
{
SBI_PUTCHAR(ch);
}
voidmain()
{
init_printf(0,stdout_putc);
tfp_printf("helloworld
");
while(1){}
}
只需要移植init_printf
接口就可以使用tfp_printf
進行串口輸出了。
結果如下:
8.小結
第一階段實現了opensbi的啟動流程,同時通過系統調用訪問串口輸出。已經實現了S-Mode下訪問M-Mode的初步計劃,并且通過串口進行基本的輸出過程。隨著工程的不斷增加,后續會增加makefile工程組織,riscv下的中斷處理、以及定時器中斷的實現,下篇文章主要介紹這些。
-
編程
+關注
關注
88文章
3521瀏覽量
93273 -
串口
+關注
關注
14文章
1533瀏覽量
75465 -
RISC
+關注
關注
6文章
460瀏覽量
83566
原文標題:opensbi下的riscv64裸機系列編程1(串口輸出)
文章出處:【微信號:Embeded_IoT,微信公眾號:嵌入式IoT】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論