HC32F4A0+LAN8720+FreeRTOS+LwIP:嵌入式以太网PING测试实战 简介面向嵌入式开发者的HC32F4A0网络通信验证例程将华大半导体的高性能MCU与FreeRTOS、LWIP协议栈及LAN8720 PHY芯片相结合完成基于Ping命令的以太网连通性测试。资源包共1606个文件压缩后约98.81MB主要包含H/C源码、预编译库文件.a/.lib、链接脚本.icf/.ld、工程文件.uvprojx/.ewp以及文档说明等源码与工程结构完整便于直接导入开发环境查看。已有306人浏览学习。例程重点演示系统时钟与外设初始化、FreeRTOS任务/队列/信号量创建、LWIP网络接口与IP地址配置、LAN8720寄存器及MII接口设置并基于ICMP回显请求与应答实现Ping测试与丢包率统计覆盖从底层驱动到协议栈集成的关键环节可帮助开发者理解实时系统与协议栈的协同机制。适合具备一定嵌入式开发经验、希望快速在HC32F4A0上搭建以太网通信验证平台的工程师参考。 接到这个hc32f4a0-ddl-freertos-lwip(HC32F4A0 LAN8720 LWIP PING test).zip压缩包时我第一反应是这活儿太典型了。国产MCU华大HC32F4A0跑FreeRTOS LwIP外接一颗LAN8720 PHY芯片目标就是让板子能ping通网关、ping通电脑。这几乎是嵌入式以太网开发的必修课也是很多工程师从裸机裸奔转向“RTOS协议栈物理层”整合的第一个硬骨头。我手里正好有HC32F4A0的开发板也踩过这个组合的不少坑。这篇博文就把整个项目的拆解思路、移植过程、PING测试实操和问题排查一次性讲透希望能帮你少走弯路。1. 项目全景为什么用这套组合做以太网通信先把这个项目要解决的核心问题说清楚。HC32F4A0是华大半导体的Cortex-M4F内核高性能MCU主频能到240MHz片上Flash有1MBSRAM有512KB关键是内置了10/100M以太网MAC控制器支持MII和RMII两种接口。LAN8720则是一颗超高性价比的RMII接口PHY芯片功耗低、外围电路简单在STM32生态里被用烂了。这套组合的意义就是用一颗国产M4加一颗廉价PHY快速实现带以太网能力的小型物联网网关、工业数据采集器或者远程升级设备。为什么还要加上FreeRTOS和LwIP这层软件因为在实际产品里网络协议栈通常不是孤立的它要和串口、传感器采集、控制逻辑打架。裸机写RAW API的LwIP也能跑但一旦业务复杂度上来串口要收发、GPIO要中断、网口还要满速转发裸机状态机写起来就乱了。FreeRTOS提供任务调度、信号量、队列这些基础件LwIP在RTOS上跑多线程模型才能发挥最大性能。这套方案的核心架构分四层硬件层HC32F4A0 LAN8720 带网络变压器的RJ45座驱动层华大DDL库的ETH外设驱动 自写的PHY驱动系统层FreeRTOS提供任务调度、时基、内存管理协议层LwIP协议栈运行在独立任务中可选静态IP或DHCP对于刚入门的朋友可能觉得包名里的DDL有点陌生。DDL是Device Driver Library也就是华大官方提供的固件库它把寄存器操作封装成了外设驱动函数。LwIP的移植工作说白了就是写一个网卡底层接口让协议栈能通过ETH外设收发数据。PING测试则是验证整个链路是否打通的最快方式——不用写TCP、UDP应用能ping通网关就说明MAC、PHY、协议栈、RAM配置全部正常。2. 硬件层面LAN8720电路设计和RMII接口要点这个项目看起来是纯软件问题但我建议先把硬件搞对否则软件再怎么调也白搭。2.1 RMII接口的信号与时序RMIIReduced Media Independent Interface是一种精简的以太网接口标准相比MII接口它把数据线从16根砍到了7根左右对MCU引脚占用非常友好。HC32F4A0的ETH外设和LAN8720之间需要连接的信号如下TXD0、TXD1两比特发送数据TX_EN发送使能RXD0、RXD1两比特接收数据CRS_DV载波侦听/数据有效REF_CLK50MHz参考时钟MDIO、MDC管理接口用来读写PHY寄存器这里最关键的往往是时钟。RMII规范要求收发双方共享一个50MHz的REF_CLK。LAN8720有两种时钟方案一种是从外部晶振输入25MHz然后PHY内部通过PLL倍频产生50MHz并从CLKOUT脚输出给MAC另一种是直接外部给LAN8720一个50MHz时钟源。HC32F4A0的ETH外设设计上支持从外部PHY接收REF_CLK但具体要看你的开发板引脚有没有接对。注意MDIO/MDC是慢速管理接口速度一般不超过2.5MHz它对时序不敏感反而是REF_CLK一旦不对网口收发直接失效。2.2 PHY地址、复位和中断引脚LAN8720的PHY地址由PHYAD[2:0]引脚决定但在实际模块上通常只开放了PHYAD0也就是RXER引脚默认情况下模块会把它下拉到地对应PHY地址0x00。如果硬件上把RXER接上拉电阻地址就是0x01。绝大多数成品模块默认地址是0x00你的驱动代码初始化PHY时MDIO总线地址要填0x00。复位引脚是必须认真对待的一个点。LAN8720手册要求复位信号低电平持续时间至少25毫秒很多人的驱动代码里GPIO翻转太快导致PHY上电没完全复位就开始配置读出来的寄存器全是0xFF。正确做法是拉低复位脚延时50ms以上再拉高然后等PHY内部稳定再延时至少100ms最后再通过MDIO读取PHY ID寄存器确认。另外LAN8720还有一个nINT中断输出引脚可以用来连接MCU的EXTI外部中断实现网线插拔检测。这个不是必需的但强烈建议接上因为它能让你的代码及时感知到网线断开和恢复而不是靠轮询PHY状态寄存器。2.3 网络变压器和RJ45座常规做法是选用内部集成网络变压器的RJ45插座例如HR911105A这类。如果LAN8720和网络变压器之间加上共模电感和电阻匹配网络对EMI抗干扰有好处。量产产品建议照抄TI或微芯官方评估板的网络隔离电路。3. 软件框架HC32F4A0 DDL驱动移植全流程硬件准备好之后开始折腾软件。这个项目的软件主要分四块ETH外设初始化、PHY驱动、FreeRTOS集成、LwIP网卡接口。3.1 工程目录规划我习惯把工程目录按模块分好DSL库文件、操作系统源码、协议栈源码、应用代码分开放这样以后维护和升级都方便。典型结构如下project/ ├── source/ │ ├── startup/ // 启动文件和链接脚本 │ ├── ddl/ // HC32F4A0官方DDL库 │ ├── freertos/ // FreeRTOS内核源码 │ ├── lwip/ // LwIP协议栈源码 │ ├── app/ │ │ ├── eth_phy.c // PHY驱动 │ │ ├── eth_netif.c // LwIP底层网卡接口 │ │ └── main.c // 主函数、任务创建 │ └── ...3.2 ETH外设初始化HC32F4A0的ETH外设初始化核心是配置GPIO复用、MAC模式、DMA、中断。DDL库的ETH例程里一般有现成的初始化函数但你需要根据自己的板子修改引脚复用表。void ETH_GPIO_Config(void) { /* 使能GPIOA、GPIOB、GPIOC、GPIOE时钟 */ PWC_Fcg0PeriphClockCmd(PWC_FCG0_PERIPH_GPIOA | PWC_FCG0_PERIPH_GPIOB | PWC_FCG0_PERIPH_GPIOC | PWC_FCG0_PERIPH_GPIOE, ENABLE); /* RMII: TXD0/TXD1/TX_EN/RXD0/RXD1/CRS_DV/REF_CLK/MDC/MDIO */ /* 具体引脚根据开发板原理图设置 */ stc_gpio_cfg_t stcGpioCfg; stcGpioCfg.enDir GPIO_DIR_OUT; stcGpioCfg.enOD GPIO_OD_DISABLE; stcGpioCfg.enPu GPIO_PU_DISABLE; stcGpioCfg.enPd GPIO_PD_DISABLE; stcGpioCfg.enST GPIO_ST_DISABLE; stcGpioCfg.enCTRL GPIO_CTRL_PF1; /* 复用功能1 */ GPIO_SetFunc(PORT_A, PIN_01, GPIO_CTRL_PF1); /* 具体引脚看原理图 */ }MAC初始化还要注意ETH_MAC_MODE需要设置全双工、100Mbps或者设置为自协商模式由PHY的结果来决定。DMA的发送描述符和接收描述符要在RAM中分配HC32F4A0的DMA要求描述符地址按32字节对齐。3.3 自写PHY驱动PING测试时90%的问题出在PHY驱动没写对。PHY驱动本身不难就是通过MDIO总线读写寄存器的过程。初始化流程如下void LAN8720_Init(void) { /* 1. 硬复位PHY */ GPIO_ResetBits(PORT_X, PIN_nRST); /* 拉低复位引脚 */ delay_ms(50); /* 保持50ms */ GPIO_SetBits(PORT_X, PIN_nRST); /* 释放复位 */ delay_ms(100); /* 等待PHY稳定 */ /* 2. 读取PHY ID确认通信正常 */ uint16_t id1 MDIO_Read(0x00, 2); /* 地址0x00寄存器2 */ uint16_t id2 MDIO_Read(0x00, 3); /* 寄存器3 */ if (id1 0x0007 id2 0x0200) { /* LAN8720 ID: 0x0007 0x0200 */ } /* 3. PHY软复位 */ MDIO_Write(0x00, 0x00, 0x8000); /* BMCR寄存器bit151软复位 */ delay_ms(20); /* 4. 配置自协商模式 */ MDIO_Write(0x00, 0x00, 0x1200); /* 100M全双工开启自协商 */ }这里有个关键点DP83848、KSZ8081、LAN8720这些PHY芯片的寄存器地址是基本兼容的因为都遵循IEEE 802.3标准。但PHY ID、扩展寄存器会有差异。写完代码后先把寄存器读出来的值打印出来确认访问没问题是关键。3.4 FreeRTOS与LwIP的整合HC32F4A0跑FreeRTOS移植本身不难做三次上下文切换就行了。需要注意的其实是内存规划FreeRTOS的堆用heap_4.c也就是支持碎片合并的分配器一般给16KB~32KBLwIP的PBUF池和网络接口缓冲区HC32F4A0内存充足建议给足尤其网口在跑TCP大数据时内存紧张是最容易卡死的元凶LwIP在RTOS环境下有三种运行模式这里强烈推荐多线程模式也就是让LwIP的tcpip_thread单独运行业务代码通过API函数收发数据。配置项在lwipopts.h里几个关键宏#define NO_SYS 0 /* 使用操作系统 */ #define LWIP_DHCP 1 /* 使能DHCP */ #define LWIP_NETCONN 1 /* 使能Netconn API */ #define LWIP_SOCKET 0 /* 不用Socket API节省内存 */ #define MEM_SIZE (32 * 1024) /* 内存池大小 */ #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) /* 接收窗口 */ #define PBUF_POOL_SIZE 16注意TCP_MSS不要改得太大LwIP内部要分配和MSS匹配的发送缓冲区。HC32F4A0虽然有512KB RAM但实际项目中你还得给业务逻辑留内存。通常设置TCP_MSS为1460字节对应标准以太网帧1500字节减去IP和TCP头部这个值能保证和绝大多数网络设备互通。4. 网卡底层接口LwIP移植最关键的部分LwIP移植的本质是向协议栈提供一个Link Layer接口也就是网卡的发送和接收函数。在代码里体现为三个核心函数。4.1 low_level_output从协议栈到硬件static err_t low_level_output(struct netif *netif, struct pbuf *p) { /* 把PBUF链表中的数据拆分成可以塞进DMA描述符的片段 */ /* 或者拼接到一个连续缓冲区简单但低速 */ uint32_t length 0; struct pbuf *q; uint8_t *buf tx_buf; for (q p; q ! NULL; q q-next) { memcpy(buf length, q-payload, q-len); length q-len; } /* 填充发送描述符启动DMA发送 */ ETH_DMA_SetTxDesc(RMII_BUF, TxDesc, length); return ERR_OK; }这版代码走了“先拼接再发送”的笨路径用一块静态缓冲区承载整个以太网帧。这样做的好处是逻辑简单、稳定可靠MTU是1500字节加上以太网头也就1514字节用一块1.6KB的静态缓冲区完全够用不需要大型软件工程也不需要理解LwIP PBUF链表结构。4.2 low_level_input从硬件到协议栈接收方向关键在于及时性。HC32F4A0的ETH DMA收到完整帧后会置一个中断标志。我们要在中断服务例程里尽量少做事情更推荐的是用“信号量通知任务处理”的套路void ETH_IRQHandler(void) { /* 判断接收DMA中断标志 */ if (ETH_DMA_GetITStatus(ETH_DMA_IER_R)) { /* 唤醒tcpip_thread对应的接收处理函数 */ sys_signal_netif_locked(); } ETH_DMA_ClearITPendingBit(ETH_DMA_IER_R); }在eth_netif.c的轮询代码里我们不断尝试从DMA接收描述符链表中取出已经完成的帧组装成pbuf结构调用netif-input()上报给LwIP协议栈。这中间有个关键点就是接收缓冲区必须由LwIP分配的pbuf提供也就是在初始化时预先给接收描述符绑定pbuf的payload地址这样DMA直接把数据从网卡搬到RAM后协议栈零拷贝就能读取数据。4.3 网卡状态检查与IP地址配置PHY链接状态的检查是PING测试能否成功的第一个关卡。建议在网卡初始化时启动一个周期性地链接状态检查任务比如每两秒读取PHY寄存器1基本状态寄存器的bit1自动协商完成和bit0链接建立。当链接状态从DOWN变UP时调用netif_set_link_up()反向则调用netif_set_link_down()。静态IP配置最简单的方式是直接固定IP4_ADDR(ipaddr, 192, 168, 1, 200); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); netif_add(netif, ipaddr, netmask, gw, NULL, eth_netif_init, ethernet_input); netif_set_default(netif); netif_set_up(netif);提示开发调试阶段建议先用静态IP。DHCP跑起来要等DHCP Offer/ACK的完整交互如果网络环境里没有DHCP Server链路通了也没IP地址ping测试直接失败排查起来头大。把静态IP调通之后再切换DHCP。5. 实操记录从烧录到PING通的完整过程5.1 测试环境准备我实测时用了一台HC32F4A0开发板、一个LAN8720模块、一根网线、一台家用路由器当交换机用网关192.168.1.1。电脑网口配了静态IP 192.168.1.100子网掩码255.255.255.0网关192.168.1.1。电脑和开发板都插在同一台路由器下。如果不用路由器直接用网线把开发板和电脑的网卡连起来也是可以的但双方都必须提前设置好同网段的静态IP交叉线和直连线现在的网卡一般都能自适应不用纠结线序。5.2 启动日志和PHY自检为了方便调试我在PHY驱动和LwIP初始化里加了串口打印。启动后串口输出如下HC32F4A0 Ethernet Test PHY ID: 0x0007 0x0200 (LAN8720) PHY Auto-Negotiation done, 100M FULL-DUPLEX Link UP, speed: 100 Mbps LwIP init done, IP: 192.168.1.200PHY ID读出来是0x0007 0x0200证明MDIO通信正常。链路状态寄存器显示自协商完成、100M全双工说明RMII引脚配置和PHY时钟没问题。到了这里LwIP初始化还要往下执行分配IP、启动网卡等步骤。如果卡在PHY ID读取失败优先检查LAN8720复位电路和MDIO引脚的接线如果自协商一直不完成大概率是REF_CLK没给对逻辑分析仪量一下PHY的CLKOUT脚有没有50MHz输出。5.3 电脑端PING命令测试开发板IP是192.168.1.200电脑上是192.168.1.100。我在Windows的命令行里执行ping 192.168.1.200 -t输出的结果如下正在 Ping 192.168.1.200 具有 32 字节的数据: 来自 192.168.1.200 的回复: 字节32 时间1ms TTL255 来自 192.168.1.200 的回复: 字节32 时间1ms TTL255 来自 192.168.1.200 的回复: 字节32 时间1ms TTL255 丢失 0 (0% 丢失)第一次PING能通时间在1ms以内没有丢包说明从电脑发出ARP请求、开发板收到并应答、再到回包这一条软件链路是完全打通的。TTL255说明IP数据报没有经过Linux/Windows主机转发是开发板LwIP直接返回的。这里解释一下PING的原理PING走的是ICMP协议。电脑先发ICMP Echo Request开发板的LwIP收到后自动回ICMP Echo Reply数据报内容原样返回。这个过程不涉及应用层业务所以通常作为以太网链路是否能工作的“最基础体检”。5.4 性能验证与优化方向PING通之后我用ping 192.168.1.200 -n 100连续发100个包确认长时间稳定性。结果还是0丢包平均时间0.5ms左右。后来用iperf之类的工具测TCP吞吐在限定了LwIP内存参数后能跑到30Mbps左右对一个小型联网设备来说够用了。如果你需要更低的PING延时通常要考虑两个方面一个是中断响应和任务切换的实时性检查其他高优先级任务是否长时间占用CPU另一个是DMA描述符数量和PBUF池大小中断来处理的速度跟不上的时候加大以下两个参数ETH_RX_DESC_CNT从4改成16PBUF_POOL_SIZE从16改成32但代价是内存占用上升每个PBUF池元素就按TCP_MSS1460字节算16个就多占23KB多在内存紧张的MCU上要掂量着来。6. 踩坑记录PING不通的五个经典原因拿着这套代码跑了几天踩了不少坑也帮朋友排过几次问题。PING不通的类型大概可以归成五类每一类都有很典型的特征和排查逻辑。6.1 PHY就是读不到ID症状初始化代码里打印PHY ID总是0xFFFF。排查思路先确认MDIO引脚的GPIO复用是不是对的然后量PHY的供电电压LAN8720需要有3.3V和1.2V两路部分模块已内置LDO再用示波器看复位引脚有没有拉到过低电平至少几十毫秒。最常见的坑是PHY地址没对模块地址是1代码却用了0。可以让代码循环扫描地址0和1看看哪个能读出有效ID。6.2 链路能建立但PING不通症状网线插上后PHY状态显示Link UP但PING的时候开发板“收不到”数据从不会回应。排查思路这种情况大概率是DMA接收路径断了。先用逻辑分析仪或示波器看RMII接口的CRS_DV、RXD0、RXD1有没有波形。如果有波形但LwIP收不到数据检查接收描述符的初始化。尤其注意接收描述符在初始化时是否正确地绑定了LwIP为它分配的接收buffer地址以及DMA的中断标志是否被正确清零。一个很隐蔽的小坑在芯片Fatal Bus Error或DMA中断标志位不清除的情况下CPU永远不会收到新的接收中断。6.3 能通但丢包率很高、PING延迟忽高忽低症状偶尔能PING通但时间从0.5ms跳到50ms甚至偶尔超时。排查思路内存碎片。LwIP内存池不够用导致协议栈无法及时处理突发流量。另外检查接收描述符个数如果系统在满负荷收发时接收描述符被占满且回收不及时丢帧就会发生。解决方法把PBUF_POOL_SIZE调大TCP_WND调大让接收窗口比带宽时延积更大。6.4 能从这里PING通电脑但电脑PING不过开发板症状开发板主动PING电脑能通电脑PING开发板失败。排查思路多半是ARP的问题。电脑发ARP请求询问“谁是192.168.1.200”开发板必须应答。如果LwIP初始化的时候netif-hwaddrMAC地址没有正确赋值或者MAC地址重复了ARP响应就不会被电脑接受。开发板PING电脑能通是因为开发板先发了ARP请求电脑的ARP缓存里已经有了开发板的MAC但反向的ARP学习就被干扰了。给MAC地址一个唯一的值不用非要买OUI随便填02:00:00:00:00:01之类的本地管理地址就行但不要和局域网内其他设备冲突。6.5 LwIP初始化就崩溃或HardFault症状程序跑起来没一会儿进HardFault或者LwIP初始化时直接死机。排查思路先查堆栈空间。FreeRTOS默认任务栈大小只有128字节如果tcpip_thread栈给得不够跑几步就溢出。LwIP的tcpip_thread建议至少给512字节Word为单位的话就是2048字节否则内部调用tcpip_thread过程中很容易栈溢出。另一个是优先级问题tcpip_thread的优先级不要设计成太高否则它抢占低优先级任务时一些共享数据结构的互斥保护操作没做好就会产生恶性并发访问。7. 几个值得留意的LwIP配置细节写到最后再补充几个实际开发中经常被忽视的细节这些都是文档上不常写但真正影响稳定的点。7.1 ARP缓存与超时在局域网内设备之间靠ARP表找MAC地址。LwIP默认的ARP缓存条目数ARP_TABLE_SIZE是10如果你的网关设备比较多建议调大比如20。不然ARP条目被频繁驱逐每次都要重新广播PING会偶尔慢一拍。7.2 零拷贝与PBUFLwIP的接收路径如果做得好可以实现零拷贝——驱动直接复用PBUF的内存作为DMA缓冲区。HC32F4A0的RAM足够大直接用这种方式TCP接收时CPU占用会低很多。但注意PBUF的大小必须能容纳整个以太网帧不然DMA可以把数据写到缓冲区旁边造成内存踩踏。7.3 FreeRTOS和LwIP共用的时基问题LwIP需要一个毫秒级时基FreeRTOS的SysTick频率如果能配成1ms直接用xTaskGetTickCount()作为LwIP的sys_now()返回值就能省一个定时器。如果你用vTaskDelay(1)代替在高负载下会有时间漂移TCP的重传计算就偏了。稳妥做法是u32_t sys_now(void) { return (u32_t)xTaskGetTickCount(); }7.4 任务优先级的精心设计我最终的优先级分配是ETH接收任务优先级最高tcpip_thread次之PHY链路检查任务较低业务任务最低。接收任务优先级高是因为它只做最基本的数据搬运耗时极短不会阻塞系统。而tcpip_thread内部要处理TCP协议栈需要较长的CPU时间放在中等优先级能保证处理不完时被抢占也不会数据错乱。写在最后从零开始移植这套HC32F4A0 LAN8720 FreeRTOS LwIP完整的跑通PING前后花了大概一天半时间。中间大部分时间不是花在写代码上而是花在定位问题、翻手册和看参考例程上。实际干活的时候你会发现只要PHY寄存器能正常读写、RMII时钟不翻车剩下的事情基本就是填坑和调参。做过一遍之后这套东西并没有想象中那么玄乎。你可以把LwIP理解成一个对硬件层有六个函数要求的黑盒FreeRTOS就是给它提供线程和同步机制的操作系统底座你的核心工作就是把中间那几个口子接好。PING测试则是第一道验收门能通后面做TCP服务器、MQTT上云、HTTP网页就都有了扎实的地基。最后给一个小技巧调试LwIP时把LWIP_DEBUG打开ICMP_DEBUG、ARP_DEBUG和ETHARP_DEBUG都设为LWIP_DBG_ON启动时就能看到ARP协议栈在干什么实在PING不通的时候这些日志比你在代码里print一百句话都管用。本文还有配套的精品资源点击获取