STM32
復位後非初始化變量不被置零方法#
一些產品,當系統復位後(非上電復位),可能要求保持住復位前 RAM 中的數據,用來快速恢復現場,或者不至於因瞬間復位而重啟現場設備。而Keil MDK
在默認情況下,任何形式的復位都會將 RAM 區的非初始化變量數據清零。如何設置非初始化數據變量不被零初始化,這是本篇文章所要探討的。
我這裡是需要將變量值修改後存到 flash 裡面,使得掉電不丟失,但存在一個問題,按下復位按鍵後,這部分內容會被初始值覆蓋掉,因此這部分內容需要不被初始化,在我設定好一次後,掉電和復位均不會清除這裡的內容。
在給出方法之前,先來了解一下代碼和數據的存放規則、屬性,以及復位後為何默認非初始化變量所在 RAM 都被初始化為零了呢。
什麼是初始化數據變量,什麼又是非初始化數據變量?
定義一個變量:int nTimerCount=20;
變量nTimerCount
就是初始化變量,也就是已經有初值;
如果定義變量:int nTimerCount;
變量nTimerCount
就是一個非賦值的變量,Keil MDK
默認將它放到屬性為ZI
的輸入節。
那麼,什麼是“ZI”
,什麼又是“輸入節”
呢?這要了解一下 ARM 映像文件(image)的組成了,這部分內容略顯無聊,但我認為這是非常有必要掌握的。
ARM 映像文件的組成:
一個映像文件由一個或多個域(region,也有譯為 “區”)組成
每個域包含一個或多個輸出段(section,也有譯為 “節”)
每個輸出段包含一個或多個輸入段
各個輸入段包含了目標文件中的代碼和數據
輸入段中包含了四類內容:代碼、已經初始化的數據、未經過初始化的存儲區域、內容初始化為零的存儲區域。每個輸入段有相應的屬性:只讀的(RO
)、可讀寫的(RW
)以及初始化成零的(ZI
)。
一個輸出段中包含了一些列具有相同的RO
、RW
和ZI
屬性的輸入段。輸出段屬性與其中包含的輸入段屬性相同。
一個域包含一到三個輸出段,各個輸出段的屬性各不相同:RO
屬性、RW
屬性和ZI
屬性
到這裡我們就可以知道,一般情況下,代碼會被放到RO
屬性的輸入節,已經初始化的變量會被分配到RW
屬性輸入區,而“ZI”
屬性輸入節可以理解為是初始化成零變量的集合。
已經初始化變量的初值,會被放到硬件的哪裡呢?(比如定義int nTimerCount=20;
那麼初始值 20 被放到哪裡呢?),我覺得這是一個有趣的問題,比如Keil
在編譯完成後,會給出編譯文件大小的信息,如下所示:
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)
很多人不知道這是怎麼計算的,也不知道究竟放入 ROM/Flash 中的代碼有多少。其實,那些已經初始化的變量,是被放入RW
屬性的輸入節中,而這些變量的初值,是被放入 ROM/Flash 中的。有時候這些初值的量比較大,Keil
還會將這些初值壓縮後再放入 ROM/Flash 以節省存儲空間。那這些初值是誰在何時將它們恢復到 RAM 中的?ZI
屬性輸入節中的變量所在 RAM 又是誰在何時給用零初始化的呢?要了解這些東西,就要看默認設置下,從系統復位,到執行 C 代碼中你編寫的 main 函數,Keil
幫你做了些什麼。
硬件復位後,第一步是執行復位處理程序,這個程序的入口在啟動代碼裡 (默認),摘錄一段cortex-m3
的復位處理入口代碼:
Reset_Handler PROC ;PROC等同於FUNCTION,表示一個函數的開始,與ENDP相對?
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
初始化堆棧指針、執行完用戶定義的底層初始化代碼(SystemInit
函數)後,接下來的代碼調用了__main
函數,這裡__main
函數會調用一些列的 C 庫函數,完成代碼和數據的複製、解壓縮以及ZI
數據的零初始化。數據的解壓縮和複製,其中就包括將儲存在 ROM/Flash 中的已初始化變量的初值複製到相應的 RAM 中去。對於一個變量,它可能有三種屬性,用const
修飾符修飾的變量最可能放在RO
屬性區,已經初始化的變量會放在RW
屬性區,那麼剩下的變量就要放到ZI
屬性區了。默認情況下,ZI
數據的零初始化會將所有ZI
數據區初始化為零,這是每次復位後程序執行 C 代碼的 main 函數之前,由編譯器 “自作主張” 完成的。所以我們要在 C 代碼中設置一些變量在復位後不被零初始化,那一定不能任由編譯器 “胡作非為”,我們要用一些規則,約束一下編譯器。
分散加載文件對於連接器來說至關重要,在分散加載文件中,使用UNINIT
來修飾一個執行節,可以避免__main
對該區節的ZI
數據進行零初始化。這是要解決非零初始化變量的關鍵。因此我們可以定義一個UNINIT
修飾的數據節,然後將希望非零初始化的變量放入這個區域中。於是,就有了第一種方法:
\1. 修改分散加載文件,增加一個名為MYRAM
的執行節,該執行節起始地址為0x2000C000
,長度為0x2000
字節(8KB
),由UNINIT
修飾:
那修飾文件在哪呢?修飾文件在工程文件夾中的Objects
文件夾內,有一個.sct
後綴的文件,這個文件就是修飾文件,sct
全名scatter
,意味分散。.sct
文件就是分散加載文件。分散加載文件就是可以通過這個腳本文件來自己定義各個不同的位置,哪裡存的是代碼、哪裡存的是數據,去哪個特定的地址找到下一步需要運行的函數等等。我們打開它看一下
我們在文檔最後的}
前,添加名為MYRAM
的執行節,如下所示
; *************************************************************
; *** 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)
}
}
那麼,如果在程序中有一個
數組,你不想讓它復位後零初始化,就可以這樣來定義變量:
uint8_t PressureHi[16] __attribute__((at(0x2000C000)));
變量屬性修飾符__attribute__((at(adder)))
用來將變量強制定位到 adder 所在地址處。由於地址0x2000C000
開始的8KB
區域ZI
變量不會被零初始化,所以處在這一区域的數組PressureHi
也就不會被零初始化了。
這種方法的缺點是顯而易見的:要自己分配變量的地址,如果非零初始化數據比較多,這將是件難以想象的大工程(以後的維護、增加、修改代碼等等)。所以要找到一種辦法,讓編譯器去自動分配這區域的變量。\2. 分散加載文件同方法 1,如果還是定義一個數組,可以用下面方法:
uint8_t PressureHi[16] __attribute__((section("NO_INIT"),zero_init));
變量屬性修飾符__attribute__((section(“name”),zero_init))
用於將變量強制定義到 name 屬性數據節中,zero_init
表示將未初始化的變量放到ZI
數據節中。因為“NO_INIT”
這顯性命名的自定義節,具有UNINIT
屬性。(強烈推薦最簡單的方法)
\3. 如何將一個模塊內的非初始化變量都非零初始化?
假如該模塊名字為test.c
,修改分散加載文件如下所示:
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)
}
}
定義時使用如下方法:
int uTimerCount __attribute__((zero_init));
這裡,變量屬性修飾符__attribute__((zero_init))
用於將未初始化的變量放到ZI
數據節中變量,其實Keil
默認情況下,未初始化的變量就是放在ZI
數據區的。