CC Blog Design Solutions Research & Design Hub

I2C User Interface (I/O)

Written by Jeff Bachiochi

Button It Up

Almost every project that interfaces with a user requires some way of providing I/O to the system. I add inexpensive push buttons to a low-budget display via an already-established I2C interface, using an I/O expander.

  • How can I add I/O to an embedded system?
  • How can I use an I2C interface?
    What is an I/O expander?
  • Arduino MEGA 2560
  • Adafruit Monochrome 0.96” 128×64 OLED
  • Graphic Display
  • Bud Industries DMB-4771
  • Microchip’s MCP23008 I/O expander
  • Alcoswitch FSM series push button from TE Connectivity

Auser interface can take on many forms. The light switch on the wall of your home handles an input task without the need for feedback. You assume when you flick it up a light will go on somewhere. It could be in the same room, such as a ceiling lamp, or on the other side of a door, such as a porch light. In a similar way, outputs don’t always require any input. A wall clock, for example, will happily display the time without the need for any input from you.

Often, both input and output are required to handle a task. In a simple I/O system, pre-labeled buttons serve as fixed input selections, while a display gives the user prompts, or feedback, or both. I like the clean look of a display that has a built-in touchscreen. It not only removes the need for physical input switches, but also allows the screen touch areas to be redefined. There are two issues with touchscreens, though—they are generally large displays, and they can be quite expensive.

This month I want to take one of the smallest yet useful displays and add input buttons, to make it a useful 4-wire addition to many projects. The display I will use is the 0.96” SSD1306 OLED, which has a resolution of 128 x 64 pixels. I picked up an inexpensive DIN rail enclosure, the Bud Industries DMB-4771 (Figure 1). The top panel of this enclosure is 42mm x 49mm, or less than 2”. Few displays will fit in a panel this small. It is most likely meant for a couple of switches, or maybe a connector or two. For this project I will squeeze eight push buttons along with the OLED display onto a PCB that fits this enclosure.

The panel size for the chosen enclosure determines the size of the PCB for this project. Both fit into the enclosure in the same physical space. So, we know the size of the PCB will be 42mm x 49mm. Figure 2 is the schematic for the project. By using an Alcoswitch FSM series push button from TE Connectivity, you can choose from a number of stem lengths to make it work for most applications (PCB to panel height). I placed four push buttons under the display, and two off to each side.

Figure 1
I found this inexpensive DIN rail enclosure that seems like pretty good deal. It's easy to mount, has cutouts for screw terminals and can support a number of PCBs.
Figure 1
I found this inexpensive DIN rail enclosure that seems like pretty good deal. It’s easy to mount, has cutouts for screw terminals and can support a number of PCBs.
FIGURE 2
Microchip's MCP23008 I/O expander gives you access to any combination of eight I/Os vias an I2C interface. I'm using it as eight push button inputs sharing the I2C bus with an SSD1306 display.
FIGURE 2
Microchip’s MCP23008 I/O expander gives you access to any combination of eight I/Os vias an I2C interface. I’m using it as eight push button inputs sharing the I2C bus with an SSD1306 display.

I use the non-commercial version of the Eagle Schematic and Layout program (now part of Fusion 360 by Autodesk). This personal-use software has some size limitations (80cm x 80cm), but has been a staple in the industry for years. A number of inexpensive PCB fab houses cater to the hobbyist. My preference is OSH Park in Portland, Oregon. A run of three PCBs costs around $5/square inch and ships in 9-12 days. For complicated circuits, it’s hardly worth prototyping a circuit by hand, especially if you wish to use the circuit in more than one project.

MCP23008 I/O EXPANDER

It only makes sense to use an I2C device here, since the SSD1306 display is already I2C, meaning the interface to the microcontroller does not change. It like getting the added benefit of user input for free! You can see in the block diagram of the Microchip MCP23008 (Figure 3) that this device gives the user access to eight general purpose I/O’s through an I2C interface.

Figure 3 
Block diagram of the MCP23008 I/O Expander, an I2C device that has eight individually programmable I/O. The 11 registers make it extremely flexible. An interrupt output can be used, or you can poll for a change of state.
Figure 3
Block diagram of the MCP23008 I/O Expander, an I2C device that has eight individually programmable I/O. The 11 registers make it extremely flexible. An interrupt output can be used, or you can poll for a change of state.

The ability of this device to handle bit-defined I/O makes it very flexible, but also means that several registers must be configured to determine how it will be used. The default register settings configure the 8 I/O bits as all inputs, and this port can be sampled by reading one of the 11 registers. For you to understand just how flexible this device can be, we’ll need to look at all the registers in the MCP23008 (Table 1). The function of each register is summarized in Table 2. Incidentally, you can get the same device with an SPI interface (MCP23S08) for those so inclined.

Table 1 
With these 11 registers you can program each of the eight pins as input or output. Interrupts can be generated for any logical change of state. Inputs can have internal pull-ups enabled.
Table 1
With these 11 registers you can program each of the eight pins as input or output. Interrupts can be generated for any logical change of state. Inputs can have internal pull-ups enabled.
Table 2 
The 11 registers in the MCP23008 and their functions (listed in order of discussion).
Table 2
The 11 registers in the MCP23008 and their functions (listed in order of discussion).

The Registers and Their Functions: Let’s begin with the registers associated with inputs. The first register, IODIR (address 0x00), sets the direction of each bit. As I stated above, the device defaults to all inputs as noted in this register. It follows the standard used by most microcontrollers; the direction register uses the format 1=input and 0=output. The second register, IPOL (address 0x01), can invert the polarity of an input. In this case a “0” indicates that no inversion takes place. Let’s skip to register address 0x06, GPPU. This register can enable an internal pull-up (1=pull-up enabled) for any bits configured as an input. Last, register address 0x09, GPIO, will reflect the present logic level of any input, with respect to the IPOL register (address 0x01).

When using a bit as an output, the first register, IODIR (address 0x00), should have its corresponding bit cleared, 0=output. To place a value on any bit configured as an output, use the last register, OLAT (address 0x0A).

You’ll note that the MCP23008 has the ability to create an interrupt. You don’t have to physically use the interrupt output to make use of an interrupt. Although the interrupt output can eliminate having to poll the MCP23008 registers, you still must read the appropriate registers to determine what has happened. To enable an interrupt on an input bit, you must set the appropriate bit in register address 0x02, GPINTEN. How an interrupt is determined is dependent on the bit value in register address 0x04, INTCON. If the INTCON bit=0, an interrupt is generated whenever the input bit changes. If the INTCON bit=1, an interrupt is generated when the input bit is different from the bit in the DEFVAL register (address 0x03). This is similar to generating an interrupt on each edge, and choosing to generate an interrupt on only one edge (or change of state).

What happens when an interrupt occurs? The INTF register (address 0x07) reflects the state of any and all enabled interrupt pins that have caused the interrupt. The INTCAP register (address 0x08) will only reflect the first interrupt seen. Reading this register will reset both the INTCAP and INTF registers.

If you’ve been keeping track (Table 2), there is one register left, the IOCON register at address 0x05. This register has to do with four control functions. Bit 5 determines if reading more than one register at a time will auto-increment the register pointer. Bit 4 controls the slew rate on the SDA output to help prevent noise when using slower speeds. Bit 2 selects between open drain and push-pull output of the interrupt output pin. And bit 1 selects the idle logic level of the interrupt output pin. While I am not using the interrupt output in this project, the I2C interface has been laid out for 5 pins, so you can use a 4- or 5-pin connector.

A QUICK REVIEW

You’ve seen this used in several of my previous projects—Adafruit’s Monochrome 0.96” 128×64 OLED Graphic Display, with I2C and SPI, through 8-pins. I am using a 4-pin (I2C only) version that I bought on Amazon. If you go this route, beware that there are a couple of different pinout offerings (see Figure 4). You might have to rework your cable/circuit. Other than that, they work the same way, and the Adafruit library will handle these.

Figure 4 
This 0.96” OLED SSD1306 display is available with some different 4-pin configurations. These are also available with different screen colors. I prefer the white displays.
Figure 4
This 0.96” OLED SSD1306 display is available with some different 4-pin configurations. These are also available with different screen colors. I prefer the white displays.

The I2C address has an alternate address that can be used if you have another device with the same 7-bit address (0x78/7A). The display is memory mapped, and is transferred to the display a whole screen at a time. Therefore, you can write different areas of the memory at will, without worrying about partial screen flickering, because the screen doesn’t update until you use the display.display() command.

Simple, pixel-based commands are available with the Adafruit_SSD1306.h library. With this, you can clear, invert, and display a screen. This includes commands for get and draw a pixel, draw line and screen scrolling. To do some fancier graphics, including text fonts, you will need to add the Adafruit_GFX.h library. This library adds commands to draw and fill, circles, triangles, and rectangles. Bitmaps are supported, along with text commands that include sizing and fonts. The Adafruit example sketch for this display really shows off the use of most commands, so you can refer to it to learn how to use those commands. The default is a 5×7 character font (6×8 including a blank right column and blank lower row). That’s a screen width of 21 characters, and a screen height of 8 rows.

I could go through the register use and how we can write to the display using just I2C transfers, but the complexity of this and the graphic routines is beyond the scope of this project article. Adafruit’s libraries are the best and most useful functions of any libraries out there. Let’s stick with these libraries and save column space for the device I’ve not used in a project up to now, the 8-bit I/O expander. Let’s look at a sketch to make use this new I2C peripheral.

ARDUINO IDE

This user I/O peripheral can be employed with any of the Arduino microcontrollers, since it will run on 3.3V or 5V. I’ll be using a Arduino MEGA 2560 for this project, because I already had an I2C port wired on a prototype shield. The first application will simply allow the user access to the MCP23008’s registers, and it starts off with some initialization for the expander. The menu presented comes from menuArray[], a list of Strings. When the MCP23008 registers are read, the values are stored in valueArray[], an array of register values. The array byteArray[] is used in the I2C functions to pass data back and forth (see Listing 1).

As we saw back in the block diagram in Figure 3, there are 11 registers available to the user. This first application will exercise those registers. A menu shows user choices and the latest values shored in those registers. These are updated every time the menu is displayed. The main loop contains only two functions, readAllRegisters() and displayMenu().

I use three functions for reading all the registers. The first checks the I2C port to determine if its active, the second writes to the port, and the third reads from the port. The first is not necessary; its just used as a general check on any I2C address used (Listing 2).

Listing 1
This demo app will allow access to the MCP23008 I2C 8-bit Expander. Debug data will be sent to the USB port for connection to an optional debugging serial terminal.

//--------------------------------------------
//  start define MCP23008
//--------------------------------------------
#include <Wire.h>
byte i2cAddress = 0x20;                       // 0x20
byte byteCount;
char character;
const byte menuCount = 11;
byte byteArray[menuCount ];
const byte myDebug = 0;               // 1=verbose
const String menuArray[menuCount] = 
{
“A - Pin Direction  “,
“B - Input Polarity “,
“C - Interrupt En   “,
“D - Default Value  “,
“E - Int Control    “,
“F - IO Control     “,
“G - Pullup En      “,
“H - Int Flag       “,
“I - Int Capture    “,
“J - Pin Value      “,
“K - Output Latch   “
};
int valueArray[menuCount] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
//**************************************
//  end define MCP23008
//**************************************
Listing 2

The read and write routines make extensive use of byteCount and byteArray[] to pass all the register data back and forth. Note the use of myDebug option to display progress or disable all debug information.

//--------------------------------------------
//  start function readI2C()
//--------------------------------------------
void readI2C()
{
  if(anybodyHome(i2cAddress))
  {
    if(myDebug & 1)
    {
      Serial.println(“Reading these bytes...”);
    }
    long i = 0;  
    if (Wire.requestFrom(i2cAddress, byteCount))
    {
      while (Wire.available())  // slave may send less than requested
      {
        int c = Wire.read(); // receive a byte as character
        if(myDebug & 1)
        {
          Serial.print(c,DEC);
          Serial.print(“,”);
        }
        byteArray[i]=c;
        i++;
      }
    }
    delay(100);
    if(myDebug & 1)
    {
      Serial.println();
    }
  }
  else
  {
    if(myDebug & 1)
    {
      Serial.println(“Nobody There!”); 
    }   
  }
}
//--------------------------------------------
//  end function readI2C()
//--------------------------------------------
//--------------------------------------------
//  start function writeI2C()
//--------------------------------------------
void writeI2C()
{
  if(anybodyHome(i2cAddress))
  {  
    Wire.beginTransmission(i2cAddress); //  transmit to device #8
    for(byte i=0; i<byteCount; i++)
    {
      Wire.write(byteArray[i]);  //  sends one byte
      if(myDebug & 1)
      {
        Serial.print(“Writing 0x”);
        Serial.println(byteArray[i],HEX);
      }    
    }
    Wire.endTransmission(); //  stop transmitting
    if(myDebug & 1)
    {
      Serial.print(“Finished sending “);
      Serial.print(byteCount);
      Serial.println(“ bytes”);
    }
  }
  else
  {
    if(myDebug & 1)
    {
      Serial.println(“Nobody There!”);  
    }  
  }
}
//--------------------------------------------
//  end function writeI2C()
//--------------------------------------------
//--------------------------------------------
//  start function readI2C()
//--------------------------------------------
void readI2C()
{
  if(anybodyHome(i2cAddress))
  {
    if(myDebug & 1)
    {
      Serial.println(“Reading these bytes...”);
    }
    long i = 0;  
    if (Wire.requestFrom(i2cAddress, byteCount))
    {
      while (Wire.available())  // slave may send less than requested
      {
        int c = Wire.read(); // receive a byte as character
        if(myDebug & 1)
        {
          Serial.print(c,DEC);
          Serial.print(“,”);
        }
        byteArray[i]=c;
        i++;
      }
    }
    delay(100);
    if(myDebug & 1)
    {
      Serial.println();
    }
  }
  else
  {
    if(myDebug & 1)
    {
      Serial.println(“Nobody There!”); 
    }   
  }
}
//--------------------------------------------
//  end function readI2C()
//--------------------------------------------
//--------------------------------------------
//  start function writeI2C()
//--------------------------------------------
void writeI2C()
{
  if(anybodyHome(i2cAddress))
  {  
    Wire.beginTransmission(i2cAddress); //  transmit to device #8
    for(byte i=0; i<byteCount; i++)
    {
      Wire.write(byteArray[i]);  //  sends one byte
      if(myDebug & 1)
      {
        Serial.print(“Writing 0x”);
        Serial.println(byteArray[i],HEX);
      }    
    }
    Wire.endTransmission(); //  stop transmitting
    if(myDebug & 1)
    {
      Serial.print(“Finished sending “);
      Serial.print(byteCount);
      Serial.println(“ bytes”);
    }
  }
  else
  {
    if(myDebug & 1)
    {
      Serial.println(“Nobody There!”);  
    }  
  }
}
//--------------------------------------------
//  end function writeI2C()
//--------------------------------------------

The displayMenu() function not only displays the menuArray[] strings, but also the present values of all the MCP23008 registers. The values displayed are in binary format, so you can easily see which bits (I/Os) are in what states. The function then waits for user input. The function will not complete unless the user enters something. If that something is one of the menu items (A-K), then separate functions are called for each menu choice.

There are two read-only registers, H – INTF and I – INTCAP. The other functions will let you change the register by entering a decimal value when asked. You can actively play with the buttons while pressing “Enter,” and a new menu displays with any updated values. Once you feel comfortable with how this device works, you can move on to integrating the display with the expander.

BUTTON/DISPLAY I/O

Now let’s add some visual output (feedback) in a second application. Although we can display 8 rows of 21 characters, this application will use some graphic boxes that require extra room surrounding any text, so I won’t be able to have 8 rows of text. You can certainly get away with using no graphics, but I think you’ll see it is effective for this demonstration.

I’ll skip a lot of code here, and show only bits and pieces. The complete code is available on the Circuit Cellar Article Code and Files webpage. I’m going to assume that the eight buttons have no labels on them, so I want to use the screen to label these switches 1-8. To make it obvious what is going on, let’s display eight button-shaped boxes in the same format as the physical switches on the PCB: 1-4 across the bottom, and two on each side—5-6 and 7-8. Since the screen is 128 bits wide, I can use a total of 32 bits for each button icon. Using a character font of 5×7, I’ll need to have a box height of 7+2+2 or 11 pixels. (The characters’ eighth pixel will always be blank, so I can make use of that.) With a character width of 6 (including the blank right column) and a border of 2 on each end (one white and one black), I’ll have room for 30–2–2 or 26/6 = 4 complete characters.

Using these numbers, I can draw button icons for each of the eight buttons. To help simplify this process, I use a number of arrays (see Listing 3). The array myPosition[] holds all the values necessary to feed the drawRoundRectangle() function. Each icon is drawn as a border with text from the appropriate myLabels[] array, when the button is in the idle state. When pressed, the draw function uses the fillRoundRectangle() function to visually show that the button is pressed. The bEnable[] array holds Boolean states for each switch. An icon is drawn only when its associated bEnable[] bit=1.

For this second application after the MCP23008 and SSD1306 have been initialized, the only thing the main loop has to do is look for a change in button state, determined by the check4Button() function. This is called every time millis() is equal to or exceeds myTime, which is an offset of myDelay from the last time check4Button() was executed. The value of myDelay is the number of milliseconds between button checks. In this routine, comparisons are made to determine if a button has changed states since the last check. The value in myButtonInterrupts is from the INTF register of the MCP23008. Note that Listing 4 is a snippet of the test for button 1. The button1Pressed() and button1Released() functions are where you would put some code if your application depended on this action.

If any change takes place, we set the updateScreen flag so the LCD screen can be updated and reflect the present state of the buttons. The showDisplay() function then uses similar logic to check4Button() function to determine which icon (if any) to display (Listing 5).

This second application merely reads the physical buttons and reflects their state through the labeled button icons on the display. In this application, each icon can display 4 characters within its border. Even with all 8 switches enabled, there is still a central area on the OLED screen where you can put up to 6 lines of text. The last application shows how this might be used to handle time and date.

Listing 3
The showPushedButton() or showIdleButton() functions are only called if the bEnable() array bit=1. The switch of interest, number 1-8, determines which array values are used for the drawing functions.

const byte myPosition[] = 
{
  //xPosition, yPosition, width, height, radius
  0,  52, 30, 11, 3,
  32, 52, 30, 11, 3,
  64, 52, 30, 11, 3,
  96, 52, 30, 11, 3, 
  0,  0,  30, 11, 3, 
  0,  26, 30, 11, 3, 
  96, 0,  30, 11, 3, 
  96, 26, 30, 11, 3,
}; 
const String myLabels[] = { “  1 “, “  2 “, “  3 “, “  4 “, “  5 “, “  6 “, “  7 “, “  8 “ };
boolean bEnable[] = { 1, 1, 1, 1, 1, 1, 1, 1 };

//**************************************
//  start function showPushedButton(byte t)
//**************************************
void showPushedButton(byte t)
{
  byte p = t * 5;
  display.fillRoundRect(myPosition[p], myPosition[p+1], myPosition[p+2], myPosition[p+3], myPosition[p+4], SSD1306_WHITE);
}
//**************************************
//  end function showPushedButton(byte t)
//**************************************
//**************************************
//  start function showIdleButton(byte t)
//**************************************
void showIdleButton(byte t)
{
  byte p = t * 5;
  display.drawRoundRect(myPosition[p], myPosition[p+1], myPosition[p+2], myPosition[p+3], myPosition[p+4], SSD1306_WHITE);
  display.setCursor(myPosition[p] + 2, myPosition[p+1] + 2);
  display.setTextSize(1);               // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);  // Draw white text
  display.print(myLabels[t]);
}
//**************************************
//  end function showIdleButton(byte t)
//**************************************
Listing 4
In this snippet, we test whether bit 0 (button 1) of the button state has changed and to what state it has changed. You would then take the necessary action based on the change.

  if ((lastBI & 1) != (myButtonInterrupts & 1)) 
  {
    if (((myButtonInterrupts & 1) == 1) && ((lastBI & 1) == 0)) 
    {
      button1Pressed();
    }
    if (((myButtonInterrupts & 1) == 0) && ((lastBI & 1) == 1)) 
    {
      button1Released();
    }
  }
Listing 5

In this snippet, we test whether bit 0 (button 1) of the button state is pressed, and bEnable[0] array to determine whether or not to display this icon. 

if ((myButtonInterrupts & 1) && bEnable[0])
// button 1 enabled
  {                    // button 1 pushed
    showPushedButton(0);
  } 
  else 
  {                    // button 1 idle
    showIdleButton(0);
  }
PUTTING IT ALL INTO PRACTICE

Many of my applications require a real time clock (RTC). This often requires buttons to allow the user to set or change the date or time. Let’s put this I/O module to the test, and see if we can make it usable without the user referring to some manual. To keep this third application simple, no RTC hardware is used. I will use the millis() timer to increment seconds; therefore, time will be lost when the power is removed. There are only basic checks on the legality of the values that are incremented and decremented. This is just meant to show how the buttons can be reassigned to different tasks, based on what needs to be accomplished, and not a complete RTC task.

I’ve defined some arrays that can be switched in and out. There are five modes; you can see the label strings that will fill button icons for each of the modes. Once the Set button is pressed in mode 0, the mode changes to mode 1. Mode 1 labels will be displayed, and the user will chose one of these (DOW, Date, Time, Done).

Let’s say we’ve pressed the Date button, so the mode becomes 2. A new screen is displayed (Month, Day, Year, Done). If we choose “Day,” the mode becomes 4, and the sub-mode becomes 2. A new screen is displayed (inc, dec, done). When the Inc or Dec buttons are pressed, the “Day” value on the screen changes. Pressing the Done button brings us back to mode 0. Note: Each mode also has a bxEnabled[] array associated with it. It controls which buttons (icons) are displayed. Only displayed buttons are active. Actual screens are shown in Figure 5.

Figure 5
Various screenshots that show how pressing a button alters what is displayed on the screen. This gives the user an easy and efficient way to move through a process.
Figure 5
Various screenshots that show how pressing a button alters what is displayed on the screen. This gives the user an easy and efficient way to move through a process.

When a button is pressed or released, we can do four things: change the mode, change the sub-mode, operate on a value, or do nothing. Depending on the mode, we need to display a different set of buttons. This is accomplished by transferring a particular set of arrays of constants (Listing 6) into myLabels[] and bEnable[] which are used in the display routine. Once in mode 4, the display shows the Inc and Dec buttons. We’ve just come from choosing one of seven variables. To remind the users what sub-mode they have chosen, a new line of text, from arraySet[], is displayed on the screen.

The present DOW, Date, and Time are always shown in the center of the screen. When the Inc and Dec buttons are used, the chosen variable is updated and will be displayed live. When the present variable has been adjusted, the Done button changes the mode to 0, which brings the application to the idle screen (Listing 7).

Listing 6

Here we see the arrays that have been added to handle different screen modes. When you have selected a variable to set, an extra line of text is added to the screen to indicate which one you are changing. You can see a few actual screens in Figure 5.

String myLabels[] =  	 {“  0  “, “  1 “, “  2 “, “  3 “, “  4 “, “  5 “, “  6 “, “  7 “};
const String mode0Labels[] =  {“    “, “    “, “    “, “    “, “    “, “    “, “ set”, “    “};
const String mode1Labels[] =  {“ DOW”, “Date”, “Time”, “done”, “    “, “    “, “    “, “    “};
const String mode2Labels[] =  {“ mon”, “ day”, “ yr “, “done”, “    “, “    “, “    “, “    “};
const String mode3Labels[] =  {“ hr “, “ min”, “ sec”, “done”, “    “, “    “, “    “, “    “};
const String mode4Labels[] =  {“    “, “    “, “    “, “done”, “ inc”, “ dec”, “    “, “    “};
boolean bEnable[] = { 1, 1, 1, 1, 1, 1, 1, 1 };
const boolean b0Enable[] = { 0, 0, 0, 0, 0, 0, 1, 0 };
const boolean b1Enable[] = { 1, 1, 1, 1, 0, 0, 0, 0 };
const boolean b2Enable[] = { 1, 1, 1, 1, 0, 0, 0, 0 };
const boolean b3Enable[] = { 1, 1, 1, 1, 0, 0, 0, 0 };
const boolean b4Enable[] = { 0, 0, 0, 1, 1, 1, 0, 0 };
boolean tEnable[] = { 0, 0, 0, 0, 0, 0};
const String arrayDOW[7] = {“Sunday”,”Monday”,”Tuesday”,”Wednesday”,”Thursday”,”Friday”,”Saturday”};
const String arraySet[7] = {“set DOW”,”set Month”,”set Day”,”set Year”,”set Hour”,”set Minute”,”set Second”};
Listing 7

In application 2 we saw how pressing and releasing any button called a function. In this third application, we see what happens with the release of button 5. We only want do something if the present mode = 4. In this mode, the button icon is labeled Inc. Depending on the sub-mode value, which was set in a previous mode, we will increment one of seven variables.

//**************************************
//  start function button5Released()
//**************************************
void button5Released() 
{
  Serial.println(“Button 5 Released”);
  if(mode == 4)
  {
    switch(submode)                     // increment
    {
      case 0:                           // DOW
        dow++;
        if(dow >= 8)
        {
          dow = 1;
        }               
        break;
      case 1:                           // month
        month++;
        if(month >= 13)
        {
          month = 1;
        }               
        break;
      case 2:                           // day
        day++;
        if(day >= 32)
        {
          day = 1;
        }               
        break;
      case 3:                           // year
        year++;               
        break;
      case 4:                           // hour
        hour++;
        if(hour >= 24)
        {
          hour = 0;
        }                       
        break;
      case 5:                           // minute
        minute++;
        if(minute >= 60)
        {
          minute = 0;
        }                       
        break;
      case 6:                           // second
        second++;
        if(second >= 60)
        {
          second = 0;
        }                       
        break;                
    }
  }
  updateScreen = true;
}
//**************************************
//  end function button5Released()
//**************************************
THE END

Although this brings this project to a chose, it is only the beginning of where you might take this in one of your own applications. Whether you replicate this PCB or incorporate its components into your own circuit, I think you may find this will handle most situations where you need some user feed back for specific tasks. This project was conceived to provide some required user I/O in a modular fashion at a minimal cost. I wish I had done this years ago. It would have saved countless hours.

You may have noticed that there is an LED tied to the interrupt output of the MCP23008. This was used as an indication of a button press. I think a piezo device could be used here to provide audible feedback when a button is pressed. An interrupt is cleared when the INTCAP or GPIO registers are read.

It may be of interest to note that I tossed around the idea of using a microcontroller on this PCB to handle the button inputs. This could have been handled as a remote I2C device, but the cost of the MCP23008 was only about a buck and a half and required no programming. With too much to do, and so little time, I chose to use the I/O expander. 

RESOURCES
Adafruit | www.adafruit.com
Arduino | arduino.cc
Autodesk | www.autodesk.com
Bud Industries | www.budind.com
Microchip Technology | www.microchip.com
OSH Park | oshpark.com
TE Connectivity | www.te.com

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • AUGUST 2023 #397 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
Website | + posts

Jeff Bachiochi (pronounced BAH-key-AH-key) has been writing for Circuit Cellar since 1988. His background includes product design and manufacturing. You can reach him at: jeff.bachiochi@imaginethatnow.com or at: www.imaginethatnow.com.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

I2C User Interface (I/O)

by Jeff Bachiochi time to read: 22 min