umtkmの忘備録

【Nucleo F401RE】内蔵フラッシュにデータを書き込んでみる(mbed越しに)

 ロボット作ってると,電源を切ってもデータを残したいということが結構あります.例えば,センサのキャリブレーション値とか.ArduinoとかだとEEPROMが内蔵されてるんで楽なんですが,STM32には無いんで,頑張って内蔵フラッシュにデータを書き込んでみようと思います.

環境

 オフライン環境が前提です(リンカスクリプトをいじるので).mbedのオンラインコンパイラからExportしただけでOKです.詳細はググってください.

  • Nucelo F401RE
  • macOS
  • arm-none-eabi-gcc

セクタとページ

 EEPROMなんかだと1byteごとにデータの書き換えができるんですが,内蔵フラッシュとなるとそうはいきません.STM32の内蔵フラッシュには,次の特徴があります.

  • 全てのbitが1の場所には,任意のデータを書き込める.
  • 1じゃないbitがある(=既に書き込み済み)場合,データは書き込めない.
  • 全てのbitを1にするには,セクタやページごと行う必要がある.

はぁ? って感じですね.要は,一度書き込んだあとは,セクタやページごとリセットしないと書き込めないってことです.なんて不便なんでしょう.

 んで,セクタやページってなんじゃらほいって話なんですが,これは数k~数百kByteくらいのまとまりのことを指してます.セクタは数十k~数百kByteほどの比較的大きなまとまり,ページは数kバイトの比較的小さなまとまりとして使い分けられているようです.STM32F4では,いくつかのセクタに,STM32F3,F1ではたくさんのページに分割されています.

フラッシュのどこにデータを置くか

 さて,セクタとページについて大雑把に理解したところで,フラッシュのどこにデータを置くかを決めないと書き込みも読み込みもできません.具体的に,セクタが何個あって,サイズはどのくらいで,アドレスは何なのか知る必要がありますね.リファレンスマニュアルを読みましょう.
(RM0368 Reference manual STM32F401xB/C and STM32F401xD/E advanced ARM®-based 32-bit MCUs より)
 僕の部屋に落ちてたF401の場合,どうやら7つのセクタに分割されていますね.16kByteのが4つと,64kBが1つ,128kBのが3つありますね.このセクタのうちのどれか一つにデータ保存用の領域を確保すればいいわけです.そんなたくさんのデータを置くわけじゃ無いんで本当は16kBのセクタを使いたいんですが,結構めんどくさいので一番お尻の128kBのセクタを使ってみようと思います(何も考えずにやるとプログラムが動作しなくなる…).このセクタはアドレス0x08060000から始まってるみたいですね.

リンカスクリプトを編集する

 リンカスクリプトはオブジェクトファイルをリンクするときに使われていて,メモリアドレス周りを決定してるみたいです.内蔵フラッシュはプログラムを置くスペースとしても使われてるんで,コードがさっき決定した場所に置かれてしまうとまずいわけです.コードが突然消えてしまったら実行時に何が起こるかわかりません.そこでリンカスクリプトをいじります.では,f401のmbedでのリンカスクリプトの最初の7行くらいを見てみます.オンラインコンパイラからエクスポートした場合には,

1
mbed/TARGET_NUCLEO_F401RE/TOOLCHAIN_GCC_ARM/STM32F401XE.ld

にあると思います.

1
2
3
4
5
6
/* Linker script to configure memory regions. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000194, LENGTH = 96k - 0x194
}

“FLASH”としてアドレス0x08000000から512KBが確保されてるぽいってのが雰囲気的にわかると思います.ここにプログラムが置かれるんですね.ではこいつのセクタ7の部分を,つまりケツの128KByteを削ぎ落として,その部分に128kByteのアドレス0x08060000から始まる”DATA”を追加してみましょう.

1
2
3
4
5
6
7
/* Linker script to configure memory regions. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 384K
DATA (rx) : ORIGIN = 0x08060000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000194, LENGTH = 96k - 0x194
}

こんな感じです.384Kってのはもちろん512K-128Kのことです.暗算すると死ぬよ? これでコードがデータと衝突することはなくなりました(プログラムに使える領域が384kByteに減ってしまいましたが…).

mbed越しにHALを呼び出して消去&書き込み

 フラッシュを消去したり書き込んだりするには,まずはフラッシュをアンロックする必要があります.といっても,HAL_FLASH_Unlock()って関数を呼び出すだけです.そのあとにセクタを丸ごと消去して,1Byteずつ書き込んでやればOKです.当然,アンロックしたのですから,操作が終わったらロックすることを忘れずに.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _eraseFlash(void)
{
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS; /* セクタを選ぶ */
erase.Sector = FLASH_SECTOR_7; /* セクタ7を指定 */
erase.NbSectors = 1; /* 消すセクタの数.今回は1つだけ */
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; /* 3.3Vで駆動するならこれで */

uint32_t pageError = 0;

HAL_FLASHEx_Erase(&erase, &pageError); /* HAL_FLASHExの関数で消去 */
}

void writeFlash(uint32_t address, uint8_t *data, uint32_t size)
{
HAL_FLASH_Unlock(); /* フラッシュをアンロック */
_eraseFlash(); /* セクタ7を消去 */
do {
/* 1Byteずつフラッシュに書き込む */
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, address, *data);
} while (++address, ++data, --size);
HAL_FLASH_Lock(); /* フラッシュをロック */
}

フラッシュの内容を読み込む

 読み込みは簡単です.書き込んだアドレスを参照するだけです.具体的には,こんな感じで読めます.

1
*((uint8_t*)address)

読み出したい型のポインタ型に強制的にキャストして参照してます.uint8_tのところは読み出したい型で,floatでも何でもいけちゃいます.ただ毎回これってのも面倒なので,関数にしてみます.

1
2
3
4
void loadFlash(uint32_t address, uint8_t *data, uint32_t size)
{
memcpy(data, (uint8_t*)address, size);
}

もはやmemcpy()を適当にラップしただけですね.これでスマートに読み込みが行えます.

複数のデータの保存

 保存するデータが1つだけなら何も考えずにできるんですが,複数となると管理が面倒です.そこで今回は構造体を書き込んでみることにしましょう.さっき作った関数を使ってみます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct {
int chino;
float maya;
uint16_t megu;
} Chimame;

int main(void)
{
const uint32_t address = 0x8060000;
Chimame chimame;

/* フラッシュから構造体にデータを読み込む */
loadFlash(address, (uint8_t*)&chimame, sizeof(Chimame));

/* 構造体のメンバに変更を加える */
chimame.chino = 144;
chimame.maya = 140.0;
chimame.megu = 145;

/* フラッシュに構造体のデータを書き込む */
writeFlash(address, (uint8_t*)&chimame, sizeof(Chimame));

return 0;
}

起動後にloadFlash()で構造体のデータを読み込んで,変更があった場合にはwriteFlash()で構造体を書き込めばいいんじゃないんでしょうか.一応,別のプログラムを書き込んでもデータはそのままでした.

まとめ

 リンカスクリプトをいじって領域を確保してやれば,HALの関数を使って簡単にデータの読み書きができます.構造体を書き込むと管理がとても楽になります.