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.
This commit is contained in:
334
tools/firmware_overview.py
Executable file
334
tools/firmware_overview.py
Executable file
@@ -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()
|
||||
Reference in New Issue
Block a user