panda's tech note

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カウンタで動かすためにポート43h0x36を書き込んでいます。次に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ビットモードまで突っ走りたい……。