feat: AGC phases 4-5 — STM32 outer-loop AGC class + main.cpp integration

Implements the STM32 outer-loop AGC (ADAR1000_AGC) that reads the FPGA
saturation flag on DIG_5/PD13 once per radar frame and adjusts the
ADAR1000 VGA common gain across all 16 RX channels.

Phase 4 — ADAR1000_AGC class (new files):
- ADAR1000_AGC.h/.cpp: attack/recovery/holdoff logic, per-channel
  calibration offsets, effectiveGain() with OOB safety
- test_agc_outer_loop.cpp: 13 tests covering saturation, holdoff,
  recovery, clamping, calibration, SPI spy, reset, mixed sequences

Phase 5 — main.cpp integration:
- Added #include and global outerAgc instance
- AGC update+applyGain call between runRadarPulseSequence() and
  HAL_IWDG_Refresh() in main loop

Build system & shim fixes:
- Makefile: added CXX/CXXFLAGS, C++ object rules, TESTS_WITH_CXX in
  ALL_TESTS (21 total tests)
- stm32_hal_mock.h: const uint8_t* for HAL_UART_Transmit (C++ compat),
  __NOP() macro for host builds
- shims/main.h + real main.h: FPGA_DIG5_SAT pin defines

All tests passing: MCU 21/21, GUI 92/92, cross-layer 29/29.
This commit is contained in:
Jason
2026-04-13 20:14:31 +05:45
parent ffba27a10a
commit 666527fa7d
9 changed files with 637 additions and 3 deletions
@@ -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 <cstring>
// ---------------------------------------------------------------------------
// 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<int16_t>(agc_base_gain) + cal_offset[channel_index];
if (raw < static_cast<int16_t>(min_gain))
return min_gain;
if (raw > static_cast<int16_t>(max_gain))
return max_gain;
return static_cast<uint8_t>(raw);
}
@@ -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 <stdint.h>
// 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
@@ -23,6 +23,7 @@
#include "usbd_cdc_if.h" #include "usbd_cdc_if.h"
#include "adar1000.h" #include "adar1000.h"
#include "ADAR1000_Manager.h" #include "ADAR1000_Manager.h"
#include "ADAR1000_AGC.h"
extern "C" { extern "C" {
#include "ad9523.h" #include "ad9523.h"
} }
@@ -224,6 +225,7 @@ extern SPI_HandleTypeDef hspi4;
//ADAR1000 //ADAR1000
ADAR1000Manager adarManager; ADAR1000Manager adarManager;
ADAR1000_AGC outerAgc;
static uint8_t matrix1[15][16]; static uint8_t matrix1[15][16];
static uint8_t matrix2[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}; 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(); 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 /* [GAP-3 FIX 2] Kick hardware watchdog — if we don't reach here within
* ~4 s, the IWDG resets the MCU automatically. */ * ~4 s, the IWDG resets the MCU automatically. */
HAL_IWDG_Refresh(&hiwdg); HAL_IWDG_Refresh(&hiwdg);
@@ -141,6 +141,15 @@ void Error_Handler(void);
#define EN_DIS_RFPA_VDD_GPIO_Port GPIOD #define EN_DIS_RFPA_VDD_GPIO_Port GPIOD
#define EN_DIS_COOLING_Pin GPIO_PIN_7 #define EN_DIS_COOLING_Pin GPIO_PIN_7
#define EN_DIS_COOLING_GPIO_Port GPIOD #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_Pin GPIO_PIN_9
#define ADF4382_RX_CE_GPIO_Port GPIOG #define ADF4382_RX_CE_GPIO_Port GPIOG
#define ADF4382_RX_CS_Pin GPIO_PIN_10 #define ADF4382_RX_CS_Pin GPIO_PIN_10
+29 -1
View File
@@ -16,10 +16,17 @@
################################################################################ ################################################################################
CC := cc CC := cc
CXX := c++
CFLAGS := -std=c11 -Wall -Wextra -Wno-unused-parameter -g -O0 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 # Shim headers come FIRST so they override real headers
INCLUDES := -Ishims -I. -I../9_1_1_C_Cpp_Libraries 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 source files compiled against mock headers
REAL_SRC := ../9_1_1_C_Cpp_Libraries/adf4382a_manager.c 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 that need platform_noos_stm32.o + mocks
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only 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 \ .PHONY: all build test clean \
$(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \ $(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) $(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@ $(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 --- # --- Individual test targets ---
test_bug1: test_bug1_timed_sync_init_ordering test_bug1: test_bug1_timed_sync_init_ordering
@@ -129,6 +129,14 @@ void Error_Handler(void);
#define GYR_INT_Pin GPIO_PIN_8 #define GYR_INT_Pin GPIO_PIN_8
#define GYR_INT_GPIO_Port GPIOC #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 #ifdef __cplusplus
} }
#endif #endif
@@ -175,7 +175,7 @@ void HAL_Delay(uint32_t Delay)
mock_tick += 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) uint16_t Size, uint32_t Timeout)
{ {
spy_push((SpyRecord){ spy_push((SpyRecord){
@@ -34,6 +34,10 @@ typedef uint32_t HAL_StatusTypeDef;
#define HAL_MAX_DELAY 0xFFFFFFFFU #define HAL_MAX_DELAY 0xFFFFFFFFU
#ifndef __NOP
#define __NOP() ((void)0)
#endif
/* ========================= GPIO Types ============================ */ /* ========================= GPIO Types ============================ */
typedef struct { 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); void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
uint32_t HAL_GetTick(void); uint32_t HAL_GetTick(void);
void HAL_Delay(uint32_t Delay); 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 ============================== */ /* ========================= SPI stubs ============================== */
@@ -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 <cassert>
#include <cstdio>
#include <cstring>
// 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;
}