EEPROMを使ったKey-Valueストア(NVRAM)の実装
作りたいものの全体像から参照しています。
作りたいもの
OTAアップデートを実装する際、OTAアップデートされないデバイス固有の情報(デバイスIDや、デバイスごとに異なる認証情報など)を保存領域が必要になります。メインの記憶域(Raspberry PiならSDカードなど)にパーティションを作っても良いのですが、ハンドリングの明確さ・容易さ(SDカードを変えても変わらない)からEEPROMにKey-Valueストアを作り、使用したいと思います。
動作する全ソースコードをGitHubで公開しています。
ハードウェア
- RasPi4
- EEPROM
- AT24C256(アトメル製の256ビットEEPROM、2.7〜5.5V、I2C接続)
- モジュールを使いました。Amazonで買えます。
- データシートはこちらを参照しました。
デバイスドライバ
ターゲット上のデバイスドライバはYocto 5.0のRasPi4ターゲット標準のi2c-devデバイスドライバを使用し、ユーザスペースから制御します。なお、書き込みはターゲットとは別のRasPi3にRasPi OSを入れ、こちらも標準のi2c-devデバイスドライバを使用します。
YoctoでのI2Cのenableはこちらを参照してください。
EEPROMレイアウト
EEPROMは書き込みはページ単位、読み出しはワード単位となります。AT24C256では1ページが64ワード、1ワードが8bitなので、1ページは64バイトになります。(オペレーションではワード単位で書き込みできますが、内部的にはページを書き換えています)
ということで、64バイトにアラインする形でレイアウトします。
まず最初の1ページはヘッダ領域とします。またKey-ValueペアのKeyは32バイトに保存し、最大64個とします。残りをデータ領域とします。
実装
それでは作っていきます。ソースコード用のGitリポジトリ(nvram)と、Yoctoで使用する際のリポジトリは分離する構成とします。このページではnvram部分の解説をします。
JSONファイルからKey-Valueペアを取得し、すべて書き込むnvram-write
、Keyを指定してValueを取得するnvram-read
、これらの共通する処理を実装するlibnvram
の3つのコンポーネントを実装します。
まずnvramリポジトリを作成し、nvram-write、nvram-read、libnvramディレクトリを作成します。
libnvram
Cで書きました。
EEPROMデバイス周り
ほかのEEPROMデバイスを使用するかもしれないので、共通したEEPROMへの読み書き関数はeeprom.h
に定義しておきます。それを特定のEEPROMデバイス(ここではAT24C256)専用のコードで実装します。これでincludeするヘッダとリンクするCコードを切り替えるだけで、呼び出し元としてはデバイスの差異を意識せずEEPROMへの読み書きを呼び出せることを意図しています。
int eeprom_i2c_open(const char *device);
void eeprom_i2c_close(int i2c_fd);
int eeprom_page_write(int i2c_fd, uint16_t addr, char *buf);
int eeprom_page_read(int i2c_fd, uint16_t addr, char *buf);
i2c-devドライバを使用したEEPROMデバイス(AT24C256)へのwrite/read関数を用意します。まずヘッダでI2Cアドレスを指定します。このデバイスは電源投入時のA0/A1/A2のH/L状態でI2Cアドレスを変更できます。モジュールのピンヘッダをすべて抜いたのでアドレスは0x50になります。
#include "eeprom.h"
#ifndef A0
#define A0 0
#endif /* A0 */
#ifndef A1
#define A1 0
#endif /* A1 */
#ifndef A2
#define A2 0
#endif /* A2 */
#define EEPROM_ADDR ((0x05 << 4) | (A2 << 2) | (A1 << 1) | A0)
I2Cまわりの実装はi2c-devの普通の使い方になりますので説明は割愛します(at24c256.hのeeprom_i2c_open/close/write/read関数参照)。このEEPROMデバイス固有の処理としてページの読み書きをeeprom_page_write
、eeprom_page_read
に実装していきます。
ページを書き込むには、2バイトのEEPROMアドレスに続いてページ分のデータを書き込みます。簡単のためeeprom_page_write_data_t
を作成し、値をセットしてchar配列にキャストしてI2Cに書き込んでいます。また書き込んだ後にデータシートにあるWrite Cycle Time(5Vの場合10ms)だけwaitしています。
ページを読み込むには、2バイトのEEPROMアドレスのみ書き込むとEEPROMデバイスがページサイズ分のデータを返してくるので読み込んでいきます。
typedef struct {
char addr[EEPROM_ADDR_SIZE];
char data[EEPROM_PAGE_SIZE];
} eeprom_page_write_data_t;
int eeprom_page_write(int i2c_fd, uint16_t addr, char *buf) {
eeprom_page_write_data_t page_write_data;
page_write_data.addr[0] = (char) ((addr >> 8) & 0xff);
page_write_data.addr[1] = (char) (addr & 0xff);
memcpy(page_write_data.data, buf, EEPROM_PAGE_SIZE);
int ret;
ret = eeprom_i2c_write(i2c_fd, EEPROM_ADDR, sizeof(eeprom_page_write_data_t), (char *) &page_write_data);
if(ret < 0) {
return ret;
}
// wait for write cycle time
struct timespec write_cycle = {
.tv_sec = 0,
.tv_nsec = WRITE_CYCLE_TIME_MS * 1000000,
};
ret = nanosleep(&write_cycle, NULL);
if(ret < 0) {
return EEPROM_ERR;
}
return EEPROM_SUCCESS;
}
int eeprom_page_read(int i2c_fd, uint16_t addr, char *buf) {
int ret;
char _addr[] = {
(char) ((addr >> 8) & 0xff),
(char) (addr & 0xff),
};
ret = eeprom_i2c_write(i2c_fd, EEPROM_ADDR, EEPROM_ADDR_SIZE, _addr);
if(ret < 0) {
return ret;
}
ret = eeprom_i2c_read(i2c_fd, EEPROM_ADDR, EEPROM_PAGE_SIZE, buf);
if(ret < 0) {
return ret;
}
return EEPROM_SUCCESS;
}
EEPROMレイアウト・Key-Valueストア周り
ここまででEEPROMのページの読み書きができるようになったので、EEPROMレイアウト・Key-Valueストアを実装していきます。
NVRAMのレイアウトはこのように実装しました。デバイス自身は256kbitなのですが、このうち最初の128kbitのみ使用します。これは現在それほどサイズが必要ないことと、将来的に運用中の更新などで2面持ちしたい場合があるかもしれない為の措置です。
#define NVRAM_SIZE (128 * 1024 / 8) // 128K
#define NVRAM_MAGIC_SIZE (16)
#define NVRAM_N_DATA (64)
#define NVRAM_KEY_SIZE (24)
typedef struct {
char magic[NVRAM_MAGIC_SIZE];
uint32_t index_off;
uint32_t data_off;
char pad[40];
} nvram_head_t; // 64 bytes
typedef struct {
char key[NVRAM_KEY_SIZE];
uint32_t off;
uint32_t size;
} nvram_index_t; // 32 bytes
#define NVRAM_DATA_SIZE (NVRAM_SIZE - sizeof(nvram_head_t) - sizeof(nvram_index_t) * NVRAM_N_DATA)
typedef struct {
nvram_head_t head;
nvram_index_t index[NVRAM_N_DATA];
char data[NVRAM_DATA_SIZE];
} nvram_t;
書き込みは、上記nvram_t
をメモリ(スタックに置くには大きすぎるのでヒープに)上に構築した後、一度に連続してEEPROMに書き込んでいきます。
読み出しは、nvram_index_t
をシーケンシャルに読み込んで、指定されたKeyと一致したらoff
とsize
を使用してデータ領域から値を確保します。
nvram-read
指定された引数から、nvram_open
→nvram_read
→nvram_close
するだけのほぼラッパーです。
nvram-write
指定された引数から、JSONファイルをパースし、nvram_t
を構築し、書き込んでいます。JSONファイルの読み出しにはnlohmann/jsonを使用しています。なので、nvram-writeだけC++です。
使い方
ビルド
普通のcmakeなので以下のようにビルドします。
mkdir build
cd build
cmake ..
make
これでbuild下にlibnvram
、nvram-read
、nvram-write
ディレクトリが作成され、その中に実行ファイルがビルドされます。以下のように実行できます。
LD_LIBRARY_PATH=<libnvramへのフルパス>:$LD_LIBRARY_PATH ./nvram-read <キー>
LD_LIBRARY_PATH=<libnvramへのフルパス>:$LD_LIBRARY_PATH ./nvram-write -c <jsonファイルへのパス>
Discussion