STM32のFlashに電源を切っても消えてほしくないデータを保存する方法を調べて実装してみました。

はじめに

マイクロマウスのプログラムを作る上で、電源を切っても迷路データを保持し続けるという機能が必要になりました。 マイクロマウスの機体にはSTM32F405を使っているので、STM32でFlash領域にデータを保存する方法について調べて実装をしてみました。

以下の話はSTM32F405 + gcc + Standard Peripheral Libraryを想定した話ですが、 他のSTM32マイコンやHAL Driverを使っている場合もだいたい同じだと思います。

STM32のFLASH特徴

STM32は自身のFlashを書き換えることができます。 ただし、SRAMのように自由には読み書きをすることはできません。 Flashへのアクセスは具体的に以下のような制約があります。

  • 読み取りはSRAMと同様
  • 書き込み、消去はブロックごとにしかできない
  • 書き込み、消去はunlockをしないとできない

ブロックというのはある程度まとまった領域のことで、 STM32のデータシート上ではSectorやPageと言われています。 STM32F405ではこのブロックはSectorと言われていて、 以下のような分割、配置になっています。

※画像はSTM32F4x5,7のReferenceManual(RM0090)より

使用するデバイスによって分割のされ方は大きく異なるので注意が必要です。

書き込み消去がブロックごとにしかできないというのは、 詳しくは次のようなことです。

  • 1であるbitを1にする操作はバイトごとにできる
  • 0であるbitを1にする操作はブロックごとにしかできない

この性質は他のFLASHメモリにもある性質みたいです(0,1が逆かもしれないですが)。 バイトごとに0であるbitを1にすることはできないので、 SRAMのように自由に書き換えることはできません。

以上のことを踏まえて、STM32でFlashにデータを書き込みは次の手順で行います。

  1. FLASHをunlockする
  2. FLASHのあるブロックを消去する
  3. FLASHにデータを書き込む(バイトごとに書き込める)
  4. FLASHをlockする

何かの拍子にFlashを書き換えてしまうことがないように、 Flashの書き換えはlockを解除しないとできないようになっています。 安全のために書き換えが終わったらちゃんとlockしておきます。

RAMを使った実装

先に説明しました通り、Flashにデータを書き込むときにはいったんFlashをブロック消去する必要があります。 すでに書き込まれているデータを消してしまわないように、 一旦FlashのブロックにあるデータをRAMに退避してからFlashをブロック消去します。

データを1byte書き換えるたびにブロックを読み出して消去していては無駄が多いので、 ブロック全体をRAMに読み出し、 RAM上で好きにデータを書き換えて、最後にまとめてデータをFlashに書き戻すという流れで Flashを書き換えます。

  1. FLASHのあるブロックのデータを全てRAMにコピーする
  2. RAM上でデータを編集する
  3. FLASHをブロック消去する
  4. RAM上のデータをブロックに書き戻す

どのSectorを使うか

上の実装では、使用するFLASHのSectorと同じサイズのRAMが必要ということになります。 そこまで大きなデータをバックアップするわけではないので、 サイズが一番小さい16kByteのセクターを使うことにします。

キリがいいのでSector0を使いたいところですが、 Sector0(0x08000000~0x08003FFF)には割り込みベクタを置かないといけないので、 Sector1(0x08040000~0x08007FFF)を使うことにします。

実装

Flashを操作する機能を以下のようにC言語で実装しました。

#define BACKUP_FLASH_SECTOR_NUM		FLASH_Sector_1
#define BACKUP_FLASH_SECTOR_SIZE	1024*16

// Flashから読みだしたデータを退避するRAM上の領域
// 4byteごとにアクセスをするので、アドレスが4の倍数になるように配置する
static uint8_t work_ram[BACKUP_FLASH_SECTOR_SIZE] __attribute__ ((aligned(4)));

// Flashのsector1の先頭に配置される変数(ラベル)
// 配置と定義はリンカスクリプトで行う
extern char _backup_flash_start;


// Flashのsectoe1を消去
bool Flash_clear()
{
	FLASH_Unlock();
	FLASH_Status result = FLASH_EraseSector(BACKUP_FLASH_SECTOR_NUM, VoltageRange_3);
	FLASH_Lock();

	return result == FLASH_COMPLETE;
}

// Flashのsector1の内容を全てwork_ramに読み出す
// work_ramの先頭アドレスを返す
uint8_t* Flash_load()
{
	memcpy(work_ram, &_backup_flash_start, BACKUP_FLASH_SECTOR_SIZE);
	return work_ram;
}

// Flashのsector1を消去後、work_ramにあるデータを書き込む
bool Flash_write_back()
{
    // Flashをclear
	if (!Flash_clear()) return false;

	uint32_t *p_work_ram = (uint32_t*)work_ram;

	FLASH_Unlock();

    // work_ramにあるデータを4バイトごとまとめて書き込む
	FLASH_Status result;
	const size_t write_cnt = BACKUP_FLASH_SECTOR_SIZE / sizeof(uint32_t);
	for (size_t i=0;i<write_cnt;i++) {
		result = FLASH_ProgramWord(
					(uint32_t)(&_backup_flash_start)+sizeof(uint32_t)*i,
					p_work_ram[i]
				);
		if (result != FLASH_COMPLETE) break;
	}

	FLASH_Lock();

	return result == FLASH_COMPLETE;
}

FLASHにバックアップ領域を確保する

Flashにバックアップ用にきちんと領域を確保しておかないと、 プログラムのリンク時にFlashのSector1に実行コードが配置されてしまい、 Flashを書き換えるとプログラムがきちんと実行されなくなってしまいます。

これをC言語のソースファイル上だけではうまく実現することができなかったので、 リンカスクリプトをいじることにしました。 具体的にはリンカスクリプトで以下の二つのことをしました。

  • 上のソース中の_backup_flash_startをFlashのSecto1の先頭に配置する
  • FlashのSector1に実行コードが配置されないようにする

ちなみにリンカスクリプトの書き方や動作については リンカ・ローダ実践開発テクニック: 実行ファイルを作成するために必須の技術という本を参考にしました。

この本は特にマイコン用というわけではありませんが、 C言語のプログラムをコンパイルした後に行われるリンク、 プログラムを実行する際のロード(スタートアップルーチンなど)について書かれています。 原理の解説にとどまるだけではなく、実験コードとその解説も載っているため、 動作を追うことで理解を深めることができます。

今回は以下のようなリンカスクリプトを用いました。 リンカスクリプト自体は結構長いので、 今回の話に関係する部分のみを抜粋しています。

MEMORY
{
    /*
    RAMとかの設定
    */
    FLASH_SECTOR0 (rx) : ORIGIN = 0x08000000, LENGTH = 16K
    FLASH_SECTOR1 (r) : ORIGIN = 0x08004000, LENGTH = 16K
    FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 992K
}

 /* 以下sectionの定義の抜粋 */

.isr_vector :
{
    /* 略 */
} >FLASH_SECTOR0
    
.backup_flash :
{
	_backup_flash_start = .;
	. = . + LENGTH(FLASH_SECTOR1);
} >FLASH_SECTOR1

.text :
{
    /* 略 */
} >FLASH

/* 略 */

STM32F405のFlashは0x08000000から1024kByteあるので、 通常リンカスクリプトには以下のように書かれています。

FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K

今回はこれを割り込みベクタを配置するSecto0、バックアップに使うSector1、その他に分割しました。

割り込みベクタの入っている.isr_vectorセクションをFLASH_SECTOR0に、 バックアップ用に確保する.backup_flashセクションをFLASH_SECTOR1に、 他の実行コードの入ってる.textセクションや、その他Flashに配置されるものを FLASHに割り当てています。 これによってリンク時にFlashのsecto1に実行コードが配置されることはありません。

リンカスクリプトのセクション定義の中で「.」はロケーションカウンタといって現在の位置を表しています。 _backup_flash_start = .とすることで現在の位置に_backup_flash_startというラベルを割り当てています。 _backup_flash_startという変数にアドレスを代入しているのではなく、 _backup_flash_startという変数をそのアドレスに配置するという意味なので、 C言語上でFlashのsector1の先頭アドレスを取得するには&_backup_flash_startとします。

正確には_backup_flash_startは.backup_flashの先頭に配置されますが、 FLASH_SECTOR1には.backup_flashセクションしか配置されないので、 _backup_flash_startはFLASH_SECTOR1の先頭に配置されます。

実際に使ってみる

次のようなコードを実行してみました。

uint32_t *flash_data = (uint32_t*)Flash_load();
printf("flash_data:%lu\n", *flash_data);
(*flash_data)++;
if (!Flash_write_back()) {
	printf("Failed to write flash\n");
}

内容としてはこんな感じです。

  1. Flashからデータを読み出す
  2. 先頭の4バイトをuint32_tとして解釈し、表示
  3. 2の値をカウントアップ
  4. Flashに書き戻す

実際に実行してみると、マイコンをリセットするたびに表示される値がカウントアップされていくのが確認できました。

おわりに

リンカ・ローダ実践開発テクニック: 実行ファイルを作成するために必須の技術 で得た知識を実践するいい機会になりました。