(1)此系列文章是跟着汪辰老师的RISC-V课程所记录的学习笔记。
(2)该课程相关代码gitee链接;
(3)PLCT实验室实习生长期招聘:招聘信息链接
(4)
(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
(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!
}
(1)因为qemu的串口0地址为0x10000000,所以这里以0x10000000为基地址进行偏移操作。这里建立一个宏的目的是为了后续方便操作。
/* --- 这个是在platform.h中 ---*/
#define UART0 0x10000000L
/* --- uart.c中 ---*/
#define UART_REG(reg) ((volatile uint8_t *)(UART0 + reg))
(2)这里的宏定义其实就是对照者如下表格来进行的宏定义的。这个时候肯定会有人有疑问,怎么偏移0地址和偏移1地址都是有多个寄存器的呢?
<1>对于偏移0地址而言,这个是串口的数据收发寄存器,一般来说,单片机的发送寄存器和接受寄存器都是同一个寄存器,这样可以保证资源的重复利用。那么单片机是如何知道这个寄存器的数据,到底是发送出去的,还是接受过来的呢?这个就是硬件层面的配置了。对于写软件程序的我们,只需要知道,如果向这个寄存器写入数据,那么单片机就会发送数据。如果你要读取这个寄存器的数据,那么就是接收数据。 个人感觉这个可能和51单片机的IO准双向电路设计原理类似,感兴趣的可以去了解这部分电路设计。
<2>DLL
和DLM
又是什么呢?当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
(3)
LSR
寄存器的bit0
是用于检测接受数据是否已经过来了,如果接收到了数据bit0 == 1
。LSR
寄存器的bit5
是用于判断串口数据是否发送出去,如果发送出去了bit5 == 0
。
#define LSR_RX_READY (1 << 0)
#define LSR_TX_IDLE (1 << 5)
(4)上面说了,硬件上已经做好了处理,如果你写入数据,软件层面直接赋值。读取数据,直接获取这个寄存器数据即可。因此写法如下:
#define uart_read_reg(reg) (*(UART_REG(reg)))
#define uart_write_reg(reg, v) (*(UART_REG(reg)) = (v))
(1)首先,我们对IER寄存器全部写入0,意思是关闭所有的中断。
IER BIT 7-4 | IER BIT-3 | IER BIT-2 | IER BIT-1 | IER BIT-0 |
---|---|---|---|---|
全部置0 | modem状态寄存器中断 | 接收中断 | 发送完成中断 | 接收就绪中断 |
// 失能所有中断
uart_write_reg(IER, 0x00);
(1)因为我们需要配置波特率,而配置波特率需要使用到
DLL
和DLM
寄存器。所以需要设置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);
(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));
(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);
}
(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