1

Seeing as this is my first post I would like to thank everyone ahead who take their time to delve into my whimsical code, whoever you may be, be merciful ;)

For one of my uni projects, I would like to make a physical interface that can be operated via touch "keys" that rely on the human capacitance effect. I have relentlessly researched it (as much as feasible in my time as a student for this project) and even created a working prototype with 4 unique keys.

As you will see in my code below, I am using the capacitiveSensor.h library, which I could unfortunately not find much documentation about other than a few videos and tutorials that build upon it.

I tried deciphering the workings of it by looking at its code to see if I can manipulate it for my own biddings but quickly realized it is beyond my skill to understand.

The problem I am facing right now is that while the touch keys work, they trigger each other because of the capacitance build up when one of the keys is being pressed or when the hand is close to other keys while pressing a key.

Essentially, I am looking for an algorithm that will reduce processing time & effort for the Arduino as the one I have implemented below slows it down way too much to be of any use for my project.

Sorry for the long post, here is my current code:

#include < CapacitiveSensor.h > 
#include < pitches.h >



CapacitiveSensor cs_4_2 = CapacitiveSensor(4, 2); // 10M resistor between pins 4 & 2, pin 2 is sensor pin, add a wire and or foil if desired
CapacitiveSensor cs_4_3 = CapacitiveSensor(4, 3); // 10M resistor between pins 4 & 6, pin 6 is sensor pin, add a wire and or foil
CapacitiveSensor cs_4_5 = CapacitiveSensor(4, 5); // 10M resistor between pins 4 & 8, pin 8 is sensor pin, add a wire and or foil
CapacitiveSensor cs_4_6 = CapacitiveSensor(4, 6);




# define COMMON_PIN 4 // send pin
# define CAP_THRESHOLD 450 // overall threshold
# define NUM_OF_KEYS 4 // Number of keys that are on the keyboard
# define noteDur 50 // duration of note
# define BUZZER_PIN 12 // The output pin for the piezo buzzer
# define noMulti 0.9 // incremental factor per key
# define overkill 5 // register no touch after this cap factor is exceeded

// This macro creates a capacitance "key" sensor object for each key on the piano keyboard:
# define CS(Y) CapacitiveSensor(COMMON_PIN, Y)

CapacitiveSensor keys[] = {
CS(2), CS(3), CS(5), CS(6)
};


void setup() {

Serial.begin(57600);

// Turn off autocalibrate on all channels:
for (int i = 0; i < NUM_OF_KEYS; ++i) {
keys[i].set_CS_AutocaL_Millis(0xFFFFFFFF);
}


}


void loop() {


// needs to be repeated here to work in THIS scope
long sensor01 = cs_4_2.capacitiveSensor(30); // pin 2 is cap sensor, pin 4 is "common send"
long sensor02 = cs_4_3.capacitiveSensor(30);
long sensor03 = cs_4_5.capacitiveSensor(30);
long sensor04 = cs_4_6.capacitiveSensor(30);





Serial.println("");
Serial.print("key1");
Serial.print("\t");
Serial.print("key2");
Serial.print("\t");
Serial.print("key3");
Serial.print("\t");
Serial.print("key4");
Serial.println("");
Serial.print(keys[0].capacitiveSensor(CAP_THRESHOLD));
Serial.print("\t");
Serial.print(keys[1].capacitiveSensor(CAP_THRESHOLD));
Serial.print("\t");
Serial.print(keys[2].capacitiveSensor(CAP_THRESHOLD));
Serial.print("\t");
Serial.print(keys[3].capacitiveSensor(CAP_THRESHOLD));






// algo for key 1
if (keys[0].capacitiveSensor(CAP_THRESHOLD) > CAP_THRESHOLD &&
keys[1].capacitiveSensor(CAP_THRESHOLD) < keys[0].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[2].capacitiveSensor(CAP_THRESHOLD) < keys[0].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[3].capacitiveSensor(CAP_THRESHOLD) < keys[0].capacitiveSensor(CAP_THRESHOLD) * noMulti) {
Serial.print("key pressed on sense1");
Serial.println("");
tone(BUZZER_PIN, NOTE_A7, noteDur);

}

// algo for key 2
else if (keys[1].capacitiveSensor(CAP_THRESHOLD) > CAP_THRESHOLD &&
keys[0].capacitiveSensor(CAP_THRESHOLD) < keys[1].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[2].capacitiveSensor(CAP_THRESHOLD) < keys[1].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[3].capacitiveSensor(CAP_THRESHOLD) < keys[1].capacitiveSensor(CAP_THRESHOLD) * noMulti) {
Serial.print("key pressed on sense2");
Serial.println("");
tone(BUZZER_PIN, NOTE_B7, noteDur);

}
// algo for key 3
else if (keys[2].capacitiveSensor(CAP_THRESHOLD) > CAP_THRESHOLD &&
keys[1].capacitiveSensor(CAP_THRESHOLD) < keys[2].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[0].capacitiveSensor(CAP_THRESHOLD) < keys[2].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[3].capacitiveSensor(CAP_THRESHOLD) < keys[2].capacitiveSensor(CAP_THRESHOLD) * noMulti) {
Serial.print("key pressed on sense3");
Serial.println("");
tone(BUZZER_PIN, NOTE_C7, noteDur);

}
// algo for key 4
else if (keys[3].capacitiveSensor(CAP_THRESHOLD) > CAP_THRESHOLD &&
keys[1].capacitiveSensor(CAP_THRESHOLD) < keys[3].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[2].capacitiveSensor(CAP_THRESHOLD) < keys[3].capacitiveSensor(CAP_THRESHOLD) * noMulti &&
keys[0].capacitiveSensor(CAP_THRESHOLD) < keys[3].capacitiveSensor(CAP_THRESHOLD) * noMulti) {
Serial.print("key pressed on sense4");
Serial.println("");
tone(BUZZER_PIN, NOTE_D7, noteDur);

}


}
  • Guys, I realized how to "bypass" the lag caused by the capacitance build up once a key is triggered - namely by increasing the size of the resistors (from 1k to 1mOhm) between the "keys", e.g. the aluminum foil in this case, and the Arduino input pins. What this does is reducing the maximum capacitance that can be reached on each key and so reduces the overall period needed to register the maximum capacitance, which I assume is done by some magic internally in CapacitiveSensor.h. As a result, pressing a key does not lag the Arduino nearly as much as was the case when using 1k resistors. – Ginzburg Dec 26 '15 at 1:04
1

You might experiment with the code shown at the end of this answer. It is fairly responsive, in the sense that readings are taken reasonably quickly (several per millisecond), which should leave a good deal of time available for other processing.

In this code, all of the pins that are on the same port are sampled at the same time. For example, pins 2,3,5,6 on an Uno are all on port D so their four pads can be tested all at the same time. But if the pins list includes pins from k different ports, sampling will take up to k times as long as when sampling from a single port. For example, on a Mega2560, pins 2, 3, 5 are on port E and pin 6 is on port H, so this code's execution time when processing buttons on pins 2,3,5,6 is about twice as long on a Mega as on an Uno.

[Edit: For Uno port-to-pin assignments, see an Arduino Uno Pinout Diagram. On such diagrams, labels like ADC0 PCINT8 14 A0 23 PC0 show the Arduino digital pin reference number (eg, 14), analog pin label (eg, A0), its various functional designations (ADC0, PCINT8, and PC0, meaning analog input pin 0, pin-change-interrupt pin 8, and bit 0 of port C), and the physical pin number (23). For hardware level information about Uno ports, see Atmel's document #8271. Use Google images similarly to find other-model diagrams, eg Mega 2560 pinouts.]

You may need to tune some of the constants to fit your own circuitry or switch-function requirements. I'm not able to tell from the question what resistor sizes you are using or what your circuit is. For preliminary tuning, enable the secondly() function and disable testChange() functions. If your circuit is similar to that shown below but uses higher resistances, you may need to increase nsamp, IsHighCount, and preSamp.

Here is the general idea of circuitry I used: enter image description here

In the above, "Drive" refers to pin 4. "Sense 1" ... "Sense j" (with j=4) refer to pins 2, 3, 5, 6, as in your example code. The capacitors illustrated are tape-covered aluminum-foil pads, eg 22 x 44 mm and the resistors are 440 KΩ each (two 220 KΩ's in series). Those resistor values produce an RC time constant of a few dozen microseconds when a button isn't touched, and a couple hundred microseconds when a button is touched.

Here is some example output:

2 on  199 7587
2 off 179 7702
1 on  196 10798
1 off 154 10802
1 on  253 10900
1 off 180 10924
2 on  219 18097
2 off 166 18134
3 on  214 21105
3 off 155 21385
3 on  227 24312
3 off 72 24535

In that output, the first column is switch number. The last column is the current reading of millis() at time of printing. The third column is total counts accrued from 12 sets of contact readings. The simple-minded on/off algorithm used in the testChangeA() function is that a contact is on when its 12-sample count exceeds 180. One could also add calibrations, adaptive threshholds, debouncing, etc to the code.

Here is the code (which was run on an Uno, but I tested most of it on a Mega also, and it should run ok on other Arduinos as well since the direct port accesses are computed via functions like portInputRegister, portModeRegister, etc in countPins7() setup before it calls portSampler7() to run the test.

/* Via serial port, tell the current time when capacitive readings on
 various digital pins show a button has been "pressed" or "released".
 JW - 25 Dec 2015

 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
// Set upper size on pins count per sample; #samples saved per reading;
//  #samples tossed; threshold for Button-is-on; #samples per set; etc
enum { nbits=8, nsamp=40, preSamp=1, SettleDown=90 };
enum { IsHighCount=15, SampleSets=12, IsHighTotal=IsHighCount*SampleSets };
void setup() {
  Serial.begin(115200);     // initialize serial port
}
//-----------------------------------------------------------
// portSampler7(): Sample a port nsamp times and return the results.
// Inputs:  drivePin, portCode, pinsMask, preWait.  Constant nsamp.
// Output:  array of nsamp samples. 
// This version takes 8 cycles per sample, and as inputs switch 
// on after about one RC time constant, on a 16MHz Arduino an
// m-count result (that is, a bit clear in m of the nsamp readings)
// suggests RC is about m/2 + preWait/2 us.
void portSampler7(uint8_t drivePin, uint8_t portCode, uint8_t pinsMask,
          uint8_t preWait, uint8_t *samples) {
  volatile uint8_t *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
  for (uint8_t i=0; i<SettleDown; ++i) *pin;  // Delay to let pins settle low
  *ddr &= ~pinsMask;         // Make our pins be high-impedance inputs
  digitalWrite(drivePin,HIGH); // Turn on driver pin
  for (uint8_t i=0; i<preWait; ++i) *pin;  // Baseline Delay
  for (uint8_t i=0; i<nsamp; ++i) // Take and store nsamp readings
    samples[i] = *pin;
}
//-----------------------------------------------------------
/* Sample a pins-list and report results in counts array.  Eg,
   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.
   Use preWait to suppress baseline-level counts (and to reduce 
   sample array size) by waiting a while before beginning to sample.  
 */
uint8_t countPins7(uint8_t drivePin, char *pinsList,
           uint8_t *counts, uint8_t preWait) {
  uint8_t pnum, pomask, pocode, potem, i, j, mul;
  uint8_t sampl[nsamp], pim[nbits], pat[nbits], 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
    portSampler7(drivePin, pocode, pomask, preWait, 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;
      uint8_t m=pim[j];
      for (count=i=0; i<nsamp; ++i)
    count += sampl[i] & m;
      counts[pat[j]] += nsamp - count/m;
    }
    nat += bout;        // Count number of pins done so far
  }
  return pcon;          // Return # of pins processed
}
//-----------------------------------------------------------
// secondly() will report counts at start-of-second
void secondly() {
  uint8_t counts[nbits], j, nc;
  if (millis()%1000) return;    // Wait for start-of-second
  nc = countPins7(4, "2,3,5,6", counts, preSamp);
  Serial << "Counts: ";
  for (j=0; j<nc; ++j)      // Marshal output
      Serial << counts[j] << " ";
  Serial << " t=" << millis() << endl;
  while ((millis()%1000)==0) {}; // Await end-of-millisecond  
}
//-----------------------------------------------------------
uint8_t ostate[nbits] = {0};
void testChange1() {
  uint8_t counts[nbits], j, nc, s;
  nc = countPins7(4, "2,3,5,6", counts, preSamp);
  for (j=0; j<nc; ++j) {
    s = counts[j] > IsHighCount;
    if (s ^ ostate[j]) {
      Serial << j << (s? " on  " : " off ") << counts[j] << " " << millis() << endl;
      ostate[j] = s;
    }
  }
}
//-----------------------------------------------------------
void testChangeA() {
  uint8_t counts[nbits], j, k, nc, s;
  int sumcount[nbits]={0};
  for (k=0; k<SampleSets; ++k) {
    nc = countPins7(4, "2,3,5,6", counts, preSamp);
    for (j=0; j<nc; ++j) {
      sumcount[j] += counts[j];
    }
  }
  for (j=0; j<nc; ++j) {
    s = sumcount[j] > IsHighTotal;
    if (s ^ ostate[j]) {
      Serial << j << (s? " on  " : " off ") << sumcount[j] << " " << millis() << endl;
      ostate[j] = s;
    }
  }
}
//-----------------------------------------------------------
// Take samples of rise times at  designated pins.  When a pin's
// time changes from small to large, report rising edge; or 
// when it changes from large to small, report falling edge.
void loop() {
  //secondly();
  //testChange1();
  testChangeA();
}
  • Thank you for a most comprehensive answer jwpat7! I will try your solution but I have to ask - why does setting the input pins to high impedance is important? Also, I did not know of the presence of the ports you mention in your answer, is there a reference to them in the arduino documentation you could perhaps share with me? Again, thank you for taking your time to reproduce my problem here! – Ginzburg Dec 27 '15 at 18:58
  • 1
    Each input pin is monitoring the voltage on a pad, as that voltage drops according to v(t) = v(0) * exp(-tau * t), where tau = RC time constant. If the input pin weren't hi-Z it would affect the rate of voltage drop. Regarding ports, see edit – James Waldby - jwpat7 Dec 27 '15 at 22:57
  • @Grove, I don't understand your comment, eg what you mean by “the pulsating nature” or by “the on/off algorithm”. Three notes: 1, There should be a layer of insulation (eg packing tape or a solder-resist coating) on top of each key, so it shouldn't matter to the user how the key is being tested. 2, If you mean "only test the button when it's being pressed", I don't know of a way to do that. 3, why does whatever you are referring to matter? Ie will it save energy, time, or what? – James Waldby - jwpat7 Jan 9 '16 at 0:53
  • Hey jwpat7, I deleted my original comment because I found a way to solve my problem: I was trying to attach an event to a specific key once it was pressed but used the on/off algorithm to check its state and was naturally unsuccessful because when a key was pressed it still was less than 180 at one point in the "capacitance cycle". So instead, I just used the j variable as a starting point to see which key was being pressed, as j returns a constant value, i.e. the key number. Thank you again for your help! – Ginzburg Jan 9 '16 at 23:25

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.