第5章 有線通信 ― UART/SPI/I2C、そしてEthernet・Webhook/MQTTへ
前章までで、私たちのSoCは「頭脳(PicoRV32)」「机(内蔵RAM)」「基本動作(GPIO・タイマ・PWM)」「広い本棚(外部SPI Flash)」を備えました。しかしこのままでは、世界と言葉を交わせません。先輩ボードと比べたときの弱点表で何度も出てきた「通信が貧弱/無い」を、本章で正面から埋めます。
本講座の旗印は「ボードはフィジカルAIの身体、頭脳はネット越しのAI(API)」でした。通信はその身体の神経です。センサの値を外へ送り、サーバやAIからの指示を受け取る――その入り口を、UART・SPI・I2C、そして外付けEthernet(W5500)として作っていきます。
追記(専用ビルド工程の進捗)。 本章の中心である UART(uart_wb)と SPI(spi_wb)は、専用ビルド工程 M4 でシミュレーション検証が完了しました(UART自己ループバックとSPI送受の1バイト往復が PASS)。さらに第6章の結合(M5)でも両者の再利用が PASS しています。I2C(i2c_master)と Ethernet(W5500)は本講座の本筋(M1〜M5)の外で、引き続き骨格であり、採用時に同じ工程で検証します([改訂マーカー:I2C IOBUF極性/W5500実装詳細])。
5.1 なぜ「有線通信」から始めるのか
理由は三つあります。第一に、先輩ボードの弱点を最も素直に上回れる領域だからです。第二に、無線を積まず技適済みドングルをUSBで外付け=有線機器として扱うという本講座の設計思想(方針[M])の土台になるからです。第三に、ここがIP調達7ステップの反復練習に最適だからです。性格の違う三つのIP(UART・SPI・I2C)を続けて迎えることで、「方言の橋渡し」をいろいろな角度から体験できます。
先輩はどうしているか
Arduino UNO や CH32V003 は、UART・SPI・I2C をチップ内蔵の固定ペリフェラルとして持ちます。便利ですが、中身は変えられません。
私たちはどうするか
通信ペリフェラルを自分でWishboneバスに接ぐ。どのIPを選び、どう橋渡しするかを全部自分で決められる――この自由こそ差別化です。
そして本章には、隠れた主役がもう一人います。「Wishboneへの寄り方が三者三様」という事実です。第2章ではCPU(picorv32_wb)を、第4章では spimemio を調達しました。本章の三つのIPは、それぞれ違う距離感でWishboneと向き合っており、まさに統合(7ステップのstep6)の生きた教科書になります。
IP リセット 幅 / アドレス Wishbone との距離
───────────────────────────────────────────────────────────────────────
picorv32_wb wb_rst_i 正論理 32bit B4 マスタ(基準)
simpleuart resetn 負論理 生レジスタ方言 非Wishbone(重い橋)
simple_spi rst_i 非同期・負論理 8bit / 2bitアドレス ネイティブ(軽い橋)
i2c_master wb_rst_i 同期・正論理 8bit / 3bitアドレス B2 準拠(物理層に注意)
+arst_i 非同期
5.2 UART ― simpleuart を7ステップで調達する
最初の通信は、いちばん基本的なUART(調歩同期シリアル)です。第2章で「Hello相当」をMMIO LED書き込みで済ませた約束([D-05])を、ここで本物のシリアル通信として回収します。
- ①要求を言語化する。 対応バス=最終的にWishboneへ載せたい/機能=1バイトの送受信ができる調歩同期シリアル/リソース=小さく/ライセンス=寛容(ISC・BSD・MIT級)。
- ②探索先を定める。 第4章で spimemio を採った PicoSoC(公式参照SoC, picorv32/picosoc/)と同じ畑をまず当たります。同じ作者・同じ作法のIPは、統合の手間も読み解きも揃いやすいからです。
- ③候補を絞る。 PicoSoC同梱の simpleuart を第一候補に。理由は、同作者・同ライセンス・小型・実SoCでの実績、の4点。
- ④現物を確認する(一次情報を自分の目で)。 取得元は raw.githubusercontent.com/YosysHQ/picorv32/master/picosoc/simpleuart.v(137行・単一モジュール)。 確認できた事実:著作権は © 2017 Claire Xenia Wolf(picorv32本体と同一人物・同一表記)、ライセンスは ISC形式("Permission to use, copy, modify, and/or distribute … AS IS")。第4章までと統一的に扱えます。
肝心なのはインターフェースの方言です。引き継ぎ段階では「素I/F方言」と聞いていましたが、現物はそれより単純で、PicoRV32のメモリI/F(mem_valid/mem_ready…)ですらありません。生のレジスタ・ポート直結方言でした。
┌──────────── simpleuart(生レジスタ方言)────────────┐
ラッパから │ reg_div_we[3:0] / reg_div_di[31:0] / reg_div_do[31:0] │ → ボーレート設定
駆動する │ reg_dat_we / reg_dat_re / reg_dat_di[31:0] │
ポート群 │ reg_dat_do[31:0](受信 or ~0=空) / reg_dat_wait(送信busy)│ → ser_tx / ser_rx
└─────────────────────────────────────────────────────┘
挙動の要点:
・reg_dat_do は 受信があれば受信バイト、無ければ ~0(=0xFFFFFFFF)。「読んで -1 なら空」。
・reg_dat_wait は 送信中に書こうとすると立つ=この間ホストは待たされる。
・reg_dat_re(読み)で受信バッファをクリア。clk / resetn(負論理)で動く。
実SoCでの結線も一次情報(同リポジトリの picosoc.v)で裏取りしました。分周レジスタを 0x0200_0004、データレジスタを 0x0200_0008 に置き、CPUへ返す ready を div_sel または (dat_sel かつ wait でない) で作っています。送信busyの間は ready を返さずCPUを止める――この一行が後の橋渡しの心臓部になります(UART割り込みは PicoSoC では未使用=ポーリング。本講座の「まずポーリング」方針と一致)。
- ⑤採否を判断し、理由を記録する。 採用。理由は、畑が同じで読み解きやすい・ライセンス整合・小型・実績。代償は「Wishboneでないのでラッパが要る」こと。だがそのラッパ作りこそ、第4章 spimemio(案A)で学んだ「方言の橋渡し」の良い反復になります。
-
⑥統合する(Wishboneスレーブ・ラッパ = [D-07]案A の再適用)。
生レジスタ方言を、Wishboneスレーブの薄いラッパ1枚で迎えます。ラッパの責務は4つ:(a) 2レジスタ(DIV/DATA)のアドレスデコード、(b) Wishboneの書き込み信号を
reg_*_we/reへ変換、(c) reg_dat_wait を ack の遅延に翻訳(busyの間ackを返さない=上の ready 論理を移植)、(d) リセット極性の変換(simpleuartは負論理 resetn、picorv32_wb は正論理 wb_rst_i)。
// uart_wb.v ― simpleuart を Wishbone B4 スレーブで包むラッパ(骨格/設計はM4でsim検証済)
module uart_wb (
input wb_clk_i,
input wb_rst_i, // 正論理(picorv32_wb 由来。押す=1)
input [31:0] wb_adr_i,
input [31:0] wb_dat_i,
output [31:0] wb_dat_o,
input wb_we_i,
input [3:0] wb_sel_i,
input wb_stb_i,
input wb_cyc_i,
output wb_ack_o,
output ser_tx,
input ser_rx
);
// アドレスデコード(DIV=+0x04, DATA=+0x08:PicoSoC 慣習に倣う)
wire div_sel = wb_cyc_i & wb_stb_i & (wb_adr_i[3:0] == 4'h4);
wire dat_sel = wb_cyc_i & wb_stb_i & (wb_adr_i[3:0] == 4'h8);
wire [31:0] reg_div_do, reg_dat_do;
wire reg_dat_wait;
simpleuart uart_i (
.clk (wb_clk_i),
.resetn (~wb_rst_i), // 極性変換:正論理→負論理
.ser_tx (ser_tx),
.ser_rx (ser_rx),
.reg_div_we (div_sel & wb_we_i ? wb_sel_i : 4'b0),
.reg_div_di (wb_dat_i),
.reg_div_do (reg_div_do),
.reg_dat_we (dat_sel & wb_we_i),
.reg_dat_re (dat_sel & ~wb_we_i),
.reg_dat_di (wb_dat_i),
.reg_dat_do (reg_dat_do),
.reg_dat_wait(reg_dat_wait)
);
assign wb_dat_o = div_sel ? reg_div_do : reg_dat_do;
// ack:DIV は即時、DATA は送信 busy(wait) が明けてから返す
// ※この一本が PicoSoC の ready 論理の移植。タイミング最終形は専用工程で詰める
assign wb_ack_o = div_sel | (dat_sel & ~reg_dat_wait);
endmodule
- ⑦検証する。 ユニット(TX→RXループバックで1バイト往復)→結合(SoCに載せ、Pmod経由でPCと文字をやり取り)。シミュ→FPGAの順で確認します。詳しくは 5.8 の【確認】に集約します。
reg_dat_wait を ack に翻訳し忘れると、送信が詰まったときCPUが先へ進んでしまい、文字が化けます。「busyの間は待たせる」が約束。ボーレートは cfg_divider(クロック分周比)で決まるので、Basys 3 の 100MHz から目標ボーレートへ割る値を実機で実測します([改訂マーカー:ボーレート実測])。
5.3 SPI ― simple_spi(Wishboneネイティブ)を7ステップで
次はSPIマスタです。UARTとの対比が効きます。simpleuart は「Wishboneでない」ので重い橋(全面ラッパ)が要りましたが、SPIでは最初からWishboneを話すIPを選び、橋がどれだけ軽くなるかを見ます。
- ①要求。 SPIマスタ/Wishbone対応/CPOL・CPHA設定可/小型/寛容ライセンス。
- ②探索先。 OpenCores と、そのGitHubミラー(freecores 組織)。古典だが枯れていて読みやすい素のVerilogが揃います。
- ③候補。 simple_spi(MC68HC11E系のSPIインターフェース)。
-
④現物確認。
raw.githubusercontent.com/freecores/simple_spi/master/rtl/verilog/simple_spi_top.v(325行)。
© 2002 Richard Herveille、BSD系の寛容ライセンス("may be used and distributed without restriction … AS IS")。
ヘッダに明記の通り 8bit WISHBONE スレーブ:
clk_i / rst_i(非同期・負論理)/ cyc_i / stb_i / adr_i[1:0] / we_i / dat_i[7:0] / dat_o[7:0] / ack_o / inta_o。SPI線はsck_o / mosi_o / miso_iの3本。レジスタは SPCR/SPSR/SPDR/SPER(68HC11命名)。
- ⑤採否。 採用。Wishboneネイティブなので橋が軽い。ただし差分は残る:8bit幅/2bitアドレス/リセット極性/CS無し。これらを薄いアダプタで吸収します。
- ⑥統合(薄いアダプタ)。 32bitのWishboneの下位バイトレーンに8bitコアを載せ、4本のレジスタを32bit境界(+0x0/4/8/C)に並べ、リセット極性を変換します。CSはGPIO側で駆動。
// spi_wb.v ― simple_spi(8bit WB) を 32bit Wishbone のバイトレーンに載せる薄アダプタ(骨格/設計はM4でsim検証済)
module spi_wb (
input wb_clk_i,
input wb_rst_i, // 正論理
input [31:0] wb_adr_i,
input [31:0] wb_dat_i,
output [31:0] wb_dat_o,
input wb_we_i,
input wb_stb_i,
input wb_cyc_i,
output wb_ack_o,
output sck_o,
output mosi_o,
input miso_i
// ※CS(スレーブセレクト)は simple_spi に無い。GPIO 側のビットで駆動する
);
wire [7:0] spi_dat_o;
simple_spi spi_i (
.clk_i (wb_clk_i),
.rst_i (~wb_rst_i), // 非同期・負論理へ極性変換
.cyc_i (wb_cyc_i),
.stb_i (wb_stb_i),
.adr_i (wb_adr_i[3:2]), // 32bit境界で4レジスタ(SPCR/SPSR/SPDR/SPER)を選ぶ
.we_i (wb_we_i),
.dat_i (wb_dat_i[7:0]), // 下位バイトに載せる
.dat_o (spi_dat_o),
.ack_o (wb_ack_o),
.inta_o (), // 割り込みは未使用(まずポーリング)
.sck_o (sck_o),
.mosi_o (mosi_o),
.miso_i (miso_i)
);
assign wb_dat_o = {24'b0, spi_dat_o}; // 8bit→32bit ゼロ拡張
endmodule
- ⑦検証。 SPI接続の外部IC(例:SPIセンサや小型ディスプレイ)を読み書き。シミュ→FPGA(Pmod経由)。
5.4 I2C ― i2c_master を7ステップで(物理層がカギ)
三つ目はI2Cです。ここでの学びは物理層(オープンドレイン)。Wishboneネイティブでも、ピンの作りが特殊だと、FPGA側で一手間が要ることを体験します。
- ①要求。 I2Cマスタ/Wishbone対応/標準・Fastモード/オープンドレイン対応/寛容ライセンス。
- ②③探索先・候補。 OpenCores の定番 I2C(freecores/i2c)。i2c_master。
-
④現物確認。
raw.githubusercontent.com/freecores/i2c/master/rtl/verilog/i2c_master_top.v(327行)。
© 2001 Richard Herveille(SPIと同じ作者)、BSD系の寛容ライセンス。ヘッダに "WISHBONE revB.2 compliant" と明記。
ポート:
wb_clk_i / wb_rst_i(同期・正論理)/ arst_i(非同期, ARST_LVL で極性指定)/ wb_adr_i[2:0] / wb_dat_i[7:0] / wb_dat_o[7:0] / wb_we_i / wb_stb_i / wb_cyc_i / wb_ack_o / wb_inta_o。 I2C線はscl_pad_i / scl_pad_o / scl_padoen_oとsda_pad_i / sda_pad_o / sda_padoen_o――つまりオープンドレインの三状態を、入力・出力・出力許可の3信号に分けて出してきます。レジスタは PRER(lo/hi)/CTR/TXR/RXR/CR/SR。
なぜ3信号に分かれているか。I2CのSCL/SDAは「Lowに引っ張るか、手を離すか」だけのオープンドレイン線で、Highは外部プルアップ抵抗が作ります。FPGAの内部信号には「手を離す(Hi-Z)」が無いので、コアは「出力値(常に0)」「出力許可」「入力読み戻し」の3本に分け、物理ピンでの合成はFPGA側のIOBUFに任せる設計なのです。
i2c_master の内部信号 FPGAのIOBUF 物理線(Pmod)
─────────────────────────────────────────────────────────────────────
scl_pad_o (常に 0) ─────┐
scl_padoen_o(出力許可) ───┼──→ [ IOBUF ] ──→ SCL ──┬── 外部プルアップ → VCC
scl_pad_i (読み戻し) ←──┘ │
I2Cデバイス
sda も同様(sda_pad_o / sda_padoen_o / sda_pad_i)
※プルアップ抵抗(数kΩ)を Pmod 側に付け忘れると、まったく通信できない(典型のつまずき)
// I2C 物理線:内部の三状態(pad_o/padoen_o)を IOBUF で本物のオープンドレインにする(骨格・未検証)
// ※IOBUF は FPGA ベンダ依存(下記は Xilinx の例。Basys 3 = Artix-7)
IOBUF scl_buf (
.I (scl_pad_o), // 常に 0(Low だけ駆動)
.T (scl_padoen_o), // 出力許可(このコアでは active-low の解釈に合わせる)
.O (scl_pad_i), // ピンの状態を読み戻す
.IO (SCL) // 物理ピン(外部プルアップ必須)
);
// sda も同じ形でもう一つ実体化する
- ⑤採否。 採用。Wishbone(B2)ネイティブ。差分は 幅(8bit/3bitアドレス)/リセット2系統/物理層オープンドレイン。
- ⑥統合。 SPIと同じ幅アダプタ+リセット極性変換に加え、IOBUFの実体化と外部プルアップ(Pmodに抵抗)が要る点が新しい。
- ⑦検証。 I2Cセンサ(温湿度など)のIDレジスタを読む→値を読む。シミュ→FPGA。
.T(出力許可)は、コアの padoen_o が active-low か否かで反転の要否が変わります。骨格段階では断定せず、専用工程で i2c_master_top.v の定義を突き合わせて確定します([改訂マーカー:IOBUF極性])。
5.5 バスブリッジ ― Wishbone ⇄ AXI4-Lite/APB と、B2/B4 の差
ここまでの三つはWishbone(または方言)でした。しかし世の多くのIP――特に大手ベンダ製――は AXI4-Lite や APB を話します。それらをWishbone B4の世界へ迎えるのがバスブリッジです。考え方だけ押さえます(具体コードは登場するIPに応じて専用工程で)。
AXI4-Lite → Wishbone
AXI4-Lite は読み/書きで独立した5チャネル(AW/W/B/AR/R)。これを「アドレス+データ+ack」のWishbone1サイクルに畳み込む薄いブリッジを噛ませます。
APB → Wishbone
APB は元々シンプル(setup→accessの2相)。Wishboneのclassicサイクルとよく似ており、ブリッジは軽量です。
もう一つ、本章には世代差も登場しました。i2c_master は Wishbone B2準拠、私たちの主バスは B4 です。幸い、古典シングルサイクル(classic standard cycle)はB2からB4までほぼ後方互換で、B4が足したのは主にバースト転送のための CTI/BTE 信号です。私たちの周辺レジスタはシングルアクセスで足りるため、B2のコアもそのまま会話できます。「同じWishboneでも版がある」と知っておくことが、将来の取り違えを防ぎます。
発展:なぜ薄いブリッジで足りるのか(クロック・幅・バースト)
ブリッジが重くなるのは主に3要因――クロックドメインが違う(非同期FIFOが要る)、データ幅が違う(パッキング/アンパッキング)、バースト転送がある(長さ管理)――のときです。本講座の周辺は、同一クロック・小さい幅・シングルアクセスが基本なので、ブリッジは状態数の少ない素直なFSMで済みます。重い構成は第14章(基板)や将来の拡張で必要になったときに、改めて7ステップで調達・自作を判断します。
5.6 Ethernet ― W5500 を 5.3 の SPI で外付けする
「有線でネットへ」の本命がEthernetです。ここで7ステップを回すと、正直で示唆に富む結論に行き着きます。
┌──────── 我々のSoC(FPGA / Basys 3)────────┐ 外部チップ 物理 │ PicoRV32 ── Wishbone ── spi_wb(simple_spi) │ ─SPI→ [ W5500 ] ─RJ45→ LAN / Internet └────────────────────────────────────────────┘ (TCP/IP内蔵) ソフト側:W5500 のソケットレジスタを SPI で読み書きして TCP/UDP を張る
具体的なスペック・型番対応・入手性は版や流通で変わる値なので、本文では断定しません。実装に入る際は WIZnet の一次情報(データシート)で、ピン配置・SPIモード・ソケットレジスタの作法を確認してください([改訂マーカー:W5500実装詳細])。本格実装(複数ポート・コネクタ)は第14章の基板化へ送ります。
5.7 上位 ― Webhook / MQTT / サーバ連携の考え方
通信線が通れば、いよいよ旗印の「ボード=身体、頭脳=ネット越しAI」が動き出します。物理層(UART/Ethernet/技適済みドングル)の上に、アプリ層のプロトコルを載せます。
Webhook(HTTP POST)
イベントが起きたら、決めたURLへJSONをPOSTする「片方向の通知」。実装が軽く、サーバ/AIのAPIへ橋渡しする最初の一歩に向きます。
MQTT(pub/sub)
ブローカを介して話題(topic)ごとに publish / subscribe する軽量プロトコル。多数のセンサ・機器を疎結合でつなぐ常時接続型に向きます。
5.8 【確認】この章の最小確認
本章で迎えた3つの通信IPを、シミュ→FPGA実機の順で最小確認します。
- UART単体(シミュ)。 uart_wb の TX→RX ループバックで、1バイトが往復することを波形で確認。
- UART結合(FPGA)。 SoCに載せ、Pmod経由でPCと文字をやり取り("hello" 往復)。本物のシリアル通信を達成。
- SPI(FPGA)。 spi_wb + GPIOのCSで、外部SPI ICのIDレジスタを読む。
- I2C(FPGA)。 IOBUF+外部プルアップを付け、I2Cセンサを読む。プルアップ忘れが最初の関門。
これで私たちのSoCは、世界と言葉を交わす神経(UART・SPI・I2C)を得ました。次章では、その神経の先に二つの実用機能をつなぎます――記憶(microSDカード=SPIの再利用)と、ネットへの声(技適済み無線モジュール=UARTの再利用)です。新しく調達するIPはゼロ。本章で迎えた部品を組み合わせ、何ができて何ができないのかを正直に見ていきます。