;----------------------------------------------------------------- ; MTX - a simple multitasking kernel for 68HC11 ; ; Written by Ganesh Gopalakrishnan (ganesh@cs) as part of his ; CS 563 ("Microprocessor Lab") course at University of Utah, in Feb 1995, ; to illustrate some of the principles behind device programming, ; multitasking, and the use of data abstraction. ; ; The code has been tested somewhat, but no guarantees are expressed ; or implied. This was basically my "background" activity over a few ; weeks amidst my "other work", and I was also learning about the 6811 ; concurrently. It was very educational trying to write a multitasking ; kernel for a processor I was learning about, and trying to get it working. ; Some of my coding practices are recommended - others aren't!! Hopefully ; you will find the code modular and well documented inside, even if not ; that pretty or efficient. I might also have gone overboard saving registers. ; ; Suggestions for improvements and enhancements will be greatly appreciated! ; The code has been run under the P&E simulator SIM11E. A simple script file ; containing the lines "a 0", "b 0", "ix 0", "iy 0", "pc mtxorg", ; "sciwindow", and "go" will get MTX running under this simulator (registers ; are initialized only to "please" the simulator, as it doesn't like to ; stack un-initialized registers. Testing on an "E2" chip is pending because ; I don't have an "E2" based BOTboard yet. However anything running on a ; P&E SIM11E has, without a problem, run on the real hardware ... so far, at ; least. I used the RCASM11 assembler, also produced by P&E. These are good ; assemblers and simulators for the price. (Details about P&E are in the ; 6811 FAQs. They can give you access to demo versions.) ; ; Three tasks are hardwired: null task, which outputs ".", ; task1 which gets a char, adds one to it, and outputs the next char, ; and task2 which keeps outputting an "A". The baud rate is set to 9600. ; ; Features: ; ; Fits in under 900 bytes - so can really put in the EEPROM of an E2 ; chip, with room to spare. ; ; Each task must fit in 32 bytes. ; ; Two system-calls supported: getch and putch ; ; Known bugs: ; ; getch() is not fairly handled - so if two tasks fight for a char via ; a getch system call, the one coming first in the round-robin order ; always gets it. ; ; Sometimes when I type a character (e.g. "1"), I see a "B" printed out. ; I suspect this is because of the way SIM11E simulates a virtual TTY via ; the "sciwindow" feature - but never had time to confirm this by running ; MTX on "real" hardware. I think I get a "B" because task1 somehow thinks ; it read an "A" and incremented it to become a "B". In reality, an "A" ; is continually emitted by task2 - could task1 somehow be fooled into ; thinking that the last "A" output by task2 is an input addressed to task1? ; I don't know. I don't see any "bugs" in my code (to the extent I've studied ; it) that will explain this. ;----------------------------------------------------------------- ; Version where null task outputs "." and baud rate is 9600 pagewidth 132t ;******************************** MTX **************************** MTXORG EQU $F800 ; for an E series chip, the 2K EEPROM ; starts at F800 PTSize EQU $07 ; three processes to begin with ; 07 in unary is 00000111 which has 3 bits on ; This includes the NULL process also BPTSize EQU $03 ; BPTSize is the process-table size ; (PTSize) in Binary. TaskSize EQU $20 ; Size of data+stack of a task StkFrameSize EQU $9 ; Size of a 6811 stack frame InitSPOff EQU (TaskSize-StkFrameSize) ; works out to be $17 (23 dec.) KQbegin EQU $0000 ; start keyboard buffer from location 0 ; This is where allocation in MTX begins KQSize EQU $8 ; 8 characters can be buffered DQSize EQU $8 ; 8 characters can be buffered MTXSP EQU $00C0 ; The SP of MTX - a location before ; the pseudo-vectors begin ;----------------------------------------------------------------- $MACRO move_x_to_y PSHX PULY $MACROEND $MACRO move_s_to_y TSY DEY ; TSY copies SP+1 to IY - so correct it! $MACROEND $MACRO move_s_to_x TSX DEX ; TSX copies SP+1 to IX - so correct it! $MACROEND $MACRO move_x_to_s INX TXS ; TXS copies IX - 1 into SP - so correct it! $MACROEND $MACRO point_ix_to_dispq LDX #KQbegin LDX 0,X ; first entry of KBDQ is pointer to DISPQ $MACROEND $MACRO point_ix_to_pt LDX #KQbegin LDX 0,X ; first entry of KBDQ is pointer to DISPQ LDX 0,X ; first entry of DISPQ is pointer to PT $MACROEND ;----------------------------------------------------------------- ORG MTXORG ;----------------------------------------------------------------- mtx equ * ; Disable interrupts SEI ; Set MTX's SP at 00c0 - should be OK even for an A1 chip LDS #MTXSP ; Create the keyboard buffer of size KQSize at KQbegin LDX #KQbegin LDAB #KQSize JSR createq ; IX ; | ; v ; --------- ; | KQ | ; --------- ; ; Now get the next free location beyond the keyboard queue - it's easy in our ; design: the pointer for that free location is the first word of the keyboard ; queue; The following instruction accomplishes the task! LDX 0,X ; IX ; | ; v ; --------- ; | KQ | ; --------- ; ; Create the display buffer at this address of size DQSize LDAB #DQSize JSR createq ; IX ; | ; v ; ----------- ; | KQ | DQ | ; ----------- ; Again walk the link and get the free-space beyond the display queue LDX 0,X ; IX ; | ; v ; ------------- ; | KQ | DQ | ; ------------- ; Now, create a process-table of size PTSize at this address, via "creatept" ; ; creatept needs to know where to put the saved SP values ; of the various tasks. The following picture shows what we want to achieve... ; IX ; | PTESiz ; v <----> ; ---------------------------------------------------------------------------- ; | | | | | | | | | | ; | KQ | DQ | PT header stuff | PTE1 | PTE2 | ... | PTEn | Null-task | Task1| ; | | | | | | | | data stack| d s| ; ---------------------------------------------------------------------------- ; |--- PTE1Off -------> |<----->| ; ; InitSPOff ; ; |-- (PTE1Off + BPTSize * PTESiz) + InitSPOff ---------> ; ; In the above, "n" is the same as BPTSize ; ; As per this picture, we need to deposit the SP for the null task at ; ; IX + (PTE1Off + BPTSize * PTESiz) + InitSPOff ; ; Let's compute this value in IY and pass IY as a parameter to creatept ; To do the above, first transfer IX to IY move_x_to_y ; Then load ((PTE1Off + BPTSize * PTESiz) + InitSPOff) - 1 into ACCB ; The "-1" is to make the initial SP point one above the place where ; CCR is stored so that "RTI" works OK... LDAB #(((PTE1Off+(BPTSize*PTESiz))+InitSPOff)-1) ; Now add ACCB to IY so that IY points to the stack-top of the null task ABY ; Now load in ACCA the size of the process table LDAA #PTSize ; And now create a process table jsr creatept ; Walk the link in the process table and get the free-space beyond it ; I.e. we want to achieve ; ; IX ; | ; v ; ---------------------------------------------------------------------------- ; | | | | | | | | | | ; | KQ | DQ | PT header stuff | PTE1 | PTE2 | ... | PTEn | Null-task | Task1| ; | | | | | | | | data stack| d s| ; ---------------------------------------------------------------------------- LDX 0,X ; Where IX now points to is where the task areas (data + stack) will begin. ; Now make IX point to where the top of the initial system stack ; will be - this will be 22 bytes away from where IX is now. LDAB #(InitSPOff-1) ABX ; IX now looking one before CCR (top item in stack) of a stack ; ; IX ; | ; v ; ---------------------------------------------------------------------------- ; | | | | | | | | | | ; | KQ | DQ | PT header stuff | PTE1 | PTE2 | ... | PTEn | Null-task | Task1| ; | | | | | | | | data stack| d s| ; ---------------------------------------------------------------------------- ; We will need this IX later to initialize the SP - so save it PSHX ;----------------------------------------------------------------- ; For each task, do the follg. ;----------------------------------------------------------------- ; 1. Null task ; Here, build the null-task stack - the PC to be set ; in this stack is passed via IY LDY #nulltask JSR make_init_stk ; Now load ACCB with the task size - to step thru the remaining tasks LDAB #TaskSize ;----------------------------------------------------------------- ; 1. Task1 ; Make IX point to Task1's stack top ABX ; IX ; | ; v ; ---------------------------------------------------------------------------- ; | | | | | | | | | | ; | KQ | DQ | PT header stuff | PTE1 | PTE2 | ... | PTEn | Null-task | Task1| ; | | | | | | | | data stack| d s| ; ---------------------------------------------------------------------------- LDY #task1 JSR make_init_stk ;----------------------------------------------------------------- ; 2. Task2 ; Make IX point to Task2's stack top - similar to previous situation ABX LDY #task2 JSR make_init_stk ; OK, get back the saved IX, and make it the initial SP ; value by transferring it to SP. PULX move_x_to_s ; Will replace this code with a loop later on... ;----------------------------------------------------------------- ; Initialize the SCI subsystem JSR init_sci ; Initialize the RTI subsystem JSR init_rti ; Initialize the SWI subsystem (patch pseudo-vectors etc.) JSR init_swi ; Pretend we had been interrupted out of the null task - so go back to it ; via an RTI!!! Interrupts will be allowed once inside that task because ; we will be loading a CCR with the I bit cleared. roll: RTI ;***************************************************************** ;----------------------------------------------------------------- ; The null task nulltask EQU * LDX #REGBASE BRCLR JSCSR,X TDRE * ; wait for tdre LDAA #$2E ; ASCII code for "." STAA SCDR BRA nulltask ;----------------------------------------------------------------- ; Task1 TASK1 EQU * SWI FCB GETCH_SWI INCA SWI FCB PUTCH_SWI BRA TASK1 ;----------------------------------------------------------------- ; Task2 task2 EQU * LDAA #$41 SWI FCB PUTCH_SWI BRA TASK2 ;----------------------------------------------------------------- ; init_swi SWIPSV EQU $00F4 init_swi EQU * ; patch pseudo vector for swi interrupt LDAA #JMPOP STAA SWIPSV ; sci pseudo-vector opcode being patched LDD #SWIINTR STD SWIPSV+1 ; Done programming the SCI subsystem RTS ;----------------------------------------------------------------- ; SWIINTR ; ; Handle system-calls here ; ; System-calls handled are: ; ; SWI GETCH_SWI - getch() ; SWI PUTCH_SWI - putch(ch) ; GETCH_SWI EQU 0 PUTCH_SWI EQU 1 ; Others to be added later ; SWIINTR EQU * ; Copy current SP into IX - SP pointing to stack frame of SWI interrupt move_s_to_x ; Save this IX PSHX ; Bump saved PC in stack - also returns IY pointing at the SWI opcode JSR bump_pc_in_stk ; Test what SWI it is LDAB 0,Y CMPB #GETCH_SWI BNE must_be_putch ; Handle getch() ; Find out if there is a character to be had i.e. if KBDQ is empty LDX #KQbegin JSR isemptyq ; carry set => queue empty - otherwise nonempty BCS need_to_block_getch ; There is a char to be had - dequeue it, put it into the stack frame & RTI JSR dequeue ; Restore IX to point to SWI interrupt stack frame PULX ; Stick the returned character into the stack frame JSR put_a_in_stk ; That's all! Return back to the broken task RTI ; Getch blocks after recording reason for blocking in ACCB need_to_block_getch EQU * LDAB #GETCH_SWI ; this is the reason to block ;----------------------------------------------------------------- need_to_block EQU * point_ix_to_pt ; Load curproc into ACCA - need to replace this with an adt op later LDAA CurProcOff,X ; Now, take this process off the ready-list and block it JSR unready_and_block ; Now restore the SWI SP from the stack into IY where it is ; expected as a parameter by context-switch PULY ; Now, context-switch JMP context_switch ;---------------------------------------------------------------- need_to_block_putch EQU * LDAB #PUTCH_SWI BRA need_to_block ;---------------------------------------------------------------- must_be_putch EQU * ; Need to context-switch if output buffer is full... ; Check that we really have PUTCH SWI CMPB #PUTCH_SWI BNE panic_illegal_swi ; Handle putch() ; See if there is room in display queue point_ix_to_dispq JSR isfullq ; Carry set => full - need to block this process BCS need_to_block_putch ; Otherwise there is room - enqueue character for output JSR enqueue ; Now that DISPQ is non-empty, enable TIE interrupts LDX #REGBASE BSET JSCCR2,X TIE ; Recover IX from stack PULX ; Done! RTI ;---------------------------------------------------------------- panic_illegal_swi EQU * STOP ; Illegal SWI opcode ;----------------------------------------------------------------- ; init_sci ; ; Initialize the SCI subsystem for MTX - hardwire parameters ; for now - can parameterize later ; SCDR EQU $102F ; SCI data register SCSR EQU $102E ; SCI status reg RDRF EQU $20 ; Bit RDRF, TC EQU $40 ; TC, and TDRE EQU $80 ; TDRE of SCSR TIE EQU $80 ; TIE bit of SCCR2 TCIE EQU $40 ; TCIE bit of SCCR2 SCIPSV EQU $00C4 ; Pseudo-vector for the SCI interrupts JMPOP EQU $7E ; Opcode for JMP (extended mode) JPORTD EQU $08 ; offset from $1000 for PORTD JDDRD EQU $09 ; offset from $1000 for DDRD JSCCR2 EQU $2D ; offset from $1000 for SCCR2 JSCDR EQU $2F ; offset from $1000 for SCDR JBAUD EQU $2B ; offset from $1000 for BAUD JSCSR EQU $2E ; offset from $1000 for SCSR REGBASE EQU $1000 ; Base of peripheral registers init_sci EQU * ; Set base of peripheral registers LDX #REGBASE ; Set bit1 of DDRC, preparing to o/p 1 on Tx (Tx is the same as PD1) LDAA #$02 STAA JPORTD,X ; bit1 of DDRD is set - so drive PD1 high STAA JDDRD,X ; Configure data direction of PD1 as output ; Turn off XMTR and RCVR CLR JSCCR2,X ; do TE=RE=0, thus turning off XMTR and RCVR ; Set baud rate = 9600 LDAA #$30 ; TCLR=0, 0=0, SCP1=1, SCP0=1, ; RCKB=0, SCR2=0, SCR1=-0, SCR0=0 STAA JBAUD,X ; This gives 9600 baud ; Keep the transmitter interrupt disabled for now... ; Enable the receiver interrupt RIE ; Also turn on the transmitter and the receiver ldaa #$2C ; tie=0, tcie=0, rie=1, ilie=0, ; te=1, re=1, rwu=0, sbk=0 staa JSCCR2,X ; patch pseudo vector for sci interrupt LDAA #JMPOP STAA SCIPSV ; sci pseudo-vector opcode being patched LDD #SCIINTR STD SCIPSV+1 ; Done programming the SCI subsystem RTS ;----------------------------------------------------------------- ; Common SCIINTR handler ; ; Handles SCI interrupts ; ; Branches to do_keyboard or do_display depending on flag set (RDRF or TDRE) ; of SCSR. ; SCIINTR EQU * LDX #REGBASE ; Check if RDRF set BRSET JSCSR,X RDRF do_keyboard ; Else check if TDRE set BRSET JSCSR,X TDRE do_display ; Else panic - unknown interrupt... panic: STOP ;----------------------------------------------------------------- ; The keyboard driver do_keyboard do_keyboard EQU * ; Get the character into acca LDAA SCDR ; Now check if there is room in the display queue in order to ; echo. For this, point IX to DISPQ point_ix_to_dispq ; Check if full JSR isfullq ; carry set => full, so can't enqueue for echoing. Ring bell and return BCS bell_and_return ; Otherwise, save the char in ACCB, as ACCA will soon get clobbered TAB ; Save ACCB also, as it too will get clobbered soon! PSHB ; All args OK for enqueueing into display queue - just do it! JSR enqueue ; Now that DISPQ is non-empty, enable TIE interrupts LDX #REGBASE BSET JSCCR2,X TIE ; Point IX to KBDQ LDX #KQbegin ; Check if full JSR isfullq ; carry set => full, so can't enqueue it. Ring bell and return BCS bell_and_return ; May not need to enqueue, after all, if there is a blocked process. ; In order to check this, get IX to point to the base of the process table point_ix_to_pt ; Get Curproc into ACCA - in preparing to find the next blocked (if any) proc LDAA CurProcOff,X ; Find next process blocked for getch (if any, that is). ; For this, load ACCB with GETCH_SWI and call find_nxt_blked LDAB #GETCH_SWI JSR find_nxt_blked ; If ACCA came back as zero, no process is blocked; in which case just return ; Else ACCA points to the process that's blocked - wake it up! TSTA ; If acca is zero, none blocked ... then we must enqueue the character BEQ need_to_enqueue ; Otherwise there is a blocked process - the one pointed-to by ACCA ; Remove it from the blocked list and ready it ; also give it the char in ACCB (which is to be restored from the stack) PULB JSR unblock_and_ready ; Return to the interrupted execution RTI ;----------------------------------------------------------------- need_to_enqueue EQU * ; Restore ACCB from stack PULB TBA LDX #KQbegin ; Now enqueue it into the keyboard buffer ; Since IX still pointing to KQbegin, all arg registers are OK- just do it! JSR enqueue ; That's all RTI ;----------------------------------------------------------------- bell_and_return EQU * ; Wait for TDRE, ring bell, and then RTI LDX #REGBASE BRCLR JSCSR,X TDRE * ; wait for tdre LDAA #$07 ; bell character STAA SCDR RTI ;----------------------------------------------------------------- ; The display driver do_display do_display EQU * ; If the display queue is not empty, dequeue the next char from DISPQ and ; send it ; Point IX to DISPQ point_ix_to_dispq ; Check if empty JSR isemptyq ; carry set => empty - nothing more to display - see if any process is ; output-blocked BCS check_for_op_blked ; carry clear => not-empty - so there is work to do ; dequeue next character - character returned into ACCA, ... JSR dequeue ; send it, and now also check for output-blocked processes, if any STAA SCDR ;----------------------------------------------------------------- check_for_op_blked EQU * ; Check if there is a task that was blocked on putch() in the past point_ix_to_pt ; Get current process into acca LDAA CurProcOff,X ; Into ACCB note the block condition, and find next blocked LDAB #PUTCH_SWI JSR find_nxt_blked ; ACCA = 0 means none blocked on putch; else ACCA tells the blocked process TSTA BEQ done_display ;----------------------------------------------------------------- ; We need to ready this task - and get & enqueue the ; char it tried to send in the past JSR unblock_and_ready ; The above call must return via ACCB the char that was attempted to be sent ; Get that char into ACCA TBA point_ix_to_dispq JSR enqueue ; We know for sure that the display queue is non-empty - so ; we can return without bothering to do the isemptyq check, below. RTI ;----------------------------------------------------------------- ; Return to the interrupted execution done_display EQU * ; If DISPQ is empty, disable TIE interrupts - else don't point_ix_to_dispq JSR isemptyq ; If non-empty, don't turn off TIE interrupts - carry clear => not empty BCC over_and_out LDX #REGBASE BCLR JSCCR2,X TIE ; and return over_and_out EQU * RTI ;----------------------------------------------------------------- ; init_rti ; ; Initialize the RTI subsystem for MTX - hardwire parameters ; for now - can parameterize later ; TMSK2 EQU $1024 ; TMSK2 register JTMSK2 EQU $24 ; offset from $1000 TFLG2 EQU TMSK2+1 ; TFLG2 register JTFLG2 EQU JTMSK2+1 ; offset from $1000 PACTL EQU TFLG2+1 ; PACTL register RTIION EQU $40 ; RTII bit is on RTIFON EQU RTIION ; RTIF bit is on RTIPSV EQU $00EB $MACRO clear_rtif_flag PSHX LDX #REGBASE ; Turn on the RTIF bit, which is (p.180 of textbook) ; A BIT THAT IS CLEARED BY WRITING A ONE INTO IT! BSET JTFLG2,X RTIFON PULX $MACROEND init_rti EQU * ; PE1,0 bits are 0,0 to operate at freq. E / 2^13 / 1; other PACTL bits zeroed CLR PACTL ; Enable interrupts at the end of every RTI period via RTII by turning on the ; RTII bit of TMSK2 (Note: Don't forget to clear RTIF inside RTI handler) LDAA #RTIION STAA TMSK2 clear_rtif_flag ; A macro-call ; patch pseudo vector for RTI interrupt LDAA #JMPOP STAA RTIPSV ; sci pseudo-vector opcode being patched LDD #RTIINTR STD RTIPSV+1 ; Done RTS ;----------------------------------------------------------------- $MACRO set_a_to_next_process ; This macro is being written for the current prototype ; under the assumption that no more than 7 processes will ; be used (including the null process) - if not, this macro ; will have to be changed... the MSB of the byte indicating ; curproc should be allowed to wrap-around... LSLA $MACROEND ;----------------------------------------------------------------- ; RTI interrupt handler RTIINTR EQU * ; First clear RTIF flag clear_rtif_flag ; Grab the SP of the currently running process into IY move_s_to_y ; Now, context switch JMP context_switch ; no point JSR to context-switch! ;----------------------------------------------------------------- ; context_switch is not really a subroutine because we don't ; "return" to the caller! However, as far as "parameter passing" ; goes, treat it like one. ; ; Called with SP of currently running process in IY ; Saves this value into the saved SP field of curproc ; Then determines next ready process and runs it context_switch EQU * ; Next, save this IY value into the correct PTE; for this, ; 1. Point IX to the PT point_ix_to_pt ; 2. Load curproc into ACCA LDAA CurProcOff,X ; 3. Call "get_at_sp_of_procno" with IX pointing to the PT and ACCA ; containing the process number. Sets IX pointing to the address of ; the saved SP in the PT JSR get_at_sp_of_procno ; 4. Save SP contained in IY here! STY 0,X ; Find next ready process from one above curproc. ; For this, ; 1. Point IX to pt ... once again point_ix_to_pt ; 2. Set ACCA to correspond to the next process set_a_to_next_process ; 3. Find next ready process starting from here. There HAS to be a ready ; process - at least the one which we got interrupted out of - so ; don't worry about the case where ACCA = 0 (which would have ; indicated "none ready") JSR find_nxt_ready ; Now record this process as Curproc STAA CurProcOff,X ; Now load the saved SP of this process and get going. For this, ; 1. Point IX to the SP of the process to be run JSR get_at_sp_of_procno ; 2. Load the SP of this process and off we go! LDS 0,X RTI ;--------------------------------------------------------- ; Subroutine get_at_sp_of_procno: Move to the saved stack-pointer ; of a given process ; ; Input: ACCA = procno ; IX = proctable ; ; Output: IX = Address of saved SP of procno ; ; No side-effects to memory ; ; No other registers affected get_at_sp_of_procno EQU * PSHA PSHB ; Point IX at the first saved SP LDAB #PTE1Off ABX ; We are done if curproc is the null process - prepare ACCA ; to correspond to this fact - will test for ACCA = 0 as loop-test ; If ACCA=0 after LSRA , we are done ; Keep PTE size in ACCB to bump up IX if not LDAB #PTESiz ; while ACCA <> 0 do whichproc: LSRA BEQ found_proc ABX BRA whichproc found_proc EQU * PULB PULA RTS ;----------------------------------------------------------------- ; A stack object: ADT to manipulate stack frames ; ---------------------------------------------- ; No separate data allocated for this object, per se; this is just ; a way of treating the standard HC11 stack-frame as an ADT object. ; ; Given the standard hc11 stack-frame, support the following ; operations: ; ; make_init_stk : make up the initial stack frame to run the ; null task after system initialization ; ; Stack frame looks like this: ; Count offsets from (say) IX register pointing one above CCR CCROff EQU 1 ACCBOff EQU 2 ACCAOff EQU 3 IXHOff EQU 4 IXLOff EQU 5 IYHOff EQU 6 IYLOff EQU 7 PCHOff EQU 8 PCLOff EQU 9 ;----------------------------------------------------------------- ; Subroutine make_init_stk: Makes an initial stack for a process ; ; Input: IX = points one above where CCR is filled ; IY = PC to be set for the task of which this is the stack ; ; Memory side-effect: Makes a stack frame ; ; No registers affected make_init_stk EQU * ; Make CCR with S=0,X=1,H=0,I=0,N=0,Z=0,V=0,C=0 i.e. $40 ; Most important : I must be zero above - or else the task ; can't be interrupted !! CLR CCROff,X BSET CCROff,X $40 ; Clear registers CLR ACCBOff,X CLR ACCAOff,X CLR IXHOff,X CLR IXLOff,X CLR IYHOff,X CLR IYLOff,X ; Init the PC conveyed thru IY STY PCHOff,X ; Done RTS ;----------------------------------------------------------------- ; Subroutine put_a_in_stk: Puts ACCA into a given stack frame ; ; Input: ACCA = value v to be put into ACCA of the stack frame ; IX = points to the top of stack ; ; Memory side-effects: Puts value v into the ACCA field of the stack frame ; ; No registers affected put_a_in_stk EQU * STAA ACCAOff,X RTS ;----------------------------------------------------------------- ; Subroutine get_a_out_of_stk: Read off ACCA of a given stack frame ; ; Input: IX = pointer to the top of stack ; ; Output: ACCA = value loaded from ACCA field of the stack ; ; No memory side effects ; ; No registers affected get_a_out_of_stk EQU * LDAA ACCAOff,X RTS ;----------------------------------------------------------------- ; Subroutine bump_pc_in_stk: increment PC in a given stack frame ; Used to look beyond the SWI opcode in the stack ; ; Input: IX = top of stack ; ; Output: IY = current PC in stack frame ; ; Memory side-effects: Bump up the PC in the stack frame ; ; No other registers affected bump_pc_in_stk EQU * ; Get PC in stack into IY LDY PCHOff,X ; Bump it up INY ; Store back STY PCHOff,X ; Restore IY to point to SWI opcode DEY ; Done RTS ;----------------------------------------------------------------- ; A queue object ; -------------- ; The first word of the queue object is a pointer to "last+1" - i.e. one beyond ; the extent of the queue. This helps string along the next object as if it ; were a link in a linked-list. BaseOff EQU 0 ; Base of queue object Last1Off EQU BaseOff+0 ; Offset of location one beyond max location FstOff EQU BaseOff+2 FrontOff EQU BaseOff+4 ; Offset of front pointer RearOff EQU BaseOff+6 ; Offset of rear pointer StatOff EQU BaseOff+8 ; Offset of status byte QdatOff EQU BaseOff+9 ; Offset of first data byte FULLBIT EQU $01 ; Position of the full-bit EMPTYBIT EQU $02 ; Position of the empty-bit ; Address: Qobject ; Offset: ; Last1Off ----- Data: Last1high ; Last1Off+1 ----- Data: Last1low ; FstOff ----- Data: FstDatHigh ; FstOff+1 ----- Data: FstDatLow ; FrontOff ----- Data: Fronthigh ; FrontOff+1 ----- Data: Frontlow ; RearOff ----- Data: Rearhigh ; RearOff+1 ----- Data: Rearlow ; StatOff ----- Data: Status byte ; QdatOff ----- Data: actual queue data, byte 1 ; ... ;----------------------------------------------------------------- ; Subroutine enqueue : enqueue - called when queue is not full ; ; Input: ACCA = ch to be enqueued ; IX = Queue object ; ; Memory side-effects: Returns after character is stored, rear pointer updated, ; and full status updated ; ; No registers affected enqueue EQU * ; Save IY which will get clobbered PSHY ; Get rear pointer into Y LDY RearOff,X ; Store character there STAA 0,Y ; Increment rear INY ; Have we gone beyond the high-end of the queue ? CPY Last1Off,X ; If so, fix rear to point to queue base BEQ fix_rear ; Else rear is correct; save it away STY RearOff,X ; ...and now check if queue full BRA chk_full FIX_REAR EQU * ; Get base into Y LDY FstOff,X ; Store base as rear STY RearOff,X CHK_FULL EQU * ; We know that the queue is NOT empty - record that now! BCLR StatOff,X EMPTYBIT ; Get new rear into Y LDY RearOff,X ; Compare with original front - we use the full-detection approach that ; says that a circular buffer is full if front = rear after an enqueue ; has been performed. CPY FrontOff,X ; If equal, full BEQ RECORD_FULL ; Otherwise leave full and empty alone - restore IY and return! PULY RTS RECORD_FULL EQU * ; Set full-bit BSET StatOff,X FULLBIT ; Restore IY and return PULY RTS ;----------------------------------------------------------------- ; Subroutine createq : create a queue of given specifications ; ; Input: IX = Queue object base ; ACCB = size of the queue ; ; Memory side-effects: Creates a queue object beginning at Qobject ; and capable of holding size number of bytes ; ; No registers affected createq EQU * ; Save registers A and IY PSHA PSHY ; Get contents of Reg X into Reg Y (a trick method - better ones?) move_x_to_y ; For various reasons (lack of certain opcodes 6811) free up ACCD (hence ACCB) PSHB ; For various reasons, we need to get Y into D XGDY ; Now add QdatOff to ACCD to make ACCD look at Q byte 1 ADDD #QdatOff ; Store D as FstDatHigh and FstDatLow STD FstOff,X ; Now free up reg D by putting it into INDY XGDY ; now, Y is looking at Q byte 1 ; Now, get size of queue back from stack PULB ; Now, make Y contain the address Lasthigh and Lastlow by adding queue size ABY ; Now store Y at Last1Off STY Last1Off,X ; Now initialize front and rear to point to Q byte 1; ; First, get D to contain FstDatHigh and FstDatLow LDD FstOff,X ; Now Store D at FrontOff and RearOff STD FrontOff,X STD RearOff,X ; Now fix the status byte to read ``empty'' with all other bits off CLR StatOff,X BSET StatOff,X EMPTYBIT ; Restore ACCA and IY and return PULY PULA RTS ;----------------------------------------------------------------- ; Subroutine dequeue : dequeues a character - called when queue is not empty ; ; Input : IX = Qobject ; ; Output: ACCA = result of dequeue ; ; Memory side effects: dequeue from the queue ; ; No other registers affected dequeue EQU * ; Save IY PSHY ; Get front pointer into Y LDY FrontOff,X ; Read character there LDAA 0,Y ; Increment front INY ; Have we gone beyond the high-end of the queue ? CPY Last1Off,X ; If so, fix front to point to queue base BEQ fix_front ; Else front is correct; save it away STY FrontOff,X ; ...and now check if queue empty BRA chk_empty FIX_FRONT EQU * ; Get base into Y LDY FstOff,X ; Store base as rear STY FrontOff,X CHK_EMPTY EQU * ; We know that the queue is NOT full - record that BCLR StatOff,X FULLBIT ; Get new front into Y LDY FrontOff,X ; Compare with original front - we use the empty-detection approach that ; says that a circular buffer is empty if front = rear after a dequeue ; has been performed. CPY RearOff,X ; If equal, emptyxs BEQ RECORD_EMPTY ; Otherwise leave full and empty alone - restore IY and return! PULY RTS RECORD_EMPTY EQU * ; Record empty bit BSET StatOff,X EMPTYBIT ; Restore IY and return PULY RTS ;----------------------------------------------------------------- ; Subroutine isfullq: Returns the queue full status ; ; Input: IX = queue object ; ; Output: Carry flag = full status - 1 if full, 0 if not ; ; No registers affected, and no memory side-effects isfullq EQU * ; Test the status byte and branch to set_carry if the full bit is set BRSET StatOff,X FULLBIT set_carry ; The carry isn't to be set CLC ; return RTS set_carry EQU * SEC RTS ;----------------------------------------------------------------- ; Subroutine isemptyq : checks for queue being empty ; ; Input: IX = queue object ; ; Output: carry flag = queue empty status - 1 if empty, 0 if not ; ; No registers affected, and no memory side effects isemptyq EQU * ; Test the status byte and branch to set_carry1 if the empty bit is set BRSET StatOff,X EMPTYBIT set_carry1 ; The carry isn't to be set CLC ; return RTS set_carry1 EQU * SEC RTS ;----------------------------------- END OF QUEUE OBJECT ;----------------------------------------------------------------- ; A process-table object ; ---------------------- ; The first word of the process-table object is a pointer to "last+1" - ; i.e. one beyond the extent of the process-table. This helps string ; along the next object as if it were a link in a linked-list. ; Abbreviations: PT = Process Table and PTE = Process Table Entry ; ; Address: PTEObject ; Offset: L1Off ----- Data: Last1H - Hbyte of one beyond PT ; L1Off+1 ----- Data: Last1L - Lbyte of one beyond PT ; PTSizeOff ----- Data: Size of PT IN UNARY FORMAT ; BLKL_g_Off ----- Data: list of processes blocked for getch ; BLKL_p_Off ----- Data: list of processes blocked for putch ; READYLOff ----- Data: list of ready processes ; CurProcOff ----- Data: Unary byte indicating Curproc ; ; Within each PTE, the following offsets are available ; ; SPOff ----- Data: SPH ; SPOff+1 ----- Data: SPL ; STOff ----- Data: State ; WhyBlkOff ----- Data: GETCH_SWI or PUTCH_SWI - anything ; else is illegal to read & use ; ; PTEn is at PTE1 address + PTESiz * (n-1) ; PTESiz EQU 4 ; Currently a PTE (process table ENTRY) is ; ; SPH, SPL,State, and ? where ? is space for ; adding info in the future - i.e. 4 bytes L1Off EQU BaseOff+0 ; Base of the PT object PTSizeOff EQU BaseOff+2 BLKL_g_Off EQU BaseOff+3 BLKL_p_Off EQU BaseOff+4 READYLOff EQU BaseOff+5 CurProcOff EQU BaseOff+6 PTE1Off EQU BaseOff+7 SPOff EQU 0 ; Offset for saved SP within a PTE STOff EQU 2 ; Offset for the saved state within a PTE WhyBlkOff EQU 3 ; Offset of the "why blocked" field from ; base of PTE ; The following information is currently not really being used Ready EQU $01 ; State Ready is modeled this way Blocked EQU $02 ; State Blocked is modeled this way Running EQU $04 ; State Running is modeled this way ;----------------------------------------------------------------- ; Subroutine creatept : create a process table ; ; Input: IX = PT base ; IY = The beginning value of the stack pointers to be stuck into PTEs ; ACCA = The number of PTEs ; ; Create a PT with PTbase conveyed thru IX, initial stack pointer InitSP ; conveyed thru IY, and size conveyed thru ACCA. InitSP corresponds to the ; initial SP value for the null task. ; ; Assume that size is at least 1 ; ; NOTE: SIZE IS IN UNARY FORMAT ; ; All the tasks in the initial PT are kept in the ready state ; and their SP fields are initialized with the correct SP values ; ; All tasks are also placed on the READYLIST ; ; Return with IX still pointing to PTbase ; ; Memory side-effects: a process table is created ; ; Registers affected - none creatept EQU * ; Save ACCA, ACCB, IY, and IX to avoid destroying them PSHA PSHB PSHY PSHX ; Clear the BLKLISTs CLR BLKL_g_Off,X CLR BLKL_p_Off,X ; Also set bits in READYLIST corresponding to all the process - this ; is made convenient by the unary representation of PTE size STAA READYLOff,X ; Store size (in unary format) of PTE contained in ACCA STAA PTSizeOff,X ; Set current process number to that of the null process (process-number zero) ; So set bit-0 of the CurProc byte CLR CurProcOff,X BSET CurProcOff,X $01 ; Init ACCB with PTESiz LDAB #PTESiz ; For each PTE, store INITSP into the saved SP field, and set process ; state to Ready ; Make X point to PTE1 LDAB #PTE1Off ABX ; Reload PTESiz in ACCB for the loop DoAPTE LDAB #PTESiz DoAPTE EQU * ; Store saved SP STY SPOff,X ; Put PTE into the "Ready" state CLR STOff,X BSET STOff,X Ready ; Set the "why blocked" field to an illegal value to avoid inadvertent ; access before initialization CLR WhyBlkOff,X BSET WhyBlkOff,X $04 ; Add PTESiz in ACCB to IX ABX ; Update IY to point to the next SP - that for task 1, task 2, ... ; These SPs are TaskSize away from each other - to do the arithmetic, ; need to free up ACCB, do the arith, and pull it back... PSHB LDAB #TaskSize ABY PULB ; Are we done with all PTEs? Find out... LSRA ; because SIZE of PTE is in UNARY, we do LSR (zero-fill) BEQ ptefinish ; Go back BRA DoAPTE ptefinish EQU * ; Done with All PTEs - X is pointing to Last1 (last + 1) ; Store this value away at L1Off ; To do that, need to get IX into IY and restore IX to the base ; of the PT object move_x_to_y ; Restore IX PULX ; Store IY away STY L1Off,X ; Restore IY, ACCB, and ACCA PULY PULB PULA ; and return RTS ;----------------------------------------------------------------- ; Subroutine find_nxt_ready : Find NEXT ready process in ready list, ; STARTING FROM bit pointed-to by procno (this is how "next ready" is defined). ; ; Input: IX = PTbase ; ACCA = procno ; ; Output: ACCA = all zeros if no process is ready (would be an error!) ; = a non-zero value indicating which process is ready ; ; Returns "process 1" to be a ready process only if that's the ; only ready process! This will avoid running the null process if others ; are meanwhile runnable ; ; No side-effects ; ; No other registers affected find_nxt_ready EQU * ; Save IX and ACCB PSHX PSHB ; Increment IX to point to ready-list LDAB #READYLOff ABX ; Copy readylist into accb and mask out bit 1 of readylist LDAB 0,X BCLR 0,X $01 ; "bit 1" corresponds to the null task ; Now call find_nxt_set JSR find_nxt_set ; Restore ready list STAB 0,X ; Is ACCA <> 0? If so, found another ready task than the null task TSTA BNE found_non_null ; Otherwise the null task is the only one ready - record thus in acca ; This is because the null task NEVER blocks LDAA #$01 ; says that the null task is ready found_non_null EQU * ; Restore IX and ACCB PULB PULX ; Done! RTS ;----------------------------------------------------------------- ; Subroutine find_nxt_blked : Find next blocked process in one of ; the blocked-list (BLKL_g_Off or BLKL_p_Off) ; starting from bit pointed-to by procno (this is how "next blocked" is ; defined). ; ; Input: IX = PTbase ; ACCA = procno ; ACCB = the blocked status (GETCH_SWI and PUTCH_SWI) being sought ; (Correspondingly we search in BLKL_g_Off and BLKL_p_Off) ; ; Output: ACCA = all zeros, if no process is blocked in the said manner ; a non-zero value indicating the position of the process ; that is blocked in the said manner. ; ; No memory side-effects ; ; No other registers affected find_nxt_blked EQU * ; Save IX and ACCB PSHX PSHB ; Increment IX to point to the right blocked-list ... how much to ; increment depends on which blocked-list is of interest CMPB #GETCH_SWI BNE is_putch LDAB #BLKL_g_Off BRA get_to_block_list ;----------------------------------------------------------------- is_putch EQU * CMPB #PUTCH_SWI BNE panic_illegal_blk_list LDAB #BLKL_p_Off ;----------------------------------------------------------------- get_to_block_list EQU * ABX ; Call find_nxt_set JSR find_nxt_set ; Restore IX and ACCB PULB PULX ; Done! RTS panic_illegal_blk_list EQU * STOP ; Illegal blocked list specifier ;----------------------------------------------------------------- ; Subroutine find_nxt_set: Find next set 1-bit in byte pointed to by IX ; where the start of the search is defined by the procno word ; ; Input: IX = point either at the ready-list or one of the blocked lists ; ; ACCA = procno ; ; Output: ACCA = zero, if no set 1-bit found ; ; = non-zero, if there is a set bit in the ready or blocked ; lists. ; ; Search towards higher bit positions before wrapping around ; to the lowest bit position. ; ; Algorithm: ; ; Check if byte being examined is zero ; If so, return with ACCA cleared ; Else ; OR procno with byte being examined ; IF change occurred, that bit wasn't set ; restore procno ; IF no change, that bit was set - stop! ; ; No memory side-effects ; ; No other registers affected find_nxt_set EQU * ; Is byte pointed by IX zero? TST 0,X ; If non-zero, there is work to do BNE do_work ; Else done! Clear ACCA and return CLRA RTS do_work EQU * ; Save ACCB onto the stack PSHB ; Copy ACCA into ACCB TAB more_find EQU * ; Now OR procno in ACCA with ORAA 0,X ; Compare ACCA with that byte CMPA 0,X ; Not same? Continue with rotated acca as per above algorithm BNE rot_cont ; Same? Then found a set written bit! Restore ACCA and BRA to finish_find TBA BRA finish_find rot_cont EQU * ; Restore ACCA from ACCB TBA ; Is ACCA already with MSB set? if so, next ACCA is $01 TSTA ; No, acca doesn't have MSB set.... BPL continue_asl ; Has MSB set - so next ACCA is $01 LDAA #$01 BRA after_asl continue_asl EQU * ; Go towards higher process numbers via asla ASLA after_asl EQU * ; Save this ACCA onto ACCB TAB ; Go loop till set bit (guaranteed) is found BRA more_find finish_find EQU * ; ACCA now tells the next runnable process number! ; Restore ACCB and return PULB RTS ;----------------------------------------------------------------- ; Subroutine unblock_and_ready: Unblock process of the specified variety, ; completing suspended operations such as getch or putch (if any) ; ; Input: IX = PTbase ; ; ACCA = procno ; ; ACCB = if unblocking a process which was blocked on getch ; then the character to be "fed" to this process ; else don't-care ; ; Output: ACCB = if unblocking a process which was blocked on putch ; then the character that could not be put out is returned ; else no change ; ; Memory side-effects: changes to the process table ; ; No other registers affected unblock_and_ready EQU * ; Save IY, IX, ACCA, and ACCB PSHY PSHX PSHA PSHB ; Copy ACCA into ACCB TAB ; Set corresp. bit in READYL ORAA READYLOff,X ; Store this back into READYL STAA READYLOff,X ; Can't take this process off the blocked list yet - need ; to find why blocked etc... remember X in Y till then move_x_to_y ; Restore ACCA from ACCB - ACCA must have procno in order for ; get_at_sp_of_procno below to locate the SP address! TBA ; Now, locate the stack frame of this process to stick the char in accb JSR get_at_sp_of_procno ; Find out why this process was blocked LDAA WhyBlkOff,X CMPA #GETCH_SWI ; was blocked on getch... BEQ blk_on_getch CMPA #PUTCH_SWI ; was blocked on putch... BEQ blk_on_putch ; Otherwise panic! STOP ; blocked for unknown reason in unblock_and_ready blk_on_getch EQU * ; Take it off the getch blocked list EORA BLKL_g_Off,Y ; Store this back into BLKL STAA BLKL_g_Off,Y ; IX is now facing the saved SP for this process - lap up this saved SP! LDX 0,X ; Now, bring back ACCB saved away on the stack (accb has the char) PULB ; Need to get char into ACCA - so tba TBA ; Stick ACCA into the stack frame JSR put_a_in_stk BRA done_ubr blk_on_putch EQU * ; Restore ACCA from ACCB, given that ACCB is preserving the procno ; and ACCA has been trashed for the CMPA test above TBA ; Take it off the putch blocked list EORA BLKL_p_Off,Y ; Store this back into BLKL STAA BLKL_p_Off,Y ; IX is now facing the saved SP for this process - lap up this saved SP! LDX 0,X ; Now, bring back ACCB saved away on the stack (accb has the char) PULB ; Read off ACCA - the character attempted to be sent in the past - from stack JSR get_a_out_of_stk ; Put it in ACCB TAB done_ubr EQU * ; Now restore ACCA, IX, and we are done! PULA PULX PULY RTS ;----------------------------------------------------------------- ; Subroutine unready_and_block: unready process and block it for the ; specified reason ; ; Input: IX = PTbase ; ACCA = procno ; ACCB = Reason to block (one of GETCH_SWI or PUTCH_SWI) ; ; Memory side effects: procno put into blocked state ; and removed from ready state ; ; No other registers affected unready_and_block EQU * ; Save IX PSHX ; Save ACCB PSHB ; Find out reason for blocking CMPB #GETCH_SWI BNE blk_for_putch ; Blocking for getch... ; Save ACCA into ACCB TAB ; Set corresp. bit in BLKL ORAA BLKL_g_Off,X ; Store this back into BLKL STAA BLKL_g_Off,X BRA after_blking blk_for_putch EQU * ; Check if we really got a PUTCH_SWI code CMPB #PUTCH_SWI ; Nope! In trouble! BNE panic_bad_swi_code ; Save ACCA into ACCB TAB ; Set corresp. bit in BLKL ORAA BLKL_p_Off,X ; Store this back into BLKL STAA BLKL_p_Off,X after_blking EQU * ; Restore ACCA from ACCB TBA ; Take this process off the ready list EORA READYLOff,X ; Store this back into READYL STAA READYLOff,X ; Restore ACCA from ACCB TBA ; Restore ACCB PULB ; Point IX to the saved SP of the process being blocked JSR get_at_sp_of_procno ; Save ACCB at WhyBlkOff offset from IX STAB WhyBlkOff,X ; Restore IX and we are done! PULX RTS panic_bad_swi_code EQU * STOP ; bit bad SWI code and died. ;------------------------------------------ END OF PTADT ; The following ORGs are to facilitate execution in the single-chip ; mode. To execute MTX in the special bootstrap mode (say in ; a PCbug session), just short tx and rx and reset the processor ; and the execution will automatically transfer to MTXORG. ORG $FFD6 ; sci vector FDB $00C4 ORG $FFF0 ; rti vector FDB $00EB ORG $FFF6 ; swi vector FDB $00F4 ORG $FFFE ; reset vector FDB mtxorg ;; --- end of mtx.asm --- ;;