Turn your Arduino into a capacitor tester ...

Following on from this thread I decided to try to make a capacitance tester with my Arduino Uno.

It works pretty well for fairly low-value capacitors. :slight_smile:

Circuit:

The objective here is to find the time interval T (tau really) in the equation:

T = R * C

Solved for C:

C = T / R

So we want to charge the capacitor under test with a suitable voltage (eg. 5V from an Arduino output pin) and measure how long it takes to reach 63.2% of that voltage. So we need a reference voltage of 5 * 0.632, namely 3.16V.

The voltage divider illustrated above should provide approximately that:

Vout = Vin * (R2 / (R1 + R2))

5 * (3100 / (1800 + 3100)) = 3.1632653061224

You may need to use a few resistors (eg. 3 x 1K plus 100 ohms) to make up the 3100 ohm resistor.

We now need to time the interval indicated by the arrow and stop when the rising edge reaches the reference voltage:

The internal analogue comparator is just the thing for the job. We connect the reference voltage to the AIN1 pin (negative reference) and connect our capacitor to the AIN0 pin (positive reference) and then configure an interrupt on the rising edge. I chose a value of 10K for the resistor to give a reasonably slow charge time.

A simple sketch follows:

/*
Capacitance meter

Author: Nick Gammon
Date:   27 June 2013

Pulse pin (D2): Connect to capacitor via 10K resistor.

Reference voltage of 0.632 of output pin (pulsePin) connected to D7.
In my case I used 3.06V because I measured 4.84 on the 5V pin.

Measure pin (D6) connected to first leg of capacitor, other leg connected to Gnd.

Like this:

Capacitor to test:

D2  ----> 10K ----> D6 ----> capacitor_under_test ----> Gnd

Reference voltage:

+5V ----> 1.8K ---> D7 ---> 3.1K ----> Gnd

*/

const byte pulsePin = 2;
const unsigned long resistance = 10000;

volatile boolean triggered;
volatile boolean active;
volatile unsigned long startTime;
volatile unsigned long duration;

ISR (ANALOG_COMP_vect)
  {
  // grab time quickly
  unsigned long now = micros ();
  if (active)
    {
    duration = now - startTime;
    triggered = true;
    digitalWrite (pulsePin, LOW);  // start discharging capacitor
    }
  }

void setup ()
  {
  pinMode (pulsePin, OUTPUT);
  digitalWrite (pulsePin, LOW);
  
  Serial.begin (115200);
  Serial.println ("Started.");
  ADCSRB = 0;           // (Disable) ACME: Analog Comparator Multiplexer Enable
  ACSR =  _BV (ACI)     // (Clear) Analog Comparator Interrupt Flag
        | _BV (ACIE)    // Analog Comparator Interrupt Enable
        | _BV (ACIS0) | _BV (ACIS1);  // ACIS1, ACIS0: Analog Comparator Interrupt Mode Select (trigger on rising edge)
   }  // end of setup

void loop ()
  {
  // start another test?
  if (!active)
    {
    active = true;
    triggered = false;
    digitalWrite (pulsePin, HIGH);  // start charging capacitor
    startTime = micros ();  // remember when
    }
    
  // if the ISR noted the time interval is up, display results
  if (active && triggered)
    {
    active = false;
    Serial.print ("Capacitance = ");
    Serial.print (duration * 1000 / resistance);
    Serial.println (" nF");
    triggered = false;
    delay (3000);
    }
  
  }  // end of loop

With a 47 nF capacitor connected, I get this result:

Started.
Capacitance = 47 nF
Capacitance = 46 nF
Capacitance = 47 nF
Capacitance = 46 nF

Not too bad. But with a 6.8 nF capacitor we only get one decimal place:

Capacitance = 6 nF
Capacitance = 7 nF
Capacitance = 7 nF
Capacitance = 7 nF
Capacitance = 6 nF
Capacitance = 6 nF
Capacitance = 7 nF
Capacitance = 6 nF
Capacitance = 7 nF

Plus, the micros() function only returns time to the nearest 4 microseconds.


A slightly more elaborate sketch uses Timer 1 with a prescaler of 1 to get a higher resolution timer.

/*
Capacitance meter

Author: Nick Gammon
Date:   27 June 2013

Pulse pin (D2): Connect to capacitor via 10K resistor.

Reference voltage of 0.632 of output pin (pulsePin) connected to D7.
In my case I used 3.06V because I measured 4.84 on the 5V pin.

Measure pin (D6) connected to first leg of capacitor, other leg connected to Gnd.

Like this:

Capacitor to test:

D2  ----> 10K ----> D6 ----> capacitor_under_test ----> Gnd

Reference voltage:

+5V ----> 1.8K ---> D7 ---> 3.1K ----> Gnd

*/

const byte pulsePin = 2;
const float resistance = 10000.0;

volatile boolean triggered;
volatile boolean active;

volatile unsigned long timerCounts;
volatile unsigned long overflowCount;

ISR (TIMER1_OVF_vect)
{
  ++overflowCount;               // count number of Counter1 overflows  
}  // end of TIMER1_OVF_vect

ISR (ANALOG_COMP_vect)
  {
  // grab counter value before it changes any more
  unsigned int timer1CounterValue;
  timer1CounterValue = TCNT1;  // see datasheet, page 117 (accessing 16-bit registers)
  
  if (active)
    {
    // if just missed an overflow
    if (TIFR1 & TOV1)
      overflowCount++;
    // calculate total count
    timerCounts = (overflowCount << 16) + timer1CounterValue;  // each overflow is 65536 more
    triggered = true;
    digitalWrite (pulsePin, LOW);  // start discharging capacitor
    }
  }

void setup ()
  {
  pinMode (pulsePin, OUTPUT);
  digitalWrite (pulsePin, LOW);
  
  Serial.begin (115200);
  Serial.println ("Started.");
  ADCSRB = 0;           // (Disable) ACME: Analog Comparator Multiplexer Enable
  ACSR =  _BV (ACI)     // (Clear) Analog Comparator Interrupt Flag
        | _BV (ACIE)    // Analog Comparator Interrupt Enable
        | _BV (ACIS0) | _BV (ACIS1);  // ACIS1, ACIS0: Analog Comparator Interrupt Mode Select (trigger on rising edge)
   }  // end of setup

void startTiming ()
  {
  active = true;
  triggered = false;

  // prepare timer
  overflowCount = 0;            // no overflows yet
  // reset Timer 1
  TCCR1A = 0;             
  TCCR1B = 0;   
  TCNT1 = 0;      // Counter to zero
  // Timer 1 - counts clock pulses
  TIMSK1 = _BV (TOIE1);   // interrupt on Timer 1 overflow

  // get on with it
  digitalWrite (pulsePin, HIGH);  // start charging capacitor
  // start Timer 1, no prescaler
  TCCR1B =  _BV (CS10);
    
  } // end of startTiming

void finishTiming ()
  {
  active = false;
  Serial.print ("Capacitance = ");
  float capacitance = (float) timerCounts * 1000.0 / 16.0 / resistance;
  Serial.print (capacitance);
  Serial.println (" nF");
  triggered = false;
  delay (3000);
  }  // end of finishTiming
  
void loop ()
  {
  // start another test?
  if (!active)
    startTiming ();
    
  // if the ISR noted the time interval is up, display results
  if (active && triggered)
    finishTiming ();
  
  }  // end of loop

Output for 6.8 nF capacitor:

Capacitance = 6.96 nF
Capacitance = 6.96 nF
Capacitance = 6.96 nF
Capacitance = 6.95 nF
Capacitance = 6.96 nF
Capacitance = 6.97 nF

And the 47 nF capacitor looks good:

Capacitance = 46.94 nF
Capacitance = 46.91 nF
Capacitance = 46.94 nF
Capacitance = 46.85 nF
Capacitance = 46.91 nF
Capacitance = 46.91 nF
Capacitance = 46.91 nF

Unfortunately a 100 nF capacitor seems to give a high reading:

Started.
Capacitance = 126.75 nF
Capacitance = 126.67 nF
Capacitance = 126.61 nF
Capacitance = 126.74 nF
Capacitance = 126.65 nF
Capacitance = 126.69 nF

Does anyone know why this would be? Is there leakage in the capacitor which makes it take longer to charge, or something? Judging by research I have done measuring capacitance is not really trivial, so I suppose I should be happy it works reasonably well.

Does anyone know why this would be?

I think most common ceramic capacitors are ±20% so 127 nF is almost in range.

How close is the 10 K resistor to 10 K?

How close is the 10 K resistor to 10 K?

10.003 K

I think most common ceramic capacitors are ±20% so 127 nF is almost in range.

It's a plastic one, and it measures around 110 nF on the LCR meter so maybe the calculations aren't all that far out.

Unfortunately a 100 nF capacitor seems to give a high reading:

I just tried your sketch with 3 different 100nF capacitors:

Capacitance = 108.81 nF

Capacitance = 98.34 nF

Capacitance = 101.14 nF

and a 10nF

Capacitance = 10.96 nF

So well within the rating (I don't know if it is 10% or 20%)

I also tried with a 10 M ohm resistor, I was hoping to be able to read even smaller capacitors like 22pF, but no luck.

no luck

But ?
0 pF or in the 10 nF ( 10,000 pF ) range as well ?

With a 22pF capacitor I get

Capacitance = 0.05 nF

But then I discovered that without a capacitor I get: Capacitance = 0.02 nF

So the capacitans of the circuit itself is 0.02nF, thus the reading could be interpreted as 0.03 nF ? which is close.

Edit:

So I tried multiplying the result with 1.000 and subtracting the capacitans of the circuit I get with a 22 pF capacitor
Capacitance = 23.72 pF
Capacitance = 25.85 pF
Capacitance = 26.34 pF
Capacitance = 23.64 pF

And 2 x 22pF in parallel:
Capacitance = 48.66 pF
Capacitance = 50.91 pF
Capacitance = 48.44 pF
Capacitance = 48.19 pF

I think that part of the problem with measuring small capacitors is that pin leakage, though small, becomes troublesome compared to the current that a small capacitor draws through a big resistor. We need to keep the charge time long enough to get adequate resolution, but we need the charging current to be much larger than the leakage current. It's hard to get both with small capacitors.

I remember doing this with Timer1 as the clock, and that it ran at 16MHz. Here's what I recall:

  • Enable Timer1 Overflow and Input Capture interrupts.
  • Set the Analog Comparator to trigger Timer1 input capture
  • In the Timer1 OVF ISR, increment a counter
  • In the Timer1 Input Capture ISR, stop the timer and grab the Input Capture Register

To make a measurement,

  • Set Timer1 count to zero
  • Set the Timer1 overflow counter to zero
  • Start Timer1
  • Raise the charging pin high

The charging duration was ((overflow count << 16) + (Input Capture Register)) / 16,000,000, in seconds. With a faster clock, it's possible to get adequate resolution with higher charging currents. Also, interrupt latency isn't a factor - no code has to respond to anything immediately, since all the high-resolution timing functions are done by the hardware. I never calibrated the device against any standard, but I recall that values I got for small capacitors were reasonable compared to their markings.

I'm getting this from my memory, with help from the datasheet. I think there's more to it than I've said here. If anybody's interested, I'll hunt for the sketch and post it.

You may need to use a few resistors (eg. 3 x 1K plus 100 ohms) to make up the 3100 ohm resistor.

You could use a 10K, 10 turn potentiometer and adjust it for the voltage required.
+5V to the top, 0V to the bottom, adjust for 3.16V at wiper.

I wonder what results you get if a constant current source was used to charge the capacitor.

This snippet has a race condition (and has a simple bug highlighted below)...

  timer1CounterValue = TCNT1;  // see datasheet, page 117 (accessing 16-bit registers)
  
  if (active)
    {
    // if just missed an overflow
    if (TIFR1 & TOV1)
      overflowCount++;

It is possible for the overflow to occur after saving the TCNT1 value.

// if just missed an overflow
if ( (TIFR1 & (1 << TOV1)) && (timer1CounterValue <= 128) )
overflowCount++;

I think the "128" has to be high enough to cover the time between the interrupt occurring and TCNT1 being saved.

if ( (TIFR1 & (1 << TOV1)) && (timer1CounterValue <= 128) )
overflowCount++;

Incrementing the global variable creates a problem. When the interrupt service routine returns, the overflow flag is still set. overflowCount will be incremented a second time by the overflow interrupt service routine.

Clearing the overflow flag is a bad idea. That sets up another race condition. The correct way to handle overflowCount is to save the value to a local variable and work with the local variable.

Good pick up on the bug.

You are right about testing the timer value, and it would need to be done immediately, so that not enough clock cycles can elapse for it to pass the tested-for value. Bear in mind it is timer 1, so we can probably test for quite a high value (eg. 256, 512).

The correct way to handle overflowCount is to save the value to a local variable and work with the local variable.

In this particular code we are done with overflowCount at this point so making a copy is not necessary.

However in principle (if we keep counting) yes you are right because then the overflow interrupt would kick in and increment it again.

How about this then?

ISR (ANALOG_COMP_vect)
  {
  // grab counter value before it changes any more
  unsigned int timer1CounterValue;
  timer1CounterValue = TCNT1;  // see datasheet, page 117 (accessing 16-bit registers)
  unsigned long overflowCopy = overflowCount;
  
  if (active)
    {
    // if just missed an overflow
    if ((TIFR1 & _BV (TOV1)) && timer1CounterValue < 256)
      overflowCopy++;
    // calculate total count
    timerCounts = (overflowCopy << 16) + timer1CounterValue;  // each overflow is 65536 more
    triggered = true;
    digitalWrite (pulsePin, LOW);  // start discharging capacitor
    }
  }

LarryD:
You could use a 10K, 10 turn potentiometer and adjust it for the voltage required.
+5V to the top, 0V to the bottom, adjust for 3.16V at wiper.

I did that in the end, but I encountered another problem. It turned out that "5V" was actually 4.85V, so I had to scale down the pot to be 4.85 * 0.632 namely 3.065V. At least the fixed resistors (if correct) should divide whatever-the-voltage-is in the correct ratio.

Erni:
With a 22pF capacitor I get

Capacitance = 0.05 nF

But then I discovered that without a capacitor I get: Capacitance = 0.02 nF

I got an even larger value without a capacitor (0.28 nF) but I do have wires going everywhere. Probably the stray capacitance is what is being reported.

Looks good.

Oops - I see that I read only half of Nick's original post. The other half covered using Timer1, rather than micros(). I foolishly responded with a description of what Nick had largely already shown. Apologies to Nick, and to anyone that wasted time with that redundancy.

There was one fresh idea, though - using the Timer1 Input Capture Register (ICR) to grab the end of the charging interval. The ICR capture happens in hardware, triggered by the analog comparator, with a few system clock cycles of delay from the synchronizing logic. Fetching TCNT1 in an ISR takes a lot longer: at least four cycles to respond to the interrupt, and another bunch of cycles - about 34, I think - while the ISR pushes the internal registers, plus whatever latency comes from execution other interrupts. I mark a minimum delay of close to 40 cycles, corresponding to an offset in the capacitance measurement of about 0.25 nF with a 10K resistor. To use the ICR, the comparator has to be set up to trigger the capture, and the edge selection happens in the Timer1 registers. The program needs a capture ISR, and the interrupt needs to be enabled. There's still ambiguity to resolve with overflow flag; it looks to me like it resolves the same way.

I'll add that getting a resistor divider with a ratio really close to 0.632 isn't entirely necessary, and the project can be freed from the tyranny of available resistor values. The formula, using arbitrary resistors, is:

capacitance = (float) timerCounts * 1000.0 / 16.0 / resistance / log( ( R1 + R2 ) / R2 );

where R1 and R2 are the resistors in the divider, with R1 grounded, and R2 at Vcc. When (R1 / (R1 + R2)) is 0.632, the log() term becomes 1, and the this equation reduces to the original formula.

Finally, I think it's a good idea to wrap the statements that start the timer and start charging the capacitor with cli() ... sei(), to keep another interrupt from happening between them, leading to anomalous results.

[Edit: spelling]

Good points. Although I can't quite get my head around the race condition of the overflow happening at the wrong moment.

The input capture register will record the TCNT1 value, but what if it overflows before we can read it? Do we count the overflow or not?

Imagine that the timer overflowed, but before the overflow interrupt could add one to the overflow count, the capture occurs (on the analog comparator). For example, we might be in another ISR.

As the capture interrupt is higher priority we get the new (low) value on ICR1, but overlook the overflow. However if we do count the overflow bit, we won't know for sure if it occurred after the capture completed.

In the current code we read both the TCNT1 and the overflow counter within a few clock cycles of each other, and then check the overflow bit.

Logically, there's no real difference between using the ICR to capture TCNT1, and reading TCNT1 directly in an ISR. Using the ICR just connects the capture of TCNT1 and the comparator's change of state more closely in time. If the capture ISR sees TOV1 active, the decision about whether the overflow gets processed or not is based on the magnitude of the ICR. If it's small, the overflow happened first, and it counts; if it's large, the capture happened first, and the overflow doesn't count. The test in the code above still works:

    if ((TIFR1 & _BV (TOV1)) && timer1CounterValue < SomethingSmall)

The assumption is that the capture ISR will execute fairly soon after the capture event. The ISRs aren't hugely challenging; there's no reason to believe that an interrupt will pend for, say, a millisecond - the level at which millis() becomes unusable. If that assumption holds, then, because Timer1 takes over four milliseconds to roll over, the ICR will always be less than half-scale if the overflow happened first, and it will be more than half scale if the interrupt happened later. In the test, SomethingSmall could be 0x8000, unsigned.

Well I can't get that to work. Code below:

const byte pulsePin = 2;
const float resistance = 10000.0;

volatile boolean triggered;
volatile boolean active;

volatile unsigned long timerCounts;
volatile unsigned long overflowCount;

ISR (TIMER1_OVF_vect)
{
  ++overflowCount;               // count number of Counter1 overflows  
}  // end of TIMER1_OVF_vect

ISR (TIMER1_CAPT_vect)
  {
  // grab counter value before it changes any more
  unsigned int timer1CounterValue;
  timer1CounterValue = ICR1;  // see datasheet, page 117 (accessing 16-bit registers)
  unsigned long overflowCopy = overflowCount;
  
  if (active)
    {
    // if just missed an overflow
    if ((TIFR1 & _BV (TOV1)) && timer1CounterValue < 0x7FFF)
      overflowCopy++;
    // calculate total count
    timerCounts = (overflowCopy << 16) + timer1CounterValue;  // each overflow is 65536 more
    triggered = true;
    digitalWrite (pulsePin, LOW);  // start discharging capacitor
    }
  }

void setup ()
  {
  pinMode (pulsePin, OUTPUT);
  digitalWrite (pulsePin, LOW);
  
  Serial.begin (115200);
  Serial.println ("Started.");
  ADCSRB = 0;           // (Disable) ACME: Analog Comparator Multiplexer Enable
  ACSR =  _BV (ACI)     // (Clear) Analog Comparator Interrupt Flag
        | _BV (ACO)     // Analog Comparator Output
        | _BV (ACIC)    // Analog Comparator Input Capture Enable
        | _BV (ACIS0) | _BV (ACIS1);  // ACIS1, ACIS0: Analog Comparator Interrupt Mode Select (trigger on rising edge)
   PRR = 0;
   }  // end of setup

void startTiming ()
  {
  active = true;
  triggered = false;
  noInterrupts ();

  // prepare timer
  overflowCount = 0;            // no overflows yet
  // reset Timer 1
  TCCR1A = 0;             
  TCCR1B = 0;   
  TCNT1 = 0;      // Counter to zero
  // Timer 1 - counts clock pulses
  TIMSK1 = _BV (TOIE1) | _BV (ICIE1);   // interrupt on Timer 1 overflow and input capture

  // get on with it
  digitalWrite (pulsePin, HIGH);  // start charging capacitor
  // start Timer 1, no prescaler
  TCCR1B =  _BV (CS10) | _BV (ICES1);  // plus Input Capture Edge Select
  interrupts ();
    
  } // end of startTiming

void finishTiming ()
  {
  active = false;
  Serial.print ("Capacitance = ");
  float capacitance = (float) timerCounts * 1000.0 / 16.0 / resistance;
  Serial.print (capacitance);
  Serial.println (" nF");
  triggered = false;
  delay (3000);
  }  // end of finishTiming
  
void loop ()
  {
  // start another test?
  if (!active)
    startTiming ();
    
  // if the ISR noted the time interval is up, display results
  if (active && triggered)
    finishTiming ();
  
  }  // end of loop

Minor changes between what works (without using the ICR register) and above.

The ISR just doesn't fire, but I think I have got the bits set right.

Dude, I'd love to help, but I can't - I couldn't reproduce the problem. Your code, unchanged, with hardware that looks like your schematics, worked for me, first time. I used a 10 uF, 220 nF, 2.2 uF, and they all measured reasonably. The assembly was easy to bungle with errant leads touching each other, but, other than my own fatfingers, I couldn't get it to fail.

In my code, I didn't set ACO, I didn't set the interrupt mode select bits in ACSR, and I did set the digital input disable bits for the analog comparator in DIDR1. Also, I used falling edge, but that's just the way I hooked it up. I tried setting those bits in my code, and not setting DIDR1, but none of those things would make it fail.

Looks to me like it works. Maybe a wire fell out?

[Edit: add this] I got 0.0063 nF with an open circuit - one tick.