LoginSignup
1
0

Verilog-HDLのキャストで躓いた話

Last updated at Posted at 2023-12-17

この記事は、EEIC Advent Calender 2023 17日目の記事として書かれています。

目次

1.はじめに
2.Verilogにおけるキャストについて
3.signedって言ったのにキャストされない問題
4.おわりに

はじめに

EEIC3年のまっつんと言います。普段はTwitter(Xという名称は認めていません)でオタク話をしたり競馬で負けたりしています。

僕は後期実験で「FPGAを用いたアルゴリズム実装(課題12)」「マイクロプロセッサの設計と実装(課題6)」と2シーズン連続でVerilogを書く実験を選択し、その中で符号付き演算のために行うキャストの仕様に躓いたことが何回かあったので、備忘録的にこの記事にまとめました。普段からVerilogを書き慣れている方から見たら当たり前の内容と思いますが、温かい目で見ていただければ幸いです。

Verilogにおけるキャストについて

Cやpythonなどのソフトウェア言語でのキャストといえば、変数の型変換を指すのでした。これはつまり、「同じ値を別の形式のビット列で表現する」ということなので、キャスト前と後ではビット列の内容が変化します。例えば、値 "2" を表すint形の変数をfloat型にキャストすると、ビット列が32'h00000002から32'h40000000に変化します(ビット列はVerilog風に書いています)。

一方Verilogにおけるキャストとは、「ビット列が符号付きかどうかの解釈変更」です。例えば、

wire [3:0] unsigned_value = 4'b1101
wire signed [3:0] signed_value = 4'b1101

と書いたとき、unsigned_valueの値は普通に13と解釈されますが、signedと宣言したsigned_valueは符号付きの値と解釈されるため、2の補数表現を考えて値は-3となります。

また、演算の際に$signed(変数名)と宣言すると、その変数をsignedとして計算してくれます。

擬似コード
unsigned_calc <= unsigned_value * -2 // -26になって欲しい
signed_calc <= $signed(unsigned_value) * -2 // 6になって欲しい

ここでポイントとなるのが、「signedとunsignedを切り替えてもビット列自体に変化はなく、その解釈の仕方が変わるだけ」ということです。ソフトウェアでのキャストのノリで扱っているとそのうち脳がバグるので注意する必要があります。というかそうやって脳がバグったってのがここから先の話。

ちなみに、学生実験中の我々およびネット上の一部の文献はこのsignedとunsignedの切り替えの操作を「キャスト」と呼んでいたのですが、正式な用語として「キャスト」が用いられているという裏付けを取ることはできませんでした。なんかソフトウェア的な「キャスト」とはやってることが違うし、SystemVerilogには普通にintやfloat型があるので正式にはそっちの変換のことを「キャスト」って呼んでたりしそう。この辺の正式な用語について知ってる方はぜひ教えてください。

signedって言ったのにキャストされない問題

僕が遭遇した躓きを再現したのが次のモジュールです。

module signed_by_signed (
    input clk,
    input [7:0] unsigned_input //8'b11111001
);

    wire [7:0] signed_wire = $signed(unsigned_input); //事前にキャストしたつもり

    reg signed [7:0] signed_operand; //キャスト済み
    reg state; //表示用、気にしないで

    //結果格納用レジスタたち
    reg [7:0] raw;
    reg [15:0] raw_extended;
    reg [7:0] cast_before_calc;
    reg [15:0] cast_before_calc_extended;
    reg signed [15:0] cast_before_calc_extended_signed;
    reg [15:0] cast_and_calc_extended;

    initial begin
        signed_operand = 8'b11111110; // -2
        state = 0;
    end

    always @(posedge clk ) begin
        // -2 * -7 = 14 になれば成功
        raw <= signed_operand * unsigned_input; // キャストしない
        raw_extended <= signed_operand * unsigned_input; // キャストしない、ビット拡張
        cast_before_calc_extended <= signed_operand * signed_wire; // 事前にキャストしたつもり、ビット拡張
        cast_before_calc_extended_signed <= signed_operand * signed_wire; // ↑の結果をsignedに
        cast_and_calc_extended <= signed_operand * $signed(unsigned_input); // キャストしてから計算、ビット拡張
        state <= 1;
    end

    //表示
    always @(negedge clk ) begin
        if(state == 1) begin
            $display(" -2 * -7 = 14 になれば成功");
            $display("① キャストしない -> %b = %d", raw, raw);
            $display("② キャストしない、ビット拡張 -> %b = %d", raw_extended, raw_extended);
            $display("③ 事前にキャストしたつもり、ビット拡張 -> %b = %d", cast_before_calc_extended, cast_before_calc_extended);
            $display("④ ↑の結果をsignedに -> %b = %d", cast_before_calc_extended_signed, cast_before_calc_extended_signed);
            $display("⑤ キャストしてから計算、ビット拡張 -> %b = %d", cast_and_calc_extended, cast_and_calc_extended);
            //↑これ全部copilotが書いてくれた。かがくのちからってすげー!
        end
    end

endmodule

要はsigned同士の掛け算を実現したいというコードです。

これにテストベンチをつけて実行した出力が↓です。

 -2 * -7 = 14 になれば成功
① キャストしない -> 00001110 =  14
② キャストしない、ビット拡張 -> 1111011100001110 = 63246
③ 事前にキャスト、ビット拡張 -> 1111011100001110 = 63246
④ ↑の結果をsignedに -> 1111011100001110 =  -2290
⑤ キャストしてから計算、ビット拡張 -> 0000000000001110 =    14

正しい結果を得るまでに5段階も踏んでしまいました。順に見ていきましょう。

①では、8ビットの変数8'b111110018'b11111110を掛け合わせて、結果を8ビットに格納しています。その結果、望んだ値である14を得ることができました。そもそも2の補数表現は、符号のことを全く考えずにビット演算をしても正しい結果が得られるというのが嬉しかったので、これは当然の結果と言えます。

しかし、今回の実験では結果を倍精度で保持する必要があった場面がありました。そこで、①と同じ計算の結果を16ビットに格納したのが②でした。その結果、下位8ビットだけ見れば正しかった結果が、倍精度で見ると正しくない結果になっていることがわかりました。

Verilogの仕様上、符号を考慮した計算を行ってほしい場合は、計算に用いる変数が全てsignedとして宣言されていなければなりません。今回はunsigned_inputというunsignedの値が用いられているため、正しく解釈されないことになります。

じゃあunsigned_inputの方もキャストしてやろうということで、signed_wireという新しい変数を作り、そこにunsigned_inputの値をキャストしてから「代入」したのが③ですが、結果は変わりませんでした。

これは、(「代入」という言葉遣いからも分かるとおり)僕がまだソフトウェアの感覚でVerilogを書いていたことが原因でした。

wire [7:0] signed_wire = $signed(unsigned_input);

という記述は、「8桁のwiresigned_wireを作り、それをunsigned_inputと接続する」ということを表しています。また、$signedは先述の通り「値8'b11111001を持つunsigned_inputの解釈を変更する」という宣言に過ぎず、接続先のsigned_wireの値には一切影響を及ぼしません。signed_wireには値8'b11111001が入っており、signed_wireの宣言はunsignedとしてなされているため、signed_wireは符号付きの値 "-7" ではなく符号なしの値 "249" として解釈されてしまっています。(つまり、signed_wireの方をキャストしていれば "-7" として解釈してくれていたのですが、そのことに気づいたのはこの記事の作成準備中でした...。)

そこから迷走し、結果を格納するレジスタの方をsignedで宣言してみたりしても、1111011100001110というビット列の解釈が変わるだけなので正しい結果は得られません。最終的に、「計算に用いる変数が全てsignedとして宣言されている」条件を確実に満たすために、演算を宣言する式の中で`$signed'と宣言することでようやく正しい結果を得ることができました。

ちなみにこのデバッグは深夜にやっていたのですが、気づいた時に深夜テンションも相まっての脳汁が出まくったことでこの記事の執筆に至りました。もう1本書く予定の記事がふざけてるから真面目そうな記事で中和しておこうみたいなこと全く考えてないよ。

おわりに

長々と書いてしまいましたが、signedの演算をするときはオペランドを直接$signedでキャストするのが一番確実そうです。それはそうと、ソフトウェア脳のままVerilogを書いていると非本質的なところで沼りまくるので、皆さんも一緒にソフトウェア脳から脱却しましょう。

追記

一通り記事を書き終わってから気づいたのですが、「ハードウエア設計論以来Verilogを忘れている人が読んでもわかるようにしよう」と思ってそこそこ頑張って説明を書いたのに、よく考えたら後期実験でVerilog書いてた人たちには既知の内容だろうし、そうでない人はこれからもあんまりVerilog書く機会ないだろうからあんまり役に立たないし、1個下の後輩は今読んでもさっぱりだろうしで、誰に向けて書いたのかよくわからなくなってしまいました。でもめんどいのでこのまま公開します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0