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