chirp-v2 PR-B: 3-waveform mem generator + 11 new .mem files

Rewrite gen_chirp_mem.py to emit the SHORT (1 µs), MEDIUM (5 µs), and LONG
(30 µs) waveform set on both TX and RX paths. The script is now the single
source for every chirp .mem file; the legacy 6-file set on disk
(long_chirp_lut.mem, long_chirp_seg{0,1}_{i,q}.mem, short_chirp_{i,q}.mem)
is no longer regenerated and gets deleted in PR-C/PR-E when its consumer
modules are removed.

Generated artifacts (committed):
  TX (8-bit unsigned offset-binary, fs_dac = 120 MHz):
    tx_short_lut.mem    120  lines
    tx_medium_lut.mem   600  lines
    tx_long_lut.mem     3600 lines
  RX (Q15 I/Q hex, fs_sys = 100 MHz, all 2048 lines for uniform BRAM sizing):
    rx_short_i.mem  / rx_short_q.mem    100  active + 1948 zero-pad
    rx_medium_i.mem / rx_medium_q.mem   500  active + 1548 zero-pad
    rx_long_seg0_i.mem  / rx_long_seg0_q.mem   2048 (samples [0..2047])
    rx_long_seg1_i.mem  / rx_long_seg1_q.mem   952 active + 1096 zero-pad

Phase model unchanged from chirp-v1: phi(n) = 2π·F_BASEBAND_LOW·t +
π·(BW/T)·t² with F_BASEBAND_LOW=10 MHz and BW=20 MHz. The same formula now
runs three durations and two sample rates from one helper.
rx_long_seg0_i.mem is bit-exact to the legacy long_chirp_seg0_i.mem on disk
(diff -q reports identical) — proves the SHORT/MEDIUM additions did not
perturb the LONG path.

Verification:
  - all 11 files have correct line counts (above)
  - script is idempotent (re-run produces byte-identical output)
  - ruff clean (one E501 line-length + two RUF046 redundant-int casts fixed)
  - phase regression at long-seg0 against pre-chirp-v2 reference: bit-exact

No RTL or testbench changes. The legacy .mem files remain on disk for the
existing chirp_memory_loader_param.v / plfm_chirp_controller.v consumers
until PR-C and PR-E delete those modules. No module references the new
files yet.
This commit is contained in:
Jason
2026-04-30 17:46:08 +05:45
parent 340c6d628d
commit f5b8e7a20b
12 changed files with 20854 additions and 174 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+150 -174
View File
@@ -1,55 +1,41 @@
#!/usr/bin/env python3
"""
gen_chirp_mem.py Generate all chirp .mem files for AERIS-10 FPGA.
gen_chirp_mem.py Generate the 11 chirp .mem files for the AERIS-10 FPGA.
Generates the 6 chirp .mem files used by chirp_memory_loader_param.v:
- long_chirp_seg{0,1}_{i,q}.mem (4 files, 2048 lines each)
- short_chirp_{i,q}.mem (2 files, 50 lines each)
3-ladder waveform set (chirp-v2 PR-B). Replaces the legacy 2-waveform script.
The legacy 6-file output (long_chirp_seg{0,1}_{i,q}.mem, short_chirp_{i,q}.mem,
long_chirp_lut.mem) is no longer regenerated by this script; the legacy files
on disk are deleted in PR-C (RX) and PR-E (TX) when their consumers go away.
Long chirp:
The 3000-sample baseband chirp (30 us at 100 MHz system clock) is
segmented into 2 blocks of 2048 samples. Each segment covers a
different time window of the chirp:
seg0: samples 0 .. 2047
seg1: samples 2048 .. 4095 (only 952 valid chirp samples; 1096 zeros)
TX side (DAC LUTs, 8-bit unsigned offset-binary, fs_dac = 120 MHz):
tx_short_lut.mem 120 samples (1 µs)
tx_medium_lut.mem 600 samples (5 µs)
tx_long_lut.mem 3600 samples (30 µs)
The memory loader stores 2*2048 = 4096 contiguous samples indexed
by {segment_select[0], sample_addr[10:0]}. The long chirp has
3000 samples, so:
seg0: chirp[0..2047] all valid data
seg1: chirp[2048..2999] + 1096 zeros (samples past chirp end)
RX side (matched-filter reference, Q15 I/Q hex, fs_sys = 100 MHz, all 2048 entries):
rx_short_i.mem / rx_short_q.mem 100 active + 1948 zero-pad
rx_medium_i.mem / rx_medium_q.mem 500 active + 1548 zero-pad
rx_long_seg0_i.mem / rx_long_seg0_q.mem 2048 (samples 0..2047 of long chirp)
rx_long_seg1_i.mem / rx_long_seg1_q.mem 952 active + 1096 zero-pad
(samples 2048..2999 of long chirp)
Short chirp:
50 samples (0.5 us at 100 MHz), same chirp formula with
T_SHORT_CHIRP and CHIRP_BW.
Phase model (baseband, post-DDC):
phase(n) = 2*pi*F_BASEBAND_LOW*t + pi * chirp_rate * t^2, t = n / FS_SYS
Phase model (baseband, post-DDC for RX, pre-upmix for TX):
phase(n) = 2π · F_BASEBAND_LOW · t + π · chirp_rate ·
chirp_rate = CHIRP_BW / T_chirp
F_BASEBAND_LOW = 10 MHz (DAC chirp low-edge frequency)
F_BASEBAND_LOW = 10 MHz
CHIRP_BW = 20 MHz (uniform across all three waveforms same range res)
This produces a F_BASEBAND_LOW..(F_BASEBAND_LOW+CHIRP_BW) baseband upchirp.
End-to-end frequency plan (TX-I, unchanged from chirp-v1):
DAC LUT : 10..30 MHz @ fs_dac=120 MHz
TX upmix : LO=10.500 GHz, high-side -> RF transmitted: 10.510..10.530 GHz
RX downmix: LO=10.380 GHz, high-side -> IF at ADC: 130..150 MHz
DDC NCO : 120 MHz exactly (ddc_400m.v) -> baseband: 10..30 MHz
End-to-end frequency plan (TX-I, 2026-04-28):
DAC LUT : 10..30 MHz @ fs_dac=120 MHz (plfm_chirp_controller.v;
Hilbert-confirmed for both
long and short LUTs)
TX upmix : LO=10.500 GHz (adf4382a_manager.h:35), high-side
-> RF transmitted: 10.510..10.530 GHz
RX downmix: LO=10.380 GHz (adf4382a_manager.h:36), high-side
-> IF at ADC: 130..150 MHz
DDC NCO : 120 MHz exactly (ddc_400m.v:201)
-> baseband: 10..30 MHz <-- matched-filter reference
radar_scene.py uses the same F_BASEBAND_LOW and CHIRP_BW constants; both
must stay in sync.
Sideband orientation (high-side at both mixers) is the conventional choice
and consistent with all design comments / antenna match (10.25..10.75 GHz);
loopback capture would settle it definitively. If either mixer turns out to
be low-side, the sign of F_BASEBAND_LOW flips and/or the chirp direction
reverses; revisit before re-generating .mem files.
radar_scene.py uses the same F_BASEBAND_LOW; both must stay in sync.
Scaling: 0.9 * 32767 (Q15)
Scaling: 0.9 of full-scale for the Q15 (RX) path. TX LUT scales 127·0.9 114
into the ±127 swing around the 128 (8'd128) idle code.
Usage:
python3 gen_chirp_mem.py
@@ -60,163 +46,153 @@ import os
import sys
# ============================================================================
# AERIS-10 Parameters (matching radar_scene.py)
# AERIS-10 Parameters
# ============================================================================
CHIRP_BW = 20e6 # 20 MHz sweep bandwidth
FS_SYS = 100e6 # System clock (100 MHz, post-CIC)
T_LONG_CHIRP = 30e-6 # 30 us long chirp duration
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp duration
FFT_SIZE = 2048
# DAC chirp baseband low-edge frequency. The TX LUT in plfm_chirp_controller.v
# is a 10..30 MHz upchirp at fs_dac=120 MHz (Hilbert-confirmed for both long
# and short LUTs). With TX_LO=10.500 GHz, RX_LO=10.380 GHz (adf4382a_manager.h)
# and the 120 MHz DDC NCO (ddc_400m.v), high-side mixing places the post-DDC
# echo at 10..30 MHz baseband, not 0..20 MHz. The matched-filter reference
# must include this +10 MHz DC offset.
F_BASEBAND_LOW = 10e6
LONG_CHIRP_SAMPLES = int(T_LONG_CHIRP * FS_SYS) # 3000
SHORT_CHIRP_SAMPLES = int(T_SHORT_CHIRP * FS_SYS) # 50
LONG_SEGMENTS = 2
SCALE = 0.9 # Q15 scaling factor (matches radar_scene.py)
Q15_MAX = 32767
CHIRP_BW = 20e6 # 20 MHz sweep — same for all three waveforms
FS_SYS = 100e6 # System clock for RX matched filter
FS_DAC = 120e6 # DAC clock for TX waveform synthesis
F_BASEBAND_LOW = 10e6 # Low edge of post-DDC baseband
SCALE = 0.9 # Backoff from full-scale (both paths)
# Output directory (FPGA RTL root, where .mem files live)
# Waveform durations (seconds) — see radar_params.vh RP_DEF_*_CHIRP_CYCLES_V2
T_SHORT = 1e-6 # 1 µs (PR-B v2; chirp-v1 was 0.5 µs)
T_MEDIUM = 5e-6 # 5 µs
T_LONG = 30e-6 # 30 µs
# RX FFT segment size — chirp_reference_rom (PR-C) uses one 2048-entry BRAM
# per waveform lane. LONG is the only waveform that crosses the segment
# boundary; SHORT and MEDIUM fit in a single 2048-entry buffer with zero-pad.
FFT_SIZE = 2048
# Q15 / 8-bit envelopes
Q15_MAX = 32767
DAC_MID = 128 # 8'd128 idle code (offset-binary)
DAC_AMP = 127 # ±127 swing around DAC_MID
# Output directory: FPGA RTL root (.mem files live alongside the .v files)
MEM_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')
def generate_full_long_chirp():
"""
Generate the full 3000-sample baseband chirp in Q15.
# ============================================================================
# Sample generation — single phase model reused for both TX and RX
# ============================================================================
def chirp_phase(n: int, fs: float, t_chirp: float) -> float:
"""Phase at sample n for the LFM upchirp."""
chirp_rate = CHIRP_BW / t_chirp
t = n / fs
return 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
Returns:
(chirp_i, chirp_q): lists of 3000 signed 16-bit integers
"""
chirp_rate = CHIRP_BW / T_LONG_CHIRP # Hz/s
chirp_i = []
chirp_q = []
for n in range(LONG_CHIRP_SAMPLES):
t = n / FS_SYS
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
re_val = round(Q15_MAX * SCALE * math.cos(phase))
im_val = round(Q15_MAX * SCALE * math.sin(phase))
chirp_i.append(max(-32768, min(32767, re_val)))
chirp_q.append(max(-32768, min(32767, im_val)))
def gen_rx_iq(t_chirp: float):
"""Generate Q15 I/Q samples at fs_sys for one chirp duration."""
n_samples = round(t_chirp * FS_SYS)
chirp_i, chirp_q = [], []
for n in range(n_samples):
ph = chirp_phase(n, FS_SYS, t_chirp)
re = max(-32768, min(32767, round(Q15_MAX * SCALE * math.cos(ph))))
im = max(-32768, min(32767, round(Q15_MAX * SCALE * math.sin(ph))))
chirp_i.append(re)
chirp_q.append(im)
return chirp_i, chirp_q
def generate_short_chirp():
"""
Generate the 50-sample short chirp in Q15.
Returns:
(chirp_i, chirp_q): lists of 50 signed 16-bit integers
"""
chirp_rate = CHIRP_BW / T_SHORT_CHIRP # Hz/s (much faster sweep)
chirp_i = []
chirp_q = []
for n in range(SHORT_CHIRP_SAMPLES):
t = n / FS_SYS
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
re_val = round(Q15_MAX * SCALE * math.cos(phase))
im_val = round(Q15_MAX * SCALE * math.sin(phase))
chirp_i.append(max(-32768, min(32767, re_val)))
chirp_q.append(max(-32768, min(32767, im_val)))
return chirp_i, chirp_q
def gen_tx_real(t_chirp: float):
"""Generate 8-bit unsigned offset-binary real samples at fs_dac."""
n_samples = round(t_chirp * FS_DAC)
samples = []
for n in range(n_samples):
ph = chirp_phase(n, FS_DAC, t_chirp)
# Real-valued upchirp: cos(phase). Map ±1 → 0..255 with 128 as DC.
v = round(DAC_AMP * SCALE * math.cos(ph)) + DAC_MID
samples.append(max(0, min(255, v)))
return samples
def to_hex16(value):
"""Convert signed 16-bit integer to 4-digit hex string (unsigned representation)."""
# ============================================================================
# .mem writers
# ============================================================================
def to_hex_signed16(value: int) -> str:
"""Signed 16-bit → 4-char hex (two's complement, lowercase)."""
if value < 0:
value += 0x10000
return f"{value:04x}"
def write_mem_file(filename, values):
"""Write a list of 16-bit signed integers to a .mem file (hex format)."""
def to_hex_u8(value: int) -> str:
"""Unsigned 8-bit → 2-char hex (uppercase, matches legacy long_chirp_lut.mem)."""
return f"{value:02X}"
def write_lines(filename: str, lines):
path = os.path.join(MEM_DIR, filename)
with open(path, 'w') as f:
for v in values:
f.write(to_hex16(v) + '\n')
for line in lines:
f.write(line + '\n')
return path
def write_rx_iq_padded(prefix: str, chirp_i, chirp_q, target_len: int = FFT_SIZE,
offset: int = 0):
"""
Write {prefix}_i.mem and {prefix}_q.mem with `target_len` lines.
`offset` selects which slice of the chirp to write used for LONG segmentation.
Cells past the chirp end are zero-padded. Cells before the chirp start (impossible
here but kept symmetric) are also zero.
"""
i_lines, q_lines = [], []
for k in range(target_len):
src_idx = k + offset
if 0 <= src_idx < len(chirp_i):
i_lines.append(to_hex_signed16(chirp_i[src_idx]))
q_lines.append(to_hex_signed16(chirp_q[src_idx]))
else:
i_lines.append('0000')
q_lines.append('0000')
write_lines(f"{prefix}_i.mem", i_lines)
write_lines(f"{prefix}_q.mem", q_lines)
def write_tx_lut(filename: str, samples):
"""Write 8-bit TX LUT — one sample per line, exactly len(samples) lines."""
write_lines(filename, [to_hex_u8(v) for v in samples])
# ============================================================================
# main
# ============================================================================
def verify_phase_match():
"""
Cross-check that each generated chirp matches the closed-form phase model
bit-exactly. radar_scene.py uses the same phase formula and constants;
if it ever diverges, this assertion will fire.
"""
for name, t_chirp in [("SHORT", T_SHORT), ("MEDIUM", T_MEDIUM), ("LONG", T_LONG)]:
i, q = gen_rx_iq(t_chirp)
n_check = min(64, len(i))
for n in range(n_check):
ph = chirp_phase(n, FS_SYS, t_chirp)
ei = max(-32768, min(32767, round(Q15_MAX * SCALE * math.cos(ph))))
eq = max(-32768, min(32767, round(Q15_MAX * SCALE * math.sin(ph))))
assert i[n] == ei and q[n] == eq, f"{name} phase mismatch at n={n}"
def main():
verify_phase_match()
# ---- Long chirp ----
long_i, long_q = generate_full_long_chirp()
# ---- TX LUTs (8-bit real, fs_dac = 120 MHz) ----
write_tx_lut("tx_short_lut.mem", gen_tx_real(T_SHORT)) # 120 lines
write_tx_lut("tx_medium_lut.mem", gen_tx_real(T_MEDIUM)) # 600 lines
write_tx_lut("tx_long_lut.mem", gen_tx_real(T_LONG)) # 3600 lines
# Verify first sample matches generate_reference_chirp_q15() from radar_scene.py
# (which only generates the first 1024 samples)
# Segment into 4 x 1024 blocks
for seg in range(LONG_SEGMENTS):
start = seg * FFT_SIZE
end = start + FFT_SIZE
seg_i = []
seg_q = []
valid_count = 0
for idx in range(start, end):
if idx < LONG_CHIRP_SAMPLES:
seg_i.append(long_i[idx])
seg_q.append(long_q[idx])
valid_count += 1
else:
seg_i.append(0)
seg_q.append(0)
FFT_SIZE - valid_count
write_mem_file(f"long_chirp_seg{seg}_i.mem", seg_i)
write_mem_file(f"long_chirp_seg{seg}_q.mem", seg_q)
# ---- Short chirp ----
short_i, short_q = generate_short_chirp()
write_mem_file("short_chirp_i.mem", short_i)
write_mem_file("short_chirp_q.mem", short_q)
# ---- Verification summary ----
# Self-check: recompute the phase formula and verify the seg0 .mem matches.
# radar_scene.py.generate_reference_chirp_q15() uses the same phase form
# and the same F_BASEBAND_LOW; the two stay in sync by construction.
chirp_rate = CHIRP_BW / T_LONG_CHIRP
mismatches = 0
for n in range(FFT_SIZE):
t = n / FS_SYS
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
expected_i = max(-32768, min(32767, round(Q15_MAX * SCALE * math.cos(phase))))
expected_q = max(-32768, min(32767, round(Q15_MAX * SCALE * math.sin(phase))))
if long_i[n] != expected_i or long_q[n] != expected_q:
mismatches += 1
if mismatches == 0:
pass
else:
return 1
# Check magnitude envelope
max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q, strict=False))
# Check seg1 zero padding (samples 3000-4095 should be zero)
seg1_i_path = os.path.join(MEM_DIR, 'long_chirp_seg1_i.mem')
with open(seg1_i_path) as f:
seg1_lines = [line.strip() for line in f if line.strip()]
# Indices 952..2047 in seg1 (global 3000..4095) should be zero
nonzero_tail = sum(1 for line in seg1_lines[952:] if line != '0000')
if nonzero_tail == 0:
pass
else:
pass
# ---- RX I/Q (Q15, fs_sys = 100 MHz, all padded to FFT_SIZE=2048) ----
short_i, short_q = gen_rx_iq(T_SHORT) # 100 active samples
medium_i, medium_q = gen_rx_iq(T_MEDIUM) # 500 active samples
long_i, long_q = gen_rx_iq(T_LONG) # 3000 active samples
write_rx_iq_padded("rx_short", short_i, short_q) # offset=0
write_rx_iq_padded("rx_medium", medium_i, medium_q) # offset=0
write_rx_iq_padded("rx_long_seg0", long_i, long_q, offset=0) # [0..2047]
write_rx_iq_padded("rx_long_seg1", long_i, long_q, offset=FFT_SIZE) # [2048..2999]+pad
return 0
File diff suppressed because it is too large Load Diff
+600
View File
@@ -0,0 +1,600 @@
F2
E3
B9
7F
45
1C
0E
20
4C
88
C2
E8
F1
DA
A7
6A
32
12
12
34
6C
AB
DD
F2
E4
B7
78
3C
15
10
2F
68
A9
DD
F2
E2
B1
6F
33
11
15
3D
7C
BD
E9
F0
D0
93
4F
1D
0E
29
63
A8
DF
F2
DC
A2
5C
23
0E
24
5D
A4
DD
F2
DA
9F
57
1F
0E
2B
6B
B3
E7
F0
CB
87
40
13
15
44
8C
D0
F1
E2
A7
5C
20
0E
2F
74
BF
ED
EA
B7
6B
28
0E
29
6C
B9
EC
EB
B8
6A
26
0E
2D
74
C2
EF
E6
AA
59
1C
11
3E
8C
D5
F2
D5
8C
3C
10
1E
60
B3
EB
EA
B0
5D
1C
11
43
96
DD
F1
C6
74
28
0E
34
85
D3
F2
D0
7F
2F
0E
2F
80
D1
F2
D0
7E
2D
0E
34
88
D8
F2
C6
6F
23
10
43
9D
E4
ED
B0
55
15
1A
60
BB
F0
DC
8C
33
0E
34
8C
DD
EF
B7
59
16
1A
64
C2
F2
D3
7B
26
10
48
A8
EC
E3
94
36
0E
37
95
E4
EA
A2
41
0E
2F
8C
E0
EC
A7
45
0F
2E
8C
E1
EB
A3
40
0E
35
96
E7
E6
95
33
0E
44
A9
EE
DA
7E
23
14
5D
C3
F2
C3
5C
13
24
82
DF
EB
9D
36
0E
47
B1
F1
D0
6A
17
20
7C
DD
EB
9C
33
0F
4F
BB
F2
C2
57
10
2F
98
EB
DC
78
1C
1C
79
DD
EA
94
2B
12
61
CE
F0
A7
39
0F
52
C2
F2
B4
43
0E
49
BB
F2
B9
47
0E
47
B9
F2
B8
45
0E
4B
BF
F2
B0
3D
0F
56
C9
F0
A2
30
12
68
D8
EA
8C
21
1C
82
E7
DC
6E
13
2F
A3
F1
C2
4B
0E
4F
C7
F0
9C
28
18
7C
E5
DC
6A
11
37
B1
F2
B0
36
12
6D
DF
E2
72
13
34
AF
F2
AE
33
14
76
E4
DA
63
0F
44
C2
F0
95
21
21
96
F0
BF
40
10
6C
E1
DC
63
0F
49
C9
EC
86
18
2F
AF
F2
A2
26
1E
95
F1
B9
36
15
80
EC
C9
45
10
70
E5
D3
51
0E
64
E0
DA
59
0E
5E
DD
DC
5D
0E
5D
DD
DC
5B
0E
60
DF
D9
55
0E
68
E4
D2
4B
10
76
EA
C6
3D
15
88
F0
B5
2D
1E
9F
F2
9D
1D
2F
B9
EE
7F
11
49
D3
E0
5C
0E
6C
E8
C6
39
18
96
F2
9F
1C
34
C2
EA
6E
0E
60
E4
CB
3C
18
98
F2
98
17
3E
CE
E2
59
0F
7A
EF
B0
23
2D
BD
EA
6A
0E
6D
EC
B9
28
29
B9
EB
6B
0E
70
ED
B4
23
2F
C3
E6
5C
0F
82
F1
9F
17
44
D8
D5
40
1A
A5
F0
78
0E
6B
ED
B0
1F
38
D0
DA
45
18
A4
F0
74
0E
74
F0
A2
17
49
DF
C9
2F
29
C2
E3
4F
15
9F
F0
72
0E
7C
F2
94
11
5D
EA
B1
1C
43
DD
C8
2B
2F
CD
D9
3C
21
BB
E4
4D
18
AB
EB
5D
12
9D
EF
6A
0F
91
F1
74
0E
88
F2
7B
0E
82
F2
7F
0E
80
+120
View File
@@ -0,0 +1,120 @@
F2
E3
B7
7C
40
17
0F
2B
64
A8
DE
F2
DC
A3
5B
21
0E
2B
6E
BA
EB
EB
B7
68
24
0E
35
84
D2
F2
D1
80
2E
0E
35
8C
DC
F0
B7
58
15
1D
6E
CD
F2
C0
5B
13
24
80
DE
EB
9C
33
0F
52
C0
F2
B7
46
0E
47
BB
F2
B2
3C
0F
5D
D2
EB
8A
1D
24
98
F0
C0
40
10
6E
E3
D8
58
0E
5D
DC
DC
5B
0E
64
E3
D1
46
12
84
F0
AE
24
2B
BB
EB
6C
0E
6E
ED
B2
21
35
CD
DC
46
18
A8
EE
68
0F
8C
F2
7C
0E
80