title "rtl_frameworkTMR0.asm by L. Wyard-Scott" ;;; ********************************************************************* ;;; Filename: rtl_frameworkTMR0.asm ;;; Purpose: Real-time loop framework based upon TMR0. ;;; A tick-time of 1ms is used. ;;; This code is crystal frequency dependent and assumes ;;; a 4MHz timing element is used. Modifications will ;;; be required if this is not the case. ;;; Activity time is shown on PortA4. A pull-up resistor ;;; is required to display this signal. ;;; Revision: 0.1 ;;; Date: 03 October 2005 ;;; Author: L. Wyard-Scott ;;; ********************************************************************* ;;; Revision History: ;;; 0.1 03 Oct 2005 ;;; - Added support for 16F87XA devices. ;;; 0.0 22 Feb 2005 ;;; - Creation. ;;; ;;; ********************************************************************* list ; Turn on list output. ;; Include register, bit, and other info specific to ;; the specified device. ifdef __16F873 #include messg "Assembling for PIC16F873." endif ifdef __16F874 #include messg "Assembling for PIC16F874." endif ifdef __16F876 #include messg "Assembling for PIC16F876." endif ifdef __16F877 #include messg "Assembling for PIC16F877." endif ifdef __16F873A #define A_Device #include messg "Assembling for PIC16F873A." endif ifdef __16F874A #define A_Device #include messg "Assembling for PIC16F874A." endif ifdef __16F876A #define A_Device #include messg "Assembling for PIC16F876A." endif ifdef __16F877A #define A_Device #include messg "Assembling for PIC16F877A." endif ;;; -------------------------------------------------------------------- ;;; Included files (usually library headers). eg) #include "del_lib.h" ;;; -------------------------------------------------------------------- ;;; -------------------------------------------------------------------- ;;; Assembler Equates Section. Define assembly-time constants here ;;; using the EQU assembler directive. ;;; eg) DEBUGGING EQU 1 ;;; -------------------------------------------------------------------- ;;; --------------------------------------------------------------------- ;;; Variable Address Assignments. ;;; --------------------------------------------------------------------- UDATA ; Start of uninitialized data section. ; Reserve memory for variables using the ; "RES" directive here. Note that the ; linker will assign addresses. ; eg) ADcounter res 1 ; (reserves 1 byte for ADcounter). ;;; A flag to inform the main program that a tick has passed. NEWTICK RES 1 ;;; Variable used for manipulating TMR0 value to obtain tick time. TMR0_VALUE RES 1 ;;; Variables to keep track of 24-hour time in BCD format in TimeOfDayTask. HOURS RES 1 MINUTES RES 1 SECONDS RES 1 HUNDREDTHS RES 1 THOUSANDTHS RES 1 ;;; Flag variable to indicate that a second has passed. ;;; This is set within the TimeOfDayTask. NEWSECOND RES 1 UDATA_OVR ; Uninitialized data overlay section. ; Upon occasion, temporary variables ; can be over-written by other temporary ; variables. These would appear here. ; BUT! If you ever intend on time-sliced ; or pre-emptive multitasking ; don't *ever* use this directive! ;; Used in BCDIncrement. BCDValue: res 1 ;; For saving context. STATUS_Context: res 1 ; Saving of STATUS on interrupt. PCLATH_Context: res 1 ; Saving of PCLATH on interrupt. UDATA_SHR ; Uninitialized data shared section. ; Variables that appear here appear ; across all banks, meaning that no ; bank selection will be required. ; Variables of this type are *required* ; when saving CPU context when an ; interrupt occurs. Not all devices ; have shared memory. In that case, you ; need to reserve memory across individual ; banks at the same offset by using ; multiple UDATA or UDATA_SHR assignments ; with an optional address specified. W_Context: res 1 ; Context saving on interrupt: W. ;;; --------------------------------------------------------------------- ;;; Establish the OPTION register bit values. ;;; --------------------------------------------------------------------- ;;; The '__CONFIG' directive is used to embed PIC configuration data ;;; within an assembly file. The labels following the directive ;;; are located in the .inc file. See the device data sheet for ;;; additional information on the configuration word. ifndef A_Device __CONFIG _CP_OFF & _WDT_OFF & _BODEN_ON & _PWRTE_ON & _XT_OSC & _WRT_ENABLE_ON & _LVP_OFF & _CPD_OFF endif ;;; _CP_OFF: turn off code protection. Don't change this unless you ;;; want a device that can never be programmed again. This ;;; is a "bug" in some PIC devices. ;;; _WDT_OFF: turn off the watchdog timer. ;;; _BODEN_ON: turn on power brown-out reset. ;;; _PWRTE_ON: turn on power-up timer. ;;; _XT_OSC: specify that the device is using an XT oscillator. ;;; _WRT_ENABLE_ON: enable writing to data EEPROM. ;;; _LVP_OFF: disable low-voltage in-circuit programming. ;;; _CPD_OFF: disable data EEPROM write protection. ifdef A_Device messg "A revision device." __CONFIG _CP_OFF & _WDT_OFF & _BODEN_ON & _PWRTE_ON & _XT_OSC & _WRT_OFF & _LVP_OFF & _CPD_OFF endif ;;; _CP_OFF: turn off code protection. Don't change this unless you ;;; want a device that can never be programmed again. This ;;; is a "bug" in some PIC devices. ;;; _WDT_OFF: turn off the watchdog timer. ;;; _BODEN_ON: turn on power brown-out reset. ;;; _PWRTE_ON: turn on power-up timer. ;;; _XT_OSC: specify that the device is using an XT oscillator. ;;; _WRT_OFF: disable write-protection of program FLASH. ;;; _LVP_OFF: disable low-voltage in-circuit programming. ;;; _CPD_OFF: disable data EEPROM write protection. ;;; -------------------------------------------------------------------- ;;; Interrupt Service Routine. ;;; -------------------------------------------------------------------- ISR: CODE 0x0004 ; Location of interrupt vector. ;; Save the CPU context. ;; Note that W_Context was declared under the UDATA_SHR section. ;; In the PIC16F876/7, it will appear across all banks of memory ;; and therefore no bank switching is required. For the ;; PIC16F873/4, a linker script and use of the UDATA_SHR section ;; reserves space for W_Context across all banks, even though ;; there are actually two separate locations. This code takes ;; this possible separation into account. See the linker script ;; (16F873.lkr), available from the same location as this ;; template for how this allocation occurs. movwf W_Context swapf STATUS,W banksel STATUS_Context movwf STATUS_Context movf PCLATH,W movwf PCLATH_Context ;; Why the swapf instruction, above? That's a good question. ;; The reason is that, unlike a move instruction, swapf doesn't ;; affect any bits in STATUS, which is important! ;; There are situations where you will need to save (and also ;; restore more than just these two registers. ;; ------------------------------------------------------------ ;; Your interrupt service routine code goes here. ;; ------------------------------------------------------------ ;; You'll generally need to follow these steps: ;; 1. Determine what caused the interrupt by checking ;; appropriate interrupt flags. You'll usually know which ;; ones to check, since you enable interrupts from certain ;; peripherals in the main program. ;; 2. Take appropriate action. ;; 3. Clear the interrupt source. How to do this depends ;; on the peripheral that caused the interrupt, but sometimes ;; it is as simple as clearing a flag. ;; ------------------------------------------------------------ ;; Determine what generated the ISR. ;; For an "official" Real Time Loop, there is only one possible ;; source: a periodic (timer) interrupt. ;; Determine if T0IF is set. banksel INTCON btfsc INTCON,T0IF goto T0OverFlowISR ;; Usually other exceptions are handled in some manner: ;; displaying an error, restarting, or somehow recovering. ;; In this case, an unwanted interrupt is just ignored. ;; Come back to this point when your interrupt processing is ;; complete. ISR_Restore: ;; Restore the CPU context. banksel PCLATH_Context movf PCLATH_Context,W movwf PCLATH swapf STATUS_Context,W movwf STATUS ; Restores whichever bank W_Context ; actually appears in. swapf W_Context,F ; See the note above as to why swapf swapf W_Context,W ; is used. ;; Return from the interrupt, which pulls an address off the ;; (invisible) stack. retfie ;;; ******************************************************************** ;;; Stub Name: T0OverFlowISR ;;; Description: Execution is passed to this stub from the ISR ;;; if a Timer 0 overflow has occured. ;;; This code is used to acknowledge the interrupt, ;;; and to schedule the time for the next interrupt. ;;; THIS ROUTINE CONTAINS CRYSTAL-SPECIFIC CODE. ;;; 4MHz <-> TcycTMR0 = 8 uS. ;;; Tick Time 1ms <-> 125 counts. ;;; ******************************************************************** T0OverFlowISR: ;; Acknowledge the interrupt by clearing T0IF in INTCON. bcf INTCON,T0IF ;; Set RA4 high to indicate this is the start of the tick ;; (or quite close to it, anyway). bsf PORTA,4 ;; Set the flag to inform the main program ;; that an interrupt has ocurred (and a tick has passed). banksel NEWTICK bsf NEWTICK,0 ;; If this routine was executed instantaneously upon an ;; overflow interrupt, then placing a value of 255 - 125 ;; in TMR0 would make the next interrupt occur in 1ms. ;; However, time does progress, and the value in TMR0 has ;; probably been incremented a few times before execution ;; gets here. In order to deal with this, the value in TMR0 ;; is subtracted from the value, above, to get an accurate ;; 1ms tick time. ;; There is an added complication: when a write to TMR0 occurs, ;; there is a 2 cycle delay before any clocking is accepted. ;; These two problems are addressed by the following code. ;; Preload the value 131 = 256 - 125 into W where ;; 256 = time at which the timer overflows. ;; 125 = number of timer increments corresponding to 1ms ;; (at 4MHz fXTAL, TcntTMR0 = 8 uS) movlw 131 ;; Wait for a count to occur -- this way we can say with ;; reasonable certainty that an increment of the timer ;; won't be missed. banksel TMR0 btfsc TMR0,0 goto TMR0_WaitForClear nop ; To keep timing consistent for both branches. TMR0_WaitForSet: btfss TMR0,0 goto TMR0_WaitForSet goto TMR0_JustIncremented TMR0_WaitForClear: btfsc TMR0,0 goto TMR0_WaitForClear goto TMR0_JustIncremented ; Needed to keep timing consistent. TMR0_JustIncremented: nop ; Earliest timer increment here. nop nop nop ; Latest timer increment here. ;; Now add the value to that currently in TMR0. addwf TMR0,F ;; That's all! The next interrupt should occur 1ms from ;; the time that this one occured. If you want a longer ;; tick time, I suggest adding code here which increments ;; a counter variable and sets NEWTICK only when the appropriate ;; number of interrupts has occured. ;; Go to where registers are restored. goto ISR_Restore ;; ------------------------------------------------------------ ;; Your main code starts here. If it just follows the ISR, that ;; is usually OK. ;; ------------------------------------------------------------ Main: ;; ------------------------------------------------------------- ;; Initialization ;; ------------------------------------------------------------- banksel INTCON bcf INTCON, GIE ; Disable all interrupts. ;; Variable initialization. banksel HOURS clrf HOURS clrf MINUTES clrf SECONDS clrf HUNDREDTHS clrf THOUSANDTHS ;; Make RB5 an output. banksel TRISB bcf TRISB,5 banksel PORTB bcf PORTB,5 ;; Display the time on the first pass. banksel NEWSECOND movlw 1 movwf NEWSECOND ;; Clear the NEWTICK flag. banksel NEWTICK clrf NEWTICK banksel TMR0 movlw d'131' ; Schedule the first interrupt in movwf TMR0 ; 1ms (from now). ;; Enable the Timer 0 tick interrupts. call TMR0_Init_Interrupts ;; Enable interrupts in general. INTCON is in all banks. bsf INTCON, GIE ;; ------------------------------------------------------------- ;; The Real Time Loop. ;; ------------------------------------------------------------- RTLoop: ;; Call tasks in succession here. ;; It is wise to write tasks as subroutines so that they can ;; be easily commented out or re-ordered. call TimeOfDayTask ;; | ;; | More RTL Tasks HERE! ;; | ;; Turn on the led on RB5 if the number of seconds is even, off otherwise banksel SECONDS btfss SECONDS,0 goto LEDON LEDOFF: banksel PORTB bcf PORTB,5 goto RTLDone LEDON: banksel PORTB bsf PORTB,5 ;; All tasks are complete. Bring the activity ;; pin, RA4, low. RTLDone: banksel PORTA bcf PORTA,4 ;; NEWTICK will be set within the Timer 0 overflow ISR. ;; Poll until this occurs. banksel NEWTICK RTWait: btfss NEWTICK,0 goto RTWait ;; Clear the flag. clrf NEWTICK goto RTLoop ;; Don't let execution fall down to here, otherwise the ;; subroutines (or whatever follows) will be executed without ;; parameters, and chaos reigns when a "return" instruction ;; executes with no return address on the stack! ;;; -------------------------------------------------------------------- ;;; Application-specific Subroutines. ;;; -------------------------------------------------------------------- ;;; ******************************************************************** ;;; Subroutine Name: TimeOfDayTask ;;; Description: Keeps track of 24-hour time in BCD variables, ;;; Right down to the thousandth of a second. ;;; HH:MM:SS:HH:M ;;; Requires: Nothing. ;;; Returns: Nothing. ;;; Locations Affected: Time variables. ;;; ******************************************************************** TimeOfDayTask: ;; Increment the thousandths. banksel THOUSANDTHS incf THOUSANDTHS,F ;; If thousandths is 0x0A, reset it, and increment hundredths. movlw 0x0A subwf THOUSANDTHS,W btfss STATUS,Z goto TimeOfDayEnd clrf THOUSANDTHS ;; Increment the hundredths. banksel HUNDREDTHS movf HUNDREDTHS,W call BCDIncrement banksel HUNDREDTHS movwf HUNDREDTHS ;; If a carry hasn't happened, then exit. btfss STATUS,C goto TimeOfDayEnd ;; Otherwise, increment seconds (and set a flag). banksel NEWSECOND movlw 1 movwf NEWSECOND banksel SECONDS movf SECONDS,W call BCDIncrement banksel SECONDS movwf SECONDS ;; If not equal to 0x60, then exit this task. sublw 0x60 btfss STATUS,Z goto TimeOfDayEnd ;; Equal to 0x60, so clear the seconds and increment minutes. clrf SECONDS banksel MINUTES movf MINUTES,W call BCDIncrement banksel MINUTES movwf MINUTES ;; Same deal as before: if not 0x60, exit. sublw 0x60 btfss STATUS,Z goto TimeOfDayEnd ;; Equal to 0x60, so clear the minutes and increment the hours. clrf MINUTES banksel HOURS movf HOURS,W call BCDIncrement banksel HOURS movwf HOURS ;; If not equal to BCD 24, exit. sublw 0x24 btfss STATUS,Z goto TimeOfDayEnd ;; Hours is 24, so reset it to 0. clrf HOURS TimeOfDayEnd: return ;;; ******************************************************************** ;;; Subroutine Name: BCDIncrement ;;; Description: Increments the BCD value in W. ;;; Requires: W contains the (packed) BCD value to which ;;; a value of 1 will be added. ;;; Returns: C=1 if the value went from 0x99 -> 0x00 ;;; W contains the incremented BCD value. ;;; Locations Affected: BCDValue (temporary variable). ;;; ******************************************************************** BCDIncrement: ;; This routine works by: ;; a) Adding one to the value. ;; b) If the LSNibble is 0xA, then it is reset to 0 and ;; MSNibble is incremented. ;; c) If the MSnibble is 0xA, then it is reset to 0 and ;; the STATUS Carry bit (C) is set, indicating that ;; a carry into the 100's has happened. addlw 1 ;; Save the BCD value. banksel BCDValue movwf BCDValue andlw 0x0F ; Mask off MSNibble. sublw 0x0A btfss STATUS,Z goto BCDIncrementEndNoCarry ;; Reset LSNibble to 0. movf BCDValue,W andlw 0xF0 ;; Increment the MSNibble. addlw 0x10 movwf BCDValue andlw 0xF0 sublw 0xA0 btfss STATUS,Z goto BCDIncrementEndNoCarry ;; Reset the MSNibble to 0. Since this would happen ;; only if the LSNibble was 0, the returned byte can ;; be set to 0. movlw 0 bsf STATUS,C ; Set the carry bit to indicate ; 99 + 1 = C=1 W=00. return BCDIncrementEndNoCarry: ;; Get the BCD value into W. movf BCDValue,W bcf STATUS,C return ;;; ******************************************************************** ;;; Subroutine Name: TMR0_Init_Interrupts ;;; Description: Enables Timer 0 overflow interrupts and a ;;; prescaler of divide-by 8. ;;; ;;; Requires: Nothing. ;;; Returns: Nothing. ;;; Locations Affected: W contains the value written to OPTION_REG. ;;; ******************************************************************** TMR0_Init_Interrupts: banksel TMR0 clrf TMR0 ; Clear Timer 0 register. bcf INTCON, T0IE ; Disable Timer 0 interrupts (temporarily). bcf INTCON, T0IF ; Clear pending interrupts. ;; Make RA4 a (low) digital output. ;; This output is open-drain. bcf PORTA,4 ; Initialize value to low. banksel TRISA bcf TRISA,4 ; Set direction of RA4 to output. ;; The following code sets the OPTION_REG value to ;; b76543210 ;; xx0x0010 ;; by masking. ;; b5 (T0CS) = 0 selects timer clock source as instruction cycle clk. ;; b3 (PSA) = 0 assignes the prescaler to the Timer 0 module. ;; b2:b0 (PS2:PS0) = 010 selects divide-by-eight prescaler. banksel OPTION_REG movf OPTION_REG,W andlw b'11010010' iorlw b'00000010' movwf OPTION_REG banksel INTCON bsf INTCON, T0IE ; Enable Timer 0 overflow interrupts. return ;;; ******************************************************************** ;;; Subroutine Name: ;;; Description: ;;; Requires: ;;; Returns: ;;; Locations Affected: ;;; ******************************************************************** ;;; -------------------------------------------------------------------- ;;; Code-section Constant Data. ;;; -------------------------------------------------------------------- ;;; --------------------------------------------------------------------- ;;; Vectors Section. ;;; --------------------------------------------------------------------- ;;; Establish the reset vector. Note that the "0x0000" after the CODE ;;; directive ensures that the linker places the code at program memory ;;; location 0x0000. Reset: CODE 0x0000 pagesel Main ; Selects page of program memory with. goto Main ; the actual program, then goes to it. END ; End of file.