• Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

Improved K8LH fixed delay subsystem (PIC)

Status
Not open for further replies.

Mike - K8LH

Well-Known Member
Just wanted to pass along a new and improved K8LH PIC fixed delay subsystem for those PIC assembly language programmers who might be interested.

For those of you not familiar with my subsystem it was designed to be a general purpose replacement for the code generated by the PICLIST delay code generator, capable of generating precise delays in cycles, microseconds, or milliseconds using almost any clock (4, 8, 12, 16, or 20 MHz). It also allows you to subtract N number of cycles from the delay for isochronous code timing.

Basically I replaced the 4-cycle inner/outer timing loop in my old timing subroutine with Mike Bond's 4-word 5-cycle 16-bit timing loop and shaved three words off of the subroutine in the process. This also reduced timing subroutine "overhead" and associated minimum delay from 16 cycles to 11 cycles.

Code:
;
;  DelayCy() clock equate and macro delay operand multipliers
;
        radix   dec
clock   equ     4               ; 4, 8, 12, 16, 20 MHz
usecs   equ     clock/4         ; cycles/microsecond multiplier
msecs   equ     clock/4*1000    ; cycles/millisecond multiplier
;
;  DelayCy() macro generates four instructions
;
DelayCy macro   delay           ; 11..327690 cycle delay range
        movlw   high((delay-11)/5)+1
        movwf   DelayHi
        movlw   low ((delay-11)/5)
        call    uDelay-((delay-11)%5)
        endm
;
;  example code for simulation testing
;
SimTest DelayCy(10*msecs)       ; <- put simulator PC here
        nop                     ; <- put simulator break point here
;
;  uDelay(11..327690) subroutine     Mike McLaren, K8LH, Mar-09
;
;  9 words, 1 RAM variable, 14-bit core
;
        nop                     ; entry for (delay-11)%5 == 4     |B0
        nop                     ; entry for (delay-11)%5 == 3     |B0
        nop                     ; entry for (delay-11)%5 == 2     |B0
        nop                     ; entry for (delay-11)%5 == 1     |B0
uDelay  addlw   -1              ; subtract 5 cycle loop time      |B0
        skpc                    ; borrow? no, skip, else          |B0
        decfsz  DelayHi,F       ; done?  yes, skip, else          |B0
        goto    uDelay          ; do another loop                 |B0
        return                  ;                                 |B0
;
Then while studying the subsystem code I wondered if I could include some of the 'nop' instructions in the timing loop to increase the upper delay limit without affecting timing loop "overhead".

It turns out that each 'nop' included in the timing loop will increase the upper delay limit by 65536 cycles but you need one additional 'nop' at the top of the timing subroutine to handle the additional delay%loop subroutine entry point. Still, one additional word for each additional 65536 cycles isn't bad.

Code:
;
;  DelayCy() clock equate and macro delay operand multipliers
;
        radix   dec
clock   equ     8               ; 4, 8, 12, 16, 20 MHz
usecs   equ     clock/4         ; cycles/microsecond multiplier
msecs   equ     usecs*1000      ; cycles/millisecond multiplier
;
;  5 cycle loop delay range 11..327690 cycles
;  6 cycle loop delay range 11..393226 cycles
;  7 cycle loop delay range 11..458762 cycles
;  8 cycle loop delay range 11..524298 cycles
;
loop    equ     8               ; select 5..8 cycle loop
;
;  DelayCy() macro generates four instructions
;
DelayCy macro   delay           ; minimum 11 cycles
        movlw   high((delay-11)/loop)+1
        movwf   DelayHi
        movlw   low ((delay-11)/loop)
        call    uDelay-((delay-11)%loop)
        endm
;
;  example code for simulation testing
;
SimTest DelayCy(8*usecs)        ; <- put simulator PC here
        nop                     ; <- put simulator break point here
;
;  16 bit uDelay subroutine with adjustable 5..8 cycle loop time
;
;  12 words, 1 RAM variable, 14 bit core   ---  Mike, K8LH
;
        nop                     ; entry for (delay-11)%loop == 7  |B0
        nop                     ; entry for (delay-11)%loop == 6  |B0
        nop                     ; entry for (delay-11)%loop == 5  |B0
        nop                     ; entry for (delay-11)%loop == 4  |B0
        nop                     ; entry for (delay-11)%loop == 3  |B0
        nop                     ; entry for (delay-11)%loop == 2  |B0
        nop                     ; entry for (delay-11)%loop == 1  |B0
uDelay  addlw   -1              ; subtract "loop" cycle time      |B0
        skpc                    ; borrow? no, skip, else          |B0
        decfsz  DelayHi,F       ; done?  yes, skip, else          |B0
        goto    uDelay-loop+5   ; do another loop                 |B0
        return                  ;                                 |B0
;
Finally I modified the DelayCy() macro to handle larger delays by allowing it to call the timing subroutine multiple times. This version is listed in the next post.

Regards, Mike
 
Last edited:

Mike - K8LH

Well-Known Member
More than a few times someone has asked how to generate a 3 second delay, or a 1 hour delay, etc., without using interrupts.

Well here's an example 256 hour appliance timer which probably isn't very practical that uses the "modified macro" version of my fixed delay subsystem (necessary for 1 second delay).

You need to setup the 'hours', 'minutes', and 'seconds' variables before falling into the code. Memory usage is 37 words @ 4 MHz clock and climbs to 69 words @ 20 MHz clock.

Food for thought. Mike

Code:
;
;  appliance timer (isochronous loop) demo requires presetting
;  the "hours", "minutes" and "seconds" variables to values in
;  the range of 00:00:01 to 23:59:59 (or perhaps to 255:59:59)
;
        radix   dec
timer
        bsf     relaypin        ; turn relay on                   |B0
sloop   DelayCy(1000*msecs-16)  ; 1 sec minus 16 cycle loop time  |B0
        movlw   59              ; W = 59 (mins/secs reset value)  |B0
        decf    seconds,F       ; decrement seconds               |B0
        btfsc   seconds,7       ; negative? no, skip, else        |B0
        decf    minutes,F       ; decrement minutes               |B0
        btfsc   seconds,7       ; negative? no, skip, else        |B0
        movwf   seconds         ; reset seconds = 59              |B0
        btfsc   minutes,7       ; negative? no, skip, else        |B0
        decf    hours,F         ; decrement hours                 |B0
        btfsc   minutes,7       ; negative? no, skip, else        |B0
        movwf   minutes         ; reset minutes = 59              |B0
        movf    seconds,W       ;                                 |B0
        iorwf   minutes,W       ;                                 |B0
        iorwf   hours,W         ; timer timed-out?                |B0
        skpz                    ; yes, skip, else                 |B0
        goto    sloop           ; loop again                      |B0
        bcf     relaypin        ; turn relay off                  |B0
;
Code:
;
;  DelayCy() clock equate and macro delay operand multipliers
;
        radix   dec
clock   equ     4               ; clock frequency in Megahertz
usecs   equ     clock/4         ; cycles/microsecond multiplier
msecs   equ     usecs*1000      ; cycles/millisecond multiplier
;
;  5 cycle loop delay range 11..327690 cycles
;  6 cycle loop delay range 11..393226 cycles
;  7 cycle loop delay range 11..458762 cycles
;  8 cycle loop delay range 11..524298 cycles
;
loop    equ     8               ; select 5 to 8 cycle loop timing
;
;  DelayCy() macro generates four instructions plus an additional
;  four instructions for each loop*65536 cycles in the delay
;
DelayCy macro   delay           ; minimum 11 cycles
        local   cycles
        cycles = delay
    while cycles > (loop*65536+10)
        movlw   high((loop*65536-11)/loop)+1
        movwf   DelayHi
        movlw   low ((loop*65536-11)/loop)
        call    uDelay-((65536-11)%loop)
        cycles -= (loop*65536)
    endw
        movlw   high((cycles-11)/loop)+1
        movwf   DelayHi
        movlw   low ((cycles-11)/loop)
        call    uDelay-((cycles-11)%loop)
        endm
;
;  example code for simulation testing.
;
SimTest DelayCy(500*msecs)      ; <- put simulator PC here
        nop                     ; <- put simulator break point here
;
;  16 bit uDelay subroutine with adjustable 5..8 cycle loop time
;
;  12 words, 1 RAM variable, 14 bit core   ---  Mike, K8LH
;
        nop                     ; entry for (delay-11)%loop == 7  |B0
        nop                     ; entry for (delay-11)%loop == 6  |B0
        nop                     ; entry for (delay-11)%loop == 5  |B0
        nop                     ; entry for (delay-11)%loop == 4  |B0
        nop                     ; entry for (delay-11)%loop == 3  |B0
        nop                     ; entry for (delay-11)%loop == 2  |B0
        nop                     ; entry for (delay-11)%loop == 1  |B0
uDelay  addlw   -1              ; subtract "loop" cycle time      |B0
        skpc                    ; borrow? no, skip, else          |B0
        decfsz  DelayHi,F       ; done?  yes, skip, else          |B0
        goto    uDelay+5-loop   ; do another loop                 |B0
        return                  ;                                 |B0
;
 
Last edited:
Just curious as to the math methods used to solve cycle perfect delays. It would seem by its very nature that integer math would be less than perfect. What type of algorithm or analysis would lead to a generic delay formula?

Tried to get a cycle perfect 10us delay to be used with your even clock series of 4, 8, 12, 16, or 20 MHz. This turned into real fiasco when extrapolating from one clock speed to another. Basically it turned into a trial and error session, compile-simulate, recompile-simulate......:(. Needless to say, this approach has been abandoned until further enlightenment is obtained:p.
 

Sceadwian

Banned
Cycle perfect delays are never easy, and if you have interrupt code running it may not even be possible, do you really need perfect accuracy? You could always take a look at software UART code written in assembly, they have to have very accurate timing loops and usually list the equations required to get a specific delay from their tuned loops.
 
Thanks for the good idea, on looking up the assembler for a soft Uart. Well that's why a reality check is required with the forum, to counter act the all too frequent brain burps. And yes, the accuracy would be required for a soft Uart.
 

Mike - K8LH

Well-Known Member
Tried to get a cycle perfect 10us delay to be used with your even clock series of 4, 8, 12, 16, or 20 MHz. This turned into real fiasco when extrapolating from one clock speed to another. Basically it turned into a trial and error session, compile-simulate, recompile-simulate......:(. Needless to say, this approach has been abandoned until further enlightenment is obtained:p.
If you're trying to test different clock speeds then yes you have to rebuild the project each time after you change the 'clock' equate.

Are you having difficulty with the MPLAB simulator stop watch? The simulator stop watch will automatically display the appropriate unit of measure after you tell it what clock frequency you're using (in the <debugger> menu under <settings>).

Anyway, you get a "cycle perfect" 10 microsecond delay with my subsystem by setting the subsystem "clock" equate to your clock frequency (4, 8, 12, 16, or 20 MHz) and using the following macro call;

Code:
        DelayCy(10*usecs)       ; <- put simulator PC here
        nop                     ; <- put simulator break point here
Someone mentioned bit-banged serial routines? Here's a couple TX routines to ponder.

Code:
;
;  generic bit-banged 9600 baud "TX" subroutine
;
Put232
        bcf     PORTA,TxPin     ; send 'start' bit
        movwf   TxByte          ; save Tx data
        movlw   9               ; 8 data bits + 1 stop bit
        movwf   BitCtr          ; setup bit counter
PutBit  DelayCy(52*usecs-10)    ; delay 52 usecs minus 10 cycles
        setc                    ; shift in 'stop' bits
        rrf     TxByte,F        ; put data bit in Carry
        movf    PORTA,W         ; read PORTA
        andlw   ~(1<<TxPin)     ; clr TxPin bit to send a '0'
        skpnc                   ; a '0' bit? yes, skip, else
        iorlw   1<<TxPin        ; set TxPin bit to send a '1'
        movwf   PORTA           ; send at precise 52 usec intervals
        decfsz  BitCtr,F        ; all 9 bits? yes, skip, else
        goto    PutBit          ; send next bit
        return                  ;
Code:
;
;  generic bit-banged serial "TX" subroutine
;
;
;   19200 baud ->  52.1 usec bit time
;   57600 baud ->  17.4 usec bit time
;  115200 baud ->   8.7 usec bit time
;
bitRate equ     9600            ; specify desired baudrate
bitTime equ     (usecs*10000000/bitRate+5)/10

Put232
        bcf     PORTA,TxPin     ; send 'start' bit
        movwf   TxByte          ; save Tx data
        movlw   9               ; 8 data bits + 1 stop bit
        movwf   BitCtr          ; setup bit counter
PutBit  DelayCy(bitTime-10)     ; delay 'bitTime' minus 10 cycles
        setc                    ; shift in 'stop' bits
        rrf     TxByte,F        ; put data bit in Carry
        movf    PORTA,W         ; read PORTA
        andlw   ~(1<<TxPin)     ; clr TxPin bit to send a '0'
        skpnc                   ; a '0' bit? yes, skip, else
        iorlw   1<<TxPin        ; set TxPin bit to send a '1'
        movwf   PORTA           ; send at precise bitTime intervals
        decfsz  BitCtr,F        ; all 9 bits? yes, skip, else
        goto    PutBit          ; send next bit
        return                  ;
 
Last edited:
Mike thanks for sharing your generous amounts of code. Tried your sample code in the MPLAB simulator, it's excellent! No problems shifting back and forth for different clocks now or previously.

The previous problem "(of my own code creation)" lay in devising a looping scheme to account for the different clock multipliers. Can contribute the failure to faulty logic, and not finding the golden number of loop cycles. Still a rookie when using assembler and MPLAB.

Will be studying your soft serial routines.

Regards,
Kent
 
Last edited:

Mike - K8LH

Well-Known Member
Hi Kent,

Sorry for the misunderstanding. I thought you were saying that you were having problems with my subsystem or with the MPLAB simulator.

Someone said "cycle perfect delays are never easy" but in fact they really are. You just need to take into account the "loop time" or "cycles/loop", the extra "delay%loop" cycles, and the "overhead". You need a couple simple equates for translating "usecs" or "msecs" to "cycles" for almost any clock. You can also pretty much eliminate "overhead" for short delays by using simple "in-line" delay macros;

Regards, Mike

Code:
;******************************************************************
;                                                                 *
;  inDlyCy(1..1027) Tcy, in-line, generates 1 to 6 instructions   *
;                                                                 *
;******************************************************************

        radix   dec
clock   equ     8               ; specify clock (4, 8, 12, 16, 20)
usecs   equ     clock/4         ; cycles/usec operand multiplier

inDlyCy macro   delay           ; 1..1027 cycles
     if delay > 3
        movlw   delay/4         ;
        addlw   -1              ; 4 cycle loop (14 bit core)
        skpz                    ; done? yes, skip, else
        goto    $-2             ; loop again
     endif
     if delay%4 >= 2
        goto    $+1             ; 2 cycles for delay%4 >= 2
     endif
     if delay&1
        nop                     ; 1 cycle for delay%4 == 1 or 3
     endif
        endm

SimTest inDlyCy(1*usecs)        ; <- put simulator PC here
        nop                     ; <- put simulator break point here
;
 
Last edited:

Mike - K8LH

Well-Known Member
Just wanted to update this old thread with a more recent version of the DelayCy() subsystem code. The differences are;

() the code throws an error message if the DelayCy() parameter is out-of-range. Increase 'dloop' constant if you need longer delays.
() the code supports either 14-bit core (12F/16F) or 16-bit core (18F) devices by changing one instruction in the macro and in the uLoop subroutine.

Cheerful regards, Mike McLaren, K8LH

Code:
;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
;  K8LH DelayCy() subsystem macro generates four instructions     ~
;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        radix dec
clock   equ     16              ; 4, 8, 12, 16, 20 (MHz), etc.
usecs   equ     clock/4         ; cycles/microsecond multiplier
msecs   equ     usecs*1000      ; cycles/millisecond multiplier
dloop   equ     5               ; loop size, 5 to ??? cycles
;
;  -- loop --  -- delay range --  -- memory overhead ----------
;  5-cyc loop, 11..327690 cycles,  9 words (+4 each macro call)
;  6-cyc loop, 11..393226 cycles, 10 words (+4 each macro call)
;  7-cyc loop, 11..458762 cycles, 11 words (+4 each macro call)
;  8-cyc loop, 11..524298 cycles, 12 words (+4 each macro call)
;
DelayCy macro   cycles          ; range, see above
    if (cycles<11)|(cycles>(dloop*65536+10))
        error " DelayCy range error "
    else
        movlw   high((cycles-11)/dloop)+1
        movwf   delayhi
        movlw   low ((cycles-11)/dloop)
        rcall   uLoop-(((cycles-11)%dloop)*2)    ; (18F version)
;       call    uLoop-((cycles-11)%dloop)        ; (16F version)
    endif
        endm
Code:
;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
;  K8LH DelayCy() subsystem 16-bit uLoop subroutine               ~
;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a = dloop-1
      while a > 0
        nop                     ; (cycles-11)%dloop entry points  |
a -= 1
      endw
uLoop   addlw   -1              ; subtract 'dloop' loop time      |
        skpc                    ; borrow? no, skip, else          |
        decfsz  delayhi,F       ; done?  yes, skip, else          |
        bra  uLoop-dloop*2+10   ; do another loop (18F version)   |
;       goto uLoop-dloop+5      ; do another loop (16F version)   |
        return                  ;                                 |
;******************************************************************
 
Last edited:
Status
Not open for further replies.

Latest threads

EE World Online Articles

Loading
Top