;------------------------------------------------------------------------------- ; Title: Cat Repellent. ; ; Author: Rob Jansen, Copyright (c) 2020..2020, all rights reserved. ; ; Revision: ; 2020-08-01 : Rev 1.0. First version ; 2020-08-22 : Rev 1.1. Added automatic sleep to prevent full battery drain. ; ; Compiler: 2.5q4 ; ; ; Description: Create a 40 kHz tone on the PWM output when triggered by an ; external interrupt coming from a PIR Module. The PIC uses the ; hardware PWM for driving the piezo ultrasonic transmitter. ; A LED will be on when the PWM is active. When the supply voltage ; drops below 3.0 Volt the LED will start to blink when the device ; is active to indicate a low battery voltage. ; When the supply voltage drops below 2.7 Volt, the device will ; switch off immediately. ; ; Sources: ; ;------------------------------------------------------------------------------- ; include 12f615 ; Target PICmicro ; ; Use internal clock and internal reset. pragma target OSC INTOSC_NOCLKOUT ; Internal Clock pragma target PWRTE Enabled ; Power up timer pragma target MCLR Internal ; Reset internal pragma target WDT Disabled ; No watchdog pragma target BROWNOUT Disabled ; Disable to save power pragma target IOSCFS F4MHZ ; Set internal oscillator to 4 MHz pragma target clock 4_000_000 ; Oscillator frequency 4 MHz enable_digital_io() ; make all pins digital I/O ; Wait some time for the hardware to stabilize. _usec_delay(100_000) ; Tune the oscillator (not the factory default) to get a frequency of exactly ; 40 kHz. Note this is PIC dependent and it should normally not be needed to ; tune the oscillator ; OSCTUNE = 0b0000_1010 ; Increases the oscillator frequency. -- ------------------------- Pin definitions ----------------------------------- ; LED indicating when the piezzo buzzer is active, connected to pin 7. alias led is pin_GP0 pin_GP0_direction = output ; The external interrupt pin is used to wakeup the cat repeller, pin 5. pin_GP2_direction = input ; We do not use GP2 as PWM output but use the altnerate pin. ; This makes it possible to still use the external interrupt. APFCON_P1ASEL = TRUE -- Move PWM output from GP2 to GP5. Pin 2. pin_GP5_direction = output APFCON_P1BSEL = TRUE -- Move PWM output from GP0 to GP4. Pin 3. pin_GP4_direction = output ; Enable weak pull up ports for inputs that are not connected. GP3 (MCLR\) is ; connected to VDD. WPU_WPUA1 = TRUE ; Weak pull-up enabled for GP1. WPU_WPUA2 = FALSE ; No weak pull-up enabled for GP2. OPTION_REG_NGPPU = FALSE -- ------------------------- Contants & Variables ------------------------------ ; Timer values in timer ticks of 20 ms. const word ON_TIME = 3_000 ; 60 seconds on time. const byte BLINK_TIME = 25 ; Half a second on an half a second off. ; Values for detecting when the voltage of the battery gets too low. The higher ; the figure, the lower the supply voltage. const word OFF_VOLTAGE_LEVEL = 218 ; Equals a battery voltage of about 2.7V. const word LOW_VOLTAGE_LEVEL = 198 ; Equals a battery voltage of about 3.0V. const byte VOLTAGE_SAMPLES = 10 ; Number of samples before deciding to blink. ; ADC initialization value. Right justified, VDD 0,6 Volt reference ADC enabled. ; For the off value we remove the 0,6 Volt reference otherwise a low power ; consumption remains when in sleep mode const byte ADC_INIT_VALUE = 0b1001_0101 const byte ADC_OFF_VALUE = 0b1000_0100 ; LED is turned on when output is LOW. const bit LED_ON = LOW const bit LED_OFF = HIGH ; Global variables var word adc_value var word on_timer var byte blink_timer var byte sample_counter var bit led_active var bit blink_the_led ;-------------------------Procedures and Functions ----------------------------- ; Init the ADC. We follow the steps from the datasheet. procedure adc_init() Is ; We use Fosc/8 which is OK for a 4 MHz clock. All pins are digital_io. ANSEL = 0b0001_0000 ; ADC initialization value. Right justified, VDD 0,6 Volt reference ADC enabled. ; For the off value we remove the 0,6 Volt reference otherwise a low power ; consumption remains when in sleep mode. ADCON0 = ADC_INIT_VALUE VRCON_CMVREN = FALSE ; Disable comparator reference to save power (default). VRCON_FVREN = TRUE ; 0.6 Volt reference must be enabled PIR1_ADIF = FALSE end procedure ; Start one cycle of the ADC converter. procedure adc_start() is ADCON0_GO = TRUE end procedure ; Enable the ADC and 0.6 Volt reference again. procedure adc_enable() is ADCON0 = ADC_INIT_VALUE VRCON_FVREN = TRUE end procedure ; Disable the ADC and disable the 0.6 Volt reference to save power. procedure adc_disable() is ADCON0 = ADC_OFF_VALUE VRCON_FVREN = FALSE end procedure ; Start the PWM which is controlled by Timer 2. procedure pwm_start() is T2CON_TMR2ON = TRUE end procedure ; Stop the PWM which is controlled by Timer 2. procedure pwm_stop() is T2CON_TMR2ON = FALSE end procedure ; Initialize PWM according to description in 16F615 Data Sheet page 210 ; The period is set to 25 us (40 kHz) with a duty cycle of 50% procedure pwm_init() is pwm_stop() ; The PWM must generate a frequency of 40 kHz = 25 us period time ; Register PR2 holds the PWM Timer Period using the following formula: ; PWM Period = (PR2 + 1) * 4 * Tosc * Timer2 prescale value where Tosc ; Tosc = 1/Fosc and Fosc = 4 MHz.Setting the prescaler to 1 and PR2 at 24: ; (24 + 1) * 4 * 1/4.000.000 * 1 = 25 us. Period Cycle = 40 kHz T2CON_TOUTPS = 0b0000 ; Postscaler is 1:1 (no postscaling). T2CON_T2CKPS = 0b00 ; Prescaler divide by 1 PR2 = 24 ; For this PIC this value brings it closer to 25 us. ; Set CCP1CON to half bridge PWM Mode CCP1CON_P1M = TRUE ; Set CCP1CON to have P1A active high and P1B active low in PWM Mode CCP1CON_CCP1M = 0b1100 ; The Pulse Width uses the value in CCPR1L and two LSB's in DC1B to get a ; 10 bit resolution. 10 bits are needed since it runs 4 times as fast, see ; formula below. The Pulse Width becomes: ; CCPR1L + CCP1CON_DCB * Tosc * Timer2 prescale value ; We set the Pulse with to 12,5 us which is 50% of the complete period ; This means a value of 12,5 us / Tosc (= 1/4 Mhz) = 50 = 0x32 = 0b0011 0010 ; So the two bits in CCP1CON_DCB becomes 0b10 and the value of CCPR1L becomes ; the value shifted right two places = 0b0000_1100 CCP1CON_DCB = 0b10 CCPR1L = 0b0000_1100 end procedure ; Disable some hardware and put the device in sleep. Power consumption is ; minimal. When an interrupt occurs we wakeup again. procedure sleep_and_wakeup() is pwm_stop() adc_disable() led = LED_OFF ; Let it all run-out. _usec_delay(10_000) ; Sleep well. asm sleep ; The program continues here after an external interrupt. pwm_start() adc_enable() led_active = LED_ON blink_the_led = FALSE sample_counter = 0 end procedure ; This procedure is called because of an external interrupt, generated by the ; PIR module. When the PIC is asleep it will wake up the on-time will be reset. procedure motion_dected() is pragma interrupt if INTCON_INTF then INTCON_INTF = FALSE on_timer = 0 end if end procedure ;--------------------------- Main program starts here -------------------------- ; Initialize. pwm_init() adc_init() on_timer = 0 blink_timer = 0 sample_counter = 0 blink_the_led = FALSE led_active = LED_OFF ; Initialize the external interrupt, trigger on rising edge. OPTION_REG_INTEDG = TRUE INTCON_INTF = FALSE INTCON_INTE = TRUE INTCON_GIE = TRUE ; Go to sleep and wait for the PIR to wake the device up. sleep_and_wakeup() forever loop ; This loop only runs when the device is active. Loop time is 20 ms. _usec_delay(20_000) ; When the on time has passed we go to sleep. if on_timer == ON_TIME then sleep_and_wakeup() else on_timer = on_timer + 1; end if ; Check if we need to blink the led. if blink_the_led then if blink_timer == BLINK_TIME then led_active = !led_active blink_timer = 0 else blink_timer = blink_timer + 1 end if end if ; Always update the LED. led = led_active ; Check if the ADC is done. if PIR1_ADIF then PIR1_ADIF = FALSE ; New ADC value captured, copy data. adc_value = word(ADRESH * 255) + word(ADRESL) ; Check the new value. First check if is needs to be switched off. if (adc_value > OFF_VOLTAGE_LEVEL) then ; Battery going too low, check if it was an incident. if sample_counter == VOLTAGE_SAMPLES then ; Voltage below critical value. Prevent batteries to be completely ; drained by switching off again. sleep_and_wakeup() else sample_counter = sample_counter + 1 end if elsif (adc_value > LOW_VOLTAGE_LEVEL) then ; Battery going low, check if it was an incident. if sample_counter == VOLTAGE_SAMPLES then sample_counter = 0 ; Once detected the blink stays active until sleep. blink_the_led = TRUE else sample_counter = sample_counter + 1 end if else ; No low voltage, reset. sample_counter = 0 end if end if ; Start a new ADC conversion. This can always be done since the conversion ; time is always shorter than the loop time. adc_start() end loop