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>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user