6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Sat Sep 21, 2024 5:49 am

All times are UTC




Post new topic Reply to topic  [ 17 posts ]  Go to page 1, 2  Next
Author Message
PostPosted: Mon Sep 14, 2020 10:22 am 
Offline

Joined: Tue Jul 07, 2020 10:35 am
Posts: 40
Location: Amsterdam, NL
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:

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?


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 14, 2020 11:38 am 
Offline
User avatar

Joined: Thu Dec 11, 2008 1:28 pm
Posts: 10938
Location: England
What does DEC16 look like?


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 14, 2020 11:52 am 
Offline

Joined: Tue Jul 07, 2020 10:35 am
Posts: 40
Location: Amsterdam, NL
DEC16 is a macro that I lifted from https://wiki.nesdev.com/w/index.php/Synthetic_instructions:
Code:
;; 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.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 14, 2020 12:09 pm 
Offline
User avatar

Joined: Thu Dec 11, 2008 1:28 pm
Posts: 10938
Location: England
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.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 14, 2020 12:13 pm 
Offline

Joined: Tue Sep 03, 2002 12:58 pm
Posts: 325
Procrastin8 wrote:
Code:

  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:
  LDA One_Second
  LDX One_Second+1

One_Second .word 1000

or
Code:
  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.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 14, 2020 12:17 pm 
Offline
User avatar

Joined: Thu Dec 11, 2008 1:28 pm
Posts: 10938
Location: England
Ah! Well-spotted.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 8:15 am 
Offline

Joined: Tue Jul 07, 2020 10:35 am
Posts: 40
Location: Amsterdam, NL
Yay that worked! Well I also had to change my polling code:
Code:
  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:
@Poll:
  LDA VIA_INTERRUPT_FLAG
  BPL @Poll

Perhaps I got my mask wrong originally or something but that just wasn't working.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 8:52 am 
Offline
User avatar

Joined: Thu Dec 11, 2008 1:28 pm
Posts: 10938
Location: England
looks like it might be the same thing - your mask is a constant, not a location:
LDA #VIA_INTERRUPT_MASK_TIMER1 ; $40


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 9:01 am 
Offline

Joined: Tue Jul 07, 2020 10:35 am
Posts: 40
Location: Amsterdam, NL
Wow I have to really be careful about this. Blindspot in my assembly adventure, for sure. Thank you, BigEd.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 9:27 am 
Offline

Joined: Tue Sep 03, 2002 12:58 pm
Posts: 325
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.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 7:08 pm 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8390
Location: Midwestern USA
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!


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 7:27 pm 
Offline

Joined: Tue Jul 07, 2020 10:35 am
Posts: 40
Location: Amsterdam, NL
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?


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 8:41 pm 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8390
Location: Midwestern USA
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:
;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:
;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:
         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!


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 8:54 pm 
Offline

Joined: Tue Oct 02, 2018 4:22 am
Posts: 48
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.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 15, 2020 9:02 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8510
Location: Southern California
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?


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 17 posts ]  Go to page 1, 2  Next

All times are UTC


Who is online

Users browsing this forum: No registered users and 9 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: