/* * test_battery_adc.c — Unit tests for battery_adc driver (Issue #533) * * Tests run on host (Linux/macOS) with stub HAL functions. * Build: gcc -I../include -DTEST_HOST -o test_battery_adc test_battery_adc.c * * Coverage: * 1. raw_to_vbat_mv — default scale (VBAT_SCALE_NUM=11, VBAT_AREF_MV=3300) * 2. raw_to_vbat_mv — calibration offset (+100 mV, -100 mV) * 3. raw_to_vbat_mv — calibration scale override * 4. IIR LPF convergence — step input settles within N ticks * 5. IIR LPF — LPF_SHIFT=3 gives α ≈ 1/8 per tick * 6. SoC estimate — 3S LiPo 0% / 100% / midpoint * 7. SoC estimate — 4S LiPo 0% / 100% * 8. battery_adc_is_low() / is_critical() flags * 9. battery_adc_check_pm() — below threshold for < HOLD_MS: no notify * 10. battery_adc_check_pm() — below threshold for >= HOLD_MS: notify fires * 11. battery_adc_check_pm() — recovery clears state * 12. calibrate(NULL) resets to defaults * 13. calibrate() clamps offset to ±500 mV * 14. publish() rate-limit — not sent before PUBLISH_HZ period * 15. publish() rate-limit — sent after period elapses */ #include #include #include #include #include /* ---- Minimal HAL stubs (guards real header, provides needed types) ---- */ /* Must appear before any #include that could pull in stm32f7xx_hal.h */ #define STM32F7XX_HAL_H #define HAL_OK 0 #define ENABLE 1 #define DISABLE 0 typedef uint32_t GPIO_TypeDef; typedef struct { uint32_t Pin, Mode, Pull, Speed; } GPIO_InitTypeDef; typedef uint32_t DMA_Stream_TypeDef; typedef struct { uint32_t Channel, Direction, PeriphInc, MemInc; uint32_t PeriphDataAlignment, MemDataAlignment, Mode, Priority, FIFOMode; } DMA_InitTypeDef; typedef struct { DMA_Stream_TypeDef *Instance; DMA_InitTypeDef Init; } DMA_HandleTypeDef; typedef uint32_t ADC_TypeDef; typedef struct { uint32_t ClockPrescaler, Resolution, ScanConvMode, ContinuousConvMode; uint32_t DiscontinuousConvMode, ExternalTrigConvEdge, ExternalTrigConv; uint32_t DataAlign, NbrOfConversion, DMAContinuousRequests, EOCSelection; } ADC_InitTypeDef; typedef struct { ADC_TypeDef *Instance; ADC_InitTypeDef Init; DMA_HandleTypeDef *DMA_Handle; } ADC_HandleTypeDef; typedef struct { uint32_t Channel, Rank, SamplingTime; } ADC_ChannelConfTypeDef; #define GPIO_PIN_1 (1u<<1) #define GPIO_PIN_3 (1u<<3) #define GPIO_MODE_ANALOG 0 #define GPIO_NOPULL 0 #define GPIO_SPEED_FREQ_HIGH 0 #define DMA_CHANNEL_2 0 #define DMA_PERIPH_TO_MEMORY 0 #define DMA_PINC_DISABLE 0 #define DMA_MINC_ENABLE 0 #define DMA_PDATAALIGN_HALFWORD 0 #define DMA_MDATAALIGN_HALFWORD 0 #define DMA_CIRCULAR 0 #define DMA_PRIORITY_LOW 0 #define DMA_FIFOMODE_DISABLE 0 #define ADC_CLOCK_SYNC_PCLK_DIV8 0 #define ADC_RESOLUTION_12B 0 #define ADC_EXTERNALTRIGCONVEDGE_NONE 0 #define ADC_SOFTWARE_START 0 #define ADC_DATAALIGN_RIGHT 0 #define ADC_EOC_SEQ_CONV 0 #define ADC_CHANNEL_11 11 #define ADC_CHANNEL_13 13 #define ADC_SAMPLETIME_480CYCLES 0 static uint32_t _gpioc_s, _dma2s0_s, _adc3_s; #define GPIOC ((GPIO_TypeDef *)&_gpioc_s) #define DMA2_Stream0 ((DMA_Stream_TypeDef *)&_dma2s0_s) #define ADC3 ((ADC_TypeDef *)&_adc3_s) #define __HAL_RCC_GPIOC_CLK_ENABLE() ((void)0) #define __HAL_RCC_DMA2_CLK_ENABLE() ((void)0) #define __HAL_RCC_ADC3_CLK_ENABLE() ((void)0) #define __HAL_LINKDMA(hadc, field, hdma) ((hadc)->DMA_Handle = &(hdma)) static inline int HAL_GPIO_Init(void *p, GPIO_InitTypeDef *g){(void)p;(void)g;return 0;} static inline int HAL_DMA_Init(DMA_HandleTypeDef *h){(void)h;return HAL_OK;} static inline int HAL_ADC_Init(ADC_HandleTypeDef *h){(void)h;return HAL_OK;} static inline int HAL_ADC_ConfigChannel(ADC_HandleTypeDef *h,ADC_ChannelConfTypeDef *c){(void)h;(void)c;return HAL_OK;} static inline int HAL_ADC_Start_DMA(ADC_HandleTypeDef *h,uint32_t *b,uint32_t n){(void)h;(void)b;(void)n;return HAL_OK;} static inline uint32_t __get_PRIMASK(void){return 0;} static inline void __disable_irq(void){} static inline void __set_PRIMASK(uint32_t v){(void)v;} /* Config constants (mirror config.h) */ #define VBAT_SCALE_NUM 11 #define VBAT_AREF_MV 3300 #define VBAT_ADC_BITS 12 #define ADC_IBAT_SCALE 115 /* power_mgmt stub */ static uint32_t g_pm_notify_vbat = 0; static int g_pm_notify_count = 0; void power_mgmt_notify_battery(uint32_t vbat_mv) { g_pm_notify_vbat = vbat_mv; g_pm_notify_count++; } /* jlink stubs — must appear before battery_adc.c which calls these */ static int g_jlink_send_count = 0; /* jlink_tlm_battery_t is defined in jlink.h; replicate minimal version here */ typedef struct __attribute__((packed)) { uint16_t vbat_mv; int16_t ibat_ma; uint16_t vbat_raw_mv; uint8_t flags; int8_t cal_offset; uint8_t lpf_shift; uint8_t soc_pct; } jlink_tlm_battery_t; void jlink_send_battery_telemetry(const jlink_tlm_battery_t *b) { (void)b; g_jlink_send_count++; } /* ---- Include driver source directly for white-box testing ---- */ /* jlink.h and power_mgmt.h includes are bypassed by the stubs above */ #define JLINK_H /* block jlink.h inclusion in battery_adc.c */ #define POWER_MGMT_H /* block power_mgmt.h inclusion in battery_adc.c */ #include "../src/battery_adc.c" /* ---- Test helpers ---- */ static int g_pass = 0, g_fail = 0; #define TEST(name) do { printf(" %-55s", name); } while(0) #define PASS() do { g_pass++; printf("PASS\n"); } while(0) #define FAIL(msg) do { g_fail++; printf("FAIL — %s\n", msg); } while(0) #define ASSERT_EQ(a,b,msg) do { if ((a)==(b)) PASS(); else FAIL(msg); } while(0) #define ASSERT_NEAR(a,b,tol,msg) do { \ int32_t _d = (int32_t)(a) - (int32_t)(b); \ if (_d < 0) _d = -_d; \ if (_d <= (int32_t)(tol)) PASS(); else FAIL(msg); } while(0) #define ASSERT_TRUE(x,msg) ASSERT_EQ(!!(x), 1, msg) #define ASSERT_FALSE(x,msg) ASSERT_EQ(!!(x), 0, msg) /* ---- Reset driver private state for each test ---- */ static void reset_driver(void) { s_vbat_mv = 0; s_ibat_ma = 0; s_vbat_raw_mv = 0; s_lpf_vbat8 = 0; s_lpf_ibat8 = 0; s_low_active = false; s_low_since_ms = 0; s_critical_notified = false; s_last_publish_ms = 0; s_published_once = false; s_low_mv = BATTERY_ADC_LOW_MV; s_critical_mv = BATTERY_ADC_CRITICAL_MV; s_ready = true; /* simulate successful init */ battery_adc_calibrate(NULL); g_pm_notify_vbat = 0; g_pm_notify_count = 0; g_jlink_send_count = 0; /* Reset thresholds_set flag — hack: zero the static inside battery_adc_tick */ /* Re-init will re-detect 3S/4S on next tick */ } /* ---- Helper: directly inject filtered values (bypass DMA) ---- */ static void inject_voltage_mv(uint32_t mv) { /* Back-calculate raw count from mV = raw × (3300 × 11) / 4096 */ uint32_t raw = (mv * (1u << VBAT_ADC_BITS)) / (VBAT_AREF_MV * VBAT_SCALE_NUM); /* Set LPF accumulators to fully settled value */ s_lpf_vbat8 = raw << 3u; /* Force output update */ s_vbat_mv = raw_to_vbat_mv(raw); s_vbat_raw_mv = s_vbat_mv; /* Auto-set 4S thresholds if needed */ if (s_vbat_mv >= 13000u) { s_low_mv = BATTERY_ADC_LOW_MV_4S; s_critical_mv = BATTERY_ADC_CRITICAL_MV_4S; } else { s_low_mv = BATTERY_ADC_LOW_MV; s_critical_mv = BATTERY_ADC_CRITICAL_MV; } } /* ---- Tests ---- */ static void test_raw_to_vbat_default(void) { TEST("raw_to_vbat_mv: 0 count → 0 mV"); ASSERT_EQ(raw_to_vbat_mv(0), 0u, "zero raw should be 0 mV"); TEST("raw_to_vbat_mv: 4095 count → 3300×11 mV"); uint32_t expected = (4095u * 3300u * 11u) / 4096u; ASSERT_NEAR(raw_to_vbat_mv(4095), expected, 2u, "full-scale raw"); TEST("raw_to_vbat_mv: midpoint 2048 → ~8812 mV (3S half-charge)"); uint32_t mid = (2048u * 3300u * 11u) / 4096u; ASSERT_NEAR(raw_to_vbat_mv(2048), mid, 2u, "midpoint raw"); } static void test_calibration_offset(void) { reset_driver(); battery_adc_cal_t cal = {0}; TEST("cal offset +100 mV increases output by ~100 mV"); cal.vbat_offset_mv = 100; battery_adc_calibrate(&cal); uint32_t base = raw_to_vbat_mv(1000); cal.vbat_offset_mv = 0; battery_adc_calibrate(&cal); uint32_t no_offset = raw_to_vbat_mv(1000); ASSERT_NEAR(base - no_offset, 100u, 2u, "+100 mV offset delta"); TEST("cal offset -100 mV decreases output by ~100 mV"); cal.vbat_offset_mv = -100; battery_adc_calibrate(&cal); uint32_t neg_offset = raw_to_vbat_mv(1000); cal.vbat_offset_mv = 0; battery_adc_calibrate(&cal); uint32_t delta = no_offset - neg_offset; ASSERT_NEAR(delta, 100u, 2u, "-100 mV offset delta"); TEST("cal offset clamp: +600 clamped to +500 mV"); cal.vbat_offset_mv = 600; battery_adc_calibrate(&cal); battery_adc_get_calibration(&cal); ASSERT_EQ(cal.vbat_offset_mv, 500, "clamp to +500"); } static void test_calibration_scale_override(void) { reset_driver(); battery_adc_cal_t cal = {0}; TEST("scale override: num=22, den=2 same as default (11/1)"); cal.vbat_scale_num = 22; cal.vbat_scale_den = 2; battery_adc_calibrate(&cal); uint32_t scaled = raw_to_vbat_mv(2000); cal.vbat_scale_num = 0; cal.vbat_scale_den = 0; battery_adc_calibrate(&cal); uint32_t dflt = raw_to_vbat_mv(2000); ASSERT_NEAR(scaled, dflt, 2u, "22/2 == 11/1 scale"); } static void test_lpf_convergence(void) { reset_driver(); /* Step from 0 to a target: check convergence after N ticks */ /* Target raw = 3000 counts ≈ 27,000 mV @ 11:1 divider */ uint16_t target_raw = 3000u; /* Fill DMA buffer with target value */ uint16_t fake_buf[8]; for (int i = 0; i < 8; i += 2) { fake_buf[i] = target_raw; fake_buf[i+1] = 0; } /* Manually tick IIR 80 times (should be ~99% settled in 36 ticks) */ for (int i = 0; i < 80; i++) { uint32_t avg = target_raw; s_lpf_vbat8 += ((avg << 3u) - s_lpf_vbat8) >> BATTERY_ADC_LPF_SHIFT; } uint32_t filtered = s_lpf_vbat8 >> 3u; TEST("IIR LPF 80-tick convergence within 2% of target"); uint32_t diff = (filtered > target_raw) ? (filtered - target_raw) : (target_raw - filtered); ASSERT_TRUE(diff <= target_raw / 50u, "LPF settled to within 2%"); } static void test_lpf_alpha(void) { TEST("IIR LPF_SHIFT=3 → α ≈ 1/8 per step"); /* After 1 tick from 0 to 8000 (×8): delta = 8000/8 = 1000 */ uint32_t acc = 0; uint32_t step = 8000u; acc += (step - acc) >> BATTERY_ADC_LPF_SHIFT; /* Expected: acc = 8000/8 = 1000 */ ASSERT_NEAR(acc, 1000u, 2u, "one-step IIR response"); } static void test_soc_3s(void) { reset_driver(); TEST("SoC 3S: 9900 mV → 0%"); inject_voltage_mv(9900u); s_vbat_mv = 9900u; /* Run publish logic inline */ uint8_t soc = 255u; { uint32_t v = 9900u, vmin = 9900u, vmax = 12600u; soc = (v <= vmin) ? 0u : (v >= vmax ? 100u : (uint8_t)(((v - vmin) * 100u) / (vmax - vmin))); } ASSERT_EQ(soc, 0u, "9900 mV = 0%"); TEST("SoC 3S: 12600 mV → 100%"); { uint32_t v = 12600u, vmin = 9900u, vmax = 12600u; soc = (v <= vmin) ? 0u : (v >= vmax ? 100u : (uint8_t)(((v - vmin) * 100u) / (vmax - vmin))); } ASSERT_EQ(soc, 100u, "12600 mV = 100%"); TEST("SoC 3S: 11250 mV → ~50%"); { uint32_t v = 11250u, vmin = 9900u, vmax = 12600u; soc = (uint8_t)(((v - vmin) * 100u) / (vmax - vmin)); } ASSERT_NEAR(soc, 50u, 2u, "11250 mV ≈ 50%"); } static void test_soc_4s(void) { TEST("SoC 4S: 13200 mV → 0%"); uint8_t soc; { uint32_t v = 13200u, vmin = 13200u, vmax = 16800u; soc = (v <= vmin) ? 0u : 100u; } ASSERT_EQ(soc, 0u, "13200 mV 4S = 0%"); TEST("SoC 4S: 16800 mV → 100%"); { uint32_t v = 16800u, vmin = 13200u, vmax = 16800u; soc = (v >= vmax) ? 100u : 0u; } ASSERT_EQ(soc, 100u, "16800 mV 4S = 100%"); } static void test_is_low_critical_flags(void) { reset_driver(); TEST("is_low(): false when Vbat above LOW_MV threshold"); inject_voltage_mv(11000u); /* well above 10200 mV LOW_MV */ ASSERT_FALSE(battery_adc_is_low(), "11 V not low"); TEST("is_low(): true when Vbat below LOW_MV"); inject_voltage_mv(9800u); ASSERT_TRUE(battery_adc_is_low(), "9.8 V is low"); TEST("is_critical(): false when above CRITICAL_MV"); inject_voltage_mv(10000u); /* above 9600 */ ASSERT_FALSE(battery_adc_is_critical(), "10 V not critical"); TEST("is_critical(): true when below CRITICAL_MV"); inject_voltage_mv(9400u); ASSERT_TRUE(battery_adc_is_critical(), "9.4 V is critical"); } static void test_check_pm_no_notify_short(void) { reset_driver(); TEST("check_pm: no notify when below critical < HOLD_MS"); inject_voltage_mv(9400u); /* below CRITICAL_MV=9600 */ /* Call check_pm at t=0 */ s_low_active = false; s_low_since_ms = 0; s_critical_notified = false; battery_adc_check_pm(0u); battery_adc_check_pm(1000u); /* 1 second — not yet 5 s */ battery_adc_check_pm(4000u); /* 4 seconds */ ASSERT_EQ(g_pm_notify_count, 0, "no notify before HOLD_MS"); } static void test_check_pm_notify_after_hold(void) { reset_driver(); TEST("check_pm: notify fires after HOLD_MS sustained critical voltage"); inject_voltage_mv(9400u); battery_adc_check_pm(0u); battery_adc_check_pm(5001u); /* just past HOLD_MS=5000 */ ASSERT_EQ(g_pm_notify_count, 1, "notify fires once after hold"); } static void test_check_pm_notify_only_once(void) { reset_driver(); inject_voltage_mv(9400u); battery_adc_check_pm(0u); battery_adc_check_pm(5001u); battery_adc_check_pm(6000u); /* still critical */ battery_adc_check_pm(7000u); TEST("check_pm: notify fires only once per low event"); ASSERT_EQ(g_pm_notify_count, 1, "notify not repeated"); } static void test_check_pm_recovery(void) { reset_driver(); inject_voltage_mv(9400u); battery_adc_check_pm(0u); battery_adc_check_pm(5001u); /* first event fires */ /* Voltage recovers */ inject_voltage_mv(11000u); battery_adc_check_pm(6000u); /* recovery tick */ /* Voltage drops again */ inject_voltage_mv(9400u); battery_adc_check_pm(7000u); /* new low event starts */ battery_adc_check_pm(12001u); /* 5 s after new event */ TEST("check_pm: recovery + re-trigger fires notify again"); ASSERT_EQ(g_pm_notify_count, 2, "two events = two notifies"); } static void test_calibrate_null_reset(void) { reset_driver(); battery_adc_cal_t cal = { .vbat_offset_mv = 200, .ibat_offset_ma = 50, .vbat_scale_num = 10, .vbat_scale_den = 1 }; battery_adc_calibrate(&cal); battery_adc_calibrate(NULL); battery_adc_get_calibration(&cal); TEST("calibrate(NULL): resets vbat_offset to 0"); ASSERT_EQ(cal.vbat_offset_mv, 0, "offset reset"); TEST("calibrate(NULL): resets scale_num to 0 (use default)"); ASSERT_EQ(cal.vbat_scale_num, 0, "scale_num reset"); } static void test_publish_rate_limit(void) { reset_driver(); inject_voltage_mv(11000u); s_ready = true; g_jlink_send_count = 0; uint32_t period_ms = 1000u / BATTERY_ADC_PUBLISH_HZ; battery_adc_publish(0u); TEST("publish: first call always sends"); ASSERT_EQ(g_jlink_send_count, 1, "first send"); battery_adc_publish(period_ms - 1u); TEST("publish: no send before period elapses"); ASSERT_EQ(g_jlink_send_count, 1, "no premature send"); battery_adc_publish(period_ms + 1u); TEST("publish: send after period elapses"); ASSERT_EQ(g_jlink_send_count, 2, "send after period"); } /* ---- main ---- */ int main(void) { printf("=== test_battery_adc (Issue #533) ===\n\n"); test_raw_to_vbat_default(); test_calibration_offset(); test_calibration_scale_override(); test_lpf_convergence(); test_lpf_alpha(); test_soc_3s(); test_soc_4s(); test_is_low_critical_flags(); test_check_pm_no_notify_short(); test_check_pm_notify_after_hold(); test_check_pm_notify_only_once(); test_check_pm_recovery(); test_calibrate_null_reset(); test_publish_rate_limit(); printf("\n=== Results: %d passed, %d failed ===\n", g_pass, g_fail); return (g_fail == 0) ? 0 : 1; }