{
I've written a simple device driver in TP, and it works.  From some things I've
heard, it won't work in all versions of DOS (it's an .EXE format device driver,
not a .BIN format one).  There are tons of restrictions on what you can do in
it - DOS isn't reentrant, and the TP system library isn't designed to do things
while DOS is active, so I don't even let it get initialized, etc., etc.

It's still a bit of a mess, but here it is, for your enjoyment and edification:
 a character device driver that keeps a buffer of 255 characters, called
TPDEVICE.

To try it out, compile it (you'll need OPro or TPro; sorry, but stack swapping
is essential, and I wouldn't want to try to write code to do it myself), put it
into your CONFIG.SYS (on a floppy disk, please!) as

  device=tpdev.exe

and then reboot.   Hopefully you won't crash, but if you do, you'll have to
reboot from a different disk and remove it from CONFIG.SYS.

Then you can try

  COPY TPDEVICE CON

to see the initialization message, and

  ECHO This is a line for the buffer >TPDEVICE

to replace it with a new one.
}
{ DOS character device driver written entirely in TP 6 }

{ Written by D.J. Murdoch for the public domain, May 1991 }

{$S-,F-}       { Stack checking wouldn't work here, and we assume near calls }
{$M $1000,0,0} { We can't use the heap and don't use the stack.  This
                 setting doesn't really matter though, since you normally
                 won't run TPDEV }

program tpdev;

uses
  opint;  { OPro interrupt services, needed for stack switching }

procedure strategy_routine(bp:word); interrupt; forward;
procedure interrupt_routine(bp:word); interrupt; forward;

procedure header; assembler;
{ Here's the trick:  an assembler routine in the main program, guaranteed to
  be linked first in the .EXE file!!}
asm
  dd $FFFFFFFF    { next driver }
  dw $8000        { attributes of simple character device }
  dw offset strategy_routine
  dw offset interrupt_routine
  db 'TPDEVICE'
end;

const
  stDone = $100;
  stBusy = $200;

  cmInit  = 0;
  cmInput = 4;
  cmInput_no_wait = 5;
  cmInput_status  = 6;
  cmInput_flush   = 7;
  cmOutput        = 8;
  cmOutput_Verify = 9;
  cmOutput_status = 10;
  cmOutput_flush  = 11;

type
  request_header = record
    request_length : byte;
    subunit        : byte;
    command_code   : byte;
    status         : word;
    reserved       : array[1..8] of byte;
    case byte of
      cmInit : (num_units  : byte;
                first_free : pointer;
                args       : ^char;
                drive_num  : byte;);
      cmInput :  { also used for output }
               (media_descriptor : byte;
               buffer            : pointer;
               byte_count        : word);
      cmInput_no_wait : (next_char : char);
  end;

var
  local_stack  : array[1..4000] of byte;
  end_of_stack : byte;
  request      : ^request_header;
  line         : string;

procedure handler(var regs : intregisters);
{ This routine is called by the strategy routine, and handles all requests.
  The data segment is okay, and we're running on the local_stack so we've got
 plenty of space, but remember:
   ****** The initialization code for SYSTEM and all other units hasn't
          ever been called!!  ******** }
begin
  with request^ do
  begin
    case command_code of

      cmInit :
      begin
        { Last thing in the data segment in TP6 - No heap!!}
        first_free := ptr(dseg, ofs(saveint75) + 4);
        status     := stDone;
        line       := 'TPDRIVER successfully initialized.';
      end;

      cmInput :
      begin
        if byte_count > length(line) then
          byte_count := length(line);
        move(line[1], buffer^, byte_count);
        line := copy(line, byte_count + 1, 255);
        status := stDone;
      end;

      cmInput_no_wait :
      begin
        if length(line) > 0 then
        begin
          next_char := line[1];
          status := stDone;
        end
        else
          status := stBusy;
      end;

      cmInput_Status,
      cmOutput_Status,
      cmInput_Flush,
      cmOutput_Flush : status := stDone;

      cmOutput,
      cmOutput_Verify :
      begin
        if byte_count + length(line) > 255 then
          byte_count := 255 - length(line);
        move(buffer^, line[length(line) + 1], byte_count);
        line[0] := char(byte(byte_count + length(line)));
        status := stDone;
      end;
    end;
  end;
end;

procedure RetFar; assembler;
{ Replacement for the IRET code that ends the interrupt routines below }
asm
  mov sp,bp
  pop bp
  pop es
  pop ds
  pop di
  pop si
  pop dx
  pop cx
  pop bx
  pop ax
  retf
end;

procedure strategy_routine(bp : word);
var
  regs : intregisters absolute bp;
begin
  with regs do
    request := ptr(es, bx);
  RetFar;
end;

procedure interrupt_routine(bp : word);
var
  regs : intregisters absolute bp;
begin
  SwapStackandCallNear(Ofs(handler), @end_of_stack, regs);
  RetFar;
end;

begin
  writeln('TPDEVICE - DOS device driver written *entirely* in Turbo Pascal.');
  writeln('Install using DEVICE=TPDEV.EXE in CONFIG.SYS.');
  request := @header;  { Need a reference to pull in the header. }
end.
