TECH I Vol.39 MIPSプロセッサ入門

第8章 VR4131の周辺機能の使い方

補足説明

中森 章


 ここでは本書に掲載しきれなかったサンプルプログラムについて解説します.

1.キャッシュ操作の詳細

●キャッシュの操作
 キャッシュ自体はCPUの機能なので,それをどう操作するかは簡単には説明しがたいものがあります.これまでも,キャッシュのFILL操作例を示してきましたが,もっとキャッシュそれ自体に焦点を絞ったサンプル・プログラムが欲しいところです.
 そこで,キャッシュに書いたデータをDRAMにライトバックするプログラムを作ってみましょう.
 VR4131のキャッシュ・ラインはコンフィグ・レジスタの設定で16バイトと32バイトに切り替えることが可能です.たとえば,64Kバイトの領域をデータ・キャッシュを通じて,バイト単位にライトしたデータを,ライトバックして埋めて行く処理に関し,キャッシュ・ラインが16バイトと32バイトではどの程度の時間差があるか興味深いです.その実行時間を比較するプログラムを作ってみましょう.リストAがそのプログラムです.
 リストAの構成としては,
(1)データ・キャッシュのライン・サイズを16バイトに設定(dcache16関数)
(2)データ・キャッシュを初期化(dache_init16関数)
(3)タイマを設定(timer_start関数)
(4)0x80400000番地から0x8040FFFF番地にデータをライトし,16バイト・ライトするごとにデータ・キャッシュをライトバックする(write16関数)
(5)タイマの値を読み取る(timer_end関数)
(6)タイマの値をコンソールに表示する
(7)データ・キャッシュのライン・サイズを32バイトに設定(dcache32関数)
(8)データ・キャッシュを初期化(dache_init32関数)
(9)タイマを設定(timer_start関数)
(10)0x80400000番地から0x8040FFFF番地にデータをライトし,32バイト・ライトするごとにデータ・キャッシュをライトバックする(write32関数)
(11)タイマの値を読み取る(timer_end関数)
(12)タイマの値をコンソールに表示する
というものになっています.タイマ関数としては本誌掲載リスト4でも使用したカウント・レジスタ(CP0レジスタ番号9)を使用するものとします.
 ここで,キャッシュの操作についてまとめておきます.今回はデータ・キャッシュのみですが,命令キャッシュに関しても同様のことが当てはまります.

●キャッシュの初期化
 MIPSアーキテクチャには1命令でキャッシュを初期化する命令はありません.CACHE命令を使ってキャッシュ・ライン単位に初期化していきます.通常,キャッシュの初期化はIndex Store Tag操作で各キャッシュ・ラインのタグ部に0を書き込んで行きます.
 VR4131は2ウェイキャッシュを採用しているので,一つのインデックスに対して二つのラインが選ばれます.これらを区別することが必要な場合もあります.Index Store Tag命令では仮想アドレスを使ってキャッシュ・ラインをインデックスしますが,その仮想アドレスのビット13が1の場合がウェイ1を指し示し,ビット13が0の場合がウェイ0を示します.
 仮想アドレスのビット13でウェイを示すということは,キャッシュの初期化において,キャッシュの総容量さえわかればダイレクトマップの場合と同一の命令列でキャッシュの初期化ができることになります.つまり,
    mtc0  $0,$28         /* TagLo */
    mtc0  $0,$29         /* TagHi */
    li    $2,0x80000000
    li    $3,0x80004000  /* 容量が16Kバイトの場合 */
loop:
    cache 0x09,0($2)     /* Index Store Tag Dcache */
    addiu $2,$2,0x10     /* ライン・サイズが32バイトと分かっている場合は0x20を加算すればよい */
    bne   $2,$3,loop
    nop
でよいことになります.この命令列ではウェイに関する情報はありません.もっとも,キャッシュが2ウェイであることを知っていれば,ウェイ0とウェイ1を同時に初期化する次のような命令列も考えられます.
    mtc0  $0,$28         /* TagLo */
    mtc0  $0,$29         /* TagHi */
    li    $2,0x80000000
    li    $3,0x80002000  /* 容量が16Kバイトの場合 */
loop:
    cache 0x09,0x0000($2)/* Index Store Tag Dcache way-0 */
    cache 0x09,0x2000($2)/* Index Store Tag Dcache way-1 */
    addiu $2,$2,0x10     /* ライン・サイズが32バイトと分かっている場合は0x20を加算すればよい */
    bne   $2,$3,loop
    nop
 リストAのプログラムではキャッシュを初期化するために,Index Store Tag操作ではなく,Index Writeback Invalidate操作を使用しています.Index Store Tagの場合,キャッシュ・ラインがDirtyであっても強制的にタグを0にするので,Dirtyなキャッシュ・ラインはメイン・メモリにライトバックされないまま消えてしまいます.
 Index Writeback Invalidate操作を使えば,Dirtyなラインが存在する場合は,その内容をメイン・メモリにライトバックしてからキャッシュを無効化します.プログラムの実行時に,もしかしたら,u-bootで使用していたキャッシュ・データがDirtyなまま残っていることを考慮して,Index Writeback Invalidate操作を使用してあります.

●無効化するアドレスが決まっている場合
 上述の例ではインデックスを使ってキャッシュを無効化しましたが,無効化するアドレスが決まっている場合はヒット操作を使うことができます.それには,Hit Invalidate操作とHit Writeback Invalidate操作があります.これらは次のようにして使用します.
   li  $2,addr
   cache 0x11,0($2)  /* Hit Invalidate Dcache */
あるいは,
   li  $2,addr
   cache 0x15,0($2)  /* Hit Writeback Invalidate Dcache */
です.Hit Writeback Invalidate操作が無効化するキャッシュ・ラインがDirtyな場合,その内容をライトバックしてから無効化を行うのに対して,Hit Invalidate操作はキャッシュ・ラインがDirtyであるかないかにかかわらず(ライトバックを行わず)に無効化を行います.
 これらの操作をインデックスを使って行おうとすると,ウェイ0とウェイ1の両方が無効化対象となるので,二つのCACHE命令が必要です.たとえば,
   li    $2,addr
   cache 0x01,0x0000($2)  /* Index Writeback Invalidate Dcache */
   cache 0x01,0x2000($2)  /* Index Writeback Invalidate Dcache */
のようになります.1番目のCACHE命令がウェイ0と1のどちらを無効化しているかは,指定する仮想アドレス(今の場合はaddr)に依存します.仮想アドレスaddrのビット13が0の場合は,1番目のCACHE命令がウェイ0,2番目のキャッシュ命令がウェイ1を示しています.もし仮想アドレスaddrのビット13が1の場合は,ウェイの0と1が逆になります.いずれにしろ,二つのCACHE命令でウェイ0とウェイ1の両方を指定していることには変わりません.
 インデックス操作はキャッシュがヒットするかしないかにかかわらず,指定したインデックスのキャッシュ・ラインを無効化するので,キャッシュのヒット率の観点からは好ましくありません.ヒット操作で済む場合は,なるべくヒット操作を使った方が,CACHE命令の実行数とキャッシュのヒット率の観点から有利です.

●キャッシュのライトバック
 データ・キャッシュの特定のラインを強制的にメイン・メモリにライトバックするためには,キャッシュの初期化でも使用したIndex Writeback Invalidate操作やHit Writeback Invalidate操作,またはHit Writeback操作があります.この場合も,インデクス操作では,ウェイ0とウェイ1の両方に対してCACHE命令を実行する必要があります.まだ説明していないHit Writeback操作の実行例を以下に示します.
   li    $2.addr
   cache 0x19,0($2)
 さて,リストAのプログラムの実行結果を図Aに示します.得られるタイマの値が小さいほど実行時間が短いことを意味するので,ラインサイズ32バイトの方が16バイトのときよりも約39%(6294633/4524888=1.39)高速に処理できることがわかります.
 なお,このプログラムは,なぜか実行が不安定です.キャッシュを操作するせいか,u-bootのgoコマンド実行後にハングアップすることが多々あります.そういう場合はリセットをかけてから再びgoコマンドを実行してみてください.

リストA キャッシュ操作プログラム(cache.c)

図A リストAの実行結果
# loads
## Ready for S-Record download ...
..
## First Load Addr = 0x80200000
## Last  Load Addr = 0x80200CA9
## Total Size      = 0x00000CAA = 3242 Bytes
## Start Addr      = 0x80200000
# go 80200000
## Starting application at 0x80200000 ...
line size=16 time=6294633
line size=32 time=4524888
## Application terminated, rc = 0x0
#


2.CSIでDMA転送をする

 本書ではCPUによるプログラム転送の例を解説していますが,ここではDMAを使って転送するプログラムについて説明します.これはCSIの送受信モードを使用して1回の転送(8回または16回のクロック変化)で送信と受信を同時に行います.今回のプログラムはCSIのSOUT(出力)とSIN(入力)が結線されてループバック状態になっていることを前提とします.DRAMからDMAで読んできたデータをクロック同期でシリアル転送し,ループバックして,自身で受信し,DMAでDRAMに書き込みます.このプログラムがリストBです.CSIのループバック機能はBLANCAではサポートされてないので,このプログラムが正しく動作するかどうかは未検証です(かなり自信はありますが…).
 リストBの内容自体はCSIをDMAで使う場合の設定手順をそのまま示してあるだけなので特に説明は要らないと思います.
 参考までに,特に何もしない状態(ループバックなし)で実行した場合の結果を図Bに示します.試しに実行してみると,正常実行時と同じく,ページ境界割り込みが発生して終了しています.ただ,転送結果の比較は失敗するようです(当たり前ですが).一応,受信側がハイレベルの信号(オープン状態がハイに見える)を受信しているらしく,受信側DMA結果はオール1となっています(u-bootのmdコマンドで確認).送信側は空中に向かってデータが出て行ったのでしょうね.

リストB DMA転送版CSI送受信サンプルプログラム(csi_dma.c)

図8 リストBの実行結果
# loads
## Ready for S-Record download ...
..
## First Load Addr = 0x80000000
## Last  Load Addr = 0x80000FAF
## Total Size      = 0x00000FB0 = 4016 Bytes
## Start Addr      = 0x80000000
# go 80000000
## Starting application at 0x80000000 ...
Making Source Data
CSI Setup
DMA Start
Wait for Interrupt
Interrupt Occurred!
R_P1STP
Checking Destination Data
DMA Result is NG!
## Application terminated, rc = 0x0
# md a0210000 20
a0210000: 00010000 00030002 00050004 00070006    ................
a0210010: 00090008 000b000a 000d000c 000f000e    ................
a0210020: 00110010 00130012 00150014 00170016    ................
a0210030: 00190018 001b001a 001d001c 001f001e    ................
a0210040: 00210020 00230022 00250024 00270026     .!.".#.$.%.&.'.
a0210050: 00290028 002b002a 002d002c 002f002e    (.).*.+.,.-.../.
a0210060: 00310030 00330032 00350034 00370036    0.1.2.3.4.5.6.7.
a0210070: 00390038 003b003a 003d003c 003f003e    8.9.:.;.<.=.>.?.
# md a0220000 20
a0220000: ffffffff ffffffff ffffffff ffffffff    ................
a0220010: ffffffff ffffffff ffffffff ffffffff    ................
a0220020: ffffffff ffffffff ffffffff ffffffff    ................
a0220030: ffffffff ffffffff ffffffff ffffffff    ................
a0220040: ffffffff ffffffff ffffffff ffffffff    ................
a0220050: ffffffff ffffffff ffffffff ffffffff    ................
a0220060: ffffffff ffffffff ffffffff ffffffff    ................
a0220070: ffffffff ffffffff ffffffff ffffffff    ................
#
※LoopBackしてないので比較結果はNGになります.
 転送結果は不定(今の場合はオール1)です.


3.MIPS逆アセンブラを作る

 もう少しサイズの大きなプログラムとして,逆アセンブラを作ってみました.逆アセンブラの母体は本書のAppendix1に掲載したMIPSシミュレータsimipsの逆アセンブラ部分を使います.全体のソース・プログラムは本書サポート・ページのAppendix1のアーカイブ・ファイルをダウンロードして見てください.
 ここでは,逆アセンブラ関数をmain関数から呼び出す部分のみリストCに示します.
 リストCを見てもらえばわかると思いますが,シリアルのコンソールから先頭アドレス(16進数の場合は0xを先頭に付けるのを忘れないように)を読み込み,そのアドレスから逆アセンブルを開始します.逆アセンブラの本体はdismips関数です.第二引き数(本来は終了アドレスを示す文字列を指定する)に何も指定してないので,20命令を逆アセンブルしたらリターンし,新しいアドレス入力待ちになります.
 ここで,重要なトピックスがあります.これまでのプログラムでは考慮が不要でしたが,プログラムが巨大なせいか,グローバル・ポインタ相対のデータ・アクセスがコンパイルしたオブジェクト・コードの中に出てきます.u-bootでは,スタック・ポインタは設定してくれますが,グローバル・ポインタは設定してくれないので,何も考慮しないとプログラム中での大域変数のアクセスが誤動作します.
 それを防止するためのオマジナイがmain関数の前にある,
  extern _gp;
  asm("la $28,_gp");
external宣言とasm文です.これによってプログラムの実行前にグローバル・ポインタ($28)を設定することができるようになります.実際にグローバル・ポインタをどのアドレスに割り付けるかはリンカに与えるコマンド・ファイル内に記述します.リストDがそのリンカ・スクリプト・ファイルです.
 リストCの実行結果を図Cに示します.プログラム実行の終了はコンソールからマイナス('-')という文字を入力すると終了します.いきなりですが,ここでお詫びです.この逆アセンブルプログラムですが,最初にgoコマンドを実行時,ときどきハングアップしてしまいます.理由はよくわかりません.おかしな挙動を示す場合はプログラムのロードからやり直してください.

リストC 逆アセンブラ呼び出し部分(siu.cのmain関数付近)

リストD リンカ・スクリプト・ファイル(mips.lds)

図C リストCの実行結果
# loads
## Ready for S-Record download ...
.........................
## First Load Addr = 0x80200000
## Last  Load Addr = 0x80209F5F
## Total Size      = 0x00009F60 = 40800 Bytes
## Start Addr      = 0x80200000
# go 80200000
## Starting application at 0x80200000 ...

bfc00000 100000ff : beq r0,r0,0xbfc00400
bfc00004 00000000 : nop
bfc00008 100000fd : beq r0,r0,0xbfc00400
bfc0000c 00000000 : nop
bfc00010 10000149 : beq r0,r0,0xbfc00538
bfc00014 00000000 : nop
bfc00018 10000147 : beq r0,r0,0xbfc00538
bfc0001c 00000000 : nop
bfc00020 10000145 : beq r0,r0,0xbfc00538
bfc00024 00000000 : nop
bfc00028 10000143 : beq r0,r0,0xbfc00538
bfc0002c 00000000 : nop
bfc00030 10000141 : beq r0,r0,0xbfc00538
bfc00034 00000000 : nop
bfc00038 1000013f : beq r0,r0,0xbfc00538
bfc0003c 00000000 : nop
bfc00040 1000013d : beq r0,r0,0xbfc00538
bfc00044 00000000 : nop
bfc00048 1000013b : beq r0,r0,0xbfc00538
bfc0004c 00000000 : nop

80200000 3c1c8021 : lui r28,0x8021
80200004 0808008e : j   0x80200238
80200008 279c9f60 : addiu       r28,r28,-0x60a0
8020000c 3c02af00 : lui r2,0xaf00
80200010 308400ff : andi        r4,r4,0x00ff
80200014 34450800 : ori r5,r2,0x0800
80200018 34430805 : ori r3,r2,0x0805
8020001c 90620000 : lbu r2,0(r3)
80200020 30420020 : andi        r2,r2,0x0020
80200024 1040fffd : beq r2,r0,0x8020001c
80200028 00000000 : nop
8020002c 03e00008 : jr  r31
80200030 a0a40000 : sb  r4,0(r5)
80200034 3c02af00 : lui r2,0xaf00
80200038 34440809 : ori r4,r2,0x0809
8020003c 24030001 : addiu       r3,r0,0x0001
80200040 a0830000 : sb  r3,0(r4)
80200044 34450803 : ori r5,r2,0x0803
80200048 a0800000 : sb  r0,0(r4)
8020004c 34460801 : ori r6,r2,0x0801

## Application terminated, rc = 0x0
#

4.電卓プログラムを作る

 最後に電卓プログラムを作りましょう.電卓プログラムのキーポイントはシリアル通信の1文字入力と1文字出力です.これらの関数は本誌掲載リスト2のプログラムの中で定義されています.これらを用いてprintf関数(もどき)やscanf関数(もどき)を作成して電卓プログラムを構築します.
 リストEに電卓プログラムの本体を示します.プログラム自体はそんなに大きなものではないのですぐ理解できるでしょう.ただ,関数の再帰呼び出しを多用していますので,プログラムを読むには慣れが必要です.プログラム中のgetchar関数やputchar関数の実体が本誌掲載リスト1の関数群です.詳しくは,リストEのcalc.cと同じフォルダにある,printf2.cやscanf.cといったソースファイルを参照してください.
 図Dに電卓プログラムの実行結果を示します.「->」というプロンプトに対して計算式を入力します(10進計算のみ).すると,その計算結果が次の行に表示されます.「q」を入力するとプログラムの実行が終了します.

リストE 電卓プログラム(calc.c)

図D リストEの実行結果
# loads
## Ready for S-Record download ...
....
## First Load Addr = 0x80200000
## Last  Load Addr = 0x80201E67
## Total Size      = 0x00001E68 = 7784 Bytes
## Start Addr      = 0x80200000
# go 80200000
## Starting application at 0x80200000 ...
# go 80200000
## Starting application at 0x80200000 ...
->1+2+3
= 6
->1-2-3
= -4
->1+2*3+4*5
= 27
->(1+2)*(3+4)-(4+3)*(2-1)
= 14
->q
= 0
## Application terminated, rc = 0x0
#