ファームウェアフルスクラッチで必須のテク "CAR" - キャッシュをメモリとして使う (cache as ram)

前回 : https://qiita.com/tanakmura/items/2a631a3b81bf583bf4b3

外部デバイスは、使うのが簡単なものと難しいものがある。

DRAMメモリは、どっちかというとアクセスが難しいほうの部類に入る。
まず、コンデンサに充電したり、充電したのを読んだりするアナログな作業をするのがコンピュータ的ではない。それに加えて、高速信号をパラレルにやりとりするので、それも大変である。

その都合上、DRAMへアクセスするためにはそれなりの手順を踏んで初期化する必要がある。

初期化前にDRAMにアクセスするとどうなるかというと、少なくともD945GCLF2ではハングアップして停止してしまうようだ。

```
    # 停止してしまう
    mov 0, %esi
```

このDRAMの初期化を記憶装置なしで処理するのは結構大変だが、じゃあDRAMが使えないとしたらどこに記憶するねん？という話になる。

幸い、現代のCPUは数十KiB以上のそこそこ大きなキャッシュを積んでいる。DRAMの初期化が終わるまでは、このキャッシュを記憶装置として使うことが多い。

このように普通のRAMのようにして使うキャッシュをCache as ram (CAR) と呼ぶ。これにあわせて、この概念をcarと呼んだりする。以降の解説でもこの概念を"car"と呼ぶことにする。


AMD の BIOS、OS 開発者向けマニュアル、いわゆる BKDG には、L2cache を通常のメモリとして使う方法が書かれている。これがcarに該当する。

https://www.amd.com/content/dam/amd/en/documents/archived-tech-docs/programmer-references/42301_15h_Mod_00h-0Fh_BKDG.pdf#G5.3758175

Intel のマニュアルには該当する箇所が見つけられなかったが、

https://github.com/intel/FSP

Intel のファームウェア実装用ライブラリ FSP には TempRamInit というものがあり、これを使うと同等のことが行なわれると考えられる。(FSPはまさに、DRAMの初期化をするライブラリである。詳しく知りたい人はリンク先のマニュアルを参照してほしい)



さて、ではこのcarをどうやって実現するか、というと、本質的には、キャッシュを有効にするだけだ。

キャッシュは、アクセスする領域の大きさがキャッシュの大きさに収まっているうちは eviction は起こらない。evictionが起こらなければ書きこんだ値が残るので、記憶域として使える。

https://github.com/tanakamura/my_firmware/blob/31b562565f067586d28d2aba1654f61421c4c332/d945gclf_car/cache.s

```
	## disable cache (やらなくてもいいはず)
	mov	%cr0, %eax
	or	$0x60000000, %eax
	mov	%eax, %cr0

	## enable mtrr
	mov	$((1<<11)|0), %eax
	mov	$0, %edx
	mov	$0x2ff, %ecx
	wrmsr

	## wb : 0xfffc0000 - 0xffffffff
	xor	%edx, %edx
	mov	$0xfffc0006, %eax
	mov	$0x200, %ecx 	# mtrr base
	wrmsr
	mov	$(0xfffc0000 | (1<<11)), %eax
	mov	$0x201, %ecx	# mtrr mask
	wrmsr

	## enable cache
	mov	%cr0, %eax
	and	$0x9fffffff, %eax
	mov	%eax, %cr0

	## fill (やらなくてもいいはず)
	mov	$0xfffc0000, %esi
	mov	$(256*1024/4), %ecx
	rep	lodsl
```

IA32 でキャッシュを有効にするには、

1. cr0 の CD(1<<30) と NW (1<<29) をクリアする
2. キャッシュ可能領域を mtrr で設定する

の2stepだ。

cr0 のフラグはそんなに難しくないだろう。

MTRR は、領域ごとにキャッシュ可能かどうかを定義するレジスタだ。使いかたは


1. msr 0x2ff の (1<<11) を立てる (MTRR有効化)。
2. msr 0x2ff の下位ビットでデフォルトキャッシュタイプを指定する (ここでは0)
3. (MSR 0xFE を見て使えるMTRRの数を調べる : ここではやってない(手抜き))
4. msr 0x200 にキャッシュ方法を指定する。上位ビットでレンジのbaseアドレスを設定する
5. msr 0x201 に(1<<11) を立てて有効化する。上位ビットでマスクを指定する


領域は4KiBごとに設定できる。

範囲は、マスクで変えられる。

```
<アクセスするアドレス> & <201で設定したマスク> == <200で設定したbaseアドレスl> & <201で設定したマスク>
```

が一致していると 0x200 で設定したキャッシュタイプが有効になる。

キャッシュタイプは、

- 0 : 無効
- 6 : Write Back

となる。1-5の値はマニュアルを参照してほしい。

上の設定をすることで、

1. デフォルト = 0 (MTRRで指定した範囲外はキャッシュ不可)
2. 0xfffc0000 - 0xffffffff の 256KiB が WB でキャッシュ

という設定になる。D945GCLF2 で使われている Atom 330 ではコア毎に、512KiB の L2B キャッシュが付いている、この半分を使う設定になる(全部使って動くかどうかわからなかったので半分です)。チップ全体では1MiBのL2キャッシュが付いているが、car ではコアが占有しているキャッシュしか使えない、注意しよう。(corebootではL1Dに入るサイズしか使わないようになっている。このモチベーションを私はよく知らない。)

キャッシュの仕組み上、キャッシュを使ってアクセスする領域は連続にする必要がある。厳密には、手でwayのアドレスを計算すれば衝突しない範囲を見つけることはできるかもしれないが、連続した領域を使うのが普通だし、わざわざ分割する理由はないだろう。


ともかく、これでキャッシュが使えるようになった。範囲内に収まっているうちは通常のメモリと同じように使えるので、ここにスタックを設定すれば、念願のcall/retが使えるようになり、それにあわせてC言語も使えるようになる。

https://github.com/tanakamura/my_firmware/blob/31b562565f067586d28d2aba1654f61421c4c332/d945gclf_car/init.s#L114


もちろん、間違ってRustで書いてしまっても問題ないね。

https://github.com/tanakamura/my_firmware/blob/31b562565f067586d28d2aba1654f61421c4c332/d945gclf_car/r/src/lib.rs#L44


これが動くまではレジスタに戻りアドレス書いて疑似call/retのようなことをやりがち

https://github.com/tanakamura/my_firmware/blob/31b562565f067586d28d2aba1654f61421c4c332/d945gclf_car/print.s#L25

https://github.com/tanakamura/my_firmware/blob/31b562565f067586d28d2aba1654f61421c4c332/d945gclf_car/init.s#L114


例えば、ubrx というマザーボード上に簡易コンソールをつくろうというプロジェクトのプログラムでは、

64bit mmx レジスタに一個ずつ16bitレジスタの値を入れていくみたいなことをやっている(4回までpushできる)

https://github.com/pbatard/ubrx/blob/master/mmx_stack.inc



さて、x86の問題として、キャッシュラインフィルができないという問題がある。

```
mov %eax, 4
```

などとやると、キャッシュライン64byteあるとしたら、一旦64byteをコアの外から読んで、ラインフィルしたあと、そのうちの4byteを書きかえるという動作になる。

上の場合、0-63 byte にあるデータをコアの外から読んできて、そのうちの 4-7 byte 目をL1Dキャッシュ上で書きかえる、という動作になる。

PowerPC では dcbz というキャッシュラインを丸ごと0に埋める命令があって、このラインフィル時のreadを消せるのだが、x86にはそれがない。一応ある程度新しいx86はライトコンバインが優秀で、64byte連続で書きこめばこのreadが消えることが多いようだが、消える保証はない。

ここで問題なのは、DRAM初期化前は

```
mov 0, %eax
```

とすると、DRAMにアクセスできなくてハングアップしてしまうという点だ。これはつまり、DRAMにアクセスしてしまうアドレスをキャッシュ可能にしても、必ずDRAMへのreadが発生してしまい、CPUが停止してしまうことになる。

上の
```
mov %eax, 4
```
は、実際には
```
mov 0, L1Dcache # アドレス0から64byteキャッシュへロード
mov %eax, L1DCache[4] # キャッシュ内の4byte目を書きかえ
```
という動作になり、アドレス0へのアクセスが発生する。


さて、ではなぜ上のプログラムは、キャッシュ可能領域に設定して動いているのだろうか？これは、DRAMにアクセスしない範囲をキャッシュ可能領域としているからだ。
じゃあどうやってDRAMアクセスする範囲を決めるの？

次回、x86 マザーボードのメモリマップの話へ続きます。 https://qiita.com/tanakmura/items/80988d6976fe7e7e01b1