Wren'sTech

Projects and Technical Ramblings

Measure ALL the things

Leave a comment

My multimeter has been all nicely soldered together for a while now, but it didn’t actually do anything. Doing something is obviously an important criterion for actually being a multimeter, so this was an issue that I needed to address.

I need to measure voltage, current, capacitance and resistance, over large ranges and with reasonable precision. I’m doing all of this with the AVR’s 10 bit successive-approximation analogue-to-digital converter (ADC), although I’m getting 12 bits of precision out of it by oversampling.

I’ll post the schematic here – you might want to keep it open in another tab, because I’ll refer back to it. A lot.

Voltage

Voltage was the first on the chopping block. To get a decent range, I used the battery supply voltage as the reference for the ADC; this means that the converter measures the scaled voltage as a fraction of the battery voltage, between 0 and 100%.

The problem with this is that the battery voltage is by nature not constant! I needed to measure the battery voltage. To do this, I delved deep into the ancient and terrible tome that is the AVR datasheet.

Pictured: the first copy of the AVR datasheet. Printed on human skin, blood for ink

The ATmega32PA (the microcontroller on my meter) has a rarely-used but very useful feature: an internal bandgap voltage reference. In principal it’s similar to an LED and a current source of a few microamps. What this does is give me a known voltage of 1.1V: if I use the ADC to measure this reference, as a fraction of the battery voltage, I can then calculate the battery voltage. This gives me a transfer standard with which to measure everything else.

Reading the datasheet (page 255) I eventually came up with the magical incantation which makes this measurement work:

#define VIN_BANDGAP 0x1e
uint16_t get_adc_10bit_fast(uint8_t pin)
{
    ADMUX = 0x40 | pin; // AREF = VCC
    ADCSRA = 0x87;      // Enabled, clk/64 = 125kHz
    ADCSRA |= 0x40;
    while (ADCSRA & 0x40);
    uint8_t result = ADCL;
    return result | (((uint16_t)ADCH) << 8);
}

uint16_t get_battery_voltage()
{
    ADMUX = 0x5e;   // AREF = VCC, input = 1.1v bandgap reference
    ADCSRA = 0x86;  // ADC enabled, don't start conversion yet, auto trigger off, no interrupt, ck/128 = 62.5 kHz
    _delay_us(500);
    uint32_t result = 0;
    for (int i = 16; i; --i)
    {
        result += get_adc_10bit_fast(VIN_BANDGAP);
    }
    return 4538487 / result;   // = 1024 (number of ADC steps) * 1.1 (bandgap voltage) * 16 (number of readings) * 256 (fxp88 fixed point)
}

Taking 16 readings and averaging them increases the effective number of bits by two, because it increases the signal-to-noise ratio by four. (See: Wikipedia)

We then divide a big integer constant by this sum, and the result is the battery voltage in an 8.8 fixed point format. The exact value of this constant is not quite the one described in the comments: it varies slightly with each chip, and was derived by cross-referencing the value from this function with an actual measurement of the battery voltage. Sorted.

The 0.5ms delay is to allow the bandgap reference (which has a very high output impedance) to charge the AVR’s internal capacitance to the correct value – not giving it time to settle led to a very peculiar problem. More on this later!

We can now actually measure the voltage.

 

#define PORT_VATTEN PORTA
#define DDR_VATTEN DDRA
#define VATTEN_1M 0x20
#define VATTEN_100k 0x10
#define VATTEN_5k 0x04
#define VATTEN_ALL (VATTEN_1M | VATTEN_100k | VATTEN_5k)

#define VIN_V 6

uint16_t get_adc_12bit(uint8_t pin)
{
    uint16_t result = 0;
    for (uint8_t i = 16; i; --i)
    {
        result += get_adc_10bit_fast(pin);
    }
    return result >> 2;
}

uint8_t voltage_range = 0;
void set_voltage_range(uint8_t range)
{
    uint8_t range_attens[] = {VATTEN_1M, VATTEN_100k, VATTEN_5k};
    DDR_VATTEN |= VATTEN_ALL;
    PORT_VATTEN &= ~VATTEN_ALL;
    PORT_VATTEN |= range_attens[range];
}

float get_voltage()
{
    set_voltage_range(voltage_range);
    uint16_t lower_limits[] = {0, 151, 49};
    uint16_t upper_limits[] = {941, 805, 1024};
    uint16_t multipliers[] = {2, 11, 201};

    uint16_t result = get_adc_10bit_fast(VIN_V);
    if (result < lower_limits[voltage_range])
        voltage_range--;
    else if (result > upper_limits[voltage_range])
        voltage_range++;

    _delay_us(10);

    uint32_t reading = get_adc_12bit(VIN_V);
    uint32_t battery = get_battery_voltage();
    reading = (reading * multipliers[voltage_range] * battery);
    reading >>= 7;
    return reading * (1/16384.f);
}

get_adc_12bit() is a quick-and-easy function which gets a 12-bit reading from the AVR’s 10-bit ADC by averaging 16 readings – see Atmel’s app note on oversampling and decimation.

set_voltage_range() is for controlling the attenuation circuitry at the front end of the multimeter – see the upper left of the schematic. All voltage measurements go through a fixed-gain op amp circuit, but switching the MOSFETs allows the voltage to be attenuated first by different voltage dividers, so large voltages can be scaled down in order to measure  them.

get_voltage() needs only to select the appropriate range and then scale the 12-bit reading to get the final voltage. This was originally all fixed-point maths, which worked perfectly, but I gave in and used floats because it’s just easier.

I stuck a 1602 display and a shift register onto the logic analyser header (which should be input but I’m using it as an output for now) so that I can have some form of readout until the proper screens arrive. Here I’m measuring the 32-ish volt output from my power supply:

Looks good! There is a problem though:

Voltage seems to drop off significantly towards the upper end of each range (the meter clicks up a range at about 3.8 volts). Due to the very high impedance of the attenuator circuitry, I initially assumed this was due to leakage currents in the MOSFETs, and was dreaming up all sorts of compensation schemes. As it turned out, it was due to this line I mentioned earlier in the battery voltage measurement:

_delay_us(500);

Without this line (or when the delay is smaller, on the order of 10us) we have the problem above; with it, we do not. When the ADC input is switched from the voltage input (at a higher voltage) to the reference, if it is not given time to settle then the reference will read as being a higher voltage than it is, and so the calculated battery voltage will be smaller. The measured voltage will be scaled down accordingly. This effect becomes more pronounced at higher voltages, which explains the downward nosedive seen above.

Current

Current measurement was much more straightforward:

void switch_current(bool onoff)
{
    DDR_IGATE |= IGATE;
    if (onoff)
        PORT_IGATE |= IGATE;
    else
        PORT_IGATE &= ~IGATE;
}

float get_current()
{
    switch_current(CURRENT_ON);
    _delay_us(50);
    uint32_t reading = get_adc_12bit(VIN_I);
    reading = (reading * 19 * get_battery_voltage()) / 46;
    reading >>= 6;
    return reading * (1/16384.f);
}

Again, this was originally all fixed point maths, but I converted to a float at the end. I’m after peace of mind, not efficiency!

The meter can measure up to about 1.7A, and will happily take 1 amp continuously with little or no heating effects.

The Megameter agrees with my Extech EX330 to within 1% across its range, and measures with a resolution (and pretty much an accuracy) of 1mA, which is “good enough”.

In between these two photos I had an accident which involved having to reflow, remove and replace soldered SMD parts with a frying pan – hence the missing buttons!

The measurement circuitry consists of nothing more than a 0.22ohm shunt resistor, the beefiest MOSFET I have ever seen in a SOT23 package (put in as many vias as I could so that I could use the planes as a heatsink), and a fixed 11x gain op amp circuit. Does the job!

Capacitance

The final measurement which I have tackled so far is capacitance. The meter has a useful range of 1nF -> >600uF, which is far better than my EX330, as well as being about as accurate. It’s based on the well-known principle of RC discharge curves – by discharging the capacitor, charging it through one of a range of resistors (for various ranges of capacitance value), and then timing how long it takes to reach a certain threshold voltage, it’s possible to calculate the capacitance. I derived this formula:

 

C = \frac{-t}{R \ln(1 - \frac{V_{trig}}{V_{cc}})}

 

Vtrig is the threshold voltage at which the capacitor stops charging. To give this a known value, I used the internal bandgap reference, and the AVR’s analogue comparator to detect when the capacitor reaches this voltage. This involved yet more datasheet black magic:

inline void set_rc_range(uint8_t range)
{
    uint8_t ranges[] = {RC_LOW, RC_MID, RC_HI};
    DDR_RC &= ~(RC_LOW | RC_MID | RC_HI);
    PORT_RC &= ~(RC_LOW | RC_MID | RC_HI);
    DDR_RC |= ranges[range];
    PORT_RC |= ranges[range];
}

uint8_t cap_range = 2;
volatile uint8_t cap_aco_mask = 0x20;

// Timer 1 overflow vector: stop the timer, set its result back to a large value,
// jump up a range, and force the wait loop to exit.
ISR(TIMER1_OVF_vect) {
    TCCR1B = 0x00;
    cap_aco_mask = 0x00;
    cap_range--;
    TCNT1 = 0xfff0;
}

void choose_cap_range(float result)
{
    // all values are in microfarads
    switch(cap_range)
    {
    case 0:
        cap_range++;
        break;
    case 1:
        if (result < 5.f)
            cap_range++;
        break;
    case 2:
        if (result > 6.f)
            cap_range--;
        else if (result < 0.5f)
            cap_range++;
        break;
    case 3:
        if (result > 0.6f)
            cap_range--;
        break;
    }
}

float get_capacitance()
{
    uint16_t resistances[] = {47, 470, 20000, 20000};
    uint8_t divisors[] = {1, 1, 1, 8};          // compensates for timer clock frequency differences
    uint8_t tccrs[] = {0x02, 0x02, 0x02, 0x01}; // selects the clock frequency input to the timer
    uint8_t subtractors[] = {1, 1, 1, 10};      // compensates for code execution time

    // let the RC_SENSE net float by switching off the current pass MOSFET
    switch_current(CURRENT_OFF);

    // Get values ready to plug into registers (timing is critical later)
    uint8_t rc_range = cap_range;
    if (rc_range > 2)
        rc_range = 2;
    uint8_t tccr_start = tccrs[cap_range];
    cap_aco_mask = 0x20;

    // Switch off the ADC so we can use its multiplexer, and set up the comparator:
    ADCSRA = 0x00;  // ADC off
    ADCSRB = 0x40;  // ACME = 1 (connect ADC multiplexer to the comparator)
    ADMUX = VIN_RC; // RC pin connected to comparator negative input
    ACSR = 0x40;    // 1.1v bandgap reference connected to comparator positive input

    // connect RC -> 47 ohms -> ground, float other pins, then wait 50ms for discharge
    set_rc_range(0);
    PORT_RC &= ~RC_LOW;
    DDR_RCSENSE &= ~RC_SENSE;
    PORT_RCSENSE &= ~RC_SENSE;
    _delay_ms(50);

    // set up timer1
    TCCR1A = 0x00;
    TCCR1B = 0x00;
    TCNT1 = 0;
    TIMSK1 = 0x01; // overflow interrupt enabled
    sei();

    // connect rc -> resistance -> vcc and start timer.
    set_rc_range(rc_range);
    TCCR1B = tccr_start;

    // exits when comparator outputs 0 (voltage has reached 1.1v) or the timer overflow interrupt has fired
    while (ACSR & cap_aco_mask);
    // stop timer and float pins
    TCCR1B = 0x00;
    DDR_RC |= RC_LOW;
    PORT_RC &= ~RC_LOW;
    // C = -t / (R * ln(1 - Vtrig / Vcc)
    float result = -((float)(TCNT1 - subtractors[cap_range]) / divisors[cap_range]) / (log(1 - 1.03f / (get_battery_voltage() * (1/256.f))) * (float)resistances[cap_range]);
    choose_cap_range(result);
    return result;
}

I initially had trouble making the comparator code work. I was doing this in the wait loop, to wait until the analogue comparator output (ACO) went low: (capacitor is connected to the inverting input)

 

while (ACSR & ACO);

 

The “gotcha” here is that the constant ACO, defined by avr/io.h, is the bit position (5), not the bit value (0x20). The idiomatic way of doing this is to write

 

while (ACSR & (1 << ACO));

 

But this looks stupid, so I just used the hex constant (in a named variable, whose purpose will be explained shortly).

The only other complicating factor is that of timer overflows. The timer is a 16 bit counter, clocked at either 1 or 8MHz depending on range, so it will overflow after either 65ms or 8ms. You could detect this overflow by doing a comparison against a value suitably lower than 0xffff in the wait loop, but every single instruction in this loop reduces the precision of the measurement.

The comparison can be made faster by comparing only the high byte, but this still reduces the precision. If only a processor had a way of handling asynchronous events separate to the main flow of execution… wait.

The timer is set up to trigger an interrupt when it overflows. The interrupt itself is handled by separate silicon, and does not slow down execution until it is actually triggered, at which point execution jumps to the service routine.

The interrupt stops the timer, changes the ACO mask to zero (which forces the while loop to exit), sets the count back to a very large value, and bumps the capacitance range up by one.

I stuck all of this together into a main function that lets me choose measurement by clicking on the buttons.

Up next: resistance measurement, SPI flash and TV remote!

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s