New MCP23S17 Library

tonton81

Well-known member
Here is a MCP23S17 SPI library I have been working on. It's still in progress for more features but functions pretty well.

It has chip detection, can identify how many chips are on the given chip select, and assign the pin ordering properly. Example, if you have 3 chips addressed as 0, 3, and 7, using pinMode(16, OUTPUT) would essentially put 2nd MCP chip (address 3) bankA pin 0 in OUTPUT mode. Similarly, digitalWriting pin 32 would put 3rd MCP chip (address 7) bankA pin 0 in the given state. 3 chips tally up to 16*3 = 48 gpios, which are 0->47.

Code:
    void pinMode(uint8_t pin, uint8_t mode); // set pin mode
    void digitalWrite(uint8_t pin, bool level); // write pin
    bool digitalRead(uint8_t pin); // read pin
    void toggle(uint8_t pin); // toggle pin
    void invert(uint8_t pin); // invert input pin state
    void enableInterrupt(uint8_t pin, uint8_t mode); // functional in register configuration, but not handled yet (supports RISING, FALLING, and CHANGE), planning to make it event driven.
    void disableInterrupt(uint8_t pin); // disables interrupt if enabled.
    void info(); // printout of all chip pin states, and registers in both HEX and BIN formats

Displayed output of demo:
mcp23s17.jpg

Sketch code:
Code:
#include <SPI.h>
#include "mcp23s17.h"
MCP23S17<&SPI, 10, 400000> mcp; // SPI BUS, CS, SPISPEED

void setup() {
  Serial.begin(115200);
  delay(1000);
  SPI.begin();
  mcp.begin(); // must be always ran AFTER SPI's begin call.
  mcp.pinMode(14, OUTPUT);
  mcp.pinMode(12, OUTPUT);
  for ( uint8_t i = 0; i < 8; i++ ) {
    mcp.pinMode(i, INPUT_PULLUP);
  }
}

void loop() {
  static uint32_t cycle = millis();
  if ( millis() - cycle > 1000 ) {
    mcp.digitalWrite(12, !mcp.digitalRead(12));
    mcp.digitalWrite(14, !mcp.digitalRead(14));
    mcp.info();
    cycle = millis();
  }
}

board tested with, designed for rasbpi, but wired to teensy 4 :D
it has 8x mcp23s17's
it has it's own regulator and gpios are 3.3v, so i run it off the VIN of T4

1582705159294-1.jpg

PS, didn't know what to call the library, so I named it MCP23S17........

Repo: https://github.com/tonton81/MCP23S17
 
Last edited:
Nice. Just for REF - how long to:

When OUTPUT:
> Set all 48 pins to 1
> Then set all pins to 0

When INPUT
> how long to read all 48 pins?
 
this is tested at 10MHz, MCP chip max, running on Teensy 4.

2 Samples, with 48 pins (it uses first 3 chips), and with 128 pins (8 chips, 0->127). Sketch code here:

Code:
#include <SPI.h>
#include "mcp23s17.h"
MCP23S17<&SPI, 10, 10000000> mcp; // SPI BUS, CS, SPISPEED

void setup() {
  Serial.begin(115200);
  delay(1000);
  SPI.begin();
  mcp.begin();
  for ( uint8_t i = 0; i < 127; i++ ) {
    mcp.pinMode(i, OUTPUT);
  }
  for ( uint8_t i = 0; i < 127; i++ ) {
    mcp.digitalWrite(i, LOW);
  }

}

void loop() {
  static uint32_t cycle = millis();
  if ( millis() - cycle > 1000 ) {

    uint32_t tim = micros();
    for ( uint8_t i = 0; i < 47; i++ ) {
      mcp.digitalWrite(i, HIGH);
    }
    Serial.println(micros() - tim);

    tim = micros();
    for ( uint8_t i = 0; i < 127; i++ ) {
      mcp.digitalRead(i);
    }
    Serial.println(micros() - tim);


    mcp.info();
    cycle = millis();
  }
}

at 10MHz:

359uS to set 48 pins (0->47) as logic 1.
972uS to set 128 pins (0->127) as logic 1.

for reading:

182uS to read 48 pins (0->47).
493uS to read 128 pins (0->127).

The uS values are consistant, without fluctuations.
 
Very nice.
Is this only for SPI? What about the MCP23008 and MCP23017 chips that use I2C? Coverage of these chips as well would be the icing on the cake.
 
Yes its possible to make the I2C version as well, perhaps after the features expand first on this version, but don't expect the I2C version to be this fast
 
this is tested at 10MHz, MCP chip max, running on Teensy 4.

2 Samples, with 48 pins (it uses first 3 chips), and with 128 pins (8 chips, 0->127).

Now, I haven't looked at your library, and I've not used the SPI version, but I have used the I2C versions from Adafruit for the MCP23008 and MCP23017. Under the covers, the protocol transfers 8 bits at a time, so instead of doing 48 calls to set a single bit, i.e.:

Code:
    for ( uint8_t i = 0; i < 47; i++ ) {
      mcp.digitalWrite(i, HIGH);
    }

you might want to provide an interface to set 8 bits at a time. Or possibly, have an option like displays use where you call a function to set a bit in the array internally, and then at the end send out all of the changed words, and then read back all of the words at once. Obviously you want the normal digitalWrite interface to do the immediate SPI/I2C calls, but if performance is critical, it may be useful to have an alternate interface.
 
Yes I plan to add port values, and eventually event driven features

Michael, display features? Well digitalRead/Write must be live but what do you propose as an addition, a buffer before write? I was also thinking of adding an array of data that could be passed through to the gpios during the same assertion in case specific timings were needed by a user when toggling gpios at fast rates, i havn't tested tho yet whether they take effect on deassertion or during transfer, we will see..... for event based data, i plan to have an array comparison for the INTCAP register to let user handlers know their pin has been set or not based on their interrupt setting. Most likely will be having mcp.events() in loop handling that. If theres more ideas mightes well implement them while library is fresh :)

The nice thing about the chips Ive used in the past they retained GPIO states when reprogramming teensy (or unplugging teensy) due to being self-powered, I might readd that capability as part of this library

Maybe also like you said we should store the GPIO state locally and have a function write to it, and have events() dump it on each cycle onto the chips? And what should we name the functions? Should we implement a queue system where the writes take effect every cycle rather than direct? But this would rely on events() being run to take effect

For the events() I plan to read X detected chips INTCAP registers for interrupt differences with local array before firing user callbacks which are enabled, that alone is 128 function pointers and an array to store 8 DWORDS just for the INTCAP comparisons
 
Last edited:
Yes I plan to add port values, and eventually event driven features

Michael, display features? Well digitalRead/Write must be live but what do you propose as an addition, a buffer before write? I was also thinking of adding an array of data that could be passed through to the gpios during the same assertion in case specific timings were needed by a user when toggling gpios at fast rates, i havn't tested tho yet whether they take effect on deassertion or during transfer, we will see..... for event based data, i plan to have an array comparison for the INTCAP register to let user handlers know their pin has been set or not based on their interrupt setting. Most likely will be having mcp.events() in loop handling that. If theres more ideas mightes well implement them while library is fresh :)

I was thinking of something similar to a display framebuffer or a WS2812B pattern, where you have an array available locally, and you do whatever you want to the array, and then you issue a command to send the updates to the board, and/or read the current registers.

And if the changes by pins set is expected to be fairly long (by computer standards), you could combine multiple boards interrupt pin to a single interrupt. When you get the interrupt, you have to query several ports to see what changed.
 
We could deal with hardware interrupts but that leaves issue with running SPI within the ISR as well as loop, when a flag should be set. The problem i mentioned earlier is for as long as the pin holds state, and it NEVER clear and will always fire repeatedly, until the pin is deasserted and the gpio is reread. Ideally to go around this I was thinking of instead of a hardware solution that isn't so perfect, probably just have the INTCAP register read on different functions (like if we do a digitalRead for example, the INTCAP register would be read and then your GPIO state will be read then returned. This would notify the local buffer that an interrupt occured and callbacks will fire when events() rolls around
 
I didn't want to make things too complicated, the library already assigns pins in order of amount of chips detected. I did add 2 functions for people who want to write the GPIO registers directly, can be either 8 bit wide or 16 bit wide.

Code:
    uint8_t value = 0xFF;
    uint16_t value16 = 0xFFFF;
    mcp.writeGPIO(MCP23S17_0, value, BANK_0); // writes to first 8 pins of chip address 0
    mcp.writeGPIO(MCP23S17_1, value, BANK_1); // writes to last 8 pins of chip address 1
    mcp.writeGPIO(MCP23S17_4, value16); // writes to 16 pins of chip address 4


Although the pin count is automated for pinMode, digitalRead, and digitalWrite, 0->127 max (IF all 8 chips are detected), I don't see an easy way to make chip specific ports simpler for the user. They must keep track of which chips have their connections. Writing gpio ports while their pins are set to input, their pins remain unchanged, only the output drivers are updated

Also, it takes 4uS to write the port, no reading involved.
It takes 8uS for a digitalWrite (4uS to get the register to be updated, and 4uS to write it back).
 
I been thinking alot and tried something new. To read 128 GPIOs with digitalRead earlier was resulting in 740 uS total. I have got that number down to 71 uS from the same function. I'm using a little trick of swapping the actual port of a unique chip with local register.

Basically there are 8 micros() counters in digitalRead now:

Code:
  static uint32_t pinScan[8] = { 5000 };


what happens now, is if you are reading multiple pins, the port of the given calculated chip, the micros() counter is checked. If it is expired, the port is read once and the micros is updated. When the 2nd read from the same port happens within 50uS, the local register is read instead. If another chip register is checked, it's own counter will be validated to be within 50uS before checking register, else the new port will be read into local register while pinstate is returned, and counter reset:

Code:
    if (micros() - pinScan[i] < 50) {
      return ( chipData[i][8] & (1UL << pin) );
    }

This was the demo code:

Code:
    tim = micros();
    for ( uint8_t i = 0; i < 128; i++ ) {
      mcp.digitalRead(i);
    }
    Serial.printf("digitalRead 128 pins: %llu uS\n", micros() - tim);

Output:
Code:
digitalRead 128 pins: 71 uS

740 uS before optimisation
71 uS after

What do you think? Any flaws in this or is it unique? :)

The beauty is the function is self contained, no code bloat or register dumps elsewhere needed.

If this is good, I will make the register micros counters as part of the class instead of static local, so calling 2 different functions on the same register will validate with common timer to update and check.

I might implement a circular array for the interrupt checks, as updating a single local register on several attempts before accessing it in events can throw off a toggle state. Any interrupts enabled should be checked via local register to see if its enabled when the circular queue pushes an event. This would allow many pins to be checked (via local registers only) before the INTCAP rewrites itself, giving events() time to process the handlers

EDIT: Did the same to digitalWrite:
Output of back to back writes and reads:
Code:
digitalWrite 48 pins: 224 uS (previously, 359uS for 47 pins!)
digitalRead 128 pins: 64 uS
writeGPIO: 4 uS
digitalWrite 14: 9 uS


This is my method:

Code:
MCP23S17_FUNC bool MCP23S17_OPT::digitalRead(uint8_t pin) {
  if ( pin >= (__builtin_popcount(detectedChips) * 16U) ) return 0;
  for ( uint8_t i = 0; i < 8; i++ ) {
    if ( !(detectedChips & (1U << i)) ) continue;
    if ( pin > 15 ) {
      pin -= 16;
      continue;
    }

 [COLOR="#FF0000"]   if (micros() - counter_GPIO[i] < 50) {
      return ( chipData[i][9] & (1UL << pin) );
    }[/COLOR]

    bus->beginTransaction(SPISettings(speed,MSBFIRST,SPI_MODE0)); /* read port register */
    ::digitalWriteFast(chipSelect, LOW);
    bus->transfer16(((0x41 | (i << 1)) << 8) | 0x10);
[COLOR="#FF0000"]    chipData[i][8] = __builtin_bswap16(bus->transfer16(0xFFFF)); /* Dump INTCAP */
    chipData[i][9] = __builtin_bswap16(bus->transfer16(0xFFFF)); /* read GPIOs */
[/COLOR]    ::digitalWriteFast(chipSelect, HIGH);
    bus->endTransaction();
    counter_GPIO[i] = micros();
 [COLOR="#FF0000"]   return (chipData[i][9] & (1UL << pin));
 [/COLOR]   break;
  }
  return 0;
}






MCP23S17_FUNC void MCP23S17_OPT::digitalWrite(uint8_t pin, bool level) {
  if ( pin >= (__builtin_popcount(detectedChips) * 16U) ) return;
  for ( uint8_t i = 0; i < 8; i++ ) {
    if ( !(detectedChips & (1U << i)) ) continue;
    if ( pin > 15 ) {
      pin -= 16;
      continue;
    }

    if (micros() - counter_GPIO[i] > 50) {
      bus->beginTransaction(SPISettings(speed,MSBFIRST,SPI_MODE0)); /* read port register */
      ::digitalWriteFast(chipSelect, LOW);
      bus->transfer16(((0x41 | (i << 1)) << 8) | 0x10);
[COLOR="#FF0000"]      chipData[i][8] = __builtin_bswap16(bus->transfer16(0xFFFF)); /* Dump INTCAP */
      chipData[i][9] = __builtin_bswap16(bus->transfer16(0xFFFF));
[/COLOR]      ::digitalWriteFast(chipSelect, HIGH);
      bus->endTransaction();
      [COLOR="#FF0000"]counter_GPIO[i] = micros();[/COLOR]
    }

[COLOR="#FF0000"]    chipData[i][9] = (chipData[i][9] & ~(1UL << pin)) | (level << pin);[/COLOR]

    bus->beginTransaction(SPISettings(speed,MSBFIRST,SPI_MODE0)); /* write port register */
    ::digitalWriteFast(chipSelect, LOW);
    bus->transfer16(((0x40 | (i << 1)) << 8) | 0x12);
    bus->transfer16(__builtin_bswap16([COLOR="#FF0000"]chipData[i][9][/COLOR]));
    ::digitalWriteFast(chipSelect, HIGH);
    bus->endTransaction();

    break;
  }
}
 
Last edited:
Following the above updates to digitalRead and Write, also added toggle() to the list for fast toggling of gpios. Interrupt registers as well as local are set when commanded. I also implemented resync capability so that when teensy reboots/reprograms the port expanders will not reinitialize to defaults if already configured. This retains GPIO states accross reboots/shutdowns of teensy. I am currently working on the CHANGE, RISING, FALLING events, for comparison between local INTCON, DEFVAL to the new mcp INTCAP value, and if a match will be found an interrupt would be queued to a circular buffer (not array) which stores 128 unique pin values (if pin already in buffer for event, it is tossed, and events will dequeue the current one when needed.

Will see how well this works out, I already setup 128 pointer array for the gpio handlers and the function will be functional soon :)
 
Hows this for results?

Code:
digitalWrite 48 pins: 214 uS
digitalRead 128 pins: 52 uS
writeGPIO: 4 uS
toggle 16: 4 uS
digitalWrite 18: 8 uS

Demo code:

Code:
#include <SPI.h>
#include "mcp23s17.h"
MCP23S17<&SPI, 10, 10000000> mcp; // SPI BUS, CS, SPISPEED

void setup() {
  Serial.begin(115200);
  delay(1000);
  SPI.begin();
  mcp.begin();
  pinMode(14, OUTPUT);
  for ( uint8_t i = 0; i < 128; i++ ) {
    if ( i > 63 ) {
      mcp.pinMode(i, INPUT);
    }
    else mcp.pinMode(i, OUTPUT);
  }
  for ( uint8_t i = 0; i < 128; i++ ) {
    mcp.digitalWrite(i, LOW);
  }

  mcp.pinMode(113, INPUT_PULLUP);
  mcp.attachInterrupt(113, myInt, RISING);
}

void myInt() {
  Serial.println("INTERRUPT !");
}

void loop() {
  mcp.events();
  static uint32_t cycle = millis();
  if ( millis() - cycle > 1000 ) {

    uint32_t tim = micros();
    for ( uint8_t i = 0; i < 48; i++ ) {
      mcp.digitalWrite(i, HIGH);
    }
    Serial.printf("digitalWrite 48 pins: %llu uS\n", micros() - tim);

    tim = micros();
    for ( uint8_t i = 0; i < 128; i++ ) {
      mcp.digitalRead(i);
    }
    Serial.printf("digitalRead 128 pins: %llu uS\n", micros() - tim);

    tim = micros();
    mcp.writeGPIO(MCP23S17_0, 0x7, BANK_1);
    Serial.printf("writeGPIO: %llu uS\n", micros() - tim);

    tim = micros();
    mcp.toggle(16);
    Serial.printf("toggle 16: %llu uS\n", micros() - tim);

    tim = micros();
    mcp.digitalWrite(18, LOW);
    Serial.printf("digitalWrite 18: %llu uS\n", micros() - tim);

    mcp.info();
    Serial.println();
    cycle = millis();
    digitalWrite(14, !digitalRead(14));
  }
}

pin 14 on Teensy 4 is toggling every second, demonstrating the interrupt callback working in events :) Code has been updated on github.
 
Update:

Bug: I fixed a bug in writeGPIO() when writing 8 bits had the banks swapped.

defaults() was previously added to clear all registers of the chip to default, except the HAEN bits of course. Rebooting teensy or reprogramming retains GPIO expander configuration and copies it's registers to local cache. If not configured, they are initialized to defaults normally. defaults() was added in case the user prefers to blank out the chips instead of retaining their current state.

New: added setCache(micros) to fetch cache instead of chip when GPIO reads are needed. Previously we used 50uS hardcoded which had performance improovement. It is still default, however, with this function, if we set 300 uS as value for example, we end up with these results:

Code:
digitalWrite 48 pins: 191 uS
digitalRead 128 pins: 25 uS
writeGPIO: 4 uS
toggle 16: 4 uS
digitalWrite 18: 4 uS
 
Last edited:
Forum only update currently, teensythreads support for MCP23S17, allows shared SPI bus with a library mutex pointer passed in by a reference mutex from caller (setMutex()).
Not specifying setMutex runs normal behaviour.

Code:
[ATTACH]20618._xfImport[/ATTACH]

1) Create a global thread mutex:
Code:
Threads::Mutex TEST;

2) Pass it's reference to the library:
Code:
mcp.setMutex(TEST);

3) Use your mutex with your other devices, the MCP23S17 will use the mutex on the same SPI bus to prevent SPI collisions between threads and SPI peripherals.
 
Hi, im having trouble using this lib with my teensy 4, i have used another lib to successfully setting the outputs of the mcp from low to high and back. but this library just says no mcp detected. could anyone make a simple example of reading the (8)GPA pins and writing to the (8)GPB pins, my mcp chip select is pin37 of the teensy and all address bits are tied to ground, so adress is 0.

mcp.png
 
Yes, i tried, just get "no mpc detected" could it be that i i got some other lib named "mcp23s17.h" installed that interferes? if i get the no mpc ouput in the serial monitor i guess least the library works correct?
Its very strange as i could send data to the mpc with another lib. using the exact same wirig, using cs pin 37. But i had problems receiving data from the mpc, checking my oscilloscope i saw that there was data on all spi connections.
 
well it should work with demo above, i have 8 chips running on a T4.0 on SPI with pin 10 as CS

if it's wired and configured properly, mcp.info() will detect it
 
very strange.

using this lib i can write: https://github.com/sumotoy/gpio_MCP23S17/blob/master/gpio_MCP23S17.h

no sucess at reading though

code:
Code:
#include <SPI.h>
#include <gpio_MCP23S17.h>   // import library
int va = 3;

gpio_MCP23S17 mcp(37,0x20);//instance (address A0,A1,A2 tied to +)
int de = 100;
void setup(){
  mcp.begin();//x.begin(1) will override automatic SPI initialization
  mcp.gpioPinMode(OUTPUT);
}
void loop(){

 for(int i = 0; i < 8; i++){  
    mcp.gpioDigitalWrite(i, HIGH);
    
    delay(1);
    }
delay(10000);
}
 
if you have a problem at reading then yeah, that won't make my library work as it needs to read the registers. make sure your SPI MISO and MOSI lines are correct, theres an SPI connection issue somewhere
 
i dont know what to test anymore. very confused, i have continuity on the miso line, between teensy and the mcp, i have an adc sends data on the same spi bus just fine. there seem to be data sent and recived only when restarting my teensy, then theres not even clock output, the output from the mcp seem to contain very little data. Should it not try to at least send clock and data to the mcp regularly? not just once on startup?
 
Last edited:
Back
Top