A Unix Like Operating System for 6809 Microprocessors Part II
Table of Contents
by Stephen L. Childress
from the July 1983 issue of Micro magazine
(Note: The figures are not available for this article because they are impossible to read on my source.)
The modularization of the I/O system allows OS-9 to enhance the standard I/O at run time, not assembly or patch time. Device names and addresses are not fixed by the operating system but, rather, the program may attempt I/O to any device name. Of course, an appropriate module of that name must be loaded and ready to go at that time. Device names, driver procedures, register addresses, peripheral idiosyncracies, etc., are stated outside the core of the operating system and may be extended with ease. A device driver and descriptor for any data acquisition device may be loaded and accessed by programs using these techniques. In fact, should RBF and SCF fail to meet the needs of some device, an entirely new manager may be loaded and used alongside RBF and SCF. An example of this might be a 9-track tape drive that is not random-block oriented but is more than a character-oriented device. Perhaps an SBF (sequential block manager) module would be best:
This named-module concept is quite different from the old-school conventions, and it takes a while for the merits of the scheme to become apparent. The key point is that the modular software concept avoids considering where a piece of code is in memory until run time (not design time, assembly time, or link-load time). Although the name-to-address conversion takes a while, the module look-up directory makes the time acceptable.
You may wonder how modules can be expected to float around in memory . Won’t machine instructions like JMP and JSR become confused? They would on an 8080 micro, but the 6809 uses relative addressing for branching, including long-distance branches and subroutine calls. Relative addressing is permitted on all instructions including those that access constants like numbers and strings. Since programs are assigned memory for variables (RAM) at run-time when invoked by KERNAL rather than the old way (assembly time), the four 16-bit index registers of the 6809 access data workspace without the programmer knowing where it will be.
After years of working the old way, these ideas took some time to register with me. Look at the tiny assemblylanguage program for OS-9 in figure 1. It should help you understand how position-independent module code works.
The code in this module is completely position-independent; i.e., it may be placed anywhere in memory and executed without link-loading or other preparatory adjustments, thanks to the 6809’s powerful addressing. Look at the listing for a moment to understand how the code becomes modular and position-independent. You see the familiar ORG (origin) directive, but it means something quite different in the OS-9 system: ORG sets up a “data section” of the program, which contains only memory reservations (RMBs).
The symbols VARA and VARB just after the ORG statement take on the values of zero and one, respectively; RMB produces no code. These numbers (actually the symbols VARA and VARB I will be used as offsets into the data storage area, whose exact address is not known at the moment. Remember, OS-9 assigns a data area [RAM region) at run time.
Now the MOD statement creates a module header, which contains all of the information needed to determine the module’s name and attributes. The MOD statement introduces the ’ ‘purecode" part of a module, which should not contain storage for variables. When the KERNAL activates this module, the 6809’s registers are set up as described in the listing’s comments.
How do you address the variables? The 6809’s U (index) register contains the address of the data area you will be using at the time the program runs. By now it should be obvious that each module may be given (by KERNAL) a unique piece of RAM for its variables. In this module, MEM in the header states that the module needs 203 hex bytes, minimum, of data area. The KERNAL ’s memory-management routine simply locates 203 bytes (actually, memory is handed out in multiples of 256 bytes) of unused memory and places that address in the U register before running the module.
Now consider the stack for the module; since the 6809 has a general 16-bit stack pointer register (S), the KERNAL simply sets S to the address of the end of the data area for the module. Thus, the module gets a private stack, unique from other modules’ stack space. Why separate stacks? For time sharing. There may be several modules active simultaneously, each contending for CPU time as handed out by the KERNAL’s scheduler. To switch modules, the KERNAL merely preserves the currently executing module’s registers, loads up the new module’s registers, and executes the new module starting where it last was when its time slice expired (due to a clock interrupt). Thus, time sharing (or multi programming) is made practical on even a small computer with only 64K. Each program uses just enough memory for its modules’ code and a second region of memory for as many variables as needed. Therefore, many small modules or a fewer number of large modules may be run simultaneously.
Look again at the code in listing 1 to understand how to avoid ever using absolute memory addresses. Now you know addressing of variables merely uses an index register to point to a data area. But what about constants, which are imbedded in a module’s pure code? The 6809’s relative addressing mode comes to the rescue. In the listing, program is to send an ASCII message string to the user; it’s labelled MSG, but remember that the location of the module in memory is not known until nm time. The old way of getting the address of MSG is: LDX #MSG (or equivalent 8080/Z-80 code, etc.), which would place the value of MSG in the X register. But this is just the address shown in the listing, not the address at which MSG falls according to the module’s address in memory. So the 6809’s LEAX (Load Effective Address into X) instruction is used instead of LDX. The LEAX instruction contains (in bytes(s) following the opcode) the distance from the instruction to the label (MSG). When the LEAX instruction is executed, the 6809 chip does the following:
-
Gets the distance part of the LEAX instruction, which follows the opcode in one or two bytes.
-
Adds that distance value (which may be negative) to the current PC (program counter).
-
Places the result in the X register (16 bits).
After step 3, the X register has the actual memory address of MSG ready to ship off in a call for I/O to OS-9 (the OS-9 ISWRLN statement). The load effective address mode in the 6809 (we looked only at the PCR, PC-relative case) allows actual, absolute memory addresses to be determined easily at run time and ignored at assembly time.
Here are a couple of other key points shown in the listing: in order to be unaware of the addresses of the KERNAL’s support for I/O calls, etc., all OS-9 system calls are made using the “OS-9” assembler directive. This merely produces a SW12 (software interrupt, trap, or whatever you wish to call it), which uses an address vector that the KERNAL has set up. The desired KERNAL action is stated by placing a code in the byte following the SWI2 instruction. In the listing example, the ISWRLN and FSEXIT codes are used. There are several dozen KERNAL function codes upon which to draw, each using the techniques shown for passing parameters in the registers. The point is that the KERNAL itself is a module like all the others and its address may be ignored.
Finally, the I/O performed (ISWRLN) by the module was done using a “path” number passed in a register. Like Unix, OS-9 programs are assigned three I/O paths at the time they are run. These are called the “standard paths” since few programs need more than three I/O “channels.” When the user (or some other program) causes a module to be executed, the KERNAL sets up these three paths in a careful manner. Path zero is called the “standard input” and, for the case where a user invoked the module from a terminal, is associated with the terminal’s keyboard. Paths one and two are the “standard output” and “standard error output,” normally the user’s terminal screen (or paper). The actual devices on the other end of these I/O paths are not known to a module [but may be determined if needed). Delightfully simple, a program merely reads on path and writes on paths 1 and 2, completely ignoring what device is there.
Thinking back to the OS-9 block diagram shown in part I, the user’s terminal often is affiliated with the SCF and ACIA modules that are handling I/O for the module. Each module activated by KERNAL is given paths 0, 1, and 2 for standard I/O, though path in module A may be a different device than path in module B. None the less, the modules are unconcerned with what’s on the other end of the paths. Additional I/O paths to files or devices may be established at run time by any program.
Incarnations
Let’s step back now and look at how these ideas all blend for the user’s benefit. As an example, consider that two terminals are attached to the computer and each person wants to run BASIC. BASIC 1BASIC09 for OS-9) is merely a large module that has all of the module conventions sho\vn for the simple example. After booting up the system, the primary terminal is activated by automatically running the SHELL program module with the three standard I/O paths set up for the primary terminal, TERM. The user sitting at TERM’S keyboard types in a command to invoke module SHELL with the I/O paths set for the second terminal, Tl. Invoking a program causes the KERNAL to create a new “process”. The time-sharing scheduler of the KERNAL gives each process the CPU for a time slice, then switches to another process. In this example there are two processes bidding for CPU time, both of which happen to be running the same program module, SHELL. SHELL, like any other module, is pure code and its variables are in a data space assigned by the KERNAL. The user at the TERM terminal and the user at the Tl terminal are, to OS-9, two separate processes, each having an assigned data area. Both processes are running the SHELL module’s code; the same code is used for both processes, not two copies. As these two processes alternately get CPU time, they run sections of the code in SHELL, though not in lock-step. When a clock interrupt causes the KERNAL to switch processes, the PC for the interrupted process is one of the items preserved for recall when the process is later reactivated. Thus, the two processes running SHELL each march through the code in SHELL but have independent data and stack areas as well as I/O devices.
Since the two terminals are both running SHELL now, each user may instruct SHELL (via the keyboard) to run BASIC. The KERNAL dutifully handles the first user’s request to execute module “BASIC09”, copies it from disk to memory, and activates the BASIC09 module with a data area and the I/O devices for that user. When that user’s (process) time slice expires (typically 1/10 of a second), the other user’s process comes up and his request for BASIC is handed to the KERNAL. KERNAL discovers that BASIC09 is already in memory, so it simply activates BASIC09 a second time with the other user’s data area and I/O devices. Now two people are sharing one copy of BASIC, each with private data areas and I/O devices. To emphasize that only one copy of the pure code within BASIC09 is present you could say that there are two incarnations of BASIC09 occurring. Time sharing makes each user feel as though he has his own CPU.
The time-sharing and Unix-like schemes I have shown here have been available previously only in minicomputer systems and presumed to be far beyond the means of the microcomputer. But they do work in OS-9 because:
-
OS-9 hands out memory in 256-byte chunks to minimize waste.
-
All code is “pure” so that you never need two copies of the same program in memory.
-
The 6809’s addressing modes make handling separate data and programcode areas trivial.
-
All code is position-independent so that it may run anywhere without awkward and slow relocation.
-
The Unix I/O path philosophy allows programs to ignore exactly what devices it is working with. When required, a program can determine the device characteristics (file versus CRT, CRT controls, etc.) for special applications like screen forms and, of course, open disk files on other paths.
All This on a 64K Micro?
Study the features I’ve discussed to see whether or not they really are practical on a 64K computer [OS-9 also supports the SS50 machines with 20-bit bus addresses for 1Mb memories). For example: if you have one user who runs the LIST utility to copy a file to a printer and he/she simultaneously runs another program to edit a text file, these activities require;
LIST - 256 bytes of code, 512 bytes of data area.
EDIT - 8K bytes of code, 16K bytes of data area.
This is 8192 + 16384 + 256 + 512 of memory = 25,344 bytes. There is ample room for this, considering the size of the system software (KERNAL, IOMAN, et. aL, as shown in the MDIR hsting in part I) . In fact, a second person may share EDIT with a new data area, or several more incarnations of LIST could run if other printers or devices are required. Since LIST is running concurrently, you have a print spool going without a $400 spool box.
The physical use of memory, in general terms, would be (approximately) :
Mem Area Utilization
AOOO-FFFF OS-9 system code modules: KERNAL, I/O drivers, etc. BFOO-BFFF Code module LIST, 0.25K 9F00-BEFF Code module EDIT, 8K 5700-9EFF (unused memory) 0600-46FF Data area for EDIT, 16K 0400-05FF Data area for LIST, 0.5K 0000-03FF Data area for system modules
As you can see, there is ample unused memory for other concurrent programs (perhaps other users) to claim. Also, you could use a program larger than EDIT and still have room to spare.
In another instance, there might be two users, each running BASIC with relatively small programs (BASIC09 is an interactive compiler so program storage requirements are small). In this case, there would be a 2 IK area for BASIC and, say, two 8K areas for programs, for a total of 37K. If the collection of I/O drivers and other system software in use at the time were, say, 24K, the two-user BASIC case requires 24 + 37K = 61 of the 64K. This is a tight fit, but each user can run a sophisticated program in 8K of program space (about 250 lines of code, which is about a 5-page listing of BASIC code). If one of those users is running a program with large data requirements, such as big arrays, both users might not be able to use the system concurrently. Considering the highly sophisticated capabilities of BASIC09, it is indeed practical to run two users for many jobs on a small 64K computer. Clearly, running smaller programs like editors, assemblers, print-listers, and applications programs is much less demanding.
Interrupts — Immoral for Micros?
Surprisingly, few micros use interrupts, probably because of manufacturers’ concerns about irregular schemes used by some peripheral vendors. Microware took a rather bold step and enabled interrupts within QS-9. With the reentrant module code, this turns out to be quite easy. But the clincher is that the serial I/O driver (ACIA) is designed expressly to handle CRT input and output via interrupts. For years the I/O boards have tied the interrupt request from the I/O chips (UARTs) to the bus IRQ line, but the operating systems never used interrupts! In OS-9, the ACIA driver accepts interrupts from the keyboard(s) and takes in characters when you type them. The keystrokes are buffered (up to 80 or more) by ACIA for each terminal. When the application program has time to read from the keyboard, ACIA delivers the characters thus far accumulated or, if none are buffered up, waits for more to arrive. The program is completely unaware of all this and may call for characters by ones or by lines. For output, ACIA accepts characters from a program and sends out one per interrupt to the terminal. If the program gets ahead of the device, in order to catch up ACIA puts the program to sleep for a short time. These same techniques may be done for parallel interfaces via the standard PIA driver.
The interrupt-driven I/O makes a profound difference in the usefulness of the system. In the SS50 machines, the serial cards contain the MC6850 UART, which has an interrupt request pin that is simply tied to the wire-or bus IRQ line — there are no vectors in hardware or other exotic conditions. How does OS-9 decide who is interrupting? Simple; each I/O driver, at startup time, tells the I/O manager (IOMAN) that it has devices that will interrupt, and states the address of each device’s status register and which bit signifies a “done” condition. The interrupt sequence then goes like this:
-
Device has or needs data and sets IRQ on the bus true.
-
The 6809 does an interrupt sequence, stacking all of the registers and vectoring to a routine in IOMAN.
-
IOMAN goes through the list of devices, which it now knows may interrupt, and finds some device whose “done” bit is true.
-
IOMAN then branches to that device’s I/O driver, say ACIA, passing the address of the data area for that device (which was set up when the device was made known to OS-9).
-
The driver looks at the device’s status register to decide what to do: input, output, error condition, etc.
-
The driver then tells lOMAN to return to the interrupted program.
-
IOMAN and KERNAL decide either return to the interrupted program or perhaps to switch processes.
Essentially lOMAN and the KERNAL make an I/O driver believe that the machine has a fancy vectored (and prioritized) interrupt system. Again, this simplifies the drivers. The ACIA driver does everything “right”. For example, your keystrokes are accepted but not echoed back to the screen until some program actually reads the characters. This keeps the screen in order regardless of whether the user is ahead or behind the program’s state. Also, if you fill up the keyboard buffer, ACIA sends a beep to the terminal to tell you to wait a moment. The usual “stop scroll,” “abort program,” “no echo,” and other special keys are provided for also.
Many drivers may be written to use the periodic clock interrupts to poll the device status at some 100 times per second. This is convenient for controlling devices that cannot, due to the interface design, produce interrupts. But what about an old disk interface that uses programmed I/O (not DMA)? The driver for these disks will hog a lot of CPU time (one or two revolutions of the disk at 300 or so RPM) but, happily, interrupts may be left on while doing the long, slow seek; unless the user is blazing away on his keyboard, you don’t suffer too much. And DMA-based disk controllers are the norm now, with many fancy LSI chips for DMA and disk control. The controllers for Winchester hard disks are so smart that the CPU time for servicing these is much less than for a floppy.
You can see that many of the features of the modem minicomputer operating system software are indeed feasible for even 8-bit microcomputers. OS-9 will run on an Apple II or a $3500 SS50 machine. At the time this was written, there was well-founded speculation that Tandy will offer OS-9 for the Color Computer. And why not?