Programmable Macropad V2

In this Instructable I will walk through how I build my new and improved Macropad. I made one a while back and it has always had some things I would like to improve on it. The main one being the location of the state LEDs. When redesigning the case I figured why not take a look at the whole thing and see if I can do a better job. So I did.

This macropad is capable of 10 buttons per layout and up to 16 layouts in total. If you can do it I’ll be impressed. It also has a click rotary encoder, that has a fixed function, volume and play/pause.

In this version I am sticking with an Arduino Pro Micro, and adding another button on. While I was reading I also decided to use diodes just like a really keyboard. This also makes coding a bit easier as a lot of libraries already exist for this.

I won’t spend a lot of time on why I made the changes I did, but I’ll touch on it here and there throughout. Honestly they both work great. I just wanted a new challenge.

Supplies:

Materials

I am providing links to what I used but don’t feel like you need these exact parts. Get, or use what makes sense for you.

(11) Key Switches & Caps (I used some I had, otherwise you could print some)
(1) Arduino Pro Micro

(1) Rotary Encoder

(11) IN4148 Diodes

Case & Hardware

(4) LEDs

(1) 330 ohm Resistor (or what ever size you need for the color you are using)

Wire

USB Cable

Optional:

Rubber Feet

Tools

  • Soldering Iron
  • Pliers
  • Snips
  • Wire strippers
  • Scissors
  • 3D printer (optional)

Step 1: Case

As with any project I like to get the case started first. Especially when printing one, as it takes a while and I only need to slice and load the file and the printer does the rest. Here is a link to the case that I designed for this project: https://www.thingiverse.com/thing:4792978

This project does necessitate the at least the top of the case being done before moving on as all of the parts need to be clipped into the top and then soldered together.

You don’t need to print a case for this. You can use what ever you have access too.

For this case design I would recommend checking the dimensions on your keys, LEDs, and encoder before printing the whole thing.

Step 2: Assemble the Top

The begins by installing all the switches into the plate. The best fit on the switches is a bit tight, that way you can remove any key caps and the switch won’t pull out.

After adding the switches bend the ground leg of the LED down at a right angle and place them into their spots at the bottom of the plate with the bend leg on the outside.

If your encoder came with header pins, like mine did, you will need to remove them before you can put the encoder in place as the pins run into the switches. To do that use your snips to clip the plastic that holds them all together and then remove them one at a time using a soldering iron to heat the pin and solder and a pair of pliers to pull the pin out. Once the pins have been removed you can put the encoder in place and attach it. It may also be easier to wait until you have soldered the wires onto the encoder before putting it in place.

Step 3: Soldering

Soldering this is the hardest part.

Each of the switches needs a diode and a wire. We are basically making a grid wires with a switch and diode at each intersection. The diodes help in case more than one switch is pressed at a time. I’ve attached a circuit layout that I used (or pretty close). The pins aren’t hugely important, as they can easily be swapped in the code later. Make sure that you avoid pin 0 and 1 (RX and TX) as I think they are used to communicate with the computer; tying into them might be weird.

There are 3 pins that are important. They are the pins from the encoder. The encoder will work best if the moment tracking pins connected to interrupt capable pins on the Micro. On the Pro Micro attached to pin 2 and 3. They are important as they track the movement of the encoder. If you have the same encoder I do they are the DT and CLK pins. The encoder I have also needs +5v to work, so the power needs to run from the RAW pin. The RAW pin exposes the +5v coming from the USB port. All other pins will run at 3.3v.

Step 4: Code

Now the fun part. For this version I am breaking the code down into a lot more functions. Functions are useful as the make it a lot easier to keep each section of your code small. By making each section smaller, its easier to follow.

The main thing to remember with functions is that each function should only do 1 thing. The other thing to remember is why write code many times when you can wrap it in a function and use it a bunch of times.

I’ve attached the code below and if you are interested I have an in depth walk through in the next step.

For the actual key functions I’ve left a few in Layout2() for you to look at. With this code always remember when sending key strokes make sure that you release any keys you press. Some of the functions like .send() do release the keys they press but .press() does not. Use .releaseAll() to release pressed keys.

You can change the pin numbers at the top to what you have used.

#include <Keypad.h>
#include <Encoder.h>
#include <Bounce2.h>
#include "HID-Project.h"
//Keypad buttons
int R1 = 6;
int R2 = 5;
int R3 = 21;
int R4 = 20;
int C1 = 7;
int C2 = 8;
int C3 = 9;
const byte ROWS = 4;
const byte COLS = 3;
char keys[COLS][ROWS] = {
  {'X','7','4','1'},
  {'*','8','5','2'},
  {'-','9','6','3'}
};
byte rowPins[ROWS] = {R1, R2, R3, R4};
byte colPins[COLS] = {C1, C2, C3};
Keypad kpd = Keypad( makeKeymap(keys), colPins, rowPins, COLS, ROWS);

//State LED pins
int S1 = 15;
int S2 = 14;
int S3 = 16;
int S4 = 10;
const int numStates = 4;
const int States[numStates] = {S1, S2, S3, S4};
int currentState = 0;

int lastDebounceTime = 0;
const int debounceTime = 50;

//Encoder
int SW = 19;
int DT = 2;
int CLK = 3;
Encoder volumeKnob(DT,CLK);
Bounce encoderButton = Bounce(SW,10);
int timeLimit = 500;
long oldPosition = -999;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  for (int i = 0; i < numStates; i++){
    pinMode(States[i], OUTPUT);
    digitalWrite(States[i], LOW);
  }

  pinMode(CLK, INPUT_PULLUP);
  
  Keyboard.begin();
  //Consumer.begin();
  
  Serial.print("Ready");
  
  StartAnimation();
  digitalWrite(States[currentState], HIGH);
}

void StartAnimation(){
  int waitTime = 250;
  digitalWrite(S1, HIGH);
  delay(waitTime);
  digitalWrite(S2, HIGH);
  delay(waitTime);
  digitalWrite(S3, HIGH);
  delay(waitTime);
  digitalWrite(S4, HIGH);
  delay(waitTime);
  digitalWrite(S1, LOW);
  delay(waitTime);
  digitalWrite(S2, LOW);
  delay(waitTime);
  digitalWrite(S3, LOW);
  delay(waitTime);
  digitalWrite(S4, LOW);
  delay(waitTime);
  return;
}

void ChangeState(){
  digitalWrite(States[currentState], LOW);
  currentState++;
  if (currentState == numStates){
    currentState = 0;
  }
  digitalWrite(States[currentState], HIGH);
  //Serial.print("State Changed. Current State: "); Serial.println(currentState);
  delay(100);
  return;
}

void Layout1(char button){
  switch(button){
    case '1':
      Keyboard.print('1');
      break;
    case '2':
      Keyboard.print('2');
      break;
    case '3':
      Keyboard.print('3');
      break;
    case '4':
      Keyboard.print('4');
      break;
    case '5':
      Keyboard.print('5');
      break;
    case '6':
      Keyboard.print('6');
      break;
    case '7':
      Keyboard.print('7');
      break;
    case '8':
      Keyboard.print('8');
      break;
    case '9':
      Keyboard.print('9');
      break;
  };
}//
void Layout2(char button){
  switch(button){
    case '1'://
      break;
    case '2'://
      break;
    case '3'://
      break;
    case '4'://
      break;
    case '5'://
      break;
    case '6'://Return
      Keyboard.press(KEY_RETURN);
      Keyboard.releaseAll();
      break;
    case '7'://Escape
      Keyboard.press(KEY_ESC);
      Keyboard.releaseAll();
      break;
    case '8'://
      break;
    case '9'://
      break;
  };
}

void Layout3(char button){
  switch(button){
    case '1':
      Keyboard.print('7');
      break;
    case '2':
      Keyboard.print('8');
      break;
    case '3':
      Keyboard.print('9');
      break;
    case '4':
      Keyboard.print('4');
      break;
    case '5':
      Keyboard.print('5');
      break;
    case '6':
      Keyboard.print('6');
      break;
    case '7':
      Keyboard.print('1');
      break;
    case '8':
      Keyboard.print('2');
      break;
    case '9':
      Keyboard.print('3');
      break;
  };
}

void Layout4(char button){
  switch(button){
    case '1':
      Keyboard.print('1');
      break;
    case '2':
      Keyboard.print('2');
      break;
    case '3':
      Keyboard.print('3');
      break;
    case '4':
      Keyboard.print('4');
      break;
    case '5':
      Keyboard.print('5');
      break;
    case '6':
      Keyboard.print('6');
      break;
    case '7':
      Keyboard.print('7');
      break;
    case '8':
      Keyboard.print('8');
      break;
    case '9':
      Keyboard.print('9');
      break;
  };
}

void loop() {
  //check the key matrix first
  char key = kpd.getKey();
  if(key) {
    switch(key){
      case '*':
        ChangeState();
        break;
      case '-':
        Keyboard.press(KEY_RIGHT_CTRL);
        Keyboard.press('s');
        delay(10);
        Keyboard.releaseAll();
        break;
      default:
        switch(currentState){
          case 0:
            Layout1(key);
            break;
          case 1:
            Layout2(key);
            break;
          case 2:
            Layout3(key);
            break;
          case 3: 
            Layout4(key);
            break;
        }
    }
  }

  //check the encoder button
  if(encoderButton.update()) {
    if(encoderButton.fallingEdge()) {
      int fall = millis();
      while(!encoderButton.update()){}
      if(encoderButton.risingEdge()){
        int rise = millis();
        //Serial.println(rise - fall);
        if(rise - fall > timeLimit){
          Consumer.write(MEDIA_NEXT);
          Serial.print("Next");
        } else {
          Consumer.write(MEDIA_PLAY_PAUSE);
          Serial.print("Play/Pause");
        }
      }
      Keyboard.releaseAll();
    }
  }

  //check encoder rotation
  long newPosition = volumeKnob.read();
  if(newPosition != oldPosition){
    Serial.print(newPosition);

    if((newPosition - oldPosition) > 0) {
      //volumeup
      Serial.println("volume up");
      Consumer.write(MEDIA_VOLUME_UP);
    } else {
      //volumedown
      Serial.println("volume down");
      Consumer.write(MEDIA_VOLUME_DOWN);
    }
    oldPosition = newPosition;
    Keyboard.releaseAll();
    delay(200); //a delay of 200 seems to be the sweet spot for this encoder.
  }  
}

Source: Programmable Macropad V2

Scroll to Top
Scroll to Top