Copyright (c) Hyperion Entertainment and contributors.

Exec Tasks

From AmigaOS Documentation Wiki
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 Tasks

One of the most powerful features of the Amiga operating system is its ability to run and manage multiple independent program tasks, providing each task with processor time based on their priority and activity. These tasks include system device drivers, background utilities, and user interface environments, as well as normal application programs. This multitasking capability is provided by the Exec library's management of task creation, termination, scheduling, event signals, traps, exceptions, and mutual exclusion.

This section deals with Exec on a lower level than most applications programmers need and assumes you are already familiar with the Exec basics discussed in the Introduction to Exec.

Task Structure

Exec maintains task context and state information in a task-control data structure. Like most Exec structures, Task structures are dynamically linked onto various task queues through the use of an embedded Exec list Node structure (see Exec Lists and Queues). Any task can find its own task structure by calling FindTask(NULL). The C-language form of this structure is defined in the <exec/tasks.h> include file:

struct Task  {
    struct Node tc_Node;
    UBYTE       tc_Flags;
    UBYTE       tc_State;
    BYTE        tc_IDNestCnt;   /* intr disabled nesting */
    BYTE        tc_TDNestCnt;   /* task disabled nesting */
    ULONG       tc_SigAlloc;    /* sigs allocated */
    ULONG       tc_SigWait;     /* sigs we are waiting for */
    ULONG       tc_SigRecvd;    /* sigs we have received */
    ULONG       tc_SigExcept;   /* sigs we will take excepts for */
    UWORD       tc_TrapAlloc;   /* traps allocated */
    UWORD       tc_TrapAble;    /* traps enabled */
    APTR        tc_ExceptData;  /* points to except data */
    APTR        tc_ExceptCode;  /* points to except code */
    APTR        tc_TrapData;    /* points to trap code */
    APTR        tc_TrapCode;    /* points to trap data */
    APTR        tc_SPReg;       /* stack pointer */
    APTR        tc_SPLower;     /* stack lower bound */
    APTR        tc_SPUpper;     /* stack upper bound + 2*/
    VOID      (*tc_Switch)();   /* task losing CPU */
    VOID      (*tc_Launch)();   /* task getting CPU */
    struct List tc_MemEntry;    /* allocated memory */
    APTR        tc_UserData;    /* per task data */
};

Most of these fields are not relevant for simple tasks; they are used by Exec for state and administrative purposes. A few fields, however, are provided for the advanced programs that support higher level environments (as in the case of processes) or require precise control (as in devices). The following sections explain these fields in more detail.

Task Creation

To create a new task manually you must allocate a task structure, initialize its various fields, and then link it into Exec with a call to AddTask(). This is not the recommended way to create a task. This detailed information is only being provided for reference.

The task structure may be allocated by calling the AllocVecTags() function with the MEMF_SHARED attribute and AVT_ClearWithValue tag.

The Task fields that require initialization depend on how you intend to use the task. For the simplest of tasks, only a few fields must be initialized:

tc_Node
The task list node structure. This includes the task's priority, its type, and its name (refer to Exec Lists and Queues).
tc_SPLower
The lower memory bound of the task's stack.
tc_SPUpper
The upper memory bound of the task's stack.
tc_SPReg
The initial stack pointer. Because task stacks grow downward in memory, this field is usually set to the same value as tc_SPUpper.

Zeroing all other unused fields will cause Exec to supply the appropriate system default values. Allocating the structure with the AVT_ClearWithValue tag is an easy way to be sure that this happens.

Once the structure has been initialized, it must be linked to Exec. This is done with a call to AddTask() in which the following parameters are specified:

AddTask(struct Task *task, APTR initialPC, APTR finalPC )

The task argument is a pointer to your initialized Task structure. Set initialPC to the entry point of your task code. This is the address of the first instruction the new task will execute.

Set finalPC to the address of the finalization code for your task. This is a code section that will receive control if the initialPC routine ever performs a return. This exists to prevent your task from being launched into random memory upon an accidental return. The finalPC routine should usually perform various program-related clean-up duties and should then remove the task. If a zero is supplied for this parameter, Exec will use its default finalization code (which simply calls the RemTask() function).

Task Creation With CreateTask()

The recommended of creating a task is with CreateTask() or CreateTaskTags() functions.

CreateTaskTags(CONST_STRPTR name, int32 priority, CONST_APTR initialPC, uint32 stacksize, ...);

A task created with CreateTaskTags() may be removed with the DeleteTask() function, or it may simply return when it is finished. CreateTaskTags() adds a MemList to the tc_MemEntry of the task it creates, describing all memory it has allocated for the task, including the task stack and the Task structure itself. This memory will be deallocated by Exec when the task is either explicitly removed (RemTask() or DeleteTask()) or when it exits to Exec's default task removal code (RemTask()).

Depending on the priority of a new task and the priorities of other tasks in the system, the newly added task may begin execution immediately.

Sharing Library Pointers
Although in most cases it is possible for a parent task to pass a library base to a child task so the child can use that library, for some libraries, this is not possible. For this reason, the only library base sharable between tasks is Exec's library base.

Here is an example of simple task creation. In this example there is no coordination or communication between the main process and the simple task it has created. A more complex example might use named ports and messages to coordinate the activities and shutdown of two tasks. Because our task is very simple and never calls any system functions which could cause it to be signaled or awakened, we can safely remove the task at any time.

Keep This In Mind.
Because the simple task's code is a function in our program, we must stop the subtask before exiting.
// simpletask.c - Uses the function CreateTaskTags() to create a simple subtask.
 
#include <exec/types.h>
#include <exec/memory.h>
#include <exec/tasks.h>
#include <libraries/dos.h>
 
#include <proto/exec.h>
#include <proto/dos.h>
 
#include <stdlib.h>
 
#define STACK_SIZE 16000
 
uint32 sharedvar = 0;
 
/* our function prototypes */
void simpletask(void);
void cleanexit(CONST_STRPTR, int32);
 
int main()
{
    sharedvar = 0;
 
    struct Task *task = IExec->CreateTaskTags("SimpleTask", 0, (CONST_APTR)simpletask, STACK_SIZE,
      TAG_END);
 
    if(task == NULL)  cleanexit("Can't create task", RETURN_FAIL);
 
    IDOS->Printf("This program initialized a variable to zero, then started a\n");
    IDOS->Printf("separate task which is incrementing that variable right now,\n");
    IDOS->Printf("while this program waits for you to press RETURN.\n");
    IDOS->Printf("Press RETURN now: ");
    IDOS->FGetC( IDOS->Input() );
 
    IDOS->Printf("The shared variable now equals %ld\n", sharedvar);
 
    /* We can simply remove the task we added because our simpletask does not make */
    /* any system calls which could cause it to be awakened or signalled later.    */
    IExec->Forbid();
    IExec->DeleteTask(task);
    IExec->Permit();
    cleanexit("", RETURN_OK);
}
 
void simpletask(void)
{
    while(sharedvar < 0x8000000) sharedvar++;
    /* Wait forever because main() is going to RemTask() us */
    IExec->Wait(0);
}
 
void cleanexit(CONST_STRPTR s, int32 e)
{
    if(s != NULL) IDOS->Printf("%s\n", s);
    exit(e);
}

Task Stack

Every task requires a stack. All normal code execution occurs on this task stack. Special modes of execution (processor traps and system interrupts for example) execute on a single supervisor mode stack and do not directly affect task stacks.

Task stacks are normally used to store local variables, subroutine return addresses, and saved register values. Additionally, when a task loses the processor, all of its current registers are preserved on this stack (with the exception of the stack pointer itself, which must be saved in the task structure).

The amount of stack used by a task can vary widely. As a general rule of thumb, a minimum stack size of 16000 bytes should be used. If any GUI elements are involved, a minimum stack size of 80000 bytes is recommended.

Because stack-bounds checking is not provided as a service of Exec, it is important to provide enough space for your task stack. Stack overflows are always difficult to debug and may result not only in the erratic failure of your task but also in the mysterious malfunction of other Amiga subsystems. Some compilers may provide a stack-checking option.

You Can't Always Check The Stack.
Such stack-checking options generally cannot be used if part of your code will be running on the system stack (interrupts, exceptions, handlers, servers) or on a different task's stack (libraries, devices, created tasks).

When choosing your stack size, do not cut it too close. Remember that any recursive routines in your code may use varying amounts of stack, and that future versions of system routines may use additional stack variables. By dynamically allocating buffers and arrays, most application programs can be designed to function comfortably within the recommended minimum process stack size of 16000 bytes.

Task Priority

A task's priority indicates its importance relative to other tasks. Higher-priority tasks receive the processor before lower-priority tasks do. Task priority is stored as a signed number ranging from -128 to +127. Higher priorities are represented by more positive values; zero is considered the neutral priority. Normally, system tasks execute somewhere in the range of +20 to -20, and most application tasks execute at priority 0.

It is not wise to needlessly raise a task's priority. Sometimes it may be necessary to carefully select a priority so that the task can properly interact with various system tasks. The SetTaskPri() Exec function is provided for this purpose.

Task Termination

Task termination may occur as the result of a number of situations:

  • A program returning from its initialPC routine and dropping into its finalPC routine or the system default finalizer.
  • A task trap that is too serious for a recovery action. This includes traps like processor bus error, odd address access errors, etc.
  • A trap that is not handled by the task. For example, the task might be terminated if your code happened to encounter a processor TRAP instruction and you did not provide a trap handling routine.
  • An explicit call to RemTask() or DeleteTask().

Task termination involves the deallocation of system resources and the removal of the task structure from Exec. The most important part of task termination is the deallocation of system resources. A task must return all memory that it allocated for its private use, it must terminate any outstanding I/O commands, and it must close access to any system libraries or devices that it has opened.

It is wise to adopt a strategy for task clean-up responsibility. You should decide whether resource allocation and deallocation is the duty of the creator task or the newly created task. Often it is easier and safer for the creator to handle the resource allocation and deallocation on behalf of its offspring. In such cases, before removing the child task, you must make sure it is in a safe state such as Wait(0L) and not still using a resources or waiting for an event or signal that might still occur.

Note
Certain resources, such as signals and created ports, must be allocated and deallocated by the same task that will wait on them. Also note that if your subtask code is part of your loaded program, you must not allow your program to exit before its subtasks have cleaned up their allocations, and have been either deleted or placed in a safe state such as Wait(0).

Task Exclusion

From time to time the advanced system program may find it necessary to access global system data structures. Because these structures are shared by the system and by other tasks that execute asynchronously to your task, a task must prevent other tasks from using these structures while it is reading from or writing to them. Task exclusion methods described below prevent the operating system from switching tasks by forbidding or disabling. A section of code that requires the use of either of these mechanisms to lock out access by others is termed a critical section.

Use of these methods is discouraged!
For arbitrating access to data between your tasks, mutexes are a superior solution (See Exec Mutexes). The below solution is deprecated and will be removed from the AmigaOS soon.

Forbidding Task Switching

Forbidding is used when a task is accessing shared structures that might also be accessed at the same time from another task. It effectively eliminates the possibility of simultaneous access by imposing non-preemptive task scheduling. This has the net effect of disabling multitasking for as long as your task remains in its running state.

While forbidden, your task will continue running until it performs a call to Wait() or exits from the forbidden state. Interrupts will occur normally, but no new tasks will be dispatched, regardless of their priorities.

When a task running in the forbidden state calls the Wait() function, directly or indirectly, it implies a temporary exit from its forbidden state. Since almost all stdio, device I/O, and file I/O functions must Wait() for I/O completion, performing such calls will cause your task to Wait(), temporarily breaking the forbid. While the task is waiting, the system will perform normally. When the task receives one of the signals it is waiting for, it will again reenter the forbidden state.

To become forbidden, a task calls the Forbid() function. To escape, the Permit() function is used. The use of these functions may be nested with the expected affects; you will not exit the forbidden mode until you call the outermost Permit().

As an example, the Exec task list should only be accessed when in a Forbid() state. Accessing the list without forbidding could lead to incorrect results or it could crash the entire system. To access the task list also requires the program to disable interrupts which is discussed in the next section.

Disabling Tasks

Disabling is similar to forbidding, but it also prevents interrupts from occurring during a critical section. Disabling is required when a task accesses structures that are shared by interrupt code. It eliminates the possibility of an interrupt accessing shared structures by preventing interrupts from occurring. Use of disabling is strongly discouraged.

To disable interrupts you can call the Disable() function. To enable interrupts again, use the Enable() function.

Like forbidden sections, disabled sections can be nested. To restore normal interrupt processing, an Enable() call must be made for every Disable(). Also like forbidden sections, any direct or indirect call to the Wait() function will enable interrupts until the task regains the processor.

WARNING: It is important to realize that there is a danger in using disabled sections.
Because the software on the Amiga depends heavily on its interrupts occurring in nearly real time, you cannot disable for more than a very brief instant. Disabling interrupts for more than 250 microseconds can interfere with the normal operation of vital system functions, especially serial I/O.

It is never necessary to both Disable() and Forbid(). Because disabling prevents interrupts, it also prevents preemptive task scheduling. When disable is used within an interrupt, it will have the effect of locking out all higher level interrupts (lower level interrupts are automatically disabled by the CPU). Many Exec lists can only be accessed while disabled. Suppose you want to print the names of all system tasks. You would need to access both the TaskReady and TaskWait lists from within a single disabled section. In addition, you must avoid calling system functions that would break a disable by an indirect call to Wait() (Printf() for example). In this example, the names are gathered into a list while task switching is disabled. Then task switching is enabled and the names are printed.

// tasklist.c - Snapshots and prints the ExecBase task list
#include <exec/types.h>
#include <exec/lists.h>
#include <exec/nodes.h>
#include <exec/memory.h>
#include <exec/execbase.h>
 
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/utility.h>
 
#include <string.h>
 
CONST_STRPTR VersTag = "$VER: tasklist 53.1 (21.6.2012)";
 
extern struct ExecBase *SysBase;
 
/* Use extended structure to hold task information */
struct TaskNode {
    struct Node tn_Node;
    uint32 tn_TaskAddress;
    uint32 tn_SigAlloc;
    uint32 tn_SigWait;
    TEXT tn_Name[32];
};
 
int main()
{
    struct TaskNode *tnode = NULL;
    struct TaskNode *rnode = NULL;
 
    /* Allocate memory for our list */
    struct List *ourtasklist = IExec->AllocSysObjectTags(ASOT_LIST, TAG_END);
 
    if (ourtasklist != NULL)
    {
        /* Make sure tasks won't switch lists or go away */
        IExec->Disable();
 
        /* Snapshot task WAIT list */
        struct List *exectasklist = &(SysBase->TaskWait);
        for (struct Node *execnode = IExec->GetHead(exectasklist);
             execnode != NULL; execnode = IExec->GetSucc(execnode))
        {
            tnode = IExec->AllocVecTags(sizeof(struct TaskNode),
              AVT_ClearWithValue, 0,
              TAG_END);
 
            if (tnode != NULL)
            {
                /* Save task information we want to print */
                IUtility->Strlcpy(tnode->tn_Name, execnode->ln_Name, sizeof(tnode->tn_Name));
                tnode->tn_Node.ln_Pri = execnode->ln_Pri;
                tnode->tn_TaskAddress = (uint32)execnode;
                tnode->tn_SigAlloc    = ((struct Task *)execnode)->tc_SigAlloc;
                tnode->tn_SigWait     = ((struct Task*)execnode)->tc_SigWait;
                IExec->AddTail(ourtasklist, (struct Node *)tnode);
            }
            else break;
        }
 
        /* Snapshot task READY list */
        exectasklist = &(SysBase->TaskReady);
        for (struct Node *execnode = IExec->GetHead(exectasklist);
             execnode != NULL; execnode = IExec->GetSucc(execnode))
        {
            tnode = IExec->AllocVecTags(sizeof(struct TaskNode),
              AVT_ClearWithValue, 0,
              TAG_END);
 
            if (tnode != NULL)
            {
                /* Save task information we want to print */
                IUtility->Strlcpy(tnode->tn_Name, execnode->ln_Name, sizoeof(tnode->tn_Name));
                tnode->tn_Node.ln_Pri = execnode->ln_Pri;
                tnode->tn_TaskAddress = (uint32)execnode;
                tnode->tn_SigAlloc    = ((struct Task *)execnode)->tc_SigAlloc;
                tnode->tn_SigWait     = ((struct Task*)execnode)->tc_SigWait;
                IExec->AddTail(ourtasklist, (struct Node *)tnode);
                if(!rnode)  rnode = tnode;  /* first READY task */
            }
            else
                break;
        }
 
        /* Re-enable interrupts and taskswitching */
        IExec->Enable();
 
        /* Print now (printing above would have defeated a Forbid or Disable) */
        IDOS->Printf("Pri Address     SigAlloc    SigWait    Taskname\n");
 
        struct TaskNode *node = (struct TaskNode *)IExec->GetHead(ourtasklist);
        IDOS->Printf("\nWAITING:\n");
        while (tnode = (struct TaskNode *)IExec->GetSucc((struct Node*)node))
        {
            if(tnode == rnode)
                IDOS->Printf("\nREADY:\n");  /* we set rnode above */
 
            IDOS->Printf("%02d  0x%08lx  0x%08lx  0x%08lx %s\n",
                    node->tn_Node.ln_Pri, node->tn_TaskAddress, node->tn_SigAlloc,
                    node->tn_SigWait, node->tn_Name);
 
            /* Free the memory, no need to remove the node, referenced once only */
            IExec->FreeVec(node);
            node = tnode;
        }
 
        IExec->FreeSysObject(ASOT_LIST, ourtasklist);
 
        /* Say who we are */
        IDOS->Printf("\nTHIS TASK:\n");
        struct Task *task = FindTask(NULL);
        IDOS->Printf("%02d  0x%08lx  0x%08lx  0x%08lx %s\n",
                task->tc_Node.ln_Pri, task, task->tc_SigAlloc,
                task->tc_SigWait, task->tc_Node.ln_Name);
    }
 
    return 0;
}

Task Mutexes and Semaphores

Mutexes and semaphores can be used for the purposes of mutual exclusion. With this method of locking, all tasks agree on a locking convention before accessing shared data structures. Tasks that do not require access are not affected and will run normally, so this type of exclusion is considered preferable to forbidding and disabling. This form of exclusion is explained in more detail in Mutexes and Semaphores.

Task Exceptions

Exec can provide a task with its own task-local "interrupt" called an exception. When some exceptional event occurs, an Exec exception occurs which stops a particular task from executing its normal code and forces it to execute a special, task-specific exception handling routine.

To set up an exception routine for a task requires setting values in the task's control structure (the Task structure). The tc_ExceptCode field should point to the task's exception handling routine. If this field is zero, Exec will ignore all exceptions. The tc_ExceptData field should point to any data the exception routine needs.

Exec exceptions work using signals. When a specific signal or signals occur, Exec will stop a task and execute its exception routine. Use the Exec function SetExcept() to tell Exec which of the task's signals should trigger the exception.

When an exception occurs, Exec stops executing the tasks normal code and jumps immediately into the exception routine, no matter what the task was doing. The exception routine operates in the same context the task's normal code; it operates in the CPU's user mode and uses the task's stack.

Before entering the exception routine, Exec pushes the normal task code's context onto the stack. Exec then puts certain parameters in the processor registers for the exception routine to use. D0 contains a signal mask indicating which signal bit or bits caused the exception. Exec disables these signals when the task enters its exception routine. If more than one signal bit is set (i.e. if two signals occurred simultaneously), it is up to the exception routine to decide in what order to process the two different signals. A1 points to the related exception data (from tc_ExceptData), and A6 contains the Exec library base. You can think of an exception as a subtask outside of your normal task. Because task exception code executes in user mode, however, the task stack must be large enough to supply the extra space consumed during an exception.

While processing a given exception, Exec prevents that exception from occurring recursively. At exit from your exception-processing code, you should make sure D0 contains the signal mask the exception routine received in D0 because Exec looks here to see which signals it should reactivate. When the task executes the RTS instruction at the end of the exception routine, the system restores the previous contents of all of the task registers and resumes the task at the point where it was interrupted by the exception signal.

Exceptions Are Tricky.
Exceptions are difficult to use safely. An exception can interrupt a task that is executing a critical section of code within a system function, or one that has locked a system resource such as the disk or blitter (note that even simple text output uses the blitter.) This possibility makes it dangerous to use most system functions within an exception unless you are sure that your interrupted task was performing only local, non-critical operations.

Task Traps

Task traps are synchronous exceptions to the normal flow of program control. They are always generated as a direct result of an operation performed by your program's code. Whether they are accidental or purposely generated, they will result in your program being forced into a special condition in which it must immediately handle the trap. Address error, privilege violation, zero divide, and trap instructions all result in task traps. They may be generated directly by the 68000 processor (Motorola calls them "exceptions") or simulated by software.

A task that incurs a trap has no choice but to respond immediately. The task must have a module of code to handle the trap. Your task may be aborted if a trap occurs and no means of handling it has been provided. Default trap handling code (tc_TrapCode) is provided by the OS. You may instead choose to do your own processing of traps. The tc_TrapCode field is the address of the handler that you have designed to process the trap. The tc_TrapData field is the address of the data area for use by the trap handler.

The system's default trap handling code generally displays a Software Error Requester or Alert containing an exception number and the program counter or task address. Processor exceptions generally have numbers in the range hex 00 to 2F. The 68000 processor exceptions of particular interest are as follows.

Traps (68000 Exception Vector Numbers)
2 Bus error access of nonexistent memory
3 Address error long/word access of odd address (68000)
4 Illegal instruction illegal opcode (other than Axxx or Fxxx)
5 Zero divide processor division by zero
6 CHK instruction register bounds error trap by CHK
7 TRAPV instruction overflow error trap by TRAPV
8 Privilege violation user execution of supervisor opcode
9 Trace status register TRACE bit trap
10 Line 1010 emulator execution of opcode beginning with $A
11 Line 1111 emulator execution of opcode beginning with $F
32-47 Trap instructions TRAP N instruction where N = 0 to 15

A system alert for a processor exception may set the high bit of the longword exception number to indicate an unrecoverable error (for example $8000 0005 for an unrecoverable processor exception #5). System alerts with more complex numbers are generally Amiga-specific software failures. These are built from the definitions in the <exec/alerts.h> include file.

The actual stack frames generated for these traps are processor-dependent. The 68010, 68020, and 68030 processors will generate a different type of stack frame than the 68000. If you plan on having your program handle its own traps, you should not make assumptions about the format of the supervisor stack frame. Check the flags in the AttnFlags field of the ExecBase structure for the type of processor in use and process the stack frame accordingly.

Trap Handlers

For compatibility with the 68000, Exec performs trap handling in supervisor mode. This means that all task switching is disabled during trap handling.

At entry to the task's trap handler, the system stack contains a processor-dependent trap frame as defined in the 68000/10/20/30 manuals. A longword exception number is added to this frame. That is, when a handler gains control, the top of stack contains the exception number and the trap frame immediately follows.

To return from trap processing, remove the exception number from the stack (note that this is the supervisor stack, not the user stack) and then perform a return from exception (RTE).

Because trap processing takes place in supervisor mode, with task dispatching disabled, it is strongly urged that you keep trap processing as short as possible or switch back to user mode from within your trap handler. If a trap handler already exists when you add your own trap handler, it is smart to propagate any traps that you do not handle down to the previous handler. This can be done by saving the previous address from tc_TrapCode and having your handler pass control to that address if the trap which occurred is not one you wish to handle.

The following example installs a simple trap handler which intercepts processor divide-by-zero traps, and passes on all other traps to the previous default trap code. The example has two code modules which are linked together. The trap handler code is in assembler. The C module installs the handler, demonstrates its effectiveness, then restores the previous tc_TrapCode.

;/* trap_c.c - Execute me to compile me with SAS C 5.10
LC -b0 -cfistq -v -y -j73 trap_c.c
Blink FROM LIB:c.o,trap_c.o,trap_a.o TO trap LIBRARY LIB:LC.lib,LIB:Amiga.lib
quit

trap_c.c - C module of sample integer divide-by-zero trap
*/
#include <exec/types.h>
#include <exec/tasks.h>
#include <clib/exec_protos.h>
#include <stdlib.h>
#include <stdio.h>

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

extern ULONG trapa();           /* assembler trap code in trap_a.asm */

APTR oldTrapCode;
ULONG countdiv0;

void main(int argc, char **argv)
{
    struct Task *thistask;
    ULONG k,j;

    thistask = FindTask(NULL);

    /* Save our task's current trap code pointer */
    oldTrapCode = thistask->tc_TrapCode;

    /* Point task to our assembler trap handler code.  Ours will just count */
    /* divide-by-zero traps, and pass other traps on to the normal TrapCode */
    thistask->tc_TrapCode = (APTR)trapa;

    countdiv0 = 0L;

    for(k=0; k<4; k++)            /* Let's divide by zero a few times */
       {
       printf("dividing %ld by zero... ",k);
       j = k/0L;
       printf("did it\n");
       }
    printf("\nDivide by zero happened %ld times\n",countdiv0);

    thistask->tc_TrapCode = oldTrapCode;     /* Restore old trap code */
}
* trap_a.asm - Example trap handling code (leaves D0 intact).  Entered
* in supervisor mode with the following on the supervisor stack:
*    0(sp).l = trap#
*    4(sp) Processor dependent exception frame

    INCLUDE "exec/types.i"
    INCLUDE "libraries/dos.i"

        XDEF _trapa
        XREF _countdiv0
        XREF _oldTrapCode

        CODE
_trapa:                                 ; our trap handler entry
        CMPI.L  #5,(SP)                 ; is this a divide by zero ?
        BNE.S   notdiv0                 ; no
        ADD.L   #1,_countdiv0           ; yes, increment our div0 count
endtrap:
        ADDQ    #4,SP                   ; remove exception number from SSP
        RTE                             ; return from exception
notdiv0:
        TST.L   _oldTrapCode            ; is there another trap handler ?
        BEQ.S   endtrap                 ; no, so we'll exit
        MOVE.L  _oldTrapCode,-(SP)      ; yes, go on to old TrapCode
        RTS                             ; jumps to old TrapCode

        END

Trap Instructions

The TRAP instructions in the 68000 generate traps 32-47. Because many independent pieces of system code may desire to use these traps, the AllocTrap() and FreeTrap() functions are provided. These work in a fashion similar to that used by AllocSignal() and FreeSignal(), mentioned above.

Allocating a trap is simply a bookkeeping job within a task. It does not affect how the system calls the trap handler; it helps coordinate who owns what traps. Exec does nothing to determine whether or not a task is prepared to handle a particular trap. It simply calls your code. It is up to your program to handle the trap.

To allocate any trap, you can use the following code:

if (-1 == (trap = IExec->AllocTrap(-1)))
    IDOS->Printf("all trap instructions are in use\n");

Or you can select a specific trap using this code:

if (-1 == (trap = IExec->AllocTrap(3)))
    IDOS->Printf("trap #3 is in use\n");

To free a trap, you use the FreeTrap() function passing it the trap number to be freed.

Processor and Cache Control

Exec provides a number of to control the processor mode and, if available, the caches. All these functions work independently of the specific M68000 family processor type. This enables you to write code which correctly controls the state of both the MC68000 and the MC68040. Along with processor mode and cache control, functions are provided to obtain information about the condition code register (CCR) and status register (SR). No functions are provided to control a paged memory management unit (PMMU) or floating point unit (FPU).

Processor and Cache Control Functions

Function Description
GetCC() Get processor condition codes.
SetSR() Get/set processor status register.
SuperState() Set supervisor mode with user stack.
Supervisor() Execute a short supervisor mode function.
UserState() Return to user mode with user stack.
CacheClearE() Flush CPU instruction and/or data caches.
CacheClearU() Flush CPU instruction and data caches.
CacheControl() Global cache control.
CachePostDMA() Perform actions prior to hardware DMA.
CachePreDMA() Perform actions after hardware DMA.

Supervisor Mode

While in supervisor mode, you have complete access to all data and registers, including those used for task scheduling and exceptions, and can execute privileged instructions. In application programs, normally only task trap code is directly executed in supervisor mode, to be compatible with the MC68000. For normal applications, it should never be necessary to switch to supervisor mode itself, only indirectly through Exec function calls. Remember that task switching is disabled while in supervisor mode. If it is absolutely needed to execute code in supervisor mode, keep it as brief as possible.

Supervisor mode can only be entered when a 680x0 exception occurs (an interrupt or trap). The Supervisor() function allows you to trap an exception to a specified assembly function. In this function your have full access to all registers. No registers are saved when your function is invoked. You are responsible for restoring the system to a sane state when you are done. You must return to user mode with an RTE instruction. You must not return to user mode by executing a privileged instruction which clears the supervisor bit in the status register. Refer to a manual on the M68000 family of CPUs for information about supervisor mode and available privileged instructions per processor type.

The MC68000 has two stacks, the user stack (USP) and supervisor stack (SSP). As of the MC68020 there are two supervisor stacks, the interrupt stack pointer (ISP) and the master stack pointer (MSP). The SuperState() function allows you to enter supervisor mode with the USP used as SSP. The function returns the SSP, which will be the MSP, if an MC68020 or greater is used. Returning to user mode is done with the UserState() function. This function takes the SSP as argument, which must be saved when SuperState() is called. Because of possible problems with stack size, Supervisor() is to be preferred over SuperState().

Status Register

The processor status register bits can be set or read with the SetSR() function. This function operates in supervisor mode, thus both the upper and lower byte of the SR can be read or set. Be very sure you know what you are doing when you use this function to set bits in the SR and above all never try to use this function to enter supervisor mode. Refer to the M68000 Programmers Reference Manual by Motorola Inc. for information about the definition of individual SR bits per processor type.

Condition Code Register

On the MC68000 a copy of the processor condition codes can be obtained with the MOVE SR,<ea> instruction. On MC68010 processors and up however, the instruction MOVE CCR,<ea> must be used. Using the specific MC68000 instruction on later processors will cause a 680x0 exception since it is a privileged instruction on those processors. The GetCC() function provides a processor independent way of obtaining a copy of the condition codes. For all processors there are 5 bits which can indicate the result of an integer or a system control instruction:

  • X - extend
  • N - negative
  • Z - zero
  • V - overflow
  • C - carry

The X bit is used for multiprecision calculations. If used, it is copy of the carry bit. The other bits state the result of a processor operation.

Cache Functions

As of the MC68020 all processors have an instruction cache, 256 bytes on the MC68020 and MC68030 and 4 KBytes on a MC68040. The MC68030 and MC68040 have data caches as well, 256 bytes and 4 KBytes respectively. All the processors load instructions ahead of the program counter (PC), albeit it that the MC68000 and MC68010 only prefetch one and two words respectively. This means the CPU loads instructions ahead of the current program counter. For this reason self-modifying code is strongly discouraged. If your code modifies or decrypts itself just ahead of the program counter, the pre-fetched instructions may not match the modified instructions. If self-modifying code must be used, flushing the cache is the safest way to prevent this.

DMA Cache Functions

The CachePreDMA() and CachePostDMA() functions allow you to flush the data cache before and after Direct Memory Access. Typically only DMA device drivers benefit from this. These functions take the processor type, possible MMU and cache mode into account. When no cache is available they end up doing nothing. These functions can be replaced with ones suitable for different cache hardware. Refer to the SDK for implementation specifics.

Since DMA device drivers read and write directly to memory, they are effected by the CopyBack feature of the MC68040 (explained below). Using DMA with CopyBack mode requires a cache flush. If a DMA device needs to read RAM via DMA, it must make sure that the data in the caches has been written to memory first, by calling CachePreDMA(). In case of a write to memory, the DMA device should first clear the caches with CachePreDMA(), write the data and flush the caches again with CachePostDMA().

Function Reference

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

Exec Task Function Description
AddTask() Add a task to the system.
AllocTrap() Allocate a processor trap vector.
Disable() Disable interrupt processing.
Enable() Enable interrupt processing.
FindTask() Find a specific task.
Forbid() Forbid task rescheduling.
FreeTrap() Release a process trap.
Permit() Permit task rescheduling.
SetTaskPri() Set the priority of a task.
RemTask() Remove a task from the system.
CacheClearE() Flush CPU instruction and/or data caches.
CacheClearU() Flush CPU instruction and data caches.
CacheControl() Global cache control (V37).
CachePostDMA() Perform actions prior to hardware DMA.
CachePreDMA() Perform actions after hardware DMA.
GetCC() Get processor condition codes.
SetSR() Get/set processor status register.
SuperState() Set supervisor mode with user stack.
Supervisor() Execute a short supervisor mode function.
UserState() Return to user mode with user stack.
CreateTask() Setup and add a new task.
DeleteTask() Delete a task created with CreateTask().