Advent Calendar 2018: advos
Day 5: ブートローダ(タイマ)
今日はリアルモードでタイマを使ってブートローダで起動のカウントダウンをします。
以下のような画面で,キーボードで1を選んだら64ビットモードでブート,2を選んだら電源を落とすようにします。また,10秒入力がなければ自動的にブートするようにします。
Welcome to advos!
Select one:
1: Boot (64 bit mode)
2: Power off
Press key:[ ]
advos will boot in 05 sec.
タイマとカウントダウン
GRUBなどのブートローダのようにOSの起動までカウントダウンをします。上述の「N秒経ったらブートする」という機能を実装するにはタイマを使う必要があります。今日はまずはタイマの説明からしようと思います。
CPUの内部クロックの他に時間を計測するためのProgrammable Interval Timer (PIT)が搭載されています。他にもより精度の高いHPETやACPIタイマがありますが,今回は一番簡単なPITを使います。
IntelのチップセットにはIntel 8254というPITが搭載されています。詳しくはIntel 8254 PROGRAMMABLE INTERVAL TIMERを参照してください。
Intel 8254は内部に3つのカウンタを持っており,それぞれI/Oポートの40h,41h,42hに接続されています。また,1つめのカウンタのタイマ割り込みはIRQ 0に割り当てられています。なお,2つ目のカウンタは存在しないこともあるため使われません。また,3つ目のカウンタは一般的にスピーカに接続されているため,以下で説明するMode 2または3の波形生成により音を生成するのに使います。今回は,時間を計測するタイマ割り込みにつかうので,1つ目のカウンタ(ポート40h
)のみを使用します。
Intel 8254は次の6つのモードがあります。
- Mode 0: カウンタが0になったときに割り込みを発生させ,カウンタ値を設定値に戻す
- Mode 1: ゲート入力をトリガーにしてカウントダウンを開始。ゲート入力は3つ目のカウンタのみ接続(ポート
61h
のビット0) - Mode 2: レート生成(パルス)
- Mode 3: 矩形波生成
- Mode 4: ソフトウェアトリガーのパルス生成
- Mode 5: ゲート入力を取りがとしたパルス生成
今回はカウントダウンのためにMode 3を使います。PITのモード設定・コマンドはポート43h
に書き込むことで行います。
以下はPITを初期化し,100HzでIRQ 0に割り込みを入れるコードです。まず,PITをChannel 0(1つ目のカウンタ),Mode 3,16カウンタで動かすためにポート43h
に0x36
を書き込んでいます。次にChannel 0のカウンタを設定します。Intel 8254は1193181.67Hzでカウントダウンをするため,100Hzで割り込みを入れるにはこれを100で割った数(0x2e9b
=11931
)を設定します。設定した瞬間からカウントダウンが始まります。
#define PIT_CHANNEL0 (0x0 << 6)
#define PIT_HILO (0x3 << 4)
#define PIT_MODE3 (0x3 << 1)
#define PIT_BINARY (0)
movb $(PIT_BINARY|PIT_MODE3|PIT_HILO|PIT_CHANNEL0),%al
outb %al,$0x43
movw $0x2e9b,%ax /* Frequency=100Hz: 1193181.67/100 */
outb %al,$0x40 /* Counter 0 (least significant 4 bits) */
movb %ah,%al /* Get most significant 4 bits */
outb %al,$0x40 /* Counter 0 (most significant 4 bits) */
タイマ割り込みハンドラ
昨日のキーボード割り込みハンドラと同様にIRQ 0の割り込みハンドラを書きました。boottimerにはブートまでのカウントダウン値を1センチ秒単位で入れています(初期値は10秒=10000センチ秒)。割り込みハンドラが100Hzで呼ばれるため,1センチ秒(1/100秒)ごと減算していきます。現在のカウンタを秒にして10進数2桁をmsg_count
の文字列
小数演算(x87 FPU)は少しややこしいので,ブートローダではカウンタもすべて整数で扱います。
intr_irq0:
pushw %ax
pushw %bx
pushw %cx
pushw %dx
pushw %si
movw boottimer,%ax
testw %ax,%ax
jz 1f
decw %ax
movw %ax,boottimer
1:
movb $100,%dl /* Convert centisecond to second */
divb %dl /* Q=%al, R=%ah */
xorb %ah,%ah
movb $10,%dl
divb %dl /* Q(%al) = tens digit, R(%ah) = unit digit */
addb $'0',%al
addb $'0',%ah
movw %ax,msg_count
movw $msg_countdown,%si
call putbstr
movb $0x20,%al /* Notify */
outb %al,$0x20 /* End-Of-Interrupt (EOI) */
popw %si
popw %dx
popw %cx
popw %bx
popw %ax
iret
キーボード割り込みハンドラの修正
昨日実装したキーボード割り込みハンドラはそのままキー入力を出力していましたが,今日は一部を変更して以下のようにします。
addw %ax,%bx
movb (%bx),%al /* Get ascii code from the keyboard code */
movb %al,bootmode
call putc /* Print the character */
movb $0x08,%al /* Print backspace */
call putc /* for the next input */
jmp 6f
このコードでは入力された文字(ASCIIコード変換済み)を.data
セクションのbootmode
に保存しています。さらにその入力文字を出力後,0x08
(バックスペース)を入力しています。これにより,次に別の文字を入力した場合に,上書きされるようにしています。
画面最下部へのメッセージ表示
今回はブートメッセージを表示する以外に,画面の最下部にブートまでの時間やステータスを表示しようと思います。INT 13hでも良いのですが,今回はVIDEO RAM領域(Memory Mapped I/O (MMIO))領域に書き込むことで出力します。VGAモードのVIDEO RAMはリニアアドレスでB8000h
から割り当てられています。80x25の文字画面モードでは1文字につき2バイトが割り当てられています。上位バイトは文字色(下位4ビット)・背景色(上位4ビット)を指定し,下位バイトはASCIIコードを指定します。なので,画面最下部の1行(25行目)は0xb8000 + 80*24*2
からの160バイトです。 基本的にはこれまで使っていたputstr
と同様に1文字ずつ読み込み,文字色・背景色をセット(0x7
)し,画面に出力しています。
putbstr:
pushw %ax
pushw %es
pushw %di
movw $0xb800,%ax /* Memory 0xb8000 */
movw %ax,%es
movw $(80*24*2),%di /* 24th (zero-numbering) line */
putbstr.load:
lodsb /* Load %al from %ds:(%si) , then incl %si */
testb %al,%al /* Stop at null */
jnz putbstr.putc /* Call the function to output %al */
popw %di
popw %es
popw %ax
ret
putbstr.putc:
movb $0x7,%ah
stosw /* Write %ax to [%di], then add 2 to %di */
jmp putbstr.load
キーボードとタイマによる起動処理
これでブートモニタのUIはできてきました。あとはキーボードハンドラとタイマを使ってブートオプションを選ぶところです。以下がブートオプションに従ってブートまたは電源を落とす処理に遷移するコードです。
/* Wait until the timer reached zero or keyboard input */
1:
sti
hlt
cli
movb bootmode,%al
cmpb $'1',%al
je boot
cmpb $'2',%al
je poweroff
cmpw $0,boottimer
je boot
jmp 1b
boot:
movw $msg_boot,%si
call putbstr
jmp halt
poweroff:
movw $msg_poweroff,%si
call putbstr
jmp halt
上から順に説明します。
sti
hlt
cli
まず,キーボードもタイマも割り込みハンドラで処理するため,割り込みがあるまではCPU処理を止めます。割り込みハンドラから処理が返るとhlt
命令の次から再開されるので,まず割り込みを無効にします。キーボード割り込みハンドラで文字が入力された場合,bootmode
に入力されたキーが入っているので,これに従いブート,または電源OFFのルーチンを呼び出します。もし入力キーが'1'
でも'2'
でもなければ,最初に戻り割り込みを待ちます。タイマ割り込みハンドラでカウンタが0秒になった場合もbootmode
に'1'
を入力しているため,ブートのルーチンに移ります。
boot:
movw $msg_boot,%si
call putbstr
jmp halt
poweroff:
movw $msg_poweroff,%si
call putbstr
jmp halt
今日は,ブート処理,電源を落とす処理を実装していないので,画面最下部にブートオプションに従ったメッセージを表示して終了します。
まとめと明日の予定
明日はまず電源を落とす処理を実装して行く予定です。余裕があったらカーネルを読み込んで64ビットモードまで突っ走りたい……。