で使ってるカンペの内容です
BIOS プログラミングやる
よくXとかでBIOSプログラミングの話書いてるけど、実際どんなのかわかりますかね
何やっているか実演していきたい
BIOSってなに?
OSが起動する前の初期化をやる。
特に、PCのメインメモリであるDRAMは初期化しないと使えない。これの初期化をやる。
PCIは初期化しないとコンフィグ空間以外アクセスできない
PCIeは初期化しないと何も通信できない
初期化が終わったらストレージからOSを探してきて起動する。
昔のいわゆるレガシーBIOSでは、OSが使うBIOSサービスの実装がBIOSに入ってる
今のUEFIでは runtime service の実装がBIOSに入ってる。
起動したときに出てくる画面はBIOSに入ってるプログラムが表示してる。
ストレージを付けなくても起動することが分かる。
このBIOSはどこに入ってるかというと、いわゆるROMに入ってる。
昔のマザボだと、32pin のものが多い。
ある程度新しいマザボだと、8pin の SPI Flash が使われている。
32pin のほうが物理的には大きいが、容量は新しい SPI Flash のほうが大きい。
32pin ROM は、アドレスバスやデータバスがそのまま出ている
SPI Flash は、シリアライズされたコマンドを使ってデータをやりとりする。
手元で実験用に使ってるマザーボードは、
ASUS の P2B98-XV というマザーボード
ASUS がマザーボード単体で販売したものと、HP の PC に組み込まれた OEM の2 versionがあるらしい。
何故か BIOS が別実装になっていて、ASUS 公式 BIOS が Award BIOS、HP OEM BIOS が Phoenix BIOS になってる。
ROM をさしかえると、中身が変わってるのが確認できる。
P2B98-XV
BIOSプログラミングでは、マザーボードの情報がCPU以上に大事なので、説明しておく。
P2B98-XV は、440 chipset を採用して、Slot1 CPU が利用可能なマザーボード
リリースは、1999年らしい。みんなが Win98 を使ってた頃。
私が PC を使いはじめた頃だが、この頃は本当に使ってただけなので、当時マザーボードとか詳しかったわけではないです
Slot1 CPU というのは、このゴツいカセットみたいなやつで、ここにあるのは
Pentium2 400MHz。
これを取りはずすと、ノースブリッジが見える。
ノースブリッジというのは、CPU、メインメモリ、PCI、AGP を接続するブリッジ。
これは、443BX というチップが使われている。
この頃のマザーボードは積層数が少ないので、目視でもなんとなくバスの構成が見える。
ノースブリッジから、左側にCPUへのバスが出ていて、右側へメモリへのバスが出ている。
この下に AGP に繋がった ATI のビデオチップが付いてるが、今回は使わないので無視。
さらに下に行くと、サウスブリッジも確認できる。
これは、ノースブリッジから出てるPCIの下にぶら下がっている。
スロット形状としては、PCI slot と サウスブリッジは、別の形をしてるが、
バスとしては同じPCIバスにぶら下がってる。
サウスブリッジの中には、USB、IDE、PCI to ISA bridge などが入ってる
この ISA ブリッジには、ISA slot、super IO、BIOS が入った ROM がぶら下がっている。
もう知らない人も多いと思うので解説しておくと、
本当に古いPCは、ISA bus だけが存在した。
この ISA bus は初期化とかもなく、CPU から出た信号がそのまま出てるようなバスで、
命令に書かれているアドレスやデータがそのまま出てくる。
各種拡張カードは、その出てきた信号を好きなように解釈して、好きなように応答するというのが ISA bus。
この世代では過去互換のために残されていて、PCI で拾われなかった I/O や MMIO アクセスが ISA bus に流れてくる。
古いPCでは、この ISA bus に 0x3f0 の I/O アドレスにアクセスするとフロッピー、
0x3f8 の I/O アドレスにアクセスすると UART (RS232 シリアル) になるというように習慣で決まっていた。
古いPCではそれぞれ専用のコントローラが付いてたが、集積度が上がると、大したサイズではなくなったので、
SuperIO というチップにまとめられた。
SuperIO は1チップで、UART, Printer(Parallel), FDC, PS/2 keyboard などを制御する。
この SuperIO が ISA bus に付いてる。
なお、ISA bus は、アドレスとデータがパラレルで出ていて遅い割に配線が多いので、現代のPCでは
この ISA bus を時分割したみたいな LPC が使われていて、SuperIO は LPC の下に繋がっている。
このマザボでは、PC87309というSuperIOチップが付いてる。
ついでにクロックも説明しておく。
水晶が付いてる。
これが ICS9148BF というクロックジェネレータに入って、
- FSB の 100MHz (FSB というのは、CPU - ノースブリッジ - DRAM 間で使われるクロック)
- USB の 48MHz
- PCI の 33MHz
などを作っている
クロックを作るってなんやねん。
VCO という発振器があって、電圧かけると、電圧にあわせて発振する。
これと水晶から出てるクロックを比較して、一致させるみたいな PLL という回路がある(よく知らない)
これを使うと、水晶から出てきた綺麗なクロックを作って、好きな値のクロックを作るということができる。
それをやってるのが ICS9148BF というチップ。
これは I2C で繋がっていて、I2C に適切な信号を流すとクロック変えられる。
POST カードって何
BIOS プログラミングで便利なので持っておきましょう
mov al, 0xFF
mov dx, 0x80
out dx, al
みたいに、port 80 に 1byte 出すと、その 1byte を拾って表示してくれる。
port 80っていうのはどこで決まっとんのや?
これは PCI に付けてるが、PCI 準拠ではない。
config accessもできなくて、PCI bus に流れてくる port 80 へのアクセスを監視してるだけ。
この世代のチップセットは AGP 初期化前なら、CPU から出た I/O アクセスは
全部 PCI へ行くので、CPU が I/O port 80 に書けば、それが PCI bus に出て、POST カードが拾える
ROM と x86 リセット
というわけでプログラムを書いていく。
書いていくと言っても何をすればいいの? → ROM の最後 16byte に命令を置けばいい。
x86 ではリセット直後は、
CS base = 0xFFFF_0000 IP = FFF0 からはじまる
これはつまり、32bit アドレス空間上の、0xFFFF_FFF0 に書かれた命令を実行するということになる。
PC マザボ用のチップセットでは、この x86 のリセット仕様にあわせて、
0xFFF0_0000 - 0xFFFF_FFFF あたりがROMにマップされるようになっている。
ROMサイズがこれの領域より小さい場合は、ミラーされるので、ROM の最後の 16byte が、
0xFFFF_FFF0 から見えるようになる。
x86に詳しい人は気付いたかもしれないが、リセット直後の状態はちょっとヘンである。
CS base = 0xFFFF_0000 IP = 0xFFF0 からはじまる
sel = 0xFFFF
であるが、16bit mode では普通は、base 値は selector 値のx16 になるはずである、が、
リセット直後は real mode だが base と selector 値は一致してない。
再現しようと思ったら、protect mode で CS base いじって、
real mode に戻るみたいなことをしないといけない。
ROM かきかえ環境
ROM かきかえは、ROMライタを使って書きかえるのが、一番正攻法である。
最近は TL866 というのを使う人が多いらしい。
私はROMライタは持ってない。
実はマザーボードは、ROM ライタとして使える。
一旦起動すると、ROMははずして大丈夫なので、ホットスワップして、
Linuxならflashromコマンドで書くとかいう野蛮なことができる。
あと私は、「ROM焼き大丈夫!」というを持ってて、これを使ってる。
これは、スイッチひとつでROMのホットスワップができるという装置で、
これを使えば野蛮度は若干マシになる。
中身は、ROMのCS信号を切りかえてるらしい。
やってみると分かるが、どれも大変である。
ROM の足はかなり折れやすく、付けたり外したりはあんまりしたくない。
あと、逆に付けると一瞬でROMが破壊される。
私のソリューションとしては、ROMソケットとZynqを繋げて、
ROM信号をFPGAに入れて、その信号とFPGAの block ram を繋げて、
FPGA block ram 内に、BIOS ROM を入れるというようにしてる。
この block ram は、zynq の ARM からもアクセスできるようにしていて、
ARM の Linux から ROM image が更新できるようになってる。
ARM に block ram を更新する http server を立てて、
どこかから http put を送ると、そのデータが rom image になるというようにしてる。
超快適で、コンパイル時間を除いた TAT は 1sec 未満である。
JTAGが付いた開発ボードを除けば、世界最強のBIOS開発環境ができてる自信がある。
ちなみに、手元の zybo は、digilent のマニュアルによると、Block ram が、240KiB となってるが、
これは byte 単位アクセスしたときのリミットになる。
Zynq の block ram は ECC のために、9bit で 1 単位となっていて、ECCを使わないのであれば、270KiBまで使える。
Xilinx の memory generator はそのへんちゃんと対応していて、アクセス単位を32bitにすると、256KiB の SRAM block が作れる。
実演 : POST カードに 1byte 出す
というわけで、POST カードに値を出してみるか。
カンペ
nasm くりかえし
times N db 0
実演 : uart から Hello World を出す
UART は、super io 内に入ってる。
super io の作法に従って、UART にI/Oアドレス付ける必要がある。
アドレスはなんでもいいが、PCでは普通0x3f8 なので 0x3f8 にしよう。
BIOS の設定項目で、COM1 のアドレス変えられるみたいなのを見たことあるかもしれない。
それに対応するのがこれである。
super io の作法は標準化されてない。
チップ毎にやり方を調べないといけない。
さらに、super io の base address も固定されてない場合があり、
マザボメーカーの仕様にあわせる必要がある。
幸い、P2B98-XV では、公式BIOSに入ってる ACPI DSDT に、
super io の base address が書いてあった。
OperationRegion (N309, SystemIO, 0x015C, 0x02)
Field (N309, ByteAcc, NoLock, Preserve)
{
NIDX, 8,
NDAT, 8
}
分かりにくいが、0x15C が PC87309 のベースアドレスになるっぽい。
PC87309 の中にレジスタがある。
0x15C で、レジスタを選ぶ
0x15D を読めば、今のレジスタ値が、書けばレジスタ値が更新できる。
このレジスタを書きかえて、UART の base address を設定する。
super io の中のデバイスには、それぞれ LDN (Logical Device Number) という番号が振られている。
レジスタ 7 に、LDN を書けば、対応するデバイスを設定できるようになる。
UART の LDN は 3 になる
そのあと、IOAH(0x60), IOAL(0x61) に 0x3f8 を書けば、UART のベースアドレスが、0x3f8 になる。
最後に 0x30 に 1 を書くと、今選んでいる LDN のデバイスが有効になる。
カンペ
;;; x86 outs
cx に文字数入れる
rep outsb
;;; セグメントのためにアドレス振り直す
times (192*1024) db 0
code_start:
org 0
code_end:
tiems (64*1024 - code_end - code_start) db 0
reset:
jmp 0xF000:0
;;; UART 初期化
%macro out8 $2
mov dx, %1
mov al, %2
out dx, al
%endmacro
%macro sio_write $2
out8 0x15c, %1
out8 0x15d, %2
%endmacro
sio_write 0x7, 0x3 ; LDN=3
sio_write 0x60, 0x3 ; IOAH=3
sio_write 0x61, 0xf8 ; IOAL=f8
sio_write 0x30, 1 ; ACT = 1
init_uart:
mov al, 0b00000111
mov dx, 0x3f8 + 2 ; FCR
out dx, al
; dlab=1, parity no, stop=1, data=8
mov al, 0b10000011
mov dx, 0x3f8 + 3 ; LCR
out dx, al
; divider = 1
mov dx, 0x3f8 ; UART_DIV_LO
mov al, 0x1
out dx, al
mov dx, 0x3f9 ; UART_DIV_HI
mov al, 0x0
out dx, al
; dlab=0, parity no, stop=1, data=8
mov al, 0b00000011
mov dx, 0x3f8 + 3 ; LCR
out dx, al
出ましたね。