简介:面向希望在STM32H743开发板上运行MicroPython的嵌入式开发者,这套板级移植源码围绕外扩32M SQPI Flash与32M SDRAM的适配展开,可用于搭建Python脚本快速验证环境,也适合学习STM32H7系列在MicroPython下的板级支持组织方式,对已有C语言与HAL基础、打算做硬件二次开发的读者同样友好。包内共5个文件,压缩包约4KB,由C语言板级初始化、两个H头文件、CSV引脚映射表和make编译配置组成,分别承担外设初始化、宏与HAL参数定义、引脚分配及构建规则,体量小但关键环节齐全。已有1367人学习。借助其中的存储与外设配置,读者可对照理解SQPI Flash和SDRAM在板级支持包中的接入方式,复现开发板固件构建,并据此裁剪引脚、调整时钟与外设参数,减少从零移植时的排查成本;也可作为后续联网、传感采集等Python应用的前置硬件适配参考。
1. 内部 2MB 撑不住以后:一块 H743 板子的外扩存储改造值不值得做
内部 Flash 只有 2MB,一个带 network、ssl、ffi 的 micropython 固件编出来就能吃掉大半,剩下那点空间连放几个 .py 脚本都紧张;STM32H743 的 1MB 片内 SRAM 看着不少,真跑起图像缓冲或者几十 KB 的 JSON 解析,GC 一触发就开始掉帧。这套源码要解决的正是这两件事:把 32M 的 SQPI flash 挂成 MicroPython 的根文件系统,把 32M 的 SDRAM 并进堆区和缓冲区。它不是给纯新手点灯用的 demo,是给已经能把 MicroPython 跑起来、但被存储卡住脖子的那批人准备的;你得能看懂 board 目录结构、会写 mpconfigboard.h、知道 FMC 和 QSPI 控制器大概在干什么,否则时序算错一格就是一块只会发热的板子。
2. 存储映射与时钟树:H743 跑 MicroPython 前必须先钉死的三件事
H743 这颗芯片的存储布局比 F4 系列复杂得多,不是"把堆改大一点"就能收工的。它有几块物理上完全不同的 RAM:DTCM 128KB 紧贴内核、零等待、不经过 Cache;AXI SRAM 512KB 挂在总线矩阵上;还有 SRAM1~SRAM4 加起来三百多 KB。MicroPython 的堆默认放在哪块、栈放在哪块、DMA 缓冲区放在哪块,直接决定了后面 SDRAM 接进来之后是锦上添花还是一地鸡毛。把这三件事按顺序钉死,后面外扩 Flash 和 SDRAM 才有讨论的基础。
2.1 先画清楚地址地图:哪些区域能挂外设,哪些不能
动手改代码之前,我会先在纸上把这几段地址抄一遍,因为后面每一个宏、每一行链接脚本都要跟它们对齐:
| 区域 | 起始地址 | 容量 | 典型用途 | 能不能给 DMA |
|---|---|---|---|---|
| ITCM | 0x00000000 | 64KB | 中断向量、极热代码 | 否 |
| DTCM | 0x20000000 | 128KB | 主栈、GC 根节点 | 否 |
| AXI SRAM | 0x24000000 | 512KB | 主堆、大缓冲 | 是 |
| SRAM1~3 | 0x30000000 | 288KB | 外设 DMA 缓冲 | 是 |
| SRAM4 | 0x38000000 | 64KB | 低功耗域缓冲 | 是 |
| QSPI 映射窗口 | 0x90000000 | 最大 256MB | 外部 flash 内存映射读 | 否 |
| FMC Bank5 | 0xC0000000 | 最大 256MB | 外部 SDRAM | 否(需过 MPU) |
这张表里有两个点最容易翻车。第一,DTCM 不能给 DMA 用,很多人在 DTCM 上开一个数组喂给 SPI,然后对着逻辑分析仪怀疑人生;第二,QSPI 和 FMC 都是"映射窗口",不是真内存,访问延迟比片内高一个数量级,把它们当普通 RAM 用之前必须过 MPU 配置属性。我一般会把主栈和 GC 的根对象留在 DTCM,把大块 ByteArray、帧缓冲放到 SDRAM,中间的 AXI SRAM 留给需要被 DMA 访问的缓冲。
还有一个细节:QSPI 映射窗口的地址是固定的 0x90000000,但只有在使能内存映射模式之后才可读,普通 SPI 模式下读这个地址会直接 BusFault。源码里外扩 flash 的驱动分成两条路径——一条走命令式读写(用于擦写文件系统),一条走映射读(用于 XIP 执行或直接读资源),两条路径的切换要保证当前没有正在进行的传输,否则控制器状态机会卡死。
2.2 时钟树:QSPI 和 FMC 的内核时钟必须单独分配
H743 跑到 400MHz 以上要开 VOS0 电压等级,而 QSPI 和 FMC 的外设内核时钟并不是简单地从 HCLK 分频下来的,它们各自有一个多路选择器。常见做法是:PLL1 出 CPU 时钟,PLL2 专门出 200MHz 给 FMC 和大部分外设,PLL3 或者 PLL2R 分一路给 QSPI。这样做的原因是 FMC 的 SDRAM 时序全是按 FMC 时钟周期换算的,一旦你为了省事把它挂在会动态变频的 HCLK 上,调好的时序在降频时全部失效。
板级目录里最小可用的骨架大概长这样:
// ports/stm32/boards/H743_EXT/mpconfigboard.h #define MICROPY_HW_BOARD_NAME "H743-EXT-32M" #define MICROPY_HW_MCU_NAME "STM32H743" // 外部晶振,板子上实际焊的是 25M,写成别的值串口全是乱码 #define MICROPY_HW_HSE_VALUE (25000000) // 时钟:CPU 480MHz,FMC/QSPI 内核 200MHz #define MICROPY_HW_CLK_PLLM (5) // 25M / 5 = 5MHz 参考 #define MICROPY_HW_CLK_PLLN (192) // 5M * 192 = 960MHz VCO #define MICROPY_HW_CLK_PLLP (2) // 960 / 2 = 480MHz CPU #define MICROPY_HW_CLK_PLLQ (4) // 960 / 4 = 240MHz #define MICROPY_HW_CLK_PLLR (4) // 关掉用不到的模块,先把固件压到 1.5MB 以内再谈别的 #define MICROPY_PY_NETWORK (0) #define MICROPY_PY_SSL (0) #define MICROPY_PY_BLUETOOTH (0)这几个宏看起来平平无奇,实际每个都有对应的坑。MICROPY_HW_HSE_VALUE写错是最经典的:编译一切正常,烧进去串口吐出来全是问号,因为 UART 的波特率是从错误的时钟推算出来的。PLLP设成 2 之后 CPU 是 480MHz,但前提是电压等级已经是 VOS0,如果板子的电源管理配置还停在 VOS1,芯片会直接跑飞或者进 HardFault。至于关模块那几行,纯粹是被 2MB 内部 Flash 逼的——先把固件体积压下来,才有空间给后面的文件系统和脚本。
改完这些,编译命令是:
# 先同步子模块,否则 littlefs / extmod 的头文件会缺 make -C ports/stm32 submodules # 板级目录名要和 boards/ 下的文件夹完全一致 make -C ports/stm32 BOARD=H743_EXT -j8 # 产物在 build-H743_EXT/firmware.hex 和 firmware.dfu ls -lh ports/stm32/build-H743_EXT/firmware.*编译报错里最常见的是region RAM overflowed,说明堆还是按默认大小算的,得在 board 目录里加上MICROPY_HW_HEAP_...相关的覆盖;另一种是undefined reference to qspi_...,那基本是子模块没拉全。
2.3 板级引脚表与 QSPI/SDRAM 的选型判断
pins.csv这份文件决定了 MicroPython 里machine.Pin("PB6")能不能用,格式是"引脚名,端口,序号,复用功能"。外扩 flash 的 CS、CLK、IO0~IO3 这六根线必须在这里出现,否则你没法在 REPL 里手动拉 CS 做调试。我一般会把 SDRAM 的控制线也写进去,虽然它们运行时被 FMC 独占,但在排查"到底有没有信号出来"的时候,能把引脚切回 GPIO 用万用表量一下电平,比盯着示波器猜强得多。
选型上有个判断标准值得说清楚:如果板子上焊的是 32MB 容量的 QSPI NOR,那它的擦除粒度大概率是 4KB 扇区 + 64KB 块。这个粒度和 littlefs 的 block size 必须匹配,配成 4KB 是最稳的,配大了浪费空间,配小了每次 GC 都要擦一堆扇区,写一个 100KB 的文件能卡你三秒。至于 SDRAM,32MB 容量、16 位位宽的颗粒最常见,行地址 13 位、列地址 9 位、4 个 bank,这组参数决定了后面 FMC 的RowBitsNumber、ColumnBitsNumber、BankNumber三个字段怎么填,填错的表现是只有一半地址能读写,或者每次复位之后随机几个字节变成 0xFF。
3. 32M SQPI Flash 落地:从扇区驱动到 littlefs 挂载根目录
外扩 flash 在 MicroPython 里不是"挂上去就完事",它有三种用法,选错了后面所有工作都要重来。第一种是把它整个做成块设备,直接当根文件系统,import os之后/就在外部 flash 上;第二种是不做文件系统,只开内存映射,用来放字体、模型、图片这类只读资源;第三种是切成两半,前一半给 littlefs,后一半留给 raw 分区做参数存储或者固件备份。这份源码走的是第一种,也是最适合"板子要长期跑脚本"的场景,因为脚本和配置都能通过 REPL 直接改,不用重新烧固件。
3.1 三种接法的取舍:为什么根文件系统该落在外部 flash
内部 2MB Flash 里,固件本身占掉 1.4~1.8MB,能留给内部文件系统的往往只有几百 KB,而这个内部文件系统还跟代码共享同一个 bank,写文件时 CPU 取指会被总线仲裁拖慢。把根文件系统挪到外部 QSPI flash 之后,内部 Flash 只负责放固件,读写文件走独立的 QSPI 通道,两边互不干扰,这是最直接的好处。
代价也很明确:外部 flash 的随机读延迟比内部高很多,import一个模块要经过 QSPI 控制器、可能还要过 Cache,如果模块很多,启动时间会肉眼可见地变长。我的处理办法是把启动时必须加载的模块预编译成 .mpy 塞进固件内部,只把业务脚本放在外部文件系统上。这样启动时读的都是内部 flash,只有真正跑业务逻辑才去碰外部存储。
第二种"只做映射读"的方案适合资源型应用——比如你要从 flash 里直接读一张 800x480 的位图做显示,映射模式下可以用指针直接访问,不需要先读进 RAM。但这种模式下没办法执行写操作,也不可能跑 littlefs,所以它没法替代文件系统,只能作为补充。
3.2 块设备驱动与 littlefs 挂载:扇区、块大小和缓存怎么配
板级配置里跟外部 flash 相关的宏,核心是这几个:
// 32MByte = 256Mbit,注意这里是 bit 数,写错了文件系统大小直接对不上 #define MICROPY_HW_SPIFLASH_SIZE_BITS (256 * 1024 * 1024) // 四线模式下这几根线必须全部配好,少一根会退化成单线,速度掉到 1/4 #define MICROPY_HW_QSPI_CS (pin_B6) #define MICROPY_HW_QSPI_CLK (pin_B2) #define MICROPY_HW_QSPI_IO0 (pin_D11) #define MICROPY_HW_QSPI_IO1 (pin_D12) #define MICROPY_HW_QSPI_IO2 (pin_E2) #define MICROPY_HW_QSPI_IO3 (pin_D13) // 打开块设备作为根文件系统,这一行决定 os.statvfs('/') 报的是内部还是外部 #define MICROPY_HW_BDEV_IS_SPIFLASH (1) // 扇区擦除 4KB,跟芯片手册的 Sector Erase 对齐 #define MICROPY_HW_SPIFLASH_SECTOR_SIZE (4096)MICROPY_HW_SPIFLASH_SIZE_BITS这个宏名里带 BITS,单位就是比特,32M 字节的颗粒要写 256×1024×1024。我第一次移植时按字节写成了 32×1024×1024,结果os.statvfs('/')报出来 4MB,写满之后再写就报OSError: [Errno 28] ENOSPC,查了半天才发现是单位问题。这种宏命名在不同版本的 ports/stm32 里改过几轮,源码里如果搜不到,就去ports/stm32/spibdev.c里看当前版本用的是哪一套,别硬套网上的老配置。
块设备层封装好之后,文件系统的挂载是自动的,烧进去第一次上电如果没有文件系统,MicroPython 会自己格式化。想手动确认,在 REPL 里跑:
import os # 看根文件系统容量和块大小,确认挂在外部 flash 上 st = os.statvfs('/') print("block size:", st[0]) print("total MB:", st[2] * st[0] // (1024 * 1024)) print("free MB:", st[3] * st[0] // (1024 * 1024)) # 实测一把擦写速度,写入块越大越能看出 QSPI 的极限 import time data = b"x" * 65536 t0 = time.ticks_ms() with open("/test.bin", "wb") as f: for _ in range(16): f.write(data) print("write 1MB:", time.ticks_diff(time.ticks_ms(), t0), "ms")这段脚本里st[0]是块大小,st[2]是总块数,两个一乘才是容量。写入测试故意用了 64KB 的块循环 16 次而不是一次性写 1MB,因为一次性写大 buffer 会先在 RAM 里拼好再下发,测出来的是 RAM 带宽不是 flash 带宽;分块写更接近真实业务场景,也能顺带暴露 littlefs 在 GC 时的卡顿。
3.3 编译期开关与版本差异:宏名对不上时怎么找
MicroPython 的 STM32 端口在不同版本之间,外部 flash 相关宏的命名改过不止一次。有的版本用MICROPY_HW_SPIFLASH_*,有的版本把它抽象成了通用的 bdev 接口,board 目录下会多一个bdev.c自己做mp_blockdevice_...的注册。碰上这种情况,判断方法很简单:在ports/stm32/下搜MP_QSTR_加上你的设备名,或者直接搜bdev,看默认实现是怎么拿 board 宏的。
我自己踩过一次:照着一份老配置写好了MICROPY_HW_SPIFLASH_CS,编译不报错,运行起来os.statvfs('/')报的是内部 flash 的 512KB,说明新版本压根没读这个宏,走了默认的内部实现。解决方式是去看boards/下跟你芯片最接近的那个官方板目录,抄它的mpconfigboard.mk里SRC_C那几行,把自定义的 bdev 源文件加进编译列表。这个动作本身就是一次很好的学习——你能看到官方是怎么处理"有些板子外扩、有些没有"这种分支的。
4. 32M SDRAM 落地:FMC 时序换算、堆迁移与 Cache 一致性
SDRAM 比 QSPI flash 难,难在它没有"擦写"这种离散操作,全是连续时序,任何一个参数差一个周期,表现都是随机的。有的板子上电跑十分钟才出错,有的跑一次内存扫描就挂,还有的只在温度低的时候挂——这类问题靠猜是猜不出来的,只能把时序参数一项项按手册算清楚。这一章的顺序是:先算时序,再配 MPU,最后才动堆,顺序反了会多花两倍时间。
4.1 FMC 时序参数换算:从手册的 ns 到寄存器的周期数
SDRAM 的时序参数在芯片手册里都是纳秒,FMC 的寄存器填的是时钟周期数,换算公式是寄存器值 = ceil(纳秒 / 时钟周期) - 1。假设 FMC 内核时钟是 200MHz,一个周期 5ns,我通常按下面这张表填:
| 参数 | 手册典型值 | 换算过程 | 寄存器值 |
|---|---|---|---|
| tRCD 行到列延迟 | 18ns | ceil(18/5)-1 | 3 |
| tRP 预充电时间 | 18ns | ceil(18/5)-1 | 3 |
| tWR 写恢复时间 | 15ns | ceil(15/5)-1 | 2 |
| tRC 行周期 | 60ns | ceil(60/5)-1 | 11 |
| tRFC 自刷新周期 | 42ns | ceil(42/5)-1 | 8 |
| tXSR 退出自刷新 | 72ns | ceil(72/5)-1 | 14 |
| tMRD 模式寄存器设置 | 2 tCK | 直接填 | 1 |
除了这几个,还有两个容易漏的:刷新计数COUNT = (64ms / 行数) × 时钟频率,32MB 颗粒通常是 8192 行,算下来(64/8192)ms × 200MHz ≈ 1562,填 1562 或者保守一点填 1500;以及 CAS 延迟,常见颗粒支持 2 或 3,填 3 更稳但慢一点,填 2 需要确认颗粒在这个频率下确实支持。填进初始化代码大概是这样:
// SDRAM 初始化时序,参数全部按上面的表换算得到 FMC_SDRAM_TimingTypeDef timing = {0}; timing.LoadToActiveDelay = 3; // tMRD timing.ExitSelfRefreshDelay = 14; // tXSR timing.SelfRefreshTime = 8; // tRFC timing.RowCycleDelay = 11; // tRC timing.WriteRecoveryTime = 2; // tWR timing.RPDelay = 3; // tRP timing.RCDDelay = 3; // tRCD // 刷新计数,8192 行 / 64ms,200MHz 下算出来约 1562 FMC_SDRAM_ProgramRefreshRate(FMC_Bank, 1562); // 上电后必须发一次预充电全部 bank + 至少 8 次自动刷新,否则内容全是随机值 // 这段顺序错了,后面跑多久都是玄学注释里那句"顺序错了就是玄学"不是夸张。SDRAM 上电之后必须先发预充电命令把所有 bank 关掉,再连发 8 次以上自动刷新,然后才能写模式寄存器。少发几次刷新的表现是:前几 KB 读写正常,越往后越容易读到随机值,而且每次上电出错的地址都不一样。我第一版就是因为刷新次数少写了几次,跑了一个礼拜才定位到。
4.2 堆迁移:哪些数据该搬进 SDRAM,哪些必须留在片内
堆搬到 SDRAM 不是把起始地址改一改就完事。MicroPython 的 GC 是标记清除,遍历的时候要频繁访问对象头,如果所有对象都在 SDRAM 上,每次 GC 的耗时能翻两三倍。我的做法是保留默认堆在 AXI SRAM,另外在 SDRAM 上开一块"大对象区",专门给bytearray、帧缓冲、解析大 JSON 用。
具体到代码,board 目录里覆盖堆的边界:
// 主堆仍然放 AXI SRAM,保证 GC 遍历速度 #define MICROPY_HW_HEAP_BEGIN ((void *)0x24000000) #define MICROPY_HW_HEAP_END ((void *)(0x24000000 + 480 * 1024)) // SDRAM 单独暴露给 Python 层,用 machine.mem32 或者自定义模块访问 #define MICROPY_HW_SDRAM_BASE (0xC0000000) #define MICROPY_HW_SDRAM_SIZE (32 * 1024 * 1024) // 上电做一次全片读写测试,出问题早点发现比跑一半挂掉强 #define MICROPY_HW_SDRAM_STARTUP_TEST (1)MICROPY_HW_SDRAM_STARTUP_TEST这个开关强烈建议打开。它会在启动时对整片 SDRAM 做一次写-读-校验,代价是启动慢几百毫秒,收益是时序配错时你立刻就知道,而不是等到业务跑到一半才发现某块地址读出来是 0。量产固件里可以关掉,但调试阶段别省这一步。
SDRAM 暴露给 Python 的方式,常见做法是写一个极简的 C 模块,把基地址和长度包成memoryview,或者干脆用machine.mem32按地址读写。前者安全、后者灵活,我一般两个都留,调试用后者,业务用前者。
4.3 Cache 与 MPU:M7 上最容易被忽略的一致性坑
Cortex-M7 有 D-Cache,开了之后 SDRAM 的读写速度能差出三倍,但代价是所有经过 DMA 的缓冲区都要手动维护一致性。MicroPython 本身不太用 DMA,真正的问题出在你自己加的外设上——比如你用 SPI 加 DMA 往屏幕上刷一张存在 SDRAM 里的图,CPU 写完图之后数据还在 Cache 里没落到 SDRAM,DMA 读到的是旧内容,屏幕上是花屏。这种问题在逻辑分析仪上什么都看不出来,因为总线上的数据确实发出去了,只是发的是错的。
处理办法是给 SDRAM 区域配 MPU,分两段:给普通数据用的写回写分配区域,和给 DMA 缓冲用的写穿透或者不缓存区域。写穿透会损失一点性能,但省掉了每次传输前后手动 clean/invalidate 的麻烦,对于刷新率不高的场景完全够用。如果确实要压榨性能,那就得在每次 DMA 传输前后加上SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr,而且要保证地址和长度按 32 字节对齐,不对齐的话 invalidate 会把相邻的有用数据一起丢掉,这个坑比花屏更难查。
5. 避坑排查:五个让 H743 外扩存储翻车的现场记录
下面这五条全是我在这块板子上真实撞过的,顺序按"出现频率从高到低"排。每条都按现象、原因、解决三段写,遇到问题可以对着顺序一条条排除,比漫无目的地翻代码快得多。
5.1 上电就 HardFault,连串口都没输出
现象:烧完固件,复位之后没有任何串口输出,用调试器挂上去看,PC 停在一个跟存储地址有关的异常处理里。
原因:SDRAM 的控制器时钟还没使能,初始化代码就已经往 0xC0000000 写数据了。H743 上 FMC 的时钟挂在 AHB3 上,必须在SystemClock_Config之后、SDRAM 初始化之前显式开;另一个常见原因是 MPU 没配,访问外部存储区域默认属性是 Strongly-ordered,任何非对齐访问都会触发异常。
解决:把 FMC 时钟使能放在时钟配置的最后一步,SDRAM 初始化之前加一句__HAL_RCC_FMC_CLK_ENABLE();MPU 配置用HAL_MPU_ConfigRegion把 0xC0000000 起 32MB 设成 Normal、Non-shareable、Write-back,并且这一段的DisableExec要设成允许执行,否则放在这里的代码跑不了。
5.2 文件系统能挂上,写大文件之后目录损坏
现象:小文件读写都正常,一旦写入超过几百 KB,重启之后os.listdir('/')报错,或者文件长度变成一个离谱的数字。
原因:MICROPY_HW_SPIFLASH_SECTOR_SIZE填的和颗粒实际的擦除粒度不一致。填成 64KB 的时候,littlefs 以为一个块就是 64KB,做 GC 时会按这个粒度搬数据,但硬件擦除的最小单位是 4KB,中间的映射关系全乱了。
解决:查颗粒手册的 Sector Erase 那一栏,确认是 4KB 还是 64KB,填最小的那个粒度。改完宏之后必须整片擦除一次再重新格式化,因为旧的文件系统元数据是按错误块大小写的,不清掉会一直报错。
5.3 堆搬到 SDRAM 之后偶发数据错乱
现象:跑一段时间之后,某个全局对象的值莫名变成 0,或者字符串里出现乱码,重启之后又正常。
原因:Cache 一致性。CPU 写的数据还在 D-Cache 里,DMA 或者另一条总线上的外设读的是 SDRAM 里的旧值。另一种可能是 SDRAM 时序里的 tWR 或 tRP 填小了,在高温或者批次不同的颗粒上刚好临界。
解决:先关 Cache 跑一遍,如果问题消失,就确认是一致性问题,按 4.3 的 MPU 方案配;如果关 Cache 还在错,那就是时序问题,把所有时序参数各加 1 个周期再试,能稳定就说明原来那组是临界的,别心疼那点性能。
5.4 烧写成功但程序不跑,调试器显示停在 0x90000000
现象:DFU 或者 SWD 烧写都提示成功,复位之后程序不执行,调试器读 PC 发现是 0x90000000 开头的地址。
原因:链接脚本里把一部分代码段放到了 QSPI 的映射区,但启动时 QSPI 还没进入内存映射模式,CPU 从那里取指直接取到无效数据。
解决:要么把代码全部放回内部 Flash,QSPI 只做数据区;要么在启动代码的最前面加一段"从内部 Flash 运行的 QSPI 初始化",初始化完再跳转到外部。后者对时序要求很高,没有特殊需求不建议做。
5.5 REPL 输出乱码或者干脆没输出
现象:串口工具能连上,但收到的全是不可打印字符,或者一个字节都没有。
原因:换了 HSE 频率或者改了 PLL 分频之后,UART 的波特率计算基准变了,但MICROPY_HW_HSE_VALUE没跟着改;另一种是板子上的 USB 虚拟串口和物理串口同时使能,固件默认输出走的是另一个。
解决:先把MICROPY_HW_HSE_VALUE和板上晶振核对一遍,再看mpconfigboard.h里MICROPY_HW_UART_REPL和MICROPY_HW_USB_...的开关,确保你接的那个口就是固件输出的那个口。这两个都对了还是乱码,就把波特率降到 115200 试一次,排除时钟本身的偏差。
6. 验收与进阶:三组基准数据判断这套移植到底成没成
移植完不等于能用,我一般会跑三组数据来验收,每组都有明确的通过标准,达不到就说明还有地方没配对。
第一组是存储容量和稳定性:
import os, time # 容量核对,32M 的 flash 格式化之后可用应该在 30MB 上下 st = os.statvfs('/') print("flash total MB:", st[2] * st[0] // (1024 * 1024)) # 写满再删,验证 GC 和目录结构没坏 chunk = b"a" * 32768 t0 = time.ticks_ms() for i in range(200): with open("/f%d.bin" % i, "wb") as f: f.write(chunk) print("write 6.4MB:", time.ticks_diff(time.ticks_ms(), t0), "ms") for i in range(200): os.remove("/f%d.bin" % i) print("free after clean:", os.statvfs('/')[3] * os.statvfs('/')[0] // (1024 * 1024))通过标准是写入过程中不报 ENOSPC,删除之后可用空间回到接近初始值。如果删完空间回不来,说明 littlefs 的 GC 没正常工作,多半还是块大小配错了。
第二组是 SDRAM 带宽,直接反映时序和 Cache 配得对不对:
import time # 用 mem32 直接扫 SDRAM,测纯写带宽 base = 0xC0000000 n = 1024 * 1024 # 1MB 个字 t0 = time.ticks_us() for i in range(0, n, 4): import machine machine.mem32[base + i] = i dt = time.ticks_diff(time.ticks_us(), t0) print("write 4MB:", dt / 1000, "ms,", 4 * 1024 * 1024 / (dt / 1e6) / 1e6, "MB/s")32 位总线、200MHz FMC 时钟下,理论峰值是 400MB/s,实测能到 150~250MB/s 就算正常,低于 80MB/s 一般是没开 Cache 或者时序填得太保守。
第三组是 GC 耗时,这决定了板子能不能跑长时间业务:
import gc, micropython gc.collect() micropython.mem_info() # 反复分配释放,看 GC 耗时是否稳定 import time t0 = time.ticks_ms() for _ in range(200): a = bytearray(4096) del a gc.collect() print("200 次 GC:", time.ticks_diff(time.ticks_ms(), t0), "ms")单次 GC 超过 20ms 就值得优化了,通常是因为堆里有大量小对象,或者堆被放在了 SDRAM 上。
进阶方向有两个:一是固件裁剪,把manifest.py里用不到的模块去掉,把业务脚本预编译成 .mpy 打进固件,启动速度能提升一倍以上;二是把这套板级目录整理成模板,新板子只要改 pins.csv 和时序表就能复用。我现在的习惯是,从那以后每次拿到一块新板子,不管多急,都强制走一遍 SDRAM 全片读写、QSPI 擦写循环、REPL 连续输出这三件套,全程不到五分钟,但能省掉后面几天的返工。源码包里带的就是这套已经跑通的板级目录和时序表,拿过去对着自己的颗粒改几个数就能编译,希望帮到你。
本文还有配套的精品资源,点击获取