1. 为什么选Nucleo-F767ZI做以太网实战
拿到Nucleo-F767ZI这块板子的时候,我第一反应是"这板子做网络应用简直是作弊"。STM32F767ZI自带MAC控制器,板载还焊了一颗Micrel的KSZ8081RNB PHY芯片,RJ45接口也是现成的,连网线就能跑。但真正动手配置的时候,从CubeMX点选到ping通,中间踩的坑足够写满两页A4纸。
这篇内容适合三类人看:第一类是刚拿到Nucleo-F767ZI,想跑通以太网但被CubeMX里一堆选项搞晕的嵌入式新手;第二类是从F103或者F407转过来,发现老经验在新平台上不太管用的老手;第三类是做工业网关、数据采集终端,需要快速验证以太网通信方案的工程师。我会把原理图分析、CubeMX配置、PHY寄存器调试、LwIP协议栈适配这几个环节串起来讲,重点放在"为什么这么配"和"配错了会怎样"上面。
先说一下这块板子的硬件底子。Nucleo-F767ZI的以太网部分用的是RMII接口,不是MII。RMII省引脚,但时钟要求更严格,REF_CLK必须是50MHz,由PHY或者外部晶振提供。板子上KSZ8081的REF_CLK是从STM32的PA1引脚输出的,这个引脚在CubeMX里要配置成RCC_MCO_2或者直接用ETH_REF_CLK。很多人卡在第一步就是没搞清楚这个时钟路径,导致PHY一直处于复位状态,MDIO读出来全是0xFFFF。
另外,Nucleo-F767ZI的RJ45座子自带网络变压器,但如果你是自己画板子,变压器和终端电阻的匹配网络一定要照着PHY手册来,差分对走线阻抗控制在50欧姆,否则链路能协商上但丢包率会高得离谱。我实测过一块差分线没做阻抗控制的板子,ping大包的时候丢包率能到30%,换成Nucleo原板就稳在0%。
2. 原理图里必须盯死的几个关键网络
2.1 RMII信号线的引脚映射
Nucleo-F767ZI的原理图在ST官网能下到,MB1137这个文档。翻到以太网那一页,重点看这几个信号:
- ETH_RMII_REF_CLK:PA1,50MHz参考时钟输出
- ETH_RMII_MDIO:PA2,管理接口数据
- ETH_RMII_MDC:PC1,管理接口时钟
- ETH_RMII_CRS_DV:PA7,载波侦听/数据有效
- ETH_RMII_RXD0:PC4
- ETH_RMII_RXD1:PC5
- ETH_RMII_TX_EN:PB11
- ETH_RMII_TXD0:PB12
- ETH_RMII_TXD1:PB13
这些引脚在CubeMX里配置ETH外设的时候会自动分配,但你要知道它们对应的是哪个物理引脚,因为一旦你外接了其他模块占用了这些脚,就得重新规划。比如PA1如果被用作ADC输入,那REF_CLK就得改由PHY自己产生,这时候KSZ8081的REF_CLK输入要接25MHz晶振,然后内部PLL倍频到50MHz。
2.2 PHY复位电路和地址配置
KSZ8081的复位引脚是低电平有效,Nucleo板上通过一个10k上拉电阻接到3.3V,同时连到STM32的复位电路。这意味着每次MCU复位,PHY也会跟着复位。这个设计省了一个GPIO,但有个隐患:如果你在程序里想单独复位PHY而不复位MCU,就做不到。我遇到过一次PHY寄存器被写乱的情况,只能按板子上的复位键才能恢复。
PHY地址方面,KSZ8081的PHYAD[0]引脚在Nucleo板上是下拉到地的,所以PHY地址是0x00。在CubeMX的ETH配置里,PHY Address要填0。如果你用的是自己画的板子,PHYAD引脚悬空或者上拉,地址就变了,这时候MDIO扫描会找不到设备。
2.3 时钟树配置的坑
STM32F767的ETH外设需要两个时钟:一个是AHB总线时钟,一个是REF_CLK。在CubeMX的Clock Configuration页面,ETH的时钟源要选HCLK,然后分频。我一般把HCLK设到200MHz,ETH分频系数设成2,得到100MHz的ETH内核时钟。REF_CLK那边,如果选的是"Output from PA1",CubeMX会自动把PA1配置成50MHz输出,这个50MHz是从PLL48CLK或者外部晶振分频来的。
这里有个细节:STM32F767的PLL配置里,PLLQ分频出来的时钟要正好是48MHz,因为USB和SDIO都依赖它。如果你把PLLQ设错了,ETH的REF_CLK也会跟着偏,PHY可能能协商上但通信不稳定。我建议在CubeMX里直接点"Resolve Clock Issues",让它自动算,然后手动核对一遍PLLQ的值。
3. CubeMX配置以太网的完整流程
3.1 新建工程和引脚分配
打开CubeMX,选STM32F767ZITx,然后开始配置。第一步先把调试接口打开,SYS里的Debug选Serial Wire,不然下载一次程序后SWD引脚被占用,下次就连不上了。RCC里HSE选Crystal/Ceramic Resonator,Nucleo板上的晶振是8MHz。
接下来在Connectivity里选ETH,Mode选RMII。这时候CubeMX会自动把PA1、PA2、PA7、PC1、PC4、PC5、PB11、PB12、PB13这些引脚标成绿色。检查一下有没有冲突,特别是PA1,如果之前被其他外设占了,要先释放。
3.2 ETH参数配置详解
在ETH的Parameter Settings里,有几个关键选项:
- PHY Interface:RMII
- Auto Negotiation:Enable
- Speed:100Mbps(如果PHY支持,也可以选10Mbps)
- Duplex Mode:Full Duplex
- Carrier Sense During Transmit:Disable
- Receive Own Transmit:Disable
- Loopback Mode:Disable
- Checksum Offload:Disable(LwIP里可以软件校验,硬件校验有时候和协议栈配合有问题)
- PHY Address:0
- PHY Special Control/Status Register:这个不用填,CubeMX会自动处理
Advanced Parameters里,Rx Buffer Size我一般设成ETH_RX_BUF_SIZE_256,Tx Buffer Size设成ETH_TX_BUF_SIZE_256。这两个值影响内存占用和吞吐量,256字节是折中方案,跑TCP吞吐能到80Mbps左右。如果要跑UDP高速传输,可以设成512或者1024,但F767的SRAM要规划好,别把堆栈挤爆了。
3.3 LwIP协议栈的添加
在Middleware里选LwIP,Mode选"Enabled with DHCP"。DHCP方便快速验证,但如果你要固定IP,就选Static IP,然后填IP地址、子网掩码、网关。我建议先用DHCP跑通,再改静态,因为DHCP能自动获取网络参数,减少配置错误。
LwIP的配置里,MEM_SIZE设成16KB,MEMP_NUM_PBUF设成16,PBUF_POOL_SIZE设成16。这些值根据你的应用调整,如果只是ping和简单TCP,默认值够用。如果要跑HTTP服务器或者MQTT,得适当加大。
注意:LwIP的LWIP_NETIF_HOSTNAME可以设成"nucleo-f767",这样在路由器里能看到设备名,方便识别。
3.4 生成代码前的检查清单
生成代码之前,我习惯做这几件事:
- 在Project Manager里把Toolchain设成Makefile或者STM32CubeIDE,看你用什么开发环境。
- Code Generator里勾选"Generate peripheral initialization as a pair of .c/.h files",这样代码结构清晰。
- 检查Clock Configuration,确认ETH时钟是100MHz,REF_CLK是50MHz。
- 在NVIC Settings里,ETH全局中断要使能,优先级设成5左右,别太高也别太低。
点Generate Code之后,CubeMX会生成完整的工程,包括HAL库、LwIP协议栈、ETH驱动。接下来就是编译下载,然后看能不能ping通。
4. 从编译到ping通:实操过程记录
4.1 编译下载和串口输出
生成的工程直接编译,一般不会有错误。下载到板子后,用USB转串口模块接Nucleo的USART3(PD8/PD9),波特率115200。程序跑起来后,串口会打印LwIP的初始化信息,包括PHY地址、协商速度、IP地址。
如果串口没输出,先检查串口线有没有接对,Nucleo-F767ZI的USART3默认是PD8和PD9,但如果你在CubeMX里改了引脚,就要对应调整。另外,LwIP的调试输出要打开LWIP_DEBUG和UART_DEBUG,不然看不到信息。
4.2 PHY寄存器读取和链路状态检查
如果串口打印出"PHY not found"或者"Link down",就要手动读PHY寄存器。在代码里加一段:
uint32_t phyreg; HAL_ETH_ReadPHYRegister(&heth, 0, PHY_BSR, &phyreg); printf("PHY BSR: 0x%04X\r\n", phyreg);PHY_BSR是基本状态寄存器,地址0x01。如果读出来是0xFFFF,说明MDIO通信失败,检查MDC时钟和MDIO上拉电阻。正常值应该是0x786D左右,bit2是Link Status,1表示链路已连接。
如果Link Status是0,检查网线有没有插好,对端设备是不是千兆口。KSZ8081是百兆PHY,接千兆路由器也能协商到100M,但有些老路由器兼容性不好,可以换一个口试试。
4.3 ping测试和常见网络问题
链路通了之后,在电脑上ping板子的IP。如果ping不通,按这个顺序排查:
- 电脑和板子是不是同一网段:板子DHCP获取的IP是192.168.1.x,电脑也要在这个网段。
- 防火墙有没有拦ICMP:Windows防火墙默认允许ping,但有些安全软件会拦。
- 板子的ARP表有没有响应:在串口里打印LwIP的ARP表,看有没有电脑的MAC地址。
- 网线是不是好的:换一根网线试试,我遇到过网线水晶头氧化导致链路不稳定。
ping通之后,可以试试TCP通信。LwIP自带一个echo服务器示例,在lwip.c里把LWIP_ECHO_SERVER打开,然后电脑上用telnet连板子的7端口,发什么回什么。
5. 常见问题速查表和避坑技巧
5.1 PHY初始化失败排查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| MDIO读回0xFFFF | MDC时钟太快 | 降低MDC时钟,CubeMX里ETH的MDC分频设成HCLK/42 |
| PHY地址不对 | PHYAD引脚配置错误 | 检查原理图,确认PHYAD[0]是下拉还是上拉 |
| 链路协商不上 | 网线或对端设备问题 | 换网线,换路由器端口,强制设置PHY为100M全双工 |
| ping丢包严重 | 差分线阻抗不匹配 | 检查PCB走线,差分对要等长,阻抗50欧姆 |
| 中断不触发 | NVIC优先级配置错误 | 把ETH中断优先级设成5,确保没有其他中断抢占 |
5.2 时钟配置的独家经验
STM32F767的ETH对时钟很敏感。我试过把HCLK超频到216MHz,ETH分频后是108MHz,结果PHY能协商上但TCP传输大量丢包。后来老老实实回到200MHz,ETH 100MHz,就稳了。所以别为了跑分去超频,ETH外设的时钟范围是25MHz到100MHz,超了就不保证稳定。
REF_CLK那边,如果你用PA1输出50MHz,CubeMX会自动配置RCC。但要注意,PA1的GPIO速度要设成Very High,不然50MHz方波边沿会变缓,PHY可能识别不了。在GPIO配置里把PA1的Maximum output speed改成Very High。
5.3 LwIP内存配置的坑
LwIP默认的MEM_SIZE是1600字节,跑ping没问题,但一开TCP服务器就崩。我建议至少设成16KB,PBUF_POOL_SIZE设成16。如果跑HTTP,MEM_SIZE要32KB以上。F767有512KB SRAM,别省这点内存。
另外,LWIP_NETIF_TX_SINGLE_PBUF这个选项要打开,不然发送大包的时候LwIP会分配多个pbuf,增加内存碎片。打开之后,发送缓冲区是连续的,DMA直接搬,效率高很多。
5.4 中断和DMA的配合
ETH的收发中断和DMA是绑定的。CubeMX生成的代码里,ETH中断处理函数会调用HAL_ETH_IRQHandler,然后回调到LwIP的ethernetif_input。如果你在中断里做了太多事情,比如打印日志,会导致中断响应变慢,丢包。我一般只在中断里置个标志,然后在主循环里处理。
DMA描述符的地址要对齐到4字节,CubeMX生成的代码已经处理了。但如果你自己改描述符数量,记得改ETH_RX_DESC_CNT和ETH_TX_DESC_CNT,这两个值在ethernetif.c里定义。我一般设成4个RX、4个TX,够用了。
6. 从ping通到实际应用:下一步怎么走
ping通只是第一步,实际项目里还要跑TCP、UDP、MQTT、HTTP这些协议。LwIP自带这些协议栈,但配置起来又是一堆坑。比如MQTT要开LWIP_MQTT,然后实现mqtt_connection_cb回调。HTTP服务器要开LWIP_HTTPD,然后把网页文件放到文件系统里。
我个人的习惯是先用ping验证链路层,再用TCP echo验证传输层,最后上应用层协议。每一步都单独测试,出问题了容易定位。如果一上来就堆一堆功能,调试的时候根本不知道是哪一层的问题。
另外,Nucleo-F767ZI的以太网性能其实不错,我实测TCP吞吐能到80Mbps左右,UDP能到90Mbps。如果做数据采集网关,这个性能足够了。但如果要跑千兆,就得换F7系列里带千兆MAC的型号,比如F767的某些版本或者H7系列。
最后分享一个调试技巧:如果LwIP跑着跑着就死机,先看mem_free和memp_free的统计,在lwipopts.h里打开LWIP_STATS和MEMP_STATS,然后定期打印。如果内存一直减少,说明有内存泄漏,检查pbuf有没有正确释放。我遇到过一次pbuf没释放导致跑几个小时就死机,后来在ethernetif_input里加了pbuf_free就解决了。