The Apple II had an attached keyboard, which Quinti-Maze used as its input device.

By Bilby - Own work, CC BY 3.0

The Feather M4 does not have a keyboard, and while the display I’ve chosen does have a touchscreen, it’s not a good touchscreen and I’m not very motivated to invent a touch interface for Quinti-Maze.

Instead, enter the keypad.

In particular, an AdaFruit 3x4 Matrix Keypad. I have a few of these sitting around from when I thought I would use them for an escape room prop.

A matrix keypad is formed by rows and columns of conductors, connected by a momentary switch, the key.

This particular keypad has a somewhat random-seeming arrangment of the connectors at the bottom.

"raspberry_pi_PID3845_pinout" by Carter Nelson is licensed under CC BY-SA 3.0

Here’s the Rust code to scan the keypad.

let mut cols: [DynPin; 3] = [pins.a2.into(), pins.a0.into(), pins.a4.into()];
let mut rows: [DynPin; 4] = [
    pins.a1.into(),
    pins.d0.into(),
    pins.a5.into(),
    pins.a3.into(),
];

for row in rows.iter_mut() {
    row.into_pull_up_input();
}

let mut delayer = Delay::new(core.SYST, &mut clocks);

const KEYS: &[&[char]] = &[
    &['1', '2', '3'],
    &['4', '5', '6'],
    &['7', '8', '9'],
    &['*', '0', '#'],
];

loop {
    for (row_index, row) in rows.iter_mut().enumerate() {
        row.into_push_pull_output();
        row.set_low().ok();
        delayer.delay_us(50u8);
        for (col_index, col) in cols.iter_mut().enumerate() {
            let index = row_index * 3 + col_index;
            col.into_pull_up_input();
            let col_value = col.is_low().unwrap_or_else(|_| {
                rprintln!("is_low failed");
                false
            });
            if col_value {
                rprintln!(
                    "row {} col {} col_value {} value {:#?}",
                    row_index,
                    col_index,
                    col_value,
                    KEYS[row_index][col_index]
                );
            }
        }
        row.into_pull_up_input();
    }
}

But this isn’t exactly what I want, as I only want to report when a key goes down, and then not report it again until the key goes up and then down again. So let’s add an array to track state so we can report only the changes…

let last_value: &mut [&mut [bool]] = &mut [
    &mut [false, false, false],
    &mut [false, false, false],
    &mut [false, false, false],
    &mut [false, false, false],
];

… and use it…

if col_value != last_value[row_index][col_index] {
    rprintln!(
        "row {} col {} col_value {} value {:#?}",
        row_index,
        col_index,
        col_value,
        KEYS[row_index][col_index]
    );
    last_value[row_index][col_index] = col_value;
}

… which should be fine, except with a single press on the asterisk key I see:

row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'
row 3 col 0 col_value true value '*'
row 3 col 0 col_value false value '*'

What the heck? I have a theory, though, and to prove it let me hook up my beloved Saleae Logic 8.

It appears that as a keypad button comes up, it produces a rapid series of logic 1 and 0 values as the voltage slowly rises. This is referred to as bouncing, and techniques to over come it debouncing.

Luckily, there’s already a Rust crate to perform software debouncing, called debouncr.

A simple and efficient no_std input debouncer that uses integer bit shifting to debounce inputs. The basic algorithm can detect rising and falling edges and only requires 1 byte of RAM for detecting up to 8 consecutive high/low states or 2 bytes of RAM for detecting up to 16 consecutive high/low states.

While the regular algorithm will detect any change from “bouncing” to “stable high/low” as an edge, there is also a variant that will only detect changes from “stable high” to “stable low” and vice versa as an edge (see section “Stateful Debouncing”).

The algorithm is based on the Ganssle Guide to Debouncing (section “An Alternative”).

Here it is, all put together.

    let mut cols: [DynPin; 3] = [pins.a2.into(), pins.a0.into(), pins.a4.into()];
    let mut rows: [DynPin; 4] = [
        pins.a1.into(),
        pins.d0.into(),
        pins.a5.into(),
        pins.a3.into(),
    ];

    for row in rows.iter_mut() {
        row.into_pull_up_input();
    }

    let mut debouncers: [KeyDebouncer; 12] = [
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
        debounce_stateful_3(false),
    ];

    let mut delayer = Delay::new(core.SYST, &mut clocks);

    loop {
        for (row_index, row) in rows.iter_mut().enumerate() {
            row.into_push_pull_output();
            row.set_low().ok();
            delayer.delay_us(50u8);
            for (col_index, col) in cols.iter_mut().enumerate() {
                let index = row_index * 3 + col_index;
                col.into_pull_up_input();
                let col_value = col.is_low().unwrap_or_else(|_| {
                    rprintln!("is_low failed");
                    false
                });
                let edge = debouncers[index].update(col_value);
                if Some(Edge::Rising) == edge {
                    rprintln!(
                        "row {} col {} value {:#?}",
                        row_index,
                        col_index,
                        KEYS[row_index][col_index]
                    );
                }
            }
            row.into_pull_up_input();
        }
    }