Binary-operated Button Matrix Keypad

by bevopsmoment in Circuits > Arduino

53 Views, 0 Favorites, 0 Comments

Binary-operated Button Matrix Keypad

WhatsApp Image 2026-02-15 at 00.36.09.jpeg

So, I wanted to create a soundboard with an ESP32-C3 supermini chip. For that, obviously, you need some way to send data to the chip, and what better way to do it than a keypad. Essentially, this is a button matrix that you press on to get a number.

How does it work?

Well, the dev board iterates through all the OUTPUT pins associated with the columns of the matrix and sends a digital "ON" signal through each of them in turn. In the meantime, another set of INPUT pins associated with the rows perform digital read. When a row pin detects an "ON" signal, it means a button was pressed.

Based on the row that was read, and the current activated column, you can calculate the correct button pressed.

There are several keypad libraries. The first search I got for "esp32 keypad library" was this website.

It works great, the problem is, they use an entire pin for each row and column of the keypad! It might be fine when you use a regular ESP32, and you have like 30+ available GPIOs to choose from, but if you are working with a tiny ESP32-C3 supermini, that only has 12 pins including strapping pins, you need to think about how to downsize your pin count.


My solution?

Encoding the powered column and the active row with binary numbers!

In my project, I managed to save 3 pins and ended up using only 5, which overall gave me exactly the amount I needed for the soundboard (I had to skip GPIO 2, because it was always on and I could not figure out how to use it).

Supplies

  1. ESP32-C3 supermini.
  2. Could work with any reasonable board. I used this one because it's the smallest I could fit for my project.
  3. 1 Keypad.
  4. I used a 4x4 keypad. This is a nice size as it offers enough buttons for usage and for demonstrating the mechanism of how this binary encoding works without being too complicated.
  5. 1 NOT gate (74LS04).
  6. 1 AND gate (74LS08).
  7. 1 OR gate (74LS32).
  8. 4 10K Pull-Down resistors.
  9. For the OR gate. ROW4 should also have a pull-down resistor, I don't know why as it is not connected to the OR gate, but it didn't work properly without it.

Making Plans

Firstly, how many pins do we ACTUALLY need?

For the columns, you need to power each of them in sequence, so the amount of states is equal to the amount of columns. Therefore, you need to use log_2(COLUMN amount), rounded up.


For the rows, each row represents a state, but unlike the columns, there should be a neutral, UNPRESSED state. Therefore, you need to use log_2(ROW amount + 1), rounded up.

For my example, there are ceil(log_2(4)) = 2 column pins, and ceil(log_2(4+1)) = 3 rows.


Now, how to translate a position from a binary number and vice versa?


COLS (Binary to Position):

The columns are easier, so we'll start with them.

We need to translate a 2-bit number to a 1-in-4 choice, so let's define that

00 activates column 1, 01 activates columns 2, 10 activates column 3, and 11 activates column 4.

Therefore:

  1. activate column 1 if NOT bit#1 AND NOT bit#2
  2. activate column 2 if bit#1 AND NOT bit#2
  3. activate column 3 if NOT bit#1 AND bit#2
  4. activate column 4 if bit#1 AND bit#2.


ROWS (Position to Binary):

Here, we need to encode 5 states into a 3 bit number. We will just calculate the number in binary and send it to the esp32. The numbers are:

  1. 0->000
  2. 1->001
  3. 2->010
  4. 3->011
  5. 4->100.

So, we can see that bit#1 is ON if row#1 OR row#3 are ON, bit#2 is ON if row#2 OR row#3, and bit#3 is just equal to row#4.

Wire Up

SCH_Schematic1_1-Logic Gate Layout_2026-02-14(1).png

Translating the calculations into actual wiring according to the schematic.

Code

Let's break the code to three sections.

We'll start with the constants and variables:

// Definitions


// Constants

const int MASK1 = 1; // Mask for decoding what column to power - bit#1.
const int MASK2 = 2; // Mask for decoding what column to power - bit#2

const int TICKLENGTH = 20; // Length of system power tick (in mili-seconds).
const int COOLOFF = 200; // Amount of ticks before a new press could be interpreted.
const int NOTPRESSED = -1; // Value denoting that no key was detected.
const bool DEBUGGING = true; // Printing debugging messages to Serial.

const uint8_t COLS_NUM = 2; // Amount of column pins = log2(Actual amount of used columns in keypad), rounded up.
const uint8_t ROWS_NUM = 3; // Amount of row pins.
const int COLS[COLS_NUM] = {8, 9}; // Define the column pins.
const int ROWS[ROWS_NUM] = {21, 20, 10}; // Define the row pins, most significant to the left.
const char KEYPAD_ARR[16] = {"0", "1", "2", "3",
"4", "5", "6", "7",
"8", "9", "A", "B",
"C", "D", "E", "F"}; // The values when pressing the matching key.
// Global variables
int currentColumn = 0; // Storing the current active column.
bool PRESSED = false; // Is a button pressed?
int pressedTimer = 0; // OPTIONAL cooldown timer cycle counter.


There are only two functions, one to power the columns, and another to read the rows:


void powerCols(int currentColumn){
int STATE[COLS_NUM]; // The current state of the i-th bit of the column binary number.
if(0 <= currentColumn && currentColumn <= 3){
// Translate the column state to the 2-bit number.
STATE[0] = (currentColumn & MASK1);
STATE[1] = (currentColumn & MASK2);
}else{
Serial.println("Current column out of bounds");
return;
}
// Write to the board
for(int i = 0; i<COLS_NUM; i++){
digitalWrite(COLS[i], STATE[i]);
}

}

int readRows(){
int totalNum=0; // The row state based on the 3-bit number read.
for(int i = 0; i<ROWS_NUM; i++){
totalNum *= 2; // Bit shift (times the number by two)
int digRead = digitalRead(ROWS[i]); // Read the i-th bit.
if(DEBUGGING){
Serial.print("Checking pin #");
Serial.print(ROWS[i]);
Serial.print(" in row #");
Serial.print(i+1);
Serial.print(" read ");
Serial.println(digRead);
}
totalNum += digRead;
}
Serial.println(totalNum); // OPTIONAL Print the row number calculated.
return (totalNum - 1); // No row -> -1, Row 1 -> 0, Row 2 -> 1 etc.
}


Finally, the setup and loop functions:

void setup() {
// put your setup code here, to run once:
Serial.begin(115200);

Serial.println("Starting!");
//Initializing columns
for(int i = 0; i<COLS_NUM; i++){
pinMode(COLS[i], OUTPUT);
}
//Initializing rows
for(int i = 0; i<ROWS_NUM; i++){
pinMode(ROWS[i], INPUT);
}
}

void loop() {

//Power columns
powerCols(currentColumn);

//Read from rows
if(!PRESSED){
int currentRow = readRows();
if(DEBUGGING){
Serial.print("Current Row: ");
Serial.println(currentRow);
Serial.println("----------\n");
}

//If match found
if(currentRow != NOTPRESSED){
if(DEBUGGING){
Serial.print("Found match on row #");
Serial.print(currentRow);
Serial.print(" and column #");
Serial.println(currentColumn);
Serial.print("Value of key is: ");
Serial.println(currentRow*4 + currentColumn + 1);
}
char value = KEYPAD_ARR[currentRow*4 + currentColumn]; // Get the value from the keypad.
PRESSED = true;
delay(200);
}
}else{
//If pressed
pressedTimer = pressedTimer + 1;

//if timer is over cooloff limit
if(pressedTimer > COOLOFF){
pressedTimer=0;
PRESSED = false;
}
}

currentColumn = (currentColumn + 1) % 4;
delay(TICKLENGTH);
}