Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

Nintendo DS RTC Serial Protocol: GPIO Edges, Command Bytes, and BCD Time

Emulate the Nintendo DS RTC over serial GPIO, from command and bit edges to BCD time, status modes, alarms, and deterministic save-state tests.

The Nintendo DS real-time clock is not a memory-mapped collection of ordinary time registers. Software communicates with a serial RTC through GPIO-like lines, selecting the device, clocking command and data bits, and interpreting register bytes that use packed BCD and control flags. The interface is slow compared with CPU execution, but its edges, bit order, direction, and transaction boundaries are all part of the emulated machine.

This matters because a title may set the date, read alarm state, switch between 12-hour and 24-hour time, or poll the clock repeatedly. A host system clock can supply a convenient initial time, but it cannot replace the peripheral protocol. The emulator still has to produce coherent command responses, preserve the clock across save states, and define what happens when the guest changes a field or resets the RTC while a serial transaction is in flight.

The wire-level transaction

At the software-visible layer, the ARM7 controls the RTC through a small group of serial signals represented in the DS I/O space: chip select, serial clock, data direction, and the shared serial data line. The guest drives command and write data into the device, or changes the data direction and samples read data back. A transaction begins when chip select becomes active and ends when it is released. Starting a new transaction resets command and bit counters; it does not mean every register value in the RTC itself is reset.

Each serial byte is assembled from individual clocked data bits. The device needs to know whether the current transfer is still receiving a command, receiving command arguments, or shifting response bytes. Keep input_bit, input_byte, command, argument_index, output_bit, and output_index explicit. A code path that simply notices a write to the RTC data register and copies eight bits at once misses incomplete transactions, a change of direction, and reads performed at the wrong edge.

The command byte identifies a register group and whether the operation is a read or a write. The documented standard command family includes status registers, date/time fields, alarms, clock adjustment, and a free register. Command bits also have a transfer-order convention: DS RTC implementations account for bit ordering and, on some transfers, reversed command nibble interpretation. Do not infer a universal “SPI mode” solely from the shared clock and data wires; use the DS programming reference and a known-good implementation to validate which edge samples the line and how command bits are assembled.

Read and write payload lengths depend on the selected register. A date/time read returns a series of fields, while a status read may return one byte and have read side effects for selected status bits. A write may consume a single status value, several clock bytes, or an alarm tuple. After command decoding, use a register-specific transfer length and semantics; accepting an arbitrary number of bytes can cause the next command to be misinterpreted as leftover payload.

BCD fields and status interpretation

The RTC date/time representation is not a binary integer timestamp. Year, month, day, weekday, hour, minute, and second are stored in byte-sized fields, with decimal digits packed into nibbles. For example, decimal 42 is encoded as 0x42, not 0x2A. Validation should check each BCD nibble separately before applying the field’s range. Values that are syntactically valid BCD may still be out of range for a month, hour mode, or second field.

The hour byte includes mode and AM/PM information. In 12-hour mode, a marker bit distinguishes morning from afternoon and the numeric field follows 12-hour convention. In 24-hour mode, the field spans 00 through 23 and the mode bit is different. If software changes the status register’s hour-format setting while a time is stored, the emulator must convert or reinterpret the current hour consistently with the chip’s behavior. Simply toggling one status bit while leaving the hour encoding untouched can turn noon into midnight or expose invalid values.

Date progression requires calendar rules. Seconds carry to minutes, minutes to hours, hours to day, then month and year. The day-of-week field advances independently but in lockstep with the date. Month lengths depend on the month, and leap-year behavior should follow the RTC’s documented two-digit year convention rather than the host language’s default calendar range. If the software sets an invalid BCD byte, define the device-compatible sanitization behavior explicitly; silently normalizing it with a host datetime library can diverge from established console software.

Status registers expose oscillator, reset, 12/24-hour selection, alarm, and interrupt-related controls. Some status bits are self-clearing or read-cleared; some control alarm matching or periodic output. Keep writable control state separate from pending event flags. Alarms can match only selected fields, and an “ignore this component” bit changes the comparison. The RTC’s interrupt or output behavior is not identical to a CPU interrupt: the console GPIO/interrupt integration determines how the device signal becomes observable to software.

Serial state must survive partial accesses

A game can pause or be interrupted between the first and eighth bit of a command. The emulated peripheral must preserve the current bit count and shift register until the next active clock edge, not prematurely decode a partial command. Likewise, a CPU reset or chip-select release during a byte must abort or finalize according to the documented pin behavior. These cases are easy to miss if the implementation offers only atomic byte methods to the rest of the emulator.

The serial data line changes ownership with direction. During a read, the RTC drives response bits and the CPU samples them. During a write, the CPU drives command or payload bits and the RTC samples. Ensure that a device output cannot leak into a write transaction and that the line returns to its inactive electrical state at transaction end. Save states must include line levels and direction state as well as the internal command counters, because restoring only the date and time is insufficient if a save occurred mid-read.

Clock advancement also needs a clear policy. The RTC can advance from emulated machine time, from a saved virtual epoch, or from host time under an explicitly chosen compatibility policy. Mixing a host wall-clock read on every game request with emulated cycle-based ticking creates discontinuities when the host clock changes or when emulation is paused. A deterministic mode should derive seconds from emulated cycles, preserve its fractional remainder, and serialize it. A real-time mode should document that behavior and still produce a coherent snapshot per transaction.

A BCD and command-payload fixture

The following small fixture validates BCD values and packs a date/time payload in a fixed seven-byte field order. It is not a replacement for the RTC command/edge machine; its purpose is to make field validation and hour conversion testable before serial I/O is layered on top.

from dataclasses import dataclass


def from_bcd(value, minimum, maximum):
    low, high = value & 0x0F, (value >> 4) & 0x0F
    if low > 9 or high > 9:
        raise ValueError("invalid packed BCD digit")
    decoded = high * 10 + low
    if not minimum <= decoded <= maximum:
        raise ValueError("BCD field is outside its documented range")
    return decoded


@dataclass(frozen=True)
class DsRtcDateTime:
    year: int
    month: int
    day: int
    weekday: int
    hour: int
    minute: int
    second: int

    def validate(self):
        if not 0 <= self.year <= 99 or not 1 <= self.month <= 12:
            raise ValueError("year or month is invalid")
        if not 1 <= self.day <= 31 or not 0 <= self.weekday <= 6:
            raise ValueError("day or weekday is invalid")
        if not 0 <= self.hour <= 23 or not 0 <= self.minute <= 59:
            raise ValueError("time field is invalid")
        if not 0 <= self.second <= 59:
            raise ValueError("seconds are invalid")


assert from_bcd(0x42, 0, 99) == 42
assert DsRtcDateTime(26, 10, 4, 0, 17, 30, 5).validate() is None

A production implementation should validate real month-specific day limits, hour-mode bits, weekday encoding, and register side effects at the protocol boundary. Keep the generic checker small and place mode-dependent rules in the command handler so tests can assert that a status write changes how the next hour byte is interpreted.

Emulator test plan

Begin with a complete read transaction. Assert chip select, issue one known command bit by bit, switch data direction, sample every response bit, and confirm the byte sequence and final line levels. Repeat with the reverse command-bit order if the reference implementation’s command normalization requires it, but keep the externally documented serial ordering separate from any internal nibble transformation. A passing static date test should not conceal the wrong edge or bit order.

Then test writes for status, date/time, and alarms. Verify that invalid BCD digits are handled deliberately, that a valid status change affects hour conversion, that an alarm mask ignores only the selected fields, and that a status read clears only the documented flags. Start an operation, stop after a partial command, release chip select, and issue a fresh valid command; the new transaction must not inherit old shift state. Repeat with the save-state boundary after each possible bit position.

For calendar tests, advance one second at a time across 59 to 00, 23:59 to midnight, month ends, year rollover, and leap-day cases supported by the chosen year mapping. Compare a fully deterministic run before and after save-state restore. For host-clock mode, freeze or inject the time source so tests remain reproducible. Keep alarm comparisons and periodic events independent from the calendar tick so a status write cannot accidentally skip a second.

Acceptance criteria

A reliable DS RTC implementation exposes the serial transaction as state, decodes command and direction correctly, validates BCD by field, tracks hour-format flags, advances calendar fields with a defined time source, and preserves partial transactions in save states. Diagnostics should report pins, edge count, command, argument and response positions, decoded register, and time state. This lets a title-specific clock defect be traced to serial framing, field conversion, calendar rollover, or interrupt behavior rather than patched with a different host date.

Related:

Sources:

Comments