はじめに
前回は、Shrike-Liteの回路図を確認し、RP2040からFPGAへ1bitのON/OFF状態を渡しました。
RP2040側のGPIOをON/OFFすると、その信号が基板上の配線を通ってFPGAへ入り、FPGA側LEDにも反映されるところまで確認しました。
今回は、Shrike公式GitHubリポジトリに公開されている spi_loopback_led サンプルを利用して、RP2040とFPGAの間でSPI通信を行います。
ただし、公式サンプルをそのまま動かして終わりにはしません。
今回の目的は、次の二つです。
- 公式サンプルに含まれるSPI Slave用Verilogモジュールを、自分のForgeFPGAプロジェクトへ取り込んで再利用する
- 再利用する共通部分と、問題ごとに変更する部分を整理し、今後のAtCoder実装に使えるテンプレートを作る
SPI通信を一からVerilogで実装するのはかなり大変です。
しかし、公式サンプルの中に使えそうなSPI Slaveモジュールがあれば、その部分を共通部品として利用し、受信後の処理だけを問題ごとに書き換えることができます。
今回は、FPGAが受信した1byteに1を加えてRP2040に返す簡単な処理を例として実装します。そのうえで、FPGA側とMicroPython側を共通部分と問題ごとに変更する部分に分け、atcoder_spi_template として整理します。
今回やること
今回の流れは以下です。
今回作る回路では、RP2040から受信した1byteにFPGA側で1を加え、その結果を次のSPI通信でRP2040へ返します。
FPGA側のLED制御は行いません。
RP2040から1byte送信
↓
公式サンプル由来のSPI Slaveモジュールで受信
↓
main.vの問題ごとに変更する部分に、動作確認用として受信値に1を加える
↓
次のSPI通信で加算結果をRP2040へ返す
↓
共通部分と問題ごとに変更する部分をテンプレートとして整理
公式spi_loopback_ledサンプル
今回利用するのは、Shrike公式GitHubリポジトリの以下のサンプルです。
主なファイル構成は次のようになっています。
spi_loopback_led
├── bitstream
│ └── spi_loopback_led.bin
├── ffpga
│ └── src
│ ├── spi_target.v
│ └── top.v
├── firmware
│ └── micropython
│ └── spi_loopback_led.py
└── spi_loopback_led.ffpga
このサンプルでは、RP2040がSPI Master、FPGAがSPI Slaveとして動きます。
公式コードではSPI Slave側を Target と表現しているため、Verilogモジュール名も spi_target になっています。
サンプルの動作は以下のようになっています。
RP2040から0xABを送信
→ FPGA側LEDを点灯
RP2040から0xFFを送信
→ FPGA側LEDを消灯
受信した1byte
→ 次のSPI通信でRP2040へ返す
外部のジャンパ線やLEDは必要ありません。
Shrike-Lite上のRP2040、FPGA、FPGA側LEDだけで動作を確認できる構成になっています。
サンプルを二つの部分に分けて見る
このサンプルのFPGA側は、二つのVerilogファイルで構成されています。
spi_target.v
top.v
それぞれは次のような役割を担っているようです。
| ファイル | 役割 |
|---|---|
spi_target.v |
SPI信号を処理して1byteを送受信する |
top.v |
受信したデータを元にLEDのON/OFF制御とSPIループバックデータをセットする |
spi_target.v は、SCK、CS、MOSI、MISOといったSPI信号を扱います。
一方、top.v は spi_target.v から受け取ったデータを使い、サンプル固有の処理を行っています。
今回再利用するのは、通信処理を担当する spi_target.v です。
公式サンプル固有のLED制御はコピーせず、AtCoder実装テンプレート用の main.v を新たに作成します。
spi_target.vの入出力
spi_target.v の主な入出力を整理すると、次のようになります。
SPIピン側
| 信号 | 方向 | 役割 |
|---|---|---|
i_ss_n |
入力 | CS信号 |
i_sck |
入力 | SPIクロック |
i_mosi |
入力 | RP2040から受信する1bit信号 |
o_miso |
出力 | RP2040へ返す1bit信号 |
o_miso_oe |
出力 | MISO出力の有効化 |
自分のロジック側
| 信号 | 方向 | 役割 |
|---|---|---|
o_rx_data |
出力 | SPIで受信したデータ |
o_rx_data_valid |
出力 | 1byteの受信完了を示すパルス |
i_tx_data |
入力 | 次のSPI通信で返すデータ |
o_tx_data_hold |
出力 | 送信データを取り込むタイミング |
SPIのシリアル信号を、自分の回路で扱いやすい1byteのデータへ変換してくれるわけです。
自分の main.v 側では、主に次の3本を見れば処理を作れます。
o_rx_data
o_rx_data_valid
i_tx_data
これなら、SCKのエッジやbit数を自分で数える処理を、問題ごとに書き直す必要はありません。
新しいForgeFPGAプロジェクトを作る
今回は、公式の spi_loopback_led.ffpga を直接変更せず、新しいプロジェクトを作成します。
プロジェクト名は、次のようにします。
atcoder_spi_template
ForgeFPGA Workshopでの新規プロジェクト作成手順は、前回の記事で確認したものと同じです。
使用するデバイスは、Shrike-Liteに搭載されている以下を選びます。
SLG47910V (Rev BB)
Project settings画面では以下のように値をセットします。
新しいプロジェクトを作成したら、Verilogファイルを置くフォルダを確認します。
atcoder_spi_template
└── ffpga
└── src
└── main.v
spi_target.vを自分のプロジェクトへ取り込む
公式サンプルに含まれる次のファイルを、自分のプロジェクトへ取り込みます。
examples/spi_loopback_led/ffpga/src/spi_target.v
ただし、Windowsのエクスプローラーなどを使って .v ファイルをプロジェクトフォルダへコピーしただけでは、ForgeFPGA Workshop上でプロジェクトのソースファイルとして認識されないようです。
そこで、ForgeFPGA Workshop上から新しいVerilogファイルを作成し、公式サンプルの内容をコピーします。
まず、画面上部のツールバー"New Custom Module"ボタンをクリックするか、ForgeFPGA WorkshopのFileメニューから New Custom Module を選びます。
モジュール登録ウィンドウが表示されますので、spi_target を入力してCreateです。
ForgeFPGA Workshop上に空の spi_target.v が作成されたら、画面左側の Sources ツリーからspi_target.vファイルをダブルクリックして開きます。
公式サンプルの spi_target.v の内容をすべてコピーし、先ほどオープンした spi_target.v へ貼り付けて保存します。
これで、公式サンプルのSPI Slaveモジュールが、自分のプロジェクトのVerilogソースとして登録されます。
この時点のファイル構成は次のようになります。
atcoder_spi_template
└── ffpga
└── src
├── spi_target.v
└── main.v
spi_target.v が公式サンプルから取り込んだ共通の通信部品、main.v が問題ごとの処理を記述するファイルです。
ライセンスについて
公式サンプルのコードを自分のプロジェクトへ取り込むため、ライセンスも確認しておきます。
Shrike公式リポジトリでは、ソフトウェアはGNU General Public License v2.0(GPL-2.0)で公開されています。
一方、HDL/RTL、回路図、PCBファイル、制約ファイルなどのハードウェア設計は、CERN Open Hardware Licence v1.2で公開されています。
今回取り込む spi_target.v はVerilogで記述されたHDLなので、利用や再配布を行う際には、公式リポジトリの LICENSE_HW.md も確認する必要があります。
本記事では学習目的で公式サンプルを利用していますが、自分のプロジェクトを公開・配布する場合は、使用したファイルに適用されるライセンス条件を確認してください。
テンプレート用のmain.vを書く
今回の main.v では、動作確認用として次の処理を行います。
1byteを受信する
↓
受信値に1を加える
↓
加算結果を返送用レジスタへ保存する
FPGA側のLED制御は行いません。
今後テンプレートを別のAtCoder問題へ使うときに迷わないよう、コード内で共通部分と問題ごとに変更する部分を分けておきます。
以下のコードをForgeFPGA Workshopで main.v に貼り付けてください。
(* 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_valid;
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_valid) begin
tx_data <= rx_data + 8'd1;
end
end
// ===== 共通部分:公式サンプルから取り込んだSPI Slaveモジュール設定と接続 =====
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(rx_data_valid),
.i_tx_data(tx_data),
.o_tx_data_hold()
);
endmodule
今回、問題ごとに変更する部分には、動作確認用として次の処理を書きます。
tx_data <= rx_data + 8'd1;
例えば 0x12 を受信すると、0x13 を返送用レジスタへ保存します。
0x12を受信
↓
FPGA側で1を加える
↓
0x13を返送用レジスタへ保存
tx_data は8bitなので、0xFF に1を加えると桁あふれして 0x00 になります。
SPI通信の低レベル処理は、共通部分として取り込んだ spi_target.v が担当します。
IO Plannerでピン割り当てする
次に、IO PlannerでVerilog上の信号とFPGA側のピンを対応させます。
今回は、公式 spi_loopback_led サンプルと同じピン割り当てを使います。
| FPGA側 | Verilog上の信号 |
|---|---|
GPIO4_IN (Input) |
spi_ss_n |
GPIO3_IN (Input) |
spi_sck |
GPIO5_IN (Input) |
spi_mosi |
GPIO6_OE (Output) |
spi_miso_en |
GPIO6_OUT (Output) |
spi_miso |
GPIO18_IN (Input) |
rst_n |
OSC_EN (Output) |
clk_en |
OSC_CLK (Input) |
clk |
公式サンプルのプロジェクトをForgeFPGA Workshopで開き、IO Plannerの設定を見比べながら設定すると分かりやすいです。
このピン割り当ても、今後のAtCoder実装で共通して使う部分です。
合成してbitstreamを生成する
main.v と spi_target.v をプロジェクトへ追加し、IO Plannerの設定が終わったら、合成とbitstream生成を行います。
操作は第3回、第4回と同じです。
Synthesize
↓
Generate Bitstream
エラーがなくbitstreamを生成できたら、次のファイルを確認します。
atcoder_spi_template/ffpga/build/bitstream/FPGA_bitstream_MCU.bin
分かりやすいように、次の名前へ変更します。
atcoder_spi_template.bin
atcoder_spi_template.bin をShrike-LiteのUSBドライブへコピーします。
RP2040側のテストコードを作る
次に、RP2040からSPI通信を行うMicroPythonコードを作ります。
ファイル名は次のようにします。
atcoder_spi_template_test.py
こちらも、今後問題を変更するときに使いやすいよう、共通部分と問題ごとに変更する部分をコメントで分けておきます。
コードは以下です。
from machine import Pin, SPI
import time
import shrike
# ===== 共通部分:bitstream名とShrike-Liteのピン設定 =====
BITSTREAM = "atcoder_spi_template.bin"
SCK = 2
CS = 1
MOSI = 3
MISO = 0
FPGA_RESET = 14
# ===== 共通部分:FPGAへのbitstream書き込みとリセット =====
shrike.reset()
shrike.flash(BITSTREAM)
reset_pin = Pin(FPGA_RESET, Pin.OUT, value=1)
reset_pin.value(0)
time.sleep_ms(100)
reset_pin.value(1)
time.sleep_ms(100)
# ===== 共通部分:SPI Masterの初期化 =====
cs = Pin(CS, Pin.OUT, value=1)
spi = SPI(
0,
baudrate=1_000_000,
polarity=0,
phase=0,
bits=8,
firstbit=SPI.MSB,
sck=Pin(SCK),
mosi=Pin(MOSI),
miso=Pin(MISO)
)
# ===== 共通部分:1byteのSPI送受信 =====
def spi_exchange(value):
tx = bytes([value])
rx = bytearray(1)
cs.value(0)
spi.write_readinto(tx, rx)
cs.value(1)
return rx[0]
# ===== 問題ごとに変更する部分:入力データと期待値の確認 =====
# 今回は「受信値に1を加えて次回返す」回路をテストする。
patterns = [0x00, 0x12, 0x7F, 0xFF]
expected_rx = 0x00
for value in patterns:
received = spi_exchange(value)
result = "PASS" if received == expected_rx else "FAIL"
print(
"TX=0x{:02X} RX=0x{:02X} EXPECT=0x{:02X} {}".format(
value,
received,
expected_rx,
result
)
)
# 今回の期待値計算
expected_rx = (value + 1) & 0xFF
time.sleep_ms(100)
# 動作確認用の後処理:最後に送った0xFFの処理結果を読み出す
received = spi_exchange(0x00)
result = "PASS" if received == expected_rx else "FAIL"
print(
"TX=0x00 RX=0x{:02X} EXPECT=0x{:02X} {}".format(
received,
expected_rx,
result
)
)
SPI0は次の設定で使っています。
| 項目 | 設定 |
|---|---|
| 通信速度 | 1MHz |
| polarity | 0 |
| phase | 0 |
| データ長 | 8bit |
| bit順 | MSB first |
polarity=0、phase=0 なのでSPI Mode 0です。
1byteを送受信する
spi_exchange() では、RP2040からFPGAへ1byteを送りながら、FPGAから返される1byteを受け取ります。
def spi_exchange(value):
tx = bytes([value])
rx = bytearray(1)
cs.value(0)
spi.write_readinto(tx, rx)
cs.value(1)
return rx[0]
CSをLowにしてFPGAを選択し、write_readinto() で送信と受信を同時に行います。
1byteの転送が終わったら、CSをHighに戻します。
この関数は、今後のAtCoder実装でも共通部分として利用します。
返ってくるのは一つ前の受信データを処理した結果
今回のFPGA回路では、1byteを受信したあとで1を加え、その結果を返送用レジスタへ保存します。
if (rx_data_valid) begin
tx_data <= rx_data + 8'd1;
end
SPIでは送信と受信が同時に進むため、その通信で受信している値を処理し、同じ通信の途中で返すことはできません。
そのため、FPGAから返ってくるのは、一つ前のSPI通信で受信した値に1を加えた結果です。
1回目
RP2040から0x00を送信
FPGAから初期値0x00を受信
2回目
RP2040から0x12を送信
FPGAから前回の0x00に1を加えた0x01を受信
3回目
RP2040から0x7Fを送信
FPGAから前回の0x12に1を加えた0x13を受信
4回目
RP2040から0xFFを送信
FPGAから前回の0x7Fに1を加えた0x80を受信
5回目
RP2040から0x00を送信
FPGAから前回の0xFFに1を加えた0x00を受信
最後の 0xFF + 1 = 0x00 は、8bit演算の桁あふれによるものです。
これは不具合ではなく、今回の返送ロジックとSPIの全二重通信による動作です。
今後FPGAへ処理を依頼して結果を受け取る場合にも、基本的には次の形を使います。
コマンドやデータを送る
↓
FPGAが処理する
↓
次のSPI通信で結果を受け取る
Thonnyのシェルで実行して確認する
Shrike-LiteをPCへ接続し、ThonnyからRP2040上のMicroPythonへ接続します。
FPGA用の atcoder_spi_template.bin と、RP2040側の atcoder_spi_template_test.py をShrike-Liteへ保存しておきます。
そのあと、RP2040に接続されているThonnyのシェルから次のコマンドを実行します。
exec(open("atcoder_spi_template_test.py").read())
正常に動作すれば、Thonnyのシェルに次のようなログが表示されます。
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
1回目の受信値は、リセット時に設定した返送用レジスタの初期値 0x00 です。
2回目以降は、一つ前に送った値にFPGA側で1を加えた結果が返っています。
0x00 → 0x01
0x12 → 0x13
0x7F → 0x80
0xFF → 0x00
RP2040から送ったデータにFPGA側の動作確認用処理を適用し、その結果をRP2040へ返すことができました。
公式サンプルから取り込んだもの
今回、公式サンプルから利用したのは、主に次の部分です。
spi_target.v
SPI信号のピン割り当て
RP2040側のSPI設定
今回の動作確認用に作成したのは、次の部分です。
受信データに1を加える処理
FPGAから返すデータの決め方
テストパターンと期待値の計算
整理すると、次のような役割分担になります。
公式サンプルから、必要な部分を取り出して共通部品として整理すれば、複雑な機能をすべて一から作らなくても、自分の目的に合った回路を作り始めることができてお得ですね。
AtCoder実装テンプレートとして使う
今回作成したプロジェクトは、今後のAtCoder実装の出発点として使います。
テンプレートのフォルダ構成は、次のように整理します。
atcoder_spi_template
├── bitstream
│ └── atcoder_spi_template.bin
├── ffpga
│ └── src
│ ├── spi_target.v
│ └── main.v
├── firmware
│ └── micropython
│ └── atcoder_spi_template_test.py
└── atcoder_spi_template.ffpga
ForgeFPGA Workshopが生成する作業用ファイルとは別に、再利用したいソース、bitstream、MicroPythonコードを分かりやすい位置へまとめておきます。
今後AtCoder問題をShrike-Liteへ実装するときは、このフォルダをコピーし、問題名に合わせてプロジェクトを作ります。
共通部分と問題ごとに変更する部分は、次のように整理できます。
| 対象 | 共通部分 | 問題ごとに変更する部分 |
|---|---|---|
spi_target.v |
公式サンプル由来のSPI Slave処理 | 原則として変更しない |
main.v |
外部端子、SPI信号、spi_target の接続 |
受信データの保存方法、コマンド処理、判定・演算、返送データ |
| IO Planner | RP2040とFPGAをつなぐSPI・リセット・クロックのピン割り当て | 原則として変更しない |
| bitstream | 生成・リネーム・Shrike-Liteへのコピー手順 | 問題ごとの main.v から再生成する |
| MicroPythonの初期化部 | bitstream書き込み、FPGAリセット、SPI初期化、spi_exchange()
|
必要に応じて通信速度や転送単位を変更する |
| MicroPythonのテスト部 | PASS/FAIL表示の基本形 | 入力データ、コマンド列、期待値、結果の読み出し手順 |
実際のAtCoder問題では、今回の加算処理を、問題ごとの判定回路や演算回路へ置き換えることにします。
例えば、全体の構成は次のようになります。
RP2040側
問題の入力を整形する
↓
SPIでコマンドとデータを送る
↓
spi_target.v
1byteずつ受信する
↓
main.vの問題ごとに変更する部分
判定や演算を行う
↓
返送用データを準備する
↓
spi_target.v
MISOで結果を返す
↓
RP2040側
結果を表示して期待値と比較する
SPI通信部分はテンプレートの共通部品として使い、問題ごとに変更するのは、その上に載せる処理とテスト内容です。
これなら、AtCoder問題を一つ実装するたびにSPI Slave回路やRP2040側の初期化処理を作り直す必要はありません。
前回の1bit通信との違い
前回と今回の違いをまとめると、次のようになります。
| 項目 | 前回 | 今回 |
|---|---|---|
| 通信方法 | GPIO線1本 | SPI |
| 送信単位 | ON/OFFの1bit | 1byte |
| 通信方向 | RP2040からFPGA | RP2040とFPGAの双方向 |
| FPGA側の処理 | 入力をLEDへそのまま出力 | 受信完了を検出してデータを処理 |
| 実装方法 | 接続線を調べて自力実装 | 公式サンプルのモジュールを再利用 |
| 再利用方法 | 実験ごとの個別実装 | 共通部分と問題ごとに変更する部分を分けてテンプレート化 |
| 確認方法 | 2個のLED | Thonnyのシェル上の送受信ログ |
前回は、RP2040とFPGAの間にある信号線を確認し、1bitの状態を直接渡しました。
今回は、公式サンプルのSPI Slaveモジュールを取り込み、受信した1byteに1を加えて、その結果をRP2040へ返しました。
さらに、共通して利用する部分と問題ごとに変更する部分を整理し、次回以降のAtCoder実装に使える形へまとめました。
今回のまとめ
今回は、公式の spi_loopback_led サンプルから spi_target.v を取り出し、自分で作成した atcoder_spi_template プロジェクトへ組み込みました。
SPIの受信処理、送信処理、SCKやCSの扱いは spi_target.v に任せ、main.v では動作確認用として受信値に1を加える処理だけを書きました。
公式サンプルからSPI Slave機能を取り込む
↓
共通部品としてmain.vから呼び出す
↓
受信データを問題ごとに変更する処理へ渡す
↓
処理結果をRP2040へ返す
↓
FPGA側とMicroPython側をテンプレートとして整理する
公式サンプルに含まれる便利な部品を、自分のコードへ取り込んで使えることを確認できました。
また、main.v と atcoder_spi_template_test.py に共通部分と問題ごとに変更する部分を明示し、フォルダ構成と役割分担も整理しました。
これで、Shrike-Lite上のRP2040からFPGAへデータを渡し、FPGA側で処理し、結果をRP2040へ返すためのAtCoder実装テンプレートが完成しました。
準備編のまとめ
これで、準備編として予定していた5回が終わりました。
| 回 | 確認したこと |
|---|---|
| 準備編① | Shrike-Liteの概要と開発環境 |
| 準備編② | RP2040側のMicroPython開発ループ |
| 準備編③ | FPGA側のVerilog開発ループ |
| 準備編④ | GPIOによる1bit信号の受け渡し |
| 準備編⑤ | 公式SPI Slave機能の再利用とAtCoder実装テンプレートの作成 |
最初は、RP2040側とFPGA側で別々にLEDを点滅させました。
次に、回路図を確認して、RP2040からFPGAへ1bit信号を渡しました。
そして今回は、公式サンプルに含まれるSPI Slaveモジュールを自分のプロジェクトへ取り込み、受信した1byteに1を加えて返す処理を行いました。
さらに、FPGA側とMicroPython側を共通部分と問題ごとに変更する部分に分け、今後のAtCoder実装に再利用できるテンプレートとして整理しました。
これで、Shrike-Lite上のRP2040とFPGAを組み合わせて、AtCoder問題の処理を試すための準備が整いました。
次回
次回からは、今回完成した atcoder_spi_template を使い、AtCoderの問題を実際にShrike-Liteへ実装してみます。
問題ごとに考える必要があるのは、主に次の部分です。
- 問題の入力をどのようなデータとしてFPGAへ渡すか
- RP2040とFPGAをどう役割分担するか
- FPGA側でどのような判定や演算を行うか
- 結果をどのような形式で返すか
- どのようにテストするか
SPI通信部分には、今回取り込んだ spi_target.v と、テンプレートに整理した共通処理を利用できます。
普通にプログラムを書けば簡単に解ける問題を、あえてRP2040とFPGAへ分けて実装するとどうなるのか。
いよいよ次回から試してみます。






