ARTICLE DETAIL

资讯详情

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

STM32H7上LWIP协议栈压缩至32KB内存的千兆以太网优化

STM32H7上LWIP协议栈压缩至32KB内存的千兆以太网优化 把LWIP协议栈塞进32KB内存还要在STM32H7上跑千兆以太网这事儿一开始听着像硬凑但我手头这个项目确实是被逼到了这一步。H7的RAM本来很宽裕可AXI SRAM被图形界面和采集算法占走了大半最后留给网络协议栈的只有一块32KB的SRAM4。我花了两天晚上把常规LWIP工程从七八十KB内存一路压到32KB以内跑通了基于RGMII的千兆链路TCP连接能稳稳挂一整天不掉线。这篇就把完整的优化思路和参数配置抄出来适合正在抠RAM、被LWIP内存折腾的人参考。1. 项目背景与整体设计思路1.1 为什么是32KB一个真实的内存困局先说清楚这不是H7没内存而是系统里各模块都在抢。H743有一块AXI SRAM0x24000000512KB带宽高适合大块连续数据图形层和高速采集理所当然放在这里SRAM1/2/3总共约256KB被RTOS任务栈、DSP算法库占掉大半最后能专门划给以太网的是我挑的SRAM4区域。SRAM4在0x38000000一共64KB其中一半还要给别的功能用以太网极限预算就是32KB。初听很离谱但嵌入式项目里这种“你只能分到这么多”的约束太常见了越早接受越早开始想办法。那32KB到底能不能跑千兆实话实说跑千兆线速125MB/s级别需要很大的缓冲窗口32KB铁定不够。但我把目标定在“稳定千兆以太网通信”链路协商到1000M FullTCP连接长期稳定1KB以内的小包不丢突发大流量不把系统搞死。这个目标下32KB是可行的。很多工业设备说白了就是心跳、参数配置、命令上报不需要拿iperf跑满带宽。1.2 方案选型RAW API比Netconn省在哪LWIP常见三种接口RAW、Netconn、Socket。Netconn和Socket是线程安全封装开发爽但每个连接都要配套队列、信号量、netbuf额外占用少则几百字节多则1KB以上在32KB的预算里扛不住。RAW API写起来繁琐要自己管理各种回调可它没有那层封装tcp_pcb pbuf就是绝大部分开销内存可控得多。所以我的方案很明确采用RAW API NoSys模式在lwipopts.h里直接关掉LWIP_SOCKET和LWIP_NETCONN。如果项目里还有RTOS也不冲突把网络接收处理放在一个独立线程里用信号量唤醒ethernetif_input数据路径完全绕开Socket层。CubeMX生成LWIP代码时如果选了NoSys很多OS封装也不会编译进来内存省了一大截。2. 内存消耗全解优化前先算清楚账2.1 32KB都去哪了一份完整的内存支出清单不夸张地说CubeMX默认生成一个带LWIP的工程内存占用轻松超过80KB。其中几大块是内存项目默认/常见值占用估算ETH DMA描述符4 RX 4 TX每个一二十字节约0.2KBETH RX DMA缓冲4 × 2048B8KBETH TX DMA缓冲4 × 2048B8KBLWIP堆 MEM_SIZE默认可到64KB实际分配数十KBPBUF池16 × 1568B约25KBTCP/UDP PCB池等默认数值偏大若干KBNetconn/Socket层若开每连接数百字节若干KB要把这些压进32KB等于砍掉一半以上甚至砍到三分之一。我建议把所有开销分成两条线第一DMA描述符和收发缓冲区是硬件刚性开销只能压缩数量和单缓冲大小第二协议栈内部的堆、内存池、并发连接数是软开销按实际业务逐项裁剪。先算清楚账再动手改不然就是盲调。2.2 千兆链路对缓冲的特殊要求千兆PHY通过RGMII接口和MCU的MAC直连链路速率由PHY决定但“本站能不能连续接收突发帧”由DMA缓冲和PBUF池共同决定。我建议接收路径优先保证RX DMA缓冲数量保持4个单缓冲长度取1536字节正好覆盖标准以太网帧1518字节还带一点点余量这样RX路径合计约6KB。发送端尽量利用LWIP的PBUF池参与DMA发送不保留整块大TX缓冲或者把TX缓冲数量压到2个又能省出3KB左右。注意1536字节的RX缓冲意味着带VLAN Tag的1522字节帧或巨型帧无法完整接收。如果你的业务里明确有这类帧这项优化就不成立。我在项目里确认过所有下行帧都是标准1500字节以内的IP包才敢这么砍。优化之前先明确业务帧的最大长度这是不能跳过的步骤。3. CubeMX配置与LWIP裁剪把不用的功能全扔掉3.1 ETH外设与PHY的CubeMX配置我用STM32CubeMX 6.x生成基础工程。芯片选STM32H743ETH接口这里要特别提醒开发板常见的RMII接口只能跑到100Mbps千兆必须走RGMII并搭配一颗千兆PHY。我手里这块板用的PHY是RTL8211FPHY地址被硬件拉到0所以在CubeMX的ETH配置里PHY Address填0同时勾选ETH的DMA中断。时钟配置同样关键。H7的MAC时钟来自RCC里的ETH相关时钟RGMII模式下PHY的125MHz参考时钟必须有稳定来源要么外部晶振芯片要么由MCU的MCO输出。这个时钟不对表现极其诡异PHY状态寄存器能读通但千兆链路就是协商不上。生成代码后HAL_ETH_Init里会读PHY寄存器PHY地址写错的话连ID都读不到。CubeMX生成的代码默认4个RX描述符、4个TX描述符RX缓冲长度由stm32h7xx_hal_eth.h里的ETH_RX_BUFFER_SIZE控制。这些值可以直接改但更重要的一步是把它们对应的数组放到正确的内存段这是第4节的重头戏。3.2 lwipopts.h逐项裁剪核心CubeMX的LWIP配置界面能设一部分参数但真正细致的裁剪还得直接改lwipopts.h。我在32KB预算下最终落地的一套关键配置如下参数32KB优化值说明MEM_SIZE8192协议栈动态堆别超过12KBMEMP_NUM_PBUF8报文等待队列MEMP_NUM_TCP_PCB4并发TCP连接业务主要用1条MEMP_NUM_TCP_SEG4TCP分段队列内存占用大头之一MEMP_NUM_UDP_PCB2给局域网检测协议留的MEMP_NUM_NETBUF0RAW模式不需要MEMP_NUM_NETCONN0不开NetconnPBUF_POOL_SIZE8接收关键池建议不低于6PBUF_POOL_BUFSIZE1568覆盖最大标准帧TCP_SND_BUF16384发送窗口TCP_WND16384接收窗口TCP_QUEUE_OOSEQ0关闭乱序队列LWIP_IPV60关掉IPv6LWIP_SNMP0关掉SNMPLWIP_IGMP0关掉组播LWIP_DHCP1继续用DHCPLWIP_DNS1按需开启这里面我最想强调TCP_QUEUE_OOSEQ。默认开启时LWIP会把乱序到达的TCP段缓存起来重新排序功能是好的但每个乱序段都要占内存在弱网环境尤其费。千兆有线这种低乱序场景我直接关掉省下不少内存。业务场景要是有大量丢包和重传那这个决定需要重新评估。3.3 PBUF池与DMA缓冲的参数联动PBUF池大小和接收稳定性是强相关的。网卡收到帧后ETH驱动从PBUF池拿一个PBUF_POOL类型的pbuf把DMA缓冲里的数据复制进去再交给协议栈。PBUF_POOL_SIZE太小突发流量时池子一空新到的帧直接被丢上层表现为偶发丢包。我最终用的是RX DMA缓冲4×1536字节加上PBUF_POOL_SIZE 8这一块总共约12KB实测跑了一整天控制帧零丢失。PBUF_POOL_BUFSIZE也有讲究。LWIP计算这个值的时候已经把链路层头、IP头、TCP头以及payload对齐全算进去了所以1568这类值不是拍脑袋定的是满足1520字节左右实际帧体量的安全值。我曾看人为了省内存把它改成1400结果协议栈解析时payload错位TCP三次握手都完不成。这种硬性开销不能省省了就是在给自己埋雷。4. 实操过程与核心环节实现在32KB里把链路跑起来4.1 DMA描述符与缓冲区的内存放置这是H7专属的大坑。ETH DMA是总线主机它访问不了CPU核心的DTCM区域。很多人把DMA描述符数组定义在普通RAM段编译一版发现运行随机死机大概率就是内存落到了DMA不可达的地方。稳妥的做法是把DMA描述符和RX/TX缓冲放到0x38000000的SRAM4并用链接脚本固定。C语言里这样定义ETH_DMADescTypeDef dma_rx_desc[ETH_RX_DESC_CNT] __attribute__((section(.eth_dma), aligned(4))); ETH_DMADescTypeDef dma_tx_desc[ETH_TX_DESC_CNT] __attribute__((section(.eth_dma), aligned(4))); uint8_t rx_buff[ETH_RX_DESC_CNT][ETH_RX_BUFFER_SIZE] __attribute__((section(.eth_buf), aligned(32))); uint8_t tx_buff[ETH_TX_DESC_CNT][ETH_TX_BUFFER_SIZE] __attribute__((section(.eth_buf), aligned(32)));链接脚本里添加对应的段.eth_dma (NOLOAD) : { . ALIGN(4); *(.eth_dma) } SRAM4 .eth_buf (NOLOAD) : { . ALIGN(32); *(.eth_buf) } SRAM4描述符要求4字节对齐缓冲区我统一32字节对齐这正好和Cache line长度一致。为什么选SRAM4而不是AXI SRAM因为SRAM4的地址范围可以被M7核的MPU配置成Non-cacheable收发时不用每次去手动Clean和Invalidate Cache省一堆隐患。MPU配置示意如下MPU_Region_InitTypeDef mpu {0}; mpu.Enable MPU_REGION_ENABLE; mpu.BaseAddress 0x38000000; mpu.Size MPU_REGION_SIZE_64KB; mpu.TypeExtField MPU_TEX_LEVEL_1; mpu.AccessPermission MPU_REGION_FULL_ACCESS; mpu.DisableExec MPU_INSTRUCTION_ACCESS_DISABLE; mpu.IsShareable MPU_ACCESS_NOT_SHAREABLE; mpu.IsCacheable MPU_ACCESS_NOT_CACHEABLE; mpu.IsBufferable MPU_ACCESS_NOT_BUFFERABLE; HAL_MPU_ConfigRegion(mpu);如果你非要把缓冲放在AXI SRAM里也不是不能跑但收发路径里必须成对做Cache Clean和Invalidate描述符和缓冲一个都不能落。内存紧张的项目我强烈建议直接走Non-cacheable路线少写几十行Cache维护代码少踩一堆随机性死机。4.2 LWIP与ETH驱动的衔接零拷贝发送放进32KB之后默认HAL流程会显得很笨重CPU先把TCP报文复制进TX DMA缓冲DMA再发出去。这一份拷贝占掉不小的TX缓冲。我为了挤出空间把发送改成了零拷贝low_level_output里不让数据进固定发送数组而是让DMA描述符直接指向当前pbuf的payload发送完成中断里再释放这个pbuf。零拷贝有个前提pbuf的payload地址必须满足DMA的4字节对齐。LWIP的PBUF池在PBUF_POOL_BUFSIZE设计时已经做了对齐PBUF_RAM类型也满足MEM_ALIGNMENT所以可以用。如果你开着Cache发送前需要调SCB_CleanDCache_by_Addr描述符的buffer地址也要更新成当前pbuf地址。HAL库默认的HAL_ETH_TransmitFrame会自己拷贝想要零拷贝就得改底层驱动改动量不小。我的建议是时间紧、项目新先用默认拷贝方式跑通把TX DMA缓冲数量从4减到2先省出3KB等整体稳定了再回头改成零拷贝。零拷贝省下的不光是RAM还减少一次大包复制对发送时序更有帮助属于“收益大于操作成本”的优化。4.3 实测数据稳定PING与长期TCP连接按上面这套配置最终LWIP相关内存加ETH DMA缓冲统计下来约29KB留了约3KB余量。测试环境是STM32H743主频480MHzRTL8211F千兆PHYRGMII连接PC机千兆网卡直连。第一轮测试连续ping 1000个包包长1420字节平均延迟0.4ms左右零丢包。第二轮测试板子作为TCP客户端每50ms向PC上位机发一条100字节心跳连续跑26小时TCP连接一次没断。第三轮做压力测试用iperf灌数据吞吐稳定在10Mbps左右再往上会开始丢包。这个结果符合32KB小缓冲的预期——控制类通信完全够用网络存储之类的高吞吐场景就别指望了。测试过程中有个意外发现TCP_WND是从16384调到32768之后接收缓冲区一下吃掉一大片MEM_SIZE整体内存逼近32KB上限。所以每调一个窗口参数都要看一遍mem_stats和memp_stats别凭感觉加。内存优化是总账牵一发而动全身。5. 常见问题与排查技巧实录5.1 链路协商不上、PHY地址不对现象很典型ETH_FLAG_LINK一直不置位HAL_ETH_GetState返回错误。第一步查PHY地址。RTL8211F的地址由硬件引脚决定常见的是0或1CubeMX里填的PHY Address必须和板子实际一致。可以用HAL_ETH_ReadPHYRegister读PHY ID寄存器读出来全是0xFFFF就是地址不对或者PHY根本没上电。另一个坑是RGMII的125MHz参考时钟有的板子需要MCU输出有的需要外部时钟源时钟不对时PHY寄存器都能读通但千兆从不上线只能拿示波器量时钟脚。5.2 接收乱码、CRC错误、偶发丢包这类问题九成出在Cache和内存属性上。如果DMA缓冲放在AXI SRAM又开着Cache接收中断里没有做InvalidateCPU读到的很可能还是旧缓存行。我换成SRAM4并配置Non-cacheable之后问题直接消失。另外描述符的初始化也要注意增强描述符模式下描述符里的保留位必须清零否则DMA状态机可能跑飞。我建议初始化时用memset把所有描述符整体清零再逐字段赋值不要只给用到的字段赋值就完事。5.3 内存池耗尽导致的“不死不活”表现是系统没死机但连续收发大流量后网络线程开始卡住或者出现分配失败日志。这个时候最有效的手段是打开LWIP统计宏lwipopts.h里把LWIP_STATS设为1LWIP_STATS_DISPLAY设为1周期调用stats_display()就能看到pbuf池、heap的当前占用和峰值。我踩过一次很冤枉的坑MEMP_NUM_TCP_SEG设成2结果一次TCP窗口发送就分配不出seg发送队列卡死。调大后马上恢复。这属于内存省过头排查方向要从“哪里够用”调成“哪里不够用”。5.4 我的避坑清单快查版不要把DMA描述符和缓冲放在DTCMETH DMA访问不到。描述符必须4字节对齐缓冲区至少4字节对齐建议32字节对齐。PHY地址、125MHz时钟、PHY供电上电先确认这三样。使用Cache时收发路径要成对做Clean/Invalidate嫌麻烦就改用Non-cacheable内存。每次改动LWIP配置后用stats_display看heap和pbuf池峰值别等死机再猜。TCP_WND、TCP_SND_BUF、MEMP_NUM_TCP_SEG会互相放大内存别单独猛调一个。标准帧按1518字节算PBUF_POOL_BUFSIZE不要低于1560附近硬砍会毁掉协议栈解析。把LWIP从动辄几十KB内存压到32KB最核心的不是背下哪几个参数而是心里永远有一张内存账本哪些是DMA硬开销哪些是并发资源哪些是当前场景根本用不到可以直接关掉的功能。做完这个项目我最大的体会是内存优化永远是一笔总账单独调哪个参数都可能顾此失彼但把它们放在同一张表里取舍就清晰了。最后再分享一个小技巧配置完所有参数后一定多留出2到3KB余量线上问题排查时你会感谢这2KB的。
返回列表