diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp index 6410153..d4f3773 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp @@ -13,6 +13,7 @@ void USBHandler::reset() { start_flag_received = false; buffer_index = 0; current_settings.resetToDefaults(); + fault_ack_received = false; } 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); + // 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) { case USBState::WAITING_FOR_START: processStartFlag(data, length); diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.h b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.h index 725fef2..331ffd1 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.h +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.h @@ -29,6 +29,11 @@ public: // Reset USB handler 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: RadarSettings current_settings; USBState current_state; @@ -38,6 +43,7 @@ private: static constexpr uint32_t MAX_BUFFER_SIZE = 256; uint8_t usb_buffer[MAX_BUFFER_SIZE]; uint32_t buffer_index; + bool fault_ack_received; void processStartFlag(const uint8_t* data, uint32_t length); void processSettingsData(const uint8_t* data, uint32_t length); 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 01876a6..ae76731 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 @@ -2054,6 +2054,10 @@ int main(void) HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin); HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin); HAL_Delay(250); + if (usbHandler.isFaultAckReceived()) { + system_emergency_state = false; + usbHandler.clearFaultAck(); + } } DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared"); } diff --git a/9_Firmware/9_1_Microcontroller/tests/Makefile b/9_Firmware/9_1_Microcontroller/tests/Makefile index 1daece2..f190c65 100644 --- a/9_Firmware/9_1_Microcontroller/tests/Makefile +++ b/9_Firmware/9_1_Microcontroller/tests/Makefile @@ -70,7 +70,8 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \ test_gap3_idq_periodic_reread \ test_gap3_emergency_state_ordering \ 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_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 $(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_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ) $(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@ diff --git a/9_Firmware/9_1_Microcontroller/tests/test_gap3_fault_ack_clears_emergency.c b/9_Firmware/9_1_Microcontroller/tests/test_gap3_fault_ack_clears_emergency.c new file mode 100644 index 0000000..dafaf98 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_gap3_fault_ack_clears_emergency.c @@ -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 +#include +#include +#include +#include + +/* ── 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; +} diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 52176d2..6494f5b 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -103,6 +103,15 @@ class Opcode(IntEnum): 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 # ============================================================================ diff --git a/9_Firmware/tests/cross_layer/contract_parser.py b/9_Firmware/tests/cross_layer/contract_parser.py index 0a11e52..dcb5dec 100644 --- a/9_Firmware/tests/cross_layer/contract_parser.py +++ b/9_Firmware/tests/cross_layer/contract_parser.py @@ -108,12 +108,23 @@ class ConcatWidth: def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]: """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: filepath = GUI_DIR / "radar_protocol.py" 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 match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL) if not match: @@ -123,7 +134,8 @@ 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()): name = m.group(1) value = int(m.group(2), 16) - opcodes[value] = OpcodeEntry(name=name, value=value) + if value not in mcu_only: + opcodes[value] = OpcodeEntry(name=name, value=value) return opcodes