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文章
3576浏览量
93545 -
串口
+关注
关注
14文章
1540浏览量
76101 -
RISC
+关注
关注
6文章
461浏览量
83638
原文标题:opensbi下的riscv64裸机系列编程1(串口输出)
文章出处:【微信号:Embeded_IoT,微信公众号:嵌入式IoT】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论