« Back to home

16 Key Keypad TwinTab

This post describes how to use I2C peripherals with the Clixx.IO system. Although it focuses on using the Raspberry Pi as a host most of the concepts apply to other microcontrollers as well.

It will describe how to set up I2C functionality on the Raspberry Pi, build a Clixx TwinTab with an 8 port IO expander chip interface on it and then show you how to use that to read a generic 16 key telephone keypad. The circuit diagrams and sample code used in this post can be found in my repository so you can duplicate it for yourself.

How I2C Works

The I2C protocol (also know as TWI or two wire interface) is a simple communications protocol designed to allow communications between multiple ICs over a very compact shared serial interface.

I2C Bus

I2C is implemented as a shared bus, allowing multiple devices to exchange data. At any given time there is a single master devices controlling one or more slave devices. Each slave has a unique address on the bus and will only respond to communications sent to it's address.

There are a number of advantages to using I2C, most obviously it minimises the number of pins required on the microcontroller and simplifies the PCB layout. You can use this ability to greatly extend the number of digital IO pins available to your circuit by using an IO expander. For analog inputs it allows you to place the ADC closer to the input source and feed the data to the microcontroller over a digital connection that is less susceptible to noise and other interference.

Another advantage is that it allows you to use more complex ICs that would normally require a large number of interface pins - devices such as the DS1307 real time clock chip. This chip keeps accurate track of the current time, supports alarms and has battery backup support (including charging). To implement this functionality directly on a microcontroller would require a fair amount of external circuitry - with I2C you can treat this like a black box that you simply query for information when you need it.

You can find a more detailed description of how the protocol works at this site. In this post I am going to concentrate on a simple device - the Microchip MCP23008 8 port IO expander and use the WiringPi Python bindings to talk to it.

The MCP23008 IO Expander

The MCP23008 provides a set of 8 bi-directional IO pins that can be configured, read and written over the I2C interface.


Interfacing this to the Clixx.IO I2C TwinTab connectors is very simple as you can see from the schematic to the left. Apart from the two I2C connectors (SDA and SCL) and the 8 IO pins the MCP23008 also provides a number of other pins that need to be connected ...

  1. The RESET pin is active low and is used to reinitialise the chip, in this circuit we simply tie it to VCC with a pull up resistor to keep the chip active.
  2. The INT pin can be used to generate an interrupt signal if one of the input pins changes state. The behaviour of this pin is configured when you configure the chip over I2C, I'll cover the details later in the post.
  3. The address pins (A0, A1 and A2) are used to customise the I2C address of the chip. An I2C address is 7 bits long, the chip uses fixed values for four of the bits in the address and allows you to specify the other three bit by pulling these lines high or low. This allows you to use multiple chips on the same I2C bus by ensuring they each have a separate address. In this circuit they are attached to VCC through pull up resistors and default to '1'. Each address line has a two pin jumper that, when shorted out, will pull the pin to ground and make that bit a '0'.

    The rest of the circuit simply routes power, ground and the 8 IO pins to a 10 pin header that can then be connected to the rest of your circuit.

Building the TwinTab Board

Breadboard Layout

To prototype the circuit and do some basic testing I built a version of it on stripboard. The layout is shown in the image to the right.

To simplify prototyping Clixx.IO boards you can use the template Fritzing projects in my respository. These projects have the connectors in place and the appropriate custom PCB board shape so you can simply copy and rename the file and start work on your own circuit.

Because there is only a single IC and a handful of discrete components this is a relatively easy board to make. Double check the positions of the track cuts and hook up wires before you begin soldering.

One problem I had when I constructed the board was improperly cut tracks - the tool I use to do this is getting quite old now and can leave a small, barely visible trace of copper behind. In this case it was pulling the RESET line to ground, effectively disabling the chip. It might be a good idea to verify the track cuts with a multimeter to ensure they are complete.

Enabling I2C on the Raspberry Pi

I use the Rasbian distribution on my Raspberry Pi so all the examples I give are for that specific version of Linux. If you are using other distributions the instructions should be much the same although the locations for configuration files may be slightly different.

Unfortunately Raspbian has disabled support for I2C (and the similar SPI bus) disabled out of the box so you will have to manually enable it before you can interface to any SPI devices.

Adafruit have a good tutorial about the process. These are the steps you need to take:

First, make sure the modules that support I2C will get loaded at boot time. You need to edit the /etc/modules file ...

 sudo nano /etc/modules

... and add these two lines to the end of the file.

 i2c-bcm2708     i2c-dev

Next, you need to make sure that these modules have not been blocked. Edit the /etc/modprobe.d/raspi-blacklist.conf ...

 sudo nano /etc/modprobe.d/raspi-blacklist.conf

... and change these lines ...

 blacklist spi-bcm2708     blacklist i2c-bcm2708

... to comments by putting a '#' in front of them ...

    #blacklist spi-bcm2708
    #blacklist i2c-bcm2708

 The next step is to install some support software that will make it easier to work with I2C devices from Python and from the command line. Install the *python-sambas* library and the *i2c-tools* package with the following command ...

     sudo apt-get install python-smbus i2c-tools

 Finally, you need to reboot your machine to load the drivers.

# Connecting the TwinTab

 If you have a dock (either the [official one](http://shop.clixx.io/index.php?main_page=product_info&cPath=1&products_id=2) or my homebrew version you can simply insert the TwinTab into the I2C slot. If you are connecting to the Raspberry Pi using hookup cables you should make all the connections with the power disconnected. Make sure you are connected to [the right pins](http://clixx.io/documents/raspberry-pi-pin-mappings.html).

 Once everything is connected and the Pi is turned on you can make sure the device is picked up with the following command:
 pi@picard ~ $ sudo i2cdetect -y 1

Earlier models of the Pi (the original version with 256Mb of RAM) use a different I2C channel so you will need to use the following command instead:

 The result will be a table showing all I2C devices that responded and the address they are set to. The MCP23008 board (with all address lines shorted out) will result in something like the following:
      0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f     00:          -- -- -- -- -- -- -- -- -- -- -- -- --     10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --     20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --     30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --     40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --     50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --     60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --     70: -- -- -- -- -- -- -- --

This shows that it is responding at address 20 (hex). Now you are ready to start using the board.

Connecting to a Keypad

Keypad Circuit

I have a couple of 16 key keypads that I got from eBay, as these have an 8 pin connector they seemed like a good device to test out the interface with. They are also a useful thing to use in various projects so the work done here can be reused later.

The keypad is arranged as a simple matrix with 4 rows and 4 columns with the individual key switches connecting the rows and columns together. The schematic to the right shows this a bit more clearly.

Detecting a Keypress

To read the state of the keys you need to query each row and then check the state of the columns to determine which, if any, keys were pressed. In this case we want to use 4 of the pins on the MCP23008 as outputs to drive the rows and the other 4 pins as inputs to read the columns.

At first glance the obvious solution is to drive each of the row outputs high one at a time and then test which of the columns is high to determine which keys are currently held down. What this doesn't take into account is that the unpressed keys will leave the inputs floating and you won't be able to reliably read the state. What we need to do is to keep the column lines at a high voltage level by pulling them to VCC with a resistor. We then pull each row to ground in turn (while keeping the other rows high) - if a key in the row is pressed it's column will be pulled low and read as a 0, if it's not pressed it will read 1.

We don't need to use any additional circuitry for this - the MCP23008 can do all of this internally.

MCP23008 Configuration Registers

The MCP23008 has 11 registers that are used to control it's operation, read the values currently on the data pins and change the values being output of those pins. For a full reference and detailed explanation of the registers please refer to the datasheet, in this post I'll only look at a subset - just the ones that need to be configured for this example. These are outlined in the table below:

Name|Addr (hex)|bit 7|bit 6|bit 5|bit 4|bit 3|bit 2|bit 1|bit 0 ----|----------|-----|-----|-----|-----|-----|-----|-----|----- IODIR|00|IO7|IO6|IO5|IO4|IO3|IO2|IO1|IO0 IPOL|01|IP7|IP6|IP5|IP4|IP3|IP2|IP1|IP0 GPPU|06|PU7|PU6|PU5|PU4|PU3|PU2|PU1|PU0 GPIO|09|GP7|GP6|GP5|GP4|GP3|GP2|GP1|GP0

The first register, IODIR, is used to set the direction of the pin. If the corresponding bit in the register is set to 1 then the pin becomes an input, if set to 0 the pin is an output. So setting the register to the value 0x0F would make pins 7, 6, 5 and 4 outputs and pins 3, 2, 1 and 0 inputs.

The next register, GPPU, is used to enable or disable the pull up resistor associated with the pin. Setting a bit to 1 will enable the pull up, 0 will disable it.

Using pull up resistors inverts the sense of the logic. The IPOL register allows you to flip it back by changing the polarity of the pin. If the corresponding bit is set the value on the input pin will be inverted, a logic high value on the pin will result in a binary 0 being read for the pin.

The GPIO register is used to read and write values from the pins. Reading this register will give you the values of all of the pins regardless of their direction. If they are set of outputs the value returned will be the last value written. Writing to this register will change the output values of any pins configured as outputs but will not change the state of any inputs.

Sample Code

The following sample code uses the WiringPi I2C functions to control the IO extender. This code is very simple and a lot more verbose than it needs to be to make it a bit clearer what is going on.

Running this program will simply scan the keypad at regular intervals and print out the label of the key that is currently pressed. It does not support multiple keys being pressed at once, it is just for demonstration purposes.

The source for the program is as follows:

    #!/usr/bin/env python
    # Test code for the keyboard interface
    #------------------------------------------------------------------     import wiringpi2     from time import sleep

     KEYCODES = {       '10000111': '1',       '01000111': '2',       '00100111': '3',       '00010111': 'A',       '10001011': '4',       '01001011': '5',       '00101011': '6',       '00011011': 'B',       '10001101': '7',       '01001101': '8',       '00101101': '9',       '00011101': 'C',       '10001110': '*',       '01001110': '0',       '00101110': '#',       '00011110': 'D',       }

     def toBinary(val):       """ Convert a byte to it's binary representation.       """       result = ""       mask = 0x80       for i in range(8):         if val & mask:           result = result + "1"         else:           result = result + "0"         mask = mask >> 1       return result

     if __name__ == "__main__":       wiringpi2.wiringPiSetupPhys()       dev = wiringpi2.wiringPiI2CSetup(0x20)       if dev < 0:         print "ERROR: Could not connect to device!"         exit(1)
      # Set up for our operations       wiringpi2.wiringPiI2CWriteReg8(dev, 0x09, 0x00) # Clear outputs       wiringpi2.wiringPiI2CWriteReg8(dev, 0x00, 0xF0) # Direction       wiringpi2.wiringPiI2CWriteReg8(dev, 0x06, 0xF0) # Pull ups       wiringpi2.wiringPiI2CWriteReg8(dev, 0x01, 0xF0) # Polarity
      # Go into loop       last = 0       while True:         mask = 0x08         for i in range(4):           wiringpi2.wiringPiI2CWriteReg8(dev, 0x09, ~mask & 0x0F)           val = wiringpi2.wiringPiI2CReadReg8(dev, 0x09)           if ((val & 0xF0) <> 0x00) and (val <> last):             print KEYCODES[toBinary(val)]             last = val           mask = mask >> 1

 You need to save this program as *keypad.py* and then run it as follows:
 sudo python keypad.py

As you press keys on the keypad (one at a time please) you will see the values printed out on the console.

Next Steps

The program can be obviously be made more efficient by skipping all the translations to binary strings and use simple logical operations to test if bits are set or not. It should also be extended to support multiple simultaneous key presses.

How you report those key presses is up to you - you could fire events representing key up and key down actions or simply return a 16 bit mask with '1' bits indicating which keys are currently held down.

You might also not use the keypad at all and use the MCP23008 to interface with a completely different system.