My earlier post, Capacitance meter using touchRead(), showed the beginnings of a capacitance meter, using the touchRead() routine in the Teensyduino environment.
Today I’ll share a slightly more complicated program that uses the TSI (touch sensing input) peripheral more directly to make an autoranging capacitance meter that can measure capacitors down to 1pF and up to 3µF. The repeatability of the measurements is not great (about ±3%), but the linearity seems pretty good.
The program includes the ability to zero out the test fixture (important for measuring small capacitances) and to calibrate the meter to a known capacitor. I don’t have a capacitor with a tight tolerance, so I had to do my calibration against a DT-9205A multimeter, which is not a very reliable standard. Still, it seemed more consistent than the labeling on the cheap ceramic capacitors I have, some of which seem to be off by a factor of two!
The multimeter could not measure capacitances at the low end of the range, so only nominal values are used there.
A number of the capacitors in the 100pF–10nF range seemed to drift consistently upward for quite a while, both with the multimeter and my homebrew capacitance meter. Touching them with my finger brought them sharply back down again, but touching them with stainless steel tweezers did not. I suspect that the effect is a thermal one, with the capacitance dropping when my fingers warmed the capacitors and going up again as the capacitors cooled to room temperature. The change was several percent, which is consistent with a low-quality “Y” dielectric.
// Preliminary program for a capacitance meter
// Kevin Karplus
// 2016 Jul 13
// To use:
// * Connect a serial monitor (like the Arduino IDE) to the USB port
// * Connect capacitor to measure between pin 0 and ground
// * Press "a" and <return> to make an autorange measurement.
// Capacitances in the range 1pF to 3uF can be measured, but large
// capacitors take a long time to measure, up to 9s for 3uF.
// The measurement time can be reduced by reducing NUM_READS, perhaps
// replacing the constant by an array dependent on range.
// A factor of about 10 reduction in measurement time is available, as
// NUM_READS is currrently set to 20.
// Note: readings may drift by +- 5%. I've not yet determined the
// cause of this drift. It may be thermal (cooling from finger
// temperature to room temperature may raise the capacitance by 2%,
// depending on the additives to the ceramic used as the dielectric).
// To calibrate:
// * Remove capacitors from pin 0
// * Type 'z' to measure the counts for the empty test fixture
// * Pick a known capacitor (around 1nF) and connect it between
// pin 0 and ground. The capacitor must be small enough
// to be measurable on the highest-resolution range (<=3nF).
// To calibrate with a larger capacitor, first calibrate with
// a small one to get the highest-resolution range set, then
// repeat the calibration with the larger capacitor, which
// will only reset the larger ranges.
// * Type 'k', followed by an integer known capacitance in pF,
// followed by a separator (like ';')
// * Type 'c', to measure the pF per count
// Note: the calibrations are reported to the USB serial port, so they
// can be saved and used to edit the source code, changing count_for_0pF
// and pf_per_count arrays.
// The measurement process can be followed in more detail by turning
// on debugging with the 'D' command.
// Debugging can be turned off with the 'd' command.
// Capacitance is measured on pin 0 (one of several pins with TSI),
// because pin 0 is immediately next to ground, making it easy to connect
// small capacitors between pin 0 and ground.
#define TOUCH_PIN (0)
// forward references to later routines
int32_t capRead(uint8_t pin,
uint8_t I_ext=3, uint8_t I_ref=4,
uint8_t Prescale=2, uint8_t N_scan=9);
float autoCapRead(uint8_t pin);
int readInt(void);
void print_calib(int count, int n_cycles=1);
float read_counts_per_cycle(uint8_t i_ext, uint8_t i_ref,float count_for_zero);
volatile uint8_t debug;
// The current for the external oscillator and the reference
// oscillator of the TSI interface determine the capacitance range, resolution,
// and speed of the measurement.
// The program uses the highest-resolution range that does not cause
// the 16-bit counter to overflow.
// Settings of the current paramters for the different ranges
// The highest-resolution range is first, the widest range is last.
#define NUM_RANGES (6)
const int8_t i_ext_choices[NUM_RANGES] = {2,3,4,5,6,7};
const int8_t i_ref_choices[NUM_RANGES] = {5,4,3,2,1,0};
float pF_per_count[NUM_RANGES]=
{0.2126, 0.772, 2.897, 11.08, 47., 195.3};
float count_for_0pF[NUM_RANGES] =
{48.7612, 13.82, 3.813, 1.0605, 0.273, 0.0517};
// parameters for calibration
int C_known=0; // known capacitance value
void setup()
{
pinMode(TOUCH_PIN, INPUT);
Serial.begin(115200);
debug=0;
}
void loop()
{
if (Serial.available())
{ char c=Serial.read();
if (c=='D') {debug=1;}
else if (c=='d') {debug=0;}
else if (c=='a')
{ // do one reading and print it
Serial.print(autoCapRead(TOUCH_PIN));
Serial.println(" pF");
}
else if (c=='k')
{ // set known capacitance for calibration checks
C_known=readInt();
}
else if (c=='z')
{ // Set the count_for_0pF array for each range.
// Print the current parameters and zero count for each range.
// Only issue this command if there is no capacitor connected
// to TOUCH_PIN
for (int range=0; range<NUM_RANGES; range++)
{ uint8_t i_ext=i_ext_choices[range];
uint8_t i_ref=i_ref_choices[range];
count_for_0pF[range] =read_counts_per_cycle(i_ext,i_ref,0.0);
Serial.print("# zero for ");
Serial.print(i_ext); Serial.print("\t");
Serial.print(i_ref); Serial.print("\t");
Serial.println(count_for_0pF[range],4);
}
}
else if (c=='c')
{ // do autocalibration, setting pF_per_count
// printing known capacitance,
// external current setting,
// reference current setting,
// average pF_per_count for a single cycle
if (C_known==0)
{ Serial.println("# use 'k<known C in pF>;' first");
return;
}
for (int range=0; range<NUM_RANGES; range++) { uint8_t i_ext=i_ext_choices[range]; uint8_t i_ref=i_ref_choices[range]; float calib=read_counts_per_cycle(i_ext,i_ref,count_for_0pF[range]); if (!isnan(calib)) { calib = C_known/calib; pF_per_count[range]= calib; } else { Serial.print("# "); // comment out overflows } Serial.print(C_known); Serial.print("\t"); Serial.print(i_ext); Serial.print("\t"); Serial.print(i_ref); Serial.print("\t"); Serial.println(calib,4); } } } } // print a calibration line void print_calib(int count, uint8_t i_ext, uint8_t i_ref, int n_cycles) { float avg_count = (count+0.0)/n_cycles; Serial.print(C_known); Serial.print("\t"); Serial.print(i_ext); Serial.print("\t"); Serial.print(i_ref); Serial.print("\t"); Serial.print(count); Serial.print("\t"); Serial.print(n_cycles); Serial.print("\t"); Serial.println(C_known/avg_count,4); } float read_counts_per_cycle(uint8_t i_ext, uint8_t i_ref, float count_for_zero) { // Do one measurement and return average counts/cycle - count_for_zero. // If debug set, print C_known, count, i_ext, i_ref, n_cycles, cap/count // // Actually does NUM_READS+1 measurements: one with a single cycle, // then again NUM_READS times with as many cycles as can be fit without // overflowing counter. if (debug) { Serial.println("# C\ti_ext\ti_ref\tcount\tn_cycle\tC/avg_count"); } int count=capRead(TOUCH_PIN, i_ext, i_ref, 0, 0); if (count>=0xFFFF)
{ if (debug)
{ Serial.print("# "); // comment out overflows
print_calib(count,i_ext,i_ref,1);
}
return NAN; // abort rest of calibration check
}
// Determine max number of cycles that can fit
int cycles= 0xFFFE/count; // how many cycles to use
int prescale,n_scan;
for (prescale=0; cycles>32 && prescale<7; prescale++) {cycles/=2;} if (cycles==0) {n_scan=0;} else if (cycles>32) {n_scan=31;}
else {n_scan=cycles-1;}
cycles = (n_scan+1)<<prescale;
#define NUM_READS (20)
// To do: replace NUM_READS with a const uint8_t num_reads[NUM_RANGES]
// array, to speed up measurement at high ranges.
int sum_count=0;
for (int i=0; i<NUM_READS; i++) { count=capRead(TOUCH_PIN, i_ext, i_ref, prescale, n_scan); if (debug) { print_calib(count, i_ext, i_ref, cycles); } sum_count+=count; } return (sum_count+0.0)/ (NUM_READS*cycles) -count_for_zero; } // Read a non-negative integer from Serial as a series of digits, terminated // by any non-digit (recommend using something obvious like ';'). // The terminating character is discarded. int readInt(void) { int value=0; while (!Serial.available()) {} // wait for next char char c=Serial.read(); while (c>='0' && c<='9')
{ value= 10*value + (c-'0');
while (!Serial.available()) {} // wait for next char
c=Serial.read();
}
return value;
}
float autoCapRead(uint8_t pin)
{
// Return the capcitance in pF at the pin, using the highest-resolution
// range that doesn't overflow the counter.
// pick the lowest (highest resolution) range that doesn't overflow
int32_t count; // number of counts of ref oscillator
int range;
for (range=0, count=0xFFFF; range<NUM_RANGES && count>=0xFFFF; range++)
{ count=capRead(pin, i_ext_choices[range],i_ref_choices[range],0,0);
}
range--;
if (count>=0xFFFF)
{ return NAN; // capacitance too big to measure with TSI
}
float count_per_cycle=read_counts_per_cycle(i_ext_choices[range],
i_ref_choices[range],
count_for_0pF[range]);
return pF_per_count[range]*count_per_cycle;
}
/* Raw reading is based on the
* Teensyduino Core Library touch.c (which implements touchRead)
* http://www.pjrc.com/teensy/
* Copyright (c) 2013 PJRC.COM, LLC.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* 1. The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* 2. If the Software is incorporated into a build system that allows
* selection among a list of target devices, then similar target
* devices manufactured by PJRC.COM must be included in the list of
* target devices and selectable in the same manner.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// #include "core_pins.h"
// capacitance is supposed to be
// Cref * (1<<I_ext) / ( (1<<I_ref) * (1<<Prescale) * (N_scan+1) ) * COUNT
// but use
// Cref * ext_current[I_ext] / ( ref_current[I_ref] * (1<<Prescale) * (N_scan+1) ) * COUNT // because the current ratios are not a constant factor of 2 // with DVOLT==0 (slowest, but least noise sensitive), // I_ext = I_ref-1, // Prescale=2, // N_scan=9, // Capacitance is approx 0.01846 pF * COUNT, // so Cref approx 1.47694pF #if defined(__MK20DX128__) || defined(__MK20DX256__) // These settings give approx 0.02 pF sensitivity and 1200 pF range // Lower current, higher number of scans, and higher prescaler // increase sensitivity, but the trade-off is longer measurement // time and decreased range. static const uint8_t pin2tsi[] = { //0 1 2 3 4 5 6 7 8 9 9, 10, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13, 0, 6, 8, 7, 255, 255, 14, 15, 255, 12, 255, 255, 255, 255, 255, 255, 11, 5 }; #elif defined(__MK66FX1M0__) static const uint8_t pin2tsi[] = { //0 1 2 3 4 5 6 7 8 9 9, 10, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13, 0, 6, 8, 7, 255, 255, 14, 15, 255, 255, 255, 255, 255, 11, 12, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; #elif defined(__MKL26Z64__) static const uint8_t pin2tsi[] = { //0 1 2 3 4 5 6 7 8 9 9, 10, 255, 2, 3, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13, 0, 6, 8, 7, 255, 255, 14, 15, 255, 255, 255 }; #endif // for I_ref=I_ext+1, N_scan=9, Prescale=2, output is approx pF * 50 // time to measure 33 pF is approx 0.25 ms // time to measure 1000 pF is approx 4.5 ms int32_t capRead(uint8_t pin, uint8_t I_ext, uint8_t I_ref, uint8_t Prescale, uint8_t N_scan) { uint32_t ch; int32_t count; if (pin >= NUM_DIGITAL_PINS) return 0;
ch = pin2tsi[pin];
if (ch == 255) return 0;
*portConfigRegister(pin) = PORT_PCR_MUX(0);
SIM_SCGC5 |= SIM_SCGC5_TSI;
#if defined(KINETISK)
TSI0_GENCS = 0;
TSI0_PEN = (1 << ch);
TSI0_SCANC = TSI_SCANC_REFCHRG(I_ref) | TSI_SCANC_EXTCHRG(I_ext);
TSI0_GENCS = TSI_GENCS_NSCN(N_scan) | TSI_GENCS_PS(Prescale) | TSI_GENCS_TSIEN | TSI_GENCS_SWTS;
delayMicroseconds(10);
while (TSI0_GENCS & TSI_GENCS_SCNIP) ; // wait
delayMicroseconds(1);
count= *((volatile uint16_t *)(&TSI0_CNTR1) + ch);
#elif defined(KINETISL)
TSI0_GENCS = TSI_GENCS_REFCHRG(I_ref) | TSI_GENCS_EXTCHRG(I_ext) | TSI_GENCS_PS(Prescale)
| TSI_GENCS_NSCN(N_scan) | TSI_GENCS_TSIEN | TSI_GENCS_EOSF;
TSI0_DATA = TSI_DATA_TSICH(ch) | TSI_DATA_SWTS;
delayMicroseconds(10);
while (TSI0_GENCS & TSI_GENCS_SCNIP) ; // wait
delayMicroseconds(1);
count= TSI0_DATA & 0xFFFF;
#endif
if (debug)
{
Serial.print(" I_ext= "); Serial.print(I_ext);
Serial.print(" I_ref= "); Serial.print(I_ref);
Serial.print(" Prescale= "); Serial.print(Prescale);
Serial.print(" N_scan= "); Serial.print(N_scan);
Serial.print(" count= "); Serial.print(count);
Serial.println();
}
return count;
}