Created
July 16, 2017 19:20
-
-
Save stecman/89ba440e97c8bafe8d1bb0ebe02025dc to your computer and use it in GitHub Desktop.
Watch PS/2 keyboard signals to add extra keyboard noise with relays (AVR / Arduino)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Read output from a PS/2 keyboard and make a relay click per key-press. | |
* | |
* On an Arduino board, the connections are: | |
* | |
* PS/2 clock -> pin 2 (PD2) | |
* PS/2 data -> pin 3 (PD3) | |
* | |
* Relays are on PORTB starting from PB0 to PB[NUM_RELAYS - 1]. Look for | |
* a pin out diagram of your Arduino to see where PORTB is connected. On | |
* a Pro Mini, this is pins 8 to 13 for example. | |
*/ | |
#include <avr/io.h> | |
#include <util/delay.h> | |
#include <avr/interrupt.h> | |
#include <stdbool.h> | |
// Number of relays connected to PORTB | |
#define NUM_RELAYS 4 | |
// Timing configuration for relays | |
// The timer rate should be the average desired delay between a keypress and sound | |
// Relays always trigger on the _next_ timer event to keep the activation period consistent | |
#define TIMER_RATE_MS 2 | |
#define RELAY_PULSE_MS 35 | |
#define RELAY_COOLDOWN_MS 50 | |
#define CLK_IN PD2 | |
#define DATA_IN PD3 | |
inline void setup() | |
{ | |
// Set up PS/2 clock line as an input | |
DDRD &= ~(_BV(CLK_IN) | _BV(DATA_IN)); | |
PORTD = _BV(CLK_IN) | _BV(DATA_IN); // Pull high (prevents floating when disconnected) | |
// Set up all of PORTB as relay outputs | |
DDRB = 0xFF; | |
PORTB = 0xFF; // Pull high (relay inputs are active low) | |
// Enable INT0 interrupt for listening to PS/2 clock line | |
EIMSK |= (1<<INT0); | |
// Trigger INT0 on rising clock edge | |
EICRA |= _BV(ISC11) | _BV(ISC10); | |
// Setup timer 1 to handle relay trigger without blocking | |
OCR1A = (F_CPU/1024/1000) * TIMER_RATE_MS; // Value to count to (compare register A) | |
TCCR1B |= (1 << WGM12); // Reset timer on compare A match | |
TIMSK1 |= (1 << OCIE1A); // Trigger interrupt on overflow | |
} | |
// | |
// Clicker | |
// | |
// Start TIMER1 | |
inline void startRelayTimer() | |
{ | |
TCCR1B |= (1 << CS12) | (1 << CS10); // Run timer with a prescaler of 1024 | |
} | |
// Stop and clear TIMER1 | |
inline void stopRelayTimer() | |
{ | |
TCCR1B &= ~( (1 << CS12) | (1 << CS10) ); // Stop timer | |
TCNT1 = 0; // Reset timer value | |
} | |
// Relay should be turned on next time the timer fires | |
// These values are a count of TIMER_RATE_MS periods | |
const static uint8_t kRelayFire = (RELAY_PULSE_MS/TIMER_RATE_MS) + (RELAY_COOLDOWN_MS/TIMER_RATE_MS); | |
// Relay should turned off once this value is reached, but not marked available | |
const static uint8_t kRelayCooldown = (RELAY_COOLDOWN_MS/TIMER_RATE_MS); | |
// Relay is off and ready for use | |
const static uint8_t kRelayOff = 0; | |
// List of relay states | |
volatile uint8_t relayState[NUM_RELAYS] = {}; | |
/// Make a sound on an available relay | |
void triggerClick(uint8_t keycode) | |
{ | |
uint8_t startIndex = 0; | |
// Give a different sound for some keys | |
switch (keycode) { | |
// This position makes a clicky sound | |
case 0x0D: // Tab | |
case 0x29: // Esc | |
case 0x59: // Shift | |
case 0x66: // Backspace | |
case 0x76: // Esc | |
startIndex = 2; | |
break; | |
// This position makes a "thunk" sound | |
case 0x5A: // Enter | |
case 0x11: // Alt | |
case 0x14: // Control | |
case 0x12: // Shift | |
startIndex = 1; | |
break; | |
// This position makes a typewriter-esque sound | |
default: | |
break; | |
} | |
// Disable interrupts around this as we're changing volatile data | |
cli(); | |
// Trigger the first free relay next time the timer fires | |
// The relay is always operated on the next timer event to prevent keystrokes | |
// from being "lost" if they start too close to the timer firing and don't | |
// make an audible click. | |
for (uint8_t i = startIndex; i < NUM_RELAYS; ++i) { | |
if (relayState[i] == kRelayOff) { | |
relayState[i] = kRelayFire; | |
break; | |
} | |
} | |
// Enable interrupts again | |
sei(); | |
// Start the timer to turn relays off (if it's not already running) | |
startRelayTimer(); | |
} | |
ISR (TIMER1_COMPA_vect) | |
{ | |
bool updated = false; | |
for (uint8_t i = 0; i < NUM_RELAYS; ++i) { | |
if (relayState[i] > kRelayCooldown && relayState[i] <= kRelayFire) { | |
// Turn on the relay | |
PORTB &= ~_BV(i); | |
// Move state towards off and ready | |
relayState[i]--; | |
updated = true; | |
} | |
else if (relayState[i] > kRelayOff && relayState[i] <= kRelayCooldown) { | |
// Turn off the relay when it's about to become free again | |
// The relay won't be available again until the state is zero again | |
// This allows a cooldown so the relay is not immediately activated | |
// again before is has time to phsyically move to its off state. | |
PORTB |= _BV(i); | |
relayState[i]--; | |
updated = true; | |
} | |
} | |
// If nothing was updated, stop the timer to avoid wasting cycles | |
if (!updated) { | |
stopRelayTimer(); | |
} | |
} | |
// | |
// PS/2 scancode reading | |
// | |
// Ah yes, a one byte buffer | |
volatile uint8_t lastKeystroke = 0x0; | |
volatile bool keystrokeWaiting = false; | |
/// Read data from PS/2 interface when the clock line goes low | |
ISR (INT0_vect) | |
{ | |
// What number bit in the current sequence are we receiving | |
static int8_t bitCount = -1; | |
// The byte currently being received | |
static uint8_t data = 0x0; | |
// Handle current bit in the 11 bit keyboard -> host transmission | |
// Bits start at -1 to make collecting these bits easier | |
switch (bitCount) { | |
case -1: | |
// The first bit is always zero (start bit), so nothing to do | |
bitCount++; | |
break; | |
case 8: | |
// Parity bit | |
bitCount++; | |
break; | |
case 9: | |
// Store complete keystroke data | |
lastKeystroke = data; | |
keystrokeWaiting = true; | |
// Stop bit - reset | |
data = 0x0; | |
bitCount = -1; | |
// Wait for the extended low clock period at the end of the signal | |
// This is again some crappy software debouncing | |
_delay_us(100); | |
break; | |
default: | |
// Push the DATA_IN bit from PIND to the byte of data we're receiving | |
// The bit in PIND is moved to bit zero, then shifted to the current bit | |
data |= ( (PIND >> DATA_IN) & 0x1 ) << bitCount; | |
// Move to the next bit | |
bitCount++; | |
break; | |
} | |
// Wait for the ~10-16KHz clock to go high again | |
// This is a shitty software fix instead of a hardware one for debouncing | |
_delay_us(45); | |
} | |
// Flag to trigger no feedback for the following keypress | |
// This is currently only used for ignoring key releases | |
bool isRelease = false; | |
// Flag to indicate the next keystroke is an extended scancode | |
bool isExtended = false; | |
// Map of keys that are currently pressed | |
// This is requried to ignore repeats from the keyboard controller, as repeats have | |
// no markings to identify them as an automatic repeat. This is a huge waste of | |
// memory, since boolean values are being stored as bytes. | |
// There are two arrays declared so we don't have to use a 16-bit index | |
uint8_t pressedKeys[256] = {}; | |
uint8_t pressedKeysExtended[256] = {}; | |
inline void handlePendingKeypress() | |
{ | |
uint8_t keycodeForTrigger = 0x0; | |
// Decide quickly what to do with a pending keypress, while interrupts are disabled | |
if (keystrokeWaiting) { | |
cli(); // Disable interrupts while reading volatiles | |
// Parse any modifiers in the scancode | |
switch (lastKeystroke) { | |
case 0xE0: // Extended key code | |
// This indicates the scancode more than one byte long | |
// We cheat a bit by letting the extra bytes trigger key presses, | |
// but relying on the sound trigger timeout to debounce | |
isExtended = true; | |
break; | |
case 0xF0: // Break code (key released) | |
isRelease = true; | |
break; | |
default: // Other keystroke | |
keycodeForTrigger = lastKeystroke; | |
break; | |
} | |
// Reset state | |
keystrokeWaiting = false; | |
sei(); // Enable interrupts again | |
} | |
// Handle detected state after interrupts are enabled again | |
if (keycodeForTrigger != 0x0) { | |
uint8_t* _pressRegister; | |
if (isExtended) { | |
// If we're dealing with an extended code, switch press state stores | |
isExtended = false; | |
_pressRegister = &pressedKeysExtended; | |
} else { | |
_pressRegister = &pressedKeys; | |
} | |
if (isRelease) { | |
// Mark key as no longer pressed, but make no sound | |
_pressRegister[lastKeystroke] = false; | |
isRelease = false; | |
} else if (!_pressRegister[lastKeystroke]) { | |
// New key being pressed - trigger sound | |
_pressRegister[lastKeystroke] = true; | |
triggerClick(keycodeForTrigger); | |
} | |
} | |
} | |
int main(void) | |
{ | |
// select minimal prescaler (max system speed) | |
CLKPR = 0x80; | |
CLKPR = 0x00; | |
// Set up inputs, outputs and timers | |
setup(); | |
// Ignore any pending interrupt that occurred during setup | |
EIFR |= (1<<INTF0); | |
// Enable interrupts | |
sei(); | |
for (;;) { | |
// Watch for any processing we need to do | |
// Everything else is handled by interrupts | |
handlePendingKeypress(); | |
} | |
return 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This makefile is set up to work with the Arduino bootloader | |
# Depending on your device, you may need to adjust the PROGRAMMER | |
# line to point to the right serial port. | |
# | |
# To use, just run "make flash" with an arduino connected | |
DEVICE = atmega328p | |
CLOCK = 8000000 | |
PROGRAMMER = -c arduino -P /dev/ttyUSB0 -b57600 | |
SOURCES = $(shell find -name '*.c' -or -name '*.cpp' -or -name '*.S') | |
OBJECTS = $(SOURCES:.c=.o) | |
AVRDUDE = avrdude $(PROGRAMMER) -p $(DEVICE) | |
COMPILE = avr-gcc -Wall -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE) | |
COMPILE += -I -I. -I./lib/ | |
COMPILE += -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums | |
COMPILE += -ffunction-sections -fdata-sections -Wl,--gc-sections | |
COMPILE += -Wl,--relax -mcall-prologues | |
COMPILE += -std=gnu99 | |
# symbolic targets: | |
all: $(SOURCES) main.hex | |
.c.o: | |
$(COMPILE) -c $< -o $@ | |
.S.o: | |
$(COMPILE) -x assembler-with-cpp -c $< -o $@ | |
# "-x assembler-with-cpp" should not be necessary since this is the default | |
# file type for the .S (with capital S) extension. However, upper case | |
# characters are not always preserved on Windows. To ensure WinAVR | |
# compatibility define the file type manually. | |
.c.s: | |
$(COMPILE) -S $< -o $@ | |
flash: all | |
$(AVRDUDE) -U flash:w:main.hex:i | |
fuse: | |
echo For computing fuse byte values see the fuse bit calculator at http://www.engbedded.com/fusecalc/ | |
# Xcode uses the Makefile targets "", "clean" and "install" | |
install: flash fuse | |
clean: | |
find -name '*.d' -exec rm {} + | |
find -name '*.o' -exec rm {} + | |
rm -f main.hex main.elf | |
# file targets: | |
main.elf: $(OBJECTS) | |
$(COMPILE) -o main.elf $(OBJECTS) | |
main.hex: main.elf | |
rm -f main.hex | |
avr-objcopy -j .text -j .data -O ihex main.elf main.hex | |
avr-size --format=avr --mcu=$(DEVICE) main.elf | |
# Targets for code debugging and analysis: | |
disasm: main.elf | |
avr-objdump -d main.elf | |
cpp: | |
$(COMPILE) -E main.c |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment