==============================================================================
  INTELLIVISION INTERRUPTS
==============================================================================

This file describes information about the Intellivision's 60Hz (or 50Hz
for PAL) VBLANK interrupt.  Much of the information below is adapted
from posts I (Joe Zbiciak) made to INTVPROG or other information I have 
posted to the web.  Many of these posts I've summarized were collected for
me by Ryan Kinnen.  (Thanks Ryan!)

This is not meant to be an exhaustive guide to Intellivision interrupts
and all their hardware peculiarities.  It is intended to be a reliable
guide to Intellivision interrupts for people writing new games.


------------------------------------------------------------------------------
  OVERVIEW:  What is an Interrupt?
------------------------------------------------------------------------------

Interrupts are external signals that are connected to a CPU.  They are
a way of telling the CPU that some device external to them needs attention.  
They can also provide a way for keeping time.

On the CP-1600, there are two flavors of interrupt:  Maskable Interrupt
and Non-maskable Interrupt.  This document only covers Maskable Interrupts,
as the Non-maskable Interrupt pin is not connected to anything in the
Intellivision.

Interrupts can come from various sources, such as peripherals, timers, and
so on.  On the PC that many of you are familiar with, you've probably seen
the term "IRQ #" associated with various add-in devices, such as a sound
card, mouse port, or other add-in card.  IRQ stands for "Interrupt Request", 
and in the world of the PC, the IRQ # is a mechanism for sorting out 
interrupt sources.

The Intellivision world is much simpler.  Interrupts come from one place,
and one place only:  The STIC.  


------------------------------------------------------------------------------
  INTELLIVISION INTERRUPTS:  The VBLANK Interrupt
------------------------------------------------------------------------------

The STIC chip (AY-3-8900) is the display chip at the heart of the
Intellivision.  It provides the graphical display output.  It also is
the sole source of interrupts in the Intellivision Master Component.

The STIC generates an interrupt at the end of active display.  This means
that it produces interrupts at a fixed rate that is tied to the refresh
rate of the display.  For NTSC Intellivisions, this rate is approximately
60Hz.  For PAL Intellivisions, the rate is 50Hz.  The interrupt occurs at
the very end of active display, where the STIC "blanks" the display and
starts displaying the solid screen border at the bottom of the screen.
For this reason, is often called the Vertical Blanking (or VBLANK)
interrupt.

The STIC interrupts the CPU during VBLANK because the STIC needs a little
attention every frame.  This is the primary purpose of the interrupt.
The reason for this is that certain STIC-controlled resources are only
available to the CPU during the VBLANK period, as long as the display
is enabled.  Specifically, the following are only visible during VBLANK:

 -- The GROM and GRAM.  These are visible for approximately 3780 cycles.
 -- The STIC's registers.  These are visible for approximately 2000 cycles.

During active display, the STIC has full control over the GROM, GRAM,
and its registers, so CPU accesses to them will not work -- writes will
be dropped and reads will return garbage.

Because these resources available for a short period of time, the STIC
does allow more unlimited access as long as the display is blanked.  This
allows for quicker initialization of GRAM and the STIC.  This leads to the
second reason for the VBLANK interrupt:  To enable the display, the
STIC Display Enable register at location $0020 must be written to on
EVERY frame.  If this register is not written, the display will blank.
(VBLANK interrupts will still occur at a fixed rate, though.)

For these reasons, an interrupt service routine in a typical game will
is responsible for doing the following:

 -- First, if the game wants the display to actually be displayed, the
    routine must hit the display enable by writing to location $0020.
    The STIC ignores the value written -- just write anything there.

 -- Next, it must update any of the STIC registers -- read MOB
    collisions, update MOB positions, slide the scrolling registers,
    update the color stack, whatever.  Again, these are only available
    during VBLANK, and are unavailable during active display.

 -- After that, it must update any graphics cards in GRAM.  GRAM is only
    accessible during VBLANK, but it is accessable for longer than the 
    STIC registers are, and so you'll typically start your GRAM updates 
    after your register updates are complete.
    

Although the VBLANK interrupt is primarily used for feeding updates to the
STIC, it also happens to be very useful as a source of timing information.
Its fixed clock rate makes it a useful "heartbeat" for the system.
Therefore, once all the graphics responsibilities are taken care of,
usually the VBLANK routine processes anything that needs a fixed
time-base, such as:

 -- Update any sounds that are playing.  You don't like music which
    slows down or speeds up uncontrollably, do you?  Or sound effects 
    that pause all of the action?  Didn't think so.

 -- Update timers, clocks, or other sources of "timebase" information.

------------------------------------------------------------------------------
  VBLANK INTERRUPT NUMBERS
------------------------------------------------------------------------------

Below is a quick summary of the numbers given above for the VBLANK
interrupts, plus a few others.  The NTSC numbers are fairly accurate.
The PAL numbers are estimates.

    NTSC Intellivision Timing Parameters     
 
    Parameter                                   Measurement
    NTSC STIC Clock Rate                        3579545 Hz
    STIC Scanlines per Frame                    262 scanlines
    Active Scanlines per Frame                  192 scanlines
    Actual Effective Frame Rate                 59.92 Hz
    CP-1600 Clock Rate (NTSC/4)                 894886.25 Hz
    CPU Cycles per Frame:
     -- Display disabled                        14934 cycles
     -- Display enabled, vertical delay == 0   ~13518 cycles
     -- Display enabled, vertical delay > 0    ~13572 cycles
    CPU Cycles per Scanline                     57 cycles
    CPU Cycles available in Bus Copy mode       3780 - 3790 cycles
 
 
    PAL Intellivision Timing Parameters (ESTIMATES)
 
    Parameter                                   Measurement
    PAL STIC Clock Rate                         4000000 Hz
    STIC Scanlines per Frame                 ?? 312 scanlines
    Active Scanlines per Frame                  192 scanlines
    Actual Effective Frame Rate                ~50.00 Hz
    CP-1600 Clock Rate (PAL/4)                  1000000 Hz
    CPU Cycles per Frame:
     -- Display disabled                       ~20000 cycles
     -- Display enabled, vertical delay == 0 ??~18584 cycles
     -- Display enabled, vertical delay > 0  ??~18638 cycles
    CPU Cycles per Scanline                  ?? 57 cycles
    CPU Cycles available in Bus Copy mode    ?? 8840 - 8850 cycles


NOTE:  The estimates marked "??" are very possibly BOGUS.  You have
       been warned.  If you happen to measure these values, PLEASE LET
       ME KNOW THE RIGHT NUMBERS!!!  Thanks!


------------------------------------------------------------------------------
  INTERRUPT DISPATCHING
------------------------------------------------------------------------------

When the CP-1600 receives an interrupt request, it first needs to
decide whether to take the interrupt or not.  If the CPU is executing
non-interruptible instructions (such as MVO or shifts), or if interrupts
are masked (say with DIS, JD or JSRD), the CPU ignores the interrupt
for as long as interrupts are blocked.  Note that the CPU only has
a couple thousand cycles to recognize the interrupt, so keep those
non-interruptible sequences short.

Once the CPU recognizes the interrupt, it performs the following steps:

 -- It issues an "INTAK" (Interrupt AcKnowledge) bus phase.  This tells
    the System RAM and the STIC that it saw the interrupt and is beginning
    its interrupt processing.  This enables "Bus Copy" mode in the
    System RAM, which allows the CPU to see the STIC's registers.
 
 -- It pushes the current Program Counter (R7) address onto the stack.

 -- It branches to location $1004 and executes the code there.


> NOTE:  If you want the display to remain enabled or want access to GRAM
> and GROM, it is important that you actually allow the CPU to take
> interrupts.  If you leave interrupts disabled for extended periods of
> time, the CPU will not issue an "INTAK", and you will not be able to 
> access the STIC, GRAM or GROM.  Also, it appears that STIC registers 
> lose their state after a few frames if interrupts are not taken regularly.


The code at $1004 is an EXEC routine which dispatches the interrupt
and handles returning from the interrupt.  The dispatch portion of the
EXEC code always runs.  The return code runs only at your discretion.
When an interrupt is taken, all 6 registers are saved, along with the
current state of the flags.  (It's not necessary to save R6 and R7.
Think about it.)  Then, the dispatcher calls the function whose address
is stored in locations $0100 / $0101. 

If your interrupt handler behaves like a normal function call (eg.
returns to the address in R5 when its done), the EXEC's interrupt
dispatcher will restore all the registers and flags for you.  If it does
not, then this responsibility falls to your code.  (You may have very
specific reasons for doing this.)

This is what the dispatcher and return code look like:

L1004:  PSHR    R0          ; $1004  Save R0
        GSWD    R0          ; $1005  \__ Save flags
        PSHR    R0          ; $1006  /
        PSHR    R1          ; $1007  Save R1
        PSHR    R2          ; $1008  Save R2
        NOP                 ; $1009  interruptible, needed for STIC
        PSHR    R3          ; $100A  Save R3
        PSHR    R4          ; $100B  Save R4
        PSHR    R5          ; $100C  Save R5
        MOVR    R7,     R5  ; $100D  \__  R5 points to $1014 
        ADDI    #$0006, R5  ; $100E  /
        MVII    #$0100, R4  ; $1010  \
        SDBD                ; $1012   |-- Dispatch to user ISR at $100/$101.
        MVI@    R4,     R7  ; $1013  /

L1014:  PULR    R5          ; $1014  Restore R5
        PULR    R4          ; $1015  Restore R4
        PULR    R3          ; $1016  Restore R3
        PULR    R2          ; $1017  Restore R2
        PULR    R1          ; $1018  Restore R1
        PULR    R0          ; $1019  \__ Restore flags
        RSWD    R0          ; $101A  /
        PULR    R0          ; $101B  Restore R0
        PULR    R7          ; $101C  Return to game


The code from $1004 through $1013 comprises the dispatcher.  The code
from $1014 through $101C is the return code.

------------------------------------------------------------------------------
  INTERRUPT SERVICE ROUTINE (ISR) VECTOR            
------------------------------------------------------------------------------

The locations $0100 and $0101 mentioned above are sometimes referred to
as the "Interrupt Service Routine Vector" or "ISR Vector".  ("Interrupt
Service Routine" is usually abbreviated ISR.)   Locations $0100 and $0101 
are the locations that you must update in order to "hook" the VBLANK 
interrupt.

To hook the interrupt, you must write the address of your ISR to these
locations in "double-byte data" format.  Be careful:  This sequence must
be non-interruptible.  You do not want the dispatcher to run between the
updates to $0100 and $0101.  Here are two common, safe sequences for 
hooking the VBLANK interrupt:

    
        ; Assume R0 has address of interrupt handler
        MVII    #$100,  R4
        MVO@    R0,     R4
        SWAP    R0
        MVO@    R0,     R4

    or:

        ; Assume R0 has address of interrupt handler
        MVO     R0,     $100
        SWAP    R0
        MVO     R0,     $101

These are safe because MVO@ and SWAP are both non-interruptible.
This means that an interrupt cannot be serviced between the first MVO
and last MVO instruction.


If you're writing an EXEC based game, you need to perform an additional
step BEFORE you hook the interrupt.  Also, your ISR must perform an 
additional step at the end of its processing.  You must save the old 
value of the ISR vector, and remember to branch to that address after 
running your own ISR.  For example:


        MVII    #$100,  R4
        SDBD
        MVI@    R4,     R1        ; Get address of original ISR
        MVO     R1,     OLDISR    ; Save it somewhere in 16-bit RAM

        MVII    #MYISR, R0        ;\
        SUBI    #2,     R4        ; |
        MVO@    R0,     R4        ; |-- Install my ISR routine.
        SWAP    R0                ; |
        MVO@    R0,     R4        ;/

Then, in your ISR, you'd do something like this:

MYISR   PROC
        PSHR    R5                ; Save return address

        ; ... do stuff

        PULR    R5                ; Restore return address in R5
        MVI     OLDISR, PC        ; Call previous interrupt handler
        ENDP

This form of ISR chaining can cause other problems, particularly if your
ISR is fairly lengthy.  Discussion of those problems is beyond the scope
of this document.


------------------------------------------------------------------------------
  PROGRAMMING IN THE PRESENCE OF INTERRUPTS
------------------------------------------------------------------------------

Because interrupts save and restore the state of the machine, most code
typically does not have to worry about interrupts.  Interrupts are like
TV commercials -- you fade out, have the commercial, fade in, and resume
the show.

As a result, life is mostly worry-free even with interrupts.  There are
a few things, though, you do need to worry about:  (The list looks long,
but it actually pretty short -- I am just a stickler for details.)

 -- Make sure interrupts are enabled most of the time.  (eg. use
    "EIS" or "JSRE" to enable them.)  This ensures they're serviced
    in a timely fashion, so that you have plenty of time for
    accessing the STIC, etc.

 -- Make sure you don't accidentally block interrupts by having
    too many "non-interruptible" instructions in a row.  (On the
    CP-1600, the shift instructions, MVO/PSHR instructions, and a
    few others are "non-interruptible.")  For other hardware reasons,
    you should have no more than about three or four of these in
    a row.  (About 4-5 shifts, or 3-4 MVOs -- keep it to no more than
    about ~30 - ~40 cycles.)  Insert a NOP if you have to.  

    For example, I broke this long sequence of MVO's with a DECR
    and a NOP.  In this example, I have no more than 3 in a row,
    since these are direct-addressing MVOs which take 11 cycles
    each.  Having 4 direct-mode MVOs in a row may be ok, but when
    you're not sure, erring on the side of caution doesn't hurt.

STOPALLTASKS    PROC
        MVI     TSKACT, R0
        CLRR    R1
        MVO     R1,     TSKACT
        MVO     R1,     TSKQHD
        MVO     R1,     TSKQTL
        DECR    R1
        MVO     R1,     TSKTBL+2        ; period == -1 for task 0
        MVO     R1,     TSKTBL+6        ; period == -1 for task 1
        MVO     R1,     TSKTBL+10       ; period == -1 for task 2
        NOP                             ; (interruptible, for STIC)
        MVO     R1,     TSKTBL+14       ; period == -1 for task 3
        JR      R5

        ENDP

 -- If you have any data in memory that's accessed by both your
    interrupt routine AND your main program, make sure you access that
    data "atomically" from the main program.  To be really safe, guard
    accesses to this data by disabling interrupts, accessing the data,
    and then re-enabling interrupts.  Otherwise, if an interrupt happens
    and mucks with this data while you're while you're accessing it,
    bad things can happen.  In embedded programming parlance, these are
    called "critical sections".

    For example, in 4-Tris, I have a set of task structures in memory
    that are updated from both the interrupt handler and the main program.
    So, when I access these from the main program, I shut off interrupts
    around the access like so:

STARTTASK       PROC
        MVI@    R5,     R0
        SLL     R0,     2       ; R0 = R0 * 4 (four words/entry)
        MVII    #TSKTBL,R4      ; R4 = &TSKTBL[0]
        ADDR    R0,     R4      ; R4 = &TSKTBL[n]

        DIS                     ; Entering critical section.
        MVI@    R5,     R0      ; ! Get Function pointer
        MVO@    R0,     R4      ; ! ... and write it
        MVO@    R2,     R4      ; ! Write task instance data
        MVI@    R5,     R0      ; ! Get task period
        MVO@    R0,     R4      ; ! ... and write it
        MVI@    R5,     R0      ; ! Get task period reinit
        MVO@    R0,     R4      ; ! ... and write it
        EIS                     ; Done with critical section.

        JR      R5
        ENDP

    Another example is updating locations $100/$101 to point to your own
    interrupt handler.  (That is covered in the previous section.)  You 
    need to either disable interrupts, or use a non-interruptible sequence
    of instructions to update these locations to point to your handler.
    As noted previously, the following sequences work quite nicely:

        ; Assume R0 has address of interrupt handler
        MVII    #$100,  R4
        MVO@    R0,     R4
        SWAP    R0
        MVO@    R0,     R4

    or:

        ; Assume R0 has address of interrupt handler
        MVO     R0,     $100
        SWAP    R0
        MVO     R0,     $101

    These work because MVO and SWAP are non-interruptible, which
    means an interrupt cannot be taken immediately after that 
    instruction.  For these sequences, it ends up meaning that no
    interrupt can occur between the first MVO and the second MVO, which
    is what we desire.

 -- Keep your interrupt handler short.  If the interrupt handler
    takes too many cycles, then the next interrupt will come before the
    first one finishes, and you'll either lose the interrupt or you'll
    starve the main program for cycles.

 -- Make sure you have enough stack space.  The interrupt dispatcher
    in the EXEC needs 9 or so words of stack space just to dispatch
    to your ISR, so you need to be sure you have enough headroom on
    the stack.  Stack overflow bugs are hard to track down.  The stack
    normally starts at $2F1 and grows UPWARDS (towards higher-numbered
    addresses).

And one last thing:

 -- If you have functions that are called by both the interrupt
    handler and the main program, make sure these functions are REENTRANT.
    A function is reentrant if it stores all of its local state in
    registers and/or on the stack.  If it has fixed locations that it
    uses for temporary storage, it's likely not reentrant.

    For instance, if you look at the "DEC16" and "DEC32" functions
    in 4-Tris (or in the SDK-1600 library), these functions are not
    reentrant because they have fixed temporary storage.  If I call
    this function in my main program, then it gets interrupted, and in
    my ISR I call the same function -- *BLAM* -- I kill the temporary
    values that were stored by the main program and Bad Things happen.
    (I actually had a bug like this while I was debugging 4-Tris.
    Ironically, the bug was in my debugging code which displayed some
    status info from the interrupt handler.  ;-)

    One way to handle non-reentrant code is to protect it as though it
    were a critical-section.  For very short routines, this may be 
    acceptable, but typically it ends up locking out interrupts for 
    far too long.  Another solution is to save the information from the
    fixed local storage on the stack prior to calling the function,
    and restore it afterwards.  While it's more work, it's typically
    much more acceptable.  

    For example, DEC_16 stores two bytes worth of data in the local
    variables "DEC_0" and "DEC_1".  We can store those in a single
    word on the stack, and restore them later.  Note that the following
    code relies on DEC_0 and DEC_1 being stored in 8-bit RAM.  

DEC16_WRAPPER   PROC

        PSHR    R5              ; Save return address

        MOVR    R0,     R5      ; Save R0 temporarily
        MVI     DEC_1,  R0      ; \
        SWAP    R0              ;  |__ Save DEC_0 and DEC_1
        XOR     DEC_0,  R0      ;  |
        PSHR    R0              ; /
        MOVR    R5,     R0      ; Restore R0
    
        CALL    DEC16

        PULR    R0              ;
        MVO     R0,     DEC_0   ; \
        SWAP    R0              ;  |-- Restore DEC_0 and DEC_1
        MVO     R0,     DEC_1   ; / 
    
        PULR    PC              ; Return
        ENDP


------------------------------------------------------------------------------
  ISR TRICKS
------------------------------------------------------------------------------

Here are a few tricks I've developed over time related to interrupts.
These apply to non-EXEC games only.

--------------------
 SAVING STACK SPACE
--------------------

When writing a non-EXEC game, one typically does not need to chain to
the original interrupt handler.  This also means that one will always
return to a fixed address from an ISR:  Location $1014 in the interrupt
dispatcher and return code.

What this means is that it is not necessary to save the return address
in R5 inside your ISR.  Recall that a typical ISR behaves like a normal
function call.  It therefore saves the return address in R5, and branches
to that address when its done:


ISR     PROC
        PSHR    R5      ; save return address

        ; do stuff

        PULR    PC      ; return to saved address
        ENDP

This uses a whole word of stack space to store a value that could be
considered constant.  How wasteful!

Instead, you can safely just branch to location $1014 directly.  If
you're using a 16-bit wide ROM, this can be accomplished with a "B"
instruction:

ISR     PROC

        ; do stuff

        B       $1014   ; return via ISR return code
        ENDP

Alternately, if you're in a narrower ROM (such as a 10-bit ROM), you
can use the J instruction instead:


ISR     PROC

        ; do stuff

        J       $1014   ; return via ISR return code
        ENDP


Either approach ends up saving a couple cycles, and more importantly,
saves a word of stack space.


----------------
 INITIALIZATION
----------------

Typically, at the start of the game, you need to blast a bunch of
information into the GRAM, and you don't care about returning into the
EXEC or anything.  In those cases, your "ISR" really doesn't need to
worry about restoring the state of the machine and returning to the
interrupted code.

Rather, it's convenient in an initialization routine to just take over
the machine.  Such an "initialization interrupt handler" typically will 
do the following:

 -- Disable interrupts.

 -- Reset the stack pointer, discarding whatever was on the stack.
    In a non-EXEC game, $2F0 is a good place to reset the stack pointer
    to.  (The EXEC usually initializes it to $2F1, because it keeps
    a magic number at $2F0.)

 -- Install the real ISR routine in the ISR vector.

 -- Blast data into the GRAM and STIC registers.

 -- Re-enable interrupts.

 -- Branch to the start of the program.

Notice that this interrupt handler does not return.  It completely
loses the state of the code that it interrupts.  Therefore, it is
common to set up the vector for the initialization ISR, and then
pause the CPU with a "spin loop."

4-Tris uses this particular technique.  The following example is 
based loosely on 4-Tris' code for accomplishing this trick:


;;==========================================================================;;
;;  TITLE / START                                                           ;;
;;  The title string followed by our startup code.                          ;;
;;==========================================================================;;
TITLE:  BYTE    102, "GAME", 0          ; Title: GAME, Copyright 2002
START:  ; Intercept/preempt EXEC initialization and just do our own.

        MVII    #INIT,  R0              ; Our initialization routine
        MVII    #$100,  R4              ; ISR vector
        MVO@    R0,     R4              ; Write low half
        SWAP    R0                      ;
        MVO@    R0,     R4              ; Write high half
                                       
        EIS                             ; Enable interrupts
@@spin: DECR    PC                      ; Wait for one to happen

;;==========================================================================;;
;;  INIT                                                                    ;;
;;  Initializes the ISR, etc.  Gets everything ready to run.                ;;
;;  This is called via the ISR dispatcher, so it's safe to bang GRAM from   ;;
;;  here, too.                                                              ;;
;;                                                                          ;;
;;   -- Zero out memory to get started                                      ;;
;;   -- Set up variables that need to be set up here and there              ;;
;;   -- Set up GRAM image                                                   ;;
;;   -- Drop into the main game state-machine.                              ;;
;;==========================================================================;;
INIT:   PROC
        DIS
        MVII    #$2F0,  R6              ; Reset the stack pointer

        CLRR    R4                      ; zero all system RAM, PSG0,& STIC.
        MVII    #$20,   R1              ; $00...$1F. (The STIC)
        CALL    FILLZERO

        ADDI    #8,     R4              ; $28...$32. (The rest of the STIC)
        MVII    #11,    R1
        CALL    FILLZERO

        MVII    #$F0,   R4              ; $F0...$35D. We spare the rand seed 
        MVII    #$26D,  R1              ; values in $35E..$35F to add some 
        CALL    FILLZERO                ; randomness.

        MVI     COLSTK, R1              ; Force display to color-stack mode

        MVII    #MAINISR, R0            ; Point ISR vector to our ISR
        MVO     R0,     $100            ; store low half of ISR vector
        SWAP    R0                      ;
        MVO     R0,     $101            ; store high half of ISR vector


        ; Default the GRAM image to be same as GROM.
        MVII    #GROM,  R5              ; Point R5 at GROM
        MVII    #GRAM,  R4              ; Point R4 at GRAM
        MVII    #$200,  R0
@@gromcopy:
        MVI@    R5,     R1
        MVO@    R1,     R4
        DECR    R0
        BNEQ    @@gromcopy

        ; Copy our GRAM font into GRAM overtop of default.
        CALL    LOADFONT
        DECLE   FONT

        ; Ok, everything's ready to roll now.
        EIS


        ;; TOP LEVEL GAME STATE MACHINE LIVES HERE.

        ENDP


------------------------------------------------------------------------------
  OTHER Q & A
------------------------------------------------------------------------------

I have captured much of the useful Intellivision interrupt information 
above.  Much of the above material is adapted from posts I've made to
INTVPROG.  Still, it isn't exhaustive.  Below are some mostly un-edited 
emails that fill in a few gaps.  The only edits are corrections.

These emails (and the ones which sourced the material above) were collected 
for me by Ryan Kinnen.  (Thanks again, Ryan!)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

euphoric_cat [euphoric_cat@yahoo.com] wrote:
| My actual problem turns out that I was trying to execute too many 
| instructions between the time that the VBLANK interrupt occurs, 
| and the time the $0020 handshake was occurring.  

You're probably best off just punching $0020 as one of the first
instructions in your ISR.

I discovered that STIC registers are only accessible for about 2000
cycles during and ISR, while the GRAM is accessible for longer (~3780
cycles).

The game I'm currently working on uses all of the available GRAM
cycles, which is why I noticed.  :-)  My MOB updates weren't working
right when I performed them after my GRAM updates, but they worked
right when I did them before the GRAM.  I investigated further
and worked out the 2000 vs 3780 number.  (These numbers aren't exact --
I'm just remembering approximate figures off the top of my head.)

Besides, it doesn't pay to be too exact, since there could be a
bit of latency in dispatching to your ISR.  You're better off to
underestimate and thus have some slack.

Regards,

--Joe


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Not Your Average Joe [im14u2c@primenet.com] wrote:
| This is what the dispatcher looks like:
| 
| L1004:  PSHR    R0          ; $1004  Save R0
|         GSWD    R0          ; $1005  \__ Save flags
|         PSHR    R0          ; $1006  /
|         PSHR    R1          ; $1007  Save R1
|         PSHR    R2          ; $1008  Save R2
|         NOP                 ; $1009  interruptible, needed for STIC
|         PSHR    R3          ; $100A  Save R3
|         PSHR    R4          ; $100B  Save R4
|         PSHR    R5          ; $100C  Save R5
|         MOVR    R7,     R5  ; $100D  \__  R5 points to $1014 
|         ADDI    #$0006, R5  ; $100E  /
|         MVII    #$0100, R4  ; $1010  \
|         SDBD                ; $1012   |-- Dispatch to user ISR at $100/$101.
|         MVI@    R4,     R7  ; $1013  /


It occurs to me that I didn't explain why that NOP needed to be there.

It has to do with how the STIC reads BACKTAB.

13 to 14 times a frame, the STIC asks the CPU to halt (using the BUSRQ
bus request line).  During these halts, the STIC twiddles the system RAM,
fetching the next row of cards into a special buffer.  Once the cards
are fetched, it releases the CPU.

For BUSRQ to work, the CPU must be executing interruptible
instructions.  The instructions must be of the "interruptible"
type.  It does not matter if the global Interrupt Enable is on or
off.  MVOs and shifts are not interruptible.  Most other instructions
are interruptible.

If the CPU is executing a long sequence of non-interruptible
instructions, the BUSRQ will be ignored.  This causes the STIC's
twiddling of the System RAM to fail.  The result is that the STIC
will replay the same row of cards a second time, and the rest of
the display will shift down by one row of cards.  To the person
playing the game, it'll look like the screen jumped.

To be safe, limit code to about 40-50 cycles of non-interruptible
instructions.  Insert an interruptible instruction to break up the
sequence.  (FWIW, early versions of 4-Tris had this type of bug.)

That's all fine and good, you're thinking, but why do I need this
in the ISR dispatcher?  This issue only occurs during active display,
and it's not active here, right?

Yes, you're right.  While it's possible that the interrupt dispatcher
could get delayed until sometime during what would be active display
(say, by a code protected w/ DIS and EIS), that's irrelevant.  The
display will be blanked, because (a) the interrupt wasn't taken
during the vertical blank interval, and (b) $0020 wasn't hit during
the appropriate interval either, so the display will be blank.

I don't think the APh guys who wrote the code (David Rolfe and
company) fully understood all the foibles of the STIC at the time.
Who can blame 'em?  Or maybe they expected a possible need to manually
branch to $1004 sometime?  Who knows.

Regards,

--Joe
