0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Shrike-LiteでAtCoder問題を解く(13):ABC467A - さっそくSPIテンプレートV2を使ってみる

0
Posted at

はじめに

前回は、SPIで1byteを受信したことを示すrx_data_strobeを共通化し、SPIテンプレートV2を作成しました。

今回は、そのSPIテンプレートV2をさっそく使って、AtCoder Beginner Contest 467のA問題をShrike-Liteへ実装します。

問題も回路もシンプルですので、今回はテンプレートをコピーして実装し、実機で動作を確認するところまで進めます。


この連載での役割分担と縛り(更新版: メモリに関する項目追加)

第6回の記事でも触れましたが、この連載でのRP2040とFPGAの役割分担を改めて明示しておきます。

第10回でBRAMを利用できるようになりましたので、問題を解くために使用できる記憶領域についても、ここで縛りルールを決めておきましょう。

問題を解くためにRP2040のSRAMを使ってよいか少し悩みましたが、これもShrike-Lite上の資源です。FPGAから発行された読み書きリクエストに従って使用する場合に限り、利用してよいことにします。

以下が、ここからの新しいルールブックです。

この連載では、Shrike-LiteをOut-of-the-box、つまり購入時の構成のまま使用します。

  • Shrike-Liteへジャンパケーブル、外付け回路、追加のハードウェアを接続しない
  • RP2040とFPGA間の通信には、Shrike-Lite基板上にあらかじめ用意された接続だけを利用する
  • 開発は、Thonny/MicroPythonとForgeFPGA Workspaceを用いてできる範囲に限定する
  • テストケースはMicroPythonスクリプトへ埋め込み、外部PCからUSBシリアル通信などで逐次入力しない
  • RP2040では、入力データの選別、並べ替え、反転など、問題を解くためのデータ変換を行わない
  • RP2040側でデータを置き換えてよいのは、固有名詞やYesNoなどを通信用の数値表現へ変換し、また元の表現へ戻す場合だけとする
  • 問題を解くための記憶領域として、FPGA上のBRAMやRP2040のSRAM上に確保した領域を利用してよい
  • RP2040のSRAMを問題を解くための記憶領域として利用する場合は、FPGAが発行する読み書きリクエストに従ってアクセスする
  • この場合、読み書きするアドレス、データ、操作内容はFPGA側が指定し、RP2040側では問題の内容に基づいて読み書き対象を選択しない

RP2040が担当するのは、上記の縛りを破らない範囲で、テストケースの保持、FPGAとの通信、FPGAから返された結果と期待値の比較、標準出力へのテスト結果表示などです。

外部PC上のThonnyはプログラムの実行や結果確認に使用しますが、問題データの逐次入力や一時保存には使用しません。

問題を解くための計算や判定、およびRP2040のSRAMへ読み書きする対象の決定は、FPGA側で行います。


ABC467Aの問題

身長H[cm]と体重W[kg]が与えられます。

BMIが25以上ならYes、25未満ならNoを出力する問題です。

制約は次のとおりです。

1 <= H <= 300
1 <= W <= 300

HWは、どちらも9bitで表現できます。


解法を整数の比較へ変換する

BMIの定義をそのまま使うと、身長をcmからmへ変換したうえで除算を行う必要があります。

今回は、次のように式を変形します。

W / (H / 100)^2 >= 25

10000W >= 25H^2

400W >= H^2

したがって、FPGAでは次の条件だけを判定すれば十分です。

400 * W >= H * H なら Yes
それ以外なら No

最大値は次のようになります。

H^2   <= 300^2   = 90000
400W  <= 400*300 = 120000

どちらも17bitに収まります。実装では、演算時のビット幅を明示するため18bitの信号として扱います。

浮動小数点数や除算は使わず、乗算と整数比較だけで実装します。


RP2040とFPGAの役割分担

今回の役割分担は次のようにします。

処理 担当
テストケースからHWを取り出す RP2040
9bit値を2byteへ分割して送信する RP2040
400W >= H^2を判定する FPGA
判定結果を1bitで返す FPGA
YesまたはNoへ変換し、期待値と比較する RP2040

RP2040では問題の判定を行わず、入力値の分割、SPI通信、結果表示だけを担当します。


SPI MOSIのフレーム構成

MOSIは、SPIテンプレートと同じく上位3bitをコマンド、下位5bitをデータとして使用します。

bit 7           bit 5 bit 4                 bit 0
+--------------------+----------------------------+
|    COMMAND[2:0]    |          DATA[4:0]         |
+--------------------+----------------------------+

HWは9bitですので、それぞれを上位4bitと下位5bitへ分割します。

1個の9bit値
  1byte目:bit8:5
  2byte目:bit4:0

HWを合わせると、入力データの送信は合計4byteです。

今回使用するコマンドは次のとおりです。

コマンド DATAの内容
NOP 000 00000
SEND_H_HI 001 0 + H[8:5]
SEND_H_LO 010 H[4:0]
SEND_W_HI 011 0 + W[8:5]
SEND_W_LO 100 W[4:0]
DEBUG 110 通常処理では未使用
RESET 111 00000

MicroPython側では、9bit値を次のように分割できます。

upper = (value >> 5) & 0x0F
lower = value & 0x1F

送信byteは、これまでと同じ式で作成します。

tx = (command << 5) | data

SPI MISOのフレーム構成

MISOは、bit7をVALID、bit0をANSWERとして使用します。

bit 7 bit 6                           bit 1 bit 0
+--------+----------------------------------+--------+
| VALID  |             reserved             | ANSWER |
+--------+----------------------------------+--------+
MISO 意味
0x81 有効な返信、答えはYes
0x80 有効な返信、答えはNo
0x00 有効な返信なし

SPI通信シーケンス

1ケースの通信は次の順番で行います。

RESET
NOP
SEND_H_HI
SEND_H_LO
SEND_W_HI
SEND_W_LO
NOP          ← VALIDとANSWERを読み出す
RESET
NOP

FPGAはSEND_W_LOを受信した時点で、受信中の下位5bitを含むWを使って判定し、MISO送信用の値を準備します。

これにより、その次のNOPで判定結果を読み出せます。


SPIテンプレートV2をコピーする

前回作成したatcoder_spi_template_v2をコピーし、ABC467A用のプロジェクトを作成します。

abc467a/
├── abc467a.ffpga
├── bitstream/
│   └── abc467a.bin
├── ffpga/
│   └── src/
│       ├── main.v
│       └── spi_target.v
└── firmware/
    └── micropython/
        └── abc467a_test.py

今回、spi_target.vは変更しません。

main.vの問題固有部分では、V2で追加したrx_data_strobeをそのまま使用します。

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        // 初期化
    end else if (rx_data_strobe) begin
        // 受信した1byteを1回だけ処理
    end
end

各問題のmain.vへ受信信号の立上り検出回路を追加する必要はありません。


AIへVerilog実装を依頼する

main.vの実装は、AIへ次のように依頼します。

Shrike-LiteでABC467A - Obesityを実装するため、
atcoder_spi_template_v2のmain.vを元に、
abc467a用のmain.vを作成してください。

spi_target.vは変更しないでください。
Verilogで実装し、SystemVerilog固有構文は使用しないでください。

問題の入力はHとWです。

- 1 <= H <= 300
- 1 <= W <= 300
- Hは9bitで保持する
- Wは上位4bitだけを保持する

判定条件は次のとおりです。

    400 * W >= H * H ならANSWER=1
    それ以外ならANSWER=0

計算途中の値は18bitで扱ってください。

MOSIはbit7:5をCOMMAND、bit4:0をDATAとします。

コマンド:
- NOP       = 3'b000
- SEND_H_HI = 3'b001
- SEND_H_LO = 3'b010
- SEND_W_HI = 3'b011
- SEND_W_LO = 3'b100
- DEBUG     = 3'b110
- RESET     = 3'b111

受信方法:
- SEND_H_HIのDATA[3:0]をH[8:5]へ保存
- SEND_H_LOのDATA[4:0]をH[4:0]へ保存
- SEND_W_HIのDATA[3:0]をWの上位4bitとして保存
- SEND_W_LOのDATA[4:0]は保存しない

SEND_W_LOを受信したクロックで、保存済みのW上位4bitと
今回受信したDATA[4:0]を組み合わせてWを作り、
判定結果をtx_dataへ保存してください。

MISOは次の形式です。

- bit7: VALID
- bit6:1: 0
- bit0: ANSWER
- Yesは0x81
- Noは0x80
- 有効な返信なしは0x00

RESETではH、Wの上位4bit、tx_dataを0へ初期化してください。
問題固有処理はrx_data_strobeを条件に1回だけ実行してください。

main.vを全文提示してください。

今回も記事とコードの草稿作成にはAIを使用しています。


Verilogコード例(問題固有部分)

    // ===== 問題ごとに変更する部分 =====
    // ABC467Aでは、HとWをそれぞれ2byteで受信する。
    // SEND_W_LOを受信したとき、保存済みの上位4bitと
    // 受信中の下位5bitからWを組み立て、浮動小数点演算を使わず、
    // 400 * W >= H * Hで肥満かどうかを判定する。
    localparam NOP       = 3'b000;
    localparam SEND_H_HI = 3'b001;
    localparam SEND_H_LO = 3'b010;
    localparam SEND_W_HI = 3'b011;
    localparam SEND_W_LO = 3'b100;
    localparam DEBUG     = 3'b110;
    localparam RESET     = 3'b111;

    reg [8:0] h;
    reg [3:0] w_hi;

    wire [8:0]  completed_w;
    wire [17:0] h_operand;
    wire [17:0] w_operand;
    wire [17:0] h_squared;
    wire [17:0] w_times_400;

    // Wの下位5bitは保持せず、今回受信中の下位5bitを使ってWを組み立てる。
    assign completed_w = {w_hi, rx_data[4:0]};
    assign h_operand = {9'b0, h};
    assign w_operand = {9'b0, completed_w};
    assign h_squared = h_operand * h_operand;
    assign w_times_400 = 18'd400 * w_operand;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            h       <= 9'd0;
            w_hi    <= 4'd0;
            tx_data <= 8'h00;
        end else if (rx_data_strobe) begin
            case (rx_data[7:5])
                NOP: begin
                    tx_data <= 8'h00;
                end

                SEND_H_HI: begin
                    h[8:5] <= rx_data[3:0];
                end

                SEND_H_LO: begin
                    h[4:0] <= rx_data[4:0];
                end

                SEND_W_HI: begin
                    // Wの上位4bitを保存する。
                    w_hi <= rx_data[3:0];
                end

                SEND_W_LO: begin
                    // 保存済みの上位4bitと今回受信した下位5bitを組み合わせて判定する。
                    // 判定結果は次のSPI通信で返信する。
                    if (w_times_400 >= h_squared) begin
                        tx_data <= 8'h81;
                    end else begin
                        tx_data <= 8'h80;
                    end
                end

                DEBUG: begin
                    // デバッグコマンドではレジスタを変更しない。
                end

                RESET: begin
                    h       <= 9'd0;
                    w_hi    <= 4'd0;
                    tx_data <= 8'h00;
                end

                default: begin
                    // 未定義コマンドではレジスタを変更しない。
                end
            endcase
        end
    end

AIへMicroPython実装を依頼する

次に、実機テスト用のabc467a_test.pyを作成します。

Shrike-LiteでABC467AをテストするMicroPythonプログラムを作成してください。

atcoder_spi_template_v2_test.pyを参考にして、
abc467a_test.pyを新規作成してください。

bitstream名はabc467a.binです。

SPI設定、ピン設定、FPGAリセット処理、
1byteのSPI送受信処理は既存テンプレートと同じにしてください。

HとWは9bit値として扱い、それぞれ次の2byteへ分割してください。

- 上位byteのDATA:value[8:5]
- 下位byteのDATA:value[4:0]

コマンド:
- NOP       = 0b000
- SEND_H_HI = 0b001
- SEND_H_LO = 0b010
- SEND_W_HI = 0b011
- SEND_W_LO = 0b100
- DEBUG     = 0b110
- RESET     = 0b111

1ケースの通信順序:
1. SEND_H_HI
2. SEND_H_LO
3. SEND_W_HI
4. SEND_W_LO
5. NOPを送信してMISOを受信
6. RESET
7. NOP

MISOはbit7がVALID、bit0がANSWERです。

- 0x81ならYes
- 0x80ならNo
- VALIDが0ならNO_REPLYとしてFAIL

次のテストケースを実行してください。

- (180, 60, "No")
- (182, 188, "Yes")
- (180, 80, "No")
- (180, 81, "Yes")
- (200, 99, "No")
- (200, 100, "Yes")
- (300, 224, "No")
- (300, 225, "Yes")
- (1, 1, "Yes")
- (300, 300, "Yes")

各ケースについてNAME、H、W、TX、RX、VALID、EXPECT、RESULT、
PASSまたはFAIL、処理時間を1行で表示してください。

最後にPASS数、FAIL数、TOTAL、全体の処理時間を1行で表示してください。
1件がFAILになっても残りのテストを続けてください。

MicroPythonコード例 (問題固有部分)

# ===== 問題ごとに変更する部分:ABC467Aの入力と期待値 =====
# HとWをそれぞれ上位4bit、下位5bitに分割してFPGAへ送信する。
# 期待結果はテストケースへあらかじめ埋め込み、
# MicroPython側ではABC467Aの判定を行わない。

# ===== ABC467A固有処理 =====
NOP = 0b000
SEND_H_HI = 0b001
SEND_H_LO = 0b010
SEND_W_HI = 0b011
SEND_W_LO = 0b100
DEBUG = 0b110
RESET = 0b111


# 期待結果はあらかじめ埋め込み、MicroPython側では問題の判定を行わない。
TEST_CASES = [
    ("case_no_1",          180,  60, "No"),
    ("case_yes_1",         182, 188, "Yes"),
    ("boundary_180_below", 180,  80, "No"),
    ("boundary_180_equal", 180,  81, "Yes"),
    ("boundary_200_below", 200,  99, "No"),
    ("boundary_200_equal", 200, 100, "Yes"),
    ("boundary_300_below", 300, 224, "No"),
    ("boundary_300_equal", 300, 225, "Yes"),
    ("minimum_values",       1,   1, "Yes"),
    ("maximum_values",     300, 300, "Yes"),
]


def make_command(command, data=0):
    # DATAは必ず下位5bitへ制限する。
    return (command << 5) | (data & 0x1F)


def split_9bit(value):
    # 9bit値を上位4bitと下位5bitへ分割する。
    return (value >> 5) & 0x0F, value & 0x1F


def decode_reply(rx):
    # bit7のVALIDを確認し、bit0のANSWERをYesまたはNoへ変換する。
    valid = (rx >> 7) & 0x01
    if valid == 0:
        return valid, "NO_REPLY"

    answer = rx & 0x01
    return valid, "Yes" if answer == 1 else "No"


def run_test_case(name, h, w, expected):
    h_hi, h_lo = split_9bit(h)
    w_hi, w_lo = split_9bit(w)
    tx_bytes = [
        make_command(SEND_H_HI, h_hi),
        make_command(SEND_H_LO, h_lo),
        make_command(SEND_W_HI, w_hi),
        make_command(SEND_W_LO, w_lo),
    ]

    # SEND_H_HI直前から、結果を受信するNOPの完了までを測定する。
    start_us = time.ticks_us()
    spi_exchange(tx_bytes[0])
    spi_exchange(tx_bytes[1])
    spi_exchange(tx_bytes[2])
    spi_exchange(tx_bytes[3])
    rx = spi_exchange(make_command(NOP))
    elapsed_us = time.ticks_diff(time.ticks_us(), start_us)

    valid, result = decode_reply(rx)
    passed = valid == 1 and result == expected
    status = "PASS" if passed else "FAIL"

    # 次のテストケースに備えてFPGA側の保持値と返信値を初期化する。
    spi_exchange(make_command(RESET))
    spi_exchange(make_command(NOP))

    tx_text = "[0x{:02X},0x{:02X},0x{:02X},0x{:02X}]".format(
        tx_bytes[0],
        tx_bytes[1],
        tx_bytes[2],
        tx_bytes[3]
    )
    print(
        "NAME={} H={} W={} TX={} RX=0x{:02X} VALID={} EXPECT={} "
        "RESULT={} {} TIME_US={}".format(
            name,
            h,
            w,
            tx_text,
            rx,
            valid,
            expected,
            result,
            status,
            elapsed_us
        )
    )

    return passed


# 最初の通信でFPGA側の保持値と返信値を初期化する。
spi_exchange(make_command(RESET))
spi_exchange(make_command(NOP))

pass_count = 0
total_start_us = time.ticks_us()

for name, h, w, expected in TEST_CASES:
    if run_test_case(name, h, w, expected):
        pass_count += 1

total_time_us = time.ticks_diff(time.ticks_us(), total_start_us)
fail_count = len(TEST_CASES) - pass_count

print(
    "SUMMARY PASS={} FAIL={} TOTAL={} TOTAL_TIME_US={}".format(
        pass_count,
        fail_count,
        len(TEST_CASES),
        total_time_us
    )
)

Verilogを合成する

main.vを保存したら、ForgeFPGA Workspaceで合成とbitstream生成を行います。

Synthesize
↓
Generate Bitstream

生成したbitstreamは、次の名前へ変更します。

abc467a.bin

合成結果

合成後のリソースレポートは次の通りでした。

Shrike-LiteでAtCoder問題を解く(13)_001.png

18bitの乗算を使用しているため、単純な比較だけを行ったこれまでの問題よりもLUT使用量が増えたようですね。それでもLUT使用率は17.5%で、まだ十分に余裕があります。


実機で動作確認する

abc467a.binabc467a_test.pyをShrike-Liteへ配置し、Thonnyからテストを実行します。

テスト結果

[shrike_fpga] flashing: abc467a.bin
[shrike_flash] FPGA programming done.
NAME=case_no_1 H=180 W=60 TX=[0x25,0x54,0x61,0x9C] RX=0x80 VALID=1 EXPECT=No RESULT=No PASS TIME_US=796
NAME=case_yes_1 H=182 W=188 TX=[0x25,0x56,0x65,0x9C] RX=0x81 VALID=1 EXPECT=Yes RESULT=Yes PASS TIME_US=902
NAME=boundary_180_below H=180 W=80 TX=[0x25,0x54,0x62,0x90] RX=0x80 VALID=1 EXPECT=No RESULT=No PASS TIME_US=886
NAME=boundary_180_equal H=180 W=81 TX=[0x25,0x54,0x62,0x91] RX=0x81 VALID=1 EXPECT=Yes RESULT=Yes PASS TIME_US=902
NAME=boundary_200_below H=200 W=99 TX=[0x26,0x48,0x63,0x83] RX=0x80 VALID=1 EXPECT=No RESULT=No PASS TIME_US=865
NAME=boundary_200_equal H=200 W=100 TX=[0x26,0x48,0x63,0x84] RX=0x81 VALID=1 EXPECT=Yes RESULT=Yes PASS TIME_US=883
NAME=boundary_300_below H=300 W=224 TX=[0x29,0x4C,0x67,0x80] RX=0x80 VALID=1 EXPECT=No RESULT=No PASS TIME_US=865
NAME=boundary_300_equal H=300 W=225 TX=[0x29,0x4C,0x67,0x81] RX=0x81 VALID=1 EXPECT=Yes RESULT=Yes PASS TIME_US=862
NAME=minimum_values H=1 W=1 TX=[0x20,0x41,0x60,0x81] RX=0x81 VALID=1 EXPECT=Yes RESULT=Yes PASS TIME_US=882
NAME=maximum_values H=300 W=300 TX=[0x29,0x4C,0x69,0x8C] RX=0x81 VALID=1 EXPECT=Yes RESULT=Yes PASS TIME_US=887
SUMMARY PASS=10 FAIL=0 TOTAL=10 TOTAL_TIME_US=33591

1ケースあたり、およそ0.9msでPASSしています。ACですね。


今回のまとめ

今回は、前回作成したSPIテンプレートV2をコピーし、ABC467Aを実装しました。

ABC467Aの判定式は、浮動小数点数や除算を使わず、次の整数比較へ変換できます。

400 * W >= H * H

また、9bitのHWは、それぞれ上位4bitと下位5bitの2byteに分けて送信しました。

問題固有処理では、V2で共通化したrx_data_strobeをそのまま利用できます。テンプレート側の受信ストローブ生成処理を意識せず、入力の組み立てと判定処理に集中できました。


次回

次回から、ABC467Cの実装に取り組む予定です。お楽しみに。

前回:Shrike-LiteでAtCoder問題を解く(12) Interlude:受信ストローブを共通化したSPIテンプレートV2を作る

次回:Shrike-LiteでAtCoder問題を解く(14):ABC467C Naive実装編(準備中)

0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?