Sega Pico
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 at200001
, 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_dpad ; Read the joypad (controller #1) move.b $800003,d0 eor.b #255,d0 and.b #%10011111,d0 move.b d0,joypad rts
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;
Copera
Emulation
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
LAB_00006648
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
LAB_0000665a
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;
SekInterrupt(2);
return;
}
Keyboard Pico
Emulation
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:


Controls
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)

Korean model has some layout differences (e.g. brackets position) but can still be fully used.
Limitations
- 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 against0x2a (0b00101010)
instead of0x3f (0b00111111)
as usually seen in games that don’t require the Keyboard Pico. The bits for0x2a
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 requires0b100
to be set instead of requiring all0b111
bits).- In the PicoDrive fork, a separate “Test Page” was added to set value
0x2a
.
- In the PicoDrive fork, a separate “Test Page” was added to set value
- 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.
- However, the page 6 value at address
Key sets
- Keys are encoded and compared by default with AT scan code set
- Includes a few custom values for non-standard keys
0x13
: CJK0x17
: Romaji0x64
: Home0x67
: Sound
- Key code table offsets in Kantan Waku Waku Keyboard
0x717fc
: alphanumeric0x7182c
: 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
- Includes a few custom values for non-standard keys
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
):
- Write
0x60
to0x200001
- 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 LAB_000006ae 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
- Read data in a loop
- An internal data index is kept by the keyboard, which advances with alternated writes of
0x20
and0x0
to0x200001
; - 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: break elif sign == +1 and value > 0: break # Ready to parse value value = value & 0xf # Ask to move index to next value if sign == -1: (0x200001) = 0 sign = +1 else: (0x200001) = 0x20 sign = -1
- An internal data index is kept by the keyboard, which advances with alternated writes of
- Write
0x60
to0x200001
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 of0x20
and0x0
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
Methodology
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:
#ifndef FAMEC_NO_GOTOS
#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
; ...
set_shift
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.