-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add Parameter Group system documentation for developers #11271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sensei-hacker
merged 1 commit into
iNavFlight:maintenance-9.x
from
sensei-hacker:docs/document-parameter-group-system
Feb 8, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| # Parameter Groups System | ||
|
|
||
| ## Overview | ||
|
|
||
| Parameter Groups (PG) is INAV's system for managing persistent configuration settings. Each configuration struct is registered with a unique ID and version number, stored in EEPROM/flash, and automatically validated on boot. | ||
|
|
||
| ## Quick Reference | ||
|
|
||
| **When modifying a parameter group struct:** | ||
| - ✅ **Increment version** - Safest option, always works | ||
| - ⚠️ **Don't increment** - Only safe for comments or non-structural changes | ||
|
|
||
| **Key files:** | ||
| - `src/main/config/parameter_group.h` - Macros and registry structure | ||
| - `src/main/config/parameter_group.c` - Runtime load/store functions | ||
| - `src/main/config/parameter_group_ids.h` - PGN definitions | ||
| - `src/main/fc/settings.yaml` - Maps CLI settings to PG fields | ||
|
|
||
| ## How It Works | ||
|
|
||
| ### Registration | ||
|
|
||
| Parameter groups use linker sections to build a compile-time registry: | ||
|
|
||
| ```c | ||
| // In yourfeature.h | ||
| typedef struct yourFeatureConfig_s { | ||
| uint8_t enabled; | ||
| uint16_t rate; | ||
| } yourFeatureConfig_t; | ||
|
|
||
| PG_DECLARE(yourFeatureConfig_t, yourFeatureConfig); | ||
|
|
||
| // In yourfeature.c | ||
| PG_REGISTER_WITH_RESET_TEMPLATE( | ||
| yourFeatureConfig_t, | ||
| yourFeatureConfig, | ||
| PG_YOUR_FEATURE_CONFIG, // Unique ID from parameter_group_ids.h | ||
| 0 // Version number | ||
| ); | ||
|
|
||
| PG_RESET_TEMPLATE(yourFeatureConfig_t, yourFeatureConfig, | ||
| .enabled = 1, | ||
| .rate = 100 | ||
| ); | ||
| ``` | ||
|
|
||
| ### Version Checking | ||
|
|
||
| On boot, `pgLoad()` validates versions before loading settings: | ||
|
|
||
| ```c | ||
| void pgLoad(const pgRegistry_t* reg, int profileIndex, | ||
| const void *from, int size, int version) | ||
| { | ||
| pgReset(reg, profileIndex); // Always reset to defaults first | ||
|
|
||
| if (version == pgVersion(reg)) { | ||
| // Only copy if versions match | ||
| const int take = MIN(size, pgSize(reg)); | ||
| memcpy(pgOffset(reg, profileIndex), from, take); | ||
| } | ||
| // If version mismatch: defaults remain, no corruption | ||
| } | ||
| ``` | ||
|
|
||
| **Key behavior:** Version mismatch causes settings to remain at defaults. This prevents corruption when struct layout changes. | ||
|
|
||
| ## Version Management | ||
|
|
||
| ### When to Increment Version | ||
|
|
||
| Version numbers are stored in the top 4 bits of the PGN field (valid range: 0-15). | ||
|
|
||
| **The safe rule:** When modifying a parameter group struct, increment the version. | ||
|
|
||
| **Changes requiring version increment:** | ||
| - Removing a field | ||
| - Changing field type or size | ||
| - Reordering fields | ||
| - Adding packing attributes | ||
|
|
||
| **Example - Removing a field:** | ||
|
|
||
| ```c | ||
| // Before: version 4 | ||
| typedef struct blackboxConfig_s { | ||
| uint16_t rate_num; | ||
| uint16_t rate_denom; | ||
| uint8_t device; | ||
| uint8_t invertedCardDetection; // Removing this | ||
| uint32_t includeFlags; | ||
| int8_t arm_control; | ||
| } blackboxConfig_t; | ||
| PG_REGISTER_WITH_RESET_TEMPLATE(blackboxConfig_t, blackboxConfig, PG_BLACKBOX_CONFIG, 4); | ||
|
|
||
| // After: version 5 (MUST increment!) | ||
| typedef struct blackboxConfig_s { | ||
| uint16_t rate_num; | ||
| uint16_t rate_denom; | ||
| uint8_t device; | ||
| // invertedCardDetection removed - field offsets changed! | ||
| uint32_t includeFlags; // Now at different offset | ||
| int8_t arm_control; // Now at different offset | ||
| } blackboxConfig_t; | ||
| PG_REGISTER_WITH_RESET_TEMPLATE(blackboxConfig_t, blackboxConfig, PG_BLACKBOX_CONFIG, 5); | ||
| ``` | ||
|
|
||
| **Why:** Without version increment, `pgLoad()` sees matching versions (4==4) and copies old data, but field offsets have changed. Result: `includeFlags` reads wrong bytes from EEPROM, causing corruption. | ||
|
|
||
| With version increment (4→5), `pgLoad()` detects mismatch and keeps defaults instead. | ||
|
|
||
| ### Changes NOT Requiring Version Increment | ||
|
|
||
| - Comments only | ||
| - Default values in `PG_RESET_TEMPLATE` (doesn't affect EEPROM layout) | ||
| - settings.yaml entries (CLI mapping, not struct layout) | ||
|
|
||
| ### Technical Note on Appending | ||
|
|
||
| The `pgLoad()` function uses `MIN(size, pgSize(reg))` when copying data. This means: | ||
| - If new firmware has larger struct, it copies available bytes and keeps defaults for new fields | ||
| - If new firmware has smaller struct, it copies only what fits | ||
|
|
||
| This behavior theoretically allows appending fields at the end without version increment. However, **the recommended practice is to always increment** unless you have specific reason not to and understand the implications. | ||
|
|
||
| ## Complete Example | ||
|
|
||
| ### yourfeature.h | ||
| ```c | ||
| #pragma once | ||
|
|
||
| #include <stdint.h> | ||
| #include "config/parameter_group.h" | ||
|
|
||
| typedef struct yourFeatureConfig_s { | ||
| uint8_t enabled; | ||
| uint16_t updateRate; | ||
| uint32_t flags; | ||
| } yourFeatureConfig_t; | ||
|
|
||
| PG_DECLARE(yourFeatureConfig_t, yourFeatureConfig); | ||
| ``` | ||
|
|
||
| ### yourfeature.c | ||
| ```c | ||
| #include "yourfeature.h" | ||
| #include "config/parameter_group_ids.h" | ||
|
|
||
| PG_REGISTER_WITH_RESET_TEMPLATE( | ||
| yourFeatureConfig_t, | ||
| yourFeatureConfig, | ||
| PG_YOUR_FEATURE_CONFIG, | ||
| 0 | ||
| ); | ||
|
|
||
| PG_RESET_TEMPLATE(yourFeatureConfig_t, yourFeatureConfig, | ||
| .enabled = 1, | ||
| .updateRate = 100, | ||
| .flags = 0 | ||
| ); | ||
|
|
||
| void yourFeatureInit(void) | ||
| { | ||
| if (!yourFeatureConfig()->enabled) { | ||
| return; | ||
| } | ||
| // Use configuration... | ||
| } | ||
| ``` | ||
|
|
||
| ### parameter_group_ids.h | ||
| ```c | ||
| typedef enum { | ||
| // ... existing entries ... | ||
| PG_YOUR_FEATURE_CONFIG = 150, // Choose unused ID | ||
| // ... | ||
| } pgn_e; | ||
| ``` | ||
|
|
||
| ### settings.yaml | ||
| ```yaml | ||
| - name: your_feature_enable | ||
| description: "Enable yourFeature" | ||
| field: yourFeatureConfig.enabled | ||
| type: uint8_t | ||
| min: 0 | ||
| max: 1 | ||
| default_value: 1 | ||
|
|
||
| - name: your_feature_rate | ||
| description: "Update rate in Hz" | ||
| field: yourFeatureConfig.updateRate | ||
| type: uint16_t | ||
| min: 1 | ||
| max: 1000 | ||
| default_value: 100 | ||
| ``` | ||
|
|
||
| ## Accessing Configuration | ||
|
|
||
| ### Read-only (preferred) | ||
| ```c | ||
| if (yourFeatureConfig()->enabled) { | ||
| processAtRate(yourFeatureConfig()->updateRate); | ||
| } | ||
| ``` | ||
|
|
||
| ### Mutable (use sparingly) | ||
| ```c | ||
| yourFeatureConfigMutable()->updateRate = newRate; | ||
| ``` | ||
|
|
||
| ## Registration Macros | ||
|
|
||
| | Macro | Use | | ||
| |-------|-----| | ||
| | `PG_DECLARE(type, name)` | Declare system PG in header | | ||
| | `PG_DECLARE_PROFILE(type, name)` | Declare profile PG in header | | ||
| | `PG_REGISTER_WITH_RESET_TEMPLATE(...)` | Register with template defaults | | ||
| | `PG_REGISTER_WITH_RESET_FN(...)` | Register with custom reset function | | ||
| | `PG_RESET_TEMPLATE(type, name, ...)` | Define default values | | ||
|
|
||
| ## Common Patterns | ||
|
|
||
| ### Boolean Fields | ||
| ```c | ||
| uint8_t enabled; // Use uint8_t, not bool (EEPROM needs fixed sizes) | ||
| ``` | ||
|
|
||
| ### Enums | ||
| ```c | ||
| typedef enum { MODE_A = 0, MODE_B = 1 } mode_e; | ||
| uint8_t mode; // Store enum as uint8_t | ||
| ``` | ||
|
|
||
| ### Bitfields | ||
| ```c | ||
| #define FLAG_A (1 << 0) | ||
| #define FLAG_B (1 << 1) | ||
| uint32_t flags; | ||
| ``` | ||
|
|
||
| ## Testing Version Changes | ||
|
|
||
| When incrementing a version: | ||
|
|
||
| 1. Flash old firmware, configure non-default settings, save | ||
| 2. Flash new firmware with incremented version | ||
| 3. Verify settings reset to defaults (not corrupted) | ||
| 4. Check for errors in CLI | ||
|
|
||
| ## See Also | ||
|
|
||
| - `src/main/config/parameter_group.h` - Full macro definitions | ||
| - `src/main/config/parameter_group.c` - Implementation | ||
| - Betaflight PG documentation: https://betaflight.com/docs/development/ParameterGroups | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Update the documentation snippet to show basic input validation (e.g.,
from != NULLandsize > 0) before callingmemcpy, since real callers may pass invalid pointers/sizes; keep a safe fallback of “defaults only” when validation fails. [Learned best practice, importance: 6]