Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 16 of 16

Thread: New MCP23S17 Library

Hybrid View

  1. #1
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437

    New MCP23S17 Library

    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:
    Click image for larger version. 

Name:	mcp23s17.jpg 
Views:	48 
Size:	107.7 KB 
ID:	19290

    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
    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

    Click image for larger version. 

Name:	1582705159294-1.jpg 
Views:	34 
Size:	146.5 KB 
ID:	19291

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

    Repo: https://github.com/tonton81/MCP23S17
    Last edited by tonton81; 03-06-2020 at 08:49 AM.

  2. #2
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    12,236
    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?

  3. #3
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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.

  4. #4
    Senior Member+ MichaelMeissner's Avatar
    Join Date
    Nov 2012
    Location
    Ayer Massachussetts
    Posts
    3,837
    Quote Originally Posted by tonton81 View Post
    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.

  5. #5
    Member
    Join Date
    Jan 2020
    Location
    Port Elizabeth
    Posts
    51
    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.

  6. #6
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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

  7. #7
    Member
    Join Date
    Jan 2020
    Location
    Port Elizabeth
    Posts
    51
    Quote Originally Posted by tonton81 View Post
    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
    I look forward to it.

  8. #8
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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 by tonton81; 03-06-2020 at 08:34 PM.

  9. #9
    Senior Member+ MichaelMeissner's Avatar
    Join Date
    Nov 2012
    Location
    Ayer Massachussetts
    Posts
    3,837
    Quote Originally Posted by tonton81 View Post
    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.

  10. #10
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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

  11. #11
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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).

  12. #12
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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;
        }
    
        if (micros() - counter_GPIO[i] < 50) {
          return ( chipData[i][9] & (1UL << pin) );
        }
    
        bus->beginTransaction(SPISettings(speed,MSBFIRST,SPI_MODE0)); /* read port register */
        ::digitalWriteFast(chipSelect, LOW);
        bus->transfer16(((0x41 | (i << 1)) << 8) | 0x10);
        chipData[i][8] = __builtin_bswap16(bus->transfer16(0xFFFF)); /* Dump INTCAP */
        chipData[i][9] = __builtin_bswap16(bus->transfer16(0xFFFF)); /* read GPIOs */
        ::digitalWriteFast(chipSelect, HIGH);
        bus->endTransaction();
        counter_GPIO[i] = micros();
        return (chipData[i][9] & (1UL << pin));
        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);
          chipData[i][8] = __builtin_bswap16(bus->transfer16(0xFFFF)); /* Dump INTCAP */
          chipData[i][9] = __builtin_bswap16(bus->transfer16(0xFFFF));
          ::digitalWriteFast(chipSelect, HIGH);
          bus->endTransaction();
          counter_GPIO[i] = micros();
        }
    
        chipData[i][9] = (chipData[i][9] & ~(1UL << pin)) | (level << pin);
    
        bus->beginTransaction(SPISettings(speed,MSBFIRST,SPI_MODE0)); /* write port register */
        ::digitalWriteFast(chipSelect, LOW);
        bus->transfer16(((0x40 | (i << 1)) << 8) | 0x12);
        bus->transfer16(__builtin_bswap16(chipData[i][9]));
        ::digitalWriteFast(chipSelect, HIGH);
        bus->endTransaction();
    
        break;
      }
    }
    Last edited by tonton81; 03-07-2020 at 11:23 PM.

  13. #13
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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

  14. #14
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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.

  15. #15
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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 by tonton81; 03-11-2020 at 07:16 AM.

  16. #16
    Senior Member
    Join Date
    Dec 2016
    Location
    Montreal, Canada
    Posts
    3,437
    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.

    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •