Tangwx

Tangwx

博客网站

Method for not zeroing non-initialized variables after STM32 reset

Method to keep non-initialized variables from being set to zero after STM32 reset#

Some products may require keeping the data in the RAM before the system reset (non-power-on reset) to quickly restore the scene or prevent the restart of the scene device due to instant reset. By default, Keil MDK will clear the data of non-initialized variables in the RAM for any form of reset. This article discusses how to set non-initialized data variables to not be zero-initialized.

In my case, I need to modify the variable value and store it in flash to prevent data loss during power-off. However, there is a problem that this part of the content will be overwritten by the initial value after pressing the reset button. Therefore, this part of the content needs to be uninitialized. Once I set it, both power-off and reset will not clear the content here.

Before providing the method, let's first understand the rules and attributes of code and data storage, as well as why non-initialized variables in RAM are initialized to zero by default after reset.

What are initialized data variables and non-initialized data variables?
Define a variable: int nTimerCount = 20; The variable nTimerCount is an initialized variable, which means it already has an initial value.
If you define a variable: int nTimerCount; The variable nTimerCount is an uninitialized variable, and Keil MDK will default to placing 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 the ARM image file. This part may seem boring, but I think it is necessary to master.

Composition of the 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.

Each input section contains four types of content: code, initialized data, uninitialized storage area, and storage area initialized to zero. Each input section has corresponding attributes: read-only (RO), read-write (RW), and initialized to zero (ZI).
Each 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.
Each region contains one to three output sections, and the attributes of each output section are different: RO attribute, RW attribute, and ZI attribute.

From here, we can know that in general, code will be placed in the input section with the RO attribute, initialized variables will be allocated to the input section with the RW attribute, and the input section with the "ZI" attribute can be understood as a collection of variables initialized to zero.

Where are the initial values of initialized variables placed in the hardware? (For example, if you define int nTimerCount = 20;, where is the initial value 20 placed?) I think this is an interesting question. For example, after Keil completes the compilation, it will provide 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 don't know how this is calculated or how much code is placed in ROM/Flash. In fact, the initial values of those initialized variables are placed in the input section with the RW attribute, and these initial values are placed in ROM/Flash. Sometimes these initial values are relatively large, and Keil will compress these initial values before placing them in ROM/Flash to save storage space. So who and when will restore these initial values to RAM? Who and when will the variables in the input section with the "ZI" attribute be zero-initialized? To understand these things, you need to see what Keil does for you by default from system reset to executing the main function you write in C code.

After hardware reset, the first step is to execute the reset handler, the entry of this program 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 following code calls the __main function. Here, the __main function will call a series of C library functions to complete the copying, decompression, and zero initialization of the code and data. The decompression and copying of data include copying the initial values of initialized variables stored in ROM/Flash to the corresponding RAM. For a variable, it may have three attributes. Variables modified by the const qualifier are most likely placed in the input section with the RO attribute, and initialized variables will be placed in the input section with the RW attribute. Then the remaining variables need to be placed in the input section with the "ZI" attribute. By default, the zero initialization of the "ZI" data will initialize all "ZI" data sections to zero. This is done by the compiler before the main function of the program executes C code after each reset. So we need to set some variables in the C code to not be zero-initialized after reset. We cannot let the compiler do whatever it wants. We need to use some rules to constrain 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. Therefore, we can define an execution section named MYRAM in the scatter-loading file, with a starting address of 0x2000C000 and a length of 0x2000 bytes (8KB), modified by UNINIT:

Where is the modification file? The modification file is in the Objects folder in the project folder, with a file extension of .sct. This file is the modification file, and sct stands for scatter. The .sct file is the scatter-loading file. The scatter-loading file allows you to define different positions, such as where the code is stored, where the data is stored, and where to find the next function to be executed at a specific address. Let's open it and take a look:

We add an 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 there is an array in the program that you don't want to be zero-initialized after reset, you can define the variable like this:

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 where the ZI variables are not zero-initialized, the array PressureHi in this region will not be zero-initialized.

The disadvantage of this method is obvious: you have to allocate the address of the variable yourself. If there are many non-zero-initialized data, this will be an unimaginable task (maintenance, adding, modifying code, etc. in the future). So we need to find a way to let the compiler automatically allocate variables in this region.

In the scatter-loading file, use the same method as method 1. If you still want to define an array, you 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. zero_init means that uninitialized variables are placed in the ZI data section. Because "NO_INIT" is an explicitly named custom section with the UNINIT attribute. (The simplest method is strongly recommended)

How to make all non-initialized variables in a module non-zero-initialized?
If the module name is test.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)
  }
}

Use the following method for definition:

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 section.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.