基于MM32F5微控制器的FSMC接口外接SRAM存储器的用法

引言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()函数),不开放给用户修改,而其中使用的和计算出的内存地址,也都是在编译过程中预先定义的。
如果用户强行在链接命令文件中指定默认的主内存空间为外扩存储,那么在芯片启动过程中,预定义的初始化运行时环境的操作,将会在未初始化好fsmc接口等硬件的时候直接访问fsmc扩展出的内存空间,必然出错。可能会提示的错误是hardfault,标记为访问了无效的地址。此时,若是用户在集成开发环境的__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支持编译工具链的初始化过程,然后在应用程序中初始化fsmc外设(此时仍使用片内sram),然后再试图重建内存管理系统,将芯片系统中内存相关的指针人为重建在外扩sram中呢?且不说这是一个极其麻烦的过程,需要把编译工具链中的每个同内存相关的配置变量都翻出来重新人为计算并赋值一遍,一个明显的限制在于,所有将要放在外扩的大sram中的数据必须在较小的片内sram中必须预先存放一份副本,之后在应用程序运行的过程中转移到外扩sram。此时,在编译阶段,编译器会限定整个程序能使用的存储空间不能大于片内sram的大小,否则编译器仍然会报错并拒绝生成可执行文件。这就限制了能够直接使用外扩sram的空间,同最常用的将外扩sram当成辅助存储空间的做法没有实质区别。使用bootloader初始化硬件环境的思路为了让用户的工程直接在一个可用的外扩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工程顶级执行流程函数main()在实现最简单功能的bootloader工程中,缺省使用片内的sram作为主内存设备,仅仅需要完成的工作包括:
初始化外扩sram的接口fsmc在nvic_vtor寄存器中重定位中断向量表的基地址。后续application工程中的中断向量表将位于自己可执行二进制文件的最开始。application工程执行过程中,将通过nvic_vtor寄存器和中断向量表项的偏移值确定实际的中断服务程序入口地址。为application工程的栈指针寄存器sp(msp/psp)赋初值。这个初值即为application工程中的中断向量表的第一个表项中存放的数值,这个值是由application工程的链接器算出来的。最后跳转到application可执行程序的位置,后续执行application工程。
为了确定从芯片上电到执行application程序这段时间,bootloader确实按照预期正常工作,在本例实现的bootloader工程中使用了一个gpio控制的小灯指示执行过程中可能出现的错误:
初始化配置指示灯亮如果顺利执行到跳转到application的前一步,那么就可以熄灭指示灯,顺利进入跳转过程如果在bootloader执行的过程中遇到任何问题,例如可以增加一个验证外扩sram可以工作的检测过程,就在原地等待,此时指示灯将保持常亮本例创建的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内部时钟,尽量还原到芯片刚上电后进入用户程序的状态。但同外扩内存相关的外设资源(引脚、时钟等),则必须保持激活状态。
验证固件函数app_check_image()关于验证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_limit )) { return 1u; /* unavailable sram area. */ } /* validate the addr for pc. */ if ((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寄存器),这里也有一些考虑:
如果在application工程的启动程序中,有重置栈指针的操作,那么在bootloader工程中就没必要从application工程的文件中提取栈地址并重置栈顶指针了。但实际上大多数工程的启动程序中都没有这个步骤,而是依赖于微控制内核硬件的自动行为,从中断向量表的第一个表项中提取栈顶地址作为内核栈指针的初值。从完全模拟芯片启动行为的角度上看,在进入application之前,仍然需要给application工程一个位于主内存空间中的缺省栈指针,就像bootloader工程中上电后执行的第一条指令时,硬件就已经自动从中断向量表的第一个表项中提取了栈顶地址赋给栈指针。从bootloader跳转到application的过程中,微控制器不会自动将application中断向量表的第一个存放栈顶地址值,硬件自动为栈指针赋值的操作仅仅,只好由bootloader预先准备好。关于重置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工程联合调试。
约定分配bootloader和application工程的存储空间mm32f5微控制器内部存储空间分布,如表x所示。
bootloader工程和application工程的可执行文件都存放在片内flash存储器上。bootloader使用片内sram作为主sram,application使用外扩sram作为主sram,映射到fsmc bank3的0x68000000 - 0x68100000的1mb大小的空间上。
bootloader工程的链接命令文件bootloader工程和application工程的可执行文件都存放在片内flash存储器上。芯片上电后,缺省先执行bootloader工程,故bootloader工程的程序位于片内flash存储空间的首部,预留16kb。bootloader使用部分片内sram作为主sram,预留64kb,用户也可以根据实际需要调整。
在bootloader工程的链接命令文件中有关于存放程序文件地址空间的定义。见代码x。
代码 x bootloader工程的链接命令文件
/*--------------------- flash configuration ----------------------------------; flash configuration; flash base address ; flash size (in bytes) ; *----------------------------------------------------------------------------*/#define __rom_base 0x08000000#define __rom_size 0x00004000/*--------------------- embedded ram configuration ---------------------------; ram configuration; ram base address ; ram size (in bytes) ; *----------------------------------------------------------------------------*/#define __ram_base 0x30000000#define __ram_size 0x00010000application工程的链接命令文件application工程的可执行文件也保存在片内flash上,位于bootloader程序文件之后。若在应用中没有特别的需要,application工程可占用剩余的所有flash存储空间。application使用外扩sram存储作为主sram,映射到fsmc bank3的0x68000000 - 0x68100000的1mb大小的空间上。
在application工程的链接命令文件中有关于存放程序文件地址空间的定义。见代码x。
代码x application工程的链接命令文件
/*--------------------- flash configuration ----------------------------------; flash configuration; flash base address ; flash size (in bytes) ; *----------------------------------------------------------------------------*/#define __rom_base 0x08004000#define __rom_size 0x0003c000/*--------------------- embedded ram configuration ---------------------------; ram configuration; ram base address ; ram size (in bytes) ; *----------------------------------------------------------------------------*/#define __ram_base 0x68000000#define __ram_size 0x00100000在application工程中开发应用试验并查看分配在外扩sram的存储空间前文说到,用户在application工程中开发应用,不需要专门配置外扩sram相关硬件的操作,即可直接使用外扩sram存储作为主存储器。不过,在application工程中仍需要调整一下链接文件,将flash存储空间定义到片内flash存储器上除了bootloader已经在首部占用的其余空间,将ram空间定义到外扩存储器映射的存储空间中。这部分操作,已经在application工程的链接命令文件中配置好了,不需要用户在代码层面做任何特殊的设置。
在样例工程application中定义全局变量 uint8_t ch;见代码x。
代码x 在application工程中定义全局变量
#include board_init.huint8_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开始的外扩内存区间。
clock_init()虽说在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引导的工程中,芯片上电后,直接进入用户软件的管辖范围后,遇到预设的断点就停了下来。带有bootloader引导的application工程中,芯片上电后,先执行bootloader代码(没有预设断点),后来跳转到application工程后,遇到了application中预设的断点,才开始停下来。这个过程是集成开发环境工具自动执行的,用户在使用上不会有任何区别,只是可能会感受到启动调试后到可以再次启动“运行”操作中间等待的时间稍微长了一点。而这段等待的时间,正是bootloader在运行呢。
结论本文探讨了基于mm32f5微控制器的fsmc接口外接sram存储器的用法,试图寻找一种让编译器自动管理外扩内存的开发方法。使用bootloader工程引导application工程组合的方式,可以解决这个问题:在bootloader工程中初始化外扩sram的相关硬件,使得application工程可以在编译过程中就可以将外扩sram用起来。最终用户在application工程中开发自己的应用,可以直接使用外扩的大sram作为主内存,同时也可以将片内较小的sram作为辅助存储继续使用。

NVIDIA CloudXR将提供高质量的XR流媒体
电气人必知的82条电气专业术语详解
高精度运算放大器AiP07简介
电涡流传感器特性研究与应用实验
芯片设计到底难在哪里?
基于MM32F5微控制器的FSMC接口外接SRAM存储器的用法
景区负氧离子检测设备的功能特点
5G商转倒数 PCB业者迎新蓝海
2020年华为5G智能手机的出货量优势有望进一步扩大
重庆建设国家新一代人工智能创新发展试验区
电压、电流、电阻的计算公式
基于ISA总线技术实现多路DDS同步和IQ正交输出的信号源设计
视频编解码器是什么 编解码器技术原理作用解析
英特尔透露多款3D芯片创新架构与封装技术
2019年本田媒体大会开幕 计划2025年在中国投放20款以上电动化车型
Redmi Note10系列预热:屏幕尘埃落定
租用云服务器的好处
关于微型马达电机小知识的详细说明
OPPO如何开展并商用10倍无损变焦技术
稳压电源电路的工作原理