Skip to content

drivers: add MCP23x17 I/O expander support #10688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 20, 2025

Conversation

gschorcht
Copy link
Contributor

@gschorcht gschorcht commented Jan 2, 2019

Contribution description

This PR adds a driver for Microchip MCP23x17 I/O expanders which can be used to extend the number of GPIOs over I2C or SPI. It is a complete rework of the driver that was provided in PR #7652 and rewritten from scratch.

The following expander devices are supported:

Expander Type Interface Pseudomodule
MCP23017 16-bit I/O expander I2C mcp23017 or mcp23x17_i2c
MCP23S17 16-bit I/O expander SPI mcp23s17 or mcp23x17_spi

The main features of the driver are:

  • Its interface is compatible with the peripheral GPIO interface. The only difference is that the functions have the prefix mcp23x17_ and require an additional parameter, the pointer to the expander device of type mcp23x17_
  • It supports the GPIO extension API as defined in PR periph_*: Allow extensions for external periph adapters #9582 and provided in PR periph/gpio: support for extension API #9860 and periph/gpio: support for extension API (part 2) #9958.
  • It supports the following GPIO modes
    • GPIO_IN, GPIO_IN_PU and GPIO_OUT in hardware
    • GPIO_OD and GPIO_OD_PU are emulated.
  • Optionally, the a low-active push-pull INT signal of MCP23x17 expanders can be used to provide external interrupt capabilities for all expander pins. The driver interface is compatible to module periph_gpio_irq
  • It supports direct GPIO mapping to SAUL which is compatible to the peripheral GPIO SAUL interface. Thus MCP23x17 I/O expander pins can be used with SAUL in the same way as peripheral GPIOS.
  • Multiple MCP23x17 expander modules and different variants can be used simultaneously.

Benchmarks

MCP23x17 driver run-time performance benchmark over SPI using 10 MHz clock speed:

                 nop loop:       634us  ---   0.063us per call  ---   15772870 calls per sec
                 gpio_set:    540009us  ---  54.000us per call  ---      18518 calls per sec
               gpio_clear:    540000us  ---  54.000us per call  ---      18518 calls per sec
              gpio_toggle:    811005us  ---  81.100us per call  ---      12330 calls per sec
                gpio_read:    271001us  ---  27.100us per call  ---      36900 calls per sec
               gpio_write:    539005us  ---  53.900us per call  ---      18552 calls per sec

MCP23x17 driver run-time performance benchmark over SPI using 1 MHz clock speed:

                 nop loop:       634us  ---   0.063us per call  ---   15772870 calls per sec
                 gpio_set:   1429044us  ---  142.904us per call  ---       6997 calls per sec
               gpio_clear:   1429005us  ---  142.900us per call  ---       6997 calls per sec
              gpio_toggle:   2144009us  ---  214.400us per call  ---       4664 calls per sec
                gpio_read:    715000us  ---  71.500us per call  ---      13986 calls per sec
               gpio_write:   1428004us  ---  142.800us per call  ---       7002 calls per sec

MCP23x17 driver run-time performance benchmark over I2C using 400 kHz clock speed:

                 nop loop:       639us  ---   0.063us per call  ---   15649452 calls per sec
                 gpio_set:   1965040us  ---  196.504us per call  ---       5088 calls per sec
               gpio_clear:   1960008us  ---  196.000us per call  ---       5102 calls per sec
              gpio_toggle:   3088493us  ---  308.849us per call  ---       3237 calls per sec
                gpio_read:   1127503us  ---  112.750us per call  ---       8869 calls per sec
               gpio_write:   1960004us  ---  196.000us per call  ---       5102 calls per sec

Reference board was an esp8266-esp-12x.

Testing procedure

The test application tests/driver_mcp23x17 can be used to test each MCP23x17 I/O expander pin with shell commands.

  1. Compile the application. Dependent on the MCP23x17 expander module, compile it with at least one of the pseudomodules mcp23x17_i2c or mcp23x17_spi, e.g., for the I2C version MCP23017:

    USEMODULE=mcp23x17_i2c make flash -C tests/driver_mcp23x17 BOARD=...
    
  2. Add the interrupt functionality by enabling module mcp23x17_irq and override the default GPIO pin for the interrupt signal, e.g. for the I2C version MCP23017:

    CFLAGS="-DMCP23X17_PARAM_I2C_INT=\(GPIO\(0,6\)\)" \
    USEMODULE="mcp23x17_i2c mcp23x17_irq"  flash -C tests/driver_mcp23x17 BOARD=...
    
  3. Connect the MCP23x17 I/O expander module using the interface and the GPIO for the interrupt signal as defined during compilation.

  4. Connect a GPIO pin of the MCU, e.g. GPIO_PIN(0,4), and a MCP23x17 I/O expander pin, e.g. Port A (port 16 in test application) and Pin 6.

  5. Execute some or all of the following commands (results in comment):

     > init_in 0 4
     > init_od_pu 16 6
    
     > set 0 4
     > read 0 4          # GPIO pin (port 0, pin 4)) is HIGH
    
     > clear 16 6
     > read 0 4          # GPIO pin (port 0, pin 4)) is LOW
    
     > init_od 16 6
     > init_in_pu 0 4
    
     > set 16 6
     > read 0 4          # GPIO pin (port 0, pin 4)) is HIGH
    
     > clear 16 6
     > read 0 4          # GPIO pin (port 0, pin 4)) is LOW
    
     > init_in 16 6
     > read 16 6         # MCP23x17 pin (dev 16, pin 6)) is HIGH
     > init_out 0 4 
     > read 16 6         # MCP23x17 pin (dev 16, pin 6)) is LOW
    
     > set 0 4 
     > read 16 6         # MCP23x17 pin (dev 16, pin 6)) is HIGH
    
     > init_int 16 6 0
     > clear 0 4         # INT: external interrupt from MCP23x17 pin 6
     > set 0 4
    
     > init_int 16 6 1
     > clear 0 4
     > set 0 4           # INT: external interrupt from MCP23x17 pin 6
    
     > init_int 16 6 2
     > clear 0 4         # INT: external interrupt from MCP23x17 pin 6
     > set 0 4           # INT: external interrupt from MCP23x17 pin 6 
    
     > disable_int 16 6
     > clear 0 4
     > enable_int 16 6
     > set 0 4           INT: external interrupt from MCP23x17 pin 6
     > clear 0 4         INT: external interrupt from MCP23x17 pin 6
    
     > init_int 16 5 2
     > disable_int 16 5
     > set 0 4           INT: external interrupt from MCP23x17 pin 6
    

Issues/PRs references

The driver is compatible with the GPIO extension API, PRs #9582, #9860 and #9958, but it does not depend on it.

@gschorcht
Copy link
Contributor Author

@ZetaR60 Again a GPIO expander driver that is working perfect with your last version of the extension API.

@gschorcht gschorcht added Type: new feature The issue requests / The PR implemements a new feature for RIOT Area: drivers Area: Device drivers Area: SAUL Area: Sensor/Actuator Uber Layer labels Jan 2, 2019
@stale
Copy link

stale bot commented Aug 10, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you want me to ignore this issue, please mark it with the "State: don't stale" label. Thank you for your contributions.

@stale stale bot added the State: stale State: The issue / PR has no activity for >185 days label Aug 10, 2019
@benpicco benpicco added the CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR label Sep 3, 2019
@stale stale bot removed the State: stale State: The issue / PR has no activity for >185 days label Sep 3, 2019
@stale
Copy link

stale bot commented Mar 10, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you want me to ignore this issue, please mark it with the "State: don't stale" label. Thank you for your contributions.

@stale stale bot added the State: stale State: The issue / PR has no activity for >185 days label Mar 10, 2020
@gschorcht gschorcht added State: don't stale State: Tell state-bot to ignore this issue State: waiting for other PR State: The PR requires another PR to be merged first labels Mar 10, 2020
@stale stale bot removed State: stale State: The issue / PR has no activity for >185 days labels Mar 10, 2020
Copy link
Contributor

@jeandudey jeandudey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a little bit old but left some comments for a first pass, I hope it gets a rebase in the near future as the driver seems very complete.

return -MCP23X17_ERROR_NO_DEV;
}
#endif
if (params->reset_pin != GPIO_UNDEF) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (params->reset_pin != GPIO_UNDEF) {
if (gpio_is_valid(params->reset_pin)) {

* If GPIO pin for interrupt signals INTA and INTB is defined in parameters,
* we use interrupts.
*/
if (params->int_pin != GPIO_UNDEF) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito

return MCP23X17_OK;
}

int mcp23x17_gpio_init(mcp23x17_t *dev, gpio_t pin, gpio_mode_t mode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm bit unsure about using gpio_t in here as some CPUs could define it as an structure, I think it may be more suitable to add a proper mc23x17_gpio_t type, same could apply for mode as CPUs could define their own and probably the IO expander has it's modes defined independently of the host.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gpio_t and gpio_mode_t are used here intentionally. As a result of the review of the first driver provided in PR #7652, the goal of this PR was to implement an API that is compatible with the GPIO API in order to integrate the GPIO expanders transparently.

At the moment, gpio_t must be a scalar for all GPIO implementations because there are hundreds of comparisons in RIOT that use the == operator.

In PR #12877 and later in PR #14602 I tried to introduce a GPIO API using a structured gpio_t type more than a year ago, but I didn't succeed until now. So there is no reason not to use gpio_t at the moment. Once, the structured GPIO is introduced, the driver could easily be changed, as the example of the low-level GPIO API implementation of an expander driver in PR #14602 shows.

@MrKevinWeiss MrKevinWeiss added this to the Release 2021.07 milestone Jun 22, 2021
@MrKevinWeiss MrKevinWeiss removed this from the Release 2021.07 milestone Jul 15, 2021
@github-actions github-actions bot added Area: doc Area: Documentation Area: tests Area: tests and testing framework labels Oct 27, 2021
@crasbe
Copy link
Contributor

crasbe commented Mar 11, 2025

But the Kconfig stuff is obsolete now anyways, so it could be deleted entirely.

I think my assessment back then was incorrect, but I can create a separate PR to add the support back in and debug the issues that were present at the time.


IMO this is good for merge. I went through it very thoroughly a couple of months ago and the test worked out too.
Since this is a driver, the impact on anything else would be minimal if we overlooked anything.

@benpicco benpicco added this pull request to the merge queue Mar 11, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 11, 2025
#define MCP23X17_EVENT_PRIO EVENT_PRIO_HIGHEST
#endif

/* interrutp service routine for IRQs */
Copy link
Contributor

@crasbe crasbe Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* interrutp service routine for IRQs */
/* interrupt service routine for IRQs */

This is the typo the static tests were complaining about. Odd that the workflow didn't catch it before?

Edit: a local run of make static-test doesn't complain about it either.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typo is still here 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😎

@crasbe crasbe added CI: full build disable CI build filter CI: no fast fail don't abort PR build after first error CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR and removed CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR labels Mar 21, 2025
@crasbe
Copy link
Contributor

crasbe commented Mar 26, 2025

So basically there are three issues that let the CI fail:

  1. The cc2538 CPU not supporting GPIO_OD and GPIO_OD_PU:
    #define HAVE_GPIO_MODE_T
    typedef enum {
    GPIO_IN = ((uint8_t)OVERRIDE_DISABLE), /**< input, no pull */
    GPIO_IN_ANALOG = ((uint8_t)OVERRIDE_ANALOG), /**< input, analog */
    GPIO_IN_PD = ((uint8_t)OVERRIDE_PULLDOWN), /**< input, pull-down */
    GPIO_IN_PU = ((uint8_t)OVERRIDE_PULLUP), /**< input, pull-up */
    GPIO_OUT = ((uint8_t)OVERRIDE_ENABLE), /**< output */
    GPIO_OD = (0xff), /**< not supported */
    GPIO_OD_PU = (0xff) /**< not supported */
    } gpio_mode_t;
    /** @} */
    #endif /* ndef DOXYGEN */

In this case, GPIO_OD and GPIO_OD_PU are both 0xff, which leads to the issue in the switch case.
Some other CPUs don't support GPIO_OD_PU, but that is not an issue because the mode is never 0xff.

I'm not sure yet what's the best way to solve this. The gpio_init function checks for 0xff. It would probably be possible to set GPIO_OD to a second value like 0xfe and change the condition to if (mode >= MODE_NOTSUP) with #define MODE_NOTSUP (0xf0)

int gpio_init(gpio_t pin, gpio_mode_t mode)
{
/* check if mode is valid */
if (mode == MODE_NOTSUP) {
return -1;
}

0xf0 is still very far away from the valid configuration values:

/**
* @brief Values to override pin configuration
*/
typedef enum {
OVERRIDE_DISABLE = 0x0,
OVERRIDE_ANALOG = 0x1,
OVERRIDE_PULLDOWN = 0x2,
OVERRIDE_PULLUP = 0x4,
OVERRIDE_ENABLE = 0x8,
} cc2538_ioc_over_t;


  1. The atmega8 doesn't have enough memory. This is easily solved by adding a tests/drivers/mcp23x17/Makefile.ci with the following content:
BOARD_INSUFFICIENT_MEMORY := \
    atmega8 \
    #

  1. The kintis CPU used by the mulle board and others include the unistd.h file, which is taken from newlib and implements the function read. This gives a conflict with the built-in read function.

The easiest solution would probably be to rename the internal read function to _read in line 340 and 485.


I tested the changes locally and they work.
@gschorcht if you want to, I can create a PR for the first point.

@Enoch247
Copy link
Contributor

FYI, you can use make -C tests/drivers/mcp23x17 generate-Makefile.ci to auto generate the Makefile.ci.

@gschorcht
Copy link
Contributor Author

gschorcht commented May 13, 2025

@crasbe Let's come back to this 6 years old PR which I had to revise completly a couple of times due to bigger changes in the master.

First of all, many thanks for testing the PR so comprehensive 👍
If it would be easy for you, I would prefer when you create a PR with your changes.

@crasbe crasbe added State: waiting for other PR State: The PR requires another PR to be merged first and removed State: don't stale State: Tell state-bot to ignore this issue State: waiting for maintainer State: Action by a maintainer is required labels May 13, 2025
@crasbe crasbe removed the State: waiting for other PR State: The PR requires another PR to be merged first label May 14, 2025
Copy link
Contributor

@crasbe crasbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the one "interrutp" typo. You can squash the changes directly.

If CI is happy, this can be merged :)

@crasbe crasbe added this pull request to the merge queue May 19, 2025
Merged via the queue into RIOT-OS:master with commit 876792e May 20, 2025
25 checks passed
@gschorcht
Copy link
Contributor Author

@crasbe Thanks for reviewing and merging.

@gschorcht gschorcht deleted the drivers_mcp23x17 branch May 20, 2025 05:34
@Teufelchen1 Teufelchen1 added this to the Release 2025.07 milestone Jul 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: doc Area: Documentation Area: drivers Area: Device drivers Area: SAUL Area: Sensor/Actuator Uber Layer Area: tests Area: tests and testing framework CI: full build disable CI build filter CI: no fast fail don't abort PR build after first error CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR Type: new feature The issue requests / The PR implemements a new feature for RIOT
Projects
None yet
Development

Successfully merging this pull request may close these issues.