; Application for a simple Arduino watchdog and reset.
;
; This app watches the "blinking LED" on the Arduino. If it stops
; blinking, then it has likely hung up on the Ethernet device and
; requires a reset.
;
; This is a stripped-down version of the misc1-board, so we'll
; use the same pinouts.
;
; =====
;
; PIN ASSIGNMENTS
;
;   A0 - for testing: an LED output -- a RESET occurred
;   A1 - for testing: an LED output -- follows ALIVE
;   A3 - watchdog/alive  (input; read-only)
;   A4 - Arduino reset pin  (output)
;
; Note: A4 should have an external pull-up so that, during the HIGH-Z
; condition of chip initialization we will not reset the Arduino.
; Maximum sink current is 200 mA; a 10 kΩ resistor would sink just
; 0.5 mA -- well within the maximum.
;
; Note: the target Arduino board is the old "Arduino Ethernet"
;   https://docs.arduino.cc/retired/boards/arduino-ethernet-rev3-without-poe
; which uses the ATMega328 microcontroller. See the datasheet, section
; "System and Reset Characteristics". t(RST) is 2.5 µs (for all variants
; of the 328 that I've found).
;
; =====
;
; OPERATION SEQUENCE
;
; Count the number of 32.8 ms Timer0 rollovers. Each time we see the
; "alive" signal on the Arduino (the LED) change state (blink), then
; reset the counter. If too many rollovers occur, then the Arduino
; is likely hung up, and requires a reset.
;
; =====
;
;


        list p = 16f688
        include p16f688.inc

        constant USING_WAIT_MACROS = 1
        include ../include/handy.inc

        __config DEFAULT_CONFIG

; Add a little bit more code, for testing purposes. Attach an LED
; to these PORTA pin. Comment them out for production.
 constant TESTING_LED1 = 0
 constant TESTING_LED2 = 1

; Define our various pins
 constant ALIVE = 3             ; PORTA, input
 constant ARD_RESET = 4         ; PORTA, output

; Define this module's variables
.gpr0   udata

; Arduino watchdog timer counter. Each count represents 32.8ms on TMR0.
WATCHDOG res    1
 constant WDOG_COUNT = D'100'   ; 3.28 seconds

; Initialize the WATCHDOG counter.
init_wdog_counter:      macro
        movlw   WDOG_COUNT
        movwf   WATCHDOG
        endm

; At power-on/reset, the Arduino takes some time to begin flashing
; its alive signal. Let's ignore the first 3 watchdog trips.
; Thus: 3 * 3.28 == 9.84 seconds for startup.
IGNORE  res     1
 constant IGNORE_COUNT = 3

; Last value we saw on the ALIVE pin (bitmask)
ALIVE_LAST res  1
 constant ALIVE_MASK = (1 << ALIVE)


.reset: org  0x0000
        goto main


.main:  code                    ; relocatable
main:

        disable_comparators
        ; Now in Bank 0

        ; Latch ARD_RESET as HIGH, so we don't reset the Arduino during
        ; the setup process.
        bsf     PORTA, ARD_RESET

        ; We need zeroes in ALIVE_LAST. If the mask bit flips on the first
        ; read: no big deal. It will just reset the counter right away.
        clrf    ALIVE_LAST

 ifdef TESTING_LED1
        ; NOTE: PORTA is still an INPUT, so any read-modify-write will
        ; toss previous bits that have been set. Thus, we need to write
        ; the entire desired value to PORTA.

        ; For testing purposes, turn an LED on when we perform a reset.
        ; It will remain on. Just power-reset the PIC to turn it off.
        ; We will hook the LED to TESTING_LED1.
        ; ### for testing: active HIGH

        ; TESTING_LED2 will mirror the blink from the Arduino
        ; ### for testing: active LOW

        movlw   (1 << ARD_RESET) | (0 << TESTING_LED1) | (1 << TESTING_LED2)
        movwf   PORTA
 endif

        bank1s
        run_at_8MHz

        ; Ensure AN* are configured as DIGITAL pins (default is ANALOG).
        clrf    ANSEL

        ; At reset time, all ports are INPUT/HIGH-Z. Turn RA4 (ARD_RESET)
        ; into an OUTPUT.
        bcf     TRISA, ARD_RESET

 ifdef TESTING_LED1
        ; Set the LED pins to be OUTPUTs.
        bcf     TRISA, TESTING_LED1
        bcf     TRISA, TESTING_LED2
 endif

        ; Prepare the OPTION_REG register:
        ;   ~RAPU:  x    (don't care; default is 1)
        ;   INTEDG: x    (don't care; default is 1)
        ;   T0CS:   0    TIMER0 uses internal instruction clock cycle
        ;   T0SE:   x    (don't care; default is 1)
        ;   PSA:    0    prescaler assigned to TIMER0
        ;   PS<2:0> 111  1:256 scaling, so overflow occurs every 32.8ms
        ;
        ; Note: before untying the prescaler from the WDT (default), the
        ;       WDT should be cleared to avoid an accidental trip.
        clrwdt
        movlw   b'11010111'
        movwf   OPTION_REG

        ; Note: we do not initialize TMR0. Whenever it first fires is
        ; just fine, as it only means a 32.8 ms potential variance
        ; from when we "start" (which is arbitrary to begin with).

        bank0s

        ; Initialize our startup ignore counter. Add 1, as we're about
        ; to decrement it.
        movlw   IGNORE_COUNT+1
        movwf   IGNORE

wait_startup:
        decf    IGNORE

        ; FALLTHRU

reset_counter:
        ; Initialize the WATCHDOG counter.
        init_wdog_counter

        ; FALLTHRU

busy_loop:
        ; ASSUME: Bank 0.

        ; Has TIMER0 overflowed? If so, then we need to decrement
        ; WATCHDOG and possibly take action.
        btfsc   INTCON, T0IF
        goto    timer_overflowed ; bit was set

        ; See if ALIVE has changed value.
        movfw   PORTA
        andlw   ALIVE_MASK
        xorwf   ALIVE_LAST, w

        ; If W (the ALIVE bit) is zero, then no change has occurred.
        ; Keep looking for time overflows and changes.
        bz      busy_loop

        ; ALIVE has changed state (w == ALIVE_MASK). Use this value
        ; to toggle ALIVE_LAST.
        xorwf   ALIVE_LAST, F

 ifdef TESTING_LED2
        ; Have TESTING_LED2 follow the ALIVE pin value (in ALIVE_LAST).
        ; Just clear the bit, then set if necessary. This happens in
        ; microseconds, so no worries if it turns off/back-on.
        bcf     PORTA, TESTING_LED2
        tstf    ALIVE_LAST
        skpz
        bsf     PORTA, TESTING_LED2
 endif

        ; Reset the WATCHDOG counter, and keep watching.
        goto    reset_counter


timer_overflowed:
        ; ASSUME: Bank 0.

        ; Note that the Timer0 Interrupt Flag (T0IF) gets set upon
        ; overflow. Interrupts are not enabled, so the flag gets set
        ; and nothing more happens. But we need to clear it, so that
        ; we can look for it getting raised at the next overflow.
        bcf     INTCON, T0IF

        ; We count from WDOG_COUNT down to zero. If this hits zero,
        ; then we assume the Arduino is stuck, and will reset it.
        decfsz  WATCHDOG, F
        ; Hasn't reached zero. Keep looking for activity.
        goto    busy_loop

        ; Too long without activity. MAYBE time for a reset! If IGNORE
        ; has hit zero, then do the reset. Otherwise, decrement it,
        ; and keep looking for activity.
        tstf    IGNORE
        bnz     wait_startup

        ; Pull ARD_RESET LOW. This sinks current through the external
        ; pull-up (maximum sink is 200 mA total).
        bcf     PORTA, ARD_RESET

        ; ATMega328's t(RST) is 2.5 µs, so hold the pin LOW for 6 instruction
        ; cycles (0.5 µs each) to produce a 3 µs external-reset signal.
        wait_6

        ; Return to HIGH state.
        bsf     PORTA, ARD_RESET

 ifdef TESTING_LED1
        ; ### for testing: active HIGH
        bsf     PORTA, TESTING_LED1
 endif

        ; Reset the WATCHDOG counter, and hope the reset worked.
        goto    reset_counter


; stupid assemblers don't realize EOF means *end*
        end
