Tangwx

Tangwx

博客网站

Method for not zeroing non-initialized variables after STM32 reset.

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:

  1. Modify the scatter-loading file by adding an execution section named MYRAM with the starting address of 0x2000C000 and a length of 0x2000 bytes (8KB), decorated with UNINIT:
    Where is the scatter-loading file located? The scatter-loading file is located in the Objects folder in the project directory and has a .sct extension. This file is the scatter-loading file, where sct 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.

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

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

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.

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