fix(mcu): MCU-A2 — site-configurable mag declination, persisted in BKPSRAM

The magnetometer yaw correction used a hardcoded -0.61 deg literal baked
in for one deployment site. Yaw_Sensor was wrong by (site_decl + 0.61)
deg at every other site whenever the UM982 dual-antenna heading was
unavailable.

Backed the value with BKPSRAM (slots 1+2 — slot 0 is the MCU-A7
emergency flag) and exposed set_mag_declination_deg / get_mag_declination_deg.
Default returns the legacy -0.61 deg when no override has been written so
the original site stays correct out of the box; a host command (or
future GPS-derived auto-calibration) writes the new site value once and
it persists across every reset path until main-power removal.

Hardened with a +/-30 deg range clamp on both write AND read paths — real
magnetic declinations are roughly +/-25 deg worldwide, so a wider value
indicates a calibration error or BKPSRAM corruption (VBAT brown-out, bit
flip) rather than a legitimate site. Defensive read-side clamp prevents
a corrupted slot from propagating a wild heading offset.

Replaced the single use site at the magnetometer yaw computation with
the getter; legacy global Mag_Declination retained and kept in sync by
the setter for any external linkage.

Added test_mcu_a2_mag_declination (10 cases): default, set/get,
persistence across reset, power-cycle clear, write-side clamp both
directions, plausible-site passthrough, defensive read-side clamp on
corruption, wrong-magic fallback, pre-fix bearing-error regression.
MCU regression now 81/81.
This commit is contained in:
Jason
2026-04-28 09:45:41 +05:45
parent 4a102e30fe
commit 0a49320e31
3 changed files with 190 additions and 2 deletions
@@ -147,7 +147,14 @@ float M11=1.0, M12=-0.0, M13=0.0,
float ax, ay, az, gx, gy, gz, mx, my, mz,mxc,myc,mzc; // variables to hold latest sensor data values
float q[4] = {1.0f, 0.0f, 0.0f, 0.0f}; // vector to hold quaternion
float temperature;
float Mag_Declination = -0.61; //0°
/* MCU-A2: site-specific magnetic declination. Was a hardcoded -0.61° literal
* baked in for one deployment location. Default kept for backward compatibility
* with existing sites; runtime override flows through set_mag_declination_deg()
* and persists across reset in BKPSRAM (slots 1+2 slot 0 is the MCU-A7
* emergency flag). Reads via get_mag_declination_deg() at the use site. */
#define MAG_DECLINATION_DEFAULT_DEG (-0.61f)
#define MAG_DECLINATION_LIMIT_DEG (30.0f)
float Mag_Declination = MAG_DECLINATION_DEFAULT_DEG; /* legacy global, retained for any external linkage */
float RxAcc,RyAcc,RzAcc;
float Rate_Ayz_1,Rate_Axz_1,Rate_Axy_1;
@@ -919,6 +926,46 @@ static bool emergency_persist_check(void) {
return *EMERGENCY_PERSIST_ADDR == EMERGENCY_PERSIST_MAGIC;
}
////////////////////////////////////////////////////////////////////////////////
// MCU-A2: site-configurable magnetic declination, persisted in BKPSRAM
//
// Slot layout (BKPSRAM, 4-byte words):
// [0] MCU-A7 emergency-state magic
// [1] MCU-A2 mag-declination magic
// [2] MCU-A2 mag-declination value (float, bit-cast as uint32)
//
// Survives every reset path; cleared only on main-power removal. Range
// clamped to ±30° before persisting (anything beyond that is a calibration
// error rather than a legitimate site value — physical declination ranges
// are roughly -25° to +25° worldwide).
////////////////////////////////////////////////////////////////////////////////
#define MAG_DECL_PERSIST_MAGIC 0xCAFEFACEU
#define MAG_DECL_MAGIC_ADDR ((volatile uint32_t *)(BKPSRAM_BASE + 4))
#define MAG_DECL_VALUE_ADDR ((volatile uint32_t *)(BKPSRAM_BASE + 8))
void set_mag_declination_deg(float deg) {
if (deg > MAG_DECLINATION_LIMIT_DEG) deg = MAG_DECLINATION_LIMIT_DEG;
if (deg < -MAG_DECLINATION_LIMIT_DEG) deg = -MAG_DECLINATION_LIMIT_DEG;
emergency_persist_init_clocks();
union { float f; uint32_t u; } cvt = { .f = deg };
*MAG_DECL_VALUE_ADDR = cvt.u;
*MAG_DECL_MAGIC_ADDR = MAG_DECL_PERSIST_MAGIC;
Mag_Declination = deg; /* keep legacy global in sync for any external readers */
}
float get_mag_declination_deg(void) {
emergency_persist_init_clocks();
if (*MAG_DECL_MAGIC_ADDR == MAG_DECL_PERSIST_MAGIC) {
union { float f; uint32_t u; } cvt = { .u = *MAG_DECL_VALUE_ADDR };
/* defensive clamp in case BKPSRAM was corrupted by VBAT brown-out */
float v = cvt.f;
if (v > MAG_DECLINATION_LIMIT_DEG) v = MAG_DECLINATION_LIMIT_DEG;
if (v < -MAG_DECLINATION_LIMIT_DEG) v = -MAG_DECLINATION_LIMIT_DEG;
return v;
}
return MAG_DECLINATION_DEFAULT_DEG;
}
////////////////////////////////////////////////////////////////////////////////
//:::::RF POWER AMPLIFIER DAC5578 Emergency stop function using CLR pin/////////
////////////////////////////////////////////////////////////////////////////////
@@ -1724,7 +1771,7 @@ int main(void)
float magRawX = mx*cos(Pitch_Sensor*PI/180.0f) - mz*sin(Pitch_Sensor*PI/180.0f);
float magRawY = mx*sin(Roll_Sensor*PI/180.0f)*sin(Pitch_Sensor*PI/180.0f) + my*cos(Roll_Sensor*PI/180.0f)- mz*sin(Roll_Sensor*PI/180.0f)*cos(Pitch_Sensor*PI/180.0f);
Yaw_Sensor = (180*atan2(magRawY,magRawX)/PI) - Mag_Declination;
Yaw_Sensor = (180*atan2(magRawY,magRawX)/PI) - get_mag_declination_deg(); /* MCU-A2 */
if(Yaw_Sensor<0)Yaw_Sensor+=360;
@@ -70,6 +70,7 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \
test_mcu_a7_emergency_persist \
test_mcu_a5_pa_cal_gate \
test_mcu_a6_recovery_dispatch \
test_mcu_a2_mag_declination \
test_gap3_iwdg_config \
test_gap3_temperature_max \
test_gap3_idq_periodic_reread \
@@ -175,6 +176,9 @@ test_mcu_a5_pa_cal_gate: test_mcu_a5_pa_cal_gate.c
test_mcu_a6_recovery_dispatch: test_mcu_a6_recovery_dispatch.c
$(CC) $(CFLAGS) $< -o $@
test_mcu_a2_mag_declination: test_mcu_a2_mag_declination.c
$(CC) $(CFLAGS) $< -lm -o $@
# Gap-3 safety tests -- mock-only (needs spy log for GPIO sequence)
test_gap3_emergency_stop_rails: test_gap3_emergency_stop_rails.c $(MOCK_OBJS)
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) -o $@
@@ -0,0 +1,137 @@
/*******************************************************************************
* test_mcu_a2_mag_declination.c
*
* MCU-A2: magnetic declination was a hardcoded -0.61° literal baked in for
* one deployment site. Yaw_Sensor is wrong by (site_decl - (-0.61))° at
* every other site whenever GPS dual-antenna heading is unavailable.
*
* Production fix backs the value with a BKPSRAM slot and exposes a
* setter/getter pair. Default returns to the legacy -0.61° when no override
* has been written, preserving backward compatibility for the original
* site. Range is clamped to ±30° (real-world declinations are roughly
* -25° to +25°, so anything beyond is a calibration error).
*
* This test models the BKPSRAM slot, replays the setter/getter, and
* verifies clamping, persistence across "reset", default-on-empty, and
* defensive clamping if BKPSRAM is corrupted.
******************************************************************************/
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define MAG_DECL_DEFAULT (-0.61f)
#define MAG_DECL_LIMIT (30.0f)
#define MAG_DECL_MAGIC 0xCAFEFACEU
/* Models BKPSRAM slot for declination (magic + value). */
static uint32_t g_magic;
static uint32_t g_value_bits;
static void simulated_power_cycle(void) { g_magic = 0; g_value_bits = 0; }
static void simulated_corrupt(uint32_t magic, float value) {
g_magic = magic;
union { float f; uint32_t u; } cvt = { .f = value };
g_value_bits = cvt.u;
}
static void set_decl(float deg) {
if (deg > MAG_DECL_LIMIT) deg = MAG_DECL_LIMIT;
if (deg < -MAG_DECL_LIMIT) deg = -MAG_DECL_LIMIT;
union { float f; uint32_t u; } cvt = { .f = deg };
g_value_bits = cvt.u;
g_magic = MAG_DECL_MAGIC;
}
static float get_decl(void) {
if (g_magic == MAG_DECL_MAGIC) {
union { float f; uint32_t u; } cvt = { .u = g_value_bits };
float v = cvt.f;
if (v > MAG_DECL_LIMIT) v = MAG_DECL_LIMIT;
if (v < -MAG_DECL_LIMIT) v = -MAG_DECL_LIMIT;
return v;
}
return MAG_DECL_DEFAULT;
}
static bool feq(float a, float b) { return fabsf(a - b) < 0.001f; }
int main(void)
{
printf("=== MCU-A2: mag-declination BKPSRAM persistence + clamp ===\n");
/* 1. Empty BKPSRAM -> legacy default for backward compatibility. */
printf(" Test 1: empty BKPSRAM returns default -0.61 ... ");
simulated_power_cycle();
assert(feq(get_decl(), MAG_DECL_DEFAULT));
printf("PASS\n");
/* 2. Setter writes; getter returns the written value. */
printf(" Test 2: set 12.4 then get ... ");
set_decl(12.4f);
assert(feq(get_decl(), 12.4f));
printf("PASS\n");
/* 3. Value persists across simulated reset (BKPSRAM survives). */
printf(" Test 3: persists across reset ... ");
/* simulated reset = process state preserved (BKPSRAM survives) */
assert(feq(get_decl(), 12.4f));
printf("PASS\n");
/* 4. Power cycle clears BKPSRAM -> back to default. */
printf(" Test 4: power-cycle restores default ... ");
simulated_power_cycle();
assert(feq(get_decl(), MAG_DECL_DEFAULT));
printf("PASS\n");
/* 5. Setter clamps high. */
printf(" Test 5: set +45 clamps to +30 ... ");
set_decl(45.0f);
assert(feq(get_decl(), 30.0f));
printf("PASS\n");
/* 6. Setter clamps low. */
printf(" Test 6: set -45 clamps to -30 ... ");
set_decl(-45.0f);
assert(feq(get_decl(), -30.0f));
printf("PASS\n");
/* 7. Plausible site values pass through unmodified. */
printf(" Test 7: realistic site values pass through ... ");
const float sites[] = { -22.5f, -8.0f, -0.61f, 0.0f, 4.3f, 11.2f, 17.9f };
for (size_t i = 0; i < sizeof(sites)/sizeof(sites[0]); i++) {
set_decl(sites[i]);
assert(feq(get_decl(), sites[i]));
}
printf("PASS\n");
/* 8. Defensive clamp on getter — if BKPSRAM is corrupted to an
* out-of-range value (VBAT brown-out, bit flip), getter still returns
* a safe value rather than propagating a wild offset. */
printf(" Test 8: corrupt +1000 BKPSRAM clamps to +30 on read ... ");
simulated_corrupt(MAG_DECL_MAGIC, 1000.0f);
assert(feq(get_decl(), 30.0f));
printf("PASS\n");
/* 9. Wrong magic -> default (corruption that doesn't preserve magic). */
printf(" Test 9: wrong magic returns default ... ");
simulated_corrupt(0xDEADBEEFU, 5.0f);
assert(feq(get_decl(), MAG_DECL_DEFAULT));
printf("PASS\n");
/* 10. Pre-fix regression: hardcoded -0.61 used at non-default site
* yields wrong heading by (site_decl - (-0.61))°. Confirm the fixed
* path returns the configured site value, eliminating the offset. */
printf(" Test 10: post-fix yaw correction matches configured site ... ");
set_decl(11.2f);
float pre_fix_decl_used = -0.61f;
float post_fix_decl_used = get_decl();
float bearing_error = post_fix_decl_used - pre_fix_decl_used;
assert(feq(bearing_error, 11.81f)); /* heading offset corrected */
printf("(error %.2f degrees), PASS\n", (double)bearing_error);
printf("\n=== MCU-A2: ALL TESTS PASSED ===\n\n");
return 0;
}