前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >opensbi下的riscv64裸机系列编程1(串口输出)

opensbi下的riscv64裸机系列编程1(串口输出)

作者头像
bigmagic
发布2021-01-08 11:24:56
4.5K0
发布2021-01-08 11:24:56
举报
文章被收录于专栏:嵌入式iot

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去执行。

这个对于qemu的启动模式来说十分的有用。

FW_DYNAMIC

这种模式跳转的时候会传递动态的参数

这里是通过寄存器a2传递了fw_dynamic_info结构体信息。

为了简化模型,目前只通过FW_JUMP方式进行跳转。

下载opensbi的代码

代码语言:javascript
复制
git clone https://github.com/riscv/opensbi.git

进行编译

代码语言:javascript
复制
export CROSS_COMPILE=riscv64-unknown-elf-
make PLATFORM=generic clean
make PLATFORM=generic FW_JUMP_ADDR=0x80200000

注意FW_JUMP_ADDR=0x80200000是指定的跳转地址。当然可以指定固件跳转到其他的地址。

生成fw_jump.elf位于platform/generic/firmware/fw_jump.elf

3.基本环境的准备

3.1 准备qemu

可以到官网下载最新的qemu

代码语言:javascript
复制
https://www.qemu.org

解压后进行安装与编译。

代码语言:javascript
复制
tar xvf qemu-5.2.0.tar.xz
./configure --target-list=riscv64-softmmu
make
sudo make install

3.2 准备交叉编译工具链

可以到官网上下载对应的交叉编译工具链

代码语言:javascript
复制
https://www.sifive.com/software

准备交叉编译工具链

代码语言:javascript
复制
export PATH=$PATH:/opt/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14/bin/

4.工程完善

相关的实验代码已经放到仓库

代码语言:javascript
复制
https://github.com/bigmagic123/riscv64_opensbi_baremetal/tree/master/01_startup

工程的目录结构如下:

代码语言:javascript
复制
.
├── 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文件。

代码语言:javascript
复制
riscv64-unknown-elf-gcc -nostdlib -c entry.s -o entry.o
riscv64-unknown-elf-gcc -nostdlib -c main.c -o main.o
riscv64-unknown-elf-ld -o fw_bin/hello.elf -Tlink.ld entry.o main.o

编译了entry.smain.c文件,并通过link.ld文件进行链接。

link.ld

链接脚本规定了程序的布局

代码语言:javascript
复制
OUTPUT_ARCH( "riscv" )
OUTPUT_FORMAT("elf64-littleriscv")
ENTRY( _start )
SECTIONS
{
  /* text: test code section */
  . = 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

该文件描述了执行的入口函数。

代码语言:javascript
复制
    .section .text.entry
    .globl _start
_start:
    /* setup stack */
    la    sp, stack_top           # setup stack pointer
    call main
halt:   j     halt                    # enter the infinite loop

loop:
    j loop

    .section .bss.stack
    .align 12
    .global stack_top
stack_top:
    .space 4096 * 4
    .global stack_top

最关键的是两点:

  • 设置函数堆地址
  • 跳转到main函数
代码语言:javascript
复制
stack_top:
    .space 4096 * 4
    .global stack_top

将栈顶设置,通过call跳转到c语言的main函数。

main.c

代码语言:javascript
复制
#include "sbi.h"
void main()
{
    SBI_PUTCHAR('H');
    SBI_PUTCHAR('e');
    SBI_PUTCHAR('l');
    SBI_PUTCHAR('l');
    SBI_PUTCHAR('o');
    SBI_PUTCHAR('\n');
    while(1) {}
}

这个程序会调用opensbi的函数,此时可以在S-Mode访问M-Mode的串口输出服务。

5.封装的sbi接口

可以通过下面的官方文档来了解其使用。

代码语言:javascript
复制
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的实现:

代码语言:javascript
复制
#define SBI_ECALL(__num, __a0, __a1, __a2)                           \
({                                                                  \
    register unsigned long a0 asm("a0") = (unsigned long)(__a0);    \
    register unsigned long a1 asm("a1") = (unsigned long)(__a1);    \
    register unsigned long a2 asm("a2") = (unsigned long)(__a2);    \
    register unsigned long a7 asm("a7") = (unsigned long)(__num);   \
    asm volatile("ecall"                                            \
                 : "+r"(a0)                                         \
                 : "r"(a1), "r"(a2), "r"(a7)                        \
                 : "memory");                                       \
    a0;                                                             \
})

根据上述的解释,ecall采用的是内嵌汇编函数。

代码语言:javascript
复制
ecall
 ii a0,101
 li a1,0
 li a2,0
 li a7,1

这个内嵌汇编的展开形式如上面所示,a0a1a2表示传递的参数,a7表示系统调用号。

而根据内嵌汇编的语法,有着如下的格式

代码语言:javascript
复制
asm(assembler template
    : /* output operands */
    : /* input operands */
    : /* clobbered registers list */
);

对于C语言来说,其函数的调用规则是处理器规定的,而编译器可以按照这种规则进行翻译代码。riscv的函数调用规则可以按照下面的文档进行操作。

代码语言:javascript
复制
https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf

而对于main函数中的SBI_PUTCHAR其展开为

代码语言:javascript
复制
#define SBI_CONSOLE_PUTCHAR 1
#define SBI_PUTCHAR(__a0) SBI_ECALL_1(SBI_CONSOLE_PUTCHAR, __a0)
#define SBI_ECALL_1(__num, __a0) SBI_ECALL(__num, __a0, 0, 0)

可以看到通过ecall只传递一个参数。

6.程序运行

fw_bin文件夹下输入./run.sh就可以运行看到效果了。

而这条操作的代码如下:

代码语言:javascript
复制
qemu-system-riscv64 -M  sifive_u -bios fw_jump.elf -kernel hello.elf -nographic

对应的machine是sifive_u。bios是fw_jump.elf

7.printf函数的实现

对于printf函数的使用很容易,但是深入了解其实现机制,发现并不简单,因为可变参数的特性使得其变得复杂起来。

实验代码如下:

代码语言:javascript
复制
https://github.com/bigmagic123/riscv64_opensbi_baremetal/tree/master/02_printf

看一个glibc中的prinf的实现机制。

代码语言:javascript
复制
#include <ansidecl.h>
#include <stdarg.h>
#include <stdio.h>
 
/* Write formatted output to stdout from the format string FORMAT.  */
/* VARARGS1 */
int printf(const char *format,...)
{
  va_list arg;
  int done;
 
  va_start(arg, format);
  done = vprintf(format, arg);
  va_end(arg);
 
  return done;
}

对于上述的定义

代码语言:javascript
复制
int printf(const char *format,...)

format表示固定的参数,...表示可变的参数。

主要的实现过程利用三个函数进行

代码语言:javascript
复制
va_start(p,format) //将指针p移到第一个变量参数
var=va_arg(p,变量类型)//已知变量的情况下,移到下个参数变量
va_end(p)//结束参数使用等价于p=NULL

这里为了实现方便,我直接使用开源的tinyprintf

代码语言:javascript
复制
https://github.com/cjlano/tinyprintf

移植的过程也很容易,在main.c文件中作如下的实现:

代码语言:javascript
复制
#include "sbi.h"
#include "tinyprintf.h"
#define UNUSED(x) (void)(x)
static void stdout_putc(void *unused,char *ch)
{
        SBI_PUTCHAR(ch);
}
void main()
{
    init_printf(0, stdout_putc);

    tfp_printf("hello world\n");
    while(1) {}
}

只需要移植init_printf接口就可以使用tfp_printf进行串口输出了。

结果如下:

8.小结

第一阶段实现了opensbi的启动流程,同时通过系统调用访问串口输出。已经实现了S-Mode下访问M-Mode的初步计划,并且通过串口进行基本的输出过程。随着工程的不断增加,后续会增加makefile工程组织,riscv下的中断处理、以及定时器中断的实现,下篇文章主要介绍这些。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式IoT 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • opensbi下的riscv64裸机系列编程1(串口输出)
    • 1.说明
      • 2.opensbi的编译
        • 3.基本环境的准备
          • 3.1 准备qemu
          • 3.2 准备交叉编译工具链
        • 4.工程完善
          • 5.封装的sbi接口
            • 6.程序运行
              • 7.printf函数的实现
                • 8.小结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档