Zolatron 64: Using the 6522 VIA’s timers – part 1

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.

Keeping count

The bar LED displays a 16-bit binary value.

In order to learn about how these timers work, I decided to implement a counter. The basic process is this:

  1. We start a timer. This counts down from a number we give it.
  2. When the timer reaches 0, it fires off an interrupt.
  3. In the interrupt service routine, we increment a variable by one.
  4. The timer automatically restarts from the previously set number.
  5. 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)

7 6 5 4 3 2 1 0
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.

7 6 5 4 3 2 1 0
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.

7 6 Operation PB7
0 0 Timed interrupt each time T1 is loaded Disabled
0 1 Continuous interrupts 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.

7 6 5 4 3 2 1 0
IRQ Timer 1 Timer 2 CB1 CB2 Shift Register CA1 CA2
Set by

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
Cleared by

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.

Main program

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.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.