Skip to content

Instantly share code, notes, and snippets.

@dogtopus
Last active April 15, 2024 23:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dogtopus/aaefed8545139b112a3cba768b7c7403 to your computer and use it in GitHub Desktop.
Save dogtopus/aaefed8545139b112a3cba768b7c7403 to your computer and use it in GitHub Desktop.
SEGA Grand Pianist related stuff

SEGA Grand Pianist

SD Card

SD card slot is on its own separate PCB connected via a 8-pin JST PH (?) jumper cable.

Tested with 2GB SD card formatted with FAT16. Cards smaller than 2GB might also work but are untested (since the smallest cards I got are 2GB).

(Official spec stated that it supports 128MB-2GB SD cards formatted with FAT16.)

FEM files

FEM files are just type 0 (single track) MIDI files. Other types are not supported and the player refuses to load them.

The filename encodes the song information as a 5-character alphanumerical song ID (not necessarily needs to be unique. seems to affect the display order), followed by the song title (15 characters max, supports ASCII as well as Japanese characters, Latin alphabets other than those covered in ASCII range are not supported).

MIDI feature support

Some messages in the MIDI file e.g. program_change as well as meta messages gets ignored by the player during playback and doesn't seem to cause any issue.

Only piano voice is present (if it's not already obvious enough).

Messages besides note_on, note_off and set_tempo are not well tested. Channel usage besides 0 and 1 are not tested. Ticks-per-beat can be set to values other than 48 and the player seems to handle it fine.

Support scripts

The scripts below require Python and mido.

See femboy.py for a type 1 to type 0 MIDI converter that should work well for simple MIDIs despite being really sloppily written.

See sweep.py for a MIDI key sweep generator.

Piano mechanism

The player uses 88 individual solenoids controlled by 4 driver ICs for the key animation. For key scanning, it is implemented as a 11x8 matrix consisted of 3 shift registers (2 SIPO 74HC595 as drivers and 1 PISO 74HC165 as the sensor). Each key has its own anti-ghosting diode. NKRO is theoretically possible with this hardware implementation.

Connector pinout - Keyboard matrix

Connector: JST PH (6 pin)

Pinout:

# Name Description
1 VCC Power (~3.546V)
2 LATCH Driver latch
3 CLK Shift register clock input
4 DI Driver data input
5 DO Sensor data output
6 GND Ground

The key index, starting from the left most key, can be calculated with XXXXYYY, where X is the driver column (from driver shift register #1 pin QA i.e. column 0, to driver shift register #2 pin QC i.e. column 10) and Y is the sensor row (from sensor shift register pin A to pin H i.e. row 0-7) all in binary.

Connector pinout - Power

Connector: JST EH (6 pin)

Pinout:

# Name Description
1 VP Solenoid power (~6.65V with 6.66V Vin)
2 VP
3 VDD Digital input power (~3.6V)
4 VDD
5 PG Solenoid ground
6 PG

Connector pinout - Solenoid data bus

Connector: JST EH (12 pin)

Pinout:

# Name Description
1-8 ~D0-~D7 Parallel data (inverted)
9 RESET Reset
10 XFER Initiate transfer (active high) (?)
11 LATCH Latch data (DDR?) (?)
12 DG Digital input ground

Solenoid data bus protocol

Seems to be a 8-bit DDR (double data rate) bus. Each transfer is 16 bits wide and are done in a single LATCH pulse cycle.

Simplified timing

  • Starts from XFER and LATCH set to low.
  • Host pulls D0-D7 high (logic 0) and pulls XFER high to initiate a transfer.
  • Host writes the high 8 bits data to D0-D7 and pulls LATCH high.
  • Host writes the low 8 bits data to D0-D7 and pulls both XFER and LATCH low.

Refresh rate: ~550Hz (~1.8ms interval)

Transfer rate: ~100kT/s

Nominal LATCH frequency: 400-450 kHz

Protocol

Each transfer seems to have the following format:

CCDD

(C: command, D: data)

A list of currently known commands:

Command Name Description
0x0d select Notify the chip with ID data that all subsequent transfers are targeting it.
0x20-0x22 set_solenoid_byte0 - set_solenoid_byte2 Set the solenoid state (byte 0-2, 24-bit bitfield)
0x23 set_solenoid_power (?) Unknown. Might be related to how much power to use for the solenoid.

The 4 driver chips on board have the ID of 0x11, 0x22, 0x33 and 0x44.

Key state bitfield

+------+-----------------------+-----------------------+-----------------------+-----------------------+
| Chip | 0x44                  | 0x33                  | 0x22                  | 0x11                  |
+------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| Byte | 2     | 1     | 0     | 2     | 1     | 0     | 2     | 1     | 0     | 2     | 1     | 0     |
+------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| Bit  | 95-72                 | 71-48                 | 47-24                 | 23-0                  |
+------+-----------------------+-----------------------+-----------------------+-----------------------+

The leftmost key is bit 0 and the rightmost key is bit 87.

Bit 95-88 seems to be unused and probably should be kept as 0.

Initialization sequence

// 4 empty chip select commands
0d11
0d22
0d33
0d44

// Clear all solenoid status
0d11 231f 2000 2100 2200
0d22 231f 2000 2100 2200
0d33 231f 2000 2100 2200
0d44 231f 2000 2100 2200

Display and control panel

Connector pinout

Connector: JST PH (7 pin)

# Name Description
1 VCC Power (~3.55V)
2 VCC
3 RX Control panel UART RX (main board TX)
4 TX Control panel UART TX (main board RX)
5 RESET Control panel reset
6 GND Ground
7 GND

Protocol

UART @ 9600 8n1. Only the high level menu contexts (enabled menu options, filenames, etc.) are sent over this connection. All graphical works are handled on the control panel microcontroller itself.

TODO

#!/usr/bin/env python3
import copy
import sys
import mido
if __name__ == '__main__':
infile, outfile = sys.argv[1:3]
midin = mido.MidiFile(infile)
midout = mido.MidiFile()
midout.type = 0
trkout = midout.add_track()
midout.ticks_per_beat = midin.ticks_per_beat
tempo = 0
for msg in midin:
if msg.is_meta and msg.type == 'set_tempo':
tempo = msg.tempo
msg = copy.copy(msg)
if hasattr(msg, 'time'):
if msg.time != 0 and tempo == 0:
raise ValueError('Time is not zero when tempo is not set.')
elif msg.time != 0 and tempo != 0:
msg.time = round(mido.second2tick(msg.time, midout.ticks_per_beat, tempo))
trkout.append(msg)
midout.save(outfile)
#!/usr/bin/env python3
import mido
if __name__ == '__main__':
mid = mido.MidiFile()
trk = mid.add_track()
tempo = mido.bpm2tempo(120)
mid.type = 0
trk.append(mido.MetaMessage('set_tempo', tempo=tempo, time=0))
elapsed_ticks = int(mido.second2tick(0.25, mid.ticks_per_beat, tempo))
for i in range(21, 109):
trk.append(mido.Message('note_on', channel=0, note=i, velocity=127, time=elapsed_ticks))
if i > 21:
trk.append(mido.Message('note_off', channel=0, note=i-1, velocity=0, time=0))
trk.append(mido.Message('note_off', channel=0, note=108, velocity=0, time=elapsed_ticks))
print(mid)
mid.save('ZZ999Sweep.fem')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment