Arduino compatible LCD Keypad Shield ADC Reader

An example sketch for lcd keypad shield which acts as a ADC value tester for the buttons and the hardware Voltage Divider implemented with the A0 pin for any Arduino standard LCD Keypad Shield. Wet tested with Arduino UNO and MEGA.

!!ANNOUNCEMENT!!
ADC Reader Advanced is out, which uses a KMR 1.8 TFT to interface along with the Keypad Shield.
Arduino compatible LCD Keypad Shield ADC Reader Advanced

ADC Reader Firmware Source Code:

/******************************************************************************
 * ╔══════════════════════════════════════════════════════════════════════════╗
 * ║                       CORE1D AUTOMATION LABS                             ║
 * ║                      ADC READER FIRMWARE v1.2                            ║
 * ║                                                                          ║
 * ║    Advanced LCD Keypad Shield Interface with Real-time Statistics        ║
 * ╚══════════════════════════════════════════════════════════════════════════╝
 *
 *  ┌─────────────────────────────────────────────────────────────────────────┐
 *  │  DESCRIPTION:                                                           │
 *  │  Real-time ADC monitoring system with LCD display and keypad interface  │
 *  │  Features running statistics (average ± standard deviation) for         │
 *  │  pressed keys with overflow protection and optimized display updates    │
 *  └─────────────────────────────────────────────────────────────────────────┘
 *
 *  ╔═══════════════════════════════════════════════════════════════════════╗
 *  ║                    LCD KEYPAD SHIELD PIN LAYOUT                       ║
 *  ╠═══════════════════════════════════════════════════════════════════════╣
 *  ║                                                                       ║
 *  ║   ┌─────────────────────────────────────────────────────┐             ║
 *  ║   │ ┌─────────────────────────────────────────────────┐ │ LCD DISPLAY ║
 *  ║   │ │  (5x8)x16x2 dots LCD Display | Hitachi HD44780  │ │   MODULE    ║
 *  ║   │ │ ███████████████████████████████████████████████ │ │             ║
 *  ║   │ │ ███████████████████████████████████████████████ │ │             ║
 *  ║   │ └─────────────────────────────────────────────────┘ │             ║
 *  ║   │                                                     │             ║
 *  ║   │  [RIGHT] [UP] [DOWN] [LEFT] [SELECT]                │             ║
 *  ║   └─────────────────────────────────────────────────────┘             ║
 *  ║                            │                                          ║
 *  ║   ┌────────────────────────┼─ SHIELD CONNECTION ────────────────────┐ ║
 *  ║   │                        ▼                                        │ ║
 *  ║   │    ARDUINO MEGA 2560 / UNO DIGITAL PINS                         │ ║
 *  ║   │                                                                 │ ║
 *  ║   │    Pin  4  ────────── LCD Data 4 (D4)                           │ ║
 *  ║   │    Pin  5  ────────── LCD Data 5 (D5)                           │ ║
 *  ║   │    Pin  6  ────────── LCD Data 6 (D6)                           │ ║
 *  ║   │    Pin  7  ────────── LCD Data 7 (D7)                           │ ║
 *  ║   │    Pin  8  ────────── LCD Register Select (RS)                  │ ║
 *  ║   │    Pin  9  ────────── LCD Enable (EN)                           │ ║
 *  ║   │    Pin 10  ────────── LCD Backlight Control (Optional)          │ ║
 *  ║   │                                                                 │ ║
 *  ║   │    ANALOG PINS                                                  │ ║
 *  ║   │    Pin A0  ────────── Keypad Voltage Divider                    │ ║
 *  ║   │                                                                 │ ║
 *  ║   │    POWER CONNECTIONS                                            │ ║  
 *  ║   │    5V      ────────── LCD VCC & Keypad VCC                      │ ║
 *  ║   │    GND     ────────── LCD GND & Keypad GND                      │ ║
 *  ║   └─────────────────────────────────────────────────────────────────┘ ║
 *  ║                                                                       ║
 *  ║   KEYPAD RESISTOR LADDER (Estimated ADC Values):                      ║
 *  ║   ┌─────────┬─────────────┬─────────────────────────────────────────┐ ║
 *  ║   │  BUTTON │ ADC RANGE   │              NOTES                      │ ║
 *  ║   ├─────────┼─────────────┼─────────────────────────────────────────┤ ║
 *  ║   │  RIGHT  │   0 -  80   │ Direct connection to GND                │ ║
 *  ║   │  UP     │  81 - 150   │ 330Ω resistor to GND                    │ ║
 *  ║   │  DOWN   │ 151 - 270   │ 620Ω resistor to GND                    │ ║
 *  ║   │  LEFT   │ 271 - 620   │ 1kΩ resistor to GND                     │ ║
 *  ║   │  SELECT │ 621 - 880   │ 3.3kΩ resistor to GND                   │ ║
 *  ║   │  NONE   │ 881 - 1023  │ 10kΩ pull-up to 5V (no button)          │ ║
 *  ║   └─────────┴─────────────┴─────────────────────────────────────────┘ ║
 *  ╚═══════════════════════════════════════════════════════════════════════╝
 *
 *  ┌─────────────────────────────────────────────────────────────────────────┐
 *  │  FEATURES:                                                              │
 *  │  • Real-time ADC value display (0-1023)                                 │
 *  │  • Active key detection with hysteresis                                 │
 *  │  • Running statistics (Average ± Standard Deviation)                    │
 *  │  • Memory-efficient PROGMEM usage                                       │
 *  │  • Overflow protection for long-duration measurements                   │
 *  │  • Custom characters (Plus-Minus symbol)                                │
 *  │  • Optimized display updates (flicker-free)                             │
 *  │  • Type-writer intro animation                                          │
 *  └─────────────────────────────────────────────────────────────────────────┘
 *
 *  ╔═══════════════════════════════════════════════════════════════════════╗
 *  ║                          ACKNOWLEDGMENTS                              ║
 *  ╠═══════════════════════════════════════════════════════════════════════╣
 *  ║                                                                       ║
 *  ║  Special thanks to:                                                   ║
 *  ║                                                                       ║
 *  ║  • Arduino Team & Community                                           ║
 *  ║    For the excellent Arduino IDE and ecosystem                        ║
 *  ║                                                                       ║
 *  ║  • LiquidCrystal Library Authors                                      ║
 *  ║    Original: David A. Mellis, Limor Fried (Adafruit)                  ║
 *  ║    Maintained by: Arduino Team                                        ║
 *  ║    Library Version: 1.0.7+                                            ║
 *  ║    https://github.com/arduino-libraries/LiquidCrystal                 ║
 *  ║                                                                       ║
 *  ╚═══════════════════════════════════════════════════════════════════════╝
 *
 *  ┌─────────────────────────────────────────────────────────────────────────┐
 *  │  HARDWARE COMPATIBILITY:                                                │
 *  │  • Arduino UNO R3                                                       │
 *  │  • Arduino MEGA 2560                                                    │
 *  │  • Arduino NANO or any other DIP Atmega (with pin mapping)              │
 *  │  • Compatible 16x2 LCD Keypad Shields                                   │
 *  │                                                                         │
 *  │  TESTED ON:                                                             │
 *  │  • Arduino IDE 2.x                                                      │
 *  │  • Hardware Configuration 0 (as calibrated)                             │
 *  └─────────────────────────────────────────────────────────────────────────┘
 *
 *  ╔═══════════════════════════════════════════════════════════════════════╗
 *  ║                             LICENSE                                   ║
 *  ╠═══════════════════════════════════════════════════════════════════════╣
 *  ║                                                                       ║
 *  ║  MIT License                                                          ║
 *  ║                                                                       ║
 *  ║  Copyright (c) 2024 Core1D Auto Labs                                  ║
 *  ║                                                                       ║
 *  ║  Permission is hereby granted, free of charge, to any person          ║
 *  ║  obtaining a copy of this firmware and associated documentation       ║
 *  ║  files (the "Firmware"), to deal in the Firmware without              ║
 *  ║  restriction, including without limitation the rights to use,         ║
 *  ║  copy, modify, merge, publish, distribute, sublicense, and/or sell    ║
 *  ║  copies of the Firmware, and to permit persons to whom the            ║
 *  ║  Firmware is furnished to do so, subject to the following             ║
 *  ║  conditions:                                                          ║
 *  ║                                                                       ║
 *  ║  The above copyright notice and this permission notice shall be       ║
 *  ║  included in all copies or substantial portions of the Firmware.      ║
 *  ║                                                                       ║
 *  ║  THE FIRMWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      ║
 *  ║  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES      ║
 *  ║  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND             ║
 *  ║  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT          ║
 *  ║  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,         ║
 *  ║  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING         ║
 *  ║  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR        ║
 *  ║  OTHER DEALINGS IN THE FIRMWARE.                                      ║
 *  ║                                                                       ║
 *  ╚═══════════════════════════════════════════════════════════════════════╝
 *
 *  ┌─────────────────────────────────────────────────────────────────────────┐
 *  │  VERSION HISTORY:                                                       │
 *  │  v1.0 - Basic ADC reading                                               │
 *  │  v1.1 - Running statistics and custom characters                        │
 *  │  v1.2 - Display optimization                                            │
 *  └─────────────────────────────────────────────────────────────────────────┘
 *
 *  Author: Sir Ronnie from Core1D Automation Labs
 *  Created: 2024
 *  Last Modified: 2024
 *  
 ******************************************************************************/

#include <LiquidCrystal.h>

// Initialize the library with the interface pins
// Standard LCD Keypad Shield connections for Arduino MEGA 2560
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

// Analog pin for keypad reading
#define KEYPAD_PIN A0

// Custom character definitions (stored in PROGMEM)
const byte PLUS_MINUS_CHAR[8] PROGMEM = {
  0b00100,  // ..#..
  0b00100,  // ..#..
  0b11111,  // #####
  0b00100,  // ..#..
  0b00100,  // ..#..
  0b00000,  // .....
  0b11111,  // #####
  0b00000   // .....
};

// Custom character indices
#define CHAR_PLUS_MINUS 0

// Constants stored in flash memory
const char* const KEY_NAMES[] PROGMEM = {
  "RIGHT", "UP", "DOWN", "LEFT", "SELECT", "NONE"
};

const char INTRO_LINE1[] PROGMEM = "Core1D Auto Labs";
const char INTRO_LINE2[] PROGMEM = "ADC Reader v1.2";
const char HEADER_LINE[] PROGMEM = "ADC:     Key:";
const char READY_MSG[] PROGMEM = "Ready...";
const char PRESS_KEY_MSG[] PROGMEM = "Press any key...";
const char AVG_LABEL[] PROGMEM = "AVG:";
const char STDDEV_LABEL[] PROGMEM = " ";
const char SPACES_4[] PROGMEM = "    ";
const char SPACES_7[] PROGMEM = "       ";
const char SPACES_16[] PROGMEM = "                ";

// Key detection thresholds - made more robust with hysteresis
struct KeyThreshold {
  int lower;
  int upper;
  const char* name;
};

const KeyThreshold KEY_THRESHOLDS[] = {                 // Set as per the callibration of Hardware 0. 
  {0, 80, "RIGHT"},      // ~0-50 typical, with margin
  {81, 150, "UP"},       // ~130-160 typical, with margin  
  {151, 270, "DOWN"},    // ~300-330 typical, with margin
  {271, 620, "LEFT"},    // ~480-510 typical, with margin
  {621, 880, "SELECT"},  // ~720-750 typical, with margin
  {881, 1023, "NONE"}    // No key pressed
};

const int NUM_KEYS = sizeof(KEY_THRESHOLDS) / sizeof(KeyThreshold);

// Display variables
int lastAdcValue = -1;
int lastKeyIndex = -1;

// Key press tracking variables
bool keyCurrentlyPressed = false;
unsigned long keyPressStartTime = 0;
unsigned long adcSum = 0;        // Changed to unsigned long to prevent overflow
unsigned long adcSumSquares = 0; // Changed to unsigned long for large sums
unsigned int adcCount = 0;       // Changed to unsigned int (max 65535 samples)
int lastAverage = -1;
int lastStdDev = -1;
unsigned int minAdc = 1023;      // Changed to unsigned int
unsigned int maxAdc = 0;         // Changed to unsigned int

// Overflow protection constants
const unsigned int MAX_SAMPLES = 60000;  // Prevent overflow in calculations
const unsigned long MAX_ADC_SUM = 4000000000UL; // ~4 billion limit

void setup() {
  // Initialize LCD
  lcd.begin(16, 2);
  
  // Create custom characters
  createCustomChars();
  
  // Show intro with simple animation
  showIntro();
  
  // Initialize display
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("ADC:     Key:"));
  lcd.setCursor(0, 1);
  lcd.print(F("Ready..."));
}

void loop() {
  unsigned long currentMillis = millis();
  
  // Read raw ADC value directly - no filtering or correction
  int adcValue = analogRead(KEYPAD_PIN);
  
  int keyIndex = getKeyIndex(adcValue);
  const char* keyName = KEY_THRESHOLDS[keyIndex].name;
  
  // Track key press events
  bool keyPressed_bool = (keyIndex != NUM_KEYS - 1); // Last index is "NONE"
  
  if (keyPressed_bool && !keyCurrentlyPressed) {
    // Key just pressed - start tracking
    keyCurrentlyPressed = true;
    keyPressStartTime = currentMillis;
    
    // Initialize statistics - safe assignments
    adcSum = (unsigned long)adcValue;
    adcSumSquares = (unsigned long)adcValue * (unsigned long)adcValue;
    adcCount = 1;
    minAdc = maxAdc = (unsigned int)adcValue;
    lastAverage = adcValue;
    lastStdDev = 0;
    updateBottomLine(lastAverage, lastStdDev);
    
  } else if (keyPressed_bool && keyCurrentlyPressed) {
    // Key still pressed - update running statistics with overflow protection
    if (adcCount < MAX_SAMPLES && adcSum < MAX_ADC_SUM) {
      adcSum += (unsigned long)adcValue;
      adcSumSquares += (unsigned long)adcValue * (unsigned long)adcValue;
      adcCount++;
      
      // Track min/max safely
      unsigned int currentAdc = (unsigned int)adcValue;
      if (currentAdc < minAdc) minAdc = currentAdc;
      if (currentAdc > maxAdc) maxAdc = currentAdc;
      
      // Calculate new statistics only if we have enough data
      if (adcCount > 1) {
        int newAverage = (int)(adcSum / adcCount);
        
        // Calculate standard deviation with overflow protection
        unsigned long meanSquares = adcSumSquares / adcCount;  // E[X²]
        unsigned long avgSquared = (unsigned long)newAverage * (unsigned long)newAverage;  // (E[X])²
        
        int newStdDev = 0;
        if (meanSquares >= avgSquared) {  // Prevent negative variance
          unsigned long variance = meanSquares - avgSquared;
          newStdDev = (int)sqrt(variance);
        }
        
        // Only update display if values actually changed
        if (newAverage != lastAverage || newStdDev != lastStdDev) {
          lastAverage = newAverage;
          lastStdDev = newStdDev;
          updateBottomLine(newAverage, newStdDev);
        }
      }
    } else {
      // Overflow protection - stop accumulating but continue tracking min/max
      unsigned int currentAdc = (unsigned int)adcValue;
      if (currentAdc < minAdc) minAdc = currentAdc;
      if (currentAdc > maxAdc) maxAdc = currentAdc;
    }
    
  } else if (!keyPressed_bool && keyCurrentlyPressed) {
    // Key just released
    keyCurrentlyPressed = false;
    
    // Reset tracking variables safely
    adcSum = 0;
    adcSumSquares = 0;
    adcCount = 0;
    lastAverage = -1;
    lastStdDev = -1;
    minAdc = 1023;
    maxAdc = 0;
    updateBottomLine(-1, -1); // Clear average display
  }
  
  // Only update top line if values changed (prevents flickering)
  if (adcValue != lastAdcValue || keyIndex != lastKeyIndex) {
    updateTopLine(adcValue, keyName);
    lastAdcValue = adcValue;
    lastKeyIndex = keyIndex;
  }
  
  // Small delay for ADC stability
  delay(20);
}

void createCustomChars() {
  // Load custom character bitmaps from PROGMEM
  byte charBuffer[8];
  
  // Load plus-minus character
  for (int i = 0; i < 8; i++) {
    charBuffer[i] = pgm_read_byte(&PLUS_MINUS_CHAR[i]);
  }
  lcd.createChar(CHAR_PLUS_MINUS, charBuffer);
}

void showIntro() {
  lcd.clear();
  
  // Type-writer effect for line 1
  const char* line1 = INTRO_LINE1;
  for (int i = 0; i <= strlen_P(line1); i++) {
    lcd.setCursor(0, 0);
    for (int j = 0; j < i; j++) {
      lcd.write(pgm_read_byte(&line1[j]));
    }
    delay(80);
  }
  
  delay(300);
  
  // Type-writer effect for line 2
  const char* line2 = INTRO_LINE2;
  for (int i = 0; i <= strlen_P(line2); i++) {
    lcd.setCursor(0, 1);
    for (int j = 0; j < i; j++) {
      lcd.write(pgm_read_byte(&line2[j]));
    }
    delay(80);
  }
  
  delay(1500);
  
  // Simple fade-out effect
  for (int fade = 0; fade < 3; fade++) {
    lcd.clear();
    delay(200);
    lcd.setCursor(0, 0);
    lcd.print((__FlashStringHelper*)line1);
    lcd.setCursor(0, 1);
    lcd.print((__FlashStringHelper*)line2);
    delay(200);
  }
}

void updateTopLine(int adcValue, const char* keyName) {
  // Update ADC value (with fixed position)
  lcd.setCursor(4, 0);
  lcd.print(F("    ")); // Clear previous value
  lcd.setCursor(4, 0);
  lcd.print(adcValue);
  
  // Update key name - only clear what's needed
  lcd.setCursor(9, 0);
  lcd.print(F("       ")); // Clear previous key name
  lcd.setCursor(9, 0);
  lcd.print(keyName);
}

void updateBottomLine(int average, int stdDev) {
  lcd.setCursor(0, 1);
  lcd.print(F("                ")); // Clear entire line once
  lcd.setCursor(0, 1);
  
  if (average == -1) {
    // No key pressed - show ready message
    lcd.print(F("Press any key..."));
  } else {
    // Key pressed - show running average ± standard deviation
    lcd.print(F("AVG:"));
    
    // Right-align numbers with proper formating
    if (average < 100) lcd.print(F(" "));
    if (average < 10) lcd.print(F(" "));
    lcd.print(average);
    lcd.print(F(" "));
    lcd.write((uint8_t)CHAR_PLUS_MINUS); // Use the custom plus-minus character
    
    if (stdDev < 100) lcd.print(F(" "));
    if (stdDev < 10) lcd.print(F(" "));
    lcd.print(stdDev);
  }
}

int getKeyIndex(int adcValue) {
  // Find which threshold range the ADC value falls into
  for (int i = 0; i < NUM_KEYS; i++) {
    if (adcValue >= KEY_THRESHOLDS[i].lower && adcValue <= KEY_THRESHOLDS[i].upper) {
      return i;
    }
  }
  return NUM_KEYS - 1; // Default to "NONE" if no match
}

// Utility function to get key name by index (for future use)
const char* getKeyName(int keyIndex) {
  if (keyIndex >= 0 && keyIndex < NUM_KEYS) {
    return KEY_THRESHOLDS[keyIndex].name;
  }
  return "UNKNOWN";
}