Friday, March 23, 2012

6522 VIA Experiment #5

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:
  1. The code to set up the timer and interrupt service routine.
  2. 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:
  1. First save on the stack any registers we will be using so we can restore them. In our case only the accumulator.
  2. Read the Timer 1 low byte. This clears the interrupt.
  3. 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.
  4. Similarly we check if hours reached 24, in which case it rolls over to zero.
  5. 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.

No comments: