ぐだぐだ低レベルプログラミング(39) RISC-V、mul、RV32M拡張その2

Joseph Halfmoon

前回は、RISC-Vの乗算命令と除算命令を1命令づつ動かしてみました。今回は 乗算命令 の全貌を見てみます。といっても4命令ですが。実装がシンプルになるように考慮した結果でしょうが、命令の使用方法はクセが強い、気がします(個人の感想です。)それにnucleriのツールチェーンにも制限が?

まずは毎度の参照資料へのリンクの再掲載です。

  • RISC-Vの公式ドキュメンテーションはこちら
  • 実験に使用しているGD32VF103VBT6開発ボードはこちら

なお、ビルドはVSCode+PlatformIOで行っていますが、PlatformIOは上記の開発ボードがターゲットボードには含まれていません。ターゲットボードは、Longan Nano、Frameworkは GigaDevice GD32V-SDKとして行っています。ぶちゃけ全端子が使えるかわりディスプレイが無いLongan Nanoと謀って?いる感じ。デバッガはSipeedの「お求めやすい」RISC-V Debugger(実体はFT)使っています。

RV32Mの乗算命令

くだくだと言葉で説明するのが面倒になって図にしました。冒頭のアイキャッチ画像に掲げましたのが、「符号付き」掛ける「符号付き」の場合です。

以下が「符号なし」掛ける「符号なし」

mul_unsignedそして「符号付き」掛ける「符号なし」です。

mul_us以下原則です。

  • レジスタ上の32ビット整数同士の演算
  • mul命令で下32ビットの結果が得られる
  • 上32ビットの結果は、上記のように使い分ける必用がある

32ビット数同士を乗算すると結果は64ビットになります。多くのアーキテクチャでは、倍幅になる乗算結果を格納するために返り値用のレジスタを隣同士の2個とれる、といった塩梅にしています。しかしRISC-Vでは、1命令でレジスタ2個書くのは人生複雑にするだけだ?(人生をハードウエアと置き換えてくださいませ)ということで排除しているみたいです。

実験用のアセンブラ関数

全4命令を1命令づつシミジミ味わうために1命令づつの関数といたしました。前回と違い実行サイクル数の測定などはしないので、スタックフレームも用意せず、ただ呼んで、計算して、戻るだけのシンプルすぎるもの

.section    .text
.align      2
.globl      mulLOW, mulHSS, mulHUU, mulHSU

mulLOW:
    mul     a0, a1, a2
    ret

mulHSS:
    mulh    a0, a1, a2
    ret

mulHUU:
    mulhu   a0, a1, a2
    ret

mulHSU:
    mulhsu  a0, a1, a2
    ret
アセンブラ関数の呼び出し側ソース

まずは、アセンブラ関数呼び出し用のヘッダファイルに追加した部分がこちら。

uint32_t mulLOW(uint32_t a0, uint32_t a1, uint32_t a2);
uint32_t mulHSS(uint32_t a0, uint32_t a1, uint32_t a2);
uint32_t mulHUU(uint32_t a0, uint32_t a1, uint32_t a2);
uint32_t mulHSU(uint32_t a0, uint32_t a1, uint32_t a2);

typedef struct {
    uint32_t Low;
    uint32_t High;
} HighLowWord;

typedef union {
    uint64_t U64;
    int64_t  I64;
    HighLowWord Work;
} MULTST64;

ABI的には、a0, a1 の2つのレジスタを使えば 64ビットの数値を戻せることになっています。1関数でフル64ビットの結果を戻すならそうすべきでしょう。しかし今回は、32ビット毎に「味わう」ために戻り値は32ビット縛りです。引数、戻り値ともに符号付きになったり、符号無しになったりします。簡単のため、関数プロトタイプはすべて unsigned といたしました。signedで扱う場合は、Cの方で無理やりキャストしてね、という割り切り(これは当方のメンドイ病)?

形式上32ビット符号無で戻ってくる値2つをくつけて64ビットの符号無だったり、符号付きだったりに「戻す」ために、上記のように構造体を内部に含む共用体を定義してみました。

しかし、printfする段になって愕然。64ビット符号あり、64ビット符号無のためにはフォーマット指定子 %lld、%llu を使えば良いと思ったのですが、この指定子使えなかったです(ldとかとぼけたことを出力します。)これは nucleiのツールチェーンの実装の問題?組み込み用途だと64bit整数などまず出てこないし手を抜いたのかなあ。別な方法あるのかなあ。

良く分からなかったので、今回は、とりあえず32ビット毎に16進で出力して「お茶」を濁しましたです。カッコ悪いな。main()関数に追加した部分がこちら。

int32_t  a0i, a1i, a2i;
uint32_t a0u, a1u, a2u, result;
MULTST64 temp;

a0i = 0; a1i = 123; a2i = -3;
result = mulLOW((uint32_t)a0i, (uint32_t)a1i, (uint32_t)a2i);
printf(" 123 x -3  = %d\n", (int32_t)result);
a0i = 0; a1i = -123; a2i = -3;
result = mulLOW((uint32_t)a0i, (uint32_t)a1i, (uint32_t)a2i);
printf("-123 x -3 = %d\n", (int32_t)result);
a0u = 0; a1u = 123; a2u = 3;
result = mulLOW(a0u, a1u, a2u);
printf(" 123 x  3 = %u\n", result);

// 6,442,450,941 or 0x1_7FFF_FFFD
// -6,442,450,941 or 0xFFFF_FFFE_8000_0003
a0i = 0; a1i = 2147483647; a2i = -3;
temp.Work.Low  = mulLOW((uint32_t)a0i, (uint32_t)a2i, (uint32_t)a1i);
temp.Work.High = mulHSU((uint32_t)a0i, (uint32_t)a2i, (uint32_t)a1i); 
printf("0xFFFF_FFFE_8000_0003> H: 0x%08x L: 0x%08x\n", temp.Work.High, temp.Work.Low);
a0i = 0; a1i = -2147483647; a2i = -3;
temp.Work.Low  = mulLOW((uint32_t)a0i, (uint32_t)a1i, (uint32_t)a2i);
temp.Work.High = mulHSS((uint32_t)a0i, (uint32_t)a1i, (uint32_t)a2i); 
printf("0x0000_0001_7FFF_FFFD> H: 0x%08x L: 0x%08x\n", temp.Work.High, temp.Work.Low);
a0u = 0; a1u = 2147483647; a2u = 3;
temp.Work.Low  = mulLOW(a0u, a1u, a2u);
temp.Work.High = mulHSS(a0u, a1u, a2u); 
printf("0x0000_0001_7FFF_FFFD> H: 0x%08x L: 0x%08x\n", temp.Work.High, temp.Work.Low);
実機(GD32VF103)上の実行結果

最初の3つが32ビット範囲に収まるもの。3種組わせ。後の3つが64ビットに「はみ出す」もの。先ほどの理由で16進でみずらいです。

LOOP : 1
123 x -3 = -369
-123 x -3 = 369
123 x 3 = 369
0xFFFF_FFFE_8000_0003> H: 0xfffffffe L: 0x80000003
0x0000_0001_7FFF_FFFD> H: 0x00000001 L: 0x7ffffffd
0x0000_0001_7FFF_FFFD> H: 0x00000001 L: 0x7ffffffd

予定通りの結果で良かった。かったるいけれども次は除算だな。

ぐだぐだ低レベルプログラミング(38) RISC-V、mulとdiv、RV32M拡張その1 へ戻る