Help with PCF8574 for a 4 x 4 keypad failing to enter values

I am hoping someone will have an idea where I went wrong. In a project I am working on I have coded a I2C 4 x 4 keypad for a user to enter a value and have the sketch store that value upon entering the ‘#’ button and clear that value when the ‘*’ button is pressed. I have completed whole sketch and after some debugging after compiling I have uploaded the sketch to the Nano Every I am using and though the I2C 4 x 20 Display seems to be working properly, I cannot get any values I have entered in with the keypad to display. I have only included the parts of the code for the 4 x 4 keypad below and any supporting code due to the sketch being complicated.

#include <Wire.h>
#include <AccelStepper.h>
#include <LiquidCrystal_I2C.h>
#include <PCF8574.h>

// ----------------- USER CONFIG -----------------
const uint8_t LCD_ADDR  = 0x27; // change to your LCD I2C address
const uint8_t KBD_ADDR  = 0x20; // change to your PCF8574 I2C address

// PCF8574 keypad wiring: P0..P3 -> rows; P4..P7 -> cols (change if your wiring differs)
const byte ROWS = 4;
const byte COLS = 4;
char hexaKeys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};
const uint8_t kbdRowPins[ROWS] = {0,1,2,3}; // PCF8574 pins for rows
const uint8_t kbdColPins[COLS] = {4,5,6,7}; // PCF8574 pins for cols
.
.
.
// Objects
PCF8574 pcf(KBD_ADDR);
LiquidCrystal_I2C lcd(LCD_ADDR, 20, 4);
.
.
.
// Keypad scan timing
unsigned long lastKbdScan = 0;
const unsigned long KBD_SCAN_INTERVAL = 40; // ms
.
.
.
// Helper prototypes
char scanKeypad();
.
.
.
void setup() {
  Serial.begin(9600);
  Wire.begin();
  pcf.begin();
.
.
.
// Setup PCF8574 for keypad
  if (!pcf.isConnected()) {
    Serial.println("Warning: PCF8574 not responding at configured address.");
  }
  // initialize pins: set columns as outputs and rows as inputs
  for (uint8_t i=0;i<8;i++) {
    pinMode(i, INPUT); // default
    digitalWrite(i, HIGH); // release
  }
  for (uint8_t c=0;c<COLS;c++) {
    pinMode(kbdColPins[c], OUTPUT);
    digitalWrite(kbdColPins[c], HIGH);
  }
  for (uint8_t r=0;r<ROWS;r++) {
    pinMode(kbdRowPins[r], INPUT);
  }
.
.
.
void loop() {
  // Keypad scanning (numbers, # to save, * to clear). Only used for entering requested amount.
  if (millis() - lastKbdScan >= KBD_SCAN_INTERVAL) {
    lastKbdScan = millis();
    char k = scanKeypad();
    if (k) {
      Serial.print("K: "); Serial.println(k);
      if (k >= '0' && k <= '9') {
        if (inputBuffer.length() < 6) inputBuffer += k; // allow up to 6 digits
      } else if (k == '*') {
        inputBuffer = "";
        requestedCC = 0;
        leftoverCC = 0.0;
        dispensedCC_at_limit = 0;
        actionText = "Finish";
      } else if (k == '#') {
        if (inputBuffer.length() > 0) {
          requestedCC = inputBuffer.toInt();
          inputBuffer = "";
          leftoverCC = (float)requestedCC; // initially leftover equals requested
          dispensedCC_at_limit = 0;
          actionText = "Ready";
          Serial.print("Requested set to: "); Serial.println(requestedCC);
        } else if (k == 'A'){
          //Start Calibration Routine
          inCalibration = true;
          runCalibration();
          // ignore empty '#'
        }
      }
    }
  }
.
.
.
char scanKeypad() {
  for (int c = 0; c < COLS; c++) {
    // release all cols
    for (int cc = 0; cc < COLS; cc++) digitalWrite(kbdColPins[cc], HIGH);
    // pull this column low
    digitalWrite(kbdColPins[c], LOW);
    delayMicroseconds(30);
    for (int r = 0; r < ROWS; r++) {
      int v = digitalRead(kbdRowPins[r]);
      if (v == 0) { // low = pressed
        delay(20);
        if (digitalRead(kbdRowPins[r]) == 0) {
          char k = hexaKeys[r][c];
          // simple wait-for-release to avoid repeated rapid triggers
          // (commented out to allow quick entries; keep as needed)
          while (digitalRead(kbdRowPins[r]) == 0) delay(5);
          digitalWrite(kbdColPins[c], HIGH); // restore
          return k;
        }
      }
    }
    digitalWrite(kbdColPins[c], HIGH);
  }
  return 0;


When debugging, in the char scanKeypad() function, it originally looked like this

char scanKeypad() {
  // Drive each column low in turn and read rows (typical matrix scan)
  for (int c = 0; c < COLS; c++) {
    // release all cols HIGH
    for (int i = 0; i < COLS; i++) pcf.digitalWrite(kbdColPins[i], HIGH);
    // drive this column LOW
    pcf.digitalWrite(kbdColPins[c], LOW);
    delayMicroseconds(50);
    for (int r = 0; r < ROWS; r++) {
      int v = pcf.digitalRead(kbdRowPins[r]);
      if (v == 0) { // key pressed (pulls line low)
        // simple debounce: wait briefly and re-check
        delay(20);
        int v2 = pcf.digitalRead(kbdRowPins[r]);
        if (v2 == 0) {
          // wait until release before returning (optional) — we'll return immediately though
          char k = hexaKeys[r][c];
          // restore column
          pcf.digitalWrite(kbdColPins[c], HIGH);
          return k;
        }
      }
    }
    // restore column
    pcf.digitalWrite(kbdColPins[c], HIGH);
  }
  return 0;

With the ‘pcf.” in the code, it was giving me errors upon compiling, but when I removed them, it compiled without this error.

I was hoping someone would provide some guidance on where to look. If I need to, I can upload the entire sketch if that would help.

Thanks,

And those errors would be...?

Have you tried using the Keypad library?

Here are the errors

C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino: In function 'void setup()':
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:132:9: error: 'class PCF8574' has no member named 'pinMode'
     pcf.pinMode(i, INPUT); // default
         ^~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:133:9: error: 'class PCF8574' has no member named 'digitalWrite'
     pcf.digitalWrite(i, HIGH); // release
         ^~~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:136:9: error: 'class PCF8574' has no member named 'pinMode'
     pcf.pinMode(kbdColPins[c], OUTPUT);
         ^~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:137:9: error: 'class PCF8574' has no member named 'digitalWrite'
     pcf.digitalWrite(kbdColPins[c], HIGH);
         ^~~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:140:9: error: 'class PCF8574' has no member named 'pinMode'
     pcf.pinMode(kbdRowPins[r], INPUT);
         ^~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino: In function 'char scanKeypad()':
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:330:43: error: 'class PCF8574' has no member named 'digitalWrite'
     for (int cc = 0; cc < COLS; cc++) pcf.digitalWrite(kbdColPins[cc], HIGH);
                                           ^~~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:332:9: error: 'class PCF8574' has no member named 'digitalWrite'
     pcf.digitalWrite(kbdColPins[c], LOW);
         ^~~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:335:19: error: 'class PCF8574' has no member named 'digitalRead'
       int v = pcf.digitalRead(kbdRowPins[r]);
                   ^~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:338:17: error: 'class PCF8574' has no member named 'digitalRead'
         if (pcf.digitalRead(kbdRowPins[r]) == 0) {
                 ^~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:343:15: error: 'class PCF8574' has no member named 'digitalWrite'
           pcf.digitalWrite(kbdColPins[c], HIGH); // restore
               ^~~~~~~~~~~~
C:\Users\john.francis.PLASTOCOR\Documents\Arduino\sketch_sep9a_Precision_Dispenser_Initial\sketch_sep9a_Precision_Dispenser_Initial.ino:348:9: error: 'class PCF8574' has no member named 'digitalWrite'
     pcf.digitalWrite(kbdColPins[c], HIGH);
         ^~~~~~~~~~~~

Did I forget a step in setting up the Keypad for use with I2C?

I did not think that the keypad library had I2C functionality. I will check on this.

There are different libraries for the pcf8574/5.

I think it is the Adafruit library which has the .digitalRead() and digitalWrite() syntax.


#include <Adafruit_PCF8575.h>

#include <Adafruit_PCF8574.h>

I'd have tried compiling your entire sketch, but it seems to be absent. As is a MRE demonstrating the alleged problem.

So I made a simple sketch that calls some of the functions in the PCF8574 library.

It compiled for me just fine with a Nano Every selected.

#include <PCF8574.h>

PCF8574 pcf8574(0x39);

void setup() {
   Serial.begin(115200);
   if( !pcf8574.begin() ) {
      Serial.print("PCF8574 Init failed");
      while( true ) {
         yield();
      }
   }
   pcf8574.pinMode(0, OUTPUT);
}

void loop() {
   pcf8574.digitalWrite(0, !pcf8574.digitalRead(0));
   delay(500);
}
arduino-cli compile -b arduino:megaavr:nona4809:mode=off --warnings all --output-dir ~/tmp --no-color (in directory: /home/me/Documents/sketchbook/NanoEvery/test)
Sketch uses 9466 bytes (19%) of program storage space. Maximum is 49152 bytes.
Global variables use 717 bytes (11%) of dynamic memory, leaving 5427 bytes for local variables. Maximum is 6144 bytes.
Compilation finished successfully.

Does it need to have I2C? You are already using 8 pins on the Arduino.

As I stated previously, I only included the keyboard parts. Please see below for the entire sketch that I took the initial keyboard code from.

// DispenseController_NanoEvery_v2.ino
// Implements keypad entry via PCF8574 (I2C), 4x20 LCD via I2C, stepper (STEP+DIR),
// a trigger button (press & release to start dispense), Move In / Move Out buttons (hold),
// Resume button to dispense leftover after limit stop, limit switch stops dispensing,
// display rows:
//   Row1: Requested amount (cc)
//   Row2: Amount dispensed up to limit activation (cc)
//   Row3: "EMPTY" when limit active
//   Row4: Current action ("Dispense","Move In","Move Out","Finish")
// Clears all saved values at power-up.
//
// Configure the pin assignments and calibration constant STEPS_PER_CC below.

#include <Wire.h>
#include <AccelStepper.h>
#include <LiquidCrystal_I2C.h>
#include <PCF8574.h>

// ----------------- USER CONFIG -----------------
const uint8_t LCD_ADDR  = 0x27; // change to your LCD I2C address
const uint8_t KBD_ADDR  = 0x20; // change to your PCF8574 I2C address

// PCF8574 keypad wiring: P0..P3 -> rows; P4..P7 -> cols (change if your wiring differs)
const byte ROWS = 4;
const byte COLS = 4;
char hexaKeys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};
const uint8_t kbdRowPins[ROWS] = {0,1,2,3}; // PCF8574 pins for rows
const uint8_t kbdColPins[COLS] = {4,5,6,7}; // PCF8574 pins for cols

// Stepper driver pins (Arduino digital pins)
const uint8_t STEP_PIN = 20;     // step
const uint8_t DIR_PIN  = 21;     // direction
//const uint8_t EN_PIN   = 5;     // optional enable pin (active LOW). Set to 255 if unused.

// Non-keypad buttons (active LOW with INPUT_PULLUP)
const uint8_t TRIGGER_PIN  = 26;  // press & release to start dispense
const uint8_t MOVE_IN_PIN   = 22;  // hold to move in
const uint8_t MOVE_OUT_PIN  = 23; // hold to move out
const uint8_t RESUME_PIN    = 24; // press to dispense leftover after limit stop

// Limit switch (active LOW)
const uint8_t LIMIT_PIN = 25; // limit switch active = LOW

/*  For future version
//LED + Buzzer (external, not onboard LED)
const uint8_t LED_PIN = XX;
const uint8_t BUZZER_PIN = XX;
*/

// Mechanical calibration - CHANGE THIS to match your system
// Steps required to move plunger such that 1.0 cc is dispensed.
float STEPS_PER_CC = 17.0; // <-- set to your calibrated value

// Motion tuning
const float DISPENSE_SPEED_STEPS_PER_SEC = 400.0;  // speed while dispensing
const float MANUAL_MOVE_SPEED_STEPS_PER_SEC = 1000.0; // speed while Move In/Out
const float ACCEL_STEPS_PER_SEC2 = 800.0;

// ------------------------------------------------

// Objects
PCF8574 pcf(KBD_ADDR);
LiquidCrystal_I2C lcd(LCD_ADDR, 20, 4);
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);

// Runtime state (cleared at power-up)
String inputBuffer = "";
volatile int requestedCC = 0;            // user requested amount (cc)
volatile float leftoverCC = 0.0;         // amount left to dispense (cc) after limit stop
volatile int dispensedCC_at_limit = 0;   // integer cc dispensed up to limit activation
volatile bool isEmpty = false;           // true if limit switch active
volatile bool dispensing = false;

long dispenseStartPosition = 0;          // stepper position when a dispense started
long dispenseTargetPosition = 0;         // stepper target (absolute)
long stepsPerCCLong = 0;                 // cached integer for steps per cc rounding

String actionText = "Finish";


// Keypad scan timing
unsigned long lastKbdScan = 0;
const unsigned long KBD_SCAN_INTERVAL = 40; // ms

// Debounce
unsigned long lastTriggerDebounce = 0;
const unsigned long TRIGGER_DEBOUNCE_MS = 50;
bool triggerWasPressed = false;
bool lastTriggerState = HIGH;

//Calibration State
bool inCalibration = false;
long calStartSteps = 0;
float calMeasuredCC = 0.0;

// Helper prototypes
char scanKeypad();
void updateLCD();
void startDispenseFromRequested(); // start full requested
void startDispenseCC(float cc);    // start dispense of a specific cc amount (non-blocking)
void stopDispenseByLimit();
void finishDispenseNormally();
void runCalibration();
bool isButtonPressed(uint8_t pin); // debounced read (active LOW)
//void enableDriver(bool en);

void setup() {
  Serial.begin(9600);
  Wire.begin();
  

  // Clear saved values upon power-up (requirement #10)
  inputBuffer = "";
  requestedCC = 0;
  leftoverCC = 0.0;
  dispensedCC_at_limit = 0;
  isEmpty = false;
  dispensing = false;
  actionText = "Finish";

  // Setup PCF8574 for keypad
  if (!pcf.isConnected()) {
    Serial.println("Warning: PCF8574 not responding at configured address.");
  }
  // initialize pins: set columns as outputs and rows as inputs
  for (uint8_t i=0;i<8;i++) {
    pcf.pinMode(i, INPUT); // default
    pcf.digitalWrite(i, HIGH); // release
  }
  for (uint8_t c=0;c<COLS;c++) {
    pcf.pinMode(kbdColPins[c], OUTPUT);
    pcf.digitalWrite(kbdColPins[c], HIGH);
  }
  for (uint8_t r=0;r<ROWS;r++) {
    pcf.pinMode(kbdRowPins[r], INPUT);
  }

  // Setup LCD
  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Power-up cleared");
  lcd.setCursor(0,1);
  lcd.print("Enter amount (cc)");
  delay(700);
  lcd.clear();

  // Setup stepper pins & AccelStepper
  pinMode(STEP_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);
  /*if (EN_PIN != 255) {
    pinMode(EN_PIN, OUTPUT);
    digitalWrite(EN_PIN, HIGH); // disable driver at start (active LOW)
  }
  */
  stepper.setMaxSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
  stepper.setAcceleration(ACCEL_STEPS_PER_SEC2);

  // Buttons & limit switch
  pinMode(TRIGGER_PIN, INPUT_PULLUP);
  pinMode(MOVE_IN_PIN, INPUT_PULLUP);
  pinMode(MOVE_OUT_PIN, INPUT_PULLUP);
  pinMode(RESUME_PIN, INPUT_PULLUP);
  pinMode(LIMIT_PIN, INPUT_PULLUP);

  // Precompute integer steps per cc (rounded) for tracking convenience
  stepsPerCCLong = (long)round(STEPS_PER_CC);

  updateLCD();
  Serial.println("Ready. Enter amount on keypad, press # to save. '*' clears entry.");
}

// ---------------------- Main Loop ------------------------
void loop() {
  // Keypad scanning (numbers, # to save, * to clear). Only used for entering requested amount.
  if (millis() - lastKbdScan >= KBD_SCAN_INTERVAL) {
    lastKbdScan = millis();
    char k = scanKeypad();
    if (k) {
      Serial.print("K: "); Serial.println(k);
      if (k >= '0' && k <= '9') {
        if (inputBuffer.length() < 6) inputBuffer += k; // allow up to 6 digits
      } else if (k == '*') {
        inputBuffer = "";
        requestedCC = 0;
        leftoverCC = 0.0;
        dispensedCC_at_limit = 0;
        actionText = "Finish";
      } else if (k == '#') {
        if (inputBuffer.length() > 0) {
          requestedCC = inputBuffer.toInt();
          inputBuffer = "";
          leftoverCC = (float)requestedCC; // initially leftover equals requested
          dispensedCC_at_limit = 0;
          actionText = "Ready";
          Serial.print("Requested set to: "); Serial.println(requestedCC);
        } else if (k == 'A'){
          //Start Calibration Routine
          inCalibration = true;
          runCalibration();
          // ignore empty '#'
        }
      }
    }
  }

  // Read limit switch (active LOW)
  bool limitActive = (digitalRead(LIMIT_PIN) == LOW);
  if (limitActive && !isEmpty) {
    // limit just activated
    isEmpty = true;
    // if dispensing, stop and compute leftover
    if (dispensing) {
      // compute steps moved so far during this dispense
      long posNow = stepper.currentPosition();
      long stepsMovedSinceStart = labs(posNow - dispenseStartPosition);
      float ccDone = (float)stepsMovedSinceStart / STEPS_PER_CC;
      dispensedCC_at_limit = (int)floor(ccDone + 1e-6);
      float requestedF = (float)requestedCC;
      leftoverCC = max(0.0f, requestedF - ccDone);
      stopDispenseByLimit();
    }
  } else if (!limitActive && isEmpty) {
    // limit released
    isEmpty = false;
  }

  // Trigger button handling: press & release starts dispense of requested amount
  // Debounce & detect press-release sequence
  bool trigState = digitalRead(TRIGGER_PIN);
  if (trigState != lastTriggerState) {
    lastTriggerDebounce = millis();
    lastTriggerState = trigState;
  }
  if (millis() - lastTriggerDebounce > TRIGGER_DEBOUNCE_MS) {
    if (trigState == LOW) {
      triggerWasPressed = true;
    } else {
      if (triggerWasPressed) {
        triggerWasPressed = false;
        // user pressed & released -> start dispense
        if (!isEmpty && requestedCC > 0 && !dispensing) {
          startDispenseFromRequested();
        }
      }
    }
  }

  // Move In / Move Out (non-keypad physical buttons) - hold to move continuously
  bool moveInPressed  = (digitalRead(MOVE_IN_PIN)  == LOW) && !isEmpty;
  bool moveOutPressed = (digitalRead(MOVE_OUT_PIN) == LOW) && !isEmpty;

  if (moveInPressed && !dispensing) {
    // Move In - define direction negative (adjust sign if your assembly is reversed)
    actionText = "Move In";
    //enableDriver(true);
    stepper.setMaxSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
    stepper.setSpeed(-MANUAL_MOVE_SPEED_STEPS_PER_SEC); // negative for "in"
    stepper.runSpeed();
  } else if (moveOutPressed && !dispensing) {
    // Move Out - positive direction
    actionText = "Move Out";
    //enableDriver(true);
    stepper.setMaxSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
    stepper.setSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
    stepper.runSpeed();
  } else {
    // neither held: if not dispensing, stop manual motion and optionally disable driver
    if (!dispensing) {
      stepper.setSpeed(0);
      //enableDriver(false);
      if (actionText == "Move In" || actionText == "Move Out") actionText = "Finish";
    }
  }

  // Resume button: dispense leftover when pressed (single press)
  static bool resumePrev = HIGH;
  bool resumeState = digitalRead(RESUME_PIN);
  if (resumeState != resumePrev) {
    delay(20);
    resumePrev = resumeState;
  }
  if (resumeState == LOW && !dispensing && leftoverCC > 0.001 && !isEmpty) {
    // start dispensing leftover amount
    startDispenseCC(leftoverCC);
  }

  // Running a dispense (non-blocking using AccelStepper)
  if (dispensing) {
    //enableDriver(true);
    // run() returns true while still moving; run() handles acceleration & deceleration
    stepper.run();

    // Update dispensedCC_at_limit continuously (for display) - but it's only final if limit hit
    long posNow = stepper.currentPosition();
    long stepsMovedSinceStart = labs(posNow - dispenseStartPosition);
    float ccDone = (float)stepsMovedSinceStart / STEPS_PER_CC;
    dispensedCC_at_limit = (int)floor(ccDone + 1e-6);
    // if we reached target
    if (stepper.distanceToGo() == 0) {
      // finished normally
      finishDispenseNormally();
    }
  }

  // Regular LCD refresh (not every loop)
  static unsigned long lastLCD = 0;
  if (millis() - lastLCD > 200) {
    lastLCD = millis();
    updateLCD();
  }
}

// ---------------------- Functions ------------------------

/* scanKeypad()
   Basic matrix scan using PCF8574.
   Returns 0 if no key pressed, or the key char.
   This routine contains a small debounce.
*/
char scanKeypad() {
  for (int c = 0; c < COLS; c++) {
    // release all cols
    for (int cc = 0; cc < COLS; cc++) pcf.digitalWrite(kbdColPins[cc], HIGH);
    // pull 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 == 0) { // low = pressed
        delay(20);
        if (pcf.digitalRead(kbdRowPins[r]) == 0) {
          char k = hexaKeys[r][c];
          // simple wait-for-release to avoid repeated rapid triggers
          // (commented out to allow quick entries; keep as needed)
          //while (digitalRead(kbdRowPins[r]) == 0) delay(5);
          pcf.digitalWrite(kbdColPins[c], HIGH); // restore
          return k;
        }
      }
    }
    pcf.digitalWrite(kbdColPins[c], HIGH);
  }
  return 0;
}

// update LCD rows per requirements 5..8
void updateLCD() {
  lcd.setCursor(0,0);
  // Row1: requested amount in cc
  lcd.print("Req: ");
  if (requestedCC > 0) {
    lcd.print(requestedCC);
    lcd.print(" cc");
    // pad
    for (int i = 0; i < 10; i++) lcd.print(' ');
  } else if (inputBuffer.length() > 0) {
    lcd.print(inputBuffer);
    lcd.print(" cc");
    for (int i = 0; i < 10; i++) lcd.print(' ');
  } else {
    lcd.print("0 cc");
    for (int i = 0; i < 13; i++) lcd.print(' ');
  }

  // Row2: amount dispensed up to activation of limit (or current dispensed during motion)
  lcd.setCursor(0,1);
  lcd.print("Dispensed: ");
  lcd.print(dispensedCC_at_limit);
  lcd.print(" cc");
  for (int i = 0; i < 6; i++) lcd.print(' ');

  // Row3: "EMPTY" when limit active (requirement #7)
  lcd.setCursor(0,2);
  if (isEmpty) {
    //flashEmptyAlert(true);
    lcd.print("EMPTY");
        for (int i = 5; i < 20; i++) lcd.print(' ');
  } else {
    // clear
    for (int i = 0; i < 20; i++) lcd.print(' ');
    //flashEmptyAlert(false);
  }

  // Row4: action text (Dispense, Move In, Move Out, Finish)
  lcd.setCursor(0,3);
  lcd.print(actionText);
  for (int i = actionText.length(); i < 20; i++) lcd.print(' ');
}

// Start dispensing the currently requested amount
void startDispenseFromRequested() {
  if (requestedCC <= 0) return;
  startDispenseCC((float)requestedCC);
}

// Start dispensing a specific CC amount (non-blocking)
void startDispenseCC(float cc) {
  if (isEmpty) {
    Serial.println("Cannot dispense: EMPTY (limit active).");
    actionText = "Finish";
    return;
  }
  long steps = (long)round(cc * STEPS_PER_CC);
  if (steps <= 0) return;
  // prepare stepper
  //enableDriver(true);
  stepper.setAcceleration(ACCEL_STEPS_PER_SEC2);
  stepper.setMaxSpeed(DISPENSE_SPEED_STEPS_PER_SEC);
  dispenseStartPosition = stepper.currentPosition();
  // relative move forward (assume positive direction dispenses)
  stepper.move(steps);
  dispenseTargetPosition = dispenseStartPosition + steps;
  dispensing = true;
  actionText = "Dispense";
  Serial.print("Dispense started: "); Serial.print(cc); Serial.print(" cc (");
  Serial.print(steps); Serial.println(" steps)");
}

// Called when limit switch triggered during dispense
void stopDispenseByLimit() {
  if (!dispensing) return;
  // Stop stepper immediately by clearing target (we can moveTo current pos)
  long posNow = stepper.currentPosition();
  stepper.stop(); // decelerates to stop
  stepper.setCurrentPosition(posNow); // freeze position
  dispensing = false;
  actionText = "Finish";
  // dispensedCC_at_limit and leftoverCC already computed by caller
  Serial.println("Dispense stopped by limit. Marked EMPTY.");
}

// Called when dispense finishes normally (target reached)
void finishDispenseNormally() {
  dispensing = false;
  //enableDriver(false);
  // All requested dispensed -> clear leftover
  leftoverCC = 0.0;
  dispensedCC_at_limit = requestedCC;
  actionText = "Finish";
  Serial.println("Dispense finished normally.");
}

//Calibration Function
void runCalibration(){
  lcd.clear();
  lcd.setCursor (0,0); lcd.print("Calibration Mode");
  lcd.setCursor (0,1); lcd.print("Move plunger out");
  lcd.setCursor (0,2); lcd.print("Press TRIGGER");
  lcd.setCursor(0,3); lcd.print("when ready");
  
  //Wait for user to press Trigger
  while (digitalRead(TRIGGER_PIN) == HIGH);

  calStartSteps - stepper.currentPosition();
  lcd.clear(); lcd.setCursor(0,0); lcd.print("Enter measured cc");
  //User would enter measured cc on keypad after moving a known amount
  //Simplify: assume known 10cc movement
  calMeasuredCC = 10.0;
  long movedSteps = abs(stepper.currentPosition() - calStartSteps);
  if(calMeasuredCC > 0.1){
    STEPS_PER_CC = movedSteps / calMeasuredCC;
  }
  lcd.clear(); lcd.setCursor(0,0); lcd.print("New Cal: ");
  lcd.print(STEPS_PER_CC,1); lcd.print(" steps/cc");
  delay(2000);
  inCalibration = false;
}

// enable/disable driver (active LOW enable)
/*void enableDriver(bool en) {
  if (EN_PIN == 255) return;
  if (en) digitalWrite(EN_PIN, LOW);  // enable driver
  else    digitalWrite(EN_PIN, HIGH); // disable driver
}
*/
/* To be used in later version
void flashEmptyAlert(bool state) {
  if (state) {
    digitalWrite(LED_PIN, HIGH);
    digitalWrite(BUZZER_PIN, HIGH);
  } else {
    digitalWrite(LED_PIN, LOW);
    digitalWrite(BUZZER_PIN, LOW);
  }
  */

As the last responder has coded, I do not see any difference between what I had and what that contributor has provided. I saw in another thread that there could be more than one instance of the library and I will need to check that.

When I came up with the design, I was attempting to reduce the wiring burden overall and make it more streamlined. (see below for pic of cabinet)

In the upper right hand side of the picture is the keyboard and LCD mounting plate. If you look at the Nano Breakout board, it is already quite busy and I do not have enough terminals to direct wire a keypad, therefore I2C communications.

I see, nice layout too, very clean.

Thanks, I try to keep the technician in mind when I design equipment.

Having dug deeper into the PCF8574.h library, it does not support .digitalread, .digitalwrite, and .pinMode. I will need to rewrite this portion of the code to incorporate the new library. I would still like to hear of any other suggestions.

Thanks to all who have responded.

Use one of the libraries which supports the syntax.

I think it improves the understandability/readability of the code to be using the digitalRead and digitalWrite syntax with the pcf.

1 Like

https://docs.arduino.cc/libraries/i2ckeypad/

2 Likes

The pcf8574 chip can only sink on it's outputs, so your keypad will never work, use a mcp23017, it's outputs willsource and sink
Unlike

The PCF8574 has a weak pullup (50K equivalent) when a HIGH is written to a port pin. It works just fine with a keypad. If that were not the case, I would have to have a stern discussion with the 4x4 keypad I have connected to a PCF8574 on my homebrew DCC throttle. The one that's been working flawlessly for over a year now.

1 Like

@jpfrancis690

When you want to read a keypad via I2C I suggest you use an IC expander which has an built in keypad logic. The SX1509 is such a chip, you will find simple break out modules with the SX1509.

Sparkfun has a nice library for it and a good description.

by the way: I2C is not a wire bus. I don't think you will get a reliable result in your setup with that long I2C wire and all the other components around your installation.

I do not understand the comment of I2C is not a wire bus. Theoretically, one could attach up to 128 devices on the bus due to addressing, but there is also physical limitations, which the primary limiting factor is bus capacitance (limited to 400 pF if I remember correctly). The I2C devices I have researched can only support a small number of configurable addresses, so that is another limitation.

In my use case, the cable I am using is not too long for this application and after further troubleshooting I have determined that the keyboard wiring to the solder pads does not match the desired PCF8574 pin assignment for keypads. I will need to somehow change the keypad mapping so that the correct columns and rows are read properly.

Common usage of I2C is to connect components on one PCB. Not by wire.
I just see your picture and see lot of other components around your I2C wires. If everything works - fine for you. However - I wouldn't rely on that to much.