Using a Gamecube ASCII Keyboard Controller with libogc

February 15, 2021 —

My weekend project to figure out how to access key press event information from the old Gamecube ASCII Keyboard Controller was a success. This controller was extremely niche, only released in Japan and only intended for playing a single game, Phantasy Star Online Episode I & II.

Because it is such a niche accessory, the Gamecube homebrew development kit devkitPro (and libogc, the primary library you end up using with devkitPro when doing Gamecube/Wii development), does not have any support for it. No one has cared enough to add it apparently over the past almost 20 years.

I have a project idea in mind that centered around the use of this keyboard controller, so figuring out how to access key press events via libogc was step number one.

On the Gamecube, access to the controllers is done via the "SI" (serial interface) hardware, which operates using a proprietary serial protocol. Libogc includes an implementation of this protocol that is pretty low-level and also unfortunately completely undocumented with very few examples. After much digging into the recesses of the interwebs, the only existing code that I could find that supported the keyboard controller, was the gcn-si driver found in the gc-linux sources. Unfortunately, because this is a driver added to a fork of the Linux kernel intended for the Gamecube, it does not use libogc at all and provides its own unique implementation of the Gamecube SI protocol, which is fairly different from the libogc implementation.

It was still useful to me to read through this to understand some aspects of it though. In fact, I can say that personally, I found it easier to understand the gcn-si driver than I did the libogc SI code, heh.

However, the end goal was of course to implement something that was going to interoperate with libogc, as I will absolutely want to use libogc for my project anyway. Initially I went into this thinking I was going to have to fork libogc and update bits of the SI code, as, while there were a few scattered references to the keyboard controller in the code, it clearly wasn't fully featured and I didn't even see any indications that it had been tested (because I could find no code that use it anywhere else).

There were two useful bits of documentation from the "Yet Another Gamecube Documentation" website. Specifically, section 5.8, which discusses the SI registers, and section 9 which talks about input devices (controllers) and includes a short sub-section on the keyboard controller. This is really helpful information, but the descriptions are light and, of course, there is no example code or anything.

My previous efforts, discussed in my previous post, with getting remote debugging via GDB working proved very worth-while here for exploring how the SI protocol worked with libogc in light of the lack of documentation.

After banging my head against it for a while and going down a few dead-ends (because I really don't know what I'm doing with this, heh. I have no real background with working with hardware at this level!) I came up with a simple working example that I uploaded to this Github repository. This is a very bare-bones example project, but it's definitely something to build on. I also don't claim this is bug-free! I've ONLY tested it on real hardware fortunately, so I do feel pretty good about it. That said, I would not be surprised to find out that I am using the libogc SI API incorrectly in places.

Anyway, there were basically three key things to figure out with this. Fair warning! I am almost certainly doing some things wrong here with the SI API! I would love it if someone more knowledgeable than me stumbled across this post and was able to point out my mistakes.

Detection

This feels like a pretty basic thing to do. But it was a bit trickier than I realized, and I had to fiddle around with it a lot to get something that worked reliably before stumbling across this piece of code from the Nintendont project that initialized all of the controller ports in a way that seemed to always result in my subsequent code that detects the keyboard controller working completely reliably.

#if defined(HW_DOL)
#define SI_REG_BASE 0xCC006400    // gamecube
#elif defined(HW_RVL)
#define SI_REG_BASE 0xCD006400    // wii
#else
#error Hardware model unknown? Missing a devkitPPC preprocessor definition somewhere...
#endif

#define SIREG(n)               ((vu32*)(SI_REG_BASE + (n)))
#define SICOMCSR               (SIREG(0x34))

#define PAD_ENABLEDMASK(chan)  (0x80000000 >> chan)

static void SI_AwaitPendingCommands(void) {
    while(*SICOMCSR & 0x1);
}

// ------------

u32 buf[2];
for (int i = 0; i < 4; ++i) {
    SI_GetResponse(i, buf);
    SI_SetCommand(i, 0x00400300);
    SI_EnablePolling(PAD_ENABLEDMASK(i));
}
SI_AwaitPendingCommands();      // not actually sure if this is needed, but it doesn't appear to 
                                // hurt anything ...

Some basic definitions for the SI hardware registers off the start, but the for loop below it just initializes all of the controller ports as if they are normal (non-keyboard) controllers. This still seems wrong to me, but it does seem to cause the subsequent detection code to not fail to find the keyboard controller intermittently.

The way that Nintendont was using SI_GetResponse before SI_SetCommand was interesting to me. After seeing this I went back and checked libogc and noticed one place where it also did this. If I understand the SICnINBUFH and SICnINBUFL registers correctly, then I guess this clears the register's buffer first (the hardware double-buffers it) if there was some leftover data that was not yet read. I know I wasn't doing this in my initial attempts and I can't help but wonder if this may have been the source of some of my difficulties.

int keyboardChan = -1;

for (int i = 0; i < 4; ++i) {
    u32 type = SI_DecodeType(SI_GetType(i));
    if (type == SI_GC_KEYBOARD)
        keyboardChan = i;
}

But anyway, once you have all the controller ports initialized (even though they were initialized with command 0x00400300 which is apparently just for "standard" controllers), this method of using SI_GetType and SI_DecodeType seems to work great from that point on.

Initializing

Once the keyboard controller has been detected, initializing it is quite simple. Just issue command 0x00540000 to the SI channel that the keyboard controller is connected to.

u32 buf[2];
SI_GetResponse(chan, buf);
SI_SetCommand(chan, 0x00540000);
SI_EnablePolling(PAD_ENABLEDMASK(chan));
SI_TransferCommands();
SI_AwaitPendingCommands();

Again, there may be some small mistakes here ... Also again note that use of SI_GetResponse followed by SI_SetCommand as before. I'm unsure if I really need both SI_TransferCommands() and SI_AwaitPendingCommands() (SI_AwaitPendingCommands() is not included in libogc, but I included it above), but using them both here like this hasn't caused any problems for me so far ... ?

Initializing the keyboard is important, as without doing this the hardware will not report key press events via the SI protocol. The 0x00540000 command enables polling.

Reading Key Press Events

Now, the good stuff. We just read from the SICnINBUFH and SICnINBUFL registers (where n is the channel number) for the keyboard controller's channel. The easiest way to do this with libogc is via SI_GetResponse.

u32 buffer[2];
u8 key1, key2, key3;
if (SI_GetResponse(chan, buffer)) {
    key1 = buffer[1] >> 24;
    key2 = (buffer[1] >> 16) & 0xff;
    key3 = (buffer[1] >> 8) & 0xff;
}

The other (first) double-word value in buffer doesn't appear to ever hold useful data, regardless of what keys are pressed.

The keyboard controller hardware is limited in the number of simultaneous key presses it can recognize. And the exact number depends on which keys you press specifically. Sometimes three keys will be registered at the same time, and other times only two keys can be registered at most. If you go over the limit (whether it is 2 or 3 depending on the specific keys pressed) the keyboard hardware reports either 0x01 or 0x02 for all three key "slots." A value of 0x00 in any of the three key "slots" indicates that nothing is being pressed.

Otherwise, the values returned in these three bytes indicates the key being pressed.

The "Fn" key is the only one on the keyboard controller that does not trigger an event when pressed. The "Fn" key is used by the hardware to switch the key code reported only when one of the other keys with a bordered text label is held at the same time. For example, pressing the "Insert" key by itself will be reported as 0x4d, but pressing "Fn" and "Insert" at the same time will be reported as only a single key press 0x0a.

Pressing other meta/modifier keys (e.g. "Shift", "Ctrl" or "Alt") at the same time as other keys does not alter the key code values. They will all be reported by the same individual key code values as if they were pressed by themselves. Similarly, the "Caps Lock" key does not trigger any state or anything like that and is not treated specially in any way by the keyboard hardware.