As some background, I am using an Arduino Nano Every to control a linear actuator to move a specific distance that is calculated from a volume that is entered by an operator via a 4 x 4 matrix keyboard (gotten off Amazon - link here: Amazon.com: DIYmalls 4x4 Matrix Membrane Keypad 16 Key Keyboard Module Array Switch for Arduino ESP32 : Tools & Home Improvement ) and the system also uses a 4 x 20 LCD display that has an I2C adapter all ready installed. So I wrote my program initally using one of the available I2C libraries specifically for keyboards and using standard keyboard mapping I am having problems with having the correct volume show up on the display due to when I press a button on the keypad, I am not getting the correct value on the display. After some investigation, I seems that the wiring (keymap) of this keypad does not match the library pinouts of the PCF8574 libraries that I have used (libraries used: I2CKeyPad.h and Adafruit_PCF8574.h). Is there a way to change the key map so that the libraries will work?
Below is the code for keymaping
// Keypad mapping and PCF8574 pin mapping
const byte ROWS = 4;
const byte COLS = 4;
char keymap[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// PCF8574 pins: change if you wired differently
const uint8_t kbdRowPins[ROWS] = {3,2,1,0}; // P3..P0
const uint8_t kbdColPins[COLS] = {7,6,5,4}; // P7..P4
This is giving me the incorrect numbers when I press a key.
The whole sketch is as follows (Sorry, it is kind of involved)
/*
Keypad is a 4×4 matrix wired to a PCF8574 I/O expander on I²C (fully implemented scan).
LCD is a 20×4 I²C module. Both share the same I²C bus.
User types up to 3 digits on keypad; press # to save the requested cc. * clears the typed buffer.
Conversion from cc → millimeters uses constant CC_TO_MM = 0.17 (1 cc = 0.17 mm) as you requested earlier. You must set STEPS_PER_MM for your leadscrew/mechanism so STEPS_PER_CC = STEPS_PER_MM * CC_TO_MM.
When a dispense finishes normally it will retract by a short distance (RETRACT_MM) and that retract distance (in steps) is added to the next dispense as extra travel but is NOT counted in the reported dispensed volume. That extra is tracked in extraCarrySteps.
If the limit switch triggers during any movement the motion stops immediately. If the limit switch triggered during a dispense that is currently running, the running total is NOT updated and leftoverCC is set so the remaining amount may be finished later (user presses the Finish or Resume button). The LCD displays EMPTY while the limit is active.
Move In / Move Out are non-keypad hold-buttons. Finish is a non-keypad button that performs the Finish movement described in your points.
All runtime saved values are cleared at power-up as required. (No EEPROM used.)
*/
// DispenseController_NanoEvery_Sketch.ino
// Uses: Wire.h, AccelStepper.h, LiquidCrystal_I2C.h, Adafruit_PCF8574.h
// Implements keypad entry, dispense, move in/out, finish, limit switch handling,
// retract & carry-forward retract distance, LCD status, and clears runtime values at startup.
#include <Wire.h>
#include <AccelStepper.h>
#include <LiquidCrystal_I2C.h>
#include <Adafruit_PCF8574.h>
#include <EEPROM.h>
// ----------------- USER CONFIG -----------------
const uint8_t LCD_ADDR = 0x27; // change as required
const uint8_t PCF8574_ADDR = 0x20; // change as required
//------------------- EEPROM Storage -------------
const int EEPROM_ADDR = 0;
// Keypad mapping and PCF8574 pin mapping
const byte ROWS = 4;
const byte COLS = 4;
char keymap[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// PCF8574 pins: change if you wired differently
const uint8_t kbdRowPins[ROWS] = {3,2,1,0}; // P3..P0
const uint8_t kbdColPins[COLS] = {7,6,5,4}; // P7..P4
// Stepper driver pins (Arduino digital pins)
const uint8_t STEP_PIN = 20;
const uint8_t DIR_PIN = 21;
const uint8_t EN_PIN = 255; // set to 255 if unused
// Pushbuttons (wired to GND, using INPUT_PULLUP)
const uint8_t TRIGGER_PIN = 26; // press & release starts dispense
const uint8_t MOVE_IN_PIN = 22; // hold = move in
const uint8_t MOVE_OUT_PIN = 23; // hold = move out
const uint8_t FINISH_PIN = 24; // press = Finish movement/resume leftover
// Limit switch (active LOW when hit)
const uint8_t LIMIT_PIN = 25;
// Conversion constants
const float CC_TO_MM = 0.17; // 1 cc = 0.17 mm
// You must set STEPS_PER_MM for your leadscrew/actuator
const float STEPS_PER_MM = 10000.0; // <-- SET THIS to your hardware (example)
const float STEPS_PER_CC = STEPS_PER_MM * CC_TO_MM;
// Retract distance at end of a dispense / finish (in mm)
const float RETRACT_MM = 0.5; // small retract distance; adjust to suit
const long RETRACT_STEPS = (long)round(RETRACT_MM * STEPS_PER_MM);
// Motion tuning
const float DISPENSE_SPEED = 400.0; // steps / sec
const float MANUAL_SPEED = 900.0; // steps / sec
const float ACCEL = 800.0; // steps / sec^2
// ----------------- Objects -----------------
Adafruit_PCF8574 pcf;
LiquidCrystal_I2C lcd(LCD_ADDR, 20, 4);
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
// ----------------- Runtime state (cleared at power-up) -----------------
// User-entered requested amount (cc)
String inputBuffer = ""; // numeric digits typed (max 3 digits enforced)
int requestedCC = 0; // saved value after '#'
// Dispense tracking
float leftoverCC = 0.0; // amount left to be dispensed when limit stops
float totalDispensedCC = 0.0; // running total of dispensed cc (updated only on successful full cycles)
int dispensedCC_display = 0; // shown on row 2 (integer)
// Extra retract carry (in steps) — retract at end of move increases next forward move by this many steps
long extraCarrySteps = 0;
// Movement state
bool dispensing = false;
bool retracting = false; // we use two-phase: forward dispense then retract
bool isEmpty = false; // limit switch currently active
String actionText = "Finish";
// Internal step tracking
long dispenseStartPos = 0;
long dispenseRequestedSteps = 0; // steps corresponding to requestedCC (not including extra carry)
// Keypad scan timing
unsigned long lastKbdScan = 0;
const unsigned long KBD_SCAN_INTERVAL = 30; // ms
// Trigger debounce
unsigned long lastTriggerDebounce = 0;
const unsigned long TRIGGER_DEBOUNCE_MS = 50;
bool lastTriggerState = HIGH;
// Alert LED / buzzer pins (optional — not used here, left if you want to add)
// const uint8_t ALERT_LED_PIN = 6;
// const uint8_t ALERT_BUZZER_PIN = 7;
// ----------------- Prototypes -----------------
char scanKeypad();
void handleKey(char k);
void updateLCD();
void startDispenseForRequested();
void startDispenseCC(float cc); // start forward motion (non-blocking)
void startRetractAfterDispense(); // after forward completed, initiate retract
void stopMovementByLimit();
void finishDispenseNormallyAfterRetract();
void enableDriver(bool en);
// ----------------- setup -----------------
void setup() {
Serial.begin(115200);
Wire.begin();
// Initialize PCF8574 (keypad expander)
// Adafruit_PCF8574::begin may accept no args or the address depending on library version.
// Use begin() and print warning if not connected.
if (!pcf.begin(PCF8574_ADDR)) {
// Some variants simply pcf.begin(); If this errors, comment out address.
Serial.println("PCF8574 init returned false or begin signature differs. If needed adjust pcf.begin() call.");
}
// configure PCF8574 pins: columns as outputs (drive HIGH to release), rows as inputs
for (uint8_t c = 0; c < COLS; c++) {
pcf.pinMode(kbdColPins[c], OUTPUT);
pcf.digitalWrite(kbdColPins[c], HIGH); // release
}
for (uint8_t r = 0; r < ROWS; r++) {
pcf.pinMode(kbdRowPins[r], INPUT);
pcf.digitalWrite(kbdRowPins[r], HIGH); // pull-up/quasi-high
}
// LCD init
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Dispense Controller");
lcd.setCursor(0,1);
lcd.print("Startup - cleared");
delay(700);
lcd.clear();
// Stepper and enable pin
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
if (EN_PIN != 255) {
pinMode(EN_PIN, OUTPUT);
digitalWrite(EN_PIN, HIGH); // disable by default (active LOW)
}
stepper.setMaxSpeed(MANUAL_SPEED);
stepper.setAcceleration(ACCEL);
stepper.setCurrentPosition(0);
// Buttons and limit
pinMode(TRIGGER_PIN, INPUT_PULLUP);
pinMode(MOVE_IN_PIN, INPUT_PULLUP);
pinMode(MOVE_OUT_PIN, INPUT_PULLUP);
pinMode(FINISH_PIN, INPUT_PULLUP);
pinMode(LIMIT_PIN, INPUT_PULLUP);
// Initialize runtime state (Requirement 12: clear all saved values at power up)
inputBuffer = "";
requestedCC = 0;
leftoverCC = 0.0;
totalDispensedCC = 0.0;
dispensedCC_display = 0;
extraCarrySteps = 0;
dispensing = false;
retracting = false;
isEmpty = false;
actionText = "Finish";
updateLCD();
Serial.println("Ready.");
}
// ----------------- main loop -----------------
void loop() {
// Keypad scan at a limited rate
if (millis() - lastKbdScan >= KBD_SCAN_INTERVAL) {
lastKbdScan = millis();
char k = scanKeypad();
if (k) {
handleKey(k);
}
}
// Limit switch handling (active LOW)
bool limitActive = (digitalRead(LIMIT_PIN) == LOW);
if (limitActive && !isEmpty) {
// newly activated
isEmpty = true;
// stop whatever is moving and store leftover if it's a dispense
stopMovementByLimit();
} else if (!limitActive && isEmpty) {
// released
isEmpty = false;
// CLEAR EMPTY message will be handled by LCD update
}
// Trigger button: press & release to start dispense
bool trig = digitalRead(TRIGGER_PIN);
if (trig != lastTriggerState) {
lastTriggerDebounce = millis();
lastTriggerState = trig;
}
if (millis() - lastTriggerDebounce > TRIGGER_DEBOUNCE_MS) {
static bool wasPressed = false;
if (trig == LOW) {
wasPressed = true;
} else {
if (wasPressed) {
wasPressed = false;
// on release after press
if (!dispensing && !retracting && !isEmpty && requestedCC > 0) {
startDispenseForRequested();
}
}
}
}
// Finish button (non-keypad): perform Finish function
if (digitalRead(FINISH_PIN) == LOW && !dispensing && !retracting && !isEmpty) {
// Finish: move the distance equal to amount left to be dispensed, then retract and update totals
// The leftoverCC is used
if (leftoverCC <= 0.0f) {
// nothing to finish
} else {
actionText = "FINISH";
startDispenseCC(leftoverCC); // reuse dispense code; on success we will add requestedCC to total after retract completes
}
}
// Move In / Move Out manual (hold buttons)
bool moveInHeld = (digitalRead(MOVE_IN_PIN) == LOW);
bool moveOutHeld = (digitalRead(MOVE_OUT_PIN) == LOW);
if (!dispensing && !retracting) {
if (moveInHeld && !isEmpty) {
actionText = "MOVE IN";
enableDriver(true);
stepper.setMaxSpeed(MANUAL_SPEED);
stepper.setSpeed(-MANUAL_SPEED); // negative for "in" direction (adjust if reversed)
stepper.runSpeed();
} else if (moveOutHeld && !isEmpty) {
actionText = "MOVE OUT";
enableDriver(true);
stepper.setMaxSpeed(MANUAL_SPEED);
stepper.setSpeed(MANUAL_SPEED);
stepper.runSpeed();
} else {
// not manually moving
if (actionText == "MOVE IN" || actionText == "MOVE OUT") actionText = "Finish";
enableDriver(false);
}
}
// Non-blocking dispense/retract handling
if (dispensing) {
enableDriver(true);
stepper.run(); // handles acceleration
// update progress display (exclude extra carry steps when calculating dispensed amount)
long stepsMoved = labs(stepper.currentPosition() - dispenseStartPos);
long requestedSteps = dispenseRequestedSteps; // steps corresponding to requestedCC (no extra carry)
long stepsCountedAsDispensed = min(stepsMoved, requestedSteps);
float ccDone = (float)stepsCountedAsDispensed / STEPS_PER_CC;
dispensedCC_display = (int)floor(ccDone + 1e-6f);
leftoverCC = max(0.0, (float)requestedCC - ccDone);
// Forward phase finished?
if (stepper.distanceToGo() == 0) {
// forward finished — now start retract phase if retract length > 0
dispensing = false;
// start retract
if (RETRACT_STEPS > 0) {
// Retract is negative relative to forward
retracting = true;
// carry the retract steps to next dispense (per requirement)
extraCarrySteps += RETRACT_STEPS;
actionText = "RETRACT";
// start retract move
stepper.move(-RETRACT_STEPS);
} else {
// no retract; finish normally: update totals and clear action
finishDispenseNormallyAfterRetract();
}
}
} else if (retracting) {
enableDriver(true);
stepper.run();
// if retract movement completed
if (stepper.distanceToGo() == 0) {
// retract done normally — update totals depending on whether this was a normal dispense or a Finish
finishDispenseNormallyAfterRetract();
retracting = false;
}
}
// LCD update periodically (not every loop)
static unsigned long lastLCD = 0;
if (millis() - lastLCD > 150) {
lastLCD = millis();
updateLCD();
}
}
// ----------------- Functions -----------------
// Non-blocking start of dispense based on requestedCC; adds any extraCarrySteps to forward travel
void startDispenseForRequested() {
if (requestedCC <= 0) return;
startDispenseCC((float)requestedCC);
}
// Start dispense for a given cc value (float allowed for leftover)
void startDispenseCC(float cc) {
if (cc <= 0.0f) return;
if (isEmpty) {
actionText = "Finish";
return;
}
// compute steps for requested cc (only for counting dispensed amount)
long reqSteps = (long)round(cc * STEPS_PER_CC);
dispenseRequestedSteps = reqSteps;
// include extraCarrySteps that were stored from previous retract
long totalForwardSteps = reqSteps + extraCarrySteps;
// start non-blocking relative move
enableDriver(true);
stepper.setAcceleration(ACCEL);
stepper.setMaxSpeed(DISPENSE_SPEED);
dispenseStartPos = stepper.currentPosition();
stepper.move(totalForwardSteps);
dispensing = true;
retracting = false;
actionText = "DISPENSE";
Serial.print("Start dispense: cc="); Serial.print(cc);
Serial.print(" reqSteps="); Serial.print(reqSteps);
Serial.print(" extraCarry="); Serial.print(extraCarrySteps);
Serial.print(" totalSteps="); Serial.println(totalForwardSteps);
}
// Called when movement is stopped by limit switch
void stopMovementByLimit() {
// If actively moving (either forward or retract), stop and compute leftover if it was a forward dispense
if (dispensing || retracting) {
stepper.stop(); // requests deceleration to stop
enableDriver(false);
// compute how many steps moved since dispense start (may be during forward or retract).
long posNow = stepper.currentPosition();
long stepsMovedFromStart = labs(posNow - dispenseStartPos);
// Determine how many of those counted as "dispensed" (exclude extraCarrySteps)
long countedDispensedSteps = min(stepsMovedFromStart, dispenseRequestedSteps);
float ccDone = (float)countedDispensedSteps / STEPS_PER_CC;
dispensedCC_display = (int)floor(ccDone + 1e-6f);
leftoverCC = max(0.0f, (float)requestedCC - ccDone);
// Because it was stopped by limit, do NOT update totalDispensedCC and do NOT apply extraCarry update.
// extraCarrySteps remains as-is (the retract didn't complete).
dispensing = false;
retracting = false;
actionText = "Finish"; // user can resume with Finish button
Serial.println("Movement stopped by limit. LEFTOVER stored; total not updated.");
}
}
// Called after retract finishes normally to finalize totals
void finishDispenseNormallyAfterRetract() {
// Normal completion: the forward phase fully finished and the retract completed normally.
// Requirement: update running total by adding the amount dispensed (i.e., requestedCC)
// Note: per requirements, the retract mm is added to next dispense via extraCarrySteps, but not counted in dispensed total.
totalDispensedCC += (float)requestedCC;
// Because the retract already added RETRACT_STEPS to extraCarrySteps earlier (in forward completion),
// we do not add it here again.
// Clear state
dispensedCC_display = requestedCC; // full amount reflected
leftoverCC = 0.0f;
requestedCC = 0; // clear requested after a full successful dispense per typical behavior
actionText = "Finish";
Serial.print("Normal dispense finished. Total dispensed now = ");
Serial.println(totalDispensedCC);
}
// Enable/disable stepper driver (if EN_PIN used)
void enableDriver(bool en) {
if (EN_PIN == 255) return;
digitalWrite(EN_PIN, en ? LOW : HIGH); // active LOW enable
}
// ----------------- Keypad scanning via PCF8574 -----------------
// Columns are outputs (drive LOW one at a time), rows are inputs read (active LOW).
// Debounce: short delay; waits for release (max timeout) to avoid repeat flood.
char scanKeypad() {
for (int c = 0; c < COLS; c++) {
// release all columns
for (int cc = 0; cc < COLS; cc++) pcf.digitalWrite(kbdColPins[cc], HIGH);
// drive this column low
pcf.digitalWrite(kbdColPins[c], LOW);
delayMicroseconds(30);
for (int r = 0; r < ROWS; r++) {
int v = pcf.digitalRead(kbdRowPins[r]);
if (v == LOW) { // active low -> pressed
delay(12); // debounce
if (pcf.digitalRead(kbdRowPins[r]) == LOW) {
char k = keymap[r][c];
// wait for release with timeout to avoid blocking forever
unsigned long t0 = millis();
while (pcf.digitalRead(kbdRowPins[r]) == LOW && millis() - t0 < 600) {
delay(8);
}
// restore column
pcf.digitalWrite(kbdColPins[c], HIGH);
return k;
}
}
}
// restore column
pcf.digitalWrite(kbdColPins[c], HIGH);
}
return 0;
}
// ----------------- Handle keypad keys -----------------
void handleKey(char k) {
if (k >= '0' && k <= '9') {
// allow up to 3 digits
if (inputBuffer.length() < 3) {
inputBuffer += k;
}
} else if (k == '*') {
// clear typed buffer
inputBuffer = "";
} else if (k == '#') {
// save typed value as requestedCC
if (inputBuffer.length() > 0) {
requestedCC = inputBuffer.toInt();
// initialize leftover to requested (if previously partial dispense existed, user may overwrite)
leftoverCC = (float)requestedCC;
dispensedCC_display = 0;
inputBuffer = "";
actionText = "Ready";
Serial.print("Requested CC saved: "); Serial.println(requestedCC);
}
}
// keys A,B,C,D reserved for future functions if needed
}
// ----------------- LCD update -----------------
void updateLCD() {
// Row 1: requested amount
lcd.setCursor(0, 0);
lcd.print("Req: ");
if (requestedCC > 0) {
lcd.print(requestedCC);
lcd.print(" cc ");
} else if (inputBuffer.length() > 0) {
lcd.print(inputBuffer);
lcd.print(" cc ");
} else {
lcd.print("0 cc ");
}
// Row 2: total amount dispensed (running total)
lcd.setCursor(0, 1);
lcd.print("Total Disp: ");
// print with one decimal if fractional, but primary values are integers
lcd.print(totalDispensedCC, 2);
lcd.print(" cc ");
// Row 3: EMPTY when limit active
lcd.setCursor(0, 2);
if (isEmpty) {
lcd.print("EMPTY ");
} else {
lcd.print(" ");
}
// Row 4: current action text
lcd.setCursor(0, 3);
lcd.print(actionText);
// pad to clear previous longer text
for (int i = actionText.length(); i < 20; i++) lcd.print(' ');
}
Is there a generic I2C keypad library that makes it easy to change the keymap?
Thanks ahead of time for your assistance.

