#!/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()