PRINTER considerations in DOS programming

Additional PRINTER in Turbo Pascal

This is simply a "copy" of the Printer unit of Turbo Pascal with modifications for the printer port LPT2.

unit Printer2;
var  LST2: Text;   {for Writeln(LST2,'This is printed on LPT2'); }
procedure LstBinaryMode; Assembler;
  MOV    BX,LST2.handle   {note: LST2 MUST BE IN DS:}
  MOV    AX,4400H  {IOcheck, get device info}
  INT    21H
  OR     DL,20H    {bit 5 = raw mode, else textmode}
  XOR    DH,DH     {must be 0 for AX=4401H}
  MOV    AX,4401H  {set device info}
  INT    21H

In Textmode the   ^Z    #$1A  is treated as End Of File. This historic "feature" must be switched off for random character printing. This is very similar to the /b switch in the COMMAND.COM - COPY command.

Btw.: You should also consider the MODE command for printer redirection, where applicable!

Note: The acronym LST is the abbreviation of LiST device. LPT means LinePrinTer, and PRN is simply PRiNter. These come from the very early CP/M times, when the serial ports were named RDR: and PUN: for tape reader and punch, later named AUX: for auxiliary terminals. The teletype was connected to the TTY: port (the operators CON:sole, outputting on paper) and later the terminal with its CathodeRayTube CRT was the CON:sole. Note the colon, it was the symbol for devices, now (in MSDOS...) only used for disks, with some lack of logic.

This bypasses the DOS printer handling. It is useful for graphics printing,
but you get no Write(LSTx,Textstring,numbers) feature.
2 examples, one of them using ASM.

Uses DOS; {for "Registers"}
Procedure PrintBIOS(Prn : Integer;Cc : Char);
Var Regs : Registers;
  with Regs do
      AL := Cc;
      AH := 0;   {print-write command}
      DX := Prn; {0=LPT1, 1=LPT2, 2=LPT3}
    End; {with Regs do}

Procedure PrintBIOS(Prn : Integer;Cc : Char); Assembler;
  MOV   AL,Cc
  MOV   DX,Prn
  AND   DX,3   {for "security"}
  PUSH  BP }
  INT   17H
{ POP   BP
  POP   DS }
   PrintBIOS(1,'A');   {for printer LPT2: }

For more information look in the famous Ralph Brown's Interrupt list at INT 17H.
AH = 00h
AL = character to write
DX = printer number (00h-02h)
AH = printer status  (see below)

AH = 01h
DX = printer number (00h-02h)
AH = printer status (see below)
Note: Some printers report that they are ready immediately after initialization when they actually are not; a more reliable result may be obtained by calling AH=02h after a brief delay
Note: This does usually not initialize the printer, eg. set standard font, size etc. The printers use customized Esc - commands for their initialisation, look in the manuals.

AH = 02h
DX = printer number (00h-02h)
AH = printer status
Note: PRINTFIX from MS-DOS 5.0 hooks this function and always returns AH=90h

Bitfields for printer status:

Bit(s)  Description
 7      not busy       80H
 6      acknowledge    40H
 5      out of paper   20H
 4      selected       10H
 3      I/O error       8H
 2-1    unused
 0      timeout         1H

Better operating systems than MSDOS offer buffered printing, e.g. DR-Multiuser DOS  and  REAL/32.
This is very useful because of the multitasking overhead for printer attachment etc. which must be  done
by the OS for every system call.

More about Printing
Dr. John Stockton's Printing article:

12345678 {      This Unit is a replacement for the Printer unit that   } 
{ came with Turbo Pascal Version 4.0 and 5.0.  Its purpose is } 
{ fourfold.                                                   } 
{                                                             } 
{ First: It will allow a user to change the printer port that } 
{ the LST file is writing to on the fly.  This takes the      } 
{ place of LstOutPtr and the routine on page 369 of the Turbo } 
{ Pascal Version 3.0 manual.                                  } 
{                                                             } 
{ Second: This unit will free the programmer from the need to } 
{ check to see if the printer is ready to accept characters.  } 
{ If the printer is not ready, the unit will place a line on  } 
{ the screen prompting the user to fix the printer and press  } 
{ a key.  This process will continue until the printer is     } 
{ made ready or the user Aborts or Ignores the printing       } 
{ operation.  NOTE: BIOS does not return correct error codes  } 
{ for Non-Existent printers or printer ports because the      } 
{ printer is not there to return any error codes at all.      } 
{                                                             } 
{ Third: This unit will also circumvent DOS's stripping of a  } 
{ Ctrl-Z ($1A, the End Of File character) when writing to the } 
{ printer as an ASCII device. Ctrl-Z was usually sent as part } 
{ of a graphics string to a printer.  In version 3.0 of Turbo } 
{ Pascal, an ASCII device was opened in binary mode.  In      } 
{ version 4.0, an ASCII device is opened in ASCII mode and    } 
{ DOS thus strips a Ctrl-Z.                                   } 
{                                                             } 
{ Fourth: This also provides a good example of a Text file    } 
{ device driver.                                              } 
{ Warning: This Driver has not been tested on a non-buffered  } 
{ printer, as the smallest buffer I could find was 80 chars.  } 

{      Type this to a file called PRINTIT4.PAS                } 

{ Written by the Lizard King, Clifford Roche email


Unit Print_it_4; 


Uses DOS,CRT; 
  LST : Text;                      { Public LST file variable } 
Function VerifyPortL (LPT: WORD): Byte; 

Procedure SetPrinter( Port:Byte ); 
{      SetPrinter sets the printer number to Port where Port  } 
{ is 'n' in 'LPTn'.  ie.  To write to LPT1: SetPrinter(1),    } 
{ for LPT2: SetPrinter(2).  SetPrinter changes the Port that  } 
{ subsequent Write operations will write to.  This lets you   } 
{ change the printer that you are printing to on the fly.     } 


Function PrinterCheck( PortNum, Error:Byte; Var Pos:Word):Boolean; 
  Response : Char; 
  Regs     : Registers; 
  OldTextAttr : Byte; 
  NewPos : Word; 
  Response := 'R';                { Assume Retry              } 
  NewPos := Pos;                  { Assume no Error           } 
  While ((Error and $29) <> 0) and (Response = 'R') do 
    NewPos := Pos - 1;            { Decrement to reprint char } 
    OldTextAttr := TextAttr;      { Save Old Attribute        } 
    TextAttr := TextAttr or $80;  { Turn on Blink Bit         } 
    Write( #13'Printer Not Ready!   ' );     { Write the user } 
    Write( 'A) Abort, R) Retry, I) Ignore '#13 ); { a message } 
    TextAttr := OldTextAttr;      { Restore Old Attribute     } 
    Response := Upcase( Readkey );{ Read Char and upcase it   } 
    ClrEol;                       { Clear Line                } 
    If Response = 'A' then        { If Abort then exit        } 
      halt( 160 );                { Note: Uses Exit Proc.     } 
    If Response = 'R' then 
      Regs.AH := 2;                 { Code for Check Status   } 
      Regs.DX := PortNum;           { Printer port number -1  } 
      Intr($17,Regs);               { Call printer service    } 
      Error := Regs.AH;             { save Printer Error Code } 
                                    { 00000001 = Time Out     } 
                                    { 00000010 = Unused       } 
                                    { 00000100 = Unused       } 
                                    { 00001000 = I/O Error    } 
                                    { 00010000 = Selected     } 
                                    { 00100000 = Out of Paper } 
                                    { 01000000 = Acknowledge  } 
                                    { 10000000 = Not busy     } 
  PrinterCheck := Response = 'R'; 
  Pos := NewPos; 

Function PrinterReady(PortNum:Byte):Boolean; 
  Ready    : Boolean; 
  Dummy    : word; 
  Regs     : Registers; 
    Regs.AH := 2;                   { Code for Check Status   } 
    Regs.DX := PortNum;             { Printer port number -1  } 
    Intr($17,Regs);                 { Call printer service    } 
    PrinterReady := PrinterCheck( PortNum, Regs.AH, Dummy ) 

{      The following routines MUST be FAR calls because they  } 
{ are called by the Read and Write routines.  (They are not   } 
{ Public (in the implementation section) because they should  } 
{ only be accessed by the Read and Write routines.)           } 


{      LSTNoFunction performs a NUL operation for a Reset or  } 
{ Rewrite on LST (just in case).                              } 

Function LSTNoFunction( Var F: TextRec ): integer; 
  LSTNoFunction := 0;                    { No error           } 

{      LSTOutputToPrinter sends the output to the Printer     } 
{ port number stored in the first byte or the UserData area   } 
{ of the Text Record.                                         } 

Function LSTOutputToPrinter( Var F: TextRec ): integer; 
  Regs: Registers; 
  P : Word; 
  With F do 
    P := 0; 
    If PrinterReady( F.UserData[1] ) Then 
    While (P < BufPos) do 
      Regs.AL := Ord(BufPtr^[P]); 
      Regs.AH := 0; 
      Regs.DX := UserData[1]; 
      If Not PrinterCheck( F.UserData[1], Regs.AH, P ) then 
        P := BufPos; 
    BufPos := 0; 
  LSTOutputToPrinter := 0              { No error           } 


{      AssignLST both sets up the LST text file record as     } 
{ would ASSIGN, and initializes it as would a RESET.  It also } 
{ stores the Port number in the first Byte of the UserData    } 
{ area.                                                       } 

Procedure AssignLST( Port:Byte ); 
  With TextRec(LST) do 
      Handle      := $FFF0; 
      Mode        := fmOutput; 
      BufSize     := SizeOf(Buffer); 
      BufPtr      := @Buffer; 
      BufPos      := 0; 
      OpenFunc    := @LSTNoFunction; 
      InOutFunc   := @LSTOutputToPrinter; 
      FlushFunc   := @LSTOutputToPrinter; 
      CloseFunc   := @LSTOutputToPrinter; 
      UserData[1] := Port - 1;  { We subtract one because } 
  end;                          { DOS Counts from zero.   } 

function VerifyPortL (LPT: Word): Byte; 
{Pass 1 in LPT to see if the printer is hooked up.} 
    mov ah,2 
    mov dx,LPT 
    dec dx 
    int $17 
    mov @Result,ah 
end;  {GetPrinterStatus} 

Procedure SetPrinter( Port:Byte ); { Documented above     } 
  With TextRec(LST) do 
    UserData[1] := Port - 1;{ We subtract one because DOS } 
End;                        { Counts from zero.           } 

Begin  { Initialization } 
  AssignLST( 1 );           { Call assignLST so it works  } 
end.                        { like Turbo's Printer unit   } 

This file is available in textmode for download with the right mouse button.printit4pas.txt

Glaser    Subject:  Re: HELP WITH PRINTING!!!
      Date:  Sat, 11 Jul 1998 11:28:35 GMT
      From: (Horst Kraemer)
Organization:Unlimited Surprise Systems, Berlin
 Newsgroups: comp.lang.pascal.borland

On Sat, 11 Jul 1998 09:26:58 +0100, Jim Barr
<> wrote:

>>:>And do not forget that if you are reading from a file and printing to
>>:>the printer, you MUST close the file to ensure that ALL the file goes to
>>:>the printer

Clearly, what you said, cannot be true.

You are reading data from a file (read(f,blah)) and sending them
immediately to the printer "file" (write(lst,blah).

If you _did_ read data from the file the data are in your variable
blah and if you sent 'blah' then to the printer they are going to be
printed. The fact of closing the file f _after_ already having got the
data into your program and sent them to the printer cannot have any
inpact on the past.

Things are dramatically different if you _write_ something _to_ a TEXT
file on disk, though. You are obviously mixing up "reading from a disk
file" and "writing to a disk file".

var f:TEXT;

After executing this program you will have and empty file 'bla' (0
bytes) on disk. The reason is that a 'write' doesn't go directly do
disk in the moment it is executed. It will go to a buffer (of size 128
bytes by default) which is part of the variable f. If the file is a
_disk_ file, data from the buffer will be written do disk (more
precisely: TP will hand the data to a DOS-function which will write
the data now or later to disk) only in one of the 3 cases a) b) c).

a) the buffer is full
b) flush(f)
c) close(f)

In the above case the buffer is not full, i.e. the data in the buffer
will never go to DOS because there was no flush(f) and no close(f).

The action of flush(f) is : commit the buffer data to DOS.
The file will stay open. close(f) will implictly call flush(f).

There are very few situations why you would ever use flush(f) in a TP
program. One of them is:

You are keeping a log file which you rewrite/append at program startup
and which you intend to close at program exit. If your program will
crash with a run time error or if the user cancels the program with ^C
or ^Break (in case the program does not inbit this by
CRT.checkbreak:=false _and_ SetCbreak(false)) the log file will be
closed by DOS at program exit but the buffer of logfile may still
contain data which are "written" by the program but not yet commited
to do DOS. DOS's 'close' does not know that there are buffered data in
the TP program.

In this case it is advisable to flush it after every writeln.


to be sure that everything what was "written" by the program up to the
possible Break/RTE will go to disk.

There is a trick to avoid the necessity of flush(log). _After_
resetting/appending a TEXT file use

        with textrec(f) do FlushFunc:=InOutFunc;

Now every write(f,..)-command will automatically execute an implicit
flush(f), i.e. it will behave as undelayed output to the screen.



Franz Glaser