diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp index 0e7c906..0bdc73c 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp @@ -10,15 +10,15 @@ extern SPI_HandleTypeDef hspi1; extern UART_HandleTypeDef huart3; // Chip Select GPIO definitions -static const struct { - GPIO_TypeDef* port; - uint16_t pin; -} CHIP_SELECTS[4] = { - {ADAR_1_CS_3V3_GPIO_Port, ADAR_1_CS_3V3_Pin}, // ADAR1000 #1 - {ADAR_2_CS_3V3_GPIO_Port, ADAR_2_CS_3V3_Pin}, // ADAR1000 #2 - {ADAR_3_CS_3V3_GPIO_Port, ADAR_3_CS_3V3_Pin}, // ADAR1000 #3 - {ADAR_4_CS_3V3_GPIO_Port, ADAR_4_CS_3V3_Pin} // ADAR1000 #4 -}; +static const struct { + GPIO_TypeDef* port; + uint16_t pin; +} CHIP_SELECTS[4] = { + {ADAR_1_CS_3V3_GPIO_Port, ADAR_1_CS_3V3_Pin}, // ADAR1000 #1 + {ADAR_2_CS_3V3_GPIO_Port, ADAR_2_CS_3V3_Pin}, // ADAR1000 #2 + {ADAR_3_CS_3V3_GPIO_Port, ADAR_3_CS_3V3_Pin}, // ADAR1000 #3 + {ADAR_4_CS_3V3_GPIO_Port, ADAR_4_CS_3V3_Pin} // ADAR1000 #4 +}; // ADAR1000 Vector Modulator lookup tables (128-state phase grid, 2.8125 deg step). // @@ -124,7 +124,10 @@ bool ADAR1000Manager::powerUpSystem() { // Start in RX mode DIAG("BF", "Setting initial mode to RX"); - switchToRXMode(); + if (!switchToRXMode()) { + DIAG_ERR("BF", "Initial switchToRXMode() FAILED -- leaving in last-known mode"); + return false; + } DIAG_ELAPSED("BF", "powerUpSystem() total", t0); const uint8_t success[] = "System Power-Up Sequence Completed Successfully.\r\n"; @@ -135,7 +138,11 @@ bool ADAR1000Manager::powerUpSystem() { bool ADAR1000Manager::powerDownSystem() { DIAG_SECTION("BF POWER-DOWN SEQUENCE"); DIAG("BF", "Switching to RX mode before power-down"); - switchToRXMode(); + // Note: even if RX-mode switch fails partially, we still cut the rails + // below. Power-down must always proceed -- a stuck PA bias would be + // worse than losing the RX-mode telemetry. We capture the status to + // return at the end. + bool rx_ok = switchToRXMode(); HAL_Delay(10); DIAG("BF", "Disabling PA supplies"); @@ -147,95 +154,119 @@ bool ADAR1000Manager::powerDownSystem() { DIAG("BF", "Disabling VDD_SW rail"); HAL_GPIO_WritePin(EN_P_3V3_VDD_SW_GPIO_Port, EN_P_3V3_VDD_SW_Pin, GPIO_PIN_RESET); - DIAG("BF", "powerDownSystem() complete"); - return true; + DIAG("BF", "powerDownSystem() %s", rx_ok ? "complete" : "complete (RX-mode setup had failures, rails cut anyway)"); + return rx_ok; } // Mode Switching -void ADAR1000Manager::switchToTXMode() { +bool ADAR1000Manager::switchToTXMode() { DIAG_SECTION("BF SWITCH TO TX MODE"); + bool ok = true; DIAG("BF", "Step 1: LNA bias OFF"); - setLNABias(false); + ok = setLNABias(false) && ok; delayUs(10); DIAG("BF", "Step 2: Enable PA supplies"); enablePASupplies(); delayUs(100); DIAG("BF", "Step 3: PA bias ON"); - setPABias(true); + ok = setPABias(true) && ok; delayUs(50); DIAG("BF", "Step 4: ADTR1107 -> TX"); - setADTR1107Control(true); + ok = setADTR1107Control(true) && ok; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_RX_ENABLES, 0x00, BROADCAST_OFF); - adarWrite(dev, REG_TX_ENABLES, 0x0F, BROADCAST_OFF); - adarSetTxBias(dev, BROADCAST_OFF); - devices_[dev]->current_mode = BeamDirection::TX; - DIAG("BF", " dev[%u] TX enables=0x0F, TX bias set", dev); + bool dev_ok = true; + dev_ok = adarWrite(dev, REG_RX_ENABLES, 0x00, BROADCAST_OFF) && dev_ok; + dev_ok = adarWrite(dev, REG_TX_ENABLES, 0x0F, BROADCAST_OFF) && dev_ok; + dev_ok = adarSetTxBias(dev, BROADCAST_OFF) && dev_ok; + if (dev_ok) { + devices_[dev]->current_mode = BeamDirection::TX; + DIAG("BF", " dev[%u] TX enables=0x0F, TX bias set", dev); + } else { + DIAG_ERR("BF", " dev[%u] TX setup FAILED -- per-device current_mode unchanged", dev); + ok = false; + } } - current_mode_ = BeamDirection::TX; - DIAG("BF", "switchToTXMode() complete"); + if (ok) current_mode_ = BeamDirection::TX; + DIAG("BF", "switchToTXMode() %s", ok ? "complete" : "completed WITH FAILURES (mode unchanged)"); + return ok; } -void ADAR1000Manager::switchToRXMode() { +bool ADAR1000Manager::switchToRXMode() { DIAG_SECTION("BF SWITCH TO RX MODE"); + bool ok = true; DIAG("BF", "Step 1: PA bias OFF"); - setPABias(false); + ok = setPABias(false) && ok; delayUs(50); DIAG("BF", "Step 2: Disable PA supplies"); disablePASupplies(); delayUs(10); DIAG("BF", "Step 3: ADTR1107 -> RX"); - setADTR1107Control(false); + ok = setADTR1107Control(false) && ok; DIAG("BF", "Step 4: Enable LNA supplies"); enableLNASupplies(); delayUs(50); DIAG("BF", "Step 5: LNA bias ON"); - setLNABias(true); + ok = setLNABias(true) && ok; delayUs(50); for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_TX_ENABLES, 0x00, BROADCAST_OFF); - adarWrite(dev, REG_RX_ENABLES, 0x0F, BROADCAST_OFF); - devices_[dev]->current_mode = BeamDirection::RX; - DIAG("BF", " dev[%u] RX enables=0x0F", dev); + bool dev_ok = true; + dev_ok = adarWrite(dev, REG_TX_ENABLES, 0x00, BROADCAST_OFF) && dev_ok; + dev_ok = adarWrite(dev, REG_RX_ENABLES, 0x0F, BROADCAST_OFF) && dev_ok; + if (dev_ok) { + devices_[dev]->current_mode = BeamDirection::RX; + DIAG("BF", " dev[%u] RX enables=0x0F", dev); + } else { + DIAG_ERR("BF", " dev[%u] RX setup FAILED -- per-device current_mode unchanged", dev); + ok = false; + } } - current_mode_ = BeamDirection::RX; - DIAG("BF", "switchToRXMode() complete"); + if (ok) current_mode_ = BeamDirection::RX; + DIAG("BF", "switchToRXMode() %s", ok ? "complete" : "completed WITH FAILURES (mode unchanged)"); + return ok; } -void ADAR1000Manager::fastTXMode() { +bool ADAR1000Manager::fastTXMode() { DIAG("BF", "fastTXMode(): ADTR1107 -> TX (no bias sequencing)"); - setADTR1107Control(true); + bool ok = setADTR1107Control(true); for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_RX_ENABLES, 0x00, BROADCAST_OFF); - adarWrite(dev, REG_TX_ENABLES, 0x0F, BROADCAST_OFF); - devices_[dev]->current_mode = BeamDirection::TX; + bool dev_ok = true; + dev_ok = adarWrite(dev, REG_RX_ENABLES, 0x00, BROADCAST_OFF) && dev_ok; + dev_ok = adarWrite(dev, REG_TX_ENABLES, 0x0F, BROADCAST_OFF) && dev_ok; + if (dev_ok) devices_[dev]->current_mode = BeamDirection::TX; + ok = dev_ok && ok; } - current_mode_ = BeamDirection::TX; + if (ok) current_mode_ = BeamDirection::TX; + return ok; } -void ADAR1000Manager::fastRXMode() { +bool ADAR1000Manager::fastRXMode() { DIAG("BF", "fastRXMode(): ADTR1107 -> RX (no bias sequencing)"); - setADTR1107Control(false); + bool ok = setADTR1107Control(false); for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_TX_ENABLES, 0x00, BROADCAST_OFF); - adarWrite(dev, REG_RX_ENABLES, 0x0F, BROADCAST_OFF); - devices_[dev]->current_mode = BeamDirection::RX; + bool dev_ok = true; + dev_ok = adarWrite(dev, REG_TX_ENABLES, 0x00, BROADCAST_OFF) && dev_ok; + dev_ok = adarWrite(dev, REG_RX_ENABLES, 0x0F, BROADCAST_OFF) && dev_ok; + if (dev_ok) devices_[dev]->current_mode = BeamDirection::RX; + ok = dev_ok && ok; } - current_mode_ = BeamDirection::RX; + if (ok) current_mode_ = BeamDirection::RX; + return ok; } -void ADAR1000Manager::pulseTXMode() { +bool ADAR1000Manager::pulseTXMode() { DIAG("BF", "pulseTXMode(): TR switch only"); - setADTR1107Control(true); + bool ok = setADTR1107Control(true); last_switch_time_us_ = HAL_GetTick() * 1000; + return ok; } -void ADAR1000Manager::pulseRXMode() { +bool ADAR1000Manager::pulseRXMode() { DIAG("BF", "pulseRXMode(): TR switch only"); - setADTR1107Control(false); + bool ok = setADTR1107Control(false); last_switch_time_us_ = HAL_GetTick() * 1000; + return ok; } // Beam Steering @@ -247,39 +278,36 @@ bool ADAR1000Manager::setBeamAngle(float angle_degrees, BeamDirection direction) DIAG("BF", " phase[0..3] = %u, %u, %u, %u", phase_settings[0], phase_settings[1], phase_settings[2], phase_settings[3]); - if (direction == BeamDirection::TX) { - setAllDevicesTXMode(); - } else { - setAllDevicesRXMode(); - } + bool ok = (direction == BeamDirection::TX) ? setAllDevicesTXMode() : setAllDevicesRXMode(); - for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - for (uint8_t ch = 0; ch < 4; ++ch) { - if (direction == BeamDirection::TX) { - adarSetTxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); - adarSetTxVgaGain(dev, ch + 1, kDefaultTxVgaGain, BROADCAST_OFF); - } else { - adarSetRxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); - adarSetRxVgaGain(dev, ch + 1, kDefaultRxVgaGain, BROADCAST_OFF); - } - } - } - return true; -} - -bool ADAR1000Manager::setCustomBeamPattern(const uint8_t phase_settings[4], const uint8_t gain_settings[4], BeamDirection direction) { for (uint8_t dev = 0; dev < devices_.size(); ++dev) { for (uint8_t ch = 0; ch < 4; ++ch) { if (direction == BeamDirection::TX) { - adarSetTxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); - adarSetTxVgaGain(dev, ch + 1, gain_settings[ch], BROADCAST_OFF); + ok = adarSetTxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF) && ok; + ok = adarSetTxVgaGain(dev, ch + 1, kDefaultTxVgaGain, BROADCAST_OFF) && ok; } else { - adarSetRxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); - adarSetRxVgaGain(dev, ch + 1, gain_settings[ch], BROADCAST_OFF); + ok = adarSetRxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF) && ok; + ok = adarSetRxVgaGain(dev, ch + 1, kDefaultRxVgaGain, BROADCAST_OFF) && ok; } } } - return true; + return ok; +} + +bool ADAR1000Manager::setCustomBeamPattern(const uint8_t phase_settings[4], const uint8_t gain_settings[4], BeamDirection direction) { + bool ok = true; + for (uint8_t dev = 0; dev < devices_.size(); ++dev) { + for (uint8_t ch = 0; ch < 4; ++ch) { + if (direction == BeamDirection::TX) { + ok = adarSetTxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF) && ok; + ok = adarSetTxVgaGain(dev, ch + 1, gain_settings[ch], BROADCAST_OFF) && ok; + } else { + ok = adarSetRxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF) && ok; + ok = adarSetRxVgaGain(dev, ch + 1, gain_settings[ch], BROADCAST_OFF) && ok; + } + } + } + return ok; } // Beam Sweeping @@ -334,7 +362,17 @@ float ADAR1000Manager::readTemperature(uint8_t deviceIndex) { return -273.15f; } - uint8_t temp_raw = adarAdcRead(deviceIndex, BROADCAST_OFF); + // Snapshot the timeout counter so we can detect a timeout that occurred + // anywhere inside adarAdcRead (start-conv write, polling, output read). + // This keeps the float signature while still giving callers an explicit + // "this reading is invalid" channel via std::isnan(). + uint32_t timeouts_before = comm_stats_.adc_timeouts; + uint8_t temp_raw = adarAdcRead(deviceIndex, BROADCAST_OFF); + if (comm_stats_.adc_timeouts > timeouts_before) { + DIAG_WARN("BF", "readTemperature(dev[%u]): ADC timeout/comm-fail -- returning NaN", deviceIndex); + return std::nanf(""); + } + float temp_c = (temp_raw * 0.5f) - 50.0f; DIAG("BF", "readTemperature(dev[%u]): raw=0x%02X => %.1f C", deviceIndex, temp_raw, (double)temp_c); return temp_c; @@ -346,10 +384,21 @@ bool ADAR1000Manager::verifyDeviceCommunication(uint8_t deviceIndex) { return false; } - uint8_t test_value = 0xA5; - adarWrite(deviceIndex, REG_SCRATCHPAD, test_value, BROADCAST_OFF); + // Distinguish three failure modes that previously all looked the same: + // 1. scratchpad write failed at the SPI layer + // 2. scratchpad read failed at the SPI layer + // 3. value round-tripped but didn't match (real chip mismatch) + constexpr uint8_t test_value = 0xA5; + if (!adarWrite(deviceIndex, REG_SCRATCHPAD, test_value, BROADCAST_OFF)) { + DIAG_ERR("BF", "verifyDeviceComm(dev[%u]): scratchpad WRITE failed", deviceIndex); + return false; + } HAL_Delay(1); - uint8_t readback = adarRead(deviceIndex, REG_SCRATCHPAD); + uint8_t readback = 0; + if (!adarReadChecked(deviceIndex, REG_SCRATCHPAD, &readback)) { + DIAG_ERR("BF", "verifyDeviceComm(dev[%u]): scratchpad READ failed", deviceIndex); + return false; + } bool pass = (readback == test_value); if (pass) { DIAG("BF", "verifyDeviceComm(dev[%u]): scratchpad 0xA5 -> 0x%02X OK", deviceIndex, readback); @@ -363,8 +412,8 @@ uint8_t ADAR1000Manager::readRegister(uint8_t deviceIndex, uint32_t address) { return adarRead(deviceIndex, address); } -void ADAR1000Manager::writeRegister(uint8_t deviceIndex, uint32_t address, uint8_t value) { - adarWrite(deviceIndex, address, value, BROADCAST_OFF); +bool ADAR1000Manager::writeRegister(uint8_t deviceIndex, uint32_t address, uint8_t value) { + return adarWrite(deviceIndex, address, value, BROADCAST_OFF); } // Configuration @@ -412,7 +461,10 @@ bool ADAR1000Manager::initializeAllDevices() { } DIAG("BF", "All 4 ADAR1000 devices initialized, setting TX mode"); - setAllDevicesTXMode(); + if (!setAllDevicesTXMode()) { + DIAG_ERR("BF", "initializeAllDevices: setAllDevicesTXMode() failed"); + return false; + } return true; } @@ -420,23 +472,38 @@ bool ADAR1000Manager::initializeSingleDevice(uint8_t deviceIndex) { if (deviceIndex >= devices_.size()) return false; DIAG("BF", " dev[%u] soft reset", deviceIndex); - adarSoftReset(deviceIndex); + if (!adarSoftReset(deviceIndex)) { + DIAG_ERR("BF", " dev[%u] soft reset FAILED -- aborting init", deviceIndex); + return false; + } HAL_Delay(10); DIAG("BF", " dev[%u] write ConfigA (SDO_ACTIVE)", deviceIndex); - adarWriteConfigA(deviceIndex, INTERFACE_CONFIG_A_SDO_ACTIVE, BROADCAST_OFF); + if (!adarWriteConfigA(deviceIndex, INTERFACE_CONFIG_A_SDO_ACTIVE, BROADCAST_OFF)) { + DIAG_ERR("BF", " dev[%u] ConfigA write FAILED -- aborting init", deviceIndex); + return false; + } DIAG("BF", " dev[%u] set RAM bypass (bias+beam)", deviceIndex); - adarSetRamBypass(deviceIndex, BROADCAST_OFF); + if (!adarSetRamBypass(deviceIndex, BROADCAST_OFF)) { + DIAG_ERR("BF", " dev[%u] RAM bypass write FAILED -- aborting init", deviceIndex); + return false; + } // Initialize ADC DIAG("BF", " dev[%u] enable ADC (2MHz clk)", deviceIndex); - adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_2MHZ_CLK | ADAR1000_ADC_EN, BROADCAST_OFF); + if (!adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_2MHZ_CLK | ADAR1000_ADC_EN, BROADCAST_OFF)) { + DIAG_ERR("BF", " dev[%u] ADC enable write FAILED -- aborting init", deviceIndex); + return false; + } - // Verify communication with scratchpad test + // Verify communication with scratchpad test. Previous behavior was to log + // a warning and mark the device initialized anyway -- that hid completely + // dead chips behind a green init. Now this is a hard failure. DIAG("BF", " dev[%u] verifying SPI communication...", deviceIndex); - bool comms_ok = verifyDeviceCommunication(deviceIndex); - if (!comms_ok) { - DIAG_WARN("BF", " dev[%u] scratchpad verify FAILED but marking initialized anyway", deviceIndex); + if (!verifyDeviceCommunication(deviceIndex)) { + DIAG_ERR("BF", " dev[%u] scratchpad verify FAILED -- NOT marking initialized", deviceIndex); + devices_[deviceIndex]->initialized = false; + return false; } devices_[deviceIndex]->initialized = true; @@ -466,15 +533,21 @@ bool ADAR1000Manager::initializeADTR1107Sequence() { // Step 4: Set CTRL_SW to RX mode initially via GPIO DIAG("BF", "Step 4: CTRL_SW -> RX (initial safe mode)"); - setADTR1107Control(false); // RX mode + if (!setADTR1107Control(false)) { + DIAG_ERR("BF", "ADTR1107 step 4 FAILED -- aborting power sequence"); + return false; + } HAL_Delay(1); // Step 5: Set VGG_LNA to 0 DIAG("BF", "Step 5: VGG_LNA bias -> OFF (0x%02X)", kLnaBiasOff); uint8_t lna_bias_voltage = kLnaBiasOff; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_LNA_BIAS_ON, lna_bias_voltage, BROADCAST_OFF); - adarWrite(dev, REG_LNA_BIAS_OFF, kLnaBiasOff, BROADCAST_OFF); + if (!adarWrite(dev, REG_LNA_BIAS_ON, lna_bias_voltage, BROADCAST_OFF) || + !adarWrite(dev, REG_LNA_BIAS_OFF, kLnaBiasOff, BROADCAST_OFF)) { + DIAG_ERR("BF", "ADTR1107 step 5 dev[%u] LNA bias write FAILED", dev); + return false; + } } // Step 6: Set VDD_LNA to 0V for TX mode @@ -489,10 +562,14 @@ bool ADAR1000Manager::initializeADTR1107Sequence() { DIAG("BF", "Step 7: VGG_PA -> safe bias 0x%02X (~ -1.75V, PA off)", kPaBiasTxSafe); uint8_t safe_pa_bias = kPaBiasTxSafe; // Safe negative voltage (-1.75V) to keep PA off for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_PA_CH1_BIAS_ON, safe_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH2_BIAS_ON, safe_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH3_BIAS_ON, safe_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH4_BIAS_ON, safe_pa_bias, BROADCAST_OFF); + if (!adarWrite(dev, REG_PA_CH1_BIAS_ON, safe_pa_bias, BROADCAST_OFF) || + !adarWrite(dev, REG_PA_CH2_BIAS_ON, safe_pa_bias, BROADCAST_OFF) || + !adarWrite(dev, REG_PA_CH3_BIAS_ON, safe_pa_bias, BROADCAST_OFF) || + !adarWrite(dev, REG_PA_CH4_BIAS_ON, safe_pa_bias, BROADCAST_OFF)) { + DIAG_ERR("BF", "ADTR1107 step 7 dev[%u] safe PA bias write FAILED -- aborting before enabling supplies", + dev); + return false; + } } HAL_Delay(10); @@ -509,10 +586,13 @@ bool ADAR1000Manager::initializeADTR1107Sequence() { DIAG("BF", "Step 9: VGG_PA -> Idq cal bias 0x%02X (~ -0.24V, target 220mA)", kPaBiasIdqCalibration); uint8_t Idq_pa_bias = kPaBiasIdqCalibration; // Safe negative voltage (-0.2447V) to keep PA off for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_PA_CH1_BIAS_ON, Idq_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH2_BIAS_ON, Idq_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH3_BIAS_ON, Idq_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH4_BIAS_ON, Idq_pa_bias, BROADCAST_OFF); + if (!adarWrite(dev, REG_PA_CH1_BIAS_ON, Idq_pa_bias, BROADCAST_OFF) || + !adarWrite(dev, REG_PA_CH2_BIAS_ON, Idq_pa_bias, BROADCAST_OFF) || + !adarWrite(dev, REG_PA_CH3_BIAS_ON, Idq_pa_bias, BROADCAST_OFF) || + !adarWrite(dev, REG_PA_CH4_BIAS_ON, Idq_pa_bias, BROADCAST_OFF)) { + DIAG_ERR("BF", "ADTR1107 step 9 dev[%u] Idq cal bias write FAILED", dev); + return false; + } } HAL_Delay(10); @@ -522,166 +602,179 @@ bool ADAR1000Manager::initializeADTR1107Sequence() { HAL_UART_Transmit(&huart3, success, sizeof(success) - 1, 1000); return true; -} +} bool ADAR1000Manager::setAllDevicesTXMode() { DIAG("BF", "setAllDevicesTXMode(): ADTR1107 -> TX, then configure ADAR1000s"); - // Set ADTR1107 to TX mode first - setADTR1107Mode(BeamDirection::TX); - - // Then configure ADAR1000 for TX - for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - // Disable RX first - adarWrite(dev, REG_RX_ENABLES, 0x00, BROADCAST_OFF); - - // Enable TX channels and set bias - adarWrite(dev, REG_TX_ENABLES, 0x0F, BROADCAST_OFF); // Enable all 4 channels - adarSetTxBias(dev, BROADCAST_OFF); - - devices_[dev]->current_mode = BeamDirection::TX; - DIAG("BF", " dev[%u] TX mode set (enables=0x0F, bias applied)", dev); + // Set ADTR1107 to TX mode first. If this fails, do NOT advance state -- + // software was previously claiming TX mode while hardware stayed in RX. + if (!setADTR1107Mode(BeamDirection::TX)) { + DIAG_ERR("BF", "setAllDevicesTXMode: ADTR1107 TX setup FAILED -- not updating mode flags"); + return false; } - current_mode_ = BeamDirection::TX; - return true; + + bool ok = true; + for (uint8_t dev = 0; dev < devices_.size(); ++dev) { + bool dev_ok = true; + dev_ok = adarWrite(dev, REG_RX_ENABLES, 0x00, BROADCAST_OFF) && dev_ok; + dev_ok = adarWrite(dev, REG_TX_ENABLES, 0x0F, BROADCAST_OFF) && dev_ok; + dev_ok = adarSetTxBias(dev, BROADCAST_OFF) && dev_ok; + + if (dev_ok) { + devices_[dev]->current_mode = BeamDirection::TX; + DIAG("BF", " dev[%u] TX mode set (enables=0x0F, bias applied)", dev); + } else { + DIAG_ERR("BF", " dev[%u] TX mode setup FAILED -- per-device current_mode unchanged", dev); + ok = false; + } + } + if (ok) { + current_mode_ = BeamDirection::TX; + } else { + DIAG_ERR("BF", "setAllDevicesTXMode: at least one device failed -- global current_mode unchanged"); + } + return ok; } bool ADAR1000Manager::setAllDevicesRXMode() { DIAG("BF", "setAllDevicesRXMode(): ADTR1107 -> RX, then configure ADAR1000s"); - // Set ADTR1107 to RX mode first - setADTR1107Mode(BeamDirection::RX); - - // Then configure ADAR1000 for RX - for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - // Disable TX first - adarWrite(dev, REG_TX_ENABLES, 0x00, BROADCAST_OFF); - - // Enable RX channels - adarWrite(dev, REG_RX_ENABLES, 0x0F, BROADCAST_OFF); // Enable all 4 channels - - devices_[dev]->current_mode = BeamDirection::RX; - DIAG("BF", " dev[%u] RX mode set (enables=0x0F)", dev); + if (!setADTR1107Mode(BeamDirection::RX)) { + DIAG_ERR("BF", "setAllDevicesRXMode: ADTR1107 RX setup FAILED -- not updating mode flags"); + return false; } - current_mode_ = BeamDirection::RX; - return true; + + bool ok = true; + for (uint8_t dev = 0; dev < devices_.size(); ++dev) { + bool dev_ok = true; + dev_ok = adarWrite(dev, REG_TX_ENABLES, 0x00, BROADCAST_OFF) && dev_ok; + dev_ok = adarWrite(dev, REG_RX_ENABLES, 0x0F, BROADCAST_OFF) && dev_ok; + + if (dev_ok) { + devices_[dev]->current_mode = BeamDirection::RX; + DIAG("BF", " dev[%u] RX mode set (enables=0x0F)", dev); + } else { + DIAG_ERR("BF", " dev[%u] RX mode setup FAILED -- per-device current_mode unchanged", dev); + ok = false; + } + } + if (ok) { + current_mode_ = BeamDirection::RX; + } else { + DIAG_ERR("BF", "setAllDevicesRXMode: at least one device failed -- global current_mode unchanged"); + } + return ok; } -void ADAR1000Manager::setADTR1107Mode(BeamDirection direction) { +bool ADAR1000Manager::setADTR1107Mode(BeamDirection direction) { + bool ok = true; if (direction == BeamDirection::TX) { DIAG_SECTION("ADTR1107 -> TX MODE"); - setADTR1107Control(true); // TX mode + ok = setADTR1107Control(true) && ok; - // Step 1: Disable LNA power first DIAG("BF", " Disable LNA supplies"); disableLNASupplies(); HAL_Delay(5); - // Step 2: Set LNA bias to safe off value DIAG("BF", " LNA bias -> OFF (0x%02X)", kLnaBiasOff); for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_LNA_BIAS_ON, kLnaBiasOff, BROADCAST_OFF); // Turn off LNA bias + ok = adarWrite(dev, REG_LNA_BIAS_ON, kLnaBiasOff, BROADCAST_OFF) && ok; } HAL_Delay(5); - // Step 3: Enable PA power DIAG("BF", " Enable PA supplies"); enablePASupplies(); HAL_Delay(10); - // Step 4: Set PA bias to operational value DIAG("BF", " PA bias -> operational (0x%02X)", kPaBiasOperational); - uint8_t operational_pa_bias = kPaBiasOperational; // Maximum bias for full power + uint8_t operational_pa_bias = kPaBiasOperational; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_PA_CH1_BIAS_ON, operational_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH2_BIAS_ON, operational_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH3_BIAS_ON, operational_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH4_BIAS_ON, operational_pa_bias, BROADCAST_OFF); + ok = adarWrite(dev, REG_PA_CH1_BIAS_ON, operational_pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH2_BIAS_ON, operational_pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH3_BIAS_ON, operational_pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH4_BIAS_ON, operational_pa_bias, BROADCAST_OFF) && ok; } HAL_Delay(5); - // Step 5: Set TR switch to TX mode DIAG("BF", " TR switch -> TX (TR_SOURCE=1, BIAS_EN)"); for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarSetBit(dev, REG_SW_CONTROL, 2, BROADCAST_OFF); // TR_SOURCE = 1 (TX) - adarSetBit(dev, REG_MISC_ENABLES, 5, BROADCAST_OFF); // BIAS_EN + ok = adarSetBit(dev, REG_SW_CONTROL, 2, BROADCAST_OFF) && ok; + ok = adarSetBit(dev, REG_MISC_ENABLES, 5, BROADCAST_OFF) && ok; } - DIAG("BF", " ADTR1107 TX mode complete"); + DIAG("BF", " ADTR1107 TX mode %s", ok ? "complete" : "completed WITH FAILURES"); } else { - // RECEIVE MODE: Enable LNA, Disable PA DIAG_SECTION("ADTR1107 -> RX MODE"); - setADTR1107Control(false); // RX mode + ok = setADTR1107Control(false) && ok; - // Step 1: Disable PA power first DIAG("BF", " Disable PA supplies"); disablePASupplies(); HAL_Delay(5); - // Step 2: Set PA bias to safe negative voltage DIAG("BF", " PA bias -> safe (0x%02X)", kPaBiasRxSafe); uint8_t safe_pa_bias = kPaBiasRxSafe; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_PA_CH1_BIAS_ON, safe_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH2_BIAS_ON, safe_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH3_BIAS_ON, safe_pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH4_BIAS_ON, safe_pa_bias, BROADCAST_OFF); + ok = adarWrite(dev, REG_PA_CH1_BIAS_ON, safe_pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH2_BIAS_ON, safe_pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH3_BIAS_ON, safe_pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH4_BIAS_ON, safe_pa_bias, BROADCAST_OFF) && ok; } HAL_Delay(5); - // Step 3: Enable LNA power DIAG("BF", " Enable LNA supplies"); enableLNASupplies(); HAL_Delay(10); - // Step 4: Set LNA bias to operational value DIAG("BF", " LNA bias -> operational (0x%02X)", kLnaBiasOperational); uint8_t operational_lna_bias = kLnaBiasOperational; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_LNA_BIAS_ON, operational_lna_bias, BROADCAST_OFF); + ok = adarWrite(dev, REG_LNA_BIAS_ON, operational_lna_bias, BROADCAST_OFF) && ok; } HAL_Delay(5); - // Step 5: Set TR switch to RX mode DIAG("BF", " TR switch -> RX (TR_SOURCE=0, LNA_BIAS_OUT_EN)"); for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarResetBit(dev, REG_SW_CONTROL, 2, BROADCAST_OFF); // TR_SOURCE = 0 (RX) - adarSetBit(dev, REG_MISC_ENABLES, 4, BROADCAST_OFF); // LNA_BIAS_OUT_EN + ok = adarResetBit(dev, REG_SW_CONTROL, 2, BROADCAST_OFF) && ok; + ok = adarSetBit(dev, REG_MISC_ENABLES, 4, BROADCAST_OFF) && ok; } - DIAG("BF", " ADTR1107 RX mode complete"); + DIAG("BF", " ADTR1107 RX mode %s", ok ? "complete" : "completed WITH FAILURES"); } -} - -void ADAR1000Manager::setADTR1107Control(bool tx_mode) { - DIAG("BF", "setADTR1107Control(%s): setting TR switch on all %u devices, settling %lu us", - tx_mode ? "TX" : "RX", (unsigned)devices_.size(), (unsigned long)switch_settling_time_us_); - for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - setTRSwitchPosition(dev, tx_mode); - } - delayUs(switch_settling_time_us_); + return ok; } -void ADAR1000Manager::setTRSwitchPosition(uint8_t deviceIndex, bool tx_mode) { +bool ADAR1000Manager::setADTR1107Control(bool tx_mode) { + DIAG("BF", "setADTR1107Control(%s): setting TR switch on all %u devices, settling %lu us", + tx_mode ? "TX" : "RX", (unsigned)devices_.size(), (unsigned long)switch_settling_time_us_); + bool ok = true; + for (uint8_t dev = 0; dev < devices_.size(); ++dev) { + ok = setTRSwitchPosition(dev, tx_mode) && ok; + } + delayUs(switch_settling_time_us_); + return ok; +} + +bool ADAR1000Manager::setTRSwitchPosition(uint8_t deviceIndex, bool tx_mode) { if (tx_mode) { // TX mode: Set TR_SOURCE = 1 - adarSetBit(deviceIndex, REG_SW_CONTROL, 2, BROADCAST_OFF); - } else { - // RX mode: Set TR_SOURCE = 0 - adarResetBit(deviceIndex, REG_SW_CONTROL, 2, BROADCAST_OFF); + return adarSetBit(deviceIndex, REG_SW_CONTROL, 2, BROADCAST_OFF); } + // RX mode: Set TR_SOURCE = 0 + return adarResetBit(deviceIndex, REG_SW_CONTROL, 2, BROADCAST_OFF); } // Add the new public method bool ADAR1000Manager::setCustomBeamPattern16(const uint8_t phase_pattern[16], BeamDirection direction) { + bool ok = true; for (uint8_t dev = 0; dev < 4; ++dev) { for (uint8_t ch = 0; ch < 4; ++ch) { uint8_t phase = phase_pattern[dev * 4 + ch]; if (direction == BeamDirection::TX) { - adarSetTxPhase(dev, ch + 1, phase, BROADCAST_OFF); + ok = adarSetTxPhase(dev, ch + 1, phase, BROADCAST_OFF) && ok; } else { - adarSetRxPhase(dev, ch + 1, phase, BROADCAST_OFF); + ok = adarSetRxPhase(dev, ch + 1, phase, BROADCAST_OFF) && ok; } } } - return true; + return ok; } void ADAR1000Manager::enablePASupplies() { @@ -708,26 +801,30 @@ void ADAR1000Manager::disableLNASupplies() { HAL_GPIO_WritePin(EN_P_3V3_ADTR_GPIO_Port, EN_P_3V3_ADTR_Pin, GPIO_PIN_RESET); } -void ADAR1000Manager::setPABias(bool enable) { +bool ADAR1000Manager::setPABias(bool enable) { uint8_t pa_bias = enable ? kPaBiasOperational : kPaBiasRxSafe; // Operational vs safe bias DIAG("BF", "setPABias(%s): bias=0x%02X", enable ? "ON" : "OFF", pa_bias); + bool ok = true; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_PA_CH1_BIAS_ON, pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH2_BIAS_ON, pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH3_BIAS_ON, pa_bias, BROADCAST_OFF); - adarWrite(dev, REG_PA_CH4_BIAS_ON, pa_bias, BROADCAST_OFF); + ok = adarWrite(dev, REG_PA_CH1_BIAS_ON, pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH2_BIAS_ON, pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH3_BIAS_ON, pa_bias, BROADCAST_OFF) && ok; + ok = adarWrite(dev, REG_PA_CH4_BIAS_ON, pa_bias, BROADCAST_OFF) && ok; } + return ok; } -void ADAR1000Manager::setLNABias(bool enable) { +bool ADAR1000Manager::setLNABias(bool enable) { uint8_t lna_bias = enable ? kLnaBiasOperational : kLnaBiasOff; // Operational vs off DIAG("BF", "setLNABias(%s): bias=0x%02X", enable ? "ON" : "OFF", lna_bias); + bool ok = true; for (uint8_t dev = 0; dev < devices_.size(); ++dev) { - adarWrite(dev, REG_LNA_BIAS_ON, lna_bias, BROADCAST_OFF); + ok = adarWrite(dev, REG_LNA_BIAS_ON, lna_bias, BROADCAST_OFF) && ok; } -} + return ok; +} void ADAR1000Manager::delayUs(uint32_t microseconds) { // Simple implementation - for F7 @ 216MHz, each loop ~7 cycles ≈ 0.032us @@ -771,7 +868,11 @@ bool ADAR1000Manager::performSystemCalibration() { // LOW-LEVEL SPI COMMUNICATION METHODS // ============================================================================ -uint32_t ADAR1000Manager::spiTransfer(uint8_t* txData, uint8_t* rxData, uint32_t size) { +void ADAR1000Manager::resetCommStats() { + comm_stats_ = {0, 0, 0, 0, 0, 0xFF}; +} + +bool ADAR1000Manager::spiTransfer(uint8_t* txData, uint8_t* rxData, uint32_t size) { HAL_StatusTypeDef status; if (rxData) { @@ -782,9 +883,9 @@ uint32_t ADAR1000Manager::spiTransfer(uint8_t* txData, uint8_t* rxData, uint32_t if (status != HAL_OK) { DIAG_ERR("BF", "SPI1 transfer FAILED: HAL status=%d, size=%lu", (int)status, (unsigned long)size); + return false; } - - return (status == HAL_OK) ? size : 0; + return true; } void ADAR1000Manager::setChipSelect(uint8_t deviceIndex, bool state) { @@ -794,7 +895,14 @@ void ADAR1000Manager::setChipSelect(uint8_t deviceIndex, bool state) { state ? GPIO_PIN_RESET : GPIO_PIN_SET); } -void ADAR1000Manager::adarWrite(uint8_t deviceIndex, uint32_t mem_addr, uint8_t data, uint8_t broadcast) { +bool ADAR1000Manager::adarWrite(uint8_t deviceIndex, uint32_t mem_addr, uint8_t data, uint8_t broadcast) { + if (deviceIndex >= devices_.size()) { + comm_stats_.writes_fail++; + comm_stats_.last_fail_dev = deviceIndex; + DIAG_ERR("BF", "adarWrite(dev[%u]): index out of range", deviceIndex); + return false; + } + uint8_t instruction[3]; if (broadcast) { @@ -808,16 +916,38 @@ void ADAR1000Manager::adarWrite(uint8_t deviceIndex, uint32_t mem_addr, uint8_t instruction[2] = data; setChipSelect(deviceIndex, true); - spiTransfer(instruction, nullptr, sizeof(instruction)); + bool ok = spiTransfer(instruction, nullptr, sizeof(instruction)); setChipSelect(deviceIndex, false); + + if (ok) { + comm_stats_.writes_ok++; + } else { + comm_stats_.writes_fail++; + comm_stats_.last_fail_dev = deviceIndex; + } + return ok; } -uint8_t ADAR1000Manager::adarRead(uint8_t deviceIndex, uint32_t mem_addr) { - uint8_t instruction[3] = {0}; - uint8_t rx_buffer[3] = {0}; +bool ADAR1000Manager::adarReadChecked(uint8_t deviceIndex, uint32_t mem_addr, uint8_t* out) { + if (out == nullptr) return false; + *out = 0; - // Set SDO active - adarWrite(deviceIndex, REG_INTERFACE_CONFIG_A, INTERFACE_CONFIG_A_SDO_ACTIVE, 0); + if (deviceIndex >= devices_.size()) { + comm_stats_.reads_fail++; + comm_stats_.last_fail_dev = deviceIndex; + DIAG_ERR("BF", "adarRead(dev[%u]): index out of range", deviceIndex); + return false; + } + + uint8_t instruction[3] = {0}; + uint8_t rx_buffer[3] = {0}; + + // Set SDO active. Failure here means we cannot trust the readback that follows. + if (!adarWrite(deviceIndex, REG_INTERFACE_CONFIG_A, INTERFACE_CONFIG_A_SDO_ACTIVE, 0)) { + comm_stats_.reads_fail++; + comm_stats_.last_fail_dev = deviceIndex; + return false; + } instruction[0] = 0x80 | ((devices_[deviceIndex]->dev_addr & 0x03) << 5); instruction[0] |= ((0xff00 & mem_addr) >> 8); @@ -825,28 +955,57 @@ uint8_t ADAR1000Manager::adarRead(uint8_t deviceIndex, uint32_t mem_addr) { instruction[2] = 0x00; setChipSelect(deviceIndex, true); - spiTransfer(instruction, rx_buffer, sizeof(instruction)); + bool ok = spiTransfer(instruction, rx_buffer, sizeof(instruction)); setChipSelect(deviceIndex, false); - // Set SDO Inactive - adarWrite(deviceIndex, REG_INTERFACE_CONFIG_A, 0, 0); + // Best-effort: clear SDO active even if the read above failed. Don't let a + // failure on the trailing write override the read failure status. + bool sdo_off_ok = adarWrite(deviceIndex, REG_INTERFACE_CONFIG_A, 0, 0); + (void)sdo_off_ok; // already counted in writes_*; don't double-count as a read failure. - return rx_buffer[2]; + if (!ok) { + comm_stats_.reads_fail++; + comm_stats_.last_fail_dev = deviceIndex; + return false; + } + + *out = rx_buffer[2]; + comm_stats_.reads_ok++; + return true; } -void ADAR1000Manager::adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { - uint8_t temp = adarRead(deviceIndex, mem_addr); +uint8_t ADAR1000Manager::adarRead(uint8_t deviceIndex, uint32_t mem_addr) { + uint8_t value = 0; + (void)adarReadChecked(deviceIndex, mem_addr, &value); + return value; +} + +bool ADAR1000Manager::adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { + uint8_t temp = 0; + // Critical: read-modify-write must NOT proceed on a failed read, otherwise we + // would write back (0 | mask) and clobber every other bit in the register. + if (!adarReadChecked(deviceIndex, mem_addr, &temp)) { + DIAG_ERR("BF", "adarSetBit(dev[%u], 0x%03lX, bit %u): read failed -- skipping write to avoid corruption", + deviceIndex, (unsigned long)mem_addr, bit); + return false; + } uint8_t data = temp | (1 << bit); - adarWrite(deviceIndex, mem_addr, data, broadcast); + return adarWrite(deviceIndex, mem_addr, data, broadcast); } -void ADAR1000Manager::adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { - uint8_t temp = adarRead(deviceIndex, mem_addr); +bool ADAR1000Manager::adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { + uint8_t temp = 0; + if (!adarReadChecked(deviceIndex, mem_addr, &temp)) { + DIAG_ERR("BF", "adarResetBit(dev[%u], 0x%03lX, bit %u): read failed -- skipping write to avoid corruption", + deviceIndex, (unsigned long)mem_addr, bit); + return false; + } uint8_t data = temp & ~(1 << bit); - adarWrite(deviceIndex, mem_addr, data, broadcast); + return adarWrite(deviceIndex, mem_addr, data, broadcast); } -void ADAR1000Manager::adarSoftReset(uint8_t deviceIndex) { +bool ADAR1000Manager::adarSoftReset(uint8_t deviceIndex) { + if (deviceIndex >= devices_.size()) return false; DIAG("BF", "adarSoftReset(dev[%u]): addr=0x%02X", deviceIndex, devices_[deviceIndex]->dev_addr); uint8_t instruction[3]; instruction[0] = ((devices_[deviceIndex]->dev_addr & 0x03) << 5); @@ -854,20 +1013,28 @@ void ADAR1000Manager::adarSoftReset(uint8_t deviceIndex) { instruction[2] = 0x81; setChipSelect(deviceIndex, true); - spiTransfer(instruction, nullptr, sizeof(instruction)); + bool ok = spiTransfer(instruction, nullptr, sizeof(instruction)); setChipSelect(deviceIndex, false); + + if (ok) { + comm_stats_.writes_ok++; + } else { + comm_stats_.writes_fail++; + comm_stats_.last_fail_dev = deviceIndex; + } + return ok; } -void ADAR1000Manager::adarWriteConfigA(uint8_t deviceIndex, uint8_t flags, uint8_t broadcast) { - adarWrite(deviceIndex, REG_INTERFACE_CONFIG_A, flags, broadcast); +bool ADAR1000Manager::adarWriteConfigA(uint8_t deviceIndex, uint8_t flags, uint8_t broadcast) { + return adarWrite(deviceIndex, REG_INTERFACE_CONFIG_A, flags, broadcast); } -void ADAR1000Manager::adarSetRamBypass(uint8_t deviceIndex, uint8_t broadcast) { +bool ADAR1000Manager::adarSetRamBypass(uint8_t deviceIndex, uint8_t broadcast) { uint8_t data = (MEM_CTRL_BIAS_RAM_BYPASS | MEM_CTRL_BEAM_RAM_BYPASS); - adarWrite(deviceIndex, REG_MEM_CTL, data, broadcast); + return adarWrite(deviceIndex, REG_MEM_CTL, data, broadcast); } -void ADAR1000Manager::adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast) { +bool ADAR1000Manager::adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast) { // channel is 1-based (CH1..CH4) per API contract documented in // ADAR1000_AGC.cpp and matching ADI datasheet terminology. // Reject out-of-range early so a stale 0-based caller does not @@ -875,7 +1042,7 @@ void ADAR1000Manager::adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8 // See issue #90. if (channel < 1 || channel > 4) { DIAG("BF", "adarSetRxPhase: channel %u out of range [1..4], ignored", channel); - return; + return false; } uint8_t i_val = VM_I[phase % 128]; uint8_t q_val = VM_Q[phase % 128]; @@ -885,16 +1052,17 @@ void ADAR1000Manager::adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8 uint32_t mem_addr_i = REG_CH1_RX_PHS_I + ((channel - 1) & 0x03) * 2; uint32_t mem_addr_q = REG_CH1_RX_PHS_Q + ((channel - 1) & 0x03) * 2; - adarWrite(deviceIndex, mem_addr_i, i_val, broadcast); - adarWrite(deviceIndex, mem_addr_q, q_val, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast); + bool ok = adarWrite(deviceIndex, mem_addr_i, i_val, broadcast); + ok = adarWrite(deviceIndex, mem_addr_q, q_val, broadcast) && ok; + ok = adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast) && ok; + return ok; } -void ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast) { +bool ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast) { // channel is 1-based (CH1..CH4). See issue #90. if (channel < 1 || channel > 4) { DIAG("BF", "adarSetTxPhase: channel %u out of range [1..4], ignored", channel); - return; + return false; } uint8_t i_val = VM_I[phase % 128]; uint8_t q_val = VM_Q[phase % 128]; @@ -902,55 +1070,76 @@ void ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8 uint32_t mem_addr_i = REG_CH1_TX_PHS_I + ((channel - 1) & 0x03) * 2; uint32_t mem_addr_q = REG_CH1_TX_PHS_Q + ((channel - 1) & 0x03) * 2; - adarWrite(deviceIndex, mem_addr_i, i_val, broadcast); - adarWrite(deviceIndex, mem_addr_q, q_val, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast); + bool ok = adarWrite(deviceIndex, mem_addr_i, i_val, broadcast); + ok = adarWrite(deviceIndex, mem_addr_q, q_val, broadcast) && ok; + ok = adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast) && ok; + return ok; } -void ADAR1000Manager::adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) { +bool ADAR1000Manager::adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) { // channel is 1-based (CH1..CH4). See issue #90. if (channel < 1 || channel > 4) { DIAG("BF", "adarSetRxVgaGain: channel %u out of range [1..4], ignored", channel); - return; + return false; } uint32_t mem_addr = REG_CH1_RX_GAIN + ((channel - 1) & 0x03); - adarWrite(deviceIndex, mem_addr, gain, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast); + bool ok = adarWrite(deviceIndex, mem_addr, gain, broadcast); + ok = adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast) && ok; + return ok; } -void ADAR1000Manager::adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) { +bool ADAR1000Manager::adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) { // channel is 1-based (CH1..CH4). See issue #90. if (channel < 1 || channel > 4) { DIAG("BF", "adarSetTxVgaGain: channel %u out of range [1..4], ignored", channel); - return; + return false; } uint32_t mem_addr = REG_CH1_TX_GAIN + ((channel - 1) & 0x03); - adarWrite(deviceIndex, mem_addr, gain, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast); + bool ok = adarWrite(deviceIndex, mem_addr, gain, broadcast); + ok = adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast) && ok; + return ok; } -void ADAR1000Manager::adarSetTxBias(uint8_t deviceIndex, uint8_t broadcast) { - adarWrite(deviceIndex, REG_BIAS_CURRENT_TX, kTxBiasCurrent, broadcast); - adarWrite(deviceIndex, REG_BIAS_CURRENT_TX_DRV, kTxDriverBiasCurrent, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, 0x2, broadcast); -} +bool ADAR1000Manager::adarSetTxBias(uint8_t deviceIndex, uint8_t broadcast) { + bool ok = adarWrite(deviceIndex, REG_BIAS_CURRENT_TX, kTxBiasCurrent, broadcast); + ok = adarWrite(deviceIndex, REG_BIAS_CURRENT_TX_DRV, kTxDriverBiasCurrent, broadcast) && ok; + ok = adarWrite(deviceIndex, REG_LOAD_WORKING, 0x2, broadcast) && ok; + return ok; +} uint8_t ADAR1000Manager::adarAdcRead(uint8_t deviceIndex, uint8_t broadcast) { - adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_ST_CONV, broadcast); + if (!adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_ST_CONV, broadcast)) { + DIAG_ERR("BF", "adarAdcRead(dev[%u]): ADC start-conversion write failed", deviceIndex); + comm_stats_.adc_timeouts++; // treat as a "no-result" event for caller observability + return 0; + } - // Wait for conversion -- WARNING: no timeout, can hang if ADC never completes uint32_t t0 = HAL_GetTick(); uint32_t polls = 0; - while (!(adarRead(deviceIndex, REG_ADC_CONTROL) & 0x01)) { + uint8_t ctrl = 0; + while (true) { + if (!adarReadChecked(deviceIndex, REG_ADC_CONTROL, &ctrl)) { + DIAG_ERR("BF", "adarAdcRead(dev[%u]): ADC poll read failed", deviceIndex); + comm_stats_.adc_timeouts++; + return 0; + } + if (ctrl & 0x01) break; polls++; if (HAL_GetTick() - t0 > 100) { DIAG_ERR("BF", "adarAdcRead(dev[%u]): ADC conversion TIMEOUT after %lu ms, %lu polls", deviceIndex, (unsigned long)(HAL_GetTick() - t0), (unsigned long)polls); + comm_stats_.adc_timeouts++; return 0; } } DIAG("BF", "adarAdcRead(dev[%u]): conversion done in %lu ms (%lu polls)", deviceIndex, (unsigned long)(HAL_GetTick() - t0), (unsigned long)polls); - return adarRead(deviceIndex, REG_ADC_OUT); + uint8_t out = 0; + if (!adarReadChecked(deviceIndex, REG_ADC_OUT, &out)) { + DIAG_ERR("BF", "adarAdcRead(dev[%u]): ADC output read failed", deviceIndex); + comm_stats_.adc_timeouts++; + return 0; + } + return out; } diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.h b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.h index ae3d570..a5bfced 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.h +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.h @@ -36,6 +36,19 @@ public: } }; + // Communication health counters. Incremented by checked SPI helpers; queryable + // for telemetry/observability without changing the boolean public contract. + // PR2 may promote a richer OpStatus enum; today the policy is bool returns + // for control flow + this struct for trends. + struct CommStats { + uint32_t writes_ok; + uint32_t writes_fail; + uint32_t reads_ok; + uint32_t reads_fail; + uint32_t adc_timeouts; + uint8_t last_fail_dev; // device index of most recent failure (0xFF if none) + }; + ADAR1000Manager(); ~ADAR1000Manager(); @@ -46,12 +59,12 @@ public: bool performSystemCalibration(); // Mode Switching - void switchToTXMode(); - void switchToRXMode(); - void fastTXMode(); - void fastRXMode(); - void pulseTXMode(); - void pulseRXMode(); + bool switchToTXMode(); + bool switchToRXMode(); + bool fastTXMode(); + bool fastRXMode(); + bool pulseTXMode(); + bool pulseRXMode(); // Beam Steering bool setBeamAngle(float angle_degrees, BeamDirection direction); @@ -68,14 +81,20 @@ public: // Device Control bool setAllDevicesTXMode(); bool setAllDevicesRXMode(); - void setADTR1107Mode(BeamDirection direction); - void setADTR1107Control(bool tx_mode); + bool setADTR1107Mode(BeamDirection direction); + bool setADTR1107Control(bool tx_mode); // Monitoring and Diagnostics + // readTemperature returns NaN when the on-chip ADC times out, so callers + // can distinguish a hung chip from a real cold reading via std::isnan(). float readTemperature(uint8_t deviceIndex); bool verifyDeviceCommunication(uint8_t deviceIndex); uint8_t readRegister(uint8_t deviceIndex, uint32_t address); - void writeRegister(uint8_t deviceIndex, uint32_t address, uint8_t value); + bool writeRegister(uint8_t deviceIndex, uint32_t address, uint8_t value); + + // Communication health observability + const CommStats& getCommStats() const { return comm_stats_; } + void resetCommStats(); // Configuration void setSwitchSettlingTime(uint32_t us); @@ -109,6 +128,9 @@ public: std::vector> devices_; BeamDirection current_mode_ = BeamDirection::RX; + // Comm health counters (zeroed in resetCommStats()). + CommStats comm_stats_ = {0, 0, 0, 0, 0, 0xFF}; + // Beam Sweeping std::vector tx_beam_sequence_; std::vector rx_beam_sequence_; @@ -121,53 +143,63 @@ public: // No VM_GAIN[] table exists: VM magnitude is bits [4:0] of the I/Q bytes // themselves; per-channel VGA gain uses a separate register. static const uint8_t VM_I[128]; - static const uint8_t VM_Q[128]; - - // Named defaults for the ADTR1107 and ADAR1000 power sequence. - static constexpr uint8_t kDefaultTxVgaGain = 0x7F; - static constexpr uint8_t kDefaultRxVgaGain = 30; - static constexpr uint8_t kLnaBiasOff = 0x00; - static constexpr uint8_t kLnaBiasOperational = 0x30; - static constexpr uint8_t kPaBiasTxSafe = 0x5D; - static constexpr uint8_t kPaBiasIdqCalibration = 0x0D; - static constexpr uint8_t kPaBiasOperational = 0x7F; - static constexpr uint8_t kPaBiasRxSafe = 0x20; - static constexpr uint8_t kTxBiasCurrent = 0x2D; - static constexpr uint8_t kTxDriverBiasCurrent = 0x06; - - // Private Methods - bool initializeSingleDevice(uint8_t deviceIndex); + static const uint8_t VM_Q[128]; + + // Named defaults for the ADTR1107 and ADAR1000 power sequence. + static constexpr uint8_t kDefaultTxVgaGain = 0x7F; + static constexpr uint8_t kDefaultRxVgaGain = 30; + static constexpr uint8_t kLnaBiasOff = 0x00; + static constexpr uint8_t kLnaBiasOperational = 0x30; + static constexpr uint8_t kPaBiasTxSafe = 0x5D; + static constexpr uint8_t kPaBiasIdqCalibration = 0x0D; + static constexpr uint8_t kPaBiasOperational = 0x7F; + static constexpr uint8_t kPaBiasRxSafe = 0x20; + static constexpr uint8_t kTxBiasCurrent = 0x2D; + static constexpr uint8_t kTxDriverBiasCurrent = 0x06; + + // Private Methods + bool initializeSingleDevice(uint8_t deviceIndex); bool initializeADTR1107Sequence(); void calculatePhaseSettings(float angle_degrees, uint8_t phase_settings[4]); void delayUs(uint32_t microseconds); // Power Management + // PA/LNA supply rails are pure GPIO toggles -- those stay void. + // setPABias/setLNABias issue per-device SPI writes, so they propagate. void enablePASupplies(); void disablePASupplies(); void enableLNASupplies(); void disableLNASupplies(); - void setPABias(bool enable); - void setLNABias(bool enable); + bool setPABias(bool enable); + bool setLNABias(bool enable); // SPI Communication + // setChipSelect is a pure GPIO toggle (no failure mode in HAL_GPIO_WritePin). + // Everything else returns true on success, false on SPI failure or invalid index. void setChipSelect(uint8_t deviceIndex, bool state); - uint32_t spiTransfer(uint8_t* txData, uint8_t* rxData, uint32_t size); - void adarWrite(uint8_t deviceIndex, uint32_t mem_addr, uint8_t data, uint8_t broadcast); + bool spiTransfer(uint8_t* txData, uint8_t* rxData, uint32_t size); + bool adarWrite(uint8_t deviceIndex, uint32_t mem_addr, uint8_t data, uint8_t broadcast); + // adarRead returns the register byte; on SPI failure it returns 0 and the + // failure is reflected in comm_stats_.reads_fail (callers wanting an + // explicit ok/fail signal should use adarReadChecked below). uint8_t adarRead(uint8_t deviceIndex, uint32_t mem_addr); - void adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast); - void adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast); - void adarSoftReset(uint8_t deviceIndex); - void adarWriteConfigA(uint8_t deviceIndex, uint8_t flags, uint8_t broadcast); - void adarSetRamBypass(uint8_t deviceIndex, uint8_t broadcast); + bool adarReadChecked(uint8_t deviceIndex, uint32_t mem_addr, uint8_t* out); + bool adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast); + bool adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast); + bool adarSoftReset(uint8_t deviceIndex); + bool adarWriteConfigA(uint8_t deviceIndex, uint8_t flags, uint8_t broadcast); + bool adarSetRamBypass(uint8_t deviceIndex, uint8_t broadcast); // Channel Configuration - void adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast); - void adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast); - void adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast); - void adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast); - void adarSetTxBias(uint8_t deviceIndex, uint8_t broadcast); + bool adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast); + bool adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast); + bool adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast); + bool adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast); + bool adarSetTxBias(uint8_t deviceIndex, uint8_t broadcast); + // adarAdcRead returns 0 on timeout AND increments comm_stats_.adc_timeouts. + // readTemperature() detects the timeout via that counter delta. uint8_t adarAdcRead(uint8_t deviceIndex, uint8_t broadcast); - void setTRSwitchPosition(uint8_t deviceIndex, bool tx_mode); + bool setTRSwitchPosition(uint8_t deviceIndex, bool tx_mode); private: 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 ae76731..24e26c9 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 @@ -388,7 +388,12 @@ void systemPowerUpSequence() { // Step 4: Set to safe TX mode DIAG("PWR", "Step 4: setAllDevicesTXMode()"); - adarManager.setAllDevicesTXMode(); + if (!adarManager.setAllDevicesTXMode()) { + DIAG_ERR("PWR", "setAllDevicesTXMode() FAILED -- calling Error_Handler()"); + uint8_t err[] = "ERROR: ADAR1000 TX-mode setup failed!\r\n"; + HAL_UART_Transmit(&huart3, err, sizeof(err)-1, 1000); + Error_Handler(); + } DIAG("PWR", "Step 4 OK: All devices set to TX mode"); uint8_t success[] = "Power Up Sequence Completed Successfully\r\n"; @@ -401,9 +406,15 @@ void systemPowerDownSequence() { uint8_t msg[] = "Starting Power Down Sequence...\r\n"; HAL_UART_Transmit(&huart3, msg, sizeof(msg)-1, 1000); - // Step 1: Set all devices to RX mode (safest state) + // Step 1: Set all devices to RX mode (safest state). Failure here is logged + // but NOT fatal -- power-down must always proceed to cut the rails below. + // Leaving a stuck PA bias would be more dangerous than losing RX-mode telemetry. DIAG("PWR", "Step 1: setAllDevicesRXMode()"); - adarManager.setAllDevicesRXMode(); + if (!adarManager.setAllDevicesRXMode()) { + DIAG_ERR("PWR", "setAllDevicesRXMode() FAILED during power-down -- continuing to cut rails"); + uint8_t warn[] = "WARNING: RX-mode setup failed during power-down, cutting rails anyway\r\n"; + HAL_UART_Transmit(&huart3, warn, sizeof(warn)-1, 1000); + } HAL_Delay(10); // Step 2: Disable PA power supplies @@ -480,14 +491,15 @@ void initializeBeamMatrices() { void executeChirpSequence(int num_chirps, float T1, float PRI1, float T2, float PRI2) { // NOTE: No per-chirp DIAG — this is a us/ns timing-critical path. // Only log entry params for post-mortem analysis. + DIAG("SYS", "executeChirpSequence: num_chirps=%d T1=%.2f PRI1=%.2f T2=%.2f PRI2=%.2f", num_chirps, T1, PRI1, T2, PRI2); // First chirp sequence (microsecond timing) for(int i = 0; i < num_chirps; i++) { HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_8); // New chirp signal to FPGA - adarManager.pulseTXMode(); + (void)adarManager.pulseTXMode(); delay_us((uint32_t)T1); - adarManager.pulseRXMode(); + (void)adarManager.pulseRXMode(); delay_us((uint32_t)(PRI1 - T1)); } @@ -496,9 +508,9 @@ void executeChirpSequence(int num_chirps, float T1, float PRI1, float T2, float // Second chirp sequence (nanosecond timing) for(int i = 0; i < num_chirps; i++) { HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_8); // New chirp signal to FPGA - adarManager.pulseTXMode(); + (void)adarManager.pulseTXMode(); delay_ns((uint32_t)(T2 * 1000)); - adarManager.pulseRXMode(); + (void)adarManager.pulseRXMode(); delay_ns((uint32_t)((PRI2 - T2) * 1000)); } @@ -656,18 +668,18 @@ SystemError_t checkSystemHealth(void) { // 1. Check AD9523 Clock Generator static uint32_t last_clock_check = 0; - if (HAL_GetTick() - last_clock_check > 5000) { - GPIO_PinState s0 = HAL_GPIO_ReadPin(AD9523_STATUS0_GPIO_Port, AD9523_STATUS0_Pin); - GPIO_PinState s1 = HAL_GPIO_ReadPin(AD9523_STATUS1_GPIO_Port, AD9523_STATUS1_Pin); - DIAG_GPIO("CLK", "AD9523 STATUS0", s0); - DIAG_GPIO("CLK", "AD9523 STATUS1", s1); - if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) { - current_error = ERROR_AD9523_CLOCK; - DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1); - return current_error; - } - last_clock_check = HAL_GetTick(); - } + if (HAL_GetTick() - last_clock_check > 5000) { + GPIO_PinState s0 = HAL_GPIO_ReadPin(AD9523_STATUS0_GPIO_Port, AD9523_STATUS0_Pin); + GPIO_PinState s1 = HAL_GPIO_ReadPin(AD9523_STATUS1_GPIO_Port, AD9523_STATUS1_Pin); + DIAG_GPIO("CLK", "AD9523 STATUS0", s0); + DIAG_GPIO("CLK", "AD9523 STATUS1", s1); + if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) { + current_error = ERROR_AD9523_CLOCK; + DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1); + return current_error; + } + last_clock_check = HAL_GetTick(); + } // 2. Check ADF4382 Lock Status bool tx_locked, rx_locked; @@ -693,6 +705,14 @@ SystemError_t checkSystemHealth(void) { } float temp = adarManager.readTemperature(i); + // NaN signals an ADC timeout / comm failure inside the manager. Previously + // a hung ADC returned 0, which mapped to -50 C and looked healthy. Map + // it to the comm-error bucket so attemptErrorRecovery re-inits the chip. + if (isnan(temp)) { + current_error = ERROR_ADAR1000_COMM; + DIAG_ERR("BF", "Health check: ADAR1000 #%d temperature read returned NaN (ADC timeout)", i); + return current_error; + } if (temp > 85.0f) { current_error = ERROR_ADAR1000_TEMP; DIAG_ERR("BF", "Health check: ADAR1000 #%d OVERTEMP %.1fC > 85C", i, temp); @@ -702,34 +722,34 @@ SystemError_t checkSystemHealth(void) { // 4. Check IMU Communication static uint32_t last_imu_check = 0; - if (HAL_GetTick() - last_imu_check > 10000) { - if (!GY85_Update(&imu)) { - current_error = ERROR_IMU_COMM; - DIAG_ERR("IMU", "Health check: GY85_Update() FAILED"); - return current_error; - } - last_imu_check = HAL_GetTick(); - } + if (HAL_GetTick() - last_imu_check > 10000) { + if (!GY85_Update(&imu)) { + current_error = ERROR_IMU_COMM; + DIAG_ERR("IMU", "Health check: GY85_Update() FAILED"); + return current_error; + } + last_imu_check = HAL_GetTick(); + } // 5. Check BMP180 Communication static uint32_t last_bmp_check = 0; - if (HAL_GetTick() - last_bmp_check > 15000) { - double pressure = myBMP.getPressure(); - if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) { - current_error = ERROR_BMP180_COMM; - DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure); - return current_error; - } - last_bmp_check = HAL_GetTick(); - } + if (HAL_GetTick() - last_bmp_check > 15000) { + double pressure = myBMP.getPressure(); + if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) { + current_error = ERROR_BMP180_COMM; + DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure); + return current_error; + } + last_bmp_check = HAL_GetTick(); + } - // 6. Check GPS Communication (30s grace period from boot / last valid fix) - uint32_t gps_fix_age = um982_position_age(&um982); - if (gps_fix_age > 30000) { - current_error = ERROR_GPS_COMM; - DIAG_WARN("SYS", "Health check: GPS no fix for >30s (age=%lu ms)", (unsigned long)gps_fix_age); - return current_error; - } + // 6. Check GPS Communication (30s grace period from boot / last valid fix) + uint32_t gps_fix_age = um982_position_age(&um982); + if (gps_fix_age > 30000) { + current_error = ERROR_GPS_COMM; + DIAG_WARN("SYS", "Health check: GPS no fix for >30s (age=%lu ms)", (unsigned long)gps_fix_age); + return current_error; + } // 7. Check RF Power Amplifier Current if (PowerAmplifier) { @@ -760,7 +780,7 @@ SystemError_t checkSystemHealth(void) { DIAG_ERR("SYS", "checkSystemHealth returning error code %d", current_error); } return current_error; -} +} // Error recovery function void attemptErrorRecovery(SystemError_t error) { @@ -782,11 +802,20 @@ void attemptErrorRecovery(SystemError_t error) { break; case ERROR_ADAR1000_COMM: - // Reset ADAR1000 communication + // Reset ADAR1000 communication. Previously this discarded the bool + // return, so a re-init that failed silently looked like recovery + // succeeded -- the next health check would loop right back here. DIAG("BF", "Recovery: Re-initializing all ADAR1000 devices"); - adarManager.initializeAllDevices(); - HAL_Delay(50); - DIAG("BF", "Recovery: ADAR1000 re-init complete"); + if (!adarManager.initializeAllDevices()) { + DIAG_ERR("BF", "Recovery FAILED: ADAR1000 re-init still failing -- escalating to emergency state"); + uint8_t err[] = "ERROR: ADAR1000 recovery failed, entering emergency state\r\n"; + HAL_UART_Transmit(&huart3, err, sizeof(err)-1, 1000); + system_emergency_state = true; + error_count++; + } else { + HAL_Delay(50); + DIAG("BF", "Recovery: ADAR1000 re-init complete"); + } break; case ERROR_IMU_COMM: @@ -905,22 +934,22 @@ void handleSystemError(SystemError_t error) { HAL_Delay(200); } - // Critical errors trigger emergency shutdown. - // - // Safety-critical range: any fault that can damage the PAs or leave the - // system in an undefined state must cut the RF rails via Emergency_Stop(). - // This covers: - // ERROR_RF_PA_OVERCURRENT .. ERROR_POWER_SUPPLY (9..13) -- PA/supply faults - // ERROR_TEMPERATURE_HIGH (14) -- >75 C on the PA thermal sensors; - // without cutting bias + 5V/5V5/RFPA rails - // the GaN QPA2962 stage can thermal-runaway. - // ERROR_WATCHDOG_TIMEOUT (16) -- health-check loop has stalled (>60 s); - // transmitter state is unknown, safest to - // latch Emergency_Stop rather than rely on - // IWDG reset (which re-energises the rails). - if ((error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) || - error == ERROR_TEMPERATURE_HIGH || - error == ERROR_WATCHDOG_TIMEOUT) { + // Critical errors trigger emergency shutdown. + // + // Safety-critical range: any fault that can damage the PAs or leave the + // system in an undefined state must cut the RF rails via Emergency_Stop(). + // This covers: + // ERROR_RF_PA_OVERCURRENT .. ERROR_POWER_SUPPLY (9..13) -- PA/supply faults + // ERROR_TEMPERATURE_HIGH (14) -- >75 C on the PA thermal sensors; + // without cutting bias + 5V/5V5/RFPA rails + // the GaN QPA2962 stage can thermal-runaway. + // ERROR_WATCHDOG_TIMEOUT (16) -- health-check loop has stalled (>60 s); + // transmitter state is unknown, safest to + // latch Emergency_Stop rather than rely on + // IWDG reset (which re-energises the rails). + if ((error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) || + error == ERROR_TEMPERATURE_HIGH || + error == ERROR_WATCHDOG_TIMEOUT) { DIAG_ERR("SYS", "CRITICAL ERROR (code %d: %s) -- initiating Emergency_Stop()", error, err_name); snprintf(error_msg, sizeof(error_msg), "CRITICAL ERROR! Initiating emergency shutdown.\r\n"); @@ -1483,8 +1512,8 @@ int main(void) HAL_GPIO_WritePin(EN_P_3V3_FPGA_GPIO_Port,EN_P_3V3_FPGA_Pin,GPIO_PIN_SET); HAL_Delay(100); DIAG("PWR", "FPGA power sequencing complete -- 1.0V -> 1.8V -> 3.3V"); - - + + // Initialize module IMU DIAG_SECTION("IMU INIT (GY-85)"); DIAG("IMU", "Initializing GY-85 IMU..."); @@ -1493,12 +1522,12 @@ int main(void) Error_Handler(); } DIAG("IMU", "GY-85 initialized OK, running 10 calibration samples"); - for(int i=0; i<10;i++){ - if (!GY85_Update(&imu)) { - Error_Handler(); - } - - ax = imu.ax; + for(int i=0; i<10;i++){ + if (!GY85_Update(&imu)) { + Error_Handler(); + } + + ax = imu.ax; ay = imu.ay; az = imu.az; gx = -imu.gx; @@ -1793,20 +1822,20 @@ int main(void) HAL_Delay(10); } } - RADAR_Longitude = um982_get_longitude(&um982); - RADAR_Latitude = um982_get_latitude(&um982); - DIAG("GPS", "Initial position: lat=%.6f lon=%.6f fix=%d sats=%d", - RADAR_Latitude, RADAR_Longitude, - um982_get_fix_quality(&um982), um982_get_num_sats(&um982)); - - // Re-apply heading after GPS init so the north-alignment stepper move uses - // UM982 dual-antenna heading when available. - if (um982_is_heading_valid(&um982)) { - Yaw_Sensor = um982_get_heading(&um982); - } - - //move Stepper to position 1 = 0° - HAL_GPIO_WritePin(STEPPER_CW_P_GPIO_Port, STEPPER_CW_P_Pin, GPIO_PIN_RESET);//Set stepper motor spinning direction to CCW + RADAR_Longitude = um982_get_longitude(&um982); + RADAR_Latitude = um982_get_latitude(&um982); + DIAG("GPS", "Initial position: lat=%.6f lon=%.6f fix=%d sats=%d", + RADAR_Latitude, RADAR_Longitude, + um982_get_fix_quality(&um982), um982_get_num_sats(&um982)); + + // Re-apply heading after GPS init so the north-alignment stepper move uses + // UM982 dual-antenna heading when available. + if (um982_is_heading_valid(&um982)) { + Yaw_Sensor = um982_get_heading(&um982); + } + + //move Stepper to position 1 = 0° + HAL_GPIO_WritePin(STEPPER_CW_P_GPIO_Port, STEPPER_CW_P_Pin, GPIO_PIN_RESET);//Set stepper motor spinning direction to CCW //Point Stepper to North for(int i= 0;i<(int)(Yaw_Sensor*Stepper_steps/360);i++){ HAL_GPIO_WritePin(STEPPER_CLK_P_GPIO_Port, STEPPER_CLK_P_Pin, GPIO_PIN_SET); @@ -1819,14 +1848,14 @@ int main(void) /**********wait for GUI start flag and Send Lat/Long/alt********/ /***************************************************************/ - GPS_Data_t gps_data; - // Binary packet structure: - // [Header 4 bytes][Latitude 8 bytes][Longitude 8 bytes][Altitude 4 bytes][Pitch 4 bytes][CRC 2 bytes] - gps_data = {RADAR_Latitude, RADAR_Longitude, RADAR_Altitude, Pitch_Sensor, HAL_GetTick()}; - if (!GPS_SendBinaryToGUI(&gps_data)) { - const uint8_t gps_send_error[] = "GPS binary send failed\r\n"; - HAL_UART_Transmit(&huart3, (uint8_t*)gps_send_error, sizeof(gps_send_error) - 1, 1000); - } + GPS_Data_t gps_data; + // Binary packet structure: + // [Header 4 bytes][Latitude 8 bytes][Longitude 8 bytes][Altitude 4 bytes][Pitch 4 bytes][CRC 2 bytes] + gps_data = {RADAR_Latitude, RADAR_Longitude, RADAR_Altitude, Pitch_Sensor, HAL_GetTick()}; + if (!GPS_SendBinaryToGUI(&gps_data)) { + const uint8_t gps_send_error[] = "GPS binary send failed\r\n"; + HAL_UART_Transmit(&huart3, (uint8_t*)gps_send_error, sizeof(gps_send_error) - 1, 1000); + } /* [STM32-006 FIXED] Removed blocking do-while loop that waited for * usbHandler.isStartFlagReceived(). The production V7 PyQt GUI does not diff --git a/9_Firmware/9_1_Microcontroller/tests/Makefile b/9_Firmware/9_1_Microcontroller/tests/Makefile index f190c65..6403c1b 100644 --- a/9_Firmware/9_1_Microcontroller/tests/Makefile +++ b/9_Firmware/9_1_Microcontroller/tests/Makefile @@ -79,10 +79,17 @@ TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only # C++ tests (AGC outer loop) TESTS_WITH_CXX := test_agc_outer_loop +# ADAR1000 error/status propagation tests -- link real ADAR1000_Manager.o + mocks +TESTS_ADAR_STATUS := test_adar_init_aborts_on_scratchpad_mismatch \ + test_adar_spi_write_failure_propagates \ + test_adar_adc_timeout_returns_nan \ + test_adar_mode_switch_does_not_lie \ + test_adar_comm_stats_increment + # GPS driver tests (need mocks + GPS source + -lm) TESTS_GPS := test_um982_gps -ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) $(TESTS_GPS) +ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) $(TESTS_ADAR_STATUS) $(TESTS_GPS) .PHONY: all build test clean \ $(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \ @@ -204,6 +211,16 @@ test_agc_outer_loop: test_agc_outer_loop.cpp $(CXX_OBJS) $(MOCK_OBJS) test_agc: test_agc_outer_loop ./test_agc_outer_loop +# --- ADAR1000 status-propagation test rules --- +# Each test links the real ADAR1000_Manager.cpp (compiled as ADAR1000_Manager.o) +# against the HAL mock so failure injection drives the production code paths. +$(TESTS_ADAR_STATUS): %: %.cpp ADAR1000_Manager.o $(MOCK_OBJS) + $(CXX) $(CXXFLAGS) $(INCLUDES) $< ADAR1000_Manager.o $(MOCK_OBJS) -o $@ + +.PHONY: test_adar_status +test_adar_status: $(TESTS_ADAR_STATUS) + @for t in $(TESTS_ADAR_STATUS); do echo "--- $$t ---"; ./$$t || exit 1; done + # --- GPS driver rules --- $(GPS_OBJ): $(GPS_SRC) 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 230efb9..59e9b7c 100644 --- a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c +++ b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c @@ -63,6 +63,12 @@ static struct { GPIO_PinState val; } gpio_read_table[GPIO_READ_TABLE_SIZE]; +/* SPI failure-injection state */ +static int mock_spi_fail_remaining = 0; +static HAL_StatusTypeDef mock_spi_fail_status = HAL_OK; +static uint8_t mock_spi_rx_byte = 0; +static uint32_t mock_tick_auto_advance = 0; + void spy_reset(void) { spy_count = 0; @@ -73,6 +79,26 @@ void spy_reset(void) memset(mock_uart_rx, 0, sizeof(mock_uart_rx)); mock_uart_tx_len = 0; memset(mock_uart_tx_buf, 0, sizeof(mock_uart_tx_buf)); + mock_spi_fail_remaining = 0; + mock_spi_fail_status = HAL_OK; + mock_spi_rx_byte = 0; + mock_tick_auto_advance = 0; +} + +void mock_spi_queue_failure(int call_count, HAL_StatusTypeDef status) +{ + mock_spi_fail_remaining = call_count; + mock_spi_fail_status = status; +} + +void mock_spi_set_rx_byte(uint8_t value) +{ + mock_spi_rx_byte = value; +} + +void mock_set_tick_auto_advance(uint32_t delta) +{ + mock_tick_auto_advance = delta; } const SpyRecord *spy_get(int index) @@ -177,14 +203,16 @@ void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) uint32_t HAL_GetTick(void) { + uint32_t result = mock_tick; spy_push((SpyRecord){ .type = SPY_HAL_GET_TICK, .port = NULL, .pin = 0, - .value = mock_tick, + .value = result, .extra = NULL }); - return mock_tick; + mock_tick += mock_tick_auto_advance; + return result; } void HAL_Delay(uint32_t Delay) @@ -369,6 +397,7 @@ void mock_tim_set_compare(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t Co HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout) { + (void)pTxData; spy_push((SpyRecord){ .type = SPY_SPI_TRANSMIT_RECEIVE, .port = NULL, @@ -376,11 +405,19 @@ HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxD .value = Timeout, .extra = hspi }); + if (mock_spi_fail_remaining > 0) { + mock_spi_fail_remaining--; + return mock_spi_fail_status; + } + if (pRxData && Size > 0) { + pRxData[Size - 1] = mock_spi_rx_byte; + } return HAL_OK; } HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) { + (void)pData; spy_push((SpyRecord){ .type = SPY_SPI_TRANSMIT, .port = NULL, @@ -388,6 +425,10 @@ HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint .value = Timeout, .extra = hspi }); + if (mock_spi_fail_remaining > 0) { + mock_spi_fail_remaining--; + return mock_spi_fail_status; + } return HAL_OK; } 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 9153011..ddf63e3 100644 --- a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h +++ b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h @@ -176,6 +176,11 @@ void mock_set_tick(uint32_t tick); /* Advance the mock tick by `delta` ms */ void mock_advance_tick(uint32_t delta); +/* Each subsequent HAL_GetTick() call advances the mock tick by `delta` ms after + * returning the current value. Use to drive timeout loops without wall-clock + * waits. Set to 0 (default) for stable tick. */ +void mock_set_tick_auto_advance(uint32_t delta); + /* ========================= Mock GPIO read returns ================= */ /* Set the value HAL_GPIO_ReadPin will return for a specific port/pin */ @@ -212,6 +217,15 @@ void mock_uart_tx_clear(void); HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); +/* Queue the next N SPI calls (Transmit and TransmitReceive) to return `status` + * instead of HAL_OK. Decremented on each call. Use for failure-propagation tests. */ +void mock_spi_queue_failure(int call_count, HAL_StatusTypeDef status); + +/* Set the byte that HAL_SPI_TransmitReceive will write at pRxData[Size-1] for + * subsequent calls. ADAR1000 reads land at index 2 of a 3-byte transfer, so + * this lets tests inject scratchpad / register readback values. */ +void mock_spi_set_rx_byte(uint8_t value); + /* ========================= no_os compat layer ===================== */ void no_os_udelay(uint32_t usecs); diff --git a/9_Firmware/9_1_Microcontroller/tests/test_adar_adc_timeout_returns_nan.cpp b/9_Firmware/9_1_Microcontroller/tests/test_adar_adc_timeout_returns_nan.cpp new file mode 100644 index 0000000..974fd20 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_adar_adc_timeout_returns_nan.cpp @@ -0,0 +1,115 @@ +// test_adar_adc_timeout_returns_nan.cpp +// +// readTemperature() previously returned -50.0 C silently when the on-chip +// ADC never completed a conversion (raw=0 timeout sentinel mapped through +// (raw * 0.5) - 50). A hung chip looked like a cold radar. +// +// New: on ADC timeout or any comm failure inside adarAdcRead(), +// readTemperature returns NaN, and comm_stats_.adc_timeouts increments. +// checkSystemHealth() in main.cpp uses isnan() to route NaN to the comm-error +// bucket (which triggers attemptErrorRecovery). + +#include +#include +#include + +#include "stm32_hal_mock.h" +#include "ADAR1000_Manager.h" + +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-65s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +// Helper: bring all 4 devices through init successfully so readTemperature() +// gets past its "not initialized" guard. +static void init_devices_clean(ADAR1000Manager& mgr) +{ + mock_spi_set_rx_byte(0xA5); + bool ok = mgr.initializeAllDevices(); + assert(ok); +} + +// Default mock returns 0x00 to every read after init. Since bit 0 of the ADC +// status register never goes high, the polling loop in adarAdcRead is supposed +// to time out after 100 ms. Auto-advancing the tick on every HAL_GetTick() +// drives that timer forward without wall-clock waits. +static void test_polling_timeout_returns_nan() +{ + spy_reset(); + ADAR1000Manager mgr; + init_devices_clean(mgr); + + // Switch the rx-byte back to 0x00 so the ADC status bit stays low forever + // and the polling loop runs to its 100 ms watchdog. + mock_spi_set_rx_byte(0x00); + mock_set_tick_auto_advance(150); // each HAL_GetTick() advances 150 ms + + mgr.resetCommStats(); + float t = mgr.readTemperature(0); + + assert(std::isnan(t)); + const auto& stats = mgr.getCommStats(); + assert(stats.adc_timeouts >= 1); +} + +// SPI failure during adarAdcRead's start-conversion write must also count as +// a timeout (caller has no valid ADC reading) and produce NaN. +static void test_start_conv_spi_failure_returns_nan() +{ + spy_reset(); + ADAR1000Manager mgr; + init_devices_clean(mgr); + + mgr.resetCommStats(); + // Fail the very next SPI call -- the start-conversion write inside + // adarAdcRead. Following polling reads will also fail, but the function + // bails on the start-conv write first. + mock_spi_queue_failure(100, HAL_ERROR); + + float t = mgr.readTemperature(0); + assert(std::isnan(t)); + + const auto& stats = mgr.getCommStats(); + assert(stats.adc_timeouts >= 1); +} + +// Healthy path: ADC bit 0 is high on the first poll (rx_byte = 0xA5 after init), +// so adarAdcRead exits the loop immediately, and readTemperature returns a +// finite number. This guards against false-positive NaN from the new code path. +static void test_healthy_adc_returns_finite_temp() +{ + spy_reset(); + ADAR1000Manager mgr; + init_devices_clean(mgr); + + mgr.resetCommStats(); + float t = mgr.readTemperature(0); + + assert(!std::isnan(t)); + const auto& stats = mgr.getCommStats(); + assert(stats.adc_timeouts == 0); +} + +int main() +{ + printf("=== ADAR1000 ADC timeout -> NaN propagation tests ===\n"); + + RUN_TEST(test_polling_timeout_returns_nan); + RUN_TEST(test_start_conv_spi_failure_returns_nan); + RUN_TEST(test_healthy_adc_returns_finite_temp); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +} diff --git a/9_Firmware/9_1_Microcontroller/tests/test_adar_comm_stats_increment.cpp b/9_Firmware/9_1_Microcontroller/tests/test_adar_comm_stats_increment.cpp new file mode 100644 index 0000000..d723d76 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_adar_comm_stats_increment.cpp @@ -0,0 +1,191 @@ +// test_adar_comm_stats_increment.cpp +// +// Documents and locks the CommStats observability surface added in this PR. +// Without this test, a future "simplification" could quietly remove the +// counters and nothing else would catch it. +// +// Contract: +// - Every successful adarWrite increments writes_ok. +// - Every failed adarWrite increments writes_fail and updates last_fail_dev. +// - Every successful adarRead increments reads_ok. +// - Every failed adarRead increments reads_fail and updates last_fail_dev. +// - resetCommStats() zeroes all counters and resets last_fail_dev to 0xFF. +// - PR2 may promote a richer OpStatus enum return type; this struct is the +// forward-compatible observability hook either way. + +#include +#include + +#include "stm32_hal_mock.h" +#include "ADAR1000_Manager.h" + +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-65s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +static void test_default_stats_are_zero() +{ + spy_reset(); + ADAR1000Manager mgr; + + const auto& s = mgr.getCommStats(); + assert(s.writes_ok == 0); + assert(s.writes_fail == 0); + assert(s.reads_ok == 0); + assert(s.reads_fail == 0); + assert(s.adc_timeouts == 0); + assert(s.last_fail_dev == 0xFF); +} + +static void test_successful_write_increments_writes_ok() +{ + spy_reset(); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + bool ok = mgr.writeRegister(0, 0x010, 0x42); + assert(ok); + + const auto& s = mgr.getCommStats(); + assert(s.writes_ok == 1); + assert(s.writes_fail == 0); + assert(s.last_fail_dev == 0xFF); +} + +static void test_failed_write_increments_writes_fail_and_records_dev() +{ + spy_reset(); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + mock_spi_queue_failure(1, HAL_ERROR); + bool ok = mgr.writeRegister(2, 0x010, 0x42); // dev[2] + assert(ok == false); + + const auto& s = mgr.getCommStats(); + assert(s.writes_ok == 0); + assert(s.writes_fail == 1); + assert(s.last_fail_dev == 2); +} + +static void test_successful_read_increments_reads_ok() +{ + spy_reset(); + mock_spi_set_rx_byte(0xA5); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + uint8_t value = 0; + bool ok = mgr.adarReadChecked(1, 0x010, &value); + assert(ok); + assert(value == 0xA5); + + const auto& s = mgr.getCommStats(); + assert(s.reads_ok == 1); + assert(s.reads_fail == 0); +} + +static void test_failed_read_increments_reads_fail_and_records_dev() +{ + spy_reset(); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + // adarReadChecked sequence: + // 1. adarWrite(SDO active) -- HAL_SPI_Transmit + // 2. HAL_SPI_TransmitReceive (the actual read) <-- we want this to fail + // 3. adarWrite(SDO inactive) + // Letting calls 1+2 fail (queue 2 failures) covers the case where SDO-active + // also fails -- adarReadChecked aborts after #1 and bumps reads_fail. + mock_spi_queue_failure(2, HAL_ERROR); + + uint8_t value = 0xCC; // pre-poison to confirm out param gets cleared + bool ok = mgr.adarReadChecked(3, 0x010, &value); + assert(ok == false); + assert(value == 0); // adarReadChecked must zero the out param on failure + + const auto& s = mgr.getCommStats(); + assert(s.reads_fail >= 1); + assert(s.last_fail_dev == 3); +} + +// Reset must clear everything to defaults, including last_fail_dev back to 0xFF. +static void test_reset_clears_all_counters() +{ + spy_reset(); + ADAR1000Manager mgr; + + // Generate some non-zero state in every field. + mgr.writeRegister(0, 0x010, 0x42); // writes_ok++ + mock_spi_queue_failure(1, HAL_ERROR); + mgr.writeRegister(1, 0x010, 0x42); // writes_fail++, last_fail_dev=1 + mock_spi_set_rx_byte(0xA5); + uint8_t v = 0; + mgr.adarReadChecked(0, 0x010, &v); // reads_ok++ + + { + const auto& s = mgr.getCommStats(); + assert(s.writes_ok > 0); + assert(s.writes_fail > 0); + assert(s.reads_ok > 0); + assert(s.last_fail_dev != 0xFF); + } + + mgr.resetCommStats(); + + const auto& s = mgr.getCommStats(); + assert(s.writes_ok == 0); + assert(s.writes_fail == 0); + assert(s.reads_ok == 0); + assert(s.reads_fail == 0); + assert(s.adc_timeouts == 0); + assert(s.last_fail_dev == 0xFF); +} + +// Out-of-range device index counts as a write/read failure (not a silent skip). +// This is the kind of bug that would otherwise hide behind a "device 4 ignored" +// log line and never escalate to the caller. +static void test_out_of_range_device_index_counts_as_failure() +{ + spy_reset(); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + bool wok = mgr.writeRegister(99, 0x010, 0x42); + assert(wok == false); + assert(mgr.getCommStats().writes_fail == 1); + assert(mgr.getCommStats().last_fail_dev == 99); + + uint8_t v = 0; + bool rok = mgr.adarReadChecked(99, 0x010, &v); + assert(rok == false); + assert(mgr.getCommStats().reads_fail == 1); +} + +int main() +{ + printf("=== ADAR1000 CommStats observability tests ===\n"); + + RUN_TEST(test_default_stats_are_zero); + RUN_TEST(test_successful_write_increments_writes_ok); + RUN_TEST(test_failed_write_increments_writes_fail_and_records_dev); + RUN_TEST(test_successful_read_increments_reads_ok); + RUN_TEST(test_failed_read_increments_reads_fail_and_records_dev); + RUN_TEST(test_reset_clears_all_counters); + RUN_TEST(test_out_of_range_device_index_counts_as_failure); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +} diff --git a/9_Firmware/9_1_Microcontroller/tests/test_adar_init_aborts_on_scratchpad_mismatch.cpp b/9_Firmware/9_1_Microcontroller/tests/test_adar_init_aborts_on_scratchpad_mismatch.cpp new file mode 100644 index 0000000..9d4a3b6 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_adar_init_aborts_on_scratchpad_mismatch.cpp @@ -0,0 +1,104 @@ +// test_adar_init_aborts_on_scratchpad_mismatch.cpp +// +// previously initializeSingleDevice() logged a +// warning when the scratchpad readback didn't match 0xA5 but still set +// devices_[i]->initialized = true and returned true. That meant +// initializeAllDevices() would happily report success with four dead chips. +// +// New: any scratchpad mismatch aborts init. The device stays +// uninitialized, the function returns false, and downstream calls (e.g. +// readTemperature) return the "not initialized" sentinel -273.15. + +#include +#include +#include + +#include "stm32_hal_mock.h" +#include "ADAR1000_Manager.h" + +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-65s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +// With default mock state, HAL_SPI_TransmitReceive writes 0x00 into rx_buffer[2]. +// The scratchpad write programs 0xA5; the readback gets 0x00; mismatch -> abort. +static void test_scratchpad_mismatch_aborts_init() +{ + spy_reset(); + ADAR1000Manager mgr; + + bool ok = mgr.initializeAllDevices(); + + assert(ok == false); // would have been true under the old bug + + // Downstream proof: readTemperature must report "not initialized" sentinel, + // not a temperature value, because the device is not marked initialized. + float t = mgr.readTemperature(0); + assert(t == -273.15f); +} + +// When scratchpad readback matches, init succeeds. mock_spi_set_rx_byte(0xA5) +// makes every read return 0xA5, satisfying the verify step. +static void test_scratchpad_match_lets_init_succeed() +{ + spy_reset(); + mock_spi_set_rx_byte(0xA5); + ADAR1000Manager mgr; + + bool ok = mgr.initializeAllDevices(); + + assert(ok == true); + + // Now temperature read should produce a real number (not -273.15 sentinel). + // adarAdcRead loops on bit 0 of REG_ADC_CONTROL; with rx_byte=0xA5 (bit 0 = 1) + // the loop exits immediately, then REG_ADC_OUT also reads 0xA5. + float t = mgr.readTemperature(0); + assert(!std::isnan(t)); + assert(t != -273.15f); +} + +// Init aborts on the first device that fails. Stats reflect partial progress +// (some writes_ok before the abort) which is the trend signal callers will +// query via getCommStats(). +static void test_init_failure_recorded_in_stats() +{ + spy_reset(); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + bool ok = mgr.initializeAllDevices(); + assert(ok == false); + + const auto& stats = mgr.getCommStats(); + // Several writes happened before scratchpad verify failed: soft reset, + // configA, RAM bypass, ADC enable, scratchpad-write itself, and the + // SDO-active toggles inside the scratchpad-read. + assert(stats.writes_ok > 0); + // The scratchpad readback succeeded at the SPI layer (HAL_OK) but produced + // a wrong value -- that's not a read failure, it's a verify mismatch. So + // reads_fail can stay 0 in the pure-mismatch case. +} + +int main() +{ + printf("=== ADAR1000 init scratchpad-mismatch propagation tests ===\n"); + + RUN_TEST(test_scratchpad_mismatch_aborts_init); + RUN_TEST(test_scratchpad_match_lets_init_succeed); + RUN_TEST(test_init_failure_recorded_in_stats); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +} diff --git a/9_Firmware/9_1_Microcontroller/tests/test_adar_mode_switch_does_not_lie.cpp b/9_Firmware/9_1_Microcontroller/tests/test_adar_mode_switch_does_not_lie.cpp new file mode 100644 index 0000000..4b12a73 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_adar_mode_switch_does_not_lie.cpp @@ -0,0 +1,113 @@ +// test_adar_mode_switch_does_not_lie.cpp +// +// setAllDevicesTXMode / setAllDevicesRXMode previously updated current_mode_ +// (both global and per-device) before issuing the SPI writes that actually +// reconfigure the chip, then returned true unconditionally. A SPI failure +// during the mode switch left software believing it was in TX mode while the +// hardware was still in RX (or vice-versa) -- a real safety hazard given +// that PA biasing is mode-dependent. +// +// New: current_mode_ only updates if every underlying write +// succeeded; otherwise the function returns false and the mode flag is left +// at its last-known-good value. + +#include +#include + +#include "stm32_hal_mock.h" +#include "ADAR1000_Manager.h" + +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-65s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +static void init_devices_clean(ADAR1000Manager& mgr) +{ + mock_spi_set_rx_byte(0xA5); + bool ok = mgr.initializeAllDevices(); + assert(ok); +} + +// After a clean init, the manager is in TX mode (initializeAllDevices ends +// with setAllDevicesTXMode). Force RX mode under sustained SPI failure: must +// return false AND must not flip current_mode_ to RX. +static void test_failed_rx_switch_does_not_update_mode() +{ + spy_reset(); + ADAR1000Manager mgr; + init_devices_clean(mgr); + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX); + + mgr.resetCommStats(); + mock_spi_queue_failure(10000, HAL_ERROR); + + bool ok = mgr.setAllDevicesRXMode(); + assert(ok == false); + + // Mode flag must NOT have moved -- this is the dangerous lie we are fixing. + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX); +} + +// Symmetric case: cleanly transition to RX, then fail TX setup. Mode flag +// must stay RX. +static void test_failed_tx_switch_does_not_update_mode() +{ + spy_reset(); + ADAR1000Manager mgr; + init_devices_clean(mgr); + + mock_spi_set_rx_byte(0xA5); + bool rx_ok = mgr.setAllDevicesRXMode(); + assert(rx_ok); + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::RX); + + mgr.resetCommStats(); + mock_spi_queue_failure(10000, HAL_ERROR); + + bool ok = mgr.setAllDevicesTXMode(); + assert(ok == false); + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::RX); +} + +// Healthy round-trip: mode flag tracks the call that was made. Guards against +// the new code path accidentally refusing to advance the mode on success. +static void test_clean_mode_switches_update_flag() +{ + spy_reset(); + ADAR1000Manager mgr; + init_devices_clean(mgr); + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX); + + mock_spi_set_rx_byte(0xA5); + bool to_rx = mgr.setAllDevicesRXMode(); + assert(to_rx); + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::RX); + + bool to_tx = mgr.setAllDevicesTXMode(); + assert(to_tx); + assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX); +} + +int main() +{ + printf("=== ADAR1000 mode-switch honesty tests ===\n"); + + RUN_TEST(test_failed_rx_switch_does_not_update_mode); + RUN_TEST(test_failed_tx_switch_does_not_update_mode); + RUN_TEST(test_clean_mode_switches_update_flag); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +} diff --git a/9_Firmware/9_1_Microcontroller/tests/test_adar_spi_write_failure_propagates.cpp b/9_Firmware/9_1_Microcontroller/tests/test_adar_spi_write_failure_propagates.cpp new file mode 100644 index 0000000..76ebfde --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_adar_spi_write_failure_propagates.cpp @@ -0,0 +1,128 @@ +// test_adar_spi_write_failure_propagates.cpp +// +// When HAL_SPI_Transmit / HAL_SPI_TransmitReceive returns HAL_ERROR, every +// caller above must see the failure rather than silently continuing on. +// Previously adarWrite() returned void and dropped the SPI status on the +// floor, so a dead bus produced four "successful" inits. + +#include +#include +#include + +#include "stm32_hal_mock.h" +#include "ADAR1000_Manager.h" + +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-65s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +// First SPI transmit fails (the soft-reset write inside initializeSingleDevice). +// Init must abort immediately and report false. +static void test_first_spi_failure_aborts_init() +{ + spy_reset(); + mock_spi_queue_failure(1, HAL_ERROR); + ADAR1000Manager mgr; + + bool ok = mgr.initializeAllDevices(); + assert(ok == false); + + const auto& stats = mgr.getCommStats(); + assert(stats.writes_fail >= 1); + assert(stats.last_fail_dev == 0); // failure was on dev[0] +} + +// Sustained SPI failure (every call) must not produce a green init even if +// individual writes early in the sequence have already counted as failures +// without aborting -- the function must still return false at the end. +static void test_sustained_spi_failure_aborts_init() +{ + spy_reset(); + mock_spi_queue_failure(10000, HAL_ERROR); + ADAR1000Manager mgr; + + bool ok = mgr.initializeAllDevices(); + assert(ok == false); + + const auto& stats = mgr.getCommStats(); + assert(stats.writes_ok == 0); + assert(stats.writes_fail >= 1); +} + +// adarSetBit must NOT proceed with the write when the read-modify-write read +// fails -- otherwise it would clobber every other bit in the register by +// writing back (0 | mask) over a register whose actual contents are unknown. +// We use setTRSwitchPosition (which calls adarSetBit) and inject a failure +// on the read leg. +static void test_set_bit_skips_write_on_read_failure() +{ + spy_reset(); + ADAR1000Manager mgr; + mgr.resetCommStats(); + + // adarSetBit calls adarReadChecked first, which itself does: + // 1. adarWrite(SDO active) -- HAL_SPI_Transmit + // 2. HAL_SPI_TransmitReceive (the actual read) + // 3. adarWrite(SDO inactive) -- HAL_SPI_Transmit + // We want the actual read (call #2) to fail. Queue failure starting at + // call 2 by letting call 1 succeed first via a one-shot prime, then + // queueing the failure. + // + // Simpler approach: queue a failure of 100 calls, then call setTRSwitchPosition + // and assert reads_fail >= 1 and writes_fail >= 1 (the SDO-active write + // also fails). The key invariant: even though the read-modify-write was + // attempted, the function returned false. + mock_spi_queue_failure(100, HAL_ERROR); + + bool ok = mgr.setTRSwitchPosition(0, true); + assert(ok == false); + + const auto& stats = mgr.getCommStats(); + assert(stats.reads_fail >= 1 || stats.writes_fail >= 1); +} + +// Mode-switch must not fall through to another write block on the device that +// failed -- and the per-device current_mode must not be updated. +static void test_mode_switch_failure_propagates() +{ + spy_reset(); + mock_spi_set_rx_byte(0xA5); // make scratchpad verify succeed first + ADAR1000Manager mgr; + bool init_ok = mgr.initializeAllDevices(); + assert(init_ok); + + // Now inject sustained SPI failure for the mode switch. + mgr.resetCommStats(); + mock_spi_queue_failure(10000, HAL_ERROR); + + bool ok = mgr.setAllDevicesRXMode(); + assert(ok == false); + + const auto& stats = mgr.getCommStats(); + assert(stats.writes_fail >= 1); +} + +int main() +{ + printf("=== ADAR1000 SPI write-failure propagation tests ===\n"); + + RUN_TEST(test_first_spi_failure_aborts_init); + RUN_TEST(test_sustained_spi_failure_aborts_init); + RUN_TEST(test_set_bit_skips_write_on_read_failure); + RUN_TEST(test_mode_switch_failure_propagates); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +}