Simple circuit for lithium battery charge meter

I wanted a simple meter using 4 LEDs to display the charge of a lithium battery.  My project used a single battery(1S), so it needed to display voltages between 3 - 4.2 Volts.  If you need to measure multiple batteries in seris you should be able to modify the circuit by changing the voltage divider resistors, and regulate the voltage to the MCU.  I used an AVR ATTINY26L because I had them, but you can use any MCU that can handle a supply voltage from a little less than 3 volts to 4.5V.

Components

  • 1x Atmel ATTINY26L
  • 4x LEDs or LED array
  • 2x 1k resistors (for resistive divider to ADC)
  • 4x resistors for LEDs. See LED resistor calculator
  • 1x 0.1uF to 1uF larger capacitor (for bypass capacitor)
  • 1x push button switch (optional)

Circuit

Schematic

 

 

STK-500 configuration for programing

  • ISP6PIN to SPROG1
  • PORTE RST to PORTB PB7
  • PORTE XT1 to PORTB PB4
  • ATTINY26L in socket SCKT3700A1
  • PORTB PB0 to LED0
  • PORTB PB1 to LED1
  • PORTB PB2 to LED2
  • PORTB PB3 to LED3
  • PORTA PA0 to Vin

 

Fuses

Oddly internal oscillator was enabled already on my MCU, but you may need to enable it.

  • default for avrdude is "safemode: Fuses OK (E:FF, H:F7, L:E1)"
  • Set low fuse bits to 0xE1 for internal oscillator
  • avrdude commandline flags "-U lfuse:w:0xE1:m"
  • See http://eleccelerator.com/fusecalc/

 

Problems with circuit

As the battery voltage decreases the brightness decreases. To fix this you could enable Vref on the MCU.  Attach the LED + Resistors between Vref and the PORTB pins, then invert variables in the code.  I probable whould have done this.

Download code, Makefile, schematic, etc


References

 

 

 

Code

/*
 * Lithium battery level meter
 *
 * Created: ZyMOS 04/2020
 *
 * Microcontroller
 *     ATTiny26
 *
 * Resistor divider to ADC Channel 0
 *    R1/R2 = 1/2
 *
 * Internal RC Oscilator 1MHz
 *    Fuse bits
 *         CLKSEL[3:0] = 0001
 *        PLLCK = 1 ?
 *        Note: For all fuses “1” means unprogrammed while “0” means programmed.
 *
 * Leds output
 *     PB0-PB3
 *
 */


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

// Global
uint16_t battery_level;

#define std_delay 35
#define F_CPU 12000000UL

// ADC
void adc_init(void){

    ADMUX |=     (0 << ADLAR)| // Set left shift result
                (1 << REFS1)| // Set Vref to internal (2.56V), pin out not connected
                (0 << REFS0); // Set Vref to internal (2.56V), pin out not connected
        // Int Ref, pin not connected

    ADCSR |=     (1 << ADEN)| // Enable ADC
                (1 << ADPS2)| // Set prescaler to 128 (111)
                (1 << ADPS1)| // Set prescaler to  128 (111)
                (1 << ADPS0);  // Set prescaler to  128 (111)
}


// ADC read
uint16_t adc_read(uint8_t ch)
{
   // ADC = Vin/Vref * 1024

    //Select ADC Channel ch must be 0-7
   /* ch=0b00001001; */
       ADMUX &= 0xF0;
    ADMUX|=ch;

   //Start Single conversion
   ADCSR |= (1<<ADSC);

   //Wait for conversion to complete
   while(!(ADCSR & (1<<ADIF)));

   //Clear ADIF by writing one to it
   ADCSR |= (1<<ADIF);


   return(ADC);
}


// ADC average
uint16_t adc_average(uint8_t adc_channel) {
    uint16_t result=0;
    uint16_t result2=0;

        uint8_t adc_samples=50;    //times 2
        uint8_t adc_delay=50; //us
        uint8_t z;    

        // average first half
        for(z=0;z<adc_samples;z++){
             result = result + adc_read(adc_channel); // Read Analog value from channel-0
            _delay_us(adc_delay);
        }
        result = result / adc_samples;

        // average second half
        for(z=0;z<adc_samples;z++){
             result2 = result2 + adc_read(adc_channel); // Read Analog value from channel-0
            _delay_us(adc_delay);
        }
        result2 = result2 / adc_samples;

        // average 2 halfs
        result = result2 + result;
        result =  result / 2;

        return(result);

}

/* void set_battery_leds(uint16_t battery_level){ */
    /* PORTB &= 0b11110000; */
    /* if(battery_level > 100){ */
        /* PORTB |= 0b00010000; */
    /* } */
/* } */


void led_test(void){
    PORTB = 0b00000000;
    _delay_ms(std_delay);
    PORTB = 0b00001000;
    _delay_ms(std_delay);
    PORTB = 0b00000100;
    _delay_ms(std_delay);
    PORTB = 0b00000010;
    _delay_ms(std_delay);
    PORTB = 0b00000001;
    _delay_ms(std_delay);
    PORTB = 0b00000010;
    _delay_ms(std_delay);
    PORTB = 0b00000100;
    _delay_ms(std_delay);
    PORTB = 0b00001000;
    _delay_ms(std_delay);
    PORTB = 0b00000000;
    _delay_ms(std_delay);


}

void display_led_value(uint16_t value){
    
    /* Value = Rgain * Vbatt/Vref + 1024 */

    // Full = 4.2
    // Empty = 3
    // Resistor gain = 1/2
    // LEDs = 0 : <3.1V      :         value <= 620
    // LEDs = 1 : 3.1 - 3.3V : 620 < value <= 660
    // LEDs = 2 : 3.3 - 3.6V : 660 < value <= 720
    // LEDs = 3 : 3.6 - 3.9V : 720 < value <= 780
    // LEDs = 4 : >3.9V      : 780 < value

    // 2.19V = 780 (876)
    // 2.012 = 720 (804)
    // 1.855 = 660 (742)
    // 1.741 = 620 (696)
    
    /* Theoretical */
    /* uint16_t value0=620; */
    /* uint16_t value1=660; */
    /* uint16_t value2=720; */
    /* uint16_t value3=780; */

    /* Actual */
    uint16_t value0=560;
    uint16_t value1=594;
    uint16_t value2=630;
    uint16_t value3=702;

    _delay_ms(std_delay*10);
    
    if(value <= value0){
        PORTB = 0b00000000;
        _delay_ms(std_delay);

    }else if(value > value0 && value <= value1){
        PORTB = 0b00000000;
        _delay_ms(std_delay);
        PORTB = 0b00001000;
        _delay_ms(std_delay);

    }else if(value > value1 && value <= value2){
        PORTB = 0b00000000;
        _delay_ms(std_delay);
        PORTB = 0b00001000;
        _delay_ms(std_delay);
        PORTB = 0b00001100;
        _delay_ms(std_delay);

    }else if(value > value2 && value <= value3 ){
        PORTB = 0b00000000;
        _delay_ms(std_delay);
        PORTB = 0b00001000;
        _delay_ms(std_delay);
        PORTB = 0b00001100;
        _delay_ms(std_delay);
        PORTB = 0b00001110;
        _delay_ms(std_delay);

    }else if(value > value3){
        /* PORTB = 0b00000000; */
        /* _delay_ms(std_delay); */
        /* PORTB = 0b00001000; */
        /* _delay_ms(std_delay); */
        /* PORTB = 0b00001100; */
        /* _delay_ms(std_delay); */
        /* PORTB = 0b00001110; */
        /* _delay_ms(std_delay); */
        PORTB = 0b00001111;
        _delay_ms(std_delay);

    }

    /* _delay_ms(std_delay); */


}


int main(void)
{

    /* DDRA &= ~(1 << DDA7);  // PD0 is now an input */
    /* PORTA |= (1 << PORTA7);   // turn On the Pull-up enabled */
    // port A7, input with pullup resistor

    /* std_delay=50 */

    /* uint8_t stabilize_count=0; */
    /* uint8_t stabilize_delay=5; */
    
    uint8_t adc_channel = 0;

    /* initialize display, cursor off */
    adc_init();


    // Set led outputs
    DDRB |= (1 << DDB0) | (1 << DDB1) | (1 << DDB2)| (1 << DDB3);
    
    // startup delay
    _delay_ms(std_delay);
    
    // Startup led display
    led_test();

    while(1) {

        /* uint16_t x;         */
        /* for ( x = 0; x < 600; x++ ) { //loop 60 sec */

            // loop delay
            /* _delay_ms(std_delay); */
        
            // get the battery level from adc
            battery_level = adc_average(adc_channel);            
            
            display_led_value(battery_level);
            /* set_battery_leds(battery_level); */

        /* } //loop once a min */


    } // infinite loop
} //main