• Hi there Gordon! I wanted to ask your advice on the best approach to take writing some keyboard matrix scanning code.

    I had a go at this a couple of years ago using an MCP23017, clocking the row outputs sequentially from the espruino & reading the column inputs on the MCP each cycle to determine which keys were pressed. This worked okay but I had a couple of weird quirks with the MCP chip & ended up burning way too many espruino cycles just turning the row lines on and off - not the best approach!

    I'm taking another crack at it, this time using an MDBT42Q instead of a Pico, I think using the NRF52's PPI to keep as much off the processor as possible is the right approach. I was thinking of using the NRF52's GPIO pins to drive the rows sequentially again, then listening for an interrupt from the MCP to know when to read its buffer to get the column states, and if anything has changed send it back up to the processor properly to send a keycode. I wanted to ask if that sounds sensible and, if so, which events and tasks it would make sense for me to set up.

    I appreciate that's a reeeeaallly long question, but I'd love a nudge in the right direction to get me going :)

  • Hi!

    For the KeyPad module, there's a cunning(ish) way of handling KeyPads that doesn't involve scanning them all the time: http://www.espruino.com/modules/KeyPad.j­s

    Basically you put all the columns into 'pulldown' state and watch them, then set all the rows to 1.

    So then you can just wait until the pins change state, and then do a proper scan to see what was pressed, then go back to watching the pins again.

    If you're only planning on handling one press at a time then you can just do the whole thing like that, since the state of the pins will change whenever a button is pressed or released.

    If you want to try and handle > 1 press (which isn't always guaranteed on a matrix keypad) then you'll have to keep scanning while a button is held, but even that isn't such a big deal as I imagine the buttons won't stay pressed for too long?

  • Thats a great suggestion, it sounds so obvious now you've said it :) I'll definitely be handling >1 press but that still takes a lot of load off the espruino, if nothing else it'll probably save me some power I would've burned sequentially turning pins on and off.

    I'm guessing I'll still want to do some messing around doing the scanning with the low level NRF52 lib to offload that to a peripheral rather than having the core do it. I'll likely still want to do processing on the core while keys are pressed/held to send keycodes with modifiers and things, but if I only have to do that after a pin state change thats great.

    My end goal is to build a mechanical keyboard firmware lib that handles the hardware level stuff & does a bit of extra helpful stuff like classify press/release cycles as a 'tap' or a 'hold' without the user having to do that, provides a lightweight mechanism for defining keymaps as a JSON file, and lets the user write their own javascript functions to make their keyboard do interesting stuff. This already exists in proper embedded code (QMK) but I'd love to do something similar on espruino since Javascript is so much more accessible :)

  • That sounds like a great project!

    I'm not sure using PPI for scanning (via MCP23017) is even possible. You could potentially use 'Compiled JS' or maybe even inline C to increase the poll speed though: http://www.espruino.com/Compilation

    You mentioned scanning a 4x16 matrix in http://forum.espruino.com/conversations/­352979/#comment15489686 - you should be able to do that with http://www.espruino.com/MDBT42Q without an IO expander?

    If so, you could use Compiled JS or Inline C to do the scanning and you could do it really fast - I don't think there'd be a need for PPI or anything like that to help you out.

  • Thanks, I'm looking forward to getting it working :)

    I'm going to give the compiled JS a shot today hopefully! You're right that I wouldn't need the MCP chip to scan a 4x16 matrix, originally I wanted to do it that way for future expandability but the MDBT42Q has 32 GPIO pins, so that gives me the headroom to do a full size keyboard (6x22) with some extra GPIO to spare to do things like LED backlighting/dedicated buttons

    So, set up watches on my columns, only scan when a watch gets triggered to stop us burning processor cycles at idle, and do the scanning in compiled JS or inline C to get a speed boost - that sounds like enough for me to get going with and see how far I get :)

    Thanks for helping me out on this one, I might go quiet for a couple of days while I try this out but I'll post and update to let you know how its working out!

  • Great!

    Just to add, I think you can have a maximum of 8 watches on nRF52 - so if you're doing 4x16 you'll need to watch the 4, not the 16 :)

  • @27percent,

    I'm sure you already read thru the conversation about Exploring adding setWatch/clearWatch (interrupt handling) to MCP23017 (MCP2308) Portexpander Ports
    and its related conversations.

    It's a good approach to scan only after an interrupt happened.

    Regarding the setting and reading of pins, array is of help. Unfortunately, setting pin modes has to happen pin by pin. I once had asked @Gordon to also consider the array for setting pin mode the same way as reading and writing to pins goes.

    I used similar approach - setWatch() and scanning after event - for resistive touch screen without driver (Espruino is directly driving and reading the 4 electrodes of a resistive touch screen: Resistive Touchscreen directly (no touch controller)). I later used it in the UI modules http://www.espruino.com/ui (how come is in conversation: Modular and extensible UI framework and ui elements.).

  • Hey! Just wanted to post an update, I had a go at implementing the suggestions yesterday evening & got something working - it catches even fast button presses but occasionally drops them which is something I'm looking into. I've attached some code, its still ugly POC code & I'm sure there are things I can do to make it run more efficiently :)

    //======================================­=================================
    // MATRIX
    //======================================­=================================
    const matrix = (rows, cols) => {
      let _scanning;
      let _onStopScanning;
      let _numberOfIdleScansBeforeStop;
      let _numberOfActiveKeys;
      let _numberOfIdleScans;
      let _keyStates;
      let _rowStates;
      let _cols = cols;
      let _rows = rows;
      
      const defaultKeyStates = new Array(_rows.length).fill(new Array(_cols.length).fill(0));
      // const resetKeyStates = () => _keyStates = defaultKeyStates;
    
      // Make this flexible for different rows/cols
      const resetKeyStates = () => _keyStates = [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
      ];
      // Make this flexible for different rows/cols
      const resetRowStates = () => _rowStates = [ 0, 0, 0, 0 ];
      const resetNumberOfIdleScans = () => _numberOfIdleScans = 0;
      const resetNumberOfActiveKeys = () => _numberOfActiveKeys = 0;
      
      const getMatrixState = () => _keyStates;
      const getKeyState = (row, col) => _keyStates[row][col];
      
      const setPinModes = () => {
        "compiled";
        // @TODO: Can we set an array of pinmodes instead of iterating?
        _rows.forEach(row => pinMode(row, 'output'));
        _cols.forEach(col => pinMode(col, 'input_pulldown'));
      };
      
      const unsetPinModes = () => {
        "compiled";
        _rows.forEach(row => pinMode(row, 'input_pulldown'));
        _cols.forEach(col => pinMode(col, 'output'));
      };
      
      const setup = () => {
        "compiled";
        resetNumberOfIdleScans();
        resetNumberOfActiveKeys();
        resetKeyStates();
        resetRowStates(); 
      };
    
      const startScanning = (onChange, onStop, numberOfIdleScansBeforeStop, interval) => {
        "compiled";
        console.log("======================");
        setPinModes();
        _numberOfIdleScansBeforeStop = numberOfIdleScansBeforeStop;
        _onStopScanning = onStop;
        if (!_scanning) _scanning = setInterval(() => scanMatrix(onChange), interval || 1);
      };
      const stopScanning = () => {
        "compiled";
        resetNumberOfIdleScans();
        resetNumberOfActiveKeys();
        resetKeyStates();
        resetRowStates();
        unsetPinModes();
        if (_scanning) { clearInterval(_scanning); _scanning = undefined; }
        if (_onStopScanning) _onStopScanning();
      };
      const isScanning = () => !!_scanning;
      
      const updateNumberOfIdleScans = (activeKeys, callback) => {
        "compiled";
        if (activeKeys === 0) _numberOfIdleScans = _numberOfIdleScans + 1;
        callback();
      };
      const stopOnIdle = () => {
        "compiled";
        if (_numberOfIdleScansBeforeStop && (_numberOfIdleScans >= _numberOfIdleScansBeforeStop)) stopScanning();
      };
      
      const updateNumberOfActiveKeys = (isActive) => {
        "compiled";
        if (isActive) {_numberOfActiveKeys = _numberOfActiveKeys + 1;}
        else if (_numberOfActiveKeys > 0) {_numberOfActiveKeys = _numberOfActiveKeys - 1;}
      };
    
      const parseRowToString = (row) => {
        "compiled";
        return row.toString(2);
      };
    
      const detectChange = (rowState, currentRowIndex, onChange) => {
        "compiled";
        if (rowState !== _rowStates[currentRowIndex]) {
          _rowStates[currentRowIndex] = rowState;
          const parsedRow = parseRowToString(rowState);
    
          // @TODO: This feels inefficient
          for (let colIndex = 0; colIndex < parsedRow.length; colIndex++) {
            const col = Number(parsedRow[colIndex]);
            if (col !== _keyStates[currentRowIndex][colIndex]) {
              _keyStates[currentRowIndex][colIndex] = col;
              onChange(currentRowIndex, colIndex, col);
              updateNumberOfActiveKeys(col);
            }
          }
          
          
        }
      };
    
      const scanMatrix = (onChange) => {
        "compiled";
        _rows.forEach((currentRow, currentRowIndex) => {
          digitalWrite(currentRow, 1);
    
          detectChange(digitalRead(_cols), currentRowIndex, onChange);
    
          digitalWrite(currentRow, 0);
        });
        updateNumberOfIdleScans(_numberOfActiveK­eys, stopOnIdle);
      };
      
      setup();
    
      return {
        getMatrixState: getMatrixState,
        getKeyState: getKeyState,
        startScanning: startScanning,
        stopScanning: stopScanning,
        isScanning: isScanning
      };
    };
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    //======================================­=================================
    // USAGE
    //======================================­=================================
    const ROWS = [D14, D15, D17, D30];
    const COLS = [D16, D18, D19, D20, D22, D25, D26, D27, D28, D29, D31];
    
    let _rowWatchers;
    
    const myMatrix = matrix(ROWS, COLS);
    
    const onMatrixChange = (currentRow, currentCol, isActive) => {
      // Keep track of key press/release duration
      // Classify as tap or hold
      
      // keyPress when a key is pressed, payload containing key row/column
      // keyRelease when a key is released, payload containing row/column, duration of press
      // keyTap when a key press/release cycle is < 200ms, payload containing row/column
      // keyHoldStart when a key has been pressed for > 200ms, payload containing row/column
      // keyHoldEnd fired when held key is released, payload containing row/column, duration of press
      console.log(currentRow, currentCol, isActive);
    };
    
    const onMatrixStop = () => {
      "compiled";
      enable();
    };
    
    const handleWatchTrigger = (e) => { 
      "compiled";
      unsetWatches(_rowWatchers);
      setColsLow(COLS);
      myMatrix.startScanning(onMatrixChange, onMatrixStop, 2, 1);
    };
    
    const setColsHigh = (cols) => {
      "compiled";
      cols.forEach(col => digitalWrite(col, 1));
    };
    const setColsLow = (cols) => {
      "compiled";
      cols.forEach(col => digitalWrite(col, 0));
    };
    const setWatches = (rows) => {
      "compiled";
      _rowWatchers = rows.map(row => setWatch((e) => handleWatchTrigger(e), row, { repeat:false, edge:'rising', debounce: 10 }));
    };
    
    // @TODO: Make this more resilient, this errors when it can't clear a watch
    const unsetWatches = (rowWatchers) => {
      "compiled";
      rowWatchers.forEach(rowWatcher => clearWatch(rowWatcher));
    };
    
    const enable = () => {
      "compiled";
      ROWS.forEach(row => pinMode(row, 'input_pulldown'));
      COLS.forEach(col => pinMode(col, 'output'));
      setColsHigh(COLS);
      setWatches(ROWS);
    };
    
    enable();
    
    
  • Just a quick question on the pinModes/setWatch method - is there a way to set an array of pin modes & watches like how I'm able to read an array of pins with digitalRead? I couldn't find anything about it in the docs, is it something on the roadmap? I'm trying to get rid of any unnecessary loops to get this to run faster and I think It'd be really useful :)

  • is there a way to set an array of pin modes & watches like how I'm able to read an array of pins with digitalRead?

    I'm afraid there isn't anything at the moment, no...

    As you had done, COLS.forEach(col => pinMode(col, 'output')); would work pretty well though, or maybe: COLS.forEach(col => col.mode('output'));

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

Taking another crack at keyboard matrix scanning using the NRF52 PPI

Posted by Avatar for consolenaut @consolenaut

Actions