从STM32F407到STM32H743:使用CubeMx实现基于Lwip+LAN8720A+FreeRTOS的以太网数据传输(MPU、D-Cache、I-Cache踩坑指南)
程序跑一会儿就进 HardFault_Handler,PC 类似 0x32F9xxxx(明显是 SRAM),说明在执行被 DMA 写坏的“垃圾代码”。DMA(ETH、SDMMC 等)直接访问 SRAM,看不到 Cache 中尚未写回的修改。CPU 读写数据 → 先在 Cache 里操作,不一定立刻写回 SRAM;DMA 向 SRAM 写 Rx 数据,CPU 还在读旧 Cache;:所有用 DMA
从STM32F407到STM32H743:使用CubeMx实现基于Lwip+LAN8720A+FreeRTOS的以太网数据传输(MPU、D-Cache、I-Cache踩坑指南)
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,把功能跑通
调试阶段,最简单粗暴:
int main(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() 中加计数:
volatile uint32_t eth_rx_cnt = 0;
void HAL_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_t data[65536];
uint32_t rcv_len = 0;
void multicast_receive_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr, u16_t port) {
if (p != NULL) {
uint8_t *payload_ptr = p->payload;
uint32_t data_remaining = p->len;
if(data_remaining > 0) {
rcv_len += data_remaining;
// 执行内存拷贝
memcpy(data,
payload_ptr,
data_remaining);
}
pbuf_free(p);
}
}
// 初始化MULTICAST接收协议控制块
void multicast_receive_init(void) {
struct udp_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);
}
}
在任务开始的位置调用
void StartDefaultTask(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写个上位机测试
import socket
import argparse
import time
import sys
import io
from tqdm import tqdm # 进度条库
import psutil
import os
import threading
# 强制设置输出编码为 UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# 定义默认值
BUFFER_SIZE = 1000 # 每次发送的UDP数据包大小
def set_cpu_affinity(processor_num):
# 获取当前进程
p = psutil.Process(os.getpid())
# 将进程绑定到一个特定的CPU核心(如CPU 0)
p.cpu_affinity([processor_num]) # 绑定到核心0
# 设置进程的优先级为高优先级
p.nice(psutil.HIGH_PRIORITY_CLASS)
def calculate_delay(datarate):
# 计算UDP数据包发送之间的延时
bytes_per_second = datarate * 1000000 / 8 # 将bps转为每秒字节数(每字节8个比特)
packets_per_second = bytes_per_second / BUFFER_SIZE # 每秒可发送的数据包数
return 1 / packets_per_second # 发送每个数据包后的延时(秒)
def busy_wait(delay):
start = time.perf_counter()
while time.perf_counter() - start < delay:
pass
def udp_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:
# 打开要发送的文件
with open(file, 'rb') as f:
# 获取文件大小
file_size = f.seek(0, 2) # 移动到文件末尾,获取文件大小
f.seek(0) # 重置文件指针到开头
print(f"开始从 {local_ip} 发送文件 {file} 到 {multicast_ip}:{multicast_port}")
print(f"按照速率 {datarate} Mbps 发送数据,间隔 {delay:.6f} 秒")
# 初始化进度条
with tqdm(total=file_size, unit='B', unit_scale=True, desc="发送进度") as pbar:
# 读取并发送数据
while True:
data = f.read(BUFFER_SIZE)
if not data:
break
udp_socket.sendto(data, (multicast_ip, multicast_port))
pbar.update(len(data)) # 更新进度条
busy_wait(delay) # 使用忙等待控制发送速率
print("文件发送完成")
except FileNotFoundError:
print(f"文件 {file} 未找到,请检查路径")
finally:
# 关闭套接字
udp_socket.close()
def main():
# 设置命令行参数解析
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亲和力
if args.cpu_affi is not None:
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 + 正确初始化顺序 的配置,大多都能一次性解决。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)