はじめに
今回は、AtCoder Beginner Contest 466のA問題をShrike-Liteへ実装してみます。
前回のABC456Aでは、入力値とコマンドを1byteのMOSIフレームへ収めることができました。
しかし、ABC466Aでは、入力値の範囲が-100~100です。符号付き8bitのデータが必要になるため、コマンドと入力値を1byteへ同居させることができません。
そこで今回は、次の新しい仕組みを導入します。
- 1個の入力値を2byteに分割して送る
- 各byteの上位4bitへ、そのbyteの役割を示すコマンドを入れる
- FPGAからの返信を有効にしたまま、入力値を連続して送る
- 答えが
Noに確定したら、途中でもEODを送って終了する
前回と共通するテンプレート作成やSPI初期化の説明は簡略化し、マルチバイトSPI通信を中心に整理します。
1. 問題の解法
N個の選択肢があり、各選択肢を選んだときの嬉しさがX_iとして与えられます。
すべてのX_iが負であればYes、1個でも0以上の値があればNoです。
すべての X_i < 0 なら Yes
1個でも X_i >= 0 なら No
FPGA側では、答えを表す1bitを最初に1へ設定しておきます。
ANSWER = 1
入力値を受け取るたびに符号を確認し、0以上の値があればANSWERを0へ変更します。
if X_i >= 0:
ANSWER = 0
一度0になったANSWERは、そのテストケースが終了するまで0のままです。
このため、Noが確定した時点で残りの入力値を送る必要はありません。
2. RP2040とFPGAの役割分担
今回の役割分担は次のとおりです。
| 処理 | 担当 |
|---|---|
テストケースからX_iを取り出す |
RP2040 |
| 符号付き8bitへ変換し、上位・下位4bitに分割する | RP2040 |
| SPIで2byteに分けて送る | RP2040 |
| 2個の4bitデータを8bitへ戻す | FPGA |
X_i >= 0を判定する |
FPGA |
YesまたはNoに相当する1bitを返す |
FPGA |
| 期待値との比較、表示、時間測定を行う | RP2040 |
NはFPGAへ送りません。
RP2040が入力値を順番に送り、最後にEODを送ることでデータ列の終了を知らせます。
3. マルチバイトSPI通信の方式を決める
符号付き8bitの値を送る方法として、次の三つが考えられます。
| 方針 | 内容 |
|---|---|
| コマンドにメタデータを持たせる | 各byteが上位4bitか下位4bitかをコマンドで示す |
| FPGA側のFSMで順番を管理する | 1byte目、2byte目をFPGAの状態で判別する |
| 8bitデータをそのまま流す | データの意味を通信順序だけで決める |
今回は、各byteへメタデータを持たせる方式を採用します。
MOSIの上位4bitをコマンド、下位4bitをデータとします。
bit 7 bit 4 bit 3 bit 0
+------------------------------+-------------------------------+
| COMMAND[3:0] | DATA[3:0] |
+------------------------------+-------------------------------+
1個のX_iは、次の2byteで送ります。
1byte目:SEND_HIGH + X_iの上位4bit
2byte目:SEND_LOW + X_iの下位4bit
各byte自身が役割を示すため、FPGA側で「次は上位byteか、下位byteか」を管理する受信FSMは不要です。
FPGAに優しい設計ですね。
4. MOSIコマンドとデータ形式
今回使用するコマンドは次のとおりです。
| コマンド | COMMAND | 用途 |
|---|---|---|
NOP |
0000 |
RESET後のWAIT確認 |
SEND_HIGH |
0001 |
入力値の上位4bitを送る |
SEND_LOW |
0010 |
入力値の下位4bitを送り、1個の値を確定する |
EOD |
0011 |
データ列の終了 |
DEBUG |
1110 |
デバッグ用 |
RESET |
1111 |
内部状態の初期化 |
コマンドだけを送る場合、下位4bitは0000とします。
| コマンド | 送信byte |
|---|---|
NOP |
0x00 |
EOD |
0x30 |
DEBUG |
0xE0 |
RESET |
0xF0 |
符号付き8bitへの変換
X_iは2の補数形式の符号付き8bitとして送ります。
MicroPython側では、次の式で0~255の値へ変換できます。
encoded = x & 0xFF
上位・下位4bitは次のように取り出します。
high = (encoded >> 4) & 0x0F
low = encoded & 0x0F
送信byteは次の式で作ります。
tx_high = (SEND_HIGH << 4) | high
tx_low = (SEND_LOW << 4) | low
たとえば、X_i=-100は8bitの2の補数で0x9Cです。
X_i = -100
8bit表現 = 1001_1100 = 0x9C
SEND_HIGH + 1001 = 0001_1001 = 0x19
SEND_LOW + 1100 = 0010_1100 = 0x2C
FPGAはSEND_HIGHで受け取った1001を保持し、SEND_LOWで受け取った1100と結合します。
1001 + 1100 = 1001_1100 = -100
5. MISOの返信形式
MISOは前回と同様に1byteです。
bit 7 bit 6 bit 1 bit 0
+--------+----------------------------------+--------+
| VALID | reserved | ANSWER |
+--------+----------------------------------+--------+
| bit | 名前 | 内容 |
|---|---|---|
| 7 | VALID |
有効な判定結果がある場合に1
|
| 6~1 | 予約 | 常に0
|
| 0 | ANSWER |
1ならYes、0ならNo |
返信値は次の三種類です。
| MISO | 意味 |
|---|---|
0x81 |
有効な返信、現在の答えはYes |
0x80 |
有効な返信、答えはNoで確定 |
0x00 |
まだ入力値を1個も受信していない |
RESET直後は次の状態です。
VALID = 0
ANSWER = 1
最初のSEND_LOWを受信し、1個目の入力値が完成した時点でVALIDを1にします。
それ以降、VALIDはRESETまで1のままです。
ANSWERは、負の値だけを受信している間は1を維持します。0以上の値を受信したら0へ変更し、その後は0を維持します。
6. SPI通信シーケンス
SPIではMOSI送信とMISO受信が同時に行われますが、MOSIで送ったデータがMISOで受ける受信データに即時に反映されるわけではありません。
SEND_LOWを受信した時点でFPGAが判定結果を更新するため、その結果をRP2040が受け取れるのは次のSPI転送です。
今回は、この1byte分の遅れを利用して、次の入力値のSEND_HIGHを返信読出しにも使います。
| MOSI | 同時に受信するMISO |
|---|---|
SEND_HIGH(X_i) |
X_(i-1)までの判定結果 |
SEND_LOW(X_i) |
X_(i-1)までの判定結果 |
EOD |
最後に完成した入力値までの判定結果 |
初期化
各テストケースの開始時は、前回と同様です。
RESET → NOP(WAIT)
全入力値が負の場合
すべての値をSEND_HIGH、SEND_LOWの順で送ります。
最後のSEND_LOWで更新された結果は、続くEOD送信時のMISOで読み出します。
Noが途中で確定した場合
あるX_iが0以上だった場合、FPGAはSEND_LOW(X_i)の受信後にANSWER=0とします。
RP2040は、通常は次の入力値のSEND_HIGHを送ったときに0x80を受信します。
この時点で答えは確定しているため、SEND_LOWは送らず、直ちにEODを送ります。
SEND_LOW(X_i)
↓ FPGAがANSWER=0へ更新
SEND_HIGH(X_(i+1))
↓ MISOで0x80を受信
EOD
X_(i+1)の上位4bitはFPGAへ届いていますが、SEND_LOWが送られないため入力値としては確定しません。
各byteへSEND_HIGH、SEND_LOWのメタデータを付けた効果が、このような途中終了でも現れます。
0以上の値が最後の要素だった場合は、続くEOD送信時に0x80を受信します。
テストケース終了
最終結果を受信した後は、次の順番でWAIT状態へ戻します。
EOD → RESET → NOP(WAIT)
7. FPGA側の実装方針
FPGA側で必要な主なレジスタは次の三つです。
| レジスタ | 用途 |
|---|---|
x_high[3:0] |
SEND_HIGHで受信した上位4bitを保持 |
reply_valid |
1個以上の入力値を処理したことを示す |
answer |
1ならYes、0ならNo |
コマンドごとの処理は次のようになります。
| コマンド | FPGA側の処理 |
|---|---|
SEND_HIGH |
DATA[3:0]をx_highへ保存 |
SEND_LOW |
{x_high, DATA[3:0]}を1個の値として確定し、符号を判定 |
EOD |
判定結果を保持 |
RESET |
VALID=0、ANSWER=1へ初期化 |
NOP |
状態を変更しない |
X_iが負かどうかは、8bit値の最上位bitで判定できます。
上位4bitを保持しているx_high[3](x_highレジスタの最上位ビット)が符号bitです。
x_high[3] = 1 なら負
x_high[3] = 0 なら0以上
したがって、SEND_LOW受信時の判定は概念的に次のようになります。
reply_valid <= 1'b1;
if (x_high[3] == 1'b0)
answer <= 1'b0;
一度answer=0になった後は、負の値を受信しても1へ戻しません。
MISOへ返す値は次の形です。
{reply_valid, 6'b000000, answer}
8. MicroPython側の実装方針
MicroPythonファイル名はabc466a_test.py、bitstream名はabc466a.binとします。
SPIの初期化、ピン設定、周波数、CPOL、CPHA、FPGAリセット、1byte送受信処理は既存テンプレートをそのまま使用します。
各テストケースでは次の処理を行います。
-
RESETを送る -
NOPを送りWAIT状態を確認する - 各
X_iを符号付き8bitへ変換する -
SEND_HIGHを送る - MISOが
0x80ならEODを送り、入力送信を終了する -
SEND_LOWを送る - まだ未送信データがあれば4.に戻る、すべての値を送り終えたら
EODを送る - 最終的な
VALIDとANSWERを確認する -
RESET、NOPを送りWAIT状態へ戻す
実装では、返信確認を関数にしておくと分かりやすくなります。
def decode_reply(rx):
valid = (rx >> 7) & 0x01
answer = rx & 0x01
return valid, answer
VALID=1かつANSWER=0を受信した時点で、答えはNoに確定します。
ANSWER=1の間は、残りの入力値を送信します。
テストケース
公式入力例に加え、境界値と早期終了を確認できるケースを用意します。
TEST_CASES = [
([2, 0, -1, 2], "No"),
([-5, -2, -1], "Yes"),
([-5, 0, -1], "No"),
([0, -2, 0, -1], "No"),
([-100, -1], "Yes"),
([100, -100], "No"),
([-100, -100, -100, -100, -100,
-100, -100, -100, -100, -100], "Yes"),
([-1, -1, 0], "No"),
]
各ケースについて、次の情報を1行で表示させます。
- 入力値
- 実際に送信したデータ数
- 早期終了したかどうか
- 最終RX
- 期待結果
- FPGAの結果
- PASSまたはFAIL
- 概算処理時間
テンプレートからABC466A用プロジェクトを作る
前回と同様に、atcoder_spi_templateをコピーしてABC466A用プロジェクトを作ります。
主な変更対象は次のファイルです。
abc466a/
├─ abc466a.ffpga
├─ bitstream/
│ └─ abc466a.bin
├─ ffpga/
│ └─ src/
│ ├─ main.v
│ └─ spi_target.v
└─ firmware/
└─ micropython/
└─ abc466a_test.py
spi_target.vとMicroPython側のShrike-Lite共通処理は変更しません。
問題固有部分は、main.vのコマンド処理とabc466a_test.pyのテスト・通信処理です。
AIへMicroPython実装を依頼する
依頼文は、前回と重複する共通部分を省略し、今回固有の通信仕様を中心にします。
Shrike-LiteでABC466A - Compromiseをテストするための
MicroPythonプログラムを作成してください。
既存の atcoder_spi_template_test.py を参考にして、
abc466a_test.py を新規作成してください。
bitstream名は abc466a.bin です。
Shrike-Liteへのbitstream書き込み、FPGAリセット、
SPI初期化、ピン設定、周波数、CPOL、CPHA、
1byteのSPI送受信処理は既存テンプレートをそのまま使用してください。
main.v、spi_target.v、abc466a.ffpga、abc466a.binは変更しないでください。
MOSIは上位4bitをCOMMAND、下位4bitをDATAとします。
コマンド:
- NOP = 0b0000
- SEND_HIGH = 0b0001
- SEND_LOW = 0b0010
- EOD = 0b0011
- DEBUG = 0b1110
- RESET = 0b1111
各X_iは x & 0xFF で符号付き8bitの2の補数表現へ変換し、
SEND_HIGHで上位4bit、SEND_LOWで下位4bitを送信してください。
MISOはbit7をVALID、bit0をANSWERとします。
- 0x81:有効な返信、Yes
- 0x80:有効な返信、No
- 0x00:有効な返信なし
各ケースの開始時は RESET → NOP としてください。
各入力値についてSEND_HIGHを送信し、
受信したMISOがVALID=1かつANSWER=0なら、
SEND_LOWを送らずにEODを送信して終了してください。
Noが確定していなければSEND_LOWを送信し、
次の入力値へ進んでください。
すべての入力値を送信した場合はEODを送信し、
EOD送信時に受信したMISOを最終結果として使用してください。
最終結果を受信した後は RESET → NOP でWAIT状態へ戻してください。
公式入力例、境界値、全件負、0を含むケース、
先頭・途中・末尾でNoが確定するケースをテストしてください。
各ケースについて入力、送信した値の個数、最終RX、
期待結果、結果、PASS/FAIL、概算処理時間を表示してください。
1件がFAILになっても残りのケースを継続してください。
完成したVerilogコード
以下がmain.vの問題固有部分の実装です。
// ===== 問題ごとに変更する部分 =====
// ABC466A - Compromise
localparam [3:0] CMD_NOP = 4'b0000;
localparam [3:0] CMD_SEND_HIGH = 4'b0001;
localparam [3:0] CMD_SEND_LOW = 4'b0010;
localparam [3:0] CMD_EOD = 4'b0011;
localparam [3:0] CMD_DEBUG = 4'b1110;
localparam [3:0] CMD_RESET = 4'b1111;
wire [3:0] cmd;
wire [3:0] data;
assign cmd = rx_data[7:4];
assign data = rx_data[3:0];
reg [3:0] x_high;
reg reply_valid;
reg answer;
wire next_answer;
assign next_answer = answer & x_high[3];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
x_high <= 4'b0000;
reply_valid <= 1'b0;
answer <= 1'b1;
tx_data <= 8'h00;
end else if (rx_data_valid) begin
case (cmd)
CMD_NOP: begin
end
CMD_SEND_HIGH: begin
x_high <= data;
end
CMD_SEND_LOW: begin
reply_valid <= 1'b1;
answer <= next_answer;
tx_data <= {1'b1, 6'b000000, next_answer};
end
CMD_EOD: begin
end
CMD_DEBUG: begin
end
CMD_RESET: begin
x_high <= 4'b0000;
reply_valid <= 1'b0;
answer <= 1'b1;
tx_data <= 8'h00;
end
default: begin
end
endcase
end
end
// ===== 問題ごとに変更する部分ここまで =====
前回同様、合成~bitstream生成を行います。
以下は実機合成の利用リソースレポートです。
birstreamはabc466a.binにリネームして、Shrike-LiteのUSBドライブにコピーしておきます。
完成したMicroPythonコード
次にMicroPythonコードのABC466A固有部分です。
# ===== ABC466A固有部分 =====
# ABC466A - Compromise
NOP = 0b0000
SEND_HIGH = 0b0001
SEND_LOW = 0b0010
EOD = 0b0011
DEBUG = 0b1110
RESET = 0b1111
TEST_CASES = [
([2, 0, -1, 2], "No"),
([-5, -2, -1], "Yes"),
([-5, 0, -1], "No"),
([0, -2, 0, -1], "No"),
([-100, -1], "Yes"),
([100, -100], "No"),
(
[-100, -100, -100, -100, -100,
-100, -100, -100, -100, -100],
"Yes",
),
([-1, -1, 0], "No"),
]
def make_frame(command, data=0):
return ((command & 0x0F) << 4) | (data & 0x0F)
def parse_reply(rx):
valid = (rx >> 7) & 0x01
answer = rx & 0x01
if valid == 0:
result = "NO_REPLY"
elif answer:
result = "Yes"
else:
result = "No"
return valid, answer, result
def split_signed8(x):
encoded = x & 0xFF
high = (encoded >> 4) & 0x0F
low = encoded & 0x0F
return high, low
def send_command(command):
return spi_exchange(make_frame(command, 0))
def reset_to_wait():
spi_exchange(make_frame(RESET, 0))
spi_exchange(make_frame(NOP, 0))
def run_case(values, expected):
reset_to_wait()
sent_count = 0
early_stop = 0
final_rx = 0x00
# 測定対象は、最初のSEND_HIGH直前から最終結果RXまでとする。
start_us = time.ticks_us()
for x in values:
high, low = split_signed8(x)
rx = spi_exchange(make_frame(SEND_HIGH, high))
valid, answer, result = parse_reply(rx)
if valid and answer == 0:
early_stop = 1
final_rx = rx
eod_rx = send_command(EOD)
eod_valid, eod_answer, eod_result = parse_reply(eod_rx)
if eod_valid:
final_rx = eod_rx
break
spi_exchange(make_frame(SEND_LOW, low))
sent_count += 1
else:
final_rx = send_command(EOD)
elapsed_us = time.ticks_diff(time.ticks_us(), start_us)
valid, answer, actual = parse_reply(final_rx)
passed = actual == expected
status = "PASS" if passed else "FAIL"
print(
"VALUES={} TOTAL={} SENT={} EARLY_STOP={} RX=0x{:02X} EXPECT={} RESULT={} {} TIME_US={}".format(
values,
len(values),
sent_count,
early_stop,
final_rx,
expected,
actual,
status,
elapsed_us
)
)
reset_to_wait()
return passed
total_start_us = time.ticks_us()
pass_count = 0
fail_count = 0
for values, expected in TEST_CASES:
if run_case(values, expected):
pass_count += 1
else:
fail_count += 1
total_time_us = time.ticks_diff(time.ticks_us(), total_start_us)
print(
"SUMMARY PASS={} FAIL={} TOTAL_TIME_US={}".format(
pass_count,
fail_count,
total_time_us
)
)
# ===== 問題ごとに変更する部分ここまで =====
完成したMicroPythonコードはabc466a_test.pyとして、Shrike-LiteのUSBフォルダにコピーしておきます。
動作確認
完成したコードが予定通りに使用になっているか確認してみてください。
| 確認項目 | 内容 |
|---|---|
| 2byte復元 | 正数・負数・境界値を正しく8bitへ戻せるか |
| VALID | 最初の入力値処理後から1を維持するか |
| ANSWER | 負の値だけなら1を維持するか |
| 0の判定 |
0を受信した時点で0へ変わるか |
| ラッチ | 一度0になった後、負の値で1へ戻らないか |
| EOD | 最終返信の読出しに使えるか |
| 早期終了 |
0x80受信後に残りの入力送信を停止するか |
| RESET | 次のテストケースで初期状態へ戻るか |
問題がなさそうなら、Thonnyからabc466a_test.pyを開き、実行します。
以下が手元の実機での実行結果です。
[shrike_flash] FPGA reset done
[shrike_flash] FPGA reset done
[shrike_fpga] Starting FPGA flash...
[shrike_fpga] flashing: abc466a.bin
[shrike_flash] FPGA programming done.
VALUES=[2, 0, -1, 2] TOTAL=4 SENT=1 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=1000
VALUES=[-5, -2, -1] TOTAL=3 SENT=3 EARLY_STOP=0 RX=0x81 EXPECT=Yes RESULT=Yes PASS TIME_US=1544
VALUES=[-5, 0, -1] TOTAL=3 SENT=2 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=1364
VALUES=[0, -2, 0, -1] TOTAL=4 SENT=1 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=927
VALUES=[-100, -1] TOTAL=2 SENT=2 EARLY_STOP=0 RX=0x81 EXPECT=Yes RESULT=Yes PASS TIME_US=1109
VALUES=[100, -100] TOTAL=2 SENT=1 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=990
VALUES=[-100, -100, -100, -100, -100, -100, -100, -100, -100, -100] TOTAL=10 SENT=10 EARLY_STOP=0 RX=0x81 EXPECT=Yes RESULT=Yes PASS TIME_US=4445
VALUES=[-1, -1, 0] TOTAL=3 SENT=3 EARLY_STOP=0 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=1631
SUMMARY PASS=8 FAIL=0 TOTAL_TIME_US=32901
無事テストケースで全PASSしました。最大ケースの N=10 の場合で 4.45msで処理が終わっています。ACですね。
テストケースごとのSPI転送回数は、通常終了なら2N+1回、早期終了なら2×SENT+2回です。
処理時間は想定通り、ほぼSPI転送回数に比例しているようです。(実測でSPI転送1回あたり 210~250uSec)
SPI自体は1MHzなので1byteの転送は理論上8µsです。
MicroPythonによる関数呼び出しやCS操作、SPI APIのオーバーヘッドが今の実装では支配的になっていることがわかります。
高速化を進める方法はいくつもあると思いますので、ぜひ皆さんトライしてみてください。
動作確認のおまけ (安易な高速化実験の結果報告)
RP2040の動作クロックを250MHzまで上げて同じ測定を行ってみました。
デフォルト設定クロックは125MHzです。
MicroPythonコードのbitstream書き込みとSPI初期化の間に、クロック変更コードを挟みます。
# ===== 共通部分: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)
# ===== RP2040のMCUクロック変更 =====
machine.freq(250_000_000)
print("CPU_FREQ={}MHz".format(machine.freq() // 1_000_000))
# ===== 共通部分: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)
)
結果は以下のようになりました。
CPU_FREQ=250MHz
VALUES=[2, 0, -1, 2] TOTAL=4 SENT=1 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=890
VALUES=[-5, -2, -1] TOTAL=3 SENT=3 EARLY_STOP=0 RX=0x81 EXPECT=Yes RESULT=Yes PASS TIME_US=1083
VALUES=[-5, 0, -1] TOTAL=3 SENT=2 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=988
VALUES=[0, -2, 0, -1] TOTAL=4 SENT=1 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=699
VALUES=[-100, -1] TOTAL=2 SENT=2 EARLY_STOP=0 RX=0x81 EXPECT=Yes RESULT=Yes PASS TIME_US=848
VALUES=[100, -100] TOTAL=2 SENT=1 EARLY_STOP=1 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=709
VALUES=[-100, -100, -100, -100, -100, -100, -100, -100, -100, -100] TOTAL=10 SENT=10 EARLY_STOP=0 RX=0x81 EXPECT=Yes RESULT=Yes PASS TIME_US=3067
VALUES=[-1, -1, 0] TOTAL=3 SENT=3 EARLY_STOP=0 RX=0x80 EXPECT=No RESULT=No PASS TIME_US=1167
SUMMARY PASS=8 FAIL=0 TOTAL_TIME_US=23420
N=10のテストケースで45%程度の速度向上が見込めるようです。(4.45ms → 3.07ms)
ただ、RP2040の動作クロック上限保証は200MHz(@1.15Vコア電圧)ですから、スペック範囲外のオーバークロックです。
個体によっては動かないかもしれませんし、ボードの故障などのトラブルにつながる可能性もあります。
これから暑い季節になります。200MHzを超えるクロック設定については、くれぐれも自己責任でお試しいただくようお願いします。
私は300MHz (MicroPythonでの設定可能な最高クロック) テストで見事にハングアップさせました。
今回のまとめ
今回は、ABC466Aを題材に、1個の入力値を複数byteへ分割して送るSPI通信を設計しました。
MOSIは上位4bitをコマンド、下位4bitをデータとし、符号付き8bitのX_iを次の2byteで送ります。
SEND_HIGH + 上位4bit
SEND_LOW + 下位4bit
各byteへ役割を示すコマンドを持たせたため、FPGA側で受信順を管理する必要がなくなりました。
FPGA側の判定結果は、最初の入力値を処理した後からVALIDにし、0以上の値を受信した時点でANSWERを1から0へ変更します。
SPIの1byte分の返信遅延は、次のSEND_HIGHを返信読出しにも使用することで吸収します。すべての値を送信した場合は、EODを最終返信の読出しに兼用します。
Noが途中で確定した場合は、その時点でEODを送り、残りの入力値の送信を省略します。
前回の1byte完結型から、データ分割、返信の継続、パイプライン、早期終了まで一歩進んだSPI通信になりました。
前回: Shrike-LiteでAtCoder問題を解く(6):ABC456A - Dice
次回: Shrike-LiteでAtCoder問題を解く(8):ABC466B - Representative Balls(制約版)を実装する
