-- ------------------------------------------------------------------------ -- Title: Simple ESR Meter v3.1 -- -- Copyright (c) 2009 Kinsa Manka -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see . -- -- Compiler: jal 2.4l -- -- Description: -- Digital version of an analog ESR meter -- -- Sources: -- http://members.shaw.ca/swstuff/esrmeter.html -- -- Changes: -- - removed VDD Self Starting Circuit -- - removed battery monitoring -- - included a simple calibration routine -- - uses PIC16F977 -- -- ------------------------------------------------------------------------ pragma task 4 include 16f877 pragma target clock 8_000_000 pragma target OSC HS pragma target WDT disabled pragma target PWRTE disabled pragma target CP disabled pragma target WRT all_protected pragma target BROWNOUT enabled pragma target LVP disabled pragma target CPD disabled enable_digital_io() include delay -- setup pins ; led display pin_B0_direction = input pin_B1_direction = input pin_B2_direction = input pin_B3_direction = input pin_B4_direction = input pin_B6_direction = input pin_B7_direction = input pin_D6_direction = input pin_B5_direction = output pin_D5_direction = output pin_D7_direction = output ; ADC pin_AN0_direction = input ; esr signal pin_D4_direction = input ; button -- aliases var bit disp_a is pin_B2_direction var bit disp_b is pin_B1_direction var bit disp_c is pin_B4_direction var bit disp_d is pin_B6_direction var bit disp_e is pin_B7_direction var bit disp_f is pin_B0_direction var bit disp_g is pin_D6_direction var bit disp_dp is pin_B3_direction var bit disp_1 is pin_D7 var bit disp_2 is pin_B5 var bit disp_3 is pin_D5 var bit button is pin_D4 -- initialize pins PORTA = 0 PORTB = 0 PORTD = 0 -- setup PWM procedure setup_PWM() is pin_CCP1_direction = output ; max resolution PR2 = 0xFF T2CON = 0b_0000_0100 ; set frequency ~100khz PR2 = byte(20) - 1 var byte t2con_tmp = T2CON & 0b_1111_1000 T2CON = t2con_tmp | 0b_0000_0100 ; set dutycycle CCPR1L = 18 ; 18 counts on, 1 count off ; start PWM CCP1CON = 0b_0000_1100 end procedure -- LED display routines const byte SSEG[] = { 0b00111111, -- 0, LED Segment: A,B,C,D,E,F 0b00000110, -- 1, LED Segment: B,C 0b01011011, -- 2, LED Segment: A,B,D,E,G 0b01001111, -- 3, LED Segment: A,B,C,D,G 0b01100110, -- 4, LED Segment: B,C,F,G 0b01101101, -- 5, LED Segment: A,C,D,F,G 0b01111101, -- 6, LED Segment: A,C,D,E,F,G 0b00000111, -- 7, LED Segment: A,B,C 0b01111111, -- 8, LED Segment: A,B,C,D,E,F,G 0b01101111, -- 9, LED Segment: A,B,C,D,F,G 0b01110111, -- a, LED Segment: A,B,C,E,F,G 0b01111100, -- b, LED Segment: C,D,E,F,G 0b00111001, -- c, LED Segment: A,D,E,F 0b01011110, -- d, LED Segment: B,C,D,E,G 0b01111001, -- e, LED Segment: A,D,E,F,G 0b01110001 -- f LED Segment: A,E,F,G } var volatile byte disp_dig[3] = { 0,0,0 } var bit disp_clr = true var bit disp_dot0 at disp_dig[0]:7 var bit disp_dot1 at disp_dig[1]:7 var bit disp_dot2 at disp_dig[2]:7 procedure disp_err() is disp_dig[0] = 0b01010000 disp_dig[1] = 0b01010000 disp_dig[2] = 0b01111001 end procedure procedure disp_ovr() is disp_dig[0] = 0b00111000 disp_dig[1] = 0b10111111 disp_dig[2] = 0b00000000 end procedure procedure disp_cal() is disp_dig[0] = 0b00111000 disp_dig[1] = 0b01110111 disp_dig[2] = 0b00111001 end procedure procedure disp_done() is disp_dig[0] = 0b01111001 disp_dig[1] = 0b01010100 disp_dig[2] = 0b01011110 end procedure ; print 3 decimal digits procedure disp_dec(word in data) is var word x ; x.xx if (data <= 999) then disp_dig[2] = SSEG[data / 100] x = data % 100 disp_dig[1] = SSEG[x / 10] x = x % 10 disp_dig[0] = SSEG[x] disp_dot2 = true ; xx.x elsif (data <= 9999) then disp_dig[2] = SSEG[data / 1000] x = data % 1000 disp_dig[1] = SSEG[x / 100] x = x % 100 disp_dig[0] = SSEG[x / 10] disp_dot1 = true ; xxx. else disp_dig[2] = SSEG[data / 10000] x = data % 10000 disp_dig[1] = SSEG[x / 1000] x = x % 1000 disp_dig[0] = SSEG[x / 100] disp_dot0 = true end if end procedure ; print the 3 left-most digits procedure disp_hex(word in data) is var byte x[2] at data disp_dig[0] = SSEG[ x[0] & 0xf ] disp_dig[1] = SSEG[ x[0] >> 4 ] disp_dig[2] = SSEG[ x[1] & 0xf ] end procedure var volatile bit disp_blink = false var byte disp_blink_cnt = 0 ; display is multiplexed by sequencing on the individual segments instead of ; per digit in order to reduce the parts count procedure multiplex_disp( byte in seg ) is -- blank display disp_a = input disp_b = input disp_c = input disp_d = input disp_e = input disp_f = input disp_g = input disp_dp = input if (disp_blink) then disp_blink_cnt = disp_blink_cnt + 1 if (disp_blink_cnt < 128) then return end if end if -- multiplex segments case seg of 0 : block disp_a = output disp_1 = disp_dig[0] & 0b00000001 disp_2 = disp_dig[1] & 0b00000001 disp_3 = disp_dig[2] & 0b00000001 end block 1 : block disp_b = output disp_1 = disp_dig[0] & 0b00000010 disp_2 = disp_dig[1] & 0b00000010 disp_3 = disp_dig[2] & 0b00000010 end block 2 : block disp_c = output disp_1 = disp_dig[0] & 0b00000100 disp_2 = disp_dig[1] & 0b00000100 disp_3 = disp_dig[2] & 0b00000100 end block 3 : block disp_d = output disp_1 = disp_dig[0] & 0b00001000 disp_2 = disp_dig[1] & 0b00001000 disp_3 = disp_dig[2] & 0b00001000 end block 4 : block disp_e = output disp_1 = disp_dig[0] & 0b00010000 disp_2 = disp_dig[1] & 0b00010000 disp_3 = disp_dig[2] & 0b00010000 end block 5 : block disp_f = output disp_1 = disp_dig[0] & 0b00100000 disp_2 = disp_dig[1] & 0b00100000 disp_3 = disp_dig[2] & 0b00100000 end block 6 : block disp_g = output disp_1 = disp_dig[0] & 0b01000000 disp_2 = disp_dig[1] & 0b01000000 disp_3 = disp_dig[2] & 0b01000000 end block 7 : block disp_dp = output disp_1 = disp_dig[0] & 0b10000000 disp_2 = disp_dig[1] & 0b10000000 disp_3 = disp_dig[2] & 0b10000000 end block end case end procedure -- display loop -- helper routine for updating the display task display() is var bit disp_start = on var byte disp_seg = 0 var bit disp_wait var bit TMR1H_0x01 at TMR1H:0 forever loop if disp_start then multiplex_disp(disp_seg) disp_seg = disp_seg + 1 if disp_seg == 8 then disp_seg = 0 end if ; start timer disp_start = off else ; wait for ~1ms if ( disp_wait ^ TMR1H_0x01) then disp_wait = TMR1H_0x01 disp_start = on end if end if suspend end loop end task -- non-blocking ADC routines -- setup ADC procedure setup_ADC() is ; set analog pin 0, ESR signal ADCON1_PCFG = 0b1110 ; VRef = Vdd ; right justified ADCON1_ADFM = true ; select conversion clock, Tad = 32 TOSC @8MHz = 4.0 usec ADCON0_ADCS = 0b10 ; Fosc/32 ; turn on A/D module ADCON0_ADON = true end procedure var volatile dword ADC_sum = 0 var volatile word ADC_cnt = 0 const byte ADC_DELAY = 3 ; in 10us, 2*Tad + conversion delay (19.72usec) -- helper routine for reading the ADC task read_ADC() is var word x var byte ax[2] at x forever loop if (! ADCON0_NDONE) then ; store results ax[0] = ADRESL ax[1] = ADRESH ADC_sum = ADC_sum + x ADC_cnt = ADC_cnt + 1 ADCON0_CHS = 0b_000 ; AN0 ; wait 2*Tad + conversion delay, ~30 usec delay_10us(ADC_DELAY) ; start cycle again ADCON0_GO = true else ; wait until conversion is done suspend end if end loop end task const byte FILTER_SHIFT = 4 ; Parameter K var dword filter_reg = 0 ; Delay element ; refer to EDN article at http://www.edn.com/article/CA6335310.html function low_pass_filter( word in inp ) return word is ; Update filter with current sample. filter_reg = filter_reg - (filter_reg >> FILTER_SHIFT) + inp ; Scale output for unity gain. return word (filter_reg >> FILTER_SHIFT) end function var volatile byte update_counter = 0, ; display update counter long_press_counter = 0, ; button press counter button_cnt = 0 var volatile bit debounced_button = true, button_tf = false, button_released = false, button_pressed = false, button_long_pressed = false, counter_flag = false const byte LONG_PRESS_TIMEOUT = 15 ; ~ 2s procedure update_timers() is var bit TMR1H_0x80 at TMR1H:7 ; update timers if ( counter_flag ^ TMR1H_0x80) then ; updated ~ 130 ms counter_flag = TMR1H_0x80 update_counter = update_counter + 1 long_press_counter = long_press_counter + 1 end if end procedure procedure button_debounce() is var bit TMR1H_0x01 at TMR1H:0 if ( button_tf ^ ( TMR1H_0x01 )) then button_tf = TMR1H_0x01 if (debounced_button ^ button) then button_cnt = button_cnt + 1 if (button_cnt > 4) then debounced_button = button button_cnt = 0 end if else button_cnt = 0 end if end if end procedure procedure check_button() is button_debounce() if (! debounced_button) then button_released = false button_pressed = true if (long_press_counter > LONG_PRESS_TIMEOUT) then button_long_pressed = true end if else long_press_counter = 0 end if if (button_pressed & debounced_button) then button_released = true button_pressed = false button_long_pressed = false end if end procedure task timer_and_button() is forever loop update_timers() check_button() suspend end loop end task var bit new_data = false var word ADC_val = 0 procedure oversample_ADC() is ; oversample ADC, effective ADC resolution = 13 bits ; refer to atmel application note AVR121 if (ADC_cnt == 256) then var dword z var word y[2] at z z = low_pass_filter( ADC_sum >> 5 ) ; use 13 bits ADC_cnt = 0 ADC_sum = 0 ; add hysteresis, ignore changes of 1 digit if ((ADC_val < y[0]) | (ADC_val > (y[0]+1))) then ADC_val = y[0] new_data = true end if end if end procedure -- --------------------------------- -- -- calibration data -- -- --------------------------------- -- ; calibration was performed using a 150 ohms multiturn trimmer ; the resistance was measured using a digital meter ; and the ADC value was taken at 0x10 digit interval. const byte MAX_INDEX = 85 ; maximum table entries const word CALIB_max = 0xa80 ; this is the ADC value of the maximum calibration ; range -- calibration data -- this is split in two tables since it cannot fit on a single bank of the PIC const word CALIB1[] = { 0x0000, 0x0036, 0x0041, 0x004B, 0x0056, 0x0060, 0x006B, 0x0075, 0x0080, 0x008A, 0x0094, 0x009F, 0x00A9, 0x00B3, 0x00BD, 0x00C8, 0x00D2, 0x00DC, 0x00E6, 0x00F0, 0x00FB, 0x0105, 0x010F, 0x0119, 0x0124, 0x012E, 0x0139, 0x0143, 0x014D, 0x0158, 0x0163, 0x016D, 0x0178, 0x0183, 0x018E, 0x0199, 0x01A4, 0x01AF, 0x01BA, 0x01C6, 0x01D1, 0x01DD, 0x01E8, 0x01F4, 0x0200, 0x020C, 0x0218, 0x0225, 0x0231, 0x023E, 0x024A, 0x0257, 0x0265, 0x0272, 0x027F, 0x028D, 0x029B, 0x02A9, 0x02B7, 0x02C5, 0x02D4, 0x02E2, 0x02F1, 0x0300, 0x0310, 0x031F, 0x032F, 0x033F, 0x0350, 0x0360, 0x0371, 0x0382, 0x0393, 0x03A5, 0x03B7, 0x03C9, 0x03DB, 0x03EE, 0x0400, 0x0414, 0x0427, 0x043B, 0x044F, 0x0463, 0x0478 } -- the first element of the next table is the value of the last element in the -- previous table, i.e. CALIB2[0] == CALIB1[MAX_INDEX] const word CALIB2[] = { 0x0478, 0x048D, 0x04A2, 0x04B8, 0x04CE, 0x04E5, 0x04FC, 0x0513, 0x052B, 0x0543, 0x055D, 0x0576, 0x0591, 0x05AC, 0x05C7, 0x05E4, 0x0601, 0x061F, 0x063E, 0x065D, 0x067E, 0x069F, 0x06C2, 0x06E5, 0x070A, 0x072F, 0x0756, 0x077D, 0x07A6, 0x07D0, 0x07FB, 0x0828, 0x0855, 0x0884, 0x08B5, 0x08E6, 0x0919, 0x094E, 0x0984, 0x09BB, 0x09F4, 0x0A2E, 0x0A6A, 0x0AA8, 0x0AE8, 0x0B2A, 0x0B6F, 0x0BB7, 0x0C02, 0x0C50, 0x0CA3, 0x0CF9, 0x0D55, 0x0DB5, 0x0E1A, 0x0E85, 0x0EF5, 0x0F6C, 0x0FE8, 0x106C, 0x10F7, 0x118A, 0x1227, 0x12CE, 0x1380, 0x143E, 0x150A, 0x15E3, 0x16CA, 0x17C2, 0x18CA, 0x19E4, 0x1B11, 0x1C51, 0x1DAD, 0x1F30, 0x20E6, 0x22DA, 0x2518, 0x27AC, 0x2AA2, 0x2E04, 0x31E0, 0x363F, 0x3B2F } -- this code is based on Microchip's application note AN942 function esr( word in PwLI_Input ) return word is var byte PwLI_Index var word PwLI_Slope, PwLI_Offset var byte span span = byte (PwLI_Input & 0x0f) PwLI_Index = byte (PwLI_Input >> 4) if ( PwLI_Index < (MAX_INDEX - 1) ) then PwLI_Slope = CALIB1[PwLI_Index + 1] - CALIB1[PwLI_Index] PwLI_Offset = (PwLI_Slope * span) >> 4 return CALIB1[PwLI_Index] + PwLI_Offset else PwLI_Slope = CALIB2[PwLI_Index + 1 - (MAX_INDEX - 1)] - CALIB2[PwLI_Index - (MAX_INDEX - 1)] PwLI_Offset = (PwLI_Slope * span) >> 4 return CALIB2[PwLI_Index - (MAX_INDEX - 1)] + PwLI_Offset end if end function var word esr_val = 0, zero = 0, zero_ADC = 0, ADC_val_min =0, ADC_val_max = 0 const byte DISP_UPDATE = 5 ; ~ 0.5 s procedure display_result() is ; display esr if (ADC_val >= ADC_val_max) then disp_ovr() elsif (ADC_val < ADC_val_min) then disp_err() else if (new_data & (update_counter >= DISP_UPDATE)) then esr_val = esr(ADC_val - ADC_val_min) ; make sure display value is positive if (esr_val >= zero) then disp_dec((esr_val - zero)) disp_blink = false else disp_dec((zero - esr_val)) disp_blink = true end if new_data = false update_counter = 0 end if end if end procedure function data_eeprom_read(byte in addr) return byte is EEADR = addr EECON1_EEPGD = false ; address data memory EECON1_RD = true ; start read return EEDATA end function procedure data_eeprom_write(byte in addr, byte in data) is EEDATA = data EEADR = addr EECON1_EEPGD = false ; address data memory EECON1_WREN = true ; enable writes ; special 5 instruction write sequence EECON2 = 0x55 EECON2 = 0xAA EECON1_WR = true EECON1_WREN = false repeat until (!EECON1_WR) end procedure function read_CALIB_MIN() return word is var word x var byte ax[2] at x ax[0] = data_eeprom_read(0) ax[1] = data_eeprom_read(1) return x end function procedure write_CALIB_MIN(word in cal) is var byte ax[2] at cal data_eeprom_write(0, ax[0]) data_eeprom_write(1, ax[1]) end procedure -- --------------------------------- -- -- main() code starts here -- -- --------------------------------- -- PRAGMA EEDATA 0x00, 0x00 ; erase CALIB_min block var bit calibration = false setup_PWM() setup_ADC() -- setup TIMER1 T1CON = 0b_0011_0101 ; 1:8 prescale, internal osc start timer_and_button() start display() ; start the display routine start read_ADC() ; start the ADC routine ADC_val_min = read_CALIB_MIN() if (ADC_val_min == 0) then calibration = true ; start calibration if EEPROM is empty end if while (update_counter == 0) loop ; wait until debounce is working suspend end loop if (button_pressed | calibration) then ; enable calibration disp_cal() disp_blink = true repeat suspend until (button_released | calibration) button_released = false ; wait for another button press ; short button press will cancel calibration ; long button press will start calibration repeat suspend until (button_released | button_long_pressed ) disp_blink = false if (button_long_pressed | calibration) then repeat suspend until (button_released | calibration) button_released = false ; preload filter ADC_sum = 0x800 << 5 filter_reg = 0x800 << FILTER_SHIFT ADC_cnt = 256 forever loop oversample_ADC() disp_hex(ADC_val - zero_ADC) ; long button press will exit calibration if (button_long_pressed & (zero_ADC != 0)) then ; save data to eeprom write_CALIB_MIN(zero_ADC) disp_done() repeat suspend until (button_released) ADC_val_min = zero_ADC exit loop end if ; zero display if (button_released) then button_released = false zero_ADC = ADC_val end if suspend end loop end if end if ; end calibration ADC_val_max = ADC_val_min + CALIB_max ; preload the LPF for "instant-on" ADC_sum = ADC_val_min << 5 filter_reg = ADC_val_min << FILTER_SHIFT ADC_cnt = 256 forever loop oversample_ADC() display_result() if (button_released) then button_released = false zero = esr_val new_data = true end if ; allow other tasks to run suspend end loop end block