オリジナル16ビットCPUコアMoscoviumに、FPGAボード(Sipeed Lichee Tang Primer)に内蔵されたSDRAMを接続してみました。
「#19-7 SDRAMを使って見よう(1)」
「#19-8 SDRAMを使って見よう(2)」
「#19-9 SDRAMを使って見よう(3)」
SDRAMは96MHzで駆動したのですが、Moscoviumは24MHz動作なため、アクセスサイクル数はリード3サイクル、ライト2サイクル@24MHzと、内蔵RAM(リード/ライト共1サイクル@24MHz)に比べると少し遅めでした……。
まあ、良いのですよ。メインメモリは遅いもの、と相場が決まっているので。3サイクルリードと言うスピードよりも、8Mバイトと言う圧倒的なメモリ容量が特長なのですから。
前回から引き続き、キャッシュメモリコントローラの詳細設計を進めて、SDRAMアクセスを高速化してみましょう。今回、実機(FPGAボード)での動作確認まで終わらせたいと思います。
Moscovium用に作るキャッシュメモリコントローラは、2ウェイセットアソシアティブ、ライトスルー、LFU、メモリ容量4Kバイト、です(前回記事を参照)。以下表にI/Oレジスタ一覧を示して、各レジスタの説明を続けます。
Addr | Name | R/W | Bit:15〜0 | Memo | |||||||||||||||
0xf0f2 | cachctl | RW | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − |
CC HE |
cache control |
0xf0f4 | cachtag | R | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | tag array control |
W |
VC LR |
− | − | − | − | − | − | − | TIDX[7:0] | ||||||||||
0xf0f6 | cachlfu | R | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | lfu counter control |
W |
CC LR |
− | − | − | − | − | − | − | − | CIDX[6:0] | |||||||||
0xf0f8 | cachcnth | RW | CACC[31:16] | cache access counter higher word | |||||||||||||||
0xf0fa | cachcntl | RW | CACC[15:0] | cache access counter lower word | |||||||||||||||
0xf0fc | cachhith | RW | CHIT[31:16] | cache hit counter higher word | |||||||||||||||
0xf0fe | cachhitl | RW | CHIT[15:0] | cache hit counter lower word |
BUSC出力のセレクト信号はbcs_sdrc_nです。上表の範囲でアサートされるので、SDRAMC8Mユニット内では、アドレスbadr[3:0]を復号してレジスタを特定しています。このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。
32ビットのカウンタcachcntとcachhitを使うと、キャッシュヒット率を知ることが可能です。
キャッシュヒット率:Rhit=cachhit/cachcnt×100%
カウンタには任意の値を書き込むことが可能なので、適切なタイミングでカウンタをクリアしておくと、特定の期間(処理)におけるヒット率を計測することが可能です。
リセット解除後、キャッシュメモリは無効になっています。有効にするには、cachctlレジスタのCCHEビットに1を書き込めば良いのですが、その前にやっておくべきことがあります。TAGアレイとLFUカウンタはRAMなので、リセット解除後、値は不確定であり、初期化が必要なのです。
cachtagレジスタでTAGアレイ、cachlfuレジスタでLFUカウンタにアクセスが可能となっています。アクセス方法は、I/Oレジスタの説明を参照してください。
初期化手順は決まっているので、以下にコード例を示して、その下に解説を続けます。
// wait for sdram controller ready
ldwi r7,sdrcctl
ldw r0,[r7]
andi r0,sdrc_sdrm
beq err_no_sdram // ERROR:SDRAMが搭載されていない
// polling SRDY flag
ldw r0,[r7]
andi r0,sdrc_srdy
beq pcnt-6
まずは、SDRAMコントローラの準備ができるまで待ちます。
equ max_idx,128 // maximum index address
// clear tag array
ldwi r7,cachtag
ldwi r0,cchc_vclr
ldwi r1,cchc_vclr+2*max_idx // *2 way
stw [r7],r0
addi r0,1
cmp r0,r1
blo pcnt-8
全IDXアドレス(Nidx=128個)、かつ、全ウェイ(2ウェイ)分のTAGアレイをクリアするコードです。cachtagレジスタのVCLRビットに1を立てるのと同時に2ウェイ分のIDXアドレス値を0〜255(=2ウェイ×Nidx−1)まで書き込めば、初期化終了です。
// clear lfu counter
ldwi r7,cachlfu
ldwi r0,cchc_cclr
ldwi r1,cchc_cclr+max_idx
stw [r7],r0
addi r0,1
cmp r0,r1
blo pcnt-8
全IDXアドレス(Nidx=128個)分のLFUカウンタをクリアするコードです。cachlfuレジスタのCCLRビットに1を立てるのと同時にIDXアドレス値を0〜127(=Nidx−1)まで書き込めば、初期化終了です。
// cache enable
ldwi r7,cachctl
ldbiu r0,cchc_cche
stw [r7],r0
最後にcachctlレジスタのCCHEビットに1を書き込んで、キャッシュメモリを有効に設定しました。以上で初期化手順は終了です。
「良く解らないけどキャッシュを有効にして使いたい」場合、上のコードをそのまま使えばOKです。
※TAGアレイとLFUカウンタに使用したTD IDEの分散RAMは、論理セル(LUT)を使った模擬RAMなので、シミュレーション上は起動後0に初期化されているように見えました。このためシミュレーションでは、上の初期化手順をすっ飛ばしても期待通りに動作しました。シミュレーション時間を短縮することができます。実機(FPGAボード)では、初期化手順を順守しなければなりません。
途中でキャッシュメモリを無効にしたい場合は、単にcachctlレジスタのCCHEビットを0にクリアするだけです。
その後、SDRAMを直接(キャッシュメモリ無効状態で)変更した場合、再びキャッシュメモリを有効化するためには、上のTAGアレイ初期化手順から再度実行してください。
なお、キャッシュメモリには、もう一つ、データアレイ用のRAMが存在しますが、TAGアレイが初期化されていればデータアレイの初期化は不要です。
キャッシュメモリコントローラ(CACHEC)はCPUとSDRAMコントローラ(SDRAMC)の間に入り込んで、CPUに対してはSDRAMのふり、SDRAMに対してはCPUのふりをします。そもそも、CPUとメモリバスの間には、普通のバスステートコントローラBUSC2040Dが挟まっています。
BUSC、SDRAMC、CACHECが互いに邪魔しないように連携を取りながら共存するので、以下図のような構造になっています。
BUSCは、SDRAM領域へのアクセスだった場合、アクセスその物はSDRAMCに丸投げして、制御が戻る(sdc_brdy=1になる)まで待機します。
CACHECは、キャッシュヒットした場合とキャッシュミスした場合とでは、SDRAMに対するリードアクセスサイクル数が異なるので、それをcch_hit信号でBUSCに伝えています。
キャッシュミスした時のキャッシュフィルサイクルは、データ幅32ビット×4サイクル@96MHzのバーストリードとなるので、専用バスでSDRAMC → CACHECへ転送しています。
以下リンクより、SDRAMを搭載したMoscoviumマイコンMCOC115SC0408のデザインをダウンロードできます。FPGAボード(Sipeed Lichee Tang Primer)への書き込み手順はこちらを参照してください。
キャッシュメモリ用のRAMマクロは、TD IDEのIP Generatorを使って以下図の通り、作成してください。
デュアルポートで、ポートAとPORT-Bのアクセスバス幅(width=16/32)が異なっているので注意してください。バイト書き込み(Byte Write Enable)も有効化しておきます。
TAGアレイとLFUカウンタにもRAMを使用するのですが、サイズが小さく、かつ、レイテンシ0で読み出せる必要が有ったので、TD IDEの分散RAMマクロを使いました。
TD IDEのIP Generatorでは、「DRAM」と記載される場合が有って紛らわしいのですが、D=Dynamicではなくて、Distributed(分散)です。分散RAMマクロは、論理セル(LUT)を使って作る模擬RAMなので、コンパイル(論理合成・配置配線)するとLUT数に含まれ、RAMマクロを消費しません。
分散RAMは、CACHE2W4Kユニットのデザイン内に含まれているので、皆様がIP Generatorを使って生成する必要はありません。
RAMマクロに比べて面積が大きくなるので、あまり大きなサイズの場合は、RAMマクロを使った方が良いと思います。逆に、RAMマクロでは大きすぎてもったいない場合は、分散RAMを使うという選択肢があります。
なお、TD IDEのIP Generatorで、分散RAMマクロを生成しようとすると、そのウインドウに推定LUT数が表示されます。参考にはなるのですが、実際にコンパイル(論理合成・配置配線)してみると、推定LUT数よりもずっと多くのLUTが必要となります。
推定LUT数だけで「こんな小さいなら良いか」って、判断するのは早急です。良く確認して使った方が良いと思います。
以下に、コンパイル(論理合成・配置配線)結果をまとめておきます。
予想通り、CPUからキャッシュメモリを抜けてメモリアクセスするパスがクリティカルパスとしてレポートされました。タイミングはメットしていて、まだ余裕があるので問題はありません。
SDRAM搭載エディションMCOC115SD0408と、今回のキャッシュメモリ搭載エディションMCOC115SC0408では、キャッシュメモリの有無だけが異なっています。MCOC115SD0408とMCOC115SC0408のLUT数を比較してみると、キャッシュメモリ単体での論理規模はおよそ1,500LUTと判りました。
CPU単体のLUT数(約2,000LUT)に匹敵するほどの論理規模です。
そこまで派手に大きな論理を作り込んだ覚えはない、ので、多くはTAGアレイとLFUカウンタ用に使用した分散RAMが消費したLUT数だと思われます。
最後は実物(FPGAボード)で動作確認しておきましょう。
※現金を表すキャッシュ(cash)とは別(cache)なので、混同なされませぬ様m(1__)m。
オリジナル16ビットCPUコアMoscoviumで実行するプログラムは、「#19-9 SDRAMを使って見よう(3)」で使った、SDRAMのマーチングテストを実行するプログラムです。キャッシュメモリ用の機能を追加したプログラムを以下にリンクしておきます。
プログラム:test_sdram.asm
シリアルモニタから受け付けているコマンドに、キャッシュメモリコントローラを有効化/無効化するコマンドを追加してあります。
それ以外のコマンドは、こちらに説明があります。
MCOC115SC0408を書き込んだFPGAボードにFTDI等のUSB⇔UART変換モジュールを接続して、電源を投入してください。
以下図は、実験中の様子です(写真は使い回し)。
実験サンプル:【 】内はシリアルモニタからの送信データ
【march】
DBG: write all 0
DBG: read 0, write 1
DBG: read 1, write 0
DBG: read all 0
OK: marching finished: 0x000021dc[ms]
キャッシュメモリを使わず、普通にSDRAM全面(8Mバイト)のマーチングテストを実行しました。実行時間は約8.7s(0x21dc[ms])でした。
【cche,1】
OK: cache on1/off0=0001
キャッシュメモリを有効化しました。
【march】
DBG: write all 0
DBG: read 0, write 1
DBG: read 1, write 0
DBG: read all 0
OK: marching finished: 0x00001e86[ms]
キャッシュメモリ有効の状態でマーチングテストを実行しました。実行時間は約7.8s(0x1e86[ms])でした。って、あれ?ちょっと(1割程)しか速くなってないじゃない〜。おかしいな。
【cche,0】
OK+ hit/acc cnt=0x00a80000/0x00c00000
OK: cache on1/off0=0000
キャッシュメモリを無効化しました。キャッシュメモリON〜OFF間の、キャッシュヒット回数とキャッシュアクセス回数が表示されました。
キャッシュヒット回数:Nhit=11,010,048回(0x00a8_0000)
キャッシュアクセス回数:Nacc=12,582,912回(0x00c0_0000)
キャッシュヒット率:Rhit=Nhit/Nacc×100%=87.5%
ヒット率は85%を超えていますね。平均アクセスサイクル数≒1.4サイクルです。キャッシュメモリの効果としては、相当高速になっていることが確認できました。な・の・に、マーチングテストが1割ほどしか高速化できなかったのは何故でしょうか?
マーチングテストのコード(一部抜粋↓)を眺めて見ると:
// read 1, write 0 ldwi r6,mch_top>>16 ldwi r7,mch_top&0xffff |
初期設定 | ||
mch_r1w0_loop: | ループ先頭 | ||
movtc tr,r6 ldwx r3,[r7] stwx [r7],r0 |
メモリ(SDRAM)アクセス キャッシュメモリ無効では6サイクル キャッシュメモリ有効ではヒット4/ミス7サイクル |
||
add r7,r2 adc r6,r0 |
アドレス更新 2サイクル |
||
cmp r3,r1 bne mch_fail |
期待値比較 2サイクル |
||
cmp r7,r5 cmb r6,r4 blo mch_r1w0_loop |
ループ継続判定 4サイクル |
SDRAMにアクセスする命令(橙着色、4または7サイクル)に比べて、アドレス更新や期待値比較、ループ制御など、それ以外の処理部分(その他、8サイクル)が大勢を占めているので、SDRAMアクセスだけがキャッシュメモリによって高速化されても、効果が感じられないみたいです。
アララ……、Moscoviumの32ビット処理が遅すぎるんですね。16ビット幅を超える24ビットアドレス処理の為、32ビット処理を多用しているのだけど、そこがとっても遅くなっているようです。
こんな状態だと、CACHE2W4Kユニットの1,500LUTを消費してまでキャッシュメモリを搭載する意味がないかもしれないです……。費用対効果悪すぎm(1x_x)m!
キャッシュメモリとしては、確実に高速アクセスに貢献できていることが確認できました。キャッシュメモリコントローラとしては大成功です!が、それ以前に、Moscoviumコア自体の処理が遅すぎて、実験の結果としては残念なことになってしまいました。Moscoviumをもう少し何とかしないとダメ🙅かもしれませんね。