Compare commits
2 Commits
5ad90ade49
...
62d1c3c876
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62d1c3c876 | ||
|
|
4649cf530f |
@@ -9,7 +9,7 @@ python h8536_decompiler.py ROM\M27C512@DIP28_1.BIN --out build\rom_decompiled.as
|
||||
If you are using the repo-local venv:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe h8536_decompiler.py --out build\rom_decompiled.asm --json build\rom_decompiled.json --callgraph-dot build\callgraph.dot
|
||||
.\.venv\Scripts\python.exe h8536_decompiler.py --out build\rom_decompiled.asm --json build\rom_decompiled.json --cycles --callgraph-dot build\callgraph.dot
|
||||
```
|
||||
|
||||
## What It Does
|
||||
@@ -25,6 +25,7 @@ If you are using the repo-local venv:
|
||||
- Parses the DTC vector table described by the manual.
|
||||
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
|
||||
- Emits function summaries and a direct-call graph in JSON, with optional Graphviz DOT output.
|
||||
- Adds Appendix A cycle estimates to JSON and can append them to ASM comments.
|
||||
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
|
||||
|
||||
The generated listing is written to:
|
||||
@@ -50,6 +51,7 @@ python h8536_decompiler.py --help
|
||||
- `--linear`: linear-sweep the selected range instead of tracing from vectors.
|
||||
- `--start H'1000 --end H'D100`: constrain the decode range.
|
||||
- `--br H'FE`: resolve short absolute `@aa:8` operands through a known base-register value.
|
||||
- `--cycles`: append Appendix A cycle estimates to assembly comments.
|
||||
- `--callgraph-dot build\callgraph.dot`: write a Graphviz DOT call graph.
|
||||
|
||||
## Code Layout
|
||||
@@ -62,5 +64,6 @@ python h8536_decompiler.py --help
|
||||
- `h8536/analysis.py`: recursive tracing, linear sweep, labels, function grouping, and call graph analysis.
|
||||
- `h8536/data_analysis.py`: unreached string and pointer-table candidate scans.
|
||||
- `h8536/memory.py`: manual-derived memory-region tagging.
|
||||
- `h8536/cycles.py`: Appendix A cycle estimate tables.
|
||||
- `h8536/render.py`: assembly and JSON output.
|
||||
- `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3420
build/rom_pseudocode.c
Normal file
3420
build/rom_pseudocode.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from .analysis import build_call_graph, collect_labels, linear_sweep, trace
|
||||
from .cycles import annotate_cycles
|
||||
from .data_analysis import analyze_unreached_data
|
||||
from .decoder import H8536Decoder
|
||||
from .formatting import parse_int
|
||||
@@ -31,6 +32,7 @@ def main() -> int:
|
||||
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("--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("--callgraph-dot", type=Path, default=None, help="optional Graphviz DOT call graph output")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -60,6 +62,7 @@ def main() -> int:
|
||||
else:
|
||||
instructions = trace(decoder, starts, args.start, end)
|
||||
labels.update(collect_labels(instructions.values(), vectors))
|
||||
annotate_cycles(instructions, args.mode)
|
||||
data_candidates = analyze_unreached_data(rom, instructions, args.start, end)
|
||||
call_graph = build_call_graph(instructions, vectors, labels)
|
||||
|
||||
@@ -75,6 +78,7 @@ def main() -> int:
|
||||
traced=not args.linear,
|
||||
dtc_vectors=dtc_vectors,
|
||||
data_candidates=data_candidates,
|
||||
show_cycles=args.cycles,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
376
h8536/cycles.py
Normal file
376
h8536/cycles.py
Normal file
@@ -0,0 +1,376 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .decoder import is_ea_byte
|
||||
from .model import Instruction
|
||||
|
||||
|
||||
CYCLE_SOURCE = "manual Appendix A.4, tables A-7/A-8"
|
||||
CYCLE_ASSUMPTION = "on-chip instruction fetch/operand access, no external wait states"
|
||||
|
||||
|
||||
def annotate_cycles(instructions: dict[int, Instruction], mode: str) -> None:
|
||||
for instruction in instructions.values():
|
||||
instruction.cycles = estimate_cycles(instruction, mode)
|
||||
|
||||
|
||||
def cycle_comment(cycles: dict[str, object] | None) -> str:
|
||||
if not cycles:
|
||||
return ""
|
||||
if "cycles" in cycles:
|
||||
return f"cycles={cycles['cycles']}"
|
||||
if "cycles_min" in cycles and "cycles_max" in cycles:
|
||||
return f"cycles={cycles['cycles_min']}-{cycles['cycles_max']}"
|
||||
if "not_taken" in cycles and "taken" in cycles:
|
||||
return f"cycles={cycles['not_taken']}/{cycles['taken']} nt/t"
|
||||
if "trap_not_taken" in cycles:
|
||||
return f"cycles={cycles['trap_not_taken']}/{cycles['trap_taken']} no-trap/trap"
|
||||
if "false" in cycles:
|
||||
return f"cycles={cycles['false']}/{cycles['count_minus_1']}/{cycles['taken']} false/-1/t"
|
||||
return ""
|
||||
|
||||
|
||||
def estimate_cycles(instruction: Instruction, mode: str) -> dict[str, object] | None:
|
||||
if not instruction.valid:
|
||||
return None
|
||||
|
||||
if instruction.raw and is_ea_byte(instruction.raw[0]):
|
||||
base = _estimate_general_cycles(instruction, mode)
|
||||
else:
|
||||
base = _estimate_direct_cycles(instruction, mode)
|
||||
if base is None:
|
||||
return None
|
||||
|
||||
base.setdefault("source", CYCLE_SOURCE)
|
||||
base.setdefault("assumption", CYCLE_ASSUMPTION)
|
||||
return base
|
||||
|
||||
|
||||
def _info(
|
||||
cycles: int,
|
||||
base_cycles: int | None = None,
|
||||
adjustment: int = 0,
|
||||
stack_adjustment: int = 0,
|
||||
note: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
info: dict[str, object] = {"cycles": cycles}
|
||||
if base_cycles is not None:
|
||||
info["base_cycles"] = base_cycles
|
||||
if adjustment:
|
||||
info["alignment_adjustment"] = adjustment
|
||||
if stack_adjustment:
|
||||
info["stack_adjustment"] = stack_adjustment
|
||||
if note:
|
||||
info["note"] = note
|
||||
return info
|
||||
|
||||
|
||||
def _branch_info(not_taken: int, taken: int, instruction: Instruction) -> dict[str, object]:
|
||||
adjusted_taken = taken + _branch_adjust(instruction.address)
|
||||
info: dict[str, object] = {
|
||||
"not_taken": not_taken,
|
||||
"taken": adjusted_taken,
|
||||
"base_taken": taken,
|
||||
}
|
||||
if adjusted_taken != taken:
|
||||
info["alignment_adjustment_taken"] = adjusted_taken - taken
|
||||
return info
|
||||
|
||||
|
||||
def _mode_min_max(mode: str, minimum: int, maximum: int) -> int:
|
||||
return maximum if mode == "max" else minimum
|
||||
|
||||
|
||||
def _branch_adjust(address: int) -> int:
|
||||
# Table A-8(a): branch/system transfer adjustment is 0 at even starts, 1 at odd starts.
|
||||
return address & 1
|
||||
|
||||
|
||||
def _ea_mode(raw: bytes) -> tuple[str, str] | None:
|
||||
if not raw:
|
||||
return None
|
||||
b = raw[0]
|
||||
if 0xA0 <= b <= 0xAF:
|
||||
return "reg", "W" if b & 0x08 else "B"
|
||||
if 0xB0 <= b <= 0xBF:
|
||||
return "predec", "W" if b & 0x08 else "B"
|
||||
if 0xC0 <= b <= 0xCF:
|
||||
return "postinc", "W" if b & 0x08 else "B"
|
||||
if 0xD0 <= b <= 0xDF:
|
||||
return "indirect", "W" if b & 0x08 else "B"
|
||||
if 0xE0 <= b <= 0xEF:
|
||||
return "disp8", "W" if b & 0x08 else "B"
|
||||
if 0xF0 <= b <= 0xFF:
|
||||
return "disp16", "W" if b & 0x08 else "B"
|
||||
if b == 0x04:
|
||||
return "imm", "B"
|
||||
if b == 0x0C:
|
||||
return "imm", "W"
|
||||
if b == 0x05:
|
||||
return "abs8", "B"
|
||||
if b == 0x0D:
|
||||
return "abs8", "W"
|
||||
if b == 0x15:
|
||||
return "abs16", "B"
|
||||
if b == 0x1D:
|
||||
return "abs16", "W"
|
||||
return None
|
||||
|
||||
|
||||
def _a8_adjust(address: int, ea_mode: str, size: str, mov_imm_to_ea: bool = False) -> int:
|
||||
# Table A-8(b), simplified for decoded modes used by this tool.
|
||||
if mov_imm_to_ea:
|
||||
if size == "B":
|
||||
return 1
|
||||
even = {
|
||||
"reg": 2,
|
||||
"predec": 0,
|
||||
"postinc": 2,
|
||||
"indirect": 2,
|
||||
"disp8": 2,
|
||||
"disp16": 0,
|
||||
"abs8": 2,
|
||||
"abs16": 2,
|
||||
}
|
||||
odd = {
|
||||
"reg": 0,
|
||||
"predec": 2,
|
||||
"postinc": 0,
|
||||
"indirect": 0,
|
||||
"disp8": 0,
|
||||
"disp16": 2,
|
||||
"abs8": 0,
|
||||
"abs16": 0,
|
||||
}
|
||||
return (odd if address & 1 else even).get(ea_mode, 0)
|
||||
|
||||
even_default = {
|
||||
"reg": 0,
|
||||
"predec": 1,
|
||||
"postinc": 0,
|
||||
"indirect": 1,
|
||||
"disp8": 1,
|
||||
"disp16": 1,
|
||||
"abs8": 0,
|
||||
"abs16": 1,
|
||||
"imm": 0,
|
||||
}
|
||||
odd_default = {
|
||||
"reg": 0,
|
||||
"predec": 0,
|
||||
"postinc": 1,
|
||||
"indirect": 0,
|
||||
"disp8": 0,
|
||||
"disp16": 0,
|
||||
"abs8": 1,
|
||||
"abs16": 0,
|
||||
"imm": 0,
|
||||
}
|
||||
return (odd_default if address & 1 else even_default).get(ea_mode, 0)
|
||||
|
||||
|
||||
def _memory_read_base(ea_mode: str, size: str) -> int:
|
||||
if ea_mode == "reg":
|
||||
return 2 if size == "B" else 3
|
||||
if ea_mode == "imm":
|
||||
return 3 if size == "B" else 4
|
||||
if ea_mode in {"disp8", "disp16", "abs16"}:
|
||||
return 6
|
||||
return 5
|
||||
|
||||
|
||||
def _memory_write_base(ea_mode: str, size: str) -> int:
|
||||
if ea_mode == "reg":
|
||||
return 2 if size == "B" else 3
|
||||
if ea_mode in {"disp8", "disp16", "abs16"}:
|
||||
return 6
|
||||
return 5
|
||||
|
||||
|
||||
def _read_modify_write_base(ea_mode: str, size: str) -> int:
|
||||
if ea_mode == "reg":
|
||||
return 2 if size == "B" else 3
|
||||
if ea_mode in {"disp8", "disp16", "abs16"}:
|
||||
return 8
|
||||
return 7
|
||||
|
||||
|
||||
def _imm_to_ea_base(ea_mode: str, size: str) -> int:
|
||||
if ea_mode == "reg":
|
||||
return 2 if size == "B" else 3
|
||||
if ea_mode in {"disp8", "disp16", "abs16"}:
|
||||
return 8 if size == "B" else 9
|
||||
return 7 if size == "B" else 8
|
||||
|
||||
|
||||
def _estimate_general_cycles(instruction: Instruction, mode: str) -> dict[str, object] | None:
|
||||
parsed = _ea_mode(instruction.raw)
|
||||
if parsed is None or len(instruction.raw) < 2:
|
||||
return None
|
||||
ea_mode, ea_size = parsed
|
||||
mnemonic = instruction.mnemonic
|
||||
op = instruction.raw[1 + (1 if ea_mode in {"imm", "abs8"} else 2 if ea_mode == "abs16" else 0)]
|
||||
if ea_mode in {"disp8"}:
|
||||
op = instruction.raw[2]
|
||||
elif ea_mode == "disp16":
|
||||
op = instruction.raw[3]
|
||||
|
||||
mov_imm_to_ea = mnemonic.startswith("MOV:G") and instruction.operands.startswith("#")
|
||||
adjustment = _a8_adjust(instruction.address, ea_mode, ea_size, mov_imm_to_ea)
|
||||
|
||||
if mnemonic.startswith(("ADD:G", "ADDS", "SUB", "SUBS", "ADDX", "SUBX", "AND.", "OR.", "XOR.", "CMP:G")):
|
||||
base = _memory_read_base(ea_mode, ea_size)
|
||||
elif mnemonic.startswith("MOV:G"):
|
||||
base = _imm_to_ea_base(ea_mode, ea_size) if mov_imm_to_ea else _memory_write_base(ea_mode, ea_size)
|
||||
elif mnemonic.startswith(("BTST", "TST")):
|
||||
base = _memory_read_base(ea_mode, ea_size)
|
||||
elif mnemonic.startswith(("BSET", "BCLR", "BNOT", "CLR", "NEG", "NOT", "SHAL", "SHAR", "SHLL", "SHLR", "ROTL", "ROTR", "ROTXL", "ROTXR", "TAS")):
|
||||
base = _read_modify_write_base(ea_mode, ea_size)
|
||||
elif mnemonic.startswith("ADD:Q"):
|
||||
base = 4 if ea_mode == "reg" else _read_modify_write_base(ea_mode, ea_size)
|
||||
elif mnemonic.startswith("MULXU.B"):
|
||||
base = 18 if ea_mode == "reg" else 19
|
||||
elif mnemonic.startswith("MULXU.W"):
|
||||
base = 25
|
||||
elif mnemonic.startswith("DIVXU.B"):
|
||||
base = 21 if ea_mode == "reg" else 23
|
||||
elif mnemonic.startswith("DIVXU.W"):
|
||||
base = 28 if ea_mode == "reg" else 29
|
||||
elif mnemonic.startswith(("LDC.B", "STC.B")):
|
||||
base = 4 if ea_mode in {"reg", "imm"} else 6 if ea_mode in {"abs8", "indirect", "predec", "postinc"} else 7
|
||||
elif mnemonic.startswith(("LDC.W", "STC.W")):
|
||||
base = 6 if ea_mode in {"reg", "imm"} else 7 if ea_mode in {"abs8", "indirect", "predec", "postinc"} else 8
|
||||
elif mnemonic.startswith(("ORC", "ANDC", "XORC")):
|
||||
base = 4
|
||||
adjustment = 0
|
||||
elif mnemonic.startswith(("EXTS", "EXTU", "SWAP")):
|
||||
base = 3
|
||||
adjustment = 0
|
||||
elif mnemonic.startswith(("MOVFPE", "MOVTPE")):
|
||||
cycles = _mode_min_max(mode, 13, 20)
|
||||
return _info(cycles, note="E-clock peripheral transfer")
|
||||
else:
|
||||
return None
|
||||
|
||||
return _info(base + adjustment, base, adjustment)
|
||||
|
||||
|
||||
def _estimate_direct_cycles(instruction: Instruction, mode: str) -> dict[str, object] | None:
|
||||
raw = instruction.raw
|
||||
mnemonic = instruction.mnemonic
|
||||
if not raw:
|
||||
return None
|
||||
op = raw[0]
|
||||
|
||||
if mnemonic.startswith("B") and 0x20 <= op <= 0x3F:
|
||||
info = _branch_info(3, 7, instruction)
|
||||
if mnemonic == "BRA":
|
||||
info["cycles"] = info["taken"]
|
||||
elif mnemonic == "BRN":
|
||||
info["cycles"] = info["not_taken"]
|
||||
return info
|
||||
if mnemonic.startswith("SCB/"):
|
||||
taken = 8 + _branch_adjust(instruction.address)
|
||||
return {
|
||||
"false": 3,
|
||||
"count_minus_1": 4,
|
||||
"taken": taken,
|
||||
"base_taken": 8,
|
||||
}
|
||||
if mnemonic == "BSR":
|
||||
base = 9
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "PC word push to stack")
|
||||
if mnemonic == "JMP":
|
||||
if op == 0x10:
|
||||
base = 7
|
||||
elif len(raw) >= 2 and raw[0] == 0x11 and 0xD0 <= raw[1] <= 0xD7:
|
||||
base = 6
|
||||
elif len(raw) >= 2 and raw[0] == 0x11 and 0xE0 <= raw[1] <= 0xE7:
|
||||
base = 7
|
||||
elif len(raw) >= 2 and raw[0] == 0x11 and 0xF0 <= raw[1] <= 0xF7:
|
||||
base = 8
|
||||
else:
|
||||
return None
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
return _info(base + adjust, base, adjust)
|
||||
if mnemonic == "JSR":
|
||||
if op == 0x18:
|
||||
base = 9
|
||||
elif len(raw) >= 2 and raw[0] == 0x11 and 0xD8 <= raw[1] <= 0xDF:
|
||||
base = 9
|
||||
elif len(raw) >= 2 and raw[0] == 0x11 and 0xE8 <= raw[1] <= 0xEF:
|
||||
base = 9
|
||||
elif len(raw) >= 2 and raw[0] == 0x11 and 0xF8 <= raw[1] <= 0xFF:
|
||||
base = 10
|
||||
else:
|
||||
return None
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "PC word push to stack")
|
||||
if mnemonic == "PJMP":
|
||||
base = 9 if op == 0x13 else 8
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
return _info(base + adjust, base, adjust)
|
||||
if mnemonic == "PJSR":
|
||||
base = 15 if op == 0x03 else 13
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 6 if mode == "max" else 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "return address push to stack")
|
||||
if mnemonic == "TRAPA":
|
||||
return _info(_mode_min_max(mode, 17, 22))
|
||||
if mnemonic == "TRAP/VS":
|
||||
return {
|
||||
"trap_not_taken": 3,
|
||||
"trap_taken": _mode_min_max(mode, 18, 23) + _branch_adjust(instruction.address),
|
||||
"base_trap_taken": _mode_min_max(mode, 18, 23),
|
||||
}
|
||||
if mnemonic == "RTE":
|
||||
base = _mode_min_max(mode, 13, 15)
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
return _info(base + adjust, base, adjust)
|
||||
if mnemonic == "RTS":
|
||||
base = 8
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "PC word pop from stack")
|
||||
if mnemonic == "RTD":
|
||||
base = 9
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "PC word pop from stack")
|
||||
if mnemonic == "PRTS":
|
||||
base = 12
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 6 if mode == "max" else 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "return address pop from stack")
|
||||
if mnemonic == "PRTD":
|
||||
base = 13
|
||||
adjust = _branch_adjust(instruction.address)
|
||||
stack = 6 if mode == "max" else 4
|
||||
return _info(base + stack + adjust, base, adjust, stack, "return address pop from stack")
|
||||
if mnemonic == "UNLK":
|
||||
return _info(5)
|
||||
if mnemonic == "LINK":
|
||||
return _info(6 if len(raw) == 2 else 7)
|
||||
if mnemonic == "LDM.W":
|
||||
n = raw[1].bit_count()
|
||||
cycles = 6 + 4 * n
|
||||
return _info(cycles, note=f"6+4n, n={n}")
|
||||
if mnemonic == "STM.W":
|
||||
n = raw[1].bit_count()
|
||||
cycles = 6 + 3 * n
|
||||
return _info(cycles, note=f"6+3n, n={n}")
|
||||
if mnemonic in {"NOP", "SLEEP"}:
|
||||
return _info(2)
|
||||
if mnemonic.startswith("CMP:E"):
|
||||
return _info(2)
|
||||
if mnemonic.startswith("CMP:I"):
|
||||
return _info(3)
|
||||
if mnemonic.startswith("MOV:E"):
|
||||
return _info(2)
|
||||
if mnemonic.startswith("MOV:I"):
|
||||
return _info(3)
|
||||
if mnemonic.startswith(("MOV:L", "MOV:S", "MOV:F")):
|
||||
return _info(5)
|
||||
return None
|
||||
@@ -28,6 +28,7 @@ class Instruction:
|
||||
references: list[int] = field(default_factory=list)
|
||||
writes_br: bool = False
|
||||
br_value: int | None = None
|
||||
cycles: dict[str, object] | None = None
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .cycles import cycle_comment
|
||||
from .formatting import h16, label_for
|
||||
from .memory import MEMORY_REGIONS, region_for
|
||||
from .model import Instruction
|
||||
@@ -30,6 +31,7 @@ def format_listing(
|
||||
traced: bool,
|
||||
dtc_vectors: dict[int, DtcVectorEntry] | None = None,
|
||||
data_candidates: dict[str, list[dict[str, object]]] | None = None,
|
||||
show_cycles: bool = False,
|
||||
) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("; H8/536 ROM disassembly")
|
||||
@@ -43,6 +45,8 @@ 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.")
|
||||
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("")
|
||||
lines.append("; Memory Map")
|
||||
for region in MEMORY_REGIONS:
|
||||
@@ -84,7 +88,15 @@ def format_listing(
|
||||
lines.append(f"{labels[address]}:")
|
||||
raw = " ".join(f"{byte:02X}" for byte in ins.raw)
|
||||
padded_raw = raw.ljust(14)
|
||||
comment_parts = [part for part in (ins.comment, _reference_comment(ins) if not ins.comment else "") if part]
|
||||
comment_parts = [
|
||||
part
|
||||
for part in (
|
||||
ins.comment,
|
||||
_reference_comment(ins) if not ins.comment else "",
|
||||
cycle_comment(ins.cycles) if show_cycles else "",
|
||||
)
|
||||
if part
|
||||
]
|
||||
comment = f" ; {'; '.join(comment_parts)}" if comment_parts else ""
|
||||
lines.append(f"{address:04X}: {padded_raw} {ins.text}{comment}")
|
||||
lines.append("")
|
||||
@@ -128,6 +140,7 @@ def write_json(
|
||||
"operands": ins.operands,
|
||||
"kind": ins.kind,
|
||||
"targets": ins.targets,
|
||||
"cycles": ins.cycles,
|
||||
"references": [
|
||||
{
|
||||
"address": address,
|
||||
|
||||
48
tests/test_cycles_manual_examples.py
Normal file
48
tests/test_cycles_manual_examples.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
|
||||
from h8536.cycles import estimate_cycles
|
||||
from h8536.decoder import H8536Decoder
|
||||
from h8536.rom import Rom
|
||||
|
||||
|
||||
def decode_at(data: list[int], address: int = 0):
|
||||
return H8536Decoder(Rom(bytes(data), base=address)).decode(address)
|
||||
|
||||
|
||||
class ManualCycleExamplesTest(unittest.TestCase):
|
||||
def test_add_word_register_indirect_matches_manual_even_and_odd_examples(self):
|
||||
even = decode_at([0xD8, 0x21], 0x0100)
|
||||
odd = decode_at([0xD8, 0x21], 0x0101)
|
||||
|
||||
self.assertEqual(even.text, "ADD:G.W @R0, R1")
|
||||
self.assertEqual(estimate_cycles(even, "min")["cycles"], 6)
|
||||
self.assertEqual(estimate_cycles(even, "min")["base_cycles"], 5)
|
||||
self.assertEqual(estimate_cycles(even, "min")["alignment_adjustment"], 1)
|
||||
|
||||
self.assertEqual(odd.text, "ADD:G.W @R0, R1")
|
||||
self.assertEqual(estimate_cycles(odd, "min")["cycles"], 5)
|
||||
self.assertEqual(estimate_cycles(odd, "min")["base_cycles"], 5)
|
||||
|
||||
def test_jsr_register_indirect_matches_manual_stack_adjusted_example(self):
|
||||
even = decode_at([0x11, 0xD8], 0xFC00)
|
||||
odd = decode_at([0x11, 0xD8], 0xFC01)
|
||||
|
||||
self.assertEqual(even.text, "JSR @R0")
|
||||
self.assertEqual(estimate_cycles(even, "min")["cycles"], 13)
|
||||
self.assertEqual(estimate_cycles(even, "min")["base_cycles"], 9)
|
||||
self.assertEqual(estimate_cycles(even, "min")["stack_adjustment"], 4)
|
||||
|
||||
self.assertEqual(odd.text, "JSR @R0")
|
||||
self.assertEqual(estimate_cycles(odd, "min")["cycles"], 14)
|
||||
self.assertEqual(estimate_cycles(odd, "min")["alignment_adjustment"], 1)
|
||||
|
||||
def test_conditional_branch_keeps_taken_and_not_taken_counts(self):
|
||||
instruction = decode_at([0x26, 0x02], 0x0000)
|
||||
cycles = estimate_cycles(instruction, "min")
|
||||
|
||||
self.assertEqual(cycles["not_taken"], 3)
|
||||
self.assertEqual(cycles["taken"], 7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user