前言

(1)此系列文章是跟着汪辰老师的RISC-V课程所记录的学习笔记。 (2)该课程相关代码gitee链接;

start.S

(1)qemu模拟的板子有8个内核,为了让我们跟方便理解,汪辰老师只使用了一个内核。
(2)如下是进行判断,当前执行任务的是否是第一个内核。如果是的,往下执行,如果不是第一个内核,跳转到park任务中。

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 hart 0

(3)这里的wfi是让内核进入低功耗状态。如果没有这条语句,其他7个内核不进行任何操作,只是空转没必要,因此直接让他们进入休眠即可。这样省电。

park:
	wfi
	j	park

(4)这里主要是进行一些堆栈操作,最后的j,是跳转到c程序中。start_kernel你可以修改为任意名字,只要的c程序入口函数也修改为对应的函数名即可。

slli	t0, t0, 10		# shift left the hart id by 1024
la	sp, stacks + STACK_SIZE	# set the initial stack pointer
				# to the end of the first stack space
add	sp, sp, t0		# move the current hart stack pointer
				# to its place in the stack space

j	start_kernel		# hart 0 jump to c

kernel.c

(1)这里实际上就是在串口上打印一个Hello, RVOS!,之后进入空转状态。

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!
}

uart.c

宏定义

(1)因为qemu的串口0地址为0x10000000,所以这里以0x10000000为基地址进行偏移操作。这里建立一个宏的目的是为了后续方便操作。

/* --- 这个是在platform.h中 ---*/
#define UART0 0x10000000L
/* --- uart.c中 ---*/
#define UART_REG(reg) ((volatile uint8_t *)(UART0 + reg))

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_数据

(2)这里的宏定义其实就是对照者如下表格来进行的宏定义的。这个时候肯定会有人有疑问,怎么偏移0地址和偏移1地址都是有多个寄存器的呢?
<1>对于偏移0地址而言,这个是串口的数据收发寄存器,一般来说,单片机的发送寄存器和接受寄存器都是同一个寄存器,这样可以保证资源的重复利用。那么单片机是如何知道这个寄存器的数据,到底是发送出去的,还是接受过来的呢?这个就是硬件层面的配置了对于写软件程序的我们,只需要知道,如果向这个寄存器写入数据,那么单片机就会发送数据。如果你要读取这个寄存器的数据,那么就是接收数据。 个人感觉这个可能和51单片机的IO准双向电路设计原理类似,感兴趣的可以去了解这部分电路设计。
<2>DLLDLM 又是什么呢?当LCR寄存器的bit7为1时候,偏移地址0和偏移地址1的两个寄存器就是被当成了波特率发生器寄存器,DLL就是波特率发生器的低8位,DLM就是高8位。(不理解的话,后面有更详细的介绍)

#define RHR 0	// Receive Holding Register (read mode)
#define THR 0	// Transmit Holding Register (write mode)
#define DLL 0	// LSB of Divisor Latch (write mode)
#define IER 1	// Interrupt Enable Register (write mode)
#define DLM 1	// MSB of Divisor Latch (write mode)
#define FCR 2	// FIFO Control Register (write mode)
#define ISR 2	// Interrupt Status Register (read mode)
#define LCR 3	// Line Control Register
#define MCR 4	// Modem Control Register
#define LSR 5	// Line Status Register
#define MSR 6	// Modem Status Register
#define SPR 7	// ScratchPad Register

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_寄存器_02

(3)LSR寄存器的bit0是用于检测接受数据是否已经过来了,如果接收到了数据bit0 == 1LSR寄存器的bit5是用于判断串口数据是否发送出去,如果发送出去了bit5 == 0

#define LSR_RX_READY (1 << 0)
#define LSR_TX_IDLE  (1 << 5)

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_数据_03

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_寄存器_04

(4)上面说了,硬件上已经做好了处理,如果你写入数据,软件层面直接赋值。读取数据,直接获取这个寄存器数据即可。因此写法如下:

#define uart_read_reg(reg) (*(UART_REG(reg)))
#define uart_write_reg(reg, v) (*(UART_REG(reg)) = (v))

uart_init()

失能中断

(1)首先,我们对IER寄存器全部写入0,意思是关闭所有的中断。

IER BIT 7-4

IER BIT-3

IER BIT-2

IER BIT-1

IER BIT-0

全部置0

modem状态寄存器中断

接收中断

发送完成中断

接收就绪中断

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_学习_05

// 失能所有中断
uart_write_reg(IER, 0x00);
波特率配置

(1)因为我们需要配置波特率,而配置波特率需要使用到DLLDLM寄存器。所以需要设置LCR寄存器的bit7为高电平。为了防止其他位被破坏,所以先读取LCR寄存器的值,再对bit7进行操作。

uint8_t lcr = uart_read_reg(LCR);

(2)使能内部波特率计数器锁存(DLAB),因为如果要配置波特率,需要先将这一位置为高电平。之后0地址和1地址的寄存器就变成了波特率设置寄存器。

uart_write_reg(LCR, lcr | (1 << 7));

(3)因为我们的仿真器是使用的1.8432MHZ的晶振进行的计算,最终需要生成的波特率是38.4K,就需要向DLL中写入3。至于为什么需要波特率是38.4K,这个我就不太清楚了。

uart_write_reg(DLL, 0x03);

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_risc-v_06

(4)从上面的表我们可以看出,如果波特率为50,需要存入2304。而tb16550这款芯片的寄存器只有8位,明显是存放不下来的。因此IER寄存器在DLAB被使能的时候变成DLM,作为波特率生成器的高八位。因为我们需要写入的是3,所以DLM直接写0。

uart_write_reg(DLM, 0x00);
异步通讯格式配置

(1)这里就是设置有效数据为8位,停止位为1,不进行校验(当bit3为0时候,bit4--bit5都没有用)。因为不需要中断,所以bit6为0,波特率设置已经完成了,所以bit7为0。
(2)通过上面的分析,即可得出,LCR寄存器写入一个0000 0011即可。

lcr = 0;
uart_write_reg(LCR, lcr | (3 << 0));

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_risc-v_07

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_#define_08

uart_putc()

(1)这个函数就是用于发送一个字符数据的,如果玩过51单片机的同学都会知道,你进行串口发送数据的时候,都需要等待第一个数据发送完成,才可以开始第二个数据发送。否则就会出现,第一个数据还没有发送完,就被第二个数据覆盖了。因此第一个数据就无法成功的输出。
(2)通过查阅数据手册可知,当LSR寄存器的bit5 == 0时候,表示数据发送完成,因此这里需要进行一次while死循环。
(3)当LSR寄存器的bit5 == 0条件满足,退出while()循环,就可以发送数据了。

int uart_putc(char ch)
{
	while ((uart_read_reg(LSR) & LSR_TX_IDLE) == 0);
	return uart_write_reg(THR, ch);
}

学习开发一个RISC-V上的操作系统(汪辰老师) — 01-helloRVOS程序讲解_学习_09

uart_puts()

(1)因为我们需要发送一个字符串,而字符串的最后一位是'/0',所以写法如下。

void uart_puts(char *s)
{
	while (*s) {
		uart_putc(*s++);
	}
}

参考文章

(1)TECHNICAL DATA ON 16550
(2)https://gitee.com/unicornx/riscv-operating-system-mooc/blob/main/code/os/01-helloRVOS/uart.c
(3)https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c