Aug 15, 2019

Let's Build a Keytar - Part II: Keys

For the kays, I bought a used M-Audio Keystation 49e for 20 bucks. It is a pretty simple midi controller with 4 octaves of standard size keys. It also got a modulation and pitch bend wheel as well as octave shift buttons. I might reuse some of these parts too.

Let's jump into an exciting reverse engineering adventure!



When I opened the device I was surprised how it is organized. There is a main single layer PCB with all the jacks (Midi, USB and sustain pedal), the power switch and the main processor (some ST microcontroller) on the backside. A second PCB contains the 9 V power jack (going straight to a 7805). The buttons and wheels are on a third board screwed to the front panel. Everything is interconnected with flat cables and connectors with standard 2.54 mm spacing. The most interesting bit however, is the keyboard module, which contains its own 8-bit microcontroller, a W78E05D. This is in charge of the scanning of the keyboard and touch velocity sensing. It connects via a 14 pin cable to the main board.

The main board (front)

Main board, back side

The keyboard module

The velocity board

Keyboard scanning and velocity sensing microcontroller
Initially I thought of directly using the midi signal by the main controller and reading it in by an Arduino or Teensy. However, after playing with the keyboard I noticed that the controller crashes from time time and can only recover by turning it off and on again. That's why I decided to only use the keyboard module. As I could not find any information about the pinout or the protocol used by the device, I had to reverse engineer it.

From the traces on both boards it was easy to find out that the pins on the far right (furthest away from the USB connector) are +5 V and Gnd, respectively. All other lines go to I/O pins on the main micro controller. With the keyboard module unatached, I tested using a 10 k resistor to Gnd and 5V, which of the lines are high impedence, i.e. inputs and which are outputs. It turned out that pins 0, 1 and 2 are outputs of the main controller, and 3 to 11 are all inputs. (pin number 0 is the pin next to Gnd; I know this seems unconventional, but that's how I attached my logic analyzer later and I think it's less confusing if we stick to a consistent labeling). With my logic analyzer I then tried reverse engineering the protocol that was used. I had to solder an additional row of pins to the board so I can sniff the communication. The picture below shows how it is attached.

Logic analyzer attached to the connection between main board and keyboard module (pin numbers annotated)
The next step is to find out what each of the lines is for. My first test was to press down the low C on the keyboard and look at the signals in the logic analyzer. Two things happen here: on a macroscopic level, we see a square-wave pattern on the top most line (pin 11). We will investigate that later. Zooming in, we see a short parallel data transfer on the lines. Lines 4-11 carry 8 bits of information, line 3 seems to be the request signal from the keyboard to the main microcontroller, and line 2 the acknowledge from the main µC to the keyboard.
Macroscopic view of the signals when the lowest C is pressed down: Square wave and parallel data transfer
So there is a simple asynchronous handshake happening: If the keyboard wants to send a byte (e.g. if a key is pressed), it requests a transmission by pulling the req signal low. The main µC will respond with pulling ack low if it is ready. Then the keyboard puts the data on the 8 bit parallel bus and then sets req high again (this is comparable to the rising edge of a clock signal). The main µC will read the data and acknolege it by pulling ack high again. The transfer of one byte takes around 10 µs.

Data transfer when the lowest C is pressed. 8 bit values are given in hex.
To see how the data should be interpreted, I pressed different keys with different velocities. This way, I found out that the first byte is the key code, starting at 0x98 for the lowest C, 0x99 for C# and so on, and the 2nd byte is the velocity, ranging up to 0x7F. It can be noted that the key code always ha the MSB set to 1, while the velocity byte has MSB = 0. This way we can synchronize the transmitter very easily to the 2-byte packets. Also, when ignoring the MSB of the key code, the numerical values of the keys and velocities are already complient with the Midi standard (0x98 & 0x7F = 0x30, key C2).

Key up (release) messages are encoded as key down with 0 velocity, as can be seen in the next figure.
Data transfer of a key up / release message. It can be clearly seen that the velocity byte is 0.
Knowing this, the essence of reading the keyboard seems quite easy. Questions remain, what the square wave pattern is and what lines 0 and 1 are for.

Let's start with investigating the sqaure waves. The waves are present while any key is held down and have a frequency of about 5.5 kHz. However, the patterns differ in dependence of the key that is pressed. For example, a C creates the pattern on line 11, C# on line 10, D on line 9 and so on. The G# is again visible on line 11, but with a phase shift. This can be easily seen when C augmented chord is played (see next figure). The first 0 on line 11 corresponds to the G#, the second to the C. In the same time slot, the E is visible on line 7.
Square wave pattern of a C augmented chord in the lowest octave
After thinking a while about these patterns, it hit me: This is the keyboard scanning patterns and the controller is just reusing the same I/O pins. As the request and acknowledge lines are low anyways, these patterns are ignored by the main µC. Another interesting thing to note is that the pattern starts even slightly before the key down message is sent. This indicates that the velocity is probabably measured as a time difference between two button events.

For the meaning of lines 0 and 1 I took a look at the startup sequence on just the four control / handshake lines, Here, things happen at a much larger timescale. After a short pulse on the output pins of the main µC, all lines are held low for about 800 ms. Then, pin 0 and pin 2 (ack) go to their idle state 'high'. Pin 1 remains low,  however with some irregular glitches I could not reproduce. From these findings I assume pin 0 is an active-low reset signal, the meaning of pin 1 I could not understand yet.

Startup sequene on the control lines between main µC and keyboard module
However, with all these findings, it is a rather straightforward task to read use the keyboard module with an Arduino. I connected the parallel port to pins A0-A5, 2, 3 (LSB to MSB), the reset line to pin 4, ack to pin 5 and request to pin 6. The mysterious line 1 of the keyboard is connected to GND. I wrote a little program that translates the data from the keyboard to Midi, using the AltSoftSerial library (pin 9 is the Midi output, hardcoded).
#include &ltAltSoftSerial.h&gt

const int parallel_port[] = {14, 15, 16, 17, 18, 19, 2, 3};
const int reset_pin = 4;
const int ack_pin = 5;
const int req_pin = 6;

AltSoftSerial altSerial;

void setup() {
  pinMode(reset_pin, OUTPUT);
  pinMode(ack_pin, OUTPUT);
  pinMode(req_pin, INPUT);
  for (int i = 0; i < 8; i++) {
    pinMode(parallel_port[i], INPUT);
  }
  Serial.begin(115200);
  digitalWrite(ack_pin, HIGH);
   // reset
  digitalWrite(reset_pin, LOW);
  delay(10);
  digitalWrite(reset_pin, HIGH);
  
  Serial.println("Start");
  altSerial.begin(31250);
  //release all notes
  for (int note = 0x0; note < 0x60; note ++) {
    noteOn(0, note, 0x00);
  }
  changeProgram(0,87);
}
void loop() {
  static int state = 0;
  static byte note = 0;
  if (state == 0) {
    //idle
    if (!digitalRead(req_pin)) {
      digitalWrite(ack_pin, LOW);
      state = 1;
    }
  } else if (state == 1) {
    if (digitalRead(req_pin)) {
      byte x = read_port();
      if (x & 0x80) {
        note = x &0x7F;
      } else {
        byte vel = x;
        noteOn(0, note, vel);
      }
      digitalWrite(ack_pin, HIGH);
      Serial.println(x, HEX);
      state = 0;
    }
  }
}
byte read_port() {
  byte x = 0;
  for (int i = 0; i < 8; i++) {
    x |= digitalRead(parallel_port[i]) << i; 
  }
  return x;
}
void noteOn(int ch, int pitch, int velocity) {
  altSerial.write(0x90 + ch);
  altSerial.write(pitch);
  altSerial.write(velocity);
}
void changeProgram(int ch, int prog) {
  altSerial.write(0xC0 + ch);
  altSerial.write(prog);
}


After some testing, I suspect that the line 1 is some kind of mode select pin. If you put it High, the keyboard will spit out the numbers 0xF6 to 0xFF, looping endlessly. No idea what this function is for. Additionally, I measured the time of the handshake with the Arduino. It takes around 73 µs, so it is quite slow compared to the original µC. However I'm sure one could optimize this by using direct port manipulation. In the keytar, I will use a faster Teensy controller anyways.

No comments:

Post a Comment