0 - Introduction
In a previous article we learnt how to use a keypad with the Esp32 using the Arduino libraries, in this one, we will go a step lower and use a Arduino Nano with no Arduino libraries. A perfect fit for our ‘Baremetal’ series.
For this article I will use a 4 by 4 keyboard (like this one), that way you have access to 16 buttons in only 8 pins!
Besides the keyboard, you will also need an Arduino (like this one) and at least 8 wires. Besides the hardware, you will also need the uart library we made in a previous article (uart.c and uart.h).
1 - Code
Let’s start by including the header files we will use and then defining the pins and pin counts. The code will use the
#include <avr/io.h>
#include <avr/interrupt.h>
#include "uart.h"
#define KEYBOARD_ROW_FIRSTPIN PD2
#define KEYBOARD_ROW_COUNT 4
#define KEYBOARD_COL_FIRSTPIN PC0
#define KEYBOARD_COL_COUNT 4
After that, we can declare the variable we will be using, 3 bidimensional arrays, one to store the characters, one for storing the current state of the buttons and one to store the last state. We will use the last state to know if a button was pressed, released or is being held:
// keyboard chars
uint8_t btn_chars[KEYBOARD_ROW_COUNT][KEYBOARD_COL_COUNT] =
{
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
// current keyboard state
uint8_t btn_state[KEYBOARD_ROW_COUNT][KEYBOARD_COL_COUNT] = {{0}};
// last keyboard state
uint8_t btn_laststate[KEYBOARD_ROW_COUNT][KEYBOARD_COL_COUNT] = {{0}};
Now, to initialize the keyboard pins, you set the first 4 (or 3) to output and the other 4 (or 3) to input. The output pins will need to be set as High and the input pins will need to be set as pullup:
void keyboard_init()
{
DDRD |= _BV(KEYBOARD_ROW_FIRSTPIN) | _BV(KEYBOARD_ROW_FIRSTPIN + 1) |
_BV(KEYBOARD_ROW_FIRSTPIN + 2) | _BV(KEYBOARD_ROW_FIRSTPIN + 3); // set as output
PORTD |= _BV(KEYBOARD_ROW_FIRSTPIN) | _BV(KEYBOARD_ROW_FIRSTPIN + 1) |
_BV(KEYBOARD_ROW_FIRSTPIN + 2) | _BV(KEYBOARD_ROW_FIRSTPIN + 3); // set as high
DDRC &= ~(_BV(KEYBOARD_COL_FIRSTPIN) | _BV(KEYBOARD_COL_FIRSTPIN + 1) |
_BV(KEYBOARD_COL_FIRSTPIN + 2) | _BV(KEYBOARD_COL_FIRSTPIN + 2)); // set as input
PORTC |= _BV(KEYBOARD_COL_FIRSTPIN) | _BV(KEYBOARD_COL_FIRSTPIN + 1) |
_BV(KEYBOARD_COL_FIRSTPIN + 2) | _BV(KEYBOARD_COL_FIRSTPIN + 2); // enable pullup
}
To update the state of the keyboard, for every row, set it’s output pin to Low, save the previous state and read the current state of every input pin, if any of them is also low, then you know they are being pressed, set the output pin again, to high and repeat for the other rows:
// update keyboard buttons state
void keyboard_update()
{
for (uint8_t y = 0; y < KEYBOARD_ROW_COUNT; y++) // for each row (output pin)
{
PORTD &= ~(_BV(KEYBOARD_ROW_FIRSTPIN + y)); // set out pin as low
for (uint8_t x = 0; x < KEYBOARD_COL_COUNT; x++) // for each column (input pin)
{
btn_laststate[y][x] = btn_state[y][x]; // save last state
btn_state[y][x] = !(PINC & _BV(KEYBOARD_COL_FIRSTPIN + x)); // read new state (low = btn pressed)
}
PORTD |= (_BV(KEYBOARD_ROW_FIRSTPIN + y)); // set out pin as high
}
}
Finally, on main, initialize the uart, enable global interrupts and initialize the keyboard then, on an infinite loop, update the keyboard state and print any characters that were pressed this ‘frame’:
int main()
{
// init uart
uart_init(115200, 1);
// enable global interrupts
sei();
// init keyboard pins
keyboard_init();
while (1)
{
// update keyboard state
keyboard_update();
// print to uart any that were pressed just now
for (uint8_t y = 0; y < KEYBOARD_ROW_COUNT; y++)
for (uint8_t x = 0; x < KEYBOARD_COL_COUNT; x++)
if (btn_state[y][x] && !btn_laststate[y][x])
uart_sendc(btn_chars[y][x]);
}
return 0;
}
To compile the code, get the makefile we made in a previous article and run ‘make all’. Then to upload it to your arduino run ‘make upload’.
And that’s it, you should now, after uploading the code, be able to press any button on the keyboard and get the matching character in your Serial terminal! You can find the whole code at the bottom of this article.
You can also test this code on the Wokwi simulator.
Thanks for reading and stay tuned for more tech insights and tutorials. Until next time, and keep exploring the world of tech!
#include <avr/io.h>
#include <avr/interrupt.h>
#include "uart.h"
#define KEYBOARD_ROW_FIRSTPIN PD2 // PD2, 3, 4 and 5 on nano it is D2, D3, D4 and D5
#define KEYBOARD_ROW_COUNT 4
#define KEYBOARD_COL_FIRSTPIN PC0 // PC0, 1, 2 and 3 on nano it is A0, A1, A2 and A3
#define KEYBOARD_COL_COUNT 4
// keyboard chars
uint8_t btn_chars[KEYBOARD_ROW_COUNT][KEYBOARD_COL_COUNT] =
{
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
// current keyboard state
uint8_t btn_state[KEYBOARD_ROW_COUNT][KEYBOARD_COL_COUNT] = {{0}};
// last keyboard state
uint8_t btn_laststate[KEYBOARD_ROW_COUNT][KEYBOARD_COL_COUNT] = {{0}};
// initialize the keyboard
void keyboard_init()
{
DDRD |= _BV(KEYBOARD_ROW_FIRSTPIN) | _BV(KEYBOARD_ROW_FIRSTPIN + 1) |
_BV(KEYBOARD_ROW_FIRSTPIN + 2) | _BV(KEYBOARD_ROW_FIRSTPIN + 3); // set as output
PORTD |= _BV(KEYBOARD_ROW_FIRSTPIN) | _BV(KEYBOARD_ROW_FIRSTPIN + 1) |
_BV(KEYBOARD_ROW_FIRSTPIN + 2) | _BV(KEYBOARD_ROW_FIRSTPIN + 3); // set as high
DDRC &= ~(_BV(KEYBOARD_COL_FIRSTPIN) | _BV(KEYBOARD_COL_FIRSTPIN + 1) |
_BV(KEYBOARD_COL_FIRSTPIN + 2) | _BV(KEYBOARD_COL_FIRSTPIN + 2)); // set as input
PORTC |= _BV(KEYBOARD_COL_FIRSTPIN) | _BV(KEYBOARD_COL_FIRSTPIN + 1) |
_BV(KEYBOARD_COL_FIRSTPIN + 2) | _BV(KEYBOARD_COL_FIRSTPIN + 2); // enable pullup
}
void keyboard_update()
{
for (uint8_t y = 0; y < KEYBOARD_ROW_COUNT; y++) // for each row (output pin)
{
PORTD &= ~(_BV(KEYBOARD_ROW_FIRSTPIN + y)); // set out pin as low
for (uint8_t x = 0; x < KEYBOARD_COL_COUNT; x++) // for each column (input pin)
{
btn_laststate[y][x] = btn_state[y][x]; // save last state
btn_state[y][x] = !(PINC & _BV(KEYBOARD_COL_FIRSTPIN + x)); // read new state (low = btn pressed)
}
PORTD |= (_BV(KEYBOARD_ROW_FIRSTPIN + y)); // set out pin as high
}
}
int main()
{
// init uart
uart_init(115200, 1);
// enable global interrupts
sei();
// init keyboard pins
keyboard_init();
while (1)
{
// update keyboard state
keyboard_update();
// print to uart any that were pressed just now
for (uint8_t y = 0; y < KEYBOARD_ROW_COUNT; y++)
for (uint8_t x = 0; x < KEYBOARD_COL_COUNT; x++)
if (btn_state[y][x] && !btn_laststate[y][x])
uart_sendc(btn_chars[y][x]);
}
return 0;
}