はじめに

Wi-Fi を使わずに、Pico と同じ形の基板 1 枚で有線 LAN に出る。WIZnet の W55RP20-EVB-Pico は、そのためのボードです。この記事のゴールは、箱から出して USB と LAN ケーブルをつなぎ、DHCP で IP をもらい、PC から ping が通り、TCP でデータを送って同じものが返ってくるところまで。そこへ行く前に確かめたいのは 2 つです。中で RP2040 と Ethernet チップがどうつながっているのか、そして「ハードウェア TCP/IP」と呼ばれるチップの仕組みを、手元のコードが本当に使っているのか。だからこの記事では、内部配線と TCP/IP の役割分担を図で押さえてから、Arduino と公式 C 例題の 2 つの経路で同じ最初の一歩を通します。

センサーノードや Web サーバー、速度の比較といった応用は、次の記事以降で扱います。ここは基礎だけです。

📌 この記事でわかること
  • W55RP20 は RP2040 と W5500 を 1 パッケージにした SiP。Pico と同じピン配置だが、使えない GPIO がある
  • RP2040 と W5500 は SPI でつながっていて、そのピンがハードウェア SPI の割り当てに合わないので PIO で SPI を作っている
  • 同じ基板に 2 つの使い方 がある。チップの TCP/IP を使う(公式 C 例題)か、生フレームだけ受け取ってソフトウェアで処理する(Arduino の arduino-pico)か
  • 最初の一歩は DHCP → ping → TCP エコー。両方の経路で手順を示す

🧩 W55RP20-EVB-Pico は何者か

W55RP20-EVB-Pico。RJ45 の隣に W55RP20 が 1 チップで載り、USB-C の手前に BOOTSEL と RUN の 2 つのボタンが並ぶ。シルクの 20・21・40 はピン番号

W55RP20-EVB-Pico。RJ45 の隣に W55RP20 が 1 チップで載り、USB-C の手前に BOOTSEL と RUN の 2 つのボタンが並ぶ。シルクの 20・21・40 はピン番号

RP2040 と W5500 を 1 パッケージにした「ioNIC」

W55RP20 は、Raspberry Pi の RP2040(Pico のマイコン)と、WIZnet の有線 LAN コントローラ W5500 を 1 つのパッケージに収めたチップです。WIZnet はこの系統を ioNIC(Internet Offload Network Interface Chip)と呼んでいて、W55RP20 はその第 1 弾として 2024 年 11 月に出ました(スイッチサイエンスマガジンの WIZnet 寄稿)。

RP2040 と W5500 を 1 つの外形にまとめた SiP(System in Package)なので、ソフトウェアから見ると「RP2040 の GPIO に W5500 が SPI でつながっている」状態そのものです。Pico に W5500 のモジュールを配線したのと同じ構成が、最初からパッケージの中で済んでいる、と考えるとわかりやすいです。

評価ボードの W55RP20-EVB-Pico は、これを Pico と同じ外形・同じピン配置の基板に載せ、RJ45 コネクタと USB Type-C を付けたものです。

項目 仕様(WIZnet Docs・スイッチサイエンス商品ページ)
マイコン RP2040 相当(Cortex-M0+ デュアル・最大 133 MHz・SRAM 264 KB)
フラッシュ 2 MB(パッケージ内蔵)
Ethernet W5500 相当(10/100BASE-T・PHY 内蔵・オートネゴシエーション)
ハードウェア TCP/IP TCP・UDP・ICMP・IPv4・ARP・IGMP・PPPoE
ソケット 8 本を同時に使える。送受信バッファは合計 32 KB
コネクタ RJ45(PoE 対応)・USB Type-C・3 ピン SWD
PoE 別売の WIZPoE-P1 をスルーホールに付ける(ピンソケットは未実装)

Pico と同じところ、違うところ

外形とピン配置は Pico と同じですが、内部で W5500 に使っているぶん、外に出ている GPIO は減っています。

Raspberry Pi Pico W55RP20-EVB-Pico
外に出ない GPIO GP23〜25(基板内で使用) GP17・GP20〜25
電源モード切替 GP23(SMPS) GP16(DC/DC モード選択)
VBUS 検出 GP24 GP18
オンボード LED GP25 GP19
VSYS/3 の測定 GP29(ADC3) GP29(ADC3)で同じ
USB Micro-USB USB Type-C

ボードの機能に使っている GP16・GP18・GP19 も含めると、汎用 GPIO として気兼ねなく使えるのは GP0〜15 と GP26〜28 の 19 本と考えるのが安全です。Zephyr のボード定義も、ピンヘッダとして定義しているのはこの 19 本だけです。

⚠️ Pico のコードを流用するときの注意

Pico で LED_BUILTIN(GP25)や GP20〜22 を使っていたコードは、そのままでは動きません。GP25 は W5500 のリセット、GP20〜23 は W5500 の SPI につながっています。LED は GP19 です。

40 ピンのうち GPIO と GND 以外の 7 本(VBUS・VSYS・3V3_EN・3V3・ADC_VREF・AGND・RUN)は Pico と同じ意味です。3V3_EN を GND に落とすと 3.3 V が止まり、RP2040 と W5500 の両方の電源が落ちる点だけ、Pico より影響範囲が広くなっています。

「Pico + W5500 モジュール」と比べて何がうれしいか

これまで Pico で有線 LAN をやるなら、W5500 のモジュールを SPI で配線するのが定番でした。W55RP20-EVB-Pico はそれを 1 枚にしたものです。比べると次のようになります。

Pico + W5500 モジュール W55RP20-EVB-Pico
配線 SPI 4 本+INT・RST・電源をジャンパで 0 本(パッケージ内で完結)
面積 Pico + モジュール 2 枚ぶん Pico 1 枚ぶん
自由に使える GPIO 約 20 本(Pico の 26 本から SPI・INT・RST の 6 本を引く) 19 本(GP0〜15・GP26〜28)で、ほぼ同じ

使える GPIO の数はほとんど変わらないので、うれしいのは配線と面積です。SPI の配線の取り違えや、ジャンパ線の接触不良で W5500 が見えない、という定番のつまずきが最初から起きません。


🔗 RP2040 と W5500 はどうつながっているか

内部配線:SPI 4 本+INT・RST

パッケージの中で、RP2040 の GPIO20〜25 が W5500 につながっています。ピンの割り当ては、公式 C 例題(WIZnet-PICO-C)の port/ioLibrary_Driver/inc/wizchip_spi.h、arduino-pico の w55rp20.h、Zephyr のボード定義で一致しています。

flowchart TD subgraph PKG["W55RP20(1 パッケージ)"] P0["RP2040 の PIO
(SPI を実装)"] G["RP2040 の
GPIO24・GPIO25"] S["W5500
SPI スレーブ"] IR["W5500
INTn・RSTn"] PHY["W5500
MAC / PHY"] P0 -- "CS・SCK・MOSI
(GPIO20・21・23)" --> S S -- "MISO(GPIO22)" --> P0 IR -- "INTn → GPIO24" --> G G -- "GPIO25 → RSTn" --> IR S --> PHY end PHY --- RJ["RJ45"]
RP2040 の GPIO W5500 側 向き
GPIO20 CS(チップセレクト) RP2040 → W5500
GPIO21 SCK(クロック) RP2040 → W5500
GPIO22 MISO(W5500 からのデータ) W5500 → RP2040
GPIO23 MOSI(W5500 へのデータ) RP2040 → W5500
GPIO24 INTn(割り込み) W5500 → RP2040
GPIO25 RSTn(リセット) RP2040 → W5500

なぜハードウェア SPI ではなく PIO なのか

RP2040 にはハードウェアの SPI が 2 つありますが、どの GPIO にどの信号を出せるかは決まっています。GPIO20〜23 に出せるのは SPI0 だけで、その割り当ては次のとおりです(arduino-pico の SPI.cpp が持つ有効ピン表・RP2040 の GPIO 機能表)。

信号 SPI0 で出せる GPIO(20〜23 の範囲) W55RP20 の配線 合うか
RX(MISO) GPIO20 GPIO22 合わない
CSn GPIO21 GPIO20 (CS は GPIO 操作で代用できる)
SCK GPIO22 GPIO21 合わない
TX(MOSI) GPIO23 GPIO23 合う

SCK と MISO が入れ替わった形になっているので、ハードウェア SPI ではこの配線を駆動できません。そこで PIO(RP2040 のプログラマブル I/O)のステートマシンで SPI の波形を作っています。PIO は任意の GPIO に任意のタイミングで信号を出せるので、ピンの割り当てに縛られません。

一次でも確かめられます。WIZnet-PICO-C の wizchip_spi.h は W55RP20 のときだけ USE_PIO を定義し、arduino-pico の w55rp20.h は WIZNET_PIO_SPI_*_PIN という名前でピンを定義しています。Zephyr のボード定義には「W55RP20 は W5500 に PIO ベースの SPI を使うので、ハードウェア SPI(spi0)は無効にする」というコメントがあります。

💡 ワード解説:PIO(プログラマブル I/O)

RP2040 に 2 ブロック(ステートマシン合計 8 個)載っている、小さな命令セットで I/O の波形を作る専用ハードウェアです。CPU とは独立に動くので、SPI・UART・WS2812 の信号などを、CPU の時間を使わずに任意のピンで出せます。W55RP20 では、このうち 1 つを W5500 との SPI に使っています。

SPI でやりとりしている中身:アドレス・制御・データ

W5500 の SPI は、1 回の通信(CS が Low の間)が 3 つの部分でできています(ioLibrary_Driver の w5500.h・w5500.c)。

CS     --+                                                    +--
         +----------------------------------------------------+
MOSI      [ Address 16bit ][ Control 8bit ][ Data N byte ... ]
                            |BSB[4:0]|RWB|OM[1:0]|
部分 中身
Address(2 バイト) 読み書きするレジスタやバッファのオフセット
Control(1 バイト) BSB=どのブロックか(共通レジスタ/ソケット n のレジスタ/送信バッファ/受信バッファ)・RWB=読み 0/書き 1・OM=長さの決め方(00 なら CS を戻すまで続ける)
Data(N バイト) 書き込む値、または読み出した値

BSB は、共通レジスタが 0、ソケット n のレジスタが 1+4n、送信バッファが 2+4n、受信バッファが 3+4n です。たとえば W5500 の版番号レジスタ(VERSIONR・オフセット 0x0039)を読むと 0x04 が返ってきます。公式 C 例題は起動時にこの値を確かめていて、SPI がうまく通らないと ACCESS ERR : VERSION != 0x04 を出して止まります。SPI の配線が見えないぶん、この 1 行が「RP2040 と W5500 が話せているか」の最初の手がかりになります。


⚙️ ハードウェア TCP/IP(TOE)とは

チップが TCP/IP を処理し、マイコンは「ソケット」に読み書きするだけ

ネットワークの通信には、何層もの手順があります。相手の MAC アドレスを調べる ARP、IP パケットの組み立て、ping に応える ICMP、届いたか確かめて再送する TCP、送りっぱなしの UDP。普通のマイコンでは、これらをソフトウェア(TCP/IP スタック、たとえば lwIP)が CPU の時間とメモリを使って処理します。

W5500 は、この処理をチップの中の回路で行います。WIZnet はこれを TOE(TCP/IP Offload Engine)、日本語で「ハードウェア TCP/IP」と呼んでいます。マイコン側から見えるのは ソケット と呼ばれる 8 本の窓口だけで、やることは次の程度です。

  1. ソケット 0 を「TCP・ポート 5000」で開く(レジスタに書く)
  2. 「待ち受けて」と命令する
  3. 接続されたら、受信バッファに溜まったデータを SPI で読む
  4. 返したいデータを送信バッファに SPI で書き、「送って」と命令する

再送やウィンドウ制御、ARP の問い合わせ、ping への応答は、チップが勝手にやります。送受信バッファは送信 16 KB・受信 16 KB の合計 32 KB で、8 本のソケットに 1・2・4・8・16 KB 単位で割り振ります(既定は各 2 KB)。

もう 1 つの使い方:MACRAW + ソフトウェアスタック

W5500 には、TCP/IP の処理を使わずに イーサネットフレームをそのままマイコンに渡すモード(MACRAW) もあります。この場合、ARP も IP も TCP も、マイコン側のソフトウェアスタックが処理します。W5500 は「PHY 付きの LAN の口」として働くだけになります。

flowchart LR subgraph A["ハードウェアソケット(TOE)"] direction TB A1["アプリ
socket / send / recv"] --> A2["SPI"] A2 --> A3["W5500 の中で
TCP・UDP・IP
ARP・ICMP"] A3 --> A4["MAC / PHY
→ LAN"] end subgraph B["MACRAW + lwIP"] direction TB B1["アプリ
WiFiClient など"] --> B2["RP2040 上の lwIP が
TCP・UDP・IP
ARP・ICMP"] B2 --> B3["SPI(生フレーム)"] B3 --> B4["W5500 は
MAC / PHY だけ
→ LAN"] end A ~~~ B

同じ基板で、経路によって使い方が違う

ここが基礎として押さえておきたいところです。W55RP20-EVB-Pico を Arduino(arduino-pico)で使うと、W5500 のハードウェア TCP/IP は使われていません。

arduino-pico 6.3.1 の lwIP_w55rp20 ライブラリは、ヘッダに「original sources: W5500MacRaw」と書かれていて、begin() の中でソケット 0 を MACRAW モードで開き、16 KB ずつのバッファを全部ソケット 0 に割り当てています。TCP/IP は RP2040 上の lwIP が処理します。一方、公式の C 例題(WIZnet-PICO-C)は WIZnet の ioLibrary を使い、ハードウェアソケットで通信します。

Arduino(arduino-pico 6.3.1) 公式 C 例題(WIZnet-PICO-C)
W5500 の使い方 MACRAW(生フレーム) ハードウェアソケット(TOE)
TCP/IP を処理する場所 RP2040 上の lwIP W5500 の中
アプリの書き方 WiFiClient・WiFiServer(Wi-Fi と同じ API) socket()・listen()・send()・recv()
同時接続の上限 lwIP の設定しだい ソケット 8 本

どちらが良い・悪いではありません。arduino-pico の方式は Pico W の Wi-Fi と同じ API で書けるので、Wi-Fi 用のコードをほぼそのまま有線に持ってこられます。ハードウェアソケットの方式は、RP2040 の CPU とメモリを TCP/IP に使わずに済みます。WIZnet には、lwIP の API のまま TOE と MACRAW を切り替えられる WIZnet-PICO-LWIP-TOE-C という例題集もあります。どちらが速いか、CPU をどれだけ使うかは、応用編で測ります。

💡 所感:チップの看板機能を使わない経路が標準、という面白さ

W5500 の売りはハードウェア TCP/IP なのに、いちばん手軽な Arduino の経路では、それを使わずに PHY 付きの口として使っています。逆に言えば、W55RP20 は「RP2040 + 有線 LAN の口」としても「TCP/IP を肩代わりしてくれるチップ」としても使える、ということです。Wi-Fi のコードを有線に持ってきたいのか、RP2040 の CPU をアプリに全部使いたいのかで、経路を選べます。


🛠️ 開発環境を整理する

この記事で実際に通すのは、(a) Arduino IDE と (b) VS Code + 公式 C 例題の 2 つです。

経路 使うもの 向いている人
(a) Arduino Arduino IDE + arduino-pico 6.3.1 Pico や Pico W を Arduino で触っている人
(b) C/C++ VS Code + Raspberry Pi Pico 拡張 + WIZnet-PICO-C Pico SDK で書く人・ハードウェアソケットを使いたい人
(c) そのほか MicroPython・CircuitPython・Zephyr 公式の例題やボード定義がある(この記事では扱わない)

(a) Arduino IDE + arduino-pico

  1. Arduino IDE の「ファイル → 基本設定 → 追加のボードマネージャの URL」に次を足す

    https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
    
  2. ボードマネージャで「Raspberry Pi Pico/RP2040/RP2350」(earlephilhower 版)を入れる。この記事では 6.3.1 を使います

  3. 「ツール → ボード」で WIZnet W55RP20-EVB-Pico を選ぶ

  4. 例題は「ファイル → スケッチ例 → lwIP_w55rp20 → WiFiClient-W55RP20」にあります

ライブラリ lwIP_w55rp20 はコアに同梱されているので、別途入れるものはありません。

(b) VS Code + WIZnet-PICO-C

  1. VS Code に Raspberry Pi Pico 拡張を入れる(Pico SDK とツールチェーンは拡張が用意します) 拡張の入れ方・blink での動作確認・Import の手順は Pico SDK の環境構築の記事にまとめています

  2. 例題を submodule ごと取得する

    git clone --recurse-submodules https://github.com/WIZnet-ioNIC/WIZnet-PICO-C.git
    

    libraries/ の下に ioLibrary_Driver・mbedtls・pico-sdk が submodule として入ります。--recurse-submodules を付けないと、これらが空のフォルダになります

  3. ルートの CMakeLists.txt でボードを選ぶ。取得した時点(main・2026-10-06 版)では W6300_EVB_PICO2 が有効になっているので、次のように書き換えます

    # set(BOARD_NAME W6300_EVB_PICO2)
    set(BOARD_NAME W55RP20_EVB_PICO)
    

    W55RP20_EVB_PICO を選ぶと、_WIZCHIP_=W5500 と、ピン定義(PIO SPI)が自動で選ばれます

  4. VS Code の Raspberry Pi Pico 拡張でこのフォルダをプロジェクトとして読み込み、ビルドする。build/examples/dhcp_dns/ などに .uf2 ができます

UF2 の書き込み方(BOOTSEL を押しながら USB をつなぎ、RPI-RP2 ドライブにコピー)は Pico と同じです。UF2 の仕組みは自作ロジアナ #2 で書いています。


🚀 最初の一歩:DHCP → ping → TCP エコー

ここからが手順です。家庭のルーターのように DHCP サーバーがある LAN を前提にしています。

flowchart TD A["① LAN ケーブルと
USB-C をつなぐ"] --> B["② (a) スケッチを書き込み
DHCP の IP と MAC を見る"] B --> C["③ PC から ping"] C --> D["④ (a) TCP エコー
PC から送って返ってくるか"] D --> E["⑤ (b) 公式 C 例題で
dhcp_dns と loopback"] E --> F["⑥ LED を見る"]

① LAN ケーブルと USB-C をつなぐ

ルーターやスイッチングハブから LAN ケーブルを RJ45 に、PC から USB-C をつなぎます。電源は USB から取ります。

USB だけをつないで LAN ケーブルを挿していない間は、RJ45 の LED は点きません。LAN ケーブルを挿すと、緑が点灯し、オレンジが点滅します。LED の見分け方は⑥で扱います。

USB をつなぐと、PC には COM ポート(USB シリアルデバイス)が 1 つ現れます。USB の ID(VID 2E8A・PID 000A)から見ると、出荷時に入っているのは Pico SDK で作られたファームです。シリアルを開いても何も出てきません。

② DHCP で IP をもらい、IP と MAC をシリアルに出す(Arduino)

arduino-pico の例題 WiFiClient-W55RP20 は外部のサーバーにつなぎに行くので、LAN の中だけで完結するように、DHCP で IP をもらって表示し、TCP 5000 番で受けたデータをそのまま返すスケッチにしました。④の TCP エコーも、このスケッチ 1 本で試せます。

// W55RP20-EVB-Pico: DHCP で IP を取り、TCP 5000 番でエコーする(arduino-pico 6.3.1)
#include <W55RP20lwIP.h>

Wiznet55rp20lwIP eth(20);   // CS = GPIO20(基板の内部配線)
WiFiServer server(5000);    // TCP エコー用のポート

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 5000) {}  // USB シリアルを開くまで最大 5 秒待つ

  if (!eth.begin()) {                    // MAC 省略 = チップ固有 ID から生成
    Serial.println("W5500 not found");
    while (true) delay(1000);
  }
  Serial.print("DHCP");
  while (!eth.connected()) {             // IP が付くまで待つ
    Serial.print('.');
    delay(500);
  }
  Serial.println();

  uint8_t mac[6];
  eth.macAddress(mac);
  Serial.printf("MAC : %02X:%02X:%02X:%02X:%02X:%02X\n",
                mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  Serial.print("IP  : "); Serial.println(eth.localIP());
  Serial.print("MASK: "); Serial.println(eth.subnetMask());
  Serial.print("GW  : "); Serial.println(eth.gatewayIP());

  server.begin();
  Serial.println("TCP echo server on port 5000");
}

void loop() {
  WiFiClient client = server.accept();
  if (!client) return;
  Serial.print("client: "); Serial.println(client.remoteIP());
  while (client.connected()) {
    int n = client.available();
    if (n > 0) {
      uint8_t buf[128];
      n = client.read(buf, min(n, (int)sizeof(buf)));
      client.write(buf, n);              // 受け取った分をまとめて返す
    }
    delay(50);                           // lwIP のポーリング(既定 20 ms)が走る間を空ける
  }
  client.stop();
  Serial.println("closed");
}

loop() の delay(50) は飾りではありません。これを抜くと、エコーの途中から基板が ping にも応えなくなります。理由は④の後で説明します。

ボード「WIZnet W55RP20-EVB-Pico」を選び、Arduino IDE から書き込みます。BOOTSEL ボタンを押す必要はありません。 IDE がシリアルポートを 1200 bps で開き直す合図で基板が書き込みモードに入り、RPI-RP2 ドライブが一瞬現れて、UF2 が自動でコピーされます。スケッチの大きさは約 115 KB(フラッシュの 5%)、グローバル変数は約 68 KB(RAM の 26%)です。

⚠️ 書き込むと COM ポートの番号が変わる

出荷時のファーム(Pico SDK 製)と arduino-pico では、USB の製品 ID(PID)が違います。出荷時は 000A、arduino-pico のスケッチを書き込むと 1029 になり、Windows は別のデバイスとして扱って COM ポートの番号を振り直します(筆者の PC では COM12 → COM13)。書き込み後にシリアルモニタが開けないときは、「ツール → ポート」で新しい番号を選び直してください。

シリアルモニタを 115200 bps で開くと、次のように表示されます(IP などは筆者の LAN の例です)。

DHCP.....
MAC : 3A:38:36:3A:00:19
IP  : 192.168.0.53
MASK: 255.255.255.0
GW  : 192.168.0.1
TCP echo server on port 5000

LAN ケーブルを挿した状態でリセットすると、eth.begin() から 0.5 秒(点 1 つ)で IP が付きました。LAN ケーブルを挿していないと、DHCP の後ろの点が 500 ms ごとに増え続け、ケーブルを挿すと IP が付いて先へ進みます。W5500 not found は LAN ケーブルがなくても出ません。チップの初期化は SPI の先の W5500 と話せれば通るからです。

このスケッチと④の PowerShell は、GitHub の ramtuc/wiznet-pico-basics(MIT ライセンス)に置いています。スケッチは arduino/w55rp20-evb-pico/dhcp_tcp_echo/、PowerShell は tools/tcp_echo_client.ps1 です。

💡 MAC アドレスはどう決まるか

Arduino(arduino-pico): eth.begin() に MAC を渡さないと、RP2040 の固有 ID(フラッシュのユニーク ID)から 6 バイトを作ります。先頭バイトの下位 2 ビットを「ローカル管理・ユニキャスト」に設定するので、MAC の 2 文字目(先頭バイトの下の桁)は 2・6・A・E のどれかになります。基板ごとに違う値になり、書き込み直しても変わりません。決まった値にしたいときは eth.begin(mac) で渡します。

上の表示例の 3A:38:... も 2 文字目が A です。スケッチを 5 回書き込み直しても MAC は変わらず、ルーターも毎回同じ IP を配りました。

公式 C 例題(WIZnet-PICO-C): ソースに 00:08:DC:12:34:56 が書いてあります。00:08:DC は WIZnet のベンダー番号です。同じ例題を書き込んだ基板を 2 枚同じ LAN につなぐと MAC がぶつかるので、そのときは末尾を変えてください。

③ PC から ping

シリアルに出た IP に、PC から ping を打ちます。

ping 192.168.x.x

返事をしているのは、Arduino の経路では RP2040 上の lwIP、公式 C 例題の経路では W5500 の中の回路です。同じ ping でも、応えている場所が違います。 それは数字にそのまま出ました。同じ基板・同じ LAN で 10 発ずつ打った結果です((b) は⑤の dhcp_dns を動かしている間)。

経路 応えている場所 RTT(最小/平均/最大) TTL
(a) Arduino(MACRAW + lwIP) RP2040 上の lwIP 2/13/21 ms 255
(b) 公式 C 例題(ハードウェアソケット) W5500 の回路 10 発すべて 1 ms 未満 128

どちらも損失は 0 です。(a) の RTT が 2〜21 ms と幅広いのは、④の後で説明する lwIP の 20 ms ごとのポーリングのためと考えられます。ping が W5500 に届いても、RP2040 が次にポーリングするまで返事は作られません。その待ち時間は 0〜20 ms のどこかになり、RTT の幅とおおむね重なります。(b) では W5500 がその場で返事を作るので、RP2040 の都合を待ちません。

TTL も違います。255 は lwIP の既定値です(lwIP の opt.h で IP_DEFAULT_TTL が 255、ping の返事に使う ICMP_TTL もこれに従う)。(b) で返ってきた 128 は、W5500 が返した値です。Windows の ping は返事の TTL を表示するので、どちらが応えているかを TTL で見分けられます。

④ TCP エコー(Arduino)

PC から TCP 5000 番につないで文字を送り、同じ文字が返ってくれば TCP の往復ができています。Windows なら、次を tcp_echo_client.ps1 という名前で保存して PowerShell から実行します。ファイルは UTF-8(BOM 付き)で保存してください。 Windows PowerShell 5.1 は BOM なしの UTF-8 を cp932 として読むため、日本語コメントの次の行が消え、接続できているのに返事が空になります(PowerShell 7 では起きません)。

# TCP エコーの確認 (Windows PowerShell 5.1 / PowerShell 7)
# 使い方: .\tcp_echo_client.ps1 192.168.x.x "hello W6300"
# このファイルは UTF-8 (BOM 付き) で保存してある。BOM なしで保存し直すと Windows PowerShell 5.1 が
# 日本語コメントを cp932 として読み、コメントの直後の行が消えて返事が空になる (PowerShell 7 は問題なし)
param(
  [string]$ip = "192.168.x.x",            # シリアルに出た IP (W55RP20 / W6300)
  [string]$text = "hello"                 # 送る文字列 (記事では hello W55RP20 / hello W6300)
)
$c = New-Object System.Net.Sockets.TcpClient($ip, 5000)
$s = $c.GetStream()
$s.ReadTimeout = 3000                     # 3 秒返事がなければエラーで止める (ms)
$msg = [Text.Encoding]::ASCII.GetBytes("$text`n")
$s.Write($msg, 0, $msg.Length)
$buf = New-Object byte[] 64
$got = ""
while ($got -notmatch "`n") {             # 改行が返ってくるまで読み続ける (loop)
  $n = $s.Read($buf, 0, $buf.Length)
  if ($n -le 0) { break }                 # 相手が接続を切ったら終わり (EOF)
  $got += [Text.Encoding]::ASCII.GetString($buf, 0, $n)
}
$got
$c.Close()

日本語コメントの末尾を (ms)・(loop) のような ASCII で終えているのも同じ理由で、BOM なしで保存されてしまっても行が消えないようにしています。VS Code なら右下のエンコード表示から「UTF-8 with BOM」で保存し直せます。

実行は、IP と送る文字列を引数で渡します(IP はシリアルに出た値)。

.\tcp_echo_client.ps1 192.168.x.x "hello W55RP20"

スクリプトの実行が実行ポリシーで止められる場合は、powershell -ExecutionPolicy Bypass -File .\tcp_echo_client.ps1 192.168.x.x "hello W55RP20" の形にすると、その 1 回だけ実行できます。

hello W55RP20 と表示されれば成功です。筆者の環境では 21 ms で返り、続けてもう 1 行送ると 41 ms で返りました。スクリプトが最後に $c.Close() で切ると、シリアルに client: <PC の IP> に続いて closed が出ます。2 回続けて実行しても同じように返り、その後の ping も通ります。

Read() を 1 回呼ぶだけにしないのは、TCP には「1 回の送信=1 回の受信」という区切りがないからです。返事が 2 つのパケットに分かれて届くと、1 回目の Read() は先に着いた分だけを返します。そこで、行の終わり(改行)が来るまで読み続けます。読み残しがあるまま Close() すると、Windows は普通の切断(FIN)ではなく RST(強制切断)を送るので、その意味でも読み切ってから閉じます。

Linux や macOS なら nc 192.168.x.x 5000 でつなぎ、打った行がそのまま返ってくるかを見ます。Test-NetConnection 192.168.x.x -Port 5000 は「つながるか」だけを確かめる用途に使えます(データの往復までは見ません)。

⚠️ なぜ delay(50) が要るのか:lwIP は 20 ms ごとのポーリングで動く

arduino-pico の有線 Ethernet は、lwIP を割り込みではなく 20 ms 周期のポーリングで動かしています。ドキュメント(docs/ethernet.rst)には「By default it will run every 20ms」とあり、libraries/lwIP_Ethernet/src/LwipEthernet.cpp にも _pollingPeriod = 20 があります。

落とし穴は、ユーザーのコードが lwIP を呼ぶたびに、このポーリングが先送りされることです。同じファイルのコメントに「It causes ethernet_timeout_reached to not be called when user code does frequent lwip calls」とあります。client.connected() や client.available() も lwIP の呼び出しなので、これを休みなく呼ぶループでは、受信の処理(相手からの切断・ARP・ping)が一度も走りません。

while (client.available()) client.write(client.read()); を休みなく回す書き方では、実際に次のようになりました。

  • 1 バイト目の h だけが返り、残りが返らない
  • PC が切っても closed が出ない
  • その間は ARP にも ping にも応えない(基板が止まったように見える)。ループを抜けると元に戻る

ループの 1 周ごとにポーリング周期より長く休む delay(50) を入れると直ります。delay(1) では直りませんでした。1 バイトずつ write() するのもやめ、受け取った分をまとめて返します。arduino-pico の WiFiClient は Nagle アルゴリズムが既定で有効なので(WiFiClient.cpp の defaultNoDelay = false)、1 バイトずつ書くと、先の 1 バイトの ACK が来るまで次を送らずに溜めます。PC 側の遅延 ACK と重なると、細切れの送信が待ち合いになります。

ポーリングの周期は lwipPollingPeriod(ms) で変えられますが、短くしすぎると Ethernet の処理が間に合わず固まる、とドキュメントにあります。

もう 1 つの書き方は、1 行受けて返したら、サーバー側から切る 1 往復型です。client.readStringUntil('\n') で 1 行読み、client.println() で返して client.stop() します。arduino-pico の WiFi ライブラリにある WiFiServer の例題も、1 行受けて返したら接続を終える形です。ループで lwIP を呼び続けないので同じ問題が起きず、この形でも 2 回続けて返り、ping も通りました。その代わり、1 回の接続で送れるのは 1 行だけです。

⑤ 同じことを公式 C 例題で(ハードウェアソケット)

今度は W5500 のハードウェアソケットで同じことをします。WIZnet-PICO-C の 2 つの例題を順に使います。Import とビルドの手順は Pico SDK の環境構築の記事の④にあります。RP2040 の基板は Windows では Zadig を入れないと拡張の Run が使えないので、ここではビルドしてできた .uf2 を BOOTSEL のドライブにコピーして書き込みました。

dhcp_dns(examples/dhcp_dns/wizchip_dhcp_dns.c)は、DHCP で IP をもらって表示し、www.wiznet.io の IP を DNS で引きます。取得した時点のソースでは .dhcp = NETINFO_DHCP になっているので、そのままビルドして wizchip_dhcp_dns.uf2 を書き込み、USB の COM ポートをシリアル端末で開いてリセットします(例題の出力は USB シリアルに出る設定です)。

dhcp_dns の出力(シリアル端末は Tera Term)。MAC はソースに書かれた 00:08:DC:12:34:56、IP は DHCP でもらった値(192.168.0.x は筆者の LAN の例)。最後の 2 行が www.wiznet.io を DNS で引いた結果

dhcp_dns の出力(シリアル端末は Tera Term)。MAC はソースに書かれた 00:08:DC:12:34:56、IP は DHCP でもらった値(192.168.0.x は筆者の LAN の例)。最後の 2 行が www.wiznet.io を DNS で引いた結果

起動直後の [PIO SPI CLOCK SPEED : 50.00 MHz] (sys=200.00 MHz) は、この例題が RP2040 を 200 MHz で動かし、PIO で作った SPI を 50 MHz で回していることを示しています。続いて DHCP client running の後に MAC・IP・サブネットマスク・ゲートウェイ・DNS サーバーが並び、DHCP leased time : 7200 seconds(リース 2 時間)、DHCP success と出ます。DNS は DNS success で、www.wiznet.io は 183.111.138.249 に引けました。SPI の節で触れた ACCESS ERR : VERSION != 0x04 は出ず、RP2040 と W5500 は SPI で話せています。

loopback(examples/loopback/wizchip_loopback.c)は、TCP 5000 番で受けたデータを返すサーバーです。こちらは 固定 IP(既定は 192.168.11.2)なので、自分の LAN に合わせて書き換えてからビルドします。

static wiz_NetInfo g_net_info = {
    .mac = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}, // MAC address
    .ip = {192, 168, 11, 2},                     // ← LAN 内の空いている IP に
    .sn = {255, 255, 255, 0},                    // Subnet Mask
    .gw = {192, 168, 11, 1},                     // ← ルーターの IP に
    .dns = {8, 8, 8, 8},                         // DNS server
    // ...
};

ここでは筆者の LAN に合わせて、IP を 192.168.0.250、ゲートウェイを 192.168.0.1 に書き換えました(どちらも筆者の LAN の例です)。

loopback の起動表示。network configuration が static(固定 IP)になり、書き換えた IP とゲートウェイが出る。DNS はソースのまま 8.8.8.8

loopback の起動表示。network configuration が static(固定 IP)になり、書き換えた IP とゲートウェイが出る。DNS はソースのまま 8.8.8.8

書き込んだら、④と同じ PowerShell(IP の引数を固定 IP に変える)で 5000 番に送ります。hello W55RP20 が 12 ms で返り、もう一度実行すると 1 ms で返りました。loopback は接続が来てもシリアルには何も出さないので、成否は PowerShell 側で見ます。その後の ping も通ります。

⑥ LED を見る

RJ45 の 緑はリンク、オレンジは通信のたびに点滅します。チップの横に並ぶ緑の LED 4 つは W55RP20 の LED 用のピン(SPD・LINK・DUP・ACT)につながっていて、ユーザー LED は別にある GPIO19 の LED です(回路図 V1.00)。


💡 こういうことにも使える(応用編の予告)

  • 有線のセンサーノード: Wi-Fi が届きにくい場所の温湿度を MQTT で送る(自宅センサー基盤 #3・#4 の受け側にそのままつながる)
  • 小さな Web サーバー: ブラウザから GPIO の状態を見る・切り替える
  • Modbus TCP: 工場や設備で使われる産業用プロトコルの端末にする
  • PoE 給電の機器: WIZPoE-P1 を付けて LAN ケーブル 1 本で電源も取る

🔧 トラブルシューティング

症状 見るところ 試すこと
RJ45 の LED が点かない ケーブル・ハブのポート 別のケーブル・ポートで試す。PC で同じケーブルがリンクするか確かめる
DHCP の点が止まらない ルーターの DHCP・リンク LED 固定 IP で試す(Arduino は eth.config(IPAddress(192,168,1,50), IPAddress(192,168,1,1), IPAddress(255,255,255,0)); を begin() の前に)
W5500 not found / ACCESS ERR ボードの選択・BOARD_NAME Arduino はボード「WIZnet W55RP20-EVB-Pico」、C 例題は BOARD_NAME W55RP20_EVB_PICO になっているか
ping は通るが TCP が返らない PC のファイアウォール・ポート番号 ポート 5000 で合っているか。同じ IP の機器が他にないか(MAC・IP の重複)
エコーの途中から ping も通らなくなる loop() の受信ループ ループが休まず lwIP を呼び続けている。1 周ごとに delay(50) を入れる(④の後の解説)
PowerShell の返事が空行だけ(接続も ping も通る) .ps1 の保存形式 BOM なしの UTF-8 だと Windows PowerShell 5.1 が日本語コメントの次の行を落とす。UTF-8(BOM 付き)で保存し直すか、PowerShell 7 で実行する
PowerShell に最初の数文字しか出ない PC 側の読み方 Read() 1 回で終えていないか。改行が来るまで読むループにする
書き込み後にシリアルモニタが開けない COM ポートの番号 書き込むと USB の PID が変わり番号が変わる。「ツール → ポート」で選び直す
BOOTSEL でドライブが出ない USB ケーブル 充電専用ケーブルでないか。BOOTSEL を押したまま USB を挿す
途中でリセットする・不安定 USB の給電 PC 直結の USB ポートやセルフパワーのハブにつなぎ替える

✅ まとめ

  • W55RP20-EVB-Pico は、RP2040 と W5500 を 1 パッケージにした W55RP20 を載せた Pico 互換ボード。外に出る GPIO は GP0〜15・GP26〜28 が中心で、LED は GP19
  • RP2040 と W5500 は SPI(CS 20・SCK 21・MISO 22・MOSI 23)と INT 24・RST 25 でつながる。SCK と MISO がハードウェア SPI の割り当てに合わないので、PIO で SPI を作っている
  • 同じ基板に 2 つの使い方がある。Arduino(arduino-pico)は MACRAW + lwIP、公式 C 例題(WIZnet-PICO-C)はハードウェアソケット
  • 最初の一歩は DHCP → ping → TCP エコー。両方の経路で同じ確認ができる

次の一手は、この 2 つの経路を同じ基板で比べることです。スループット、RP2040 の CPU 使用率、コードの長さがどう変わるかを、応用編で測ります。


関連記事


参考