June 3, 2022

QMK: 153 encoders anyone?

You can connect up to 153 encoders directly to a single Arduino Pro Micro that has 18 I/O pins. Or connect a bunch of encoders and still have many pins left for the regular keys!


Why

I wanted to create a module for the Work Louder Creator keyboard with as many encoders as possible while keeping it usable. If I used two pins per encoder, I would run out of pins to connect buttons in encoders to. So I ended up with 8 encoders that took only 5 pins. Read more about the Big L.


How

Encoders have two pins: A and B, that send pulses at a different time — that's used to determine the rotation direction of the encoder.

From the QMK documentation on attaching encoders we know that multiple encoders can share a single pin as long as one pin for each encoder is unique:

You could even support three encoders using only three pins (one per encoder) however in this configuration, rotating two encoders which share pins simultaneously will often generate incorrect output. For example:

#define ENCODERS_PAD_A { B1, B1, B2 }
#define ENCODERS_PAD_B { B2, B3, B3 }

Here rotating Encoder 0 B1 B2 and Encoder 1 B1 B3 could be interpreted as rotating Encoder 2 B2 B3 or B3 B2 depending on the timing. This may still be a useful configuration depending on your use case

From that I figured out that we can use mathematics to find the number of unique pairs of pins for encoders given the number of total pins. The pairs have to be unique, otherwise there won't be any way for the encoder to determine which encoders were rotated exactly.

We don't care about the order of pins in the pairs, the pairs just need to be unique, so we need to use the combinations formula. Here is the calculator I used. r = 2 (pairs), n is the number of pins you want to use for encoders.

So for example if we want to use three pins, we we can attach three encoders to them. Encoder A: pins 1 and 2, Encoder B: pins 2 and 3, Encoder C: pins 1 and 3. Here is the list so you don't have to calculate this yourself:

For the Big L module, I decided to use 8 encoders. From the table above it's easy to find that you can connect up to 10 encoders to 5 pins. 4 pins can only fit 6 encoders. So I used 5 pins. All encoders work flawlessly!

I guess the same would also work for the keys, so this way you can fit even more keys than with a matrix. Number of required pins will be the sum of rows and columns, max number of pins is 18=9+9 and 9×9=81 keys. However that would require a ton of wires, so I doubt anyone would do the same for encoders either. But for small projects like this, using the combinations approach seems the best!


Bonus: Encoder actions library

Controlling so many encoders with the regular switch expression or if/else in the encoder_update_kb function would be a huge pain if you want easy customization. Here is what this abomination looks like for 8 encoders (had to be done for testing):

It would be much easier to customize it if encoder actions were treated as regular keys in the matrix... And this is exactly what Encoder Map feature in QMK allows you to do. However, it's not supported in VIA (RIP 💀), so for VIA let's use a workaround.

Encoder Actions library is like an early prototype of the Encoder Mapping in QMK. The library doesn't have a dedicated source, I found it in the Work Louder keyboards QMK firmware written by Drashna.

Adding encoder actions to QMK

  1. Add NO_PIN keys to the matrix for the “virtual” keys (config.h)
  2. #define ENCODERS 8 — Define the number of encoders and their clockwise and counterclockwise rotations: they should correspond to the virtual keys pins. (config.h)
  3. Define the layout for the matrix (big_l.h) so you can later use it in keymap.c

Here I'm using row at index 1 for CW keys and row at index 2 for CCW. The row 0 is used for the encoder push buttons, allowing you to assign actions for when encoders are pressed down.

And here is the keymap.c that uses this layout. Encoder presses type letters, the encoder rotations type numbers and letters. After this, the actions can be edited in VIA if necessary.

I modified the Encoder Actions library slightly to make it work without VIA as well. That required simply editing the condition to not include VIA. Before and after in encoder_actions.c file:

-- #if defined(VIA_ENABLE) && defined(ENCODER_ENABLE)
++ #if defined(ENCODER_ENABLE)

Adding encoder actions to VIA

Now for VIA support, you need to add these virtual keys to the layout in the Keyboard Layout Editor. I suggest putting them below or to the side of the encoder push button, I did the former for Sexypad.

Now we need to create a VIA-readable layout file in JSON format. I did that by copying an example from the VIA repo and replacing the layout with mine from KLE. Just make sure the vendorId and productId fields in VIA are identical to the ones defined in QMK config.h file.

Import the JSON file via “Design” tab in VIA to configure the keyboard. If the JSON format is incorrect, VIA should show an error. Otherwise, go back to “Configure” and you should see your device and layout. Save the JSON layout file somewhere (I put it in my QMK keymap folder) and import it to VIA every time you want to change the configuration of the keyboard.


Thanks for reading!

Here is the link to the firmware for the Big L in case you need it for reference. And here is the Sexypad firmware, which has VIA support.