From e7907368689fe86bddb85d4876a2aa65e91f5f0f Mon Sep 17 00:00:00 2001 From: "Dr. Christian Giessen" Date: Mon, 27 Apr 2026 13:49:40 +0000 Subject: [PATCH] Add firmware overview tool for quick structural analysis tools/firmware_overview.py analyzes PM binaries and reports: - Memory map (empty / DSP kernel / control flow / code+data) - Entry point detection - All DO UNTIL loops with MAC operation count - All immediate constants with register names and addresses - I/O operations with ADSP-2191 peripheral register names - Optional DM cross-reference (I-reg targets) - Summary statistics Includes ADSP-2191 I/O register name table for SPORT, SPI, UART, Timer, GPIO, DMA, System Controller, and Interrupts. --- tools/firmware_overview.py | 334 +++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100755 tools/firmware_overview.py diff --git a/tools/firmware_overview.py b/tools/firmware_overview.py new file mode 100755 index 0000000..a087c7e --- /dev/null +++ b/tools/firmware_overview.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +"""firmware_overview.py -- Quick structural overview of ADSP-219x firmware. + +Analyzes a PM binary and produces a human-readable report: + - Memory map (code vs data vs empty regions) + - Function-like blocks (code between RTS/JUMP boundaries) + - DSP kernels (DO UNTIL loops with MAC operations) + - I/O configuration (peripheral register writes) + - All immediate constants loaded into registers + - Cross-reference: which addresses are referenced + +Usage: + python3 firmware_overview.py firmware_pm.bin + python3 firmware_overview.py firmware_pm.bin --dm firmware_dm.bin +""" + +import argparse +import os +import struct +import sys + + +# ADSP-2191 I/O register names (partial, most common) +IO_NAMES = { + 0x00: "SYSCTRL", 0x01: "SYSTAT", 0x02: "IOPG", + 0x04: "BWCTRL", 0x05: "BWSTAT", + 0x10: "PICR0", 0x11: "PICR1", 0x12: "PICR2", 0x13: "PICR3", + 0x14: "IMASK0", 0x15: "IMASK1", + 0x20: "TCTL", 0x21: "TCOUNT", 0x22: "TPERIOD", 0x23: "TSCALE", + 0x40: "SPORT0_CTL", 0x41: "SPORT0_DIV", + 0x42: "SPORT0_MCTL", 0x43: "SPORT0_CS0", + 0x44: "SPORT0_TX", 0x45: "SPORT0_RX", + 0x46: "SPORT0_STAT", + 0x60: "SPORT1_CTL", 0x61: "SPORT1_DIV", + 0x62: "SPORT1_MCTL", 0x63: "SPORT1_CS0", + 0x64: "SPORT1_TX", 0x65: "SPORT1_RX", + 0x66: "SPORT1_STAT", + 0x80: "SPICTL", 0x81: "SPISTAT", 0x82: "SPIFLG", + 0x83: "SPIBAUD", 0x84: "SPIRX", 0x85: "SPITX", + 0xA0: "UART_TX", 0xA1: "UART_RX", 0xA2: "UART_CTL", + 0xA3: "UART_STAT", 0xA4: "UART_DIV", + 0xC0: "FLAGD", 0xC1: "FLAGP", 0xC2: "FLAGS", + 0xC3: "FLAGC", +} + +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): + """Read 24-bit PM words from packed binary.""" + 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 classify_word(w): + """Return a rough classification of a PM word.""" + if w == 0: + return "null" + b23_22 = (w >> 22) & 3 + if b23_22 == 3: + return "compute_multi" # Type 1 + b23_16 = (w >> 16) & 0xFF + if b23_16 == 0x0A: + return "rts" + if (w >> 18) == 0x07: + return "jump" + if (w >> 19) == 0x03: + return "jump_cond" + if b23_16 == 0x16: + return "do_until" + if b23_16 == 0x0B: + return "jump_indirect" + if b23_16 == 0x05: + return "ljump" + return "code" + + +def find_constants(words): + """Extract all immediate constant loads.""" + constants = [] + for i, w in enumerate(words): + addr = i * 3 + b23_22 = (w >> 22) & 3 + b21_20 = (w >> 20) & 3 + + # Type 6: Dreg = Imm16 + if b23_22 == 1 and b21_20 == 0: + val = (w >> 4) & 0xFFFF + reg = REG0[w & 0xF] + constants.append((addr, reg, val, "dreg")) + + # Type 7 REG1: Reg1 = Imm16 + elif b23_22 == 1 and b21_20 == 1: + val = (w >> 4) & 0xFFFF + reg = REG1[w & 0xF] + constants.append((addr, reg, val, "reg1")) + + # Type 7 REG2: Reg2 = Imm16 + elif b23_22 == 0 and b21_20 == 3: + val = (w >> 4) & 0xFFFF + reg = REG2[w & 0xF] + constants.append((addr, reg, val, "reg2")) + + # Type 33: Reg3 = Data12 + elif (w >> 16) == 0x10: + val = (w >> 4) & 0xFFF + constants.append((addr, "REG3", val, "short")) + + # Type 34: IO write + if (w >> 16) == 0x06 and ((w >> 15) & 1): + d = (w >> 12) & 1 + io_addr = (((w >> 13) & 3) << 8) | ((w >> 4) & 0xFF) + dreg = REG0[w & 0xF] + io_name = IO_NAMES.get(io_addr, f"IO_0x{io_addr:03X}") + if d: + constants.append((addr, io_name, None, + f"io_write({dreg})")) + else: + constants.append((addr, dreg, None, + f"io_read({io_name})")) + + return constants + + +def find_do_loops(words): + """Find DO UNTIL loops and their bodies.""" + loops = [] + for i, w in enumerate(words): + if (w >> 16) == 0x16: + rel = (w >> 4) & 0xFFF + term = w & 0xF + cond = ["EQ", "NE", "GT", "LE", "LT", "GE", + "AV", "NOT AV", "AC", "NOT AC", + "SWCOND", "NOT SWCOND", "MV", "NOT MV", + "NOT CE", "TRUE"][term] + srel = rel if rel < 0x800 else rel - 0x1000 + target = i * 3 + srel * 3 + # Count MAC ops in loop body + mac_count = 0 + for j in range(i + 1, min(i + srel + 1, len(words))): + if (words[j] >> 22) & 3 == 3: # Type 1 + amf = (words[j] >> 13) & 0x1F + if amf < 16: # MAC operation + mac_count += 1 + loops.append((i * 3, target, cond, srel, mac_count)) + return loops + + +def memory_map(words, chunk_size=64): + """Build a coarse memory map.""" + regions = [] + i = 0 + while i < len(words): + end = min(i + chunk_size, len(words)) + chunk = words[i:end] + n_null = sum(1 for w in chunk if w == 0) + n_multi = sum(1 for w in chunk + if (w >> 22) & 3 == 3) + n_jump = sum(1 for w in chunk + if classify_word(w) in + ("jump", "jump_cond", "rts", "do_until")) + + if n_null > len(chunk) * 0.9: + kind = "empty" + elif n_multi > len(chunk) * 0.3: + kind = "dsp_kernel" + elif n_jump > len(chunk) * 0.1: + kind = "control_flow" + else: + kind = "code/data" + + if regions and regions[-1][2] == kind: + regions[-1] = (regions[-1][0], end * 3, kind) + else: + regions.append((i * 3, end * 3, kind)) + i = end + return regions + + +def analyze_pm(path, dm_path=None): + """Full PM analysis.""" + words = read_pm(path) + size = len(words) * 3 + + print(f"=== PM Firmware Overview ===") + print(f"File: {path}") + print(f"Size: {size} bytes ({len(words)} instructions)") + print() + + # Memory map + regions = memory_map(words) + print(f"--- Memory Map ---") + for start, end, kind in regions: + label = {"empty": "EMPTY (null)", + "dsp_kernel": "DSP KERNEL (MAC-heavy)", + "control_flow": "CONTROL FLOW (jumps)", + "code/data": "CODE / DATA"}[kind] + print(f" 0x{start:06X}-0x{end:06X}" + f" ({(end-start)//3:4d} words) {label}") + print() + + # Entry point + if words: + w0 = words[0] + if (w0 >> 18) == 0x07: + rel = ((w0 >> 4) & 0x3FFF) | ((w0 & 3) << 14) + srel = rel if rel < 0x8000 else rel - 0x10000 + target = srel * 3 + print(f"--- Entry Point ---") + print(f" Reset vector: JUMP 0x{target:06X}") + print() + elif w0 == 0: + print(f"--- Entry Point ---") + print(f" Reset vector: NOP (code starts at 0x000003)") + print() + + # DO UNTIL loops (DSP kernels) + loops = find_do_loops(words) + if loops: + print(f"--- DSP Loops ({len(loops)} found) ---") + for addr, end, cond, length, macs in loops: + kind = "" + if macs > 0: + kind = f" [MAC kernel, {macs} multiply-accumulate ops]" + elif cond == "NOT CE": + kind = " [counter loop]" + print(f" 0x{addr:06X}: DO 0x{end:06X} UNTIL {cond}" + f" ({length} words){kind}") + print() + + # Constants + constants = find_constants(words) + if constants: + # Group by type + reg_loads = [(a, r, v, t) for a, r, v, t in constants + if t in ("dreg", "reg1", "reg2", "short")] + io_ops = [(a, r, v, t) for a, r, v, t in constants + if t.startswith("io_")] + + if reg_loads: + print(f"--- Constants ({len(reg_loads)} register loads) ---") + # Show unique values + seen = {} + for addr, reg, val, _ in reg_loads: + key = (reg, val) + if key not in seen: + seen[key] = [] + seen[key].append(addr) + + for (reg, val), addrs in sorted(seen.items(), + key=lambda x: x[0][1]): + sval = val - 0x10000 if val >= 0x8000 else val + locs = ", ".join(f"0x{a:06X}" for a in addrs[:3]) + more = f" +{len(addrs)-3} more" if len(addrs) > 3 else "" + print(f" {reg:8s} = 0x{val:04X}" + f" ({sval:6d}) @ {locs}{more}") + print() + + if io_ops: + print(f"--- I/O Operations ({len(io_ops)} found) ---") + for addr, reg, _, typ in io_ops: + print(f" 0x{addr:06X}: {typ}") + print() + + # DM analysis if provided + if dm_path and os.path.isfile(dm_path): + print(f"--- DM Cross-Reference ---") + dm_data = open(dm_path, "rb").read() + dm_size = len(dm_data) + print(f" DM file: {dm_path} ({dm_size} bytes," + f" {dm_size//2} words)") + + # Find all I-register loads and show what's at those + # DM addresses + for addr, reg, val, typ in constants: + if reg.startswith("I") and reg[1:].isdigit() \ + and typ in ("reg1", "reg2"): + dm_byte = val * 2 + if dm_byte + 20 <= dm_size: + sample = [] + for j in range(10): + w = struct.unpack_from( + ">H", dm_data, dm_byte + j * 2)[0] + sample.append(f"0x{w:04X}") + print(f" {reg} = 0x{val:04X} -> DM content:" + f" [{', '.join(sample[:5])} ...]") + print() + + # Summary + n_code = sum(1 for w in words if w != 0) + n_mac = sum(1 for w in words if (w >> 22) & 3 == 3) + n_jump = sum(1 for w in words + if classify_word(w) in + ("jump", "jump_cond", "jump_indirect")) + n_rts = sum(1 for w in words + if classify_word(w) == "rts") + + print(f"--- Summary ---") + print(f" Total words: {len(words)}") + print(f" Non-zero: {n_code}" + f" ({n_code*100//max(len(words),1)}%)") + print(f" Multifunction: {n_mac} (MAC/ALU + memory)") + print(f" Jumps/Calls: {n_jump}") + print(f" Returns: {n_rts}") + print(f" DO loops: {len(loops)}") + print(f" I/O accesses: {len(io_ops) if constants else 0}") + + +def main(): + parser = argparse.ArgumentParser( + description="Quick overview of ADSP-219x PM firmware.") + parser.add_argument("file", help="PM binary file (packed 3-byte)") + parser.add_argument("--dm", help="Optional DM binary for" + " cross-reference") + 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_pm(args.file, args.dm) + + +if __name__ == "__main__": + main()