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問題を解く(12) Interlude:受信ストローブを共通化したSPIテンプレートV2を作る

0
Last updated at Posted at 2026-07-20

はじめに

ここまで、Shrike-LiteでAtCoder問題を解くためのSPIテンプレートを使い、いくつかの問題を実装してきました。

最初のSPIテンプレートを作成した手順や、Shrike公式サンプルからspi_target.vを取り込む方法については、第5回で詳しく説明しています。

今回は新しいAtCoder問題には進まず、これまで使ってきたSPIテンプレートへ小さな改良を加えます。

第8回のABC466B制約版では、spi_target.vが出力するrx_data_validをそのまま処理条件にすると、1byteの受信に対して処理が複数回実行される場合のあることが分かりました。

そこで第8回以降は、各main.vに次の回路を追加していました。

reg  rx_data_valid_d;
wire rx_data_strobe;

assign rx_data_strobe =
    rx_data_valid && !rx_data_valid_d;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_data_valid_d <= 1'b0;
    end else begin
        rx_data_valid_d <= rx_data_valid;
    end
end

この処理はAtCoder問題固有の回路ではありません。

SPIで新しい1byteを受信したことを、問題固有回路へ1回だけ通知するための共通処理です。

今回は、この受信ストローブ生成回路をspi_target.vへ移し、SPIテンプレートV2として整理します。


今回やること

今回の変更点は一つだけです。

rx_data_validの立上り検出
main.vの問題固有部分
        ↓
spi_target.vの共通部分

V1では、spi_target.vから出力されたrx_data_validを、各main.vで1クロックパルスへ変換していました。

V2では、spi_target.vから最初からrx_data_strobeを出力します。

これで、新しいAtCoder問題を実装するときに、毎回同じ立上り検出回路を書く必要がなくなります。


rx_data_validをそのまま使わない理由

spi_target.vrx_data_validは、1byteを受信した瞬間だけHighになる信号ではありません。

1byteの受信後、SSが解除されるか、次のbyteの先頭を受信するまで、複数クロックHighを維持する場合があります。

イメージは次のようになります。

rx_data_valid   ____████████____
rx_data_strobe  ____█___________

V1の動作確認回路では、受信値に1を加えた値をtx_dataへ代入していました。

tx_data <= rx_data + 8'd1;

同じ値が複数回代入されても最終結果は変わらないため、この段階では問題が表面化しませんでした。

しかし、次のような副作用を持つ処理では困ります。

  • カウンタを1増やす
  • 配列を1段シフトする
  • BRAMの読み書き要求を1回だけ発行する
  • FSMを次の状態へ進める

このような処理は、1byteの受信につき1回だけ実行する必要があります。

V2では、問題固有回路からrx_data_validの継続時間を意識しなくてよいようにします。


SPIテンプレートV2の構成

V1のプロジェクトをコピーし、V2用のプロジェクトを作ります。

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

今回は、第5回で作成したV1のプロジェクトをコピー・リネームし、次の二つのVerilogファイルを変更します。

ファイル 変更内容
spi_target.v o_rx_data_strobeを追加する
main.v rx_data_valid_dを持たず、rx_data_strobeを直接使う

トップモジュールの外部ポートは変更しないため、IO Plannerの設定変更は必要ありません。

MicroPython側の通信方法とSPI設定は変更しません。


V2で追加する信号

spi_target.vへ、次の出力を追加します。

output o_rx_data_strobe;

既存のo_rx_data_validは削除しません。

o_rx_data_valid
  受信データが有効な期間を示すレベル信号

o_rx_data_strobe
  新しい1byteを受信したことを示す1クロックパルス

既存信号を残しておけば、これまでの回路との対応関係も確認できます。

新しいAtCoder実装では、基本的にo_rx_data_strobeだけを処理条件として使います。


AIへVerilogファイルの変更を依頼する

今回は、V1のmain.vspi_target.vを元に、AIへ次のように変更を依頼します。

Shrike-Lite用のatcoder_spi_templateをV2へ更新してください。

既存のmain.vとspi_target.vを元に、rx_data_strobe生成処理を
main.vの問題固有側からspi_target.vへ移動してください。

spi_target.vには、次の出力ポートを追加してください。

- o_rx_data_strobe

既存のo_rx_data_validは削除しないでください。

spi_target.v内部では、o_rx_data_validの立上りを検出し、
1byteの受信につき1クロックだけHighになる
o_rx_data_strobeを生成してください。

main.vでは、次の宣言と処理を持たせないでください。

- rx_data_valid_d
- rx_data_validの立上り検出処理

main.vの問題固有処理は、rx_data_validではなく
rx_data_strobeを条件に実行してください。

動作確認用の処理は、V1と同じく
受信した8bit値に1を加えてtx_dataへ保存する処理とします。

Verilogで実装し、SystemVerilog固有構文は新たに追加しないでください。

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

AIが生成したコードは、そのまま使わず、V1との差分と信号の接続を確認します。

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


spi_target.vの全文

以下がV2のspi_target.vです。

Shrike公式リポジトリのspi_loopback_ledサンプルに含まれるspi_target.vを元に、o_rx_data_strobeを追加しています。

// Based on Vicharak Shrike official spi_loopback_led/spi_target.v.
// Modified for atcoder_spi_template_v2 on 2026-07-19:
// - Added o_rx_data_strobe.
// Original hardware design license: CERN OHL v1.2.

module spi_target #(
    parameter CPOL = 1'b0,  // 0: clock is low in idle, 1: clock is high in idle
    parameter CPHA = 1'b0,  // 0: sample on leading edge, 1: sample on trailing edge
    parameter WIDTH = 8,    // SPI data width
    parameter LSB = 1'b0    // 1: LSB first, 0: MSB first
) (
    // Common ports
    input i_clk,
    input i_rst_n,

    // Control signal
    input i_enable,

    // SPI interface ports
    input i_ss_n,
    input i_sck,
    input i_mosi,
    output o_miso,
    output o_miso_oe,

    // RX internal ports
    output reg [WIDTH-1:0] o_rx_data,
    output reg o_rx_data_valid,
    output o_rx_data_strobe,

    // TX internal ports
    input [WIDTH-1:0] i_tx_data,
    output o_tx_data_hold
);

    // Signal declaration
    reg [2:0] r_ss_n_sync;
    reg [2:0] r_sck_sync;
    reg [$clog2(WIDTH-1):0] r_transmision_count;
    reg [WIDTH-1:0] r_miso_data;

    // V2追加:
    // 1クロック前のo_rx_data_validを保持し、
    // 立上りだけをo_rx_data_strobeとして出力する。
    reg r_rx_data_valid_d;

    wire w_sck_r_edge;
    wire w_sck_f_edge;
    wire w_sck_edge;
    wire w_sck_edge_op;

    // SPI input signals synchronization
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_ss_n_sync <= 'b111;
        end else if (i_enable) begin
            r_ss_n_sync <= {r_ss_n_sync[1:0], i_ss_n};
        end else begin
            r_ss_n_sync <= 'b111;
        end
    end

    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_sck_sync <= 'h0;
        end else if (i_enable) begin
            r_sck_sync <= {r_sck_sync[1:0], i_sck};
        end else begin
            r_sck_sync <= 'h0;
        end
    end

    // Create rising and falling edge signals from spi_clk signal
    assign w_sck_r_edge = ~r_sck_sync[2] & r_sck_sync[1];
    assign w_sck_f_edge = r_sck_sync[2] & ~r_sck_sync[1];

    assign w_sck_edge =
        (CPHA ^ CPOL) ? w_sck_f_edge : w_sck_r_edge;

    assign w_sck_edge_op =
        (CPHA ^ CPOL) ? w_sck_r_edge : w_sck_f_edge;

    // Create transmission bit counter
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_transmision_count <= 'h0;
        end else if (!i_enable || r_ss_n_sync[1]) begin
            r_transmision_count <= 'h0;
        end else if (w_sck_edge) begin
            if (r_transmision_count == WIDTH-1) begin
                r_transmision_count <= 'h0;
            end else begin
                r_transmision_count <= r_transmision_count + 1'b1;
            end
        end
    end

    // Create o_rx_data bus
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            o_rx_data <= 'h0;
        end else if (w_sck_edge) begin
            if (LSB) begin
                o_rx_data <= {i_mosi, o_rx_data[WIDTH-1:1]};
            end else begin
                o_rx_data <= {o_rx_data[WIDTH-2:0], i_mosi};
            end
        end
    end

    // Create o_rx_data_valid signal
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            o_rx_data_valid <= 1'b0;
        end else if (r_ss_n_sync[1] ||
                     (r_transmision_count == 0 && w_sck_edge)) begin
            o_rx_data_valid <= 1'b0;
        end else if (w_sck_edge &&
                     r_transmision_count == WIDTH-1) begin
            o_rx_data_valid <= 1'b1;
        end
    end

    // V2追加:
    // o_rx_data_validを1byteにつき1回だけHighになる
    // 1クロックパルスへ変換する。
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_rx_data_valid_d <= 1'b0;
        end else begin
            r_rx_data_valid_d <= o_rx_data_valid;
        end
    end

    assign o_rx_data_strobe =
        o_rx_data_valid & ~r_rx_data_valid_d;

    // Create o_tx_data_hold signal
    assign o_tx_data_hold =
        (~CPHA & r_ss_n_sync[2] & ~r_ss_n_sync[1]) |
        (r_transmision_count == 0 & w_sck_edge_op);

    // Create o_miso and OE signals
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_miso_data <= 'h0;
        end else if (o_tx_data_hold) begin
            r_miso_data <= i_tx_data;
        end else if (w_sck_edge_op) begin
            if (LSB) begin
                r_miso_data <= r_miso_data >> 1;
            end else begin
                r_miso_data <= r_miso_data << 1;
            end
        end
    end

    assign o_miso =
        (LSB) ? r_miso_data[0] : r_miso_data[WIDTH-1];

    assign o_miso_oe = ~r_ss_n_sync[2];

endmodule

V2で追加した中心部分は次の箇所です。

reg r_rx_data_valid_d;

always @(posedge i_clk or negedge i_rst_n) begin
    if (!i_rst_n) begin
        r_rx_data_valid_d <= 1'b0;
    end else begin
        r_rx_data_valid_d <= o_rx_data_valid;
    end
end

assign o_rx_data_strobe =
    o_rx_data_valid & ~r_rx_data_valid_d;

o_rx_data_validがLowからHighへ変化した最初の1クロックだけ、o_rx_data_strobeがHighになります。


main.vの全文

以下がV2のmain.vです。

V1と同じく、動作確認用として受信値に1を加え、次のSPI通信で返します。

(* top *) module main (
    // ===== 共通部分:Shrike-LiteとSPIの外部端子 =====
    (* iopad_external_pin, clkbuf_inhibit *) input clk,
    (* iopad_external_pin *) output clk_en,
    (* iopad_external_pin *) input rst_n,

    (* iopad_external_pin *) input  spi_ss_n,
    (* iopad_external_pin *) input  spi_sck,
    (* iopad_external_pin *) input  spi_mosi,
    (* iopad_external_pin *) output spi_miso,
    (* iopad_external_pin *) output spi_miso_en
);

    // ===== 共通部分:内部クロックとSPI送受信データ =====
    assign clk_en = 1'b1;

    wire [7:0] rx_data;
    wire       rx_data_strobe;
    reg  [7:0] tx_data;

    // ===== 問題ごとに変更する部分 =====
    // 今回は動作確認用として、受信値に1を加えて返す。
    // AtCoder問題を実装するときは、
    // この処理を問題固有の判定・演算へ置き換える。
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tx_data <= 8'h00;
        end else if (rx_data_strobe) begin
            tx_data <= rx_data + 8'd1;
        end
    end

    // ===== 共通部分:SPI Targetモジュール =====
    spi_target #(
        .CPOL(1'b0),
        .CPHA(1'b0),
        .WIDTH(8),
        .LSB(1'b0)
    ) u_spi_target (
        .i_clk(clk),
        .i_rst_n(rst_n),
        .i_enable(1'b1),

        .i_ss_n(spi_ss_n),
        .i_sck(spi_sck),
        .i_mosi(spi_mosi),
        .o_miso(spi_miso),
        .o_miso_oe(spi_miso_en),

        .o_rx_data(rx_data),
        .o_rx_data_valid(),
        .o_rx_data_strobe(rx_data_strobe),

        .i_tx_data(tx_data),
        .o_tx_data_hold()
    );

endmodule

問題ごとに変更する部分では、rx_data_validではなくrx_data_strobeを使用します。

else if (rx_data_strobe) begin
    tx_data <= rx_data + 8'd1;
end

V1でmain.v側に置いていた次の要素は、V2では不要です。

rx_data_valid_d
rx_data_strobeのassign文
rx_data_valid_dを更新するalwaysブロック

問題固有部分には、「新しい1byteを受信したときに何をするか」だけを書けるようになりました。


MicroPythonテストコード

MicroPython側のSPI通信手順はV1から変更しません。

第5回のatcoder_spi_template_test.pyをコピーし、ファイル名とコード内のbitstream名だけをV2用へ変更します。

BITSTREAM = "atcoder_spi_template_v2.bin"

テストする値もV1と同じです。

patterns = [0x00, 0x12, 0x7F, 0xFF]

SPIは全二重通信なので、送信した値に1を加えた結果は、次のSPI通信で受信します。

期待する表示形式は次のとおりです。

TX=0x00 RX=0x00 EXPECT=0x00 PASS
TX=0x12 RX=0x01 EXPECT=0x01 PASS
TX=0x7F RX=0x13 EXPECT=0x13 PASS
TX=0xFF RX=0x80 EXPECT=0x80 PASS
TX=0x00 RX=0x00 EXPECT=0x00 PASS

合成してbitstreamを生成する

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

今回の変更はspi_target.vとmain.vの内部接続だけで、トップモジュールの外部ポートは変更していません。そのため、IO Plannerの設定変更は必要ありません。

Synthesize
↓
Generate Bitstream

生成されたファイルを次の名前へ変更します。

atcoder_spi_template_v2.bin

今回の合成と配置配線ではエラーはありませんでしたが、外部I/OのFF配置とクロック入力に関する警告が表示されました。

Shrike-LiteでAtCoder問題を解く(12)_002.png

これらは今回追加したrx_data_strobeに関する警告ではないようです。まずは生成されたbitstreamを使い、実機でSPI通信の動作を確認することにします。

今回のリソース使用レポートは次の通りです。

Shrike-LiteでAtCoder問題を解く(12)_003.png


V1とV2のリソース使用量を比較する

V1とV2のリソース使用量を比較すると、次のようになりました。

Resource V1 V2 増加量
CLB LUTs 33 46 +13
CLB FFs 26 33 +7
IOB FFs 4 4 変更なし
CLBs 9 10 +1
GPIOs 5 5 変更なし
4k BRAMs 0 0 変更なし

V2ではV1と比較して、CLB LUTが13個、CLB FFが7個増えました。

今回追加したのは、1bitレジスタと立上り検出用の簡単な論理だけですが、回路を変更すると周辺論理の最適化やCLBへの配置方法も変わるため、ソースコード上の追加量とリソース差は必ずしも一対一にはならないようです。

それでも、CLB使用量は9区画から10区画へ増えただけです。V2全体でもCLB LUT使用率は4.11%、CLB使用率は7.14%であり、AtCoder問題固有の回路を追加する余地は十分に残っています。


実機で動作確認する

atcoder_spi_template_v2.binatcoder_spi_template_v2_test.pyをShrike-Liteへ配置し、Thonnyからテストコードを実行します。

今回の実機テストでは、V2へ変更してもV1と同じSPI送受信結果になることと、追加したrx_data_strobeの接続を通して問題固有処理が実行されることを確認します。

V1とV2でSPI通信仕様は変えていません。したがって、RP2040側から見た送受信結果もV1と同じになるはずです。

手元の実機でテストしたところ問題なく動作しました。

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


V1とV2の違い

V1とV2の違いを整理すると、次のようになります。

項目 V1 V2
o_rx_data_valid 出力する そのまま残す
o_rx_data_strobe なし 新しく出力する
立上り検出回路 main.vに記述 spi_target.vに集約
問題固有処理の条件 問題ごとに変換して使う rx_data_strobeを直接使う
SPI通信仕様 8bit、1byte遅延 変更なし
MicroPython側 V1用 ファイル名とbitstream名のみ変更

変更量は小さいですが、今後のmain.vからSPI通信側の都合を一つ減らすことができます。


今後のAtCoder実装で使うとき

V2テンプレートから新しいAtCoder用プロジェクトを作る場合、問題固有部分では次の形を基本にします。

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

カウンタ更新、配列シフト、FSM遷移、BRAM要求なども、原則としてrx_data_strobeを基準にします。

もちろん、数クロックかかる処理を実装する場合は、rx_data_strobeを開始イベントとして使い、その後はFSMなどで処理を進めます。

rx_data_strobe
    ↓
処理開始
    ↓
FSMで数クロック処理
    ↓
結果をtx_dataへ準備

これで問題固有部分は、SPI受信信号の継続時間ではなく、受信したデータと実装したい処理に集中できます。


今回のまとめ

今回は、新しいAtCoder問題を実装する前のInterludeとして、SPIテンプレートをV2へ更新しました。

主な変更点は次のとおりです。

  • spi_target.vo_rx_data_strobeを追加
  • o_rx_data_validの立上り検出を共通化
  • main.vからrx_data_valid_dを削除
  • 問題固有処理ではrx_data_strobeを直接使用
  • SPI通信仕様とMicroPython側の処理は変更なし

V1を作った時点では、rx_data_validが見えれば、実装で困ることはないかな、と考えていました。

しかし、配列シフトやBRAMアクセスを含む問題を実装したことで、1byteの受信を1回の処理イベントとして扱ったほうが、実装の見通しが良くなることに気づきました。

今回の変更により、今後のAtCoder実装では、受信ストローブ生成処理を問題ごとに記述する必要がなくなりました。

実際の問題で繰り返し使う処理を共通部分へ移し、SPIテンプレートをより扱いやすい形に整理できました。


次回

次回は、SPIテンプレートV2を使って、AtCoder Beginner Contest 467の問題をShrike-Liteへ実装する予定です。お楽しみに。

前回:前回:Shrike-LiteでAtCoder問題を解く(11):ABC466B - Representative Balls(完全版・BRAM利用)

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


参考資料

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?