DTC and SCI improvements
This commit is contained in:
16
h8536/cli.py
16
h8536/cli.py
@@ -8,8 +8,11 @@ from .cycles import annotate_cycles
|
||||
from .data_analysis import analyze_unreached_data
|
||||
from .decoder import H8536Decoder
|
||||
from .formatting import parse_int
|
||||
from .peripheral_access import analyze_peripheral_access
|
||||
from .render import format_callgraph_dot, format_listing, write_json
|
||||
from .rom import Rom
|
||||
from .sci import analyze_sci
|
||||
from .timing import summarize_timing
|
||||
from .vectors import read_dtc_vectors_max, read_dtc_vectors_min, read_vectors_max, read_vectors_min
|
||||
|
||||
|
||||
@@ -31,10 +34,14 @@ def main() -> int:
|
||||
parser.add_argument("--end", type=parse_int, default=None, help="decode upper address limit, exclusive")
|
||||
parser.add_argument("--entry", type=parse_int, action="append", default=[], help="extra entry point to trace")
|
||||
parser.add_argument("--br", type=parse_int, default=None, help="optional BR value for @aa:8 short absolute operands")
|
||||
parser.add_argument("--clock-hz", type=parse_int, default=None, help="oscillator clock in Hz for SCI baud inference")
|
||||
parser.add_argument("--linear", action="store_true", help="linear-sweep the selected range instead of tracing from vectors")
|
||||
parser.add_argument("--cycles", action="store_true", help="append Appendix A cycle estimates to assembly comments")
|
||||
parser.add_argument("--timing", action="store_true", help="include straight-line block and loop cycle summaries")
|
||||
parser.add_argument("--callgraph-dot", type=Path, default=None, help="optional Graphviz DOT call graph output")
|
||||
args = parser.parse_args()
|
||||
if args.clock_hz is not None and args.clock_hz <= 0:
|
||||
parser.error("--clock-hz must be positive")
|
||||
|
||||
data = args.rom.read_bytes()
|
||||
rom = Rom(data)
|
||||
@@ -65,6 +72,9 @@ def main() -> int:
|
||||
annotate_cycles(instructions, args.mode)
|
||||
data_candidates = analyze_unreached_data(rom, instructions, args.start, end)
|
||||
call_graph = build_call_graph(instructions, vectors, labels)
|
||||
timing_summary = summarize_timing(instructions, labels, call_graph) if args.timing else None
|
||||
sci_analysis = analyze_sci(instructions, clock_hz=args.clock_hz)
|
||||
peripheral_access = analyze_peripheral_access(instructions)
|
||||
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(
|
||||
@@ -78,7 +88,10 @@ def main() -> int:
|
||||
traced=not args.linear,
|
||||
dtc_vectors=dtc_vectors,
|
||||
data_candidates=data_candidates,
|
||||
timing_summary=timing_summary,
|
||||
show_cycles=args.cycles,
|
||||
sci_analysis=sci_analysis,
|
||||
peripheral_access=peripheral_access,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -92,6 +105,9 @@ def main() -> int:
|
||||
dtc_vectors=dtc_vectors,
|
||||
data_candidates=data_candidates,
|
||||
call_graph=call_graph,
|
||||
timing_summary=timing_summary,
|
||||
sci_analysis=sci_analysis,
|
||||
peripheral_access=peripheral_access,
|
||||
)
|
||||
if args.callgraph_dot:
|
||||
args.callgraph_dot.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
129
h8536/dtc.py
Normal file
129
h8536/dtc.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
from .formatting import h16
|
||||
from .memory import region_for
|
||||
from .rom import Rom
|
||||
from .tables import IO_REGISTERS
|
||||
|
||||
|
||||
DTC_REGISTER_INFO_SIZE = 8
|
||||
DTC_RESERVED_MODE_MASK = 0x1FFF
|
||||
|
||||
|
||||
class DtcEndpointInfo(TypedDict):
|
||||
address: int
|
||||
text: str
|
||||
name: str | None
|
||||
region: str
|
||||
increment: bool
|
||||
increment_step: int
|
||||
|
||||
|
||||
class DtcModeInfo(TypedDict):
|
||||
raw: int
|
||||
size: str
|
||||
bytes_per_transfer: int
|
||||
source_increment: bool
|
||||
destination_increment: bool
|
||||
source_increment_step: int
|
||||
destination_increment_step: int
|
||||
reserved: int
|
||||
reserved_set: bool
|
||||
|
||||
|
||||
class DtcCountInfo(TypedDict):
|
||||
raw: int
|
||||
transfers: int
|
||||
bytes: int
|
||||
zero_means_65536: bool
|
||||
|
||||
|
||||
class DtcRegisterInfo(TypedDict, total=False):
|
||||
address: int
|
||||
valid: bool
|
||||
error: str
|
||||
dtmr: int
|
||||
dtsr: int
|
||||
dtdr: int
|
||||
dtcr: int
|
||||
mode: DtcModeInfo
|
||||
source: DtcEndpointInfo
|
||||
destination: DtcEndpointInfo
|
||||
count: DtcCountInfo
|
||||
|
||||
|
||||
def _endpoint(address: int, increment: bool, increment_step: int) -> DtcEndpointInfo:
|
||||
name = IO_REGISTERS.get(address)
|
||||
return {
|
||||
"address": address,
|
||||
"text": name or h16(address),
|
||||
"name": name,
|
||||
"region": region_for(address).name,
|
||||
"increment": increment,
|
||||
"increment_step": increment_step if increment else 0,
|
||||
}
|
||||
|
||||
|
||||
def _mode(dtmr: int) -> DtcModeInfo:
|
||||
size = "word" if dtmr & 0x8000 else "byte"
|
||||
bytes_per_transfer = 2 if size == "word" else 1
|
||||
source_increment = bool(dtmr & 0x4000)
|
||||
destination_increment = bool(dtmr & 0x2000)
|
||||
source_step = bytes_per_transfer if source_increment else 0
|
||||
destination_step = bytes_per_transfer if destination_increment else 0
|
||||
reserved = dtmr & DTC_RESERVED_MODE_MASK
|
||||
return {
|
||||
"raw": dtmr,
|
||||
"size": size,
|
||||
"bytes_per_transfer": bytes_per_transfer,
|
||||
"source_increment": source_increment,
|
||||
"destination_increment": destination_increment,
|
||||
"source_increment_step": source_step,
|
||||
"destination_increment_step": destination_step,
|
||||
"reserved": reserved,
|
||||
"reserved_set": reserved != 0,
|
||||
}
|
||||
|
||||
|
||||
def decode_dtc_register_info(rom: Rom, address: int) -> DtcRegisterInfo:
|
||||
"""Decode the four-word DTMR/DTSR/DTDR/DTCR block pointed to by a DTC vector."""
|
||||
end = address + DTC_REGISTER_INFO_SIZE - 1
|
||||
if end > 0xFFFF:
|
||||
return {
|
||||
"address": address,
|
||||
"valid": False,
|
||||
"error": f"register information block {h16(address)}+{DTC_REGISTER_INFO_SIZE} exceeds page 0",
|
||||
}
|
||||
if not rom.contains(address, DTC_REGISTER_INFO_SIZE):
|
||||
return {
|
||||
"address": address,
|
||||
"valid": False,
|
||||
"error": f"register information block {h16(address)}-{h16(end)} is outside ROM image",
|
||||
}
|
||||
|
||||
dtmr = rom.u16(address)
|
||||
dtsr = rom.u16(address + 2)
|
||||
dtdr = rom.u16(address + 4)
|
||||
dtcr = rom.u16(address + 6)
|
||||
mode = _mode(dtmr)
|
||||
transfers = 0x10000 if dtcr == 0 else dtcr
|
||||
|
||||
return {
|
||||
"address": address,
|
||||
"valid": True,
|
||||
"dtmr": dtmr,
|
||||
"dtsr": dtsr,
|
||||
"dtdr": dtdr,
|
||||
"dtcr": dtcr,
|
||||
"mode": mode,
|
||||
"source": _endpoint(dtsr, mode["source_increment"], mode["source_increment_step"]),
|
||||
"destination": _endpoint(dtdr, mode["destination_increment"], mode["destination_increment_step"]),
|
||||
"count": {
|
||||
"raw": dtcr,
|
||||
"transfers": transfers,
|
||||
"bytes": transfers * mode["bytes_per_transfer"],
|
||||
"zero_means_65536": dtcr == 0,
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .model import EA
|
||||
from .tables import IO_BITFIELDS, IO_REGISTERS
|
||||
from .tables import IO_BITFIELDS, IO_DTE_BITS, IO_PRIORITY_FIELDS, IO_REGISTERS
|
||||
|
||||
|
||||
def h8(value: int) -> str:
|
||||
@@ -66,6 +66,12 @@ def label_or_h(address: int, labels: dict[int, str]) -> str:
|
||||
|
||||
|
||||
def _bitfield_name(address: int, bit: int) -> str | None:
|
||||
priority_name = _priority_bit_name(address, bit)
|
||||
if priority_name:
|
||||
return priority_name
|
||||
dte_name = _dte_bit_name(address, bit)
|
||||
if dte_name:
|
||||
return dte_name
|
||||
return IO_BITFIELDS.get(address, {}).get(bit)
|
||||
|
||||
|
||||
@@ -89,6 +95,54 @@ def _adcsr_semantics(value: int) -> str:
|
||||
return f"A/D {state}, {mode} {channels}, {conversion}, {interrupt}"
|
||||
|
||||
|
||||
def _priority_bit_name(address: int, bit: int) -> str | None:
|
||||
for shift, source in IO_PRIORITY_FIELDS.get(address, ()):
|
||||
if shift <= bit <= shift + 2:
|
||||
return f"{source} priority bit {bit - shift}"
|
||||
if address in IO_PRIORITY_FIELDS and bit in (7, 3):
|
||||
return "reserved priority bit"
|
||||
return None
|
||||
|
||||
|
||||
def _dte_bit_name(address: int, bit: int) -> str | None:
|
||||
fields = IO_DTE_BITS.get(address)
|
||||
if fields is None:
|
||||
return None
|
||||
source = fields.get(bit)
|
||||
return f"{source} DTC enable" if source else "reserved DTE bit"
|
||||
|
||||
|
||||
def _ipr_semantics(address: int, value: int) -> str:
|
||||
parts: list[str] = []
|
||||
for shift, source in IO_PRIORITY_FIELDS[address]:
|
||||
priority = (value >> shift) & 0x07
|
||||
parts.append(f"{source} priority={priority}")
|
||||
reserved = _set_bit_numbers(value, 0x88)
|
||||
if reserved:
|
||||
parts.append(f"reserved bits {reserved} should be 0")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def _dte_semantics(address: int, value: int) -> str:
|
||||
fields = IO_DTE_BITS[address]
|
||||
parts = [
|
||||
f"{source} {'DTC enabled' if value & (1 << bit) else 'CPU interrupt'}"
|
||||
for bit, source in sorted(fields.items(), reverse=True)
|
||||
]
|
||||
assigned_mask = 0
|
||||
for bit in fields:
|
||||
assigned_mask |= 1 << bit
|
||||
reserved = _set_bit_numbers(value, (~assigned_mask) & 0xFF)
|
||||
if reserved:
|
||||
parts.append(f"reserved bits {reserved} should be 0")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def _set_bit_numbers(value: int, mask: int) -> str:
|
||||
bits = [str(bit) for bit in range(7, -1, -1) if value & mask & (1 << bit)]
|
||||
return ", ".join(bits)
|
||||
|
||||
|
||||
def _sci_smr_semantics(value: int) -> str:
|
||||
mode = "sync" if value & 0x80 else "async"
|
||||
char_len = "7-bit" if value & 0x40 else "8-bit"
|
||||
@@ -157,6 +211,10 @@ def _rstcsr_semantics(value: int) -> str:
|
||||
|
||||
|
||||
def _semantic_values(address: int, value: int) -> str:
|
||||
if address in IO_PRIORITY_FIELDS:
|
||||
return _ipr_semantics(address, value)
|
||||
if address in IO_DTE_BITS:
|
||||
return _dte_semantics(address, value)
|
||||
if address == 0xFEE8:
|
||||
return _adcsr_semantics(value)
|
||||
if address in (0xFED8, 0xFEF0):
|
||||
|
||||
256
h8536/peripheral_access.py
Normal file
256
h8536/peripheral_access.py
Normal file
@@ -0,0 +1,256 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .formatting import h16
|
||||
from .model import Instruction
|
||||
|
||||
|
||||
MANUAL_REFERENCES = [
|
||||
"Manual/0900766b802125d0.md:12185 FRT FRC/OCRA/OCRB/ICR use TEMP for 16-bit CPU access",
|
||||
"Manual/0900766b802125d0.md:12193 FRT byte access order is upper byte then lower byte",
|
||||
"Manual/0900766b802125d0.md:12212 OCRA/OCRB reads are direct; writes still use TEMP",
|
||||
"Manual/0900766b802125d0.md:17546 A/D ADDRA-ADDRD lower byte is accessed through TEMP",
|
||||
"Manual/0900766b802125d0.md:17556 A/D full-result byte reads must be upper byte then lower byte",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TempRegisterPair:
|
||||
name: str
|
||||
high: int
|
||||
low: int
|
||||
module: str
|
||||
read_only: bool = False
|
||||
high_only_read_ok: bool = False
|
||||
direct_read_without_temp: bool = False
|
||||
|
||||
|
||||
TEMP_REGISTER_PAIRS: tuple[TempRegisterPair, ...] = (
|
||||
TempRegisterPair("FRT1_FRC", 0xFE92, 0xFE93, "FRT TEMP"),
|
||||
TempRegisterPair("FRT1_OCRA", 0xFE94, 0xFE95, "FRT TEMP", direct_read_without_temp=True),
|
||||
TempRegisterPair("FRT1_OCRB", 0xFE96, 0xFE97, "FRT TEMP", direct_read_without_temp=True),
|
||||
TempRegisterPair("FRT1_ICR", 0xFE98, 0xFE99, "FRT TEMP", read_only=True),
|
||||
TempRegisterPair("FRT2_FRC", 0xFEA2, 0xFEA3, "FRT TEMP"),
|
||||
TempRegisterPair("FRT2_OCRA", 0xFEA4, 0xFEA5, "FRT TEMP", direct_read_without_temp=True),
|
||||
TempRegisterPair("FRT2_OCRB", 0xFEA6, 0xFEA7, "FRT TEMP", direct_read_without_temp=True),
|
||||
TempRegisterPair("FRT2_ICR", 0xFEA8, 0xFEA9, "FRT TEMP", read_only=True),
|
||||
TempRegisterPair("FRT3_FRC", 0xFEB2, 0xFEB3, "FRT TEMP"),
|
||||
TempRegisterPair("FRT3_OCRA", 0xFEB4, 0xFEB5, "FRT TEMP", direct_read_without_temp=True),
|
||||
TempRegisterPair("FRT3_OCRB", 0xFEB6, 0xFEB7, "FRT TEMP", direct_read_without_temp=True),
|
||||
TempRegisterPair("FRT3_ICR", 0xFEB8, 0xFEB9, "FRT TEMP", read_only=True),
|
||||
TempRegisterPair("ADDRA", 0xFEE0, 0xFEE1, "A/D TEMP", read_only=True, high_only_read_ok=True),
|
||||
TempRegisterPair("ADDRB", 0xFEE2, 0xFEE3, "A/D TEMP", read_only=True, high_only_read_ok=True),
|
||||
TempRegisterPair("ADDRC", 0xFEE4, 0xFEE5, "A/D TEMP", read_only=True, high_only_read_ok=True),
|
||||
TempRegisterPair("ADDRD", 0xFEE6, 0xFEE7, "A/D TEMP", read_only=True, high_only_read_ok=True),
|
||||
)
|
||||
|
||||
TEMP_PAIR_BY_ADDRESS = {
|
||||
address: (pair, byte_name)
|
||||
for pair in TEMP_REGISTER_PAIRS
|
||||
for address, byte_name in ((pair.high, "high"), (pair.low, "low"))
|
||||
}
|
||||
|
||||
READ_ONLY_ROOTS = {"BTST", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}
|
||||
WRITE_ROOTS = {"BCLR", "BNOT", "BSET", "CLR", "MOV:G", "MOV:S", "MOVTPE", "NEG", "NOT"}
|
||||
|
||||
|
||||
def analyze_peripheral_access(instructions: Mapping[int, Instruction]) -> dict[str, object]:
|
||||
annotations: dict[int, list[str]] = {}
|
||||
warnings: list[dict[str, object]] = []
|
||||
instruction_metadata: dict[int, list[dict[str, object]]] = {}
|
||||
pending: dict[tuple[str, str], dict[str, object]] = {}
|
||||
|
||||
for address in sorted(instructions):
|
||||
ins = instructions[address]
|
||||
accesses = _instruction_accesses(ins)
|
||||
if not accesses:
|
||||
continue
|
||||
instruction_metadata[address] = [_public_access(access) for access in accesses]
|
||||
for access in accesses:
|
||||
note, warning = _analyze_access(access, pending)
|
||||
if note:
|
||||
annotations.setdefault(address, []).append(note)
|
||||
if warning:
|
||||
warnings.append(warning)
|
||||
|
||||
for item in pending.values():
|
||||
pair = item["pair"]
|
||||
assert isinstance(pair, TempRegisterPair)
|
||||
if _pending_high_byte_is_suspicious(pair, str(item["direction"])):
|
||||
warnings.append(
|
||||
{
|
||||
"address": item["address"],
|
||||
"register": pair.name,
|
||||
"severity": "warning",
|
||||
"message": (
|
||||
f"{pair.name} high byte access was not followed by low byte; "
|
||||
"manual recommends word access or high-byte then low-byte"
|
||||
),
|
||||
"manual": MANUAL_REFERENCES,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"manual_references": MANUAL_REFERENCES,
|
||||
"warnings": warnings,
|
||||
"annotations": {
|
||||
address: "; ".join(parts)
|
||||
for address, parts in sorted(annotations.items())
|
||||
},
|
||||
"instructions": instruction_metadata,
|
||||
}
|
||||
|
||||
|
||||
def peripheral_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
|
||||
if not analysis:
|
||||
return ""
|
||||
annotations = analysis.get("annotations")
|
||||
if not isinstance(annotations, Mapping):
|
||||
return ""
|
||||
comment = annotations.get(address)
|
||||
return str(comment) if comment else ""
|
||||
|
||||
|
||||
def peripheral_metadata_for_instruction(
|
||||
analysis: Mapping[str, object] | None,
|
||||
address: int,
|
||||
) -> list[dict[str, object]]:
|
||||
if not analysis:
|
||||
return []
|
||||
instructions = analysis.get("instructions")
|
||||
if not isinstance(instructions, Mapping):
|
||||
return []
|
||||
metadata = instructions.get(address)
|
||||
return metadata if isinstance(metadata, list) else []
|
||||
|
||||
|
||||
def peripheral_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]:
|
||||
if not analysis:
|
||||
return {"manual_references": MANUAL_REFERENCES, "warnings": []}
|
||||
return {
|
||||
"manual_references": analysis.get("manual_references", MANUAL_REFERENCES),
|
||||
"warnings": analysis.get("warnings", []),
|
||||
}
|
||||
|
||||
|
||||
def _analyze_access(
|
||||
access: dict[str, object],
|
||||
pending: dict[tuple[str, str], dict[str, object]],
|
||||
) -> tuple[str, dict[str, object] | None]:
|
||||
pair = access["pair"]
|
||||
assert isinstance(pair, TempRegisterPair)
|
||||
direction = str(access["direction"])
|
||||
byte = str(access["byte"])
|
||||
size = str(access["size"])
|
||||
key = (pair.name, direction)
|
||||
|
||||
if size == "W" and byte == "high":
|
||||
pending.pop(key, None)
|
||||
return f"{pair.name} word {direction}; TEMP byte-order hazard avoided", None
|
||||
|
||||
if byte == "high":
|
||||
pending[key] = access
|
||||
if direction == "read" and pair.high_only_read_ok:
|
||||
return f"{pair.name} high-byte read; valid 8-bit result, read low byte next for full 10-bit value", None
|
||||
if direction == "read" and pair.direct_read_without_temp:
|
||||
return f"{pair.name} high-byte read; OCRA/OCRB reads do not use TEMP", None
|
||||
return f"{pair.name} high-byte {direction}; next low-byte access completes TEMP transfer", None
|
||||
|
||||
previous = pending.pop(key, None)
|
||||
if previous is not None:
|
||||
return f"{pair.name} low-byte {direction}; completes high->low TEMP transfer", None
|
||||
|
||||
warning = {
|
||||
"address": access["address"],
|
||||
"register": pair.name,
|
||||
"severity": "warning",
|
||||
"message": (
|
||||
f"{pair.name} low byte {direction} before matching high byte; "
|
||||
"TEMP may contain stale or unrelated data"
|
||||
),
|
||||
"manual": MANUAL_REFERENCES,
|
||||
}
|
||||
return f"{pair.name} low-byte {direction} before high byte; check TEMP ordering", warning
|
||||
|
||||
|
||||
def _pending_high_byte_is_suspicious(pair: TempRegisterPair, direction: str) -> bool:
|
||||
if direction == "read" and pair.high_only_read_ok:
|
||||
return False
|
||||
if direction == "read" and pair.direct_read_without_temp:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _instruction_accesses(ins: Instruction) -> list[dict[str, object]]:
|
||||
size = _mnemonic_size(ins.mnemonic)
|
||||
direction = _access_direction(ins)
|
||||
if direction is None:
|
||||
return []
|
||||
|
||||
accesses: list[dict[str, object]] = []
|
||||
for ref in ins.references:
|
||||
pair_info = TEMP_PAIR_BY_ADDRESS.get(ref)
|
||||
if pair_info is None:
|
||||
continue
|
||||
pair, byte = pair_info
|
||||
if size == "W" and ref == pair.low:
|
||||
byte = "low_unaligned"
|
||||
accesses.append(
|
||||
{
|
||||
"address": ins.address,
|
||||
"instruction": ins.text,
|
||||
"register": pair.name,
|
||||
"pair": pair,
|
||||
"high_address": pair.high,
|
||||
"low_address": pair.low,
|
||||
"referenced_address": ref,
|
||||
"referenced_address_hex": h16(ref),
|
||||
"byte": byte,
|
||||
"size": size,
|
||||
"direction": direction,
|
||||
},
|
||||
)
|
||||
return accesses
|
||||
|
||||
|
||||
def _public_access(access: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in access.items()
|
||||
if key != "pair"
|
||||
}
|
||||
|
||||
|
||||
def _access_direction(ins: Instruction) -> str | None:
|
||||
root = _mnemonic_root(ins.mnemonic)
|
||||
if root in READ_ONLY_ROOTS:
|
||||
return "read"
|
||||
if root in {"BCLR", "BNOT", "BSET", "CLR", "NEG", "NOT"}:
|
||||
return "write"
|
||||
if root in {"ADD:Q", "ADD:G", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
|
||||
return "write"
|
||||
if root in {"MOV:G", "MOV:S", "MOVTPE"}:
|
||||
return "write" if _destination_operand(ins.operands).startswith("@") else "read"
|
||||
if root in {"MOV:L", "MOV:F", "MOVFPE"}:
|
||||
return "read"
|
||||
if root == "STC":
|
||||
return "write"
|
||||
if root == "LDC":
|
||||
return "read"
|
||||
return None
|
||||
|
||||
|
||||
def _destination_operand(operands: str) -> str:
|
||||
if "," not in operands:
|
||||
return operands.strip()
|
||||
return operands.rsplit(",", 1)[1].strip()
|
||||
|
||||
|
||||
def _mnemonic_root(mnemonic: str) -> str:
|
||||
return mnemonic.rsplit(".", 1)[0]
|
||||
|
||||
|
||||
def _mnemonic_size(mnemonic: str) -> str:
|
||||
return "W" if mnemonic.endswith(".W") else "B"
|
||||
652
h8536/pseudocode.py
Normal file
652
h8536/pseudocode.py
Normal file
@@ -0,0 +1,652 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
JsonObject = dict[str, Any]
|
||||
|
||||
|
||||
BRANCH_CONDITIONS = {
|
||||
"BRN": "0",
|
||||
"BHI": "!C && !Z",
|
||||
"BLS": "C || Z",
|
||||
"BCC": "!C",
|
||||
"BCS": "C",
|
||||
"BNE": "!Z",
|
||||
"BEQ": "Z",
|
||||
"BVC": "!V",
|
||||
"BVS": "V",
|
||||
"BPL": "!N",
|
||||
"BMI": "N",
|
||||
"BGE": "N == V",
|
||||
"BLT": "N != V",
|
||||
"BGT": "!Z && (N == V)",
|
||||
"BLE": "Z || (N != V)",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PseudocodeOptions:
|
||||
include_asm: bool = True
|
||||
include_addresses: bool = True
|
||||
include_cycles: bool = False
|
||||
emit_declarations: bool = True
|
||||
max_functions: int | None = None
|
||||
|
||||
|
||||
def generate_pseudocode(
|
||||
payload: JsonObject,
|
||||
*,
|
||||
source_name: str = "",
|
||||
options: PseudocodeOptions | None = None,
|
||||
) -> str:
|
||||
opts = options or PseudocodeOptions()
|
||||
instructions = list(payload.get("instructions", []))
|
||||
label_names = _collect_label_names(payload)
|
||||
functions = _function_nodes(payload, instructions, label_names)
|
||||
if opts.max_functions is not None:
|
||||
functions = functions[: opts.max_functions]
|
||||
|
||||
lines: list[str] = []
|
||||
lines.extend(_file_header(source_name, payload))
|
||||
if opts.emit_declarations:
|
||||
lines.extend(_declarations(instructions, functions, label_names))
|
||||
|
||||
by_address = {int(ins["address"]): ins for ins in instructions}
|
||||
all_addresses = sorted(by_address)
|
||||
emitted: set[int] = set()
|
||||
for function in functions:
|
||||
function_lines, used_addresses = _render_function(function, by_address, label_names, opts)
|
||||
if function_lines:
|
||||
lines.extend(function_lines)
|
||||
emitted.update(used_addresses)
|
||||
|
||||
orphan_addresses = [address for address in all_addresses if address not in emitted]
|
||||
if orphan_addresses:
|
||||
lines.extend(_render_orphan_block(orphan_addresses, by_address, label_names, opts))
|
||||
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def load_pseudocode_input(path: Path) -> JsonObject:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
if not isinstance(payload, dict) or "instructions" not in payload:
|
||||
raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
|
||||
return payload
|
||||
|
||||
|
||||
def write_pseudocode(input_path: Path, output_path: Path, options: PseudocodeOptions) -> None:
|
||||
payload = load_pseudocode_input(input_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(
|
||||
generate_pseudocode(payload, source_name=str(input_path), options=options),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate conservative C-like pseudocode from h8536_decompiler JSON output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"input",
|
||||
nargs="?",
|
||||
type=Path,
|
||||
default=Path("build/rom_decompiled.json"),
|
||||
help="structured JSON emitted by h8536_decompiler.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
default=Path("build/rom_pseudocode.c"),
|
||||
help="pseudocode output path",
|
||||
)
|
||||
parser.add_argument("--no-asm", action="store_true", help="omit original assembly from line comments")
|
||||
parser.add_argument("--no-addresses", action="store_true", help="omit instruction addresses from line comments")
|
||||
parser.add_argument("--cycles", action="store_true", help="include cycle estimates when present in JSON")
|
||||
parser.add_argument("--no-declarations", action="store_true", help="omit register/function declarations")
|
||||
parser.add_argument("--max-functions", type=int, default=None, help="emit only the first N functions")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
options = PseudocodeOptions(
|
||||
include_asm=not args.no_asm,
|
||||
include_addresses=not args.no_addresses,
|
||||
include_cycles=args.cycles,
|
||||
emit_declarations=not args.no_declarations,
|
||||
max_functions=args.max_functions,
|
||||
)
|
||||
write_pseudocode(args.input, args.out, options)
|
||||
print(f"wrote {args.out}")
|
||||
return 0
|
||||
|
||||
|
||||
def _file_header(source_name: str, payload: JsonObject) -> list[str]:
|
||||
vector_count = len(payload.get("vectors", []))
|
||||
function_count = len(payload.get("call_graph", {}).get("nodes", []))
|
||||
instruction_count = len(payload.get("instructions", []))
|
||||
source = f" from {source_name}" if source_name else ""
|
||||
return [
|
||||
"/*",
|
||||
f" * H8/536 C-like pseudocode{source}",
|
||||
" *",
|
||||
" * This is a conservative structural translation of the decompiler JSON.",
|
||||
" * Helpers such as set_flags_cmp8(), MEM8[], BIT(), C/Z/N/V, and",
|
||||
" * return_from_interrupt() are pseudocode placeholders, not a runtime ABI.",
|
||||
" *",
|
||||
f" * vectors: {vector_count}, functions: {function_count}, instructions: {instruction_count}",
|
||||
" */",
|
||||
"",
|
||||
"#include <stdint.h>",
|
||||
"",
|
||||
"typedef uint8_t u8;",
|
||||
"typedef uint16_t u16;",
|
||||
"",
|
||||
"#define BIT(n) (1u << (n))",
|
||||
"extern volatile u8 MEM8[0x10000];",
|
||||
"extern volatile u16 MEM16[0x10000];",
|
||||
"",
|
||||
"u16 R0, R1, R2, R3, R4, R5, R6, R7;",
|
||||
"u16 SR;",
|
||||
"u8 CCR, BR, EP, DP, TP;",
|
||||
"int C, Z, N, V;",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def _declarations(instructions: list[JsonObject], functions: list[JsonObject], labels: dict[int, str]) -> list[str]:
|
||||
lines: list[str] = []
|
||||
registers = _referenced_io_registers(instructions)
|
||||
if registers:
|
||||
lines.append("/* H8/536 register field symbols used by this ROM. */")
|
||||
for name, (address, width) in sorted(registers.items(), key=lambda item: item[1][0]):
|
||||
c_type = "u16" if width == 16 else "u8"
|
||||
lines.append(f"extern volatile {c_type} {c_identifier(name)}; /* 0x{address:04X} */")
|
||||
lines.append("")
|
||||
|
||||
if functions:
|
||||
lines.append("/* Function entry points discovered from vectors and call targets. */")
|
||||
for function in functions:
|
||||
label = labels.get(int(function["start"]), str(function.get("label", "")))
|
||||
lines.append(f"void {c_identifier(label)}(void);")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def _referenced_io_registers(instructions: list[JsonObject]) -> dict[str, tuple[int, int]]:
|
||||
registers: dict[str, tuple[int, int]] = {}
|
||||
for ins in instructions:
|
||||
width = _size_bits(_mnemonic_size(str(ins.get("mnemonic", ""))))
|
||||
for ref in ins.get("references", []):
|
||||
name = ref.get("name")
|
||||
if not name:
|
||||
continue
|
||||
address = int(ref["address"])
|
||||
old = registers.get(name)
|
||||
old_width = old[1] if old else 8
|
||||
registers[name] = (address, max(old_width, width))
|
||||
return registers
|
||||
|
||||
|
||||
def _collect_label_names(payload: JsonObject) -> dict[int, str]:
|
||||
labels: dict[int, str] = {}
|
||||
for vector in payload.get("vectors", []):
|
||||
target = vector.get("target")
|
||||
label = vector.get("target_label")
|
||||
if target is not None and label:
|
||||
labels[int(target)] = c_identifier(str(label))
|
||||
for node in payload.get("call_graph", {}).get("nodes", []):
|
||||
start = int(node["start"])
|
||||
labels[start] = c_identifier(str(node.get("label") or _label_for(start)))
|
||||
for ins in payload.get("instructions", []):
|
||||
for target in ins.get("targets", []):
|
||||
labels.setdefault(int(target), c_identifier(_label_for(int(target))))
|
||||
return labels
|
||||
|
||||
|
||||
def _function_nodes(
|
||||
payload: JsonObject,
|
||||
instructions: list[JsonObject],
|
||||
labels: dict[int, str],
|
||||
) -> list[JsonObject]:
|
||||
nodes = [dict(node) for node in payload.get("call_graph", {}).get("nodes", [])]
|
||||
if nodes:
|
||||
nodes.sort(key=lambda node: int(node["start"]))
|
||||
return nodes
|
||||
|
||||
if not instructions:
|
||||
return []
|
||||
start = int(min(ins["address"] for ins in instructions))
|
||||
end = int(max(ins["address"] for ins in instructions))
|
||||
return [
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"label": labels.get(start, _label_for(start)),
|
||||
"sources": [],
|
||||
"instruction_count": len(instructions),
|
||||
"calls": [],
|
||||
"unresolved_calls": 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _render_function(
|
||||
function: JsonObject,
|
||||
by_address: dict[int, JsonObject],
|
||||
labels: dict[int, str],
|
||||
opts: PseudocodeOptions,
|
||||
) -> tuple[list[str], set[int]]:
|
||||
start = int(function["start"])
|
||||
end = int(function.get("end", start))
|
||||
addresses = [address for address in sorted(by_address) if start <= address <= end]
|
||||
if not addresses:
|
||||
return [], set()
|
||||
|
||||
name = c_identifier(labels.get(start, str(function.get("label") or _label_for(start))))
|
||||
local_targets = _local_target_addresses(addresses, by_address) | {
|
||||
address for address in addresses if address in labels
|
||||
}
|
||||
|
||||
lines = [f"void {name}(void)", "{"]
|
||||
sources = function.get("sources") or []
|
||||
if sources:
|
||||
lines.append(f" /* vector sources: {', '.join(str(source) for source in sources)} */")
|
||||
|
||||
for address in addresses:
|
||||
if address in local_targets and address != start:
|
||||
lines.append(f"{labels.get(address, _label_for(address))}:")
|
||||
ins = by_address[address]
|
||||
statement = _translate_instruction(ins, labels)
|
||||
comment = _line_comment(ins, opts)
|
||||
lines.append(f" {statement}{comment}")
|
||||
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
return lines, set(addresses)
|
||||
|
||||
|
||||
def _render_orphan_block(
|
||||
addresses: list[int],
|
||||
by_address: dict[int, JsonObject],
|
||||
labels: dict[int, str],
|
||||
opts: PseudocodeOptions,
|
||||
) -> list[str]:
|
||||
lines = ["void unreached_or_unowned_code(void)", "{"]
|
||||
local_targets = _local_target_addresses(addresses, by_address) | {
|
||||
address for address in addresses if address in labels
|
||||
}
|
||||
for address in addresses:
|
||||
if address in local_targets:
|
||||
lines.append(f"{labels.get(address, _label_for(address))}:")
|
||||
ins = by_address[address]
|
||||
lines.append(f" {_translate_instruction(ins, labels)}{_line_comment(ins, opts)}")
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def _local_target_addresses(addresses: list[int], by_address: dict[int, JsonObject]) -> set[int]:
|
||||
address_set = set(addresses)
|
||||
targets: set[int] = set()
|
||||
for address in addresses:
|
||||
for target in by_address[address].get("targets", []):
|
||||
target = int(target)
|
||||
if target in address_set:
|
||||
targets.add(target)
|
||||
return targets
|
||||
|
||||
|
||||
def _translate_instruction(ins: JsonObject, labels: dict[int, str]) -> str:
|
||||
mnemonic = str(ins.get("mnemonic", ""))
|
||||
operands = str(ins.get("operands", ""))
|
||||
kind = str(ins.get("kind", "normal"))
|
||||
ops = split_operands(operands)
|
||||
base = _mnemonic_base(mnemonic)
|
||||
size = _mnemonic_size(mnemonic)
|
||||
|
||||
if kind == "return":
|
||||
if ops:
|
||||
return f"return_with_stack_adjust({_format_operand(ops[0], size)});"
|
||||
return "return;"
|
||||
if kind == "rte":
|
||||
return "return_from_interrupt();"
|
||||
if kind == "sleep":
|
||||
return "sleep_until_interrupt();"
|
||||
if kind == "call":
|
||||
return _call_statement(ins, labels, ops)
|
||||
if kind in {"branch", "jump"}:
|
||||
return _branch_or_jump_statement(ins, labels, ops, base)
|
||||
|
||||
if base.startswith("."):
|
||||
return f"emit_data({_quoted(str(ins.get('text', mnemonic)))});"
|
||||
|
||||
if base in {"MOV", "MOV:G", "MOV:I", "MOV:E", "MOV:L", "MOV:S", "MOV:F"} and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
return f"{dest} = {_cast(source, size)};"
|
||||
|
||||
if base in {"MOVFPE"} and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
return f"{dest} = read_eclock({source});"
|
||||
if base in {"MOVTPE"} and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
return f"write_eclock({dest}, {source});"
|
||||
|
||||
if base in {"ADD", "ADD:G", "ADD:Q", "ADDS"} and len(ops) == 2:
|
||||
return _binary_update(ops, "+=", size)
|
||||
if base in {"SUB", "SUBS"} and len(ops) == 2:
|
||||
return _binary_update(ops, "-=", size)
|
||||
if base == "OR" and len(ops) == 2:
|
||||
return _binary_update(ops, "|=", size)
|
||||
if base == "AND" and len(ops) == 2:
|
||||
return _binary_update(ops, "&=", size)
|
||||
if base == "XOR" and len(ops) == 2:
|
||||
return _binary_update(ops, "^=", size)
|
||||
|
||||
if base in {"ADDX", "SUBX", "MULXU", "DIVXU"} and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
helper = _helper_name(base, size)
|
||||
return f"{dest} = {helper}({dest}, {source});"
|
||||
|
||||
if base in {"CMP", "CMP:G", "CMP:I", "CMP:E"} and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size)
|
||||
return f"{_helper_name('set_flags_cmp', size)}({dest}, {source});"
|
||||
if base == "TST" and len(ops) == 1:
|
||||
return f"{_helper_name('set_flags_tst', size)}({_format_operand(ops[0], size)});"
|
||||
|
||||
if base == "CLR" and len(ops) == 1:
|
||||
return f"{_format_operand(ops[0], size, lvalue=True)} = 0;"
|
||||
if base == "NEG" and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} = -{target};"
|
||||
if base == "NOT" and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} = ~{target};"
|
||||
|
||||
if base in {"SHAL", "SHLL"} and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} <<= 1;"
|
||||
if base in {"SHAR", "SHLR"} and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} >>= 1;"
|
||||
if base in {"ROTL", "ROTR", "ROTXL", "ROTXR"} and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} = {_helper_name(base.lower(), size)}({target});"
|
||||
if base == "SWAP" and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} = swap_bytes({target});"
|
||||
if base == "EXTU" and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} = zero_extend8({target});"
|
||||
if base == "EXTS" and len(ops) == 1:
|
||||
target = _format_operand(ops[0], size, lvalue=True)
|
||||
return f"{target} = sign_extend8({target});"
|
||||
|
||||
if base in {"BSET", "BCLR", "BNOT", "BTST"} and len(ops) == 2:
|
||||
return _bit_statement(base, ops, size)
|
||||
|
||||
if base == "LDC" and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
return f"{dest} = {_cast(source, size)};"
|
||||
if base == "STC" and len(ops) == 2:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
return f"{dest} = {_cast(source, size)};"
|
||||
if base == "ORC" and len(ops) == 2:
|
||||
return _binary_update(ops, "|=", size)
|
||||
if base == "ANDC" and len(ops) == 2:
|
||||
return _binary_update(ops, "&=", size)
|
||||
if base == "XORC" and len(ops) == 2:
|
||||
return _binary_update(ops, "^=", size)
|
||||
|
||||
if base == "LDM" and len(ops) == 2:
|
||||
return f"pop_registers({_register_list_argument(ops[1])});"
|
||||
if base == "STM" and len(ops) == 2:
|
||||
return f"push_registers({_register_list_argument(ops[0])});"
|
||||
if base == "LINK" and len(ops) == 2:
|
||||
return f"link_frame({_format_operand(ops[1], size)});"
|
||||
if base == "UNLK":
|
||||
return "unlink_frame();"
|
||||
if base == "TRAPA" and ops:
|
||||
return f"trap({_format_operand(ops[0], size)});"
|
||||
if base == "TRAP/VS":
|
||||
return "trap_vs();"
|
||||
if base == "NOP":
|
||||
return "/* nop */;"
|
||||
|
||||
return f"asm_{_safe_token(base)}({_quoted(str(ins.get('text') or mnemonic))});"
|
||||
|
||||
|
||||
def _branch_or_jump_statement(ins: JsonObject, labels: dict[int, str], ops: list[str], base: str) -> str:
|
||||
target = _target_label(ins, labels)
|
||||
if base in {"BRA", "JMP", "PJMP"}:
|
||||
if target:
|
||||
return f"goto {target};"
|
||||
expr = _format_operand(ops[0], "") if ops else "unknown_target"
|
||||
return f"goto_indirect({expr});"
|
||||
if base.startswith("SCB/"):
|
||||
register = _format_operand(ops[0], "") if ops else "R?"
|
||||
cond = base.split("/", 1)[1].lower()
|
||||
return f"if (scb_{cond}({register})) goto {target or 'unknown_target'};"
|
||||
condition = BRANCH_CONDITIONS.get(base, f"cond_{_safe_token(base)}()")
|
||||
return f"if ({condition}) goto {target or 'unknown_target'};"
|
||||
|
||||
|
||||
def _call_statement(ins: JsonObject, labels: dict[int, str], ops: list[str]) -> str:
|
||||
target = _target_label(ins, labels)
|
||||
if target:
|
||||
return f"{target}();"
|
||||
expr = _format_operand(ops[0], "") if ops else "unknown_target"
|
||||
return f"call_indirect({expr});"
|
||||
|
||||
|
||||
def _target_label(ins: JsonObject, labels: dict[int, str]) -> str:
|
||||
targets = ins.get("targets", [])
|
||||
if targets:
|
||||
target = int(targets[0])
|
||||
return labels.get(target, _label_for(target))
|
||||
return ""
|
||||
|
||||
|
||||
def _binary_update(ops: list[str], operator: str, size: str) -> str:
|
||||
source = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
return f"{dest} {operator} {_cast(source, size)};"
|
||||
|
||||
|
||||
def _bit_statement(base: str, ops: list[str], size: str) -> str:
|
||||
bit = _format_operand(ops[0], size)
|
||||
dest = _format_operand(ops[1], size, lvalue=True)
|
||||
bit_expr = f"BIT({bit})"
|
||||
if base == "BSET":
|
||||
return f"{dest} |= {bit_expr};"
|
||||
if base == "BCLR":
|
||||
return f"{dest} &= ~{bit_expr};"
|
||||
if base == "BNOT":
|
||||
return f"{dest} ^= {bit_expr};"
|
||||
return f"set_flags_btst({dest}, {bit});"
|
||||
|
||||
|
||||
def split_operands(operands: str) -> list[str]:
|
||||
if not operands:
|
||||
return []
|
||||
parts: list[str] = []
|
||||
start = 0
|
||||
depth = 0
|
||||
for idx, char in enumerate(operands):
|
||||
if char in "({":
|
||||
depth += 1
|
||||
elif char in ")}" and depth:
|
||||
depth -= 1
|
||||
elif char == "," and depth == 0:
|
||||
parts.append(operands[start:idx].strip())
|
||||
start = idx + 1
|
||||
parts.append(operands[start:].strip())
|
||||
return [part for part in parts if part]
|
||||
|
||||
|
||||
def _format_operand(operand: str, size: str, *, lvalue: bool = False) -> str:
|
||||
op = _replace_h_literals(operand.strip())
|
||||
if op.startswith("#"):
|
||||
return op[1:]
|
||||
if op.startswith("@(") and op.endswith(")"):
|
||||
inner = op[2:-1]
|
||||
pieces = split_operands(inner)
|
||||
if len(pieces) == 2:
|
||||
disp, reg = pieces
|
||||
offset = f"{reg} - {disp[1:]}" if disp.startswith("-") else f"{reg} + {disp}"
|
||||
return f"{_mem_name(size)}[{offset}]"
|
||||
if re.fullmatch(r"@-R[0-7]", op):
|
||||
return f"{_mem_name(size)}[--{op[2:]}]"
|
||||
if re.fullmatch(r"@R[0-7]\+", op):
|
||||
return f"{_mem_name(size)}[{op[1:-1]}++]"
|
||||
if re.fullmatch(r"@R[0-7]", op):
|
||||
return f"{_mem_name(size)}[{op[1:]}]"
|
||||
if op.startswith("@BR:"):
|
||||
return f"{_mem_name(size)}[(BR << 8) | {op[4:]}]"
|
||||
if op.startswith("@0x"):
|
||||
return f"{_mem_name(size)}[{op[1:]}]"
|
||||
if op.startswith("@"):
|
||||
return c_identifier(op[1:])
|
||||
if op.startswith("{") and op.endswith("}"):
|
||||
return _register_list_argument(op)
|
||||
if re.fullmatch(r"loc_[0-9A-Fa-f]{4}", op):
|
||||
return c_identifier(op)
|
||||
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_/\?]*", op):
|
||||
return c_identifier(op)
|
||||
return op
|
||||
|
||||
|
||||
def _cast(expr: str, size: str) -> str:
|
||||
if size == "B":
|
||||
return f"(uint8_t)({expr})"
|
||||
if size == "W":
|
||||
return f"(uint16_t)({expr})"
|
||||
return expr
|
||||
|
||||
|
||||
def _line_comment(ins: JsonObject, opts: PseudocodeOptions) -> str:
|
||||
parts: list[str] = []
|
||||
if opts.include_addresses:
|
||||
parts.append(f"{int(ins['address']):04X}")
|
||||
if opts.include_asm:
|
||||
text = str(ins.get("text") or _instruction_text(ins))
|
||||
parts.append(text)
|
||||
comment = str(ins.get("comment") or "").strip()
|
||||
if comment:
|
||||
parts.append(comment)
|
||||
parts.extend(_metadata_comments(ins))
|
||||
if opts.include_cycles and ins.get("cycles"):
|
||||
parts.append(_cycle_summary(ins["cycles"]))
|
||||
if not parts:
|
||||
return ""
|
||||
return " /* " + "; ".join(_sanitize_comment(part) for part in parts) + " */"
|
||||
|
||||
|
||||
def _metadata_comments(ins: JsonObject) -> list[str]:
|
||||
comments: list[str] = []
|
||||
sci = ins.get("sci")
|
||||
if isinstance(sci, dict):
|
||||
for inference in sci.get("inferences", []):
|
||||
if isinstance(inference, dict) and inference.get("comment"):
|
||||
comments.append(str(inference["comment"]))
|
||||
|
||||
for access in ins.get("peripheral_access", []):
|
||||
if not isinstance(access, dict):
|
||||
continue
|
||||
register = access.get("register")
|
||||
direction = access.get("direction")
|
||||
size = access.get("size")
|
||||
byte = access.get("byte")
|
||||
if register and direction:
|
||||
comments.append(f"{register} {size} {direction} {byte} TEMP access")
|
||||
return comments
|
||||
|
||||
|
||||
def _instruction_text(ins: JsonObject) -> str:
|
||||
mnemonic = str(ins.get("mnemonic", ""))
|
||||
operands = str(ins.get("operands", ""))
|
||||
return f"{mnemonic} {operands}".strip()
|
||||
|
||||
|
||||
def _cycle_summary(cycles: JsonObject) -> str:
|
||||
if "cycles" in cycles:
|
||||
return f"cycles={cycles['cycles']}"
|
||||
if "not_taken" in cycles and "taken" in cycles:
|
||||
return f"cycles={cycles['not_taken']}/{cycles['taken']} nt/t"
|
||||
return "cycles=?"
|
||||
|
||||
|
||||
def _mnemonic_base(mnemonic: str) -> str:
|
||||
return mnemonic.rsplit(".", 1)[0] if "." in mnemonic else mnemonic
|
||||
|
||||
|
||||
def _mnemonic_size(mnemonic: str) -> str:
|
||||
suffix = mnemonic.rsplit(".", 1)[-1] if "." in mnemonic else ""
|
||||
if suffix in {"B", "W"}:
|
||||
return suffix
|
||||
if mnemonic.startswith("CMP:I"):
|
||||
return "W"
|
||||
if mnemonic.startswith("CMP:E"):
|
||||
return "B"
|
||||
return ""
|
||||
|
||||
|
||||
def _size_bits(size: str) -> int:
|
||||
return 16 if size == "W" else 8
|
||||
|
||||
|
||||
def _mem_name(size: str) -> str:
|
||||
return "MEM16" if size == "W" else "MEM8"
|
||||
|
||||
|
||||
def _helper_name(base: str, size: str) -> str:
|
||||
suffix = {"B": "8", "W": "16"}.get(size, "")
|
||||
return f"{_safe_token(base)}{suffix}"
|
||||
|
||||
|
||||
def _register_list_argument(operand: str) -> str:
|
||||
inner = operand.strip().strip("{}")
|
||||
regs = [c_identifier(part.strip()) for part in inner.split(",") if part.strip()]
|
||||
return ", ".join(regs) if regs else "/* empty */"
|
||||
|
||||
|
||||
def _replace_h_literals(text: str) -> str:
|
||||
return re.sub(r"H'([0-9A-Fa-f]+)", lambda match: "0x" + match.group(1).upper(), text)
|
||||
|
||||
|
||||
def c_identifier(name: str) -> str:
|
||||
cleaned = re.sub(r"[^0-9A-Za-z_]", "_", name.strip())
|
||||
cleaned = re.sub(r"_+", "_", cleaned).strip("_")
|
||||
if not cleaned:
|
||||
cleaned = "unnamed"
|
||||
if cleaned[0].isdigit():
|
||||
cleaned = "_" + cleaned
|
||||
return cleaned
|
||||
|
||||
|
||||
def _safe_token(text: str) -> str:
|
||||
return c_identifier(text).lower()
|
||||
|
||||
|
||||
def _label_for(address: int) -> str:
|
||||
return f"loc_{address:04X}"
|
||||
|
||||
|
||||
def _quoted(text: str) -> str:
|
||||
return json.dumps(text)
|
||||
|
||||
|
||||
def _sanitize_comment(text: str) -> str:
|
||||
return str(text).replace("*/", "* /").replace("\r", " ").replace("\n", " ")
|
||||
123
h8536/render.py
123
h8536/render.py
@@ -4,14 +4,57 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
from .cycles import cycle_comment
|
||||
from .dtc import DtcEndpointInfo, DtcRegisterInfo
|
||||
from .formatting import h16, label_for
|
||||
from .memory import MEMORY_REGIONS, region_for
|
||||
from .model import Instruction
|
||||
from .peripheral_access import (
|
||||
peripheral_comment_for_instruction,
|
||||
peripheral_json_payload,
|
||||
peripheral_metadata_for_instruction,
|
||||
)
|
||||
from .rom import Rom
|
||||
from .sci import sci_comment_for_instruction, sci_json_payload, sci_metadata_for_instruction
|
||||
from .tables import IO_REGISTERS
|
||||
from .timing import format_timing_summary
|
||||
from .vectors import DtcVectorEntry
|
||||
|
||||
|
||||
def _dtc_endpoint_text(endpoint: DtcEndpointInfo) -> str:
|
||||
address = endpoint["address"]
|
||||
text = endpoint["text"]
|
||||
return f"{text} ({h16(address)})" if text != h16(address) else text
|
||||
|
||||
|
||||
def _dtc_register_lines(vector_addr: int, entry: DtcVectorEntry, info: DtcRegisterInfo) -> list[str]:
|
||||
target = entry["register_info_address"]
|
||||
if not info.get("valid"):
|
||||
error = info.get("error", "register information unavailable")
|
||||
return [f"; {h16(vector_addr)} {entry['source']:<24} {h16(target)} unavailable: {error}"]
|
||||
|
||||
mode = info["mode"]
|
||||
source = info["source"]
|
||||
destination = info["destination"]
|
||||
count = info["count"]
|
||||
lines = [
|
||||
(
|
||||
f"; {h16(vector_addr)} {entry['source']:<24} {h16(target)} "
|
||||
f"{mode['size']} x{count['transfers']} ({count['bytes']} bytes): "
|
||||
f"{_dtc_endpoint_text(source)} -> {_dtc_endpoint_text(destination)} "
|
||||
f"[src+={mode['source_increment_step']}, dst+={mode['destination_increment_step']}]"
|
||||
),
|
||||
(
|
||||
f"; DTMR={h16(info['dtmr'])} DTSR={h16(info['dtsr'])} "
|
||||
f"DTDR={h16(info['dtdr'])} DTCR={h16(info['dtcr'])}"
|
||||
),
|
||||
]
|
||||
if mode["reserved_set"]:
|
||||
lines.append(f"; warning: DTMR reserved bits set ({h16(mode['reserved'])})")
|
||||
if count["zero_means_65536"]:
|
||||
lines.append("; DTCR raw zero means an initial transfer count of 65536")
|
||||
return lines
|
||||
|
||||
|
||||
def _reference_comment(ins: Instruction) -> str:
|
||||
parts: list[str] = []
|
||||
for address in ins.references:
|
||||
@@ -31,7 +74,10 @@ def format_listing(
|
||||
traced: bool,
|
||||
dtc_vectors: dict[int, DtcVectorEntry] | None = None,
|
||||
data_candidates: dict[str, list[dict[str, object]]] | None = None,
|
||||
timing_summary: dict[str, list[dict[str, object]]] | None = None,
|
||||
show_cycles: bool = False,
|
||||
sci_analysis: dict[str, object] | None = None,
|
||||
peripheral_access: dict[str, object] | None = None,
|
||||
) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("; H8/536 ROM disassembly")
|
||||
@@ -45,6 +91,9 @@ def format_listing(
|
||||
lines.append("; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.")
|
||||
lines.append("; - The register field is H'FE80-H'FFFF; names below come from appendix B.")
|
||||
lines.append("; - @aa:8 short absolute operands use BR as the upper address byte.")
|
||||
lines.append("; - SCI baud inference uses section 14.2.8 BRR formulas when SMR/BRR are known.")
|
||||
if sci_analysis and sci_analysis.get("clock_hz") is None:
|
||||
lines.append("; - Pass --clock-hz to convert SCI BRR settings into numeric baud rates.")
|
||||
if show_cycles:
|
||||
lines.append("; - Cycle counts use Appendix A tables A-7/A-8 for on-chip access with no external wait states.")
|
||||
lines.append("")
|
||||
@@ -63,6 +112,10 @@ def format_listing(
|
||||
target = entry["register_info_address"]
|
||||
lines.append(f"; {h16(vector_addr)} {entry['source']:<24} -> {h16(target)}")
|
||||
lines.append("")
|
||||
lines.append("; DTC Register Information")
|
||||
for vector_addr, entry in sorted(dtc_vectors.items()):
|
||||
lines.extend(_dtc_register_lines(vector_addr, entry, entry["register_info"]))
|
||||
lines.append("")
|
||||
|
||||
if data_candidates:
|
||||
strings = data_candidates.get("strings", [])
|
||||
@@ -81,6 +134,9 @@ def format_listing(
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
if timing_summary:
|
||||
lines.extend(format_timing_summary(timing_summary))
|
||||
|
||||
for address in sorted(instructions):
|
||||
ins = instructions[address]
|
||||
if address in labels:
|
||||
@@ -92,6 +148,8 @@ def format_listing(
|
||||
part
|
||||
for part in (
|
||||
ins.comment,
|
||||
sci_comment_for_instruction(sci_analysis, address),
|
||||
peripheral_comment_for_instruction(peripheral_access, address),
|
||||
_reference_comment(ins) if not ins.comment else "",
|
||||
cycle_comment(ins.cycles) if show_cycles else "",
|
||||
)
|
||||
@@ -111,6 +169,9 @@ def write_json(
|
||||
dtc_vectors: dict[int, DtcVectorEntry] | None = None,
|
||||
data_candidates: dict[str, list[dict[str, object]]] | None = None,
|
||||
call_graph: dict[str, object] | None = None,
|
||||
timing_summary: dict[str, list[dict[str, object]]] | None = None,
|
||||
sci_analysis: dict[str, object] | None = None,
|
||||
peripheral_access: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
payload = {
|
||||
"vectors": [
|
||||
@@ -130,35 +191,53 @@ def write_json(
|
||||
],
|
||||
"data_candidates": data_candidates or {"strings": [], "pointer_tables": []},
|
||||
"call_graph": call_graph or {"nodes": [], "edges": []},
|
||||
"timing_summary": timing_summary or {"blocks": [], "loops": []},
|
||||
"sci": sci_json_payload(sci_analysis),
|
||||
"peripheral_access": peripheral_json_payload(peripheral_access),
|
||||
"instructions": [
|
||||
{
|
||||
"address": ins.address,
|
||||
"address_region": region_for(ins.address).name,
|
||||
"bytes": ins.raw.hex().upper(),
|
||||
"text": ins.text,
|
||||
"mnemonic": ins.mnemonic,
|
||||
"operands": ins.operands,
|
||||
"kind": ins.kind,
|
||||
"targets": ins.targets,
|
||||
"cycles": ins.cycles,
|
||||
"references": [
|
||||
{
|
||||
"address": address,
|
||||
"name": IO_REGISTERS.get(address),
|
||||
"region": region_for(address).name,
|
||||
"kind": region_for(address).kind,
|
||||
}
|
||||
for address in ins.references
|
||||
],
|
||||
"comment": ins.comment,
|
||||
"valid": ins.valid,
|
||||
}
|
||||
_instruction_payload(ins, sci_analysis, peripheral_access)
|
||||
for ins in (instructions[addr] for addr in sorted(instructions))
|
||||
],
|
||||
}
|
||||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _instruction_payload(
|
||||
ins: Instruction,
|
||||
sci_analysis: dict[str, object] | None = None,
|
||||
peripheral_access: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
payload: dict[str, object] = {
|
||||
"address": ins.address,
|
||||
"address_region": region_for(ins.address).name,
|
||||
"bytes": ins.raw.hex().upper(),
|
||||
"text": ins.text,
|
||||
"mnemonic": ins.mnemonic,
|
||||
"operands": ins.operands,
|
||||
"kind": ins.kind,
|
||||
"targets": ins.targets,
|
||||
"cycles": ins.cycles,
|
||||
"references": [
|
||||
{
|
||||
"address": address,
|
||||
"name": IO_REGISTERS.get(address),
|
||||
"region": region_for(address).name,
|
||||
"kind": region_for(address).kind,
|
||||
}
|
||||
for address in ins.references
|
||||
],
|
||||
"comment": ins.comment,
|
||||
"valid": ins.valid,
|
||||
}
|
||||
sci_metadata = sci_metadata_for_instruction(sci_analysis, ins.address)
|
||||
if sci_metadata:
|
||||
payload["sci"] = sci_metadata
|
||||
peripheral_metadata = peripheral_metadata_for_instruction(peripheral_access, ins.address)
|
||||
if peripheral_metadata:
|
||||
payload["peripheral_access"] = peripheral_metadata
|
||||
return payload
|
||||
|
||||
|
||||
def format_callgraph_dot(call_graph: dict[str, object]) -> str:
|
||||
lines = ["digraph callgraph {"]
|
||||
lines.append(' graph [rankdir="LR"];')
|
||||
|
||||
375
h8536/sci.py
Normal file
375
h8536/sci.py
Normal file
@@ -0,0 +1,375 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from .formatting import parse_int
|
||||
from .model import Instruction
|
||||
|
||||
|
||||
SCI_REGISTERS: dict[str, dict[str, int]] = {
|
||||
"SCI1": {"SMR": 0xFED8, "BRR": 0xFED9, "SCR": 0xFEDA},
|
||||
"SCI2": {"SMR": 0xFEF0, "BRR": 0xFEF1, "SCR": 0xFEF2},
|
||||
}
|
||||
|
||||
SCI_REGISTER_BY_ADDRESS = {
|
||||
address: (channel, register)
|
||||
for channel, registers in SCI_REGISTERS.items()
|
||||
for register, address in registers.items()
|
||||
}
|
||||
|
||||
FORMULAS = {
|
||||
"async": "B = clock_hz / (64 * 2^(2n) * (N + 1))",
|
||||
"sync": "B = clock_hz / (8 * 2^(2n) * (N + 1))",
|
||||
}
|
||||
|
||||
MANUAL_REFERENCES = [
|
||||
"Manual/0900766b802125d0.md:15837 SMR selects SCI mode and CKS1/CKS0 internal clock source",
|
||||
"Manual/0900766b802125d0.md:16027 SCR.CKE1 selects internal or external clock source",
|
||||
"Manual/0900766b802125d0.md:16177 BRR and SMR.CKS determine the baud-rate generator",
|
||||
"Manual/0900766b802125d0.md:16303 asynchronous BRR formula",
|
||||
"Manual/0900766b802125d0.md:16379 synchronous BRR formula",
|
||||
"Manual/0900766b802125d0.md:16410 SCI clock source selection tables",
|
||||
]
|
||||
|
||||
_READ_ONLY_ROOTS = {"BTST", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}
|
||||
|
||||
|
||||
def analyze_sci(
|
||||
instructions: Mapping[int, Instruction],
|
||||
clock_hz: int | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Track SCI setup writes in listing order and infer conservative baud metadata."""
|
||||
state: dict[str, dict[str, int | None]] = {
|
||||
channel: {"SMR": None, "BRR": None, "SCR": None} for channel in SCI_REGISTERS
|
||||
}
|
||||
channels: dict[str, dict[str, list[dict[str, object]]]] = {
|
||||
channel: {"writes": [], "configurations": []} for channel in SCI_REGISTERS
|
||||
}
|
||||
annotations: dict[int, str] = {}
|
||||
instruction_metadata: dict[int, dict[str, object]] = {}
|
||||
seen_configurations: set[tuple[object, ...]] = set()
|
||||
|
||||
for address in sorted(instructions):
|
||||
ins = instructions[address]
|
||||
writes = _apply_instruction(ins, state)
|
||||
if not writes:
|
||||
continue
|
||||
|
||||
affected_channels = sorted({str(write["channel"]) for write in writes})
|
||||
instruction_entry: dict[str, object] = {"writes": writes}
|
||||
for write in writes:
|
||||
channel = str(write["channel"])
|
||||
channels[channel]["writes"].append(write)
|
||||
|
||||
emitted: list[dict[str, object]] = []
|
||||
for channel in affected_channels:
|
||||
inference = _infer_channel(channel, state[channel], clock_hz)
|
||||
if inference is None:
|
||||
continue
|
||||
key = _configuration_key(channel, inference, clock_hz)
|
||||
if key in seen_configurations:
|
||||
continue
|
||||
seen_configurations.add(key)
|
||||
inference = {**inference, "address": ins.address, "instruction": ins.text}
|
||||
channels[channel]["configurations"].append(inference)
|
||||
emitted.append(inference)
|
||||
|
||||
if emitted:
|
||||
annotation = "; ".join(str(item["comment"]) for item in emitted if item.get("comment"))
|
||||
if annotation:
|
||||
annotations[ins.address] = annotation
|
||||
instruction_entry["inferences"] = emitted
|
||||
instruction_metadata[ins.address] = instruction_entry
|
||||
|
||||
return {
|
||||
"clock_hz": clock_hz,
|
||||
"formulas": FORMULAS,
|
||||
"manual_references": MANUAL_REFERENCES,
|
||||
"channels": channels,
|
||||
"annotations": annotations,
|
||||
"instructions": instruction_metadata,
|
||||
}
|
||||
|
||||
|
||||
def sci_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
|
||||
if not analysis:
|
||||
return ""
|
||||
annotations = analysis.get("annotations")
|
||||
if not isinstance(annotations, Mapping):
|
||||
return ""
|
||||
comment = annotations.get(address)
|
||||
return str(comment) if comment else ""
|
||||
|
||||
|
||||
def sci_metadata_for_instruction(analysis: Mapping[str, object] | None, address: int) -> dict[str, object] | None:
|
||||
if not analysis:
|
||||
return None
|
||||
instructions = analysis.get("instructions")
|
||||
if not isinstance(instructions, Mapping):
|
||||
return None
|
||||
metadata = instructions.get(address)
|
||||
return metadata if isinstance(metadata, dict) else None
|
||||
|
||||
|
||||
def sci_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]:
|
||||
if not analysis:
|
||||
return {
|
||||
"clock_hz": None,
|
||||
"formulas": FORMULAS,
|
||||
"manual_references": MANUAL_REFERENCES,
|
||||
"channels": {channel: {"writes": [], "configurations": []} for channel in SCI_REGISTERS},
|
||||
}
|
||||
return {
|
||||
"clock_hz": analysis.get("clock_hz"),
|
||||
"formulas": analysis.get("formulas", FORMULAS),
|
||||
"manual_references": analysis.get("manual_references", MANUAL_REFERENCES),
|
||||
"channels": analysis.get("channels", {}),
|
||||
}
|
||||
|
||||
|
||||
def _apply_instruction(ins: Instruction, state: dict[str, dict[str, int | None]]) -> list[dict[str, object]]:
|
||||
root = _mnemonic_root(ins.mnemonic)
|
||||
if root in _READ_ONLY_ROOTS:
|
||||
return []
|
||||
|
||||
size = _mnemonic_size(ins.mnemonic)
|
||||
base_address = _destination_base_address(ins, size)
|
||||
if base_address is None:
|
||||
return []
|
||||
|
||||
operation = root
|
||||
value: int | None
|
||||
if root in {"BCLR", "BNOT", "BSET"}:
|
||||
bit = _immediate_bit(ins.operands)
|
||||
value = None
|
||||
targets = list(_target_registers(base_address, "B"))
|
||||
if bit is not None and len(targets) == 1:
|
||||
channel, register, _address = targets[0]
|
||||
current = state[channel][register]
|
||||
if current is not None:
|
||||
if root == "BSET":
|
||||
value = current | (1 << bit)
|
||||
elif root == "BCLR":
|
||||
value = current & ~(1 << bit)
|
||||
else:
|
||||
value = current ^ (1 << bit)
|
||||
value &= 0xFF
|
||||
return _record_writes(ins, targets, value, operation, state)
|
||||
|
||||
if root == "CLR":
|
||||
value = 0
|
||||
else:
|
||||
value = _immediate_source_value(ins.operands)
|
||||
if value is None and root == "NOT":
|
||||
targets = list(_target_registers(base_address, size))
|
||||
if len(targets) == 1:
|
||||
channel, register, _address = targets[0]
|
||||
current = state[channel][register]
|
||||
value = None if current is None else (~current) & 0xFF
|
||||
|
||||
targets = list(_target_registers(base_address, size))
|
||||
if not targets:
|
||||
return []
|
||||
if root in {"CMP", "CMP:E", "CMP:G", "CMP:I", "TST"}:
|
||||
return []
|
||||
return _record_writes(ins, targets, value, operation, state)
|
||||
|
||||
|
||||
def _record_writes(
|
||||
ins: Instruction,
|
||||
targets: list[tuple[str, str, int]],
|
||||
value: int | None,
|
||||
operation: str,
|
||||
state: dict[str, dict[str, int | None]],
|
||||
) -> list[dict[str, object]]:
|
||||
width = max(len(targets), 1)
|
||||
writes: list[dict[str, object]] = []
|
||||
for index, (channel, register, register_address) in enumerate(targets):
|
||||
byte_value = _byte_for_target(value, width, index)
|
||||
state[channel][register] = byte_value
|
||||
event: dict[str, object] = {
|
||||
"address": ins.address,
|
||||
"instruction": ins.text,
|
||||
"channel": channel,
|
||||
"register": register,
|
||||
"register_address": register_address,
|
||||
"operation": operation,
|
||||
"value": byte_value,
|
||||
}
|
||||
if byte_value is not None:
|
||||
event["value_hex"] = _hex8(byte_value)
|
||||
writes.append(event)
|
||||
return writes
|
||||
|
||||
|
||||
def _target_registers(base_address: int, size: str) -> list[tuple[str, str, int]]:
|
||||
width = 2 if size == "W" else 1
|
||||
targets: list[tuple[str, str, int]] = []
|
||||
for offset in range(width):
|
||||
address = base_address + offset
|
||||
register = SCI_REGISTER_BY_ADDRESS.get(address)
|
||||
if register is None:
|
||||
continue
|
||||
channel, name = register
|
||||
targets.append((channel, name, address))
|
||||
return targets
|
||||
|
||||
|
||||
def _destination_base_address(ins: Instruction, size: str) -> int | None:
|
||||
destination = _destination_operand(ins.operands)
|
||||
if destination is None or not destination.startswith("@"):
|
||||
return None
|
||||
width = 2 if size == "W" else 1
|
||||
for address in ins.references:
|
||||
if any((address + offset) in SCI_REGISTER_BY_ADDRESS for offset in range(width)):
|
||||
return address
|
||||
return None
|
||||
|
||||
|
||||
def _destination_operand(operands: str) -> str | None:
|
||||
operands = operands.strip()
|
||||
if not operands:
|
||||
return None
|
||||
if "," not in operands:
|
||||
return operands
|
||||
return operands.rsplit(",", 1)[1].strip()
|
||||
|
||||
|
||||
def _immediate_source_value(operands: str) -> int | None:
|
||||
source = operands.rsplit(",", 1)[0].strip() if "," in operands else ""
|
||||
if not source.startswith("#"):
|
||||
return None
|
||||
try:
|
||||
return parse_int(source[1:])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _immediate_bit(operands: str) -> int | None:
|
||||
source = operands.rsplit(",", 1)[0].strip() if "," in operands else ""
|
||||
if not source.startswith("#"):
|
||||
return None
|
||||
try:
|
||||
bit = parse_int(source[1:])
|
||||
except ValueError:
|
||||
return None
|
||||
return bit if 0 <= bit <= 7 else None
|
||||
|
||||
|
||||
def _mnemonic_root(mnemonic: str) -> str:
|
||||
return mnemonic.split(".", 1)[0]
|
||||
|
||||
|
||||
def _mnemonic_size(mnemonic: str) -> str:
|
||||
if mnemonic.endswith(".W"):
|
||||
return "W"
|
||||
return "B"
|
||||
|
||||
|
||||
def _byte_for_target(value: int | None, width: int, index: int) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
shift = 8 * (width - index - 1)
|
||||
return (value >> shift) & 0xFF
|
||||
|
||||
|
||||
def _infer_channel(
|
||||
channel: str,
|
||||
registers: Mapping[str, int | None],
|
||||
clock_hz: int | None,
|
||||
) -> dict[str, object] | None:
|
||||
smr = registers.get("SMR")
|
||||
brr = registers.get("BRR")
|
||||
scr = registers.get("SCR")
|
||||
if smr is None or brr is None:
|
||||
return None
|
||||
|
||||
mode = "sync" if smr & 0x80 else "async"
|
||||
n = smr & 0x03
|
||||
denominator = _baud_denominator(mode, n, brr)
|
||||
clock_source = "unknown" if scr is None else ("external" if scr & 0x02 else "internal")
|
||||
inference: dict[str, object] = {
|
||||
"channel": channel,
|
||||
"mode": mode,
|
||||
"mode_summary": _mode_summary(smr),
|
||||
"smr": smr,
|
||||
"smr_hex": _hex8(smr),
|
||||
"brr": brr,
|
||||
"brr_hex": _hex8(brr),
|
||||
"scr": scr,
|
||||
"scr_hex": _hex8(scr) if scr is not None else None,
|
||||
"cks_n": n,
|
||||
"cks_divisor": 1 << (2 * n),
|
||||
"denominator": denominator,
|
||||
"clock_source": clock_source,
|
||||
"formula": FORMULAS[mode],
|
||||
}
|
||||
|
||||
if clock_hz is None:
|
||||
inference["baud_bps"] = None
|
||||
inference["confidence"] = "partial"
|
||||
inference["reason"] = "clock_hz_missing"
|
||||
inference["comment"] = (
|
||||
f"{channel} {_mode_summary(smr)} BRR N={brr} CKS n={n}; baud needs --clock-hz"
|
||||
)
|
||||
return inference
|
||||
|
||||
if clock_source == "external":
|
||||
inference["baud_bps"] = None
|
||||
inference["confidence"] = "partial"
|
||||
inference["reason"] = "external_clock_selected"
|
||||
inference["comment"] = (
|
||||
f"{channel} {_mode_summary(smr)} external clock selected; "
|
||||
f"BRR N={brr} CKS n={n}, internal baud not inferred"
|
||||
)
|
||||
return inference
|
||||
|
||||
baud = clock_hz / denominator
|
||||
inference["baud_bps"] = baud
|
||||
inference["confidence"] = "high" if clock_source == "internal" else "partial"
|
||||
inference["reason"] = None if clock_source == "internal" else "scr_clock_source_unknown"
|
||||
noun = "baud" if clock_source == "internal" else "baud generator"
|
||||
source_note = "; SCR clock source unknown" if clock_source == "unknown" else ""
|
||||
inference["comment"] = (
|
||||
f"{channel} {_mode_summary(smr)} {noun} {_format_bps(baud)} "
|
||||
f"(BRR N={brr}, CKS n={n}, clock={clock_hz} Hz{source_note})"
|
||||
)
|
||||
return inference
|
||||
|
||||
|
||||
def _configuration_key(channel: str, inference: Mapping[str, object], clock_hz: int | None) -> tuple[object, ...]:
|
||||
return (
|
||||
channel,
|
||||
inference.get("smr"),
|
||||
inference.get("brr"),
|
||||
inference.get("clock_source"),
|
||||
clock_hz,
|
||||
)
|
||||
|
||||
|
||||
def _baud_denominator(mode: str, n: int, brr: int) -> int:
|
||||
base = 8 if mode == "sync" else 64
|
||||
return base * (1 << (2 * n)) * (brr + 1)
|
||||
|
||||
|
||||
def _mode_summary(smr: int) -> str:
|
||||
if smr & 0x80:
|
||||
return "sync"
|
||||
char_len = "7-bit" if smr & 0x40 else "8-bit"
|
||||
if smr & 0x20:
|
||||
parity = "odd parity" if smr & 0x10 else "even parity"
|
||||
else:
|
||||
parity = "no parity"
|
||||
stop = "2 stop" if smr & 0x08 else "1 stop"
|
||||
return f"async {char_len} {parity} {stop}"
|
||||
|
||||
|
||||
def _format_bps(baud: float) -> str:
|
||||
rounded = round(baud)
|
||||
if abs(baud - rounded) < 0.001:
|
||||
return f"{rounded} bps"
|
||||
return f"{baud:.3f}".rstrip("0").rstrip(".") + " bps"
|
||||
|
||||
|
||||
def _hex8(value: int) -> str:
|
||||
return f"H'{value & 0xFF:02X}"
|
||||
@@ -43,10 +43,12 @@ IO_REGISTERS: dict[int, str] = {
|
||||
0xFE91: "FRT1_TCSR",
|
||||
0xFE92: "FRT1_FRC_H",
|
||||
0xFE93: "FRT1_FRC_L",
|
||||
0xFE94: "FRT1_OCRA_L",
|
||||
0xFE95: "FRT1_OCRB_L",
|
||||
0xFE96: "FRT1_ICR_H",
|
||||
0xFE97: "FRT1_ICR_L",
|
||||
0xFE94: "FRT1_OCRA_H",
|
||||
0xFE95: "FRT1_OCRA_L",
|
||||
0xFE96: "FRT1_OCRB_H",
|
||||
0xFE97: "FRT1_OCRB_L",
|
||||
0xFE98: "FRT1_ICR_H",
|
||||
0xFE99: "FRT1_ICR_L",
|
||||
0xFEA0: "FRT2_TCR",
|
||||
0xFEA1: "FRT2_TCSR",
|
||||
0xFEA2: "FRT2_FRC_H",
|
||||
@@ -129,6 +131,54 @@ IO_REGISTERS: dict[int, str] = {
|
||||
}
|
||||
|
||||
|
||||
IO_PRIORITY_FIELDS: dict[int, tuple[tuple[int, str], ...]] = {
|
||||
0xFF00: ((4, "irq0"), (0, "irq1")),
|
||||
0xFF01: ((4, "irq2/irq3"), (0, "irq4/irq5")),
|
||||
0xFF02: ((4, "FRT1"), (0, "FRT2")),
|
||||
0xFF03: ((4, "FRT3"), (0, "8-bit timer")),
|
||||
0xFF04: ((4, "SCI1"), (0, "SCI2")),
|
||||
0xFF05: ((4, "A/D"),),
|
||||
}
|
||||
|
||||
|
||||
IO_DTE_BITS: dict[int, dict[int, str]] = {
|
||||
0xFF08: {
|
||||
4: "irq0",
|
||||
0: "irq1",
|
||||
},
|
||||
0xFF09: {
|
||||
5: "irq3",
|
||||
4: "irq2",
|
||||
1: "irq5",
|
||||
0: "irq4",
|
||||
},
|
||||
0xFF0A: {
|
||||
6: "FRT1 OCIB",
|
||||
5: "FRT1 OCIA",
|
||||
4: "FRT1 ICI",
|
||||
2: "FRT2 OCIB",
|
||||
1: "FRT2 OCIA",
|
||||
0: "FRT2 ICI",
|
||||
},
|
||||
0xFF0B: {
|
||||
6: "FRT3 OCIB",
|
||||
5: "FRT3 OCIA",
|
||||
4: "FRT3 ICI",
|
||||
1: "8-bit timer CMIB",
|
||||
0: "8-bit timer CMIA",
|
||||
},
|
||||
0xFF0C: {
|
||||
6: "SCI1 TXI",
|
||||
5: "SCI1 RXI",
|
||||
2: "SCI2 TXI",
|
||||
1: "SCI2 RXI",
|
||||
},
|
||||
0xFF0D: {
|
||||
4: "A/D ADI",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
_FRT_TCR_BITS = {
|
||||
7: "ICIE",
|
||||
6: "OCIEB",
|
||||
|
||||
214
h8536/timing.py
Normal file
214
h8536/timing.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .formatting import h16, label_for
|
||||
from .model import Instruction
|
||||
|
||||
|
||||
def summarize_timing(
|
||||
instructions: dict[int, Instruction],
|
||||
labels: dict[int, str] | None = None,
|
||||
call_graph: dict[str, object] | None = None,
|
||||
) -> dict[str, list[dict[str, object]]]:
|
||||
labels = labels or {}
|
||||
addresses = sorted(instructions)
|
||||
if not addresses:
|
||||
return {"blocks": [], "loops": []}
|
||||
|
||||
starts = _block_starts(instructions, addresses, labels, call_graph)
|
||||
blocks = [_summarize_block(start, instructions, labels, starts) for start in sorted(starts)]
|
||||
loops = _summarize_loops(instructions, labels)
|
||||
return {
|
||||
"blocks": [block for block in blocks if block["instruction_count"]],
|
||||
"loops": loops,
|
||||
}
|
||||
|
||||
|
||||
def cycle_bounds(cycles: dict[str, object] | None) -> tuple[int, int] | None:
|
||||
if not cycles:
|
||||
return None
|
||||
if "cycles" in cycles:
|
||||
value = int(cycles["cycles"])
|
||||
return value, value
|
||||
if "cycles_min" in cycles and "cycles_max" in cycles:
|
||||
return int(cycles["cycles_min"]), int(cycles["cycles_max"])
|
||||
if "not_taken" in cycles and "taken" in cycles:
|
||||
values = [int(cycles["not_taken"]), int(cycles["taken"])]
|
||||
return min(values), max(values)
|
||||
if "trap_not_taken" in cycles and "trap_taken" in cycles:
|
||||
values = [int(cycles["trap_not_taken"]), int(cycles["trap_taken"])]
|
||||
return min(values), max(values)
|
||||
if "false" in cycles and "count_minus_1" in cycles and "taken" in cycles:
|
||||
values = [int(cycles["false"]), int(cycles["count_minus_1"]), int(cycles["taken"])]
|
||||
return min(values), max(values)
|
||||
return None
|
||||
|
||||
|
||||
def _block_starts(
|
||||
instructions: dict[int, Instruction],
|
||||
addresses: list[int],
|
||||
labels: dict[int, str],
|
||||
call_graph: dict[str, object] | None,
|
||||
) -> set[int]:
|
||||
address_set = set(addresses)
|
||||
starts = {addresses[0]}
|
||||
starts.update(address for address in labels if address in address_set)
|
||||
|
||||
for node in (call_graph or {}).get("nodes", []):
|
||||
start = int(node["start"])
|
||||
if start in address_set:
|
||||
starts.add(start)
|
||||
|
||||
for ins in instructions.values():
|
||||
starts.update(target for target in ins.targets if target in address_set)
|
||||
if ins.kind in {"branch", "jump", "return", "rte", "sleep", "invalid"}:
|
||||
next_address = ins.address + max(ins.size, 1)
|
||||
if next_address in address_set:
|
||||
starts.add(next_address)
|
||||
return starts
|
||||
|
||||
|
||||
def _summarize_block(
|
||||
start: int,
|
||||
instructions: dict[int, Instruction],
|
||||
labels: dict[int, str],
|
||||
starts: set[int],
|
||||
) -> dict[str, object]:
|
||||
addresses = []
|
||||
pc = start
|
||||
cycles_min = 0
|
||||
cycles_max = 0
|
||||
unknown_cycles = 0
|
||||
terminator: Instruction | None = None
|
||||
|
||||
while pc in instructions:
|
||||
if addresses and pc in starts:
|
||||
break
|
||||
ins = instructions[pc]
|
||||
addresses.append(pc)
|
||||
bounds = cycle_bounds(ins.cycles)
|
||||
if bounds is None:
|
||||
unknown_cycles += 1
|
||||
else:
|
||||
cycles_min += bounds[0]
|
||||
cycles_max += bounds[1]
|
||||
terminator = ins
|
||||
next_pc = pc + max(ins.size, 1)
|
||||
if ins.kind in {"branch", "jump", "return", "rte", "sleep", "invalid"}:
|
||||
break
|
||||
if next_pc in starts:
|
||||
break
|
||||
pc = next_pc
|
||||
|
||||
end = addresses[-1] if addresses else start
|
||||
return {
|
||||
"start": start,
|
||||
"end": end,
|
||||
"label": labels.get(start, label_for(start)),
|
||||
"instruction_count": len(addresses),
|
||||
"cycles_min": cycles_min,
|
||||
"cycles_max": cycles_max,
|
||||
"unknown_cycles": unknown_cycles,
|
||||
"terminator": terminator.text if terminator else "",
|
||||
"targets": list(terminator.targets if terminator else []),
|
||||
}
|
||||
|
||||
|
||||
def _summarize_loops(
|
||||
instructions: dict[int, Instruction],
|
||||
labels: dict[int, str],
|
||||
) -> list[dict[str, object]]:
|
||||
loops: list[dict[str, object]] = []
|
||||
for ins in sorted(instructions.values(), key=lambda item: item.address):
|
||||
for target in ins.targets:
|
||||
if target > ins.address or target not in instructions:
|
||||
continue
|
||||
body = [
|
||||
address
|
||||
for address in sorted(instructions)
|
||||
if target <= address <= ins.address
|
||||
]
|
||||
if not body:
|
||||
continue
|
||||
cycles_min = 0
|
||||
cycles_max = 0
|
||||
unknown_cycles = 0
|
||||
has_call = False
|
||||
for address in body:
|
||||
body_ins = instructions[address]
|
||||
has_call = has_call or body_ins.kind == "call"
|
||||
bounds = cycle_bounds(body_ins.cycles)
|
||||
if bounds is None:
|
||||
unknown_cycles += 1
|
||||
else:
|
||||
cycles_min += bounds[0]
|
||||
cycles_max += bounds[1]
|
||||
loops.append(
|
||||
{
|
||||
"start": target,
|
||||
"end": ins.address,
|
||||
"label": labels.get(target, label_for(target)),
|
||||
"back_edge": ins.address,
|
||||
"back_edge_text": ins.text,
|
||||
"instruction_count": len(body),
|
||||
"cycles_min": cycles_min,
|
||||
"cycles_max": cycles_max,
|
||||
"unknown_cycles": unknown_cycles,
|
||||
"has_call": has_call,
|
||||
"kind": _loop_kind(ins, has_call),
|
||||
},
|
||||
)
|
||||
return loops
|
||||
|
||||
|
||||
def _loop_kind(instruction: Instruction, has_call: bool) -> str:
|
||||
if instruction.mnemonic.startswith("SCB/"):
|
||||
return "counter_delay_loop" if not has_call else "counter_loop"
|
||||
if instruction.mnemonic in {"BRA", "BRN"}:
|
||||
return "unconditional_loop" if not has_call else "loop_with_call"
|
||||
return "delay_loop_candidate" if not has_call else "loop_with_call"
|
||||
|
||||
|
||||
def format_timing_summary(summary: dict[str, list[dict[str, object]]], *, max_items: int = 40) -> list[str]:
|
||||
lines: list[str] = []
|
||||
blocks = summary.get("blocks", [])
|
||||
loops = summary.get("loops", [])
|
||||
if not blocks and not loops:
|
||||
return lines
|
||||
|
||||
lines.append("; Timing Summary")
|
||||
if blocks:
|
||||
lines.append("; Straight-line blocks")
|
||||
for block in blocks[:max_items]:
|
||||
lines.append(
|
||||
"; block "
|
||||
f"{h16(int(block['start']))}-{h16(int(block['end']))} "
|
||||
f"{str(block['label']):<20} "
|
||||
f"ins={block['instruction_count']:<3} "
|
||||
f"cycles={_cycle_range(block)} "
|
||||
f"unknown={block['unknown_cycles']}",
|
||||
)
|
||||
if len(blocks) > max_items:
|
||||
lines.append(f"; ... {len(blocks) - max_items} more blocks")
|
||||
if loops:
|
||||
lines.append("; Backward-branch loop candidates")
|
||||
for loop in loops[:max_items]:
|
||||
lines.append(
|
||||
"; loop "
|
||||
f"{h16(int(loop['start']))}-{h16(int(loop['end']))} "
|
||||
f"{str(loop['label']):<20} "
|
||||
f"{loop['kind']:<22} "
|
||||
f"cycles/iteration={_cycle_range(loop)} "
|
||||
f"back_edge={h16(int(loop['back_edge']))}",
|
||||
)
|
||||
if len(loops) > max_items:
|
||||
lines.append(f"; ... {len(loops) - max_items} more loops")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def _cycle_range(item: dict[str, object]) -> str:
|
||||
minimum = int(item["cycles_min"])
|
||||
maximum = int(item["cycles_max"])
|
||||
if minimum == maximum:
|
||||
return str(minimum)
|
||||
return f"{minimum}-{maximum}"
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
from .dtc import DtcRegisterInfo, decode_dtc_register_info
|
||||
from .rom import Rom
|
||||
from .tables import VECTOR_NAMES_MIN
|
||||
|
||||
@@ -11,6 +12,7 @@ class DtcVectorEntry(TypedDict):
|
||||
source: str
|
||||
register_info_address: int
|
||||
target: int
|
||||
register_info: DtcRegisterInfo
|
||||
|
||||
|
||||
DTC_VECTOR_NAMES_MIN: dict[int, str] = {
|
||||
@@ -71,12 +73,13 @@ def read_vectors_max(rom: Rom) -> dict[int, tuple[str, int]]:
|
||||
return vectors
|
||||
|
||||
|
||||
def _dtc_entry(vector_address: int, source: str, target: int) -> DtcVectorEntry:
|
||||
def _dtc_entry(rom: Rom, vector_address: int, source: str, target: int) -> DtcVectorEntry:
|
||||
return {
|
||||
"vector_address": vector_address,
|
||||
"source": source,
|
||||
"register_info_address": target,
|
||||
"target": target,
|
||||
"register_info": decode_dtc_register_info(rom, target),
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +91,7 @@ def read_dtc_vectors_min(rom: Rom) -> dict[int, DtcVectorEntry]:
|
||||
target = rom.u16(addr)
|
||||
if target in (0x0000, 0xFFFF):
|
||||
continue
|
||||
vectors[addr] = _dtc_entry(addr, source, target)
|
||||
vectors[addr] = _dtc_entry(rom, addr, source, target)
|
||||
return vectors
|
||||
|
||||
|
||||
@@ -100,5 +103,5 @@ def read_dtc_vectors_max(rom: Rom) -> dict[int, DtcVectorEntry]:
|
||||
target = rom.u16(addr + 2)
|
||||
if target in (0x0000, 0xFFFF):
|
||||
continue
|
||||
vectors[addr] = _dtc_entry(addr, source, target)
|
||||
vectors[addr] = _dtc_entry(rom, addr, source, target)
|
||||
return vectors
|
||||
|
||||
Reference in New Issue
Block a user