Introduction: MacroPad With Tile Based Buttons

About: A retired software developer, living in Waterloo, Ontario, Canada, who appreciates having the time to make whatever the heck he damn well feels like!

MacroPads have been a hot topic for years now as part of the tremendous continuing interest in custom keyboards. A search of Instructables for "MacroPad" returns 27 results. With software like QMK or Kaleidoscope to ease the burden of creating and configuring the MacroPad firmware, and cheap microcontrollers like the Arduino pro Micro or Raspberry Pi RP2040 this shouldn't come as a surprise to anyone.

The reason that I'm jumping into an already very busy category is that I think that a tile based interface solves a couple of shortcomings common to most current MacroPads. 

Labels/Legends

If the MacroPad is a simple add-on number pad, then it's easy to find keycaps with the appropriate legends to populate it.

For MacroPads that are meant to be programmed with arbitrary user selected macros, then the legends become a concern. What does a "Check Inventory" button look like? Most commercial MacroPad offerings skirt the issue by shipping with attractively color coordinated blank keycaps leaving the user to memorize the function of each key.

There are other solutions. For instance, at the high end, "ADAPTIVE MACRO-PAD USES TINY OLED SCREENS AS KEYCAPS".

Cool except for the high per key cost and complexity. 

Another simpler and cheaper solution is Relegendable Keycaps. The clear plastic caps allow you to create and insert printed text or icons.

This is a pretty good solution, but it only works if the number of macros you want is less than or equal to the number of keys on the MacroPad. Which brings us to the second problem.

Many Macros/Few Keys

Once you begin using a MacroPad you start thinking about all the useful macros that you would like to use. You soon run out of keys to map the macros to. Configuration software like QMK solves this problem with "layers". Layers allow the user to define multiple sets of macros for the keys available. Typically one key is assigned to switch between layers in one of the following manners:

  • only for the next key press
  • as long as the layer switch key is pressed
  • till the toggle key is pressed again (there could be many layers)

One problem here is that a layer typically applies to all of the keys on the MacroPad. So when you switch the layer all of the key's macros change. Think of how this really exacerbates the legend issue. The user must remember all of the key macro assignments based on these hidden layers as the keycaps (except for the ones with tiny OLED screens) will be wrong.

So what is the solution?

A Tile Based Interface

My solution was to build a MacroPad that has eight keys based on nice Fubata MD-4PCS switches. Beside each key is a slot that can hold a "special" tile which identifies a macro to be associated with that key. To be perfectly clear, all of the available macros will be defined in the firmware in much the same way as with other MacroPads. The tile holds a "simple numeric value" that maps the associated key to a specific internally defined macro sequence when the tile is inserted into the slot.

Tiles can be swapped at any time changing the key's macro immediately. I'm a big fan of this tactile based interface. 

It's pretty obvious how this solves the few keys many macros problem. The user can define a large number of macros (up tp 100) internally and can activate any eight macros at a time by simply slotting in the appropriate tiles.  

Furthermore the tile's approximately 2U size is a great surface for defining meaningful, easy to understand, labels for the macro's action. Of course if you are attached to cool but cryptic icons you can do that too.

You can see all of the details for this first version in my MacroPad With Tile Based Interface Instructable.

As I started using this first version, I found myself occasionally trying to press the tile with the label instead of the button beside it. I'm sure my muscle memory would eventually make the adjustment, but it got me thinking about how I could make the tiles the buttons too. This would reduce the overall size of the MacroPad. I also wanted to clean up the point-to-point wiring rats nest of the first version.

Supplies

  • 1 - Arduino pro Micro
  • 8 - Fubata MD-4PCS Switches
  • 1 - CD4067BE 16-Channel Analog Multiplexer
  • 16 - EQ-430L SMD Linear Hall Effect Sensors 
  • 40 - 3 mm x 1.6 mm Neodymium Magnets
  • 1 - Micro USB Cable for pro Micro
  • 1 - Main Board PCB
  • 8 - Tile Button PCBs
  • 8 - 4 pin 90 degree male headers
  • 8 - 4 pin 90 degree female headers
  • 8 - 2U key stabilizer wires (optional)
  • Miscellaneous - Wire, Filament, etc.

Note: You can find the Gerber files for the PCBs here:

https://github.com/kidmirage/MacroPad-With-A-Tile-Based-Interface/tree/main/PCBs

Step 1: Print the Parts

All of the printed parts were designed with Autodesk Fusion 360. I'm a big fan. I printed the parts with no supports and the following settings (unless other wise specified):

Print Resolution: .2 mm

Infill: 20%

Filament: AMZ3D PLA

Colors: Black, White, Silver, Orange (or whatever you want)

Notes: Print the parts in their default orientation.

To make a Tile Based MacroPad 2 (TBM2) you will need to print the following parts:

  • 1 - TBM2 MacroPad Base (Printed in 2 parts, Top and Bottom)
  • 8 - TBM2 Tile Holder Button Top
  • 8 - TMB2 Tile Holder Button Bottom
  • 8 - TBM2 Tile Holder Button PCB Lock
  • 8 - TBM2 Switch Stabilizer Holder (optional)
  • 4 - TBM2 MacroPad Foot Pad (optional - Will accept a 12 mm x12 mm adhesive rubber foot)

For each Tile you will need to print the following parts:

  • 1 - TBM2 Tile Base
  • 2 - TBM2 Tile Magnet Holders (with the appropriate magnet distances)
  • 1 - TBM2 Tile Top (Pause the print at the 1.2 mm mark and switch filaments to print the label.) (NOTE: I have included the step file for these so you can make your own tile labels.)

Step 2: Assemble the Base

Compared to the dead bug wiring of the previous versions, putting together this new MacroPad couldn't be easier. 

  1. First soldered in a 24 pin socket and install the CD4067BE 16-Channel Analog Multiplexer.
  2. Insert the pro Micro so that the legs of the header protrude from the bottom of the PCB about 1 mm. Solder the pro Micro in place.
  3. Solder in the 4 pin 90 degree male headers as depicted below.

With the eight Fubata MD-4PCS switches inserted, carefully slide the PCB over the switch pins. Be patient and careful as it's a little tricky getting all of the pins to line up.

NOTE: Before you solder the button pins to the PCB, make sure that the buttons are inserted firmly flush with the base or else the switch could end up tilting a bit which causes them to scrape against the slots in the top of the base and possibly stick. (Don't ask me how I know.)

Step 3: Assemble and Test the Tile Holder Buttons

My SMD Experience

I had never worked with SMD parts before so I though I would briefly share my experience.

My first time using SMD parts went well. After practicing on some tiny 0402 resistors, the hall effect sensor parts seemed absolutely gigantic. This board was basically a "Hello, World." for SMD fabrication. I probably could have hand soldered the parts (maybe) or applied the solder paste by hand, but I was looking for the full SMD experience. So I designed and printed a jig to hold the PCB.

I had a stencil created along with the PCBs and used this rig to setup ten Tile Button Sensor panels. Then I used the reflow oven at Kwartzlab to finish the parts. There are lots of instructions out there for doing reflow in a normal oven or electric frying pan. A big thanks to Erin and Konstantin at Kwartzlab for their help getting me going on this.

Tile Holder Button Assembly

To make a Tile Holder Button:

  1. Solder four leads to the PCB header from the bottom as pictured above. (NOTE: The leads in the photo above are way too long. The leads should be at least 46 mm long.) The square via (pin 1) on the header is ground and the opposite side is power. The two middle pins are for the hall effect sensor outs. I used a soft silicone stranded 28 AWG wire for this as the solid core wire that I was using for the previous version was a little "stiff" and had a tendency to make the buttons tilt a bit.
  2. Slide the leads through the slot in the bottom of the Tile Holder Button Top. 
  3. Install the Tile Holder Button PCB Lock with the cutouts facing down. The tabs on the short side of the PCB Lock should securely snap into the groves on the inside of the Tile Holder Button Top. 
  4. Attach the Tile Holder Button Bottom with four M3 x 6 mm bolts making sure that the wires are routed through the wiring channel provided.

Testing

I have a pro Micro setup to test Tile Buttons. I wired up the new Tile Holder Button to my pro Micro.

Pro Micro    EQ-430L
~~~~~~~~~ ~~~~~~~
VCC +5V
GND GND
A0 S0
A1 S1

I wrote a simple sketch to read the sensor.

/*
MacroPad Sensor Calibration.
*/

int sensorPin1 = A0; // Sensor input pins.
int sensorPin2 = A1
int sensorValue1 = 0; // Sensor values.
int sensorValue2 = 0;


void setup() {
Serial.begin(115200);
Serial.println("MacroPad Sensor Test!");
}

void loop() {
// Read the values from the sensor.
sensorValue1 = analogRead(sensorPin1);
sensorValue2 = analogRead(sensorPin2);
Serial.println(sensorValue1, sensorValue2);
delay(1000);
}

And fired up the Serial Monitor in the Arduino IDE.

I ran my simple button testing Arduino sketch and dropped in one of my calibration tiles and: 

"YES!" it works."

Adding A Connector

I used 4-pin 90 degree female headers for connectors. Before soldering the connector to the leads I slid heat shrink tubes onto the wires. The four wire connections should run parallel between the header holes on the PCB to the header pins on the connectors. When done soldering, slide the shrink tubes over the solder joints and hit them with some heat.

There is one thing to watch out for when attaching the connectors.

For button holders going on the left side of the Base, the connector is installed facing down as in the left picture above. Right hand side buttons have the connector facing up, and the connector has to be rotated 180 degrees before attaching them to the Base PCB headers. The wires for for left side switches should be 42 mm long and for the right side 46 mm (to allow for the twist in the wires).

So make 4 left side button assemblies and 4 right side button assemblies.

Step 4: Final Assembly

Stabilizing (Optional)

I did a quick test of the Tile Holder Button that I put together and mounted it on one of the Fubata MD-4PCS switches. Needles to say given that the Tile Holder Button is much heavier and larger that a standard 2U keycap it felt a little "loose" or "floppy". In most cases even standard 2U keycaps use stabilizers like these shown below to give them a solid feel.

In fact I had a set of stabilizers similar to these, but unfortunately I could not make them work with my big ass buttons, but it occurred to me that I might be able to just use the stabilizer wires. So after a few false starts I modeled the following parts:

On the left is a Switch Stabilizer Holder to go on top of the Fubata switches to hold the stabilizer wire. To the right you can see the modifications I made to the Tile Button Holder bottom piece adding the drop down panels with holes to accept the stabilizer wire. To set this up, first glue the stabilizer wire holder cap to the switch (left below). When the glue dries snap in the stabilizer wire.

Then it is actually pretty easy to slide the two ends of the stabilizer wire into the holes I made in the Tile Holder bottom while pressing the button onto the switch's MX (+) connector. 

Now I will be the first to admit that my DIY stabilizer is not nearly as good as those commercially available, however the button feel is better with than without, so I consider the effort to be worth while. Your call.

Attach the Tile Holder Buttons

Not much to say here.

  1. For each Tile Holder Button, slide the connector down through the slot in the Tile Base. The connector has the hook in through the slot from the outside to the inside of the base. If you try and do it wrong the base side will prevent the connector from dropping in.
  2. Attach the female Tile Holder connector the the appropriate male connector on the Base PCB. I used offset needle nose pliers to help with this task.
  3. Slide the Tile Button Holder bottom onto the Fubata switch (with or without sliding in the stabilizer wires).

Finally attach the MacroPad Base top to the bottom with 4 M4 x 6 mm bolts. Optionally you can add some rubber feet as I have here using 4 MacroPad Foot Pads.

Done.

Step 5: Make Some Tiles

There are a couple things that you should be aware of when making tiles.

Polarity

The Hall effect sensors I am using will return a different value depending on the polarity of a nearby magnet. The magnet holders that I am using (shown above) are set at 5 different distances from the sensor, which results in potentially 10 distinct values 5 with one polarity and 5 with the opposite polarity.

So I have assigned one set of values returned to the numbers 0, 1, 2, 3, and 4, same as the labels on the top of the magnet holders. With the magnets inserted into the holders with the reversed polarity the are assigned the values 5, 6, 7, 8, and 9. 

Orientation

If you insert a tile into a Button Holder, the two numbers represented by the magnets inside the tile will be read when the button is pressed. Say the vales were 1 and 2 (mapped to 12). If you flip that tile upside down, the numbers will be read as 2 and 1 or 21. This will obviously not map to the same macro.

Recalibration

The magnets for my previous MacroPad used two 6 mm x 1.8 mm cylindrical neodymium magnets per sensor. For the new sensor I had to go back to the drawing board to find magnets more suitable because the new Hall Effect sensors were much more sensitive to magnetic fields. I have quite a few cylindrical magnets of various sizes, so I started by manually holding them in turn above the new sensor, moving them closer and further away, while monitoring the values returned by the Test Tile Button! sketch. It wasn't until I got to my smallest magnet, 3 mm x 1.6 mm, that I found one that looked like it would work. 

I did some further testing to verify these smaller magnets. I created some test tiles to hold these magnets at various distances from the sensors.

The numbers on the tiles indicate the distance from the sensor (in mm). Using these test tiles and the tile button holder just assembled I get the following values.

Tile # / Distance       Polarity Value      Reverse Polarity Value
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
1 mm  1018 - 1018 0 - 0
2 mm 895 - 877 100 - 105
3 mm 750 - 729 254 - 280
4 mm 642 - 636 341 - 364
5 mm 562 - 588 389 - 405
6 mm 549 - 569 419 - 428

These look pretty reasonable with a good separation between the readings based on distance. Of course this means that tiles created for this version of the MacroPad will not be compatible with the previous versions. So time to make some new tiles.

Making Some New Calibration Tiles

In order to get accurate tile readings, it's important to calibrate the linear hall effect sensors. I found from experience that for the best results each sensor should have its own calibration values for each of the possible tile variations. The first step is to create some calibration tiles.

I printed some Tile Bases, a couple of sets of the new Magnet Holders, and some Tile Tops. I'm using up my orange filament left over from Halloween. 

Here I am about half done. Each tile contains two magnet holders set to the number that appears on the label. (NOTE: since you only have to use these once, you don't have to print or attach the top labels like I did. When calibration is done you can reuse these tiles for your first macros.)

The magnets inserted into the 0-4 magnet holders have the opposite polarity of the magnets inserted into the 5-9 holders. I used a small piece of thin two sided tape to hold the magnets in and join the top and bottom parts of the magnet holders, then inserted them into the Tile Bases. Once they test out OK I'll add the labeled tops to complete the calibration set.

Step 6: Calibrate the Tile Holder Buttons

The calibration program runs from the Arduino IDE and outputs to the Serial monitor. The Board: should be set to "Arduino Leonardo". (NOTE: I'm assuming a working knowledge of the Arduino IDE. If not check out this tutorial.) The calibration program and the macropad program require a couple of extra libraries in order to run.

HID-Project Library - Open Arduino IDE Library Manager by selecting "Tools" menu -> "Manager Libraries...". Search "HID-Project" and press "install" button.

debounce Library - Open Arduino IDE Library Manager by selecting "Tools" menu -> "Manager Libraries...". Search "debounce", select "debounce" and press "install" button.

With the MacroPad plugged in and the Arduino IDE set to it's COM port, and NO tiles in the MacroPad, flash and run the Calibrate_MacroPad_3.ino sketch. When the program first starts up you see the following:

The midValues array printed represents the "at rest" or no magnet present readings from the hall effect sensors. Notice that the values do not vary much between sensors. This table should be copied into the MacroPad_3.ino sketch replacing the existing table with freshly calibrated values.

Following the prompt, when you put the "zero" tile into each of the eight slots and press the corresponding buttons, you should see something like:

For each button pressed the two sensors for that button are read and the sensor numbers and values are emitted. When all eight slots have been calibrated the next tile number is asked for.

When all ten tiles have been calibrated the calValues table is emitted.

Simply cut the table text from the Serial window and paste it into the MacroPad_3.ino sketch replacing the existing table. 

The MacroPad should now be setup to accurately read tiles and correctly map them to the defined macros. You can now flash the pro Micro with the updated MicroPad_3.ino sketch.

You will find the Calibrate_MacroPad_3.ino and MacroPad_3.ino sketches attached.

Step 7: Defining Macros

Macros are defined within the MacroPad_3.ino sketch. Here is the relevant section. I have set things up so you should not have to make any "programming" changes to the file.

/*****
* Standard Key Codes.
******
KEY_RESERVED KEY_ENTER KEY_PAGE_DOWN
KEY_ERROR_ROLLOVER KEY_RETURN KEY_RIGHT_ARROW
KEY_POST_FAIL KEY_ESC KEY_LEFT_ARROW
KEY_ERROR_UNDEFINED KEY_BACKSPACE KEY_DOWN_ARROW
KEY_A KEY_TAB KEY_UP_ARROW
KEY_B KEY_SPACE KEY_RIGHT
KEY_C KEY_MINUS KEY_LEFT
KEY_D KEY_EQUAL KEY_DOWN
KEY_E KEY_LEFT_BRACE KEY_UP
KEY_F KEY_RIGHT_BRACE KEY_NUM_LOCK
KEY_G KEY_BACKSLASH KEYPAD_DIVIDE
KEY_H KEY_NON_US_NUM KEYPAD_MULTIPLY
KEY_I KEY_SEMICOLON KEYPAD_SUBTRACT
KEY_J KEY_QUOTE KEYPAD_ADD
KEY_K KEY_TILDE KEYPAD_ENTER
KEY_L KEY_COMMA KEYPAD_1
KEY_M KEY_PERIOD KEYPAD_2
KEY_N KEY_SLASH KEYPAD_3
KEY_O KEY_CAPS_LOCK KEYPAD_4
KEY_P KEY_F1 KEYPAD_5
KEY_Q KEY_F2 KEYPAD_6
KEY_R KEY_F3 KEYPAD_7
KEY_S KEY_F4 KEYPAD_8
KEY_T KEY_F5 KEYPAD_9
KEY_U KEY_F6 KEYPAD_0
KEY_V KEY_F7 KEYPAD_DOT
KEY_W KEY_F8 KEY_NON_US
KEY_X KEY_F9 KEY_APPLICATION
KEY_Y KEY_F10 KEY_MENU
KEY_Z KEY_F11
KEY_1 KEY_F12
KEY_2 KEY_PRINT
KEY_3 KEY_PRINTSCREEN
KEY_4 KEY_SCROLL_LOCK
KEY_5 KEY_PAUSE
KEY_6 KEY_INSERT
KEY_7 KEY_HOME
KEY_8 KEY_PAGE_UP
KEY_9 KEY_DELETE
KEY_0 KEY_END


You can add any of these codes to the keyboard lists below for use in macro definitions.
In the keyboardKeyNames table add the code as a string delimited by "".
In the keyboardKeyCodes table ad the code as is without quotes.
Don't forget the commas at the end of each entry.
It is IMPORTANT to add the name and code lines in the same position in the list.
*****/

String keyboardKeyNames[] = {
"KEY_LEFT_CTRL",
"KEY_UP_ARROW",
"KEY_DOWN_ARROW",
"KEY_LEFT_ARROW",
"KEY_RIGHT_ARROW"
};

KeyboardKeycode keyboardKeyCodes[] {
KEY_LEFT_CTRL,
KEY_UP_ARROW,
KEY_DOWN_ARROW,
KEY_LEFT_ARROW,
KEY_RIGHT_ARROW
};

/******
* Media Key Codes.
*******
MEDIA_RECORD  MEDIA_VOLUME_MUTE
MEDIA_FAST_FORWARD MEDIA_VOL_MUTE
MEDIA_REWIND  MEDIA_VOLUME_UP
MEDIA_NEXT    MEDIA_VOL_UP
MEDIA_PREVIOUS MEDIA_VOLUME_DOWN
MEDIA_PREV    MEDIA_VOL_DOWN
MEDIA_STOP
MEDIA_PLAY_PAUSE
MEDIA_PAUSE

You can add any of these codes to the media lists below for use in macro definitions.
In the consumerKeyNames table add the code as a string delimited by "".
In the consumerKeyCodes table ad the code as is without quotes.
Don't forget the commas at the end of each entry.
It is IMPORTANT to add the name and code lines in the same position in the list.
*****/

String consumerKeyNames[] = {
"MEDIA_PLAY_PAUSE",
"MEDIA_VOL_MUTE",
"MEDIA_VOLUME_UP",
"MEDIA_VOLUME_DOWN"
};

ConsumerKeycode consumerKeyCodes[]{
MEDIA_PLAY_PAUSE,
MEDIA_VOL_MUTE,
MEDIA_VOLUME_UP,
MEDIA_VOLUME_DOWN
};

/*****
* Define the macros here.
*****
String macros[] = {
// Macros 0-9.
"PRESS,KEY_LEFT_CTRL,a,RELEASE_ALL", // select
"PRESS,KEY_LEFT_CTRL,c,RELEASE_ALL", // copy
"PRESS,KEY_LEFT_CTRL,x,RELEASE_ALL", // cut
"PRESS,KEY_LEFT_CTRL,v,RELEASE_ALL", // paste
"KEY_UP_ARROW", // up
"KEY_LEFT_ARROW", // left
"KEY_RIGHT_ARROW", // right
"KEY_DOWN_ARROW", // down
"MEDIA_VOLUME_DOWN", // louder
"MEDIA_VOLUME_UP", // softer
// Macros 10-19.
"MEDIA_VOL_MUTE", // mute
"MEDIA_PLAY_PAUSE", // pause/play
"..."
};

The important part is this last macros table. When a key on the pad is pressed, it's corresponding tile is read to get the two digit tile number. This number is used as an index into the macros table to get the appropriate macro string which is parsed to determine the keys to send to the PC. The macro string is split into "tokens" at the commas and the tokens are processed from left to right with the following rules:

  1. If the token is "PRESS", a flag is set to to indicate that all subsequent characters (c) will be sent with the Keyboard.press(c) function. If the press token is not set (default at start of parsing), characters will be sent with Keyboard.write(c).
  2. If the token starts with "KEY_", then the token is used to lookup a keycode in the keyboardKeyCodes table. That keycode is sent to the PC via the Keyboard press or write functions depending on the press flag. 
  3. If the token starts with "MEDIA_", then the token is used to lookup a keycode in the consumerKeyCodes table. That keycode is sent to the PC via the Consumer press or write functions depending on the press flag. 
  4. If the token is "RELEASE", then a release flag is set. The next character processed (c) will be sent to the PC with a Keyboard.release(c) function and the release flag will be cleared.
  5. If the token is "RELEASE_ALL", then the Keyboard.releaseAll() function will be executed and the release flag cleared.
  6. If the token is a hex digit of the form "0xnn" exactly where nn is a valid hex number, then the value based on that number will be sent to the PC via the Keyboard press or write depending on the press flag.
  7. If the token is not recognized as one of the "key words" above, then it’s assumed to be an ASCII character or a string. If the token is a single character it will  be sent to the PC via the Keyboard press or write depending on the press flag. If it's a string (s) it will be sent to the PC with a Keyboard.print(s) call. (NOTE there is no corresponding Keyboard.println(s) call because the user can simply add a \n to the end of the string if that is what they want.) 

By way of explanation of the above the USB library used to send keys has the following functions.

Keyboard.write(c)     // Sends the character (c) as a key press followed by a key release.
Keyboard.print("Hi") // Send all the charcters in the string passed as key press/releases.
Keyboard.press(c) // Sends the character (c) as a key press with no key release.
Keyboard.release(c) // Sends a key release for character (c).
Keyboard.releaseAll() // Send a release for all characters pressed.
Consumer.write(c) // Sends the character (c) as a key press followed by a key release.
Consumer.press(c) // Sends the character (c) as a key press with no key release.

So for example my copy macro above "PRESS,KEY_LEFT_CTRL,c,RELEASE_ALL" means send the LEFT_CTRL key then without releasing send the 'c' key then release both keys. It's the same as when you manually press and hold the CTRL key then press the 'c' key then release both.

So someone should be able define the macros within this code without knowing any programming.

Step 8: Final Thoughts

Well the last few months have been interesting. Over that period of time I have made three similar tile based MacroPads. I did not create an Instructable for the blue one above because it was a stepping stone between the MacroPad with actual keys and the latest with Tile Button Holders. The big difference with blue is that it was hand wired like the first MacroPad and I wanted to clean up the wiring by using PCBs.

As each project wrapped up I kept thinking of improvements that I had to try. Fortunately for me, MacroPad 3 finally felt "done" but I'm not sure I'm completely done with tile based interfaces. We'll see I guess.

Anything Goes Contest

Participated in the
Anything Goes Contest