A Teeny-tiny Operating System

Paul R. Potts

1987

In my sophomore year of college I took two interesting classes that helped “make my bones” as a programmer. One was called Assembly Language, and one was called Operating Systems. In the assembly language class we wrote a series of assignments that culminated in writing a (simplified) assembler for the VAX 11/750, in VAX 11/750 assembly language. In the operating systems class, we read Operating System Concepts, Second Edition by Peterson and Silberschatz and studied the theory behind operating systems. But we also had a “practicum” — we implemented an operating system.

Well, sort of. We weren’t Linus Torvalds writing Linux back then. We had a small lab set up with Motorola 68000 “trainer” development boards, each of which contained the same microprocessor that was in the original Apple Macintosh. We used a cross-assembler that ran on the VAX 11/750 to build 68000 assembly-language code for this board, download it, and run it. There was a little low-level “monitor” program in ROM on this board that made this possible.

Why the 68000? We and our instructor were quite interested in the 68000 at the time, as it was a very capable microprocessor, with a larger memory space and wider data bus than the earlier 6502 used in the Apple II or 8088 used in the original IBM PC. Our instructor Simon Tung was especially interested in the illegal opcode exception-handling, as this was used by Apple to implement the early Macintosh Toolbox. I vividly recall him drawing a diagram of the exception-handling logic at the gate level.

The exception-handling logic provided a fast and efficient way to trigger an exit from user code to privileged system code in ROM. This saved a lot of RAM by keeping most of the toolbox code in ROM — remember, RAM was scarce and expensive back then compared to ROM. But this mechanism worked by using a jump table stored in RAM, which allowed code on disc to patch operating system calls, both to fix bugs and to do other neat tricks. I would eventually teach how to write a variety of programs for old MacOS in C and Pascal, and that became a big part of my early career, but that’s another story.

We began with some very simple low-level functions, but the final assignment was to put them together into a “teeny-tiny operating system.” It was a program that ran a timer and read keyboard input using interrupts, launched a number of tasks, and switched periodically between tasks. So we’d press a key, and the program would start spitting out that character to the terminal at a specific rate. We’d press additional keys, and it would start more tasks to do the same thing with different characters at different rates. We could kill processes. This functionality is what an operating system does. Of course, it’s not all a modern operating system does.

I don’t recall all the details (it’s been almost 40 years, after all), but I recall that I had my program working perfectly, with beautiful timing, but I screwed something up, and I was never able to get the timing to work quite that well again. Oh well. If you spot the bug, let me know!

Note that the web version of the code is a bit ugly, because my web page template imposes a strict width restriction on preformatted text, which results in the ends of some comments getting cut off (although you can scroll right to read them). Until I can fix that, I recommend reading the PDF version of this file.

* Original filename: as3.3.src;
                LLEN            120             ;ALIGN NICELY

******************************************************************
***********    ASSIGNMENT 3 PART 3 FOR CS 256 BY PAUL POTTS
***********    THIS PROGRAM IMPLEMENTS UP TO 16 PROCESSES CALLED
***********    'USER' WHICH SIMPLY PRINT OUT A SPECIFIC CHARACTER
***********    STORED IN THE PCB.  THE DELAYS FOR NEW PROCESSES
***********    START OUT BEING 1000 UNITDELAYS OF .001 SECONDS,
***********    OR 1 SECOND. IN ORDER TO KEEP THE PROCESSES
***********    FROM OVERFLOWING EVEN A LARGE QUEUE THE SMALLEST DELAY
***********    ALLOWED MUST BE 5 UNITDELAYS.  THE USER CAN CHANGE THE DELAY
***********    BY STRIKING '+' TO DOUBLE IT OR '-' TO CUT IT
***********    IN HALF WHILE THE PROCESS IS RUNNING.  THE KEY
***********    '#' WILL TERMINATE EXECUTION OF THE PROCESS.
*********** THE SYSTEM STARTS OUT RUNNING NO PROCESSES AND GENERATING NO
*********** TIMER INTERRUPTS TO SWAP PROCESSES.  PROCESSES ARE ADDED TO THE
*********** SYSTEM BY STRIKING KEYS OTHER THAN THE ONES MENTIONED ABOVE.
*********** THESE PROCESSES WILL CONTINUE TO CYCLE UNTIL THEY ARE KILLED.
*********** NOTE THAT IF THE TIME DELAY OF A PROCESS IS MADE SMALL ENOUGH
*********** IT WILL PRINT SEVERAL REPETITIONS OF ITS CHARACTER IN ITS
*********** TIME SLICE, SINCE IT WILL CALL "DISPLAY" SEVERAL TIMES
*********** BEFORE IT IS SWAPPED OUT.
******************************************************************
* THE FOLLOWING ARE SYSTEM-DEPENDENT LABELS GIVEN TO THE SPECIAL
* REGISTERS OF THE 68000 BOARD
******************************************************************

ACIAVECTOR      EQU             4*29    ;VECTOR FOR ACIA INTERRUPT
TIMERVECTOR     EQU             4*26    ;VECTOR FOR TIMER INTERRUPT
ALINEVECTOR     EQU             4*10    ;VECTOR FOR A-LINE HANDLER
TCR             EQU             $10021          ;TIMER CONTROL REGISTER
TIV             EQU             $10023          ;TIMER INTERRUPT VECTOR
CPR             EQU             $10025          ;COUNTER PRELOAD AREA
ACIACS          EQU             $10040          ;CONTROL/STATUS REGISTER
ACIAD           EQU             $10042          ;DATA REGISTER
ACIAMODE        EQU             15              ;THE MODE WANTED
QSIZE           EQU             40              ;40 SPACES IN THE QUEUE
SRSTART         EQU             $2000           ;INITIAL SR OF EACH PROCESS
DELAY1          EQU             100             ;DELAY OF PROCESS=.1 SEC.
NEWSLICE        EQU             125000          ;STARTING SLICE=1 SEC.

******************************************************************

TOP             ORG             $1000           ;START HERE
                JMP             START           ;BRANCH DOWN
TIMESLICE       DS.L            1               ;START TIMESLICE AT 10 SEC.

*************************************************************************
**** THIS DEFINITION OF THE QUEUE DOES NOT USE INTERNAL LABELS BUT JUST
**** A POINTER TO THE WHOLE QUEUE.  THUS, QUEUE POINTS TO THE HEAD OFFSET,
**** QUEUE+2 POINTS TO THE TAIL OFFSET, QUEUE+4 POINTS TO THE LENGTH, AND
**** QUEUE +6 IS THE FIRST BYTE OF THE QUEUE.  THE QUEUE STORAGE TAKES
**** LENGTH OF QUEUE PLUS ONE BYTES, ALL BUT ONE OF WHICH CAN HOLD ACTUAL
**** DATA.  THE EXTRA IS USED TO FIGURE OUT WHEN THE QUEUE IS FULL.
*************************************************************************

QUEUE           DS.W            1               ;THE HEAD POINTER
                DS.W            1               ;THE TAIL POINTER
                DC.W            QSIZE           ;THE QUEUE LENGTH
                DS.B            QSIZE+1         ;THE LENGTH PLUS AN EXTRA

******************************************************************
* PCOUNT HOLDS THE NUMBER OF ACTIVE PROCESSES, FROM ZERO TO 16
******************************************************************

PCOUNT          DS.B            1               ;NUMBER OF PROCESSES

******************************************************************
* THIS IS THE STRUCTURE OF A PCB IN MEMORY:
*
* storage   holds
* BYTE:     ASCII ID CHARACTER FOR THE PROCESS
* BYTE:     FOR FUTURE USE
* WORD:     FOR HOLDING THE STATUS REGISTER
* LONGWORD: PC
* LONGWORD: D0
* LONGWORD: D1
* LONGWORD: D2
*
* TOTAL PCB: EACH TAKES 20 BYTES
*
******************************************************************

PCBTOP          DS.B    320     ;SPACE FOR 30 PCBS

PCBPOINTER      DS.L    1       ;POINTER TO THE CURRENT PCB

*******************************************************
* CODE TO ALLOW OR DISALLOW THE HANDLING OF INTERRUPTS
*******************************************************

UNBLOCKINT      MACRO
                ANDI    #$F8FF,SR       ;ENABLE INTERRUPTS
                ENDM

********************************************************

BLOCKINT        MACRO
                ORI     #$0700,SR       ;DISABLE INTERRUPTS
                ENDM

********************************************************
* DISPLAY FORCES AN A-LINE EXCEPTION TO OCCUR, AND THE
* SYSTEM JUMPS TO THE CODE BELOW, AHANDLER, WHEN IT OCCURS.
********************************************************

DISPLAY         MACRO
                DC.W    $A000           ;A-LINE FOR DISPLAY
                ENDM

**************************************************************
* THE A-LINE HANDLER
* THIS ROUTINE SIMPLY COPIES THE CURRENT PROCESS ID CHARACTER
* INTO D7, THEN CALLS ENQ TO PUT IT IN THE QUEUE.
* THE REGISTERS A6, D0, D1, AND D2 ARE NOT DESTROYED, BUT
* OTHERS ARE USED IN THE QUEUE ROUTINE.
**************************************************************

AHANDLER        BLOCKINT
                MOVEA.L PCBPOINTER,A5           ;ADDRESS POINTED TO
                MOVE.B  (A5),D7                 ;PUT ID CHAR INTO D7
                JSR     ENQ                     ;ENQUEUE THE BYTE
                ADD.L   #2,$2(A7)               ;UPDATE OLD PC
                RTE

***************************************************************
* SUBROUTINE:  INITALINE
* THIS ROUTINE SIMPLY DOES INITIALIZATION OF THE A-LINE EMULATOR.
***************************************************************

INITALINE       MOVE.L  #AHANDLER,ALINEVECTOR           ;A-LINE VECTOR
                RTS

***********************************************************************
* SUBROUTINE:  INITPCBS
* THIS ROUTINE SETS UP THE INITIAL VALUES OF THE THREE PCBS IN MEMORY.
* THIS IS WRITTEN AS CODE INSTEAD OF SIMPLY FILLING MEMORY WITH
* DC.W OR DC.L SO THAT THE PROGRAM CAN BE RESTARTED WITHOUT LOSING
* THE INITIAL CONTENTS OF THE PCBS.
*
* REGISTERS USED: A0 AND D4 ONLY.  THE ORIGINAL VALUES ARE RESTORED
***********************************************************************

INITPCBS        MOVEM.L D4/A0,-(A7)             ;PUSH REGISTERS
                MOVEA.L #PCBTOP,A0              ;HOLD TEMPORARILY
                MOVE.L  A0,PCBPOINTER           ;SET UP INITIAL VALUE
                MOVE.B  #0,PCOUNT               ;NUMBER OF PROCESSES=0
                MOVE.L  #15,D4                  ;SET UP LOOP COUNTER

COUNTLOOP       MOVE.B  #0,(A0)                 ;SET ID/OUTPUT CHAR TO NULL
                ADD.L   #2,A0                   ;POINT TO SR
                MOVE.W  #SRSTART,(A0)           ;COPY INTO PCB
                ADD.L   #2,A0                   ;POINT TO FIRST PC
                MOVE.L  #WHILELOOP,(A0)         ;COPY INTO PCB
                ADD.L   #16,A0                  ;POINT TO NEXT PCB
                DBEQ    D4,COUNTLOOP            ;BUILD NEXT PCB
                MOVEM.L (A7)+,D4/A0             ;POP REGISTERS
                RTS

***********************************************************************
****** THIS ROUTINE IS MOD:  IT ACCEPTS THE OFFSET STORED IN A WORD
****** (OF 1..QSIZE + 1) AND RETURNS THE REAL MEMORY ADDRESS TO PUT
****** OR GET THE DATA BYTE.  IT SERVES TO WRAP AROUND THE QUEUE SO
****** THAT IT ISN'T POSSIBLE TO WALK OFF THE BOTTOM OF THE DATA
****** STRUCTURE BY INCREMENTING YOUR OFFSETS TOO MUCH.  IF THE POINTER
****** IS TOO LARGE (MEANING THAT IT CONTAINS QSIZE+1) THE MACRO
****** RESETS IT TO ZERO, OR THE BEGINNING OF THE QUEUE.
***********************************************************************
* The first parameter is the word offset
* The second parameter is the real address of where the byte is or should go

MOD             MACRO
                MOVEM.L D1-D2/A4,-(A7)  ;PUSH STUFF ON
                MOVEA.L A6,A4           ;START AT TOP OF QUEUE
                MOVE.W  \1,D1           ;HOLD THE VALUE OF THE OFFSET
                MOVE.W  #0,\1           ;SET THE OLD OFFSET FOR WRAPAROUND
                ADD.L   #6,A4           ;POINT TO QUEUE DATA
                MOVE.L  A4,\2           ;POINT TO THE START OF QUEUE DATA
                CMP.W   #QSIZE,D1       ;IS IT WITHIN 1..QSIZE?
                BGT     \@              ;SHOULD WE WRAP THE BYTE AROUND?
                MOVE.W  D1,\1           ;RESTORE THE VALUE OF THE POINTER
                ADD.W   \1,\2           ;OR INCLUDE OFFSET

* THIS CASE IS EXECUTED IF THE QUEUE HAS WRAPPED AROUND.  THE POINTER
* IN \1 IS THEN LEFT AT ZERO (AS IT WAS SET ABOVE) AND THE ADDRESS IS
* LEFT AT THE START OF THE QUEUE DATA (AS IT WAS SET IN A4.)

\@              MOVEM.L (A7)+,D1-D2/A4  ;POP STUFF OFF
                ENDM

****************************************************************
*
* THE PURPOSE OF THIS MACRO IS TO CONSUME .001 SECONDS BY
* LOOPING 280 TIMES.  THE PASCAL EQUIVALENT CODE IS:
*
* IT USES REGISTER D2 TO COUNT DOWN AND THUS DESTROYS ITS CONTENTS
*
* PROCEDURE UNITDELAY;
* VAR X: INTEGER;
* BEGIN
*       FOR X:=280 DOWNTO 1 DO {NOTHING};
* END;
*
***************************************************************

UNITDELAY       MACRO
                MOVE.L  #280,D2         ;COUNT DOWN FROM 280
\@              DBEQ    D2,\@           ;LOOP IF NOT DONE
                ENDM

****************************************************************
* THIS MACRO MULTIPLIES THE USER'S T BY TWO.  IT CAN AVOID THE
* LIMITS OF WORD MULTIPLICATION BY SHIFTING BY ONE BIT
* INSTEAD.  THE USER'S T IS PASSED IN D0.  THIS MACRO ALSO
* ALLOWS YOU TO INCREASE YOUR TIME DELAY EVEN IF DIVIDE_T
* HAS SET IT TO ZERO, SO THAT YOU CAN'T BECOME "STUCK."
* IT AFFECTS NO OTHER REGISTERS.
****************************************************************

MULTIPLY_T      MACRO
                ASL.L   #1,D0                   ;MULTIPLY D0 BY 2
                ENDM

******************************************************************
* THIS IS WRITTEN AS A MACRO TO AVOID PROBLEMS WITH THE REMAINDER
* IN THE UPPER WORD OF D0 AND TO SIMPLIFY THE MAIN PROCEDURE.
* THE MACRO SIMPLY DIVIDES THE USER'S DELAY BY TWO.
* IT USES NO OTHER REGISTERS THAN D0 (THE USERS'S DELAY)
******************************************************************

DIVIDE_T        MACRO
                ASR.L   #1,D0           ;DIVIDE D0 BY 2
                CMP.L   #5,D0           ;IS DELAY TOO SMALL?
                BGT     \@              ;IF NOT, SKIP DOWN
                MOVE.L  #5,D0           ;SET IT TO FIVE.
\@              NOP                     ;FINISH MACRO
                ENDM

***********************************************************************
****** THIS SUBROUTINE SETS UP THE TIMER TO BEGIN GENERATING
****** INTERRUPTS EVERY (TIMESLICE) CYCLES--WHEN AN INTERRUPT
****** OCCURS IT WILL TRAP TO THE CONTENTS OF VECTOR 15
****** TIMESLICE CAN BE ALTERED BY THE PERSON RUNNING THE PROGRAM.
* NOTE THAT TIMERINIT NO LONGER ENABLES THE TIMER: THIS IS DONE
* WHEN PROCESSES ARE ADDED SO THAT TIMER INTERRUPTS DO NOT OCCUR
* WHEN NO PROCESSES ARE RUNNING IN THE SYSTEM.
* IT CHANGES NO REGISTERS PERMANENTLY.
***********************************************************************

TIMERINIT       MOVEM.L A0/D0,-(A7)             ;PUSH
                MOVE.L  #NEWSLICE,TIMESLICE     ;INITIAL TIMESLICE
                MOVE.L  TIMESLICE,D0            ;NUMBER OF CYCLES
                MOVEA.L #CPR,A0                 ;COUNTER PRELOAD REGISTERS
                MOVEP.L D0,0(A0)                ;HAVE TO USE THIS INSTRUCTION
                MOVE.B  #26,TIV                 ;AUTOVECTOR NUMBER
                MOVE.L  #MULTIPLEXER,TIMERVECTOR        ;VECTOR FOR TIMER
                MOVEM.L (A7)+,A0/D0             ;POP
                RTS                             ;RETURN

***********************************************************************
****** THIS SUBROUTINE INITIALIZES THE ACIA1 AND SETS UP THE VECTOR
****** SO THAT WHEN AN INTERRUPT OCCURS THE EXCEPTION HANDLER ACIAHANDLER
****** WILL BE CALLED TO DETERMINE WHETHER IT IS RECEIVE (FROM THE SCREEN),
****** TRANSMIT (FROM THE KEYBOARD) OR OVERRUN THAT IS CAUSING THE INTERRUPT.
****** WHEN AN INTERRUPT OCCURS FROM THE ACIA IT TRAPS THROUGH THE 29TH
****** VECTOR OF THE TABLE.  IT CHANGES NO REGISTERS PERMANENTLY.
***********************************************************************

INITACIA        MOVE.L  D4,-(A7)        ;PUSH
                MOVE.B  #3,ACIACS       ;RESET ACIA
                MOVE.W  #$1000,D4       ;TIMING CONSTANT
WAIT            DBRA    D4,WAIT         ;PAUSE
                MOVE.B  #ACIAMODE,ACIACS        ;SET ACIA MODE
                MOVE.L  #ACIAHANDLER,ACIAVECTOR ;TRAP THROUGH VECTOR 29
                MOVE.L  (A7)+,D4                ;POP
                RTS

***********************************************************************
****** THIS PROCEDURE FIGURES OUT WHAT KIND OF ACIA INTERRUPT HAS OCCURED
****** AND CALLS WHATEVER IT HAS TO TO HANDLE THE SITUATION.  THE THREE
****** POSSIBLE SITUATIONS ARE RECEIVE INTERRUPT (FROM THE SCREEN READY
****** FOR ANOTHER CHARACTER), TRANSMIT INTERRUPT (WHEN A KEY HAS BEEN
****** PRESSED ON THE KEYBOARD), OR OVERRUN.  IN PRACTICE THIS ROUTINE
****** NEVER CALLS THE OVERFLOW.
* A "-" WILL DIVIDE THE DELAY VALUE BY 2, A "+" WILL MULTIPLY IT BY 2
* A "#" WILL SIGNIFY THAT THE PROCESS IS NOW DEAD.  THE PROCESS WILL RESET
* THE TIMER SO THAT IT DOESN'T HAVE TO WAIT FOR ITS TIMESLICE TO EXPIRE.
* THUS, WHEN YOU KILL A PROCESS THE NEXT ONE IS SWAPPED IN WITHIN A FEW
* CYCLES.  I GIVE THE TIMER 25 CYCLES BEFORE THE NEXT INTERRUPT HITS SO
* THAT IT GIVES ACIAHANDLER A CHANCE TO FINISH EXCEPTION PROCESSING AND
* RETURN TO THE DELAY LOOP BEFORE THE NEXT PROCESS IS SWAPPED IN.
* THE REGISTERS A5, D5, AND D6 ARE DESTROYED AND NOT RESTORED.
***********************************************************************

ACIAHANDLER     BLOCKINT

**********************************************************************
* THE FIRST PART OF THE CODE FIGURES OUT FROM THE STATUS BITS OF THE
* ACIA WHAT KIND OF INTERRUPT HAS OCCURED, AND JUMPS ACCORDINGLY.
**********************************************************************

                BTST.B  #0,ACIACS               ;IS IT FROM THE KEYBOARD?
                BNE     RECEIVE                 ;IF SO, RECEIVE A KEY PRESS
                BTST.B  #1,ACIACS               ;IS THE SCREEN READY FOR MORE?
                BNE     SEND                    ;IF SO, BRANCH TO SEND
                JMP     ENDHANDLER              ;RETURN IF SPURIOUS INT.

*************************************************************************
* THIS CODE HANDLES AN INTERRUPT FROM THE KEYBOARD:  STRUCTURALLY, IT
* IS A WHOLE SERIES OF IF-THEN STATEMENTS.  IT IS WRITTEN AS ONE LONG
* ROUTINE RATHER THAN SUBROUTINES TO SPEED UP EXECUTION TIME AND TO
* PREVENT THE STACK FROM BEING FILLED WITH SUBROUTINE CALLS.
*************************************************************************

RECEIVE         MOVEA.L PCBPOINTER,A5           ;HOLD ADDRESS OF POINTER
                MOVE.B  ACIAD,D6                ;STORE IT IN D4
                CMP.B   #'+',D6                 ;IF (KEY='+') THEN T:=T*2
                BNE     NOT_MULTIPLY            ;ELSE
                CMP.B   #0,(A5)                 ;IS IT A NULL PROCESS?
                BEQ     ENDHANDLER              ;IF SO, DON'T MULTIPLY

* CALL THE MACRO TO DOUBLE THE USERS DELAY *

                MULTIPLY_T

                JMP     ENDHANDLER              ;CONTINUE WHILE LOOP
NOT_MULTIPLY    CMP.B   #'-',D6                 ;IF (KEY='-') THEN DIVIDE T/2
                BNE     NOT_DIVIDE              ;ELSE
                CMP.B   #0,(A5)                 ;IS IT A NULL PROCESS?
                BEQ     ENDHANDLER              ;IF SO, DON'T DIVIDE
                DIVIDE_T

* CALL THE MACRO DO CUT THE USERS DELAY TIME IN HALF *

                JMP     ENDHANDLER

**************************************************************
* THIS CODE HANDLES THE DEATH OF A PROCESS.  TO BE "KILLED"
* A PROCESS SIMPLY HAS ITS ID CHAR SET TO NULL AND PCOUNT REDUCED
* BY ONE, AND THEN FORCE A CONTEXT SWITCH IN 25 CYCLES.
* NOTE THAT IF THERE ARE NO PROCESSES REMAINING, WE DON'T
* WANT TO DO ANY MORE SWAPPING, SO WE LOAD THE STACK WITH THE
* ADDRESS OF THE WAIT_ON_P LOOP AND WHEN THE PROGRAM HITS AN
* RTE THE LOOP IS JUMPED BACK TO.
**************************************************************

NOT_DIVIDE      CMP.B   #'#',D6                 ;IS PROCESS KILLED?
                BNE     CALL_ADDP               ;OR CHAR IS SOMETHING ELSE
                CMP.B   #0,(A5)                 ;ONLY KILL A LIVE PROCESS
                BEQ     ENDHANDLER              ;NOT A DEAD ONE
                MOVEA.L PCBPOINTER,A5           ;ADDRESS POINTED TO
                MOVE.B  #0,(A5)                 ;MARK PROCESS FOR DEATH
                SUB.B   #1,PCOUNT               ;DECREASE NUMBER OF PROCESSES
                CMP.B   #0,PCOUNT               ;ARE THERE ANY LEFT?
                BNE     SWAP_IN_NEXT            ;IF SO, SWAP IN NEXT ONE
                MOVE.B  #0,TCR                  ;TURN OFF TIMER
                MOVE.W  (A7)+,D5                ;POP OFF SR
                MOVE.L  (A7)+,A5                ;POP OFF PC
                MOVE.L  #WAIT_ON_P,A5           ;LOAD WITH ADDRESS OF WAITLOOP
                MOVE.L  A5,-(A7)                ;PUSH ALTERED PC
                MOVE.W  A5,-(A7)                ;PUSH ALTERED SR
                JMP     ENDHANDLER              ;RETURN

SWAP_IN_NEXT    MOVE.B  #0,TCR                  ;TURN OFF TIMER
                MOVE.L  #25,D5                  ;NUMBER OF CYCLES
                MOVEA.L #CPR,A0                 ;COUNTER PRELOAD REGISTERS
                MOVEP.L D5,0(A0)                ;HAVE TO USE THIS INSTRUCTION
                MOVE.B  #$A1,TCR                ;RESTART TIMER
                JMP     ENDHANDLER              ;RETURN

******************************************************************
* THIS CODE CALLS ADDPROCESS TO ADD A NEW PROCESS AND THEN FORCES
* A CONTEXT SWITCH WITHIN 25 CYCLES.  ADDPROCESS ITSELF TAKES CARE
* OF UPDATING THE PCOUNT IN CASE NO PROCESSES CAN BE ADDED.
******************************************************************

CALL_ADDP       JSR     ADDPROCESS              ;ADD A PROCESS WITH KEY=ID
                MOVE.B  #0,TCR                  ;TURN OFF TIMER
                MOVE.L  #25,D5                  ;NUMBER OF CYCLES
                MOVEA.L #CPR,A0                 ;COUNTER PRELOAD REGISTERS
                MOVEP.L D5,0(A0)                ;HAVE TO USE THIS INSTRUCTION
                MOVE.B  #$A1,TCR                ;RESTART TIMER
ENDHANDLER      UNBLOCKINT
                RTE

****************************************************************
* THIS CODE HANDLES AN INTERRUPT FROM THE SCREEN READY TO RECEIVE
* MORE INFORMATION.  IT CALLS DEQ TO REMOVE A BYTE OF TEXT FROM
* THE QUEUE AND THEN SENDS IT DIRECTLY TO THE ACIA DATA REGISTER.
****************************************************************

SEND            JSR     DEQ                     ;RETURN BYTE IN D7
                MOVE.B  D7,ACIAD                ;SEND BYTE TO SCREEN
                UNBLOCKINT
                RTE

**************************************************************************
****** THIS SUBROUTINE INITIALIZES THE QUEUE WHICH WILL HOLD LENGTH BYTES.
****** NOTE THAT THE QUEUE MUST BE ONE BYTE BIGGER THAN THE MAXIMUM NUMBER
****** OF BYTES IT CAN HOLD, SO THAT YOU CAN TELL WHEN THE QUEUE IS FULL.
**************************************************************************

INITQ           MOVEA.L #QUEUE,A6       ;A6 POINTS TO THE QUEUE STRUCTURE
                MOVE.L  A6,-(A7)        ;PUSH POINTER
                MOVE.W  #0,(A6)         ;SET HEAD TO ZERO
                ADD.L   #2,A6           ;POINT TO TAIL OFFSET
                MOVE.W  #0,(A6)         ;THE TAIL=HEAD=0 FOR NEW EMPTY QUEUE
                MOVE.L  (A7)+,A6        ;POP THE QUEUE POINTER
                RTS                     ;RETURN

**************************************************************************
****** THIS SUBROUTINES PUTS A BYTE INTO THE QUEUE
****** FIRST, IT INCREMENTS TAIL AND MODS IT TO FIGURE OUT WHERE THE
****** BYTE SHOULD GO.  THEN, IT COMPARES THE HEAD AND TAIL TO SEE IF
****** THE QUEUE HAS OVERFLOWED OR NOT.  THEN, IT USES A BASE ADDRESS
****** AND THE OFFSET TO CALCULATE WHERE IN MEMORY TO PUT THE BYTE
****** AND FINALLY, THE BYTE GOES IN THE QUEUE!
****** THEN, THE RECIEVE INTERRUPT IS FROM THE ACIA1 IS TURNED ON SO
****** THE TERMINAL CAN REQUEST CHARACTERS TO BE SENT TO IT.
****** THE CONTENTS OF THE ADDRESS REGISTERS A1,A2,AND A3 ARE DESTROYED.
**************************************************************************

ENQ             MOVEA.L A6,A1           ;COPY POINTER
                MOD     (A1),A2         ;FIND REAL ADDRESS OF HEAD
                ADD.L   #2,A1           ;POINT TO TAIL OFFSET
                ADD.W   #1,(A1)         ;INCREMENT OFFSET
                MOD     (A1),A3         ;GET THE REAL ADDRESS OF TAIL
                CMPA.L  A2,A3           ;HAS HEAD CRASHED INTO TAIL?
                BEQ     SHUTDOWN        ;EXIT IF IT HAPPENS
                MOVE.B  D7,(A3)         ;PUT THE BYTE IN THE QUEUE
                MOVE.B  #$B5,ACIACS     ;TURN ON TRANSMIT (SCREEN) INTERRUPTS
                RTS                     ;RETURN TO MAIN LOOP

**************************************************************************
****** THIS SUBROUTINE PULLS A BYTE OUT OF THE QUEUE.  HEAD IS INCREMENTED
****** AND THE CHARACTER IS RETURNED AS THE PARAMETER.  IF THE QUEUE IS
****** THEN EMPTY, THE INTERRUPTS FROM THE TERMINAL ARE TURNED OFF
****** SO THAT IT DOES NOT REQUEST ANY MORE CHARACTERS.
****** MOD IS USED SO THAT THE INCREMENTS ARE WRAPPED AROUND.
****** THE CONTENTS OF THE ADDRESS REGISTERS A1, A2, AND A3 ARE DESTROYED.
**************************************************************************

DEQ             MOVEA.L A6,A1           ;COPY POINTER
                ADD.W   #1,(A1)         ;INCREMENT HEAD
                MOD     (A1),A2         ;CALCULATE THE HEAD ADDRESS TO USE
                MOVE.B  (A2),D7         ;GET THE BYTE FROM THE HEAD OF QUEUE
                ADD.L   #2,A1           ;POINT TO TAIL OFFSET
                MOD     (A1),A3         ;CALCULATE TAIL ADDRESS TO COMPARE
                CMPA.L  A2,A3           ;IS THE QUEUE EMPTY?  (HEAD=TAIL)
                BNE     QNOTEMPTY       ;IF SO, TURN OFF ACIA INTERRUPTS
                MOVE.B  #$95,ACIACS     ;TURN OFF TRANSMIT (SCREEN) INTERRUPTS
QNOTEMPTY       RTS                     ;RETURN

******************************************************************
* THE MULTIPLEXER RUNS WHEN A TIMER INTERRUPT OCCURS.  THE STACK IS
* USED TO PLACE THE OLD SR AND SP FROM THE USER PROCESS WHICH WAS
* INTERRUPTED, AND THEN A7,D0,AND D1 ARE STORED.  NOTE THAT THE
* STACK POINTER IS STORED AS IT WAS WHEN THE INTERRUPT HANDLER
* BEGAN EXECUTING, SO THAT THE CONTEXT IS RESTORED PROPERLY BY
* THE RTE AT THE END OF THE HANDLER.  THE CIRCULAR DATA STRUCTURE
* IS WALKED THROUGH UNTIL THE NEXT ACTIVE PROCESS IS FOUND.
* NOTE THAT THIS MULTIPLEXER SHOULD NEVER BE CALLED IF THERE
* ARE NO ACTIVE PROCESSES, BECAUSE TIMER INTERRUPTS WOULD BE
* SHUT DOWN.
*****************************************************************

MULTIPLEXER     BLOCKINT
                MOVE.B          #$0,TCR         ;DISABLE TIMER
                MOVEA.L         PCBPOINTER,A3   ;LOAD ADDRESS HELD THERE
                MOVEA.L         A3,A5           ;COPY TO TEMPORARY POINTER
                ADD.L           #2,A3           ;MOVE TO STORAGE OF SR
                MOVE.W          (A7)+,(A3)      ;POP SR INTO PCB
                ADD.L           #2,A3           ;POINT TO PC
                MOVE.L          (A7)+,(A3)      ;POP OLD PC IN
                ADD.L           #4,A3           ;POINT TO STORAGE OF D0
                MOVE.L          D0,(A3)         ;COPY D0 INTO PCB
                ADD.L           #4,A3           ;POINT TO STORAGE OF D1
                MOVE.L          D1,(A3)         ;COPY D1 INTO PCB
                ADD.L           #4,A3           ;POINT TO STORAGE OF D2
                MOVE.L          D2,(A3)         ;COPY D2 INTO PCB
                ADD.L           #4,A3           ;POINT TO NEXT PCB

*********************************************************************
* THIS PORTION OF THE CODE WALKS THROUGH THE PCBS TO FIND THE NEXT
* ONE (CIRCULARLY) WHICH IS ACTIVE AND RESTORES IT.
************************************************************************

LOOK            MOVEA.L         #PCBPOINTER,A4  ;COPY POINTER'S ADDRESS
                CMPA.L          A3,A4           ;HAVE WE HIT BOTTOM?
                BNE             IS_IT_ACTIVE    ;IF NOT, TEST FOR ACTIVE
                MOVEA.L         #PCBTOP,A3      ;IF SO, WRAPAROUND
IS_IT_ACTIVE    CMP.B           #0,(A3)         ;IF NOT,
                BEQ             KEEP_LOOKING    ;KEEP LOOKING
                JMP             RESTORECONTEXT  ;OR ELSE SWAP IT IN
KEEP_LOOKING    ADD.L           #20,A3          ;POINT TO NEXT PROCESS
                JMP             LOOK            ;LOOP AROUND

***********************************************************
* THIS CODE RESTORES THE CONTEXT OF A PROCESS FROM THE PCB
***********************************************************

RESTORECONTEXT  MOVE.L          A3,PCBPOINTER   ;UPDATE POINTER
                ADD.L           #4,A3           ;POINT TO STORAGE OF PC
                MOVE.L          (A3),-(A7)      ;PUSH OLD PC
                SUB.L           #2,A3           ;POINT TO STORED SR
                MOVE.W          (A3),-(A7)      ;PUSH OLD SR
                ADD.L           #6,A3           ;POINT TO STORAGE OF D0
                MOVE.L          (A3),D0         ;RESTORE D0
                ADD.L           #4,A3           ;POINT TO STORAGE OF D1
                MOVE.L          (A3),D1         ;RESTORE D1
                ADD.L           #4,A3           ;POINT TO STORAGE OF D2
                MOVE.L          (A3),D2         ;RESTORE D2

*********************************************************************
* THIS CODE FINISHES UP AND RETURNS CONTROL TO THE NEXT USER PROCESS
*********************************************************************

CONTINUE        MOVE.L  TIMESLICE,D4    ;NUMBER OF CYCLES
                MOVEA.L #CPR,A5         ;COUNTER PRELOAD REGISTERS
                MOVEP.L D4,0(A5)        ;HAVE TO USE THIS INSTRUCTION
                MOVE.B  #$A1,TCR        ;ENABLE TIMER
                UNBLOCKINT
                RTE                     ;CONTINUE EXECUTION OF USER PROCESS

**********************************************************************
************** ADD PROCESS ADDS A PROCESS TO THE LIST ****************
* THIS SUBROUTINE IS CALLED WHEN A KEY BESIDES THE +,-, OR # IS PRESSED.
* IT WALKS THROUGH THE LIST OF PCBS UNTIL IT FINDS A NULL PROCESS OR
* HAS WALKED ALL THE WAY AROUND WITHOUT FINDING ONE.  IF IT FINDS A
* NULL PROCESS THE CHARACTER OF THE KEY PRESSED IS MADE THE ID CHAR OF
* A NEW PROCESS AND THE PROCESS IS INSERTED INTO THE PCB LIST.
**********************************************************************

ADDPROCESS      MOVEA.L PCBPOINTER,A3           ;LOAD ADDRESS
                MOVEA.L #PCBPOINTER,A4          ;MEMORY LOCATION
                MOVEA.L A3,A5                   ;COPY TO TEMPORARY POINTER
WRAP            CMPA.L  A3,A4                   ;HAVE WE HIT BOTTOM
                BNE     IS_IT_USED              ;IF NOT CHECK FOR ALL FULL
                MOVEA.L #PCBTOP,A3              ;OR ELSE WRAPAROUND
IS_IT_USED      CMP.B   #0,(A3)                 ;IS PROCESS USED?
                BEQ     ADDIT                   ;IF NOT, ADD HERE
ARE_ALL_USED    ADD.L   #20,A3                  ;POINT TO NEXT PCB
                CMPA.L  A3,A5                   ;HAVE WE GONE AROUND?
                BNE     PCB_LOOP                ;IF NOT, CONTINUE
                RTS                             ;OR QUIT WITHOUT ADDING
PCB_LOOP        JMP     WRAP                    ;CONTINUE SEARCH
ADDIT           MOVE.B  D6,(A3)                 ;SET PROCESS ID
                ADD.B   #1,PCOUNT               ;PCOUNT:=PCOUNT+1
                ADD.L   #8,A3                   ;POINT TO DELAY IN PCB
                MOVE.L  #DELAY1,(A3)            ;STORE INITIAL DELAY=1 SEC.
                CMP.B   #1,PCOUNT               ;IS THIS THE FIRST PROCESS
                BNE     DOWNHERE                ;IF NOT, DON'T SET D0
                MOVE.L  #DELAY1,D0              ;SET UP D0 FOR FIRST DELAY
DOWNHERE        RTS                             ;RETURN TO ACIAHANDLER

**********************************************************************
* MAIN PROGRAM
* THIS FIRST SETS UP THE PCBPOINTER, THEN THE QUEUE, THEN THE ACIA,
* THEN THE TIMER, THEN UNBLOCKS THE INTERRUPTS AND ENTERS
* THE LOOP TO WAIT FOR THE FIRST PROCESS TO BE INVOKED BY ADDPROCESS.
**********************************************************************

START           MOVEA.L #$7000,A7       ;SET STACK POINTER TO $7000
                JSR     INITQ           ;SET UP THE I/O QUEUE
                JSR     INITPCBS        ;SET UP THE 16 PCBS IN RAM
                JSR     INITALINE       ;SET UP THE A-LINE EMULATOR
                JSR     TIMERINIT       ;START TIMER INTERRUPTS
                JSR     INITACIA        ;SET UP THE ACIA

                MOVE.B  #$95,ACIACS     ;TURN ON RECEIVE (KEY) INTERRUPTS
                UNBLOCKINT

***********************************************************************

WAIT_ON_P       CMP.B   #0,PCOUNT       ;ARE THERE NO PROCESSES?
                BEQ     WAIT_ON_P       ;IF SO, LOOP HERE

***********************************************************************
* THIS IS THE SHARED CODE:  IT CONSISTS OF THE DISPLAY MACRO (WHICH IS
* A SYSTEM CALL) AND THE CODE FOR THE DELAY PROCEDURE.  THE REGISTERS
* USED BY THE MAIN PROCEDURE ARE D0, D1 AND D2 FOR THE CONSTANT DELAY,
* OUTER LOOP DELAY TIMER, AND UNITDELAY TIMER.  THESE REGISTERS ARE
* STORED WHEN THE PROCESS IS SWAPPED IN AND OUT.
***********************************************************************

WHILELOOP       DISPLAY                 ;CALL THE A-LINE HANDLER TO DISPLAY

********************************************************************
* THIS IS THE CODE FOR DELAY.  I AM LEAVING IT IN THE MAIN PROCEDURE
* SO THAT THE CONTEXT SWITCHER WON'T HAVE SUBROUTINE CALLS ON THE
* STACK TO CONTEND WITH, AND SO EACH PROCESS CAN RESUME PROPERLY.
* SEPARATE STACKS WOULD BE EASIER WITH A MEMORY MANAGEMENT UNIT.
********************************************************************

                MOVE.L  D0,D1           ;COPY T TO LOCAL
DELOOP          UNITDELAY               ;CALL UNITDELAY
                DBEQ    D1,DELOOP       ;REPEAT T TIMES
                JMP     WHILELOOP       ;GO BACK TO DISPLAY: REPEAT

******************************************************************
* SHUTDOWN IS CALLED WHEN THE QUEUE OVERFLOWS.
******************************************************************

SHUTDOWN        MOVE.B  #$0,TCR         ;DISABLE TIMER
                MOVE.B  #229,D7         ;RETURN CONTROL
                TRAP    #14             ;TO TUTOR
                END     TOP             ;END OF THE PROGRAM

Portfolio IndexWriting Archive