Attempting to use any pcf8574 library to work with my 4 x4 keypad

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.

have a look at my library, and check if it fills your needs

2 Likes

I did try to use your library, which led me down this path to having an issue with the key mapping. As you have stated in your README file, the pins P0 - P3 are for rows and the P4 - P7 are for columns, which are true in my case but instead of P0 = Row 1…P3 = Row 4, my keypad is the reverse, and the for the columns P7 = Column 1… P4 = Column 4. How would you change that key map so that when I press the “8” key, the number 8 is displayed on the LCD display?

The library the seller links to has exactly this key mapping. Check the correct connection to the PCF8574 ports.

#include <Keypad.h>

const byte ROWS = 4; //four rows
const byte COLS = 4; //four columns
//define the cymbols on the buttons of the keypads
char hexaKeys[ROWS][COLS] = {
  {'0','1','2','3'},
  {'4','5','6','7'},
  {'8','9','A','B'},
  {'C','D','E','F'}
};
byte rowPins[ROWS] = {3, 2, 1, 0}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {7, 6, 5, 4}; //connect to the column pinouts of the keypad

//initialize an instance of class NewKeypad
Keypad customKeypad = Keypad( makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS); 
// 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**

So is it that you do not realize that with a different row/column order, you need to change the numbers in these two statements, or is it that you want us to somehow imagine the correct order, and tell you what it should be?

In the code that I included, I have it coded like the last part that you have listed. Maybe my question is in library that I was using for the PCF8574 (I have switched from Adafruit_PCF8574 and tried using I2CKeyPad, but got the same result) how do I go about changing the PCF8574 pin assignments. In the README document for I2CKeyPad library the creator states the following;

Which he does say that it may take some effort to get the correct pins connected.

I do not want someone to do the work for me, I just need some guidance on how it is done.

No, I have changed the numbers for the pins to the correct Row and Column numbers and I am still getting errors. I am wondering if there is some coding snippet that I am missing to allow me to change the pin designations on the PCF8574.
I don’t want someone to do the work for me, I just want some guidance.

If it was me, I'd look at the keypad using an ohmmeter, find out what keys relate to what rows and columns that way.
But this should be a clue:


Note that the first and last pins aren't connected, leaving you with C1..C4L1...L4

If that's not what you're looking for, tell us more. If you're still getting wrong keys, perhaps listing all 16 results would help us sort you out. Something like
1-7 2-8 3-9 A-C
4-4 5-5 6-6 B-B
7-1 8-2 9-3 C-A
- 0-0 #-# D-D
would, for example, indicate that rows 1 and 3 are swapped. It gets more complicated when there's a mix of rows and columns swapped, or jumbled.

@jpfrancis690

Regarding the I2CKeyPad library, please run I2Ckeypad_demo01.ino

This sketch has a line ``char keys[] = "123A456B789C*0#DNF"; // N = NoKey, F = Fail

If you run this sketch and presses keys, it shows the index it returns and the translated key.
That shows how the mapping is done.
Please post the output of the sketch with all keys pressed once

Assuming you have mirrored the rows you should replace the line above with another one to match the actual mapping. I expect something like this:

char keys[] = "*0#D789C456B123ANF";

It seems like they just swap places

const uint8_t kbdRowPins[ROWS] = {4,5,6,7}; // PCF8574 pins for rows
const uint8_t kbdColPins[COLS] = {0,1,2,3}; // PCF8574 pins for cols

OR

const uint8_t kbdRowPins[ROWS] = {7,6,5,4}; // PCF8574 pins for rows
const uint8_t kbdColPins[COLS] = {3,2,1,0}; // PCF8574 pins for cols