(* Copyright 1990 by Clarence Wilkerson. All rights reserved *) (* This emulator has reached the endpoint of its evolution, and I've decided to make it available to the public in source code form. There are other shareware or public domain emulators, but none that I know of with source code. One interesting feature is that it is almost entirely in Turbo Pascal. I have not included because of its length the CCP assembly language code. I had at one time plans to emulate this in Turbo Pascal. *) { Things left to do 1) IOBYTE ok 2) ENHANCED CCP by offloading functions to turbo nope 3) TERMINAL EMULATION ok H19, check nansi code 4) SOFTWARE EMULATION for Z80 nope and support for z80 boards nope 5) Emulate the FCB's better ok 6) bootup configuration file ok 7) allow cp/m 3.0 emulation nope } program v20boot; uses crt,dos,printer; (* This is an CP/M 2.2 emulation program using the NEC v20 8080 emulation mode. Turbo Pascal 4.0-5.5 revised edition . The major design aim was to do this almost exclusively in Turbo Pascal rather than assembler. Structure of the source: 1) V20BOOT.PAS TP 4.0-5.5 pascal source with 99.9% of 8088 code 2) GOTOCPM.ASM MASM source file for routine to enter V20 8080 emulation mode. 3) V20BDOS.ASM ASM80 source for stub BDOS and BIOS in 8080 code 3) Not included because of size V20CCP.ASM minor changes to ZCPR v.2 , stripping user code and submit cpability Binary files included: 1) V20BOOT.EXE complete compiled version 2) MEDLEY.ARC set of public domain CP/M programs.. LASM, MLOAD DISK76, SD useful for rebuilding and testing 3) V20BDOS.OBJ linkable form of 8080 code V20bdos 3) V20CCP.OBJ " "" V20CCP 4) GOTOCPM.OBJ " " of help routine, gotocpm used as external routine in Pascal source. Description: General Theory: The V20-30 series contains an 8080 emulation mode. The mode is entered by executing a "BRKEM XXX", where XXX is a software interrupt between 0 and $ff. Many of these interrupts on a pc are reserved for other functions. You may have to poke around on your fully configured system to find a set of 4 consecutive interrupts to be used by this program. I use $C0-> C4 as the default. This is changeable in the INITV20.CPM file. The interrupt points to a segment:offset, and the 8080 code begins executing at that point. The registers for the emulation have the obvious matches to the 8088 registers, except that 8080 sp = 8086 bp. Also, you must set the 8080 data segment in ds before you enter the BRKEM. The 8080 emulation mode has calls to execute 8088 code, via interrupt calls, and to return from emulation mode permanently. I have implemented most of the BDOS-BIOS emulation in Turbo Pascal, with access from the 8080 code via these interrupt calls. In short, this programs allocates a 8080 code segment, sets up bdos and bios stubs in it, and then enters emulation mode. The CCP used is a slightly modified ZCPR, so the look and feel is aprroximately that of a stock CP/M 2.2 system. The program goes to some trouble to emulate CP/M bdos calls exactly, as well as the console - printer oriented bios calls. One can write a fairly short emulator if the exactitude of bdos calls is ignored, and it might suffice for some purposes. However, there is a difference in the File control blocks for CP/M versus MSDOS, and several CP/M calls have no exact MSDOS analogue. For example, few of the first generation emulators will run the directory programs SD or DISK76. Often, however, assemblers and compilers will run under the direct translation method. On the other hand, this program will not run the WordStar Install program correctly, since it apparently tries to implement random disk access by manipulating reserved fields in the FCB. All BDOS functions are supported, including getalloc and get DPB, ( this was important for using SD and DISK76 ). Not many of the bios functions are supported, espcially the disk oriented ones. The main thing this loses for you is the direct ability to use CP/M diskettes, and to use DU, the disk zapper. I made up for the loss of CP/M disks with my READCPM utility, also written in Turbo Pascal. The DU loss is tougher --DU is very handy and semi-programmable. I find Norton's Utilities as a poor replacement in some aspects. It wouldn't take that much effort to merge my READCPM and this code, but that's another project. One limitation is that this V20 emulation mode does not run z-80 code. However, this Turbo program is written so that only a few changes would be required to support a memory mapped Z80 card or software 8080 or z80 emulation. Joan Riff's Z80MU exists already, however. CP/M features missing stem from my hacking which eliminated from ZCPR CCP user numbers and support for SUBMIT. To make up for this lack of support for SUBMIT, the standard console functions from bdos and bios are left in place so that cp/m utilities such as EX can be used. Furthermore, the IOBYTE feature is implemented, and one can add new physical devices to supplement the ones included ( raw io and H19 emulation ). *) const ESC = #27; CARRYFLAG = $01; WHICHCOMM : byte = $01; { USE COMM1 } DEFAULTIOBYTE : byte = $81; { CRTT, LPTT } BDOSSIZE :word = $600; {12 cp/m sectors} { size of disk file in cp/m sectors } CCPSIZE :word = $800; {16 cp/m sectors} NUMALLOCBYTES = $100; {bytes in 8080 bdos for bit map of allocation } escinflag: boolean=false; escoutflag: boolean=false; escstr: string[64] =''; { 8088 interrupts used } { should make this configurable } baseinterrupt : byte = $c0; coldb = $0; { plus baseinterrupt starting point } ccphandle = $1; bdoshandle = $2; bioshandle = $3; { these handles are indices into table of addresses } { special V-20 instructions } CALLN = $eded; { used from 8080 mode not reversed byte order } RETEM = $edfd; { used from 8080 mode stored in mem as ed,fd } BKREM = $0fff; { used from 8086 mode } RETI = $CF; { used from 8086 mode } { cpm-80 constants } { i/o byte masks } consolemask = $03; consoleshift= $0; readermask = $0c; readershift = $2; punchmask = $30; punchshift = $4; listmask = $c0; listshift = $6; { possible console values SHR 0 } TTY = 0; CRTT = 1; (* CRT conflict with DOS unit in TP 4.0 *) BAT = 2; UC1 = 3; { possible reader values SHR 2 } { TTY = 0; } RDR0 = 1; UR1 = 2; UR2 = 3; { possible punch values SHR 4 } { TTY = 0 } PUN = 1; UP1 = 2; UP2 = 3; { possible list values SHR 6 } { TTY = 0; } { CRT = 1; } LPTT = 2; UL1 = 3; { output to a file } { offsets in CPM-80 code segment } ccpaddr : word = $f200; bdosaddr :word = $fa00; biosaddr :word = $ff00; bdosdpb = $9; { + bdosaddr } bdosalloc= $19; { + bdosaddr } jmp = $c3; jmpwarm = $0; jmpbdos = $5; kfcb1 = $80; kfcb2 = $5c; cpmstack = $ff80; (* default value *) cpmoutname: string[64] ='V20OUT.LST'; { list output to a file } cpm80 : word = 0; type segment = array[0..$7ff0] of word; filler = array[0..$1000] of word; reg8080 = record case integer of 1: (a,x,l,h,c,b,e,d : byte); { 8080 mode } 2: (achr,u,lchr,hchr,cchr,bchr,echr,dchr: char); 3: (psw,hl,bc,de : word); { word mode } end; reg8080ptr = ^reg8080; interruptrec = record loc : byte; offset : word; segment : word; end; regpakptr =^registers; { cp/m and dos types } fcbname = array[0..11] of char; nstring = string[64]; anystring = string[255]; dpbblk = record case integer of 1: (spt : word; bsh : byte; blm : byte; exm : byte; dsm : word; drm : word; al01 : word; cks : word; off : word; ); 2: (dpb : array[1..15] of byte); end; allocblk = record case integer of 1: ( allshort: array[1..16] of byte); 2: ( alllong: array[1..8] of word); end; cpmfcb = record case integer of 1: ( drive: byte; name : array[1..11] of char; { 1..11 } curext : byte; { 12 } rs12 : word; { 13..14 } recused : byte; { 15 } ablk : allocblk; { 15..31 } currec : byte; { 32 } randrec: word; { 33..34 } overflow: byte { 35 } ); 2: (fcb : array[0..35] of byte); end; dosfcb = record case integer of 1: ( drive: byte; { 0 for default before opening, then A=1,..etc } name : array[1..11] of char; { 8 + 3, spaces to fill both pieces} curext : word; recsize : word; { set to 80 hex on opening } filesize: array[1..4] of byte; date : word; time : word; reserved : array[24..31] of byte; currec : byte; randrec: array[1..2] of word); { user should set to 0 normally } 2: (fcb : array[0..36] of byte); 3: ( cpmseqfcb: array[0..32] of byte; fillit : array[33..36] of byte) { for sequential records }; 4: (cpmrandfcb: array[0..35] of byte; myextra : byte { for random access }); end; dosdir = record case integer of 1: ( dskflg : byte; name : array[1..8] of char; ext : array[1..3] of char; attr : byte; rsvrd : array[1..10] of byte; time : word; date : word; strtcluster : word; size : array[1..4] of byte); 2: ( cpmdir : array[0..31] of byte); end; fname = array[1..11] of char; alloc = array[0.. NUMALLOCBYTES] of byte; { replace absolute variables in the 8080 address space by pointers } var myseg : word; mystack : word; myinterrupt : word; oldinterrupt : array[0..3] of interruptrec; newinterrupt : array[0..3] of interruptrec; cursX, cursY : byte; { h-19 emulation } bioschar : ^char; bioswhich,biosbyte : ^byte; cpmseg : ^segment; extra : ^filler; biosreg : reg8080ptr ; bdosreg : reg8080ptr ; ccpregs : reg8080ptr ; user : ^byte ; { cpm80:4;} iobyte : ^byte ; { cpm80:3;} abortflag : ^byte ; { cpm80:$ffff;} { put <> 0 to abort } zero : ^byte ; { cpm80:0; } { misc. variables } lastdma : word; ccpfile : file; bdosfile : file; cpmout : text; { for output to disk file from list } initf : text; { set cp/m parameters } z : registers; myownfcb : dosfcb; listouthandle : word; procedure v20ccp ; external ; {$L v20ccp.obj} { 8080 code stored in "v20ccp.obj" } { contains 8080 code ccp extracted from zcpr 2.0 } { with user numbers removed, and SUBMIT file removed } { Fancier versions could pass most functions off to Turbo routines } { such as assigning MSDOS paths for user numbers or drives } { changing directories } procedure v20bdos ; external ; {$L v20bdos.obj} { 8080 code stored in "v20bdos.obj" } { has cp/m 2.2 console handlers, passes rest off to routines here to } { imitate with msdos functions} { Bios is only a stub, handles character routines only, others when } { graceful imitation is possible} { fancier version could add cp/m disk imitation for a floppy} { but not necessary given my read cp/m } function getcurrent : byte; var u : registers; begin u.ah:=$19; msdos(u); getcurrent:=u.al and $0f; end; { getcurrent } procedure resetdisk; var u : registers; begin u.ah:=$0d; msdos(u); user^:=2; end; { resetdisk } procedure setjumps; var u : registers; s : ^fname; j : integer; temp: byte; begin temp:=iobyte^; { now intialize lowest page of memory in the cpm80 seg } fillchar(zero^,$ff,0); iobyte^:=temp; abortflag^:=0; { special } mem[cpm80:0] :=jmp; memw[cpm80:1]:=biosaddr+3; mem[cpm80:5] :=jmp; memw[cpm80:6]:=bdosaddr + 6; { skip serial number } user^:= getcurrent; { blank fcb's } s:=ptr(cpm80,$5d); for j:=1 to 11 do s^[j]:=' '; s:=ptr(cpm80,$6d); for j:=1 to 11 do s^[j]:=' '; end; { set jumps } procedure putnibble( x : byte); begin x:=x and $0f; if x < 10 then write(chr($30+x)) else write(chr(55+x)); end; procedure putbyte( x : byte ); begin x:=x and $ff; putnibble(x shr 4); putnibble(x); end; procedure putword( x : word); begin putbyte(hi(x)); putbyte(lo(x)); end; procedure putinteger( x : integer); begin putbyte(hi(x)); putbyte(lo(x)); end; procedure setinterrupt ( x : interruptrec); { set interrupt vector x to xseg:xofs } var z : registers; begin z.ds:= x.segment; z.dx:= x.offset; z.ah:= $25; z.al:= x.loc; { load ah with 25h , al with interrupt number} msdos(z); end; { setinterrupt } procedure getinterrupt ( var x : interruptrec); { get interrupt vector x into xseg,xofs } var z : registers; begin z.ah:= $35; z.al:= x.loc; msdos(z); x.segment:=z.es; x.offset :=z.bx; end; { getinterrupt } procedure setdefdma; var z :registers; begin z.ds:=cpm80; z.dx:=$80; z.ah:=$1a; { set default dma while we're at it } msdos(z); end; { setdefdma } procedure loadbdos; var u : ^byte; bbbb : ^byte; begin u :=ptr(cpm80,bdosaddr); bbbb:=@v20bdos; move(bbbb^,u^,BDOSSIZE); end; { load bdos } procedure loadccp; var u : ^byte; cccc : ^byte; begin u:= ptr(cpm80,ccpaddr); cccc:=@v20ccp; move(cccc^,u^,CCPSIZE); end; { move ccp } procedure warmboot( var x : registers); var u : registers; begin { reload ccp and reset dma to $80 } loadccp; resetdisk; setdefdma; { stack and DMA } lastdma:=$80; setjumps; { clean up 0 to $ff to ffh in cpm80 seg } x.bx:=ccpaddr + 3; end; { warmboot } procedure notimplemented; var ch : char; begin write(#7,#7,'Warning! Bios call '); putbyte(biosreg^.a); writeln('H not implemented.'); writeln; write('Resume 8080 program or abort? '); ch:=readkey; write(ch); if not ( upcase(ch) = 'R') then abortflag^ := $ff else writeln; end; { not implemented } { bios routines next } { cpm/80 sends parameters in c or bc, gets back in a and hl } { first are physical character output routines } { CRT device } procedure CRTOUTPUT( x : byte); var u : registers; begin write(chr(x)); end; { crtoutput } procedure CRTINPUT( var x : byte); var ch: char; begin ch:=readkey; x:=ord(ch); end; { CRTINPUT } procedure CRTSTATUS( var x : byte); var u : registers; begin if keypressed then x:=$ff else x:=0; end; { CRTSTATUS } { TTY device } { use native TURBO routines } { emulates H-19 terminal codes } var escqueue : char; procedure ttystatus( var x : byte); begin if escinflag then x:=$ff { if have some in queue } else if keypressed then x:=$ff else x:=$0; end; { ttystatus } procedure longescape; var x,y : byte; begin { now take a look at longer escape sequences } case escstr[2] of { cursor positioning } 'Y' : if length(escstr) >=4 then begin y:=ord(escstr[3]) - 31; x:=ord(escstr[4]) - 31; gotoxy(x,y); escstr:='';escoutflag:=false; end; { various setup codes not implemented } 'x','y' : if length(escstr) >=3 then begin escstr:=''; escoutflag:=false; end; end; { case } end; { long escape } procedure processescape; { try minor emulation of h-19} var x,y,i,j : byte; notproc : boolean; begin notproc:=false; case escstr[2] of 'E' : clrscr; 'M' : delLine; 'L' : InsLine; 'K' : ClrEol; 'H' : gotoxy(1,1); 'J' : { EOS } begin x:=whereX; y :=whereY; clreol; for j:=y +1 to 25 do begin gotoxy(1,j);clreol; end; gotoxy(x,y); end; 'p' : lowvideo; 'q' : NormVideo; 'l' : { erase entire line begin delLine; InsLine; end; 'N' : { delete char } write(' '); 'A' : { cursor up } begin x:=whereX; y:=whereY -1; if y < 1 then x:=1; gotoxy(x,y); end; 'B' : { cursor down } begin x:=whereX; y:=whereY +1; if y > 25 then y:=25; gotoxy(x,y); end; 'C' : { cursor right } begin x:=whereX +1; y:=whereY; if x > 80 then x:=80; gotoxy(x,y); end; 'D' : { cursor left } begin x:=whereX-1; y:=whereY; if y < 1 then x:=1; gotoxy(x,y); end; 'j' : { push cursor } begin cursX:=whereX; cursY:=whereY; end; 'k' : { pop cursor } gotoxy(cursX,cursY); 'n' : begin end; '@' : begin end; { go to insert mode } 'b' : begin { erase beginning of display } x:=whereX;y:=whereY; for i:=1 to y-1 do begin gotoxy(1,i); clreol; end; gotoxy(1,y); for i:=1 to y do write(' '); gotoxy(x,y); end; 'I' : { reverse index } begin x:=whereX; y:=whereY -1; if y < 1 then y:=1; gotoxy(x,y); end; 'o' : begin { erase beginning of line } x:=whereX;y:=whereY; gotoxy(1,y); for i:=1 to x do write(' '); gotoxy(x,y); end; { erase beginning of line } 'F','G','[','=','>','t','u','<','z','Z','#',']','}','{','v','w' : begin end; 'Y','y','x' : begin notproc:=true; longescape; end; end; { case } if not notproc then begin escoutflag:=false; escstr:=''; exit; end; end; { processescape } procedure ttyoutput( x : byte); var s,y,yy : byte; j : integer; begin if x=27 then begin { start accumulating chars again } escoutflag:=true; escstr:= ''+ ESC; { start over } exit; end; if not escoutflag then begin if x <> 9 then write(chr(x)) { expand tabs } else begin s:=WhereY;y:=WhereX; yy:=y; y:= 9 + 8*(y div 8); if y > 80 then y:=80; (* do it with blanks *) for j:=yy to y do write(' '); gotoxy(y,s); end; end else begin { check for escape sequence abort } if x=30 then begin escoutflag:=false; escstr:='';exit;end; escstr:=escstr+chr(x); processescape; end; end; { ttyoutput } function translate( ch : char) : char; { translate turbo scan codes to H-19 codes } begin case ch of #75,#115,#178 : translate:='D'; #77,#116,#180 : translate:='C'; #72,#160,#175 : translate:='A'; #80,#164,#183 : translate:='B'; #71,#174 : translate:='H'; #59,#84,#94,#104 : translate:='S'; { function keys 1..5 } #60,#85,#95,#105 : translate:='T'; #61,#86,#96,#106 : translate:='U'; #62,#87,#97,#107 : translate:='V'; #63,#88,#98,#108 : translate:='W'; #66,#91,#101,#111: translate:='P'; { blue, grey, red } #67,#92,#102,#112: translate:='Q'; #68,#93,#103,#113: translate:='R'; else translate:=ch; end; { case } end; { translate } procedure ttyinput( var x : byte); var ch: char; begin { see if any in line already } if escinflag then begin x:=ord(escqueue); escinflag:=false; exit; end; { else wait for a real character } ch:=readkey; x:=ord(ch); { return the character } if (ch = ESC) and keypressed then begin ch:=readkey; escinflag:=true; escqueue:= translate(ch); { change on the fly } end; end; { ttyinput } { user console 1 routines to allow external CRT on COMM1 } { use interrupt 14h } procedure uc1status( var x : byte); var u : registers; begin u.ah:=3; u.dx:=WHICHCOMM; intr($14,u); { status now in u.ah } if (u.ah and $1) <> 0 then x:=$ff else x:=$0; end; { uc1status } procedure uc1input( var x : byte); var u : registers; begin repeat uc1status(x) until x = $ff; u.ah:=2; u.dx:=whichcomm; intr($14,u); x:=u.al; end; { uc1input } procedure uc1output( x : byte); var u : registers; begin repeat u.ah:=3; u.dx:=whichcomm; intr($14,u); until u.ah and $40 <> 0; u.ah:=1; u.al:=x; u.dx:=whichcomm; intr($14,u); end; { uc1output } var nextlist : byte; { routed to a file, V20OUT.LST, presently } procedure ul1output( x : byte); var u : registers; begin u.bx:=listouthandle; u.ds:=dseg; u.dx:=ofs(nextlist); { a 1 byte buffer } nextlist:=x; u.cx:=1; u.ah:=$40; msdos(u); if u.flags and carryflag <> 0 then write(#7); if u.ax<> 1 then write('?'); end; { ul1output } procedure breader( var x : byte); var ch : char; u : registers; begin case (iobyte^ and readermask) shr readershift of TTY : begin ch:=readkey; x:=ord(ch); end; RDR0 ,UR1, UR2:begin u.ah:=$03; msdos(u); x:=u.al; end; end; { case } end; { breader } procedure bpunch( x : byte); var u : registers; begin case (iobyte^ and punchmask) shr punchshift of TTY,PUN,UP1,UP2: begin u.ah:=$04; u.dl:=x; msdos(u); end; end; end; { bpunch } procedure blist( x : byte); begin case (iobyte^ and listmask) shr listshift of TTY: TTYOUTPUT(x); CRTT: crtoutput(x); LPTT: write(lst,chr(x)); UL1: ul1output(x); end; { case } end; { blist } procedure blistst( var x : byte); begin x:=$0ff; end; { blistst } procedure bconst( var x : byte); begin case (iobyte^ and consolemask) of TTY : TTYSTATUS(x); CRTT: CRTSTATUS(x); BAT : if keypressed then x:=$ff else x:=0; UC1 : uc1status(x); end; { case } end; procedure bconin( var x : byte); var u : registers; ch : char; begin { bconin } case (iobyte^ and consolemask) of CRTT: crtinput(x); TTY : ttyinput(x); BAT : breader(x) ; UC1 : uc1input(x); end; { case } end; { bconin } procedure bconout( x : byte); begin case (iobyte^ and consolemask) of TTY : ttyoutput(x); CRTT : crtoutput(x); BAT : blist(x); UC1 : uc1output(x); end; end; { bconout } procedure bseldsk( var x : registers); var z : registers; begin z.ah:=$0e; z.dl:=x.cl; { restored at end } msdos(z); x.bx:=0; { no DPH for now } end; { bseldsk } procedure bsetdma(var x : registers); var z : registers; begin { do msdos STA } { DMA address in BC } z.ah:=$1a; z.dx:=x.cx; { x.dx is restored at end ; } lastdma:=x.cx; msdos(z); end; { bsetdma } function bsectran( x : word) : word; begin bsectran:=x; end; { sectran } var x : registers; var biostmp : byte; { next routine accessed only via CALLN BIOSHANDLE from 8080 mode } procedure biosemul; interrupt; (* for turbo 4.0, try built in stuff *) { communicates via biosreg, a has call, bc data, returns in hl } var x :registers; begin (* writeln('Got to BIOS. call ',biosreg^.a); *) x.CX:=biosreg^.bc; { pass data on } x.ds:=cpm80; case bioswhich^ of 4: (* bconout(x.cl);*) case (iobyte^ and consolemask) of CRTT : write(bioschar^); TTY : ttyoutput(biosbyte^); BAT : blist(biosbyte^); UC1 : uc1output(biosbyte^); end; 0: abortflag^:=1; (* cold boot should abort *) 1: begin warmboot(x); x.bx:=ccpaddr + 3; { jump to beginning of ccp } end; { check this for the new bdos !!! } 2: bconst(x.al); 3: bconin(x.al); 5: blist(x.cl); 6: bpunch(x.cl); { punch } { do as COM1 later } 7: breader(x.al); { reader } { do as COM1 later } 8: begin end; { home } { fake it } 9: bseldsk(x); { does not return DPE pointer } 10: notimplemented ; { settrk } 11: notimplemented ; { setsec } 12: bsetdma(x); 13: notimplemented ; { read } 14: notimplemented ; { write } 15: blistst( x.al); 16: biosreg^.hl:=bsectran(x.cx); { probably not used } else notimplemented; end; { case } biosreg^.a:=x.al; biosreg^.hl:=x.bx; (* wrong for 16 *) end; { bios emulation } { now bdos routines } { cpm80 sends parameters in e or de, gets back in a or hl } procedure nobdos( n : byte); begin writeln;write('BDOS call # ');putbyte(n);writeln(' not implemented.'); writeln; abortflag^:=1; end; { nobdos } function getalloc : word; var v : ^alloc; u : registers; begin v:=ptr(cpm80,bdosaddr+bdosalloc); fillchar(v^[0],NUMALLOCBYTES,$ff); { say that it's used } u.dl:=0; { default drive } u.ah:=$36; msdos(u); if u.ax=$ffff then writeln('Invalid CP/M drive number.'); if u.bx <> 0 then begin { compute number of free blocks, and mark them with a zero } if u.bx > (NUMALLOCBYTES shl 3) then u.bx:= NUMALLOCBYTES shl 3 ; v^[0]:= (not (u.bx and $0007)) and $ff; { partial block } fillchar(v^[$1], (u.bx shr 3 ),0); { whole blocks } end; getalloc:=bdosaddr+bdosalloc; end; { getalloc } { construct a fake dpb from the DOS data } { need this for programs like DISK76 and SD to work } function makedpb : word; var u : registers; fatid : byte; numalloc : word; { number of allocation blocks } secalloc : byte; { number of sectors per alloc block } physize : word; { number of bytes in physical sector } fake : dpbblk; cdpb : ^dpbblk; fat : ^byte; cpmphys : byte; small : boolean; blksize : word; { cpm alloc block size } begin cdpb:=ptr(cpm80,bdosaddr+bdosdpb); fillchar(cdpb^,16,0); with fake do begin u.ah:=$1b; { use newer dos call } msdos(u); fat:=ptr(u.ds,u.bx); fatid:=fat^; { in asm code, would have to protect DS } u.ah:=$36;u.dl:=0; msdos(u); numalloc:=u.dx; { number of allocation units } secalloc:=u.al; { physical sectors per allocation unit } physize:=u.cx; { size of physical sector } cpmphys:= physize shr 7; { 128 byte sectors per physical sector } {assume multiple of 128 } if fatid <> $f8 then begin { check for dos 1.x versus dos 2.x formats } if (fatid and 2) <> 0 then spt:= 8*cpmphys else spt:=9*cpmphys; end else begin { is harddisk } spt:=18*4; { 18 physical 512 byte sectors per track } end; { compute block size } { minimum of 1k for cpm } blksize:= ( secalloc * physize ) shr 10 ; { in 1024 chunks } if blksize = 0 then begin numalloc:=numalloc shr 1 ; blksize:=1; end; { smallest allowable cpm blocks are 1k, so adjust } if numalloc > 255 then small:=true else small:=false; { decides byte or word value for alloc blocks in cpm fcb } if (numalloc < 0) or ( numalloc > 8*$d0) then numalloc:=8*$d0; { only have $d0 bytes for allocation vector } case blksize of 1: begin bsh:=3; blm:=7; end; 2: begin bsh:=4; blm:=15; end; 4: begin bsh:=5; blm:=31; end; 8: begin bsh:=6; blm:=63; end; 16: begin bsh:=7; blm:=127; end; else writeln(#$0d,#$0a,#7,'Screwy blocksize.'); end; { case } { set extent mask block } { 16k per extent. exm is # of extents per dir entry -1} { there are 16 bytes of data/entry, so if 1 byte pointers, gives} { 16 blocks/entry. If 2 bytes per counter, gives 8 blocks/entry } if small then exm:= blksize -1 else exm:= (blksize shr 1) -1; if exm < 0 then begin writeln; writeln('Bad decode of BPB information giving invalid CP/M DPB.'); writeln; end; dsm:=numalloc-1; drm:=511; cks:=0; al01:=$ff00; off:=0; { no reserved tracks } end; { with fake } { pass a allocation vector .. a bit map of usage of disk blocks } cdpb^:=fake; makedpb:=bdosaddr+bdosdpb; end; { makedpb } var help : dosfcb; w : ^dosfcb; procedure setdma; { make sure the dma address is set correctly } var t : registers; begin t.ah:=$1a; t.ds:=cpm80; t.dx:=lastdma; msdos(t); end; { setdma } { CP/M programs seemed to destroy parts of the record in fcb needed } { by msdos to do findnext. The following kludge is to save the } { result of the fcb for each findfile or findnext for use later. } { If different fcbs were used, this could be a problem???? } { However, the big problem would be a fallacious match and then } { a delete, so avoid this by a check on the fcb address } { However, this did not work.... } { The CCP - EMUL interaction clobbers the DMA address somewhere } { kludge.. set the first directory byte = 0 always } { because msdos puts it to be disk number a=1,..e=5 } { sets returned size in size bytes of fcb since some programs } { eg, SD, use this to size the file, instead of the BDOS CALL } procedure fakeit; var u : ^dosdir; y, x : longint; z,t : byte; j : word; begin { adjust directory entry returned for right size and user } { MSDOS returns 1st byte to be disk number. CP/M expects user number } { Our CP/M emulation has only userr 0, so set this to be zero, and } { make the extent byte and the leftover sectors byte } u:=ptr(cpm80,lastdma); { look at the directory entry returned } { read the MSDOS file size from this directory entry } x:= longint(u^.size[1]) + (longint(u^.size[2]) shl 8) + (longint(u^.size[3]) shl 16) + (longint(u^.size[4]) shl 24) ; if x > $400000 then x:=$400000; { set a cut off size of 4 megs } y:= x shr 14 ; { number of 16k extents } { if y > 256, in trouble, so x < 256*16k = 4 meg } x:= x and $3FFF; { get remainder mod 16384 } x:= x shr 7; { number of physical sectors } t:= x ; for j:=12 to 31 do u^.cpmdir[j]:=0; { zero the rest of it } u^.cpmdir[0]:=0; { set user byte = 0 } u^.cpmdir[$f]:=t; { sectors left over from last extent } u^.cpmdir[$c]:=z; { extent byte } end; { fakeit } { according to cp/m rules, a program can do no disk calls between } { findfirst and findnext, or between sucessive findnext calls } { MSDOS claims you can, but not with same FCB } procedure findfile(var z : registers); var u : ^dosfcb; p : ^byte; begin u:=ptr(z.ds,z.dx); z.ah:=$11; msdos(z); p:=ptr(cpm80,lastdma); if p^<> $e5 then fakeit; myownfcb:=u^; { save the exact fcb for use for find next } z.bl:=z.al; { bdos returns a in l } {writeln('findfile a= ',z.al); } end; { findfile } procedure findnext( var z : registers); var u : ^dosfcb; p : ^byte; begin u:=ptr(z.ds,z.dx); u^:=myownfcb; { replace it by my copy from last time } z.ah:=$12; msdos(z); myownfcb:=u^; { hoard it away again } p:=ptr(cpm80,lastdma); { see what it returned } if p^<> $e5 then fakeit; { writeln('findnext a= ',z.al); } z.bl:=z.al; end; { findnext } { all disk calls go directly to MSDOS bdos } { character calls go through 8080 BIOS and on to 8086 bios emulator } { adapt for interrupt servicing } var f : ^dosfcb; mine : dosfcb; { NOTES on interfacing Turbo modules as CALLN service routines For a Turbo routine with no parameters or local storage the code produced is push bp mov bp,sp push bp jump xxxx xxxx: ....... Turbo code jmp yyyy yyyy: mov sp,bp pop bp ret I'm trying the sequence push ds mov ds, cs:[oldds] ; at xxxx pop ds mov sp,bp pop bp ; before jump to yyyy iret } var spin : word; cpmrecord : word; procedure bdosemul; { communicates with bdosreg pointer } { emulate cpm80 bdos using msdos bdos } { problem with record lengths.. 36 versus 37 bytes } { so doctor the fcbs on read and write calls } interrupt; (* for turbo 4.0 *) begin x.ah:=bdosreg^.c; { could do this in 8080 bdos } { prepare for msdos call } x.dx:=bdosreg^.de; x.ds:=cpm80; case x.ah of { calls 0-c are handled by 8080 bdos stub via bios calls } { this allows for more compatibility with CP/M-80 console handling } $D : msdos(x); $E..$10,$13: begin msdos(x); x.bl:=x.al; end; $11: findfile(x); $12: findnext(x); $14: begin { read seq. } setdma; f:=ptr( cpm80,bdosreg^.de); mine:=f^; { switch fcb's for disk calls } x.dx:=ofs(mine); x.ds:=seg(mine); mine.recsize:=$80; msdos(x); f^.cpmseqfcb:=mine.cpmseqfcb; { identify these pieces } if x.al=3 then x.al:=0; { read 1 more } x.bl:=x.al; if x.al=2 then writeln(#7,'BDOS read error. DTA too small.'); { msdos sends a 3 for partial record read. } { But cpm only knows about 128 byte records, so fake it } { one problem is that some dos programs do not terminate text file with $1a. should probably fake this } end; { read } $15: begin { write seq. } setdma; f:=ptr( cpm80,bdosreg^.de); mine:=f^; { switch fcb's for disk calls } mine.recsize:=$80; x.dx:=ofs(mine); x.ds:=seg(mine); msdos(x); x.bl:=x.al; f^.cpmseqfcb:=mine.cpmseqfcb; { identify these pieces } x.bl:=x.al; end; $16..$17: begin msdos(x); if x.al <> 0 then x.al:=$ff; x.bl:=x.al; end; $18: begin x.ah:=$19;msdos(x); x.ah:=$e; msdos(x); { now convert number in x.al to vector in x.bx} spin:=$ffff; spin:=spin shr (16 - x.al); x.bx:=spin; end; { log in vector } $19: begin msdos(x); x.bl:=x.al; end; $1a: begin { set dma address } lastdma:=x.dx; setdma; end; $1b: begin x.bx:=getalloc; x.al:=x.bl;end; { have to calculate .. use $1c dos call } $1c: begin x.al:=0;end; { set write protect..fake it } $1d: begin x.bx:=0; x.al:=0; end; { no write protected disks } $1e: begin x.bl:=0; x.al:=0; end; { set attributes } $1f: x.bx:=makedpb; { make it up in 8080 space } $20: begin { get/set user number } if x.dl=$ff then begin x.al:=0; x.bl:=0; end; end; $21..$22: begin { random read and write } writeln(#7,'Used Random READ-WRITE calls.'); setdma; f:=ptr( cpm80,bdosreg^.de); { get the record number to save } mine:=f^; { switch fcb's for disk calls } cpmrecord:=f^.randrec[1]; mine.randrec[2]:=0; x.dx:=ofs(mine); x.ds:=seg(mine); mine.recsize:=$80; msdos(x); f^.cpmrandfcb:=mine.cpmrandfcb; { identify these pieces } f^.randrec[1]:= cpmrecord; { restore the record number } x.bl:=x.al; end; $23 : begin { file size } (* writeln(#7,'Used File Size DOS call.'); *) f:=ptr( cpm80,bdosreg^.de); { get the record number to save } mine:=f^; { switch fcb's for disk calls } x.dx:=ofs(mine); x.ds:=seg(mine); mine.recsize:=$80; msdos(x); f^.cpmrandfcb:=mine.cpmrandfcb; { identify these pieces } x.bl:=x.al; end; $24 : begin { set random record number} (* writeln(lst,'Used SET RND REC'); *) f:=ptr( cpm80,bdosreg^.de); mine:=f^; { switch fcb's for disk calls } x.dx:=ofs(mine); x.ds:=seg(mine); mine.recsize:=$80; msdos(x); f^.cpmrandfcb:=mine.cpmrandfcb; { identify these pieces } x.bl:=x.al; end; $25 : begin { selective reset disk } x.ah:=$0d; msdos(x); x.al:=0; x.bl:=0; end; $28: begin { zero fill random write } x.ah:=$22; { fake it with usual random write } setdma; f:=ptr( cpm80,bdosreg^.de); { get the record number to save } mine:=f^; { switch fcb's for disk calls } cpmrecord:=f^.randrec[1]; mine.randrec[2]:=0; x.dx:=ofs(mine); x.ds:=seg(mine); mine.recsize:=$80; msdos(x); f^.cpmrandfcb:=mine.cpmrandfcb; { identify these pieces } f^.randrec[1]:= cpmrecord; { restore the record number } x.bl:=x.al; end; else nobdos(x.cl); end; { case } bdosreg^.hl:=x.bx; bdosreg^.a:=x.al; end; { bdos emulation } var st : ^anystring; (* this later piece is not implemented yet the thought was to rewrite the ccp in pascal and add various features *) procedure ccpemul; interrupt; begin case ccpregs^.c of 0 : begin { chdir } st:=ptr(cpm80,ccpregs^.de +1); ChDir(st^); user^:=getcurrent; end; 1 : begin st:=ptr(cpm80,ccpregs^.de +1); MkDir(st^) end; 2 : begin st:=ptr(cpm80,ccpregs^.de +1); RmDir(st^) end; 3 : begin st:=ptr(cpm80,ccpregs^.de +1); user^:=getcurrent; GetDir(user^+1,st^) end; else begin end; { pass them through } end; { case } end; { ccp emul } procedure logo; var x : word; begin clrscr; writeln('CP/M-80 Emulator for NEC V-20 and the IBM-PC'); writeln('Uses default setup or reads from file '); write('8080 Code Loaded at 0');putword(cpm80);writeln('H Segment'); write('Uses interrupts ');putbyte(baseinterrupt); write('..');putbyte(baseinterrupt+3);writeln('H.'); writeln; write('CCP loaded at ');putword(ccpaddr); write('H, BDOS at ') ;putword(bdosaddr); write('H, BIOS at ') ;putword(biosaddr);writeln('H.'); x:=ccpaddr; writeln('TPA of ',x:6,' bytes.'); write('Default IObyte is ');putbyte(defaultiobyte);writeln('H.'); if(defaultiobyte and 1) = 0 then writeln('TTY: device emulates H-19 terminal.'); writeln('UL1: device is ',cpmoutname,'.'); writeln; writeln('Copyright Nov.10, 1985 by Clarence Wilkerson'); writeln('All rights reserved.'); writeln('Modified for Turbo Pascal 4.0, December 6, 1988'); writeln; writeln; writeln('Exit with the BYE command, or by writing 1 in location $ffff.'); writeln;writeln; end; procedure findcpm; var uu : ^byte; begin { put some error checking here eventually } new(cpmseg); new(extra); { hope these are together } cpm80:=seg(cpmseg^[0]) + (ofs(cpmseg^[0]) shr 4 ) + 1 ; uu:=ptr(cpm80,0); fillchar( uu^,$ffff,#0); end; { findcpm } procedure interruptset; var i : integer; begin for i:=0 to 3 do begin oldinterrupt[i].loc:=baseinterrupt + i; newinterrupt[i].loc:=baseinterrupt + i; getinterrupt(oldinterrupt[i]); { store it away for restoration } end; newinterrupt[coldb].segment :=cpm80; newinterrupt[coldb].offset :=biosaddr; { DO 8080 COLD BOOT ROUTINE } newinterrupt[ccphandle ].segment :=seg(ccpemul); newinterrupt[ccphandle ].offset :=ofs(ccpemul); newinterrupt[bdoshandle].segment :=seg(bdosemul); newinterrupt[bdoshandle].offset :=ofs(bdosemul); newinterrupt[bioshandle].segment :=seg(biosemul); newinterrupt[bioshandle].offset :=ofs(biosemul); MEM[CPM80:$FFb0]:=BASEinterrupt+COLDB; MEM[CPM80:$FFb1]:=BASEinterrupt+CCPHANDLE; MEM[CPM80:$FFb2]:=BASEinterrupt+BDOSHANDLE; MEM[CPM80:$FFb3]:=BASEinterrupt+BIOSHANDLE; for i:=0 to 3 do setinterrupt(newinterrupt[i]); end; { interruptsetup } procedure opencpmoutput; var u : registers; begin { use dos 2.0 handle calls } u.ah:=$3c; u.ds:=seg(cpmoutname); u.dx:=ofs(cpmoutname) + 1; cpmoutname:=cpmoutname + #0; u.cx:=0; msdos(u); listouthandle:=u.ax; if u.flags and carryflag <> 0 then begin writeln;writeln('Error on list handle opening.'); writeln; end; end; { open cpm output } procedure closehandle( x : integer); var u : registers; begin u.ah:=$3e; u.bx:=x; msdos(u); if u.flags and carryflag <> 0 then begin writeln;writeln('Error on closing handle.'); end; end; { closehandle } procedure restoreinterrupts; var i : integer; begin for i:=0 to 3 do setinterrupt(oldinterrupt[i]); end; { restore interrupt vectors } procedure digest(var s : anystring); var i,n,code : integer; t,tt,ttt : anystring; begin t:=''; for i:=1 to length(s) do begin s[i]:=upcase(s[i]); if s[i] in ['A'..'Z','0'..'9','=','.','-','\',':','$'] then t:=t+s[i]; { strip spaces and junk off } end; { for } n:=pos('=',t); if n=0 then exit; tt:=copy(t,1,n-1); ttt:=copy(t,n+1,255); if tt= 'IOBYTE' then begin val(ttt,n,code); if (code = 0) and (length(ttt) >0 ) then defaultiobyte:=n; end { get iobyte } else if tt= 'LIST' then begin if length(ttt) > 0 then cpmoutname:=ttt; end { get LST output file } else if tt= 'INTER' then begin val(ttt,n,code); if (code = 0) and (length(ttt) >0 ) then baseinterrupt:=n ; end { get beginning interrupt number } else if tt= 'CCP' then begin val(ttt,n,code); if (code = 0) and (length(ttt) >0 ) then CCPSIZE:=n * 128; end { length of ccp in 128 byte chunks } else if tt= 'ADDR' then begin val(ttt,n,code); if (code = 0) and (length(ttt) >0 ) then CCPADDR:=n; end { beginning of ccp } else if tt= 'BDOS' then begin val(ttt,n,code); if (code = 0) and (length(ttt) >0 ) then BDOSSIZE:=n * 128; end; { length of bdos-bios in 128 byte chunks } end; { digest } PROCEDURE getinitfile; var n : integer; parmstrng : anystring; { allows change of most of the setup parameters } begin assign (initf,'INITV20.CPM'); {$I-} reset(initf); n:=IOresult; {$I+} if n <> 0 then exit; { no file } while not eof(initf) do begin readln(initf,parmstrng); digest(parmstrng); end; { while not eof } close(initf); end; { get initialization file } procedure initcpm; { set some cpm addresses } begin lastdma :=$80; bdosreg :=ptr(cpm80,$ffe0); biosreg :=ptr(cpm80,$ffd0); bioswhich:=ptr(cpm80,$ffd0); bioschar:=ptr(cpm80,$ffd4); biosbyte:=ptr(cpm80,$ffd4); ccpregs :=ptr(cpm80,$ffc0); user :=ptr(cpm80,4); (* user byte *) iobyte :=ptr(cpm80,3); (* iobyte is implemented *) iobyte^ :=defaultiobyte; abortflag:=ptr(cpm80,$ffff); zero :=ptr(cpm80,0); bdosaddr :=ccpaddr + CCPSIZE; end; { initcpm } procedure gotocpm(seg8080,stack8080pointer,brkemint : word); external; {$L gotocpm.obj} (* Has to be assembled with MASM or TASM ; ; ; procedure gotocpm(cpmseg,cpmstack,baseint : word); ;begin ; contains a kludge to get variable interrupt call ; or BRKEM call ; have to rewrite byte after the opcode ; on machines with a cache, could be a problem code segment word public assume cs:code public gotocpm gotocpm proc near push bp ; mov bp,sp ; standard set up code, no local variables mov ax,[bp + 4] ; brkem int # patch mov byte ptr cs:hack,al ; modify code mov ax,[bp + 8] ; using near call, so last param at bp + 4 ; since we pushed bp to start, plus return address mov dx,[bp +6] ; cpmstack to dx push bp ; used by V20 as stack pointer push ds ; set equal to CP/M data=code mov ds,ax ; [cpm80] set data segment for cpm mov bp, dx ; cpmstack to set cp/m stack db 0fh,0ffh ; BRKEM hack: db 000h; ; BRKEM XX ; uses XX interrupt ; overwritten in this version pop ds pop bp mov sp,bp ; standard exit code pop bp ret 6 ; 3 two byte parameters pushed on stack gotocpm endp code ends end *) var ExitSave : pointer; procedure CPMExit; (* get here by disk error, etc, so cold booting CP/M would be best solution. However, have not done. *) begin writeln('Error in CP/M handler.'); ErrorAddr:=Nil; ExitProc:=Nil; end; begin { main } GETINITFILE; findcpm; { get the 64k code space for cp/m-80 } logo; initcpm; { set up some cpm variables } setjumps; { set lower memory } loadccp; loadbdos; opencpmoutput; interruptset; { set up the interrupts } setdefdma; { set stack and DTA, data segment } iobyte^:=DEFAULTIOBYTE; myseg:=cpm80; mystack:=cpmstack; myinterrupt:=baseinterrupt; Exitsave:=ExitProc; ExitProc:=@CPMexit; (* set up segments and use BRKEM to get to emulator *) gotocpm(myseg,mystack,myinterrupt); ExitProc:=ExitSave; restoreinterrupts; closehandle(listouthandle); end .