View Full Version : CP/M Driver patching continued...

October 7th, 2016, 07:43 AM
Following on from this thread: http://www.vcfed.org/forum/showthread.php?48430-CP-M-driver-patching ...

Some background: The LoTech CP/M driver was written by Firebox for the Model IV Montezuma CP/M implementation. It leverages the ramdisk driver space (actually supplants it from what I can see) and this may explain its entry code, which calls read or write based on the value in the C register. As a result, it doesn't appear to be playing friendly with the CP/M rule book, which says you provide a DPB and a set of diskl access routines and CP/M does the rest. I think it is like this because it's copying the ramdisk interface, but I am not sure.

Looking around the Lifeboat CP/M memory map gives me various interesting pieces of information. My approach to porting the existing LoTech CP/M driver is starting to look like this:

- Redirect the BIOS jump table SELDISK routine (patch it) to a new routine in upper memory. Functional description: If drive < 4 then call stock SELDISK, else call HDD SELDISK (this returns the DPB for the hard drive). Note, the Lifeboat BIOS hardcodes this check, and returns an error if you try to select a drive that is higher than D:; moreover, it doesn't have any drive configuration options other than "single disk" or "multiple disk", the effect of which is to turn on / off prompting you to swap disks. Interestingly though, it does automatically detect the drive's format (Single, Double or Extended density) and I have found the DPB for each of these.
- Should be able to leave SETDMA as we are going to use our own 256 byte sector size buffer
- Patch SELSEC with new routine: if drive < 3 then call stock SELSEC, else call HDD SELSEC
- Patch READDSK and WRITEDSK routines as above.
- Not sure about SETTRACK. The existing code has a sector select which also sets cylinder and head by it by successively dividing the logical sector down. The CF interface requires these, so perhaps I should provide SETTRACK (but I'm not sure where to store the cylinder / head results in between successive calls - probably somewhere accessible to read and write routines!).

Now, to get this into memory at runtime, I need a loader. This will be similar to the existing program in that it will install the DPB block and the HDD driver code, then perform the required patch operations.

In use I expect to run the loader at each cold boot using an AUTO call (just like the existing driver), and it will only work with a 63K CP/M, naturally. Once it is working I will generate a boot disk and put it and the standalone loader on pski's OS site. Subject to Firebox's approval, I will also post the ported source code.

So... any comments, advice, etc? Am I on the right track here?

October 7th, 2016, 08:46 AM
If you're going to use a loader, why not assemble your code as a page-relocatable file and have the loader stick it wherever it's needed, irrespective of system size?

October 7th, 2016, 09:12 AM
Because I don't know how to do that, and I thought I'd start by getting it working. I don't want too many variables in the pot here.

October 7th, 2016, 09:44 AM
If you patch the CBIOS vector calls for SELDSK, SETTRK, SETSEC, SETDMA, READ and WRITE with your own code after boot (see if the request applies to your device, otherwise, pass it on), you can implement the functions however you see fit, so long as you read and write 128-byte blocks in the end. SELDSK does have to return a pointer to a DPB in any case, even if you have to fabricate one.

Do not count on the calling routine to call SETTRK, SETDMA or SETSEC in any particular order. 40 years ago I was burned by this one--there are some badly-behaved applications out there. How you use SETTRK and SETSEC to map to a location in your RAM drive is between you and your bathroom mirror. It doesn't matter to the external world, since RAMdisks aren't portable. So make it convenient--say, 64 sectors per track, for example. Just a bunch of shifting to get to a physical RAM address--no involved arithmetic needed.

Are you sure that you're not looking at the code for WRITE, which does check the C register ? The idea there is that the value in C assists the WRITE routine in blocking/deblocking when physical sector sizes are larger than 128 bytes. The WRITE routine can determine from C whether the operation is a write to a directory block (you want to honor those right away with real physical I/O), or if it involves a write to a previously un-allocated block (you don't have read the sector before inserting new data) or is a write to a block that's been written to before (you have to read the physical sector, insert your data, and, depending on your buffering algorithm, write the sector back or hold it for the next (sequential) write. Keeps unneeded disk accesses to a minimum.

October 8th, 2016, 12:41 AM
Hi Chuck

Just to clarify a few points about what I am doing here:

I'm not writing a RAMDISK driver. I'm porting an existing driver for the TRS-80 Model IV LoTech card (a CF card adapter). It runs under MM CP/M on the Model IV. The reason ramdisks came into the conversation is that this driver replaces the MM CP/M ramdisk code, and as such has a slightly different interface; that is, it doe not implement SELDSK, SETTRK, SETSEC, SETDMA, READ and WRITE. Instead, there is a single entry point that calls WRITE if A is 2, else calls it READ. READ and WRITE both call a sector select routine that calculates head, cylinder, track and (physical) sector, which are passed to the CF card for read or write operation.

I can ask some pretty general questions about the SELDSK, SETTRK, SETSEC, SETDMA, READ and WRITE routines now...

Why is there a SETTRK but no SETHEAD or SETSIDE? Presumably there were no double sided drives when this was created.
Is SETSEC expected to be called after SETTRK? In my case, I need to know the logical track number before I can calculate the physical sector number. The existing code works it out from the logical sector number.
Does the BIOS store the track number in SETTRK, if so where?
Do I even have to implement SETTRK / SETSEC / SETDMS? I could just stub them out, and implement READ and WRITE only (per the existing driver, which has its own SETSEC built in).
The existing driver is calculating the physical sector number without referring to the DPB. There is a DPB, but it isn't referred to by the SETSEC routine. What, then, is using the DPB?

October 8th, 2016, 01:23 AM
OK.. backing up for a moment and doing some more reading. Looks like the BDOS wants the DPB, so it can maintain the directory records, and the BIOS doesn't need to use it.

So, assuming all patched BIOS functions check for a drive number > 3 and if so call my routine, else call the stock routine, we get this for my routines:

SETDSK - Return DPB of the hard disk.
SETDMA - NOP - Actually, I think the existing driver uses the standard 128 byte buffer, so may not need to patch this)
READ - Calculate physical track, head, sector from logical sector in DE, set track, head, sector in CF card and read 128 byte record, copy to address in HL
WRITE - Calculate physical track, head, sector from logical sector in DE, set track, head, sector in CF card and write 128 byte record, copy to address in HL

I should really take a look at MM CP/M's SETTRK, SETDMS and SETSEC routines to be sure..

October 8th, 2016, 02:34 AM
Well this sucks. SELDISK in MMCPM has NDISKS as 10, which allows drives A: to J:, but the RAMDISK is M:.

October 8th, 2016, 08:28 AM
Recall that CP/M started out when there was a single 8" floppy standard 77 tracks with 26 128-byte FM sectors, which remained the standard distribution for CP/M for years. Double-sided drives hadn't made their appearance yet. Hard drives were a vision from the future. So you do the arithmetic.

The best reference IMOHO is till the "CP/M 2.2 System Alteration Guide". I started with early versions of 1.4 and things weren't explained then nearly as clearly.

October 12th, 2016, 06:33 AM
Some progress to report:

I'm using the MM CP/M driver's "load on demand" approach. This entails using a transient called COMBINE.COM to mash the loader and driver code into a single COM file. You have to know where the driver code begins (it must be at the end of the combined file, but you can locate it by looking at a dump of the file, noting the address, then adding the TPA offset - 100h - to it). The driver code is compiled with ORG EC00 so that all the addresses are correct after loading. It is necessary to examine the .LST file of the compiled driver to ascertain the addresses of some of the values the loader needs to populate in the driver post loading.

I have a DPB for the CF drive from the original driver, but I have chosen to use the DPH of Disk 4 for now (my system is a single 8" drive plus HxC emulator, giving A, B and C, so D's DPH is free). The hard drive is hardcoded as E:, though.

The loader is simple, and does the following:

Copy driver code into high memory (EC00h) from the
Patch the BIOS SELDSK, READSEC, WRITESEC vectors. Store old vectors in the driver's specific old vector locations.
Patch the DPH for disk 4 so that it contains the hard drive DPB vector.

It needs additional safety code to check that there is free space at the top of memory for the driver, and (maybe) a check to see if the CF card is connected. All in good time....

Right now, it has completed building and I'm in ddt, checking the address for the driver source I computed is correct. It is...

I am sure it is going to crash at first load!

More soon...

October 12th, 2016, 07:33 AM
Sure enough.. it's not loading the driver or doing the patches, because it thinks they're already done. Need to look into this - it is testing to see if the saved SELDSK vector is set, but has garbage at load time. Commented that out for now, and it is loading properly, and resetting the vectors for SELDSH, READDSK, WRITEDSK correctly. What's more, the drive select code appears to be working (I can access all normal drives, so vectors are being properly redirected).

But... I have a BDOS Err on E: - Bad Sector. Ah! I noted that the formatter does not like the first 4 sectors of track 0 of the DOM I'm using, so I will probably have to add an offset to the DPB or try a different DOM / CF card..

[Edit: Adding an offset didn't work.. I'll have to do a bit of debugging, or write a test program.]

Dwight Elvey
October 12th, 2016, 08:27 AM
It might be the sector size is wrong for track0. Often,
even if the main code is double density, the track0 is single
density. That would have a different sector size.

October 12th, 2016, 09:11 AM
Are you certain that the access is being made to E:? One of the more common errors made when tinkering with the BIOS is ignoring the (re) setting of location 4 (CDISK).

October 12th, 2016, 12:04 PM
I haven't set CDISK, Chuck. Better check the Alteration Guide! What is it used for? If the BDOS selects the drive, it gets the DPH / DPB addresses, so why... ah. To check if the drive changed. I got an extra error on A: after the E: error. I bet that's because CDISK still said A: but the DPH was still E: (undetected disk change so didn't call SELDISK).

@Dwight: Unlikely. All tracks have the same format and besides, it's a known working driver with DPD already defined. Apart from CDISK, my money's on the DPH's DPB vector not being configured right.

October 13th, 2016, 02:31 AM
Oh well, the vectors all look right and proper.

@Chuck, is it really the job of SELDSK to set CDISK? I had a look at the sample CP/M BIOS and CDISK is not set by SELDSK: http://www.gaby.de/cpm/manuals/archive/cpm22htm/axa.htm (SELDSK starts at line 354 of the listing). Neither is CDISK set in the sample SELDSK shown in the alteration guide here: http://www.gaby.de/cpm/manuals/archive/cpm22htm/ch6.htm#Section_6.10 (and scroll down a little).

What it actually says is:

A responsibility of the SELDSK subroutine is to return the base address of the DPH for the selected drive. The following sequence of operations returns the table address, with a 0000H returned if the selected drive does not exist.

..so I am seeking a little clarification here. I think CDISK is managed by the BDOS...

October 13th, 2016, 05:11 AM
Also... I wrote a little test program to save a sector of text to the CF card and reload it to a separate address. It worked, so I think the problem is CP/M integration rather than the driver code itself.

Interesting point is I have not provided SETSEC/SETTRK routines, becasue both read and write routines compute these values for themselves. Maybe I should stub them out if the CF card is selected.

By the way, this is being developed directly on the Model II, with a little help from a PC. If I have a lot of editing to do I use PIP to transfer the source code the PC via Serial Port A (but it is only working at 300 baud). Then edit and transfer back. This is because I have no decent editor on the MII, just WS and the ultra slow vi clone.

October 13th, 2016, 07:36 AM
The important thing is that CDISK is not set by the "warm boot" routine. Cold boot should just zero it. But yes, it's managed by BDOS--but you can get into trouble if you fool with it.

October 18th, 2016, 09:33 AM
You may have read that I got Kermit running on the MII, which means I have a reasonably reliable transfer mechanism over serial at 9600 baud. Hooray!

The feeling of triumph hasn't lasted long, though. I am back to trying to debug the driver. At the moment, I re-vectored all the disk routines to new handlers. They look like what you said, Chuck:

[Patch] SELDSK, SETTRK, SETSEC, SETDMA, READ and WRITE with your own code after boot (see if the request applies to your device, otherwise, pass it on)

I have a small helper called isHDD:

; Is the currently selected disk the hard drive?
isHDD: ld a,(sdisk) ; get selected disk
cp hdisk ; is HDD? Z flag will be set if so
ret ; caller to check...

..and my patched code does this "is it the hard disk? if not, call original vector" thing:

; New HOMEDSK routine
nhmedsk:call isHDD ; is HDD selected?
jp z,hmhdd ;
ld hl,(OHMEDSK) ; not HDD..
jp (hl) ; .. so call original SELDSK
hmhdd: ld bc,0 ; is HDD: select track 0
jp sthdd ; call the HDD part of settrack
ret ; ..and return

; New SELDSK routine (return HDD DPH if HDD, else call original vector)
nseldsk:ld a,c ; Get the drive selection
ld (sdisk),a ; store it for future use
call isHDD ; is HDD selected?
jp z,sdhdd ;
ld hl,(OSELDSK) ; not HDD..
jp (hl) ; ..so call original SELDSK
sdhdd: ld hl,hddph ; is HDD: select hard disk DPH
ret ; ..and return

; New SETTRK routine (save track for HDD, else call original vector)
nsettrk:call isHDD ; is HDD selected?
jp z,sthdd ;
ld hl,OSETTRK ; not HDD...
jp (hl) ; .. so call original vector
sthdd: ld (strack), bc ; HDD: save the track for future use

; New SETSEC routine (save sector for HDD, else call original vector)
nsetsec:call isHDD ; is HDD selected?
jp z,sshdd ;
ld hl,OSETSEC ; not HDD..
jp (hl) ; ..so call original vector
sshdd: ld (slsec),bc ; HDD: Save the sector number

; New SETDMA routine (save DMA for HDD, else call original vector)
nsetdma:call isHDD ; is HDD selected?
jp z,sahdd ;
ld hl,OSETDMA ; not HDD..
jp (hl) ; .. so call original vector
sahdd: ld (sdma),bc ; HDD: Save the DMA address

; New SECTRAN routine (no translate for HDD, else call original vector)
call isHDD ; is HDD selected?
jp z,snhdd ;
ld hl,OSECTRN ; not HDD..
jp (hl) ; ..so call original vector
snhdd: ld h,b ; HDD: return sector untranslated
ld l,c

The read and write code is the same, save that it has all of Firebox's code in it, and it's modified to use the logical sector saved in the nsetsec function, and the DMA set in the nsetdma function (if the HDD is selected).

What I don't understand is I've redone the vectors in the BIOS jump table to point to my "new" vectors, and stored the addresses of the "old" vectors (well, they are in equ statements), and I have double checked that the addresses are correct - all of them. However, if I load the driver and patch the vectors, it all grinds to a halt, even though drive A: is selected (which of course means that the "old" BIOS functions get called. I even checked with ZSID that this is the case, and sure enough, my test program demonstrates correct calling of the old vectors when A: is selected and correct calling of the new vectors for drive e:.

Something is going on, but I cannot work out what it is. My test code looks like this (note, the vector addresses in the CALL instructions are the locations of the address for the BIOS jump vectors, so I deduct 1 to give the address of the JP instruction itself):

154 019F test:
155 019F 0E 00 ld c,0
156 01A1 CD E81B call SELDSK-1 ; select hdd
157 01A4 FF rst 38h ;2
159 01A5 CD E818 call HOMEDSK-1 ; home the disk
160 01A8 FF rst 38h ;0
162 01A9 01 0017 ld bc,17h ; select track 17h
163 01AC CD E81E call SETTRK-1 ;4
164 01AF FF rst 38h
166 01B0 01 0080 ld bc,80h ; set DMA (to 80h)
167 01B3 CD E824 call SETDMA-1
168 01B6 FF rst 38h
170 01B7 01 0298 ld bc,298h ; translate sctor 298h
171 01BA CD E830 call SECTRAN-1
172 01BD FF rst 38h
174 01BE 01 0298 ld bc,298h ; select track 298
175 01C1 CD E821 call SETSEC-1
176 01C4 FF rst 38h

The RST 38 instructions return to the debugger. When running each segment, I can see that the SETDMA is causing the problem. But when running SETDMA on its own, there is no problem (it returns properly).

I'm at a bit of a loss here.... maybe I am running out of stack space. Can't really see it, as I am only calling the isHDD helper and the rest is the call to the original vector. Is the BIOS stack really that small that it cannot handle an extra 4 bytes or so? I suppose I could inline the isHDD function as it is so small - that would save one CALL instruction per BIOS call. I doubt that will help, though.

October 18th, 2016, 10:33 AM
Something to think about--is the address that you're storing the DMA address in the same as used for the other BIOS disk calls? In other words, not every setup sequence for READ/WRITE involves a SETDMA call--very often, code will do this once and assume that it applies to all drive accesses.

October 18th, 2016, 10:00 PM
No, I don't actually know where the DMA is being stored for drives A: to C:, and it doesn't matter because the HDD code has its own DMA location. I guess the answer would be to default the HDD DMA to 0080h, but I haven't even tried to select it yet. I'm still trying to get the driver working with A: (that is, all my driver code just passing through to the old code).

I did notice something odd, though. Running the test segments, I can see that some of the functions return with a bad stack pointer. SP is 0100h at runtime, but after calling a few of the functions in the test block, it comes back as 00FEh, which suggests a PUSH / POP mismatch somewhere.

Further investigation required here...

October 18th, 2016, 11:36 PM
Hmm, this looks wrong...

; New SELDSK routine (return HDD DPH if HDD, else call original vector)
nseldsk:ld a,c ; Get the drive selection
ld (sdisk),a ; store it for future use
call isHDD ; is HDD selected?
jp z,sdhdd ;
ld hl,(OSELDSK) ; not HDD..
jp (hl) ; ..so call original SELDSK
sdhdd: ld hl,hddph ; is HDD: select hard disk DPH
ret ; ..and return

; New SETTRK routine (save track for HDD, else call original vector)
nsettrk:call isHDD ; is HDD selected?
jp z,sthdd ;
ld hl,OSETTRK ; not HDD...
jp (hl) ; .. so call original vector
sthdd: ld (strack), bc ; HDD: save the track for future use

In nseldsk I'm calling the OSELDSK using the contents of the variable holding the vector. In nsettrk I'm calling the address of the variable holding the vector. Basically forgot to put brackets around the OSETTRK arguments (and others).

[Edit: Corrected, and now I see no stack corruption, but it still is not working for Drive A:.]

October 20th, 2016, 11:39 PM
Well, perseverance pays off as usual.

I ran the patch code one vector at a time, exiting ZSID to the CCP and testing drive access each time. I was surprised to see that SELDISK, the first one I tried, caused a lockup. I had put some debug code in each call so I could see what was going on at warm boot. The debug code calls the BDOS to dump a character to the PUN: output. I think this was corrupting the stack, or causing some similar problem - is it safe to call the BDOS from the BIOS? I guess the answer is "no"!

So, having removed the debug code, I stepped through the patch code, again exiting to the CCP after each section. One by one, I saw that there was no lockup at the a: prompt. Eventually I completed the operation successfully - all required vectors were patched, and here I was at the A: prompt, all working as normal.

Time to access the CF card. E:. And I got a prompt! E: is selected and I can see NO FILES in the DIR. So far so good. Let's pip a file in there! I got an error this time - UNABLE TO CLOSE FILE - so tried a few STAT checks. Looks like it is 485k in size. Hmmm, this is because I hadn't patched the DPB properly, so I went in to ZSID and executed the DPB patch code.

Now, on selecting E: in the CCP, it goes into a lockup with the A: drive clicking continuously. All I did was patch E:'s DBP in... and this DPB is the same as the original driver's DPB, so it should work, given that E: is tested and formatted successfully with the original formatter. I can see drive access lights on the CF adapter as well, so it is definitely vectoring to the CF card read/write routine correctly.

This counts as "progress" to me, and I will keep on hacking away at it. :)

Next test is to run the loader directly from the CCP instead of from within the debugger.

October 21st, 2016, 07:15 AM
So, next step was to create my own DPH for the CF card, and allocate space for the ALV scratch pad buffer. This proved to be the reason why the thing was failing - because I used the DPH for Disk 3, its ALV scratch pad was too small and was trampling over other parts of the BIOS. Creating my own DPH means the BDOS has the space to manage the disk storage allocation properly. In fact, for this implementation, we need the odd amount of 313 bytes for the ALV scratch pad, that being the value of (DSM/8 )+1, the DSM being 2499. And of course, the fourth DPH can be used to support drive 4 (which doesn't exist on my system, but does on systems with fully populated drive expansion bays).

Once I sorted that out it started working (http://www.vcfed.org/forum/showthread.php?54567-Announce-Model-II-Lifeboat-CP-M-LoTech-CF-card-adapter-drivers).

@Chuck(G): Thank you for your advice and patience. You were right to insist I read the CP/M Alteration Guide. ;)

October 21st, 2016, 08:05 AM
My pleasure. One of the things that confounds BIOS writers is the unusual terminology used in the Alteration Guide.

BSH/BLM - "Block" (cluster) size and mask in terms of 128-byte sectors, described by a shift and mask (in powers of 2). Thus, BSH 3 tells the BDOS to take 128 and shift it 3 left = 1024, the block size. Correspondingly, BLM is 7 (2**3-1) or the number of 128 byte sectors in a cluster -1. Strictly speaking, only BLM or BSH is needed, but BDOS asks for both.
ALV - Allocation bitmap; describes the entire disk allocation, one bit per "cluster".
AL0, AL1 - the first 16 bits of ALV, any set bits are "preallocated", so almost always, you'll see bits set from the leftmost position of AL0, continuing into AL1 (almost always 0). This is usually done to reserve space for the directory, but can also describe disk space that's reserved for some other purpose, such as a continuation of the BIOS.
DSM - how many clusters on the drive
DRM - how many 32-byte directory entries there are--must be included in the AL0/AL1 bit map.
EXM - described earlier in this thread; the extent mask for each directory entry
OFS - offset in "tracks", which can be anything that the sector address calculation says it is.

CKS - the number of directory entries checksummed to determine a disk change
CSV - "Checksum vector" one byte per CKS, used to contain 8-bit checksums of the first CKS directory entries

Note that since you can't change hard drives on the fly, CKS/CSV for those is usually 0. Early MS/PC DOS ran into the same issue that CP/M (and a host of other OSes did) that changing disks was not under control of the OS, so you could find yourself reading or writing the wrong disk, just because the user has decided to get ahead of the game and change disks unexpectedly. High-density drives usually feature a hardware "disk changed" indicator to help out with this problem, but most low-density drives (e.g. 360K) do not--and in the case of CP/M, not all 8" drives.

There were several ways to approach this. Apple, in the Macs, put drive eject under program control--and expensive option and one prone to failure. A "drive lock" was available with some drives--usually a solenoid-operated interposer in the drive latch mechanism. But again, most drives didn't have this. CP/M resorted to the "checksum" option, which, while not foolproof was better than nothing.

October 23rd, 2016, 12:54 PM
Hmmm... I'm wondering if I shouldn't roll my own BIOS here. I already have the source for CP/M's BDOS and CCP in Z80 assembler.

Lifeboat CP/M's BIOS has a lot of code in it for the disk routines which I don't need. Rolling my own'll make it easier to build a drive mapping table.. hmm, hmm.... I'd have to pull out the basic BIOS functions as-is (although I am certain that they are simple enough, no point reinventing the wheel). Hmm, hmm... Reason is I'm out of space with a 5Meg CF card. If I want 15 drives +1 floppy, I will need to economise somehow. As it is, for 12 drives I need an extra 3k of RAM... hmm, hmm... And I will need to implement sector block/deblock I think, so as not to waste any space on the CF drive.

This is where the madness begins!

October 23rd, 2016, 03:45 PM
Well, I never thought much of Lifeboat, even when I was dealing with them. So have at it! :)

October 24th, 2016, 12:34 AM
On the TRS-80 Model II, Lifeboat presents to me as the nearest thing to a plain vanilla CP/M implementation. That's why I prefer it. The others seem to have custom stuff that makes driver patching non-standard if it is to be done properly. Especially Pickles & Trout CP/M, which is supposed to be the gold standard, but it implements loadable drivers of some sort and I would rather learn how to do it the DRI way before learning the P&T way.

Besides, Lifeboat, being vanilla, boots up really, really fast, on an 8" drive, or an HxC floppy emulator.