Port expander (MCP23017) and setWatch

Posted on
  • I'm using an 8x8 keypad matrix, to save pins I am planning on using MCP23017 port expander (I2C). Will I be able to add button listener to make this work (via setWatch)?


  • Sure, MCP23017 has a really nice event / interrupt features...

    Configure an interrupt, connect it to a pin and watch that pin for the press off a button. On event, scan the keyboard, set a different interrupt and watch for the release of the button. This way you preserve the most cycles.

    Simpler - but spending cycles - is to scan the keyboard every 20..100 ms. A simple scan will not distinguish nor prevent multi button presses. The existing module makes MCP23017 pin usage quite transparent.

  • Thanks for the reply @allObjects - So I did a quick test, hooked up via I2C and instantiated as "port". Tried:


    Got an error:

    ERROR: Invalid pin

    I suspect that the virtual pin is not compatible with the Pin() object the setWatch function expects...

    Or, did you meant hooking up setWatch differently?

  • I went ahead and tried the interval route, and seemed to have made it work. Possible downside is the interval eating resource for other stuff, but I think I can get away with it by being smart in clearing the interval when input is safely not needed.

    My solution:

    var i2c = new I2C();
        scl: B2,
        sda: B3
    var address = 0; //this is the address, set by the address pins.
    // for MCP23017
    var port = require("MCP23017").connect(i2c, null, address);
    var colPins = [port.A7, port.A6, port.A5, port.A4];
    var rowPins = [port.A3, port.A2, port.A1, port.A0];
    //set col to low
    for (var i in colPins) {
    //set row to 1 via pull up
    for (var j in rowPins) {
    var col = -1;
    var row = -1;
    var lastButton = null;
    function mapToButton(c, r) {
        if (c === 3 && r === 3) return ("B1");
        if (c === 2 && r === 3) return ("B2");
        if (c === 1 && r === 3) return ("B3");
        if (c === 0 && r === 3) return ("B4");
        if (c === 3 && r === 2) return ("B5");
        if (c === 2 && r === 2) return ("B6");
        if (c === 1 && r === 2) return ("B7");
        if (c === 0 && r === 2) return ("B8");
        if (c === 3 && r === 1) return ("B9");
        if (c === 2 && r === 1) return ("B10");
        if (c === 1 && r === 1) return ("B11");
        if (c === 0 && r === 1) return ("B12");
        if (c === 3 && r === 0) return ("B13");
        if (c === 2 && r === 0) return ("B14");
        if (c === 1 && r === 0) return ("B15");
        if (c === 0 && r === 0) return ("B16");
    function scanCol() {
        //set col to low
        var i, j;
        for (i in colPins) {
        //set row to 1 via pull up
        for (j in rowPins) {
        for (i in rowPins) {
            if (rowPins[i].read() === 0) {
                col = i;
    function scanRow() {
        //set row to low
        var i, j;
        for (i in rowPins) {
        //set row to 1 via pull up
        for (j in colPins) {
        for (i in colPins) {
            if (colPins[i].read() === 0) {
                row = i;
        if (col >= 0 && row >= 0) {
            var btn = mapToButton(col, row);
            if (lastButton !== btn) {
                lastButton = btn;
    setInterval(function () {
        col = -1;
        row = -1;
        if (col >= 0) {
        } else {
            lastButton = null;
    }, 50);

    Would still love to hear if it is possible to use setWatch btw!

  • @n00b, sorry to be not clear enough in my initial post.

    The watch is not on a virtual pin made visible through the available driver (module). The watch is on an Espruino pin(s) connected to one(both) of the interrupt pin(s) INTA (and INTB) of the MCP23017 Portexpander.

    You still need to do some scanning similar you did with the interval, but you do only when a press happens and you do less. You do less because the you get already row (or col) by reading the register that loads the pin states at interrupt time.

    The current module is more for predictable/'static' behavior... When I came across the MCP23017 Portexpander, I tried to write a module that supports all options. But there are too many to generalize, and the module would become too fat. Therefore, I gave up and decided to go custom for the particular application when using it.

    The general logic for time multiplexed reading of up to 64 buttons is:

    Setup for watching press event:

    • reset
    • configure 8 pins out, 8 pins input pull-up
    • configure an interrupt on the 8 input pins
    • set watch for press
    • enable interrupt

    On watch (interrupt) press event:

    • disable interrupt (if it not already happened automatically)
    • read the capture register (which gives you the row (or col))
    • loop through 8 output pins:
      • set only the 1 pin on high
      • read the 8 input pins
      • if any of the 8 input pins is low, then
        • the current output pin that is high denotes the col (or row)
        • you can exit (break the loop)

    Setup for release event:

    • put all outputs back to high
    • set watch for release
    • enable interrupt

    On watch (interrupt) release event:

    • disable interrupt (if it not already happened automatically)
    • read the capture register (which gives you the row (or col))
      • if any of the 8 bits is still low,
        • then there is still a button pressed and you resume with 'Setup for release event:'
        • else all buttons are released and you resume with
        • set watch for press event
        • enable interrupt

    The above pseudo code does not talk about how to preset the register for detecting the interrupt. In the setup for watch press, it is all high. For the preset of release detection, you have to determine empirically (may what was read by the interrupt capture register). Also, no cover all corner cases are covered, such as things already changed again since the interrupt. This happens especially with bouncing (and this bouncing may have to be captured differently than in the bounce option of the watches). To capture/overcome switch bouncing is not a trivial thing... see conversation about Problems with setWatch.

  • Thanks for the detailed explanations. Seems like since I have the row and column scanning routines already, the remaining thing to do is set up the interrupt. Will this work like a switch? So if I connect INTA to say B13 on espruino, do I simply setwatch B13 and anytime button is pressed on the matrix an event will fire? Or do I need to set up something between the expander GPIO and the INTA?

    Much appreciated!

  • do I simply setwatch B13 and anytime button is pressed on the matrix an event will fire?

    ...not exactly, but closely...

    Depending how you setup the interrupt, it detects a change of a pin or it detects a difference with a preset value.

    I can go a bit more into detail when you describe how you plan your circuitry.

    PortExpannder has two ways to generate an interrupt: a pin changes state (from low to high or high low) or a pin's state is different from the specification in the in register in which you write the resting state: all high (1)... the same as your inputs have with no button pressed. Then you activate the interrupt. As soon as a port changes (is pulled low by pressing the switch), PortExpander notices a difference and fires the interrupt. Take a look at the data sheet , pages 12 and following.

  • Looking at the datasheet, seems like I need to setup the expander's interrupt pin. How can I do that? Is it by writing via I2C? I'm afraid a lot of these hardware concepts is flying over my head :) Here's how I wire up the expander, espruino, and the matrix (omitting some details such as I2C pullup resistors, power, and gnd):

  • Nice pic of the schematics!

    I assume you are not connecting A0..A2 to anything and are thus setting HAEN - Hardware Address Enable - bit to 0 in control byte. (This feature allows you to address 8 PortExpanders (hard-wired, and even more, 'soft-wired').

    setup the expander's interrupt pin... by writing via I2C?

    That's correct. You write to the control/config registers of the PortExpander. They are listed on p12 of the datasheet: TABLE 3-1: REGISTER ADDRESSES.

    This is for creating in interrupt when ever the value changes on an input pin:

    • 3.5.1 I/O DIRECTION REGISTER (p18): declare 4 pins as output, 4 as input (register address 0, value 7)
    • 3.5.3 INTERRUPT-ON-CHANGE CONTROL REGISTER (p19): declare 'interrupt on change' for the input pins (register address 2, value 7)
    • 3.5.5 INTERRUPT CONTROL REGISTER (p20): declare '*interrupt on change compared with previous value' for the input pins (register address 4, value 0)
    • 3.5.6 CONFIGURATION REGISTER (p20): (register 5). I would start with value 0. Since this resets bit 1, the watch has to be on falling edge.
    • 3.5.7 PULL-UP RESISTOR CONFIGURATION REGISTER (p22): declare 'pull-up' for the input pins (register address 6, value 7). This means, that you have to write all 0 to the output pins. When a button is pressed, the input changes state to low. The scan has then to set only one after the other to low to figure out the pressed button's col.

    Have fun... (now you may understand why a generic module would be ginormous and practically just an unnecessary repetition of what a direct config already is).

  • Tried your code as published in post #4... What ever A0..3 I connect with A4..7, the rowScan() never returns something... I used the Espruino-Wifi with your connections B6/B7 (not B2 and B3 as in code shown by you) and also with B8/B9. I tried soft I2C (var i2c = new I2C()) as well as the built in I2C1.

  • @n00b, got a version with interrupt and setWatch going. I was a pretty useful experience. I had MCP23017 laying around for quite a long time... but the complexity and many options had raised the threshold to pick it up. Now it is done (for the most part). To really not miss any events or get stuck in a particular state, setInterval() is still a good helper. Furthermore, catching bouncing and other not so nice reality effects are interesting matter. Looking forward to hear from you.

  • @allObjects That's awesome that you made your version! I'm dying to get back to the project, but still can't touch it until next week (if only I can do this all day everyday :)). I'll give it a shot, first reimplementing the interrupt setup and handling with my code to learn, and give your 'module' a try. I'd really be interested in comparing the perf of setInterval with your implementation. I moved to a NodeMCU esp8266 for a fuller build (with 3d printed chassis) - putting Date().ms comparison at the start and the end of my scan routine with setInterva of 50ms gives me ~50ms so no hit, but when I moved to try on ESP32 (for more pins) I get 150ms. Could this be also because of I2C performance difference?

    Anyway, thanks so much, will try and report back.

  • Really? 150ms,...

    I get fully handled button down-up sequences (pressTimes) done in less than 20ms... without loading the system with scanning... it all depends how shortly you can press the button and how you set the optional debounce and upcheck times (there is even room in the 'button definitively up' check mechanism that could shorten the times even more... and the I2C communication runs on the slowest bitrate: 100K (it can be up to 1.7M, with SPI up to 10MHz, but requires solid wiring... breadboard connection quality and related wiggle on key presses gave me I2C timeouts... ;(...

    My scan to decode the col at interrupt event has average of two (2) - half the number of cols - I2C writes/reads - 2 writes and 1 read - with a 1 bit shift and 3 comparison (includes the loop checks)... and this all in Espruino JS. Even if you keep the polling architecture for input in your app as I showed with the

    ciao Arduino

    application example, you gain a lot not wasting all this scanning when nothing happens AND gain responsiveness in your application.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview

Port expander (MCP23017) and setWatch

Posted by Avatar for n00b @n00b