当前位置: 首页 > 資訊 >

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