This is a simple PCB keyboard that you can easily use for your Arduino projects.

I’m currently working on a project that will have an integrated keyboard, and I’m running into a problem: how do I include a keyboard in my board prototype? I can’t use a USB keyboard or an existing Arduino based keyboard because the keyboard in the actual project is directly connected to the microcontroller that handles all the other functions. So I designed this basic PCB-based 64-key prototype keyboard matrix.

This PCB does not contain any ICs. 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’s great for prototyping projects that include an integrated keyboard.

The project needs to contain detailed, heavily commented code to make it work with any Arduino compatible board with enough I/O pins available – 11 pins are required. The keyboard has 64 keys, including shift, caps, ctrl, alt, fn, and modifiers for “special”. There are also six additional keys for customizing anything you like. The function of each key can be defined individually, including the function of each key when the modifier is activated. In my opinion, this is more useful than the existing keyboard code, which severely limits your ability to customize the behavior of the keys.

The provided code will print the text to Serial. You can easily change this setting if you want the text to go elsewhere.

A note on program size:

The project code is very large because it doesn’t use any existing libraries. I wrote this code completely from scratch to achieve the customizability I need. On the Arduino UNO, this will use 9100 bytes (28%) of program memory and 394 bytes (19%) of dynamic memory for global variables.

My code could probably be more efficient, and the library and sketch for the keyboard would definitely be smaller, but this is the only way I can design that gives complete flexibility for each key of each modifier. It also takes into account real-world keyboard usage. For example, my code with Caps Lock enabled and pressing the Shift key produces lowercase letters as it should. By default, pressing ESC while holding down the FN key does nothing. But this behavior is fully customizable, so you can change it at will.

Required:

Custom PCB

6x6x5mm Tactile Momentary Buttons (x64)

1N4148 Switching Diode (x64)

1×8 pin headers, female or male (x2)

74HC595 shift register

jumper

Breadboard

Arduino Uno or any Arduino compatible microcontroller development board

Step 1: How the Keyboard Matrix Works

Why do I need a keyboard matrix?

This keyboard has 64 keys. If you were to connect each of these buttons directly to your board, you would need 64 I/O pins. That’s a lot of pins, more than available on most development boards. To bring it down to a more reasonable number, we can use a keyboard matrix, which only requires the number of pins equal to the square root of the number of keys (rounded up).

The keyboard matrix is ​​set up so that every keyswitch in a row is connected, and every keyswitch 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 the key in that column and row 1 has been pressed. Then we deactivate row 1 and activate row 2, and check all columns again. After all rows have been activated, we simply start over from the first row.

How to scan the keyboard matrix:

Since we are using a microcontroller, “active” means setting the line to LOW or HIGH. In this case, we set the row low because we are using the microcontroller’s built-in pull-up resistors on the column input pins. Without pull-up or pull-down resistors, the input pins would react unpredictably due to the interface, which would register false button presses.

The ATmega328P microcontroller used in the Arduino UNO does not have any built-in pull-down resistors, only pull-up resistors. So we are using these. Pull-up resistors connect each input pin to 5V, ensuring they always read high until the button is pressed.

All rows are also normally set to HIGH, which prevents column pins from being connected to row pins, whether the button is pressed or not. But when we’re ready to check a row, we can set that row to LOW. If the button in that row is pressed, this will provide a path for the input pin to be pulled to ground – causing the column to now read LOW.

So, in summary: we set a row to LOW, then check which column pins are now reading LOW. These correspond to pressed buttons. This process happens very fast, so we can scan the entire keyboard many times per second. My code limits it to 200 per second to balance performance, bouncing and making sure every keystroke is captured.

Diodes, ghosting, and n-key rollover:

Diodes in the circuit prevent accidental key presses when certain button combinations are held down. Diodes only allow current to flow in one direction, preventing ghosting. If we didn’t use diodes, pressing some key could cause another key that wasn’t pressed to be registered, because current flows through the adjacent switch. This is shown in a simplified graph, where pressing any three adjacent keys causes the key in the fourth corner to be registered, even if it is not pressed. The diodes prevent this and enable “n-key rollover”, which means we can press as many keys as we want with any combination we want without any issues.

Save pins with a shift register:

Astute you may have noticed that I said that the number of pins required for the keyboard matrix is ​​equal to the square root of the number of keys, but I also said that my keyboard design only requires 11 pins. Should be 16, right? No, because we are using a 74HC595 shift register. This shift register lets us control up to eight output pins using only three I/O pins of the Arduino. These three pins let us send a byte (eight bits) to the shift register, which sets its eight output pins high or low. By using shift registers for the output row pins, we save 5 complete I/O pins!

“So why not use shift registers for the input pins as well?” you ask. The simplest answer is that the input requires a different type of shift register, and I don’t have that at hand. But using shift registers for input also complicates the way we read columns and can cause noise and “bouncing” issues. I just want to say that I don’t need to bear this headache in this case.

Step 2: PCB Design

Schematic Design

Now that you understand how the keyboard matrix works, my PCB design should be simple. I designed the PCB in KiCAD and started with the schematic. I just placed a button symbol and a diode symbol and copied and pasted them until I had a grid of 64 keys. Then I added two 1×8 pin symbols, one for the row and one for the column. One side of the button is connected in columns and the other side of the button is connected in rows.

The next step is to assign a PCB footprint to each schematic symbol. The footprint library included with KiCAD has the necessary footprints built in. When you design your own PCB, you have to be very careful to choose the right package, because these actually end up on your PCB. There are many components with very similar footprints, but slightly different spacing or otherwise. Make sure to choose the component that matches your actual component.

Package and Pin Numbering

Pay special attention to the pin numbers. KiCAD has a weird issue where the schematic diode symbol pin numbers don’t match the package pin numbers. This caused the diodes to be reversed, which was a serious problem considering their polarity and had to create a custom diode package and swap the pin numbers.

PCB layout

After completing the schematic and assigning the footprints, I proceeded to the actual PCB layout. The board outline was created in Autodesk Fusion 360, exported as DXF, and imported into KiCAD on the Edge Cuts layer. Most of the work after that is simply arranging the buttons so that they are laid out like a normal keyboard.

PCB manufacturing

After designing the board, I simply drew all the layers and added them to a zip folder. The folder is available here and can be uploaded directly to PCB manufacturing services such as JLCPCB.

Step 3: PCB Assembly

This is the easiest but most tedious step in the whole project. Just solder all the components in place. They are all through-hole components and are easy to solder. Pay special attention to the orientation of the diodes. The marking on the diode should match the marking on the PCB.

In my experience, the easiest way is to use a third hand to hold the PCB in place and put all the diodes in first. Then flip the board over and solder them all, then clip the leads. Then place all the buttons and solder them. Then solder the pin headers in place. You can use female or male headers, it’s entirely up to you. If you use male headers and then put them under the board, the spacing is correct and they can be glued directly to the breadboard.

Step 4: Connect the Keyboard to Your Arduino

The wiring looks complicated, but when you pay attention to the details, it’s actually not that bad.

Eight jumpers will go directly from the column headers to the following Arduino pins:

1st column 》 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 the breadboard, across the middle rest. Note the orientation of the chip, the dot indicates pin 1.

Check the wiring diagram for the location of the 5V and ground connections. The shift register has two pins connected to 5V and two pins 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 reason, the output pins of the shift register are arranged in a counter-intuitive way. When connecting the shift register to the row pins, pay special attention to the shift register pin diagram. they are:

Line 1 > Shift (Q0) 15

Line 2 > Shift (Q1) 1

Line 3 > Shift (Q2) 2

Line 4 > Shift (Q3) 3

Line 5 > Shift (Q4) 4

Line 6 > Shift (Q5) 5

Shift 7 》 Shift (Q6) 6

Shift 8 》 Shift (Q7) 7

Nothing is connected to the Arduino 0 or 1 pins as those are also used for the serial port and cause conflicts.

Step 5: Flash the Arduino Code

There’s nothing special about this step, just upload the code like any other Arduino project.

Everything in the code is well commented for you to read, so I won’t go into details here. Basically, pins are set up as inputs and outputs. The main loop contains only one timer function. Every 5ms, it calls the function to scan the keyboard. Before checking each column, the function calls a separate function to set up the shift register. The pressed key prints its value to the serial.

If you want to change what is printed when you press a key, just change Serial.print(“_”); in the if statement corresponding to the condition. For example, you can set what is printed when you hold down FN and press N. The same goes for every other key with every modifier.

Many keys do nothing at all in this code because it just prints to the serial. This means that the backspace key doesn’t work because you can’t delete from the serial monitor that data has already been received. However, feel free to use changes if you wish.

Use the keyboard in a project

Printing to the serial port is fine, but that’s not really what this keyboard is about. The purpose of this keyboard is to prototype more complex projects. That’s why it’s easy to change the function. For example, if you want to print the typed text to the OLED screen, you can simply replace each Serial.print( with display.print (or whatever your specific display needs. The Arduino IDE’s Replace All tool is great for Replace all of these in one step.

ProtoKeyboardV1-Bits.ino:

/*    Sketch for Prototyping Keyboard V1.2
 *    by Cameron Coward 1/30/21
 *    
 *    Tested on Arduino Uno. Requires custom PCB
 *    and a 74HC595 shift register.
 *    
 *    More info: https://www.hackster.io/cameroncoward/64-key-prototyping-keyboard-matrix-for-arduino-4c9531
 */

const int rowData = 2; // shift register Data pin for rows
const int rowLatch = 3; // shift register Latch pin for rows
const int rowClock = 4; // shift register Clock pin for rows

// these are our column input pins. Pin 0 and Pin 1 are not used,
// because they cause issues (presumably because they’re TX and RX)
const int colA = A0; 
const int colB = A1;
const int colC = A2; 
const int colD = A3;
const int colE = A4;
const int colF = A5;
const int colG = 5;
const int colH = 6;

// shiftRow is the required shift register byte for each row, rowState will contain pressed keys for each row
const byte shiftRow[] = {B01111111, B10111111, B11011111, B11101111, B11110111, B11111011, B11111101, B11111110};
byte rowState[] = {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000};
byte prevRowState[] = {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000};

// ASCII codes for keys with no modifiers pressed. Modifiers are NULL (0),
// because we will check those separately and their values should not be printed.
const char key[] = {
  0, 49, 50, 51, 52, 53, 54, 55,
  56, 57, 48, 45, 61, 0, 9, 113,
  119, 101, 114, 116, 121, 117, 105, 111,
  112, 91, 93, 92, 7, 97, 115, 100,
  102, 103, 104, 106, 107, 108, 59, 39,
  0, 0, 122, 120, 99, 118, 98, 110,
  109, 44, 46, 47, 0, 0, 0, 0,
  32, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with shift pressed AND caps is acTIve
const char capsShiftKey[] = {
  0, 33, 64, 35, 36, 37, 94, 38,
  42, 40, 41, 95, 43, 0, 9, 113,
  119, 101, 114, 116, 121, 117, 105, 111,
  112, 123, 125, 124, 7, 97, 115, 100,
  102, 103, 104, 106, 107, 108, 58, 22,
  0, 0, 122, 120, 99, 118, 98, 110,
  109, 44, 46, 47, 0, 0, 0, 0,
  32, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with shift pressed.
const char shiftKey[] = {
  0, 33, 64, 35, 36, 37, 94, 38,
  42, 40, 41, 95, 43, 0, 9, 81,
  87, 69, 82, 84, 89, 85, 73, 79,
  80, 123, 125, 124, 7, 65, 83, 68,
  70, 71, 72, 74, 75, 76, 58, 22,
  0, 0, 90, 88, 67, 86, 66, 78,
  77, 44, 46, 47, 0, 0, 0, 0,
  32, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with ctrl pressed.
const char ctrlKey[] = {
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 9, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  32, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with spcl pressed.
const char spclKey[] = {
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with alt pressed.
const char altKey[] = {
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with fn pressed.
const char fnKey[] = {
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0
};

// ASCII codes for keys with caps is acTIve
const char capsKey[] = {
  0, 49, 50, 51, 52, 53, 54, 55,
  56, 57, 48, 45, 61, 0, 9, 81,
  87, 69, 82, 84, 89, 85, 73, 79,
  80, 91, 93, 92, 7, 65, 83, 68,
  70, 71, 72, 74, 75, 76, 59, 39,
  0, 0, 90, 88, 67, 86, 66, 78,
  77, 44, 46, 47, 0, 0, 0, 0,
  32, 0, 0, 0, 0, 0, 0, 0
};

long previousKeyboardMicros = 0;        // will store last TIme keyboard was checked
 
// the follow variables is a long because the TIme, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.
long keyboardInterval = 500;           // interval at which to check keyboard (microseconds)

int rowToCheck = 0; // We check one row per loop of checkKeyboard(), this combined with keyboardInterval 
                    // gives the shiftRegister time to fully update between row checks

bool caps = false;  // is caps lock on?
bool shift = false; // is either left or right shift pressed?
bool capsShift = false; // are shift AND caps active?
bool ctrl = false; // is the ctrl key pressed?
bool spcl = false;  // is the spcl key pressed?
bool alt = false; // is the alt key pressed?
bool fn = false;  // is the function key pressed?

void setup() {
  Serial.begin(9600);

  // setup all column pin as inputs with internal pullup resistors
  pinMode(colA, INPUT_PULLUP); 
pinMode(colB, INPUT_PULLUP);
  pinMode(colC, INPUT_PULLUP);
  pinMode(colD, INPUT_PULLUP);
  pinMode(colE, INPUT_PULLUP); 
  pinMode(colF, INPUT_PULLUP);
  pinMode(colG, INPUT_PULLUP);
pinMode ( colH , INPUT_PULLUP ) ;

  // the outputs needed to control the 74HC595 shift register
  pinMode(rowLatch, OUTPUT);
  pinMode(rowClock, OUTPUT);
  pinMode(rowData, OUTPUT);

  updateShiftRegister(B11111111); // make sure shift register starts at all HIGH
}

void loop() {
 mainTimer();
}

void mainTimer() {

  unsigned long currentMicros = micros(); // how many microseconds has the Arduino been running?
  
  if(currentMicros – previousKeyboardMicros > keyboardInterval) { // if elapsed time since last check exceeds the interval
    // save the last time the keyboard was checked
    previousKeyboardMicros = currentMicros;   
 
    checkKeyboard(); // check all of the keys and print out the results to serial
  }
}

void updateShiftRegister(byte row) {
  //this function sets the shift register according to the byte that was passed to it
 
  digitalWrite(rowLatch, LOW); // set latch to low so we can write an entire byte at once
  shiftOut(rowData, rowClock, MSBFIRST, row);  // write that byte
  digitalWrite(rowLatch, HIGH); // set latch back to high so it shift register will remain stable until next change
}

void checkKeyboard() {

  // set the shift register to the current row’s byte value, from the shiftRow[] byte array
  updateShiftRegister(shiftRow[rowToCheck]);

  // Check each column
  if (digitalRead(colA) == LOW) {
    bitSet(rowState[rowToCheck], 0);
  } else {
    bitClear(rowState[rowToCheck], 0);
  }
  
  if (digitalRead(colB) == LOW) {
    bitSet(rowState[rowToCheck], 1);
  } else {
    bitClear(rowState[rowToCheck], 1);
  }
  
  if (digitalRead(colC) == LOW) {
    bitSet(rowState[rowToCheck], 2);
  } else {
    bitClear(rowState[rowToCheck], 2);
  }
  
  if (digitalRead(colD) == LOW) {
    bitSet(rowState[rowToCheck], 3);
  } else {
    bitClear(rowState[rowToCheck], 3);
  }
  
  if (digitalRead(colE) == LOW) {
    bitSet(rowState[rowToCheck], 4);
  } else {
    bitClear(rowState[rowToCheck], 4);
  }
  
  if (digitalRead(colF) == LOW) {
    bitSet(rowState[rowToCheck], 5);
  } else {
    bitClear(rowState[rowToCheck], 5);
  }
  
  if (digitalRead(colG) == LOW) {
    bitSet(rowState[rowToCheck], 6);
  } else {
    bitClear(rowState[rowToCheck], 6);
  }
  
  if (digitalRead(colH) == LOW) {
    bitSet(rowState[rowToCheck], 7);
  } else {
    bitClear(rowState[rowToCheck], 7);
  }

  // set all shift register pins to HIGH, this keeps values from “bleeding” over to the next loop
  updateShiftRegister(B11111111);

rowToCheck = rowToCheck + 1; // iterate to next row

  // after checking the 8th row, check the states (button presses) and then start over on the 1st row
  if (rowToCheck > 7 ) {
    checkPressedKeys();
    rowToCheck = 0;
  }
}

void checkPressedKeys() {
  // check if either shift key is pressed
  if (bitRead(rowState[5], 1) | bitRead(rowState[6], 4)) {
    shift = true;
  } else {
shift = false;
  }

  // check if either ctrl key is pressed
  if (bitRead(rowState[6], 5) | bitRead(rowState[7], 3)) {
    ctrl = true;
  } else {
    ctrl = false;
  }

  // check if either spcl key is pressed
  if (bitRead(rowState[6], 6) | bitRead(rowState[7], 2)) {
    spcl = true;
  } else {
spcl = false;
  }

  // check if either alt key is pressed
  if (bitRead(rowState[6], 7) | bitRead(rowState[7], 1)) {
    alt = true;
  } else {
    alt = false;
  }

  // check if FN key is pressed
  if (bitRead(rowState[7], 4)) {
    fn = true;
  } else {
    fn = false;
  }

  // check caps is active and shift is pressed
  if (shift == true && caps == true) {
    capsShift = true;
  } else {
    capsShift = false;
  }
  
  for (int i = 8; i >= 0; i–) {                    // iterate through each row
    for (int j = 7; j >= 0; j–) {                  // iterate through each bit in that row
      
      bool newBit = bitRead(rowState[i], j);             // check the state of that bit
      bool prevBit = bitRead(prevRowState[i], j);         // check the previous state of that bit
      
      if ((newBit == 1) && (prevBit == 0)) {                       // only allows button press if state has changed to true
          int thisChar = (i * 8) + j;               // calculate which position in char array to select

          if (capsShift == true) {
            processKey(capsShiftKey[thisChar]);
          } else if (shift == true) {
            processKey(shiftKey[thisChar]);
          } else if (ctrl == true) {
            processKey(ctrlKey[thisChar]);
          } else if (alt == true) {
            processKey(altKey[thisChar]);
          } else if (spcl == true) {
            processKey(spclKey[thisChar]);
          } else if (fn == true) {
            processKey(fnKey[thisChar]);
          } else if (caps == true) {
            processKey(capsKey[thisChar]);
          } else {
            processKey(key[thisChar]);     
          }
      }
      
      if (newBit == 1) {
          bitSet(prevRowState[i], j);     // set previous bit state to true if a key is pressed
      } else {
          bitClear(prevRowState[i], j);   // set previous bit state to false if key isn’t pressed, so it can be pressed again
      }
    }
  }
}

void processKey(char receivedKey) {
  if (receivedKey == 7) {                 // check for special functions in the same way as caps (add new “else if” statements)
    caps = !caps;
  } else {
    Serial.print(receivedKey);            // if char does not correspond to a special function, simply print that char
  }
}

Leave a Reply

Your email address will not be published.