アプリケーションによっては,製品出荷時にユニークなIDを記憶させたり,キャリブレーション値を保持する時にMCU内蔵のEEPROMへデータを書き込む事があります.
しかしながら,STM32シリーズのチップにはEEPROMがありません.どうやらFlashメモリをEEPROMのように書き換えて使う想定のようです.
FLASHメモリと言えば書き換え可能な回数が限られており,頻繁な書き換えは信頼性の低下が懸念されます.
そこでデータシートを参照してみると,最低でも1万回の書き換えが保証されているようでした.
これならループを回して書き換えたりしない限り,メモリの劣化は考えなくても良さそうです.
懸念はそれだけではありません.Flashメモリは1バイトづつ書き込む事はできるものの,消去はセクタ単位での一括消去となります.故に,書き換える場合には以下の手順が必要です.
書き換えたいセクタをSRAMへ読み出す
当該セクタを消去
SRAM上で書き換え
SRAMからFlashへ書き戻し
一般にFlashメモリのセクタサイズは数kByteから数百kByteと(組込みの世界では)比較的大きな単位になります.
そのため,セクタサイズが余りに大きいと書き込んであるデータをSRAMへ退避する事ができない場合があります.
STM32F411xCのメモリ構成を見てみましょう.
Sector0からSector7までの8つのセクタで構成されており,セクタサイズは16kByteから128kByteのようです.
更に,Flashメモリの後方へ行くほどセクタサイズは大きくなるようでSector5以降は128kByteものサイズになっています.
一般にプログラムはメモリの先頭から書き込まれていくのでメモリ後方ほど空いていることが期待できるため,後方の領域を書き換えて使いたくなります.
しかし前述した書き換え手順を踏むとすると,128kByteのSRAMを持つMCUで128kByteのSectorを丸ごと読み出すのは不可能です.
一方,16kByteのSector0についてはリセットベクタ等を含む割り込みベクタテーブルを配置しなければならず,アプリケーションを動作をさせる上で動かすことのできない重要な情報を配置するセクタになっています.
Sector0の後半を書き換え領域に使うアイデアもアリですが,リスクが付き纏います.
例えば,Sector0のerase直後や書き換え中に電源が落ちたりリセットしてしまった場合にはベクタテーブルが破壊されるため,暴走や文鎮化する可能性があります.
そこで,リンカスクリプトを編集してSector1をアプリケーションから書き換えできる領域として予約することにします.16kByteならばSRAMへデータ退避用領域を確保できるでしょう.
STM32の開発を System Workbench for STM32 で CubeMX を使っている場合にはリンカスクリプトが自動生成され,次のようにメモリが定義されていました.
(STM32F411RETx_FLASH.ld)
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}
Flashメモリが512kByteの一塊になっているので,これをセクタの切れ目で3つに分割します.
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH0 (rx) : ORIGIN = 0x8000000, LENGTH = 16K
FLASH1 (rx) : ORIGIN = 0x8004000, LENGTH = 16K
FLASH2 (rx) : ORIGIN = 0x8008000, LENGTH = 480K
}
このメモリ定義に合わせてセクションの定義を書き換えます.
Sector0に配置しなければならない.isr_vector
をFlash0
に,その他のセクションをSector2
以降のFlash2
に配置されるようにします.
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH0
/* The program code and other data goes into FLASH */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >FLASH2
(以下略)
こんな感じ.
これだけでもSector1には何もデータが書き込まれないことが保証されますが,明示的にセクションを追加しておきます.
.userdata : {
__USER_DATA = .;
KEEP(*(.userdata))
FILL(0xff)
} > FLASH1
わかりやすくシンボル__USER_DATA
を使ってコードからこのセクタへアクセスできるようにしました.
buildするにあたって,userdata領域へ書き込むオブジェクトは何もないのでオプトアウトされないようKEEP
しておきます.
ついでにFlashメモリの消去後の初期値は0xff
なので空き領域を0xff
でFILL
しておきました.
一旦buildして生成されたmapファイルを覗いて期待した通りに配置されたかどうか確認します.
(output.map)
(前略)
.isr_vector 0x08000000 0x198
0x08000000 . = ALIGN (0x4)
*(.isr_vector)
.isr_vector 0x08000000 0x198 startup/startup_stm32f411xe.o
0x08000000 g_pfnVectors
0x08000198 . = ALIGN (0x4)
.userdata 0x08004000 0x0
0x08004000 __USER_DATA = .
*(.userdata)
FILL mask 0xff
.text 0x08008000 0x4ec0
(後略)
うまくいったようです.
Cソースの適当な場所で
extern const void * __USER_DATA;
を宣言しておく事でSector1の先頭アドレスが得られます.
STM32のHALドライバにはFLashを書き換えるためのドライバが用意されています.これを使えば
HAL_FLASH_Program(TYPEPROGRAM_BYTE, __USER_DATA, hoge);
このように書き込むことができます.
__USER_DATA「の」アドレスを参照することでSector 1の先頭アドレスが得られます。
アドレスを取得するには&__USER_DATAとする必要があるので、__USER_DATAをポインタとして宣言しておくことでアドレスを取得できるような書き方はミスリーディングかと思います。