Skip to content

Link research notebook

Known input

  • Archive SHA-256: db47b705f88e415eee80194716cc75f53dc25fac85e680b0d46ab01fde64ee51
  • ROM SHA-256: f0d89fcef129450105e1e45bb29693f30d30a00dd2e92168e7253438dca38a63
  • ROM size: 2 MiB
  • Header title: SAMURAI2v000
  • Entry PC: 0x200046 (file offset 0x46)
  • Catalog: 0x0030; subcatalog: 0x1a; mode: 0x10 (color)

What is documented already

The console exposes one SIO channel and is commonly specified at 19200 bit/s. The TLCS-900 family serial peripheral has mode, control, baud-rate, and buffer registers (usually named SC0MOD, SC0CR, BR0CR, and SC0BUF). Family manuals describe UART and clocked I/O modes, but the exact NGPC SoC register map, external wiring, boot-ROM services, and game-level protocol still need to be verified rather than inferred from a related Toshiba part.

MAME models the NGPC's TMP95C061 integrated serial registers at 0x50 (SC0BUF), 0x51 (SC0CR), 0x52 (SC0MOD), and 0x53 (BR0CR). Its second on-chip channel occupies 0x54 through 0x57, followed by ODE at 0x58. These names are now emitted by the standalone disassembler.

Immediate static-analysis questions

  • Does the game call a BIOS communication service, or touch SIO registers?
  • Which code path is reached from the two-player menu?
  • How are peer/master roles selected when both cable ends are identical?
  • What are the framing, handshake, retry, timeout, and disconnect semantics?
  • Is gameplay lockstep, input exchange, or state exchange?

BIOS communication ABI

The game does not access channel 0 registers directly. Its wrapper at 0x2304cd multiplies W by four, reads a function pointer from the BIOS table at 0xfffe00, and calls it. Communication service IDs are:

ID BIOS name Inputs / result observed in HLE
10 COMINIT initialize; RA3=0 on success
11 COMSENDSTART flush/start queued transmission
12 COMRECIVESTART enable/start reception
13 COMCREATEDATA transmit byte from RB3
14 COMGETDATA receive byte into RB3; RA3=0 success, 1 empty
15 COMONRTS assert/enable RTS
16 COMOFFRTS deassert/disable RTS
17 COMSENDSTATUS pending transmit count in RWA3
18 COMRECIVESTATUS received byte count in RWA3
19 COMCREATEBUFDATA send RB3 bytes from XHL3
1a COMGETBUFDATA receive up to RB3 bytes into XHL3

The old NeoPop/Mednafen HLE treats transmission as a byte queue, triggers HDMA/interrupt channel 11 after writes and channel 12 after reads, and models RTS through I/O byte 0xb2. This describes the software-facing ABI, not yet the physical wire timing.

Confirmed Samurai Shodown 2 handshake

The communication subsystem occupies approximately 0x206c34–0x2077ff and uses a nine-entry state dispatcher at 0x206cd0. Initialization calls COMINIT, COMRECIVESTART, and COMONRTS.

The game initialization path calls 0x206c34 from 0x200289. Its main loop calls the communication tick routine 0x206c8f from 0x2001a7 once per iteration/frame. That routine indexes the state table using byte 0x564e. The table is:

Raw offset Logical state Handler
00 0 206cf4
04 1 206d31
08 2 206e8b
0c 3 206ed1
10 4 206f25
14 5 206f68
18 6 206ed0
1c 7 206f32
20 8 206d0c

The stored value is a byte offset into the pointer table, hence the multiples of four; it is not an ordinal state number.

Whole-ROM writes refine the table into this behavioral state machine:

Raw Behavior Principal transition
FF disabled/no dispatcher call external code writes 00 to start
00 initialize BIOS communication initialize, receive start, RTS on; then 04
04 advertise/search repeatedly send FC identity; accepted FC → 08, accepted FD → 0C
08 FD response send FD identity; then 0C
0C finalize identity handshake drain/handshake RTS and enter 14
10 shutdown request preserve only status bit 7; then 1C
14 active normal/F8 transport remains until error, reset, or external shutdown
18 reserved no-op no whole-ROM producer found
1C shutdown/flush RTS off, drain receive queue, RTS on; then FF
20 reserved restart helper would return to 04; no whole-ROM producer found

External state-machine code writes only 00, 10, or FF; identity and shutdown handlers own the other live transitions. Thus 10 is a deliberate session-close request rather than another handshake phase.

The first endpoint sends this four-byte identity frame using four COMCREATEDATA calls, then COMSENDSTART:

FC 01 00 30

The receiver requires byte 0 to be FC or FD, byte 1 to be 01, and the big-endian word in bytes 2–3 to equal cartridge catalog 0x0030. When an FC identity is accepted, the peer answers:

FD 01 00 30

Evidence supports FC as a role request and FD as its response, 01 as a protocol revision or message subtype, and 0030 as the game/catalog compatibility value. The endpoint that receives FD after sending FC uses the game's player-1/input-slot-0 branch. The endpoint that receives FC, sends FD, and records status bit 0 uses player-2/input-slot-1 behavior. Electrical arbitration when both endpoints attempt FC simultaneously still requires BIOS/hardware tracing.

The trace decoder therefore infers roles only for an unambiguous directional A→B FC followed by B→A FD. If FC is observed in both directions before an FD, it reports negotiation ambiguity instead of assigning roles. A physical capture will show whether CTS/RTS prevents both requests from reaching their peers, whether one BIOS queue wins, or whether retries eventually diverge.

Later synchronization traffic uses F8-prefixed three-byte input messages, with FE and FF as control/sentinel values.

Gameplay transport framing (current evidence)

Normal transport control bytes are in F0–F7. The receiver subtracts F0 and rejects values above seven. The low three bits have these observable effects:

  • bit 0 is a level-style phase/ready barrier. Most transport uses base F0, but game-state routines switch the base to F1. The communication tick snapshots the local bit at 5651 and the received bit at 5652; transition code waits until both snapshots match and equal one before advancing. It is therefore not an alternating per-packet acknowledgment bit;
  • bit 1 toggles a one-bit peer state and refreshes a 60-tick timer;
  • bit 2 means a payload descriptor and ring-buffer bytes follow.

The payload sender emits a control byte with bit 2 set, then a 7-bit queued-byte count, followed by exactly that many bytes from the 64-byte transmit ring at 0x566d. It computes the first ring position as write_index - count modulo 64. The receiver validates the announced count against a 64-byte window, receives the bytes into the ring at 0x56ad, and wraps indices with & 0x3f. BIOS buffered services 19 and 1a split transfers at the ring boundary when necessary.

Ordinary traffic uses base control F0; it becomes F2 when the local toggle/event flag is emitted, F4 when payload follows, and F6 when both conditions apply. During a pre-game synchronization barrier the game switches the base to F1, making the corresponding event/payload forms F3, F5, and F7 possible through the same OR operations.

Transport bookkeeping and timeout evidence

The normal-transport variables currently have these evidence-backed roles:

Address Meaning in normal transport
5653 locally generated base control (F0 normally, F1 during a phase barrier)
5651 low-bit snapshot of the current locally generated control
5652 low-bit snapshot of the last received F0–F7 control
5655.bit0 peer event/toggle state; XORed for every received bit-1 control
5655.bit7 pending local event; emitted as control bit 1, then cleared
5656 60-tick event/watchdog countdown
5659 transmit queued count; bit 7 marks a transfer in progress
565b transmit ring write position
565d receive queued count; bit 7 has an additional state role
565e receive ring write position
565f payload receive substate/sentinel (00, FF, FE, or count)

Receiving control bit 1 XORs 5655.bit0 and reloads 5656 to 60. Sending a pending local event ORs bit 1 into the control and clears 5655.bit7. While either event flag is set, 5656 is decremented by the communication tick; at zero the flags are cleared and the session follows its timeout/error path. Because the tick is called from the main loop, these constants are presently described as ticks, not milliseconds or frames. Runtime tracing is required to convert them to wall-clock limits.

After an ordinary transmission, the short resend/idle countdown at 5654 is loaded with 15. Session setup loads it with 30. Several terminal paths set the public status byte at 564f to FF (error/timeout) or FE (teardown state). These labels describe observed control flow; exact user-visible error names remain to be mapped from the menu code.

564f has four observed values:

Value Behavior
00 ordinary active status
01 activation/restart pending; active handler resets transport bookkeeping
FE orderly F8 reset: after F8 FE FE, the next active tick emits standalone FE and disables
FF timeout, received standalone FE, explicit game abort, or fatal link error

When no acceptable byte arrives, the active handler decrements either the short activity countdown (5654) or the 60-tick event watchdog (5656). An expiry writes 564f=FF, clears the transient wait flag, and raises status bit 2 for the caller. Receiving standalone FE also selects FF, although static code alone does not prove which user-facing error message follows. FE status, by contrast, belongs to the orderly F8 FE FE reset exchange.

Separately observed fixed frames are:

F8 00 <P2 input&7f>
F8 FE FE
F8 <P1 input&7f> <P2 input&7f>

F8 frames are used in the later synchronization path rather than the bulk ring-buffer path. F9 is the positive completion acknowledgment for a bulk payload: after committing all announced bytes to its receive ring, the receiver sends F9; a sender with an in-flight transfer clears its transmit count when it receives F9. FA is accepted only in that same in-flight state but does not clear the transfer. A whole-ROM instruction-pattern audit finds all 32 literal COMCREATEDATA service selectors, confined to the identified link routines. Their literal byte set is 00, 01, F8, F9, FC, FD, FE; there is no literal FA producer, while the only explicit FA reference is the receive-side comparison. Register-sourced sends were also manually traced in these routines and do not produce FA. This strongly indicates receive-only compatibility or a reserved peer control, but its semantics remain unknown until another title or wire trace produces it. A standalone FE is sent while tearing the session down and received FE selects an error/termination path; FF is used extensively as an internal no-byte/error sentinel. The ordinary two 7-bit values are controller states. The game stores current local input at 0x570d, current remote input at 0x5715, previous values at 0x5711 and 0x5719, and computes newly pressed edges as current & ~previous. The hardware/game bit mapping is:

Bit Mask Control
0 01 Up
1 02 Down
2 04 Left
3 08 Right
4 10 A
5 20 B
6 40 Option

Thus F8 <value0> <value1> carries the ordered player-1/player-2 input pair. The player-2 endpoint initiates an exchange with F8 00 <P2 input>. Player 1 receives that offer, samples its local input, and replies with F8 <P1 input> <P2 input>. Player 2 consumes the combined reply. Internally, slot 570d is player 1 and 5715 is player 2; which slot is local is selected by the FC/FD role.

F8 FE FE remains a special reset/termination form. F8 00 <value> is always input-related in live code: it is either the player-2 offer or a combined reply whose player-1 input happens to be idle. A stateless decoder cannot distinguish those two byte-identical cases, but direction plus the negotiated FC/FD role does: the FD responder sends offers and the FC requester sends combined replies.

The F8 error-propagation sequence is:

  1. A local 564f=FF while F8 mode is enabled waits for the BIOS transmit queue to empty, sends F8 FE FE, and changes 564f to FE.
  2. A receiver that has consumed F8 and then sees FE jumps to that same send path immediately; it does not need to inspect the third byte first.
  3. On the next FE-status cleanup in the F8 branch, pending receive bytes are drained and status bit 3 is cleared.
  4. The normal active-transport FE cleanup sends standalone FE, returns the dispatcher to disabled, and clears session flags.

This is an error/reset propagation handshake, distinct from a received standalone FE, which directly selects the fatal FF path.

Session status byte (564d)

The individual bits are manipulated as follows. Names marked provisional are behavioral descriptions rather than original game symbols:

Bit Evidence-backed behavior
0 accepted an FC 01 <catalog> identity
1 accepted an FD 01 <catalog> identity; later selects one ordering branch for F8 input exchange
2 per-tick timeout/error event; cleared on tick entry and set by several expired-countdown paths
3 enables the F8 gameplay synchronization branch
4 normal-transport tick busy/progress marker; set on tick entry and cleared after a transmission is started
5 session initialized/handshake-complete phase marker
6 identity frame validated
7 active-session request/enable; gates entry into normal transport

Bits 0 and 1 also explain why F8 values have role-dependent ordering: the identity marker accepted during setup selects which side reads its local input before receiving the peer value and which side receives first before replying.

The game-state transition at 20da5a starts a 150-tick outer timeout, changes the base control to F1, and waits for 5651 == 5652 == 1. It then clears the active-session bit, waits a role-dependent short delay (15 ticks, plus four on the non-FD branch), sets status bit 3, restores base F0, resets both phase snapshots, and proceeds toward F8 gameplay exchange. This is direct evidence that control bit 0 is a symmetric phase barrier and that identity role affects turn ordering/timing after the barrier.

Generated evidence:

  • out/link-primary-linear.asm: primary handshake and gameplay state machine
  • out/link-secondary-linear.asm: secondary communication helpers
  • out/link-support-linear.asm: additional BIOS communication callsites
  • out/reachable.asm: recursively reachable code from cartridge entry
  • make audit-comms: repeatable whole-ROM audit of byte-producing BIOS calls
  • tools/samurai2_link.py: executable game-level reference model for identity, phase barriers, normal transport, F9 acknowledgment, termination, and F8 role-ordered input exchange. It intentionally excludes the still-unverified BIOS/UART/electrical layer.

Disabled alternate exchange block

The code at 21cef5–21cfbb superficially looks like an alternate role-ordered two-byte exchange using the BIOS byte services directly. It is not reachable in the shipped ROM's surrounding routine: one condition jumps around it, while the other path reaches 21cee3 and then executes the unconditional jump at 21cef2 to 21cfbd, skipping the whole block. A whole-ROM direct-call search also finds no entry into 21cef5. Treat this as abandoned/disabled code, not a second on-wire gameplay protocol.

Public status helpers

2077cd returns a three-way link status derived from dispatcher state 14, status bit 7, tick-progress bit 4, and status byte 564f:

  • FFFF when state 14 is active and status bit 7 is set;
  • 0001 when state 14 is inactive/ready, bit 4 is clear, and 564f is zero;
  • 0000 otherwise (including states other than 14).

207794 uses that result as a guarded session activation/reset helper. It leaves the FFFF and 0001 cases unchanged; in the remaining state-14 case it sets bit 7, clears gameplay-sync bit 3, sets 564f=1, restores base control F0, and resets the local/peer phase snapshots. Numerous menu and gameplay state machines call these helpers, which is why the visible scheduling delays around them must not be confused with UART byte timeouts.

Toolchain decision

GNU binutils on the research host has no TLCS-900 target. Mednafen/NeoPop has a complete GPL-2.0 TLCS-900H decoder split across its TLCS-900h directory, but it must be extracted into a small standalone frontend or used through a trace-enabled emulator build. Do not treat a linear sweep from an incomplete opcode table as authoritative: this ISA has prefix-dependent source, destination, and register forms, and game ROMs freely intermix code and data.

Physical/UART layer: documented facts and testable predictions

The closest documented peripheral is Toshiba's TMP95C061 channel 0. Its datasheet specifies that UART transmission shifts the least-significant bit first, raises INTTX0 when the transmit buffer empties, and uses 16 receiver samples per data bit with majority sampling at counts 7, 8, and 9. Channel 0 also has hardware CTS: when CTS becomes high, transmission stops after the current frame; RTS is implemented in software with a general-purpose output. Sources: Toshiba TMP95C061 datasheet, serial configuration and timing, pp. 124–145 and the current Toshiba product/document page.

The surviving SDK-derived descriptions consistently specify the NGPC link as 19200 bit/s, 8 data bits, no parity, one stop bit, with CTS/RTS flow control. This is credible but has not yet been verified from our BIOS or wire capture; the available public discussion explicitly attributes it to an official SDK document rather than reproducing that document. See the archived technical discussion.

At the NGPC's 6.144 MHz source clock, the TMP95C061 baud formula predicts 19200 bit/s from the phi-T0 = fc/4 baud-generator input with divisor 5:

6,144,000 / 4 / 5 / 16 = 19,200 bit/s

That predicts BR0CR clock-select zero and divisor 5 (likely register value 05), 8-bit UART mode with reception and CTS enabled in SC0MOD, and 10 wire bits per byte: one low start bit, eight LSB-first data bits, and one high stop bit. A byte should therefore occupy approximately 520.833 microseconds. These register values are predictions to test against a real NGPC BIOS trace, not yet implementation facts.

The connector-level voltage and pin order remain unverified. Community bench work reports roughly 3.0 V high levels and identifies ground/shield by continuity, but numbering in those reports is explicitly arbitrary. We should derive connector orientation and TX/RX/RTS/CTS mapping ourselves with the passive cable and two consoles rather than copy an unverified diagram.

Physical capture safety

Do not assume the connector pinout or logic voltage. With consoles powered off, first establish ground and continuity through the passive cable. Then measure idle voltages with a multimeter. Only attach a high-impedance analyzer whose input limits are compatible; never drive a pin until its direction and electrical characteristics are established. Record console revisions, battery voltage, cartridge hashes, analyzer sample rate, and exact menu actions.

Physical capture cross-validation (Samurai Shodown 2, VS, 2 matches)

First live two-console capture (logic_analysis/ngpc_samurai2_vs.sal, decoded to logic_analysis/decoded_ch2_ch6.csv, LA @ 24 MHz). Full reconciliation in logic_analysis/PROTOCOL_FINDINGS.md. The wire matches the static model, and resolves items previously marked "requires tracing":

  • On-wire line format confirmed: 19200 8N1, idle-high, non-inverted — 100% valid framing. The long-quoted SDK figure is now measured, not predicted.
  • Wall-clock timing (previously only "ticks"): input exchange and idle heartbeat both run at 33.39 ms (~30 Hz); FC→peer-FD gap ≈ 103 ms.
  • Role assignment on real hardware: the endpoint that got its FC out first became the FC requester / player 1 (sends F8 <P1> <P2> replies); the peer accepted and answered FD, becoming responder / player 2 (sends F8 00 <P2> offers). The P1 reply echoes the P2 offer value 99.9% — direct confirmation of the role-ordered F8 <P1input> <P2input> exchange and the FC/FD arbitration.
  • Button bitmap confirmed exactly as tabulated above (verified again in out/link-primary-linear.asm: slots 0x570d/0x5715, and …,0x7f).
  • F0/F1 base-control and the pre-game F1 phase barrier appear as predicted; F9/F4 bulk-transport bursts appear during menu/setup, not gameplay.

Still open: clean /RTS(0xb2)+/CTS capture on the flow-control lines, and mapping tick constants (15/30/60/150) to wall-clock via the 33.39 ms base.

The game-level protocol is now confirmed by executing the ROM, not just reading it. emulator/race-link/ runs the real Samurai Shodown 2 link code on an extracted TLCS-900H core (from libretro/RACE), with the BIOS COM services implemented as a byte transport to a model peer, and the game's own comms tick 0x206c8f driven once per frame. make -C emulator/race-link test prints:

ROM emitted FC 01 .. 30 (advertise handshake) : PASS
ROM emitted FD 01 .. 30 (responder reply)     : PASS
ROM emitted F0 normal-transport heartbeat     : PASS
ROM emitted F8 00 <input> gameplay offer      : PASS
ROM decoded peer reply into slots 570d/5715   : PASS
RESULT: ALL CHECKS PASSED

Confirmed dynamically on the real code:

  • The state machine walks 00→04→08→0C→14 exactly as documented; writing 00 to 0x564e starts it (the menu's action).
  • Status-bit gating verified by execution and by re-reading the branch: the active handler 0x206f68 does bit 7,(0x564d); jrl Z,0x207447bit 7 set = normal (F0) transport; bit 7 clear + bit 3 set = F8 gameplay branch (0x207447). This corrects the intuitive reading of the bits.
  • The F8 send path emits F8 00 <input> only when 0x564f == 0x01 (the per-frame exchange request), input sourced from (0x56ed) & 0x7f.
  • The receive path stores the peer's F8 <p1> <p2> reply into 0x570d (P1) and 0x5715 (P2); the decoded button bits match the table above under live data.

Three independent sources — static disassembly, physical capture, and now emulated execution — agree. Remaining emulator work is a two-real-instance (two-process) run and menu-driven entry; the protocol layer itself is verified.