Now that I deciphered where everything goes and how everything works, it was time to make sure I build a foundation to make the I/O a little more usable, instead of fetching raw data. Fist stop: make the input buttons accessible.
Working with keypresses is actually not as simple as I intuitively thought. If you have one button attached to one input pin and do a simple digitalRead(pin);
, then you know your button is pressed. But if you want to do an action only once on every key press? You could have a little variable that keeps track of whether the key was not pressed last time and is now — basically working on the leading edge. But then you still have the problem of key bounce to take care of.
And what if you want to detect long presses (e.g. to go into a settings menu) or held presses (press once to increase a counter, keep pressing to keep increasing it)?
But first, the basics, how to detect a simple keypress with the scan matrix used in this board?
I found an old CD player and am slowly turning it into a kitchen clock/timer, and teaching myself hacking along the way. Instances of this series are:
Simple key detection
In my previous post I displayed a quite raw result of which keys were pressed, once every second.
First an explanation of my program structure: My program will have a main loop that runs multiple times per second (say 20-50, I haven’t fully decided on that yet). The interval is long enough to not have to care about button bounce (I hope), but short enough that button presses feel “instantaneous”.
The following code fills an array keyState[n]
where n is the key number with the duration (in number of runs of the function) of the keypresses. The duration is capped at 250.
/**
Scans the keyboard matrix and puts the result in keyState[];
each value of keyState will have an int telling how many runs of scanKeys this key has been pressed (max 250);
*/
void scanKeys() {
/*
The key matrix scan is inverted from traditional, it only scans on COM lines with value "0".
A scanned "1" denotes no key press, a "0" is a keypress
*/
shiftOut(data_pin, clock_pin, LSBFIRST, 127); // prime shift register with 11111110 to "erase" any data that might be left in it.
for (int i = 7; i >= 0; i--) { // loop over 8 shift register lines // loop backwards to have values 0-13 only - we still shift forward.
for (int j = 0; j < 2; j++) { // loop over 2 sense lines
int n = 8 * j + i; // calculate key number
int line = (j) ? key_sense_pin_1 : key_sense_pin_2; // select the right matrix input pin
bool keyPressed = !digitalRead(line);
if (keyPressed) {
keyState[n]++;
if (keyState[n] > 250) { // advance the keyState while testing [ugly code]
keyState[n] = 250; // don't make the code roll over the 255 boundary
}
} else {
keyState[n] = 0;
}
}
shiftBit(); // advance to the next key matrix row line
}
}
The outer loop (line 11) is run backwards. With the layout the keys are connected to the matrix, I had keys 3-15. Running the loop backwards means that my keys are numbered 0-12, so I can make the keyState
array 13 elements long.
I will spare you the many errors I made while coming up with this, but there are two that took me way too long to find out:
- Set
keyState[n]=0
one level too high. All my keypresses came out zero. Even though many includesSerial.print()
‘s showed that I was registering the keys well. - Call
shiftBit()
one level too deep, within the “j” loop, which meant only half of the keys could be detected.
I attribute both these errors partially to the Arduino IDE by default having an indentation of 2 spaces. I prefer 4 spaces. Fortunately I was not the first one to hate this.
[The Wandering Engineer] explains that you can set the editor tab size in the preferences.txt
(location is found at the bottom of the preferences menu, but you have to open a text editor).
Find the line with editor.tabs.size=2
and change 2 to whatever you like.
Insanely enough you have to change the tab size for the Auto Formatter (Ctrl+T – I didn’t know this existed before I looked at where to find the preferences) in {Arduino IDE installation folder}/lib/formatter.conf
and change the value of indent=spaces=2
to the same value you changed the editor tab size to. Thx [Pert] for this last tip!
Back to the key scanning. The code above shows me what key is pressed and how long. If I run
void loop() {
scanKeys();
for (int i = 0; i < numberOfKeys; i++) {
if (keyState[i] > 10) {
Serial.println("Key " + (String)i + " pressed long");
} else if (keyState[i] > 1) {
Serial.println("Key " + (String)i + " pressed");
}
}
delay(30);
}
I get
So, I see what key is pressed and by counting how long, I see that a key is pressed long. But how to differentiate between a short and long press?
Advanced key detection
One solution to differentiating between a short and long button press is to act on a short button press only at release time, a solution I found in [Xn1Ch1]’s instructable.
I decided to implement a little state machine with 6 states:
- Button not pressed
- Button pressed
- Button released after state 1 (do short press actions)
- Button long pressed (do long press actions)
- Button long press held
- Button long press held longer (do long press held actions)
In this state machine, whenever a button is pressed, a counter keyStateCounter[]
is started that increments with each invocation of scanKeys()
where the key is still pressed.
States 1 and 4 test for the value of the keyStateCounter
and if it’s not longPressCount
(state 1) or LongPressRepeatCount
(state 4), the state will not change (but the keyStateCounter
will increment).
In state 1, only when keyStateCounter
equals longPressCount
states 3 is reached and M.M. longPressCounter
to transition from 4 to 5.
When a key is released in state 1, the state machine goes to state 2, so the main program can test for that state (signifying a short press). States 0, 2, 3, 4 and 5 transition to state 0 and reset the counter to 0 when no key is pressed.
/**
Scans the keyboard matrix and puts the result in keyState[];
KeyStates can be
0: key is not pressed
1: key is pressed (do nothing)
2: key has been released after a short press
3: key has been long pressed
4: key has been long pressed and held (do nothing)
5: key has been long pressed and held repeatedly
The states 2, 3 and 5 are seen only once and should be tested upon to detect the keypresses
*/
void scanKeys() {
static uint8_t keyStateCounter[numberOfKeys];
static const int longPressCount = longPressDuration / mainLoopDuration; // at what counter value does a press count as a long press?
static const int longPressRepeatCount = (longPressDuration+longPressRepeatDuration) / mainLoopDuration; // at what counter value does a press count as a repeated long press?
/*
The key matrix scan is inverted from traditional, it only scans on COM lines with value "0".
A scanned "1" denotes no key press, a "0" is a keypress
*/
shiftOut(data_pin, clock_pin, LSBFIRST, 127); // prime shift register with 11111110 to "erase" any data that might be left in it.
for (int i = 7; i >= 0; i--) { // loop over 8 shift register lines // loop backwards to have values 0-13 only - we still shift forward.
for (int j = 0; j < 2; j++) { // loop over 2 sense lines
int n = 8 * j + i; // calculate key number
int line = (j) ? key_sense_pin_1 : key_sense_pin_2; // select the right matrix input pin
bool keyPressed = !digitalRead(line);
if (n<numberOfKeys) { // if n>=numberOfKeys, we have garbage
if (keyPressed) {
switch (keyState[n]) {
case 0: // button was not pressed
keyState[n]=1;
keyStateCounter[n]=1;
break;
case 1: // button short pressed
if (keyStateCounter[n]==longPressCount) {
keyState[n]=3;
}
keyStateCounter[n]++;
break;
case 2: // short button press released
// this state should not exist, unless a very fast key release and repress happens. For safety, go back to zero
keyState[n]=0;
keyStateCounter[n]=0;
break;
case 3: // button long pressed
keyState[n]=4;
keyStateCounter[n]++;
break;
case 4: // button long pressed and held
if (keyStateCounter[n]==longPressRepeatCount) {
keyState[n]=5;
}
keyStateCounter[n]++;
break;
case 5: // button long pressed and held long enough to count as a repeat press
keyState[n]=4;
keyStateCounter[n]=longPressCounter;
break;
}
} else { // key was not pressed
if (keyState[n] == 1) {
keyState[n]=2;
keyStateCounter[n]++;
} else {
keyState[n]=0;
keyStateCounter[n]=0;
}
}
}
}
shiftBit(); // advance to the next key matrix row line
}
}
This implements the state machine. Lines 21-26 do the actual looping trough the shift register and key scanning.
Lines 28-68 implement the state machine in two sections: Line 29-59 decide what to do when button n was pressed and line 60-68 when button n was not pressed. In the latter, I preferred an if
statement, as all states except state 1 lead to state 0 when no button is pressed.
On line 27 there is an if (n<numberOfKeys)
. When I didn’t have this I got erratic results: Key presses when there were none and when I made a printout of the states I got output like this
B7s0c0 B15s23c0 B6s0c0 B14s109c0 B5s0c0 B13s120c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
B7s0c0 B15s23c0 B6s0c0 B14s146c0 B5s0c0 B13s103c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
B7s0c0 B15s23c0 B6s0c0 B14s183c0 B5s0c0 B13s86c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
B7s0c0 B15s23c0 B6s0c0 B14s220c0 B5s0c0 B13s69c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
B7s0c0 B15s24c0 B6s0c0 B14s2c1 B5s0c0 B13s52c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s2c1 B9s0c0 B0s0c0 B8s0c0
Key 1 state 2 = Short press
B7s0c0 B15s24c0 B6s0c0 B14s40c2 B5s0c0 B13s38c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
B7s0c0 B15s24c0 B6s0c0 B14s77c0 B5s0c0 B13s21c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
B7s0c0 B15s24c0 B6s0c0 B14s114c0 B5s0c0 B13s0c0 B4s0c0 B12s0c0 B3s0c0 B11s0c0 B2s0c0 B10s0c0 B1s0c0 B9s0c0 B0s0c0 B8s0c0
Each line here contains a printout B<button number>s<state>c<counter> for each of the buttons.
Occasionally I would get a line that showed I had a short press (state 2), which was not possible as the previous state was 0. Also, some buttons had incredibly high states, which should not be possible. I was afraid I had crosstalk somewhere, something that is really hard to detect without an oscilloscope (which I don’t have). But when I saw my states were erratic and higher than 5 in buttons 13, 14 and 15, I suddenly had a brain wave: keyState[13]
(and 14,15) were set, even though the array is only 13 elements long. I had created my first buffer overflow!
If this overwrites adjacent data or executable code, this may result in erratic program behavior […]
From the Wikipedia page on Buffer overflow
And this erratic behavior was exactly what I got! So this is why the check for n<numberOfKeys
and if n >= numberOfKeys, the whole state machine section is skipped.
The results
A tip on code clarity
I made the following construct:
if (keyState[n]++ > 250) { // advance the keyState while testing [ugly code]
This tests whether keyState[n] is over 250, and at the same time (whether keyState[n] is or is not over 250) increase keyState[n] by one. This compiles to 2394 bytes.
A more readable alternative is
keyState[n]++; // advance the keystate
if (keyState[n] > 250) {
This increases the readability, but adds one line of code. But the compiled size is still 2394 bytes. Which means there is no good reason to use the first construction.
Something I also love:
line = (j) ? key_sense_pin_1 : key_sense_pin_2;
The more readable alternative is
if (j) {
line = key_sense_pin_1;
} else {
line = key_sense_pin_2;
}
This longer alternative adds two bytes to compiled size. But for very simple things, I prefer the short version, in my view it actually increases code readability. But only if it is a really simple calculation.