Top

第2章 キー入力を取得する

第1章では、UEFI仕様書から機能を調べ、UEFIアプリケーションの作成・実行までの流れを説明しました。この章では、キー入力の取得方法を紹介し、簡単なシェルもどきを作ってみます。

2.1 EFI_SIMPLE_TEXT_INPUT_PROTOCOL

キー入力を取得する関数は"EFI_SIMPLE_TEXT_INPUT_PROTOCOL"の中にあります。EFI_SIMPLE_TEXT_INPUT_PROTOCOLも、前の章でテキスト出力のために使用したEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLと同じく、SystemTableのメンバです(図2.1)。

EFI_SYSTEM_TABLEの定義(一部)(再掲)

図2.1: EFI_SYSTEM_TABLEの定義(一部)(再掲)

EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義はリスト2.1の通りです。リスト2.1では使用する関数のみ定義しています。EFI_SIMPLE_TEXT_INPUT_PROTOCOLの全体は、仕様書の"11.3 Simple Text Input Protocol(P.420)"を参照してください。

リスト2.1: EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義

struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL {
        unsigned long long _buf;
        unsigned long long (*ReadKeyStroke)(
                struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
                struct EFI_INPUT_KEY *Key);
};

キー入力は、EFI_SIMPLE_TEXT_INPUT_PROTOCOLの"ReadKeyStroke"関数で取得できます(仕様書"11.3 Simple Text Input Protocol(P.423)"参照)。ReadKeyStroke関数はノンブロッキングの関数で、関数実行時にキー入力が無ければエラーのステータスを返します。

引数の意味は以下の通りです(第1引数は省略)。

struct EFI_INPUT_KEY *Key
取得した文字を格納するポインタ。

なお、EFI_INPUT_KEY構造体の定義はリスト2.2の通りです。

リスト2.2: EFI_INPUT_KEYの定義

struct EFI_INPUT_KEY {
        unsigned short ScanCode;
        unsigned short UnicodeChar;
};

また、リスト2.2のメンバの意味は以下の通りです。

unsigned short ScanCode
Unicode範囲外のキー(ESC、上下左右、Fn等)の値を表すスキャンコード。スキャンコードの一覧は仕様書P.410の"Table 88"を参照。本書ではESCキー(ScanCode=0x17)のみ使用する。Unicode範囲内のキー(英数字、Enter)が入力された時、ScanCodeへは0が格納される。
unsigned short UnicodeChar
Unicode範囲内のキー入力時、入力文字に対応するUnicode値が格納される。Unicode範囲外のキー入力時、0が格納される。

2.2 エコーバックプログラムを作ってみる

ReadKeyStroke関数を使用して、取得した文字を画面へ出力する「エコーバック」のサンプルをリスト2.3に示します。サンプルのディレクトリは"sample2_1_echoback"です。

リスト2.3: sample2_1_echoback/main.c

 1: struct EFI_INPUT_KEY {
 2:     unsigned short ScanCode;
 3:     unsigned short UnicodeChar;
 4: };
 5: 
 6: struct EFI_SYSTEM_TABLE {
 7:     char _buf1[44];
 8:     struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL {
 9:             unsigned long long _buf;
10:             unsigned long long (*ReadKeyStroke)(
11:                     struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
12:                     struct EFI_INPUT_KEY *Key);
13:     } *ConIn;
14:     unsigned long long _buf2;
15:     struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
16:             unsigned long long _buf;
17:             unsigned long long (*OutputString)(
18:                     struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
19:                     unsigned short *String);
20:             unsigned long long _buf2[4];
21:             unsigned long long (*ClearScreen)(
22:                     struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This);
23:     } *ConOut;
24: };
25: 
26: void efi_main(void *ImageHandle __attribute__ ((unused)),
27:           struct EFI_SYSTEM_TABLE *SystemTable)
28: {
29:     struct EFI_INPUT_KEY key;
30:     unsigned short str[3];
31:     SystemTable->ConOut->ClearScreen(SystemTable->ConOut);
32:     while (1) {
33:             if (!SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn,
34:                                                    &key)) {
35:                     if (key.UnicodeChar != L'\r') {
36:                             str[0] = key.UnicodeChar;
37:                             str[1] = L'\0';
38:                     } else {
39:                             str[0] = L'\r';
40:                             str[1] = L'\n';
41:                             str[2] = L'\0';
42:                     }
43:                     SystemTable->ConOut->OutputString(SystemTable->ConOut,
44:                                                       str);
45:             }
46:     }
47: }

リスト2.3では、EFI_SIMPLE_TEXT_INPUT_PROTOCOLの定義をEFI_SYSTEM_TABLEに追加しています。

efi_main関数内について、ClearScreen後のwhileの無限ループがエコーバックの処理です。ReadKeyStrokeでキー入力を取得できたら、str配列へヌル文字(L'\0')を付加した文字列として格納し、OutputStringで画面へ表示します。なお、Enterキーの入力時はCR('\r')を取得するため、取得した文字がCRのときはLF('\n')も出力するようにしています。

サンプルを実行すると、入力した文字がそのまま表示されるエコーバック動作を確認できます(図2.2)。

エコーバックサンプル実行の様子

図2.2: エコーバックサンプル実行の様子

補足: キー入力を待つ(WaitForKey)

リスト2.3では、while()内でReadKeyStroke関数が成功するまで、ReadKeyStroke関数を何度も呼び出しています。しかし、キー入力が得られるまでCPUを休ませてあげた方がCPUに優しいです。

そのためにEFI_SIMPLE_TEXT_INPUT_PROTOCOLには"WaitForKey"というメンバ変数があります(リスト2.4、仕様書"11.3 Simple Text Input Protocol(P.421)")。

リスト2.4: WaitForKeyの定義

struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL {
        unsigned long long _buf;
        unsigned long long (*ReadKeyStroke)(
                struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
                struct EFI_INPUT_KEY *Key);
        void *WaitForKey;
};

"void *"は、UEFIでイベントを指す"EFI_EVENT"型の実体で、仕様書上はEFI_EVENT WaitForKeyと定義されています。仕様書P.421のWaitForKeyの説明にも記載の通り、WaitForKeyはWaitForEvent関数で使用できます。

WaitForEventは指定したイベントの発生を待つ関数です。SystemTable->BootServices内で定義されています。BootServicesはEFI_BOOT_SERVICESという構造体で、主にブートローダー向けにUEFIが提供している関数(サービス)を持ちます(詳細は次の章で説明します)。WaitForEventの定義はリスト2.5の通りです。

リスト2.5: WaitForEventの定義

unsigned long long (*WaitForEvent)(
        unsigned long long NumberOfEvents,
        void **Event,
        unsigned long long *Index);

引数の意味は以下の通りです。

unsigned long long NumberOfEvents
第2引数Eventに指定するイベントの数。
void **Event
イベントリスト。
unsigned long long *Index
発生したイベントのイベントリスト内のインデックスを設定する変数のポインタ。

WaitForKeyとWaitForEventを使用して、リスト2.6の様にキー入力を待つことができます。

リスト2.6: WaitForKeyとWaitForEventを使用する例

 1: struct EFI_INPUT_KEY key;
 2: unsigned long long waitidx;
 3: 
 4: /* キー入力取得まで待機 */
 5: SystemTable->BootServices->WaitForEvent(1,
 6:     &(SystemTable->ConIn->WaitForKey), &waitidx);
 7: 
 8: /* キー入力取得 */
 9: SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &key);

2.3 シェルっぽいものを作ってみる

ここまでで、コンソール画面上での文字の入出力ができるようになりました。OSっぽいものを作る上で、まずはシェルっぽいものを作ってみます。サンプルのディレクトリは"sample2_2_shell"です。

このサンプルでは、これからOSっぽいものを作っていく上で土台となるソースコード構成を用意します。ここでは、関数化を適宜行い、ソースコードを以下の様に分けます。

main.c
エントリポイント(efi_main)を配置。
efi.h,efi.c
UEFI仕様上の定義や、初期化処理を配置。
common.h,common.c
汎用的に使用される定義や関数を配置。
shell.h,shell.c
シェルの処理を配置。

エントリポイントの引数であるSystemTableは、何をするにしても必要になるため、グローバル変数へ格納しておくことにします。その処理を行うのがefi.cの初期化処理(efi_init関数)です(リスト2.7)*1。efi_initではSetWatchdogTimerという関数を呼び出していますが、これについては後述のコラムで説明します。

[*1] EDK2やgnu-efiといった開発環境やツールチェインでも同様に、SystemTable等をグローバル変数へ格納する枠組みになっています。

リスト2.7: sample2_2_shell/efi.c

 1: #include "efi.h"
 2: #include "common.h"
 3: 
 4: struct EFI_SYSTEM_TABLE *ST;
 5: 
 6: void efi_init(struct EFI_SYSTEM_TABLE *SystemTable)
 7: {
 8:     ST = SystemTable;
 9:     ST->BootServices->SetWatchdogTimer(0, 0, 0, NULL);
10: }

そして、シェルのソースコードはリスト2.8、エントリポイントのソースコードはリスト2.9の通りです。

リスト2.8: sample2_2_shell/shell.c

 1: #include "common.h"
 2: #include "shell.h"
 3: 
 4: #define MAX_COMMAND_LEN     100
 5: 
 6: void shell(void)
 7: {
 8:     unsigned short com[MAX_COMMAND_LEN];
 9: 
10:     while (TRUE) {
11:             puts(L"poiOS> ");
12:             if (gets(com, MAX_COMMAND_LEN) <= 0)
13:                     continue;
14: 
15:             if (!strcmp(L"hello", com))
16:                     puts(L"Hello UEFI!\r\n");
17:             else
18:                     puts(L"Command not found.\r\n");
19:     }
20: }

リスト2.9: sample2_2_shell/main.c

 1: #include "efi.h"
 2: #include "shell.h"
 3: 
 4: void efi_main(void *ImageHandle __attribute__ ((unused)),
 5:           struct EFI_SYSTEM_TABLE *SystemTable)
 6: {
 7:     SystemTable->ConOut->ClearScreen(SystemTable->ConOut);
 8:     efi_init(SystemTable);
 9: 
10:     shell();
11: }

リスト2.8では、"OSっぽいもの"ということで、プロンプトに"poiOS"と付けてみました*2

[*2] "mockOS"とか"OSmodoki"とかも考えていたのですが、Google検索してみると、どちらも既に世の中に存在するようです。

リスト2.8で登場した各種の定数や、関数puts・gets・strcmpは、common.hとcommon.cで定義しています。これまで説明したUEFIの機能の呼び出し方を関数化しただけなので、紙面上では特に紹介しません(独特な実装方法をしているわけでもないので)。気になる方は、GitHubのサンプルコードをダウンロードして見てみてください。

また、リスト2.9では、efi_init関数でUEFIの初期化処理を行い、shell関数を実行することでシェルを起動しています。以降、main.cは書き換えません。

そして、サンプル実行の様子は図2.3の通りです。

シェルもどき実行の様子

図2.3: シェルもどき実行の様子

5分のウォッチドッグタイマを解除する

実はUEFIアプリケーション起動時、ウォッチドッグタイマがセットされています。その時間は5分ですので、何もしないでいると、UEFIアプリケーションが起動してから5分後に再起動することになります。ウォッチドッグタイマはSystemTable->BootServices->SetWatchdogTimer関数で解除できます。

SetWatchdogTimer関数の定義はリスト2.10の通りです(仕様書"6.5 Miscellaneous Boot Services(P.201)")。

リスト2.10: SetWatchdogTimerの定義

unsigned long long (*SetWatchdogTimer)(
        unsigned long long Timeout,
        unsigned long long WatchdogCode,
        unsigned long long DataSize,
        unsigned short *WatchdogData);

また、引数の意味は以下の通りです。

unsigned long long Timeout
ウォッチドッグのタイムアウト時間。0を指定するとウォッチドッグタイマー無効化。無効化の際、その他の引数は0あるいはNULLで良い。
unsigned long long WatchdogCode
タイムアウト時のウォッチドッグコード(イベント番号)を指定。本書では使用しない。
unsigned long long DataSize
WatchdogDataのデータサイズ(バイト指定)。
unsigned short *WatchdogData
オプショナルであり、本書では使用しない。加えて、何なのか良く分かっていないです(ウォッチドッグイベント発生時にログに記録される追加の説明?)。

ウォッチドッグタイマー無効化のコード例はリスト2.11の通りです。

リスト2.11: ウォッチドッグタイマ無効化

ST->BootServices->SetWatchdogTimer(0, 0, 0, NULL);

Top