Introduction: MacroPad With Tile Based Interface

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 MacroPad 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.

Supplies

  • 1 - Arduino pro Micro
  • 8 - MX Keycaps
  • 8 - Fubata MD-4PCS Switches
  • 1 - CD4067BE 16-Channel Analog Multiplexer
  • 16 - SS49E Linear Hall Effect Sensors 
  • 120 - 6 mm x 1.6 mm Neodymium Magnets
  • 1 - Micro USB Cable for pro Micro
  • Miscellaneous - Wire, Filament, etc.

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: Dark Violet, Mauve, Lavender

Notes: Print the parts in their default orientation.

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

  • 1 - TBM MacroPad Base
  • 1 - TBM MacroPad Body
  • 1 - TBM MacroPad Chip Holder
  • 4 - TBM MacroPad Foot Pad
  • 8 - TBM MacroPad Slot Bottom

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

  • 1 - TBM Tile Base
  • 2 - TBM Tile Magnet Holders
  • 1 - TBM Tile Top (Pause the print at the 1.2 mm mark and switch filaments to print the label.)

Step 2: Building the Base

The core of this MacroPad will be built around this 3D printed base (TBM MacroPad Base).

The center panel has holes to mount the eight Fubata MD-4PCS switches. To the left and right are eight tile slots. You can see at the bottom of each slot two indentations to hold SS49E linear hall effect sensors with holes to pass through the sensor's leads. 

Adding The Sensors

  1. Start by bending the leads on one of the SS49E linear sensors at about 1 mm from the base towards the flat side of the sensor to about 80 degrees.
  2. Insert the sensor into one of the slot indentations by sliding the leads through the slot's hole and pressing it in place. You should be able to feel it quietly clicking into place.
  3. Add the second sensor to the slot in the same way except that it's oriented 180 degrees from the first. Get one of the slot bottom cover pieces (TBM MacroPad Slot Bottom). Notice that there are two raised bumps.
  4. Glue the bottom cover into place making sure that the bumps are facing down. You should apply the glue around the SS49Es without getting any glue on the sensors. (Note: I got burned once when the glue I was using was actually conductive.)

Repeat for the other slots and sensors.

Adding The Buttons

Here is a look at my Fubata MD-4PCS switches which I'm using because I had a bunch from some of my retro computer projects. They are nice and clicky as retro keys should be. Snap in the buttons making sure that the two small studs (one of which is circled in red below) align with the holes in the base.

I love the light lavender keycaps which inspired the overall aesthetic for this build. 

On the backside I now have wiring access to the sensors and switches. In addition I designed a frame to hold the pro Micro and the CD4067BE 16-Channel Analog Multiplexer (TBM MacroPad Chip Holder). 

Here is another look. The pro Micro and multiplexer will be attached with two sided tape.

Ready to wire.

Step 3: Do the Wiring

The first thing I did was to trim the leads on the SS49E sensors and spread them out a bit for easier access.

Then I wired the power connections to all the sensors and the buttons. Obviously a lot of care was taken not to introduce shorts, especially with the tiny hall effect sensors.

Next I wired the outputs of the 16 sensors and power to the multiplexer (left below). Finally I connected the pro Micro (right below) to the buttons, multiplexer, and project power.

Here are all the connections:

Arduino pro Micro   CD4067BE Multiplexer     Wire Color        Description
~~~~~~~~~~~~~~~~~   ~~~~~~~~~~~~~~~~~~~~     ~~~~~~~~~~        ~~~~~~~~~~~
      VCC                +5V                Dark Orange     Also powers the sensors.
      GND                GND                   Black        Project Ground
      A0              Common Input             White        Read the channel selected
by S0-S3.       
   I/O Pin 2-9                              Light Orange    Button 1-8
   I/O Pin 15            S0                                 These four inputs select
which of the 16 analog
channels to read.           
                                              Yellow         
   I/O Pin 14            S1                   Yellow
   I/O Pin 16            S2                   Yellow
   I/O Pin 10            S3                   Yellow
    I0 - I15                 Green        Sensors 1-16
      GND                E                     Black        This is an inverted chip
select pin. 

Step 4: Making Tiles

A tile starts with a base (TBM Tile Base). The base has two square "wells" to hold the magnets. I learned through testing that with one magnet and a single linear hall effect sensor, given the thickness of a tile as a constraint and the sensitivity of the sensor, you can reliably differentiate between 10 different tiles. So with two magnets and two sensors I can have 100 unique tiles. You can see the dark violet base below.

You can also see in this picture five lavender magnet holders (TBM Tile Magnet Holders). When trying to measure the strength of the magnetic fields consistently between different tiles it's important to set the magnets in the exact same position each time. Each of the five magnet holders positions the magnet at a different distance from the base (where the sensor will be), from 0 mm to 3 mm. Of course we know that the magnetic field strength decreases as distance increases. The linear hall effect sensors are responsive enough to detect these small differences. 

The voids in the magnet holders are sized to snugly hold two 6 mm diameter x 1.6 mm high neodymium magnetic disks. There are also some wedges (found in TBM Tile Magnet Holders) to make sure that the magnets inserted from the side are precisely centered over the sensor. Here is a better look at the holders. The numbers help me keep them straight. Also when putting the magnet holder into the base wells the printed number should always be on top.

But Mike that's good for only five different tiles. Right, but it turns out that the hall effect sensors I am using can detect the polarity of the magnetic field in addition to its strength. So you can double the number of different tiles recognized to ten by just reversing the polarity of the magnets and using the same five holders. 

To make a tile simply drop two "loaded" magnet holders into the wells and attach a labelled "top" (TBM Tile Tops) to the tile with a few drops of glue. Of course you will have to keep track of the "values" associated with the magnets used and map them to the specific macro that the label of that tile indicates. More on this later.

Step 5: Calibration

In order to get accurate tile readings, it's important to calibrate the linear hall effect sensors. I found that for the best results, each sensor should have its own calibration values for each of the possible tile variations. With 16 sensors and 10 possible tile variants this can be a pretty tedious process. So to make things easier I create a calibration sketch. NOTE that you only have to do this calibration once.

To start I create a special set of calibration tiles.

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 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.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.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.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.ino sketch.

You will find the Calibrate_MacroPad.ino and MacroPad.ino sketches attached.

Step 6: Defining Macros

Macros are defined within the MacrePad.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 7: Finishing Touches


Attach the MacroPad Base to the MacroPad Body (TBM MacroPad Body).

When I did the wiring, I chose to use jumper wires to connect to the proMico rather than soldering directly to the header. I was glad that I did because the USB connector on the pro Micro broke and I had to replace the board. At any rate the jumper wires extended below the bottom of the case, so I added some rubber feet (TBM MacroPad Foot Pad) which not only fixed that issue, it made the box more stable.


I also added auto-repeat if the macro key is held down for more than 2 seconds.

Step 8: Final Thoughts

Super happy with the final project. I works well. The tiles are recognized with perfect fidelity. I think that this is the best looking project I have ever done (your mileage may vary). 

The tiles have a nice weight to them. It's hard to articulate how satisfying this tactile interface is to use, at least IMHO.

I think I did a pretty good job of keeping the overall project cost down. My BOM looks like this:

Part                         Cost (CAD $)         Thoughts
~~~~                         ~~~~~~~~~~~~         ~~~~~~~~
Arduino Pro Micro              $10.00             Paid the Amazon tax for this one. 
                                                 Less than half the price on AliExpress 
                                                 if you have the patience.
8 Keycaps                       $8.00             Paid about a buck a piece. Again
Amazon tax but love the colo           
8 Fubata MD-4PCS switches       $4.00             These I got at a pretty good price of
50 cents apiece.
CD4067BE Multiplexer            $5.00             The DIP versions of this part are
getting pretty rare. 
16 SS49E Hall Effect Sensors    $6.40             About 40 cents each.
120 6 mm x 2 mm Magnets        $10.00            Used about 100. Cheaper versions
available for sure.
Miscellaneous                   $2.00            Wire, printer filament, etc.
                               ======
TOTAL                          $45.40             In US $ this total would be $33.11. 

I'm pretty sure with careful shopping one could get this down to $30.00 CAD bucks or so. 

For a macropad, this one is a little on the large size. The tiles do take up a fair amount of real estate, but I think this is partially offset by the clarity of the labels. In truth, because of their size, I find myself occasionally trying to press the tile instead of the button. I'm sure my muscle memory will eventually make the adjustment, but it has gotten me thinking about how I could make the tiles the buttons as well. This would reduce the overall size of the MacroPad. I have a few ideas on how I could make this work. Stay tuned.

If you are interested in more technical details of the MacroPad build check out my Hackaday Project.