2

I am working on project and using Arduino Pro Mini (Atmega328p running on 2xAA) to measure the time to charge a capacitor (when the volt level is high). This is the code I used while testing:

int counter = 0;

// Charge the capacitor
digitalWrite(8,HIGH);

while (digitalRead(7) == LOW) {
    counter = counter +1;
    if (counter > 250) {
        break; // CHARGING FAILED!
    }
}

The code was working fine when the board was running at 16MHz since the loop was fast enough to give good result. ( counter =~ 28 when the test cap is charged).

However, the intention is to run the system on a battery with clock of 1MHz (to reduce power consumption) and when tested at that speed the counter only reaches 2~3 because the loop it too slow now that the cap will charge in about two to three cycles.

So I did two changes which are using byte instead of int for the counter, and reading the value of the pin directly from the PIND register:

byte counter = 0;

// Charge the capacitor
digitalWrite(8,HIGH);

while ((PIND & (1<<PD7)) == 0) {
    counter = counter +1;
    if (counter > 250) {
        break; // CHARGING FAILED!
    }
}

Both of these changes increased the speed and now counter reads ~20 which is much better and almost close to the 16MHz.

Is there any further optimizations?

Can I use one of the general registers instead of the counter, if yes then how? and will that improve the speed?

Is there a more clever and faster way to do this loop?


  • Update:

I am going to follow the suggested answer and use the analog comparator, but I also wanted to add to my finding which might help others with general optimization.

Instead of incrementing the counter and comparing if it is larger than some value, it is faster to decrements and compare if it is equal to zero. The counter improved to ~23.

  • 1
    You can change the CPU clock prescaler in software, so you can make the CPU run at 1MHz most of of the time, but increase it to 16MHz during measurements. – Gerben Sep 1 '15 at 13:19
  • 1
    the intention is to run the system on a battery with clock of 1MHz (to reduce power consumption) - however it takes longer to do whatever-it-is you are doing. I have a page about minimizing power usage which you may find helpful. – Nick Gammon Sep 1 '15 at 21:48
  • 1
    Actually your page is what I used when doing my power optimizations. But the reason I switched to 1MHz is because I do a lot of slow UART with long delays, so I think instead of wasting 16MHz on no-op i will put it into saving energy. – Fahad Alduraibi Sep 1 '15 at 22:29
  • 1
    Also the multi-meter I have (FLUKE 179) is not good enough to measure low values in mA and for some unknown reason it damaged two atmega328p chip when i tried to program them using USBasp while the ammeter was connected in series with the power! I didn't think it was the cause at first, but after the second one died in the same way I started to believe! – Fahad Alduraibi Sep 1 '15 at 22:34
  • 2
    I haven't confirmed this on the Atmel processor but most processor have a decrement and jump if zero/not zero machine code instructions. This will typically be much faster than an increment, compare and jump if greater than/less than. I suspect this is why your optimization worked. – linhartr22 Sep 3 '15 at 19:12
2

It looks like Ignacio Vazquez-Abrams and I have been doing similar things. :)

I have a page about making a capacitor tester:

Turn your Arduino into a capacitor tester

However since link-only replies are frowned on (the link might go down) I'll summarize it here.

The simple approach is to charge the capacitor until it reaches 63.2% of the charging voltage.

That is because:

1 - e^(-1) = 0.63212

See RC Time constant - Wikipedia.

We can use the Analog Comparator to trigger an interrupt when the voltage reaches that level.

We need the capacitor to be on pin D6 on the Uno and a reference voltage on pin D7.

See Using the Arduino Analog Comparator for more details about the Analog Comparator.

This is much faster than using analogRead, because it triggers on an exact voltage.

Your basic objective is to find how long it takes to reach 63.2% of the supplied voltage (in the example being 1 volt):

Capacitor tester 1

In this particular example it took 47 µs for a 47 nF capacitor:

Capacitor tester 2

Instead of using an exact reference voltage (ie. 63.2% of 5 volts) you can do some fancy maths instead. Here is my test circuit:

Capacitor test circuit

DUT = Device Under Test

We plug the values of the resistors into the code like this:

const float Rc = 10000;    // charging resistor
const float R1 = 1000;     // between ground and D7
const float R2 = 1800;     // between +5V and D7
const float clockRate_us = 16;  // 16 MHz
const float k = 1000 / (clockRate_us * Rc * log ((R1 + R2) / R2));

Note: For high accuracy you should use the measured resistance values, not just the nominal ones.


Now with some fairly simple code we can use the constant k to work out the capacitance:

/*
Capacitance meter

Author: Nick Gammon
Date:   2 July 2013

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

Reference voltage connected to D7 (AIN1) as per below.

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

Like this:

Capacitor to test:

D2  ----> Rc ----> D6 ----> capacitor_under_test ----> Gnd

Reference voltage:

+5V ----> R2 ---> D7 ---> R1 ----> Gnd

*/

const byte pulsePin = 2;   // the pin used to pulse the capacitor
const float Rc = 10000;    // charging resistor
const float R1 = 1000;     // between ground and D7
const float R2 = 1800;     // between +5V and D7
const float clockRate_us = 16;  // 16 MHz

const float k = 1000 / (clockRate_us * Rc * log ((R1 + R2) / R2));

volatile boolean triggered;
volatile boolean active;

volatile unsigned long timerCounts;
volatile unsigned long overflowCount;

ISR (TIMER1_OVF_vect)
{
  ++overflowCount;               // count number of Counter 1 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 & bit (TOV1)) && timer1CounterValue < 0x7FFF)
      overflowCopy++;
    // calculate total count
    timerCounts = (overflowCopy << 16) + timer1CounterValue;  // each overflow is 65536 more
    triggered = true;
    digitalWrite (pulsePin, LOW);  // start discharging capacitor
    TCCR1B = 0;    // stop the timer
    }  // end if active
  }  // end of TIMER1_CAPT_vect

void setup ()
  {
  pinMode (pulsePin, OUTPUT);
  digitalWrite (pulsePin, LOW);

  Serial.begin (115200);
  Serial.println ("Started.");
  ADCSRB = 0;           // (Disable) ACME: Analog Comparator Multiplexer Enable
  ACSR = bit (ACIC);    // Analog Comparator Input Capture Enable
  DIDR1 |= bit (AIN1D) | bit (AIN0D); // Disable digital buffer on comparator inputs
  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 = bit (TOIE1) | bit (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 =  bit (CS10) | bit (ICES1);  // plus Input Capture Edge Select
  interrupts ();

  } // end of startTiming

void finishTiming ()
  {
  active = false;
  Serial.print ("Capacitance = ");
  float capacitance = (float) timerCounts * k;
  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

For extra accuracy the code uses the Input Capture Unit, which is something that remembers the exact value in Timer 1 when the match occurs. This avoids the delay of 2 to 3 µs while the ISR kicks into action.


Results

I measured a few values using a capacitance-substitution box. To check on the box I measured also with a high-precision multimeter. The value on the right is what the sketch gave (rounded).

(All values in nF)

Nominal    Meter     Sketch
Value      Measure   Measure

    40         42        42
   100         98       102
   200        202       201
   300        312       313
   400        389       391
  1000       1004      1010
  2000       2011      2012
  3000       2950      2953
  4000       4160      4170

The sketch output certainly seems to be close to the nominal and measured values.


What about using the internal voltage reference (1.1v) instead of AIN0? which one will be better as the 2xAA batteries voltage drops over time?

The voltage doesn't matter as it is not in the equation. It is the ratio that matters, and that will scale with battery drop (ie. the ratio between the two resistors will always be the same).

You need to use Vcc because it is turned on and off at an output pin (to start charging the capacitor).


Also what is the difference between TIMER1_CAPT_vect which you used here and ANALOG_COMP_vect which you used in the example in your site?

If you are not using the Input Capture Unit, then you need to get the Analog Comparator interrupt. However if you use the Input Capture Unit then you use the Timer1 Capture Event which is triggered when you get the capture.

| improve this answer | |
  • 1
    Nice! Given the level of accuracy this approach could potentially shoot for, I'd also suggest measuring the actual resistance of Rc, R1, R2 rather than using the nominal values – tardate Sep 1 '15 at 12:23
  • 1
    Thank you Nick, I just leaned about the analog comparator from you. If I want to monitor 2 caps (1 is used as reference to tackle changes in temp and other effects) and by looking at the datasheet it seems that I can use the analog inputs instead of AIN1 by changing ADEN and ADMUX, will that work? any side effects? – Fahad Alduraibi Sep 1 '15 at 19:26
  • 1
    I'd also suggest measuring the actual resistance of Rc, R1, R2 rather than using the nominal values - I'll amend that just to make sure readers realize that should be done. – Nick Gammon Sep 1 '15 at 20:55
  • 1
    it seems that I can use the analog inputs instead of AIN1 - that should work. Make sure you turn the ADC off as mentioned in the datasheet. I don't see what could go wrong. :) – Nick Gammon Sep 1 '15 at 21:31
  • What about using the internal voltage reference (1.1v) instead of AIN0? which one will be better as the 2xAA batteries voltage drops over time? – Fahad Alduraibi Sep 2 '15 at 20:17
2

Get rid of the loop altogether. Instead of twiddling and checking bits manually, use timer 1 to trigger the output and capture the input.

If you perform the output on pin 10 and the input on pin 8 then you can use timer 1's output compare to send the pulse out and its input capture to detect when the capacitor has charged.

Untested:

#include <avr/interrupt.h>

void setup(void)
{
  cli();
  DDRB |= _BV(PB2); // Configure OC1B as output
  TCCR1B &= ~(_BV(CS12) | _BV(CS11) | _BV(CS10)); // Stop timer 1
  TCNT1 = 0; // Initialize timer count
  OCR1A = 0xffff; // Reset the timer at this count
  OCR1B = 0xf000; // Upper limit of the timed value, plenty of time before timer reset
  ICR1 = 0xff00; // Set to an invalid initial value
  TCCR1A = _BV(COM1B1); // Trigger output at 0, clear at upper limit
  TCCR1B = _BV(ICES1); // Capture on leading edge
  TCCR1B |= _BV(WGM13) | _BV(WGM12); // High bits for 16-bit fast PWM
  TCCR1A |= _BV(WGM11) | _BV(WGM10); // Low bits for 16-bit fast PWM
  TIMSK = _BV(ICIE1) | _BV(OCIE1B); // Enable input capture and limit interrupts
  sei();
  TCCR1B |= _BV(CS10); // Start the timer at its highest clock speed
}

ISR(TIMER1_CAPT_vect)
{
  TIMSK &= ~(_BV(ICIE1) | _BV(OCIE1B)); // Disable input and limit interrupts
  TCCR1B &= ~(_BV(CS12) | _BV(CS11) | _BV(CS10)); // Stop the timer
  PORTB &= ~_BV(PB2); // Kill the charging output
  // Check ICR1 for the captured value
}

ISR(TIMER1_COMPB_vect)
{
  if (ICR1 == 0xff00)
  {
    // WELL CRAP, charging is taking too long
    // Maybe try with a slower clock?
  }
}

void loop()
{
  // Do other stuff
}
| improve this answer | |

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.