从STM32F407到STM32H743:使用CubeMx实现基于Lwip+LAN8720A+FreeRTOS的以太网数据传输(MPU、D-Cache、I-Cache踩坑指南)
- Chapter0 从STM32F407到STM32H743:使用CubeMx实现基于Lwip+LAN8720A+FreeRTOS的以太网数据传输(MPU、D-Cache、I-Cache踩坑指南)
- 一、H7 架构的不同:缓存机制带来的好处与难点
- 1.1 D-Cache 为什么会搞坏程序?
- 二、如何过渡:从“关 Cache”到“正确使用 Cache”
- 2.1 第一步:先关 D-Cache,把功能跑通
- 2.2 第二步:重新打开 Cache,但只让“安全区域”被缓存
- 三、具体如何配置 STM32CubeMX
- 3.1 STM32CubeMx实操
- 四、调试要点
- 4.1 典型症状:怀疑是 Cache / MPU 问题时
- 4.2 抓包 + 回调计数
- 4.3 分步排除思路
- 4.4 以太网联通测试
- 五、UDP接收测试
- 5.1 测试
- 5.2 代码
- 六、总结
Chapter0 从STM32F407到STM32H743:使用CubeMx实现基于Lwip+LAN8720A+FreeRTOS的以太网数据传输(MPU、D-Cache、I-Cache踩坑指南)
原文链接:https://blog.csdn.net/qq_39432978/article/details/155713794
一、H7 架构的不同:缓存机制带来的好处与难点
在 F1/F4 上写程序,几乎不用管 Cache 和 MPU;
到了 STM32H7,情况完全不同:
- 内核换成 Cortex-M7,带 I-Cache + D-Cache,主频 400MHz/480MHz;
- 片上 RAM 分为 D1 / D2 / D3 域,还有 ITCM/DTCM 等不同速度的存储器;
- ETH、SDMMC、FMC、USB 等大量外设通过 DMA + 总线矩阵 高速访问内存。
设计目标是:在不加外部 RAM 的情况下,把性能榨干。
缺点是:所有用 DMA 的外设都必须考虑 和 D-Cache 的一致性。
1.1 D-Cache 为什么会搞坏程序?
开启 D-Cache 后:
CPU 读写数据 → 先在 Cache 里操作,不一定立刻写回 SRAM;
DMA(ETH、SDMMC 等)直接访问 SRAM,看不到 Cache 中尚未写回的修改。
如果以太网的 DMA 描述符、Rx/Tx 缓冲区 放在可缓存区域:
CPU 写描述符,只写进 Cache;
DMA 看到的仍是旧数据,可能跑飞;
DMA 向 SRAM 写 Rx 数据,CPU 还在读旧 Cache;
各种结构体和返回地址被“写花”,最终 随机 HardFault。
典型现象:
以太网链路灯亮,PC 能识别网卡,但 ping 不通;
Wireshark 只看到 PC 在不停发 ARP,板子一点反应没有;
程序跑一会儿就进 HardFault_Handler,PC 类似 0x32F9xxxx(明显是 SRAM),说明在执行被 DMA 写坏的“垃圾代码”。
二、如何过渡:从“关 Cache”到“正确使用 Cache”
对从 F4/F7 迁移过来的用户,推荐 两步走。
2.1 第一步:先关 D-Cache,把功能跑通
调试阶段,最简单粗暴:
intmain(void){SCB_DisableICache();SCB_DisableDCache();// 先关 D-Cache,保证 DMA 与 CPU 看同一份内存HAL_Init();SystemClock_Config();...}此时:
- 以太网、SD 卡、USB 等 DMA 外设行为接近 F4;
- 不再出现“跑一阵随机 HardFault”的情况;
- 性能略降,但足以支撑大多数应用,适合 先把协议栈和业务逻辑全部打通。
2.2 第二步:重新打开 Cache,但只让“安全区域”被缓存
功能稳定后再考虑性能:
I-Cache 基本可以一直打开,风险很小;
D-Cache 必须配合 MPU:
- 只给普通变量、算法数据等使用的 RAM 开 Cache;
- 所有 DMA 访问的内存(ETH 描述符、Rx/Tx buffer、SD 缓冲区等)
→ 放到 Non-Cacheable 的 MPU 区域。
这样:
对 DMA 外设来说,相当于“关闭 Cache”,可靠;
对 CPU 来说,大部分内存仍可享受 D-Cache 带来的性能提升。
三、具体如何配置 STM32CubeMX
3.1 STM32CubeMx实操
以下以 H743 + LAN8720A + lwIP 为例。
配置RCC
选择TIM7
关键步骤,配置I-Cache和D-Cache,为 ETH DMA 区创建 Non-Cacheable Region
在 CubeMX 里打开System → Cortex-M7 → MPU,增加一个 Region 专门给 ETH DMA 使用的内存,例如:
Region X:ETH DMA 区
- Base Address:0x30044000(按实际工程来)
- Size:32KB(覆盖 0x30044000 ~ 0x30047FFF)
- TEX:level 0
- Access Permission:ALL ACCESS PERMITTED
- Instruction Access:DISABLE
- Shareable:ENABLE
- Cacheable:DISABLE ← 关键
- Bufferable:ENABLE
这就把 0x30044000 ~ 0x30047FFF标记为 Non-Cacheable,
ETH DMA 与 CPU 访问这块内存时不会被 D-Cache 影响。
配置以太网
记得打开中断
注意这里要添加以太网PHY的复位引脚,否则无法正常工作,默认输出为高即可,注意要和实际使用的引脚对应
配置LWIP,我这里选择关闭DHCP
注意这里的地址一定要对上(被坑过)
这里选择只有LAN8742,他和LAN8720A是通用的。ST默认只支持Microchip的,需要DS83484可以手动更改
FreeRTOS选择CMSIS_V2版本,裸机跑也可以,但是注意裸机需要手动在while(1)中手动调用LWIP_Process()这个函数,使用RTOS不需要手动干预
时钟这里选择400MHz
其它默认即可,最后生成代码
MPU配置总结
确保在 CubeMX 的 ETH 配置 中设置(示例):
- Tx Descriptor Length:4
- First Tx Descriptor Address:0x30044060
- Rx Descriptor Length:4
- First Rx Descriptor Address:0x30044000
- Rx Buffers Address:0x30044200
- Rx Buffers Length:1536
- ETH_RX_BUFFER_CNT(LwIP 配置):例如 12
只要保证这些地址全部落在同一块 D2 区域即可。
四、调试要点
4.1 典型症状:怀疑是 Cache / MPU 问题时
链路灯和网卡都正常,但 ping 不通;
Wireshark 中只看到 PC 发 ARP:
Who has 192.168.x.x? Tell 192.168.x.1
运行一段时间后进入 HardFault_Handler,PC 落在类似 0x32F9xxxx 这样的 SRAM 地址上。
此时可以先做一个实验:
SCB_DisableICache();SCB_DisableDCache();若立刻恢复正常,基本可以确认是 Cache/MPU 的问题。
4.2 抓包 + 回调计数
- 用 Wireshark 看 ARP:
只看到请求,看不到响应 → 板子没有回包; - 在 HAL_ETH_RxCpltCallback() 中加计数:
volatileuint32_teth_rx_cnt=0;voidHAL_ETH_RxCpltCallback(ETH_HandleTypeDef*heth){eth_rx_cnt++;}= eth_rx_cnt == 0:说明 MAC 没收到帧,问题在更底层(时钟、GPIO、HAL_ETH_Start、描述符等);
- eth_rx_cnt 在增加但 ARP 仍无响应:说明 lwIP 没有正常处理(可能是 ethernetif_input / sys_check_timeouts 没跑)。
4.3 分步排除思路
先关 D-Cache 验证是否为 Cache 问题;
检查 ETH 描述符和缓冲区地址/长度是否匹配;
确认这些地址全部包含在 Non-Cacheable 的 MPU Region 中;
确认 HAL_ETH_Start() 返回 HAL_OK;
确认主循环或任务中持续调用:
- 裸机:MX_LWIP_Process()
= RTOS:ethernetif_input(&gnetif); + sys_check_timeouts();
4.4 以太网联通测试
五、UDP接收测试
5.1 测试
在freertos.c文件中添加如下代码
uint8_tdata[65536];uint32_trcv_len=0;voidmulticast_receive_callback(void*arg,structudp_pcb*pcb,structpbuf*p,constip_addr_t*addr,u16_tport){if(p!=NULL){uint8_t*payload_ptr=p->payload;uint32_tdata_remaining=p->len;if(data_remaining>0){rcv_len+=data_remaining;// 执行内存拷贝memcpy(data,payload_ptr,data_remaining);}pbuf_free(p);}}// 初始化MULTICAST接收协议控制块voidmulticast_receive_init(void){structudp_pcb*pcb;pcb=udp_new();pcb->so_options|=SOF_REUSEADDR;// 允许地址重用if(pcb!=NULL){udp_bind(pcb,IP4_ADDR_ANY,5007);udp_recv(pcb,multicast_receive_callback,NULL);}}在任务开始的位置调用
voidStartDefaultTask(void*argument){/* init code for LWIP */MX_LWIP_Init();/* USER CODE BEGIN StartDefaultTask */multicast_receive_init();/* Infinite loop */for(;;){osDelay(1);}/* USER CODE END StartDefaultTask */}用python写个上位机测试
importsocketimportargparseimporttimeimportsysimportiofromtqdmimporttqdm# 进度条库importpsutilimportosimportthreading# 强制设置输出编码为 UTF-8sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8')# 定义默认值BUFFER_SIZE=1000# 每次发送的UDP数据包大小defset_cpu_affinity(processor_num):# 获取当前进程p=psutil.Process(os.getpid())# 将进程绑定到一个特定的CPU核心(如CPU 0)p.cpu_affinity([processor_num])# 绑定到核心0# 设置进程的优先级为高优先级p.nice(psutil.HIGH_PRIORITY_CLASS)defcalculate_delay(datarate):# 计算UDP数据包发送之间的延时bytes_per_second=datarate*1000000/8# 将bps转为每秒字节数(每字节8个比特)packets_per_second=bytes_per_second/BUFFER_SIZE# 每秒可发送的数据包数return1/packets_per_second# 发送每个数据包后的延时(秒)defbusy_wait(delay):start=time.perf_counter()whiletime.perf_counter()-start<delay:passdefudp_send_data(file,local_ip,multicast_ip,multicast_port,datarate):# 创建UDP套接字udp_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)# 强制绑定到物理网卡(示例地址需替换为实际值)udp_socket.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_IF,socket.inet_aton(local_ip)# 关键修改点[3](@ref))# 计算发送数据包之间的延时,防止超过串口速率delay=calculate_delay(datarate)try:# 打开要发送的文件withopen(file,'rb')asf:# 获取文件大小file_size=f.seek(0,2)# 移动到文件末尾,获取文件大小f.seek(0)# 重置文件指针到开头print(f"开始从{local_ip}发送文件{file}到{multicast_ip}:{multicast_port}")print(f"按照速率{datarate}Mbps 发送数据,间隔{delay:.6f}秒")# 初始化进度条withtqdm(total=file_size,unit='B',unit_scale=True,desc="发送进度")aspbar:# 读取并发送数据whileTrue:data=f.read(BUFFER_SIZE)ifnotdata:breakudp_socket.sendto(data,(multicast_ip,multicast_port))pbar.update(len(data))# 更新进度条busy_wait(delay)# 使用忙等待控制发送速率print("文件发送完成")exceptFileNotFoundError:print(f"文件{file}未找到,请检查路径")finally:# 关闭套接字udp_socket.close()defmain():# 设置命令行参数解析parser=argparse.ArgumentParser(description="UDP 数据发送工具")parser.add_argument("--file",default='test1G.dat',help="要发送的文件名")parser.add_argument("--local_ip",default='192.168.137.1',help="目标 IP 地址")parser.add_argument("--multicast_ip",default='192.168.137.10',help="目标 IP 地址")parser.add_argument("--multicast_port",type=int,default=5007,help="目标端口")parser.add_argument("--datarate",type=int,default=2,help="UDP发送速率 (Mbps)")parser.add_argument("--cpu_affi",type=int,default=100,help="指定CPU亲和力的CPU编号")# 解析命令行参数args=parser.parse_args()# 设置CPU亲和力ifargs.cpu_affiisnotNone:set_cpu_affinity(args.cpu_affi)print(f"正在使用CPU核心:{args.cpu_affi}运行任务")else:print("未指定CPU亲和力")# 解析命令行参数args=parser.parse_args()# 调用发送函数udp_send_data(args.file,args.local_ip,args.multicast_ip,args.multicast_port,args.datarate)if__name__=="__main__":main()开始测试,可以看到几乎打满了百兆网
IAR中打开Live Watch,可以看到接收到的数据刚好等于1GB,没有丢包,此时H7的主频为400MHz
根据之前的测试经验,F4只能跑到大约20Mbps,H7的上限确实比F4要高很多。
为了验证H7的架构优势,这里特地将H7的主频设置到170MHz,与F4的168MHz基本保持一致,再次进行测试,实测可以跑到70Mbps左右,可见同主频下H7相比F4确实有架构优势。
进一步的,在同样主频下,关掉D-Cache和I-Cache之后再测一遍,发现只能跑到40Mbps左右,可见优势很大一部分来源于指令和数据缓存机制,所以这部分功能尽量用起来,它是区别于传统MCU的核心
5.2 代码
https://gitee.com/dwgan/stm32h743
六、总结
STM32H7 相比 F4/F7,最大的变化就是引入了 I-Cache / D-Cache 和更复杂的内存体系。
它极大提升了性能,但也让 DMA 外设变得“更脆弱”,必须正确处理缓存一致性。推荐迁移路径:
1. 第一阶段:关闭 D-Cache,只打开 I-Cache,把所有功能跑通;
2. 第二阶段:使用 MPU 把所有 DMA 内存标成 Non-Cacheable,再重新打开 D-Cache。
在 CubeMX 中:
把 ETH 的描述符和缓冲区放在 D2 SRAM 的一块连续区域;
为这块区域配置 Non-Cacheable 的 MPU Region;
main() 中先 MPU_Config() 再 EnableICache/EnableDCache。调试时,多利用:
Wireshark 抓包看 ARP 是否有响应;
中断回调计数看是否收到数据;
HardFault 时的 PC 地址判断是否被 DMA 写坏。
如果你也在 H7 上被以太网/SD 卡等外设折磨过,不妨先试试 关 D-Cache。
如果问题瞬间消失,那大概率就是 Cache/MPU 的“锅”,
按上文这套Non-Cacheable + MPU + 正确初始化顺序的配置,大多都能一次性解决。