RISCV基础开发(六)

gewenbin
gewenbin
gewenbin
188
文章
15
评论
2021年8月17日22:57:10 评论 1,403

QEMU裸机开发之打印字符

1.文件结构总览

先整体看下本章节裸机程序的工程文件组成,如下图所示。

RISCV基础开发(六)

  • address.h:主要定义了一些外设寄存器的基址还有内存基址等。
  • entry.S:入口文件,主要设置了栈。
  • kernel.ld:编译用的链接脚本。
  • Makefile:makefile文件,控制源码的编译过程。
  • start.c:是C语言实现的入口,在entry.S中会调转到C函数中。
  • uart.c和uart.h:串口驱动源码。

2.Makefile

Makefile控制着源码的编译,如下图所示。

# compile objects set
KERNEL_IMAGE_NAME=kernelimage

OBJS = \
  entry.o \
  start.o \
  uart.o

# cross compiler and flag set
CROSS_COMPILER = riscv64-unknown-elf-
CC = $(CROSS_COMPILER)gcc
AS = $(CROSS_COMPILER)gas
LD = $(CROSS_COMPILER)ld
OBJCOPY = $(CROSS_COMPILER)objcopy
OBJDUMP = $(CROSS_COMPILER)objdump

CFLAGS = -Wall -Werror -O -fno-omit-frame-pointer -ggdb
CFLAGS += -mcmodel=medany
CFLAGS += -ffreestanding -fno-common -nostdlib -mno-relax
CFLAGS += -I.
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)

# Disable PIE when possible (for Ubuntu 16.10 toolchain)
ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]no-pie'),)
CFLAGS += -fno-pie -no-pie
endif
ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]nopie'),)
CFLAGS += -fno-pie -nopie
endif

LDFLAGS = -z max-page-size=4096

# compile kernel
$(KERNEL_IMAGE_NAME): $(OBJS) kernel.ld
    $(LD) $(LDFLAGS) -T kernel.ld -o $(KERNEL_IMAGE_NAME) $(OBJS) 
    $(OBJDUMP) -S $(KERNEL_IMAGE_NAME) > kernel.asm
    $(OBJDUMP) -t $(KERNEL_IMAGE_NAME) | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernel.sym
    
clean: 
    rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg \
    $(OBJS) *.asm *.sym \
    $(KERNEL_IMAGE_NAME)
    
# qemu set
QEMU = qemu-system-riscv64

ifndef CPUS
CPUS := 1
endif

QEMUOPTS = -machine virt -bios none -kernel $(KERNEL_IMAGE_NAME) -m 128M -smp $(CPUS) -nographic

qemu: $(KERNEL_IMAGE_NAME)
    $(QEMU) $(QEMUOPTS)

这个makefile修改自xv6工程的makefile,需要注意的是qemu的启动命令,我们使用了“-bios none”选项,表示不使用OpenSBI这类的软件,而是直接加载我们编译的程序运行,所以我们写的程序开始就是运行在m模式下的。这里默认使用了128MB内存,并且使用单核方式。

3.链接脚本

链接脚本控制着整个程序的链接地址,如下图所示。

OUTPUT_ARCH( "riscv" )
ENTRY( _entry )

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  . = 0x80000000;

  .text : {
    *(.text .text.*)
    . = ALIGN(0x1000);
    PROVIDE(etext = .);
  }

  .rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
  }

  .data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
  }

  .bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
  }
  
  .stack (NOLOAD) : {
    . = ALIGN(16);
    PROVIDE (__stack_start = .);
    
    . += 128 * 1024;
    
    . = ALIGN(16);
    PROVIDE (__stack_end = .);
  }

  PROVIDE(end = .);
}

整个程序链接地址是从0x80000000开始的,然后划分了5个段:代码段、只读数据段、数据段、清零数据段、栈段。栈段默认分配了128KB空间用于m模式和s模式下正常和异常时使用,这个在后面涉及到时会具体讲解。

4.入口处理

entry.S主要是设置了栈地址,因为我们使用的是模拟器,所以不需要初始化内存这类外设,只需要将栈设置好就可以跳转到c语言函数运行了,如下所示。

.section .text

.global _entry
_entry:

# sp = __stack_start + ((hartid + 1) * 4096)
    la sp, __stack_start
    li a0, 4096
    csrr a1, mhartid
    addi a1, a1, 1
    mul a0, a0, a1
    add sp, sp, a0

    call start
    
loop:
    j loop

这段代码参考自xv6,原来是可以设置多核下的栈的,因为现在我们只使用了单核,所以栈地址被设置为__stack_start+4096这个值,__stack_start就是在链接脚本里定义的,这个栈是m模式正常运行时所使用的栈,大小为4KB。栈设置完成之后,直接就调用c函数实现的start进行处理。

5.C语言主函数处理

strat.c中目前只有一个c语言实现的start函数,如下所示。

#include "uart.h"

void start(void)
{
    uart_puts("in start.\r\n");
}

目前的实现很简单,就是调用串口驱动发送字符串函数“uart_puts”打印一串信息。

6.串口驱动

我们qemu选用的平台是virt,使用的串口为16550兼容串口,由于是模拟器,我们可以直接实现发送函数就可以实现字符输出打印功能,而不需要去初始化,如下所示。

#include "address.h"

// the UART control registers.
// some have different meanings for
// read vs write.
// see http://byterunner.com/16550.html
#define RHR 0                 // receive holding register (for input bytes)
#define THR 0                 // transmit holding register (for output bytes)
#define IER 1                 // interrupt enable register
#define IER_RX_ENABLE (1<<0)
#define IER_TX_ENABLE (1<<1)
#define FCR 2                 // FIFO control register
#define FCR_FIFO_ENABLE (1<<0)
#define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs
#define ISR 2                 // interrupt status register
#define LCR 3                 // line control register
#define LCR_EIGHT_BITS (3<<0)
#define LCR_BAUD_LATCH (1<<7) // special mode to set baud rate
#define LSR 5                 // line status register
#define LSR_RX_READY (1<<0)   // input is waiting to be read from RHR
#define LSR_TX_IDLE (1<<5)    // THR can accept another character to send

// the UART control registers are memory-mapped
// at address UART0. this macro returns the
// address of one of the registers.
#define Reg(reg) ((volatile unsigned char *)(UART0_REG_BASE + reg))
#define ReadReg(reg) (*(Reg(reg)))
#define WriteReg(reg, v) (*(Reg(reg)) = (v))

// add a character to the output buffer and tell the
// UART to start sending if it isn't already.
void uart_putc(char c)
{
    // wait for Transmit Holding Empty to be set in LSR.
    while((ReadReg(LSR) & LSR_TX_IDLE) == 0);

    WriteReg(THR, c);
}

void uart_puts(char *msg)
{
    char c;

    if (!msg) {
        return;
    }

    while ((c = *msg) != '\0') {
        uart_putc(c);
        msg++;
    }
}

逻辑也很简单,就是轮询等待发送寄存器为空,然后再写入数据发送,串口模块寄存器基址是定义在address.h中的,如下所示。

#ifndef QEMU_RISCV64_ADDRESS_H_
#define QEMU_RISCV64_ADDRESS_H_

// the kernel expects there to be RAM
// for use by the kernel and user pages
// from physical address 0x80000000 to PHYSTOP.
#define KERNBASE 0x80000000L
#define PHYSTOP (KERNBASE + 128*1024*1024)

#define UART0_REG_BASE      (0x10000000L)
#define CLINT_REG_BASE      (0x02000000L)

#endif /* QEMU_RISCV64_ADDRESS_H_ */

其中CLINT的基址在后面设置核间中断和timer会用到,后面会讲解。

7.测试

在命令行输入“make qemu”即可编译,编译完成后会自动启动qemu运行程序,如果一切顺利会在最后看到“in start.”这个信息输出,如下所示。

gewenbin@gewenbin-virtual-machine:~/Desktop/qemu_test/lesson1$ make qemu
riscv64-unknown-elf-gcc    -c -o entry.o entry.S
riscv64-unknown-elf-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie   -c -o start.o start.c
riscv64-unknown-elf-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie   -c -o uart.o uart.c
riscv64-unknown-elf-ld -z max-page-size=4096 -T kernel.ld -o kernelimage entry.o start.o uart.o 
riscv64-unknown-elf-objdump -S kernelimage > kernel.asm
riscv64-unknown-elf-objdump -t kernelimage | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel.sym
qemu-system-riscv64 -machine virt -bios none -kernel kernelimage -m 128M -smp 1 -nographic
in start.
QEMU: Terminated
gewenbin@gewenbin-virtual-machine:~/Desktop/qemu_test/lesson1$

如果想从qemu中退出到命令行,先按住“ctrl+a”,然后再按“x”键即可。输入“make clean”即可清零编译结果。

8. 工程源码

链接:https://pan.baidu.com/s/1TnTYr7mywdKj5bxpdmWnyA,提取码:q772,见lesson1。

gewenbin
  • 本文由 发表于 2021年8月17日22:57:10
  • 转载请务必保留本文链接:http://www.databusworld.cn/10516.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: