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)
This commit is contained in:
Dr. Christian Giessen
2026-04-27 13:52:41 +00:00
parent e790736868
commit 22da85de66

270
tools/dm_xref.py Executable file
View File

@@ -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 "<H"
data = open(path, "rb").read()
if len(data) % 2:
data += b"\x00"
return [struct.unpack_from(fmt, data, i)[0]
for i in range(0, len(data), 2)]
def signed16(v):
return v - 0x10000 if v >= 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()