Sonntag, 16. November 2014

Capacitance measurement using the Charge-Transfer-Acquisition-Principle

Some time ago, I read in a magazine about a technique to measure tiny variations in capacitance. I don't remember the name of the magazine, so I cannot give any reference to it. My attempts to google for it were not successful either. Finally, I decided to give it a try.
Edit: Just now I read in the "ELV journal 5/2014 p.47" that this technique is called "Charge-Transfer-Acquisition-Principle". It is mainly used in connection with capacitive touch buttons. ST www.st.com builds controllers for this kind of applications. Search for "capacitive touch" on their website.

This is how it is supposed to work:
Figure 1: principle of capacity measurement
Two capacitors, a small one (the one that is to be measured) and a larger one (the reference), are connected in series. The idea is: Charge the larger capacitor in several steps, using a current that has to flow through the small capacitor as well. The small one will build up a reverse voltage much quicker than the large one. The current will stop. Reset the small capacitor by shorting it. Repeat the charge-reset cycle and count how many cycles are needed to "fully" charge the large capacitor. The smaller the small capacitor is, the more cycles are needed.


It might be difficult to make absolute measurements of capacitance with this technique. But that is not needed if only variations in capacitance are of interest. The first application that comes to mind are capacitive touch buttons. I believe there are many other interesting things that can be done with this.
In that article, the procedure was realized by connecting the tree points of the circuit to I/O pins of a microcontroller.

I connected the points a,b and c in Fig. 1 to the ATMEGA8 at pins PC2, PC1 and PC0 respectively. As large cap, I used 145 nF (multimeter measurement), and  for the small one I use a 10 nF.
The rest is done in the firmware. The program has to switch the three pins to the right sequence. I consider the big capacitor to be "full" when the voltage (after discharging the small one) at point a is recoginzed as "high" by the microcontroller.
To initialize a new measurements, both capacitors have to be discharged. this is ensured by putting all three points to GND and wait a bit:

// discharge both caps
PORTC &= ~((1<<PC0) | (1<<PC1) | (1<<PC2)); // PORTC = ?????000
DDRC |= (1<<DDC0) | (1<<DDC1) | (1<<DDC2); // DDRC = ?????111
_delay_us(50);


After this, I go to the reset step in Fig. 1: b and c connected to GND (shorted) and a configured as input. A has to be configured as input (high impedance) to prevent it from loosing any charge. 
DDRC ^= (1<<DDC2);
After initializing, the charge and reset states have to be repeated until the big cpacitor is full. The chargin step is achieved by the following:
  • disconnect b from GND (make it an input)  DDRC ^= (1<<DDC1);
  • connect point a to +5V (VCC), i.e. set it to 1 PORTC ^= (1<<PC2); Note that point a is still an input, but writing 1 to the PORTC:2 bit enables the pull-up resistor which is charging the two capacitors
  • wait until the small capacitor is full, i.e. point b is above the threshold to be deteced as high PORTC ^= (1<<PC2);
Going back to "reset" is done by:
  • disconnect the pull-up connection to VCC from point a. This prevent the big capacitor from loosing any charge PORTC ^= (1<<PC2);
  • put point b to GND to discharge the small capacitor, as it was done at the end of the initialization DDRC ^= (1<<DDC1);
All these steps are just done by swapping the correct bits in either PORTC or DDRC using XOR operations.

After each charge-reset cycle, one can test if the big capacitor is already charged, i.e. if point a is 1:
(PINC & (1<<PC2)); // condition to end the measurement

Here are the voltage traces of point a and point b on the oscilloscope:
Figure 2: The voltages at point a (purple) and point b (blue)

Fig. 2 shows all cycles of one measurement. Each spike is one cycle. One can see how the lower end of the purple (point a) line is higher after each cycle because the big capacitor carries more and more charge. Nine cycles are needed to complete the measurement. The bigger the difference between the two capacitors, the more cycles are needed and the more sensitive is the measurement. 
The technique really works nicely and reliably. However, measuring 10 nF can be still done with my multimeter. But this method scales down to much smaller capacitors. I tried a 10 nF as big capacitor, and nothing at all as small capacitor ( there is the capacitance of the breadboard lines): I could see difference in number of cycles by putting a 2pF capacitor.
It is nice to have an oscilloscope to see what is going on. If You don't have one, you can output the number of cycles as a binary number, using LEDs. 

The hardware is trivial (no picture this time). Here is the complete firmware:


#define F_CPU 1000000
#include <avr/io.h>
#include <util/delay.h>

// 
// PC2 -----|
//          |
//      =========    large capacitor
//          |
// PC1 -----+
//          |
//         ===       small capacitor
//          |
// PC0 -----|
//

void initialize()
{
 // discharge both caps
 PORTC &= ~((1<<PC0) | (1<<PC1) | (1<<PC2));
 DDRC  |= (1<<DDC0) | (1<<DDC1) | (1<<DDC2);
 _delay_us(50);

 // go to state: reset (discharge small cap)
 DDRC ^= (1<<DDC2);
 // now:
 //  DDRC  = 0b?????011
 //  PORTC = 0b?????000
}
void cycle()
{
 // assume:
 //  DDRC  = 0b?????011
 //  PORTC = 0b?????000

 // go to state: charge (large cap through small cap)
 DDRC  ^= (1<<DDC1);
 // now:
 //  DDRC  = 0b?????001
 //  PORTC = 0b?????000
 PORTC ^= (1<<PC2);
 // now:
 //  DDRC  = 0b?????001
 //  PORTC = 0b?????100
 while (!(PINC & (1<<PC1)));

 // go back to state: reset
 PORTC ^= (1<<PC2);
 DDRC  ^= (1<<DDC1);

}

int main(void)
{
 DDRD  |= (1 << DDD0);
 //PORTD |= (1 << PD0);
 while(1)
 {
  // create trigger for oscilloscope visualization
  PORTD ^= (1 << PD0);

  initialize();

  int cycles;
  for (cycles = 0; !(PINC & (1<<PC2)); ++cycles) cycle();

  // create trigger for oscilloscope visualization
  PORTD ^= (1 << PD0);

  _delay_ms(10);
 }
 
 return 0 ;
}

Keine Kommentare:

Kommentar veröffentlichen