From 22da85de66b0d544231420c4a50454ce487c5b7d Mon Sep 17 00:00:00 2001 From: "Dr. Christian Giessen" Date: Mon, 27 Apr 2026 13:52:41 +0000 Subject: [PATCH] Add DM cross-reference tool tools/dm_xref.py extracts all DM address references from PM code and shows the actual DM contents at those addresses: - I-register loads with matching L-register buffer lengths - Direct DM(addr) reads and writes - CNTR loop counters - Hex, decimal, and Q15 format for DM content - Automatic detection of uninitialized (zero) regions - Configurable context window (--context N words) --- tools/dm_xref.py | 270 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100755 tools/dm_xref.py diff --git a/tools/dm_xref.py b/tools/dm_xref.py new file mode 100755 index 0000000..7508e85 --- /dev/null +++ b/tools/dm_xref.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""dm_xref.py -- Extract all DM address references from PM code +and show what's stored at those addresses in a DM dump. + +Finds: + - I-register loads (I0=0x100 → DM base address) + - Direct DM access (DM(0x1234) = reg / reg = DM(0x1234)) + - DM immediate modify (reg = DM(I0 += offset)) + - L-register loads (circular buffer lengths) + - CNTR loads (loop counts) + +Usage: + python3 dm_xref.py firmware_pm.bin firmware_dm.bin + python3 dm_xref.py firmware_pm.bin firmware_dm.bin --context 16 +""" + +import argparse +import os +import struct +import sys + + +REG0 = ["AX0", "AX1", "MX0", "MX1", "AY0", "AY1", "MY0", "MY1", + "MR2", "SR2", "AR", "SI", "MR1", "SR1", "MR0", "SR0"] +REG1 = ["I0", "I1", "I2", "I3", "M0", "M1", "M2", "M3", + "L0", "L1", "L2", "L3", "IMASK", "IRPTL", "ICNTL", "STACKA"] +REG2 = ["I4", "I5", "I6", "I7", "M4", "M5", "M6", "M7", + "L4", "L5", "L6", "L7", "RES", "RES", "CNTR", "LPSTACKA"] + + +def read_pm(path): + data = open(path, "rb").read() + words = [] + for i in range(0, len(data) - 2, 3): + w = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2] + words.append(w) + return words + + +def read_dm(path, endian="big"): + fmt = ">H" if endian == "big" else "= 0x8000 else v + + +def extract_dm_refs(pm_words): + """Extract all DM address references from PM code.""" + refs = [] + + for i, w in enumerate(pm_words): + pm_addr = i * 3 + b23_22 = (w >> 22) & 3 + b21_20 = (w >> 20) & 3 + + # Type 6: Dreg = Imm16 (data constants, not DM refs) + # Skip — these load values, not addresses + + # Type 7 REG1: I/M/L register loads + if b23_22 == 1 and b21_20 == 1: + val = (w >> 4) & 0xFFFF + reg = REG1[w & 0xF] + if reg.startswith("I"): + refs.append({ + "pm_addr": pm_addr, + "type": "I-reg load", + "reg": reg, + "dm_addr": val, + "desc": f"{reg} = 0x{val:04X}" + }) + elif reg.startswith("L"): + refs.append({ + "pm_addr": pm_addr, + "type": "L-reg (buf len)", + "reg": reg, + "dm_addr": None, + "value": val, + "desc": f"{reg} = {val} (buffer length)" + }) + + # Type 7 REG2: I4-I7/M4-M7/L4-L7/CNTR loads + if b23_22 == 0 and b21_20 == 3: + val = (w >> 4) & 0xFFFF + reg = REG2[w & 0xF] + if reg.startswith("I"): + refs.append({ + "pm_addr": pm_addr, + "type": "I-reg load", + "reg": reg, + "dm_addr": val, + "desc": f"{reg} = 0x{val:04X}" + }) + elif reg.startswith("L"): + refs.append({ + "pm_addr": pm_addr, + "type": "L-reg (buf len)", + "reg": reg, + "dm_addr": None, + "value": val, + "desc": f"{reg} = {val} (buffer length)" + }) + elif reg == "CNTR": + refs.append({ + "pm_addr": pm_addr, + "type": "CNTR", + "reg": reg, + "dm_addr": None, + "value": val, + "desc": f"CNTR = {val} (loop count)" + }) + + # Type 3: Direct DM access — DM(addr16) = reg / reg = DM(addr16) + if b23_22 == 2: + d = (w >> 20) & 1 + addr = (w >> 4) & 0xFFFF + reg_idx = w & 0xF + is_ireg = (w >> 21) & 1 + rname = REG1[reg_idx] if is_ireg else REG0[reg_idx] + direction = "write" if d else "read" + refs.append({ + "pm_addr": pm_addr, + "type": f"DM direct {direction}", + "reg": rname, + "dm_addr": addr, + "desc": (f"DM(0x{addr:04X}) = {rname}" if d + else f"{rname} = DM(0x{addr:04X})") + }) + + return refs + + +def show_dm_content(dm_words, dm_addr, context=8): + """Format DM content at a given address.""" + if dm_addr is None or dm_addr >= len(dm_words): + return " (outside DM range)" + + end = min(dm_addr + context, len(dm_words)) + hex_vals = [] + dec_vals = [] + for j in range(dm_addr, end): + v = dm_words[j] + hex_vals.append(f"0x{v:04X}") + dec_vals.append(f"{signed16(v):6d}") + + lines = [] + lines.append(f" Hex: [{', '.join(hex_vals)}]") + lines.append(f" Dec: [{', '.join(dec_vals)}]") + + # Check if it looks like Q15 coefficients + vals = [signed16(dm_words[j]) for j in range(dm_addr, end)] + if all(abs(v) <= 32767 for v in vals): + q15 = [f"{v/32768:.4f}" for v in vals] + lines.append(f" Q15: [{', '.join(q15)}]") + + # Check for all zeros + if all(v == 0 for v in vals): + lines.append(f" (all zeros — uninitialized)") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Extract DM references from PM code and" + " show DM contents.") + parser.add_argument("pm_file", help="PM binary (packed 3-byte)") + parser.add_argument("dm_file", help="DM binary (16-bit words)") + parser.add_argument("--endian", choices=["big", "little"], + default="big") + parser.add_argument("--context", type=int, default=8, + help="Words of DM context to show" + " (default: 8)") + args = parser.parse_args() + + for f in (args.pm_file, args.dm_file): + if not os.path.isfile(f): + print(f"Error: {f} not found", file=sys.stderr) + sys.exit(1) + + pm_words = read_pm(args.pm_file) + dm_words = read_dm(args.dm_file, args.endian) + + refs = extract_dm_refs(pm_words) + + print(f"PM: {args.pm_file} ({len(pm_words)} instructions)") + print(f"DM: {args.dm_file} ({len(dm_words)} words)") + print() + + # Group by type + ireg_loads = [r for r in refs if r["type"] == "I-reg load"] + direct_access = [r for r in refs + if r["type"].startswith("DM direct")] + buf_lengths = [r for r in refs + if r["type"] == "L-reg (buf len)"] + counters = [r for r in refs if r["type"] == "CNTR"] + + # I-register loads → buffer base addresses + if ireg_loads: + print(f"=== Buffer Base Addresses" + f" ({len(ireg_loads)} I-reg loads) ===") + seen = {} + for r in ireg_loads: + key = (r["reg"], r["dm_addr"]) + if key not in seen: + seen[key] = r + for (reg, dm_addr), r in sorted(seen.items()): + print(f"\n{reg} = 0x{dm_addr:04X}" + f" (PM @ 0x{r['pm_addr']:06X})") + # Find matching L-register for buffer length + lreg = reg.replace("I", "L") + for lr in buf_lengths: + if lr["reg"] == lreg: + print(f" {lreg} = {lr['value']}" + f" (circular buffer, {lr['value']} words)") + break + print(show_dm_content(dm_words, dm_addr, args.context)) + print() + + # Direct DM accesses + if direct_access: + print(f"=== Direct DM Accesses" + f" ({len(direct_access)} found) ===") + seen_addrs = {} + for r in direct_access: + a = r["dm_addr"] + if a not in seen_addrs: + seen_addrs[a] = [] + seen_addrs[a].append(r) + + for dm_addr in sorted(seen_addrs.keys()): + ops = seen_addrs[dm_addr] + descs = [r["desc"] for r in ops[:3]] + more = f" +{len(ops)-3} more" if len(ops) > 3 else "" + print(f"\nDM 0x{dm_addr:04X}:") + for d in descs: + print(f" {d}") + if more: + print(f" {more}") + print(show_dm_content(dm_words, dm_addr, args.context)) + print() + + # Loop counters + if counters: + print(f"=== Loop Counters ({len(counters)} found) ===") + for r in counters: + print(f" PM 0x{r['pm_addr']:06X}: {r['desc']}") + print() + + # Summary + all_dm_addrs = set() + for r in refs: + if r.get("dm_addr") is not None: + all_dm_addrs.add(r["dm_addr"]) + + print(f"=== Summary ===") + print(f" Unique DM addresses referenced: {len(all_dm_addrs)}") + print(f" Buffer bases (I-reg): {len(ireg_loads)}") + print(f" Direct DM access: {len(direct_access)}") + print(f" Buffer lengths (L-reg): {len(buf_lengths)}") + print(f" Loop counters (CNTR): {len(counters)}") + + +if __name__ == "__main__": + main()