It’s more likely than you might think.
I have a cool programmable QMK keyboard.
I also happen to have a liking for the Rust programming language.
Unfortunately, however, my OLKB Planck rev6 from Massdrop only officially supports QMK, which is written in C. While QMK is amazing, and allows me to program every last button on my keyboard, it’s still written in C, and C is short for Cringe. Apply the rule of transitivity to this, and it’s clear that QMK is cringe too.
My biggest problem with QMK is the fact that it’s a poorly documented keyboard firmware framework. The codebase feels untraversable and opaque. There are functions and macros with purposes unbeknownst to me, and I don’t feel like I own the functions, only what they do.
Here’s an illustrative example of what I mean:
// When the default layer changes
layer_state_t default_layer_state_set_user(layer_state_t state) {
// Stuff goes here
}
// When the activated layers change
layer_state_t layer_state_set_user(layer_state_t state) {
// doMagic();
}
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
// More code
}
// When the keyboard starts
void keyboard_post_init_user(void) {
// Lorem ipsum dolor sit amet
}
These are functions that I haven’t defined, only implemented. They act sort of like callbacks, meaning that they are called in certain circumstances and are expected to perform certain actions. The worst part, however, is that I haven’t found any documentation for what functions I can implement, and what each function do. This means that it’s difficult to add QMK features to your keyboard firmware, and your only resort is to read other people’s code and asking for help in the official discord channel.
Repeat after me: Don’t use discord as a communication platform for FOSS projects. Got it? Cool.
Fortunately, there are quite a lot of examples in the QMK repo, and the people on the QMK discord are quite friendly and helpful. But it shouldn’t have to be like that.
Consult a friend
I have a friend that built his own keyboard in Rust. Like, he even wrote the 3D-models for the case in Rust (using a self-implemented OpenSCAD-codegen library in Rust, as you do). He’s using a crate called keyberon which exposes helpful structs for building your own keyboard firmware in rust. And besides the obvious improvement of being written in Rust, it’s also a badly documented library instead of a badly documented framework. This means that it wont try to hijack how I build and flash the firmware, and it’s just like any other library.
Luckily for me, Mr wezm on GitHub had developed a keyberon-firmware
for his Clueboard 66%
Low Profile Keyboard, which happens to use the same
microcontroller as my OLKB Planck, namely the STM32F303
.
So porting it to my keyboard should be trivial, right?
Well, mostly.
The first problem, after setting up a highly imperative development environment 😩 by using rustup and installing specific targets, was figuring out what to keep and what to replace from the rightfully stolen code from wezm.
One obvious thing that had to change was the keyboard matrix.
“What’s a matrix?”, you might ask. Basically: keyboards have many keys, and microcontrollers don’t have that many GPIO pins. So instead of getting a microcontroller with 100+ pins, we cheat and arrange the keyboard switches in a “matrix” configuration, where we can index each switch using a row and a column. Then the microcontroller can figure out which keys are being pressed by sending current through each column, and seeing which rows get activated, or vice versa.
In a beautiful world these pins would be logically ordered along the pins on the microcrontroller, but it’s abundantly clear we don’t, and thus the pins aren’t ordered. Fortunately, QMK already had to figure this out before me, so all I had to do was yoink their solution.
qmk_firmware/keyboards/planck/rev6_drop/config.h:40
#define MATRIX_ROW_PINS \
{ A10, A9, A8, B15, C13, C14, C15, A2 }
#define MATRIX_COL_PINS \
{ B11, B10, B2, B1, A7, B0 }
Here are the row and column pins, ez.
After having my friend convince me that rust will make heavy use of type-theory to prove that the binary I just compiled won’t let the magic smoke escape from my keyboard, I tried flashing it. To my surprise, it kinda worked, even.
Well, what did work was the better part of the left half of the
keyboard. I quite quickly realized this was because the keyboard
layout was stored in a 12x4xN
array (where N is the
number of layers), and the matrix was defined as a 6x8
array, with 8 pins for the rows, and 6 pins for the columns.
But why?
Just math, really.
My keyboard could be using a 48x1
matrix, one for each
key, however, this would require a microcontroller that has 49 GPIO
pins available for the matrix alone. It could also be using a
12x4
matrix, which would arguably make the most sense,
since the keyboard has all the switches already arranged in a 12 by 4
grid. This would only require 12 + 4 = 16
pins, which is
significantly less. However, we could save two more pins by using a
6x8
matrix, effectively moving the right half of the
keyboard to be located below the left half instead. This would only
require 6 + 8 = 14
pins, and add a bit of extra
complexity. But man pins aren’t cheap in this economy.
Or are they..?
After some investigating, I found that it’s common to apply some transformations on the matrix events to have them better match the actual layout of the keyboard.
So I added some code like this:
.transform(|r, c| if r < 4 { (r, c) } else { (r - 4, c + 6) }) event
This got me almost the entire way, and the right half was now working too, but something was up with the bottom row: some keys weren’t where I expected them to be.
After some more investigating, I found that the matrix was even more complicated than I first thought, as can be seen in this beautiful C macro for creating planck layouts:
#define LAYOUT_ortho_4x12( \
k00, k01, k02, k03, k04, k05, k40, k41, k42, k43, k44, k45, \
k10, k11, k12, k13, k14, k15, k50, k51, k52, k53, k54, k55, \
k20, k21, k22, k23, k24, k25, k60, k61, k62, k63, k64, k65, \
k30, k31, k32, k73, k74, k75, k70, k71, k72, k33, k34, k35 \
) { \
{ k00, k01, k02, k03, k04, k05 }, \
{ k10, k11, k12, k13, k14, k15 }, \
{ k20, k21, k22, k23, k24, k25 }, \
{ k30, k31, k32, k33, k34, k35 }, \
{ k40, k41, k42, k43, k44, k45 }, \
{ k50, k51, k52, k53, k54, k55 }, \
{ k60, k61, k62, k63, k64, k65 }, \
{ k70, k71, k72, k73, k74, k75 } \
}
This macro transforms the physical switch positions into what the
matrix expects, and as we can see, it transforms from a
“12x4
” array (it’s really just a 48x1
array
with some formatting) to a 6x8
array. But carefully
looking at the last row of the input matrix, we can see that second
(k33 to k35) and the fourth (k73 to k75) quarter seem to be
swapped.
If I had to guess, they did this because it significantly eased routing the traces on the PCB.
I just added another transform, to get the following code:
event
.transform(|r, c| if r < 4 { (r, c) } else { (r - 4, c + 6) })
.transform(|r, c| match (r, c) {
// If the event is in the second quarter, swap with the fourth
(3, c @ 3..=5) => (r, c + 6),
// If the event is in the fourth quarter, swap with the second
(3, c @ 9..) => (r, c - 6),
(r, c), _ =>
})
I love me some weird match blocks
The next thing I was looking forward to implement was controlling the underglow RGB. On QMK I had used it for indicating what layer I was on, and I wanted to do the same here with keyberon. But first I had to get them working at all.
After some investigating in the QMK source code, I found this code:
qmk_firmware/keyboards/planck/rev6_drop/config.h:106
/*
* WS2812 Underglow Matrix options
*/
#define RGB_DI_PIN A1
#define RGBLED_NUM 9
#define RGB_MATRIX_LED_COUNT RGBLED_NUM
#define WS2812_PWM_DRIVER PWMD2
#define WS2812_PWM_CHANNEL 2
#define WS2812_PWM_PAL_MODE 1
#define WS2812_DMA_STREAM STM32_DMA1_STREAM2
#define WS2812_DMA_CHANNEL 2
This told me that WS2812 is probably the LED driver, that the pin A1 is important somehow, and that it’s probably using PWM.
A quick search on crates.io resulted in two relevant crates:
While I could have been smarter about picking one of them, trial and error worked, because ws2812-spi seemed to expect three pins, which ws2812-timer-delay only expects one pin and a timer. So ws2812-timer-delay indeed seems like a better fit here.
Luckily, my friend was also using the same driver, and with the help of his code and some examples, I wrote some code that looked like it was going to work.
But, well, it didn’t work.
In fact, nothing worked now. The keyboard just froze whenever I flashed the firmware.
After more trial and error of commenting parts of my code out until things worked again, I discovered the faulty line:
.start::<Nanoseconds>(3.MHz().to_duration().unwrap()); led_timer
This line starts a timer running at 3MHz. Or I mean, it was supposed to, because instead it just made the microcontroller crash.
After some discussing with my friend as to why this could be, I
opened an issue
on the stm32f3xx-hal
-crate, which included the code for
the timer itself. Within 24h, the maintainer had not only answered my
issue by creating and merging a pull request, but they even pushed a
new version to crates.io. Talk about express delivery!
All I had to do now was to update the
stm32f3xx-hal
-crate to the next minor version in my
Cargo.toml, and recompile. And this time it just worked.
It’s quite a satisfying feeling when this happens, because it’s a sort of validation that there was nothing wrong with the code I was writing.
Maybe it’s like I know what I’m doing after all…
Now that I had gotten the rust firmware itself working, all that was left was to make sure I could build it reproducibly using nix. ;)
This took some time to get working, since it’s not just a
cargo build
and rock’n’roll, but more of a:
cargo objcopy --release -- -O binary planck.bin
dfu-util -d 03a8:a4f9 -a 0 --dfuse-address 0x08000000:leave -D planck.bin
as adapted from the wezm/clueboard-rust-firmware crate’s README.md.
In the end, I wound up using fenix to create the
custom toolchain (since the keyboard isn’t running x86_64-linux) from
a rust-toolchain.toml
-file, and crane to build the
firmware quickly and reproducibly. I also added an
apps
-output to the flake, so that I can run
nix run .#flash
, and nix will not only build the
firmware, but also flash it on the keyboard for me.
Now do I not only have Rust running on my keyboard (built with Nix), but as far as I’m aware, I’m the only person in the world running Rust an OLKB Planck. So yeah, I’m basically famous now.