FEATURED · 精选文章

树莓派Pico存储架构详解:ROM/SRAM/Flash物理布局与访问机制

发布时间 / 2026/9/11 10:31:34
来源 / 创域科博编辑部
栏目 / 资讯中心
树莓派Pico存储架构详解:ROM/SRAM/Flash物理布局与访问机制 1. 为什么得先“扒透”Pico的存储结构这不是炫技是踩坑前的必修课树莓派 Pico 不是普通单片机它没有传统意义上的“操作系统”也没有 Linux 那套抽象层兜底。你写的每一行 C 代码、每一段 MicroPython 字节码最终都得直面 ROM、SRAM、Flash 这三块物理芯片——它们不是概念是实实在在会烧坏、会跑飞、会读错的硅基实体。我第一次用 Pico 控制舵机时明明逻辑没问题舵机却抖得像帕金森查了三天才发现是全局变量被误塞进了 Flash 区域每次写入都触发了 Flash 擦除周期导致中断响应延迟飙升。后来在调试 STM32 F429 外扩 SRAM 时又遇到过类似问题变量地址映射错位数据直接写进了 QSPI Flash 的命令寄存器整块 Flash 被锁死连 ST-Link 都识别不了。这些都不是理论错误是硬件级的硬伤。所以“扒透”不是为了显摆懂底层而是为了让你写的代码能稳稳地跑在那块 2MB 的 Flash 上、那块 264KB 的 SRAM 里、那块 128KB 的 ROM 中。尤其当你开始做固件升级、OTA 更新、或者需要把大段波形数据固化进 Flash 时如果连 QSPI Flash 的页擦除大小4KB、扇区擦除大小64KB、以及 ROM 中 BootROM 的启动流程都搞不清轻则下载失败报错error: flash download failed - target dll has been cancelled重则变砖。这教程不讲虚的不堆术语只讲你焊板子、写代码、烧固件时真正会卡住的点——比如为什么memcpy不能随便拷贝到 Flash 地址、为什么全局变量放 SRAM 比放 Flash 快 100 倍、为什么 QSPI 和 SPI 看似相似实则协议天差地别。适合所有正在用 Pico 做真实项目的开发者无论你是用 C 写裸机驱动还是用 MicroPython 做 IoT 终端甚至只是想搞懂手头那个“疑似黑 ROM 设备 IP”背后到底发生了什么。2. 存储架构全景图三块芯片如何分工协作一张图看懂物理边界与访问路径Pico 的存储系统不是一块板子上随便贴三颗芯片那么简单它是一套经过精密设计的分层访问体系。核心是 RP2040 这颗 SoC它内部集成了 CPU、总线矩阵、DMA 控制器而外部连接着三类独立存储器件内置 ROM、片内 SRAM、外置 QSPI Flash。它们之间不存在“共享总线”的幻觉而是通过不同总线、不同控制器、不同时序规则严格隔离。理解这个物理拓扑是避免后续所有地址冲突、访问超时、数据错乱的前提。2.1 ROM只读的“出厂说明书”藏在芯片硅片里Pico 的 ROM 是固化在 RP2040 芯片硅基内部的掩膜 ROM容量固定为 128KB地址空间从0x00000000开始。它不是可擦写的 Flash也不是掉电丢失的 RAM它是芯片制造时就刻进去的“硬编码”。里面存的不是你的程序而是 BootROM —— 也就是芯片上电后第一段执行的代码。它的作用非常具体检测 USB 是否连接、判断是否进入 BOOTSEL 模式、初始化 QSPI 控制器、从外部 Flash 加载 UF2 固件、校验签名、跳转到用户代码入口。你永远无法修改它也无需关心它怎么运行但你必须知道它的存在会影响你的启动流程。比如当你用picotool强制进入 BOOTSEL 模式时实际就是让 BootROM 放弃从 Flash 启动转而监听 USB 接口而当你看到开发板 LED 快闪三次说明 BootROM 已成功加载 UF2 并移交控制权。很多初学者误以为 ROM 是“系统存储”其实它更像一本印在芯片上的《使用手册》你只能阅读不能涂改。2.2 SRAMCPU 的“办公桌”快但小掉电即失Pico 的 SRAM 分为两块256KB 的 SRAM0 和 8KB 的 SRAM1合计 264KB地址空间从0x20000000开始。这是 CPU 执行指令、存放变量、分配栈空间的唯一高速区域。它的特点是读写速度极快纳秒级延迟支持字节/半字/全字随机访问但容量有限且掉电数据全丢。关键在于SRAM 是唯一允许 CPU 直接执行代码XIP和读写数据的内存。你定义的int count 0;、函数局部变量、malloc 分配的堆内存全部落在这里。这也是为什么 STM32 F429 全局变量可以放在外扩 SRAM —— 因为外扩 SRAM 通过 FSMC 总线接入其访问时序被配置成等效于片内 SRAM。但在 Pico 上没有外扩 SRAM 接口所有变量必须挤在这 264KB 里。一旦你声明一个uint8_t big_buffer[200000];它就会吃掉近 200KB SRAM留给栈和堆的空间所剩无几极易触发 HardFault。我实测过当 SRAM 使用率超过 90%USB CDC 串口通信就开始丢包因为 USB 中断服务程序没地方压栈了。2.3 Flash你的“硬盘”慢但大需擦除才能写Pico 板载的是 Winbond W25Q80DV 8MB QSPI Flash 芯片通过四线 QSPI 总线连接到 RP2040 的 GPIO0–3。注意它不是 SPI Flash也不是普通的 NAND/NOR Flash而是专为高速 XIPeXecute In Place优化的 Quad SPI Flash。它的物理特性决定了所有操作逻辑读取支持高速连续读最高 104MHz可直接从 Flash 地址取指令执行XIPMicroPython 解释器就靠这个实现“无需复制到 RAM 就能运行”。写入不能像 SRAM 那样直接*ptr 0xFF;必须先擦除再写入。最小擦除单位是页Page大小为 256 字节常用擦除单位是扇区Sector大小为 4KB最大擦除单位是块Block大小为 64KB。寿命每个扇区擦写次数约 10 万次频繁写日志或状态标志必须做磨损均衡否则某扇区提前报废。地址映射Flash 地址空间从0x10000000开始但 BootROM 只认前 2MB即0x10000000–0x101FFFFF为有效固件区。超出部分虽能读写但无法被 BootROM 加载。提示很多人混淆 QSPI 和 SPI。SPI 是标准四线SCK, MOSI, MISO, CSQSPI 在此基础上增加了三根数据线IO0–IO3支持四线并行传输带宽翻倍。RP2040 的 QSPI 控制器还内置 FIFO 和 DMA能自动处理命令序列比软件模拟 SPI 效率高一个数量级。这也是为什么 Pico 的 UF2 下载速度能达到 400KB/s而普通 SPI Flash 通常只有 50KB/s。3. 核心细节深挖ROM/SRAM/Flash 的地址空间、访问权限与编译链接真相光知道三块芯片在哪还不够你得清楚编译器、链接脚本、启动代码是如何把你的 C 代码“塞”进这三块物理空间的。这一步出错轻则程序跑飞重则 Flash 锁死。下面拆解最常被忽略的三个硬核细节。3.1 地址空间全景与内存映射表不是所有地址都能随便读写RP2040 的地址空间是 32 位但并非所有地址都对应有效硬件。官方《RP2040 Datasheet》第 2.3.1 节给出了完整映射我把它浓缩成开发者最需关注的五段地址范围大小名称访问特性典型用途0x00000000–0x0001FFFF128KBROM只读BootROM 代码0x20000000–0x20041FFF264KBSRAM读写用户代码、变量、栈、堆0x20042000–0x20042FFF4KBPeripheral Block 0读写GPIO、UART、I2C 等外设寄存器0x40000000–0x4007FFFF512KBQSPI Flash (XIP)只读XIP 模式MicroPython 固件、C 程序代码段0x10000000–0x107FFFFF8MBQSPI Flash (Direct)读写需 QSPI 控制器用户数据存储、固件备份关键陷阱就藏在这里ROM 地址不可写尝试向0x00001000写数据CPU 会触发 BusFault程序立即死机。SRAM 地址不能执行代码虽然0x20000000开始是 SRAM但 CPU 默认禁止从此区域取指NX bit除非你手动关闭 MPU 或配置特定属性。这就是为什么裸机 C 程序的.text段必须链接到 Flash 地址0x10000000而.data和.bss段才复制到 SRAM。QSPI Flash 的双重身份0x40000000是 XIP 映射地址CPU 可直接从此取指0x10000000是物理地址需通过 QSPI 控制器发送命令读写。两者指向同一块 Flash但访问方式、速度、权限完全不同。MicroPython 固件就放在0x40000000而你用pico-sdk的flash_driverAPI 写数据时操作的是0x10000000。3.2 编译链接脚本.ld 文件你的代码如何被“分发”到三块芯片Pico SDK 默认使用pico_sdk/src/boards/include/boards/pico.h中定义的链接脚本pico_standard_linker_script.ld。它不是黑盒而是明确告诉链接器“把代码放 Flash把变量放 SRAM把只读数据放 Flash”。我们来逐段解析关键片段MEMORY { FLASH (rx) : ORIGIN 0x10000000, LENGTH 2M /* 注意仅前2MB被BootROM识别 */ RAM (rwx) : ORIGIN 0x20000000, LENGTH 264K } SECTIONS { .text : { *(.text.startup) /* 启动代码必须放在Flash开头 */ *(.text) /* 主程序代码 */ *(.rodata) /* 只读数据如字符串常量、const数组 */ } FLASH .data : { *(.data) /* 初始化变量编译时存Flash启动时复制到SRAM */ *(.data.*) /* 同上 */ } RAM AT FLASH /* 关键.data段内容存Flash加载地址是RAM */ .bss : { *(.bss) /* 未初始化变量启动时清零只占SRAM空间 */ *(.bss.*) /* 同上 */ *(COMMON) /* 全局未初始化符号 */ } RAM }这段脚本揭示了三个核心事实.text和.rodata强制放在 Flash因为它们是只读的且 Flash 容量大适合存放代码和常量。MicroPython 的字节码、C 的函数体、Hello World字符串全在这里。.data是“双地址段”它在 Flash 里存一份初始值AT FLASH上电后 BootROM 或 startup code 会把它 memcpy 到 SRAM 的指定位置 RAM。这就是为什么你声明int x 123;编译后123存在 Flash 里运行时x的值才出现在 SRAM。.bss只占 SRAM 空间它不占用 Flash启动时由 C runtime 自动 memset 为 0。int y;这样的未初始化变量就归这里管。注意如果你用__attribute__((section(.my_flash_data)))把自定义数据段强行塞进.text它确实会进 Flash但你必须自己负责读取——因为链接器不会为你生成 memcpy 代码。很多 OTA 升级失败就是因为开发者把新固件镜像放在自定义 Flash 段却忘了在 bootloader 里手动 copy 到 RAM 执行。3.3 启动流程与 BootROM 的真实行为从上电到 main() 的每一步Pico 的启动不是“上电→跑 main()”这么简单而是一场由 BootROM 主导的精密接力赛。整个过程耗时约 10–20ms每一步都可能失败上电复位RP2040 内部复位电路拉低 RESET 引脚CPU 进入复位状态。BootROM 初始化CPU 从0x00000000ROM 起始取第一条指令开始执行 BootROM。它首先配置 PLL、设置系统时钟默认 12MHz然后检测RUN和BOOTSEL引脚电平。模式判定若BOOTSEL为低按键按下强制进入 USB Boot 模式枚举为 Mass Storage 设备等待 UF2 文件拖入。若BOOTSEL为高则检查 Flash 地址0x10000000开头是否为有效 UF2 签名0x0A 0x0A 0x0A 0x0A。若无效LED 快闪三次后停机若有效继续下一步。UF2 解析与加载BootROM 读取 UF2 文件头提取 payload 数据通常是 ARM Cortex-M0 二进制将其解密如有、校验 CRC然后写入 Flash 的0x10000000起始地址。跳转执行加载完成后BootROM 读取 Flash 中0x10000000处的向量表首项SP 初始值再读取第二项Reset Handler 地址然后BX跳转过去。此时控制权移交给你写的main()函数。这个流程解释了为什么error: flash download failed - target dll has been cancelled会频繁出现如果 UF2 文件损坏CRC 错BootROM 拒绝加载LED 闪烁异常如果 Flash 某扇区已损坏擦写超限写入失败picotool报错如果你用 OpenOCD 烧录时QSPI 时钟配置错误如qspi_clk_div 2导致频率超 133MHzBootROM 读取失败直接卡死。4. 实操全流程从查看 Flash ID 到安全擦写手把手完成一次底层存储操作理论讲完现在动手。下面以“安全更新 Pico 的 Flash 数据区”为例带你走一遍完整的底层操作链。所有命令基于官方pico-sdk和picotool无需额外工具。4.1 第一步确认 Flash 型号与 ID避免“认错媳妇”不同批次 Pico 可能用不同厂商 FlashWinbond、GigaDevice、AdestoID 不同擦除/写入时序也略有差异。必须先确认否则flash_driver可能发错命令。# 1. 进入 BOOTSEL 模式按住 BOOTSEL 键再按 RESET # 2. 此时 Pico 会挂载为 RPI-RP2打开终端 picotool info # 输出示例 # Device: RP2040 Bootrom v1.0 # Board: Raspberry Pi Pico (RP2040) # Flash: W25Q80DV (Winbond) - ID: 0xEF40140xEF4014是 Winbond W25Q80DV 的 JEDEC ID。其中0xEF是厂商 IDWinbond0x40是设备类型Serial Flash0x14是容量码8Mbit 1MB注意W25Q80DV 是 8Mbit但 Pico 板载的是 8MB 版本ID 为0xEF6017。如果显示0xC84017则是 GigaDevice GD25Q80C。二者命令集基本兼容但某些高级功能如 Quad Enable寄存器位不同。实操心得picotool info只能读 JEDEC ID无法读 SFDPSerial Flash Discoverable Parameters表。若需精确时序参数如erase_sector_time_ms必须用逻辑分析仪抓 QSPI 波形或查阅对应 Flash 的 datasheet。我曾因误用 GD25Q80C 的时序参数烧录 W25Q80DV导致擦除超时Flash 进入 Busy 状态长达 30 秒。4.2 第二步读取 Flash 内容验证当前状态用picotool读取前 1KB确认是否为空白全0xFF或已有数据# 读取 Flash 地址 0x10000000 开始的 1024 字节保存为 dump.bin picotool read 0x10000000 1024 dump.bin # 查看十六进制内容 xxd dump.bin | head -n 5 # 若输出全是 ff则 Flash 空白若出现 00 01 02...说明已有数据写入4.3 第三步安全擦除目标扇区4KB为写入铺路Flash 写入前必须擦除。切记擦除是“归零”操作会把整个扇区4KB变成0xFF。不能只擦一行必须整扇区。# 擦除地址 0x10000000 开始的 4KB 扇区即扇区 0 picotool erase --sector 0 # 或者指定绝对地址效果相同 picotool erase --range 0x10000000 0x10001000 # 成功后输出Erased 4096 bytes at 0x10000000注意--sector参数是逻辑扇区号从 0 开始。Pico 的 8MB Flash 共有 2048 个扇区8MB / 4KB 2048。擦除命令本质是向 Flash 发送0x20Sector Erase命令QSPI 控制器自动处理时序和等待 BUSY 信号。不要用--chip全片擦除那要 40 秒且会抹掉你的 UF2 固件4.4 第四步写入自定义数据验证读写通路准备一个 256 字节的测试文件test_data.bin可用dd if/dev/urandom oftest_data.bin bs1 count256生成写入扇区开头# 将 test_data.bin 写入 Flash 地址 0x10000000 picotool load test_data.bin --base 0x10000000 # 验证写入结果 picotool read 0x10000000 256 verify.bin cmp test_data.bin verify.bin echo Match! || echo Mismatch!4.5 第五步在 C 代码中安全访问 Flash 数据区写完数据还得让运行中的程序能读它。以下是在main.c中安全读取 Flash 数据的范例#include pico/stdlib.h #include hardware/flash.h // Flash 数据区起始地址避开 UF2 固件区选 0x10100000 #define MY_DATA_ADDR (0x10100000) #define MY_DATA_SIZE 256 void read_my_data(uint8_t *buf) { // 关键禁用中断防止 Flash 操作被中断打断 uint32_t ints save_and_disable_interrupts(); // 从 Flash 直接读取XIP 模式无需 QSPI 命令 memcpy(buf, (const void*)MY_DATA_ADDR, MY_DATA_SIZE); restore_interrupts(ints); } void write_my_data(const uint8_t *buf) { // 写入前必须擦除所在扇区 uint32_t sector_addr MY_DATA_ADDR ~(FLASH_SECTOR_SIZE - 1); // 对齐到扇区边界 flash_range_erase(sector_addr, FLASH_SECTOR_SIZE); // 写入flash_range_program 自动处理页对齐 flash_range_program(MY_DATA_ADDR, buf, MY_DATA_SIZE); } int main() { stdio_init_all(); uint8_t data[256]; // 读取 read_my_data(data); printf(First byte: 0x%02X\n, data[0]); // 修改并写回 data[0] ^ 0xFF; write_my_data(data); // 再读验证 read_my_data(data); printf(After write: 0x%02X\n, data[0]); }这段代码的关键点flash_range_erase和flash_range_program是 Pico SDK 封装的安全 API自动处理扇区对齐、页对齐、BUSY 等待save_and_disable_interrupts()是必须的因为 Flash 擦除/写入期间QSPI 控制器占用总线若此时发生 USB 中断可能导致总线冲突MY_DATA_ADDR必须避开0x10000000–0x100FFFFFUF2 固件区否则写入会破坏固件变砖风险极高。5. 常见问题与排查技巧实录那些让你抓狂的 Flash/SRAM 错误我替你踩过了在 Pico 开发中存储相关错误往往症状诡异、原因隐蔽。下面是我整理的 7 类高频问题附带真实日志、定位方法和一招毙命的解决方案。5.1 问题error: flash download failed - target dll has been cancelled反复出现现象用picotool或 VSCode 插件烧录时进度条走到 90% 突然中断终端报此错Pico LED 常亮不闪。排查思路检查 USB 线缆劣质线缆供电不足导致 QSPI 通信电压不稳。换一根带屏蔽层的短线1m问题消失。检查 Flash 状态用picotool info确认 Flash ID 是否正常。若显示Unknown Flash说明 BootROM 无法读取 ID可能是 QSPI 信号线GPIO0–3接触不良或静电击穿。检查 UF2 文件用file my_app.uf2确认文件未损坏用uf2conv --info my_app.uf2查看 payload CRC 是否匹配。终极方案强制全片擦除慎用。# 进入 BOOTSEL 模式 picotool erase --chip # 等待 40 秒彻底清空 Flash # 重新烧录 UF2 picotool load my_app.uf25.2 问题全局变量值随机变化或printf输出乱码现象定义volatile uint32_t sensor_value 0;主循环中sensor_value但串口打印却是0, 1, 0, 3, 0, 5...。根本原因变量被错误链接到了 Flash 地址而 Flash 不支持快速写入。每次sensor_value实际触发了一次 Flash 页擦除写入耗时数毫秒期间中断被屏蔽导致其他任务如 UART 接收丢数据。验证方法# 查看链接后的 map 文件 arm-none-eabi-gcc -T pico_standard_linker_script.ld ... -Wl,-Mapbuild/app.map grep sensor_value build/app.map # 若地址在 0x10xxxxxx则在 Flash若在 0x20xxxxxx则在 SRAM解决强制变量放 SRAM。// 方法1用 section attribute __attribute__((section(.data.sram))) volatile uint32_t sensor_value 0; // 方法2修改链接脚本新增 SRAM 段 // 在 .ld 文件中添加 // .data_sram (NOLOAD) : { *(.data.sram) } RAM5.3 问题QSPI Flash 读取速度慢XIP 模式下代码执行卡顿现象MicroPython 脚本运行缓慢time.sleep_ms(1)实际延时 10msC 程序中for(i0;i10000;i)循环耗时远超预期。原因分析QSPI Flash 默认工作在 Single I/O 模式SPI带宽仅 ~20MB/s。Pico 的 XIP 模式要求 Quad I/OQSPI带宽可达 ~80MB/s。若 Flash 的 Quad Enable 位未置位CPU 只能降频读取。诊断命令# 读取 Flash 状态寄存器SR1 picotool flash-status --sr1 # 正常应返回 0x02QE bit 置位若为 0x00则 QE 未启用修复步骤# 1. 发送 Write Enable 命令0x06 picotool flash-cmd 0x06 # 2. 写入状态寄存器置位 QE bit0x02 picotool flash-cmd 0x01 --data 0x02 # 3. 验证 picotool flash-status --sr1 # 应返回 0x025.4 问题cannot load flash device descriptionOpenOCD 烧录失败现象用 OpenOCD J-Link 烧录时提示此错target create失败。根源OpenOCD 配置文件rp2040.cfg中指定的 Flash 型号与实际不符。默认是w25q80dv若你用的是 GD25Q80C必须修改。解决# 编辑 openocd/rp2040.cfg # 将 line: flash bank rp2040.flash rp2040 0x10000000 0 0 0 $_CHIPNAME # 改为: flash bank rp2040.flash gd25q80c 0x10000000 0 0 0 $_CHIPNAME # 同时确保 openocd/tcl/target/gd25q80c.cfg 存在可从 OpenOCD 源码复制5.5 问题SRAM 不足HardFault_Handler被触发但没打印信息现象程序运行几秒后死机调试器显示 PC 停在HardFault_Handler但串口无输出。快速定位法在HardFault_Handler中添加最简打印void HardFault_Handler(void) { asm(mov r0, #0x20000000); // SRAM 起始地址 asm(str r0, [r0]); // 强制写入 SRAM观察是否触发 MemManage while(1); }若str指令触发 MemManage则证明 SRAM 已满栈溢出。用arm-none-eabi-size build/app.elf查看各段大小text data bss dec hex filename 42312 1240 42312 85864 14f68 app.elf # bss 42KB接近 SRAM 264KB 上限需优化优化技巧将大数组改为static const移至 Flash.rodata减少printf格式化开销改用printf(%d, x)而非printf(Value%d\n, x)关闭stdio的缓冲setvbuf(stdout, NULL, _IONBF, 0);。5.6 问题qspi flash读取返回全0x00而非0xFF现象新 Flash 芯片首次读取所有字节都是0x00而非预期的0xFF。真相这是 Flash 的“出厂态”。NOR Flash 出厂时单元为0x00擦除后才变为0xFF。0xFF是“空白”状态0x00是“未编程”状态。很多文档说“擦除后为0xFF”其实是把“擦除”和“出厂态”混为一谈。验证# 读取空白 Flash picotool read 0x10000000 16 blank.bin xxd blank.bin # 若全 00则是出厂态若全 FF则已擦除过 # 执行一次擦除 picotool erase --sector 0 # 再读 picotool read 0x10000000 16 erased.bin xxd erased.bin # 应全 FF5.7 问题sram 和 dram 的区别和联系为何 Pico 不用 DRAM深度解答SRAM静态 RAM靠触发器存储无需刷新速度快ns 级功耗高集成度低1bit 需 6 个晶体管。Pico 的 264KB 是片内 SRAM与 CPU 同 die延迟极低。DRAM动态 RAM靠电容存储需定期刷新Refresh速度慢数十 ns功耗低集成度高1bit 需 1 个晶体管1 个电容。PC 内存、手机 RAM 都是 DRAM。Pico 为何不用 DRAM成本、功耗、复杂度。DRAM 需专用内存控制器、时钟、刷新电路RP2040 为降低成本和功耗选择集成 SRAM。这也决定了 Pico 的定位微控制器非应用处理器。想用 DRAM选 Raspberry Pi Zero 2 W它用的是 LPDDR2。最后分享一个小技巧当你需要在 Pico 上存大量传感器历史数据时别死磕 Flash 寿命。我的方案是——用 SD 卡模块SPI 接口 FatFS 文件系统。SD 卡的擦写寿命10 万次远高于 QSPI Flash10 万次/扇区且容量动辄 32GB成本不到 10 元。真正的工程思维不是把所有东西都塞进芯片而是选对工具。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻