# Gas station without pumps

## 2016 July 13

### Autoranging capacitance meter using TeensyLC

Filed under: Circuits course — gasstationwithoutpumps @ 09:59
Tags: , , , ,

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
uint8_t I_ext=3, uint8_t I_ref=4,
uint8_t Prescale=2, uint8_t N_scan=9);
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())
if (c=='D') {debug=1;}
else if (c=='d') {debug=0;}
else if (c=='a')
{   // do one reading and print it
Serial.println(" pF");
}
else if (c=='k')
{   // set known capacitance for calibration checks
}
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];
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;

// 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
}
return value;
}

{
// 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++)
}
range--;

if (count>=0xFFFF)
{    return NAN;	// capacitance too big to measure with TSI
}

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;
}

## 2016 July 11

Filed under: Circuits course — gasstationwithoutpumps @ 12:57
Tags: , ,

I decided to see whether the TSI (touch sensing input) on the Teensy boards was good enough to use as a capacitance meter. My first attempt was to use the touchRead() interface from the Teensyduino environment.

The test code was very simple:

#define LED_PIN  (13)
#define TOUCH_PIN (0)

void setup()
{
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
pinMode(TOUCH_PIN, INPUT);
Serial.begin(115200);
}

void loop()
{
if (Serial.available())
if (c=='1')
{   int sum_touch=0;
for(int i=100; i>0; i--)
digitalWrite(LED_PIN, HIGH);
delay(20);
digitalWrite(LED_PIN, LOW);
}
Serial.println(sum_touch);
}
}
}

I just read the pin with touchRead 100 times, adding the results. I measured several different capacitors to find out the range of readings and to fit a function to the data.

The way the touch sensing works is that there are two hysteresis oscillators in the microcontroller, one using a fixed reference capacitance and the other using the capacitance of a pin.  The feedback in each oscillator is not a resistor, but switches between a positive and a negative constant-current source. This should give the input to the Schmitt trigger a clean triangle wave.  There are 8 different constant-current sources to choose from, and different sources can be chosen for the two oscillators.  The amount of hysteresis can also be chosen.  The pin-controlled oscillator output is divided down by a pair of counters (one that can be set to 1, 2, 4, 8, …, 128, the other to  1, 2, 3, 4, … , 32), and the number of pulses of the reference oscillator is counted for one tick of the divided-down pin-controlled oscillator.  The touchRead() function fixes which current sources, which hysteresis voltage, and which counter settings are used, so that the reading is directly proportional to the period of the pin-controlled oscillator, which in turn is directly proportional to the capacitance.

Therefore, the readings should be linear with the capacitance, but there is likely to be some parasitic capacitance that needs to be added (indeed, I get a reading of around 551.7 when the Teensy board is plugged into a bread board, with no deliberate capacitance is added).  So the model I want to fit is $y= a (C + C_{0})$.

The simple linear fit measures each capacitor within about 10% of its nominal value, which may be as accurate as the capacitors are (these were cheap ceramic assortments from China, with no specs).

To get good fits at the low end, I fit the C0 parameter only for the data points at 0pF, 1pF, and 5pF, but fit the scaling parameter over the whole range. It is not visible on the plot, but each capacitor was measured multiple times, and the variation in measurement was less than ±0.3%.  Of course, this is for averages of 100 readings, so the raw readings may vary up to ±3%—I’ve not tested for variation.  Somewhat surprisingly, the % variation seemed to be larger at the high end than at the low end—I would have expected the high end to have less variation (because of reduced quantization error).

I checked the capacitance of a small aluminum foil and packing tape touch pad, like we use in the applied electronics course.  It was about 7.05pF when not touched, and 60.8pF when touched with moderate pressure.

Because touchRead() reports 65535 for any capacitance that is too large, the maximum measurable capacitance is 65534/55.3942 – 10.1921 pF = 1173pF. If I want to measure large capacitors, I’ll need to rewrite the touchRead() code to use the TSI functions of the microcontroller more directly. I’ve started work on that, but I’ll save it for a later post. I expect to be able to measure up to at least 1µF. That code will also include translation to standard capacitance units and calibration options.