开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6
20.1 Cortex-M存储结构的工作原理
20.1.1 Cortex-M内核的存储器映射
存储器映射是指把芯片中或芯片外的FLASH,RAM,外设,BOOTBLOCK等进行统一编址。即用地址来表示对象。这个地址绝大多数是由厂家规定好的,用户只能用而不能改。用户只能在挂外部RAM或FLASH的情况下可进行自定义。
如下图,是Cortex-M3存储器映射结构图。
Cortex-M3是32位的内核,因此其PC指针可以指向2^32=4G的地址空间,也就是0x0000_0000——0xFFFF_FFFF这一大块空间。根据图中描述,Cortex-M3内核将0x0000_0000——0xFFFF_FFFF这块4G大小的空间分成8大块:代码、SRAM、外设、外部RAM、外部设备、专用外设总线-内部、专用外设总线-外部、特定厂商等,因此使用该内核的设计者必须按照这个进行各自芯片的存储器结构设计。
20.1.2 Cortex-M存储器结构
首先,我们对比一下Cortex-M3存储器结构和STM32存储器结构:
图中可以很清晰的看到,STM32的存储器结构和Cortex-M3的很相似,不同的是,STM32加入了很多实际的东西,如:Flash、SRAM等。只有加入了这些东西,才能成为一个拥有实际意义的、可以工作的处理芯片——STM32。
STM32的存储器地址空间被划分为大小相等的8块区域,每块区域大小为512MB。
对STM32存储器知识的掌握,实际上就是对Flash和SRAM这两个区域知识的掌握。
20.2 FLASH读写数据
20.2.1 STM32的Flash
STM32的Flash,严格说,应该是Flash模块。该Flash模块包括:Flash主存储区(Main memory)、Flash信息区(Information block),以及Flash存储接口寄存器区(Flash memory interface)。三个组成部分分别在0x0000 0000——0xFFFF FFFF不同的区域,如下表所示。
STM32的闪存模块由:主存储器、信息块和闪存储器块3部分组成。
主存储器,该部分用来存放代码和数据常数(如加const类型的数据)。对于大容量产品,其被划分为256页,每页2K,注意,小容量和中容量产品则每页只有1K字节。从下图可以看出主存储起的起始地址为0X08000000,B0、B1都接GND的时候,就从0X08000000开始运行代码。
信息块,该部分分为2个部分,其中启动程序代码,是用来存储ST自带的启动程序,
用于串口下载,当B0接3.3V,B1接GND时,运行的就这部分代码,用户选择字节,则一般用于配置保护等功能。
闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。
对于主存储器和信息块的写入有内嵌的闪存编程管理;编程与擦除的高压由内部产生。
在执行闪存写操作时,任何对闪存的读操作都会锁定总线,在写完成后才能正确进行,在进行读取或擦除操作时,不能进行代码或者数据的读取操作。
下面对STM32的存储器进行总结。
图中淡蓝色就是你需要知道的。
Peripherals:外设的存储器映射,对该区域操作,就是对相应的外设进行操作;
SRAM:运行时临时存放代码的地方;
Flash:存放代码的地方;
System Memory:STM32出厂时自带的你只能使用,不能写或擦除;
Option Bytes:可以按照用户的需要进行配置(如配置看门狗为硬件实现还是软件实现);
今后,你的编写代码、程序运行、寄存器设置、ICP、IAP都依靠这些东西。
20.2.2 FLASH读写实现-标准库
Flash的寄存器有很多,ST的工程师已经封装好了,直接用就可以,我这里就不在贴出来了。
Flash操作很简单,我们将数据写入到Flash中,再将其读出来。主要有以下步骤:
1.Flash解锁操作
2.清除Flash标志
3.页擦除
4.读写操作
5.锁定
核心代码如下:
#define FLASH_ADR 0x0807F800
/**
* @brief flash test
* @param WriteAddr, InData
* @retval OutData
*/
uint32_t flash_test(uint32_t WriteAddr, uint32_t InData)
{
uint32_t OutData = 0;
//解锁
FLASH_Unlock();
//清除标志位
FLASH_ClearFlag(FLASH_FLAG_BSY|FLASH_FLAG_EOP| FLASH_FLAG_PGERR|FLASH_FLAG_WRPRTERR);
//要擦出页的起始地址
FLASH_ErasePage(WriteAddr);
//写数据
FLASH_ProgramWord(WriteAddr, InData);
//锁定
FLASH_Lock();
OutData=(*(__IO uint32_t*)(WriteAddr));
return OutData;
}
程序就不讲了,这里需要注意一个C语言的知识点。
OutData = (*(__IO uint32_t*)( WriteAddr));
这一句很多新手很懵逼,也就是从一个地址中读取数据,多看看指针相关的知识就好理解了。
主函数如下:
/**
* @brief main
* @param None
* @retval None
*/
int main(void)
{
uint32_t InData = 12345678;
uint32_t OutData;
/*SysTick Init*/
SysTick_Init();
/* USART1 config 115200 8-N-1 */
USART_Config();
printf("InData = %d\r\n",InData);
// flash test
OutData= flash_test(FLASH_ADR, InData);
printf("OutData = %d\r\n",OutData);
if(OutData == InData)
{
printf("Flash test success !\r\n");
}
else
{
printf("Flash test fail !\r\n");
}
while(1)
{}
}
20.2.3 FLASH读写实现-HAL库
我们在串口的例子的基础上写代码即可。
HAL库和标准库稍微有些不同,这里有个Flash擦除的结构体。
/**
* @brief FLASH Erase structure definition
*/
typedef struct
{
uint32_t TypeErase; /*!< TypeErase: Mass erase or page erase. This parameter can be a value of @ref FLASHEx_Type_Erase */
uint32_t Banks; /*!< Select banks to erase when Mass erase is enabled. This parameter must be a value of @ref FLASHEx_Banks */
uint32_t PageAddress; /*!< PageAdress: Initial FLASH page address to erase when mass erase is disabled This parameter must be a number between Min_Data = 0x08000000 and Max_Data = FLASH_BANKx_END (x = 1 or 2 depending on devices)*/
uint32_t NbPages; /*!< NbPages: Number of pagess to be erased. This parameter must be a value between Min_Data = 1 and Max_Data = (max number of pages - value of initial page)*/
} FLASH_EraseInitTypeDef;
该结构体定义了Flash擦写的类型,地址和页数信息等。
代码逻辑和标准库差不多,核心代码如下:
/**
* @brief flash test
* @param WriteAddr, InData
* @retval OutData
*/
uint32_t flash_test(FLASH_EraseInitTypeDef EraseInitStruct, uint32_t InData)
{
uint32_t PageError = 0;
uint32_t OutData = 0;
//解锁
HAL_FLASH_Unlock();
//清除标志位
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
//擦除页
if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK)
{
printf("Erase failed\r\n");
}
//写数据
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, EraseInitStruct.PageAddress, InData);
//锁定
HAL_FLASH_Lock();
OutData=(*(__IO uint32_t*)(EraseInitStruct.PageAddress));
return OutData;
}
主函数如下所示:
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
uint32_t InData = 12345678;
uint32_t OutData;
FLASH_EraseInitTypeDef EraseInitStruct = {
.TypeErase = FLASH_TYPEERASE_PAGES, //擦除类型:page擦除,即擦除整页。也可以选择擦除整片
.PageAddress = FLASH_ADR, //擦除起始地址
.NbPages = 1 //擦除页数
};
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("InData = %d\r\n",InData);
// flash test
OutData= flash_test(EraseInitStruct, InData);
printf("OutData = %d\r\n",OutData);
if(OutData == InData)
{
printf("Flash test success !\r\n");
}
else
{
printf("Flash test fail !\r\n");
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_Delay(1000);
}
/* USER CODE END 3 */
}
20.2.4实验结果
将程序边看一完成后下载到板子中,通过串口助手,按下板子的复位按键可以看到如下现象。
20.3 SRAM启动
20.3.1 Cortex-M的启动模式
首先要回顾一下Cortex-M的启动模式,因为启动模式决定了向量表的位置,Cortex-M有三种启动模式:
1)主闪存存储器(Main Flash)启动:从Cortex-M内置的Flash启动(0x0800 0000-0x0807 FFFF),一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。以0x08000000 对应的内存为例,则该块内存既可以通过0x00000000 操作也可以通过0x08000000 操作,且都是操作的同一块内存。
2)系统存储器(System Memory)启动:从系统存储器启动(0x1FFFF000 – 0x1FFF F7FF),这种模式启动的程序功能是由厂家设置的。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的ISP程序中,提供了串口下载程序的固件,可以通过这个ISP程序将用户程序下载到系统的Flash中。以0x1FFFFFF0对应的内存为例,则该块内存既可以通过0x00000000 操作也可以通过0x1FFFFFF0操作,且都是操作的同一块内存。
3)片上SRAM启动:从内置SRAM启动(0x2000 0000-0x3FFFFFFF),既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。SRAM 只能通过0x20000000进行操作,与上述两者不同。从SRAM 启动时,需要在应用程序初始化代码中重新设置向量表的位置。
用户可以通过设置BOOT0和BOOT1的引脚电平状态,来选择复位后的启动模式。如下表所示:
启动模式只决定程序烧录的位置,加载完程序之后会有一个重映射(映射到0x00000000地址位置);真正产生复位信号的时候,CPU还是从开始位置执行。
值得注意的是Cortex-M上电复位以后,代码区都是从0x00000000开始的,三种启动模式只是将各自存储空间的地址映射到0x00000000中。
20.3.2 Cortex-M的SRAM
不同类型的Cortex-M单片机的SRAM大小是不一样的,但是他们的起始地址都是0x2000 0000,终止地址都是0x2000 0000+其固定的容量大小。
SRAM的理解比较简单,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。设备断电后,SRAM中存储的数据就会丢失。
20.3.3片上SRAM启动实现
在使用片上SRAM调试之前,需要了解为何要使用片上SARM来启动程序,因此STM32的片上Flash的擦写次数有限,若超过最大擦除次数则会损坏内部Flash,因此在平时的程序调试阶段,最好使用SRAM启动。
总的来说,SRAM启动程序有如下用途:
1.调试阶段,需要频繁更新程序,可以SRAM启动,加快调试,减少flash擦写损耗
2.程序SWD/JTAG接口已经配置为普通端口,程序启动后无法程序更新,可在SRAM中启动后,再更新flash程序
3.程序已经开启了读保护,可在SRAM启动后,进行读保护关闭
片上SRAM启动实现的方法很简单,这里以STM32F103+Keil5举例,实现方法如下:
1.修改中断向量表基地址
打开SystemInit()的VECT_TAB_SRAM宏定义,也可在C/C++配置全局定义。
2.修改内存分配
修改了内存分配,也就是修改分散加载文件xxx.sct文件。
修改后再次生成工程,分散文件也会修改。
下面谈谈SRAM参数的分配,首先确定MCU的RAM大小,笔者使用的MCU是SM32F103ZE,因此SRAM大小是64KB,然后还需要根据程序的编译大小来分配SRAM空间。
如果想方便点可以直接看MAP文件。
FLASH和RAM的大小分别如下:
Flash = Code + RO Data + RW Data = 3.55KB(3636)
RAM = RW-data + ZI-data=1.18KB(1208)
因此笔者这里分配空间如下:
IROM1地址为0x2000 0000, 大小是0x3000=12288=12kB
IRAM1地址为0x2000 3000, 大小是0x2000=8192=8kB
当然啦,SRAM空间分配需要根据自己的 MCU来决定。
3.修改debug配置
新建RAM.ini文件,配置如下:
/*----------------------------------------------------------------------------
* Name: RAM.ini
* Purpose: RAM Debug Initialization File
* Note(s):
*----------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------
Setup() configure PC & SP for RAM Debug
*----------------------------------------------------------------------------*/
FUNC void Setup (void) {
SP = _RDWORD(0x20000000); // Setup Stack Pointer
PC = _RDWORD(0x20000004); // Setup Program Counter
_WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table Offset Register
}
FUNC void OnResetExec (void) { // executes upon software RESET
Setup(); // Setup for Running
}
load %L incremental
Setup(); // Setup for Running
g, main
然后加载进来。
RAM.ini文件主要是初始化SP和PC指针。Cortex-M3复位时会从0x00000000和0x00000004的相对位置取出MSP初值以及将复位向量地址赋给PC,在SRAM中的绝对位置就是0x20000000和0x20000004。STM32F1需要手动配置。
4.修改下载配置
需要把程序下载到SRAM,修改相应的下载地址。
5.修改中断向量表指针
由启动文件可知,程序启动首先执行Reset_Handler函数,SRAM启动硬件强制将PC指针赋值为0x200001E0,PC指针加1开始执行启动文件,故Reset_Handler函数的运行地址需为0x200001E1才行,否则程序就会跑飞。
因此需要在中断向量表的末尾添加SPACE 0x96,同时在Reset_Handler函数中,调用系统时钟配置前,添加如下语句:
LDR SP, =__initial_sp
将实际的栈顶指针地址赋值给SP,这样做的目的是避免默认的栈顶指针指向Code区。
当然还可以在main函数中添加系统初化函数。
SystemInit()
6.修改启动模式
这里选择SRAM启动,因此BOOT0->1 BOOT1->1。
最后编译程序,然后点击 debug即可调试程序了。
可以单步调试,当然退出调试,程序还能运行,只要不断电。
也可直接下载到板子中,当时不能断电。
欢迎访问我的网站
BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎
资源获取方式
1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码