mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-05-23 00:02:14 +00:00
Merge pull request #131 from soufianebouaddis/fix/adar1000-comm-status-propagation
fix: propagate SPI/ADC communication failures in ADAR1000_Manager
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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<std::unique_ptr<ADAR1000Device>> 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<BeamConfig> tx_beam_sequence_;
|
||||
std::vector<BeamConfig> 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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user