Semi-accurate blocking VIA-based delays

Programming the 6502 microprocessor and its relatives in assembly and other languages.
Procrastin8
Posts: 40
Joined: 07 Jul 2020
Location: Amsterdam, NL

Semi-accurate blocking VIA-based delays

Post by Procrastin8 »

I am building up my VIA knowledge and was able to successfully create a blocking (not interrupt based) delay solution with 8 bit precision. Now I want to expand it to 16 bit as calling my subroutine multiple times was cumbersome. My goals with this code were to have msec resolution, keep port B free (I am using it for output), poll the IFR, and return when I have decremented a 16 bit value to zero. But it actually takes _way_ longer than calculated. My breadboard 6502 is using a 2mhz crystal oscillator, so I calculated I would want a timer 1 16-bit latched value of 1998 but maybe this is wrong?

Code: Select all


Delay_Msec_Register .equ $04
Delay_Msec_Count .equ 1998 // using this as the counter-latch value -> 1000Hz

Loop:
  LDA #<One_Second
  LDX #>One_Second
  JSR Delay_Msec
  JMP Loop

;; delays execution for given msec
;; 
;; {Param} A - low-byte of 16 bit msec value
;; {Param} X - high-byte of 16 bit msec value
Delay_Msec:
  STA Delay_Msec_Register   ; move our values from registers to ZP space
  STX Delay_Msec_Register + 1
  SEI
  LDA #(VIA_INTERRUPT_MASK_SET | VIA_INTERRUPT_MASK_TIMER1) ; $C0
  STA VIA_INTERRUPT_ENABLE
  STZ VIA_AUX_CONTROL       ; one-shot, interrupt-only
@Msec:
  LDA #<Delay_Msec_Count
  STA VIA_TIMER1_COUNTER_LOW
  LDA #>Delay_Msec_Count
  STA VIA_TIMER1_COUNTER_HIGH    ; this store triggers the countdown (and clears prev interrupt flag)
  LDA VIA_INTERRUPT_MASK_TIMER1 ; $40
@Poll:
  BIT VIA_INTERRUPT_FLAG
  BEQ @Poll
  DEC16(Delay_Msec_Register)       ; a macro that will decrement a 16 bit value
  BNE @Msec                                  ; low-byte zero?
  LDA Delay_Msec_Register + 1
  BNE @Msec                                  ; high-byte zero?
@Done:
  LDA VIA_TIMER1_COUNTER_LOW  ; read of T1 counter low clears the IFR before we reenable interrupts
  CLI
  RTS

One_Second .word 1000

This code takes about 30 sec per iteration though, not 1 sec like I thought I had calculated. Any ideas/help this wonderful community can offer me?
User avatar
BigEd
Posts: 11463
Joined: 11 Dec 2008
Location: England
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by BigEd »

What does DEC16 look like?
Procrastin8
Posts: 40
Joined: 07 Jul 2020
Location: Amsterdam, NL

Re: Semi-accurate blocking VIA-based delays

Post by Procrastin8 »

DEC16 is a macro that I lifted from https://wiki.nesdev.com/w/index.php/Syn ... structions:

Code: Select all

;; 16 bit DEC
;;
;; {PARAM} low-byte address of 16-bit value
;;
;; NOTE: SR status is based on lower byte only
.macro DEC16(ADDRESS)
    LDA ADDRESS
    BNE @NoWrap
    DEC ADDRESS + 1
@NoWrap:
    DEC ADDRESS
.endmacro
It has been tested independently and appears to work but maybe I got something wrong.
User avatar
BigEd
Posts: 11463
Joined: 11 Dec 2008
Location: England
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by BigEd »

Thanks. It just might be useful to see your assembly listing with the byte values. It might be that there's something obvious, but I'm not seeing it.
John West
Posts: 383
Joined: 03 Sep 2002

Re: Semi-accurate blocking VIA-based delays

Post by John West »

Procrastin8 wrote:

Code: Select all


  LDA #<One_Second
  LDX #>One_Second

One_Second .word 1000
This is loading A and X with the address of One_Second, not its contents. Either

Code: Select all

  LDA One_Second
  LDX One_Second+1

One_Second .word 1000
or

Code: Select all

  LDA #<One_Second
  LDX #>One_Second

One_Second .equ 1000
would be correct. Unless your assembler is doing something quite different with .word than I'm used to.
User avatar
BigEd
Posts: 11463
Joined: 11 Dec 2008
Location: England
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by BigEd »

Ah! Well-spotted.
Procrastin8
Posts: 40
Joined: 07 Jul 2020
Location: Amsterdam, NL

Re: Semi-accurate blocking VIA-based delays

Post by Procrastin8 »

Yay that worked! Well I also had to change my polling code:

Code: Select all

  LDA VIA_INTERRUPT_MASK_TIMER1 ; $40
@Poll:
  BIT VIA_INTERRUPT_FLAG
  BEQ @Poll
because this wasn't working. Instead I changed it back to

Code: Select all

@Poll:
  LDA VIA_INTERRUPT_FLAG
  BPL @Poll
Perhaps I got my mask wrong originally or something but that just wasn't working.
User avatar
BigEd
Posts: 11463
Joined: 11 Dec 2008
Location: England
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by BigEd »

looks like it might be the same thing - your mask is a constant, not a location:
LDA #VIA_INTERRUPT_MASK_TIMER1 ; $40
Procrastin8
Posts: 40
Joined: 07 Jul 2020
Location: Amsterdam, NL

Re: Semi-accurate blocking VIA-based delays

Post by Procrastin8 »

Wow I have to really be careful about this. Blindspot in my assembly adventure, for sure. Thank you, BigEd.
John West
Posts: 383
Joined: 03 Sep 2002

Re: Semi-accurate blocking VIA-based delays

Post by John West »

This might be experience with other languages tripping you up. In a language like C, most of the time that you refer to the name of a variable, you are dealing with its value. "a = b+1" means "take the value of the variable b, add 1, and store the result in a". In 6502 assembly language, an instruction like "ADC b" contains its own dereference, and it means "add the value stored at address b to the A register". The distinction between value, name, and storage is crucial. Higher level languages have the same distinction, but it's expressed in different ways and can be easier to ignore.
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by BigDumbDinosaur »

I wouldn't do the time delay as you are doing. Instead, I'd have the VIA timer generate a periodic IRQ at, say, 100 Hz and have the interrupt service handler update a counter every 100 IRQs (assuming a 100 Hz IRQ rate). When a time delay is needed I would add the desired delay period in seconds to the current value of the counter and spin in a loop until the counter reaches that sum.

The principle advantage of this arrangement is the avoidance of having to mess with the VIA each time a time delay is needed. A 16-bit counter will give you a maximum delay range of 65,536 seconds, or about 18.2 hours. A 32-bit counter will give you a maximum delay range of 4,294,967,296 seconds or about 1,193,046.4 hours (a little more than 49 days).

No modern operating environment keeps time in hardware, excepting some embedded applications. It's done in software using a multi-byte progressive counter (several counters in many cases), which makes for easy future and past time calculations. The code required to advance the counter with the passage of time is succinct, even with an eight-bit MPU like the 65C02.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
Procrastin8
Posts: 40
Joined: 07 Jul 2020
Location: Amsterdam, NL

Re: Semi-accurate blocking VIA-based delays

Post by Procrastin8 »

BigDumbDinosaur, yep, just trying to get a handle on the VIA one step at a time. I did an even dumber, more brute force version before this. Now that I have this working (with this group's help, thank you!) I feel ready to tackle real interrupts. What is the typical approach here though, carve out 2 bytes of memory (not ZP I assume) that will hold the current count?
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by BigDumbDinosaur »

Procrastin8 wrote:
What is the typical approach here though, carve out 2 bytes of memory (not ZP I assume) that will hold the current count?
Two bytes at least, four bytes if you want to use the counter for something other than just a time-delay counter. For example, if you use a 32-bit counter that is initialized to $00000000 at boot time and then incremented once per second you have a system uptime counter, as well as a counter for setting delays and alarms. The 65C02 code required to increment the counter is compact and fast.

The usual procedure is to also have a one-byte "jiffy" counter along with your main counter. The jiffy counter is decremented on each VIA timer IRQ. When it reaches zero the main counter is updated. The following code fragment is an example of this procedure:

Code: Select all

;update 16-bit time counter...
;
         dec jiffyct           ;jiffy counter
         bne skip              ;not time to update
;
         lda #hz               ;jiffy IRQ rate (e.g., 100)
         sta jiffyct           ;reset counter
         inc counter           ;bump counter LSB
         bne skip              ;done with counter
;
         inc counter+1         ;bump counter MSB
;
skip     ...program continues...
The above code, which uses a 16-bit counter, would be part of your interrupt service routine and would be executed each time an IRQ has occurred and has been determined to be the result of a VIA timer underflow. A 100 Hz jiffy rate is common in UNIX-like operating systems and gives you 10 millisecond resolution. jiffyct should be initialized to the jiffy rate at boot time.

The time delay part is easy as well. As I said, the procedure is to add the time delay period to the current counter value and then spin in a loop until the counter reaches the computed sum.

Code: Select all

;generate time delay: .X = time delay in seconds LSB
;                     .Y = time delay in seconds MSB
;
;all registers are used
;
;NOTE: This code will not work on an NMOS 6502.  Also, if
;      $0000 is passed as the delay period this function's
;      behavior is undefined.
;
timdel   clc
         txa                   ;delay period LSB
         sei                   ;halt counter updates
         adc counter           ;time counter LSB
         tax                   ;future time counter LSB
         tya                   ;delay period MSB
         adc counter+1         ;time counter MSB
         tay                   ;future time counter MSB
         lda jiffyct           ;current jiffy counter value
         cli                   ;resume counter updates
;
;	———————————————————————————————————————————————————
;	Now we repeatedly compare the time counter with our
;	computed future counter value & break the loop when
;	equality is attained.  To maximize precision, we
;	also compare the jiffy counter value in .A with the
;	constantly-changing jiffy count & only do a main
;	counter comparison when they are the same.  Since
;	the jiffy count is only updated when an IRQ occurs
;	the WAI instruction is used to "sleep" until an IRQ
;	"wakes up" the MPU.
;	———————————————————————————————————————————————————
;
timdel01 wai                   ;wait for any IRQ
         cmp jiffyct           ;check jiffy count
         bne timdel01          ;not time to check main counter
;
         cpy counter+1         ;check counter MSB
         bcc timdel01          ;wait some more
;
         cpx counter           ;check counter LSB
         bcc timdel01          ;wait some more
;
         rts                   ;delay has expired — return to caller
Call the above function with:

Code: Select all

         ldx #<delay
         ldy #>delay
         jsr timdel
As for where to set up the counters, keeping them in absolute memory will result in code that is about 25 percent slower than if the counters are on page zero. I generally recommend that any variables that are to be manipulated in an interrupt service routine be kept on page zero to reduce background processing load.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
thedrip
Posts: 48
Joined: 02 Oct 2018

Re: Semi-accurate blocking VIA-based delays

Post by thedrip »

I keep my Jiffy counter in zero page for sure, but the 4 byte uptime counter and also my RTC clock which is BCD seem to do fine in regular memory.

Only the Jiffy counter is touched on every Interupt. the lowest byte of the uptime counter is touched once per second, as is the seconds byte of the RTC. Next byte up is so infrequent as to not really matter for what I do.

The once a day where the RTC rolls over all fields or once every 191 days when the 4th byte of the uptime ticket increments can be a bit long for an ISR, but I have not had issues because of it.

My approach to the Jiffy counter is a little bit different as well. the RTC is kept as BCD to make displaying that much easier. Since the ISR will be in BCD potentially anyway I keep by Jiffy counter in BCD as well, so it's just a matter of add 1, check for carry. No need to reset to 100 decimal each time it resets.

I carry through the RTC bytes using BCD math checking for appropriate rollovers (60,60,24), then update the 4 bytes of uptime counter as regular binary.

I can't claim any of this to be original thought, at most it's a minor modification of the timer presented in Garth's primer series.
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Re: Semi-accurate blocking VIA-based delays

Post by GARTHWILSON »

Backing up and adding to BDD's recommendation four posts up (I see he added another post while I was writing):
See the 6502 interrupts primer at http://wilsonminesco.com/6502interrupts/ . Under the "SETTING UP AN INTERRUPT" heading, it shows how to set up what he's talking about, with code, and how to service it with an ISR that's only five instructions including the RTI. I put mine on NMI, with nothing else on that interrupt, so there's no polling necessary. I don't even initialize it, because a target time is added to the current value, whatever it is.

Now the processor can be productive doing other things while waiting for the proper time, and comes back and checks frequently, comparing the clock variable's value to the target time to see if it's time to do whatever it's supposed to do for that task. There can be lots of these going on at the same time, essentially in a simple way of multitasking. Such tasks will usually exit right away because their time has not yet come; so they take very little execution time before returning to the routine called the "cyclic executive" which is just a short infinite loop of JSRs. Each routine it calls has its own variables to remember where it left off and what it might be waiting for. The individual routines' variable are usually not in ZP, but the running clock is, and never gets reset.

Let's say you have a keypad scanner routine that involves debouncing, delay-before-repeat (for keys you want to repeat if you hold the key down), and repeat speed after repeating begins; however, you don't want other tasks to affect the repeat speed. When each portion is started, it looks at the timer's current value, adds the desired amount of time to it, and puts the result in a variable used by only that one task. Each time that task runs, it uses another variable of its own, the state variable, to know that in this case it's waiting for a certain time, then goes to that part of the program that compares the current time to the target time. If the current time has caught up to the target time, it does its thing; otherwise it just exits and lets something else run.

This is described in the 6502-oriented article, "Simple methods for multitasking without a multitasking OS, for systems that lack the resources to implement a multitasking OS, or where hard realtime requirements would rule one out anyway," specifically under the heading "Cyclic Executive." I've done this many times in my work, even making early PIC16 microcontrollers multitask very efficiently.
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?
Post Reply