├── hw ├── flatmate.brd ├── flatmate.png ├── flatmate.sch ├── flatmate-long.brd ├── flatmate-long.sch └── flatmate-long-brd.png ├── Makefile ├── avr_adc.h ├── avr_adc.c ├── README.markdown └── flatmate.c /hw/flatmate.brd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixbigot/Flat-Mate/HEAD/hw/flatmate.brd -------------------------------------------------------------------------------- /hw/flatmate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixbigot/Flat-Mate/HEAD/hw/flatmate.png -------------------------------------------------------------------------------- /hw/flatmate.sch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixbigot/Flat-Mate/HEAD/hw/flatmate.sch -------------------------------------------------------------------------------- /hw/flatmate-long.brd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixbigot/Flat-Mate/HEAD/hw/flatmate-long.brd -------------------------------------------------------------------------------- /hw/flatmate-long.sch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixbigot/Flat-Mate/HEAD/hw/flatmate-long.sch -------------------------------------------------------------------------------- /hw/flatmate-long-brd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixbigot/Flat-Mate/HEAD/hw/flatmate-long-brd.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #AVRDUDE_PORT=/dev/ttyUSB0 2 | AVRDUDE_PORT=/dev/tty.usbserial-A5001s6e 3 | #MCU = atmega168 4 | MCU = attiny45 5 | #MCU = attiny25 6 | 7 | #F_CPU = 8000000 8 | F_CPU = 1000000 9 | 10 | TARGET = flatmate 11 | 12 | SRC = flatmate.c avr_adc.c 13 | 14 | include ../avr-libs/avr-tmpl.mk 15 | AVRDUDE_PORT=/dev/tty.usbserial-A5001s6e 16 | 17 | 18 | -------------------------------------------------------------------------------- /avr_adc.h: -------------------------------------------------------------------------------- 1 | #ifndef _AVR_ADC_H 2 | #define _AVR_ADC_H 3 | 4 | /* 5 | *@ ADC control constants 6 | * 7 | */ 8 | #define ADC_VREF_VCC 0 9 | #define ADC_VREF_EXT_PB0 _BV(REFS0) 10 | #define ADC_VREF_INT_1V1 _BV(REFS1) 11 | #define ADC_VREF_INT_2V56 (_BV(REFS2)|_BV(REFS1)) 12 | #define ADC_VREF_INT_2V56_BYPASS (_BV(REFS2)|_BV(REFS1)|_BV(REFS0)) 13 | 14 | #define ADC_PRESCALER_DIV2 _BV(ADPS0) 15 | #define ADC_PRESCALER_DIV4 _BV(ADPS1) 16 | #define ADC_PRESCALER_DIV8 (_BV(ADPS1)|_BV(ADPS0)) 17 | #define ADC_PRESCALER_DIV16 _BV(ADPS2) 18 | #define ADC_PRESCALER_DIV64 (_BV(ADPS2)|_BV(ADPS1)) 19 | #define ADC_PRESCALER_DIV128 (_BV(ADPS2)|_BV(ADPS1)|_BV(ADPS0)) 20 | 21 | /* 22 | *@ ADC control function prototypes 23 | * 24 | * A real simple abstraction for the Analog-to-Digital converter 25 | * 26 | * These simple functions (many of them 1-liners) may work out _fatter_ than just 27 | * inlining the bit-twiddlery, but in cases where you have the room to spare, 28 | * the code is much more maintainable. 29 | * 30 | */ 31 | extern void adc_enable(unsigned char on); 32 | extern void adc_setref(unsigned char ref); 33 | extern void adc_setch(unsigned char ch); 34 | extern void adc_setprescaler(unsigned char ps); 35 | extern void adc_intenable(unsigned char on); 36 | 37 | extern void adc_start(void); 38 | extern int adc_wait(void); 39 | extern int adc_read(void); 40 | extern int adc_poll(void); 41 | 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /avr_adc.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Quick AVR ADC primitives. 3 | * 4 | * Written for ATTINY45, not genericized yet. 5 | */ 6 | #include 7 | #include 8 | #include 9 | #include "avr_adc.h" 10 | 11 | #if !defined(__AVR_ATtiny45__) && !defined(__AVR_ATtiny25__) 12 | #error "This library only tested on Tiny45 so far" 13 | #endif 14 | 15 | /* 16 | *@ Analog to digital converter routines 17 | */ 18 | int adc_read(void) 19 | { 20 | int adc_val; 21 | 22 | // read 10-bit value from ADCH:ADCL. MUST read low-byte first 23 | adc_val = ADCL; 24 | adc_val |= (ADCH << 8); 25 | return adc_val; 26 | } 27 | 28 | void adc_start(void) 29 | { 30 | // set the 'start conversion' bit 31 | ADCSRA |= _BV(ADSC); 32 | } 33 | 34 | int adc_wait(void) 35 | { 36 | // FIXME: add a timeout failure case 37 | while (ADCSRA & _BV(ADSC)) 38 | _delay_ms(1); 39 | return 0; 40 | } 41 | 42 | int adc_poll(void) 43 | { 44 | adc_start(); 45 | adc_wait(); 46 | return adc_read(); 47 | } 48 | 49 | void adc_setref(unsigned char ref) 50 | { 51 | unsigned char admux_copy = ADMUX; 52 | 53 | // clear reference settings 54 | admux_copy &= ~(_BV(REFS1)|_BV(REFS0)|_BV(REFS2)); 55 | // apply new settings 56 | admux_copy |= ref; 57 | ADMUX = admux_copy; 58 | } 59 | 60 | void adc_setch(unsigned char ch) 61 | { 62 | unsigned char admux_copy = ADMUX; 63 | 64 | // clear channel settings in our copy of ADMUX 65 | admux_copy &= ~(_BV(MUX3)|_BV(MUX2)|_BV(MUX1)|_BV(MUX0)); 66 | 67 | // apply new settings 68 | if (ch <= 3) 69 | { 70 | // FIXME: diff ADC modes, not supported, only single-ended channels [0..3] 71 | admux_copy |= ch; 72 | } 73 | ADMUX = admux_copy; 74 | } 75 | 76 | void adc_setprescaler(unsigned char ps) 77 | { 78 | unsigned char adcsra_copy = ADCSRA; 79 | adcsra_copy &= ~(_BV(ADPS2)|_BV(ADPS1)|_BV(ADPS0)); 80 | adcsra_copy |= ps; 81 | ADCSRA = adcsra_copy; 82 | } 83 | 84 | void adc_enable(unsigned char on) 85 | { 86 | if (on) 87 | ADCSRA |= _BV(ADEN); 88 | else 89 | ADCSRA &= ~_BV(ADEN); 90 | } 91 | 92 | void adc_intenable(unsigned char on) 93 | { 94 | if (on) 95 | ADCSRA |= _BV(ADIE); 96 | else 97 | ADCSRA &= ~_BV(ADIE); 98 | } 99 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Flat Mate - a battery voltage monitor with ATTiny45 2 | =================================================== 3 | 4 | This project monitors the voltage of a battery and displays a 5 | 5-element bar-graph indicating battery charge state. Optionally 6 | MOSFET control may be used to cut off supply to protect the battery 7 | from over-discharge. 8 | 9 | See construction pictures on [my blog](http://blog.unixbigot.id.au/2011/03/flat-mate-protect-your-lipo-cells-from.html) 10 | 11 | It is particularly applicable for multi-cell Lithium-ion Polymer 12 | (LiPo) packs, which are easily ruined if over-discharged. 13 | 14 | It is intended for use in high power applications such as portable 15 | lighting, robotics and power tools where a charged SLA or LiPo battery 16 | is discharged in the presence of a human operator. The first 17 | deployment was for a high-power bicycle headlamp. Design was inspired 18 | by the simple state-of-charge indicators present on some electric 19 | bicycles. 20 | 21 | The advantage over existing inexpensive "off the shelf" protection 22 | circuits is that this circuit gives a visual indication of 23 | state-of-charge, presented as an easy-to-read bar-graph. Existing 24 | protection circuits (often designed for flashlights or model aircraft) 25 | either have no display (sudden surprise cutoff), digital voltage 26 | display (requires memorizing cutoff voltages) or audio-only alert (no 27 | visual display). 28 | 29 | An Atmel ATTiny45 microcontroller is used to sample the battery 30 | voltage and control the LED bargraph. Power for the microcontroller 31 | is derived from a 78L05 regulator connected to the main battery. 32 | Battery voltage is monitored using an analog input via a voltage 33 | divider and compared to the microcontroller's 1.1v internal voltage 34 | reference. 35 | 36 | The bargraph will show 4 LEDs when at "full charge" and 37 | 1 LED when at minimum usable voltage. When voltage is critical 38 | a 5th "alert" LED will illuminate (and output supply can be terminated). 39 | 40 | The circuit can be used purely as a voltage indicator by leaving off 41 | the optional MOSFET switch components. If a MOSFET is fitted this can 42 | be used to disconnect the main load when the battery is depleted. 43 | 44 | Hardware Details 45 | ---------------- 46 | 47 | ![schematic](hw/flatmate.png) 48 | 49 | The bargraph consists of 5 3mm LEDs (green, yellow and red) arranged 50 | GYYYR. The first four (GYYY) form the bargraph and the 5th (red) is 51 | the critical alert. The first three are connected to dedicated output 52 | pins, the final two share a single pin in a totem-pole arrangement. 53 | The fourth LED shares state with the output FET (if fitted), when this 54 | LED is lit, power is good and the FET is enabled. When this output 55 | pin is cleared, the fifth LED (the complementary red CRITICAL 56 | alert) is illuminated and the FET is disabled. 57 | 58 | As the monitor circuit is indended for use in high-drain situations 59 | (0.5--10A), the approx 20mA consumed by the monitoring circuit is 60 | considered negligible. In particular the voltage divider is not 61 | isolated from supply between samples, so presents a constant drain of 62 | around 850uA. Current through the bargraph elements is limited by a 63 | 1kΩ resistor to around 3mA per element, so the bargraph consumes up to 64 | 12mA when at FULL level. 65 | 66 | 67 | Software Details 68 | ---------------- 69 | 70 | Software is configurable for the type and number of cells. The 71 | initial application of a 3-cell (11.4v nominal) Lithium-ion Polymer 72 | (LiPo) battery will be used to describe the code. 73 | 74 | Four voltage thresholds are configured (in millivolts), depending on battery type. 75 | For a 3S LiPo these are: 76 | 77 | BMV_FULL = 12.000v 78 | BMV_GOOD = 11.000v 79 | BMV_LOW = 10.000v 80 | BMV_CRIT = 9.000v 81 | 82 | The code converts these thresholds to analog sample values at compile 83 | time, defining corresponding VL_FULL..CRIT constants. 84 | 85 | These thresholds give the following LED readouts: 86 | 87 | if >= FULL output GYYY_ 88 | if >= GOOD output _YYY_ 89 | if >= LOW output __YY_ 90 | if >= CRIT output ___Y_ 91 | if < CRIT output ____R 92 | 93 | ### Setup 94 | 95 | * Configure analog pin as input 96 | * Configure LED pins as outputs 97 | * Strobe the LEDs for 1s to see that they all work 98 | * Configure timer 1 to give 4Hz interrupts 99 | * Configure ADC for internal 1v1 reference, selecting single-ended channel 2 100 | * Set ADC clock prescaler to give 125kHz, take and discard one sample 101 | 102 | ### Main loop 103 | 104 | * do nothing, go straight to sleep mode until timer interrupt. 105 | 106 | ### Timer interrupt handler 107 | 108 | * Read battery voltage 109 | * initiate conversion 110 | * busy-wait until conversion complete 111 | * read completed value 112 | * Update sample value 113 | * add sample value to accumulator 114 | * if 4 samples are accumulated, calculate the average and reset accumulator 115 | * Update the display whenver accumulator is reset 116 | * Clear all LEDs 117 | * Light the CRITICAL LED (and kill power) if level is <=VL_CRIT 118 | * Set the bargraph appropriately if level is >VL_CRIT (see above) 119 | 120 | Since samples are accumulated at 4Hz, and each 4 samples are averaged, 121 | the outputs will be changed only once per second. Averaging is used 122 | to avoid falsely detecting a CRITICAL state when transient spikes such 123 | as motor start occur. 124 | 125 | Construction 126 | ============ 127 | 128 | The circuit was prototyped on a solderless breadboard, with battery 129 | simulated using a 24v supply and an adjustable-output regulator 130 | (LM317T). 131 | 132 | A selection of LEDs with one leg replaced by a current-limiting 133 | resistor have been made to simplify use of LEDs in breadboard 134 | prototypes. 135 | 136 | For in-circuit programming I use the tuxgraphics AVRUSB500v2, as this 137 | programmer features a 5-pin SIL programming header, which is very 138 | convenient for breadboards. 139 | 140 | Once the design was proven, a version was constructed on through-hole 141 | prototyping board with components arranged to take advantage of 142 | pin-bridging where possible. This turned out to be difficult since 143 | the chosen double-sided prototype board featured a solder mask that 144 | was *VERY* resistant to bridging (this would normally be a laudable 145 | feature). A plain copper single-sided protoboard would have actually 146 | been a better choice. 147 | 148 | The prototype has been deployed for use as a bicycle headlamp battery 149 | pack. 150 | 151 | Subsequent revisions will use ATTiny25 in SMD package, and SMD 152 | components. 153 | 154 | -------------------------------------------------------------------------------- /flatmate.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Flat Mate - Simple Li-poly battery monitor 3 | * 4 | * DEVICE: attiny45 5 | * FUSES: LHX: 0x062 0xDF 0xFF -- 1MHz RC oscillator 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "avr_adc.h" 15 | 16 | #if defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny25__) 17 | /* 18 | *@@ Pin assignments for ATtiny45 19 | * 20 | * Pins: 21 | * 22 | * LEDA - (PB0) battery level ok led (yellow /red bicolour totem-pole arrangement) 23 | * LEDB - (PB1) battery level low led (yellow) 24 | * LEDC - (PB2) battery level good led (yellow) 25 | * LEDD - (PB3) battery level full led (green) 26 | * BATI - (PB4) Battery voltage sense input 27 | * 28 | * Interrupts: 29 | * 30 | * TICK - timer tick from internal timer 1 (clk/2048) = 1MHz/2k + TOP=122 => 4.0023 Hz 31 | * ADC - ADC conversion interrupt 32 | * 33 | */ 34 | 35 | #define TICK_OCR OCR1C 36 | #define TICK_TOP 122 37 | #define TICK_CLK_DIV_1024 (_BV(CS13)|_BV(CS11)|_BV(CS10)) 38 | #define TICK_CLK_DIV_2048 (_BV(CS13)|_BV(CS12)) 39 | #define TICK_CLK_DIV_4096 (_BV(CS13)|_BV(CS12)|_BV(CS10)) 40 | #define TICK_CLK_DIV_16384 (_BV(CS13)|_BV(CS12)|_BV(CS11)|_BV(CS10)) 41 | #define TICK_CLK_MODE TICK_CLK_DIV_2048 42 | #define TICK_OVF_VECT TIM1_OVF_vect 43 | 44 | // Primary output is a "battery OK/Alert" signal on PB0 that drives 45 | // a BiColor LED and/or a MOSFET. 46 | // 47 | // Secondary output is a three-element bargraph on PB[123] 48 | // 49 | #define LED_DDR DDRB 50 | #define LED_PORT PORTB 51 | #define LED_PIN PINB 52 | #define LEDA_BIT PB0 53 | #define LEDB_BIT PB1 54 | #define LEDC_BIT PB2 55 | #define LEDD_BIT PB3 56 | 57 | #define LEDA_BV _BV(LEDA_BIT) 58 | #define LEDB_BV _BV(LEDB_BIT) 59 | #define LEDC_BV _BV(LEDC_BIT) 60 | #define LEDD_BV _BV(LEDD_BIT) 61 | #define LEDALL_BV (LEDA_BV|LEDB_BV|LEDC_BV|LEDD_BV) 62 | 63 | // battery voltage level (Vb) input uses Analog input 2 on PB4 (pin 3) 64 | // Vb/12 via voltage-divider is compared against internal 1.1V reference 65 | // (ADC unit has choice of 1.1v, 2.56v and 5v(VCC) references.) 66 | #define BATI_DDR DDRB 67 | #define BATI_PORT PORTB 68 | #define BATI_BIT PB4 69 | #define BATI_BV _BV(BATI_BIT) 70 | 71 | #else 72 | // insert constants for other supported chips here 73 | #error unsupported chip 74 | #endif 75 | 76 | /* 77 | *@ Configuration constants 78 | * 79 | * 80 | */ 81 | #define USE_TIMER 1 82 | #define USE_BARGRAPH 1 83 | #define CELL_COUNT 3 84 | 85 | /* 86 | *@@ Voltage trigger levels. 87 | * 88 | * Battery voltage is read through a voltage divider and compared to the internal voltage reference. 89 | * 90 | * If 91 | * Vin ----+ 92 | * R1 93 | * +----- Vout (BATI) 94 | * R2 95 | * | 96 | * = 97 | * . (gnd) 98 | * 99 | * Then Vout = Vin * ( R2 / (R1 + R2) ) 100 | * 101 | * ; Use this Emacs lisp function to calculate divisors 102 | * (defun rn2div (rup rdown) (/ (float rdown) (+ rup rdown))) 103 | * 104 | * 105 | * eg. R1=12k R2=1k => Vout = Vin * (1000 / (1000 + 12000)) 106 | * Vin * 0.0769 107 | * 108 | * R1=20k R2=10k => Vout = Vin * 0.3333 Ileak = 0.4mA @ 12v 109 | * R1=2k2 R2=1k => Vout = Vin * 0.3125 110 | * R1=3k3 R2=1k => Vout = Vin * 0.232 111 | * R1=3k9 R2=1k => Vout = Vin * 0.204 Ileak = 2.4mA @ 12v 112 | * R1=39k R2=10k => Vout = Vin * 0.204 Ileak = 0.24mA @ 12v 113 | * R1=4k7 R2=1k => Vout = Vin * 0.175 114 | * R1=10k R2=1k => Vout = Vin * 0.0909 Ileak = 1mA @ 12v 115 | * R1=12k R2=1k => Vout = Vin * 0.0769 Ileak = 0.92mA @ 12v 116 | * 117 | * Fully charged LiPo is 4.23v/cell, discharged is 2.7v/cell (nominal voltage 3.7v/cell) 118 | * For battery endurance, do not discharge below 3.0v/cell (aircraft users commonly use 2.9v/cell as limit) 119 | * 120 | * A 2-cell battery (nominally 7.46v) varies from 8.46v to 5.40v, with low-volt alert at 6.00v 121 | * A 3-cell battery (nominally 11.1v) thus varies from 12.9v to 8.10v, with low-volt alert at 9.00v 122 | * A 4-cell battery (nominally 14.8v) thus varies from 16.9v to 10.8v, with low-volt alert 12 12.0v 123 | * NOTE: a 4-cell battery requires a different voltage divider than 2-and-3 cells (use 15:1 not 12:1) 124 | * 125 | * 126 | *@@ Analog read values for defined voltage levels 127 | * 128 | * For a 3-cell battery, we consider 12v+ to be "full", 11v "good", 10v "low" and 9v "critical" 129 | * (BMV_foo constants are these values in millivolts) 130 | * 131 | * In AVR-worldview, we use 12:1 voltage divider and read 10-bit ADC comparisons versus AREF (1.1v) 132 | * 133 | * So 12v becomes 1.00V when divided. 134 | * Compared to 1.1v reference this gives an ADC result of 1024*(1.0/1.1) == 859 135 | * 136 | * An alternative approach is to use a smaller voltage divisor and compare 137 | * against Vcc (5.0v), but in practice a 12:1 divisor is easier to achieve 138 | * due to the standard first preference resistor value series. 139 | * 140 | * You can use these Emacs lisp defuns to calculate threshold analog values for your voltage levels 141 | * 142 | * (defun volts2int (v sf ref) (round (/ (* 1024.0 (* (float v) sf) ) (float ref)))) 143 | * (defun vlist2int (sf ref levels) (mapcar (lambda (v) (volts2int (float v) sf ref)) levels)) 144 | * eg. (volts2int 12 0.333 5.0) => 818 145 | * (vlist2int (rn2div 10000 1000) 1.1 '(12 11 10 9)) => (1016 931 846 762) 146 | * (vlist2int (rn2div 20000 10000) 5.0 '(12 11 10 9)) => (819 751 683 614) 147 | * (vlist2int (rn2div 12000 1000) 1.1 '(12 11 10 9))=> (859 788 716 644) 148 | * 149 | * for 4-cell, use a 15:1 divider 150 | * (vlist2int (rn2div 15000 1000) 1.1 '(16 14.5 13 12)) => (931 844 756 698) 151 | * 152 | * The above lines calculate the VL_* values shown below 153 | */ 154 | 155 | #if CELL_COUNT == 4 156 | /* Use a 15:1 voltage divider */ 157 | 158 | #define BMV_FULL 16000 159 | #define VL_FULL 931 160 | 161 | #define BMV_GOOD 14500 162 | #define VL_GOOD 844 163 | 164 | #define BMV_LOW 13000 165 | #define VL_LOW 756 166 | 167 | #define BMV_CRIT 12000 168 | #define VL_CRIT 698 169 | 170 | #elif CELL_COUNT == 3 171 | /* Use a 12:1 voltage divider */ 172 | 173 | #define BMV_FULL 12000 174 | #define VL_FULL 859 175 | 176 | #define BMV_GOOD 11000 177 | #define VL_GOOD 788 178 | 179 | #define BMV_LOW 10000 180 | #define VL_LOW 716 181 | 182 | #define BMV_CRIT 9000 183 | #define VL_CRIT 644 184 | 185 | #elif CELL_COUNT == 2 186 | /* Use a 12:1 voltage divider */ 187 | 188 | #define BMV_FULL 8000 189 | #define VL_FULL 573 190 | 191 | #define BMV_GOOD 7300 192 | #define VL_GOOD 523 193 | 194 | #define BMV_LOW 6650 195 | #define VL_LOW 476 196 | 197 | #define BMV_CRIT 6000 198 | #define VL_CRIT 430 199 | 200 | #else 201 | #error "Unsupported cell count" 202 | #endif 203 | 204 | 205 | /* 206 | *@ Global data 207 | * 208 | * We take 4 samples (storing results in accumulator) then take an average. 209 | * 210 | */ 211 | #define SAMPLE_SIZE 4 212 | char nsample; /* current number of samples in accumulator */ 213 | int sum; /* value of accumulator (sum of samples) */ 214 | int level; /* voltage level (mean of previous sample set) */ 215 | 216 | 217 | void delay_1s(void) 218 | { 219 | char i; 220 | for (i=0;i<100;i++) 221 | _delay_ms(10); 222 | } 223 | 224 | 225 | /* 226 | *@ Application subroutines 227 | */ 228 | void update_leds(unsigned int level) 229 | { 230 | /* 231 | * Copy current port value into temporary storage for modification. 232 | * This allows atomic update without turning off the load power. 233 | */ 234 | unsigned char leds = LED_PORT; 235 | 236 | leds &= ~LEDALL_BV; 237 | /* 238 | * The critical level LED is independent of the bargraph leds. 239 | * It's a biColor RED when crit, and YELLOW for power-on-voltage-OK 240 | * The same pin may also be connected to a FET that can turn off the load 241 | */ 242 | if (level > VL_CRIT) 243 | { 244 | /* battery is OK, yellow LED lit, FET active */ 245 | leds |= LEDA_BV; 246 | } 247 | /* otherwise battery is FLAT, red LED lit, FET off */ 248 | 249 | #if USE_BARGRAPH 250 | /* 251 | * OPTIONAL 3-element Bargraph showing state-of-charge. 252 | * 253 | * >=FULL GYYY_ 254 | * >=GOOD _YYY_ 255 | * >=LOW __YY_ 256 | * >=CRIT ___Y_ 257 | * = VL_FULL) 261 | { 262 | /* battery FULL or higher */ 263 | leds |= (LEDB_BV|LEDC_BV|LEDD_BV); 264 | } 265 | else if (level >= VL_GOOD) 266 | { 267 | /* battery GOOD or better */ 268 | leds |= (LEDB_BV|LEDC_BV); 269 | } 270 | else if (level >= VL_LOW) 271 | { 272 | /* battery better than LOW */ 273 | leds |= LEDB_BV; 274 | } 275 | /* 276 | * Between LOW and CRIT the bargraph will be dark, 277 | * only the BiColour CRIT/GOOD led will light (i.e. YELLOW=power good) 278 | */ 279 | #endif 280 | 281 | /* Write back the modified port state */ 282 | LED_PORT = leds; 283 | } 284 | 285 | void update_sample(int v) 286 | { 287 | /* 288 | * bump the global Count and Sum values, 289 | * and when we have accumulated SAMPLE_SIZE samples, compute an average 290 | */ 291 | sum += v; 292 | ++nsample; 293 | 294 | if (nsample >= SAMPLE_SIZE) 295 | { 296 | /* 297 | * we have a full set of samples, calculate mean and update display 298 | */ 299 | level = sum/nsample; 300 | update_leds(level); 301 | 302 | /* 303 | * reset accumulator 304 | */ 305 | nsample=0; 306 | sum=0; 307 | } 308 | } 309 | 310 | #if USE_TIMER 311 | /* 312 | *@ Interrupt Service routins 313 | * 314 | * At every clock interrupt we trigger an ADC input 315 | */ 316 | ISR(TICK_OVF_VECT) 317 | { 318 | /* 319 | * We got our 1/4 second interrupt 320 | * 321 | * Read the analog input and update the sample set 322 | */ 323 | update_sample(adc_poll()); 324 | } 325 | 326 | /* 327 | * Interrupt service routine for Analog-to-Digital Conversion Complete 328 | * 329 | * We only ever use the one ADC channel (channel 2, PB4). 330 | */ 331 | ISR(ADC_vect) 332 | { 333 | /* 334 | * An analog-to-digital conversion for pin BATI_BIT (PB0) has completed 335 | */ 336 | PINB|=LEDB_BV; 337 | ADCSRA|=_BV(ADIF); 338 | (void)adc_read(); 339 | } 340 | #endif 341 | 342 | /* 343 | *@ IO init 344 | */ 345 | void 346 | ioinit (void) 347 | { 348 | int i,v; 349 | unsigned char l; 350 | 351 | /* 352 | * Set up BATI pin as analog input 353 | */ 354 | BATI_DDR &= ~BATI_BV; 355 | 356 | /* 357 | * Set up all LED pins as ouputs 358 | * 359 | * For debug, cycle a pattern across all leds for 1s 360 | */ 361 | #if USE_BARGRAPH 362 | LED_DDR |= LEDALL_BV; 363 | LED_PORT &= LEDALL_BV; 364 | 365 | v=0; 366 | for (i=0; i<10; i++) { 367 | l = LED_PORT; 368 | l &= ~(LEDALL_BV); 369 | // Do not strobe the LEDA pin as this may control the load power 370 | switch(v) 371 | { 372 | case 0: 373 | l|=LEDB_BV; 374 | break; 375 | case 1: 376 | l|=LEDC_BV; 377 | break; 378 | case 2: 379 | l|=LEDD_BV; 380 | break; 381 | v=-1; 382 | } 383 | v++; 384 | if (v>2) 385 | v=0; 386 | LED_PORT = l; 387 | _delay_ms(100); 388 | } 389 | LED_PORT &= ~(LEDALL_BV); 390 | #else 391 | LED_DDR |= LEDA_BV; 392 | #endif 393 | 394 | #if USE_TIMER 395 | /* 396 | * Set up timer 1 as clock tick source 397 | * 398 | * FIXME: really could disable timer interrupts altogether and 399 | * set ADC to trigger from timer compare match 400 | */ 401 | TCCR1 |= _BV(PWM1A); /* clear timer on OCR1C match, generate overflow interrupt */ 402 | TCCR1 |= TICK_CLK_MODE; /* prescaler divider 2048 */ 403 | TICK_OCR = TICK_TOP; 404 | TIMSK |= _BV(TOIE1); /* overflow interrupt enable */ 405 | #endif 406 | 407 | /* 408 | * Set up analog-to-digital converter 409 | * 410 | * Internal 1v1 reference, using single-ended channel 2 (PB4, pin 3) 411 | */ 412 | adc_setref(ADC_VREF_INT_1V1); 413 | 414 | ADMUX|=_BV(MUX1); // ADC2(PB4) 415 | //adc_setch(2); 416 | 417 | /* 418 | * set ADC clock prescaler - we have 1MHz clock, we want 50-200kHz ADclk 419 | */ 420 | adc_setprescaler(ADC_PRESCALER_DIV8); 421 | 422 | /* 423 | * enable ADC unit by settting ADEN in ADCSRA 424 | */ 425 | adc_enable(1); 426 | 427 | /* 428 | * Perform one (polled) conversion and discard result 429 | * (datasheet says first conversion after ADC enable is less accurate than subsequent) 430 | */ 431 | (void)adc_poll(); 432 | 433 | #if 0 && USE_TIMER 434 | /* 435 | * enable ADC interrupts 436 | */ 437 | ADCSRA|=_BV(ADIF); 438 | ADCSRA&=~_BV(ADATE); 439 | ADCSRA |= _BV(ADIE); 440 | //adc_intenable(1); 441 | #endif 442 | 443 | sei (); 444 | } 445 | 446 | int 447 | main (void) 448 | { 449 | 450 | ioinit (); 451 | 452 | /* loop forever, the interrupts are doing the rest */ 453 | for (;;) 454 | { 455 | #if USE_TIMER 456 | sleep_mode(); 457 | #else 458 | /* 459 | * take an analog sample, save sample, update leds 460 | */ 461 | update_sample(adc_poll()); 462 | _delay_ms(250); 463 | #endif 464 | } 465 | 466 | return (0); 467 | } 468 | --------------------------------------------------------------------------------