AnalogKeyMatrix allows you to read multiple keys (momentary switches) from a key matrix outputing a single voltage to an Arduino analog input pin easily, to be interpreted as "press / release", "double-click", "long-press" patterns.
Although designed for number pad key matrixes, it works with any source of voltage input that is divided into any number of parts (number of keys or buttons) plus an OFF state which could be VCC or GND or close to them. This flexibility allows it to be used with devices such as keypads, keyboards and even musical CV keyboards.
The constructor is somewhat complex and should be called during setup whereas the methods called afterwards are quite light-weight for fast performance.
This is my current active project and, although it appears to function smoothly, it's early enough that I'm sure there are some bugs that will be ironed out in the next few weeks. Go ahead and download it but check for updates soon thereafter!
AnalogKeyMatrix my_keymatrix (
uint8_t pin,
uint8_t numOfKeys = AKM_defaultNumOfKeys,
uint16_t keyVals[AKM_defaultNumOfKeys] = AKM_defaultKeyVals,
short releaseState = -1,
double releaseTol = 0.1
);
1st Constructor Argument: uint8_t pin
Is the pin number your object will use to call analogRead(). It is the only
required argument.
2nd Constructor Argument: uint8_t numOfKeys
This is the size of the 3rd argument array. It's default is:
uint8_t AKM_defaultNumOfKeys = 12;
3nd Constructor Argument: uint16_t keyVals[]
The third argument Is an array of approximate analogRead() return values for each key in your input and the second argument is the size of that array. The values need not be
in order so that it might be indexed in a way that's meaningful to you. For example,
with a number pad you would want the zero (0) key to be index 0 even if other keys have lower voltage output. For non-numeric keys with names you may define an enum or similar for readability. The array does, however, have to constitute a valid C array, indexed 0 to size-1).
Here is the defaults set in the library, which can be overwritten. It is a numeric keypad:
uint16_t AKM_defaultKeyVals[12] = {
510, // 0 key
1023, 930, 850, // 1, 2, 3
790, 730, 680, // 4, 5, 6
640, 600, 570, // 7, 8, 9
540, 490 // bottom row left and right of 0 key
};
4th Constructor Argument: short releaseState
The fourth argument specifies whether GND or VCC is received when keys all are
released. Set it to HIGH for VCC, LOW for GND or -1 (the default value)
to have it be determined automatically from the values of keyVals[]. If a 0
is found, release is HIGH and if 1023 is found, it's LOW.
5th Constructor Argument: double releaseTol
The key values don't need to be exact since a min and max will be calculated
during the constructor as halfway between each value you specify in the keyVals[] third argument. That halfway point might not be halfway for the min of the lowest
value in the case of release being GND... or max of the top value when release is
HIGH. It will be if the third argument, releaseTol is set to 0.5. With a
very low release tolerance of, say 0.1, It might mean that the min for the
lowest key is 1 and release is only 0 from analogRead(), for example.
The default is 0.1 which leave a little room for the release state and more
room for the lowest or highest key than any other key.
After you construct your object...
AnalogKeyMatrix my_keymatrix(A0);
You can call .read()
my_keymatrix.read();
Data Member: my_keymatrix.log[] array of AKM_State struct pointers
After calling .read(), will have three data members of concern to you that will be updated. my_keymatrix.log is an array of pointers to the current and previous
states read. Each element is a pointer to a struct of the type AKM_State.
The most recent is always the zeroth element, the previous is the next element
and least recent is the last element.
Data Members: my_keymatrix.now and my_keymatrix.now AKM_State struct pointers
Typically you would be concerned with the most recent and previous reading,
which have their own data member .now and .prev, which are just pointers to
the same data. This shows the fields in all of these AKM_State structs:
my_keymatrix.now->val; // int return from analogRead(), always updated
my_keymatrix.now->event; // int, see AKM_* event statuses
my_keymatrix.now->key; // int key number or -1 if idle
my_keymatrix.now->ms; // unsigned long from millis()
Reading directly from return of .read() Method
.read() returns a pointer to the AKM_State now struct, so you could save the
output from .read() to a variable (AKM_State * now = my_keymatrix.read()) or,
if you only need to look at one field, you can grab it from the call to .read():
if (my_keymatrix.read()->event == ...
Which might be quicker. Also note that you can read the same data with the .log
data member:
// .log[0] same as .now ...
my_keymatrix.log[0]->event;
my_keymatrix.log[0]->event; // etc...
// .log[1] same as .prev ...
my_keymatrix.log[1]->val;
my_keymatrix.log[1]->event; // etc...
If .read() found a key is currently pressed down, my_keymatrix.now->event
will be 2 (or possibly greater of class is extended). If not, it is less
than 2. You can compare it's value to these macros defined in this class
library:
#define AKM_IDLE 0
#define AKM_RELEASE 1
#define AKM_PRESS 2
The AKM_RELEASE status only happens once after a press, then the next
status and those after will be AKM_IDLE if no subsequent key press is
found.
__ AKM_PRESS status__
my_keymatrix.now->val; // => int from analogRead() IN range for ->key
my_keymatrix.now->event; // => AKM_PRESS (int8_t)
my_keymatrix.now->key; // => short int key number (never -1)
my_keymatrix.now->ms; // => millis() at FIRST in series of AKM_PRESS
When the object first finds any key is pressed, a new event slot in .log[]
is written to as seen above with ->now pointing to it. ->ms is updated
but if the next reading also shows a key pressed (even a different key),
->ms remains as it was. In fact, ->now is simply written over in this
case, with updated ->val and ->key. If you want to see how long it's
been pressed, compare ->ms (the initial time AKM_PRESS occurred) with
your own call to .millis().
__ AKM_RELEASE status__
my_keymatrix.now->val; // => analogRead() int OUT of range of any key
my_keymatrix.now->event; // => AKM_RELEASE (int8_t)
my_keymatrix.now->key; // => short int key number of previous event (.prev->key)
my_keymatrix.now->ms; // => millis() at FIRST in series of AKM_RELEASE
When the key is finally released, the log advances to a new AKM_State slot
position and makes the event AKM_RELEASE. A new timestamp and a new value are
recorded but ->key will be the last key pressed so you can see what
key was just released. You can compare the two timestamps to see how long it
was pressed before being released.
Although the AnalogKeyMatrix doesn't have a direct way to determine if two keys are
pressed at the same time, you can still determine this but going back in the
.log.
As said previously, a second reading of the same key being pressed
does not make a new event BUT two consecutive readings of DIFFERENT keys
does. The second key press is logged just like the first, with a new slot
and all fields updated. When a AKM_RELEASE is found, you can check the past two
events to see if they are both AKM_PRESS. The time between each can be determined
with the three ->ms.
You can even check for more than one modifier by by going back further in the log (see 'Other Global Setup Constants' section for limitations).
If you want to see if a key is pressed for over a certain threshold time,
look for a AKM_PRESS for a particular key and keep comparing it's ->ms,
which not updated and therefore just the time the key was first pressed,
to the current return from .millis().
After AKM_RELEASE is logged (if the next reading doesn't show another key
pressed) the .now->event will be AKM_IDLE. All fields including ->val and
->ms are updated. ->key will be -1 indicating no key (and not a valid
array index). So if you are checking for a particular key, you can skip the
step of looking for ->event and just look at ->key. This is helpful if you
are looking for Long Press since, in that case, you are probably not waiting
until you see it released.
Also if you are detecting a timeout where the user has not pressed anything for
a period of time, know that AKM_IDLE does not get updated with a new ->ms
timestamp. In fact, nothing in .log or related members are changed, the same
.now pointer is repeatedly returned from .read(). Therefore, your method of
timing the inactivity should be comparing this ->ms as a start time to whatever
return you get from .millis().
When the constructor is called the entire .log array is filled with some readings
and default values. All elements will show these values:
->val // return from a single call to analogRead()
->event // usually AKM_IDLE but could be AKM_PRESS
->key // usually -1 (if idle) but could be a key number
->ms // return from a single call to millis()
It is possible, if the constructor is called very early, that Arduino will
return a junk value from some functions so you may need to be wary in assuming
a key is held down. You can, of course, call .read() several time, which would
be absolutely necessary if you want to check for multiple keys being held.
As mentioned, AKM_defaultNumOfKeys and AKM_defaultKeyVals[AKM_defaultNumOfKeys] are variable that can be written to before calling the constructor. There are also two other values which are macros in the AnalogKeyMatrix library that you may want to change. Here they are as defined in the library:
#ifndef AKM_MaxNumOfKeys
#define AKM_MaxNumOfKeys 48 // maximum size of AKM_defaultKeyVals[] supported
#endif
#ifndef AKM_LogSize
#define AKM_LogSize 6 // size of .log[] array
#endif
These are purposely quite high. AKM_LogSize of 6 allows for 3 keys to be pressed,
then released. And AKM_MaxNumOfKeys 48 allows for a four octave CV keyboard or an
abbreviated alpha-numeric keyboard. But not using all of that takes a bit of
memory and, in the case of AKM_LogSize, a bit of extra clock CPU clock cycles
during .read().