Files
gps/targets/fpga/ti375c529/tools/prepare_efinity_rtl.py
T

317 lines
12 KiB
Python

#!/usr/bin/env python3
"""Apply small, deterministic Efinity synthesis adaptations to generated RTL."""
from __future__ import annotations
import argparse
import math
import re
from pathlib import Path
ROCKET_STORE_DATA = """ else begin
automatic logic [3:0][63:0] _GEN_24 =
{{ex_rs_1}, {{2{ex_rs_1[31:0]}}}, {{4{ex_rs_1[15:0]}}}, {{8{ex_rs_1[7:0]}}}};
mem_reg_rs2 <= _GEN_24[ex_reg_mem_size];
end"""
EFINITY_STORE_DATA = """ else begin
// Efinity 2025.2 leaves the block-local packed automatic array above
// undriven (VDB-1002). Spell out the byte-lane replication instead.
case (ex_reg_mem_size)
2'h0: mem_reg_rs2 <= {8{ex_rs_1[7:0]}};
2'h1: mem_reg_rs2 <= {4{ex_rs_1[15:0]}};
2'h2: mem_reg_rs2 <= {2{ex_rs_1[31:0]}};
default: mem_reg_rs2 <= ex_rs_1;
endcase
end"""
ATOMIC_LUT_DECLARATION = re.compile(
r"^(?P<indent>\s*)automatic logic \[3:0\]\[3:0\] (?P<name>_GEN_[0-9]+) "
r"= '\{4'hC, 4'h8, 4'hE, 4'h6\};$",
re.MULTILINE,
)
NCO_ROM_START = '(*rom_style = "distributed" *) logic [PH_ACC_WL-1 : 0] freq_word_arr'
NCO_ROM_END = "logic [PH_ACC_WL-1 : 0] sin_phase, cos_phase;"
def parse_integer_parameter(text: str, name: str) -> int:
match = re.search(rf"\bparameter\s+{re.escape(name)}\s*=\s*([0-9]+)\s*;", text)
if match is None:
raise RuntimeError(f"could not find integer parameter {name}")
return int(match.group(1))
def parse_sv_integer(token: str) -> tuple[int, int | None]:
token = token.strip().replace("_", "")
match = re.fullmatch(r"(-)?([0-9]+)'([dDhHbB])([0-9a-fA-F]+)", token)
if match is not None:
sign, width_text, radix_text, digits = match.groups()
radix = {"d": 10, "h": 16, "b": 2}[radix_text.lower()]
value = int(digits, radix)
if sign:
value = -value
return value, int(width_text)
if re.fullmatch(r"-?[0-9]+", token):
return int(token), None
raise RuntimeError(f"unsupported SystemVerilog integer literal: {token!r}")
def parse_array_parameter(text: str, name: str) -> list[tuple[int, int | None]]:
match = re.search(
rf"\bparameter\s+\[[^\]]+\]\s+{re.escape(name)}\s*\[[^\]]+\]\s*=\s*\{{(.*?)\}}\s*;",
text,
re.DOTALL,
)
if match is None:
raise RuntimeError(f"could not find array parameter {name}")
return [parse_sv_integer(token) for token in match.group(1).split(",") if token.strip()]
def sv_case_function(
name: str,
input_width: str,
output_width: str,
values: list[int],
literal_width: int,
) -> str:
lines = [
f"function automatic logic [{output_width}-1 : 0] {name};",
f" input logic [{input_width}-1 : 0] index;",
" begin",
" case (index)",
]
for index, value in enumerate(values):
literal = f"-{literal_width}'sd{-value}" if value < 0 else f"{literal_width}'d{value}"
lines.append(f" {index}: {name} = {literal};")
lines.extend(
[
f" default: {name} = '0;",
" endcase",
" end",
"endfunction",
]
)
return "\n".join(lines)
def round_sv_real(value: float) -> int:
"""Match real-to-integral assignment rounding used by SystemVerilog."""
return math.floor(value + 0.5) if value >= 0 else math.ceil(value - 0.5)
def build_nco_rom_functions(definitions: str) -> str:
phase_width = parse_integer_parameter(definitions, "ACQ_NCO_PH_ACC_WL")
sine_integer_bits = parse_integer_parameter(definitions, "ACQ_SIN_IL")
sine_fraction_bits = parse_integer_parameter(definitions, "ACQ_SIN_FL")
lut_address_width = parse_integer_parameter(definitions, "ACQ_SIN_LUT_DEPTH_WL")
freq_values = [value for value, _ in parse_array_parameter(definitions, "FREQ_WORD_ARRAY")]
pullin_values = [value for value, _ in parse_array_parameter(definitions, "PULLIN_FREQ_WORD_ARRAY")]
sine_width = sine_integer_bits + sine_fraction_bits
sine_values = [
round_sv_real(
math.sin(2.0 * math.pi * index / (1 << lut_address_width))
* (1 << sine_fraction_bits)
)
for index in range(1 << lut_address_width)
]
return "\n\n".join(
[
"// Efinity 2025.2 mis-elaborates the original initial/$sin ROMs.\n"
"// These constant functions are generated from definitions.sv for Ti375 only.",
sv_case_function(
"efinity_freq_word", "FREQ_BIN_WL", "PH_ACC_WL", freq_values, phase_width
),
sv_case_function(
"efinity_pullin_word", "NUM_PULLIN_WL", "PH_ACC_WL", pullin_values, phase_width
),
sv_case_function(
"efinity_sine", "LUT_DEPTH_WL", "ACQ_SIN_IL + ACQ_SIN_FL", sine_values, sine_width
),
]
)
def rewrite_acquisition_nco(source: Path, destination: Path, definitions: str) -> None:
text = source.read_text(encoding="utf-8")
start = text.find(NCO_ROM_START)
end = text.find(NCO_ROM_END, start)
if start < 0 or end < 0:
raise RuntimeError(f"could not locate acquisition NCO ROM block in {source}")
text = text[:start] + build_nco_rom_functions(definitions) + "\n\n" + text[end:]
old_sum = (
"freq_word_arr[freq_bin_idx_i] + "
"pullin_freq_word_arr[pullin_freq_bin_idx_i]"
)
new_sum = (
"efinity_freq_word(freq_bin_idx_i) + "
"efinity_pullin_word(pullin_freq_bin_idx_i)"
)
if text.count(old_sum) != 2:
raise RuntimeError(f"expected two NCO phase increments in {source}")
text = text.replace(old_sum, new_sum)
if text.count("sinwav_lut[truncated_sin_phase]") != 1:
raise RuntimeError(f"expected one sine ROM read in {source}")
if text.count("sinwav_lut[truncated_cos_phase]") != 1:
raise RuntimeError(f"expected one cosine ROM read in {source}")
text = text.replace("sinwav_lut[truncated_sin_phase]", "efinity_sine(truncated_sin_phase)")
text = text.replace("sinwav_lut[truncated_cos_phase]", "efinity_sine(truncated_cos_phase)")
destination.write_text(text, encoding="utf-8")
def rewrite_tracking_nco(source: Path, destination: Path, definitions: str) -> None:
text = source.read_text(encoding="utf-8")
declaration = (
'(*rom_style = "distributed" *) logic [SIN_IL-1 : -SIN_FL] '
"sinwav_lut [0 : 2**LUT_DEPTH_WL-1];"
)
values = [value for value, _ in parse_array_parameter(definitions, "TRCK_SINWAV_LUT")]
expected_entries = parse_integer_parameter(definitions, "NUM_TRCK_SINWAV_LUT")
if len(values) != expected_entries:
raise RuntimeError(
f"expected {expected_entries} tracking sine entries, got {len(values)}"
)
function = (
"// Efinity 2025.2 leaves the original package-array initialized ROM undriven.\n"
"// This Ti375-only constant function preserves the exact definitions.sv values.\n"
+ sv_case_function(
"efinity_tracking_sine",
"LUT_DEPTH_WL",
"SIN_IL + SIN_FL",
values,
parse_integer_parameter(definitions, "TRCK_CARR_NCO_SIN_WL"),
)
)
if text.count(declaration) != 1:
raise RuntimeError(f"expected one tracking sine ROM declaration in {source}")
text = text.replace(declaration, function)
initializer = """initial begin
sinwav_lut = TRCK_SINWAV_LUT;
end
"""
if text.count(initializer) != 1:
raise RuntimeError(f"expected one tracking sine ROM initializer in {source}")
text = text.replace(initializer, "")
if text.count("sinwav_lut[accum_truncated_sin]") != 1:
raise RuntimeError(f"expected one tracking sine ROM read in {source}")
if text.count("sinwav_lut[accum_truncated_cos]") != 1:
raise RuntimeError(f"expected one tracking cosine ROM read in {source}")
text = text.replace(
"sinwav_lut[accum_truncated_sin]", "efinity_tracking_sine(accum_truncated_sin)"
)
text = text.replace(
"sinwav_lut[accum_truncated_cos]", "efinity_tracking_sine(accum_truncated_cos)"
)
destination.write_text(text, encoding="utf-8")
def write_mem_file(path: Path, entries: list[tuple[int, int | None]], width: int) -> None:
mask = (1 << width) - 1
path.write_text(
# Binary keeps each row exactly TWIDDLE_WIDTH bits. A hexadecimal row
# would round 15 bits up to four digits and Efinity warns about a
# spurious 16-to-15-bit truncation even though the value is correct.
"".join(f"{value & mask:0{width}b}\n" for value, _ in entries),
encoding="ascii",
)
def rewrite_twiddle_rom(source: Path, destination: Path) -> None:
text = source.read_text(encoding="utf-8")
old = """initial begin
cos_rom = COS_ROM_ARRAY;
sin_rom = SIN_ROM_ARRAY;
end"""
new = """initial begin
// Generated Ti375 files are resolved through Efinity's memory-init include path.
$readmemb("ti375_twiddle_cos_15.mem", cos_rom);
$readmemb("ti375_twiddle_sin_15.mem", sin_rom);
end"""
if text.count(old) != 1:
raise RuntimeError(f"expected one twiddle ROM initializer in {source}")
destination.write_text(text.replace(old, new), encoding="utf-8")
def prepare_gps_rtl(source_dir: Path, output_dir: Path) -> None:
definitions_path = source_dir / "common" / "definitions.sv"
definitions = definitions_path.read_text(encoding="utf-8")
output_dir.mkdir(parents=True, exist_ok=True)
rewrite_acquisition_nco(
source_dir / "acq_src" / "acq_nco_carr.sv",
output_dir / "acq_nco_carr.sv",
definitions,
)
rewrite_tracking_nco(
source_dir / "trck_src" / "NCO_carr.sv",
output_dir / "NCO_carr.sv",
definitions,
)
rewrite_twiddle_rom(
source_dir / "acq_src" / "twiddle_ram.sv",
output_dir / "twiddle_ram.sv",
)
cos_entries = parse_array_parameter(definitions, "COS_ROM_ARRAY")
sin_entries = parse_array_parameter(definitions, "SIN_ROM_ARRAY")
expected_entries = parse_integer_parameter(definitions, "NUM_TWIDDLE_ROM_IDX")
if len(cos_entries) != expected_entries or len(sin_entries) != expected_entries:
raise RuntimeError(
f"expected {expected_entries} twiddle entries, got "
f"cos={len(cos_entries)} sin={len(sin_entries)}"
)
write_mem_file(output_dir / "ti375_twiddle_cos_15.mem", cos_entries, 15)
write_mem_file(output_dir / "ti375_twiddle_sin_15.mem", sin_entries, 15)
def rewrite_rocket_store_data(path: Path) -> None:
text = path.read_text(encoding="utf-8")
if EFINITY_STORE_DATA in text:
return
if text.count(ROCKET_STORE_DATA) != 1:
raise RuntimeError(f"expected one Rocket store-data array in {path}")
path.write_text(text.replace(ROCKET_STORE_DATA, EFINITY_STORE_DATA), encoding="utf-8")
def rewrite_tilelink_atomic_lut(path: Path) -> None:
text = path.read_text(encoding="utf-8")
if "// Avoid Efinity VDB-1002 on the block-local packed LUT." in text:
return
match = ATOMIC_LUT_DECLARATION.search(text)
if match is None:
raise RuntimeError(f"expected one TileLink atomic packed LUT in {path}")
if ATOMIC_LUT_DECLARATION.search(text, match.end()) is not None:
raise RuntimeError(f"expected only one TileLink atomic packed LUT in {path}")
indent = match.group("indent")
temporary = match.group("name")
old_read = f"{indent}cam_a_0_lut <= {temporary}[auto_in_a_bits_param[1:0]];"
new_read = f"""{indent}// Avoid Efinity VDB-1002 on the block-local packed LUT.
{indent}case (auto_in_a_bits_param[1:0])
{indent} 2'h0: cam_a_0_lut <= 4'h6;
{indent} 2'h1: cam_a_0_lut <= 4'hE;
{indent} 2'h2: cam_a_0_lut <= 4'h8;
{indent} default: cam_a_0_lut <= 4'hC;
{indent}endcase"""
if text.count(old_read) != 1:
raise RuntimeError(f"expected one TileLink atomic packed LUT read in {path}")
text = text[: match.start()] + text[match.end() + 1 :]
text = text.replace(old_read, new_read)
path.write_text(text, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--verilog-dir", type=Path, required=True)
parser.add_argument("--gps-source-dir", type=Path, required=True)
parser.add_argument("--gps-output-dir", type=Path, required=True)
args = parser.parse_args()
rewrite_rocket_store_data(args.verilog_dir / "Rocket.sv")
rewrite_tilelink_atomic_lut(args.verilog_dir / "TLAtomicAutomata_pbus.sv")
rewrite_tilelink_atomic_lut(args.verilog_dir / "TLAtomicAutomata_cbus.sv")
prepare_gps_rtl(args.gps_source_dir, args.gps_output_dir)
if __name__ == "__main__":
main()