There’s no getting away from it. The 6522 Versatile Interface Adapter (VIA) is, well, versatile. Alongside giving you two 8-bit general purpose I/O ports – much like the GPIO pins on a Raspberry Pi, Arduino and the like – it also has a bunch of handy extra features.
These include a shift register and control lines that you can employ to fire off interrupts. But here we’ll be dealing with the timers.
In particular, we’ll talk about Timer 1 – the more versatile of the two timers in each 6522.
As is so often the case, this little exploration was inspired by a video by Ben Eater. So here it is.
What we’re going to cover here, though, is not a simple reacreation of Ben’s work but a slightly different implementation I chose in order to learn about timers. Ben was more concerned with replicating the kind of delay() function you get in frameworks like the Arduino IDE. I’ve done that too (although, again, in a slightly different way) but that will be for a future post.
In order to learn about how these timers work, I decided to implement a counter. The basic process is this:
- We start a timer. This counts down from a number we give it.
- When the timer reaches 0, it fires off an interrupt.
- In the interrupt service routine, we increment a variable by one.
- The timer automatically restarts from the previously set number.
- We display the variable holding the count on two 8-segment bar LEDs, effectively showing a binary representation of the number.
Another way of going about this, if all you want to do is display the value of the variable, is to use a loop in which the value is incremented and displayed, followed by a short sleep before going around again. But then the computer is entirely occupied with this one task and can do nothing else while sleeping.
Using a timer and interrupts means the computer can get on with other things. Yes folks, the Zolatron 64 is going to be multi-tasking!
Beginning of time
Let’s start with the timer. We’re going to have to familiarise ourselves with some of the 6522 VIA’s registers. There are five key registers, plus another two that we don’t have to deal with directly here (although you may want to for more complex tasks).
You access the registers by reading or writing an address in memory. Exactly which address depends on the memory map of your computer. In the case of the Zolatron, I’ve reserved some space at address $A400 for this VIA. Each register has its own address relative to this – so, for example, the Auxiliary Control Register (ACL) is at address $0B, so we add this to $A400 to get the address $A40B.
Let’s meet the main registers.
- T1CL – Timer 1 Counter (low byte) @ $04
- T1CH – Timer 1 Counter (high byte) @ $05
- ACL – Auxiliary Control register @ $0B
- IFR – Interrupt Flag Register @ $0D
- IER – Interrupt Enable Register @ $0E
The ACL, IFR and IER are not specific to the 6522’s timers, but parts of them are relevant.
Here’s some 6502 assembler code setting up constants to name these addresses as well as some other constants we’re going to be using.
VIA_T1CL = $A804 ; timer 1 counter low VIA_T1CH = $A805 ; timer 1 counter high VIA_ACL = $A80B ; Auxiliary Control register VIA_IER = $A80E ; Interrupt Enable Register VIA_IFR = $A80D ; Interrupt Flag Register TIMER_COUNT = $0510 ; A two-byte memory location to store a counter TIMER_INTVL = $270E ; The number the timer is going to count down from
As you can see, we need a two-byte memory location to store a counter. This can be anywhere in RAM – I’ve put it, somewhat arbitrarily, at location $0510 because, why not? In our code, we should initialise this to 0.
lda #0 sta TIMER_COUNT ; Or use STZ if using a 65C02, without the LDA line above. sta TIMER_COUNT + 1
Interrupt Enable Register (IER)
|Set/Clear||Timer 1||Timer 2||CB1||CB2||Shift Register||CA1||CA2|
To get started, we need to tell the 6522 VIA that we want to use interrupts with Timer 1. We do this with the Interrupt Enable Register (IER). And you set this in a slightly unusual way. When you write a byte to the IER:
- If bit 7 of the byte value is a 1, any other bits set to 1 will be copied to the relevant bits of the IER. In effect, this is like ORing the IER with the byte.
- If bit 7 is 0, any other bits set to 1 will unset (set to 0) the corresponding bits in the IER.
- Other than bit 7, bits set to zero in the value you’re writing have no effect on the register bits.
So, to enable the interrupts for Timer 1, we write %11000000 to the IER. This does not affect any other interrupt settings. The code is below.
lda #%11000000 ; setting bit 7 sets interrupts and bit 6 enables Timer 1 sta VIA_IER
To disable the interrupts for Timer 1, you’d write %01000000 to the IER.
Auxiliary Control Register (ACL)
This is used to set the timer modes.
|T1 Timer Control||T2 Timer Control||Shift Register Control||PB||PA|
Our main interest here lies in bits 6 and 7, which determine how Timer 1 behaves.
|0||0||Timed interrupt each time T1 is loaded||Disabled|
|1||0||Timed interrupt each time T1 is loaded||One-Shot Output|
|1||1||Continuous interrupts||Square Wave Output|
The ‘timed interrupt’ option is a one-shot mode. When the timer is loaded, it starts counting down and then issues an interrupt when it has reached zero. And that’s all there is to it.
‘Continuous interrupts’ is a free-running mode which means that the timer restarts counting down from the previously set number after the interrupt is issued.
There are options to have bit 7 of Port B toggle when the interrupt happens, but we don’t want that here. So for our purposes, we’ll set ACL to 01000000 (in binary).
lda #%01000000 sta VIA_ACL
Timer 1 Counter registers
There are two counter registers that hold the high and low bytes of a 16-bit number. This is the number you’re going to count down from. When you set these registers, they get copied into two other ‘latch’ registers – T1LL and T1LH. The latch holds on to the entered number so that you don’t have to enter it again, unless you want to change it. That’s not the case in our example, so we’ll leave T1LL and T1LH alone.
You enter the value for the low byte (T1CL) first. As soon as you enter the value for the high byte (T1CH), the timer starts counting down.
; We set up TIMER_INTVL earlier,,, lda #<TIMER_INTVL ; Load low byte of our 16-bit value sta VIA_T1CL lda #>TIMER_INTVL ; Load high byte of our 16-bit value sta VIA_T1CH ; This starts the timer running
Interrupt Flag Register (IFR)
So far, we have the timer set up and running. Now we need to do something with it. For one thing, we need to do something every time the interrupt fires. And that something is increment our counter whose value we’re storing at TIMER_COUNT.
When an interrupt fires, the thing that fired it sets a bit in the Interrupt Flag Register (IFR). In our case, Timer 1 sets bit 6.
|IRQ||Timer 1||Timer 2||CB1||CB2||Shift Register||CA1||CA2|
Any enabled interrupt
|Time-out of T1||Time-out of T2||CB1 active edge||CB2 active edge||Complete 8 shifts||CA1 active edge||CA2 active edge|
Clear all interrupts
|Read T1CL low or write T1LH high||Read T2 low or write T2 high||Read or write ORB||Read or write ORB||Read or write shift register||Read or write ORA||Read or write ORA|
Here’s an example of what the code might look like within your ISR handling routine (I call mine ISR_handler).
.ISR_handler pha : phx : phy ; other ISR stuff perhaps bit VIA_IFR ; Bit 6 copied to overflow flag bvc isr_timer_end ; Overflow clear, so not this... .isr_timer lda VIA_T1CL ; Clears the interrupt inc TIMER_COUNT ; Increment low byte bne isr_timer_end ; Low byte didn't roll over, so we're all done inc TIMER_COUNT + 1 ; previous byte rolled over, so increment high byte .isr_timer_end jmp exit_isr ; other isr stuff .exit_isr ply : plx : pla rti
We use the trick of applying BIT to the IFR. This doesn’t affect the register but it copies the value of bit 6 from it to the overflow flag. If the flag is clear (0) then we know it wasn’t the timer that caused the interrupt and can bug out.
Then we load the value of T1CL into A – not because we need it but because this has the effect of clearing the interrupt flag.
Next we increment the low byte of the counter. If this was already at 255, it will roll over to 0. We can test for this using BNE. If it did roll over, we increment the high byte of the counter.
All that’s left is to periodically check the value of TIMER_COUNT in the main part of your program. How you do this is up to you, but here’s a subroutine I find handy. It returns a value in the memory location FUNC_RESULT. And that value is one of the three constants I’ve defined as EQUAL, LESS_THAN and MORE_THAN. Here’s the code setting those up.
LESS_THAN = 0 EQUAL = 1 MORE_THAN = 2 FUNC_RESULT = $0512 ; or a RAM location of your choice
And here’s the subroutine.
.via_chk_timer sei ; Don't want interrupts changing the value of TIMER_COUNT pha lda TIMER_COUNT + 1 ; Compare the high bytes first as if they aren't cmp #<TIMER_INTVL + 1 ; equal, we don't need to compare the low bytes bcc via_chk_timer_less_than ; Count is less than interval bne via_chk_timer_more_than ; Count is more than interval lda TIMER_COUNT ; High bytes equal - what about low bytes? cmp TIMER_INTVL bcc via_chk_timer_less_than bne via_chk_timer_more_than lda #EQUAL ; COUNT = INTVL - this what we're looking for. jmp via_chk_timer_reset .via_chk_timer_less_than lda #LESS_THAN ; COUNT < INTVL - counter isn't big enough yet jmp via_chk_timer_end ; so let's bug out. .via_chk_timer_more_than lda #MORE_THAN ; COUNT > INTVL - shouldn't happen, but still... .via_chk_timer_reset stz VIAC_TIMER_COUNT ; reset counter stz VIAC_TIMER_COUNT + 1 .via_chk_timer_end sta FUNC_RESULT pla cli rts
This is 65C02 code (note use of stz). If you’re using a 6502, you’ll probably want to get creative with X or Y to hold the return result that you put into FUNC_RESULT.
You can call this from anywhere in the main code and then just compare FUNC_RESULT with EQUAL, MORE_THAN or LESS_THAN in whatever way suits your purpose.
For showing the counter, all I do in the main body of the program is write the low and high bytes of TIMER_COUNT to port A and port B of the VIA, which is driving the LEDs.