Pārlūkot izejas kodu

Enable modules to persist data (#26201)

Joel Challis 1 mēnesi atpakaļ
vecāks
revīzija
8522bb342d

+ 8 - 8
builddefs/build_keyboard.mk

@@ -187,9 +187,9 @@ include $(COMMUNITY_RULES_MK)
 
 ifneq ($(COMMUNITY_MODULES),)
 
-$(INTERMEDIATE_OUTPUT)/src/community_config.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
+$(INTERMEDIATE_OUTPUT)/src/community_post_config.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
 	@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
-	$(eval CMD=$(QMK_BIN) generate-community-config-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_config.h $(KEYMAP_JSON))
+	$(eval CMD=$(QMK_BIN) generate-community-post-config-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_post_config.h $(KEYMAP_JSON))
 	@$(BUILD_CMD)
 
 $(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@@ -227,10 +227,10 @@ $(INTERMEDIATE_OUTPUT)/src/split_transaction_id_community_modules.inc: $(KEYMAP_
 	$(eval CMD=$(QMK_BIN) generate-split-transaction-id-community-modules-inc -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/split_transaction_id_community_modules.inc $(KEYMAP_JSON))
 	@$(BUILD_CMD)
 
-COMMUNITY_CONFIG_H = $(INTERMEDIATE_OUTPUT)/src/community_config.h
+COMMUNITY_POST_CONFIG_H = $(INTERMEDIATE_OUTPUT)/src/community_post_config.h
 SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c
 
-generated-files: $(INTERMEDIATE_OUTPUT)/src/community_config.h $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h $(INTERMEDIATE_OUTPUT)/src/led_matrix_community_modules.inc $(INTERMEDIATE_OUTPUT)/src/rgb_matrix_community_modules.inc $(INTERMEDIATE_OUTPUT)/src/split_transaction_id_community_modules.inc
+generated-files: $(INTERMEDIATE_OUTPUT)/src/community_post_config.h $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h $(INTERMEDIATE_OUTPUT)/src/led_matrix_community_modules.inc $(INTERMEDIATE_OUTPUT)/src/rgb_matrix_community_modules.inc $(INTERMEDIATE_OUTPUT)/src/split_transaction_id_community_modules.inc
 
 endif
 
@@ -331,10 +331,6 @@ define config_h_community_module_appender
 endef
 $(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call config_h_community_module_appender,$(module))))
 
-ifneq ($(COMMUNITY_CONFIG_H),)
-    CONFIG_H += $(COMMUNITY_CONFIG_H)
-endif
-
 ifneq ("$(wildcard $(KEYBOARD_PATH_5)/config.h)","")
     CONFIG_H += $(KEYBOARD_PATH_5)/config.h
 endif
@@ -376,6 +372,10 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_config.h)","")
     POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h
 endif
 
+ifneq ($(COMMUNITY_POST_CONFIG_H),)
+    POST_CONFIG_H += $(COMMUNITY_POST_CONFIG_H)
+endif
+
 CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h
 KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c
 

+ 3 - 0
data/constants/module_hooks/1.1.3.hjson

@@ -0,0 +1,3 @@
+{
+    // This version exists to signify addition of eeconfig support.
+}

+ 33 - 17
docs/features/community_modules.md

@@ -135,28 +135,45 @@ This file defines LED matrix effects in the same form as used with `led_matrix_k
 
 This file defines RGB matrix effects in the same form as used with `rgb_matrix_kb.inc` and `rgb_matrix_user.inc` (see [Custom RGB Matrix Effects](rgb_matrix#custom-rgb-matrix-effects)). Effect mode names are prepended with `RGB_MATRIX_COMMUNITY_MODULE_`.
 
-### Custom split keyboard data sync
+### Additional Customization
 
-Defines follow the convention, `SPLIT_TRANSACTION_IDS_MODULE_<MODULE>` (see [Custom data sync](split_keyboard#custom-data-sync)). 
+#### Split Keyboard Data Sync
+
+Defines follow the convention, `SPLIT_TRANSACTION_IDS_MODULE_<MODULE>` (see [Custom data sync](split_keyboard#custom-data-sync)).
+
+#### Persistent Configuration
+
+Defines follow the convention, `EECONFIG_MODULE_<MODULE>_DATA_SIZE` and `EECONFIG_MODULE_<MODULE>_DATA_VERSION` (see [Custom Persistent Configuration](../feature_eeprom#datablock)).
+
+When configured, the following APIs are available:
+
+| API Format                                 | Example (`hello_world` module)                | API Version |
+|--------------------------------------------|-----------------------------------------------|-------------|
+| `eeconfig_is_<module>_datablock_valid`     | `eeconfig_is_hello_world_datablock_valid`     | `1.1.3`     |
+| `eeconfig_read_<module>_datablock`         | `eeconfig_read_hello_world_datablock`         | `1.1.3`     |
+| `eeconfig_update_<module>_datablock`       | `eeconfig_update_hello_world_datablock`       | `1.1.3`     |
+| `eeconfig_init_<module>_datablock`         | `eeconfig_init_hello_world_datablock`         | `1.1.3`     |
+| `eeconfig_read_<module>_datablock_field`   | `eeconfig_read_hello_world_datablock_field`   | `1.1.3`     |
+| `eeconfig_update_<module>_datablock_field` | `eeconfig_update_hello_world_datablock_field` | `1.1.3`     |
 
 ### Compatible APIs
 
 Community Modules may provide specializations for the following APIs:
 
-| Base API                         | API Format                                | Example (`hello_world` module)              | API Version |
-|----------------------------------|-------------------------------------------|---------------------------------------------|-------------|
-| `keyboard_pre_init`              | `keyboard_pre_init_<module>`              | `keyboard_pre_init_hello_world`             | `0.1.0`     |
-| `keyboard_post_init`             | `keyboard_post_init_<module>`             | `keyboard_post_init_hello_world`            | `0.1.0`     |
-| `pre_process_record`             | `pre_process_record_<module>`             | `pre_process_record_hello_world`            | `0.1.0`     |
-| `process_record`                 | `process_record_<module>`                 | `process_record_hello_world`                | `0.1.0`     |
-| `post_process_record`            | `post_process_record_<module>`            | `post_process_record_hello_world`           | `0.1.0`     |
-| `housekeeping_task`              | `housekeeping_task_<module>`              | `housekeeping_task_hello_world`             | `1.0.0`     |
-| `suspend_power_down`             | `suspend_power_down_<module>`             | `suspend_power_down_hello_world`            | `1.0.0`     |
-| `suspend_wakeup_init`            | `suspend_wakeup_init_<module>`            | `suspend_wakeup_init_hello_world`           | `1.0.0`     |
-| `shutdown`                       | `shutdown_<module>`                       | `shutdown_hello_world`                      | `1.0.0`     |
-| `process_detected_host_os`       | `process_detected_host_os_<module>`       | `process_detected_host_os_hello_world`      | `1.0.0`     |
-| `default_layer_state_set`        | `default_layer_state_set_<module>`        | `default_layer_state_set_hello_world`       | `1.1.0`     |
-| `layer_state_set`                | `layer_state_set_<module>`                | `layer_state_set_hello_world`               | `1.1.0`     |
+| Base API                         | API Format                                | Example (`hello_world` module)               | API Version |
+|----------------------------------|-------------------------------------------|----------------------------------------------|-------------|
+| `keyboard_pre_init`              | `keyboard_pre_init_<module>`              | `keyboard_pre_init_hello_world`              | `0.1.0`     |
+| `keyboard_post_init`             | `keyboard_post_init_<module>`             | `keyboard_post_init_hello_world`             | `0.1.0`     |
+| `pre_process_record`             | `pre_process_record_<module>`             | `pre_process_record_hello_world`             | `0.1.0`     |
+| `process_record`                 | `process_record_<module>`                 | `process_record_hello_world`                 | `0.1.0`     |
+| `post_process_record`            | `post_process_record_<module>`            | `post_process_record_hello_world`            | `0.1.0`     |
+| `housekeeping_task`              | `housekeeping_task_<module>`              | `housekeeping_task_hello_world`              | `1.0.0`     |
+| `suspend_power_down`             | `suspend_power_down_<module>`             | `suspend_power_down_hello_world`             | `1.0.0`     |
+| `suspend_wakeup_init`            | `suspend_wakeup_init_<module>`            | `suspend_wakeup_init_hello_world`            | `1.0.0`     |
+| `shutdown`                       | `shutdown_<module>`                       | `shutdown_hello_world`                       | `1.0.0`     |
+| `process_detected_host_os`       | `process_detected_host_os_<module>`       | `process_detected_host_os_hello_world`       | `1.0.0`     |
+| `default_layer_state_set`        | `default_layer_state_set_<module>`        | `default_layer_state_set_hello_world`        | `1.1.0`     |
+| `layer_state_set`                | `layer_state_set_<module>`                | `layer_state_set_hello_world`                | `1.1.0`     |
 | `led_matrix_indicators`          | `led_matrix_indicators_<module>`          | `led_matrix_indicators_hello_world`          | `1.1.0`     |
 | `led_matrix_indicators_advanced` | `led_matrix_indicators_advanced_<module>` | `led_matrix_indicators_advanced_hello_world` | `1.1.0`     |
 | `rgb_matrix_indicators`          | `rgb_matrix_indicators_<module>`          | `rgb_matrix_indicators_hello_world`          | `1.1.0`     |
@@ -164,7 +181,6 @@ Community Modules may provide specializations for the following APIs:
 | `pointing_device_init`           | `pointing_device_init_<module>`           | `pointing_device_init_hello_world`           | `1.1.0`     |
 | `pointing_device_task`           | `pointing_device_task_<module>`           | `pointing_device_task_hello_world`           | `1.1.0`     |
 
-
 ::: info
 An unspecified API is disregarded if a Community Module does not provide a specialization for it.
 :::

+ 13 - 0
keyboards/handwired/onekey/keymaps/nvm_test/keymap.c

@@ -0,0 +1,13 @@
+// Copyright 2026 QMK
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include QMK_KEYBOARD_H
+
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {LAYOUT_ortho_1x1(KC_A)};
+
+void keyboard_post_init_user(void) {
+    // Customise these values to desired behaviour
+    debug_enable = true;
+    debug_matrix = true;
+    // debug_keyboard=true;
+    // debug_mouse=true;
+}

+ 10 - 0
keyboards/handwired/onekey/keymaps/nvm_test/keymap.json

@@ -0,0 +1,10 @@
+{
+    "config": {
+        "features": {
+            "console": true
+        }
+    },
+    "modules": [
+        "qmk/nvm_test"
+    ]
+}

+ 159 - 6
lib/python/qmk/cli/generate/community_modules.py

@@ -169,6 +169,139 @@ def _generate_modules_rules(keyboard, filename):
     return lines
 
 
+def _module_slugs(modules):
+    return [Path(m).name.lower() for m in modules]
+
+
+def _render_eeconfig_declarations(modules):
+    lines = []
+    lines.append('')
+    lines.append('// nvm eeconfig')
+    for module_slug in _module_slugs(modules):
+        lines.extend([
+            f'#if (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            f'bool     eeconfig_is_{module_slug}_datablock_valid(void);',
+            f'uint32_t eeconfig_read_{module_slug}_datablock(void *data, uint32_t offset, uint32_t length) __attribute__((nonnull));',
+            f'uint32_t eeconfig_update_{module_slug}_datablock(const void *data, uint32_t offset, uint32_t length) __attribute__((nonnull));',
+            f'void     eeconfig_init_{module_slug}_datablock(void);',
+            f'#    define eeconfig_read_{module_slug}_datablock_field(__object, __field) eeconfig_read_{module_slug}_datablock(&(__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field))',
+            f'#    define eeconfig_update_{module_slug}_datablock_field(__object, __field) eeconfig_update_{module_slug}_datablock(&(__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field))',
+            '',
+            f'bool     nvm_eeconfig_is_{module_slug}_datablock_valid(void);',
+            f'uint32_t nvm_eeconfig_read_{module_slug}_datablock(void *data, uint32_t offset, uint32_t length);',
+            f'uint32_t nvm_eeconfig_update_{module_slug}_datablock(const void *data, uint32_t offset, uint32_t length);',
+            f'void     nvm_eeconfig_init_{module_slug}_datablock(void);',
+            f'#endif // (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            '',
+        ])
+
+    lines.append('typedef struct PACKED {')
+    for module_slug in _module_slugs(modules):
+        lines.extend([
+            f'#if (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            f'    uint32_t {module_slug}_version;',
+            f'    uint8_t  {module_slug}[EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE];',
+            f'#endif // (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+        ])
+    lines.append('} eeprom_modules_t;')
+    lines.append('')
+
+    for module_slug in _module_slugs(modules):
+        lines.append(f'#define EECONFIG_MODULE_{module_slug.upper()}_VERSION (uint32_t *)(EECONFIG_MODULES_DATABLOCK + (offsetof(eeprom_modules_t, {module_slug}_version)))')
+        lines.append(f'#define EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK (uint8_t *)(EECONFIG_MODULES_DATABLOCK + (offsetof(eeprom_modules_t, {module_slug})))')
+    lines.append('')
+
+    lines.append('bool eeconfig_is_modules_datablock_valid(void);')
+    lines.append('void eeconfig_init_modules_datablock(void);')
+    lines.append('')
+
+    return lines
+
+
+def _render_eeconfig_implementation(modules):
+    lines = []
+
+    lines.append('')
+    lines.append('// nvm eeconfig')
+    lines.append('#if defined(NVM_DRIVER_EEPROM)'),
+    lines.append('#    include "nvm_eeprom_eeconfig_internal.h"')
+    lines.append('#    include "eeprom.h"')
+    lines.append('#endif // defined(NVM_DRIVER_EEPROM)'),
+    lines.append('')
+
+    for module_slug in _module_slugs(modules):
+        lines.extend([
+            f'#if (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            f'bool eeconfig_is_{module_slug}_datablock_valid(void) {{ return nvm_eeconfig_is_{module_slug}_datablock_valid(); }}',
+            f'uint32_t eeconfig_read_{module_slug}_datablock(void *data, uint32_t offset, uint32_t length) {{ return nvm_eeconfig_read_{module_slug}_datablock(data, offset, length); }}',
+            f'uint32_t eeconfig_update_{module_slug}_datablock(const void *data, uint32_t offset, uint32_t length) {{ return nvm_eeconfig_update_{module_slug}_datablock(data, offset, length); }}',
+            f'void eeconfig_init_{module_slug}_datablock(void) {{ nvm_eeconfig_init_{module_slug}_datablock(); }}',
+            '',
+            '#    if defined(NVM_DRIVER_EEPROM)',
+            f'bool nvm_eeconfig_is_{module_slug}_datablock_valid(void) {{',
+            f'    return eeprom_read_dword(EECONFIG_MODULE_{module_slug.upper()}_VERSION) == (EECONFIG_MODULE_{module_slug.upper()}_DATA_VERSION);',
+            '}',
+            f'uint32_t nvm_eeconfig_read_{module_slug}_datablock(void *data, uint32_t offset, uint32_t length) {{',
+            f'    if (eeconfig_is_{module_slug}_datablock_valid()) {{',
+            f'        void *ee_start = (void *)(uintptr_t)(EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK + offset);',
+            f'        void *ee_end   = (void *)(uintptr_t)(EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK + MIN((EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE), offset + length));',
+            '        eeprom_read_block(data, ee_start, ee_end - ee_start);',
+            '        return ee_end - ee_start;',
+            '    } else {',
+            '        memset(data, 0, length);',
+            '        return length;',
+            '    }',
+            '}',
+            f'uint32_t nvm_eeconfig_update_{module_slug}_datablock(const void *data, uint32_t offset, uint32_t length) {{',
+            f'    eeprom_update_dword(EECONFIG_MODULE_{module_slug.upper()}_VERSION, (EECONFIG_MODULE_{module_slug.upper()}_DATA_VERSION));',
+            f'    void *ee_start = (void *)(uintptr_t)(EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK + offset);',
+            f'    void *ee_end   = (void *)(uintptr_t)(EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK + MIN((EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE), offset + length));',
+            '    eeprom_update_block(data, ee_start, ee_end - ee_start);',
+            '    return ee_end - ee_start;',
+            '}',
+            f'void nvm_eeconfig_init_{module_slug}_datablock(void) {{',
+            f'    eeprom_update_dword(EECONFIG_MODULE_{module_slug.upper()}_VERSION, (EECONFIG_MODULE_{module_slug.upper()}_DATA_VERSION));',
+            f'    void   *start     = (void *)(uintptr_t)(EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK);',
+            f'    void   *end       = (void *)(uintptr_t)(EECONFIG_MODULE_{module_slug.upper()}_DATABLOCK + (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE));',
+            '    long    remaining = end - start;',
+            '    uint8_t dummy[16] = {0};',
+            f'    for (int i = 0; i < EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE; i += sizeof(dummy)) {{',
+            '        int this_loop = remaining < sizeof(dummy) ? remaining : sizeof(dummy);',
+            '        eeprom_update_block(dummy, start, this_loop);',
+            '        start += this_loop;',
+            '        remaining -= this_loop;',
+            '    }',
+            '}',
+            '#    endif // defined(NVM_DRIVER_EEPROM)',
+            f'#endif // (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            '',
+        ])
+
+    lines.append('bool eeconfig_is_modules_datablock_valid(void) {')
+    lines.append('    return true')
+    for module_slug in _module_slugs(modules):
+        lines.extend([
+            f'#if (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            f'        && eeconfig_is_{module_slug}_datablock_valid()',
+            f'#endif // (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+        ])
+    lines.append('    ;')
+    lines.append('}')
+    lines.append('')
+
+    lines.append('void eeconfig_init_modules_datablock(void) {'),
+    for module_slug in _module_slugs(modules):
+        lines.extend([
+            f'#if (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+            f'    eeconfig_init_{module_slug}_datablock();',
+            f'#endif // (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE) > 0',
+        ])
+    lines.append('}')
+    lines.append('')
+
+    return lines
+
+
 @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
 @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
 @cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode")
@@ -196,11 +329,11 @@ def generate_community_modules_rules_mk(cli):
 
 @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
 @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
-@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_config.h for.')
+@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_post_config.h for.')
 @cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
-@cli.subcommand('Creates a community_config.h from a keymap.json file.')
-def generate_community_config_h(cli):
-    """Creates a community_config.h from a keymap.json file
+@cli.subcommand('Creates a community_post_config.h from a keymap.json file.')
+def generate_community_post_config_h(cli):
+    """Creates a community_post_config.h from a keymap.json file
     """
     if cli.args.output and cli.args.output.name == '-':
         cli.args.output = None
@@ -215,14 +348,29 @@ def generate_community_config_h(cli):
     modules = get_modules(cli.args.keyboard, cli.args.filename)
     if len(modules) > 0:
         lines.append('// Split transactions')
-        for module in modules:
+        for module_slug in _module_slugs(modules):
             lines.extend([
-                f'#ifdef SPLIT_TRANSACTION_IDS_MODULE_{Path(module).name.upper()}',
+                f'#ifdef SPLIT_TRANSACTION_IDS_MODULE_{module_slug.upper()}',
                 '#    define SPLIT_TRANSACTION_RPC',
                 '#endif',
             ])
         lines.append('')
 
+        lines.append('// nvm eeconfig')
+        for module_slug in _module_slugs(modules):
+            lines.extend([
+                f'#ifndef EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE',
+                f'#    define EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE 0',
+                '#endif',
+                f'#ifndef EECONFIG_MODULE_{module_slug.upper()}_DATA_VERSION',
+                f'#    define EECONFIG_MODULE_{module_slug.upper()}_DATA_VERSION (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE)',
+                '#endif',
+                '',
+            ])
+        module_size = " + ".join([f'(4 + (EECONFIG_MODULE_{module_slug.upper()}_DATA_SIZE))' for module_slug in _module_slugs(modules)])
+        lines.append(f'#define EECONFIG_MODULE_DATA_SIZE ({module_size})')
+        lines.append('')
+
     dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
 
 
@@ -245,6 +393,7 @@ def generate_community_modules_h(cli):
         '#pragma once',
         '#include <stdint.h>',
         '#include <stdbool.h>',
+        '#include <string.h>',
         '#include <keycodes.h>',
         '',
         '#include "compiler_support.h"',
@@ -272,6 +421,8 @@ def generate_community_modules_h(cli):
                 lines.extend(_render_api_declarations(api, Path(module).name))
         lines.append('')
 
+        lines.extend(_render_eeconfig_declarations(modules))
+
         lines.append('// Core wrapper')
         for api in api_list:
             lines.extend(_render_api_declarations(api, 'modules', user_kb=False))
@@ -309,6 +460,8 @@ def generate_community_modules_c(cli):
         for api in api_list:
             lines.extend(_render_core_implementation(api, modules))
 
+        lines.extend(_render_eeconfig_implementation(modules))
+
     dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
 
 

+ 5 - 0
modules/qmk/nvm_test/config.h

@@ -0,0 +1,5 @@
+// Copyright 2026 QMK
+// SPDX-License-Identifier: GPL-2.0-or-later
+#pragma once
+
+#define EECONFIG_MODULE_NVM_TEST_DATA_SIZE 4

+ 53 - 0
modules/qmk/nvm_test/nvm_test.c

@@ -0,0 +1,53 @@
+// Copyright 2026 QMK
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <stdint.h>
+#include "community_modules.h"
+#include "debug.h"
+#include "timer.h"
+#include "eeconfig.h"
+
+ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 1, 3);
+
+typedef struct my_config_t {
+    uint32_t data;
+} my_config_t;
+
+static my_config_t config;
+
+// Helpers required to bind to debounce helper
+void eeconfig_read_my_config(my_config_t *value) {
+    eeconfig_read_nvm_test_datablock(value, 0, sizeof(my_config_t));
+}
+void eeconfig_update_my_config(my_config_t *value) {
+    eeconfig_update_nvm_test_datablock(value, 0, sizeof(my_config_t));
+}
+
+EECONFIG_DEBOUNCE_HELPER(my_config, config);
+
+void keyboard_post_init_nvm_test(void) {
+    if (!eeconfig_is_nvm_test_datablock_valid()) {
+        eeconfig_init_nvm_test_datablock();
+    }
+
+    eeconfig_init_my_config();
+}
+
+bool process_record_nvm_test(uint16_t keycode, keyrecord_t *record) {
+    if (!record->event.pressed) {
+        config.data += 1;
+        eeconfig_flag_my_config(true);
+    }
+
+    return true;
+}
+
+void housekeeping_task_nvm_test(void) {
+    eeconfig_flush_my_config(false);
+
+    static uint32_t last_sync = 0;
+    if (timer_elapsed32(last_sync) > 1000) {
+        last_sync = timer_read32();
+
+        dprintf("Config: %ld\n", config.data);
+    }
+}

+ 5 - 0
modules/qmk/nvm_test/qmk_module.json

@@ -0,0 +1,5 @@
+{
+    "module_name": "Example nvm persistence",
+    "maintainer": "QMK Maintainers",
+    "license": "GPL-2.0-or-later"
+}

+ 8 - 0
quantum/eeconfig.c

@@ -39,6 +39,10 @@
 #    include "connection.h"
 #endif // CONNECTION_ENABLE
 
+#ifdef COMMUNITY_MODULES_ENABLE
+#    include "community_modules.h"
+#endif
+
 #ifdef VIA_ENABLE
 bool via_eeprom_is_valid(void);
 void via_eeprom_set_valid(bool valid);
@@ -148,6 +152,10 @@ void eeconfig_init_quantum(void) {
     eeconfig_init_user_datablock();
 #endif // (EECONFIG_USER_DATA_SIZE) > 0
 
+#ifdef COMMUNITY_MODULES_ENABLE
+    eeconfig_init_modules_datablock();
+#endif // COMMUNITY_MODULES_ENABLE
+
 #if defined(VIA_ENABLE)
     // Invalidate VIA eeprom config, and then reset.
     // Just in case if power is lost mid init, this makes sure that it gets

+ 7 - 1
quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h

@@ -56,8 +56,14 @@ typedef struct PACKED {
 
 #define EECONFIG_KB_DATABLOCK ((uint8_t *)(EECONFIG_BASE_SIZE))
 #define EECONFIG_USER_DATABLOCK ((uint8_t *)((EECONFIG_BASE_SIZE) + (EECONFIG_KB_DATA_SIZE)))
+#define EECONFIG_MODULES_DATABLOCK ((uint8_t *)((EECONFIG_BASE_SIZE) + (EECONFIG_KB_DATA_SIZE) + (EECONFIG_USER_DATA_SIZE)))
+
+// Actual value is injected via the generated community_post_config.h
+#ifndef EECONFIG_MODULE_DATA_SIZE
+#    define EECONFIG_MODULE_DATA_SIZE 0
+#endif
 
 // Size of EEPROM being used, other code can refer to this for available EEPROM
-#define EECONFIG_SIZE ((EECONFIG_BASE_SIZE) + (EECONFIG_KB_DATA_SIZE) + (EECONFIG_USER_DATA_SIZE))
+#define EECONFIG_SIZE ((EECONFIG_BASE_SIZE) + (EECONFIG_KB_DATA_SIZE) + (EECONFIG_USER_DATA_SIZE) + (EECONFIG_MODULE_DATA_SIZE))
 
 STATIC_ASSERT((intptr_t)EECONFIG_HANDEDNESS == 14, "EEPROM handedness offset is incorrect");