☰
STM32H743 MicroPython移植:QSPI Flash与SDRAM配置排错
2026/11/5 4:22:35 网站建设 项目流程

简介:面向STM32H743开发板进行MicroPython移植的开发者,这份源码包提供可放入MicroPython板级目录的板卡支持代码,重点覆盖外扩32M QSPI Flash与32M SDRAM的适配,可用于学习Python在STM32H7平台上的板级移植方法、存储扩展初始化及外设引脚配置。包内共5个文件,以2个h头文件、1个c源文件和1个mk构建配置为主,另含1个csv引脚映射表,分别承担时钟与外设宏定义、板级初始化、编译选项组织及引脚复用描述,压缩包约4KB,体量轻便、结构集中。该源码已有1367人学习,适合具备一定STM32与嵌入式基础、希望参考H7系列MicroPython板级适配的读者。借助其中的板级初始化代码、HAL配置和构建配置,可快速理解QSPI Flash与SDRAM在MicroPython端口中的接入方式,并对照自身开发板完成存储扩展与引脚配置的移植实践。

1. 一块 H743 开发板玩 MicroPython,真正难的往往不是编译

第一次给 STM32H743 开发板移植 MicroPython,我以为最难的是把编译器装好。编完才发现,真正让人反复排查的是外扩存储器:内部 2MB flash 装完固件只剩几百 KB,外挂的 32MB QSPI flash 挂上去却只认到 8MB,32MB 的 SDRAM 只要访问一个字节,系统就直接进入 HardFault,REPL 完全没反应。这几个现象几乎覆盖了 STM32 上跑 python 的全部门槛。想跑轻量的人工智能 demo、想把图片和音频放进板子,光有固件还不够,必须让外部 flash 和 SDRAM 都被正确识别。这套环境适合手里已有 H743 板子、愿意敲命令的人;只想插上即用的人不算目标读者,因为你不会知道自己到底跑在多大的内存上。

2. 环境清点:主机工具链、源码树和两块外扩存储器先分清

动手之前把家里的东西清点一遍,比编到一半再回头补依赖省事得多。这一章不急着敲 make,先把主机上该装什么、源码树里哪几个目录要改、外扩的两块存储器在系统里各自扮演什么角色定下来。这三件事定错了,后面所有步骤都会白做:工具链版本不够,H7 的浮点 ABI 会报找不到库;板级目录建错位置,make BOARD=直接提示找不到板子;地址和容量单位搞混,就会出现“烧录成功,但一运行就出错”。

2.1 主机侧四样东西,少一样都会在编译中途出错

我一般把主机依赖拆成四类看:编译器、构建工具、脚本解释器、烧录工具。编译器决定能不能生成 Cortex-M7 的代码,构建工具决定子模块和 HAL 能不能拉全,python3 是编译期生成 qstr 表和冻结模块用的,烧录工具决定你改完一行配置之后多久能看到结果。下面这条命令覆盖前三类,在常见的 Linux 发行版上可以直接跑:

# 一次装齐,避免编到一半才发现缺工具 sudo apt-get update sudo apt-get install -y \ git make python3 python3-pip \ gcc-arm-none-eabi binutils-arm-none-eabi \ libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib

这里每个包都有明确用途:gcc-arm-none-eabi是交叉编译器本体,binutils-arm-none-eabi提供arm-none-eabi-size、objcopy这些后面看固件体积要用的工具,libnewlib-arm-none-eabi是 C 库,缺了会在链接阶段报cannot find -lc。装完立刻验版本,这一步别省:

arm-none-eabi-gcc -v 2>&1 | tail -n 2 # 期望看到 gcc version 10.3 及以上

为什么卡 10.3?H7 内核是 Cortex-M7,编译参数里会带-mfpu=fpv5-d16 -mfloat-abi=hard,老一点的工具链包里没有对应的 multilib,链接时报cannot find -lc或者浮点调用直接跑飞。版本表我习惯这么记:

组件建议版本用途装错的典型表现
arm-none-eabi-gcc10.3 及以上交叉编译 Cortex-M7链接找不到-lc,浮点异常
make4.x驱动板级构建子模块规则不执行
python33.8 及以上生成 qstr、冻结模块编译期脚本报语法错误
git任意较新版本拉源码与子模块submodules目标失败
SWD 烧录命令行与调试器匹配写 flash、复位连得上但写不进去

烧录工具我放在最后一章再说,因为它跟验证是绑在一起的。这里只需要确认一件事:调试器能被系统识别,插上板子能看到设备节点。

2.2 源码树和板级目录:boards 下新建一个自己的板子

MicroPython 的 STM32 端口把所有板子的差异都收在ports/stm32/boards/下面,一块新板子本质上就是“复制一份同系列板子的目录,然后改内容”。我建议直接照抄一颗同系列、带外扩存储的板子,因为它已经把 QSPI 和 SDRAM 的骨架写好了,你只需要改参数,而不是从空文件开始理解每个宏:

micropython/ ├── mpy-cross/ # 主机上跑的交叉编译器,先编它 ├── py/ # 解释器核心,一般不动 ├── lib/ # 子模块:HAL、CMSIS、文件系统 └── ports/stm32/ ├── boards/ │ └── MY_H743/ # 自己的板级目录 │ ├── mpconfigboard.mk # 芯片型号、链接脚本、源文件 │ ├── mpconfigboard.h # 板级宏:晶振、QSPI、SDRAM │ └── pins.csv # 引脚命名,REPL 里按名字取 pin ├── qspi.c / qspi.h # QSPI 外设驱动 ├── sdram.c # FMC 上的 SDRAM 初始化 └── storage.c # 文件系统挂载

mpconfigboard.mk决定“用哪颗芯片、链接脚本选哪个、编译哪些源文件”,mpconfigboard.h决定“板子上焊的是什么器件、宏开不开”,pins.csv决定你写Pin('LED')时能不能找到对应的脚。新手最容易犯的错是把这三个文件混着改:把芯片容量写进.h,把外设参数写进.mk,结果编译器不报错,但生成的固件行为和预期完全不一致。

2.3 QSPI flash 与 SDRAM 在系统里的分工

这两块外部器件经常被混为一谈,其实它们在系统里的角色完全不同。QSPI flash 挂在 QUADSPI 外设上,被映射成一段只读可执行的地址空间,同时也能当块设备写;SDRAM 挂在 FMC 上,是一段可读写的普通内存。H7 系列的地址分布大致是这样:

区域起始地址典型容量在本项目里的用途
内部 flash0x080000002MB放固件代码和常量
AXI SRAM0x24000000512KB主 RAM,栈和部分堆
DTCM0x20000000128KB中断栈、实时性要求高的数据
QSPI flash 映射区0x9000000032MB文件系统,存脚本和资源
SDRAM 映射区0xC000000032MBGC 堆、大缓冲、帧缓冲

把这张表记住,后面的报错就都能对上号:报MemoryError说明堆不够,问题在 SDRAM;报OSError: [Errno 5]说明文件系统没挂上,问题在 QSPI;一访问 0x90000000 就进入 HardFault,说明 QSPI 的内存映射模式没打开或者时序不对。

提示:不同型号的 H7 外设名不完全一样,有的叫 QUADSPI,有的叫 OCTOSPI,容量和最大频率也不同。动手前先翻一眼自己那颗芯片的数据手册,别拿别人的参数直接套。

3. 交叉编译链落地:从 mpy-cross 到 firmware.elf 的完整一条链

环境清点完就可以开始编了,但顺序很重要。MicroPython 的构建分两段:第一段在主机上编出mpy-cross,它负责把.py预编译成.mpy;第二段才是用交叉编译器编目标板固件。很多人跳过第一段直接编板子,然后发现冻结模块那一步报错,又回头补,白白浪费时间。这一章按实际执行顺序走一遍,顺便把每一步的产物和它的意义说清楚。

3.1 工具链装上之后,先用一条命令验版本

装完包不等于装对了。系统里可能同时存在多个工具链,PATH里排在前面的未必是你以为的那个。我习惯跑两条命令确认:一条看版本,一条看它支持的目标架构:

arm-none-eabi-gcc --version | head -n 1 arm-none-eabi-gcc -print-multi-lib | grep -i fpv5 # 输出里应该能看到 cortex-m7 对应的 fpv5-d16 / hard 组合

第二条命令是很多人不知道的排查手段。如果输出里没有fpv5-d16/hard这一项,说明这套工具链压根没编 Cortex-M7 硬浮点的库,无论你怎么改 Makefile 都链接不过。遇到这种情况,常见做法是去工具链的官方发布页下那个带年份的版本包,解压后把bin/加到PATH前面,而不是继续在包管理器里换版本,因为仓库里的版本往往滞后。

验证通过之后,还要确认一件事:arm-none-eabi-gcc和arm-none-eabi-size来自同一套工具链。混着装会出现“编译通过、size 报格式不支持”的奇怪现象,这类问题排查起来很花时间,不如一开始就用同一目录下的工具。

3.2 mpy-cross 必须先编,跨版本缓存会带来麻烦

mpy-cross是给主机用的编译器,把.py编译成字节码。它和固件之间有版本约定,版本不匹配时运行期会报incompatible .mpy file。所以顺序永远是先编它,而且换了源码分支之后要重新编:

git clone --depth 1 <源码仓库地址> micropython cd micropython make -C mpy-cross -j$(nproc) # 主机编译器,几十秒就好 ./mpy-cross/build/mpy-cross --version

这里-j$(nproc)是并行编译,mpy-cross本身不大,几秒到几十秒。编完产出的可执行文件在mpy-cross/build/下,后面编板子固件时构建脚本会自动调用它,所以你不需要手动指定路径,只要保证它存在并且版本和源码树一致。

一个容易忽略的点:如果你之前编过别的板子,mpy-cross目录里可能残留旧的目标文件。换分支之后我一般会执行make -C mpy-cross clean再编一次。这类残留导致的问题表现得很不稳定,可能今天能过、明天换个宏就报错,属于典型的“看似随机、实则有原因”的问题。

3.3 拉子模块和全量编译:一次干净的构建长什么样

STM32 端口的依赖都在子模块里:HAL 驱动、CMSIS、文件系统实现。不拉子模块直接 make,会在编译第一批源文件时就报头文件找不到。正确顺序是先拉子模块,再全量编译:

cd ports/stm32 make BOARD=MY_H743 submodules # 只拉这块板子需要的依赖 make BOARD=MY_H743 -j$(nproc) V=1 2>&1 | tee build.log

V=1会把每一条编译命令都打出来,第一次编板子建议加上,出问题时能直接从日志里看到是哪个文件、哪条命令、哪个宏出错,比看一行Error 1强太多。tee build.log是为了保留现场,编了几分钟才失败的时候,翻日志比重编一遍快。

编译成功后,build-MY_H743/目录下会有一堆产物,容量和用途各不相同:

产物用途
firmware.elf带调试符号的完整镜像,排查 HardFault 靠它
firmware.hex带地址信息的烧录文件,烧录工具都认
firmware.bin纯二进制,从 0x08000000 起烧
firmware.dfu走 USB 升级通道时用
firmware.map链接映射表,查符号地址和段大小

编完先看一眼体积,这一步能提前发现很多问题:

arm-none-eabi-size build-MY_H743/firmware.elf # text 放 flash,data 初始化数据,bss 是清零区

如果bss突然变成几百 KB,而你并没有加大缓冲,那基本可以确定有哪个大数组被定义成了全局变量,正悄悄占用内部 RAM。H7 的 AXI SRAM 只有 512KB,这种无意的占用会让运行期栈溢出,表现形式是随机进入 HardFault。

4. 板级配置改造:mpconfigboard 里哪些宏最关键

到这里固件能编出来了,但它还只是“一颗通用 H7”,既不知道板子上焊的是 25MHz 还是 8MHz 晶振,也不知道外挂 flash 多大、SDRAM 是几根地址线。真正让 MicroPython 认下这块板子的,是板级目录里那几个宏。这一章按“芯片和时钟 → QSPI → SDRAM → 文件系统位置”的顺序改,每一步都能单独验证。

4.1 mpconfigboard.mk 与 mpconfigboard.h:先对齐芯片和晶振

.mk管构建,.h管参数。先把芯片型号和链接脚本对齐,这部分照抄同系列板子最稳:

# 决定用哪套 HAL、哪份链接脚本 MCU_SERIES = H7 CMSIS_MCU = STM32H743xx AF_FILE = boards/stm32h743_af.csv LD_FILES = boards/stm32h743.ld boards/common_bl.ld boards/common_bss.ld # 代码段起点;用了外部 bootloader 的话这里要跟着偏移 TEXT0_ADDR = 0x08000000

MCU_SERIES和CMSIS_MCU一定要匹配,写错会导致 HAL 里选到另一颗芯片的头文件,编译可能通过,但寄存器定义全错位。AF_FILE是复用功能表,管的是串口、SPI 这些外设能不能正确映射到引脚。LD_FILES里那几个链接脚本,顺序有意义:主脚本定义存储布局,后面的补充文件定义 bootloader 和 bss 段的处理方式。

.h里最先要改的是板名、芯片名和晶振:

#define MICROPY_HW_BOARD_NAME "H743-SQPI-32M" #define MICROPY_HW_MCU_NAME "STM32H743" #define MICROPY_HW_XTAL_FREQ (25000000) // 板载晶振,写错时钟树全歪

晶振这一行是整份配置里最容易被忽略、后果最严重的一行。写错之后系统照样能起来,但所有派生时钟都按错误的比例算:串口波特率偏、USB 枚举失败、QSPI 时钟超规格。表现是“能烧进去、能连上调试器、就是没有 REPL”,特别容易被误判成固件问题。我的习惯是上电先用示波器或者频率计确认晶振实际频率,再去改这一行,而不是照着原理图猜。

4.2 QSPI flash:容量单位是 bit,时序要留余量

外扩 flash 出问题,九成出在两个地方:容量单位写错,和时序设太激进。MicroPython 里这类容量宏的单位是 bit,不是 byte,32MB 要写成32 * 1024 * 1024 * 8。写成32 * 1024 * 1024的话,文件系统会按 4MB 去划分,于是你明明焊了 32MB,os.statvfs却告诉你只有 4MB。

// QSPI flash:给文件系统用的 32MB #define MICROPY_HW_QSPIFLASH_SIZE_BITS (32 * 1024 * 1024 * 8) #define MICROPY_HW_QSPIFLASH_CS (pin_B6) #define MICROPY_HW_QSPIFLASH_SCK (pin_B2) // 其余数据线按板子实际连线填,四线模式下要全部给出

驱动层的时序参数在qspi.c里,字段名不同版本略有差异,但含义就那几个:

参数含义我一般怎么取
时钟预分频QSPI 内核时钟分频先取 1/8 跑通,再逐级提到 1/2
采样相位决定在时钟上升沿还是下降沿采样读 ID 不稳时先调这个
CS 高电平时间两次命令之间的片选间隔flash 手册里给的最小值再留一档
地址字节数3 字节还是 4 字节超过 16MB 必须用 4 字节模式
内存映射使能是否允许从 0x90000000 直接取指挂文件系统时必须打开

调试顺序我固定成这样:先把时钟降到最低,读 flash 的 ID,能读对再开内存映射,能读到正确数据再提速。跳过读 ID 直接开高速,出现的问题往往是“偶尔能起来”,这种不稳定问题最难查。32MB 的片子还要特别注意 4 字节地址模式,不开的话超过 16MB 的部分会回绕到低地址,写进去的数据把前面的内容覆盖掉,而你还以为写成功了。

4.3 SDRAM:FMC 时序按手册算,别照抄别的板子

SDRAM 比 flash 娇气得多。它的时序参数单位是 SDCLK 周期数,不是纳秒,换算错了就是随机丢数据。以一颗 32MB、16 位宽、4 bank、13 行地址、9 列地址的片子为例,参数大致这样取:

参数手册值100MHz 下折算说明
CAS 延迟速度等级决定3频率越高取值越大
tRCD20ns2 周期行激活到列读写
tRP20ns2 周期预充电时间
tRAS42ns5 周期行激活最短保持
tMRD2 周期2模式寄存器到命令间隔
刷新计数64ms / 8192 行按主频算算小了会慢慢丢数据

对应到初始化结构体里,字段名按你手上那份源码对一遍,值按上表填:

// 时序单位是 SDCLK 周期数,不是 ns,别直接抄手册数字 static const sdram_timing_t timing = { .cas_latency = 3, // 与速度等级匹配 .tmrd = 2, // 模式寄存器命令间隔 .trcd = 2, // 20ns @ 100MHz .trp = 2, .tras = 5, // 最小 42ns,向上取整 .txsr = 7, // 退出自刷新 };

刷新计数那条特别值得说。SDRAM 靠周期性刷新保住数据,算小了不会立刻出错,而是过几分钟开始出现个别位翻转。表现是系统跑一段时间莫名其妙重启,或者文件系统校验失败,你回头去查代码永远查不到。我一般会把刷新周期留一档余量,宁可损失一点带宽。

4.4 文件系统和堆的位置:别把 SDRAM 塞进 .bss

外扩存储器初始化完成后,还要决定它们在内核里怎么被用起来。QSPI flash 作为块设备注册给文件系统,上电时在它上面挂 littlefs,挂载点通常就是/flash;SDRAM 则是通过板级宏告诉内核“这块内存可以用”,由运行期把 GC 堆指过去。

链接脚本里不要给 SDRAM 定义MEMORY区域,更不要把.bss段放进去:

/* 链接脚本里只声明片内存储,外部 SDRAM 由运行期初始化 */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K RAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K }

原因很直接:.bss段是在启动代码里被整体清零的,那时候 SDRAM 控制器还没初始化,往 0xC0000000 写数据必然进入 HardFault。外部存储器只能由 C 代码在 FMC 初始化完成之后再去用,这也是为什么板级宏里要单独给一个 SDRAM 容量,而不是靠链接脚本声明。搞混这一点,症状就是“一上电就进入 HardFault,烧回原厂固件又正常”,很多人会误判成芯片坏了。

5. 排查清单:编译、烧录、运行三个阶段最容易出错的地方

下面这五条都是我自己真实踩过的,按出现阶段排序。每条按“现象 → 原因 → 解决”写,方便你对着日志找。

5.1 编译和链接阶段的三个现象

现象一:链接报region RAM overflowed by xxx bytes,但你并没有定义大数组。 原因:主 RAM 区域容量写小了,或者某个源文件里开了大尺寸的静态缓冲(比如网络缓冲、帧缓冲),把 512KB 的 AXI SRAM 吃穿。 解决:先看firmware.map里哪个符号占得最多,arm-none-eabi-nm --size-sort排个序就能定位。确实需要大缓冲的,改成运行期从 SDRAM 里申请,不要定义成全局数组。同时确认链接脚本里的LENGTH和芯片实际容量一致,H743 不同尾缀的 RAM 大小不一样。

现象二:报fatal error: stm32h7xx_hal_conf.h: No such file or directory。 原因:子模块没拉全,或者板级目录里少了stm32h7xx_hal_conf.h这份配置文件,HAL 找不到它就编译不下去。 解决:回ports/stm32目录重新跑一次make BOARD=MY_H743 submodules,然后检查板级目录里有没有这份文件,没有就从同系列板子复制一份,再把用不到的外设模块关掉,能省不少 flash。

现象三:改完宏之后编译飞快,但固件行为完全没变。 原因:增量编译没有识别到头文件的改动,或者build-xxx目录里混了别的板子的目标文件。 解决:换板子或者大改宏之后,先make BOARD=MY_H743 clean再编。这个习惯能省掉大量“改了没用”的困惑,我自己吃过好几次亏才养成。

5.2 烧录和运行阶段的两个现象

现象四:烧录工具显示写入成功、校验通过,复位后没有任何输出。 原因:一是启动地址不对,TEXT0_ADDR和实际烧录偏移不一致;二是 H7 的双 bank flash 让代码落在了另一块 bank 上,而启动配置指向的是空的那块;三是选项字节里的启动地址被改过。 解决:先用调试器连上,读 0x08000000 的前几个字,看看是不是有效的栈顶和复位向量。不是的话说明烧错位置了。再检查选项字节里的启动配置,必要时整片擦除后重新烧。别一上来就怀疑固件逻辑,这类问题九成在烧录环节。

现象五:REPL 能进,但一执行os.listdir('/')就卡死或者重启。 原因:QSPI 的时序太激进,或者内存映射模式下读取用了错误的 dummy 周期,单条命令能过、连续读就出错。 解决:把 QSPI 预分频降到最低再试,能过就说明是时序余量不够,逐级往上提,找到稳定点之后退一档使用。同时确认 flash 的读命令和 dummy 周期与手册一致,不同厂家的片子这两个参数不通用。

注意:出现“偶尔正常、偶尔异常”的情况,先怀疑时钟和时序,不要先怀疑 Python 代码。硬件的边界条件不会给你明确的报错,它只会让系统在某个时刻以某种方式重启。

6. 装完怎么验证:三条自检把 flash 和 SDRAM 都过一遍

固件烧进去、REPL 能敲字,只说明内核跑起来了,不代表外扩存储器真的可用。我习惯在上电后跑三条自检,分别对应系统信息、文件系统容量和内存可用量:

import os, gc, machine, micropython print(os.uname().machine) # 确认板名与预期一致 print(machine.freq()) # 主频对不对,时钟树有没有配错 print(os.statvfs('/flash')) # 块大小 x 块数,应接近 32MB gc.collect() print(gc.mem_free()) # SDRAM 挂上后应该是几十 MB 量级 micropython.mem_info()

statvfs返回的块数乘块大小,就是文件系统实际能用的空间。如果只有 4MB 左右,回去查SIZE_BITS是不是少乘了 8;如果直接抛异常,说明文件系统没挂上,问题在 QSPI 初始化。mem_free更直观,堆还在片内 RAM 时通常只有几十 KB 到一百多 KB,堆挪到 SDRAM 之后会跳到几十 MB,这个数量级的变化一眼就能看出来。

文件系统要做一次写读回环,别只看容量。写一个几百 KB 的文件,重新挂载,再读回来比对:

data = bytes(range(256)) * 1024 # 256KB 测试数据 with open('/flash/t.bin', 'wb') as f: f.write(data) with open('/flash/t.bin', 'rb') as f: assert f.read() == data # 不一致就是 QSPI 读写有问题 print('flash ok')

SDRAM 用地址相关图样做冒烟测试,比全片写零靠谱得多。全写 0 或者全写 0xFF,地址线断了也测不出来:

import machine BASE, SIZE, STEP = 0xC0000000, 32 * 1024 * 1024, 0x1000 for off in range(0, SIZE, STEP): # 每个 4KB 页写一个带地址信息的图样 machine.mem32[BASE + off] = 0xA5A50000 | (off & 0xFFFF) for off in range(0, SIZE, STEP): # 读回比对,不一致说明该地址段有问题 assert machine.mem32[BASE + off] == (0xA5A50000 | (off & 0xFFFF)), hex(off) print('sdram ok')

这个测试只覆盖每个 4KB 页的首字,跑一遍几十秒,用来做上电冒烟够用。它抓不住的是相邻位之间的干扰和刷新不足导致的缓慢丢数据,那类问题得靠长时间跑大块数据搬运才能暴露。我一般还会额外做一件事:把 SDRAM 当成大缓冲连续读写十几分钟,看有没有偶发的不一致,这比任何参数表都更能说明时序有没有余量。

从那以后我每焊一块新板子,第一次上电都强制走一遍这三条自检,容量对不上就先不改代码。这套习惯帮我省下过好几次通宵,也让我明白一件事:外扩存储器的问题从来不会自己消失,它只会在你以为一切正常的时候出现。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询