Keypad decoder IC

I've got it working now. Was a combination of using the wrong address for the PCF8574; it's a PCF8574AP, so the address for A0, A1, & A2 all grounded is 0x38. Plus had the columns and rows for the keypad connected to the wrong P0:P7 pins of the PCF8574AP. Currently just using a sparkfun 4x3 keypad to get it working to start with.

I have a 220ohm resistor between the column pins of the keypad and the PCF8574AP connected to Vcc.

Here's the current code:

Display.h:

class LedControl;


class CDisplay
{
  public:
    CDisplay(const int dataPin, const int clkPin, const int csPin, const int numDevices);
    ~CDisplay();

    void Init();
    void ShowNumber(int deviceId, unsigned long number);
    void ClearDisplay(int deviceId);

  private:
    void printNumber(int addr, unsigned long number);
    unsigned int didgitCount(const unsigned long num, const unsigned int max);
    
    const int m_nDataPin;
    const int m_nClkPin;
    const int m_nCsPin;
    const int m_nNumDevices;
    LedControl* m_pLC;
};

Display.cpp:

#include "Display.h"
#include <LedControl.h>


CDisplay::CDisplay(int dataPin, int clkPin, int csPin, int numDevices)
  : m_nDataPin(dataPin)
  , m_nClkPin(clkPin)
  , m_nCsPin(csPin)
  , m_nNumDevices(numDevices)
{
  m_pLC = new LedControl(dataPin, clkPin, csPin, numDevices);
}


CDisplay::~CDisplay()
{
    delete m_pLC;
}


void CDisplay::Init()
{
    for (int addr = 0; addr < m_pLC->getDeviceCount(); ++addr)
    {
        // Initialize the module 
        m_pLC->shutdown(addr , false);
        // display brightness adjustment 
        m_pLC->setIntensity(addr , 0x06);
    }

    for (int num = 1; num < 10; ++num)
    {
        for (int addr = 0; addr < m_pLC->getDeviceCount(); ++addr)
        {
            ShowNumber(addr+1, num);
        }

        delay(500);
    }

    for (int addr = 0; addr < m_pLC->getDeviceCount(); ++addr)
    {
        m_pLC->clearDisplay(addr);
    }
}


void CDisplay::ShowNumber(int deviceId, unsigned long number)
{
  printNumber(deviceId-1, number);
}


void CDisplay::ClearDisplay(int deviceId)
{
  m_pLC->clearDisplay(deviceId-1);
}


void CDisplay::printNumber(int addr, unsigned long number)
{
  unsigned int digits = didgitCount(number, 8);
  // Calculate the value of each digit 
  int digit1 = number % 10 ;
  int digit2 = (number / 10)% 10 ;
  int digit3 = (number / 100)% 10 ;  
  int digit4 = (number / 1000)% 10 ;
  int digit5 = (number / 10000)% 10 ;
  int digit6 = (number / 100000)% 10 ;
  int digit7 = (number / 1000000)% 10 ;
  int digit8 = (number / 10000000)% 10 ;
  
  // Display the value of each digit in the display 
  switch (digits)
  {
    case 8:
      m_pLC->setDigit(addr, 7, (byte)digit8, false);
    case 7:
      m_pLC->setDigit(addr, 6, (byte)digit7, false);
    case 6:
      m_pLC->setDigit(addr, 5, (byte)digit6, false);
    case 5:
      m_pLC->setDigit(addr, 4, (byte)digit5, false);
    case 4:
      m_pLC->setDigit(addr, 3, (byte)digit4, false);
    case 3:
      m_pLC->setDigit(addr, 2, (byte)digit3, false);
    case 2:
      m_pLC->setDigit(addr, 1, (byte)digit2, false);
    case 1:
      m_pLC->setDigit(addr, 0, (byte)digit1, false);
  }
}


//
//  Returns the number of digits that need to
//  be lit up on the LED, to show num
//
unsigned int CDisplay::didgitCount(const unsigned long num, const unsigned int max)
{
  if (num == 0 || num == 1) return 1;
  if (num == 10) return 2;
    
    unsigned int count(max);

    for (; count > 0; --count)
    {
        if (pow((double)10, (double)(count - 1)) < num)
            break;
    }

    return count;
}

Keypad.ino:

/*
 * Test program for PCF8574AP I2C I/O expander
 * - for use with 4 x 3 keypad.
 * 
 * Sparkfun keypad (pin row or column):
 * 7    6   5   4   3   2   1
 * R1   R2  C2  R3  C0  R0  C1
 * 
 * On port-expander (port - row or column):
 * P7 P6  P5  P4  P3  P2  P1  P0
 * -  R3  R2  R1  R0  C2  C1  C0
 * 
 */

 #include <Wire.h>
 #include "Display.h"

 #define DIN_PIN       5
 #define CS_PIN        6
 #define CLK_PIN       7
 #define NUM_DISPLAYS  1

 int m_nRow, m_nColumn;

 char m_keymap[4][3] = {{'1','2','3'},{'4','5','6'},{'7','8','9'},{'*','0','#'}};

 CDisplay m_display(DIN_PIN, CLK_PIN, CS_PIN, NUM_DISPLAYS);

 #define m_addr 0x38  // Address with three address pins grounded.

 void setup()
 {
    Wire.begin();
    Serial.begin(9600);
    m_display.Init();
 }

 void loop()
 {
    char key = getKey();

    if (key >= 0x30 && key <= 0x39)
    { // numeric
      long num = key - 0x30;
        m_display.ShowNumber(1, num);
    }
    
    delay(300);
 }

 char getKey()
 {
    char key = '\0';
    byte send_pattern, receive_pattern,test_pattern; 
    byte send_pattern_array[]={B11110111, B11101111, B11011111, B10111111};
    byte test_pattern_array[]={B00000001, B00000010, B00000100};
    int i=0;

    for (i=0; i<4;i++)
    {
        // Try each row. Send 0 on R1 port, the bit on the pressed column
        // will turn from 1 to 0 
        send_pattern = send_pattern_array[i];
        expanderWrite(send_pattern);
        receive_pattern=expanderRead();
    
        if (send_pattern != receive_pattern)
        {
            m_nRow = i;
      
            for (int j = 0; j < 3; j++)
            {
              test_pattern = test_pattern_array[j] & receive_pattern;
              Serial.println(btoa(test_pattern));
          
              if(test_pattern == 0)
                m_nColumn = j;
            }
      
            Serial.print("key pressed: row:");
            Serial.print(m_nRow);
            Serial.print(" column: ");
            Serial.print(m_nColumn);
            Serial.println();

            key = m_keymap[m_nRow][m_nColumn];

            Serial.print("key: ");
            Serial.println(key);
        }
    }

    return key;
 }

 char* btoa(int i)
 {
    static char zeros[] = "00000000";
    char tmpstr[9];

    for (int i = 0; i < 8; i++)
    {
      zeros[i] = '0';
    }

    tmpstr[0] = '\0';

    itoa(i, tmpstr, 2);
    zeros[8 - strlen(tmpstr)] = '\0';
    strcat(zeros, tmpstr);

    return zeros;
 }

 void expanderWrite(byte data)
 {
    Wire.beginTransmission(m_addr);
    Wire.write(data);
    Wire.endTransmission();
 }

 byte expanderRead()
 {
    byte data;

    Wire.requestFrom(m_addr, 1);
    
    if (Wire.available())
    {
        data = Wire.read();
    }

    return data;
 }

I'll adapt the code to work with my 5x3 keypad later. But I've got it working.

However, it doesn't always capture the key press. Maybe I should adapt the code to get the key pressed on an event if that's possible?

The pcf chip doesn't have an interrupt pin as far as I remember, unfortunately. Some possible causes & tips that should speed things up significantly & avoid losing presses:

1.. When you try the 5x3 keypad, scan by column instead of row. Only 3 write-read operations instead of 5.

  1. Remove any debugging Serial.print()s once you have the code working. They will cause significant slowdown, especially at 9600 baud. In the meantime try 115200 baud.

  2. Increase the i2c clock speed from the standard 100MHz to 400MHz. Google for the necessary function.

  3. As I mentioned way back, you can do a "quick scan" to see if any keys are pressed. If none are pressed, don't bother with the full scan. For this, send the binary pattern to pull all rows low together. Then read the column data back. If all columns are 1, you know that no keys are pressed.

  4. You don't need those 220R. When you write a 1 to the pcf, it is like INPUT_PULLUP on an Arduino pin. You won't cause a short by connecting it to ground.

I've improved the code as follows:

/*
 * Test program for PCF8574AP I2C I/O expander
 * - for use with 4 x 3 keypad.
 * 
 * Sparkfun keypad (pin row or column):
 * 7    6   5   4   3   2   1
 * R1   R2  C2  R3  C0  R0  C1
 * 
 * On port-expander (port - row or column):
 * P7 P6  P5  P4  P3  P2  P1  P0
 * -  R3  R2  R1  R0  C2  C1  C0
 * 
 */

 #include <Wire.h>
 #include "Display.h"

 #define DIN_PIN       5
 #define CS_PIN        6
 #define CLK_PIN       7
 #define NUM_DISPLAYS  1

 char m_keymap[4][3] = {{'1','2','3'},{'4','5','6'},{'7','8','9'},{'*','0','#'}};

 CDisplay m_display(DIN_PIN, CLK_PIN, CS_PIN, NUM_DISPLAYS);

 #define m_addr 0x38  // Address with three address pins grounded.

void setup()
{
    Wire.begin();
    Serial.begin(9600);
    m_display.Init();
}

void loop()
{
    char key = getKey();

    if (key >= 0x30 && key <= 0x39)
    { // numeric
      long num = key - 0x30;
        m_display.ShowNumber(1, num);
    }

    delay(200);
}

 char getKey()
 {
      char key('\0');
      bool key_pressed(false), found(false);
      byte row_checks[]={B00001000, B00010000, B00100000, B01000000};
      byte col_checks[]={B00000001, B00000010, B00000100};

      pcfWrite(B00000111);
      byte receive = pcfRead();
      int col(-1);

      for (; col < 3 && !key_pressed; col++)
      {
          key_pressed = (receive & col_checks[col+1]) == 0;
      }

      if (key_pressed)
      {
          pcfWrite(B11111000);
          receive = pcfRead();
          
          for (int row = 0; row < 4 && !found; row++)
          {
              if ((receive & row_checks[row]) == 0)
              {           
                  found = true;
                  key = m_keymap[row][col];
              }
          }
      }

      return key;
 }

 char* btoa(int i)
 {
    static char zeros[] = "00000000";
    char tmpstr[9];

    for (int i = 0; i < 8; i++)
    {
      zeros[i] = '0';
    }

    tmpstr[0] = '\0';

    itoa(i, tmpstr, 2);
    zeros[8 - strlen(tmpstr)] = '\0';
    strcat(zeros, tmpstr);

    return zeros;
 }

 void pcfWrite(byte data)
 {
    Wire.beginTransmission(m_addr);
    Wire.write(data);
    Wire.endTransmission();
 }

 byte pcfRead()
 {
    byte data;

    Wire.requestFrom(m_addr, 1);
    
    if (Wire.available())
    {
        data = Wire.read();
    }

    return data;
 }

I located this thread on increasing the I2C speed I couldn't locate the Wire.o and twi.o files on my PC to delete them to force a re-compile.

Is it not possible to use the INTerupt pin on the PCF; P13, to know when a key has been pressed?

It's just

Wire.setClock(400000);

Sorry, for some reason I thought the 8475 didn't have an INT pin. Not sure you need it anyway with the other suggestions. But you could use it to implement my suggestion #4.

Which suggestions did you implement yet?

I refactored the getKey() function, only two write/reads now. See code in my last msg.

If you set the I2C to 400MHz, is it dependant on the CPU running at 16MHz? As I'll eventually have the project running standalone and was considering running at 8MHz so as not to need an external crystal.

Two read-writes? Clever! Can you explain the logic?

An i2c clock of 400KHz is still 20 X slower than the CPU clock of 8MHz, so I would have thought that would be ok. But test it, of course.

First check to see if any key on a column has been pressed, by sending 111 to pcf read the result, then do a bitwise AND with the result against 1 for column#0, if not == 0 check for column#1 with bitwise AND with the result against 10, if not == 0 check for column#2 with bitwise AND with result against 100. Same for checking for rows with different binary values.

Checking for columns with 111 send, read result, if result was 110 then 110 bitwise AND 1 == 0, so COL#0 was pressed.

Hence you only need one write/read to locate column on which key was pressed and one more write/read to locate the row.

Get it?

I'll have to see if I can get it down to just one write/read. Thinking about it I think it must be possible. I'm not at my PC and Arduino so can't check. Probably can do with sending B11111111 and read result. Then check for column pressed against first three bits, and for rows against last five bits.

Yes, I get it. You're pretty good at this. Now I can't understand why you ever used those old decoder chips!

Your idea works great as long as only one button is ever pushed at once, which will probably be the case. If you wanted to deal with 2-key-rollover, you would need to do a scan, but I guess you don't need that.

I don't think your single read idea would work. With all columns and rows at 5V, no current can follow when a button is pressed, so when you read back, everything will still be "1".

Not done any work in the past week, been away. I've been playing with it today to try and improve the key detection by using the PCF interrupt. I connected a 4.7K resistor between Vcc and the PCF P#13, between the resistor and the P#13 I've connected the Adrduino Pin#2.

This is now the code:

/*
 * Test program for PCF8574AP I2C I/O expander
 * - for use with 4 x 3 keypad.
 * 
 * Sparkfun keypad (pin row or column):
 * 7    6   5   4   3   2   1
 * R1   R2  C2  R3  C0  R0  C1
 * 
 * On port-expander (port - row or column):
 * P7 P6  P5  P4  P3  P2  P1  P0
 * -  R3  R2  R1  R0  C2  C1  C0
 * 
 */

 #include <Wire.h>
 #include "Display.h"

 #define DIN_PIN       5
 #define CS_PIN        6
 #define CLK_PIN       7
 #define NUM_DISPLAYS  1

 char m_keymap[4][3] = {{'1','2','3'},{'4','5','6'},{'7','8','9'},{'*','0','-'}};

 CDisplay m_display(DIN_PIN, CLK_PIN, CS_PIN, NUM_DISPLAYS);

 #define m_addr 0x38  // Address with three address pins grounded.

 long m_number;
 bool m_ISR_change;

 unsigned long prevMillis = millis();

 byte row_checks[]={B00001000, B00010000, B00100000, B01000000};
 byte col_checks[]={B00000001, B00000010, B00000100};

void setup()
{
    //Wire.setClock(400000);
    Wire.begin();
    Serial.begin(9600);
    m_display.Init();

    pcfWrite(B11111000);

    pinMode(2, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(2), ISRoutine, CHANGE);

    m_number = 0;
    m_ISR_change = false;
}

void loop()
{
    byte data = pcfRead();

    if (m_ISR_change)
    {
        m_ISR_change = false;
        Serial.println("Interrupt");

        char key = getKey();

        if (key >= 0x30 && key <= 0x39)
        { // numeric
           int digit = key - 0x30;
    
           m_number *= 10;
           m_number += digit;
        }
        else if (key == '-')
        {
            m_display.ClearDisplay(1);
            m_display.ShowNumber(1, m_number);
            m_number = 0;
        }
    }
}

void ISRoutine()
{
    if (millis() > prevMillis + 250)
    {
        m_ISR_change = true;
        prevMillis = millis();
    }
}

 char getKey()
 {
      char key('\0');
      bool key_pressed(false), found(false);
      
      pcfWrite(B00000111);
      byte receive = pcfRead();
      int col(-1);

      for (; col < 3 && !key_pressed; col++)
      {
          key_pressed = (receive & col_checks[col+1]) == 0;
      }

      if (key_pressed)
      {
          pcfWrite(B11111000);
          receive = pcfRead();
          
          for (int row = 0; row < 4 && !found; row++)
          {
              if ((receive & row_checks[row]) == 0)
              {           
                  found = true;
                  key = m_keymap[row][col];
                  Serial.print("Key: ");
                  Serial.println(key);
              }
          }
      }

      return key;
 }

 char* btoa(int i)
 {
    static char zeros[] = "00000000";
    char tmpstr[9];

    for (int i = 0; i < 8; i++)
    {
      zeros[i] = '0';
    }

    tmpstr[0] = '\0';

    itoa(i, tmpstr, 2);
    zeros[8 - strlen(tmpstr)] = '\0';
    strcat(zeros, tmpstr);

    return zeros;
 }

 void pcfWrite(byte data)
 {
    Wire.beginTransmission(m_addr);
    Wire.write(data);
    Wire.endTransmission();
 }

 byte pcfRead()
 {
    byte data;

    Wire.requestFrom(m_addr, 1);
    
    if (Wire.available())
    {
        data = Wire.read();
    }

    return data;
 }

It works quite well now and much more responsive now. Do you think I should put any capacitors to smooth the circuit out between the gnd pins and ground?

I found that it didn't work properly until I hit upon the idea of putting a pcfRead() at the beginning of the loop, can you explain why that would be? Without that it would read a column and then on only read that one column, it didn't matter which column you initially pressed a key.

Caps between ground and ground? Not sure what you mean? You should have a bypass cap (0.1uF) close to the Vcc & ground pins of the chip.

Any variables used in an interrupt routine should be declared "volatile".