util/scripts: Add spd-decode for LPDDR5 SPD hex
Decode LPDDR5/LPDDR5X .spd.hex dumps into spd_tools-compatible JSON. The default output is a single-part memory_parts.json-style document without the bits that spd_gen adds automatically. Signed-off-by: Sean Rhodes <sean@starlabs.systems> Change-Id: I49874bcd42cf3981277abbfa997ec185088f0715 Reviewed-on: https://review.coreboot.org/c/coreboot/+/89785 Reviewed-by: Matt DeVillier <matt.devillier@gmail.com> Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
This commit is contained in:
parent
2ac2df0eda
commit
fd6c0aa55b
1 changed files with 311 additions and 0 deletions
311
util/scripts/spd-decode-ddr5
Executable file
311
util/scripts/spd-decode-ddr5
Executable file
|
|
@ -0,0 +1,311 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""
|
||||
Decode an LPDDR5/LPDDR5X SPD text hex file into JSON.
|
||||
|
||||
Default output format is compatible with coreboot's spd_tools `spd_gen`:
|
||||
it emits a `memory_parts.json` containing a single part entry.
|
||||
|
||||
This tool is intentionally named `spd-decode` even though it currently only
|
||||
supports LPDDR5/LPDDR5X SPDs, so it can be extended to other SPD types later.
|
||||
|
||||
Usage:
|
||||
spd-decode <path/to/spd.hex> [--pretty] [--name NAME]
|
||||
[--include-nondefault-timings]
|
||||
spd-decode <path/to/spd.hex> --format raw [--pretty]
|
||||
|
||||
Options:
|
||||
--format spd_gen Output a `memory_parts.json`-style document for
|
||||
coreboot's spd_tools `spd_gen`.
|
||||
--format raw Output the fully decoded SPD fields as JSON.
|
||||
--name NAME Override the part name in spd_gen output.
|
||||
--include-nondefault-timings
|
||||
Include timing fields in spd_gen output only when they
|
||||
differ from spd_gen defaults.
|
||||
--pretty Pretty-print JSON (tabs) and include a command header.
|
||||
|
||||
Only LPDDR5 (0x13) and LPDDR5X (0x15) SPDs are supported.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
def _json_dumps_tabs(obj) -> str:
|
||||
# Dump JSON with one-space indent and convert leading spaces to tabs.
|
||||
s = json.dumps(obj, indent=1, separators=(",", ": "), ensure_ascii=False)
|
||||
lines = []
|
||||
for line in s.splitlines():
|
||||
n = 0
|
||||
while n < len(line) and line[n] == " ":
|
||||
n += 1
|
||||
if n:
|
||||
line = ("\t" * n) + line[n:]
|
||||
lines.append(line)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _to_signed_byte(b: int) -> int:
|
||||
return b - 256 if b >= 128 else b
|
||||
|
||||
|
||||
def _parse_hex_file(path: str) -> List[int]:
|
||||
data: List[int] = []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
for p in line.split():
|
||||
if len(p) != 2:
|
||||
raise ValueError(f"Invalid hex byte '{p}' in {path}")
|
||||
data.append(int(p, 16))
|
||||
return data
|
||||
|
||||
|
||||
def _decode_time_ps(mtb_byte: int, ftb_byte: int) -> int:
|
||||
# MTB unit = 125 ps. FTB is signed 8-bit offset in ps.
|
||||
return (mtb_byte * 125) + _to_signed_byte(ftb_byte)
|
||||
|
||||
|
||||
def _decode_time_ns_from_16bit_mtb(lsb: int, msb: int) -> float:
|
||||
# 16-bit MTB value in units of 125ps -> convert to ns (float)
|
||||
mtb = (msb << 8) | lsb
|
||||
ps = mtb * 125
|
||||
return ps / 1000.0
|
||||
|
||||
|
||||
def _round_int(x: float) -> int:
|
||||
return int(round(x))
|
||||
|
||||
|
||||
def _closest_speed_from_tck_ps(tck_ps: int, set_rev: int) -> int:
|
||||
# Allowed LP5 speed bins in Mbps
|
||||
bins = [5500, 6400, 7500, 8533]
|
||||
if set_rev == 0x10:
|
||||
# ADL set encodes CK tCKmin = 1 / (WCK rate / 4) = 8e6 / speed
|
||||
guess = 8_000_000.0 / max(tck_ps, 1)
|
||||
else:
|
||||
# Default SPD encodes WCK tCKmin = 2e6 / speed
|
||||
guess = 2_000_000.0 / max(tck_ps, 1)
|
||||
return int(min(bins, key=lambda b: abs(b - guess)))
|
||||
|
||||
|
||||
def _default_part_name_from_path(path: str) -> str:
|
||||
base = os.path.basename(path)
|
||||
for suffix in (".spd.hex", ".hex"):
|
||||
if base.endswith(suffix):
|
||||
base = base[: -len(suffix)]
|
||||
break
|
||||
return base
|
||||
|
||||
|
||||
def _lp5_defaults_for_optional_attribs(density_gb: Optional[int]) -> Dict[str, int]:
|
||||
# Match util/spd_tools/src/spd_gen/lp5.go defaults.
|
||||
trfc = {
|
||||
4: (180, 90),
|
||||
6: (210, 120),
|
||||
8: (210, 120),
|
||||
12: (280, 140),
|
||||
16: (280, 140),
|
||||
24: (380, 190),
|
||||
32: (380, 190),
|
||||
}
|
||||
trfcab, trfcpb = trfc.get(density_gb, (0, 0))
|
||||
return {
|
||||
"trfcabNs": trfcab,
|
||||
"trfcpbNs": trfcpb,
|
||||
"trcdMinNs": 18,
|
||||
"trpabMinNs": 21,
|
||||
"trppbMinNs": 18,
|
||||
}
|
||||
|
||||
|
||||
def decode_lp5(spd: List[int]) -> Dict:
|
||||
if len(spd) < 349:
|
||||
raise ValueError("LPDDR5 SPD must contain at least 349 bytes")
|
||||
|
||||
# Byte indices (match util/spd_tools/src/spd_gen/lp5.go)
|
||||
IDX = {
|
||||
"REV": 1,
|
||||
"MEM_TYPE": 2,
|
||||
"DENSITY_BANKS": 4,
|
||||
"ADDR": 5,
|
||||
"PKG_TYPE": 6,
|
||||
"OPT_FEATURES": 7,
|
||||
"OTHER_OPT_FEATURES": 9,
|
||||
"MODULE_ORG": 12,
|
||||
"BUS_WIDTH": 13,
|
||||
"TIMEBASES": 17,
|
||||
"TCK_MIN": 18,
|
||||
"TAA_MIN": 24,
|
||||
"TRCD_MIN": 26,
|
||||
"TRPAB_MIN": 27,
|
||||
"TRPPB_MIN": 28,
|
||||
"TRFCAB_LSB": 29,
|
||||
"TRFCAB_MSB": 30,
|
||||
"TRFCPB_LSB": 31,
|
||||
"TRFCPB_MSB": 32,
|
||||
"TRPPB_FINE": 120,
|
||||
"TRPAB_FINE": 121,
|
||||
"TRCD_FINE": 122,
|
||||
"TAA_FINE": 123,
|
||||
"TCK_FINE": 125,
|
||||
"MPN_START": 329,
|
||||
"MPN_END": 348,
|
||||
}
|
||||
|
||||
mem_type = spd[IDX["MEM_TYPE"]]
|
||||
rev = spd[IDX["REV"]]
|
||||
if mem_type == 0x13:
|
||||
mem_type_str = "LPDDR5"
|
||||
lp5x = False
|
||||
elif mem_type == 0x15:
|
||||
mem_type_str = "LPDDR5X"
|
||||
lp5x = True
|
||||
else:
|
||||
mem_type_str = f"Unknown(0x{mem_type:02x})"
|
||||
lp5x = False
|
||||
|
||||
density_code = spd[IDX["DENSITY_BANKS"]] & 0x0F
|
||||
density_map = {
|
||||
0x4: 4,
|
||||
0xB: 6,
|
||||
0x5: 8,
|
||||
0x8: 12,
|
||||
0x6: 16,
|
||||
0x9: 24,
|
||||
0x7: 32,
|
||||
}
|
||||
density_per_die_gb = density_map.get(density_code)
|
||||
|
||||
dies_per_package = ((spd[IDX["PKG_TYPE"]] >> 4) & 0x7) + 1
|
||||
ranks_per_channel = ((spd[IDX["MODULE_ORG"]] >> 3) & 0x7) + 1
|
||||
bit_width_per_channel = (spd[IDX["MODULE_ORG"]] & 0x7) * 8
|
||||
channels_per_pkg = 1 << ((spd[IDX["PKG_TYPE"]] >> 2) & 0x3)
|
||||
|
||||
tck_ps = _decode_time_ps(spd[IDX["TCK_MIN"]], spd[IDX["TCK_FINE"]])
|
||||
speed_mbps = _closest_speed_from_tck_ps(tck_ps, rev)
|
||||
if not lp5x and speed_mbps >= 7500:
|
||||
lp5x = True
|
||||
|
||||
taa_ps = _decode_time_ps(spd[IDX["TAA_MIN"]], spd[IDX["TAA_FINE"]])
|
||||
trcd_ns = _round_int(_decode_time_ps(spd[IDX["TRCD_MIN"]], spd[IDX["TRCD_FINE"]]) / 1000.0)
|
||||
trpab_ns = _round_int(_decode_time_ps(spd[IDX["TRPAB_MIN"]], spd[IDX["TRPAB_FINE"]]) / 1000.0)
|
||||
trppb_ns = _round_int(_decode_time_ps(spd[IDX["TRPPB_MIN"]], spd[IDX["TRPPB_FINE"]]) / 1000.0)
|
||||
trfcab_ns = _round_int(_decode_time_ns_from_16bit_mtb(spd[IDX["TRFCAB_LSB"]], spd[IDX["TRFCAB_MSB"]]))
|
||||
trfcpb_ns = _round_int(_decode_time_ns_from_16bit_mtb(spd[IDX["TRFCPB_LSB"]], spd[IDX["TRFCPB_MSB"]]))
|
||||
|
||||
mpn_bytes = spd[IDX["MPN_START"] : IDX["MPN_END"] + 1]
|
||||
try:
|
||||
mpn = bytes(mpn_bytes).decode("ascii").strip()
|
||||
except Exception:
|
||||
mpn = ""
|
||||
|
||||
return {
|
||||
"memoryType": mem_type_str,
|
||||
"revision": f"0x{rev:02x}",
|
||||
"channelsPerPackage": channels_per_pkg,
|
||||
"manufacturerPartNumber": mpn or None,
|
||||
"attributes": {
|
||||
"densityPerDieGb": density_per_die_gb,
|
||||
"diesPerPackage": dies_per_package,
|
||||
"bitWidthPerChannel": bit_width_per_channel,
|
||||
"ranksPerChannel": ranks_per_channel,
|
||||
"speedMbps": speed_mbps,
|
||||
"lp5x": lp5x,
|
||||
"tckMinPs": tck_ps,
|
||||
"taaMinPs": taa_ps,
|
||||
"trcdMinNs": trcd_ns,
|
||||
"trpabMinNs": trpab_ns,
|
||||
"trppbMinNs": trppb_ns,
|
||||
"trfcabNs": trfcab_ns,
|
||||
"trfcpbNs": trfcpb_ns,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def detect_and_decode(spd: List[int]) -> Dict:
|
||||
if len(spd) < 3:
|
||||
raise ValueError("SPD data too short")
|
||||
mem_type = spd[2]
|
||||
if mem_type in (0x13, 0x15):
|
||||
return decode_lp5(spd)
|
||||
raise NotImplementedError(f"Unsupported or unknown SPD memory type 0x{mem_type:02x}")
|
||||
|
||||
|
||||
def _to_spd_gen_memory_parts(decoded: Dict, part_name: str, include_nondefault_timings: bool) -> Dict:
|
||||
attribs_in = decoded.get("attributes", {})
|
||||
|
||||
attribs_out = {
|
||||
"densityPerDieGb": attribs_in.get("densityPerDieGb"),
|
||||
"diesPerPackage": attribs_in.get("diesPerPackage"),
|
||||
"bitWidthPerChannel": attribs_in.get("bitWidthPerChannel"),
|
||||
"ranksPerChannel": attribs_in.get("ranksPerChannel"),
|
||||
"speedMbps": attribs_in.get("speedMbps"),
|
||||
}
|
||||
|
||||
if attribs_in.get("lp5x"):
|
||||
attribs_out["lp5x"] = True
|
||||
|
||||
if include_nondefault_timings:
|
||||
defaults = _lp5_defaults_for_optional_attribs(attribs_in.get("densityPerDieGb"))
|
||||
for k in ("trfcabNs", "trfcpbNs", "trcdMinNs", "trpabMinNs", "trppbMinNs"):
|
||||
v = attribs_in.get(k)
|
||||
if v is None:
|
||||
continue
|
||||
if defaults.get(k) and v != defaults[k]:
|
||||
attribs_out[k] = v
|
||||
|
||||
part = {"name": part_name, "attribs": attribs_out}
|
||||
return {"parts": [part]}
|
||||
|
||||
|
||||
def main(argv: List[str]) -> int:
|
||||
ap = argparse.ArgumentParser(description="Decode LPDDR5 SPD .hex file to JSON")
|
||||
ap.add_argument("hexfile", help="Path to SPD hex file (text with space-separated bytes)")
|
||||
ap.add_argument(
|
||||
"--format",
|
||||
choices=("spd_gen", "raw"),
|
||||
default="spd_gen",
|
||||
help="Output JSON format: spd_gen (memory_parts.json) or raw",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--name",
|
||||
help="Part name for spd_gen output (defaults to MPN or input filename)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--include-nondefault-timings",
|
||||
action="store_true",
|
||||
help="Include timing fields only when they differ from spd_gen defaults (spd_gen output only)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--pretty",
|
||||
action="store_true",
|
||||
help="Pretty-print JSON output with a command header",
|
||||
)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
spd = _parse_hex_file(args.hexfile)
|
||||
decoded = detect_and_decode(spd)
|
||||
|
||||
if args.format == "raw":
|
||||
out_obj = decoded
|
||||
else:
|
||||
part_name = args.name or decoded.get("manufacturerPartNumber") or _default_part_name_from_path(args.hexfile)
|
||||
out_obj = _to_spd_gen_memory_parts(decoded, part_name, args.include_nondefault_timings)
|
||||
|
||||
if args.pretty:
|
||||
header = f"// Generated by:\n// {' '.join([sys.argv[0]] + argv)}\n\n"
|
||||
print(header + _json_dumps_tabs(out_obj), end="")
|
||||
else:
|
||||
print(json.dumps(out_obj, separators=(",", ":"), ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Loading…
Add table
Add a link
Reference in a new issue