Building an ADC Keyboard on ESP32
In this workshop, we are going to implement a production level ADC keyboard using ESP32, and native development kit - ESP IDF. Some knowledge can be transfered to Arduino, but this code works right away on ESP IDF 5 projects.
Workshop video
You can watch also the video, which I have explained a lot of details and code here:
What is AD Keyboard?
Ad keyboard is a 3 wire (GND, Input, Output) keypad system, often controlling 2 or more keys using the same wire. The promise is simple, by placing resistors on the way of current, we make different voltages upon pressing any button. Hence, by reading an analog gpio on a microcontroller, we can understand which key is pressed. This is interesting, because allows you to consume single gpio for many keys, vs digital input that requires 1 pin per button.
Challenges
During this experiment, I've realised few things:
- By default the keyboard I have is giving no resistence, so "unpressed" state is equal to 3.3v
- The keybaord I've got, makes a 0 value on left press. This is not perfect, because if minimal starting point was 300 (on 12Bit measurement, range is 0-4096), I could detect 0 as no physical keyboard connected.
- You need a lot of code, to simulate long press, also preventing duplicate press events.
Overall the workshop is simple. We are connecting an ESP dev board to AD Keyboard.
Download the workshop
esp32-adc-keyboard.zipDependencies
You need to have such dependencies at least in your CMakeLists.txt:
idf_component_register(SRCS "esp32-adc-keyboard.c"
INCLUDE_DIRS "."
REQUIRES driver esp_common esp_adc
)
Your main code:
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "./adc-keyboard.c"
enum adc_keys {
KEY_UP = 1,
KEY_DOWN,
KEY_LEFT,
KEY_RIGHT,
KEY_OK
};
void adc_keyboard_task(void *arg)
{
adc_keyboard_config_t cfg = {
.adc_channel = ADC_CHANNEL_7,
.gpio_pin = GPIO_NUM_35,
.log_topic = "MAIN_KEYBOARD"
};
setup_adc_keyboard(&cfg);
void key_callback(int key) {
ESP_LOGI("MAIN_KEYBOARD", "Key pressed inline: %d", key);
}
adc_key_range_t ranges[] = {
{0, 100, KEY_LEFT},
{101, 500, KEY_UP},
{501, 1250, KEY_DOWN},
{1251, 2000, KEY_RIGHT},
{2001, 3200, KEY_OK},
};
ad_keyboard_event_listener_cycle(&cfg, ranges, sizeof(ranges)/sizeof(ranges[0]), key_callback);
}
void app_main(void)
{
xTaskCreatePinnedToCore(adc_keyboard_task, "adc_keyboard_task", 2048, NULL, 5, NULL, 1);
}
Library code.
Place this file alongside your project, it's kinda a library, for connecting AD Keyboard. I did not plan to publish it, it's easier to just put the file per project and modify if needed, than managing a library. Place this next to your main c file in esp idf.
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "driver/adc.h"
#define KEY_NONE 0
// --- Config struct ---
typedef struct {
int adc_channel;
int gpio_pin;
adc_bits_width_t bitwidth;
adc_atten_t atten;
int adc_unit;
uint32_t default_vref;
int wait_cycles_long_press;
int long_press_emit_wait;
int sample_count;
const char* log_topic;
} adc_keyboard_config_t;
// --- Key range struct ---
typedef struct {
int min_mv;
int max_mv;
int key;
} adc_key_range_t;
// --- Callback type ---
typedef void (*key_event_cb_t)(int key);
// Default configuration
static const adc_keyboard_config_t default_adc_keyboard_cfg = {
.bitwidth = ADC_BITWIDTH_12,
.atten = ADC_ATTEN_DB_11,
.default_vref = 1100,
.adc_unit = ADC_UNIT_1,
.wait_cycles_long_press = 35,
.long_press_emit_wait = 200,
.sample_count = 10,
.log_topic = "ADC_KEYBOARD"
};
// Merge user config with defaults
void merge_adc_keyboard_config(adc_keyboard_config_t* cfg) {
if (cfg->bitwidth == 0) cfg->bitwidth = default_adc_keyboard_cfg.bitwidth;
if (cfg->atten == 0) cfg->atten = default_adc_keyboard_cfg.atten;
if (cfg->default_vref == 0) cfg->default_vref = default_adc_keyboard_cfg.default_vref;
if (cfg->wait_cycles_long_press == 0) cfg->wait_cycles_long_press = default_adc_keyboard_cfg.wait_cycles_long_press;
if (cfg->long_press_emit_wait == 0) cfg->long_press_emit_wait = default_adc_keyboard_cfg.long_press_emit_wait;
if (cfg->sample_count == 0) cfg->sample_count = default_adc_keyboard_cfg.sample_count;
if (cfg->log_topic == NULL) cfg->log_topic = default_adc_keyboard_cfg.log_topic;
}
// --- Detect key from voltage using ranges ---
int detect_key(int mv, const adc_key_range_t* ranges, int ranges_count) {
for (int i = 0; i < ranges_count; i++) {
if (mv >= ranges[i].min_mv && mv <= ranges[i].max_mv) {
return ranges[i].key;
}
}
return KEY_NONE;
}
// --- Capture current pressed key ---
int capture_current_pressed(const adc_keyboard_config_t* cfg, const adc_key_range_t* ranges, int ranges_count) {
int sum = 0;
for (int i = 0; i < cfg->sample_count; i++) {
sum += adc1_get_raw(cfg->adc_channel);
}
int avg_raw = sum / cfg->sample_count;
return detect_key(avg_raw, ranges, ranges_count);
}
// --- Setup GPIO and ADC ---
void setup_adc_keyboard(const adc_keyboard_config_t* cfg) {
merge_adc_keyboard_config(cfg);
gpio_set_direction(cfg->gpio_pin, GPIO_MODE_DISABLE);
gpio_set_pull_mode(cfg->gpio_pin, GPIO_FLOATING);
adc1_config_width(cfg->bitwidth);
adc1_config_channel_atten(cfg->adc_channel, cfg->atten);
vTaskDelay(pdMS_TO_TICKS(500));
}
// --- Main listener loop ---
void ad_keyboard_event_listener_cycle(
const adc_keyboard_config_t* cfg,
const adc_key_range_t* ranges,
int ranges_count,
key_event_cb_t callback
) {
int last_key = KEY_NONE;
int kept_same_key_cycles = 0;
ESP_LOGI(cfg->log_topic, "Starting ADC keyboard listener");
while (1) {
int current_key = capture_current_pressed(cfg, ranges, ranges_count);
// --- Same key held ---
if (last_key == current_key) {
if (current_key != KEY_NONE) {
if (kept_same_key_cycles > cfg->wait_cycles_long_press) {
ESP_LOGI(cfg->log_topic, "[Long press] Key %d", current_key);
callback(current_key);
vTaskDelay(pdMS_TO_TICKS(cfg->long_press_emit_wait));
}
kept_same_key_cycles++;
} else {
kept_same_key_cycles = 0;
}
}
// --- Key state changed ---
else {
kept_same_key_cycles = 0;
if (current_key == KEY_NONE) {
ESP_LOGI(cfg->log_topic, "Key released");
} else if (last_key == KEY_NONE) {
ESP_LOGI(cfg->log_topic, "[Press] Key %d", current_key);
callback(current_key);
}
}
last_key = current_key;
vTaskDelay(pdMS_TO_TICKS(30));
}
}