Expands the original notaz’s Pico doc with new findings from reversing a few Pico games.

Memory map

  • 200000-200001: PS/2 peripheral I/O, (range guessed; observed 1 byte r/w at 200001, see communication protocol)
  • 800003: Buttons I/O - bit 5 and 6
    • Can be checked, e.g. credits screen of The Lion King: Adventures at Pride Rock
    • Can be overriden on read (i.e. or’d with 0x60), therefore assumed to be set, e.g. ECC Junior no Hajimete Eigo Vol. 3 Patty-chan no o-Tanjoubi, Pocket Monsters: Suuji o Tsukamaeyou!
    • Can be ignored on read, e.g. The Muppets On The Go!
        ; Read the joypad (controller #1)
        move.b    $800003,d0
        eor.b	#255,d0
        and.b	#%10011111,d0
        move.b	d0,joypad
  • 800005-80000b: MSB/LSB for Pen X/Y coordinates
    • Can be used as entropy source, e.g. to pick one of 3 pico logo animations in Kiteretsu Daihyakka: Edo ni Itte Kiteretsusai-sama ni Au nari
  • bff800-bfffff: Copera I/O (range guessed, see external interrupt protocol)
    • bff801: Callback bitmask set by the system, used to select which callback functions will be called by IRQ2;
    • bff805: State variable set both by system and games;



Support was added on a PicoDrive fork to make these games playable. We are still missing sound chip and peripheral I/O. In particular, the microphone is required to control some games.

External interrupt protocol (based on Copera no Chikyuu Daisuki)

All Copera games have a handler defined for the external interrupt (a.k.a. IRQ2). It is composed of multiple callbacks which are set in a fixed order by games, then called conditionally based on a bitmask set by the system.

On the following IRQ2 handler, we named co_cb_mask as the bitmask, and work_ext_cbs as the work RAM address used to store pointers to callback functions:

cbs = &work_ext_cbs;
puVar1 = &DAT_00bff800;
co_state = 0xb;
cb_i = co_cb_mask;
if (((co_cb_mask & 1) != 0) || ((co_cb_mask & 2) != 0)) {
  co_state = 0xb;
  if (*work_ext_cbs_i == 1) {
    cb_i = (*work_ext_cbs)();
  else if (*work_ext_cbs_i == 2) {
    cb_i = (*work_ext_cbs2)();
if ((cb_i & 4) != 0) {
  cb_i = (*(code *)cbs[2])();
  puVar1[5] = 0xb;
if ((cb_i & 8) != 0) {
  cb_i = (*(code *)cbs[3])();
  puVar1[5] = 0xb;
if ((cb_i & 0x10) != 0) {
  cb_i = (*(code *)cbs[4])();
  puVar1[5] = 0xb;
if ((cb_i & 0x20) != 0) {
  cb_i = (*(code *)cbs[5])();
  puVar1[5] = 0xb;
if ((cb_i & 0x40) != 0) {
  cb_i = (*(code *)cbs[6])();
  puVar1[5] = 0xb;
if ((cb_i & 0x80) != 0) {
  (*(code *)cbs[7])();
  puVar1[5] = 0xb;
return in_D0;

After callbacks are set, the game performs some checks in a loop, which are only breaked when two work RAM variables have the same value. One of them (named work_co_chk1) is updated by game code during the check loop:

00006642 22 79 00        movea.l    (work_co_chk1).l,A1
         ff 0b 94
                     XREF[1]:     0000665c(j)
00006648 52 89           addq.l     #0x1,A1
0000664a b3 fc 00        cmpa.l     #0xff0b94,A1
         ff 0b 94
00006650 66 00 00 08     bne.w      LAB_0000665a
00006654 22 7c 00        movea.l    #0xff0994,A1
         ff 09 94
                     XREF[1]:     00006650(j)
0000665a 12 98           move.b     (A0)+,(A1)=>work_co_chk_init
0000665c 51 c8 ff ea     dbf        D0w,LAB_00006648
00006660 23 c9 00        move.l     A1,(work_co_chk1).l
         ff 0b 94

The other (named work_co_chk2) is updated in the IRQ2 handler, by callback 3:

undefined4 FUN_00006522(void) {
  undefined4 in_D0;

  if (work_co_chk2 == work_co_chk1) {
    work_co_wait = 0; // check loops can now be breaked
  else {
    work_co_chk2 = (undefined4 *)((int)work_co_chk2 + 1);
    if ((undefined4 **)work_co_chk2 == &work_co_chk1) {
      work_co_chk2 = (undefined4 *)&work_co_chk_init;
    co_chk1_val = *(undefined *)work_co_chk2;
    work_co_wait = 1;
    co_state = 0xe;
  return in_D0;

These updates can also be seen in a trace log, which was added under ./cpu/fame/famec.c:

#define WRITE_BYTE_F(A, D)      \
    ctx->write_byte(A, D); \
    if ((A >= 0xbff800 && A <= 0xbfffff) || A == 0xff0b94 || A == 0xff0b98 || A == 0xff0994) { \
        printf("tr: w8 @%04X (%04X),%04X\n", GET_PC, A, D);  \

#define WRITE_WORD_F(A, D)      \
    ctx->write_word(A, D); \
    if ((A >= 0xbff800 && A <= 0xbfffff) || A == 0xff0b94 || A == 0xff0b98 || A == 0xff0994) { \
        printf("tr: w32 @%04X (%04X),%04X\n", GET_PC, A, D);  \

Our implementation at ./pico/pico.c does just enough to pass these loops. When the game sets state 0xbff805 = 0x9, all callbacks have already been set, so we set bitmask 0xbff801 |= 0x08 to let callback 3 be called by IRQ2, which in turn is called at a given interval, allowing both work RAM check variables to eventually meet:

if ((PicoPicohw.copera[0x1] > 0)
        && PicoPicohw.line_counter - prev_line_cnt_irq2 > 33) {
  prev_line_cnt_irq2 = PicoPicohw.line_counter;

Keyboard Pico


Support was added on a PicoDrive fork.

Note that activating most minigames requires interacting with the Storyware via the pen button. Since these games don’t render a pen cursor, you can use the emulator’s pen position overlay to guide you. Below are two examples of positions where minigames in Keyboard Pico: Kantan Waku Waku Keyboard can be activated:


Key Mapping
F8 Switch input method (a new one was added for PS/2)
F9 Toggle PS/2 connection on/off
F10 Toggle pen position overlay on/off

Keyboard layout (based on ANSI 104)

Japanese model:

Korean model has some layout differences (e.g. brackets position) but can still be fully used.


  • Key bindings are hardcoded at compile time (see ./platform/common/input_pico.h and ./platform/common/inputmap_kbd.h), missing logic to allow reading and storing to configuration file.

Technical overview

Test page

  • Some of these games (e.g. Kantan Waku Waku Keyboard) contain a “Pico Keyboard Test Mode” that can be activated by switching to page 6, then resetting the game while holding down the pad’s red button.
    • However, the page 6 value at address 80000d is compared against 0x2a (0b00101010) instead of 0x3f (0b00111111) as usually seen in games that don’t require the Keyboard Pico. The bits for 0x2a cannot be set by flipping pages, since those pages will only cover page sensors in a sequential manner. Presumably, this test mode would have been activated by manually covering page sensors with other objects. The games account for this mechanism in their page parsing logic, since they only check the most significant bit to detect page changes (e.g. page 3 only requires 0b100 to be set instead of requiring all 0b111 bits).
      • In the PicoDrive fork, a separate “Test Page” was added to set value 0x2a.
    • It’s worth noting that Kibodeu Piko (a Korean game that uses the Keyboard Pico) compares page values with sequential bits, so the masked bits detection is something exclusive to the Japanese games, and not related to the Keyboard Pico protocol. This game uses an entirely different keyboard test mode.

Key sets

  • Keys are encoded and compared by default with AT scan code set
    • Includes a few custom values for non-standard keys
      • 0x13: CJK
      • 0x17: Romaji
      • 0x64: Home
      • 0x67: Sound
    • Key code table offsets in Kantan Waku Waku Keyboard
      • 0x717fc: alphanumeric
      • 0x7182c: others
    • Key code table values
        # scan code set 2 (AT, default)
        LANG=C grep -Pbr '\x16\x1e\x26\x25\x2e\x36\x3d\x3e\x46\x45'
        # grep: Keyboard Pico - Kantan Waku Waku Keyboard (Japan).md: binary file matches
        # grep: Kitty to Minna no Keyboard Pico - Kitty to Minna no Hajimete no Keyboard! (Japan).md: binary file matches
        # grep: Pet to Issho ni Tanoshiku Asobo - Pasokon Pico (Japan).md: binary file matches
        # grep: Keyboard Pico 2 - Sawattemiyou! Yoiko no Hajimete Keyboard (Japan).md: binary file matches
        # scan code set 1 (XT)
        LANG=C grep -Pbr '\x15\x1d\x24\x2d\x2c\x35\x3c\x43\x44\x4d'
        # [same as above]
        # key index table
        LANG=C grep -Pbr '\x10\x16\x04\x11\x13\x18\x14\x08\x0e\x0f'
        # grep: Keyboard Pico - Kantan Waku Waku Keyboard (Japan).md: binary file matches

Communication protocol (based on Kantan Waku Waku Keyboard)

All communication is done through a single port memory-mapped at 0x200001.

There are at least 13 sets of 4 bits of data that can be read (data names taken from the “Pico Keyboard Test Mode”):

Index Data Notes
0x0 M5ID Always 0b0001
0x1 M6ID Always 0b0011
0x2 Data size Always 0b0100
0x3 Pad 1 RLDU This and the 2 sets below seem to be for a Mega Drive 6-button joypad. Although the Pico pad has a joystick and 2 buttons, these aren’t mapped to this data
0x4 Pad 2 SACB See Pad 1 notes
0x5 Pad 3 RXYZ See Pad 1 notes
0x6 L& Keyboard type Unknown, there’s logic to check for value 0b111 but without any observable effect
0x7 Caps / Num / Scroll Lock 1 of 3 least significant bits is set when the corresponding lock is active
0x8 Make / Break code Either 0b1110 or 0b0111 is set to register key down or key up
0x9 Data 7654 Most significant 4 bits of key code
0xa Data 3210 Least significant 4 bits of key code
0xb   Unknown
0xc   Unknown

This data is parsed as follows (based on subroutine 00000706):

  1. Write 0x60 to 0x200001
  2. Wait for command to be processed by the keyboard, usually implemented as a busy loop
     000006a8 2f 07           move.l     D7,-(SP)
     000006aa 3e 3c 03 09     move.w     #0x309,D7w
                          XREF[1]:     000006b6(j)
     000006ae 48 e7 ff 00     movem.l    {  D7 D6 D5 D4 D3 D2 D1 D0},-(SP)
     000006b2 4c df 00 ff     movem.l    (SP)+,{ D0 D1 D2 D3 D4 D5 D6 D7}
     000006b6 51 cf ff f6     dbf        D7w,LAB_000006ae
     000006ba 2e 1f           move.l     (SP)+,D7
     000006bc 4e 75           rts
  3. Read data in a loop
    • An internal data index is kept by the keyboard, which advances with alternated writes of 0x20 and 0x0 to 0x200001;
    • To signal that the read data is ready to be parsed after each of the above writes, the sign of the byte value is switched. While the sign doesn’t switch, the game waits in a busy loop;
    • Pseudocode
        i = 0
        sign = -1
        while i < 0xc:
            # Wait for sign to switch
            j = 0
            while j < 0x46:
                value = (0x200001)
                if sign == -1 and value < 0:
                elif sign == +1 and value > 0:
            # Ready to parse value
            value = value & 0xf
            # Ask to move index to next value
            if sign == -1:
                (0x200001) = 0
                sign = +1
                (0x200001) = 0x20
                sign = -1
  4. Write 0x60 to 0x200001

While this applies to the test mode, there’s a slight difference when parsing this data in-game (subroutine 0005ff0a):

  • After the initial write of 0x60, there are two subsequent writes of 0x20 and 0x0 without reading any data. The rest of the loop proceeds as usual.
  • In this case, we need to advance the index 1 time (vs. 2 times), then advance as usual for the rest of the loop (1 time for each write).

An implementation in the PicoDrive fork can be seen in ./pico/pico/memory.c


For reversing these interactions over the PS/2 port, a general approach is to trace memory read/writes on the port, check where they are stored in registers or RAM, then trace those RAM addresses.

The following snippets add this tracing to PicoDrive:

#define READ_BYTE_F(A, D)           \
       D = ctx->read_byte(A) & 0xFF; \
       if (A == 0x200001 || A == 0xffffe6 - 9) { \
               printf("tr: r @%04X (%04X),%04X\n", GET_PC, A, D);  \

#define READ_WORD_F(A, D)           \
       D = ctx->read_word(A) & 0xFFFF; \
       if (A == 0x200001 || A == 0xffffe6 - 9) { \
               printf("tr: r16 @%04X (%04X),%04X\n", GET_PC, A, D);  \

#define WRITE_BYTE_F(A, D)      \
    ctx->write_byte(A, D); \
       if (A == 0x200001 || A <= 0xffffe6 || A >= 0xffffe6 - 0xc) { \
               printf("tr: w @%04X (%04X),%04X\n", GET_PC, A, D);  \

Another example, to check how the game handles the Shift key:

#define NEXT                    \
    printf("PC: %p\n",PC); \
    if (GET_PC == 0x72254 || GET_PC == 0x73054) { \
        u32 d0 = DREGu32(0) & 0xFFFFFFFF; \
        printf("pcx: @%04X %04X\n", GET_PC, d0);  \
    } \
    if (GET_PC == 0x72f30) { \
        printf("pcx: SHIFT=1\n");  \
    } \
    if (GET_PC == 0x73060) { \
        printf("pcx: SHIFT=0\n");  \
    } \
    FETCH_WORD(Opcode);         \
    goto *JumpTable[Opcode];

To figure out how keycodes were parsed beyond the test mode, it was a matter of tracing references to the scan code set.

First, subroutine 0005ff28 does a read loop as described above in the protocol, followed by processing the read values in subroutine 0005ff6c, including the keycode:

0005ff84 10 2e ff f6     move.b     (-0xa,A6),D0b ; Data 3210
0005ff88 74 00           moveq      #0x0,D2
0005ff8a 14 2e ff f7     move.b     (-0x9,A6),D2b ; Data 7654
0005ff8e e9 4a           lsl.w      #0x4,D2w
0005ff90 80 42           or.w       D2w,D0w

Which then gets compared with the scan code set:

; parsed keycode will be stored in D0b
0007224e 4e b9 00        jsr        FUN_0005ff28
         05 ff 28
; ...

; store keycode in memory
00072284 13 c0 00        move.b     D0b,(DAT_00ff1b28).l
         ff 1b 28
; ...

; read value from scan code set 2
000722ca 12 30 00 00     move.b     (0x0,A0,D0w*0x1)=>DAT_000717fc,D1b
; compare with parsed keycode
000722ce b2 39 00        cmp.b      (DAT_00ff1b28).l,D1b
         ff 1b 28
000722d4 67 00 00 0e     beq.w      LAB_000722e4
000722d8 52 40           addq.w     #0x1,D0w
000722da 0c 40 00 30     cmpi.w     #0x30,D0w
; loop
000722de 6d ea           blt.b      LAB_000722ca

Parsing the Shift key:

00072d1e 0c 00 00 12     cmpi.b     #0x12,D0b ; Shift key scan code = 0x12
00072d22 67 00 02 0c     beq.w      set_shift
00072d26 60 00 03 a4     bra.w      end
; ...

00072f30 33 fc 00        move.w     #0x1,(is_shift_active).l
         01 00 ff
         1b 4e
00072f38 60 00 01 92     bra.w      end

Following data accesses can be tricky with the m68k calling convention: there’s no function prologue and epilogue for saving and restoring registers, and sometimes there’s more than one return value passed in registers D0 and D1. Usually these lead to incomplete decompilation.

The ideal way to automate this work is through taint analysis, but no tools support m68k. Besides doing manual fixups in the decompilation, I found the usual diff tracing approach to help in finding relevant code paths.