核心内容摘要
探索“大车”的无限可能:一段关于力量与温情的视觉盛宴
从STM32F407到STM32H743使用CubeMx实现基于LwipLAN8720AFreeRTOS的以太网数据传输MPU、D-Cache、I-Cache踩坑指南Chapter0 从STM32F407到STM32H743使用CubeMx实现基于LwipLAN8720AFreeRTOS的以太网数据传输MPU、D-Cache、I-Cache踩坑指南
H7 架构的不同缓存机制带来的好处与难点
1 D-Cache 为什么会搞坏程序
如何过渡从“关 Cache”到“正确使用 Cache”
1 第一步先关 D-Cache把功能跑通
2 第二步重新打开 Cache但只让“安全区域”被缓存
具体如何配置 STM32CubeMX
1 STM32CubeMx实操
调试要点
1 典型症状怀疑是 Cache / MPU 问题时
2 抓包 回调计数
3 分步排除思路
4 以太网联通测试
UDP接收测试
1 测试
2 代码
六、
总结Chapter0 从STM32F407到STM32H743使用CubeMx实现基于LwipLAN8720AFreeRTOS的以太网数据传输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 D-Cache 为什么会搞坏程序开启 D-Cache 后CPU 读写数据 → 先在 Cache 里操作不一定立刻写回 SRAMDMAETH、SDMMC 等直接访问 SRAM看不到 Cache 中尚未写回的修改。
如果以太网的 DMA 描述符、Rx/Tx 缓冲区 放在可缓存区域CPU 写描述符只写进 CacheDMA 看到的仍是旧数据可能跑飞DMA 向 SRAM 写 Rx 数据CPU 还在读旧 Cache各种结构体和返回地址被“写花”最终 随机 HardFault。
典型现象以太网链路灯亮PC 能识别网卡但 ping 不通Wireshark 只看到 PC 在不停发 ARP板子一点反应没有程序跑一会儿就进 HardFault_HandlerPC 类似 0x32F9xxxx明显是 SRAM说明在执行被 DMA 写坏的“垃圾代码”。
如何过渡从“关 Cache”到“正确使用 Cache”对从 F4/F7 迁移过来的用户推荐 两步走。
1 第一步先关 D-Cache把功能跑通调试阶段最简单粗暴intmain(void){SCB_DisableICache();SCB_DisableDCache();// 先关 D-Cache保证 DMA 与 CPU 看同一份内存HAL_Init();SystemClock_Config();...}此时- 以太网、SD 卡、USB 等 DMA 外设行为接近 F4- 不再出现“跑一阵随机 HardFault”的情况- 性能略降但足以支撑大多数应用适合 先把协议栈和业务逻辑全部打通。
2 第二步重新打开 Cache但只让“安全区域”被缓存功能稳定后再考虑性能I-Cache 基本可以一直打开风险很小D-Cache 必须配合 MPU只给普通变量、算法数据等使用的 RAM 开 Cache所有 DMA 访问的内存ETH 描述符、Rx/Tx buffer、SD 缓冲区等→ 放到 Non-Cacheable 的 MPU 区域。
这样对 DMA 外设来说相当于“关闭 Cache”可靠对 CPU 来说大部分内存仍可享受 D-Cache 带来的性能提升。
具体如何配置 STM32CubeMX
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 XETH DMA 区Base Address0x30044000按实际工程来Size32KB覆盖 0x30044000 ~ 0x30047FFFTEXlevel 0Access PermissionALL ACCESS PERMITTEDInstruction AccessDISABLEShareableENABLECacheableDISABLE ← 关键BufferableENABLE这就把 0x30044000 ~ 0x30047FFF标记为 Non-CacheableETH DMA 与 CPU 访问这块内存时不会被 D-Cache 影响。
配置以太网记得打开中断注意这里要添加以太网PHY的复位引脚否则无法正常工作默认输出为高即可注意要和实际使用的引脚对应配置LWIP我这里选择关闭DHCP注意这里的地址一定要对上被坑过这里选择只有LAN8742他和LAN8720A是通用的。
ST默认只支持Microchip的需要DS83484可以手动更改FreeRTOS选择CMSIS_V2版本裸机跑也可以但是注意裸机需要手动在while(
中手动调用LWIP_Process()这个函数使用RTOS不需要手动干预时钟这里选择400MHz其它默认即可最后生成代码MPU配置
总结确保在 CubeMX 的 ETH 配置 中设置示例Tx Descriptor Length4First Tx Descriptor Address0x30044060Rx Descriptor Length4First Rx Descriptor Address0x30044000Rx Buffers Address0x30044200Rx Buffers Length1536ETH_RX_BUFFER_CNTLwIP 配置例如 12只要保证这些地址全部落在同一块 D2 区域即可。
调试要点
1 典型症状怀疑是 Cache / MPU 问题时链路灯和网卡都正常但 ping 不通Wireshark 中只看到 PC 发 ARPWho has
192.
x.x? Tell
192.
x.1运行一段时间后进入 HardFault_HandlerPC 落在类似 0x32F9xxxx 这样的 SRAM 地址上。
此时可以先做一个实验SCB_DisableICache();SCB_DisableDCache();若立刻恢复正常基本可以确认是 Cache/MPU 的问题。
2 抓包 回调计数用 Wireshark 看 ARP只看到请求看不到响应 → 板子没有回包在 HAL_ETH_RxCpltCallback() 中加计数volatileuint32_teth_rx_cnt0;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 没跑。
3 分步排除思路先关 D-Cache 验证是否为 Cache 问题检查 ETH 描述符和缓冲区地址/长度是否匹配确认这些地址全部包含在 Non-Cacheable 的 MPU Region 中确认 HAL_ETH_Start() 返回 HAL_OK确认主循环或任务中持续调用裸机MX_LWIP_Process() RTOSethernetif_input(gnetif); sys_check_timeouts();
4 以太网联通测试
UDP接收测试
1 测试在freertos.c文件中添加如下代码uint8_tdata[65536];uint32_trcv_len0;voidmulticast_receive_callback(void*arg,structudp_pcb*pcb,structpbuf*p,constip_addr_t*addr,u16_tport){if(p!NULL){uint8_t*payload_ptrp-payload;uint32_tdata_remainingp-len;if(data_remaining
{rcv_lendata_remaining;// 执行内存拷贝memcpy(data,payload_ptr,data_remaining);}pbuf_free(p);}}// 初始化MULTICAST接收协议控制块voidmulticast_receive_init(void){structudp_pcb*pcb;pcbudp_new();pcb-so_options|SOF_REUSEADDR;// 允许地址重用if(pcb!NULL){udp_bind(pcb,IP4_ADDR_ANY,
;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(
;}/* USER CODE END StartDefaultTask */}用python写个上位机测试importsocketimportargparseimporttimeimportsysimportiofromtqdmimporttqdm# 进度条库importpsutilimportosimportthreading# 强制设置输出编码为 UTF-8sys.stdoutio.TextIOWrapper(sys.stdout.buffer,encodingutf-
# 定义默认值BUFFER_SIZE1000# 每次发送的UDP数据包大小defset_cpu_affinity(processor_num):# 获取当前进程ppsutil.Process(os.getpid())# 将进程绑定到一个特定的CPU核心如CPU 0p.cpu_affinity([processor_num])# 绑定到核心0# 设置进程的优先级为高优先级p.nice(psutil.HIGH_PRIORITY_CLASS)defcalculate_delay(datarate):# 计算UDP数据包发送之间的延时bytes_per_seconddatarate*1000000/8# 将bps转为每秒字节数每字节8个比特packets_per_secondbytes_per_second/BUFFER_SIZE# 每秒可发送的数据包数return1/packets_per_second# 发送每个数据包后的延时秒defbusy_wait(delay):starttime.perf_counter()whiletime.perf_counter()-startdelay:passdefudp_send_data(file,local_ip,multicast_ip,multicast_port,datarate):# 创建UDP套接字udp_socketsocket.socket(socket.AF_INET,socket.SOCK_DGRAM)# 强制绑定到物理网卡示例地址需替换为实际值udp_socket.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_IF,socket.inet_aton(local_ip)# 关键修改点[3](ref))# 计算发送数据包之间的延时防止超过串口速率delaycalculate_delay(datarate)try:# 打开要发送的文件withopen(file,rb)asf:# 获取文件大小file_sizef.seek(0,
# 移动到文件末尾获取文件大小f.seek(
# 重置文件指针到开头print(f开始从{local_ip}发送文件{file}到{multicast_ip}:{multicast_port})print(f按照速率{datarate}Mbps 发送数据间隔{delay:.6f}秒)# 初始化进度条withtqdm(totalfile_size,unitB,unit_scaleTrue,desc发送进度)aspbar:# 读取并发送数据whileTrue:dataf.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():# 设置命令行参数解析parserargparse.ArgumentParser(descriptionUDP 数据发送工具)parser.add_argument(--file,defaulttest1G.dat,help要发送的文件名)parser.add_argument(--local_ip,default
192.
168.
1
1,help目标 IP 地址)parser.add_argument(--multicast_ip,default
192.
168.
1
10,help目标 IP 地址)parser.add_argument(--multicast_port,typeint,default5007,help目标端口)parser.add_argument(--datarate,typeint,default2,helpUDP发送速率 (Mbps))parser.add_argument(--cpu_affi,typeint,default100,help指定CPU亲和力的CPU编号)# 解析命令行参数argsparser.parse_args()# 设置CPU亲和力ifargs.cpu_affiisnotNone:set_cpu_affinity(args.cpu_affi)print(f正在使用CPU核心{args.cpu_affi}运行任务)else:print(未指定CPU亲和力)# 解析命令行参数argsparser.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只能跑到大约20MbpsH7的上限确实比F4要高很多。
为了验证H7的架构优势这里特地将H7的主频设置到170MHz与F4的168MHz基本保持一致再次进行测试实测可以跑到70Mbps左右可见同主频下H7相比F4确实有架构优势。
进一步的在同样主频下关掉D-Cache和I-Cache之后再测一遍发现只能跑到40Mbps左右可见优势很大一部分来源于指令和数据缓存机制所以这部分功能尽量用起来它是区别于传统MCU的核心
2 代码https://gitee.com/dwgan/stm32h743
六、
总结STM32H7 相比 F4/F7最大的变化就是引入了 I-Cache / D-Cache 和更复杂的内存体系。
它极大提升了性能但也让 DMA 外设变得“更脆弱”必须正确处理缓存一致性。
推荐迁移路径
第一阶段关闭 D-Cache只打开 I-Cache把所有功能跑通
第二阶段使用 MPU 把所有 DMA 内存标成 Non-Cacheable再重新打开 D-Cache。
在 CubeMX 中把 ETH 的描述符和缓冲区放在 D2 SRAM 的一块连续区域为这块区域配置 Non-Cacheable 的 MPU Regionmain() 中先 MPU_Config() 再 EnableICache/EnableDCache。
调试时多利用Wireshark 抓包看 ARP 是否有响应中断回调计数看是否收到数据HardFault 时的 PC 地址判断是否被 DMA 写坏。
如果你也在 H7 上被以太网/SD 卡等外设折磨过不妨先试试 关 D-Cache。
如果问题瞬间消失那大概率就是 Cache/MPU 的“锅”按上文这套Non-Cacheable MPU 正确初始化顺序的配置大多都能一次性解决。