feat: 2048-pt FFT upgrade with decimation=4, 512 output bins, 6m spacing

Complete cross-layer upgrade from 1024-pt/64-bin to 2048-pt/512-bin FFT:

FPGA RTL (14+ modules):
- radar_params.vh: FFT_SIZE=2048, RANGE_BINS=512, 9-bit range, 6-bit stream
- fft_engine.v: 2048-pt FFT with XPM BRAM
- chirp_memory_loader_param.v: 2 segments x 2048 (was 4 x 1024)
- matched_filter_multi_segment.v: BRAM inference for overlap_cache, explicit ov_waddr
- mti_canceller.v: BRAM inference for prev_i/q arrays (was fabric FFs)
- doppler_processor.v: 16384-deep memory, 14-bit addressing
- cfar_ca.v: 512 rows, indentation fix
- radar_receiver_final.v: rising-edge detector for frame_complete, 11-bit sample_addr
- range_bin_decimator.v: 512 output bins
- usb_data_interface_ft2232h.v: bulk per-frame with Manhattan magnitude
- radar_mode_controller.v: XOR edge detector for toggle signals
- rx_gain_control.v: updated for new bin count

Python GUI + Protocol (8 files):
- radar_protocol.py: 512-bin bulk frame parser, LSB-first bitmap
- GUI_V65_Tk.py, v7/*.py: updated for 512 bins, 6m range resolution

Golden data + tests:
- All .hex/.csv/.npy golden references regenerated for 2048/512
- fft_twiddle_2048.mem added
- Deleted stale seg2/seg3 chirp mem files
- 9 new bulk frame cross-layer tests, deleted 6 stale per-sample tests
- Deleted stale tb_cross_layer_ft2232h.v and dead contract_parser functions
- Updated validate_mem_files.py for 2048/2-segment config

MCU: RadarSettings.cpp max_distance/map_size 1536->3072

All 4 CI jobs pass: 285 tests, 0 failures, 0 skips
This commit is contained in:
Jason
2026-04-16 17:27:55 +05:45
parent affa40a9d3
commit e9705e40b7
178 changed files with 687738 additions and 122880 deletions
+41 -130
View File
@@ -152,54 +152,6 @@ def parse_python_packet_constants(filepath: Path | None = None) -> dict[str, Pac
}
def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPacketField]:
"""
Extract byte offsets from parse_data_packet() by finding struct.unpack_from calls.
Returns fields in byte order.
"""
if filepath is None:
filepath = GUI_DIR / "radar_protocol.py"
text = filepath.read_text()
# Find parse_data_packet method body
match = re.search(
r'def parse_data_packet\(.*?\).*?(?=\n @|\n def |\nclass |\Z)',
text, re.DOTALL
)
if not match:
raise ValueError("Could not find parse_data_packet()")
body = match.group()
fields: list[DataPacketField] = []
# Match patterns like: range_q = _to_signed16(struct.unpack_from(">H", raw, 1)[0])
for m in re.finditer(
r'(\w+)\s*=\s*_to_signed16\(struct\.unpack_from\("(>[HIBhib])", raw, (\d+)\)',
body
):
name = m.group(1)
fmt = m.group(2)
offset = int(m.group(3))
fmt_char = fmt[-1].upper()
size = {"H": 2, "I": 4, "B": 1}[fmt_char]
fields.append(DataPacketField(
name=name, byte_start=offset,
byte_end=offset + size - 1,
width_bits=size * 8
))
# Match detection = raw[9] & 0x01
for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body):
name = m.group(1)
offset = int(m.group(2))
fields.append(DataPacketField(
name=name, byte_start=offset, byte_end=offset, width_bits=1
))
fields.sort(key=lambda f: f.byte_start)
return fields
def parse_python_status_fields(filepath: Path | None = None) -> list[StatusWordField]:
"""
Extract bit shift/mask operations from parse_status_packet().
@@ -354,34 +306,63 @@ def parse_verilog_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry
return opcodes
def _parse_verilog_defines(filepath: Path | None = None) -> dict[str, str]:
"""Parse `define macros from radar_params.vh into {name: raw_value}."""
if filepath is None:
filepath = FPGA_DIR / "radar_params.vh"
if not filepath.exists():
return {}
defines: dict[str, str] = {}
for m in re.finditer(
r'`define\s+(\w+)\s+(.+?)(?://.*)?$',
filepath.read_text(),
re.MULTILINE,
):
defines[m.group(1)] = m.group(2).strip()
return defines
def _resolve_verilog_literal(val_str: str) -> int:
"""Convert a Verilog literal (e.g. 6'b000_111 or 10000) to int."""
if "'" in val_str:
base_char = val_str.split("'")[1][0].lower()
digits = val_str.split("'")[1][1:].replace("_", "")
base = {"b": 2, "d": 10, "h": 16, "o": 8}[base_char]
return int(digits, base)
return int(val_str)
def parse_verilog_reset_defaults(filepath: Path | None = None) -> dict[str, int]:
"""
Parse the reset block from radar_system_top.v.
Returns {register_name: reset_value}.
Resolves `define macros from radar_params.vh.
"""
if filepath is None:
filepath = FPGA_DIR / "radar_system_top.v"
text = filepath.read_text()
defines = _parse_verilog_defines()
defaults: dict[str, int] = {}
# Match patterns like: host_radar_mode <= 2'b01;
# Also: host_detect_threshold <= 16'd10000;
# Also: host_stream_control <= `RP_STREAM_CTRL_DEFAULT;
for m in re.finditer(
r'(host_\w+)\s*<=\s*(\d+\'[bdho][0-9a-fA-F_]+|\d+)\s*;',
r'(host_\w+)\s*<=\s*(`\w+|\d+\'[bdho][0-9a-fA-F_]+|\d+)\s*;',
text
):
reg = m.group(1)
val_str = m.group(2)
# Parse Verilog literal
if "'" in val_str:
base_char = val_str.split("'")[1][0].lower()
digits = val_str.split("'")[1][1:].replace("_", "")
base = {"b": 2, "d": 10, "h": 16, "o": 8}[base_char]
value = int(digits, base)
# Resolve macro or parse Verilog literal
if val_str.startswith("`"):
macro_name = val_str[1:]
if macro_name not in defines:
continue # skip unresolvable macros
value = _resolve_verilog_literal(defines[macro_name])
else:
value = int(val_str)
value = _resolve_verilog_literal(val_str)
# Only keep first occurrence (the reset block comes before the
# opcode decode which also has <= assignments)
@@ -436,15 +417,15 @@ def parse_verilog_packet_constants(
return int(vlog_m.group(1))
return int(val, 16) if val.startswith("0x") else int(val)
header_val = _find(r"localparam\s+HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)")
_find(r"localparam\s+HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)") # bulk frames — no data header
footer_val = _find(r"localparam\s+FOOTER\s*=\s*(\d+'h[0-9a-fA-F]+)")
status_hdr = _find(r"localparam\s+STATUS_HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)")
data_size = _find(r"DATA_PKT_LEN\s*=\s*(\d+'d\d+)")
status_size = _find(r"STATUS_PKT_LEN\s*=\s*(\d+'d\d+)")
# FT2232H uses bulk per-frame transfers — no fixed data packet size.
# Only status packets have a fixed size.
return {
"data": PacketConstants(header=header_val, footer=footer_val, size=data_size),
"status": PacketConstants(header=status_hdr, footer=footer_val, size=status_size),
}
@@ -559,76 +540,6 @@ def get_usb_interface_port_widths(filepath: Path | None = None) -> dict[str, int
return widths
def parse_verilog_data_mux(
filepath: Path | None = None,
) -> list[DataPacketField]:
"""
Parse the data_pkt_byte mux from usb_data_interface_ft2232h.v.
Returns fields with byte positions and signal names.
"""
if filepath is None:
filepath = FPGA_DIR / "usb_data_interface_ft2232h.v"
text = filepath.read_text()
# Find the data mux case block
match = re.search(
r'always\s+@\(\*\)\s+begin\s+case\s*\(wr_byte_idx\)(.*?)endcase',
text, re.DOTALL
)
if not match:
raise ValueError("Could not find data_pkt_byte mux")
mux_body = match.group(1)
entries: list[tuple[int, str]] = []
for m in re.finditer(
r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);",
mux_body
):
idx = int(m.group(1))
expr = m.group(2).strip()
entries.append((idx, expr))
# Group consecutive bytes by signal root name
fields: list[DataPacketField] = []
i = 0
while i < len(entries):
idx, expr = entries[i]
if expr == "HEADER" or expr == "FOOTER":
i += 1
continue
# Extract signal name (e.g., range_profile_cap from range_profile_cap[31:24])
sig_match = re.match(r'(\w+?)(?:\[|$)', expr)
if not sig_match:
i += 1
continue
signal = sig_match.group(1)
start_byte = idx
end_byte = idx
# Find consecutive bytes of the same signal
j = i + 1
while j < len(entries):
next_idx, next_expr = entries[j]
if next_expr.startswith(signal):
end_byte = next_idx
j += 1
else:
break
n_bytes = end_byte - start_byte + 1
fields.append(DataPacketField(
name=signal.replace("_cap", ""),
byte_start=start_byte,
byte_end=end_byte,
width_bits=n_bytes * 8,
))
i = j
return fields
# ===================================================================
# STM32 / C layer parser
@@ -803,7 +714,7 @@ def parse_radar_params_vh() -> dict[str, int]:
"""
Parse `define values from radar_params.vh.
Returns dict like {"RP_FFT_SIZE": 1024, "RP_DECIMATION_FACTOR": 16, ...}.
Returns dict like {"RP_FFT_SIZE": 2048, "RP_DECIMATION_FACTOR": 4, ...}.
Only parses defines with simple integer or Verilog literal values.
Skips bit-width prefixed literals (e.g. 2'b00) — returns the numeric value.
"""