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
もゼロ初期化されません。
この方法の欠点は明らかです:変数のアドレスを自分で割り当てる必要があります。非ゼロ初期化データが多い場合、これは想像を絶する大きなプロジェクトになる可能性があります(将来のメンテナンス、コードの追加、変更など)。したがって、コンパイラにこの領域の変数を自動的に割り当てさせる方法を見つける必要があります。
- 分散ロードファイルは方法 1 と同じです。配列を定義する場合は、次の方法を使用できます。
uint8_t PressureHi[16] __attribute__((section("NO_INIT"),zero_init));
変数の属性修飾子__attribute__((section("name"),zero_init))
は、変数を name 属性のデータセクションに強制的に定義するために使用されます。zero_init
は、初期化されていない変数をZI
データセクションに配置することを意味します。なぜなら、"NO_INIT"
という明示的に名前付けられたカスタムセクションは、UNINIT
属性を持っているからです。(最も簡単な方法を強くお勧めします)
- モジュール内のすべての非初期化変数を非ゼロ初期化する方法はありますか?
モジュールの名前が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
データ領域に配置されます。