keyboard, UART, VGAの初期化(xv6を例に)

はじめに

本記事では、xv6においてUART, keyboardがboot時にどの様に初期化されているのかを確認する。ソフトウェアエンジニアにとって一番たいへんなところがマジックナンバー(0x3f8, 115200, 9600など)の意味の理解だと思うので、この数字の出自をなるべく詳しく追っていく。

diskも調べたかったが記事が長くなりすぎたのもあり、別の記事で調べたい。本記事ではlegacy deviceであるUART, keyboardの初期化について説明しようと思う。xv6はrev-11( https://github.com/mit-pdos/xv6-public/tree/xv6-rev11 )をもとにしてる

Note) 実はVGAについては、xv6内で初期化のコードが1行もないので、概要とxv6における使用例を簡単にまとめている。

準備

PIIX4

本記事では、PIIX4なるものがちょこちょこ出てくるが、これについて簡単にまとめる。PIIX4のdocumentについては、 https://www.intel.com/Assets/PDF/datasheet/290562.pdf にある。

コンピュータの論理的な全体像はだいたい以下のような感じである(物理的にはマザーボードに乗っかっている):

f:id:knknkn11626:20190614134947p:plain
https://4donline.ihs.com/images/VipMasterIC/IC/INTL/INTLS00368/INTLS00368-1.pdf より

MTXCと書かれたものがnorthbridgeと呼ばれるもので、DRAM(メモリ)やL2 cache, CPU間のcontrollerの役割を果たす。

一方、その下のPIIX4はsouthbridgeとよばれ、周辺機器全般の制御1を行っているLSIである(interruptをI/O APICを伝達することもしてる)。PCI-to-ISA bridgeがあるので、PCIだけでなく、ISAも扱える。x86だとin, out命令によって、ISAバスに接続されている(legacy) deviceが操作できる。

PIIX4自体はI/O portの0x0CF8と0x0CFCを介していろいろ設定することができるが、xv6では特にPIIX4の設定をしているわけではないので、PIIX4がhard resetされたときのデフォルト値を使っていると思われる2。次章以降では各種設定値の根拠としてPIIX4を参照することになる。xv6ではIDEPCIバスでなくISAバスで(つまり、in, out命令によって直接)操作する(多分)

Note) I/O APICは外づけとして、PIIX4との間でinterruptの情報のやり取りがされるっぽい。多分内部で、APICREQとAPICACKの信号を送り合ってhandshakeしてる。

keyboard controller(8042)

reference

詳細

keyboard (unit)の方で、キー入力(key down, key up)が発生すると、大体以下のようなことが起こる:

  1. keyboard controller(8042)がscan codeをうけとる(AT互換機なら、大体scan code set 2になってる)
  2. keyboard controller(8042)はscan code set 2のコードからset 1に変換する
  3. input bufferにこのコードが格納され、the Input Buffer Full(IBF) flagがONに
  4. keyboard controllerがIRQ 1をI/O APICに送る(このとき、PIIX4(southbridge)とI/O APIC間でAPICREQ, APICACKのハンドシェイクが起こっているようだ) I/O APICの方ではINTIN1から信号を受け取るみたい
  5. Redirect Table Entryのentryのidx1に書き込まれた情報を元に(指定の)CPUにinterrupt vectorを送る
  6. local APICがinterrupt vectorを受け取り、x86側(hardware側)で色々やって3、interrupt handlerが走る(xv6のkeyboardの場合 vectors.plのvector33関数)

みたいな感じ。今回見ていくconsoleinitはIRQ 1をI/O APICに送るような設定をしている。

keyboard controllerとかの話は、Reference[1-1]を参照してください。scan codeについては、Reference[1-6]を見れば確かにkey upとkey downでscan codeが異なるのだな、ということがわかると思います。

consoleinitより前は、A20 gateのONとともにIRQ1がenableになっている(ただし、cliとなっているので、interruptは無視されている状態)

consoleinit:
  - devsw[CONSOLE].(write|read) = console(write|read): struct devsw devsw[NDEV];
  - ioapicenable(IRQ_KBD, 0): enable IRQ1

main -> consoleinitでやっていることは2行だけ。

1行目はfs.cのreadi, writei関数で使用するためにdevsw[CONSOLE=1]にcallbackをセットしている。これらの関数はdiskからの読み書きで使用する関数なので、keyboardの入力とは直接は関係しない。詳しくは、xv6におけるdiskの記事で詳しく述べたいと思う。

本記事では、2行目についてやや詳しく説明したい。

ioapicenableはirq numberをI/O APICのRedirection Tableに登録する関数だ。

// ioapic.c
struct ioapic {
  uint reg;
  uint pad[3];
  uint data;
};

volatile struct ioapic *ioapic;

static void
ioapicwrite(int reg, uint data)
{
  ioapic->reg = reg;
  ioapic->data = data;
}

void
ioapicenable(int irq, int cpunum)
{
  ioapicwrite(REG_TABLE+2*irq, T_IRQ0 + irq); // #define REG_TABLE  0x10 T_IRQ0: 32
  ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24);
}

これは何をしているかというと、I/O APICのMemory Mapped I/OのRegisterにデータを書き込んでいる。I/O APICはどのCPUにinterruptの処理をさせるか決める役割を担うから、あるIRQ(IRQ number)が来たときのCPUの割り振りをI/O APICのregisterに書き込んで置く必要があるというわけだ。

f:id:knknkn11626:20190602170543p:plain
Reference[1-2]の3.0. REGISTER DESCRIPTIONより

Memory Mapped I/Oはメモリ(DRAM)内にmappedされたI/O portでI/O-mapped I/Oとは異なる。詳しくは、 https://qiita.com/knknkn1162/items/cb06f19e1f999bf098a1#io-port%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6 を参考に。

ioapic変数はglobalとなっており、これはioapic = (volatile struct ioapic*)IOAPIC;に配置されている。これは、I/O APICのmemory mapped registerのbase addressである。

xv6では、#define IOAPIC 0xFEC0_0000となっている4。 デフォルト値が0xFEC0_0000となっているというわけだ。

ioapicwriteの第一引数はI/O Register Selectでindex値(つまり番地)を指定する:

f:id:knknkn11626:20190602172108p:plain
Reference[1-2] 3.0. REGISTER DESCRIPTIONより

第一引数は0x10+2*irq及び、0x10+2*irq+1irq numberを指定している。これは、Redirection TableというやつのEntry #irqの要素に値を書き込むことを行っている。

f:id:knknkn11626:20190602172557p:plainf:id:knknkn11626:20190602172601p:plainf:id:knknkn11626:20190602172608p:plain
Reference[1-2]よりRedirection Table Entryの中身

これを見ると、bit7:0にはirq numberをbit63:56にはdestination fieldを書き込めば良い。これは、cpuの番号を表す。xv6ではmultiprocessorに対応している5が、そのときにcpu番号が割り振られているので、その番号を指定する。

void
ioapicenable(int irq, int cpunum)
{
  ioapicwrite(REG_TABLE+2*irq, T_IRQ0 + irq); // LSB
  ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24); // MSB
}

2行目で24bitの左シフトとなっているのは、bit63:56にcpuの番号を書き込みたいという意図から。

今回の場合はioapicenable(IRQ_KBD, 0)(IRQ_KBD=1)だったから、IRQ 1のinterruptに対してはcpu #0で対応してほしい旨をI/O APICのRedirection Tableに書き込むことで伝えたことになる。

Note) IRQ numberは歴史的な背景から、いくつかの(legacyな)デバイスでは決まっている:

f:id:knknkn11626:20190602174519p:plain
Reference[1-4] Chapter 4: Interrupts and Exceptionsより

I/O APICの前身が8259 PIC(Programmable Interrupt Controller)で、この頃から各IRQとデバイスとの対応が取られていたようだ。Reference[1-5]が分かりやすい資料になっている。

その後

初期化が終わった後は、keyboard入力によって、interrupt(IRQ1)が生じるので、 interrupt vector(32+1)がCPUに伝達され、CPU側で自動的にいろいろ準備をして、vector.plのvector33が呼び出される。(これはIDTに予め指定されている: idtinitにて) 詳細は http://cstmize.hatenablog.jp/entry/2019/03/20/xv6%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AEException%E3%81%A8Interrupt%E3%81%AE%E6%8C%99%E5%8B%95 の"Interrupt発生直後のhardware(x86)の対応"を参照のこと。 vector33関数 -> alltraps -> trap関数が呼ばれ、以下に行き着く:

void trap(struct trapframe *tf) {
  // skip
  case T_IRQ0 + IRQ_KBD:
    // exec consoleintr(kbdgetc);
    kbdintr();
    lapiceoi(); // lapicw(EOI, 0);
    break;
  // skip
}

void
kbdintr(void)
{
  consoleintr(kbdgetc);
}

lapiceoiの実体はlocal APICのEOI register6に値を書き込む

consoleintrは大体以下のような関数である:

// console.c
// Console input and output.
// Input is from the keyboard or serial port.
// Output is written to the screen and serial port.
consoleintr(int (*getc)(void))
{
  while((c = getc()) >= 0){ // get character
    // skip
    uartputc(c);
    cgaputc(c);
    // skip
}

keyboard入力の場合、kbdgetcでcharacterをreadし、uartputcでcharacterをCPU側に送信し7、cgaputcで文字とカーソル部分をconsoleに書き込む。 UARTについては次章で、CGA(VGA)については最後の章で取り扱うことにする。

kbdgetcはざっくりいうと、scan code set 18に沿ったデータがinputでkeyboard controllerから受け取るデータを変換して、ascii文字にする。このとき、key downの他にkey upしたときもkeyboard interruptが走るので、上手いこと処理する。

自分が調べた中では、keyboard controllerの簡易FPGA実装しているReference[1-6]をざっくり見ればニュアンスは分かると思う。

UART(16550A)

reference

本記事ではできるだけ詳細に見ていきたいので、基本的には一次情報であるreference[2-1]を参照する。Reference[2-4], [2-5]はserial portのFPGA実装だが、よりレイヤの低い参考文献として分かりやすいと思う。

概要

UARTとはUniversal Asynchronous Receiver/Transmitterの略でシリアルポート。一番初めは8250 UARTだったが、拡張版の16450や16550がある。16550はバグが有ったので9、修正版の16550Aがチップセット(自分の場合はsouthbridgeのPIIX4)の中に統合されている。xv6では、ascii文字のconsoleへの出力で用いられている(TODO: UARTのreadはなにか接続しない限り使用されない?10)

Note) パラレルポートは主にプリンタで使用されるっぽい。

詳細

void
uartinit(void)
{
  char *p;

  outb(COM1+2, 0);
  outb(COM1+3, 0x80);    // Unlock divisor
  outb(COM1+0, 115200/9600);
  outb(COM1+1, 0);
  outb(COM1+3, 0x03);    // Lock divisor, 8 data bits.
  outb(COM1+4, 0);
  outb(COM1+1, 0x01);    // Enable receive interrupts.

  // If status is 0xFF, no serial port.
  if(inb(COM1+5) == 0xFF)
    return;
  uart = 1;

  // Acknowledge pre-existing interrupt conditions;
  // enable interrupts.
  inb(COM1+2);
  inb(COM1+0);

  ioapicenable(IRQ_COM1, 0); // #define IRQ_COM1         4

  // Announce that we're here.
  for(p="xv6...\n"; *p; p++)
    uartputc(*p);
}

UARTではいろいろ初期化設定した後に、ioapicenableにてI/O APICのredirection table entryのindex 4に、interrupt vector(32+4)とredirection先となるCPU#0を書き込んでいる。最後に設定がうまく言っているか確認している。(qemuで確かめるとconsoleにxv6...という文字が出るはずだ)

その前のoutb, inb(I/O portのread/write)の命令群が一体何をしているのかを詳しく見ていこう:

  1. FIFOの機能を切る(16450 compatible modeに): outb(COM1+2, 0);
  2. DLAB(Divisor Latch Access Bit)を1に: outb(COM1+3, 0x80);
  3. divisor latch byteを0x0C(12)にセット(12分周してる): outb(COM1+0, 115200/9600); outb(COM1+1, 0);
  4. DLAB=0に戻した上で、word length=8bitに設定: outb(COM1+3, 0x03);
  5. do flow control under software control: outb(COM1+4, 0);
  6. Received Data Available Interrupt をONに: outb(COM1+1, 0x01)
  7. serial portがそもそも存在してるかを確認(存在してなければ全てのerror bitが1になるはず): inb(COM1+5) == 0xFF
  8. Transmitter Holding Register Empty InterruptとReceived Data Available Interruptをresetしておく: inb(COM1+2); inb(COM1+0);
  9. I/O APICのredirection table entryの4にinterrupt vector(32+4)とredirection先となるCPU#0を書き込む ioapicenable(IRQ_COM1, 0);

それぞれ詳しく見ていく。なお、uartputcについては、keyboard入力の挙動についての記事でまとめたい。

Note) こういうのを紐解くにはひたすら一次情報や仕様書を追いかけるのがベストだが、不慣れならbochsのI/O port一覧(Reference[2-7])に簡易的なportの用途が書いてあるので、関係ありそうな用語を追っていくと良いと思う。

COM1(0x3f8)

これに関しては、reference[2-6]の下の表によれば、default値が0x00なので、0x03f8が使用される(Serial port AとBは共用っぽい?)。

f:id:knknkn11626:20190614114024p:plain
PIIX4のDEVICE RESOURCE C (FUNCTION 3)より

bochsのI/O port listの03F8-03FFの部分も簡易的に参考になると思う。

レジスタ一覧はreference[2-1]によれば、以下のようになっている:

f:id:knknkn11626:20190614161122p:plainf:id:knknkn11626:20190614161130p:plain
reference[2-1]の8-6 Register Mapsより

0x3f8はbase addressで、2行目に数字が書いてあるのがbase addressからのoffsetである。(DLABについては次節で) 順番にuartinitが何をしているのかを見ていきたい。

16450 compatible modeに変更

1つめのoutb(COM1+2, 0);について。FIFO Control Registerに0を書き込んでいる。

Reference[2-1]の8.6.4 FIFO Control Registerを参照すれば、

  • bit0は Writing a 1 to FCR0 enables both the XMIT and RCVR FIFOs. Resetting FCR0 will clear all bytes in both FIFOs. とあるので、0とすることでFIFOの機能を全てdisableにしている。
  • bit1, 2, 3はbit0が1のときに有効なので、気にしなくて良い
  • bit4,5はreservedで0
  • bit 6,7はtrigger level。これはbit0=1かつbit1=0の場合に有効なので、気にしなくて良い。有効である場合にどのようにbitをsetすればよいのかは8.6.4 FIFO Control Registerに表があるので、参照のこと

DLAB=1に

2つめのoutb(COM1+3, 0x80);について。ここは単純に、bit7(Divisor Latch Access Bit (DLAB))をONにしているだけだ。Reference[2-1]の8.6.2 Line Control Registerによれば、

Bit 7: This bit is the Divisor Latch Access Bit (DLAB). It must be set high (logic 1) to access the Divisor Latches of the Baud Generator during a Read or Write operation. It must be set low (logic 0) to access the Receiver Buffer, the Transmitter Holding Register, or the Interrupt Enable Register.

とある。これはdivisor latchをセットするときに使用するビットだ。次章で詳しく述べたいと思う。

divisor latch byteを0x0Cにセット

3つめのoutb(COM1+0, 115200/9600); outb(COM1+1, 0);について。前節でDLAB=1とすることでdivisor latchをセットするモードに入った。divisor latchは16bitで構成されており、COM1+0=0x3f8は下位8bit, COM1+1=0x3f9は上位8bitとなっている。以下、なぜ0x0C=115200/9600を書き込む必然性があるのかについて説明したい。以下の説明についてはReference[2-8]に全面的に頼っている。

16550Aはcrystal frequency(基準クロック)が1.8432 MHzである。CPUで言うところのclock周波数を想像してもらえれば良い。serial portなのでデータをbit/clkごとに送受信する。最大16bitの送受信を取り扱うので、1.8432[MHz]/12=115200[bit/s]が得られる。

一方で、AT互換機のシリアルポートでは標準規格であるRS-232-Cに準拠しており、最大伝送速度が9600bit/sとなっている。ということで115200[bit/s]から更に分周(divisor)する必要性が生じるため、このようなregister(divisor Latch register)がある。

f:id:knknkn11626:20190614165602p:plain
Reference[2-1], 8-6-1 Registers, Table 4. Baud Rates, Divisors and Crystalsより

表にもある通り、115200/9600=12をdivisorにセットする。表を見ても分かる通り、8bit上限(255)を超える場合があるため、divisor latchは16bitとなっている。

Note) baud rateというのはparity error bitなどを含んだ総ビットを意味する。似たようなものにkbpsがあるが、これはparity bitなどを除いた正味の伝送速度である(少しこの部分自信ない)。

Note) Reference[2-1]によれば、crystal frequencyは他にも3.072 MHz, 18.432 MHzがあるらしいが、 https://en.wikipedia.org/wiki/Crystal_oscillator_frequencies を見た限りでは1.8432[MHz]がUART clock; allows integer division to common baud rates.とあり、こちらが一般的なようだ。

DLAB=0に戻した上で、word length=8bitに設定

4ばんめのoutb(COM1+3, 0x03)の説明。bit7(Divisor Latch Access Bit (DLAB))をOFFにしている。その他にbit0, 1に1をセットしている。これは伝送するbitの単位の指定。この様にセットした場合は8bitとなる。8.6.2 Line Control Registerを参照のこと。

残りはよくわからないが1をセットすると、機能がenableになるたぐいのものだと思う。xv6では全て0にセットされている。

do flow control under software control

5番目のoutb(COM1+4, 0);の設定。Modem Control Registerと呼ばれる。あるイベントごとにsignalを出すか出さないかを選択できるみたいだが、xv6では全てOFFにしている

Received Data Available Interrupt をONに

6番目の設定。outb(COM1+1, 0x01); 8.6.6 Interrupt Enable Registerのbit0をみると、Received Data Available Interruptをenableにするflagだとある。UARTの受信データが届いたときにinterruptを発生させるかのフラグ。

Note) bit1のTransmitter Holding Register Empty InterruptというのはUARTの送信後にoutput bufferが空になったことをinterruptによって伝えるものだ。UARTの「その後」の節で触れるように、consoleへの出力の際にUARTが使用されるので、そのたびにinterruptを生じては困るのでOFFにしている。

serial portの確認

7番目でinb(COM1+5) == 0xFFがtrue出ないかどうかの確認をしている。8.6.3 Line Status Registerを見る限り、送受信時のstatus errorのbitが並んでいる。serial portがそもそも存在していない場合は全てerror bitが立つのでこのような確認をしているものと考えられる。

Transmitter Holding Register Empty InterruptとReceived Data Available Interruptのreset

8番目のinb(COM1+2); inb(COM1+0);の設定。Interrupt Identification RegisterとReceiver Buffer Registerを空読みしている。ここはinterruptの状態をreset(初期化)しているようだ。

f:id:knknkn11626:20190614180535p:plain
Reference[2-1], 8.6.1 Registers, Table 3. UART Reset Configurationより

表のIIRとはInterrupt Identification Registerのこと。RBRとはReceiver Buffer Registerのこと。それぞれ空読みすることによって、Data ReadyとTransmitter Holding Register Error(THRE)をresetしているようだ。

Note) Transmitter Holding Register Empty Interruptはこれ以後ずっとOFFだが、Transmitter Holding Register自体はCOM1+5のbit5で使用できる。

I/O APICの設定

最後はioapicenable(IRQ_COM1, 0);で、IRQ_COM1=4だから、I/O APICのredirection table entryの4にinterrupt vector(32+4)とredirection先となるCPU#0を書き込む。

ここについては、keyboardのI/O APICの設定説明と似たような感じなので、省略。

その後

UARTは2つの場合で用いられる:

  1. consoleへのascii出力 (consoleinitにてuartputc関数として): 送信 -> これについてはkeyboard controllerの章の「その後」の節で少し触れた。kbdgetcで得られたascii文字をconsole上に出力する。

  2. 受信: (TODO: 外部からシリアルポートをつないだとき??) -> こちらは、uartinitにてReceived Data Available Interrupt をONにしたので、interrupt(IRQ4)が発生する。

IRQ4が発生 -> interrupt vector#36がlocal APICに伝わる -> x86のhardware処理 -> vector36関数(vectors.pl) -> alltraps -> trap関数で以下のコードに行き着く:

void trap(struct trapframe *tf) {
  // skip
  case T_IRQ0 + IRQ_COM1:
    uartintr();
    lapiceoi(); // lapicw(EOI, 0);
    break;
  // skip
}

void uartintr(void)
{
  consoleintr(uartgetc);
}

consoleintrはkeyboardのinterruptと同じだった。uartgetcはCOM1+5のbit5(Transmitter Holding Register)が1になる(inb(COM1+5) & 0x20が真になる)のを待ってデータをoutb(COM1+0, c);で取り出す。

VGA

reference

概要

VGAについては、初期化コードがxv6の中にないが、cgaputcを使用するときに、memory mapped I/O(static ushort *crt = (ushort*)P2V(0xb8000);)されているので、この部分を中心に見ていくことにする。P2Vはphysical memory addressからvirtual memory addressへの変換のマクロ。0xb8000はphysical memory addressなので。

以下の説明は全面的にReference[3-2]に依っている。

もともとVGA以前のビデオシステムとしては、MDA, CGA, EGAがある:

f:id:knknkn11626:20190615144622j:plain
Reference[3-2]のVGAの章より、ビデオシステムの変遷

MDA(Monochrome Display Adapter), CGA(Color Graphics Adapter) < EGA(Enhanced Graphic Adapter) < VGA(Video Graphics Array) < SVGA(Super VGA)のような感じで上位互換の関係になっている。AT互換機の殆どがVGAをサポートしているため、xv6でもVGAで(カーソル部分を)描画している。ただし、機能的にはカーソルの描画とカーソル位置の取得更新のみしか扱っておらず、CGA相当の機能しか用いられていない11

ビデオシステムのハードウェア構成としては、ざっくり以下のようになっている:

  • VRAM(Video Ram, ビデオバッファ): 表示データを記憶する。physical memoryの0xA_0000 ~ 0xB_FFFFにマップされている。
  • CRT(Cathode Ray Tube) controller: text mode(CHARACTER based modeとも)の表示の制御を行う(I/O port 0x3D4, 3D5を介して操作12)。MDA, CGAではモトローラのMC6845を使用しているが、VGAでもこの6845と上位互換のCRT controllerを用いている。
  • graphic controller: graphic mode(BIT MAP mode)の表示の制御を行う。 (I/O port 0x3CE, 3CFを介して操作)
  • ビデオDAC: ビットマップデータからRGB信号を作成(I/O port 0x3C6, 0x3C7, 0x3C8, 0x3C9を介して操作)
  • Video BIOS: INT10HをBIOSに送ることで表示に必要なサービスを提供する。(physical memory: 0xC_0000~0xD_FFFFに拡張BIOS空間として展開されている)

CRT(Cathode Ray Tube)はディスプレイのことで、この辺の物理的な仕組みについてはReference[3-1]が分かりやすいと思う。 xv6では、カーソル位置の取得、更新にCRT controller(I/O port 0x3D4, 3D5)のread/writeを行い、VRAMに書き込んでいる。xv6では、(少なくとも明には)後者3つの機能は用いられていない。

本章では、前者2つの機能に絞って説明する13

VRAM

まず、VRAMについては、physical memoryの0xA_0000 ~ 0xB_FFFF(128K byte memory space)にマップされているが、Reference[3-6]によれば、MDA, CGA

  • The CGA Video System has 16 Kbytes of Video RAM starting at B8000hex
  • The MDA Video System has 4 Kbytes of Video RAM starting at B0000hex

とある14VGAは0xA_0000からスタートのようだ15。xv6では、CGAのstart addressである、B8000がstatic ushort *crt = (ushort*)P2V(0xb8000);にセットされている。

CRT controller

CRT controllerはMC6845互換になっているので、一次情報であるReference[3-4]を用いてなるべく確認したい。ちなみに、CRT controller(内部)とCRT(ディスプレイ本体)は違うものなので気をつける。

xv6では、cgaputc関数でCGA controllerの操作を行っているが、カーソル位置を以下のように取得している:

// static void cgaputc(int c) {
  // skip
  int pos;
  outb(CRTPORT, 14); // cursor location high
  pos = inb(CRTPORT+1) << 8;
  outb(CRTPORT, 15); // cursor location low
  pos |= inb(CRTPORT+1);
  // skip
}

CRTPORT=0x3d4だから、最初に0x0E=14(cursor location high)を0x3d4(address)に指定し、0x3d5(data)を読むことでで位置(上位8bit)を取得している。 後半2行も似たような感じだが、最初に0x0E=15(cursor location low)を0x3d4(address)に指定し、0x3d5(data)を読むことでで位置(下位8bit)を取得している。

// http://bochs.sourceforge.net/techspec/PORTS.LSTより
         00  horizontal total
         01  horizontal displayed
         02  horizontal sync position
         03  horizontal sync pulse width
         04  vertical total
         05  vertical displayed
         06  vertical sync position
         07  vertical sunc pulse width
         08  interlace mode
         09  maximum scan lines
         0A  cursor start
         0B  cursor end
         0C  start address high
         0D  start address low
         0E  cursor location high
         0F  cursor location low
         10  light pen high
         11  light pen low

0x0A, 0x0Bにcursor start/end registerがあるが、これで、カーソルの形状を指定できる:

f:id:knknkn11626:20190615160908p:plain
Reference[3-4]より、cursor control

デフォルトはおそらく、start address=9, end address=10となっている模様。(これを見ると、カーソルの形状が縦でなく下の横線であることがわかる)

posstatic ushort *crt = (ushort*)P2V(0xb8000);のindexである。col + 80*row=posになっている。(というのもCGAは80*25/16色のカラーテキストをサポートしているため。Reference[3-2]を参照のこと。)

cgaputc関数の中でcharacter(c)が引数になっているので、これを元に更新される。VRAM領域(crt)は大体、以下のように書き込まれる:

crt[pos++] = (c&0xff) | 0x0700;
crt[pos] = ' ' | 0x0700; // ' ': space

テキストモードでは、VRAMに文字コードを書き込めば、ビデオシステム内のフォントデータを用いて自動的に各ピクセルのデータに変換される。 Reference[3-6]によれば、

  • bit[7:0]: asciiコード
  • bit[11:8]: Foreground colours (0b0111 : write)
  • bit[14:12]: Background colours (0b000: black)
  • bit[15]: BLINKING (0: no)

となっているようだ。

カーソルの位置の更新をする場合は以下のような感じ。取得部分と対応しているのがわかると思う:

  outb(CRTPORT, 14);
  outb(CRTPORT+1, pos>>8);
  outb(CRTPORT, 15);
  outb(CRTPORT+1, pos);

  1. “The 82371AB PCI ISA IDE Xcelerator (PIIX4) is a multi-function PCI device implementing a PCI-to-ISA bridge function, a PCI IDE function, a Universal Serial Bus host/hub function, and an Enhanced Power Management function."とある

  2. During a hard reset, the PIIX4 sets its internal registers to predetermined default states.とある。

  3. http://cstmize.hatenablog.jp/entry/2019/03/20/xv6%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AEException%E3%81%A8Interrupt%E3%81%AE%E6%8C%99%E5%8B%95 の"Exception発生直後のhardware(x86)の対応"を参照

  4. PIIX4のAPIC_BASEでPCIを介して変更可能だが、普通はデフォルトのまま。

  5. xv6だと、 https://pdos.csail.mit.edu/6.828/2018/readings/ia32/MPspec.pdf を参考に

  6. EOI registerはphysical memory address : 0xFEE0_00B0 に位置するmemory mapped I/O register。詳しくは、Intel SDM vol.3 10.8.5 Signaling Interrupt Servicing Completionを参照のこと。なぜ書き込む値が0なのかの必然性が分からなかったので、誰か教えてくだされ。

  7. TODO: あまり自信がなくて、cgaputcがコンソールに文字およびカーソルを表示させる役割を果たすので、consoleintrにおけるuartputcの役割がちょっとわからない…

  8. “keyboard controller(8042)はscan code set 2のコードからset 1に変換する"とkeyboard controllerの章の詳細に書いてあることに注意

  9. Reference[2-3]を参照

  10. console.c に “Input is from the keyboard or serial port. Output is written to the screen and serial port"とある。

  11. VGAのvideo BIOS(Int 10H)で互換性を変更できるらしいので、最初はCGA互換のモードになっているっぽい(未確認)

  12. ちなみに、0x3B4, 0x3B5も同じ様にCRT controllerへの操作となっているがこちらはMDA(モノクロ)用。

  13. 全体像については、Reference[3-2]のp.147の図4-3 VGAのハードウェア構成が良いと思う。または、Reference[3-4]のFigure1が簡易的な模式図となっている。

  14. “The two original Video Interface Standards (MDA ad CGA) were assigned space in this are so as not to clash with one another, so they could coexist in the same computer."とあるようにMDAとCGAは共存しているらしい

  15. 実はVGAは256K byte必要なので、VRAMの128K byte空間では足りない。多分、Video BIOSを用いてその編制御しているものと思われる。