 I have a new computer. Let's hook it up and see what it does. It seems to think it's an Apple II. This is an Olimax Neo 6502. It's one of the new line of modern designed retro style computers. I covered their Aegon Lite previously on this channel. While the Aegon Lite is built around a Z80 processor, this is built around a 6502 processor. The Neo 6502 is a much simpler system than the Aegon Lite. The Aegon Lite has a EZ80 system on a chip, hooked up via a UART to the ESP32 processor, which provides all the IO. This operates similarly. It's got a 6502 here, hooked up to RP2040, except while the EZ80 is a computer in its own right. It's got its own RAM and a whole bunch of peripherals. This does not. This is just a bare processor with nothing else. Everything that the 6502 here sees is emulated in software by the RP2040. So RAM, peripherals, video, the lot. This makes this quite an interesting system because the RP2040 can be reprogrammed to emulate the hardware of various different systems. So this whole machine can, for example, emulate an Apple II with a real 6502. Of course, the RP2040 is so powerful that it would be feasible to simply emulate the 6502 in software. So you wouldn't need this chip at all. But if you did that, you wouldn't get a real 6502 data bus, which is exposed here. There are only really three chips of interest on this board. There's the 6502, which is actually a 65CO2 with a whole bunch of added quality of life instructions, which make it so much nicer to work with. The RP2040 itself, which has got built-in peripherals and built-in RAM, 192K, if I remember correctly, and this two-megabyte flash chip, which is what the RP2040 runs off. These three chips here are just level shifters. The 6502 runs at five volts, and the RP2040 runs at 3.3 volts. Me from the future here. Okay, so that's not quite right. I mean, they are level shifters, but they're not being used as level shifters. Apparently they're multiplexers instead. This means the entire board runs at 3.3 volts, which honestly surprised me. I didn't know you could get 6502s around off 3.3 volts. This does make the board a lot simpler, but the downside is that the expansion port runs at 3.3 volts, so you can't use five-volt peripherals like the range of devices for the RP2014. Anyway, moving on. For peripherals, you get HDMI, which is also bit-banged in software by the RP2040. The RP2040 is doing a lot of heavy lifting in this system. You have USB provided in hardware by the RP2040. This allows you to use, like, USB keyboard and mass storage. Audio out and a USB-C connector that's just used for power. This connector provides raw access to the 6502 bus, allowing you to hook up peripherals to it. It is completely hardware-compatible with real 6502s because it is a real 6502. While this interface is Olimax standard peripheral interface, it's got SPI on it. They make a whole bunch of add-ons that you can get that will work on this and a bunch of other systems as well, such as SD card storage. But I don't have that. I'm going to use the USB storage instead. The Apple II screen I showed you earlier is because the RP2040 here is currently loaded with the Apple emulation firmware. And just to repeat, it is emulating the hardware, not the CPU. There are a few other emulation packages in progress. There's a C64 one, although I will be interested to know whether this has enough spare CPU bandwidth to also emulate a SID chip. That will be interesting to see. But I'm not actually interested in the Apple II firmware. I'm going to be using a different set of firmware, which I shall fire up now. So here is it all hooked up. Let me just plug it in. This is Paul Scott-Robson's Neobasic firmware package. This is a abstract system that's intended for writing games in basic. It's got a decent structured basic. It emulates no particular real-life 6502-based machine. Instead, you get a system with lots of useful functionality for doing stuff like hardware sprites, graphics, stuff like that. Hardware sprites is, of course, all implemented in code on the RP2040. It uses a USB keyboard for input and uses a USB key for storage. It is a bit fussy about what keyboards, what hubs and what USB sticks it uses, but I have a set that works. The firmware for the Neobasic 602 in general is pretty early. There are rough edges. Reprogramming this thing was really easy. All you have to do is plug it into a PC with the boot key held down, and it becomes a mass storage device. You just copy a UF2 firmware file onto it. Because it's an RP2040. Same chip that's on the Raspberry Pi Pico. I love this thing. It's got a really cool set of functionality, a SDK that is both well documented and actually works, and lots of quality of life stuff to make it easy to work with. So what do you get with this? Well, I'll give you a few demos. This is loading programs off the USB stick, and it's configured for a USB keyboard. The keyboard a bit further up. So let's try this. Let's load it off the USB stick and we run it. And this is a simple demo showing how, well, it's Frogger. It's not actually playable, as far as I can tell. It shows the use of sprites and just generally what kind of graphics are available. And if you're wondering why I am filming the screen rather than using a hardware capture device, it's because I can't be bothered to set it all up. And I really don't fancy typing into this thing while looking at OBS Studio on a screen. I have an HDMI splitter on order. So other stuff you get are, there's a playable Galaxians game. I can remember what the fire key is. L, obviously. These are all the quick demos hacked up in basic. So I can list the program. There it is. You can see some procedure definitions, nice features like syntax coloring. You can go through, you can cursor around the screen and edit stuff. So if you're used to the C64 or Commodore Pet, that will work well. You can also load machine code programs with syntax like this and then run them. So this is written in Pascal and compiled on a PC and transferred over to this system. So you can run machine code stuff, which is nice. And that is what attracts me to this thing because what we have here is a 6502-based system with as much RAM as possible. It's got a full 64K of RAM with only a handful of bytes at the top that's used for the interface that talks to the RP2040, which means it's absolutely ideal for a CPM65 port and also appear to be kind of typecast doing that kind of thing now, but never mind. The other great thing about this firmware package is there's an emulator. Here we are on the PC. Let me fire up the emulator. And here we have a NeoBasic system running in a box. It has file system access to the host file system. I should point out this is not emulating the Neo6502, even though it says Neo6502 emulated in the title bar, it is emulating the NeoBasic firmware package. So there is no ARM emulator in this. The API implemented by the RP2040 is simulated and the 602 is emulated. So this is why being able to catalog the disk shows the contents of the host file system. So I can run and emulate anything that runs on the 602 side of things and it just works. But if I'm going to port CPM65 to this, this is gonna have to be a lot more complicated. So let's talk about memory. Now the virtual machine that the NeoBasic firmware package emulates is very simple. RAM starts at zero and it goes up to the end and it stops. There is one item of interest in here, which is the mailbox used to communicate with the outside world. It is apparently configurable, but I haven't figured that out yet. So I'm just gonna leave it here. But of course, by the time we actually get to do stuff, BASIC is running. Now the firmware package loads BASIC at, I believe, this address and it goes up to about here. There is also a small kernel which lives just above here. So where does that actually end? FF1O, so however, none of this is actually in ROM. It's all RAM so we can just overwrite it. So the way I've got it set up is our program starts here, but we don't want it to be there. We actually want it to be right up at the top of memory. We could just load it there, but that's a little bit complicated because as the program has to be aligned at the top of memory, then every time we add something to the program, it will get a bit bigger, the load address will move down and we will keep having to change the address that we load it at and run it. So what we're gonna do instead is just relocate it. I have boilerplate in CPM 65 for doing this. So let me just go and do that. Well, that was an annoying Faf, but it was mostly fiddling with the build script. Because we are putting stuff at the top of memory with the program working down, this gets on very badly with the linker that's part of LLVM Moss, which prefers to start at the bottom of memory and work up. So we have to do this mad dance where we link it first at a particular address I picked C1000 for that. Then we use that file to figure out how big our code is. And then we link it again, passing in the size of the code so that the linker script here can calculate where to put everything. But we should now have it working. We have a 256 byte object, which is interesting. I'm not gonna work. Okay, I think that is more accurate. Over here, I'm dumping the symbols, which means I can actually see where it's placed, everything without needing to start the emulator. So we now have the load address that is where the unrelocated data is at 8021, the exec address, that is where it's supposed to go after it's been relocated, at FE9. And we have BDOS start, which is the actual routine we're calling, is at the beginning of the code. So we should now be able to run this. Now we brave and we get a cue. Great. We can start writing code now. Now I'm actually intending to do something a little bit different from the previous CPM65 ports. The way CPM normally works is the BIOS defines a low level interface for doing stuff like reading and writing sectors from disk. However, the Neo6502 has the file system implementation in the RP2040, so that there is no low level sector interface instead. The 6502 processor, whatever code is running there, calls out to the RP2040 via one of these system calls, and that does the file operation on its behalf. So if I were to do a low level sector interface, then either you'd have to put the whole disk image in a file on a FAT file system, which is not very great, or I would have to go and add to this list of system calls the ability to read and write individual sectors from the USB stick, which is also not great. So what I am going to do instead is rather than do a normal port, I am going to implement a interface layer so that instead of having a normal BDOS and a CPM file system, when user programs call out to the BDOS to do file operations, those operations get translated into FAT file system operations through the RP2040. This should allow normal CPM65 programs to run on this, but all their files are stored on the FAT file system, which would be so much more convenient. So this is actually going to be a little bit different from the other ports. Normally, CPM contains a BIOS and the BDOS, which is standard, I've got the source code for the BIOS right here. This, I have a very long series of YouTube videos where I write this and it's mostly correct. And the BDOS calls to the BIOS to do all the work, but instead what I'm going to do is reimplement this Now this looks like, as it's like 3,000 lines of code, that looks like it's a really terrifying amount of work, but actually nearly everything here will be trivial because nearly all these functions either won't do anything on this system, for example there will be no allocation bitmap or will just call out to a host file system operation such as, you know, delete file, read, write, etc. And all this is going to be combined into a single loadable thing which is this. So this is going to get quite a lot bigger. So what we're going to have to do is have both a BDOS jump table and a BIOS jump table. So let's just add some boilerplate for that. Okay, that's the boilerplate done. We have a simple BDOS entry point which this is the thing that programs will call to actually, let me change the name of that, BDOS entry point. This is the thing that programs will call to do stuff and here is the big jump table and here are the stub implementations of everything and down here we have the BIOS implementation, the jump table and the entry point are actually provided in common code so all we have to do is stub out these and provide an empty device driver chain and it builds and we have a program that is 364 bytes long most of which contains jump table. So we're going to have to make it do something. So the first thing is console.io. Up here we have a simple thing to actually print a character. We need to actually hook that up to the device driver system and then give it a try and see what happens. The device driver system, if I load up the ORIC BIOS is a linked list of devices each of which have a name. This defines a device driver called TTY with this ID, this strategy function and this is the next thing on the chain rather and here is the strategy routine for doing it and here are some functions for actually doing input and output. So copy this code intact, copy all of that, change this to zero because there are no more device drivers on the chain. That now fails to compile because we need to define TTY const, TTYcon in, TTYcon out and it builds. So now we just need an implementation of, let's start with TTYcon out. We've got this up here. The input character is in A so we get rid of that and we don't need anything else. So up here in our BDoS start we can simply do TTYcon out. Let's actually change that to a W just so that we know that things are different. Let's be brave and just run it and what do we get? We get a W. It works. So I'm actually going to do some cleanup. There is the magic constant here that I want to factor out and I'll just implement the rest of the TTY. So I have implemented TTY, I have a little routine here that will just continuously read a character and then echo it. The actual routines were simple but then I realized I wanted to factor stuff out so I did that so now they're even simpler. I've added these helper functions for making calls to the other processor and notice here that I'm waiting for the command before I execute the command. That's because if you don't care about the result of the command then the 6502 can continue executing code while the RP2040 is actually doing something. In the cases where you do want to look at the result we have this that just calls the function and waits for the result. So let's just fire that up and see what it does. Whether it actually works or not. I think that's working. More BIOS stuff. Well, most of it's the disk interface. We can implement these functions because these are just boilerplate. I'll actually just do that now. And here we are again and that took rather longer than I was expecting, like several days longer. You know how when you find like a loose thread on a sweater and you pull it and everything starts to unravel? Yeah. Anyway, I have now split the single BDOS file up into lots of files. All of these here. So it is all nicely modular and with proper interfaces. So if you want to replace the file system, all you have to do is swap out this file system file and you're done. So I've updated our combined BIOS BDOS to do this. So all we need to do is initialize the BIOS drivers, which is another standard piece of code. And then we jump to BDOS core, which is the main routine of the BDOS and all this does is do some initialization and then call BDOS exit, which counterintuitively is the place where the BDOS starts up. The exit here is referring to applications exiting. So this will then reload the ccp.sys into memory. It's a bit complicated as it's got to go in the right place and jump into it and everything is good. So let's fire it up and see what it does. Where did I put that? That one. So sys8000 couldn't open ccp and it holds. That is absolutely correct. That message is coming from the BDOS core. No, it's not. It's coming from BDOS exit and is here. It's doing that because it has tried to open the BDOS ccp. It's tried to open the ccp, which should be in a file called ccp.sys and not only is it not there, but our implementation of BDOS open file does nothing. So let's add some code. Now, this is our stub BDOS. And here are all the things that we need to implement. Let us put this in each place. So what this does is it's a magic opcode which causes the emulator to break into the debugger. And we want this so that when we run it, then whenever we call one of the BDOS functions that we need to implement, it nicely drops us into the debugger and then we can just look up what the address is at 8024 with .elf8024, I think it was. This is a reset file system. So now we're going to start writing code. So the way the firmware interface works is this file defines all the entry points into the RP2040's firmware. So what we want to do is to find the file access ones that are here. Now, this is actually a pre-release version of the firmware that has a whole bunch of file access primitives that I added. So here you can see this is group3 function for open a file, close a file, seek, tell, etc. And we're going to be using these for most of our file access. There's still some stuff that needs to be done in particular for dealing with directories but we're going to start with this and try and load a CCP. So reset file system. In the original BDOS, what this is supposed to do is to mount the file system. This is already done by the RP2040 firmware. So all we want to do here is just make sure all our files are closed. And that should be a reset file system done. Now I know that beginSys and endSys, these are for housekeeping. They're called by the BDOS whenever a system call starts or ends. They're used by the original file system. We're not using them so they just become an RTS. So build, run, let's see where we get to. OK, 8031. Interesting. Every now and again a file, a command vanishes from the bash command history and I don't know why. OK, open file. So that is our next one actually. We're going to do login drive. Login drive is easy. Login drive is used to tell the BDOS that a disk might have changed and it needs to remount the file system on that disk. But we don't have to worry about that because all that is handled by the RP2040 firmware. Open file. Right, now this is the first really annoying thing and we're going to have to do some design. So it turns out that CPM refers to files using a structure called an FCB which is defined here. An FCB is a user side structure allocated by the application that the BDOS puts all the information in that it needs to access a file. Because it's allocated on the application side, this means that the BDOS can have as many files open as it likes provided the application has created FCBs for them. To put that another way, the BDOS does not allocate any data for open files because the entire state is in here. It's 32 bytes long plus a bit. There's 32 bytes that represent the directory entry on disk plus three, four extra bytes that only exist in memory. The reason why it's like that is so that the BDOS can just load a directory entry of disk into the FCB and then it's allocated. It makes life so much simpler. The first few bytes contain the drive and the file name. Then there is some internal housekeeping data. Then there is more internal housekeeping data. These four are used to determine where in the file the seek pointer is while the 16 bytes from here represent the allocated blocks for this chunk of the file. This is all like exposing so many low level details of the underlying file system which are not going to be valid for us. So this is only going to work for programs that do not try to access internal details of the FCB. But luckily nearly all the CPM 65 programs I have written so I know they don't do that. Though this is changing, we're getting people submitting programs which is awesome. Unfortunately, because these structures exist in the application side of things, the application allocates them. Because all the state the BDOS uses for accessing a file is in this structure. If the application just discards the structure, that's equivalent to closing the file. What this means is that in CPM closing files is optional. You only need to close a file if you have been writing to the file. If you're just reading from it, you don't have to and nobody bothers. Which is a bit of a problem with a system like what we've got here because the host file system does allocate files when you open them. So we have a set number of files that we can have open on the host and an unlimited number of files that we can have open in CPM. So we're going to have to close and reopen files as needed. So that if a program exits and it hasn't bothered to close any files, we need to be able to automatically close them as needed as applications open new files. So what this is going to happen is that we need a map between FCBs and actual files, file handles. So we are going to have some structures. Let me see how this is going to work. Every time we access a file, we are going to have to look up the file name in the FCB in a table to see whether that file is open, whether it needs to be reopened, and what the file handle is. There is a problem in that the application is allowed to move FCBs about in memory, so we can't just use the pointer to the FCB. We actually have to look at the data. So let's define what our table is going to look at. We can have eight files open at once. This is a limit in the RP2040 firmware, although it can be changed. Okay, let's define our table. So this is okay. And up here when we reset the file system, we are going to have to do some initialization. We need to mark all the files in the table as being unused. Okay. So that brings us down to open file. On entry, the pointer to the FCB is in param. And in fact it will be param for every routine which wants an FCB. So what we want to do for open is this. And here we want to be able to walk through the table, comparing the file names until we find one that matches or we reach the end of the table. This is an annoying but mostly just brute force piece of code. So let me just get this out of the way quickly. Okay, now I did have to make a few adjustments. I had forgotten that the file handle is actually allocated by us. We give the back end a file number from zero to seven. And then it has eight slots for files, so we don't need to store it. We can calculate it from the position of this file name in the table. But we do need to keep track of whether it's in use or not, and that's what flags is for. Here is our get file handle code for each file in the table. We check to see if the drive matches, then we check to see if the user number matches. And then we check all the bytes of the file name one at a time. And then we check to see if the file is open and if it is open, then we use this file. If it's not, we go on to the next one until we run out. So let's do some stepping through. So here we are. Our FCB is in param, which is at four, zero, one, two, three, four. So that will be F8E9, F8E9. So drive one, there is the file name, and the rest if it doesn't matter. Of course, there is nothing in our table currently. So let's step. After stepping through, it does actually seem to work, at least for the trivial case when the table is empty. So we reach here. We know that the file is not currently open, or the FCB that we're interested in does not have a slot in the file table, which is not quite the same thing. We need to allocate a slot. Now, eventually we're going to have to worry about evicting files. But for now, all we need to do is skim through looking for an empty table entry, which we can do with this. Okay, I don't really like this code very much. It feels that what I'm doing here is rather similar to what I'm doing here. But this is doing a comparison, and this is doing a copy. There is almost certainly an easier way to do it. For example, we can construct a file table entry in static memory, and then do a simple byte comparison to try and find the one that matches. That would be smaller. Let's leave it like this for the time being. So we should be here. Well, we've looked onto the table and we haven't found our file. So we're now going to look for an empty slot. So we get the flags for the first slot, which is a zero. This particular table entry is empty. So we start copying stuff in. And we've reached the end here, and here is our constructed file table. And then we go back up to file copy. You know what? I'm going to change this. Okay, that is shorter and much cleaner. So here we construct our template. Here we look for a slot that matches. Here we find an empty slot. Copy our template into the empty slot. And the eviction code is going to go here. Let me now, once again, step through this and see if it works. Well, it didn't work for a sort of reason to stupid, but it does indeed now work. Well, as much as I tested it. So we have found a matching slot in the file table. We now need to actually open the file by calling the firmware interface. The firmware interface. So we want to do open file handle. And the parameters are the file handle, cp. We put file handle in parameter zero. A pointer to the file name as a Pascal string in parameter one. And some flags in parameter three to indicate that we want to open the file for read and write. And our function is func file open. New file func blocking that will then give us a error code if it didn't work. So the thing we're missing is the file name. And I think I have made a bit of a mistake. So our file tab here contains a expanded cpm file name like that. We want to produce a dos file name. And more than that, we want to add on the drive and the user. Or that would work better actually. We could do the conversion here in the open call. But anything that's going to need to work with a file name is going to need the dos file name. So I think that we would have been better off converting the file name in the file tab. So that we can just point the backend routines at the file name thing in the file tab. That would make so much more sense. Okay. File names are of the form initial backslash drive and user. And they are Pascal strings, which means they are prefixed by a length byte. Then we have the file name at one. And I'm going to have to change all my code again to do the conversion. Well, naturally the new version is dramatically simpler than the old one. We don't need to store any of the stuff like the drive or the user number or flags byte because they are going into the file name. And the file name length at the beginning can double as our in use byte because zero is not a valid file name length. So I've changed this to return the file number in A and the offset into the file name table in X. So we compute the address of the file name, reset the flag, and now we call the thing. So it's the first time we're actually going to call this routine. A and X are zero because this is the first file. So compute the file name. Let me just put the, this is the mailbox. So F7C4 should be the address of our file name flags. Let me just check the file name F7C4. And there it is. And it doesn't work. Okay, I stepped over the bit of code that actually did the work. So if we go take another look at our mailbox, we can see it's finished because there's zero in group. But Erno is zero. It thinks that worked. That should not have worked because there is no directory A. I am just going to put some tracing into the emulator because I think that will be useful and then I will report back. Well that was awesome. The emulator was mostly working correctly. However, firstly, the number I had picked for the flags was wrong. I had picked three from memory and I needed to pick two. So read write create was trying to create a file for read write access. And that was working. But DOS file names, backslashes, there is the file that got created. So the real hardware is using the ubiquitous FATFS library that I'm pretty sure handles forward slashers. So let's just put forward slashers everywhere in here. So that means I need to go to here and change these two forward slashers. Those are the only two. And there is one emulator bug that I do need to fix which is it hasn't put this file in the right place. Everything should go in the storage directory. So I will go away and fix that because I wrote this file, the file stuff in the emulator. So, you know, my responsibility to make it work. And then we should be able to very easily fix up open file to actually work. After fixing the emulator, I ran it again and now you can see there is an error code there 01, meaning that it was unable to open the file. But now let us do, we should really be down casing this stuff, but let's not deal with that right now. Let's create the file and run it again. So result success down there. The emulator opened the file and in the mailbox, there is a zero that worked. Great. So we have created the file. We now need to check for an error. And if an error occurs, we need to return the correct thing. Okay, so now we know the file is open. Wait a minute, wait a minute. We need to do more than that. So we're preserving X that is the offset into the file table. So that we can clear the size bit, the size bite rather to indicate that this, that the file table entry is unused. STZ, by the way, is one of the 65 CO2s extra instructions along with PLX and PHX. And they make writing codes so much more convenient. Anyway, because get file handle here will have allocated us a new entry in the table that is in use, marked as being in use. And this will mark it as not being used because the open failed. And now we need to initialize the FCB data, which I can find the definition of here. So we want to clear FCBX. That's the extent count. We want to start at the beginning of the file, so zero. S1 and S2 contain, one of them is unused, the other contains the top few bits of the extent count. If you want to clear them all anyway. And RC is the record count, that's the number of records in the current extent. Okay, is that going to work? No, we can't do STZ there, never mind. So now, when we run it, we hit the breakpoint there. Go. And we open the file and we break at F8C9. F8C9 is read sequential. Great, we have successfully opened the file and now the BDOS is trying to read data out of it. We are making progress. I have taken a break, which is why all the windows are in different places, but let's start on read. We have two entry points for read. The reason for this is strictly an implementation detail from the stock file system. For our perspective, they are both identical. So we shall just do that to declare that global because someone is going to try to call it. Right, reading. Well, the first thing we need to do, of course, is to get the file handle of the file we're dealing with. And in fact, I've realized that this is not quite right. We'll fix that later. Then we need, now that we have the file handle in the right place, we need to seek the underlying file to the correct location. So the way CPM handles file position is a little bit shonky because there's two ways to do it. There's one way which involves a bunch of internal fields in the FCB, which is used for sequential reads and writes. And there's the other way which is used for random access reads and writes. And in fact, when you make a random access call, all it does is it converts the random access record number into the internal representation and then does a read sequential. But it's straightforward enough. There are three byte fields, which is the current record, and a record is 128 bytes in CPM terms. The current extent, an extent is 16k, so there are some records within an extent. And there's the extent high byte, also called the module, which is half megabyte chunks. So all we really need to do is to shift all these into approximately the right place. Well, into actually exactly the right place. So convert the file position to an absolute location. And we are using the seek function, which puts a 32-bit parameter starting at CP-param-1. This is a lot of annoying bit fiddling. Well, that's ugly as sin and it's going to be really annoying to test, but it should work. So this has assembled the appropriate call for a seek. So that will be, what's it called again? Funk file seek. And we do want to check the result. You look up the file again, we put all the stuff in it. We want group three, read data is function eight, and it takes a file handle, pointer, and size. So the file handle is already in the right place, so we just need the pointer, which is being set by userDMA. And the size is always going to be 128 bytes. Okay, so if we get to here, then if the error number is set, then this hasn't failed. As in this has failed, so set carry and return. Set carry and return. Otherwise, clear carry and return. And let's put our breakpoint in there, build it, and it doesn't work. Read eight, it should be for read handle. Okay. And now we build it and see what happens. Run it, that is. All right, we've stopped here. And our parameter is in four, zero, one, two, three, four, that's F seven, E one. So if you look at that, this is the FCB of the CCP dot sys. All these fields are zero, so this should just step through and assemble zero in the parameter block, which is at FFO four. Right, that's not going to do anything interesting because it's file handle zero and an offset of zero, so all the zeros. But this is the bit where it's actually going to do the seek. Okay, the log down here says that we've seeked to file handle zero. And the error code is not visible. I think the error code goes there, if I remember correctly. Yeah, this one. So that has worked. Right, we are now going to try and do the read itself. And it now occurs to me that the file is empty. But let's just do it anyway. So this gets the location that we want to write the result to, which should be F six oh yeah. That is computed based on the length of the CCP from the file. In fact, it's trying to read the header of the file now, which is going to be empty. So that's just not going to work. And that's the length eight zero zero zero. Yeah, that should work. Okay, and go. Right. We've read 128 bytes from the file and it's worked. But if we go look at F six zero zero, the result is zero because it's all empty. And then if I continue, this is going to get very confused and probably crash. Yeah. Okay, so let's find the real CCP. It should be in source CCP source plus CCP to storage a CCP dot sis. This is it. This is the command shell that CPM users, the user will interact with this. It is not part of the BDOS so that it can be discarded if necessary in order to get more memory. And in fact, there is one more bit we need to do down here, which is that for sequential read, we need to update the FCB to point at the next record. Let's run that again. So first time through, we know this works and it crashes. What did we load into F six, oh, there is stuff here in F six, oh, so it has correctly loaded some data. If I move on to F six, four, oh, F six, seven, oh, you can see that it is loaded 128 bytes of data. So why has that failed? Yeah, I push here and I do not pop here. So let's give this another go. So go and it has loaded to, yep, that's correct. It's seeked to the right place and it has loaded to where the CCP will eventually go, which is good. So now if we keep doing F five, we see that it then goes into complete nowhere. So I will go and find out what's going on and fix it. Okay, there's just some bad code in my logic here. It is now successfully reading the entire file, but we're not checking for end of file here. So that's not, wait a minute, we should be checking for end of file. When we do the read, it should set CP Erno correctly based on whether it actually read data or not. So here is where Erno goes. So if I step through, you can see that goes to zero. It should change to being a failure when we reach F 400. So let's just skip forwards. F 400, it has failed and Erno is not being set. That is a emulator bug. Let me go and fix it. Fixed. Our error code value is set indicating that the read has failed. So when I go from here, it should launch the CCP and hopefully do something. It hangs. So this is using X as the offset into the file table here. It's incrementing that X got pushed onto the stack here. We pull it off again here, add FT size, put it into X, but we didn't bother to set the carry flag, which means that it might be adding one in the wrong place if carry was set. And I think we're doing that again. No, we got that right here. Fantastic. So it's loaded the CCP. It has tried to open $$$.sub. This is how it knows whether it's running a batch or not. And then it's dumped us to the prompt. Does the keyboard work? The keyboard works. Our command prompt works. It's trying to run programs. There are no programs to run. Let's put one in. Okay, AT basic. It works. Okay, so all we can do is load files. I also see that this is printing an extra new line. So that will be something to do with the TTY. When CPM wants to print a new line, it does a carriage return new line sequence. And I bet what's happening is that the firmware, when it sees a carriage return, is moving the cursor down as well as moving it back. So that the carriage return line feed sequence is doing two returns rather than one. Anyway, let's try to program. Oh, backspace doesn't work. Does control work? No. I don't know what key this is returning. Oops. Ten print hello world list. Nothing. Yes, there is something there. For I equals one to 10. 20 next I perfect. Okay, well, we have read sequential working. It needs some refactoring. This code for converting the file position needs to be pulled out because we're going to be using it again for write. So I will actually go and do that and implement write sequential because it's the same code. It's just doing a write here instead of a read. And then I will come back. Unfortunately, I paused the recording and forgot to enable it so you didn't get the time lapse. But here are the new refactored read and write routines. And you can see just how complex that logic is. So before moving on, we might as well knock out read random, write random and write random filled. These are the versions of the same routines that use the random record indicator in the FCB rather than the actual seek position. So that is these three bytes that just gives a linear 16 bit index to the 128 byte record that you want to read. And in fact, we already have down here in file system, where you seek to random location. This is basically the code that we're going to be using. We just cut and paste this. It converts the R0, RNR1 into current record S2 and EX. And there we are. And all we do is update the FCB to point at the right place and then we just do a read. So that is literally one extra instruction for each routine to call the helper. And that's it. Write random filled is an additional system call. So normal CPM file systems allocate disk space in blocks that can be anything from 2k to I think 64k. And when you write to a given record, it might allocate a new block to put that record in. Normally the rest of the record is filled with garbage, just whatever was on the disk. Write random filled ensures that it's all zero initialized. But because the file system is actually doing the back end code, we don't care. These are just both the same thing, which is nice because that saves quite a lot of space. Now I mentioned earlier that I'd got this a bit wrong. So let's just fix that. I'd forgotten that we are going to have to reopen the file if necessary. So we're going to have to split this up into get file table entry. And then we're going to have to put in another routine, which is going to be, to call this something similar and reopen file table entry. So what this will do is it will search for a file table entry, possibly allocating a new one if required while evicting an old one. And then it will ensure that the underlying file is open. This is because when you do a read or write, you may need to reopen the underlying file because that file has been previously evicted. The first thing we need to do is to get file table entry. And then we just want to do an open. This will always open the file read write. That's fine. CPM always does this. You can't open files read only, but you can mark disks read only. But we're not going to bother implementing that. It might be useful later, but there is exactly zero bits of software that use this. The one thing this won't do is create the file. Up here in open, this is going to be get and reopen. And this does the open. If the open failed, we mark the file as being unused. Then we clear the four FCB bytes that contain the seek location. Open opens the file and seeks at the beginning. In fact, we don't need to call this at all because it will get called lazily when we do the read or write. But we do want to check that the file exists. So down here, this just needs to become get and reopen file table entry. Okay, that should work. We haven't actually done close, so none of this should be particularly relevant. Let's just make sure it still works. No, it doesn't. We have opened the file. I'm an idiot. Well, that was annoying mostly due to sloppy code where I forgot to add on file name lend to various pointers. Now, of course, file name lend is not zero, so that matters. Anyway, here it does now appear to be working. See, it's opened the file here and then it's reading from it. Here is our file table, which contains a single file. That 01 means it's in use. That is it's opened at the back end. So we continue to load our CCP. Now we run basic. It's now going to try to open that. X is 26, which should be pointing at this byte here. Anyway, this should now try to open at basic, which it has into file descriptor two. Here we can see there the slot is now in use. So go. It runs through this code. Of course, every time it needs to look up a file descriptor file descriptor file table entry. So I believe we're now back in the place where we were. So let's just run it. Good. Good. Okay, and they also fixed the carriage return thing. It now just ignores the character that's not what the back end thinks is a return. We should be able to save programs. No, we can't. Interesting. Where are we? F7DO. Ah, right. Okay, we've got a few more to do before we can save programs. We need to implement create file and delete file. Create file acts just like open, except it will create a new file, except it will also refuse to open the file if it already exists. Delete file does exactly what you think it would do. So create file is actually a little bit annoying because we have to check to see whether the file exists before we create it. CPM does this. Unfortunately, the back end file code does not. It's got a create and truncate, which will overwrite an existing file, which is not the same thing. I took a bit of time off to add the remaining file operations that we need to the back end, which I should add. I am also adding the real hardware versions of these as well as just the emulator. So hopefully this should actually work on the real device. And refactored create file. So this will now work. It's just if a file already exists, it will overwrite it. So the stuff for opening the file is actually done by this new routine here that takes the file access mode. So what we really want to do is before we create the file, we need to test that the file exists, which we can actually do using the new dat, that one, that one, the new stat routine. So let me just add file stat 16. So here we just need to set up to do the stat. All you need to do is pass in the stringed pointer to the file and then look at the error code to see whether it worked or not. So func file stat new file func blocking. If it is equal to zero, this means that the stat worked. Therefore we need to fail. However, we do need to convert the file name so that we can pass that in. So get file table entry. The first thing it does is to convert the file name into temp file entry. So we will just pull that out. So here at the top of this, we just need to call convert file name. So up here in create file, we need to call convert file name. And then we need to put a pointer to the string, which is file name len in the parameter block. Okay, so we should now be able to create files. However, there is, before we can actually save stuff from Altera basic, we also need to implement delete, which is well and close. But first thing we're going to do is delete. This should be straightforward. All we're going to do is convert the file name, copy all this code here. Stick it here, except instead of doing file func stat, we do file func delete like so. And this needs to be b, n, e. If an error occurred, return a failure. So all we're going to do is convert the file name from the delete. And we should be good to go. Right. So when we run this, I should actually be able to do del flawed and a raw flawed. Sorry. Del is DOS, error is CPM. And it did try to delete the file using the correct file name. That's not right. That's an emulator bug I need to fix. This thinks it succeeded, which is possibly not right. I can't remember whether that's supposed to return an error or not. Okay. Okay. The basic save. So it deletes the file. It tries to stat the file. That's the stuff we just did. And that fails with an error. It then creates the file. Three here is create and that succeeds. And then we get an IO error. So it has not actually done the thing. It has created a Fnaught.bass file, which is 257 bytes long, which I don't believe is... No, that's not right. That's a basic file from NeoBasic. We want to look in the A directory. There is our file and it is zero bytes long. Okay. It's created the file, but then it's failed to write to it. So that will probably be a bug in write. But I did never actually saw it call write. Well, that's embarrassing. I believe that is all the problem was. By forgetting to load this, the flag here was not set correctly. And therefore it just didn't work. Let's see if there's another go. Save load.bass. Okay. It tried to write some stuff. And we have stopped at F7C7, which I believe is going to be F7C7 close. Right. Now, I remember I said earlier that close was optional for read-only files in CPM. It is not optional for writable files because close on the original file system is the thing that actually rewrites the directory entry with any new blocks that have been allocated, the new file size, et cetera. So just closing the file isn't quite as easy as just calling back end close. We also have to sync the back end file size with the size in size in the FCB. Except it's not quite that simple because a CPM file can consist of multiple extents and an FCB points at one specific extent and then gets updated. And in fact, we are going to need to fix some bugs in, well, everywhere because we're not keeping the record count in the FCB updated based on which extent you're in. So here we are moving to a new extent by incrementing the extent field. The record count field in the FCB tells you how many records there are in the current extent. For any extent other than the last, this will always be 128. If it is the last, then it's not 128. So here we need to look at the current file location and the length of the file to determine whether we are in the last extent or not. And the other place where you can move to a new extent is here. So we will do want to check to see whether the new extent is different or not. Actually, if the new extent or the new S2 is different, then we need to do this. So that should be the new improved code. Whenever you move to a new extent either via random access or via just incrementing and the extent or the module changes, then it will hit this code and hit the breakpoint. It's actually a bit more complicated than I was thinking because if we write a large file with multiple extents and then we move to a new extent, that is, we seek backwards in the file, we do have to flush the record count from the last extent to the file if we're on the last extent because that's the only place where the length of the file is stored. So we have the code to flush the size. I've pulled out the code here, the computer, the seek location so that it can now be used for both the current record, which is FCBCR, and the record count, which is FCBRC here. And then we just truncate it with set size. Is last extent is stubbed out. There's a bit I've missed. Move to new extent does, yeah, we haven't done that at all. So move to new extent and is last extent are going, we're going to need to do those. But let's just see if close works. Okay, we have opened the file, deleted the old file, checked that the file doesn't exist, opened the new file, opened and created the new file, seeked to the beginning, written 128 block of memory, 128 byte block of memory. And now we check to see if we're at the last extent, which we are. That is, it always returns that we are. So we now need to flush the file size. And that's not going to work. Because we haven't been updating the record count when we write to the file. F45B, load the current record, it is one. We have written one record to the file. Now we need to update the RC field, which is in that byte there. So it's currently zero. So we compare it, carry a set, we update it, one. Excellent. And go. We're now in close here. So is this the last extent? Yes, it is. Try to flush the count, compute the new seek position, which gets written into the mailbox. So it's these four bytes here, 800000128, set size. Nothing's peered down here because I forgot to add tracing to that call. But I believe that worked. No, it's still doing it. No, sorry, we skipped up here. We have correctly closed the file. So now we mark the file as no longer being in use in the file table. F5DB, which is file number two, 012fnord.bas. That one there should turn into a zero. It does. Go. Right. Well, we should have saved the file. So let's take a look at it. Storage afnord.bas. And that should be an empty Altira basic file. Right. This is good because it means we can now write test programs in Altira basic. Unfortunately, I have discovered a small problem, which is that when an existing file is opened, it is immediately truncated to be zero length long because we're not populating the RC field properly. Sorry. So this has in fact deleted my old test program. You can see here the logging from what's happened. So it looks like just going to have to do things properly. In fact, I believe it's a bit easier than I was thinking. So working on the bit shifting code, I realized it actually made a mistake with the seek location computation because the extent and module numbers are, they go up to 32. That's five bits. I previously had six bits. So we have seven bits of record number. One, two, three, four, five bits of extent. One, two, three, four, five bits of module. So in fact, the top byte is always zero, giving a upper limit to CPM file sizes of that or 16 megabytes, which honestly is fine for a system like this. This does simplify things slightly because this field is always zero, but it does mean that I'm going to have to rewrite all this bit shifting code. Okay. I think that's done. I believe it's simpler than I was expecting. We have this routine called update RC field that simply sets the RC field correctly for the current seek position. So we just call this whenever we open file or adjust the seek position. So let's give this a try. Okay. This is opening the CCP. So we should see this in action. The first thing it's going to do is let's take a look at the CC, the FCB, which is in F63C. The first thing it's going to do is to compute the extent number from the module number and the extent, which of course should be zero because this is a brand new file. Yes, it is indeed all zero. Then we get the real size of the file, which is not that. Okay. Yeah. I'd use a percent D instead of a percent S for printing the error message. Okay. That's worked. And we here we can see that the size that's been written into the mailbox is two. That's not right. All right. One fix later. And we now see both the message is correct. The size is correct. And we see the size here in 7A07. And it's decided that there are OE records in the file. That is OE times 128 is 1792, which is not the right number of records because I hadn't taken into account that the bottom seven bits of the file length, that is the number of bytes in the record might not be zero. If they are zero, we need to bump up the number of records to take that into account. So we do this with that. I hope. And just like that, I have managed to save and load my first program. That was quite a lot harder than I was expecting. And I by no means expect this code to be correct. There's going to be a lot of edge cases that need fixing for this. But anyway, we should be able to write test programs now. We are making some good progress. Here is the list of system calls we have yet to implement. And some of these are actually trivial. So let's just do those. We have these only make sense when using the actual CPM file system. And this one as well. And that one counts too. So what these are going to do is to simply fail with an error. Okay, reset disk does nothing. Actually, we are going to implement get login bitmap. Get login bitmap is used to determine which drives are currently mounted. And for us, this is all of them, which makes implementing it very easy. Compute file size. What this does is it seeks to the end of the file and writes the random record number into the FCB. And we already have quite a lot of the code for doing this. So here's the compute file size code, which is straightforward. Make sure the file is open. Fetch the size. That gives us the four byte true size. This should be here. This should be 6. We then shift the whole number left one byte to give us the size in records. And then round up if there's a trading number of bytes. Right, so next let's do... Oh, set drive read only. This one is also unimplemented. Let's do rename file. So rename on CPM is a little bit awkward. The API isn't really set up for passing in two parameters at a time. So what happens is you give it a pointer to an FCB with two file names in it. One file name in the first 16 bytes and the second in the second. So the old name is at param plus zero. The new name is at param plus 16. So we have to somehow turn these into strings to pass to the back end. We do have code for doing this, but it will only ever read from param plus zero and only ever write to the temporary storage. But we can cheat it into doing what we want. We do get file table entry. This will convert this name, the old name, into a file table entry. So that we will then be copied into memory somewhere. We then want to save the offset. We now need to add a 16 to param, if zero increment param plus one. We then call convert file name. This will then convert the new name, which is in param plus 16 and put it into the temporary file table slot. So now it should just be a matter of... So I think that should be our code. Let me just stick a breakpoint here. Let's see if it works. Okay, rename the file. The rename command in CPM is a bit weird. You put the new name first, so new file equals old file. Right. It has called the thing. We look at our mailbox, which should have two string pointers to it, which is F506. Yup, that is old file. And the other one is F4DB. That's the new one. So let's see if it works. Renaming storage a old file to storage, not a directory. Yes, it was an emulator bug, which I fixed. Let's see if it works now. Renamed old file to new file. Okay. And we should be able to then remove it. Delete new file. Okay, excellent. Let me just try deleting something that doesn't exist. Yeah, the fact that this doesn't fail is a little bit concerning. That could be an emulator bug. There was a problem with the emulator, which I've now fixed. You notice that this is now returning success rather than okay. Okay means that the emulator thought this succeeded. Success is actually the error code coming from the operating system. The distinction is that I have put in the code that checks to see whether the file exists or not. So despite that saying success, this is actually reporting to CPM that the delete failed, which is then being ignored by the CCP. So that's correct. So we have delete working. Where are we? Find first, find next and set file atras. Set file atras. I'm actually not going to bother with for now. It's fiddly and annoying and not very interesting, but we will look at find first and find next because those are going to be tricky. So the way these work is the user supplies an FCB containing a pattern, which is essentially a file name, which can be something like this. These characters can be either letters or question marks. And that the user number can also be a question mark, but I don't think we can support that currently. I might need to do a bit of work there. And when you call this and then subsequently when you call find next, it will return you the next file it's found matching this pattern. So the way we're going to have to do this is to ask the back end to enumerate the directory and then keep reading files until the pattern matches. So let me think the first thing to do is to convert the file name. We're actually not going to be using most of the file name. We're only going to be using the drive, which now makes me think that maybe it would be easier to construct the file name without. No, it must be fine. This creates a file name like A5 something. So this should now give us a file name which looks like A5. Now we call the back end to start the enumeration. All right. The way this works is we use convert file name to convert the pattern to the appropriate file, but the back end doesn't use the pattern. All we want is the drive prefix, that is the directory that the files are in. So we find the slash here and truncate the string. That number is not right. That should be a three. Zero, one, two, three. We look at that character. If it's a slash, then we truncate it there. If it's not a slash, then that means that the user code that must be two digits there is a slash is one character further on. We then tell the back end to start enumerating that directory. If that works, we then fall through into find next to fetch the next file, rather than the next file name. So what does this do? Well, we want to read the file and then compare it against the pattern. So reading the file is straightforward. We need to give the back end the buffer that we're going to put the file name in, which is going to be, we're going to reuse our temp file buffer for this, because we're not going to be using it otherwise. So this will give us the file name in DOS format in the buffer. We now want to convert this to an FCB. Find next and find first, return the FCB of the file they found into the DMA area, which gives us somewhere to put the file name, which is nice. Okay, I think that might work. Stick a break point there. Let's give it a try and see. It's a bit fiddly because we need to cope with file names that may not actually be CPM compatible. We need to do something sensible with them. So anyway, let's run CPM into a DIR. That will try and do a find first, find next. Okay, we are here. Temp file entry contains the file we've seen. That is F47C, Nord.bas, with the sizes in 47B. Here, two, three, four, six, seven, eight, nine. Okay, so F098 is where it's going to put the result. So read a character, is it a dot? It is not a dot. Have we reached the end of the string? We have not. Convert to uppercase, store, F. Go around again. Dot. We now want to start writing spaces to the FCB until we reach the extension part. This isn't going to work. I haven't actually told it to stop when we run out of file name. All right, we have correctly converted the file name. Here you can see it in our FCB. We do want to set a few other things, such as the drive. However, we know the drive number that is in our pattern. So we can just write that to... We can just copy that over to the FCB. So the only remaining things we have to do are to fill out the four size bytes to tell CPM how big this file is. Now ReadDeer here has given us the length. It gets placed into the mailbox. But we then have to convert this to a module extent record number. But we do actually have code for doing this. It is update FCB for record number. This stuff here does the conversion. We would have to convert the length the back end gives us into records. And then use this routine to convert to module extent record. However, it's probably easiest to do it ourselves. Right, so we have created our FCB. We now need to match it against the pattern that the user gave us to see whether this is a file that the user is interested in. So here is our pattern matching code. Luckily matching FCBs is very easy because of the fixed layout. So OK, so we have the pattern in F1 3D. All question marks because this is trying to do a directory listing of the entire disk. And F098 is the returned FCB. So now we just step through this, which I can predict will work because it's not doing anything interesting. It's skipping all the pattern bytes because they're question marks. So you should be able to press F5 and it reads the file. Now it's on to the next file. And we have a directory listing. It's the wrong directory, but it is a directory listing. This has given us the directory listing of storage rather than storage slash A. So this could either be an emulator bug. It's not an emulator bug. The path being given to OpenDeer is wrong. So this stuff where we edit the file name is wrong. So let's give that a go. Right, we're putting the wrong pointer into the mailbox because I had forgotten that there is the initial in use byte that now exists. Fantastic! There is a directory listing of our emulator disk with lots of nice CPM stuff in it. So we should be able to do dump.com and it doesn't work. Why doesn't it work? It looks like it's read the dump program. So that cannot open file is probably coming from dump itself. It is loading and editing dump, loading and running dump. Let's try beedit. Beedit is a very simple text editor modeled after a basic interpreter. So that looks like it saved one record. There is our test.txt. We should be able to type it. Yup, that worked. I wonder if LS will work. No, but I think most of this is now working. Let's just check the wild card. So let's look for all com files. Yup, all text files. Yup, all files beginning with C. Yup, that's working. We should be able to switch to other drives but I haven't done the bit that creates the directory and also I believe that the CCP has some bugs when it comes to multiple drives so I don't think that's going to work. Yeah, it mispouses the command line and it tries to run it as a program so that's fine, easily fixed. Objdump.com Okay, objdump is written in C whereas dump is written in machine code. In fact, the source file is right here. Aha! Dumping a file that was not previously open works. I have a breakpoint in there so I'll just continue. Lots of hex dump. Dumping a file that is open that is easy in the file table does not. This is because when we call this we then check cperno to see if it's exceeded or not and of course if this does not call the back end to open the file because the file is already open then cperno is uninitialized but that is easy to fix. So this is where the open actually happens. So I think the simplest thing to do is to put this in so as we zero out cperno so that if this is not called then we think things have succeeded. Okay, dump dump.com Perfect, it works. Okay, this is actually looking pretty solid. There are bugs, I mean bugs that need to be fixed and there is one massive missing feature. If I dump a bunch of... Actually I don't need to do that. I want to open lots of files so let's try assembling something. to hex.com Oh, it worked. I wasn't expecting that. I was expecting it to run out of file handles but you can run the program that we just... Yeah, it works. It assembles. What I was expecting is that it would eventually run out of file table entries. Why was that trying to seek file handle 8? There aren't 8 file handles. These are all producing bad command. Right, something has got horribly corrupted. Anyway, what I was trying to say was in order to do anything actually particularly useful with this I am going to have to implement the eviction stuff from the file table which is currently just stubbed out with a byte 3. Where is it? Here. Which means needing to implement a last recently used list to determine which file table entry to evict. So that will be the next thing that he's doing. After a break, let's do this eviction code. We want an LRU, least recently used list. The idea is that whenever we evict a file we want to evict the one that's been used longest ago. This way the most commonly used files will remain in the file table and therefore remain open in the back end while the files that no one is using anymore will expire out of it and be closed. Traditionally these are implemented using linked lists because it's very cheap and easy to pull an item out of the list and then insert it back in the beginning. But the 6.02 doesn't really like linked lists and our file table is just an array. So we're going to do this I think with a simple array in memory which is going to be num files bytes long and this is just going to contain the indices of the file table entries. Filling this in ought to be straightforward. First we need to initialize it. It doesn't actually matter what order we initialize this in. So in fact it's being initialized backwards so that because we're putting the most recently used item at the end that's going to be slot 7. Actually let's not do that. We're going to put the most recently used item at the beginning. Okay so here we update it. We have in a, in pointer plus zero we should have the index into the table. So how are we going to do this? Well here it is. This should if I've got it right update the LRU table so that the index we've just allocated goes at the beginning. Let's give that a try and see what it does. Okay here we are updating the list for the first time. Our table is at F4BB and here you see it allocated nicely in order. I think this will do nothing because we're allocating entry zero. So let's just check it does nothing. Yeah that's doing nothing. Okay let's go for the second one then. Right this is going to try to reopen the CCP. Yeah I'm going to be hitting this code every time we open a file which is not great. We've just tried to open index one and that has failed. So this should now, it should still update the LRU. So this is going to count down. This one is going to match. So we skip it. Now we go for the last one which is zero. That get copied into that slot and then we put the one into that slot. Go. Right. Let's just run a command. Okay. It's now opened copy. So go. And it's reused file table entry one so nothing has changed. Yeah and it does this for every single, there we go. We're now opening the CCP again into entry two. I'm actually a bit surprised at that because we should still have entry zero containing the CCP. So why is it putting it into two? Anyway this should now put two on the front like so. Reload the CCP to go back to the shell. We check for that file again. That will go into slot three. No it doesn't. It found that. So why did that work? Yeah there's clearly something wrong in this code. This bit seems to be working fine but something up here isn't working right. I think we are overrunning one of these buffers. And anyway this bit works. So in order to evict a file we're going to need to load very simply the oldest item in the LiU and we want to close it. Okay so in order to get the file name of this we have to convert the index into the offset into the file table. The file table size is currently 1419 bytes which is a really annoyingly unround number. I was kind of hoping that we could make it a nice power of two but for some reason I was thinking it was below 16. Yes, 20. Maybe that's why we've been getting buffer overruns to be honest. If we could simply multiply the index by 20 to get the offset I would actually simplify some of the logic but that's actually pretty hard on the 602 multiplication. So there's a thing you can do. You factor out your 20 into powers of two. So x times 20 is equivalent to x times 16 plus x times 4. These are both powers of two so they're actually simple to do. So if we shift our value left twice that gives us the 4 store that somewhere in a temporary shift it to two again that gives us the 16 add the two together and that gives us our multiplication by 20 and that is 1, 2, 3, 4, 5, 6, 7, 8, 9 bytes. And it's actually kind of easier just to make a table. We've only got eight of them. We can just do an index lookup of the table. Well, it's pretty vile that it should work so that we can now stick this in an index register look up our vile table offset and put that in x and that gives us the offset to the item that's being evicted. So we can simply look to see whether it is in fact in use. We can then close it using basically the same code as with this. We only need to pass in the file handle. Well, I have found one horrifying bug. So here in close, we do the close and then we mark the file in the file table as not being in use to prevent it from being closed again except the address that we're using is pointing at the wrong place. It's pointing here at the A which is the length field. So for some reason x which in this case is zero is one, two, great. So it's zeroing out the length field rather than the flag but this is extremely peculiar. I know what's going on. I'm overwriting x here. Well, that's not going to do anyone any good. We need to save this. So I will move that to here. We are closing the file. x is a sane value and our table is f3e0 which looks fine and we also do not see multiple ccp.cissers in the table. So we've now hit the break point up here in reset which we'll get rid of and we are back at the prompt and it's worked and it hasn't crashed. Okay, let's try this again. This time we're going to try and evict something. x.lsasm.ex.com It assembles. We're going to copy x.com to... We are here. We are looking at the LRU table which is getting the last slot which is 6 and 6 was just used so that's not right. Why are we writing this to the table of offsets? That is really not going to work. Okay, that's probably explaining why everything is going so crazily. Okay, 6 is still at the end which is very peculiar. So whatever we fixed, it wasn't that. Load the handle we're going to evict which is 6. That is the index in the file table. Put it into y. Load the offset which is 7.8. Okay. I don't know where my head is today. I forgot to update that. Right. Okay, let's give this a go without the break points. Okay. x.com to... Copy x.com to foo. That worked. In fact, because we reset the system the CCP does this when it gets restarted we didn't actually get a chance to evict anything. So what have we got that will actually do lots of files? Right. Beed it. Okay. We now have a big.asm which when we assemble it it should load all the include files which will blow the file table. So we look through the logs. What did it do? Well, we read the assembler. We open the output file. We open the first include file and so on and so on and we hit 7. Wait a minute. Where did all the rest go? It got to 4. Where's 5, 6 and 7? Okay. That's not worked. Here we are at the point where we want to close a file. Here's our LRU. 7 is at the front because that's the one we've just opened. 2 is last. We're going to close file entry 2. It's in use. We're going to close it. File entry 2. That's very much not the right response from the emulator. That is the response you get when you load a file using the system call entry point to load files because that needs to go into A, not Y. Y is used for function codes across CPM 65 but this particular call wants it in A. Okay. Let's give this a go. As in to out close. Okay. Now we do the close. We close file handle 2. Did it succeed? No, it didn't because that's the wrong thing too. I don't know why I'm making all these stupid mistakes today. It's clearly not a good day for programming for me. Do the close. Was there an error? No. We now go here where we're going to fill in the new slot that's now empty with the one from the template. This should now open file handle 2. Yes, with 5.ink. Good. This is working. And then we close 3, reopen it for the output file. Close 3 because we have to close it manually because it's a right file. Then we write out the symbol file which again it's picking file handle 3 which is good. And then we reload the CCP. Okay, I think that may finally work. Yeah, that all looks good. So I think it's now basically done. There's some stuff that needs fixing. One of them is that delete here actually takes a wild card. So the naive approach of just converting the file name and doing a delete on the back end won't work. We're going to have to do a directory listing and delete files that way. Another is that user codes and multiple drives are hopelessly broken. The drives are known to be broken. It's on my list of things to fix. User codes should work because there are actually people who use them. But user codes are wretched in every way. The biggest issue here is that we need to be able to move a file from one user code to another. Set file attributes. In fact, file attributes in general need doing. These are stored in the high bit of the file name. So that's going to be a bit irritating because you have to keep populating them every time we do anything with the FCB. But I think we are now at the point where the next thing to do is to try this on real hardware and we see whether all the work I've done on the emulator actually works. Sorry, all the work I've done... We know the work I've done on the emulator works because here's the emulator right here. But in parallel, I've been adding the functionality for the real hardware. I am very much convinced that's not going to work out of the box. There's only one way to find out. Here we are back at the workbench. Let's us fire the thing up and see what happens. I have a different USB stick because the previous one expired messily when I was trained to use it. And a different keyboard because using a full size keyboard with this thing is pointless and also the weird layout means that I'm much less likely to try typing UK on a US keyboard layout. So, we actually let's... If I do a cat, you can see what's on the disc. Load cpm65.neo to hex8000 sys8000 At least it didn't crash. After several hours of debugging, including setting up a hardware debugger, compiling and installing OpenOCD and fiddling with lots of configuration files, I figured out that the reason why I couldn't open that file is because that file is not actually on the USB stick. I just forgotten to put it there. So, that's nice. So, there are our files. We load the program. The FIS read file stuff you're seeing there is tracing that I installed in the firmware in order to figure out what it was doing. I'll take that out in a moment. Let's run it and we get a prompt. That looks like a cpm prompt and indeed, I can run some basic built-in commands, but it doesn't actually work. So, there is clearly something wrong with my implementation of the file system API that's running on the RP2040 here on the board. So, I'm just going to have to go figure out how it works and I'm not going to bother doing that on camera because I reckon this has been going on long enough as is. So, I am just going to fade to black and then come back to you with news. And here it is. And let me tell you, this took flipping ages. I had to implement quite a lot of functionality into the firmware. And, of course, that introduced bugs and I had to replace the bugs with different bugs, et cetera, et cetera. Plus, I found a fun LLVM bug that was causing some programs to go horribly wrong and had to fix that as well. Anyway, let me just... Hang on, I need to load it first. Let me just fire it up the same way as before. And here it is. As you can see, I've implemented a proper screen driver. We now get a cursor and a delete key and all that nice stuff. Most of the programs work. We've got... This was a contributed program. It's a simple Conway's Game of Life simulation. Well, that was apparently not a very interesting setup. Let's put a dot there and see what happens. Because all the screen handling is done on the Raspberry Pi, it's actually a really nice, quick TTY. So I can, for example... We've got source code here. I can just dump that rapidly. We can compile it. It is super fast. Most of that is because the file system is extremely fast compared to the processor, but the processor itself runs at about 6 MHz, which is good and quick for 6502. It actually runs irregularly. It's clocked by the RP2040, so you only get a 6502 clock cycle when the RP2040 here wants it to do something. The CPM65 has a device driver model that the original CPM didn't, so I can do devices. It shows us that we have two devices installed, the TTY and the screen driver. The screen driver provides fast access to the screen handling stuff that the life program is using. We can install new drivers, so if I can do cap stroke, as it says, everything is now in capital letters. So if I do, for example, free, that prints it all in capitals. If I type a program, it's all in capitals, etc. If I do devices, you can see that there it has installed a new driver. Unfortunately, you can't uninstall them, so I'm just going to have to reboot it. So let's take a look at free again. It's got F-000 bytes of usable memory, which is a lot of memory for a system like this. This is good because it means we can run big programs. I'm wondering if my cow-goal compiler will run on this. I haven't done a CPM65 port of it because it uses so much memory that all the platforms I've targeted so far for CPM65 have enough. But that's a different problem. I demonstrated AlteraBasic. That does work just fine. It's an Atari-style basics. It's a bit odd compared to Microsoft Basic, but it works and it's open source by, I think, yep. And I can dump that. And we have here is the basic program, which is tokenized. What else have we got? Well, the great thing about CPM is it gives you all the tools you need to do all the work you can think of. In this case, an assembler. So let's try writing a program. So we're going to fire up QE. This is a VI-like editor I wrote. It functions just fine. So if we do include CPM65.ink for the headers, start trying to remember the name of the symbols, particularly this one. I think it's BDOS print string. BDOS RTS. Start byte world 1310. And that should be a function in Hello World in machine code. So we save that as hello.asm. We save that as hello.asm. Okay, there's a bug there. I'll address that later. That did work the last time I tried it. So clearly I've broken something. Anyway, here we have our source file. We can assemble this with asm. It's nice and fast in this system. Hello. And there we get the expected output. And I can dump the com file, which as you can see is mostly empty, but hex bytes aren't very useful. So we can dump it with obj dump, which disassembles a com file and shows you where the CPM65 relocations are. So we can see here, this one is where the BDOS entry point gets patched in when you load the program. These two relocations, AX becomes a pointer to the string, which is down here. You can see the ASCII there. And this JSR gets patched up to point at that jump instruction. So when you load it, it gets relocated and it becomes the fixed up image of the program in memory so it will run. Look, it still runs. So there are still a pile of bugs, which I need to fix, as well as new ones I've discovered, like that QE bug that just showed up. I will deal with this later, but this is now a nice functioning version of CPM65 for the Neo6502, which I have to say is a extremely pleasant device to run CPM65 on. It would be nice to have higher resolution, like 80 columns across rather than the existing 53. However, at the top it says screen size, 5229 is the inclusive size, so it's actually 53 by 30. It would be nice to have 80 column text, however, because the DVI output is just bit banged by the RP2040, all you need is an alternate firmware package and we can have it. So this screen resolution is not tied to the board at all. It's just that's what the firmware package is. And I've been looking at ways to change the screen mode on the fly. It's not something people really want to do, so it's not part of the Pico DVI library, so that's what this is using. Anyway, there you have me porting CPM65 to this new device, having one of the oldest operating systems running on one of the newest and most perverse single board computers you can get. It feels very satisfying, at least to me. Everything I've done is uploaded to GitHub. I am upstreaming all my firmware changes. A lot of them have been accepted so far. Now I need to, you know, contribute the bug fixes to the things that I wrote that turn out to be broken. The LLVM bug is fixed. I just need to wait for a release. The CPM65 stuff, I need to tidy up somewhat and finish, and then that will also be uploaded to GitHub. Links will be in the description below, although possibly hasn't gone live yet. So if you have one of these devices, give it a try. Anyway, I am going to leave it there. As always, I hope you all enjoyed this video. Please let me know what you think in the comments.