
简介这份源码面向已具备STM32与MicroPython基础的嵌入式开发者解决STM32H743开发板在移植MicroPython时外扩32M SQPI Flash与32M SDRAM的板级适配问题。资源包共5个文件约4KB以C源文件、头文件为主辅以引脚映射CSV与编译配置mk文件分别承担板级初始化、HAL配置、引脚定义与构建参数设定放入micropython/ports/stm32/boards/目录即可参与固件编译。目前已有1366人学习下载说明该移植方案在同类开发板中具有一定参考价值。读者可从中获得H743平台存储外扩的板级配置思路、SQPI Flash与SDRAM的初始化参考以及可直接复用的编译工程骨架便于对照自身硬件快速搭建MicroPython运行环境并排查移植中的配置问题。1. 一块 H743 开发板跑起 MicroPython到底值不值得折腾手里有块 STM32H743 的开发板480MHz 主频、1MB SRAM性能放在 MCU 里算相当能打。但裸机写 C 调外设改一个参数就得重新编译烧录迭代一次几十秒就没了。有人把 MicroPython 移植到这块板子上还外扩了 32MB 的 SQPI Flash 和 32MB 的 SDRAM等于给 Python 解释器配了一块能装文件系统的大硬盘和一块能跑大数组的内存。这份源码解决的就是「让 H743 变成一个能跑 Python 脚本的嵌入式开发平台」这件事。适合谁适合想用 Python 快速验证外设逻辑、又不想放弃 H7 性能的开发者也适合拿它做教学板或原型验证的团队。下面从目录结构一路拆到二次开发把能跑起来和能改得动这两件事讲透。2. 工程结构与启动链路从复位向量到 Python 解释器2.1 目录骨架与各层职责拿到源码包先别急着打开 Keil 或 CubeIDE 编译。花十分钟把目录结构摸清楚后面改配置的时候能省掉大量翻文件的时间。这类 H7 移植工程通常按「芯片启动层 → 板级支持层 → MicroPython 内核层 → 用户脚本层」四层组织目录大致长这样project/ ├── Core/ │ ├── Inc/ # 板级头文件时钟、引脚、外设句柄声明 │ └── Src/ │ ├── main.c # 入口初始化时钟和外设后跳进 MicroPython │ ├── stm32h7xx_it.c # 中断向量实现 │ └── system_stm32h7xx.c# 系统时钟配置 ├── Drivers/ │ ├── CMSIS/ # 内核头文件与启动汇编 │ └── STM32H7xx_HAL_Driver/ # HAL 库 ├── micropython/ │ ├── ports/stm32/ # 移植核心mpconfigboard.h、board 目录 │ ├── py/ # 解释器内核 │ └── extmod/ # 扩展模块文件系统、网络等 ├── mpy-cross/ # 交叉编译器生成 .mpy 字节码 └── firmware/ # 编译产物与烧录脚本Core/Src/main.c是上电后第一个跑起来的用户代码它负责把 H7 的时钟树拉到 480MHz、初始化 SDRAM 控制器和 QSPI 接口然后调用 MicroPython 的入口。micropython/ports/stm32/是整个移植的重心板级差异几乎都收敛在这里的mpconfigboard.h和board/子目录里。mpy-cross是宿主机上的工具用来把.py预编译成.mpy减少板子上的解析开销。理解这个分层之后改引脚、改 Flash 大小、加自定义模块都能快速定位到该动哪个文件。2.2 启动流程SDRAM 和 QSPI 必须在解释器之前就绪H7 的启动链路比 F4 复杂因为外扩的 SDRAM 和 QSPI Flash 都需要在 MicroPython 堆初始化之前完成配置。顺序错了表现就是 HardFault 或者文件系统挂载失败。典型流程是复位 → SystemInit 设基础时钟 → main 里配 MPU 和 Cache → 初始化 FMC/SDRAM → 初始化 QSPI → 初始化 MicroPython 堆 → 挂载文件系统 → 进 REPL。关键在 MPU 配置。H7 带 D-CacheSDRAM 映射的地址区间如果不设成 non-cacheable 或者 write-throughDMA 和外设访问会出现数据不一致这种问题排查起来非常折磨人。常见做法是把 SDRAM 区域配成 Normal, Non-cacheable牺牲一点性能换确定性。代码大致如下/* MPU 配置SDRAM 区域设为 non-cacheable避免 DMA 数据不一致 */ static void MPU_Config(void) { MPU_Region_InitTypeDef init {0}; HAL_MPU_Disable(); init.Enable MPU_REGION_ENABLE; init.Number MPU_REGION_NUMBER0; init.BaseAddress 0xC0000000; /* SDRAM 起始地址 */ init.Size MPU_REGION_SIZE_32MB; init.SubRegionDisable 0x00; init.TypeExtField MPU_TEX_LEVEL1; init.AccessPermission MPU_REGION_FULL_ACCESS; init.DisableExec MPU_INSTRUCTION_ACCESS_DISABLE; init.IsShareable MPU_ACCESS_NOT_SHAREABLE; init.IsCacheable MPU_ACCESS_NOT_CACHEABLE; /* 关键不缓存 */ init.IsBufferable MPU_ACCESS_NOT_BUFFERABLE; HAL_MPU_ConfigRegion(init); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }BaseAddress和Size必须和实际焊接的 SDRAM 容量、FMC 地址映射一致写错了不会报错只会在访问越界时随机死机。IsCacheable设成NOT_CACHEABLE是血泪经验早期设成 cacheable 之后LCD 刷屏偶尔出现半屏花屏查了两天才定位到 Cache 和 DMA 打架。QSPI 那边则要注意内存映射模式memory-mapped mode的配置只有进了这个模式Flash 才能像普通内存一样被 XIP 执行和读取MicroPython 的文件系统才有地方放。2.3 板级配置mpconfigboard.h 里改什么micropython/ports/stm32/boards/下每个板子一个目录核心文件是mpconfigboard.h和mpconfigboard.mk。前者定义引脚、外设、堆大小后者决定编译哪些模块。外扩 32MB SDRAM 之后最该改的是堆的起始地址和大小/* mpconfigboard.h 片段把 MicroPython 堆放到 SDRAM */ #define MICROPY_HW_BOARD_NAME H743-SQPI-SDRAM #define MICROPY_HW_MCU_NAME STM32H743 /* 堆起点指向 SDRAM大小留出文件系统缓存余量 */ #define MICROPY_HEAP_START ((void *)0xC0000000) #define MICROPY_HEAP_END ((void *)(0xC0000000 24 * 1024 * 1024)) /* QSPI Flash 作为文件系统载体 */ #define MICROPY_HW_SPIFLASH_SIZE_BITS (32 * 1024 * 1024) #define MICROPY_HW_SPIFLASH_CS pin_B6 #define MICROPY_HW_SPIFLASH_SCK pin_B2MICROPY_HEAP_START指向 SDRAM 基址MICROPY_HEAP_END留了 8MB 给其他用途这个比例不是死的跑大数组就多给堆跑文件多就多留缓存。MICROPY_HW_SPIFLASH_*这几个宏要和原理图上 QSPI 的片选、时钟引脚对上接错了文件系统直接挂不上串口只会打印一行 mount failed。改完这些mpconfigboard.mk里再把FROZEN_MANIFEST指向自己的模块清单就能把常用 Python 模块烧进固件省掉板子上传文件的步骤。3. 编译与烧录把固件跑起来的完整操作3.1 工具链准备与编译命令这类工程一般支持两种编译方式一是用 Makefile 走 arm-none-eabi-gcc二是用 CubeIDE/Keil 打开工程文件。源码包如果带了micropython/ports/stm32/Makefile优先走命令行可复现性最好。宿主机上需要装arm-none-eabi-gcc、make、python3以及dfu-util或st-flash用于烧录。编译前先编mpy-cross因为固件构建过程会调用它# 1. 编译宿主机工具 mpy-cross cd mpy-cross make -j$(nproc) cd .. # 2. 进入移植目录指定板子名编译 cd micropython/ports/stm32 make BOARDH743_SQPI_SDRAM -j$(nproc) # 3. 产物在 build-H743_SQPI_SDRAM/ 下 ls build-H743_SQPI_SDRAM/firmware.*BOARD的值必须和boards/下的目录名完全一致大小写敏感。-j$(nproc)是并行编译H7 的 HAL 库文件多单线程编一次要几分钟。如果报arm-none-eabi-gcc: command not found检查工具链是否加进了 PATH如果报某个board目录找不到说明板级目录名拼错了。编译成功后产物通常是.elf、.bin、.hex三件套.bin用于 DFU 或外部烧录器.hex给 ST-Link Utility 用。3.2 烧录方式与首次上电验证烧录有两条路ST-Link 走 SWD或者 USB DFU。开发板如果板载了 ST-Link直接st-flash write firmware.bin 0x08000000最省事。走 DFU 的话先把 BOOT0 拉高复位进 DFU 模式然后# DFU 方式烧录注意地址是内部 Flash 起始 dfu-util -a 0 -s 0x08000000:leave -D build-H743_SQPI_SDRAM/firmware.bin-s 0x08000000:leave里的:leave表示烧完自动跳转运行不加的话板子会停在 DFU 模式不动。首次上电后接串口通常是 USART3 或 USB CDC波特率 115200应该能看到 MicroPython 的 REPL 提示符。如果没有任何输出先量晶振起振没有再看main.c里串口初始化的引脚和波特率对不对。能进 REPL 之后敲import os; os.listdir(/)验证 QSPI 文件系统是否挂载成功敲import gc; gc.mem_free()看堆是不是落在 SDRAM 区间数值应该是二十多 MB 级别而不是内部 SRAM 的几百 KB。3.3 文件系统与脚本下发QSPI Flash 挂载成文件系统之后往板子上传脚本有几种方式。最直接的是 REPL 里用Ctrl-E进粘贴模式把脚本贴进去执行长期使用建议用mpremote或ampy通过串口传文件# 用 mpremote 拷贝脚本到板子文件系统 mpremote connect /dev/ttyUSB0 fs cp main.py :main.py # 直接运行板子上的脚本 mpremote connect /dev/ttyUSB0 run main.pyfs cp的源路径是宿主机冒号后面是板子上的路径。传大文件时注意 QSPI 写入速度32MB 全写满要几十秒别以为卡死了就拔线。boot.py会在上电时自动执行适合放初始化代码main.py在boot.py之后跑放业务逻辑。这两个文件名是 MicroPython 的约定改了就不会自动执行这点和桌面 Python 的习惯不一样新手容易在这里翻车。4. 核心模块拆解SDRAM、QSPI 与解释器的衔接4.1 SDRAM 初始化FMC 时序参数怎么算外扩 SDRAM 能不能稳定跑全看 FMC 的时序参数。H7 的 FMC 时钟来自 HCLK3480MHz 主频下通常是 240MHz 或者分频后的值。SDRAM 芯片手册里给的是纳秒级的 tRCD、tRP、tRAS要换算成 FMC 的时钟周期数。以常见的 166MHz 颗粒为例tRCD18ns在 240MHz 下就是 18ns × 240MHz ≈ 4.3向上取整填 5。这类换算错一个数表现是读写偶发错误跑内存测试能过但跑一会儿就崩。/* FMC SDRAM 时序配置数值需按颗粒手册换算 */ hsdram1.Init.ReadBurst FMC_SDRAM_RBURST_ENABLE; hsdram1.Init.ReadPipeDelay FMC_SDRAM_RPIPE_DELAY_1; hsdram1.Init.CASLatency FMC_SDRAM_CAS_LATENCY_3; /* CL3 */ hsdram1.Init.WriteProtection FMC_SDRAM_WRITE_PROTECTION_DISABLE; hsdram1.Init.SDBank FMC_SDRAM_BANK1; hsdram1.Init.ColumnBitsNumber FMC_SDRAM_COLUMN_BITS_NUM_9; hsdram1.Init.RowBitsNumber FMC_SDRAM_ROW_BITS_NUM_13; hsdram1.Init.MemoryDataWidth FMC_SDRAM_MEM_BUS_WIDTH_16; hsdram1.Init.InternalBankNumber FMC_SDRAM_INTERN_BANKS_NUM_4;CASLatency要和颗粒支持的 CL 值匹配设高了能跑但浪费周期设低了直接读错。ColumnBitsNumber和RowBitsNumber决定容量9 列 13 行 4 bank 16 位宽算下来正好 32MB和标题里的容量对上。初始化完成后建议跑一遍写满再读回的测试别信「能进 REPL 就没事」SDRAM 的间歇性错误往往在跑大数组时才暴露。4.2 QSPI Flash 映射文件系统如何落到 32MB 空间QSPI Flash 在 MicroPython 里通常走stm32port 的spiflash驱动配置成块设备后挂到 littlefs 或 FAT 上。关键是内存映射模式的地址窗口H7 的 QSPI 映射区一般在0x90000000开始。文件系统读写走的是间接模式indirect mode只有 XIP 执行才用映射模式两者切换时要注意先等总线空闲。/* QSPI 进入内存映射模式供 XIP 和直接读取使用 */ QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0xEB; /* 快速读四线指令 */ cmd.AddressMode QSPI_ADDRESS_4_LINES; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_4_LINES; cmd.AlternateBytes 0x00; cmd.DataMode QSPI_DATA_4_LINES; cmd.DummyCycles 6; /* 按颗粒手册填 */ cmd.NbData 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_MemoryMapped(hqspi, s_mem_mapped_cfg);DummyCycles是最容易填错的一个参数不同厂家的颗粒要求不一样填少了读出来全是 0xFF 或者乱码。AlternateBytes在四线模式下通常配成 0具体看颗粒是否支持连续读模式。文件系统大小通过MICROPY_HW_SPIFLASH_SIZE_BITS声明驱动会按这个值划分块设备改大了超出实际容量会在写满时出错改小了浪费空间按实际颗粒容量填最稳。4.3 解释器堆与 GC大内存下的行为差异MicroPython 的 GC 是标记清除式堆大了之后单次 GC 的扫描时间会变长。内部 SRAM 只有几百 KB 时感觉不到堆挪到 24MB 的 SDRAM 之后一次 full GC 可能占用几十毫秒对实时性有要求的场景要留意。可以在关键循环里调gc.collect()的时机或者用gc.threshold()控制自动回收的触发点。import gc # 堆变大后适当提高阈值减少 GC 频次 gc.threshold(512 * 1024) # 手动回收并观察剩余 gc.collect() print(free:, gc.mem_free(), alloc:, gc.mem_alloc())gc.threshold的单位是字节设太小 GC 频繁拖慢执行设太大内存涨得快。SDRAM 堆的另一个坑是访问速度比内部 SRAM 慢中断里如果碰了堆上的对象延迟会明显上升。常见做法是把中断服务程序里用到的缓冲区放内部 SRAM通过链接脚本的 section 属性指定别让高频中断去碰 SDRAM。5. 避坑与排查五个真实会卡住你的问题5.1 上电无输出串口一片安静现象是烧录成功、复位后串口没有任何字符。原因通常有三个时钟没配到 480MHz 导致串口波特率偏差过大、串口引脚被复用成了别的功能、或者启动文件里的堆栈大小不够在进 main 之前就崩了。解决顺序是先量晶振再用调试器单步看SystemClock_Config返回值最后检查stm32h7xx_it.c里 HardFault 有没有被触发。我一般会在main第一行点一个 GPIO 翻转示波器一看就知道代码到底跑没跑起来。5.2 文件系统挂载失败报 mount failed现象是 REPL 能进但os.listdir(/)报错或者返回空。原因多半是 QSPI 引脚配置和原理图对不上或者DummyCycles填错导致读出的 ID 不对。先在 REPL 里读 Flash ID 验证通信import stm32; print(stm32.spiflash_id())之类的接口具体函数名看驱动实现ID 不对就回头查引脚和时序。另一个可能是 Flash 从没格式化过首次挂载需要先os.mkfs或者驱动里带自动格式化逻辑。5.3 SDRAM 跑一会儿就死机现象是刚上电正常跑几分钟或者跑大数组时随机 HardFault。原因基本锁定在 MPU 配置和 Cache 一致性上DMA 写 SDRAM 而 CPU 读的是 Cache 里的旧数据。解决办法是把 SDRAM 区域设成 non-cacheable或者在 DMA 操作前后手动SCB_CleanInvalidateDCache()。前者牺牲性能但省心后者性能好但每个 DMA 点都要记得加漏一个就是一个玄学 bug。5.4 编译报错找不到 board 目录现象是make BOARDxxx提示No such board。原因是板级目录名和BOARD参数大小写或拼写不一致或者目录放错了层级。ports/stm32/boards/下每个目录名就是合法的 BOARD 值进去看一眼mpconfigboard.mk是否存在缺这个文件也会被跳过。改完目录名记得make clean增量编译有时会缓存旧的板级配置。5.5 脚本传上去不自动执行现象是main.py明明传到了根目录上电却不跑。原因是文件名不对比如传成了Main.py或者main.py.txt或者boot.py里有异常导致后续不执行。MicroPython 只认小写的boot.py和main.py且必须在文件系统根目录。排查时先在 REPL 里import os; print(os.listdir(/))确认文件名再手动exec(open(main.py).read())看有没有语法错误。6. 二次开发加自定义模块和裁剪固件的实操思路改造这块板子的第一步不是写代码是决定「哪些逻辑放 C 层、哪些放 Python 层」。我的习惯是高频调用的外设读写、时序敏感的操作放 C 做成machine的扩展模块业务逻辑、状态机、协议解析放 Python改起来不用重新烧固件。加自定义模块的入口在micropython/ports/stm32/下新建一个modxxx.c然后在mpconfigboard.mk的SRC_C里加进去再用MP_REGISTER_MODULE注册。/* modboard.c一个最小的自定义模块暴露板子温度读取 */ #include py/runtime.h #include py/obj.h static mp_obj_t board_get_temp(void) { /* 这里换成实际的 ADC 或传感器读取 */ return mp_obj_new_float(36.5f); } static MP_DEFINE_CONST_FUN_OBJ_0(board_get_temp_obj, board_get_temp); static const mp_rom_map_elem_t board_module_globals_table[] { { MP_ROM_QSTR(MP_QSTR_get_temp), MP_ROM_PTR(board_get_temp_obj) }, }; static MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); const mp_obj_module_t board_module { .base { mp_type_module }, .globals (mp_obj_dict_t *)board_module_globals, }; MP_REGISTER_MODULE(MP_QSTR_board, board_module);MP_REGISTER_MODULE的第二个参数是模块名Python 里import board就能用。MP_ROM_QSTR把字符串常量塞进 ROM省 RAM。改完重新编译烧录REPL 里import board; board.get_temp()验证。裁剪固件则是反向操作mpconfigboard.mk里去掉不用的SRC_Cmpconfigboard.h里关掉不用的外设宏固件能小几百 KB启动也快一点。验证改造是否成功我一般走三步先跑一遍原有功能回归REPL、文件系统、SDRAM 读写再跑新模块的单元测试脚本最后做一次断电重启确认boot.py链路没被破坏。这套流程看着笨但能挡住大部分「改完这里崩那里」的问题。从那以后我每次动板级配置都强制走一遍「编译 → 烧录 → 串口回归 → 断电重启」四步不再图省事直接热复位。希望帮到你。本文还有配套的精品资源点击获取