半導体設計民主化のための自作RISC-V SoC実践編

PicoRV32 と Wishbone で作る、フィジカルAI時代の実用オリジナルチップ

← 講義ポータルに戻る 実験並走記録 本章のRTLは骨格(教材用) UART/SPIは専用ビルド工程でsim検証済:M4 PASS

第5章 有線通信 ― UART/SPI/I2C、そしてEthernet・Webhook/MQTTへ

前章までで、私たちのSoCは「頭脳(PicoRV32)」「机(内蔵RAM)」「基本動作(GPIO・タイマ・PWM)」「広い本棚(外部SPI Flash)」を備えました。しかしこのままでは、世界と言葉を交わせません。先輩ボードと比べたときの弱点表で何度も出てきた「通信が貧弱/無い」を、本章で正面から埋めます。

本講座の旗印は「ボードはフィジカルAIの身体、頭脳はネット越しのAI(API)」でした。通信はその身体の神経です。センサの値を外へ送り、サーバやAIからの指示を受け取る――その入り口を、UART・SPI・I2C、そして外付けEthernet(W5500)として作っていきます。

本章の留保(毎章の約束)。 掲載するRTLはすべて骨格(教材用)です。完全な動くコードと、構文/import/シミュレーション/FPGA実機の検証は、設計合意のうえで専用のビルド工程に一括して回します(決定ログ [D-06])。一方、本章で名前を挙げるIP(simpleuart / simple_spi / i2c_master)は、作者・ライセンス・ポート・対応バスを一次情報(GitHubの現物)で確認済みです。確認の道筋そのものを教材として残します。

追記(専用ビルド工程の進捗)。 本章の中心である 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])を、ここで本物のシリアル通信として回収します。

  1. ①要求を言語化する。 対応バス=最終的にWishboneへ載せたい/機能=1バイトの送受信ができる調歩同期シリアル/リソース=小さく/ライセンス=寛容(ISC・BSD・MIT級)。
  2. ②探索先を定める。 第4章で spimemio を採った PicoSoC(公式参照SoC, picorv32/picosoc/)と同じ畑をまず当たります。同じ作者・同じ作法のIPは、統合の手間も読み解きも揃いやすいからです。
  3. ③候補を絞る。 PicoSoC同梱の simpleuart を第一候補に。理由は、同作者・同ライセンス・小型・実SoCでの実績、の4点。
  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 では未使用=ポーリング。本講座の「まずポーリング」方針と一致)。

  1. ⑤採否を判断し、理由を記録する。 採用。理由は、畑が同じで読み解きやすい・ライセンス整合・小型・実績。代償は「Wishboneでないのでラッパが要る」こと。だがそのラッパ作りこそ、第4章 spimemio(案A)で学んだ「方言の橋渡し」の良い反復になります。
  2. ⑥統合する(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)。
骨格(教材用・M4でsim検証済)
// 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
  1. ⑦検証する。 ユニット(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を選び、橋がどれだけ軽くなるかを見ます。

  1. ①要求。 SPIマスタ/Wishbone対応/CPOL・CPHA設定可/小型/寛容ライセンス。
  2. ②探索先。 OpenCores と、そのGitHubミラー(freecores 組織)。古典だが枯れていて読みやすい素のVerilogが揃います。
  3. ③候補。 simple_spi(MC68HC11E系のSPIインターフェース)。
  4. ④現物確認。 raw.githubusercontent.com/freecores/simple_spi/master/rtl/verilog/simple_spi_top.v(325行)。 © 2002 Richard HerveilleBSD系の寛容ライセンス("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命名)。
重要な落とし穴:このコアにハードのSS(チップセレクト)線はありません。 SPIは複数のスレーブを CS で選びますが、simple_spi は CS を自前で出しません。よって CSは第3章で作ったGPIOのビットで叩くのが定石です。「Wishboneネイティブ=無調整で載る」ではない、という良い実例です。
  1. ⑤採否。 採用。Wishboneネイティブなので橋が軽い。ただし差分は残る:8bit幅/2bitアドレス/リセット極性/CS無し。これらを薄いアダプタで吸収します。
  2. ⑥統合(薄いアダプタ)。 32bitのWishboneの下位バイトレーンに8bitコアを載せ、4本のレジスタを32bit境界(+0x0/4/8/C)に並べ、リセット極性を変換します。CSはGPIO側で駆動。
骨格(教材用・M4でsim検証済)
// 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
  1. ⑦検証。 SPI接続の外部IC(例:SPIセンサや小型ディスプレイ)を読み書き。シミュ→FPGA(Pmod経由)。

5.4 I2C ― i2c_master を7ステップで(物理層がカギ)

三つ目はI2Cです。ここでの学びは物理層(オープンドレイン)。Wishboneネイティブでも、ピンの作りが特殊だと、FPGA側で一手間が要ることを体験します。

  1. ①要求。 I2Cマスタ/Wishbone対応/標準・Fastモード/オープンドレイン対応/寛容ライセンス。
  2. ②③探索先・候補。 OpenCores の定番 I2C(freecores/i2c)。i2c_master
  3. ④現物確認。 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_osda_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 も同じ形でもう一つ実体化する
  1. ⑤採否。 採用。Wishbone(B2)ネイティブ。差分は 幅(8bit/3bitアドレス)/リセット2系統/物理層オープンドレイン
  2. ⑥統合。 SPIと同じ幅アダプタ+リセット極性変換に加え、IOBUFの実体化と外部プルアップ(Pmodに抵抗)が要る点が新しい。
  3. ⑦検証。 I2Cセンサ(温湿度など)のIDレジスタを読む→値を読む。シミュ→FPGA。
IOBUFの極性は要・実機確認。 上の .T(出力許可)は、コアの padoen_o が active-low か否かで反転の要否が変わります。骨格段階では断定せず、専用工程で i2c_master_top.v の定義を突き合わせて確定します([改訂マーカー:IOBUF極性])。

5.5 バスブリッジ ― Wishbone ⇄ AXI4-Lite/APB と、B2/B4 の差

ここまでの三つはWishbone(または方言)でした。しかし世の多くのIP――特に大手ベンダ製――は AXI4-LiteAPB を話します。それらを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ステップを回すと、正直で示唆に富む結論に行き着きます。

結論:FPGA側に載せる「W5500のRTL IP」は存在しません。 W5500 は WIZnet社のハードワイヤードTCP/IPチップで、TCP/IPスタックをチップ内部に持ち、ホストとはSPIで会話します。つまり私たちのEthernetは、5.3で作ったSPIマスタをそのまま再利用し、外部のW5500チップ(Pmod)をSPI経由で制御する話に帰着します。新規RTLの自作も新規調達も不要――「既存IPの再利用+外部チップで解く」という判断自体が、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 する軽量プロトコル。多数のセンサ・機器を疎結合でつなぐ常時接続型に向きます。

誇張はしません。 ボード単体でAI推論を回すとは約束しません。本ボードの役割は「身体」――センサで取り、通信で送り、指示で動く。重い推論はネット越しのAI(API)に委ねる、という役割分担です。具体的なプロトコル実装やライブラリは、ソフトの足場を整える第11章と、各サービス側の作法に沿って進めます。

5.8 【確認】この章の最小確認

本章で迎えた3つの通信IPを、シミュ→FPGA実機の順で最小確認します。

  1. UART単体(シミュ)。 uart_wb の TX→RX ループバックで、1バイトが往復することを波形で確認。
  2. UART結合(FPGA)。 SoCに載せ、Pmod経由でPCと文字をやり取り("hello" 往復)。本物のシリアル通信を達成。
  3. SPI(FPGA)。 spi_wb + GPIOのCSで、外部SPI ICのIDレジスタを読む。
  4. I2C(FPGA)。 IOBUF+外部プルアップを付け、I2Cセンサを読む。プルアップ忘れが最初の関門。
[改訂マーカー] 本章で実機実測して確定する値:各IPのメモリマップ番地、UARTのボーレート分周比、IOBUFの極性、追加した周辺込みのFPGA利用率。完全な動くRTL一式と上記検証は、設計合意済みの専用ビルド工程でまとめて実施します。

これで私たちのSoCは、世界と言葉を交わす神経(UART・SPI・I2C)を得ました。次章では、その神経の先に二つの実用機能をつなぎます――記憶(microSDカード=SPIの再利用)と、ネットへの声(技適済み無線モジュール=UARTの再利用)です。新しく調達するIPはゼロ。本章で迎えた部品を組み合わせ、何ができて何ができないのかを正直に見ていきます。