Top

第2章 HPET

前章で取得したHPETテーブルを使って、HPETレジスタのアドレスを取得し、レジスタ経由でHPETを制御します。

2.1 HPETの情報を表示してみる

HPETのレジスタ郡の先頭アドレスを取得し、試しにHPETのレジスタをいくつか見てみます。

この項のサンプルディレクトリは「020_dump_regs」です。

2.1.1 やること

まず、ACPIから取得したHPETのテーブルは、図2.1の構造になっています。

HPETテーブルの構造

図2.1: HPETテーブルの構造

SDTH含め先頭から40バイト目にレジスタ郡の先頭アドレスを指す12バイトのエントリーがあります。HPETのデータシート曰く「12バイトのACPIアドレスフォーマット」とのことで、12バイトの中の最後の8バイトにレジスタ郡の先頭アドレスが入っています。

レジスタがどのように並んでいるかは決まっているので、先頭アドレスが分かれば、アクセスしたいレジスタの位置までオフセットを足せば良いです。そうして得られたアドレスをポインタアクセスで読み書きすれば、レジスタに対してアクセスできます。

なお、64ビットで使用時、HPETの各レジスタはサイズが64ビット(8バイト)で、読み書きの単位も64ビットあるいは32ビットの単位で行うことが決められています。そのため、レジスタの特定の1ビットだけを書き換えたい場合も、64ビット分全てを変数へ読み出して、読み出した変数上で目的の1ビットの書き換えを行った後に、書き換え済みの変数で64ビット分全てをレジスタへ書き戻す、といういわゆるread-modify-writeを行います。

2.1.2 実装: レジスタ郡の先頭アドレス取得

まずはレジスタ郡の先頭アドレスを取得する処理を実装します。HPETに関する処理はhpet.cというソースファイルへ記述することにし、hpet_initという関数を作成してそこでレジスタのアドレス取得まで行います(リスト2.1)。

リスト2.1: 020_dump_regs/hpet.c

/* 追加(ここから) */
#include <acpi.h>

struct __attribute__((packed)) HPET_TABLE {
        struct SDTH header;
        unsigned int event_timer_block_id;
        struct ACPI_ADDRESS base_address;
        unsigned char hpet_number;
        unsigned short minimum_tick;
        unsigned char flags;
};

unsigned long long reg_base;

void hpet_init(void)
{
        /* HPET tableを取得 */
        struct HPET_TABLE *hpet_table = (struct HPET_TABLE *)get_sdt("HPET");

        /* レジスタの先頭アドレスを取得 */
        reg_base = hpet_table->base_address.address;
}
/* 追加(ここまで) */

なお、struct ACPI_ADDRESSはACPI側の仕様なので、acpi.hへ定義を追加します(リスト2.2)。

リスト2.2: 020_dump_regs/include/acpi.h

#ifndef _ACPI_H_
#define _ACPI_H_

struct __attribute__((packed)) SDTH {
        char Signature[4];
        unsigned int Length;
        unsigned char Revision;
        unsigned char Checksum;
        char OEMID[6];
        char OEM_Table_ID[8];
        unsigned int OEM_Revision;
        unsigned int Creator_ID;
        unsigned int Creator_Revision;
};

/* 追加(ここから) */
struct __attribute__((packed)) ACPI_ADDRESS {
        unsigned char address_space_id;
        unsigned char register_bit_width;
        unsigned char register_bit_offset;
        unsigned char _reserved;
        unsigned long long address;
};
/* 追加(ここまで) */

/* ・・・ 省略 ・・・ */

include/hpet.hを作成しhpet_init関数のプロトタイプ宣言を追加し(リスト2.3)、main.cから呼び出すようにしておきます(リスト2.4)。

リスト2.3: 020_dump_regs/include/hpet.h

/* 追加(ここから) */
#ifndef _HPET_H_
#define _HPET_H_

void hpet_init(void);

#endif
/* 追加(ここまで) */

リスト2.4: 020_dump_regs/main.c

/* ・・・ 省略 ・・・ */
#include <fs.h>
#include <hpet.h> /* 追加 */
#include <common.h>

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        /* ・・・ 省略 ・・・ */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* 変更(ここから) */
        /* HPETの初期化 */
        hpet_init();
        /* 変更(ここまで) */
        while (1);

        /* ・・・ 省略 ・・・ */

最後にMakefileへhpet.oをコンパイル対象に追加したら(リスト2.5)この節での作業は終わりです。

リスト2.5: 020_dump_regs/Makefile

# ・・・ 省略 ・・・
OBJS = main.o iv.o fbcon.o fb.o font.o kbc.o x86.o intr.o pic.o \
        hpet.o acpi.o handler.o fs.o common.o   # hpet.oを追加
# ・・・ 省略 ・・・

2.1.3 実装: GCIDRを見てみる

それでは、何かHPETのレジスタを表示させてみます。

まずはHPETのハードウェア固有情報が格納されている読み取り専用のレジスタである「General Capabilities and ID Register(GCIDR)」の内容を表示させてみることにします。

レジスタGCIDRの各ビットの意味は表2.1の通りです。表2.1では、使わないビットは省略しています。全てのビットを知りたい場合はHPETのデータシート*1を参照してください。

[*1] 本書末尾の「参考情報」参照

表2.1: GCIDRについて

ビット0~7REV_ID実装されている機能のリビジョン番号
ビット8~12NUM_TIM_CAP持っているタイマーの数
ビット13COUNT_SIZE_CAPカウンタサイズ:32bit(=0)/64bit(=1)
ビット14RESERVED予約領域
ビット15LEG_RT_CAPLegacyReplacementRoute
 ・・・ 
ビット32~64COUNTER_CLK_PERIODカウント周期(フェムト秒)

特にCOUNTER_CLK_PERIODは重要な値です。HPETは固定周期でカウントアップするカウンタがあり、そのカウンタがあらかじめ設定された値になったら割り込みを発生させる、といった挙動が基本的な挙動となりますが、そのカウンタのカウントアップ周期がCOUNTER_CLK_PERIODで示されています。単位のフェムト秒は「10のマイナス15乗」秒です。なお、GCIDRは読み取り専用のレジスタですので、周期を変えることはできません。

また、COUNT_SIZE_CAPについて、本書では64ビット幅のHPETを対象としているため、もしこの設定が0(32ビット)の場合は本書の64ビットで扱っている箇所を32ビットへ読み替える必要があります*2

[*2] そもそも本書が64ビットのx86を対象としているため、そのような環境でHPETだけ32ビットということは無いと思いますが。

NUM_TIM_CAPが示す「タイマーの個数」は「比較器の個数」と考えた方が良いです。カウンタは1つのHPETで1つです。「この値になったら割り込みを発生させる」といった値の設定や割り込みの挙動設定などを行うレジスタが1つのHPETに対してNUM_TIM_CAPの個数分存在します。

LEG_RT_CAPは旧来の割り込み経路が使用できるか否かを示します。1がセットされていると「LegacyReplacementRoute」という機能が使用できることを示し、旧来のPICを使用した経路で割り込みを通知できます。このビットはあくまでもHPETがLegacyReplacementRouteに対応しているか否かで、実際に使用するか否かの設定は後述する別のレジスタで行います。

以上を踏まえて、GCIDRの定義とGCIDRを表示する関数dump_gcidrをhpet.cへ追加します(リスト2.6)。

リスト2.6: 020_dump_regs/hpet.c

#include <acpi.h>
#include <fbcon.h>        /* 追加 */

/* ・・・ 省略 ・・・ */

/* 追加(ここから) */
/* General Capabilities and ID Register */
#define GCIDR_ADDR      (reg_base)
#define GCIDR   (*(volatile unsigned long long *)GCIDR_ADDR)
union gcidr {
        unsigned long long raw;
        struct __attribute__((packed)) {
                unsigned long long rev_id:8;
                unsigned long long num_tim_cap:5;
                unsigned long long count_size_cap:1;
                unsigned long long _reserved:1;
                unsigned long long leg_rt_cap:1;
                unsigned long long vendor_id:16;
                unsigned long long counter_clk_period:32;
        };
};
/* 追加(ここまで) */

void hpet_init(void)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void dump_gcidr(void)
{
        puts("GCIDR\r\n");

        union gcidr r;
        r.raw = GCIDR;

        puts("REV ID             ");
        putd(r.rev_id, 3);
        puts("\r\n");

        puts("NUM TIM CAP        ");
        putd(r.num_tim_cap, 2);
        puts("\r\n");

        puts("COUNT SIZE CAP     ");
        putd(r.count_size_cap, 1);
        puts("\r\n");

        puts("LEG RT CAP         ");
        putd(r.leg_rt_cap, 1);
        puts("\r\n");

        puts("COUNTER CLK PERIOD ");
        putd(r.counter_clk_period, 10);
        puts("\r\n");
}
/* 追加(ここまで) */

リスト2.6について、hpet.cの上部でレジスタアクセスを簡単に行うための定数と共用体(&構造体)を定義しています。

GCIDRはレジスタ郡の中の先頭のレジスタなので、hpet_init関数で設定したreg_baseがそのままGCIDRのアドレスとなります。

定数「GCIDR」はレジスタアクセスを行うための定数です。この定数に対して読み書きを行うことでレジスタGCIDRへの読み書きが行えます。

続いて、レジスタのビットフィールドへの操作が簡単に行えるように構造体のビットフィールドを定義しています。共用体としてunsigned long long型の変数としてもアクセスできるようにしているのは、read-modify-writeで64ビット分丸ごと読み出したり書き込んだりするためです。構造体側は読み出した後で特定のビットのみアクセスする際に使います。

以上の定義を使用してdump_gcidr関数ではGCIDRの内容を表示しています。「r.raw = GCIDR;」がレジスタGCIDRの64ビット分すべてを共用体変数rのvalへ読み出している処理で、その後、変数rの構造体側のメンバを使用して特定のビットのみ表示させています。

あとは、hpet.hへプロトタイプ宣言を追加し(リスト2.7)、main.cから呼び出します(リスト2.8)。

リスト2.7: 020_dump_regs/include/hpet.h

#ifndef _HPET_H_
#define _HPET_H_

void hpet_init(void);
void dump_gcidr(void);  /* 追加 */

#endif

リスト2.8: 020_dump_regs/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        /* ・・・ 省略 ・・・ */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* HPETの初期化 */
        hpet_init();
        dump_gcidr();   /* 追加 */
        while (1);

        /* ・・・ 省略 ・・・ */

2.1.4 動作確認

実行してみるとQEMUの場合、図2.2の様に表示されます。また、筆者の実機(Lenovo ThinkPad E450)で試すと図2.3の通りでした。

GCIDRのダンプ結果(QEMU)

図2.2: GCIDRのダンプ結果(QEMU)

GCIDRのダンプ結果(実機)

図2.3: GCIDRのダンプ結果(実機)

図2.2図2.3の結果から、表2.2がわかります。

表2.2: QEMUと実機でのGCIDRダンプ結果からわかること

 QEMU実機
タイマーの数(NUM_TIM_CAP)27
カウンタサイズ(COUNT_SIZE_CAP)64bit64bit
LegacyReplacementRoute(LEG_RT_CAP)使用可使用可
カウント周期(COUNTER_CLK_PERIOD)10ns約70ns

2.1.5 実装: その他のレジスタも見てみる

HPETの挙動を把握するために同様に以下2つのレジスタも見てみます。

GCRはHPET自体の有効化/無効化などを含む全タイマーで共通の設定を行う読み書き可能なレジスタです(表2.3)。

表2.3: GCRについて

ビット0ENABLE_CNFHPET自体の有効/無効
ビット1LEG_RT_CNFLegacyReplacementRouteの有効/無効
ビット2~63RESERVED予約領域

ENABLE_CNFフラグは「HPET自体を止めたい/開始させたい」場合に使っていきます。このフラグ無効化することでHPETのカウンタが止まり、割り込みも発生しなくなります。(逆に有効化すればカウンタ動作が開始し、別途設定する割り込み設定が有効化されていれば割り込みが発生するようになります。)

LEG_RT_CNFは、LegacyReplacementRouteを使用するか否かの設定です。本書の場合、前著*3にてPICを使用していましたので、本著でもPICを使って割り込みを扱うことにします*4

[*3] フルスクラッチで作る!x86_64自作OS

[*4] 新しい(今の)割り込みコントローラであるAPICは、また別の機会にチャレンジしてみたいです。本書ではHPETがちゃんと制御できることにフォーカスします。

次に、MCRはHPETに一つだけ存在するすべてのタイマーで共通の読み書き可能なカウンタです(表2.4)。

表2.4: MCRについて

ビット0~63カウンタ値

それでは、GCRとMCRの定義と、それぞれの内容を表示するdump_gcr関数とdump_mcr関数をhpet.cへ追加します(リスト2.9)。

リスト2.9: 020_dump_regs/hpet.c

/* ・・・ 省略 ・・・ */

/* General Capabilities and ID Register */
#define GCIDR_ADDR      (reg_base)
#define GCIDR   (*(volatile unsigned long long *)GCIDR_ADDR)
union gcidr {
        /* ・・・ 省略 ・・・ */
};

/* 追加(ここから) */
/* General Configuration Register */
#define GCR_ADDR        (reg_base + 0x10)
#define GCR     (*(volatile unsigned long long *)GCR_ADDR)
union gcr {
        unsigned long long raw;
        struct __attribute__((packed)) {
                unsigned long long enable_cnf:1;
                unsigned long long leg_rt_cnf:1;
                unsigned long long _reserved:62;
        };
};

/* Main Counter Register */
#define MCR_ADDR        (reg_base + 0xf0)
#define MCR     (*(volatile unsigned long long *)MCR_ADDR)
/* 追加(ここまで) */

/* ・・・ 省略 ・・・ */

void dump_gcidr(void)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void dump_gcr(void)
{
        puts("GCR\r\n");

        union gcr r;
        r.raw = GCR;

        puts("ENABLE CNF ");
        putd(r.enable_cnf, 1);
        puts("\r\n");

        puts("LEG RT CNF ");
        putd(r.leg_rt_cnf, 1);
        puts("\r\n");
}

void dump_mcr(void)
{
        puts("MCR ");
        puth(MCR, 16);
        puts("\r\n");
}
/* 追加(ここまで) */

プロトタイプ宣言をhpet.hへ追加し(リスト2.10)、main.cへ呼び出し処理を追加したら(リスト2.11)終わりです。

リスト2.10: 020_dump_regs/include/hpet.h

#ifndef _HPET_H_
#define _HPET_H_

void hpet_init(void);
void dump_gcidr(void);
void dump_gcr(void);    /* 追加 */
void dump_mcr(void);    /* 追加 */

#endif

リスト2.11: 020_dump_regs/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        /* ・・・ 省略 ・・・ */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* HPETの初期化 */
        hpet_init();
        dump_gcidr();
        /* 追加(ここから) */
        dump_gcr();
        dump_mcr();
        volatile unsigned long long wait = 1000000;
        while (wait--);
        dump_mcr();
        /* 追加(ここまで) */
        while (1);

        /* ・・・ 省略 ・・・ */

カウンタが動いていれば変化が分かるようにdump_mcr関数はウェイトをはさんで2回実行しています。

2.1.6 動作確認

実行するとQEMUでは図2.4のように、実機では図2.5のように表示されました。

020_dump_regsの実行結果(QEMU、GCRとMCR追加版)

図2.4: 020_dump_regsの実行結果(QEMU、GCRとMCR追加版)

020_dump_regsの実行結果(実機、GCRとMCR追加版)

図2.5: 020_dump_regsの実行結果(実機、GCRとMCR追加版)

図2.4図2.5の結果から、以下が分かります。

2.1.7 実装: HPET初期化時にHPETを明示的に無効化

これまでの動作確認から、起動時にHPETが動いているか停止しているかはマシンによって(少なくともQEMUと実機で)異なることが分かりました。

次項から実装するHPETを動作させる関数を呼び出すまでの間、HPETが動いている必要は無いので、hpet_init関数でHPETを一旦無効化しておくことにします(リスト2.12)。

リスト2.12: 020_dump_regs/hpet.c

/* ・・・ 省略 ・・・ */

void hpet_init(void)
{
        /* HPET tableを取得 */
        struct HPET_TABLE *hpet_table = (struct HPET_TABLE *)get_sdt("HPET");

        /* レジスタの先頭アドレスを取得 */
        reg_base = hpet_table->base_address.address;

        /* 追加(ここから) */
        /* 使うまでHPETは止めておく */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 0;
        GCR = gcr.raw;
        /* 追加(ここまで) */
}

/* ・・・ 省略 ・・・ */

2.2 sleepを実装してみる

まずは、割り込みを使わない簡単な例として、指定された時間処理を止める「sleep関数」を実装してみます。

この項のサンプルディレクトリは「021_sleep」です。

2.2.1 やること

前項で確認したGCRとMCRのレジスタを使って、以下の方針で実装します。

  1. MCRのenable_cnfが無効であれば有効化する
  2. MCRを監視して時間経過を待つ
  3. MCRのenable_cnfを元に戻す

2.2.2 実装

さっそく実装してみます(リスト2.13)。

リスト2.13: 021_sleep/hpet.c

#include <acpi.h>
#include <fbcon.h>

#define US_TO_FS        1000000000      /* 追加 */

/* ・・・ 省略 ・・・ */

void dump_mcr(void)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void sleep(unsigned long long us)
{
        /* 現在のmain counterのカウント値を取得 */
        unsigned long long mc_now = MCR;

        /* usマイクロ秒後のmain counterのカウント値を算出 */
        unsigned long long fs = us * US_TO_FS;
        union gcidr gcidr;
        gcidr.raw = GCIDR;
        unsigned long long mc_duration = fs / gcidr.counter_clk_period;
        unsigned long long mc_after = mc_now + mc_duration;

        /* HPETが無効であれば有効化する */
        union gcr gcr;
        gcr.raw = GCR;
        unsigned char to_disable = 0;
        if (!gcr.enable_cnf) {
                gcr.enable_cnf = 1;
                GCR = gcr.raw;

                /* sleep()を抜ける際に元に戻す(disableする) */
                to_disable = 1;
        }

        /* usマイクロ秒の経過を待つ */
        while (MCR < mc_after);

        /* 元々無効であった場合は無効に戻しておく */
        if (to_disable) {
                gcr.raw = GCR;
                gcr.enable_cnf = 0;
                GCR = gcr.raw;
        }
}
/* 追加(ここまで) */

sleep関数の引数で指定できる単位はマイクロ秒(us)としました。HPETのカウンタのカウント周期は数十ns(QEMUでは10ns)でしたので、引数をナノ秒とすると引数に1を指定された時、最低でも数十ナノ秒sleepしてしまいます。関数の仕様として「引数にはHPETの周期以上を指定すること」とするのも微妙に感じたので、単位を切り上げて引数にはマイクロ秒を指定することとしました。

sleep関数冒頭では、MCRの現在のカウント値を取得し、引数usをカウンタのカウント数へ変換した値を足すことで、指定された時間経過後のカウンタ値を求めています。

その後は、GCRでHPETが無効であれば有効化し(その際は「後で無効化する」旨のフラグをセットし)、MCRが先ほど求めたカウンタ値以上になるまで待機、最後に元々HPETが無効であったならば無効化して終わりです。

実装の最後にhpet.hへsleep関数のプロトタイプ宣言を追加し(リスト2.14)、main.cへ動作確認用のsleep関数呼び出し処理を追加します(リスト2.15)。

リスト2.14: 021_sleep/include/hpet.h

#ifndef _HPET_H_
#define _HPET_H_

#define MS_TO_US        1000            /* 追加 */
#define SEC_TO_US       1000000         /* 追加 */

void hpet_init(void);
void dump_gcidr(void);
void dump_gcr(void);
void dump_mcr(void);
void sleep(unsigned long long us);      /* 追加 */

#endif

リスト2.15: 021_sleep/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        /* ・・・ 省略 ・・・ */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* HPETの初期化 */
        hpet_init();

        /* 変更(ここから) */
        /* 5秒sleepしてみる */
        puts("WAIT ...");
        sleep(5 * SEC_TO_US);
        puts(" DONE\r\n");
        /* 変更(ここまで) */

        while (1);

        /* ・・・ 省略 ・・・ */

2.2.3 動作確認

実行すると"WAIT ..."が表示されてから(図2.6)5秒後に" DONE"が表示されます(図2.7)。

021_sleepの実行結果(&quot;WAIT ...&quot;)

図2.6: 021_sleepの実行結果("WAIT ...")

021_sleepの実行結果(&quot; DONE&quot;)

図2.7: 021_sleepの実行結果(" DONE")

2.3 alertを実装してみる

次は割り込みを使ってみます。割り込みを使用した例の第1弾として、指定された時間経過後に指定された処理を行うalert関数を実装してみます。

この項のサンプルディレクトリは「022_alert」です。

2.3.1 やること

やることは大きく分けて以下の2つです。

  1. HPETの割り込み設定
  2. ハンドラの設定

【HPETの割り込み設定】

HPETの割り込み設定は「Timer N Configuration and Capabilities Register (TNCCR)」というタイマー毎に存在するレジスタへ行います(表2.5)。

表2.5: TNCCRについて

ビット0RESERVED予約領域
ビット1INT_TYPE_CNFエッジトリガー(=0)/レベルトリガー(=1)
ビット2INT_ENB_CNF割り込み無効(=0)/有効(=1)
ビット3TYPE_CNF非周期割り込み(=0)/周期割り込み(=1)
ビット4PER_INT_CAP周期タイマーをサポートするか否か(ro)
ビット5SIZE_CAP32ビットサイズ(=0)/64ビットサイズ(=1)(ro)
ビット6VAL_SET_CNFタイマーのアキュムレータへのダイレクトアクセス許可
ビット7RESERVED予約領域
ビット832MODE_CNF強制的に32ビットモードにする
ビット9〜13INT_ROUTE_CNF割り込み経路設定
ビット14FSB_EN_CNFFront Side Bus(FSB)を使用するか否か
ビット15FSB_INT_DEL_CAPFSBをサポートしているか否か (ro)
ビット16RESERVED予約領域
ビット17〜63INT_ROUTE_CAP割り込みが配送されるI/O APICを示す(ro)

「(ro)」が付いているビットは書き換えできないビットです。

LegacyReplacementRoute使用時、割り込み経路は旧来のPICで決まっているので、INT_ROUTE_CNF等は設定しても何も起こりません。

今回TNCCRは以下のように設定します。

その他のビットは特に意識する必要はないので、何もしません。

また、TNCCRの場合はレジスタへの書き込みアクセス時、RESERVEDには0を書き込むことがデータシートで指定されている*5ので、明示的に0を書くようにします。

[*5] IA-PC HPET Specification Rev 1.0a P.17 Timer N Configuration and Capability Register Field Definitions

そして、「いつ割り込みを発生させるか」を設定するのが「Timer N Comparator Register (TNCR)」という、これもタイマー毎に存在するレジスタです(表2.6)。このレジスタを指して単に「コンパレータ(比較器)」と呼んだりもします。

表2.6: TNCRについて

ビット0〜63割り込みを発生させたいカウンタの値

最後がGCRのleg_rt_cnfです。本書ではPICで割り込みを扱うので、このフラグを有効化しておきます。

【ハンドラの設定】

割り込みのハンドラの設定としては、まず、x86 CPUが持つ割り込み設定のテーブル(IDT)の設定と、PIC側でHPETの割り込み番号の割り込みのマスクを解除する設定が必要です。

いずれも前著でset_intr_desc関数(intr.c)、enable_pic_intr関数(pic.c)を作っているので、それらを使います。

また、set_intr_desc関数ではその割り込みが発生したときに呼び出すハンドラを引数でしていする必要があるので、HPET割り込み発生時に呼び出されるHPET側のハンドラ関数も用意します。

なお、今回実装するalert関数では、指定された時間経過後に実施したい処理を関数ポインタとして引数に指定することとします。HPET側はalert関数呼び出しで指定された関数を、割り込み発生時にHPETのハンドラから呼び出すことにします。

2.3.2 実装

【HPETの割り込み設定】

まず、hpet.cへTNCCRとTNCRの定義を追加します(リスト2.16)。

リスト2.16: 022_alert/hpet.c

#include <acpi.h>
#include <fbcon.h>

#define TIMER_N         0       /* 使用するタイマー番号 */  /* 追加 */

#define US_TO_FS        1000000000

/* ・・・ 省略 ・・・ */

/* Main Counter Register */
#define MCR_ADDR        (reg_base + 0xf0)
#define MCR     (*(volatile unsigned long long *)MCR_ADDR)

/* 追加(ここから) */
/* Timer N Configuration and Capabilities Register */
#define TNCCR_ADDR(n)   (reg_base + (0x20 * (n)) + 0x100)
#define TNCCR(n)        (*(volatile unsigned long long *)(TNCCR_ADDR(n)))
#define TNCCR_INT_TYPE_EDGE     0
#define TNCCR_INT_TYPE_LEVEL    1
#define TNCCR_TYPE_NON_PERIODIC 0
#define TNCCR_TYPE_PERIODIC     1
union tnccr {
        unsigned long long raw;
        struct __attribute__((packed)) {
                unsigned long long _reserved1:1;
                unsigned long long int_type_cnf:1;
                unsigned long long int_enb_cnf:1;
                unsigned long long type_cnf:1;
                unsigned long long per_int_cap:1;
                unsigned long long size_cap:1;
                unsigned long long val_set_cnf:1;
                unsigned long long _reserved2:1;
                unsigned long long mode32_cnf:1;
                unsigned long long int_route_cnf:5;
                unsigned long long fsb_en_cnf:1;
                unsigned long long fsb_int_del_cap:1;
                unsigned long long _reserved3:16;
                unsigned long long int_route_cap:32;
        };
};

/* Timer N Comparator Register */
#define TNCR_ADDR(n)    (reg_base + (0x20 * (n)) + 0x108)
#define TNCR(n) (*(volatile unsigned long long *)(TNCR_ADDR(n)))
/* 追加(ここまで) */

/* ・・・ 省略 ・・・ */

併せて使用するタイマー番号の定義「TIMER_N」も追加しておきました。

次に、hpet_init関数へ割り込み周りの初期設定を追加します(リスト2.17)。

リスト2.17: 022_alert/hpet.c

/* ・・・ 省略 ・・・ */

unsigned long long reg_base;
unsigned int counter_clk_period;        /* 追加 */

void hpet_init(void)
{
        /* HPET tableを取得 */
        struct HPET_TABLE *hpet_table = (struct HPET_TABLE *)get_sdt("HPET");

        /* レジスタの先頭アドレスを取得 */
        reg_base = hpet_table->base_address.address;

        /* 使うまでHPETは止めておく
         * 併せてLegacy Replacement Route有効化 */    /* 追加 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 0;
        gcr.leg_rt_cnf = 1;                             /* 追加 */
        GCR = gcr.raw;

        /* 追加(ここから) */
        /* カウント周期を取得 */
        union gcidr gcidr;
        gcidr.raw = GCIDR;
        counter_clk_period = gcidr.counter_clk_period;

        /* 割り込み設定初期化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.int_type_cnf = TNCCR_INT_TYPE_EDGE;
        tnccr.int_enb_cnf = 0;
        tnccr.type_cnf = TNCCR_TYPE_NON_PERIODIC;
        tnccr.val_set_cnf = 0;
        tnccr.mode32_cnf = 0;
        tnccr.fsb_en_cnf = 0;
        tnccr._reserved1 = 0;
        tnccr._reserved2 = 0;
        tnccr._reserved3 = 0;
        TNCCR(TIMER_N) = tnccr.raw;
        /* 追加(ここまで) */
}

/* ・・・ 省略 ・・・ */

type_cnfの初期値を「非周期割り込み」側へ倒しているのは、これがどちらであるかによってval_set_cnfへ「書いてはいけない値」が変わるからです。

val_set_cnfはデータシートを読む限り、周期割り込み時のみ「動作中だけどコンパレータを書き換えるよ」ということを伝えるレジスタのようで、かつ「1は書いて良いが0は書いてはいけない(自動的に0に戻る)」と決められています。また、「周期割り込みでは無いときに1を書いてはいけない」という決まりもあります。

そこで、初期化時は「非周期割り込み」として、val_set_cnfへは0を書いています。

また、リスト2.17では、カウンタのカウント周期をあらかじめ取得し、グローバル変数counter_clk_periodへ格納するようにしています。

そして、alert関数を追加し、割り込みやHPET自体の有効化やコンパレータ設定を行います(リスト2.18)。(hpet.hへのalert関数のプロトタイプ宣言追加もよしなにやっておきます。)

リスト2.18: 022_alert/hpet.c

/* ・・・ 省略 ・・・ */

void sleep(unsigned long long us)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void alert(unsigned long long us, void *handler)
{
        /* 非周期割り込みで割り込み有効化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.int_enb_cnf = 1;
        tnccr.type_cnf = TNCCR_TYPE_NON_PERIODIC;
        tnccr._reserved1 = 0;
        tnccr._reserved2 = 0;
        tnccr._reserved3 = 0;
        TNCCR(TIMER_N) = tnccr.raw;

        /* main counterをゼロクリア */
        MCR = (unsigned long long)0;

        /* コンパレータ設定 */
        unsigned long long femt_sec = us * US_TO_FS;
        unsigned long long clk_counts = femt_sec / counter_clk_period;
        TNCR(TIMER_N) = clk_counts;

        /* HPET有効化 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 1;
        GCR = gcr.raw;
}
/* 追加(ここまで) */

引数で受け取ったhandlerの設定などは次節で行います。

【ハンドラの設定】

まず、割り込み発生時に一番最初に呼び出されるハンドラ「hpet_handler」をhandler.sに作成します(リスト2.19)。

リスト2.19: 022_alert/handler.s

        .global default_handler
default_handler:
        jmp     default_handler

        /* 追加(ここから) */
        .global hpet_handler
hpet_handler:
        push    %rax
        push    %rcx
        push    %rdx
        push    %rbx
        push    %rbp
        push    %rsi
        push    %rdi
        mov     %rsp, %rdi
        call    do_hpet_interrupt
        pop     %rdi
        pop     %rsi
        pop     %rbp
        pop     %rbx
        pop     %rdx
        pop     %rcx
        pop     %rax
        iretq
        /* 追加(ここまで) */

        .global kbc_handler
kbc_handler:
        /* ・・・ 省略 ・・・ */

hpet_handlerから呼び出されるdo_hpet_interrupt関数も作成しておきます(リスト2.20)。

リスト2.20: 022_alert/hpet.c

#include <pic.h>  /* 追加 */
#include <acpi.h>
#include <fbcon.h>
#include <hpet.h> /* 追加 */

#define TIMER_N         0       /* 使用するタイマー番号 */

#define US_TO_FS        1000000000

/* ・・・ 省略 ・・・ */

void sleep(unsigned long long us)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void do_hpet_interrupt(unsigned long long current_rsp)
{
        /* HPET無効化 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 0;
        GCR = gcr.raw;

        /* 割り込みを無効化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.int_enb_cnf = 0;
        tnccr._reserved1 = 0;
        tnccr._reserved2 = 0;
        tnccr._reserved3 = 0;
        TNCCR(TIMER_N) = tnccr.raw;

        /* ユーザーハンドラを呼び出す */
        if (user_handler)
                user_handler(current_rsp);

        /* PICへ割り込み処理終了を通知(EOI) */
        set_pic_eoi(HPET_INTR_NO);
}
/* 追加(ここまで) */

void alert(unsigned long long us, void *handler)
{
        /* ・・・ 省略 ・・・ */
}

HPETの割り込み番号(HPET_INTR_NO)は今後hpet.cの外でも使うためhpet.hへ追加します(リスト2.21)。alert関数のプロトタイプ宣言も併せて追加しておきます(リスト2.21)。

リスト2.21: 022_alert/include/hpet.h

#ifndef _HPET_H_
#define _HPET_H_

#define MS_TO_US        1000
#define SEC_TO_US       1000000
#define HPET_INTR_NO    32              /* 追加 */

void hpet_init(void);
void dump_gcidr(void);
void dump_gcr(void);
void dump_mcr(void);
void sleep(unsigned long long us);
void alert(unsigned long long us, void *handler);       /* 追加 */

#endif

なお、HPETの割り込み番号の定数「HPET_INTR_NO」を「32」として追加していますが、これはPICの時代から決められているものです*6

[*6] 割り込み番号32はIRQ番号だと0で、厳密にはIRQ0がPICの時代からタイマーの割り込みと決められています。IRQが32番から始まるのはそのように割り込みの設定を行っているからで、詳しくは前著「フルスクラッチで作る!x86_64自作OS(パート1)」をご覧ください。

また、HPETのC言語部分のハンドラ(do_hpet_interrupt関数)にスタックポインタを渡しているのは、次章でスケジューラを実装する際に使用するためです。

そして、do_hpet_interrupt関数から呼び出されるuser_handler関数も関数ポインタをグローバル変数として用意しておき、alert関数内で関数ポインタを設定するようにします(リスト2.22)。

リスト2.22: 022_alert/hpet.c

#include <pic.h>
#include <acpi.h>
#include <fbcon.h>
#include <hpet.h>
#include <common.h>       /* 追加 */
/* ・・・ 省略 ・・・ */

/* Timer N Comparator Register */
#define TNCR_ADDR(n)    (reg_base + (0x20 * (n)) + 0x108)
#define TNCR(n) (*(volatile unsigned long long *)(TNCR_ADDR(n)))

void (*user_handler)(unsigned long long current_rsp) = NULL;    /* 追加 */

/* ・・・ 省略 ・・・ */

void alert(unsigned long long us, void *handler)
{
        /* ユーザーハンドラ設定 */        /* 追加 */
        user_handler = handler; /* 追加 */

        /* ・・・ 省略 ・・・ */

ここまでで各種ハンドラの作成は完了です。

x86 CPUの割り込み設定(IDT)へ最初に呼び出すハンドラ(hpet_handler)の登録と、PICへHPETの割り込みマスクの解除をhpet_init関数へ追加します(リスト2.23)。

リスト2.23: 022_alert/hpet.c

#include <intr.h> /* 追加 */
#include <pic.h>
#include <acpi.h>
#include <fbcon.h>
#include <hpet.h>
#include <common.h>

#define TIMER_N         0       /* 使用するタイマー番号 */

#define US_TO_FS        1000000000

/* ・・・ 省略 ・・・ */

void hpet_handler(void);        /* 追加 */
void (*user_handler)(unsigned long long current_rsp) = NULL;

void hpet_init(void)
{
        /* ・・・ 省略 ・・・ */

        /* 割り込み設定初期化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.int_type_cnf = TNCCR_INT_TYPE_EDGE;
        tnccr.int_enb_cnf = 0;
        tnccr.type_cnf = TNCCR_TYPE_NON_PERIODIC;
        tnccr.val_set_cnf = 0;
        tnccr.mode32_cnf = 0;
        tnccr.fsb_en_cnf = 0;
        tnccr._reserved1 = 0;
        tnccr._reserved2 = 0;
        tnccr._reserved3 = 0;
        TNCCR(TIMER_N) = tnccr.raw;

        /* 追加(ここから) */
        /* IDTへHPET割り込みのハンドラ登録 */
        set_intr_desc(HPET_INTR_NO, hpet_handler);

        /* PICの割り込みマスク解除 */
        enable_pic_intr(HPET_INTR_NO);
        /* 追加(ここまで) */
}

/* ・・・ 省略 ・・・ */

ここまででhpet.cの追加作業は完了で、あとはmain.cから呼び出すようにします(リスト2.24)。

リスト2.24: 022_alert/main.c

/* ・・・ 省略 ・・・ */

void handler(void);     /* 追加 */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* CPU周りの初期化 */
        gdt_init();
        intr_init();

        /* 周辺ICの初期化 */
        pic_init();
        hpet_init();    /* 追加 */
        kbc_init();

        /* ファイルシステムの初期化 */
        fs_init(_fs_start);

        /* 追加(ここから) */
        /* 5秒後にalertをセットしてみる */
        puts("WAIT ...");
        alert(5 * SEC_TO_US, handler);
        /* 追加(ここまで) */

        /* CPUの割り込み有効化 */
        enable_cpu_intr();

        /* haltして待つ */
        while (1)
                cpu_halt();
}

/* 追加(ここから) */
void handler(void)
{
        puts(" DONE");
}
/* 追加(ここまで) */

2.3.3 動作確認

実行するとsleepの例と同様に、"WAIT ..."が表示されてから5秒後に" DONE"が表示されます。(見た目は同じなのでスクリーンショットは省略)

2.4 周期タイマー(ptimer)を実装してみる

最後に指定された周期で周期的に動作させる「周期タイマー(ptimer)」を実装してみます。

この項のサンプルディレクトリは「023_ptimer」です。

2.4.1 やること

使うレジスタはalertを実装した際に使ったものと同じで、一部設定を変更するだけで周期タイマーになります。

なお、コンパレータの挙動としては、非周期割り込みの際は、カウンタがコンパレータと一致したら割り込みが発生してそれで終わりでした。周期割り込みの場合、割り込みが発生すると、コンパレータに最後に書き込まれた値が現在のコンパレータへ自動で加算されます。

例えば、コンパレータに最後に書き込まれた値が0x0123の場合、以下の挙動となります。

  1. カウンタが0x0123へ到達し、割り込み発生
  2. コンパレータへは最後に書かれた値が自動で加算され、新たなコンパレータ値は0x0246となる
  3. カウンタが0x0246へ到達し、割り込み発生
  4. コンパレータへは最後に書かれた値が自動で加算され、新たなコンパレータ値は0x0369となる

実装の方針としては、周期タイマーの設定を行う「ptimer_setup関数」と周期タイマーを開始させる「ptimer_start関数」、停止させる「ptimer_stop関数」の3つを実装することにします。

2.4.2 実装

さっそくptimer_setup関数とptimer_start関数、ptimer_stop関数を実装してみます(リスト2.25)。

リスト2.25: 023_ptimer/hpet.c

/* ・・・ 省略 ・・・ */

unsigned long long reg_base;
unsigned int counter_clk_period;
unsigned long long cmpr_clk_counts;     /* 追加 */

/* ・・・ 省略 ・・・ */

void alert(unsigned long long us, void *handler)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void ptimer_setup(unsigned long long us, void *handler)
{
        /* HPET無効化 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 0;
        GCR = gcr.raw;

        /* ユーザーハンドラ設定 */
        user_handler = handler;

        /* 周期割り込みで割り込み有効化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.int_enb_cnf = 1;
        tnccr.type_cnf = TNCCR_TYPE_PERIODIC;
        tnccr._reserved1 = 0;
        tnccr._reserved2 = 0;
        tnccr._reserved3 = 0;
        TNCCR(TIMER_N) = tnccr.raw;

        /* コンパレータ設定値を計算しておく */
        unsigned long long femt_sec = us * US_TO_FS;
        cmpr_clk_counts = femt_sec / counter_clk_period;
}

void ptimer_start(void)
{
        /* コンパレータ初期化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.val_set_cnf = 1;
        TNCCR(TIMER_N) = tnccr.raw;
        TNCR(TIMER_N) = cmpr_clk_counts;

        /* main counter初期化 */
        MCR = (unsigned long long)0;

        /* HPET有効化 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 1;
        GCR = gcr.raw;
}

void ptimer_stop(void)
{
        /* HPET無効化 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 0;
        GCR = gcr.raw;

        /* 割り込みを無効化 */
        union tnccr tnccr;
        tnccr.raw = TNCCR(TIMER_N);
        tnccr.int_enb_cnf = 0;
        tnccr._reserved1 = 0;
        tnccr._reserved2 = 0;
        tnccr._reserved3 = 0;
        TNCCR(TIMER_N) = tnccr.raw;
}
/* 追加(ここまで) */

ptimer_setup関数でコンパレータ値を計算するまでで、実際の設定をptimer_start関数で行うようにしているのは、ptimer_stop関数で止められた後、ptimer_start関数で再開させる際にコンパレータを初期化する必要があるからです。

また、HPET側の割り込みハンドラであるdo_hpet_interrupt関数冒頭でタイマーと割り込みの無効化を行っているので、このままだと1周期目の割り込みでそれらが無効化されてしまいます。

is_oneshotという「現在ワンショット動作中であるか否か」のフラグを追加します(リスト2.26)。

リスト2.26: 023_ptimer/hpet.c

/* ・・・ 省略 ・・・ */

unsigned long long reg_base;
unsigned int counter_clk_period;
unsigned long long cmpr_clk_counts;
unsigned char is_oneshot = 0;   /* 追加 */

/* ・・・ 省略 ・・・ */

void do_hpet_interrupt(unsigned long long current_rsp)
{
        /* 変更(ここから) */
        if (is_oneshot == 1) {
                /* HPET無効化 */
                union gcr gcr;
                gcr.raw = GCR;
                gcr.enable_cnf = 0;
                GCR = gcr.raw;

                /* 割り込みを無効化 */
                union tnccr tnccr;
                tnccr.raw = TNCCR(TIMER_N);
                tnccr.int_enb_cnf = 0;
                tnccr._reserved1 = 0;
                tnccr._reserved2 = 0;
                tnccr._reserved3 = 0;
                TNCCR(TIMER_N) = tnccr.raw;

                /* ワンショットタイマー設定を解除 */
                is_oneshot = 0;
        }
        /* 変更(ここまで) */

        /* ユーザーハンドラを呼び出す */
        if (user_handler)
                user_handler(current_rsp);

        /* PICへ割り込み処理終了を通知(EOI) */
        set_pic_eoi(HPET_INTR_NO);
}

void alert(unsigned long long us, void *handler)
{
        /* ・・・ 省略 ・・・ */

        /* コンパレータ設定 */
        unsigned long long femt_sec = us * US_TO_FS;
        unsigned long long clk_counts = femt_sec / counter_clk_period;
        TNCR(TIMER_N) = clk_counts;

        /* ワンショットタイマー設定 */      /* 追加 */
        is_oneshot = 1;                 /* 追加 */

        /* HPET有効化 */
        union gcr gcr;
        gcr.raw = GCR;
        gcr.enable_cnf = 1;
        GCR = gcr.raw;
}

/* ・・・ 省略 ・・・ */

最後にhpet.hへプロトタイプ宣言を追加し(コード省略)、main.cから呼び出してみます(リスト2.27)。

リスト2.27: 023_ptimer/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* ・・・ 省略 ・・・ */

        /* ファイルシステムの初期化 */
        fs_init(_fs_start);

        /* 1秒周期の周期タイマー設定 */             /* 変更 */
        ptimer_setup(1 * SEC_TO_US, handler);   /* 変更 */

        /* CPUの割り込み有効化 */
        enable_cpu_intr();

        /* 周期タイマースタート */                /* 追加 */
        ptimer_start();                         /* 追加 */

        /* haltして待つ */
        while (1)
                cpu_halt();
}

void handler(void)
{
        /* 変更(ここから) */
        static unsigned char counter = 0;
        if (counter < 10)
                putc('0' + counter++);
        else
                ptimer_stop();
        /* 変更(ここまで) */
}

2.4.3 動作確認

実行すると「0」から「9」までの数字が1秒周期で出力されたところで停止します(図2.8)。

023_ptimerの実行結果

図2.8: 023_ptimerの実行結果


Top