MCP23S08: 8-Bit I/O Expander: Part One: Extra Ports for LEDs

So you’ve got 100 LEDs, 72 Buttons, and a microcontroller with 18 I/O pins?  You could try multiplexing the LED.  Use 8 pins for the cathodes of LEDs and drive groups of 8 LEDs with transistors to turn them on groups at a time.  That’s the cheapest thing you could do, but the more LEDs the more complicated it gets and the more pins you still need.  You could do 96 LEDs with 20 pins, 8 for cathodes and 12 for multiplexing transistors.  You’d have to sink all that current and switch everything fast enough to maintain persistence of vision (about 30Hz).  There must be a better way.

And there certainly is.  There are shift registers, which are a great way to go especially if you’re doing simple input and output.  They have to be polled regularly, but you can send them information serially which they put out in parallel, using only a few microcontroller pins.  What I’ve landed upon recently are I/O Expanders from Microchip, particularly the MCP23S08.  Follow the jump to learn about them and how to use them.

The MCP23S08 is an 8-bit I/O Expander.  It communicates via the SPI, Serial Parallel Interface, protocol which is a relatively simple serial communication method.  What it has that simple shift registers do not is some smarts.  It is capable of triggering interrupts to the microcontroller on input pins, a very useful feature that frees the microcontroller from having to poll switches, meaning the microcontroller does not have to constantly check the state of a switch to see if it has been pressed.  The MCP23S08 can send a signal to the microcontroller when a switch is pressed and the microcontroller can respond by asking the MCP23S08 which button has been pressed.  The I/O Expander can also act as a bank of outputs, useful for driving LEDs, relays, transistors, or whatever other thing your system may contain.  I’m using it to drive LEDs and read the switches.  The MCP23sS08 sinks all the current for the LEDs, avoiding the problem of overburdening the microcontroller.  The MCP23S08 can sink up to 150mA total for all pins and up to 25mA per pin.  That’s more than enough for 8 LEDs.

As I said before, the MCP23S08 communicates via SPI.  An alternative is the MCP23008 which uses I2C.  Which one you use comes down to preference and the capability of the microcontroller in the system.  The AVR AtMega164PA can do either and makes it fairly easy to do either.  I was already using some other devices in the system on the SPI bus, so I decided to stick with it for the I/O Expanders.  SPI runs at a higher frequency and is full duplex with separate lines for in and out. I2C uses the same line for in and out, so it uses less pins, but operates at a slower frequency.  For my purposes, ultimate speed is not critical, but it helps to move quickly so that I won’t have to worry about it.  SPI communication requires extra pins for chip select.  This can turn into a lot of pins if you’ve got a lot of chips.  The MCP23S08 manages this situation by having some hardware addressing built in.  By placing bias voltages on two pins, you give chips a hardware address that is included in control messages.  This way, you can control four chips with only one chip select line. In my case, I’m using six I/O expanders with two chip select lines.

The MCP23S08 has ten configuration registers that control whether an i/o pin is an input or output, whether and how an interrupt is configured on an input pin, and whether an output pin is high or low.  These registers can be configured one at a time or through serial mode where you tell the MCP23S08 to load a value into one register and then keep sending bytes to load into the next register and the next until you finish.  I configure these registers in this manner.  Every message to an MCP23S08 consists of at least three bytes.  The first byte is a command saying which chip I’m talking to and whether I want to write to a register or read from it.  The next byte is a register address, which of the registers you want to write to or read from.  The last byte is then a value to be written, or a byte read from a register.  Before I can start communicating with the I/O Expander, I configure the AVR micro to communicate:

    sei();//turn on interrupts
    //configure the SPI for this transmission
    SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0);//turn on the SPI, the micro is the master in the system, and communicate at a frequency of (system clock)/16

Now, I’ve made a very simple function to do the communication. You don’t have to wait for the SPI to transmit and, in the middle of the microcontroller doing something else, I wouldn’t wait.  During configuration, the micro doesn’t need to be doing anything else, so I’m waiting for each message to finish.

    void SPI_SimpleTransmit(unsigned char cData)


    SPDR = cData;

    while(!(SPSR & (1<<SPIF)))


By loading a value into the SPDR register, the micro starts up the SPI clock and will start transmitting the value one bit at a time.  We wait for the micro to signal that is finished by setting the SPI interrupt flag in the SPI status register.  We just keep checking this register and while we wait, we reset the watchdog flag.  As I said, we don’t have to wait.  We could set up an interrupt service routine that would be triggered by the flag being set.  This is how we will use these devices in the main routine.

So, that’s the function and this is how I use it to set up one of the I/O Expanders:


    SPI_SimpleTransmit(IOEXPANDER_U13_WRITE);//transmit the address for U10

    SPI_SimpleTransmit(0x00);//transmit the register address for i/o direction

    SPI_SimpleTransmit(0x00);//transmit value for i/o dir = all outputs

    SPI_SimpleTransmit(0x00);//transmit value for input polarity

    SPI_SimpleTransmit(0x00);//transmit value for interrupt enable

    SPI_SimpleTransmit(0x00);//transmit value for interrupt compare value

    SPI_SimpleTransmit(0x00);//transmit value for interrupt control register

    SPI_SimpleTransmit(0x1A);//transmit value for i/o control register – sequential operation enabled, hardware address enable, interrupt active low

    SPI_SimpleTransmit(0x00);//transmit value for pull-up resistors

    SPI_SimpleTransmit(0x00);//transmit value for interrupt flag register – this write is ignored

    SPI_SimpleTransmit(0x00);//transmit value for interrupt capure register – this write is ignored also

    SPI_SimpleTransmit(0x0F);//transmit value for gpio register – value of i/o


First, I trigger the chip select line to go from 1 to 0, telling it that a transmission is coming.  Next, I transmit the command to write to a specific I/O Expander.  Then, a register address, 0, for the first register.  Then I transmit data for the first register, 0 for all outputs.  The address in the MCP23S08 advances to the next register sequentially automatically, so, I transmit values for all the other registers, one byte at a time. At the end of the transmission, I raise the chip select line, signaling that I’m finished.  At the end of this, the MCP23S08 is configured for all outputs and four of the eight will be high and four of the outputs will be low.  In a later post, I’ll discuss setting one up as inputs with interrupts configured.

Posted in General Electronics, Rockit and tagged , , , , , , , , , , , .


  1. Hey great explanation of the MCP23S08!

    I am currently using this chip in a project at school along with other people, but we’ve been having a problem getting the chip to work consistently. Sometimes the LEDs turn on with the pattern we want and sometimes they don’t turn on at all. We’ve used a scope to see if the correct data is reaching the pins of the expander and everything seems to be working properly. We are leaning to the idea that there may be a timing issue between the PIC16F1939 and the MCP23S08 just because the functionality is so random.n Hopefully you may be able to provide some insight.

    Many thanks,

    • Hi Charles,

      There could be many reasons it wouldn’t work consistently. One big one can be data integrity, the signal that your sending isn’t reaching its destination intact. If you have too much capacitance, say on a breadboard, which can add 100s of pF of capacitance to ground, the rise and fall times of the signal can slowed dramatically, and if they’re too long, the signal will be corrupted, and the receiver will not accept it as a good signal. Another possibility is signal timing. You should be sending three bytes to the chip, one after another. If there is some delay in sending bytes, the command may be confused. One good way to get it to work better and account for delays is to slow down your transmission. You can transmit very slowly because the microcontroller controls the clock. Take it down to 50-100kHz and see if it works better. This will also help with the capacitance problem. The other thing that I’ve found that will get these chips to stop behaving, is not clearing the interrupt flags. If you’re using the interrupts and you get into a situation where an interrupt isn’t being cleared, the chip will stop responding. My best guess though is that your transmission is sloppy, either capacitively corrupted, or you’re not sending the bytes one after another. When you’re looking at the signal at the receiving end, does it look like a nice square wave where the ones and zeroes are equal in length, or are zeroes much longer than ones? I would put the clock on one channel on a scope, the data on another channel, and the chip select on a third, and make sure that things look like they should in the timing diagrams on the datasheet. I hope that helps. I too had problems when I started to use this chip, which is why I put together this post to try to make it easier. Let me know how it works out.



Leave a Reply