
**********************************************************************
files: Plus3.TXT, Plus3.TAP
       CAT2String.BAS, DOS_FC.BAS
       http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_demo.zip
       http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_fc.zip
**********************************************************************


** +3 CAT to string
   ================
CAT2String
This small BASIC routine will put a +3 CAT into s$().

CAT2String.BAS [code]
   2 DEF FN p(a)=PEEK a+256*PEEK (a+1)
  20 LET k1=0: LET k2=0: FOR a=0 TO 0: NEXT a
  21 DIM s$(200): POKE FN p(23631)+5,129: POKE FN p(23631)+6,15:
     LET k1=FN p(23643): CAT : LET k2=FN p(23643):
     POKE FN p(23631)+5,244: POKE FN p(23631)+6,9:
     FOR a=k1 TO k2-1: LET s$(a-k1+1)=CHR$ PEEK a: NEXT a:
     PRINT s$(TO a-k1)
  22 PAUSE 0: RANDOMIZE USR 5808: GO TO 21
[/code]

It uses channel 'R' (or 'X' as it is on the +3) to redirect stream #2 to WORKSP from where it can be retrieved. The string must be DIMed in advance to accommodate the longest CAT expected so that VARS doesn't expand during the loop, otherwise WORKSP is always just out of reach.  System variable 23631 is CHANS. System variable 23643 is K_CUR, which gets incremented as each CAT byte is copied to WORKSP.

Any variables used in the CAT loop must be declared earlier in the program, so that they don't need to be created during the first time through the loop. In this case what's needed is an initialisation line of:
	LET k1=0: LET k2=0: FOR a=0 TO 0: NEXT a
To tidy up afterwards, call $16B0 SET_MIN. Disks can be swapped at the PAUSE to check it out with different inputs. The length of s$ should be set to about 20 * max.files + 10.


**********************************************************************


** +3 BASIC DOS_Demo
   =================
DOS_Demo
This +3 DOS utility  provides most of the disk functions that might be needed but which don't have BASIC commands for them.
[center][img]http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_dems.gif[/img][/center]
A READ check is included to prevent critical areas from being overwritten. The [url=http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_demo.zip]DOS_Demo[/url] ZIP file has the program in Z80 & TAP form plus annotated BAS & ASM listings.

   =================

I've finished with this for now, which provides something to demonstrate the use of most of the DOS functions listed on page 210 of the +3 manual. The ZIP file above has been updated. I've added the memory map option, which is called automatically prior to a READ and can also be selected independently from the menu. It breaks down the reserved areas by page and address range for these areas:
     REM 1: BASIC program code & data
     REM 2: Machine & GO SUB stacks
     REM 3: Assembler program code & data
     REM 4: DOS/RAM buffer extents
     REM 5: DOS workspace in page 7
If there's a conflict with a READ address then any affected range is highlighted and the READ abandoned.

[center][img]http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_dems.gif[/img]  [img]http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_demm.gif[/img][/center]

I haven't bothered about protecting the Editor variables in page 7 as I considered them less important. I suppose that the Editor might crash on returning to BASIC if its workspace was overwritten - it's not all that reliable at the best of times - but I couldn't see anything particularly critical in the +3 ROM 2 disassembly. I'll have to experiment at some time.

The memory map uses the function GO SUB FN g(q,r,p,a,z,g), where q,r= overall address range, p=page, a,z=page address range, g=GOSUB line. The parameters passed on each call vary dependent upon whether the address range is known in advance or has to be calculated, and whether the address range spans more than one page. Pages 5 and 2 have double entries because of their alternative mappings.

   =================

I've added an alternative version to [url=http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_demo.zip]DOS_Demo[/url] called DOS_DeFn which allows +3 DOS functions to be called from BASIC using a function of the form:

[center][b]GO SUB FN a(a$,f$,b,c,d,e)[/b][/center]

For example:
[list=a]
[*][b]GO SUB FN a("O","DEMO",3,1,0,1)[/b]
will open file "DEMO" for reading,
[*][b]GO SUB FN a("s","DEMO",3,0,6,128)[/b]
will set the file pointer to 1536 (start point is 128),
[*][b]GO SUB FN a("R","=200",3,0,195,0)[/b]
will read 200 bytes into page 0 starting at 49920,
[*][b]GO SUB FN a("C","DEMO",3,0,0,0)[/b]
closes the file.
[/list]

Here's a list of the available functions and their required parameters; plus there's three extensively commented listings included in the ZIP file.
[code]
REM a$=action:      parameters:
     A=ABANDON      b=file#
     C=CLOSE        b=file#
     D=SETDRIVE     f$=drive A-P, or Q=get default in f$
     F=FREESPACE    f$=drive A-P, returns HL
     G=GET1346      n/a, returns b,c,d,e as for SET1346
     H=REFHEAD      b=file#, returns IX and h(8)
     I=INITIALISE   n/a
     M=MEMORY CHECK b=line#, c=page, returns m(16,3) : page,start,end; fl reset on error
     O=OPEN         b=file#, f$=filename, c=access mode, d=create action, e=open action
     R=READ         b=file#, c=page, de=address,
                    f$=string of length to read into f$,
                    or "=numeric_expression" length to read to address
     S=SET1346      b=1st DOS buffer, c=qty DOS buffers,
                    d=1st RAM buffer, e=qty RAM buffers
     U=SETUSER      f$=user A-P, or Q=get default in f$
     V=VERSION      n/a, returns d.e
     W=WRITE        b=file#, c=page, de=address,
                    f$=string of data to write from f$,
                    or "=numeric_expression" length to write from address
     a=SETACCESS    b=file#, c=access mode
     e=GETEOF       b=file#, returns cde
     f=FLUSH        f$=drive A-P
     g=GETPOSITION  b=file#, returns cde
     s=SETPOSITION  b=file#, cde=pointer
[/code]

   =================

I've updated [url=http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_demo.zip]DOS_Demo.ZIP[/url] with a few mostly minor changes. The only significant one is this, which returns updated values for the FN (a) parameters to maintain consistency with the program notes. I found that I had to reset DEFADD before returning and so the FN (a) parameters weren't visible any longer. Lines 921-923 copy b,c,d,e & f$ into the main variables area so that they remain visible.

[code]
 920 RESTORE 902: GO SUB 915:
     FOR o=k1 TO LEN o$: GOSUB kM+(100 AND a$=o$(o) AND LEN f$): NEXT o
 921 FOR h=kC-k3 TO kC: LET h(h)=VAL VAL$ (""""+CHR$ (h+57)+""""): NEXT h:
     LET g$=f$
 922 RESTORE FN c()+k1: POKE dh,k0: REM disable DEFADD redirection
 923 FOR h=kC-k3 TO kC: POKE FN p(23639)+55,h+57: LET b=h(h): NEXT h:
     LET f$=g$: RETURN
[/code]

   =================

Small Correction

The assignments of c,d,e in line 4151 have been replaced with this:

[code]
     LET lv=23406:
     POKE lv,INT(cde/k64k): POKE lv+k1,INT(de/kFG): POKE lv+k2,FN l(de,kFG):
     LET c=PEEK lv: LET d=PEEK (lv+k1): LET e=PEEK (lv+k2):
[/code]

Although +3DOS can only read up to $ffff bytes at a time, it supports file sizes up to $ffffff bytes. DOS_Demo expects all number parameters to be in 2-byte smallint form, but for file pointer values over $ffff the value will be in 5-byte FP form. This change just forces the three bytes of the file pointer to be passed as smallints; it only affects function "s" SET_POSITION. On the +3 bytes 23406/7/8 are part of LASTV "last value printed by calculator" and so are free for use as temporary storage.


**********************************************************************


** File comparator
   ===============
FC
If you've got a version of Windows - such as XP - which provides a DOS Command Prompt option, then you can use FC for text or binary file comparisons. Type "HELP FC" at the prompt for the command line options.

   ===============

An alternative is HxD by Mael Horz, an excellent free hex editor program for Windows which has a file comparison option which shows the two file hex dumps one above the other and highlights each difference in turn. It does a byte comparison, so it would be a bit tedious if there were a lot of differences, or if one file was slightly offset from the other (ie. where the files are mostly the same, but one has an extra chunk near the start), but that feature is very handy for tracking down minor discrepancies. I use the comparison feature mostly for tracking down the alterations in hacked versions of games.

   ===============

If block1 starts at "first" and block2 starts at "last+1" then a BASIC comparison routine could be just this one-liner:

[code]
  10 LET b=last-first+1:
     FOR a=first TO last:
       POKE 23618,10: POKE 23620,5+NOT (PEEK a-PEEK (a+b)):
       PRINT a;" ";PEEK a,a+b;" ";PEEK (a+b):
     NEXT a
[/code]

Each pass jumps to statement 5 (PRINT) or 6 (NEXT) depending upon whether corresponding bytes are different or not.

   ===============

Here's a more advanced version of my earlier routine. It reads the blocks from +3 disk 32 bytes at a time so it's not restricted by what can fit into memory. This is the controlling routine; the external GOSUBs refer to routines in my [url=http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_demo.zip]+3DOS_Demo[/url] library. It's too slow in BASIC, but it gives an example of the type of structure which might be used for a simple byte comparison; comparing narrative text files efficiently is a whole lot more complicated. The full listing is in the [url=http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_fc.zip]DOS_FC[/url] archive; the assembler listing for the DOS routines is in the other one.
[center][img]http://www.users.globalnet.co.uk/~jg27paw4/pourri/dos_fc.gif[/img][/center]
Each pair of 32 byte blocks is printed one on top of the other if different, or skipped if not; only mis-matched bytes are printed. Printable characters are printed plain; non-printable are printed as a compressed hex byte. In the example picture the end of the shorter file has already been reached, so only the longer file is printing.

DOS_FC.BAS [code]
   2 LET DOS=32000+11: CLEAR DOS-12: GO SUB 900:
     LOAD "miniset-LH"CODE VAL "32512+256":
     LOAD "miniset-RH"CODE VAL "33280+256":
     CLS : DIM r$(k2,kC): DIM r(k2,k5):
     INPUT "file#1",r$(k1)'"file#2",r$(k2): LET off=k0
   3 FOR i=k1 TO k2:
       PRINT "Opening ";r$(i): GO SUB FN a("O",r$(i),i+k2,k1,k0,k1):
       GO SUB FN a("g","",i+k2,k0,k0,k0): LET r(i,k1)=cde: REM get SOF
       GO SUB FN a("e","",i+k2,k0,k0,k0): LET r(i,k2)=cde: REM get EOF
       LET r(i,k3)=r(i,k2)-r(i,k1): LET r(i,k4)=134+i-k1:  REM size : load point
       LET r(i,k5)=k32-(k32-r(i,k3) AND r(i,k3) < k32):    REM block length
     NEXT i
  11 FOR i=k1 TO k2
  12   LET r(i,k5)=k32-(k32-r(i,k3) AND r(i,k3) < k32):
       IF r(i,k5) THEN
         GO SUB FN a("R","=r(i,k5)",i+k2,k0,r(i,k4),k0):   REM read a block
         LET r(i,k3)=r(i,k3)-r(i,k5): REM deduct from size
  13 NEXT i
  14 LET dif=k0: REM check if the blocks differ
  15 FOR j=k1 TO k32: FOR i=k1 TO k2: REM pad a short block with spaces
  16     IF j > r(i,k5) THEN POKE r(i,k4)*kFG+j-k1,k32:
  17    NEXT i:
        LET dif=dif+(PEEK (34304+j-k1)<>PEEK (34560+j-k1)):
     NEXT j
  18 PRINT off;AT 24-PEEK 23689,k0;:
     IF NOT dif THEN GO TO 20: REM no diff = no print
  19 PRINT : LET set=+kFG: REM print a block from each file
     FOR j=34304 TO 34560 STEP kFG: FOR i=k1 TO k32:
         GO SUB 30:
       NEXT i: LET set=-set: BRIGHT k1:
     NEXT j: BRIGHT k0
  20 LET off=off+k32: REM loop back if either file is not finished
     IF r(k1,k5) OR r(k2,k5) THEN GO TO kB
  28 FOR i=k1 TO k2:
       PRINT "Closing ";r$(i): GO SUB FN a("C","",i+k2,k0,k0,k0):
     NEXT i
  29 GO TO 5260
  30 LET ch1=PEEK (j+i-k1): LET ch2=PEEK (j+i-k1+set): REM print a block from each file
     IF ch1=ch2 THEN PRINT " ";: RETURN: REM if pair matches print space
  31 IF ch1>=k32 AND ch1<=k7F THEN PRINT CHR$ ch1;: RETURN: REM if printable char, print
  32 LET ch2=FN l(ch1,kG): LET ch1=(ch1-ch2)/kG: REM if non-printable char, print hex byte
     POKE 23607,127: PRINT CHR$ FN m(ch1);:      REM using compressed font
     POKE 23607,130: PRINT OVER k1;CHR$ k8;CHR$ FN m(ch2);:
     POKE 23607,60:
     RETURN : DEF FN m(v)=v+48+(k7 AND v > k9)
[/code]


**********************************************************************


** 128k+3 RAM paging from BASIC
   ============================

I'd always thought that OUT had to be used to page RAM, which from BASIC would be an "OUT 32765,ddd" command. Then I was browsing through the +3 manual and noticed that it says that doing a "CLEAR 49151" (ie. to just below $C000) and then "POKE 23388,16+n" (being the BANK_M system variable) will switch in page 'n'. That means that an "OUT 32765,16+n" would do the same thing, although it's rather redundant as the POKE is still required. This is because the +3's maskable interrupt routine restores whatever page is recorded in BANK_M before returning, so only the POKE is needed to switch pages when it's running. Take care that the BASIC's machine stack isn't in the outgoing page.


**********************************************************************


** +3e Streams & Channels
   ======================

The example below will read a whole file into a string in one go; it seems quick enough. It is essential that the string channel is opened before the file channel, otherwise it won't work properly.

[code]
30 DIM f$(128)
40 OPEN #5,"v>f$"
45 OPEN #4,"test.txt"
50 FOR a=1 to 128: PRINT #5; INKEY$ #4;: NEXT a
60 PRINT f$
[/code]

   ===========================

I set up a test file with 99 catalogue entries, totalling about 3.5k, and using the PRINT #5; INKEY$ #4 method it took about 10 seconds to copy the file to the string, which seems OK. The string must be defined before the string channel is opened, and the string channel must be opened before the file channel, otherwise the PRINT ... INKEY$ will give an error.

If the files are set up with fixed length fields & records then the only thing to check for is EOF. If DIM #4 is used to get the file size then there's no need to waste time checking the input, as the loop end value can be set = INT (file size / record length) * record length.

   ===========================

It's explained at the [url=http://www.worldofspectrum.org/zxplus3e/]The ZX Spectrum +3e Homepage[/url], specifically the [url=http://www.worldofspectrum.org/zxplus3e/channels.html]+3e Extended Channels[/url] page, "Variable Channels" section.


**********************************************************************


** RAM used by +3DOS
   =================

The +3 ROM 2 disassembly defines the page 7 usage in the "sysvarp7.def" section. The cache buffer code starts at $1530 in that file and has plenty of comments to explain what it's doing. The +2A/+3 ROM disassemblies are available here:

http://www.fruitcake.plus.com/Sinclair/Spectrum128/ROMDisassembly/Spectrum128ROMDisassembly4.htm


**********************************************************************


** Running the +3 Self-Test Program without the Magic Word
   =======================================================

The +3 Self-Test Program runs from the test screen after pressing "QAZPLM" on a real +3, but it's not possible to get all six keys to register at the same time on an emulator. Running this short piece of code will do it.

[code]
        di
        ld   bc,$1ffd
        ld   a,($5b67)           ;BANK678
        and  %11111011
        ld   ($5b67),a
        out  (c),a
        ld   bc,$7ffd
        ld   a,($5b5c)           ;BANKM
        and  %11101111
        ld   ($5b5c),a
        out  (c),a
        ld   hl,$0351+$14
        ld      c,(hl)          ; get address of routine to execute
        inc     hl
        ld      b,(hl)
        push    bc              ; stack address
        ret                     ; and "return" to it
[/code]

Quite a few of the screens can be completed without "connecting the loopback connector" or having "factory test equipment", but to get through all of them it's necessary to have the +3 test disk to put in.


**********************************************************************


** +3e Load & Run from Assembler
   =============================

This routine, when run from the command line on a +3/+3e, will load & run a BASIC program saved with an auto-LINE number and return to BASIC afterwards (ie. it's just the same as typing LOAD "DISK"). The disassemblies available at the [url=http://www.fruitcake.plus.com]ZX Resource Centre[/url] come in very handy for this sort of thing.

[code]        org  32000
T_ADDR: equ  23668
CH_ADD: equ  23645
RETADDR:equ  23386
BANK678:equ  23399
        ld   bc,7            ;make room for filename in WORKSP
        rst  $30
        ld   hl,NAME
        ld   bc,7
        push de
        ldir
        ld   a,$84           ;parameter offset for LOAD
        ld   (T_ADDR),a
        pop  de              ;address of quoted file name in WORKSP
        ld   (CH_ADD),de
        set  7,(iy+01)       ;set runtime mode
        di
        ld   a,(BANK678)
        res  2,a
        ld   (BANK678),a
        ld   bc,$1ffd
        out  (c),a           ;switch to ROM.1
        ei
        call $0889
        di
        ld   a,(BANK678)
        set  2,a
        ld   (BANK678),a
        ld   bc,$1ffd
        out  (c),a           ;switch to ROM.3
        ei
        ld   de,$10e9
        ld   (RETADDR),de
        ret
NAME:   defm '"DISK"'
        defb $0d[/code]

   =============================

I used my DOS_Demo program set to auto-run to test it. It loads & runs DOS_Demo, then returns to BASIC (ie. the calling program) when I quit the program (although I'm not absolutely certain as to why, because I'm not restoring CH_ADD)). Anyhoo, here's a very simple test:

[code]
  10 LET a$="hello world": PRINT a$: STOP
     CLEAR
     SAVE "DISK" LINE 10
     NEW
     RANDOMIZE USR 33000 : REM location of loader routine
[/code]

This should print "hello world".

The system variable LODDRV (PEEK 23417) should be set to the letter of the drive containing the program to be loaded, unless that is already included in NAME, in which case the LD BC at the start should be adjusted accordingly. BC should equal (length of name + 2 for "" + 1 for ENTER).

The NAME string must be quoted in the DEFM and end with $0d as the LOAD routine uses SCANNING to evaluate the expression. For handling multiple programs the routine would have to be passed a pointer to the filename and then CPIR for the $0d at the end and LD BC accordingly. At the end it should probably call either $16BF SET_WORK or $19E8 RECLAIM_2 to reclaim the space used by the name, otherwise it will get bigger each time around.


**********************************************************************


** Converting commercial disk games to use standard BASIC loaders
   ==============================================================

Commercial protected disks will usually use a bootstrap loader via DOS_BOOT in ROM2:$1ace. This uses one of the special RAM configurations RAM 4,7,6,3 with the game's boot routine loaded at $fe10 in page 3. This will usually switch back to a normal configuration fairly quickly to read the CODE blocks for the game directly from the disk (ie. bypassing +3DOS). Then it's a matter of finding where the read routine is in the code; there will be some precursor point where registers will have been set up indicating the location & size of the block to read, which will be called a number of times. So I put a break in there and when it returns I save that block from RAM to a BIN file. When I've got them all I save them as normal CODE files and then disassemble the loader blocks to check if anything needs to be done in between. Then I construct a BASIC loader based on that. Here's my notes from [url=http://www.users.globalnet.co.uk/~jg27paw4/pourri/ownotes.txt]Operation Wolf[/url] to give a (probably confusing) idea of what's involved. The lines starting "SAVE..." are where I'm saving the BIN files. The assembler fragments at the end are my notes when I was working out where to break in the code.

Sometimes there may be more than one read subroutine. With one conversion I did (I think it was Doom) I found that I was missing some chunks of code, which was because some blocks were being read via a second subroutine which I'd missed, as I'd presumed that the first routine I found was the only one.

This might be of interest as well, also from Op.Wolf. As you can see, although there were numerous code blocks on the original disk, the new BASIC loader resolved into something quite simple.

[code]
1000 CLEAR 25999:
     LET j=NOT PI: LET k=SGN PI: LET l=k+k: LET m=INT PI: LET u=26000:
     LET a=m*m+k: LET b=a+k: LET c=b+k: LET d=c+k: LET e=d+k: LET f=e+k: LET g=f+k
1100 FOR x=k TO f: READ d$: LET l$=CHR$ (VAL d$(k)*g+VAL d$(l)): PRINT l$;":p.";d$(m+k);" a=";:
       FOR y=l TO m:
         READ d$: LET v=j: FOR z=j TO m: LET v=v+VAL d$(m+k-z)*g^z: NEXT z:
         PRINT v;"> 26000" AND y=m,"s=" AND y=l;: NEXT y
1200   INPUT d$: IF x<f THEN SAVE l$ CODE u,v
1201 NEXT x
1300 FOR x=k TO m: READ l$,v: SAVE l$ CODE u,v: LET u=u+v: NEXT x: STOP
     REM The last 256 bytes (all zeroes) of file "e" are not saved so as not to overwrite the 128k system variables
     REM after loading the SCREEN$; they are copied in later.
2000 DATA "6103","c000","4000","6202","8000","0200","6302","8500","0200",
"6403","fed3","012d","6505","4000","1b00","6802","8000","4000","6903",
"c000","3000","6a00","c000","4000","6b01","c000","4000","6c04","c000",
"4000","6e06","c000","4000","6f05","4000","1000","6605","5cd8","0800",
"6705","632f","1cd1","6d07","c000","4000"
2001 DATA "x",6912,"y",3328,"z",6144

****

REM      7:$c000<-->$c05f XXXX (  96 bytes) used by +3 BASIC }shadow
REM      7:$c060<-->$daff free (6816 bytes)                  }screen
REM      7:$db00<-->$e7ff XXXX (3328 bytes) used by +3 DOS
REM      7:$fe00<-->$ffff used to load bootsector by +3 DOS ROM; otherwise:
REM      7:$e800<-->$ffff free unused by +3 DOS after boot (6144 bytes, 6kb)

****

   1 REM Operation Wolf loader
  10 CLEAR 32767: LOAD "e" SCREEN$: LOAD "h" CODE 32768:
     LOAD "ow" CODE 38672: RANDOMIZE USR 38672
[/code]


**********************************************************************
