Link research notebook¶
Known input¶
- Archive SHA-256:
db47b705f88e415eee80194716cc75f53dc25fac85e680b0d46ab01fde64ee51 - ROM SHA-256:
f0d89fcef129450105e1e45bb29693f30d30a00dd2e92168e7253438dca38a63 - ROM size: 2 MiB
- Header title:
SAMURAI2v000 - Entry PC:
0x200046(file offset0x46) - 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:
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:
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 toF1. The communication tick snapshots the local bit at5651and the received bit at5652; 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 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:
- A local
564f=FFwhile F8 mode is enabled waits for the BIOS transmit queue to empty, sendsF8 FE FE, and changes564ftoFE. - A receiver that has consumed
F8and then seesFEjumps to that same send path immediately; it does not need to inspect the third byte first. - On the next FE-status cleanup in the F8 branch, pending receive bytes are drained and status bit 3 is cleared.
- 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 machineout/link-secondary-linear.asm: secondary communication helpersout/link-support-linear.asm: additional BIOS communication callsitesout/reachable.asm: recursively reachable code from cartridge entrymake audit-comms: repeatable whole-ROM audit of byte-producing BIOS callstools/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:
FFFFwhen state 14 is active and status bit 7 is set;0001when state 14 is inactive/ready, bit 4 is clear, and564fis zero;0000otherwise (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:
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
FCout first became the FC requester / player 1 (sendsF8 <P1> <P2>replies); the peer accepted and answeredFD, becoming responder / player 2 (sendsF8 00 <P2>offers). The P1 reply echoes the P2 offer value 99.9% — direct confirmation of the role-orderedF8 <P1input> <P2input>exchange and the FC/FD arbitration. - Button bitmap confirmed exactly as tabulated above (verified again in
out/link-primary-linear.asm: slots0x570d/0x5715,and …,0x7f). F0/F1base-control and the pre-gameF1phase barrier appear as predicted;F9/F4bulk-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.
Dynamic validation — real ROM executed (emulator/race-link)¶
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→14exactly as documented; writing00to0x564estarts it (the menu's action). - Status-bit gating verified by execution and by re-reading the branch: the active
handler
0x206f68doesbit 7,(0x564d); jrl Z,0x207447— bit 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 when0x564f == 0x01(the per-frame exchange request), input sourced from(0x56ed) & 0x7f. - The receive path stores the peer's
F8 <p1> <p2>reply into0x570d(P1) and0x5715(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.