Copyright (c) Hyperion Entertainment and contributors.

Exec Interrupts

From AmigaOS Documentation Wiki
Revision as of 14:56, 20 September 2018 by Janne Peräaho (talk | contribs) (Added code review request)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Warning.png This page is not yet fully updated to AmigaOS 4.x some of the information contained here may not be applicable in part or totally.
Codereview.png Code samples on this page are not yet updated to AmigaOS 4.x some of them may be obsolete or incompatible with AmigaOS 4.x.

Exec Interrupts

Interrupts

Exec manages the decoding, dispatching, and sharing of all system interrupts. This includes control of hardware interrupts, software interrupts, task-relative interrupts (see the discussion of exceptions in Exec Tasks), and interrupt disabling and enabling. In addition, Exec supports a more extended prioritization of interrupts than that provided in the CPU.

The proper operation of multitasking depends heavily on the consistent management of the interrupt system. Task activities are often driven by inter-system communication that is originated by various interrupts.

Sequence of Events During an Interrupt

Before useful interrupt handling code can be executed, a considerable amount of hardware and software activity must occur. Each interrupt must propagate through several hardware and software interfaces before application code is finally dispatched:

  • A hardware device decides to cause an interrupt and sends a signal to the interrupt control portions of the 4703 (Paula) custom chip.

  • The 4703 interrupt control logic notices this new signal and performs two primary operations. First, it records that the interrupt has been requested by setting a flag bit in the INTREQ register. Second, it examines the INTENA register to determine whether the corresponding interrupt and the interrupt master are enabled. If both are enabled, the 4703 generates an interrupt request by placing the priority level of the request onto the three 68000 interrupt control input lines (IPL0, IPL1, IPL2).

  • These three signals correspond to seven interrupt priority levels in the 68000. If the priority of the new interrupt is greater than the current processor priority, an interrupt sequence is initiated. The priority level of the new interrupt is used to index into the top seven words of the processor address space. The odd byte (a vector number) of the indexed word is fetched and then shifted left by two to create an offset into the processor's auto-vector interrupt table. The vector offsets used are in the range of $064 to $07C. These are labeled as interrupt autovectors in the 68000 manual. The auto-vector table appears in low memory on a 68000 system, but its location for other 68000 family processors is determined by the processor's CPU Vector Base Register (VBR). VBR can be accessed from supervisor mode with the MOVEC instruction.

  • The processor then switches into supervisor mode (if it is not already in that mode), and saves copies of the status register and program counter (PC) onto the top of the system stack (additional information may be saved by processors other than the 68000). The processor priority is then raised to the level of the active interrupt.

  • From the low memory vector address (calculated in step three above), a 32-bit autovector address is fetched and loaded into the program counter. This is an entry point into Exec's interrupt dispatcher.

    Exec must now further decode the interrupt by examining the INTREQ and INTENA 4703 chip registers. Once the active interrupt has been determined, Exec indexes into an ExecBase array to fetch the interrupt's handler entry point and handler data pointer addresses.

  • Exec now turns control over to the interrupt handler by calling it as if it were a subroutine. This handler may deal with the interrupt directly or may propagate control further by invoking interrupt server chain processing.

You can see from the above discussion that the interrupt autovectors should never be altered by the user. If you wish to provide your own system interrupt handler, you must use the Exec SetIntVector() function. You should not change the contents of any autovector location.

Task multiplexing usually occurs as the result of an interrupt. When an interrupt has finished and the processor is about to return to user mode, Exec determines whether task-scheduling attention is required. If a task was signaled during interrupt processing, the task scheduler will be invoked. Because Exec uses preemptive task scheduling, it can be said that the interrupt subsystem is the heart of task multiplexing. If, for some reason, interrupts do not occur, a task might execute forever because it cannot be forced to relinquish the CPU.

Interrupts by Priority

Interrupt Priorities

Interrupts are prioritized in hardware and software. The 68000 CPU priority at which an interrupt executes is determined strictly by hardware. In addition to this, the software imposes a finer level of pseudo-priorities on interrupts with the same CPU priority. These pseudo-priorities determine the order in which simultaneous interrupts of the same CPU priority are processed. Multiple interrupts with the same CPU priority but a different pseudo-priority will not interrupt one another. Interrupts are serviced by either an exclusive handler or by server chains to which many servers may be attached, as shown in the Type field of the next table. The table above summarizes all interrupts by priority.

The 8520s (also called CIAs) are Amiga peripheral interface adapter chips that generate the INT2 and INT6 interrupts. For more information about them, see the Amiga Hardware Reference Manual.

As described in the Motorola 68000 Programmer's Manual, interrupts may nest only in the direction of higher priority. Because of the time-critical nature of many interrupts on the Amiga, the CPU priority level must never be changed by user or system code. When the system is running in user mode (multitasking), the CPU priority level must remain set at zero. When an interrupt occurs, the CPU priority is raised to the level appropriate for that interrupt. Lowering the CPU priority would permit unlimited interrupt recursion on the system stack and would "short-circuit" the interrupt-priority scheme.

Because it is dangerous on the Amiga to hold off interrupts for any period of time, higher-level interrupt code must perform its business and exit promptly. If it is necessary to perform a time-consuming operation as the result of a high-priority interrupt, the operation should be deferred either by posting a software interrupt or by signalling a task. In this way, interrupt response time is kept to a minimum. Software interrupts are described in a later section.

Nonmaskable Interrupt

The 68000 provides a nonmaskable interrupt (NMI) of CPU priority 7. Although this interrupt cannot be generated by the Amiga hardware itself, it can be generated on the expansion bus by external hardware. Because this interrupt does not pass through the 4703 interrupt controller circuitry, it is capable of violating system code critical sections. In particular, it short-circuits the DISABLE mutual-exclusion mechanism. Code that uses NMI must not assume that it can access system data structures.

Interrupts are serviced on the Amiga through the use of interrupt handlers and servers. An interrupt handler is a system routine that exclusively handles all processing related to a particular 4703 interrupt. An interrupt server is one of possibly many system routines that are invoked as the result of a single 4703 interrupt. Interrupt servers provide a means of interrupt sharing. This concept is useful for general-purpose interrupts such as vertical blanking.

At system start, Exec designates certain interrupts as handlers and others as server chains. The PORTS, COPER, VERTB, EXTER, and NMI interrupts are initialized as server chains. Therefore, each of these may execute multiple interrupt routines per each interrupt. All other interrupts are designated as handlers and are always used exclusively.

Interrupt Data Structure

Interrupt handlers and servers are defined by the Exec Interrupt structure. This structure specifies an interrupt routine entry point and data pointer. The C definition of this structure is as follows:

struct Interrupt
{
    struct Node is_Node;
    APTR        is_Data;
    VOID      (*is_Code)();
};

Once this structure has been properly initialized, it can be used for either a handler or a server.

Environment

Interrupts execute in an environment different from that of tasks. All interrupts execute in supervisor mode and utilize the single system stack. This stack is large enough to handle extreme cases of nested interrupts (of higher priorities). Interrupt processing has no effect on task stack usage.

All interrupt processing code, both handlers and servers, is invoked as assembly code subroutines. Normal assembly code register conventions dictate that the D0, D1, A0 and A1 registers be free for scratch use. In the case of an interrupt handler, some of these registers also contain data that may be useful to the handler code. See the section on handlers below.

Because interrupt processing executes outside the context of most system activities, certain data structures will not be self-consistent and must be considered off limits for all practical purposes. This happens because certain system operations are not atomic in nature and might be interrupted only after executing part of an important instruction sequence. For example, memory allocation and deallocation routines do not disable interrupts. This results in the possibility of interrupting a memory-related routine. In such a case, a memory linked list may be inconsistent during and interrupt. Therefore, interrupt routines must not use any memory allocation or deallocation functions.

In addition, interrupts may not call any system function which might allocate memory, wait, manipulate unprotected lists, or modify ExecBase->ThisTask data (for example Forbid(), Permit(), and mathieee libraries). In practice, this means that very few system calls may be used within interrupt code. The following functions may generally be used safely within interrupts:

Alert()
Disable()
Enable()
Signal()
Cause()
GetMsg()
PutMsg()
ReplyMsg()
FindPort()
FindTask()

and if you are manipulating your own List structures while in an interrupt:

AddHead()
AddTail()
RemHead()
RemTail()
FindName()
FindIName()
GetHead()
GetTail()
GetSucc()
GetPred()

In addition, certain devices (notably the timer device) specifically allow limited use of SendIO() and BeginIO() within interrupts.

Interrupt Handlers

As described above, an interrupt handler is a system routine that exclusively handles all processing related to a particular 4703 interrupt. There can only be one handler per 4703 interrupt. Every interrupt handler consists of an Interrupt structure (as defined above) and a single assembly code routine. Optionally, a data structure pointer may also be provided. This is particularly useful for ROM-resident interrupt code.

An interrupt handler is passed control as if it were a subroutine of Exec. Once the handler has finished its business, it must return to Exec by executing an RTS (return from subroutine) instruction rather than an RTE (return from exception) instruction. Interrupt handlers should be kept very short to minimize service-time overhead and thus minimize the possibilities of interrupt overruns. As described above, an interrupt handler has the normal scratch registers at its disposal. In addition, A5 and A6 are free for use. These registers are saved by Exec as part of the interrupt initiation cycle.

For the sake of efficiency, Exec passes certain register parameters to the handler (see the list below). These register values may be utilized to trim a few microseconds off the execution time of a handler. All of the following registers (D0/D1/A0/A1/A5/A6) may be used as scratch registers by an interrupt handler, and need not be restored prior to returning.

Don't Make Assumptions About Registers
Interrupt servers have different register usage rules (see the "Interrupt Servers" section).

Interrupt Handler Register Usage

Here are the register conventions for interrupt handlers.

D0
Contains no valid information.
D1
Contains the 4703 INTENAR and INTREQR registers values AND'ed together. This results in an indication of which interrupts are enabled and active.
A0
Points to the base address of the Amiga custom chips. This information is useful for performing indexed instruction access to the chip registers.
A1
Points to the data area specified by the is_Data field of the Interrupt structure. Because this pointer is always fetched (regardless of whether you use it), it is to your advantage to make some use of it.
A5
Is used as a vector to your interrupt code.
A6
Points to the Exec library base (SysBase). You may use this register to call Exec functions or set it up as a base register to access your own library or device.

Interrupt handlers are established by passing the Exec function SetIntVector(), your initialized Interrupt structure, and the 4703 interrupt bit number of interest. The parameters for this function are as follows:

SetIntVector(ULONG intNumber, struct Interrupt *interrupt)

The first argument is the bit number for which this interrupt server is to respond (example INTB_VERTB). The possible bits for interrupts are defined in <hardware/intbits.h>. The second argument is the address of an interrupt server node as described earlier in this article. Keep in mind that certain interrupts are established as server chains and should not be accessed as handlers.

The following example demonstrates initialization and installation of an assembler interrupt handler. See the Resources for more information on allocating resources, and Serial Device for the more common method of serial communications.

;/* rbf.c - Execute me to compile me with SAS C 5.10
LC -d0 -b1 -cfistq -v -y -j73 rbf.c
Blink FROM LIB:c.o,rbf.o,rbfhandler.o TO rbf LIBRARY LIB:LC.lib,LIB:Amiga.lib
quit

** rbf.c - serial receive buffer full interrupt handler example.
** Must be linked with assembler handler rbfhandler.o
**
** To receive characters, this example requires ASCII serial input
** at your Amiga's current serial hardware baud rate (ie. 9600 after
** reboot, else last baud rate used)
*/

#include <exec/execbase.h>
#include <exec/memory.h>
#include <exec/interrupts.h>
#include <resources/misc.h>
#include <hardware/custom.h>
#include <hardware/intbits.h>
#include <dos/dos.h>

#include <clib/exec_protos.h>
#include <clib/misc_protos.h>

#include <stdio.h>
#include <string.h>

#ifdef LATTICE
int CXBRK(void)  { return(0); }  /* Disable Lattice CTRL/C handling */
void chkabort(void) { return; }  /* really */
#endif

#define BUFFERSIZE 256

extern void RBFHandler();   /* proto for asm interrupt handler */
void main(void);

struct MiscResource *MiscBase;
extern struct ExecBase *SysBase;
extern struct Custom far custom;    /* defined in amiga.lib */

static UBYTE *allocname = "rbf-example";

struct RBFData {
    struct Task *rd_Task;
    ULONG rd_Signal;
    ULONG rd_BufferCount;
    UBYTE rd_CharBuffer[BUFFERSIZE + 2];
    UBYTE rd_FlagBuffer[BUFFERSIZE + 2];
    UBYTE rd_Name[32];
};

void main(void)
{
    struct RBFData *rbfdata;
    UBYTE *currentuser;
    BYTE signr;
    struct Device *serdevice;
    struct Interrupt *rbfint, *priorint;
    BOOL priorenable;
    ULONG signal;

    if (MiscBase = OpenResource("misc.resource"))
    {
        currentuser = AllocMiscResource(MR_SERIALPORT, allocname);        /* Allocate the serial */
        if (currentuser)                                                  /* port registers.     */
        {
            printf("serial hardware allocated by %s. Trying to remove it\n",
                   currentuser);                                         /* Hey! someone got it! */
            Forbid();
            if (serdevice = (struct Device *)FindName(&SysBase->DeviceList, currentuser))
                RemDevice(serdevice);
            Permit();

            currentuser = AllocMiscResource(MR_SERIALPORT, allocname);          /* and try again */
        }
        if (currentuser == NULL)
        {                                                                      /* Get the serial */
            currentuser = AllocMiscResource(MR_SERIALBITS, allocname);         /* control bits.  */
            if (currentuser)
            {
                printf("serial control allocated by %s\n", currentuser);            /* Give up. */
                FreeMiscResource(MR_SERIALPORT);
            }
            else
            {                                                                  /* Got them both. */
                printf("serial hardware allocated\n");
                if ((signr = AllocSignal(-1)) != -1)          /* Allocate a signal bit for the   */
                {                                             /* interrupt handler to signal us. */
                    if (rbfint = AllocMem(sizeof(struct Interrupt), MEMF_PUBLIC|MEMF_CLEAR))
                    {
                        if (rbfdata = AllocMem(sizeof(struct RBFData), MEMF_PUBLIC|MEMF_CLEAR))
                        {
                            rbfdata->rd_Task = FindTask(NULL);        /* Init rfbdata structure. */
                            rbfdata->rd_Signal = 1L << signr;

                            rbfint->is_Node.ln_Type = NT_INTERRUPT;      /* Init interrupt node. */
                            strcpy(rbfdata->rd_Name, allocname);
                            rbfint->is_Node.ln_Name = rbfdata->rd_Name;
                            rbfint->is_Data = (APTR)rbfdata;
                            rbfint->is_Code = RBFHandler;
                                                                        /* Save state of RBF and */
                            priorenable = custom.intenar & INTF_RBF ? TRUE : FALSE; /* interrupt */
                            custom.intena = INTF_RBF;                             /* disable it. */
                            priorint = SetIntVector(INTB_RBF, rbfint);

                            if (priorint) printf("replaced the %s RBF interrupt handler\n",
                                                 priorint->is_Node.ln_Name);
                            printf("enabling RBF interrupt\n");
                            custom.intena = INTF_SETCLR | INTF_RBF;

                            printf("waiting for buffer to fill up. Use CTRL-C to break\n");
                            signal = Wait(1L << signr | SIGBREAKF_CTRL_C);

                            if (signal & SIGBREAKF_CTRL_C) printf(">break<\n");
                            printf("Character buffer contains:\n%s\n", rbfdata->rd_CharBuffer);

                            custom.intena = INTF_RBF;               /* Restore previous handler. */
                            SetIntVector(INTB_RBF, priorint);
                                                                  /* Enable it if it was enabled */
                            if (priorenable) custom.intena = INTF_SETCLR|INTF_RBF;    /* before. */

                            FreeMem(rbfdata, sizeof(struct RBFData));
                        }
                        else  printf("can't allocate memory for rbf data\n");
                        FreeMem(rbfint, sizeof(struct Interrupt));
                    }
                    else printf("can't allocate memory for interrupt structure\n");
                    FreeSignal(signr);
                }
                else printf("can't allocate signal\n");

                FreeMiscResource(MR_SERIALBITS);   /* release serial hardware */
                FreeMiscResource(MR_SERIALPORT);
            }
        }
    } /* There is no 'CloseResource()' function */
}

The assembler interrupt handler code, RBFHandler, reads the complete word of serial input data from the serial hardware and then separates the character and flag bytes into separate buffers. When the buffers are full, the handler signals the main process causing main to print the character buffer contents, remove the handler, and exit.

Note
The data structure containing the signal to use, task address pointer, and buffers is allocated and initialized in main(), and passed to the handler (shown below) via the is_Data pointer of the Interrupt structure.
* rbfhandler.asm. Example interrupt handler for rbf.
*
* Assembled with Howesoft Adapt 680x0 Macro Assembler Rel. 1.0
* hx68 from: rbfhandler.asm to rbfhandler.o INCDIR include:
* blink from lib:c.o rbf.o rbfhandler.o to rbf lib lib:lc.lib lib:amiga.lib
*
    INCLUDE "exec/types.i"
    INCLUDE "hardware/custom.i"
    INCLUDE "hardware/intbits.i"

        XDEF    _RBFHandler

JSRLIB MACRO
       XREF _LVO\1
       JSR  _LVO\1(A6)
       ENDM

BUFLEN    EQU    256

       STRUCTURE RBFDATA,0
        APTR   rd_task
        ULONG  rd_signal
        UWORD  rd_buffercount
        STRUCT rd_charbuffer,BUFLEN+2
        STRUCT rd_flagbuffer,BUFLEN+2
        STRUCT rd_name,32
        LABEL RBFDATA_SIZEOF

* Entered with:
*  D0 == scratch
*  D1 == INTENAT & INTREQR (scratch)
*  A0 == custom chips (scratch)
*  A1 == is_Data which is RBFDATA structure (scratch)
*  A5 == vector to our code (scratch)
*  A6 == pointer to ExecBase (scratch)
*
* Note - This simple handler just receives one buffer full of serial
* input data, signals main, then ignores all subsequent serial data.
*
    section code

_RBFHandler:                            ;entry to our interrupt handler

        MOVE.W  serdatr(A0),D1          ;get the input word (flags and char)

        MOVE.W  rd_buffercount(A1),D0   ;get our buffer index
        CMPI.W  #BUFLEN,D0              ;no more room in our buffer ?
        BEQ.S   ExitHandler             ;yes - just exit (ignore new char)
        LEA.L   rd_charbuffer(A1),A5    ;else get our character buffer address
        MOVE.B  D1,0(A5,D0.W)           ;store character in our character buffer
        LEA.L   rd_flagbuffer(A1),A5    ;get our flag buffer address
        LSR.W   #8,d1                   ;shift flags down
        MOVE.B  D1,0(A5,D0.W)           ;store flags in flagbuffer

        ADDQ.W  #1,D0                   ;increment our buffer index
        MOVE.W  D0,rd_buffercount(A1)   ;   and replace it
        CMPI.W  #BUFLEN,D0              ;did our buffer just become full ?
        BNE.S   ExitHandler             ;no - we can exit
        MOVE.L  A0,-(SP)                ;yes - save custom
        MOVE.L  rd_signal(A1),D0        ;get signal allocated in main()
        MOVE.L  rd_task(A1),A1          ;and pointer to main task
        JSRLIB  Signal                  ;tell main we are full
        MOVE.L  (SP)+,A0                ;restore custom
                                        ;Note: system call trashed D0-D1/A0-A1
ExitHandler:
        MOVE.W  #INTF_RBF,intreq(A0)    ;clear the interrupt
        RTS                             ;return to exec

        END

Interrupt Servers

As mentioned above, an interrupt server is one of possibly many system interrupt routines that are invoked as the result of a single 4703 interrupt. Interrupt servers provide an essential mechanism for interrupt sharing.

Interrupt servers must be used for PORTS, COPER, VERTB, EXTER, or NMI interrupts. For these interrupts, all servers are linked together in a chain. Every server in the chain will be called in turn as long as the previous server returned with the processor's Z (zero) flag set. If you determine that an interrupt was specifically for your server, you should return with the processor's Z flag cleared (non-zero condition) so that the remaining servers on the chain will be skipped.

Use The Z Flag
VERTB (vertical blank) servers should always return with the Z (zero) flag set. The processor Z flag is used rather than the normal function convention of returning a result in D0 because it may be tested more quickly by Exec upon the server's return.

The easiest way to set the condition code register is to do an immediate move to the D0 register as follows:

SetZflag_Calls_Next:
        MOVEQ   #0,D0
        RTS

ClrZflag_Ends_Chain:
        MOVEQ   #1,D0
        RTS

The same Exec Interrupt structure used for handlers is also used for servers. Also, like interrupt handlers, servers must terminate their code with an RTS instruction.

Interrupt servers are called in priority order. The priority of a server is specified in its is_Node.ln_Pri field. Higher-priority servers are called earlier than lower-priority servers. Adding and removing interrupt servers from a particular chain is accomplished with the Exec AddIntServer() and RemIntServer() functions. These functions require you to specify both the 4703 interrupt number and a properly initialized Interrupt structure.

Servers have different register values passed than handlers do. A server cannot count on the D0, D1, A0, or A6 registers containing any useful information. However, the highest priority system vertical blank server currently expects to receive a pointer to the custom chips A0. Therefore, if you install a vertical blank server at priority 10 or greater, you must place custom ($DF F000) in A0 before exiting. Other than that, a server is free to use D0-D1 and A0-A1/A5-A6 as scratch.

Interrupt Server Register Usage

D0
Scratch.
D1
Scratch.
A0
Scratch except in certain cases (see note above).
A1
Points to the data area specified by the is_Data field of the Interrupt structure. Because this pointer is always fetched (regardless of whether you use it), it is to your advantage to make some use of it (scratch).
A5
Points to your interrupt code (scratch).
A6
Scratch.

In a server chain, the interrupt is cleared automatically by the system. Having a server clear its interrupt is not recommended and not necessary (clearing could cause the loss of an interrupt on PORTS or EXTER).

Here is an example of a program to install and remove a low-priority vertical blank interrupt server:

;/* vertb.c - Execute me to compile me with SAS C 5.10
LC -d0 -b1 -cfistq -v -y -j73 vertb.c
Blink FROM LIB:c.o,vertb.o,vertbserver.o TO vertb LIBRARY LIB:LC.lib,LIB:Amiga.lib
quit ; */
/* vertb.c - Vertical blank interrupt server example.  Must be linked with vertbserver.o. */

#include <exec/memory.h>
#include <exec/interrupts.h>
#include <dos/dos.h>
#include <hardware/custom.h>
#include <hardware/intbits.h>
#include <clib/exec_protos.h>
#include <stdio.h>

#ifdef LATTICE
int CXBRK(void)  { return(0); }  /* Disable Lattice CTRL/C handling */
void chkabort(void) { return; }  /* really */
#endif

extern void VertBServer();  /* proto for asm interrupt server */

void main(void)
{
    struct Interrupt *vbint;
    ULONG counter = 0;
    ULONG endcount;
                                                       /* Allocate memory for  */
    if (vbint = AllocMem(sizeof(struct Interrupt),     /* interrupt node. */
                         MEMF_PUBLIC|MEMF_CLEAR))
    {
        vbint->is_Node.ln_Type = NT_INTERRUPT;         /* Initialize the node. */
        vbint->is_Node.ln_Pri = -60;
        vbint->is_Node.ln_Name = "VertB-Example";
        vbint->is_Data = (APTR)&counter;
        vbint->is_Code = VertBServer;


        AddIntServer(INTB_VERTB, vbint); /* Kick this interrupt server to life. */

        printf("VBlank server will increment a counter every frame.\n");
        printf("counter started at zero, CTRL-C to remove server\n");

        Wait(SIGBREAKF_CTRL_C);
        endcount = counter;
        printf("%ld vertical blanks occurred\nRemoving server\n", endcount);

        RemIntServer(INTB_VERTB, vbint);
        FreeMem(vbint, sizeof(struct Interrupt));
    }
    else printf("Can't allocate memory for interrupt node\n");
}

This is the assembler VertBServer installed by the C example:

* vertbserver.asm. Example simple interrupt server for vertical blank
*
* Assembled with Howesoft Adapt 680x0 Macro Assembler Rel. 1.0
* hx68 from: vertbserver.asm to vertbserver.o INCDIR include:
* blink from lib:c.o vertb.o vertbserver.o to vertb lib lib:lc.lib lib:amiga.lib
*
    INCLUDE "exec/types.i"
    INCLUDE "hardware/custom.i"
    INCLUDE "hardware/intbits.i"

        XDEF    _VertBServer

* Entered with:       A0 == scratch (execpt for highest pri vertb server)
*  D0 == scratch      A1 == is_Data
*  D1 == scratch      A5 == vector to interrupt code (scratch)
*                     A6 == scratch
*
    section code

_VertBServer:
        ADDI.L  #1,(a1)           ; increments counter is_Data points to
        MOVEQ.L #0,d0             ; set Z flag to continue to process other vb-servers
        RTS                       ;return to exec
        END

Software Interrupts

Exec provides a means of generating software interrupts. Software interrupts execute at a priority higher than that of tasks but lower than that of hardware interrupts, so they are often used to defer hardware interrupt processing to a lower priority. Software interrupts use the same Interrupt data structure as hardware interrupts. As described above, this structure contains pointers to both interrupt code and data, and should be initialized as node type NT_INTERRUPT (not NT_SOFTINT which is an internal Exec flag).

A software interrupt is usually activated with the Cause() function. If this function is called from a task, the task will be interrupted and the software interrupt will occur. If it is called from a hardware interrupt, the software interrupt will not be processed until the system exits from its last hardware interrupt. If a software interrupt occurs from within another software interrupt, it is not processed until the current one is completed. However, individual software interrupts do not nest, and will not be caused if already running as a software interrupt.

Software interrupts are prioritized. Unlike interrupt servers, software interrupts have only five allowable priority levels: -32, -16, 0, +16, and +32. The priority should be put into the ln_Pri field prior to calling Cause().

Software interrupts can also be generated by message arrival at a PA_SOFTINT message port. The applications of this technique are limited since it is not permissible, with most devices, to send IO requests from within interrupt code. However, the timer.device does allow such interactions, so a self-perpetuating PA_SOFTINT timer port can provide an application with quite consistent timing under varying multitasking loads. The following example demonstrates use of a software interrupt and a PA_SOFTINT port. See the Exec Messages and Ports for more information about messages and ports.

/* timersoftint.c - Timer device software interrupt message port example. */
 
#include <exec/memory.h>
#include <exec/interrupts.h>
#include <devices/timer.h>
#include <dos/dos.h>
#include <proto/exec.h>
#include <proto/dos.h>
 
#define MICRO_DELAY 1000
#define OFF     0
#define ON      1
#define STOPPED 2
 
struct TSIData {
    uint32 tsi_Counter;
    uint32 tsi_Flag;
    struct MsgPort *tsi_Port;
};
 
struct TSIData *tsidata;
 
void tsoftcode(void);    /* Prototype for our software interrupt code */
 
int main()
{
    uint32 endcount;
 
    /* Allocate message port, data & interrupt structures. */
    tsidata = IExec->AllocVecTags(sizeof(struct TSIData),
      AVT_Type, MEMF_SHARED,
      AVT_Lock, TRUE,
      TAG_END);
 
    /* Set up the (software)interrupt structure. Note that this task runs at  */
    /* priority 0. Software interrupts may only be priority -32, -16, 0, +16, */
    /* +32. Also note that the correct node type for a software interrupt is   */
    /* NT_INTERRUPT. (NT_SOFTINT is an internal Exec flag). This is the same  */
    /* setup as that for a software interrupt which you Cause(). */
    struct Interrupt *softint = IExec->AllocSysObjectTags(ASOT_INTERRUPT,
      ASOINTR_Code, tsoftcode,
      ASOINTR_Data, tsidata,
      TAG_END);
 
    struct MsgPort *port = IExec->AllocSysObjectTags(ASOT_PORT,
      ASOPORT_AllocSig, FALSE,
      ASOPORT_Action, PA_SOFTINT,
      ASOPORT_Target, softint,
      TAG_END);
 
    if(tsidata != NULL && softint != NULL && port != NULL)
    {
      softint->is_Node.ln_Pri = 0;
 
      /* Allocate timerequest */
      struct TimeRequest *tr = IExec->AllocSysObjectTags(ASOT_IOREQUEST,
        ASOIOR_Size, sizeof(struct TimeRequest),
        ASOIOR_ReplyPort, port,
        TAG_END);
 
      if (tr != NULL)
      {
        /* Open timer.device. NULL is success. */
        if (!(IExec->OpenDevice("timer.device", UNIT_MICROHZ, (struct IORequest *)tr, 0)))
        {
          tsidata->tsi_Flag = ON;        /* Init data structure to share globally. */
          tsidata->tsi_Port = port;
 
          /* Send of the first timerequest to start. IMPORTANT: Do NOT   */
          /* BeginIO() to any device other than audio or timer from      */
          /* within a software or hardware interrupt. The BeginIO() code */
          /* may allocate memory, wait or perform other functions which  */
          /* are illegal or dangerous during interrupts.                 */
          IDOS->Printf("starting softint. CTRL-C to break...\n");
 
          tr->Request.io_Command = TR_ADDREQUEST;    /* Initial iorequest to start */
          tr->Time.Microseconds = MICRO_DELAY;        /* software interrupt.        */
          IExec->BeginIO((struct IORequest *)tr);
 
          IExec->Wait(SIGBREAKF_CTRL_C);
          endcount = tsidata->tsi_Counter;
          IDOS->Printf("timer softint counted %lu milliseconds.\n", endcount);
 
          IDOS->Printf("Stopping timer...\n");
          tsidata->tsi_Flag = OFF;
 
          while (tsidata->tsi_Flag != STOPPED) IDOS->Delay(10);
 
          IExec->CloseDevice((struct IORequest *)tr);
        }
        else IDOS->Printf("couldn't open timer.device\n");
      }
      else IDOS->Printf("couldn't create timerequest\n");
 
      IExec->FreeSysObject(ASOT_IOREQUEST, tr);
    }
 
    IExec->FreeSysObject(ASOT_INTERRUPT, softint);
    IExec->FreeSysObject(ASOT_PORT, port);
    IExec->FreeVec(tsidata);
 
    return 0;
}
 
void tsoftcode(void)
{
    /* Remove the message from the port. */
    struct TimeRequest *tr = (struct TimeRequest *)IExec->GetMsg(tsidata->tsi_Port);
 
    /* Keep on going if main() hasn't set flag to OFF. */
    if ((tr) && (tsidata->tsi_Flag == ON))
    {
        /* increment counter and re-send timerequest--IMPORTANT: This         */
        /* self-perpetuating technique of calling BeginIO() during a software */
        /* interrupt may only be used with the audio and timer device.        */
        tsidata->tsi_Counter++;
        tr->Request.io_Command = TR_ADDREQUEST;
        tr->Time.Microseconds = MICRO_DELAY;
        IExec->BeginIO((struct IORequest *)tr);
    }
    /* Tell main() we're out of here. */
    else tsidata->tsi_Flag = STOPPED;
}

Disabling Interrupts

As mentioned in Exec Tasks, it is sometimes necessary to disable interrupts when examining or modifying certain shared system data structures. However, for proper system operation, interrupts should never be disabled unless absolutely necessary, and never for more than 250 microseconds. Interrupt disabling is controlled with the Disable() and Enable() functions.

In some system code, there are nested disabled sections. Such code requires that interrupts be disabled with the first Disable() and not re-enabled until the last Enable(). The system Enable() and Disable() functions are designed to permit this sort of nesting.

Disable() increments a counter to track how many levels of disable have been issued. Only 126 levels of nesting are permitted. Enable() decrements the counter, and reenables interrupts when the last disable level has been exited.

Quick Interrupts

One of the features of the Zorro III bus is the Quick Interrupt, also known as the vectored interrupt. This feature allows Zorro III hardware to supply a vector number to the system when an interrupt occurs. The system uses this vector number to go directly to an interrupt routine.

Conventional Amiga Interrupts

The Amiga handles normal interrupts from Zorro II cards using an interrupt server chain. There are two interrupts available from the Zorro II bus, the PORTS and EXTER interrupt server chains. If a driver for a Zorro II card needs to use an interrupt, it adds an interrupt routine to the appropriate chain. When the interrupt occurs, Exec calls each routine in the interrupt chain, which are sorted in priority order. Exec continues until it finds the routine that corresponds to the device that triggered the interrupt.

The server chain allows several routines to share a single interrupt. This means that several devices trigger the same interrupt, so each interrupt routine must do some processing to determine if its card triggered the interrupt or if some other source caused the interrupt. For example, an interrupt routine might examine a register on its card to determine that the card triggered the interrupt.

Although this scheme allows unrelated pieces of software to easily share an interrupt, it can make the interrupt overhead rather high. These two interrupt server chains also handle interrupts from the CIA chips, which are used to trigger a variety of events. As a result, these server chains can contain a multitude of interrupt routines.

Consider what happens when a Zorro II card generates a PORTS interrupt. Exec has to perform some set up and then step through the PORTS server chain. Exec calls each interrupt routine in priority order looking for the routine that services this interrupt. If there are 20 interrupt routines of higher priority than the card's interrupt routine in the server chain, Exec has to call 20 other routines before it gets to the correct routine.

Zorro III Quick Interrupts

Quick interrupts avoid the overhead involved in Exec's interrupt server chains. Exec only helps set up the quick interrupt, which it does via the exec.library function ObtainQuickVector(). Once Exec has set up the quick interrupt routine, it does not intervene. Unlike conventional Amiga interrupt routines, which are called as subroutines from Exec's main interrupt code, the Amiga jumps directly to the quick interrupt routine using a private vector. This behavior requires quick interrupt routines to take some special precautions.

There are two important differences between a conventional Amiga interrupt routine and a quick quick interrupt routine. A quick interrupt routine must save and restore all of the registers it changes, including D0, D1, A0, and A1. It must do this because, unlike regular interrupt routines, Exec doesn't do it for you. Also, a quick interrupt routine ends with a RTE (return from exception) instruction.

If your quick interrupt routine is 100% self-contained and does not access any operating system structures or routines, then the work is rather simple. Just save the registers you use, perform your interrupt processing, restore the registers, and end with an RTE. If, however, the routine needs to call the OS or use an OS structure, it must check if the interrupt has been delayed. This is necessary in case the interrupt hit the CPU just after the CPU had told the hardware to hold off interrupts (see the Autodoc for ObtainQuickVector() to find out how to perform this test).

As the Amiga OS is a dynamic operating system, quick interrupts are allocated by the OS. If your hardware/software wants to use a quick interrupt, it must allocate a vector with ObtainQuickVector(). This routine accepts a pointer to the quick interrupt code (not a pointer to an Interrupt structure). If Exec installed the vector, ObtainQuickVector() returns the vector number. When the quick interrupt occurs, the Zorro III card sends this vector number to the CPU, which tells the CPU where the interrupt code is.

ObtainQuickVector() returns 0 if there are no more vectors. Since the number of vectors is limited, any Zorro III device that uses quick interrupts must be able to fall back to the Amiga's conventional interrupt scheme.

Function Reference

The following chart gives a brief description of the Exec functions that control interrupts. See the SDK for details about each call.

Interrupt Function Description
AddIntServer() Add an interrupt server to a system server chain.
Cause() Cause a software interrupt.
Disable() Disable interrupt processing.
Enable() Restart system interrupt processing.
RemIntServer() Remove an interrupt server from a system server chain.
SetIntVector() Set a new handler for a system interrupt vector.