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:
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:
- LED_CS_ENABLE;
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
LED_CS_DISABLE;
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.