這系列的文章會記錄我嘗試做出一個 Linux OS kernel的過程,要主是依照 https://littleosbook.github.io/ 這系列的文章,加上自已踩下的坑,希望能幫助同樣有做一個作業系統夢想的各位。
承接上文到 4.3,我們會了解 I/O ports 的使用方法。序列埠 (Serial port) 作為經典的 I/O ports ,它的作用與為什麼我們的電腦會用到它。(在原文中有提到,在這時我們只需要用 Serial port 作 Output 用,而不會用到他作 Input ,這與之後的內容會有所關連,請先注意。)
序列埠是一個硬體與主機板之間溝通的介面,比如我們常常用到的 USB 、VGA 、網路線插口,都是一種 Serial port。
第一個被傳送到 serial port 的資料是 config data,作用是在硬體與硬體之間能夠有效通訊,所以他們之間會需要一些 handshaking,包括 :
- 指定傳送 data 的速度 ( baud rate )。
- 指定對應於接收錯誤時所使用的 data (parity bit, stop bit)。
- parity bit 被稱作同位位元,是最簡單的錯誤檢測碼。如果傳輸過程中包括校驗位在內的奇數個資料位發生改變,那麼奇偶校驗位將出錯表示傳輸過程有錯誤發生。
- stop bit 被稱作停止位元,用以代表傳送結束。若把 stop bit 設定為 1,則當傳輸線上的信號一直為 1 時,就是表示沒有資料傳送。當傳輸線上的信號由1變為0,即表示有資料將傳送。
- 指定 data 的長度,在 5-8 之間 (data bits)。
而 serial port 就有一個 I/O port — — “line command port” 來用作保存 config。
注意 : 在內文提出的 "line command port",在我大量的 google 下還是沒有找到相應的資料,所以請各位對這個 port 是否確實存在保持適當的懷疑,或是在下方給我留言,謝謝。
首先設定 baud rate,serial port 大多數的 baud rate 上限都是 115200 bps,如果在速度內傳兩個結果,就是 115200 / 2 = 57600
Hz。因為分母 (2) 是一個 16bit 的數字,跟 framebuffer 的 I/O port 一樣,我們只能以先後傳前後 8 bit 的值來處理,以下是傳 0x80
到 "line command port" 的例子 :
#include "io.h" /* io.h is implement in the section "Moving the cursor" *//* The I/O ports *//* All the I/O ports are calculated relative to the data port. This
* is because all serial ports (COM1, COM2, COM3, COM4) have their
* ports in the same order, but they start at different values.
*/#define SERIAL_COM1_BASE 0x3F8 /* COM1 base port */
#define SERIAL_DATA_PORT(base) (base)
#define SERIAL_FIFO_COMMAND_PORT(base) (base + 2)
#define SERIAL_LINE_COMMAND_PORT(base) (base + 3)
#define SERIAL_MODEM_COMMAND_PORT(base) (base + 4)
#define SERIAL_LINE_STATUS_PORT(base) (base + 5)/* The I/O port commands *//* SERIAL_LINE_ENABLE_DLAB:
* Tells the serial port to expect first the highest 8 bits on the
* data port, then the lowest 8 bits will follow
*/#define SERIAL_LINE_ENABLE_DLAB 0x80/* serial_configure_baud_rate:
* Sets the speed of the data being sent. The default speed of a
* serial port is 115200 bits/s. The argument is a divisor of that
* number, hence the resulting speed becomes (115200 / divisor)
* bits/s.
* @param com The COM port to configure
* @param divisor The divisor
*/
void serial_configure_baud_rate(unsigned short com, unsigned short divisor) {
outb(SERIAL_LINE_COMMAND_PORT(com),
SERIAL_LINE_ENABLE_DLAB);
outb(SERIAL_DATA_PORT(com),
(divisor >> 8) & 0x00FF);
outb(SERIAL_DATA_PORT(com),
divisor & 0x00FF);
}
config data 傳送的方法同樣是由 "line command port" 傳送 1 個 byte 來實現,以下是 8 bits 的排列 :
Bit: | 7 | 6 | 5 4 3 | 2 | 1 0 |
Content: | d | b | prty | s | dl |
- DLAB 是用於在傳送 16 bit 的值時,告訴 Serial port 先接收前 8 bit 的值,再接收後 8 bit 的值。
- break control ( 找不到相關資料。)
我們會使用最常用的值 0x03
,來表示 8 bits 的內容,沒有 parity bit,1 stop bit 與不使用 break control。這些 config 會傳送到 "line command port",程式如下 :
/** serial_configure_line:
* Configures the line of the given serial port. The port is set to
* have data length of 8 bits, no parity bits, one stop bit and
* break control disabled.
*
* @param com The serial port to configure
*/
void serial_configure_line(unsigned short com)
{
/* Bit: | 7 | 6 | 5 4 3 | 2 | 1 0 |
* Content: | d | b | prty | s | dl |
* Value: | 0 | 0 | 0 0 0 | 0 | 1 1 | = 0x03
*/
outb(SERIAL_LINE_COMMAND_PORT(com), 0x03);
}
如上,我們就可以完成一組 serial port 之間的協定,讓兩者可以開始溝通。
當資料被傳送到 serial port,他們會被存放到 buffer 中。所以當你傳送 data 到 serial port 的速度比 serial port 傳送出去的速度快的話,那 data 就會先存在 buffer 裡面。如果傳得太快太多,傳 buffer 都滿了的話,data 就會漏掉。換句話說,buffer 是一個 FIFO ( First In First Out ) 的序列。
buffer 的 config data 如下 :
Bit: | 7 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Content: | lvl | bs | r | dma | clt | clr | e |
在文章這邊他們使用的是 0xC7 = 11000111
作為 config 值 :
- 使用 FIFO 序列。
- 清除 receiver 和transmission 的 FIFO 序列。
- 使用 14 byte 作為 buffer 的大小。
但為什麼使用 0xC7
? 文章中沒有多說明!
(The WikiBook on serial programming [32] explains the values in more depth.) ← 這就是原文 !!!
那電腦怎麼知道 Data 是準備好被傳送與被接收的呢 ? 資料總不能沒有通知就在兩個地方跑來跑去,萬一兩方都剛好要傳資料時,通道不就塞住了嗎。這時我們就會用到 RTS (Ready To Transmit) 跟 DTR (Data Terminal Ready)。當 serial port 的 RTS 跟 DTR pin 都被設為 1 時,就表示準備好傳送資料,而 RTS 跟 DTR 是被設計於 modem control register 中。
modem 的 config data 如下 :
Bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Content: | r | r | af | lb | ao2 | ao1 | rts | dtr |
在這我們因為不用 Input 只做 Output 動作,所以沒有使用 interrupt,使用的是 0x03= 00000011
作為 config 值 。
把 data 寫進 serial port 時,需要確保 transmit FIFO 序列為空,這可以透過 line status 的第 5 個 bit 為 1 來確認。
而讀取 I/O port 的內容是透過in
assembly 進行,沒有辦法使用直接用 C 來做到,所以如 out
assembly 一樣,我們要寫一下 assembly 再包給 C 使用。
global inb; inb - returns a byte from the given I/O port
; stack: [esp + 4] The address of the I/O port
; [esp ] The return addressinb:
mov dx, [esp + 4] ; move the address of the I/O port to
; the dx register
in al, dx ; read a byte from the I/O port and
; store it in the al register
ret ; return the read byte
再在 io.h 檔中增加一個 header :
/* in file io.h *//** inb:
* Read a byte from an I/O port.
*
* @param port The address of the I/O port
* @return The read byte
*/
unsigned char inb(unsigned short port);
最後再使用 C 來檢查 transmit FIFO 是否為空 :
#include "io.h"/** serial_is_transmit_fifo_empty:
* Checks whether the transmit FIFO queue is empty or not for the
* given COM port.
*
* @param com The COM port
* @return 0 if the transmit FIFO queue is not empty
* 1 if the transmit FIFO queue is empty
*/
int serial_is_transmit_fifo_empty(unsigned int com)
{
/* 0x20 = 0010 0000 */
return inb(SERIAL_LINE_STATUS_PORT(com)) & 0x20;
}
接著我們要設定一下 Bochs 來讓它開啟一個 serial port。先打開我們的 bochsrc.txt
檔,然後增加下面這一行 config :
com1: enabled=1, mode=file, dev=com1.out
現在 serial port 1 的 output 會儲存到com1.out
檔。
下一章,我們會了解 Segmentation。