Weekly driver: keypad matrix circuits
I wrote a driver for keypad matrix circuits as part of the embedded rust weekly driver initiative.
The point of a keypad matrix is to let you use a small number of microcontroller pins to read from many keys, arranged in a grid of rows and columns. To check if a certain key is pressed, you set its column pin low and read the voltage on its row pin. The keypad driver handles that scanning process transparently.
It makes use of the embedded-hal hardware abstraction layer in two ways. First, it's platform-agnostic: it can run on any device that implements the HAL, and use any row and column pins that implement InputPin and OutputPin traits, respectively. Second, for every key in the keypad, it gives you a virtual KeypadInput pin that also implements the InputPin trait. This means you can treat each KeypadInput like a single, normal input pin, even though the driver is secretly scanning the matrix for you whenever you read from it. This approach was inspired by the shift-register-driver crate, which lets you control a shift register through virtual output pins.
Most of the work happens in keypad_struct!(), a big complicated macro. It creates a struct that owns the keypad's row and column pins and can decompose() itself into virtual KeypadInput pins. There are two reasons why we need to use macros to make the driver sufficiently generic:
-
Every single pin has a unique type, and we don't know which pins will be used. We know that they all implement a certain trait, but that doesn't help much because it doesn't tell us the size of the type, so we can't directly stick it into the struct. If we could use dynamic allocation, we could just store pins on the heap as boxed trait objects. But we need to run on embedded platforms without an allocator! So, we can use a macro to generate a struct containing the exact, concrete pin types the user provides.
-
We don't know how many pins there will be, because the keypad could have any number of rows and columns. That makes it hard to implement
decompose(), which needs to iterate over the row and column pins. We can't store pins in arrays because they all have unique types, but we can store them in tuples instead. The problem is that you can't actually iterate over a tuple, and even indexing into arbitrary fields of a tuple using a macro is stupidly hard. The best approach I could come up with was to repeatedly destructure the tuple with different patterns, like this:
let tuple = (0, 1, 2);
let array = [
{
let (ref x, ..) = tuple;
x
},
{
let (_, ref x, ..) = tuple;
x
},
{
let (_, _, ref x, ..) = tuple;
x
},
];
for reference in array.into_iter() {
// ...
}
So that's how the keypad_struct!() macro iterates over tuples of pins. It counts the length of the tuple (also tricky!), creates patterns with increasing numbers of underscores, and uses them to build up a temporary array of references that it can iterate over. Luckily it only needs to run this code once, and not every time we read from a pin.
Overall this was a fun exercise in working around limitations.