STM32H743移植MicroPython:外扩32M QSPI Flash与SDRAM实战
简介这份源码面向使用STM32H743开发板并希望运行MicroPython的嵌入式开发者重点解决官方移植未覆盖外扩32M SQPI Flash与32M SDRAM的板级适配问题。资源包共5个文件约4KB包含2个h头文件、1个c源文件、1个mk编译配置和1个csv引脚映射表分别承担板级初始化、外设配置、编译参数与引脚定义等职责放入micropython/ports/stm32/boards/对应目录后即可参与固件编译。目前已有1363人学习下载说明该移植方案在同类开发板中具有一定参考价值。读者可据此获得一份可直接复用的板级支持包理解SQPI Flash与SDRAM在MicroPython下的初始化流程、内存映射与引脚配置思路并对照自身硬件快速完成固件构建与验证减少从零适配的时间成本。1. 从一块 H743 开发板说起为什么我要把 MicroPython 塞进 32M 外扩存储的板子手头这块 STM32H743 开发板主频 480MHz片上 1MB SRAM、2MB Flash跑裸机或 RTOS 都绰绰有余。但真正让我动念头移植 MicroPython 的是它板载的那两颗外扩芯片一颗 32MB 的 SQPI Flash一颗 32MB 的 SDRAM。片上 2MB Flash 装个 MicroPython 固件都紧巴巴更别说往里塞 Python 脚本和文件系统而 32MB SQPI Flash 正好拿来当只读文件系统存 .py 和资源32MB SDRAM 则能给 MicroPython 的堆和帧缓冲提供大块连续内存。这套组合在跑图像处理、音频缓冲、轻量 AI 推理这类吃内存的场景时比片上资源宽裕太多。这篇笔记拆的就是这份「STM32H743 移植 MicroPython 外扩 SQPI Flash SDRAM」的源码包从工程结构、内存映射、驱动适配一路讲到编译烧录和排错。适合已经能点亮 H743、想上 MicroPython 又不想被内存卡脖子的嵌入式从业者新手照着步骤也能走通熟手可以直接看内存布局和 QSPI 时序那几段。2. 工程结构与内存映射先搞清楚 32M 外扩到底挂在哪条总线上拿到源码包别急着编译先把目录和链接脚本读一遍。H743 的存储控制器和外设映射决定了 MicroPython 的堆、文件系统、代码段各自能落在哪这一步搞错后面不是编译报错就是运行到一半 HardFault。2.1 源码包目录拆解与关键文件职责这份工程常见做法是沿用 MicroPython 官方 ports/stm32 的骨架再叠加板级目录。典型结构如下stm32h743-micropython/ ├── ports/stm32/ # MicroPython 官方 STM32 移植层 │ ├── boards/ │ │ └── MYH743/ # 板级目录名字按自己板子改 │ │ ├── mpconfigboard.mk # 板级编译开关 │ │ ├── mpconfigboard.h # 时钟、引脚、堆大小宏 │ │ ├── pins.csv # 引脚名到端口的映射 │ │ └── stm32h7xx_hal_conf.h │ ├── qspi.c / qspi.h # QSPI Flash 驱动 │ ├── sdram.c / sdram.h # SDRAM 初始化 │ ├── main.c # 入口堆和文件系统在这里挂载 │ └── ... ├── lib/ │ ├── stm32lib/ # ST 的 HAL 与 CMSIS │ └── ... ├── mpy-cross/ # 交叉编译器编译 .py 到 .mpy └── Makefilempconfigboard.mk决定这块板子编哪些功能比如MICROPY_HW_ENABLE_SDCARD、MICROPY_HW_QSPI这类开关。mpconfigboard.h里最关键的是MICROPY_HW_MCU_NAME、晶振频率、以及堆的起止地址宏。main.c里会调用qspi_init()和sdram_init()再把 SQPI Flash 注册成块设备给 littlefs 用。读这三处基本就能判断这份源码是不是按你的板子改的。2.2 H743 内存映射SDRAM 与 QSPI 的地址窗口STM32H743 的外扩存储不是随便映射的FMC/SDRAM 和 QUADSPI 各有固定地址窗口。常见配置是存储控制器典型映射地址用途SDRAMFMC (Bank1/Bank2)0xC0000000 起MicroPython 堆、帧缓冲SQPI FlashQUADSPI0x90000000 起只读文件系统、.mpy片上 DTCM—0x20000000栈、关键变量片上 AXI SRAM—0x24000000普通堆/数据SDRAM 挂在 FMC 上地址从 0xC0000000 开始32MB 就是 0xC0000000~0xC1FFFFFF。QUADSPI 的内存映射模式把 Flash 映射到 0x90000000CPU 可以直接按地址读但写要走命令。MicroPython 的堆一般放在 SDRAM因为片上 SRAM 不够 Python 对象挥霍文件系统放 QSPI Flash掉电不丢。2.3 链接脚本与堆区划分链接脚本.ld要显式把堆区指到 SDRAM。常见做法是在mpconfigboard.h里定义// SDRAM 映射基址与大小 #define MICROPY_HW_SDRAM_BASE (0xC0000000UL) #define MICROPY_HW_SDRAM_SIZE (32 * 1024 * 1024UL) // MicroPython GC 堆从 SDRAM 划一块出来 #define MICROPY_HEAP_START (MICROPY_HW_SDRAM_BASE) #define MICROPY_HEAP_SIZE (24 * 1024 * 1024UL)这里留了 24MB 给 GC 堆剩下 8MB 给帧缓冲或大块 DMA 缓冲。参数说明MICROPY_HEAP_SIZE不是越大越好堆太大 GC 扫描一轮耗时长实时性会掉一般按实际脚本内存峰值留 1.5~2 倍余量。MICROPY_HEAP_START必须 8 字节对齐SDRAM 基址天然满足。改完这两个宏main.c里gc_init()会拿它们初始化堆。提示SDRAM 初始化必须在gc_init()之前完成否则堆指针指向一片没配好的内存一上电就 HardFault。3. QSPI Flash 与 SDRAM 驱动适配时序参数和初始化顺序是命门外扩存储能不能用八成看驱动初始化和时序。H743 的 QUADSPI 和 FMC 配置项多参数错一个就表现为读回全 0xFF 或者随机翻车。这一章把两个驱动的关键配置和验证方法讲透。3.1 QUADSPI 初始化与内存映射模式SQPI Flash 常见型号支持 1-4-4 模式命令 1 线、地址 4 线、数据 4 线。初始化分两步先配 QUADSPI 外设参数再发命令让 Flash 进入 QPI 或内存映射模式。// qspi.c 关键片段 void qspi_init(void) { // 1. 时钟使能QUADSPI 挂在 AHB3别忘了开 __HAL_RCC_QSPI_CLK_ENABLE(); // 2. 配置 QUADSPI时钟分频、采样沿、片选高电平时间 hqspi.Instance QUADSPI; hqspi.Init.ClockPrescaler 2; // 内核时钟/3按 Flash 最高频率调 hqspi.Init.FifoThreshold 4; hqspi.Init.SampleShifting QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize 25; // 2^25 32MB hqspi.Init.ChipSelectHighTime QSPI_CS_HIGH_TIME_4_CYCLE; hqspi.Init.ClockMode QSPI_CLOCK_MODE_0; HAL_QSPI_Init(hqspi); // 3. 发命令让 Flash 进入 4 线模式具体命令看 Flash 手册 qspi_write_enable(); qspi_enter_quad_mode(); }参数说明ClockPrescaler决定 QSPI 时钟H743 的 QUADSPI 时钟源常见 200MHz 左右分频后别超过 Flash 手册标称的最高频率很多 SQPI Flash 是 104MHz 或 133MHz。FlashSize 25表示 2 的 25 次方字节即 32MB这个值填错会导致地址越界读到回绕数据。SampleShifting在高速下建议开半周期采样否则读数据容易错位。内存映射模式让 Flash 内容直接出现在 0x90000000CPU 按地址读即可但要注意映射模式下不能直接写写操作得退出映射、发写命令、再进映射。3.2 SDRAM 的 FMC 时序配置SDRAM 初始化比 QSPI 更啰嗦FMC 要配 bank、行列地址位宽、刷新周期、以及一堆时序参数。常见 32MB SDRAM 是 4 bank × 13 行 × 9 列 × 16 位。// sdram.c 关键片段 void sdram_init(void) { FMC_SDRAM_TimingTypeDef timing {0}; // 时序参数单位是 FMC 时钟周期数必须查 SDRAM 手册 timing.LoadToActiveDelay 2; // tRCD timing.ExitSelfRefreshDelay 7; // tXSR timing.SelfRefreshTime 4; // tRAS timing.RowCycleDelay 7; // tRC timing.WriteRecoveryTime 2; // tWR timing.RPDelay 2; // tRP timing.RCDDelay 2; // tRCD // 命令时钟配置使能、预充电、刷新、加载模式寄存器 FMC_SDRAM_SendCommand(hsdram, cmd, 0xFFFF); // ... 依次发预充电、8 次刷新、模式寄存器设置 }参数说明这些时序值不是拍脑袋填的要拿 SDRAM 手册里的纳秒值除以 FMC 时钟周期再向上取整。填小了读写不稳定填大了性能掉但通常还能跑。RowCycleDelay和SelfRefreshTime是最容易填错的两个翻车现象是跑几分钟后随机死机。3.3 上电验证用内存测试脚本确认两块存储都活着驱动写完别急着上 MicroPython先用一段裸机测试确认读写正常。// 简单 SDRAM 读写测试 #define TEST_ADDR (MICROPY_HW_SDRAM_BASE 0x1000) uint32_t *p (uint32_t *)TEST_ADDR; for (int i 0; i 1024; i) p[i] 0xA5A50000 | i; for (int i 0; i 1024; i) { if (p[i] ! (0xA5A50000 | i)) { printf(SDRAM fail %d\n, i); break; } }QSPI Flash 则用擦除-写入-读回验证注意先擦扇区再写。两块都过了再进 MicroPython 移植能省掉大量「到底是驱动还是解释器」的扯皮。注意SDRAM 测试地址别从 0xC0000000 开始前几 KB 可能被堆或向量表占用从偏移 0x1000 起更安全。4. MicroPython 移植与文件系统挂载让 Python 脚本住进 32M Flash驱动通了接下来是把 MicroPython 的堆、文件系统、模块搜索路径接到外扩存储上。这一步决定你写 Python 脚本时能不能import到 Flash 里的模块。4.1 板级配置mpconfigboard.h 与 mpconfigboard.mk板级目录里两个文件控制编译行为。mpconfigboard.mk里常见开关# 启用 QSPI 和 SDRAM 支持 MICROPY_HW_ENABLE_QSPI 1 MICROPY_HW_ENABLE_SDRAM 1 # 文件系统类型 MICROPY_HW_LFS1 1 # 堆大小字节 MICROPY_HW_HEAP_SIZE 25165824mpconfigboard.h里定义引脚和堆地址#define MICROPY_HW_MCU_NAME STM32H743 #define MICROPY_HW_XTAL_FREQ (25000000) #define MICROPY_HW_HEAP_START (0xC0000000) #define MICROPY_HW_HEAP_SIZE (24 * 1024 * 1024)参数说明MICROPY_HW_HEAP_SIZE和前面MICROPY_HEAP_SIZE要一致否则链接和运行会对不上。MICROPY_HW_LFS1表示用 littlefs 挂第一个块设备通常就是 QSPI Flash。4.2 把 QSPI Flash 注册成 littlefs 块设备MicroPython 的main.c里要构造一个块设备结构体把 QSPI 的读、写、擦除函数指针填进去再调lfs2_mount。// 块设备操作读、写、擦除 static int qspi_bdev_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buf, lfs_size_t size) { uint32_t addr block * c-block_size off; qspi_read(addr, buf, size); // 走 QUADSPI 读 return 0; } static int qspi_bdev_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buf, lfs_size_t size) { uint32_t addr block * c-block_size off; qspi_write(addr, buf, size); // 先擦后写内部处理 return 0; } static int qspi_bdev_erase(const struct lfs_config *c, lfs_block_t block) { qspi_erase(block * c-block_size, c-block_size); return 0; } // 配置块大小、块数量按 Flash 实际参数填 struct lfs_config qspi_lfs_cfg { .read qspi_bdev_read, .prog qspi_bdev_prog, .erase qspi_bdev_erase, .block_size 4096, // 擦除扇区 4KB .block_count 8192, // 32MB / 4KB .cache_size 256, .lookahead_size 32, };参数说明block_size必须等于 Flash 的最小擦除单位多数 SQPI Flash 是 4KB。block_count是总容量除以块大小32MB/4KB8192。cache_size影响读写性能太小会频繁调用底层太大会吃 RAM256 是常见折中。lookahead_size给 littlefs 做块分配位图32 字节够用。4.3 挂载文件系统与模块搜索路径挂载完成后把 Flash 挂到/flash并把/flash加进sys.path这样import才能找到里面的 .py 或 .mpy。// main.c 里挂载 lfs_t lfs; int err lfs2_mount(lfs, qspi_lfs_cfg); if (err) { // 首次挂载失败通常是没格式化格式化后再挂 lfs2_format(lfs, qspi_lfs_cfg); lfs2_mount(lfs, qspi_lfs_cfg); } // 注册到 MicroPython 的 VFS mp_vfs_mount(/flash, vfs_lfs, ...);挂载后进 REPL 验证import os os.listdir(/flash) # 应该能看到文件 with open(/flash/test.py, w) as f: f.write(print(hello from qspi)) import test # 能 import 说明 sys.path 生效如果os.listdir报错先确认块设备读写测试过没过如果import找不到检查sys.path里有没有/flash。提示首次上电 Flash 是空的littlefs 挂载会失败代码里要处理「挂载失败就格式化」的逻辑否则每次都要手动擦。5. 编译、烧录与避坑那些让我熬夜的 QSPI 和堆问题工程配好编译烧录这一路照样有坑。这一章按「现象 → 原因 → 解决」列几条我实际踩过的都是能复现的。5.1 编译报错QSPI 相关符号未定义现象undefined reference to qspi_init或HAL_QSPI_Init找不到。原因mpconfigboard.mk里开了MICROPY_HW_ENABLE_QSPI但qspi.c没被加进编译源列表或者 STM32 HAL 的 QSPI 模块没在stm32h7xx_hal_conf.h里使能。解决检查 Makefile 的SRC_C是否包含qspi.c并在stm32h7xx_hal_conf.h里把HAL_QSPI_MODULE_ENABLED打开。5.2 运行 HardFault堆指针指向未初始化的 SDRAM现象上电后串口打印几行就 HardFault或者gc_init直接挂。原因main.c里gc_init()在sdram_init()之前调用了堆指针指向 0xC0000000 但 FMC 还没配好一访问就异常。解决把sdram_init()提到gc_init()之前顺序是「时钟 → SDRAM → QSPI → gc_init → 文件系统」。5.3 QSPI 读回全 0xFF 或随机值现象内存映射模式下读 0x90000000 全是 0xFF或者数据偶尔错。原因三种可能——ClockPrescaler分频太小导致 QSPI 时钟超过 Flash 上限SampleShifting没开高速下采样点落在数据跳变沿Flash 没进 4 线模式还在 1 线模式但代码按 4 线读。解决先把分频调大降速验证能读通再逐步提速开半周期采样确认qspi_enter_quad_mode()发对了命令。5.4 littlefs 挂载失败或写入报错现象lfs2_mount返回负值或写文件时报LFS_ERR_CORRUPT。原因block_size和 Flash 实际擦除单位不一致或者block_count填大了超出 32MB。解决查 Flash 手册确认擦除扇区大小block_count严格按容量除以块大小算别多填。首次挂载失败先格式化。5.5 堆够大但脚本仍报内存不足现象MemoryError但gc.mem_free()显示还有十几 MB。原因MicroPython 的 GC 堆是连续的如果 SDRAM 里堆区和帧缓冲、DMA 缓冲地址交叠实际可用连续块被切碎。解决在mpconfigboard.h里把堆区、帧缓冲区、DMA 区显式分段别让它们抢同一段地址用gc.collect()后看mem_free是否回升。注意SDRAM 时序参数填错不一定立刻崩可能跑几分钟才随机死机压力测试至少跑半小时再下结论。6. 进阶玩法把 .mpy 预编译进 QSPI以及内存布局的验证习惯基础跑通后有两个方向值得折腾。一是把常用库预编译成 .mpy 放进 QSPI Flash 的只读区省 RAM 又加快导入二是固化一套内存布局验证流程每次改板级配置都跑一遍。先说 .mpy。MicroPython 的 .mpy 是字节码比 .py 省解析时间和内存。用mpy-cross编译./mpy-cross/mpy-cross -marcharmv7emsp -o mylib.mpy mylib.py-march要和目标平台匹配H743 是 Cortex-M7 带单精度 FPU常见用armv7emsp。编好的 .mpy 拷进 QSPI Flash 的/flash/libs再把该目录加进sys.path。导入时 MicroPython 优先找 .mpy省掉源码解析。注意 .mpy 版本要和固件 ABI 一致固件升级后旧 .mpy 可能不兼容报invalid .mpy file就重新编。再说内存布局验证。我一般固定跑三段第一段裸机内存测试确认 SDRAM 和 QSPI 读写第二段进 REPL 跑gc.mem_free()和os.statvfs(/flash)确认堆和文件系统容量对得上第三段跑一个吃内存的脚本比如开个大列表再gc.collect()看内存能否回收。三段都过才认为这次板级配置是稳的。验证项命令/方法期望结果SDRAM 读写裸机测试循环无 fail 打印QSPI 读写擦写读回数据一致堆容量gc.mem_free()接近配置值文件系统os.statvfs(/flash)总块数≈8192内存回收大列表 gc.collect()free 回升有个细节os.statvfs返回的块大小和总块数要和你lfs_config里填的一致不一致说明挂载的不是你以为的那个设备。还有SDRAM 跑高频时如果发现偶发错误先降 FMC 时钟验证再逐步提别一上来就顶格跑。从那以后我每次改板级配置都强制走一遍这三段验证哪怕只改了一个堆大小宏。外扩存储这东西配置对了很稳错一个参数就是玄学死机后悔药没得吃。希望帮到你。本文还有配套的精品资源点击获取