STM32内蔵のEEPROMを使用する方法

STM32L0シリーズやSTM32L1シリーズにはEEPROMが内蔵されています。
内蔵EEPROMに記録した内容は電源が切れても保持されるため設定などを記録しておくのに便利です。
今回はHLAを使用してSTM32L081を例に内蔵EEPROMにデータを読み書きする方法を紹介します。

# EEPROMの場所について
STM32に使われているCortex-Mでは、メモリマップという方式を用いて1つの線形メモリ上にSRAMやFlash ROM、IOポートなどを割り当てて管理しています。このようにすることでC言語上からはポインタを用いてSRAM以外にFlash ROMやIOポートにもアクセスすることができるようになります。以下の図はSTM32L0x1のメモリマップです[^1]。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401306/287a4641-7299-ec47-4148-bb03807e1385.png)

EEPROMも同様に同じ線形メモリ上に割り当てられているため、ポインタを用いてアクセスすることができます。データシートを見ると、STM32L081のEEPROMの場所は**0x08080000から0x080817FFまで**の6kBであることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401306/d6a109e4-0157-fe19-e799-2d35b097fefd.png)

# EEPROMに書き込む
EEPROMにデータを書き込むには`HAL_FLASHEx_DATAEEPROM_Program`という関数を使用します。
ただし、通常の状態だとEEPROMは書き込み保護がされているため書き込む前に保護を解除する必要があります。EEPROMの保護を解除するには`HAL_FLASHEx_DATAEEPROM_Unlock`という関数を、再度保護するには`HAL_FLASHEx_DATAEEPROM_Lock`という関数を使用します。

```c
HAL_FLASHEx_DATAEEPROM_Program (uint32_t TypeProgram, uint32_t Address, uint32_t Data);
HAL_FLASHEx_DATAEEPROM_Unlock(void);
HAL_FLASHEx_DATAEEPROM_Lock(void);
```

第1引数の`TypeProgram`には何バイト単位で書き込むかを指定します。指定できるのは以下の3種類です。

- `FLASH_TYPEERASEDATA_BYTE`　 　　1byte(8bit)単位
- `FLASH_TYPEERASEDATA_HALFWORD`　2byte(16bit)単位
- `FLASH_TYPEERASEDATA_WORD` 　　　4byte(32bit)単位

第2引数には書き込み先のアドレスを指定します。**EEPROMの範囲外のアドレスを指定するとHardFaultします。** 
STM32L081では0x08080000から0x080817FFまでの値を指定することができます。

第3引数には実際に書き込むデータを入力します。

以上のことを踏まえて、4byte(32bit)単位でデータを書き込む関数を作ると以下のようになります。

```c
void eeprom_writeWord(uint32_t address, uint32_t value) {
	if (!IS_FLASH_DATA_ADDRESS(address)) {
		return;
	}
	HAL_FLASHEx_DATAEEPROM_Unlock();
	HAL_FLASHEx_DATAEEPROM_Program(FLASH_TYPEPROGRAMDATA_WORD, address, value);
	HAL_FLASHEx_DATAEEPROM_Lock();
}
```

なお、`IS_FLASH_DATA_ADDRESS(address)`は`address`で指定した値がEEPROMのアドレスの範囲内にあるかどうかを判定するマクロです。(`stm32l0xx_hal_flash_ex.h`の中で定義されています。)
実際にEEPROMに書き込む前に、上記の例のように指定されたアドレスがEEPROMの範囲内であるか判定するようにすることでエラーを防ぎます。

# EEPROMから読み出す
HALにEEPROMからデータを読み出す関数は存在しません。というのも、1つの線形メモリ上にEEPROMが存在するため、ポインタを用いて直接アドレスを指定するだけで読めてしまうからです[^2]。
以下のようにしてデータを読み出すことができます。`address`にはEEPROM上の読みたいデータがある場所を指定します

```c
uint8_t  value = *(__I uint8_t *)address; //1byte(8bit)単位で読み出し
uint16_t value = *(__I uint16_t*)address; //2byte(16bit)単位で読み出し
uint32_t value = *(__I uint32_t*)address; //4byte(32bit)単位で読み出し
```
`__I`という見慣れない修飾子がついていますが、これは`volatile`と同義です。STM32でアドレスを直接指定してデータを読み出すときに必要になるようです[^3]。


# まとめ
4byte(32bit)単位でデータを読み書きする関数の例を以下にまとめておきます。(もう少しまともな実装例がある気がしますが、気が向いたら更新します。)

```c
void eeprom_writeWord(uint32_t address, uint32_t value) {
	if (!IS_FLASH_DATA_ADDRESS(address)) {
		return;
	}
	HAL_FLASHEx_DATAEEPROM_Unlock();
	HAL_FLASHEx_DATAEEPROM_Program(FLASH_TYPEPROGRAMDATA_WORD, address, value);
	HAL_FLASHEx_DATAEEPROM_Lock();
}

void eeprom_readWord(uint32_t address, uint32_t* value) {
	if (!IS_FLASH_DATA_ADDRESS(address)) {
		return;
	}
	*value = *(__I uint32_t*)address;
}
```

[^1]: STM32L0x1 Reference Manuals (RM0377) https://www.st.com/content/st_com/en/products/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus/stm32-ultra-low-power-mcus/stm32l0-series/stm32l0x1/stm32l081cb.html
[^2]: EEPROMに書き込む場合もポインタを用いて直接書き込むことができるように思えますが、推奨しません。正しく書き込むためには書き込み前と後にいくつかの手順が必要なようです。`stm32l0xx_hal_flash_ex.c`の中を見るとそれが分かります。HALを使用する場合、書き込みには素直に`HAL_FLASHEx_DATAEEPROM_Program`を用いましょう。
[^3]: STM32以外にもCortex-Mを用いたARM系のマイコンでは同様のようです。詳細は「C言語 volatile」などでググってみたり、CMSISのリファレンスを覗いてみたりしましょう。