
很多人第一次理解STM32内存都是靠背芯片型号F103C8T6是64KB Flash、20KB SRAMF103ZET6是512KB Flash、64KB SRAM。但真到项目里要算地址、划Bootloader分区、或者排查越界导致HardFault的时候光背型号就不够用了。我早期就吃过一次亏往一块F103C8T6的0x08010000地址写数据写操作怎么都进不了Flash折腾半天翻数据手册才发现芯片总容量才64KBFlash地址到0x0800FFFF就到顶了再往上写硬件连寄存器都不给你响应。那次之后我彻底意识到地址范围和容量之间的换算不是只有面试才考。以下这篇内容我就把STM32内存大小与地址的对应关系、容量和地址边界的计算方法、以及实际项目中拿这些知识排过的一堆坑一次讲透。1. 先看清STM32的存储骨架4GB地址空间里哪里能放程序、哪里跑数据1.1 Flash为什么固定在0x08000000STM32用的Cortex-M内核是32位地址总线理论上可寻址4GB空间范围是0x00000000到0xFFFFFFFF。但ST并没有把这4GB全挂上一块通用内存而是按照ARM架构手册里对地址空间的规划把整个4GB切成几大功能区然后再在特定功能区内放置Flash、SRAM和外设寄存器。处理器复位后从哪个地址取第一条指令取决于BOOT引脚的配置。STM32支持从主Flash启动、从系统存储器启动、从SRAM启动三种方式。主Flash的物理地址被放在0x08000000但芯片上电默认要从0x00000000开始取向量表于是启动逻辑做了这么一件事把0x08000000这个物理区域的地址映射到0x00000000去。换句话说当你用BOOT0拉低从Flash启动时CPU访问的0x00000000实际对应的就是0x08000000这个物理Flash。你不妨把0x08000000理解成Flash的“门牌号”程序、只读常量、初始化数据最终都链接到这个地址区间。后面你设计Bootloader时要规划App起始地址就是基于这个固定门牌号往前算的。1.2 SRAM为什么固定在0x20000000SRAM区在地址空间里位于0x20000000起始的位置这块区域也是512MB大小。STM32的片上SRAM就挂在0x20000000开头地址连续向上增长。运行时的变量、堆栈、malloc动态内存全在这一块物理区域内分配。与Flash不同SRAM没有启动映射问题地址就是地址不需要做什么别名映射。在F1这种老系列里SRAM就是一整块连续空间20KB就是0x20000000到0x20004FFF64KB就是0x20000000到0x2000FFFF。但从F4开始ST把部分SRAM拆成了好几块比如F407有一块从0x20000000起始的128KB主SRAM还额外挂了一块从0x10000000起始的64KB CCM SRAM。关于CCM的坑我后面会专门讲这里先记住主SRAM首地址永远是0x20000000。1.3 外设区和内核系统区别把这两块当普通内存外设寄存器区起始于0x40000000GPIO、USART、SPI、I2C、TIM、DMA、ADC这些外设的寄存器全部映射在这片区域内。访问这块区域跟访问SRAM完全不同它是满内存映射I/OCPU往0x40000000附近写数据本质是向外设寄存器下发指令不是写内存。常见的错误是新手把外设地址和SRAM混淆在调试时看到一个“高地址”就往里写数据写坏了还找不到原因。内核系统区在0xE0000000起始嵌套向量中断控制器NVIC、SysTick定时器、系统控制块SCB都在这里。这块区域属于Cortex-M内核自己管理应用代码虽然能访问它但不能把上面当成数据缓冲池。Vector Table偏移寄存器VTOR就在这个区域后面做Bootloader应用跳转时你会跟它打很多次交道。2. 地址差与容量的换算公式、十六进制心算技巧、反推边界方法2.1 核心公式末地址减去首地址再加一才是真实字节数这个公式是理解内存和地址关系的基石。内存地址从0开始编号如果首地址是0x08000000末地址是0x0800FFFF那么地址数量等于0x0800FFFF - 0x08000000 1等于0x00010000也就是65536字节64KB。为什么必须加1因为首尾两个地址本身都占了一个字节的位置。用一个生活化例子解释一栋楼从门牌号101到门牌号199一共有多少个门牌号199减101等于98但实际是98加1等于99个门牌号因为第101号这个门牌也要算进去。内存地址也一样0x08000000这个地址代表的那个字节不能被漏掉。实际项目里经常有人把加1忘掉导致Bootloader里给App留出的分区少算了1KB或几字节虽然只差一个字节但凑巧App固件刚好压在分区末尾时烧写就会莫名失败。所以我习惯先在草稿纸上把末地址、首地址、字节数三列写清楚再往代码里填。2.2 十六进制偏移量表KB到地址增量的速查做嵌入式开发要习惯用十六进制表达容量。1KB等于1024字节十六进制是0x400。往下推算容量十六进制字节数常见用途1KB0x400EEPROM模拟块、配置页4KB0x1000Flash页大小F1系列16KB0x4000小容量芯片整片Flash20KB0x5000F103C8的SRAM32KB0x8000Bootloader分区常用大小48KB0xC000F103RC的SRAM64KB0x10000F103C8的Flash也常见于F1 SRAM上限128KB0x20000中等容量的Flash或SRAM256KB0x40000F103RC的Flash512KB0x80000F103ZE/F411的Flash1MB0x100000F407的Flash2MB0x200000F429的Flash实操建议是把这个表贴在工位旁边。我每次拿到新芯片第一步就查型号手册里的容量然后对照这张表在链接脚本里把LENGTH字段填准。别小看这张表它能省下大量拿计算器按十六进制的时间。2.3 由容量反推末地址计算方法和验算逻辑已知首地址和容量求末地址的公式是末地址 首地址 容量 - 1以F103C8的20KB SRAM为例首地址0x20000000 容量20KB 0x5000 末地址 0x20000000 0x5000 - 1 0x20004FFF反过来验算0x20004FFF - 0x20000000 1 0x5000 20480 20KB这里有个容易踩的小坑当你习惯了“容量减一”之后容易把“末地址 首地址 容量”这个不带减一的写法误当成对。如果写成0x20000000加0x5000得到0x20005000你就会多算一个字节这个多出来的地址实际上已经落在SRAM边界外面了。调试时如果你用这个地址去访问内存可能不会立刻崩溃但已经偷偷越界等到程序跑飞时根本不知道问题出在哪。3. 常见STM32型号的Flash/SRAM地址边界对照表3.1 F1系列从F103C8到F103ZE的容量阶梯STM32F1系列是入门最常见的。同是F103家族不同后缀容量差很多。以下都是我在实际调板时反复核对过的边界地址型号Flash容量Flash地址范围SRAM容量SRAM地址范围F103C8T664KB0x08000000 - 0x0800FFFF20KB0x20000000 - 0x20004FFFF103RBT6128KB0x08000000 - 0x0801FFFF20KB0x20000000 - 0x20004FFFF103RCT6256KB0x08000000 - 0x0803FFFF48KB0x20000000 - 0x2000BFFFF103ZET6512KB0x08000000 - 0x0807FFFF64KB0x20000000 - 0x2000FFFF注意F103RBT6和F103C8T6的Flash虽然一个是128KB、一个是64KB但SRAM都是20KB。这个差异对选型影响很大。如果你程序体积大、内存占用小选RBT6就够反过来如果内存需求高多出来的Flash容量根本缓解不了SRAM压力就得换ZET6或者直接跳F4系列。3.2 F4系列F411、F407、F429的SRAM结构差异F4系列比F1复杂多了。F411的SRAM是连续的128KB地址干净利落。到F407SRAM被拆成两块主SRAM 128KB在0x20000000另外64KB CCM独立放在0x10000000。F429更夸张主SRAM达到192KB范围从0x20000000到0x2002FFFF再加上64KB CCM总共256KB。型号Flash容量Flash地址范围主SRAM容量主SRAM地址范围CCM容量与地址F411CEU6512KB0x08000000 - 0x0807FFFF128KB0x20000000 - 0x2001FFFF无F407VGT61MB0x08000000 - 0x080FFFFF128KB0x20000000 - 0x2001FFFF64KB 0x10000000F429ZIT62MB0x08000000 - 0x081FFFFF192KB0x20000000 - 0x2002FFFF64KB 0x10000000F407和F429上有个坑CCM内存虽然也算容量但它挂在0x10000000和主SRAM的0x20000000并不连续。你写一个很大的缓冲区数组然后取它的地址普通数组在主SRAM里地址是0x2000xxxx但如果你指定了某个段属性放在CCM里地址就跳到0x1000xxxx。很多人第一次在调试器里看到0x10000000开头的地址会懵以为芯片出问题了。3.3 CCM内存的限制地址不连续带来的操作陷阱CCM全称Core Coupled Memory直接耦合在CPU内核上CPU访问它比访问主SRAM更快但DMA和外设完全碰不到它。也就是说如果你把DMA接收缓冲区的地址分配到CCM里DMA根本没法搬运数据程序表现就是USART收发数据全乱、ADC采不到数排查很久才发现是缓冲区落在CCM里。正确用法是把CCM留给CPU的紧急运算。比如高频中断里用的中间变量、快速排序的临时数组、或者关键状态机标志位放CCM后访问速度有优势。但凡是DMA要操作的缓冲区、以太网描述符、USB端点缓冲必须放在0x20000000起始的主SRAM里。做Bootloader时如果不好好分配这几个区App跳转后大概率跑飞。4. 在链接脚本和map文件里看清内存边界越界排查记录4.1 .ld文件中ORIGIN与LENGTH的含义和修改后果开发STM32如果用GCC工具链链接脚本.ld文件就是内存布局的宪法。以F103C8T6为例默认链接脚本开头的MEMORY段是这样的MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }ORIGIN告诉链接器这段内存从哪里开始LENGTH告诉它总容量。链接器按这个定义放置代码段、数据段、BSS段和堆栈。一旦你改错LENGTH——比如把64K误写成128K链接器以为有128KB Flash可用编译时不会报错但生成的bin文件超过实际Flash容量时烧录器会往0x08010000后面的地址写数据芯片根本不存在这些地址烧写要么失败要么校验不通过。当年我在一个Bootloader项目里就是改错了这个值折腾了一个晚上最后用命令对比bin文件大小和硬件容量才定位到问题。同样的道理RAM的LENGTH如果比实际大链接器把大数组分配到不存在的SRAM区域程序上电后访问这些地址会触发HardFault。这里的关键原则链接脚本里的LENGTH必须以数据手册的物理容量为准不能拍脑袋写。4.2 编译map文件里的RO、RW、ZI与地址分布Keil和GCC编译后都能生成.map文件里面记录了每个段的绝对地址和大小。Keil工程里打开生成的.map文件通常能看到末尾的统计信息Total RO Size (Code RO Data) 1234 ( 1.20KB) Total RW Size (RW Data ZI Data) 5678 ( 5.54KB) Total ROM Size (Code RO Data RW Data) 6912 ( 6.75KB)这里的Total RO Size是程序代码加只读常量的大小Total RW Size是变量占用的SRAM空间Total ROM Size是整个程序烧进Flash需要的空间。把这三项和链接脚本里的LENGTH对照看就能知道当前程序离内存上限还有多远。有一个经常被忽视的点RW Size既占Flash又占SRAM。因为初始化的全局变量其初始值要保存在Flash里上电后复制到SRAM。所以就算你程序代码不多但定义了巨大的有初值数组Flash照样会被吃掉一部分。遇到Flash紧张时可以把大数组改成无初值声明ZI段就能省下Flash里的初始值空间。这个细节在实际项目中非常实用。4.3 三类典型内存越界事故的排查链路事故一一个全局数组越界写。症状是程序间歇性跑飞尤其在某个特定函数调用后。排查流程先用调试器在HardFault中断入口挂断把栈回溯出来找到触发异常的PC指针附近代码再反查代码里哪个大数组在越界写。很多时候是for循环的索引上限取错比如数组定义为uint8_t buf[64]循环却写到buf[64]其实已经多写了一个字节。事故二堆栈溢出。症状往往更隐蔽。程序前期正常运行很久后突然崩溃。你可以在调试器里观察栈指针SP的值和链接脚本里RAM末尾地址对比。比如RAM末尾是0x20004FFF栈往低地址方向生长当SP跌到0x20004000甚至更低说明已经压爆栈踩到其他变量区域了。处理办法一般是减小局部大数组、把大缓冲改成静态或全局分配、或者在链接脚本里增大栈段。事故三Flash写穿。比如做IAP升级时把App固件写到0x08000000开头结果覆盖了Bootloader。症状是下次上电直接变砖连Bootloader都没了。这类事故最根本的防护是从Bootloader端做地址范围检查写入前判断目标地址是否落在Bootloader自己占用的Flash段内一旦超界直接拒绝写入。5. Bootloader与App分区把内存地址计算落到实际项目5.1 给App划分Flash地址空间的完整计算Bootloader项目是检验地址计算基本功的最好场景。假设芯片还是F103C8T6Flash总共64KB。我想在前面放16KB Bootloader那Bootloader占用的地址就是起始地址0x08000000 Bootloader大小16KB 0x4000 Bootloader末地址0x08000000 0x4000 - 1 0x08003FFFApp起始地址就等于Bootloader末地址加1App起始地址 0x08004000整个Flash末尾是0x0800FFFF所以App可用空间是可用大小 0x0800FFFF - 0x08004000 1 0xC000 48KBApp的链接脚本MEMORY段就应该是MEMORY { FLASH (rx) : ORIGIN 0x08004000, LENGTH 48K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这里有一个关键坑如果Bootloader实际是16KB但分区的App起始地址算成了0x08008000那App可用空间只有32KB白白浪费16KB。如果反过来把Bootloader实际占用空间算少了Bootloader编译后的体积超过预留的16KBApp起始地址就会落在Bootloader中间后果是灾难性的。5.2 向量表重定位App地址偏移后必须做的补救App链接到0x08004000后上电时CPU还是从0x08000000启动也就是先跑Bootloader。Bootloader初始化到一定阶段需要跳转到App。跳转前有一件事必须做把向量表偏移寄存器VTOR指向App的新基地址。在Cortex-M3/M4上具体操作是#define APP_FLASH_ADDR 0x08004000U void jump_to_app(void) { uint32_t app_stack *(volatile uint32_t *)APP_FLASH_ADDR; uint32_t app_reset *(volatile uint32_t *)(APP_FLASH_ADDR 4); SCB-VTOR APP_FLASH_ADDR; __set_MSP(app_stack); void (*app_main)(void) (void (*)(void))app_reset; app_main(); }这个流程里第一行读App起始地址处的字作为栈顶指针第二行读App起始地址向后偏移4字节处的字作为复位中断函数入口。然后设置VTOR、重新设置主栈指针、跳转。很多人忘了设置VTORApp里一旦发生中断CPU仍然从0x08000000处的向量表找中断服务函数结果找到的是Bootloader的向量表整个中断逻辑全乱套。5.3 运行期验证地址边界的几个防御写法实际项目里光靠人工算地址不够最好在运行期主动校验。可以在Bootloader跳转前检查App的复位向量是否合理uint32_t app_reset_value *(volatile uint32_t *)(APP_FLASH_ADDR 4); if ((app_reset_value 0xFFF00000) ! 0x08000000) { error_handler(); }这个判断的含义是App的复位中断入口地址应该落在Flash区域0x08000000起始空间如果读取到的值不在Flash区域说明该地址根本没烧写合法程序此时跳转必然跑飞提前拦截比跳转后再傻眼强得多。我在OTA升级方案里也做了同样的校验升级包下载完成后先验证整个固件的有效性和地址边界再决定是否允许跳转基本能避免刷入坏固件后变砖的情况。写在最后的一点项目习惯每次拿到一块没用过的STM32板子我最先做的不是点灯而是做三步第一去数据手册Chapter 1的表里查这颗芯片的Flash和SRAM容量以及地址范围第二打开对应工程的.ld链接脚本核对ORIGIN和LENGTH有没有写错第三编译一次看map文件里的RO/RW/ZI三项总量距离上限还有多少余量。这三步一共花不到五分钟但能帮你攒下一大堆宝贵的调试时间。地址和容量的换算看似是基础得不能再基础的东西但几十年嵌入式开发经验告诉我越是基础的规则越值得每回都认真对待一次。