ARTICLE DETAIL

资讯详情

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

STM32F407+LAN8720以太网调试全指南:从CubeMX配置到LWIP与FreeRTOS实战

STM32F407+LAN8720以太网调试全指南:从CubeMX配置到LWIP与FreeRTOS实战 兄弟最近在折腾以太网STM32F407 LAN8720 这个组合可以说是目前嵌入式网络应用里最经典也最折磨人的搭配之一。经典在于F407自带MACLAN8720又是性价比极高的PHY芯片二者通过RMII接口一接就能让你的板子“上网”折磨人在于从硬件布线到CubeMX配置再到LWIP协议栈在FreeRTOS里的“水土不服”每一步都有坑等着你。随便搜一下就知道大批人在“Ping不通”上卡了好几天最后发现竟是引脚配错或者时钟树没点对。这篇文章主要帮你解决这几个问题怎么用CubeMX 6.4把整个工程配出来配置完之后还需要手动改哪些代码以及在完全零基础的情况下怎么一步步排查“死活Ping不通”的问题。整个过程需要你有一点STM32基础知道怎么下载代码知道GPIO、串口大概是什么那就够了。至于LWIP和FreeRTOS的原理我会在配置过程中用大白话讲透确保你不仅会用还能知道为什么这么用。1. 动手之前先聊两句硬件在打开CubeMX之前硬件上的坑得先排掉。否则代码写得再漂亮网线插上去灯都不亮全是白搭。1.1 为什么F407要配LAN8720而不是直接用W5500很多刚入门的朋友会问这个问题。其实道理很简单F407内部自带了MAC层也就是数据链路层它需要外接一个PHY芯片也就是物理层芯片来负责把数字信号变成模拟信号传到网线上。LAN8720就是这颗PHY它负责将F407发出来的MII/RMII接口数据转换成网线上的差分信号。而W5500这种芯片是把MAC和PHY都集成进去了甚至硬件TCP/IP协议栈都在里面虽然用起来简单但它绕过了F407内置的MAC不符合我们“用单片机自身资源实现网络通信”的初衷而且成本也更高。所以做高性能、低成本的方案F407 独立PHY是更主流、更灵活的选择。1.2 RMII接口的硬件接线少一根都不行LAN8720支持MII和RMII两种模式我们通常用RMII因为它只需要8根数据线而MII需要16根。RMII接口的接线是有严格规定的你可以把它想象成两个人在用固定频率的电话对讲线接错了对方说什么你都听不见。具体接线如下ETH_RMII_REF_CLK50MHz参考时钟这是最关键的一根线必须由外部晶振或MCU输出50MHz时钟LAN8720和F407的MAC都要靠这个时钟同步。ETH_RMII_CRS_DV载波侦听/数据有效主要用于区分数据线和控制线。ETH_RMII_RXD0、ETH_RMII_RXD1接收数据2位接收数据线。ETH_RMII_TXD0、ETH_RMII_TXD1发送数据2位发送数据线。ETH_RMII_TX_EN发送使能告诉PHY“我要发数据了”。ETH_RMII_MDC、ETH_RMII_MDIO管理接口用于配置PHY内部寄存器相当于“遥控器信号线”。在F407上这些引脚通常有固定的映射。比如RMII_REF_CLK在PA1CRS_DV在PA7RXD0在PC4RXD1在PC5TXD0在PB12TXD1在PB13TX_EN在PB11MDC在PC1MDIO在PA2。我用过好几块不同的板子发现有些偷懒的开发板会把RXD0和RXD1换到别的引脚上这就要求你在CubeMX里手动配置或者干脆改代码映射。所以第一步一定先去查你手上板子的原理图确认每一个脚位别想当然。1.3 LAN8720的硬件设计重点就查三处硬件上最容易出问题的集中在三个地方建议上电前用万用表量一遍第一PHY地址。LAN8720的第七脚RXER/PHYAD0决定了它的I2C地址。一般内部下拉默认地址是0x00也就是ADDR为0。如果你的板子上这个引脚被上拉了地址就变成了0x01那CubeMX里得选对PHY Address不然MDIO通信会失败。第二50MHz参考时钟的来源。LAN8720通常有两种做法一种是有源晶振直接给LAN8720提供50MHz时钟然后LAN8720再把时钟通过REF_CLK引脚反馈给STM32另一种是STM32的MCO引脚输出50MHz给LAN8720。用CubeMX自动生成的项目多数模块默认是从STM32获取时钟但网上很多资料是基于外部有源晶振的方式。你布线时怎么接的配置就要怎么来这是最容易踩的坑之一。第三网络变压器的中心抽头。LAN8720的驱动能力比较弱中心抽头必须接3.3V也就是通过一个电阻连接到3.3V电源千万不能接地。接地会导致信号幅度不够网线插上去电脑识别不了。这个问题很隐蔽很多人查了半天代码最后发现是硬件问题。2. 重点CubeMX 6.4 完整配置流程打开CubeMX 6.4我默认你已经装好了F4的固件包如果没装在MCU选择界面会自动提示你下载等一会儿就好。2.1 时钟树RCC这里乱配必死时钟是所有外设的基础网络模块对时钟极度敏感。LAN8720的RMII接口需要50MHz的参考时钟这50MHz必须非常精准哪怕是几十个ppm的偏差都会导致网络丢包严重甚至不通。在CubeMX的Clock Configuration标签页F407的最高主频是168MHz你需要把HCLK调到168MHz然后看APB1和APB2的总线时钟。这里有个细节以太网模块ETH的时钟挂在AHB1总线上理论上只要AHB1不超频就行但问题是RMII的参考时钟必须单独从MCO2引脚输出。我的做法是这样的在RCC里将HSE设置为Crystal/Ceramic Resonator外部高速晶振通常板子上是8MHz或25MHz然后在Clock Configuration里找到MCO2把它的时钟源设置为PLLI2S并分频到50MHz。为什么要用PLLI2S因为PLLI2S可以精确产生50MHz而不干扰系统主频。这是ST官方推荐的做法。如果你用的板子是有源晶振直接给LAN8720的那MCO2可以不配RMII_REF_CLK信号由PHY反过来提供。设置完之后你会看到PA1引脚上自动出现了ETH_RMII_REF_CLK的功能。这时要注意MCO2的输出引脚是PC9它也要配置为MCO2功能。2.2 引脚配置按原理图一个一个对在Pinout Configuration界面找到Connectivity下的ETH。勾选ETH模式选择RMII。在下方弹出的引脚配置里把PHY Address设为0x00根据你的硬件来PHY Clock Connection设为MCO2 from RCC如果时钟由MCO2提供。此时右侧的芯片图会自动把RMII相关引脚拉出来比如PA1、PA2、PA7、PB11、PB12、PB13、PC1、PC4、PC5等。你需要和你的原理图核对一遍如果有一两个引脚不对直接在芯片图上手动点选改成原理图上的引脚。这一步我觉得可以多花点时间别懒错了后面非常痛苦。2.3 LWIP配置内存和协议栈参数有讲究同样在Connectivity下找到LWIP并勾选。注意CubeMX里的LWIP是ST官方移植好的它依赖一个操作系统抽象层也就是说你可以选择让它跑在裸机上也可以让它跑在FreeRTOS上。我们这里选择FreeRTOS版本这样网络处理和任务调度能并行不会因为网络收发占用大量CPU时间。在LWIP配置项里需要改几个关键参数Memory Size默认是128KB建议改为160KB以上。因为F407的RAM有192KB堆太小了协议栈容易崩TCP连接一多就内存不足。Memory settings里的PBUF Pool Size默认是16可以改为20。TCP/IP Thread Priority如果跑FreeRTOS这个线程优先级默认是OS_PRIORITY_ABOVE_NORMAL建议调低一点比如Normal否则网络中断可能抢占你应用任务的时间片。TCP Window默认16KB保持不动即可。DHCP如果路由器有DHCP可以打开但调试初期建议关闭使用静态IP减少变量。然后在LWIP的Key Options里你会看到Netif、ARP、ICMP等选项ICMP一定要选上不然Ping不通。默认是勾选的但有些人手贱会取消这里提醒一下。IPv6和DHCP这类特性初期调试就全关掉能省不少内存和代码复杂度。2.4 FreeRTOS集成让LWIP跑在任务里在Middleware and Software Packs里找到FreeRTOS选择CMSIS_V1或V2均可。需要注意的是CubeMX 6.4默认的CMSIS版本可能会影响接口兼容性我习惯用CMSIS_V1因为网上参考资料多报错也好找。配置以下几项在Config Parameters里把TOTAL_HEAP_SIZE设为一个合适的值。因为LWIP要占一部分堆再加上你的应用任务和FREERTOS本身的TCB建议设为50KB以上最好70KB左右。RAM不足的话CubeMX编译时会直接报错到时再调整。在Integration里把USE_NEWLIB_REENTRANT选上有些库函数比如printf重定向需要它。默认生成的代码会在main.c的MX_FREERTOS_Init()里创建一个默认的StartDefaultTask你可以留着也可以删掉我们后面会自己建任务。最关键的是CubeMX会自动生成一个ethernetif.c文件这里面实现了底层网卡驱动和OS适配层但是它默认是用信号量或互斥锁的你要确保LWIP的sys_arch.c正确实现。CubeMX 6.4里如果选对了FreeRTOS版本这些都会自动生成不需要你手写。2.5 生成代码后必须做的三件“善后”事CubeMX自动生成的代码是能编译过但离真正能Ping通还差了“三件套”第一确认lwipopts.h里的NO_SYS等于0。这是因为我们跑FreeRTOSLWIP必须以多线程模式运行。如果这个宏是1表示裸机FreeRTOS的调度就白搭了。CubeMX默认会根据你选的OS来配置但最好看一眼。第二检查ethernetif_init()里的PHY地址CubeMX生成的代码里会有一个ETH_PHY_ADDR宏定义确保它是0或与实际硬件一致否则初始化PHY时MDIO会一直读不到寄存器。第三挂载网卡。在main.c里CubeMX会生成MX_LWIP_Init()函数但不会自动把网卡状态调起来。你需要手动调用MX_LWIP_Init()并在FreeRTOS的一个任务里周期性地调用netif-linkoutput等功能或者更简单的做法直接调用lwip的tcpip_thread让它循环处理。不过通常的战术是在MX_LWIP_Init()之后手动把netif_set_up和netif_set_link_up调一下。3. 实操让LWIP和FreeRTOS握手并跑通Ping配置完接下来就是实际撸代码了。这块内容网上零零散散我把自己整理好的完整思路放出来。3.1 FreeRTOS任务和LWIP线程的关系很多人搞不懂LWIP和FreeRTOS是怎么配合的。你只需要记住一个模型LWIP就是一个普通的外设库但它内部维护了几个线程比如tcpip_thread负责协议栈、eth_rx_thread负责收包、eth_tx_thread其实没有这个发送是直接调函数。这些线程LWIP不是自己创建的而是通过系统抽象层sys_arch.c调用FreeRTOS的API来创建的。所以你在CubeMX里配置FreeRTOS其实就是在给LWIP准备一个“运行环境”。当你在自己的代码里写xTaskCreate创建应用任务时LWIP的核心线程已经在后台跑起来了。它们之间用队列、信号量、互斥锁通信。理解这一点你就能明白为什么LWIP任务要分配栈空间以及为什么lwipopts.h里的内存池大小决定了TCP并发能力。3.2 核心代码初始化与轮询在你的main函数里初始化逻辑大概是这样的int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ETH_Init(); MX_FREERTOS_Init(); MX_LWIP_Init(); osKernelStart(); while (1) { // 不会走到这里因为OS调度已经接管了 } }注意MX_LWIP_Init()里的核心逻辑是去调用lwip_init()和netif_add()。netif_add()是网卡接口的注册函数注册之后还需要手动设置netif为up状态。CubeMX生成的代码里有下面这段一定确保存在static void MX_LWIP_Init(void) { // ... 初始化变量 ... ip_addr_t ipaddr, netmask, gateway; IP_ADDR4(ipaddr, 192, 168, 1, 10); IP_ADDR4(netmask, 255, 255, 255, 0); IP_ADDR4(gateway, 192, 168, 1, 1); lwip_init(); netif_add(gnetif, ipaddr, netmask, gateway, NULL, ethernetif_init, tcpip_input); netif_set_default(gnetif); netif_set_up(gnetif); netif_set_link_up(gnetif); }这一段直接决定了你的板子能否被电脑看到。如果你的IP和电脑不在同一网段Ping当然不通这是很多人忽略的物理问题。3.3 在FreeRTOS任务中如何收发数据一旦网络通了剩下的就是写应用层逻辑了。我用一个最简单UDP服务器的例子来说明因为UDP比TCP简单不需要处理连接状态适合测试。创建一个任务void vUdpServerTask(void *argument) { struct netconn *conn; struct netbuf *buf; void *data; u16_t len; err_t err; conn netconn_new(NETCONN_UDP); netconn_bind(conn, IP_ADDR_ANY, 8080); while (1) { err netconn_recv(conn, buf); if (err ERR_OK) { netbuf_data(buf, data, len); // 此时data指向接收到的数据len是你的数据长度 // 可以做任何处理 netbuf_delete(buf); } vTaskDelay(1); } }这里用到的netconn API是LWIP的高级API它已经封装好了互斥和信号量。如果你用最原始的raw API比如udp_new、udp_bind就得自己在回调里小心处理线程安全问题非常容易出Bug。对于应用开发者来说强烈推荐用netconn API不仅代码少而且不容易踩并发坑。3.4 关于Ping通的技巧结尾再谈为什么别人能Ping通你的板子就是不行排了所有配置这里我把从硬件到软件最可能的几个坑再啰嗦一遍。4. 这可能是全网最“接地气”的排查清单4.1 硬件和电气连接是重灾区我见过不下十个案例都是卡在硬件上。所以这一步我把优先级提到最前。从网线插上去观察RJ45座子的灯开始。正常状态下Link/Act灯常亮或闪烁表示物理链路通了。Speed灯亮起表示协商到了100MbpsLAN8720最高就是100M。如果两个灯都没反应硬件电路出问题的概率在90%以上。这时候先量电源LAN8720的AVDD和VDD是否都是3.3VREF_CLK引脚是否能量到50MHz方波如果是MCO2方式提供时钟用示波器点PC9应该能看到50MHz的方波。如果量不到看看是不是MX_GPIO_Init()里把PC9的复用功能改没了。如果时钟没问题再看RMII的四根数据线TXD0、TXD1、RXD0、RXD1和EN、CRS_DV连线是否有虚焊。这些线在信号完整性上有要求尽量短不要在板子上绕圈。4.2 软件配置自查排得清清楚楚排除硬件后再回头审视软件。我在这张表里整理了七八成新手会犯的错误以及对应的处理思路。现象可能原因检查/解决方法Ping不通但灯全亮IP不在同一网段用静态IP关DHCP板子和电脑都设为192.168.1.xPing不通电脑提示超时PHY初始化失败检查PHY Address的硬件配置打印ETH_MMC寄存器确认MDIO通信正常Ping通但丢包严重RMII参考时钟不稳定检查MCO2配置必须精确50MHz用示波器看PC9波形一段时间后死机内存耗尽增大lwipopts.h里的Mem Size或者减少PBUF池子大小重连路由器后不通网线拔插后PHY未重新协商在link_int任务里检测ETH_FLAG_LINK重新初始化网卡这里有个概念如果你用的是开发板板上板载的PHY地址大多是0但有些万能的淘宝板用的是0x01。我建议你调试时加一行printf在初始化后读取PHY的ID寄存器地址是2和3打印出来。如果是0x0000或0xFFFF说明MDIO就没通赶紧查硬件。打印寄存器可以用下面的代码uint32_t idr1 0, idr2 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_ISFR, idr1); HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_ISFR 1, idr2); printf(PHY ID: %04X %04X\r\n, idr1, idr2);正常情况下LAN8720应为0x0007后面的ID可以是0xC109之类的。如果读到0xFFFF优先检查PHY地址线。4.3 一个实测好用的“十分钟Ping通技巧”最后再分享一个我自己的习惯动作。这个问题我踩过很多次尤其在新焊的板子上更容易犯。当你第一次连接上位机Ping的时候不要急着用路由器直接用网线连电脑和开发板。然后把电脑的有线网卡IP手动设为192.168.1.2子网掩码255.255.255.0网关192.168.1.1开发板的IP设为192.168.1.10。然后先用开发板发送一个UDP广播包到255.255.255.255的某个端口再用电脑抓包工具看看能否收到UDP包。如果能收到说明MAC层和PHY收发没问题如果收不到再PingPing不通的排查范围就会缩小到ARP和IP层也就是LWIP协议栈的配置问题。这个“UDP广播探测法”比直接Ping更能快速定位问题层级。Ping不通有可能只是ARP没回复但底层数据收发其实是好的。5. 更进一步的思考为什么有人能一整天连看都不看就调通这不是玄学。用过一段时间你就会发现网络调试本质上是“分层排查”。硬件量不通先查硬件硬件通了再查驱动驱动通了再查IP配置。千万不要从头到尾一遍遍翻代码那样效率极低。我在实际项目里还会把LWIP的DEBUG宏打开让协议栈自己打印调试信息。在lwipopts.h里把LWIP_DEBUG设为1然后选择合适的调试通道比如NETIF_DEBUG、ETHARP_DEBUG等。开着调试信息虽然会拖慢速度但排查问题非常直观你能看到ARP请求进来没有IP层有没有回复。对了还有一点很多人喜欢在CubeMX更新后重新生成代码然后之前的修改全被覆盖了。我建议是这样的所有需要手写的代码尽量放在用户代码区也就是BEGIN/END注释之间比如USER CODE BEGIN xxx这样哪怕重新生成也不会丢。我之前就吃过这个亏改好了的LWIP钩子重新生成全没了还得逆向找回来。6. 最后聊一个很多新手会卡住的点就是LAN8720的复位。一般LAN8720的复位引脚会接在某个GPIO上比如NRST或者由MCU某个引脚控制。如果是MCU控制要在初始化ETH之前把复位脚拉低至少10ms再拉高并延时等PHY稳定。这个操作必须在MX_ETH_Init()之前做。如果你在CubeMX里生成了代码但没有对复位脚做延时经常会遇到第一次上电Ping不通按一下复位键就通了的情况。这就是因为PHY没有来得及完成上电启动MCU就急急忙忙去读它的寄存器了。解决方法是在GPIO初始化之后加入一段延时或者手动拉一下复位引脚。// 在MX_ETH_Init()之前 HAL_GPIO_WritePin(LAN8720_RST_GPIO_Port, LAN8720_RST_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(LAN8720_RST_GPIO_Port, LAN8720_RST_Pin, GPIO_PIN_SET); HAL_Delay(100);我甚至见到过有人调试了两天最后发现就是复位引脚没拉高PHY芯片一直处于复位状态当然Ping不通。所以如果你的板子莫名其妙“偶尔不通”优先把复位时序打印出来看。7. 写在最后关于这个组合的一些实在话F407加LAN8720这套方案虽然调试过程折磨人但一旦调通了你会对整个网络通信的底层原理有非常深的理解——比直接用CH395、W5500这种集成芯片理解深刻多了。你会知道什么是RMII为什么50MHz时钟这么重要TX_EN和CRS_DV到底起了什么作用ARP广播包又是怎么回事。这些知识在你以后用任何带MAC的MCU比如H7、RT系列时都能直接复用因为它们的基本逻辑是一样的。所以如果你现在正卡在Ping不通别急大概率就是上述某个细节忽略了。按本文的思路从硬件电气、时钟、PHY地址、LWIP配置、FreeRTOS集成这几个维度一层层剥开问题总会浮出水面。我个人更建议你在项目初期专门用一块独立的开发板花一天时间把裸机版本先用HAL库调通加上串口打印再套上FreeRTOS。很多人一上来就裸机OSLWIP三层叠buff出问题的时候根本不知道是哪一层出的问题。等到你确认拉到最底层也能通再逐步往上加操作系统排查起来会轻松十倍。
返回列表