Copyright (c) Hyperion Entertainment and contributors.
Exec Interrupts
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. |
Contents
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 the "Exec Tasks" chapter), 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.
Figure 26-1: 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 chapter. 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.
Don't Trash A6 |
---|
Software interrupts execute in an environment almost identical to that of hardware interrupts, and the same restrictions on allowable system function calls (as described earlier) apply to both. Note however that, unlike other interrupts, software interrupts must preserve A6. |
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 - Execute me to compile me with SAS C 5.10 LC -b1 -d0 -cfistq -v -y -j73 timersoftint.c Blink FROM LIB:c.o,timersoftint.o TO timersoftint LIBRARY LIB:LC.lib,LIB:Amiga.lib quit ; */ /* 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 <clib/exec_protos.h> #include <clib/dos_protos.h> #include <clib/alib_protos.h> #include <stdio.h> #ifdef LATTICE int CXBRK(void) { return(0); } /* Disable Lattice CTRL/C handling */ void chkabort(void) { return; } /* really */ #endif #define MICRO_DELAY 1000 #define OFF 0 #define ON 1 #define STOPPED 2 struct TSIData { ULONG tsi_Counter; ULONG tsi_Flag; struct MsgPort *tsi_Port; }; struct TSIData *tsidata; void tsoftcode(void); /* Prototype for our software interrupt code */ void main(void) { struct MsgPort *port; struct Interrupt *softint; struct timerequest *tr; ULONG endcount; /* Allocate message port, data & interrupt structures. Don't use CreatePort() */ /* or CreateMsgPort() since they allocate a signal (don't need that) for a */ /* PA_SIGNAL type port. We need PA_SOFTINT. */ if (tsidata = AllocMem(sizeof(struct TSIData), MEMF_PUBLIC|MEMF_CLEAR)) { if(port = AllocMem(sizeof(struct MsgPort), MEMF_PUBLIC|MEMF_CLEAR)) { NewList(&(port->mp_MsgList)); /* Initialize message list */ if (softint = AllocMem(sizeof(struct Interrupt), MEMF_PUBLIC|MEMF_CLEAR)) { /* 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 not 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(). If our */ /* interrupt code was in assembler, you could initialize is_Data here to */ /* contain a pointer to shared data structures. An assembler software */ /* interrupt routine would receive the is_Data in A1. */ softint->is_Code = tsoftcode; /* The software interrupt routine */ softint->is_Data = tsidata; softint->is_Node.ln_Pri = 0; port->mp_Node.ln_Type = NT_MSGPORT; /* Set up the PA_SOFTINT message port */ port->mp_Flags = PA_SOFTINT; /* (no need to make this port public). */ port->mp_SigTask = (struct Task *)softint; /* pointer to interrupt structure */ /* Allocate timerequest */ if (tr = (struct timerequest *) CreateExtIO(port, sizeof(struct timerequest))) { /* Open timer.device. NULL is success. */ if (!(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. */ printf("starting softint. CTRL-C to break...\n"); tr->tr_node.io_Command = TR_ADDREQUEST; /* Initial iorequest to start */ tr->tr_time.tv_micro = MICRO_DELAY; /* software interrupt. */ BeginIO((struct IORequest *)tr); Wait(SIGBREAKF_CTRL_C); endcount = tsidata->tsi_Counter; printf("timer softint counted %ld milliseconds.\n", endcount); printf("Stopping timer...\n"); tsidata->tsi_Flag = OFF; while (tsidata->tsi_Flag != STOPPED) Delay(10); CloseDevice((struct IORequest *)tr); } else printf("couldn't open timer.device\n"); DeleteExtIO(tr); } else printf("couldn't create timerequest\n"); FreeMem(softint, sizeof(struct Interrupt)); } FreeMem(port, sizeof(struct MsgPort)); } FreeMem(tsidata, sizeof(struct TSIData)); } } void tsoftcode(void) { struct timerequest *tr; /* Remove the message from the port. */ tr = (struct timerequest *)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->tr_node.io_Command = TR_ADDREQUEST; tr->tr_time.tv_micro = MICRO_DELAY; 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. Although assembler DISABLE and ENABLE macros are provided, we strongly suggest that you use the system functions rather than the macros for upwards compatibility and smaller code size.
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.
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. |