 Day four. Let's keep on going. So the story so far is that we have our 6.02 CPM clone working sufficiently to mount finger quotes, the file system, and open the CCP file. The next thing we have to do is to load the CCP file. We can see here in our emulated BBC micro if I boot it. If we reset it and then boot it, we can see that it prints this message indicating that it has successfully got through the initialization procedure. It's reset the disk. We've opened the FCB. We now want to read data out of it. So since last time I did actually go through and do a bit of cleanup. The main thing is that I got annoyed by the bug that I kept perpetrating with the anonymous labels, where I forget to put a label in to jump to the wrong one. So I took a bit of time to build some structured programming macros, which these are zero cost things. Each one of these assembles into a single branch instruction. And these allow simple loops, simple conditionals, useful stuff like that, which makes the code much easier to read and is more reliable to write. I have also found the original digital research source code for the AT-AT-BDOS. This is very dense because it's using multiple statements per line. The whole thing is only about 2000 lines for the entire operating system, but is very well commented and you can see the use of indentation for structure. So it's actually easier to read. And if some things are actually clearer here than they were in the annotated disassembly I was previously using. So one of the things that's come up is down here. So when we select a disk, we copy the DPH, the structure that describes the disk into local storage. This is actually wrong because some of these fields such as these scratch fields and the buffer and so on, we actually want to modify. For example, we want to maintain the current sector value for each drive. So we don't want to write to the copy, we want to write to the original. For things like these, it's fine because these are all pointers. So we copy the pointers into local storage and then dereference the pointer. But for these words, we do not want to do that. So let us go and fix that now. So instead of copying the whole thing, we only want to copy a few fields. And in fact, in fact. So in the 8080, what it did was it, let me try and find the relevant code. So the disassembly called them scratch, but is this the one? I think this is it down here. All the variable names here are of course different. No, that's not it. Okay, so here is the code where it's selected disk. Let me find that. So select the drive, select active drive. Here we are. So we call the BIOS. If there is no error, the carry is clear, we do the copy. So over here, it calls the BIOS. If there is no error, it copies. What does it copy? HL is the pointer to the DPH. This is 8080 machine code, which I am less fluent in than Z80. Okay, so this instruction dereferences HL into E. Okay, this is loading the first word in incrementing. Here are our three scratch words. Yes, SHLD. It is storing HL, that is the pointer, into these variables. Now, we could do that, but on the 6502, the only way to dereference a pointer is to index it with Y. So indexing into a structure is just as expensive as indexing a pointer. So in fact, we are going to create another zero page variable. It is not the DCB, it is the DPH. Okay, so we actually don't want to copy these. So these four words here, these are also pointers. Every time we use them, we could load them out of the DPH, but I actually think it is worth also putting them in zero page. So that will let us get rid of this completely. I think that we still want this, because we are going to want to refer directly to a lot of these values, and if we are going to copy that out, we don't need this variable either. So we just need to copy these. So now, did I make structures for the DPH? I did not. Let me just go away and define some structures from the documentation. Okay, done that here, and I am copying the names from the original source code, which are not very readable. So here are our three scratch variables, which contains the maximum directory entry scene, the current record, and the current sector. Hang on, hang on. I have read the wrong things. And also, these things have changed to a 24-bit value. So this is going to be current sector. I think a D word will do. Got the documentation here. Got three scratchers, Dearbuff, DPP, CSV, ALV. Okay, so we now want to copy four of those values. And luckily, they are, well, these three which are in a row, and there's the DPB. So this is going to be DPH, CDR, MAT, DPH, Y. This gives us the low byte. And this is going to end up, no. I mean, I am forgetting what I just said. We don't want to copy these because we want to use the versions being pointed at. We want to copy the pointers, these, and we are going to end up copying the actual DPB. Okay, so this should be Dearbuff. So that's going to end up in directory buffer and so on. We're going to loop like this. These are in awkward order. See, we could just, if these were three words in a row, we could just use a simple loop. But this is going to want to go into temp. Unless we do just allocate, make it easier. So we wish to loop and store, increase Y, compare Y with A plus two, that means the end until equals. Okay, copy the DPB into local storage. We already have the DPB pointer in memory. So that will simply be that. That should be a double colon for namespaces. Okay. And that's actually going to be current DPB. 93 current DPH. Right. So this is what this wants to get a pointer to the current sector variable, which is in the DPH. So we are going to load the DPH into memory, add, and that's called Karasek. If carry set, increase X. Okay. Calculate Dearbuff sector. Calculate the block and sector addresses of the Dearbuff. Y and DPH. Y, Y, LDA0. DPH, Y. 84. This is going to reset the current sector to zero. So this is going to start at Karasek. Increase Y, compare with its three bytes until equal. Why doesn't it like that? Does this need to be a low byte? No. dphdir buff should be a small integer. I don't think we want to put... yeah, that's not right. What does this do? Unary Bank... oh, right. That's the bank. That's if you're using banked memory, like many 602 systems have multiple memory banks, so they can access more than 64K. So you end up with three byte pointers, but we're not one of them, so that's not relevant for us. So I would very much expect that to work, but it's not. Yeah, that's wasting three bytes, which I don't like. We shouldn't have to do that, but we'll go with it. Okay, let's see if this works. Yes, it does. Good. Let's just take a look at the size of our BDOS. Yes, yes. The 8080 BDOS is 3.5K. Now, that's actually... it's not as big as it looks, because we have the relocation table after the code, which is most of a page to 256 bytes, so it's still technically under a thousand bytes. Anyway, that does seem to be working. We are actually also going to take a moment to implement the maximum directory entry thing. So we want to add, what's it called? Not that one. cdrmax, and this is from here. So all this is going to do is test to see if cdrmax... to test to see if current dirrent is bigger than cdrmax and if it is update cdrmax. And then we call it from the login routine. So we'll end up with cdrmax being set correctly, which we can then use in the rest of the file system. We'll have to call this again when creating a new file, when we add another directory entry. But the end result is this should always be correct. Let's say this also needs to be reset at some point. This one, here we go. If cdrmax is greater than the current dirrent, boy, this is a 16-bit comparison in 6502 code. These are horrible. Now they're so horrible, so I will in fact just go and look up the code and copy it. Here is some very simple code. This does a 16-bit subtraction, discarding the result, which leaves the carry and zero flags set correctly. Okay, so we actually want to... Yeah, this is a subtraction, so it goes in increasing size order. The other way you can do it is to compare the high bytes and then compare the low bytes. But this is actually smaller. So, just looking at the order we want to do things in. So if we make adder1 be cdrmax, so if the carry is set, then adder0 is bigger or equal. So here we want to copy dph into current dirrent. We can optimize this if we swap them around, actually. No, sorry, I'm an idiot. We do want to do it this way around because we are left here with a being current dirrent that we want to store in dph,y. So we decrement y load current dirrent plus zero store. And this actually wants to be current dirrent throughout. I'm actually wondering... Yeah, my fingers want so badly to type dph, so let's actually just humor them and change this throughout. Okay, so update cdrmax corrupta and y. No, we don't actually want to do that there. Where's our login? Here we go, we want to do it here. Update bitmap for dirrent, update cdrmax. Right. And this does still run, which is a good thing. This also allows us to find the read directory entry thing. Here we go. Find next because we can now check the current dirrent value against cdrmax. If current dirrent is higher, we know that the tree is empty. So give up now. Okay, so load cdrmax. And then we want to do the same comparison. But no, no we don't. Because we have just read a directory entry. So current dirrent has not been incorrect. Yeah, we are going to have to do a magnitude comparison because you want to know if current dirrent is higher than cdrmax. So what we care about is bigger. So adder1 needs to be current dirrent if carry clear. So at this point, we know that there is no more. So we're actually going to put this code down here because we're calling it in multiple places. So here we can just say beq no more files. And here we can just say bcc no more files. So if we put a debug statement there and then run it, we see one dirrent because we're looking for the ccp which is the first file in the directory. So yes, we only see one. So if we actually just go up here and change this. So that doesn't, the file name doesn't match. We should see three. Yep, works just fine. Okay, so let us now try loading the file. So we're going to want to implement sequential read. Now the ccp, if we go and look at our notes, the ccp is actually supposed to be loaded way up high to free up the bottom of the tpa for loading the transient program. Problem is that's rather trickier than it looks because we need to load the entire program into memory and then relocate it. And of course the relocation, let me put it here. So the program actually goes up to here and then you have three bytes of relocation information. And the bigger the program gets, the more relocation information there is. So here is the bedoss and the relocation information starts probably about here. So all of this is relocation, which means that if we just do the naive thing and subtract the program size from the hymem, then when we load it, all the relocation stuff will actually spew over the top of hymem, which we don't want. So I'm not sure the right way to do this. We could just measure the length of the file, load the file that much below hymem, and then waste this much space. Because remember, however, that the file is actually, it does actually have some bss defined. Bss is storage that's not in the file. It's initialized to zero. So if there is more bss than there is relocation information, then everything's fine. We only waste space if there's less bss than relocation. But we still also have to make sure that if there is more bss than relocation information, we still have to make sure there's enough space for the bss when we load the thing, which is all really annoying. So I think for now, just in the interest of development, I'm going to load the CCP low at the TPA. We can move it later. I'll need to think about this offline, how to do it. But this will get us up and running more effectively. And let us do lots of testing before that actually happens. So we now want to read the CCP into memory. So we go find the CPM reference. What we are going to do is find the VDoS functions. Not that one. That one. Actually, we did want that one. We want to find sequential reads. That is number 20, entry point 20. So what this does is it just loads the next record from the file into memory. That's all it does. So we want to set the DMA address, which we can get from here. And then loop reading one at a time. Let me think. So we do actually have this user DMA field that we're going to use to store the pointer. So all we want to do is get the TPA. And in fact, I'm not going to mouse about the table, we're going to put some helper routines. One of these costs five bytes. Two for the LDY and three for the BIOS call. So by using a helper like this, it reduces the amortized cost to three bytes. It's not a lot, but it's still useful. So we're probably at some point going to want to call setTPA. That might be the CCP's job. We might not want it at all, but we'll do that when we need to. Anyway, let's load the address. So that is the address we want to load at. So now we do a loop and we are going to call read. I came up with I defined names for all the entry points. It turns out that CPM doesn't really have defined names for the entry points. I just made some up. So this is BDOS read sequential. This is the address of the FCB. Wait a minute. We are in the BDOS. So we're just going to call the entry point like so. On exit, if the exit value is 1, well, if it's not 0 to be honest, then we reach the end. As usual for the 6502, we're going to declare that the carry flag defines an error. So break if carry set. So now we just want to add 128 to the userDMA pointer and go again. So we can easily do that with just a userDMA plus 0. If the result is 0, then this means we've gone from 80 to 00. Therefore, we need to increment this. Done. I didn't define one of those. Break if CS. I would like to be able to take the condition as a parameter, but I actually don't know how to do that in CA65's macro language. Okay, read sequential is undefined. And we're also going to get rid of our error. Set if carry set like so. Okay, so now what we need to do is implement read sequential. So where is our openA file? All right. Okay, if we run this now, it's going to actually let's set carry to indicate that we reach the end. This should print readSeq once and terminate. Yes, it has. Oh, we left that, dear ent. Debug thing in. We have to be really sparing with our debug messages because ASCII text adds up really quick on an 8-bit machine. Okay, sequentialSeq. These old assemblers had very low numbers of significant figures and identifiers, partly to save memory, partly to save complexity. So trying to guess what they've called things isn't great. This read is actually reading from the console. It's the buffered input thing. We're going to have to implement that eventually. Oh, they're all called... Are they called funk? Select disk. Where's the jump table? Here it is. Yeah, they're all actually... They don't have names. They've got numbers. But we know we're function 20, so... So what this is going to be doing, I know that from elsewhere, is, if I can find it, if the FCB has a drive number in it, then we want to select that drive. If it doesn't have a drive number in it, then I'm not sure how it's actually supposed to work because it does need to select the drive to make anything happen at all. FinalControlBlock. DR0 for default. 1 to 16 for A to P. But after the FCB has been opened, then I would expect the default value to be replaced with an actual drive number. Hmm. Actually, have we already done this? What? So we use open file. That was 15. So if we go look for funk 15. Yeah, re-select is select FCB drive. We've done this. I don't think we've done it right. And we also need to get the FCB out of HL and into, well in our case it's out of XA and into the current FCB variable. So we get rid of that properly. So this is going to be old user FCB. That does the setup. So this fetches the FCB and looks at the bottom drive, which is 0 to 31. You can actually specify question mark as the drive. Can you? So you get the FCB and DE drive. 0 for default. 1 to 16 for A to P. So why is it looking for 30? Where is the question mark in the ASCII table? 3F. Non-zero. Right, non-zero is auto drive select. That is, you select the drive and it's specified in the FCB. Yeah, this is also setting the drive and the user code, which we are not actually doing. I think that new user FCB here should be this clear as S2, which is the module byte. And then we jump through to here where we get the drive byte. I think that we're going to need to update the drive byte if it's 0 with the current drive. Okay, so here's why it's ending it with 31 rather than 15. It's because the byte contains the drive number plus 1 so that we can get 0 in it. Hang on. If the drive number goes from 0 to 16, then the user code can't be in here. On disk, this byte is used to store the user code. So we save the drive. This is updating the drive byte in the FCB with the user code. But if you overwrite the drive byte in the FCB, then the next time round, how does it know what the drive byte is? Because all information about the open file has to be in the FCB. Right, here's what's happening. It does indeed update the FCB, and then after everything has finished, it puts that byte back the way it was. So it saves it for the duration of the BDOS call. And that actually happens automatically by the dispatcher. Okay, so we have FCB disk is the disk named in the FCB. Old disk is the disk on entry to the BDOS. So let's take a look at the other disassembly and see what this calls them. Okay, mode is C-CIO. E-param is L-info. So we've got the auto-select flag. Okay, big disk is single. Auto is resale to indicate auto-selection mode. We have old drive. We have auto-flag. Yeah, I don't think this disassembly is quite right in many places. Okay, anyway, let's just add some of this. Drive currently being worked on. We have old FCB drive. The reselection flag. The reselection is, I believe, it's word for putting the drive back at the end of processing. So if that has happened, then we need to fix the user's FCB on exit and select the correct drive. Okay, I think, honestly, we can do better than that. I don't believe we need an actual flag. Okay, that's... So here's the selection code. We get the drive byte. We add it with this to get a subtract drive. We want to subtract one, but of course, we don't have a... The 6502 doesn't allow you to increment or decrement A. The 6502 does, which is irritating. So we now have the value... We now have the drive in X compare with... Actually, if that was zero, it'll have gone negative. So if minus... If it's negative, then this means that we actually want to take... We want to get the current drive and use that instead of this. That's going to be simply LDX current drive. Store it in the current working drive. Store the drive number that was in the FCB in old FCB drive. Wait a minute. But if we're overriding it with old drive, then we're not actually changing the current drive. So I actually think that we want to do that up front. That will save a copy of the current drive that we are about to override. So at the end of this, we have the current drive in X. So if we save the old FCB drive before we fiddle with it, that means we save time putting it back the way it was. So we... At this point, we have the drive byte in zero to 15 numbering in A. We are in the user code. We save it back to the FCB. We... Hang on a second. We're using Active Drive to specify what the drive currently being selected is. So current drive only ever represents the drive the user asked for. So I don't think we save this at all. We do need to save old FCB drive. So we set the Active Drive to the one in the FCB. We update the FCB with the Active Drive and we select the drive. So now we end up with an FCB that has the current user number and the current drive in it all together in the way that we expect to see in the actual directory entry. So I think that should work. No colon. That means that I don't have a ZIF MI yet. These do not match because this is the opposite of this. We are skipping over the code that is being selected by the if statement. So we also have the opposite. So plus and minus. Okay. So there is actually a little bit of a problem here, which is we call, we open the file. This is then going to fiddle with the FCB. So that when we call read sequential, it's going to see the fiddled with FCB, which isn't what we want. We want it as it was. So we're actually going to do it like this. So we actually have a special internal entry point that bypasses old user FCB because everything is still selected from when we called open file. So the question is, of course, does it still work? Couldn't open CCB? No, it does not still work. Well, that's because we're using the current drive. We always want to load the CCP from Drive A. So that should be a one. It still doesn't work. And we can also move this to here and inline it. Okay. So this is still going to wait a minute. I think I know why it failed. Oh, no, I did put that back the way it was. Okay. Let's step through it. So we're here. We do our initialization. We're in exit. Reset the disk. Prepare for opening the CCP.sys. So we're now stepping in. New user FCB goes to here. We store the FCB pointer in 6.7, which is one CDA. There's an FCB with Drive A there. But we are now, we are clearing the S2 byte. This is the module byte. It's the upper byte of the extent counter. So that's done. It was already clear. Okay. So we get the current drive, which is two. Why is it two? Also, why is that line there anyway? That's because we were going to save it somewhere, but we're not doing that now. So we now have the drive number in A, which is a one. And let me just change this to FCB DR. Store it for later. And it decrement it in X. X is now zero, which is what we expect. If it was negative, this means that we want to use the current drive. But it's not. So we skip to here. Put it back into A, which is zero. Save it. I know what's going on. Or in the current user, which is garbage. That's why it's not working. We are not initializing current user and current drive. So that has to happen in this initialization code. If you ever get more of this, that will need to turn into a loop. Okay. We have successfully read the file. It has opened it and everything. Okay. So I think that's now working. Let's go back to read sequential and our code. This is funk 20. So we are now in secret disk read, which is the code that actually does the work. So this is actually keeping a sequential IO flag, but I'm not sure why. These values back into current FCB. So this big piece of code is the right stuff. And it's using, I have no idea what that's doing. Probably what this is all about is updating the FCB to point at the next record after a sequential operation. When you do a random access operation, it leaves the FCB pointing at the record you asked for. So if you do a random access read and then a sequential read, you should read the record just being read with the random access operation. Where are we read random pointers in the FCB? We updated the next record to read using sequential IO calls will be the record just read. Okay. So when we do a sequential read, we need to read the record currently being pointed out by the FCB and then increment the FCB. So the next read gets the next record. When we do a random access read, we want to set the FCB to point at the record needing to be read, but then not update the pointers. Let's not worry about that for now. We'll need to when we get on to the random stuff. We may be able to do better than just having a simple flag to be honest. So we want to get the next record from the FCB. We already have the FCB handy. We want to look at the FCB to see if the record currently being pointed at is greater than the number of records in the extent. So what does that FCB look like? RC is, that's not very helpful, you know. This is the record count. CR is the current record. So we are going to load the current record, compare it against the record count and if it's higher, so if the record count is the same value as CR then the internal subtract will have a zero. So CR will be clear. If it is larger then this will overflow so we end up with CR being set. So not enough records in extent. Actually we'll do end of extent. So this means we then need to go on to the next extent or stop. We'll do that later. So once we get here we know that we are actually in the current extent. So we need to figure out which block it is. So I guess this index function, yeah here we go. Compute disk block number from current FCB. Get FCB block. This proc end proc stuff is to do with scope management in the assembler which I need to make the structured macros work. Compute disk map position for V record to HL. Which will record lots of fiddly arithmetic. So dm position should, yeah that figures out the block index. Okay, so what we need to do is to fetch the current record. We then want to shift it left the right number of bits to get a block index. And we actually have that in the dpb. We are looking for block shift here. So we are going to, hang on a second, this is a 8-bit value. So we want to shift left that many times. ASLA, DEX until zero. No we don't. No we don't. We want to shift right because there are multiple records in each block. So in each extent can be up to 16k long. And there are 128 records. So the record count can actually go from 0 to 128. If we are a big disk, then each block occupies 2 bytes. So we are actually going to, we want blocks on disk. Blocks on disk plus 1. Now we are big disk. ZIF, yes. So we are actually going to a return offset into the current block in the fcv in y. So we get the current disk block. And now, because it can be either 8 or 16 bits, that's making life so much difficult. Get dm here is probably a routine to do this. Return disk map value from position given by bc. That gets the address of the disk map, that's the allocation vector in the fcv index. It returns the disk block value in a 16 bit register. So are we a big disk? Yes. Therefore, we want to load the low byte, load the high byte. So this gives us a 16 bit disk block value in xa. So we now want to see whether the disk block is actually 0. If it's 0 then there is no actual data. That means the block is not allocated. CPM files can be sparse. You don't have to allocate the blocks in sequential order. Luckily the sequential read routine is defined to treat hitting an empty block as in the file. So we are here. Error 2 if reading unwritten data. So we actually want to compare a and x to see if it's a 0. If 0 then error code, error happened, exit. Do I actually read sequential? Well it says 2 here but returns 1. Yeah, there's this comment. Okay, that's not very helpful honestly. So we do have a block allocated. We now want to translate this to a block address. This is where we shift the thing left. So that will be, yes, we want to shift our block number left to get a absolute sector number. Block numbers are 16 bits so this will give us a 24 bit value. So we can't use shift L here. We're using current block. We are not using current block anywhere. We actually I believe want to put the block in the DPH. But why is the current block in the DPH, the current sector? Yeah, we put the current sector in the DPH because in CPM80 the current track and sector was in DPH. And it needed to keep track of those due to the way it did the arithmetic for calculating track and sector. It would rather than just doing a simple division and modulus, it would step up or down until the value was in range. Well I say a simple division and modulus on the 8080 that would be immediately complicated. But we aren't doing that. So actually I don't believe we want to use this at all. So these become unused. So instead we are going to define a 24 bit value here. This will make the code easier. So instead of having to do a addition, we simply do current sector, current sector, setsec, callbiles. And the calculate dear end sector again is just going to be, what's this doing? I thought I changed that. Yep. That's not a pointer, that's an absolute value. Okay, good. So we now have a block number. We actually want to stick this in current sector but one byte up. And in fact this code put it there. Do you want to? No, let's just do it here for the time being. The reason we're doing it one byte up is because this multiplies the value by eight. We want to turn this into a sector number. So we are now going to shift it right by one bit. So we will end up multiplying the block number by 128, which is not what we want to do. Okay, we actually want to shift the current sector up by block shift bit. So z, repeat, shift left, sector zero, roll current sector. This one, roll current sector. This two, dex until zero. So this gives us a block number. And I also realize that I need to put this here. Okay, we're not done yet because we've got the record number offset into the block. So the record number is in current record. So we get the record number. And now we want to mask it with block mask. And now we want to add it to the current sector. So if carry is set, in current sector plus one. If this is zero, meaning rollover, in current sector plus two. Okay, we now have the sector number of the block in current sector. So the sector number of the record in current sector. So we actually want to do the read. So we are going to set the user DMA address, call read sector, and that should do it. What we haven't done is incremented the sector, incremented the FCB record count. So that's our current record. In fact, is there an indexed ink on the 6502? Zero page, zero page comma x. No, there isn't. That we're looking for a ind comma y. But no, there's only these four. Because I was hoping we'd be able to just do it here, but we can't do an indirect load through any index register. No, we can't. So we're actually going to have to CLC ADC one. So this will count up until we come through here and we hit the end of allocated data. Like which point we stop or we reach the end of the extent at which point we will hit this piece of unimplemented code. Okay, it builds. Let's run it and see what happens. So we are here. Okay, we are down in exit, reset the disk, open the file. We got a message. What message did we get? End of extent. It's not the one I was hoping for, to be honest. We should have got here. Anyway, let's do a hard reset. We want to go to 1937. So continue 1937. We're here. We're here. Get the TPA, set the user address, which is in 1D98. So we're putting it at 1E00. And now we actually start the loop. We get the FCB, go to read sequential. We know the old user FCB works. So that our FCB, which is being pointed at in spy address at 671B6B is correct. We also see that we have a record count of 128. That's not right. Now record count of zero. Set this to zero when opening a file and then leave it to CPM. We haven't computed the record count. That's what we haven't done. Anyway, let's just step through and see what happens. Our current record, which is this one, is zero. So that has compared CR with 00. 00 and 00. And you ended up with the carry set. Really? Why? So what that's going to do is subtract zero from zero, ending up with zero. It hasn't overflowed. The carry should not be set. Is this something weird about the 6502 carry or my number is not right? So FCB plus 15 should be this one. FCB plus to zero. It's all hex should be this one. Yes, the MP does set the carry. In fact, I think we can just compare these for quality because it's post incremented. So yeah, my earlier ranting about this was wrong. Okay, but let's find open file and attempt to set up the record count. Open file is 15. So in this code, it's just called open called directory record count. The record count is stored on the disk. It has to be. Here we go. 01. So how did it get to be zero? So we fetch the extent byte that the user asked for. Stash. Copy the DRN into the user's FCB. Reset S2, which is, I believe it's this one. Where the user extent bytes, I bet this code is wrong. I think my comparison, I'm not getting the way comparisons work. Correct. Okay, let's let me actually go and look that up. Yes, carry is set if the first is less than or equal to the second. So this will actually subtract the value at 20 from 21. And if they're equal, the carry flag will be set. Great. So because we've just done the compare for equality, I think we can change that to ABCC and then swap the sense of these. Okay. So let's go through this again. Shall we step through all our boilerplate, reset the disk, load the file. And we see that our record count is still zero. That I didn't do anything. Also, that's wrong. Let me try and get this right. If carry is clear, this means that this value, the one in the FCB is smaller than, sorry, means that this value, the one in temp B, is smaller than the one in the FCB. So I think we want to swap these if they are equal. Again, just stick all of this in a if to make it clear of what's going on. So if carry is clear, if the carry is set, then this means that this one is going to be larger or equal to dear end extent. We've already done that, done the test for equality. So if carry is set, then the user extent is larger. Therefore, we're actually looking at a position after the end of the file. Otherwise, user extent must be smaller. Okay. D6B. And it's still zero. Here, user FCB. Call find first. Okay, we're here. We fetch the user extent byte, which is zero. Stash. Copy the dear end. So that is this loop here. This BPL is the end of the loop. So we want to stop at one line D0. Okay. Our dear end is at one D6B. So there is a one. Set bit seven of S2. Done. Compare the user extent byte with the dear end. Wait a minute, wait a minute. Temp B is the user extent, not the dear end. The one in the FCB is now the dear end extent. Right. So this should be carry clear. Here. So after the comparison, carry is set. This STA is supposed to be setting the actual extent count, which means that after this comparison, the record count in the dear end needs to be left in A. So I am going to have to swap these over again. So FCB minus dear end, if the carry is set, the user extent is larger. I think I'm probably going to go through every single possible combination of this stuff. This kind of logic wrangling is something I'm extremely bad at. It's all part of being no good at mental arithmetic. I suspect it's a small touch of dysnumia. So carry is set. And Z is set, indicating that the two extent bytes are equal. So we should skip over all of this stuff and go down here. And we set the record count to the extent that is very wrong. Very, very wrong. That's the core problem. This should actually be the record count from the dear end. And we only need to update that if we're overriding the record count because the user extent does not match the dear end extent. That is, the dear end has two extents per directory entry on our particular file system configuration. So the directory entry we're looking for actually contains extents 0 and 1. So if the user asks for extent 1, then we, but the directory entry says 0, then we know that the user is trying to look at data that's not in the directory extent. So we go through here. Likewise, if the directory shows 1 and the user's FCB is asking for extent 0, then we know that the extent must be full, therefore we go through here. Now I am not at all sure any of this is still right, but I think it is more right than it was. So let's do this one more time. Okay, we are now here. So if we look at our extent, which is at 1d6b, there's a 1 in it. Good. And we should go back up to here. So we figure out the address and we go through read sequential. So compare the current record, which is 0, and the record count, which is 1, because this file currently only has one record in it. Right, we're here. Get the disk block value in XA. So get the FCB block index. Get the current record, which is 0 at the beginning of the file. Get the block shift value, which is 3. Shift it right three times, still 0. Are we a big disk? That's 0, we are a small disk. So we will skip over this to double the index. Right. Add on the allocation map opposite and put it in Y. That seems to have worked. So we are here. Are we a big disk? Well, we know that we are not. So we skip down to here, where we load the block number out of the FCB, which is a 2, that is the right block number, and the high byte is a 0. We are here. Stash in the current sector, and that should be current sector plus 1. This is to check to see, we're just oring the bottom two bytes together to check to see if they're 0. So what's this come up with? It's not 0. That is accidentally right. So let's just move on. We are here. That sets the top byte of the sector number. We now are shifting the sector number left three bits, which is at 1D9A200 to get the sector number. So shift, shift, shift, shift, shift, shift, shift, shift, shift. 1D9A, this is now our sector number. Add on the record number, we fetch the current record, and it with the block mask, it's a 0. So now we do a three, this code adds a 8-bit value to a 24-bit value. Add, store, carry is not set, so we go down to here. Why is still pointing at the current record? So we want to increment that. Done. We now actually want to do a read at the user DMA address, which is 0.8, which is 1E00. So let's... Okay, we're now in the BIOS. We didn't really want to be in the BIOS, but that's where we are. So this is the set DMA call. We are now here, so we're going to read the sector, and it should go in, end up at 1E00. And everything looks fine. Let's dump 1E00. Yep, that is our CCP record. That worked. Good. So let's see what happens the next time through. We also want to put the CLC here to indicate that everything is fine. There is no error here. So everything is fine. So we're here. We're advancing the DMA address. Set the top bit. That would have worked better had I actually written it back. And go back to the loop again. Fetch the FCB. We're here. Compare these two values. We should now not take the branch. We haven't. We hit the end of extent error. Excellent. Everything has worked. That's quite unusual. Okay, so we have... Well, end of extent is... So at this point, we want to look to see if there is another extent. So we actually want to... Okay. The record count in the FCB must be 128, indicate that this FCB is full. If it is not, that's in Y. If it's not full, then we know that there cannot be any more data. So, and we now want to return an EOF error code. So that's 1, EOF, set, carry, return. So this is now going to become BEQ EOF, and this is going to become BNE EOF. So we will hit this line here if we want to get to the next extent. So what we will do is increment the extent number, reopen the file, and continue on. I'm not going to do that now. So, CCP file read. It has successfully read the whole file into memory. Excellent. So, at the beginning of the file, at the beginning of any executable in our system, we have the four-byte header, which contains the number of zero-page bytes in use, the number of TPA pages in use, including the BSS, and the offset in the file to the relocation table. An executable will then be followed by a jump instruction, and we are going to patch that jump instruction with a pointer to the BDOS entry point function. It's the same model that we used for the BIOS entry. Use capitals, because this one is important. We want to compute the location again, but we're going to do pointer stuff. So we're going to put this in temp. So the low byte of entry, this is going to be five. That's the fifth byte. The low byte of entry goes into high byte of entry goes here. So temp is now pointing at our program. So to relocate it, we just call the relocate routine in the BIOS with the appropriate address. ldx, temp plus one, lz, y, locate, jsr, call the BIOS. Okay, that will relocate it. Now we want to execute it. So we need to add on the offset to the program entry point. That's here. So it's five, six, seven, eight, and jump. Okay. So this should land us in the CCP at this line of code. And the first thing it will do is exit. There is no code in here. So we have the com header and an exit. Let's try it and see what happens. So skip forward until we reset the disk system. Open the file. Read the CCP into memory. So we read a record. Did it work? It worked. Read the second record. It did not work. And A is one indicating a end of file. Get the BIOS entry vector again. So that is in the address here at 1E00. Here is our program, the vector. So now if I look at 1E00, we see that there is an address here. Good. Call relocate. And we should see that... In fact, I don't think anything will... There is one relocation of a vector at B. That's... Yeah, that's here. This is a jump instruction that's going to that BIOS... That BDoS entry point vector that we just set up. And that's the only thing that gets patched. So if we... Let's see, it says 0400. This should turn into 041E. It has. Okay, so we now advance our pointer and jump into the BDoS, the CCP. 01234567. Anyway, we were at 199F. Break 199F. Continue. Here we go. And we're in the CCP. We have loaded our first program. So all we do is we call the exit program entry point, jump to the beginning of... Jump to the BDoS entry point at the beginning of the program, which in turn has been patched to jump to the actual entry point in the BDoS. And then we have hit our BDoS entry and we print the message and halt. Good, good. In fact, what that particular entry point will do is call exit, which will load the CCP again and execute it again and do this over and over. Let's put this up here. Actually, no, let's not put that there. Good. So the CCP is not supposed to exit. Even on... Actually, actually, I bet that when you type control C in the CCP, the way the CCP reloads all its disks... Yes, yeah. When you press control C in the CCP, it does exit back to the BIOS on normal CPM80, which reloads the BDoS in the CCP of disk. So if you have two different CPM versions on different disks, you can change disks, press control C and get the new one, and you don't even have to reboot the system. So let's commit this. Okay. And there's more to do... I mean, there's a lot more to do like the rest of the operating system, but I think that makes a good place to stop for now. Next time, we'll actually implement this BIOS entry point, BDoS entry point, sorry, and write some CCP code. And just thinking about sizes, our BDoS is loaded at 1900 and the CCP loads at 1.00. So we are 1280 bytes. That's a little bit over a kilobyte, which I think isn't bad. We can chop some stuff out like the debugging routine. But yeah, I think that is pretty good. Okay, until next time then.