/************************************************************ * 750mA Constant current power-supply for 5W Luxeon LED * with low battery warning ("winks" LED once every 10s) * and low battery shutoff (two battery chemistry modes) ************************************************************/ #define VERSION_STRING "Kinkajou 1.0.5" // Change Log: // v1.0 rc 4 - Added newline after headers, moved version string // v1.0 rc 5 - Implemented test coverage mechanism // Later // thermal shutoff // logging lo/hi battery voltage // really calibrate temperature sensor #include <16f684.h> #device ADC=10 /* Compile-time flags */ #define TEST_COVERAGE #define WATCHDOG #define LOG_TEMPERATURE #define LOG_LOWV_MINUTES #define SHOW_BATTERY //#define TEST_WDT /* General Parameters */ // EEPROM addresses: 16 bit data stored little-endian! #define HOUR_METER_LSB_EEADR 0x00 #define HOUR_METER_MSB_EEADR 0x01 #define MAX_TEMP_LSB_EEADR 0x02 #define MAX_TEMP_MSB_EEADR 0x03 #define LOWV_COUNT_LSB_EEADR 0x04 #define LOWV_COUNT_MSB_EEADR 0x05 #define TEST_COVERAGE_LSB_EEADR 0x06 #define TEST_COVERAGE_MSB_EEADR 0x07 #define TIMER2_PERIOD 0x1f #define DUTY_100 ((TIMER2_PERIOD+1)*4) #define MAX_DUTY ((128*8)+1) //65*8 is approx 50% /* Pin definitions */ #define LED_PIN PIN_C3 // Digital I/O #define PB_PIN PIN_A0 // Digital I/O #define MODESW_PIN PIN_A3 // Digital I/O (NOT MCLR!) #define VREF_PIN PIN_A1 // Analog Vref (AN1) #define VSENSE_PIN PIN_A2 // Analog Input AN2 #define I2CDAT_PIN PIN_A4 // Digital I/O #define I2CCLK_PIN PIN_A5 // Digital Output #define TSENSE_PIN PIN_C0 // Analog Input AN4 #define BATSENSE_PIN PIN_C1 // Analog Input AN5 #define ISENSE_PIN PIN_C2 // Analog Input AN6 #define TX_PIN PIN_C4 // Digital Output #define PWM_PIN PIN_C5 // PWM (digital output) P1A #define PWM_BIT 5 // for querying TRISC #define EEPROM_SCL I2CCLK_PIN #define EEPROM_SDA I2CDAT_PIN // ADC channel definitions #define VSENSE_CHANNEL 2 #define TSENSE_CHANNEL 4 #define BATSENSE_CHANNEL 5 #define ISENSE_CHANNEL 6 /* Configuration - aka "fuses" */ #ifdef WATCHDOG #fuses INTRC_IO, NOMCLR, PUT, WDT #else //~WATCHDOG #fuses INTRC_IO, NOMCLR, PUT, NOWDT #endif // WATCHDOG #use delay(clock=8000000, restart_wdt) #use rs232(baud=19200, xmit=TX_PIN, DISABLE_INTS) //#use i2c(master, restart_wdt, sda=I2CDAT_PIN, scl=I2CCLK_PIN) /* Special registers and bits */ #byte STATUS_REG = 0x03 #define C_BIT 0 #byte PIR_REG = 0x0c #define TMR2IF_BIT 1 #byte ADCON0_REG = 0x1f #define VCFG_BIT 6 #byte PR2_REG = 0x92 #byte CCPR1L_REG = 0x13 #byte CCP1CON_REG = 0x15 #define DC1B_BIT 4 #define DC1B_MASK 0x3 #define DC1B0_BIT 4 #define DC1B1_BIT 5 #define CCP1M_BIT 0 #define CCP1M_MASK 0xf #byte T2CON_REG = 0x12 #define T2CKPS_BIT 0 #define T2CKPS_MASK 0x3 #define TMR2ON_BIT 2 #byte TRISC_REG = 0x87 /* Macros */ #define LDB(byte, bit, mask) (((byte) >> (bit)) & (mask)) #define DPB(byte, bit, mask, value) \ byte = ((byte) & (((mask) << (bit)) ^ 0xff)) \ | (((value) & (mask)) << (bit)) #define NO_OPT(s) s #define OPT(a,b) a #define LED_ON() output_low(LED_PIN) // Turn on status LED #define LED_OFF() output_high(LED_PIN) // Turn off status LED #define CARRY() (bit_test(STATUS_REG, C_BIT)) #define HEARTBEAT() LED_ON(); restart_wdt(); LED_OFF(); #ifdef TEST_COVERAGE #define COVERAGE(n) bit_clear(gCoverage, n) #else //~TEST_COVERAGE #define COVERAGE(n) #endif // TEST_COVERAGE /* REGULATOR */ /* Parameters */ #define MIN_VOLTAGE ((10<<1)+1) // 10.5V =1.75V/cell (min gel) #define WARN_VOLTAGE (11<<1) // 11.0V half a volt to go, enough? #define MAX_VOLTAGE ((14<<1)+1) // 14.5V ~=2.40V/cell (max wet) #define ONE_5_Q1 ((1<<1)+1) // 1.5V #define MINIMAL_ZERO_CURRENT 15 // detects whether ICD2 is attached // Empirically-derived parameters #define INITIAL_SETPOINT 226 //222 //up 2% - Target I is independent of pwm res #define INITIAL_PWM (40<<3) // safe duty @15V, expect 5 of overshoot #define STARTUP_PERIOD (32*4) // want *8 for Q3, but 8-bit counter so *4 #define LOG2_P_DIVISOR 3 //log2(8) // was 5 log2(32), but too much roundoff #define LOG2_I_DIVISOR 7 //log2(128) // EXP: 7 to lower threshold -> loser! #define ONE_MINUTE 229 // about a minute in terms of 3.8Hz (228.88) #define HALF_SECOND 2 // about half a second /* Global state variables */ #ifdef TEST_COVERAGE int16 gCoverage; #endif // TEST_COVERAGE unsigned int g4Hz_counter = (ONE_MINUTE - HALF_SECOND); // "0-minute" behavior int1 gPending_minute = 0; signed int gMinute_counter = -1; // remember zero-minute! int1 gPending_tenth_hour = 0; int16 gHour_meter = 0; int16 gInitial_hour_meter; int16 gMax_temperature = 0; int16 gTemperature; #ifdef LOG_LOWV_MINUTES signed int gLowV_minutes = 0; signed int16 gLowV_total_minutes; #endif // LOG_LOWV_MINUTES long gSetpoint; signed long gPWM; // shadow register //int gInitialBattery; // Not really used int8 new_ccpr1l; int8 new_dc1b; int8 new_ccpr1l_plus1; int8 new_dc1b_plus1; int8 dither_patterns[8] = {0x00, 0x01, 0x11, 0x49, //0x49 = 0b01001001 0x55, 0x6b, 0x77, 0x7f}; //0x6b = 0b01101011 int8 gDither; /* Prototypes */ void status_led_show_error (void); long round_log2 (signed long numerator, int log2_denominator); void init_regulator (void); void regulator_one_move (void); void initialize_pwm (void); int16 read_adc_vcc_as_vref (void); int16 read_adc_average4 (void); void newline(void); void set_pwm_duty10 (int16 duty); void print_q1 (int n); int get_battery (void); void timer2_isr (void); int16 read_eeprom16 (int lsb_address); void write_eeprom16 (int lsb_address, int16 data16); int temp_raw2c (signed int16 raw); void update_lowv_count_eeprom (void); #inline int16 read_temperature (void); #inline int16 read_battery (void); #inline int16 read_current (void); #inline int16 read_voltage (void); #inline int8 read_mode_switch (void); /* Functions */ void main (void) { int battery; int i; int16 dim_pwm; setup_oscillator(OSC_8MHZ); setup_adc_ports(sAN1|sAN2|sAN4|sAN5|sAN6); setup_adc(ADC_CLOCK_INTERNAL); bit_set(ADCON0_REG, VCFG_BIT); // setup_adc_ports ignores VSS_VREF flag? newline(); newline(); printf(VERSION_STRING); putc(' '); putc('('); printf(__DATE__ " " __TIME__); putc(')'); #ifdef TEST_COVERAGE gCoverage = read_eeprom16(TEST_COVERAGE_LSB_EEADR); // starts 0xffff printf (" %4lx", gCoverage); #endif // TEST_COVERAGE newline(); newline(); printf(OPT("Batt: ", "Initial battery: ")); print_q1(get_battery()); putc('V'); newline(); #ifdef TEST_WDT printf(OPT("TWDT:SW = %d", "TEST_WDT! Mode Switch = %d"), read_mode_switch()); newline(); #endif // TEST_WDT /*** Initializations ***/ #ifdef WATCHDOG setup_wdt(WDT_DIV_512); // default 16ms #endif // WATCHDOG initialize_pwm(); delay_ms(150); // wait for transients to die down if (read_current() > MINIMAL_ZERO_CURRENT) { COVERAGE(0); //printf(OPT("I!=0, huh?", // "Non-zero I reading w/PWM=0...\r\n")); printf(OPT("Unplug ICD2?\r\n", "...maybe you need to disconnect ICD2 programmer.\r\n")); status_led_show_error(); } // Don't do this if connected to ICD2 gHour_meter = read_eeprom16(HOUR_METER_LSB_EEADR); printf("On: "); if (gHour_meter == 0xffff) { // If eeprom hour meter locations are both 0xff, clear them to zero ("NEW") COVERAGE(1); gHour_meter = 0; write_eeprom16(HOUR_METER_LSB_EEADR, gHour_meter); printf("NEW"); } else { COVERAGE(2); printf("%lu.%dh", gHour_meter / 10, (int) (gHour_meter % 10)); } newline(); gInitial_hour_meter = gHour_meter; #ifdef LOG_TEMPERATURE gMax_temperature = read_eeprom16(MAX_TEMP_LSB_EEADR); if (gMax_temperature == 0xffff) { // If eeprom max temp locations are both 0xff, clear them to zero COVERAGE(3); gMax_temperature = 0; write_eeprom16(MAX_TEMP_LSB_EEADR, gMax_temperature); } else { COVERAGE(4); printf("MaxT: %dC (%ld)", temp_raw2c(gMax_temperature), gMax_temperature); newline(); } #endif // LOG_TEMPERATURE #ifdef LOG_LOWV_MINUTES gLowV_total_minutes = read_eeprom16(LOWV_COUNT_LSB_EEADR); if (gLowV_total_minutes == 0xffff) { // New, clear to zero COVERAGE(5); gLowV_total_minutes = 0; write_eeprom16(LOWV_COUNT_LSB_EEADR, gLowV_total_minutes); } else { COVERAGE(6); printf(NO_OPT("LowB: %ldm"), gLowV_total_minutes); newline(); } #endif // LOG_LOWV_MINUTES newline(); enable_interrupts(GLOBAL); // enable minute timer setup_timer_1(T1_INTERNAL | T1_DIV_BY_8); // 8MHz/4/8/2^16 = about 3.8Hz enable_interrupts(INT_TIMER1); /* main LED regulator */ init_regulator(); /**** TOP-LEVEL LOOP ****/ regulator_loop: COVERAGE(7); regulator_one_move(); /*** Process done once/minute at user level ***/ if (gPending_minute) { COVERAGE(8); gPending_minute = 0; #ifdef TEST_WDT if (!read_mode_switch()) { printf("DIE!"); newline(); death_spiral: goto death_spiral; // should cause WDT reset } #endif // TEST_WDT battery = get_battery(); if (battery < WARN_VOLTAGE) { COVERAGE(9); dim_pwm = gPWM - (gPWM >> 3); // 7/8th for (i = 0; i < 2; ++i) { set_pwm_duty10(dim_pwm); delay_ms(20); set_pwm_duty10(gPWM); delay_ms(100); } set_pwm_duty10(dim_pwm); delay_ms(20); set_pwm_duty10(gPWM); delay_ms(10); // let LED current stabilize again #ifdef LOG_LOWV_MINUTES // increment once a minute every time we "wink" ++gLowV_minutes; #endif // LOG_LOWV_MINUTES // Back to our regularly scheduled program... } // Shutdown happens after warning (mostly for "zero-minute" case) if (battery < MIN_VOLTAGE) { COVERAGE(10); delay_ms(100); // so you see third wink output_float(PWM_PIN); //turn off LED, no change to PWM #ifdef LOG_LOWV_MINUTES // write out when we actually shut down // use initial total plus this session update_lowv_count_eeprom(); #endif // LOG_LOWV_MINUTES #ifdef TEST_COVERAGE write_eeprom16(TEST_COVERAGE_LSB_EEADR, gCoverage); #endif // TEST_COVERAGE halt_loop: // spin, but keep WDT happy HEARTBEAT(); delay_ms(1); // lower status LED duty cycle goto halt_loop; } // ** end once/minute ** } /*** Process done once every six minutes at user level ***/ if (gPending_tenth_hour) { COVERAGE(11); gPending_tenth_hour = 0; gHour_meter = read_eeprom16(HOUR_METER_LSB_EEADR); ++gHour_meter; write_eeprom16(HOUR_METER_LSB_EEADR, gHour_meter); printf("%ld ", gHour_meter - gInitial_hour_meter); #ifdef SHOW_BATTERY print_q1(get_battery()); putc('V'); putc(' '); #endif // SHOW_BATTERY #ifdef LOG_TEMPERATURE gTemperature = read_temperature(); gMax_temperature = read_eeprom16(MAX_TEMP_LSB_EEADR); if (gTemperature > gMax_temperature) { // only write if new maximum COVERAGE(12); write_eeprom16(MAX_TEMP_LSB_EEADR, gTemperature); } printf("%dC (%ld) ", temp_raw2c(gTemperature), gTemperature); #endif // LOG_TEMPERATURE #ifdef LOG_LOWV_MINUTES // writes out when we actually shut down, but also do periodically // use initial total plus this session update_lowv_count_eeprom(); #endif // LOG_LOWV_MINUTES #ifdef TEST_COVERAGE write_eeprom16(TEST_COVERAGE_LSB_EEADR, gCoverage); printf (" %4lx", gCoverage); #endif // TEST_COVERAGE #if defined(SHOW_BATTERY) || defined(LOG_TEMPERATURE) || defined(TEST_COVERAGE) newline(); #endif // ** end once every 6 minutes ** } HEARTBEAT(); goto regulator_loop; } long round_log2 (signed long numerator, int log2_denominator) { short negative; long doubled, doubled_rounded, rounded; negative = (numerator < 0); if (negative) { numerator = -numerator; } doubled = numerator >> (log2_denominator - 1); doubled_rounded = doubled + 1; // add 1/2LSB rounded = doubled_rounded >> 1; if (negative) { return (-rounded); } else { return rounded; } } void init_regulator (void) { gPWM = INITIAL_PWM; gSetpoint = INITIAL_SETPOINT; //gInitialBattery = get_battery(); } void regulator_one_move (void) { long current; signed long error; signed int pAdjustment; current = read_current(); error = gSetpoint - current; pAdjustment = round_log2(error, LOG2_P_DIVISOR); // update shadow register gPWM = gPWM + pAdjustment; // lower and upper limits if (gPWM < 0) gPWM = 0; if (gPWM > MAX_DUTY) gPWM = MAX_DUTY; // update hardware PWM registers set_pwm_duty10(gPWM); } /* existing low-level drivers, etc. */ void initialize_pwm (void) { // TRISC<5> = 1 // disable P1A output during configuration output_float(PWM_PIN); // CCP1CON<7:6> = 00 // Single output // CCP1CON<5:4> = xx // duty cycle set later, 0b00 for now // CCP1CON<3:2> = 11 // PWM mode // CCP1CON<1:0> = 00 // All PWM outputs active-high //setup_ccp1(0b00001100); // CCP_PWM, active high CCP1CON_REG = 0b00001100; // no dead band delay // auto-shutdown: drive pins A and C to '0' (probably not needed) // Configure and clear TMR2 // clear interrupt: TMR2IF/PIR<1> = 0 bit_clear(PIR_REG, TMR2IF_BIT); // set prescale: T2CKPS/T2CON<1:0> = 1 (verify) // enable Timer2: TMR2ON/T2CON<2> = 1 // PR2 value = 0x1F // max resolution 7 bits setup_timer_2(T2_DIV_BY_1, TIMER2_PERIOD, 4); // postscale (1n, 16y, 4y) // wait for TMR2 overflow (TMR2IF=1) while (!bit_test(PIR_REG, TMR2IF_BIT)); // TRISC<5> = 0 // enable output output_low(PWM_PIN); // configure as output, value doesn't matter // ECCPASE/ECCPAS<7>=0 // In dither world set_pwm_duty10 doesn't actually do any work //timer2_int_count = 1; // spoof failsafe inside set_pwm_duty10() set_pwm_duty10(0); // would be done in t2 isr, but not running yet (ignore dither pattern) CCPR1L_REG = new_ccpr1l; CCP1CON_REG &= 0xcf; // (char) ~0x30 // don't use DPB, save time CCP1CON_REG |= new_dc1b; // fire up interrupts enable_interrupts(INT_TIMER2); } #inline int16 read_temperature (void) { set_adc_channel(TSENSE_CHANNEL); return read_adc(); } #inline int16 read_battery (void) { set_adc_channel(BATSENSE_CHANNEL); return read_adc_vcc_as_vref(); } #inline int16 read_current (void) { set_adc_channel(ISENSE_CHANNEL); return read_adc_average4(); } #inline int16 read_voltage (void) { set_adc_channel(VSENSE_CHANNEL); return read_adc_vcc_as_vref(); } int16 read_adc_average4 (void) { int16 total; total = read_adc() + read_adc() + read_adc() + read_adc(); return ((total + 2) >> 2); } int16 read_adc_vcc_as_vref (void) { int16 sample; bit_clear(ADCON0_REG, VCFG_BIT); // Use Vcc as Vref (extra range) delay_us(10); sample = read_adc_average4(); bit_set(ADCON0_REG, VCFG_BIT); // Return to using VREF pin and Vref delay_us(10); return sample; } #inline int8 read_mode_switch (void) { return input(MODESW_PIN); } void newline (void) { printf(NO_OPT("\r\n")); } void set_pwm_duty10 (int16 duty) // Really ought to be named set_pwm_duty13 // CCPR1L = duty<9:5> // 10:5 if we want to support 100% duty cycle // DC1B = duty<4:3> // dither = duty<2:0> { int16 duty_plus1; duty_plus1 = duty + 8; // Q3 disable_interrupts(GLOBAL); new_ccpr1l = (int8)((duty >> 5) & 0xff); // probably no need to mask new_dc1b = (int8)((duty & 0x18) << 1); // i.e. duty >> 3, but insert at 5:4 new_ccpr1l_plus1 = (int8)((duty_plus1 >> 5) & 0xff); // no need to mask? new_dc1b_plus1 = (int8)((duty_plus1 & 0x18) << 1); gDither = dither_patterns[(int8)(duty & 0x7)]; // 3 fraction bits enable_interrupts(GLOBAL); } int get_battery (void) { long battery; battery = read_battery(); battery = (battery << 1) / 27; // twice the actual voltage or fixed point: Q1 return (int) battery; } void print_q1 (int n) { printf(NO_OPT("%2u."), n >> 1); if (n & 0x1) { putc('5'); } else { putc('0'); } } void status_led_show_error (void) { output_float(PWM_PIN); //make real sure main LED is off #ifdef TEST_COVERAGE write_eeprom16(TEST_COVERAGE_LSB_EEADR, gCoverage); #endif // TEST_COVERAGE loop: LED_ON(); delay_ms(100); //interrupts stretch this some LED_OFF(); delay_ms(100); restart_wdt(); goto loop; } #int_timer2 void timer2_isr (void) { #asm rlf gDither, 0; // dup bit 7 into carry rlf gDither, 1; // to get 8-bit rotate-left #endasm if (CARRY()) { COVERAGE(13); CCPR1L_REG = new_ccpr1l_plus1; CCP1CON_REG &= 0xcf; // (char) ~0x30 // don't use DPB, save time CCP1CON_REG |= new_dc1b_plus1; } else { COVERAGE(14); CCPR1L_REG = new_ccpr1l; CCP1CON_REG &= 0xcf; // (char) ~0x30 // don't use DPB, save time CCP1CON_REG |= new_dc1b; } } #int_timer1 void timer1_isr (void) { COVERAGE(15); ++g4Hz_counter; if (g4Hz_counter >= ONE_MINUTE) { gPending_minute = 1; g4Hz_counter = 0; ++gMinute_counter; } if (gMinute_counter >= 6) { gPending_tenth_hour = 1; gMinute_counter = 0; } } int16 read_eeprom16 (int lsb_address) { unsigned int eedata; int16 data16; eedata = read_eeprom(lsb_address + 1); //MSB data16 = eedata << 8; eedata = read_eeprom(lsb_address); //LSB data16 |= eedata; return data16; } void write_eeprom16 (int lsb_address, int16 data16) { write_eeprom(lsb_address, data16 & 0xff); //LSB write_eeprom(lsb_address + 1, data16 >> 8); //MSB } int temp_raw2c (signed int16 raw) { return (signed int) ((8 * (raw - 370)) / 45); // div-by-5.62 } void update_lowv_count_eeprom (void) { write_eeprom16(LOWV_COUNT_LSB_EEADR, gLowV_total_minutes // because first wink doesn't count + ((gLowV_minutes == 0) ? 0 : gLowV_minutes - 1)); } /*EOF*/