Digital I/O Touch Detection Code Improvements

I have been working with the same basic approach to digital touch detection since the inception of the Keyglove. That process goes something like this:

  1. Pull all touch points to logic high
  2. Select possible combination [X, Y] from touch definition array
  3. Set point [X] low
  4. Measure the level of point [Y], and if it is low, then touch combination [X, Y] is active
  5. Increment test combination index and go back to step #2 until complete

At first, this method of scanning to detect connectivity between arbitrary I/O pins without mechanical switches seemed wonderfully functional and easily implemented. I used Arduino’s built-in digitalWrite() and digitalRead() functions, which was the simple solution. It was also plenty fast enough at the time. However, it has a couple of now-obvious shortcomings, plus another not-so-obvious one which I am hoping that I diagnosed correctly.

  • There’s a lot more bit flipping than there needs to be. For example, the “Y” sensor gets pulsed low five times during the course of all combination tests, and it only really needs to be pulsed once if the subsequent corresponding pin reads are all done in the right order.
  • The digitalWrite() and digitalRead() functions are ridiculously slow compared to direct port manipulation (~20 times slower).
  • The specific polling speed is generating some capacitance or crosstalk or I don’t even know what, but the end result is that I’m getting phantom touch reports when nothing is actually touching. The problem is far, far less likely to occur when I set the CPU to 4MHz instead of 8MHz, which is what leads me to believe it has to do with the polling frequency and pattern.

My original I/O read loop looks like this:

// loop through every possible 1-to-1 sensor combination and record levels
for (i = 0; i < KG_BASE_COMBINATIONS; i++) {
    p1 = combinations[i][0];
    p2 = combinations[i][1];

    pinMode(p1, OUTPUT);    // change to OUTPUT mode
    digitalWrite(p1, LOW);  // bring LOW (default input level is HIGH)
    if (i < 32) {
        // write value to sensors1
        if (digitalRead(p2) == LOW) bitSet(detect1, i);
    } else {
        // write value to sensors2
        if (digitalRead(p2) == LOW) bitSet(detect2, i - 32);
    }
    pinMode(p1, INPUT);     // reset to INPUT mode
    digitalWrite(p1, HIGH); // enable pullup
}

It produces pulses that are about 11.4 microseconds wide, and the whole cycle through all tests takes about 2 milliseconds (or ~20% of the 10ms-wide interval allowed for the entire board to run at 100 Hz):

keyglove_io_polling_loop

It's really very clean and functional, and certainly a straightforward way to do it without a complete architecture shift and writing code that will only function with one specific Arduino or Teensy or Keyglove board design at a time. Well, honestly, 20% of a 10ms cycle is way too long for that functionality. Improving it is definitely worth writing some platform-specific code with a new architecture.

So I did.

The new code directly accesses the PIN and PORT registers that the AT90USB1286 provides (e.g. DDRB, PORTB, and PINB), and it will only work properly on that specific MCU. It also avoids any kind of abstracted pin definition array and runs through the entire complement of touch combinations manually in one long list. I'm not opposed to loops, of course, but it's difficult to work efficiently with hardware registers at a bit level when using loops.

Here's what one portion of the new code looks like, the part that checks all of the "Z" combinations:

// check on Z combinations (PB5)
SET(DDRB, 5);       // set to OUTPUT
CLR(PORTB, 5);      // set to LOW
delayMicroseconds(3); // give the poor receiving pins a chance to change state
_pina = PINA; _pinf = PINF;
CLR(DDRB, 5);       // set to INPUT
SET(PORTB, 5);      // pull HIGH
if (!(_pinf & (1 << 2))) touches[3] |= 0x02;    // M (PF2)
if (!(_pinf & (1 << 3))) touches[3] |= 0x04;    // N (PF3)
if (!(_pinf & (1 << 4))) touches[3] |= 0x08;    // O (PF4)
if (!(_pina & (1 << 2))) touches[3] |= 0x10;    // P (PA2)
if (!(_pina & (1 << 1))) touches[3] |= 0x20;    // Q (PA1)
if (!(_pina & (1 << 0))) touches[3] |= 0x40;    // R (PA0)

The approach here is streamlined in two different ways compared to the previous approach:

  1. Direct register access is used instead of pinMode(), digitalRead(), and digitalWrite(). This is MUCH faster.
  2. Combinations are set and read in batches instead of one at a time. So all combinations that use "Z" are done at once by setting "Z" low and then individually reading the pin register bits for all other potentially affected pins. This means that instead of doing 240 pin mode/level change operations for all 60 combinations, I only have to do 11 (and even this could be reduced further). Also, because of point #1 here, each of those 11 actions executes about 20 times faster than each of the 240 actions before.

The bottom line is that the new approach completes the entire test in about 50 microseconds instead of 2 milliseconds. That is a speed increase by a factor of 40. Amazing.

One interesting hiccup I had to work through was that although the code looked right and the majority of the touch combinations appeared to work, there were some that just would not register properly, even though they worked when I manually tied the failing pins to ground. The solution turned out to be that delayMicroseconds(3) call. The failing combinations involved, in every case, the input pins on the first port that I read after changing the logic level of the single output pin being tested. Since the very next call was to read the other port status, this executed so fast that the internal pin circuitry didn't have enough time to detect the voltage change by the time I tried to read it. Go figure! Adding a tiny delay solved the problem instantly.

The above code (direct register polling) is what I have running now for development, but there is actually something more I need to do to improve it significantly: stop polling and use interrupts instead, at least while there are no active touches. This will allow the MCU to sleep for the vast majority of the time, greatly extending battery life. The downside to interrupts is that there is no easy way to detect exactly which two pins were connected together, unless you have a zillion different interrupts. I don't, and I wouldn't want to do that even if I could, because it's too complicated given the other option. Instead, this will be the approach:

  1. Enable "falling edge" interrupts for a subset of pins (e.g. palm and thumb sensors), and pull them all high.
  2. Set the rest of the pins to output/low.
  3. When any of the interrupts are triggered, enter the polling loop to determine exactly which touches are active.
  4. As soon as no more touches are active, stop polling and go back to interrupt-only.

Even when actively in use, I expect that touches won't be active for more than 20% of the time due to human dexterity and reaction time. Therefore, I expect this approach should let the MCU sleep for a lot of the time that the device is on.

1 Comment
  1. I suppose you are aware of Pauls (Teensy developer) improvements to Arduino core like digitalWriteFast which compiles down single instruction (but requires constact as pin number, so any loops etc must be unrolled first), I can’t recall if there are similar versions of pinmode.

    Portability is important so it miht be a good idea to put these platform specific port manipulations to macros separate platform headers.

Leave a Reply