-
• #2
...continued...
The questioning conversations triggered my activities to finally tackle MCP23017 PortExpander.
My MCP23017 lived so far neglected life since about the time when @DrAzzy gave it a push and provided the MCP23017.js module. Having also a key pad at hand (with a broke key... argh...) left me with no excuse to walk the talk... (as 'kind-a' outlined in mentioned questioning conversation).
Here a good cut of working code (also attached as file for easy 'grab' and play for yourself:
// MCP32017I2CA4x4KPD9.js // // MCP32917 (MCP32908) PortExpander driving // 4x4 Keypad using interrupt approach for key down // and intervalled chack for key up // simple (preferred) usage: ----- var d=` MCP32917 (MCP32908) PortExpander driving 4x4 Keypad using interrupt and setWatch vs. full scans to detect keyDown I2C1.setup({scl:B8, sda:B9, bitrate:1000000}); var kpd = new Kpd(function(keypad,key,upTime,pressTime){ console.log(key,"pressed for", pressTime); }).connect(I2C1,0,B0,A0).enable(); `; // key pad config ----- (used Espruino-Wifi and built-in I2C) // but can run on soft I2C aa well. var i2a = I2C1 // i2c appliance portexpander is connected to , i2c = B8 // i2c clock , i2d = B9 // i2c data , i2s = 100000 // i2c speed (bitrate per second 100/400/1700k) , pxa = 0 // portexpander 3-bit address (0..7) , pxi = B0 // interrupt - required w/ setWatch , pxr = A0 // reset (active low, 'clean' alternatives work too) , dbnc = 10 // debounce [ms] - optional , uchk = 100 // upcheck [ms] - optional ; // KPD - Key PaD definition ----- // constructor // // - downCB - down event callback (null if none); parms: // - kpd - keypad instance // - key - key number 0..15 // - time - time on down event (float from getTime()) // - upCB - up event callback (absent or null if none); parms: // - kpd - keypad instance // - key - key number 0..15 // - time - time on up event (float from getTime()) // - duration - press duration time (float, diff of up - down) // - cancelled - truey when keypad got disabled while key down // - (optional) debounce in ms - when is key to be considered stable // - (optional) upcheck in ms - when is key considered to be released var Kpd = function(upCB,downCB,debounce,upcheck) { this.upCB = upCB; this.downCB = downCB; this.debounce = (debounce) ? debounce : 0; this.upcheck = (upcheck) ? upcheck : 0; this.down = false; // state / key pressed this.key = -1; // last key this.dwnT = 0; // getTime() of down event (press) this.upT = 0; // getTime() of up event (release) this.pressT = 0; // press duration (down) this.watch = null; // watch sitting on interrupt this.timeout = false; // timeout to do debouncing this.enabled = false; // state / keypad enabled }, p = Kpd.prototype; // .connect() w/ setup i2c appliance, adjusted address (<<1) and interrupt // // NOTE: EXPECTS BANK=0 (after reset) - to switch to BANK=1 (once only) // - i2c // - addr - portextender 3-bit address 0..7 // - intr - interrupt on pin // - rst - reset on pin (omittable w/ RC HW reset) p.connect = function(i2c,addr,intr,rst) { this._rst(rst); var ioc = 128; // IOCONfig: BANK=1, SEQ, pinMode(intr,"input_pulldown"); this.intr = intr; // portexpander interrupt on pin this.i2c = i2c; // i2c appliance setp on scl and sda pins this.addr = 32+addr; // i2c address / adjusted and complemented this._w(10,ioc) // IOCON - I/O EXPANDER CONFIGURATION: BANK=1,SEQ ._w( 0, // starting with reg 00 and 8-bit mode sequentially [ 15 // 00 IODIR - I/O DIRection: 0/1 = out/inp , 15 // 01 IPOL - INPUT POLARITY: 0/1 = equal/opposite , 0 // 02 GPINTEN - INTERRUPT-ON-CHANGE: 0/1 = dis/en-able - FOR NOW ,240 // 03 DEFVAL - DEFAULT VALUE (for int trigger): don't care , 0 // 04 INTCON - INTERRUPT-ON-CHANGE CONTROL: 0/1 = do not/compare ,ioc // 05 IOCON - I/O EXPANDER CONfig: BANK=1,... (as above) , 15 // 06 GPPU - GPIO PULL-UP RESISTOR 100k: 0/1 = dis/en-abled ]); return this; }; p._rst = function(rst) { // private if (rst !== undefined) { pinMode(rst,"output"); rst.reset(); } this.rst = rst; if (rst !== undefined) { rst.set(); } }; // enable/disable (w/ arg false) keypad p.enable = function(b) { if ((b=((b===undefined)||b)) != this.enabled) { return (b) ? this._enable() : this._disable(); } return this; }; // all private / for convenience; NOTE: EXPECT BANK=1 p._w = function(r,dArr) { this.i2c.writeTo(this.addr,r,dArr); return this; }; p._ro = function(amnt) { return this.i2c.readFrom(this.addr,amnt); }; p._r = function(r,amnt) { return this._w(r,[])._ro(amnt); }; // watch callback for key down // private; NOTE: EXPECTS BANK=1 p._down = function(state, time) { this.watch = false; var dwnT = getTime() // capture down time , rnb = this._r(7,1)[0]; // 07 INTF: read interrupt flags,... this._w(2,0); // 02 GPINTEN: ...disable ints,... this.timeout = setTimeout(this._dChk.bind(this) // ...check deferred... ,this.debounce,dwnT,rnb); // ...down bits for row and col info }; // key down check (after debaounce time) // private; NOTE: EXPECTS BANK=1 p._dChk = function(dwnT,rnb) { this.timeout=false; var rc=0, rb=1, r=0, cnb, cc=0, cb=239, c=0; // prep to... while (++rc < 5 && !r) { // ...find 1st row bit for row info if (rnb&rb) r = rc; else rb <<= 1; } while (++cc < 5 && !c) { // ...find 1st col bit for col info cnb = this._w(9,cb)._r(9,1)[0]&15; // 09 GPIO: write/read - scan col if (cnb&rb) c = cc; else cb <<= 1; } this._w(9,15); // put 'normal' (all outs on low) if (r && c) { // row and col present, key still pressed this.dwnT=dwnT; this.down=true; this.key=(r-1)*4+(c-1); this.upT=0; this.timeout=setTimeout(this._uChk.bind(this),this.upcheck,true); if (this.downCB) setTimeout(this.downCB,1,this,this.key,this.dwnT); } else { // was a fluke/press within debounce this.timeout=setTimeout(this._uChk.bind(this),this.upcheck,false); } }; // key up check (by upcheck interval) // private; NOTE: EXPECTS BANK=1 p._uChk = function(frst) { var upT = getTime() // capture up time... , dwn = this._r(9,1)[0]&15; // 09 GPIO: or any downs if (frst || dwn) { this.timeout=setTimeout(this._uChk.bind(this),this.upcheck,!!dwn); } else { this.timeout=false; this.pressT=(this.upT=upT)-this.dwnT; this.watch = setWatch(this._down.bind(this),this.intr,{edge:"falling"}); this._w(2,15); // 02 GPINTEN - enable interrupts if (this.down) { this.down=false; if (this.upCB) { setTimeout(this.upCB,1,this,this.key,this.upT,this.pressT); } } } }; p._enable = function() { // private; NOTE: EXPECTS BANK=1 this._w(2, 0) // 02 GPINTEN - disable interrupts / prec ._r(8, 1); // 08 INTCAP - INT CAPTURE reg to clear int this._w(9,15) // 09 GPIO - G I/O reg: all outs low / prec ._w(2,15); // 02 GPINTEN - enable interrupts / prec this.watch = setWatch(this._down.bind(this),this.intr,{edge:"falling"}); this.enabled = true; return this; }; p._disable = function() { // private; NOTE: EXPECTS BANK=1 this.enabled = false; this.upT = getTime(); // take cancel/disable time this._w(2,0).r(8,1); // 02 GPINTEN / 08 INTCAP - disable/clear ints if (this.watch) { clearWatch(this.watch); this.watch = false; } if (this.timeout) { clearTimeout(this.timeout); this.timeout = false; } if (this.down) { this.down = false; this.pressT = this.upT-this.dwnT; if (this.upCB) { // disable while pressed, ...cancelled setTimeout(this.upCB,1,this,this.key,this.upT,this.pressT,true); } } return this; }; // KPD ----- callbacks ----- // key down callback var downCB = function(kpd, key, time) { console.log(key,"... down at ",time); // key, down time }; // key up callback (cancelled truy when disabled while pressing a key) var upCB = function (kpd, key, time, duration, cancelled) { if (!cancelled) { // key, upTime, pressDuration console.log(key,". up at ",time," after ",duration); } else { // key, upTime, pressDuration, ...cancelled by kpd.enable(false) console.log("keypad got disabled while pressing key ",key ," for ",duration," secs at ",time); } }; // KPD ----- examples ----- var kpd; function onInit() { i2a.setup({scl:i2c, sda:i2d, bitrate:i2s}); // kpd = new Kpd(upCB).connect(i2a,pxa,pxi,pxr).enable(); // preferred // kpd = new Kpd(null,downCB).connect(i2a,pxa,pxi,pxr).enable(); // kpd = new Kpd(upCB,downCB).connect(i2a,pxa,pxi,pxr).enable(); // kpd = new Kpd(upCB,downCB,dbnc,uchk) // full // .connect(i2a,pxa<<1,pxi,pxr,dbnc).enable(); // var kpd = new Kpd().connect(i2a,pxa,pxi,pxr).enable(); // ciao Arduino // var lastUpT = kpd.upT; // setInterval(function(){ // if ( ! kpd.down && (kpd.upT !== lastUpT)) { // console.log(kpd.key," up at ",(lastUpT = kpd.upT)); // } },50); } setTimeout(onInit,500); // for upload/development convenience (remove)
...to be continued...
1 Attachment
-
• #3
Nice - thanks! Do you think the interrupt handling is something that could be added to the existing MCP23017 module?
I'm wondering whether I could modify
setWatch
such that if it was given an object with a.watch
method, it would call that - then you could transparently use pins on an IO expander? -
• #4
...continued...
The MCP23017 I happened to get are I2C models, stamped: MCP23017 E/SP. Not as familiar to me as SPI, it gave me some grieve on top of the challenge what the universality of the chip already posed to me, especially the re-config from 16 bit to 8 bit mode (and of course also some lousy wiring flaws and coding typos made me burn some night oil).
The code is not the most leanest, but having implemented a touch UI, I knew about the needs and challenges to meet. Therefore, the 'Kpd' module (or class) supports the use of multiple types of callbacks: callback on key up (preferred), callback on key down, and both. It even supports a polling mode if you want to live an Arduino loop and lifecycle...
The code also supports bouncy hardware and clumsy key press and releases behavior.
Example output - with key number first in line - from provided sample callback look like this:
_____ _ | __|___ ___ ___ _ _|_|___ ___ | __|_ -| . | _| | | | | . | |_____|___| _|_| |___|_|_|_|___| |_| http://espruino.com 1v94 Copyright 2016 G.Williams > =undefined 0 . up at 946686483.38686466217 after 0.22292900085 5 . up at 946686484.37534046173 after 0.17869758605 9 . up at 946686487.18709087371 after 0.13181495666 7 . up at 946686513.53536319732 after 0.06225681304 9 . up at 946686515.38583564758 after 0.04290485382 1 . up at 946686517.51007080078 after 0.04504585266 2 . up at 946686519.69350910186 after 0.04776573181
and:
6 ... down at 946691897.44143199920 8 ... down at 946691898.60526180267 2 ... down at 946691898.89713573455
and both down AND up callbacks:
4 ... down at 946692008.95299339294 4 . up at 946692009.08904552459 after 0.13605213165 5 ... down at 946692010.02036190032 5 . up at 946692010.13677501678 after 0.11641311645 5 ... down at 946692014.20031738281 5 . up at 946692014.23853874206 after 0.03822135925 2 ... down at 946692016.39242458343 2 . up at 946692016.42747020721 after 0.03504562377 1 ... down at 946692021.18497848510 1 . up at 946692021.22566699981 after 0.04068851470 >
Usage with no callbacks - ciao Arduino - can look like this:
var kpd = new Kpd().connect(i2a,pxa,pxi,pxr).enable(); // ciao Arduino var lastUpT = kpd.upT; setInterval(function(){ if ( ! kpd.down && (kpd.upT !== lastUpT)) { console.log(kpd.key," up at ",(lastUpT = kpd.upT)); } },50);
and related output:
7 up at 946692250.16881752014 6 up at 946692252.47182941436 6 up at 946692253.89186477661 8 up at 946692255.25380229949 6 up at 946692264.55666446685
Even though the interval driven polling / Arduino loop is not the most efficient, it is though already a lot more efficient that doing just every thing with polling / in loop.
As mentioned earlier, the implementation could be leaner (
.enable()
and ```.disable() can be removed when arm part of .enable is put into connect), including more efficient to the point that also the key up is detected with interrupts and setWatch. Eventually I will get 'there'. Also, it would be nice to have a 'key repeat' callback and related configuration... *...I'm thinking about it.... (help / enhancement contributions are always welcome...). -
• #5
...@Gordon, you go my 6AM brain going with your #3 post...
After a while thinking about what components - hw side / js side - and lifecycle events current
setWatch()
/clearWatch()
entails and what the watch using application needs to contribute - I could see some optional additions in the watch options doing the job. These additions would mirror or replicate in a way all the kind of things you already do for the current 'plain' watches.For example, there must 'things' that:
- know to pull the data from the interruptor and create payload for downstream
- stick the payload into the JS queue for processing by callback in watch
- knows when and how to clear and re-setup interrupt depending on watch's repeat option
- provide means to detect loss and possibly prevent loosing interrupts
- provide access to the watch object (including its option object).
I'm sure that current STM MC / Espruino interrupt mechanism - including the event queue - is not a trivial thing... and going now through additional 'unpredictable' / semi-robust communication to perform the handling does not make it easier. Yes, it makes it simpler for the application / user, but the easier it is for the user the more complex it is for the system. A If we can find a robust solution along the 80/20 rule, that will be really great.
I said
'unpredictable' / semi-robust communication
because if I2C has an internal error, the whole application crashes... all is dead... a watch dog would have to restart the app, and the app would also have to 'feed' the watch dog properly so that it would not inadvertently restart. There is so much intertwined / layered / micro I2C calls that I have 'no clue' how I could 'easily'/not heavy handed catch internal errors / timeouts / exceptions... it just seems to be messy to me until now.
clearWatch() logically 'maps' to 'disable interrupt'. Therefore it is crucial that the watch object can be accessed by the handle and also options object with it's extension in order to handle things that are not easy to generalize / automate / require user/application involvement at particular life cycle events...
...all sounds... and is still very fuzzy...
I have to gain a bit more experience with the current all-in-application setup and let it mature before making specific suggestions / state requirements. I was also looking into the key matrix module how I could make a leaner version and from that derive change requests for the current setWatch() / clearWatch() / set/clear interrupts via I2c.
- know to pull the data from the interruptor and create payload for downstream
-
• #6
For keypads I'd imagine you could easily handle whatever you need outside of an IRQ in the setWatch without too much trouble.
I mean, what you're suggesting is basically the ability to run JS code in an IRQ, since you'd have to be able to access I2C to read the values back. You can run 'compiled' JS in an IRQ I believe, but really that's pushing things a bit far I think. I could enable JS IRQs, but it's likely to be super unstable.
-
• #7
I took a deep dive into the MCP23017 (I2C) Portexpander module (and rebuilt - no offense to be taken here) to understand it's wokings and prepare a test bed with full regression. So fare, regression includes validating reset and input only (activities are jotted down separate conversation: Exploring adding setWatch/clearWatch (inerrupt handling) to MCP23017 (MCP2308) Portexpander Ports).
While working through the code, I rethought the interrupt story... to the point where I'm thinking to add watch function to the portexpander pins and hide the fact that this makes only one watch in Espruino. The part from JS IRQs would only be for putting the application service request into the queue instead of calling the watch callbacks 'myself' (in a setTimeout to defer), in order to make the interrupt service routine to behave more like a hardware interrupt than a software interrupt... and achieve in shorter handling. I'm worried about loosing interrupts/events. This is anyway something that has to be looked into because while I2C communication with the portexpander goes on to deal with an interrupt, interrupt is disabled until port register or interrupt capture register is read. Port register may already be different from interrupt register because port register - as by definition - delivers the pin states when read, interrupt capture register - as by definition - captures/latches the pin states when the interrupt happened. In 8 bit portexpander (23008) and 16 in portexpander (23017) in 8-bit mode with separate, non-mirrored AB interrupt pins, the register latch 8 bit at once. In 16 bit / mirrored mode (AB interrupt pins), the registers latch 16 bit at once. Teasing out the details - which pin caused the interrupt from flag register(s() and what was its state at interrupt time from capture register(s) - by parsing the 8/16 bits and invoking (or placing into a queue) the application interrupt/watch callback for the related pins is an interrupt service routine by itself... like a service to the service...
As said, still in the phase of completing the test bed for current functionality. After that I will venture into adding watch capabilities in one or the other ways to the Portexpander Ports.
In respect to a Keypad solution, it could be just one dedicated keypad module specific to the use of a Portexpander rather than two modules - a module for a keypad using a module for interrupt enabled Portexpander...
-
• #8
Uploaded schematics / wiring as addition to first post #1.
-
• #9
application service request into the queue (instead of calling the watch callbacks - may be in a setTimeout - 'myself'),
You could do an
emit
on trigger and catch the event in another bit of the code which would get executed next idle... -
• #10
@Wilberforce,
emit
along the the line of pseudo code below:Setup of portexpander and enable interrupt on a pin A3:
var x = require("PX23017").connect(...); x.enableInterruptOn("A3", function(state, time, lastTime) { // service interrupt on pin A3 // code to serve interrupt ),{ ... }); // interrupt options...
with Internals of
.enableInterruptOn(pinName, callback, optionsObj) {...
... // handle the options object ... this.on(pinName,callback); // prep for the emit(... ...
and with
emit(...
on event... // figuring out event details, such as pin, state, time, lastTime var pinNane = ....; // for example "A3" ... this.emit(pinName, state, time, lastTime);
Espruino site provides a nice, lean example for driving a KeyPad Matrix. Code is lean, pin usage is pretty fat: 8 pins: 4 for columns and 4 for rows.
For applications where pins become a shortage and I2C, SPI, OneWire,... become a great and efficient tool to drive peripherals, MicroChips PortExpander MCP23017 / Datasheet with version for 16-bit / 2 x 8-bit (and MCP2308 - 8-bit) come in handy - see pic of breadboard setup.
Espruino provides an easy to use MCP23017.js module and application example MCP23xxx I2C and SPI port expanders. The provided module though just supports simple input and output - nicely emulating the single bit / one pin Port approach.
The existing module works fine for certain types of inputs and outputs, but for scanning for example a 4x4 or even 8x4 key pad, a lot of code has to be executed at a decent rate which eats away a lot of computing power and let's the application feel sluggish... If for sure may work but raises the question how to do it more efficient with, for example,
.setWatch()
(see conversation).The wiring follows the sketching found above conversation. How it looks tangible is shown in attached pic. The code is subject to following posts.
...to be continued...
2 Attachments