From c80ea695dc9b2e790093760cfcb0fe51c924553b Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 15:44:54 +1000 Subject: [PATCH] Serial TX/RX builder --- README.md | 21 ++ build/rom_serial_pseudocode.c | 220 +++++++++++++++ h8536/serial_pseudocode.py | 477 ++++++++++++++++++++++++++++++++ h8536_serial_pseudocode.py | 5 + tests/test_serial_pseudocode.py | 144 ++++++++++ 5 files changed, 867 insertions(+) create mode 100644 build/rom_serial_pseudocode.c create mode 100644 h8536/serial_pseudocode.py create mode 100644 h8536_serial_pseudocode.py create mode 100644 tests/test_serial_pseudocode.py diff --git a/README.md b/README.md index 7d364b3..d076a8a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ To turn the structured decompile output into conservative C-like pseudocode: .\.venv\Scripts\python.exe h8536_pseudocode.py build\rom_decompiled.json --out build\rom_pseudocode.c --cycles ``` +To generate a focused RX/TX serial-path pseudocode view from the reconstruction metadata: + +```powershell +.\.venv\Scripts\python.exe h8536_serial_pseudocode.py build\rom_decompiled.json --out build\rom_serial_pseudocode.c +``` + ## What It Does - Decodes the H8/500 instruction set used by the H8/536. @@ -35,6 +41,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. - Annotates SCI protocol actions such as TDRE waits, TDR writes, RDR reads, RX/TX interrupt enables, and receive-error clears. - Reconstructs evidence-supported SCI1 serial frame candidates, including the apparent six-byte TX/RX units and XOR checksum seeded by `0x5A`. +- Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction candidates. - Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver. - 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. @@ -90,6 +97,18 @@ python h8536_pseudocode.py --help - `--no-structure`: preserve label/goto output instead of simple structured `if`/loop output. - `--max-functions N`: emit only the first `N` functions for focused review. +For focused serial pseudocode: + +```powershell +python h8536_serial_pseudocode.py --help +``` + +- `--tx-only`: emit only the candidate transmit path. +- `--rx-only`: emit only the candidate receive path. +- `--no-evidence`: omit evidence-address comments. +- `--no-manual`: omit manual-reference comments. +- `--no-board`: omit board/MAX202 comments. + ## Code Layout - `h8536_decompiler.py`: compatibility wrapper for the CLI. @@ -111,9 +130,11 @@ python h8536_pseudocode.py --help - `h8536/sci.py`: SCI setup tracking and baud inference. - `h8536/sci_protocol.py`: SCI transmit/receive/status semantic annotations. - `h8536/serial_reconstruction.py`: cautious higher-level SCI frame reconstruction from decompiled evidence. +- `h8536/serial_pseudocode.py`: focused RX/TX protocol pseudocode generation from reconstruction metadata. - `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path. - `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis. - `h8536/pseudocode.py`: JSON-to-C-like pseudocode generation. - `h8536/render.py`: assembly and JSON output. - `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers. - `h8536_pseudocode.py`: pseudocode CLI wrapper. +- `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper. diff --git a/build/rom_serial_pseudocode.c b/build/rom_serial_pseudocode.c new file mode 100644 index 0000000..2a113dc --- /dev/null +++ b/build/rom_serial_pseudocode.c @@ -0,0 +1,220 @@ +/* + * H8/536 focused SCI RX/TX pseudocode from build\rom_decompiled.json + * + * This is a protocol-oriented reconstruction from decompiler JSON metadata. + * It is intentionally phrased as candidate behavior: it summarizes evidence + * from the ROM without claiming source-level intent or a proven packet format. + */ + +/* Candidate summary: + * - SCI1 TX 6-byte frame at H'F858-H'F85D: confidence high (0.95) + * reason: all required independent evidence groups were observed + * - SCI1 RX 6-byte frame captured at H'F868-H'F86D: confidence high (0.9) + * reason: RX count, copy, and checksum-validation evidence were observed; no explicit header/sync byte was identified + * caveat: candidate frame means six consecutive bytes within the observed RX timing/state machine, not a proven delimited packet + */ + +/* Board path: + * Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver. + * - TXD: H8 pin 66 P95/TXD <-> MAX202 pin 11 + * evidence: MAX202 pin 11 traces to H8 pin 66 + * - RXD: H8 pin 67 P96/RXD <-> MAX202 pin 12 + * evidence: MAX202 pin 12 traces to H8 pin 67 + */ + +/* Manual anchors used by the decompiler metadata: + * - 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 + * - 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 + */ + +#include +#include + +typedef uint8_t u8; +typedef uint16_t u16; + +extern volatile u8 MEM8[0x10000]; + +#define SCI1_SCR MEM8[0xFEDAu] +#define SCI1_TDR MEM8[0xFEDBu] +#define SCI1_SSR MEM8[0xFEDCu] +#define SCI1_RDR MEM8[0xFEDDu] + +#define SCI_SCR_TIE 0x80u +#define SCI_SCR_RIE 0x40u +#define SCI_SCR_TE 0x20u +#define SCI_SCR_RE 0x10u +#define SCI_SSR_TDRE 0x80u +#define SCI_SSR_RDRF 0x40u +#define SCI_SSR_ORER 0x20u +#define SCI_SSR_FER 0x10u +#define SCI_SSR_PER 0x08u + +#define TX_FRAME_LENGTH 6u +#define TX_FRAME(n) MEM8[(u16)(0xF858u + (n))] +#define TX_INDEX MEM8[0xF9C2u] + +#define RX_FRAME_LENGTH 6u +#define RX_CAPTURE(n) MEM8[(u16)(0xF868u + (n))] +#define RX_FRAME(n) MEM8[(u16)(0xF860u + (n))] +#define RX_INDEX MEM8[0xF9C3u] +#define RX_INTERBYTE_TIMEOUT MEM8[0xF9C1u] +#define RX_COMPLETE_TIMER MEM8[0xF9C5u] + +/* + * TX reconstruction evidence + * candidate/evidence-supported SCI1 6-byte TX frame hypothesis using buffer H'F858-H'F85D with checksum byte H'F85D seeded by H'005A + * checksum formula: checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4 + * evidence addresses: + * - checksum_byte: H'BA64 + * - initial_send_from_buffer_start: H'BA6E, H'BA72 + * - tx_buffer_region: H'BA3A, H'BA42, H'BA4A, H'BA50, H'BA54, H'BA58, H'BA5C, H'BA60, H'BA64, H'BA6E, H'BE05, H'BE0D, H'BE15 + * - tx_checksum_seed: H'BA4E + * - tx_index_compare_frame_length: H'BAC3 + * - tx_index_increment: H'BABF + * - tx_index_initialized_to_one: H'BA76 + * - tx_isr_indexed_send: H'BAAB, H'BAB1, H'BAB5 + * - xor_checksum_chain: H'BA50, H'BA54, H'BA58, H'BA5C, H'BA60, H'BA64 + */ +static u8 sci1_tx_candidate_checksum(void) +{ + u8 checksum = 0x5Au; + checksum ^= TX_FRAME(0); + checksum ^= TX_FRAME(1); + checksum ^= TX_FRAME(2); + checksum ^= TX_FRAME(3); + checksum ^= TX_FRAME(4); + return checksum; +} + +void sci1_tx_start_candidate_frame(void) +{ + /* The ROM appears to have populated TX_FRAME(0..4) before this point. */ + TX_FRAME(5) = sci1_tx_candidate_checksum(); + + while ((SCI1_SSR & SCI_SSR_TDRE) == 0u) { + /* wait for transmit data register empty */ + } + + SCI1_TDR = TX_FRAME(0); + TX_INDEX = 1u; + SCI1_SSR &= (u8)~SCI_SSR_TDRE; + SCI1_SCR |= SCI_SCR_TIE; +} + +void sci1_txi_candidate_isr(void) +{ + if (TX_INDEX < TX_FRAME_LENGTH) { + SCI1_TDR = TX_FRAME(TX_INDEX); + TX_INDEX = (u8)(TX_INDEX + 1u); + SCI1_SSR &= (u8)~SCI_SSR_TDRE; + } + + if (TX_INDEX >= TX_FRAME_LENGTH) { + SCI1_SCR &= (u8)~SCI_SCR_TIE; + } +} + +/* + * RX reconstruction evidence + * candidate/evidence-supported SCI1 6-byte RX frame hypothesis using capture buffer H'F868-H'F86D; checksum byte H'F865 is validated against XOR seeded by H'005A + * checksum formula: checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4 + * evidence addresses: + * - rx_checksum_seed: H'BBD6 + * - rx_complete_timer: H'BB9E + * - rx_copy_capture_to_frame_buffer: H'BBB3, H'BBBB, H'BBC3, H'BBB7, H'BBBF, H'BBC7 + * - rx_index_increment_store: H'BB94, H'BB96 + * - rx_indexed_store: H'BB90 + * - rx_isr_compare_frame_length: H'BB9A + * - rx_processor_requires_six_bytes: H'BBAB + * - rx_rdr_read: H'BB6D + * - rx_xor_checksum_validation: H'BBD6, H'BBD8, H'BBDC, H'BBE0, H'BBE4, H'BBE8, H'BBEC + */ +static u8 sci1_rx_candidate_checksum(void) +{ + u8 checksum = 0x5Au; + checksum ^= RX_FRAME(0); + checksum ^= RX_FRAME(1); + checksum ^= RX_FRAME(2); + checksum ^= RX_FRAME(3); + checksum ^= RX_FRAME(4); + return checksum; +} + +bool sci1_process_rx_candidate_frame(void) +{ + u8 i; + + if (RX_INDEX != RX_FRAME_LENGTH) { + return false; + } + + for (i = 0u; i < RX_FRAME_LENGTH; i++) { + RX_FRAME(i) = RX_CAPTURE(i); + } + + if (sci1_rx_candidate_checksum() != RX_FRAME(5)) { + RX_INDEX = 0u; + return false; + } + + RX_INDEX = 0u; + return true; +} + +bool sci1_rx_byte_received_candidate_isr(void) +{ + u8 byte; + + SCI1_SSR &= (u8)~SCI_SSR_RDRF; + byte = SCI1_RDR; + + if (RX_INTERBYTE_TIMEOUT == 0u) { + RX_INDEX = 0u; + } + + RX_INTERBYTE_TIMEOUT = 5u; + RX_CAPTURE(RX_INDEX) = byte; + RX_INDEX = (u8)(RX_INDEX + 1u); + + if (RX_INDEX == RX_FRAME_LENGTH) { + RX_COMPLETE_TIMER = 0x14u; + return sci1_process_rx_candidate_frame(); + } + + return false; +} + +void sci1_rx_error_candidate_isr(void) +{ + SCI1_SSR &= (u8)~(SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER); + RX_INDEX = 0u; +} diff --git a/h8536/serial_pseudocode.py b/h8536/serial_pseudocode.py new file mode 100644 index 0000000..08a4c85 --- /dev/null +++ b/h8536/serial_pseudocode.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +JsonObject = dict[str, Any] + + +@dataclass(frozen=True) +class SerialPseudocodeOptions: + include_tx: bool = True + include_rx: bool = True + include_evidence: bool = True + include_manual: bool = True + include_board: bool = True + + +def generate_serial_pseudocode( + payload: JsonObject, + *, + source_name: str = "", + options: SerialPseudocodeOptions | None = None, +) -> str: + opts = options or SerialPseudocodeOptions() + tx_candidate = _find_candidate(payload, "candidate_sci1_tx_frame") + rx_candidate = _find_candidate(payload, "candidate_sci1_rx_frame") + + lines: list[str] = [] + lines.extend(_file_header(source_name, tx_candidate, rx_candidate)) + if opts.include_board: + lines.extend(_board_comment_lines(payload)) + if opts.include_manual: + lines.extend(_manual_reference_lines(payload)) + lines.extend(_declarations(tx_candidate, rx_candidate)) + + emitted = False + if opts.include_tx and tx_candidate: + lines.extend(_tx_functions(tx_candidate, opts)) + emitted = True + if opts.include_rx and rx_candidate: + lines.extend(_rx_functions(rx_candidate, opts)) + emitted = True + + if not emitted: + lines.append("/* No requested SCI serial reconstruction candidates were present in the JSON input. */") + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + +def load_serial_pseudocode_input(path: Path) -> JsonObject: + with path.open("r", encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict) or "instructions" not in payload: + raise ValueError(f"{path} does not look like h8536_decompiler JSON output") + return payload + + +def write_serial_pseudocode( + input_path: Path, + output_path: Path, + options: SerialPseudocodeOptions, +) -> None: + payload = load_serial_pseudocode_input(input_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text( + generate_serial_pseudocode(payload, source_name=str(input_path), options=options), + encoding="utf-8", + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Generate focused C-like SCI RX/TX pseudocode from h8536_decompiler JSON output.", + ) + parser.add_argument( + "input", + nargs="?", + type=Path, + default=Path("build/rom_decompiled.json"), + help="structured JSON emitted by h8536_decompiler.py", + ) + parser.add_argument( + "--out", + type=Path, + default=Path("build/rom_serial_pseudocode.c"), + help="focused serial pseudocode output path", + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--tx-only", action="store_true", help="emit only the candidate TX path") + mode.add_argument("--rx-only", action="store_true", help="emit only the candidate RX path") + parser.add_argument("--no-evidence", action="store_true", help="omit evidence-address comments") + parser.add_argument("--no-manual", action="store_true", help="omit manual-reference comments") + parser.add_argument("--no-board", action="store_true", help="omit board/MAX202 comments") + args = parser.parse_args(argv) + + options = SerialPseudocodeOptions( + include_tx=not args.rx_only, + include_rx=not args.tx_only, + include_evidence=not args.no_evidence, + include_manual=not args.no_manual, + include_board=not args.no_board, + ) + write_serial_pseudocode(args.input, args.out, options) + print(f"wrote {args.out}") + return 0 + + +def _file_header( + source_name: str, + tx_candidate: JsonObject | None, + rx_candidate: JsonObject | None, +) -> list[str]: + source = f" from {source_name}" if source_name else "" + lines = [ + "/*", + f" * H8/536 focused SCI RX/TX pseudocode{source}", + " *", + " * This is a protocol-oriented reconstruction from decompiler JSON metadata.", + " * It is intentionally phrased as candidate behavior: it summarizes evidence", + " * from the ROM without claiming source-level intent or a proven packet format.", + " */", + "", + ] + if tx_candidate or rx_candidate: + lines.append("/* Candidate summary:") + for candidate in (tx_candidate, rx_candidate): + if not candidate: + continue + lines.append( + " * - " + + _candidate_label(candidate) + + f": confidence {candidate.get('confidence', 'unknown')} " + + f"({candidate.get('confidence_score', 'n/a')})" + ) + reason = str(candidate.get("confidence_reason") or "").strip() + if reason: + lines.append(f" * reason: {_comment_text(reason)}") + caveat = str(candidate.get("caveat") or "").strip() + if caveat: + lines.append(f" * caveat: {_comment_text(caveat)}") + lines.append(" */") + lines.append("") + return lines + + +def _board_comment_lines(payload: JsonObject) -> list[str]: + board = payload.get("board_profile") + if not isinstance(board, dict): + return [] + lines = ["/* Board path:"] + summary = str(board.get("summary") or "").strip() + if summary: + lines.append(f" * {summary}") + for trace in _mapping_items(board.get("traces")): + signal = trace.get("signal", "?") + h8_pin = trace.get("h8_pin", "?") + h8_name = trace.get("h8_pin_name", "?") + max202_pin = trace.get("max202_pin", "?") + evidence = str(trace.get("evidence") or "").strip() + lines.append(f" * - {signal}: H8 pin {h8_pin} {h8_name} <-> MAX202 pin {max202_pin}") + if evidence: + lines.append(f" * evidence: {_comment_text(evidence)}") + lines.append(" */") + lines.append("") + return lines + + +def _manual_reference_lines(payload: JsonObject) -> list[str]: + refs: list[str] = [] + for section in ("sci_protocol", "board_profile"): + data = payload.get(section) + if isinstance(data, dict): + refs.extend(str(ref) for ref in data.get("manual_references", []) if ref) + refs = _dedupe(refs) + if not refs: + return [] + lines = ["/* Manual anchors used by the decompiler metadata:"] + for ref in refs: + lines.append(f" * - {_comment_text(ref)}") + lines.append(" */") + lines.append("") + return lines + + +def _declarations(tx_candidate: JsonObject | None, rx_candidate: JsonObject | None) -> list[str]: + candidate = tx_candidate or rx_candidate or {} + channel = str(candidate.get("channel") or "SCI1") + tdr = _int_field(tx_candidate, "tdr_address", 0xFEDB) + rdr = _int_field(rx_candidate, "rdr_address", 0xFEDD) + scr = tdr - 1 if tdr else 0xFEDA + ssr = rdr - 1 if rdr else 0xFEDC + + lines = [ + "#include ", + "#include ", + "", + "typedef uint8_t u8;", + "typedef uint16_t u16;", + "", + "extern volatile u8 MEM8[0x10000];", + "", + f"#define {channel}_SCR MEM8[{_c_hex(scr)}]", + f"#define {channel}_TDR MEM8[{_c_hex(tdr)}]", + f"#define {channel}_SSR MEM8[{_c_hex(ssr)}]", + f"#define {channel}_RDR MEM8[{_c_hex(rdr)}]", + "", + "#define SCI_SCR_TIE 0x80u", + "#define SCI_SCR_RIE 0x40u", + "#define SCI_SCR_TE 0x20u", + "#define SCI_SCR_RE 0x10u", + "#define SCI_SSR_TDRE 0x80u", + "#define SCI_SSR_RDRF 0x40u", + "#define SCI_SSR_ORER 0x20u", + "#define SCI_SSR_FER 0x10u", + "#define SCI_SSR_PER 0x08u", + "", + ] + + if tx_candidate: + tx_start = _int_field(tx_candidate, "buffer_start", 0xF858) + tx_index = _int_field(tx_candidate, "tx_index_address", 0xF9C2) + length = _int_field(tx_candidate, "frame_length", 6) + lines.extend( + [ + f"#define TX_FRAME_LENGTH {length}u", + f"#define TX_FRAME(n) MEM8[(u16)({_c_hex(tx_start)} + (n))]", + f"#define TX_INDEX MEM8[{_c_hex(tx_index)}]", + "", + ], + ) + + if rx_candidate: + capture_start = _int_field(rx_candidate, "capture_buffer_start", 0xF868) + frame_start = _int_field(rx_candidate, "validation_buffer_start", 0xF860) + rx_index = _int_field(rx_candidate, "rx_index_address", 0xF9C3) + timeout = _int_field(rx_candidate, "interbyte_timeout_address", 0xF9C1) + complete = _int_field(rx_candidate, "complete_timer_address", 0xF9C5) + length = _int_field(rx_candidate, "frame_length", 6) + lines.extend( + [ + f"#define RX_FRAME_LENGTH {length}u", + f"#define RX_CAPTURE(n) MEM8[(u16)({_c_hex(capture_start)} + (n))]", + f"#define RX_FRAME(n) MEM8[(u16)({_c_hex(frame_start)} + (n))]", + f"#define RX_INDEX MEM8[{_c_hex(rx_index)}]", + f"#define RX_INTERBYTE_TIMEOUT MEM8[{_c_hex(timeout)}]", + f"#define RX_COMPLETE_TIMER MEM8[{_c_hex(complete)}]", + "", + ], + ) + + return lines + + +def _tx_functions(candidate: JsonObject, opts: SerialPseudocodeOptions) -> list[str]: + length = _int_field(candidate, "frame_length", 6) + seed = _int_field(candidate, "checksum_seed", 0x5A) + data_length = max(length - 1, 0) + lines = _candidate_comment_block("TX reconstruction evidence", candidate, opts) + lines.extend( + [ + "static u8 sci1_tx_candidate_checksum(void)", + "{", + f" u8 checksum = {_c_hex(seed, width=2)};", + ], + ) + for index in range(data_length): + lines.append(f" checksum ^= TX_FRAME({index});") + lines.extend( + [ + " return checksum;", + "}", + "", + "void sci1_tx_start_candidate_frame(void)", + "{", + " /* The ROM appears to have populated TX_FRAME(0..4) before this point. */", + f" TX_FRAME({data_length}) = sci1_tx_candidate_checksum();", + "", + f" while ((SCI1_SSR & SCI_SSR_TDRE) == 0u) {{", + " /* wait for transmit data register empty */", + " }", + "", + " SCI1_TDR = TX_FRAME(0);", + " TX_INDEX = 1u;", + " SCI1_SSR &= (u8)~SCI_SSR_TDRE;", + " SCI1_SCR |= SCI_SCR_TIE;", + "}", + "", + "void sci1_txi_candidate_isr(void)", + "{", + " if (TX_INDEX < TX_FRAME_LENGTH) {", + " SCI1_TDR = TX_FRAME(TX_INDEX);", + " TX_INDEX = (u8)(TX_INDEX + 1u);", + " SCI1_SSR &= (u8)~SCI_SSR_TDRE;", + " }", + "", + " if (TX_INDEX >= TX_FRAME_LENGTH) {", + " SCI1_SCR &= (u8)~SCI_SCR_TIE;", + " }", + "}", + "", + ], + ) + return lines + + +def _rx_functions(candidate: JsonObject, opts: SerialPseudocodeOptions) -> list[str]: + length = _int_field(candidate, "frame_length", 6) + seed = _int_field(candidate, "checksum_seed", 0x5A) + data_length = max(length - 1, 0) + lines = _candidate_comment_block("RX reconstruction evidence", candidate, opts) + lines.extend( + [ + "static u8 sci1_rx_candidate_checksum(void)", + "{", + f" u8 checksum = {_c_hex(seed, width=2)};", + ], + ) + for index in range(data_length): + lines.append(f" checksum ^= RX_FRAME({index});") + lines.extend( + [ + " return checksum;", + "}", + "", + "bool sci1_process_rx_candidate_frame(void)", + "{", + " u8 i;", + "", + " if (RX_INDEX != RX_FRAME_LENGTH) {", + " return false;", + " }", + "", + " for (i = 0u; i < RX_FRAME_LENGTH; i++) {", + " RX_FRAME(i) = RX_CAPTURE(i);", + " }", + "", + f" if (sci1_rx_candidate_checksum() != RX_FRAME({data_length})) {{", + " RX_INDEX = 0u;", + " return false;", + " }", + "", + " RX_INDEX = 0u;", + " return true;", + "}", + "", + "bool sci1_rx_byte_received_candidate_isr(void)", + "{", + " u8 byte;", + "", + " SCI1_SSR &= (u8)~SCI_SSR_RDRF;", + " byte = SCI1_RDR;", + "", + " if (RX_INTERBYTE_TIMEOUT == 0u) {", + " RX_INDEX = 0u;", + " }", + "", + " RX_INTERBYTE_TIMEOUT = 5u;", + " RX_CAPTURE(RX_INDEX) = byte;", + " RX_INDEX = (u8)(RX_INDEX + 1u);", + "", + " if (RX_INDEX == RX_FRAME_LENGTH) {", + " RX_COMPLETE_TIMER = 0x14u;", + " return sci1_process_rx_candidate_frame();", + " }", + "", + " return false;", + "}", + "", + "void sci1_rx_error_candidate_isr(void)", + "{", + " SCI1_SSR &= (u8)~(SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER);", + " RX_INDEX = 0u;", + "}", + "", + ], + ) + return lines + + +def _candidate_comment_block( + title: str, + candidate: JsonObject, + opts: SerialPseudocodeOptions, +) -> list[str]: + lines = ["/*", f" * {title}"] + comment = str(candidate.get("comment") or candidate.get("short_comment") or "").strip() + if comment: + lines.append(f" * {_comment_text(comment)}") + formula = str(candidate.get("checksum_formula") or "").strip() + if formula: + lines.append(f" * checksum formula: {_comment_text(formula)}") + if opts.include_evidence: + evidence = candidate.get("evidence_addresses_hex") + if isinstance(evidence, dict): + lines.append(" * evidence addresses:") + for key in sorted(evidence): + addresses = ", ".join(str(item) for item in evidence.get(key, [])) + lines.append(f" * - {key}: {addresses}") + lines.append(" */") + return lines + + +def _find_candidate(payload: JsonObject, kind: str) -> JsonObject | None: + serial = payload.get("serial_reconstruction") + if not isinstance(serial, dict): + return None + candidates = serial.get("candidates") + if not isinstance(candidates, list): + return None + for candidate in candidates: + if isinstance(candidate, dict) and candidate.get("kind") == kind: + return candidate + return None + + +def _candidate_label(candidate: JsonObject) -> str: + kind = str(candidate.get("kind") or "candidate") + channel = str(candidate.get("channel") or "SCI") + length = candidate.get("frame_length", "?") + if kind.endswith("_tx_frame"): + start = candidate.get("buffer_start_hex") or _h(_int_field(candidate, "buffer_start", 0)) + end = candidate.get("buffer_end_hex") or _h(_int_field(candidate, "buffer_end", 0)) + return f"{channel} TX {length}-byte frame at {start}-{end}" + if kind.endswith("_rx_frame"): + capture_start = candidate.get("capture_buffer_start_hex") or _h( + _int_field(candidate, "capture_buffer_start", 0), + ) + capture_end = candidate.get("capture_buffer_end_hex") or _h( + _int_field(candidate, "capture_buffer_end", 0), + ) + return f"{channel} RX {length}-byte frame captured at {capture_start}-{capture_end}" + return f"{channel} {kind}" + + +def _mapping_items(value: object) -> list[JsonObject]: + if not isinstance(value, list): + return [] + return [item for item in value if isinstance(item, dict)] + + +def _int_field(candidate: JsonObject | None, key: str, default: int) -> int: + if not candidate: + return default + value = candidate.get(key) + if isinstance(value, bool): + return int(value) + if isinstance(value, int): + return value + return default + + +def _c_hex(value: int, *, width: int = 4) -> str: + return f"0x{value & 0xFFFF:0{width}X}u" + + +def _h(value: int) -> str: + return f"H'{value & 0xFFFF:04X}" + + +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 _comment_text(text: str) -> str: + return text.replace("*/", "* /").replace("\r", " ").replace("\n", " ") diff --git a/h8536_serial_pseudocode.py b/h8536_serial_pseudocode.py new file mode 100644 index 0000000..daddbe5 --- /dev/null +++ b/h8536_serial_pseudocode.py @@ -0,0 +1,5 @@ +from h8536.serial_pseudocode import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_serial_pseudocode.py b/tests/test_serial_pseudocode.py new file mode 100644 index 0000000..db50b1d --- /dev/null +++ b/tests/test_serial_pseudocode.py @@ -0,0 +1,144 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from h8536.serial_pseudocode import ( + SerialPseudocodeOptions, + generate_serial_pseudocode, + write_serial_pseudocode, +) + + +def candidate_payload() -> dict: + return { + "instructions": [], + "sci_protocol": { + "manual_references": [ + "Manual/0900766b802125d0.md:15794 RDR receive data register", + "Manual/0900766b802125d0.md:15823 TDR transmit data register", + ], + }, + "board_profile": { + "summary": "Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.", + "manual_references": [ + "Manual/0900766b802125d0.md:2417 FP-80 H8/536 pin 66 is P95/TXD", + ], + "traces": [ + { + "signal": "TXD", + "h8_pin": 66, + "h8_pin_name": "P95/TXD", + "max202_pin": 11, + "evidence": "MAX202 pin 11 traces to H8 pin 66", + }, + { + "signal": "RXD", + "h8_pin": 67, + "h8_pin_name": "P96/RXD", + "max202_pin": 12, + "evidence": "MAX202 pin 12 traces to H8 pin 67", + }, + ], + }, + "serial_reconstruction": { + "candidates": [ + { + "kind": "candidate_sci1_tx_frame", + "channel": "SCI1", + "frame_length": 6, + "buffer_start": 0xF858, + "buffer_start_hex": "H'F858", + "buffer_end": 0xF85D, + "buffer_end_hex": "H'F85D", + "checksum_address": 0xF85D, + "tx_index_address": 0xF9C2, + "tdr_address": 0xFEDB, + "checksum_seed": 0x5A, + "checksum_formula": "checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4", + "confidence": "high", + "confidence_score": 0.95, + "confidence_reason": "all required independent evidence groups were observed", + "comment": "candidate/evidence-supported SCI1 6-byte TX frame hypothesis", + "evidence_addresses_hex": { + "tx_checksum_seed": ["H'BA4E"], + "xor_checksum_chain": ["H'BA50", "H'BA54"], + }, + }, + { + "kind": "candidate_sci1_rx_frame", + "channel": "SCI1", + "frame_length": 6, + "capture_buffer_start": 0xF868, + "capture_buffer_start_hex": "H'F868", + "capture_buffer_end": 0xF86D, + "capture_buffer_end_hex": "H'F86D", + "validation_buffer_start": 0xF860, + "validation_buffer_end": 0xF865, + "checksum_address": 0xF865, + "rx_index_address": 0xF9C3, + "rdr_address": 0xFEDD, + "interbyte_timeout_address": 0xF9C1, + "complete_timer_address": 0xF9C5, + "checksum_seed": 0x5A, + "checksum_formula": "checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4", + "confidence": "high", + "confidence_score": 0.9, + "confidence_reason": "RX count, copy, and checksum-validation evidence were observed", + "caveat": "candidate frame means six consecutive bytes, not a proven delimited packet", + "comment": "candidate/evidence-supported SCI1 6-byte RX frame hypothesis", + "evidence_addresses_hex": { + "rx_rdr_read": ["H'BB6D"], + "rx_xor_checksum_validation": ["H'BBD6", "H'BBEC"], + }, + }, + ], + }, + } + + +class SerialPseudocodeTest(unittest.TestCase): + def test_generates_focused_tx_and_rx_candidate_paths(self): + text = generate_serial_pseudocode(candidate_payload(), source_name="rom.json") + + self.assertIn("focused SCI RX/TX pseudocode from rom.json", text) + self.assertIn("SCI1 TX 6-byte frame at H'F858-H'F85D", text) + self.assertIn("SCI1 RX 6-byte frame captured at H'F868-H'F86D", text) + self.assertIn("MAX202 pin 11 traces to H8 pin 66", text) + self.assertIn("Manual/0900766b802125d0.md:15823 TDR transmit data register", text) + self.assertIn("#define TX_FRAME(n) MEM8[(u16)(0xF858u + (n))]", text) + self.assertIn("#define RX_CAPTURE(n) MEM8[(u16)(0xF868u + (n))]", text) + self.assertIn("checksum ^= TX_FRAME(4);", text) + self.assertIn("TX_FRAME(5) = sci1_tx_candidate_checksum();", text) + self.assertIn("SCI1_TDR = TX_FRAME(0);", text) + self.assertIn("void sci1_txi_candidate_isr(void)", text) + self.assertIn("SCI1_SSR &= (u8)~SCI_SSR_RDRF;\n byte = SCI1_RDR;", text) + self.assertIn("RX_CAPTURE(RX_INDEX) = byte;", text) + self.assertIn("return sci1_process_rx_candidate_frame();", text) + self.assertIn("rx_xor_checksum_validation: H'BBD6, H'BBEC", text) + + def test_tx_only_option_omits_rx_functions(self): + text = generate_serial_pseudocode( + candidate_payload(), + options=SerialPseudocodeOptions(include_rx=False), + ) + + self.assertIn("void sci1_tx_start_candidate_frame(void)", text) + self.assertNotIn("sci1_rx_byte_received_candidate_isr", text) + + def test_write_serial_pseudocode_writes_output_file(self): + with tempfile.TemporaryDirectory() as tmp: + input_path = Path(tmp) / "rom.json" + output_path = Path(tmp) / "serial.c" + input_path.write_text( + json.dumps(candidate_payload()), + encoding="utf-8", + ) + + write_serial_pseudocode(input_path, output_path, SerialPseudocodeOptions()) + + self.assertIn("sci1_process_rx_candidate_frame", output_path.read_text(encoding="utf-8")) + + +if __name__ == "__main__": + unittest.main()