Hello, OS!

资料传输

常见的资料传输方式有两种:

  • Serial
    将一串资料拆成多个资料,一次传一个资料。
    • pros: 成本低
  • Parallel
    有多条传输线,因此可以一次传输一串资料。
    • pros:

传输速率

  • Baud rate: 每秒可以传送的资讯量
  • Bit rate: 每秒可以传送的资料量

以资工人的角度来看,我们比较不需在意从 Tx 到 Rx 中间的资讯量传送速度 (Baud rate),像是: 如果连续传送两个 1,在这两个 1 之间肯定不会是无间格或是无辨识讯号的,有可能会利用波型变化告诉 Rx 这是不同的讯号。因此,我们平常看到/在意的传输数率通常是 Bit rate

UART

UART (Universal Asynchronous Receiver/Transmitter)是一种异步收发传输器,它可以将数据透过串列通讯和平行通讯间作传输转换。

如下图,通用异步接受器-发送器 (UART) 把资料的字节按照顺序发送。另一端的 UART 再将资料还原。每个 UART 都包含了一个移位暂存器。

数据传输模型

在嵌入式设备上,我们所使用的 UART 都是利用写好的 IP。
若我们要自己实现 UART protocol,就必须根据 UART 的规则在 GPIO 上实现其逻辑。

mini-riscv-os

在 mini-risc-v 这个迷你作业系统中,使用 UART 实作字元传输的功能,我们可以参考其原始码一探究竟。
由於 mini-riscv-os 被设计来执行在 QEMU 的 Virt 虚拟机上面,所以有关记忆体映射的操作都应该参考 Virt 原始码中的定义。
以 UART 为例,记忆体会从 0x10000000 开始映射:

0x10000000 THR (Transmitter Holding Register) 同时也是 RHR (Receive Holding Register)
0x10000001 IER (Interrupt Enable Register)
0x10000002 ISR (Interrupt Status Register)
0x10000003 LCR (Line Control Register)
0x10000004 MCR (Modem Control Register)
0x10000005 LSR (Line Status Register)
0x10000006 MSR (Modem Status Register)
0x10000007 SPR (Scratch Pad Register)

对应的原始码如下:

// os.c
#define UART 0x10000000
#define UART_THR (uint8_t *)(UART + 0x00) // THR:transmitter holding register
#define UART_LSR (uint8_t *)(UART + 0x05) // LSR:line status register
#define UART_LSR_EMPTY_MASK 0x40 // LSR Bit 6: Transmitter empty; both the THR and LSR are empty

什麽是记忆体映射呢?
可以参考 MMIO

如果希望作业系统印出字元,则需要往 UART 的 THR 送资料。在这之前,我们需要检查 UART 传送区是否为空,也就是确认 LSR 是否为 1

// os.c
int lib_putc(char ch)
{
	while ((*UART_LSR & UART_LSR_EMPTY_MASK) == 0)
		;
	return *UART_THR = ch;
}

有了基本的 UART 字元传送功能後,我们还需要有函式处理输出的功能:

// os.c
void lib_puts(char *s) {
	while (*s) lib_putc(*s++);
}

利用指标做处理,便能将一长串资料分开来传送。
做到这步,Hello OS! 就顺利完成了:

int os_main(void)
{
	lib_puts("Hello OS!\n");
	while (1) {}
	return 0;
}

Bootloader

即使程序码完成了,也不代表丢到虚拟机器上它就会自己跑起来。
如同运行在大众使用的电脑的作业系统,在开机时也同样需要有程序将作业系统引导至记忆体上才能够顺利启动,这样的程序就是 Bootloader。

mini-riscv-os 的 Bootloader 是使用 RISC-V 的组合语言所撰写:

.equ STACK_SIZE, 8192

.global _start

_start:
    # setup stacks per hart
    csrr t0, mhartid                # read current hart id
    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 stack space
    add  sp, sp, t0                 # move the current hart stack pointer
                                    # to its place in the stack space

    # park harts with id != 0
    csrr a0, mhartid                # read current hart id
    bnez a0, park                   # if we're not on the hart 0
                                    # we park the hart

    j    os_main                    # hart 0 jump to c

park:
    wfi
    j park

stacks:
    .skip STACK_SIZE * 4            # allocate space for the harts stacks

工作原理大致如下:

  1. 首先,读取当前的硬体执行绪的序号
  2. 以这个序号为基准,算出 Stack pointer 的位址
  3. 因为 mini-riscv-os 只支援单个硬体执行绪,所以我们要确保只有 hart 0 会进到我们的主程序 os_main()
  4. j os_main 这条指令会让处理器跳到 C 档案中定义的 os_main(),此时,作业系统就顺利完成载入的工作了!

其他 Hart (硬体执行绪) 都会待在 park 中不断地循环。

Reference


<<:  Day16 X Polyfill-less Bundling Script & File Compression

>>:  [Day 16] 阿嬷都看得懂的通用 .html 档案结构

Day02 WebRTC 简介

一场全球大流行的 COVID-19 疫情,以及 H264、H265、VP8、VP9等影音压缩技术加...

AE新手必学の三种常用追踪方法01-Day28

经然第28天了,如果没有这个活动我应该也不会每天练习 一样是六指渊的教学:https://www.s...

CLOUDWAYS 服务器方案评比 - linode / VULTR-HF / Digital Ocean-DP

在 CLOUDWAYS❐ 中,提供了5大云端主机供应商的方案☞ linode 、 VULTR 、 D...

【Day 30】 一趟挑战失败的铁人赛英雄之旅

大家好,2020 铁人赛来到了最後一天。我自己是没想过竟然可以连写两年。 最後一天当然要写心得文,这...

[Day 24] 资料产品在部署阶段的五个大坑

上线之後才是开始。 第一坑 开发和部署环境不一致 如果一开始开发和部署没有「乔好」环境的话,那上线的...