In this experiment we will play with interrupts and show how to implement a time of day clock using the 6522 VIA.
Interrupts are a feature of most microprocessors that allows the normal flow of execution to be, well, interrupted. It is normally triggered by a hardware line.
Rather than delve into interrupts in detail I refer you to the 6502 data sheet as well as this good tutorial .
The timers on the 6522 can generate interrupts when a timer counts down to zero. In our example we'll implement a time of day clock by having the timer regularly generate interrupts, and incrementing memory locations in the interrupt service routine to track the time of day.
Because this is entirely interrupt driven, it won't (or shouldn't) affect programs running on the machine if they don't disable interrupts or conflict with the memory locations we use.
There will be two parts to our code:
- The code to set up the timer and interrupt service routine.
- The interrupt service routine itself.
The set up routine needs to first disable interrupts while we set things up. The interrupt vector (address) on the Replica 1 is $0100, which is in RAM. We want to point this to our own code, so we write a JMP instruction there to transfer control to our interrupt routine.
I decided to run the timer to generate interrupts every 100th of a second (a time interval sometimes called a jiffy). We'll need a memory location to count the number of jiffies. We'll also count seconds, minutes, and hours. Each of these can fit in an 8-bit memory location.
To set up the 6522 VIA, we enable interrupts from timer 1 in the Interrupt Enable Register. We set timer 1 to continuous mode with the PB7 output disabled. Now we can re-enable interrupts on the processor. We write the low and high bytes of the timer with the value that corresponds to a 1/100th of a second, which starts the timer. Now, whenever the timer counts down to zero it will generate an interrupt on the IRQ line of the 6522 chip which is wired to the IRQ line on the 6502.
Our interrupt routine needs to do the following:
- First save on the stack any registers we will be using so we can restore them. In our case only the accumulator.
- Read the Timer 1 low byte. This clears the interrupt.
- Now we increment the jiffies count. If we reach 100 we want to roll it over to zero and add one to the minutes If the minutes were incremented, we check if they reached 60. If so, we roll them over to zero and increment the hours count.
- Similarly we check if hours reached 24, in which case it rolls over to zero.
- When done we restore the accumulator from the stack and return. Note that we use an RTI (Return From Interrupt) instruction and not RTS for this.
For debug purposes I added code that will write the characters "S", "M", and "H" when the seconds, minutes, and hours are incremented. You can comment out this code when not testing the software. The complete code is below.
.include "6522.inc"
ECHO = $FFEF ; Woz monitor
COUNT = 19998 ; 100 Hz sample rate (10 msec interrupts) assuming 2 MHz CPU clock
IRQ = $0100 ; IRQ vector
JIFFIES = $0403 ; 100ths of seconds
SECONDS = $0402 ; counts seconds
MINUTES = $0401 ; counts minutes
HOURS = $0400 ; counts hours
SEI ; mask interrupts
LDA #$4C ; JMP ISR instruction
STA IRQ ; Store at interrupt vector
LDA #<ISR
STA IRQ+1
LDA #>ISR
STA IRQ+2
LDA #0 ; Set clock to zero
STA JIFFIES
STA SECONDS
STA MINUTES
STA HOURS
LDA #%11000000
STA IER ; enable T1 interrupts
LDA #%01000000
STA ACR ; T1 continuous, PB7 disabled
CLI ; enable interrupts
LDA #<COUNT
STA T1CL ; Set low byte of count
LDA #>COUNT
STA T1CH ; Set high byte of count
RTS ; Done
; Interrupt service routine
ISR:
PHA ; save A
BIT T1CL ; Clears interrupt
LDA JIFFIES
CLC
ADC #1
STA JIFFIES
CMP #100 ; reached 1 second?
BNE DONE ; if not, done for now
LDA #'S' ; for test purposes
JSR ECHO
LDA #0 ; reset jiffies
STA JIFFIES
LDA SECONDS ; increment seconds
CLC
ADC #1
STA SECONDS
CMP #60 ; reached 1 minute?
BNE DONE ; if not, done for now
LDA #'M' ; for test purposes
JSR ECHO
LDA #0 ; reset seconds
STA SECONDS
LDA MINUTES ; increment minutes
CLC
ADC #1
STA MINUTES
CMP #60 ; reached 1 hour?
BNE DONE ; if not, done for now
LDA #'H' ; for test purposes
JSR ECHO
LDA #0 ; reset minutes
STA MINUTES
LDA HOURS ; increment hours
CLC
ADC #1
STA HOURS
CMP #24 ; reached 24 hours?
BNE DONE ; if not, done for now
LDA #0 ; reset hours
STA HOURS
DONE:
PLA ; restore A
RTI ; and return
When run, it sets up the interrupt handler and then returns to the Woz monitor. You can examine the time of day values by dumping memory in the Woz monitor, e.g.
401.404
and see the locations that store the hours, minutes, seconds, and jiffies. Do it a few times to satisfy yourself that it is counting. If you want you can manually write the current time in hours minutes and seconds to set the clock to the correct time. I ran it overnight and it was still within one second of the correct time.
Here is a picture of the IRQ line on an oscilloscope showing the regularly spaced pulses every 10 milliseconds.
Scope Probe on Pin 4 (IRQ) of the 6502 |
100msec Interrupts from the 6522 |
If you left in the debug code you should also see "S", "M", and "H" characters appearing (which is a little annoying when you are using the Woz monitor).
It's fortunate that the interrupt vector in the Replica 1 points to RAM, as it lets us put our handler routine there. The vector for NMI also points to RAM but the NMI line is not connected to any devices. The reset vector points to ROM, the Woz Monitor, as it should.
Unfortunately $0100 is not a great choice as on the 6502 the stack sits in page 1 of memory. There is a chance that the JMP to our interrupt handler will get corrupted if the stack pointer reaches that location and data pushed on the stack writes over it. We could improve our example program by initializing the stack pointer to somewhere away from $0100 to reduce the chances of this but it could still happen. If you want to make use of interrupts on the Replica 1 you should probably reprogram your EEPROM to point the IRQ vector somewhere else.
To show that the clock routine runs independently of the main code executing on the processor, we can run BASIC and still access the time of day. Here is a simple BASIC program that shows the time of day by PEEKing the appropriate memory locations.
LOMEM=1100 : REM TO MAKE SURE WE DON'T WRITE OVER OUR ISR
10 H=PEEK (1024) : REM $0400
20 M=PEEK (1025) : REM $0401
30 S=PEEK (1026) : REM $0402
40 PRINT H;":";M;":";S
50 IF PEEK (1026)=S THEN 50 : WAIT FOR SECONDS TO CHANGE
60 GOTO 10
Typical output looks like the screen below:
Output of BASIC Program Showing Time |
Are there any limitations of our little real time clock? Well, yes. We only count time. You could easily imagine extending it to track the day, month, and year. The time gets lost if the system is powered down or even reset (this stops the timer on the VIA). If any software disables interrupts, we will stop counting time during that period. If there are interrupts from other devices, our code doesn't handle that. Finally, there is the possible stack corruption issue described earlier.
There are dedicated hardware real-time clock (RTC) chips that do a better job, but of course they aren't free.
I've been looking for some example code to test my prototype of I/O card (built with WDC's W65C22) for my homebrew computer. Testing basic I/O is easy, but interrupts can be tricky.
ReplyDeleteThis post is very helpful.
Thanks!
---
Marek
thank you, very useful, I needed it for my apple1 emulator (which has a VIA)
ReplyDelete