How to add your SPI flash memory into the STM32CubeProgrammer’s external loader?
1. Introduction:
Application that requires external memories to be manufactured with initial content can be very challenge to debug and become a heavy toll during the production cycle as usually one would need to use two programming process using different toolset, one for the external memory and another for program the STM32 internal flash memory. The STM32CubeProgrammer’s External Loader is a feature that allows a direct access to external memories by STM32CubeProgrammer and even by the STM32CubeIDE to read, program, and erase data without the use of any additional tool other than a regular STLINK and even without ever changing the internal flash memory of the STM32. It is a feasible way to accelerate the debug cycle and the manufacturing program process on applications that uses external memory.
This article main objective is to provide a step-by-step guide on how to develop your own External Loader to manage external memories. This demo implementation uses a NUCLEO-G070RB and simple SPI-Flash memory, but it can be easily tailored for another microcontrollers and memory types.
2. Pre-requisite:
To develop this article’s demo, the following materials were necessary:
- Hardware:
- Micro USB cable: to power and program the board
- NUCLEO-G070RB
- External SPI memory (W25Q64)
- Software:
- STM32CubeIDE
- STM32CubeProgrammer
3. Theory:
To directly access external memories without having to program the STM32 internal flash memory to host the driver and basic functions, the External Loader replies on loading all these needed functions in the STM32’s RAM. Then the STM32CubeProgrammer can freely access the RAM memory and thus calling these functions using the SWD communication, granting the STM32CubeProgrammer the capability to perform read, program, and erase processes.
The STM32CubeProgrammer already comes with all external loaders created for all our evaluation boards and respective memories, but you can add more memories according to your application. We need to create a project that executes all the management functions from the STM32’s RAM and this will require a custom linker file, a specific header, and function set in order to build a *.stldr file, which is the method used by the STM32CubeProg External Loader to map all the memory features. This *.stldr can be created using a compiler post-build and then manually added in the STM32CubeProgrammer and STM32CubeIDE program folder.
4. Development:
First, we need to create the External Loader with the drivers to manage our external memory, and setup that to run in RAM. So, let us create a project in STM32CubeIDE for the STM32G070RBT6 (NUCLEO-G070RB’s Microcontroller).
Configure the SPI according to the memory specifications, in this example a SPI flash memory is used:
The main code needs the peripheral initialization functions and later the driver for the memory, also, having the peripherals with individual source and header files make the project cleaner and easier to follow. To do this, go to Project Manager -> Code Generator and check the box “Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”.
Then Generate the code clicking on “Device Configuration Tool Code Generation” or going to Project -> Generate Code.
Since we need that all the functions run from the RAM, the following changes are needed in the linker file - STM32G070RBTX_FLASH.ld:
- Change the entry point of the code from the Reset_Handler to start in the Init function, which will be created later.
/* Entry Point */
ENTRY(Init)
- Add the following line after the Entry Point to generate 2 segments in binary file, the first for Loader code (that will receive the Memory functions), and second for the device info (that will store some information used by Cube Programmer and STLINK).
/* Generate 2 segment for Loader code and device info */
PHDRS {Loader PT_LOAD ; SgInfo PT_LOAD ; }
- As the functions will run from the RAM, remove the FLASH area in the Memory Areas code and change the RAM area name to RAM_D1. Change the start address of the RAM to 0x20000004, to prevent a conflict with the Reset Handler that will be in the first memory position (this change is necessary when the entry point is different than Reset Handler) and as we’ll later change the NVIC to be placed in RAM as well
/* Memories definition */
MEMORY
{
RAM_D1 (xrw) : ORIGIN = 0x20000004, LENGTH = 36K-4
}
- Change the position of the interrupt vector to be allocated on RAM_D1 and make the memory offset .
.isr_vector :
{
. = . + 0x1FC; /* ISR vector offset */
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >RAM_D1 :Loader
- Given that the RAM area’s name was changed, we need to change this on the stack address settings
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM_D1) + LENGTH(RAM_D1); /* end of "RAM" Ram type memory */
- A change in the *.text section was necessary to prevent an optimization where the compiler tends to remove the unused function. We need to maintain all the functions in the RAM because it will be called from the STM32CubeProgrammer. For that, add the “KEEP(*Loader_Src.o ( .text* ))” line code.
/* The program code and other data into RAM type memory */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue Arm to thumb code */
*(.glue_7t) /* glue thumb to Arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
KEEP (*Loader_Src.o ( .text* ))
. = ALIGN(4);
_etext = .; /* define a global symbol at end of code */
} >RAM_D1 : Loader
- Change .ARM.extab.
.ARM.extab : {
*(.ARM.extab* .gnu.linkonce.armextab.*)
} >RAM_D1
- Remove the RAM function sections from the .data section.
/* Initialized data sections into "RAM" Ram type memory */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM_D1 :Loader
- Force all the sections to be placed in the Loader segment of the RAM by changing the “>FLASH” to “>RAM_D1 : Loader”.
- Create the .Dev_Info section, note that this section is placed into SgInfo region recreated in step #2.
.Dev_Info :
{
__Dev_info_START = .;
*(.Dev_info*)
KEEP(*(.Dev_info))
__Dev_info_END = .;
} >RAM_D1 :SgInfo
- Remove all the “. = ALIGN(4)” commands of .ARM, .preinit_array, .init_array, .fini_array.
- Cut the .text, .Dev_Info and .rodata sections and paste after de .bss section. An example linker code is presented in the final part of this article series.
Once all linker edit is completed it is time to import your custom external memory driver for the project. If you do not have one yet, you can use our knowledge base to learn how to create one.
The
part 2 will show how to create the necessary files of the External Loader and the relationship between the STM32CubeProgrammer and the created project
See you soon!