-- ------------------------------------------------------------------------ -- Title: Simple ESR Meter v3.0 -- -- 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 -- -- ------------------------------------------------------------------------ pragma task 4 include 16f690 pragma target clock 8_000_000 pragma target OSC INTOSC_NOCLKOUT OSCCON_IRCF = 0b_111 pragma target WDT disabled pragma target PWRTE disabled pragma target MCLR internal pragma target CP disabled pragma target IESO disabled pragma target FCMEN disabled enable_digital_io() include delay -- setup pins ; led display pin_A0_direction = input pin_A1_direction = input pin_B4_direction = input pin_B6_direction = input pin_C0_direction = input pin_C1_direction = input pin_C2_direction = input pin_C4_direction = input pin_A2_direction = output pin_B5_direction = output pin_C3_direction = output ; ADC pin_AN8_direction = input ; esr signal pin_B7_direction = input ; button -- aliases var bit disp_a is pin_C2_direction var bit disp_b is pin_B4_direction var bit disp_c is pin_C0_direction var bit disp_d is pin_A1_direction var bit disp_e is pin_A0_direction var bit disp_f is pin_B6_direction var bit disp_g is pin_C4_direction var bit disp_dp is pin_C1_direction var bit disp_1 is pin_B5 var bit disp_2 is pin_A2 var bit disp_3 is pin_C3 var bit button is pin_B7 -- initialize pins PORTA = 0 PORTB = 0 PORTC = 0 OPTION_REG_NRAPU = false ; enable weak pull-ups WPUA = 0 ; disable pull-ups on port A WPUB = 0b_1000_0000 ; enable on button pin (B7) -- 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 < 100) 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 8, ESR signal, pin 9, battery level ANSELH = 0b0011 ; select conversion clock, 16 TOSC @8MHz = 2.0 usec ADCON1_ADCS = 0b101 ; internal reference ADCON0_VCFG = false ; right justified ADCON0_ADFM = true ; 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 = 1 ; in 10us, 2*Tad + conversion delay -- 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_1000 ; AN8 ; wait 2*Tad + conversion delay, ~10 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 = 0 ; 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, 0x001A, 0x0029, 0x0035, 0x0041, 0x004D, 0x0059, 0x0063, 0x006D, 0x0076, 0x0080, 0x0089, 0x0093, 0x009D, 0x00A6, 0x00B2, 0x00BF, 0x00C8, 0x00CF, 0x00D9, 0x00E5, 0x00EF, 0x00F8, 0x0105, 0x0111, 0x011A, 0x0124, 0x0130, 0x013A, 0x0143, 0x014F, 0x0159, 0x0165, 0x016F, 0x0178, 0x0184, 0x0190, 0x019A, 0x01A6, 0x01B0, 0x01BE, 0x01CA, 0x01D6, 0x01E0, 0x01EE, 0x01FD, 0x0209, 0x0217, 0x0224, 0x0230, 0x023C, 0x024A, 0x0259, 0x0265, 0x0273, 0x0282, 0x028E, 0x029C, 0x02AB, 0x02BB, 0x02C8, 0x02D8, 0x02E7, 0x02F3, 0x0306, 0x0315, 0x0326, 0x0337, 0x0347, 0x0358, 0x0369, 0x037A, 0x038D, 0x03A3, 0x03B7, 0x03C9, 0x03DB, 0x03EE, 0x03FE, 0x0418, 0x0432, 0x0444, 0x0460, 0x0466, 0x0480 } -- 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[] = { 0x0480, 0x0492, 0x04B0, 0x04C4, 0x04CE, 0x04E8, 0x050B, 0x051C, 0x053F, 0x055A, 0x056A, 0x058D, 0x05A7, 0x05C1, 0x05EC, 0x060E, 0x0629, 0x0640, 0x065D, 0x0680, 0x06A2, 0x06CC, 0x06F0, 0x071B, 0x0747, 0x0769, 0x078C, 0x07B7, 0x07E4, 0x07FD, 0x0831, 0x0853, 0x0876, 0x08AA, 0x08DE, 0x0912, 0x0946, 0x0983, 0x09B0, 0x09EC, 0x0A32, 0x0A6E, 0x0AA0, 0x0AE6, 0x0B18, 0x0B5E, 0x0BA4, 0x0BF4, 0x0C4E, 0x0C97, 0x0CD0, 0x0D16, 0x0D70, 0x0DCA, 0x0E2E, 0x0E9C, 0x0EE2, 0x0F4B, 0x0FD2, 0x1040, 0x10AE, 0x1126, 0x118A, 0x1220, 0x12C0, 0x1342, 0x1414, 0x1496, 0x1554, 0x1612, 0x16DA, 0x1798, 0x1874, 0x196E, 0x1A72, 0x1B94, 0x1CCA, 0x1DCE, 0x1F2C, 0x2080, 0x2224, 0x23D2, 0x25D0, 0x279C, 0x29B8 } -- 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 EEDAT end function procedure data_eeprom_write(byte in addr, byte in data) is EEDAT = 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