シリアル通信をやる上で参考になったサイトのパクリ個人的なまとめ
なぜシリアル通信をするのか
とりあえず何かしら文字を出力したいという時、例えばC言語だったらprintf
、C#だったらSystem.Console.WriteLine
のように標準出力関数を使って文字を出力する。ところがこれらの機能はOSが提供しているので、そもそもOSが存在しない自作OS環境では使うことができない。もちろん画面に文字を表示する機能を実装すればいいだけの話だが、メモリを直接書き換える分、初心者にとっては割と敷居が高かったり、実装途中で原因不明の例外が発生したり...
QEMUにはシリアルコンソールを表示する機能があり、OS側でシリアル通信を実装すれば、コンソールに文字を出力させることができる。画面に文字を出力するよりかは難易度が低めだし、早めに実装できるのでおすすめ。
方法
OSDev Wikiを眺めてみると詳しいやり方が書いてあった。ありがたい。実機ではUARTと呼ばれるハードウェアチップがシリアル通信を制御していて、メモリマップドI/Oを介してOSはUARTを制御できる。QEMUやVirtualBoxはこれをエミュレートできるので、我々はI/OレジスタをIN命令とOUT命令で操作するだけでデータの送受信を行うことができる。
シリアル通信をする前に使うポートを指定したり、ボーレートを設定したりなど初期化が必要になってくる。また、データ受信時の割り込み処理をするためにIRQハンドラも設定する必要があるが、今回はそれは使わずにループで受信処理を書こうと思う。(手抜き)ちなみにこのループでいちいち調べに行く方法をポーリングと呼ぶらしい。
ポート
COMポート(シリアルポート)とI/Oポートの対応表。特に何も設定していなければ、QEMUのシリアルコンソールはCOM1を使うようになっている。また、COM1とCOM3はIRQ4、COM2とCOM4はIRQ3に対応しているが、COM3以降はポートが他のハードウェアと競合することがあるため、ほとんどは1と2のみを使う。
COMポート | I/Oポート | IRQ |
---|---|---|
COM1 | 0x3f8 | 4 |
COM2 | 0x2f8 | 3 |
COM3 | 0x3e8 | 4 |
COM4 | 0x2e8 | 3 |
... | ... | × |
I/Oレジスタ
16550 UARTのI/Oレジスタ。ソースコードを眺めると、QEMUでは16550AがUARTのエミュレーションとして実装されていることがわかる。ちなみに16550Aの「A」は16550のバグ修正版という意味らしい。
Interrupt Identification Register、Modem Status Register、Scratch Registerは使ってないので割愛。
I/Oポートのオフセット | 命令 | 説明 |
---|---|---|
+0(DLAB = 0) | OUT | Transmit Holding Register(THR) |
+0(DLAB = 0) | IN | Receive Buffer Register(RBR) |
+0(DLAB = 1) | IN/OUT | Divisor Latch LSB(DLL) |
+1(DLAB = 0) | IN/OUT | Interrupt Enable Register(IER) |
+1(DLAB = 1) | IN/OUT | Divisor Latch MSB(DLM) |
+2 | IN | Interrupt Identification Register(IIR) |
+2 | OUT | FIFO Control Register(FCR) |
+3 | IN/OUT | Line Control Register(LCR) |
+4 | IN/OUT | Modem Control Register(MCR) |
+5 | IN/OUT | Line Status Register(LSR) |
+6 | IN/OUT | Modem Status Register(MSR) |
+7 | IN/OUT | Scratch Register(SCR) |
Transmit Holding Register / Receive Buffer Register
THRに書き込んだデータはUARTに送信され、RBRでUARTが受信したデータを確認することができる。
Divisor Latch LSB / MSB
LSBが最下位ビットでMSBが最上位ビット。
ボーレート
Wikipediaから引用。 ja.wikipedia.org
ボー (baud) は、変調レートの単位である。ボーは、搬送波に対する1秒間あたりの変調の回数と定義される。
個人的にはボーレート=転送速度だと思っていたが、実はそうではないらしい。例えば100ボーレート(1秒あたり100回の変調)で1回の変調で2ビット送信できるとすると、速度は100 * 2 = 200bpsになる。UARTは最大で115200bpsで通信することができるが、種類によってはもっと早く通信できるものもあるらしい。
UARTではボーレートではなく除数をレジスタに書き込む。例えば38400ボーレートに設定したいときは115200 / 38400 = 3となる。
Interrupt Enable Register
どの割り込み生成するかを設定する。0が無効で1が有効。
7~4ビット | 3ビット | 2ビット | 1ビット | 0ビット |
---|---|---|---|---|
予約済み | ステータス変更 | Error/Breakシグナルの検知 | THRが空 | データ受信関連 |
FIFO Control Register
0ビット目を1にすることでFIFOが有効になり、1ビット目と2ビット目を1にすることでFIFOを初期化できる。
7~6ビット | 5~4ビット | 3ビット | 2ビット | 1ビット | 0ビット |
---|---|---|---|---|---|
レシーバトリガ | 予約済み | DMAモード選択 | 送信FIFOの破棄 | 受信FIFOの破棄 | FIFO有効 |
レシーバトリガ
FIFOが有効になっているとき、いくつデータを受信した段階で割り込みを発生させるか。
書き込むビット | データの数 |
---|---|
00 | 1個 |
01 | 4個 |
10 | 8個 |
11 | 14個 |
Line Control Register
7ビット | 6ビット | 5~3ビット | 2ビット | 1~0ビット |
---|---|---|---|---|
DLAB | ブレーク信号 | パリティ | ストップビット | データビット |
DLAB
Divisor Latch Access Bit。0または1のみを設定。これを変更することによってレジスタのマッピングを切り替える。
パリティ
誤り検出の手法。データにパリティビット(1ビット)を付与し、データとパリティビットを合わせた全ビットの「1」の数が常に偶数か奇数になるようにパリティビットで調整する。パリティビットの扱いをどうするかをレジスタに書き込む。一般的にはハードウェアではなくプロトコルが誤り検出を行うため、Noneを設定する。
書き込むビット | パリティ | 動作 |
---|---|---|
000 | None | パリティなし |
001 | Odd | 奇数 |
011 | Even | 偶数 |
101 | Mark | パリティビットは常に1 |
111 | Space | パリティビットは常に0 |
ストップビット
各データの終わりに送信されるビット。送信側と受信側がしっかり同期しているかどうかを確認するために使われる。通常は1個のストップビットを使うが、データ長によっては1.5個や2個使う場合がある。
書き込むビット | ストップビットの数 |
---|---|
0 | 1個 |
1 | 1.5 or 2個 |
データビット
ビット数は5~8ビットが設定できる。データにはASCIIコードが使われるので、8ビットに設定するのが一般的。(最低7ビット)
書き込むビット | ビット数 |
---|---|
00 | 5 |
01 | 6 |
10 | 7 |
11 | 8 |
Modem Control Register
7~5ビット | 4ビット | 3ビット | 2ビット | 1ビット | 0ビット |
---|---|---|---|---|---|
予約済み | ループバックモード | OUT2ピン | OUT1ピン | RTSピン | DTRピン |
ループバックモード
1にするとTHRとRBRが接続され、同じ値を取るようになる。MCRの各ピンもそれぞれUARTの内部信号の値を取るようになる。UARTの診断テストに使う。
OUT2ピン
1にすることでIRQラインを切断し、複数のシリアルポートで1本のIRQラインを共有する。
Line Status Register
各ステータスを示す。
7ビット | 6ビット | 5ビット | 4ビット | 3ビット | 2ビット | 1ビット | 0ビット |
---|---|---|---|---|---|---|---|
受信FIFOのエラー | TEMT | THRE | BI | FE | PE | OE | DR |
TEMT
Transmitter Empty。トランスミッターが空=動いていないときに1になる。
THRE
Transmitter Holding Register Empty。文字通りTHRが空のときに1になる。
BI
Break Interrupt。ブレーク信号を検知したときに1になる。
FE
Framing error。ストップビットの欠落を検知したときに1になる。
PE
Parity error。パリティによるエラー発生時に1になる。
OE
Overrun error。UARTが新しいデータを受信したとき、受信バッファに空きがなかった場合に1になる。
DR
Data ready。受信FIFOにデータが存在するときに1になる。
Rustで実装
OSDev Wikiにあるサンプルコードに沿って実装した。実はResultの使い方がまだイマイチよくわかってなかったりする。
serial.rs
use crate::arch::asm; pub const IO_PORT_COM1: u16 = 0x3f8; pub const IO_PORT_COM2: u16 = 0x2f8; pub const IO_PORT_COM3: u16 = 0x3e8; pub const IO_PORT_COM4: u16 = 0x2e8; pub const IO_PORT_COM5: u16 = 0x5f8; pub const IO_PORT_COM6: u16 = 0x4f8; pub const IO_PORT_COM7: u16 = 0x5e8; pub const IO_PORT_COM8: u16 = 0x4e8; pub struct SerialPort { io_port: u16, is_init: bool, } impl SerialPort { pub fn new(io_port: u16) -> Self { return Self { io_port, is_init: false }; } pub fn init(&mut self) { asm::out8(self.io_port + 1, 0x00); // IER - disable all interrupts asm::out8(self.io_port + 3, 0x80); // LCR - enable DLAB asm::out8(self.io_port + 0, 0x03); // DLL - set baud late 38400 bps asm::out8(self.io_port + 1, 0x00); // DLM asm::out8(self.io_port + 3, 0x03); // LCR - disable DLAB, 8bit, no parity, 1 stop bit asm::out8(self.io_port + 2, 0xc7); // FCR - enable FIFO, clear TX/RX queues, 14byte threshold asm::out8(self.io_port + 4, 0x0b); // MCR - IRQs enabled, RTS/DSR set asm::out8(self.io_port + 4, 0x1e); // MCR - set loopback mode, test the serial chip asm::out8(self.io_port + 0, 0xae); // RBR - test the serial chip (send 0xae) if asm::in8(self.io_port + 0) != 0xae { return; } // if serial isn't faulty, set normal mode asm::out8(self.io_port + 4, 0x0f); self.is_init = true; } pub fn receive_data(&self) -> Result<u8, &str> { if !self.is_init { return Err("Serial port wasn't initialized"); } let res = asm::in8(self.io_port + 5) & 1; if res == 0 { return Err("Hasn't received data"); } return Ok(asm::in8(self.io_port)); } pub fn send_data(&self, data: u8) -> Result<(), &str> { if !self.is_init { return Err("Serial port wasn't initialized"); } while self.is_transmit_empty() == 0 {} asm::out8(self.io_port, data); return Ok(()); } fn is_transmit_empty(&self) -> u8 { return asm::in8(self.io_port + 5) & 0x20; } }
main.rs
のカーネルエントリポイントの一部
let mut serial = SerialPort::new(serial::IO_PORT_COM1); serial.init(); serial.send_data(b'H').unwrap(); serial.send_data(b'e').unwrap(); serial.send_data(b'l').unwrap(); serial.send_data(b'l').unwrap(); serial.send_data(b'o').unwrap(); serial.send_data(b'!').unwrap(); serial.send_data(b'\n').unwrap(); loop { if let Ok(data) = serial.receive_data() { serial.send_data(data).unwrap(); } //asm::hlt(); }
receive_data()
で受け取ったデータをsend_data()
に流すことでシリアルコンソールから文字が入力できるようになった。十字キーでカーソルも動いたりするので、コンソール内の自由な場所に文字を上書きできる。まだ割り込みハンドラを作ってないのでHLT命令を飛ばすと動かなくなってしまう。
QEMUのオプションに-serial mon:stdio
をつけることでシリアルコンソールの内容をターミナルにリダイレクトさせることが可能。
参考サイト
ja.wikipedia.org wiki.osdev.org www.japansensor.co.jp www2.denshi.numazu-ct.ac.jp archive.linux.or.jp