I got the Adafruit KB2040 to make a USB keypad and (eventually) a Dactyl. I had a difficult time finding a good explanation of how to use the RP2040 to make a USB keyboard without relying on a keyboard-only firmware like QMK. I hope this article can help others with similar goals. The end result is a working USB keyboard using only the Raspberry Pi Pico SDK and our C code.
There's a USB keyboard guide for the chip I'm using here, but it uses CircuitPython, which I won't use. If I'm programming embedded, I'm going to use C (or better, Cakelisp-generated C). I like to have as few layers between me and the hardware as possible.
Ben Eater has an incredible video on the USB protocol for keyboards. He shows via oscilloscope the USB wire protocol and explains bit-by-bit how it works. I think if one wanted to make the simplest possible keyboard software-wise, they could directly send these signals without implementing the entire USB device spec. While tempted by this idea, I am too busy to do something like that, so I must find a little help. Either way, I consider that video a must-watch to understand the complexity which goes into USB and the decisions that you'll need to make specifically for your USB keyboard.
Why not use QMK?
QMK is a popular firmware for keyboards.
Software is complicated. I like to understand how things really work. I didn't use QMK because I want to know exactly what I'm putting on the chip and how the keyboard actually functions. I only want the features I want and like to be able to make any features without having to conform to someone else's framework (and its limitations).
If I use QMK, all I really learn is how QMK works. If I write instead straight to the chip's SDK, I get to learn more about how my chip works. If all I ever want to make is keyboards, then learning QMK is probably the best thing to do, but I don't just want to make keyboards. I drive E-paper displays and program my watch and want to do other stuff with these awesome little chips.
QMK is trying to support a large number of different keyboards on a large number of different chips. It's a much harder problem to solve, and results in complexity. I have a single chip and a single layout, so I can make a much simpler project.
In short, I learn more useful things by not using QMK, even if it means I have to learn more and do more work.
Problems with Arduino
I initially implemented matrix scanning in Arduino, which is the recommended path when first setting up the KB2040, at least if you're not using CircuitPython. However, I could not find any direct path to implementing a USB device through the Arduino SDKs.
In addition, I'm not sure why but I had to do strange things to get the matrix scanning correctly. I browsed various keyboard implementation blogs when dealing with floating pin issues. This blog in particular managed to solve my floating pin issues, but I didn't really understand why I needed to set the pin modes in the way they did.
The Pico version was more intuitive, since I did not need to toggle pull-ups on each column scan. The pin initialization code is here. This is no doubt a lack of understanding on my part of exactly how the pull-ups interact with my matrix wiring.
Pico SDK to the rescue
I knew I wanted to peel away any layers built on top of the RP2040. Luckily, the Raspberry Pi RP2040 has an official SDK called Pico written primarily in C which allows us to work as close to the chip as possible.
The drawback is that we are now specifically coding for the RP2040, so if we want to switch boards to an AVR chip or something we will need to rewrite some of our code. The advantage is we can more easily understand what's going on since there are fewer abstractions over our chip.
See Getting started with Pico and the pico-sdk ReadMe for the most up-to-date official documentation for Pico.
My project's readme has complete instructions for cloning and building the SDK and
picotool as well as how to flash the board.
Because we are making a USB device, you must ensure you run the following commands to clone
tinyusb and initialize its submodules:
cd pico-sdk git submodule update --init cd lib/tinyusb git submodule update --init
You'll know you did it right once the CMake output says it successfully found TinyUSB. If it hasn't, it will tell you that USB isn't supported.
I am using the Adafruit KB2040, which means I need to add
-DPICO_BOARD=adafruit_kb2040 to the
cmake line. You can see the list of RP2040 boards here. If you have some other board, just open one of those headers and you'll see there's not much information you need to specify to support your custom board.
I built the pico-examples and flashed the USB
hello_world and USB hid composite examples to ensure Pico was working correctly with my board. The latter example will move the mouse, change the volume, input the character
A, and do a fake "joystick" report when the Boot button is pressed on the RP2040 in order to showcase the device's full HID capabilities. It is exciting that we can implement all of these types of devices with TinyUSB, but for this project we only need the keyboard interface.
Once I knew everything was working, I copied over the
dev_hid_composite project and necessary CMake files and ported my matrix scanning code to Pico. The src directory shows the complete set of files needed to build our firmware in conjunction with the Pico SDK (you can ignore
When implementing a new USB device, there are a variety of "usb descriptors" that need to be specified to tell the host computer about your device.
The Open Source Hardware Association has an FAQ page on USB vendor and product IDs. In short, it is not possible to get a valid USB vendor ID without paying the USB Implementers Forum $5,000, at least as of 2013. If you make a keyboard with intent to sell it to a large number of people, you should get a valid vendor ID.
Needless to say, a person building a keyboard for private use shouldn't need to pay that much just to get it working. In the tinyusb HID device example they use a VID of
0xCafe and a PID based on the interfaces provided. I think this
0xCafe is just a hexadecimal number that spells out a fun word and happens to be an unused vendor ID (which you can verify here).
Luckily, at least on Linux, this "invalid" VID doesn't cause any issues besides not showing the device manufacturer when running e.g.
lsusb, which instead shows:
Bus 001 Device 004: ID cafe:4004 Generic 4-Port USB 2.0 Hub
This name is incorrect because we know our
cafe device with PID
4004 is in fact an HID device, not a USB hub. On my other computer the device shows up as
cafe:4004 with no product name or device name at all. This may be a bug in
lsusb or the underlying product name retrieval, I'm not sure.
Assumptions in TinyUSB keyboard implementation
If you watched Ben Eater's USB keyboard video you'll know that there are multiple ways to implement a USB keyboard. TinyUSB's hid composite example limits the number of keys sent in a report to 6. We know we can raise this number if we do some more work to tell the host we want to send more information.
This is an example of an assumption made by TinyUSB which we know isn't a requirement. The more we learn about how things work, the more flexibility we gain and the fewer limitations we have. In my case, I am fine with the 6 key limitation and simply "roll over" the other pressed keys to the next report.
One question I still have with the TinyUSB HID implementation is whether it runs at full speed or high speed, which will influence the keyboard's input latency. I need to investigate further, but my guess is that it runs at full speed by default and that I could reduce latency by using a different part of the tinyusb API.
With all my talk in this article of stripping away layers, TinyUSB is indeed another layer and may therefore need to be looked into whether it can be stripped away. I will leave it for now since I'm not looking to get deeper into the USB weeds on this project at least.
This keypad project was a test run for my full keyboard project, which is a Dactyl. I was looking at building a Dactyl-Manuform but opted not to because the two innermost columns have fewer keys than I'm used to. The arrow keys on the Kinesis Advantage2 layout are positioned such that I don't need to move my hands to move around, which is a key ergonomic and efficiency gain. It makes Vim-style HJKL unnecessary because motion doesn't require moving the hands. I believe in having more keys and fewer modifiers, which means complex layering setups with e.g. a motion and a input layer would not be favorable.
If you would like to avoid all the hassle (and fun) of building your own keyboard, I highly recommend the Kinesis Advantage series. I don't have the Advantage 360 but it looks fantastic. I own two Advantage2s and besides the lack of split and full programmability I am very happy with them. I've dealt with pain in my wrists for a long time, and I believe the Advantage2 helped a lot.1 I know ergonomic keyboards might not look as elegant as those tiny ortholinear keyboards, but for anyone with typing pain they are a must.
For a nice view of all the interesting things happening in the custom keyboard world, check out kbd.news, which I found to be a great source for inspiration and information.
I also recommend hand strengthening with a hand grip strengthener and setting up your desk, chair (mine is an Aeron bought from a local used furniture reseller), and keyboard height ergonomically. If you had to do one of these things, go for the grip strengthener. I do only 20 or so reps a day on each hand to great effect.↩︎