Sunday, August 18, 2013

PS/2 Keyboard Emulation with Arduino UNO

I have a growing collection of older keyboards that I really like but have no way of using due to the extinct connectors and protocols that they rely on. As an example, my IBM model F uses a 5-pin DIN connector and my Sun Type 4, an 8-pin mini-DIN. Hardly standard stuff. So I came upon the idea of using an Arduino UNO that's been (let's be honest) collecting dust for a while to interface these with a PS/2 keyboard port. I opted not to use USB because I wanted to implement the whole thing myself from scratch and PS/2 seemed like a gentler first step. It's been a long weekend but I'm very pleased with the fruits of my labour.

PS/2

The PS/2 protocol has it's roots in the third iteration of IBM's personal computer. It uses two thirds of a 6-pin mini-DIN connector to transmit +5V, ground, clock and data as shown below:

The protocol is explained brilliantly by these documents but I'll walk through it again here for the sake of continuity. Serial communication occurs between a 'device' (keyboard or mouse) and a 'host' (computer) on a bi-directional data line. Both the clock and data lines interface using open collectors that are held at +5V by a pull-up resistor (this is the idle state). The device or host can send a signal by lowering the impedance and bringing the line to ground. Because of this I'll use the terms 'pull' low and 'release' high when discussing changes in state. Each frame consists of 11 or 12 bits as follows:
  • 1 start bit (low)
  • 8 data bits
  • 1 parity bit (odd - high if there are an even number of 1's in the data)
  • 1 stop bit (high)
  • 1 'ack' bit (only in host-to-device communications)
The device reads from and writes to the data line on the rising edge of the clock while the host does so on the falling edge (it's best to wait half a cell before actually operating though as the signal's rise time can be quite slow). The clock signal is generated by the device except for when the host is inhibiting the device.

Device-to-host communication

The device can send a signal to the host whenever the data and clock lines are high (idle state). The device writes the signal on the rising edge of the clock and the host reads the signal on the falling edge of the clock. In practice however it's best to perform transitions halfway through the clock cell. Below I've illustrated one frame. Each full clock cycle (falling edge to falling edge) must be between 60 and 100 microseconds.


The device write sequence:
  1. Pull data low to set the start bit
  2. Pull the clock low (host reads start bit)
  3. Release the clock
  4. Set first data bit
  5. Pull clock low (host reads data bit)
  6. Repeat 3-5 for remaining data bits and parity bit
  7. Release clock
  8. Release data to set stop bit
  9. Pull clock low (host reads stop bit)
  10. Release clock
The host read sequence:
  1. Wait for the device to pull the clock low
  2. Read the start bit
  3. Wait for the device to release the clock
  4. Wait for the device to pull the clock low
  5. Read the first data bit
  6. repeat 3-5 for each data bit, the parity bit and the stop bit
Host-to-device communication

Since the device controls the clock, the host has to request a clock signal whenever it wants to send a frame. It does so by pulling the clock low and holding it low for 100 microseconds. With the data high, this is the inhibit state which tells the device that it should stop sending information. The host then pulls data low and releases the clock indicating the device should read the start bit of the frame and begin generating clocks.


This image is actually misleading because the the high values are not really generated by either party - it is the resting state of the line. Think of the color as determining who is in control of a signal.

The host write sequence:
  1. Pull the clock low for > 100 microseconds
  2. Pull data low (start bit)
  3. Release clock
  4. Wait for device to pull clock low
  5. Set first data bit
  6. Wait for device to release clock (device reads bit)
  7. Repeat 4-6 for data bits, parity bit and start bit
  8. Wait for device to  pull clock low
  9. Read ack bit
The device read sequence:
  1. Wait for the host to pull the clock low
  2. Wait for the host to release the clock
  3. Read the start bit
  4. Pull clock low (host writes first data bit)
  5. Release clock
  6. Read first data bit
  7. Repeat 4-6 for each data bit, parity bit and stop bit
  8. Pull clock low
  9. Release clock
  10. Pull data line low (set ack bit)
  11. release clock
Arduino

There are a couple of tricks to implementing this protocol on the Arduino. The first is using the internal pull-up resistors. To write a high signal, set the pinMode to INPUT and write a the high signal with digitalWrite. Similarly, to write a low signal, set the pinMode to LOW and again write the signal with digitalWrite. This  ensures that the correct impedance is selected and allows the Arduino to communicate with the collector on the other end. The second trick is, when emulating a device, to connect the host's +5V pin to the Arduino's Vin pin and the host's ground pin to the Arduino's ground. This ensures that the Arduino has the correct reference voltage.


Both the computer and the keyboard connected to the Arduino. This setup is useful for determining the 'handshake' in which the host enables the device and begins listening.


Computer Handshake

When a PS/2 powers up it performs a self check and then sends a 0xAA to indicate that it is ready to interface with the host. What follows is an exchange that culminates in the computer accepting input from the keyboard. This exchange will vary from machine to machine however I stepped through the process with my ASRock motherboard and discovered this sequence. I suspect most machines are similar:

Power up
device: 0xAA (ready)
host:   0xF2 (ID)
device: 0xFA (acknowledge)
device: 0xAB (first byte of ID)
host:   0xED (set LED)
device: 0xFA
host:   0x00 (second byte of set LED, indicates all LED's off)
device: 0xFA
host:   0xF4 (keyboard enabled)
device: 0xFA
host:   0xED
device: 0xFA
host:   0x00
device: 0xFA
host:   0xF3 (set time delay)
device: 0xFA
host:   0x00 (second byte of time delay, not sure what it means)
device: 0xFA

This is the Arduino library I've written to implement emulate PS/2 hosts and devices. This will likely be out of date very soon so check out the github repository here.

//===-- PS2Emu.h - PS/2 Keyboard Host and Device Emulation -----------------===/
//
// Written by Daniel Kudrow (dkudrow@cs.ucsb.edu)
// August 2013
//
// Library to emulate device and hosts of PS/2 protocol
//
// TODO:
// check data/clock state before acting
// handle host inhibition
// listen for host requests to send
// handle host interrupt
//
//===-----------------------------------------------------------------------===/


#ifndef ps2emu_h
#define ps2emu_h

#include "Arduino.h"

class PS2Emu {
  private:
    int clk; // clock pin
    int dat; // data pin
    int cell; // one half of clock cycle time
    // logical high is high-impedance
    void setHigh(int pin) { pinMode(pin, INPUT); digitalWrite(pin, HIGH); }
    // logical low is low-impedance
    void setLow(int pin) { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); }

  public:
    // construct and initialize
    PS2Emu(int c, int d, int t=40) : clk(c), dat(d), cell(t) {
        setHigh(clk);
        setHigh(dat);
    }
    // read as host (from device)
    unsigned char hostRead();
    // read as device (from host)
    unsigned char devRead();
    // write as host (to device)
    void hostWrite(unsigned char);
    // write as device (to host)
    void devWrite(unsigned char);
    // FIXME listen for host request to send
    bool devListen();
};

#endif


//===-- PS2Emu.cpp - PS/2 Keyboard Host and Device Emulation ---------------===/
//
// Written by Daniel Kudrow (dkudrow@cs.ucsb.edu)
// August 2013
//
// Library to emulate device and hosts of PS/2 protocol
//
// TODO:
// check data/clock state before acting
// handle host inhibition
// listen for host requests to send
// handle host interrupt
//
//===-----------------------------------------------------------------------===/

#include "ps2emu.h"

unsigned char PS2Emu::hostRead() {
  unsigned char data = 0x00;
  unsigned char p = 0x01;

  // discard the start bit
  while (digitalRead(clk) == HIGH);
  while (digitalRead(clk) == LOW);  
  
  // read each data bit
  for (int i=0; i<8; i++) {
    while (digitalRead(clk) == HIGH);
    if (digitalRead(dat) == HIGH) {
      data = data | (1 << i);
      p = p ^ 1;
    }
    while (digitalRead(clk) == LOW);
  }
  
  // read the parity bit
  while (digitalRead(clk) == HIGH);
  if (digitalRead(dat) != p) {
    // TODO handle parity bit
  }
  while (digitalRead(clk) == LOW);
  
  // discard the stop bit
  while (digitalRead(clk) == HIGH);
  while (digitalRead(clk) == LOW);
  
  return data;
}

unsigned char PS2Emu::devRead() {
  unsigned char data = 0x00;
  unsigned char p = 0x01;
  
  // wait for the host to release the clock
  while (digitalRead(dat) == HIGH);
  while (digitalRead(clk) == LOW);
  
  // read start bit
  delayMicroseconds(cell/2);
  setLow(clk);
  delayMicroseconds(cell);
  setHigh(clk);
  delayMicroseconds(cell/2);

  // read data bits
  for (int i=0; i<8; i++) {
    if (digitalRead(dat) == HIGH) {
      data = data | (1 << i);
      p = p ^ 1;
    }
    delayMicroseconds(cell/2);
    setLow(clk);    
    delayMicroseconds(cell);
    setHigh(clk);
    delayMicroseconds(cell/2);
  }

  // read parity bit
  if (digitalRead(dat) != p) {
    // handle parity bit
  }
  delayMicroseconds(cell/2);
  setLow(clk);    
  delayMicroseconds(cell);
  setHigh(clk);
  delayMicroseconds(cell/2);
  
  // send 'ack' bit
  setLow(dat);
  delayMicroseconds(cell/2);
  setLow(clk);    
  delayMicroseconds(cell);
  setHigh(clk);
  setHigh(dat);

  return data;
}

void PS2Emu::hostWrite(unsigned char data) {
  unsigned char p = 0x01;

  // inhibit device transmission
  setLow(clk);
  delayMicroseconds(110);
  // send request to communicate with device
  setLow(dat);
  setHigh(clk);

  // send data
  for (int i=0; i<8 ;i++) {
    while (digitalRead(clk) == HIGH);
    if (data & (1 << i)) {
      setHigh(dat);
      p = p ^ 1;
    } else {
      setLow(dat);
    }
    while (digitalRead(clk) == LOW);
  }
  
  // send parity bit
  while (digitalRead(clk) == HIGH);
  if (p) {
    setHigh(dat);
  } else {
    setLow(dat);
  }
  while (digitalRead(clk) == LOW);

  // send stop bit
  while (digitalRead(clk) == HIGH);
  setHigh(dat);
  while (digitalRead(clk) == LOW);

  // eat the 'ack' bit
  while (digitalRead(clk) == HIGH);
  while (digitalRead(clk) == LOW);

}

void PS2Emu::devWrite(unsigned char data) {
  unsigned char p = 0x01;;

  // set start bit
  setLow(dat);
  delayMicroseconds(cell/2);
  setLow(clk);
  delayMicroseconds(cell);
  setHigh(clk);
  delayMicroseconds(cell/2);

  // set data bits
  for (int i=0; i<8; i++) {
    if (data & (1 << i)) {
      setHigh(dat);
      p = p ^ 1;
    } else {
      setLow(dat);
    }
    delayMicroseconds(cell/2);
    setLow(clk);
    delayMicroseconds(cell);
    setHigh(clk);
    delayMicroseconds(cell/2);
  }

  // set parity bit
  if (p) {
    setHigh(dat);
  } else {
    setLow(dat);
  }
  delayMicroseconds(cell/2);
  setLow(clk);
  delayMicroseconds(cell);
  setHigh(clk);
  delayMicroseconds(cell/2);

  // stet stop bit
  setHigh(dat);
  delayMicroseconds(cell/2);
  setLow(clk);
  delayMicroseconds(cell);
  setHigh(clk);
  delayMicroseconds(cell/2);
}

// FIXME this doesn't work
bool PS2Emu::devListen() {
  if (digitalRead(clk) == HIGH)
    return false;

  int c_stop = micros();
  int c_start = micros();

  while (digitalRead(clk) == LOW) {
    c_stop = micros();
    if (c_stop - c_start >= 100)
      return true;
  }

  return false;
}