Update: I have improved the code, see "Update" at the end.
I am currently working on a project that will have an integrated keyboard, which presented a problem: how do I include a keyboard in the development board prototype? I can't use a USB keyboard or an existing Arduino-based keyboard, because the keyboard in the actual project is connected directly to the microcontroller that handles all of the other functions. So I designed this basic PCB-based 64-key prototyping keyboard matrix.
This PCB does not contain any ICs (integrated circuits). The rows and columns of the keyboard matrix are connected directly to the pin headers so that the keyboard can be connected to an Arduino or any other microcontroller. It is perfect for prototyping your projects that will include an integrated keyboard.
I have included detailed, heavily-commented code to make this work with any Arduino-compatible development board that has an enough I/O pins available—11 pins are required. The keyboard has 64 keys, including modifiers for shift, caps, ctrl, alt, fn, and "special." There are also six additional keys that can be used for whatever you like. Every single key's functions can be defined individually, including each key's function when a modifier is active. In my opinion, this is significantly more useful than existing keyboard code that severely limits your ability to customize key behavior.
The code provided will print text to Serial. This can easily be changed if you want the text to go somewhere else.
A Note Regarding Program Size:
The code that I provide is pretty large, because it doesn't utilize any existing libraries whatsoever. I wrote this code completely from scratch in order to enable the customizability that I required. On an Arduino UNO, this will use 9100 bytes (28%) of program storage space and global variables use 394 bytes (19%) of dynamic memory.
My code could probably be more efficient and there libraries and sketches for keyboards are certainly smaller, but this is the only way I could devise to provide complete flexibility over every key with every modifier. It also takes into account real-world keyboard usage. For example, with my code pressing the Shift key while Caps Lock is enabled will result in a lowercase letter as it should. By default, holding down the FN key while pressing ESC won't do anything. But that behavior is completely customizable, so you can change it however you like.
Supplies:- The custom PCB
- 6x6x5mm tactile momentary push buttons (x64)
- 1N4148 switching diodes (x64)
- 1x8 pin headers, female or male (x2)
- 74HC595 shift register
- Jumper wires
- Breadboard
- Arduino Uno or any Arduino-compatible microcontroller development board
Why is a keyboard matrix necessary?
This keyboard has 64 keys. If you were to connect each and every one of those buttons to your development board directly, you would need 64 I/O pins. That is a lot of pins and more than most development boards have available. To get that down to a far more reasonable number we can use a keyboard matrix, which only requires a number of pins equal to the square root (rounded up) of the number of keys.
A keyboard matrix is setup so every key switch in a row is connected and every key switch in a column is connected. When we want to see which keys are pressed, we "activate" the first row and then check each column. If a particular column is active, we know that the key in that column and row 1 has been pressed. We then deactivate row 1 and activate row 2, then check all of the columns again. After all of the rows have been activated, we simply start back over at the first row.
How we scan the keyboard matrix:
Because we're working with a microcontroller, "activate" means setting that row to either LOW or HIGH. In this case, we are setting the row to LOW because we are using the microcontroller's built-in pullup resistors on our column input pins. Without either a pullup or pulldown resistor, an input pin will react unpredictably as a result of interface, which will register false button presses.
The ATmega328P microcontroller used in the Arduino UNO does not have any built-in pulldown resistors, only pullup resistors. So we're using those. The pullup resistors connect each input pin to 5V, ensuring that they always read HIGH until a button is pressed.
All of the rows are also normally set to HIGH, which prevents the column pins from connecting to the row pins whether a button has been pressed or not. But when we're ready to check a row, we can set that row to LOW. If a button in that row is pressed, this will provide a path for the input pin to be pulled to ground—resulting in that column now reading as LOW.
So, to summarize: we set a row to LOW and then we check to see which column pins are now reading LOW. Those correspond to pressed buttons. This process happens very quickly, so we can scan the entire keyboard many times per second. My code limits this to 200 times per second, which balances performance, bouncing, and ensuring every key press is caught.
Diodes, ghosting, and n-key rollover:
The diodes in the circuit are there to prevent unintended key presses when certain button combinations are held down. Diodes only allow current to flow in one direction, which prevents ghosting. If we didn't use diodes, then pressing certain keys could cause another unpressed key to be registered, as current flows through the adjacent switches. This is shown in the simplified graphic where pushing anything three adjacent keys causing the key in the fourth corner to be registered even when it isn't pressed. The diodes prevent that and enable "n-key rollover, " which means we can press as many keys as we want in whatever combination we want without any issues.
Saving pins with a shift register:
The astute among you probably noticed that I said a keyboard matrix requires a number of pins equal to the square root of the number of keys, but that I also said that my keyboard design only requires 11 pins. It should be 16, right? Nope, because we're using a 74HC595 shift register. This shift register lets us use only three of the Arduino's I/O pins to control up to eight output pins. Those three pins let us send a byte (eight bits) to the shift register, which sets its eight outputs pins to either HIGH or LOW. By using the shift register for the output row pins, we save 5 whole I/O pins!
"So why not use a shift register for the input pins, too?" you ask. The simplest answer is that input requires a different kind of shift register and I didn't have that type on hand. But using a shift register for input also complicates how we read the columns and can cause issues with noise and "bouncing." Suffice it to say that it's a headache that I didn't need to take on in this case.
Step 2: PCB DesignSchematic Design
Now that you understand how a keyboard matrix works, my PCB design should be straightforward. I designed the PCB in KiCAD (sorry Eagle judges) and started with the schematic. I simply placed a button symbol and a diode symbol, then copy and pasted those until I had my grid of 64 keys. Then I added two 1x8 pin header symbols, one for the rows and one for the columns. One side of the buttons were connected in columns and the other side of buttons were connected in rows.
The next step was to assign PCB footprints to each of those schematic symbols. KiCAD's included footprint library had the necessary footprints built-in. When you're designing your own PCBs, you have to be very careful to choose the correct footprints, because those are what will actually end up on your PCB. There are many components that have very similar footprints, but with slightly different pitches or whatever. Make sure you choose those that match your real-world components.
Footprints and Pin Numbers
Pay particular attention to the pin numbers. KiCAD has a weird issue where the schematic diode symbol pin numbers don't match the footprint pin numbers. This results in diodes being backwards, which is a serious issue given their polarity. I didn't catch that mistake and had to thrown away the first batch of PCBs that I ordered. To fix this problem on the second revision, I had to create a custom diode footprint with the pin numbers swapped.
PCB Layout
With the schematic done and footprints assigned, I moved onto the actual PCB layout. The board outline was created in Autodesk Fusion 360, exported as a DXF, and then imported into KiCAD on the Edge Cuts layer. The vast majority of the work after that was simply arranging the buttons so that they had a layout similar to a normal keyboard.
Then all of the traces were routed. Because the actual button layout doesn't match the neat and tidy matrix in the schematic, this part got a little messy and I had to resort to using vias in some places. Vias let you route a trace from one layer to another, which is really helpful when you're using a 2-layer board that has a lot of overlapping traces. Finally, I added filled regions, because it's good practice.
PCB Fabrication
With the board designed, I simply plotted all of the layers and added them to a zip folder. That folder is provided here and can be uploaded directly to a PCB fabrication service like JLCPCB.
Here is the link to the PCB Gerber files: https://drive.google.com/file/d/10YriLLtghV0Sb84Wm...
Step 3: PCB AssemblyThis is the easiest, but most tedious, step in the entire project. Just solder all of the components in place. They're all through-hole components and are easy to solder. Pay particular attention to the orientation of the diodes. The mark on the diodes should match the marks on the PCB.
In my experience, it was easiest to hold the PCB in place with a third hand and put all of the diodes in first. Then flip the board and solder them all, then clip the leads. Then place all of the buttons and solder those. Then solder the pin headers in place. You can use either female or male pin headers, it is completely up to you. If you use male head and put then underneath the board, the spacing is correct to stick them directly into a breadboard.
Step 4: Connect the Keyboard to Your ArduinoThe wiring looks complicated, but it really isn't that bad when you pay attention to where everything is going.
Eight jumper wires will go from the column header directly into the following Arduino pins:
- Column 1 > A0
- Column 2 > A1
- Column 3 > A2
- Column 4 > A3
- Column 5 > A4
- Column 6 > A5
- Column 7 > 5
- Column 8 > 6
Next, place the 74HC595 shift register on your breadboard straddling the middle break. Take note of the orientation of the chip! The dot indicates Pin 1
Take a look at the wiring diagram to see where the 5V and Ground connections go. The shift register has two pins connected to 5V and two pins connected to Ground.
Only three wires are needed to connect the shift register to the Arduino. They are:
- Shift (Clock) 11 > 4
- Shift (Latch) 12 > 3
- Shift (Data) 14 > 2
For some silly reason, the shift register's output pins are arranged in a counterintuitive way. Pay special attention to the shift register pinout diagram when connecting those to your row pins. They are:
- Row 1 > Shift (Q0) 15
- Row 2 > Shift (Q1) 1
- Row 3 > Shift (Q2) 2
- Row 4 > Shift (Q3) 3
- Row 5 > Shift (Q4) 4
- Row 6 > Shift (Q5) 5
- Shift 7 > Shift (Q6) 6
- Shift 8 > Shift (Q7) 7
Nothing is connected to the Arduino 0 or 1 pins, because those are also used for the Serial port and cause conflicts.
Step 5: Flash the Arduino CodeFlash your Arduino with the code provided here. There is nothing special about this, just upload the code like you would with any other Arduino project.
Everything in the code has detailed comments that you can read through, so I won't go into much detail here. Basically, the pins are setup as inputs and outputs. The main loop just contains a timer function. Every 5ms, it calls the function to scan the keyboard. That function calls a separate function to set the shift register before each the columns are checked. Pressed keys print their value to Serial.
If you want to change what is printed when you press a key, just change the Serial.print("_"); in the if statement that corresponds to the condition. For example, you can set what is printed when you hold FN and press N. The same is true for every other key with each modifier.
Many keys don't do anything at all in this code, because it is just printing to Serial. That means the backspace key doesn't have an effect, because you can't delete from the Serial monitor—that data has already been received. You are, however, free to use change that if you like.
Using the Keyboard with Your Own Projects
It's nice to print to serial, but that isn't really the point of this keyboard. This keyboard's purpose is for prototyping more complex projects. That's why it is easy to alter the functionality. If, for instance, you wanted to print the typed text to an OLED screen, you could simply replace every Serial.print( with display.print( or whatever your particular display requires. The Arduino IDE's Replace All tool is great for replacing those all in one quick step.
Thanks for reading this and I hope that this keyboard helps you with your projects!
This new code is completely rewritten and performs better than the original code. This was mostly done to address an issue with my algorithm that kept characters from being entered every time a key was pressed. The original code checked to make sure a particular key wasn't the last key to be pressed. This caused a problem if 2 or more keys were held down, which would cause something like "fgfgfgfgfgfgfgfgfgfg" to be entered. This also kept you from entering the same key over and over again really quickly, such as when you type the two m's in the word "bummer."
The new code solves both of these problems and is also more elegant. Instead of keeping track of the last key to be pressed, we check the state of the entire keyboard and compare it against the entire keyboard state in the last loop. This means the loop can run much faster, and you can also enter the same key over and over very quickly. Performance is dramatically improved. All characters are also in arrays at the top, so you can find them and change them easily. There are independent arrays for every modifier. The code is also much shorter.
The only downside to this new approach is that it uses more dynamic memory—though it uses significantly less program space. On an Arduino Uno, it now uses: 3532 bytes (10%) of program storage space and 605 bytes (29%) of dynamic memory.
As an added bonus, this code works just as well on fast microcontrollers like the ARM Cortex-M4. The interval timer to check the keyboard is in microseconds, so it will perform the same on any board. You can also easily adjust how often the keyboard is checked. By default, it runs one loop every 500 microseconds. It takes 8 loops to check the keyboard, for a total of 4000 microseconds (4 milliseconds, or 250 times per second)—though it may take longer if the micro isn't fast enough to run the code that quickly.
Comments