树莓派Pico存储底层精讲:ROM、SRAM、Flash地址映射与启动流程

发布时间:2026/9/9 9:37:50
树莓派Pico存储底层精讲:ROM、SRAM、Flash地址映射与启动流程 树莓派 Pico 虽然看起来只是一块几十块钱的开发板但把它拆开看它其实是一台微缩的计算机Boot ROM、SRAM、Flash 这三个存储角色缺一不可。我第一次接触 Pico 时只当它是个能点灯的板子直到后来要在掉电时保存设备配置才意识到如果不理解存储底层很多看似简单的功能都会踩坑。这篇文章就以经典 PicoRP2040为例把 ROM、SRAM、Flash 的原理、地址映射、启动流程和实操方法完整过一遍适合已经能跑通 Blink、想往深处走的开发者。如果你用的是 Pico 2RP2350架构思路类似只是容量和寄存器细节有变化读起来同样有参考价值。先说一下我默认你用的是官方 C SDK能正常编译和烧录。后面所有示例都基于 PC 上的 Linux 或 macOS 终端操作Windows 下命令名称略有不同但思路完全一致。1. 先看清 Pico 的存储地图ROM、SRAM、Flash 各自忙什么1.1 三种存储器的定位与区别很多人会把 ROM 和 Flash 混为一谈在 Pico 上这个误解特别容易发生。Pico 里确实有一块 ROM地址在0x00000000是芯片出厂时固化好的 64KB Mask ROM里面放着最底层的启动代码。它不能改也没人会去改。Flash 则是板载的一颗外部 QSPI NOR Flash通常 2MB 或 4MB用户代码、字体表、配置数据都放在这里。一个是芯片内部设备一个是芯片外部设备只是它们读起来都像“非易失存储”。SRAM 又是另一个维度。它属于片上高速内存容量是 264KB掉电后数据全部丢失但访问速度最快。程序运行时的调用栈、全局变量、临时缓冲区、堆基本都在 SRAM 里。ROM、SRAM、Flash 三者并非“一个替代另一个”的关系而是分工协作ROM 负责“第一次启动”SRAM 负责“跑起来”Flash 负责“记住东西”。1.2 RP2040 的地址空间一份随手能查的速查表理解存储底层第一步就是看地址映射。RP2040 把各种存储和寄存器统一映射到 32 位地址空间里CPU 访问不同地址实际上就是在访问不同设备。我平时最常用的就是下面这张表区域地址范围容量/说明Boot ROM0x00000000 ~ 0x0000FFFF64KB Mask ROM出厂烧死负责启动引导XIP Flash 映射区0x10000000 ~把外部 QSPI Flash 映射成只读地址CPU 可直接从这取指执行SRAM0x20000000 ~ 0x20041FFF264KB 片内高速 RAM栈、堆、全局变量都在这外设寄存器0x40000000 ~GPIO、UART、I2C、DMA 等外设的控制寄存器这张表最大的价值是帮你在看数据手册和反汇编时快速定位当你看到一个地址以0x10000000开头说明它指向 Flash以0x20000000开头说明它在 SRAM 里。很多调试工具输出的地址配合这张表一眼就能判断问题出在存储还是外设。1.3 为什么 Pico 要用外部 Flash和 STM32 那种把 Flash 做进芯片内部的方案不同RP2040 把 Flash 放到了芯片外面。这样做最直接的好处是成本低、容量可控。你不用在流片阶段就绑死 Flash 容量板卡厂商可以在 1MB、2MB、4MB 甚至 16MB 之间自由选择只要 QSPI 接口兼容就行。坏处也不是没有外部 Flash 的读取速度远慢于芯片内部 Flash所以 RP2040 专门做了一个 XIP 控制器和一个小缓存来弥补。另一个问题是启动更复杂——芯片上电时第一段程序执行后还得先初始化外部 Flash才能跳转到用户代码。这个“第一段初始化程序”是谁执行的就是 Boot ROM 和第二级引导程序。接下来我就顺着启动流程往下扒。2. Boot ROM芯片上电后的第一个“天命主角”2.1 64KB Mask ROM 到底固化在哪Boot ROM 是 RP2040 芯片内部一块独立的掩膜只读存储器生产芯片时电路就已经固化用户没有任何办法修改。它的主要职责是芯片上电后第一个执行、初始化最基本的硬件、决定从哪条路进入用户程序。这里要注意区分Boot ROM 不是 U 盘里那个能拖入文件的小系统更不是 Arduino 引导程序。你在编译 Pico 工程时看到的boot2第二级引导会被放在 Flash 里但真正最开始执行的第一级引导始终是 Boot ROM。Mask ROM 的优势是稳定、不可损坏哪怕你反复烧错程序只要 Boot ROM 正常板子就能恢复到可烧录状态。2.2 从按下 BOOTSEL 到跑起用户程序的完整链路Pico 上有一个 BOOTSEL 按钮很多人只知道“按住再插 USB 就能进下载模式”但不知道它背后的完整逻辑。其实这个按钮接在 Flash 的片选引脚上按下时会把 Flash 从电路里摘掉Boot ROM 检测到这个状态后就不会尝试从 Flash 加载程序而是进入 USB 引导模式。完整的启动链路是这样的上电复位CPU 从0x00000000开始执行 Boot ROM 代码。Boot ROM 做最基本的时钟和栈初始化。检测 Flash 片选引脚状态也就是看 BOOTSEL 是否被按下。如果按下 BOOTSEL进入 USB 引导模式枚举出一个名为RPI-RP2的 U 盘等待 UF2 文件。如果没按下Boot ROM 从 Flash 的起始位置读取第二级引导程序。第二级引导程序初始化外部 QSPI Flash 的时钟和访问模式。第二级引导跳转到用户代码入口Reset_Handler开始跑你的程序。这里面最容易踩坑的是第二步到第五步的过渡。如果 Flash 里烧了一段坏代码Boot ROM 读出来发现无法执行板子并不会自动帮你恢复而是可能表现为上电后没反应。这时候不用慌按住 BOOTSEL 再上电问题基本都能解决。2.3 理解 UF2 拖拽烧录才算真正用过 Boot ROMBoot ROM 里最实用的功能之一就是 UF2 拖拽烧录。UF2 是一种专门为低端 USB 设备设计的固件格式Boot ROM 会把虚拟 U 盘收到的文件解析成一段段带地址的数据直接写入 Flash。整个过程不需要额外驱动也不依赖烧录器操作门槛极低。有几点经验值得分享。UF2 文件并不是直接把二进制内容原样写进 Flash它内部按固定大小的块组织每块都包含地址、块编号、总块数和数据。Boot ROM 校验这些信息后才会把数据写到对应地址。所以如果你手工把.bin文件改名成.uf2拖进去大概率没有反应。使用官方 SDK 编译时CMake 会自动生成.uf2如果自己写链接脚本或手动合并程序最好用uf2conv.py工具转换不要图省事直接改名。另一点是你看到的RPI-RP2虚拟 U 盘容量并不是 Flash 的真实容量它只是 Boot ROM 虚拟出来的一个固定大小盘符。想确认实际 Flash 容量要靠后面说的 JEDEC ID 或编译工具链信息。3. SRAM264KB 高能内存在 Pico 里怎么用才不浪费3.1 6 个 44KB Bank 和双核并行RP2040 的 SRAM 可不像普通单片机那样只是一整块内存。它内部由 6 个 bank 组成每个 bank 容量是 44KB合计 264KBCPU 看来地址连续从0x20000000到0x20041FFF。为什么要拆成 6 个 bank因为 RP2040 是双核 Cortex-M0如果两个核同时访问同一个内存硬件仲裁会拖慢速度。拆成多个 bank 后两个核可以尽量访问不同 bank减少竞争。每个 bank 都设计有多个访问端口CPU0、CPU1、DMA 控制器可以并行访问不同的 bank。不过如果两个 CPU 或 DMA 同时命中同一个 bank还是会冲突硬件会自动分时处理但实时性会受到一定影响。所以做双核高性能应用时把高频访问的数据分散到不同 bank比盲目堆缓存更有效。3.2 谁会被放到 SRAM栈、堆、全局变量和“紧急代码”程序运行时的核心数据基本都在 SRAM 里。栈是函数调用、局部变量、中断压栈的地方堆是malloc或 Cnew申请内存的区域全局变量和静态变量也占 SRAM但它们在启动时会从 Flash 拷贝初值过来。另外一些对执行速度要求极高的代码也会特意放到 SRAM 里运行因为 Flash 再怎么用 XIP 加速还是不如 SRAM 快。Pico SDK 里有两个很实用的宏__not_in_flash_func和__time_critical_func。用它们修饰函数链接器会把函数放到 SRAM 的特殊 section 里而不是默认的 Flash。例如#include pico/platform.h __not_in_flash_func(void hard_isr(void)) { // 这段代码会在 SRAM 中执行不受 Flash 读取瓶颈影响 }这类函数适合放中断处理、时间敏感算法、低功耗唤醒后的首段逻辑。不过要注意SRAM 容量有限别把整个工程都丢进去否则链接时会直接报区域溢出。3.3 编译完怎么看 SRAM 占用很多新手调试“程序跑飞”或“上电卡死”最后发现是 SRAM 溢出。其实编译完就能看到内存占用不用等到运行时。在构建目录下执行arm-none-eabi-size build/your_project.elf输出类似text data bss dec hex filename 2556 20 1700 4276 10b4 your_project.elf这里的text是代码和只读数据占用的 Flash 空间data是已初始化全局变量它既占 Flash用于存初值又占 SRAM运行时驻留bss是未初始化全局变量只占 SRAM。所以 SRAM 占用大约等于data bss再叠加栈和堆。如果这个值接近 270336 字节说明内存已经很紧张了。3.4 双核通信时要避开的 SRAM 坑双核编程最容易出问题的地方就是随便定义一个全局变量然后两个核一边写一边读。SRAM 本身的访问是原子的但这个“原子”只针对单次总线访问对于多字节变量或读改写操作并不安全。Pico SDK 提供了multicore库里面有 FIFO 通道和锁机制工程上优先用这些官方封装而不是自己靠一个全局标志位硬等。另外要留意Core0 和 Core1 的栈是各自独立的但默认栈大小并不大。如果某个核里用了较深的递归或大数组要主动调大栈配置否则表现就是极其诡异的随机重启或 HardFault。这种问题用调试器看不一定能复现最好一开始就通过链接脚本和启动文件把两个核的栈大小规划好。4. Flash外部 QSPI NOR Flash 的 XIP、Cache 与擦写4.1 板载 Flash 和 XIP为什么程序能直接在 Flash 里跑Pico 板载 Flash 是一颗 QSPI NOR Flash常见的是 Winbond W25Q162MB和 W25Q324MB。它通过 QSPI 接口连到 RP2040芯片内部有 XIP 控制器会把 Flash 映射到0x10000000开始的一段只读地址空间。CPU 取指令时只要地址落在 XIP 范围就会自动触发 Flash 读取看起来就像在读取普通内存一样。这就是“Execute in Place”的含义——代码不需要拷贝到 SRAM也能直接从 Flash 执行。但别被“直接执行”四个字骗了Flash 的随机访问速度远低于 SRAM。XIP 控制器内部设了硬件缓存对顺序执行的代码优化效果很好如果程序里频繁跳转、大量调用函数、反复查询大表缓存命中率就会下降程序实际运行速度会被 Flash 带宽拖住。所以当某个中断处理函数卡顿明显时把它挪到 SRAM 执行往往立竿见影。4.2 NOR Flash 的擦写规则4096 扇区、寿命、掉电安全NOR Flash 有个重要特性可以按字节随机读但不能按字节覆盖写。写操作只能把 1 变成 0想要把 0 恢复成 1必须先执行擦除操作。Pico 板载 Flash 的擦除基本单位通常是 4KB sector编程单位是 256 字节 page。也就是说哪怕你只想改一个字节也需要先擦掉整个 4KB 扇区再把整块数据写回去。擦写寿命也要心里有数。NOR Flash 的擦写次数一般在十万次量级如果你在程序里每隔几百毫秒就把配置写到固定地址几天内就可能达到寿命上限。掉电安全问题更隐蔽擦除和写入中间如果突然断电Flash 内容可能处于“半擦半写”的损坏状态。所以工程化做法是要么使用双备份轮流写入要么在数据头部加魔术字和校验值启动时先验证再使用。4.3 实操用官方 API 在 Flash 里保存用户数据Pico SDK 提供了pico_flash相关的 API核心是flash_range_erase和flash_range_program。下面是一个最简单的写配置示例#include pico/stdlib.h #include hardware/flash.h #include hardware/sync.h #include string.h #define TARGET_OFFSET (1024 * 1024) // 写到偏移 1MB 处避开代码区 #define SECTOR_SIZE 4096 void save_config(const uint8_t data[SECTOR_SIZE]) { // 写 flash 期间不能有中断也不能让另一个核从 XIP 取指 uint32_t flags save_and_disable_interrupts(); flash_range_erase(TARGET_OFFSET, SECTOR_SIZE); flash_range_program(TARGET_OFFSET, data, SECTOR_SIZE); restore_interrupts(flags); }读回数据更简单直接访问 XIP 映射地址即可const uint8_t *config (const uint8_t *)(XIP_BASE TARGET_OFFSET);这里必须强调一个坑flash_range_erase和flash_range_program执行期间CPU 不能从 Flash 取指。官方 SDK 内部会尽量处理但如果你在其他核上跑程序或者中断里含有 Flash 上的代码就可能触发 HardFault。稳妥做法是写 Flash 前保证代码运行在 SRAM并用save_and_disable_interrupts关掉中断。要是双核应用还要先让另一个核进入空闲等待状态否则后果很难排查。关于偏移量我的建议是不要用0也不要随手写一个固定地址。先看目标固件编译后的text data占了多大空间再把数据区放在程序末尾之后最好留出至少 64KB 余量。否则你写到代码区域轻则程序崩溃重则板子变砖需要重新烧录。4.4 想换更大 Flash 或改板子至少要注意这三件事第一确认新 Flash 的 JEDEC ID。不同厂商的 NOR FlashID 前缀和容量字节都不一样。常见 Winbond 颗粒 ID 如下Flash 颗粒JEDEC ID容量W25Q16JVEF 40 152MB16MbitW25Q32JVEF 40 164MB32MbitW25Q128JVEF 40 1816MB128Mbit第二第二级引导boot2要和 Flash 型号匹配。Pico SDK 里预置了多个boot2_*.S比如针对 Winbond 的、针对通用 0x03h 命令的。如果你的 Flash 颗粒读时序或 QSPI 命令集不一样默认 boot2 可能初始化失败现象就是程序下载成功但上电后不运行。换 Flash 后要重新检查PICO_BOOT_STAGE2_FILE配置。第三别把 XIP 地址范围当成 Flash 的全部容量。RP2040 的 XIP 控制器理论上支持到 16MB 地址空间但实际板载容量可能只有 2MB 或 4MB。你往超过容量的地址写数据不会自动生效甚至可能被总线返回无效数据。规范做法是在代码开头读取 Flash 大小所有写入前做范围检查。5. 边做边查定位问题靠 map、size 和 JEDEC ID5.1 三分钟看懂编译产物我遇到过不少开发者明明编译通过了上电就是没反应第一反应是“是不是 GPIO 接错了”。其实先看一眼编译产物能省很多时间。在构建目录执行arm-none-eabi-size build/your_project.elf除了前面看的 SRAM 占用还能看到 Flash 占用。如果text已经接近你板载 Flash 容量链接器可能早就报警了只是你没注意。这时候要么裁剪功能要么换更大容量 Flash。另一个常用命令是picotool info如果板子处于烧录后的运行状态它会显示当前运行固件的一些信息包括某些工具版本可以显示 Flash 大小。5.2 变量到底放哪了用 map 和 nm 来一次“溯源”当你怀疑某个变量被放到了 Flash 导致读取性能差或者某个大数组把 SRAM 塞爆了最直接的方法是看链接器生成的.map文件。在构建目录里搜索变量名能看到它被分配到的地址和所属 section。配合地址映射表你立刻能判断它是在 Flash 还是 SRAMgrep -E config_buf|system_info build/your_project.map如果想看所有符号的地址分布可以用nmarm-none-eabi-nm -n build/your_project.elf | grep [BbDd] B和b表示 BSS 段通常在 SRAMD和d表示已初始化数据运行时也在 SRAMT和t是代码段默认在 Flash。如果某个函数被__not_in_flash_func修饰它的T地址会落在0x20000000到0x20041FFF之间而不是0x10000000开头这也是快速验证“代码有没有成功放进 RAM”的好办法。5.3 确认 Flash 型号的三个途径当你需要给 Pico 扩展外部 Flash 或者怀疑板载 Flash 容量时可以按下面三个途径确认。第一个是看丝印。板子背面的 Flash 芯片表面会印型号比如W25Q16JVSIQ或W25Q32JVSIQ。第二个是读 JEDEC ID。标准 SPI NOR Flash 都支持发0x9F命令后回读三个字节的 ID比如EF 40 16。第三个是用 picotool 或厂商工具。不过要注意Boot ROM 虚拟的 U 盘容量不代表真实 Flash 容量以丝印或 ID 为准。6. 常见问题与排查技巧实录6.1 问题速查表实际折腾过程中我踩过不少坑也帮朋友排查过不少。这里整理成一张速查表按现象、可能原因、处理方式三列来写现象可能原因处理方式按住 BOOTSEL 上电U 盘没出现USB 线是充电线没有数据通道换一根正规数据线直接插主机 USB 口UF2 拖进去后板子没反应UF2 文件损坏、非官方转换格式、Flash 容量不够重新编译生成 UF2用uf2conv.py校验程序能编译上电后不运行boot2 与 Flash 型号不匹配检查PICO_BOOT_STAGE2_FILE换对应 boot2调用flash_range_erase时 HardFault写 Flash 期间代码仍在 Flash 取指或另一个核在跑关中断、把写 Flash 相关函数放到 SRAM、双核同步等待重启后 Flash 数据还是旧值写入偏移和代码区重叠、没擦除成功用 map 确认代码区范围选安全偏移加校验值SRAM 不足编译报 region overflow全局变量/栈/堆设得过大看 size 输出精简数组调整 linker script双核程序偶发卡死多字节变量未原子访问、SRAM bank 冲突用multicore库的 FIFO 和锁避免双核频繁写同一变量6.2 写 Flash 时最容易翻车的三个操作习惯第一个习惯是魔法数字满天飞。我见过不少代码直接写flash_range_erase(0x100000, 4096)但换了固件版本后代码体积变大这个偏移突然就和代码区重叠了。正确做法是在工程里定义一个常量并写一个启动断言确保TARGET_OFFSET大于固件实际大小。第二个习惯是不做写入完整性校验。工业产品最怕掉电写坏配置所以我一般会在数据结构头部放 4 字节魔术字和 CRC 校验值。开机读取时先校验不通过就恢复默认配置。这样即使写入中途断电也不会用一份损坏的配置继续跑。第三个习惯是高频写入不加磨损均衡。如果设备每秒钟都要记录一次状态固定写同一个扇区寿命会很快耗尽。简单的方案是把 Flash 按扇区组成环形缓冲轮流写入更复杂就用 LittleFS 这类文件系统。Pico 上移植 LittleFS 并不复杂需要做掉电可靠存储时别再直接对着固定地址硬写。6.3 一点经验别等到项目炸了才去看内存布局我在实际使用中最大的体会是存储底层的内容平时容易被忽略但一到项目后期就会变成“玄学问题”。嵌入式开发的很多疑难杂症比如偶发重启、写数据丢失、双核卡死归根结底都是对 ROM、SRAM、Flash 的边界和特性理解不够。与其等到炸了再查不如在项目初期就把地址映射表、link script、map 文件这几样东西翻熟。最后再分享一个小技巧每次写完 Flash 操作代码都做一次冷启动和热重启两种测试并在数据区打上版本号。这样一旦后续出现异常你能快速判断是写入时机问题、还是 Flash 区域被覆盖、还是固件升级时数据格式变更导致的不兼容。把底层思路理清了Pico 这台“小电脑”才算真正属于你。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询