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,但只让“安全区域”被缓存

功能稳定后再考虑性能:

  1. I-Cache 基本可以一直打开,风险很小;

  2. 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 分步排除思路

  1. 先关 D-Cache 验证是否为 Cache 问题;

  2. 检查 ETH 描述符和缓冲区地址/长度是否匹配;

  3. 确认这些地址全部包含在 Non-Cacheable 的 MPU Region 中;

  4. 确认 HAL_ETH_Start() 返回 HAL_OK;

  5. 确认主循环或任务中持续调用:

  • 裸机: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 + 正确初始化顺序 的配置,大多都能一次性解决。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐