ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32内存映射与地址换算:从Flash SRAM大小到链接脚本配置

STM32内存映射与地址换算:从Flash SRAM大小到链接脚本配置 搞嵌入式这些年经常有刚入行的朋友拿着编译报错截图来问我明明芯片手册上写着Flash 64KB、SRAM 20KB为什么代码一编译就说Out of Memory或者反过来程序跑着跑着就进HardFault查了半天发现是数组越界写到了不该写的地址。说到底这些问题都绕不开一件事STM32的内存地址空间是怎么划分的内存大小和地址范围到底怎么换算。今天就把这块彻底讲透从内存映射到型号命名从地址计算到链接脚本配置一次说清楚。1. 先搞懂STM32的内存映射到底是怎么回事1.1 为什么很多新手栽在“地址”和“大小”上STM32是32位单片机CPU的寻址能力是4GB也就是地址从0x00000000到0xFFFFFFFF一共2的32次方个字节。注意这里说的是字节地址也就是说每一个地址对应内存中的一个字节并不是一个地址对应一个字或者一个int。很多初学者在这里就开始糊涂看到0x20000000这个地址以为它是一个“很大的数”搞不清楚它和“20KB SRAM”之间到底什么关系。实际上地址和大小是两回事。地址是“门牌号”大小是“房子的面积”。20KB SRAM只占地址空间里很小的一段它从0x20000000这个门牌号开始往后排0x5000个字节也就是20KB0x5000 5 * 16^3 20480 20 * 1024所以SRAM的地址范围就是0x20000000到0x20004FFF。后面我会给出一整套换算方法这里先建立这个基本概念。1.2 一张图看懂STM32的地址空间划分STM32把4GB地址空间分成了8个区域每个区域512MB。芯片设计上不是每个区域都真的挂了存储器和外设但地址空间是提前划分好的这样CPU访问起来有统一的规则。对于搞应用开发的人来说最常用的是前4个区域0x00000000 - 0x1FFFFFFF代码区主要放FlashSTM32内部Flash的起始地址是0x08000000但通过Boot引脚配置可以把0x08000000映射到0x00000000这样CPU上电后从0x00000000取向量表实际访问的就是Flash里的内容。0x20000000 - 0x3FFFFFFFSRAM区片上SRAM都在这个区域比如F103系列从0x20000000开始F407系列除了0x20000000的SRAM还有CCM RAM在0x10000000。0x40000000 - 0x5FFFFFFF外设区所有的片上外设寄存器都挂在这里GPIO、USART、SPI、I2C、定时器、DMA等每一个外设被分配一段地址里面放它的各种寄存器。0xE0000000 - 0xFFFFFFFFCortex内核私有区包括系统控制块SCB、SysTick定时器、NVIC、调试组件等这部分是ARM内核规定好的芯片厂商不能改。1.3 4GB寻址空间的极限与实际可用区域虽然理论上有4GB寻址空间但STM32实际用到的地址非常少。拿F103来说Flash最大512KBSRAM最大64KB外设寄存器加起来也就几十KB剩下绝大部分地址空间是保留的访问了会触发总线错误。这一点在写程序时要特别注意尤其是做地址校验时不能只看地址范围还要确认这个地址对应的存储器和外设是否真实存在。不同芯片的内存大小不一样但地址映射的“骨架”是一样的。比如F103C8T6的Flash是64KB地址范围是0x08000000到0x0800FFFFF103ZET6的Flash是512KB地址范围是0x08000000到0x0807FFFF。起始地址都一样区别在结束地址而这个结束地址就是由芯片的具体容量决定的。理解了这一点后面做链接脚本配置、启动文件分析时就能得心应手。2. 怎么从芯片型号一眼看出Flash和SRAM大小2.1 STM32命名字段拆解STM32的型号看起来是一串神秘代码比如STM32F103C8T6其实每个字段都有明确含义。以这个经典型号为例逐个拆解STM32固定前缀代表意法半导体的32位微控制器系列。F代表通用型闪存产品线还有L表示低功耗H表示高性能等。103代表产品子系列。103是增强型72MHz主频101是基本型36MHz105/107是互联型。C代表引脚数。C表示48脚R表示64脚V表示100脚Z表示144脚。8代表Flash容量。这里的数字或字母对应的容量有固定规则8就是64KB。T代表封装形式T是LQFP封装。6代表温度范围6是-40°C到85°C工业级7是-40°C到105°C。很多人在意的是“8”这个位其实F103C8T6和F103RCT6、F103ZET6的区别一部分就在容量代码上。容量代码的对照关系非常关键直接决定了你能写多大的程序、能用多大的RAM。常见对照如下容量代码Flash大小SRAM大小F103系列416KB4KB / 6KB632KB10KB864KB20KBB128KB20KBC256KB48KBD384KB64KBE512KB64KB2.2 常见主流型号内存容量速查表实际项目里遇到的芯片型号五花八门我把最常见的几颗料整理成一张速查表方便查阅型号FlashSRAM备注STM32F103C8T664KB20KB蓝板最常用的型号STM32F103CBT6128KB20KBSTM32F103RCT6256KB48KB入门级开发板常用STM32F103ZET6512KB64KB正点原子/野火精英板常用STM32F407VET6512KB192KB含128KB CCM RAM高性能带DSP和FPUSTM32F407ZGT61MB192KB含128KB CCM RAM探索者开发板常用STM32H743ZIT62MB1MB高性能M7内核STM32G030F6P632KB8KB低成本小封装STM32L011F4P632KB8KB低功耗M0内核这里面有个细节容易踩坑F407的SRAM分为两部分0x20000000开始有128KB0x10000000开始的CCM RAM有64KB总计192KB。但CCM RAM是不能被DMA访问的而且它和普通SRAM不在同一个连续地址块里配置链接脚本时要单独划分。我见过有人直接把SRAM大小填成192KB结果CCM RAM被DMA和CPU同时访问数据错乱排查了半天。2.3 型号记忆法和容量对应技巧容量代码和实际大小的对照关系熟练之后可以这样记忆代码4对应16KB6对应32KB6比4大容量翻倍8对应64KBB对应128KBC对应256KB以此类推。基本规律是从4开始每次容量乘216、32、64、128、256、512。字母D对应384KB是个例外E又回到512KB。为了保险起见任何型号都以数据手册为准。我在实际开发中遇到过一次特殊情况STM32F103C8T6这个型号官方标注Flash是64KB但早期流片版本实际有128KB物理Flash很多人用它烧录128KB的程序也能跑。这在圈子里属于公开的秘密但正规产品设计千万不要这么干因为恢复出厂校验或者换一批芯片后程序可能就跑不起来了。3. 内存大小与地址的换算方法核心实操3.1 已知地址范围怎么算容量地址范围换算容量的公式很简单容量 结束地址 - 起始地址 1。关键在于十六进制的加减法要熟练。举个例子已知某芯片Flash起始地址0x08000000结束地址0x0803FFFF那么容量 0x0803FFFF - 0x08000000 1 0x40000 262144字节 256KB。0x40000怎么算成256KB十六进制0x40000是4 * 16^4 262144除以1024得到256。再举一个SRAM的例子。F103ZET6的SRAM起始地址0x20000000容量64KB。64KB的十六进制是多少64 * 1024 65536转十六进制就是0x10000。所以结束地址 0x20000000 0x10000 - 1 0x2000FFFF。这句话建议背诵容量算结束地址永远要“加大小减1”因为地址是从0数起的不是从1数起的。十六进制容量换算表建议记熟十进制容量十六进制常见用途1KB0x400小段RAM/寄存器块16KB0x4000小容量Flash型号20KB0x5000F103C8T6的SRAM64KB0x10000F103C8T6的Flash128KB0x20000F103CBT6的Flash256KB0x40000F103RCT6的Flash512KB0x80000F103ZET6的Flash1MB0x100000F407ZGT6的Flash3.2 已知容量怎么反推结束地址反过来已知起始地址和容量求结束地址公式就是结束地址 起始地址 容量 - 1。这个场景最常见的地方就是配置链接脚本、设置启动文件的栈顶地址。以F103C8T6为例SRAM容量20KB。20KB转十六进制20 * 1024 20480 0x5000。所以SRAM结束地址 0x20000000 0x5000 - 1 0x20004FFF。再看栈顶地址Cortex-M内核是从0x00000000读取初始SP栈指针而STM32启动时把Flash映射到0x00000000实际上读的是0x08000000处的初始化值。启动文件里定义栈顶为0x20005000也就是SRAM的最大地址加1这样栈从高地址向低地址增长时就不会一上来就撞到堆或全局变量区。如果换成F103ZET6SRAM是64KB 0x10000栈顶就是0x20010000。很多新手直接把C8T6工程改成ZET6忘了改启动文件里的栈顶地址结果程序能编译能烧录运行起来却动不动就HardFault。3.3 链接脚本里的地址配置实操链接脚本Linker Script是内存地址和大小最直接的应用场景。GCC工具链用.ld文件Keil用分散加载文件.sct本质都是在告诉链接器哪段地址放代码、哪段地址放数据。一个F103C8T6的GCC链接脚本内存配置长这样MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这段配置的意思非常直白Flash起始于0x08000000大小64KBRAM起始于0x20000000大小20KB。链接器在分配段时会严格限制在LENGTH范围内如果代码超过了64KB链接阶段就会直接报错。Keil的分散加载文件配置方式稍微绕一点但原理相同LR_IROM1 0x08000000 0x00010000 { ER_IROM1 0x08000000 0x00010000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (RW ZI) } }这里0x00010000就是64KB0x00005000就是20KB都是十六进制直接表达大小。我建议把上一节的换算表放在手边每次新建工程配置分散加载文件时核对一遍。3.4 启动文件里栈顶地址为什么是0x20000000加SRAM大小Cortex-M内核的启动流程是上电后从地址0x00000000读取初始SP值从0x00000004读取复位向量然后跳转执行。由于STM32内部Flash的起始地址是0x08000000通过芯片设计的映射机制0x00000000处看到的就是0x08000000处的数据。所以启动文件startup_stm32f103xb.s里第一行通常写着Stack_Size EQU 0x400 AREA STACK, NOINIT, READWRITE, ALIGN3 Stack_Mem SPACE Stack_Size __initial_sp这里的__initial_sp是栈顶地址链接器把它定位到RAM的最高地址。很多编译环境里栈顶会被链接器自动计算为RAM起始地址加长度但如果手动指定一定要确保等于0x20000000 SRAM大小。栈是从高地址向低地址生长的如果栈顶设小了栈空间容易溢出写坏全局变量和堆数据问题非常隐蔽。4. 寄存器地址的计算外设地址偏移的底层逻辑4.1 总线基地址与外设地址的组成寄存器地址计算是“地址计算方法”的另一大应用场景。STM32把外设寄存器挂在不同总线上外设寄存器的地址由三部分组成总线基地址 外设偏移地址 寄存器偏移地址。以F103为例APB1总线的基地址是0x40000000APB2总线的基地址是0x40010000AHB总线的基地址是0x40018000。每个外设在所属总线上有一个固定的偏移比如GPIOA在APB2上的偏移是0x0800所以GPIOA的基地址 0x40010000 0x0800 0x40010800。注意F4系列完全不同GPIOA的基地址是0x40020000因为F4的外设排列和总线分布跟F1不一样。这也是很多人从F1转到F4后一脸懵的原因。4.2 寄存器地址计算实例以GPIO为例GPIOA的基地址是0x40010800GPIO端口寄存器区的大小是0x400每个寄存器偏移和地址如下寄存器偏移地址作用CRL0x000x40010800端口配置低寄存器PIN0-7CRH0x040x40010804端口配置高寄存器PIN8-15IDR0x080x40010808输入数据寄存器ODR0x0C0x4001080C输出数据寄存器BSRR0x100x40010810置位/复位寄存器BRR0x140x40010814复位寄存器LCKR0x180x40010818配置锁定寄存器如果不用HAL库和标准库直接操作寄存器点亮LED代码是这样的#define GPIOA_BASE 0x40010800UL #define GPIOA_CRH (*(volatile uint32_t *)(GPIOA_BASE 0x04)) #define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE 0x0C)) // 设置PIN8为推挽输出50MHz GPIOA_CRH ~(0xFUL 0); GPIOA_CRH | (0x03UL 0); // 将PIN8输出高电平 GPIOA_ODR | (1UL 8);这里有两个值得注意的细节。一是寄存器地址必须用volatile修饰告诉编译器这个地址的内容可能被硬件修改不要做优化缓存。二是寄存器操作要尽量用“读-改-写”的方式避免影响其他位的状态。实际产品开发中如果做了位带操作Bit Banding还可以把某个寄存器的一位映射到位带区的一个32位地址实现原子性的位操作这又是地址计算的高级用法。4.3 用指针直接操作寄存器的注意事项直接用指针操作寄存器虽然高效但对地址计算的准确性要求很高。我看过太多把地址写错导致外设无响应的例子。这里有几个心得供参考地址必须用无符号整型常量不要用int防止符号扩展出问题。访问寄存器要用uint32_t指针而且尽量使用volatile防止编译器优化。外设寄存器地址核对时把数据手册里的Memory Map和头文件定义对照起来看不要凭记忆写。F1和F4的外设地址差异很大换平台时一定要重新核对头文件里的地址宏。5. 开发环境里的内存查看与常见问题排查5.1 KEIL / STM32CubeIDE里怎么看Flash和RAM占用每次编译完编译输出窗口会有一行Program Size信息对Keil来说长这样Program Size: Code12345 RO-data345 RW-data56 ZI-data1024这个信息的含义要搞清楚Flash占用 Code RO-data RW-dataRAM占用 RW-data ZI-data。为什么RW-data既算Flash又算RAM因为RW-data是初始值非零的全局变量它的初始值在烧录时要存到Flash里上电后再拷贝到RAM中运行。ZI-data是初始值为零或未初始化的数据只占RAM。STM32CubeIDE用的是GCC工具链编译末尾会显示类似这样的统计text data bss dec hex filename 13884 2152 2132 18168 46f8 firmware.elf这里text是代码加只读数据data是已初始化数据bss是未初始化数据。Flash占用全局看text dataRAM占用看data bss。记住这个换算关系后就能准确判断程序离内存上限还有多远。5.2 典型问题排查速查表内存和地址相关的坑五花八门我把实际工作中遇到的高频问题整理成一张速查表问题表现可能原因排查方法编译报L6406E No spaceFlash/RAM配置超出芯片容量查看Map文件找出占用大户确认芯片型号和分散加载文件一致运行时偶发HardFault栈溢出/数组越界/野指针在HardFault_Handler里打断点查看栈回溯减小局部大数组检查malloc返回值程序烧录后不启动栈顶地址错误/启动文件选错核对startup文件与芯片系列匹配检查__initial_sp地址是否等于RAM顶全局变量被莫名篡改栈向上生长撞了堆/全局区调大Stack_Size减少递归和过大的栈上数组HAL库初始化后外设无响应外设基地址宏和芯片不匹配打开头文件核对外设基地址确认总线时钟已使能5.3 我实际踩过的几个坑第一个坑是工程迁移时忘了改链接脚本。把F103C8T6的工程改到F103RCT6Keil里Target选项页的RAM大小没改还是20KB导致程序跑到一半变量区溢出整个系统随机复位。后来我养成习惯换芯片第一件事把Options for Target里的ROM/RAM地址和大小、启动文件、芯片型号三处一起改。第二个坑是堆和栈的设置。默认启动文件里Heap_Size通常是0x200Stack_Size是0x400。如果用了malloc动态分配或者RTOS创建任务这个默认值完全不够。有位同事在FreeRTOS里创建了10个任务栈全部动态分配结果内存被榨干调度器初始化就失败。解决方法是先估算每个任务栈2KB10个任务20KB加上内核和中断栈32KB的F103C8T6根本装不下后来换成了F103RCT6并且把Heap_Size提到0x1000。第三个坑是位带操作时算错了位带别名地址。STM32的位带区在0x20000000到0x200FFFFFSRAM区和0x40000000到0x400FFFFF外设区位带别名区的公式是#define BIT_BAND_ADDR(addr, bit) ((addr 0xF0000000) 0x2000000 ((addr 0xFFFFF) 5) (bit 2))我试过照抄网上的公式没注意掩码范围结果操作的是错误地址位没置上反把相邻位改了。这里提醒一句位带公式里的0x2000000偏移是固定值但和楼上说的内存基地址别搞混0x20000000是SRAM基地址0x02000000是位带别名区的偏移基数两者相差一个数量级写错一个0就是完全不同的地址。最后再分享一个通用排查技巧当怀疑程序访问了非法地址时不要盯着代码干看直接在HardFault_Handler里加上一段现场保护逻辑把PC指针、LR寄存器和几个关键寄存器通过串口打印出来对照反汇编代码定位具体的出错指令。这个方法在排查内存越界、栈溢出、空指针访问时极其有效比一味猜来猜去强太多。搞清STM32的内存地址布局和计算方法很多底层问题都能在写代码阶段提前避开开发效率能提升不止一个档次。
返回列表