Introducing Interrupts
Interrupts are a way for a device in the system to ask the CPU to stop what 
it's currently doing and do something on its behalf, or to inform the CPU
that something happened.  In the Intellivision, the STIC interrupts the 
CPU 60 times a second (NTSC) or 50 times a second (PAL/SECAM) to tell the 
CPU that it's done drawing the display.  This lets the CPU know that the 
STIC is ready to accept certain commands, and provides a time base for games 
and programs.
In most systems, interrupts come in two flavors:  "maskable interrupts" and 
"non-maskable interrupts."  These sometimes go by the name IRQ and NMI
(the common parlance, especially in the 6800/6502 world), or in the case 
of the CP1610, INTRM and INTR.  A maskable interrupt is an interrupt 
the CPU can choose to ignore until it's convenient.  That is, the CPU can 
"mask" (aka. block or postpone) the interruption.  A non-maskable interrupt 
is an interrupt that forces the CPU to do something immediately.
The Intellivision system only has maskable interrupts, so that's all this 
tutorial will explore.  Other systems often have more elaborate interrupt
structures.
Contents
- 1 Interrupts and Interruptibility
- 2 What Happens When the CPU Takes an Interrupt?
- 3 Reasons for Turning Off Interrupts
- 4 Example 1: A simple "elapsed time" clock
- 5 Example 2: A simple "wait timer"
Interrupts and Interruptibility
As mentioned above, interrupt requests ask the CPU to do something other
than what it's currently doing when it is convenient to do so.  Interrupts 
are like phone calls.  When the phone rings, you can either answer it 
immediately, answer it after several rings, or you can ignore the phone
altogther.
Phone calls interrupt whatever it is you're doing when you answer the call.
If you're not busy, it's likely convenient to answer the phone immediately.  
If you are busy doing something tricky—for example, carrying a bowl 
of hot soup—you might wait to answer it until you've set it down.  If 
you're away from the phone for awhile—for instance, you're in an 
important meeting—you might check your voice mail (if you have it) when 
you get back.  At any rate, the phone's unlikely to still be ringing when
you do get back.
CPU interrupts are similar, as we'll see in a moment.  Programs have 
situations that are analagous to each of the three scenarios above.  The 
CP1610's notion of "convenient" is called "interruptible."  Interruptible 
means "the CPU will process an interrupt that arrives during this instruction 
after the instruction completes."  The CPU is interruptible if the following 
two conditions are true:
- Interrupts are enabled
- The current instruction is an "interruptible instruction."
Programs can enable (unmask) interrupts with the EIS, 
JE and JSRE instructions.  They can disable 
(mask) interrupts with DIS, JD and 
JSRD instructions.  These instructions are useful for
enabling and disabling interrupts for long periods of time, or over critical
groups of instructions.  This is a bit like turning the ringer off on your 
phone when you don't want to be bothered.
Individual instructions in the CP1610 instruction set are also classified 
as interruptible or non-interruptible.  This is a unique feature of CP1610
assembly language.  We don't know why General Instrument made specific 
instructions non-interruptible.  Nonetheless, several useful idioms arise 
out of their selections.  The non-interruptible instructions you'll 
encounter most commonly are writes (MVO, 
MVO@, and PSHR), shifts/rotates and 
the various 4 cycle instructions such as EIS, 
DIS and SDBD.  Non-interruptible 
instructions are akin to ignoring your phone's ringer for a moment while 
you get something else out of the way.
What Happens When the CPU Takes an Interrupt?
When the Intellivision responds to an interrupt, it takes the following steps:
- The CPU writes the current value of the program counter (R7) to the stack. This value indicates the instruction that will run when we return from the interrupt.
- The CPU then jumps to location $1004. The hardware sets this address, and there's no easy way to change this address on the Intellivision.
- The EXEC interrupt code at $1004 then saves the CPU's state on the stack. The code pushes R0 through R5 and a copy of the flags register onto the stack. Altogether, interrupts take up a minimum of 8 words of stack space.
- The EXEC interrupt code sets up a return address ($1014) in R5, and then jumps to the interrupt handler whose address is stored in RAM at locations $100-$101.
The sequence of events above is the same for all Intellivisions.  The
Intellivision I, Intellivision II, Sears Super Video Arcade, Tandyvision
One, INTV System III and INTV Super Pro System all perform the same steps.
(It is possible to modify the Intellivision to do something different,
but why?)  It makes sense then to understand this sequence exactly as it
is described above.
So What are Interrupt Handlers?
Interrupt handlers (also known as Interrupt Service Routines or ISRs) are
functions that get invoked by the EXEC in response to an interrupt.  In the
Intellivision, these functions handle tasks that must be attended to 
periodically, since the Intellivision's STIC triggers an interrupt 60
times a second (50 times a second on PAL/SECAM systems).
ISRs have many duties.  During normal game play, an ISR might do the 
following:
- Update STIC registers
- Update the GRAM
- Update any music and/or sound effects
- Update any timers or counters that keep track of time
- Update object positions based on their "velocity"
- Check the hand controllers for new inputs
This list isn't perfect, but it is representative.  The exact demands 
placed on an ISR depend on the nature of the game program and what that 
program needs at the time.
The first step is probably the most important one.  By default, the STIC
will blank the display unless the CPU specifically asks it to display the
next frame.  This is referred to as the "STIC handshake."  To keep the
display active (non-blank), programs must write to location $0020 within
about 2000 cycles of the STIC generating an interrupt.  If the CPU does
not write to this location, the STIC will blank the display during the
next frame.
The remaining steps really depend on how your game is structured.  Future
tutorials will cover these needs in more depth.
Setting up an Interrupt Handler
Programs can set up an interrupt handler by writing its address to locations $100 and $101. The address is stored in Double Byte Data format. The following code shows one way of setting up "MYISR" as the interrupt handler:
MVII #MYISR, R0 MVO R0, $100 ; write out lower 8 bits of ISR address SWAP R0 ; swap lower/upper halves MVO R0, $101 ; write out upper 8 bits of ISR address
There are other ways to do this, but this is adequate for most purposes.
This sequence is also non-interruptible,, which is important, as 
we will see below.
Interrupt handlers behave just like any other function.  The EXEC sets up
R5 to point to the return address, $1014.  Since the return address is always
$1014, some interrupt handlers (such as those in Space Patrol) simply branch directly to $1014 rather than keeping the value
of R5 around.
Reasons for Turning Off Interrupts
Normally, programs leave interrupts enabled, and don't pay much attention
to which instructions are interruptible.  They let interrupts happen when 
they happen.  Sometimes, though, programs have good reason to turn off 
interrupts.
Critical Sections
To the program, interrupts look like they occur between two other 
instructions.  The CPU stops doing what it's doing, processes the interrupt, 
and then resumes what it had been doing.  Most of the time, this is ok:  
What the interrupt code does has little to do with what the interrupted 
code is doing.  That isn't always true though.
Suppose, for example, you're trying to increment a variable.  This is a 
three step process:
MVI VAR, R0 INCR R0 MVO R0, VAR
Suppose also that an interrupt handler tries to decrement this same variable:
MVI VAR, R1 DECR R1 MVO R1, VAR
This can lead to disaster.  Imagine if an interrupt happens in the middle
of the first instruction sequence.  The CPU ends up executing the following
instruction sequence with respect to VAR:
MVI VAR, R0 INCR R0 ; ---> interrupt occurs MVI VAR, R1 DECR R1 MVO R1, VAR ; <--- return from interrupt MVO R0, VAR
What value gets left in VAR?  In this case, it looks like the 
decrement never happened.  That's because the MVO R0, VAR 
writes the incremented value after MVO R1, VAR wrote the decremented 
value.  Also, the decremented value isn't even the right value.
This type of code is referred to as a "critical section."  The 
MVI/INCR/MVO sequence is
critical because it needs to execute without interruption.  Using the phone
call analogy above, this is similar to carrying a hot bowl of soup:  You 
don't want to spill the soup, and carrying it requires your full attention, 
so you want to do so without interruption until you safely set down the soup.
To fix the example above, you can wrap the critical code with 
EIS and DIS instructions:
DIS MVI VAR, R0 INCR R0 MVO R0, VAR EIS
What this does is prevent the CPU from servicing an interrupt while executing
this code.  Now the interrupt handler and the main program can increment and
decrement and the right thing always happens.
A Useful Idiom: MVO/SWAP/MVO
Because some of CP1610's instructions are non-interruptible, certain very useful idioms are non-interruptible. Perhaps the most important idiom is the one for writing Double Byte Data. The following sequence is not interruptible, because MVO@ and SWAP are not interruptible:
MVO@ R0, R4 ; write lower 8 bits SWAP R0 MVO@ R0, R4 ; write upper 8 bits
This idiom is very useful for setting up the interrupt handler address.  
If the sequence were interruptible, programs would have to manually 
disable interrupts while setting up a new interrupt handler address.  
Otherwise, if an interrupt happened before the address was completely set
up, the program could crash.
For example, suppose $100/$101 point to an interrupt handler at location
$5678.  This means location $100 holds $78, and location $101 holds $56.
(See Double Byte Data if you're unsure why.)  Now we're setting up a
new interrupt handler "ISR2" that happens to be at location $FEDC.  The
code for this might look like so:
MVII #ISR2, R0 MVO R0, $100 ; Write out lower 8 bits ($DC in this example) SWAP R0 MVO R0, $101 ; Write out upper 8 bits ($FE in this example)
Next, suppose the MVO/SWAP/MVO sequence is
interruptible.  If an interrupt occurs after the first MVO
but before the second MVO, what would happen?
The answer is simple:  The EXEC would branch to whatever code or data is at
location $56DC.  Who knows what's at this address?  This address is a 
meaningless mixture of $5678 (the old ISR) and $FEDC (the new ISR).  The most
likely outcome is that the program crashes mysteriously.
Fortunately, because MVO and SWAP are both
non-interruptible on the CP1610, this isn't possible.  The code does
safely update the interrupt handler address.  Any interrupt that arrives
during this sequence will be ignored until after the sequence completes.
Program Initialization
Sometimes, programs need to turn off (or simply find it convenient to turn 
off) interrupts for extended period of time.  This happens most often during
initialization.  (By initialization, I mean any major change in machine and
game setup, not just the setup that happens right after reset.)
During initialization, the program may be setting up large bits of memory, 
such as the contents of GRAM and so on.  This may take more than the 
time the STIC usually allots for this.  Thus, such code may disable 
interrupts to simplify program design and prevent misbehavior.  
This is analagous to being "away from the phone" in the metaphor above, only 
there is no "voice mail."  If the CPU doesn't respond to an interrupt in about
1600 cycles, the interrupt simply gets dropped.  This is like what happens
when someone hangs up after letting the phone ring for awhile.  The next
interrupt still occurs though.
Example 1: A simple "elapsed time" clock
The following example attempts to demonstrate how interrupts work without
getting caught up in too many details of how the rest of the system works.
This example shows a simple elapsed-time clock on screen that shows how many hours,
minutes, seconds and 60ths of a second (tics) have elapsed 
since the program started.  It uses an interrupt handler to keep track of 
the time and to keep the screen from going blank.
Let's build this up by pieces.
The Interrupt Handler
The interrupt handler has two major responsibilities: Keep the screen on, and update the time. Updating the time has several smaller steps:
- Increment the number of tics that have occurred this second. If that number is less than 60, then we're done updating the time so skip to the end.
- Zero out the # of tics/second and increment the number of seconds. If this number is less than 60, then we're done updating the time, so skip to the end.
- Zero out the # of seconds and increment the number of minutes. If this number is less than 60, then we're done updating the time, so skip to the end.
- Zero out the number of minutes and increment the number of hours.
The following code shows one way to implement the ISR.  The first statement,
MVO R0, $20 keeps the screen enabled.  The rest of the code
updates the elapsed time.  There are more clever ways to do this.  
This code is written for clarity.  (In general, write for clarity first, and
get fancy later only if you need to.)
MYISR   PROC
 
        MVO     R0,     $20     ; Keep screen enabled.
 
        MVI     TIC,    R0      ; Get current tic count into R0
        MVI     SEC,    R1      ; Get current seconds count into R1
        MVI     MIN,    R2      ; Get current minutes count into R2
        MVI     HOUR,   R3      ; Get current hours count into R3
 
        INCR    R0              ; Increment tic count
        CMPI    #60,    R0      ; Tic < 60?
        BLT     @@time_done     ; Yes:  Done updating time
 
        CLRR    R0              ; Reset tic count
        INCR    R1              ; Increment seconds count
        CMPI    #60,    R1      ; Seconds < 60?
        BLT     @@time_done     ; Yes:  Done updating time
 
        CLRR    R1              ; Reset seconds count
        INCR    R2              ; Increment minutes count
        CMPI    #60,    R2      ; Minutes < 60?
        BLT     @@time_done     ; Yes:  Done updating time
 
        CLRR    R2              ; Reset minutes count
        INCR    R3              ; Increment hours count
 
@@time_done:
        MVO     R0,     TIC     ; Store updated tic count
        MVO     R1,     SEC     ; Store updated seconds count
        MVO     R2,     MIN     ; Store updated minutes count
        MVO     R3,     HOUR    ; Store updated hours count
 
        JR      R5
        ENDP
This interrupt handler is fairly straightforward.  It's all that this 
particular example needs.
The Main Loop
The main loop merely needs to print out the time whenever it changes.
The loop runs "forever."  The main steps are:
- 
        Wait for the time to change.  We can do this by waiting for the
        value of TICto change.
- Take a snapshot of the current time. While it might not be immediately obvious, this is a critical section. We don't want the time to change on us while we're reading it.
- Print out the various time components: Hours, Minutes, Seconds and Tics.
- Go back to step 1.
Let's take a look of each of these steps in turn.
Step 1: Wait for the time to change
We can tell if the time has changed by looking at the TIC 
variable and seeing if its value is different than the last time we displayed
the time.  This works because the value of TIC changes every
time the time changes, even if the other time components don't change.
The safest and easiest way to do this is to store a copy of the previous value 
of TIC somewhere.  In this example, we'll store it in a place 
called PTIC.  The code to do this might look like so:
@@wait_time:
        MVI     TIC,    R0      ; Get current value of TIC
        CMP     PTIC,   R0      ; Has the time changed?
        BEQ     @@wait_time     ; No:  Keep looping
When the loop above exits, we know the time has changed since the last time
we displayed it.
Step 2: Take a snapshot of the time
Because the interrupt handler updates the time inside the interrupt handler,
we need to be careful when we read the time so that we get a consistent 
view of the time.  Otherwise, if an interrupt occurs while we're reading
the time, we could get a scrambled copy.  For example, suppose the current
time is 11:59:59:59 and is about to roll over to 12:00:00:00.  If an 
interrupt comes in after we've read tics, seconds and minutes but before 
we read the hours, we might see 12:59:59:59, which is completely wrong.  
Even worse, on our next tic we will likely see 12:00:00:01.  
 
If this did happen, time will appear to have jumped forward and then 
backward by very large amouints, which is bad.  We want to see either 
11:59:59:59 or 12:00:00:00 in this case, not some mixture of the two.  If 
we were doing much more with the time than just displaying it, such an error 
could cause all sorts of strange problems.
Now, in this particular example program, such an error is unlikely to 
happen and if it did happen, the consequences are minor.  In more complex 
programs, though, it could happen that by the time we get around to 
displaying the time again, the time is about to change.  Therefore, rather 
than write risky code that happens to work for this example, it's better 
to write correct code that will continue to work even if it gets reused in 
a much more complex program.
The most straightforward way to read the time, then, is to disable the 
interrupts and read the time with interrupts off.  This copy of the time 
is guaranteed to be consistent no matter what.  For the purposes of this
example, we'll read the time and put a copy of it on the stack with PSHR.
DIS ; Disable ints (begin critical section) MVI TIC, R0 ; Read tic count MVO R0, PTIC ; Remember TIC in PTIC for next time PSHR R0 ; Save tic count on the stack MVI SEC, R0 ; Read seconds count PSHR R0 ; Save seconds count on the stack MVI MIN, R0 ; Read minutes count PSHR R0 ; Save minutes count on the stack MVI HOUR, R0 ; Read hours count EIS ; Enable ints (end critical section)
Notice that we re-read TIC and copy its value to 
PTIC inside this critical section.  This is perhaps overkill,
in that it's highly unlikely that the current time will change between 
when the @@wait_time loop completes and when we snapshot the
time.  It would require our code to be running way behind compared to the
60Hz (or 50Hz) interrupt clock.  That said, the above way of writing the 
code ensures that we only start a new loop when the current time is different 
than the displayed time, no matter what, and the displayed time is always 
100% consistent.
Step 3: Displaying the time
To display the time, we'll use the SDK-1600
routine PRNUM16, found 
here.  The
PRNUM16.z routine prints the number in R0 in a fixed width field,
complete with leading zeroes.  It prints the value to the screen, starting at 
the display location specified in R4.  It returns with R4 pointing to the 
first position after the displayed number.  This is exactly what we want.
The PRNUM16 function describes its inputs and outputs as 
follows (edited slightly for clarity):
 INPUTS
     R0  Number to print.                               
     R2  Width of field. 
     R3  Format word, added to digits to set the color. 
     R4  Pointer to location on screen to print number  
                                                        
 OUTPUTS                                                
     R0  Zeroed                                         
     R1  Unmodified                                     
     R2  Unmodified                                     
     R3  Unmodified                                     
     R4  Points to first character after field.         
This is perfect for our needs.  The example program will display the time
as four 2 digit fields separated by an empty space.  To keep the example
simple, we won't cover just yet how to display colons between the time 
segments.  Later tutorials on the STIC will cover this.
The time string will take up 8 characters for the time digits, and then 
another 3 characters for the spaces between them.  This makes the time 
11 characters long.  To display this approximately centered in the screen,
we can display it in Row 5, Column 5.  Recall that the BACKTAB is a
20 x 12 character grid.  This means that the first character will be at
location $0200 + 5*20 + 5.  (See the Hello World Tutorial
for a refresher on how this calculation works.)
The display format word controls what color the number appears in, and in
fancier cases, what font.  Such details are beyond the scope of this 
tutorial, but rest assured we'll cover them eventually.  For now, we'll 
display  the time in white.  White is color #7, and so our format word 
will be 7.
With that in mind, the following code shows how to display the time.  Recall
that when we start this code, R0 already holds a copy of HOUR
and the stack holds MIN, SEC and TIC.
MVII #2, R2 ; Set our field width to 2 MVII #7, R3 ; Set our format word to "white" MVII #$200 + 5*20 + 5, R4 ; Start in row 5, column 5 CALL PRNUM16.z ; Display the hours INCR R4 ; Leave a blank space after hours PULR R0 ; Get minutes CALL PRNUM16.z ; Display the minutes INCR R4 ; Leave a blank space after minutes PULR R0 ; Get seconds CALL PRNUM16.z ; Display the seconds INCR R4 ; Leave a blank space after seconds PULR R0 ; Get tics CALL PRNUM16.z ; Display the tics
Notice how PULR gets used to recall the previously saved 
values of HOUR, MIN and SEC.  The
PULR instructions mirror the PSHR 
instructions we used earlier to save these values.  With the stack, every
thing that gets pushed on the stack needs to be pulled off later.  Furthermore,
what gets pushed onto the stack gets pulled off in reverse order.  In this
case, we pushed the snapshot of TIC followed by the 
snapshot of SEC followed by the snapshot of MIN.
When we displayed the time, these came off the stack in reverse 
order—minutes, seconds and then tics.
Step 4: Go back to step 1
This is perhaps the easiest step. Since our code has nothing better to do than display the time, all it needs to do is jump back to the top of the loop. To do this, we can put a label before step 1, and branch to it. The resulting loop looks like, from end to end:
@@outer_loop:                       ; outermost loop
        ;; Step 1:  Wait for time to change
@@wait_time:
        MVI     TIC,    R0      ; Get current value of TIC
        CMP     PTIC,   R0      ; Has the time changed?
        BEQ     @@wait_time     ; No:  Keep looping
 
        ;; Step 2:  Snapshot the time 
        DIS                     ; Disable ints (begin critical section)
        MVI     TIC,    R0      ; Read tic count
        MVO     R0,     PTIC    ; Remember TIC in PTIC for next time
        PSHR    R0              ; Save tic count on the stack
        MVI     SEC,    R0      ; Read seconds count
        PSHR    R0              ; Save seconds count on the stack
        MVI     MIN,    R0      ; Read minutes count
        PSHR    R0              ; Save minutes count on the stack
        MVI     HOUR,   R0      ; Read hours count
        EIS                     ; Enable ints (end critical section)
        ;; Step 3:  Display the updated time
        MVII    #2,     R2      ; Set our field width to 2
        MVII    #7,     R3      ; Set our format word to "white"
        MVII    #$200 + 5*20 + 5, R4    ; Start in row 5, column 5
        CALL    PRNUM16.z       ; Display the hours
        INCR    R4              ; Leave a blank space after hours
        PULR    R0              ; Get minutes
        CALL    PRNUM16.z       ; Display the minutes
        INCR    R4              ; Leave a blank space after minutes
        PULR    R0              ; Get seconds
        CALL    PRNUM16.z       ; Display the seconds
        INCR    R4              ; Leave a blank space after seconds
        PULR    R0              ; Get tics
        CALL    PRNUM16.z       ; Display the tics
        ;; Step 4:  Go back to step 1.
        B       @@outer_loop
Now, you might wonder why we added a second label, @@outer_loop,
when there's no code between @@outer_loop and 
@@wait_time.  After all, it's not strictly necessary.  The
answer is pragmatism.
While it may be true at the moment that the additional label is redundant, that 
may not be true forever.  At some point, we might want to add code that runs 
before we wait for the time sync-up.  That code will need to go between the
two labels.  After all, displaying the updated time might be the last
thing we do, as opposed to the only thing we do.
Also, it's simply clearer.  Our last step is to "do everything again."  
This has different meaning than "wait for the time to change."  Descriptive
labels make it clearer to the reader what the purpose of a given branch is.
Labels are free.  Don't be afraid to add labels if it makes the code clearer.
Setting Things Up
So far, we've looked at the meat of the problem—the interrupt 
handler and the time display—but we've skipped entirely over the
problem of setting things up.  Most of that is pretty easy.  Four 
steps remain:  We must find a place to put the time, set the initial time
to zero, clear the screen, and make our ISR the current interrupt handler.
Allocating Our Variables
You might be thinking "We already have a place to put the time!"  Well,
yes and no.  In the fragments of code above, I've used the names 
PTIC, TIC, SEC, MIN 
and HOUR to refer to the various variables this program needs.
Those variables do indeed hold the current time, as well as the previous
value of TIC.
I haven't told the assembler where to put these though.  If you were to 
try to assemble the code above, you'd get errors for this reason.  One
way to rectify this is to assign addresses to each of these names with the
EQU directive.
Each of these quantities fits in 8 bits, so it's natural to allocate these
values in 8 bit Scratchpad RAM.  This memory resides at addresses 
$0100 - $01EF in the Intellivision memory map.  Locations $0100 - $0101 hold
the interrupt handler address.  The remaining locations are available for 
our use when we avoid using the EXEC.  (The EXEC uses some of these
locations for its internal bookkeeping.)
In this example, we do not use the bulk of the EXEC, and so we have the 
entire Scratchpad available to us.  Therefore, the following code snippet 
works just fine.  (There are better ways to do this in large programs.  We 
will look at those later.)
PTIC EQU $102 ; Previous value of 'tic' TIC EQU $103 ; Current number of tics (0..59) SEC EQU $104 ; Current number of seconds (0..59) MIN EQU $105 ; Current number of minutes (0..59) HOUR EQU $106 ; Current number of hours (0..59)
There are many ways we might initialize these values.  The most 
straightforward is just to zero out all of the Scratchpad RAM, except for
locations $100 and $101.  The SDK-1600 routine 
FILLZERO
is perfect for this task:
MVII #$102, R4 ; Start filling at location $102 MVII #$0EE, R1 ; Fill $EE locations (up through $1EF) CALL FILLZERO
This will clear out all of the Scratchpad memory, and thus set all five 
of our variables—PTIC, TIC, SEC,
MIN, and HOUR—to zero.  Hold that thought 
a moment, because we'll come back to it.
Clearing the Screen
The SDK-1600 function CLRSCR does a fine job of clearing the
screen.  This function resides in the same file as FILLZERO
and FILLMEM.  This is because the three functions are very 
closely related.  This makes it tempting to think a little harder about what
exactly we're doing. 
The straightforward next step in program initialization would be to 
CALL CLRSCR after clearing out the Scratchpad RAM.  The
screen resides at $0200 - $02EF, and this fills these locations with 0.
If you recall, the code above just got done filling locations $0102 - $01EF
with 0 as well.  It seems like we should be able to combine the two acts.
It turns out we can.  Addresses $01F0 - $01FF refer to the Programmable Sound Generator (aka. PSG).  We can safely write 0s to all of its locations.
In fact, this is the preferred way to initialize the PSG when its state is
otherwise unknown.  Thus, we can combine the FILLZERO and 
CLRSCR calls into a single FILLZERO call that
zeros $0102 - $02EF.  The combined code looks like so:
MVII #$102, R4 ; Start filling at location $102 MVII #$1EE, R1 ; Fill $1EE locations (up through $2EF) CALL FILLZERO
This kills two birds with one stone.  (Three birds if you count initializing
the PSG, although in this example it happens that the EXEC has done that for 
us already.)
Setting up the Interrupt Handler
Finally, all that's left is setting up the interrupt handler to point to
the time counting code we wrote above.  Recall that our routine is called 
MYISR.  The following code sets that up:
MVII #MYISR, R0 ; \ MVO R0, $100 ; |_ Write out "MYISR" to $100-$101 SWAP R0 ; | Double Byte Data. MVO R0, $101 ; / EIS ; Make sure interrupts are enabled
Putting it All Together
No Intellivision program is complete without an appropriate ROM header 
and INCLUDE directives for all of the library functions.  This particular
example needs 
fillmem.asm 
and 
prnum16.asm from SDK-1600.  Download these files and put them in a directory.
Then put the following source code in a new file named elapsed.asm
in the same directory.  This source code contains all of the snippets from
above, along with a ROM header to make it work.  The added portions are in 
bold.  As you can see, the additions are minor and are largely boilerplate
you've seen before from the Hello World Tutorial.
PTIC    EQU     $102    ; Previous value of 'tic'
TIC     EQU     $103    ; Current number of tics (0..59)
SEC     EQU     $104    ; Current number of seconds (0..59)
MIN     EQU     $105    ; Current number of minutes (0..59)
HOUR    EQU     $106    ; Current number of hours
        ROMW    16      ; 16-bit ROM
        ORG     $5000   ; Standard ROM memory map starts at $5000
;----------------------------------------------------------------------------
; EXEC-friendly ROM header.
;----------------------------------------------------------------------------
ROMHDR: BIDECLE ZERO            ; MOB picture base   (points to NULL list)
        BIDECLE ZERO            ; Process table      (points to NULL list)
        BIDECLE MAIN            ; Program start address
        BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
        BIDECLE ONES            ; GRAM pictures      (points to NULL list)
        BIDECLE TITLE           ; Cartridge title/date
        DECLE   $03C0           ; Flags:  No ECS title, run code after title,
                                ; ... no clicks
ZERO:   DECLE   $0000           ; Screen border control
        DECLE   $0000           ; 0 = color stack, 1 = f/b mode
ONES:   DECLE   1, 1, 1, 1, 1   ; Color stack initialization
;----------------------------------------------------------------------------
TITLE   STRING  $107, "Elapsed Time Demo", 0  ; Title string and date (2007)
MAIN    PROC
        MVII    #$102,  R4      ; Start filling at location $102
        MVII    #$1EE,  R1      ; Fill $1EE locations (up through $2EF)
        CALL    FILLZERO
        MVII    #MYISR, R0      ; \
        MVO     R0,     $100    ;  |_ Write out "MYISR" to $100-$101
        SWAP    R0              ;  |  Double Byte Data.
        MVO     R0,     $101    ; /
        EIS                     ; Make sure interrupts are enabled
@@outer_loop:                       ; outermost loop
        ;; Step 1:  Wait for time to change
@@wait_time:
        MVI     TIC,    R0      ; Get current value of TIC
        CMP     PTIC,   R0      ; Has the time changed?
        BEQ     @@wait_time     ; No:  Keep looping
        ;; Step 2:  Snapshot the time 
        DIS                     ; Disable ints (begin critical section)
        MVI     TIC,    R0      ; Read tic count
        MVO     R0,     PTIC    ; Remember TIC in PTIC for next time
        PSHR    R0              ; Save tic count on the stack
        MVI     SEC,    R0      ; Read seconds count
        PSHR    R0              ; Save seconds count on the stack
        MVI     MIN,    R0      ; Read minutes count
        PSHR    R0              ; Save minutes count on the stack
        MVI     HOUR,   R0      ; Read hours count
        EIS                     ; Enable ints (end critical section)
        ;; Step 3:  Display the updated time
        MVII    #2,     R2      ; Set our field width to 2
        MVII    #7,     R3      ; Set our format word to "white"
        MVII    #$200 + 5*20 + 5, R4    ; Start in row 5, column 5
        CALL    PRNUM16.z       ; Display the hours
        INCR    R4              ; Leave a blank space after hours
        PULR    R0              ; Get minutes
        CALL    PRNUM16.z       ; Display the minutes
        INCR    R4              ; Leave a blank space after minutes
        PULR    R0              ; Get seconds
        CALL    PRNUM16.z       ; Display the seconds
        INCR    R4              ; Leave a blank space after seconds
        PULR    R0              ; Get tics
        CALL    PRNUM16.z       ; Display the tics
        ;; Step 4:  Go back to step 1.
        B       @@outer_loop
        ENDP
MYISR   PROC
 
        MVO     R0,     $20     ; Keep screen enabled.
 
        MVI     TIC,    R0      ; Get current tic count into R0
        MVI     SEC,    R1      ; Get current seconds count into R1
        MVI     MIN,    R2      ; Get current minutes count into R2
        MVI     HOUR,   R3      ; Get current hours count into R3
 
        INCR    R0              ; Increment tic count
        CMPI    #60,    R0      ; Tic < 60?
        BLT     @@time_done     ; Yes:  Done updating time
 
        CLRR    R0              ; Reset tic count
        INCR    R1              ; Increment seconds count
        CMPI    #60,    R1      ; Seconds < 60?
        BLT     @@time_done     ; Yes:  Done updating time
 
        CLRR    R1              ; Reset seconds count
        INCR    R2              ; Increment minutes count
        CMPI    #60,    R2      ; Minutes < 60?
        BLT     @@time_done     ; Yes:  Done updating time
 
        CLRR    R2              ; Reset minutes count
        INCR    R3              ; Increment hours count
 
@@time_done:
        MVO     R0,     TIC     ; Store updated tic count
        MVO     R1,     SEC     ; Store updated seconds count
        MVO     R2,     MIN     ; Store updated minutes count
        MVO     R3,     HOUR    ; Store updated hours count
 
        JR      R5
        ENDP
;; =======================================================================
;;  Include SDK-1600 library functions
;; =======================================================================
        INCLUDE     "prnum16.asm"
        INCLUDE     "fillmem.asm"
To assemble the code above, type:
as1600 -o elapsed -l elapsed.lst elapsed.asm
And that's it.
    
The cartridge header tells the EXEC to minimize its initialization work 
and simply run the code that appears immediately after the title string.  
That code is our initialization code.  Once that code runs, it drops into 
the main program loop.
In the background, the interrupt handler updates the elapsed time with every interrupt from the STIC.
This causes the @@wait_time loop in the main program to exit,
and the rest of the code to update the displayed time. Wash, rinse, repeat.
Further Things to Contemplate About the Elapsed Time Demo
This timer is a pretty simple piece of code.  It's also specific to NTSC
systems, insofar as its notion of hours, minutes and seconds are built around
60 tics/second.  How would you change the code to keep time correctly on a
PAL/SECAM system that generates 50 interrupts per second?  How might you 
detect whether a given system is NTSC or PAL/SECAM at run time?
The timer currently does not display colons between the various time 
components.  Where might you insert code to do this?  Is that the best 
place for this code?  Why or why not?
The "tics" display goes by very quickly, so quickly that it's mostly a blur.
Typically, humans aren't very concerned about time displays with resolution
smaller than a second, except maybe on stopwatches.  How would you change 
this code to only update once a second?
What happens when this program runs for more than 99 hours?  More than 256
hours?  (That is, other than your Intellivision overheating . . . .)
With that in mind, how would you add a days indicator to the program? 
The background color behind the timer display is brown.  Why is this?  How
might you change this?
Example 2: A simple "wait timer"
The purpose of this example is to demonstrate how to introduces pauses in
a program.  The "wait timer" consists of two parts:  A short bit of code 
in the ISR that decrements a count if it's non-zero, and a separate function
that sets the count and waits for it to become zero.
The wait-timer serves two purposes:
- Long waits can space events out. This can be useful if you want to display a message for a moment or otherwise choreograph events.
- Short waits (such as 1 tic) are useful for waiting to ensure that the screen has updated since changing something that the interrupt handler might have acted on. That is, it's a way of figuring out that an interrupt has occurred.
We'll see an example of the first way of using the wait timer in this example.
Future tutorials will reuse this mechanism for both purposes, which is the
main reason I'm introducing it here.
The Interrupt Handler Code
The wait timer code can be inserted into any interrupt handler.  The code
for the wait timer is very simple.  It decrements the value of the variable
WTIMER if it's non-zero, as shown below:
MVI WTIMER, R0 ; Get current timer count DECR R0 ; Decrement it BMI @@expired ; If it went negative, it's expired MVO R0, WTIMER ; Store updated count @@expired:
This code can be inserted into any ISR easily.  Alternately, you can set up
many ISRs to call this at the end of their run before exiting.  That's 
entirely up to how you want your program to work.  In this example, we'll have
a simple ISR that enables the screen, counts down the wait timer, and then
exits.  The entire ISR is shown below:
;; ======================================================================== ;;
;;  MYISR -- A simple interrupt service routine                             ;;
;; ======================================================================== ;;
MYISR   PROC
        MVO     R0,     $20     ; Enable the display
         
        MVI     WTIMER, R0      ; Get current timer count
        DECR    R0              ; Decrement it
        BMI     @@expired       ; If it went negative, it's expired
        MVO     R0,     WTIMER  ; Store updated count
@@expired:
        
        JR      R5              ; return from interrupt
        ENDP
A more complicated program would clearly have more stuff in its interrupt
handler.  We'll see this in future tutorials.
The Wait Code
Programs will call the following function to actually wait for something to
happen.  This function has two entry points.  WAIT will wait 
for the number of tics specified in the word after the CALL.
(See 
the branch tutorial for an explanation of this technique.)  The second entry
point expects the number of tics to wait to be in R0.
;; ======================================================================== ;;
;;  WAIT -- Wait for some number of tics                                    ;;
;;                                                                          ;;
;;  INPUTS for WAIT                                                         ;;
;;      1 decle after call:  Number of tics to wait                         ;;
;;                                                                          ;;
;;  INPUTS for WAIT.1                                                       ;;
;;      R0  Number of tics to wait                                          ;;
;;                                                                          ;;
;;  OUTPUTS                                                                 ;;
;;      R0  Zeroed                                                          ;;
;;                                                                          ;;
;;  NOTE                                                                    ;;
;;      Requires WTIMER code in ISR, and interrupts enabled.                ;;
;;                                                                          ;;
;; ======================================================================== ;;
WAIT    PROC
        MVI@    R5,     R0          ; Get # of tics to wait from after CALL
@@1:    MVO     R0,     WTIMER      ; Set up wait timer
 
        CLRR    R0                  ; \
@@wait: CMP     WTIMER, R0          ;  |- Wait for WTIMER = 0
        BNEQ    @@wait              ; /
 
        JR      R5                  ; return
        ENDP
The Rest
For this example, we'll display "Hello" and "World" about one second apart in as we did previously in the Hello World Tutorial. We'll also use the an infinite loop. We'll reuse SDK-1600's PRINT and CLRSCR functions. This results in the following code:
MAIN    PROC
 
        CALL    CLRSCR              ; Clear the screen
 
        MVII    #MYISR, R0          ; \
        MVO     R0,     $100        ;  |_ Write out "MYISR" to $100-$101
        SWAP    R0                  ;  |  Double Byte Data.
        MVO     R0,     $101        ; /
        EIS                         ; Make sure interrupts are enabled
 
@@loop:
        ;; Print "Hello" at row #5, column #7.  
        CALL    PRINT.fls
        DECLE   7, $200 + 5*20 + 7
        STRING  "Hello", 0
 
        ;; Wait for 1 second
        CALL    WAIT
        DECLE   60
 
        ;; Print "World" at row #5, column #7.  
        CALL    PRINT.fls
        DECLE   7, $200 + 5*20 + 7
        STRING  "world", 0
 
        ;; Wait for 1 second
        CALL    WAIT
        DECLE   60
 
        ;; Do it again
        B   @@loop
 
        ENDP
There really isn't much to this example, is there?
Putting it All Together
The source listing below puts all the fragments above into a complete program.
Mainly, this just adds the cartridge header, the INCLUDE
directives to include 
print.asm and
fillmem.asm
from SDK-1600, and assigns WTIMER a location in 8-bit memory.  
The bold portions are the new portions.
WTIMER  EQU     $102    ; Put WTIMER into 8-bit memory
        ROMW    16      ; 16-bit ROM
        ORG     $5000   ; Standard ROM memory map starts at $5000
;----------------------------------------------------------------------------
; EXEC-friendly ROM header.
;----------------------------------------------------------------------------
ROMHDR: BIDECLE ZERO            ; MOB picture base   (points to NULL list)
        BIDECLE ZERO            ; Process table      (points to NULL list)
        BIDECLE MAIN            ; Program start address
        BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
        BIDECLE ONES            ; GRAM pictures      (points to NULL list)
        BIDECLE TITLE           ; Cartridge title/date
        DECLE   $03C0           ; Flags:  No ECS title, run code after title,
                                ; ... no clicks
ZERO:   DECLE   $0000           ; Screen border control
        DECLE   $0000           ; 0 = color stack, 1 = f/b mode
ONES:   DECLE   1, 1, 1, 1, 1   ; Color stack initialization
;----------------------------------------------------------------------------
TITLE   STRING  $107, "Wait Timer Demo", 0  ; Title string and date (2007)
MAIN    PROC
        CALL    CLRSCR              ; Clear the screen
 
        MVII    #MYISR, R0          ; \
        MVO     R0,     $100        ;  |_ Write out "MYISR" to $100-$101
        SWAP    R0                  ;  |  Double Byte Data.
        MVO     R0,     $101        ; /
        EIS                         ; Make sure interrupts are enabled
 
@@loop:
        ;; Print "Hello" at row #5, column #7.  
        CALL    PRINT.FLS
        DECLE   7, $200 + 5*20 + 7
        STRING  "Hello", 0
 
        ;; Wait for 1 second
        CALL    WAIT
        DECLE   60
 
        ;; Print "World" at row #5, column #7.  
        CALL    PRINT.FLS
        DECLE   7, $200 + 5*20 + 7
        STRING  "world", 0
 
        ;; Wait for 1 second
        CALL    WAIT
        DECLE   60
 
        ;; Do it again
        B   @@loop
 
        ENDP
 
;; ======================================================================== ;;
;;  MYISR -- A simple interrupt service routine                             ;;
;; ======================================================================== ;;
MYISR   PROC
        MVO     R0,     $20     ; Enable the display
         
        MVI     WTIMER, R0      ; Get current timer count
        DECR    R0              ; Decrement it
        BMI     @@expired       ; If it went negative, it's expired
        MVO     R0,     WTIMER  ; Store updated count
@@expired:
        
        JR      R5              ; return from interrupt
        ENDP
 
;; ======================================================================== ;;
;;  WAIT -- Wait for some number of tics                                    ;;
;;                                                                          ;;
;;  INPUTS for WAIT                                                         ;;
;;      1 decle after call:  Number of tics to wait                         ;;
;;                                                                          ;;
;;  INPUTS for WAIT.1                                                       ;;
;;      R0  Number of tics to wait                                          ;;
;;                                                                          ;;
;;  OUTPUTS                                                                 ;;
;;      R0  Zeroed                                                          ;;
;;                                                                          ;;
;;  NOTE                                                                    ;;
;;      Requires WTIMER code in ISR, and interrupts enabled.                ;;
;;                                                                          ;;
;; ======================================================================== ;;
WAIT    PROC
        MVI@    R5,     R0          ; Get # of tics to wait from after CALL
@@1:    MVO     R0,     WTIMER      ; Set up wait timer
 
        CLRR    R0                  ; \
@@wait: CMP     WTIMER, R0          ;  |- Wait for WTIMER = 0
        BNEQ    @@wait              ; /
 
        JR      R5                  ; return
        ENDP
 
;; ======================================================================== ;;
;;  Library includes                                                        ;;
;; ======================================================================== ;;
        INCLUDE     "fillmem.asm"
        INCLUDE     "print.asm"
Copy this out to a file, wtdemo.asm, along with copies of 
print.asm and fillmem.asm that were linked to above.
Then assemble with:
as1600 -o wtdemo -l wtdemo.lst wtdemo.asm
Ta da! When you run this program, you should get a display that alternates between "Hello" and "World" in the middle of the screen with a 1 second delay between each.
