MiniOS, Some Working, but Oversimplified Code

The File System

The MiniOS supports a rudimentary file system for demo purposes. You can do much better. The Make Floppy button on the SandBox emulator takes the text file output by the OSL compiler, and builds a 1-file volume in the format described in this section. For more information on extending the MiniOS file system, see File System Design Criteria.

There are a few necessary parts to a disk file system.

1.  You need some way to give disk files names
2.  You need to be able to create new (named) files programmatically.
3.  You should be able to open and close files.
4.  You should be able to read and write concurrently from different open files.
5.  The system should extend naturally to other non-disk devices.
Other features would be nice (and most operating systems offer them), but they are not in this minimal toy:
6.  You should be able to extend a file by re-opening it and writing additional data, after it has been created and closed.
7.  You should be able to create and open more than one file at a time (we can read more than one file, but write only one).
8.  Hierarchical directories are nice.
9.  A disk should be able to hold thousands of files, not just 7 or 23.

Data Structures

We allocate the first four 256-byte sectors for the directory, but only the first sector is actually used. Each file takes exactly 32 bytes in the directory, so there is room for 7 files, plus some volume information.

The first 8 bytes of the first sector is a signature for quality control purposes, "HardDisk". Then there are four 2-byte numbers:

 +0  Signature "HardDisk" or "DiskFlop"
 +8  Reserved for a boot sector number (not currently used)
+10  The next unused sector number. On a properly formatted empty disk, this would be 4.
+12  The number of files on this volume (= the number of entries in the directory).
+14  The maximum number of files this directory can hold (7).

Each file entry in the directory has this format:

 +0  Starting sector for this file
 +2  File length in bytes (max 65534)
 +4  File name (null filled)

MiniOS keeps in memory a 4-entry table of currently open files, each with four numbers:

 +0  The file number in the directory (1-7), or else a negative device number
 +2  The next sector to be read or written
 +4  The byte offset in the current sector, 0-255; this is the next byte to be read or written.
 +6  The remaining file size unread (initially the size from the directory, or FFFF if creating a new file)
 

Memory Manager

This version of MiniOS has some rudimentary test code to verify that the MMU hardware is actually working. Another version actually uses the MMU to load a single program into the otherwise inaccessible central part of main memory and run it there. There are also some additional remarks in the Info file on setting up the MMU pointers. Please look at these resources, then tell me if you still have trouble understanding them.

In this listing, the DefaultTable is probably most confusing. It (implicitly) and the PageTable are both defined to be int[32], where each integer holds two bytes. The first byte is most significant (SandBox is defined to be "Big-Endian" so that the lowest address is at the big end of the number, that is numbers read left-to-right, English style), so it is multiplied times 256 to place it 8 bits to the left. In the stack and data part of the table, all the entries are offset by -32640, which is hex 0x8080. Since there are two one-byte entries in each int, each byte has its own write-enable bit (the 8 in 0x8080). Note that the final page is the I/O registers, so it is writable, while the code pages immediately below it are not. Some of the entries are also swapped with the next row, as a way of proving that it really is working. A better proof is the March 25 assignment demo.
 

MiniOS Listing

// Mini OS, Semaphores Demo and MMU Test added -- 2004 March 9

import Files;
import Semaphores;
import Hardware;
import Dangerous;

// ==============================================================
// Global data, used as indicated..

// Semaphore Demo data..
final int Qsize = 4;
int[32] DemoFrame;  // stack frame for second process
char[Qsize] queue;  // the buffer
int fx=0, tx=0; // fill and take buffer indexes
Semaphore fill, take; // their semaphores

// MMU Test data..
int[32] PageTable;
final int[] DefaultTable = { 0*256-32640+1, 2*256-32640+3,
  04*256-32640+05, 22*256-32640+23, 24*256-32640+25, 10*256-32640+11,
  12*256-32640+13, 14*256-32640+15, 16*256-32640+17, 18*256-32640+19,
  20*256-32640+21, 06*256-32640+07, 08*256-32640+09, 26*256-32640+27,
  28*256-32640+29, 30*256-32640+31,  96*256+97, 98*256+99,
  100*256+101, 102*256+103, 104*256+105, 106*256+107,
  108*256+109, 110*256+111, 112*256+113, 114*256+115,
  116*256+117, 118*256+119, 120*256+121, 122*256+123,
  124*256+125, 126*256+255 };

// Interrupt handler process stack frames..
int[16] MMUFrame;    // stack frame for MMU interrupt
int[16] ClockFrame;  // stack frame for Clock interrupt
int[16] DiskFrame;   // stack frame for Disk interrupt
int[16] KeyIntFrame; // stack frame for KeyIn interrupt
int[16] PrintFrame;  // stack frame for Print interrupt
int[16] Sema4Frame;  // stack frame for Signal/Wait interrupts
int[80] SysCallFrame; // stack frame for SysCall interrupt;
  // it's extra big because the SysCall handler calls lots of
  //   nested subroutines, which might collide with the next frame

// Data used by the file system..
int[16] ActiveFiles; // the 4 files you can have open, 4 ints each
final int Directory = 1024; // start of disk dir
  // watch for possible collision between this and main() stack
final int BuffersAt = Directory+256; // 256-byte buffers for files 1-4

// Memory allocator data..
int HeapTop = 32766; // allocates down from here..
final int HeapBottom = BuffersAt+1024; // .. to here

// Keyboard and Printer data..
int KeyFillPtr = BuffersAt, KeyTakePtr = BuffersAt,
  KeyShowPtr = BuffersAt; // initialize them to empty buffer
int GotRetns = 0; // the number of CRs in KeyIn buffer
boolean PrintReady = true; // =false between '\r' and interrupt

// Timer data..
int theTicks; // used by clock interrupt for Delay

// Process manager data..
int ReadyList = 0; // list of waiting processes
int CurProcess = 0; // whatever is currently running
int[8] MainPIB; // to make main() look like another process
int[] AppPIB, AppCode; // dynamically allocated for demo

// User-level file data..
FileId Kin, Cout; // KeyIn and ScreenOut, always open
String KeyString; // used by ReadStr() to buffer a line
final int[] MyName = { ORD('M')*256+ORD('i'), ORD('n')*256+ORD('i'),
   ORD('O')*256+ORD('S') }; // "MiniOS", the name of this file

// Video data, used within the system code..
int Vrow = 1, Vcol = 2; // current display V and H
final int TextColor = DoText+white;
final int TextBgnd = FillRect+minRed+minGreen+minBlue;
  // dark gray fill

final int[] FontBits = { 0, 0, 0, 0, 0,   0, 0, 190, 0, 0, // sp
   0, 14, 0, 14, 0,          40, 124, 40, 124, 40,
   76, 146, 511, 146, 100,   66, 37, 146, 328, 132,
   96, 150, 137, 118, 208,   0, 0, 14, 0, 0,
   0, 124, 130, 257, 0,      0, 257, 130, 124, 0,
   8, 42, 28, 42, 8,         16, 16, 124, 16, 16,
   0, 512, 448, 192, 0,      16, 16, 16, 16, 16,
   0, 192, 192, 0, 0,        0, 384, 96, 24, 6,
   56, 68, 130, 68, 56,      0, 4, 254, 0, 0,              // 0
   132, 194, 162, 146, 140,  68, 130, 146, 146, 108,
   48, 40, 36, 254, 32,      78, 138, 138, 138, 114,
   120, 148, 146, 146, 96,   2, 2, 226, 26, 6,
   108, 146, 146, 146, 108,  12, 146, 146, 82, 60,
   0, 204, 204, 0, 0,        0, 716, 460, 0, 0,
   0, 16, 40, 68, 130,       40, 40, 40, 40, 40,
   0, 130, 68, 40, 16,       4, 2, 162, 18, 12,
   120, 132, 306, 330, 60,   224, 56, 38, 56, 224,         // @A
   130, 254, 146, 146, 108,  124, 130, 130, 130, 68,
   130, 254, 130, 68, 56,    254, 146, 146, 130, 130,
   254, 18, 18, 18, 2,       124, 130, 130, 146, 244,
   254, 16, 16, 16, 254,     0, 130, 254, 130, 0,
   96, 128, 128, 128, 126,   254, 16, 40, 68, 130,
   254, 128, 128, 128, 128,  254, 4, 24, 4, 254,
   254, 4, 24, 32, 254,      124, 130, 130, 130, 124,
   254, 18, 18, 18, 12,      124, 130, 130, 386, 636,      // P
   254, 18, 50, 82, 140,     76, 146, 146, 146, 100,
   2, 2, 254, 2, 2,          126, 128, 128, 128, 126,
   14, 48, 192, 48, 14,      30, 224, 24, 224, 30,
   198, 40, 16, 40, 198,     6, 8, 240, 8, 6,
   194, 162, 146, 138, 134,  0, 0, 511, 257, 0,
   0, 6, 24, 96, 384,        0, 257, 511, 0, 0,
   4, 2, 1, 2, 4,            128, 128, 128, 128, 128,
   0, 2, 4, 0, 0,            64, 168, 168, 168, 240,       // `a
   254, 136, 136, 136, 112,  112, 136, 136, 136, 136,
   112, 136, 136, 136, 254,  112, 168, 168, 168, 48,
   16, 252, 18, 2, 4,        112, 648, 648, 648, 504,
   254, 8, 8, 8, 240,        0, 136, 250, 128, 0,
   512, 520, 506, 0, 0,      254, 16, 48, 72, 128,
   0, 2, 254, 0, 0,          248, 8, 240, 8, 240,
   248, 16, 8, 8, 240,       112, 136, 136, 136, 112,
   1016, 136, 136, 136, 112, 112, 136, 136, 136, 1016,     // p
   248, 16, 8, 8, 16,        144, 168, 168, 168, 72,
   0, 8, 126, 136, 128,      120, 128, 128, 128, 248,
   8, 48, 192, 48, 8,        56, 192, 48, 192, 56,
   136, 80, 32, 80, 136,     536, 608, 384, 96, 24,
   136, 200, 168, 152, 136,  0, 16, 238, 257, 0,
   0, 0, 511, 0, 0,          0, 257, 238, 16, 0,
   4, 2, 2, 4, 2 };

// ==============================================================
// Video driver subroutines, for use by the system only..

// This sets a simple rectangular clipping region
//  for concurrent programs sharing the screen by tiling..
void Clipper(int T, int L, int B, int R) {
  Video.top = T; // set clip region for this process
  Video.left = L;
  Video.bottom = B;
  Video.right = R;
  Video.datum = ClipRect;} // ~Clipper

// Erase next line, used when an output string has \r in it..
void ScreenLn() { // start new line, and clear it..
  Vcol = 2;
  Vrow = Vrow+12;
  if (Vrow>469) Vrow = 1;
  Video.left = 0;
  Video.right = 640;
  Video.top = Vrow-1;
  Video.bottom = Vrow+12;
  Video.datum = TextBgnd;} // ~ScreenLn

// Erase previous char, used when an output string has \b in it..
void ScreenBS() {
  if (Vcol<8) return; // nothing to back up over
  Video.right = Vcol;
  Vcol = Vcol-6;
  Video.left = Vcol;
  Video.top = Vrow;
  Video.bottom = Vrow+12;
  Video.datum = TextBgnd;} // ~ScreenBS

// Display one character..
void ScreenChar(char ch) { // display one character on current line
  if (ORD(ch)==13) ScreenLn();
  if (ORD(ch)==8) ScreenBS();
  if (ch<' ' || ch>'~' || Vcol>633) return; // not printable
  Video.address = ORD(ch)*10-320+&FontBits; // 5 ints x 2 bytes =10
  Video.left = Vcol;
  Video.right = Vcol+5;
  Vcol = Vcol+6;
  Video.top = Vrow;
  Video.bottom = Vrow+12;
  Video.datum = TextColor;} // ~ScreenChar

// File-oriented write to screen, called by SysCall(7)
//  when the file type is -1 (Screen/KeyIn); it assumes the data
//  is like String arrays, 1 char per 2-byte array element..
void WriteScr(int nby, int DataPtr) {
  while (nby>0) {
    ScreenChar(CHR(PEEK(DataPtr)));
    DataPtr = DataPtr+2;
    nby = nby-2;}} // ~WriteScr

// File-oriented read from keyin, called by SysCall(6)
//  when the file type is -1 (Screen/KeyIn); it packs data in
//  1 char per 2-bytes, like String arrays, omitting final return.
// Special-case: if buffer has only one element (2 bytes),
//  then a single return character is returned in it.
int ReadLine(int nby, int DataPtr) {
  // we want nby bytes (but not more than 254) in array DataPtr
  int got = 0; // the for-loop fill index
  int ch; // temp to hold current char
  boolean sho; // =true if need to display this char
  if (nby>254) nby = 254; // request is too big; trim it
  if (((KeyFillPtr-KeyTakePtr)&256)+KeyFillPtr-KeyTakePtr<nby
      && GotRetns==0) { // not enough chars to satisfy..
    while (KeyShowPtr != KeyFillPtr) { // echo what we have so far..
      ch = mmPEEK(KeyShowPtr);
      KeyShowPtr = ((KeyShowPtr+2)&254)+BuffersAt;
      ScreenChar(CHR(ch));}
    return -1;}
  while (got<nby) { // OK, we can do it this time..
    ch = mmPEEK(KeyTakePtr); // get a character from buffer
    sho = KeyShowPtr == KeyTakePtr; // false while working on the
      // keystrokes that were already echoed, then true after that.
    KeyTakePtr = ((KeyTakePtr+2)&254)+BuffersAt; // advance to next
    if (sho) { // once it's caught up, keep it up..
      ScreenChar(CHR(ch));
      KeyShowPtr = KeyTakePtr;}
    if (ch==13) { // keep returns count current..
      GotRetns = GotRetns-1;
      if (nby>2) break;} // exit if got end-of-line return
    else if (ch==8) if (nby>2) if (got>0) { // backspace..
      DataPtr = DataPtr-2; // back up over the previous character
      got = got-2;
      continue;} // don't insert it into buffer
    mmPOKE(DataPtr,ch); // '\r' isn't included unless asking for 1 byte
    DataPtr = DataPtr+2;
    got = got+2;} // '\r' will be last char in 1-byte request
  return got;} // ~ReadLine

// ==============================================================
// Disk file I/O, for use by the system only..

// Read or write one sector: flop =true if floppy; wrt =true to write;
//  get/put nby bytes at sector number sec, memory buffer at dPtr;
//  return the actual number of bytes transferred.
// Not interrupt-driven, it just busy-waits until completion.
// Called from various places inside the disk driver..
int RWsector(boolean flop, boolean wrt, int sec, int nby, int dPtr) {
  int timo = 65533; // hopefully long enough to wait
  if (flop && Enables.datum & FlopInBit ==0) return 0; // floppy not in
  if (flop) sec = ~sec; // hardware wants these negatives..
  if (wrt) nby = -nby;
  Disk.sector = sec;
  Disk.address = dPtr;
  Disk.count = nby;
  while (Enables.datum&DiskPendBit == 0 && timo !=0) // wait until done..
    timo = timo - timo/timo; // a little slower than just timo-1
  Enables.datum = IntOnBit; // turn DiskPendBit back off
  nby = nby-Disk.count;
  if (wrt) nby = -nby;
  return nby;} // ~RWsector

// Write nby bytes at address DataPtr to file index filix;
//  called by WriteFile from SysCall(7) for disk files.
// Does not properly map data through MMU!
int WrtDskFile(boolean flop, int filix, int nby, int DataPtr) {
  int sect = ActiveFiles[filix+1]; // get current file position
  int offs = ActiveFiles[filix+2];
  int BufPtr = filix*64+BuffersAt;
  int eofAt = ActiveFiles[filix+3]; // how many bytes left in this file
  int got = 0; // the # of bytes transferred
  if (nby>eofAt) if (eofAt>=0) nby = eofAt; // can't write past eof
  if (ActiveFiles[filix]+4==0) { // newly opened flop, or was reading..
    ActiveFiles[filix] = -5; // note this is 'write'
    if (offs>0) sect = sect-1;} // gotta rewrite this sector
  if (offs>0) { // partial buffer to fill..
    while (offs<256 && nby>0) { // transfer 2 bytes at a time
      POKE(BufPtr+offs, PEEK(DataPtr));
      offs = offs+2;
      DataPtr = DataPtr+2;
      nby = nby-2;
      got = got+2;
      eofAt = eofAt-2;}
    offs = offs & 255; // =0 when buffer is full
    if (offs==0) { // full: write sector out
      offs = RWsector(flop, true, sect, 256, BufPtr);
      sect = sect+1;
      offs = 0;}}
  // ASSERT: offs==0 || nby==0
  if (nby>255) { // write some whole sectors, directly from user buffer..
    offs = RWsector(flop, true, sect, nby&(-256), DataPtr);
    nby = nby-offs;
    got = got+offs;
    DataPtr = DataPtr+offs;
    eofAt = eofAt-offs;
    sect = offs/256+sect;
    offs = 0;}
  if (nby>0) { // buffer the rest (less than one sector)..
    offs = RWsector(flop, false, sect, 256, BufPtr);
    if (offs<256) nby = 0; // oops, something went wrong, kill it
    offs = 0;
    while (nby>0) {
      POKE(BufPtr+offs, PEEK(DataPtr));
      offs = offs+2;
      DataPtr = DataPtr+2;
      nby = nby-2;
      got = got+2;
      eofAt = eofAt-2;}
    sect = sect+1;}
  ActiveFiles[filix+1] = sect; // update file position info
  ActiveFiles[filix+2] = offs;
  ActiveFiles[filix+3] = eofAt;
  // ScreenChar('`');
  return got;} // ~WrtDskFile

// Read nby bytes to address DataPtr from file index filix;
//  called by ReadFile from SysCall(6)..
// Does not properly map addresses through MMU!
int RdDiskFile(boolean flop, int filix, int nby, int DataPtr) {
  int sect = ActiveFiles[filix+1]; // get current file position
  int offs = ActiveFiles[filix+2];
  int BufPtr = filix*64+BuffersAt;
  int eofAt = ActiveFiles[filix+3]; // how many bytes left in this file
  int got = 0; // the # of bytes transferred
  if (nby>eofAt) if (eofAt>=0) nby = eofAt; // can't read past eof
  if (offs>0) { // partial sector in buffer..
    while (offs<256 && nby>0) { // transfer 2 bytes at a time
      POKE(DataPtr, PEEK(BufPtr+offs));
      offs = offs+2;
      DataPtr = DataPtr+2;
      nby = nby-2;
      got = got+2;
      eofAt = eofAt-2;}
    offs = offs & 255; // =0 when buffer is empty
    ActiveFiles[filix+2] = offs; // update file position info
    ActiveFiles[filix+3] = eofAt;
    if (offs==0) { // empty, advance sector pointer too
      sect = sect+1;
      ActiveFiles[filix+1] = sect;}}
  if (nby>255) { // get some whole sectors, directly to user buffer..
    offs = RWsector(flop, false, sect, nby&(-256), DataPtr);
    nby = nby-offs;
    got = got+offs;
    DataPtr = DataPtr+offs;
    eofAt = eofAt-offs;
    sect = offs/256+sect;
    ActiveFiles[filix+1] = sect;}
  if (nby>0) { // buffer the rest (less than one sector)..
    offs = RWsector(flop, false, sect, 256, BufPtr);
    if (offs<nby) nby = offs; // maybe early eof? (shouldn't happen)
    offs = 0;
    while (nby>0) {
      POKE(DataPtr, PEEK(BufPtr+offs));
      offs = offs+2;
      DataPtr = DataPtr+2;
      eofAt = eofAt-2;
      if (eofAt+1==0) eofAt = 0; // don't cross 0
      nby = nby-2;
      got = got+2;}
    ActiveFiles[filix+2] = offs; // update file position info
    ActiveFiles[filix+3] = eofAt;
    ActiveFiles[filix+1] = sect+1;}
  return got;} // ~RdDiskFile

// Look for an unused file index, called by Open only..
int FindAvailFile() {
  if (ActiveFiles[4]==0) return 4;
  if (ActiveFiles[8]==0) return 8;
  if (ActiveFiles[12]==0) return 12;
  return 0;} // ~FindAvailFile

// 2 functions to pack and unpack bytes,
//   between char[] arrays (2 bytes for each character)
//   and disk file format (byte for byte)
//
// These are called by the file manager to convert between String
//  file names and the packed form of the directory.
// You could use them elsewhere, but they take integer pointers,
//  which requires using the (unsafe) unary-& operator.

// Pack nby bytes from char[] array frm to int[] array too
//   bytes are 1/word in char[], 2/word in int[]
void PackBytes(int nby, int frm, int too) {
  int temp;
  while (nby>0) {
    temp = mmPEEK(frm)*256;
    nby = nby-2;
    frm = frm+2;
    if (nby>=0) temp = temp + (mmPEEK(frm)&255);
    mmPOKE(too,temp);
    too = too+2;
    frm = frm+2;}} // ~PackBytes

// Unpack nby bytes from int[] array frm to char[] array too
//   bytes are 1/word in char[], 2/word in int[]
void unPackBy(int nby, int frm, int too) {
  int temp;
  while (nby>0) {
    mmPOKE(too,mmPEEK(frm-1)&255);
    mmPOKE(too+2,mmPEEK(frm)&255);
    nby = nby-2;
    frm = frm+2;
    too = too+4;}} // ~unPackBy

// Compare the String name at NamePtr to dirName (in the directory),
//  return true if they are equal (not case-sensitive);
//  NamePtr points to 1st byte of user String,
//  dirName points to a name in directory, max 27 chars;
// Called by OpenFile to find an existing file with this name..
boolean EqualName(int dirName, int NamePtr) {
  int ix = mmPEEK(NamePtr-2)/2; // get String length
  int chd, chn;
  if (ix>27) return false; // NamePtr is too long, cannot be equal
  if ((mmPEEK(dirName+3+ix)&255)!=0) return false; // too short
  while (ix>0) {
    ix = ix-1;
    chd = mmPEEK(dirName+3+ix)&255;
    if (chd<91) if (chd>64) chd = chd+32;
    chn = mmPEEK(NamePtr+ix+ix);
    if (chn<91) if (chn>64) chn = chn+32;
    if (chd != chn) return false;} // unequal
  return true;} // ~EqualName

// Store file type fity into file index fid,
//  then set start sector and eof from directory entry # fino;
//  return a FileID value.
// Called by OpenFile() on its way back to SysCall(4)
int SetActiveFile(int fid, int fino, int fity) {
  if (fity>0) ActiveFiles[fid] = fity;
  ActiveFiles[fid+1] = mmPEEK(Directory+fino*32);
  ActiveFiles[fid+2] = 0;
  ActiveFiles[fid+3] = mmPEEK(Directory+fino*32+2);
  return fid/4+1;} // ~SetActiveFile

// Open any known named file or file type
int OpenFile(int NamePtr) { // called by SysCall(4)
  char two = CHR(mmPEEK(NamePtr+2)&223); // 2nd char of name, if needed
  int fi = FindAvailFile(); // might not be needed, but cheap call
  int ix = -1; // = default device name, KeyIn/ScreenOut
  int nx;
  if (mmPEEK(NamePtr)==ORD('.')) { // device name..
    if (two == 'K') fi = 0; // KeyIn/ScreenOut are always FileId =1
    else if (two == 'S') fi = 0;
    else if (fi==0) return 0; // FindAvailFile() failed, can't open
    else if (two == 'P') ix = -2; // printer
    else if (two == 'F') ix = -4; // floppy
    else if (two == 'H') ix = -3; // hard-sectored hard disk
      else return 0; // dunno; can't open it
    ActiveFiles[fi] = ix; // set the file type
    if (ix+4 == 0) { // floppy, read the directory..
      ix = RWsector(true, false, 0, 256, Directory);
      // you should verify file name, etc...
      return SetActiveFile(fi,1,0);} // we just open file # 1
    else if (ix+3 == 0) { // hard disk, absolute sector..
      ActiveFiles[fi+1] = 0; // sector 0
      ActiveFiles[fi+2] = 0;
      ActiveFiles[fi+3] = -1;} // no size limit
    return fi/4+1;}
  else { // named HD file..
    if (mmPEEK(NamePtr-2)>54) return 0; // name too long
    if (RWsector(false, false, 0, 256, Directory)<256)
      return 0; // read failed
    ix = mmPEEK(Directory+12); // # files on this disk
    while (ix>0) // try each file..
      if (EqualName(Directory+ix*32,NamePtr))
        return SetActiveFile(fi,ix,ix); // found it
      else ix = ix-1;
    ix = mmPEEK(Directory+12)+1; // not there, add another file
    if (ix>mmPEEK(Directory+14)) return 0; // oops, too many
    nx = mmPEEK(NamePtr-2)/2; // length of file name, in chars
    PackBytes(nx, NamePtr, Directory+ix*32+4); // insert name
    mmPOKE(Directory+ix*32+4+nx,0); // null terminated
    mmPOKE(Directory+ix*32,mmPEEK(Directory+10)); // set start to next sec
    mmPOKE(Directory+ix*32+2,-1); // new file, assume 65K
    if (RWsector(false, true, 0, 256, Directory)==256) // rewrite dir
      return SetActiveFile(fi,ix,ix);} // fail if dir rewrite fails
    return 0;} // ~OpenFile

// Close the file indexed by FileId theFile..
void CloseFile(int theFile) { // called by SysCall(5)
  int fi = theFile*4-4; // point to the indexed ActiveFiles
  int BufPtr = fi*64+BuffersAt; // point to its buffer, in case needed
  int ix;
  if (fi<0) return; // not an open file (null FileId)
  if (theFile>4) return; // not an open file (bogus FileId)
  ix = ActiveFiles[fi]; // the file # on disk, or device #
  if (ix>0) { // close hard disk file..
    ActiveFiles[fi] = 0;
    if (RWsector(false, false, 0, 256, Directory)<256) return;
    if (mmPEEK(Directory+12)>=ix) return; // not closing new file
    if (ActiveFiles[fi+3]+1==0) return; // didn't write anything
    mmPOKE(Directory+12,ix); // update # files
    mmPOKE(Directory+ix*32+2,~ActiveFiles[fi+3]); // actual file size
    ix = ActiveFiles[fi+1]; // this is next sector to write
    if (ActiveFiles[fi+2]>0) // rewrite partial buffer
      if (RWsector(false, true, ix, 256, BufPtr)==256)
        ix = ix+1; // successful write, update sector #
    mmPOKE(Directory+10,ix); // that is sector for next file to open
    ix = RWsector(false, true, 0, 256, Directory); // rewrite dir
    return;}
  else if (ix+5==0) if (ActiveFiles[fi+2]>0) // writing to floppy..
    ix = RWsector(true, true, ActiveFiles[fi+1], 256, BufPtr);
  if (fi>0) ActiveFiles[fi] = 0; // leave K/S open
  } // ~CloseFile

// Read a block into array DataPtr from open FileId theFile..
int ReadFile(int theFile, int DataPtr) { // called by SysCall(6)
  int fi = theFile*4-4; // point to the indexed ActiveFiles
  int who, len;
  if (fi<0) return 0; // not an open file (null FileId)
  if (theFile>4) return 0; // not an open file (bogus FileId)
  who = ActiveFiles[fi];
  if (who==0) return 0; // not an open file (stale fileId)
  if (DataPtr==0) return 0; // no array to read into
  len = mmPEEK(DataPtr-2); // the size of the array
  if (who+1==0) return ReadLine(len, DataPtr); // Keyboard input
  if (who+4==0) return RdDiskFile(true, fi, len, DataPtr); // Floppy in
  if (who>0) { // named HD file
    if (ActiveFiles[fi+3]+1==0) return 0; // new file, nothing to read
    who = -3;}
  if (who+3==0 || who>0) // Hard disk sector or file input..
    return RdDiskFile(false, fi, len, DataPtr);
  return 0; // ignore the rest
  } // ~ReadFile

// Write a block from array DataPtr to open FileId theFile..
int WriteFile(int theFile, int DataPtr) { // called by SysCall(7)
  int fi = theFile*4-4; // point to the indexed ActiveFiles
  int who, len;
  if (fi<0) return 0; // not an open file (null FileId)
  if (theFile>4) return 0; // not an open file (bogus FileId)
  who = ActiveFiles[fi];
  if (who==0) return 0; // not an open file (stale fileId)
  if (DataPtr==0) return 0; // no array to write from
  len = mmPEEK(DataPtr-2); // the size of the array
  if (who+1==0) { // Screen output..
    WriteScr(len, DataPtr);
    return len;}
  if (who != -2) // Floppy/hard disk output..
    return WrtDskFile(who<-3, fi, len, DataPtr);
  return 0; // ignore the rest
  } // ~WriteFile

// ==============================================================

// Process management subroutine,
//  moves CurProcess to Blok2, and ReadyList to CurProcess
int SwapOut(int SemSP, int Blok2) {
  int going = CurProcess;
  int comin = ReadyList;
  if (Blok2==0) Blok2 = &ReadyList; // Blok2=0 just swaps
  if (comin !=0) if (going !=0) { // can't do it if nothing there
    ReadyList = mmPEEK(comin); // unlinked top of ReadyList
    CurProcess = comin; // and make it current
    mmPOKE(going+6,SemSP); // save current process's SP in its PIB
    mmPOKE(going, mmPEEK(Blok2)); // link it to rest of its list
    mmPOKE(Blok2,going); // move (formerly) CurProcess to its list
    return mmPEEK(comin+6);} // exit to it on caller's CoCall
  return SemSP;} // ~SwapOut

// ==============================================================
// Interrupt drivers..
// These are called ONCE to start up their respective processes.
//  After that, they do all their work when interrupts hit..

void DoSema4() {
  // These 2 interrupts happen for Signal and Wait respectively
  int SemSP = 0;  // maps to Sema4Frame[3]
  int aPtr, temp;
  Sema4Frame[6] = &Sema4Frame[9];   // initial SP
  Sema4Frame[8] = &Sema4Frame[0];   // initial FP
  Sema4Frame[0] = mmPEEK((&SemSP)-6); // copy my globals ptr
  Sema4Frame[1] = 0;        // halt if it returns (it shouldn't)
  SemSP = FORK(&Sema4Frame[6]);  // returns twice:
  if (SemSP==0) { // returned in caller's process..
    aPtr = &Sema4Frame[3]; // set 2 interrupt vector items..
    iVector[6] = aPtr;
    iVector[7] = aPtr;
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) {  // this is the interrupt handler loop..
    COCALL(&SemSP);  // wait for interrupt
    aPtr = SignalWait.address; // the semaphore (not on stack)
    if (mmPEEK(mmPEEK(SemSP)-2)&1 ==0) { // caller's opcode, =0 if Signal
      temp = ReadyList;       // get previous ready list,
      ReadyList = mmPEEK(aPtr); // replace it with process from sem,
      mmPOKE(aPtr,mmPEEK(ReadyList)); // (delete top of sem queue)
      mmPOKE(ReadyList, temp);} // ..link ready list to previous
    else SemSP = SwapOut(SemSP,aPtr);} // otherwise it's a Wait
  } // ~DoSema4

void DoDisk() {
  // This interrupt is used for switching context at the end of
  //   disk I/O, if you switched out to make use of the wait time.
  // Right now it's set up but unused.
  int SemSP = 0;  // maps to DiskFrame[3]
  int unused;
  int temp;
  DiskFrame[6] = &DiskFrame[9];  // initial SP
  DiskFrame[8] = &DiskFrame[0];  // initial FP
  DiskFrame[0] = mmPEEK((&SemSP)-6);  // copy my globals ptr
  DiskFrame[1] = 0;        // halt if it returns (it shouldn't)
  SemSP = FORK(&DiskFrame[6]);  // returns twice:
  if (SemSP==0) { // returned in caller's process..
    iVector[2] = &DiskFrame[3]; // set interrupt vector
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) {  // this is the interrupt handler loop..
    COCALL(&SemSP);  // wait for interrupt
      // YOUR CODE GOES HERE
    } // do nothing
  } // ~DoDisk

void DoKeyInts() {
  // This interrupt grabs a keystroke when it is available,
  //   and saves it in a buffer for when the program needs it.
  int SemSP = 0;  // maps to KeyIntFrame[3]
  int theKey;
  int temp;
  KeyIntFrame[6] = &KeyIntFrame[9];  // initial SP
  KeyIntFrame[8] = &KeyIntFrame[0];  // initial FP
  KeyIntFrame[0] = mmPEEK((&SemSP)-6);  // copy my globals ptr
  KeyIntFrame[1] = 0;                 // halt if it returns
  SemSP = FORK(&KeyIntFrame[6]); // create a new process (2 returns)
  if (SemSP==0) { // final return in initial process
    iVector[3] = &KeyIntFrame[3]; // set interrupt vector
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) { // this is the interrupt handler loop..
    COCALL(&SemSP);  // wait for interrupt
    theKey = KeyIn.datum & 127; // got a keystroke, save it..
    mmPOKE(KeyFillPtr, theKey); // ..in the keyboard buffer
    if (theKey==13) GotRetns = GotRetns+1; // count returns (=lines)
    KeyFillPtr = ((KeyFillPtr+2)&254)+BuffersAt; // advance ptr
    if (KeyFillPtr==KeyTakePtr) { // buffer o'flow, discard oldest
      if (mmPEEK(KeyTakePtr)==13) GotRetns = GotRetns-1;
      KeyTakePtr = ((KeyTakePtr+2)&254)+BuffersAt;
      if (KeyFillPtr==KeyShowPtr) // discard oldest show also..
        KeyShowPtr = KeyTakePtr;}
    } // ~while
  } // ~DoKeyInts

void DoPrint() {
  // This interrupt happens after line output completion
  //   Right now it's set up but unused.
  int SemSP = 0;  // maps to PrintFrame[3]
  int unused;
  int temp;
  PrintFrame[6] = &PrintFrame[9];  // initial SP
  PrintFrame[8] = &PrintFrame[0];  // initial FP
  PrintFrame[0] = mmPEEK((&SemSP)-6);  // copy my globals ptr
  PrintFrame[1] = 0;        // halt if it returns (it shouldn't)
  SemSP = FORK(&PrintFrame[6]);  // returns twice:
  if (SemSP==0) { // returned in caller's process..
    iVector[4] = &PrintFrame[3]; // set interrupt vector
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) {  // this is the interrupt handler loop..
    COCALL(&SemSP);  // wait for interrupt
    PrintReady = true;} // do nothing
  } // ~DoPrint

void DoMemMgr() {
  // This interrupt happens on a memory manager page fault
  //   Right now ...
  int SemSP = 0;  // maps to MMUFrame[3]
  int unused;
  int temp;
  MMUFrame[6] = &MMUFrame[9];  // initial SP
  MMUFrame[8] = &MMUFrame[0];  // initial FP
  MMUFrame[0] = mmPEEK((&SemSP)-6);  // copy my globals ptr
  MMUFrame[1] = 0;        // halt if it returns (it shouldn't)
  SemSP = FORK(&MMUFrame[6]);  // returns twice:
  if (SemSP==0) { // returned in caller's process..
    iVector[0] = &MMUFrame[3]; // set interrupt vector
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) {  // this is the interrupt handler loop..
    COCALL(&SemSP);  // wait for interrupt

    } // do something..
  } // ~DoMemMgr

void DoClock() {
  // This interrupt happens 8x each second, for timing purposes;
  //   we use it to maintain theTicks.
  int SemSP = 0;  // maps to ClockFrame[3]
  int prior; // holds previous Date.seconds reading, updated
  int temp;
  ClockFrame[6] = &ClockFrame[9];  // initial SP
  ClockFrame[8] = &ClockFrame[0];  // initial FP
  ClockFrame[0] = mmPEEK((&SemSP)-6);  // copy my globals ptr
  ClockFrame[1] = 0;        // halt if it returns (it shouldn't)
  SemSP = FORK(&ClockFrame[6]);  // returns twice:
  if (SemSP==0) { // returned in caller's process..
    iVector[1] = &ClockFrame[3]; // set interrupt vector
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) {  // this is the interrupt handler loop..
    COCALL(&SemSP);  // wait for interrupt
    temp = Date.seconds;
    if (temp != prior) { // ticks management, got a new second..
      prior = temp;
      theTicks = prior*8;}
    else theTicks = theTicks+1;
    // YOUR CODE GOES HERE
  } } // ~DoClock

void DoSysCall() {
  // this interrupt happens when the program executes SysCall
  int SemSP = 0;  // maps to SysCallFrame[3]
  int aPtr;
  int temp;
  SysCallFrame[6] = &SysCallFrame[9];  // initial SP
  SysCallFrame[8] = &SysCallFrame[0];  // initial FP
  SysCallFrame[0] = mmPEEK((&SemSP)-6);  // copy my globals ptr
  SysCallFrame[1] = 0;                 // halt if it returns
  SemSP = FORK(&SysCallFrame[6]); // create a new process (2 returns)
  if (SemSP==0) { // final return, into initial process
    iVector[5] = &SysCallFrame[3]; // ready, set interrupt vector
    return;}
  // otherwise returned in new process (first)..
  mmPOKE(SemSP-6,0);  // clear caller's return value
  while (true) { // this is the interrupt handler loop..
    iVector[5] = &SysCallFrame[3]; // restore double-fault crasher
    COCALL(&SemSP);  // wait for interrupt
    iVector[5] = 0;  // so it crashes "Semaphor=0" on programming error
    temp = mmPEEK(SemSP-8); // get a parameter (the last pushed)
    switch(mmPEEK(SemSP-6)) { // get opcode
    case 0:                                   // exit from program
      SemSP = SwapOut(SemSP,0); // Batch: let main take care of it
      break;
    case 1:                                   // array bounds check
      temp = mmPEEK(0xFABC);  // non-existing memory, to crash
      mmPOKE(SemSP,0);  // clear caller's PC
      break;
    case 2:                                   // memory allocator
      temp = temp+4; // # bytes wanted
      if (HeapBottom+temp<HeapTop) { // do we have enough available?
        aPtr = HeapTop-temp; // yup, allocate it..
        HeapTop = aPtr;
        temp = temp-4;
        mmPOKE(aPtr+2,temp); // record the size
        mmPOKE(aPtr,0);  // initially zero references
        aPtr = aPtr+4;} // the front of the data proper
      else aPtr = 0; // otherwise return null
      mmPOKE(SemSP-8,aPtr); // return pointer, or null
      break;
    case 3:                          // memory de-allocator request
      if (temp == 0) continue; // ignore null (initial assignment)
      temp = temp-4; // the pointer's ref count is here
      if (mmPEEK(temp) & -2 == 0) {
        mmPOKE(temp,0); // take count down to 0 for viewing
        } // dispose it (You get to write this part)
      else if (mmPEEK(temp) & -16 == 0) { // not permanent..
        mmPOKE(temp,mmPEEK(temp)-1);} // decrement reference count
      break; // caller pops the pointer
    case 4:                                   // open file
      mmPOKE(SemSP-8, OpenFile(temp)); // OpenFile does the work
      break;
    case 5:                                   // close file
      CloseFile(temp); // CloseFile does the work
      break;
    case 6:                                   // read file
      temp = ReadFile(mmPEEK(SemSP-10), temp); // ReadFile does it
      if (temp<0) // the device is busy, so..
        mmPOKE(SemSP,mmPEEK(SemSP)-1); // back up PC to try again
        // better: block this process until it's unbusy,
        //   but we don't have processes implemented yet
      else mmPOKE(SemSP-10, temp); // the return value (how many bytes)
      break;
    case 7:                                   // write file
      temp = WriteFile(mmPEEK(SemSP-10), temp);
      if (temp<0) mmPOKE(SemSP,mmPEEK(SemSP)-1); // back up PC
        else mmPOKE(SemSP-10, temp);
      break;
    case 8:                                   // seek file
      // this is left to the student as an exercise
      break;
    case 9:                                   // get file info
      mmPOKE(SemSP-8, 0); // info? what info? return 0
      break;
    case 13:            // wait for event (yield to other process)
      SemSP = SwapOut(SemSP,0);
      break;
    case 14:                                   // ticks delay
      if (temp<=0) { // asking for a delay (>0 is target time)
        temp = theTicks-temp;
        mmPOKE(SemSP-8,temp);}
      if (temp>theTicks)
        mmPOKE(SemSP,mmPEEK(SemSP)-1); // back up PC to wait some more
      break;
    }} // ~while ~switch // otherwise do nothing
  } // ~DoSysCall

// ==============================================================
// User-level system code..

// Look to see if HD is formatted, and if not, build directory
void HDformat(boolean forced) { // if forced=true, build it regardless
  int ix;
  int[128] Buff; // one sector, for the 7-file directory.
  FileId HD = Open(".H"); // all I/O is at user level!
  ix = Read(HD,Buff); // reads sector 0 into Buff
  Close(HD);
  if (!forced) if (ix==256) if (Buff[0]==ORD('H')*256+ORD('a'))
    if (Buff[1]==ORD('r')*256+ORD('d'))
    if (Buff[2]==ORD('D')*256+ORD('i'))
    if (Buff[3]==ORD('s')*256+ORD('k')) return; // it seems OK
  ix = 128; // no, set up my 1-sector directory..
  while (ix>0) {ix = ix-1; Buff[ix] = 0;} // clear the buffer,
  Buff[0] = ORD('H')*256+ORD('a'); // then insert signature,
  Buff[1] = ORD('r')*256+ORD('d');
  Buff[2] = ORD('D')*256+ORD('i');
  Buff[3] = ORD('s')*256+ORD('k');
  Buff[4] = 0; // no boot sector
  Buff[5] = 4; // next unused sector
  Buff[6] = 0; // no files yet
  Buff[7] = 7; // directory size (could be 31 in 4 sectors)
  HD = Open(".H"); // OK, rewrite the directory
  ix = Write(HD,Buff); // no error checking!
  Close(HD);} // ~HDformat

// Read one line (up to \r) from keyboard, return it as String
String ReadStr() { // return a pointer to an input line
  int ix = 0, nx = 0;
  String theStr;
  int theLen;
  theLen = Read(Kin, KeyString)/2; // get a line
  theStr = new char[theLen]; // allocate as many bytes as we got
  while (ix<theLen) { // copy them over to new String array..
    theStr[ix] = KeyString[ix];
    ix = ix+1;}
  return theStr;} // ~ReadStr

// Display output String,
//  a cleaner interface than needing to deal with returned int..
void PrintStr(String str) {int ix = Write(Cout, str);}

// Display output character..
void PrintChar(char ch) { // uses SysCall, for safety
  int nx;
  char[1] cc; cc[0] = ch;
  nx = Write(Cout, cc);}

// PrintNum, a string-free number display (uses PrintChar)
void PrintNum(int theNum) {
  if (theNum==32768) { // special-case, it has no positive..
    PrintChar('-');
    PrintChar('3');
    theNum = 2768;}
  else if (theNum<0) { // negative number, show the sign
    PrintChar('-');
    theNum = -theNum;}
  if (theNum>9) { // recursive call to do leftmost digits first
    PrintNum(theNum/10);
    theNum = theNum%10;}
  PrintChar(CHR(theNum+48));} // ~PrintNum

// Halt if stack overrun. Called by main() or a deep subroutine
//  called from main(), in case I counted wrong in space allocation..
void BombCheck() {
  int here; // a local address to test
  if (&here>Directory-100) { // oops, this is a crasher...
    PrintStr("** Stack Collision **");
    here = SysOp0(0);}} // ~BombCheck

// MMU experimentation..
void MMUtest() {
  int ix = 32;
  while (ix>0) {
    ix = ix-1;
    PageTable[ix] = DefaultTable[ix];}
  MMU.address = &PageTable;} // ~MMUtest

// This is the startup code that get the OS running.
// Call it once, before anything else..
void MiniOS() {
  BombCheck(); // verify that we have enough stack space to do this
  POKE(Directory-2,256); // make it look like array
  POKE(Directory-4,-16);
  DoSysCall(); // initialize each type of interrupt..
  DoKeyInts();
  DoDisk();
  DoPrint();
  DoClock();
  DoSema4();
  DoMemMgr();
  MainPIB[0] = 0; // create a (fake) PIB for current process..
  MainPIB[4] = 0;
  CurProcess = &MainPIB;
  Enables.datum = IntOnBit; // enables interrupts (reset turned 'em off)
  HDformat(false); // check for hard drive & initialize
  Kin = Open(".K"); // open default KeyIn & ScreenOut files..
  Cout = Open(".S");
  KeyString = new char[120]; // used by ReadStr()
  do PrintChar('\r'); while (Vrow>2); // clear screen (interrupt-safe)
  } // ~MiniOS

// Some String operators...

// Return true if str1==str2 (not case sensitive)..
boolean StrEqual(String str1, String str2) {
  int ix = LENGTH(str2);
  char ch1, ch2;
  if (LENGTH(str1) != ix) return false;
  while (ix>0) {
    ix = ix-1;
    ch1 = str1[ix];
    if (ch1>='A') if (ch1<='Z') ch1 = CHR(ORD(ch1)+32);
    ch2 = str2[ix];
    if (ch2>='A') if (ch2<='Z') ch2 = CHR(ORD(ch2)+32);
    if (ch1 != ch2) return false;}
  return true;} // ~StrEqual

// Parse out word wdno from aStr..
String GetWord(String aStr, int wdno) {
  String res;
  boolean inwd = false;
  int here = 0, bgn = -1, end = -1;
  while (here<LENGTH(aStr)) {
    if ((aStr[here]>' ') != inwd) {
      inwd = !inwd;
      if (inwd) {
        if (wdno==0) bgn = here;
        wdno = wdno-1;}
      else if (wdno<0) break;}
    here = here+1;}
  if (wdno<0) end = here;
  if (bgn<end) {
    res = new char[end-bgn];
    here=0;
    while (bgn<end) {
      res[here] = aStr[bgn];
      here = here+1;
      bgn = bgn+1;}
    return res;}
  else return "";} // ~GetWord

// Some date extraction routines..
// These extract date parts from ints; they do not access the I/O

int YearOf(int datehours) {return (datehours>>1)/4383;}
 // returns 4 for 2004
int HourOf(int datehours) {return (datehours-24000)%24;}
int JulianOf(int datehours) {return (datehours-YearOf(datehours)*8766)/24;}
int MonthDayOf(int datehours) { // computes both month and day..
  final int[] mons = {31,28,31,30,31,30,31,31,30,31,30,31};
  int yr = YearOf(datehours); // so we know if it's leapyear
  int dy = JulianOf(datehours);
  int mo = 0;
  while (dy>=mons[mo]) { // count off the months..
    dy = dy-mons[mo];
    mo = mo+1;
    if (mo==2) dy = dy-1;}
  return mo*100+dy+101;} // ~MonthDayOf
int MonthOf(int datehours) {return MonthDayOf(datehours)/100;}
int DayOf(int datehours) {return MonthDayOf(datehours)%100;}

// This cheats a little, by reaching directly into the directory,
//  to extract the name of the recently-opened floppy disk file (fno=0),
//  or the name of HD file #fno.
String GetFileName(int fno) { // return String name of the file #fno
  int ix = 0;
  String theStr;
  char[28] theName;
  int[] DirAry; // will point to Directory
  int theLen = 27;
  FileId fi;
  if (fno>0) { // HD file name..
    POKE(&DirAry,Directory);
    fi = Open(".H"); // opens the HD directory
    ix = Read(fi,DirAry);
    fno = fno-1;}
  else fi = Open(".F"); // opens the floppy directory
  unPackBy(28, Directory+fno*32+36, &theName); // get name, padded by nulls
  while (theLen>=0) {
    if (theName[theLen]>' ') break;
    theLen = theLen-1;}
  theStr = new char[theLen+1]; // allocate as many bytes as we got
  ix = 0;
  while (ix<=theLen) { // copy them over to new String array..
    theStr[ix] = theName[ix];
    ix = ix+1;}
  Close(fi);
  return theStr;} // ~GetFileName

// Returns true if current floppy is "MiniOS"
// Used to skip user interaction after flop is changed,
//  and to force DH reformat if not.
boolean MyFlopName() {
  int nx = LENGTH(MyName); // = "MiniOS" packed as int[]
  int ix = 0;
  FileId fi = Open(".F"); // opens the floppy directory
  while (ix<nx) { // compare it to "MiniOS"
    if (MyName[ix] != PEEK(Directory+36+ix*2)) nx = -1; // unequal (exit)
    ix = ix+1;}
  Close(fi);
  return nx>0;} // ~MyFlopName

// If current floppy is "MiniOS", asks for new;
//  returns true if current floppy is still "MiniOS"
boolean FlopSwap() {
  if (MyFlopName()) {
    PrintStr("Mount floppy, then click\r");
    while (Mouse.datum>=0) ;
    while (Mouse.datum<0) ;}
  return MyFlopName();} // ~FlopSwap

// Show HD directory..
void ShowDir() {
  String str;
  int fno = 0;
  while (true) {
    fno = fno+1;
    str = GetFileName(fno);
    if (LENGTH(str)==0) return;
    PrintStr("\r");
    PrintNum(fno);
    PrintStr(" ");
    PrintNum(PEEK(Directory+fno*32));
    PrintStr("  ");
    PrintNum(PEEK(Directory+fno*32+2));
    PrintStr("  ");
    PrintStr(str);}} // ~ShowDir

// Copy one (program) file from floppy to hard disk..
void CopyFlop() {
  String str;
  FileId fi, fo;
  int[128] buffer;
  int nby, totby = 0;
  BombCheck();
  if (FlopSwap()) HDformat(true); // erase hard drive
  str = GetFileName(0); // the name on disk will be same as on floppy
  PrintStr("Copying file '");
  PrintStr(str);
  PrintStr("' ");
  fi = Open(".F"); // open floppy source
  fo = Open(str); // open destination file, same name
  nby = (Read(fi, buffer)+1)/2; // read a block
  totby = buffer[0]; // get the byte count
  PrintNum(totby);
  PrintStr(" bytes ");
  while (totby>0 && nby>0) {
    while (nby<128) {buffer[nby] = 0; nby = nby+1;} // fill out partial
    if (nby*2 > Write(fo, buffer)) { // write it
      PrintStr("Write Error ");
      break;}
    PrintChar('.'); // one dot every block
    totby = totby-nby*2; // count bytes down
    nby = (Read(fi, buffer)+1)/2;} // read next block
  Close(fi);
  Close(fo);
  PrintStr(" Done.\r");} // ~CopyFlop

// Load a program file, start it up..
void LoadProgram(String prog) {
  String str;
  int[2] data; // to read just the program size
  FileId fi;
  int CodeBase, Stack, StakIx, PIB;
  int nx, ix = 0;
  BombCheck();
  fi = Open(prog);
  nx = Read(fi,data)/2;
  Close(fi);
  ix = data[0];
  PrintStr("Reading "); PrintNum(ix); PrintStr(" bytes...");
  if (nx<2 || ix<30 || ix>16000) {
    if (ix>16000) PrintStr("Program is too big");
      else PrintStr("Could not open code file");
    return;}
  AppCode = new int[(ix+1)/2]; // allocate a block big enough for code
  AppPIB = new int[1024]; // probably big enough for stack and globals
  if (AppCode==null || AppPIB==null) {
    PrintStr("Could not allocate memory");
    return;}
  CodeBase = PEEK(&AppCode)+2; // 1st 2 bytes is file size, then CB
  fi = Open(prog); // restarts at front of file
  nx = Read(fi,AppCode); // read it from the floppy
  Close(fi);
  PrintStr(" Got "); PrintNum(nx);
  if (nx<ix) return; // didn't get the whole program
  PIB = PEEK(&AppPIB); // got it..
  POKE(PIB-4,-16); // make these blocks permanent..
  POKE(CodeBase-6,-16);
  StakIx = 8; // allow 16 bytes for Process Info Block
  Stack = PIB+StakIx*2; // the rest is user space
  AppPIB[StakIx-1] = 0x2009; // SysCall(0) for quit
  AppPIB[StakIx+1] = Stack-2; // return address is that stall
  AppPIB[StakIx] = Stack; // globals ptr, =FP
  AppPIB[StakIx+2] = CodeBase;
  AppPIB[StakIx+3] = Stack;  // initial FP
  AppPIB[StakIx+4] = AppCode[1]+CodeBase; // initial PC
  AppPIB[3] = Stack+8; // initial SP, where Process Mgr can find it
  AppPIB[4] = 1;
  AppPIB[0] = ReadyList; // link to front of ReadyList
  ReadyList = PIB;
  PrintStr(" Success!\r");} // ~LoadProgram

// Start new thread...
void TakenShow() { // a thread to take chars from queue & display them
  int temp;   // maps to DemoFrame[7] (allow 4 ints for PIB)
  DemoFrame[10] = &DemoFrame[13]; // initial SP
  DemoFrame[12] = &DemoFrame[4];  // initial FP
  DemoFrame[4] = PEEK((&temp)-6);  // copy my globals ptr
  DemoFrame[5] = 0;        // halt if it returns (it shouldn't)
  temp = FORK(&DemoFrame[10]);  // returns twice:
  if (temp==0) return; // returned in caller's process
  // otherwise returned in new process (first)..
  POKE(temp-6,0);  // clear caller's return value
  POKE(CurProcess+6,temp); // save caller's SP in its PIB
  POKE(CurProcess,ReadyList); // move caller to ready list..
  ReadyList = CurProcess;
  CurProcess = &DemoFrame; // make me current
  while (true) { // OK, process the data..
    Wait(take);
    PrintChar(queue[tx]);
    Signal(fill);
    tx = (tx+1)%Qsize;}
  } // ~TakenShow

void DoSemDemo() { // Demonstrate Semaphores..
  int ix = Qsize;
  char[1] GetKey;
  while (ix>0) { // initialize this semaphore; take is already OK
    Signal(fill);
    ix = ix-1;}
  TakenShow(); // go fork off a thread to do the other end
  while (true) { // OK, process the data..
    Wait(fill);
    ix = Read(Kin, GetKey); // get a keystroke
    ix = ORD(GetKey[0]);
    queue[fx] = CHR(ix^((ix>>1)&32)); // [capitalize letters]
    Signal(take);
    fx = (fx+1)%Qsize;}
  } // ~DoSemDemo

// The main program..
void main() {
  int here = 0;
  String str, prog;
  MiniOS(); // start up operating system
  while (true) {
    here = 0;
    PrintStr("\rQuit, Copy (Flop->HD), <filename>\r> ");
    str = ReadStr();
    if (StrEqual(str,"Quit")) break;
    else if (StrEqual(str,"Demo")) DoSemDemo();
    while (true) {
      prog = GetWord(str,here);
      here = here+1;
      if (LENGTH(prog)==0) break;
      if (StrEqual(prog,"Copy")) CopyFlop();
      else if (StrEqual(prog,"Dir")) ShowDir();
      else if (StrEqual(prog,"MMUtest")) MMUtest();
      else {
        LoadProgram(prog); // load it and start it up as a process
        if (SysOp0(13)==0) ;}}} // wait for termination
  PrintStr("\rBye!");} // ~main

Rev.  2004 March 9