Expanding I/O with the I/O Expander

Expanding I/O with the I2C I/O Expander

Introduction

In this project we will use the I/O Expander IC (MCP23008) to control a seven-segment display as a counter. We will also utilize interrupts to start and stop the counter. We will introduce I2C, which is a communication protocol that the MCP23008 uses.

Before you begin, you should:
After you're done, you should:
  • Understand the basics of I2C
  • How the I/O expander can make using a seven-segment display easier.
  • How interrupts are handled by the I/O expander.

Inventory:

Qty Description Typical Image Schematic Symbol Breadboard Image
1 Button
2 220 Ω Resistor
2 10k Ω Resistor
1 LED
1 I2C I/O Expander (MCP23008-E/P)
1 4 Digit 7 Segment Display

I2C basics

I2C is a communication protocol, like UART and SPI, which uses two wires and allows for multiple masters and slaves. However, unlike both UART and SPI, I2C is only half duplex (it can only send or receive, but not both at the same time).

One of the distinctive characteristics that separates I2C from UART or SPI is that it uses two bidirectional lines with pull-up resistors (both lines are held to VCC by the pull-up resistors and brought down to ground by the device sending data). Figure 1 shows an example I2C network with a master and two slaves, and the pull resistors (RP) to pull the lines up to VCC. All the devices on the network connect to these two lines. However, there is a limit to the number of devices on a single network.

Figure 1. Basic I2C Wiring Diagram

The two lines are SDA and SCL, which are the Serial Data Line and Serial Clock. By default, both lines are held high by the pull-up resistors. I2C is like UART in that each transmission requires a start bit and a stop bit. Unlike both UART and SPI, I2C devices are required to have an address. This is because the two wires are connected to all of the devices, and an address allows a master to specify the slave it is talking to. Figure 2 shows a short animation of an I2C master reading data from one of the two slaves. It starts by sending the slave's address and whether it is reading (R) or writing (W), and once the slave acknowledges that it was the one specified it then sends the data which the master will read and the other slave will ignore.

Figure 2. Short Animation of I2C Read from Slave.

While Fig. 2 shows a simplified animation of how a master reads data from a slave device, Fig. 3 shows the general bit frame of an I2C message. The message is started by pulling SDA LOW while SCL is HIGH, and stopping the message is done by letting SDA go from LOW to HIGH while SCL is HIGH. Also, after every byte that is transferred (including the slave address + R/W bit) there is an ACK bit which the slave pulls LOW if the transfer was successful (or if the slave saw that the master was addressing it).

Figure 3. I2C data packet.

The master can send a second (or more) start bit to change the mode from read to write or vice versa. The I2C bit frame doesn't end until the stop signal is sent, and until the stop signal is sent or some data goes unacknowledged, either the master or the slave should continue to transmit data.

Seven-Segment Basics

The seven-segment display is used when a simple numeric display is needed, a good example is a digital clock. It gets the name seven-segment display from the fact that it has seven separate segments (for every digit) that can be turned on and off to form a character.

The 4 digit seven-segment display included in the parts kit uses a common anode layout, which means that each digit has its own anode that must be pulled high to enable. The schematic of the 4 digit seven-segment display is depicted in Fig. 4 so that you can see how the LEDs are oriented with respect to the pins. All of the digits share the same pins which light the segments (by pulling the segment pin low).

Figure 4. 4 Digit seven-segment display schematic.

I/O Expander Interface

Before we wire up the circuit, we should talk about what the individual pins do on the I/O expander. This will provide a basic understanding for why the pins are connected the way they are.

Figure 5. MCP23008 pinout.
  • SCL: This pin is the clock line for I2C.

  • SDA: This pin is the data line for I2C.

  • A2, A1, A0: These pins are used to configure the last three bits of the slave address this device uses, which allows up to 8 different MCP23008 I/O expanders to be used on the same I2C network.

  • RESET: This pin resets the chip whenever it is pulled low. Thus should be always held high in most use cases.

  • INT: This pin is an output pin that changes when an interrupt is triggered in the chip (provided that interrupts are enabled). This pin can be configured to go high or go low when an interrupt is triggered.

  • VSS: This pin is the chip's connection to ground.

  • GP 0-7: These pins are the general purpose I/O pins which can be configured to be input or output. More information on how to use and configure these pins are below in the programming section.

  • VDD: This pin is the chip's connection to the voltage supply.

Step 1: Assembling the circuit

The pins for I2C depend on the board that is being used. This project assumes that you are using a chipKIT™ Uno32™ or uC32™, both of which have the same pins and pin configuration. The chipKIT Max32™ has pins for I2C that do not require setting jumpers (pin 21–SCL and pin 20–SDA). So for the Uno32 and uC32, we must change two jumpers to enable I2C. The jumpers are JP6 and JP8, which are next to each other near the analog pins. The jumpers will be changed to RG3 for JP6 and RG2 for JP8, which means that jumpers move towards the analog pins.

Figure 6. I/O expander and seven-segment circuit.

Assembly Steps:

  1. Place the I/O Expander (MCP23008) on the board bridging the valley.
  2. Place the button so that 2 rows are between the button and the I/O expander.
  3. Place the seven-segment display so that 2 rows separate it from the button.
  4. Attach ground from the chipKIT board to the ground rail of the breadboard and the 5V from the chipKIT board to the voltage rail of the breadboard.
  5. Connect both the top and bottom voltage rails together.
  6. Connect one leg of the button to the voltage rail.
  7. Connect the other leg of the button on the same side to ground through a 220 Ω resistor.

For these following instructions, the pin numbering for the I/O expander and seven-segment display are depicted in Fig. 7 to help with the wiring. There is overlap for both the I/O expander and seven-segment display since we connect the I/O expander to the seven-segment display.

Figure 7. Pin numbers for I/O expander and seven-segment display.

Connections for the I/O Expander Pins:

For this section, the step number is the pin number that is being connected. For example, the third step in the list below is to connect pin 3 to ground.

  1. Connect to A5 of the chipKIT board. Place a 10 kΩ resistor from pin 1 to pin 6.
  2. Connect to A4 of the chipKIT board. Place a 10 kΩ resistor from pin 2 to pin 6.
  3. Connect to ground.
  4. Connect to ground.
  5. Connect to ground.
  6. Connect to voltage rail.
  7. Not connected to anything.
  8. Connect to pin 2 of the chipKIT board and place the anode (longer end) of the LED here and place the short end (cathode) into an empty row. In the same row as the LED's cathode, place a 220 Ω resistor to ground.
  9. Connect to ground.
  10. Connect to pin 11 of the seven-segment display.
  11. Connect to pin 7 of the seven-segment display.
  12. Connect to pin 4 of the seven-segment display.
  13. Connect to pin 2 of the seven-segment display.
  14. Connect to pin 1 of the seven-segment display.
  15. Connect to pin 10 of the seven-segment display.
  16. Connect to pin 5 of the seven-segment display.
  17. Connect to the leg of the button on the opposite of the leg with a resistor.
  18. Connect to voltage rail.

Connections for Seven-Segment Display Pins:

The next list is the done the same way as the one above, but for the seven-segment display instead of the I/O Expander.

  1. Connected to pin 14 of I/O Expander.
  2. Connected to pin 13 of I/O Expander.
  3. Not connected
  4. Connected to pin 12 of I/O Expander.
  5. Connected to pin 16 of I/O Expander.
  6. Connected to voltage rail through a 220 Ω resistor.
  7. Connected to pin 11 of I/O Expander.
  8. Not connected.
  9. Not connected.
  10. Connected to pin 15 of I/O Expander.
  11. Connected to pin 10 of I/O Expander.
  12. Not connected.

I/O Expander Registers

Before we start programming, we must talk about the I/O expander's registers. A register is a quick access memory that can be used to interact with hardware. The CPU uses registers to keep track of where it is in a program (technically, the next instruction in the program's machine code) as well as registers for temporary memory and hardware interaction. The I/O expander has registers to control its output and input pins as well as configure the behavior of the I/O expander.

There are 11 registers in the I/O expander. We will only need to configure a few for this project, but we cover each register in the link to the right. The registers we will be using in this project are the IODIR, GPINTEN, IOCON, INTCAP, and GPIO, which are covered below.

  • IODIR: The IODIR register is used to configure the direction of the GP pins on the I/O expander. Where for each bit, a '1' means input for the corresponding pin and a '0' means output for the corresponding pin. Whenever the I/O expander is reset, this register goes to its default of 255 (0xFF, 0b1111 1111). This means that on reset, all of the pins are set to input.

  • GPINTEN: The GPINTEN register is used to enable interrupts on input pins. Any bit that is '1' enables the corresponding pin to trigger an interrupt if it is an input and the logic level changes (for both rising and falling edges).

  • IOCON: The IOCON register is used to configure aspects of the device. Such as whether the device acts in sequential operation mode, which is described below. It also controls whether or not slew rate is enabled or disabled. If enabled, it allows the I/O expander to control the slew rate of the data line when driving from a high to a low. We can also choose to make the INT pin be an open-drain output or allow the INTPOL bit to determine polarity of INT pin. The INTPOL bit sets whether the INT pin gets set to high or low when an interrupt is triggered.

  • INTCAP: The INTCAP register stores the value of the pin that triggers an interrupt. In the case where an interrupt is triggered and a second one is triggered before the first interrupt is cleared. INTCAP only captures the state of the first interrupt, but INTF will record the interrupts that were triggered.

  • GPIO: This register is the interface we use to read from or write to the GP pins.

Configuring the I/O Expander

Before we can use the I/O expander to display numbers and trigger an interrupt when a button is pressed, we must configure its registers. The first byte in a message to the I/O expander is the register to be read or written to, and what happens after the first byte depends on the configuration. The default configuration for each byte transferral is done by auto incrementing after every byte. Figure 8 displays this behavior, where the I2C message sent has multiple bytes. The first byte of data sent is the register to modify and the following byte is written to that register. Every byte after that is in the next register, i.e., data 1 goes to register 2 and then data 2 goes to register 3. Reading occurs the same way; after each byte, the I/O expander sends the contents of the next register.

Figure 8. I2C Transmission of multiple bytes to I/O Expander.

We can disable this default behavior so that the register being read or written to stays on that register. This allows us to utilize input polling through the I/O expander (even though it won't be used in this project). We can leave this feature enabled because we aren't doing any input polling, but we will change it anyway since it will be good practice for configuration bit patterns (setting or clearing specific bits for configuration).

The first and most important configuration we must do is the IODIR register configuration. All GP pins are input by default, thus if we don't configure any outputs, we won't see anything displayed on the seven-segment display. We want all but the last pin to be outputs, and the last pin to be an input. Since 1=input and 0=output, then the bit pattern we want is 0b1000 0000. We will rewrite this in hexadecimal to reduce the possibility of mistyping the binary number while still being able to easily determine which bits are set. The hex number for our configuration of IODIR is 0x80.

The next configuration is to enable the interrupt on our input pin. Since it requires a '1' to be enabled, we can just use the previous hex number for GPINTEN. So the hex number for GPINTEN is 0x80.

The last configuration is for the IOCON register. This register is a special register where each bit is a different configuration option. The bits we want to set are bit 5 and bit 1. So the bit pattern will be 0b0010 0010, which as a hexadecimal is 0x22. Thus, we will set the IOCON register to 0x22.

We now have finished what needs to be done to configure the I/O expander. The next section will cover how we actually send this configuration to the I/O expander.

Sending the Configuration

Now that we have what to send to the I/O expander, we need to talk about how to go about sending it.

To make the process of sending the data for the display easier, we will make a function that handles I2C, so we only need to write code that sends data to the I/O expander once. We will call this function setI2CReg, since we are setting a register through I2C. It will take two inputs: a byte reg, and a byte val. Where reg is the register we are setting, and val is the value that the register will be set to. This function is actually very simple; we begin transmission to the device, send the data, and then end the transmission. Thus, the function setI2CReg will be:

				void setI2CReg(byte reg, byte val)
				{
				  // IO is a global variable that is the address to the I/O expander
				  Wire.beginTransmission(IO);
				  Wire.send(reg);
				  Wire.send(val);
				  Wire.endTransmission();
				}

Now we will create the complimentary function to setI2CReg, which is getI2CReg. This function will read a register from the I/O expander through I2C. The process is similar to writing through I2C, except we will have to send before we read. What we send is the register address we wish to read from. Since we want to read the data, the function should return the byte it read from the I/O expander.

So the input to the function will be a byte that we will call reg. The function will return the byte we receive from the I/O expander. The code below will show the function getI2CReg:

				byte getI2CReg(byte reg)
				{
				  int ret = 0;
				  Wire.beginTransmission(IO);
				  Wire.send(reg);
				  Wire.endTransmission();
				  Wire.requestFrom((uint8_t)IO, (uint8_t)1);
				  if(Wire.available())
					ret = Wire.receive();
				  return ret;
				}

The setup() Function I2C Section

Now that we have some functions set up, we can bring this all together so we can start the I2C portion of the setup() function. Using the numbers we calculated before, we can write the setup code for configuring the I/O expander.

Before we write the code itself, we should define some constants for the registers we'll be using so that we can easily tell what we are doing. The first register we'll define is the IODIR register with its address of 0x00. The second register is the GPINTEN register and its address is 0x02. The third register is the IOCON register, its address is 0x05. Our fourth register is the INTCAP register, which we'll cover below in the interrupt code section. Our last register is the GPIO register, which is address 0x0A and is the register we'll use to write to the seven-segment display.

We'll use #define statements to define our registers. These statements should come after the #include <Wire.h>. For reference the statements should look like this:

				#define IODIR   0x00
				#define GPINTEN 0x02
				#define IOCON   0x05
				#define INTCAP  0x08
				#define GPIO    0x0A

Now for the code in the setup() function. Remember that is just a portion of the code in the setup() function.

				Wire.begin();
				setI2CReg(IODIR,   0x08);
				setI2CReg(GPINTEN, 0x08);
				setI2CReg(IOCON,   0x22);

With that, when the chipKIT is powered up it will configure the I/O expander.

Interrupts

In the above section, we talked about setting up the interrupt on the I/O expander. In this section, we will talk about how we will use that interrupt to trigger one on the chipKIT and then handle said interrupt.

The most important thing to consider about the interrupt from the I/O expander is that it triggers an output whenever the input changes. Which means we'll be receiving two interrupts, once from the button being pressed, and again from the button being released. We will need to be able to check to see if it is from the button being pressed or released, otherwise a quick press of the button will mean that the counter continues to count since the two interrupts will disable and then re-enable the counter. Thus, the interrupt handler on the chipKIT board will have to be more complicated than just toggling the counter.

However, when dealing with interrupts and I2C at the same time we will encounter a problem. The interrupt handler can't use I2C function while inside the interrupt. This will require the use of a flag variable to handle the I/O expander interrupt inside the main loop function.

If we want the interrupt handled quickly, we will also have to implement non-blocking delays (this will be covered in a section below). This is because delay(1000) will wait for 1000 milliseconds (1 second) and our flag variable will only be handled once per second, instead of as soon as possible.

We'll start this off by talking about how we are going to handle the interrupt from the I/O expander. Our circuit has the interrupt pin from the I/O expander connected to pin 2 of the chipKIT uno32 board. This pin triggers the interrupt 1 handler if there is a function attached to it (attachInterrupt() is the function that is used to attach a function to one of the interrupts, search the reference manual of your board to determine which pins correspond to the possible external interrupts). The interrupt pins for the Uno32, Max32, and uC32 are listed below for convenience.

  • Uno32:
    • INT0 - Pin 38
    • INT1 - Pin 2
    • INT2 - Pin 7
    • INT3 - Pin 8
    • INT4 - Pin 35
  • Max32
    • INT0 - Pin 3
    • INT1 - Pin 2
    • INT2 - Pin 7
    • INT3 - Pin 21
    • INT4 - Pin 20
  • uC32
    • INT0 - Pin 38
    • INT1 - Pin 2
    • INT2 - Pin 7
    • INT3 - Pin 8
    • INT4 - Pin 35

We are going to need two functions to deal with the interrupts, since we are unable to use I2C function inside the interrupt handler. So, our interrupt handler is incredibly simple. It sets a flag variable to true and sends a serial message so we know an interrupt occurred.

The second function we need is the actual interrupt handler which deals with the I/O expander's interrupt. This function clears the I/O expander's interrupt by reading a certain register from it. This register is INTCAP, which stores the value of the pin that triggered the interrupt. We will use this result to determine if the interrupt was a rising edge or falling edge interrupt. We can do this since the I/O expander generates an interrupt whenever there is a change on the input pin and with the non-blocking delay we should be able to handle each interrupt before another is triggered (since these interrupts are from a button which generates two interrupts).

So, the main process to handle the I/O expander's interrupt is to read the INTCAP register. Filter out the main bit of interest (bit 7, the input pin we set), and clear the interrupt flag variable and toggle our counter. We will get into this function by adding an if statement in the loop() function with our interrupt flag as the condition.

The if statement in the loop() function will look like the following:

				// int_trig is our flag variable to let us know that
				// there is an interrupt on the IO expander to handle
				// (also, since int_trig is already a boolean variable, we don't need "== true")
				if(int_trig){
				  // call our function to handle the IO expander interrupt
				  I2C_isr();
				}

I2C_isr() in the code above is our interrupt handler that handles the I/O expander interrupt. Which is written here:

				void I2C_isr()
				{
				  // Read the INTCAP register on the IO expander to clear the interrupt as well as
				  // get the value of the pin when the interrupt was triggered
				  int r = getI2CReg(INTCAP);
				  // We are interested in only the 8th bit (bit 7) and the "& 1" is to ensure only
				  // that bit is read
				  r = (r>>7) & 1;
				  // if the interrupt was a rising edge, do stuff
				  // otherwise do nothing
				  if(r == 1){
				    // reset our flag variable
					int_trig = false;
				    // toggle the counter (on->off, off->on)
					increment = !increment;
				  }
				}

Where increment is the boolean that controls if the number on the display increments, and int_trig is another boolean that flags us if an interrupt was triggered.

Non-blocking delays

If we were to use the delay() function for this project, we would find that interrupts would be handled only once a second, and since we can't handle the I/O expander interrupt inside the chipKIT board's interrupt service routine ( attachInterrupt()) (I2C can't receive inside an interrupt) then when we can only handle the interrupt once delay() is done.

There is a way to delay executing one section of code while still running others. You simply need to put the code you want to delay inside of an if statement that waits for a certain amount of time to pass. If the time limit you set isn't reached, the if statement skips over the code it contains. Otherwise, once the time limit has been reached the if statement executes whatever code is inside. To keep track of the time delay, all you need are two variables. One for the “current” time and one for the previous time that the code executed. Using these two variables, we can determine if some amount of time has passed.

Below we will show two equivalent loop() functions; one uses delay() and the other uses the technique described above. Using delay() is equivalent to running a loop that does nothing except take up time and that exits after some amount of time, for example 1000 milliseconds. Firstly, using delay():

				void loop()
				{
				  /* some code here */
				  delay(1000);// delay for a second
				}

And now using the elapsed time technique:

				// Need to initialize the initial time
				int previous_time = millis();

				void loop()
				{
				  // get the current time
				  int current_time = millis();
				  // execute the same code as the code from the delay() example and 
				  // if the difference in time is greater than or equal to a second
				  if(current_time - previous_time >= 1000){
					/* same code from above */
					// update our previous time to now
					previous_time = current_time;
				  }
				  /* Some other code that should run more than once a second (not easily possible using delay) */
				}

The biggest difference how this code logically works is that delay() delays for the specified amount of time, whereas non-blocking delays execute its code once enough time has passed and is not taking up a significant amount of time otherwise. However, there is a drawback to non-blocking delays in the form of more complex and thus harder to debug code. Since instead of just the code and delay, non-blocking delays require two variables and an if statement per block of code that needs delay. The link to the right expands on how to create a library in MPIDE.

Displaying Numbers

Now we'll explain how we will display the numbers we want to show on the seven-segment display. If you pay close attention to how we wired the I/O expander to the seven-segment display, you'll notice that we linked the segments so that segment A is connected to GP0, segment B is connected to GP1, and so on. Using this information, we can create an array of bytes which should display the number on the display.

Figure 10 below shows the display segment order while the list to the right shows the mapping of the I/O expander pin (and bit) position to segment section.

Figure 10. Seven-segment display segment order.
  • A:0
  • B:1
  • C:2
  • D:3
  • E:4
  • F:5
  • G:6
 

Now we can define numbers that are displayed on the seven-segment as numbers, where the bit positions determine which segment is lit up. Since the seven-segment display is in the common anode configuration to light up a segment, we must pull the corresponding pin low. However, that means we then must define the lit segments as 0 instead of 1. So there is some getting used to using '0' to mean ON as opposed to '1'.

We'll do the first two numbers, leaving the rest to be proven by the reader. The first number is 0. So, the segments which should be lit are segments A through F. So, the bit pattern should be 0b1100 0000, which we can write as 0xC0. For 1, only two segments are lit up; segments B and C will be lit up, which means that the bit pattern is 0b1111 1001, and in hex it is 0xF9. Below is a table of seven-segment characters and the corresponding binary and hex numbers as it will be helpful.

Seven Segment Character Code Table

Seven Segment Display Character Hexadecimal Number Binary Number
0 0xC0 0b1100 0000
1 0xF9 0b1111 1001
2 0xA4 0b1010 0100
3 0xB0 0b1011 0000
4 0x99 0b1001 1001
5 0x92 0b1001 0010
6 0x82 0b1000 0010
7 0xF8 0b1111 1000
8 0x80 0b1000 0000
9 0x98 0b1001 1000

We will create an array whose index number is the number to be displayed, i.e., 0 on the seven-segment display is also at index 0 in the array. We will make this array a constant global variable since it isn't going to change. The code for the array is show here:

				// 7 Seg Character:   0     1     2     3     4     5     6     7     8     9
				const byte num[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x98};

Now, we need to send this number to the seven-segment display. This is made easy by using our setI2CReg() function. We will send our number to the GPIO register, which sets the output pins. We should also increment the number we send and only send the number if we are incrementing. The code for this is here:

				if((current_time - count_last_time > 1000) && increment){
				  // increment count and ensure it is within our array size
				  count = (++count)%BASE;
				  // send the 7seg number to the IO expander
				  setI2CReg(GPIO, num[count]);
				  // reset our delay
				  count_last_time = current_time;
				}

Where BASE is a constant equal to 10, we change this in the test your knowledge section, so we have it here to make the changes easier. The tilde before num, which is our array of bit patterns for the seven-segment display, is the bit-wise NOT operator, which flips the bits. And count is a variable which stores the current number to display.

The code

Here is the code with comments explaining various section of code where the purpose is not completely obvious.

				// This library is for I2C
				#include <Wire.h>

				#define IODIR   0x00
				#define GPINTEN 0x02
				#define IOCON   0x05
				#define INTCAP  0x08
				#define GPIO    0x0A

				// This constant is the amount of 
				// numbers in the number array
				#define BASE 10

				// This is the slave device address (I/O expander)
				const byte IO = 0x20;
				// 7 seg characeter:  0     1     2     3     4     5     6     7     8     9
				const byte num[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x98};

				// The number to display
				int count = 0;
				// Enables or disables the number increment
				bool increment = true;
				// interrupt flag, lets us know if an interrupt was triggered
				bool int_trig = false;

				void setup()
				{
				  // this pin is our interrupt pin
				  pinMode(2, INPUT);
				  Serial.begin(9600);
				  Wire.begin();
				  setI2CReg(IODIR, 0x80);
				  setI2CReg(GPINTEN, 0x80);
				  setI2CReg(IOCON, 0x22);
				  attachInterrupt(1,isr,RISING);
				}

				void loop()
				{
				  // These two variables are initialized only the first 
				  // time the loop function runs, and the values are preserved
				  static int count_last_time = millis();
				  static int pause_last_time = millis();
				  
				  int current_time = millis();
				  
				  // if our IO expander interrupt flag is set, handle it
				  if(int_trig){
					I2C_isr();
				  }
				  
				  // If it has been a second and we are NOT incrementing the counter
				  if(current_time - pause_last_time >= 1000 && !increment){
				    // send a message that we are paused and the current number
					Serial.print("Paused: ");
					Serial.print("Showing: ");
					Serial.println(count, DEC);
					// reset the start time for both pausing and counting
					// so that the counter doesn't jump to the next number 
					// after unpausing
					pause_last_time = current_time;
					count_last_time = current_time;
				  }
				  
				  // if it has been a second and we ARE incrementing the counter
				  if(current_time - count_last_time >= 1000 && increment){
				    // increment the number to display
					count = (++count)%BASE;
					// send the number to the IO expander
					setI2CReg(GPIO, num[count]);
					// reset the time so we wait another second
					count_last_time = current_time;
					// send a message with the currently displayed number
					Serial.print("Showing: ");
					Serial.println(count, DEC);
				  }
				}

				void isr()
				{
				  Serial.println("Interrupt triggered");
				  int_trig = true;
				}

				void I2C_isr()
				{
				  int r = getI2CReg(INTCAP);
				  // shift and filter the bit of interest
				  r = (r>>7) & 1;
				  
				  // if the interrupt triggered on the I/O expander is rising edge
				  // clear our interrupt flag and toggle the counting
				  if(r == 1){
					// clear our interrupt flag
					int_trig = false;
					// toggle the count increment 
					increment = !increment;
				  }
				}

				byte getI2CReg(byte reg)
				{
				  int ret = 0;
				  // We first must tell the I/O expander which register we want
				  Wire.beginTransmission(IO);
				  Wire.send(reg);
				  Wire.endTransmission();
				  // Now request the byte from the I/O expander
				  Wire.requestFrom((uint8_t)IO, (uint8_t)1);
				  if(Wire.available()){
					ret = Wire.receive();
				  }
				  return ret;
				}

				void setI2CReg(byte reg, byte val)
				{
				  Wire.beginTransmission(IO);
				  Wire.send(reg);
				  Wire.send(val);
				  Wire.endTransmission();
				}

Test Your Knowledge!

Now that you've completed this project, you should:

  • Change the code so that the counter counts in hexadecimal (0->F), this will mean that additional bit patterns must be created.
  • Change the interrupt driven system to one that uses polling. Hint: look at what we do in the I2C_isr() function, and remember button debouncing techniques.
  • Try using a wire to connect 5V0 to the seven-segment digit pins as shown in Fig. 2 (pins 6, 8, 9, and 12) and observe what happens.
  • Change the code so that the button changes whether the counter is counting up or down.

  • Other product and company names mentioned herein are trademarks or trade names of their respective companies. © 2014 Digilent Inc. All rights reserved.
  • Circuit and breadboard images were created using Fritzing.