Difference between revisions of "Introducing Interrupts"
Revision as of 20:35, 21 November 2007
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
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.
- 1 Interrupts and Interruptibility
- 2 What Happens When the CPU Takes an Interrupt?
- 3 Reasons for Turning Off Interrupts
- 4 Example Using Interrupts: A simple timer
- 4.1 The Interrupt Handler
- 4.2 The Main Loop
- 4.3 Setting Things Up
- 4.4 Putting it All Together
- 4.5 Further Things to Contemplate
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
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
JSRE instructions. They can disable
(mask) interrupts with
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 (
PSHR), shifts/rotates and
the various 4 cycle instructions such as
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
- 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 determines 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 [http://spacepatrol.info/ 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
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:
Suppose also that an interrupt handler tries to decrement this same variable:
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
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 its
value. Also, the decremented value isn't even the right value.
This type of code is referred to as a "critical section." The
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.
To fix the example above, you can wrap the critical code with
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:
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
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.
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.
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 Using Interrupts: A simple timer
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 timer onscreen 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 per 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
- 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
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
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
@@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
Step 3: Displaying the time
To display the time, we'll use the SDK-1600
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.
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
and the stack holds
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
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
PULR gets used to recall the previously saved
PULR instructions mirror the
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
SEC followed by the snapshot of
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
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,
when there's no code between
@@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
HOUR to refer to the various variables this program needs.
Those variables do indeed hold the current time, as well as the previous
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
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
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—
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
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
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
Setting up the Interrupt Handler
Finally, all that's left is setting up the interrupt handler to point to
the timer 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
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
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, "Timer 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
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 timer -l timer.lst timer.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 time every timer tic. 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
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?