Connect with us

Fanrunon - delayed turn-off for bathroom extractor fan

  • My 'Fanrunon' project is a turn-off delay timer module for use with an AC mains load such as a fan or a light. The rated maximum current is 2.5A RMS. It uses a triac for switching, and a Microchip PIC12F675 microcontroller.

    At this time, I have not built up the hardware, and I have only partly tested the firmware using the Microchip MPLAB X emulator. The design and/or the code may change once I have built and tested the circuit and the code.

    The module fits inside the cavity behind the wall switch, and receives its power from the fan current itself. Because of this design feature, and unlike many commercial turn-off delay timers, this design doesn’t require a Neutral wire in the switch cavity.

    Fanrunon uses the mains frequency as its timing source, and supports 50 and 60 Hz mains frequencies. The run-on time can be selected by switching the wall switch ON and OFF a certain number of times. The wall switch must be a changeover type (SPDT aka SPCO) so the circuit can detect its position.

    Since the Fanrunon module doesn’t connect across the mains (it doesn’t use a Neutral connection), it gets its operating power by slightly dropping the voltage available to the load.

    faurunon-pic.gif

    Circuit description:

    Turning ON the wall switch completes the electrical circuit from mains Phase, through the Fanrunon circuit (CN1 pins 1 and 2), through the wall switch, through the fan, and back to mains Neutral, and this powers up the Fanrunon circuit. D1~4 form a bridge rectifier that ensures that the voltage across the series diode string D5~9 always has the right polarity — positive at the top. The diode string conducts the load current and limits the voltage across it to around 5V (depending on load current). On every half-cycle of the mains waveform, current flows through D1~4 and produces this voltage drop across the diode string.

    This 5V supply rail passes through fusible resistor R1 to D10, a 5.1V zener diode. This arrangement ensures that if excess current flows through the D5~9 string (due to a short-circuited fan or a wiring error), D5~9 may be damaged but R1 will very quickly go open-circuit, while D10 protects the microcontroller from losing its smoke. C1 smooths the DC rail so it remains active around zero-crossings of the mains voltage, when there is insufficient voltage available at the bridge rectifier input. This provides a 5V DC (approximately) supply to the microcontroller.

    U1 is a Microchip PIC12F675-I/P microcontroller in an 8-pin DIP package. CN2 is the ICSP (in-circuit serial programming) connector which enables the firmware to be programmed into U1 using a Microchip PICkit 2 or PICkit 3 USB programming device.

    U1 uses an internal factory-calibrated R-C oscillator operating at about 4 MHz. It also uses an internal power-up reset circuit and brown-out detector, so pin 4 (-MCLR) is not used, except as VPP during in-circuit programming.

    Only three I/O pins are used on U1. Pin 2 drives piezo transducer X1 which provides audio feedback to the user. Pin 2 is referred to as "PZO" in the source code. X1 is a simple transducer, not a beeper. The PIC generates the beep frequencies internally.

    Pin 3, "fan enable", is referred to as "FEN" in the source code. It drives U2, an MOC3063 or TLP3063 triac driver IC, which drives triac Q1, which is connected in parallel with the ON contact of the wall switch via CN1 pin 4 and can keep the load and the Fanrunon circuit energised even when the wall switch is OFF. When U1 drives pin 3 high, the LED in U2 illuminates and causes its output circuit to trigger Q1. U2's output circuit includes a zero crossing detector which ensures that Q1 is only turned ON and OFF when the voltage across it crosses zero. This minimises generated interference. R7 and C2 form a snubber network to reduce the effect of noise on the mains supply and voltage spikes due to the load inductance.

    U1 pin 5 is driven from the OFF contact of the wall switch via CN1 pin 3. This signal is referred to as "SAC" ("switched AC") in the source code. While the wall switch is in the OFF position, this signal will have a rectangular wave at the AC mains frequency, with a duty cycle between 30% and 70%. When the wall switch is in the ON position, this signal is pulled low by R2. This signal is used by the firmware to (a) detect the AC mains frequency (50 and 60 Hz are supported), (b) provide an accurate timing source for measuring the hold-on period, and (c) determine the position of the wall switch.

    Here is the source code, written in assembly language for the mid-range PIC microcontroller, in its current state. It assembles properly with MPLAB X version 2.00 but may contain bugs and has not been tested.

    Code:
    ; fanrunon.asm
    ; Kris Heidenstrom
    ; [email protected]
    
            list        b=4,c=240,n=0,r=dec ; Numeric constants are decimal
    
    ; 2014-01-06.001    Started
    ; 2014-02-18.002    First complete version
    ; 2014-02-28.003    Fixed logic in SACMon; detects mains frequency in simulator
    ;
    ; Target: Microchip PIC12F675 using internal oscillator at 4 MHz nominal.
    ; This is a mid-range PIC device (8-bit data, 14-bit-wide instruction word).
    ;
    ; This firmware controls a PIC microcontroller in a "fanrunon" circuit.
    ; This is a circuit that can be retrofitted to an AC mains-powered fan that
    ; extracts steam from a bathroom, or some similar load, that was previously
    ; controlled by a wall switch, to provide a run-on time or delayed turn-off
    ; so the user can leave the room and the fan will turn off automatically.
    ;
    ; The fanrunon module is installed into the cavity behind the wall switch, and
    ; the wiring is changed. The fanrunon module doesn't need a Neutral wire; it
    ; gets its operating voltage by being connected in series with the fan.
    ;
    ; The fanrunon module requires that the wall switch is a changeover type.
    ; The first (normally open) contact completes the circuit initially, activating
    ; the fan and energising the circuit. The circuit immediately enables a triac
    ; that is connected in parallel with the switch, keeping the fan and fanrunon
    ; module powered.
    ;
    ; The second (normally open) contact on the switch is monitored by firmware
    ; via the "SAC" (switched AC) input; this allows the user to turn the wall
    ; switch ON and OFF a number of times to select between several run-on times,
    ; as well as cancel the run-on feature if desired. The SAC input receives an
    ; AC signal at AC mains frequency while the wall switch is in the OFF position.
    ;
    ; The fanrunon module also has a piezoelectric transducer to provide audible
    ; feedback to the user.
    ;
    ; MCU pin allocation
    ;
    ; #  Capabilities               Used as     Connected to
    ; 1  VDD                        VDD         Circuit VCC
    ; 2  GPIO5/T1CKI/OSC1/CLKIN     D.OUT       Piezo transducer (returned to VCC)
    ; 3  GPIO4/-T1G/OSC2/CLKOUT     D.OUT       Active-high triac enable output
    ; 4  GPIO3/-MCLR/VPP            VPP         ICSP connector (33k pullup to VCC)
    ; 5  GPIO2/T0CKI/INT/COUT       D.IN        Mains frequency pulses while wall switch is OFF
    ; 6  GPIO1/CIN-/ICSPCLK         ICSPCLK     ICSP connector only
    ; 7  GPIO0/CIN+/ICSPDAT         ICSPDAT     ICSP connector only
    ; 8  VSS                        VSS         Circuit 0V
    ;
    ;-------------------------------------------------------------------------------
    ; General operating information
    ;
    ; Digital I/O map:          See GPIO register allocation, below
    ; Interrupt sources:        None used
    ; Sleep mode:               Not used
    ; Internal oscillator:      Used; calibrated (4 MHz nominal)
    ; Watchdog:                 Not used
    ; Power-on reset:           Internal (no external reset circuit on pin 4)
    ; -MCLR (pin 4) function:   Disabled
    ; Brown-out detector:       Used; threshold is fixed at 2.1V typical
    ; Power-up timer:           Enabled
    ; EEPROM:                   Not used
    ; RAM:                      Used; 64 bytes total are available
    ; Flash ROM:                Used; 1024 words total are available.
    ; Voltage reference:        Not used; disabled
    ; Analogue comparator:      Not used; disabled
    ; 10-bit ADC:               Not used; disabled
    ; Timer 0:                  Used for timekeeping (tick on every wraparound)
    ; Timer 1:                  Not used; disabled
    ; ICSP:                     Used; pins are not shared with any other functions
    ;
    ; Register bank select:     Bank 1 is used during initialisation only.
    ;                           From then on, bank 0 is always selected.
    ; PCLATH:                   Always kept at 0x00. Program memory is 1024 words
    ;                           in the PIC12C675. Calculated gotos will only work
    ;                           within the first 256 words.
    ;
    ;-------------------------------------------------------------------------------
    ; Configuration word:
    ;
    ; D C B A 9 8 7 6 5 4 3 2 1 0
    ; x x . . . . . . . . . . . .  BG1-0: set at factory; preserved by Microchip programmer
    ; . . x x x . . . . . . . . .  Unimplemented
    ; . . . . . 1 . . . . . . . .  -CPD: data memory (EEPROM) protection (1 = no protection)
    ; . . . . . . 1 . . . . . . .  -CP: program memory code protection (1 = no protection)
    ; . . . . . . . 1 . . . . . .  BODEN: brown-out detector enable (1 = enable)
    ; . . . . . . . . 0 . . . . .  MCLRE: MCLR pin function (0 disables MCLR function on pin)
    ; . . . . . . . . . 0 . . . .  -PWRTE: Power-up Timer Enable (0 = enable)
    ; . . . . . . . . . . 0 . . .  WDTE: Watchdog enable/disable (0 = disable)
    ; . . . . . . . . . . . 1 0 0  FOSC2-0: 1,0,0 = internal osc, GP4 and GP5 pins as I/O
    ;
    ;-------------------------------------------------------------------------------
    ; INITIAL REGISTER VALUES
    ;
    ; STATUS register (0x03) INITIALISED TO 0x00 THEN MODIFIED
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  IRP: Unimplemented in this variant
    ; . 0 . . . . . .  RP1: Unimplemented in this variant
    ; . . * . . . . .  RP0: Selects register bank (0 = 0x00~0x7F; 1 = 0x80~0xFF)
    ; . . . x . . . .  -TO: Clear at startup indicates a watchdog timeout occurred (read-only)
    ; . . . . x . . .  -PD: Cleared by a SLEEP instruction to enter power-down (read-only)
    ; . . . . . x . .  Z: Zero flag
    ; . . . . . . x .  DC: Digit carry flag
    ; . . . . . . . x  C: Carry flag
    
    ;-------------------------------------------------------------------------------
    ; FILE REGISTER USAGE
    
    ; GPIO register (0x05) (digital inputs and outputs) INITIALISED
                ; 7 6 5 4 3 2 1 0
                ; 0 0 . . . . . .              Unimplemented
    PZO EQU 5   ; . . 1 . . . . .  GPIO5: PZO: output to piezo transducer - initially high
    FEN EQU 4   ; . . . 1 . . . .  GPIO4: FEN: fan enable output to triac (initially high to enable triac)
                ; . . . . x . . .  GPIO3:      -MCLR pin, used for ICSP only (VPP); input only
    SAC EQU 2   ; . . . . . x . .  GPIO2: SAC: input from AC mains pulses from wall switch OFF (else 0)
                ; . . . . . . 1 .  GPIO1:      ICSP signal only; set to output and driven high
                ; . . . . . . . 1  GPIO0:      ICSP signal only; set to output and driven high
    
    GPIO_initvalue      EQU     b'00110011'
    
    ; INTCON register (0x0B) (interrupt control and status) INITIALISED TO 0x00
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  GIE: Global interrupt enable
    ; . 0 . . . . . .  PEIE: Peripheral interrupts enable
    ; . . 0 . . . . .  T0IE: Timer 0 overflow interrupt enable
    ; . . . 0 . . . .  INTE: GP2 pin interrupt enable
    ; . . . . 0 . . .  GPIE: GPIO port state change interrupt enable
    ; . . . . . 0 . .  T0IF: Flag for timer 0 overflow (must be reset by firmware)
    ; . . . . . . 0 .  INTF: Flag for GP2 pin interrupt
    ; . . . . . . . 0  GPIF: Flag for GPIO port state change interrupt
    
    ; PIR1 register (0x0C) (second peripheral interrupt request register) NOT INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  EEIF: EEPROM interrupt flag (must be cleared by firmware)
    ; . 0 . . . . . .  ADIF: ADC interrupt flag (must be cleared by firmware)
    ; . . 0 0 . . . .  Unimplemented
    ; . . . . 0 . . .  CMIF: Comparator interrupt flag (must be cleared by firmware)
    ; . . . . . 0 0 .  Unimplemented
    ; . . . . . . . 0  TMR1IF: Timer 1 interrupt flag (must be cleared by firmware)
    ;
    ; None of those interrupt sources are enabled via PIE1 so PIR1 doesn't need to
    ; be initialised.
    
    ; T1CON register (0x10) (Timer 1 control) NOT INITIALISED (reset default is 0x00)
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  Unimplemented
    ; . 0 . . . . . .  TMR1GE: Gating enable (irrelevant if TMR1ON = 0)
    ; . . 0 0 . . . .  T1CKPS1-0: Prescale select (ditto)
    ; . . . . 0 . . .  T1OSCEN: Enables T1 low power oscillator
    ; . . . . . 0 . .  -T1SYNC: Clear to enable synchronisation for external clock
    ; . . . . . . 0 .  TMR1CS: Timer 1 clock source (0 = internal, Fosc/4)
    ; . . . . . . . 0  TMR1ON: Enable Timer 1 (0 = disable)
    
    ; CMCON register (0x19) (comparator control) INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  Unimplemented
    ; . x . . . . . .  COUT: Comparator output state (read-only)
    ; . . 0 . . . . .  Unimplemented
    ; . . . 0 . . . .  CINV: Invert comparator output flag (don't care)
    ; . . . . 0 . . .  CIS: Determines input connection for modes 5,6 (don't care)
    ; . . . . . 1 1 1  CM2-0: Comparator mode. Mode 7: disable and power down.
    
    CMCON_initvalue     EQU     b'00000111'
    
    ; ADCON0 register (0x1F) (ADC control) NOT INITIALISED (reset default is 0x00)
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  AFFM: Format (right- or left-justified) (don't care)
    ; . 0 . . . . . .  VCFG: ADC reference voltage source (0 = VCC) (don't care)
    ; . . 0 0 . . . .  Unimplemented
    ; . . . . 0 0 . .  CHS1-0: Input channel select (0-3) (don't care)
    ; . . . . . . 0 .  GO/-DONE: Bit to trigger conversion (resets to 0 when done)
    ; . . . . . . . 0  ADON: Enable/disable ADC (0 = disable)
    
    ; OPTION_REG register (0x81) (miscellaneous options) INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; 1 . . . . . . .  -GPPU: GPIO pullup control (1 disables all pull-ups)
    ; . 0 . . . . . .  INTEDG: Specifies interrupt edge for GP2/INT (not used; don't care)
    ; . . 0 . . . . .  T0CS: T0 clock source (0 selects Fosc/4)
    ; . . . 0 . . . .  T0SE: Specifies edge for GP2/T0CKI (not used; don't care)
    ; . . . . 0 . . .  PSA: Prescaler assignment (0 = Timer0; 1 = watchdog)
    ; . . . . . 0 0 1  PS2-0: Selects prescaler division; 0,0,1 = divide by 4
    
    #define OPTIONREG_initvalue b'10000001'
    
    ; TRISIO register (0x85) (tri-state enable for digital I/O) INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; 0 0 . . . . . .  Unimplemented
    ; . . 0 . . . . .  TRISIO5: output mode (PZO)
    ; . . . 0 . . . .  TRISIO4: output mode (FEN)
    ; . . . . 1 . . .  TRISIO3: always an input anyway
    ; . . . . . 1 . .  TRISIO2: input mode (SAC)
    ; . . . . . . 0 .  TRISIO1: output mode
    ; . . . . . . . 0  TRISIO0: output mode
    
    #define TRISIO_initvalue b'00001100'
    
    ; PIE1 register (0x8C) (second peripheral interrupt enable register) INITIALISED TO 0x00
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  EEIE: EEPROM interrupt enable
    ; . 0 . . . . . .  ADIE: ADC interrupt enable
    ; . . 0 0 . . . .  Unimplemented
    ; . . . . 0 . . .  CMIE: Comparator interrupt enable
    ; . . . . . 0 0 .  Unimplemented
    ; . . . . . . . 0  T1IE: Timer 1 interrupt enable
    
    ; PCON register (0x8E) (power conditions) NOT INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; 0 0 0 0 0 0 . .  Unimplemented
    ; . . . . . . 1 .  -POR: Cleared by hardware after a POR; must be set by firmware
    ; . . . . . . . 1  -BOD: Cleared by hardware after a BOD; must be set by firmware
    
    ; OSCCAL register (0x90) (on-board RC oscillator calibration register) INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; x x x x x x . .  OSCCAL5-0: Calibration value for internal RC oscillator
    ; . . . . . . 0 0  Unimplemented
    ;
    ; OSCCAL is set to the value in W after the fixed location 0x03FF has been
    ; called. The programmer and/or factory places a retlw instruction at that
    ; location, which returns the value that was determined to give the correct
    ; oscillator frequency.
    
    ; WPU register (0x95) (weak pullup enable) NOT INITIALISED
    ; 7 6 5 4 3 2 1 0
    ; 0 0 . . . . . .  Unimplemented
    ; . . 0 . . . . .  WPU5: no weak pullup
    ; . . . 0 . . . .  WPU4: no weak pullup
    ; . . . . 0 . . .  Unimplemented
    ; . . . . . 0 . .  WPU2: no weak pullup
    ; . . . . . . 0 .  WPU1: no weak pullup
    ; . . . . . . . 0  WPU0: no weak pullup
    ;
    ; This register doesn't affect anything because -GPPU in OPTION_REG is set.
    
    ; IOC register (0x96) (interrupt-on-change enable) INITIALISED TO 0x00
    ; 7 6 5 4 3 2 1 0
    ; 0 0 . . . . . .  Unimplemented
    ; . . 0 . . . . .  IOC5: no interrupt on change
    ; . . . 0 . . . .  IOC4: no interrupt on change
    ; . . . . 0 . . .  IOC3: no interrupt on change
    ; . . . . . 0 . .  IOC2: no interrupt on change
    ; . . . . . . 0 .  IOC1: no interrupt on change
    ; . . . . . . . 0  IOC0: no interrupt on change
    
    ; VRCON register (0x99) (voltage reference control) NOT INITIALISED (reset default is 0x00)
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  VREN: Enable/disable voltage reference (0 = disable)
    ; . 0 . . . . . .  Unimplemented
    ; . . 0 . . . . .  VRR: Range selection (don't care)
    ; . . . 0 . . . .  Unimplemented
    ; . . . . 0 0 0 0  VR3-0: Voltage reference selection (don't care)
    
    ; EECON1 register (0x9C) (EEPROM control) NOT INITIALISED (reset default is 0x00)
    ; 7 6 5 4 3 2 1 0
    ; 0 0 0 0 . . . .  Unimplemented
    ; . . . . 0 . . .  WRERR: Set by hardware to indicate an EEPROM write error
    ; . . . . . 0 . .  WREN: Enables write cycles when 1
    ; . . . . . . 0 .  WR: Initiates a write cycle when set
    ; . . . . . . . 0  RD: Initiates a read cycle when set
    
    ; ANSEL register (0x9F) (ADC clock and input pin type selection) INITIALISED TO 0x00
    ; 7 6 5 4 3 2 1 0
    ; 0 . . . . . . .  Unimplemented
    ; . 0 0 0 . . . .  ADCS2-0: ADC clock selection (0,0,0 = Fosc/2) (don't care)
    ; . . . . 0 0 0 0  ANS3-0: ADC input pin analogue selection (1 = analogue)
    ;
    ; Important! ANS3-0 are all 1 after a reset; this enables analogue input
    ; behaviour on those pins (and disables digital functions) by default.
    ; These bits MUST be cleared by firmware if the pins are to be used for
    ; digital functions.
    ;
    ;-------------------------------------------------------------------------------
    ; TIMEKEEPING
    ;
    ; The following frequencies assume that the internal oscillator frequency is
    ; exactly 4.000 MHz so the instruction cycle period is exactly 1 us.
    ;
    ; Timer 0 is an 8-bit free-running UP-counter. It is clocked from the 1 MHz
    ; (nominal) instruction clock prescaled by four, and wraps around every 256
    ; clocks, i.e. every 1.024 ms (nominally). Each wraparound sets the T0IF flag
    ; in INTCON, which is detected and cleared by WaitTick. The fan run-on period
    ; is timed using the AC mains-frequency signal on the SAC pin, for accuracy.
    ; The mains frequency is detected (50 and 60 Hz are supported) by measuring
    ; the interval between mains cycles on SAC using timer 0.
    ;
    ; Nominal (ideal) frequencies are:
    ;
    ; Fosc                              4.0 MHz
    ; Fosc/4                            1.0 MHz
    ; Instruction cycle period          1.0 us
    ; Timer 0 prescaler output period   4.0 us
    ; Timer 0 clock input period        4.0 us
    ; Timer 0 wraparound interval       1.024 ms
    ; Tick interval                     1.024 ms
    ; TickCount increment interval      1.024 ms
    ; TickCount wrap-around interval    262.144 ms
    ;
    ;-------------------------------------------------------------------------------
    ; OPERATION AND USE
    ;
    ; This code controls a PIC16F675 microcontroller in a circuit that is connected
    ; to an AC mains wall switch for a load such as a light or fan. Its purpose is
    ; to delay the turn-off of the load by a selectable period of time.
    ;
    ; The circuit is specifically designed to work without a Neutral connection,
    ; and gets its operating power from the current flowing in the load. In other
    ; words, it is connected in series with the load.
    ;
    ; When the wall switch is first turned ON, the circuit powers up and this code
    ; starts running. It asserts its FEN (fan enable) output, which enables a triac
    ; that is connected across the wall switch. This keeps the load powered, and the
    ; circuit running. The user then turns the wall switch off, and may turn it ON
    ; and OFF again one or more times to select longer run-on times. When the wall
    ; switch has been OFF for a short time, the circuit enters run-on mode, where
    ; it keeps the fan running for a period of time. Then it drops its FEN output,
    ; the triac turns OFF, and the load and the circuit lose power.
    ;
    ; The wall switch must be a changeover type; when it is in the OFF position, AC
    ; voltage is switched to the OFF contact and is detected by firmware on the SAC
    ; input. Firmware uses this signal as the timing source for the turn-off delay.
    ;
    ; The fanrunon firmware can be controlled through the wall switch. It also
    ; announces its status through a piezo transducer which is driven from the PZO
    ; output from the MCU, and can produce beeps at two frequencies (low frequency,
    ; about 244 Hz, and high frequency, about 488 Hz). This is how to use it.
    ;
    ; 1. When the wall switch is initially switched ON, the load and the fanrunon
    ;    circuit power up. Firmware asserts FEN so it will not lose power when the
    ;    wall switch is turned OFF. Firmware monitors the wall switch state using
    ;    the SAC input.
    ; 2. The user can switch the wall switch ON and OFF again, one or more times,
    ;    as long as those timeouts are not exceeded.
    ; 3. As long as the wall switch does not remain OFF for a time determined by
    ;    HoldT16, nor ON for a time determined by CroakT16, firmware keeps FEN
    ;    active and continues to monitor the wall switch for more ON-OFF actions.
    ; 4. Each time the wall switch is changed from ON to OFF, firmware issues a
    ;    confirmation beep at low pitch with a duration determined by BD_SwAck.
    ; 5. This allows the user to choose the run-on time as one of the predefined
    ;    periods ROT1, ROT2, etc. One ON-to-OFF transition selects ROT1, two
    ;    ON-to-OFF transitions select ROT2, etc. More than the maximum ROTn number
    ;    is translated to ROT0, which is a ten second delay that can be used when
    ;    testing.
    ; 6. When the wall switch has remained OFF for a time determined by HoldT16,
    ;    firmware enters the run-on phase. It first issues a string of beeps at
    ;    low pitch with a duration of BD_Annc, the number of beeps being equal to
    ;    the number of ON-to-OFF transitions on the wall switch (or eight if more
    ;    than the highest valid number of transitions occurred), then it begins
    ;    counting down the time from the chosen ROTn value.
    ; 7. When the selected time expires, firmware issues a low-pitched beep of
    ;    duration BD_Croak, deasserts its FEN output, and enters an endless loop.
    ;    The load, and the circuit, will power down.
    ; 8. If at any time (including during the run-on period) the wall switch is
    ;    turned ON and left ON for longer than the period set by CroakT16, firmware
    ;    jumps to Croak - it issues a low-pitched beep with duration BD_Croak,
    ;    deasserts FEN, and enters an endless loop. The load and the circuit will
    ;    remain powered while the wall switch is ON, but when it is turned OFF,
    ;    everything will power down. This allows the user to force fanrunon to
    ;    "but(t) out" and leave the load controlled by the wall switch alone.
    ;
    ;-------------------------------------------------------------------------------
    
            list        p=12F675
            #include    P12F675.INC
    
    ; The next line specifies settings to be programmed into the device's fuse bits.
    
        __CONFIG   _CPD_OFF & _CP_OFF & _BODEN_ON & _MCLRE_OFF & _PWRTE_ON & _WDT_OFF & _INTRC_OSC_NOCLKOUT
    
    ;-------------------------------------------------------------------------------
    ; MACROS
    
    movlwf  MACRO   k,f         ; Move literal via W to file register
            movlw   k
            movwf   f
            ENDM
    
    ;-------------------------------------------------------------------------------
    ; SYSTEM EQUATES
    
    ; T16 values - counts of tick16s (16-tick increments, 16.384 ms nominal; 61 per second)
    
    HoldT16     EQU     122     ; Timeout on wall switch OFF to enter run-on mode
    CroakT16    EQU     214     ; Timeout on wall switch ON to croak
    
    ; BD_ values - counts of tick4s (4-tick increments, 4.096 ms nominally; 244 per second)
    ; specifying durations of beeps and inter-beep delays.
    
    BD_Alarm    EQU     30      ; Alarm beep (reporting a Panic failure)
    BD_SwAck    EQU     50      ; Switch ON-to-OFF acknowledgement beep
    BD_Annc     EQU     36      ; Announcement beep
    BD_Croak    EQU     180     ; Long beep that indicates entering the Croak state
    
    ; Beep counts for error reporting (Panic code)
    
    BC_AlmBOD   EQU     3       ; Number of beeps for a brown-out
    BC_AlmMns   EQU     4       ; Number of beeps for mains frequency could not be determined
    
    ; Other
    
    SACTimeout  EQU     25      ; Number of ticks for timeout on AC cycle on SAC input
    
    ; Run-on times, in seconds, for 1, 2, 3, 4 and "anything else" numbers of
    ; ON-to-OFF transitions on the wall switch, are defined later in the section
    ; of code following ROTXlat.
    
    ;-------------------------------------------------------------------------------
    ; RAM ALLOCATION
    ;
    ; Normally, uninitialised data is declared in a UDATA block, but because this
    ; program is an absolute (non-relocatable) module, MPASM doesn't allow me to
    ; use UDATA; CBLOCK must be used instead. Equates for bit numbers are listed
    ; after the ENDC directive because, unlike UDATA, a CBLOCK cannot include other
    ; types of directives.
    
            CBLOCK  0x0020  ; Start of the 64-byte RAM area in the register file
                            ; All RAM locations are initialised to 0x00 at startup
    
    ; Used by various:
    
    Flags0      :1  ; Miscellaneous bitflags (see below for allocation)
    
    MiscR       :1  ; Miscellaneous general register
    
    ; Used by WaitTick:
    
    TickCount   :1  ; Counter for ticks, incremented every 1.024 ms (nominally)
    
    BeepTimer   :1  ; Countdown timer for beep and inter-beep delay durations
                    ; 0x00 = done; 0xFF is not allowed; other values are countdown.
                    ; BeepTimer is decremented on every fourth tick by WaitTick
                    ; until it reaches zero. When it is zero, WaitTick sets
                    ; PiezoBits to 0x00 to stop any beep in progress.
    
    PiezoBits   :1  ; Complement of bit pattern to send to piezo on PZO (GPIO bit 5)
    
    ; Used by SACMon:
    
    SACInt      :1  ; Tick counter for interval between SAC cycles
                    ; SACInt is initialised to 0xFF at startup. It is incremented
                    ; by SACMon on every tick, but not allowed to wrap around from
                    ; 0xFF to 0x00. It is zeroed when a cycle (three consecutive
                    ; high samples on SAC) is detected. It is used to measure the
                    ; interval between consecutive cycles so that the AC mains
                    ; frequency can be determined, and as a timeout to detect when
                    ; the AC signal at SAC has disappeared.
    
    SACHigh     :1  ; Tick counter for consecutive samples with SAC high
                    ; Incremented once per tick if SAC is high, set to 0xFF (and
                    ; will not wrap around) when a cycle has been detected, and
                    ; kept at 0x00 while SAC is low.
    
    ACCycles    :1  ; During AC mains frequency detection (ACFK=0), this variable
                    ; is incremented for every AC cycle that fits within the range
                    ; for 50 Hz and decremented for 60 Hz. When it reaches a high
                    ; enough positive or negative value, the frequency has been
                    ; determined. It is used as a counter for cycles within each
                    ; second; each time it reaches 50 or 60 (as appropriate), it
                    ; is reset, and SecondsL/H is decremented.
    
    SecondsL    :1  ; Lobyte of seconds down-counter during run-on period
    SecondsH    :1  ; Hibyte
    
    ; Used by WSWMon:
    
    SwTimer     :1  ; Count of tick16s for wall switch in stable position
    SwActs      :1  ; Counter of wall switch activations (changes from ON to OFF)
    
            ENDC    ; End of vars
    
    PanBC   EQU     SwTimer     ; Reuse SwTimer as a beep counter in Panic
    
    ; Flags0 bits:    7 6 5 4 3 2 1 0
    ACFK    EQU 7   ; * . . . . . . .  ACFK: AC mains frequency known
    AC50    EQU 6   ; . * . . . . . .  AC50: AC mains frequency: 1 = 50 Hz; 0 = 60 Hz
    SACP    EQU 5   ; . . * . . . . .  SACP: AC is present on SAC input (wall switch is in the OFF position)
    LSP     EQU 4   ; . . . * . . . .  LSP: Last known value of SACP in WSWMon
    HLDG    EQU 3   ; . . . . * . . .  HLDG: In run-on mode (prevents WSWMon beeping if wall switch goes ON-to-OFF)
                    ; . . . . . * * *  Unused
    
    ; PiezoBits values:       7 6 5 4 3 2 1 0
                            ; 0 0 0 0 0 0 0 0  = silent
    BF_High     EQU 0xAA    ; 1 0 1 0 1 0 1 0  = high-pitched beep (~488 Hz) (errors)
    BF_Low      EQU 0xCC    ; 1 1 0 0 1 1 0 0  = low-pitched beep (~244 Hz) (status info)
    
    ;===============================================================================
    Main        CODE
    
            ORG     0x0000              ; Fixed location for startup after reset
    
    Main0:
    
            clrf    STATUS              ; Writes STATUS register in either bank; selects bank 0
            clrf    INTCON              ; Initialise INTCON with all interrupts disabled
            clrf    PCLATH              ; Make sure high bits of PC will be zero
            goto    Main1               ; Continue elsewhere
    
    DummyISR:                           ; Dummy ISR at 0x0004
    
            goto    Main0               ; An interrupt occurred! Interrupts aren't used!
    
    ;===============================================================================
    ; ROTXlat - run-on time lookup table
    ;
    ; This subroutine performs a table lookup. The table contains the lobytes and
    ; hibytes of the run-on times, in seconds, for each of the possible counts in
    ; SwActs. The first two entries in the table are the lobyte and hibyte of the
    ; run-on time, in seconds, corresponding to SwActs = 0; the next two entries
    ; correspond to SwActs = 1, and so on.
    ;
    ; An invalid number of switch activations is translated to 0 and will select
    ; the first entry, a run-on time of 10 seconds, which is useful for testing.
    ;
    ; This table uses a calculated goto, where the W register is added to PCL.
    ; PCLATH is always set to 0x00. Therefore this table must be within the first
    ; 256 words of the Flash ROM.
    
    NROTs   SET     0                   ; Number of entries - none yet
    
    ROTVal  MACRO   nseconds            ; Macro to generate two retlw instructions for lobyte and hibyte of a value
            retlw   (nseconds) & 0xFF   ; Lobyte
            retlw   (nseconds) >> 8     ; Hibyte
    NROTs   SET     NROTs + 1           ; Count entry
            ENDM
    
    ROTXlat:
    
            addwf   PCL,F               ; Branch forwards according to value in W to get lobyte or hibyte of seconds count
    
            ROTVal  ( 0 * 60) + 10      ; Invalid count of ON-to-OFF transitions: 10 seconds
            ROTVal  ( 5 * 60) +  0      ; One   ON-to-OFF transitions:  5 minutes
            ROTVal  (10 * 60) +  0      ; Two   ON-to-OFF transitions: 10 minutes
            ROTVal  (15 * 60) +  0      ; Three ON-to-OFF transitions: 15 minutes
            ROTVal  (20 * 60) +  0      ; Four  ON-to-OFF transitions: 20 minutes
    
            IF      (($ - Main0) > 256)
            ERROR   "Hey! Wait, what? The end of the ROTXlat table is beyond the first 256 program words!"
            ENDIF
    
    ;===============================================================================
    ; SUPPORT FUNCTIONS
    ;
    ;-------------------------------------------------------------------------------
    ; WaitTick
    ;
    ; Wait for a timer 0 overflow (tick) (occurs every 1.024 ms nominally).
    ; Increment tick counter.
    ; Update variables and I/O pin for piezo transducer.
    ; Decrement BeepTimer unless it's already zero.
    ; If BeepTimer is zero, set PiezoBits to 0x00 to stop any beep in progress.
    ;
    ; The Timer 0 clock comes from the instruction clock frequency (Fosc / 4)
    ; (1 MHz nominal) which is then divided by a prescale divisor of 4 to produce
    ; a Timer 0 clock frequency of 250 kHz (nominal), a period of 4 microseconds.
    ; Timer 0 is an 8-bit up-counter and therefore wraps around and sets the T0IF
    ; flag every 1024 microseconds (nominally) - about 977 times per second.
    ;
    ; TickCount is incremented.
    ; BeepTimer may be decremented and PiezoBits may be modified or cleared.
    ; W is destroyed.
    ;
    ; Stack usage: 1 (the call to the subroutine)
    ;
    ; Instruction cycles: 25 assuming a tick has occurred, or longer otherwise.
    
    WaitTick:
    
            btfss   INTCON,T0IF         ; Has timer 0 wrapped around?
            goto    WaitTick            ; If not, keep waiting in a tight loop
    
            bcf     INTCON,T0IF         ; Clear T0IF flag
    
    ; Increment TickCount
    
            incf    TickCount,F         ; Increment tick counter
    
    ; Update piezo output on PZO (GPIO bit 5) by doing an 8-bit rotate on PiezoBits
    
            clrc                        ; Clear carry
            rlf     PiezoBits,F         ; Get top bit of PiezoBits to carry
            btfss   STATUS,C            ; Skip next instruction if carry set
            bsf     GPIO,PZO            ; If carry was 0, set PZO high
            btfsc   STATUS,C            ; Skip next instruction if carry clear
            bcf     GPIO,PZO            ; If carry was 1, clear PZO to low
            btfsc   STATUS,C            ; Skip next instruction if carry clear
            bsf     PiezoBits,0         ; If carry was set, bit rotated in should have been 1
    
    ; On every fourth tick, decrement BeepTimer if it's not zero
    
            movf    TickCount,W         ; Get tick count
            andlw   0x03                ; Is it a multiple of 4?
            skpz                        ; If so, continue
            return                      ; If not, nothing more to do
            tstf    BeepTimer           ; Is BeepTimer zero?
            skpz                        ; If so, don't decrement it
            decf    BeepTimer,F         ; If not, decrement BeepTimer
    
    ; If BeepTimer is zero, clear PiezoBits to stop a beep in progress.
    
            skpnz                       ; If not zero, skip instr
            clrf    PiezoBits           ; If BeepTimer is zero, stop any beep in progress
            return                      ; Another tick has occurred.
    
    ;-------------------------------------------------------------------------------
    ; BeepWait
    ;
    ; Wait for a specified duration during a beep or a silence.
    ;
    ; This function calls SACMon, but does not call WSWMon. At the times when it is
    ; used, that function isn't needed. Also, WSWMon could itself call BeepWait!
    ;
    ; Before calling, set PiezoBits to BF_Low or BF_High to produce a beep, or
    ; leave it at 0x00 for a silent delay.
    ;
    ; WaitTick is responsible for decrementing BeepTimer until it reaches zero.
    ;
    ; Call with W = beep or gap duration. Duration will be 4.096 ms (nominal) * W.
    ;
    ; W is destroyed.
    ;
    ; Stack usage: 2 including the call to this subroutine
    
    BeepWait:
    
            movwf   BeepTimer           ; Set up duration of beep
    BW1:    call    WaitTick            ; Wait for tick, do piezo stuff
            call    SACMon              ; Do SAC monitoring etc
            tstf    BeepTimer           ; Beep (or silence) still in progress?
            bnz     BW1                 ; If so, loop
            return
    
    ;-------------------------------------------------------------------------------
    ; SACMon
    ;
    ; This function must be called once after each tick for proper operation of
    ; the AC mains frequency detection and SACP updating functions.
    ;
    ; Monitor the SAC input.
    ; Update SACP (SAC present) flag in Flags0.
    ; Detect, and measure the interval between, half-cycles.
    ; If the AC frequency is unknown (ACFK=0), update ACCycles and try to determine
    ;   it. If it has been detected, set ACFK=1.
    ; If the AC mains frequency is known, count AC cycles in ACCycles and on every
    ;   second's worth of cycles, decrement SecondsL/H if non-zero.
    ;
    ; Monitor the SAC signal coming in on GPIO bit 2. This input will be low unless
    ; the wall switch is in the OFF position, in which case it will have a square
    ; wave (30~70% duty cycle) at the AC mains frequency.
    ;
    ; If SAC is low, clear SACHigh, to reset the count of consecutive high samples
    ; on SAC, and if SACTimeout has elapsed since the last time SAC was high, there
    ; is no AC signal on SAC, so clear the SACP flag in Flags0.
    ;
    ; When a new AC mains cycle has been detected, set SACHigh to 0xFF so the when
    ; SACMon is called next, it will know that SAC has been detected high and will
    ; not think there is a new AC cycle. Then calculate the time since the previous
    ; cycle was detected (in SACInt) and compare it to valid ranges for AC mains
    ; frequencies of 50 Hz and 60 Hz. If it's not within a valid range, ignore it.
    ; If it's valid, set the SACP (SAC present) flag in Flags0 to indicate that
    ; there is a mains signal present at SAC.
    ;
    ; If the AC mains frequency has not yet been detected (indicated by the ACFK
    ; flag in Flags0 being clear), perform detection of the AC mains frequency
    ; using the ACCycles variable. When ACFK=0, ACCycles is incremented for every
    ; cycle that matches the valid range for 50 Hz, and decremented for every cycle
    ; that matches the valid range for 60 Hz. Once it reaches a high enough positive
    ; or negative value that the mains frequency is known with confidence, this code
    ; sets ACFK, indicating that the mains frequency is known, clears ACCycles to
    ; zero, and if the detected frequency is 50 Hz, sets the AC50 flag. With ACFK
    ; set, ACCycles is used as a cycle counter to schedule decrements of SecondsL/H.
    ;
    ; If the mains frequency is known (ACFK = 1), increment ACCycles, which is used
    ; as a counter for AC mains cycles, and based on the AC50 flag, determine if one
    ; second has elapsed. If so, zero ACCycles and decrement the 16-bit seconds
    ; down-counter SecondsL/H unless it's already zero.
    ;
    ; Pseudocode (reverse-translated from assembly code):
    ;
    ; On startup, ACFK = FALSE; AC50 = FALSE; SACInt = 0xFF; SACHigh = 0; ACCycles = 0;
    ;
    ; SACMon {
    ;   ++SACInt (limit to 0xFF);
    ;   if (SAC input == low) {
    ;     SACHigh = 0;
    ;     if (SACInt == SACTimeout)
    ;       SACP = FALSE;
    ;     return;
    ;     }
    ;   if (SACHigh == 0xFF)
    ;     return;
    ;   ++SACHigh (limit to 0xFF);
    ;   if (SACHigh < 3)
    ;     return;
    ;   // AC cycle detected
    ;   SACHigh = 0xFF;
    ;   sacint_minus_15_in_W = SACInt - 15;
    ;   SACInt = 0;
    ;   if (sacint_minus_15_in_W >= 7)
    ;     return;
    ;   SACP = TRUE;
    ;   if (ACFK == FALSE) {
    ;     if (sacint_minus_15_in_W >= 3)
    ;       ++ACCycles;
    ;     else
    ;       --ACCycles;
    ;     if ((ACCycles != 12) && (ACCycles != -15))
    ;       return;
    ;     if (ACCycles == 12)
    ;       AC50 = TRUE;
    ;     ACCycles = 0;
    ;     ACFK = TRUE;
    ;     }//ACFK==FALSE
    ;   ++ACCycles;
    ;   if (ACCycles < (AC50 ? 50 : 60))
    ;     return;
    ;   ACCycles = 0;
    ;   if (SecondsL,H == 0)
    ;     return;
    ;   --SecondsL,H;
    ;   return;
    ;   }//SACMon
    ;
    ; May modify SACInt, SACHigh, ACCycles, SecondsL/H
    ; May modify the SACP (SAC present) flag in Flags0
    ; May set the ACFK (AC frequency known) flag in Flags0
    ; May set the AC50 (AC frequency is 50 Hz) flag in Flags0
    ;
    ; W is destroyed
    ;
    ; Stack usage: 1 (the call to this subroutine)
    
    SACMon:
    
            incfsz  SACInt,W            ; Increment cycle interval with result to W
            movwf   SACInt              ; If incremented value did not wrap around, store it back
    
    ; Test SAC input
    
            btfsc   GPIO,SAC            ; Test SAC input (mains-frequency pulse)
            goto    SAC_H               ; If it's high, continue below
    
    ; SAC is low; clear the SACHigh timer (count of consecutive high samples).
    ; Test whether 25 consecutive ticks (counted using SACInt) have elapsed since
    ; the last cycle was detected; if so, there is no mains signal present at SAC.
    
            clrf    SACHigh             ; SAC is low - clear the high timer variable
            addlw   256-SACTimeout      ; Just had too many samples with SAC low?
            skpnz                       ; If not, skip
            bcf     Flags0,SACP         ; If so, flag no AC present on SAC
            return                      ; No more to do here until SAC reads high.
    
    ; Another tick has elapsed, and SAC was high.
    ; Increment SACHigh and test whether a half-cycle has been detected.
    
    SAC_H:  incf    SACHigh,W           ; Increment counter for consecutive SAC high samples, to W
            skpnz                       ; If no wraparound, continue
            return                      ; If wraparound, SAC is still high from the current cycle - ignore it
            movwf   SACHigh             ; If no wraparound, store back to variable
            addlw   256-3               ; Have we seen three consecutive SAC=1 samples?
            skpc                        ; If so, continue
            return                      ; If not, nothing more to do
    
    ; A new AC mains cycle has been detected. Determine whether the interval since
    ; the last cycle detection is valid for a mains frequency of either 50 or 60 Hz.
    ;
    ; AC mains frequency                      50 Hz       60 Hz
    ; ------------------                     -------     -------
    ; Cycle time (ms)                         20         16.667
    ; Cycle time (ticks) (nominal)            19.53125   16.276041667
    ; 30% of cycle (ticks)(nominal)            5.859375   4.8828125
    ; New cycle detection threshold (ticks)    3          3
    ; Full cycle detection range (ticks)      18~21      15~17
    
            movlwf  0xFF,SACHigh        ; Set SACHigh to 0xFF so we don't try to detect a new cycle until SAC has been 0
            movf    SACInt,W            ; Get interval
            clrf    SACInt              ; Zero it ready for next time
            addlw   256-15              ; Subtract 15
            ; Interval 15~17 (AC mains = 60 Hz): W will now be 0~2
            ; Interval 18~21 (AC mains = 50 Hz): W will now be 3~6
            ; Interval   >21 (invalid period)  : W will now be >6
            addlw   256-7               ; Subtract 7
            ; Interval 15~17 (AC mains = 60 Hz): W will now be 249~251 and carry will be 0
            ; Interval 18~21 (AC mains = 50 Hz): W will now be 252~255 and carry will be 0
            ; Interval <15 or >21 (invalid)    : W will now be   0~248 and carry will be 1
            skpnc                       ; If no carry, continue
            return                      ; Invalid interval; return
            bsf     Flags0,SACP         ; Valid interval - flag that mains is present on SAC
            btfsc   Flags0,ACFK         ; If mains frequency is not known yet, skip
            goto    NoACFC              ; If it's known, don't try to figure it out now
    
    ; A valid AC mains cycle period has been detected, and the mains frequency is
    ; not yet known. Figure out whether the cycle interval corresponds to an AC
    ; mains frequency of 50 Hz or 60 Hz, update ACCycles, and if enough samples have
    ; been detected for this interval, set the mains frequency flag (AC50) and the
    ; mains frequency known flag (ACFK).
    
            addlw   4                   ; Set carry if interval matches 50 Hz
            movlw   0x01                ; Provisionally assume 50 Hz, increment ACCycles
            skpc                        ; Skip if 50 Hz
            movlw   0xFF                ; If 60 Hz, ACCycles will be decremented
            addwf   ACCycles,F          ; Adjust ACCycles upwards (50 Hz) or downwards (60 Hz)
            ; ACCycles:  1 ~  24    Mains frequency probably 50 Hz but not sure yet
            ; ACCycles:    >  24    Mains frequency definitely 50 Hz
            ; ACCycles: -1 ~ -30    Mains frequency probably 60 Hz but not sure yet
            ; ACCycles:    < -30    Mains frequency definitely 60 Hz
            movlw   12                  ; Value for definitely 50 Hz
            xorwf   ACCycles,W          ; Does ACCycles match?
            bnz     Not50               ; If not
            bsf     Flags0,AC50         ; Set AC50
            goto    ACKnwn              ; AC mains frequency is now known
    Not50:  movlw   256-15              ; Value for definitely 60 Hz
            xorwf   ACCycles,W          ; Does ACCycles match?
            skpz                        ; If so, AC frequency is now known
            return                      ; Don't update ACCycles, just return now
    ACKnwn: clrf    ACCycles            ; Zero ACCycles - it will subsequently be used as a cycle counter
            bsf     Flags0,ACFK         ; Remember that we know the AC mains frequency now
    
    ; A valid AC cycle has been detected - increment cycle counter, and seconds if appropriate
    
    NoACFC: incf    ACCycles,F          ; Increment AC cycles count
            movlw   256-50              ; Provisionally assume 50 Hz mains
            btfss   Flags0,AC50         ; If AC50 flag set, keep the 50
            movlw   256-60              ; If AC50 clear, assume 60
            addwf   ACCycles,W          ; Have we counted that many cycles yet?
            skpc                        ; If so, continue
            return                      ; If not, a second hasn't elapsed yet; return
            clrf    ACCycles            ; A second has elapsed - reset sub-second cycle counter
    
    ; Decrement SecondsL/H if not zero
    
            tstf    SecondsL            ; Is SecondsL zero?
            skpnz                       ; If not, skip test of SecondsH
            tstf    SecondsH            ; If lobyte is zero, test hibyte as well
            skpnz                       ; If SecondsH|SecondsL not zero, continue
            return                      ; They're zero - don't decrement them
    
            movlw   0xFF
            addwf   SecondsL,F          ; Subtract 1 from SecondsL (will normally carry unless it's now 0xFF)
            skpc                        ; Skip unless it just went from 0x00 to 0xFF, i.e. borrow is needed
            decf    SecondsH,F          ; If SecondsL went from 0x00 to 0xFF, decrement SecondsH.
    
            return                      ; Done here
    
    ;-------------------------------------------------------------------------------
    ; WSWMon
    ;
    ; This function must be called once after each tick while its functions
    ; (issuing a confirmation beep when the wall switch is turned from ON to OFF
    ; and jumping to Croak if the switch has remained ON for CroakT16) are needed.
    ;
    ; These functions are needed during the main loop (before the wall switch has
    ; been OFF for long enough to start the run-on delay) and during the run-on
    ; period.
    ;
    ; Detect change in wall switch state in SACP flag using LSP and update LSP to
    ; the current state of SACP. On any change, reset SwTimer. On a change from ON
    ; to OFF (SACP change from 0 to 1), start a short low-frequency beep to provide
    ; feedback that the wall switch was turned OFF, and increments SwActs. SwActs
    ; is a counter for ON-to-OFF transitions on the wall switch; it determines the
    ; fan run-on delay.
    ;
    ; Every 16 ticks (16.384 ms nominally), increment SwTimer without letting it
    ; wrap around from 0xFF to 0x00. This measures the amount of time that the wall
    ; switch has remained in the same state for. It is reset whenever SACP changes
    ; state by the code above this code.
    ;
    ; If the user leaves the wall switch ON steadily for more than a few seconds,
    ; this is a signal that fanrunon should disable itself and leave the control
    ; of the fan to the wall switch alone. If the wall switch is ON and SwTimer
    ; reaches CroakT16, go to Croak. (See comments around Croak: for details.)
    ;
    ; Updates LSP (last known SACP value) in Flags0
    ; May clear and/or increment SwTimer
    ; May increment SwActs
    ; May start a beep by setting PiezoBits and BeepTimer
    ; May jump to Croak and never return
    ;
    ; W is destroyed.
    ;
    ; Stack usage: 1 (the call to this subroutine)
    
            IF      (LSP != (SACP - 1))
            ERROR   "LSP must be one bit right of SACP"
            ENDIF
    
    WSWMon:
    
            rrf     Flags0,W            ; Get flags shifted right one bit into W
            xorwf   Flags0,W            ; XOR the LSP flag with the SACP flag
            andlw   1 << LSP            ; Mask off other bits
            skpz                        ; If no change, skip next instruction
            clrf    SwTimer             ; If change, reset switch steady state timer
    
            xorwf   Flags0,F            ; If they differ, toggle LSP so it follows SACP
            andwf   Flags0,W            ; Has switch just gone from ON to OFF (SACP 0->1)?
            btfss   Flags0,HLDG         ; If we're in run-on mode, don't beep - skip to the goto NoConf.
            skpnz                       ; If switch just went OFF, skip next instruction
            goto    NoConf              ; If switch didn't just go OFF, skip confirmation beep stuff
    
            incf    SwActs,F            ; Increment count of switch activations
            movlwf  BF_Low,PiezoBits    ; Low frequency beep please
            movlwf  BD_SwAck,BeepTimer  ; Start a beep of BD_SwAck duration
    
    NoConf:
            movf    TickCount,W         ; Get tick count
            andlw   0x0F                ; Is it a multiple of 16?
            skpz                        ; If so, continue
            return                      ; Not a multiple of 16
    
            incfsz  SwTimer,W           ; Increment timer for wall switch steady state
            movwf   SwTimer             ; If incremented value is not zero, store it back
    
            btfsc   Flags0,SACP         ; Only check for croak timeout if wall switch is ON
            return                      ; Wall switch is OFF - nothing more to do here
    
            movf    SwTimer,W           ; Was wall switch ON long enough to croak?
            addlw   256-CroakT16
            skpc                        ; If so, croak!
            return                      ; If not
            goto    Croak
    
    ;===============================================================================
    ; MAINLINE
    
    Main1:
    
            movlwf  GPIO_initvalue,GPIO ; Initialise port output states, including FEN = ON
    
    ; Initialise registers in bank 1
    
            bsf     STATUS,RP0          ; Select register bank 1
            errorlevel -302             ; Tell assembler not to warn about bank 1 registers being accessed
            call    0x03FF              ; Get factory oscillator calibration value
            movwf   OSCCAL              ; Store to oscillator calibration register
            movf    PCON,W              ; Read PCON for later use
            movwf   MiscR               ; Save it in MiscR
            movlwf  TRISIO_initvalue,TRISIO ; Initialise tristate control for I/O pins
            clrf    ANSEL               ; Disable analogue input functions on all I/O pins
            movlwf  OPTIONREG_initvalue,OPTION_REG ; Initialise timer 0 controls and misc
            clrf    PIE1                ; Disable various interrupt sources
            clrf    IOC                 ; Disable all interrupt-on-change sources
            errorlevel +302             ; Warnings back on for bank 1 accesses
    
    ; Switch back to register bank 0 and initialise a few more registers
    
            bcf     STATUS,RP0          ; Back to bank 0 registers from now on
            movlwf  CMCON_initvalue,CMCON ; Disable and power-down the comparator
    
    ; Initialise RAM
    
            movlwf  0x20,FSR            ; Set indirection address to first RAM location
            movf    MiscR,W             ; Get saved PCON value
    ZRAM:   clrf    INDF                ; Zero the indirectly addressed RAM byte
            incf    FSR,F               ; Advance to next RAM location
            btfss   FSR,7               ; If FSR has reached 0x80, continue
            goto    ZRAM                ; If not, loop
    
            decf    SACInt,F            ; Initial value for SACInt is 0xFF.
    
    ; Zero the Timer 0 counting register and clear pending tick flag
    
            clrf    TMR0                ; Clear the Timer 0 counting register (it's undefined at reset)
            bcf     INTCON,T0IF         ; Clear T0IF, the timer 0 wraparound (tick) flag
    
    ; From this point onwards, register bank 0 is always selected.
    ;
    ; Test the value previously read from PCON to determine whether we just had a
    ; brown-out reset. If so, generate a distinctive beep sequence to indicate a
    ; problem.
    ;
    ; If a brown-out reset (but not a power-on reset) has occurred, the -POR bit
    ; (PCON bit 1) will be 1, and the -BOR bit (PCON bit 0) will be 0. PCON will
    ; match a mask of xxxxxx10.
    
            andlw   b'00000011'         ; Only interested in -POR and -BOR
            xorlw   b'00000010'         ; Testing for this value
            bnz     MainLoop            ; If different, normal startup - go to main loop
    
    ; Brown-out reset occurred
    
            movlw   BC_AlmBOD           ; Number of beeps to indicate brown-out
    
    ; Panic entrypoint
    ;
    ; Code jumps here with a BC_ number in W when a fatal error occurs.
    ; It generates an alarm sequence - a group of high-pitched beeps then a gap,
    ; repeating indefinitely. After the first group of beeps has been issued, FEN
    ; is turned OFF, so the fanrunon circuit can be shut down by turning the wall
    ; switch OFF.
    
    Panic:
    
            movwf   PanBC               ; Keep beep count
    
    ; This code issues a specified number of high-pitched beeps, with gaps between,
    ; then a longer delay. Then it turns off FEN, so the unit can be shut down by
    ; turning the wall switch OFF, and loops back to Panic1. So it will issue the
    ; string of beeps followed by a gap, continuously until powered down.
    ;
    ; On entry at Panic, W contains the number of beeps. This is stored in PanBC,
    ; which is equated to SwTimer, a variable used by WSWMon that is not relevant
    ; if a fatal error has occurred, and will not be modified because WSWMon will
    ; not be called any more.
    ;
    ; For each beep, PiezoBits is first set to BF_High, to start a beep sounding,
    ; then BeepWait is used to set the beep duration in BeepTimer and wait for it
    ; to expire (WaitTick, called inside the wait loop in BeepWait, decrements
    ; BeepTimer once every four ticks, and clears PiezoBits to zero when BeepTimer
    ; is zero). Once the beep has finished, BeepTimer is called again, but PiezoBits
    ; is left at 0x00 so no beep is generated. BeepWait counts down the time again.
    ; This is repeated the specified number of times (the counter is kept in
    ; MiscR). At the end, an extra delay is generated by setting W to four times
    ; the normal beep or gap duration and calling BeepWait once more.
    ;
    ; Once the full sequence of beeps has been generated, FEN is turned OFF, so the
    ; user can power down the fantimer unit by switching the wall switch OFF. If
    ; power remains ON, the code loops back and repeats the beep sequence and delay
    ; indefinitely.
    
    Panic1: movf    PanBC,W             ; Get beep count
            movwf   MiscR
    Panic2: movlwf  BF_High,PiezoBits   ; Start a high-pitched beep
            movlw   BD_Alarm            ; Get duration
            call    BeepWait            ; Wait for beep to expire
            movlw   BD_Alarm            ; Get duration again
            call    BeepWait            ; Wait for an equal silent delay
            decfsz  MiscR,F             ; Count down beep count
            goto    Panic2              ; If more, loop
            movlw   BD_Alarm*4          ; Longer delay between blocks of beeps
            call    BeepWait            ; Wait
    
            bcf     GPIO,FEN            ; Disable the fan switch bypass so we can be shut down after the first alert
            goto    Panic1              ; Loop until powered down
    
    
    ; Loop waiting for the wall switch to be OFF long enough to enter the run-on delay phase
    
    MainLoop:
    
            call    WaitTick            ; Wait for tick, do piezo stuff
            call    SACMon              ; Monitor SAC input
            call    WSWMon              ; Monitor wall switch timeouts
    
    ; Detect whether the wall switch has been OFF for at least HoldT16.
    ; If so, enter the run-on delay state.
    
            movf    SwTimer,W           ; How long has the wall switch been in its current state?
            btfss   Flags0,SACP         ; If wall switch is OFF (SACP = 1), use this value
            clrw                        ; If wall switch is ON (SACP = 0), don't detect a HoldT16 timeout
    
            addlw   256-HoldT16         ; Was wall switch OFF long enough to enter the run-on state?
            bnc     MainLoop            ; If not, keep waiting
    
    ; The wall switch has been OFF long enough to start the fan run-on delay.
    ; The mains frequency should be known by now. If it isn't, there's a problem.
    
            movlw   BC_AlmMns           ; Number of beeps to indicate mains frequency unknown
            btfss   Flags0,ACFK         ; If AC frequency known, skip
            goto    Panic               ; If not known, freak out.
    
            bsf     Flags0,HLDG         ; Set HLDG flag so WSWMon won't beep any more
    
            movf    SwActs,W            ; Get count of wall switch activations
            addlw   256-NROTs           ; This will set carry if SwActs is >= NROTs
            skpnc                       ; If SwActs is valid, skip
            clrf    SwActs              ; If not valid, force it to zero
    
    ; SwActs is now in the range 0 to (NROTs-1) inclusive.
    ; Announce the number of ON-to-OFF transitions that have been counted
    
            movf    SwActs,W            ; Get number of switch activations or 0 if invalid
            skpnz                       ; If count is valid, skip
            movlw   8                   ; If invalid number of activations, announce 8
            movwf   MiscR               ; Store as counter
    
    AnnLp:  movlwf  BF_Low,PiezoBits    ; Start a low-pitched beep
            movlw   BD_Annc
            call    BeepWait            ; Wait for beep to complete
            movlw   BD_Annc             ; Set equal duration for silence
            call    BeepWait            ; Wait for silence to complete
    
            decfsz  MiscR,F             ; Count down beeps
            goto    AnnLp               ; If more beeps to do, loop
    
    ; Look up the appropriate run-on time using ROTXlat table (earlier) indexed by SwActs * 2
    
            clrc                        ; Clear carry
            rlf     SwActs,W            ; Get 2 * SwActs + 0 to W
            call    ROTXlat             ; Translate to get lobyte of seconds count
            movwf   SecondsL            ; Store as lobyte of seconds down-counter
    
            setc                        ; Set carry
            rlf     SwActs,W            ; Get 2 * SwActs + 1 to W
            call    ROTXlat             ; Translate to get hibyte of seconds count
            movwf   SecondsH            ; Store as hibyte of seconds down-counter
    
    HoldLoop:
    
            call    WaitTick            ; Wait for tick, do piezo stuff
            call    SACMon              ; Monitor SAC input
            call    WSWMon              ; Monitor wall switch timeouts
    
            movf    SecondsL,W          ; Get lobyte of Seconds down-counter
            iorwf   SecondsH,W          ; OR with hibyte to test for zero
            bnz     HoldLoop            ; If still counting, loop
    
    ; The Croak code is entered to make the fanrunon circuit disable itself and
    ; leave the fan under the sole control of the wall switch.
    ;
    ; It issues a long low-pitched beep, turns the switch bypass OFF (FEN = 0), and
    ; enters an endless loop, so that when the user turns the wall switch OFF, the
    ; fan will turn OFF and the circuit will lose power.
    
    Croak:
    
            movlwf  BF_Low,PiezoBits    ; Low frequency beep please
            movlw   BD_Croak            ; Start a beep of BD_Croak duration
            call    BeepWait            ; Issue the beep and wait for it to end
            bcf     GPIO,FEN            ; Turn off the switch bypass
            goto    $                   ; Croak - go gaga - loop forever
    
    _CODESIZE:
    
            end
    
chopnhack and hevans1944 like this.
Electronics Point Logo
Continue to site
Quote of the day

-