ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F107+LAN8720A以太网实战:CubeMX配置与LWIP移植避坑指南

STM32F107+LAN8720A以太网实战:CubeMX配置与LWIP移植避坑指南 不少人第一次在CubeMX里点开STM32F107的ETH外设时多少会有点懵——芯片手册上明明写着Ethernet MAC但工程生成之后发现事情远没结束还得自己接一颗PHY把寄存器、时钟、DMA描述符都跑通LWIP协议栈才能真正工作。F107内置MAC但不带PHY这是很多人一开始没转过弯的地方而LAN8720A正好是市面上最常用、资料最全的低成本百兆PHY两者配合可以在工业网关、数据采集器、简易物联网节点这些场景里稳定跑网络通信。这篇文章我会直接按“CubeMX配置 → 硬件连线 → LWIP移植 → 常见问题排查”这条完整路线走一遍把PHY地址、50MHz时钟来源、DMA描述符、LWIP内存参数这些最容易翻车的点全部拆开讲。代码会给出能直接抄的程度同时把背后“为什么要这么配”的逻辑也说明白。适合正在用F105/F107做联网产品或者第一次接触STM32以太网开发的朋友参考。1. 整体设计思路为什么是F107LAN8720A数据是怎么跑通的1.1 这个组合解决了什么问题STM32F107是一颗主频72MHz的Cortex-M3芯片最高配版本带256KB Flash和64KB RAM内部集成了一整套以太网MAC控制器和DMA支持10M/100M速率还带USB OTG。但要注意它只集成了MAC不会集成PHY。你可以把MAC理解为“网卡的协议处理核心”它负责组帧、解析帧、管理DMA传输而PHY物理层收发器负责把MAC送过来的数字信号变成网线上能跑的差分电平信号再对端PHY恢复成数字信号。两者缺一不可。之所以选LAN8720A核心原因是它的RMII接口引脚少、功耗低、价格便宜而且市面上流通量大参考设计好找。F107的MAC加上LAN8720A的PHY刚好能组成一套完整的10M/100M以太网方案。对比方案是外挂SPI接口的W5500但那属于“MACPHY协议栈全集成”的思路优点是MCU负担小缺点是价格高、吞吐上限低、大流量场景容易跑不满。F107LAN8720A的路线更适合需要跑完整TCP/IP协议栈、对成本和灵活性都有要求的场景。1.2 从网口到应用的数据通路整个数据通路大概是这样的网线进来的模拟差分信号先进LAN8720APHY完成解码、串并转换、自动协商等物理层工作然后通过RMII接口把4位数据TXD0/TXD1/RXD0/RXD1和控制信号TX_EN、CRS_DV、REF_CLK交给STM32F107的MAC控制器MAC把数据组装成以太网帧再通过内部的DMA描述符搬运到内存里最后LWIP协议栈从内存缓冲区里取出IP报文经过TCP/UDP层处理最终交给你的应用程序。反过来发送方向也一样应用程序把数据交给LWIPLWIP打包成TCP段或UDP报文交给MACMAC通过DMA读取内存中的数据拼成以太网帧通过RMII发送给LAN8720APHY再调制成差分信号发出去。理解这条通路之后后面所有配置都有明确指向CubeMX里配置ETH其实是在配置MAC和DMA配置PHY地址是为了让MAC能通过MDIO管理接口去读写LAN8720A的寄存器LWIP配置则是决定数据在内存里怎么排队、缓冲多大、校验和怎么算。1.3 为什么选RMII而不是MIIF107的MAC其实同时支持MII和RMII两种外部接口。MII需要16根信号线RMII只需要7根。对于百兆以太网两者实际速率都能到100Mbps但RMII把数据位宽从4位砍到2位用50MHz时钟翻倍采样来弥补所以引脚少了很多。在PCB面积紧张、引脚资源宝贵的板子上RMII几乎是默认选择。但是RMII有一个先决条件必须有一个精确的50MHz参考时钟这个时钟要同时供给MAC和PHY。这也是这个项目里最大的坑之一。很多人在这一步直接用STM32的MCO引脚输出一个“约50MHz”的时钟给LAN8720A结果网络时通时断或者干脆link都不上。原因后面细说。2. 硬件连接与电路细节先避开两个致命坑2.1 RMII引脚映射与最小连接表STM32F107的ETH引脚在芯片内部已经固定好了不可以像GPIO那样随意映射到其他引脚。这一点和F407的情况类似所以画原理图之前一定要对着数据手册的Alternate Function表查清楚。下面是F107在RMII模式下最常用的一组引脚映射信号名STM32F107引脚方向说明ETH_RMII_REF_CLKPA1输入50MHz参考时钟由PHY提供ETH_RMII_CRS_DVPA7输入载波侦听/数据有效ETH_RMII_TXD0PB12输出发送数据bit0ETH_RMII_TXD1PB13输出发送数据bit1ETH_RMII_TX_ENPB11输出发送使能ETH_RMII_RXD0PC4输入接收数据bit0ETH_RMII_RXD1PC5输入接收数据bit1ETH_MDCPC1输出MDIO管理时钟ETH_MDIOPA2双向MDIO管理数据除了这几根信号线LAN8720A还需要一个中断输出引脚nINT默认高有效可配置通常接到STM32的一个EXTI引脚用来快速感知link状态变化和远端唤醒事件如果你不想用中断也可以在主循环里轮询PHY寄存器这个后面会讲。2.2 时钟方案25MHz晶振 REF_CLK输出是最稳的做法LAN8720A内部有一个PLL电路它支持两种外部时钟输入方式一种是直接输入50MHz时钟到XI引脚另一种是外接25MHz无源晶振由芯片内部PLL倍频到50MHz。无论哪种方式LAN8720A都会在REF_CLK引脚上输出一路50MHz时钟供STM32F107的PA1使用。很多参考设计里会有一个“偷懒”做法用STM32的MCO引脚PA8输出50MHz时钟一路给LAN8720A的XI另一路接回PA1作为RMII参考时钟。这个方案理论上可行但实际上非常挑板子。因为MCO输出的时钟一般需要分频来自PLL抖动的控制不如专用时钟芯片而且MCO驱动能力有限要同时驱动PHY和MAC两个负载波形质量难保证。实测下来MCO方案在低频或短距离场景可能能通但在温度变化、网线较长、板子布线不佳时就容易丢包。我强烈建议使用LAN8720A官方数据手册上最常见的方案外部接一个25MHz无源晶振并联两个20pF左右负载电容让PHY内部PLL生成50MHz然后从REF_CLK引脚输出给STM32的PA1。这个方案里PA1是输入方向单片机不需要产生高频时钟稳定性最好模块化产品里普遍采用。如果你用的是现成的LAN8720A小模块大概率就是这种设计直接检查模块上有没有25MHz晶振就行。2.3 PHY地址和复位电路不对就全盘皆输LAN8720A的PHY地址由PHYAD0引脚的电平决定。PHYAD0下拉到地时PHY地址是0x00上拉到3.3V时地址是0x01。绝大部分成品模块默认都是下拉对应地址0。这本身不算难难的是很多人改了PHY地址之后忘了同步修改HAL库里的PhyAddress配置结果MDIO读写全部失败读寄存器读不到地址整个网络起不来。另一个常被忽略的是复位电路。LAN8720A的NRST引脚需要在上电后保持一段时间的低电平然后释放释放之后PHY内部还需要一段时间完成自检和校准这时候立刻去访问MDIO寄存器很可能读到超时或者错误值。正确做法是给PHY复位留出足够延时建议主程序初始化以太网前至少延时10ms稳妥一点可以延时50ms。有些板子把PHY的复位引脚直接接到了STM32的NRST上让PHY跟随MCU一起复位这种接法省事但不推荐在产品上用因为PHY复位之后和MCU没有明确的时序关系排查问题时会多一个不确定点。3. CubeMX工程配置从零生成一个带LWIP的项目3.1 时钟和调试口配置用CubeMX新建工程时首先要选对芯片型号STM32F107VC、STM32F107RB这类型号才带ETH外设F103系列是找不到ETH的。时钟树方面F107的系统时钟最高72MHz通常用外部8MHz晶振经PLL倍频到72MHz。这里不需要特别为ETH准备什么特殊时钟因为RMII参考时钟由PHY侧给出MAC直接用AHB总线时钟跑DMA和寄存器访问即可。调试口建议保留SWD同时打开串口1作为调试输出口。LWIP调通之前你一定会遇到各种“为什么ping不通”的问题如果没有串口打印PHY寄存器值和网络状态排查起来会非常痛苦。调好串口打印比调好网络本身更重要。3.2 ETH外设与LWIP中间件配置在Pinout Configuration界面里找到Connectivity下的ETH勾选RMII模式。此时CubeMX会自动把PA1、PA7、PB11、PB12、PB13、PC1、PC4、PC5这些引脚分配好。然后进入ETH的参数配置界面关键项有MAC地址填一个本地管理地址例如02:00:00:12:34:56。第一位bit0为0表示单播bit1为1表示本地管理。PHY地址填0对应LAN8720A默认PHYAD0下拉的情况。媒介接口选择RMII。RX描述符数和TX描述符数RAM充足时可以各配4个后续根据内存压力再调整。接下来在Middleware组里勾选LWIP。这里面参数非常多但真正需要动手改的其实就是内存相关的几个值。如果芯片是F107VC64KB RAM建议按下面的值先跑起来参数建议值说明MEM_SIZE16384LWIP动态内存池大小太小则TCP会话开不起来PBUF_POOL_SIZE8pbuf数量每个约1520字节PBUF_POOL_BUFSIZE1520能放下一整个以太网帧TCP_MSS1460最大分段大小对应MTU1500-40字节头TCP_WND5840接收窗口建议4倍TCP_MSSTCP_SND_BUF5840发送缓冲建议4倍TCP_MSSMEMP_NUM_TCP_SEG16TCP段数量太小会导致发送阻塞LWIP的默认参数是为RAM比较大的平台准备的直接用在F107上很容易出现内存不足、TCP连接建立失败之类的问题。先按上面这套参数跑通再根据实际内存占用调整。注意PBUF_POOL_BUFSIZE如果小于1520接收大包时会丢包。3.3 生成工程后必须检查的三个位置CubeMX生成的工程并不是开箱即用的至少有三个位置需要手工确认。第一个是PHY地址。HAL库的ETH初始化代码里会有PhyAddress这个字段如果生成时没有正确填入必须在代码里改成和硬件一致否则后面HAL_ETH_ReadPHYRegister、HAL_ETH_WritePHYRegister全部白搭。第二个是ethernetif.c里的DMA描述符和缓冲区定义。CubeMX生成的模板里会定义TX/RX描述符数组和缓冲区数组例如ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT] __attribute__((aligned(4))); ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT] __attribute__((aligned(4))); uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE] __attribute__((aligned(4))); uint8_t Tx_Buff[ETH_TX_DESC_CNT][ETH_TX_BUF_SIZE] __attribute__((aligned(4)));这些数组必须4字节对齐最好单独放到一个不会被优化器乱放的位置如果条件允许放到单独的内存区域更稳。F107没有F407那种CCM RAM问题但对齐属性不要删。第三个位置是HAL_ETH_Init内部的媒体接口设置。要确认生成代码里MediaInterface是HAL_ETH_RMII_MODE不是HAL_ETH_MII_MODE。这个错了根本link不上。4. 关键代码实现与解析4.1 ethernetif驱动与DMA描述符CubeMX生成的LWIP模板已经把ethernetif.c、ethernetif.h、lwip.c这些文件都放好了理论上不需要大改。但为了确认我们没有踩到暗坑还是建议看一遍low_level_init函数。它负责初始化MAC和DMA描述符并把这些描述符链接到LWIP的netif结构上。你要确认以下几点#define LAN8720A_PHY_ADDRESS 0x00U void low_level_init(struct netif *netif) { heth.Instance ETH; heth.Init.MACAddr[0] 0x02; heth.Init.MACAddr[1] 0x00; heth.Init.MACAddr[2] 0x00; heth.Init.MACAddr[3] 0x12; heth.Init.MACAddr[4] 0x34; heth.Init.MACAddr[5] 0x56; heth.Init.MediaInterface HAL_ETH_RMII_MODE; heth.Init.PhyAddress LAN8720A_PHY_ADDRESS; heth.Init.RxDescCount ETH_RX_DESC_CNT; heth.Init.TxDescCount ETH_TX_DESC_CNT; if (HAL_ETH_Init(heth) ! HAL_OK) { Error_Handler(); } }注意MACAddr是6字节数组字节顺序从高位到低位。PHY地址一定要和硬件对应。如果PHY地址填错HAL_ETH_Init里面有一步会尝试读PHY寄存器来配置Basic Control Register通常返回超时或错误你能直接观察到Error_Handler被触发那基本就是PHY地址没对上。4.2 中断处理和LWIP裸机轮询STM32F107的ETH中断入口是ETH_IRQHandler在HAL库模式下一般这样处理void ETH_IRQHandler(void) { HAL_ETH_IRQHandler(heth); } void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { ethernetif_input(gnetif); }HAL库的中断回调里直接调用ethernetif_input把收上来的包交给LWIP去处理。对于裸机应用这个写法最简单。但要注意ethernetif_input内部会获取信号量或者临界区在中断里调用时要确保LWIP在编译时开启了对应的选项一般是没问题的。如果你使用的是CubeMX默认生成的裸机工程主循环里也会有一个MX_LWIP_Process函数它内部会调用ethernetif_input和sys_check_timeoutsvoid MX_LWIP_Process(void) { ethernetif_input(gnetif); sys_check_timeouts(); }写成这样之后主循环就是LWIP的“心脏”。LWIP的TCP定时器、重传计时、DHCP超时都靠sys_check_timeouts驱动所以主循环中这个函数一定要频繁调用不能随便加长阻塞延时。4.3 一个最简单的TCP Echo Server代码调通之后第一个TCP应用强烈建议做Echo Server客户端发什么服务端原样返回什么。它逻辑简单、验证链路最直接也足够暴露协议栈配置问题。下面是一份可以在LWIP raw API下运行的完整示例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_recved(pcb, p-len); if (tcp_write(pcb, p-payload, p-len, TCP_WRITE_FLAG_COPY) ! ERR_OK) { // 发送缓冲区不足示例中直接丢弃 } pbuf_free(p); } else { tcp_close(pcb); } 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 tcp_echo_server_init(void) { echo_pcb tcp_new(); if (echo_pcb ! NULL) { err_t err tcp_bind(echo_pcb, IP_ADDR_ANY, 8080); if (err ERR_OK) { echo_pcb tcp_listen(echo_pcb); tcp_accept(echo_pcb, echo_accept); } } }这段代码里有一个细节要说明tcp_recved必须在调用tcp_write之前调用它告诉LWIP“我已经把数据从接收缓冲区取走了”LWIP会据此更新接收窗口。tcp_write使用TCP_WRITE_FLAG_COPY标志让LWIP把应用数据复制到发送缓冲避免调用者必须长期持有该内存区域。Echo场景数据量小这种简化是安全的。在主函数里完成MX_LWIP_Init之后调用tcp_echo_server_init()然后在主循环里持续调用MX_LWIP_Process()。通过电脑上的网络调试助手连接开发板IP的8080端口发送任意字符串正常情况会原样返回。4.4 静态IP与DHCP两种模式怎么切CubeMX的LWIP配置里有IP地址、子网掩码、网关这是在静态IP模式下用的。如果你想用DHCP有两种做法一种是在CubeMX参数里把DHCP选项开启这样生成代码初始化后会自动发起DHCP请求另一种是代码手动控制。手动控制的好处是可以打印日志方便观察DHCP有没有成功。示例代码void lwip_dhcp_start_with_log(void) { ip_addr_t ip; ip_addr_t mask; ip_addr_t gw; IP_ADDR4(ip, 0, 0, 0, 0); IP_ADDR4(mask, 0, 0, 0, 0); IP_ADDR4(gw, 0, 0, 0, 0); netif_set_addr(gnetif, ip, mask, gw); if (dhcp_start(gnetif) ! ERR_OK) { printf(DHCP start failed\r\n); } }DHCP请求不是立刻完成的需要等几秒。你可以周期性地检查gnetif的ip_addr是否变为非零地址确认是否成功。如果连的是电脑直连电脑端一定要手动设一个静态IP比如192.168.1.100/24否则电脑自己也在用DHCP找地址两边都等不到回应谁都拿不到IP。这个现象非常常见先排除这个人为因素再排查代码。5. 常见问题与排查实录5.1 现象与排查速查表把这段时间遇到最多的问题整理成一张表方便直接对照现象可能原因检查方法网线插入后PHY没linkPHY复位、时钟、变压器问题看LAN8720A的LED核对原理图PHY能link但ping不通PHY地址错误、MAC地址问题、LWIP内存不足读PHY ID寄存器检查MAC地址第一次能通重启后不通PHY复位时序不够、上电初始化顺序问题增加延时、看串口日志DHCP一直不获IP对端没有DHCP服务、LWIP DHCP未编译、链路未up检查DHCP宏、先在静态IP下排除网络问题网络吞吐极低DMA描述符太少、TCP窗口太小查看描述符数量、增大TCP_WND传大文件卡死TCP重传异常、LWIP内存碎片打印内存剩余检查MEMP_NUM_TCP_SEG其中一个极容易踩的坑是PHY的nINT引脚没有处理但LWIP模板里默认配置了PHY中断相关代码。如果连接不上可能是中断触发后看起来没反应。实际上很多模板并不依赖PHY中断而是靠主循环轮询网线状态。在没有明确需求时可以先把PHY中断的配置代码放一边专心验证静态IP下ping通再逐步加功能。5.2 通过寄存器直读定位问题MDIO接口是MAC和PHY之间的管理通道几乎所有PHY层问题都能通过读寄存器来定位。F107的HAL库提供了HAL_ETH_ReadPHYRegister直接使用即可。建议在初始化后打印一次PHY ID和状态寄存器uint32_t phy_id1 0; uint32_t phy_id2 0; uint32_t bsr 0; HAL_ETH_ReadPHYRegister(heth, 0x02, phy_id1); HAL_ETH_ReadPHYRegister(heth, 0x03, phy_id2); HAL_ETH_ReadPHYRegister(heth, 0x01, bsr); printf(PHY ID1: 0x%04X, PHY ID2: 0x%04X, BSR: 0x%04X\r\n, phy_id1, phy_id2, bsr);LAN8720A的PHY ID寄存器的合理读数应该是0x0007和0xC0F1如果读出来全是0xFFFF说明MDIO时序有问题或者PHY地址不对或者PHY根本没有正常复位。BSR寄存器是基本状态寄存器bit2是link statusbit5是自动协商完成位。网线插好后bsr的bit2应该为1如果一直为0问题基本在硬件水晶头、变压器、RJ45、PHY电源、时钟挨个查。5.3 串口日志是最好的调试伙伴有一次我在调试时发现所有寄存器都正常、链路也通但PC就是ping不通开发板。后来打开串口打印才发现开发板把MAC地址配成了以01开头的组播地址虽然以太网不会报错但ARP响应根本没人理会交换机会认为这是一个多播MAC导致全网都学不到这个端口的正确MAC映射。这种问题不看日志光靠抓包会浪费很多时间。用串口打印LWIP的网卡状态也很有用printf(IP: %s\r\n, ipaddr_ntoa((const ip_addr_t *)gnetif.ip_addr)); printf(Mask: %s\r\n, ipaddr_ntoa((const ip_addr_t *)gnetif.netmask)); printf(GW: %s\r\n, ipaddr_ntoa((const ip_addr_t *)gnetif.gw));IP地址全是0就说明DHCP没成功或者静态IP还没配置进去。把这句话放进周期任务里能快速看出网络是否真的进入正常工作状态。5.4 避坑清单记得检查这几个“小”问题很多项目卡住不是因为某个深奥的协议问题而是一些非常基础的小细节。硬件上LAN8720A的供电通常是3.3V但有些模块为了驱动LED或接口电平转换额外需要2.5V或1.8V参考电压这些电源如果没有处理干净PHY可能出现间歇性异常。软件上LWIP的CHECKSUM_BY_HARDWARE宏一定不要开因为F107的MAC寄存器结构并不同时支持硬件校验和卸载一旦误开发出的包校验和错误接收方会直接丢弃现象就是ping不通但抓包能看到请求。另外MAC地址也不建议用01开头或FF开头更不要全零。全零MAC在局域网里偶尔能通但某些交换机会直接丢弃不要在这种地方浪费排查时间。6. 实测结果与实用建议6.1 静态IP下的Ping和简单吞吐测试按上面的配置调通之后静态IP模式下ping开发板响应时间在1ms以内丢包率0%。TCP Echo Server的8080端口测试用网络调试助手连续发送几百条消息都能正确原样返回。这个结果在大部分百兆以太网应用场景中已经足够。如果进一步测试吞吐可以把TCP窗口从4倍MSS提升到8倍MSS同时把发送缓冲加大到8倍MSS前提是RAM还够用。F107VC的64KB RAM在这种配置下会比较紧张建议在项目里把不需要的功能模块裁剪掉比如串口缓冲区、USB缓冲区的占用能省则省。实在不够就减小PBUF_POOL_SIZE和MEMP_NUM_TCP_SEG换取TCP_WND的增大因为窗口大小直接决定TCP吞吐上限。6.2 调试阶段的几个额外小技巧最后分享几个在实际调试中比较顺手的小技巧。准备一台傻瓜交换机或者路由器把开发板和电脑都插到同一个局域网里比开发板直接连电脑更接近真实环境也能避免很多奇怪的“直连不识别”问题。每次修改硬件或接线后先不急着看代码先观察LAN8720A模块上的Link LED。如果网线插上后LED不亮这说明物理层根本没通再好的协议栈配置也白搭。很多新手一上来就盯着LWIP配置结果搞了半天实际是RJ45座子焊虚了。调试PHY寄存器时建议把打印放在初始化完成之后、LWIP开始之前这样即使LWIP因为某种原因卡住你也能看到关键信息。如果初始化时HAL_ETH_Init返回错误优先检查PHY地址和复位延时不要往下继续查协议栈。用抓包工具看网络请求时重点观察ARP请求和ARP响应。如果开发板只收到ARP请求但从不应答问题多半在MAC地址或者LWIP的ARP表异常如果开发板回了ARP但PC还是超时那就要看TCP/UDP层的数据交换是否正常。这个排查顺序比漫无目的地改参数靠谱得多。我个人在实际调试中最大的体会是STM32F107LAN8720A这套方案本身并不复杂但它横跨了硬件电路、MCU外设、协议栈三层任何一个地方出问题都会表现为“网络不通”所以排查时一定要分层去看。先确认物理层link再确认PHY寄存器可读再确认MAC能收发帧最后才去看TCP/IP栈的行为。按这个顺序走大多数问题都能在半小时内定位到具体模块。希望这篇避坑指南能让你少走弯路。
返回列表