加算命令の実装

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

今回は一気に3命令実装していきます。
加算のための命令たちで、ADD命令、ADC命令、ADIW命令です。

ADDI命令みたいなのがないことにはちょっと違和感があります。
減算にはSUBI命令というのがあるんですけどね。

ADD命令の実装

それでは早速実装していきます。
最初はADD命令からです。

ADD命令は汎用レジスタどうしの加算を行います。
加算の結果は一方の汎用レジスタに格納します。

それではいつものように「AVR®命令一式手引書」から命令の説明を引用します。

基本的な動作は単なる加算なので説明の必要はないでしょう。
複雑なのはやはりステータスレジスタの振る舞いですね。

今回初めてHフラグが出てきました。
これは下位ニブル(4ビット)からの繰り上がりがあったかどうかのフラグです。

加算器を使って1ビットずつ処理するなら簡単なんですけど、ソフトウェアでHフラグの振る舞いをエミュレートしようとすると大変です。
今回は素直に定義通りに実装することにしようと思います。

ほかのフラグも基本的には定義通りに実装しようと思います。

では、コードを見ていきます。

static void add(atmega328_t *cpu, uint16_t op)
{
  int d = (op >> 4) & 0x1f;
  int r = (op & 0xf) | (op >> 5) & 0x10;
  int rd = cpu->r[d];
  int rr = cpu->r[r];
  int value = rd + rr;
  cpu->r[d] = value;

  int sr = cpu->sr & ~(flag_H | flag_S | flag_V | flag_N | flag_Z | flag_C);
  int n = (value & 0x80) != 0;
  int v = (rd & 0x80) && (rr & 0x80) && !(value & 0x80) || !(rd & 0x80) && !(rr & 0x80) && (value & 0x80);

  if ((rd & 0x08) && (rr & 0x08) || (rr & 0x08) && !(value & 0x08) || !(value & 0x08) && (rd & 0x08))
    sr |= flag_H;
  if (n ^ v)
    sr |= flag_S;
  if (n)
    sr |= flag_N;
  if ((value & 0xff) == 0)
    sr |= flag_Z;
  if ((rd & 0x80) && (rr & 0x80) || (rr & 0x80) && !(value & 0x80) || !(value & 0x80) && (rd & 0x80))
    sr |= flag_C;
  cpu->sr = sr;

  ++cpu->clock;
}

ちょっと長くなりましたが、ほぼ仕様書の定義通りに実装しています。
本来ならCフラグvalueのビット16、つまり0x100とANDした結果が非ゼロなら1にすればいいんですが、今回はあえて定義通りに実装してみました。

ADC命令の実装

次はADC命令です。
ADC命令が基本的にはADD命令と同じなんですが、Cフラグもいっしょに加算します。
これによって、下位バイトからの繰り上がりを含めて上位バイトの加算を行うことができます。
ADC命令を使うことで、8ビットに限らず、16ビットやそれ以上の加算ができるようになります。

では、こちらも「AVR®命令一式手引書」から命令の説明を引用しながら説明していきます。

ちょっと違和感があったんですけど、ステータスレジスタの振る舞いがADD命令と同じなんですね。
本来ならCフラグの状態も考慮して、ステータスレジスタに格納されるフラグの状態が決まると思うのですが、実際にはそうはなっていないようです。

では、コードを見ていきます。

static void adc(atmega328_t *cpu, uint16_t op)
{
  int d = (op >> 4) & 0x1f;
  int r = (op & 0xf) | (op >> 5) & 0x10;
  int rd = cpu->r[d];
  int rr = cpu->r[r];
  int value = rd + rr + ((cpu->sr & flag_C) != 0);
  cpu->r[d] = value;

  int sr = cpu->sr & ~(flag_H | flag_S | flag_V | flag_N | flag_Z | flag_C);
  int n = (value & 0x80) != 0;
  int v = (rd & 0x80) && (rr & 0x80) && !(value & 0x80) || !(rd & 0x80) && !(rr & 0x80) && (value & 0x80);

  if ((rd & 0x08) && (rr & 0x08) || (rr & 0x08) && !(value & 0x08) || !(value & 0x08) && (rd & 0x08))
    sr |= flag_H;
  if (n ^ v)
    sr |= flag_S;
  if (n)
    sr |= flag_N;
  if ((value & 0xff) == 0)
    sr |= flag_Z;
  if ((rd & 0x80) && (rr & 0x80) || (rr & 0x80) && !(value & 0x80) || !(value & 0x80) && (rd & 0x80))
    sr |= flag_C;
  cpu->sr = sr;

  ++cpu->clock;
}

Cフラグの状態にかかわらず、ステータスレジスタの振る舞いが同じであればほとんどADD命令と変わりません。
共通部分は多いですが、あえてコピペで対応することにします。

ADIW命令の実装

ADIW命令は、汎用レジスタ2つを組み合わせた16ビットのレジスタ対に定数を加算する命令です。

これについても「AVR®命令一式手引書」から命令の説明を引用しながら説明していきます。

加算対象の汎用レジスタを指定するビットは2ビットしかありませんので、R24, R26, R28, R30のどれかしか選べません。
また、加算する定数は0~63の範囲になります。

ステータスレジスタの振る舞いもそこそこ複雑ですが、Hフラグが変化しないので少し楽かもしれませんね。

では、コードを見ていきますね。

static void adiw(atmega328_t *cpu, uint16_t op)
{
  int d = 24 + ((op >> 3) & 0x6);
  int K = (op & 0xf) | (op >> 2) & 0x30;
  int rdl = cpu->r[d];
  int rdh = cpu->r[d + 1];
  int value = (rdh << 8 | rdl) + K;
  cpu->r[d] = value & 0xff;
  cpu->r[d + 1] = (value >> 8) & 0xff;

  int sr = cpu->sr & ~(flag_H | flag_S | flag_V | flag_N | flag_Z | flag_C);
  int n = (value & 0x8000) != 0;
  int v = !(rdh & 0x80) && (value & 0x8000);

  if (n && v)
    sr |= flag_S;
  if (v)
    sr |= flag_V;
  if (n)
    sr |= flag_N;
  if ((value & 0xffff) == 0)
    sr |= flag_Z;
  if (!(value & 0x8000) && (rdh & 0x80))
    sr |= flag_C;
  cpu->sr = sr;

  cpu->clock += 2;
}

これも定義通りに実装しただけです。
効率を考えれば改善の余地はたくさんあると思います。

op_tableへの登録

次に、いつものようにop_tableに登録します。

opcode.phpに次のコードを追加して実行してあげればOKです。

for ($i = 0; $i < 0x400; ++$i)
{
  $opcode_table[0b0000_1100_0000_0000 | $i] = 'add';
  $opcode_table[0b0001_1100_0000_0000 | $i] = 'adc';
}

for ($i = 0; $i < 0x100; ++$i)
{
  $opcode_table[0b1001_0110_0000_0000 | $i] = 'adiw';
}

今回は 65,536 命令のうち 1,024 × 2 + 256 = 2,304 個が埋まりました。

それでは次回またお会いしましょう!