ToeKeys USB macro keyboard for toes.

ToeKeys is an 8 button interface that applies transformations to
button state before passing the state to a function to send USB
keystrokes.

Requirements:

  • Arduino Micro. May work on other atmega32u4 based products
  • 8x double throw switches
  • 16 IO pins connected as shown in toekeys_schematic.png
    Features:
  • Fast, reliable and trivial debounce code made possible
    by the use of double throw switches.
  • Fast button handling. Polling all buttons and applying all
    transformations happens within 4µs.
  • Supported button state transformations: reverse, toggle,
    repeat, strike/hold.
  • Send up to 6 keys per button.
/*  ToeKeys v0.05: USB macro keyboard : For Arduino Micro  */

#include <Arduino.h>

void setup()
{
  pinSetup();
  Keyboard.begin();
}

void loop()
{
  word button_state = pinModify(pinRead());
  runMacros(button_state);
}
/* ==============Abstract===============
  ToeKeys is an 8 button interface that applies transformations to
  button state before passing the state to a function to send USB
  keystrokes.
  
  Requirements:
    - Arduino Micro. May work on other atmega32u4 based products
    - 8x double throw switches
    - 16 IO pins connected as shown in toekeys_schematic.png
  Features:
    - Fast, reliable and trivial debounce code made possible 
      by the use of double throw switches.
    - Fast button handling. Polling all buttons and applying all
      transformations happens within 4µs.
    - Supported button state transformations: reverse, toggle, 
      repeat, strike/hold.
    - Send up to 6 keys per button.
*/
// ================Macros==================
const word repeat_rate = 200;  //milliseconds
const byte key_hold    = B00000100;
/* key_hold is standard keyboard behaviour with keydown
   and keyup coinciding with button press and release.
   If not key_hold both key reports will be sent on button press*/
/* A maximum of 6 keys can be held simultaneously (not including 
   shift, ctrl, alt). Support for sending much longer key 
   sequences is on the wishlist but not by increasing the size of
   this array. */
const char key_code[8][7] = {{'a'},
                             {' '},
                             {KEY_LEFT_CTRL},
                             {'f'},
                             {'g'},
                             {'h'},
                             {"groovy"},
                             {"kool\n"}
                            };

void runMacros(word pin_state) {
  if (!(pin_state >> 8)) return; 
  //quick exit if nothing changed
  for (byte i=0;i<8;i++){
    if ((pin_state >> (8+i)) & 1){
      if (!((key_hold >> i) & 1)){
        if ((pin_state >> i) & 1)
          macroStrike(i);
      } 
      else {
        macroHold(i, (pin_state >> i) & 1);
      }
    }
  }
}

void macroStrike(byte id) {
  Keyboard.print(key_code[id]);
}

void macroHold(byte id, byte pressed) {
  for (byte i=0;i<6;i++){
    if (pressed)
      Keyboard.press(key_code[id][i]);
    else
      Keyboard.release(key_code[id][i]);
  }
}

// ===============Pins===================
/* Port masks for reading IO ports directly .. low byte is
   primary state data. High byte is opposite throw of switches
   for debounce code  */
#define PB_MASK B11111100  // MO,MI,D11-D8  .. high byte
#define PF_MASK B11110011  // A5-A0         .. low byte
#define PD_MASK B11000011  // D3,D2,D12,D6  .. shifted << 2

const byte mod_reverse = B00000001;
const byte mod_toggle  = B00000010;
const byte mod_repeat  = B10000010;

void pinSetup()
{
  DDRB  &= ~PB_MASK;
  DDRD  &= ~PD_MASK;
  DDRF  &= ~PF_MASK;
  /*  using 3 ports to rally 16 input pins   */
  PORTB |= PB_MASK;
  PORTD |= PD_MASK;
  PORTF |= PF_MASK;
  /*  The combination of INPUT_PULLUP and reading normally 
      closed switches means 1 = "on".  Yay.  */
}

word pinRead()
{
  word pin_read = (((PINB & PB_MASK) << 8)
                 | ((PIND & PD_MASK) << 2)
                 |  (PINF & PF_MASK));
  byte bounce_mask = (pin_read >> 8) ^ pin_read;
  /*  If nether side of a double throw switch shows a closed 
      circuit it's read is masked and we treat that button 
      as if it hasn't changed since last read.  */
  static byte last_read = 0;
  byte pin_changed = (pin_read ^ last_read) & bounce_mask;
  last_read = (pin_read & bounce_mask) 
            | (last_read & ~bounce_mask);
  return last_read | (pin_changed << 8);
  /*  Button state is passed around this sketch in this format.
      High byte is change flags.  Low byte is current status. */
}

word pinModify(word mod_state)
{
  mod_state ^= mod_reverse;
  // up is down
  mod_state = modToggle(mod_state);
  // on state is toggled at button press
  mod_state = modRepeat(mod_state);
  /* a shared timer periodically asserts changed flag while 
     button is down */
  return mod_state;
}

word modToggle(word mod_state)
{
  static byte tglon_state = 0;
  tglon_state ^= mod_state & (mod_state >> 8);
  mod_state &= (mod_state | ~mod_toggle) << 8 | 0xFF;
  mod_state = (mod_state & ~mod_toggle) 
            | (tglon_state & mod_toggle);
  return mod_state;
}

word modRepeat(word mod_state)
{
  static unsigned long timer = 0;
  unsigned long now = millis();
  if ((timer + repeat_rate) < now) {
      timer = now;
      mod_state |= (mod_state & mod_repeat) << 8;
    }
  return mod_state;
}

toekeys.jpg

ToeKeys Lite is the result of a dramatic simplification of the code I've been fiddling with now and then for the last 8 months.

In my excitement in learning to program I went overboard with features and capabilities. In the end I had 3 separate timers running, a bit of assembly, a macro command set and interpreter, a key report queue, and multiple macro sets managed by a separate command button and RGB LED. There was support for macro flexibility beyond anything I've seen from commercial macro keyboard products.

I realized recently that I don't have to will to maintain and improve on such a big (for me) lump of code so I decided to rewrite it in the simplest way that still does what I need, basically to push or hold a few keyboard buttons with my feet.

Requirements:

  • Arduino Micro - will definitely not work as-is on anything else
  • 8 momentary double throw foot-switches
  • 8 diodes
  • switches are connected to 8 IO pins in a pulsed row array
    Features
  • Still pretty fast. Polling the pulsed array of switches takes roughly 40µs.
  • Only supports assigning a single char/key to a button
  • Only supports strike/hold transformation
//ToeKeys Light  -  USB macro footswitch - v010

// Hardware definition
  /* Using Arduino Micro.  see ToeKeys-schem.png
    8x double throw foot switches connected in a pulsed matrix
    1x double throw microswitch "command" button (not yet utilised)
    1x RGB LED connected to 3 PWM pins (not yet utilised)
    
    This code is very likely to fail unless the exact configuration 
    defined in the schematic is used. The single direct wired switch
    and RGB LED shown in the schematic are not used or required
    by this sketch.
  */

// Configuration
  const char key[8][2] = {
    // key code, strike mode (release without delay)
    {'a', true},
    {'b', 0},
    {'c', 0},
    {KEY_LEFT_CTRL, 0},
    {'d', 0},
    {'e', 0},
    {KEY_LEFT_SHIFT, 0}, 
    {'f', 0}
    };

// Globals
  byte btnState = 0;      // bitmap current state of buttons
  byte btnChange = 0;     // bitmap change event of buttons

// Functions
  void pinSetup() {
    // set button pins to INPUT_HIGH
    DDRF  &= ~0x03;
    PORTF |=  0x03;
    DDRD  &= ~0x0F;
    PORTD |=  0x0F;
    // set pulse pins to OUTPUT HIGH
    DDRF  |=  0xF0;
    PORTF |=  0xF0;
    }

  void btnUpdate() {
    byte priSide = 0;
    byte secSide = 0;
    for (byte i=0;i<4;i++){
      byte pulsePin = i + 4;
      // set pulse line to LOW (active)
      bitClear(PORTF, pulsePin);
      // wait for pin to settle
      delayMicroseconds(4);
      // read both sides of 2 switches into a bitmap
      priSide |= (PINF & 3) << (i*2);
      secSide |= (PIND & 3) << (i*2);
      // restore pulse line to HIGH
      bitSet(PORTF, pulsePin);
      }
    // debounce by xor both sides of a double throw switch
    byte validRead = priSide ^ secSide;
    // record which switches have changed since last read
    btnChange = (btnState ^ priSide) & validRead;
    // record new button states
    btnState &= ~validRead;
    btnState |= priSide & validRead;
    }

  void sendKeys() {
    for (int i=0;i<8;i++) {
      // skip loop if this button didn't change
      if (bitRead(btnChange,i) == 0) continue;
      // some variables for readability
      boolean btnPressed = bitRead(btnState,i);
      char thisKey = key[i][0];
      boolean strikeMode = key[i][1];
      if (strikeMode) {
        if (btnPressed) Keyboard.write(thisKey);
        }
      else { 
        if (btnPressed) Keyboard.press(thisKey);
        else Keyboard.release(thisKey);
        }
      }
    }

void setup() {
  pinSetup();
  Keyboard.begin();
  Keyboard.releaseAll();
  }

void loop() {
  btnUpdate();
  if (btnChange) sendKeys();
  }

love to see a varent that interfaces with microSD (SPI) with ini file with macro stings, ToeKeys mSD Edition :smiley: