2

I'm making touch-sensitive, light up tic-tac-toe board.

That means 9 LEDs + 9 capacitive sensors.

Each LED needs 1 pin and each sensor needs 2...meaning I'd need 27 pins to properly control each touch-and-illuminate sequence.

There aren't enough inputs on the Uno for this, so what sort of workarounds should I explore to pull this off?

  • 1
    You might use "NeoPixels" instead of LED's (or other LED strip IC's). You could then "R/G/B animate" the tic-tac-toe board, using only 1 pin :) – Paul Apr 5 '16 at 7:41
4

There are a number of common solutions to "not enough pins".

  1. Reduce the number of pins you need. This option is often discarded out of hand, but may be the best. For example, you may be able to reduce the 18 individual pins for the on/off capacitive sensor, with a different part, which may be able to give you an X,Y co-ordinate of where it is touched, with fewer pins.

  2. Multiplex the pins. This seems to be the most likely to work in this case - you will use (for example) 9 Uno pins to power the LED and capacitive sensor on each cell, one at a time. So, for example, you might attach D1 to the top left cell, D2 to the top-center, and so forth. Then you use another pin (e.g. D10) to connect to ALL the cathodes of the LEDs (via a single transistor, to protect the Uno from too much current), D11 and D12 to connect to one of the data pins of ALL of the capacitive sensors. So, you "select" cell one, by switching D1 from LOW to HIGH. Then, you put D10 to LOW, so current will from across the LEDs. Read the capacitive sensor, which is now also powered on. Although all of the censors are connected to D11 & D12, only one will be powered on, so you will only be reading that one. Once read, you probably want to delay a few milliseconds, to make sure the light shows up. Then move on to the next cell. Go round and round all cells. The LEDs will be off 8/9ths of the time, but it will still be visible. NOTE: reading more about capacitive sensors, I see that one is input, the other output. You would need a transistor for each of the "select" pins, used as a switch to connect D11 to the correct sensor.

  3. Do the same thing, but using a decade counter. Connect 2 pins from the Arduino to the decade counter - one to "count", and one to reset. First, strobe the reset - this will set the first output of the decade counter high - connected to cell 1. Then, when you want to move to the next cell, drop the "count" low, then high again. You are now on the next cell. Repeat until you get to cell 9, then send the "reset" (which guarantees you know where you are up to) to get back to cell 1.

  4. Charlieplexing (mostly suitable for output only). This is a somewhat complicated, but very efficient (pin-count wise) way of running lots of LEDs off only a few Arduino pins, by using them in combination. For example, 3 pins could run 6 LEDs - if you have pins A, B and C, run one LED from A to B, one from B to C, and one from C to A; then, run 3 more LEDs in the OPPOSITE direction (don't forget your resistors!). Depending on which LED you want lit, you might want pin A high, B low, and C high impedance (INPUT). 4 pins will get you 12 LEDs, 10 will get you 90 LEDs. If you think this might be useful, check out the Wikipedia

  5. Use chips that are designed specifically for the application you need. For example, TLC5940 chips will run up to 16 LEDs each, controlling the brightness of each LED independently. You set the brightness of each LED, then let the TLC5940 take care of the work. SIPO chips such as the 74HC595 (8 outputs) are similar, but only allow "on" or "off" for each pin. Both of these can be "daisy chained" - with the same number of Arduino pins, you can connect the output of one to the input of the next, to control more outputs.

  6. Use multiple Arduinos. Yes, this might sound expensive, but you can use an AtMega (e.g. AtMega328, which is the same chip used in the Arduino) for a fraction of the cost. You might even use an AtTiny - even cheaper (but less memory). All of these can run the same code. This is the most flexible, but is also the most work - you would need to think about which chip does what, how they interact, and how they talk to each other. This also has the advantage that, rather than a chip that runs at 16mhz, you can now have multiple chips that each run at that speed, and have more memory each. Some applications work well with one "master" chip controlling one (or more) "slaves", whereas other applications work really well as peer-to-peer, either with each node running the same code, or each doing their own thing, and passing information to other nodes. For this example, you might use one AtTiny85 for each of the 9 cells, with an Arduino to be the master. Or, use an AtTiny84, for each row of 3 cells, with one Arduino to rule them all. The AtTiny84 has 14 pins: power, ground, reset, and 11 data pins - 2 is enough for a shared data bus, such as i2c, leaving 3 pins for each of 3 cells.

  7. (not relevant for this example) For multiple on/off sensors (e.g. pushbuttons, switches), you can attach them through different values of resistors, and connect them to an ADC pin as a voltage divider. For example, connect ground via R0 to an analog pin; then connect to the same pin: pin -> resistor 1 -> pushbutton 1 -> +5v; pin -> resistor 2 -> pushbutton 2 -> +5v, and so forth. Build the circuit, and test each combination of switches/pushbuttons on/off, what the output will be. Then, in your code, figure out which combination it is closest to. Each resistor will need to be different resistance. If you only expect one switch to be on at once, it's a lot easier.

  • 1
    Could you, use a pin (or complete port) as output for 90% of the time. And use 10% of the time to sense the pin? If sensing takes a little time (and you're normally powering an LED) you might not notice the LED going off for a real small time :) – Paul Apr 4 '16 at 12:38
  • 2
    For a definitive answer, I suggest you experiment. Since the basis of capacitive touch sensors is to see how much of a capacitor is in the circuit, any component that can act like a capacitor may cause increased or decreased sensitivity – AMADANON Inc. Apr 5 '16 at 2:42
1
  1. Shift registers. A single shift register is typically 8 bits, so for nine cells you will need two parallel-in/serial-out (for the sensors) and two serial-in/parallel-out (for the leds). You can chain shift registers together to manage any number of bits, at the price of speed. Each chain of registers needs two pins - data and clock, and the parallel input needs a third pin to latch the data. 5 pins all up, for any number of inputs and outputs. You could even combine the clocks by combining 'read the sensors' and 'refresh the display' as a single operation.
  • same as #5 - I specifically mention SIPO (shift registers) as an example. – AMADANON Inc. Apr 4 '16 at 4:41
  • @PailMurrayCbr You can use Shift registers via SPI at full clock speed, if they can do it. So speed is not really a problem. – ansi_lumen Apr 4 '16 at 12:24
0

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();
}
0

Oh - capacitative sensors. I was fooling about with transistors (BD149s, I think) hooked up into darlington pairs, and I found that just touching the resistor on the base was enough to make them switch. No extra capacitor timing whatsamajig required.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.