⌨️

QMK で EEPROM 書き込みにdatablock関数を使う

に公開

はじめに

この記事は、キーボード #2 Advent Calendar 2025 の3日目の記事です。
2日目はサリチル酸さんの自作キーボードガチ勢がXREAL One Proを使って出張を攻略するでした。XREAL One Proずっと気になっています... XREAL AirとBeamは持っているのですが繋げるのが面倒で使わなくなったのですよね... 寝ながら進捗の記事も楽しみですね!

キーボード #1 Advent Calendar 2025 の方の2日目はぷるぷる@家さんのQMKのRaw HIDでPCからキーボードを操作するでかなり中身の濃い記事でしたが、その中でさらっと書かれていた「パラメータをキーボードのEEPROM (フラッシュメモリ)に書き込む」方法について、本記事で紹介したいと思います。

QMKドキュメントに書かれている方法

QMKドキュメントには、EEPROMにパラメータを読み書きする方法として、以下の関数が紹介されています。

用途 kb user
初期化 eeconfig_init_kb() eeconfig_init_user()
読み込み eeconfig_read_kb() eeconfig_read_user()
書き込み eeconfig_write_kb() eeconfig_write_user()

注意点として、これらの関数はuint32_t = 4 bytes までしか扱えません。

quantum/eeconfig.h
uint32_t eeconfig_read_user(void);
void     eeconfig_update_user(uint32_t val);

4 bytes以上のデータを扱うには、datablock関数を使用します。

datablock

有効化

datablockの関数は以下で宣言されています。

quantum/eeconfig.h
#if (EECONFIG_USER_DATA_SIZE) > 0
bool     eeconfig_is_user_datablock_valid(void);
uint32_t eeconfig_read_user_datablock(void *data, uint32_t offset, uint32_t length) __attribute__((nonnull));
uint32_t eeconfig_update_user_datablock(const void *data, uint32_t offset, uint32_t length) __attribute__((nonnull));
void     eeconfig_init_user_datablock(void);
#    define eeconfig_read_user_datablock_field(__object, __field) eeconfig_read_user_datablock(&(__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field))
#    define eeconfig_update_user_datablock_field(__object, __field) eeconfig_update_user_datablock(&(__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field))
#endif // (EECONFIG_USER_DATA_SIZE) > 0

上から

  • check関数
  • 読み込み関数
  • 書き込み関数
  • 初期化関数
  • 読み込みfield指定マクロ
  • 書き込みfield指定マクロ

ですね。
EECONFIG_USER_DATA_SIZE > 0の時に関数が有効になっているので、config.hEECONFIG_USER_DATA_SIZEで必要なbyte数を定義します。尚、EECONFIG_USER_DATA_SIZE > 0の場合、eeconfig_read_user(), eeconfig_update_user()は無効になります。

config.h
// RGBLightのHSVとModeをまとめた構造体
typedef struct {
    hsv_t hsv; // uint8_t * 3
    uint8_t mode;
} g45_hsvm_t;

// EEPROMに書き込む構造体
// Layer数分のRGBLight、各種フラグ、OS毎のdefault layerの設定を持つ
typedef struct {
    g45_hsvm_t hsvm_layer[DYNAMIC_KEYMAP_LAYER_COUNT];
    union {
        uint8_t flag_raw; // eeprom読み書き用
        struct {
            bool is_rgb_per_layer : 1;
            bool is_auto_save_rgb : 1;
            bool to_retain_val : 1;
            uint8_t dummy : 5;
        } flags;
    };
    uint8_t os_default_layer[OS_IOS + 1]; // OS_IOS は enum os_variant_t の最後のenum値
} g45_user_config_t;

// EECONFIG_USER_DATA_SIZE >= sizeof(g45_user_config_t) であること!
#define EECONFIG_USER_DATA_SIZE (4 * DYNAMIC_KEYMAP_LAYER_COUNT + 1 + OS_IOS + 1)

初期化

Firmware初回インストール時に、void eeconfig_init_user_datablock(void)が呼ばれるので、override関数で構造体を初期化して、EEPROMに書き込みます。

keymap.c
g45_user_config_t g45_user_config;

void eeconfig_init_user_datablock(void) {
    // 構造体を初期化. 詳細は割愛
    for (uint8_t i = 0; i < ARRAY_SIZE(g45_user_config.hsvm_layer); i++) {
        g45_user_config.hsvm_layer[i].hsv.h = ...;
    }
    ...

    // user datablock領域の offset 0 に g45_user_config全体を書き込み
    eeconfig_update_user_datablock(&g45_user_config, 0, sizeof(g45_user_config));
}

読み込み

keyboard接続時に呼ばれるkeyboard_post_init_user()でEEPROMから構造体にデータを読み込みます。

keymap.c
void keyboard_post_init_user(void) {
    // invalidの場合は初期化するようにします。後述参照。
    if (!eeconfig_is_user_datablock_valid()) {
        eeconfig_init_user_datablock();
    }

    // user datablock領域の offset 0 から g45_user_configに読み込み
    // EECONFIG_USER_DATA_SIZE は構造体と同じか、構造体より大きいサイズであること!
    eeconfig_read_user_datablock(&g45_user_config, 0, sizeof(g45_user_config));
}

keyboard操作中は、基本的には構造体のデータを使い、EEPROMから読み込む必要は無いと思うので他使用方法は省略します。使いたい場合は下記の書き込みをreadに置き換えて参照してください。

書き込み

keyboard操作でパラメータを更新する時に、更新されたパラメータをeepromに書き込みます。
書き込みは3つの方法があります (省略しましたがもちろん読み込みもです)。

A. 構造体全てを書き込み
B. 更新されたパラメータのみをoffset指定で書き込み
C. 更新されたパラメータのみを構造体のfield指定で書き込み

Aは初期化と同じくeeconfig_update_user_datablock(&g45_user_config, 0, sizeof(g45_user_config));を呼ぶだけです。

B. 更新されたパラメータのみをoffset指定で書き込み

datablock領域の書き込み位置(offset)、データ、データサイズを指定して書き込みます。

keymap.c
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch(keycode) {
        case USR_RGB_LAYER_TOG:
            if (rgblight_is_enabled() && record->event.pressed) {
                // 構造体のデータをlocal変数に持ってきて
                const bool cur_flag = g45_user_config.flags.is_rgb_per_layer;

                // 反転して構造体に戻す
                g45_user_config.flags.is_rgb_per_layer = !cur_flag;

                // offsetを計算
                uint32_t offset = sizeof(g45_hsvm_t) * ARRAY_SIZE(g45_user_config.hsvm_layer);

                // user datablock領域の offset位置 に 構造体のflag_rawを書き込み
                eeconfig_update_user_datablock(&g45_user_config.flag_raw, offset, sizeof(uint8_t));
            }
            return false;
...
    };
}

C. 更新されたパラメータのみを構造体のfield指定で書き込み

構造体のどのfieldを書き込むか指定します。

// B の eeconfig_update_user_datablock() を
// eeconfig_update_user_datablock_field() に置き換えるだけなので、前後は省略

...
                // 構造体のflags_rawを書き込み
                eeconfig_update_user_datablock_field(g45_user_config, flags_raw);
...

offsetの計算はミスしやすいのでCをお勧めしますが、配列で一部データだけを書き込みしたい場合はBになると思います。

フラッシュメモリの書き込みは約10万回と回数が限定されています。そんな回数に到達する可能性はかなり低いと思いますが、EEPROMへの書き込み回数はなるべく減らすべきです。そのため、keyboard操作でパラメータを更新する時は、値が変わる時だけ構造体のデータを更新してから、BまたはCの手段で更新されたパラメータだけをEEPROMに書き込み、値が変わらない時は書き込まないようにするのをお勧めします。また、連続してパラメータを更新し続けるような場合は、Timerを使うとか、他キーを入力した時にEEPROMに書き込むなどの手段も考えられます。

Version / Data Size Check

冒頭に記述した通り、datablockを使用していない場合、dataはuint32_t = 4 bytes までしか使えません。この情報は quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h のeeprom_core_t.user に保存され、eeprom_read_dword(EECONFIG_USER) で取得できます。

datablockを使用している場合、EECONFIG_USER_DATA_VERSIONが定義されていなければ、EECONFIG_USER_DATA_SIZEEECONFIG_USER_DATA_VERSIONに代入されます。

quantum/eeconfig.h
#ifndef EECONFIG_USER_DATA_VERSION
#    define EECONFIG_USER_DATA_VERSION (EECONFIG_USER_DATA_SIZE)
#endif

EECONFIG_USER_DATA_VERSIONはdatablockを初期化または更新するタイミングでeeprom_core_t.user (EECONFIG_USER) に保存されます。

quantum/nvm/eeprom/nvm_eeconfig.c
// eeconfig_update_user_datablock()からnvm_...が呼ばれる。以下同様。
uint32_t nvm_eeconfig_update_user_datablock(const void *data, uint32_t offset, uint32_t length) {
    eeprom_update_dword(EECONFIG_USER, (EECONFIG_USER_DATA_VERSION));
...
}

void nvm_eeconfig_init_user_datablock(void) {
    eeprom_update_dword(EECONFIG_USER, (EECONFIG_USER_DATA_VERSION));
...
}

datablockのcheck関数bool eeconfig_is_user_datablock_valid(void)EECONFIG_USEREECONFIG_USER_DATA_VERSIONが一致しているかどうかをチェックしています。

quantum/nvm/eeprom/nvm_eeconfig.c
bool nvm_eeconfig_is_user_datablock_valid(void) {
    return eeprom_read_dword(EECONFIG_USER) == (EECONFIG_USER_DATA_VERSION);
}

eeconfig_init_user_datablock()はFirmware初回インストール時だけ呼ばれて、更新インストール時は呼ばれません。そのため更新インストール直後はEECONFIG_USERは更新インストール前の値になります。EECONFIG_USER_DATA_VERSIONを定義していない場合、構造体のサイズがEECONFIG_USERに入っているので、構造体のサイズを変更してFirmwareを更新インストールすると、invalidになるというわけです。

尚、eeconfig_read_user_datablock()は、invalidの場合はデータを0に初期化し、datablockの情報を取得できません。
読み込み時にinvalidの時に初期化関数を呼ぶようにしていたのは、これが理由です。

quantum/nvm/eeprom/nvm_eeconfig.c
uint32_t nvm_eeconfig_read_user_datablock(void *data, uint32_t offset, uint32_t length) {
    if (eeconfig_is_user_datablock_valid()) {
...
    } else {
        // invalidの場合はデータを初期化
        memset(data, 0, length);
        return length;
    }
}

まとめ

QMKでEEPROMに書き込む方法としてdatablock関数を使う方法の紹介をしました。
QMKはドキュメント化されていない機能・関数が多々あるので、どなたかの参考になれば幸いです。

この記事はgravity45...ではなく、cocot38miniで書きました。いや、丁度cocotのファームを弄っている合間に書いたので...

Discussion