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.
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.
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:
- Is the current value diffent to the last event emitted?
- If so, were the previous 8 reads all high or all low (i.e. stable)?
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.
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