2026年現在、STM32でEthernetを動かそうとしてCubeMXを叩き、生成コードがまともに動かず、フォーラムを掘っていくと「ST自身が公式に『CubeMXは動くlwIPプロジェクトを生成できません』と認めている」という地獄に行き着きます。

ちなみに私は諦めました。約3ヶ月ほど費やして試行錯誤したけどダメで普通に発狂 (一応ソフトとしては動いてはいるのですが、実用に耐えうる信頼性を自分の中で担保できなかったという意味です。)

何度かやろうとしたけど追いきれなかったので、現状把握用にメモを残しておきます。

だいたいの情報源はPavel A.氏のumbrella topicと、Adam Berlinger氏(ST中の人)のKB記事、それとstm32-hotspotのサンプルリポジトリです。

注意

  • この記事は15個くらいの個人メモをまとめた記事になっています
  • 内容の整合性についてはあんまり検証してないので、筆者は責任を負いません
  • claudeのskillsを使って構成をかけているため、文体に違いがあることがあります

TL;DR

  • CubeMX生成の ethernetif.c には致命的バグが残ったまま。そのままじゃ動かない前提で進めます
  • STM32H5はCubeMXがlwIPすら統合できない(ST公式に言われました)。手動で持ってきます
  • STM32H7はキャッシュ・MPU・メモリ配置の3点で地雷祭り
  • 動くリポジトリを参照解にして、自プロジェクトをdiffで揃えるのが現実解だと思います。
  • とりあえずH7なら stm32-hotspot/STM32H7-LwIP-Examples を見るのが事実上の公式ルートです

何でこんなにややこしいのか

STM32のlwIP周りがややこしいのは、3つのレイヤーが別チームで動いていて噛み合っていないのが根本にあります。

レイヤー実体
TCP/IPスタック本体upstream lwIPをSTがフォークしたもの (stm32-mw-lwip)
HAL ETHドライバシリーズごとに別実装。F1/F2/F4/F7用とH5/H7用で性格が違う
ethernetif.c(glue層)CubeMXが生成。バグの大半はここ

特に ethernetif.c が問題で、STフォーラムでもAdam Berlinger氏とPavel A.氏のやり取りの中で「reworkedドライバ移行後のCubeMXテンプレートには複数の致命バグがある」とST側が認めています。

つまりCubeMXに従ったらまず動かない。しょーもなさすぎる話ですが現実そうなっています。

lwIPのバージョン状況

stm32-mw-lwip リポジトリのタグを見ればわかりますが、ざっくり以下のような感じです。

  • ST配布版の最新: v2.2.0_20250106
  • セキュリティ的下限: v2.1.3_20230818(CVE-2020-22283 / CVE-2020-22284 対応。これ未満は要更新)
  • 古いCube(旧F4 v1.24以前)には 2.0.3 が入っていました。HTTPクライアントすら無いほど古いので、当たったら即更新します
  • upstream比で常に1〜2年遅れ

どのCubeパッケージにどのバージョンが入っているかは stm32-mw-lwip のREADMEに対応表があります。それを見るのが早いです。

HAL ETHドライバの世代を把握する

ググって出てくる古い記事のコードが動かない理由は、ほぼここです。

  • Legacy ドライバ: F1/F2/F4/F7の古いパッケージ。HAL_ETH_TransmitFrame 系のAPI
  • Reworked ドライバ: 以下から導入されたAPI大改修版
    • CubeF4 v1.27.0 以降
    • CubeF7 v1.17.0 以降
    • CubeH7 全般
    • CubeH5 全般

Reworked版では HAL_ETH_Transmit_IT / HAL_ETH_ReadData / HAL_ETH_BuildRxDescriptors といった新APIに変わり、ディスクリプタ管理の作法も別物になっています。

HAL_ETH_TransmitFrame が見当たらない…」となったら世代違いを疑います。

まぁこれはHAL全体に言えることですが…

既知バグ・ゴミ地雷

Pavel A.氏が “How to make Ethernet and lwIP working on STM32” に列挙している中から、特に踏みやすいやつを抜粋します。

全シリーズ共通(reworkedドライバ系)

TxPktSemaphoreの初期化バグ

CMSIS-RTOSv2を使っていると、low_level_output の中のセマフォが最大値=初期値=1になっていて、osSemaphoreAcquire が即座に戻ります。結果、TXパケットが完了する前に HAL_ETH_ReleaseTxPacket が呼ばれてドライバ状態が壊れます。CubeMX生成の ethernetif.c に残っています。

ethernet_link_thread() がスレッドセーフじゃない

linkスレッドが HAL_ETH_Stop_IT() / HAL_ETH_Start_IT() / HAL_ETH_SetMACConfig() を呼びますが、これらはディスクリプタや周辺レジスタを書き換えます。同時に ethernetif_input() 側で HAL_ETH_ReadData() も走るので、並行アクセスで壊れます。

MX_LWIP_Init() が間違ったスレッド起動関数を渡す

↑ ガチでどういうお笑い?

/* CubeMX生成(間違い) */
osThreadNew(ethernetif_input, &gnetif, &attributes);

/* 正しい */
osThreadNew(ethernet_link_thread, &gnetif, &attributes);

FW_H7 V1.6.0などのバージョンで実在しました。_IT サフィックス忘れも類似系統です。

RAW API + RTOSなのにコアロックなし

STのH7サンプル自体がRAW APIを別スレッドから直接叩いていて、lwIPの “Common pitfalls” 違反になっています。ST公式サンプルがlwIP公式ルール違反という構図です。

CubeMX設定が黙って無視される

MEM_SIZE / LWIP_RAM_HEAP_POINTER / NO_SYS / ICMP あたりは、.iocで変えても生成コードに反映されないケースが多数報告されています。再生成のたびに git diff で確認するのが鉄則です。

Cortex-M7系(F7 / H7)特有

  • Rxバッファのキャッシュライン非アライメント: __SCB_DCACHE_LINE_SIZE(32B)境界に揃っていないと、invalidateで隣接データを壊します
  • MPU領域のアドレス間違い: NUCLEO-H743ZI公式サンプルで RX_POOL が 0x30000400 なのに MPU設定が 0x30004000 を指している、というスペルミスがSTサンプルに実在しました
  • Tx側のキャッシュ問題はMPUでは救えない: TxはPBUF_ROM/PBUF_REFで任意アドレスから来うるので、low_level_outputSCB_CleanDCache_by_Addr が必須です。ST提供コードはこれをやっていません

H72x / H73x 限定の追加地雷

D2 SRAMが 32kBしかない(H74x/H75xは288kB)。アドレス 0x30040000 - 0x30048000 は存在しないので、ここにRXバッファを置くCubeMXのデフォルトでは即死します。 てめーのメモリはてめーで管理運営

RXバッファをAXI SRAM (0x24000000) に逃がし、lwIP heapだけD2に残す構成にする必要があります。

H5シリーズ

ST公式記事の冒頭にこう書いてあります。

The current STM32CubeMX release does not support the LwIP to be automatically added for the STM32H5 series, so there is no code generation feature inside the software.

新シリーズなのにそれかよ、と思いますがそういうことらしいです。stm32-mw-lwip を手動で持ってきて統合する必要があります。STの記事に手順あり。普通に去ね

PHY周り

  • CubeMXが LAN8742 を勝手に選択 / 開き直すたびに勝手にデセレクトする現象(特にH723)。.iocを開くたびにPHY設定が外れます
  • DP83848やLAN8720を使うときはPHYドライバを差し替える必要がありますが、CubeMXからは選べないことが多いです

STM32H7を動かすための実用レシピ

stm32-hotspot/STM32H7-LwIP-Examples のREADMEとAdam Berlinger氏のKB記事 “How to create a project for STM32H7 with Ethernet and LwIP stack working” を統合した手順です。これが事実上の公式ルート。

CubeMX側でやること

  1. クロックはHSE→PLLでSYSCLK 400MHz(H743の場合)。デフォルトのまま生成しません
  2. ETHを有効化。RMIIモード。PHYアドレスをボードに合わせます(Nucleo-H743のLAN8742はアドレス0)
  3. FreeRTOSをCMSIS_V1で有効化、defaultTask スタックを 512 ワード以上に
  4. LwIP middleware:
    • 固定IP(DHCP不要なら)。例: 192.168.1.10
    • Platform settings → LAN8742を選択(保存して開き直すと外れる罠あり)
    • Key Options → MEM_SIZE = 16360LWIP_RAM_HEAP_POINTER をD2 SRAM内に
    • LWIP_NETIF_LINK_CALLBACK を有効
    • Checksum → CHECKSUM_BY_HARDWARE を有効
  5. MPU:
    • Region 0: 全4GBをStrongly Orderedで被せる(STM32H7共通テンプレ)
    • Region 1: D2 SRAMのlwIP領域を Non-cacheable Normal Memory にする
    • サイズ単位を間違えると即HardFault(256KB のつもりが 256B、結構ありますねぇ!)
  6. USE_HAL_ETH_REGISTER_CALLBACKS = 0 にしておきます。1にするとTx/Rxコールバックを手動登録しないと無音になります

リンカスクリプト

STM32H743 / H753の例です。

.lwip_sec (NOLOAD) :
{
  . = ABSOLUTE(0x30000000);
  *(.RxDecripSection)     /* DMA Rx descriptors */
  . = ABSOLUTE(0x30000060);
  *(.TxDecripSection)     /* DMA Tx descriptors */
  . = ABSOLUTE(0x30000200);
  *(.Rx_PoolSection)      /* Rx Buffer Pool */
} >RAM_D2

H72x/H73xはアドレスマップが違うので、この通りでは動きません。

生成後にコードへ入れる手直し

CubeMX生成の ethernetif.c に対して、典型的にこのへんを直すことになります。

  • low_level_output() のTxPktSemaphoreループを書き直す(Txデッドロック対策)
  • ethernet_link_threadHAL_ETH_StopHAL_ETH_Stop_IT
  • MX_LWIP_Init 内の osThreadNew(ethernetif_input, ...)ethernet_link_thread
  • RTOSあり + RAW APIなら、ユーザコードは tcpip_callback() 経由でしかlwIPを叩かない

CubeMX再生成のたびに上書きされるので、USER CODE BEGIN/END の範囲に収めるか、ミドルウェアをプロジェクト外にコピーして除外コンパイルする運用が無難です。

APIの選び方

APIスレッド前提使う場面注意点
Raw / CallbackNO_SYS=1 or TCPIPスレッドのみRTOSなし or 最小構成mainloopで sys_check_timeouts()ethernetif_input() 相当を回す
NetconnRTOS必須シーケンシャルに書きたい時別スレッドから呼んでOK。STサンプルの主流
Socket (BSD)RTOS必須POSIX移植性が欲しい時最重量。lwipopts.hLWIP_SOCKET=1

RTOSなしの場合の最重要点は、MX_LWIP_Process() をメインループで呼び続けることです。ETH割込みは無効化が推奨されてるらしいです。知らんけど。

サンプルコード

TCP Echo Server - Raw API(NO_SYS)

#include "lwip/tcp.h"

static err_t echo_recv(void *arg, struct tcp_pcb *tpcb,
                       struct pbuf *p, err_t err)
{
    if (p == NULL) {
        tcp_close(tpcb);  /* 相手が閉じた */
        return ERR_OK;
    }
    if (err != ERR_OK) {
        pbuf_free(p);
        return err;
    }

    tcp_recved(tpcb, p->tot_len);

    if (tcp_sndbuf(tpcb) >= p->tot_len) {
        tcp_write(tpcb, p->payload, p->tot_len, TCP_WRITE_FLAG_COPY);
        tcp_output(tpcb);
    }
    pbuf_free(p);
    return ERR_OK;
}

static err_t echo_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
    tcp_setprio(newpcb, TCP_PRIO_MIN);
    tcp_recv(newpcb, echo_recv);
    return ERR_OK;
}

void tcp_echo_init(void)
{
    struct tcp_pcb *pcb = tcp_new();
    tcp_bind(pcb, IP_ADDR_ANY, 7);
    pcb = tcp_listen(pcb);
    tcp_accept(pcb, echo_accept);
}

int main(void) {
    /* HAL_Init, SystemClock_Config, MX_GPIO_Init, MX_LWIP_Init() ... */
    tcp_echo_init();
    while (1) {
        MX_LWIP_Process();   /* これを呼ばないとlwIPが止まる */
    }
}

MX_LWIP_Process() を呼び忘れて「ping通るのにTCP動かない」となるパターンが多い気がします。

UDP Echo - Raw API

#include "lwip/udp.h"

static void udp_echo_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p,
                          const ip_addr_t *addr, u16_t port)
{
    udp_sendto(pcb, p, addr, port);
    pbuf_free(p);
}

void udp_echo_init(void)
{
    struct udp_pcb *pcb = udp_new();
    udp_bind(pcb, IP_ADDR_ANY, 7);
    udp_recv(pcb, udp_echo_recv, NULL);
}

TCP Echo Server - Netconn API(RTOSあり)

#include "lwip/api.h"

static void tcp_echo_thread(void *arg)
{
    struct netconn *conn, *newconn;
    struct netbuf *buf;
    void *data;
    u16_t len;

    conn = netconn_new(NETCONN_TCP);
    netconn_bind(conn, NULL, 7);
    netconn_listen(conn);

    while (netconn_accept(conn, &newconn) == ERR_OK) {
        while (netconn_recv(newconn, &buf) == ERR_OK) {
            do {
                netbuf_data(buf, &data, &len);
                netconn_write(newconn, data, len, NETCONN_COPY);
            } while (netbuf_next(buf) >= 0);
            netbuf_delete(buf);
        }
        netconn_close(newconn);
        netconn_delete(newconn);
    }
    netconn_delete(conn);
}

void start_tcp_echo(void) {
    const osThreadAttr_t a = { .name="tcp_echo", .stack_size=2048,
                               .priority=osPriorityNormal };
    osThreadNew(tcp_echo_thread, NULL, &a);
}

RAW APIをアプリスレッドから安全に叩く

これを守らないと「pingは通るがTCPがランダムに壊れる」「ときどきHardFault」になります。

#include "lwip/tcpip.h"

static void do_send(void *ctx) {
    /* TCPIPスレッド側で実行される。ここではRAW APIを安全に呼べる */
    struct my_msg *m = ctx;
    tcp_write(m->pcb, m->buf, m->len, TCP_WRITE_FLAG_COPY);
    tcp_output(m->pcb);
}

/* 任意のアプリスレッドから */
tcpip_callback(do_send, &msg);                /* ノンブロッキング */
tcpip_callback_with_block(do_send, &msg, 1);  /* ブロッキング */

動くリポジトリ一覧

ゼロから作るより、動くやつを起点にした方が早いです。

リポジトリ対象構成備考
stm32-hotspot/STM32H7-LwIP-ExamplesNucleo-H743ZI / H723 / H750-DiscoLwIP + FreeRTOSST中の人が出しているので事実上の公式リファレンス。Git tagでCube/HALバージョン対応が追えます
betocool-prog/h7_lwip_freertosNucleo-H743ZILwIP + FreeRTOS上記が動かなかった人向けに手順を再構築
android-man/STM32H7_Nucleo-H743ZI_Ethernet_LwIPNucleo-H743ZINO_SYSH7でRTOS無し構成のミニ例
windsorschmidt/stm32h7-nucleo-h743zi-ethernet-lwipNucleo-H743ZINO_SYS + MakefileCubeIDEに縛られたくない人向け
Dmivaka/STM32H7-ETH-LWIPH723系LwIPテンプレH723特有のメモリ制約に対応
eziya/STM32F4_HAL_LWIP_LABF4Raw / Netconn / MQTT / mbedTLS / AWS IoTF4で実例が網羅されている。古いが追いやすい
mirzafahad/nucleo_stm32f429zi_lwip_examplesNucleo-F429ZIRaw APITCP/UDP echo clientの最小例
STMicroelectronics/STM32CubeH7 Projects/STM32H743I-EVAL/Applications/LwIP/EvalボードRTOS + NetconnST公式。HTTP/TFTP/iperfがひと通り揃う
STMicroelectronics/stm32-mw-lwip全シリーズlwIP本体(ST フォーク)Middlewareを直接差し替えたい時

詰まったときのチェックリスト

pingが通らない / TCPが刺さる時に上から順に潰します。

  1. PHYのリンクは上がっているか — HAL_ETH_GetLinkState() で確認。LEDが光っているか
  2. PHYアドレスは合っているか — ボード固有。LAN8742 (Nucleo)は普通 0
  3. MACアドレスがユニークか — 同一セグメントに同じMACがいるとARPが壊れます
  4. MII/RMII切替のSYSCFG — Legacyドライバ系でSYSCFGクロックを開けてから切り替えないと動きません
  5. DMAバッファの配置 — D1/D2 SRAM内か。リンカマップで RxDecripSection / TxDecripSection / Rx_PoolSection のアドレスを確認
  6. MPU設定 — 該当領域がNon-cacheableか。サイズ単位(B/KB/MB)に注意
  7. DCache — 一度 SCB_DisableDCache() で切ってみます。これで動けばキャッシュコヒーレンシ問題確定
  8. PBUF_RAM でallocしているか — PBUF_POOL だとD2に置けないことがあります
  9. MX_LWIP_Process() が呼ばれているか(NO_SYS)/ TCPIPスレッドが走っているか(RTOS)
  10. RAW APIを別スレッドから呼んでいないか — Common pitfalls違反。tcpip_callback() で包みます
  11. チェックサム設定 — H/Wオフロード有効なのに lwipopts.h でもS/W計算しているとパケット破壊が起きます
  12. 割込み優先度 — FreeRTOS時、ETH割込みが configMAX_SYSCALL_INTERRUPT_PRIORITY より上だとFreeRTOS API呼んで死にます
  13. Wiresharkを当てる — STM32側からARPリクエストが出ていれば送信系は生きています。応答無いだけなら受信側か上位レイヤ

振り返り

結局のところ、STのCubeMXを信じて一発生成に期待するのが間違いで、以下の順でやるのが早そうです。

  1. ボードに合った動くリポジトリ(H7なら stm32-hotspotbetocool-prog)でまずpingが通るまで動かす
  2. その .iocethernetif.c参照解として手元に置く
  3. 自プロジェクトをCubeMXで作り直したら、生成された ethernetif.c / lwipopts.h / リンカスクリプト / MPU設定を diffで参照解と比較
  4. 差分を USER CODE BEGIN/END の中、もしくはミドルウェアをプロジェクト外にコピーして除外する形で潰す
  5. CubeMX再生成のたびに3を回す覚悟をする(毎回どこかが壊れる前提で運用する)

STがCubeMXで一発出力できる日まで、これが現実的な落とし所っぽいです。(stはあんまりややる気なさそうなので一生来ない気がするけど)


参考