Operating Systems Development Series
PIC, PIT, and exceptions
by Mike, 2008, 2009
Please note: This tutorial covers hardware interrupt handling, not software interrupt handling. If you are looking for software interrupts, please see Tutorial 15. This tutorial requires knowledge of software interrupt handling.

はじめに

ようこそ...あれ?もうチュートリアル16?

前回のチュートリアルでは、割込み処理の世界に深く潜り込みました。ソフトウェア割込み処理に必要なことはほぼすべて網羅しました。しかし、ハードウェア割り込みについてはどうでしょうか?

多くの重要なシステムデバイスは割り込みを使うので、ハードウェアデバイスによって引き起こされる割り込みを処理し、キャッチすることができるようにする必要があります。良いニュース?これはもうすでに私たちのために行われています!8259プログラマブル割り込みコントローラ(PIC)である。次のセクションで詳しく見ていきましょう。

たとえハードウェア割り込みがそれ自体で動作するようになったとしても、システムタイマーの問題に遭遇することになります。システムタイマーは、私たちが設定した有効な割り込みハンドラを使用しない限り、ハードウェア割り込みを有効にしてから数ミリ秒後にトリプルフォルトを起こします。結局のところ、無効な割り込みハンドラを呼び出すことになるわけですからね。したがって、この小さな問題は、プログラマブルインターバルタイマ(PIT)として知られるシステムタイマを再プログラムすることによっても解決されるでしょう。

このチュートリアルでは、多くのことを学びます。このチュートリアルでは、次のようなことを学びます。

  • ハードウェア割り込み
  • インタラプトチェーン
  • Hal: プログラマブルインタラプトコントローラ
  • Hal:プログラマブルインターバルタイマ
  • ハードウェアの抽象化
  • 割り込みのインプリメンテーションとHALの設計
割込み処理については触れていませんのでご注意ください。割込み処理についてはチュートリアルをご覧ください。

さて、それでは早速見ていきましょう。

ハードウェア割り込み

割り込みには、ソフトウェアで発生させるもの(INT, INT 3, BOUND, INTOなどの命令で発生させるもの)と、ハードウェアで発生させるものとがあります。

ハードウェア割り込みは、PCにとって非常に重要です。他のハードウェアデバイスから、何かが起ころうとしていることをCPUに知らせることができます。例えば、キーボードのキーストロークや、内部タイマーの1クロックの刻みなどです。

これらの割り込みが発生したときに、どのような割り込み要求(IRQ)を発生させるかをマッピングしておく必要があります。こうすることで、ハードウェアの変化を追跡することができます。

これらのハードウェア割り込みについて見てみましょう。

x86 Hardware Interrupts
8259A Input pin Interrupt Number Description
IRQ0 0x08 Timer
IRQ1 0x09 Keyboard
IRQ2 0x0A Cascade for 8259A Slave controller
IRQ3 0x0B Serial port 2
IRQ4 0x0C Serial port 1
IRQ5 0x0D AT systems: Parallel Port 2. PS/2 systems: reserved
IRQ6 0x0E Diskette drive
IRQ7 0x0F Parallel Port 1
IRQ8/IRQ0 0x70 CMOS Real time clock
IRQ9/IRQ1 0x71 CGA vertical retrace
IRQ10/IRQ2 0x72 Reserved
IRQ11/IRQ3 0x73 Reserved
IRQ12/IRQ4 0x74 AT systems: reserved. PS/2: auxiliary device
IRQ13/IRQ5 0x75 FPU
IRQ14/IRQ6 0x76 Hard disk controller
IRQ15/IRQ7 0x77 Reserved

各デバイスについては、まだそれほど心配する必要はありません。 8259Aのピンは、8259PICチュートリアルで詳しく説明されています。 この表に記載されている割り込み番号は、これらのイベントが発生したときに実行するデフォルトのDOS割り込み要求(IRQ)です。

ほとんどの場合、新しい割り込みテーブルを再作成する必要があります。このように、ほとんどのOSでは、PICが使用する割り込みを再マッピングして、IVT内で適切なIRQを呼び出すことを保証する必要があります。これは、リアルモードIVTのBIOSによって行われます。 このチュートリアルの後半で、これを行う方法についても説明します。

待てよ、このPICというのは何なんだ?ハードウェアデバイスに信号を送ることができるこれらのハードウェアデバイスはすべて、8259Aプログラマブル割り込みコントローラ(PIC)に間接的に接続されています。これは特別な、そして非常に重要なマイクロコントローラーで、マイクロプロセッサーがハードウェア割り込みを発射する必要があるときに信号を送るために使用されます。

このマイクロコントローラのプログラミングは、このチュートリアルの後半で少し行います。このマイクロコントローラはかなり複雑なので、別のチュートリアルを用意しています。こちらをお読みください。

割り込みの連鎖

IDT(Interrupt Descriptor Table)内に独自の割り込みハンドラを非常に簡単に設置することができるようになります。ソフトウェア割り込みだけでなく、ハードウェアデバイスから発生する割り込みも処理するために割り込みハンドラを作成します。覚えておいてください。ハードウェアデバイスは、プログラマブル割り込みコントローラに信号を送り、プロセッサにハードウェア割り込みの発生を要求します。PICはプロセッサに、割り込み記述子テーブル(IDT)内でどの割り込み要求(IRQ)を呼び出すかを知らせます。

しかし、待ってください...IDTの中でどのIRQを呼び出すか、PICはどのように知るのでしょうか?私たちはそれを教えています。

そのため、どの割り込みを使用するかを知らせるために、PICを再プログラムする必要があります。

さて、ソフトウェアとハードウェアの割り込みを処理するための割り込みハンドラができたとします。さて、どうでしょう?私たちの視点から見るとどうでしょうか?確かにデバイスごとにハンドラをインストールするのは簡単ですが、複数のデバイスが同じ割り込みを要求したらどうでしょう?ソフトウエア割り込みで複数の機能が必要な場合はどうでしょうか?そこで登場するのがインタラプトチェーンです。

インタラプトチェーニングは、同じ割り込み番号を共有するすべての割り込みハンドラを復元して呼び出すために使用される技術です。これは、以前の割込みルーチン(IR)を関数ポインタに保存し、新しいハンドラをインストールし、新しいIRが呼ばれるたびに以前の割込みハンドラを呼び出すことによって行われます。

以下はその例です。

void deviceInitialize () { //store previus interrupt handler prevhandler = getvect (0); //install new interrupt handler setvect (0, handler); } void deviceShutdown () { //install previus interrupt handler setvect (0, prevhandler); } void handler () { // do stuff... // call previus interrupt handler (*prevhandler) (); }
ご覧のように、割り込みの連鎖は非常に簡単です。setvect()は新しい割り込みベクタをインストールし、getvect()は割り込みベクタを返します。これらの割り込みベクタは、割り込みベクタテーブル(IVT)または割り込みディスクリプタテーブル(IDT)に格納することができます。待って、何?そうです、私たちのものです。)

割込み処理の実装の準備

私たちは割り込みと割り込み処理について多くの領域をカバーしました。テキストだけでは限界があります。ハードウェアの割り込み処理の仕組みも少し見てきましたが、まだ十分ではありません。

プログラマブルインタラプトコントローラのプログラミングを学ぶまでは、ハードウェア割り込み処理を実装できません。 また、タイミングの問題を解決するまでは、ハードウェア割り込みを有効にできません(BIOSのおかげでプログラマブルインターバルタイマがまだIRQ8に接続されているのを覚えていますか?つまり、ハードウェア割り込みを再度有効にすると、次のタイマーティックでダブルフォールトが発生するのです)。このため、Programmable Interval Timerを再プログラムする方法を学ぶ必要があります。

ここからが、読者の皆さんにとって、ややこしいところです。ハードウェアプログラミングの世界へようこそ!)

しかし、良いニュースもあります...これらのマイクロコントローラは、どれもそれほど複雑ではありません。しかし、本シリーズが複雑になり過ぎないように、これらのマイコンに特化したチュートリアルを2つ書くことにしました。このチュートリアルは、この先のデモやコードを理解するための必読書です。

このため、読者の皆さんには、次のチュートリアルを読んでから続きを読むことをお勧めします。

もし、このチュートリアルの内容をすべて理解できなくても心配しないでください。この後、このチュートリアルの内容をすべて実装していきます。:)また、私はここですべてを説明しますので、あなたを暗闇に導くことはありません。

また、上記のチュートリアルを参考文献として使用することも有用でしょう。この後のセクションで、上記のチュートリアルを多く参照します。

さてと...?何を待っているのですか?これらのチュートリアルに飛び込んでください。そして、終わったらここに戻って来てください。心配しないでください、私はただのテキストです...あなたが戻ってきても、私はまだここにいます。)

ハードウェアの抽象化

最初に見ていくのは、ハードウェア抽象化レイヤーが提供するインターフェイスです。これはinclude/hal.hと hal/hal.cppを見ればわかることです。ルーチンのほとんどは非常に単純で、私たちが開発した(そしてこれから開発する)他のインターフェース(GDT、IDT、CPU、PIC、PITなど)を単に使用するだけなので、深く説明することはありません。その代わりに、インタフェースそのものを見てみたいと思います。これはカーネルとデバイスドライバが使うインタフェースになるわけですが、なぜそうしないのでしょうか?

新しい hal.h

ここで、ハードウェアの抽象化がいかに有用であるかがわかるのです。私は、16bitのDOSをプログラミングするのと同じように簡単に使える「DOS」的なインタフェースを提供したいと思いました。そのために、さまざまな目的に使えるルーチンの簡単なリストを作りました。これらのルーチンを見てみると、そのルーチンが使用するハードウェアデバイスやテーブルを全く参照していないことがわかります。 これこそハードウェアの抽象化です。アーキテクチャを抽象化するのではなく、そのアーキテクチャが使用するハードウェアを抽象化するのです。

この後使うコードの多くは、HALの中のルーチンを使ってタスクを実行します。このため、ハードウェアの抽象化レイヤと、それが提供するルーチンを見ていただきたいと思います。

extern int _cdecl hal_initialize (); extern int _cdecl hal_shutdown (); extern void _cdecl enable (); extern void _cdecl disable (); extern void _cdecl geninterrupt (int n); extern unsigned char _cdecl inportb (unsigned short id); extern void _cdecl outportb (unsigned short id, unsigned char value); extern void _cdecl setvect (int intno, void (_cdecl far &vect) ( ) ); extern void (_cdecl far * _cdecl getvect (int intno)) ( ); extern bool _cdecl interruptmask (uint8_t intno, bool enable); extern inline void _cdecl interruptdone (unsigned int intno); extern void _cdecl sound (unsigned frequency); extern const char* _cdecl get_cpu_vender (); extern int _cdecl get_tick_count ();

もし、あなたが16bit DOSをプログラムしたことがあるなら、今すぐ家にいるような気分になれるはずです!:)

プログラマブルインタラプトコントローラ

8259:マイクロコントローラ

8259マイクロコントローラファミリは、PIC(Programmable Interrupt Controller)集積回路(IC)のセットであり、ハードウェア割り込みが要求されると、ハードウェアコントローラはPICに間接的に接続される。このため、ハードウェア割り込みを処理するためには、このマイクロコントローラのプログラム方法を理解しておく必要があります。

8259は複雑なマイクロコントローラなので、ここですべてを説明します。このため、このコントローラだけをカバーするために、完全なチュートリアルを用意しました。このため、このセクションを最大限に活用するために、PICについて学ぶために次のチュートリアルを参照してください(参照)

8259Aプログラマブルインタラプトコントローラチュートリアル

注意:ここでは、PICやハードウェア割り込み処理に関するすべてをカバーするわけではありません。上記のチュートリアルを参照してください。

8259:概要

PIC(Programmable Interrupt Controller)は、割り込み線を通じてデバイスとプロセッサの接続を行うためのマイクロコントローラです。これにより、デバイスがシステムソフトウェアやエグゼクティブからの注意を必要とするときはいつでも、プロセッサに信号を送ることができます。これがIRQ(Interrupt Request)です。

PICはハードウェア割り込み要求のすべてを制御します。これにより、異なるハードウェアデバイスが注意を必要とするときに、そのデバイスからの信号を受信することができます。フロッピーディスクコントローラ(FDC)のようなデバイスが注意を必要とするとき、それは割り当てられているIRQを発射するようにPICに指示します。ここから、PICはプロセッサに信号を送り、呼び出すべき割り込み番号を伝えます。プロセッサは次にIDTにオフセットし、リング0で割り込みハンドラを実行します。 すべての割り込みハンドラを定義したので、これで制御が可能になりました。

これの最も良いところは、PICのおかげですべて自動で行われることです。デバイスがPICに信号を送るたびに、私たちの割り込みハンドラが自動的に実行されます。プロセッサはリング0へのタスクスイッチも実行するので、我々は常にカーネルランドで要求を処理することになる。クールでしょう?

PIC自体は複雑なマイクロコントローラです。このチュートリアルを最大限に活用するために、読者の皆さんには上記のPICチュートリアルを読んでいただくことをお勧めします。

それでは、インターフェイスを見ていきましょう。このコードはすべて、このチュートリアルの最後にあるデモの中で見ることができます。

操作コマンド

オペレーションコマンドは、ビットパターンで構成される特別なコマンドです。このビットパターンを設定して、マイコンにコマンドを記述する必要がある。オペレーションコマンドには、基本的に、ICW(Initialization Command Words)とOCW(Operation Command Words)の2種類が存在します。

ICWは操作コマンドで、デバイスの初期化中にのみ使用する必要があります。OCWは、デバイスが初期化された後に、デバイスを制御するために使用されます。

pic.h:インターフェース

このファイルは、それ以外のミニドライバ全体のインターフェイスを提供します。これは、PICを制御・管理するためのインターフェイスです。私は「ミニドライバ」を、独立したソフトウェアではなく、ソフトウェアの一部に組み込まれたドライバと定義しています。

pic.h:デバイスの接続

PICのチュートリアルでは、ハードウェアの割り込みを深く掘り下げて見てきました。ハードウェアデバイスがシステムソフトウェアやエグゼクティブの注意を必要とするときに、どのようにPICに信号を送るかを見てきました。これが機能するために、各デバイスはPIC上の割り込み要求(IR)ラインに間接的に接続されています。このラインは、デバイスが使用する割り込み要求(IRQ)だけでなく、そのプリリティレベル(IRQ番号が低いほど、プリリティが高い)も表しています。

個々のデバイスとそのIRQを扱うときに役立つように、それらが使用するIRQを抽象化したいと思います。 これは移植性を高めるだけでなく、それらが美しい定数の後ろにあるため可読性を高めるのにも役に立ちます。覚えておいてください。マジックナンバーは悪です!

//! The following devices use PIC 1 to generate interrupts #define I86_PIC_IRQ_TIMER 0 #define I86_PIC_IRQ_KEYBOARD 1 #define I86_PIC_IRQ_SERIAL2 3 #define I86_PIC_IRQ_SERIAL1 4 #define I86_PIC_IRQ_PARALLEL2 5 #define I86_PIC_IRQ_DISKETTE 6 #define I86_PIC_IRQ_PARALLEL1 7 //! The following devices use PIC 2 to generate interrupts #define I86_PIC_IRQ_CMOSTIMER 0 #define I86_PIC_IRQ_CGARETRACE 1 #define I86_PIC_IRQ_AUXILIARY 4 #define I86_PIC_IRQ_FPU 5 #define I86_PIC_IRQ_HDC 6
上記の定数は、使用するすべてのデバイス(とIRQライン/番号)をリストアップしています。PICには8本のIR線があり、それゆえPICごとに8つのIRQしかありません。PICはセカンダリPICとカスケード接続できることを忘れないでください(最大8個のPICを互いにカスケード接続できます)。一般的なx86アーキテクチャでは、2個(1個のプライマリと1個のセカンダリ)だけです。

今、私たちにとって最も重要な2つのデバイスは、タイマー(I86_PIC_IRQ_TIMER)とキーボード(I86_PIC_IRQ_KEYBOARD)です。このチュートリアルでは、I86_PIC_IRQ_TIMERを使用するので、すべてがどのように連動しているかがわかると思います。

pic:8259コマンド

PICのセットアップは非常に複雑です。これは、初期化と操作に使用されるさまざまな状態を含むビットパターンである、一連のコマンドワードを通して行われます。まず、PICを制御するために使用されるオペレーションコマンドワード(OCW)を見ていきます。初期化コマンドについては、もう少し後で見ていきます。

pic:オペレーション・コマンド・ワード1

これは、IMR(Interrupt Mask Register)の値を表しています。特別なフォーマットはないので、インプリメンテーションファイルで直接扱って、ハードウェア割り込みを有効・無効にします。 サイズは1バイトです。正しいビットを設定することで、割り込み要求ラインを有効/無効にします(「マスク/アンマスク」)。1つのPICに8つのIRQしかないことを思い出してください。つまり、IMR のビット 0 は IRQ 0、ビット 1 は IRQ 1、ビット 2 は IRQ 2、...といった具合です。

この後、インタラプトマスクレジスタについて説明します。

pic:オペレーション・コマンド・ワード(OCW)2

これがPICを制御するための主要な制御語です。では、見てみましょう。

Operation Command Word (OCW) 2
Bit Number Value Description
0-2 L0/L1/L2 Interrupt level upon which the controller must react
3-4 0 Reserved, must be 0
5 EOI End of Interrupt (EOI) request
6 SL Selection
7 R Rotation option

それでは!

OCW 2のフォーマットは非常に簡単です。最初の3ビットは現在の割り込みレベルです。ビット3-4は予約です(0でなければなりません)。ビット5はEOI(End of Interrupt)を表します。Bit 6はSelectionビットです。ビット 7 はローテーションコマンドを提供します。

各コマンドは個別のビットで選択されるため、これらのコマンドをビットごとに ORして OCW 2 を生成することができます。

//! Command Word 2 bit masks. Use when sending commands #define I86_PIC_OCW2_MASK_L1 1 //00000001 //Level 1 interrupt level #define I86_PIC_OCW2_MASK_L2 2 //00000010 //Level 2 interrupt level #define I86_PIC_OCW2_MASK_L3 4 //00000100 //Level 3 interrupt level #define I86_PIC_OCW2_MASK_EOI 0x20 //00100000 //End of Interrupt command #define I86_PIC_OCW2_MASK_SL 0x40 //01000000 //Select command #define I86_PIC_OCW2_MASK_ROTATE 0x80 //10000000 //Rotation command
これです。これは、私たちにとって重要なコマンドワードです。すべての割り込みハンドラからこのコマンドワードを送信することが要求されます。

PICは実行されると割り込みをマスクオフするのを覚えていますか?これは、プロセッサがPICを確認するまで、そのIRライン上の割り込み要求がそれ以上実行できないことを意味します。これは、End of Interruptコマンドワードを正しいPICに送信することによって行われます。これは、コマンドワードのEOIビットをマスクオフすることで行えます。これがI86_PIC_OCW2_MASK_EOIが使用される理由です。

少し後に、インターフェイスにはi86_pic_send_commandルーチンがあり、これはPICにコマンドを送信するために使用されることがわかります。このルーチンを使ってEOIコマンドを送信する例を見て、どのように動作するのかを確認しましょう。

i86_pic_send_command (I86_PIC_OCW2_MASK_EOI, picNumber);
上記のコードは、EOIコマンドをpicNumberにあるPICに送ります、クール?

OCW 2はこれで終わりです。次のものに移ります。

pic:オペレーションコマンドワード 3

*このセクションに追加する予定です。
//! Command Word 3 bit masks. Use when sending commands #define I86_PIC_OCW3_MASK_RIS 1 //00000001 #define I86_PIC_OCW3_MASK_RIR 2 //00000010 #define I86_PIC_OCW3_MASK_MODE 4 //00000100 #define I86_PIC_OCW3_MASK_SMM 0x20 //00100000 #define I86_PIC_OCW3_MASK_ESMM 0x40 //01000000 #define I86_PIC_OCW3_MASK_D7 0x80 //10000000

pic.cpp:インプリメンテーション

さて...ここまでは簡単だったでしょう?おそらく、"チャレンジはどこだ!?"と思っていることでしょう。じゃあ、いいや。

pic.cppは、PICインターフェイスの実装を提供します。最初に見ておかなければならないのは、レジスタです。

pic.cppを参照してください。レジスタ定数

ここでは、PICのポート位置を抽象化するための定数を定義しています。同じポートアドレスを共有していても、すべてのレジスタ名に対して定数を定義していることに注意してください。 理由は完全性のためです。同じポート位置を共有していても、異なるレジスタであることに変わりはないのです。
//! PIC 1 register port addresses #define I86_PIC1_REG_COMMAND 0x20 // command register #define I86_PIC1_REG_STATUS 0x20 // status register #define I86_PIC1_REG_DATA 0x21 // data register #define I86_PIC1_REG_IMR 0x21 // interrupt mask register (imr) //! PIC 2 register port addresses #define I86_PIC2_REG_COMMAND 0xA0 // ^ see above register names #define I86_PIC2_REG_STATUS 0xA0 #define I86_PIC2_REG_DATA 0xA1 #define I86_PIC2_REG_IMR 0xA1
難しいことではありません。データレジスタから書き込む場合、割り込みマスクレジスタ(IMR)にアクセスし、割り込み要求を手動でマスクオフまたはマスク解除するために使用します。このようにして、割り込み要求を有効にしたり、無効にしたりすることができます。

アクセスするレジスタは、書き込み操作か読み出し操作かによって異なります。ポート 0x20 に書き込む場合は、コマンド・レジスタにアクセスすることになります。ポート 0x20 に書き込む場合は、コマンド・レジスタにアクセスし、ポート 0x20 から読み出す場合は、ステータ ス・レジスタにアクセスします。

最後に、これは実装の詳細なので、インタフェースではなく、実装(pic.cpp)の一部となります。

次に、初期化時に使用される定数について見てみましょう。

pic.cpp。初期化制御語1

これはPICを初期化するときに使用されるプライマリコントロールワードです。 これはプライマリPICコマンドレジスタに入れる必要がある7ビットの値です。このような形式になっています。

Initialization Control Word (ICW) 1
Bit Number Value Description
0 IC4 セット(1)されると、PICは初期化中にIC4を受信することを期待します
1 SNGL セット(1)されると、システム内でPICは1つだけです。クリアされた場合、PICはスレーブPICとカスケード接続され、ICW3はコントローラに送信される必要があります。
2 ADI セット(1)されると、CALLアドレス間隔は4、さもなくば8。これは通常、x86では無視され、デフォルトは0である。
3 LTIM 設定されている場合(1),レベルトリガモードで動作する。設定しない場合(0),エッジトリガモードで動作する。
4 1 初期化ビット。PICを初期化する場合、1 を設定する。
5 0 MCS-80/85: 割り込みベクターアドレス。0でなければならない
6 0 MCS-80/85: 割り込みベクターアドレス。0でなければならない
7 0 MCS-80/85: 割り込みベクターアドレス。0でなければならない

このように、いろいろなことが起こっています。これらのいくつかは、以前にも見たことがあります。これらのビットのほとんどはx86プラットフォームでは使用されないので、これはそれほど難しいことではありません。

各コマンドワードには、2種類の定数があります。1つ目のタイプはビットマスクで、データが表すビットをマスクするために使用されます。もう1つはコマンド・コントロール・ビットで、マスクと組み合わせて正しい値に設定するために使用されます。

もう少し詳しく見てみましょう。以下はICWの1ビットマスクです。最後の3ビットはx86アーキテクチャでは常に0なので、何も定義していません。

//! Initialization Control Word 1 bit masks #define I86_PIC_ICW1_MASK_IC4 0x1 //00000001 // Expect ICW 4 bit #define I86_PIC_ICW1_MASK_SNGL 0x2 //00000010 // Single or Cascaded #define I86_PIC_ICW1_MASK_ADI 0x4 //00000100 // Call Address Interval #define I86_PIC_ICW1_MASK_LTIM 0x8 //00001000 // Operation Mode #define I86_PIC_ICW1_MASK_INIT 0x10 //00010000 // Initialization Command

さて、上記のビットマスクを使えば、ICW1のビットを設定するのは簡単ですが、その意味をどうやって知るのでしょうか? つまり、設定したいビットをマスクしたとき、設定する値の意味をどうやって知るのでしょうか?そこで登場するのがコマンド・コントロール・ビットです。

コマンド制御ビットには、上記のマスクオフされたビットに設定するための定数値が格納されています。これにより、可読性と拡張性が大幅に向上します。

ICW1のコマンド・コントロール・ビットを以下に示します。見てみましょう。

#define I86_PIC_ICW1_IC4_EXPECT 1 //1 //Use when setting I86_PIC_ICW1_MASK_IC4 #define I86_PIC_ICW1_IC4_NO 0 //0 #define I86_PIC_ICW1_SNGL_YES 2 //10 //Use when setting I86_PIC_ICW1_MASK_SNGL #define I86_PIC_ICW1_SNGL_NO 0 //00 #define I86_PIC_ICW1_ADI_CALLINTERVAL4 4 //100 //Use when setting I86_PIC_ICW1_MASK_ADI #define I86_PIC_ICW1_ADI_CALLINTERVAL8 0 //000 #define I86_PIC_ICW1_LTIM_LEVELTRIGGERED 8 //1000 //Use when setting I86_PIC_ICW1_MASK_LTIM #define I86_PIC_ICW1_LTIM_EDGETRIGGERED 0 //0000 #define I86_PIC_ICW1_INIT_YES 0x10 //10000 //Use when setting I86_PIC_ICW1_MASK_INIT #define I86_PIC_ICW1_INIT_NO 0 //00000
難しくはありません。この命名規則が使われているので、何をどこで使えばいいのかが簡単にわかります。例えば、I86_PIC_ICW1_SNGL_YESは I86_PIC_ICW1_MASK_SNGLでI86_PIC_ICW1_LTIM_EDGETRIGGEREDは I86_PIC_ICW1_MASK_LTIMで使用されるのだそうです。

以下は、それらの連携例です。PICを初期化する際、初期化を有効にし、ICW4を送信する必要があります。 これを行うには、単純に以下のようにICW1を設定します。

uint8_t icw=0; icw = (icw & ~I86_PIC_ICW1_MASK_INIT) | I86_PIC_ICW1_INIT_YES; icw = (icw & ~I86_PIC_ICW1_MASK_IC4) | I86_PIC_ICW1_IC4_EXPECT;
それだけですか!そうです。すべてがどのように機能し、組み合わされているかに注目してください。これは、特定のビット(または一連のビット)を既知の値に設定するために、実装全体で使用されています。ここで一番良いのは、上記のコードを見るだけで、それが何をやっているのかがわかることです。(初期化を開始し、ICW 4を期待する)。この方法は、このシリーズを通して、ビットの設定やマスクオフの際に必要なときに使用する予定です。

初期化制御ワード2

この制御ワードは、PICが使用するIVTのベースアドレスをマップするために使用されます。

初期化制御ワード(ICW) 2
ビット番号 説明
0-2 A8/A9/A10 MCS-80/85モード時のIVT用アドレスビットA8~A10。
3-7 A11(T3)/A12(T4)/A13(T5)/A14(T6)/A15(T7) MCS-80/85 モード時の IVT 用アドレスビット A11-A15 です。80x86 モードでは、割り込みベクタのアドレスを指定します。x86モードでは0に設定してもよい。

初期化時に、ICW2をPICに送って、使用するIRQのベースアドレスを伝える必要があります。もしICW1がPICに送られたなら(初期化ビットが設定された状態で)、次にICW2を送らなければなりません。そうしないと、未定義の結果になることがあります。ほとんどの場合、不正な割り込みハンドラが実行されます。

このコマンドは複雑な形式ではないので、pic.cpp内部で直接処理され、定数はありません。

初期化制御ワード 3

このコマンドワードは、PICコントローラがどのようにカスケード接続されるかを知らせるために使用されます。複数のPICをカスケード接続するには、PICのIRラインを互いに接続する必要があります。それがどのラインなのかを知らせるために、このコマンドワードを使用します。

初期化制御ワード(ICW) 3
Bit Number Value Description
0-7 S0-S7 スレーブPICに接続されている割り込み要求(IRQ)を指定します。

このコマンドは複雑な形式ではないので、pic.cpp内部で直接処理され、定数はありません。

初期化制御ワード4

イエーイ!これは最終的な初期化制御ワードです。これは、すべてがどのように動作するかを制御します。

初期化制御ワード(ICW) 4
ビット番号 説明
0 uPM セット(1)の場合、80x86モード。MCS-80/86モードであればクリアされる。
1 AEOI 設定されている場合、最後の割り込みアクノリッジパルスで、コントローラは自動的に割り込みの終了(EOI)操作を実行します。
2 M/S BUFが設定されている場合のみ使用します。設定(1)の場合、バッファマスタを選択します。バッファスレーブの場合はクリアされます。
3 BUF 設定された場合,コントローラはバッファードモードで動作します。
4 SFNM Special Fully Nested Mode(スペシャル・フルネスト・モード)。カスケード接続された多数のコントローラーを持つシステムで使用されます。
5-7 0 予約済み、0でなければならない

これはかなり複雑なコマンドワードですが、それほど悪くはありません。それでは、定義されたビット・マスクを見てみましょう。 上図のようなフォーマットになっていることに注目してください。

//! Initialization Control Word 4 bit masks #define I86_PIC_ICW4_MASK_UPM 0x1 //00000001 // Mode #define I86_PIC_ICW4_MASK_AEOI 0x2 //00000010 // Automatic EOI #define I86_PIC_ICW4_MASK_MS 0x4 //00000100 // Selects buffer type #define I86_PIC_ICW4_MASK_BUF 0x8 //00001000 // Buffered mode #define I86_PIC_ICW4_MASK_SFNM 0x10 //00010000 // Special fully-nested mode
ICW 1と同様に、プロパティを設定するためにビットマスクと組み合わせて使用される制御ビットのセットがあります。これが...
#define I86_PIC_ICW4_UPM_86MODE 1 //1 //Use when setting I86_PIC_ICW4_MASK_UPM #define I86_PIC_ICW4_UPM_MCSMODE 0 //0 #define I86_PIC_ICW4_AEOI_AUTOEOI 2 //10 //Use when setting I86_PIC_ICW4_MASK_AEOI #define I86_PIC_ICW4_AEOI_NOAUTOEOI 0 //00 #define I86_PIC_ICW4_MS_BUFFERMASTER 4 //100 //Use when setting I86_PIC_ICW4_MASK_MS #define I86_PIC_ICW4_MS_BUFFERSLAVE 0 //000 #define I86_PIC_ICW4_BUF_MODEYES 8 //1000 //Use when setting I86_PIC_ICW4_MASK_BUF #define I86_PIC_ICW4_BUF_MODENO 0 //0000 #define I86_PIC_ICW4_SFNM_NESTEDMODE 0x10 //10000 //Use when setting I86_PIC_ICW4_MASK_SFNM #define I86_PIC_ICW4_SFNM_NOTNESTED 0 //00000
これはシンプルなスナフキンですね。^_^ 上記のコントロールビットをビットマスクと組み合わせて使うことで、コントロールワードを構築することができます。命名規則が使われているので、どのようなビットマスクと一緒に使われているのかが簡単にわかります。

インプリメンテーションで使用する定数はこれで終わりとします。では、関数に取りかかりましょう。

i86_pic_send_command ():PICにコマンドを送信します。

このルーチンは、PICのコマンドレジスタにコマンドバイトを送信します。picNumは、アクセスするPICを表すゼロベースのインデックスです。x86では、これは0か1のどちらかであるべきです。正しいコマンド・レジスタを得るために、どのPICで作業しているかをテストしていることに注意してください。

これはインターフェイスの一部ですが、インターフェイスの外ではそれほど使用されるべきものではありません。これは、必要であれば、手動でPICを送信し、制御できるようにメソッドを提供します。これは、EOIコマンドを送信するための割り込みハンドラによって必要とされるでしょう。

inline void i86_pic_send_command (uint8_t cmd, uint8_t picNum) { if (picNum > 1) return; uint8_t reg = (picNum==1) ? I86_PIC2_REG_COMMAND : I86_PIC1_REG_COMMAND; outportb (reg, cmd); }

i86_pic_send_data()とi86_pic_read_data()。PICにデータバイトを送ったり、PICからデータバイトを返したりします。

これらのルーチンは上記のルーチンと非常に似ていますが、picNumのPICに応じてPICのデータレジスタに書き込みまたは読み出しを行います。 これらのルーチンの両方がインラインであることに注意してください。これらのルーチンは小さいので、関数コールを取り除きたいと思います。
inline void i86_pic_send_data (uint8_t data, uint8_t picNum) { if (picNum > 1) return; uint8_t reg = (picNum==1) ? I86_PIC2_REG_DATA : I86_PIC1_REG_DATA; outportb (reg, data); } inline uint8_t i86_pic_read_data (uint8_t picNum) { if (picNum > 1) return 0; uint8_t reg = (picNum==1) ? I86_PIC2_REG_DATA : I86_PIC1_REG_DATA; return inportb (reg); }

i86_pic_initialize()。PICを初期化する

これはPICインターフェイスのための最終ルーチンです。これは、上記のすべてのルーチンと、初期化制御語用に定義された定数を使って、動作のために両方のPICを初期化します。

このルーチンはあまり複雑ではありません。というか、見た目ほど複雑ではありません ;)このルーチンが行うのは、PICに初期化コマンドを送るだけです。これは、コマンドワードのI86_PIC_ICW1_INIT_YESビットを設定することで行います。また、I86_PIC_ICW1_IC4_EXPECTビットを設定し、コントローラがICW 4を送信することを保証しています。 定数が可読性を向上させていることにお気づきでしょうか。

ICWは...そう...icwに格納されています。i86_pic_send_command()ルーチンを使って、両方のPICにコマンドを送ります。

ICW 1が送信された後、ICW 2を送信して初期化を開始します。ICW 2にはベース割り込み番号が含まれており、base0と base1パラメータに渡されます。

ICW 3はPICコントローラのマスターとセカンダリ間の接続に使用されます。

最後にICW 4ですが、I86_PIC_ICW4_UPM_86MODEビットをセットして、x86モードをセットアップしています。このルーチンをPICチュートリアルにある例と比較してみてください、驚くことでしょう...その類似性にとても驚かされることでしょう!

//! Initialize pic void i86_pic_initialize (uint8_t base0, uint8_t base1) { uint8_t icw = 0; //! Begin initialization of PIC icw = (icw & ~I86_PIC_ICW1_MASK_INIT) | I86_PIC_ICW1_INIT_YES; icw = (icw & ~I86_PIC_ICW1_MASK_IC4) | I86_PIC_ICW1_IC4_EXPECT; i86_pic_send_command (icw, 0); i86_pic_send_command (icw, 1); //! Send initialization control word 2. This is the base addresses of the irq's i86_pic_send_data (base0, 0); i86_pic_send_data (base1, 1); //! Send initialization control word 3. This is the connection between master and slave. //! ICW3 for master PIC is the IR that connects to secondary pic in binary format //! ICW3 for secondary PIC is the IR that connects to master pic in decimal format i86_pic_send_data (0x04, 0); i86_pic_send_data (0x02, 1); //! Send Initialization control word 4. Enables i86 mode icw = (icw & ~I86_PIC_ICW4_MASK_UPM) | I86_PIC_ICW4_UPM_86MODE; i86_pic_send_data (icw, 0); i86_pic_send_data (icw, 1); }
*ふぅー*、これでPICの大仕事はすべて終わったようです。心配しないでください、PICほど複雑ではありません。 見てみましょう...

プログラマブルインターバルタイマ

よし...PICの準備ができたので、ハードウェア割り込みを有効にすることができますね?ええ、ちょっとだけ。今のところすべて順調ですが、PIT用の割り込みハンドラはまだインストールされていません。では、次のタイマティックで何が起こるのでしょうか?

プログラマブルインターバルタイマ(PIT)は、プログラムされたカウントに達すると割り込みを発生させるカウンタです。8253および8254マイコンは、i86アーキテクチャで使用可能なPITで、i86互換システムのタイマとして使用されます。

x86アーキテクチャでは、PITはシステムタイマーとして動作し、PICのIR0ラインに接続されています。 これにより、PITはタイマーを刻むごとにIRQ 0を発生させることができます。このため、このマイクロコントローラを使用する前に、再プログラムする必要があります。

PITはプログラミングが複雑なマイコンです。このため、PITについては、別のチュートリアルを作成しました。 それでも、すべてを詳細に説明するつもりですが、ここでは、PITのすべてをカバーすることはできません。

PITについて学ぶには、以下のチュートリアルをご覧ください(参考)。

8253プログラマブルインターバルタイマチュートリアル

pit.h:インターフェース

PITの良いところは、プログラミングがそれほど複雑でないことです。それほど多くのコマンドを含んでいるわけでもなく、かといってそれほど多くのコマンドを必要とするわけでもない。PITは小さいけれども、ハードウェアのタイミングやリクエストに使われるパワフルなチップです。

操作コマンドワード

PITは、カウンタの初期化に使うオペレーションコマンドワード(OCW)を1つだけ含んでいます。 これは、カウンタのカウントモード、オペレーションモードを設定し、初期カウント値を設定するためのものです。

コマンドワードは、少し複雑です。以下は、コマンドワードの完全版です。

  • ビット0:(BCP)バイナリカウンタ
    • 0:バイナリ
    • 1:バイナリコード付き10進数(BCD)
  • ビット1-3:(M0, M1, M2)動作モード。それぞれの説明は、上記のセクションを参照してください。
    • 000:モード0:割込みまたは端子カウント
    • 001:モード 1: プログラマブルワンショット
    • 010:モード 2: レートジェネレータ
    • 011:モード3:方形波発生器
    • 100:Mode 4: ソフトウェアトリガー ストロボ
    • 101:Mode 5: ハードウェアトリガー ストロボ
    • 110:未定義、使用しないでください。
    • 111:未定義、使用せず
  • 第4-5ビット:(RL0, RL1)リード/ロード・モード。カウンタ・レジスタへのデータの読み出し、または送信を行います
    • 00:カウンタ値はI/Oライト動作時に内部コントロールレジスタにラッチされます。
    • 01:最下位バイト(LSB)のみをリードまたはロードします。
    • 10: 最上位バイト(MSB)のみ読み出し/読み出し
    • 11:LSBを先に読み出し、その後MSBを読み出す。
  • ビット6-7:(SC0-SC1)セレクトカウンタ。それぞれの説明は、上記のセクションを参照してください。
    • 00:カウンタ0
    • 01: カウンタ1
    • 10:カウンタ2
    • 11:不正な値

PICのインターフェースと同様に、コマンドのフォーマットを記述するために、いくつかのビットマスクを設定します。以下は、その内容です...

#define I86_PIT_OCW_MASK_BINCOUNT 1 //00000001 #define I86_PIT_OCW_MASK_MODE 0xE //00001110 #define I86_PIT_OCW_MASK_RL 0x30 //00110000 #define I86_PIT_OCW_MASK_COUNTER 0xC0 //11000000
なるほど、PICで設定したICWやOCWより小さいが、実はこちらの方が複雑である。PICで使うコマンドは1ビットと単純ですが、この演算コマンドワードで使うコマンドはそうではありません。

これはコマンド制御ビットが輝くところです。これらは、上記の異なるビットマスクのための異なる設定とビットの組み合わせを定義するのに役立ちます。以下はその例です。

#define I86_PIT_OCW_BINCOUNT_BINARY 0 //0 //! Use when setting I86_PIT_OCW_MASK_BINCOUNT #define I86_PIT_OCW_BINCOUNT_BCD 1 //1 #define I86_PIT_OCW_MODE_TERMINALCOUNT 0 //0000 //! Use when setting I86_PIT_OCW_MASK_MODE #define I86_PIT_OCW_MODE_ONESHOT 0x2 //0010 #define I86_PIT_OCW_MODE_RATEGEN 0x4 //0100 #define I86_PIT_OCW_MODE_SQUAREWAVEGEN 0x6 //0110 #define I86_PIT_OCW_MODE_SOFTWARETRIG 0x8 //1000 #define I86_PIT_OCW_MODE_HARDWARETRIG 0xA //1010 #define I86_PIT_OCW_RL_LATCH 0 //000000 //! Use when setting I86_PIT_OCW_MASK_RL #define I86_PIT_OCW_RL_LSBONLY 0x10 //010000 #define I86_PIT_OCW_RL_MSBONLY 0x20 //100000 #define I86_PIT_OCW_RL_DATA 0x30 //110000 #define I86_PIT_OCW_COUNTER_0 0 //00000000 //! Use when setting I86_PIT_OCW_MASK_COUNTER #define I86_PIT_OCW_COUNTER_1 0x40 //01000000 #define I86_PIT_OCW_COUNTER_2 0x80 //10000000
例を見てみましょう。例えば、カウンタ0を矩形波発生器として、バイナリカウント方式で初期化したいとします。このようにします。
uint8_t ocw=0; ocw = (ocw & ~I86_PIT_OCW_MASK_MODE) | I86_PIT_OCW_MODE_SQUAREWAVEGEN; ocw = (ocw & ~I86_PIT_OCW_MASK_BINCOUNT) | I86_PIT_OCW_BINCOUNT_BINARY; ocw = (ocw & ~I86_PIT_OCW_MASK_COUNTER) | I86_PIT_OCW_COUNTER_0;
簡単すぎると思いますが、いかがでしょうか?ocwには、PICに送信できる演算コマンドワードが入ります。これらの定数を使用することで、読みやすさを向上させるだけでなく、エラーの可能性を減少させることができることに注意してください。

私はそれがpit.hにあるすべてであると思います。次は、pit.cppに飛び込んでみましょうか?ウィー......!

pit.cpp。インプリメンテーション

これは、PITミニドライバの大部分を含んでいます。これは、インターフェースと実装の両方で使用される各ルーチンの実装を含んでいます。

pit.cpp。レジスタ

ここでは、PITのポートの位置を抽象化するための定数を定義しています。
#define I86_PIT_REG_COUNTER0 0x40 #define I86_PIT_REG_COUNTER1 0x41 #define I86_PIT_REG_COUNTER2 0x42 #define I86_PIT_REG_COMMAND 0x43 //! Global Tick count uint32_t _pit_ticks=0;
悪くないですね。I86_PIT_REG_COUNTER0、I86_PIT_REG_COUNTER1、 I86_PIT_REG_COUNTER2は、各カウンタのデータ・レジスタです。PIT には 3 つの内部カウンタがあることを思い出してください。I86_PIT_REG_COMMANDはコマンド・レジスタで、PIT を制御・操作するためにコマンド・レジスタにコマンドを書き込む必要があります。

また、_pit_ticksに注目してください。これは非常に特別で重要なグローバルです。

PITカウンタ0がPICのIR0ラインに接続されているのを覚えていますか?つまり、カウンタ0が発火すると、割り込み要求(IRQ)0が発生します。この要求を処理するために、割り込みハンドラを作成し、インストールする必要があります。

割り込みハンドラが行うべきことは、システムのグローバルティックカウントを更新することです。そのために_pit_ticksがあります。

i86_pit_irq()。PITカウンタ0割り込みハンドラ

IRQ 0の要求を処理する割り込みハンドラです。カウンタ0が発火するたびに、この割り込みハンドラを呼び出します。

このハンドラが行うことは、割り込みが発生するたびにグローバルティックカウントをインクリメントすることだけです。割り込みハンドラの一般的な書式に注意してください。

intstart()は、ハードウェア割り込みを無効にしてスタックフレームを保存し、タスクのスタックを空けずに復帰するために使用するマクロです。この目的は、単純に現在のスタックを変更されないように保護し、そのスタックを維持したままタスクに戻るためです。これらのマクロは、カーネルやデバイスドライバの割り込みハンドラで使用できるようにasm/system.hで定義されています。

割り込みは、特定のコンパイラでのみ使用される特別な定数です。MSVC++の場合は、__declspec(裸)として定義されています。これは、コンパイラが追加したコードを気にする必要がないようにするためです。一部のコンパイラはこのキーワードを直接サポートしています(最も顕著なのは16ビットコンパイラです)。他は(MSVC++のように)サポートしていないので、定義する必要があります。

interruptdone()は、ハードウェア抽象化レイヤで定義された特別なルーチンです。これは、PICに割り込み終了コマンドを送信する責任があります。

これは、私たちのすべての割り込みハンドラが使用する一般的な形式です。

void interrupt _cdecl i86_pit_irq () { //! macro to hide interrupt start code intstart (); //! increment tick count _pit_ticks++; //! tell hal we are done interruptdone(0); //! macro used with intstart to return from interrupt handler intret (); }

i86_pit_send_command ():PIT にコマンドを送信

このルーチンは、PITにコマンドを送信するための非常に重要なルーチンです。これは、送信先のコマンドポートを隠すことができ、ポート名を変更する必要がある場合に便利です。コマンドは、オペレーションコマンドワード(OCW)の形式です。

//! send command to pic void i86_pit_send_command (uint8_t cmd) { outportb (I86_PIT_REG_COMMAND, cmd); }
例えば、上記のビットマスクとコマンド制御ビットを使ってOCWを構築することができます。そして、i86_pit_send_command()を使って、OCWをPITに送ります。

i86_pit_send_data()とi86_pit_read_data()。カウンタにデータを送る、カウンタからデータを読み取る

これらのルーチンは、カウンタの読み書き時に使用されるポート名の抽象化を支援します。 これらは、現在のカウント値を設定したり取得したりするために使用されます。これらのルーチンが行うのは、カウンタに渡されたカウンタをテストして、正しいポートを取得することを確認することだけです。そして、そのポートを介して、単純な読み取りまたは書き込み操作を行うだけです。
//! send data to a counter void i86_pit_send_data (uint16_t data, uint8_t counter) { uint8_t port= (counter==I86_PIT_OCW_COUNTER_0) ? I86_PIT_REG_COUNTER0 : ((counter==I86_PIT_OCW_COUNTER_1) ? I86_PIT_REG_COUNTER1 : I86_PIT_REG_COUNTER2); outportb (port, data); } //! read data from counter uint8_t i86_pit_read_data (uint16_t counter) { uint8_t port= (counter==I86_PIT_OCW_COUNTER_0) ? I86_PIT_REG_COUNTER0 : ((counter==I86_PIT_OCW_COUNTER_1) ? I86_PIT_REG_COUNTER1 : I86_PIT_REG_COUNTER2); return inportb (port); }

i86_pit_initialize ():PITを初期化する

さて、PITの初期化について説明しましょう。はい!初期化する必要がないので、特に話すことはありません。irqは使用する割り込み番号、irCodeSegは グローバルディスクリプターテーブル(GDT)のコードスレターオフセットです。

i86_install_ir()ルーチンを使って、割り込みハンドラ(i86_pit_irq)を割り込み記述子表にインストールします。irqは、プライマリ PIC が IRQ 0 にマッピングされたことを確認するために使用したのと同じベース IRQ 番号であるべきです。

//! initialize minidriver void i86_pit_initialize (uint8_t irq, uint8_t irCodeSeg) { //! Install our interrupt handler i86_install_ir (irq, I86_IDT_DESC_PRESENT | I86_IDT_DESC_BIT32, irCodeSeg, i86_pit_irq); }

i86_pit_start_counter()。内部カウンタをスタートさせる

これは、PITインタフェースの最後のルーチンです。これはカウンタを立ち上げます。modeは、カウンタに使わせたい動作モード(I86_PIT_OCW_MODE_SQUAREWAVEGENなど)、freqは、カウンタに使わせたい周波数レートを含んでいます。

本ルーチンは、ルーチンに渡されたパラメータに基づいて、操作コマンド・ワードを構築する。

void i86_pit_start_counter (uint32_t freq, uint8_t counter, uint8_t mode) { if (freq==0) return; uint16_t divisor = 1193180 / freq; //! send operational command uint8_t ocw=0; ocw = (ocw & ~I86_PIT_OCW_MASK_MODE) | mode; ocw = (ocw & ~I86_PIT_OCW_MASK_RL) | I86_PIT_OCW_RL_DATA; ocw = (ocw & ~I86_PIT_OCW_MASK_COUNTER) | counter; i86_pit_send_command (ocw); //! set frequency rate i86_pit_send_data (divisor & 0xff, 0); i86_pit_send_data ((divisor >> 8) & 0xff, 0); //! reset tick count _pit_ticks=0; }

まとめ

これで基本的なことはすべて完了です。プロセッサのモードやアーキテクチャから、プロセッサテーブル、割り込み、割り込み管理など、このシリーズでは多くのことをカバーしました。これはカーネルの始まりであり、カーネルがそこから構築される場所です。

このチュートリアルでは、PIC、PIT、例外、およびハードウェア割り込み管理のサポートを追加しました。これは重要なステップで、多くの重要なデバイスがハードウェア割り込みを使用するからです。また、これはハードウェア割り込みを再有効化する手段を提供します(保護モードに切り替える前に、ハードウェア割り込みを無効にする必要があったことを思い出してください)。

次のチュートリアルでは、カーネル自体に戻ります。その時は、コンピュータシステムの最も基本的な側面の1つについて話をします。ページングと 低レベルのメモリ管理です。これはまた、私たち自身のシステムAPIの基礎となるものです。