STM32
リセット後の非初期化変数のゼロ化方法#
いくつかの製品では、システムのリセット後(電源オンリセットではない場合)、リセット前の RAM のデータを保持する必要がある場合があります。これは、現場の復元を迅速に行うため、または瞬時のリセットによる現場のデバイスの再起動を防ぐためです。しかし、デフォルトでは、Keil MDK
はどの形式のリセットでも、RAM 領域の非初期化変数のデータをゼロクリアします。非初期化データ変数がゼロで初期化されないようにする方法について、この記事では説明します。
私は変数の値を変更してフラッシュに保存する必要がありますが、問題があります。リセットボタンを押すと、この部分の内容が初期値で上書きされます。したがって、この部分の内容は初期化されないようにする必要があります。一度設定した後、電源断とリセットの両方でこの部分の内容がクリアされないようにする必要があります。
方法を示す前に、コードとデータの配置規則、属性、およびリセット後になぜデフォルトで非初期化変数が含まれる RAM がすべてゼロに初期化されるのかについて理解しましょう。
初期化データ変数とは何ですか?非初期化データ変数とは何ですか?
変数を定義する:int nTimerCount = 20;
変数nTimerCount
は初期化変数であり、初期値があることを意味します。
変数を定義する:int nTimerCount;
変数nTimerCount
は初期化されていない変数であり、Keil MDK
はそれをZI
属性の入力セクションに配置します。
では、「ZI」とは何で、「入力セクション」とは何ですか?これについては、ARM イメージファイルの構成について理解する必要があります。この部分は少し退屈かもしれませんが、非常に重要なことだと思います。
ARM イメージファイルの構成:
イメージファイルは、1 つ以上の領域(region)で構成されます。
各領域には、1 つ以上の出力セクション(section)が含まれます。
各出力セクションには、1 つ以上の入力セクションが含まれます。
各入力セクションには、コードとデータが含まれます。
入力セクションには、次の 4 つのタイプの内容が含まれます:コード、初期化済みデータ、初期化されていないストレージ領域、ゼロで初期化されたストレージ領域。各入力セクションには、読み取り専用(RO
)、読み書き可能(RW
)、およびゼロで初期化された(ZI
)の属性があります。
1 つの出力セクションには、同じRO
、RW
、およびZI
属性を持つ複数の入力セクションが含まれます。出力セクションの属性は、含まれる入力セクションの属性と同じです。
1 つの領域には、1 つから 3 つの出力セクションが含まれ、各出力セクションの属性は異なります: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 にコピーすることも含まれます。変数には 3 つの属性があります。const
修飾子で修飾された変数は、おそらくRO
属性領域に配置されます。初期化された変数はRW
属性領域に配置されます。したがって、残りの変数はZI
属性領域に配置する必要があります。デフォルトでは、ZI
データのゼロ初期化は、すべてのZI
データ領域をゼロで初期化します。これは、コンパイラが「勝手に」行うものであり、プログラムが C コードの main 関数を実行する前に、毎回リセット後に行われます。したがって、C コードでいくつかの変数をリセット後にゼロで初期化しないようにするには、コンパイラを「勝手に」させないようにするためのいくつかのルールを設定する必要があります。
リンカにとって、分散ロードファイルは非常に重要です。分散ロードファイルでは、UNINIT
を使用して実行セクションを修飾することで、__main
がそのセクションのZI
データをゼロ初期化しないようにすることができます。これが非ゼロ初期化変数を解決するための鍵です。したがって、UNINIT
で修飾されたデータセクションを定義し、非ゼロ初期化の変数をこの領域に配置することができます。したがって、次の方法があります。
- 分散ロードファイルを変更し、
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
データ領域に配置されます。