1
0

DTC and SCI improvements

This commit is contained in:
Aiden
2026-05-25 14:22:32 +10:00
parent 62d1c3c876
commit 80819448cf
21 changed files with 13823 additions and 86 deletions

103
tests/test_dtc.py Normal file
View File

@@ -0,0 +1,103 @@
import json
import tempfile
import unittest
from pathlib import Path
from h8536.render import format_listing, write_json
from h8536.rom import Rom
from h8536.vectors import read_dtc_vectors_min
def _manual_sci1_rxi_rom() -> Rom:
data = bytearray([0xFF] * 0xFB88)
data[0x00A2:0x00A4] = bytes([0xFB, 0x80])
data[0xFB80:0xFB88] = bytes(
[
0x20,
0x00, # DTMR: byte transfer, destination increments
0xFE,
0xDD, # DTSR: SCI1_RDR
0xFC,
0x00, # DTDR: receive buffer
0x00,
0x80, # DTCR: 128 transfers
],
)
return Rom(bytes(data))
class DtcDecodeTest(unittest.TestCase):
def test_decodes_manual_sci1_receive_register_information(self):
vectors = read_dtc_vectors_min(_manual_sci1_rxi_rom())
entry = vectors[0x00A2]
info = entry["register_info"]
self.assertTrue(info["valid"])
self.assertEqual(entry["source"], "sci1_rxi")
self.assertEqual(info["dtmr"], 0x2000)
self.assertEqual(info["mode"]["size"], "byte")
self.assertFalse(info["mode"]["source_increment"])
self.assertTrue(info["mode"]["destination_increment"])
self.assertEqual(info["source"]["address"], 0xFEDD)
self.assertEqual(info["source"]["name"], "SCI1_RDR")
self.assertEqual(info["destination"]["address"], 0xFC00)
self.assertEqual(info["destination"]["region"], "on_chip_ram")
self.assertEqual(info["count"]["transfers"], 128)
self.assertEqual(info["count"]["bytes"], 128)
def test_invalid_register_information_pointer_is_reported_conservatively(self):
data = bytearray([0xFF] * 0x0200)
data[0x00A2:0x00A4] = bytes([0xFB, 0x80])
vectors = read_dtc_vectors_min(Rom(bytes(data)))
info = vectors[0x00A2]["register_info"]
self.assertFalse(info["valid"])
self.assertIn("outside ROM image", info["error"])
def test_zero_count_represents_65536_transfers(self):
data = bytearray([0xFF] * 0x0200)
data[0x00C0:0x00C2] = bytes([0x01, 0x00])
data[0x0100:0x0108] = bytes([0xC0, 0x00, 0x01, 0x20, 0xFE, 0x80, 0x00, 0x00])
info = read_dtc_vectors_min(Rom(bytes(data)))[0x00C0]["register_info"]
self.assertEqual(info["mode"]["size"], "word")
self.assertEqual(info["mode"]["source_increment_step"], 2)
self.assertEqual(info["count"]["transfers"], 65536)
self.assertEqual(info["count"]["bytes"], 131072)
self.assertTrue(info["count"]["zero_means_65536"])
def test_listing_and_json_include_decoded_register_information(self):
rom = _manual_sci1_rxi_rom()
dtc_vectors = read_dtc_vectors_min(rom)
listing = format_listing(
Path("rom.bin"),
rom,
{},
{},
{},
"min",
traced=True,
dtc_vectors=dtc_vectors,
)
self.assertIn("; DTC Register Information", listing)
self.assertIn("sci1_rxi", listing)
self.assertIn("byte x128", listing)
self.assertIn("SCI1_RDR (H'FEDD) -> H'FC00", listing)
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "out.json"
write_json(path, {}, {}, {}, dtc_vectors=dtc_vectors)
payload = json.loads(path.read_text(encoding="utf-8"))
json_info = payload["dtc_vectors"][0]["register_info"]
self.assertEqual(json_info["mode"]["size"], "byte")
self.assertEqual(json_info["source"]["name"], "SCI1_RDR")
self.assertEqual(json_info["count"]["transfers"], 128)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,50 @@
import unittest
from h8536.decoder import H8536Decoder
from h8536.rom import Rom
def decode(data: list[int]):
return H8536Decoder(Rom(bytes(data), base=0)).decode(0)
def mov_b_immediate_to_abs16(address: int, value: int):
return decode([0x15, (address >> 8) & 0xFF, address & 0xFF, 0x06, value])
def bit_op_abs16(address: int, op: int):
return decode([0x15, (address >> 8) & 0xFF, address & 0xFF, op])
class InterruptAnnotationTest(unittest.TestCase):
def test_ipra_write_decodes_irq_priority_levels(self):
instruction = mov_b_immediate_to_abs16(0xFF00, 0x75)
self.assertEqual(instruction.text, "MOV:G.B #H'75, @IPRA")
self.assertEqual(instruction.comment, "IPRA = H'75 (irq0 priority=7; irq1 priority=5)")
def test_iprf_write_decodes_ad_priority_and_reserved_bits(self):
instruction = mov_b_immediate_to_abs16(0xFF05, 0xF8)
self.assertEqual(instruction.text, "MOV:G.B #H'F8, @IPRF")
self.assertEqual(instruction.comment, "IPRF = H'F8 (A/D priority=7; reserved bits 7, 3 should be 0)")
def test_dtee_write_decodes_dtc_routing_by_interrupt_source(self):
instruction = mov_b_immediate_to_abs16(0xFF0C, 0x24)
self.assertEqual(instruction.text, "MOV:G.B #H'24, @DTEE")
self.assertEqual(
instruction.comment,
"DTEE = H'24 (SCI1 TXI CPU interrupt; SCI1 RXI DTC enabled; "
"SCI2 TXI DTC enabled; SCI2 RXI CPU interrupt)",
)
def test_dtea_bit_set_names_dtc_enable_source(self):
instruction = bit_op_abs16(0xFF08, 0xC4)
self.assertEqual(instruction.text, "BSET.B #4, @DTEA")
self.assertEqual(instruction.comment, "set irq0 DTC enable (bit 4) of DTEA")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,90 @@
import json
import tempfile
import unittest
from pathlib import Path
from h8536.model import Instruction
from h8536.peripheral_access import analyze_peripheral_access, peripheral_comment_for_instruction
from h8536.render import write_json
from h8536.tables import IO_REGISTERS
class PeripheralAccessTest(unittest.TestCase):
def test_frt1_manual_register_map_has_full_16_bit_pairs(self):
self.assertEqual(IO_REGISTERS[0xFE94], "FRT1_OCRA_H")
self.assertEqual(IO_REGISTERS[0xFE95], "FRT1_OCRA_L")
self.assertEqual(IO_REGISTERS[0xFE98], "FRT1_ICR_H")
def test_frt_word_access_is_marked_as_temp_safe(self):
instructions = {
0x1000: Instruction(
0x1000,
bytes.fromhex("1DFEA2051234"),
"MOV:G.W",
"#H'1234, @FRT2_FRC_H",
references=[0xFEA2],
),
}
analysis = analyze_peripheral_access(instructions)
self.assertIn("TEMP byte-order hazard avoided", peripheral_comment_for_instruction(analysis, 0x1000))
self.assertEqual(analysis["warnings"], [])
def test_low_byte_read_before_high_byte_warns(self):
instructions = {
0x1000: Instruction(
0x1000,
bytes.fromhex("15FEE105"),
"MOV:G.B",
"@ADDRA_L, R0",
references=[0xFEE1],
),
}
analysis = analyze_peripheral_access(instructions)
self.assertEqual(len(analysis["warnings"]), 1)
self.assertIn("low byte read before matching high byte", analysis["warnings"][0]["message"])
self.assertIn("check TEMP ordering", peripheral_comment_for_instruction(analysis, 0x1000))
def test_ad_upper_byte_read_alone_is_allowed_for_8_bit_accuracy(self):
instructions = {
0x1000: Instruction(
0x1000,
bytes.fromhex("15FEE005"),
"MOV:G.B",
"@ADDRA_H, R0",
references=[0xFEE0],
),
}
analysis = analyze_peripheral_access(instructions)
self.assertEqual(analysis["warnings"], [])
self.assertIn("valid 8-bit result", peripheral_comment_for_instruction(analysis, 0x1000))
def test_json_includes_top_level_warnings_and_instruction_metadata(self):
instructions = {
0x1000: Instruction(
0x1000,
b"",
"MOV:G.B",
"@ADDRA_L, R0",
references=[0xFEE1],
),
}
analysis = analyze_peripheral_access(instructions)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "out.json"
write_json(path, instructions, {}, {}, peripheral_access=analysis)
payload = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(payload["peripheral_access"]["warnings"][0]["register"], "ADDRA")
instruction = payload["instructions"][0]
self.assertEqual(instruction["peripheral_access"][0]["register"], "ADDRA")
if __name__ == "__main__":
unittest.main()

126
tests/test_pseudocode.py Normal file
View File

@@ -0,0 +1,126 @@
import unittest
from h8536.pseudocode import PseudocodeOptions, generate_pseudocode, split_operands
class PseudocodeTest(unittest.TestCase):
def test_split_operands_keeps_displacement_expression_together(self):
self.assertEqual(split_operands("@(H'04,R6), R0"), ["@(H'04,R6)", "R0"])
self.assertEqual(split_operands("{R0,R1}, @-SP"), ["{R0,R1}", "@-SP"])
def test_generates_c_like_function_from_decompiler_json(self):
payload = {
"vectors": [{"address": 0, "name": "reset", "target": 0x0100, "target_label": "vec_reset_0100"}],
"call_graph": {
"nodes": [
{
"start": 0x0100,
"end": 0x0110,
"label": "vec_reset_0100",
"sources": ["reset"],
"instruction_count": 5,
"calls": [0x0200],
},
{
"start": 0x0200,
"end": 0x0200,
"label": "loc_0200",
"sources": [],
"instruction_count": 1,
"calls": [],
},
],
"edges": [],
},
"instructions": [
{
"address": 0x0100,
"text": "MOV:G.B #H'FF, @P1DDR",
"mnemonic": "MOV:G.B",
"operands": "#H'FF, @P1DDR",
"kind": "normal",
"targets": [],
"references": [{"address": 0xFE80, "name": "P1DDR", "region": "register_field"}],
"comment": "P1DDR = H'FF",
"peripheral_access": [
{
"register": "FRT2_FRC",
"direction": "write",
"size": "W",
"byte": "high",
},
],
},
{
"address": 0x0105,
"text": "MOV:G.B #H'80, @RAMCR",
"mnemonic": "MOV:G.B",
"operands": "#H'80, @RAMCR",
"kind": "normal",
"targets": [],
"references": [{"address": 0xFF11, "name": "RAMCR", "region": "register_field"}],
"comment": "RAMCR = H'80",
"sci": {
"inferences": [
{"comment": "SCI1 async baud 31250 bps"},
],
},
},
{
"address": 0x010A,
"text": "BNE loc_0110",
"mnemonic": "BNE",
"operands": "loc_0110",
"kind": "branch",
"targets": [0x0110],
"references": [],
"comment": "",
},
{
"address": 0x010C,
"text": "BSR loc_0200",
"mnemonic": "BSR",
"operands": "loc_0200",
"kind": "call",
"targets": [0x0200],
"references": [],
"comment": "",
},
{
"address": 0x0110,
"text": "RTS",
"mnemonic": "RTS",
"operands": "",
"kind": "return",
"targets": [],
"references": [],
"comment": "",
},
{
"address": 0x0200,
"text": "RTS",
"mnemonic": "RTS",
"operands": "",
"kind": "return",
"targets": [],
"references": [],
"comment": "",
},
],
}
text = generate_pseudocode(payload, options=PseudocodeOptions())
self.assertIn("void vec_reset_0100(void)", text)
self.assertIn("P1DDR = (uint8_t)(0xFF);", text)
self.assertIn("RAMCR = (uint8_t)(0x80);", text)
self.assertIn("SCI1 async baud 31250 bps", text)
self.assertIn("FRT2_FRC W write high TEMP access", text)
self.assertIn("if (!Z) goto loc_0110;", text)
self.assertIn("loc_0200();", text)
self.assertIn("loc_0110:", text)
self.assertIn("return;", text)
if __name__ == "__main__":
unittest.main()

122
tests/test_sci_inference.py Normal file
View File

@@ -0,0 +1,122 @@
import json
import tempfile
import unittest
from pathlib import Path
from h8536.model import Instruction
from h8536.render import format_listing, write_json
from h8536.rom import Rom
from h8536.sci import analyze_sci, sci_comment_for_instruction
def sci1_setup(scr: int = 0x3C) -> dict[int, Instruction]:
return {
0x1000: Instruction(
0x1000,
bytes.fromhex("15FED80624"),
"MOV:G.B",
"#H'24, @SCI1_SMR",
references=[0xFED8],
comment="SCI1_SMR = H'24",
),
0x1005: Instruction(
0x1005,
bytes([0x15, 0xFE, 0xDA, 0x06, scr]),
"MOV:G.B",
f"#H'{scr:02X}, @SCI1_SCR",
references=[0xFEDA],
comment=f"SCI1_SCR = H'{scr:02X}",
),
0x100A: Instruction(
0x100A,
bytes.fromhex("15FED90607"),
"MOV:G.B",
"#H'07, @SCI1_BRR",
references=[0xFED9],
comment="SCI1_BRR = H'07",
),
}
class SciInferenceTest(unittest.TestCase):
def test_async_internal_baud_uses_manual_brr_formula(self):
analysis = analyze_sci(sci1_setup(), clock_hz=16_000_000)
config = analysis["channels"]["SCI1"]["configurations"][0]
self.assertEqual(config["mode"], "async")
self.assertEqual(config["cks_n"], 0)
self.assertEqual(config["brr"], 7)
self.assertEqual(config["baud_bps"], 31_250)
self.assertEqual(config["formula"], "B = clock_hz / (64 * 2^(2n) * (N + 1))")
self.assertIn("SCI1 async 8-bit even parity 1 stop baud 31250 bps", config["comment"])
def test_missing_clock_keeps_baud_partial(self):
analysis = analyze_sci(sci1_setup(), clock_hz=None)
comment = sci_comment_for_instruction(analysis, 0x100A)
config = analysis["channels"]["SCI1"]["configurations"][0]
self.assertIsNone(config["baud_bps"])
self.assertEqual(config["reason"], "clock_hz_missing")
self.assertIn("baud needs --clock-hz", comment)
self.assertNotIn("31250 bps", comment)
def test_external_clock_selection_suppresses_internal_baud(self):
analysis = analyze_sci(sci1_setup(scr=0x3E), clock_hz=16_000_000)
config = analysis["channels"]["SCI1"]["configurations"][0]
self.assertIsNone(config["baud_bps"])
self.assertEqual(config["clock_source"], "external")
self.assertEqual(config["reason"], "external_clock_selected")
self.assertIn("external clock selected", config["comment"])
def test_scr_bit_writes_are_tracked_without_repeating_same_baud(self):
instructions = sci1_setup()
instructions[0x1010] = Instruction(
0x1010,
bytes.fromhex("15FEDAC7"),
"BSET.B",
"#7, @SCI1_SCR",
references=[0xFEDA],
comment="set TIE (bit 7) of SCI1_SCR",
)
analysis = analyze_sci(instructions, clock_hz=16_000_000)
writes = analysis["channels"]["SCI1"]["writes"]
self.assertEqual([write["register"] for write in writes], ["SMR", "SCR", "BRR", "SCR"])
self.assertEqual(writes[-1]["value"], 0xBC)
self.assertNotIn(0x1010, analysis["annotations"])
def test_listing_preserves_existing_comment_and_appends_sci_comment(self):
instructions = sci1_setup()
analysis = analyze_sci(instructions, clock_hz=16_000_000)
listing = format_listing(
Path("rom.bin"),
Rom(b"\xFF" * 0x20),
instructions,
{},
{},
"min",
traced=False,
sci_analysis=analysis,
)
self.assertIn("SCI1_BRR = H'07; SCI1 async 8-bit even parity 1 stop baud 31250 bps", listing)
def test_json_includes_top_level_and_instruction_sci_metadata(self):
instructions = sci1_setup()
analysis = analyze_sci(instructions, clock_hz=16_000_000)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "out.json"
write_json(path, instructions, {}, {}, sci_analysis=analysis)
payload = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(payload["sci"]["channels"]["SCI1"]["configurations"][0]["baud_bps"], 31_250)
brr_instruction = next(item for item in payload["instructions"] if item["address"] == 0x100A)
self.assertEqual(brr_instruction["sci"]["inferences"][0]["brr"], 7)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,47 @@
import unittest
from h8536.cycles import annotate_cycles
from h8536.model import Instruction
from h8536.timing import cycle_bounds, summarize_timing
class TimingSummaryTest(unittest.TestCase):
def test_cycle_bounds_use_branch_min_and_max(self):
self.assertEqual(cycle_bounds({"not_taken": 3, "taken": 8}), (3, 8))
self.assertEqual(cycle_bounds({"false": 3, "count_minus_1": 4, "taken": 8}), (3, 8))
def test_summarizes_backward_scb_loop_candidate(self):
instructions = {
0x0100: Instruction(0x0100, b"\x58\x02\x00", "MOV:I.W", "#H'0200, R0"),
0x0103: Instruction(0x0103, b"\x01\xB8\xFD", "SCB/F", "R0, loc_0103", kind="branch", targets=[0x0103]),
0x0106: Instruction(0x0106, b"\x19", "RTS", kind="return", fallthrough=False),
}
annotate_cycles(instructions, "min")
summary = summarize_timing(instructions, {0x0103: "loc_0103"})
self.assertEqual(summary["loops"][0]["start"], 0x0103)
self.assertEqual(summary["loops"][0]["kind"], "counter_delay_loop")
self.assertEqual(summary["loops"][0]["cycles_min"], 3)
self.assertEqual(summary["loops"][0]["cycles_max"], 9)
def test_summarizes_straight_line_block_to_branch(self):
instructions = {
0x0200: Instruction(0x0200, b"\x58\x00\x01", "MOV:I.W", "#H'0001, R0"),
0x0203: Instruction(0x0203, b"\x26\x02", "BNE", "loc_0207", kind="branch", targets=[0x0207]),
0x0205: Instruction(0x0205, b"\x19", "RTS", kind="return", fallthrough=False),
0x0207: Instruction(0x0207, b"\x19", "RTS", kind="return", fallthrough=False),
}
annotate_cycles(instructions, "min")
summary = summarize_timing(instructions, {0x0207: "loc_0207"})
first = summary["blocks"][0]
self.assertEqual(first["start"], 0x0200)
self.assertEqual(first["end"], 0x0203)
self.assertEqual(first["cycles_min"], 6)
self.assertEqual(first["cycles_max"], 11)
if __name__ == "__main__":
unittest.main()