ervin@pinbot.enet.dec.com (Joseph James Ervin) (05/16/91)
; MPE V1.0. Copyright Joseph James Ervin 1991. ; The following is a simple multiprogramming environment (MPE) which allows ; users to program concurrent processes to run on the HP48SX. ; Addresses of Rom routines save_registers = ^x679B ; Saves system registers. Uses C, D0. restore_registers = ^x67d2 ; Restores system register. rplcont = ^x71BE ; Jumps to the next RPL instruction. rr_rplcont = ^x5143 ; Restores the system registers and ; then does rplcont. get_short = ^x6641 ; Pops the short of TOS and returns it ; into A.A push_a_cont = ^x357C ; Push the contents of A.A onto stack ; and continue RPL. push_r0_short = ^x6537 ; Push R0 as new system binary. This ; routine uses the SAVED system ROM_GET_TICKS = ^x130E ; Returns the number of timer ticks ; in C.13. Trashes just about every ; other register. radix ^d10 ; General address definitions bos = ^x7057E ; Pointer to beginning of the stack. pict_pointer = ^x70565 ; Pointer to PICT grob. menu_pointer = ^x70556 ; Pointer to Stack Grob. keybuf = ^x704ea ; keyboard buffer. ; *************************************************************************** ; The following macro is used to generate a process header and to allocate ; storage for the process context. The macro requires as its only parameter ; the ID of the process for which the process header is to be generated. macro gen_headers n=0, label save n n = $n if (n==0) ; This is for the scheduler. $(label)_context: DATA.W 0, 0 ; Storage for B, D. DATA.A 0, 0 ; Storage for D0, D1. DATA.W 0, 0, 0, 0, 0 ; Storage for R0, R1, R2, R3, R4 $(label)_header: data.a 0, 0 else ; If not for scheduler. if def process$(n)_init ; The user got it right and defined the _init section of the ; process. Now we need to allocate storage for the context area and build the ; process header. $(label)_context: DATA.W 0, 0 ; Storage for B, D. DATA.A 0, 0 ; Storage for D0, D1. DATA.W 0, 0, 0, 0, 0 ; Storage for R0, R1, R2, R3, R4 $(label)_header: data.a 0, 0 endif endif restore n endmacro hide gen_headers ; *************************************************************************** ; The following macro is used to generate the process descriptor list used ; by the scheduler. This macro checks to see that the processes declared by ; the user have contiguous process IDs. If not, then an error message is ; generated. macro gen_descriptor n save n n = $n if (n == 0) DESCRIPTOR_LIST: DATA.A scheduler_header ; Process descriptors. contig = 1 num_of_proc = 0 endif if (!def process$(n)_init && !(n==0)) contig = 0 endif if def process$(n)_init num_of_proc = (num_of_proc + 1) if (contig==0) error Defined processes are not contiguously numbered. endif ; The user got it right and defined both the _init and _code sections of the ; process. The process context storage and header should already have been ; set up, so now we need to add the entry for the process descriptor. DATA.A process$(n)_header ; Process descriptor. endif restore n endmacro hide gen_descriptor ; *************************************************************************** ; The following macro is used to make the relative addresses stored into the ; process header into absolute addresses. macro header_init n save n, dest, src1, src2 n = $n if ((def process$(n)_init) || (n==0)) ; The user has defined this process, so we need to write the address pointers ; in the process header. if (n==0) dest = scheduler_header src1 = scheduler_context src2 = scheduler_code else dest = process$(n)_header src1 = process$(n)_context src2 = process$(n)_init endif addr $dest, D0 ; Point D0 to the process (scheduler) header. addr $src1, c ; Get address of context. move.a c, @d0 addr $src2, c add.a ^d5, d0 move.a c, @d0 endif restore n, dest, src1, src2 endmacro hide header_init ; *************************************************************************** macro fill n save n n = $n if def process$(n)_init addr process$(n)_header, c move.a c, @d0 ; Fill in the process descriptor. add.a 5, d0 ; D0 now points to the next process descriptor. endif restore n endmacro hide fill ; The following macro writes the address pointers in the process descriptor ; list. macro descriptor_init ; We know that the scheduler process exists, so we can fill in that ; descriptor. addr descriptor_list, d0 ; First, point d0 at the descriptors. addr scheduler_header, c ; Get the scheduler descriptor. move.a c, @d0 ; Fill in scheduler descriptor. add.a 5, d0 ; D0 now points to the next process descriptor. ; Now we want to sequence through the processes. For each process, if the ; user has defined the process, then we need to fill in the descriptor. fill 1 ; fill in descriptors for the 20 processes. fill 2 fill 3 fill 4 fill 5 fill 6 fill 7 fill 8 fill 9 fill 10 fill 11 fill 12 fill 13 fill 14 fill 15 fill 16 fill 17 fill 18 fill 19 fill 20 endmacro hide descriptor_init ; **************************************************************************** ; The following macro enables the user to easily configure a process to start ; at the INIT or CODE sections. The first argument is an integer specifying ; the process to be reconfigured. The second argument is the label of the new ; entry point. macro process_start n, where addr process$(n)_header, d0 ; Point d0 at the process header. add.a 5, d0 ; Move down to the code pointer. addr $where, c move.a c, @d0 ; Reconfigures process "n" to run from "where" ; the next time it comes due. endmacro hide process_start ; **************************************************************************** ; The following macro enables the user to easily configure a process to start ; at a new label. This macro takes no arguments, but expects that C.A contains ; a pointer to the new entry point. This macro then ; automatically changes the entry point of the current process to ; the address in C.A. It is expected that the process would load C.A with the ; address label, possibly with the ADDR macro. The purpose behind this is to ; allow the programmer to easily redirect the entry point of current process ; without requiring the use of the IF_PROC/ENDIF_PROC construct. This is ; particularly advantageous when many processes share a section of code. macro cur_process_start PUSH.A C ; Save the pointer to the new entry point. CALL GET_CURRENT_ID ; Get the process ID in A.B. Trashes C. CLR.W C ; MOVE.B A, C ; MOVE.W C, A ; Zero out upper bits of A. ADD.A C, C ; ADD.A C, C ; ADD.A A, C ; ...and multiply by 5. ; C.A now contains the offset from beginning of the process descriptor list. ADDR DESCRIPTOR_LIST, D0 ; Address of descriptor list. Trashes A. SWAP.A A, D0 ADD.A C, A ; Add in the offset. D0 points at process descriptor. SWAP.A A, D0 MOVE.A @D0, C ; Get the descriptor (pointer to the process header). MOVE.A C, D0 ; Point D0 to the process header. add.a 5, d0 ; Move down to the code pointer. POP.A C ; Pop the new entry point back into C. move.a c, @d0 ; Reconfigures the current process to start at the ; new entry point the next time it comes due. endmacro hide cur_process_start ;************************************************************************** ; The following macro is used by the MPE programmer to execute a certain piece ; of code conditioned on whether a specified process is current. macro if_proc n n=$n ; Do the macro. save nosym nosym = gensym swap.a c, d0 ; Save D0 in C.A addr current_context_id, d0 ; Point to current context id variable. move.b @d0, a ; Get the current context id. swap.a c, d0 ; Restore old D0. move.p2 $(n), c ; brne.b c, a, $nosym ; Skip the code if specified process is not ; current. endmacro hide if_proc macro endif_proc $NOSYM: ; Execution jumps to here if the specified process was not current. restore nosym endmacro hide endif_proc header code ; Descriptions of global RAM routines ; ADD_PROCESS ; Arguments: A.A contains the address of the process ; header of the process to be added. ; C.W contains the execution time. ; RUN_IT ; Jumped-to from the SCHEDULER. Runs the process ; whos ID is given in A.B. ; SAVE_CONTEXT ; Saves the context of the current process, as ; indicated by the CURRENT_CONTEXT_ID variable. ; RESTORE_CONTEXT ; Restores the context of the process whos ID is given ; in A.B. This routine also leaves a pointer to the ; code of the new process in C.A, so the calling ; process can either jump to the new process, or just ; use RESTORE_CONTEXT to copy the context of another ; process to itself without actually transfering ; execution to the new process. jump START ; Skip over all the global variables below and go ; straight into the initialization code. ; **************************************************************************** ; The following state variables are intended to be used with the ; SAVE_STATE and RESTORE_STATE routines. These routines save the system ; registers into the following global state variables. A and C are never ; saved or restored. SAVE_STATE trashes A and C. RESTORE_STATE trashes A, but ; preserves C. STATE_DATA1: DATA.W 0, 0 DATA.5 0, 0 ; Scratch storage for B, D, D0, D1 ; This storage area can be used by each process ; whenever some scratch storage is needed. This ; storage is used by the SAVE_STATE and ; RESTORE_STATE routines. ; **************************************************************************** ; **************************************************************************** ; The following variable is used to hold the global time variable. CUR_TIME: DATA.W 0 ; Global variable to hold current time. ; Set by scheduler. Read by processes. ; *************************************************************************** ; *************************************************************************** ; The following storage areas are to hold the register context for each ; process running in the system. CURRENT_CONTEXT_ID: DATA.B 0 ; Indicates the ID of the processor context ; we are currently in. This variable will be ; used by the SAVE_CONTEXT and RESTORE_CONTEXT ; routines. A zero identifies the scheduler. ; The other processes are numbered 1-255. ;*************************************************************************** ; The following is the memory area where the schedulers run list is kept. ; Each entry consists of a one BYTE process ID (1-255; 0 is reserved for the ; scheduler), followed by a 13 nibble time value which represents when this ; process should be run. The scheduler will step down this list, comparing the ; current time value stored in the CUR_TIME variable to the execution time of ; each process in the run list. When a process comes due, the scheduler will ; call the SWAP_CONTEXT routine which will swap in the context of the due ; process and then jump to the address given in the process header. RUN_LIST_SIZE = ^d10 ; There are 10 process slots. RUN_LIST: DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. DATA.B 0 ; ID of a process. DATA.W 0 ; Execution time of process. ; ************************************************************************* ; ************************************************************************* gen_headers 0, scheduler ; Create scheduler header. gen_headers 1, process1 ; Create process headers. gen_headers 2, process2 gen_headers 3, process3 gen_headers 4, process4 gen_headers 5, process5 gen_headers 6, process6 gen_headers 7, process7 gen_headers 8, process8 gen_headers 9, process9 gen_headers 10, process10 gen_headers 11, process11 gen_headers 12, process12 gen_headers 13, process13 gen_headers 14, process14 gen_headers 15, process15 gen_headers 16, process16 gen_headers 17, process17 gen_headers 18, process18 gen_headers 19, process19 gen_headers 20, process20 gen_descriptor 0 gen_descriptor 1 gen_descriptor 2 gen_descriptor 3 gen_descriptor 4 gen_descriptor 5 gen_descriptor 6 gen_descriptor 7 gen_descriptor 8 gen_descriptor 9 gen_descriptor 10 gen_descriptor 11 gen_descriptor 12 gen_descriptor 13 gen_descriptor 14 gen_descriptor 15 gen_descriptor 16 gen_descriptor 17 gen_descriptor 18 gen_descriptor 19 gen_descriptor 20 START: ; move.a pc, a ; debug ; move.a a, r0 ; call save_registers ; call restore_registers ; call push_r0_short ; call save_registers call save_registers header_init 0 ; Fill in the scheduler header. header_init 1 ; Fill in the process headers. header_init 2 header_init 3 header_init 4 header_init 5 header_init 6 header_init 7 header_init 8 header_init 9 header_init 10 header_init 11 header_init 12 header_init 13 header_init 14 header_init 15 header_init 16 header_init 17 header_init 18 header_init 19 header_init 20 ; Now fill in the address pointers in the process descriptor list. descriptor_init ; Now we need to clear out the RUN_LIST so we know we are starting with ; a clean slate. move.p5 run_list_size, c ; Number of entries. addr run_list, d0 ; Point at top of RUN_LIST. Trashes A. CLR.W A CLR_RUN_LIST: MOVE.B A, @D0 ADD.A ^d2, d0 add.a ^d16, d0 dec.a c brnz.a c, clr_run_list ; Clear out all the process IDs from RUN_LIST. ; Now we need to make the scheduler the current process. ADDR CURRENT_CONTEXT_ID, D0 CLR.W A MOVE.B A, @D0 ; Make scheduler the current context. ; **************************************************************************** ; NOTES for SCHEDULER: ; R0 ; Index into RUN_LIST (slot #), numbered 0 through N-1. ; R1 ; Copy of the ID of the new process to be run. ; R2 ; ; R3 ; ; R4 ; ; D0 ; ; D1 ; Address Pointer into RUN_LIST. ; B ; Contains a local copy of the CUR_TIME variable. ; D ; Number of process slots in RUN_LIST. ; Now we need to start up the scheduler. The scheduler will step down the run ; list, running each process as it comes due. The process header ; will initially point to the init code for the process. At the end of the ; initialization phase, the user may mofify the process header to ; point at the body of the process, where the real work is done. CALL GET_TICKS ; Get the time into C.13. MOVE.P2 ^d1, A ; Process ID. CALL ADD_PROCESS ; Schedule process1 for immediate execution. MOVE.P2 RUN_LIST_SIZE, A ; Number of process slots in RUN_LIST. DEC.B A ; Decrement this number by 1; 0 to (N-1) limit. MOVE.B A, C ; MOVE.B C, D ; Keep RUN_LIST_SIZE in D. CLR.W A ; MOVE.W A, R0 ; R0 holds current slot pointer. ADDR RUN_LIST, C ; Get the address of RUN_LIST into C. SWAP C, D1 ; Initialize RUN_LIST index to top of RUN_LIST. ADDR CUR_TIME, D0 ; Put the address of the CUR_TIME variable ; in D0. Trashes A. CALL GET_TICKS ; Puts the value of ticks in C.13. Trashes A. MOVE.W C, @D0 ; Update the value of the CUR_TIME variable. MOVE.W C, B ; Put a copy in B. This is the local ; copy for the scheduler. ; Now we need to step down the run list, checking the run time of each slot ; which has a nonzero ID against the current time. I will keep the run_list ; slot count in R0. Whenever the scheduler is entered from the SCHEDULER entry ; point, the scheduler will pick up at the point in the run list where it left ; off. For example, if the scheduler runs the process in slot #5, then when ; that process completes and returns to the SCHEDULER entry point, the next ; slot to be checked against the clock will be slot #6. SCHEDULER_CODE: ; This is the loop that scans the run list and runs the processes. ; This is the point where execution will return when a process completes. ; First, the scheduler needs to capture the current time, which will be used ; to determine whether a given process is due. CLR.W A ; Clear upper bits of A. SCHED: MOVE.B @D1, A ; Get the process ID of this slot. BRNZ.B A, GOOD_ID ; Is this a real process ID or is it zero? CALL NEXT_SLOT ; Else point to the next slot in the RUN_LIST. JUMP SCHEDULER_CODE ; Keep scanning the RUN_LIST for due processes. GOOD_ID: ; Check the time and run the process if it is due. ADD.A ^d2, D1 ; Point D1 at the execution time of this slot. MOVE.W @D1, C ; Get the execution time for this process. SUB.A ^d2, D1 ; Point back at ID; Required by NEXT_SLOT. BRGE.W B, C, RUN_IT ; Run the process if it has come due. ; The process ID is in A.B. CALL NEXT_SLOT ; Point to the next slot in the RUN_LIST. JUMP SCHEDULER_CODE ; Keep scanning the RUN_LIST for due processes. ; **************************************************************************** NEXT_SLOT: ; Point to the next process in the RUN_LIST. ; This is the routine that advances the RUN_LIST slot counter (R0) and address ; pointer (D1) to the next slot in a wraparound fashion. ADD.A 2, D1 ; ADD.A 16, D1 ; Point D1 to the next ID in the RUN_LIST. MOVE.B R0, A ; Get the RUN_LIST slot counter. INC.B A ; Indicate the next process slot. MOVE.B A, R0 ; Update R0 with the new slot count. MOVE.B D, C ; Get the RUN_LIST index limit. BRGE.B C, A, DONE_NEXT_SLOT ; If not end of list, then return. ; We have reached the end of the RUN_LIST, so we need to reset the RUN_LIST ; slot counter in R0 and the memory pointer in D1. CALL GET_TICKS ; Puts the value of ticks in C.13. Trashes A. MOVE.W C, B ; Put a copy in B. This is the local ; copy for the scheduler. CLR.B A MOVE.B A, R0 ; Clear out the slot counter in R0. ADDR RUN_LIST, C ; Get the address of RUN_LIST into C. SWAP C, D1 ; Initialize RUN_LIST index to top of RUN_LIST. DONE_NEXT_SLOT: RETCLRC ; **************************************************************************** RUN_IT: ; Run the process pointed to by A.B. ; This is the routine that runs the process pointed to by A.B. This is done ; by getting the process header pointer from the process descriptor list. ; The process header contains pointers to the process context and to the code ; for the process. The process context is first restored, and then execution ; jumps to the process code. MOVE.B A, R1 ; Save the new process ID for now. ADDR CUR_TIME, D0 ; Put the address of the CUR_TIME variable ; in D0. Trashes A. MOVE.W B, C ; Time that was compared to time stamp. MOVE.W C, @D0 ; Update the value of the CUR_TIME variable. CLR.W A MOVE.B A, @D1 ; Write a zero over the process ID in the ; RUN_LIST, thus deleting the process from the ; RUN_LIST. CALL NEXT_SLOT ; Point SCHEDULER to next slot in the RUN_LIST. CALL SAVE_CONTEXT ; Uses CURRENT_CONTEXT_ID as a process ID to ; save the current register context to the ; appropriate place. Does not save A or C. ADDR CURRENT_CONTEXT_ID, D0 ; Point D0 at curr context variable. ; Trashes A. MOVE.B R1, A ; Restore the new process ID. MOVE.B A, @D0 ; Store the new process ID into the ; CURRENT_CONTEXT_ID variable. CALL RESTORE_CONTEXT ; Restores the context of the new process, and ; leaves a pointer to the new process code in ; C.A. JUMP C ; Jump to new process code. ; **************************************************************************** RESTORE_CONTEXT: ; This routine uses the context ID in A.B and restores that process context. ; The pointer to the code for the new process is left in C.A. CLR.W C ; MOVE.B A, C ; MOVE.W C, A ; Zero out upper bits of A. ADD.A C, C ; ADD.A C, C ; ADD.A A, C ; ...and multiply by 5. ; C.A now contains the offset from beginning of the process descriptor list. ADDR DESCRIPTOR_LIST, D0 ; Address of descriptor list. Trashes A. SWAP.A A, D0 ADD.A C, A ; Add in the offset. D0 points at process descriptor. SWAP.A A, D0 MOVE.A @D0, C ; Get the descriptor (pointer to the process header). ; Now that we have the descriptor (a pointer to the process header), we need ; to fetch the context pointer, restore the context, and then leave a pointer ; to the process code in C.A. MOVE.A C, D0 ; Point D0 to the process header. MOVE.A @D0, A ; Get the address of the process context. ADD.A ^d5, D0 ; Point D0 to the code pointer. MOVE.A @D0, C ; Get the code pointer. PUSH.A C ; Save the code pointer on the stack for now. MOVE.A A, D0 ; Point D0 to the context area. MOVE.W @D0, C ; MOVE.W C, B ; Restore B. ADD.A ^d16, D0 MOVE.W @D0, C ; MOVE.W C, D ; Restore D. ADD.A ^d16, D0 MOVE.A @D0, C ; Restore D0 (in C for now). ADD.A ^d5, D0 SWAP.A D1, C ; Save D0 contents into D1 for now. MOVE.A @D0, C ; Get D1 contents into C. SWAP.A D1, C ; Restore D1. C holds restored D0 contents. ADD.A ^d5, D0 MOVE.W @D0, A ; MOVE.W A, R0 ; Restore R0. ADD.A ^d16, D0 MOVE.W @D0, A ; MOVE.W A, R1 ; Restore R1. ADD.A ^d16, D0 MOVE.W @D0, A ; MOVE.W A, R2 ; Restore R2. ADD.A ^d16, D0 MOVE.W @D0, A ; MOVE.W A, R3 ; Restore R3. ADD.A ^d16, D0 MOVE.W @D0, A ; MOVE.W A, R4 ; Restore R4. SWAP.A C, D0 ; Restore D0. POP.A C ; Pop the pointer to the process code in C. RET ; ...and return. ; **************************************************************************** SAVE_CONTEXT: ; This routine uses the context ID in CURRENT_CONTEXT_ID and saves that ; process context. ; Note that this routine makes sure to not alter the states of the system ; registers or the general purpose registers in the process of saving them. ; Note also that A and C are not saved. The contents of B, D, D0, D1, and ; R0-R4 are preserved by this routine. CALL GET_CURRENT_ID ; Get CURRENT_CONTEXT_ID into A.B SAVE_CONTEXT_BY_A: ; This entry point allows the process to save the ; current context to that of the process given in A.B. CLR.W C ; MOVE.B A, C ; MOVE.W C, A ; Zero out upper bits of A. ADD.A C, C ; ADD.A C, C ; ADD.A A, C ; ...and multiply by 5 to give an offset into the ; descriptor list. ; C.A now contains the offset from beginning of the process descriptor list. SWAP.A A, D0 ; Get a copy of D0 into A. SWAP.A A, C ; Do not lose the descriptor offset (save it in A). PUSH.A C ; Save old D0 on the return stack. SWAP.A A, C ; Restore the descriptor offset into C. ADDR DESCRIPTOR_LIST, D0 ; Address of descriptor list. Trashes A. SWAP.A A, D0 ; Put pointer to descriptor list into A. ADD.A C, A ; Add in the offset. MOVE.A A, D0 ; D0 points at process descriptor. MOVE.A @D0, C ; Get the descriptor (pointer to the process header). ; Now that we have the descriptor (a pointer to the process header), we need ; to fetch the context pointer and save the process context. MOVE.A C, D0 ; Point D0 to the process header. MOVE.A @D0, C ; Get the address of the context. MOVE.A C, D0 ; Point D0 to the context area. MOVE.W B, A ; MOVE.W A, @D0 ; Save B. ADD.A ^d16, D0 SWAP.W D, C MOVE.W C, @D0 ; Save D. SWAP.W D, C ADD.A ^d16, D0 POP.A C ; Recover the old D0 (pushed previously). MOVE.A C, @D0 ; Save D0. Leave copy of D0 in C. ADD.A ^d5, D0 SWAP.A D1, C ; MOVE.A C, @D0 ; Save D1. SWAP.A D1, C ; Restore D1. copy of old D0 still in C. ADD.A ^d5, D0 MOVE.W R0, A ; MOVE.W A, @D0 ; Save R0. ADD.A ^d16, D0 MOVE.W R1, A ; MOVE.W A, @D0 ; Save R1. ADD.A ^d16, D0 MOVE.W R2, A ; MOVE.W A, @D0 ; Save R2. ADD.A ^d16, D0 MOVE.W R3, A ; MOVE.W A, @D0 ; Save R3. ADD.A ^d16, D0 MOVE.W R4, A ; MOVE.W A, @D0 ; Save R4. SWAP.A C, D0 ; Restore D0. RET ; Clear carry bit and return. ; **************************************************************************** ADD_PROCESS: ; This routine will add a process to the process list. The basic method used ; is simply to insert the new process ID and execution time into the first ; empty slot (defined as one which has a zero for its process ID). ; The process ID is expected in A.B, and the process execution time is expected ; in C.13. ; ; Note: This routine changes many of the system registers, as well as the ; general purpose registers. For this reason, it is highly recommended ; that the calling routine do a SAVE_CONTEXT before executing this ; routine, followed immediately by a RESTORE_CONTEXT or a jump to the ; scheduler. MOVE.B A, R2 ; Save the new process ID. BRZ.B A, BAD_PROC_ID ; Zero is not a valid process ID. MOVE.W C, R3 ; Save the execution time of the new process. MOVE.P2 NUM_OF_PROC, C ; Get the number of defined processes. BRGE.B C, A, J_VPI ; Did the user pass in a valid ID? JUMP BAD_PROC_ID ; If not then output the error information. J_VPI: JUMP VALID_PROC_ID ; Else schedule the process. BAD_PROC_ID: CALL RESTORE_REGISTERS MOVE.P5 ^d98, C MOVE.A C, R0 CALL PUSH_R0_SHORT ; Push an error code of 98 to indicate that ; a process tried to schedule an invalid ID. CALL SAVE_REGISTERS ADDR CURRENT_CONTEXT_ID, D0 CLR.W C MOVE.B @D0, C MOVE.W C, R0 ; Push the ID of the process which CALL RESTORE_REGISTERS ; caused the error. CALL PUSH_R0_SHORT CALL SAVE_REGISTERS CLR.W A MOVE.B R2, A MOVE.A A, R0 CALL RESTORE_REGISTERS CALL PUSH_R0_SHORT ; Push the erroneous ID that the current CALL SAVE_REGISTERS ; process tried to schedule. JUMP RR_RPLCONT VALID_PROC_ID: CLR.W C MOVE.P2 RUN_LIST_SIZE, C ; Number of process slots in RUN_LIST. DEC.B C ; Decrement this number by 1; 0 to (N-1) limit. MOVE.W C, D ; Keep RUN_LIST_SIZE limit in D. CLR.W A ; MOVE.W A, R0 ; R0 holds current slot pointer. ADDR RUN_LIST, C ; Get the address of RUN_LIST into C. SWAP.A C, D1 ; Initialize RUN_LIST index to top of RUN_LIST. ADD_PROCESS_LOOP: MOVE.B @D1, A ; Check for an empty slot. BRZ.B A, FOUND_EMPTY ; CALL NEXT_SLOT ; Look at the next slot. MOVE.B R0, A ; What slot are we looking at? BRNZ.B A, ADD_PROCESS_LOOP ; Fall through the loop when every slot ; is non-empty. ; If we fall through this loop, then we are in a serious error scenario, ; because the process table has filled up and there are no empty slots. ; This is very bad. move.p5 ^d99, a ; Error code for run_list overflow. ERROR: MOVE.A A, R0 CALL RESTORE_REGISTERS CALL PUSH_R0_SHORT ; Push ^d99 error code to indicate a run-list CALL SAVE_REGISTERS ; overflow. ADDR CURRENT_CONTEXT_ID, D0 CLR.W C MOVE.B @D0, C MOVE.W C, R0 ; Push the ID of the process which CALL RESTORE_REGISTERS ; caused the error. CALL PUSH_R0_SHORT CALL SAVE_REGISTERS JUMP RR_RPLCONT ; Push the error code onto the stack and ; kill the program. FOUND_EMPTY: ; We have found an empty slot! D1 should be pointing to the ; empty slot, so all we have to do is fill it in. MOVE.B R2, A ; Get the process ID. MOVE.B A, @D1 ; Write the ID into the slot. ADD.A ^d2, D1 ; Point D1 at the execution time. MOVE.W R3, C ; Get the process execution time. MOVE.W C, @D1 ; Write the execution time into the slot. RET ; **************************************************************************** TO_SCHEDULER: ; This routine is used by processes to tell the system to swap back to the ; scheduler. This routine just restores the scheduler context and jumps to ; the scheduler. It is up to each process to save its own context. ADDR CURRENT_CONTEXT_ID, D0 CLR.W A MOVE.B A, @D0 ; Make scheduler the current context. CALL RESTORE_CONTEXT ; Restore the context of the scheduler. JUMP C ; Jump to the scheduler. ; **************************************************************************** SAVE_STATE: SWAP C, D0 ; Save D0 in C for now. Trashes C. ADDR STATE_DATA1, D0 ; Point to STATE_DATA. Trashes A. MOVE.W B, A ; MOVE.W A, @D0 ; Save B. ADD 16, D0 SWAP.W D, C MOVE.W C, A ; SWAP.W D, C MOVE.W A, @D0 ; Save D. ADD 16, D0 MOVE.A C, @D0 ; Save D0 (copied to C previously). ADD 5, D0 SWAP D1, C MOVE.A C, @D0 ; Save D1. SWAP D1, C ; Restore D1. SWAP C, D0 ; Restore D0. RETCLRC ; Clear carry bit and return. RESTORE_STATE: ADDR STATE_DATA1, D0 ; Point to STATE_DATA. Trashes A. MOVE.W C, A ; Save C in A for now. MOVE.W @D0, C ; MOVE.W C, B ; Restore B. ADD 16, D0 MOVE.W @D0, C ; Restore D. MOVE.W C, D ; Restore B. ADD 16, D0 MOVE.A @D0, C ; Restore D0 (in C for now). ADD 5, D0 SWAP.A D1, C ; Save D0 contents into D1 for now. MOVE.A @D0, C ; Get D1 contents into C. SWAP D1, C ; Restore D1. SWAP C, D0 ; Restore D0. MOVE.W A, C ; Preserve the value of C saved at top. RETCLRC ; Clear carry bit and return. GET_TICKS: CALL SAVE_STATE ; Save system registers. CALL ROM_GET_TICKS ; Get the ticks value from hardware timer. CALL RESTORE_STATE ; Restore system registers. Trashes A. RET ; Ticks is returned in C.13. GET_CURRENT_ID: ADDR CURRENT_CONTEXT_ID, C SWAP.A C, D0 ; Point D0 at CURRENT_CONTEXT_ID. MOVE.B @D0, A SWAP.A C, D0 ; Restore D0. RET GET_CUR_TIME: ADDR CUR_TIME, C SWAP.A C, D0 ; Point D0 at CUR_TIME. MOVE.B @D0, A SWAP.A C, D0 ; Restore D0. RET ; **************************************************************************** ; This is where the definition of the multiprocessing environment ends. ; **************************************************************************** ; ; **************************************************************************** ; **************************************************************************** ; THE PROCESS DEFINITIONS START HERE. ; The code below comprises the process definitions for 7 processes. Refer to ; the MPE users guide for an explanation of what each process does, and how ; the processes interact. ; PROCESS1_DATA: info: data.a ^xAAAAA ; This is a simple data pattern for process 1. data.a ^xAAAAA data.a ^xAAAAA data.1 ^xA ; This is broken up for users who lack GNU-C version ; of star. PROCESS1_INIT: process_start 1, process1_code ; reconfigures this process to start ; at the process1_code label from now on. ; This is process #1. This process will kick off the keyboard scanner process ; (process #7) and then this process will perform an ongoing register integrity ; test. This process basically implements a simple sanity check over the MPE ; system and halts execution and pushes an error code onto the stack if it ; detects a problem. All this process really does is check that MPE is doing ; the process context save/restore function properly. This process runs rather ; infrequently, so it does not add much overhead to the system. ADDR CUR_TIME, D0 MOVE.W @D0, C ; Put CUR_TIME into C. MOVE.P2 ^d7, A ; Process 7 ID. (keyboard scanner) CALL ADD_PROCESS ; schedule process 7 for execution. ; Now we need to set up the registers so that each register contains a unique ; value. Then these values will be checked each time this process is run. addr info, d0 move.w @d0, a ; Now A contains all As. move.w a, b inc.w a move.w a, c move.w c, d inc.w c move.a c, d0 inc.w c move.a c, d1 inc.w c move.w c, r0 inc.w c move.w c, r1 inc.w c move.w c, r2 inc.w c move.w c, r3 inc.w c move.w c, r4 PROCESS1_CODE: ; This is the point to where execution will jump when the process is run. The ; above initialization code is only executed the very first time this process ; is run. ADDR INFO, c ; Trashes A, and C obviously. SWAP.A C, D0 ; Point D0 at the data. MOVE.W @D0, A SWAP.A C, D0 ; Restore D0. MOVE.W A, C ; copy the data into c because only C can be compared ; with D. BREQ.W A, B, ok1 ; Test B MOVE.P5 ^D0, A ; Error code 0 for register B. JUMP ERROUT ; OK1: INC.W C BREQ.W D, C, OK2 ; Test D. MOVE.P5 ^X1, A ; Error code 1 for register D. FAILED. JUMP ERROUT ; OK2: INC.W A INC.W A SWAP.A C, D0 BREQ.A C, A, OK3 ; Test D0. MOVE.P5 ^X2, A ; Error code 2 for register D0. JUMP ERROUT ; OK3: INC.W A SWAP.A C, D0 ; Restore D0 from the previous test. SWAP.A C, D1 ; BREQ.A C, A, OK4 ; Test D1. MOVE.P5 ^X3, A ; Error code 3 for register D1. FAILED. JUMP ERROUT ; OK4: SWAP.A C, D1 ; Restore D1 from the previous test. INC.W A MOVE.W R0, C BREQ.A C, A, OK5 ; Test r0. MOVE.P5 ^X4, A ; Error code 4 for register r0. JUMP ERROUT ; OK5: INC.W A MOVE.W R1, C BREQ.A C, A, OK6 ; Test r1. MOVE.P5 ^X5, A ; Error code 5 for register r1. JUMP ERROUT ; OK6: INC.W A MOVE.W R2, C BREQ.A C, A, OK7 ; Test r2. MOVE.P5 ^X6, A ; Error code 6 for register r2. JUMP ERROUT ; OK7: INC.W A MOVE.W R3, C BREQ.A C, A, OK8 ; Test r3. MOVE.P5 ^X7, A ; Error code 7 for register r3. JUMP ERROUT ; OK8: INC.W A MOVE.W R4, C BREQ.A C, A, OK9 ; Test r4. MOVE.P5 ^X8, A ; Error code 8 for register r4. JUMP ERROUT ; OK9: CALL SAVE_CONTEXT ADDR CUR_TIME, d0 ; What time is it? MOVE.W @D0, C CLR.W A MOVE.P5 ^x1FFF, A ADD.W A, C ; MOVE.P2 ^d2, A ; Process 2 ID. CALL ADD_PROCESS ; schedule process 2 for execution in 1 sec. JUMP TO_SCHEDULER ERROUT: MOVE.A A, R0 ; Error code was placed in A above. CALL RESTORE_REgisters CALL PUSH_R0_SHort ; Push the error code for the bad register. CALL SAVE_REGISters JUMP RR_RPLCONT ; This concludes the definition of process #1. ; **************************************************************************** ; **************************************************************************** ; The definition of process #2 starts here. The purpose of this process ; is just to clear out all of the process registers. That way, if process ; #1 shows that the processor registers have the correct context for that ; process, then I know that the context save/restore by MPE was successfull. PROCESS2_INIT: CLR.W A MOVE.A A, D0 MOVE.A A, D1 MOVE.W A, B MOVE.W A, C MOVE.W C, D MOVE.W A, R0 MOVE.W A, R1 MOVE.W A, R2 MOVE.W A, R3 MOVE.W A, R4 CALL SAVE_CONTEXT ADDR CUR_TIME, d0 ; What time is it? MOVE.W @D0, C MOVE.P2 ^d1, A ; Process 1 ID. CALL ADD_PROCESS ; schedule process1 for immediate execution. JUMP TO_SCHEDULER ; This concludes the definition of process #2. ; **************************************************************************** ; **************************************************************************** ; The following process definition is for processes 3#, #4, and #5. The code ; differentiates between the three processes where necessary by using the ; IF_PROC and ENDIF_PROC macros, which are described in the users guide. process3_init: process4_init: process5_init: ; **************************************************************************** ; **************************************************************************** ; This is the beginning of the bullet process. The purpose of this ; process is to move an "F" through screen memory one pixel at a time. This ; process will move the "F" one pixel to the right each time it is called. ; At the bottom of this process, it checks to see whether it has reached the ; end of screen memory, and if not it reschedules itself for future ; execution based on the time inverval specified. This process definition ; differentiates between processes #3, #4, and #5 and reschedules these ; different processes at different intervals. This gives different speeds of ; motion for the three "bullets" on the screen. ; The RPL from which MPE was called clears the screen and tells the display ; controller to use PICT for the display (by doing a PVIEW command). ; First I need to modify the process header of this process so that execution ; will start at the PROCESS_CODE label in future invocations. addr process3_code, C ; Get address of new entry point for this ; process. cur_process_start ; Reconfigures the current process to start ; from the entry point given in C.A. MOVE.A PICT_POINTER, D0 ; Put the address of PICT in D0. MOVE.A @D0, C ; Get the pointer to PICT. ADD.A 10, C ; Skip over prolog and size fields. ADD.A 10, C ; Skip over row and column fields. MOVE.A C, D ; Save the address where the screen memory ; starts. MOVE.P5 ^d2174, A ; Number of nibbles - 2 in PICT. ADD.A A, C ; A = address of last-1 nibble in PICT. MOVE.A C, R2 ; Save it in R2. ; Now stick an "F" at the beginning of the screen memory. CLR.W A ; Clear register A. MOVE.B A, B ; Clear out the bottom byte of B (bit counter). MOVE.P1 ^xF, A ; Make A.A = F. MOVE.A D, C ; Get the pointer into PICT. MOVE.A C, D0 ; Point D0 at PICT. MOVE.1 A, @D0 ; Set the first nibble of the screen to be F. PROCESS3_CODE: PROCESS4_CODE: PROCESS5_CODE: ; Now what we want to do is to move the "F" one pixel at a time through the ; screen memory. I think I'll do it the easy way, by keeping track of the ; counter in B.B and just doing compares on that; there are only 4 cases to ; test. INC.1 B ; Increment the bit counter to the next value. MOVE.A D, C ; Get pointer into PICT. SWAP.A C, D1 ; Point D1 into PICT. CLR.B A ; Use A.B to test the current value of the bit ; counter. BREQ.1 A, B, ZERO ; INC.1 A BREQ.1 A, B, ONE INC.1 A BREQ.1 A, B, TWO THREE: MOVE.P1 ^x8, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x7, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. INC.A D ; Increment the PICT pointer. MOVE.P2 ^xF, C ; MOVE.B C, B ; So B resets to 0 on next iteration. JUMP DOIT ZERO: MOVE.A D, C ; Get the pointer into PICT. DEC.A C ; point to the nibble we just left. CLR.B A SWAP C, D1 ; Point D1 to the old PICT nibble, MOVE.1 A, @D1 ; and clear it. SWAP C, D1 ; Restore the PICT pointer in D1. MOVE.P1 ^xF, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x0, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. JUMP DOIT ONE: MOVE.P1 ^xE, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x1, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. JUMP DOIT TWO: MOVE.P1 ^xC, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x3, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. DOIT: MOVE.A R2, C ; Get the address of the last nibble in PICT. BRLT.A D, C, CONTINUE ; If we havent written the last location, then ; we should reschedule this process for future ; execution. JUMP rr_rplcont ; Else, we can just exit. CONTINUE: CALL SAVE_CONTEXT ; Save the context for this process. ADDR CUR_TIME, D0 ; Get the current run-time. MOVE.W @D0, C MOVE.W C, R0 ; Save it in R0 for now. IF_PROC 3 CLR.W A MOVE.P5 ^x11F, A ; Reschedule interval for process 3. JUMP PROCESS3_CONT2 ENDIF_PROC IF_PROC 4 CLR.W A MOVE.P5 ^x9F, A ; Reschedule interval for process 4. JUMP PROCESS3_CONT2 ENDIF_PROC IF_PROC 5 CLR.W A MOVE.P5 ^xDF, A ; Reschedule interval for process 5. JUMP PROCESS3_CONT2 ENDIF_PROC PROCESS3_CONT2: MOVE.W R0, C ; The current run-time. ADD.W A, C ; Next time that the hyphen should move. MOVE.W C, R0 ; Save it in R0. CALL GET_CURRENT_ID ; Get process ID into A.B. Trashes C. MOVE.W R0, C ; Next time process should be run. CALL ADD_PROCESS ; Schedule this process to run at the ; time given in C.A. JUMP TO_SCHEDULER ; Go back to the scheduler. ; The process definition for processes #3, #4, and #5 ends here. ; ************************************************************************* ; ************************************************************************* ; The definition of process #6 starts here. This process is very similar to ; the above definition for processes #3, #4, and #5. Like the above ; definition, process #6 will also move a "bullet" through screen memory. The ; difference is that this process will schedule itself based on absolute time, ; using the hardware timer to keep track of where the "bullet" should be on the ; screen, and then catching itself up to that point. In this manner, this ; process will not slow down as the system becomes heavily loaded, but rather ; it will use a constant amount of CPU time on the average, regardless of how ; many processes are running on the system. PROCESS6_INIT: process_start 6, PROCESS6_CODE MOVE.A PICT_POINTER, D0 ; Put the address of PICT in D0. MOVE.A @D0, C ; Get the pointer to PICT. ADD.A 10, C ; Skip over prolog and size fields. ADD.A 10, C ; Skip over row and column fields. MOVE.A C, D ; Save the address where the screen memory ; starts. MOVE.P5 ^d2174, A ; Number of nibbles - 2 in PICT. ADD.A A, C ; A = address of last-1 nibble in PICT. MOVE.A C, R2 ; Save it in R2. CLR.W A MOVE.W A, R0 ADDR CUR_TIME, D0 MOVE.W @D0, C MOVE.W C, R0 ; Save time of first invocation of process. ; Now stick an "F" at the beginning of the screen memory. CLR.W A ; Clear register A. MOVE.B A, B ; Clear out the bottom byte of B (bit counter). MOVE.P1 ^xF, A ; Make A.A = F. MOVE.A D, C ; Get the pointer into PICT. MOVE.A C, D0 ; Point D0 at PICT. MOVE.1 A, @D0 ; Set the first nibble of the screen to be F. PROCESS6_CODE: ; Now what we want to do is to move the "F" one pixel at a time through the ; screen memory. I think I'll do it the easy way, by keeping track of the ; counter in B.B and just doing compares on that; there are only 4 cases to ; test. PROCESS6_LOOP: INC.1 B ; Increment the bit counter to the next value. MOVE.A D, C ; Get pointer into PICT. SWAP.A C, D1 ; Point D1 into PICT. CLR.B A ; Use A.B to test the current value of the bit ; counter. BREQ.1 A, B, process6_ZERO ; INC.1 A BREQ.1 A, B, process6_ONE INC.1 A BREQ.1 A, B, process6_TWO PROCESS6_THREE: MOVE.P1 ^x8, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x7, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. INC.A D ; Increment the PICT pointer. MOVE.P2 ^xF, C ; MOVE.B C, B ; So B resets to 0 on next iteration. JUMP process6_DOIT PROCESS6_ZERO: MOVE.A D, C ; Get the pointer into PICT. DEC.A C ; point to the nibble we just left. CLR.B A SWAP C, D1 ; Point D1 to the old PICT nibble, MOVE.1 A, @D1 ; and clear it. SWAP C, D1 ; Restore the PICT pointer in D1. MOVE.P1 ^xF, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x0, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. JUMP process6_DOIT PROCESS6_ONE: MOVE.P1 ^xE, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x1, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. JUMP process6_DOIT PROCESS6_TWO: MOVE.P1 ^xC, A ; Put New bit pattern of nibble in A. INC.1 P ; MOVE.P1 ^x3, A ; Put new bit pattern of nibble+1 in A. MOVE.1 0, P ; Zero out the Pointer register. MOVE.2 A, @D1 ; Write out the two data values to the display. PROCESS6_DOIT: MOVE.A R2, C ; Get the address of the last nibble in PICT. BRLT.A D, C, PROCESS6_CONTINUE ; If we havent written the last location, then ; we should reschedule this process for future ; execution. JUMP rr_rplcont ; Else, we can just exit. PROCESS6_CONTINUE: CLR.W A MOVE.P5 ^x9F, A MOVE.W R0, C ; The last scheduled run-time. ADD.W C, A ; Next time that the hyphen should move. MOVE.W A, R0 ; Save next run-time. CALL GET_TICKS ; Current time. MOVE.W R0, A ; Get next run-time. BRGE.W C, A, PROCESS6_LOOPER ; Run it again if it is time. CALL SAVE_CONTEXT ; Save the context for this process. MOVE.W R0, C ; Next time process should be run. CLR.W A MOVE.P2 ^d6, A ; ID for this process into A.B. CALL ADD_PROCESS ; Schedule this process to run at the ; time given in C. JUMP TO_SCHEDULER ; Go back to the scheduler. PROCESS6_LOOPER: JUMP PROCESS6_LOOP ;************************************************************************** ;************************************************************************** ; This process is responsible for reading the keyboad, and spawning a new ; processes whenever the [1] key is pressed. All other key presses are ; discarded without action. ; This process will schedule processes #3, #4, #5, and #6 in that sequence. ; One processes will be spawned for each press of the [1] key. This process ; does one scan of the key buffer on each invocation. After process #6 is ; scheduled, this process ceases to reschedule. PROCESS7_INIT: process_start 7, process7_code move.p2 ^d2, C ; Pointer to the next-1 bullet process. MOVE.B C, B ; Save it in B. MOVE.P2 ^d6, C ; Upper process ID limit. MOVE.B C, D ; Save it in D. PROCESS7_CODE: call kb_poll ; Get key, if any brcs process7_check_key CALL SAVE_CONTEXT jump process7_sched ; If not then reschedule. PROCESS7_CHECK_KEY: move.p2 ^d49, C ; Scan code for the [+] key. breq.b c, a, process7_valid_key ; Did user press [+]? MOVE.P2 ^d47, C BREQ.B C, A, PROCESS7_ABORT CALL SAVE_CONTEXT jump process7_sched ; If not then reschedule. PROCESS7_ABORT: ; The user has pressed the [.] key, so we want to abort the application. JUMP RR_RPLCONT ; This kills the application and continues ; with the RPL thread. PROCESS7_VALID_KEY: ; The user pressed the [+] key! INC.B B ; Point to the next bullet ID. MOVE.B B, A ; Next process ID to start. MOVE.B A, R0 ; Save it in R0 for later (see below). MOVE.B D, C ; Upper process ID limit. BRGE.B C, A, PROCESS7_SPAWN ; If process number <=6 then run it. DEC.B B ; Else un-increment B, CALL SAVE_CONTEXT ; save the process context and JUMP PROCESS7_SCHED ; reschedule the process. PROCESS7_SPAWN: CALL SAVE_CONTEXT ADDR CUR_TIME, D0 ; Trashes A. MOVE.B R0, A MOVE.W @D0, C CALL ADD_PROCESS PROCESS7_SCHED: ; The current context of this process should have been saved by now. ADDR CUR_TIME, D0 ; Point to the CUR_TIME variable. MOVE.W @D0, C ; Get the current time. CLR.W A MOVE.P3 ^x3FF, A ; Reschedule this process to look at the ; keyboard roughly 8 times per second. ADD.W A, C MOVE.P2 ^d7, A ; ID for this process. CALL ADD_PROCESS PROCESS7_EXIT: JUMP TO_SCHEDULER ; Return control to the scheduler. ;; Poll keyboard. System-based version. Returns the scan code in A.A. ;; Trashes C.A, B.B, and D0. The C bit is set if a key was pressed, ;; otherwise it's cleared. This keyboard polling software was written ;; by Jan Brittenson (J.E.) ;; ;; This is a mutation of the ROM prefixed machine code routine to ;; get/peek the next entry in the keyboard buffer. kb_poll: move.5 keybuf+1, d0 ; KB Put ptr move.s @d0, a ; A.S = put ctr dec d0 move.s @d0, c ; C.S = get ctr breq.s c, a, $100 ; Ctrs are equal - buffer empty move c.15, p ; P = get ctr inc.s c ; Remove key move.s c, @d0 swap c, d0 add p+1, c add p+1, c ; C += get ctr, in bytes clr p move c, d0 ; D0 = &next key clr.a a move.b @d0, a ; A.A = key retsetc $100: clr.a a retclrc ; ************************************************************************** endcode end