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:
- Pull data low to set the start bit
- Pull the clock low (host reads start bit)
- Release the clock
- Set first data bit
- Pull clock low (host reads data bit)
- Repeat 3-5 for remaining data bits and parity bit
- Release clock
- Release data to set stop bit
- Pull clock low (host reads stop bit)
- Release clock
The host read sequence:
- Wait for the device to pull the clock low
- Read the start bit
- Wait for the device to release the clock
- Wait for the device to pull the clock low
- Read the first data bit
- 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:
- Pull the clock low for > 100 microseconds
- Pull data low (start bit)
- Release clock
- Wait for device to pull clock low
- Set first data bit
- Wait for device to release clock (device reads bit)
- Repeat 4-6 for data bits, parity bit and start bit
- Wait for device to pull clock low
- Read ack bit
The device read sequence:
- Wait for the host to pull the clock low
- Wait for the host to release the clock
- Read the start bit
- Pull clock low (host writes first data bit)
- Release clock
- Read first data bit
- Repeat 4-6 for each data bit, parity bit and stop bit
- Pull clock low
- Release clock
- Pull data line low (set ack bit)
- 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;
}