天天看點

Keil MDK下如何設定非零初始化變量(複位後變量值不丢失)

一些工控産品,當系統複位後(非上電複位),可能要求保持住複位前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也就不會被零初始化了。

這種方法的缺點是顯而易見的:要自己配置設定變量的位址,如果非零初始化資料比較多,這将是件難以想象的大工程(以後的維護、增加、修改代碼等等)。是以要找到一種辦法,讓編譯器去自動配置設定這一區域的變量。
           
  1. 分散加載文家同方法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屬性。

  1. 如何将一個子產品内的非初始化變量都非零初始化?

假如該子產品名字為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));

————————————————

版權聲明:本文為CSDN部落客「zhzht19861011」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/zhzht19861011/article/details/8780837

繼續閱讀