An interesting problem in getting microprocessors to work in a nice, responsive way is to get them to handle user input properly. For example, in the Adventure game I am working on, I have a row of buttons for the player to press. Unfortunately, sometimes the key-press isn't registered, so it was time to get into interrupts, and port expanders.
There are a few problems with handling button (key) presses:
- You may miss the press altogether, particularly if you use "polling" to check for them. Polling is when you periodically see if the key is down by checking if the pin is high, or low, or whatever indicates a key-press.
- Keys bounce, because they are implemented with springs and bits of metal. If you are not careful then the bouncing will be interpreted as multiple key-presses.
- If you have quite a few buttons (say, 16), then you use up most or all of your input pins on your processor, particularly if you use one pin per button. Even if you use a keyboard "matrix" (where you have 4 rows and 4 columns) then you still need 8 pins (4 for the rows and 4 for the columns). Whilst this is better than 16 pins, it is still a lot.
- Keyboard matrices are fiddly to wire up, and fiddly to test (you have to power each column and then test each row). Also they suffer from "phantom" key-presses, unless you wire diodes in as well. And of course, they don't lend themselves to interrupts because you need to scan the rows and columns when they are pressed, not later on.
- It is fiddly having wires going everywhere to handle all the buttons.
The solution to most of these problems is to use an I/O port expander, like the MCP23017, and interrupts. The port expander gives you 16 inputs, but only uses 2 pins on the Arduino (SDA and SCL), plus ground and power. Using interrupts lets you react to button presses even if you are doing something else when it is pressed. To do this we make use of the MCP23017's ability to generate an interrupt signal, and use one more wire to connect to one of the pins of the Arduino which can generate processor interrupts.
Wiring
My wiring for this project is as follows:
This only has a single switch shown, but I actually wired 16 switches from pins 1 to 8, and 21 to 28, all between the pin and ground.
These are the pinouts for the device:
Configure the MCP23017
There are five major registers of interest on the MCP23017, so let's look at how they are set up to make this work:
First we configure the expander to disable sequential mode, so that when we address a register it just toggles between the "A" and "B" registers. We also enable interrupt mirroring, so we only need to test one interrupt line rather than two.
// expander configuration register
expanderWriteBoth (IOCON, 0b01100000); // mirror interrupts, use sequential mode
To save mucking around with resistors, and to be a bit safer if we misconfigure the MCP23017, let's enable pull-up resistors for the switch port(s):
expanderWriteBoth (GPPUA, 0xFF); // pull-up resistor for switches
Register GPPUA is the GPIO pull-up resistor register. By writing a 1 (to each bit) of that it turns on a pull-up. Thus the port will normally be a 1, until the switch pulls it to 0 when you press it (because that grounds it).
The expanderWriteBoth function makes use of the ability of the MCP23017 to toggle registers, so that by writing twice, it writes to port A, and then port B.
Next, we'll reverse the polarity of that port because it will be 1 when not pressed, and 0 when pressed. To make things look more logical, we reverse it, so we read a 1 when the switch is closed.
expanderWriteBoth (IOPOLA, 0xFF); // invert polarity
Finally we enable interrupts for those pins:
expanderWriteBoth (GPINTENA, 0xFF); // enable interrupts
Now when any pin changes value the MCP23017 will drive the interrupt pin for port B (pin 19) low.
To be on the safe side we will immediately read from the "interrupt capture" register. This clears any interrupt that might be currently present.
// no interrupt yet
keyPressed = false;
// read from interrupt capture port to clear them
expanderRead (INTCAPA);
expanderRead (INTCAPB);
Set up Interrupt Service Routine
Now we are ready to configure the Arduino to react to interrupts on pin D2:
// pin 19 of MCP23017 is plugged into D2 of the Arduino which is interrupt 0
attachInterrupt(0, keypress, FALLING);
The interrupt service routine "keypress", which should be very brief, does this:
void keypress ()
{
digitalWrite (ISR_INDICATOR, HIGH);
keyPressed = true;
}
This merely sets a flag which the main loop can test when it is ready. That tells us that we got an interrupt. The digitalWrite is for debugging, so I can see with the logic analyser when we hit this routine.
Test interrupts
Now let's hook up some debugging connections to the logic analyser and hit the button ...
I put a trigger on the switch being pressed, and immediately you notice the rather impressive switch bounce. That must be about 20 bounces (taking around 4 ms)! That's why you need to debounce. Notice that seemingly at the same instant, the "interrupt" signal is driven low, and the "ISR" (debug signal) is driven high. The interrupt doesn't bounce because it stays active until re-armed by reading the data from the "interrupt capture" register.
Now, zooming in on the exact moment, we can see how long the interrupt took to fire:
You can clearly see that from the moment the switch was pressed, to when the MCP23017 dropped the interrupt pin to low, was only 0.125 microseconds. That's only 125 nanoseconds! Very fast.
Then we see that only 7 microseconds after the keypress the Arduino interrupt routine also fired (we can tell because of the digitalWrite). So there was a very fast response there, even though it was busy doing something else.
Zooming out to get the bigger picture, we can see that the main loop didn't notice the keypress until an entire second had elapsed. No doubt because I had it beavering away doing this:
// some important calculations here ...
for (i = 0; i < 0x7FFFF; i++)
{}
That is a loop of 524288 iterations, supposed to simulate the processor being busy (eg. updating your LCD screen).
You can also see another batch of switch bounces, circled. That switch certainly had a field day of it. That's 100 ms (1/10 of a second) after it was pressed! In fact, it looks like the second lot of bounces was when I released the switch. That would be right, because when it was released it would have jittered back and forwards a bit until settling down.
Once the main loop noticed the keypress it calls handleKeypress to find out what it was.
This does the following:
- Drop the "ISR" pin (point "A") and sets the keyPressed flag back to false, so we can detect a future keypress.
- For each port, reads from the MCP23017's "interrupt flag" register. This is so we know if this register was responsible for the interrupt or not. If we don't do this we may get an old value from a previous interrupt. These reads are not shown in the logic analyzer capture as I added them later on.
- Then, for each port, if required, read from the MCP23017's "interrupt capture" register. The purpose of this register is to remember the state of the pins the moment the interrupt occurred, even if they have changed since then. This is just as well, because you can see that by the time we noticed the keypress the key had been released again. Notice that we read 0x01 which shows that the switch on pin 1 was closed.
// Read port values, as required. Note that this re-arms the interrupts.
if (expanderRead (INFTFA))
{
keyValue &= 0x00FF;
keyValue |= expanderRead (INTCAPA) << 8; // read value at time of interrupt
}
if (expanderRead (INFTFB))
{
keyValue &= 0xFF00;
keyValue |= expanderRead (INTCAPB); // port B is in low-order byte
}
[EDIT] Code changed from earlier version to only alter keyValue if you get an interrupt for that "side" (the A side or the B side).
- The moment that the "interrupt capture" register is read the MCP23017 cancels the interrupt signal (raises it back to 1) at point "B". This effectively re-arms the interrupts for another time.
- The code then turns on the on-board LED (point "C") thus giving a visual confirmation we noticed the key-press.
Example code
The full code in the test program was:
// Author: Nick Gammon
// Date: 19 February 2011
// Demonstration of an interrupt service routine connected to the MCP23017
#include <Wire.h>
// MCP23017 registers (everything except direction defaults to 0)
#define IODIRA 0x00 // IO direction (0 = output, 1 = input (Default))
#define IODIRB 0x01
#define IOPOLA 0x02 // IO polarity (0 = normal, 1 = inverse)
#define IOPOLB 0x03
#define GPINTENA 0x04 // Interrupt on change (0 = disable, 1 = enable)
#define GPINTENB 0x05
#define DEFVALA 0x06 // Default comparison for interrupt on change (interrupts on opposite)
#define DEFVALB 0x07
#define INTCONA 0x08 // Interrupt control (0 = interrupt on change from previous, 1 = interrupt on change from DEFVAL)
#define INTCONB 0x09
#define IOCON 0x0A // IO Configuration: bank/mirror/seqop/disslw/haen/odr/intpol/notimp
//#define IOCON 0x0B // same as 0x0A
#define GPPUA 0x0C // Pull-up resistor (0 = disabled, 1 = enabled)
#define GPPUB 0x0D
#define INFTFA 0x0E // Interrupt flag (read only) : (0 = no interrupt, 1 = pin caused interrupt)
#define INFTFB 0x0F
#define INTCAPA 0x10 // Interrupt capture (read only) : value of GPIO at time of last interrupt
#define INTCAPB 0x11
#define GPIOA 0x12 // Port value. Write to change, read to obtain value
#define GPIOB 0x13
#define OLLATA 0x14 // Output latch. Write to latch output.
#define OLLATB 0x15
#define port 0x20 // MCP23017 is on I2C port 0x20
#define ISR_INDICATOR 12 // pin 12
#define ONBOARD_LED 13 // pin 13
volatile bool keyPressed;
// set register "reg" on expander to "data"
// for example, IO direction
void expanderWriteBoth (const byte reg, const byte data )
{
Wire.beginTransmission (port);
Wire.write (reg);
Wire.write (data); // port A
Wire.write (data); // port B
Wire.endTransmission ();
} // end of expanderWrite
// read a byte from the expander
unsigned int expanderRead (const byte reg)
{
Wire.beginTransmission (port);
Wire.write (reg);
Wire.endTransmission ();
Wire.requestFrom (port, 1);
return Wire.read();
} // end of expanderRead
// interrupt service routine, called when pin D2 goes from 1 to 0
void keypress ()
{
digitalWrite (ISR_INDICATOR, HIGH); // debugging
keyPressed = true; // set flag so main loop knows
} // end of keypress
void setup ()
{
pinMode (ISR_INDICATOR, OUTPUT); // for testing (ISR indicator)
pinMode (ONBOARD_LED, OUTPUT); // for onboard LED
Wire.begin ();
Serial.begin (115200);
Serial.println ("Starting ...");
// expander configuration register
expanderWriteBoth (IOCON, 0b01100000); // mirror interrupts, disable sequential mode
// enable pull-up on switches
expanderWriteBoth (GPPUA, 0xFF); // pull-up resistor for switch - both ports
// invert polarity
expanderWriteBoth (IOPOLA, 0xFF); // invert polarity of signal - both ports
// enable all interrupts
expanderWriteBoth (GPINTENA, 0xFF); // enable interrupts - both ports
// no interrupt yet
keyPressed = false;
// read from interrupt capture ports to clear them
expanderRead (INTCAPA);
expanderRead (INTCAPB);
// pin 19 of MCP23017 is plugged into D2 of the Arduino which is interrupt 0
attachInterrupt(0, keypress, FALLING);
} // end of setup
// time we turned LED on
unsigned long time = 0;
unsigned int keyValue = 0;
// called from main loop when we know we had an interrupt
void handleKeypress ()
{
delay (100); // de-bounce before we re-enable interrupts
keyPressed = false; // ready for next time through the interrupt service routine
digitalWrite (ISR_INDICATOR, LOW); // debugging
// Read port values, as required. Note that this re-arms the interrupts.
if (expanderRead (INFTFA))
{
keyValue &= 0x00FF;
keyValue |= expanderRead (INTCAPA) << 8; // read value at time of interrupt
}
if (expanderRead (INFTFB))
{
keyValue &= 0xFF00;
keyValue |= expanderRead (INTCAPB); // port B is in low-order byte
}
Serial.println ("Button states");
Serial.println ("0 1");
Serial.println ("0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5");
// display which buttons were down at the time of the interrupt
for (byte button = 0; button < 16; button++)
{
// this key down?
if (keyValue & (1 << button))
Serial.print ("1 ");
else
Serial.print (". ");
} // end of for each button
Serial.println ();
// if a switch is now pressed, turn LED on (key down event)
if (keyValue)
{
time = millis (); // remember when
digitalWrite (ONBOARD_LED, HIGH); // on-board LED
} // end if
} // end of handleKeypress
void loop ()
{
// was there an interrupt?
if (keyPressed)
handleKeypress ();
// turn LED off after 500 ms
if (millis () > (time + 500) && time != 0)
{
digitalWrite (ONBOARD_LED, LOW);
time = 0;
} // end if time up
} // end of loop
Conclusion
The example presented shows how you can notice a key-press on up to 16 buttons, even if your code was doing some very CPU-intensive work.
All this is done with a minimal number of wires connected between where the buttons are and the Arduino (ground, +5V, SDA, SCL and the interrupt line).
You could handle more than 16 switches by using more than one MCP23017, see next post.
More information
More information about I2C is here:
http://www.gammon.com.au/forum/?id=10896
My article about hooking up a graphics LCD screen using I2C is here:
http://www.gammon.com.au/forum/?id=10940
Information about keyboard matrices, diodes and ghosting:
http://www.dribin.org/dave/keyboard/one_html/
Microchip's MCP23017 device:
http://www.microchip.com/wwwproducts/Devices.aspx?dDocName=en023499
[EDIT] Amended on 20th February 2011 to test all 16 switches, and have a more sophisticated key-press detection routine. This shows in the serial output which button has been pressed. |