From 0066dcaa1e86c47af7d6244f0cb1299e626ab9d1 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Fri, 11 Sep 2020 19:14:44 +0200 Subject: [PATCH] Ported CP/M 2.2 to pc (32k version) --- bin/DISKDEF.LIB | 251 +++ bin/ED.COM | Bin 0 -> 6656 bytes bin/LOAD.COM | Bin 0 -> 1792 bytes bin/MAC.COM | Bin 0 -> 11776 bytes bin/MBASIC.COM | Bin 0 -> 24320 bytes bin/STAT.COM | Bin 0 -> 5248 bytes bin/disks.asm | 10 + build.py | 96 ++ build.sh | 8 +- cpm22-b.zip | Bin 0 -> 42510 bytes src/MONITOR.z80 | 696 ++++++++ src/bios.z80 | 344 ++++ src/cpm22.z80 | 3738 +++++++++++++++++++++++++++++++++++++++++++ src/loader.z80 | 30 + src/putsys.z80 | 56 + src/ram_monitor.z80 | 687 ++++++++ src/rom_monitor.z80 | 45 +- upload.py | 16 +- upload_serial.py | 49 + 19 files changed, 5982 insertions(+), 44 deletions(-) create mode 100644 bin/DISKDEF.LIB create mode 100644 bin/ED.COM create mode 100644 bin/LOAD.COM create mode 100644 bin/MAC.COM create mode 100644 bin/MBASIC.COM create mode 100644 bin/STAT.COM create mode 100644 bin/disks.asm create mode 100755 build.py create mode 100644 cpm22-b.zip create mode 100644 src/MONITOR.z80 create mode 100644 src/bios.z80 create mode 100644 src/cpm22.z80 create mode 100644 src/loader.z80 create mode 100644 src/putsys.z80 create mode 100644 src/ram_monitor.z80 create mode 100755 upload_serial.py diff --git a/bin/DISKDEF.LIB b/bin/DISKDEF.LIB new file mode 100644 index 0000000..221fa21 --- /dev/null +++ b/bin/DISKDEF.LIB @@ -0,0 +1,251 @@ +; CP/M 2.0 disk re-definition library +; +; Copyright (c) 1979 +; Digital Research +; Box 579 +; Pacific Grove, CA +; 93950 +; +; CP/M logical disk drives are defined using the +; macros given below, where the sequence of calls +; is: +; +; disks n +; diskdef parameter-list-0 +; diskdef parameter-list-1 +; ... +; diskdef parameter-list-n +; endef +; +; where n is the number of logical disk drives attached +; to the CP/M system, and parameter-list-i defines the +; characteristics of the ith drive (i=0,1,...,n-1) +; +; each parameter-list-i takes the form +; dn,fsc,lsc,[skf],bls,dks,dir,cks,ofs,[0] +; where +; dn is the disk number 0,1,...,n-1 +; fsc is the first sector number (usually 0 or 1) +; lsc is the last sector number on a track +; skf is optional "skew factor" for sector translate +; bls is the data block size (1024,2048,...,16384) +; dks is the disk size in bls increments (word) +; dir is the number of directory elements (word) +; cks is the number of dir elements to checksum +; ofs is the number of tracks to skip (word) +; [0] is an optional 0 which forces 16K/directory entry +; +; for convenience, the form +; dn,dm +; defines disk dn as having the same characteristics as +; a previously defined disk dm. +; +; a standard four drive CP/M system is defined by +; disks 4 +; diskdef 0,1,26,6,1024,243,64,64,2 +; dsk set 0 +; rept 3 +; dsk set dsk+1 +; diskdef %dsk,0 +; endm +; endef +; +; the value of "begdat" at the end of assembly defines the +; beginning of the uninitialize ram area above the bios, +; while the value of "enddat" defines the next location +; following the end of the data area. the size of this +; area is given by the value of "datsiz" at the end of the +; assembly. note that the allocation vector will be quite +; large if a large disk size is defined with a small block +; size. +; +dskhdr macro dn +;; define a single disk header list +dpe&dn: dw xlt&dn,0000h ;translate table + dw 0000h,0000h ;scratch area + dw dirbuf,dpb&dn ;dir buff,parm block + dw csv&dn,alv&dn ;check, alloc vectors + endm +; +disks macro nd +;; define nd disks +ndisks set nd ;;for later reference +dpbase equ $ ;base of disk parameter blocks +;; generate the nd elements +dsknxt set 0 + rept nd + dskhdr %dsknxt +dsknxt set dsknxt+1 + endm + endm +; +dpbhdr macro dn +dpb&dn equ $ ;disk parm block + endm +; +ddb macro data,comment +;; define a db statement + db data comment + endm +; +ddw macro data,comment +;; define a dw statement + dw data comment + endm +; +gcd macro m,n +;; greatest common divisor of m,n +;; produces value gcdn as result +;; (used in sector translate table generation) +gcdm set m ;;variable for m +gcdn set n ;;variable for n +gcdr set 0 ;;variable for r + rept 65535 +gcdx set gcdm/gcdn +gcdr set gcdm - gcdx*gcdn + if gcdr = 0 + exitm + endif +gcdm set gcdn +gcdn set gcdr + endm + endm +; +diskdef macro dn,fsc,lsc,skf,bls,dks,dir,cks,ofs,k16 +;; generate the set statements for later tables + if nul lsc +;; current disk dn same as previous fsc +dpb&dn equ dpb&fsc ;equivalent parameters +als&dn equ als&fsc ;same allocation vector size +css&dn equ css&fsc ;same checksum vector size +xlt&dn equ xlt&fsc ;same translate table + else +secmax set lsc-(fsc) ;;sectors 0...secmax +sectors set secmax+1;;number of sectors +als&dn set (dks)/8 ;;size of allocation vector + if ((dks) mod 8) ne 0 +als&dn set als&dn+1 + endif +css&dn set (cks)/4 ;;number of checksum elements +;; generate the block shift value +blkval set bls/128 ;;number of sectors/block +blkshf set 0 ;;counts right 0's in blkval +blkmsk set 0 ;;fills with 1's from right + rept 16 ;;once for each bit position + if blkval=1 + exitm + endif +;; otherwise, high order 1 not found yet +blkshf set blkshf+1 +blkmsk set (blkmsk shl 1) or 1 +blkval set blkval/2 + endm +;; generate the extent mask byte +blkval set bls/1024 ;;number of kilobytes/block +extmsk set 0 ;;fill from right with 1's + rept 16 + if blkval=1 + exitm + endif +;; otherwise more to shift +extmsk set (extmsk shl 1) or 1 +blkval set blkval/2 + endm +;; may be double byte allocation + if (dks) > 256 +extmsk set (extmsk shr 1) + endif +;; may be optional [0] in last position + if not nul k16 +extmsk set k16 + endif +;; now generate directory reservation bit vector +dirrem set dir ;;# remaining to process +dirbks set bls/32 ;;number of entries per block +dirblk set 0 ;;fill with 1's on each loop + rept 16 + if dirrem=0 + exitm + endif +;; not complete, iterate once again +;; shift right and add 1 high order bit +dirblk set (dirblk shr 1) or 8000h + if dirrem > dirbks +dirrem set dirrem-dirbks + else +dirrem set 0 + endif + endm + dpbhdr dn ;;generate equ $ + ddw %sectors,<;sec per track> + ddb %blkshf,<;block shift> + ddb %blkmsk,<;block mask> + ddb %extmsk,<;extnt mask> + ddw %(dks)-1,<;disk size-1> + ddw %(dir)-1,<;directory max> + ddb %dirblk shr 8,<;alloc0> + ddb %dirblk and 0ffh,<;alloc1> + ddw %(cks)/4,<;check size> + ddw %ofs,<;offset> +;; generate the translate table, if requested + if nul skf +xlt&dn equ 0 ;no xlate table + else + if skf = 0 +xlt&dn equ 0 ;no xlate table + else +;; generate the translate table +nxtsec set 0 ;;next sector to fill +nxtbas set 0 ;;moves by one on overflow + gcd %sectors,skf +;; gcdn = gcd(sectors,skew) +neltst set sectors/gcdn +;; neltst is number of elements to generate +;; before we overlap previous elements +nelts set neltst ;;counter +xlt&dn equ $ ;translate table + rept sectors ;;once for each sector + if sectors < 256 + ddb %nxtsec+(fsc) + else + ddw %nxtsec+(fsc) + endif +nxtsec set nxtsec+(skf) + if nxtsec >= sectors +nxtsec set nxtsec-sectors + endif +nelts set nelts-1 + if nelts = 0 +nxtbas set nxtbas+1 +nxtsec set nxtbas +nelts set neltst + endif + endm + endif ;;end of nul fac test + endif ;;end of nul bls test + endm +; +defds macro lab,space +lab: ds space + endm +; +lds macro lb,dn,val + defds lb&dn,%val&dn + endm +; +endef macro +;; generate the necessary ram data areas +begdat equ $ +dirbuf: ds 128 ;directory access buffer +dsknxt set 0 + rept ndisks ;;once for each disk + lds alv,%dsknxt,als + lds csv,%dsknxt,css +dsknxt set dsknxt+1 + endm +enddat equ $ +datsiz equ $-begdat +;; db 0 at this point forces hex record + endm +; + \ No newline at end of file diff --git a/bin/ED.COM b/bin/ED.COM new file mode 100644 index 0000000000000000000000000000000000000000..a0f0f5410f0542e3e6d05bfcd0fb361d84c73f26 GIT binary patch literal 6656 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 literal 0 HcmV?d00001 diff --git a/bin/LOAD.COM b/bin/LOAD.COM new file mode 100644 index 0000000000000000000000000000000000000000..b9601e002cc09df45e29331795412a0f0be8e217 GIT binary patch literal 1792 zcmbtUUu+ab7@xV@Kkaq)%2KFuYT3OzE8PR3=jAMGuXKC6&A<>jV zVhvbnVw6ba!3WR>A@RXzeA7rmeZaJv5~j&SX(A+j!3QZZ_h4c|l&G!W%w1D`c5^p7 zzu)ise)HYTHx~{@jG}*FG$`#Gt{K~kJx1^D=lixBZfRes=9G=Vt9njQ95%cl@B`aO zZ#T-m;~I|Zf>brqZmGH-q8v9}svPoyn&Aj_(T3gR9JAl^Vx|sn_I`E2q;8qP@N6o7kX)s1b zr&952hJOHvs(&yjdg-D+Qt^!uKk#5#NUHh4s8K!OaH$bpEc;c@*jp-lY3qUdh?)rP zPCii0DDK8|?oQN+HF@^Mv+Yq4KPupWWfkVe?gBI-)hg+_1>SJ+Px_Ckc38kw*((v9lz z2zynfagF^DiN|1|W}Hk6w8D5}QHk1da?`mJ^FavP1=_{u)NCgcb~{=AV=Fv9t4BC! z=M!q`T+Cbj&*@h^*r}&$i9fICS>hV^-qPdx4D974F+z~be5nDKedoT;4*5eHl z_hOK&PL48BQTda?=Lzs>XD8Hbf>5(dP)`B1VU02bwG}A6N$Cj1H?GM^A)6p~h+@(L zrvPt)5NdV{{50Ur0&f%e8Ni)u7M7rTfO?Ef%P=Dt3EQ~+eJ4qqC6V7kph-78R zeQO7qjZMNVmCO(TspJk4_Da-hgJ~PlSEKB*x^zkcjAJdWZ}X06@Marf*VUyZDf~#% zrx65f0b%h?DO{3x=iG~Ccaaq|_pOAd(R?%C9qF2eJ_+wiHi&rPmV_PNL`^xLFju4C z5}ubD>)5dtv0|Pi`$*CzQ`vrvH)@U~3z`{g$tT#Tv|=x4au|`#EXgBQP_w;!T|*8( zmF-@l+k~A`>zUK3Q=mPB@3GloR#v%3CmBF`p0F>~Yjaf@jxz}W44xy9Sl~XZ3vbB4 z`0Q^krywI7mGwS?I|;j@F22v%4AjP+Rblz}R?xy#8GpfBg-t6^6Zgy!oFjTc;|X(e z9R17da~aQwzREW`3EvW^uZivoWH-^WK>gYX^er?fHTP%}xS;8cQaTj8pKqnUimZaR zrEUU!U0pn>n1fm#4V;75jinYAA@N1caJ8j1= zl#h--AkQpztN5}gU|JF6%@}2WDd*vN2=Ce9uPB^Zc1YvLHfaxO)O5Ae@CQ+ZJ&Cul zi>mEv?3_wNghH8rn2ac;^<$l=o_Ke>Bk{o)h56ZQ<7d7Zqc5fo)qAwLi>lx*ji32s mjGlO@lG>a25JEp5qvPEuaS5it^Q_Cq&ZMp$uG7T-d;S66tN&L3 literal 0 HcmV?d00001 diff --git a/bin/MAC.COM b/bin/MAC.COM new file mode 100644 index 0000000000000000000000000000000000000000..cb0ac808be8b112556e456671f154b0aee04597c GIT binary patch literal 11776 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 HyzjpOb%2Cv literal 0 HcmV?d00001 diff --git a/bin/MBASIC.COM b/bin/MBASIC.COM new file mode 100644 index 0000000000000000000000000000000000000000..c9ec3cd39f6cf66c899bd0eca759de3275045991 GIT binary patch literal 24320 zcmeHvd2|y;)^E$3up|r1D>h!VU}H<(FvbR9%XpQ|VtE6zI}i-<1Tbs7B237>60(tH zLWYn`wn3AHh=dGdbtA{>&;%*SVGNmXIvz2}D3gpxlnG3xdB18AX1?#8^ZtG3o%0Op zuCA`Sb?a8u-Rf2|53L!S5{7E`)ZSTpx^}WWHtEyE zA?@^}t6Eq5nfOoQJ@LV03V+KN4=xTY9&G+PVb_w18TZbhDEkVE`e1o-yRhWy_bX zYuV7UsbzlH;;A!7;oHOS3V$~IShy|xZ{gaAc@fnS ziz7Bf+!oOtv9$e@H=0iWqx#dR_Or@1CXu2RTiTqArRn^$i|eX92OFEtr?oYGlwMnX zCS#e)uC z?)gn!Jpdd-y6GcZT|I#Owr=zgwYbfBr0Mj&)~Yt=E47X1f8E+@@x0o4X1vw&+46I8 z%V=TC_%+JnxwElu@#&Gq#dV*Kp8d4dIkoXZCS_^=57(V{P<6|XrczDK zOV8gu`d|~o;;$+>?vEvj2_^mB@8TzqDXYHkH4FD!0sJAb&@^6B)t^Y=A2 zIp4BWoj%=Gcix5U$1NYpEKE_|*|@s;_M;NZ=q#$P)$DXyPXDy1@o4d)wxhEb)gCQr ztUVfzHqY;0y7ktrRRTn_DRo=HSI@Jn%f&KXFs4wqmL~; zPg@o_pRs(Zs;m1LbEvL;Y3tcX>Z(rPyY%b>b#3jIR_8-?+@p1EOP4rb12$}fFm!%bXuK*SvbZz3!abd^V!RBW*kz z-QIdMz8#I4k6vi5s;N6VZADvq9rrt`q3-M_t(LPdwOZ;&k5*N`dhV06RcB+Uw#KH9 z{?O*}P*rCVs#=@dJ(sA)+V-(uRW+V}x}kBg^WD0p)5lvI+nsg{$5qV2H?I8VyVt+B z1^@WNrGNeU;;C{*Qf5aDxCtnb=Ewe4QN^VS{R+W<5Kc*nY}o4Tml-P<;I z-@0yBm%5fz>L$>s+U`5Lcamn-_p1lGcJ$nAe?#|c&FL4S4^pNLOF?&Rsi2T*YD=cVk90Zrz5#-L`J$E_K(97BFBv zJ2w1`!p%0fuG`Wjwm=3{y>l04z?QD9yVTuVMU<&_jL?>KTl>YQoz%31|NWmtk zYMx6HFWn+sUvNuwq?q}C`C|OP-2VUL&FB3;j{pBh|Nm3q|3Cr4<0#ARF*2$e-1c@# z6G6?h>V$Xc^$JN870t9OggZx?boDylE!_j}_}M!YevYe=#8Uhic`3OkhWSaMKO`M` zxtx7kVLU0bXT;N!3MQpCqe7A~Pm)n8$(SR_D3N5$mSmVD8D)};1(J+PNk*|GqehZp zWF{5*mt@*Q(u^=khFp>%kz~ju8F7+~1W87eBqK?Z5d-MjcVxEWzsZZglaqF?DzwLC zlCT*4TQcTIjbm_8@dHx5O{TTW{Q7rfq(o+n_LR)89g*qZmkk~FYfj48l9*TMl9)Mu zW+cwmyQ27UDVI>Kc_(5}sP#0*2*sY(7$I|FX^bgnJie3rCP9}@FOBg5+@w{gq*${G zb(x*L8l!(h=HpUlxxL(vN|SzqjxnjZ@daGYBDZHE##a2gjJY>-`!+`Z8(GT4r!ijD zRnnZ>^J$Fs4ViI$w4FT?!+c>vorxncTx5KC1u}wMP6GF)$||VOoXYWX)26xuTAR%C zK%3Xeb*(`ud(aP}`O)med}ScWo=LDfgFbG~3_jp+UOJ+62fXaPO1JSlxt(6E)ZZb~ zJ}1-DvPp+8+Q+?D&1-3yf0x7U)9H5VcIZ^PDRTU!_Uhv0{lBI6DBYY*sTrn9DeY0I z{t2n}Lm5|Aqdy`gj~~jkPe_>~G5Tkv{`I$8b?=SobULe@J*O1Z9=lSIPuP`QR6LMm z-dF1UfX~>JDvI*5e!xvn@Uq`4xle1pQ(e1$Iz54QDg5q9AAr>^0W1T6S#=wGK%!f^ zNuqMB@*ML|rLDMIR(zXGGG78*t#-O%s%C*?&tsOMLuQS3iY+c4|6GEW?Vl>BS<6)8 zZ8E!+y>qHA$gNNX#*OT{slq~f(^QiLKmcldDV;SnXthrc@vMI8DbxsBbtuYgnJQG8 z9`icchFH}vBDj@lTtwZ()3H9)nW@~Vsr2|%bW3%Pq)qp;C#I@Cy^(Et*ugZ!rrO6W zsPq1aE}l6x)of8i#iyX|^2qdQq+#P#_gb(D*Xow>Iv?^Zsh9V%8&taY>GIgrc>5Lq z)P5m{)x@R>pW-Tba6u`dfK7|#zR;K`&jwW>)qc$!>u~=t95h`A#)L2df_c_do`9%` zjc5Kk)euyzjJ2fN+3wi*_vt0E%#;MCFSsHKTea2wJm;#3++5eQn56VO%A@hTqRRNWErglvzf;!ma0St{m5m5@za zrur~4Jhv)MXfVm?MJkg5wXj;c(;SR5)?|z$GkYW>84QXyRG{Y$H%N;zI&zDs^Yp0D zy$8`fk}6<(2>U2K+ zuVk3|yqgZF+*h@4$&_xtatilRHF<$ma!5J}&}Ts$mRERqMF{kjrZ{dOcTy#xtb+SY zo7(I1a|d&n?Qz`qx!k)sywfY8_O-0UOOu4&5yyq6`a*BGI`Yy2aAv0g^uuE0zLWhvEZvmsLly4I7!7cmY`&3CkQcqv`(^Ypojr{7H`h^ zf|^4qm`D>F64u|IlADqn7%%EfqEh@kQAi_yGd?n0kS;*evoldYN|u+~GnlA3L{dnY+}vn`pL?kuw@J>G6Ois@tj7RTi# zU=SH+qUxj!jQE109BNIRRoKsdmk6LE1Mkp>63Ur>kcLL|AzcTg@sB{dDUvYFbUNni z1b`l~v(pklNtFyDv?2)-)IZ6ZlKf~Z&kwD_Pcm~vK{|9CMYX??LGj?BT{s}6!Fw>7 z&S19O&u&OUolgut?w%Z;wI-oq3@G~~nFW7~!z^+oNx+!wV|XDPp@)+a@nbe8K~Ov_ zWqy^U4C7*2$XF3w>8oVn59~io!a24pSvZf2Psn4ll7;!~7s(DZ>LWbrQWBRti~Fd) zRE?1+;o9rzjAW#Y4@>QAWU}#5skxlZNH#u%$Bg8ADm~MaD^4)elZ^*%yy$|Gb@Rn} zO*NNo=7ESLhk{ZP4R}_1b+QPpG1>SCDghL`I{5^%EZO*&)NJgM3D?Ops(QXi9#?u+ zCl@lGB?r+5Zkaj|1me4rrw`b#3?*aX_y~f}iSU3LeSqN6Q_en@oK0~td43+m%QXbX z&->WXO+gOIEml;L2*I*hZ@(hdNQa2DMp#10*_=;oq4r^CzFtrV1GYNOxt<7 zXQ%QDZoAt#GLe!qPM1qMENuYuRoNNp?%n|1#t-H8v3|eit&~YGQ$Ut$ASih>#W~?FVAukJ3i~^Pw0YVj zd7zi1xJgTX`llK%&wQ0kmK6PU3K5MN5$xm*;rJ4icPz8J=ucCaTM4FrXTnl(%P}NT zUZsRE;>V}kilY_Ky`geDtlD47gl{hhS0)}w9lz2^m!yK>3E_k6ss=%EPO~=x+`Vtv z2j`rU#8l^iu$O%^RcM*`Aoa2!46q-hLZd?Y@o=aOb|Pc1?M>B@`xkptp<@w)&`BOg z!xx#IlF3x3{+BW)GBt@B|8`EhIS?)w~G8wPnoDWJ=}mV+$K zlM>rFZu{^yDUDJbcv`M5zD~(-CaxbRI&QnLizPJKUPX5lq!|9v$Ek^h8O5=oa61 zPlxuDpuh2k*))?3@E;WzZK5SEOcT@-YvRkB0p2aij1#+fyxEV5vsoZg%EU*a__!oIvJJt}AH|7nzE=;^S`5 z0aJN!p3jdCgZ!tz!LwCZcR?6Dce&FAbvY*?q$m)5*~0z?N;CatT2!RYiVSqq?|C{6 z+VI)5@uG>dY0L|0f|)#&<>K#nk3W*OrZE>4WVZW}FUj_(ts^p|NwLl<&*`b?Qg8bwJhPCWMuk)EmPp&sOc>-jOdlNf;AYX2mY zo6eP_P3{pC6Y6wfy6@8WxxA_&%?qXwt&kih%HqcEuM&mt#)SN_1i>&STp2UC${%&S zIC}jsw>O7bm!7l|B*DboQ`j?9xCzv7Fp1*Anc~@=J|1o_bK&B`EE&iRxUo=T@`OS5 zRfFWSbf-{HKbbywyl3cbm(WbVm_GP&&(IN#@9`6}?b`#MLAq~!_LmHI#*fueja=B~iUeX`vBbttxK2eJi!oki0eymOxhykaw zFx?ZMA*d$eGw^KODzhhB{F+_TXunIW4mQELwUe-^#YvHx!JW_KMl*?#4J|x}hQfeJ z_24|Kkjo+s{>RjkGFWc*AUKJitP0F4FLcQqIwKRMv7G&wG-M{)fA35YbDw3u?&LB z5F^V*1e|N~0uSS5*=R&?ss)$?EQRGg*}%r9`m+qRcH+H*}mI$xuLX15G{^$@-p! zIWSMKhU_FLg10PWfI*+S_-f`wit(m%>(!r9E?k2d6v2;8Rug(+F$6j;Qg0k#Q`9ch z5MH2-YBJ8d)y0oWJGlmR@gq_fc{&h!dQ9qquG63%O)D(lPbQYAj}7Gd`2o9FE>&GD z5kWJ%ML>`|E7bI~1k&iBOVsxq+yy1^HHF+=yqk!KzPH$WV<0)tv{Vymp>s|W=b>Zb zo|s9WllY@x;=y!@W;u08L%6KgtdHg{si6ijkEyNDEMKnHF_+Ym_0dV!ZN-=6Xn?t@ z4%a1)S?yuO(3_$W)SVM)8vC^>O($`DXyEwpq-ktSp~iR_8c4pToX*#n)M4R?V?gw> zd=RxNP;26EnlL7)F1OMPH9G6%y*jutmTQPxD*=DPku$gJ;o{h=fj9Ju>0NfS#^7^$ zHO4Tra)CnEz6LtfZjJ6rzs7okzC+^&$9+^IeA@4Jbkaw}mZ2zJmNRyZu!=sWA-FT6 zq5SJevvde0cDZ?waiNXQ%X@(#9^bh#s)0T6N3jZz&Vs$36N)Yf#!43oluTDdHoUaJKyZ_^63?9kk+obL!yj~DbS+FQEeNpx`+wcP48Rx*X%S}&3} zrmZ{9B~HDIT0ifS4CFeo+P;Eat-(?27~tgd38m~n$=Fu$GQ)0~(AOu3jvZdfB;C<*QY7^zRrfYj9fVlMYd8SiSv zE;LM%&}OiDXBRf;bMj{wmzB=Nez=+>%5o`XI`iil%2c>|z#p}z<^Bkf%TZav5jHVP z_=NuZ3_G2eMJT=ErlO2uz^`9OwXy|S!n^D@Glb*xw=*K~a|w5n$Nl7y7_BUoMIhhs zU14%OIL|>^P*GXzty!Q*ozHmSHj*`R3PE* z&t`PlwgYDCbDcWtvmH9?GnPwD*|*(lGxytRW%k1MuAaW}DZ-!V&$DdBQiW^i_}g?; zc7J5D=bu>-nHENs9hp0j+|mE2<;wS2eXHy2tS(z4Rov6-`8JFFHmeV5`%iPS>%rxQ zU$Zc;<)n=yj3`Ew!zClky+zjjX!QZKdcVb`nWC^3_S$+5nD;!_x#!uAJQj|B5=mg#zdO%o3@?3lTL z>a(*mbN1lvv0-!Ot9@6pb4G`I*^Zf*biD=_@Gj)-{FQIBm`z2XE+*<|*u-#=nY%9w zbua~ni^$9!Ge#+d!sNpv<*umvKp(ubY>T3FrNhb0XIbz7{T;mc+K&d9SDGk=^_$^d z7czU24~wd*=wH54mjkV6==j(WW623A=K3h2ox89`h-m@lAEofSFd|@!gf;10aKS}I zFmS0txihEs#_J+!F2_Y$kd|fkR%YsSz5Pg8GLK=!DO6)Q%(WbnaOJ>UB-+>oIPY8_ zL0yifDaVK2_K%_VvHl-9b#CTsE?wWL5ko(I1NaR-HNx|*fj5V)S#mz07v%1Fe2i%( z;NZqnB`Dx^y9s4_s^tcpcwN+#)Tik5-06l$dSkA`#Wm*AO*t;Kjo$tUdVKLrF04jP zbSbn}E<2ACa+&9Ix&ACr)E>ONw>U~6iZIVb1y`*nw5ETT8}KtUE?35KVQ0YFfWEJ} zsPKgf`f^dw?*_%-m)<0ENrn#)^D8QMWZf^Ai@9j|PuEm>w2blOO7$30jI}0QQ4c;s z^wLuO(9#Sl>HxDM2W8Kp;7=+&)M{A1Fn~={B&G<4Ct$`HUSYMaJ$oTw7*T+X3=^Lj`@S!QmdVU)yJJ_c4Dpd^3EUeb(TYfJ@zU z0)Q=r&+M~1u3gIYBG=9A$^qCj*efxhy>AR5z^b5~`PdNjMSEC7pv=UFmj0gPo}uJT zfePAT@H@OruVLtQGxwFjKV-jhDc5FsvZw!fvf{`ve`6Rz>mz=Qw=L=r2HVTMYOvjz zxoI%7hx@PG0I&m-AWKz~pn$wK8=6#7GRyrCQmuJ~f1wvu)AC*0eGp;g`Mgg>O=;S>!jMSM%4cTgk4AUc_{1?^#uRI2uOXeT z&u1$0G1-zNOijLWiZ!_x5Le|B7CL53fN=jdi@Q3L`(?rQ_$Vp%PQC0iDCW#GU%HkD zI`lbW=sWY>k#v7PnVwM!E+2_}?hN@`yeAZdp}vbO@hZ<~zUoMRi1RQT@(IbVjOUBI zLbD5OOj&@QgwUTNd{(i0p`kg`J|Mx0JNMl0vcnyO%Zd(D(LA2 zF1INHfJF1Fg3c-MiWvzC2hjy9=y}Kx`}Tw=1xOcvm{}r@>@N#6Ws2=p7~4Qz>NAxS zy9!K?AZeS3ivYe5V%N^*C1Z$`Qf^hgrU?6|m?QLh5eeLYMt^aqZ2b>5$uF{;LD6Ls@Jp^!x7>CHP9bZ^PnI8K zuNDXfT!@-B@uxyqDfbi#U97wiR?3`0(POlk)fEbh$z?mawD4?6A*2hugiVDeifJme z3R&#qg>Y*hEfh5D%Z0)Qyb)^Yu0lI^tPp^?;&cMOttbI`w#Petg9kdl|6SovTp`_E z2s{1^{AFN#U;2n&hV$hT#;8EiOUDYCs|BbU5Z`D9a(zMOCRC(+u-?vmpBp%>*29c& zjRjFL_~(M0z}IhM7+qdpC}ZtRWaI~Uce%^kcfZ-`g9=XGTuCXyx>s?;^WaG=YG0E} zcIqtA;70L9kQO``&((tdAg9uodFB*Jc0Hk@W0k5~8RHd9Us3ldwbSCO#`>KPeU>JSS&CkK{g>qe)WKE7f@2d0EZwoz?fgQcv%lHF!A!*Q)IpZ1(Uh zp^jW?$)yNoV3OieKp&Z9C@mU_Z7XY02%Pj1Z+vy9lOtmM>ypp2FXCVy34`~jCDolahXqOY3 zniUJ9saVKovx?zQ?YsBh;d}R)g;-BhF{!O6zQxgpMN(7Tf0q!=d;T#CdbV7_v=tBS z6XM7-Gr!ouGynLXu%USCgh<~XspO6XZgq)-3J)6PU|0K!19vhH;&ScL;)D-8j}~Lk zDr`KA4|4x3DJ_C$fz!@0SV4~$iiLvHr*Z=9xneSjP1;9$b%9dD6-RMteqni3VZ^x1 z!xWDib=!2kx>9WGCn;Pyrw}pVX_=il@y%>Wcog?tiF;r?^7~IpFd&2WGnqLRNc*_; zvv@XY_BPbgBr0^q4`ojt93w#%^4WqO7lSV-$(&8x#&Gx&fA5SjhfqW}&L(EJr*XDH zV(1O9m9rg;Vs?OSoXwYJxqO)u!P)GV*(y~jSCg9*du?zw@4&940q-Qr_f+h3vY5mKdUwdP=%^3s( zx%Qjcj==+t&O7_Axn_%XoT$W1%;r8R>F46+SZoI$N78Aw0FdJ?+BX|6&Hbde&CE~5 z&`VT$w~hVh?2K5%#{q*f5rtsCJ&0z8Ed!S$1j*r!eU`_APSV=+5=X!pyp~&HD=Z%_ zztgOw054;8}b9=GVrz%?(Gtk`RkJKxGS%CV9Fcd9VR?8lue z45Dbj?k2qQucu20Z~9S3inApu2_ZFq<&~1J>|+(BMLOnO3H@IsW55v)70ktw!8^;% z<>TQVf5~X11xxYrf)R@=^2*dXz`C>mt2csR4Jw4JjywUg!5t^@;=DA>v^6)4{)rVBW*kBG`(Lofpw zD}mFWWs*R@#lqknHdSY7R z*E(-~Teu$nd)IX3dm;6L8m6d-(DU%6PjeT}Lt`r|6@#@1*9U>14s;83Mi}y%kXHu~;M3e=X__6>TdJjXO1~HxZA$aveJ9C##mY&&_gLl8nhWQ>oQ!;m-YX0j; zkh=GIZYdLPo7hoC1mDB@(R~g#duv&cyE{uWSVo)Hf5bC6qH@+#76?G2VXQ?6 zjlw&4QKUont=RRar?Y!cr)LbTZN7CVRoHEvwqw>X=(Z2_R8JbPCl7ljd?v9k5HI*j zP81k-#Tc{zpO|Zhs&`)&&-}Km^&`tLn37NPbT*1{pO^7WsU9TIG|atbnycxai(I6~ z?zxPuuwLE0_gbQf;|ZNH&n>?k9z_X~`-;t^p!jMi3k?sh_{L%>9X8aL4r}V?mRQH% zpy$l(RV1cy6Im$8jKDvDe@qHFvoL2N`^>zwI}T}|eCLqtbMv7#_cPDPQV$>M`j9&` z-fKEU5WMZJ`rDrQ>R@-Qd-rXc>vOrGEJDiO3nGP{oGYrYDv(^FwW{`C1#%(cg8Fx7 zNvORc8}`~C=1PXeG@Q7@!z5Gm;e5X<=SCSk?h>gUymEQ&H^Z=>HPU(9p|Zf=(UABo zu7dqnwbqMM8}|YEy^;U?EaRT8>}T#>UZT5~U-R-kc(I?fzIX0T_Yv1Wm+a2g<@|KA z^d~d`)b~v^%_E_1E8tSn+zE%#a?%dE=89hU+5En+t#=Iyj>4f`v!2~^SH;RbV|yR7 z{`ur$>*m*A-%AFWD4}D-O(@wj&k4uxMp+YrwBRhYnTwrL2;_o^$$9MYc@X5bc?c9d zJ`c`(WSN~&*HupG55o~KS`fSD;mF$$$y;2Z(|$OwPnZdRs{>%5`g*RZat(){t+xY!VA z;WOIh_TqQt>>i{3vP|`*EJb@>4p$E1s&^r{{*65Z@!qiRVS5^k27 zj}0H<;W0X;!{rueE7&B1huIg6&_)tnJSu}Kb6RF7rBI$eYjp3>x#3vnUMly;^R9SQ z+m!ktz1nD}e=s7$&z&gu(`$^p3$8+J*omGXI$^%1RzcKY?5Ay%O?Dt)&nX2TT0Y-D z8kR{m0Q+y4L51^u02%ais)T^{YblS{4&ofIpd07wzm_tc^YO+8;u}ZN+0FB@eK0zdA~1S&81ex7RQYz@HV{ZhI)<3VU&^-Y04TYCLRTIH-rvZv(-`n%l{><>2ccVW zkp@_b@#VqlMgEkD&#O6qHOPL0;tR^dJFw5W)}I^gHyA*9U0~v1x!GdXCGiBw$soGo z8J`azDS?XGCG`4oU{bpzcRXU^f%07T(ehmT7mt=lJEFz-Qu~3r-_il*H0w1JS3vPF%ipas_WrqPSp{ zoz+$#PyzcGRTVI9swz4Mq|!U3`#TtIg-@fdm`qHd7gr=8OyUC*!W&#BL`{5V68_G< zS|QAu*i^@^2Qg4`!T^P#2*_>h%LrNN-oj3 zn51~mC``Si{fI<5+T<#w8W3jrY6UPrIrnX zlYY!(>Vyb?hBR-8RGCa(UyRjWhH0y_8hUliep9etxIXbKQy|o;$#g&w;q>=pQz^Ae zblm&Zj(~^;^MVOpOM__&;6x_UE)!h1W03|c9>u2{op!_Th#<7FvY?JhP&&vXn^ui8 z5(r5~@CR7$e4;Zks!9?;#ZIqu`+X#st32qf|1Ea>u<$naFm;v7eabNG0?03-aXO86V@Ul7S|S|9*PwxQC1mj&!drO?Ul zSpY!Um8a;+N?`{3Y^9(_s!u0`O&qMW3NuV<`i08KG{H6+t4q7HBTY#~vOiRc)jzHb z!Oo*Us~q@YK?n0IRM43UP+uw0DQ=Y0u^(4@Ro_>7+t&zdM^26CLjU{5BQChz<^&z< zcMllSE>EG87No<2mbAb+p3kZlAf!2IfoaNU#GM_He1P4tz)6BJ+zzL&rf;8+f=$_h zABH*oEMglI!BxgKEdZ$yfVpUa7($0U2cq(VUh4r}64gmamjZ`QT_v++L5NO#fSyu$ zNmD(^z0n{>s-0O73AF~Zt8)Q1hYl`KN=$)Blnu3y<}pY>07F!zr22w=_S2R|LMBHDPN9EVfC!m!$;6Kf ziXV~&n6fG%nvSg!7d>8FmeP`{fGZFjk6~w5O_Ww4BvTCU%&+ok9+jTr&n&4zaA#>% zIjJ3VdVLlWnvTt17YVHd1gx$YuB-|cK^YG7JS24)AD7zckvVqu;i?Ma zw3xyiBh4SU(R`N-%@0=v94;)BV^t&`6{RECm#ZRAAovyS9z(lbI6V>k?@gndY_N)> zI}rZn!|5U5BObviYBs)rAlJWCA@bSBl+}>X193~;?XdCSsA5>X+i`iXFm;5Wd8Vqi z5OJAywyM_VB8ZMLnrc|Z>!Q&o#9zvAA#KgB#%mHyTUjl3(FUZ9_em?pf;N;0F!j|p zVPjJP+#&u1aa`dJ63rfM8hx_Ibp82F zkWcgY3;L&|1_L6&RZ=1jx$D&;l{T3{CGx=ds=a>wb0jX??{*naOU;_2Qd{wzi{E5mR3p0iSV8 zjcTlnDBVmBG-n&B)N3m~DeKvUU~Y1(><*bYO#5qaOv=Vxu7M5&^#$^jKs*!E?)VvX zo$jh;K&I`JaXYebNR0a`*#>>(vJ7!W2s1)#5JHfMX6D4n3D?lUkv)&Qtdpnc$r{}x z(G1TqvUo)B_<=9`&l*318Awb3VizC>4cal8-!)RsC~8N7Xp@}y3bm#A;ktwDYX|on zEobv z(7a;OPeKYXOthoc4gC;Bd6ig$cQMlvVD5>BVDQ$5)GghJ!I8=B*Dln`<)R*inyF#& z;k2(-4mEL9cJsKy=~|pkunJWdgjJO;W_}GTuY+!;uHzEK&?Gz(RB-cSJp6c7Qpfez z5$@+@?+(4gEtEroh<%zSqJVnttmC<->n853^I>2m2?*E{GyU2-5Zd;XlxG0gtrdex zT+HW~y?~B6G5BQX!1EnEv!rg%Zjzgw%R3GL1Y1>4hO+w> z=J_~s(gJgjgipU12f}c#qSH@E5sc>IUT(0GHrxm~db1(Wv6ejWgKv)wj7u)qPL`ib zOqYd2b=y&2iEUJ_r<8f7p4%Z_#VCI6)dtB=4NfbLu9cg4)a5>`_w{hkHMkKnhAbSZ z@b!}T8Qx`x(l$eF(^;|0@_GFS_?@87)(cvkL-6$hwP3v>h7H!kpQvX4TF+1F{QUri zFg%l2-$%~Uh+Kzky5XLsA5Ge8s`as3HN?ZLgEF|pE&C9U&AlBkqq;{QGvm~OyX?u@E9qGnu zmxOrZE!`LRNnkUHBfRGv0R%faU2Y!R^agDA`3zgLsp#ev*pnos->>FjRov2TV19vZ z2gvD3M3qlMid)Hp?;h1uY66`2{dzZM1W^Ng`e)Gg;|=()iA$aIG5^Boh_j|AFMmuH z7aq?%-+=n4?I-7EM!d}{rZ(W9O21)18=owhpU3l%8lH z+8nx-idcergg~O%oenfcW-KC6!IQiq*AZjla}kDyh=du9D9Frgref(a*1Ui6$a z>}Yg*Qx7nU8~s*pv=IQOB{;dsg@H*)bEC83R1|g2VtwqS`9h701NPo@MA<;2@}<=8 zm~;{j#IXeqej7|#gkmFv4#kj%$ZUAXB|-LYjRD|GPV$7fsi;bk9)<=b)J-2~B$T=C(BybmF;xPYmX z!-gdMRK(i~YIqSjD`Lqz0%ow+j+v9#=(ip~cwPDX6C(@z8)Bj;FW_#xJ=*Ms80ArFPTm;)7Cp>7qz><{~hNh)4t5y~v6ble#F< zfIFQ^@=2Zb(NPj;H3y{u3oNIqMWi}=V3BBpk?elhOxG5P>QdPlEUNj7VLu^5M!yU2 zIg26~JS0MyLK<472bo0#wy4HJtz5rIm_}b(WYRI+i-b5jxCr_foFZb&Fp8l004FG! zsf(bRPg{J0Rhy)2+F}wqn}g$Khv2_Z^>5cj0PMI7sE2mhfdB7d3Fj>aR;i1afkk3? zY+ALg7h0k#D2A&Bg!v;RQWXvjoGbmDob2>-j>WTQ(>;r$MM*)T<$fhYyyUMILtv7Z zw0|*qLjvibARziK#K*`$5_JJaK##X&Cc_v5H- z?GoOn`Nfh+pZ>CZ(xrb(b`^pWD2tyW_(pD3IjNKMy-V<60-d;^pJ*k4DBADkqJ70m zo5+-f(s5#mNCY7I-(RU-r%tjL2^J&*b20g`O%_t=Bxq|77JnlrJmF@PEp(rN9O|Ek zYHDAj{Z`JC^*1dcKsQ;$D8?H>r|c)E{wF*8 zp6{3*QWjk|Xe81~54mv?{hvn!W?huYb$#trqT>B_z9BEdLhG z7f7Mte;3WWE%3$N6wNp-5rQT;Y=J67Br~F|Lfzvpknb4aHGj@$FyNde_-Ir(4j4oC z+GGZ&tzAM0K~M_q2@xgZIhpoLIRrI?G-Rjo8#pMZH|qm(2=H&^+!1Za4&mNVVqCwI zix$q7HW)|6ho#K>tv2Lg!U8JFpO8nRG$2BS_)-pNKt&{BQ*(%Re*O1yVtW`cxX=zm zM|N-N#u~s;)0@XT*@%{08_<*^CxcdXb!@$ZKBV$Dv8T7(6fNpimOGM4!47Bsn?pih_90k4jTg zFdJlc(B0Ug$)!+vZHwk@3dd+nVThKKn5IAl&I~|zfYN}oEYKKC5{W5XVv+_SgafCr z4$EQpit-Dmxxt%oy`mQ+s~%83Nb#mrIm*~No%#BVE^d{T0_MU z_np={2>sa5D&(1BaI@q00XwVpBz~M&%=2Z74`=h*_@WgmkwHR52pT9YI;Mp*s> zc8{NE4NW|8+)uTV;lQsI+$~@$J}vzrQ2c`Q=g)?4ni}I_DRM)f4Z)a!sp;U}rN3$g zS0J4{5(6jKwN@DOjL=$6?`^}>B-*>=_3Ok!^G_^m;~F}8bsk9@Y<%O_a{FoR8mt6( z$7z}7wA6|<0AX5oqoM!wwA>qmNe%0qi(1CGYJdyOItTCX8_*5mQO>@o zAitkUgoD$e`Kqd>$eDG<7nc;qJr%0{9tp0`6Pj7~Y9y+d`_&v`@4MFEdmOhd3oUSw z3$RPttccvdZJC>KwMn9ufp+vyZ9ZpE<;QniT-b`fGTFwv96y`CPB;(AeBP~|^r^mU z^M2)Z+PFWo5mgSWfPF;)!~R8uU-KFjElT6daw493)(rVoZ&Utgvme561pA{q)I`S& zodOUiX}~dQ@%r>VYrt+%-VgZ)@=lz9nd;vz8f?tG_8;KD4jnv(j_1Js4%R{aq}Af| z1P22N5xX?dPi#ZSB+z)d!Qq~C`gB2v>Htq* zICybA^`G$kwcUKc5;z~V6neH~sWlGYO9`|)$ls-?rM%0eR>X|O#WT@M1AyhHHA|g+ zSgYK>>K%Z{INC8)(Zw0~iKYV40v0VLnfewSDFvg@wqUwK6a^-Odx-YxLv_CcAIiCV zDe-r0UP_!{;jDEji3wc0G=QlBBY@}x_|y@Z6K^V*?Mt~*LUs|NW+td^XPKoqbc-tq z!8x(ic}q99r^CTKv(z;F6gdcnvyQ3blEcj2oaPnrq>9tl%e~cM)J)-CSP8e`Nks+w zOB`hmE_K`Dl(G1_1I0XxnRgOLwX?(z!w}}`;^wumrOTW$0P*5`DS68@a0>9 z%R-o(KC{#f$jVrNa3tm0aN>uax6E||<8VzjMYk-|90SsZt;}cXp?nSWF>*==F>gUk zb*ms@sXFwhYc=-VYnKF7GpQ7-Fzh?yTFRHe7}W)W%Y>M1>@Sy%{%PR8UPxCrj+;yu zHqK);h~S0U>G1f!VR#S@jVtLdP&952Rq0ps>vwYO%(llhiRC{Ry)F9 zpwDY^@L>(Hb5Q49;8+)g)UwJE5d{h6Kq;c`DKru~3k4^PoqbzD7S+bvJF&|^Er*$u z4wWuqxgpJvrd*KDu3z4N`M<^&Osrpy^PuaOO9W|Hc#>R~eyw#mHlgXF<(z4Gpe)Fw zEe{LF7F7CjTd`~kTxSC~D~3AZs5mAbkz-8D6B607<>CX~v79Me&b+PQKJW1TJRT}R zs>DZ)K>3#xns20dj8c%3jFI%+%gGlHLTm&TjDi&JPvMl{%_{UMg)&+7jTF^!ybB{B zstsZXUsMq1lI-$-VUyd!US6Ig5gN%YTxbOkBY^X~KXW_sfTL)yDBPW1%zWCWa6`xc zyaRh3!2yn28GuedF>A%3ou0KqBb@@D5fY`|PzG;S{Mtry{@jU@5sDw>I1v-^VkvRW@ z;@(iZZf{T-%l^26lyrmsB7VG!PFU&F2#{<#B{b}n{OQj-$oTp+*QNLt(7~0U0lw67 zaOHmvp$ag-UtUsN4!@WDff;G`0Y$mEpVKcy(3|l6A2qvSrKtdVSq$B>659%+F&)_3 z_+VmqrKo=41$Ki7CE+;)IJ#;jamVUMN-NRPr&hYWP_4mk+(9hHloI;AmAcTSABxV$ zD+37mj|3|d!cCH~a#z45gpUw;>jk3hpH@1t2k-Fd|0>EJ94Ukt1G}(gNOisG^!F=y zw+r6Y-D?O7Cvpxp+iyY~aT4tPj7szX6Q?e8*W(Jpf~NBhpHuT&Dw>Sb&r;Ek^*BtN zzC7K&iUjr?Tvffu6idEUx9kq6n zTK-Tq=F3%}(2G*A96##&HE&SSqE$`KV)7seZC*_{CfmMR)4tjV0%pVfKDvx>B`1-w za-2?HvD)DRvk$`mb$T`X_-gL`)ta}^sX%WKd@OodDGnU6kE{m2KTGw3-@~JSLg9tB z{vTznb~m^gD9*gQn$RDj-4#y~z`+H;Z>@&kYPFvn$Ru7c;w6WI^yO-kz@2L2v}>?n zBy#claUn%w3~@Sp_Vo;%r8cD=I2J^7(4JxODR69GTzm|jvPLP7q`zGq6~lbH`hV2C zSvK`C)Vv8NaBP^=Y!hqdu0hS*HRM$G(93-JNRX(K7}|gscZA3vPF6##qEZT;R|xiJ zTd^gLBU6>PT(v zI&m{qQKdvw2~`Y1sR2L!>l%$lt5xHyGx?Mt*~KZ|APX6C2$dj8-9to;*8Wv~WB=#R zGVNlyK8nJA1svn2rJD0=;Vw3M*V^gvwdKX$wJ2@1Ba0nhi(`S` z)TyDuZoVdnV4z_1iheB^64;astu9#NnrAz2oVC?FLQ)!?S$imkxZ(CHm^;?Q$YhpY z@=jk*%s} zE?lBs*0qCtP#REbdbaiN=-zbOF7>Q=Wo7xZ=FOd@&Z)`$A7E>Cbm5D{8;t7Wx%mrs OZ_b|!NSK7;-~R&C%=L`` literal 0 HcmV?d00001 diff --git a/bin/STAT.COM b/bin/STAT.COM new file mode 100644 index 0000000000000000000000000000000000000000..1de359f27a749da7d3127749f3720426be6fea56 GIT binary patch literal 5248 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 literal 0 HcmV?d00001 diff --git a/bin/disks.asm b/bin/disks.asm new file mode 100644 index 0000000..945dbf8 --- /dev/null +++ b/bin/disks.asm @@ -0,0 +1,10 @@ +MACLIB DISKDEF + +DISKS 4 +DISKDEF 0, 0, 127, 0, 1024, 64, 64, 1 +DISKDEF 1,0 +DISKDEF 2,0 +DISKDEF 3,0 + +ENDDEF + diff --git a/build.py b/build.py new file mode 100755 index 0000000..4fd5d03 --- /dev/null +++ b/build.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +import math + +pageSize = 128 +sectorsPerTrack = 256 +blockSize = 16384 +maxDirs = 128 +dirBlocks = 1 + +out = open('.build/disk.img', 'wb') + +def addBootloader(name): + loader = open(name, 'rb') + b = loader.read() + + if len(b) > pageSize: + raise RuntimeError("Bootloader binary cannot be larger than one page") + + out.seek(0) + out.write(b) + +def addOS(cpmName, biosName): + cpm = open(cpmName, 'rb') + bios = open(biosName, 'rb') + + b = cpm.read() + out.seek(pageSize) + out.write(b) + + b = bios.read() + out.seek(pageSize + 0x1600) + out.write(b) + +def seekBlock(i): + # Blocks start at track 1, sector 0 + out.seek(pageSize*sectorsPerTrack + i*blockSize) + +def initDirs(): + seekBlock(0) + out.write(bytearray(([0xe5] + [0x00] * 31) * maxDirs)) + +fileCounter = 0 +def addFile(filename, n, t): + global fileCounter + + if fileCounter > maxDirs: + raise RuntimeError("Max dir entries has been reached") + + fileCounter += 1 + + if len(n) > 8: + raise RuntimeError("Filename cannot be longer than 8") + + if len(t) > 3: + raise RuntimeError("Filetype cannot be longer than 3") + + f = open(filename, 'rb') + b = f.read() + + seekBlock(0) + out.seek(32*fileCounter, 1) + + # Write user (assume 0 for now) + out.write(bytearray(1)) + + # Write the name + out.write(n.upper().encode("ascii")) + out.write((" " * (8-len(n))).encode("ascii")) + + # Write the type + out.write(t.upper().encode("ascii")) + out.write((" " * (3-len(t))).encode("ascii")) + + # Write extend (assume no extend for now) + out.write(bytearray(2)) + + # Reserved byte + out.write(bytearray(1)) + + # Number of records (Again assuming no extends <128) + out.write(math.ceil(len(b)/128).to_bytes(1, byteorder='little')) + + # We are assuming one block per file for now, so we can use fileCounter + out.write(fileCounter.to_bytes(2, byteorder='little')) + + seekBlock(fileCounter) + out.write(b) + + +addBootloader('.build/loader.bin') +addOS('.build/cpm22.bin', '.build/bios.bin') + +initDirs() + +addFile(".build/MONITOR.COM", "MONITOR", "COM") +addFile("bin/STAT.COM", "STAT", "COM") diff --git a/build.sh b/build.sh index 5780d56..4251893 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,8 @@ #!/bin/bash -mkdir -p .build && zasm -i src/rom_monitor.z80 -o .build/rom_monitor.bin +mkdir -p .build && zasm -w -i src/rom_monitor.z80 -o .build/rom_monitor.bin +mkdir -p .build && zasm -i src/ram_monitor.z80 -o .build/ram_monitor.bin +mkdir -p .build && zasm -i src/cpm22.z80 -o .build/cpm22.bin +mkdir -p .build && zasm -i src/bios.z80 -o .build/bios.bin +mkdir -p .build && zasm -i src/putsys.z80 -o .build/putsys.bin +mkdir -p .build && zasm -i src/loader.z80 -o .build/loader.bin +mkdir -p .build && zasm -i src/MONITOR.z80 -o .build/MONITOR.COM diff --git a/cpm22-b.zip b/cpm22-b.zip new file mode 100644 index 0000000000000000000000000000000000000000..b03d0bb3a1aa0704a6015d8cac256d60357bb721 GIT binary patch literal 42510 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�PRD(kX-ARJWE5O+iD^X-qP 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 or = hl (buffer address) + jp c,decimal_continue ;borrow means bc > hl + jp z,decimal_continue ;z means bc = hl + ld hl,(current_value) ;return if de < buffer address (no borrow) + scf ;get value back from RAM variable + ccf + ret ;return with carry clear, value in hl +decimal_continue: ld a,(bc) ;next char in string (right to left) + sub 030h ;ASCII value of zero char + jp m,decimal_error ;error if char value less than 030h + cp 00ah ;error if byte value > or = 10 decimal + jp p,decimal_error ;a reg now has value of decimal numeral + ld hl,(value_pointer) ;get value to add an put in de + ld e,(hl) ;little-endian (low byte in low memory) + inc hl + ld d,(hl) + inc hl ;hl now points to next value + ld (value_pointer),hl + ld hl,(current_value) ;get back current value +decimal_add: dec a ;add loop to increase total value + jp m,decimal_add_done ;end of multiplication + add hl,de + jp decimal_add +decimal_add_done: ld (current_value),hl + jp decimal_next_char +decimal_error: scf + ret + jp decimal_add +decimal_place_value: defw 1,10,100,1000,10000 +; +;Memory dump +;Displays a 256-byte block of memory in 16-byte rows. +;Called with address of start of block in HL +memory_dump: ld (current_location),hl ;store address of block to be displayed + ld a,000h + ld (byte_count),a ;initialize byte count + ld (line_count),a ;initialize line count + jp dump_new_line +dump_next_byte: ld hl,(current_location) ;get byte address from storage, + ld a,(hl) ;get byte to be converted to string + inc hl ;increment address and + ld (current_location),hl ;store back + ld hl,buffer ;location to store string + call byte_to_hex_string ;convert + ld hl,buffer ;display string + call write_string + ld a,(byte_count) ;next byte + inc a + jp z,dump_done ;stop when 256 bytes displayed + ld (byte_count),a ;not finished yet, store + ld a,(line_count) ;end of line (16 characters)? + cp 00fh ;yes, start new line + jp z,dump_new_line + inc a ;no, increment line count + ld (line_count),a + ld a,020h ;print space + call write_char + jp dump_next_byte ;continue +dump_new_line: ld a,000h ;reset line count to zero + ld (line_count),a + call write_newline + ld hl,(current_location) ;location of start of line + ld a,h ;high byte of address + ld hl, buffer + call byte_to_hex_string ;convert + ld hl,buffer + call write_string ;write high byte + ld hl,(current_location) + ld a,l ;low byte of address + ld hl, buffer + call byte_to_hex_string ;convert + ld hl,buffer + call write_string ;write low byte + ld a,020h ;space + call write_char + jp dump_next_byte ;now write 16 bytes +dump_done: ld a,000h + ld hl,buffer + ld (hl),a ;clear buffer of last string + call write_newline + ret +; +;Memory load +;Loads RAM memory with bytes entered as hex characters +;Called with address to start loading in HL +;Displays entered data in 16-byte rows. +memory_load: ld (current_location),hl + ld hl,data_entry_msg + call write_string + jp load_new_line +load_next_char: call get_char + cp 00dh ;return? + jp z,load_done ;yes, quit + ld (buffer),a + call get_char + cp 00dh ;return? + jp z,load_done ;yes, quit + ld (buffer+1),a + ld hl,buffer + call hex_to_byte + jp c,load_data_entry_error ;non-hex character + ld hl,(current_location) ;get byte address from storage, + ld (hl),a ;store byte + inc hl ;increment address and + ld (current_location),hl ;store back + ld a,(buffer) + call write_char + ld a,(buffer+1) + call write_char + ld a,(line_count) ;end of line (16 characters)? + cp 00fh ;yes, start new line + jp z,load_new_line + inc a ;no, increment line count + ld (line_count),a + ld a,020h ;print space + call write_char + jp load_next_char ;continue +load_new_line: ld a,000h ;reset line count to zero + ld (line_count),a + call write_newline + jp load_next_char ;continue +load_data_entry_error: call write_newline + ld hl,data_error_msg + call write_string + ret +load_done: call write_newline + ret +; +;Get one ASCII character from the serial port. +;Returns with char in A reg. No error checking. +get_char: in a,(3) ;get status + and 002h ;check RxRDY bit + jp z,get_char ;not ready, loop + in a,(2) ;get char + ret +; +;Subroutine to start a new line +write_newline: ld a,00dh ;ASCII carriage return character + call write_char + ld a,00ah ;new line (line feed) character + call write_char + ret +; +;Subroutine to read one disk sector (128 bytes) +;Address to place data passed in HL +;LBA bits 0 to 7 passed in C, bits 8 to 15 passed in B +;LBA bits 16 to 23 passed in E +disk_read: +rd_status_loop_1: in a,(0fh) ;check status + and 80h ;check BSY bit + jp nz,rd_status_loop_1 ;loop until not busy +rd_status_loop_2: in a,(0fh) ;check status + and 40h ;check DRDY bit + jp z,rd_status_loop_2 ;loop until ready + ld a,01h ;number of sectors = 1 + out (0ah),a ;sector count register + ld a,c + out (0bh),a ;lba bits 0 - 7 + ld a,b + out (0ch),a ;lba bits 8 - 15 + ld a,e + out (0dh),a ;lba bits 16 - 23 + ld a,11100000b ;LBA mode, select drive 0 + out (0eh),a ;drive/head register + ld a,20h ;Read sector command + out (0fh),a +rd_wait_for_DRQ_set: in a,(0fh) ;read status + and 08h ;DRQ bit + jp z,rd_wait_for_DRQ_set ;loop until bit set +rd_wait_for_BSY_clear: in a,(0fh) + and 80h + jp nz,rd_wait_for_BSY_clear + in a,(0fh) ;clear INTRQ +read_loop: in a,(08h) ;get data + ld (hl),a + inc hl + in a,(0fh) ;check status + and 08h ;DRQ bit + jp nz,read_loop ;loop until cleared + ret +; +;Subroutine to write one disk sector (128 bytes) +;Address of data to write to disk passed in HL +;LBA bits 0 to 7 passed in C, bits 8 to 15 passed in B +;LBA bits 16 to 23 passed in E +disk_write: +wr_status_loop_1: in a,(0fh) ;check status + and 80h ;check BSY bit + jp nz,wr_status_loop_1 ;loop until not busy +wr_status_loop_2: in a,(0fh) ;check status + and 40h ;check DRDY bit + jp z,wr_status_loop_2 ;loop until ready + ld a,01h ;number of sectors = 1 + out (0ah),a ;sector count register + ld a,c + out (0bh),a ;lba bits 0 - 7 + ld a,b + out (0ch),a ;lba bits 8 - 15 + ld a,e + out (0dh),a ;lba bits 16 - 23 + ld a,11100000b ;LBA mode, select drive 0 + out (0eh),a ;drive/head register + ld a,30h ;Write sector command + out (0fh),a +wr_wait_for_DRQ_set: in a,(0fh) ;read status + and 08h ;DRQ bit + jp z,wr_wait_for_DRQ_set ;loop until bit set +write_loop: ld a,(hl) + out (08h),a ;write data + inc hl + in a,(0fh) ;read status + and 08h ;check DRQ bit + jp nz,write_loop ;write until bit cleared +wr_wait_for_BSY_clear: in a,(0fh) + and 80h + jp nz,wr_wait_for_BSY_clear + in a,(0fh) ;clear INTRQ + ret +; +;Strings used in subroutines +length_entry_string: defm "Enter length of file to load (decimal): ",0 +dump_entry_string: defm "Enter no. of bytes to dump (decimal): ",0 +LBA_entry_string: defm "Enter LBA (decimal, 0 to 65535): ",0 +erase_char_string: defm 008h,01bh,"[K",000h ;ANSI sequence for backspace, erase to end of line. +address_entry_msg: defm "Enter 4-digit hex address (use upper-case A through F): ",0 +address_error_msg: defm 13,10,"Error: invalid hex character, try again: ",0 +data_entry_msg: defm "Enter hex bytes, hit return when finished.",13,10,0 +data_error_msg: defm "Error: invalid hex byte.",13,10,0 +decimal_error_msg: defm 13,10,"Error: invalid decimal number, try again: ",0 +; +;Simple monitor program for CPUville Z80 computer with serial interface. + +monitor_start: call write_newline ;routine program return here to avoid re-initialization of port + ld a,03eh ;cursor symbol + call write_char + ld hl,buffer + call get_line ;get monitor input string (command) + call write_newline + call parse ;interprets command, returns with address to jump to in HL + jp (hl) +; +;Parses an input line stored in buffer for available commands as described in parse table. +;Returns with address of jump to action for the command in HL +parse: ld bc,parse_table ;bc is pointer to parse_table +parse_start: ld a,(bc) ;get pointer to match string from parse table + ld e,a + inc bc + ld a,(bc) + ld d,a ;de will is pointer to strings for matching + ld a,(de) ;get first char from match string + or 000h ;zero? + jp z,parser_exit ;yes, exit no_match + ld hl,buffer ;no, parse input string +match_loop: cp (hl) ;compare buffer char with match string char + jp nz,no_match ;no match, go to next match string + or 000h ;end of strings (zero)? + jp z,parser_exit ;yes, matching string found + inc de ;match so far, point to next char in match string + ld a,(de) ;get next character from match string + inc hl ;and point to next char in input string + jp match_loop ;check for match +no_match: inc bc ;skip over jump target to + inc bc + inc bc ;get address of next matching string + jp parse_start +parser_exit: inc bc ;skip to address of jump for match + ld a,(bc) + ld l,a + inc bc + ld a,(bc) + ld h,a ;returns with jump address in hl + ret +; +;Actions to be taken on match +; +;Memory dump program +;Input 4-digit hexadecimal address +;Calls memory_dump subroutine +dump_jump: ld hl,dump_message ;Display greeting + call write_string + ld hl,address_entry_msg ;get ready to get address + call write_string + call address_entry ;returns with address in HL + call write_newline + call memory_dump + jp monitor_start +; +;Hex loader, displays formatted input +load_jump: ld hl,load_message ;Display greeting + call write_string ;get address to load + ld hl,address_entry_msg ;get ready to get address + call write_string + call address_entry + call write_newline + call memory_load + jp monitor_start +; +;Jump and run do the same thing: get an address and jump to it. +run_jump: ld hl,run_message ;Display greeting + call write_string + ld hl,address_entry_msg ;get ready to get address + call write_string + call address_entry + jp (hl) +; +;Help and ? do the same thing, display the available commands +help_jump: ld hl,help_message + call write_string + ld bc,parse_table ;table with pointers to command strings +help_loop: ld a,(bc) ;displays the strings for matching commands, + ld l,a ;getting the string addresses from the + inc bc ;parse table + ld a,(bc) ;pass address of string to hl through a reg + ld h,a + ld a,(hl) ;hl now points to start of match string + or 000h ;exit if no_match string + jp z,help_done + push bc ;write_char uses b register + ld a,020h ;space char + call write_char + pop bc + call write_string ;writes match string + inc bc ;pass over jump address in table + inc bc + inc bc + jp help_loop +help_done: jp monitor_start +; +;Binary file load. Need both address to load and length of file +bload_jump: ld hl,bload_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,length_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld hl,bload_ready_message + call write_string + pop hl + call bload + jp monitor_start +; +;Binary memory dump. Need address of start of dump and no. bytes +bdump_jump: ld hl,bdump_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,dump_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld hl,bdump_ready_message + call write_string + call get_char + pop hl + call bdump + jp monitor_start +;Disk read. Need memory address to place data, LBA of sector to read +diskrd_jump: ld hl,diskrd_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,LBA_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld e,00h + pop hl + call disk_read + jp monitor_start +diskwr_jump: ld hl,diskwr_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,LBA_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld e,00h + pop hl + call disk_write + jp monitor_start +exit_jump: jp 0000h ; Exit CP/M +;Prints message for no match to entered command +no_match_jump: ld hl,no_match_message + call write_string + ld hl, buffer + call write_string + jp monitor_start +; +;Monitor data structures: +; +monitor_message: defm 13,10,"ROM ver. 8",13,10,0 +no_match_message: defm "? ",0 +help_message: defm "Commands implemented:",13,10,0 +dump_message: defm "Displays a 256-byte block of memory.",13,10,0 +load_message: defm "Enter hex bytes starting at memory location.",13,10,0 +run_message: defm "Will jump to (execute) program at address entered.",13,10,0 +bload_message: defm "Loads a binary file into memory.",13,10,0 +bload_ready_message: defm 13,10,"Ready to receive, start transfer.",0 +bdump_message: defm "Dumps binary data from memory to serial port.",13,10,0 +bdump_ready_message: defm 13,10,"Ready to send, hit any key to start.",0 +diskrd_message: defm "Reads one sector from disk to memory.",13,10,0 +diskwr_message: defm "Writes one sector from memory to disk.",13,10,0 +;Strings for matching: +dump_string: defm "dump",0 +load_string: defm "load",0 +jump_string: defm "jump",0 +run_string: defm "run",0 +question_string: defm "?",0 +help_string: defm "help",0 +bload_string: defm "bload",0 +bdump_string: defm "bdump",0 +diskrd_string: defm "diskrd",0 +diskwr_string: defm "diskwr",0 +exit_string: defm "exit",0 +no_match_string: defm 0,0 +;Table for matching strings to jumps +parse_table: defw dump_string,dump_jump,load_string,load_jump + defw jump_string,run_jump,run_string,run_jump + defw question_string,help_jump,help_string,help_jump + defw bload_string,bload_jump,bdump_string,bdump_jump + defw diskrd_string,diskrd_jump,diskwr_string,diskwr_jump + defw exit_string,exit_jump + defw no_match_string,no_match_jump +code_end: + end + + diff --git a/src/bios.z80 b/src/bios.z80 new file mode 100644 index 0000000..2fd7139 --- /dev/null +++ b/src/bios.z80 @@ -0,0 +1,344 @@ +; skeletal cbios for first level of CP/M 2.0 alteration +; +ccp: equ 6400h ;base of ccp +bdos: equ 6C06h ;bdos entry +bios: equ 7A00h ;base of bios +cdisk: equ 0004h ;address of current disk number 0=a,... l5=p +iobyte: equ 0003h ;intel i/o byte +disks: equ 04h ;number of disks in the system +; + org bios ;origin of this program +nsects: equ ($-ccp)/128 ;warm start sector count +; +; jump vector for individual subroutines +; + JP boot ;cold start +wboote: JP wboot ;warm start + JP const ;console status + JP conin ;console character in + JP conout ;console character out + JP list ;list character out + JP punch ;punch character out + JP reader ;reader character out + JP home ;move head to home position + JP seldsk ;select disk + JP settrk ;set track number + JP setsec ;set sector number + JP setdma ;set dma address + JP read ;read disk + JP write ;write disk + JP listst ;return list status + JP sectran ;sector translate +; +; fixed data tables for four-drive standard +; ibm-compatible 8" disks +; no translations +; +; disk Parameter header for disk 00 +dpbase: defw 0000h, 0000h + defw 0000h, 0000h + defw dirbf, dpbd0 + defw chk00, all00 +; disk parameter header for disk 01 + defw 0000h, 0000h + defw 0000h, 0000h + defw dirbf, dpblk + defw chk01, all01 +; disk parameter header for disk 02 + defw 0000h, 0000h + defw 0000h, 0000h + defw dirbf, dpblk + defw chk02, all02 +; disk parameter header for disk 03 + defw 0000h, 0000h + defw 0000h, 0000h + defw dirbf, dpblk + defw chk03, all03 +; +; sector translate vector +trans: defm 1, 7, 13, 19 ;sectors 1, 2, 3, 4 + defm 25, 5, 11, 17 ;sectors 5, 6, 7, 6 + defm 23, 3, 9, 15 ;sectors 9, 10, 11, 12 + defm 21, 2, 8, 14 ;sectors 13, 14, 15, 16 + defm 20, 26, 6, 12 ;sectors 17, 18, 19, 20 + defm 18, 24, 4, 10 ;sectors 21, 22, 23, 24 + defm 16, 22 ;sectors 25, 26 +; +dpbd0: ;disk parameter block for all disks. + defw 256 ;sectors per track + defm 7 ;block shift factor + defm 127 ;block mask + defm 7 ;null mask + defw 511 ;disk size-1 + defw 127 ;directory max + defm 128 ;alloc 0 + defm 0 ;alloc 1 + defw 0 ;check size + defw 1 ;track offset +dpblk: ;disk parameter block for all disks. + defw 256 ;sectors per track + defm 7 ;block shift factor + defm 127 ;block mask + defm 7 ;null mask + defw 511 ;disk size-1 + defw 127 ;directory max + defm 128 ;alloc 0 + defm 0 ;alloc 1 + defw 0 ;check size + defw 0 ;track offset +; +; end of fixed tables +; +; individual subroutines to perform each function +boot: ;simplest case is to just perform parameter initialization + XOR a ;zero in the accum + LD (iobyte),A ;clear the iobyte + LD (cdisk),A ;select disk zero + JP gocpm ;initialize and go to cp/m +; +wboot: ;simplest case is to read the disk until all sectors loaded + LD sp, 80h ;use space below buffer for stack + LD c, 0 ;select disk 0 + call seldsk + call home ;go to track 00 +; + LD b, nsects ;b counts * of sectors to load + LD c, 0 ;c has the current track number + LD d, 1 ;d has the next sector to read +; note that we begin by reading track 0, sector 2 since sector 1 +; contains the cold start loader, which is skipped in a warm start + LD HL, ccp ;base of cp/m (initial load point) +load1: ;load one more sector + PUSH BC ;save sector count, current track + PUSH DE ;save next sector to read + PUSH HL ;save dma address + LD c, d ;get sector address to register C + call setsec ;set sector address from register C + pop BC ;recall dma address to b, C + PUSH BC ;replace on stack for later recall + call setdma ;set dma address from b, C +; +; drive set to 0, track set, sector set, dma address set + call read + CP 00h ;any errors? + JP NZ,wboot ;retry the entire boot if an error occurs +; +; no error, move to next sector + pop HL ;recall dma address + LD DE, 128 ;dma=dma+128 + ADD HL,DE ;new dma address is in h, l + pop DE ;recall sector address + pop BC ;recall number of sectors remaining, and current trk + DEC b ;sectors=sectors-1 + JP Z,gocpm ;transfer to cp/m if all have been loaded +; +; more sectors remain to load, check for track change + INC d + LD a,d ;sector=128?, if so, change tracks + CP 128 + JP C,load1 ;carry generated if sector<128 +; +; end of current track, go to next track + LD d, 1 ;begin with first sector of next track + INC c ;track=track+1 +; +; save register state, and change tracks + PUSH BC + PUSH DE + PUSH HL + call settrk ;track address set from register c + pop HL + pop DE + pop BC + JP load1 ;for another sector +; +; end of load operation, set parameters and go to cp/m +gocpm: + LD a, 0c3h ;c3 is a jmp instruction + LD (0),A ;for jmp to wboot + LD HL, wboote ;wboot entry point + LD (1),HL ;set address field for jmp at 0 +; + LD (5),A ;for jmp to bdos + LD HL, bdos ;bdos entry point + LD (6),HL ;address field of Jump at 5 to bdos +; + LD BC, 80h ;default dma address is 80h + call setdma +; +; ei ;enable the interrupt system + LD A,(cdisk) ;get current disk number + cp disks ;see if valid disk number + jp c,diskok ;disk valid, go to ccp + ld a,0 ;invalid disk, change to disk 0 +diskok: LD c, a ;send to the ccp + JP ccp ;go to cp/m for further processing +; +; +; simple i/o handlers (must be filled in by user) +; in each case, the entry point is provided, with space reserved +; to insert your own code +; +const: ;console status, return 0ffh if character ready, 00h if not + in a,(3) ;get status + and 002h ;check RxRDY bit + jp z,no_char + ld a,0ffh ;char ready + ret +no_char:ld a,00h ;no char + ret +; +conin: ;console character into register a + in a,(3) ;get status + and 002h ;check RxRDY bit + jp z,conin ;loop until char ready + in a,(2) ;get char + AND 7fh ;strip parity bit + ret +; +conout: ;console character output from register c + in a,(3) + and 001h ;check TxRDY bit + jp z,conout ;loop until port ready + ld a,c ;get the char + out (2),a ;out to port + ret +; +list: ;list character from register c + LD a, c ;character to register a + ret ;null subroutine +; +listst: ;return list status (0 if not ready, 1 if ready) + XOR a ;0 is always ok to return + ret +; +punch: ;punch character from register C + LD a, c ;character to register a + ret ;null subroutine +; +; +reader: ;reader character into register a from reader device + LD a, 1ah ;enter end of file for now (replace later) + AND 7fh ;remember to strip parity bit + ret +; +; +; i/o drivers for the disk follow +; for now, we will simply store the parameters away for use +; in the read and write subroutines +; +home: ;move to the track 00 position of current drive +; translate this call into a settrk call with Parameter 00 + LD c, 0 ;select track 0 + call settrk + ret ;we will move to 00 on first read/write +; +seldsk: ;select disk given by register c + LD HL, 0000h ;error return code + LD a, c + out (0dh),a ;lba bits 16 - 23, use 16 to 20 for "disk" + CP disks ;must be between 0 and 3 + RET NC ;no carry if 4, 5,... + LD l, a ;l=disk number 0, 1, 2, 3 + LD h, 0 ;high order zero + ADD HL,HL ;*2 + ADD HL,HL ;*4 + ADD HL,HL ;*8 + ADD HL,HL ;*16 (size of each header) + LD DE, dpbase + ADD HL,DE ;hl=,dpbase (diskno*16) Note typo here in original source. + ret +; +settrk: ;set track given by register c + LD a, c + out (0ch),a ;lba bits 8 - 15 = "track" + ret +; +setsec: ;set sector given by register c + LD a, c + out (0bh),a ;lba bits 0 - 7 = "sector" + ret +; +; +sectran: + ;translate the sector given by bc using the + ;translate table given by de + EX DE,HL ;hl=.trans + ADD HL,BC ;hl=.trans (sector) + ret ;debug no translation + LD l, (hl) ;l=trans (sector) + LD h, 0 ;hl=trans (sector) + ret ;with value in hl +; +setdma: ;set dma address given by registers b and c + LD l, c ;low order address + LD h, b ;high order address + LD (dmaad),HL ;save the address + ret +; +read: +;Read one CP/M sector from disk. +;Return a 00h in register a if the operation completes properly, and 0lh if an error occurs during the read. +;Disk number in 'diskno' +;Track number in 'track' +;Sector number in 'sector' +;Dma address in 'dmaad' (0-65535) +; + ld hl,(dmaad) ;memory location to place data read from disk + ld a,20h ;Read sector command + out (0fh),a +rd_wait_for_DRQ_set: in a,(0fh) ;read status + and 08h ;DRQ bit + jp z,rd_wait_for_DRQ_set ;loop until bit set +read_loop: in a,(08h) ;get data + ld (hl),a + inc hl + in a,(0fh) ;check status + and 08h ;DRQ bit + jp nz,read_loop ;loop until clear + ret + +write: +;Write one CP/M sector to disk. +;Return a 00h in register a if the operation completes properly, and 0lh if an error occurs during the read or write +;Disk number in 'diskno' +;Track number in 'track' +;Sector number in 'sector' +;Dma address in 'dmaad' (0-65535) + ld hl,(dmaad) ;memory location of data to write + ld a,30h ;Write sector command + out (0fh),a +wr_wait_for_DRQ_set: in a,(0fh) ;read status + and 08h ;DRQ bit + jp z,wr_wait_for_DRQ_set ;loop until bit set +write_loop: ld a,(hl) + out (08h),a ;write data + inc hl + in a,(0fh) ;read status + and 08h ;check DRQ bit + jp nz,write_loop ;write until bit cleared + ret +; +; the remainder of the cbios is reserved uninitialized +; data area, and does not need to be a Part of the +; system memory image (the space must be available, +; however, between"begdat" and"enddat"). +; +dmaad: defs 2 ;direct memory address +; +; scratch ram area for bdos use +begdat: equ $ ;beginning of data area +dirbf: defs 128 ;scratch directory area +all00: defs 31 ;allocation vector 0 +all01: defs 31 ;allocation vector 1 +all02: defs 31 ;allocation vector 2 +all03: defs 31 ;allocation vector 3 +chk00: defs 16 ;check vector 0 +chk01: defs 16 ;check vector 1 +chk02: defs 16 ;check vector 2 +chk03: defs 16 ;check vector 3 +; +enddat: equ $ ;end of data area +datsiz: equ $-begdat; ;size of data area + end + diff --git a/src/cpm22.z80 b/src/cpm22.z80 new file mode 100644 index 0000000..90436f0 --- /dev/null +++ b/src/cpm22.z80 @@ -0,0 +1,3738 @@ +;************************************************************** +;* +;* C P / M version 2 . 2 +;* +;* Reconstructed from memory image on February 27, 1981 +;* +;* by Clark A. Calkins +;* +;************************************************************** +; +; Set memory limit here. This is the amount of contigeous +; ram starting from 0000. CP/M will reside at the end of this space. +; +MEM EQU 32 ;for a 62k system (TS802 TEST - WORKS OK). +; +IOBYTE EQU 3 ;i/o definition byte. +TDRIVE EQU 4 ;current drive name and user number. +ENTRY EQU 5 ;entry point for the cp/m bdos. +TFCB EQU 5CH ;default file control block. +TBUFF EQU 80H ;i/o buffer and command line storage. +TBASE EQU 100H ;transiant program storage area. +; +; Set control character equates. +; +CNTRLC EQU 3 ;control-c +CNTRLE EQU 05H ;control-e +BS EQU 08H ;backspace +TAB EQU 09H ;tab +LF EQU 0AH ;line feed +FF EQU 0CH ;form feed +CR EQU 0DH ;carriage return +CNTRLP EQU 10H ;control-p +CNTRLR EQU 12H ;control-r +CNTRLS EQU 13H ;control-s +CNTRLU EQU 15H ;control-u +CNTRLX EQU 18H ;control-x +CNTRLZ EQU 1AH ;control-z (end-of-file mark) +DEL EQU 7FH ;rubout +; +; Set origin for CP/M +; + ORG (MEM-7)*1024 +; +CBASE: JP COMMAND ;execute command processor (ccp). + JP CLEARBUF ;entry to empty input buffer before starting ccp. + +; +; Standard cp/m ccp input buffer. Format is (max length), +; (actual length), (char #1), (char #2), (char #3), etc. +; +INBUFF: DEFB 127 ;length of input buffer. + DEFB 0 ;current length of contents. + DEFB 'Copyright' + DEFB ' 1979 (c) by Digital Research ' + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +INPOINT:DEFW INBUFF+2 ;input line pointer +NAMEPNT:DEFW 0 ;input line pointer used for error message. Points to +; ;start of name in error. +; +; Routine to print (A) on the console. All registers used. +; +PRINT: LD E,A ;setup bdos call. + LD C,2 + JP ENTRY +; +; Routine to print (A) on the console and to save (BC). +; +PRINTB: PUSH BC + CALL PRINT + POP BC + RET +; +; Routine to send a carriage return, line feed combination +; to the console. +; +CRLF: LD A,CR + CALL PRINTB + LD A,LF + JP PRINTB +; +; Routine to send one space to the console and save (BC). +; +SPACE: LD A,' ' + JP PRINTB +; +; Routine to print character string pointed to be (BC) on the +; console. It must terminate with a null byte. +; +PLINE: PUSH BC + CALL CRLF + POP HL +PLINE2: LD A,(HL) + OR A + RET Z + INC HL + PUSH HL + CALL PRINT + POP HL + JP PLINE2 +; +; Routine to reset the disk system. +; +RESDSK: LD C,13 + JP ENTRY +; +; Routine to select disk (A). +; +DSKSEL: LD E,A + LD C,14 + JP ENTRY +; +; Routine to call bdos and save the return code. The zero +; flag is set on a return of 0ffh. +; +ENTRY1: CALL ENTRY + LD (RTNCODE),A ;save return code. + INC A ;set zero if 0ffh returned. + RET +; +; Routine to open a file. (DE) must point to the FCB. +; +OPEN: LD C,15 + JP ENTRY1 +; +; Routine to open file at (FCB). +; +OPENFCB:XOR A ;clear the record number byte at fcb+32 + LD (FCB+32),A + LD DE,FCB + JP OPEN +; +; Routine to close a file. (DE) points to FCB. +; +CLOSE: LD C,16 + JP ENTRY1 +; +; Routine to search for the first file with ambigueous name +; (DE). +; +SRCHFST:LD C,17 + JP ENTRY1 +; +; Search for the next ambigeous file name. +; +SRCHNXT:LD C,18 + JP ENTRY1 +; +; Search for file at (FCB). +; +SRCHFCB:LD DE,FCB + JP SRCHFST +; +; Routine to delete a file pointed to by (DE). +; +DELETE: LD C,19 + JP ENTRY +; +; Routine to call the bdos and set the zero flag if a zero +; status is returned. +; +ENTRY2: CALL ENTRY + OR A ;set zero flag if appropriate. + RET +; +; Routine to read the next record from a sequential file. +; (DE) points to the FCB. +; +RDREC: LD C,20 + JP ENTRY2 +; +; Routine to read file at (FCB). +; +READFCB:LD DE,FCB + JP RDREC +; +; Routine to write the next record of a sequential file. +; (DE) points to the FCB. +; +WRTREC: LD C,21 + JP ENTRY2 +; +; Routine to create the file pointed to by (DE). +; +CREATE: LD C,22 + JP ENTRY1 +; +; Routine to rename the file pointed to by (DE). Note that +; the new name starts at (DE+16). +; +RENAM: LD C,23 + JP ENTRY +; +; Get the current user code. +; +GETUSR: LD E,0FFH +; +; Routne to get or set the current user code. +; If (E) is FF then this is a GET, else it is a SET. +; +GETSETUC: LD C,32 + JP ENTRY +; +; Routine to set the current drive byte at (TDRIVE). +; +SETCDRV:CALL GETUSR ;get user number + ADD A,A ;and shift into the upper 4 bits. + ADD A,A + ADD A,A + ADD A,A + LD HL,CDRIVE ;now add in the current drive number. + OR (HL) + LD (TDRIVE),A ;and save. + RET +; +; Move currently active drive down to (TDRIVE). +; +MOVECD: LD A,(CDRIVE) + LD (TDRIVE),A + RET +; +; Routine to convert (A) into upper case ascii. Only letters +; are affected. +; +UPPER: CP 'a' ;check for letters in the range of 'a' to 'z'. + RET C + CP '{' + RET NC + AND 5FH ;convert it if found. + RET +; +; Routine to get a line of input. We must check to see if the +; user is in (BATCH) mode. If so, then read the input from file +; ($$$.SUB). At the end, reset to console input. +; +GETINP: LD A,(BATCH) ;if =0, then use console input. + OR A + JP Z,GETINP1 +; +; Use the submit file ($$$.sub) which is prepared by a +; SUBMIT run. It must be on drive (A) and it will be deleted +; if and error occures (like eof). +; + LD A,(CDRIVE) ;select drive 0 if need be. + OR A + LD A,0 ;always use drive A for submit. + CALL NZ,DSKSEL ;select it if required. + LD DE,BATCHFCB + CALL OPEN ;look for it. + JP Z,GETINP1 ;if not there, use normal input. + LD A,(BATCHFCB+15) ;get last record number+1. + DEC A + LD (BATCHFCB+32),A + LD DE,BATCHFCB + CALL RDREC ;read last record. + JP NZ,GETINP1 ;quit on end of file. +; +; Move this record into input buffer. +; + LD DE,INBUFF+1 + LD HL,TBUFF ;data was read into buffer here. + LD B,128 ;all 128 characters may be used. + CALL HL2DE ;(HL) to (DE), (B) bytes. + LD HL,BATCHFCB+14 + LD (HL),0 ;zero out the 's2' byte. + INC HL ;and decrement the record count. + DEC (HL) + LD DE,BATCHFCB ;close the batch file now. + CALL CLOSE + JP Z,GETINP1 ;quit on an error. + LD A,(CDRIVE) ;re-select previous drive if need be. + OR A + CALL NZ,DSKSEL ;don't do needless selects. +; +; Print line just read on console. +; + LD HL,INBUFF+2 + CALL PLINE2 + CALL CHKCON ;check console, quit on a key. + JP Z,GETINP2 ;jump if no key is pressed. +; +; Terminate the submit job on any keyboard input. Delete this +; file such that it is not re-started and jump to normal keyboard +; input section. +; + CALL DELBATCH ;delete the batch file. + JP CMMND1 ;and restart command input. +; +; Get here for normal keyboard input. Delete the submit file +; incase there was one. +; +GETINP1:CALL DELBATCH ;delete file ($$$.sub). + CALL SETCDRV ;reset active disk. + LD C,10 ;get line from console device. + LD DE,INBUFF + CALL ENTRY + CALL MOVECD ;reset current drive (again). +; +; Convert input line to upper case. +; +GETINP2:LD HL,INBUFF+1 + LD B,(HL) ;(B)=character counter. +GETINP3:INC HL + LD A,B ;end of the line? + OR A + JP Z,GETINP4 + LD A,(HL) ;convert to upper case. + CALL UPPER + LD (HL),A + DEC B ;adjust character count. + JP GETINP3 +GETINP4:LD (HL),A ;add trailing null. + LD HL,INBUFF+2 + LD (INPOINT),HL ;reset input line pointer. + RET +; +; Routine to check the console for a key pressed. The zero +; flag is set is none, else the character is returned in (A). +; +CHKCON: LD C,11 ;check console. + CALL ENTRY + OR A + RET Z ;return if nothing. + LD C,1 ;else get character. + CALL ENTRY + OR A ;clear zero flag and return. + RET +; +; Routine to get the currently active drive number. +; +GETDSK: LD C,25 + JP ENTRY +; +; Set the stabdard dma address. +; +STDDMA: LD DE,TBUFF +; +; Routine to set the dma address to (DE). +; +DMASET: LD C,26 + JP ENTRY +; +; Delete the batch file created by SUBMIT. +; +DELBATCH: LD HL,BATCH ;is batch active? + LD A,(HL) + OR A + RET Z + LD (HL),0 ;yes, de-activate it. + XOR A + CALL DSKSEL ;select drive 0 for sure. + LD DE,BATCHFCB ;and delete this file. + CALL DELETE + LD A,(CDRIVE) ;reset current drive. + JP DSKSEL +; +; Check to two strings at (PATTRN1) and (PATTRN2). They must be +; the same or we halt.... +; +VERIFY: LD DE,PATTRN1 ;these are the serial number bytes. + LD HL,PATTRN2 ;ditto, but how could they be different? + LD B,6 ;6 bytes each. +VERIFY1:LD A,(DE) + CP (HL) + JP NZ,HALT ;jump to halt routine. + INC DE + INC HL + DEC B + JP NZ,VERIFY1 + RET +; +; Print back file name with a '?' to indicate a syntax error. +; +SYNERR: CALL CRLF ;end current line. + LD HL,(NAMEPNT) ;this points to name in error. +SYNERR1:LD A,(HL) ;print it until a space or null is found. + CP ' ' + JP Z,SYNERR2 + OR A + JP Z,SYNERR2 + PUSH HL + CALL PRINT + POP HL + INC HL + JP SYNERR1 +SYNERR2:LD A,'?' ;add trailing '?'. + CALL PRINT + CALL CRLF + CALL DELBATCH ;delete any batch file. + JP CMMND1 ;and restart from console input. +; +; Check character at (DE) for legal command input. Note that the +; zero flag is set if the character is a delimiter. +; +CHECK: LD A,(DE) + OR A + RET Z + CP ' ' ;control characters are not legal here. + JP C,SYNERR + RET Z ;check for valid delimiter. + CP '=' + RET Z + CP '_' + RET Z + CP '.' + RET Z + CP ':' + RET Z + CP ';' + RET Z + CP '<' + RET Z + CP '>' + RET Z + RET +; +; Get the next non-blank character from (DE). +; +NONBLANK: LD A,(DE) + OR A ;string ends with a null. + RET Z + CP ' ' + RET NZ + INC DE + JP NONBLANK +; +; Add (HL)=(HL)+(A) +; +ADDHL: ADD A,L + LD L,A + RET NC ;take care of any carry. + INC H + RET +; +; Convert the first name in (FCB). +; +CONVFST:LD A,0 +; +; Format a file name (convert * to '?', etc.). On return, +; (A)=0 is an unambigeous name was specified. Enter with (A) equal to +; the position within the fcb for the name (either 0 or 16). +; +CONVERT:LD HL,FCB + CALL ADDHL + PUSH HL + PUSH HL + XOR A + LD (CHGDRV),A ;initialize drive change flag. + LD HL,(INPOINT) ;set (HL) as pointer into input line. + EX DE,HL + CALL NONBLANK ;get next non-blank character. + EX DE,HL + LD (NAMEPNT),HL ;save pointer here for any error message. + EX DE,HL + POP HL + LD A,(DE) ;get first character. + OR A + JP Z,CONVRT1 + SBC A,'A'-1 ;might be a drive name, convert to binary. + LD B,A ;and save. + INC DE ;check next character for a ':'. + LD A,(DE) + CP ':' + JP Z,CONVRT2 + DEC DE ;nope, move pointer back to the start of the line. +CONVRT1:LD A,(CDRIVE) + LD (HL),A + JP CONVRT3 +CONVRT2:LD A,B + LD (CHGDRV),A ;set change in drives flag. + LD (HL),B + INC DE +; +; Convert the basic file name. +; +CONVRT3:LD B,08H +CONVRT4:CALL CHECK + JP Z,CONVRT8 + INC HL + CP '*' ;note that an '*' will fill the remaining + JP NZ,CONVRT5 ;field with '?'. + LD (HL),'?' + JP CONVRT6 +CONVRT5:LD (HL),A + INC DE +CONVRT6:DEC B + JP NZ,CONVRT4 +CONVRT7:CALL CHECK ;get next delimiter. + JP Z,GETEXT + INC DE + JP CONVRT7 +CONVRT8:INC HL ;blank fill the file name. + LD (HL),' ' + DEC B + JP NZ,CONVRT8 +; +; Get the extension and convert it. +; +GETEXT: LD B,03H + CP '.' + JP NZ,GETEXT5 + INC DE +GETEXT1:CALL CHECK + JP Z,GETEXT5 + INC HL + CP '*' + JP NZ,GETEXT2 + LD (HL),'?' + JP GETEXT3 +GETEXT2:LD (HL),A + INC DE +GETEXT3:DEC B + JP NZ,GETEXT1 +GETEXT4:CALL CHECK + JP Z,GETEXT6 + INC DE + JP GETEXT4 +GETEXT5:INC HL + LD (HL),' ' + DEC B + JP NZ,GETEXT5 +GETEXT6:LD B,3 +GETEXT7:INC HL + LD (HL),0 + DEC B + JP NZ,GETEXT7 + EX DE,HL + LD (INPOINT),HL ;save input line pointer. + POP HL +; +; Check to see if this is an ambigeous file name specification. +; Set the (A) register to non zero if it is. +; + LD BC,11 ;set name length. +GETEXT8:INC HL + LD A,(HL) + CP '?' ;any question marks? + JP NZ,GETEXT9 + INC B ;count them. +GETEXT9:DEC C + JP NZ,GETEXT8 + LD A,B + OR A + RET +; +; CP/M command table. Note commands can be either 3 or 4 characters long. +; +NUMCMDS EQU 6 ;number of commands +CMDTBL: DEFB 'DIR ' + DEFB 'ERA ' + DEFB 'TYPE' + DEFB 'SAVE' + DEFB 'REN ' + DEFB 'USER' +; +; The following six bytes must agree with those at (PATTRN2) +; or cp/m will HALT. Why? +; +PATTRN1:DEFB 0,22,0,0,0,0 ;(* serial number bytes *). +; +; Search the command table for a match with what has just +; been entered. If a match is found, then we jump to the +; proper section. Else jump to (UNKNOWN). +; On return, the (C) register is set to the command number +; that matched (or NUMCMDS+1 if no match). +; +SEARCH: LD HL,CMDTBL + LD C,0 +SEARCH1:LD A,C + CP NUMCMDS ;this commands exists. + RET NC + LD DE,FCB+1 ;check this one. + LD B,4 ;max command length. +SEARCH2:LD A,(DE) + CP (HL) + JP NZ,SEARCH3 ;not a match. + INC DE + INC HL + DEC B + JP NZ,SEARCH2 + LD A,(DE) ;allow a 3 character command to match. + CP ' ' + JP NZ,SEARCH4 + LD A,C ;set return register for this command. + RET +SEARCH3:INC HL + DEC B + JP NZ,SEARCH3 +SEARCH4:INC C + JP SEARCH1 +; +; Set the input buffer to empty and then start the command +; processor (ccp). +; +CLEARBUF: XOR A + LD (INBUFF+1),A ;second byte is actual length. +; +;************************************************************** +;* +;* +;* C C P - C o n s o l e C o m m a n d P r o c e s s o r +;* +;************************************************************** +;* +COMMAND:LD SP,CCPSTACK ;setup stack area. + PUSH BC ;note that (C) should be equal to: + LD A,C ;(uuuudddd) where 'uuuu' is the user number + RRA ;and 'dddd' is the drive number. + RRA + RRA + RRA + AND 0FH ;isolate the user number. + LD E,A + CALL GETSETUC ;and set it. + CALL RESDSK ;reset the disk system. + LD (BATCH),A ;clear batch mode flag. + POP BC + LD A,C + AND 0FH ;isolate the drive number. + LD (CDRIVE),A ;and save. + CALL DSKSEL ;...and select. + LD A,(INBUFF+1) + OR A ;anything in input buffer already? + JP NZ,CMMND2 ;yes, we just process it. +; +; Entry point to get a command line from the console. +; +CMMND1: LD SP,CCPSTACK ;set stack straight. + CALL CRLF ;start a new line on the screen. + CALL GETDSK ;get current drive. + ADD A,'a' + CALL PRINT ;print current drive. + LD A,'>' + CALL PRINT ;and add prompt. + CALL GETINP ;get line from user. +; +; Process command line here. +; +CMMND2: LD DE,TBUFF + CALL DMASET ;set standard dma address. + CALL GETDSK + LD (CDRIVE),A ;set current drive. + CALL CONVFST ;convert name typed in. + CALL NZ,SYNERR ;wild cards are not allowed. + LD A,(CHGDRV) ;if a change in drives was indicated, + OR A ;then treat this as an unknown command + JP NZ,UNKNOWN ;which gets executed. + CALL SEARCH ;else search command table for a match. +; +; Note that an unknown command returns +; with (A) pointing to the last address +; in our table which is (UNKNOWN). +; + LD HL,CMDADR ;now, look thru our address table for command (A). + LD E,A ;set (DE) to command number. + LD D,0 + ADD HL,DE + ADD HL,DE ;(HL)=(CMDADR)+2*(command number). + LD A,(HL) ;now pick out this address. + INC HL + LD H,(HL) + LD L,A + JP (HL) ;now execute it. +; +; CP/M command address table. +; +CMDADR: DEFW DIRECT,ERASE,TYPE,SAVE + DEFW RENAME,USER,UNKNOWN +; +; Halt the system. Reason for this is unknown at present. +; +HALT: LD HL,76F3H ;'DI HLT' instructions. + LD (CBASE),HL + LD HL,CBASE + JP (HL) +; +; Read error while TYPEing a file. +; +RDERROR:LD BC,RDERR + JP PLINE +RDERR: DEFB 'Read error',0 +; +; Required file was not located. +; +NONE: LD BC,NOFILE + JP PLINE +NOFILE: DEFB 'No file',0 +; +; Decode a command of the form 'A>filename number{ filename}. +; Note that a drive specifier is not allowed on the first file +; name. On return, the number is in register (A). Any error +; causes 'filename?' to be printed and the command is aborted. +; +DECODE: CALL CONVFST ;convert filename. + LD A,(CHGDRV) ;do not allow a drive to be specified. + OR A + JP NZ,SYNERR + LD HL,FCB+1 ;convert number now. + LD BC,11 ;(B)=sum register, (C)=max digit count. +DECODE1:LD A,(HL) + CP ' ' ;a space terminates the numeral. + JP Z,DECODE3 + INC HL + SUB '0' ;make binary from ascii. + CP 10 ;legal digit? + JP NC,SYNERR + LD D,A ;yes, save it in (D). + LD A,B ;compute (B)=(B)*10 and check for overflow. + AND 0E0H + JP NZ,SYNERR + LD A,B + RLCA + RLCA + RLCA ;(A)=(B)*8 + ADD A,B ;.......*9 + JP C,SYNERR + ADD A,B ;.......*10 + JP C,SYNERR + ADD A,D ;add in new digit now. +DECODE2:JP C,SYNERR + LD B,A ;and save result. + DEC C ;only look at 11 digits. + JP NZ,DECODE1 + RET +DECODE3:LD A,(HL) ;spaces must follow (why?). + CP ' ' + JP NZ,SYNERR + INC HL +DECODE4:DEC C + JP NZ,DECODE3 + LD A,B ;set (A)=the numeric value entered. + RET +; +; Move 3 bytes from (HL) to (DE). Note that there is only +; one reference to this at (A2D5h). +; +MOVE3: LD B,3 +; +; Move (B) bytes from (HL) to (DE). +; +HL2DE: LD A,(HL) + LD (DE),A + INC HL + INC DE + DEC B + JP NZ,HL2DE + RET +; +; Compute (HL)=(TBUFF)+(A)+(C) and get the byte that's here. +; +EXTRACT:LD HL,TBUFF + ADD A,C + CALL ADDHL + LD A,(HL) + RET +; +; Check drive specified. If it means a change, then the new +; drive will be selected. In any case, the drive byte of the +; fcb will be set to null (means use current drive). +; +DSELECT:XOR A ;null out first byte of fcb. + LD (FCB),A + LD A,(CHGDRV) ;a drive change indicated? + OR A + RET Z + DEC A ;yes, is it the same as the current drive? + LD HL,CDRIVE + CP (HL) + RET Z + JP DSKSEL ;no. Select it then. +; +; Check the drive selection and reset it to the previous +; drive if it was changed for the preceeding command. +; +RESETDR:LD A,(CHGDRV) ;drive change indicated? + OR A + RET Z + DEC A ;yes, was it a different drive? + LD HL,CDRIVE + CP (HL) + RET Z + LD A,(CDRIVE) ;yes, re-select our old drive. + JP DSKSEL +; +;************************************************************** +;* +;* D I R E C T O R Y C O M M A N D +;* +;************************************************************** +; +DIRECT: CALL CONVFST ;convert file name. + CALL DSELECT ;select indicated drive. + LD HL,FCB+1 ;was any file indicated? + LD A,(HL) + CP ' ' + JP NZ,DIRECT2 + LD B,11 ;no. Fill field with '?' - same as *.*. +DIRECT1:LD (HL),'?' + INC HL + DEC B + JP NZ,DIRECT1 +DIRECT2:LD E,0 ;set initial cursor position. + PUSH DE + CALL SRCHFCB ;get first file name. + CALL Z,NONE ;none found at all? +DIRECT3:JP Z,DIRECT9 ;terminate if no more names. + LD A,(RTNCODE) ;get file's position in segment (0-3). + RRCA + RRCA + RRCA + AND 60H ;(A)=position*32 + LD C,A + LD A,10 + CALL EXTRACT ;extract the tenth entry in fcb. + RLA ;check system file status bit. + JP C,DIRECT8 ;we don't list them. + POP DE + LD A,E ;bump name count. + INC E + PUSH DE + AND 03H ;at end of line? + PUSH AF + JP NZ,DIRECT4 + CALL CRLF ;yes, end this line and start another. + PUSH BC + CALL GETDSK ;start line with ('A:'). + POP BC + ADD A,'A' + CALL PRINTB + LD A,':' + CALL PRINTB + JP DIRECT5 +DIRECT4:CALL SPACE ;add seperator between file names. + LD A,':' + CALL PRINTB +DIRECT5:CALL SPACE + LD B,1 ;'extract' each file name character at a time. +DIRECT6:LD A,B + CALL EXTRACT + AND 7FH ;strip bit 7 (status bit). + CP ' ' ;are we at the end of the name? + JP NZ,DRECT65 + POP AF ;yes, don't print spaces at the end of a line. + PUSH AF + CP 3 + JP NZ,DRECT63 + LD A,9 ;first check for no extension. + CALL EXTRACT + AND 7FH + CP ' ' + JP Z,DIRECT7 ;don't print spaces. +DRECT63:LD A,' ' ;else print them. +DRECT65:CALL PRINTB + INC B ;bump to next character psoition. + LD A,B + CP 12 ;end of the name? + JP NC,DIRECT7 + CP 9 ;nope, starting extension? + JP NZ,DIRECT6 + CALL SPACE ;yes, add seperating space. + JP DIRECT6 +DIRECT7:POP AF ;get the next file name. +DIRECT8:CALL CHKCON ;first check console, quit on anything. + JP NZ,DIRECT9 + CALL SRCHNXT ;get next name. + JP DIRECT3 ;and continue with our list. +DIRECT9:POP DE ;restore the stack and return to command level. + JP GETBACK +; +;************************************************************** +;* +;* E R A S E C O M M A N D +;* +;************************************************************** +; +ERASE: CALL CONVFST ;convert file name. + CP 11 ;was '*.*' entered? + JP NZ,ERASE1 + LD BC,YESNO ;yes, ask for confirmation. + CALL PLINE + CALL GETINP + LD HL,INBUFF+1 + DEC (HL) ;must be exactly 'y'. + JP NZ,CMMND1 + INC HL + LD A,(HL) + CP 'Y' + JP NZ,CMMND1 + INC HL + LD (INPOINT),HL ;save input line pointer. +ERASE1: CALL DSELECT ;select desired disk. + LD DE,FCB + CALL DELETE ;delete the file. + INC A + CALL Z,NONE ;not there? + JP GETBACK ;return to command level now. +YESNO: DEFB 'All (y/n)?',0 +; +;************************************************************** +;* +;* T Y P E C O M M A N D +;* +;************************************************************** +; +TYPE: CALL CONVFST ;convert file name. + JP NZ,SYNERR ;wild cards not allowed. + CALL DSELECT ;select indicated drive. + CALL OPENFCB ;open the file. + JP Z,TYPE5 ;not there? + CALL CRLF ;ok, start a new line on the screen. + LD HL,NBYTES ;initialize byte counter. + LD (HL),0FFH ;set to read first sector. +TYPE1: LD HL,NBYTES +TYPE2: LD A,(HL) ;have we written the entire sector? + CP 128 + JP C,TYPE3 + PUSH HL ;yes, read in the next one. + CALL READFCB + POP HL + JP NZ,TYPE4 ;end or error? + XOR A ;ok, clear byte counter. + LD (HL),A +TYPE3: INC (HL) ;count this byte. + LD HL,TBUFF ;and get the (A)th one from the buffer (TBUFF). + CALL ADDHL + LD A,(HL) + CP CNTRLZ ;end of file mark? + JP Z,GETBACK + CALL PRINT ;no, print it. + CALL CHKCON ;check console, quit if anything ready. + JP NZ,GETBACK + JP TYPE1 +; +; Get here on an end of file or read error. +; +TYPE4: DEC A ;read error? + JP Z,GETBACK + CALL RDERROR ;yes, print message. +TYPE5: CALL RESETDR ;and reset proper drive + JP SYNERR ;now print file name with problem. +; +;************************************************************** +;* +;* S A V E C O M M A N D +;* +;************************************************************** +; +SAVE: CALL DECODE ;get numeric number that follows SAVE. + PUSH AF ;save number of pages to write. + CALL CONVFST ;convert file name. + JP NZ,SYNERR ;wild cards not allowed. + CALL DSELECT ;select specified drive. + LD DE,FCB ;now delete this file. + PUSH DE + CALL DELETE + POP DE + CALL CREATE ;and create it again. + JP Z,SAVE3 ;can't create? + XOR A ;clear record number byte. + LD (FCB+32),A + POP AF ;convert pages to sectors. + LD L,A + LD H,0 + ADD HL,HL ;(HL)=number of sectors to write. + LD DE,TBASE ;and we start from here. +SAVE1: LD A,H ;done yet? + OR L + JP Z,SAVE2 + DEC HL ;nope, count this and compute the start + PUSH HL ;of the next 128 byte sector. + LD HL,128 + ADD HL,DE + PUSH HL ;save it and set the transfer address. + CALL DMASET + LD DE,FCB ;write out this sector now. + CALL WRTREC + POP DE ;reset (DE) to the start of the last sector. + POP HL ;restore sector count. + JP NZ,SAVE3 ;write error? + JP SAVE1 +; +; Get here after writing all of the file. +; +SAVE2: LD DE,FCB ;now close the file. + CALL CLOSE + INC A ;did it close ok? + JP NZ,SAVE4 +; +; Print out error message (no space). +; +SAVE3: LD BC,NOSPACE + CALL PLINE +SAVE4: CALL STDDMA ;reset the standard dma address. + JP GETBACK +NOSPACE:DEFB 'No space',0 +; +;************************************************************** +;* +;* R E N A M E C O M M A N D +;* +;************************************************************** +; +RENAME: CALL CONVFST ;convert first file name. + JP NZ,SYNERR ;wild cards not allowed. + LD A,(CHGDRV) ;remember any change in drives specified. + PUSH AF + CALL DSELECT ;and select this drive. + CALL SRCHFCB ;is this file present? + JP NZ,RENAME6 ;yes, print error message. + LD HL,FCB ;yes, move this name into second slot. + LD DE,FCB+16 + LD B,16 + CALL HL2DE + LD HL,(INPOINT) ;get input pointer. + EX DE,HL + CALL NONBLANK ;get next non blank character. + CP '=' ;only allow an '=' or '_' seperator. + JP Z,RENAME1 + CP '_' + JP NZ,RENAME5 +RENAME1:EX DE,HL + INC HL ;ok, skip seperator. + LD (INPOINT),HL ;save input line pointer. + CALL CONVFST ;convert this second file name now. + JP NZ,RENAME5 ;again, no wild cards. + POP AF ;if a drive was specified, then it + LD B,A ;must be the same as before. + LD HL,CHGDRV + LD A,(HL) + OR A + JP Z,RENAME2 + CP B + LD (HL),B + JP NZ,RENAME5 ;they were different, error. +RENAME2:LD (HL),B ; reset as per the first file specification. + XOR A + LD (FCB),A ;clear the drive byte of the fcb. +RENAME3:CALL SRCHFCB ;and go look for second file. + JP Z,RENAME4 ;doesn't exist? + LD DE,FCB + CALL RENAM ;ok, rename the file. + JP GETBACK +; +; Process rename errors here. +; +RENAME4:CALL NONE ;file not there. + JP GETBACK +RENAME5:CALL RESETDR ;bad command format. + JP SYNERR +RENAME6:LD BC,EXISTS ;destination file already exists. + CALL PLINE + JP GETBACK +EXISTS: DEFB 'File exists',0 +; +;************************************************************** +;* +;* U S E R C O M M A N D +;* +;************************************************************** +; +USER: CALL DECODE ;get numeric value following command. + CP 16 ;legal user number? + JP NC,SYNERR + LD E,A ;yes but is there anything else? + LD A,(FCB+1) + CP ' ' + JP Z,SYNERR ;yes, that is not allowed. + CALL GETSETUC ;ok, set user code. + JP GETBACK1 +; +;************************************************************** +;* +;* T R A N S I A N T P R O G R A M C O M M A N D +;* +;************************************************************** +; +UNKNOWN:CALL VERIFY ;check for valid system (why?). + LD A,(FCB+1) ;anything to execute? + CP ' ' + JP NZ,UNKWN1 + LD A,(CHGDRV) ;nope, only a drive change? + OR A + JP Z,GETBACK1 ;neither??? + DEC A + LD (CDRIVE),A ;ok, store new drive. + CALL MOVECD ;set (TDRIVE) also. + CALL DSKSEL ;and select this drive. + JP GETBACK1 ;then return. +; +; Here a file name was typed. Prepare to execute it. +; +UNKWN1: LD DE,FCB+9 ;an extension specified? + LD A,(DE) + CP ' ' + JP NZ,SYNERR ;yes, not allowed. +UNKWN2: PUSH DE + CALL DSELECT ;select specified drive. + POP DE + LD HL,COMFILE ;set the extension to 'COM'. + CALL MOVE3 + CALL OPENFCB ;and open this file. + JP Z,UNKWN9 ;not present? +; +; Load in the program. +; + LD HL,TBASE ;store the program starting here. +UNKWN3: PUSH HL + EX DE,HL + CALL DMASET ;set transfer address. + LD DE,FCB ;and read the next record. + CALL RDREC + JP NZ,UNKWN4 ;end of file or read error? + POP HL ;nope, bump pointer for next sector. + LD DE,128 + ADD HL,DE + LD DE,CBASE ;enough room for the whole file? + LD A,L + SUB E + LD A,H + SBC A,D + JP NC,UNKWN0 ;no, it can't fit. + JP UNKWN3 +; +; Get here after finished reading. +; +UNKWN4: POP HL + DEC A ;normal end of file? + JP NZ,UNKWN0 + CALL RESETDR ;yes, reset previous drive. + CALL CONVFST ;convert the first file name that follows + LD HL,CHGDRV ;command name. + PUSH HL + LD A,(HL) ;set drive code in default fcb. + LD (FCB),A + LD A,16 ;put second name 16 bytes later. + CALL CONVERT ;convert second file name. + POP HL + LD A,(HL) ;and set the drive for this second file. + LD (FCB+16),A + XOR A ;clear record byte in fcb. + LD (FCB+32),A + LD DE,TFCB ;move it into place at(005Ch). + LD HL,FCB + LD B,33 + CALL HL2DE + LD HL,INBUFF+2 ;now move the remainder of the input +UNKWN5: LD A,(HL) ;line down to (0080h). Look for a non blank. + OR A ;or a null. + JP Z,UNKWN6 + CP ' ' + JP Z,UNKWN6 + INC HL + JP UNKWN5 +; +; Do the line move now. It ends in a null byte. +; +UNKWN6: LD B,0 ;keep a character count. + LD DE,TBUFF+1 ;data gets put here. +UNKWN7: LD A,(HL) ;move it now. + LD (DE),A + OR A + JP Z,UNKWN8 + INC B + INC HL + INC DE + JP UNKWN7 +UNKWN8: LD A,B ;now store the character count. + LD (TBUFF),A + CALL CRLF ;clean up the screen. + CALL STDDMA ;set standard transfer address. + CALL SETCDRV ;reset current drive. + CALL TBASE ;and execute the program. +; +; Transiant programs return here (or reboot). +; + LD SP,BATCH ;set stack first off. + CALL MOVECD ;move current drive into place (TDRIVE). + CALL DSKSEL ;and reselect it. + JP CMMND1 ;back to comand mode. +; +; Get here if some error occured. +; +UNKWN9: CALL RESETDR ;inproper format. + JP SYNERR +UNKWN0: LD BC,BADLOAD ;read error or won't fit. + CALL PLINE + JP GETBACK +BADLOAD:DEFB 'Bad load',0 +COMFILE:DEFB 'COM' ;command file extension. +; +; Get here to return to command level. We will reset the +; previous active drive and then either return to command +; level directly or print error message and then return. +; +GETBACK:CALL RESETDR ;reset previous drive. +GETBACK1: CALL CONVFST ;convert first name in (FCB). + LD A,(FCB+1) ;if this was just a drive change request, + SUB ' ' ;make sure it was valid. + LD HL,CHGDRV + OR (HL) + JP NZ,SYNERR + JP CMMND1 ;ok, return to command level. +; +; ccp stack area. +; + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +CCPSTACK EQU $ ;end of ccp stack area. +; +; Batch (or SUBMIT) processing information storage. +; +BATCH: DEFB 0 ;batch mode flag (0=not active). +BATCHFCB: DEFB 0,'$$$ SUB',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +; +; File control block setup by the CCP. +; +FCB: DEFB 0,' ',0,0,0,0,0,' ',0,0,0,0,0 +RTNCODE:DEFB 0 ;status returned from bdos call. +CDRIVE: DEFB 0 ;currently active drive. +CHGDRV: DEFB 0 ;change in drives flag (0=no change). +NBYTES: DEFW 0 ;byte counter used by TYPE. +; +; Room for expansion? +; + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0 +; +; Note that the following six bytes must match those at +; (PATTRN1) or cp/m will HALT. Why? +; +PATTRN2:DEFB 0,22,0,0,0,0 ;(* serial number bytes *). +; +;************************************************************** +;* +;* B D O S E N T R Y +;* +;************************************************************** +; +FBASE: JP FBASE1 +; +; Bdos error table. +; +BADSCTR:DEFW ERROR1 ;bad sector on read or write. +BADSLCT:DEFW ERROR2 ;bad disk select. +RODISK: DEFW ERROR3 ;disk is read only. +ROFILE: DEFW ERROR4 ;file is read only. +; +; Entry into bdos. (DE) or (E) are the parameters passed. The +; function number desired is in register (C). +; +FBASE1: EX DE,HL ;save the (DE) parameters. + LD (PARAMS),HL + EX DE,HL + LD A,E ;and save register (E) in particular. + LD (EPARAM),A + LD HL,0 + LD (STATUS),HL ;clear return status. + ADD HL,SP + LD (USRSTACK),HL ;save users stack pointer. + LD SP,STKAREA ;and set our own. + XOR A ;clear auto select storage space. + LD (AUTOFLAG),A + LD (AUTO),A + LD HL,GOBACK ;set return address. + PUSH HL + LD A,C ;get function number. + CP NFUNCTS ;valid function number? + RET NC + LD C,E ;keep single register function here. + LD HL,FUNCTNS ;now look thru the function table. + LD E,A + LD D,0 ;(DE)=function number. + ADD HL,DE + ADD HL,DE ;(HL)=(start of table)+2*(function number). + LD E,(HL) + INC HL + LD D,(HL) ;now (DE)=address for this function. + LD HL,(PARAMS) ;retrieve parameters. + EX DE,HL ;now (DE) has the original parameters. + JP (HL) ;execute desired function. +; +; BDOS function jump table. +; +NFUNCTS EQU 41 ;number of functions in followin table. +; +FUNCTNS:DEFW WBOOT,GETCON,OUTCON,GETRDR,PUNCH,LIST,DIRCIO,GETIOB + DEFW SETIOB,PRTSTR,RDBUFF,GETCSTS,GETVER,RSTDSK,SETDSK,OPENFIL + DEFW CLOSEFIL,GETFST,GETNXT,DELFILE,READSEQ,WRTSEQ,FCREATE + DEFW RENFILE,GETLOG,GETCRNT,PUTDMA,GETALOC,WRTPRTD,GETROV,SETATTR + DEFW GETPARM,GETUSER,RDRANDOM,WTRANDOM,FILESIZE,SETRAN,LOGOFF,RTN + DEFW RTN,WTSPECL +; +; Bdos error message section. +; +ERROR1: LD HL,BADSEC ;bad sector message. + CALL PRTERR ;print it and get a 1 char responce. + CP CNTRLC ;re-boot request (control-c)? + JP Z,0 ;yes. + RET ;no, return to retry i/o function. +; +ERROR2: LD HL,BADSEL ;bad drive selected. + JP ERROR5 +; +ERROR3: LD HL,DISKRO ;disk is read only. + JP ERROR5 +; +ERROR4: LD HL,FILERO ;file is read only. +; +ERROR5: CALL PRTERR + JP 0 ;always reboot on these errors. +; +BDOSERR:DEFB 'Bdos Err On ' +BDOSDRV:DEFB ' : $' +BADSEC: DEFB 'Bad Sector$' +BADSEL: DEFB 'Select$' +FILERO: DEFB 'File ' +DISKRO: DEFB 'R/O$' +; +; Print bdos error message. +; +PRTERR: PUSH HL ;save second message pointer. + CALL OUTCRLF ;send (cr)(lf). + LD A,(ACTIVE) ;get active drive. + ADD A,'A' ;make ascii. + LD (BDOSDRV),A ;and put in message. + LD BC,BDOSERR ;and print it. + CALL PRTMESG + POP BC ;print second message line now. + CALL PRTMESG +; +; Get an input character. We will check our 1 character +; buffer first. This may be set by the console status routine. +; +GETCHAR:LD HL,CHARBUF ;check character buffer. + LD A,(HL) ;anything present already? + LD (HL),0 ;...either case clear it. + OR A + RET NZ ;yes, use it. + JP CONIN ;nope, go get a character responce. +; +; Input and echo a character. +; +GETECHO:CALL GETCHAR ;input a character. + CALL CHKCHAR ;carriage control? + RET C ;no, a regular control char so don't echo. + PUSH AF ;ok, save character now. + LD C,A + CALL OUTCON ;and echo it. + POP AF ;get character and return. + RET +; +; Check character in (A). Set the zero flag on a carriage +; control character and the carry flag on any other control +; character. +; +CHKCHAR:CP CR ;check for carriage return, line feed, backspace, + RET Z ;or a tab. + CP LF + RET Z + CP TAB + RET Z + CP BS + RET Z + CP ' ' ;other control char? Set carry flag. + RET +; +; Check the console during output. Halt on a control-s, then +; reboot on a control-c. If anything else is ready, clear the +; zero flag and return (the calling routine may want to do +; something). +; +CKCONSOL: LD A,(CHARBUF) ;check buffer. + OR A ;if anything, just return without checking. + JP NZ,CKCON2 + CALL CONST ;nothing in buffer. Check console. + AND 01H ;look at bit 0. + RET Z ;return if nothing. + CALL CONIN ;ok, get it. + CP CNTRLS ;if not control-s, return with zero cleared. + JP NZ,CKCON1 + CALL CONIN ;halt processing until another char + CP CNTRLC ;is typed. Control-c? + JP Z,0 ;yes, reboot now. + XOR A ;no, just pretend nothing was ever ready. + RET +CKCON1: LD (CHARBUF),A ;save character in buffer for later processing. +CKCON2: LD A,1 ;set (A) to non zero to mean something is ready. + RET +; +; Output (C) to the screen. If the printer flip-flop flag +; is set, we will send character to printer also. The console +; will be checked in the process. +; +OUTCHAR:LD A,(OUTFLAG) ;check output flag. + OR A ;anything and we won't generate output. + JP NZ,OUTCHR1 + PUSH BC + CALL CKCONSOL ;check console (we don't care whats there). + POP BC + PUSH BC + CALL CONOUT ;output (C) to the screen. + POP BC + PUSH BC + LD A,(PRTFLAG) ;check printer flip-flop flag. + OR A + CALL NZ,LIST ;print it also if non-zero. + POP BC +OUTCHR1:LD A,C ;update cursors position. + LD HL,CURPOS + CP DEL ;rubouts don't do anything here. + RET Z + INC (HL) ;bump line pointer. + CP ' ' ;and return if a normal character. + RET NC + DEC (HL) ;restore and check for the start of the line. + LD A,(HL) + OR A + RET Z ;ingnore control characters at the start of the line. + LD A,C + CP BS ;is it a backspace? + JP NZ,OUTCHR2 + DEC (HL) ;yes, backup pointer. + RET +OUTCHR2:CP LF ;is it a line feed? + RET NZ ;ignore anything else. + LD (HL),0 ;reset pointer to start of line. + RET +; +; Output (A) to the screen. If it is a control character +; (other than carriage control), use ^x format. +; +SHOWIT: LD A,C + CALL CHKCHAR ;check character. + JP NC,OUTCON ;not a control, use normal output. + PUSH AF + LD C,'^' ;for a control character, preceed it with '^'. + CALL OUTCHAR + POP AF + OR '@' ;and then use the letter equivelant. + LD C,A +; +; Function to output (C) to the console device and expand tabs +; if necessary. +; +OUTCON: LD A,C + CP TAB ;is it a tab? + JP NZ,OUTCHAR ;use regular output. +OUTCON1:LD C,' ' ;yes it is, use spaces instead. + CALL OUTCHAR + LD A,(CURPOS) ;go until the cursor is at a multiple of 8 + + AND 07H ;position. + JP NZ,OUTCON1 + RET +; +; Echo a backspace character. Erase the prevoius character +; on the screen. +; +BACKUP: CALL BACKUP1 ;backup the screen 1 place. + LD C,' ' ;then blank that character. + CALL CONOUT +BACKUP1:LD C,BS ;then back space once more. + JP CONOUT +; +; Signal a deleted line. Print a '#' at the end and start +; over. +; +NEWLINE:LD C,'#' + CALL OUTCHAR ;print this. + CALL OUTCRLF ;start new line. +NEWLN1: LD A,(CURPOS) ;move the cursor to the starting position. + LD HL,STARTING + CP (HL) + RET NC ;there yet? + LD C,' ' + CALL OUTCHAR ;nope, keep going. + JP NEWLN1 +; +; Output a (cr) (lf) to the console device (screen). +; +OUTCRLF:LD C,CR + CALL OUTCHAR + LD C,LF + JP OUTCHAR +; +; Print message pointed to by (BC). It will end with a '$'. +; +PRTMESG:LD A,(BC) ;check for terminating character. + CP '$' + RET Z + INC BC + PUSH BC ;otherwise, bump pointer and print it. + LD C,A + CALL OUTCON + POP BC + JP PRTMESG +; +; Function to execute a buffered read. +; +RDBUFF: LD A,(CURPOS) ;use present location as starting one. + LD (STARTING),A + LD HL,(PARAMS) ;get the maximum buffer space. + LD C,(HL) + INC HL ;point to first available space. + PUSH HL ;and save. + LD B,0 ;keep a character count. +RDBUF1: PUSH BC + PUSH HL +RDBUF2: CALL GETCHAR ;get the next input character. + AND 7FH ;strip bit 7. + POP HL ;reset registers. + POP BC + CP CR ;en of the line? + JP Z,RDBUF17 + CP LF + JP Z,RDBUF17 + CP BS ;how about a backspace? + JP NZ,RDBUF3 + LD A,B ;yes, but ignore at the beginning of the line. + OR A + JP Z,RDBUF1 + DEC B ;ok, update counter. + LD A,(CURPOS) ;if we backspace to the start of the line, + LD (OUTFLAG),A ;treat as a cancel (control-x). + JP RDBUF10 +RDBUF3: CP DEL ;user typed a rubout? + JP NZ,RDBUF4 + LD A,B ;ignore at the start of the line. + OR A + JP Z,RDBUF1 + LD A,(HL) ;ok, echo the prevoius character. + DEC B ;and reset pointers (counters). + DEC HL + JP RDBUF15 +RDBUF4: CP CNTRLE ;physical end of line? + JP NZ,RDBUF5 + PUSH BC ;yes, do it. + PUSH HL + CALL OUTCRLF + XOR A ;and update starting position. + LD (STARTING),A + JP RDBUF2 +RDBUF5: CP CNTRLP ;control-p? + JP NZ,RDBUF6 + PUSH HL ;yes, flip the print flag filp-flop byte. + LD HL,PRTFLAG + LD A,1 ;PRTFLAG=1-PRTFLAG + SUB (HL) + LD (HL),A + POP HL + JP RDBUF1 +RDBUF6: CP CNTRLX ;control-x (cancel)? + JP NZ,RDBUF8 + POP HL +RDBUF7: LD A,(STARTING) ;yes, backup the cursor to here. + LD HL,CURPOS + CP (HL) + JP NC,RDBUFF ;done yet? + DEC (HL) ;no, decrement pointer and output back up one space. + CALL BACKUP + JP RDBUF7 +RDBUF8: CP CNTRLU ;cntrol-u (cancel line)? + JP NZ,RDBUF9 + CALL NEWLINE ;start a new line. + POP HL + JP RDBUFF +RDBUF9: CP CNTRLR ;control-r? + JP NZ,RDBUF14 +RDBUF10:PUSH BC ;yes, start a new line and retype the old one. + CALL NEWLINE + POP BC + POP HL + PUSH HL + PUSH BC +RDBUF11:LD A,B ;done whole line yet? + OR A + JP Z,RDBUF12 + INC HL ;nope, get next character. + LD C,(HL) + DEC B ;count it. + PUSH BC + PUSH HL + CALL SHOWIT ;and display it. + POP HL + POP BC + JP RDBUF11 +RDBUF12:PUSH HL ;done with line. If we were displaying + LD A,(OUTFLAG) ;then update cursor position. + OR A + JP Z,RDBUF2 + LD HL,CURPOS ;because this line is shorter, we must + SUB (HL) ;back up the cursor (not the screen however) + LD (OUTFLAG),A ;some number of positions. +RDBUF13:CALL BACKUP ;note that as long as (OUTFLAG) is non + LD HL,OUTFLAG ;zero, the screen will not be changed. + DEC (HL) + JP NZ,RDBUF13 + JP RDBUF2 ;now just get the next character. +; +; Just a normal character, put this in our buffer and echo. +; +RDBUF14:INC HL + LD (HL),A ;store character. + INC B ;and count it. +RDBUF15:PUSH BC + PUSH HL + LD C,A ;echo it now. + CALL SHOWIT + POP HL + POP BC + LD A,(HL) ;was it an abort request? + CP CNTRLC ;control-c abort? + LD A,B + JP NZ,RDBUF16 + CP 1 ;only if at start of line. + JP Z,0 +RDBUF16:CP C ;nope, have we filled the buffer? + JP C,RDBUF1 +RDBUF17:POP HL ;yes end the line and return. + LD (HL),B + LD C,CR + JP OUTCHAR ;output (cr) and return. +; +; Function to get a character from the console device. +; +GETCON: CALL GETECHO ;get and echo. + JP SETSTAT ;save status and return. +; +; Function to get a character from the tape reader device. +; +GETRDR: CALL READER ;get a character from reader, set status and return. + JP SETSTAT +; +; Function to perform direct console i/o. If (C) contains (FF) +; then this is an input request. If (C) contains (FE) then +; this is a status request. Otherwise we are to output (C). +; +DIRCIO: LD A,C ;test for (FF). + INC A + JP Z,DIRC1 + INC A ;test for (FE). + JP Z,CONST + JP CONOUT ;just output (C). +DIRC1: CALL CONST ;this is an input request. + OR A + JP Z,GOBACK1 ;not ready? Just return (directly). + CALL CONIN ;yes, get character. + JP SETSTAT ;set status and return. +; +; Function to return the i/o byte. +; +GETIOB: LD A,(IOBYTE) + JP SETSTAT +; +; Function to set the i/o byte. +; +SETIOB: LD HL,IOBYTE + LD (HL),C + RET +; +; Function to print the character string pointed to by (DE) +; on the console device. The string ends with a '$'. +; +PRTSTR: EX DE,HL + LD C,L + LD B,H ;now (BC) points to it. + JP PRTMESG +; +; Function to interigate the console device. +; +GETCSTS:CALL CKCONSOL +; +; Get here to set the status and return to the cleanup +; section. Then back to the user. +; +SETSTAT:LD (STATUS),A +RTN: RET +; +; Set the status to 1 (read or write error code). +; +IOERR1: LD A,1 + JP SETSTAT +; +OUTFLAG:DEFB 0 ;output flag (non zero means no output). +STARTING: DEFB 2 ;starting position for cursor. +CURPOS: DEFB 0 ;cursor position (0=start of line). +PRTFLAG:DEFB 0 ;printer flag (control-p toggle). List if non zero. +CHARBUF:DEFB 0 ;single input character buffer. +; +; Stack area for BDOS calls. +; +USRSTACK: DEFW 0 ;save users stack pointer here. +; + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +STKAREA EQU $ ;end of stack area. +; +USERNO: DEFB 0 ;current user number. +ACTIVE: DEFB 0 ;currently active drive. +PARAMS: DEFW 0 ;save (DE) parameters here on entry. +STATUS: DEFW 0 ;status returned from bdos function. +; +; Select error occured, jump to error routine. +; +SLCTERR:LD HL,BADSLCT +; +; Jump to (HL) indirectly. +; +JUMPHL: LD E,(HL) + INC HL + LD D,(HL) ;now (DE) contain the desired address. + EX DE,HL + JP (HL) +; +; Block move. (DE) to (HL), (C) bytes total. +; +DE2HL: INC C ;is count down to zero? +DE2HL1: DEC C + RET Z ;yes, we are done. + LD A,(DE) ;no, move one more byte. + LD (HL),A + INC DE + INC HL + JP DE2HL1 ;and repeat. +; +; Select the desired drive. +; +SELECT: LD A,(ACTIVE) ;get active disk. + LD C,A + CALL SELDSK ;select it. + LD A,H ;valid drive? + OR L ;valid drive? + RET Z ;return if not. +; +; Here, the BIOS returned the address of the parameter block +; in (HL). We will extract the necessary pointers and save them. +; + LD E,(HL) ;yes, get address of translation table into (DE). + INC HL + LD D,(HL) + INC HL + LD (SCRATCH1),HL ;save pointers to scratch areas. + INC HL + INC HL + LD (SCRATCH2),HL ;ditto. + INC HL + INC HL + LD (SCRATCH3),HL ;ditto. + INC HL + INC HL + EX DE,HL ;now save the translation table address. + LD (XLATE),HL + LD HL,DIRBUF ;put the next 8 bytes here. + LD C,8 ;they consist of the directory buffer + CALL DE2HL ;pointer, parameter block pointer, + LD HL,(DISKPB) ;check and allocation vectors. + EX DE,HL + LD HL,SECTORS ;move parameter block into our ram. + LD C,15 ;it is 15 bytes long. + CALL DE2HL + LD HL,(DSKSIZE) ;check disk size. + LD A,H ;more than 256 blocks on this? + LD HL,BIGDISK + LD (HL),0FFH ;set to samll. + OR A + JP Z,SELECT1 + LD (HL),0 ;wrong, set to large. +SELECT1:LD A,0FFH ;clear the zero flag. + OR A + RET +; +; Routine to home the disk track head and clear pointers. +; +HOMEDRV:CALL HOME ;home the head. + XOR A + LD HL,(SCRATCH2) ;set our track pointer also. + LD (HL),A + INC HL + LD (HL),A + LD HL,(SCRATCH3) ;and our sector pointer. + LD (HL),A + INC HL + LD (HL),A + RET +; +; Do the actual disk read and check the error return status. +; +DOREAD: CALL READ + JP IORET +; +; Do the actual disk write and handle any bios error. +; +DOWRITE:CALL WRITE +IORET: OR A + RET Z ;return unless an error occured. + LD HL,BADSCTR ;bad read/write on this sector. + JP JUMPHL +; +; Routine to select the track and sector that the desired +; block number falls in. +; +TRKSEC: LD HL,(FILEPOS) ;get position of last accessed file + LD C,2 ;in directory and compute sector #. + CALL SHIFTR ;sector #=file-position/4. + LD (BLKNMBR),HL ;save this as the block number of interest. + LD (CKSUMTBL),HL ;what's it doing here too? +; +; if the sector number has already been set (BLKNMBR), enter +; at this point. +; +TRKSEC1:LD HL,BLKNMBR + LD C,(HL) ;move sector number into (BC). + INC HL + LD B,(HL) + LD HL,(SCRATCH3) ;get current sector number and + LD E,(HL) ;move this into (DE). + INC HL + LD D,(HL) + LD HL,(SCRATCH2) ;get current track number. + LD A,(HL) ;and this into (HL). + INC HL + LD H,(HL) + LD L,A +TRKSEC2:LD A,C ;is desired sector before current one? + SUB E + LD A,B + SBC A,D + JP NC,TRKSEC3 + PUSH HL ;yes, decrement sectors by one track. + LD HL,(SECTORS) ;get sectors per track. + LD A,E + SUB L + LD E,A + LD A,D + SBC A,H + LD D,A ;now we have backed up one full track. + POP HL + DEC HL ;adjust track counter. + JP TRKSEC2 +TRKSEC3:PUSH HL ;desired sector is after current one. + LD HL,(SECTORS) ;get sectors per track. + ADD HL,DE ;bump sector pointer to next track. + JP C,TRKSEC4 + LD A,C ;is desired sector now before current one? + SUB L + LD A,B + SBC A,H + JP C,TRKSEC4 + EX DE,HL ;not yes, increment track counter + POP HL ;and continue until it is. + INC HL + JP TRKSEC3 +; +; here we have determined the track number that contains the +; desired sector. +; +TRKSEC4:POP HL ;get track number (HL). + PUSH BC + PUSH DE + PUSH HL + EX DE,HL + LD HL,(OFFSET) ;adjust for first track offset. + ADD HL,DE + LD B,H + LD C,L + CALL SETTRK ;select this track. + POP DE ;reset current track pointer. + LD HL,(SCRATCH2) + LD (HL),E + INC HL + LD (HL),D + POP DE + LD HL,(SCRATCH3) ;reset the first sector on this track. + LD (HL),E + INC HL + LD (HL),D + POP BC + LD A,C ;now subtract the desired one. + SUB E ;to make it relative (1-# sectors/track). + LD C,A + LD A,B + SBC A,D + LD B,A + LD HL,(XLATE) ;translate this sector according to this table. + EX DE,HL + CALL SECTRN ;let the bios translate it. + LD C,L + LD B,H + JP SETSEC ;and select it. +; +; Compute block number from record number (SAVNREC) and +; extent number (SAVEXT). +; +GETBLOCK: LD HL,BLKSHFT ;get logical to physical conversion. + LD C,(HL) ;note that this is base 2 log of ratio. + LD A,(SAVNREC) ;get record number. +GETBLK1:OR A ;compute (A)=(A)/2^BLKSHFT. + RRA + DEC C + JP NZ,GETBLK1 + LD B,A ;save result in (B). + LD A,8 + SUB (HL) + LD C,A ;compute (C)=8-BLKSHFT. + LD A,(SAVEXT) +GETBLK2:DEC C ;compute (A)=SAVEXT*2^(8-BLKSHFT). + JP Z,GETBLK3 + OR A + RLA + JP GETBLK2 +GETBLK3:ADD A,B + RET +; +; Routine to extract the (BC) block byte from the fcb pointed +; to by (PARAMS). If this is a big-disk, then these are 16 bit +; block numbers, else they are 8 bit numbers. +; Number is returned in (HL). +; +EXTBLK: LD HL,(PARAMS) ;get fcb address. + LD DE,16 ;block numbers start 16 bytes into fcb. + ADD HL,DE + ADD HL,BC + LD A,(BIGDISK) ;are we using a big-disk? + OR A + JP Z,EXTBLK1 + LD L,(HL) ;no, extract an 8 bit number from the fcb. + LD H,0 + RET +EXTBLK1:ADD HL,BC ;yes, extract a 16 bit number. + LD E,(HL) + INC HL + LD D,(HL) + EX DE,HL ;return in (HL). + RET +; +; Compute block number. +; +COMBLK: CALL GETBLOCK + LD C,A + LD B,0 + CALL EXTBLK + LD (BLKNMBR),HL + RET +; +; Check for a zero block number (unused). +; +CHKBLK: LD HL,(BLKNMBR) + LD A,L ;is it zero? + OR H + RET +; +; Adjust physical block (BLKNMBR) and convert to logical +; sector (LOGSECT). This is the starting sector of this block. +; The actual sector of interest is then added to this and the +; resulting sector number is stored back in (BLKNMBR). This +; will still have to be adjusted for the track number. +; +LOGICAL:LD A,(BLKSHFT) ;get log2(physical/logical sectors). + LD HL,(BLKNMBR) ;get physical sector desired. +LOGICL1:ADD HL,HL ;compute logical sector number. + DEC A ;note logical sectors are 128 bytes long. + JP NZ,LOGICL1 + LD (LOGSECT),HL ;save logical sector. + LD A,(BLKMASK) ;get block mask. + LD C,A + LD A,(SAVNREC) ;get next sector to access. + AND C ;extract the relative position within physical block. + OR L ;and add it too logical sector. + LD L,A + LD (BLKNMBR),HL ;and store. + RET +; +; Set (HL) to point to extent byte in fcb. +; +SETEXT: LD HL,(PARAMS) + LD DE,12 ;it is the twelth byte. + ADD HL,DE + RET +; +; Set (HL) to point to record count byte in fcb and (DE) to +; next record number byte. +; +SETHLDE:LD HL,(PARAMS) + LD DE,15 ;record count byte (#15). + ADD HL,DE + EX DE,HL + LD HL,17 ;next record number (#32). + ADD HL,DE + RET +; +; Save current file data from fcb. +; +STRDATA:CALL SETHLDE + LD A,(HL) ;get and store record count byte. + LD (SAVNREC),A + EX DE,HL + LD A,(HL) ;get and store next record number byte. + LD (SAVNXT),A + CALL SETEXT ;point to extent byte. + LD A,(EXTMASK) ;get extent mask. + AND (HL) + LD (SAVEXT),A ;and save extent here. + RET +; +; Set the next record to access. If (MODE) is set to 2, then +; the last record byte (SAVNREC) has the correct number to access. +; For sequential access, (MODE) will be equal to 1. +; +SETNREC:CALL SETHLDE + LD A,(MODE) ;get sequential flag (=1). + CP 2 ;a 2 indicates that no adder is needed. + JP NZ,STNREC1 + XOR A ;clear adder (random access?). +STNREC1:LD C,A + LD A,(SAVNREC) ;get last record number. + ADD A,C ;increment record count. + LD (HL),A ;and set fcb's next record byte. + EX DE,HL + LD A,(SAVNXT) ;get next record byte from storage. + LD (HL),A ;and put this into fcb as number of records used. + RET +; +; Shift (HL) right (C) bits. +; +SHIFTR: INC C +SHIFTR1:DEC C + RET Z + LD A,H + OR A + RRA + LD H,A + LD A,L + RRA + LD L,A + JP SHIFTR1 +; +; Compute the check-sum for the directory buffer. Return +; integer sum in (A). +; +CHECKSUM: LD C,128 ;length of buffer. + LD HL,(DIRBUF) ;get its location. + XOR A ;clear summation byte. +CHKSUM1:ADD A,(HL) ;and compute sum ignoring carries. + INC HL + DEC C + JP NZ,CHKSUM1 + RET +; +; Shift (HL) left (C) bits. +; +SHIFTL: INC C +SHIFTL1:DEC C + RET Z + ADD HL,HL ;shift left 1 bit. + JP SHIFTL1 +; +; Routine to set a bit in a 16 bit value contained in (BC). +; The bit set depends on the current drive selection. +; +SETBIT: PUSH BC ;save 16 bit word. + LD A,(ACTIVE) ;get active drive. + LD C,A + LD HL,1 + CALL SHIFTL ;shift bit 0 into place. + POP BC ;now 'or' this with the original word. + LD A,C + OR L + LD L,A ;low byte done, do high byte. + LD A,B + OR H + LD H,A + RET +; +; Extract the write protect status bit for the current drive. +; The result is returned in (A), bit 0. +; +GETWPRT:LD HL,(WRTPRT) ;get status bytes. + LD A,(ACTIVE) ;which drive is current? + LD C,A + CALL SHIFTR ;shift status such that bit 0 is the + LD A,L ;one of interest for this drive. + AND 01H ;and isolate it. + RET +; +; Function to write protect the current disk. +; +WRTPRTD:LD HL,WRTPRT ;point to status word. + LD C,(HL) ;set (BC) equal to the status. + INC HL + LD B,(HL) + CALL SETBIT ;and set this bit according to current drive. + LD (WRTPRT),HL ;then save. + LD HL,(DIRSIZE) ;now save directory size limit. + INC HL ;remember the last one. + EX DE,HL + LD HL,(SCRATCH1) ;and store it here. + LD (HL),E ;put low byte. + INC HL + LD (HL),D ;then high byte. + RET +; +; Check for a read only file. +; +CHKROFL:CALL FCB2HL ;set (HL) to file entry in directory buffer. +CKROF1: LD DE,9 ;look at bit 7 of the ninth byte. + ADD HL,DE + LD A,(HL) + RLA + RET NC ;return if ok. + LD HL,ROFILE ;else, print error message and terminate. + JP JUMPHL +; +; Check the write protect status of the active disk. +; +CHKWPRT:CALL GETWPRT + RET Z ;return if ok. + LD HL,RODISK ;else print message and terminate. + JP JUMPHL +; +; Routine to set (HL) pointing to the proper entry in the +; directory buffer. +; +FCB2HL: LD HL,(DIRBUF) ;get address of buffer. + LD A,(FCBPOS) ;relative position of file. +; +; Routine to add (A) to (HL). +; +ADDA2HL:ADD A,L + LD L,A + RET NC + INC H ;take care of any carry. + RET +; +; Routine to get the 's2' byte from the fcb supplied in +; the initial parameter specification. +; +GETS2: LD HL,(PARAMS) ;get address of fcb. + LD DE,14 ;relative position of 's2'. + ADD HL,DE + LD A,(HL) ;extract this byte. + RET +; +; Clear the 's2' byte in the fcb. +; +CLEARS2:CALL GETS2 ;this sets (HL) pointing to it. + LD (HL),0 ;now clear it. + RET +; +; Set bit 7 in the 's2' byte of the fcb. +; +SETS2B7:CALL GETS2 ;get the byte. + OR 80H ;and set bit 7. + LD (HL),A ;then store. + RET +; +; Compare (FILEPOS) with (SCRATCH1) and set flags based on +; the difference. This checks to see if there are more file +; names in the directory. We are at (FILEPOS) and there are +; (SCRATCH1) of them to check. +; +MOREFLS:LD HL,(FILEPOS) ;we are here. + EX DE,HL + LD HL,(SCRATCH1) ;and don't go past here. + LD A,E ;compute difference but don't keep. + SUB (HL) + INC HL + LD A,D + SBC A,(HL) ;set carry if no more names. + RET +; +; Call this routine to prevent (SCRATCH1) from being greater +; than (FILEPOS). +; +CHKNMBR:CALL MOREFLS ;SCRATCH1 too big? + RET C + INC DE ;yes, reset it to (FILEPOS). + LD (HL),D + DEC HL + LD (HL),E + RET +; +; Compute (HL)=(DE)-(HL) +; +SUBHL: LD A,E ;compute difference. + SUB L + LD L,A ;store low byte. + LD A,D + SBC A,H + LD H,A ;and then high byte. + RET +; +; Set the directory checksum byte. +; +SETDIR: LD C,0FFH +; +; Routine to set or compare the directory checksum byte. If +; (C)=0ffh, then this will set the checksum byte. Else the byte +; will be checked. If the check fails (the disk has been changed), +; then this disk will be write protected. +; +CHECKDIR: LD HL,(CKSUMTBL) + EX DE,HL + LD HL,(ALLOC1) + CALL SUBHL + RET NC ;ok if (CKSUMTBL) > (ALLOC1), so return. + PUSH BC + CALL CHECKSUM ;else compute checksum. + LD HL,(CHKVECT) ;get address of checksum table. + EX DE,HL + LD HL,(CKSUMTBL) + ADD HL,DE ;set (HL) to point to byte for this drive. + POP BC + INC C ;set or check ? + JP Z,CHKDIR1 + CP (HL) ;check them. + RET Z ;return if they are the same. + CALL MOREFLS ;not the same, do we care? + RET NC + CALL WRTPRTD ;yes, mark this as write protected. + RET +CHKDIR1:LD (HL),A ;just set the byte. + RET +; +; Do a write to the directory of the current disk. +; +DIRWRITE: CALL SETDIR ;set checksum byte. + CALL DIRDMA ;set directory dma address. + LD C,1 ;tell the bios to actually write. + CALL DOWRITE ;then do the write. + JP DEFDMA +; +; Read from the directory. +; +DIRREAD:CALL DIRDMA ;set the directory dma address. + CALL DOREAD ;and read it. +; +; Routine to set the dma address to the users choice. +; +DEFDMA: LD HL,USERDMA ;reset the default dma address and return. + JP DIRDMA1 +; +; Routine to set the dma address for directory work. +; +DIRDMA: LD HL,DIRBUF +; +; Set the dma address. On entry, (HL) points to +; word containing the desired dma address. +; +DIRDMA1:LD C,(HL) + INC HL + LD B,(HL) ;setup (BC) and go to the bios to set it. + JP SETDMA +; +; Move the directory buffer into user's dma space. +; +MOVEDIR:LD HL,(DIRBUF) ;buffer is located here, and + EX DE,HL + LD HL,(USERDMA) ; put it here. + LD C,128 ;this is its length. + JP DE2HL ;move it now and return. +; +; Check (FILEPOS) and set the zero flag if it equals 0ffffh. +; +CKFILPOS: LD HL,FILEPOS + LD A,(HL) + INC HL + CP (HL) ;are both bytes the same? + RET NZ + INC A ;yes, but are they each 0ffh? + RET +; +; Set location (FILEPOS) to 0ffffh. +; +STFILPOS: LD HL,0FFFFH + LD (FILEPOS),HL + RET +; +; Move on to the next file position within the current +; directory buffer. If no more exist, set pointer to 0ffffh +; and the calling routine will check for this. Enter with (C) +; equal to 0ffh to cause the checksum byte to be set, else we +; will check this disk and set write protect if checksums are +; not the same (applies only if another directory sector must +; be read). +; +NXENTRY:LD HL,(DIRSIZE) ;get directory entry size limit. + EX DE,HL + LD HL,(FILEPOS) ;get current count. + INC HL ;go on to the next one. + LD (FILEPOS),HL + CALL SUBHL ;(HL)=(DIRSIZE)-(FILEPOS) + JP NC,NXENT1 ;is there more room left? + JP STFILPOS ;no. Set this flag and return. +NXENT1: LD A,(FILEPOS) ;get file position within directory. + AND 03H ;only look within this sector (only 4 entries fit). + LD B,5 ;convert to relative position (32 bytes each). +NXENT2: ADD A,A ;note that this is not efficient code. + DEC B ;5 'ADD A's would be better. + JP NZ,NXENT2 + LD (FCBPOS),A ;save it as position of fcb. + OR A + RET NZ ;return if we are within buffer. + PUSH BC + CALL TRKSEC ;we need the next directory sector. + CALL DIRREAD + POP BC + JP CHECKDIR +; +; Routine to to get a bit from the disk space allocation +; map. It is returned in (A), bit position 0. On entry to here, +; set (BC) to the block number on the disk to check. +; On return, (D) will contain the original bit position for +; this block number and (HL) will point to the address for it. +; +CKBITMAP: LD A,C ;determine bit number of interest. + AND 07H ;compute (D)=(E)=(C and 7)+1. + INC A + LD E,A ;save particular bit number. + LD D,A +; +; compute (BC)=(BC)/8. +; + LD A,C + RRCA ;now shift right 3 bits. + RRCA + RRCA + AND 1FH ;and clear bits 7,6,5. + LD C,A + LD A,B + ADD A,A ;now shift (B) into bits 7,6,5. + ADD A,A + ADD A,A + ADD A,A + ADD A,A + OR C ;and add in (C). + LD C,A ;ok, (C) ha been completed. + LD A,B ;is there a better way of doing this? + RRCA + RRCA + RRCA + AND 1FH + LD B,A ;and now (B) is completed. +; +; use this as an offset into the disk space allocation +; table. +; + LD HL,(ALOCVECT) + ADD HL,BC + LD A,(HL) ;now get correct byte. +CKBMAP1:RLCA ;get correct bit into position 0. + DEC E + JP NZ,CKBMAP1 + RET +; +; Set or clear the bit map such that block number (BC) will be marked +; as used. On entry, if (E)=0 then this bit will be cleared, if it equals +; 1 then it will be set (don't use anyother values). +; +STBITMAP: PUSH DE + CALL CKBITMAP ;get the byte of interest. + AND 0FEH ;clear the affected bit. + POP BC + OR C ;and now set it acording to (C). +; +; entry to restore the original bit position and then store +; in table. (A) contains the value, (D) contains the bit +; position (1-8), and (HL) points to the address within the +; space allocation table for this byte. +; +STBMAP1:RRCA ;restore original bit position. + DEC D + JP NZ,STBMAP1 + LD (HL),A ;and stor byte in table. + RET +; +; Set/clear space used bits in allocation map for this file. +; On entry, (C)=1 to set the map and (C)=0 to clear it. +; +SETFILE:CALL FCB2HL ;get address of fcb + LD DE,16 + ADD HL,DE ;get to block number bytes. + PUSH BC + LD C,17 ;check all 17 bytes (max) of table. +SETFL1: POP DE + DEC C ;done all bytes yet? + RET Z + PUSH DE + LD A,(BIGDISK) ;check disk size for 16 bit block numbers. + OR A + JP Z,SETFL2 + PUSH BC ;only 8 bit numbers. set (BC) to this one. + PUSH HL + LD C,(HL) ;get low byte from table, always + LD B,0 ;set high byte to zero. + JP SETFL3 +SETFL2: DEC C ;for 16 bit block numbers, adjust counter. + PUSH BC + LD C,(HL) ;now get both the low and high bytes. + INC HL + LD B,(HL) + PUSH HL +SETFL3: LD A,C ;block used? + OR B + JP Z,SETFL4 + LD HL,(DSKSIZE) ;is this block number within the + LD A,L ;space on the disk? + SUB C + LD A,H + SBC A,B + CALL NC,STBITMAP ;yes, set the proper bit. +SETFL4: POP HL ;point to next block number in fcb. + INC HL + POP BC + JP SETFL1 +; +; Construct the space used allocation bit map for the active +; drive. If a file name starts with '$' and it is under the +; current user number, then (STATUS) is set to minus 1. Otherwise +; it is not set at all. +; +BITMAP: LD HL,(DSKSIZE) ;compute size of allocation table. + LD C,3 + CALL SHIFTR ;(HL)=(HL)/8. + INC HL ;at lease 1 byte. + LD B,H + LD C,L ;set (BC) to the allocation table length. +; +; Initialize the bitmap for this drive. Right now, the first +; two bytes are specified by the disk parameter block. However +; a patch could be entered here if it were necessary to setup +; this table in a special mannor. For example, the bios could +; determine locations of 'bad blocks' and set them as already +; 'used' in the map. +; + LD HL,(ALOCVECT) ;now zero out the table now. +BITMAP1:LD (HL),0 + INC HL + DEC BC + LD A,B + OR C + JP NZ,BITMAP1 + LD HL,(ALLOC0) ;get initial space used by directory. + EX DE,HL + LD HL,(ALOCVECT) ;and put this into map. + LD (HL),E + INC HL + LD (HL),D +; +; End of initialization portion. +; + CALL HOMEDRV ;now home the drive. + LD HL,(SCRATCH1) + LD (HL),3 ;force next directory request to read + INC HL ;in a sector. + LD (HL),0 + CALL STFILPOS ;clear initial file position also. +BITMAP2:LD C,0FFH ;read next file name in directory + CALL NXENTRY ;and set checksum byte. + CALL CKFILPOS ;is there another file? + RET Z + CALL FCB2HL ;yes, get its address. + LD A,0E5H + CP (HL) ;empty file entry? + JP Z,BITMAP2 + LD A,(USERNO) ;no, correct user number? + CP (HL) + JP NZ,BITMAP3 + INC HL + LD A,(HL) ;yes, does name start with a '$'? + SUB '$' + JP NZ,BITMAP3 + DEC A ;yes, set atatus to minus one. + LD (STATUS),A +BITMAP3:LD C,1 ;now set this file's space as used in bit map. + CALL SETFILE + CALL CHKNMBR ;keep (SCRATCH1) in bounds. + JP BITMAP2 +; +; Set the status (STATUS) and return. +; +STSTATUS: LD A,(FNDSTAT) + JP SETSTAT +; +; Check extents in (A) and (C). Set the zero flag if they +; are the same. The number of 16k chunks of disk space that +; the directory extent covers is expressad is (EXTMASK+1). +; No registers are modified. +; +SAMEXT: PUSH BC + PUSH AF + LD A,(EXTMASK) ;get extent mask and use it to + CPL ;to compare both extent numbers. + LD B,A ;save resulting mask here. + LD A,C ;mask first extent and save in (C). + AND B + LD C,A + POP AF ;now mask second extent and compare + AND B ;with the first one. + SUB C + AND 1FH ;(* only check buts 0-4 *) + POP BC ;the zero flag is set if they are the same. + RET ;restore (BC) and return. +; +; Search for the first occurence of a file name. On entry, +; register (C) should contain the number of bytes of the fcb +; that must match. +; +FINDFST:LD A,0FFH + LD (FNDSTAT),A + LD HL,COUNTER ;save character count. + LD (HL),C + LD HL,(PARAMS) ;get filename to match. + LD (SAVEFCB),HL ;and save. + CALL STFILPOS ;clear initial file position (set to 0ffffh). + CALL HOMEDRV ;home the drive. +; +; Entry to locate the next occurence of a filename within the +; directory. The disk is not expected to have been changed. If +; it was, then it will be write protected. +; +FINDNXT:LD C,0 ;write protect the disk if changed. + CALL NXENTRY ;get next filename entry in directory. + CALL CKFILPOS ;is file position = 0ffffh? + JP Z,FNDNXT6 ;yes, exit now then. + LD HL,(SAVEFCB) ;set (DE) pointing to filename to match. + EX DE,HL + LD A,(DE) + CP 0E5H ;empty directory entry? + JP Z,FNDNXT1 ;(* are we trying to reserect erased entries? *) + PUSH DE + CALL MOREFLS ;more files in directory? + POP DE + JP NC,FNDNXT6 ;no more. Exit now. +FNDNXT1:CALL FCB2HL ;get address of this fcb in directory. + LD A,(COUNTER) ;get number of bytes (characters) to check. + LD C,A + LD B,0 ;initialize byte position counter. +FNDNXT2:LD A,C ;are we done with the compare? + OR A + JP Z,FNDNXT5 + LD A,(DE) ;no, check next byte. + CP '?' ;don't care about this character? + JP Z,FNDNXT4 + LD A,B ;get bytes position in fcb. + CP 13 ;don't care about the thirteenth byte either. + JP Z,FNDNXT4 + CP 12 ;extent byte? + LD A,(DE) + JP Z,FNDNXT3 + SUB (HL) ;otherwise compare characters. + AND 7FH + JP NZ,FINDNXT ;not the same, check next entry. + JP FNDNXT4 ;so far so good, keep checking. +FNDNXT3:PUSH BC ;check the extent byte here. + LD C,(HL) + CALL SAMEXT + POP BC + JP NZ,FINDNXT ;not the same, look some more. +; +; So far the names compare. Bump pointers to the next byte +; and continue until all (C) characters have been checked. +; +FNDNXT4:INC DE ;bump pointers. + INC HL + INC B + DEC C ;adjust character counter. + JP FNDNXT2 +FNDNXT5:LD A,(FILEPOS) ;return the position of this entry. + AND 03H + LD (STATUS),A + LD HL,FNDSTAT + LD A,(HL) + RLA + RET NC + XOR A + LD (HL),A + RET +; +; Filename was not found. Set appropriate status. +; +FNDNXT6:CALL STFILPOS ;set (FILEPOS) to 0ffffh. + LD A,0FFH ;say not located. + JP SETSTAT +; +; Erase files from the directory. Only the first byte of the +; fcb will be affected. It is set to (E5). +; +ERAFILE:CALL CHKWPRT ;is disk write protected? + LD C,12 ;only compare file names. + CALL FINDFST ;get first file name. +ERAFIL1:CALL CKFILPOS ;any found? + RET Z ;nope, we must be done. + CALL CHKROFL ;is file read only? + CALL FCB2HL ;nope, get address of fcb and + LD (HL),0E5H ;set first byte to 'empty'. + LD C,0 ;clear the space from the bit map. + CALL SETFILE + CALL DIRWRITE ;now write the directory sector back out. + CALL FINDNXT ;find the next file name. + JP ERAFIL1 ;and repeat process. +; +; Look through the space allocation map (bit map) for the +; next available block. Start searching at block number (BC-1). +; The search procedure is to look for an empty block that is +; before the starting block. If not empty, look at a later +; block number. In this way, we return the closest empty block +; on either side of the 'target' block number. This will speed +; access on random devices. For serial devices, this should be +; changed to look in the forward direction first and then start +; at the front and search some more. +; +; On return, (DE)= block number that is empty and (HL) =0 +; if no empry block was found. +; +FNDSPACE: LD D,B ;set (DE) as the block that is checked. + LD E,C +; +; Look before target block. Registers (BC) are used as the lower +; pointer and (DE) as the upper pointer. +; +FNDSPA1:LD A,C ;is block 0 specified? + OR B + JP Z,FNDSPA2 + DEC BC ;nope, check previous block. + PUSH DE + PUSH BC + CALL CKBITMAP + RRA ;is this block empty? + JP NC,FNDSPA3 ;yes. use this. +; +; Note that the above logic gets the first block that it finds +; that is empty. Thus a file could be written 'backward' making +; it very slow to access. This could be changed to look for the +; first empty block and then continue until the start of this +; empty space is located and then used that starting block. +; This should help speed up access to some files especially on +; a well used disk with lots of fairly small 'holes'. +; + POP BC ;nope, check some more. + POP DE +; +; Now look after target block. +; +FNDSPA2:LD HL,(DSKSIZE) ;is block (DE) within disk limits? + LD A,E + SUB L + LD A,D + SBC A,H + JP NC,FNDSPA4 + INC DE ;yes, move on to next one. + PUSH BC + PUSH DE + LD B,D + LD C,E + CALL CKBITMAP ;check it. + RRA ;empty? + JP NC,FNDSPA3 + POP DE ;nope, continue searching. + POP BC + JP FNDSPA1 +; +; Empty block found. Set it as used and return with (HL) +; pointing to it (true?). +; +FNDSPA3:RLA ;reset byte. + INC A ;and set bit 0. + CALL STBMAP1 ;update bit map. + POP HL ;set return registers. + POP DE + RET +; +; Free block was not found. If (BC) is not zero, then we have +; not checked all of the disk space. +; +FNDSPA4:LD A,C + OR B + JP NZ,FNDSPA1 + LD HL,0 ;set 'not found' status. + RET +; +; Move a complete fcb entry into the directory and write it. +; +FCBSET: LD C,0 + LD E,32 ;length of each entry. +; +; Move (E) bytes from the fcb pointed to by (PARAMS) into +; fcb in directory starting at relative byte (C). This updated +; directory buffer is then written to the disk. +; +UPDATE: PUSH DE + LD B,0 ;set (BC) to relative byte position. + LD HL,(PARAMS) ;get address of fcb. + ADD HL,BC ;compute starting byte. + EX DE,HL + CALL FCB2HL ;get address of fcb to update in directory. + POP BC ;set (C) to number of bytes to change. + CALL DE2HL +UPDATE1:CALL TRKSEC ;determine the track and sector affected. + JP DIRWRITE ;then write this sector out. +; +; Routine to change the name of all files on the disk with a +; specified name. The fcb contains the current name as the +; first 12 characters and the new name 16 bytes into the fcb. +; +CHGNAMES: CALL CHKWPRT ;check for a write protected disk. + LD C,12 ;match first 12 bytes of fcb only. + CALL FINDFST ;get first name. + LD HL,(PARAMS) ;get address of fcb. + LD A,(HL) ;get user number. + LD DE,16 ;move over to desired name. + ADD HL,DE + LD (HL),A ;keep same user number. +CHGNAM1:CALL CKFILPOS ;any matching file found? + RET Z ;no, we must be done. + CALL CHKROFL ;check for read only file. + LD C,16 ;start 16 bytes into fcb. + LD E,12 ;and update the first 12 bytes of directory. + CALL UPDATE + CALL FINDNXT ;get te next file name. + JP CHGNAM1 ;and continue. +; +; Update a files attributes. The procedure is to search for +; every file with the same name as shown in fcb (ignoring bit 7) +; and then to update it (which includes bit 7). No other changes +; are made. +; +SAVEATTR: LD C,12 ;match first 12 bytes. + CALL FINDFST ;look for first filename. +SAVATR1:CALL CKFILPOS ;was one found? + RET Z ;nope, we must be done. + LD C,0 ;yes, update the first 12 bytes now. + LD E,12 + CALL UPDATE ;update filename and write directory. + CALL FINDNXT ;and get the next file. + JP SAVATR1 ;then continue until done. +; +; Open a file (name specified in fcb). +; +OPENIT: LD C,15 ;compare the first 15 bytes. + CALL FINDFST ;get the first one in directory. + CALL CKFILPOS ;any at all? + RET Z +OPENIT1:CALL SETEXT ;point to extent byte within users fcb. + LD A,(HL) ;and get it. + PUSH AF ;save it and address. + PUSH HL + CALL FCB2HL ;point to fcb in directory. + EX DE,HL + LD HL,(PARAMS) ;this is the users copy. + LD C,32 ;move it into users space. + PUSH DE + CALL DE2HL + CALL SETS2B7 ;set bit 7 in 's2' byte (unmodified). + POP DE ;now get the extent byte from this fcb. + LD HL,12 + ADD HL,DE + LD C,(HL) ;into (C). + LD HL,15 ;now get the record count byte into (B). + ADD HL,DE + LD B,(HL) + POP HL ;keep the same extent as the user had originally. + POP AF + LD (HL),A + LD A,C ;is it the same as in the directory fcb? + CP (HL) + LD A,B ;if yes, then use the same record count. + JP Z,OPENIT2 + LD A,0 ;if the user specified an extent greater than + JP C,OPENIT2 ;the one in the directory, then set record count to 0. + LD A,128 ;otherwise set to maximum. +OPENIT2:LD HL,(PARAMS) ;set record count in users fcb to (A). + LD DE,15 + ADD HL,DE ;compute relative position. + LD (HL),A ;and set the record count. + RET +; +; Move two bytes from (DE) to (HL) if (and only if) (HL) +; point to a zero value (16 bit). +; Return with zero flag set it (DE) was moved. Registers (DE) +; and (HL) are not changed. However (A) is. +; +MOVEWORD: LD A,(HL) ;check for a zero word. + INC HL + OR (HL) ;both bytes zero? + DEC HL + RET NZ ;nope, just return. + LD A,(DE) ;yes, move two bytes from (DE) into + LD (HL),A ;this zero space. + INC DE + INC HL + LD A,(DE) + LD (HL),A + DEC DE ;don't disturb these registers. + DEC HL + RET +; +; Get here to close a file specified by (fcb). +; +CLOSEIT:XOR A ;clear status and file position bytes. + LD (STATUS),A + LD (FILEPOS),A + LD (FILEPOS+1),A + CALL GETWPRT ;get write protect bit for this drive. + RET NZ ;just return if it is set. + CALL GETS2 ;else get the 's2' byte. + AND 80H ;and look at bit 7 (file unmodified?). + RET NZ ;just return if set. + LD C,15 ;else look up this file in directory. + CALL FINDFST + CALL CKFILPOS ;was it found? + RET Z ;just return if not. + LD BC,16 ;set (HL) pointing to records used section. + CALL FCB2HL + ADD HL,BC + EX DE,HL + LD HL,(PARAMS) ;do the same for users specified fcb. + ADD HL,BC + LD C,16 ;this many bytes are present in this extent. +CLOSEIT1: LD A,(BIGDISK) ;8 or 16 bit record numbers? + OR A + JP Z,CLOSEIT4 + LD A,(HL) ;just 8 bit. Get one from users fcb. + OR A + LD A,(DE) ;now get one from directory fcb. + JP NZ,CLOSEIT2 + LD (HL),A ;users byte was zero. Update from directory. +CLOSEIT2: OR A + JP NZ,CLOSEIT3 + LD A,(HL) ;directories byte was zero, update from users fcb. + LD (DE),A +CLOSEIT3: CP (HL) ;if neither one of these bytes were zero, + JP NZ,CLOSEIT7 ;then close error if they are not the same. + JP CLOSEIT5 ;ok so far, get to next byte in fcbs. +CLOSEIT4: CALL MOVEWORD ;update users fcb if it is zero. + EX DE,HL + CALL MOVEWORD ;update directories fcb if it is zero. + EX DE,HL + LD A,(DE) ;if these two values are no different, + CP (HL) ;then a close error occured. + JP NZ,CLOSEIT7 + INC DE ;check second byte. + INC HL + LD A,(DE) + CP (HL) + JP NZ,CLOSEIT7 + DEC C ;remember 16 bit values. +CLOSEIT5: INC DE ;bump to next item in table. + INC HL + DEC C ;there are 16 entries only. + JP NZ,CLOSEIT1 ;continue if more to do. + LD BC,0FFECH ;backup 20 places (extent byte). + ADD HL,BC + EX DE,HL + ADD HL,BC + LD A,(DE) + CP (HL) ;directory's extent already greater than the + JP C,CLOSEIT6 ;users extent? + LD (HL),A ;no, update directory extent. + LD BC,3 ;and update the record count byte in + ADD HL,BC ;directories fcb. + EX DE,HL + ADD HL,BC + LD A,(HL) ;get from user. + LD (DE),A ;and put in directory. +CLOSEIT6: LD A,0FFH ;set 'was open and is now closed' byte. + LD (CLOSEFLG),A + JP UPDATE1 ;update the directory now. +CLOSEIT7: LD HL,STATUS ;set return status and then return. + DEC (HL) + RET +; +; Routine to get the next empty space in the directory. It +; will then be cleared for use. +; +GETEMPTY: CALL CHKWPRT ;make sure disk is not write protected. + LD HL,(PARAMS) ;save current parameters (fcb). + PUSH HL + LD HL,EMPTYFCB ;use special one for empty space. + LD (PARAMS),HL + LD C,1 ;search for first empty spot in directory. + CALL FINDFST ;(* only check first byte *) + CALL CKFILPOS ;none? + POP HL + LD (PARAMS),HL ;restore original fcb address. + RET Z ;return if no more space. + EX DE,HL + LD HL,15 ;point to number of records for this file. + ADD HL,DE + LD C,17 ;and clear all of this space. + XOR A +GETMT1: LD (HL),A + INC HL + DEC C + JP NZ,GETMT1 + LD HL,13 ;clear the 's1' byte also. + ADD HL,DE + LD (HL),A + CALL CHKNMBR ;keep (SCRATCH1) within bounds. + CALL FCBSET ;write out this fcb entry to directory. + JP SETS2B7 ;set 's2' byte bit 7 (unmodified at present). +; +; Routine to close the current extent and open the next one +; for reading. +; +GETNEXT:XOR A + LD (CLOSEFLG),A ;clear close flag. + CALL CLOSEIT ;close this extent. + CALL CKFILPOS + RET Z ;not there??? + LD HL,(PARAMS) ;get extent byte. + LD BC,12 + ADD HL,BC + LD A,(HL) ;and increment it. + INC A + AND 1FH ;keep within range 0-31. + LD (HL),A + JP Z,GTNEXT1 ;overflow? + LD B,A ;mask extent byte. + LD A,(EXTMASK) + AND B + LD HL,CLOSEFLG ;check close flag (0ffh is ok). + AND (HL) + JP Z,GTNEXT2 ;if zero, we must read in next extent. + JP GTNEXT3 ;else, it is already in memory. +GTNEXT1:LD BC,2 ;Point to the 's2' byte. + ADD HL,BC + INC (HL) ;and bump it. + LD A,(HL) ;too many extents? + AND 0FH + JP Z,GTNEXT5 ;yes, set error code. +; +; Get here to open the next extent. +; +GTNEXT2:LD C,15 ;set to check first 15 bytes of fcb. + CALL FINDFST ;find the first one. + CALL CKFILPOS ;none available? + JP NZ,GTNEXT3 + LD A,(RDWRTFLG) ;no extent present. Can we open an empty one? + INC A ;0ffh means reading (so not possible). + JP Z,GTNEXT5 ;or an error. + CALL GETEMPTY ;we are writing, get an empty entry. + CALL CKFILPOS ;none? + JP Z,GTNEXT5 ;error if true. + JP GTNEXT4 ;else we are almost done. +GTNEXT3:CALL OPENIT1 ;open this extent. +GTNEXT4:CALL STRDATA ;move in updated data (rec #, extent #, etc.) + XOR A ;clear status and return. + JP SETSTAT +; +; Error in extending the file. Too many extents were needed +; or not enough space on the disk. +; +GTNEXT5:CALL IOERR1 ;set error code, clear bit 7 of 's2' + JP SETS2B7 ;so this is not written on a close. +; +; Read a sequential file. +; +RDSEQ: LD A,1 ;set sequential access mode. + LD (MODE),A +RDSEQ1: LD A,0FFH ;don't allow reading unwritten space. + LD (RDWRTFLG),A + CALL STRDATA ;put rec# and ext# into fcb. + LD A,(SAVNREC) ;get next record to read. + LD HL,SAVNXT ;get number of records in extent. + CP (HL) ;within this extent? + JP C,RDSEQ2 + CP 128 ;no. Is this extent fully used? + JP NZ,RDSEQ3 ;no. End-of-file. + CALL GETNEXT ;yes, open the next one. + XOR A ;reset next record to read. + LD (SAVNREC),A + LD A,(STATUS) ;check on open, successful? + OR A + JP NZ,RDSEQ3 ;no, error. +RDSEQ2: CALL COMBLK ;ok. compute block number to read. + CALL CHKBLK ;check it. Within bounds? + JP Z,RDSEQ3 ;no, error. + CALL LOGICAL ;convert (BLKNMBR) to logical sector (128 byte). + CALL TRKSEC1 ;set the track and sector for this block #. + CALL DOREAD ;and read it. + JP SETNREC ;and set the next record to be accessed. +; +; Read error occured. Set status and return. +; +RDSEQ3: JP IOERR1 +; +; Write the next sequential record. +; +WTSEQ: LD A,1 ;set sequential access mode. + LD (MODE),A +WTSEQ1: LD A,0 ;allow an addition empty extent to be opened. + LD (RDWRTFLG),A + CALL CHKWPRT ;check write protect status. + LD HL,(PARAMS) + CALL CKROF1 ;check for read only file, (HL) already set to fcb. + CALL STRDATA ;put updated data into fcb. + LD A,(SAVNREC) ;get record number to write. + CP 128 ;within range? + JP NC,IOERR1 ;no, error(?). + CALL COMBLK ;compute block number. + CALL CHKBLK ;check number. + LD C,0 ;is there one to write to? + JP NZ,WTSEQ6 ;yes, go do it. + CALL GETBLOCK ;get next block number within fcb to use. + LD (RELBLOCK),A ;and save. + LD BC,0 ;start looking for space from the start + OR A ;if none allocated as yet. + JP Z,WTSEQ2 + LD C,A ;extract previous block number from fcb + DEC BC ;so we can be closest to it. + CALL EXTBLK + LD B,H + LD C,L +WTSEQ2: CALL FNDSPACE ;find the next empty block nearest number (BC). + LD A,L ;check for a zero number. + OR H + JP NZ,WTSEQ3 + LD A,2 ;no more space? + JP SETSTAT +WTSEQ3: LD (BLKNMBR),HL ;save block number to access. + EX DE,HL ;put block number into (DE). + LD HL,(PARAMS) ;now we must update the fcb for this + LD BC,16 ;newly allocated block. + ADD HL,BC + LD A,(BIGDISK) ;8 or 16 bit block numbers? + OR A + LD A,(RELBLOCK) ;(* update this entry *) + JP Z,WTSEQ4 ;zero means 16 bit ones. + CALL ADDA2HL ;(HL)=(HL)+(A) + LD (HL),E ;store new block number. + JP WTSEQ5 +WTSEQ4: LD C,A ;compute spot in this 16 bit table. + LD B,0 + ADD HL,BC + ADD HL,BC + LD (HL),E ;stuff block number (DE) there. + INC HL + LD (HL),D +WTSEQ5: LD C,2 ;set (C) to indicate writing to un-used disk space. +WTSEQ6: LD A,(STATUS) ;are we ok so far? + OR A + RET NZ + PUSH BC ;yes, save write flag for bios (register C). + CALL LOGICAL ;convert (BLKNMBR) over to loical sectors. + LD A,(MODE) ;get access mode flag (1=sequential, + DEC A ;0=random, 2=special?). + DEC A + JP NZ,WTSEQ9 +; +; Special random i/o from function #40. Maybe for M/PM, but the +; current block, if it has not been written to, will be zeroed +; out and then written (reason?). +; + POP BC + PUSH BC + LD A,C ;get write status flag (2=writing unused space). + DEC A + DEC A + JP NZ,WTSEQ9 + PUSH HL + LD HL,(DIRBUF) ;zero out the directory buffer. + LD D,A ;note that (A) is zero here. +WTSEQ7: LD (HL),A + INC HL + INC D ;do 128 bytes. + JP P,WTSEQ7 + CALL DIRDMA ;tell the bios the dma address for directory access. + LD HL,(LOGSECT) ;get sector that starts current block. + LD C,2 ;set 'writing to unused space' flag. +WTSEQ8: LD (BLKNMBR),HL ;save sector to write. + PUSH BC + CALL TRKSEC1 ;determine its track and sector numbers. + POP BC + CALL DOWRITE ;now write out 128 bytes of zeros. + LD HL,(BLKNMBR) ;get sector number. + LD C,0 ;set normal write flag. + LD A,(BLKMASK) ;determine if we have written the entire + LD B,A ;physical block. + AND L + CP B + INC HL ;prepare for the next one. + JP NZ,WTSEQ8 ;continue until (BLKMASK+1) sectors written. + POP HL ;reset next sector number. + LD (BLKNMBR),HL + CALL DEFDMA ;and reset dma address. +; +; Normal disk write. Set the desired track and sector then +; do the actual write. +; +WTSEQ9: CALL TRKSEC1 ;determine track and sector for this write. + POP BC ;get write status flag. + PUSH BC + CALL DOWRITE ;and write this out. + POP BC + LD A,(SAVNREC) ;get number of records in file. + LD HL,SAVNXT ;get last record written. + CP (HL) + JP C,WTSEQ10 + LD (HL),A ;we have to update record count. + INC (HL) + LD C,2 +; +;* This area has been patched to correct disk update problem +;* when using blocking and de-blocking in the BIOS. +; +WTSEQ10:NOP ;was 'dcr c' + NOP ;was 'dcr c' + LD HL,0 ;was 'jnz wtseq99' +; +; * End of patch. +; + PUSH AF + CALL GETS2 ;set 'extent written to' flag. + AND 7FH ;(* clear bit 7 *) + LD (HL),A + POP AF ;get record count for this extent. +WTSEQ99:CP 127 ;is it full? + JP NZ,WTSEQ12 + LD A,(MODE) ;yes, are we in sequential mode? + CP 1 + JP NZ,WTSEQ12 + CALL SETNREC ;yes, set next record number. + CALL GETNEXT ;and get next empty space in directory. + LD HL,STATUS ;ok? + LD A,(HL) + OR A + JP NZ,WTSEQ11 + DEC A ;yes, set record count to -1. + LD (SAVNREC),A +WTSEQ11:LD (HL),0 ;clear status. +WTSEQ12:JP SETNREC ;set next record to access. +; +; For random i/o, set the fcb for the desired record number +; based on the 'r0,r1,r2' bytes. These bytes in the fcb are +; used as follows: +; +; fcb+35 fcb+34 fcb+33 +; | 'r-2' | 'r-1' | 'r-0' | +; |7 0 | 7 0 | 7 0| +; |0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0| +; | overflow | | extra | extent | record # | +; | ______________| |_extent|__number___|_____________| +; also 's2' +; +; On entry, register (C) contains 0ffh if this is a read +; and thus we can not access unwritten disk space. Otherwise, +; another extent will be opened (for writing) if required. +; +POSITION: XOR A ;set random i/o flag. + LD (MODE),A +; +; Special entry (function #40). M/PM ? +; +POSITN1:PUSH BC ;save read/write flag. + LD HL,(PARAMS) ;get address of fcb. + EX DE,HL + LD HL,33 ;now get byte 'r0'. + ADD HL,DE + LD A,(HL) + AND 7FH ;keep bits 0-6 for the record number to access. + PUSH AF + LD A,(HL) ;now get bit 7 of 'r0' and bits 0-3 of 'r1'. + RLA + INC HL + LD A,(HL) + RLA + AND 1FH ;and save this in bits 0-4 of (C). + LD C,A ;this is the extent byte. + LD A,(HL) ;now get the extra extent byte. + RRA + RRA + RRA + RRA + AND 0FH + LD B,A ;and save it in (B). + POP AF ;get record number back to (A). + INC HL ;check overflow byte 'r2'. + LD L,(HL) + INC L + DEC L + LD L,6 ;prepare for error. + JP NZ,POSITN5 ;out of disk space error. + LD HL,32 ;store record number into fcb. + ADD HL,DE + LD (HL),A + LD HL,12 ;and now check the extent byte. + ADD HL,DE + LD A,C + SUB (HL) ;same extent as before? + JP NZ,POSITN2 + LD HL,14 ;yes, check extra extent byte 's2' also. + ADD HL,DE + LD A,B + SUB (HL) + AND 7FH + JP Z,POSITN3 ;same, we are almost done then. +; +; Get here when another extent is required. +; +POSITN2:PUSH BC + PUSH DE + CALL CLOSEIT ;close current extent. + POP DE + POP BC + LD L,3 ;prepare for error. + LD A,(STATUS) + INC A + JP Z,POSITN4 ;close error. + LD HL,12 ;put desired extent into fcb now. + ADD HL,DE + LD (HL),C + LD HL,14 ;and store extra extent byte 's2'. + ADD HL,DE + LD (HL),B + CALL OPENIT ;try and get this extent. + LD A,(STATUS) ;was it there? + INC A + JP NZ,POSITN3 + POP BC ;no. can we create a new one (writing?). + PUSH BC + LD L,4 ;prepare for error. + INC C + JP Z,POSITN4 ;nope, reading unwritten space error. + CALL GETEMPTY ;yes we can, try to find space. + LD L,5 ;prepare for error. + LD A,(STATUS) + INC A + JP Z,POSITN4 ;out of space? +; +; Normal return location. Clear error code and return. +; +POSITN3:POP BC ;restore stack. + XOR A ;and clear error code byte. + JP SETSTAT +; +; Error. Set the 's2' byte to indicate this (why?). +; +POSITN4:PUSH HL + CALL GETS2 + LD (HL),0C0H + POP HL +; +; Return with error code (presently in L). +; +POSITN5:POP BC + LD A,L ;get error code. + LD (STATUS),A + JP SETS2B7 +; +; Read a random record. +; +READRAN:LD C,0FFH ;set 'read' status. + CALL POSITION ;position the file to proper record. + CALL Z,RDSEQ1 ;and read it as usual (if no errors). + RET +; +; Write to a random record. +; +WRITERAN: LD C,0 ;set 'writing' flag. + CALL POSITION ;position the file to proper record. + CALL Z,WTSEQ1 ;and write as usual (if no errors). + RET +; +; Compute the random record number. Enter with (HL) pointing +; to a fcb an (DE) contains a relative location of a record +; number. On exit, (C) contains the 'r0' byte, (B) the 'r1' +; byte, and (A) the 'r2' byte. +; +; On return, the zero flag is set if the record is within +; bounds. Otherwise, an overflow occured. +; +COMPRAND: EX DE,HL ;save fcb pointer in (DE). + ADD HL,DE ;compute relative position of record #. + LD C,(HL) ;get record number into (BC). + LD B,0 + LD HL,12 ;now get extent. + ADD HL,DE + LD A,(HL) ;compute (BC)=(record #)+(extent)*128. + RRCA ;move lower bit into bit 7. + AND 80H ;and ignore all other bits. + ADD A,C ;add to our record number. + LD C,A + LD A,0 ;take care of any carry. + ADC A,B + LD B,A + LD A,(HL) ;now get the upper bits of extent into + RRCA ;bit positions 0-3. + AND 0FH ;and ignore all others. + ADD A,B ;add this in to 'r1' byte. + LD B,A + LD HL,14 ;get the 's2' byte (extra extent). + ADD HL,DE + LD A,(HL) + ADD A,A ;and shift it left 4 bits (bits 4-7). + ADD A,A + ADD A,A + ADD A,A + PUSH AF ;save carry flag (bit 0 of flag byte). + ADD A,B ;now add extra extent into 'r1'. + LD B,A + PUSH AF ;and save carry (overflow byte 'r2'). + POP HL ;bit 0 of (L) is the overflow indicator. + LD A,L + POP HL ;and same for first carry flag. + OR L ;either one of these set? + AND 01H ;only check the carry flags. + RET +; +; Routine to setup the fcb (bytes 'r0', 'r1', 'r2') to +; reflect the last record used for a random (or other) file. +; This reads the directory and looks at all extents computing +; the largerst record number for each and keeping the maximum +; value only. Then 'r0', 'r1', and 'r2' will reflect this +; maximum record number. This is used to compute the space used +; by a random file. +; +RANSIZE:LD C,12 ;look thru directory for first entry with + CALL FINDFST ;this name. + LD HL,(PARAMS) ;zero out the 'r0, r1, r2' bytes. + LD DE,33 + ADD HL,DE + PUSH HL + LD (HL),D ;note that (D)=0. + INC HL + LD (HL),D + INC HL + LD (HL),D +RANSIZ1:CALL CKFILPOS ;is there an extent to process? + JP Z,RANSIZ3 ;no, we are done. + CALL FCB2HL ;set (HL) pointing to proper fcb in dir. + LD DE,15 ;point to last record in extent. + CALL COMPRAND ;and compute random parameters. + POP HL + PUSH HL ;now check these values against those + LD E,A ;already in fcb. + LD A,C ;the carry flag will be set if those + SUB (HL) ;in the fcb represent a larger size than + INC HL ;this extent does. + LD A,B + SBC A,(HL) + INC HL + LD A,E + SBC A,(HL) + JP C,RANSIZ2 + LD (HL),E ;we found a larger (in size) extent. + DEC HL ;stuff these values into fcb. + LD (HL),B + DEC HL + LD (HL),C +RANSIZ2:CALL FINDNXT ;now get the next extent. + JP RANSIZ1 ;continue til all done. +RANSIZ3:POP HL ;we are done, restore the stack and + RET ;return. +; +; Function to return the random record position of a given +; file which has been read in sequential mode up to now. +; +SETRAN: LD HL,(PARAMS) ;point to fcb. + LD DE,32 ;and to last used record. + CALL COMPRAND ;compute random position. + LD HL,33 ;now stuff these values into fcb. + ADD HL,DE + LD (HL),C ;move 'r0'. + INC HL + LD (HL),B ;and 'r1'. + INC HL + LD (HL),A ;and lastly 'r2'. + RET +; +; This routine select the drive specified in (ACTIVE) and +; update the login vector and bitmap table if this drive was +; not already active. +; +LOGINDRV: LD HL,(LOGIN) ;get the login vector. + LD A,(ACTIVE) ;get the default drive. + LD C,A + CALL SHIFTR ;position active bit for this drive + PUSH HL ;into bit 0. + EX DE,HL + CALL SELECT ;select this drive. + POP HL + CALL Z,SLCTERR ;valid drive? + LD A,L ;is this a newly activated drive? + RRA + RET C + LD HL,(LOGIN) ;yes, update the login vector. + LD C,L + LD B,H + CALL SETBIT + LD (LOGIN),HL ;and save. + JP BITMAP ;now update the bitmap. +; +; Function to set the active disk number. +; +SETDSK: LD A,(EPARAM) ;get parameter passed and see if this + LD HL,ACTIVE ;represents a change in drives. + CP (HL) + RET Z + LD (HL),A ;yes it does, log it in. + JP LOGINDRV +; +; This is the 'auto disk select' routine. The firsst byte +; of the fcb is examined for a drive specification. If non +; zero then the drive will be selected and loged in. +; +AUTOSEL:LD A,0FFH ;say 'auto-select activated'. + LD (AUTO),A + LD HL,(PARAMS) ;get drive specified. + LD A,(HL) + AND 1FH ;look at lower 5 bits. + DEC A ;adjust for (1=A, 2=B) etc. + LD (EPARAM),A ;and save for the select routine. + CP 1EH ;check for 'no change' condition. + JP NC,AUTOSL1 ;yes, don't change. + LD A,(ACTIVE) ;we must change, save currently active + LD (OLDDRV),A ;drive. + LD A,(HL) ;and save first byte of fcb also. + LD (AUTOFLAG),A ;this must be non-zero. + AND 0E0H ;whats this for (bits 6,7 are used for + LD (HL),A ;something)? + CALL SETDSK ;select and log in this drive. +AUTOSL1:LD A,(USERNO) ;move user number into fcb. + LD HL,(PARAMS) ;(* upper half of first byte *) + OR (HL) + LD (HL),A + RET ;and return (all done). +; +; Function to return the current cp/m version number. +; +GETVER: LD A,022H ;version 2.2 + JP SETSTAT +; +; Function to reset the disk system. +; +RSTDSK: LD HL,0 ;clear write protect status and log + LD (WRTPRT),HL ;in vector. + LD (LOGIN),HL + XOR A ;select drive 'A'. + LD (ACTIVE),A + LD HL,TBUFF ;setup default dma address. + LD (USERDMA),HL + CALL DEFDMA + JP LOGINDRV ;now log in drive 'A'. +; +; Function to open a specified file. +; +OPENFIL:CALL CLEARS2 ;clear 's2' byte. + CALL AUTOSEL ;select proper disk. + JP OPENIT ;and open the file. +; +; Function to close a specified file. +; +CLOSEFIL: CALL AUTOSEL ;select proper disk. + JP CLOSEIT ;and close the file. +; +; Function to return the first occurence of a specified file +; name. If the first byte of the fcb is '?' then the name will +; not be checked (get the first entry no matter what). +; +GETFST: LD C,0 ;prepare for special search. + EX DE,HL + LD A,(HL) ;is first byte a '?'? + CP '?' + JP Z,GETFST1 ;yes, just get very first entry (zero length match). + CALL SETEXT ;get the extension byte from fcb. + LD A,(HL) ;is it '?'? if yes, then we want + CP '?' ;an entry with a specific 's2' byte. + CALL NZ,CLEARS2 ;otherwise, look for a zero 's2' byte. + CALL AUTOSEL ;select proper drive. + LD C,15 ;compare bytes 0-14 in fcb (12&13 excluded). +GETFST1:CALL FINDFST ;find an entry and then move it into + JP MOVEDIR ;the users dma space. +; +; Function to return the next occurence of a file name. +; +GETNXT: LD HL,(SAVEFCB) ;restore pointers. note that no + LD (PARAMS),HL ;other dbos calls are allowed. + CALL AUTOSEL ;no error will be returned, but the + CALL FINDNXT ;results will be wrong. + JP MOVEDIR +; +; Function to delete a file by name. +; +DELFILE:CALL AUTOSEL ;select proper drive. + CALL ERAFILE ;erase the file. + JP STSTATUS ;set status and return. +; +; Function to execute a sequential read of the specified +; record number. +; +READSEQ:CALL AUTOSEL ;select proper drive then read. + JP RDSEQ +; +; Function to write the net sequential record. +; +WRTSEQ: CALL AUTOSEL ;select proper drive then write. + JP WTSEQ +; +; Create a file function. +; +FCREATE:CALL CLEARS2 ;clear the 's2' byte on all creates. + CALL AUTOSEL ;select proper drive and get the next + JP GETEMPTY ;empty directory space. +; +; Function to rename a file. +; +RENFILE:CALL AUTOSEL ;select proper drive and then switch + CALL CHGNAMES ;file names. + JP STSTATUS +; +; Function to return the login vector. +; +GETLOG: LD HL,(LOGIN) + JP GETPRM1 +; +; Function to return the current disk assignment. +; +GETCRNT:LD A,(ACTIVE) + JP SETSTAT +; +; Function to set the dma address. +; +PUTDMA: EX DE,HL + LD (USERDMA),HL ;save in our space and then get to + JP DEFDMA ;the bios with this also. +; +; Function to return the allocation vector. +; +GETALOC:LD HL,(ALOCVECT) + JP GETPRM1 +; +; Function to return the read-only status vector. +; +GETROV: LD HL,(WRTPRT) + JP GETPRM1 +; +; Function to set the file attributes (read-only, system). +; +SETATTR:CALL AUTOSEL ;select proper drive then save attributes. + CALL SAVEATTR + JP STSTATUS +; +; Function to return the address of the disk parameter block +; for the current drive. +; +GETPARM:LD HL,(DISKPB) +GETPRM1:LD (STATUS),HL + RET +; +; Function to get or set the user number. If (E) was (FF) +; then this is a request to return the current user number. +; Else set the user number from (E). +; +GETUSER:LD A,(EPARAM) ;get parameter. + CP 0FFH ;get user number? + JP NZ,SETUSER + LD A,(USERNO) ;yes, just do it. + JP SETSTAT +SETUSER:AND 1FH ;no, we should set it instead. keep low + LD (USERNO),A ;bits (0-4) only. + RET +; +; Function to read a random record from a file. +; +RDRANDOM: CALL AUTOSEL ;select proper drive and read. + JP READRAN +; +; Function to compute the file size for random files. +; +WTRANDOM: CALL AUTOSEL ;select proper drive and write. + JP WRITERAN +; +; Function to compute the size of a random file. +; +FILESIZE: CALL AUTOSEL ;select proper drive and check file length + JP RANSIZE +; +; Function #37. This allows a program to log off any drives. +; On entry, set (DE) to contain a word with bits set for those +; drives that are to be logged off. The log-in vector and the +; write protect vector will be updated. This must be a M/PM +; special function. +; +LOGOFF: LD HL,(PARAMS) ;get drives to log off. + LD A,L ;for each bit that is set, we want + CPL ;to clear that bit in (LOGIN) + LD E,A ;and (WRTPRT). + LD A,H + CPL + LD HL,(LOGIN) ;reset the login vector. + AND H + LD D,A + LD A,L + AND E + LD E,A + LD HL,(WRTPRT) + EX DE,HL + LD (LOGIN),HL ;and save. + LD A,L ;now do the write protect vector. + AND E + LD L,A + LD A,H + AND D + LD H,A + LD (WRTPRT),HL ;and save. all done. + RET +; +; Get here to return to the user. +; +GOBACK: LD A,(AUTO) ;was auto select activated? + OR A + JP Z,GOBACK1 + LD HL,(PARAMS) ;yes, but was a change made? + LD (HL),0 ;(* reset first byte of fcb *) + LD A,(AUTOFLAG) + OR A + JP Z,GOBACK1 + LD (HL),A ;yes, reset first byte properly. + LD A,(OLDDRV) ;and get the old drive and select it. + LD (EPARAM),A + CALL SETDSK +GOBACK1:LD HL,(USRSTACK) ;reset the users stack pointer. + LD SP,HL + LD HL,(STATUS) ;get return status. + LD A,L ;force version 1.4 compatability. + LD B,H + RET ;and go back to user. +; +; Function #40. This is a special entry to do random i/o. +; For the case where we are writing to unused disk space, this +; space will be zeroed out first. This must be a M/PM special +; purpose function, because why would any normal program even +; care about the previous contents of a sector about to be +; written over. +; +WTSPECL:CALL AUTOSEL ;select proper drive. + LD A,2 ;use special write mode. + LD (MODE),A + LD C,0 ;set write indicator. + CALL POSITN1 ;position the file. + CALL Z,WTSEQ1 ;and write (if no errors). + RET +; +;************************************************************** +;* +;* BDOS data storage pool. +;* +;************************************************************** +; +EMPTYFCB: DEFB 0E5H ;empty directory segment indicator. +WRTPRT: DEFW 0 ;write protect status for all 16 drives. +LOGIN: DEFW 0 ;drive active word (1 bit per drive). +USERDMA:DEFW 080H ;user's dma address (defaults to 80h). +; +; Scratch areas from parameter block. +; +SCRATCH1: DEFW 0 ;relative position within dir segment for file (0-3). +SCRATCH2: DEFW 0 ;last selected track number. +SCRATCH3: DEFW 0 ;last selected sector number. +; +; Disk storage areas from parameter block. +; +DIRBUF: DEFW 0 ;address of directory buffer to use. +DISKPB: DEFW 0 ;contains address of disk parameter block. +CHKVECT:DEFW 0 ;address of check vector. +ALOCVECT: DEFW 0 ;address of allocation vector (bit map). +; +; Parameter block returned from the bios. +; +SECTORS:DEFW 0 ;sectors per track from bios. +BLKSHFT:DEFB 0 ;block shift. +BLKMASK:DEFB 0 ;block mask. +EXTMASK:DEFB 0 ;extent mask. +DSKSIZE:DEFW 0 ;disk size from bios (number of blocks-1). +DIRSIZE:DEFW 0 ;directory size. +ALLOC0: DEFW 0 ;storage for first bytes of bit map (dir space used). +ALLOC1: DEFW 0 +OFFSET: DEFW 0 ;first usable track number. +XLATE: DEFW 0 ;sector translation table address. +; +; +CLOSEFLG: DEFB 0 ;close flag (=0ffh is extent written ok). +RDWRTFLG: DEFB 0 ;read/write flag (0ffh=read, 0=write). +FNDSTAT:DEFB 0 ;filename found status (0=found first entry). +MODE: DEFB 0 ;I/o mode select (0=random, 1=sequential, 2=special random). +EPARAM: DEFB 0 ;storage for register (E) on entry to bdos. +RELBLOCK: DEFB 0 ;relative position within fcb of block number written. +COUNTER:DEFB 0 ;byte counter for directory name searches. +SAVEFCB:DEFW 0,0 ;save space for address of fcb (for directory searches). +BIGDISK:DEFB 0 ;if =0 then disk is > 256 blocks long. +AUTO: DEFB 0 ;if non-zero, then auto select activated. +OLDDRV: DEFB 0 ;on auto select, storage for previous drive. +AUTOFLAG: DEFB 0 ;if non-zero, then auto select changed drives. +SAVNXT: DEFB 0 ;storage for next record number to access. +SAVEXT: DEFB 0 ;storage for extent number of file. +SAVNREC:DEFW 0 ;storage for number of records in file. +BLKNMBR:DEFW 0 ;block number (physical sector) used within a file or logical sect +LOGSECT:DEFW 0 ;starting logical (128 byte) sector of block (physical sector). +FCBPOS: DEFB 0 ;relative position within buffer for fcb of file of interest. +FILEPOS:DEFW 0 ;files position within directory (0 to max entries -1). +; +; Disk directory buffer checksum bytes. One for each of the +; 16 possible drives. +; +CKSUMTBL: DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +; +; Extra space ? +; + DEFB 0,0,0,0 +; +;************************************************************** +;* +;* B I O S J U M P T A B L E +;* +;************************************************************** +; +BOOT: JP 0 ;NOTE WE USE FAKE DESTINATIONS +WBOOT: JP 0 +CONST: JP 0 +CONIN: JP 0 +CONOUT: JP 0 +LIST: JP 0 +PUNCH: JP 0 +READER: JP 0 +HOME: JP 0 +SELDSK: JP 0 +SETTRK: JP 0 +SETSEC: JP 0 +SETDMA: JP 0 +READ: JP 0 +WRITE: JP 0 +PRSTAT: JP 0 +SECTRN: JP 0 +; +;* +;****************** E N D O F C P / M ***************** +;* + diff --git a/src/loader.z80 b/src/loader.z80 new file mode 100644 index 0000000..8045c32 --- /dev/null +++ b/src/loader.z80 @@ -0,0 +1,30 @@ +;Retrieves CP/M from disk and loads it in memory starting at E400h +;Uses calls to ROM subroutine for disk read. +;Reads track 0, sectors 2 to 26, then track 1, sectors 1 to 25 +;This program is loaded into LBA sector 0 of disk, read to loc. 0800h by ROM disk_read subroutine, and executed. + +#target bin +#code _HOME, 0x1100 + +hstbuf equ 0x1200 ;will put 256-byte raw sector here +disk_read equ 0x0296 ;subroutine in 2K ROM +cpm equ 0x7A00 ;CP/M cold start entry in BIOS + +main: + ld c,1 ;LBA bits 0 to 7 + ld b,0 ;LBA bits 8 to 15 + ld e,0 ;LBA bits 16 to 23 + ld hl,0x6400 ; Memory address -- start of CCP + +loop: + call disk_read ;subroutine in ROM + ld a,c + cp 50 + jp z,done + inc a + ld c,a + jp loop + +done: + out (1),a ;switch memory config to all-RAM + jp cpm ;to BIOS cold start entry diff --git a/src/putsys.z80 b/src/putsys.z80 new file mode 100644 index 0000000..786b4d1 --- /dev/null +++ b/src/putsys.z80 @@ -0,0 +1,56 @@ +;Copies the memory image of CP/M loaded at E400h onto tracks 0 and 1 of the first CP/M disk +;Load and run from ROM monitor +;Uses calls to BIOS, in memory at FA00h +;Writes track 0, sectors 2 to 26, then track 1, sectors 1 to 25 + +#target bin +#code _HOME, 0x1400 + +_bios equ 0x7A00 +seldsk equ _bios+0x1b +settrk equ _bios+0x1e +setsec equ _bios+0x21 +setdma equ _bios+0x24 +write equ _bios+0x2a + +monitor_warm_start equ 0x0433 ;Return to ROM monitor + +main: + ld c,00h ;CP/M disk a + call seldsk + +;Write track 0, sectors 2 to 51 + ld a,1 ;starting sector + ld (sector),a + ld hl, 0x6400 ;start of CCP + ld (address),hl + ld c,0 ;CP/M track + call settrk + +wr_trk_0_loop: + ld a,(sector) + ld c,a ;CP/M sector + call setsec + ld bc,(address) ;memory location + call setdma + call write + ld a,(sector) + cp 50 ;done: + jp z,done ;yes + inc a ;no, next sector + ld (sector),a + ld hl,(address) + ld de,128 + add hl,de + ld (address),hl + jp wr_trk_0_loop + +done: + jp 0x0000 + +sector: + db 00h + +address: + dw 0000h + end diff --git a/src/ram_monitor.z80 b/src/ram_monitor.z80 new file mode 100644 index 0000000..2add648 --- /dev/null +++ b/src/ram_monitor.z80 @@ -0,0 +1,687 @@ +;RAM monitor for a system with serial interface and IDE disk and memory expansion board. +;This program to be loaded at DC00h by ROM monitor, and run from there. +;Assumes serial port has been initialized by ROM monitor. +;Assumes the UART data port address is 02h and control/status address is 03h +; +;The subroutines use these variables in RAM, same area as ROM monitor: +current_location: equ 0x1300 ;word variable in RAM +line_count: equ 0x1302 ;byte variable in RAM +byte_count: equ 0x1303 ;byte variable in RAM +value_pointer: equ 0x1304 ;word variable in RAM +current_value: equ 0x1306 ;word variable in RAM +buffer: equ 0x1308 ;buffer in RAM -- up to stack area +;Can use same stack as ROM monitor. Stack not re-initialized, listed here for information +;ROM_monitor_stack: equ 0x13ff ;upper TPA in RAM, below RAM monitor +; +; + org 01400h + out (1),a ;change memory configuration to all-RAM + jp monitor_start +; +;Puts a single char (byte value) on serial output +;Call with char to send in A register. Uses B register +write_char: ld b,a ;store char +write_char_loop: in a,(3) ;check if OK to send + and 001h ;check TxRDY bit + jp z,write_char_loop ;loop if not set + ld a,b ;get char back + out (2),a ;send to output + ret ;returns with char in a +; +;Subroutine to write a zero-terminated string to serial output +;Pass address of string in HL register +;No error checking +write_string: in a,(3) ;read status + and 001h ;check TxRDY bit + jp z,write_string ;loop if not set + ld a,(hl) ;get char from string + and a ;check if 0 + ret z ;yes, finished + out (2),a ;no, write char to output + inc hl ;next char in string + jp write_string ;start over +; +;Binary loader. Receive a binary file, place in memory. +;Address of load passed in HL, length of load (= file length) in BC +bload: in a,(3) ;get status + and 002h ;check RxRDY bit + jp z,bload ;not ready, loop + in a,(2) + ld (hl),a + inc hl + dec bc ;byte counter + ld a,b ;need to test BC this way because + or c ;dec rp instruction does not change flags + jp nz,bload + ret +; +;Binary dump to port. Send a stream of binary data from memory to serial output +;Address of dump passed in HL, length of dump in BC +bdump: in a,(3) ;get status + and 001h ;check TxRDY bit + jp z,bdump ;not ready, loop + ld a,(hl) + out (2),a + inc hl + dec bc + ld a,b ;need to test this way because + or c ;dec rp instruction does not change flags + jp nz,bdump + ret +; +;Subroutine to get a string from serial input, place in buffer. +;Buffer address passed in HL reg. +;Uses A,BC,DE,HL registers (including calls to other subroutines). +;Line entry ends by hitting return key. Return char not included in string (replaced by zero). +;Backspace editing OK. No error checking. +; +get_line: ld c,000h ;line position + ld a,h ;put original buffer address in de + ld d,a ;after this don't need to preserve hl + ld a,l ;subroutines called don't use de + ld e,a +get_line_next_char: in a,(3) ;get status + and 002h ;check RxRDY bit + jp z,get_line_next_char ;not ready, loop + in a,(2) ;get char + cp 00dh ;check if return + ret z ;yes, normal exit + cp 07fh ;check if backspace (VT102 keys) + jp z,get_line_backspace ;yes, jump to backspace routine + cp 008h ;check if backspace (ANSI keys) + jp z,get_line_backspace ;yes, jump to backspace + call write_char ;put char on screen + ld (de),a ;store char in buffer + inc de ;point to next space in buffer + inc c ;inc counter + ld a,000h + ld (de),a ;leaves a zero-terminated string in buffer + jp get_line_next_char +get_line_backspace: ld a,c ;check current position in line + cp 000h ;at beginning of line? + jp z,get_line_next_char ;yes, ignore backspace, get next char + dec de ;no, erase char from buffer + dec c ;back up one + ld a,000h ;put a zero in buffer where the last char was + ld (de),a + ld hl,erase_char_string ;ANSI sequence to delete one char from line + call write_string ;transmits sequence to backspace and erase char + jp get_line_next_char +; +;Creates a two-char hex string from the byte value passed in register A +;Location to place string passed in HL +;String is zero-terminated, stored in 3 locations starting at HL +;Also uses registers b,d, and e +byte_to_hex_string: ld b,a ;store original byte + srl a ;shift right 4 times, putting + srl a ;high nybble in low-nybble spot + srl a ;and zeros in high-nybble spot + srl a + ld d,000h ;prepare for 16-bit addition + ld e,a ;de contains offset + push hl ;temporarily store string target address + ld hl,hex_char_table ;use char table to get high-nybble character + add hl,de ;add offset to start of table + ld a,(hl) ;get char + pop hl ;get string target address + ld (hl),a ;store first char of string + inc hl ;point to next string target address + ld a,b ;get original byte back from reg b + and 00fh ;mask off high-nybble + ld e,a ;d still has 000h, now de has offset + push hl ;temp store string target address + ld hl,hex_char_table ;start of table + add hl,de ;add offset + ld a,(hl) ;get char + pop hl ;get string target address + ld (hl),a ;store second char of string + inc hl ;point to third location + ld a,000h ;zero to terminate string + ld (hl),a ;store the zero + ret ;done +; +;Converts a single ASCII hex char to a nybble value +;Pass char in reg A. Letter numerals must be upper case. +;Return nybble value in low-order reg A with zeros in high-order nybble if no error. +;Return 0ffh in reg A if error (char not a valid hex numeral). +;Also uses b, c, and hl registers. +hex_char_to_nybble: ld hl,hex_char_table + ld b,00fh ;no. of valid characters in table - 1. + ld c,000h ;will be nybble value +hex_to_nybble_loop: cp (hl) ;character match here? + jp z,hex_to_nybble_ok ;match found, exit + dec b ;no match, check if at end of table + jp m,hex_to_nybble_err ;table limit exceded, exit with error + inc c ;still inside table, continue search + inc hl + jp hex_to_nybble_loop +hex_to_nybble_ok: ld a,c ;put nybble value in a + ret +hex_to_nybble_err: ld a,0ffh ;error value + ret +; +;Converts a hex character pair to a byte value +;Called with location of high-order char in HL +;If no error carry flag clear, returns with byte value in register A, and +;HL pointing to next mem location after char pair. +;If error (non-hex char) carry flag set, HL pointing to invalid char +hex_to_byte: ld a,(hl) ;location of character pair + push hl ;store hl (hex_char_to_nybble uses it) + call hex_char_to_nybble + pop hl ;returns with nybble value in a reg, or 0ffh if error + cp 0ffh ;non-hex character? + jp z,hex_to_byte_err ;yes, exit with error + sla a ;no, move low order nybble to high side + sla a + sla a + sla a + ld d,a ;store high-nybble + inc hl ;get next character of the pair + ld a,(hl) + push hl ;store hl + call hex_char_to_nybble + pop hl + cp 0ffh ;non-hex character? + jp z,hex_to_byte_err ;yes, exit with error + or d ;no, combine with high-nybble + inc hl ;point to next memory location after char pair + scf + ccf ;no-error exit (carry = 0) + ret +hex_to_byte_err: scf ;error, carry flag set + ret +hex_char_table: defm "0123456789ABCDEF" ;ASCII hex table +; +;Subroutine to get a two-byte address from serial input. +;Returns with address value in HL +;Uses locations in RAM for buffer and variables +address_entry: ld hl,buffer ;location for entered string + call get_line ;returns with address string in buffer + ld hl,buffer ;location of stored address entry string + call hex_to_byte ;will get high-order byte first + jp c, address_entry_error ;if error, jump + ld (current_location+1),a ;store high-order byte, little-endian + ld hl,buffer+2 ;point to low-order hex char pair + call hex_to_byte ;get low-order byte + jp c, address_entry_error ;jump if error + ld (current_location),a ;store low-order byte in lower memory + ld hl,(current_location) ;put memory address in hl + ret +address_entry_error: ld hl,address_error_msg + call write_string + jp address_entry +; +;Subroutine to get a decimal string, return a word value +;Calls decimal_string_to_word subroutine +decimal_entry: ld hl,buffer + call get_line ;returns with DE pointing to terminating zero + ld hl,buffer + call decimal_string_to_word + ret nc ;no error, return with word in hl + ld hl,decimal_error_msg ;error, try again + call write_string + jp decimal_entry +; +;Subroutine to convert a decimal string to a word value +;Call with address of string in HL, pointer to end of string in DE +;Carry flag set if error (non-decimal char) +;Carry flag clear, word value in HL if no error. +decimal_string_to_word: ld b,d + ld c,e ;use BC as string pointer + ld (current_location),hl ;store addr. of start of buffer in RAM word variable + ld hl,000h ;starting value zero + ld (current_value),hl + ld hl,decimal_place_value ;pointer to values + ld (value_pointer),hl +decimal_next_char: dec bc ;next char in string (moving right to left) + ld hl,(current_location) ;check if at end of decimal string + scf ;get ready to subtract de from buffer addr. + ccf ;set carry to zero (clear) + sbc hl,bc ;keep going if bc > or = hl (buffer address) + jp c,decimal_continue ;borrow means bc > hl + jp z,decimal_continue ;z means bc = hl + ld hl,(current_value) ;return if de < buffer address (no borrow) + scf ;get value back from RAM variable + ccf + ret ;return with carry clear, value in hl +decimal_continue: ld a,(bc) ;next char in string (right to left) + sub 030h ;ASCII value of zero char + jp m,decimal_error ;error if char value less than 030h + cp 00ah ;error if byte value > or = 10 decimal + jp p,decimal_error ;a reg now has value of decimal numeral + ld hl,(value_pointer) ;get value to add an put in de + ld e,(hl) ;little-endian (low byte in low memory) + inc hl + ld d,(hl) + inc hl ;hl now points to next value + ld (value_pointer),hl + ld hl,(current_value) ;get back current value +decimal_add: dec a ;add loop to increase total value + jp m,decimal_add_done ;end of multiplication + add hl,de + jp decimal_add +decimal_add_done: ld (current_value),hl + jp decimal_next_char +decimal_error: scf + ret + jp decimal_add +decimal_place_value: defw 1,10,100,1000,10000 +; +;Memory dump +;Displays a 256-byte block of memory in 16-byte rows. +;Called with address of start of block in HL +memory_dump: ld (current_location),hl ;store address of block to be displayed + ld a,000h + ld (byte_count),a ;initialize byte count + ld (line_count),a ;initialize line count + jp dump_new_line +dump_next_byte: ld hl,(current_location) ;get byte address from storage, + ld a,(hl) ;get byte to be converted to string + inc hl ;increment address and + ld (current_location),hl ;store back + ld hl,buffer ;location to store string + call byte_to_hex_string ;convert + ld hl,buffer ;display string + call write_string + ld a,(byte_count) ;next byte + inc a + jp z,dump_done ;stop when 256 bytes displayed + ld (byte_count),a ;not finished yet, store + ld a,(line_count) ;end of line (16 characters)? + cp 00fh ;yes, start new line + jp z,dump_new_line + inc a ;no, increment line count + ld (line_count),a + ld a,020h ;print space + call write_char + jp dump_next_byte ;continue +dump_new_line: ld a,000h ;reset line count to zero + ld (line_count),a + call write_newline + ld hl,(current_location) ;location of start of line + ld a,h ;high byte of address + ld hl, buffer + call byte_to_hex_string ;convert + ld hl,buffer + call write_string ;write high byte + ld hl,(current_location) + ld a,l ;low byte of address + ld hl, buffer + call byte_to_hex_string ;convert + ld hl,buffer + call write_string ;write low byte + ld a,020h ;space + call write_char + jp dump_next_byte ;now write 16 bytes +dump_done: ld a,000h + ld hl,buffer + ld (hl),a ;clear buffer of last string + call write_newline + ret +; +;Memory load +;Loads RAM memory with bytes entered as hex characters +;Called with address to start loading in HL +;Displays entered data in 16-byte rows. +memory_load: ld (current_location),hl + ld hl,data_entry_msg + call write_string + jp load_new_line +load_next_char: call get_char + cp 00dh ;return? + jp z,load_done ;yes, quit + ld (buffer),a + call get_char + cp 00dh ;return? + jp z,load_done ;yes, quit + ld (buffer+1),a + ld hl,buffer + call hex_to_byte + jp c,load_data_entry_error ;non-hex character + ld hl,(current_location) ;get byte address from storage, + ld (hl),a ;store byte + inc hl ;increment address and + ld (current_location),hl ;store back + ld a,(buffer) + call write_char + ld a,(buffer+1) + call write_char + ld a,(line_count) ;end of line (16 characters)? + cp 00fh ;yes, start new line + jp z,load_new_line + inc a ;no, increment line count + ld (line_count),a + ld a,020h ;print space + call write_char + jp load_next_char ;continue +load_new_line: ld a,000h ;reset line count to zero + ld (line_count),a + call write_newline + jp load_next_char ;continue +load_data_entry_error: call write_newline + ld hl,data_error_msg + call write_string + ret +load_done: call write_newline + ret +; +;Get one ASCII character from the serial port. +;Returns with char in A reg. No error checking. +get_char: in a,(3) ;get status + and 002h ;check RxRDY bit + jp z,get_char ;not ready, loop + in a,(2) ;get char + ret +; +;Subroutine to start a new line +write_newline: ld a,00dh ;ASCII carriage return character + call write_char + ld a,00ah ;new line (line feed) character + call write_char + ret +; +;Subroutine to read one disk sector (128 bytes) +;Address to place data passed in HL +;LBA bits 0 to 7 passed in C, bits 8 to 15 passed in B +;LBA bits 16 to 23 passed in E +disk_read: +rd_status_loop_1: in a,(0fh) ;check status + and 80h ;check BSY bit + jp nz,rd_status_loop_1 ;loop until not busy +rd_status_loop_2: in a,(0fh) ;check status + and 40h ;check DRDY bit + jp z,rd_status_loop_2 ;loop until ready + ld a,01h ;number of sectors = 1 + out (0ah),a ;sector count register + ld a,c + out (0bh),a ;lba bits 0 - 7 + ld a,b + out (0ch),a ;lba bits 8 - 15 + ld a,e + out (0dh),a ;lba bits 16 - 23 + ld a,11100000b ;LBA mode, select drive 0 + out (0eh),a ;drive/head register + ld a,20h ;Read sector command + out (0fh),a +rd_wait_for_DRQ_set: in a,(0fh) ;read status + and 08h ;DRQ bit + jp z,rd_wait_for_DRQ_set ;loop until bit set +rd_wait_for_BSY_clear: in a,(0fh) + and 80h + jp nz,rd_wait_for_BSY_clear + in a,(0fh) ;clear INTRQ +read_loop: in a,(08h) ;get data + ld (hl),a + inc hl + in a,(0fh) ;check status + and 08h ;DRQ bit + jp nz,read_loop ;loop until cleared + ret +; +;Subroutine to write one disk sector (128 bytes) +;Address of data to write to disk passed in HL +;LBA bits 0 to 7 passed in C, bits 8 to 15 passed in B +;LBA bits 16 to 23 passed in E +disk_write: +wr_status_loop_1: in a,(0fh) ;check status + and 80h ;check BSY bit + jp nz,wr_status_loop_1 ;loop until not busy +wr_status_loop_2: in a,(0fh) ;check status + and 40h ;check DRDY bit + jp z,wr_status_loop_2 ;loop until ready + ld a,01h ;number of sectors = 1 + out (0ah),a ;sector count register + ld a,c + out (0bh),a ;lba bits 0 - 7 + ld a,b + out (0ch),a ;lba bits 8 - 15 + ld a,e + out (0dh),a ;lba bits 16 - 23 + ld a,11100000b ;LBA mode, select drive 0 + out (0eh),a ;drive/head register + ld a,30h ;Write sector command + out (0fh),a +wr_wait_for_DRQ_set: in a,(0fh) ;read status + and 08h ;DRQ bit + jp z,wr_wait_for_DRQ_set ;loop until bit set +write_loop: ld a,(hl) + out (08h),a ;write data + inc hl + in a,(0fh) ;read status + and 08h ;check DRQ bit + jp nz,write_loop ;write until bit cleared +wr_wait_for_BSY_clear: in a,(0fh) + and 80h + jp nz,wr_wait_for_BSY_clear + in a,(0fh) ;clear INTRQ + ret +; +;Strings used in subroutines +length_entry_string: defm "Enter length of file to load (decimal): ",0 +dump_entry_string: defm "Enter no. of bytes to dump (decimal): ",0 +LBA_entry_string: defm "Enter LBA (decimal, 0 to 65535): ",0 +erase_char_string: defm 008h,01bh,"[K",000h ;ANSI sequence for backspace, erase to end of line. +address_entry_msg: defm "Enter 4-digit hex address (use upper-case A through F): ",0 +address_error_msg: defm 13,10,"Error: invalid hex character, try again: ",0 +data_entry_msg: defm "Enter hex bytes, hit return when finished.",13,10,0 +data_error_msg: defm "Error: invalid hex byte.",13,10,0 +decimal_error_msg: defm 13,10,"Error: invalid decimal number, try again: ",0 +; +;Simple monitor program for CPUville Z80 computer with serial interface. + +monitor_start: call write_newline ;routine program return here to avoid re-initialization of port + ld a,03eh ;cursor symbol + call write_char + ld hl,buffer + call get_line ;get monitor input string (command) + call write_newline + call parse ;interprets command, returns with address to jump to in HL + jp (hl) +; +;Parses an input line stored in buffer for available commands as described in parse table. +;Returns with address of jump to action for the command in HL +parse: ld bc,parse_table ;bc is pointer to parse_table +parse_start: ld a,(bc) ;get pointer to match string from parse table + ld e,a + inc bc + ld a,(bc) + ld d,a ;de will is pointer to strings for matching + ld a,(de) ;get first char from match string + or 000h ;zero? + jp z,parser_exit ;yes, exit no_match + ld hl,buffer ;no, parse input string +match_loop: cp (hl) ;compare buffer char with match string char + jp nz,no_match ;no match, go to next match string + or 000h ;end of strings (zero)? + jp z,parser_exit ;yes, matching string found + inc de ;match so far, point to next char in match string + ld a,(de) ;get next character from match string + inc hl ;and point to next char in input string + jp match_loop ;check for match +no_match: inc bc ;skip over jump target to + inc bc + inc bc ;get address of next matching string + jp parse_start +parser_exit: inc bc ;skip to address of jump for match + ld a,(bc) + ld l,a + inc bc + ld a,(bc) + ld h,a ;returns with jump address in hl + ret +; +;Actions to be taken on match +; +;Memory dump program +;Input 4-digit hexadecimal address +;Calls memory_dump subroutine +dump_jump: ld hl,dump_message ;Display greeting + call write_string + ld hl,address_entry_msg ;get ready to get address + call write_string + call address_entry ;returns with address in HL + call write_newline + call memory_dump + jp monitor_start +; +;Hex loader, displays formatted input +load_jump: ld hl,load_message ;Display greeting + call write_string ;get address to load + ld hl,address_entry_msg ;get ready to get address + call write_string + call address_entry + call write_newline + call memory_load + jp monitor_start +; +;Jump and run do the same thing: get an address and jump to it. +run_jump: ld hl,run_message ;Display greeting + call write_string + ld hl,address_entry_msg ;get ready to get address + call write_string + call address_entry + jp (hl) +; +;Help and ? do the same thing, display the available commands +help_jump: ld hl,help_message + call write_string + ld bc,parse_table ;table with pointers to command strings +help_loop: ld a,(bc) ;displays the strings for matching commands, + ld l,a ;getting the string addresses from the + inc bc ;parse table + ld a,(bc) ;pass address of string to hl through a reg + ld h,a + ld a,(hl) ;hl now points to start of match string + or 000h ;exit if no_match string + jp z,help_done + push bc ;write_char uses b register + ld a,020h ;space char + call write_char + pop bc + call write_string ;writes match string + inc bc ;pass over jump address in table + inc bc + inc bc + jp help_loop +help_done: jp monitor_start +; +;Binary file load. Need both address to load and length of file +bload_jump: ld hl,bload_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,length_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld hl,bload_ready_message + call write_string + pop hl + call bload + jp monitor_start +; +;Binary memory dump. Need address of start of dump and no. bytes +bdump_jump: ld hl,bdump_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,dump_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld hl,bdump_ready_message + call write_string + call get_char + pop hl + call bdump + jp monitor_start +;Disk read. Need memory address to place data, LBA of sector to read +diskrd_jump: ld hl,diskrd_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,LBA_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld e,00h + pop hl + call disk_read + jp monitor_start +diskwr_jump: ld hl,diskwr_message + call write_string + ld hl,address_entry_msg + call write_string + call address_entry + call write_newline + push hl + ld hl,LBA_entry_string + call write_string + call decimal_entry + ld b,h + ld c,l + ld e,00h + pop hl + call disk_write + jp monitor_start +boot_jump: ld hl,1100h + ld bc,0000h + ld e,00h + call disk_read + out (0),a ;CP/M loader uses ROM routine to read disk + jp 1100h +;Prints message for no match to entered command +no_match_jump: ld hl,no_match_message + call write_string + ld hl, buffer + call write_string + jp monitor_start +; +;Monitor data structures: +; +monitor_message: defm 13,10,"ROM ver. 8",13,10,0 +no_match_message: defm "? ",0 +help_message: defm "Commands implemented:",13,10,0 +dump_message: defm "Displays a 256-byte block of memory.",13,10,0 +load_message: defm "Enter hex bytes starting at memory location.",13,10,0 +run_message: defm "Will jump to (execute) program at address entered.",13,10,0 +bload_message: defm "Loads a binary file into memory.",13,10,0 +bload_ready_message: defm 13,10,"Ready to receive, start transfer.",0 +bdump_message: defm "Dumps binary data from memory to serial port.",13,10,0 +bdump_ready_message: defm 13,10,"Ready to send, hit any key to start.",0 +diskrd_message: defm "Reads one sector from disk to memory.",13,10,0 +diskwr_message: defm "Writes one sector from memory to disk.",13,10,0 +;Strings for matching: +dump_string: defm "dump",0 +load_string: defm "load",0 +jump_string: defm "jump",0 +run_string: defm "run",0 +question_string: defm "?",0 +help_string: defm "help",0 +bload_string: defm "bload",0 +bdump_string: defm "bdump",0 +diskrd_string: defm "diskrd",0 +diskwr_string: defm "diskwr",0 +boot_string: defm "boot",0 +no_match_string: defm 0,0 +;Table for matching strings to jumps +parse_table: defw dump_string,dump_jump,load_string,load_jump + defw jump_string,run_jump,run_string,run_jump + defw question_string,help_jump,help_string,help_jump + defw bload_string,bload_jump,bdump_string,bdump_jump + defw diskrd_string,diskrd_jump,diskwr_string,diskwr_jump + defw boot_string,boot_jump + defw no_match_string,no_match_jump + diff --git a/src/rom_monitor.z80 b/src/rom_monitor.z80 index 056ac9e..a367ef0 100644 --- a/src/rom_monitor.z80 +++ b/src/rom_monitor.z80 @@ -394,36 +394,22 @@ write_newline: ld a,00dh ;ASCII carriage return character call write_char ret ; -;Subroutine to read one disk sector (256 bytes) +;Subroutine to read one disk sector (128 bytes) ;Address to place data passed in HL ;LBA bits 0 to 7 passed in C, bits 8 to 15 passed in B ;LBA bits 16 to 23 passed in E disk_read: -rd_status_loop_1: in a,(0fh) ;check status - and 80h ;check BSY bit - jp nz,rd_status_loop_1 ;loop until not busy -rd_status_loop_2: in a,(0fh) ;check status - and 40h ;check DRDY bit - jp z,rd_status_loop_2 ;loop until ready - ld a,01h ;number of sectors = 1 - out (0ah),a ;sector count register ld a,c out (0bh),a ;lba bits 0 - 7 ld a,b out (0ch),a ;lba bits 8 - 15 ld a,e out (0dh),a ;lba bits 16 - 23 - ld a,11100000b ;LBA mode, select drive 0 - out (0eh),a ;drive/head register ld a,20h ;Read sector command out (0fh),a rd_wait_for_DRQ_set: in a,(0fh) ;read status and 08h ;DRQ bit jp z,rd_wait_for_DRQ_set ;loop until bit set -rd_wait_for_BSY_clear: in a,(0fh) - and 80h - jp nz,rd_wait_for_BSY_clear - in a,(0fh) ;clear INTRQ read_loop: in a,(08h) ;get data ld (hl),a inc hl @@ -432,27 +418,17 @@ read_loop: in a,(08h) ;get data jp nz,read_loop ;loop until cleared ret ; -;Subroutine to write one disk sector (256 bytes) +;Subroutine to write one disk sector (128 bytes) ;Address of data to write to disk passed in HL ;LBA bits 0 to 7 passed in C, bits 8 to 15 passed in B ;LBA bits 16 to 23 passed in E disk_write: -wr_status_loop_1: in a,(0fh) ;check status - and 80h ;check BSY bit - jp nz,wr_status_loop_1 ;loop until not busy -wr_status_loop_2: in a,(0fh) ;check status - and 40h ;check DRDY bit - jp z,wr_status_loop_2 ;loop until ready - ld a,01h ;number of sectors = 1 - out (0ah),a ;sector count register ld a,c out (0bh),a ;lba bits 0 - 7 ld a,b out (0ch),a ;lba bits 8 - 15 ld a,e out (0dh),a ;lba bits 16 - 23 - ld a,11100000b ;LBA mode, select drive 0 - out (0eh),a ;drive/head register ld a,30h ;Write sector command out (0fh),a wr_wait_for_DRQ_set: in a,(0fh) ;read status @@ -464,18 +440,13 @@ write_loop: ld a,(hl) in a,(0fh) ;read status and 08h ;check DRQ bit jp nz,write_loop ;write until bit cleared -wr_wait_for_BSY_clear: in a,(0fh) - and 80h - jp nz,wr_wait_for_BSY_clear - in a,(0fh) ;clear INTRQ ret ; ;Strings used in subroutines length_entry_string: defm "Enter length of file to load (decimal): ",0 dump_entry_string: defm "Enter no. of bytes to dump (decimal): ",0 LBA_entry_string: defm "Enter LBA (decimal, 0 to 65535): ",0 -;; erase_char_string: defm 008h,01bh,"[K",000h ;ANSI sequence for backspace, erase to end of line. -erase_char_string: defm 008h +erase_char_string: defm 008h,01bh,"[K",000h ;ANSI sequence for backspace, erase to end of line. address_entry_msg: defm "Enter 4-digit hex address (use upper-case A through F): ",0 address_error_msg: defm 13,10,"Error: invalid hex character, try again: ",0 data_entry_msg: defm "Enter hex bytes, hit return when finished.",13,10,0 @@ -654,11 +625,11 @@ diskwr_jump: ld hl,diskwr_message pop hl call disk_write jp monitor_warm_start -cpm_jump: ld hl,0800h +boot_jump: ld hl,1100h ld bc,0000h ld e,00h call disk_read - jp 0800h + jp 1100h ;Prints message for no match to entered command no_match_jump: ld hl,no_match_message call write_string @@ -668,7 +639,7 @@ no_match_jump: ld hl,no_match_message ; ;Monitor data structures: ; -monitor_message: defm 13,10,"ROM ver. 8",13,10,0 +monitor_message: defm 13,10,"ROM Ver. 9",13,10,0 no_match_message: defm "? ",0 help_message: defm "Commands implemented:",13,10,0 dump_message: defm "Displays a 256-byte block of memory.",13,10,0 @@ -691,7 +662,7 @@ bload_string: defm "bload",0 bdump_string: defm "bdump",0 diskrd_string: defm "diskrd",0 diskwr_string: defm "diskwr",0 -cpm_string: defm "cpm",0 +boot_string: defm "boot",0 no_match_string: defm 0,0 ;Table for matching strings to jumps parse_table: defw dump_string,dump_jump,load_string,load_jump @@ -699,6 +670,6 @@ parse_table: defw dump_string,dump_jump,load_string,load_jump defw question_string,help_jump,help_string,help_jump defw bload_string,bload_jump,bdump_string,bdump_jump defw diskrd_string,diskrd_jump,diskwr_string,diskwr_jump - defw cpm_string,cpm_jump + defw boot_string,boot_jump defw no_match_string,no_match_jump diff --git a/upload.py b/upload.py index e3defa2..8f7dae6 100755 --- a/upload.py +++ b/upload.py @@ -2,6 +2,7 @@ import serial import os import sys +import time def progressbar(it, prefix="", size=60, file=sys.stdout): count = len(it) @@ -19,26 +20,29 @@ def progressbar(it, prefix="", size=60, file=sys.stdout): def main(): if len(sys.argv) == 2: # ser = serial.Serial("COM3", timeout=1, write_timeout=1) - ser = serial.Serial("/dev/ttyACM0", timeout=1, write_timeout=1) + ser = serial.Serial("/dev/ttyUSB0", timeout=1, write_timeout=1, baudrate=115200) if ser.is_open: # Clear out any existing input ser.write(b'\n') - ser.readline() + time.sleep(0.002) # Send the upload command ser.write(b'#u\n') - print(ser.readline()) + time.sleep(0.002) path = sys.argv[1] size = os.path.getsize(path) ser.write([size & 0xFF]) + time.sleep(0.002) ser.write([(size >> 8) & 0xFF]) - i = 0 + time.sleep(0.002) + with open(path, "rb") as f: for i in progressbar(range(size), "Upload: ", 40): - ser.write(f.read(1)) + byte = f.read(1) + ser.write(byte) + time.sleep(0.002) - print(ser.readline()) ser.close() else: diff --git a/upload_serial.py b/upload_serial.py new file mode 100755 index 0000000..44e95cc --- /dev/null +++ b/upload_serial.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +import serial +import os +import sys +import time + +def progressbar(it, prefix="", size=60, file=sys.stdout): + count = len(it) + def show(j): + x = int(size*j/count) + file.write("%s[%s%s] %i/%i\r" % (prefix, "#"*x, "."*(size-x), j, count)) + file.flush() + show(0) + for i, item in enumerate(it): + yield item + show(i+1) + file.write("\n") + file.flush() + +def main(): + if len(sys.argv) == 2: + # ser = serial.Serial("COM3", timeout=1, write_timeout=1) + ser = serial.Serial("/dev/ttyUSB0", timeout=1, write_timeout=1, baudrate=115200) + if ser.is_open: + path = sys.argv[1] + size = os.path.getsize(path) + + with open(path, "rb") as f: + for i in progressbar(range(size), "Upload: ", 40): + byte = f.read(1) + ser.write(byte) + + if byte == b'#': + time.sleep(0.002) + ser.write(b'#') + time.sleep(0.002) + ser.write(b'\n') + + time.sleep(0.002) + + ser.close() + + else: + print("Failed to open serial port") + else: + print("Please provide file to upload") + +if "__main__": + main()