ervin@pinbot.enet.dec.com (Joseph James Ervin) (05/16/91)
MPE A Multi-Programming Environment for the HP-48SX by Joe Ervin 1 PURPOSE The purpose of this document is to describe the function and use of the Multi-Programming Environment, written by Joe Ervin. 2 INTRODUCTION The Multi-Programming Environment (MPE) is a set of machine language routines, data structures, and Star macros which implement an environment whereby the software developer can easily program concurrent tasks. The immediate use of MPE is for graphics animation such as in games programming, where multiple objects need to be animated on the screen at the same time (bullets, explosions, pac-men, etc.), however an environment such as this can be extremely useful for periodic keyboard polling and other events that need to occur from time to time. This distribution of MPE includes the STAR source code for MPE version 1.0, as well as a set of process definitions which comprise a simple interactive graphics demo, showing the main features of MPE. The source code and the uuencoded file have been posted separately. If you do not have access to either of these documents, please send mail to "ervin@pinbot.enet.dec.com" and I will mail them to you. 3 USER'S GUIDE The remainder of this document comprises the MPE User's Guide. This guide discusses the basic structure of a process under MPE, and then goes on to describe the various routines and macros and how they are used. These routines and macros allow the user to do such things as process scheduling, process entry-point redirection, and interprocess communication. Finally, the example set of processes which comprise the demo are described at the end of the user's guide. Page 2 3.1 Process Structure The basic idea behind MPE is that the programmer defines one or more "processes" which are activated, or invoked by MPE. MPE keeps a list of all processes which are waiting to run, and runs the processes as they come due. As each process is run, it is deleted from the run-list, thus requiring processes to be rescheduled after each invocation if repeated execution is desired. The manner in which MPE handles processes is as follows. The run-list contains a set of process ID/time pairs, where the "ID" is an 8-bit process ID, and the "time" is the absolute time (in clock ticks) at which that process should be run. The clock ticks referred to here are the same "ticks" as those that are associated with the TICKS command described in the HP48 owners manual. There are 8192 clock ticks per second. The RUN_LIST is a data structure used and maintained by MPE. The casual programmer need not be concerned with its workings. The association between processes and IDs is implied through the use of global labels in the assembler code for the processes. MPE expects that each process will have at its beginning a label of the form: PROCESSx_INIT: ...where the x in PROCESSx_INIT is a decimal number from 1 to 255. A value of 0 may NOT be used as a process ID since the MPE reserves this ID for the scheduler. The portion of MPE that manages the run-list is called the "scheduler", and will be referred to as such for the remainder of this document. When an application written using MPE is first started, the scheduler's run-list is empty. In order to activate the application, MPE automatically schedules process #1 for immediate execution. As a result, the control flow will pass immediately to the code located at PROCESS1_INIT:. NOTE This is the only time that a process will be invoked automatically by the MPE. It is the responsibility of the programmer to ensure that all processes which comprise the application are scheduled by the application itself. Processes are scheduled by using the ADD_PROCESS routine, described below. Initially, MPE defines the entry points of all processes to be the PROCESSx_INIT label of each process. In many applications, however, it will be necessary for a process to execute its initialization code only once; the first time the process is activated. In order to facilitate the distinction between process initialization code and the main process body, MPE includes a macro called PROCESS_START, which allows the process to redefine its entry point. Page 3 The PROCESS_START macro takes two parameters; the ID of the process whose entry point is to be modified, and the label corresponding to the desired entry point. For example, Figure 1 shows process #1 using the PROCESS_START macro to alter its entry point. This example also shows the use of several of the other facilities in MPE. These other facilities will be described in later sections. ;********************************************************************** PROCESS1_INIT: ; The initial entry point for process #1. PROCESS_START 1, PROCESS1_CODE ; Changes the entry point of process #1 to the ; PROCESS1_CODE label for subsequent ; invocations. . (Initialization code for process #1.) . PROCESS1_CODE: ; The main body of process #1 starts here. . (Body of process #1) . SAVE_CONTEXT . ; This causes MPE to save a "snapshot" of this . ; process's register context (B, D, D0, D1, R0-R4) . ; so that the next time this process is started, its . ; registers will contain the same data as at the . ; time SAVE_CONTEXT is called. A and C are not saved. . RESCHEDULE: ; Now we want to schedule this process to execute again ; in 1 second. ADDR CUR_TIME, D0 ; ADDR is a standard HP48 macro found in the HP48.STAR ; macro library by Jan Brittenson. CUR_TIME is a global ; MPE variable which holds a copy of the time ; (in ticks) when this process was activated. MOVE.W @D0, C CLR.W A MOVE.P5 ^x1FFF, A ADD.W A, C ; ; CUR_TIME + ^x1FFF ticks = CUR_TIME + 1 sec. MOVE.P5 ^x1, A ; We are rescheduling process #1. This would ; be ^x2 for process #2, ^x3 for process #3, ; etc.. CALL ADD_PROCESS ; Schedule process #1 for execution ; at 1 sec. from when the current ; invocation started. JUMP TO_SCHEDULER ; Jump back to the scheduler. ;********************************************************************** Figure 1 Page 4 In addition to the PROCESS_START macro, there is the CUR_PROCESS_START macro, which is very similar to PROCESS_START, but with small differences. The CUR_PROCESS_START macro works only on the current process. The CUR_PROCESS_START macro expects that the user has previously loaded the address corresponding to the new entry point for the current process into C.A. Invoking the CUR_PROCESS_START macro will then take that address in C.A and cause the current process to use that as its new entry point. The ADDR macro in the HP48 macro library by Jan Brittenson is useful for loading C.A with the address of the new entry point. The main impetus for the CUR_PROCESS_START macro is to avoid the need of multiple IF_PROC/ENDIF_PROC constructs for code which may be shared by many processes. In games that require many objects on the screen, each of which with its own process, it is conceivable that the number of IF_PROC/ENDIF_PROC constructs required by the PROCESS_START macro would be unwieldy. 3.2 Process Context 3.2.1 The SAVE_CONTEXT Routine One of the main functions of MPE is to ensure that each process has its own processor context. To do this, MPE provides the SAVE_CONTEXT routine which automatically saves the processor registers of the current process in a special data structure maintained by MPE for each process. The registers saved are B, D, D0, D1, and R0-R4. Note that MPE makes no attempt to save Registers A and C between invocations. By calling the SAVE_CONTEXT routine, a process indicates to MPE that the current contents of its registers should be used as the starting contents when the process is next invoked. It is the responsibility of the programmer to call SAVE_CONTEXT at an appropriate point in the process when all the registers have the desired contents. MPE does not provide a mechanism for saving or restoring individual registers. The SAVE_CONTEXT routine alters A and C, but leaves all other processor registers intact. MPE also provides the SAVE_CONTEXT_BY_A routine which allows the user to specify the register context for any arbitrary process. The user provides a process ID in register A.B, and SAVE_CONTEXT_BY_A will write the current register context into the context storage area maintained by MPE for that process. Note that no error checking is done on the ID provided in A.B, so the user should be careful with using this routine. 3.2.2 The RESTORE_CONTEXT Routine. Whenever a process comes due and is about to be executed, MPE uses the RESTORE_CONTEXT routine to restore the process's register context, after which MPE jumps to the process's entry point. The Page 5 RESTORE_CONTEXT routine, together with the SAVE_CONTEXT routine can also be useful to the programmer in general. The RESTORE_CONTEXT routine restores the register context of the process whose ID is given in A.B. Obviously, this routine effects all the processor registers. The RESTORE_CONTEXT routine can be useful to the programmer whenever information needs to be shared between processes. For example, in some applications, it may be desirable to extract information that another process is keeping in one of its processor registers. This may be accomplished by using the RESTORE_CONTEXT routine, specifying the other process ID in A.B. Note that it is probably advisable for a process to save its own context via SAVE_CONTEXT before calling RESTORE_CONTEXT to see the other process's registers. That way, after the extracted data has been used, the process can restore its own context if necessary and continue execution. Note that although the SAVE_CONTEXT routine does not require any arguments, the RESTORE_CONTEXT routine requires the process ID in A.B. 3.2.3 The CURRENT_CONTEXT_ID Variable In order to keep track of where to save register context when the SAVE_CONTEXT routine is called, MPE maintains a variable called CURRENT_CONTEXT_ID. This is a one byte variable which contains a copy of the current process's ID. While this variable is primarily for use by the SAVE_CONTEXT routine, it comes in very handy for certain applications. One useful action that involves the CURRENT_CONTEXT_ID is that of modifying the saved state of another process. This can be done by first saving the current process's context, then restoring the other process's context. At this point, the current process has access to all the register context of the other process. By modifying these contents, changing the CURRENT_CONTEXT_ID variable to the other process's ID, and then executing the SAVE_CONTEXT routine, the other process's context can be overwritten. The programmer must be very careful to ensure that the CURRENT_CONTEXT_ID variable is returned to the current process's ID before control is passed back to the scheduler. Another useful technique involving the CURRENT_CONTEXT_ID variable is that of sharing code among several processes. This is discussed in detail below. To make accessing the CURRENT_CONTEXT_ID variable easier, MPE includes a routine called GET_CURRENT_ID, which when called returns the current process ID into A.B. This routine modifies the contents of A. Page 6 3.3 Scheduling Processes - The ADD_PROCESS Routine As stated above, it is necessary for the user's code to handle the scheduling of all processes, except for the initial execution of process #1 at startup. Processes are scheduled by use of the ADD_PROCESS routine. This routine takes as its arguments a one byte process ID in register A, and a 16-nibble absolute time (ticks) in register C. ADD_PROCESS then schedules the specified process for execution at the specified time. The ADD_PROCESS routine modifies A, C, B, D, D0, D1, and R0, so it is recommended that this routine be executed only during the initialization phase of a process, after the process has saved its register context via the SAVE_CONTEXT routine, or any other time when the contents of these registers is not critical to the process. 3.4 The CUR_TIME Variable In order to reschedule a process at a certain approximate interval it is necessary that the process have knowledge of the system time at the point when the process was invoked. MPE provides this information in the CUR_TIME variable. The CUR_TIME variable holds a 16 nibble value representing the system time when the current process was invoked. To schedule itself on regular intervals, a process should read the time value at CUR_TIME, add in a time delay, and reschedule itself for execution at the resulting future time. Figure 2 shows a typical use of CUR_TIME. In this example, process #3 is rescheduling itself for CUR_TIME plus 1 second. This causes process #3 to execute on 1 second intervals. ;******************************************************************** ; Reschedule process #3 for execution in 1 second. ADDR CUR_TIME, D0 ; Point D0 to the CUR_TIME ; variable. MOVE.W @D0, C ; C.W now contains the time ; when this process was ; invoked by the scheduler. MOVE.P5 ^x1FFF, A ; ^x1FFF = ^d8192 = 1 second. ADD.W A, C MOVE.P2 ^d3 CALL ADD_PROCESS ; Add the process to the RUN_LIST. JUMP TO_SCHEDULER ; Return control to the scheduler. ;******************************************************************** Figure 2 The CUR_TIME variable can be accessed by using the ADDR macro from the HP48 macro library by Jan Brittenson. This method also works well for accessing any other MPE variables. The CUR_TIME variable can also be Page 7 accessed by calling the GET_CUR_TIME routine, which returns the CUR_TIME variable in C.W. This routine modifies register A. 3.5 Returning Control To The Scheduler After a process has completed its useful work for the current invocation, it needs to save its register context via the SAVE_CONTEXT routine, and then reschedule itself if desired. Finally, the last thing a process does is return control to the scheduler by jumping to the MPE label TO_SCHEDULER as follows: JUMP TO_SCHEDULER ; Returns control to the scheduler. The user should be careful to use the JUMP instruction here and NOT the CALL instruction. 3.6 Sharing Process Code In many applications, several processes may be needed which do essentially the same thing. Consider the situation where an application wants several processes which do very similar things, such as "bullets" flying about on the screen as is common in many popular arcade games. Since each of these processes would be running essentially identical code, it would be very useful if each of the "bullet" processes could actually share the same code. This can easily be accomplished by placing multiple PROCESSxINIT: labels at the top of the process. One problem that soon becomes apparent, however, is that this shared code must inevitably perform some actions that are specific to each process. It is therefore necessary that the shared code has a way of determining which process is currently executing. One example of this is that since these processes will need to reschedule themselves, the shared code must have some way of rescheduling the correct process. By accessing the CURRENT_CONTEXT_ID, the shared code can determine the ID of the process it is currently servicing. The CURRENT_CONTEXT_ID variable can be accessed by using the ADDR macro from the HP48 macro library by Jan Brittenson, as is shown below. This method also works well for accessing any other MPE variables. ;******************************************************************** ; Check which process is currently running. ADDR CURRENT_CONTEXT_ID, D0 ; Point D0 to the ; CURRENT_CONTEXT_ID variable. MOVE.B @D0, C ; C.B now contains the ID of the ; current process. Page 8 ;******************************************************************** Figure 3 Because of the usefulness of code-sharing, MPE contains a macro which makes the conditional execution of code based on the current process ID very simple. The IF_PROC and ENDIF_PROC macros allow the programmer to easily cause the execution of a specific section of code conditioned on the current context ID. The format of this structure is as follows: ;******************************************************************** IF_PROC n ; where "n" is the process ID in question. . . (ML code to be executed only if process #n is current.) . . ENDIF_PROC ;******************************************************************** Figure 4 NOTE The user should be aware that the IF_PROC macro modifies registers A and C regardless of which process ID is current. For this reason, A and C will be altered whenever IF_PROC is used. The user should therefore make no attempt to pass values into an IF_PROC/ENDIF_PROC construct using registers A or C. As an example of when to use IF_PROC, consider a section of code which is shared by processes #1, #2 and #3. Let us assume that the process needs to reschedule itself after executing. It is therefore necessary that the code pass the proper ID in register A to the ADD_PROCESS routine, regardless of which process (#1, #2, or #3) is executing the code. The following example represents one way that this could be accomplished. Page 9 ; ***************************************************************** ; Example of how to use IF_PROC. ; This code assumes that the next desired run-time for this process ; has been loaded into R0. IF_PROC 3 MOVE.W R0, C ; Next time process should be run. MOVE.P2 ^d3, A ; ID for this process into A.B. JUMP PROCESS_SCHED ENDIF_PROC IF_PROC 4 MOVE.W R0, C ; Next time process should be run. MOVE.P2 ^d4, A ; ID for this process into A.B. JUMP PROCESS_SCHED ENDIF_PROC IF_PROC 5 MOVE.W R0, C ; Next time process should be run. MOVE.P2 ^d5, A ; ID for this process into A.B. JUMP PROCESS_SCHED ENDIF_PROC PROCESS_SCHED: CALL ADD_PROCESS ; Schedule the current process to ; run at the time given in C. JUMP TO_SCHEDULER ; Return control to the scheduler. ; ***************************************************************** Figure 5 3.7 Interprocess Communication In previous sections it was described how interprocess communication could be done by modifying the latent process's context. While this is valid, it is a terribly inefficient means for communicating between processes. A much better way is to use global variables which are shared between processes. Since processes are never interrupted by the scheduler, there is no need to synchronize access of the shared variables, so processes may pass information back and forth freely using this scheme. 3.8 Process Timing One issue that the programmer needs to be aware of when running under MPE is that of process latency, particularly under heavy CPU loading, and the effect that this latency has on process timing. Consider a scenario where many processes are running under MPE. Page 10 Looking at two of these processes, say #1 and #2, let us pretend that process #1 always schedules itself to run at CUR_TIME + 1/32th seconds, while process #2 always schedules itself to run at CUR_TIME + 1/16 seconds. One would therefore expect that process #1 would execute 32 times each second, and that process #2 would execute 16 times each second. If the CPU is lightly loaded, then this is (almost) true. If, however, the CPU is heavily loaded by the other processes running under MPE, then processes #1 and #2 will undoubtedly run less frequently than their reschedule intervals would indicate. Furthermore, both processes will execute the same number of times in any given time interval. The exact frequency of repetition will be determined by the degree of CPU loading and the number of processes running under MPE. The reason for this is as follows. When the CPU is heavily loaded, processes which schedule themselves a very short time into the future will always be overdue when the scheduler checks them. In our scenario, this means that processes #1 and #2 will always be overdue by the time the scheduler gets a chance to check them in the run-list. The result is that each process will be run exactly once for each pass the scheduler makes through the run-list. Another way to understand the problem is to consider a simple graphics process whose job is to move a "bullet" across the screen at a given rate. One way to do this would be to have the process move the bullet a pixel or so and then reschedule itself for a short time into the future. The time for which the process reschedules itself could be given as the time the process was most recently executed (as stored in CUR_TIME) plus some incremental time chosen to give a desired repetition rate for the process. While this simple approach will work well when the CPU is lightly loaded, it does not work as well when the CPU is very heavily loaded. What happens when the CPU is heavily loaded is that processes may sit in the scheduler's run-list long past the time for which they were scheduled, simply because the CPU is very busy running the other processes in the system. Considering the "bullet" process, this results in "lost" time, and causes the bullet on the screen to slow down. The reason time is "lost" from the perspective of the bullet is that being ignorant of absolute time (the time indicated by the system clock) the process always reschedules itself relative to when its current invocation started, as indicated by CUR_TIME. The process makes no attempt to "catch-up" to where the bullet would have been if the CPU had been lightly loaded. One solution to this problem is to schedule processes based not on the time value stored in the CUR_TIME variable, but rather based on current time value in the system clock. Doing this enables a process to recover time that was lost to other processes. In the case of our "bullet" process, the process code could examine the system clock, determine how much real time has passed since the process was last invoked, and then move the bullet a number of pixels consistent with the desired rate of travel. The visual result on the screen in this case is that the bullet has the desired average rate of travel across the screen, regardless of CPU loading. As a side effect, the motion Page 11 of the bullet may appear a little "jittery" since the process may move it several pixels in rapid succession during each invocation, rather than moving it one pixel per invocation as in the simpler implementation. The programmer should be very careful, however, when programming processes which attempt to "catch up" to the system clock in this manner. To see how this type of process definition can lead to problems, consider a heavily loaded system with two bullet processes which run based on the absolute system time as described above. To facilitate the reading of the system clock, MPE includes a routine called GET_TICKS which returns the current (16 nibble) value of the system clock to register C. This routine modifies register A, but leaves all other processor registers intact. Examples of both implementations of the "bullet" process can be seen in the demo processes included with the MPE distribution. This demo also illustrates the problems regarding the relative frequencies of process invocation in a heavily loaded system. 3.9 Assembly-time Errors Because of the way MPE sets up its data structures for managing the processes, it is necessary that the processes be defined by the programmer with contiguous process IDs. For example, if the programmer has defined process #4, then processes #1-#3 must also have been defined. If the programmer leaves any "holes" in the process numbering scheme, MPE will generate an error message to that effect at assembly-time. 3.10 Run-time Errors There are two common errors that the programmer might be likely to make when using MPE. Both involve the use of the ADD_PROCESS routine. The first scenario is when the user attempts to schedule more processes than there are process slots in the scheduler's run-list. The current implementation of STAR allows only 10 pending processes in the run-list at a time. If the user attempts to schedule more than 10 processes concurrently, MPE will push onto the stack an error code of 99d, indicating that the run-list overflowed, and the ID of the process which made the call to ADD_PROCESS when the failure occurred. Both of these values are pushed as short binary integers. MPE then halts execution of the machine language code and continues with the RPL thread. The size of the run_list can be easily increased to accommodate applications which need more than 10 processes scheduled concurrently. Refer to the section below on "MPE Customization". The other potential misuse of ADD_PROCESS is when the user calls Page 12 ADD_PROCESS with an invalid process ID. I did this once while debugging MPE and my machine promptly crashed with "memory lost". Very unforgiving. Therefore, MPE now checks the ID passed into ADD_PROCESS to make sure it corresponds to one of the processes defined by the user. If not, then MPE pushes an error code of 98d onto the stack as a short binary, indicating that an invalid ID was passed into ADD_PROCESS. MPE then also pushes as short binaries the ID of the process which called ADD_PROCESS, and the erroneous ID, in that order. MPE then halts execution of the machine language code and continues with the RPL thread. 3.11 MPE Customization The current implementation of MPE will allow up to 20 defined processes. The user should note, however, that the version of MPE in this distribution will only allow as many as 10 processes to be concurrently scheduled. This has been done to optimize performance for the set of process definitions which comprise the MPE demo application. This can easily be changed by the programmer to hold as many processes as you need. Read on. The scheduler scans through the run list in a circular fashion, updating its copy of the system time once per pass through the run-list. As the scheduler does a pass through the run-list, it checks the time stamp of any valid processes against its copy of the system time and runs the associated process if its time stamp is earlier than the current system time. Because of this circular scan algorithm, it is highly advisable to limit the size of the run-list to the number of processes which the programmer expects to have concurrently scheduled. In this way, the scheduler does not waste valuable CPU time checking run-list slots which will never be filled. The run-list is a static data structure located at the STAR label "RUN_LIST:". The programmer should increase or decrease the size of this data allocation as needed, being sure to update the RUN_LIST_SIZE variable at the same time. (RUN_LIST_SIZE is a STAR variable which is defined just above where the RUN_LIST is located in the MPE sources.) For each new slot in the RUN_LIST, the programmer should add a "DATA.B 0" to hold the process ID and then a "DATA.W 0" to hold the process time stamp. The RUN_LIST_SIZE variable indicates the number of slots in RUN_LIST. If the current maximum of 20 processes is insufficient for a given application, then the programmer should add lines of code to the MPE data structure and initialization sections which appears just before and after the START: label. The lines which will need to be added should be fairly obvious, and will have the form of: Page 13 (appearing just before the START: label) GEN_HEADERS 21 GEN_HEADERS 22 . . . GEN_DESCRIPTOR 21 GEN_DESCRIPTOR 22 . . . (...and then appearing just after the START: label) HEADER_INIT 21 HEADER_INIT 22 . . . Figure 6 The new lines must be added to the end of each section (after the line corresponding to process #20). You will need to make similar additions in the FILL_DESCRIPTORS macro, adding more "FILL xx" lines for the additional processes. The need to make the above additions will be removed in a later version of MPE. If anyone needs more than 20 processes, please send mail. I want to see your application :-) 3.12 Assembler Considerations Due to limitations in the current version of Star (V1.04.4), applications running under MPE should be assembled with the jumpification feature turned off. This is done by using the -j switch on the command line when STAR is run. For help on the various switches which can be used on the STAR command line, invoke STAR with the -h switch, which will cause STAR to print out its standard help text. Page 14 4 MPE DEMO PROCESSES Included with this distribution of MPE is a set of seven process definitions which demonstrate the basic features of MPE. The following sections will describe briefly what each of these processes do and how they interact. 4.1 Application Overview This demo application is a simple demonstration of how MPE can be used to do graphical animation of several objects on the screen at once. This also provides a good visual demonstration of the CPU loading effects described in earlier sections. Basically, the application consists of a fire button and four "bullets". This application uses the [+] button as the fire button. When the user starts the application, the screen blanks and waits for the user to press the fire button. For each press of the [+] button, the application will fire a bullet. The bullets start at the upper left of the screen and move down the screen, row by row, until they reach the bottom of the screen. When any bullet reaches the lower right of the screen, the application stops. When the user has fired all four bullets, the fire button is disabled for the remainder of the application. The [.] key quits the application. Refer to the following section for how to download and run the application. 4.2 Downloading And Running The Application Included in this distribution is the uuencoded binary file. This binary file is a directory containing the machine language code which makes up the MPE application, and a short RPL program which runs it. The user should uudecode this file and then download the resulting binary file in the usual manner to the HP48SX (make sure to set the HP48SX kermit to binary mode. The RPL program, "DEMO", in the new directory serves only to clear the display and set the display to PICT memory before running the machine language application ("XXX"). You should _not_ run the "XXX" program directly, since if PICT memory has been purged (by doing a PICT PURGE), then the application may corrupt memory. Once started by pressing the button labeled "DEMO", the machine language program can be aborted at any time by pressing the [.] button. 4.3 Process Descriptions The following sections describe each of the seven processes which make up the application. Note that processes #1 and #2 are not integral to the application and could be omitted. Page 15 4.3.1 Process #1 This is the process which is auto-executed by MPE at startup. In terms of the application, the only thing that this process does is to start up the keyboard scanner process (#7). Another function that this process performs, in conjunction with process #2 is to do a register context integrity test periodically while the application is running. This was written mainly for debugging MPE, but may be useful to the programmer in general, so I have left it in. It runs once per second, so it does not add significant overhead to the application. 4.3.2 Process #2 This process does nothing except clear all the processor registers. The purpose of this process is to work with process #1 to implement a register context integrity test. 4.3.3 Processes #3, #4, And #5. These processes are grouped together here because they share the same process definition. This process definition shows a good example of how to share code between two or more processes. The code for this process definition makes use of the IF_PROC/ENDIF_PROC macros to do conditional execution of certain sections of code based on the current context ID. For example, the three bullets owned by processes #3, #4, and #5 all move at different speeds on the screen. This was done here to visually show the independence of the bullet processes running in the system. The process definition uses the IF_PROC/ENDIF_PROC macros to schedule the correct process at the correct interval, thus determining the speed of each bullet. One thing to note is that this process definition does not attempt to control the speed of a bullet with great accuracy. In other words, as the four bullets are fired and the CPU becomes more heavily loaded, the speed of the bullets corresponding to processes #3, #4, and #5 will slow down slightly. This is due to process latency in the scheduler. Process #6 shows a similar bullet process, but one which keeps track of absolute time and maintains an accurate velocity on the screen, independent of CPU loading. 4.3.4 Process #6 This process definition is very similar to that which defines processes #3, #4,and #5. The main difference is that process #6 keeps track of absolute time, making sure that its bullet moves at exactly the velocity indicated in the process definition. In this way, even though the bullet for this process does not start until the CPU is already very busy with the other three bullets, it will maintain its velocity at the expense of the other three bullets. Page 16 The effects of the use of absolute time in this process, versus relative time in the other three bullet processes, can be seen in two ways. The first way is that after firing the four bullets, it should be discernible that the fourth bullet (process #6) moves slightly faster than the second one (process #4). By examining the definitions of these processes, one quickly sees that processes #4 and #6 have the same rescheduling interval. The difference in speed is therefore attributable to the time that process #4 loses while sitting in the RUN_LIST waiting to be run by the scheduler. Since process #6 keeps track of absolute time internal to itself, this queuing time is accurately recovered each time the process is invoked. The second way that the use of absolute time in process #6 can be seen is by pressing and briefly holding the [ON] key. Pressing this key temporarily halts execution of the calculator, causing all processes to pause. When the key is released, however, the bullet of process #6 will quickly "zip" ahead to catch up to where it should be. A similar effect can also be seen by pressing and holding some other key (besides [ON]). Since the keyboard controller causes lots of interrupts in when this is done, it has the effect of heavily loading the CPU. What can be seen on the display is that while the user holds down a key and thereby loads the CPU, the three bullets which do not use absolute timing slow down noticably, while the bullet for process #6 maintains its correct average velocity. The manner in which process #6 "catches up" to where it should be can be seen in the bullet's jumpiness. 4.3.5 Process #7 This process comprises the keyboard scanner. This process simply checks the keybuffer for the [+] key, and if it finds it then the next bullet process is scheduled for execution. If the [.] key is pressed, this process will cause the application to exit and continue with the RPL thread. This process runs approximately 8 times per second.