At some point in college I decided that I HAD to have an old Model M keyboard. Similar to one that I had growing up as a kid. When I finally managed to track one down (For $55, which felt like so much back then) I was a little sad to discover a couple of keys didn't work. I didn't have the skills at the time to fix it, but I shelved it away and told myself I'd fix it someday.
Almost 10 years later, and I finally got around to reviving this thing.
I have to say, this was one of the most fun projects I've taken on in a while. I learned so much about how old keyboards work, how that influenced design of new ones, what new keyboards did to improve on the design, and so much more. I opted not to try and install QMK, TMK, or any other pre-made solution. I knew it was possible to do this from scratch and I really wanted to learn how. So let me walk you through what all I did. But first, here's a quick overview of all the things I learned:
[1] Scanning
[2] Debouncing
[3] Anti-ghosting and Key Rollover (KRO)
[4] Modifier keys
[5] HID
[6] Holding a key
[7] Regular Typing vs Gaming
[8] Conclusion
Scanning
So you've hooked up your ribbon cables to a microcontroller, set all the pins to input, start typing; and you see nothing... What's going on?
Long story short, your keyboard has WAY more keys than your microcontroller has inputs. So rather than getting a microcontroller with 60-100+ GPIO pins, keyboard designers put everything into rows and columns. This drastically reduces the number of pins you need to read all the input. There are some disadvantages to this; the biggest of which is in how you have to actually get the input. And that's where scanning comes in.
To get things setup, we configure all of the columns to be output pins and all of the rows to be input. We then loop over all of the columns. We start by sending power down the column. We then loop over all of the rows and look for any keypresses. We do this by reading the input on that row. If it's 0, there's no keys being pressed. If it's a 1, we have a key press. From here, all we need to do is note down what key we pressed in a spreadsheet or something. This method works because we have effectively redirected power from the column, down the row, to the pin on the microcontroller at the end of that row. It may be helpful to imagine water flowing from the microcontroller, down the column, being redirected at a row, and going to an input pin on the microcontroller. We then read any power coming into a pin as a keypress.
Now that we can scan the keys, the rest is just printing the row and column and noting which key we pressed. I went through all 101 keys until I had a spreadsheet of what row/column each key lived at.
Debouncing
If you're printing things to the screen, you'll notice that when you press a key, it may print like 5-6 times. Assuming you're not holding the key, what you're seeing is called "bouncing". This is a hardware-level occurrence where the physical interaction of the membrane or the clicking of the metal in a modern mechanical switch jitters. If you were to look at an oscilloscope while you pressed a key, you would see a lot of noise around the rising and falling edge of the signal. This is the "bounce". Keyboard manufacturers account for this with a software-defined process called "debouncing". Which is just a fancy word for "ignoring input until it settles". In the oscilloscope picture I posted, I believe each square on the grid is 1 ms. So all you need to do in your code is record the time when a key is first pressed, but don't do anything unless it's been pressed for longer than your debounce delay. There's several ways to do this, but you can check out the code if you're curious.
Anti-ghosting
On older keyboards, you can get a funny phenomenon where if you press several keys next to each other, a totally random key nearby will register. This is called "ghosting" (because you didn't press that key and it unexpectedly showed up). Thankfully this wasn't really an issue on my keyboard. I'm not totally sure why because it technically should be. I think my holding delay helps but I'm mostly guessing.
Before I explain how this is problem is non-existent on modern keyboards, it would help to explain what is called "Key Rollover" or "KRO". This is just a fancy phrase to describe how many keys you can press at once (e.g. "Ctrl + C", "Ctrl + Shift + Esc", etc). Most older keyboards that have this membrane-based column/row setup had what is called "two-key rollover" or "2KRO"; meaning you can press two keys at once. Any beyond that and the computer will most likely not register your keypress. I say most likely because it really boils down to what combination your pressing because it depends heavily on what rows and columns are involved. For example, "Ctrl + Shift + Esc" works on most keyboards as they are all on separate rows and columns from each other.
Modern keyboards have what is called "NKRO", "N" being a placeholder for any number. Meaning you can have really long combinations of almost any number of keys and it will still register. Ghosting, and the ability to have NKRO is all possible thanks to one simple component: the diode. In electrical circuitry, a diode only allows electricity to flow in one direction. Remember our analogy from earlier about water flowing down rows and columns? That was a bit idealistic. In reality, electricity follows the path of least resistance; and because we can press a bunch of keys at once (and thereby open a bunch of switches at the same time) it's possible for electricity to flow through other switches and be misread as a keypress. This typically happens when several keys are pressed in the same row, same column, or specific combinations of both.
Modifier Keys
Not long after I had some keys being printed to the screen, I went to capitalize a letter with the shift key only to realize "Oh yeah, I have to code that" lol.
It didn't take long to get this working, but it did take a while to get this working flawlessly. I won't bore you with the details of how I did it (again, the code is available if you're curious), but I just wanted to include this section as it was the first time (of many) where I realized "Oh right, that's not gonna work if I don't code it".
HID
Once I got all the keys working as well as the modifiers, it was time to actually get the microcontroller to tell the computer "Hey, I have a keyboard keypress for you to register". Any mouse or keyboard you use utilizes a protocol to register with the computer as a Human Interface Device or HID. This is what allows your keyboard to take what it knows to be keypress and tell the computer that a particular key has been pressed.
This is pretty easy to setup in CircuitPython with the "usb_hid" module as all you need to do is something like "keyboard.press(Keycode.ENTER)" in order to tell the computer that I hit "ENTER" and it should act accordingly. The tedious part of this section was updating my keymap in my code to include the CircuitPython keycodes so that I could make it more dynamic. But it's really not that bad.
Holding A Key
Shortly after getting my keyboard actually sending keypresses to a real computer, I had another "Oh right, I have to code that..." moment where I held down a key but it wasn't spamming the key. This again wasn't too hard to implement but did take some tweaking to make it feel nice without being annoying.
One thing I coded was a "holding time check" which was nothing more than some similar time-tracking and math to my debounce logic to determine whether or not I was holding a key. If you try it right now, you'll notice a small delay before the computer starts spamming that letter; that delay is what I was coding and playing with. It was actually pretty neat to be able to tweak this. I would set a value, try it out, and if it didn't feel right I could set it to exactly what I wanted. Perks of writing your own keyboard software I guess lol
Regular Typing vs Gaming
For me, the true test of whether or not I coded this thing well was if I could play a videogame with it. Typing and Gaming are very different beasts as I'm about to lay out for you.
The test game I chose was Helldivers II since it's a good mix of regular "gaming" controls mixed with some interesting mechanics that make use of the WASD keys for more than just movement (I remapped this to the arrow keys, however so that movement and the other mechanic are separate). Once I booted up Helldivers II, I went to move my character and he was just twitching. This was my final "Oh yeah, I have to code that..." moment. The way I had my letter-spamming code setup initially was it would press and then release the key afterward. This works fine for typing, but in a game it needs the key to be pressed constantly. So I needed to rework my code such that it didn't release the key until I physically released the key.
Another example is in regards to one of the interesting mechanics I mentioned. Helldivers II makes use of a mechanic whereby you hold down the left CTRL key and then press a combination of the WASD keys matching a direction (an up arrow is W, down is S, etc. However, I remapped this to my arrow keys since I'm faster at this with my right hand). Once I solved the previous issues, I couldn't get the menu to show up for this mechanic because the game was expecting the CTRL key to be constantly pressed (similar to my movement issues in the previous section). The way my code worked at the time is it would check to see if you were pressing any modifier keys and then apply those later if you pressed another key. This problem meant that I needed to check if you were pressing a modifier key and then actually press it via code. Then release when you released the key. Seems obvious now, but again highlights another case where this wasn't an issue when I was typing, but became a problem during gaming.
Conclusion
This was hands-down one of my favorite projects recently. Sure, you could say it was a bit masochistic (again, QMK exists for a reason), but it was fun to learn soooo much about how keyboards work, what certain programs and games expect from a keyboard, and then getting to code those features myself and be able to tune them since I was doing everything from scratch.