汎用レジスタファイル

こんにちは、めのんです!

前回出てきたデータメモリにも少し現れましたが、今回は汎用レジスタファイルについて考えていきます。
念のためデータメモリのメモリマップを再掲載しますね。

一番上(アドレスとしては下位)に「レジスタファイル」というのがありますね。
これが今回扱う「汎用レジスタファイル」のことです。

レジスタとは?

CPUには「レジスタ」という記憶素子が備わっています。
レジスタは値や状態を記憶するためのもので変数に似ています。
事実、Cのローカル変数はレジスタに割り付けられることもあります。

レジスタには目的によっていろんな種類のものがあります。
プログラムが今どの部分を実行しているかを保持するプログラムカウンタ、スタックという仕掛けに使うスタックポインタなどがあります。

今回考えるのは「汎用レジスタ」です。
読んで字のごとく汎用的な用途に使うレジスタです。

メモリからデータを読み込む、メモリにデータを書き込む、さまざまな演算を行うといったことは汎用レジスタを使って行うことになります。

どんなCPUにも汎用レジスタがあるかというとそうではなく、アキュムレータやインデックスレジスタなど、目的別に専用のレジスタが用意されているCPUもあります。

汎用レジスタが備わっているCPUは「RISC(Reduced Instruction Set Computer)」という分類のCPUに多く見られます。
今回、命令セットシミュレータを作ろうとしているAVRもそんなRISCの一種です。

AVRの汎用レジスタ

AVRにはR0~R31の汎用レジスタが32個備わっています。
AVRは8ビットのマイコンですので、各汎用レジスタも8ビットです。

上の図のように、AVRの汎用レジスタにはアドレスが振られています。
これはどういうことかというと、汎用レジスタをメモリと同じように扱うことができるということです。

どんなCPUでもこんな風にレジスタにアドレスが振られているかというとそんなことはなくて、このような仕様は比較的ローエンドのマイコンに多いようです。
レジスタにアドレスが振られていないCPUの方がおそらく多数派だと思います。

ところで、R26~R31の右側にXレジスタ、Yレジスタ、Zレジスタが書かれていますね。
それらは別のレジスタではなくて、R26とR27のペアをXレジスタ、R28とR29のペアをYレジスタ、R30とR31のペアをZレジスタとして扱うことができるということを表しています。

Xレジスタの場合はR26が下位側8ビットを、R27が上位側8ビットを表します。
YレジスタやZレジスタも同様に、汎用レジスタの番号が若い方が下位側になります。

これらX, Y, Zの3つのレジスタを「アドレスレジスタ」と呼びます。
データにアクセスする際にアドレスを表すために使うレジスタということです。

アドレスレジスタは汎用レジスタと同じ素子を共有していますので、どちらかを更新すれば、それにつられて他方も変化してしまいます。
たとえば、R26レジスタに0x34、R27レジスタに0x12という値を設定すれば、Xレジスタの値が0x1234に変化することになります。

命令セットシミュレータでの扱い

それでは、この汎用レジスタを命令セットシミュレータでどのように扱うかを考えていきたいと思います。

普通に考えてR0~R31のように連番が振られているのですから、配列として持つのが一番だと思います。

uint8_t r[32];  // 汎用レジスタファイル

uint8_t型というのは8ビットの符号無し整数型のことです。
Cではこんな感じでサイズごとに整数型が定義されています。

ところで、汎用レジスタだけなら上のような定義でいいのですが、アドレスレジスタのことまで考えると不十分ですね。
しかも、アドレスレジスタは汎用レジスタと同じ素子を共有していますので、そのことを表現しないといけません。

Cには同じ領域を共有するための「共用体」という機能があります。
共用体はunionを使って、次のように書くことができます。

union general_registers_t
{
  uint8_t r[32];
  struct
  {
    uint8_t _[26];
    uint16_t x;
    uint16_t y;
    uint16_t z;
  };
};

ちょっと複雑なので順に説明していきますね。

上のコードではunion general_registers_t型という共用体を定義しています。
共用体ですので同じ領域に複数のデータを配置しています。

具体的には、配列r[32]と、そのすぐあとに書いているstructで始まる構造体を同じ領域に配置しています。

構造体の中には、配列_[26]と、x, y, zというメンバが書かれていますね。
配列_[26]は位置を合わせるための詰め物です。
x, y, zをr[26]~r[31]と同じ領域に重ねないといけませんからね。

そして、x, y, zがそれぞれXレジスタ、Yレジスタ、Zレジスタということになります。
uint16_t型は16ビットの符号無し整数型のことです。

ところで、何気なしにx, y, zを定義してしまいましたが、ここでひとつ注意しないといけないことがあります。

アドレスレジスタは、対応する汎用レジスタの番号が若い方が下位側になるんでしたね。
この要件を満たすためには、命令セットシミュレータに使うCの処理系が「リトルエンディアン」でなければなりません。

リトルエンディアンというのは、複数バイトからなるデータをメモリ上に配置するとき、下位側から先に配置する方式のことです。
逆に上位側から先に配置する方式を「ビッグエンディアン」と呼びます。

プログラムを実行するコンピュータによってリトルエンディアンかビッグエンディアンかが変わってきます。
といっても、現在ではリトルエンディアンが主流のようで、ビッグエンディアンのマシンに遭遇することはあまりなくなりました。

IntelのCPUが搭載されたPCもリトルエンディアンです。
ARMはバイエンディアンといって、リトルエンディアンとビッグエンディアンを切り替えられるのですが、PCではリトルエンディアンを選択していることが多いようです。

こんな感じで、命令セットシミュレータの実装に使うC処理系にはいくつもの制約が出てきそうです。
次回はC処理系に対する要件を考えてみたいと思います。
どうぞご期待ください!