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)
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
- 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
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.
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
- 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
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; }