はじめに
ここまで、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.vのrx_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.vとspi_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.vとspi_target.vを保存したら、ForgeFPGA Workshopで合成とbitstream生成を行います。
今回の変更はspi_target.vとmain.vの内部接続だけで、トップモジュールの外部ポートは変更していません。そのため、IO Plannerの設定変更は必要ありません。
Synthesize
↓
Generate Bitstream
生成されたファイルを次の名前へ変更します。
atcoder_spi_template_v2.bin
今回の合成と配置配線ではエラーはありませんでしたが、外部I/OのFF配置とクロック入力に関する警告が表示されました。
これらは今回追加したrx_data_strobeに関する警告ではないようです。まずは生成されたbitstreamを使い、実機でSPI通信の動作を確認することにします。
今回のリソース使用レポートは次の通りです。
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.binとatcoder_spi_template_v2_test.pyをShrike-Liteへ配置し、Thonnyからテストコードを実行します。
今回の実機テストでは、V2へ変更してもV1と同じSPI送受信結果になることと、追加したrx_data_strobeの接続を通して問題固有処理が実行されることを確認します。
V1とV2でSPI通信仕様は変えていません。したがって、RP2040側から見た送受信結果もV1と同じになるはずです。
手元の実機でテストしたところ問題なく動作しました。
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.vへo_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を使ってみる


