控制/MCU
MM32F5微控制器基于Arm STAR-MC1微控制器,最高主频可达120MHz,集成了FPU单元和DSP扩展指令集,有不错的算力。但片内集成的128KB的RAM和256KB的FLASH,如果想支持代码量比较大的软件框架,就可能会力不从心,例如,TensorFlow Lite或者基于MicroPython的OpenMV这样的应用就需要更多的内存空间做缓存。但MM32F5微控制器带有FSMC接口和QSPI接口并支持基于QSPI的XIP(eXecute In Place,就地执行),可以分别外扩SRAM和FLASH存储器,这就为扩展存储资源提供了可能。在本文中,将介绍使用FSMC接口外接SRAM扩展内存的过程。在后续的文章中,在后续文章中,还会继续介绍使用QSPI对接qspiflash存储器实现外扩FLASH的过程。
MM32F5微控制器上集成了FSMC(Flexable Static Memory Controller)接口,可以外接并口的SRAM存储器。
在PLUS-F5270开发板上,对应外扩了一个1MB大小的PSRAM存储器作为扩展内存,如图x所示。
图x FSMC对接SRAM存储器
使用FSMC接口外扩的SRAM存储设备之前,必须先激活微控制器的FSMC接口,包括启用对FSMC接口外设的访问开关、配置FSMC接口对应的外部引脚,以及配置FSMC的时钟源和工作模式等操作。基于这样的使用前提,一般情况下使用外扩SRAM,都是在应用程序中激活FSMC硬件外设接口,之后通过在指定地址分配内存,或者访问绝对地址的方式访问新扩展出来的内存,但此时默认的主内存还是片内的SRAM。这种使用扩展SRAM的方式对于规模较小或者绑定具体应用的项目,因为涉及到对代码的改动以及对存储管理的工作量较小并且明确,在一定程度上是可以接受的。但对于移植已有现有的项目,或者是规模较大的框架性软件,开发者通常不愿意(也不建议)深入到代码库中去人为指定每个可能的全局变量的绝对地址,仅将管理的目标地址区间从片内SRAM转移到了外扩的SRAM而已,而希望能够一如既往地让编译器自动管理内存的分配机制。
编译器自动管理内存,就涉及到在芯片上电初始化过程中对编译器运行时环境的初始化过程中对堆栈进行初始化,配置栈顶和栈底、堆底和堆顶指针等,也包括将内存中BSS段的数据清零,将DATA段数据的初值从FLASH搬运到SRAM中等。这些操作的过程,大多被封装在集成开发环境自带的库中(例如Keil的__main
函数,经过一系列同编译器相关的准备工作后才跳转到用户的main()
函数),不开放给用户修改,而其中使用的和计算出的内存地址,也都是在编译过程中预先定义的。
__main
函数之前的SystemInit()
函数中先激活FSMC等外扩SRAM相关的硬件也是可行的,但必须要注意,这个过程中,除了CPU中仅有的寄存器外,不能使用任何栈内存,因为此时烧写在默认的中断向量表首位的栈顶地址所指向的空间还是不可访问的。具体来说,就需要用汇编命令完成对所有相关硬件外设的初始化操作,这确实是一个考验人耐心的事情。这里简单看一下SDK中的复位中断服务程序中的启动程序代码,见代码x。代码x SDK中的启动程序代码
Reset_Handler:
ldr r0, =__INITIAL_SP
msr psp, r0
ldr r0, =__STACK_LIMIT
msr msplim, r0
msr psplim, r0
#if defined (__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U)
ldr r0, =__STACK_SEAL
ldr r1, =0xFEF5EDA5U
strd r1,r1,[r0,#0]
#endif
bl SystemInit
bl __main
.fnend
.size Reset_Handler, . - Reset_Handler
为了让用户的工程直接在一个可用的外扩SRAM上建立存储管理系统,一个可行的设计,是使用额外使用一个bootloader工程(或者在芯片内部的电路实现上直接用一段ROM承载bootloader工程中的操作),在使用少量片内SRAM的条件下,通过常规的调用驱动API的方式(而不用汇编语句序列),先准备好使用FSMC的硬件环境,例如配置时钟系统、引脚复用功能、FSMC接口外设等等。在bootloader工程最后的部分,直接跳转到一个约定的、存放用户application工程的地址,开始自行application工程。在application是一个完全独立的工程,不用激活FSMC就可以直接使用外扩的SRAM,因此可以利用编译工具链直接在外扩的SRAM上重建存储管理系统。在application工程中,用户将完全不用干预内存的分配情况,就像之前一样完全交由编译器自行管理;由于不再使用bootloader工程,片内的SRAM可以作为独立的一块可用的存储空间(就像之前在片内SRAM看外扩SRAM一样),继续为应用程序提供存储服务。
进一步分析,探讨把bootloader工程放到ROM中的可能性。程序一旦写入ROM中,就不能有任何改变了。但在配置外扩SRAM的时候,仍需要人为指定外扩SRAM映射的地址范围(开始地址和空间大小),这个设定在不同应用场景中可能会不一样,受成本和功能的权衡,可能有时候会用或大或小的存储器设备,因具体选型不同配置参数也会随之发生变化,因此不适合直接固化在ROM中。除非是对应合封的芯片,需要固定规格的SRAM芯片的晶元已经同微控制器一起被封在芯片内部,倒是不失为一种高集成的SOC解决方案。或者也可以用类似回调的方式,由用户在某种基础的协议下向ROM中的小程序提供外扩SRAM芯片专属的配置,例如在手册里说明在特定的用户可编程的FLASH存储区中存放了关于SRAM的配置信息,也是可行的,但处理过程就多了几个步骤。没有扩展多种SRAM的情况下,实在没必要在芯片里设计这么一块ROM。不过,这种方式在外扩FLASH的时候确实用到了,在后续的文章中将会提及,外扩FLASH的各厂家设计生产的nor FLASH型号芯片在使用上存在差异,在flashless的芯片中必须在ROM中设计程序首先识别外扩spiflash芯片的型号,从而使用对应的配置信息初始化spiflash芯片,到时也将会有一番细致地阐述。
接下来,将详细介绍创建bootloader工程和用户在application工程中开发应用的实现过程和应用要点。
在实现最简单功能的bootloader工程中,缺省使用片内的SRAM作为主内存设备,仅仅需要完成的工作包括:
最后跳转到application可执行程序的位置,后续执行application工程。
为了确定从芯片上电到执行application程序这段时间,bootloader确实按照预期正常工作,在本例实现的bootloader工程中使用了一个GPIO控制的小灯指示执行过程中可能出现的错误:
本例创建的bootloader工程中的main()
函数,见代码x。
代码x bootloader工程中的main() 函数
/* define the memory range would be used in application firmware. */
#define BOARD_APP_EXEC_ROM_OFFSET (0x4000) /* 16KB. */
#define BOARD_APP_EXEC_ROM_BASE (0x08000000 + BOARD_APP_EXEC_ROM_OFFSET)
#define BOARD_APP_EXEC_ROM_LIMIT (0x08000000 + 0x80000)
#define BOARD_APP_EXEC_RAM_BASE (0x68000000) /* ext sram base address. */
#define BOARD_APP_EXEC_RAM_LIMIT (BOARD_APP_EXEC_RAM_BASE + 0x100000)
...
int main(void)
{
/* setup the boot clock, pins. */
BOARD_Init();
/* prepare a led to tell if everything is ok. */
app_led_init();
app_led_on();
/* setup the fsmc interface hardware for ext sram. */
app_init_sram();
/* check if the ext sram is ready. */
if (0u != app_check_image((void *)BOARD_APP_EXEC_ROM_BASE))
{
while (1); /* error: unavailable application firmware binary. */
}
/* restore as much as possible */
CLOCK_ResetToDefault();
/* turn off the led to tell every is finally ok. */
app_led_off();
/* jump to application. */
app_jump_to_image((void *)BOARD_APP_EXEC_ROM_BASE);
while (1); /* never run to this place. */
}
用户实际开发application工程时,是不应该感受到这个附加的bootloader工程的,因此,bootloader的执行时间应尽量短,执行完毕后应尽量复原至芯片上电复位的状态。在本例中,为了尽量加速bootloader工程的运行,在BOARD_Init()
函数中初始化启用的PLL,从芯片内部的8MHz时钟源倍频到120MHz,用最快速度执行完bootloader的语句后,在临跳转到application工程之前,又将系统时钟复原成原来上电缺省使用的8MHz内部时钟,尽量还原到芯片刚上电后进入用户程序的状态。但同外扩内存相关的外设资源(引脚、时钟等),则必须保持激活状态。
关于验证bootloader跳转的硬件环境,本例做得比较简单,仅在app_check_image()
函数中检查即将在application中使用的栈顶指针值和复位向量入口(中断向量表的前两个表项)。如果希望做得更严谨一些,可以再把即将使用的外扩SRAM存储空间都遍历一遍,写入数据之后再读出来查看是否一致,以确认即将使用的SRAM也是有效的。本例创建的app_check_image()
函数,见代码x。
代码x 实现app_check_image()函数
void app_check_image(void * addr)
{
uint32_t * vectorTable = (uint32_t *)addr;
/* validate the addr for sp. */
if ((vectorTable[0] < BOARD_APP_EXEC_RAM_BASE) || (vectorTable[0] > BOARD_APP_EXEC_RAM_LIMIT ))
{
return 1u; /* unavailable sram area. */
}
/* validate the addr for pc. */
if ((vectorTable[1] < BOARD_APP_EXEC_ROM_BASE) || (vectorTable[1] > BOARD_APP_EXEC_ROM_LIMIT ))
{
return 2u; /* unavailable sram area. */
}
return 0u;
}
跳转函数app_jump_to_image()
最关键的跳转函数app_jump_to_image()
,见代码x。
代码x 实现app_jump_to_image()函数
typedef void(*func_0_t)(void);
volatile uint32_t sp_base;
volatile uint32_t pc_base;
void app_jump_to_image(void * addr)
{
uint32_t * vectorTable = (uint32_t *)addr;
sp_base = vectorTable[0];
pc_base = vectorTable[1];
/* set new MSP and PSP.
* when the SP is changed, the address of variables in stack would be remapped according to the new SP.
*/
__set_MSP(sp_base);
__set_PSP(sp_base);
#if __VTOR_PRESENT == 1
SCB- >VTOR = (uint32_t)addr; /* the func's param is kept in R1 register, which would not be changed per the SP update. */
#endif
/* jump to application. */
((func_0_t)(pc_base))();
//pc_func();
/* the code should never reach here. */
while (1)
{}
}
在app_jump_to_image()
函数中,通过传入的即将跳转到可执行二进制代码区的首地址,提取位于可执行文件程序开始位置的中断向量表的前两个表项,分别为栈顶SP指针的初始值和PC指针的初始(复位中断服务程序入口地址),然后用各自不同的方法将它们赋值到硬件寄存器中生效:MSP和PSP寄存器可以直接使用汇编语句赋值,而PC指针不能由程序直接操作,但通过函数跳转命令实际可以载入新的PC值。
关于跳转之前是否重新配置SP指针(微控制器内核中的MSP和PSP寄存器),这里也有一些考虑:
关于重置SP指针的影响,这里也要特别说明。当在app_jump_to_image()
函数中执行__set_MSP(sp_base)
语句时,当前的栈指针就已经变了,此时,当前函数中使用的局部变量,还保存在原有的栈中,使用变化后的栈顶指针已经无法访问原有栈中的内容了。因此,之后再使用的sp_base
和pc_base
变量都被定义成全局变量,存放在外部内存(仍位于片内SRAM中),而不是栈中。至于addr
变量,是来自于函数传参,被存放在内核的Rn寄存器中,不受栈指针变化的影响。
通过把pc_base
赋值给PC寄存器,微控制器内核就转而执行新的PC指针指向的程序,从而完成了跳转到新程序的功能。
调试
改好代码之后,编译工程,就可以直接下载可执行文件到芯片中了。这个下载过程同正常下载工程没有任何区别,还是将可执行文件的二进制代码下载到片内FLASH存储器上。
此时,如果片内FLASH中还没有下载可用application文件到约定的地址上,有一定几率被app_check_image()函数检测为无效目标程序,直接卡在原地,并用指示灯常量警示用户。也可能碰巧通过了检测,bootloader工程最后跳转之后将会“跑飞”。
如果片内FLASH中已经下载了可用的application工程文件,例如后续重新调试bootloader添加新功能的开发过程,处理器内核执行了bootloader的跳转语句之后,就已经跳出了bootloader工程的控制范围,进入了application工程的执行序列,届时还需要配合application工程联合调试。
MM32F5微控制器内部存储空间分布,如表x所示。
bootloader工程和application工程的可执行文件都存放在片内FLASH存储器上。bootloader使用片内SRAM作为主SRAM,application使用外扩SRAM作为主SRAM,映射到FSMC Bank3的0x68000000 - 0x68100000的1MB大小的空间上。
bootloader工程和application工程的可执行文件都存放在片内FLASH存储器上。芯片上电后,缺省先执行bootloader工程,故bootloader工程的程序位于片内FLASH存储空间的首部,预留16KB。bootloader使用部分片内SRAM作为主SRAM,预留64KB,用户也可以根据实际需要调整。
在bootloader工程的链接命令文件中有关于存放程序文件地址空间的定义。见代码x。
代码 x bootloader工程的链接命令文件
/*--------------------- FLASH Configuration ----------------------------------
; < h > FLASH Configuration
; < o0 > FLASH Base Address < 0x0-0xFFFFFFFF:8 >
; < o1 > FLASH Size (in Bytes) < 0x0-0xFFFFFFFF:8 >
; < /h >
*----------------------------------------------------------------------------*/
#define __ROM_BASE 0x08000000
#define __ROM_SIZE 0x00004000
/*--------------------- Embedded RAM Configuration ---------------------------
; < h > RAM Configuration
; < o0 > RAM Base Address < 0x0-0xFFFFFFFF:8 >
; < o1 > RAM Size (in Bytes) < 0x0-0xFFFFFFFF:8 >
; < /h >
*----------------------------------------------------------------------------*/
#define __RAM_BASE 0x30000000
#define __RAM_SIZE 0x00010000
application工程的可执行文件也保存在片内FLASH上,位于bootloader程序文件之后。若在应用中没有特别的需要,application工程可占用剩余的所有FLASH存储空间。application使用外扩SRAM存储作为主SRAM,映射到FSMC Bank3的0x68000000 - 0x68100000的1MB大小的空间上。
在application工程的链接命令文件中有关于存放程序文件地址空间的定义。见代码x。
代码x application工程的链接命令文件
/*--------------------- FLASH Configuration ----------------------------------
; < h > FLASH Configuration
; < o0 > FLASH Base Address < 0x0-0xFFFFFFFF:8 >
; < o1 > FLASH Size (in Bytes) < 0x0-0xFFFFFFFF:8 >
; < /h >
*----------------------------------------------------------------------------*/
#define __ROM_BASE 0x08004000
#define __ROM_SIZE 0x0003C000
/*--------------------- Embedded RAM Configuration ---------------------------
; < h > RAM Configuration
; < o0 > RAM Base Address < 0x0-0xFFFFFFFF:8 >
; < o1 > RAM Size (in Bytes) < 0x0-0xFFFFFFFF:8 >
; < /h >
*----------------------------------------------------------------------------*/
#define __RAM_BASE 0x68000000
#define __RAM_SIZE 0x00100000
前文说到,用户在application工程中开发应用,不需要专门配置外扩SRAM相关硬件的操作,即可直接使用外扩SRAM存储作为主存储器。不过,在application工程中仍需要调整一下链接文件,将FLASH存储空间定义到片内FLASH存储器上除了bootloader已经在首部占用的其余空间,将RAM空间定义到外扩存储器映射的存储空间中。这部分操作,已经在application工程的链接命令文件中配置好了,不需要用户在代码层面做任何特殊的设置。
在样例工程application中定义全局变量 uint8_t ch;
见代码x。
代码x 在application工程中定义全局变量
#include "board_init.h"
uint8_t ch;
int main(void)
{
BOARD_Init();
printf("application.rn");
while (1)
{
ch = getchar();
putchar(ch);
}
}
编译项目后,可以查看其中的project.map
文件中,编译器自动为全局变量分配的内存位于外扩存储的内存空间中。见代码x。
代码x 编译application工程生成的project.map文件
Global Symbols
Symbol Name Value Ov Type Size Object(Section)
...
__stdin 0x68000000 Data 4 stdin.o(.data)
__stdout 0x68000004 Data 4 stdout.o(.data)
ch 0x68000008 Data 1 main.o(.bss.ch)
Image$$ARM_LIB_STACK$$ZI$$Base 0x680ff000 Number 0 anon$$obj.o ABSOLUTE
Image$$ARM_LIB_STACK$$ZI$$Limit 0x68100000 Number 0 anon$$obj.o ABSOLUTE
...
Execution Region RW_RAM (Exec base: 0x68000000, Load base: 0x08005724, Size: 0x0000000c, Max: 0x000fe000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x68000000 0x08005724 0x00000004 Data RW 310 .data mc_w.l(stdin.o)
0x68000004 0x08005728 0x00000004 Data RW 311 .data mc_w.l(stdout.o)
0x68000008 - 0x00000001 Zero RW 20 .bss.ch main.o
Execution Region ARM_LIB_HEAP (Exec base: 0x68000010, Load base: 0x0800572c, Size: 0x00001000, Max: 0x00001000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x68000010 - 0x00001000 Zero RW 1 ARM_LIB_HEAP.bss anon$$obj.o
Execution Region ARM_LIB_STACK (Exec base: 0x680ff000, Load base: 0x0800572c, Size: 0x00001000, Max: 0x00001000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x680ff000 - 0x00001000 Zero RW 2 ARM_LIB_STACK.bss anon$$obj.o
从代码x中也可以看到,application工程的运行时全局变量数据区(RW_RAW)、堆空间(ARM_LIB_HEAP)、栈空间(ARM_LIB_STACK)也都位于0x6800_0000开始的外扩内存区间。
虽说在application工程中不需要用户为使用外扩SRAM做任何特殊的设置,但由于使用了SDK代码包,还是有一点编程要点要注意。SDK的编程规范里,有要求在芯片上电启动过程中,用户在使用硬件外设之前,要使用硬件复位操作复位将要使用外设模块,以确保每次进入main()函数时,硬件外设的状态都是从确定的初始状态开始工作的。但在本例中,是通过bootloader引导进入的application,有一些已经激活的外设必须保持工作状态,例如FSMC以及对应使用的GPIO等外设模块,是不能在application工程中复位硬件的,否则,之前bootloader的准备工作就白费了,整个工程也不能正常工作。这里务必要再次确认clock_init.c文件中BOARD_InitBootClocks()函数中,关闭对FSMC和GPIO外设的复位操作,或者不要额外操作亦可。见代码x。
代码x application工程的BOARD_InitBootClocks()函数
void BOARD_InitBootClocks(void)
{
CLOCK_ResetToDefault();
CLOCK_BootToHSE120MHz();
/* UART1. */
RCC_EnableAPB2Periphs(RCC_APB2_PERIPH_UART1, true);
RCC_ResetAPB2Periphs(RCC_APB2_PERIPH_UART1);
/* GPIOA. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOA);
/* GPIOB. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOB, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOB);
/* GPIOC. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOC, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOC);
/* GPIOD. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOD, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOD);
/* GPIOE. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOE, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOE);
/* GPIOF. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOF, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOF);
/* GPIOG. */
//RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOG, true);
//RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOG);
}
改好代码之后,编译工程,就可以直接下载可执行文件到芯片中了。这个下载过程同正常下载工程没有任何区别,还是将可执行文件的二进制代码下载到片内FLASH存储器上。由于在设计链接命令文件时已经做好约定,application工程使用的片内FLASH存储区同bootloader工程是错开的,所以下载application工程的可执行文件到片内FLASH中不会冲掉之前下载的bootloader工程,但切记不要做全片擦除。
下载application工程后,可正常使用单步调试。这里可以理解一个要点:当集成开发环境中启动调试模式时,会在本工程的main()函数或者复位服务程序的第一句下一个断点,当用户启动“运行”操作后,才开始执行后续的程序。
这个过程是集成开发环境工具自动执行的,用户在使用上不会有任何区别,只是可能会感受到启动调试后到可以再次启动“运行”操作中间等待的时间稍微长了一点。而这段等待的时间,正是bootloader在运行呢。
本文探讨了基于MM32F5微控制器的FSMC接口外接SRAM存储器的用法,试图寻找一种让编译器自动管理外扩内存的开发方法。使用bootloader工程引导application工程组合的方式,可以解决这个问题:在bootloader工程中初始化外扩SRAM的相关硬件,使得application工程可以在编译过程中就可以将外扩SRAM用起来。最终用户在application工程中开发自己的应用,可以直接使用外扩的大SRAM作为主内存,同时也可以将片内较小的SRAM作为辅助存储继续使用。
全部0条评论
快来发表一下你的评论吧 !