Method for preserving non-initialized variables after STM32
reset#
Some products may require preserving the data in the RAM before a system reset (not a power-on reset) in order to quickly restore the state or prevent the restart of the device due to an instant reset. By default, Keil MDK
clears the data of non-initialized variables in the RAM during any form of reset. This article discusses how to prevent non-initialized data variables from being zero-initialized.
In my case, I need to modify the variable values and store them in flash to prevent data loss during power-off. However, there is a problem: after pressing the reset button, this part of the content will be overwritten with the initial values. Therefore, this content needs to be non-initialized and not cleared during power-off or reset.
Before providing the methods, let's understand the rules and attributes for storing code and data, as well as why non-initialized variables in RAM are initialized to zero by default after a reset.
What are initialized data variables and non-initialized data variables?
If we define a variable as int nTimerCount = 20;
, the variable nTimerCount
is an initialized variable with an initial value.
If we define a variable as int nTimerCount;
, the variable nTimerCount
is a non-initialized variable, and by default, Keil MDK
places it in the input section with the attribute ZI
.
So, what is "ZI" and what is an "input section"? To understand this, we need to know the composition of an ARM image file. This part may seem boring, but I believe it is essential to understand.
Composition of an ARM image file:
An image file consists of one or more regions.
Each region contains one or more output sections.
Each output section contains one or more input sections.
Each input section contains code and data from the target file.
Input sections contain four types of content: code, initialized data, uninitialized storage areas, and storage areas initialized to zero. Each input section has corresponding attributes: read-only (RO
), read-write (RW
), and initialized to zero (ZI
).
An output section contains a series of input sections with the same RO
, RW
, and ZI
attributes. The output section attributes are the same as the attributes of the included input sections.
A region contains one to three output sections, each with different attributes: RO
, RW
, and ZI
.
From this, we can understand that in general, code is placed in input sections with the RO
attribute, initialized variables are allocated to input sections with the RW
attribute, and the ZI
attribute input section can be understood as a collection of variables initialized to zero.
Where are the initial values of initialized variables stored in the hardware? For example, if we define int nTimerCount = 20;
, where is the initial value 20 stored? This is an interesting question. After the compilation is completed, Keil
provides information about the size of the compiled file, as shown below:
Total RO Size (Code + RO Data) 54520 ( 53.24kB)
Total RW Size (RW Data + ZI Data) 6088 ( 5.95kB)
Total ROM Size (Code + RO Data + RW Data) 54696 ( 53.41kB)
Many people do not know how this is calculated or how much code is actually stored in ROM/Flash. In fact, the initial values of those initialized variables are stored in the RW
attribute input section, and these initial values are stored in ROM/Flash. Sometimes, these initial values can be large, so Keil
compresses them before storing them in ROM/Flash to save storage space. Who and when are these initial values restored to RAM? Who initializes the variables in the ZI
attribute input section to zero? To understand these things, we need to see what Keil
does for you from system reset to executing the main
function in your C code.
After a hardware reset, the first step is to execute the reset handler, whose entry point is in the startup code (by default). Here is an excerpt of the reset handler entry code for cortex-m3
:
Reset_Handler PROC ;PROC is equivalent to FUNCTION, indicating the beginning of a function, relative to ENDP
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
After initializing the stack pointer and executing the user-defined low-level initialization code (SystemInit
function), the code calls the __main
function. In this function, a series of C library functions are called to copy and decompress code and data, as well as zero-initialize the ZI
data. This includes copying the initial values of initialized variables stored in ROM/Flash to the corresponding RAM. For a variable, it can have three attributes. Variables with the const
modifier are likely to be placed in the RO
attribute area, initialized variables are placed in the RW
attribute area, and the remaining variables are placed in the ZI
attribute area. By default, the zero initialization of ZI
data initializes all ZI
data areas to zero. This is done by the compiler before the main function in the C code is executed after each reset. Therefore, to prevent the zero initialization of certain variables after a reset, we need to impose some rules on the compiler.
The scatter-loading file is crucial for the linker. In the scatter-loading file, using UNINIT
to modify an execution section can prevent __main
from zero-initializing the ZI
data in that section. This is the key to solving the non-zero initialization variable issue. Therefore, we can define an execution section named MYRAM
with the starting address of 0x2000C000
and a length of 0x2000
bytes (8KB
), and decorate it with UNINIT
:
- Modify the scatter-loading file by adding an execution section named
MYRAM
with the starting address of0x2000C000
and a length of0x2000
bytes (8KB
), decorated withUNINIT
:
Where is the scatter-loading file located? The scatter-loading file is located in theObjects
folder in the project directory and has a.sct
extension. This file is the scatter-loading file, wheresct
stands for scatter. The.sct
file is the scatter-loading file that allows you to define different positions for code and data, where to find the next function to be executed, etc. Let's open it and take a look:
We add the execution section named MYRAM
before the }
at the end of the document, as shown below:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00040000 { ; load region size_region
ER_IROM1 0x08000000 0x00040000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x0000C000 { ; RW data
.ANY (+RW +ZI)
}
MYRAM 0x2000C000 UNINIT 0x00002000{
.ANY(NO_INIT)
}
}
So, if we have an array in the program that we don't want to be zero-initialized after a reset, we can define the variable as follows:
uint8_t PressureHi[16] __attribute__((at(0x2000C000)));
The variable attribute modifier __attribute__((at(adder)))
is used to force the variable to be located at the address specified by adder
. Since the region starting from address 0x2000C000
is not zero-initialized, the array PressureHi
located in this region will not be zero-initialized.
The disadvantage of this method is obvious: we have to allocate the addresses for the variables ourselves. If there are many non-zero-initialized data, it will be a difficult task to imagine (in terms of future maintenance, adding, modifying code, etc.). Therefore, we need to find a way to let the compiler automatically allocate variables in this region.
- In the scatter-loading file, follow the same method as in method 1. If we want to define an array, we can use the following method:
uint8_t PressureHi[16] __attribute__((section("NO_INIT"), zero_init));
The variable attribute modifier __attribute__((section("name"), zero_init))
is used to force the variable to be defined in the data section with the specified name
, and zero_init
indicates that uninitialized variables are placed in the ZI
data section. Since "NO_INIT" is an explicitly named custom section with the UNINIT
attribute, this is strongly recommended as the simplest method.
- How to make all non-initialized variables within a module non-zero-initialized?
If the module is namedtest.c
, modify the scatter-loading file as shown below:
LR_IROM1 0x00000000 0x00080000 { ; load region size_region
ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x10000000 0x0000A000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x1000A000 UNINIT 0x00002000 {
test.o (+ZI)
}
}
To define a variable, use the following method:
int uTimerCount __attribute__((zero_init));
Here, the variable attribute modifier __attribute__((zero_init))
is used to place uninitialized variables in the ZI
data section. In fact, by default, Keil
places uninitialized variables in the ZI
data area.