As noted in Paul's comment, you could use NeoPixels instead of ordinary LEDs for the display. Each NeoPixel contains RGB LEDs and a microcontroller that accepts a serial stream of bits in, cleans up the waveform, and passes what it doesn't need on through, to the next NeoPixel. This would reduce the necessary number of pins by 8, since a string of NeoPixels uses one signal pin, independent of the number of NeoPixels in the string.
For the touch-sensitive (capacitive) buttons, rather than two wires per button, you can use one sense wire per button, and one drive wire per set of buttons, thus 10 pins to sense nine buttons, rather than the 18 pins mentioned in the question. A sketch is shown below that samples nine capacitive buttons about 1300 times per second. Here is some sample output from the sketch:
At t=2354 button 0 on at level 274 xoooooooo nr= 3121
At t=2975 button 1 on at level 283 xxooooooo nr= 3943
At t=3056 button 2 on at level 271 oxxoooooo nr= 4048
At t=3235 button 3 on at level 280 oooxooooo nr= 4283
At t=3272 button 4 on at level 278 oooxxoooo nr= 4331
At t=3418 button 5 on at level 284 ooooxxooo nr= 4521
At t=3460 button 6 on at level 275 ooooooxoo nr= 4575
At t=3507 button 7 on at level 278 oooooooxo nr= 4636
At t=3632 button 8 on at level 290 oooooooxx nr= 4799
Each entry like “level 278” shows the value of a running average of counts for a button; since the program is using 270 as a threshhold value for a button being on, it's reasonable for the numbers to be slightly above 270. (Note, if the call to secondlyReport()
were uncommented, one would see that the running averages go on up into the 300 to 500 count range when a button is pressed.) The strings of o's and x's show which buttons are on and which are off. The numbers after nr=
are the number of button readings taken so far.
The program implements smoothing (via a running average with exponential decay), debounce (via “holdoff” counts, as explained in detail in a previous answer), and hysteresis (going from off to on at a high count (270) and from on to off at a lower count (170)).
The program may need tuning to work with a given set of capacitive buttons. The tuning variables used here (such as ButtonIsOn = 270
, ButtonIsOff = 170
, usSettle=60
, usPreWait=5
, and nsamp = 40
) work ok with my nine-button setup where each capacitive button is attached to an input pin (like 4,5,...12) and to a 619K resistor (a size I have a number of). The other end of each of those resistors attaches to Uno pin 13. By changing ButtonIsOn
and ButtonIsOff
, one should be able to accomodate resistor sizes from a megohm on up to perhaps 4 or 5 megohms.
Note, to install Streaming.h
(which doesn't increase code size) unzip Streaming5.zip
from arduiniana.org in your sketchbook/libraries
directory.
/* Via serial port, tell the current time when capacitive readings on
various digital pins show a button has been "pressed" or "released".
This version reads buttons using 13 as a drive pin, and two groups
of pins as capacitive switches: group D = pins 4,5,6,7, group B =
8,9,10,11,12. Each of those pins should be connected to the drive
pin via (eg) a 1M ohm resistor. JW - April 2016
See eg refs at http://playground.arduino.cc/Code/CapacitiveSensor and
http://www.arduino.cc/en/Reference/PortManipulation and
http://garretlab.web.fc2.com/en/arduino/inside/arduino/Arduino.h/digitalPinToPort.html
*/
#include <Streaming.h> // provides nicer syntax for printing
enum { pinsMax = 9 }; // upper size on pins count per sample;
enum { nPortReads = 40 }; // # bit samples saved per reading;
enum { bundleSize = 16 }; // 2^j samples carried in running average
enum { drivePin = 13 }; // # of pin to drive capacitors
enum { ButtonIsOn = 270 }; // button-on level, with hysteresis
enum { ButtonIsOff= 170 }; // button-off level " "
enum { usSettle = 60 }; // Hold sense pins low this long.
enum { usPreWait = 5 }; // Ignore probably crosstalk this long.
enum { nKeys=9 }; // # of keys in PinSet. Cannot be > pinsMax
#define PinSet "4,5,6,7,8,9,10,11,12"
unsigned int AvCount[nKeys]={0};
unsigned long int nSamples; // # of samples taken
//-----------------------------------------------------------
// Use delta=0 for startup, vs delta=1 for exponential averaging
void makeSample(int delta) { // Take a sample, add it to AvCount
byte counts[nKeys], j, nc;
unsigned int bundle;
nc = countPins7(drivePin, PinSet, counts);
for (j=0; j<nc; ++j) {
bundle = AvCount[j] * (bundleSize-delta);
AvCount[j] = bundle/bundleSize + counts[j];
}
++nSamples;
}
//-----------------------------------------------------------
void setup() {
Serial.begin(115200); // initialize serial port
nSamples = 0;
// Initialize AvCount[] with a bundle of samplecounts
for (byte j=0; j<bundleSize; ++j)
makeSample(0); // Add sample directly to total
}
//-----------------------------------------------------------
// portSampler7(): countPins7() calls this routine, which holds
// drivePin and involved pins on current port low for usSettle
// us, then makes them inputs and turns on drivePin. It waits
// usPreWait us (to weed out crosstalk), then reads the port
// nPortReads times as the pins recharge.
// The set of nPortReads port readings is returned in portReadings.
//
// Inputs: drivePin, portCode, pinsMask. Constant nPortReads.
// Output: array of nPortReads portReadings.
//
// This version takes 8 cycles per reading. Inputs switch
// on after about one RC time constant. On a 16MHz Arduino an
// m-count result (that is, a zero bit in m of the nPortReads reads)
// suggests RC is about m/2 us + usPreWait us.
void portSampler7(byte drivePin, byte portCode,
byte pinsMask, byte *portReadings) {
volatile byte *pin, *port, *ddr;
// pin, port, and ddr are pointers to registers with volatile
// contents: input port, output port, direction register.
pinMode(drivePin, OUTPUT);
digitalWrite(drivePin,LOW);
pin = portInputRegister (portCode); // eg points to PIND
port = portOutputRegister(portCode); // eg points to PORTD
ddr = portModeRegister (portCode); // eg points to DDRD
*port &= ~pinsMask; // Turn off our pins
*ddr |= pinsMask; // Make our pins be outputs, briefly
delayMicroseconds(usSettle); // Delay to let pins settle low
*ddr &= ~pinsMask; // Make our pins be high-impedance inputs
digitalWrite(drivePin,HIGH); // Turn on driver pin
delayMicroseconds(usPreWait);
for (byte i=0; i<nPortReads; ++i) // Take and store nPortReads readings
portReadings[i] = *pin;
}
//-----------------------------------------------------------
/* Decode a pins-list into one list per involved port.
Use portSampler7() to get rise-time counts for each port.
If pins are spread across k ports, countPins7() will make
k calls to portSampler7(). Results are in counts array.
Note, make pinsMax larger if pinsMax < #(pins in list).
pinsList can look like "2,3,4,5,6" or "2,4,5,12,9,23,17,7"
etc. A list may contain digits and commas only. Pins may be
listed in any desired order. Elements of counts[] will be in
list-order. Eg, in the "2,3,4,5,6" example, counts[0] is the
count for pin 2, counts[1] is the count for pin 3, etc. A
larger count implies longer rise time and larger capacitance.
*/
byte countPins7(byte drivePin, char *pinsList, byte *counts) {
byte pnum, pomask, pocode, potem, i, j, mul;
byte sampl[nPortReads], pim[pinsMax], pat[pinsMax], bout=0, nat=0, pcon;
char *c, cc;
int usedPorts=0; // To track which ports are done
for (c=pinsList, counts[0]=pcon=0; *c; ++c) // Zero the counts array
if (*c < '0' || *c > '9') counts[++pcon] = 0;
// In each pass, group bit numbers by port and sample the port
while (nat<pcon) { // Each loop pass sets one bit in usedPorts
pnum = pocode = pomask = bout = pcon = 0;
c = pinsList;
while ((cc=*c++)) { // Loop until c is at end of string
if ('0' <= cc && '9' >= cc) {
pnum = 10*pnum + cc - '0';
if ( *c < '0' || *c > '9') { // See if pin # is complete
potem = digitalPinToPort(pnum); // Get port #, in range 1 to 12
if (!(usedPorts & (1<<potem))) { // Is potem's port already done?
if (!pocode) // If pocode not set, set it
pocode = digitalPinToPort(pnum);
if (pocode == potem) { // Process pin if it's in current port
pomask |= (pim[bout] = digitalPinToBitMask(pnum));
pat[bout++] = pcon; // Save location of pin's bit in results
}
}
pnum = 0; // Clear # accumulator
++pcon; // Count pin numbers
}
}
}
// Now pomask is a mask for all bits to be read from port pocode
// and we are ready to sample current port.
// Get nPortReads port readings into sampl array.
portSampler7(drivePin, pocode, pomask, sampl);
usedPorts |= 1<<pocode; // Mark the port finished
// Count up # of zeroes (ie rise times) in sampled bits
for (j=0; j<bout; ++j) {
int count;
byte m=pim[j];
for (count=i=0; i<nPortReads; ++i)
count += sampl[i] & m;
counts[pat[j]] += nPortReads - count/m; // Return the number of Zeroes
}
nat += bout; // Count number of pins done so far
}
return pcon; // Return # of pins processed
}
//-----------------------------------------------------------
// Measure rise times at designated pins and report
// current running averages of rise times, once per second
void secondlyReport() {
unsigned int seconds=0, now;
while (1) {
now = millis()/1000;
makeSample(1); // Add a sample to running average
if (now > seconds) { // See if next second started
Serial << "Counts: ";
for (byte j=0; j<nKeys; ++j) // Show output
//for (byte j=1; j<nKeys; j+=2) // Show output
Serial << AvCount[j] << " ";
Serial << " t=" << now << " sec." << endl;
}
seconds = now;
}
}
//-----------------------------------------------------------
// Frequently sample rise times on designated pins, to
// find and report capacitive key press times & levels.
void keypressReport() {
// Old-state and holdoff-count vars
byte ostate[nKeys] = {0}, holdo[nKeys] = {0}, j, k;
while (1) {
makeSample(1); // Add a sample to running average
for (k=0; k<nKeys; ++k) {
if (holdo[k]) { // Ignore an on button during holdoff
--holdo[k];
} else {
if (AvCount[k] > ButtonIsOn ||
(AvCount[k] > ButtonIsOff && ostate[k])) {
if (!ostate[k]) { // Were we off?
ostate[k] = 1; // New button press
holdo[k] = 40; // Start a holdoff period
Serial << "At t=" << millis() << " button " << k;
Serial << " on at level " << AvCount[k] << " ";
for (j=0; j<nKeys; ++j)
Serial << (ostate[j]? 'x' : 'o');
Serial << " nr= " << nSamples << endl;
}
ostate[k] = 1; // We detected button is on
} else {
ostate[k] = 0; // We detected button is off
}
}
}
}
}
//-----------------------------------------------------------
void loop() {
if (0) secondlyReport();
keypressReport();
}