MP/M UTILITY INTERFACE TO PASCAL MT+86 MPMUTIL is a package of interface routines to MP/M-86 from Pascal MT+86 which allow system timing, subtask (subprocess) control, and MP/M queue operations. Its use is described in this memo. 1. Initialization external procedure init_mpm_util(size: integer); In the main program, there must be exactly one call to init_mpm_util before any calls to any of the other procedures and functions in MPMUTIL. The parameter SIZE is the number of bytes to reserve for stack space in the main task. Each subtask (if any) will have its own amount of stack space to be reserved, independent of the main task stack. Init_mpm_util performs all initialization needed by all of the other utilities. 2. Timing external function x_time: longint; external procedure delay(ticks: integer); MP/M provides several system calls to get the time of day to the nearest second. These two procedures provide timing to the nearest clock tick (1/60 second). X_TIME returns the current time in clock ticks since MP/M was last booted in. It takes far less than 1 ms to call XTIME, since no MP/M call is involved. By making a standard MP/M time-of-day call, the longint returned by X_TIME can be related to time of day if needed. Its main use is in computing elapsed time between two events. If this elapsed time is less than 9.1 minutes, the time in ticks will fit in a 16-bit integer without setting the sign bit. (The Pascal MT+ "short" function must be used to convert the longint difference of two calls to X_TIME.) DELAY is called with an integer representing desired delay in clock ticks. If TICKS is less than or equal to zero, there will be no delay. If 32767 < TICKS < 65536, this looks negative, and again there will be no delay. 32767 ticks is about 9.1 minutes. If 1 <= TICKS <= 32767, an MP/M call will be made requesting the delay. MP/M will not return until at least that many ticks have occurred. It may be longer, depending on priorities when the requested time has elapsed. To get delays longer than 9 minutes, make consecutive calls to DELAY. 3. Subtask control type name_type = packed array[1..8] of char; pd_type = record case boolean of true: (z1: packed array [1..5] of byte; prior: byte; flag: word; name: name_type; uda: word; user, disk: byte; z2: packed array [1..12] of byte; cns: byte; z3: packed array [1..3] of byte; list: byte; z4: packed array[1.15] of byte; cns2: byte; z5: byte; name2: name_type); false: (ray: array [0..61] of byte) end; {pd_type} uda_type = array[0..255] of byte; external procedure fix_stack(taskp: ^integer; {kludge} var uda: uda_type; size: integer); external function create_process(var pd: pd_type): boolean; external function set_priority(prioritry: byte): boolean; external procedure dispatch; external procedure terminate; external procedure abort(var pd: pd_type); Each subtask requires: a Process Descriptor table (62 bytes) a User Data Area table (256 bytes) for use by MP/M only an execution priority a name of 8 characters some stack space a starting address The Process Descriptor and User Data Area must both be in the global data area, not local procedure data. The example programs (see especially TEST) show procedure STARTUP as a way to start up a task. The PD shown here is larger than shown in the MPM Programmer's Guide; it has 14 extra bytes for use by the ABORT procedure. The UDA for each task must reside on a paragraph (16-byte) boun dary. The way to achieve this is to declare all UDA's together at the beginning of the global data area of the main program module, as in the examples. FIX_STACK must be called before the task is started by CREATE_PROCESS. FIX_STACK reserves stack space for the task. It is called with the address of the task entry point (which will be a procedure), the User Data Area for the task (each task must have its own), and the amount of stack space to reserve for the task. CREATE_PROCESS must be called immediately after FIX_STACK. It is passed the Process Descriptor table for the task (each task has its own). The PD is first set to all zeroes, and then initial ized as follows: PRIOR is set to the desired initial task priority. FLAG will normally be zero, but see MP/M call 144. NAME is set to the 8-character task name. All 8 bits of each byte are significant. UDA is set to the offset of the UDA divided by 16 (see example). USER, DISK, CNS, LIST are set to the desired default user number, disk drive, console number, and LST device number, if any are needed. (Normally 0.) NAME2 is set to the same name as NAME. Used only by ABORT. CNS2 is set to the same value as CNS. Used only by ABORT. FALSE is returned if the process could not be created as speci fied, which could be due to too many processes already created, or a duplicate process name, for example. TRUE is returned if the process was successfully created. Creating the process causes it to begin execution according to its priority. SET_PRIORITY may be called at any time by any task to alter its own priority. It is called with the desired new priority level. See the discussion of scheduling and priority below. It returns FALSE if the priority could not be set, and TRUE otherwise. DISPATCH is similar to DELAY(1), in that the task calling it gives up the CPU until its next turn to execute, whereupon it continues execution at the instruction following the DISPATCH call. DELAY(1) guarantees that at least one clock tick will occur before the task continues; depending on priorities, DISPATCH may return control immediately. TERMINATE causes the calling task to be removed from all MP/M lists, and execution of that task will stop. That is, there is no return from the TERMINATE call. No task can TERMINATE another (but see ABORT), and all tasks must be terminated somehow. In particular, if a main task starts up several subtasks, and then terminates only itself, the subtasks will continue to exist in memory and execute until explicity terminated. Re-booting MP/M will of course terminate everything. ABORT is similar to TERMINATE, except that a specified task is terminated, which will usually not be the calling task. The PD of the task to be terminated is passed to ABORT. The NAME2 and CNS2 fields must be set to match the NAME and CNS fields, or the process will not be aborted -- although some other process might be! ABORT allows a main task, for example, to terminate its sub- tasks when processing is complete. 4. Scheduling and Priority There are 256 possible priority levels in MP/M. The highest ("best") priority is 0, and the lowest ("worst") is 255. The following is a guideline for establishing priorities which will not conflict with MP/M usage: 002 - 031 Interrupt handlers 200 Task initialization 201 - 254 User tasks Every task is either running, ready to run, or waiting for some event. Only one task can run at any one time, since there is only one CPU. Tasks which are able to run, and are waiting only for the CPU are "ready". "Waiting" tasks might be waiting for a system flag, for a time delay to elapse, or for a resource (con sole, printer, disk) to become available. The MP/M scheduler is called at every clock tick, and at every interrupt. The highest-priority "ready" task is then resumed. If more than one task has the same priority, they are executed on a round-robin basis. Example: Tasks A, B, C are at priority 210, and task X is at priority 250; all are "ready", and task A is currently executing. At the next tick, task A will be placed at the end of the ready list for priority 210, and task B will be resumed. At the next tick, task C will be resumed. Then task A again, and so on. Task X will be locked out until A, B, and C are all waiting for something to happen, such as a delay to expire, or a character to be typed at the console. Note that A, B, and C can interleave their execution if any takes more than 17 ms to execute one cycle. If interleaving is undesireable, the running task can raise its priority upon entry, and decrease it to its nominal level at the end of its cycle. This will guarantee that A will complete before B or C get executed (unless A goes into a waiting state for some reason). 5. Stack Space Pascal MT+86 programs use the stack for passing parameters to procedures and functions (including run-time procedures invisible to the programmer), for temporary storage during complex calcula tions, and for storage of local variables (including formal parameters) of procedures and functions. It is difficult to estimate how much stack space a program will need, as it depends on the (dynamic) depth of procedure nesting at run time as well as on the (static) factors listed above. The linker by default assigns 8K bytes ($200 paragraphs) of stack space to a program, unless the /Z switch is used. Unfortunately, there is no reliable way to tell when stack overflow has occurred. The program may exhibit bizarre behaviour, or the system may crash entirely; the computer has no memory protection. In any event, after an amount of stack space has been decided on for the main program and each task, as shown in the calls to init_mpm_util and create_process, add up all these values to get the minimum amount of stack space needed. Convert the total to hexadecimal, drop the final digit (i.e., divide by 16), and add 1 (for round-up). Use this (or a larger value) as the number on the linker /Z switch. Example 1: Program PROGT reserves 1500 bytes of stack for itself and each of two subtasks, for a total of 4500. This is $1194 (hex). dividing by 16 and adding 1 gives $120. The default linker value of $200 is big enough, so no linker /Z switch is needed. Example 2: A hypothetical program reserves 1K bytes for the main program, and 3K bytes for each of 8 subtasks. This is 25K deci mal, or $6400. This is bigger than the linker default, so the linker command line would look something like this: linkmt main,task1,task2,task3,...,task8,fpreals,paslib/s/z:640 6. MP/M Message Queues type name_type = packed array[1..8] of char; qd_type = record z1: longint; flags: word; name: name_type; msglen: integer; nmsgs: integer; z3, z2: longint; z4: word end; qpb_type = record z: word; qid: word; nmsgs: integer; buffer: word; name: name_type end; external function queue_make(var qd: qd_type): boolean; external function queue_oper(op: byte; var qpb: qpb_type): boolean; MP/M allows any reasonable number of message queues of any reas onable size to be dynamically created and used. The maximum number of queues active at one time, and the maximum combined size of all queues active at one time is set at MP/M system generation time. (This is because the queues exist in MP/M data space, not in program data space.) A message queue is created by specifying its name, the size of a message, and the capacity of the queue in messages. Any task can write messages to or read messages from the queue once it is created and opened. (A queue needs a separate "open" operation performed after it is created.) Messages are handled strictly on a first-in, first-out (FIFO) basis. When writing to a queue, the task can elect to wait for an empty message slot to become avail able, or have the write request return with an error indication. Similarly, when reading a queue, the task can elect to wait for a message to be written into the queue if it is empty, or have the read request return in an error condition. The queue handler knows only about message size; the interpretation of messages is left strictly up to tasks which read and write the messages. See example TESTQ for sample queue usage. QUEUE_MAKE must be called once for each queue to create it. It is called with a Queue Descriptor table. The QD is initialized by setting the "z" fields to zero, FLAGS to zero (except see MP/M manual for discussion of Mutual Exclusion Queues), NAME to the name of the queue (all 8 bits of all 8 bytes are significant), MSGLEN to the size of a message in bytes (all messages in a given queue are the same size), and NMSGS to the queue capacity in messages. TRUE is returned if all is succesful, and FALSE if the queue name is duplicated, or if there is not enough space in the system data area to create the queue. The queue must be opened via a call to QUEUE_OPER before any other operation can be per formed on it. QUEUE_OPER is a general-purpose routine to perform all queue functions other than making a queue. It is called with the queue operation number and a Queue Parameter Block. The queue opera tion numbers are as follows: 135 open queue 136 delete queue 137 read queue 138 conditional read queue 139 write queue 140 conditional write queue TRUE is returned if the requested operation is successful. FALSE is returned if unsuccessful, or if an operation number outside the above range is used. The operations are discussed below. OPEN QUEUE is required once after the queue is created, and before any other operation is performed. A QPB must be initialized by setting Z to zero and NAME to the name of the queue. All 8 bits of all 8 bytes of NAME must match the NAME field in the Queue Descriptor used to create the queue. The QID field will be filled in with a queue identifier needed for further operations. The QPB must be preserved for use in further queue operations, and so should probably be a global rather than local data item. DELETE QUEUE should be done prior to termination of the last task to use the queue. If all tasks terminate and the queue is not deleted, succeeding attempts to create the queue will fail, and MP/M may have to be re-booted to clear out the queue. A copy of the QPB used to open the queue should be passed to QUEUE_OPER. READ QUEUE must be passed a copy of the QPB used to open the queue. NMSGS in the QPB copy must be set to the number of messages to be read from the queue. BUFFER must be set to the address of a data item big enough to hold NMSGS messages (if it is too small, adjacent data will be over-written). In addition, the QPB copy and the buffer must both be in the same data area. That is, both must be global or both must be local data of the same procedure. See READ_MSG in example TESTQ, where both are local to the same procedure. TRUE is returned after NMSGS have been read from the queue. If not enough messages are in the queue at the time of the call, the calling task will be put in a "waiting" state until enough additional messages are written to the queue. FALSE is returned if the queue is not open or if it has been deleted. CONDITIONAL READ QUEUE is the same as READ QUEUE except that FALSE is returned if there are not enough messages already in the queue; that is, it does not wait for the messages. WRITE QUEUE must be passed a copy of the QPB used to open the queue. NMSGS in the QPB copy must be set to the number of messages to write to the queue. BUFFER must be set to the address of a data item containing NMSGS messages. In addition, the QPB copy and the buffer must both be in the same data area. That is, both must be global or both must be local data of the same procedure. See WRITE_MSG in example TESTQ, where both are local to the same procedure. TRUE is returned after NMSGS have been written to the queue. If there is not enough space for NMSGS messages in the queue at the time of the call, the calling task will be put in a "waiting" state until enough messages are read from the queue to make room. FALSE is returned if the queue is not open or if it has been deleted. CONDITIONAL WRITE QUEUE is the same as WRITE QUEUE except that FALSE is returned if there is not enough room in the queue for NMSGS messages; that is, it does not wait for space to become available. 7. Raw Console Input external procedure r_con_raw(var str: string); At this writing, readln from the KBD: device does not work prop erly, and input lines from the CON: device are limited to 80 characters, so this extra function is included in the package. It can be used with care in conjuction with I/O to the CON: (default) device. But see the caution in the MT+86 manual sec tion 3.4.16 (discussion of ASSIGN). R_CON_RAW bypasses the MP/M interpretation of console characters to provide raw console input via MP/M function 3. It also bypasses all of the Pascal MT+86 file I/O. STR is initially cleared (length byte set to 0). Characters are read from the console and appended to STR -- including control characters. When a CR ($0D) is detected, it is not written to the string, but R_CON_RAW returns at that point to the calling program. This procedure was included to provide a simple way of using a console in Block Transmission mode, whereby the user types the 'send page' key, and data is sent to the computer with control characters delimiting fields and lines, and a final CR at the end. Cautions: a. If the terminal sends more than one CR, the data following the first CR may be lost, depending on the buffer size of the XIOS console input routine, system timing, and the console baud rate. b. The maximum string size under Pascal MT+ is 255 bytes. A full console screen is typically 1920 bytes. You must bear this in mind. Most consoles with block transmission allow the sending of only the unprotected fields, which must then be set up to total less than 255 bytes (including field separators). c. You won't want to use this procedure for manual input, since the backspace and other controls characters are merely transmitted without interpretation. Furthermore, characters typed will not be echoed to the screen. d. Characters may be lost when the console is operated at high baud rates. Characters will be lost when used in conjunction with the 8087 math library. This is because interrupts must be disabled while the 8087 is computing under the current version of MP/M. This probably makes R_CON_RAW unusable. 8. Time of Day type tod_rec = record day: integer; hour: byte; min: byte; sec: byte end; external procedure get_tod(var tod: tod_rec); This procedure implements MP/M call 155, except that it converts the hour/min/sec fields to binary instead of BCD. The fields in record TOD are filled in as follows: day = number of days since 1 Jan 1978 hour = hour of the day, 0 to 23 (24-hour clock) min = minute of the hour, 0 to 59 sec = second of the minute, 0 to 59 If you need the values in BCD instead of straight binary, you can reconvert the numbers in the calling program, or use @BDOS86.