Compare commits

...

8 Commits

Author SHA1 Message Date
Jason 5b84af68f6 fix(mcu): add FAULT_ACK command to clear system_emergency_state via USB (closes #83)
The volatile fix in the companion PR (#118) makes the safe-mode blink loop
escapable in principle, but no firmware path existed to actually clear
system_emergency_state at runtime — hardware reset was the only exit, which
fires the IWDG and re-energises the PA rails that Emergency_Stop() cut.

This change adds a FAULT_ACK command (opcode 0x40): the host sends an exact
4-byte CDC packet [0x40, 0x00, 0x00, 0x00]; USBHandler detects it regardless
of USB state and sets fault_ack_received; the blink loop checks the flag each
250 ms iteration and clears system_emergency_state, allowing a controlled
operator-acknowledged recovery without triggering a watchdog reset.

Detection is guarded to exact 4-byte packets only. Scanning larger packets
for the subsequence would false-trigger on the IEEE 754 big-endian encoding
of 2.0 (0x4000000000000000), which starts with the same 4 bytes and can
appear in normal settings doubles.

FAULT_ACK is excluded from the FPGA opcode enum to preserve the
Python/Verilog bidirectional contract test; contract_parser.py reads the
new MCU_ONLY_OPCODES frozenset in radar_protocol.py to filter it.

7 new test vectors in test_gap3_fault_ack_clears_emergency.c cover:
detection, loop exit, loop hold without ack, settings false-positive
immunity, truncated packet, wrong opcode, and multi-iteration sequence.

Reported-by: shaun0927 (Junghwan) <https://github.com/shaun0927>
2026-04-21 03:57:55 +05:45
Jason e979363730 fix(mcu): volatile emergency state + AGC holdoff zero-guard (closes #83)
Bug 1 (main.cpp:630): system_emergency_state lacked volatile. Under -O1+
the compiler is permitted to hoist the read outside the blink loop, making
while (system_emergency_state) unconditionally infinite. Once entered, the
only escape was the 4 s IWDG timeout — which resets the MCU and
re-energizes the PA rails that Emergency_Stop() explicitly cut. Marking the
variable volatile forces a memory read on every iteration so an external
clear (ISR, USB command, manual reset) can break the loop correctly.

Bug 2 (ADAR1000_AGC.cpp:59): holdoff_frames is a public uint8_t; if a
caller sets it to 0, the condition holdoff_counter >= holdoff_frames is
always true (any uint8_t >= 0), causing the AGC outer loop to increase gain
on every non-saturated frame with no holdoff delay. With alternating
sat/no-sat frames this produces a ±step oscillation that prevents the
receiver from settling. Fix: clamp holdoff_frames to a minimum of 1 in the
constructor, preserving all existing test assertions (none use 0; default
remains 4).

Reported-by: shaun0927 (Junghwan) <https://github.com/shaun0927>
2026-04-21 03:35:48 +05:45
Jason 3366ac6417 fix(fpga): widen AGC gain arithmetic to 6-bit to prevent wraparound
5-bit signed subtraction in clamp_gain wrapped for agc_attack >= 10 or
agc_decay >= 9 when |agc_gain| + step > 16, inverting gain polarity
instead of clamping — e.g. gain=-7, attack=10 produced +7 (max amplify)
rather than -7 (max attenuate), causing ADC saturation on strong returns.

Widen clamp_gain input to [5:0] and sign-extend both operands to 6 bits
({agc_gain[3],agc_gain[3],agc_gain} and {2'b00,agc_attack/decay}),
covering the full [-22,+22] range before clamping. Default attack/decay
values (1-4) are unaffected; behaviour changes only for values >= 10/9.
2026-04-21 03:06:32 +05:45
Jason db80baf34d Merge remote-tracking branch 'origin/main' into develop 2026-04-21 01:33:27 +05:45
NawfalMotii79 33d21da7f2 Remove radar system image from README
Removed the AERIS-10 Radar System image from the README.
2026-04-20 19:04:08 +01:00
NawfalMotii79 18901be04a Fix image link and update mixer model in README
Updated image link and corrected mixer model in specifications.
2026-04-19 19:06:44 +01:00
NawfalMotii79 9f899b96e9 Add files via upload 2026-04-19 19:04:48 +01:00
NawfalMotii79 88ca1910ec Merge pull request #109 from NawfalMotii79/develop
Release: merge develop into main
2026-04-19 01:27:15 +01:00
11 changed files with 183 additions and 14 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

@@ -24,6 +24,7 @@ ADAR1000_AGC::ADAR1000_AGC()
, saturation_event_count(0) , saturation_event_count(0)
{ {
memset(cal_offset, 0, sizeof(cal_offset)); memset(cal_offset, 0, sizeof(cal_offset));
if (holdoff_frames == 0) holdoff_frames = 1;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -13,6 +13,7 @@ void USBHandler::reset() {
start_flag_received = false; start_flag_received = false;
buffer_index = 0; buffer_index = 0;
current_settings.resetToDefaults(); current_settings.resetToDefaults();
fault_ack_received = false;
} }
void USBHandler::processUSBData(const uint8_t* data, uint32_t length) { void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
@@ -23,6 +24,18 @@ void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
DIAG("USB", "processUSBData: %lu bytes, state=%d", (unsigned long)length, (int)current_state); DIAG("USB", "processUSBData: %lu bytes, state=%d", (unsigned long)length, (int)current_state);
// FAULT_ACK: host sends exactly 4 bytes [0x40, 0x00, 0x00, 0x00].
// Requires exact 4-byte packet length: settings packets are always
// >= 82 bytes, so a lone 4-byte payload is unambiguous. Scanning
// inside larger packets would false-trigger on the IEEE 754
// encoding of 2.0 (0x4000000000000000) embedded in settings doubles.
static const uint8_t FAULT_ACK_SEQ[4] = {0x40, 0x00, 0x00, 0x00};
if (length == 4 && memcmp(data, FAULT_ACK_SEQ, 4) == 0) {
fault_ack_received = true;
DIAG("USB", "FAULT_ACK received");
return;
}
switch (current_state) { switch (current_state) {
case USBState::WAITING_FOR_START: case USBState::WAITING_FOR_START:
processStartFlag(data, length); processStartFlag(data, length);
@@ -29,6 +29,11 @@ public:
// Reset USB handler // Reset USB handler
void reset(); void reset();
// Fault-acknowledgement: host sends FAULT_ACK (0x40) to clear
// system_emergency_state and exit the safe-mode blink loop.
bool isFaultAckReceived() const { return fault_ack_received; }
void clearFaultAck() { fault_ack_received = false; }
private: private:
RadarSettings current_settings; RadarSettings current_settings;
USBState current_state; USBState current_state;
@@ -38,6 +43,7 @@ private:
static constexpr uint32_t MAX_BUFFER_SIZE = 256; static constexpr uint32_t MAX_BUFFER_SIZE = 256;
uint8_t usb_buffer[MAX_BUFFER_SIZE]; uint8_t usb_buffer[MAX_BUFFER_SIZE];
uint32_t buffer_index; uint32_t buffer_index;
bool fault_ack_received;
void processStartFlag(const uint8_t* data, uint32_t length); void processStartFlag(const uint8_t* data, uint32_t length);
void processSettingsData(const uint8_t* data, uint32_t length); void processSettingsData(const uint8_t* data, uint32_t length);
@@ -627,7 +627,7 @@ typedef enum {
static SystemError_t last_error = ERROR_NONE; static SystemError_t last_error = ERROR_NONE;
static uint32_t error_count = 0; static uint32_t error_count = 0;
static bool system_emergency_state = false; static volatile bool system_emergency_state = false;
// Error handler function // Error handler function
SystemError_t checkSystemHealth(void) { SystemError_t checkSystemHealth(void) {
@@ -2054,6 +2054,10 @@ int main(void)
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin); HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin); HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
HAL_Delay(250); HAL_Delay(250);
if (usbHandler.isFaultAckReceived()) {
system_emergency_state = false;
usbHandler.clearFaultAck();
}
} }
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared"); DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
} }
@@ -70,7 +70,8 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \
test_gap3_idq_periodic_reread \ test_gap3_idq_periodic_reread \
test_gap3_emergency_state_ordering \ test_gap3_emergency_state_ordering \
test_gap3_overtemp_emergency_stop \ test_gap3_overtemp_emergency_stop \
test_gap3_health_watchdog_cold_start test_gap3_health_watchdog_cold_start \
test_gap3_fault_ack_clears_emergency
# Tests that need platform_noos_stm32.o + mocks # Tests that need platform_noos_stm32.o + mocks
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
@@ -178,6 +179,9 @@ test_gap3_overtemp_emergency_stop: test_gap3_overtemp_emergency_stop.c
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
$(CC) $(CFLAGS) $< -o $@ $(CC) $(CFLAGS) $< -o $@
test_gap3_fault_ack_clears_emergency: test_gap3_fault_ack_clears_emergency.c
$(CC) $(CFLAGS) $< -o $@
# Tests that need platform_noos_stm32.o + mocks # Tests that need platform_noos_stm32.o + mocks
$(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ) $(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@ $(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@
@@ -0,0 +1,121 @@
/*******************************************************************************
* test_gap3_fault_ack_clears_emergency.c
*
* Verifies the FAULT_ACK clear path for system_emergency_state:
* - USBHandler detects exactly [0x40, 0x00, 0x00, 0x00] in a 4-byte packet
* - Detection is false-positive-free: larger packets (settings data) carrying
* the same bytes as a subsequence must NOT trigger the ack
* - Main-loop blink logic clears system_emergency_state on receipt
*
* Logic extracted from USBHandler.cpp + main.cpp to mirror the actual code
* paths without requiring HAL headers.
******************************************************************************/
#include <assert.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
/* ── Simulated USBHandler state ─────────────────────────────────────────── */
static bool fault_ack_received = false;
static volatile bool system_emergency_state = false;
static const uint8_t FAULT_ACK_SEQ[4] = {0x40, 0x00, 0x00, 0x00};
/* Mirrors USBHandler::processUSBData() detection logic */
static void sim_processUSBData(const uint8_t *data, uint32_t length)
{
if (data == NULL || length == 0) return;
if (length == 4 && memcmp(data, FAULT_ACK_SEQ, 4) == 0) {
fault_ack_received = true;
return;
}
/* (normal state machine omitted — not under test here) */
}
/* Mirrors one iteration of the blink loop in main.cpp */
static void sim_blink_iteration(void)
{
/* HAL_GPIO_TogglePin + HAL_Delay omitted */
if (fault_ack_received) {
system_emergency_state = false;
fault_ack_received = false;
}
}
int main(void)
{
printf("=== Gap-3 FAULT_ACK clears system_emergency_state ===\n");
/* Test 1: exact 4-byte FAULT_ACK packet sets the flag */
printf(" Test 1: exact FAULT_ACK packet detected... ");
fault_ack_received = false;
const uint8_t ack_pkt[4] = {0x40, 0x00, 0x00, 0x00};
sim_processUSBData(ack_pkt, 4);
assert(fault_ack_received == true);
printf("PASS\n");
/* Test 2: flag cleared and system_emergency_state exits blink loop */
printf(" Test 2: blink loop exits on FAULT_ACK... ");
system_emergency_state = true;
fault_ack_received = true;
sim_blink_iteration();
assert(system_emergency_state == false);
assert(fault_ack_received == false);
printf("PASS\n");
/* Test 3: blink loop does NOT exit without ack */
printf(" Test 3: blink loop holds without ack... ");
system_emergency_state = true;
fault_ack_received = false;
sim_blink_iteration();
assert(system_emergency_state == true);
printf("PASS\n");
/* Test 4: settings-sized packet carrying [0x40,0x00,0x00,0x00] as first
* 4 bytes does NOT trigger ack (IEEE 754 double 2.0 = 0x4000000000000000) */
printf(" Test 4: settings packet with 2.0 double does not false-trigger... ");
fault_ack_received = false;
uint8_t settings_pkt[82];
memset(settings_pkt, 0, sizeof(settings_pkt));
/* First 4 bytes look like FAULT_ACK but packet length is 82 */
settings_pkt[0] = 0x40; settings_pkt[1] = 0x00;
settings_pkt[2] = 0x00; settings_pkt[3] = 0x00;
sim_processUSBData(settings_pkt, sizeof(settings_pkt));
assert(fault_ack_received == false);
printf("PASS\n");
/* Test 5: 3-byte packet (truncated) does not trigger */
printf(" Test 5: truncated 3-byte packet ignored... ");
fault_ack_received = false;
const uint8_t short_pkt[3] = {0x40, 0x00, 0x00};
sim_processUSBData(short_pkt, 3);
assert(fault_ack_received == false);
printf("PASS\n");
/* Test 6: wrong opcode byte in 4-byte packet does not trigger */
printf(" Test 6: wrong opcode (0x28 AGC_ENABLE) not detected as FAULT_ACK... ");
fault_ack_received = false;
const uint8_t agc_pkt[4] = {0x28, 0x00, 0x00, 0x01};
sim_processUSBData(agc_pkt, 4);
assert(fault_ack_received == false);
printf("PASS\n");
/* Test 7: multiple blink iterations — loop stays active until ack */
printf(" Test 7: loop stays active across multiple iterations until ack... ");
system_emergency_state = true;
fault_ack_received = false;
sim_blink_iteration();
assert(system_emergency_state == true);
sim_blink_iteration();
assert(system_emergency_state == true);
/* Now ack arrives */
sim_processUSBData(ack_pkt, 4);
assert(fault_ack_received == true);
sim_blink_iteration();
assert(system_emergency_state == false);
printf("PASS\n");
printf("\n=== Gap-3 FAULT_ACK: ALL 7 TESTS PASSED ===\n\n");
return 0;
}
+7 -7
View File
@@ -169,11 +169,11 @@ endfunction
// ========================================================================= // =========================================================================
// Clamp a wider signed value to [-7, +7] // Clamp a wider signed value to [-7, +7]
function signed [3:0] clamp_gain; function signed [3:0] clamp_gain;
input signed [4:0] val; // 5-bit to handle overflow from add input signed [5:0] val; // 6-bit: covers [-22,+22] (max |gain|+step = 7+15)
begin begin
if (val > 5'sd7) if (val > 6'sd7)
clamp_gain = 4'sd7; clamp_gain = 4'sd7;
else if (val < -5'sd7) else if (val < -6'sd7)
clamp_gain = -4'sd7; clamp_gain = -4'sd7;
else else
clamp_gain = val[3:0]; clamp_gain = val[3:0];
@@ -246,15 +246,15 @@ always @(posedge clk or negedge reset_n) begin
// Use inclusive counts/peaks (accounting for simultaneous valid_in) // Use inclusive counts/peaks (accounting for simultaneous valid_in)
if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin
// Clipping detected: reduce gain immediately (attack) // Clipping detected: reduce gain immediately (attack)
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) - agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain[3], agc_gain}) -
$signed({1'b0, agc_attack})); $signed({2'b00, agc_attack}));
holdoff_counter <= agc_holdoff; // Reset holdoff holdoff_counter <= agc_holdoff; // Reset holdoff
end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7]) end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7])
< agc_target) begin < agc_target) begin
// Signal too weak: increase gain after holdoff expires // Signal too weak: increase gain after holdoff expires
if (holdoff_counter == 4'd0) begin if (holdoff_counter == 4'd0) begin
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) + agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain[3], agc_gain}) +
$signed({1'b0, agc_decay})); $signed({2'b00, agc_decay}));
end else begin end else begin
holdoff_counter <= holdoff_counter - 4'd1; holdoff_counter <= holdoff_counter - 4'd1;
end end
+9
View File
@@ -103,6 +103,15 @@ class Opcode(IntEnum):
STATUS_REQUEST = 0xFF STATUS_REQUEST = 0xFF
# MCU-only commands — NOT dispatched to the FPGA opcode switch.
# These values have no corresponding case in radar_system_top.v.
# Listed here so the GUI can build and send them via build_command().
# contract_parser.py filters MCU_ONLY_OPCODES out of the Python/Verilog
# bidirectional check.
FAULT_ACK = 0x40 # Exact 4-byte CDC packet; clears system_emergency_state
MCU_ONLY_OPCODES: frozenset[int] = frozenset({0x40})
# ============================================================================ # ============================================================================
# Data Structures # Data Structures
# ============================================================================ # ============================================================================
@@ -108,12 +108,23 @@ class ConcatWidth:
def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]: def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]:
"""Parse the Opcode enum from radar_protocol.py. """Parse the Opcode enum from radar_protocol.py.
Returns {opcode_value: OpcodeEntry}. Returns {opcode_value: OpcodeEntry}, excluding MCU_ONLY_OPCODES.
MCU-only opcodes have no FPGA case statement and must not appear in
the bidirectional Python/Verilog contract check.
""" """
if filepath is None: if filepath is None:
filepath = GUI_DIR / "radar_protocol.py" filepath = GUI_DIR / "radar_protocol.py"
text = filepath.read_text() text = filepath.read_text()
# Extract MCU_ONLY_OPCODES set so we can exclude those values below.
mcu_only: set[int] = set()
m_set = re.search(r'MCU_ONLY_OPCODES[^=]*=\s*frozenset\(\{([^}]*)\}\)', text)
if m_set:
for tok in m_set.group(1).split(','):
tok = tok.strip()
if tok.startswith(('0x', '0X')):
mcu_only.add(int(tok, 16))
# Find the Opcode class body # Find the Opcode class body
match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL) match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL)
if not match: if not match:
@@ -123,6 +134,7 @@ def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]
for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()): for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()):
name = m.group(1) name = m.group(1)
value = int(m.group(2), 16) value = int(m.group(2), 16)
if value not in mcu_only:
opcodes[value] = OpcodeEntry(name=name, value=value) opcodes[value] = OpcodeEntry(name=name, value=value)
return opcodes return opcodes
+2 -3
View File
@@ -7,7 +7,6 @@
[![Frequency: 10.5GHz](https://img.shields.io/badge/Frequency-10.5GHz-blue)](https://github.com/NawfalMotii79/PLFM_RADAR) [![Frequency: 10.5GHz](https://img.shields.io/badge/Frequency-10.5GHz-blue)](https://github.com/NawfalMotii79/PLFM_RADAR)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/NawfalMotii79/PLFM_RADAR/pulls) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/NawfalMotii79/PLFM_RADAR/pulls)
![AERIS-10 Radar System](https://raw.githubusercontent.com/NawfalMotii79/PLFM_RADAR/main/8_Utils/3fb1dabf-2c6d-4b5d-b471-48bc461ce914.jpg)
AERIS-10 is an open-source, low-cost 10.5 GHz phased array radar system featuring Pulse Linear Frequency Modulated (LFM) modulation. Available in two versions (3km and 20km range), it's designed for researchers, drone developers, and serious SDR enthusiasts who want to explore and experiment with phased array radar technology. AERIS-10 is an open-source, low-cost 10.5 GHz phased array radar system featuring Pulse Linear Frequency Modulated (LFM) modulation. Available in two versions (3km and 20km range), it's designed for researchers, drone developers, and serious SDR enthusiasts who want to explore and experiment with phased array radar technology.
@@ -47,7 +46,7 @@ The AERIS-10 main sub-systems are:
- **Main Board** containing: - **Main Board** containing:
- **DAC** - Generates the RADAR Chirps - **DAC** - Generates the RADAR Chirps
- **2x Microwave Mixers (LT5552)** - For up-conversion and IF-down-conversion - **2x Microwave Mixers (LTC5552)** - For up-conversion and IF-down-conversion
- **4x 4-Channel Phase Shifters (ADAR1000)** - For RX and TX chain beamforming - **4x 4-Channel Phase Shifters (ADAR1000)** - For RX and TX chain beamforming
- **16x Front End Chips (ADTR1107)** - Used for both Low Noise Amplifying (RX) and Power Amplifying (TX) stages - **16x Front End Chips (ADTR1107)** - Used for both Low Noise Amplifying (RX) and Power Amplifying (TX) stages
- **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board: - **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board:
@@ -92,7 +91,7 @@ The AERIS-10 main sub-systems are:
### Processing Pipeline ### Processing Pipeline
1. **Waveform Generation** - DAC creates LFM chirps 1. **Waveform Generation** - DAC creates LFM chirps
2. **Up/Down Conversion** - LT5552 mixers handle frequency translation 2. **Up/Down Conversion** - LTC5552 mixers handle frequency translation
3. **Beam Steering** - ADAR1000 phase shifters control 16 elements 3. **Beam Steering** - ADAR1000 phase shifters control 16 elements
4. **Signal Processing (FPGA)**: 4. **Signal Processing (FPGA)**:
- Raw ADC data capture - Raw ADC data capture