SNES Coprocessors: The Simple Ones

This post will cover the two simplest SNES coprocessors: OBC1 and S-RTC. OBC1 is an “OBJ controller” chip while S-RTC is a real-time clock chip. Neither one does any real computation on the chip itself, unlike nearly all of the other SNES coprocessors.

OBC1

This is easily the simplest SNES coprocessor. There is very little to it, so little that it feels like a stretch to call it a coprocessor rather than just a custom mapper chip.

OBC1 is an “OBJ controller” chip used by one game, Metal Combat: Falcon’s Revenge (the sequel to Battle Clash). It has a few registers and ports that enable the game to update a sprite table image in SRAM slightly more easily. …And that’s pretty much all it does.

While this is certainly the easiest coprocessor to emulate, Metal Combat is a Super Scope game, so there’s not much point in emulating OBC1 unless your emulator supports the Super Scope. It is one of the better Super Scope games though, possibly the best one!

The Sprite Table

For a brief overview of the SNES sprite table format, OAM (object attribute memory) consists of 544 bytes (512+32) that store up to 128 sprites. There are 4 bytes + 2 bits stored for each sprite:

  • Byte 0: Lowest 8 bits of 9-bit X coordinate
  • Byte 1: Y coordinate
  • Byte 2: Lowest 8 bits of 9-bit tile number
  • Byte 3: Attributes
    • Horizontal/vertical flip, color palette (0-7), priority (0-3), highest bit of 9-bit tile number
  • Additional 2 bits: Sprite size, highest bit of 9-bit X coordinate

The X coordinate needs to be 9 bits so that sprites can smoothly scroll off either the left or right edges of the screen. Sprites can also be as large as 64x64, in which case there are 319 different X coordinates that will put at least one pixel onscreen (0-255 and 449-511).

The first 512 bytes in OAM consist of bytes 0-3 for each sprite in order. The last 32 bytes in OAM consist of the additional 2 bits for each sprite in order, stored in little-endian order within a byte (bits 0-1 first, then 2-3, then 4-5, then 6-7).

The Memory Map

The cartridge memory map is slightly unique.

ROM addresses are mapped in standard LoROM style. A15 and A23 are ignored, and A16-A22 are shifted right one.

SRAM mapping is different from LoROM. Rather than the standard LoROM SRAM location of $0000-$7FFF in banks $70-$7D + $F0-$FF, OBC1 maps its SRAM to $6000-$7FFF in the I/O area banks ($00-$3F + $80-$BF) as well as banks $70-$7D (and probably $F0-$FF as well).

The eight OBC1 registers/ports are mapped to $7FF0-$7FF7 in the I/O area banks. These addresses in SRAM are only accessible through banks $70-$7D.

What It Actually Does

OBC1 maintains an OAM image in SRAM that the CPU can update through the OBC1 registers. Once the updates are complete, the CPU can initiate a DMA to copy the OAM image from SRAM into actual OAM.

The 8 registers/ports are:

  • 0-3: OAM image lower byte ports
    • For writing the first 512 bytes
  • 4: OAM image upper bits port
    • For writing the last 32 bytes
  • 5: OAM image base address in SRAM (single bit, can only be $7800 or $7C00)
  • 6: OAM image index (0-127)
  • 7: Unknown functionality (at least to me; Metal Combat only ever sets it to $0A)

The idea is that the game can write an OAM index to register 6 and then write to registers 0-4 to update that sprite’s fields in the OAM image. Register 4 is somewhat notable (and is probably the only reason this chip exists) in that it specifically only updates the 2 bits for the current OAM index - the game always writes to the lowest 2 bits of the register, and the chip handles the shifting and masking to apply the write to the correct 2 bits in SRAM. That can theoretically save CPU time for sprite table updates.

And, uh, that’s it! Here’s a screenshot of Metal Combat:

Metal Combat

S-RTC

The S-RTC is a real-time clock chip by Sharp. It was used in one game, an obscure Japan-only RPG called Daikaijuu Monogatari II. It got an English fan translation in 2020 under the title Super Shell Monsters Story II.

Real Time

A real-time clock chip is a piece of hardware that allows software to query for the current calendar time. The details of how exactly this query is shaped depends on the chip, but generally, the software initializes the RTC with a user-supplied current time and then the RTC continuously ticks forward even when the device is powered off. RTC chips from this era generally use a battery-powered crystal oscillator to keep the clock ticking.

Games can use the clock to have events take place at certain times of day, days of week, calendar dates, etc. One of the more well-known examples is Pokemon Gold/Silver/Crystal on the Game Boy Color: you can encounter different wild Pokemon at different times of day, outdoor areas are visibly darker at night, and certain special events only take place on specific days of the week (e.g. the bug catching contest is only on Tuesdays, Thursdays, and Saturdays).

Daikaijuu Monogatari II uses it for…not that much, actually. Per the game’s only guide on GameFAQs, the RTC affects the following:

  • You can tell the game your birthday, and the characters will sing you a birthday song if you enter a Time Temple on your birthday (clearly the most important usage)
  • There are optional dungeons called Time Ruins that you can only enter during a specific hour of the day (AM or PM)
  • You can have an NPC called the Timekeeper send you a special message at a given hour, which heals your party or grants you money
  • You can set a custom message that will appear onscreen at a configurable date and time

Pretty inconsequential really, but oh well!

Memory Mapping

Daikaijuu Monogatari II is one of only two games to use an ExHiROM cartridge, the other being Tales of Phantasia. ExHiROM is similar to HiROM but it’s made specifically for ROM sizes larger than 4MB - Daikaijuu Monogatari II is 5MB and Tales of Phantasia is 6MB. When mapping from SNES addresses to ROM addresses, instead of ignoring A23, ExHiROM inverts A23 and shifts it right one. In other words, accesses to SNES addresses $800000-$FFFFFF map to the first 4MB of ROM, and accesses to SNES addresses $000000-$7FFFFF map to the remaining 1-4MB of ROM.

An ExHiROM ROM address mapping looks something like this:

1
let rom_addr = (address & 0x3FFFFF) | (((address & 0x800000) ^ 0x800000) >> 1);

ExHiROM also maps SRAM to different banks than HiROM does ($80-$BF instead of $20-$3F + $A0-$BF).

There are other games larger than 4MB, such as Star Ocean (6MB) and Tengai Makyou Zero (5MB), but they all use coprocessors with memory controllers that support ROM banking.

The S-RTC chip only has two 4-bit registers that are mapped to unused addresses in the I/O area (banks $00-$3F + $80-$BF): a 4-bit read register mapped to $2800 and a 4-bit write register mapped to $2801. The write register is only used to initialize the current time, after which software will repeatedly read from the read register to get the current RTC time.

S-RTC Time

S-RTC times have 7 fields:

  • Century (19xx or 20xx)
  • Year (0-99)
  • Month (1-12)
  • Day (1-31)
  • Hour (0-23)
  • Minute (0-59)
  • Second (0-59)

The chip also derives the current day of week (0-6) from the calendar date. It’s unclear whether 0 is Sunday or Monday, but it doesn’t really matter because Daikaijuu Monogatari II doesn’t seem to use the day of week for anything.

The calendar does account for leap years, at least the “divisible by 4” rule. Unknown whether it correctly handles the “not divisible by 100 unless divisible by 400” rule, but that won’t matter unless one of these chips is still working in the year 2100.

When reading and writing, all of the fields except for century, month, and day of week are passed as two separate BCD digits (binary-coded decimal). Century is always either $9 (for 19xx) or $A (for 20xx), month fits within a single 4-bit value ($1-$C), and day of week also fits within a single 4-bit value ($0-$6).

Reads return every field in a set order, while writes expect every field other than day of week in the same order. This ends up being 13 values on reads and 12 values on writes:

  1. Second ones digit (0-9)
  2. Second tens digit (0-5)
  3. Minute ones digit (0-9)
  4. Minute tens digit (0-5)
  5. Hour ones digit (0-9)
  6. Hour tens digit (0-2)
  7. Day ones digit (0-9)
  8. Day tens digit (0-3)
  9. Month ($1-$C)
  10. Year ones digit (0-9)
  11. Year tens digit (0-9)
  12. Century ($9-$A)
  13. Day of week (0-6) (only on reads)

In-Game

I’m going to use the game itself to discuss how S-RTC reads/writes work. I’ll use the fan translated version so that the text is in English.

The game boots to the title screen (after a short intro) which confirms that the fan translation patched successfully:

DM2 Title

Immediately after the title screen, the game prompts you to input the current time to initialize the clock:

DM2 Enter Time

2020-11-10 is the date that the English fan translation released, which is a reasonable enough default date. Not sure why the hour defaults to an invalid value though…

I’ll go ahead and set the date/time to 2024-02-09 08:40, which is the current time in my time zone as I write this. This results in the game writing the following sequence of values to the S-RTC write register:

$E $4 $D $E $0 $0 $0 $0 $4 $8 $0 $9 $0 $2 $4 $2 $A $D

From the list of values just up above, it’s easy enough to pick out the slice of 12 values that corresponds to setting the time:

$0 $0 $0 $4 $8 $0 $9 $0 $2 $4 $2 $A

Reformatted to make it clearer which values correspond to which fields:

$0 $0 (second)
$0 $4 (minute)
$8 $0 (hour)
$9 $0 (day)
$2    (month)
$4 $2 (year)
$A    (century)

That leaves $E $4 $D $E $0 at the start and a trailing $D at the end.

It’s probably reasonable to assume that writing $E is some sort of command start marker and writing $D is a command end marker, which makes the $4 and $0 command bytes. In that case, $0 is obviously the command for setting the current time, but it’s unclear what command $4 is (or if the chip supports any other commands). However, given that only one game uses this chip and it only ever writes when setting the current time, it’s not really necessary to know what commands other than $0 do as long as the RTC chip behaves the way the game expects it to. You could even treat $E $4 $D $E $0 as a prelude sequence and completely ignore the concept of commands.

After setting the current time, the game gives you a settings menu and then plays a short ~3 minute intro cutscene. For some reason it sets the current time again at the end of the intro, which will cause the time to immediately be off by ~3 minutes. Not sure why it does that, but anyway, once you’re in-game you can view the current time in the “Admin” sub-menu:

DM2 Admin Menu

S-RTC reads aren’t terribly complicated. Whenever the game wants to know the current time, it reads from the S-RTC read register 15 times in sequence. It seems to expect the first and last values to be $F (some sort of boundary marker maybe?) while the middle 13 values are the S-RTC time values listed above.

If you want to correct the current time, you can use the Admin > Time setting to round the current time to the nearest minute (which you’ll need to do multiple times if you didn’t have the foresight to set the time several minutes ahead):

DM2 Time Command

Emulating a Clock

The only challenges in emulating the S-RTC chip are challenges inherent to emulating any real-time clock chip. Mainly, how do you keep the clock ticking at the correct speed, both while the game is running and while it’s not running?

To me, the most obvious approach that handles both cases is to advance the RTC chip based on current system time in some sub-second unit (e.g. milliseconds or nanoseconds). Every so often (at least multiple times per frame), do the following: get the current system time, advance the RTC based on the difference between the current system time and the system time at last update, then record the current system time in the RTC state.

Advancing the RTC means updating all of the time fields appropriately, which is kind of tedious code but not super hard to write:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
fn get_days_in_month(month: u128, year: u128) -> u128 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => if year % 4 == 0 { 29 } else { 28 },
        _ => panic!("Invalid month: {month}"),
    }
}

struct SRtcTime {
    last_update_time_nanos: u128,
    nanos: u128,
    second: u128,
    minute: u128,
    hour: u128,
    day: u128,
    month: u128,
    year: u128,
    century: u128,
}

impl SRtcTime {
    fn advance(&mut self, current_time_nanos: u128) {
        let elapsed_nanos = current_time_nanos - self.last_update_time_nanos;
        self.last_update_time_nanos = current_time_nanos;

        self.nanos += self.elapsed_nanos;
        let elapsed_seconds = self.nanos / 1_000_000_000;
        self.nanos %= 1_000_000_000;

        if elapsed_seconds != 0 {
            self.increment_second(elapsed_seconds);
        }
    }

    fn increment_second(&mut self, elapsed_seconds: u128) {
        self.second += elapsed_seconds;
        let elapsed_minutes = self.second / 60;
        self.second %= 60;

        if elapsed_minutes != 0 {
            self.increment_minute(elapsed_minutes);
        }
    }

    fn increment_minute(&mut self, elapsed_minutes: u128) {
        self.minute += elapsed_minutes;
        let elapsed_hours = self.minute / 60;
        self.minute %= 60;

        if elapsed_hours != 0 {
            self.increment_hour(elapsed_hours);
        }
    }

    fn increment_hour(&mut self, elapsed_hours: u128) {
        self.hour += elapsed_hours;
        let elapsed_days = self.hour / 24;
        self.hour %= 24;

        // Increment days one-by-one in case multiple month boundaries are crossed
        for _ in 0..elapsed_days {
            self.increment_day();
        }
    }

    fn increment_day(&mut self) {
        self.day += 1;
        if self.day > get_days_in_month(self.month, self.year) {
            self.day = 1;
            self.increment_month();
        }
    }

    fn increment_month(&mut self) {
        self.month += 1;
        if self.month > 12 {
            self.month = 1;
            self.increment_year();
        }
    }

    fn increment_year(&mut self) {
        self.year += 1;
        if self.year > 99 {
            self.year = 0;
            self.century += 1;
        }
    }
}

You don’t actually need to store everything as a u128 but I think it makes the example code simpler to avoid needing to do integer type conversions on every other line, and the current time definitely needs to be a u128 if you’re going to use nanoseconds.

With that out of the way, all that’s left is how to make the RTC state persist when the emulator is closed and re-opened. The advance() method will handle catching up the RTC to the current time from the previous RTC state as long as there’s a way to actually get that previous RTC state.

Serializing it to disk periodically or on emulator close works well enough, just like you’d do to make SRAM persist between emulator runs. The exact serialization format and where you serialize the RTC state is a matter of preference. You could append the RTC state to the end of the SRAM file, or you could store the RTC state in a separate file to keep the SRAM file as just SRAM. Either way works as long the emulator knows how to load the RTC state on startup.

Some emulators provide RTC editors so that players can directly edit the current time instead of needing to wait for specific times or dates for in-game events. The only hard part of doing that is figuring out the frontend/UI for it - once that exists, you can just directly edit your RTC state to modify the current RTC time.

An alternate possible approach to RTC advancing is to advance the RTC state based on emulator clock ticks while the game is running. This is technically more accurate in that it will more accurately handle sub-frame timing, but sub-frame RTC timing really doesn’t matter unless you’re designing or running a test ROM that will test RTC functionality (in which case the RTC emulation can’t depend on external state such as the system time). Downsides to this approach are that you’ll get clock drift if the emulator speed doesn’t exactly match actual hardware speed, and also you still have to handle catching up the RTC time during emulator startup.

The Other RTC Chip

This is getting ahead a bit, but there’s one other SNES game that uses an RTC chip: Tengai Makyou Zero, which uses the SPC7110 coprocessor with an embedded Epson RTC-4513 chip. This one is a bit more complex than the S-RTC but the core concepts are basically identical.

updatedupdated2024-02-092024-02-09