Introduction
Almost every embedded application out there, has at least a minimum storage of 1kB dedicated to store application specific variables into an external memory. Usually for small applications the memory being used is an EEPROM or a Flash. But what if we can't use an external memory for some reason? Can we use the internal flash memory in our micromillimeter. The answer is yes.
In this post I will cover step by step on how to read and write your application specific variables into system flash memory using HAL drivers.
Reference manual
The first thing we have to do is to look out for our MCU reference manual in order to read specifically about this peripheral operation, internal structure and limitations. We may have our HAL drivers, but after all, we must be aware of device capabilities.
In this post, I'm going to use ST's NUCLEO-L476RG board with STM32CubeIDE. This Nucleo-64 board comes with the STM32L476RG which has 1 MB Flash memory split into 2 banks which are split into pages of 2kB. You can find the controller's reference manual RM0351 here.
All the above details are described in section 3.2 FLASH main features
Now let's go to the next section 3.3 FLASH functional description.
In table 8, we can see the flash memory organization.
As we can see, the page size is fixed to 2kB. If you are using a different controller, that's okay, but the flash memory organization will probably be different. Not only it may have different page size, but it may have 2 or 3 different page sizes across flash memory space. So depending on the amount of memory we're going to use, we have to select an appropriate memory page that will fit our application data. The memory page that I choose to use is 511 with address 0x080FF800 which is located in bank 2. This page, should be unavailable for storing application code. Thus, the flash size for application code after selecting a single page of memory will be 1024kB - 2kB = 1022kB in total. This will be done in the linker script section which we will discuss further bellow.
Flash write and erase operations
It's known that embedded Flash memory can be programmed using 2 methods, in-circuit programming or in-application programming. Of course, what we're talking about here is in-application programming.
The in-application programming (IAP) can use any communication interface supported by the microcontroller (I/Os, USB, CAN, UART, I2C, SPI, etc.) to download programming data into memory. IAP allows the user to re-program the Flash memory while the application is running.
Please Note
While programming the microcontroller, we should be aware that any system reset will interrupt the programming operation and valid data cannot be guaranteed. An on-going Flash memory operation will not block the CPU as long as the CPU does not access the same Flash memory bank. Code or data fetches are possible on one bank while a write/erase operation is performed to the other bank (refer to Section 3.3.8: Read-whilewrite (RWW)). On the contrary, during a program/erase operation to the Flash memory, any attempt to read the same Flash memory bank will stall the bus. The read operation will proceed correctly once the program/erase operation has completed.
Erase operation
Programming the flash memory, requires that this memory page will be erased first. But in order to erase a page, we first need to unlock the flash. Each time the microcontroller starts, the flash memory is locked. HAL provides a ready made function to handle the behind the scene operation, which is a simple mechanism to verify that our application is trying to unlock the flash and not some unwanted situation, like an electric disturbance. The HAL function to unlock the flash is HAL_FLASH_Unlock() with no arguments and a return value of type HAL_StatusTypeDef.
There are 3 different erase operations available, mass erase, bank erase and page erase. We are going to use page erase in this post, since we need to erase a small section of the flash memory.
Of course HAL also provides a function for erase operations that handles all the procedure described in the reference manual. The HAL function to erase a flash is HAL_FLASHEx_Erase(&EraseInitStruct, &SECTORError) with arguments EraseInitStruct that contains details about the erase operation and SECTORError which is used in case an error has occurred that contains the address that the error occurred.
Write operation
Writing to flash is possible in two modes, Standard programming and Fast programming and only we double word data. In Standard mode, the Flash memory checks that the double‐word is erased before launching the programming. In Fast mode, 32 double‐words are programmed without verifying the Flash location. During fast programming, the CPU clock frequency (HCLK) must be at least 8 MHz. We're going to use Standard programming since we don't have any requirement for fast operation.
While flash has been unlocked in previous section and remains unlocked, we can proceed with programming. Any attempt to write a half world will raise the SIZERR flag in the FLASH_SR register. Also writing an unaligned double word data will raise PGAERR flag in the FLASH_SR register.
The function to write data into flash provided from HAL is HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data). Parameter Typeprogram refers to the flash writing mode and can be either FLASH_TYPEPROGRAM_DOUBLEWORD or FLASH_TYPEPROGRAM_FAST. Parameter address defines the address that the write will be done. Make sure the address is aligned to a word data. Argument Data is the data that will be written into the flash and it should be uint64_t.
Getting started
Let's begin with the practical part of this post. First, we need to create a new project and just hit generate code, nothing else! Once ready we must locate the file STM32L476RGTX_FLASH.ld which is the linker script. The linker script is a script that tells linker how to combine input files into a single output file.
The output file and each input file are in a special data format known as an object file format. Each file is called an object file. The output file is often called an executable. Each object file has, among other things, a list of sections. We sometimes refer to a section in an input file as an input section, similarly, a section in the output file is an output section.
Each section in an object file has a name and a size. Most sections also have an associated block of data, known as the section contents. A section may be marked as loadable, meaning that the contents should be loaded into memory when the output file is run. A section with no contents may be allocatable, which means that an area in memory should be set aside, but nothing in particular should be loaded there (in some cases this memory must be zeroed out). A section, which is neither loadable nor allocatable, typically contains some sort of debugging information.
Every loadable or allocatable output section has two addresses. The first is the VMA or virtual memory address. This is the address the section will have when the output file is run. The second is the LMA or load memory address. This is the address at which the section will be loaded. In most cases the two addresses will be the same. An example of when they might be different is when a data section is loaded into ROM, then copied into RAM when the program starts up (this technique is often used to initialize global variables in a ROM based system). In this case the ROM address would be the LMA and the RAM address would be the VMA.
You can read more details about the linker script the link bellow
An Introduction to the GNU
Compiler and Linker
Linker script modifications
First, we have to reduce the size of the flash area that the linker can use to store application code since we chose to use the last page of the flash memory. To do so, we need to locate the MEMORY command within the linker script. MEMORY directive describes the location and size of blocks of memory in the target.
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
RAM2 (xrw) : ORIGIN = 0x10000000, LENGTH = 32K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
Change LENGTH to 1022K, origin stays the same as we chosen the last page of the second bank. Next, we need to add a new entry named USER, with read only option and address 0x080FF800. The size is 2kB.
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
RAM2 (xrw) : ORIGIN = 0x10000000, LENGTH = 32K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1022K
USER (r) : ORIGIN = 0x080FF800, LENGTH = 2K
}
If we build our empty project, in build analyzer we can see that the new section is created and it's empty and the section flash is now 1022kB instead of 1024kB!
Now the last thing we need to do in our linker script, is to add a new section. Scroll down to the SECTIONS area between sections .data and .bss. Lets define our new section .user_data and tell the linker that this section will not be loaded with data by adding the keyword (NOLOAD) after the section name. To reference to this memory region on source code, we need to set 2 symbols that point to the start and the end of this section. The _user_data_start points exact at the beggining of this section and _user_data_end at the end of it.
.user_data (NOLOAD):
{
. = ALIGN(4);
KEEP(*(.user))
. = ALIGN(4);
} >USER
Source code
First we need to define a structure that will describe our application variables.
Let's name it appSettings and add few variables.
typedef struct {
uint16_t initialized;
uint16_t autolock;
uint32_t pass;
} appSettings;
For the shake of simplicity, we're going to use a mix of uint16_t and uint32_t variables. Feel free to modify as you wish but make sure the memory is aligned to 4 bytes.
To connect appSettings structure to the specific memory address in flash, we have to define where appSettings is. To do so, we define our symbol added in the linker script _user_data_start as type appSetings and keyword extern volatile, that tells the compiler this object is referenced in another file and it's type volatile which means will not be optimized.
__attribute__((section(".user"))) volatile appSettings Config;
Now using the variable name _user_data_start, we can read the data at the flash memory at address 0x080FF800. We can access each invidual member of this structure and each member will return the data based on the size of the variable. The uint16_t initialized will point to first 2 bytes after 0x080FF800 to 0x080FF801, uint32_t pass will point to 0x080FF803 to 0x080FF807 since it's 4 byte long.
Now lets fill our appSettings structure with data and write them into flash. But first we need to copy the whole section, or just the part of it which contains our appSettings data.
/* Configure the system clock */
SystemClock_Config();
uint8_t index = 0;
uint32_t Size = sizeof(appSettings)/8;
uint32_t Address = &_user_data;
uint32_t Address_end = Address + Size;
uint64_t ramConfig[Size];
/* Read settings into RAM */
while (Address < Address_end) {
ramConfig[index] = *(__IO uint64_t *)Address;
Address = Address + 8;
index++;
}
After we itterate inside the loop, we copy our data into ram into ramConfig array. At this stage, we don't expect any data in this section, so all variables will be 1s.
Lets create a new pointer variable named Config which will be a type of appSettings and we'll assign the address of previously created array ramConfig.
appSettings * Config = &ramConfig;
Now that Config points to ramConfig, our appSettings variables are mapped to ramConfig which lives into ram. Now we can use Config to modify our variables into ram prior flashing them back into flash.
Config->initialized = 0xA1B2;
Config->autolock = 0xC3D4;
Config->pass = 0xDEADBEEF;