From ddccae54a0d1faa0adc0834083355303a5b869ab Mon Sep 17 00:00:00 2001 From: "Dr. Christian Giessen" Date: Mon, 27 Apr 2026 12:13:42 +0000 Subject: [PATCH] Add DM analysis tool tools/analyze_dm.py scans 16-bit DM dumps for: - Null regions (BSS, uninitialized memory) - Q15 coefficient tables with range and average - Periodic waveforms (sine/cosine lookup tables) - ASCII strings - Unclassified data regions with hex preview Supports big-endian (default) and little-endian byte order. Outputs DM word addresses for cross-reference with PM code. --- tools/analyze_dm.py | 251 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100755 tools/analyze_dm.py diff --git a/tools/analyze_dm.py b/tools/analyze_dm.py new file mode 100755 index 0000000..08b3c06 --- /dev/null +++ b/tools/analyze_dm.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""analyze_dm.py -- Analyze ADSP-219x DM (Data Memory) dumps. + +Scans a 16-bit DM binary for: + - Null regions (uninitialized / BSS) + - Coefficient tables (Q15 fixed-point patterns) + - Lookup tables (periodic waveforms) + - ASCII strings + - Non-zero data regions + +Usage: + python3 analyze_dm.py firmware_dm.bin + python3 analyze_dm.py firmware_dm.bin --endian little +""" + +import argparse +import math +import os +import struct +import sys + + +def read_words(path, endian="big"): + """Read 16-bit words from a binary file.""" + fmt = ">H" if endian == "big" else "= 0x8000 else v + + +def find_null_regions(words, min_run=8): + """Find contiguous runs of zero words.""" + regions = [] + start = None + for i, w in enumerate(words): + if w == 0: + if start is None: + start = i + else: + if start is not None and (i - start) >= min_run: + regions.append((start, i - start)) + start = None + if start is not None and (len(words) - start) >= min_run: + regions.append((start, len(words) - start)) + return regions + + +def find_coeff_tables(words, min_len=8, max_magnitude=0x7FFF): + """Find potential Q15 coefficient tables. + + Looks for runs of non-zero values where all values are + within the Q15 range and show some variance (not constant). + """ + tables = [] + start = None + run = [] + + for i, w in enumerate(words): + sw = signed16(w) + if w != 0 and abs(sw) <= max_magnitude: + if start is None: + start = i + run = [] + run.append(sw) + else: + if start is not None and len(run) >= min_len: + mn = min(run) + mx = max(run) + if mx != mn: # not constant + tables.append((start, len(run), mn, mx, + sum(run) / len(run))) + start = None + run = [] + + if start is not None and len(run) >= min_len: + mn = min(run) + mx = max(run) + if mx != mn: + tables.append((start, len(run), mn, mx, + sum(run) / len(run))) + return tables + + +def detect_periodicity(words, start, length): + """Check if a region has periodic content (sine/cosine table).""" + if length < 16: + return None + vals = [signed16(words[start + i]) for i in range(length)] + + # Try periods from 16 to length/2 + best_period = None + best_err = float("inf") + for period in range(16, min(length // 2 + 1, 1025)): + if length < period * 2: + continue + err = 0.0 + count = 0 + for i in range(period, min(length, period * 4)): + diff = vals[i] - vals[i % period] + err += diff * diff + count += 1 + if count > 0: + rms = math.sqrt(err / count) + if rms < best_err: + best_err = rms + best_period = period + + if best_period and best_err < 500: + return best_period + return None + + +def find_strings(data, min_len=4): + """Find ASCII strings in raw bytes.""" + strings = [] + current = [] + start = None + for i, byte in enumerate(data): + if 0x20 <= byte < 0x7F: + if start is None: + start = i + current.append(chr(byte)) + else: + if current and len(current) >= min_len: + strings.append((start, "".join(current))) + current = [] + start = None + if current and len(current) >= min_len: + strings.append((start, "".join(current))) + return strings + + +def analyze(path, endian="big"): + """Run full analysis on a DM dump.""" + data = open(path, "rb").read() + size = len(data) + words = read_words(path, endian) + n_words = len(words) + + print(f"File: {path}") + print(f"Size: {size} bytes ({n_words} words, 16-bit {endian}-endian)") + print() + + # Null regions + nulls = find_null_regions(words) + if nulls: + total_null = sum(n for _, n in nulls) + print(f"=== Null Regions ({len(nulls)} found, " + f"{total_null} words = {total_null*100//n_words}%) ===") + for addr, length in nulls: + print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}" + f" ({length} words, {length*2} bytes)") + print() + + # Coefficient tables + coeffs = find_coeff_tables(words) + if coeffs: + print(f"=== Potential Coefficient Tables ({len(coeffs)} found) ===") + for addr, length, mn, mx, avg in coeffs: + q15_min = mn / 32768.0 + q15_max = mx / 32768.0 + period = detect_periodicity(words, addr, length) + pstr = f", period~{period}" if period else "" + print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}" + f" ({length} words)") + print(f" Range: {mn}..{mx} (Q15: {q15_min:.4f}" + f"..{q15_max:.4f}), avg={avg:.1f}{pstr}") + print() + + # Strings + strings = find_strings(data) + if strings: + print(f"=== ASCII Strings ({len(strings)} found) ===") + for offset, s in strings: + dm_addr = offset // 2 + print(f" Byte 0x{offset:04X} (DM ~0x{dm_addr:04X}):" + f" \"{s}\"") + print() + + # Non-null, non-coefficient data regions + covered = set() + for addr, length in nulls: + for i in range(addr, addr + length): + covered.add(i) + for addr, length, _, _, _ in coeffs: + for i in range(addr, addr + length): + covered.add(i) + + data_regions = [] + start = None + for i in range(n_words): + if i not in covered and words[i] != 0: + if start is None: + start = i + else: + if start is not None: + length = i - start + if length >= 4: + data_regions.append((start, length)) + start = None + if start is not None and (n_words - start) >= 4: + data_regions.append((start, n_words - start)) + + if data_regions: + print(f"=== Other Data Regions ({len(data_regions)} found) ===") + for addr, length in data_regions: + sample = [f"0x{words[addr+j]:04X}" + for j in range(min(length, 8))] + dots = " ..." if length > 8 else "" + print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}" + f" ({length} words)") + print(f" [{', '.join(sample)}{dots}]") + print() + + # Summary + total_nonzero = sum(1 for w in words if w != 0) + print(f"=== Summary ===") + print(f" Total words: {n_words}") + print(f" Non-zero words: {total_nonzero}" + f" ({total_nonzero*100//max(n_words,1)}%)") + print(f" Null regions: {len(nulls)}") + print(f" Coeff tables: {len(coeffs)}") + print(f" Strings: {len(strings)}") + print(f" Other data: {len(data_regions)}") + + +def main(): + parser = argparse.ArgumentParser( + description="Analyze ADSP-219x DM (Data Memory) dumps.") + parser.add_argument("file", help="DM binary file") + parser.add_argument("--endian", choices=["big", "little"], + default="big", + help="Byte order (default: big)") + args = parser.parse_args() + + if not os.path.isfile(args.file): + print(f"Error: {args.file} not found", file=sys.stderr) + sys.exit(1) + + analyze(args.file, args.endian) + + +if __name__ == "__main__": + main()