Bramley: 5. Buttons

25 October 2020

Using the rppal library and the Raspberry Pi's GPIO pins, I've added support for the six Cherry ML switches on the back of the Bramley.

The front and back of the Bramley, showing the 6 Cherry ML switched on the back

Raspberry Pi to Buttons
-----------------------
GPIO 16 -> Button 1
GPIO 20 -> Button 2
GPIO 21 -> Button 3
GPIO 25 -> Button 4
GPIO 24 -> Button 5
GPIO 23 -> Button 6

Reading from the pins is easy enough, but to correctly detect a key press, I've got some more work to do.

Interference

Without a pull-up or pull-down resistor, the value read from a GPIO pin will be floating. As a digital input, it will hover between 0 and 1, depending on background interference, and, if I'm not careful, produce unwanted key presses.

Normally, I rely on the Raspberry Pi's internal pull-up resistors to deal with this because they're easy to enable in software, and that's my comfort zone.

let gpio = Gpio::new()?;
let pin = gpio.get(16)?.into_input_pullup();

But, this time, despite enabling the internal resistor I was still experiencing 'phantom' key presses. Determined to exorcise them using hardware, I wired another 10K pull-up resistor to each switch:

GPIO pin      Button        GND
   |____________/ ___________|
          |
          Z  <- 10K resistor
          |
       3V3 pin

That seemed to block out the noise. I'm now ready to try and make sense of the signal.

Debouncing

An actuated switch doesn't produce a signal that's immedately high or low. Before stabalising, it will bounce between each state.

A GPIO signal starting high, bouncing on button press, and ending low

If I watch only for transitions from high to low (1 to 0), I would incorrectly count three keypresses. This could be avoided in hardware by using a capacitor to smooth the transition, but I'm going to do it in software where the solution is to take several readings and wait for the signal to stablise.

Normally, I make use of interrupts and timeouts to wait for a stable signal, but the code is always more complicated than I'd like. So this time I've decided to treat the Pi more like a microcontroller and just poll the pins at a regular iterval.

Every 3 milliseconds, I read a digital high/low from the six GPIO pins and emit new button events according to these rules:

If both of those are true, I can emit a new event. The code works something like this:

// Read a high/low value from the button's GPIO pin
let level = btn.read();

// We're only interested if the state has changed from
// what we last sent on the channel.
if level != state {
    // Did we read 8 stable values in a row prior to
    // this (i.e. all bits are 0 or 1)?
    if history == 0 || history == 255 {
        // Update the button's state
        state = level;
        // Send a new button Up/Down event
        tx.unbounded_send(state).unwrap();
    }
}

// Push the latest read onto the history integer by updating
// the bit at the end.
history = match level {
    Level::High => history.rotate_left(1) | 0b00000001,
    Level::Low => history.rotate_left(1) & 0b11111110,
};

// Wait 3 ms before polling again
thread::sleep(Duration::from_millis(3));

For the full context see the source code.

By waiting for 8 stable reads at 3ms intervals, I give the signal 24ms to stablise. And by storing the high/low bits in an unsigned 8-bit integer, I can easily compare it to 0 or 255 to determine if the signal is stable.

Detecting a stable signal prior to a button press

Compared to some other debouncing techniques (like using a capacitor), my approach has the drawback that it won't protect against brief background inteference during an otherwise stable signal - the hope being that this has been safely avoided by those 10K resistors.

This approach does, however, have a side-effect I like: provided the signal was stable beforehand, button presses are detected immediately. My aim is to reduce input latency wherever possible, because I know some screen updates might be slow.

What I'm unsure about is whether polling every 3ms is significantly more CPU hungry than using interrupts. Using interrupts I don't see any CPU usage because it happens inside the kernel, but for all I know it might be doing the same polling I'm doing. If anyone knows how Linux handles this I'd be curious to learn more. Anyway, I think the CPU use will be negligable either way.

Next time, I'll get a Rust program to automatically launch when the Pi is booted.

Next entry: Bramley: 6. Splash Screen and Shutdown