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_outputでSCB_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側でやること
- クロックはHSE→PLLでSYSCLK 400MHz(H743の場合)。デフォルトのまま生成しません
- ETHを有効化。RMIIモード。PHYアドレスをボードに合わせます(Nucleo-H743のLAN8742はアドレス0)
- FreeRTOSをCMSIS_V1で有効化、
defaultTaskスタックを 512 ワード以上に - LwIP middleware:
- 固定IP(DHCP不要なら)。例:
192.168.1.10 - Platform settings → LAN8742を選択(保存して開き直すと外れる罠あり)
- Key Options →
MEM_SIZE = 16360、LWIP_RAM_HEAP_POINTERをD2 SRAM内に LWIP_NETIF_LINK_CALLBACKを有効- Checksum →
CHECKSUM_BY_HARDWAREを有効
- 固定IP(DHCP不要なら)。例:
- MPU:
- Region 0: 全4GBをStrongly Orderedで被せる(STM32H7共通テンプレ)
- Region 1: D2 SRAMのlwIP領域を Non-cacheable Normal Memory にする
- サイズ単位を間違えると即HardFault(
256KBのつもりが256B、結構ありますねぇ!)
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_threadのHAL_ETH_StopをHAL_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 / Callback | NO_SYS=1 or TCPIPスレッドのみ | RTOSなし or 最小構成 | mainloopで sys_check_timeouts() と ethernetif_input() 相当を回す |
| Netconn | RTOS必須 | シーケンシャルに書きたい時 | 別スレッドから呼んでOK。STサンプルの主流 |
| Socket (BSD) | RTOS必須 | POSIX移植性が欲しい時 | 最重量。lwipopts.h で LWIP_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-Examples | Nucleo-H743ZI / H723 / H750-Disco | LwIP + FreeRTOS | ST中の人が出しているので事実上の公式リファレンス。Git tagでCube/HALバージョン対応が追えます |
betocool-prog/h7_lwip_freertos | Nucleo-H743ZI | LwIP + FreeRTOS | 上記が動かなかった人向けに手順を再構築 |
android-man/STM32H7_Nucleo-H743ZI_Ethernet_LwIP | Nucleo-H743ZI | NO_SYS | H7でRTOS無し構成のミニ例 |
windsorschmidt/stm32h7-nucleo-h743zi-ethernet-lwip | Nucleo-H743ZI | NO_SYS + Makefile | CubeIDEに縛られたくない人向け |
Dmivaka/STM32H7-ETH-LWIP | H723系 | LwIPテンプレ | H723特有のメモリ制約に対応 |
eziya/STM32F4_HAL_LWIP_LAB | F4 | Raw / Netconn / MQTT / mbedTLS / AWS IoT | F4で実例が網羅されている。古いが追いやすい |
mirzafahad/nucleo_stm32f429zi_lwip_examples | Nucleo-F429ZI | Raw API | TCP/UDP echo clientの最小例 |
STMicroelectronics/STM32CubeH7 Projects/STM32H743I-EVAL/Applications/LwIP/ | Evalボード | RTOS + Netconn | ST公式。HTTP/TFTP/iperfがひと通り揃う |
STMicroelectronics/stm32-mw-lwip | 全シリーズ | lwIP本体(ST フォーク) | Middlewareを直接差し替えたい時 |
詰まったときのチェックリスト
pingが通らない / TCPが刺さる時に上から順に潰します。
- PHYのリンクは上がっているか —
HAL_ETH_GetLinkState()で確認。LEDが光っているか - PHYアドレスは合っているか — ボード固有。LAN8742 (Nucleo)は普通
0 - MACアドレスがユニークか — 同一セグメントに同じMACがいるとARPが壊れます
- MII/RMII切替のSYSCFG — Legacyドライバ系でSYSCFGクロックを開けてから切り替えないと動きません
- DMAバッファの配置 — D1/D2 SRAM内か。リンカマップで
RxDecripSection/TxDecripSection/Rx_PoolSectionのアドレスを確認 - MPU設定 — 該当領域がNon-cacheableか。サイズ単位(B/KB/MB)に注意
- DCache — 一度
SCB_DisableDCache()で切ってみます。これで動けばキャッシュコヒーレンシ問題確定 PBUF_RAMでallocしているか —PBUF_POOLだとD2に置けないことがありますMX_LWIP_Process()が呼ばれているか(NO_SYS)/ TCPIPスレッドが走っているか(RTOS)- RAW APIを別スレッドから呼んでいないか — Common pitfalls違反。
tcpip_callback()で包みます - チェックサム設定 — H/Wオフロード有効なのに
lwipopts.hでもS/W計算しているとパケット破壊が起きます - 割込み優先度 — FreeRTOS時、ETH割込みが
configMAX_SYSCALL_INTERRUPT_PRIORITYより上だとFreeRTOS API呼んで死にます - Wiresharkを当てる — STM32側からARPリクエストが出ていれば送信系は生きています。応答無いだけなら受信側か上位レイヤ
振り返り
結局のところ、STのCubeMXを信じて一発生成に期待するのが間違いで、以下の順でやるのが早そうです。
- ボードに合った動くリポジトリ(H7なら
stm32-hotspotかbetocool-prog)でまずpingが通るまで動かす - その
.iocとethernetif.cを参照解として手元に置く - 自プロジェクトをCubeMXで作り直したら、生成された
ethernetif.c/lwipopts.h/ リンカスクリプト / MPU設定を diffで参照解と比較 - 差分を
USER CODE BEGIN/ENDの中、もしくはミドルウェアをプロジェクト外にコピーして除外する形で潰す - CubeMX再生成のたびに3を回す覚悟をする(毎回どこかが壊れる前提で運用する)
STがCubeMXで一発出力できる日まで、これが現実的な落とし所っぽいです。(stはあんまりややる気なさそうなので一生来ない気がするけど)
参考
- How to make Ethernet and lwIP working on STM32 (Pavel A.) — 既知バグの最も網羅的な一次資料
- How to create a project for STM32H7 with Ethernet and LwIP stack working (Adam Berlinger)
- stm32-hotspot/STM32H7-LwIP-Examples
- STMicroelectronics/stm32-mw-lwip
- lwIP Common pitfalls
- UM1713 — Developing applications on STM32Cube with LwIP TCP/IP stack