zebian.log

技術系備忘録とか

自作OSでシリアル通信をする

シリアル通信をやる上で参考になったサイトのパクリ個人的なまとめ

なぜシリアル通信をするのか

とりあえず何かしら文字を出力したいという時、例えばC言語だったらprintfC#だったらSystem.Console.WriteLineのように標準出力関数を使って文字を出力する。ところがこれらの機能はOSが提供しているので、そもそもOSが存在しない自作OS環境では使うことができない。もちろん画面に文字を表示する機能を実装すればいいだけの話だが、メモリを直接書き換える分、初心者にとっては割と敷居が高かったり、実装途中で原因不明の例外が発生したり...

QEMUにはシリアルコンソールを表示する機能があり、OS側でシリアル通信を実装すれば、コンソールに文字を出力させることができる。画面に文字を出力するよりかは難易度が低めだし、早めに実装できるのでおすすめ。

QEMUのシリアルコンソール

方法

OSDev Wikiを眺めてみると詳しいやり方が書いてあった。ありがたい。実機ではUARTと呼ばれるハードウェアチップがシリアル通信を制御していて、メモリマップドI/Oを介してOSはUARTを制御できる。QEMUVirtualBoxはこれをエミュレートできるので、我々は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