26omment>(edited 19-sep-91) (Old SSM Chapter 6) (Interrupt Service Routines\ISR_CHAP)

This chapter describes the ways a program (as opposed to a device handler) can transfer data between memory and a peripheral device. First it covers noninterrupt programmed I/O; next it introduces the concept of using interrupts to handle device I/O by comparing the advantages and disadvantages of in-line interrupt service routines and device handlers. After these general points have been discussed, the chapter continues with a description of the structure of an interrupt service routine, and shows in detail how to organize and write one. A skeleton example of a foreground program that contains an interrupt service routine ends this discussion of applications. The discussion is followed by a final section dealing with the considerations involved in using interrupt service routines in a single-mapped, extended memory environment. (Noninterrupt Programmed I/O)

One way to move data between memory and a peripheral device is to use noninterrupt programmed I/O. According to this method, your program operates with the device interrupts disabled and uses flags to coordinate the data transfer. Your program checks the ready bit in the status register for a particular device, moves the data when appropriate, and then either waits in a tight loop for another ready signal or does other processing and polls the device occasionally. Programmed I/O is device-specific and does not make use of operating system features designed for I/O processes. In addition, it ties up system resources until the I/O transfer is complete.

However, programmed I/O is sometimes the best method to use. For example, the Resident Monitor uses programmed I/O to print its (?MON--F--System halt) error message. It first performs a RESET to stop all active I/O. Then it waits in a tight loop for the console terminal to print the error message, one character at a time. Clearly in such a situation, where the monitor itself may be corrupted, no other job or data transfer could be running, and the console terminal is the only desirable output device. Also, the monitor .PRINT routine may have been corrupted and should not be used. Given these requirements, programmed I/O is the best method to use for printing this error message.

In an application program, noninterrupt programmed I/O can provide the quickest response to an external event with interrupts disabled.

The following lines of code from RMON demonstrate noninterrupt programmed I/O:

; ; Note that R1 points to the message text, terminated by a NUL ; TTPS is a word in memory containing the address of ; the terminal printer status register; ; its ready flag is the high-order bit of the low byte. ; TTPB is a word in memory containing the address of ; the terminal printer buffer. ; Moving a character to the printer buffer resets ; the busy flag in the status register. ; 5$: TSTB @TTPS ;Test for tt busy BPL 5$ ;If yes, test again MOVB (R1)+,@TTPB ;If no, print a character BNE 5$ ;Branch back if more to print

The device handler for the single-density diskette, DX, provides another example of programmed I/O. Reading data from the diskette one sector at a time, the handler first requests a read of one sector. The diskette completes the read operation, places the data in an internal silo, and issues an interrupt. The handler then disables diskette interrupts and uses programmed I/O to move data from the silo into memory. When it is ready to read another sector, the handler enables interrupts again.

The following lines of code are from a DX handler: ; ; Note that R4 points to the diskette status register; ; R5 points to the silo; ; R2 points to the data buffer in memory. ; TRBYT: TSTB @R4 ;Wait for transfer ready BPL TRBYT ;Branch if tr not up EFBUF: MOVB @R5,(R2)+ ;Transfer a character DEC @SP ;Check for count done BGT TRBYT ;Transfer more

Refer to the (PDP11_BOOK) for your computer for more information on noninterrupt programmed I/O. (Interrupt-Driven I/O)

Although programmed I/O is useful in a few situations, generally the best way to handle device I/O is through interrupt processing. According to this method, a program starts an I/O transfer but continues processing. When the transfer completes, the device issues an interrupt. An interrupt service routine then determines whether the transfer is incomplete, complete, or has encountered an error. It takes the appropriate action (restarting the transfer, returning to the program, or possibly retrying the transfer in case of error). The advantages of using interrupt-driven I/O are that it enables two or more processes to run concurrently and it does not monopolize system resources. (How an Interrupt Works)

An interrupt is a forced transfer of program execution that occurs because of some external event, such as the completion of an I/O transfer. The state of the processor prior to the interrupt is saved on the stack so that processing can continue smoothly after the return from the interrupt. The processor saves the Processor Status word, or PS, which reflects the current machine state, and the Program Counter, or PC, which indicates the return address.

Next, the processor loads new contents for the PC and PS from two preassigned locations in low memory, called an (interrupt vector). These words contain the address of the interrupt service routine and the new PS, which indicates the new processor priority. When the interrupt service routine completes, it executes an RTI instruction, which restores the old PS and PC from the stack, and execution resumes at the interrupted point in the original program. (Device and Processor Priorities)

Interrupt processing is closely related to device and processor priorities. (ISRRPS_FIG) illustrates the RT--11 priority structure. Each device on the system has a priority assigned to it and devices that must be serviced as soon as possible after they interrupt have the highest priority. Disks typically have priority 5; terminals and other character-oriented devices usually have priority 4. You can control the ordering of devices with the same priority. For these devices, the one closest to the CPU on the bus is serviced before other devices when interrupts occur simultaneously.

(RT--11 Priority Structure\ISRRPS_FIG) (POSTSCRIPT\ML007420B\9.5)

The central processor operates at any one of eight levels of priority, from 0 to 7. (Some older processors are an exception; they operate at either 0 or 7.) When the CPU is operating at priority 7, no device can interrupt it with a request for service. When the CPU is operating at a lower priority, only a device with a higher priority can cause an interrupt. You can adjust the processor's priority from within an interrupt service routine by modifying the Processor Status word. In an RT--11 system, software tools are provided to do this for you, so you never directly modify the PS yourself. The tools include the .MTPS and .MFPS programmed requests, and the .INTEN and .FORK macros.

The interrupt system allows the processor to continually compare its own priority with that of any interrupting devices and to acknowledge the device with the highest level above the processor's. This system can be nested -- that is, the servicing of one interrupt can be left in order to service an interrupt with a higher priority. Service continues for the lower priority device when the higher priority device is finished.

See the (PDP11_BOOK) for your computer for more information on priorities and interrupts. (Processor Status (PS) Word)

The Processor Status (PS) word occupies the highest address on the I/O page. (Again, some older processors are an exception; their PS is not addressable on the I/O page. The monitor refers to the PS by using the MTPS and MFPS instructions.) It contains information on the current status of the machine. This information includes the current processor priority, current and previous operational modes, the condition codes describing the results of the last instruction, and an indicator to cause the execution of an instruction to be trapped (used for program debugging).

(ISRPSW_FIG) illustrates the bits in the PS. Bits 5 through 7 determine the current processor priority. (In some older systems, only bit 7 determines the priority; priority is either 0 or 7.) By changing bits, you alter the CPU's priority. You can change the priority to 7, for example, to prevent any more interrupts from occurring. When you are servicing a particular interrupt, you can change the processor priority to the priority of that device so that only devices with a higher priority will interrupt that service routine. (Specifically, the device you are servicing cannot interrupt.) In general, you need not access the PS yourself; use the macros provided in RT--11, such as .INTEN and .FORK, to change the processor priority.

(Processor Status (PS) Word\ISRPSW_FIG) (POSTSCRIPT\MLO-007421B\22) (In-line Interrupt Service Routines Versus Device Handlers)

Because both noninterrupt programmed I/O and interrupt-driven I/O are valid processes in an RT--11 system, when you need to interface a new device to your system -- one that is not already supported by RT--11 -- your first decision must be whether to use in-line interrupt service or to write a device handler for it. Whatever your decision, both interrupt service routines and device handlers can include noninterrupt programmed I/O sections as well as interrupt-driven code. The normal RT--11 interface between the monitor and a peripheral device is a device handler, which exists as a memory image file on a mass storage device, and resides in memory when it is needed to perform device I/O (see (scm_chap)). A device handler usually includes an interrupt service routine within it.

If you choose to use an interrupt service routine, you must place the routine within your program so that your program directly changes the status and buffer registers for a specific device, and it can service the interrupts within its own code. This means, of course, that the interrupt service code must always be resident in memory.

On the other hand, if you choose to use a device handler, the interrupt service code is contained within the handler, not in your program. You issue .READ and .WRITE programmed requests from your main program, and the monitor and the handler together initiate the data transfer, service the interrupts, and notify your program when the transaction is done. In a single-job system, or for a background job in a multi-job system, the handler must be resident only when your program actually needs it to perform I/O. (That is, the handler must be resident whenever a file or channel is open.) For foreground jobs and system jobs in a multi-job system, you must load the handler (by using the monitor LOAD command) before you execute your program, so that the handler is always resident.

How you decide which method is more suitable for your new device depends largely on how you want the device to appear to system and application programs. In general, you might use in-line interrupt service for sensor or control devices, such as analog-to-digital converters. You should service devices that appear to be block-replaceable, file-structured mass storage devices, such as disks and diskettes, through device handlers. You can service most communications hardware by either method; the decision rests on other criteria.

The two major advantages of in-line interrupt service routines are their speed and the amount of control information they provide. Because there is no monitor overhead involved in a data transfer, an in-line routine can often handle interrupts faster than a device handler can. If the speed of servicing interrupts is crucial to your application, you may choose to write an in-line interrupt service routine even if the device is a disk.

An in-line routine has access to all the device control and status registers for a device, as well as its data buffer registers. (Of course, a device handler has access to all the same registers, but the program using the handler does not.) It can pass a lot of information to the program. This provides a great deal of flexibility in the way the program calls the interrupt service routine, and in the amount of information the routine returns to it.

The three major advantages of using device handlers are that they provide device independence for your programs, they can share processor time with other processes, and they are simple to use. Device handlers have a standard protocol for interfacing to the RT--11 monitor. There is also a standard protocol for the interface between the monitor and a program, so that any program that conforms to the monitor standards can use the handler. This includes application programs, system utility programs, and RT--11 language processors such as MACRO--11, FORTRAN, BASIC--PLUS, and PDP--11 C. Thus, the device handler makes a new device available to a large number of programs without any special modification. (In addition, a device handler for a random-access device makes the RT--11 file system available on the device at no extra cost.) In contrast, an in-line interrupt service routine makes the new device available to just one application program.

Device handlers are easy to use. Because they are the standard RT--11 means of handling device I/O, the procedure for writing them and using them is clear and straightforward. This procedure is simplified further by the fact that RT--11 provides macros to write a handler; there are also keyboard monitor commands that install handlers into the monitor device tables and load them into memory. In addition, a device handler permits you to take advantage of the monitor programmed requests for performing data transfers. Finally, a device handler is the only way you can interface a device to a virtual job in a mapped system.

(ISRIISR_FIG) highlights some differences between in-line interrupt service routines and device handlers.

(In-line Interrupt Service Routines and Device Handlers\ISRIISR_FIG) (POSTSCRIPT\ML007422B\44)

If you decide that your new device requires an in-line interrupt service routine, read the rest of this chapter to learn how to plan and write one. If you decide that a device handler is more suitable, read the rest of this chapter and then go on to (dev_book) to learn how to plan, write, and debug a handler. (How to Plan an Interrupt Service Routine)

The most important part of writing an in-line interrupt service routine is taking the time to plan carefully. Follow these guidelines: (UNNUMBERED) Get to know your device Study the structure of an interrupt service routine Study the skeleton interrupt service routine Think about the requirements of your program Prepare a flowchart of your program Write the code Test and debug the program (Get to Know Your Device)

Getting to know your new device is crucial to writing an interrupt service routine that works correctly. If your device is a Digital peripheral, consult the hardware reference manual for that device. If your device is not from Digital, study the documentation for it carefully. Regardless of the format of the documentation (whether it is a manual, a brochure, or a set of engineering prints), it should contain the vital information you need to support it on a PDP--11 system. Be sure you obtain this information.

In any case, you must understand how the device operates: what it needs from you, and how it handles data transfers. Use the following checklist to make sure you have enough device-specific information to write the service routine. Do not attempt to write any code until you have considered each question.

Some of the following questions do not apply to all types of devices. Some are for mass storage devices, some are more appropriate for sensor devices or communications devices. Consider each question carefully, though, to see if it applies to your device. (UNNUMBERED) What is the interrupt vector (or vectors) for the device?

Decide what the interrupt vector should be. Consider both conflicts with existing RT--11-supported devices and also conflicts with devices supported by other PDP--11 operating systems, if you use those systems. Once you decide on the vector, make sure the device is installed properly and that the hardware is jumpered for that address. RT--11 requires all vectors to be below location 500 and some low-memory locations are not available for use as vectors. (scm_chap) lists the current PDP--11 vector assignments. What are the control and status registers?

Learn where these registers are located and what the bits in each mean. What is the priority for the device? Is the device DMA (Direct Memory Access) or programmed transfer (word- or character-oriented)? What are the data buffer registers?

Learn where these registers are located and what the bits in each mean. What are the op codes for typical operations?

Learn how to initiate the various operations by manipulating the bits in the device registers. When does the device interrupt?

Some devices interrupt for each character; others are word-oriented, block-oriented, or packet-oriented. Some devices interrupt twice for certain operations, such as seek or drive reset. Find out if your device does this, and plan now to take this information into account later. What is the basic unit for data transfers?

This relates to the previous question, of course, but you must determine whether to send I/O requests to the device as byte, word, or block counts. If, for example, your program deals in terms of words and the device is character-oriented, you may have to convert the word count to a byte count in the service routine. Does the device want a positive or negative byte count?

Some devices require a negative byte or word count. If your device is one of those, you may need to negate the count in the service routine. What is the device structure, or geometry?

If the device is a disk, find out how the cylinders, tracks, and sectors are structured. Determine their size. Find out if the device requires interleaving, and, if so, learn how to optimize for speed. ((Interleaving )describes the process for writing data to a spinning device that requires program intervention between sectors. The disk is constantly moving; data is written into one sector, the program intervenes as the adjacent sector spins past, then more data is written into the next available sector.) What is the buffering arrangement?

Some devices transfer data to your program one character at a time. Others buffer data internally in a silo, or send it in packets. Decide how to buffer the data in your program. Make sure the buffer space you allocate is large enough. How do you calculate the address of the data on the device?

This relates to the device's structure. Study the device now and determine how to find the data you want on it. Note that RT--11 block numbers must be converted to device-specific addresses. Note also that some processors have no multiply or divide instructions. What (housekeeping) operations does the device require?

Some devices require a drive reset before a retry. Others require that the device be selected or that a disk pack be acknowledged before you can perform any operations on it. You might have to do a drive reset after a seek incomplete or a drive error, for example. How will you handle errors and exception conditions?

First you must decide which errors are hard and will abort the transfer, and which errors are soft and will retry the transfer. Some typical soft errors include checksum errors, data late errors, and timing errors. Decide how many times you will retry the transfer for soft errors, and how you will handle a hard error condition. What are the abort considerations?

Consider whether the device is relatively fast or slow. Keep in mind that you do not want to issue a controller reset if only one unit of a two-unit controller is affected by a program's abort, because this can interfere with the operation of the second unit. Similar considerations may apply to dual-ported devices. (Study the Structure of an Interrupt Service Routine)

(ISRSTRUCT_SEC) describes the structure of an interrupt service routine. Read this section carefully. (Study the Skeleton Interrupt Service Routine)

(ISRSKEL_SEC) contains a skeleton outline of a foreground job with an in-line interrupt service routine. Study this outline to be sure you understand the flow of execution. (Think About the Requirements of Your Program)

Remember that the interrupt service routine is part of your program and decide where to place it in the program. Review the material in (scm_chap) on swapping the USR. If you plan to execute your program in a mapped environment, read (ISRXM_SEC) for mapping considerations. (Prepare a Flowchart of Your Program)

Many experienced programmers prepare flowcharts after all their programs are written, or they omit them entirely. However, flowcharting a system with the complexities of interrupt service can help you find loose ends and point out errors in your logic. Flowcharts are not much help, unfortunately, in pointing out potential race conditions. (A race condition is a situation in which two or more processes attempt to modify the same data structure at the same time; as a result, the data structure is corrupted and the integrity of the processes is compromised. It may be caused by a device interrupting while its interrupt service routine is running, due to improper processor priority.) When you design your program, examine every step carefully; keep in mind what would happen if an interrupt occurred at each instruction. This kind of planning can help you avoid race conditions later.

Spend enough time to design a clean and straightforward way of handling error conditions; if your program can handle error conditions well, you will probably find that the rest of your program design works well too. (Write the Code)

If you have followed the recommended steps so far, writing the code for the interrupt service routine itself should be relatively simple. You can borrow as much code as possible from other interrupt service routines you have studied. Start with a general outline, then add details to reflect the specifics of your particular device. When you are satisfied with the code, have checked it thoroughly for logic errors, and it assembles properly, you are ready to test and debug it. (Test and Debug the Program)

The only way to test a program with in-line interrupt service is to try executing it. If the program is operating correctly, it should be able to read or write data accurately, should not lose any data, and should handle error conditions properly. Try executing the program in a test situation with data you have prepared. If you find errors, use the DBG--11 symbolic debugger to step through your program. If for some reason you cannot use DBG--11, link the program with ODT (not VDT) and try running it step by step. Make coding corrections, reassemble the program, and retry it as necessary. (Structure of an Interrupt Service Routine\ISRSTRUCT_SEC)

The following sections outline the general structure of an in-line interrupt service routine. Read them carefully and determine which items apply to your own situation. (Protecting Vectors: .PROTECT\ISRPROT_SEC)

In systems where more than one job can be running, you should use the .PROTECT programmed request to protect an interrupt vector before you move a value to it. This process makes sure that the vector does not already belong to the monitor or to another job. It gives ownership of the vector to your job, and protects it from interference from another job by setting bits in the monitor bitmap. ((rmon_chap) describes the low-memory bitmap in detail.) Your job should abort immediately if the .PROTECT request fails; your job must not access a vector that is already in use. See Sections (ISRPROT_setup_SEC\VALUE) and (ISRSKEL_SEC\VALUE) for examples of how to use .PROTECT.

See the (SML_BOOK) for the format of the .PROTECT programmed request.

Even though the .PROTECT request has no meaning in a single-job system, it is advisable to use it in your program. The request takes no action, returning immediately to your program, yet is useful later if your program needs to run in a multi-job environment. (Setting up the Interrupt Vector\isrprot_setup_sec)

Your program must take care of moving the address of your interrupt service routine to the first word of the interrupt vector. RT--11 requires all interrupts to raise the processor priority to 7, so your program must fill in the second word of the interrupt vector with 7 as the new priority. The following lines of code show a typical way for a program to set up the two-word interrupt vector. Note that a program should not set up a vector until the vector is protected. For this example, assume the device name is XX, and the interrupt vector is at 220 and 222. .MCALL .DEVICE .GVAL .INTEN .PROTECT .MCALL .RSUM .SPND .SYNCH .LIBRARY "SRC:SYSTEM.MLB" .MCALL .FIXDF .FIXDF ; ** MAIN PROGRAM ** xxVEC = vvv ; The device vector PR7 = 340 ; Priority 7 DEVPRI = 5 ; Device priority = 5 (0-7, not 000-340) xxCSR = nnnnnn ; The device control register IENABL = 100 ; Interrupt enable bit START: .PROTECT #AREA,#xxVEC ; Protect the vector BCS ERROR ; Handle .PROTECT error .GVAL #AREA,#$JOBNU ; Get our job number in R0 BCS ERROR ; Handle .GVAL error MOV R0,SYNBLK+2 ; Store our job number in synch block MOV #ISREP,@#xxVEC ; Set up first word of vector MOV #PR7,@#xxVEC+2 ; Set up second word of vector .DEVICE #AREA,#DEVLST ; Disable device on exit or abort ; Insert lines of code here to initialize input buffers in the ; service routine and to initialize other pointers and flags SPND: BIS #IENABL,@#xxCSR ; Enable interrupts .SPND ; Wait until there is some data ; Insert lines of code here to store the data and reset some flags BR SPND ; Wait for more data DEVLST: .WORD xxCSR ; List for .DEVICE .WORD 0 ; Clear xxCSR on .EXIT or abort .WORD 0 ; Terminate .DEVICE list AREA: .BLKW 3 ; Programmed request argument block ERROR: . ; Routines to handle errors . . ; ** INTERRUPT SERVICE ROUTINE ** ISREP: . ; The interrupt entry point . ; (priority is 7) . .INTEN DEVPRI ; NOTE: not ".INTEN #DEVPRI" ; Lower to device priority ; and enter system state ; with R4 and R5 available ; ; If there is more data to collect: ; BR RET ; ; If there is no more data to collect: ; .SYNCH #SYNBLK ; Go back to main program to process data BR SYNERR ; .SYNCH returns here on error .RSUM ; Wake up main program RET: RETURN ; Wait for another interrupt SYNBLK: .WORD 0,0,0,0,0,-1,0 ; Job number is filled in by code at START SYNERR: ; Process .SYNCH error (Stopping Cleanly: .DEVICE)

The .DEVICE programmed request turns off a device (by clearing its interrupt enable bit) if its associated program is aborted or when the program exits. (See the (SML_BOOK) for the format of the .DEVICE programmed request. See (ISRSKEL_SEC) of this manual for an example using .DEVICE.)

This request is not required in a single-job environment. However, even though the request has no meaning in a single-job system, it is advisable to use it in your program. The request takes no action, returning immediately to your program, yet is useful later if your program needs to run in an multi-job environment. (Lowering Processor Priority: .INTEN)

When an interrupt occurs, control passes to your interrupt service routine entry point the address you supplied as the first word of the interrupt vector. At this point, the processor priority is 7, and all other interrupts are prohibited. If you need to do anything with all interrupts disabled, this is where the code belongs. It should be as short and efficient as possible and should not destroy the contents of any registers. If this code needs to use registers, it must save them and restore them before issuing the .INTEN call. If the code executed at priority 7 is too long, system interrupt latency (a measure of how quickly the system can respond to an interrupt) will suffer. A good guideline is to spend no more than 50 microseconds at priority 7.

You should lower the processor priority to that of the device as soon as possible. This means that only devices with a higher priority than this one will be able to interrupt its service routine. To lower the priority, use the .INTEN programmed request. The stack pointer and general registers R0 through R5 must contain the same values when your interrupt service routine issues the .INTEN request as they did at the interrupt entry point. If your interrupt service routine is not written in Position-Independent Code (PIC), use the following format: .INTEN prio

The .INTEN call generates the following code: JSR R5,@54 .WORD ^C(<)PRIO*40>&340

If your interrupt service routine is written in PIC, use the .INTEN call with a second argument, (PIC). .INTEN prio,PIC

The second format generates Position-Independent Code: MOV @#54,-(SP) JSR R5,@(SP)+ .WORD ^C(<)PRIO*40>&340

Both formats cause a JSR to the monitor's INTEN routine, which lowers the processor priority and switches to system state. The monitor then calls the interrupt service routine back as a co-routine. R4 and R5 are available for use on return from the call. You must not destroy the contents of any other registers. If you need R0 through R3, save them on the stack or in memory and restore them before you exit. If you need to preserve values across the .INTEN request, you must save them in memory before the call and restore them after it. Likewise, if the contents of the PS are important, such as the values of the condition bits, you should save them before issuing the .INTEN call.

In unmapped systems, a .INTEN causes a switch to the system stack, so you should avoid using the stack excessively once you are in your interrupt service routine. In mapped systems, the stack is switched to the kernel stack upon entering the ISR.

Save and restore registers and the PS, as necessary, by using memory locations instead of the stack. Saving values in memory locations may prevent your interrupt routine from being reentrant. If you intend to use the routine for multiple devices, be careful about reentrancy when you design it.

(See the (SML_BOOK) for more information on .INTEN. See (ISRSKEL_SEC) of this chapter for an example using .INTEN. See (ISRIFSSUM_SEC) for a summary of the interrupt service routine macro calls.) (Issuing Programmed Requests: .SYNCH)

The .SYNCH call is useful mainly in the multi-job environments. Its purpose is to make sure that the correct job is running when an interrupt service routine executes a programmed request. Even though the .SYNCH call has no meaning in a single-job system, it is advisable to use it in your program. The request takes no action, returning immediately to your program, yet is useful later if your program needs to run in a multi-job environment.

For the format and a complete expansion of this macro, see the listing of the system macro library in the (SML_BOOK).

If you need to issue one or more RT--11 programmed requests from the interrupt service routine, you must first issue the .SYNCH call. Remember that the .INTEN call switched execution to system state, and programmed requests can only be made in user state. The .SYNCH call itself handles the switch back to user state. Note that you should never issue programmed requests requiring the USR from within an interrupt service routine, even after using .SYNCH. You can also issue .SYNCH after .FORK, which is covered in (ISRFORK_SEC). When you issue the .SYNCH call, R0 through R3 and the stack pointer must contain the same values as they did when the .INTEN request returned to you.

(SYNCHBLOCK_TAB) illustrates the format of the synch block, which acts like a completion queue element. The information in the seven-word synch block is placed at the head of the appropriate job's completion queue. Therefore, the code following the .SYNCH request executes as a completion routine, in user state, at priority 0. Because of this, your program must either disable interrupts before the .SYNCH call, or it must be prepared for the device to interrupt again before the .SYNCH code executes. The synch block is available for reuse when QS.CMP (offset 14(8)) is 0. You can test the synch block easily by issuing another .SYNCH. If control passes to the error return (the word following the .SYNCH call), the block is still in use. (Synch Block (.QSYDF)\SYNCHBLOCK_TAB) (\MULTIPAGE) (4\8\8\8) (#) (Offset\Name\Agent\Contents) (#0\QS.LNK\---\Pointer to next queue element or 0 if none.) (#2\QS.JOB\User\Job number.) (#4\----\-----\Reserved.) (#6\----\-----\Reserved.) (10\QS.ID\User\Argument to pass in R0.) (12\QS.SYN\Monitor\--1.) (14\QS.CMP\User\Initialize as 0; after you issue a .SYNCH, the monitor maintains the contents of this word.)

In general, a long time can elapse between the .SYNCH call and the return. First, the monitor switches to user state, and a scheduling pass is required to determine whether or not a context switch is also necessary. Then a background completion routine may have to wait for a compute-bound foreground job to become blocked. So, it may take a considerable amount of time before the code following the .SYNCH actually executes.

In the code following the .SYNCH call, R0 and R1 are free for use, as they are in any completion routine. However, you must preserve R2 through R5 if your .SYNCH routine uses them. This poses a problem for R4 and R5, which are not preserved across the call. If their contents are important, save them in memory before the .SYNCH call. You can use QS.ID in the synch block to pass a value into R0 for the synch routine.

The .SYNCH call has an unusual error return. The first word after .SYNCH is the return address on error; the second word after .SYNCH is the return on success. Routines following .SYNCH calls (and, in fact, completion routines in general) are serialized.

See (ISRSKEL_SEC) for an example using .SYNCH. See (ISRIFSSUM_SEC) for a summary of the interrupt service routine macro calls. (Running at Fork Level: .FORK\ISRFORK_SEC)

The .FORK programmed request gives you another way to lower the processor priority. (See the (SML_BOOK) for the format of the .FORK programmed request. For the complete expansion of this macro, see the listing of the system macro library in that manual.)

When you issue a .FORK call, the fork block is added to a fork queue, which is a first-in, first-out list. Fork routines (all the code following a .FORK call) execute in system state at priority 0, after all interrupts have been serviced, but before the monitor switches to user state. Context switching is inhibited as well during the time fork routines are executing. (See (ISRRPS_FIG) for a review of RT--11 priority levels.)

R4 and R5 are preserved across the .FORK call. In addition, R0 through R3 are free for use after the call. Like .SYNCH, the .FORK call assumes you have not changed R0 through R3 or the stack since the .INTEN call returned to you. See (ISRIFSSUM_SEC) for a summary of the interrupt service routine macro calls. Note that you cannot issue .FORK without a prior .INTEN call.

You must provide a four-word block of memory for the fork queue element, the last three words of which will contain the return PC, R5, and R4. The first word is a link word, which must be 0 when you issue the .FORK request. Because a .FORK routine should not be reentrant, make sure that the device cannot interrupt between the time you issue the .FORK call and the time the .FORK routine (the code following the call) begins to execute.

You cannot reuse a fork block until the fork routine has been entered. It is safe to assume that the fork block is free when the call that used it returns. See (FORKBLOCK_TAB) for an illustration of the fork block.

(Fork Block\FORKBLOCK_TAB) (4\8\8\8) (Offset\Name\Agent\Contents) (0\F.BLNK\Monitor\Link word.) (2\F.BADR\Monitor\Address of FORK routine.) (4\F.BR5\Monitor\R5 save area.) (6\F.BR4\Monitor\R4 save area.)

Generally, .FORK is used in device handlers. To use it in an interrupt service routine, you must first set up a pointer called $FKPTR. The recommended way to do this in a main program is as follows: MOV @#$SYPTR,R4 ADD $FORK(R4),R4 MOV R4,$FKPTR . . . $FKPTR: .WORD 0 XXFBLK: .WORD 0,0,0,0

Then, in the interrupt service routine, you can use the normal form of the .FORK macro: .FORK XXFBLK

The .FORK macro expands as follows: JSR R5,@$FKPTR .WORD XXFBLK-.

Note that in your interrupt service routine, no registers are free for use before the .INTEN call. After the .INTEN, you can safely use R4 and R5. See (ISRIFSSUM_SEC) for a summary of the interrupt service routine macro calls.

The .FORK request has several applications in a real-time environment because it permits lengthy but noncritical interrupt processing to be postponed until all other interrupts are dismissed. Further, the .FORK request (unlike .SYNCH) does not cause a context switch, which is a lengthy process.

The .FORK request returns at priority 0, but only when all other interrupts have been dismissed and before control is returned to the interrupted user program. (Note that you dismiss an interrupt when you leave interrupt level, by any one of several means.) (Summary of .INTEN, .FORK, and .SYNCH Action\ISRIFSSUM_SEC)

(ISRSUMMARY_TAB) summarizes the effects of the .INTEN, .FORK, and .SYNCH macro calls. (ISRREG_FIG) describes the status of the registers for each call.

(Summary of Interrupt Service Routine Macro Calls\ISRSUMMARY_TAB) (5\8\8\8\18) (1\In mapped systems, all are kernel stack; in unmapped systems, are as shown.) (MacroCall\NewPriority\NewStack(1) \Registers Availableto Use After Call\Your Data PreservedAcross Call In:) (.INTEN\Device's\System\R4, R5\None) (.FORK\0\System\R0--R5\R4, R5) (.SYNCH\0\User\R0, R1\R0)
(Summary of Registers in Interrupt Service Routine Macro Calls\ISRREG_FIG) (POSTSCRIPT\ML007423B\30.5) ((POSTSCRIPT\S559C1ISRREG_FIG.EPS\24)) (Exiting from Interrupt Service)

The .INTEN request causes the monitor to call your interrupt service routine as a co-routine. At the end of your routine, when it is time to exit, use a RETURN instruction. This returns control to the monitor, which restores R4 and R5 and then executes an RTI instruction.

You also exit from .FORK and .SYNCH routines with a RETURN instruction. Be sure that the stack is the same as it was upon entry, and that any registers that must be preserved have their original contents. (Skeleton Outline of an Interrupt Service Routine\ISRSKEL_SEC)

(SKELETONISR_FIG) shows a foreground main program that contains an in-line interrupt service routine. The foreground program performs some initialization tasks and then suspends itself. When data is available from a peripheral device, the interrupt service routine collects it. When all the data is gathered, the interrupt service routine resumes the main program, which can then process the new information before suspending itself again. The main program's processing could involve some manipulation of the new data or it could be writing the data to a file shared by a background data analysis job.

For this example, (xx )represents the device name.

(Skeleton Interrupt Service Routine\SKELETONISR_FIG) (\MULTIPAGE) .MCALL .DEVICE .GVAL .INTEN .PROTECT .MCALL .RSUM .SPND .SYNCH .LIBRARY "SRC:SYSTEM.MLB" .MCALL .FIXDF .FIXDF ; ** MAIN PROGRAM ** xxVEC = vvv ; The device vector PR7 = 340 ; Priority 7 DEVPRI = 5 ; Device priority = 5 (0-7, not 000-340) xxCSR = nnnnnn ; The device control register IENABL = 100 ; Interrupt enable bit START: .PROTECT #AREA,#xxVEC ; Protect the vector BCS ERROR ; Handle .PROTECT error .GVAL #AREA,#$JOBNU ; Get our job number in R0 BCS ERROR ; Handle .GVAL error MOV R0,SYNBLK+2 ; Store our job number in synch block MOV #ISREP,@#xxVEC ; Set up first word of vector MOV #PR7,@#xxVEC+2 ; Set up second word of vector .DEVICE #AREA,#DEVLST ; Disable device on exit or abort ; Insert lines of code here to initialize input buffers in the ; service routine and to initialize other pointers and flags SPND: BIS #IENABL,@#xxCSR ; Enable interrupts .SPND ; Wait until there is some data ; Insert lines of code here to store the data and reset some flags BR SPND ; Wait for more data DEVLST: .WORD xxCSR ; List for .DEVICE .WORD 0 ; Clear xxCSR on .EXIT or abort .WORD 0 ; Terminate .DEVICE list AREA: .BLKW 3 ; Programmed request argument block ERROR: . ; Routines to handle errors . . ; ** INTERRUPT SERVICE ROUTINE ** ISREP: . ; The interrupt entry point . ; (priority is 7) . .INTEN DEVPRI ; NOTE: not ".INTEN #DEVPRI" ; Lower to device priority ; and enter system state ; with R4 and R5 available ; ; If there is more data to collect: ; BR RET ; ; If there is no more data to collect: ; .SYNCH #SYNBLK ; Go back to main program to process data BR SYNERR ; .SYNCH returns here on error .RSUM ; Wake up main program RET: RETURN ; Wait for another interrupt SYNBLK: .WORD 0,0,0,0,0,-1,0 ; Job number is filled in by code at START SYNERR: ; Process .SYNCH error (Interrupt Service Routines in Mapped Systems\ISRXM_SEC)

If you are not planning to execute your program in a mapped environment, you need not read this section.

Of the two kinds of jobs in a mapped environment, virtual jobs and privileged jobs, virtual jobs cannot contain in-line interrupt service routines (see (xm_chap)). Virtual jobs cannot directly access the real vectors but instead use virtual vectors.

If a job containing an in-line interrupt service routine must run in a mapped environment, it must run as a privileged job. Privileged mapping makes the low 28K words of memory and the I/O page available to the program and permits the program to map portions of the user virtual address space into extended physical memory if the program requires it.

In order to understand the restrictions that mapping imposes on interrupt service routines, you must understand that when an interrupt occurs in a mapped system, its service routine executes with kernel, not user, mapping. This means that whether or not the program has mapped some of its virtual address space into extended memory, the interrupt service routine executes with the default kernel mapping to the low 28K words of memory plus the I/O page. It makes sense, therefore, that the first restriction demands that the mapping for your interrupt service routine plus any data it uses must be identical to kernel mapping at any time that an interrupt could occur.

(ISRKPM_FIG) shows the default kernel mapping scheme, which provides access to the low 28K words of memory plus the I/O page. This is also the mapping scheme for a privileged job when it first begins execution. And, this is the mapping scheme that takes effect whenever an interrupt is serviced. (The shaded areas in the figure represent memory that the user job cannot access.) In (ISRKPM_FIG), the interrupt vector at 200 and 202 contains the entry point, called ISREP:, of the interrupt service routine, and the value 340, which represents the new PS. When an interrupt occurs, the system uses kernel mapping to locate the interrupt service routine. In this example, it should start at address 120000. Since privileged mapping and kernel mapping are identical in this diagram, the interrupt service routine is located in physical memory exactly where the kernel mapping points, so it can execute correctly.

(Kernel and Privileged Mapping\ISRKPM_FIG) (POSTSCRIPT\ML007442B\26)

(ISRIME_FIG) shows a privileged job that changes the user virtual address mapping. (The shaded areas in the figure represent memory that the user job cannot access.) You can see from the example that the interrupt service routine cannot execute correctly when an interrupt occurs because the interrupt service routine is not located in physical memory where it should be. The memory area pointed to by the kernel mapping contains random data or instructions.

(Interrupt Service Routine Mapping Error\ISRIME_FIG) (postscript\MLO-007425B\26)

The second restriction for interrupt service routines relates to the way the monitor uses Page Address Register (PAR) 1 with kernel mapping. PAR1 controls the mapping for virtual addresses 20000 through 37776. When XM is first bootstrapped with kernel mapping, the virtual addresses map directly to the same physical addresses. However, the monitor itself uses PAR1 to map to EMT area blocks and to user data buffers. So, whenever the system is running, the kernel virtual addresses in the PAR1 range can be mapped just about anywhere in physical memory and you have no way of controlling it. You must be sure that your interrupt service routine and any data it needs are not located in the virtual address range mapped by PAR1. (ISRP1R_FIG) illustrates this restriction. Valid locations for interrupt service routines, assuming that privileged mapping is identical to kernel mapping at the time of the interrupt, are marked on the diagram as (OK).

If your interrupt service routine needs a window into memory, it can borrow PAR1 the same way the monitor does. It must save the contents, set the value it needs, and restore the original contents before exiting. It can do this at .INTEN or fork level, but not at synch level. If your system uses the MQ handler to communicate among system jobs and you have defined the conditional assembly parameter MQH$P2=1 during system generation, all the restrictions for PAR1 also apply to PAR2 -- the range of addresses from 40000 through 57777.

(PAR1 Restriction for Interrupt Service Routines\ISRP1R_FIG) (POSTSCRIPT\ML007426B\26)

One final piece of information is important if you use .SYNCH in your interrupt service routine. The lines of code following .SYNCH execute almost like a completion routine. Completion routines in mapped environments execute with the user stack and with user mapping. But, since the code following .SYNCH is still part of an interrupt service routine, it executes in user context, but with (kernel )mapping. So, the code following a .SYNCH call in mapped environments must observe the same restriction as the main body of the service routine: its mapping must be identical to kernel mapping at any time that an interrupt could occur, or any time the completion routine could be executing. Of course, it must observe the PAR1 and PAR2 restrictions as well. (End of Chapter)