Programmable Macropad V2
by tinyboatproductions in Circuits > Arduino
61644 Views, 677 Favorites, 0 Comments
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 from this set, otherwise you could print some)
(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
Optional:
Tools
- Soldering Iron
- Pliers
- Snips
- Wire strippers
- Scissors
- 3D printer (optional)
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.
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.
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.
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.
}
}
Code Walk Through
Ok, here is a more in depth walkthrough of the code for this project. As a warning I have no qualifications to teach anyone how to code. I just know how to google and use the Arduino forums and Stack Overflow.
I will do my best to walk through as simply as I can but leave a comment if you feel like I missed something.
There are a few main points when coding for general good coding practice:
- Don't write code someone else has written better
- Don't declare pin numbers in more than one place
- Use good variable names
- Functions should do only 1 thing
- Use debugging everywhere to find errors
- The error is usually a spelling mistake
To start with we are hitting the first bullet: don't write code someone has written better. Basically, use the libraries that smart computer people have written for us plebs to use Arduinos. In this sketch we are using 4 libraries, Keypad, Encoder, Bounce2, and HID-Project. Keypad will take care of setting up and scanning our key button matrix as well as deboncing the button presses so we only need to worry about dealing with a key press not figuring out what key was pressed/if a key was pressed. Encoder will take care of our encoder and tracking the postion of the encoder. As someone who as written an Arduino encoder function before, don't bother, it is hard and confusing. Bounce2 is a lot like Keypad but it will look at individual buttons, I use this on the encoder button as I want a short and long press function for it. And finally HID-Project will allow us to send keyboard keys as well as windows media keys, like play/pause and control the volume.
#include <Keypad.h>
#include <Encoder.h>
#include <Bounce2.h>
#include "HID-Project.h"
With those ready we can start to use them. Up first, is the keypad/key matrix. The first few lines define the pins that each row/pin is connected to. Next we define the size of our button matrix. I have 4 rows and 3 columns. Next we define the array of button symbols. I used the same number and symbol that are on the key caps for mine. After that we create an array of the pin numbers for the rows and columns. Finally we create a Keypad object called kpd to keep track of all the buttons in our keypad. A big note here, as I only have 11 buttons not 12 setting it up this way creates a phantom button, the one labeled X. This button will never be pressed as there is no switch there. In theory you could wire the encoder button here but I chose not to.
//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);//Keypad Object
Now to setup the LEDs. Similar to the keypad we just did we start with defining our pin numbers. Then note the number of states we have. In my case I went with 4 but you could do up to 16 with only 4 LEDs. As I mentioned before: if you can remember 144 different key commands be my guest, I'm not that smart. Next we collcet the pin numbers in an array called States, this will allow us to cycle through the state LEDs without needing to keep track of the pin names/numbers only the current state. Also note here that in C++ arrays start at 0, so in the States array there are 4 elements, one for each LED, the first one S1 is the 0th element and S2 is the first. All the way up to S4 which is the 3rd element. Finally in this section we tell the code what state to start in, I chose 0, but you could chose what ever layout you want.
//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;
Encoder time. Here just like before we tell the code what pins to use. Then we create an Encoder object with our encoder tracking pins, in my case DT and CLK. Again this will work best if these are interrupt capable pins.
After we setup the encoder we setup the encoder button as a Bounce object. This will take care of the button press for us making it easy to see when its press and for how long. Then as I want the button to have a short and long press I set the time limit for a long press. I chose 500ms as it felt like a good amount. Finally we give the encoder a starting position, basically where to start counting its positions from. This number has no impact on how the encoder functions.
//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;
Almost to the fun stuff, but we still gotta do some setup(). This is really just starting a bunch of processes to run in the background that we can use latter.
First here we start eh Serial output, I use this for debugging. It's really easy to add a .println() to see if something is working.
Next we use a for() loop to go through our States array we made. We just need to set them up as an output and then make sure they are turned off.
Next setup the encoder button as an input using the Micros internal pull-up resistor.
Now we start a Keyboard object/process (not exactly sure) so we can sent key strokes to the computer.
Next print out that we are ready and play the StartAnimation() on the LEDs.
Finally we set the current state that we declared earlier, 0, to high to turn the LED on.
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();
Serial.print("Ready");
StartAnimation();
digitalWrite(States[currentState], HIGH);
}
Yay, the first fun part of the code. This is the StartAnimation. It is not needed at all but I think its fun so here it is. What we do is turn an LED on then wait, turn on the next, until they are all on. Then turn them off in the same order. You could do this with 2 for loops, one turning them on and the next turning them off.
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;
}
Up next, we are going to start setting up functions to use in our main loop(). These might not make a ton of sense out of order, check the main loop to see where the are used.
So what do we need to do when state button is pressed. To write this function I looked at what I need to do and did each step. We need to turn off the current LED, so we do that first. Now we need to change the currentState and we do that by adding one. C++ has a handy feature where we can use currentState++ to add one to the current state. Now there is an issue here, we could end up with a currentState that is larger than our numStates, well the same but that's too big because the array only goes to 3 remember. What we can do here is check to see if our numStates is equal to our currentState, if it is, reset the current state to 0. Now we can turn on the new state LED and head back to the main code.
void ChangeState(){
digitalWrite(States[currentState], LOW);
currentState++;
if (currentState == numStates){
currentState = 0;
}
digitalWrite(States[currentState], HIGH);
delay(100);
return;
}
Next we have a function for each layout out. So one for each state. I do a weird thing here where I number the states staring at 1 while the currentState starts at 0. I find it easier to keep track of but do what makes sense for you.
This function takes in one variable of a char type and we call it button. This variable only exists in this function. With that button we can figure out what to send to the computer. I am using a switch case statement here, it removes the needs to have a bunch of if else statements. For this layout I just send the numbers like a num pad. In the main code on the last step you can see a few other functions like copy/past. As there is one of these for each layout, that are the same except for the key strokes, I am only going to put the one here.
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;
};
}
Loop time. The loop for this has 3 jobs, check if we pressed a button, check if we pressed the encoder button, and check if we rotated the encoder. For each of these it can then know what to do with the functions we just covered.
In the loop the first thing we do is get any kep pressed from the keypad and then use the if statement to find if it was the state key, and use the changeState function we wrote above, if it was the save key we send the save command (this has its own key as it was appering on bascially all my layouts so I gave it its own button). If it isnt either of those buttons it must be a function button so we can use another switch case based on the current state and go to the right state Layout function.
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;
}
}
}
With the buttons out of the way now we need to see if the encoder button was pressed. We can use the Bounce fucntions to check, first .update() will tell us that there is an update on this button. With that info we check for a falling edge, is the signal on the pin going from high to low. If it is, mark the time, now wait until there is another update for the button, and make sure its the button going from low to high, it as been released, again mark the time. Now we can compare the time difference and decicde if we need to send the next track function or the play/pause function. Note that here we have to use Consumer rather than Keyboard. I don't really know why, but it works this way.
//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();
}
}
And the last thing we need to do is to see if the encoder has been rotated. Here we again use the Encoder functions to read the location of the knob and set it into the newPosition. If that is not the same as the oldPosition that we set waaaaay up there then we can find the difference between the two. If the difference is posotive we need to increase the volume otherwise decrease the volume. I added a delay here as without one the volume would rocket in either direction.
//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.
}
}
You did it. You made it to the end. Like I said at the top I have not qualifiation to teach about this. I recommend you use this code as a starting point and work on it. Also feel free to break it, learn by doing. The full code will still be here to copy if you modify and it breaks.
Final Assembly
After that code walk through this should be a breeze.
Run your USB cord through the opening in the bottom of the case and plug it into the arduino. Upload your code if you haven't and test it out.
When you are ready use the hardware to screw the top on to the base.
If you are using the feet, flip the case over and add a lil foot on each corner.
You're done! Take a step back and admire your work. Its something we don't do often enough, you built a thing! A real thing, a hole project that you can set on your desk and show off. Be proud of what you've made.