这里可以把qemu提供的virt虚拟机器想象成一个真实的开发板,通过-kernel可以将os.elf放在0x80000000的内存,并让qemu直接运行该内核代码。
在Licheepi 4A上烧录操作系统是通过串口线UART来完成的,即将开发板上的TX连接主机的RX,将开发板上的RX连接TX,地线相连,然后就可以利用fastboot工具将操作系统烧录到磁盘内。
对于现在这个实验,相当于用“魔法”把操作系统烧录到了磁盘(但其实并没有),并且将开发板插上电源后,就能够运行该操作系统,但是没法观察执行的情况,即没有终端,故这里需要用到UART。
相当于是在内核代码中写了一段新的代码,即向外设UART控制器中的THR寄存器写数据,以达到在主机终端显示的效果,这里默认串口线连上了,只需要向UART的THR寄存器写数据,主机就能看到了。
Universal Asynchronous Receiver and Transmitter有几个特点:
在qemu的virt虚拟机器中,UART在内存的0x10000000地址处,想要通过UART来传输数据到主机,就需要对其中寄存器写数据,具体模拟的是NS16550a硬件UART控制器。
# qemu/hw/riscv/virt.c
static const MemMapEntry virt_memmap[] = {
[VIRT_DEBUG] = { 0x0, 0x100 },
[VIRT_MROM] = { 0x1000, 0xf000 }, # bootloader
[VIRT_TEST] = { 0x100000, 0x1000 },
[VIRT_RTC] = { 0x101000, 0x1000 },
[VIRT_CLINT] = { 0x2000000, 0x10000 },
[VIRT_ACLINT_SSWI] = { 0x2F00000, 0x4000 },
[VIRT_PCIE_PIO] = { 0x3000000, 0x10000 },
[VIRT_IOMMU_SYS] = { 0x3010000, 0x1000 },
[VIRT_PLATFORM_BUS] = { 0x4000000, 0x2000000 },
[VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },
[VIRT_APLIC_M] = { 0xc000000, APLIC_SIZE(VIRT_CPUS_MAX) },
[VIRT_APLIC_S] = { 0xd000000, APLIC_SIZE(VIRT_CPUS_MAX) },
[VIRT_UART0] = { 0x10000000, 0x100 }, # UART
[VIRT_VIRTIO] = { 0x10001000, 0x1000 },
[VIRT_FW_CFG] = { 0x10100000, 0x18 },
[VIRT_FLASH] = { 0x20000000, 0x4000000 },
[VIRT_IMSIC_M] = { 0x24000000, VIRT_IMSIC_MAX_SIZE },
[VIRT_IMSIC_S] = { 0x28000000, VIRT_IMSIC_MAX_SIZE },
[VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 },
[VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 },
[VIRT_DRAM] = { 0x80000000, 0x0 }, # kernel
};
较为关键的共有四个寄存器:
#define UART_RHR 0 // 接收保持寄存器(读)
#define UART_THR 0 // 发送保持寄存器(写)
#define UART_IER 1 // 中断使能寄存器
#define UART_LSR 5 // 线路状态寄存器(DR=数据就绪)
通过直接对内存中的0x10000000+reg来对UART设备的寄存器进行读写,寄存器位宽是8bits。
设置波特率、以及一次通信多少位的数据等。
void uart_init()
{
// 通过对IER寄存器写,关闭中断
uart_write_reg(IER, 0x00);
// 因为会有多个寄存器共享同一个地址空间,故该地址具体指哪个寄存器,需要由另一个寄存器的值来决定
// lcr的第7位决定地址空间0x10000000和0x10000001所存的数据表示的是哪个寄存器
// 波特率由16bit表示,由DLL和DLM组成
uint8_t lcr = uart_read_reg(LCR);
uart_write_reg(LCR, lcr | (1 << 7));
uart_write_reg(DLL, 0x03);
uart_write_reg(DLM, 0x00);
// 这里设置一次传输的数据位宽是8bits
lcr = 0;
uart_write_reg(LCR, lcr | (3 << 0));
}
通过轮询的方式,像THR寄存器写入要传输的数据。
void uart_putc(char ch)
{
// 不断查询Line Status Register寄存器的值,判断TX是否空闲,
while ((uart_read_reg(LSR) & LSR_TX_IDLE) == 0)
;
// 向Transmit Holding Register (write mode寄存器写入ch
uart_write_reg(THR, ch);
}
然后uart_puts函数会根据字符串的长度循环调用uart_putc,在内核函数中调用uart_puts即可。