本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。
RVOS是本课程基于RISC-V搭建的简易操作系统名称。
片上系统(System-on-Chip,SoC)是指将多个组件和功能集成到单个集成电路(IC)上的完整电子系统或微芯片。它将处理器核心、存储器、输入/输出接口、外设和其他系统组件等多个硬件组件集成在一颗芯片上。
QEMU(Quick EMUlator)是一个开源的虚拟化软件,用于模拟多种硬件平台和体系结构。“QEMU virt” 是 QEMU 中的一个虚拟平台,用于模拟基于 ARM 架构的虚拟机。它是 QEMU 支持的多个虚拟平台之一,常用于开发和测试 ARM 架构相关的软件和系统。
QEMU 是一个虚拟化平台,它通过模拟不同的硬件设备和处理器架构,提供了统一的编址和访问方式。在 QEMU 中,所有的设备都被虚拟化为统一的地址空间,并通过内存映射来访问这些设备。这样,操作系统和应用程序可以使用统一的编程接口和地址空间访问不同的设备,而不需要关注实际的物理硬件细节。
QEMU跑起来之后,BootLoader会跳转到0X8000-0000处继续执行。
QEMU的运行命令参数会携带-kernel参数,该参数指明加载我们的os.elf内核文件到内存。并且os.elf文件在链接时也指明了text代码段被加载到内存中的0x8000 0000位置处。
QEMU默认提供8个模拟的hart,这8个hart一上电都会去运行我们的kernel程序,课程为了简单起见,默认只会使用一个hart,其余hart让其进行空转:
因此,我们需要在kernel启动程序中编写程序完成上面的需求。
我们关注的是最上面的Machine Information Registers这组寄存器,这组寄存器中存放了当前机器的相关状态信息,比如: 当前hart的id.
为了读写这组状态寄存器,我们需要使用专门的CSR指令:
CSRRW指令(原子读写CSR寄存器): 一般可用于实现两个寄存器值的交换,并且这个过程是原子性的,不可打断
如果RD位为x0,则相当于将rs赋值给csr寄存器,因为向x0寄存器写入数据是没有意义的。
CSRRS(原子读并设置CSR中某一位的值):
如果RS位为x0,则只是单独对CSR寄存器进行读取。
经过上面的分析可知,如果要实现我们的需求,则需要读取mhartid寄存器:
核心汇编代码如下:
_start:
# park harts with id != 0
csrr t0, mhartid # read current hart id
mv tp, t0 # keep CPU's hartid in its tp for later usage.
bnez t0, park # if we're not on the halt0,we park the hart
...
park:
wfi
j park
Wait for Interrupt instruction (WFI)
是 RISC-V架构定义的一条休眠指令。当处理器执行到 WFI 指令之后,将会停止执行当前的指令流,进入一种空闲状态。这种空闲状态可以被称为“休眠;"状态,直到处理器接收到中断,
这里给出start.S汇编代码:
#include "platform.h"
# 每个硬件线程的栈大小为1024字节
.equ STACK_SIZE, 1024
# 声明符号 _start 为全局符号。它是程序的入口点。
.global _start
# 指定以下代码属于 .text 段,其中包含可执行指令。它标志着 _start 代码的开始。
.text
_start:
# park harts with id != 0
csrr t0, mhartid # 读取当前硬件线程的ID
mv tp, t0 # 将CPU的硬件线程ID保存在tp寄存器中以备后用
bnez t0, park # 如果不是硬件线程0,则进入休眠状态
slli t0, t0, 10 # 将硬件线程ID左移10位(相当于乘以1024)
la sp, stacks + STACK_SIZE # 将初始栈指针设置为第一个栈空间的末尾
add sp, sp, t0 # 将当前硬件线程的栈指针移动到栈空间中的相应位置
j start_kernel # hart 0 jump to c
park:
wfi
j park
stacks:
.skip STACK_SIZE * MAXNUM_CPU # 为所有硬件线程分配栈空间
.end # 文件结束
这里我们主要关注,如何做到为每个hart分配独立的栈空间(彼此隔离):
# Setup stacks, the stack grows from bottom to top, so we put the
# stack pointer to the very end of the stack range.
slli t0, t0, 10 # 将当前硬件线程 ID 左移 10 位(相当于乘以 1024)
la sp, stacks + STACK_SIZE # 将栈指针设置为第一个栈空间的末尾
add sp, sp, t0 # 将当前硬件线程的栈指针移动到其在栈空间中的位置
这段代码通过使用每个硬件线程的唯一硬件线程ID来隔离每个hart的栈空间。每个硬件线程的ID不同,因此通过将硬件线程ID左移10位(相当于乘以1024),可以为每个硬件线程分配独立的栈空间。
t0
中,使用csrr
指令。la
指令将栈指针sp
设置为stacks
标签加上STACK_SIZE
,即第一个栈空间的末尾地址。这样可以将栈指针设置为栈空间的最末尾。add
指令将栈指针sp
与硬件线程ID左移10位的结果相加,以将当前硬件线程的栈指针移动到其在栈空间中的位置。由于每个硬件线程的ID不同,因此每个硬件线程的栈指针会被正确地定位到其分配的独立栈空间的位置。例如:
UART代表通用异步收发器(Universal Asynchronous Receiver-Transmitter)
。它是一种常用的串行通信协议,用于两个设备之间的通信。UART协议允许一次只传输和接收一位数据,通过单个数据线进行通信。
UART被广泛应用于各种应用中,包括嵌入式系统、微控制器以及计算机、调制解调器和传感器等不同设备之间的通信接口。它提供了一种简单高效的方法,用于设备之间的数据传输和接收。
UART通信包括起始位,随后是数据位(通常为8位),用于错误检测的可选奇偶校验位,以及停止位或多个停止位。起始位表示数据帧的开始,而停止位表示帧的结束。数据以异步方式传输,意味着设备之间没有共享时钟信号。
UART在点对点配置中运行,其中两个设备直接连接使用两条数据线:一条用于发送数据(TX),一条用于接收数据(RX)。一个设备的TX线连接到另一个设备的RX线,反之亦然。这允许设备之间的双向通信。
UART物理接口通常由以下几个引脚组成:
除了上述必要的引脚外,UART接口可能还包括其他可选引脚,如:
这些引脚的具体命名和功能可能在不同的设备和应用中有所不同,但上述列举的是UART接口常见的引脚。
QUME是一个知名的串口模拟器和仿真工具,它可以模拟各种串口设备,包括NS16550A芯片所提供的功能。因此,通过QUME,可以模拟NS16550A串口芯片的行为和接口。
使用QUME,可以创建虚拟串口设备,并通过配置参数来模拟NS16550A芯片的寄存器、数据传输、中断和状态等功能。因此我们能够进行串口通信的仿真和测试,而无需实际的硬件设备。
QUME提供了丰富的功能,包括模拟不同的串口参数、配置波特率、数据位数、校验位、停止位等,并且可以模拟接收和发送数据,监测串口状态和中断等。这样可以在虚拟环境中进行串口编程和调试,以确保代码在实际环境中正常工作。
需要注意的是,QUME是一个软件工具,它提供了对串口功能的模拟和仿真,但并不直接与硬件设备通信。因此,在实际使用中,QUME可以作为开发、测试和调试串口通信应用程序的有用工具,但在实际的硬件系统中,需要使用NS16550A芯片或其他串口硬件来实现真正的串口通信。
NS16550A是一种常用的串口通信芯片,它提供了一个编程接口,用于配置和控制串口通信功能。以下是NS16550A芯片的编程接口的基本介绍:
通过访问这些寄存器,可以对NS16550A芯片进行编程控制,实现对串口通信的配置、数据传输和状态监测等操作。具体的编程接口使用方式和寄存器地址等信息可以参考NS16550A芯片的数据手册或相关文档。
在这里,"关闭中断"指的是禁用串口(UART)的中断功能,即禁止串口触发和处理中断事件。
串口通信中的中断通常用于以下目的:
通过禁用中断,就是告诉串口不要触发和处理这些中断事件。这样可以避免在初始化期间由于中断的发生而引起的干扰和错误。
禁用中断不会影响串口的数据传输功能,它仅仅是关闭了中断的触发和处理机制。一旦初始化完成,并且需要启用中断来处理接收和发送数据的中断事件时,可以通过适当的设置和配置重新启用中断。
完整代码注释如下: (可参考NS16550a相关文档进行学习)
void uart_init()
{
/* disable interrupts. */
uart_write_reg(IER, 0x00);
/*
* Setting baud rate. Just a demo here if we care about the divisor,
* but for our purpose [QEMU-virt], this doesn't really do anything.
*
* Notice that the divisor register DLL (divisor latch least) and DLM (divisor
* latch most) have the same base address as the receiver/transmitter and the
* interrupt enable register. To change what the base address points to, we
* open the "divisor latch" by writing 1 into the Divisor Latch Access Bit
* (DLAB), which is bit index 7 of the Line Control Register (LCR).
*
* Regarding the baud rate value, see [1] "BAUD RATE GENERATOR PROGRAMMING TABLE".
* We use 38.4K when 1.8432 MHZ crystal, so the corresponding value is 3.
* And due to the divisor register is two bytes (16 bits), so we need to
* split the value of 3(0x0003) into two bytes, DLL stores the low byte,
* DLM stores the high byte.
*/
uint8_t lcr = uart_read_reg(LCR);
uart_write_reg(LCR, lcr | (1 << 7));
uart_write_reg(DLL, 0x03);
uart_write_reg(DLM, 0x00);
/*
* Continue setting the asynchronous data communication format.
* - number of the word length: 8 bits
* - number of stop bits: 1 bit when word length is 8 bits
* - no parity
* - no break control
* - disabled baud latch
*/
lcr = 0;
uart_write_reg(LCR, lcr | (3 << 0));
}
相关宏定义:
//读写uart寄存器的相关宏定义
#define uart_read_reg(reg) (*(UART_REG(reg)))
#define uart_write_reg(reg, v) (*(UART_REG(reg)) = (v))
/*
* The UART control registers are memory-mapped at address UART0.
* This macro returns the address of one of the registers.
*/
#define UART_REG(reg) ((volatile uint8_t *)(UART0 + reg))
/* This machine puts UART registers here in physical memory. */
#define UART0 0x10000000L
这里采用轮询的方式实现数据的发送—>putc:
int uart_putc(char ch){
//不断读取LSR寄存器,获取其第五位,判断是否为0,为0表示空闲
while ((uart_read_reg(LSR) & LSR_TX_IDLE) == 0);
//如果空闲就向THR寄存器中写入数据
return uart_write_reg(THR, ch);
}
启动函数:
extern void uart_init(void);
extern void uart_puts(char *s);
void start_kernel(void){
//初始化串口设备
uart_init();
//输出字符
uart_puts("Hello, RVOS!\n");
while (1) {}; // stop here!
}
编译运行:
注意如何退出qemu: