diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.cpp new file mode 100644 index 0000000..0d4d477 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.cpp @@ -0,0 +1,116 @@ +// ADAR1000_AGC.cpp -- STM32 outer-loop AGC implementation +// +// See ADAR1000_AGC.h for architecture overview. + +#include "ADAR1000_AGC.h" +#include "ADAR1000_Manager.h" +#include "diag_log.h" + +#include + +// --------------------------------------------------------------------------- +// Constructor -- set all config fields to safe defaults +// --------------------------------------------------------------------------- +ADAR1000_AGC::ADAR1000_AGC() + : agc_base_gain(ADAR1000Manager::kDefaultRxVgaGain) // 30 + , gain_step_down(4) + , gain_step_up(1) + , min_gain(0) + , max_gain(127) + , holdoff_frames(4) + , enabled(true) + , holdoff_counter(0) + , last_saturated(false) + , saturation_event_count(0) +{ + memset(cal_offset, 0, sizeof(cal_offset)); +} + +// --------------------------------------------------------------------------- +// update -- called once per frame with the FPGA DIG_5 saturation flag +// +// Returns true if agc_base_gain changed (caller should then applyGain). +// --------------------------------------------------------------------------- +void ADAR1000_AGC::update(bool fpga_saturation) +{ + if (!enabled) + return; + + last_saturated = fpga_saturation; + + if (fpga_saturation) { + // Attack: reduce gain immediately + saturation_event_count++; + holdoff_counter = 0; + + if (agc_base_gain >= gain_step_down + min_gain) { + agc_base_gain -= gain_step_down; + } else { + agc_base_gain = min_gain; + } + + DIAG("AGC", "SAT detected -- gain_base -> %u (events=%lu)", + (unsigned)agc_base_gain, (unsigned long)saturation_event_count); + + } else { + // Recovery: wait for holdoff, then increase gain + holdoff_counter++; + + if (holdoff_counter >= holdoff_frames) { + holdoff_counter = 0; + + if (agc_base_gain + gain_step_up <= max_gain) { + agc_base_gain += gain_step_up; + } else { + agc_base_gain = max_gain; + } + + DIAG("AGC", "Recovery step -- gain_base -> %u", (unsigned)agc_base_gain); + } + } +} + +// --------------------------------------------------------------------------- +// applyGain -- write effective gain to all 16 RX VGA channels +// +// Uses the Manager's adarSetRxVgaGain which takes 1-based channel indices +// (matching the convention in setBeamAngle). +// --------------------------------------------------------------------------- +void ADAR1000_AGC::applyGain(ADAR1000Manager &mgr) +{ + for (uint8_t dev = 0; dev < AGC_NUM_DEVICES; ++dev) { + for (uint8_t ch = 0; ch < AGC_NUM_CHANNELS; ++ch) { + uint8_t gain = effectiveGain(dev * AGC_NUM_CHANNELS + ch); + // Channel parameter is 1-based per Manager convention + mgr.adarSetRxVgaGain(dev, ch + 1, gain, BROADCAST_OFF); + } + } +} + +// --------------------------------------------------------------------------- +// resetState -- clear runtime counters, preserve configuration +// --------------------------------------------------------------------------- +void ADAR1000_AGC::resetState() +{ + holdoff_counter = 0; + last_saturated = false; + saturation_event_count = 0; +} + +// --------------------------------------------------------------------------- +// effectiveGain -- compute clamped per-channel gain +// --------------------------------------------------------------------------- +uint8_t ADAR1000_AGC::effectiveGain(uint8_t channel_index) const +{ + if (channel_index >= AGC_TOTAL_CHANNELS) + return min_gain; // safety fallback — OOB channels get minimum gain + + int16_t raw = static_cast(agc_base_gain) + cal_offset[channel_index]; + + if (raw < static_cast(min_gain)) + return min_gain; + if (raw > static_cast(max_gain)) + return max_gain; + + return static_cast(raw); +} diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.h b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.h new file mode 100644 index 0000000..bf534fd --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.h @@ -0,0 +1,97 @@ +// ADAR1000_AGC.h -- STM32 outer-loop AGC for ADAR1000 RX VGA gain +// +// Adjusts the analog VGA common-mode gain on each ADAR1000 RX channel based on +// the FPGA's saturation flag (DIG_5 / PD13). Runs once per radar frame +// (~258 ms) in the main loop, after runRadarPulseSequence(). +// +// Architecture: +// - Inner loop (FPGA, per-sample): rx_gain_control auto-adjusts digital +// gain_shift based on peak magnitude / saturation. Range ±42 dB. +// - Outer loop (THIS MODULE, per-frame): reads FPGA DIG_5 GPIO. If +// saturation detected, reduces agc_base_gain immediately (attack). If no +// saturation for holdoff_frames, increases agc_base_gain (decay/recovery). +// +// Per-channel gain formula: +// VGA[dev][ch] = clamp(agc_base_gain + cal_offset[dev*4+ch], min_gain, max_gain) +// +// The cal_offset array allows per-element calibration to correct inter-channel +// gain imbalance. Default is all zeros (uniform gain). + +#ifndef ADAR1000_AGC_H +#define ADAR1000_AGC_H + +#include + +// Forward-declare to avoid pulling in the full ADAR1000_Manager header here. +// The .cpp includes the real header. +class ADAR1000Manager; + +// Number of ADAR1000 devices +#define AGC_NUM_DEVICES 4 +// Number of channels per ADAR1000 +#define AGC_NUM_CHANNELS 4 +// Total RX channels +#define AGC_TOTAL_CHANNELS (AGC_NUM_DEVICES * AGC_NUM_CHANNELS) + +class ADAR1000_AGC { +public: + // --- Configuration (public for easy field-testing / GUI override) --- + + // Common-mode base gain (raw ADAR1000 register value, 0-255). + // Default matches ADAR1000Manager::kDefaultRxVgaGain = 30. + uint8_t agc_base_gain; + + // Per-channel calibration offset (signed, added to agc_base_gain). + // Index = device*4 + channel. Default: all 0. + int8_t cal_offset[AGC_TOTAL_CHANNELS]; + + // How much to decrease agc_base_gain per frame when saturated (attack). + uint8_t gain_step_down; + + // How much to increase agc_base_gain per frame when recovering (decay). + uint8_t gain_step_up; + + // Minimum allowed agc_base_gain (floor). + uint8_t min_gain; + + // Maximum allowed agc_base_gain (ceiling). + uint8_t max_gain; + + // Number of consecutive non-saturated frames required before gain-up. + uint8_t holdoff_frames; + + // Master enable. When false, update() is a no-op. + bool enabled; + + // --- Runtime state (read-only for diagnostics) --- + + // Consecutive non-saturated frame counter (resets on saturation). + uint8_t holdoff_counter; + + // True if the last update() saw saturation. + bool last_saturated; + + // Total saturation events since reset/construction. + uint32_t saturation_event_count; + + // --- Methods --- + + ADAR1000_AGC(); + + // Call once per frame after runRadarPulseSequence(). + // fpga_saturation: result of HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_13) == GPIO_PIN_SET + void update(bool fpga_saturation); + + // Apply the current gain to all 16 RX VGA channels via the Manager. + void applyGain(ADAR1000Manager &mgr); + + // Reset runtime state (holdoff counter, saturation count) without + // changing configuration. + void resetState(); + + // Compute the effective gain for a specific channel index (0-15), + // clamped to [min_gain, max_gain]. Useful for diagnostics. + uint8_t effectiveGain(uint8_t channel_index) const; +}; + +#endif // ADAR1000_AGC_H diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp index b11cf02..324de26 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp @@ -23,6 +23,7 @@ #include "usbd_cdc_if.h" #include "adar1000.h" #include "ADAR1000_Manager.h" +#include "ADAR1000_AGC.h" extern "C" { #include "ad9523.h" } @@ -224,6 +225,7 @@ extern SPI_HandleTypeDef hspi4; //ADAR1000 ADAR1000Manager adarManager; +ADAR1000_AGC outerAgc; static uint8_t matrix1[15][16]; static uint8_t matrix2[15][16]; static uint8_t vector_0[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; @@ -2114,6 +2116,15 @@ int main(void) runRadarPulseSequence(); + /* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13), + * adjust ADAR1000 VGA common gain once per radar frame (~258 ms). */ + { + bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port, + FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET; + outerAgc.update(sat); + outerAgc.applyGain(adarManager); + } + /* [GAP-3 FIX 2] Kick hardware watchdog — if we don't reach here within * ~4 s, the IWDG resets the MCU automatically. */ HAL_IWDG_Refresh(&hiwdg); diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h index e4dbaf5..f5b8d0d 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h @@ -141,6 +141,15 @@ void Error_Handler(void); #define EN_DIS_RFPA_VDD_GPIO_Port GPIOD #define EN_DIS_COOLING_Pin GPIO_PIN_7 #define EN_DIS_COOLING_GPIO_Port GPIOD + +/* FPGA digital I/O (directly connected GPIOs) */ +#define FPGA_DIG5_SAT_Pin GPIO_PIN_13 +#define FPGA_DIG5_SAT_GPIO_Port GPIOD +#define FPGA_DIG6_Pin GPIO_PIN_14 +#define FPGA_DIG6_GPIO_Port GPIOD +#define FPGA_DIG7_Pin GPIO_PIN_15 +#define FPGA_DIG7_GPIO_Port GPIOD + #define ADF4382_RX_CE_Pin GPIO_PIN_9 #define ADF4382_RX_CE_GPIO_Port GPIOG #define ADF4382_RX_CS_Pin GPIO_PIN_10 diff --git a/9_Firmware/9_1_Microcontroller/tests/Makefile b/9_Firmware/9_1_Microcontroller/tests/Makefile index a44f962..73e7857 100644 --- a/9_Firmware/9_1_Microcontroller/tests/Makefile +++ b/9_Firmware/9_1_Microcontroller/tests/Makefile @@ -16,10 +16,17 @@ ################################################################################ CC := cc +CXX := c++ CFLAGS := -std=c11 -Wall -Wextra -Wno-unused-parameter -g -O0 +CXXFLAGS := -std=c++17 -Wall -Wextra -Wno-unused-parameter -g -O0 # Shim headers come FIRST so they override real headers INCLUDES := -Ishims -I. -I../9_1_1_C_Cpp_Libraries +# C++ library directory (AGC, ADAR1000 Manager) +CXX_LIB_DIR := ../9_1_1_C_Cpp_Libraries +CXX_SRCS := $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.cpp +CXX_OBJS := ADAR1000_AGC.o ADAR1000_Manager.o + # Real source files compiled against mock headers REAL_SRC := ../9_1_1_C_Cpp_Libraries/adf4382a_manager.c @@ -62,7 +69,10 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \ # Tests that need platform_noos_stm32.o + mocks TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only -ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) +# C++ tests (AGC outer loop) +TESTS_WITH_CXX := test_agc_outer_loop + +ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) .PHONY: all build test clean \ $(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \ @@ -156,6 +166,24 @@ test_gap3_emergency_state_ordering: test_gap3_emergency_state_ordering.c $(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ) $(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@ +# --- C++ object rules --- + +ADAR1000_AGC.o: $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_AGC.h + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +ADAR1000_Manager.o: $(CXX_LIB_DIR)/ADAR1000_Manager.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.h + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +# --- C++ test binary rules --- + +test_agc_outer_loop: test_agc_outer_loop.cpp $(CXX_OBJS) $(MOCK_OBJS) + $(CXX) $(CXXFLAGS) $(INCLUDES) $< $(CXX_OBJS) $(MOCK_OBJS) -o $@ + +# Convenience target +.PHONY: test_agc +test_agc: test_agc_outer_loop + ./test_agc_outer_loop + # --- Individual test targets --- test_bug1: test_bug1_timed_sync_init_ordering diff --git a/9_Firmware/9_1_Microcontroller/tests/shims/main.h b/9_Firmware/9_1_Microcontroller/tests/shims/main.h index 9dd05df..6543adc 100644 --- a/9_Firmware/9_1_Microcontroller/tests/shims/main.h +++ b/9_Firmware/9_1_Microcontroller/tests/shims/main.h @@ -129,6 +129,14 @@ void Error_Handler(void); #define GYR_INT_Pin GPIO_PIN_8 #define GYR_INT_GPIO_Port GPIOC +/* FPGA digital I/O (directly connected GPIOs) */ +#define FPGA_DIG5_SAT_Pin GPIO_PIN_13 +#define FPGA_DIG5_SAT_GPIO_Port GPIOD +#define FPGA_DIG6_Pin GPIO_PIN_14 +#define FPGA_DIG6_GPIO_Port GPIOD +#define FPGA_DIG7_Pin GPIO_PIN_15 +#define FPGA_DIG7_GPIO_Port GPIOD + #ifdef __cplusplus } #endif diff --git a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c index 6420f04..2b33b4f 100644 --- a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c +++ b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c @@ -175,7 +175,7 @@ void HAL_Delay(uint32_t Delay) mock_tick += Delay; } -HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, +HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) { spy_push((SpyRecord){ diff --git a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h index 1e0b61c..ac41470 100644 --- a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h +++ b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h @@ -34,6 +34,10 @@ typedef uint32_t HAL_StatusTypeDef; #define HAL_MAX_DELAY 0xFFFFFFFFU +#ifndef __NOP +#define __NOP() ((void)0) +#endif + /* ========================= GPIO Types ============================ */ typedef struct { @@ -182,7 +186,7 @@ GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); uint32_t HAL_GetTick(void); void HAL_Delay(uint32_t Delay); -HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); +HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout); /* ========================= SPI stubs ============================== */ diff --git a/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp b/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp new file mode 100644 index 0000000..9b23d28 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp @@ -0,0 +1,361 @@ +// test_agc_outer_loop.cpp -- C++ unit tests for ADAR1000_AGC outer-loop AGC +// +// Tests the STM32 outer-loop AGC class that adjusts ADAR1000 VGA gain based +// on the FPGA's saturation flag. Uses the existing HAL mock/spy framework. +// +// Build: c++ -std=c++17 ... (see Makefile TESTS_WITH_CXX rule) + +#include +#include +#include + +// Shim headers override real STM32/diag headers +#include "stm32_hal_mock.h" +#include "ADAR1000_AGC.h" +#include "ADAR1000_Manager.h" + +// --------------------------------------------------------------------------- +// Linker symbols required by ADAR1000_Manager.cpp (pulled in via main.h shim) +// --------------------------------------------------------------------------- +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-55s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +// --------------------------------------------------------------------------- +// Test 1: Default construction matches design spec +// --------------------------------------------------------------------------- +static void test_defaults() +{ + ADAR1000_AGC agc; + + assert(agc.agc_base_gain == 30); // kDefaultRxVgaGain + assert(agc.gain_step_down == 4); + assert(agc.gain_step_up == 1); + assert(agc.min_gain == 0); + assert(agc.max_gain == 127); + assert(agc.holdoff_frames == 4); + assert(agc.enabled == true); + assert(agc.holdoff_counter == 0); + assert(agc.last_saturated == false); + assert(agc.saturation_event_count == 0); + + // All cal offsets zero + for (int i = 0; i < AGC_TOTAL_CHANNELS; ++i) { + assert(agc.cal_offset[i] == 0); + } +} + +// --------------------------------------------------------------------------- +// Test 2: Saturation reduces gain by step_down +// --------------------------------------------------------------------------- +static void test_saturation_reduces_gain() +{ + ADAR1000_AGC agc; + uint8_t initial = agc.agc_base_gain; // 30 + + agc.update(true); // saturation + + assert(agc.agc_base_gain == initial - agc.gain_step_down); // 26 + assert(agc.last_saturated == true); + assert(agc.holdoff_counter == 0); +} + +// --------------------------------------------------------------------------- +// Test 3: Holdoff prevents premature gain-up +// --------------------------------------------------------------------------- +static void test_holdoff_prevents_early_gain_up() +{ + ADAR1000_AGC agc; + agc.update(true); // saturate once -> gain = 26 + uint8_t after_sat = agc.agc_base_gain; + + // Feed (holdoff_frames - 1) clear frames — should NOT increase gain + for (uint8_t i = 0; i < agc.holdoff_frames - 1; ++i) { + agc.update(false); + assert(agc.agc_base_gain == after_sat); + } + + // holdoff_counter should be holdoff_frames - 1 + assert(agc.holdoff_counter == agc.holdoff_frames - 1); +} + +// --------------------------------------------------------------------------- +// Test 4: Recovery after holdoff period +// --------------------------------------------------------------------------- +static void test_recovery_after_holdoff() +{ + ADAR1000_AGC agc; + agc.update(true); // saturate -> gain = 26 + uint8_t after_sat = agc.agc_base_gain; + + // Feed exactly holdoff_frames clear frames + for (uint8_t i = 0; i < agc.holdoff_frames; ++i) { + agc.update(false); + } + + assert(agc.agc_base_gain == after_sat + agc.gain_step_up); // 27 + assert(agc.holdoff_counter == 0); // reset after recovery +} + +// --------------------------------------------------------------------------- +// Test 5: Min gain clamping +// --------------------------------------------------------------------------- +static void test_min_gain_clamp() +{ + ADAR1000_AGC agc; + agc.min_gain = 10; + agc.agc_base_gain = 12; + agc.gain_step_down = 4; + + agc.update(true); // 12 - 4 = 8, but min = 10 + assert(agc.agc_base_gain == 10); + + agc.update(true); // already at min + assert(agc.agc_base_gain == 10); +} + +// --------------------------------------------------------------------------- +// Test 6: Max gain clamping +// --------------------------------------------------------------------------- +static void test_max_gain_clamp() +{ + ADAR1000_AGC agc; + agc.max_gain = 32; + agc.agc_base_gain = 31; + agc.gain_step_up = 2; + agc.holdoff_frames = 1; // immediate recovery + + agc.update(false); // 31 + 2 = 33, but max = 32 + assert(agc.agc_base_gain == 32); + + agc.update(false); // already at max + assert(agc.agc_base_gain == 32); +} + +// --------------------------------------------------------------------------- +// Test 7: Per-channel calibration offsets +// --------------------------------------------------------------------------- +static void test_calibration_offsets() +{ + ADAR1000_AGC agc; + agc.agc_base_gain = 30; + agc.min_gain = 0; + agc.max_gain = 60; + + agc.cal_offset[0] = 5; // 30 + 5 = 35 + agc.cal_offset[1] = -10; // 30 - 10 = 20 + agc.cal_offset[15] = 40; // 30 + 40 = 60 (clamped to max) + + assert(agc.effectiveGain(0) == 35); + assert(agc.effectiveGain(1) == 20); + assert(agc.effectiveGain(15) == 60); // clamped to max_gain + + // Negative clamp + agc.cal_offset[2] = -50; // 30 - 50 = -20, clamped to min_gain = 0 + assert(agc.effectiveGain(2) == 0); + + // Out-of-range index returns min_gain + assert(agc.effectiveGain(16) == agc.min_gain); +} + +// --------------------------------------------------------------------------- +// Test 8: Disabled AGC is a no-op +// --------------------------------------------------------------------------- +static void test_disabled_noop() +{ + ADAR1000_AGC agc; + agc.enabled = false; + uint8_t original = agc.agc_base_gain; + + agc.update(true); // should be ignored + assert(agc.agc_base_gain == original); + assert(agc.last_saturated == false); // not updated when disabled + assert(agc.saturation_event_count == 0); + + agc.update(false); // also ignored + assert(agc.agc_base_gain == original); +} + +// --------------------------------------------------------------------------- +// Test 9: applyGain() produces correct SPI writes +// --------------------------------------------------------------------------- +static void test_apply_gain_spi() +{ + spy_reset(); + + ADAR1000Manager mgr; // creates 4 devices + ADAR1000_AGC agc; + agc.agc_base_gain = 42; + + agc.applyGain(mgr); + + // Each channel: adarSetRxVgaGain -> adarWrite(gain) + adarWrite(LOAD_WORKING) + // Each adarWrite: CS_low (GPIO_WRITE) + SPI_TRANSMIT + CS_high (GPIO_WRITE) + // = 3 spy records per adarWrite + // = 6 spy records per channel + // = 16 channels * 6 = 96 total spy records + + // Verify SPI transmit count: 2 SPI calls per channel * 16 channels = 32 + int spi_count = spy_count_type(SPY_SPI_TRANSMIT); + assert(spi_count == 32); + + // Verify GPIO write count: 4 GPIO writes per channel (CS low + CS high for each of 2 adarWrite calls) + int gpio_writes = spy_count_type(SPY_GPIO_WRITE); + assert(gpio_writes == 64); // 16 ch * 2 adarWrite * 2 GPIO each +} + +// --------------------------------------------------------------------------- +// Test 10: resetState() clears counters but preserves config +// --------------------------------------------------------------------------- +static void test_reset_preserves_config() +{ + ADAR1000_AGC agc; + agc.agc_base_gain = 42; + agc.gain_step_down = 8; + agc.cal_offset[3] = -5; + + // Generate some state + agc.update(true); + agc.update(true); + assert(agc.saturation_event_count == 2); + assert(agc.last_saturated == true); + + agc.resetState(); + + // State cleared + assert(agc.holdoff_counter == 0); + assert(agc.last_saturated == false); + assert(agc.saturation_event_count == 0); + + // Config preserved + assert(agc.agc_base_gain == 42 - 8 - 8); // two saturations applied before reset + assert(agc.gain_step_down == 8); + assert(agc.cal_offset[3] == -5); +} + +// --------------------------------------------------------------------------- +// Test 11: Saturation counter increments correctly +// --------------------------------------------------------------------------- +static void test_saturation_counter() +{ + ADAR1000_AGC agc; + + for (int i = 0; i < 10; ++i) { + agc.update(true); + } + assert(agc.saturation_event_count == 10); + + // Clear frames don't increment saturation count + for (int i = 0; i < 5; ++i) { + agc.update(false); + } + assert(agc.saturation_event_count == 10); +} + +// --------------------------------------------------------------------------- +// Test 12: Mixed saturation/clear sequence +// --------------------------------------------------------------------------- +static void test_mixed_sequence() +{ + ADAR1000_AGC agc; + agc.agc_base_gain = 30; + agc.gain_step_down = 4; + agc.gain_step_up = 1; + agc.holdoff_frames = 3; + + // Saturate: 30 -> 26 + agc.update(true); + assert(agc.agc_base_gain == 26); + assert(agc.holdoff_counter == 0); + + // 2 clear frames (not enough for recovery) + agc.update(false); + agc.update(false); + assert(agc.agc_base_gain == 26); + assert(agc.holdoff_counter == 2); + + // Saturate again: 26 -> 22, counter resets + agc.update(true); + assert(agc.agc_base_gain == 22); + assert(agc.holdoff_counter == 0); + assert(agc.saturation_event_count == 2); + + // 3 clear frames -> recovery: 22 -> 23 + agc.update(false); + agc.update(false); + agc.update(false); + assert(agc.agc_base_gain == 23); + assert(agc.holdoff_counter == 0); + + // 3 more clear -> 23 -> 24 + agc.update(false); + agc.update(false); + agc.update(false); + assert(agc.agc_base_gain == 24); +} + +// --------------------------------------------------------------------------- +// Test 13: Effective gain with edge-case base_gain values +// --------------------------------------------------------------------------- +static void test_effective_gain_edge_cases() +{ + ADAR1000_AGC agc; + agc.min_gain = 5; + agc.max_gain = 250; + + // Base gain at zero with positive offset + agc.agc_base_gain = 0; + agc.cal_offset[0] = 3; + assert(agc.effectiveGain(0) == 5); // 0 + 3 = 3, clamped to min_gain=5 + + // Base gain at max with zero offset + agc.agc_base_gain = 250; + agc.cal_offset[0] = 0; + assert(agc.effectiveGain(0) == 250); + + // Base gain at max with positive offset -> clamped + agc.agc_base_gain = 250; + agc.cal_offset[0] = 10; + assert(agc.effectiveGain(0) == 250); // clamped to max_gain +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- +int main() +{ + printf("=== ADAR1000_AGC Outer-Loop Unit Tests ===\n"); + + RUN_TEST(test_defaults); + RUN_TEST(test_saturation_reduces_gain); + RUN_TEST(test_holdoff_prevents_early_gain_up); + RUN_TEST(test_recovery_after_holdoff); + RUN_TEST(test_min_gain_clamp); + RUN_TEST(test_max_gain_clamp); + RUN_TEST(test_calibration_offsets); + RUN_TEST(test_disabled_noop); + RUN_TEST(test_apply_gain_spi); + RUN_TEST(test_reset_preserves_config); + RUN_TEST(test_saturation_counter); + RUN_TEST(test_mixed_sequence); + RUN_TEST(test_effective_gain_edge_cases); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +}