一、简介
在RAM中调试代码是一种常见的嵌入式开发技术,尤其适用于STM32等微控制器。它的核心思想是将程序代码和数据加载到微控制器的内部RAM(SRAM)中运行,而不是运行在Flash存储器中。这种方法在开发过程中具有显著的优势,但也有一些限制。
1.1 为什么要在RAM中调试代码?
1、保护Flash存储器:
Flash存储器的寿命有限:Flash存储器的擦写次数是有限的(例如,STM32的Flash通常支持10,000次擦写)。频繁的调试和代码烧录可能会加速Flash的磨损。
减少擦写次数:将代码运行在RAM中可以避免对Flash的频繁擦写,从而延长Flash的使用寿命。
2、提高调试效率:
快速修改和测试:在RAM中运行代码时,可以快速修改代码并重新加载,而无需擦除和重新烧录Flash。这大大加快了调试速度。
动态调试:RAM中的代码可以动态修改,适合进行复杂的调试和测试,例如实时修改变量或函数逻辑。
3、支持高级调试功能:
断点和单步调试:在RAM中运行代码时,调试器可以更灵活地设置断点和进行单步调试,而不会受到Flash存储器的限制。
动态内存分配:某些调试功能(如动态内存分配和堆栈跟踪)在RAM中更容易实现。
1.2 在RAM中调试代码的优势
1、在RAM上调试程序时,下载速度非常快:
与内部FLASH相比,RAM存储器的写入速度要快得多,且无需擦除过程。因此,程序几乎是秒下,这为需要频繁修改代码的调试过程节省了大量时间,省去了烦人的擦除与写入FLASH的步骤。此外,虽然STM32的内部FLASH可擦除次数通常为1万次,一般的调试过程不太可能达到这个次数导致FLASH失效,但这也确实是一个考虑使用RAM的因素。
2、在RAM上调试程序时,不会改写内部FLASH的原有程序。
3、对于内部FLASH被锁定的芯片,还可以将解锁程序下载到RAM上,进行解锁操作。
1.3 在RAM中调试代码的缺陷
1、存储在RAM中的程序在掉电后会丢失,无法像存储在FLASH中那样持久保存。
2、如果使用STM32的内部SRAM存储程序,程序的执行速度与在FLASH上执行时基本相同,但内部SRAM的空间相对较小,可能会限制程序的大小。
3、如果使用外部扩展的SRAM存储程序,虽然程序空间可以非常大,但STM32读取外部SRAM的速度比读取内部FLASH慢,这会导致程序的总执行时间增加。因此,在外部SRAM中调试的程序无法完全模拟在内部FLASH中运行时的真实环境。
- 此外,STM32无法直接从外部SRAM启动,且将应用程序复制到外部SRAM的过程较为复杂(在下载程序之前,需要确保STM32能够正常控制外部SRAM)。因此,实际开发中很少会在STM32的外部SRAM中调试程序。
二、STM32的启动方式
CM-3 内核在离开复位状态后的工作过程如下图:
1、从地址 0x00000000 处取出栈指针 MSP 的初始值,该值就是栈顶的地址。
- 程序局部变量存储在栈空间中,MSP指向栈顶,防止栈溢出。
2、从地址 0x00000004 处取出程序指针 PC 的初始值,该值指向复位后应执行的第一条指令。
上述过程由内核自动设置运行环境并执行主体程序,因此它被称为自举过程。
2.1 MSP指针
MSP(Main Stack Pointer,主堆栈指针)是用于管理堆栈的一个重要寄存器,系统复位后,MSP的值会被设置为向量表的第一个值(通常是堆栈的初始地址),用于初始化堆栈。
Stack_Size EQU 0x00000400 // 定义了堆栈的大小为 0x00000400 字节(1024 字节)
AREA STACK, NOINIT, READWRITE, ALIGN=3 // 定义了一个名为 STACK 的内存区域,该区域未初始化(NOINIT),可读写(READWRITE),并且对齐到 2^3(即 8 字节)边界。
Stack_Mem SPACE Stack_Size // 在堆栈区域中分配了 Stack_Size 大小的空间。
__initial_sp // 表示堆栈指针的初始值将被设置为 Stack_Mem 的地址加上 Stack_Size 的大小,即堆栈空间的顶部。
2.2 PC指针
PC指针(程序计数器)是一个寄存器,用于存储下一条指令的地址。
1、系统复位后,首先调用 SystemInit 函数来初始化硬件;
2、然后跳转到 __main 函数,由它完成C运行时环境的初始化(如全局变量的初始化);
3、最终,程序会跳转到 main() 函数(由 __main 调用),开始执行用户程序。
PC指向Reset_Handler,跳转到Reset_Handler函数执行初始化时钟、调用main函数,最终进入main函数中执行程序。
2.3 STM32的三种启动方式
虽然内核默认访问的地址是 0x00000000 和 0x00000004,但这些地址实际上可以被重映射到其他地址空间。以 STM32F103 为例,根据芯片引脚 BOOT0 和 BOOT1 的电平状态,这两个地址可以被映射到内部 FLASH、内部 SRAM 或系统存储器。具体的映射配置取决于 BOOT 引脚的不同设置,具体映射关系见表 BOOT 引脚设置对 0 地址的映射。
当内核离开复位状态后,会从映射的地址中获取初始值,分别赋给主堆栈指针(MSP)和程序计数器(PC),然后开始执行指令。通常,我们会根据这些地址所映射到的存储器类型(如内部FLASH、SRAM或系统存储器)来区分不同的自举过程。
2.3.1 内部 FLASH 启动方式
当芯片上电后,若采样到 BOOT0 引脚为低电平,则 0x00000000 和 0x00000004 地址会被映射到内部 FLASH 的首地址 0x08000000 和 0x08000004。因此,内核在离开复位状态后,会从内部 FLASH 的 0x08000000 地址读取内容并赋值给主堆栈指针(MSP),作为栈顶地址;再从内部 FLASH 的 0x08000004 地址读取内容并赋值给程序计数器(PC),作为第一条指令的地址。具备这两个条件后,内核便开始从 PC 指向的地址中读取并执行指令。
2.3.2 内部 SRAM 启动方式
当芯片上电后,若采样到 BOOT0 和 BOOT1 引脚均为高电平,则 0x00000000 和 0x00000004 地址会被映射到内部 SRAM 的首地址 0x20000000 和 0x20000004。此时,内核会从 SRAM 空间获取内容以完成自举过程。
在实际应用中,0x00000000 和 0x00000004 地址存储的内容由启动文件(如 startup_stm32f10x.s)定义。而在链接阶段,分散加载文件(.sct 文件)会决定这些内容的最终存储位置,即它们会被分配到内部 FLASH 还是内部 SRAM。
2.3.3 内部 SRAM 启动方式
当芯片上电后,若采样到 BOOT0 引脚为高电平且 BOOT1 引脚为低电平时,内核将从系统存储器的 0x1FFFF000 和 0x1FFFF004 地址获取主堆栈指针(MSP)和程序计数器(PC)的值进行自举。
系统存储器是一段特殊的存储空间,用户无法直接访问。ST公司在芯片出厂前在系统存储器中固化了一段代码。当使用系统存储器启动时,内核会执行这段代码,该代码运行时会为 ISP(In System Program,系统内编程) 提供支持。具体来说,它会检测通过 USART1/2、CAN2 或 USB 通讯接口传输过来的信息,并根据这些信息更新内部 FLASH 的内容,从而实现产品应用程序的升级。因此,这种启动方式也被称为 ISP 启动方式。
三、内部FLASH的启动过程
在启动代码的中间部分,通过汇编指令 DCD(Define Constant Data),将 __initial_sp 和 Reset_Handler 的地址定义在了代码段的最前面,从而确保它们被放置在指定的地址空间。
在启动文件中,将栈顶地址和首条指令地址(__initial_sp 和 Reset_Handler)放置在代码的最前面,但这并不直接指定它们的绝对地址。这些内容的绝对地址是由链接器根据分散加载文件**(*.sct)**分配的。
上图为 STM32F103 的默认分散加载文件配置。
LR_IROM1:表示一个加载区域(Load Region),名称为 LR_IROM1。
- 0x08000000:加载区域的起始地址(这里是内部FLASH的起始地址);0x00080000:加载区域的大小(这里是512KB)。
ER_IROM1(内部FLASH):表示一个执行区域(Execution Region),名称为 ER_IROM1。
- 0x08000000:执行区域的起始地址(与加载地址相同,表示代码加载后直接在该地址执行);0x00080000:执行区域的大小(这里是 512KB)。
代码段分配:
*.o (RESET, +First) // 将所有对象文件中定义的 RESET 段(通常是中断向量表和复位处理程序)放在最前面。 *(InRoot$$Sections) // 将所有对象文件中定义的 InRoot 段(通常是启动代码和初始化代码)放在后面。 .ANY (+RO) // 将所有只读(Read-Only)段分配到该区域。 .ANY (+XO) // 将所有可执行但不读取的段分配到该区域。
如果把*.o (RESET, +First)放到RW_IRAM1中,那么MSP指针跟PC指针就会指向0x20000000跟0x20000004的地址。
RW_IRAM1(RAM空间):表示一个读写区域(Read-Write Region),名称为 RW_IRAM1。
0x20000000:读写区域的起始地址(这里是内部 SRAM 的起始地址);0x00010000:读写区域的大小(这里是 64KB)。
.ANY (+RW +ZI):将所有读写(Read-Write)和零初始化(Zero-Initialized)的段分配到该区域。
在分散加载文件中,加载区和执行区的首地址都被设置为 0x08000000,这恰好是内部 FLASH 的起始地址。因此,汇编文件中定义的栈顶地址和首条指令地址会被存储到 0x08000000 和 0x08000004 的地址空间中。
类似地,如果修改分散加载文件,将加载区和执行区的首地址设置为内部 SRAM 的起始地址 0x20000000,那么栈顶地址和首条指令地址将会被存储到 0x20000000 和 0x20000004 的地址空间中。
四、将代码修改为RAM自举
- 1、设置一个RAM调试的工程:
- 2、在C/C++中添加宏VECT_TAB_SRAM:
此宏在SystemInit函数中。
如果定义了 VECT_TAB_SRAM,则将中断向量表重定向到内部 SRAM 的指定位置;
如果未定义 VECT_TAB_SRAM,则将中断向量表重定向到内部 FLASH 的指定位置。
注意,两个宏之间用","分开。
3、打开.sct文件:
4、修改.sct文件,把程序分配到SRAM:
修改前的空间大小。
修改后的空间大小:原本的RAM空间为64K,现分一半用来做FLASH,一半用来做RAM
5、修改下载配置,把程序下载到SRAM:
选择Do not Erase是因为程序下载到RAM中,不需要修改FLASH,勾选Erase Full Chip或Erase Sectors的话会修改FLASH,导致程序下载失败。
RAM for Algorithm:烧录算法(Flash Programming Algorithm)预留的RAM空间。
烧录过程中的临时存储:在将程序烧录到Flash时,烧录算法需要运行,而这个算法需要一定的RAM空间来存储其运行时的数据和代码。
仅在烧录时使用:一旦烧录完成,这段RAM空间会被释放,可供应用程序(APP代码)使用。
6、修改调试器配置,初始化SP和PC指针:
/******************************************************************************/
/* Debug_RAM.ini: Initialization File for Debugging from Internal RAM */
/******************************************************************************/
/* This file is part of the uVision/ARM development tools. */
/* Copyright (c) 2005-2014 Keil Software. All rights reserved. */
/* This software may only be used under the terms of a valid, current, */
/* end user licence from KEIL for a compatible version of KEIL software */
/* development tools. Nothing else gives you the right to use this software. */
/******************************************************************************/
FUNC void Setup (void) {
SP = _RDWORD(0x20000000); // 设置栈指针SP,把0x20000000地址中的内容赋值到SP。
PC = _RDWORD(0x20000004); // 设置程序指针PC,把0x20000000地址中的内容赋值到PC。
_WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table Offset Register
}
LOAD %L INCREMENTAL // 下载axf文件到RAM
Setup(); //调用上面定义的setup函数设置运行环境
//g, main //跳转到main函数,本示例调试时不需要从main函数执行,注释掉了,程序从启动代码开始执行
这里是强制将SP、PC指针强制指向了0x20000000与0x20000000。
- 7、将工程更改为RAM调试的工程:
调试的时候不能点DOWNLOAD,要点DEBUG。
在DEBUG的时候,想要复位,不能点RST按键,要退出DEBUG后再重新点DEBUG。