1. 程式人生 > 其它 >Keil MDK下如何設定非零初始化變數(轉)

Keil MDK下如何設定非零初始化變數(轉)

文章轉載自:Keil MDK下如何設定非零初始化變數_朱工的專欄-CSDN部落格

一些工控產品,當系統復位後(非上電覆位),可能要求保持住復位前RAM中的資料,用來快速恢復現場,或者不至於因瞬間復位而重啟現場裝置。而keil mdk在預設情況下,任何形式的復位都會將RAM區的非初始化變數資料清零。如何設定非初始化資料變數不被零初始化,這是本篇文章所要探討的。

在給出方法之前,先來了解一下程式碼和資料的存放規則、屬性,以及復位後為何預設非初始化變數所在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的執行節,該執行節起始地址為0x1000A000,長度為0x2000位元組(8KB),由UNINIT修飾:

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)
}
MYRAM 0x1000A000 UNINIT 0x00002000 {
.ANY (NO_INIT)
}
}
那麼,如果在程式中有一個數組,你不想讓它復位後零初始化,就可以這樣來定義變數:

unsigned char plc_eu_backup[PLC_EU_BACKUP_BUF/8] __attribute__((at(0x1000A000)));
變數屬性修飾符__attribute__((at(adder)))用來將變數強制定位到adder所在地址處。由於地址0x1000A000開始的8KB區域ZI變數不會被零初始化,所以處在這一區域的陣列plc_eu_backup也就不會被零初始化了。

這種方法的缺點是顯而易見的:要自己分配變數的地址,如果非零初始化資料比較多,這將是件難以想象的大工程(以後的維護、增加、修改程式碼等等)。所以要找到一種辦法,讓編譯器去自動分配這一區域的變數。

2. 分散載入文家同方法1,如果還是定義一個數組,可以用下面方法:

unsigned char plc_eu_backup[PLC_EU_BACKUP_BUF/8] __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資料區的。

4.將整個程式的非初始化變數都非零初始化 看了上面的,這個已經沒有必要說了