Close

Reading the VCC with No GPIO! (LED debugger/UI)

A project log for ATtiny 0 Series programming on the cheap

The newer range of ATtinys can be programmed with an Arduino and Atmel Studio 7. Don't be put off if you have only used the Arduino IDE!

Simon MerrettSimon Merrett 05/19/2019 at 22:204 Comments

Intro

There's a nifty trick you can do with some Atmel Microchip parts that allows you to read the VCC voltage without any pins other than the normal power connection; VCC - or VDD as it's referred to in the datasheets) and GND. I first heard about this on @MickMake 's demo, where he follows along with the Microchip application note AN2447

This app note is generous enough to give sample code for the ATtiny817, which is close enough to bend for use in our ATtiny402 and many of the other 0 and 1 series parts. Note that there's a section in the app note which tells you what features your chip needs to have and therefore which chips can do this supply voltage reading trick with no extra pins or parts. BTW, the ATtiny402 isn't in that list on the current  Rev A version of the app note, so checking the ATtiny402 datasheet gave me the confidence that it had the right features to pull this off:

How it works. ADCs basically measure what fraction of an reference voltage (perhaps with a multiplier to scale it to the operating voltage of the chip) an input voltage is. These chips allow you to route the supply voltage to their ADC's reference and their internal bandgap voltage references to the ADC input to be measured. Then it's "multiplies and divides" to get a value from the ADC measurement results that represents the supply voltage.

So the ATtiny402 is ready to read the supply voltage. But how are we going to read the ATtiny402? I have dug around a little and I don't think the D (for debugging) in UPDI is going to be available with something as low cost as jtag2updi on an Arduino Nano for a very long time, if ever. If you have the smarts to do this though, please consider this log a challenge to your abilities and show us how to do it!

Given that we already have our ATtiny402 connected to an LED, we will use that as our user interface. We'll get it to blink the voltage to us - short blinks for whole Volts first, followed by long blinks for 1/10th Volts afterwards. Then a nice pause so we know we can stop counting!


Method

Firstly, you need to have got yourself to the point that you can open a project in Atmel Studio and have a way of uploading the compiled code to your ATtiny402. This could either be from the command line with avrdude or by adding your jtag2updi programmer to Atmel Studio and uploading from there. This is where we got up to in the last log.

A refresher on the connections:

Then we will compile and upload the following code:

/*
 * ATtiny402VoltageMeasure.c
 * This code reads the voltage on the ATtiny Vcc supply pin (Pin 1)
 * and blinks the result. No external voltage references, 
 * voltage dividers or other pins are required.
 * Created: 16/05/2019 01:30:05
 * Author : Simon
 */ 

#ifndef F_CPU
#define F_CPU 3300000UL // 20 MHz clock speed / 6 prescaler
#endif

#include <util/delay.h>
#include <avr/io.h>

/* A custom (not library) function to delay for a variable number of milliseconds*/
void delay_ms(int count){
    while(count--){                // check if greater than 0 and decrement the counter for next iteration 
    _delay_ms(1);                  // wait 1 millisecond
    }
}

/* A function to blink the LED, using the custom delay function*/
void blink(int flashes, int duration){
    while(flashes--){              // check if greater than 0 and decrement the flash counter for next iteration
    PORTA.OUTSET = PIN6_bm;        // LED off
    delay_ms(duration);            // wait for a set number of milliseconds using non-library function
    PORTA.OUTCLR = PIN6_bm;        // LED on
    _delay_ms(200);                // wait for 200 milliseconds
    }
}

int main(void)
{
/* This first step is to ensure we have correct clock settings
 * as we may have changed the clock source, frequency or prescaler 
 * in a previously uploaded program */    
    /* Set the Main clock to internal 20MHz oscillator*/
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSC20M_gc);

    /* Set the Main clock division factor to 6X and keep the Main clock prescaler enabled. */
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_6X_gc | CLKCTRL_PEN_bm);    

/* We will now set a pin as an output through the LED
 * to give a visual readout of the supply voltage */
    /* Configure Port A, Pin 6 as an output (remember to connect LED to PB6 and use a resistor in series to GND)*/
    PORTA.DIRSET = PIN6_bm;
/* The App Note AN2447 uses Atmel Start to configure Vref but we'll do it explicitly in our code*/
    VREF.CTRLA = VREF_ADC0REFSEL_1V1_gc;  /* Set the Vref to 1.1V*/

/* The following section is directly taken from Microchip App Note AN2447 page 13*/        
    ADC0.MUXPOS = ADC_MUXPOS_INTREF_gc    /* ADC internal reference, the Vbg*/;
    
    ADC0.CTRLC = ADC_PRESC_DIV4_gc        /* CLK_PER divided by 4 */
    | ADC_REFSEL_VDDREF_gc                /* Vdd (Vcc) be ADC reference */
    | 0 << ADC_SAMPCAP_bp                 /* Sample Capacitance Selection: disabled */;
    
    float Vcc_value = 0                   /* measured Vcc value */;
    ADC0.CTRLA = 1 << ADC_ENABLE_bp       /* ADC Enable: enabled */
    | 1 << ADC_FREERUN_bp                 /* ADC Free run mode: enabled */
    | ADC_RESSEL_10BIT_gc                 /* 10-bit mode */;
    ADC0.COMMAND |= 1;                    // start running ADC
    
    while(1) {
        if (ADC0.INTFLAGS)                // if an ADC result is ready
        {
            Vcc_value = ( 0x400 * 1.1 ) / ADC0.RES /* calculate the Vcc value */;

        /* This next section is NOT part of the app note example 
         * but is inserted to provide the blinking LED as the user interface. */
        int digits = (int)Vcc_value;      // turn the voltage reading into an integer
        int decimals = (int)(Vcc_value * 10) - digits * 10;    // calculate the voltage decimal value        
        blink(digits,200);                // blink the whole number of Volts fast (because there will be max 5 fast flashes)
        _delay_ms(1000);                  // wait a second to mark the transition to decimals
        blink(decimals,800);              // blink the tenths of a Volt slowly (because with upto 9 decimal values, slow is easier to count)
            _delay_ms(5000);              // wait 5 seconds so that groups of flashes representing a reading appear distinct
        }
    }
}

I won't break down every section but I will draw out a few observations for those coming from Arduino (or at least these things tripped me up in making this code):

  1. For loops don't work the same as in an Arduino sketch. As you will see in the code, a while loop does a good job of replacing it. There are other ways of looping, which you can Google for.
  2. Declare your other functions before they are called by the code. In Arduino, it allows you to pack them below the loop() but here you need them before they are called. In this case, you need to declare delay_ms() before it is called by blink(), which in turn needs to be declared before it is called by main();
  3. _delay_ms() doesn't work if you pass it a variable. That's why I had to create the similar-sounding delay_ms() function. It uses a counter to achieve very nearly the same effect as delaying for a variable number of milliseconds.
  4. Atmel Start is a feature of Atmel Studio which "is an innovative online tool for intuitive, graphical configuration of embedded software projects.", meaning that it sets parameters up for you. The datasheet example uses Atmel Start but we don't, so we needed to add the line about setting the Vref as 1.1V in our code.


Results

I measured my ATtiny402's supply voltage with a multimeter. From the 5V rail it was getting 4.96V. I got 4 short flashes and 9 long flashes. Success! I had similar results with the 3.3V supply pin from the Arduino Nano as the ATtiny402's supply. It was about 3.2V and I got 3 short and 2 long flashes. We know that the 1.1V reference in the ATtinys isn't highly accurate, so a calibration may be needed in your case. If so, just change the 1.1 multiplier in this line to something which works for you:

Vcc_value = ( 0x400 * 1.1 ) / ADC0.RES

Next...

If anyone else has any ideas about what else to do that's cool with these chips that really highlights the chip's features with minimal additional components, let me know. However, there's every chance you'll have better odds of making the demo work, so could quickly find yourself as a project contributor!

I don't have time at the moment, but a quadrature encoder or other cool thing using the custom configurable logic would be a good log for someone to do...

Discussions

Sander van de Bor wrote 7 days ago point

I really like this feature, and I have been using the same method with the ATtiny84a in Arduino. I had a hard time getting this to work in Arduino, but able to use the standard Vref (0.55V) seems to work:

  ADC0.MUXPOS = ADC_MUXPOS_INTREF_gc;
  /* Start conversion */
  ADC0.COMMAND = ADC_STCONV_bm;
  /* Wait for result ready */
  while(!(ADC0.INTFLAGS & ADC_RESRDY_bm));
  float voltage = (1024 * 0.55) /ADC0.RES;

You can run this in the loop(), or make a function out of it which will return the voltage value.

  Are you sure? yes | no

Simon Merrett wrote 7 days ago point

  Are you sure? yes | no

Ken Yap wrote 05/19/2019 at 23:52 point

Nice hack!

  Are you sure? yes | no

Simon Merrett wrote 05/20/2019 at 06:00 point

Thanks @Ken Yap

  Are you sure? yes | no