读写内部Flash:
1.STM32 的内部 FLASH 简介:
在 STM32 芯片内部有一个 FLASH 存储器,它主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码文件烧录到该内部 FLASH 中,由于 FLASH 存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部 FLASH 中加载代码并运行,见图 STM32 的内部框架图 。
除了使用外部的工具(如下载器)读写内部 FLASH 外,STM32 芯片在运行的时候,也能对自身的内部 FLASH 进行读写,因此,若内部 FLASH 存储了应用程序后还有剩余的空间,我们可以把它像外部 SPI-FLASH 那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。
由于访问内部 FLASH 的速度要比外部的 SPI-FLASH 快得多,所以在紧急状态下常常会使用内部FLASH 存储关键记录;为了防止应用程序被抄袭,有的应用会禁止读写内部 FLASH 中的内容,或者在第一次运行时计算加密信息并记录到某些区域,然后删除自身的部分加密代码,这些应用都涉及到内部 FLASH 的操作。
2.内部 FLASH 的构成
STM32 的内部 FLASH 包含主存储器、系统存储器、OTP 区域以及选项字节区域,它们的地址分布及大小见表 STM32 内部 FLASH 的构成 。
各个存储区域的说明如下:
• 主存储器
一般我们说 STM32 内部 FLASH 的时候,都是指这个主存储器区域,它是存储用户应用程序的空间,芯片型号说明中的 1M FLASH、2M FLASH 都是指这个区域的大小。
**主存储器分为两块,共 2MB,每块内分 12 个扇区,其中包含 4 个 16KB 扇区、1 个64KB 扇区和 7 个 128KB 的扇区。如我们实验板中使用的 STM32F407ZGT6 型号芯片,它的主存储区域大小为 1MB,所以它只包含有表中的扇区 0-扇区 11。**与其它 FLASH 一样,在写入数据前,要先按扇区擦除,而有的时候我们希望能以小规格操纵存储单元,所以 STM32F42x/43x 针对 1MB FLASH 的产品还提供了一种双块的存储格式,见表 1MB 产品的双块存储格式 。(2M 的产品按表 STM32 内部 FLASH的构成 的格式)
通过配置 FLASH 选项控制寄存器 FLASH_OPTCR 的 DB1M 位,可以切换这两种格式,切换成双块模式后,扇区 8-11 的空间被转移到扇区 12-19 中,扇区细分了,总容量不变。
要强调的是:本实验板采用的是 STM32F40x 系列的芯片,它没有双块存储格式,也不存在扇区 12-23,仅 STM32F42x/43x 系列产品才支持扇区 12-23。
• 系统存储区
系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB 以及 CAN 等 ISP 烧录功能。
• OTP 区域
OTP(One Time Program),指的是只能写入一次的存储区域,容量为 512 字节,写入后数据就无法再更改,OTP 常用于存储应用程序的加密密钥。
• 选项字节
选项字节用于配置 FLASH 的读写保护、电源管理中的 BOR 级别、软件/硬件看门狗等功能,这部分共 32 字节。可以通过修改 FLASH 的选项控制寄存器修改。
3.对内部 FLASH 的写入过程
(1)解锁
由于内部 FLASH 空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会给 FLASH 上锁,这个时候不允许设置 FLASH 的控制寄存器,并且不能对修改 FLASH 中的内容。
所以对 FLASH 写入数据前,需要先给它解锁。解锁的操作步骤如下:
(1) 往 Flash 密钥寄存器 FLASH_KEYR 中写入 KEY1 = 0x45670123
(2) 再往 Flash 密钥寄存器 FLASH_KEYR 中写入 KEY2 = 0xCDEF89AB
数据操作位数
在内部 FLASH 进行擦除及写入操作时,电源电压会影响数据的最大操作位数,该电源电压可通过配置 FLASH_CR 寄存器中的 PSIZE 位改变,见表数据操作位数 。
最大操作位数会影响擦除和写入的速度,其中 64 位宽度的操作除了配置寄存器位外,还需要在Vpp 引脚外加一个 8-9V 的电压源,且其供电时间不得超过一小时,否则 FLASH 可能损坏,所以64 位宽度的操作一般是在量产时对 FLASH 写入应用程序时才使用,大部分应用场合都是用 32位的宽度。
(2)擦除扇区
在写入新的数据前,需要先擦除存储区域,STM32 提供了扇区擦除指令和整个 FLASH 擦除 (批量擦除) 的指令,批量擦除指令仅针对主存储区。
扇区擦除的过程如下:
(1) 检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,以确认当前未执行任何 Flash 操作;
(2) 在 FLASH_CR 寄存器中,将“激活扇区擦除寄存器位 SER ”置 1,并设置“扇区编号寄存器位 SNB”,选择要擦除的扇区;
(3) 将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,开始擦除;
(4) 等待 BSY 位被清零时,表示擦除完成。
(3)查看工程的空间分布
由于内部 FLASH 本身存储有程序数据,若不是有意删除某段程序代码,一般不应修改程序空间的内容,所以在使用内部 FLASH 存储其它数据前需要了解哪一些空间已经写入了程序代码,存储了程序代码的扇区都不应作任何修改。通过查询应用程序编译时产生的“*.map”后缀文件,可以了解程序存储到了哪些区域,它在工程中的打开方式见图打开工程的 map 文件 ,也可以到工程目录中的“Listing”文件夹中找到。如下图:
可以看到ROM Size的大小就是2636,这远远没达到第一块扇区,但是我们要注意的是,在进行扇区擦除的时候不能擦除我们存储我们程序的扇区,所以保险起见,我们可以选择第二进行擦除和读写操作
这一段是某工程的 ROM 存储器分布映像,在 STM32 芯片中,ROM 区域的内容就是指存储到内部 FLASH 的代码。其中Code是代码指令,Data是数据
程序 ROM 的加载与执行空间:
上述说明中有两段分别以“Load Region LR_ROM1”及“Execution RegionER_IROM1”开头的内容,它们分别描述程序的加载及执行空间,在芯片刚上电运行时,会加载程序及数据,例如它会从程序的存储区域加载到程序的执行区域,还把一些已初始化的全局变量从 ROM 复制到 RAM空间,以便程序运行时可以修改变量的内容。加载完成后,程序开始从执行区域开始执行。
在上面 map 文件的描述中,我们了解到加载及执行空间的基地址 (Base) 都是 0x08000000,它正好是 STM32 内部 FLASH 的首地址,即 STM32 的程序存储空间就直接是执行空间;它们的大小(Size) 分别为 0x00000a4c 及 0x00000a20,执行空间的 ROM 比较小的原因就是因为部分 RW-data类型的变量被拷贝到 RAM 空间了;它们的最大空间 (Max) 均为 0x00100000,即 1M 字节,它指的是内部 FLASH 的最大空间。计算程序占用的空间时,需要使用加载区域的大小进行计算,本例子中应用程序使用的内部FLASH 是从 0x0800000至(0x08000000+0x00000aa4) 地址的空间区域。
(4)ROM 空间分布表
在加载及执行空间总体描述之后,紧接着一个 ROM 详细地址分布表,它列出了工程中的各个段(如函数、常量数据) 所在的地址 BaseAddr 及占用的空间 Size,列表中的 Type 说明了该段的类型,CODE 表示代码,DATA 表示数据,而 PAD 表示段之间的填充区域,它是无效的内容,PAD 区域往往是为了解决地址对齐的问题。
观察表中的最后一项,它的基地址是 0x08000a70,大小为 0x00000020,可知它占用的最高的地址空间为 0x08000a20,跟执行区域的最高地址 0x00000a90 一样,但它们比加载区域说明中的最高地址 0x8000a4c 要小,所以我们以加载区域的大小为准。对比表 STM32 内部 FLASH 的构成 的内部 FLASH 扇区地址分布表,可知仅使用扇区 0 就可以完全存储本应用程序,所以从扇区 1(地址 0x08004000) 后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程序空间的数据。
(5)操作内部 FLASH 的库函数
为简化编程,STM32 标准库提供了一些库函数,它们封装了对内部 FLASH 写入数据操作寄存器的过程。
/*开锁函数*/voidFLASH_Unlock(void);/*扇区擦除函数*//*参数一是选择擦除的扇区,参数二是选择以多少字节来擦除,我们一般选择32位,这样擦除的快点*/FLASH_StatusFLASH_EraseSector(uint32_tFLASH_Sector,uint8_tVoltageRange);/*这三个分别是字,半字,字节写入函数*//*参数一是选择要写入数据的地址,参数二就是要写入的数据*/FLASH_StatusFLASH_ProgramWord(uint32_tAddress,uint32_tData);FLASH_StatusFLASH_ProgramHalfWord(uint32_tAddress,uint16_tData);FLASH_StatusFLASH_ProgramByte(uint32_tAddress,uint8_tData);/*上锁函数*/voidFLASH_Lock(void);4.下面我们来编写内部Flash读写程序:
inter_flash.c
#include"inter_flash.h"#defineW_ADDRESS0x08008000voidIniter_Flash(void){char*p=(char*)W_ADDRESS;FLASH_Status status;/*开锁*/FLASH_Unlock();/*扇区擦除*/status=FLASH_EraseSector(FLASH_Sector_2,VoltageRange_3);if(status==FLASH_COMPLETE){printf("\r\n擦除成功");}else{printf("\r\n擦除失败");}/*写入数据*/status=FLASH_ProgramByte(W_ADDRESS,0x12);if(status==FLASH_COMPLETE){printf("\r\n写入成功");}else{printf("\r\n写入失败");}printf("\r\n读出数据的地址:0x%x,读出的数据:%d",W_ADDRESS,*p);/*上锁*/FLASH_Lock();}main.c
intmain(void){Systick_Init();LED_Init();USARTConfig_Init();printf("内部Flash读写程序实验\r\n");Initer_Flash();while(1){}}
可以看到也是读写成功了
设置 FLASH 的读写保护及解除
1.选项字节与读写保护
选项字节是一段特殊的 FLASH 空间,STM32 芯片会根据它的内容进行读写保护、复位电压等配置,选项字节的构成见表选项字节的构成 。
选项字节具体的数据位配置说明见表选项字节具体的数据位配置说明 。
我们主要讲解选项字节配置中的 RDP 位和 PCROP 位,它们分别用于配置读保护级别及代码读出保护。
2.RDP 读保护级别:
修改选项字节的 RDP 位的值可设置内部 FLASH 为以下保护级别:
• 0xAA:级别 0,无保护
这是 STM32 的默认保护级别,它没有任何读保护,读取内部 FLASH 及“备份 SRAM”的内容都没有任何限制。(注意这里说的“备份 SRAM”是指 STM32 备份域的 SRAM 空间,不是指主 SRAM,下同)
• 其它值:级别 1,使能读保护
把 RDP 配置成除 0xAA 或 0xCC 外的任意数值,都会使能级别 1 的读保护。在这种保护下,若使用调试功能(使用下载器、仿真器)或者从内部 SRAM 自举时都不能对内部 FLASH 及备份 SRAM 作任何访问(读写、擦除都被禁止);而如果 STM32 是从内部 FLASH 自举时,它允许对内部 FLASH 及备份 SRAM 的任意访问。
也就是说,在级别 1 模式下,任何尝试从外部访问内部 FLASH 内容的操作都被禁止,例如无法通过下载器读取它的内容,或编写一个从内部 SRAM 启动的程序,若该程序读取内部 FLASH,会被禁止。而如果是芯片自己访问内部 FLASH,是完全没有问题的,例如前面的“读写内部 FLASH”实验中的代码自己擦写内部 FLASH 空间的内容,即使处于级别 1 的读保护,也能正常擦写。
**当芯片处于级别 1 的时候,可以把选项字节的 RDP 位重新设置为 0xAA,恢复级别 0。在恢复到级别 0 前,芯片会自动擦除内部 FLASH 及备份 SRAM 的内容,即降级后原内部 FLASH 的代码会丢失。**在级别 1 时使用 SRAM 自举的程序也可以访问选项字节进行修改,所以如果原内部 FLASH 的代码没有解除读保护的操作时,可以给它加载一个 SRAM 自举的程序进行保护降级,后面我们将会进行这样的实验。
• 0xCC:级别 2,禁止调试
把 RDP 配置成 0xCC 值时,会进入最高级别的读保护,且设置后无法再降级,它会永久禁止用于调试的 JTAG 接口(相当于熔断)。在该级别中,除了具有级别 1 的所有保护功能外,进一步禁止了从 SRAM 或系统存储器的自举(即平时使用的串口 ISP 下载功能也失效),JTAG 调试相关的功能被禁止,选项字节也不能被修改。它仅支持从内部 FLASH 自举时对内部 FLASH 及 SRAM 的访问(读写、擦除)。
由于设置了级别 2 后无法降级,也无法通过 JTAG、串口 ISP 等方式更新程序,所以使用这个级别的保护时一般会在程序中预留“后门”以更新应用程序,若程序中没有预留后门,芯片就无法再更新应用程序了。所谓的“后门”是一种 IAP 程序(In Application Program),它通过某个通讯接口获取将要更新的程序内容,然后利用内部 FLASH 擦写操作把这些内容烧录到自己的内部 FLASH 中,实现应用程序的更新。
不同级别下的访问限制见图不同级别下的访问限制。
不同保护级别之间的状态转换见图不同级别间的状态转换 。
3.修改选项字节的过程
修改选项字节的内容可修改各种配置,但是,当应用程序运行时,无法直接通过选项字节的地址改写它们的内容,例如,接使用指针操作地址 0x1FFFC0000 的修改是无效的。要改写其内容时必须设置寄存器 FLASH_OPTCR 及 FLASH_OPTCR1 中的对应数据位,寄存器的与选项字节对应位置见图 FLASH_OPTCR 寄存器说明 及图 FLASH_OPTCR1 寄存器说明 ,详细说明请查阅《STM32参考手册》。
注意:Flash 选项控制寄存器位于 RAM 内存中,也就是说,直接配置该寄存器并不能实现对 Flash 内部内容的操控。不过,只要配置完该寄存器后,将 bit1 位置 1,就可以将该寄存器的配置存储到 Flash 内部。
默认情况下,FLASH_OPTCR 寄存器中的第 0 位 OPTLOCK 值为 1,它表示选项字节被上锁,需要解锁后才能进行修改,当寄存器的值设置完成后,对 FLASH_OPTCR 寄存器中的第 1 位 OPTSTRT值设置为 1,硬件就会擦除选项字节扇区的内容,并把 FLASH_OPTCR/1 寄存器中包含的值写入到选项字节。
所以,修改选项字节的配置步骤如下:
(1) 解锁,在 Flash 选项密钥寄存器 (FLASH_OPTKEYR) 中写入 OPTKEY1 = 0x0819 2A3B;接着在 Flash 选项密钥寄存(FLASH_OPTKEYR) 中写入 OPTKEY2 = 0x4C5D 6E7F。
(2) 检查 FLASH_SR 寄存器中的 BSY 位,以确认当前未执行其它 Flash 操作。
(3) 在 FLASH_OPTCR 和/或 FLASH_OPTCR1 寄存器中写入选项字节值。
(4) 将 FLASH_OPTCR 寄存器中的选项启动位 (OPTSTRT) 置 1。
(5) 等待 BSY 位清零,即写入完成。
4.设置Flash读写保护代码如下:
voidRead_Protect(void){FLASH_OB_Unlock();/*判断是否开启读保护,没有就进入该if语句*/if(FLASH_OB_GetRDP()==RESET){printf("\r\n准备开启读保护");/*开启读保护*/FLASH_OB_RDPConfig(OB_RDP_Level_1);}/*启动选项,将选项字节的配置存入到Flash*/FLASH_OB_Launch();if(FLASH_OB_Launch()==FLASH_COMPLETE){printf("\r\n开启读保护成功");}FLASH_OB_Lock();while(1){printf("\r\n按下key1按键取消读保护");if(Key_State()==1){FLASH_OB_Unlock();/*判断是否开启读保护,有就进入该if语句*/if(FLASH_OB_GetRDP()==SET){printf("\r\n准备取消读保护");/*开启读保护*/FLASH_OB_RDPConfig(OB_RDP_Level_0);}/*读保护等级降级会删除Flash区的程序代码,所以以下程序不会执行*//*启动选项,将选项字节的配置存入到Flash*/FLASH_OB_Launch();if(FLASH_OB_Launch()==FLASH_COMPLETE){printf("\r\n取消读保护成功");}FLASH_OB_Lock();}}}voidWrite_Protect(void){FLASH_OB_Unlock();/*判断是否开启写保护,没有就进入该if语句*/if((FLASH_OB_GetWRP()&0xFFF)==0xFFF){printf("\r\n准备开启写保护");/*开启扇区2写保护*/FLASH_OB_WRPConfig(OB_WRP_Sector_2,ENABLE);}/*启动选项,将选项字节的配置存入到Flash*/if(FLASH_OB_Launch()==FLASH_COMPLETE){printf("\r\n开启写保护成功");}FLASH_OB_Lock();while(1){if(Key_State()==1){FLASH_OB_Unlock();/*判断是否开启2扇区写保护,有就进入该if语句*/if((FLASH_OB_GetWRP()&0xFFB)==0xFFB){printf("\r\n取消2扇区写保护");/*开启扇区2写保护*/FLASH_OB_WRPConfig(OB_WRP_Sector_2,DISABLE);}/*启动选项,将选项字节的配置存入到Flash*/if(FLASH_OB_Launch()==FLASH_COMPLETE){printf("\r\n写保护取消保护成功");}FLASH_OB_Lock();}}}注意事项:如果你的应用程序代码中没有编写解除读写保护的逻辑,那么一旦芯片进入读写保护状态,你将无法通过常规方式(如 JTAG、串口 ISP 等)直接烧录程序来解除保护。此时唯一的解决办法是:将一段专门用于解除读写保护的程序烧录到 SRAM 内存中运行,通过该程序修改选项字节,从而解除读写保护。
在SRAM中调试代码(重点):
前言:
一般情况下,我们在 MDK 中编写工程应用后,调试时都是把程序下载到芯片的内部 FLASH 运行测试的,代码的 CODE 及 RW-data 的内容被写入到内部 FLASH 中存储。但在某些应用场合下却不希望或不能修改内部 FLASH 的内容,这时就可以使用 RAM 调试功能了,**它的本质是把原来存储在内部 FLASH 的代码 (CODE 及 RW-data 的内容) 改为存储到 SRAM 中 (内部 SRAM 或外部 SRAM 均可),芯片复位后从内部 SRAM 中加载代码并运行。**把代码下载到 RAM 中调试有如下优点:
• 下载程序非常快。RAM 存储器的写入速度比在内部 FLASH 中要快得多,且没有擦除过程,因此在 RAM 上调试程序时程序几乎是秒下的,对于需要频繁改动代码的调试过程,能节约很多时间,省去了烦人的擦除与写入 FLASH 过程。另外,STM32 的内部 FLASH 可擦除次数为 1 万次,虽然一般的调试过程都不会擦除这么多次导致 FLASH 失效,但这确实也是一个考虑使用 RAM 的因素。
• 不改写内部 FLASH 的原有程序。
• 对于内部 FLASH 被锁定的芯片,可以把解锁程序下载到 RAM 上,进行解锁。
相对地,把代码下载到 RAM 中调试有如下缺点:
• 存储在 RAM 上的程序掉电后会丢失,不能像 FLASH 那样保存。
• 若使用 STM32 的内部 SRAM 存储程序,程序的执行速度与在 FLASH 上执行速度无异,但SRAM 空间较小。
• 若使用外部扩展的 SRAM 存储程序,程序空间非常大,但 STM32 读取外部 SRAM 的速度比读取内部 FLASH 慢,这会导致程序总执行时间增加,因此在外部 SRAM 中调试的程序无法完美仿真在内部 FLASH 运行时的环境。另外,由于 STM32 无法直接从外部 SRAM 中启动且应用程序复制到外部 SRAM 的过程比较复杂 (下载程序前需要使 STM32 能正常控制外部 SRAM),所以在很少会在 STM32 的外部 SRAM 中调试程序
1.STM32 的启动方式
在前面讲解的 STM32 启动代码章节了解到 CM-3 内核在离开复位状态后的工作过程如下,见图复位序列 :
(1) 从地址 0x00000000 处取出栈指针 MSP 的初始值,该值就是栈顶的地址。
(2) 从地址 0x00000004 处取出程序指针 PC 的初始值,该值指向复位后应执行的第一条指令。
上述过程由内核自动设置运行环境并执行主体程序,因此它被称为自举过程。
虽然内核是固定访问 0x00000000 和 0x00000004 地址的,但实际上这两个地址可以被重映射到其它地址空间。以 STM32F407 为例,根据芯片引出的 BOOT0 及 BOOT1 引脚的电平情况,这两个地址可以被映射到内部 FLASH、内部 SRAM 以及系统存储器中,不同的映射配置见表 BOOT 引脚的不同设置对 0 地址的映射。
内核在离开复位状态后会从映射的地址中取值给栈指针 MSP 及程序指针 PC,然后执行指令,我们一般以存储器的类型来区分自举过程,例如内部 FLASH 启动方式、内部 SRAM 启动方式以及系统存储器启动方式。
(1) 内部 FLASH 启动方式
当芯片上电后采样到 BOOT0 引脚为低电平时,0x00000000 和 0x00000004 地址被映射到内部FLASH 的首地址 0x08000000 和0x08000004。因此,内核离开复位状态后,读取内部 FLASH 的0x08000000 地址空间存储的内容,赋值给栈指针 MSP,作为栈顶地址,再读取内部 FLASH 的0x08000004 地址空间存储的内容,赋值给程序指针 PC,作为将要执行的第一条指令所在的地址。具备这两个条件后,内核就可以开始从 PC 指向的地址中读取指令执行了。
(2) 内部 SRAM 启动方式
类似地,当芯片上电后采样到 BOOT0 和 BOOT1 引脚均为高电平时,0x00000000 和 0x00000004地址被映射到内部 SRAM 的首地址 0x20000000 和 0x20000004,内核从 SRAM 空间获取内容进行自举。
在实际应用中,由启动文件 starttup_stm32f40xx.s 决定了 0x00000000 和 0x00000004 地址存储什么内容,链接时,由分散加载文件 (sct) 决定这些内容的绝对地址,即分配到内部 FLASH 还是内部 SRAM。
(3) 系统存储器启动方式
当芯片上电后采样到 BOOT0 引脚为高电平,BOOT1 为低电平时,内核将从系统存储器的0x1FFF0000 及 0x1FFF0004 获取 MSP 及 PC 值进行自举。系统存储器是一段特殊的空间,用户不能访问,ST 公司在芯片出厂前就在系统存储器中固化了一段代码。因而使用系统存储器启动方式时,内核会执行该代码,该代码运行时,会为 ISP 提供支持 (In System Program),如检测USART1/2、CAN2 及 USB 通讯接口传输过来的信息,并根据这些信息更新自己内部 FLASH 的内容,达到升级产品应用程序的目的,因此这种启动方式也称为 ISP 启动方式。
2.内部 FLASH 的启动过程
下面我们以最常规的内部 FLASH 启动方式来分析自举过程,主要理解 MSP 和 PC 内容是怎样被存储到 0x08000000 和 0x08000004 这两个地址的。
见图启动代码中存储的 MSP 及 PC 指针内容 ,这是 STM32F407 默认的启动文件的代码,启动文件的开头定义了一个大小为 0x400 的栈空间,且栈顶的地址使用标号“__initial_sp”来表示;**在图下方定义了一个名为“Reset_Handler”的子程序,它就是我们总是提到的在芯片启动后第一个执行的代码。**在汇编语法中,程序的名字和标号都包含它所在的地址,因此,我们的目标是把“__initial_sp”和“Reset_Handler”赋值到 0x08000000 和 0x08000004 地址空间存储,这样内核自举的时候就可以获得栈顶地址以及第一条要执行的指令了。在启动代码的中间部分,使用了汇编关键字“DCD”把“__initial_sp”和“Reset_Handler”定义到了最前面的地址空间。
在启动文件中把设置栈顶及首条指令地址到了最前面的地址空间,但这并没有指定绝对地址,各种内容的绝对地址是由链接器根据分散加载文件 (*.sct) 分配的,STM32F407ZGT6 型号的默认分散加载文件配置如下图:
**分散加载文件把加载区和执行区的首地址都设置为 0x08000000,正好是内部 FLASH 的首地址,因此汇编文件中定义的栈顶及首条指令地址会被存储到 0x08000000 和 0x08000004 的地址空间。**类似地,如果我们修改分散加载文件,把加载区和执行区的首地址设置为内部 SRAM 的首地址0x20000000,那么栈顶和首条指令地址将会被存储到 0x20000000 和 0x20000004 的地址空间了。
为了进一步消除疑虑,我们可以查看反汇编代码及 map 文件信息来了解各个地址空间存储的内容,见图从反汇编代码及 map 文件查看存储器的内容 ,这是多彩流水灯工程编译后的信息,它的启动文件及分散加载文件都按默认配置。
其中反汇编代码是使用 fromelf 工具从 axf 文件生成的,具体过程可参考前面的章节了解。
从反汇编代码可了解到,这个工程的0x08000000地址存储的值为0x20000400,0x08000004地址存储的值为0x0800019d。查看 map 文件,这两个值正好是栈顶地址__initial_sp以及首条指令Reset_Handler的地址。下载器会根据 axf 文件(bin、hex 类似)存储相应的内容到内部 FLASH 中。
由此可知,当 BOOT0 为低电平时,内核复位后,从0x08000000读取到栈顶地址为0x20000400,从而了解到子程序的栈空间范围;再从0x08000004读取到第一条指令的存储地址为0x0800019d,于是跳转到该地址执行代码,即从Reset_Handler开始运行,依次执行SystemInit、__main(包含分散加载代码),最后跳转到 C 语言的main函数。
不过,我们到实际的 map 文件可以看到,这个__initial_sp并不是0x20010400,因为在它前面还有0x30字节大小的全局变量。我们到 sct 文件可以看到,.ANY(+RW +ZI)选择剩余所有节区 RW 及 ZI 属性的内容都分配到执行域RW_IRAM1中。所以我们的__initial_sp = 栈段起始 + Stack_Size。
可以这样理解:SRAM 是一个巨大的存储空间,而__initial_sp是一个0x400字节大小的存储空间的顶部地址值(它不一定为0x400大小,这个可以通过更改Stack_Size调节分配的)。这也是为什么我们定义一个数组的时候不要定义成局部变量,因为如果这个数组太大超过这个栈大小,就会导致栈溢出。
对比在内部 FLASH 中运行代码的过程,可了解到若希望在内部 SRAM 中调试代码,需要设置启动方式为从内部 SRAM 启动,修改分散加载文件控制代码空间到内部 SRAM 地址,以及把生成程序下载到芯片的内部 SRAM 中。
实验:在内部 SRAM 中调试代码
硬件:
首先我们要将BOOT0和BOOT1引脚把 BOOT0 和 BOOT1 引脚都使用跳线帽连接到 3.3V,使芯片从 SRAM 中启动。
软件设计:
(1) 在原工程的基础上创建一个调试版本;
(2) 修改分散加载sct文件,使链接器把代码分配到内部 SRAM 空间;
(3) 添加宏修改 STM32 的向量表地址;
(4) 修改仿真器和下载器的配置,使程序能通过下载器存储到内部 SRAM;
(5) 尝试给 SRAM 下载程序或仿真调试
3.创建工程的调试版本:
由于在 SRAM 中运行的代码一般仅用于调试,调试完毕后,在实际生产环境中仍然使用在内部 FLASH 中运行的代码,因此我们希望能够便捷地在调试版和发布版代码之间切换。MDK 的“Manage Project Items”功能可实现这一点,使用它可管理多个不同配置的工程,见图使用 Manage_Project_Items 添加一个工程配置。
点击“Manage Project Items”按钮,在弹出对话框左侧的“Project Target”一栏包含了原工程的名字,如图中的原工程名为“多彩流水灯”,右侧是该工程包含的文件。为了便于调试,我们在左侧的“Project Target”一栏添加一个工程名,如图中输入“SRAM_调试”,输入后点击 OK 即可。这个“SRAM_调试”版本的工程会复制原“多彩流水灯”工程的配置,后面我们再进行修改。
当需要切换工程版本时,点击 MDK 工程名的下拉菜单可选择目标工程,在不同的工程中,所有配置都是独立的,例如芯片型号、下载配置等等,但如果两个工程共用了同一个文件,对该文件的修改会同时影响两个工程,例如这两个工程都使用同一个 main 文件,我们在 main 文件修改代码,两个工程都会被修改。
在下面的教程中我们将切换到“SRAM_ 调试”版本的工程,配置出一个代码会被存储到 SRAM的多彩流水灯工程。
(1)配置分散加载文件
随后我们就配置好新的sct文件路径了
因此,在修改分散加载文件后,务必在 MDK 的“Options for Target”中为当前工程重新指定新的 sct 文件路径,否则 MDK 会使用默认的 sct 文件(即分配到内部 FLASH 的配置),导致我们辛苦修改好的 SRAM 分配配置在下次编译或调试时被覆盖回 Flash 分配,SRAM 调试也就无法生效了。
下面我们修改sct文件:
可以看到,默认的分散加载文件把加载区(Load Region LR_IROM1)和执行区(Execution Region ER_IROM1)的首地址都设置为0x08000000,正好是内部 FLASH 的首地址。因此,汇编启动文件中用DCD定义的栈顶地址__initial_sp和首条指令Reset_Handler会被链接器分配到0x08000000和0x08000004这两个地址空间存储。当 BOOT0 为低电平、芯片从内部 FLASH 启动时,内核复位后就会到0x08000000取出栈顶地址赋给 MSP,再到0x08000004取出首条指令地址赋给 PC,从而开始执行程序。
而我们要在 SRAM 中调试,就需要把这里的加载区、执行区首地址都改成内部 SRAM 的首地址0x20000000,这样栈顶和首条指令地址就会被存储到0x20000000和0x20000004,配合 BOOT0/BOOT1 都拉高(从 SRAM 启动),内核复位后就会从 SRAM 中取值自举了。
配置完毕编译工程后可在 map 文件中查看具体的空间地址分配。
可以看到,修改后的分散加载文件把加载区(Load Region LR_IROM1)和执行区(Execution Region ER_IROM1)的首地址都改成了内部 SRAM 的首地址0x20000000。因此,链接器把启动文件中用DCD定义的栈顶地址__initial_sp和首条指令Reset_Handler分配到了0x20000000和0x20000004这两个地址空间存储。
从反汇编代码可以看到,0x20000000地址存储的值0x20010430为栈顶地址,0x20000004地址存储的值为0x200001a1,这正是Reset_Handler的地址。也就是说,Reset_Handler这条首条指令被烧录到了 SRAM 中,而不是内部 FLASH。
当 BOOT0 和 BOOT1 引脚都拉高、芯片从内部 SRAM 启动时,内核复位后就会到0x20000000取出栈顶地址0x20010430赋给 MSP,再到0x20000004取出0x200001a1赋给 PC,从而跳转到 SRAM 中的Reset_Handler开始执行程序。这也验证了我们的配置生效了——代码确实被分配到了 SRAM 空间。
(2)配置中断向量表
由于 startup_stm32f40xx.s 文件中的启动代码不是指定到绝对地址的,经过它由链接器决定应存储到内部 FLASH 还是 SRAM,所以 SRAM 版本工程中的启动文件不需要作任何修改。
重点在于启动文件定义的中断向量表被存储到内部 FLASH 和内部 SRAM 时,这两种情况对内核的影响是不同的,内核会根据它的“向量表偏移寄存器 VTOR”配置来获取向量表,即中断服务函数的入口。VTOR 寄存器是由启动文件中 Reset_Handle 中调用的库函数 SystemInit 配置的
也就是说,SystemInit函数中通过条件编译#ifdef VECT_TAB_SRAM来决定向量表的存放位置:
- 定义了
VECT_TAB_SRAM:向量表被重定位到 SRAM 的首地址0x20000000,即把中断向量表存储到 SRAM 中; - 未定义
VECT_TAB_SRAM:向量表保持默认位置,即存储到内部 FLASH 的首地址0x08000000。
由于我们在 SRAM 中调试代码,代码本身已经被链接器分配到了 SRAM 空间,中断向量表也必须跟着放到 SRAM 中,否则内核通过 VTOR 寄存器获取中断服务函数入口时,会去 FLASH 中找向量表,而 FLASH 中并没有我们 SRAM 版本的向量表,就会导致中断无法正常响应。因此,在 SRAM 调试工程中,我们需要在编译选项中定义VECT_TAB_SRAM宏,让SystemInit把向量表重定位到 SRAM,从而保证中断向量表与代码所在位置一致。
见编译选项中加入宏 VECT_TAB_SRAM ,在“Options for Target-> c/c+±>Define”框中输入宏 VECT_TAB_SRAM,注意它与其它宏之间要使用英文逗号分隔开。
配置完成后重新编译工程,即可生成存储到 SRAM 空间地址的代码指令。
(3)修改 FLASH 下载配置
这个配置对话框原本是用于设置芯片内部 FLASH 信息的,当我们点击 MDK 的下载(LOAD)或(调试)按钮时,它会从此处加载配置然后下载程序到 FLASH 中,而在上图中我们把它的配置修改成下载到内部 SRAM 了,各个配置的解释如下:
• 把“Download Function”中的擦除选项配置为“Do not Erase”。这是因为数据写入到内部SRAM 中不需要像 FLASH 那样先擦除后写入。在本工程中,如果我们不选择“Do not Erase”的话,会因为擦除过程导致下载出错。
•“RAM for Algorithm”一栏是指“编程算法”(Programming Algorithm) 可使用的 RAM 空间,下载程序到 FLASH 时运行的编程算法需要使用 RAM 空间,在默认配置中它的首地址为0x20000000,即内部 SRAM 的首地址,但由于我们的分散加载文件配置,0x20000000 地址开始的 64KB 实际为虚拟 ROM 空间,实际的 RAM 空间是从地址 0x20010000 开始的,所以这里把算法 RAM 首地址更改为本工程中实际作为 RAM 使用的地址。若编程算法使用的 RAM 地址与虚拟 ROM 空间地址重合的话,会导致下载出错。
•“Programming Algorithm”一栏中是设置内部 FLASH 的编程算法,编程算法主要描述了FLASH 的地址、大小以及扇区等信息,MDK 根据这些信息把程序下载到芯片的 FLASH中,不同的控制器芯片一般会有不同的编程算法。由于 MDK 没有内置 SRAM 的编程算法,所以我们直接在原来的基础上修改它的基地址和空间大小,把它改成虚拟 ROM 的空间信息。
从这个例子可了解到,这里的配置是跟我们的分散加载文件的实际 RAM 空间和虚拟 ROM 空间信息是一致的,若您的分散加载文件采用不同的配置,这个下载选项也要作出相应的修改,不能照抄本例子的空间信息。
这个配置是针对程序下载的,配置完成后点击 MDK 的下载按钮(LOAD),程序会被下载到 STM32的内部 SRAM 中。复位后程序会正常运行 (前提是 BOOT0 和 BOOT 要被设置为 SRAM 启动) 。芯片掉电后这个存储在 SRAM 的程序会丢失,想恢复的话必须要重新下载程序。
(4)仿真器配置
上面的下载配置使得程序能够加载到 SRAM 中全速运行,但作为 SRAM 版本的程序,其功能更着重于调试,也就是说我们希望它能支持平时使用按钮 (调试、debug) 时进行的硬件在线调试、单步运行等功能。
要实现调试功能,还要在“Options for Target->Debug->Settings”中进行配置,见图设置仿真前检查代码并下载程序到 FLASH 中 。
在图中我们需要勾选“Verify Code Download”及“Download to FLASH”选项,也就是说点击调试按钮后,本工程的程序会被下载到内部 SRAM 中,只有勾选了这两个选项才能正常仿真。
经过这样的配置后,硬件仿真时与平时内部 FLASH 版本的程序无异,支持软件复位、单步运行、全速运行以及查看各种变量值等 (同样地,前提是 BOOT0 和 BOOT 要被设置为 SRAM 启动)
4.总结:
经过这样的配置后,硬件仿真时与平时内部 FLASH 版本的程序无异,支持软件复位、单步运行、全速运行以及查看各种变量值等(同样地,前提是 BOOT0 和 BOOT1 要接 3.3V,设置为 SRAM 启动)。
第一步:硬件配置。将 BOOT0 和 BOOT1 引脚都接 3.3V,让芯片从内部 SRAM 启动。内核复位后,会到 SRAM 的 0x20000000 地址取出栈顶地址赋给 MSP,再到 0x20000004 地址取出 Reset_Handler 的地址赋给 PC,作为下一个要执行的程序。
第二步:软件配置。那么 0x20000000 和 0x20000004 处的内容是从哪里来的呢?分两个来源:
- 位置由 sct 定:(RESET, +First) 把启动文件里 RESET 节区钉在 0x20000000 最前面。RESET 是启动文件里 AREA RESET 定义的节区名,节区里装的就是向量表(向量表必须位于启动地址的最前面,前两项依次是栈顶、Reset_Handler)。
- 内容由链接器填:第一项 = __initial_sp = 栈段起始 + Stack_Size,第二项 = Reset_Handler 在代码段里的排位地址。
另外,RAM 调试还需在 system_stm32f4xx.c 里定义宏 VECT_TAB_SRAM:SRAM 启动后 SystemInit 默认把 VTOR 设回 Flash 基址,定义该宏后改设 SRAM 基址,中断向量才不会查错表(Debug_RAM.ini 只在进入调试那一刻设一次 VTOR,软件复位后由 SystemInit 兜底)。
整个流程如下:
- 编译:将 .c 源文件编译成 .o 目标文件(此时还没有绝对地址,地址不固定);
- 链接:链接器根据 sct 文件(分散加载文件)的地址分配规则,把各个 .o 文件链接生成 .axf 文件(此时所有符号都有了确定的地址);
- 格式转换(按需):只有走串口 ISP、IAP 或第三方烧录工具时才用 fromelf 把 axf 转 bin/hex(注意 bin 是纯裸数据、不含地址,hex 才带地址)。Keil+DAP 下载——无论 SRAM 调试还是烧 Flash——都直接用 axf,用不到这一步;
- 加载:调试器读 axf 里的地址信息,把程序加载进 SRAM。
核心理解:sct 里 LR_IROM1 定义加载地址(程序装到哪个地址),ER_IROM1 定义运行地址(代码指令和常量运行时所在的地址)。烧录/加载跟随加载地址;本实验两者重合(都从 0x20000000 起)。Flash 配置下两者分离:代码加载=运行(Flash 就地执行);RW 数据加载在 Flash(初值副本)、运行在 RAM——不重合的部分由链接器记账(Region 表)、启动代码搬运(scatterload),搬完才进 main。一句话:重合 = 就地,不重合 = 搬家。
- LR_IROM1 → 加载地址 = 装在哪(下载/烧录时内容写到哪个地址)
- ER_IROM1 → 运行地址 = 指令和常量在哪(CPU 从这取指令、读常量)
- RW_IRAM1 → 运行地址 = 变量在哪(CPU 从这读写变量)