资讯动态

STM32裸机移植LWIP:基于LAN8720A与RMII接口的以太网实战

发布时间:2026/10/17 16:41:38 来源:尧图企业网站定制
简介面向使用STM32 HAL库开发以太网功能的嵌入式工程师这份资源给出了一套基于STM32F429IGT6、LAN8720A物理层芯片与LWIP 2.1.2协议栈的无操作系统PING通信完整工程。工程由STM32CubeMX 6.6.1生成HAL库版本为V1.27.1LAN8720A通过RMII接口与STM32的ETH外设交互STM32则用SMI接口读写PHY寄存器适合快速搭建以太网原型或学习LWIP移植。压缩包共1300个文件其中678个C源文件、450个头文件构成主体代码另有78个汇编文件、46个ICF链接脚本、25个文本说明、HAL数学库与Keil/IAR工程配置等总大小31.16MB目录清晰可直接编译运行。目前已有1988人学习下载。通过这套资料可掌握CubeMX下ETH、GPIO与LWIP的配置流程理解无操作系统中LWIP的内存管理与轮询机制并可直接参考其中的PING实现进行二次开发。1. 项目定位让一块 STM32 板子不跑操作系统就通网在嵌入式项目里给 MCU 加网口功能通常意味着先上 RTOS因为协议栈跑在裸机上听起来就像在刀尖上跳舞。但这个工程偏偏选了另一条路STM32 HAL 库 LAN8720A PHY 芯片 LWIP无操作系统目标只有一个——把 PING 跑通再把 TCP/UDP 的地基打好。拿到这份源码时我先确认了它确实是纯裸机工程没有 RTOS 的痕迹整个协议栈全靠主循环轮询加 IRQ 中断配合驱动。这个工程适合两类人一类是现有裸机产品要低成本加网口、又不想为一个小功能引入 RTOS 迁移成本的开发者另一类是刚接触 LWIP、想搞明白无操作系统移植时协议栈时间管理和内存管理怎么运作的初学者。它把 CubeMX 配置、PHY 复位时序、netif 注册、轮询调用这些最容易翻车的环节串成一条完整链路照着跑通后你会对整个 stm32 c语言 工程的网络初始化顺序有一个真正的全局认识。2. 硬件链路与协议栈选型RMII 接口、LAN8720A 与 LWIP 的三方分工一个以太网工程的技术栈可以概括成三层物理层 PHY、MAC 外设、协议栈。这三层之间由两条接口隔开——MAC 和 PHY 之间的 RMII 硬件接口以及 LWIP 和 MAC 驱动之间的 netif 软件接口。把这个分层想清楚后面读代码就不会迷路。工程选的技术栈非常标准HAL 库负责外设抽象LAN8720A 负责物理层LWIP 负责 IP 层以上的协议逻辑三者各管一段互不越界。2.1 LAN8720A 在以太网链路里的位置一块板子要跑以太网至少需要两层芯片MAC 控制器和 PHY 收发器。STM32 内部集成了 MAC 和 DMA 控制器但它不擅长直接驱动网线上的差分信号这部分正是 PHY 芯片的工作。LAN8720A 是一个 10/100M 自适应 PHY支持自动协商、极性纠正等链路层功能价格便宜在嵌入式网口方案里几乎是标配所以工程选它做物理层非常合理。MAC 和 PHY 之间的接口有两种主流选择MII 需要 16 根信号线RMII 只需要 7 根。对引脚有限的 STM32 来说RMII 几乎是唯一选择。LAN8720A 恰好支持 RMIISTM32 的以太网外设也原生支持 RMII 模式两边一拍即合。工程里所有网络信号都按 RMII 方式接入这也是为什么初始化代码里要把 ETH 外设模式配成 RMII而不是 MII。需要注意一个认知点RMII 模式下的数据线只有 TXD[1:0] 和 RXD[1:0]MAC 和 PHY 之间以 4-bit 半字节传输配合 50MHz 参考时钟等效数据率就是 100Mbps。如果参考时钟有问题速率档位会跟着出错。LAN8720A 的工作速率由自动协商决定但参考时钟必须稳定在 50MHz 才能支持 100M这是硬件设计上不能含糊的事。2.2 RMII 信号组与板级连线要点RMII 接口信号按功能分为三组数据、控制、时钟。数据组是 ETH_RMII_TXD0/TXD1 和 ETH_RMII_RXD0/RXD1控制组是 ETH_RMII_TX_EN 和 ETH_RMII_CRS_DV时钟组是 ETH_RMII_REF_CLK。此外还有 MDIO 和 MDC 两根管理线用于读写 PHY 寄存器。合起来不到 10 根引脚这就是 RMII 的性价比所在。这些引脚在 STM32 上都有固定映射以常见的 F4 系列为例PA1 是 REF_CLKPA2 是 MDIOPC1 是 MDCPB11 是 TX_ENPB12/PB13 是 TXD0/TXD1PA4 是 CRS_DVPA5/PA7 是 RXD0/RXD1。工程在 CubeMX 里就是按这组引脚做的约束。换到别的型号前务必先翻对应数据手册的复用功能表别想当然沿用同样的引脚在不同系列上复用的外设可能完全不同。板级连线还有一个高频翻车点LAN8720A 的 TX 差分对和 RX 差分对不能接反否则会出现灯亮但数据不通的诡异现象。另外网口变压器中心抽头的处理方式要严格按 PHY 数据手册来有些要接电源有些要经电容到地。这两个细节在画板时出错后期靠软件救不回来只能改板重做属于最贵的错误类型。2.3 LWIP 裸机移植的三个前置条件LWIP 当初设计时就考虑了无操作系统场景所以裸机移植不是神操作但有三个条件必须先满足否则代码要么编不过要么跑一会儿就死。第一个是内存。LWIP 的 PBUF 池、内存堆、ARP 表、TCP 控制块都要消耗 RAMSTM32 如果只有 64KB 甚至更少跑 TCP 会非常紧张。工程按目标型号的 RAM 量配置了 PBUF_POOL_SIZE 和 MEM_SIZE第 4 章我会贴出具体参数。你拿到工程后第一件事应该是打开 lwipopts.h 看内存宏再对照自己板子的 RAM 大小心里先有个数。第二个是时间基准。裸机环境下没有 tcpip_thread 替你周期性地驱动协议栈必须由主循环反复调用 sys_check_timeouts 来触发 ARP 老化、TCP 重传、DHCP 超时等定时任务。这个函数调用得越频繁协议栈对时间越敏感如果主循环里有个 while 死等外设的代码网络就会跟着罢工这是裸机网络工程最常见的死法。第三个是网卡驱动接口。LWIP 通过 netif 结构体统一抽象网卡需要提供 low_level_init、low_level_output、low_level_input 等底层函数。HAL 库的以太网驱动已经封装好了寄存器操作和 DMA 描述符管理工程里只是在 ethernetif.c 中把它们填到了 LWIP 的接口上。理解这三个前置条件比背配置参数重要得多因为它们决定了你在二次开发时能往哪里加代码、哪里不能埋雷。3. 初始化流程拆解CubeMX 配置、PHY 复位与 netif 注册3.1 CubeMX 里的关键配置项工程用 CubeMX 生成初始化骨架拿到手先看 .ioc 文件那里面记录了全部外设配置。以太网部分有几个选项不能错ETH 外设模式选 RMIIPHY 地址按板子实际接法填PHY 时钟源按电路选外部时钟或 MAC 输出。下面是工程里和以太网强相关的配置参数整理成一张表方便对照配置项工程里的值说明ETH ModeRMII必须与硬件接线一致PHY Address0LAN8720A 默认地址按模块实际配置时钟来源外部 50MHz 或 MCO 输出决定 REF_CLK 引脚方向ETH 全局中断开启接收完成通知主循环LWIP_ICMP1PING 依赖 ICMP 支持LWIP_DHCP1插网线自动获取 IP时钟树方面RMII 要求 50MHz 参考时钟必须稳定。如果板上有 50MHz 晶振直接给 LAN8720AREF_CLK 引脚要配成输入如果用 STM32 的 MCO 引脚输出 50MHz 给 PHYREF_CLK 引脚要配成复用输出。这个方向一旦反了PHY 收不到时钟后面读寄存器全是 0xFFFF整个初始化直接卡死。中断配置也在这步完成。ETH 全局中断要打开优先级建议比 SysTick 高一点但不能太高因为中断服务函数必须保持极短。裸机环境下没有 RTOS 的临界区嵌套问题但要注意中断和主循环共享的变量要加 volatile 修饰工程里的 g_eth_rx_flag 就是典型的例子漏掉 volatile 的话编译器优化可能让你看到一个永远不变的标志位。3.2 PHY 复位与检测第一道门槛PHY 复位是整个以太网初始化里最容易翻车的一步。LAN8720A 支持硬件复位和软件复位两种方式工程把两种都给了先拉低复位引脚完成硬件复位等稳定后再写 BMCR 寄存器做一次软件复位最后读 PHY ID 寄存器确认 MDIO 通信正常。/* 硬件复位 LAN8720A拉低再释放 */ HAL_GPIO_WritePin(ETH_RESET_GPIO_Port, ETH_RESET_Pin, GPIO_PIN_RESET); HAL_Delay(10); /* 拉低至少 10ms确保 PHY 内部完成放电 */ HAL_GPIO_WritePin(ETH_RESET_GPIO_Port, ETH_RESET_Pin, GPIO_PIN_SET); HAL_Delay(100); /* 释放后等待 PHY 上电稳定 */ /* 软件复位写 PHY 基本控制寄存器 bit15 */ uint16_t bmcr 0x8000; HAL_ETH_WritePHYRegister(heth, PHY_BCR, bmcr); HAL_Delay(50); /* 读 PHY ID 寄存器验证 MDIO 链路正常 */ uint32_t id1 0, id2 0; HAL_ETH_ReadPHYRegister(heth, PHY_ID1, id1); HAL_ETH_ReadPHYRegister(heth, PHY_ID2, id2); if (id1 0xFFFF || id2 0xFFFF) { /* PHY 没起来优先检查 REF_CLK 和复位引脚 */ }这段代码的逻辑分三步硬件复位保证电平干净软件复位让 PHY 进入已知状态读 ID 确认 MDIO 通路。参数上硬件复位拉低时间我习惯给 10ms比手册最小值大一个量级因为电源纹波大的板子上 1ms 复位会偶发失败释放后的 100ms 是给 PHY 内部 PLL 稳定用的如果 PHY 用的是外部 50MHz 晶振这个延时可以缩到 50ms。这里有个血泪经验MDIO 读不到数据时先别怀疑代码用示波器量一下 REF_CLK 引脚有没有 50MHz 时钟。只要时钟没起来PHY 对 MDIO 不会有任何响应。这是排查以太网初始化问题的第一板斧比反复改代码效率高得多。3.3 netif 注册与 IP 地址绑定PHY 通了之后就该把 LWIP 和网卡绑定了。初始化顺序是 lwip_init 先起协议栈再 netif_add 注册网卡最后 netif_set_up 把网卡置为可用。这个顺序不能颠倒否则 netif 状态会乱。struct netif g_netif; ip4_addr_t ipaddr, netmask, gw; IP4_ADDR(ipaddr, 192, 168, 1, 50); /* 板子 IP */ IP4_ADDR(netmask, 255, 255, 255, 0); /* 子网掩码 */ IP4_ADDR(gw, 192, 168, 1, 1); /* 网关 */ lwip_init(); netif_add(g_netif, ipaddr, netmask, gw, NULL, ethernetif_init, ethernet_input); netif_set_default(g_netif); netif_set_up(g_netif);netif_add 的参数从左到右netif 结构体指针建议全局变量、IP 地址、子网掩码、网关、state 指针这里传 NULL有自定义数据时用它挂载、初始化函数 ethernetif_init、输入函数 ethernet_input。ethernetif_init 负责填写 netif 的 MAC 地址、MTU、链路类型并调用 HAL 的 MAC 初始化ethernet_input 是协议栈接收包时的入口。MAC 地址是这个环节容易忽略的东西。工程里用宏定义了六个字节如果你的产品要批量出货每台设备的 MAC 必须不同否则同一二层网络里两台设备会互相抢包表现就是时通时断。批量生产时的常见做法是在 Flash 里保存出厂 MAC启动时读出来覆盖默认值。工程里留好了修改位置二次开发时直接替换 netif.hwaddr 的填充代码即可。4. 裸机协议栈运转轮询加中断的完整数据通路4.1 主循环里的两件事喂时间、收数据无操作系统环境下LWIP 没有独立线程所有协议栈任务都要挤在主循环里完成。这个工程的主循环结构简化后如下while (1) { sys_check_timeouts(); /* 处理 ARP 老化、TCP 重传 */ ethernetif_input(g_netif); /* 从 DMA 取数据送协议栈 */ user_app_poll(); /* 业务代码 */ }sys_check_timeouts 是 LWIP 的定时器驱动内部遍历定时事件链表发现到期的就执行回调。这个函数必须被高频调用建议间隔不超过 10ms。如果你的业务里有长时间阻塞的操作比如 Flash 擦写或加密计算要么在阻塞期间穿插调用它要么把大任务拆成状态机分片执行否则 TCP 重传超时、ARP 请求发不出去都会一起来找你。提示sys_check_timeouts 是裸机 LWIP 的命脉阻塞超过 10ms 不调用TCP 重传和 ARP 老化就会出问题。ethernetif_input 是接收路径的入口。它检查 DMA 接收描述符把收到的帧封装成 PBUF调用 netif_add 时传入的 ethernet_input 递交给协议栈。这里有个常见误用有人把 ethernetif_input 直接放到中断里调用裸机环境下风险很大因为里面涉及 PBUF 分配中断上下文里分配内存容易和主循环的内存管理打架。工程的做法是中断只置标志位主循环看到标志位再调用 ethernetif_input。4.2 接收路径上的中断与 DMA 描述符STM32 以太网外设自带 DMA收到帧后 DMA 会把数据写入内存中的描述符缓冲区然后触发 ETH 中断。裸机工程里中断服务函数被约束到最小void ETH_IRQHandler(void) { if (HAL_ETH_GetReceivedFrame_IT(heth) HAL_OK) { g_eth_rx_flag 1; /* 通知主循环取数据 */ } HAL_ETH_IRQHandler(heth); }HAL_ETH_GetReceivedFrame_IT 检查是否有完整接收帧有就置位全局标志位。真正的数据搬运 HAL_ETH_ReadData 留到主循环里做。这样设计有两个好处中断服务函数执行时间极短不影响系统里其他中断LWIP 内存操作全部发生在主循环上下文不存在嵌套问题。DMA 描述符是 STM32 以太网驱动里最像黑匣子的部分不过 HAL 库已经封装好了。工程启动时分配了 4 个 TX 描述符和 4 个 RX 描述符每个描述符关联一块缓冲区缓冲区大小由 ETH_RX_BUF_SIZE 定义。如果你想跑高负载业务把这个数量往上调但要注意 RAM 占用这是提升并发吞吐的前提。验证网卡稳定性有个土办法在电脑上执行 ping -l 1400 -n 100 连发大包丢包率超过 1% 就说明资源配少了。这个问题我在第 5 章会专门展开。4.3 PING 请求从进入到返回的完整链路PING 用的协议是 ICMP Echo。电脑 ping 板子时先发 ARP 请求解析板子 IP 对应的 MAC板子收到后由 etharp 模块自动回复 ARP 应答。电脑拿到 MAC 后发出 ICMP Echo Request板子的 icmp_input 收到请求构造 Echo Reply走 etharp_output 到 low_level_output 发送出去。如果把这条链路映射到代码调用上就是 ethernetif_input → ip_input → icmp_input → icmp_output → etharp_output → low_level_output。每一步都是通过 netif 结构体里的函数指针完成的这正是 LWIP 架构的核心协议层之间靠函数指针解耦你要改底层驱动上层协议一行不用动。PING 不通时第一个要确认的是 ARP 有没有学习成功。在电脑上执行 arp -a能看到板子 IP 的条目且状态是动态的说明二层通了问题在 ICMP 处理逻辑如果 ARP 表里根本没有板子问题在底层收发。这个排查思路比抓包快得多是我调试以太网时最常用的第一步往往能直接砍掉一半的排查路径。5. 避坑指南PING 不通时的五个高频排查点5.1 读 PHY 寄存器返回 0xFFFF现象调用 HAL_ETH_ReadPHYRegister 读 PHY ID返回 0xFFFF初始化走到一半就失败。原因MDIO 总线上没有任何设备应答。最常见的两个根因一是 PHY 的 REF_CLK 没有 50MHz 时钟PHY 处于未初始化状态二是 PHY 复位时序不彻底上电后 PHY 内部还在忙MDIO 访问被忽略。解决先用示波器量 REF_CLK 引脚确认 50MHz 方波存在再把硬件复位后的 HAL_Delay 从 100ms 加到 200ms排除上电时序问题。最后检查 MDIO 引脚的外接上拉电阻MDIO 是开漏信号必须有上拉才能通信这个在原理图上最容易被漏掉。5.2 网口灯亮但 PING 超时现象插上网线后 LINK LED 正常点亮物理链路协商成功但电脑 ping 板子 IP 全部超时。原因链路层正常不代表数据通路正常。常见原因有几个RMII 的 CRS_DV 信号接错引脚TX/RX 差分对接反MAC 地址配置成全零或全 FF导致 ARP 回复被对端直接丢弃。解决先执行 arp -a 确认 ARP 表里有没有板子条目没有就逐根检查 RMII 信号线特别是 CRS_DV 和 TX_EN 这两个控制信号时序关系错一点就会导致收发失败。MAC 地址要在代码里打日志确认实际被填入的值别靠肉眼猜。5.3 第一次 PING 通连续 PING 丢包现象单发一个包通连续发 100 个大包丢到怀疑人生丢包率甚至超过 30%。原因典型的协议栈资源不足。连续 PING 时接收队列堆积PBUF 池被占满后来的包在 low_level_input 分配 PBUF 时失败直接被丢。内存配置里 PBUF_POOL_SIZE 或 MEM_SIZE 太小或者 RX 描述符数量只有 4 个缓冲深度不够。解决调大 lwipopts.h 里的 MEM_SIZE 和 PBUF_POOL_SIZE把 RX 描述符从 4 个加到 68 个。改完重新编译看 map 文件确认 RAM 没超再烧录测试。注意描述符和 PBUF 池在以太网初始化时一次性分配改了宏必须整体重编只编译单个文件不生效。5.4 中断标志置位但主循环一直收不到数据现象在 ETH_IRQHandler 里打断点能看到 g_eth_rx_flag 被置 1但主循环里的 ethernetif_input 迟迟不处理数据或者根本不进这个函数。原因主循环被某段业务代码阻塞住了比如一个 while 等待外设标志、一个长延时、或者一段串口同步等待。裸机协议栈没有独立线程主循环一卡网络就跟着停这是裸机网络方案最典型的死法。解决检查 while(1) 里所有可能长时间阻塞的调用把超过 10ms 的阻塞逻辑改造成状态机或定时器驱动。如果业务代码实在拆不开至少要在阻塞循环内部周期性地调用 sys_check_timeouts 和 ethernetif_input保证协议栈还活着。5.5 复位后必须手动插拔网线才能 PING 通现象板子上电后 PING 不通手动把网线拔掉再插上网络立即恢复正常之后一直稳定。原因PHY 自动协商没有正确重新启动或者 PHY 配置引脚在复位释放瞬间被外部电路拉到了不确定状态导致 PHY 启动后协商模式错误。这类问题最玄学第一次遇到时我还以为是电源干扰后来定位到是复位时序。解决在 PHY 复位代码之后追加一次 restart autonegotiation读 BCR 寄存器把 bit9 置 1 再写回然后轮询 BSR 寄存器等待协商完成。同时检查配置引脚的外接上下拉电阻是否牢固用万用表量一下复位释放后的实际电平确认配置没被外部电路带偏。6. 二次开发改造从 PING 通到 TCP Echo Server 的落地思路6.1 拿到工程后的前三件事这份源码拿到手后不要急着烧录先做三件事核对PHY 地址、MAC 地址、时钟来源。PHY 地址这个坑最隐蔽很多 LAN8720A 模块把地址配置引脚拉到了 1如果你照抄工程里的 0读 PHY 寄存器全是错的PING 自然不通。遇到这个情况打开 HAL 库里的 PHY 地址宏改成 1 再编译下载通常就好了。第二件事是确认 REF_CLK 的来源。工程支持外部晶振和 MCO 输出两种方式但你的板子只能有一种接法配错方向 PHY 永远起不来。第三件事是改 MAC 地址至少要在尾字节上做区分避免和工程默认值在网络环境里冲突。这三件事做完了再编译烧录PING 通之后才开始真正的业务开发。6.2 用 raw API 加一个最小 TCP Echo Server在无操作系统环境下加 TCP 功能不需要新建线程用 LWIP 的 raw API 就够了。思路是创建一个 TCP 控制块绑定端口后监听连接建立后注册收数据回调在回调里把收到的数据原样发回。下面这段代码可以直接放进工程里在 main 里调用 echo_server_init 即可static struct tcp_pcb *echo_pcb; static err_t echo_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p ! NULL) { tcp_write(pcb, p-payload, p-len, 1); /* 原样回发 */ tcp_output(pcb); /* 触发发送 */ pbuf_free(p); /* 释放缓冲 */ } return ERR_OK; } static err_t echo_accept(void *arg, struct tcp_pcb *pcb, err_t err) { tcp_recv(pcb, echo_recv); return ERR_OK; } void echo_server_init(void) { echo_pcb tcp_new(); tcp_bind(echo_pcb, IP_ADDR_ANY, 8080); echo_pcb tcp_listen(echo_pcb); tcp_accept(echo_pcb, echo_accept); }echo_accept 在客户端连接建立时被调用echo_recv 在收到数据时被调用。tcp_write 把数据放入发送缓冲第三个参数 1 表示允许协议栈拷贝数据这样回调退出后可以安全释放 ptcp_output 立刻触发一次发送尝试pbuf_free 在数据不再需要时释放接收缓冲。这段代码全部是 LWIP 裸机 API不依赖任何操作系统服务在这个工程里原样编译就能跑。验证方式很简单电脑上用一个串口网络调试助手连板子的 IP 加 8080 端口发什么回什么TCP 收发链路就算完全打通了。从 PING 到 TCP整个改动的侵入面只有新增文件和一个初始化调用协议栈底层一行没动这就是分层设计带来的改造自由度。6.3 再往前一步的改造方向如果要做 TCP 客户端主动上报建议用 raw API 再加一个连接管理结构体把连接状态、重连超时、发送队列挂在里面回调里只做收数据和置标志位别在协议栈回调上下文里做耗时业务。如果要做 UDP 定时上报直接用 udp_new 加 udp_bind配合一个软件定时器做周期触发比 TCP 简单一个量级没有重传和连接管理负担。如果产品要跑更复杂的协议比如 HTTP Server 或 MQTT 客户端我一般会先把裸机协议栈的运转调稳再评估是否需要引入 RTOS。裸机能跑到 PING 不掉包、TCP 连续收发大字节不出错就说明这个基础已经足够扎实。从那以后我每拿到一个网络工程都会先确认它能在裸机状态下稳定 PING 通再谈业务代码的移植这个习惯让我少加了很多没必要的班希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑