1
0

LCD decompile

This commit is contained in:
Aiden
2026-05-25 15:10:32 +10:00
parent 1d7f00e59c
commit cdfb811c28
15 changed files with 8836 additions and 19 deletions

View File

@@ -1,5 +1,7 @@
# H8/536 ROM Decompiler
The ROM used is from a SONY RCP-TX7 Camera Panel. Some of the code in this repo may be bias to the functions of that particular use case with the H8/536.
This repo now includes a standalone Python helper for the H8/536 ROM image:
```powershell
@@ -33,6 +35,7 @@ To turn the structured decompile output into conservative C-like pseudocode:
- Tracks SCI setup writes and can infer baud rates from SMR/BRR when `--clock-hz` is supplied.
- Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers.
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
- Scans likely LCD/menu text records, groups display-text regions, and reports literal/near matches for terms such as `CONNECT`.
- Emits function summaries and a direct-call graph in JSON, with optional Graphviz DOT output.
- Tracks conservative per-basic-block register/control-register dataflow in JSON and comments known value changes.
- Discovers RAM/external/global symbols from memory references and pointer tables, including read/write counts and xrefs.
@@ -40,6 +43,7 @@ To turn the structured decompile output into conservative C-like pseudocode:
- Adds Appendix A cycle estimates to JSON and can append them to ASM comments.
- Summarizes straight-line block timing and backward-branch loop timing when requested.
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
The generated listing is written to:
@@ -97,6 +101,8 @@ python h8536_pseudocode.py --help
- `h8536/dataflow.py`: conservative register/control-register value tracking.
- `h8536/symbols.py`: RAM/external/global symbol discovery from references and data tables.
- `h8536/indirect.py`: indirect call/jump and pointer-table dispatch hints.
- `h8536/lcd_text.py`: LCD/menu text record scanning, fuzzy search, and text xrefs.
- `h8536/lcd_driver.py`: LCD E-clock access and busy-poll recognizer.
- `h8536/timing.py`: block and loop cycle summaries.
- `h8536/sci.py`: SCI setup tracking and baud inference.
- `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis.

View File

@@ -10,6 +10,7 @@
; - The register field is H'FE80-H'FFFF; names below come from appendix B.
; - @aa:8 short absolute operands use BR as the upper address byte.
; - SCI baud inference uses section 14.2.8 BRR formulas when SMR/BRR are known.
; - LCD inference treats E-clock H'F200/H'F201 accesses as status/control and data candidates.
; - Pass --clock-hz to convert SCI BRR settings into numeric baud rates.
; - Cycle counts use Appendix A tables A-7/A-8 for on-chip access with no external wait states.
@@ -223,6 +224,82 @@
; mem_F10E H'F10E program_or_external memory r=0 w=1 width=byte
; ... 206 more symbols omitted from listing header
; LCD/Text Scan
; search 'CONNECT': not literal, hits=0
; near: H'A025 'COMPLETED', H'8E79 'ON CONT1 OFF~X', H'8F55 'ON CONT2 OFF~X', H'94A9 'ON'
; LCD text regions
; region H'63D7-H'6758 count=15 'OPERATION', 'PAINT', 'OPERATION', 'IRIS/M.BLK'
; region H'67E0-H'6831 count=2 'TLCS Xg', 'AGC GAIN AE Xh'
; region H'6A4F-H'6C47 count=8 'AUTO FUNC XjO', 'A.IRIS MODE Xj', 'AI BACK.L~Xj', 'AUTO FUNC Xk='
; region H'6F84-H'6FC0 count=2 'OTHERS Xo', 'SHUTTER Xo'
; region H'7052-H'7477 count=15 'SET RCP', 'MASTER', 'OTHERS Xp', 'COPY TO SLAVES~Xp'
; region H'757A-H'7824 count=14 'BARS TYPE Xuz', 'SMPTE Xu', 'SPLIT Xu', 'FULLFIELD 75% Xu'
; region H'78B5-H'792F count=4 'OTHERS Xx', 'WHITE BLACK~Xx', 'COMM LINK ITEM-2Xx', 'FLARE Xy'
; region H'819C-H'87A9 count=28 'SHADING X', 'WHITE~X', 'SHADING AUTO SETX', 'BLACK~X'
; region H'883D-H'8959 count=7 'MATRIX X', 'STD FL~X', 'PRESET MATRIX X', 'H.SAT SPCL~X'
; region H'8A0C-H'8BAC count=7 'MATRIX X', 'ON SKIN OFF~X', 'SAT HUE X', 'MATRIX X'
; region H'8CB7-H'8CFD count=2 'FILTER X', '1 2 3 4 X'
; region H'8E57-H'8EA7 count=3 'LENS X', 'ON CONT1 OFF~X', 'FOCUS ZOOM X'
; ... 23 more LCD text regions
; LCD text candidates
; text H'41B0 len=35 medium '01020304050607080910111213141516X'
; text H'5B55 len=10 high '0123456789'
; text H'60F6 len=16 high '0123456789ABCDEF'
; text H'63D7 len=10 high 'OPERATION' xrefs=1
; text H'63F5 len=10 high 'PAINT' xrefs=1
; text H'6443 len=10 high 'OPERATION' xrefs=1
; text H'6461 len=10 high 'IRIS/M.BLK' xrefs=1
; text H'6490 len=10 high 'OPERATION' xrefs=1
; text H'64AE len=10 high 'LOCK' xrefs=1
; text H'652F len=19 high 'DYNA LATITUDE Xe/' xrefs=1
; text H'6551 len=18 medium 'HIGH LOW~XeP' xrefs=1
; text H'65C9 len=18 medium 'BLACK STR Xe' xrefs=1
; text H'6644 len=19 medium 'BLACK STR XfD' xrefs=1
; text H'6665 len=19 medium 'STRETCH LEVEL Xfe' xrefs=1
; text H'6683 len=18 high 'POINT1 POINT2Xf' xrefs=1
; text H'6706 len=18 medium 'BLACK STR Xg' xrefs=1
; text H'6727 len=19 medium "COMPRESS LEVEL Xg'" xrefs=1
; text H'6745 len=19 high 'POINT1 POINT2XgE' xrefs=1
; text H'67E0 len=18 medium 'TLCS Xg' xrefs=1
; text H'681F len=18 medium 'AGC GAIN AE Xh' xrefs=1
; text H'693B len=19 medium 'AUTO FUNC Xi;' xrefs=1
; text H'6A4F len=19 medium 'AUTO FUNC XjO' xrefs=1
; text H'6A8E len=18 medium 'A.IRIS MODE Xj' xrefs=1
; text H'6AAD len=17 medium 'AI BACK.L~Xj' xrefs=1
; text H'6B3D len=19 medium 'AUTO FUNC Xk=' xrefs=1
; text H'6B5E len=19 medium 'AUTO FOCUS Xk^' xrefs=1
; text H'6BEF len=18 medium 'DIAG Xk' xrefs=1
; text H'6C16 len=18 medium 'DIAG DATA Xl' xrefs=1
; text H'6C35 len=18 medium 'RESET REQ~Xl4' xrefs=1
; text H'6F84 len=18 medium 'OTHERS Xo' xrefs=1
; text H'6FAE len=18 medium 'SHUTTER Xo' xrefs=1
; text H'7052 len=14 medium 'SET RCP' xrefs=1
; text H'706F len=14 medium 'MASTER' xrefs=1
; text H'709F len=18 medium 'OTHERS Xp' xrefs=1
; text H'70C0 len=18 medium 'COPY TO SLAVES~Xp' xrefs=1
; text H'7144 len=19 medium 'CAM ID SET~XqD' xrefs=1
; text H'71C9 len=18 medium 'OTHERS Xq' xrefs=1
; text H'71F9 len=18 medium 'CAM ID IND Xq' xrefs=1
; text H'7213 len=18 medium 'TITLE IND Xr' xrefs=1
; text H'72A5 len=18 medium 'OTHERS Xr' xrefs=1
; text H'72C7 len=17 medium 'CAM BARS~Xr' xrefs=1
; text H'72E4 len=18 medium 'CLOCK IND Xr' xrefs=1
; text H'7369 len=19 medium 'OTHERS Xsi' xrefs=1
; text H'7393 len=18 high 'CENTER MARKER Xs' xrefs=1
; text H'7425 len=19 medium 'OTHERS Xt%' xrefs=1
; text H'7464 len=19 medium 'SAFETY ZONE Xtd' xrefs=1
; text H'757A len=19 medium 'BARS TYPE Xuz' xrefs=1
; text H'75A4 len=18 medium 'SMPTE Xu' xrefs=1
; ... 192 more LCD text candidates
; LCD Driver Candidates
; H'F200 lcd_status_control status/control register inferred from busy polling and command writes
; H'F201 lcd_data data register inferred from paired data reads/writes
; LCD routines
; routine H'3F40-H'3F74 lcd_wait_and_transfer lcd_command_or_address_write, lcd_data_read, lcd_data_write, lcd_status_read
; LCD busy loops
; loop H'3F4A->H'3F51 LCD busy-flag poll: read H'F200, test bit 7, branch until clear
; Timing Summary
; Straight-line blocks
; block H'1000-H'10CB vec_reset_1000 ins=42 cycles=371 unknown=0
@@ -2135,23 +2212,23 @@ loc_3F40:
3F46: 0C 06 00 48 ORC.W #H'0600, SR ; cycles=4
loc_3F4A:
3F4A: 15 F2 00 00 80 MOVFPE.B @H'F200, R0 ; refs mem_F200 in program_or_external; cycles=13
3F4F: A0 F7 BTST.B #7, R0 ; cycles=2
3F51: 26 F7 BNE loc_3F4A ; cycles=3/8 nt/t
3F4A: 15 F2 00 00 80 MOVFPE.B @H'F200, R0 ; LCD status read from E-clock H'F200; LCD busy-flag poll: read H'F200, test bit 7, branch until clear; refs mem_F200 in program_or_external; cycles=13
3F4F: A0 F7 BTST.B #7, R0 ; LCD busy-flag poll: read H'F200, test bit 7, branch until clear; cycles=2
3F51: 26 F7 BNE loc_3F4A ; LCD busy-flag poll: read H'F200, test bit 7, branch until clear; cycles=3/8 nt/t
3F53: AC F8 BTST.W #8, R4 ; cycles=3
3F55: 26 16 BNE loc_3F6D ; cycles=3/8 nt/t
3F57: AC F9 BTST.W #9, R4 ; cycles=3
3F59: 26 07 BNE loc_3F62 ; cycles=3/8 nt/t
3F5B: 15 F2 00 00 94 MOVTPE.B R4, @H'F200 ; refs mem_F200 in program_or_external; cycles=13
3F5B: 15 F2 00 00 94 MOVTPE.B R4, @H'F200 ; LCD command/address write to E-clock H'F200; refs mem_F200 in program_or_external; cycles=13
3F60: 20 10 BRA loc_3F72 ; cycles=7
loc_3F62:
3F62: 15 F2 01 00 94 MOVTPE.B R4, @H'F201 ; refs mem_F201 in program_or_external; cycles=13
3F62: 15 F2 01 00 94 MOVTPE.B R4, @H'F201 ; LCD data write to E-clock H'F201; refs mem_F201 in program_or_external; cycles=13
3F67: 1D FB 00 08 ADD:Q.W #1, @H'FB00 ; refs ram_FB00 in on_chip_ram; cycles=8
3F6B: 20 05 BRA loc_3F72 ; cycles=8
loc_3F6D:
3F6D: 15 F2 01 00 84 MOVFPE.B @H'F201, R4 ; refs mem_F201 in program_or_external; cycles=13
3F6D: 15 F2 01 00 84 MOVFPE.B @H'F201, R4 ; LCD data read from E-clock H'F201; refs mem_F201 in program_or_external; cycles=13
loc_3F72:
3F72: CF 88 LDC.W @R7+, SR ; cycles=7

File diff suppressed because it is too large Load Diff

View File

@@ -2116,21 +2116,21 @@ void loc_3F40(void)
SR &= (uint16_t)(0x00FF); /* 3F42; ANDC.W #H'00FF, SR; cycles=4 */
SR |= (uint16_t)(0x0600); /* 3F46; ORC.W #H'0600, SR; cycles=4 */
do {
R0 = read_eclock(MEM8[0xF200]); /* 3F4A; MOVFPE.B @H'F200, R0; refs mem_F200; cycles=13 */
set_flags_btst(R0, 7); /* 3F4F; BTST.B #7, R0; cycles=2 */
} while (!Z); /* 3F51; BNE loc_3F4A; cycles=3/8 nt/t */
R0 = read_eclock(MEM8[0xF200]); /* 3F4A; MOVFPE.B @H'F200, R0; LCD status read from E-clock H'F200; LCD busy-flag poll: read H'F200, test bit 7, branch until clear; refs mem_F200; cycles=13 */
set_flags_btst(R0, 7); /* 3F4F; BTST.B #7, R0; LCD busy-flag poll: read H'F200, test bit 7, branch until clear; cycles=2 */
} while (!Z); /* 3F51; BNE loc_3F4A; LCD busy-flag poll: read H'F200, test bit 7, branch until clear; cycles=3/8 nt/t */
set_flags_btst(R4, 8); /* 3F53; BTST.W #8, R4; cycles=3 */
if (!Z) goto loc_3F6D; /* 3F55; BNE loc_3F6D; cycles=3/8 nt/t */
set_flags_btst(R4, 9); /* 3F57; BTST.W #9, R4; cycles=3 */
if (!Z) goto loc_3F62; /* 3F59; BNE loc_3F62; cycles=3/8 nt/t */
write_eclock(MEM8[0xF200], R4); /* 3F5B; MOVTPE.B R4, @H'F200; refs mem_F200; cycles=13 */
write_eclock(MEM8[0xF200], R4); /* 3F5B; MOVTPE.B R4, @H'F200; LCD command/address write to E-clock H'F200; refs mem_F200; cycles=13 */
goto loc_3F72; /* 3F60; BRA loc_3F72; cycles=7 */
loc_3F62:
write_eclock(MEM8[0xF201], R4); /* 3F62; MOVTPE.B R4, @H'F201; refs mem_F201; cycles=13 */
write_eclock(MEM8[0xF201], R4); /* 3F62; MOVTPE.B R4, @H'F201; LCD data write to E-clock H'F201; refs mem_F201; cycles=13 */
MEM16[0xFB00] += (uint16_t)(1); /* 3F67; ADD:Q.W #1, @H'FB00; refs ram_FB00; cycles=8 */
goto loc_3F72; /* 3F6B; BRA loc_3F72; cycles=8 */
loc_3F6D:
R4 = read_eclock(MEM8[0xF201]); /* 3F6D; MOVFPE.B @H'F201, R4; refs mem_F201; cycles=13 */
R4 = read_eclock(MEM8[0xF201]); /* 3F6D; MOVFPE.B @H'F201, R4; LCD data read from E-clock H'F201; refs mem_F201; cycles=13 */
loc_3F72:
SR = (uint16_t)(MEM16[R7++]); /* 3F72; LDC.W @R7+, SR; cycles=7 */
return; /* 3F74; RTS; cycles=12 */

521
h8536/board_profile.py Normal file
View File

@@ -0,0 +1,521 @@
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,
"TDR": 0xFEDB,
"SSR": 0xFEDC,
"RDR": 0xFEDD,
},
"SCI2": {
"SMR": 0xFEF0,
"BRR": 0xFEF1,
"SCR": 0xFEF2,
"TDR": 0xFEF3,
"SSR": 0xFEF4,
"RDR": 0xFEF5,
},
}
SCI_REGISTER_BY_ADDRESS = {
address: (channel, register)
for channel, registers in SCI_REGISTERS.items()
for register, address in registers.items()
}
SYSCR2_ADDRESS = 0xFEFD
SYSCR2_INITIAL = 0x80
P9SCI2E_BIT = 0
SCR_INITIAL = 0x0C
MANUAL_REFERENCES = [
"Manual/0900766b802125d0.md:2417 FP-80 H8/536 pin 66 is P95/TXD",
"Manual/0900766b802125d0.md:2418 FP-80 H8/536 pin 67 is P96/RXD",
"Manual/0900766b802125d0.md:11192 Port 9 carries SCI1 and SCI2 serial signals",
"Manual/0900766b802125d0.md:11201 P96 is RXD1 input",
"Manual/0900766b802125d0.md:11202 P95 is TXD1 output",
"Manual/0900766b802125d0.md:15725 SCI1 RXD input pin",
"Manual/0900766b802125d0.md:15726 SCI1 TXD output pin",
"Manual/0900766b802125d0.md:15750 SCI register table starts with SCI1 RDR/TDR/SMR/SCR/SSR/BRR",
"Manual/0900766b802125d0.md:15758 SCI register table lists SCI2 RDR/TDR/SMR/SCR/SSR/BRR",
"Manual/0900766b802125d0.md:15794 RDR receive data register",
"Manual/0900766b802125d0.md:15823 TDR transmit data register",
"Manual/0900766b802125d0.md:15969 SCR enables and disables SCI functions",
"Manual/0900766b802125d0.md:16009 SCR.TE makes the TXD pin output",
"Manual/0900766b802125d0.md:16029 SCR.RE makes the RXD pin input",
"Manual/0900766b802125d0.md:16090 SSR contains transmit/receive status flags",
"Manual/0900766b802125d0.md:10560 SYSCR2 controls port 9 pin functions",
"Manual/0900766b802125d0.md:10631 SYSCR2.P9SCI2E controls the SCI2 functions of P92-P94",
]
BOARD_PROFILES = {
"sony_rcp_tx7": {
"name": "Sony RCP-TX7",
"summary": "Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.",
"manual_references": MANUAL_REFERENCES,
"traces": [
{
"channel": "SCI1",
"signal": "TXD",
"h8_pin": 66,
"h8_pin_name": "P95/TXD",
"h8_function": "TXD1",
"max202_pin": 11,
"evidence": "MAX202 pin 11 traces to H8 pin 66",
},
{
"channel": "SCI1",
"signal": "RXD",
"h8_pin": 67,
"h8_pin_name": "P96/RXD",
"h8_function": "RXD1",
"max202_pin": 12,
"evidence": "MAX202 pin 12 traces to H8 pin 67",
},
],
},
}
_READ_ONLY_ROOTS = {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}
_BIT_WRITE_ROOTS = {"BCLR", "BNOT", "BSET"}
def analyze_board_profile(
instructions: Mapping[int, Instruction],
board: str = "sony_rcp_tx7",
) -> dict[str, object]:
"""Annotate board-specific serial-path accesses without changing disassembly output."""
if board not in BOARD_PROFILES:
raise ValueError(f"unsupported board profile: {board}")
profile = BOARD_PROFILES[board]
annotations: dict[int, str] = {}
instruction_metadata: dict[int, dict[str, object]] = {}
channels = _initial_channel_payload(profile)
register_values: dict[int, int | None] = {
SYSCR2_ADDRESS: SYSCR2_INITIAL,
SCI_REGISTERS["SCI1"]["SCR"]: SCR_INITIAL,
SCI_REGISTERS["SCI2"]["SCR"]: SCR_INITIAL,
}
for address in sorted(instructions):
ins = instructions[address]
access_kind = _access_kind(ins)
access_records: list[dict[str, object]] = []
comments: list[str] = []
if SYSCR2_ADDRESS in _expanded_references(ins, access_kind):
value = _write_value(ins, SYSCR2_ADDRESS, register_values.get(SYSCR2_ADDRESS), 1, 0)
if access_kind == "write":
register_values[SYSCR2_ADDRESS] = value
record = _syscr2_access(ins, access_kind, register_values.get(SYSCR2_ADDRESS))
access_records.append(record)
comments.append(str(record["comment"]))
for target in _sci_targets(ins, access_kind):
register_address = int(target["register_address"])
value = _write_value(
ins,
register_address,
register_values.get(register_address),
int(target["width"]),
int(target["index"]),
)
if access_kind == "write":
register_values[register_address] = value
channel = str(target["channel"])
register = str(target["register"])
p9sci2e = _bit_state(register_values.get(SYSCR2_ADDRESS), P9SCI2E_BIT)
record = _sci_access(
ins,
channel,
register,
register_address,
access_kind,
value,
register_values.get(register_address),
p9sci2e,
)
access_records.append(record)
comments.append(str(record["comment"]))
channel_payload = channels[channel]
channel_accesses = channel_payload["accesses"]
if isinstance(channel_accesses, list):
channel_accesses.append(record)
if register == "SCR":
channel_payload["scr"] = _scr_metadata(register_values.get(register_address))
if access_records:
comment = "; ".join(_dedupe(comments))
annotations[ins.address] = comment
instruction_metadata[ins.address] = {
"accesses": access_records,
"comment": comment,
}
channels["SCI2"]["p9sci2e"] = _bit_state(register_values.get(SYSCR2_ADDRESS), P9SCI2E_BIT)
return {
"board": board,
"name": profile["name"],
"summary": profile["summary"],
"manual_references": list(profile["manual_references"]),
"traces": [dict(trace) for trace in profile["traces"]],
"channels": channels,
"annotations": annotations,
"instructions": instruction_metadata,
"state": {
"SYSCR2": _register_state(register_values.get(SYSCR2_ADDRESS)),
"P9SCI2E": _bit_state(register_values.get(SYSCR2_ADDRESS), P9SCI2E_BIT),
},
}
def board_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)
if comment is None:
comment = annotations.get(str(address))
return str(comment) if comment else ""
def board_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)
if metadata is None:
metadata = instructions.get(str(address))
return metadata if isinstance(metadata, dict) else None
def _initial_channel_payload(profile: Mapping[str, object]) -> dict[str, dict[str, object]]:
traces = [dict(trace) for trace in profile["traces"] if isinstance(trace, Mapping)]
return {
"SCI1": {
"traced_to_max202": True,
"path": "RS232/MAX202",
"pins": traces,
"scr": _scr_metadata(SCR_INITIAL),
"accesses": [],
},
"SCI2": {
"traced_to_max202": False,
"path": None,
"note": "Sony RCP-TX7 MAX202 board traces are on SCI1 P95/P96, not SCI2 P92/P93.",
"p9sci2e": False,
"scr": _scr_metadata(SCR_INITIAL),
"accesses": [],
},
}
def _syscr2_access(ins: Instruction, access: str, value: int | None) -> dict[str, object]:
p9sci2e = _bit_state(value, P9SCI2E_BIT)
comment = _syscr2_comment(access, p9sci2e)
record: dict[str, object] = {
"address": ins.address,
"instruction": ins.text,
"register": "SYSCR2",
"register_address": SYSCR2_ADDRESS,
"access": access,
"p9sci2e": p9sci2e,
"comment": comment,
}
if value is not None:
record["value"] = value
record["value_hex"] = _hex8(value)
return record
def _sci_access(
ins: Instruction,
channel: str,
register: str,
register_address: int,
access: str,
value: int | None,
state_value: int | None,
p9sci2e: bool | None,
) -> dict[str, object]:
comment = _sci_comment(channel, register, access, value if value is not None else state_value, p9sci2e)
record: dict[str, object] = {
"address": ins.address,
"instruction": ins.text,
"channel": channel,
"register": register,
"register_address": register_address,
"access": access,
"traced_to_max202": channel == "SCI1",
"comment": comment,
}
if value is not None:
record["value"] = value
record["value_hex"] = _hex8(value)
if register == "SCR":
record["scr"] = _scr_metadata(value if value is not None else state_value)
if channel == "SCI2":
record["p9sci2e"] = p9sci2e
return record
def _sci_comment(
channel: str,
register: str,
access: str,
value: int | None,
p9sci2e: bool | None,
) -> str:
if channel == "SCI1":
return _sci1_comment(register, access, value)
return _sci2_comment(register, access, p9sci2e)
def _sci1_comment(register: str, access: str, value: int | None) -> str:
if register in {"SMR", "BRR"}:
return (
f"SCI1 {register} serial init for traced RS232/MAX202 path "
"(H8 pin 66 P95/TXD to MAX202 pin 11; MAX202 pin 12 to H8 pin 67 P96/RXD)"
)
if register == "SCR":
bits = _scr_bits_text(value)
suffix = f" {bits}" if bits else ""
return (
f"SCI1 SCR {access}{suffix}; TE/RE select the traced RS232/MAX202 pins "
"(P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12)"
)
if register == "TDR":
verb = "transmits" if access == "write" else "accesses transmit buffer"
return (
f"SCI1 TDR {access} {verb} on traced RS232/MAX202 path: "
"H8 pin 66 P95/TXD -> MAX202 pin 11"
)
if register == "RDR":
verb = "receives" if access == "read" else "accesses receive buffer"
return (
f"SCI1 RDR {access} {verb} from traced RS232/MAX202 path: "
"MAX202 pin 12 -> H8 pin 67 P96/RXD"
)
if register == "SSR":
return "SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use"
return f"SCI1 {register} {access} on traced RS232/MAX202 path"
def _sci2_comment(register: str, access: str, p9sci2e: bool | None) -> str:
prefix = f"SCI2 {register} {access}; not the traced MAX202 path"
if p9sci2e is False:
return (
f"{prefix}; P9SCI2E=0 disables SCI2 pins P92/P93/P94, "
"while the board trace is SCI1 P95/P96"
)
if p9sci2e is True:
return f"{prefix}; P9SCI2E=1 may route SCI2 pins, but the board trace is SCI1 P95/P96"
return f"{prefix}; P9SCI2E state unknown, and the board trace is SCI1 P95/P96"
def _syscr2_comment(access: str, p9sci2e: bool | None) -> str:
if p9sci2e is False:
return (
f"SYSCR2 {access} leaves P9SCI2E=0; SCI2 pins are disabled, "
"so SCI2 is not the traced MAX202 path; traced RS232/MAX202 remains SCI1 P95/P96"
)
if p9sci2e is True:
return (
f"SYSCR2 {access} sets P9SCI2E=1; SCI2 pins may be enabled, "
"but Sony RCP-TX7 MAX202 traces are SCI1 P95/P96"
)
return f"SYSCR2 {access}; P9SCI2E unknown, board trace remains SCI1 P95/P96"
def _sci_targets(ins: Instruction, access: str) -> list[dict[str, object]]:
width = _mnemonic_width(ins.mnemonic)
targets: list[dict[str, object]] = []
seen: set[int] = set()
for base_address in ins.references:
target_width = width if _operand_references_memory(ins, access) else 1
for index in range(target_width):
register_address = base_address + index
if register_address in seen:
continue
register_info = SCI_REGISTER_BY_ADDRESS.get(register_address)
if register_info is None:
continue
seen.add(register_address)
channel, register = register_info
targets.append(
{
"channel": channel,
"register": register,
"register_address": register_address,
"width": target_width,
"index": index,
},
)
return targets
def _expanded_references(ins: Instruction, access: str) -> set[int]:
width = _mnemonic_width(ins.mnemonic)
target_width = width if _operand_references_memory(ins, access) else 1
addresses: set[int] = set()
for base_address in ins.references:
for index in range(target_width):
addresses.add(base_address + index)
return addresses
def _operand_references_memory(ins: Instruction, access: str) -> bool:
operand = _destination_operand(ins.operands) if access == "write" else _source_operand(ins.operands)
return bool(operand and operand.startswith("@"))
def _access_kind(ins: Instruction) -> str:
root = _mnemonic_root(ins.mnemonic)
if root in _READ_ONLY_ROOTS:
return "read"
destination = _destination_operand(ins.operands)
if destination and destination.startswith("@"):
return "write"
return "read"
def _write_value(
ins: Instruction,
register_address: int,
current_value: int | None,
width: int,
index: int,
) -> int | None:
if _access_kind(ins) != "write":
return None
root = _mnemonic_root(ins.mnemonic)
if root == "CLR":
return 0
if root in _BIT_WRITE_ROOTS:
bit = _immediate_bit(ins.operands)
if bit is None or current_value is None:
return None
if root == "BSET":
return (current_value | (1 << bit)) & 0xFF
if root == "BCLR":
return (current_value & ~(1 << bit)) & 0xFF
return (current_value ^ (1 << bit)) & 0xFF
value = _immediate_source_value(ins.operands)
if value is None:
return None
return _byte_for_target(value, width, index)
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 _source_operand(operands: str) -> str | None:
operands = operands.strip()
if not operands:
return None
if "," not in operands:
return operands
return operands.rsplit(",", 1)[0].strip()
def _immediate_source_value(operands: str) -> int | None:
source = _source_operand(operands) or ""
if not source.startswith("#"):
return None
try:
return parse_int(source[1:])
except ValueError:
return None
def _immediate_bit(operands: str) -> int | None:
value = _immediate_source_value(operands)
if value is None:
return None
return value if 0 <= value <= 7 else None
def _mnemonic_root(mnemonic: str) -> str:
return mnemonic.split(".", 1)[0]
def _mnemonic_width(mnemonic: str) -> int:
return 2 if mnemonic.endswith(".W") else 1
def _byte_for_target(value: int, width: int, index: int) -> int:
shift = 8 * (width - index - 1)
return (value >> shift) & 0xFF
def _scr_metadata(value: int | None) -> dict[str, object]:
metadata = _register_state(value)
metadata.update(
{
"tie": None if value is None else bool(value & 0x80),
"rie": None if value is None else bool(value & 0x40),
"tx_enabled": None if value is None else bool(value & 0x20),
"rx_enabled": None if value is None else bool(value & 0x10),
},
)
return metadata
def _register_state(value: int | None) -> dict[str, object]:
return {
"value": value,
"value_hex": None if value is None else _hex8(value),
}
def _scr_bits_text(value: int | None) -> str:
if value is None:
return ""
return f"TE={1 if value & 0x20 else 0} RE={1 if value & 0x10 else 0}"
def _bit_state(value: int | None, bit: int) -> bool | None:
if value is None:
return None
return bool(value & (1 << bit))
def _dedupe(items: list[str]) -> list[str]:
seen: set[str] = set()
output: list[str] = []
for item in items:
if item in seen:
continue
seen.add(item)
output.append(item)
return output
def _hex8(value: int) -> str:
return f"H'{value & 0xFF:02X}"

View File

@@ -10,6 +10,8 @@ from .dataflow import analyze_dataflow
from .decoder import H8536Decoder
from .formatting import parse_int
from .indirect import analyze_indirect_flow
from .lcd_driver import analyze_lcd_driver
from .lcd_text import analyze_lcd_text
from .peripheral_access import analyze_peripheral_access
from .render import format_callgraph_dot, format_listing, write_json
from .rom import Rom
@@ -81,6 +83,8 @@ def main() -> int:
sci_analysis = analyze_sci(instructions, clock_hz=args.clock_hz)
peripheral_access = analyze_peripheral_access(instructions)
indirect_flow = analyze_indirect_flow(rom, instructions, labels)
lcd_text = analyze_lcd_text(rom, instructions, start=args.start, end=end)
lcd_driver = analyze_lcd_driver(instructions)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(
@@ -101,6 +105,8 @@ def main() -> int:
indirect_flow=indirect_flow,
dataflow=dataflow,
symbols=symbols,
lcd_text=lcd_text,
lcd_driver=lcd_driver,
),
encoding="utf-8",
)
@@ -120,6 +126,8 @@ def main() -> int:
indirect_flow=indirect_flow,
dataflow=dataflow,
symbols=symbols,
lcd_text=lcd_text,
lcd_driver=lcd_driver,
)
if args.callgraph_dot:
args.callgraph_dot.parent.mkdir(parents=True, exist_ok=True)

311
h8536/lcd_driver.py Normal file
View File

@@ -0,0 +1,311 @@
from __future__ import annotations
import re
from collections.abc import Iterable, Mapping
from .formatting import h16
from .model import Instruction
LCD_STATUS_CONTROL = 0xF200
LCD_DATA = 0xF201
LCD_ADDRESSES = {
LCD_STATUS_CONTROL: "lcd_status_control",
LCD_DATA: "lcd_data",
}
ADDRESS_RE = re.compile(r"H'(?P<hex>[0-9A-Fa-f]{4})|0x(?P<c_hex>[0-9A-Fa-f]{4})")
REGISTER_RE = re.compile(r"\b(?P<reg>R[0-7])\b")
def analyze_lcd_driver(instructions: Mapping[int, Instruction] | Iterable[Instruction]) -> dict[str, object]:
ordered = _instruction_sequence(instructions)
by_address = {ins.address: ins for ins in ordered}
accesses = [_access_for_instruction(ins) for ins in ordered]
accesses = [access for access in accesses if access]
instruction_metadata: dict[int, list[dict[str, object]]] = {}
for access in accesses:
instruction_metadata.setdefault(int(access["address"]), []).append(access)
polling_loops = _find_polling_loops(ordered, by_address)
for loop in polling_loops:
for address, role in (
(int(loop["read_address"]), "lcd_busy_status_read"),
(int(loop["test_address"]), "lcd_busy_flag_test"),
(int(loop["branch_address"]), "lcd_busy_wait_branch"),
):
instruction_metadata.setdefault(address, []).append(
{
"address": address,
"kind": role,
"summary": loop["summary"],
"loop_start": loop["read_address"],
},
)
routines = _routine_candidates(ordered, accesses, polling_loops)
return {
"addresses": [
{"address": address, "name": name, "role": _address_role(address)}
for address, name in LCD_ADDRESSES.items()
],
"accesses": accesses,
"polling_loops": polling_loops,
"routines": routines,
"instructions": instruction_metadata,
}
def lcd_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
metadata = lcd_metadata_for_instruction(analysis, address)
if not metadata:
return ""
summaries = []
for item in metadata:
if isinstance(item, Mapping) and item.get("summary"):
summary = str(item["summary"])
if summary not in summaries:
summaries.append(summary)
return "; ".join(summaries)
def lcd_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 list(metadata) if isinstance(metadata, list) else []
def _instruction_sequence(instructions: Mapping[int, Instruction] | Iterable[Instruction]) -> list[Instruction]:
values = instructions.values() if isinstance(instructions, Mapping) else instructions
return sorted(values, key=lambda ins: ins.address)
def _access_for_instruction(ins: Instruction) -> dict[str, object] | None:
address = _lcd_address_for_instruction(ins)
if address is None:
return None
direction = _direction_for_instruction(ins, address)
role = _access_role(ins, address, direction)
register = _last_register(ins.operands)
return {
"address": ins.address,
"instruction": ins.text,
"lcd_address": address,
"lcd_name": LCD_ADDRESSES[address],
"direction": direction,
"role": role,
"register": register,
"summary": _access_summary(address, direction, role),
}
def _lcd_address_for_instruction(ins: Instruction) -> int | None:
for address in ins.references:
if address in LCD_ADDRESSES:
return address
for match in ADDRESS_RE.finditer(ins.operands):
value = int(match.group("hex") or match.group("c_hex"), 16)
if value in LCD_ADDRESSES:
return value
return None
def _direction_for_instruction(ins: Instruction, lcd_address: int) -> str:
mnemonic = ins.mnemonic.upper()
operands = [part.strip() for part in ins.operands.split(",")]
if mnemonic.startswith("MOVFPE"):
return "read"
if mnemonic.startswith("MOVTPE"):
return "write"
if len(operands) >= 2:
source_has_lcd = _operand_has_address(operands[0], lcd_address)
dest_has_lcd = _operand_has_address(operands[-1], lcd_address)
if source_has_lcd and not dest_has_lcd:
return "read"
if dest_has_lcd and not source_has_lcd:
return "write"
return "unknown"
def _operand_has_address(operand: str, address: int) -> bool:
return f"H'{address:04X}" in operand.upper() or f"0X{address:04X}" in operand.upper()
def _access_role(ins: Instruction, address: int, direction: str) -> str:
if address == LCD_STATUS_CONTROL and direction == "read":
return "lcd_status_read"
if address == LCD_STATUS_CONTROL and direction == "write":
return "lcd_command_or_address_write"
if address == LCD_DATA and direction == "read":
return "lcd_data_read"
if address == LCD_DATA and direction == "write":
return "lcd_data_write"
return "lcd_access"
def _access_summary(address: int, direction: str, role: str) -> str:
if role == "lcd_status_read":
return f"LCD status read from E-clock {h16(address)}"
if role == "lcd_command_or_address_write":
return f"LCD command/address write to E-clock {h16(address)}"
if role == "lcd_data_read":
return f"LCD data read from E-clock {h16(address)}"
if role == "lcd_data_write":
return f"LCD data write to E-clock {h16(address)}"
return f"LCD {direction} at E-clock {h16(address)}"
def _find_polling_loops(
ordered: list[Instruction],
by_address: Mapping[int, Instruction],
) -> list[dict[str, object]]:
loops: list[dict[str, object]] = []
for index, ins in enumerate(ordered):
access = _access_for_instruction(ins)
if not access or access["role"] != "lcd_status_read":
continue
register = access.get("register")
if not isinstance(register, str):
continue
test = _next_register_bit_test(ordered, index, register)
if test is None:
continue
branch = _next_back_branch_to(ordered, by_address, ordered.index(test), ins.address, test.address)
if branch is None:
continue
loops.append(
{
"read_address": ins.address,
"test_address": test.address,
"branch_address": branch.address,
"register": register,
"bit": 7,
"summary": f"LCD busy-flag poll: read {h16(LCD_STATUS_CONTROL)}, test bit 7, branch until clear",
},
)
return loops
def _next_register_bit_test(ordered: list[Instruction], index: int, register: str) -> Instruction | None:
for candidate in ordered[index + 1 : index + 5]:
if not candidate.mnemonic.upper().startswith("BTST"):
continue
if "#7" not in candidate.operands:
continue
if re.search(rf"\b{re.escape(register)}\b", candidate.operands):
return candidate
return None
def _next_back_branch_to(
ordered: list[Instruction],
by_address: Mapping[int, Instruction],
index: int,
read_address: int,
test_address: int,
) -> Instruction | None:
_ = by_address
for candidate in ordered[index + 1 : index + 5]:
if candidate.kind != "branch":
continue
targets = [int(target) for target in candidate.targets]
if read_address in targets or test_address in targets:
return candidate
return None
def _routine_candidates(
ordered: list[Instruction],
accesses: list[dict[str, object]],
polling_loops: list[dict[str, object]],
) -> list[dict[str, object]]:
if not accesses:
return []
address_to_access = {int(access["address"]): access for access in accesses}
loop_addresses = {int(loop["read_address"]) for loop in polling_loops}
routines: dict[tuple[int, int], dict[str, object]] = {}
for address in sorted(address_to_access):
start, end = _routine_span(ordered, address)
key = (start, end)
routine = routines.setdefault(
key,
{
"start": start,
"end": end,
"accesses": [],
"roles": [],
"role_hint": "lcd_access",
},
)
routine["accesses"].append(address_to_access[address])
for routine in routines.values():
roles = sorted({str(access["role"]) for access in routine["accesses"]})
routine["roles"] = roles
routine["role_hint"] = _routine_role_hint(routine, loop_addresses)
return sorted(routines.values(), key=lambda item: int(item["start"]))
def _routine_span(ordered: list[Instruction], address: int) -> tuple[int, int]:
addresses = [ins.address for ins in ordered]
index = addresses.index(address)
start = ordered[index].address
cursor = index - 1
while cursor >= 0:
previous = ordered[cursor]
if previous.kind in {"return", "rte"}:
break
if previous.address + max(previous.size, 1) != start:
break
start = previous.address
cursor -= 1
end = ordered[index].address
cursor = index
while cursor < len(ordered):
current = ordered[cursor]
end = current.address
if current.kind in {"return", "rte"}:
break
next_index = cursor + 1
if next_index >= len(ordered):
break
next_address = ordered[next_index].address
if current.address + max(current.size, 1) != next_address:
break
cursor = next_index
return start, end
def _routine_role_hint(routine: Mapping[str, object], loop_addresses: set[int]) -> str:
access_addresses = {int(access["address"]) for access in routine.get("accesses", []) if isinstance(access, Mapping)}
roles = {str(role) for role in routine.get("roles", [])}
if access_addresses & loop_addresses and "lcd_data_write" in roles and "lcd_command_or_address_write" in roles:
return "lcd_wait_and_transfer"
if access_addresses & loop_addresses:
return "lcd_wait_ready"
if "lcd_data_write" in roles:
return "lcd_write_data"
if "lcd_command_or_address_write" in roles:
return "lcd_write_command_or_address"
if "lcd_data_read" in roles:
return "lcd_read_data"
return "lcd_access"
def _last_register(operands: str) -> str | None:
matches = list(REGISTER_RE.finditer(operands))
return matches[-1].group("reg") if matches else None
def _address_role(address: int) -> str:
if address == LCD_STATUS_CONTROL:
return "status/control register inferred from busy polling and command writes"
if address == LCD_DATA:
return "data register inferred from paired data reads/writes"
return "unknown"

386
h8536/lcd_text.py Normal file
View File

@@ -0,0 +1,386 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping
from difflib import SequenceMatcher
from .formatting import h16
from .model import Instruction
from .rom import Rom
DISPLAY_PRINTABLE = set(range(0x20, 0x7F))
DISPLAY_PUNCTUATION = set(b" ./:-+,%()[]")
DEFAULT_SEARCH_TERMS = ("CONNECT",)
MOV_IW_FIRST_OPCODE = 0x58
MOV_IW_LAST_OPCODE = 0x5F
def analyze_lcd_text(
rom: Rom,
instructions: Mapping[int, Instruction] | Iterable[Instruction] | None = None,
*,
start: int | None = None,
end: int | None = None,
search_terms: Iterable[str] = DEFAULT_SEARCH_TERMS,
max_candidates: int = 240,
) -> dict[str, object]:
"""Find likely fixed-width LCD/menu strings and their nearby raw xrefs.
The firmware stores some display text as inline menu/script records rather
than as plain null-terminated strings. This pass scans the ROM bytes
directly, then correlates likely text fields with decoded and raw immediate
address loads such as ``MOV:I.W #H'63D4, R0``.
"""
lower = 0 if start is None else max(0, start)
upper = rom.end if end is None else min(rom.end, end)
candidates = _unique_candidates(
[
*_ff_terminated_candidates(rom, lower, upper),
*_printable_run_candidates(rom, lower, upper),
],
)
candidates = sorted(candidates, key=lambda item: (-float(item["score"]), int(item["address"])))[:max_candidates]
candidates.sort(key=lambda item: int(item["address"]))
instruction_list = _instruction_sequence(instructions)
xrefs_by_address = _xref_map(rom, candidates, instruction_list)
for candidate in candidates:
xrefs = xrefs_by_address.get(int(candidate["address"]), [])
if xrefs:
candidate["xrefs"] = xrefs
candidate["xref_count"] = len(xrefs)
regions = _group_regions(candidates)
searches = [_search_term(rom, candidates, term) for term in search_terms]
return {
"strings": candidates,
"regions": regions,
"searches": searches,
"notes": [
"LCD text scan is byte-oriented and conservative; strings may be inline script fields.",
"Raw xrefs include MOV:I.W immediates to the string address and nearby record prefixes.",
],
}
def lcd_text_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
if not analysis:
return ""
for candidate in analysis.get("strings", []):
if not isinstance(candidate, Mapping):
continue
for xref in candidate.get("xrefs", []):
if isinstance(xref, Mapping) and int(xref.get("address", -1)) == address:
text = str(candidate.get("trimmed") or candidate.get("text") or "").strip()
return f"LCD text xref {h16(int(candidate['address']))} {text!r}"
return ""
def _instruction_sequence(
instructions: Mapping[int, Instruction] | Iterable[Instruction] | None,
) -> list[Instruction]:
if instructions is None:
return []
values = instructions.values() if isinstance(instructions, Mapping) else instructions
return sorted(values, key=lambda ins: ins.address)
def _ff_terminated_candidates(rom: Rom, start: int, end: int) -> list[dict[str, object]]:
candidates: list[dict[str, object]] = []
address = start
while address < end:
if rom.u8(address) not in DISPLAY_PRINTABLE:
address += 1
continue
text_start = address
raw = bytearray()
while address < end and rom.u8(address) in DISPLAY_PRINTABLE:
raw.append(rom.u8(address))
address += 1
ff_count = 0
cursor = address
while cursor < end and rom.u8(cursor) == 0xFF:
ff_count += 1
cursor += 1
if ff_count and _looks_like_lcd_text(raw):
candidates.append(_candidate(text_start, raw, "ff_terminated", ff_count=ff_count))
address = max(cursor, address + 1)
return candidates
def _printable_run_candidates(rom: Rom, start: int, end: int) -> list[dict[str, object]]:
candidates: list[dict[str, object]] = []
address = start
while address < end:
if rom.u8(address) not in DISPLAY_PRINTABLE:
address += 1
continue
text_start = address
raw = bytearray()
while address < end and rom.u8(address) in DISPLAY_PRINTABLE:
raw.append(rom.u8(address))
address += 1
if _looks_like_lcd_text(raw, allow_long=True):
candidates.append(_candidate(text_start, raw, "printable_run", ff_count=0))
address += 1
return candidates
def _candidate(address: int, raw: bytearray, kind: str, *, ff_count: int) -> dict[str, object]:
text = raw.decode("ascii", errors="replace")
trimmed = text.strip()
score = _display_score(raw, kind, ff_count)
payload: dict[str, object] = {
"address": address,
"length": len(raw),
"text": text,
"trimmed": trimmed,
"kind": kind,
"score": round(score, 3),
"confidence": _confidence(score),
}
if ff_count:
payload["ff_terminators"] = ff_count
if len(raw) > 32:
payload["segments"] = _fixed_width_segments(text)
return payload
def _unique_candidates(candidates: list[dict[str, object]]) -> list[dict[str, object]]:
by_key: dict[tuple[int, int], dict[str, object]] = {}
for candidate in candidates:
key = (int(candidate["address"]), int(candidate["length"]))
existing = by_key.get(key)
if existing is None or float(candidate["score"]) > float(existing["score"]):
by_key[key] = candidate
return list(by_key.values())
def _looks_like_lcd_text(raw: bytearray, *, allow_long: bool = False) -> bool:
if len(raw) < 4:
return False
if len(raw) > 80 and not allow_long:
return False
trimmed = bytes(raw).strip()
if len(trimmed) < 2:
return False
good = sum(1 for value in trimmed if _display_char_score(value) > 0)
return good / max(len(trimmed), 1) >= 0.78
def _display_char_score(value: int) -> float:
if 0x41 <= value <= 0x5A or 0x30 <= value <= 0x39:
return 1.0
if value == 0x20:
return 0.8
if value in DISPLAY_PUNCTUATION:
return 0.7
if 0x61 <= value <= 0x7A:
return 0.35
return 0.0
def _display_score(raw: bytearray, kind: str, ff_count: int) -> float:
trimmed = bytes(raw).strip()
if not trimmed:
return 0.0
char_score = sum(_display_char_score(value) for value in trimmed) / len(trimmed)
length_bonus = 0.15 if len(raw) in {8, 10, 16, 17, 18, 19, 20} else 0.0
terminator_bonus = min(ff_count, 3) * 0.08
kind_bonus = 0.08 if kind == "ff_terminated" else 0.0
long_penalty = 0.2 if len(raw) > 40 else 0.0
return max(0.0, char_score + length_bonus + terminator_bonus + kind_bonus - long_penalty)
def _confidence(score: float) -> str:
if score >= 1.05:
return "high"
if score >= 0.82:
return "medium"
return "low"
def _fixed_width_segments(text: str) -> list[dict[str, object]]:
segments: list[dict[str, object]] = []
for width in (10, 16, 18, 20):
if len(text) < width * 2:
continue
chunks = [text[index : index + width] for index in range(0, len(text), width)]
useful = [chunk.strip() for chunk in chunks if len(chunk.strip()) >= 2]
if len(useful) >= 2:
segments.append({"width": width, "chunks": chunks})
return segments[:3]
def _xref_map(
rom: Rom,
candidates: list[dict[str, object]],
instructions: list[Instruction],
) -> dict[int, list[dict[str, object]]]:
addresses = [int(candidate["address"]) for candidate in candidates]
target_to_address: dict[int, int] = {}
for address in addresses:
for delta in range(-4, 5):
target = address + delta
if 0 <= target <= 0xFFFF:
target_to_address.setdefault(target, address)
xrefs: dict[int, list[dict[str, object]]] = {address: [] for address in addresses}
_add_decoded_xrefs(xrefs, target_to_address, instructions)
_add_raw_mov_iw_xrefs(xrefs, target_to_address, rom)
return {address: refs for address, refs in xrefs.items() if refs}
def _add_decoded_xrefs(
xrefs: dict[int, list[dict[str, object]]],
target_to_address: Mapping[int, int],
instructions: list[Instruction],
) -> None:
for ins in instructions:
for target, candidate_address in target_to_address.items():
needle = f"H'{target:04X}"
if needle in ins.operands:
xrefs[candidate_address].append(
{
"address": ins.address,
"kind": "decoded_operand",
"target": target,
"delta": target - candidate_address,
"instruction": ins.text,
},
)
def _add_raw_mov_iw_xrefs(
xrefs: dict[int, list[dict[str, object]]],
target_to_address: Mapping[int, int],
rom: Rom,
) -> None:
data = rom.data
for address in range(0, max(len(data) - 2, 0)):
opcode = data[address]
if not MOV_IW_FIRST_OPCODE <= opcode <= MOV_IW_LAST_OPCODE:
continue
target = (data[address + 1] << 8) | data[address + 2]
candidate_address = target_to_address.get(target)
if candidate_address is None:
continue
register = f"R{opcode - MOV_IW_FIRST_OPCODE}"
xref: dict[str, object] = {
"address": address,
"kind": "raw_mov_iw",
"target": target,
"delta": target - candidate_address,
"register": register,
"instruction": f"MOV:I.W #{h16(target)}, {register}",
}
bsr = _following_bsr(data, address + 3)
if bsr:
xref["following_bsr"] = bsr
xrefs[candidate_address].append(xref)
def _following_bsr(data: bytes, address: int) -> dict[str, object] | None:
if address + 2 >= len(data) or data[address] != 0x1E:
return None
displacement = (data[address + 1] << 8) | data[address + 2]
if displacement & 0x8000:
displacement -= 0x10000
target = (address + 3 + displacement) & 0xFFFF
return {"address": address, "target": target, "instruction": f"BSR {h16(target)}"}
def _group_regions(candidates: list[dict[str, object]]) -> list[dict[str, object]]:
regions: list[dict[str, object]] = []
current: list[dict[str, object]] = []
previous_end: int | None = None
for candidate in candidates:
address = int(candidate["address"])
length = int(candidate["length"])
if current and previous_end is not None and address - previous_end > 0x80:
_append_region(regions, current)
current = []
current.append(candidate)
previous_end = address + length
if current:
_append_region(regions, current)
return regions
def _append_region(regions: list[dict[str, object]], candidates: list[dict[str, object]]) -> None:
if len(candidates) < 2:
return
start = int(candidates[0]["address"])
end = max(int(item["address"]) + int(item["length"]) for item in candidates)
regions.append(
{
"start": start,
"end": end,
"count": len(candidates),
"samples": [str(item.get("trimmed") or item.get("text")) for item in candidates[:8]],
},
)
def _search_term(rom: Rom, candidates: list[dict[str, object]], term: str) -> dict[str, object]:
raw = term.encode("ascii", errors="ignore")
literal_hits = _literal_hits(rom.data, raw)
folded = term.upper()
candidate_hits = [
{
"address": int(candidate["address"]),
"text": candidate["text"],
"trimmed": candidate["trimmed"],
}
for candidate in candidates
if folded in str(candidate.get("text", "")).upper()
]
near_matches = _near_matches(candidates, term)
return {
"term": term,
"literal_hits": literal_hits,
"candidate_hits": candidate_hits,
"near_matches": near_matches,
"status": "found" if literal_hits or candidate_hits else "not_found",
}
def _literal_hits(data: bytes, needle: bytes) -> list[int]:
if not needle:
return []
hits: list[int] = []
start = 0
upper_data = data.upper()
upper_needle = needle.upper()
while True:
index = upper_data.find(upper_needle, start)
if index < 0:
return hits
hits.append(index)
start = index + 1
def _near_matches(candidates: list[dict[str, object]], term: str) -> list[dict[str, object]]:
normalized_term = _normalize_text(term)
matches: list[dict[str, object]] = []
for candidate in candidates:
normalized = _normalize_text(str(candidate.get("trimmed") or candidate.get("text") or ""))
if not normalized:
continue
ratio = SequenceMatcher(None, normalized_term, normalized).ratio()
if ratio >= 0.34:
matches.append(
{
"address": int(candidate["address"]),
"text": candidate["text"],
"trimmed": candidate["trimmed"],
"score": round(ratio, 3),
},
)
matches.sort(key=lambda item: (-float(item["score"]), int(item["address"])))
return matches[:12]
def _normalize_text(text: str) -> str:
return "".join(char for char in text.upper() if "A" <= char <= "Z" or "0" <= char <= "9")

View File

@@ -904,6 +904,14 @@ def _metadata_comments(ins: JsonObject) -> list[str]:
if isinstance(indirect, dict) and indirect.get("summary"):
comments.append(str(indirect["summary"]))
lcd_text = ins.get("lcd_text")
if isinstance(lcd_text, dict) and lcd_text.get("comment"):
comments.append(str(lcd_text["comment"]))
for lcd_item in ins.get("lcd_driver", []):
if isinstance(lcd_item, dict) and lcd_item.get("summary"):
comments.append(str(lcd_item["summary"]))
dataflow = ins.get("dataflow")
if isinstance(dataflow, dict):
changes = dataflow.get("changes")

View File

@@ -8,6 +8,8 @@ from .dataflow import state_for_instruction
from .dtc import DtcEndpointInfo, DtcRegisterInfo
from .formatting import h16, label_for
from .indirect import indirect_comment_for_instruction, indirect_metadata_for_instruction
from .lcd_driver import lcd_comment_for_instruction, lcd_metadata_for_instruction
from .lcd_text import lcd_text_comment_for_instruction
from .memory import MEMORY_REGIONS, region_for
from .model import Instruction
from .peripheral_access import (
@@ -118,6 +120,103 @@ def _dataflow_comment(analysis: dict[str, object] | None, address: int) -> str:
return "dataflow " + ", ".join(parts[:4]) + suffix
def _lcd_text_lines(lcd_text: dict[str, object] | None) -> list[str]:
if not lcd_text:
return []
strings = lcd_text.get("strings", [])
regions = lcd_text.get("regions", [])
searches = lcd_text.get("searches", [])
if not strings and not regions and not searches:
return []
lines = ["; LCD/Text Scan"]
for search in (searches if isinstance(searches, list) else []):
if not isinstance(search, dict):
continue
hits = len(search.get("literal_hits", [])) + len(search.get("candidate_hits", []))
status = "found" if hits else "not literal"
lines.append(f"; search {search.get('term')!r}: {status}, hits={hits}")
near = search.get("near_matches", [])
if isinstance(near, list) and near:
sample = ", ".join(f"{h16(int(item['address']))} {item['trimmed']!r}" for item in near[:4])
lines.append(f"; near: {sample}")
if isinstance(regions, list) and regions:
lines.append("; LCD text regions")
for region in regions[:12]:
if not isinstance(region, dict):
continue
samples = ", ".join(repr(sample) for sample in region.get("samples", [])[:4])
lines.append(
f"; region {h16(int(region['start']))}-{h16(int(region['end']))} "
f"count={region['count']:<3} {samples}",
)
if len(regions) > 12:
lines.append(f"; ... {len(regions) - 12} more LCD text regions")
if isinstance(strings, list) and strings:
lines.append("; LCD text candidates")
shown = 0
for item in strings:
if not isinstance(item, dict):
continue
if item.get("confidence") == "low" and not item.get("xref_count"):
continue
xrefs = f" xrefs={item['xref_count']}" if item.get("xref_count") else ""
lines.append(
f"; text {h16(int(item['address'])):<8} len={item['length']:<3} "
f"{item['confidence']:<6} {str(item['trimmed'])!r}{xrefs}",
)
shown += 1
if shown >= 48:
break
if len(strings) > shown:
lines.append(f"; ... {len(strings) - shown} more LCD text candidates")
lines.append("")
return lines
def _lcd_driver_lines(lcd_driver: dict[str, object] | None) -> list[str]:
if not lcd_driver:
return []
accesses = lcd_driver.get("accesses", [])
loops = lcd_driver.get("polling_loops", [])
routines = lcd_driver.get("routines", [])
if not accesses and not loops and not routines:
return []
lines = ["; LCD Driver Candidates"]
for address_info in lcd_driver.get("addresses", []):
if not isinstance(address_info, dict):
continue
lines.append(
f"; {h16(int(address_info['address']))} {address_info['name']:<18} {address_info['role']}",
)
if isinstance(routines, list) and routines:
lines.append("; LCD routines")
for routine in routines[:16]:
if not isinstance(routine, dict):
continue
roles = ", ".join(str(role) for role in routine.get("roles", []))
lines.append(
f"; routine {h16(int(routine['start']))}-{h16(int(routine['end']))} "
f"{routine['role_hint']:<24} {roles}",
)
if len(routines) > 16:
lines.append(f"; ... {len(routines) - 16} more LCD routines")
if isinstance(loops, list) and loops:
lines.append("; LCD busy loops")
for loop in loops[:16]:
if not isinstance(loop, dict):
continue
lines.append(
f"; loop {h16(int(loop['read_address']))}->{h16(int(loop['branch_address']))} "
f"{loop['summary']}",
)
lines.append("")
return lines
def format_listing(
rom_path: Path,
rom: Rom,
@@ -135,6 +234,8 @@ def format_listing(
indirect_flow: dict[str, object] | None = None,
dataflow: dict[str, object] | None = None,
symbols: dict[str, object] | None = None,
lcd_text: dict[str, object] | None = None,
lcd_driver: dict[str, object] | None = None,
) -> str:
lines: list[str] = []
lines.append("; H8/536 ROM disassembly")
@@ -149,6 +250,7 @@ def format_listing(
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.")
lines.append("; - LCD inference treats E-clock H'F200/H'F201 accesses as status/control and data candidates.")
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:
@@ -192,6 +294,8 @@ def format_listing(
lines.append("")
lines.extend(_symbol_lines(symbols))
lines.extend(_lcd_text_lines(lcd_text))
lines.extend(_lcd_driver_lines(lcd_driver))
if timing_summary:
lines.extend(format_timing_summary(timing_summary))
@@ -210,6 +314,8 @@ def format_listing(
sci_comment_for_instruction(sci_analysis, address),
peripheral_comment_for_instruction(peripheral_access, address),
indirect_comment_for_instruction(indirect_flow, address),
lcd_text_comment_for_instruction(lcd_text, address),
lcd_comment_for_instruction(lcd_driver, address),
_dataflow_comment(dataflow, address),
_reference_comment(ins, symbols) if not ins.comment else "",
cycle_comment(ins.cycles) if show_cycles else "",
@@ -236,6 +342,8 @@ def write_json(
indirect_flow: dict[str, object] | None = None,
dataflow: dict[str, object] | None = None,
symbols: dict[str, object] | None = None,
lcd_text: dict[str, object] | None = None,
lcd_driver: dict[str, object] | None = None,
) -> None:
payload = {
"vectors": [
@@ -261,8 +369,10 @@ def write_json(
"indirect_flow": indirect_flow or {"sites": []},
"dataflow": _dataflow_json_payload(dataflow),
"symbols": symbols or {"symbols": [], "by_address": {}},
"lcd_text": lcd_text or {"strings": [], "regions": [], "searches": []},
"lcd_driver": lcd_driver or {"accesses": [], "polling_loops": [], "routines": []},
"instructions": [
_instruction_payload(ins, sci_analysis, peripheral_access, indirect_flow, dataflow, symbols)
_instruction_payload(ins, sci_analysis, peripheral_access, indirect_flow, dataflow, symbols, lcd_text, lcd_driver)
for ins in (instructions[addr] for addr in sorted(instructions))
],
}
@@ -319,6 +429,8 @@ def _instruction_payload(
indirect_flow: dict[str, object] | None = None,
dataflow: dict[str, object] | None = None,
symbols: dict[str, object] | None = None,
lcd_text: dict[str, object] | None = None,
lcd_driver: dict[str, object] | None = None,
) -> dict[str, object]:
payload: dict[str, object] = {
"address": ins.address,
@@ -355,6 +467,12 @@ def _instruction_payload(
dataflow_metadata = _dataflow_instruction_payload(dataflow, ins.address)
if dataflow_metadata:
payload["dataflow"] = dataflow_metadata
lcd_text_comment = lcd_text_comment_for_instruction(lcd_text, ins.address)
if lcd_text_comment:
payload["lcd_text"] = {"comment": lcd_text_comment}
lcd_driver_metadata = lcd_metadata_for_instruction(lcd_driver, ins.address)
if lcd_driver_metadata:
payload["lcd_driver"] = lcd_driver_metadata
return payload

446
h8536/sci_protocol.py Normal file
View File

@@ -0,0 +1,446 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from .formatting import h16, parse_int
from .model import Instruction
MANUAL_REFERENCES = [
"Manual/0900766b802125d0.md:15748 SCI register map for RDR/TDR/SCR/SSR",
"Manual/0900766b802125d0.md:15794 RDR stores received data and is CPU-readable",
"Manual/0900766b802125d0.md:15823 TDR holds the next byte to transmit",
"Manual/0900766b802125d0.md:15976 SCR.TIE enables/disables TXI on TDRE",
"Manual/0900766b802125d0.md:15993 SCR.RIE enables RXI and ERI",
"Manual/0900766b802125d0.md:16008 SCR.TE enables the transmitter",
"Manual/0900766b802125d0.md:16028 SCR.RE enables the receiver",
"Manual/0900766b802125d0.md:16090 SSR flags are cleared by writing zero",
"Manual/0900766b802125d0.md:16100 SSR.TDRE means TDR can accept the next byte",
"Manual/0900766b802125d0.md:16116 SSR.RDRF means received data reached RDR",
"Manual/0900766b802125d0.md:16127 SSR.ORER reports receive overrun",
"Manual/0900766b802125d0.md:16140 SSR.FER reports framing errors",
"Manual/0900766b802125d0.md:16147 SSR.PER reports parity errors",
]
@dataclass(frozen=True)
class SciRegister:
channel: str
name: str
address: int
SCI_REGISTERS: tuple[SciRegister, ...] = (
SciRegister("SCI1", "SCR", 0xFEDA),
SciRegister("SCI1", "TDR", 0xFEDB),
SciRegister("SCI1", "SSR", 0xFEDC),
SciRegister("SCI1", "RDR", 0xFEDD),
SciRegister("SCI2", "SCR", 0xFEF2),
SciRegister("SCI2", "TDR", 0xFEF3),
SciRegister("SCI2", "SSR", 0xFEF4),
SciRegister("SCI2", "RDR", 0xFEF5),
)
SCI_REGISTER_BY_ADDRESS = {register.address: register for register in SCI_REGISTERS}
SCI_CHANNELS = ("SCI1", "SCI2")
SCR_CONTROL_BITS = {
7: ("TIE", "TX interrupt", "TXI"),
6: ("RIE", "RX/ERI interrupts", "RXI and ERI"),
5: ("TE", "transmitter", "TXD output"),
4: ("RE", "receiver", "RXD input"),
}
SSR_FLAGS = {
7: ("TDRE", "transmit data register empty"),
6: ("RDRF", "receive-data-full"),
5: ("ORER", "overrun error"),
4: ("FER", "framing error"),
3: ("PER", "parity error"),
}
SSR_CLEAR_ACTIONS = {
7: ("clear_tdre", "clear {channel} transmit data register empty flag (TDRE)"),
6: ("clear_rdrf", "clear {channel} receive-data-full flag (RDRF)"),
5: ("clear_orer", "clear {channel} overrun error flag (ORER)"),
4: ("clear_fer", "clear {channel} framing error flag (FER)"),
3: ("clear_per", "clear {channel} parity error flag (PER)"),
}
def analyze_sci_protocol(
instructions: Mapping[int, Instruction] | Iterable[Instruction],
) -> dict[str, object]:
"""Identify SCI protocol-level reads, writes, clears, and polling waits."""
ordered = _instruction_sequence(instructions)
annotations: dict[int, list[str]] = {}
instruction_metadata: dict[int, list[dict[str, object]]] = {}
channels: dict[str, dict[str, list[dict[str, object]]]] = {
channel: {"events": []} for channel in SCI_CHANNELS
}
events: list[dict[str, object]] = []
def record(event: dict[str, object]) -> None:
public = dict(event)
events.append(public)
channel = str(public["channel"])
channels[channel]["events"].append(public)
instruction_metadata.setdefault(int(public["address"]), []).append(public)
comment = public.get("comment")
if comment:
parts = annotations.setdefault(int(public["address"]), [])
text = str(comment)
if text not in parts:
parts.append(text)
tdre_wait_events = _tdre_wait_events_by_address(ordered)
for ins in ordered:
for event in _instruction_events(ins):
record(event)
for event in tdre_wait_events.get(ins.address, []):
record(event)
return {
"manual_references": MANUAL_REFERENCES,
"channels": channels,
"events": events,
"annotations": {
address: "; ".join(parts)
for address, parts in sorted(annotations.items())
},
"instructions": instruction_metadata,
}
def sci_protocol_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_protocol_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 list(metadata) if isinstance(metadata, list) else []
def sci_protocol_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]:
if not analysis:
return {
"manual_references": MANUAL_REFERENCES,
"channels": {channel: {"events": []} for channel in SCI_CHANNELS},
"events": [],
}
return {
"manual_references": analysis.get("manual_references", MANUAL_REFERENCES),
"channels": analysis.get("channels", {}),
"events": analysis.get("events", []),
}
def _instruction_events(ins: Instruction) -> list[dict[str, object]]:
events: list[dict[str, object]] = []
for register in _referenced_sci_registers(ins):
if register.name == "TDR":
events.extend(_tdr_events(ins, register))
elif register.name == "RDR":
events.extend(_rdr_events(ins, register))
elif register.name == "SSR":
events.extend(_ssr_events(ins, register))
elif register.name == "SCR":
events.extend(_scr_events(ins, register))
return events
def _tdr_events(ins: Instruction, register: SciRegister) -> list[dict[str, object]]:
if _access_direction(ins, register.address) != "write":
return []
return [
_event(
ins,
register,
action="write_tdr",
comment=f"write RS232/SCI byte to {register.channel} TDR for transmission",
),
]
def _rdr_events(ins: Instruction, register: SciRegister) -> list[dict[str, object]]:
if _access_direction(ins, register.address) != "read":
return []
return [
_event(
ins,
register,
action="read_rdr",
comment=f"read {register.channel} received byte from RDR",
),
]
def _ssr_events(ins: Instruction, register: SciRegister) -> list[dict[str, object]]:
if _access_direction(ins, register.address) != "write":
return []
bit = _immediate_bit(ins.operands)
if _mnemonic_root(ins.mnemonic) == "BCLR" and bit in SSR_CLEAR_ACTIONS:
action, comment_template = SSR_CLEAR_ACTIONS[bit]
flag, description = SSR_FLAGS[bit]
return [
_event(
ins,
register,
action=action,
bit=bit,
flag=flag,
description=description,
comment=comment_template.format(channel=register.channel),
),
]
value = _written_immediate_value(ins)
if value is None:
return []
events: list[dict[str, object]] = []
for clear_bit, (action, comment_template) in SSR_CLEAR_ACTIONS.items():
if value & (1 << clear_bit):
continue
flag, description = SSR_FLAGS[clear_bit]
events.append(
_event(
ins,
register,
action=action,
bit=clear_bit,
flag=flag,
description=description,
value=value,
comment=comment_template.format(channel=register.channel),
),
)
return events
def _scr_events(ins: Instruction, register: SciRegister) -> list[dict[str, object]]:
if _access_direction(ins, register.address) != "write":
return []
bit = _immediate_bit(ins.operands)
root = _mnemonic_root(ins.mnemonic)
if root in {"BCLR", "BSET"} and bit in SCR_CONTROL_BITS:
return [_scr_bit_event(ins, register, bit, enabled=root == "BSET")]
value = _written_immediate_value(ins)
if value is None:
return []
return [
_scr_bit_event(ins, register, bit_number, enabled=bool(value & (1 << bit_number)), value=value)
for bit_number in SCR_CONTROL_BITS
]
def _scr_bit_event(
ins: Instruction,
register: SciRegister,
bit: int,
enabled: bool,
value: int | None = None,
) -> dict[str, object]:
bit_name, noun, interrupt_source = SCR_CONTROL_BITS[bit]
verb = "enable" if enabled else "disable"
action_noun = noun.replace("/", "_").replace(" ", "_").lower()
if bit == 7:
comment = f"{verb} {register.channel} TX interrupt (TIE)"
elif bit == 6:
comment = f"{verb} {register.channel} receive and receive-error interrupts (RIE)"
elif bit == 5:
comment = f"{verb} {register.channel} transmitter (TE)"
else:
comment = f"{verb} {register.channel} receiver (RE)"
return _event(
ins,
register,
action=f"{verb}_{action_noun}",
bit=bit,
bit_name=bit_name,
enabled=enabled,
interrupt_source=interrupt_source,
value=value,
comment=comment,
)
def _tdre_wait_events_by_address(ordered: list[Instruction]) -> dict[int, list[dict[str, object]]]:
events_by_address: dict[int, list[dict[str, object]]] = {}
for index, ins in enumerate(ordered):
register = _single_sci_register(ins)
if register is None or register.name != "SSR":
continue
if _mnemonic_root(ins.mnemonic) != "BTST" or _immediate_bit(ins.operands) != 7:
continue
branch = _next_beq_back_to(ordered, index, ins.address)
if branch is None:
continue
events_by_address.setdefault(ins.address, []).append(
_event(
ins,
register,
action="wait_for_tdre",
bit=7,
flag="TDRE",
branch_address=branch.address,
branch_target=ins.address,
comment=f"wait for {register.channel} transmit data register empty (TDRE=1)",
),
)
events_by_address.setdefault(branch.address, []).append(
_event(
branch,
register,
action="tdre_wait_branch",
bit=7,
flag="TDRE",
test_address=ins.address,
branch_target=ins.address,
comment=f"repeat {register.channel} transmit-empty wait while TDRE=0",
),
)
return events_by_address
def _next_beq_back_to(ordered: list[Instruction], index: int, target: int) -> Instruction | None:
for candidate in ordered[index + 1 : index + 5]:
if _mnemonic_root(candidate.mnemonic) != "BEQ":
continue
if target in candidate.targets:
return candidate
return None
def _event(
ins: Instruction,
register: SciRegister,
*,
action: str,
comment: str,
**extra: object,
) -> dict[str, object]:
event: dict[str, object] = {
"address": ins.address,
"instruction": ins.text,
"action": action,
"channel": register.channel,
"register": register.name,
"register_address": register.address,
"register_address_hex": h16(register.address),
"comment": comment,
"manual": MANUAL_REFERENCES,
}
event.update({key: value for key, value in extra.items() if value is not None})
return event
def _instruction_sequence(
instructions: Mapping[int, Instruction] | Iterable[Instruction],
) -> list[Instruction]:
values = instructions.values() if isinstance(instructions, Mapping) else instructions
return sorted(values, key=lambda ins: ins.address)
def _referenced_sci_registers(ins: Instruction) -> list[SciRegister]:
registers: list[SciRegister] = []
seen: set[int] = set()
for address in ins.references:
register = SCI_REGISTER_BY_ADDRESS.get(address)
if register is None or register.address in seen:
continue
seen.add(register.address)
registers.append(register)
return registers
def _single_sci_register(ins: Instruction) -> SciRegister | None:
registers = _referenced_sci_registers(ins)
return registers[0] if len(registers) == 1 else None
def _access_direction(ins: Instruction, address: int) -> str | None:
root = _mnemonic_root(ins.mnemonic)
if root in {"BTST", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}:
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"}:
source, destination = _source_destination_operands(ins.operands)
if _operand_mentions_address_or_register(destination, address):
return "write"
if _operand_mentions_address_or_register(source, address):
return "read"
if root in {"MOV:L", "MOV:F"}:
return "read"
if root == "STC":
return "write"
if root == "LDC":
return "read"
return None
def _source_destination_operands(operands: str) -> tuple[str, str]:
if "," not in operands:
operand = operands.strip()
return "", operand
source, destination = operands.rsplit(",", 1)
return source.strip(), destination.strip()
def _operand_mentions_address_or_register(operand: str, address: int) -> bool:
operand_upper = operand.upper()
register = SCI_REGISTER_BY_ADDRESS.get(address)
if register and f"{register.channel}_{register.name}" in operand_upper:
return True
return f"H'{address:04X}" in operand_upper or f"0X{address:04X}" in operand_upper
def _immediate_bit(operands: str) -> int | None:
source, _destination = _source_destination_operands(operands)
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 _written_immediate_value(ins: Instruction) -> int | None:
root = _mnemonic_root(ins.mnemonic)
if root == "CLR":
return 0
if root not in {"MOV:G", "MOV:S"}:
return None
source, _destination = _source_destination_operands(ins.operands)
if not source.startswith("#"):
return None
try:
return parse_int(source[1:]) & 0xFF
except ValueError:
return None
def _mnemonic_root(mnemonic: str) -> str:
return mnemonic.rsplit(".", 1)[0].upper()

View File

@@ -0,0 +1,74 @@
import json
import unittest
from h8536.board_profile import analyze_board_profile, board_comment_for_instruction
from h8536.model import Instruction
def ins(address: int, mnemonic: str, operands: str, references: list[int]) -> Instruction:
return Instruction(address, b"\x00", mnemonic, operands, references=references)
class BoardProfileTest(unittest.TestCase):
def test_profile_records_manual_pin_table_and_board_traces(self):
analysis = analyze_board_profile({})
self.assertEqual(analysis["board"], "sony_rcp_tx7")
self.assertIn("Manual/0900766b802125d0.md:2417", " ".join(analysis["manual_references"]))
self.assertIn("Manual/0900766b802125d0.md:2418", " ".join(analysis["manual_references"]))
traces = analysis["traces"]
self.assertEqual(traces[0]["h8_pin"], 66)
self.assertEqual(traces[0]["max202_pin"], 11)
self.assertEqual(traces[1]["h8_pin"], 67)
self.assertEqual(traces[1]["max202_pin"], 12)
json.dumps(analysis)
def test_sci1_init_and_scr_comments_identify_rs232_max202_path(self):
instructions = {
0x1000: ins(0x1000, "MOV:G.W", "#H'0407, @SCI1_SMR", [0xFED8]),
0x1004: ins(0x1004, "MOV:G.B", "#H'30, @SCI1_SCR", [0xFEDA]),
}
analysis = analyze_board_profile(instructions)
self.assertIn("SCI1 SMR serial init for traced RS232/MAX202 path", board_comment_for_instruction(analysis, 0x1000))
self.assertIn("SCI1 BRR serial init for traced RS232/MAX202 path", board_comment_for_instruction(analysis, 0x1000))
scr_comment = board_comment_for_instruction(analysis, 0x1004)
self.assertIn("SCI1 SCR write TE=1 RE=1", scr_comment)
self.assertIn("P95/TXD pin 66 to MAX202 pin 11", scr_comment)
self.assertIn("P96/RXD pin 67 to MAX202 pin 12", scr_comment)
def test_sci1_data_and_status_registers_annotate_traced_path(self):
instructions = {
0x1010: ins(0x1010, "BTST.B", "#7, @SCI1_SSR", [0xFEDC]),
0x1012: ins(0x1012, "MOV:G.B", "R0L, @SCI1_TDR", [0xFEDB]),
0x1014: ins(0x1014, "MOV:G.B", "@SCI1_RDR, R0L", [0xFEDD]),
}
analysis = analyze_board_profile(instructions)
self.assertIn("SCI1 SSR status for traced RS232/MAX202 path", board_comment_for_instruction(analysis, 0x1010))
self.assertIn("H8 pin 66 P95/TXD -> MAX202 pin 11", board_comment_for_instruction(analysis, 0x1012))
self.assertIn("MAX202 pin 12 -> H8 pin 67 P96/RXD", board_comment_for_instruction(analysis, 0x1014))
def test_sci2_disabled_comments_say_it_is_not_traced_path(self):
instructions = {
0x2000: ins(0x2000, "MOV:G.B", "#H'80, @SYSCR2", [0xFEFD]),
0x2004: ins(0x2004, "MOV:G.B", "#H'30, @SCI2_SCR", [0xFEF2]),
}
analysis = analyze_board_profile(instructions)
syscr2_comment = board_comment_for_instruction(analysis, 0x2000)
self.assertIn("P9SCI2E=0", syscr2_comment)
self.assertIn("SCI2 is not the traced MAX202 path", syscr2_comment)
sci2_comment = board_comment_for_instruction(analysis, 0x2004)
self.assertIn("SCI2 SCR write; not the traced MAX202 path", sci2_comment)
self.assertIn("P9SCI2E=0 disables SCI2 pins P92/P93/P94", sci2_comment)
self.assertFalse(analysis["channels"]["SCI2"]["traced_to_max202"])
self.assertFalse(analysis["channels"]["SCI2"]["p9sci2e"])
if __name__ == "__main__":
unittest.main()

44
tests/test_lcd_driver.py Normal file
View File

@@ -0,0 +1,44 @@
import unittest
from h8536.lcd_driver import analyze_lcd_driver, lcd_comment_for_instruction
from h8536.model import Instruction
class LcdDriverTest(unittest.TestCase):
def test_detects_e_clock_lcd_accesses_and_busy_poll_loop(self):
instructions = {
0x3F4A: Instruction(
0x3F4A,
b"\x15\xF2\x00\x00\x80",
"MOVFPE.B",
"@H'F200, R0",
references=[0xF200],
),
0x3F4F: Instruction(0x3F4F, b"\xA0\xF7", "BTST.B", "#7, R0"),
0x3F51: Instruction(0x3F51, b"\x26\xF7", "BNE", "loc_3F4A", kind="branch", targets=[0x3F4A]),
0x3F53: Instruction(0x3F53, b"\x15\xF2\x00\x00\x94", "MOVTPE.B", "R4, @H'F200", references=[0xF200]),
0x3F58: Instruction(0x3F58, b"\x15\xF2\x01\x00\x94", "MOVTPE.B", "R4, @H'F201", references=[0xF201]),
0x3F5D: Instruction(0x3F5D, b"\x19", "RTS", kind="return", fallthrough=False),
}
analysis = analyze_lcd_driver(instructions)
self.assertEqual(len(analysis["accesses"]), 3)
self.assertEqual(analysis["polling_loops"][0]["read_address"], 0x3F4A)
self.assertEqual(analysis["routines"][0]["role_hint"], "lcd_wait_and_transfer")
self.assertIn("LCD busy-flag poll", lcd_comment_for_instruction(analysis, 0x3F51))
self.assertIn("LCD data write", lcd_comment_for_instruction(analysis, 0x3F58))
def test_regex_fallback_detects_operands_without_references(self):
instructions = {
0x0100: Instruction(0x0100, b"", "MOVTPE.B", "R1, @H'F201"),
}
analysis = analyze_lcd_driver(instructions)
self.assertEqual(analysis["accesses"][0]["lcd_address"], 0xF201)
self.assertEqual(analysis["accesses"][0]["role"], "lcd_data_write")
if __name__ == "__main__":
unittest.main()

55
tests/test_lcd_text.py Normal file
View File

@@ -0,0 +1,55 @@
import unittest
from h8536.lcd_text import analyze_lcd_text, lcd_text_comment_for_instruction
from h8536.model import Instruction
from h8536.rom import Rom
class LcdTextTest(unittest.TestCase):
def test_finds_ff_terminated_menu_strings_and_raw_mov_xrefs(self):
data = bytearray([0x00] * 0x2200)
data[0x1000:0x100D] = b"OPERATION " + b"\xFF\xFF\xFF"
data[0x100D:0x1013] = bytes.fromhex("58 0F FD 1E 00 1D")
data[0x1030:0x103D] = b" PAINT " + b"\xFF\xFF\xFF"
data[0x103D:0x1040] = bytes.fromhex("58 10 30")
analysis = analyze_lcd_text(Rom(bytes(data)), search_terms=("CONNECT",))
by_text = {item["trimmed"]: item for item in analysis["strings"]}
self.assertIn("OPERATION", by_text)
self.assertIn("PAINT", by_text)
operation = by_text["OPERATION"]
self.assertEqual(operation["kind"], "ff_terminated")
self.assertEqual(operation["ff_terminators"], 3)
self.assertEqual(operation["xrefs"][0]["kind"], "raw_mov_iw")
self.assertEqual(operation["xrefs"][0]["target"], 0x0FFD)
self.assertEqual(operation["xrefs"][0]["delta"], -3)
self.assertEqual(operation["xrefs"][0]["following_bsr"]["target"], 0x1030)
self.assertEqual(analysis["searches"][0]["status"], "not_found")
def test_groups_nearby_strings_into_regions(self):
data = bytearray([0x00] * 0x1400)
data[0x1200:0x120D] = b" LOCK " + b"\xFF\xFF\xFF"
data[0x1240:0x124D] = b"IRIS/M.BLK" + b"\xFF\xFF\xFF"
analysis = analyze_lcd_text(Rom(bytes(data)))
self.assertEqual(len(analysis["regions"]), 1)
self.assertEqual(analysis["regions"][0]["count"], 2)
self.assertIn("LOCK", analysis["regions"][0]["samples"])
def test_decoded_operand_xref_comment(self):
data = bytearray([0x00] * 0x1300)
data[0x1100:0x110D] = b"CONNECT? " + b"\xFF\xFF\xFF"
instructions = {
0x0100: Instruction(0x0100, b"\x58\x11\x00", "MOV:I.W", "#H'1100, R0"),
}
analysis = analyze_lcd_text(Rom(bytes(data)), instructions, search_terms=("CONNECT",))
self.assertEqual(analysis["searches"][0]["status"], "found")
self.assertIn("LCD text xref", lcd_text_comment_for_instruction(analysis, 0x0100))
if __name__ == "__main__":
unittest.main()

148
tests/test_sci_protocol.py Normal file
View File

@@ -0,0 +1,148 @@
import unittest
from h8536.model import Instruction
from h8536.sci_protocol import (
analyze_sci_protocol,
sci_protocol_comment_for_instruction,
sci_protocol_json_payload,
sci_protocol_metadata_for_instruction,
)
def ins(
address: int,
mnemonic: str,
operands: str = "",
references: list[int] | None = None,
kind: str = "normal",
targets: list[int] | None = None,
) -> Instruction:
return Instruction(
address,
b"",
mnemonic,
operands,
kind=kind,
targets=targets or [],
references=references or [],
)
class SciProtocolTest(unittest.TestCase):
def test_tdre_polling_loop_is_wait_for_transmit_data_register_empty(self):
instructions = {
0x2000: ins(0x2000, "BTST.B", "#7, @SCI1_SSR", references=[0xFEDC]),
0x2004: ins(0x2004, "BEQ", "loc_2000", kind="branch", targets=[0x2000]),
}
analysis = analyze_sci_protocol(instructions)
self.assertIn(
"wait for SCI1 transmit data register empty (TDRE=1)",
sci_protocol_comment_for_instruction(analysis, 0x2000),
)
self.assertIn(
"repeat SCI1 transmit-empty wait while TDRE=0",
sci_protocol_comment_for_instruction(analysis, 0x2004),
)
metadata = sci_protocol_metadata_for_instruction(analysis, 0x2000)[0]
self.assertEqual(metadata["action"], "wait_for_tdre")
self.assertEqual(metadata["branch_address"], 0x2004)
def test_write_to_tdr_is_rs232_sci_transmit_byte(self):
instructions = {
0x2100: ins(0x2100, "MOV:G.B", "R0, @SCI1_TDR", references=[0xFEDB]),
}
analysis = analyze_sci_protocol(instructions)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2100),
"write RS232/SCI byte to SCI1 TDR for transmission",
)
self.assertEqual(analysis["channels"]["SCI1"]["events"][0]["register"], "TDR")
def test_scr_tie_bit_set_and_clear_enable_disable_tx_interrupt(self):
instructions = {
0x2200: ins(0x2200, "BSET.B", "#7, @SCI1_SCR", references=[0xFEDA]),
0x2204: ins(0x2204, "BCLR.B", "#7, @SCI1_SCR", references=[0xFEDA]),
}
analysis = analyze_sci_protocol(instructions)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2200),
"enable SCI1 TX interrupt (TIE)",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2204),
"disable SCI1 TX interrupt (TIE)",
)
def test_receive_path_clears_rdrf_then_reads_received_byte(self):
instructions = {
0x2300: ins(0x2300, "BCLR.B", "#6, @SCI1_SSR", references=[0xFEDC]),
0x2304: ins(0x2304, "MOV:G.B", "@SCI1_RDR, R0", references=[0xFEDD]),
}
analysis = analyze_sci_protocol(instructions)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2300),
"clear SCI1 receive-data-full flag (RDRF)",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2304),
"read SCI1 received byte from RDR",
)
def test_receive_error_handler_clears_overrun_framing_and_parity_errors(self):
instructions = {
0x2400: ins(0x2400, "BCLR.B", "#5, @SCI1_SSR", references=[0xFEDC]),
0x2404: ins(0x2404, "BCLR.B", "#4, @SCI1_SSR", references=[0xFEDC]),
0x2408: ins(0x2408, "BCLR.B", "#3, @SCI1_SSR", references=[0xFEDC]),
}
analysis = analyze_sci_protocol(instructions)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2400),
"clear SCI1 overrun error flag (ORER)",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2404),
"clear SCI1 framing error flag (FER)",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2408),
"clear SCI1 parity error flag (PER)",
)
def test_immediate_scr_write_reports_protocol_control_bits(self):
instructions = {
0x2500: ins(0x2500, "MOV:G.B", "#H'B0, @SCI2_SCR", references=[0xFEF2]),
}
analysis = analyze_sci_protocol(instructions)
comment = sci_protocol_comment_for_instruction(analysis, 0x2500)
self.assertIn("enable SCI2 TX interrupt (TIE)", comment)
self.assertIn("disable SCI2 receive and receive-error interrupts (RIE)", comment)
self.assertIn("enable SCI2 transmitter (TE)", comment)
self.assertIn("enable SCI2 receiver (RE)", comment)
def test_json_payload_keeps_events_without_instruction_index(self):
instructions = {
0x2600: ins(0x2600, "MOV:G.B", "R0, @SCI2_TDR", references=[0xFEF3]),
}
analysis = analyze_sci_protocol(instructions)
payload = sci_protocol_json_payload(analysis)
self.assertIn("manual_references", payload)
self.assertEqual(payload["events"][0]["action"], "write_tdr")
self.assertEqual(payload["channels"]["SCI2"]["events"][0]["register"], "TDR")
if __name__ == "__main__":
unittest.main()