Wednesday, February 05, 2014

Trailer Lights Version 3.1 Software

The electronic hardware for the improved trailer lights was described in the last post. I got the boards in and soldered it together. It works! Now it needs some software. I chose a Microchip PIC12F609 as the brains.

I was surprised how little example PIC code I found that included interrupt handling and I found no usable example code in assembly language. I also got tripped up on the oscillator shutting down during sleep. I know it makes sense to save power, but it makes waiting for a given time period impossible while in sleep mode. Further, since the interrupt returns to the command that was loaded when the interrupt occurs, I can't get away with something simple like
 goto $ ; wait for interrupt
 commands to do after interrupt
because the commands to do after the interrupt will never occur. I know that isn't the traditional way to use an interrupt, but in this case it isn't just non-traditional--it's impossible. So I put in a tight two command loop that checks for an interrupt to have happened. It feels like a hack, but it works.

The state diagram this program implements is shown in figure 1. Rather than an entirely interrupt driven approach, I could have interrupted only on the button changes and had the timing as part of the main code. I like this approach better.

Figure 1. State diagram for trailer lights controller.
I always have a bit of trouble explaining code, so here it is. Additional explanation is mixed in with the code in a different font. I am also fighting with Blogger to get it to post code without mangling it. I've got it down to just mangling comments. I'll edit this later if I can figure how to make it mor readable. In the mean time, I'm afraid you'll have to deal with it.

;
; Trailer Boost 3.1
; Bruce McLaren
; nanoDragon LLC
; Bruce.McLaren@nanoDragon.com
;  9 Dec 13 - 5 Feb 14
;
;
; Hardware notes:
;  PIC12F609 runnimg at 32kHz using external crystal in LP mode
;
; GP0 (GP0)   : Output           - Toggles at interrupt    TEST NOT USED IN APP
; GP1 (CIN0-) : Analog, POWFB    - 20% of Trailer Battery Voltage
; GP2 (GP2)   : Output, LIGHT    - A high turns LEDs on
; GP3 (GP3)   : Input,  BUTT     - Button, low when button pressed (external pull-up)
; GP4 (OSC2)  : OSC,             - crystal Oscillator (LP mode)
; GP5 (OSC1)  : OSC,             - crystal Oscillator (LP mode)
;

#include < p12f609.inc >

; LP oscillator: Low-power crystal on GP4/OSC2/CLKOUT and GP5/OSC1/CLKIN
; WDT disabled and can be enabled by SWDTEN bit of the WDTCON register
; PWRT enabled
; MCLR pin function is digital input, MCLR internally tied to VDD
; Program memory code protection is disabled
; BOR disabled
__CONFIG _FOSC_LP       & _IOSCFS_4MHZ & _WDTE_OFF & _PWRTE_ON & _MCLRE_OFF & _CP_OFF & _BOR_OFF

; errorlevel -302  ; suppress "not in bank0" warning

; Constants
LowBatMax     equ .15 ; Number of Low Battery before shutdown: must be <255 comment-255--="">
OnTime        equ .30 ; Minutes LEDs on: must be <255 comment-255--="">
ShortTime     equ  .9 ; Minutes LEDs on if Low Battery Condition: must be <255 comment-255--="">
LongFlash     equ .29 ; Seconds LEDs flash for normal off: must be <59 comment-59--="">
ShrtFlash     equ .12 ; Seconds LEDs flash for low battery off: must be <59 comment-59--="">

; STATE bits
StateInit     equ O'0' 
StateWait     equ O'1'  ; never checked in program
StateLongOff  equ O'2'  ; never checked in program
StateShortOff equ O'3'  ; never checked in program
StatePwrDown  equ O'4'  ; never checked in program

; BUTTON bits
Pressed       equ O'0'  ; Set when button pressed,  cleared after processing never checked in program
Released      equ O'1'  ; Set when button released, cleared after processing

; Variables
CBLOCK 0x40          ; 64 bytes RAM in BANK0
    INTERRUPT        ; FF at end of interrupt routine
    STATUS_TEMP      ; STATUS save during interrupt
    W_TEMP           ; W      save during interrupt
    TENTHS           ; tenths of seconds counter
    SECONDS          ; seconds counter
    MINUTES          ; minutes counter
    LOWBATCNT        ; Low Battery Counts - inc when battery<10v dec="" when="">10V.
                     ;    Saturates at FF and 00. 
    BUTTON           ; Lower two bits contain button information
    STATE            ; Used to aid debugging 
                     ; 00000001 = Init     (STATE, StateInit)
                     ; 00000010 = Wait     (STATE, StateWait)
                     ; 00000100 = LongOff  (STATE, StateLongOff)
                     ; 00001000 = ShortOff (STATE, StateShortOff)
                     ; 00010000 = PwrDown  (STATE, StatePwrDown) Program doesn't set this
ENDC

ORG 0
    goto Init                 ; start
    nop                       ;
    nop                       ;
    nop                       ;
Interrupt:
    ; Save w, status (including BANKSEL) to STATUS_TEMP
    movwf   W_TEMP            ;
    swapf   STATUS, W         ;
    movwf   STATUS_TEMP       ; 



The W and STATUS register save is copied straight from the PIC12F609 data sheet.

    banksel    PIR1              ; Bank0
    btfss    PIR1, TMR1IF      ; was this a timer interrupt?
    goto      ButtCheck         ; if not, skip to button check
    bcf     PIR1, TMR1IF      ; clear timer interrupt
    ; Timer re-seeded with 65536-(32000/10+20)=62316 (F36C)
    bcf      T1CON, TMR1ON     ; TIMER1 disable
    movlw    H'F3'             ;
    movwf     TMR1H             ;
    movlw    H'6C'             ;
    movwf    TMR1L             ;
    bsf      T1CON, TMR1ON     ; TIMER1 enable 


I want an interrupt every 1/10 of second. It is a 32kHz crystal. The timer is disabled for five commands, which consume 20 clock cycles. Checking timing on the oscilloscope shows it to be about 2% too slow. It my really be a 32,768kHz crystal or my oscilloscope that hasn't been calibrated in five years may have drifted a bit. An initial timing error of 2% meets the application requirement so I'm not tuning it.

    ; Increment TENTHS, SECONDS, MINUTES
    ; If (MINUTES, SECONDS, TENTHS) == (255, 59, 9) Then don't inc (saturate)
    movlw    .9               ; Load 9 into W
    subwf    TENTHS, W        ; W = TENTHS - W
    btfss    STATUS, Z        ; If Z clear, don't trap
    goto     IncTime          ; TENTHS not 9, handle normal
    movlw    .59              ; Load 59 into W
    subwf    SECONDS, W       ; Compare W to SECONDS
    btfss    STATUS, Z        ; If Z clear, don't trap
    goto     IncTime          ; SECONDS not 59, handle normal
    movlw    .255             ; Load 255 into W
    subwf    MINUTES, W       ; Compare W to MINUTES
    btfss    STATUS, Z        ; If Z clear, don't trap
    goto     IncTime          ; MINUTES not 255, handle normal
    goto     LowBatCheck      ; Skip increment time


All that to make sure the time saturates even though it should never get to that point. Call it paranoid. If I was running out of program space, I would delete this code.

IncTime:
    incf     TENTHS, F        ;
    movlw    .10              ; Load 10 into W
    subwf    TENTHS, W        ; W = TENTHS - W
   btfss    STATUS, Z       ; If TENTHS!=10 (Z clear) move on, otherwise rollover TENTHS
    goto     LowBatCheck      ;
    clrf     TENTHS           ; Rollover TENTHS
    incf     SECONDS, F       ;
    movlw    .60              ; Load 60 into W (60 seconds in a minute)
    subwf    SECONDS, W       ; Compare W to SECONDS
    btfss    STATUS, Z        ;
    goto     LowBatCheck      ;
    clrf     SECONDS          ; Rollover SECONDS
    incf     MINUTES, F       ;
LowBatCheck:                  ; 
    ; The battery level is checked on the timer interrupt, i.e., every 0.1 seconds
    btfss    CMCON0, COUT      ; check comparator
    goto    BattHigh          ;
;;    bsf     GPIO, GP0         ;;; battery low

Uncommenting the bsf GPIO, GP0 line was useful in debugging the battery voltage check. There is a corresponding bcf line below that also gets uncommented and the xorwf GPIO, F that otherwise toggles the pin every interrupt needs to be commented out. The comparator is not handled well in the simulator. 

    incfsz  LOWBATCNT, F      ;
    goto    ClearTime1Int     ;
    decf    LOWBATCNT, F      ; If it incremented to 0, decrement will re-saturate
    goto    ClearTime1Int     ;
BattHigh:
    movf     LOWBATCNT, F      ;
;;    bcf     GPIO, GP0         ;;; battery high
    btfss   STATUS, Z         ; If LOWBATCNT is already 0, don't dec
    decf       LOWBATCNT, F      ;
ClearTime1Int:
    bcf      PIR1, TMR1IF      ; clear timer1 interrupt
ButtCheck:
    btfss    INTCON, GPIF      ; if not a pin interrupt,
    goto      InterruptFinish   ; skip to InterruptFinish
    btfsc    GPIO, GP3         ;
    bsf     BUTTON, Pressed   ;
    btfss   GPIO, GP3         ;
    bsf     BUTTON, Released  ;
    bcf       INTCON, GPIF      ; clear pin interrupt
InterruptFinish:
    movlw    b'00000001'       ;
    xorwf    GPIO, F           ; Toggle GP0 each interrupt for testing

Toggling the "unused" output made debugging much easier. It helped both with hardware debugging and simulation debugging. 

    clrf    INTERRUPT         ; ensure INTERRUPT clear
    comf    INTERRUPT, F      ; set INTERRUPT to FF
    ; Restore STATUS and return
    swapf    STATUS_TEMP, W   ; Swap STATUS_TEMP into W
    movwf    STATUS           ; Move W into Status
    swapf    W_TEMP, F        ; Swap W_TEMP
    swapf    W_TEMP, W        ; Swap W_TEMP into W
    retfie                    ; Return from interrupt
    nop                       ;

Init:
    banksel    STATE             ; Bank0
    clrf    STATE             ; clear STATE
    bsf     STATE, StateInit  ; STATE = Init
    ; GPIO setup
    clrf    GPIO              ; clear outputs
    banksel ANSEL             ; Bank1
    clrf    ANSEL             ;
    bsf     ANSEL, GP1        ; GP1 is analog
    clrf    TRISIO            ;
    bsf      TRISIO, GP1       ; GP1 is an input
    bsf       TRISIO, GP3       ; GP3 is an input
;    clrf    WPU               ;
;    bsf       WPU, GP3          ; GP3 has weak pull-up NO, IT CANT

As mentioned in the previous post, I messed this up.GP3 cannot have an internal weak pullup unless it used as the pin is configured as MCLR. It is a pretty clear footnote in the data sheet, I just plain missed it. I had to add an external pull-up to the pin.

    clrf    IOC               ;
    bsf     IOC, GP3          ; GP3 interrupts on change
    banksel    INTCON            ; Bank0
    bsf     INTCON, GPIE      ; GPIO Change Interrupt Enable
    ; Timer setup Timer seeded with 65536-(32000/10)=62336 (F380)
    movlw    H'F3'             ;
    movwf     TMR1H             ;
    movlw    H'80'             ;
    movwf    TMR1L             ;
    clrf    T1CON             ; No gate, No prescale, timer1 disable
    bsf     CMCON1, T1ACS     ; FOSC
    clrf    BUTTON            ; clear button
    ; comparator setup (not enabled until after wakeup)
    clrf    CMCON0            ; disabled
    bsf     CMCON0, CMR       ; CMVin+ connects to CMVref output
    bsf     CMCON1, CMHYS     ; hysteresis on
    clrf    VRCON             ; Voltage Reference disabled
    bsf     VRCON, FVREN      ; 0.6V Reference Enable
    ; Interrupt setup
    bsf     INTCON, GIE       ; GPIE already set
InitSleep:
    sleep                     ; wait for pin interrupt

I can use sleep here because this first wait is waiting for the button to be released, there is no timing and no clock. This is the low power wait until someone pushes (and releases) the button state. 

    nop                       ;
    ; Next state logic
    btfss     BUTTON, Released  ;
    goto     InitSleep         ; If button not released, go back to sleep and wait
    bsf     CMCON0, C1ON      ; enable comparator
    bsf     VRCON, FVREN      ; enable voltage reference
    bsf      T1CON, TMR1ON     ; TIMER1 enable

Wait:
    banksel STATE             ; Bank0
    clrf    STATE             ; clear STATE
    bsf     STATE, StateWait  ; STATE = Wait
    clrf    TENTHS            ; clear time
    clrf    SECONDS           ;
    clrf    MINUTES           ;
    bsf     GPIO, GP2         ; GP2 high (LEDs on)
    bcf      BUTTON, Released  ; "process" BUTTON, Released
    clrf      PIR1              ; clear peripheral interrupts including TIMER1 and Comparator
    banksel PIE1              ; Bank1
    bsf      PIE1, TMR1IE      ; Enable TIMER1 interrupt
    banksel    INTCON            ; Bank0
    bsf     INTCON, PEIE      ; Enable Periperal interrupts
    clrf    INTERRUPT         ; ensure INTERRUPT clear
WaitLoop:
    btfss    INTERRUPT, 0      ;
    goto    WaitLoop
    clrf    INTERRUPT         ; ensure INTERRUPT clear
    ; Next state logic
    ; If Released  goto LongOff
    banksel BUTTON            ; Bank0
    btfsc    BUTTON, Released  ;
    goto      LongOff           ;
    ; If Minutes > OnTime goto LongOff
    movlw    OnTime            ;
    subwf    MINUTES, W        ; W = MINUTES - OnTime
    btfsc    STATUS, C         ; If C==0, W>f
    goto    LongOff           ;
    ; If LowBatCnt > LowBatMax & Minutes > ShortTime goto ShortOff
    movlw    ShortTime         ;
    subwf    MINUTES, W        ;
    btfss   STATUS, C         ; If C==0, W>f

This check has to be against C instead of Z because the bettery can become too low after the ShortTime is complete but before OnTime is complete. Making this a Carry check ensures the shorter on-time and the quick flash that warns the user the battery is low. 

    goto     WaitLoop          ; else repeat
    movlw   LowBatMax         ;
    subwf    LOWBATCNT, W      ;
    btfsc    STATUS, C         ; If C==0, W>f
    goto    ShortOff          ;
    goto    WaitLoop          ; else repeat

LongOff:
    banksel STATE             ; Bank0
    clrf    STATE             ; clear STATE
    bsf     STATE, StateLongOff ; STATE = LongOff
    bcf      BUTTON, Released  ; "process" BUTTON, Released
    clrf    TENTHS            ; clear time
    clrf    SECONDS           ;
    clrf    MINUTES           ;
    clrf    INTERRUPT         ; ensure INTERRUPT clear
LongOffLoop:
    btfss    INTERRUPT, 0      ;
    goto    LongOffLoop
    clrf    INTERRUPT         ; ensure INTERRUPT clear
    bcf      GPIO, GP2         ; GP2 low (LEDs off)
    btfss    SECONDS, 0        ;
    bsf     GPIO, GP2         ; GP2 high (LEDs on) on even seconds
    ; Next state logic
    ; If Released  goto Wait
    btfsc    BUTTON, Released  ;
    goto      Wait              ;
    ; If Seconds > LongFlash goto PowerDown
    movlw    LongFlash         ;
    subwf    SECONDS, W        ;
    btfsc    STATUS, C         ; If C==0, W>f
    goto    PwrDown           ;
    goto    LongOffLoop       ; else repeat

ShortOff:
    banksel STATE             ; Bank0
    clrf    STATE             ; clear STATE
    bsf     STATE, StateShortOff ; STATE = ShortOff
    bcf      BUTTON, Released  ; "process" BUTTON, Released
    clrf    TENTHS            ; clear time
    clrf    SECONDS           ;
    clrf    MINUTES           ;
    clrf    INTERRUPT         ; ensure INTERRUPT clear
ShortOffLoop:
    btfss    INTERRUPT, 0      ; wait for interrupt
    goto    ShortOffLoop
    bcf      GPIO, GP2         ; GP2 low (LEDs off)
    btfss    TENTHS, 0         ;
    bsf     GPIO, GP2         ; GP2 high (LEDs on) on even tenths
    ; Next state logic
    ; If Released  goto Wait
    btfsc    BUTTON, Released  ;
    goto      Wait              ;
    ; If Seconds > ShrtFlash goto PowerDown
    movlw    ShrtFlash         ;
    subwf    SECONDS, W        ;
    btfsc    STATUS, C         ; If C==0, W>f
    goto    PwrDown           ;
    goto    ShortOffLoop      ; else repeat

PwrDown:
    banksel INTCON            ; Bank0
    bcf       INTCON, GIE       ; disable interrupt
    clrf    STATE             ; clear STATE
    bsf     STATE, StatePwrDown ; STATE = PowerDown
    bcf     GPIO, GP2         ; not LIGHT
    bcf     T1CON, TMR1ON     ; disable Timer 1
    goto Init                 ; start over

    end
;


So much for my posting every week to get this done quickly. On the other hand, I've been working so I can't complain. The next post will either be the 3d printed case or how to replace the headlight case and rim on a Honda Shadow. 

Bruce

No comments:

Post a Comment