.PRINTX * RTX.MUB 18/2/91 * .COMMENT \ A SIMPLE REAL-TIME EXECUTIVE (RTX). 1. Supports any number of tasks (limited to 255 in some places). 2. Total timer interrupt task switch time is less than 60us (Z180/6MHz). 3. Each task must have its own stack. MOD RECORD 29/8/90 Moved from MB II. 29/1/91 rtx_switch entry modded to prevent HL corruption. 4/2/91 rtx_switch reloads rtx_timer, to prevent a premature next tick 7/2/91 Calc_space task changed to 'onesec'. 18/2/91 'get_task_status' routine added. The rtx_switch does NOT reload rtx_timer if rtx is disabled (rtx_timer=255). The RTX has five entry points: 1. Initialisation. Entered (with JP) just once after power-up, to set-up initial PC and SP values. This entry kicks-off the RTX and never returns. 2. Timer interrupt (timer tick). This is the normal entry point. At each timer tick, the RTX switches to the next task. 3. Switch to next task. This entry point can be used by the currently executing task to ask the RTX to switch to the next task, e.g. if the task has nothing more to do. On the Z180, this entry point is the same as the timer tick entry point, because the timer int pushes the same things on the stack as a CALL does. On the MUB, this entry is implemented as RST 38h, for compact code. 4. Suspend a task (until re-activated). This 'kills' a task. The RTX will ignore that task, until the task is re-activated with the function below. 5. Re-activate a (suspended) task. This re-activates a (previously suspended) task. For each task, the RTX maintains the data area below: offset contents initial value +0 af from table +2 bc from table +4 de from table +6 hl from table +8 ix from table +10 iy from table +12 sp task's sp (*before* the timer interrupt) +14 pc task's entry point +16 msr task's MSR value (used with Z280 only) +18 status 0: task active, 1: task suspended +19 reserved The RTX uses push/pop instructions to save/reload registers because these are probably the fastest way. The RTX loads iopage to 00h (as part of clearing the timer 0 pending int) - this is OK because throughout the MUB ints are OFF whenever iopage<>0 and the RTX should therefore never tick with iopage<>0. Each task's MSR is preserved, although currently all tasks run with 'ei all_ints' and preserving MSR is not really necessary; it could just be forced to 'all_ints' when each task is entered. Z180 ==== If you want to use this RTX for a Z180, you might prefer to use the slightly more recent version used in the IPC/180. This supports different BBR value for each task. \ global REAL_RTX_SWITCH global RTX_ACTIVATE_ENTRY global RTX_GET_TASK_STATUS global RTX_INIT_ENTRY global RTX_SUSPEND_ENTRY global RTX_TIMER_INT_ENTRY ; code (all these are RTX tasks) external BIG_REQUESTS external FRONT_PANEL external LANC_OPERATIONS external MAIN_DATA_LOOP external NEW_LINKMAPS external ONESEC external PROCESS_IPC_BUFS external PROCESS_OP_TIMEOUTS external PROCESS_OUT_RCBUFS external PROCESS_REM_PLOFN external SC_EAROM_UPDATE external SETUP_MODE ; data external HL_SAVE external PC_SAVE external RTX_ALL_SUSPENDED external RTX_CURRENT_TASK external RTX_DATA_AREAS external RTX_DATA_PTR external RTX_TIMER external MSR_SAVE external SP_SAVE external STACK_START INCLUDE DEFS.MUB SECTION CODE,X MUB EQU TRUE ; RTX initialisation table. Terminated by a line with PC=0. ; In the future, this should be modded to hold ix,iy,sp,pc,msr values only. ; All tasks have equal priority and execute on a round-robin basis. ; ** The task *order* (i.e. their #s) is assumed by calls to rtx_activate & rtx_suspend ** ; ** THEREFORE: ADD NEW TASKS ONLY AT THE *END* OF THE TABLE ** ; ** Dont forget to EQUate rtx_tasks in defs.mub to #tasks below (incl PC=0 task) ** RTX_INITIAL_REGS: ; task# v ; v ; AF BC DE HL IX IY SP PC MSR stat+res v DW 0 0 0 0 0 0 STACK_START+(1*STACK_SIZE) MAIN_DATA_LOOP ALL_INTS 0 ; 0 DW 0 0 0 0 0 0 STACK_START+(2*STACK_SIZE) LANC_OPERATIONS ALL_INTS 0 ; 1 DW 0 0 0 0 0 0 STACK_START+(3*STACK_SIZE) BIG_REQUESTS ALL_INTS 0 ; 2 DW 0 0 0 0 0 0 STACK_START+(4*STACK_SIZE) SC_EAROM_UPDATE ALL_INTS 0001H ; 3* DW 0 0 0 0 0 0 STACK_START+(5*STACK_SIZE) NEW_LINKMAPS ALL_INTS 0001H ; 4* DW 0 0 0 0 0 0 STACK_START+(6*STACK_SIZE) SETUP_MODE ALL_INTS 0 ; 5 DW 0 0 0 0 0 0 STACK_START+(7*STACK_SIZE) ONESEC ALL_INTS 0001H ; 6* DW 0 0 0 0 0 0 STACK_START+(8*STACK_SIZE) FRONT_PANEL ALL_INTS 0001H ; 7* DW 0 0 0 0 0 0 STACK_START+(9*STACK_SIZE) PROCESS_OP_TIMEOUTS ALL_INTS 0001H ; 8* DW 0 0 0 0 0 0 STACK_START+(10*STACK_SIZE) PROCESS_OUT_RCBUFS ALL_INTS 0001H ; 9* DW 0 0 0 0 0 0 STACK_START+(11*STACK_SIZE) PROCESS_IPC_BUFS ALL_INTS 0001H ;10* DW 0 0 0 0 0 0 STACK_START+(12*STACK_SIZE) PROCESS_REM_PLOFN ALL_INTS 0001H ;11* DW 0 0 0 0 0 0 0 0 0 0 ; (*) the above tasks such marked run only once and suspend themselves, and are ; re-activated by some event. The '0001H' value sets status=01h; this makes ; the task *initially* suspended (e.g. we *dont* want to update the eeprom at ; every power-up, do we?). ; RTX initialisation/entry point. RTX_INIT_ENTRY: DI ; Necessary ! (in case of entry with EI) ; Copy initial register values from ROM into RAM. LD HL, RTX_INITIAL_REGS LD DE, RTX_DATA_AREAS LD BC, RTX_TASKS*RTX_DATA_SIZE LDIR ; Reset ptr (IX) to base+20 of the first task LDW (RTX_DATA_PTR), RTX_DATA_AREAS+RTX_DATA_SIZE LD (RTX_TIMER), RTX_TC ; RTX downcounter time constant LD (RTX_CURRENT_TASK),0 ; Initial task # := 0 ; Enable timer interrupt IF MUB ; (no action necessary because timer 0 int is already always enabled ; at the timer, and rtx1st does an EI etc) ELSE IN0A TCR SET 5, A ; Timer 1 ints (RTX) ON OUT0A TCR ENDIF ; Set-up SP to 1st POP task's initial values and jump to 1st task LD SP, RTX_DATA_AREAS JP RTX1ST ; (also does EI) ; This is the timer interrupt service routine which switches tasks. ; At entry here (and at exit) rtx_data_ptr points to the 1st byte after the ; task's data area (i.e. byte @base+20). Therefore, when we load SP from ; rtx_data_ptr and start PUSHing the registers, the first word pushed (MSR) ; is written in the right place. Rtx_data_ptr does NOT point at base+0. RTX_TIMER_INT_ENTRY: ; First, preserve the regs which we are going to corrupt, etc, while ; adjusting SP back to its value *before* the timer interrupt. ; All this must be done with instructions which dont modify flags. ; We get here from ct0 interrupt, with AF'. IF MUB LD (RTX_TIMER), RTX_TC ; Reload the down-counter EXX IOPAGE 0FEH ; Clear CC bit (reset 'int pending') LD A, 11100000B ; (this could be done *after* the RTX code OUT (0E1H), A ; but for some reason doing it there did not IOPAGE 0 ; work properly; it does not really matter) EXX EX AF, AF' ; and switch back to main AF INC SP ; Skip over Mode 3 Int Reason Code INC SP ; (always the same - boring) LD (HL_SAVE), HL ; Save HL of interrupted task POP (MSR_SAVE) ; Save MSR of interrupted task ; This entry point is used by rtx_switch, which has already loaded ; HL and MSR into hl_save and msr_save, and disabled all interrupts. RTX_ES: POP HL ; HL := PC of interrupted task LD (SP_SAVE), SP ; Save SP (the value *before* the timer int) ELSE LD (HL_SAVE), HL ; Save HL of interrupted task POP HL ; HL := PC of interrupted task LD (SP_SAVE), SP ; Save SP of interrupted task ENDIF ; HL now holds the interrupted task's PC value. ; We save the interrupted task's registers by a series of PUSHes. LD SP, (RTX_DATA_PTR) ; Set SP to interrupted task's data area IF MUB DEC SP ; Skip over status+reserved bytes DEC SP PUSH (MSR_SAVE) ; Save MSR PUSH HL ; Save PC PUSH (SP_SAVE) ; Save SP (the value *before* this interrupt) PUSH IY ; Save IY PUSH IX ; Save IX PUSH (HL_SAVE) ; Save HL ELSE DEC SP ; Skip over status+reserved bytes DEC SP DEC SP ; Z180: 'push' a dummy value instead of MSR DEC SP PUSH HL ; Save PC LD HL, (SP_SAVE) PUSH HL ; Save SP PUSH IY ; Save IY PUSH IX ; Save IX LD HL, (HL_SAVE) PUSH HL ; Save HL ENDIF PUSH DE ; Save DE PUSH BC ; Save BC PUSH AF ; Save AF ; Interrupted task is now saved. Go to next task. ; If next task's PC=0, this is the end of the task table and we wrap. LD B, RTX_TASKS ; Max # of tasks to go through RTX_NX: LD IX, (RTX_DATA_PTR) LD SP, IX ; This loads SP just right for the POPs below LD DE, RTX_DATA_SIZE ADD IX, DE ; Move rtx_data_ptr to next task's data area LD HL, RTX_CURRENT_TASK INC (HL) ; Current task # ++ IF MUB LD DE, (IX-6) ; Check if next task's PC=0 LD A, D OR E ELSE LD A, (IX-6) OR A, (IX-5) ENDIF JR NZ, RTX_01 ; NZ: no, continue LD IX, RTX_DATA_AREAS+RTX_DATA_SIZE ; else reset ptr to 1st task LD SP, RTX_DATA_AREAS ; and set SP to start POPping for 1st task LD (HL), 0 ; and reset 'current task #' RTX_01: LD (RTX_DATA_PTR), IX LD A, (IX-2) ; Now check that the newly-selected task is OR A ; not suspended. JR Z, RTX1ST ; Z: not suspended - continue DJNZ RTX_NX ; else loop through all the tasks in the table ; (If all tasks are suspended, we fall out here and the next task gets ; executed anyway, which does not really matter unless one actually ; relies on suspended tasks to *never* execute. Doing this properly ; is more complicated). LD A, 1 ; Mark 'all suspended' condition detected LD (RTX_ALL_SUSPENDED), A ; Load registers for next task. ; This is also the entry point for starting the RTX (enter with ; SP = base of 1st task's data area) RTX1ST: POP AF ; Load AF POP BC ; Load BC POP DE ; Load DE IF MUB POP (HL_SAVE) ; Recover HL POP IX ; Load IX POP IY ; Load IY POP (SP_SAVE) ; Recover SP POP (PC_SAVE) ; Recover PC POP HL ; Recover MSR ELSE POP HL LD (HL_SAVE), HL POP IX POP IY POP HL LD (SP_SAVE), HL POP HL LD (PC_SAVE), HL ENDIF LD SP, (SP_SAVE) ; Load SP ; Clear the pending interrupt, enable ints and enter the new task. IF MUB PUSH (PC_SAVE) ; Put PC on stack (for RETIL to pop-off) PUSH HL ; Likewise with MSR LD HL, (HL_SAVE) ; Load HL RETIL ; Enter new task ELSE LD HL, (PC_SAVE) PUSH HL ; Put PC on stack (for RET to pop-off) LD HL, (HL_SAVE) ; Load HL EX AF, AF' IN0A TCR ; Clear the pending timer 1 interrupt bit IN0A TMDR1L EX AF, AF' EI ; Re-enable interrupts RET ; Enter new task ENDIF ; CALL this entry point to cause a switch to the next task. Done via RST 38H. ; This entry also reloads the RTX tick down-counter, so that the next RTX tick ; cannot occur until after a whole tick period. REAL_RTX_SWITCH: IF MUB DI ; Prevent 'normal' RTX tick coming in here PUSH AF LD A, (RTX_TIMER) ; If rtx is NOT disabled, INC A JR Z, RRS_NL LD (RTX_TIMER), RTX_TC ; then reload its down-counter RRS_NL: PUSH BC LD (HL_SAVE), HL ; Save HL straight into RTX's HL save location LD C, 0 LDCTL HL, (C) ; Read current MSR LD A, L OR ALL_INTS ; Correct for 'di' above having cleared the EI bits LD L, A LD (MSR_SAVE), HL ; Save MSR straight into RTX's MSR save location POP BC POP AF JP RTX_ES ; Do (nearly) as if a timer tick occured ELSE DI EX AF, AF' LD A, RTX_TC ; Reload RTX downcounter time constant LD (RTX_TIMER), A EX AF, AF' JP RTX_TIMER_INT_ENTRY ; Do as if timer tick occurred ENDIF ; CALL this entry point to suspend a task. ; Enter with E = task # (0..254). ; If E=255, then the task currently executing is suspended. This feature ; is useful if a task does not know its own task # (i.e. 'suspend me'). ; ALSO, READ THE LARGE COMMENT BELOW. It effectively means that if a routine ; wants to suspend itself, it must use the 'task#=255' call, NOT a 'task=own#' ; call !! ; Kills DE,HL. RTX_SUSPEND_ENTRY: LD D, E ; Make a copy of task# INC E ; If E=0FFh JR NZ, RSE_05 LD A, (RTX_CURRENT_TASK) ; then use the 'current' task # instead LD E, A INC E RSE_05: DEC E IF MUB LD A, RTX_DATA_SIZE ; Index to the task's data area MULTU A, E ADDW HL, RTX_DATA_AREAS+18 ELSE PUSH DE LD D, RTX_DATA_SIZE MULDE LD HL, RTX_DATA_AREAS+18 ADD HL, DE POP DE ENDIF LD (HL), 1 ; and mark it 'suspended' ; If entry task# = 255, then we have a 'suspend me' call, and the ; task is suspended IMMEDIATELY. Normally, this is what would be ; really required. However, if task#<>255, then we have a case where ; one task (or int routine, etc) is suspending another task, and we ; do NOT then perform a rtx_switch. Doing an rtx_switch makes sense ; only on a 'suspend me' request, not on a 'suspend task n' request. INC D CALL Z, REAL_RTX_SWITCH ; and switch immediately to next task RET ; Return via here when task is re-activated ; CALL this entry point to re-activate a task. ; Enter with E = task # (0..). ; Kills AF,HL (+DE on Z180). RTX_ACTIVATE_ENTRY: IF MUB LD A, RTX_DATA_SIZE ; Index to the task's data area MULTU A, E ADDW HL, RTX_DATA_AREAS+18 ELSE LD D, RTX_DATA_SIZE MULDE LD HL, RTX_DATA_AREAS+18 ADD HL, DE ENDIF LD (HL), 0 ; and mark it 'active' RET ; CALL this entry point to find out whether a task is currently activated (not ; suspended). ; Enter with E = task # (0..). ; Returns A=00 if activated, 01 if suspended. ; Kills AF,HL (+DE on Z180). RTX_GET_TASK_STATUS: IF MUB LD A, RTX_DATA_SIZE ; Index to the task's data area MULTU A, E ADDW HL, RTX_DATA_AREAS+18 ELSE LD D, RTX_DATA_SIZE MULDE LD HL, RTX_DATA_AREAS+18 ADD HL, DE ENDIF LD A, (HL) ; and get the 'status' byte RET END