 I have a new computer. This arrived in the post. I have opened the bag but I haven't actually seen this inside yet and it is a Aegon Lite. The Aegon Lite 2 is a modern retro style single board computer. It's open source but this one was made by Olimax who sent it to me for free so thanks very much. I've also checked the promotional content thing on the YouTubes. It's based around a Z80 compatible processor significantly newer and more sophisticated than the originals. So this is a much more powerful machine than anything from back in the day. So here it is. This is the Z80. It's the processor for this thing. It's a enhanced Z80 processor with a 24 bit address bus, extended op codes and it runs at a blistering 18 megahertz. This I believe is half a mega RAM that is used by this. This is a ESP32. This is a embedded modern 32 bit microcontroller. It's got two processor cores, two megabytes of RAM. Actually that could be this thing here. It's got more RAM inside though. It is honestly a bit of a shame that this is here because this thing is probably capable of emulating this thing. So it kind of defeats the purpose of building an 8 bit computer if you've got one of these but I know why they've done it. Nobody makes a VGA output hardware anymore so it's easier just to bit bang it from a modern microcontroller than it is to find something era appropriate. So there you go. It's powered by a USB-C connector here. I've mentioned VGA output, audio output, micro SD card, assorted debugging, assorted GPIOs and this. This is the keyboard connector. It is not a USB connector even though it's quite clearly a USB connector. The Aegon light only works with PS2 keyboards. It's just happened to be using a PS2 connector so you have to get a USB keyboard that works in PS2 mode using an adapter and plug it in here without the adapter which is annoying. Hopefully I have one somewhere and this I believe is a beeper. Just peel this thing off. There it is. After much labor I do have it all set up in a huge tangle of wires connected up to my video capture so that you can see what's on the screen. Getting a keyboard that worked wasn't that difficult. This is my cheapest and nastiest keyboard in my collection. Yeah it flexes but it does seem to speak PS2. It was broken so I had to fix it but never mind. I did have to upgrade the firmware on the machine itself. This was moderately complicated. You have to upgrade the firmware in both the EZ80 and the ESP32 and you have to do it in the right order because they're dependent on each other and if you update the ESP32 first then you can't run the commands on the EZ80 to upgrade the flash there. This is possible to get around this by installing the right firmware, sorry, by installing recovery firmware on the ESP32. I did have to recompile it using the Arduino IDE and upload new firmware but that wasn't really too difficult and at least now I do have the source code for everything which is nice. So what are you seeing on the screen? Well in ROM it has a basic operating system very heavily modeled after the BBC Micro. The operating system used there is known as MOS by Acorn and this is also called MOS. It uses a, as you can see, the asterisk as a prompt. It allows you to run various commands like the Acorn's cat to get the disk directory. Also the usual set of, let me keep doing LS by Habit, the usual set of directory manipulation routines. These are not Acorn MOS compatible but I'm doing it again but they are there. There's not a lot of functionality here but it will primarily let you run commands from the SD card of which I have two installed. Flash is the flash updater and BBC basic is the basic interpreter and this is RT Russell's superb port to Sophie Wilson's equally superb original of BBC basic. It's a basic interpreter used on the BBC Micro. It's fairly unlike Microsoft basic which people may be more familiar with but it is significantly better. It's super fast. It's got structured programming features and it's got a built-in assembler which is particularly interesting. So let me just find a program. Inside BBC basic if you type a asterisk then what's left over is passed to the operating system for execution so I can still run MOS commands. If I remember I am severely confused about this. The problem is that the keyboard layout here is not like that on the BBC Micro and all my muscle memory is shrieking at me to do this to get an asterisk. Doing a star dot which is the abbreviation for cat on the BBC Micro was that which is very convenient. Here it's shift eight which is more complicated. So yes cap stock is on because all BBC basic commands are in capital letters. Let us load up a sample program like so I can run it. So the way this machine actually works is the EZ80 is basically a the usual headless Z80 machine. It's connected to a terminal via a UART that terminal being the ESP32 and the ESP32 takes care of drawing things on the screen. The EZ80 does not actually have access to the video memory. All drawing happens through sending byte sequences across the UART. The ESP32 here is called the VDP in agon terminology visual display processor. It understands the same set of codes that Acorn used. So here is the basic program that does it. The color commands on line 160 just send bytes down the wire. The gcall command on line 100 just sends bytes down the wire. The draw command on 110 for drawing lines just sends bytes down the wire. This is all it does. It's actually a really nice system and I'm not surprised they're using it here. It completely abstracts all the drawing away from the CPU. So if I just clear the screen I can move to say 100 to 100, draw to 200 to 200. That draws a line. Plot 85 draws a triangle to let me see 0300. There we go. It uses the last two points visited for the triangle. These then that's just the same as print 25 is plot 69 is draw a point. I believe it's been a very long time. Then I have to send four bytes containing the coordinates. So I'm not actually going to do that. There's a built-in command called vdu that should do it for me in an easier way. So I can say 69 and then 300 semicolon means to send that value as a 16 bit value within two bytes 300. So that should draw a point. It does. That is the same as plot 69, 301, 301. Or these numbers aren't pixels, they're in logical units. There we go. 330, 330. There should be 1280 horizontally and 1024 vertically assuming this is using the same coordinate set as the BBC micro. Yes, it is. But I'm not here to do BBC basic things. Well, I am but that's just a side effect. I'm here to do strange programming on this thing. And since I seem to be typeset as that CPM person, let's try and port CPM onto the Aegon light. I can't remember how to get out of BBC basic. That's the easiest way to do it. Now, there is already a decent port of CPM that runs natively on this thing. It replaces Moss as the operating system. So instead, I think it would be fun to try and do a CPM port that is hosted on top of Moss. There are two ways we can do this. We can either emulate the CPM bedoss, which is the file system interface. So I map CPM files to Moss files so that all your CPM files go into the file system on the flash. The other way, which is what I'm going to try to do here is to just implement the BIOS, the basic input output system, and pretend that a single big Moss file is a CPM disk. This is less useful, but it's way faster. But just to make things interesting, a sane person would do this programming on a normal PC and cross compile onto this thing. However, I am instead going to do all the development natively on the Aegon light using the software that comes with it. And remember, our software consists of BBC basic and a flashing tool. So what are we going to do? I want to write machine code and I'm not going to type in long hex numbers. Well, as I mentioned earlier, BBC basic has a built-in assembler and it's a pretty good macro assembler. So let's talk about the assembler. I switched to full screen mode because frankly it's so much easier to edit that way. Plus you get a better view of what's actually happening. Okay, so let's talk about BBC basic. It is a line oriented basic interpreter. Line oriented means that it's got the traditional line based text entry system. So if you're interested in old computers, you've probably seen something like this. The cap slot key is reversed in this mode. So I have just entered two lines. They have numbers at the beginning. The numbers indicate the order of execution. So if I run that, I get stuff. And I press escape and that quits. L dot is an abbreviation for list. Now I can add lines by number anywhere. So let's change this from a nasty go to to proper structured programming. So we add a line five and that gets sorted into order and we change our go to 10 to until false. So we now have repeat until and that should do exactly the same thing. Only very slightly faster because structured programming caches the location of the start of the structure where go to has to scan from the beginning of the program. And we can renumber it like so. Now the assembler is built into this. Let us do some basic assembly. So the first thing we want to do is to tell it where to put the code. So we are going to put the code at a hopefully unused address. The ampersand is how BBC basic shows hexadecimal numbers. The open square bracket enters assembly mode. Then we have a opcode. Close square bracket exits assembly mode. Very simple. We run that and that tells us it's assembled one instruction to address 8000. We can now execute this like so. Because you can hop in and out of assembly mode, you get to use the full power of the basic programming language. You get to define labels. Labels are just basic variables. So you get to do any operation on them. You can do macros because you can just exit assembler mode and call some basic code that generates code. It's extremely elegant. However what we've got here is actually not great. It's put the code somewhere random in memory. So our basic program starts at that address at page. The tilde sign says print and hexadecimal. That's where the program starts. That's where the program ends. So we only have 32 bytes of program. I also have a small time delay from the video capture. That is the top of our workspace. So 8000 is just randomly located somewhere in the middle. Variables get allocated from the bottom up, from low mem. However, procedure calls go on the stack which starts from the top down if I remember correctly. I also might be thinking of the 6502 version of BBC basic rather than RT Russell Z80 version. Anyway, the correct way to do it is we want to allocate a memory area. I'm going to make 4k. That should be enough. And we're going to set the program counter to that address. That dim line with no parentheses just allocates a 4k memory chunk from the heap and assigns the address to code. So now when we run it, we see that it's assembled our ret to 4339. And if you look a bit further up, you can see that low mem is 4320. So this is allocated a 4k chunk in the middle of our variable area. So this is really convenient if you want to embed bits of machine code in your basic program, because you can just allocate stuff and assemble it and call it. And back in the BBC microdays, lots of people actually did just that for writing games. You write your game in basic. Bits needed accelerating. So you just embed bits of assembly into your program. When the program ran, it would assemble it into memory and call it immediately. The downside is that assembly is quite large because it's written out in ASCII in the program text. But it was really easy. And there's some nice features for basic integration. So let me actually just do a thing. So I'm just looking at the documentation over another monitor. Here we go. Reset 10. So this is the I overwrote the wrong line. Let's put that in. That's better. Reset 10 is the MOS system call that prints a character. So the character goes in A. So if you load A with 65, like so, and run that, we get three instructions. If we call that, it prints an A that has sent 65 down the line to the VDP. That's the ESP32 that does the terminal processing and has printed it onto the screen. If you want to press a parameter from basic, all we need to do is to set the A percent variable. Let's set that to 66, which is a B. And now we're going to call not 4350, but 4352. So we skip setting A to 65. And that prints a B. When you issue the call statement, it sets all the registers based on variables A percent, B percent, C, D, E, H, L. So that's great for integration. But we're not going to do that. We want to write standalone machine code programs that can be run from MOS. And machine code programs always load at a specific address. So we don't want to assemble at that address rather than at random location that basic is allocated to us. There's a facility for that. If we set O percent, O percent tells the assembler where it wants the code to be put. P percent is the program counter. So we set P percent, which is the final destination of our program to 1000. O percent to code, which is our output buffer. We then need to enable this mode, which is opt seven, if I remember correctly, the seven is an annoying bit field. That's what you get for having an entire basic interpreter and assembler in 16k. So now if you do that, you see from the tracing that it's assembled at 1000. But the actual code has been placed at code. And I can verify that by printing in hexadecimal the contents of the memory addressed by code, which is a 3E, which is exactly what the assembler says we should have. So that is how you write programs that you can then run on MOS. I forgot an important step. First you need to save it. That's done using a MOS command. So we can say prog.bin. We want to save from code, which is 4361 to 4365. And that has saved our program. Me from the future here. Actually, this doesn't work. There's an issue with star saving MOS is either documentation or implementation. I don't know, but it doesn't work. I've cut this whole section because it's useless. Moving on. Now we want to have a valid MOS binary. And these require a specific header at the beginning. So I am just going to go away and implement that because it's going to be quite boring to watch and besides I need to look some stuff up. So be right back. Well, that was way more work than it should have been. I found a bug in MOS. The star save command doesn't work the way it's documented. I think it's documented inconsistently. And even once I figured out how the parameters work, I couldn't make it do anything other than save garbage. So I have ended up writing my own save routine by just calling the system calls. So I've put the thing into higher res mode so you can see the whole program. This is the basic minimum needed to make a executable MOS binary. It gets loaded at address zero. One of the nice things about the EZ80 is that when you're running it in Z80 emulation mode, you get a complete 64k slab of its memory to work with. So effectively we have an entire empty Z80 to do with what we will. Plus we get to use all the MOS system calls for free, which is nice. So at the top lines 100, 230, that is our air quote program on air quote. It just prints a whole set of cues. Then we have some boilerplate to forward system calls onto MOS. Line 300, we have the magic number used to identify a MOS binary and lines 350 down is the saving code. And of course to call a system call, I just use the inline assembler to produce a chunk of machine code that calls the system calls and so I can run it and it assembles and it saves. So I hit the reset button to get back to the prompt. I load it like so. Let me just wait for the banner to go away from OSSC. There we go. Now I can run it. We get cues. Okay, so now we should try and put some program into our program. So the first thing is that, there we go, paging mode. It will now pause after each page for text for me to press the shift key. So line 90 on, by the way, the reason for the colons is because this version of BBC basic seems to remove leading spaces so I can't do indentation, but I don't think that actually helps. I think it just makes all the lines look like they're commented out. So I will try something else. So what we've got here is a z80 vector table. There are vectors every eight bytes from zero up to something. I can't exactly remember where. Most you only use the bottom few. So when you make a particular system call, the reset command, you can see one being used line 110. It will then jump to that address and do stuff. Execution starts at address zero, which is at line 90. And we only get eight bytes to deal with. So usually what you have here is a jump to somewhere else in the program that's actually got like the main program. So let us put in a main program that's going to go after the header at line 320. So put in an empty line. And lower case. Load a with 68 and call reset eight. Then we want to, we can't actually exit back to Moss. So I'm just going to do a JP zero. And I actually forgot a bit. We need to put in a label at 335 main. So now I have to type the BBC basic commands in uppercase, but I want to use lower case for variable names and op codes, which is irritating. So now instead of actually doing the work at the beginning of the program, I now just want to jump to that address. So that's line 90. So that's going to be JP main. And we want to get rid of lines 110, 120. And we want to adjust line 130 to waste the appropriate amount of space. Each of these vectors is eight bytes apart. Our jump instruction takes three bytes. So we want five bytes. Okay. Now if I try and assemble it, I get a failure. That's because I have a forward reference to two main. This is a single pass compiler. So it doesn't know about forward references. So the way around this is you turn it into a multi-pass compiler. You can see at line 20, I'm setting pass to seven and then using opt pass everywhere. This is because I knew this was going to happen. So all we do is we do 20 for pass equals. Now the magic numbers we want is one pass with option four and one pass at option seven. And step three means that it will go straight from four to seven. So now everything is in a for loop. And we want to wait until we're at the bottom of the program, which is line 381. And just do next. So the automatic indentation has now indented our entire machine code program. You can see I'm actually using a inner for loop to generate all the vectors. Each of these uses a EZ80 extended instruction to make a 24-bit system call to MOS. This assembler doesn't know about 24-bit op codes. So I have to manually insert this magic byte, which is line 180. Okay. So this, if I run it, should work. Right. It's producing tracing. We can see at address zero, there is our jump to main. And there is padding. And then the next defb49 is at address eight. Good. And down at 45 is our main program. And then it saves. Okay. So let's just make sure that still works. Reset cpm.bin run. It does not work. Let me look into that. I used the wrong system call. It should be reset hex 10 rather than reset eight. Okay. So now we're in a good place to actually start implementing our cpm BIOS. So the way cpm works is that there is a platform independent BDOS, which is the operating system proper, which is all of three and a half K. There is the ccp, which is the command process. So that's the thing you actually interact with to run commands. That is also platform independent. And there's the BIOS, the basic input output system that is platform dependent. To do a cpm port, you need to implement the BIOS. And then you just load in the BDOS and ccp binary that you've got from digital research at the right address. And it all works. It's very, very easy. However, the BIOS actually loads at the top of memory. And our program here has loaded at the bottom of memory. So the first thing we're going to need to do is to relocate our actual BIOS code to where it's supposed to live. Actually, that's not quite right. The first thing we're going to do is to just print a banners to say that we've like, you know, started. And there is a system called to print strings. That is reset one eight. So just remember that. So at three one one, we're going to put a not three one one one three one one, we are going to put in our startup banner. Actually, that is going to start with 12 to clear the screen. Then we're going to get a string. Then we are going to a carriage return and the line feed. And after that, we're going to create another variable to mark the end of the banner. Okay, so our main program, we are going to set HL to the address of the banner, set BC to the length of the banner, which is of course, startup banner and minus startup banner set. And now we just to reset 18. Okay, have overwritten the wrong far the wrong line. Let's put that back. So that should actually be line three 15. Okay, now we do any startup. The Z 80 has a very handy memory copy instruction, which we are going to use for this. Where are we going to put CPM? Well, we're going to find some variables up here. In CPM terminology, B base is where the bios lives. I reckon that we're probably going to want to use half a K for this. So we're going to put that F E O O, which is half a K two pages from the top of memory. F base is where the bedoss lives. The bedoss is always 3584 bytes. I am cribbing off my other screen. So that is obviously going to have to go below the bios here. And then there's C base, which is where the CCP lives. The CCP is always two K. So that is going to go to K below F base like so. So we're going to have our bios code assembled into our program at the wrong address. And we're going to relocate it to the right address. And the way we do that is with the LDIR instruction. You set the HL register to the source. So that's going to be bios load address. You set D to the destination, which of course is going to be B base. You set BC to the length, which is going to be top of memory minus B base. And then you do an LDIR. There is more setup we're going to do, but the next thing to do is would be to jump to the appropriate address. And I think that is B base plus three. I would check up on that later. Okay. So they're at lines 450 onwards, we should have our relocation code. If you run that, that will fail at line 450 because we haven't actually put a bios in the code. Yep, bios load address is undefined. Right. So we need to make a bios. This is going to be emitted into our binary immediately after line 490, but it needs to be assembled to live at feo. Oh, this assembler can do this. All we have to do is to change p% to the appropriate location. Then start assembling again, like so. I'm actually going to do a bit of tidying and I'm client here. So if I run that, that should, I forgot to put the actual label in. So that would be 521. Ah, not 521. Okay, what I was going to do was define the bios load address here. But labels are set to the program counter value, which is going to be FF00, which is not what we want. We want to set the label to the load address. And the load address is in 0%. So what we're actually going to do is at 511, we're going to create a perfectly ordinary variable. Okay, so 0% is actually where this is being assembled into memory. It's not the load address in once our binary has been loaded at the beginning of memory. That is, is p% but it has to be the p% before we've adjusted it. So we want to set bios load address here to p%. So line 506, we copy p% the current program counter to bios load address. We then change the program counter to base, but we don't touch 0% because we want all this to be emitted into our buffer. Now, so now this should run. And it does. And you can in fact see machine code at line at address 70. That is our relocation. Then the program counter changes to FF00, which is our bios. And you can see that it is copying two pages worth of data from 007E, which is where the bios will end up after loading to the final address. Okay, let us then add a little bit more program. So in our bios proper, we are going to load a with low kz, reset, hex tend to print it. And then we are just going to halt. That just jumps to the current address. Okay, let's try that. Reset load cpm.bin, wait for the splash to go away because it's right in the awkward place, run. Let me investigate. Because after doing the copy, I told it to jump to bbase plus three rather than the more correct for this particular use case, bbase. All right, that needs to be a jp, bbase, jr is the short range jump. So now when I run it, we get our zed. Good. We can actually now start doing cpm things. So the way cpm bios work is the top of the bios is at a known address that is known to the bedos. It starts with a jump table. So whenever the bedos wants to do something like printer character or reader writer sector on disk, it just calls the known location of that particular system call. And the bios does it. So the first thing we want is to add our jump table. So let me just and the jump table contains just a list of 17 jump instructions. So we're going to start with cold boot, warm boot, check console status, et cetera. I won't make you sit through doing all these. So I will pause the video and go and enter them. So here is our jump table. It's just a set of jump instructions and then a set of labels. There is no actual code there yet. All we need to do now is to flesh these out. And they're all very simple. However, before we actually do anything, I've realized there's an important thing I've forgotten, which is that we actually need to set a stack. As soon as we start making sub routine calls, it's going to want to start pushing stuff onto the stack. We don't know where that's going to be. So let's just put the stack somewhere out of the way. 8000 will do fine. The stack that we're defining here is only used during the startup code. As soon as CPM proper starts, it will take over and put a stack somewhere else. Anyway, now we can run it and I need to use ampersands for that. There we go. And it assembles. All right. CPM system calls. So the first place we're going to start is not going to be the startup code. It's actually going to be the console because it's the easiest. Con out is the BIOS call that actually prints a character. The character is supplied in C, that is the registry. So for implementation, all we need to do is to copy C into A, call the MOS reset to print the character and we're done. Three bytes. Interestingly enough, we could actually fit those three bytes into the jump table proper, but this seems to be quite a lot of CPM code that expects the jump table to contain jump instructions. And we'll read the address out of the jump table and call it directly for reasons. So now we can print characters. Read characters. That is con in at 760. Con in will block and wait for a character and return it in the A register. And there is a MOS system call to do that. That in fact is system call zero. It's called MOS get key. So we now call system call like that. And that returns the value in A. So we are good to go. And I will just put in a little bit of tidying. There we go. Four bytes. Very easy. The next one is con ST, which reads the console status. Now this one's annoying. This is supposed to poll for a character to allow the system to know whether the user is pressed to key or not. Unfortunately, I have not found a way to do this in MOS. I'm sure there is one. I just don't know what it is. So we are going to fake it. We are going to say that a character is never ready. So to do that, we just set A to zero return. Okay. We're missing a couple of important bits. We need to actually initialize and load the BDOS and CCP. So how does CPM start up? Well, CPM has the concept of the warm boot. This happens whenever a program exits. And it causes the BIOS, the stuff we're writing here, to reload the bulk of the operating system, that is the BDOS and CCP off disk. So in effect, every time a program exits, the entire OS resets. You don't actually really notice this happening because it doesn't do anything like clear the screen. And things like the current drive selected is persistent between sessions. But nevertheless, it is happening in the background. That is all handled by the W boot entry point. On first startup, however, then execution passes to the boot entry point, which is the cold start boot. And the way we're going to do this, let me just find out where the code is 741, is on a cold start, we need to initialize some of the state that gets persisted, because of course we can't do that in warm boot because it gets persisted. And then we drop into W boot to load the rest of the operating system. There are a grand total of two bytes of persistent state. So we're just going to initialize those to zero. So x or a is a cheap way to set a to zero. First one is called my O byte. So we just write zero to that. The second one is the drive that goes to C disk that indicates what drive and user is currently selected. And we're done. And here we just fall through to W boot. And I can run that, but that's not worked. Oh, yes, having to find those. So we're going to put our definitions up here. iobite lives at address 0003. C disk lives at address 0004. And there is a third thing we're going to need in a moment called BDOS call, which lives at 0005. So we should be able to run that now. There we go. And in fact, you can see the startup code right at the top of the file there. Okay, let's tackle W boot, which should go at line 750. Right, W boots job is to do a few other tiny bits of initialization and then load the BDOS and CCP. So the other bits of initialization are we need to set up the jump that's used by user programs to call the BDOS. So we do that with reload A with C three and write it to BDOS call. That's the opcode for jump instruction. Now we need to tell it where to jump. This is going to be fbase plus six is the address where the BDOS does all its stuff. So we write that to the address after the jump instruction. And we have now put a jump to the BDOS at the bottom of memory. We now have to repeat that for address zero, which is used to reset the system after a user application has finished execution. This will just call a jump to the W boot entry point. So we put our jump instruction in zero. We then load our entry point here, W boot, and then write that. And what have I done wrong? BBC basic only understands hex in capital letters. There we go. Okay, so that should have initialized the user side of things. The next thing we need to do is to load the BDOS and BIOS. So I said normally this lives on track zero of the CPM disk, but we're not going to do it like that because we're lazy. Instead, we are just going to load it from a MOS file because that's like so much easier. So the MOS load file system core takes a pointer to the file name in HL, the address at which it's to be loaded in DE, and this of course is going to be C base, a maximum allowed size, which of course is going to be B base minus C base. Our CCP BDOS can't be any bigger than that. And then it is system core one, MOS load, reset eight to actually do the work. This could fail. But if it fails, we're stuck. So I'm not going to bother checking for an error code. So what we do next is run it. First, we want to set a to the current value of C disk. I'm cribbing off a different BIOS here. Put that into the C register on the Z80. You can't load C directly from memory, and then go. And that should be our entire startup code. And I haven't actually created the file name yet. So we're going to put the file name after the 981 BDOS CCP file name. And that's going to be called BDOS CCP dot image. And it needs to be terminated with a zero. If I remember correctly, zero terminated. Yes. Okay. So I think we're now in a relatively good place to actually try it. We should see something. It's not going to work because we haven't done any of the other system calls, but it should do something. However, I do need to print a few things. C base goes here. F base goes here. And B base goes here. So now when we run it, it should print out some addresses that we need. It doesn't. Why doesn't it? I think because doing a save instruction inside a program stops the program. Yeah. There we go. So our C base loads at 800. So what I'm going to do now is I'm going to go away to my other machine, and I'm going to create a BDOS CCP image, which loads at that address and put it on the SD card. Doing that is out of scope for this particular video. So I'm just going to do that off camera. I have added the BDOS CCP image file to the disk and also a disk image for use later. So let us actually try this and see what happens. You possibly spelling it correctly. I have possibly spelling it correctly, I said. I have not actually tried this in real life. So I'm interested to know what will happen. And the answer is nothing. Fantastic. Okay, well, that clearly doesn't work. I'm going to have to figure out why. I found out what's going on. As you can see from the 85 on the screen, I have added a bit more tracing. It seems that some of the other system calls are being executed before going to the command prompt, which I kind of should have remembered. So if I hear other system call implementations so that we can see that 85 is calling setDMA followed by sellDisk. Now I'm not surprised it's crashing at sellDisk because sellDisk is the entry point that returns a pointer to a table describing what a particular disk drive looks like. And that table contains more pointers. So it's probably following pointers off into nowhere and it's doing all kinds of weird things. We can nastily hack around this by simply telling CPM that that disk doesn't exist. So 251. We are going to load HL with zero. HL is return value. So CPM will call this. It will get back a result of zero. It will think the disk doesn't exist. And because this is the boot disk, it will probably produce an error and then go back to the command prompt. So let's see what happens. Or you know, not. Well, I suspect that we're just going to have to implement some more of the disk system. So let's do some of the more interesting bits. The disk system operates via sellDisk. That's select a particular drive you want to operate on. Set track, set sec and set DMA that tell you the BIOS, what track, sector and read, write address you want to operate on and read and write, which don't take any parameters. What most of these do is they just set a variable in memory that's then used later. So this is actually pretty easy to implement. So for set track, we want to replace this with current, check the, in fact, BC. And that's all it does. And we're also going to put in a variable like so. So you can see the implementation of set track. I'll actually just tidy that slightly to make it easier to see. There we go. Set track. Just copies of value into a variable and I put that in the wrong place. So that should build good. So now we're just going to do the same for the sector. And likewise for DMA. Okay, that looks plausible. So the next thing we're going to need to do is to do sellDisk. And this needs to return a data table describing the disk. So we're going to need one. Here are our drive tables. I copied these from a NC 200 CPM port. I did a while back so I didn't have to think about any of the numbers because they're fairly subtle to get right. This is configured for a 720k floppy disk with two reserve tracks. We're not going to be using those reserve tracks, but it'll do for now. You may notice down at addresses ff13 where it says opt fn advance. That's because I've written a little macro to advance the program counter, which is the syntax is a bit contorted, but you can see me calling it line 1913. What that's doing is it's just rerunning the opt command to set the compilation options. But the result is coming from a named function. The named function is defined at the bottom of the file. And it just adjusts p% no% and then returns the current pass which goes back into opt. It's clunky, but it actually works just fine. So that is the drive table. Now I need to hook up cell disk wherever it's got to. Still not there. The editing environment on this thing is not as nice as on the original BBC Micro. The original BBC Micro was possible to skim down a listing very conveniently because the listing speed was slower. And there was a key combination you could use that would just halt the display output. So by repeatedly lifting your finger off a key, you could get a bit more data going down. The agon lights moss doesn't have that yet. So it's a bit clumsier. Okay, so let's put a thing in there. Right, what are we going to do for cell disk? Well, we get past a drive number in C. We need to save this somewhere. So we have to put that in A to do anything useful with it. We want to save it in current drive. We then want to test, see if it's zero. And if it's zero, we return a pointer to the DPH. Otherwise, we return nothing. So what we're going to do is load HL with the DPH, compare A with zero. Actually, we can do that with OR A, which is faster. If it was zero, just return that will then return the value in HA, which we've just set. Otherwise, load HL with zero and return. So there is our cell disk implementation. And we do also want to put in current drive defined byte of zero. Okay. Oh, and while we're at it, that home call, all that does is it sets zero to the current track and sector. So that is easy to do with HL, sector, HL. That should be a two, four, three, red. Okay. And let's just put another blank line in there. So that should build. Okay. And I'm looking carefully at the last address, which is FFC zero. So we're getting quite close to full. I may have to claim another page and regenerate the BDOS CCP file. But let's see what this does. Probably nothing. Nothing. Okay. I am not quite sure why this is hanging at this point, because we should have enough present for it to do something. C nine. Now that's either a hex number or it's two traces from my program. C is sector translation table nine is read. Okay, it has actually tried to read from the disk. That's good. The sector translation table is used for soft sector mapping which is a whole thing that really shouldn't be a thing in CPM. Let me just check what we can return to say do nothing. Yes. Okay. That's straightforward. Where was it? 1650. So we can implement that with B into H C into L and return. So I can now get rid of that and that. So there is Sectron E. What this is for is it's a way to allow the BIOS to do soft mapping of sectors. Every time the BDOS sets sector, it will call this function to let the BIOS change it. And of course, we don't want to. So we just return what we were given. Okay. Read. I think I'm just going to have to bite the bullet and do the full disk system at this point. So what we're going to do, we've got this CPM disk dot image file, which I am going to be using as a disk image. We are going to open that through Moss. And every time CPM wants to read or write a sector, we just read and write a chunk of that file. So the first thing we're going to need to do is to open the file. And we only need to do this once on startup. So to save space, I'm not actually going to do it in the BIOS itself. I'm going to do it in the startup code. So let me just split up the startup code a little bit, an empty line there and a empty line there. So between calling LDL and jumping to the start of the BIOS, we are actually going to open our file. Now, scrolling through the Moss documentation, I'm looking for, here we go, Moss F open. It wants a file name in HL. So that is going to be this image file name. And it wants a an enumeration value in C, which I now know sees as selffully not in the documentation. Finding that was tricky, but the magic number we want is three. We then, oops, I need to load the system call value, call the system call. And now we want to stash the result, which is in A, into a variable like so. And we do need to allocate something for somewhere to put the variables 2141. Okay. And we need to actually set the disk image, which is going to go here, file name, and what did we call it? CPM disk. CPM disk.image 2640. Okay. Right. That assembles fine. So disk reads. For a disk read, we need to compute the offset into the file, seek there, and read a CPM sector. And CPM sectors are always 128 bytes into its final destination. I'm just looking at the MOS documentation. Okay, MOS F read is straightforward. Where is MOS seek? There it is. Okay. So what does our code look like? We are going to want a helper routine, which we're going to put here, because this is going to be used by both reads and writes. So the purpose of this is to take the sector number, the track number, but not the drive number, and seek into the file to the location of that sector. So looking at our drive table, so the number of CPM sectors per track is hex 48. So we want to multiply the track number by hex 48, add on the sector number, and then multiply the whole lot by 128 to get the byte address. Now doing a multiplication on the Z80 is fairly awkward, but luckily the Z80 has a multiplication instruction, which I'll go and look up. The multiplication instruction on the Z80 seems to be an 8 by 8 multiplication. That is, it multiplies two bytes to produce a 16 bit result, which is actually just what we need. So let's try and, where did I put my seek to sector? So we are going to, the first thing we want to multiply is going to be the track number, and that needs to go into H. The thing we want to multiply by is the number of sectors per track, that is CPM sectors, which is in the, which is in the DPB table. I mean, I could hard code it, I'm just like, not going to. So if I can find out where I put that blaster thing, there it is. So we are going to load A with the low byte of the number of sectors per track, and put that into L, then we multiply, and that gives us our 16 bit sector number in HL. So now we want to add on the sector position. So we want to load DE with current sector, add HL and DE. So now we have the absolute sector number. The next thing is we need to shift this left by seven bits to multiply with 128. It's actually much cheaper to shift right by one bit and roll over the right hand bit. So we're going to do that by setting A to zero. This is going to be the newly significant bit. So we want to, let me just look at the very complicated shift instructions. So rotate right nodes. We want to shift right logical H. Rotate right through carry L. Rotate right through carry A. So the shift right logical will shift H right by one and the bottom bit will go into the carry. We then rotate that into L so the carry goes into the top bit and the carry is set from the bottom bit and do the same again with A. And let me just double check the instruction definitions. No, I was right. We do use rotate right with carry. Okay, so now we have in the order HLA our 24 bit offset into the file. This is not the order in which MOS wants it sadly enough. So we have to rearrange things. The most significant byte goes into, hang on a second, the MOS documentation says that HL contains the least significant three bytes. That does not seem like it makes much sense. I think it's expecting that this is all happening in 24 bit mode with 24 bit registers. Oh, I will have to go and look that one up. Okay, I looked it up. Yes, we are going to have to pass our 24 bit value in HLU, which means doing odd things. Rather than shifting right into A, let me just take another look at where we are. Rather than shifting right into A, we're actually going to have to shift left into HLU. So loop time. We are wanting to shift seven bits. To actually do the shift, we need a magic prefix byte to say do this in 24 bit mode and doubling a shift left by one bit. We'll then multiply by 128, leaving the top bit undefined, which we don't really want. But I'm not sure there's a way to set just the top bit or byte of HL. I think I'm going to have to do some really odd things. Okay, let's put this back where it was. So we're now going to push this value onto the stack so we can pop it back again using a 24 bit operation. Yeah, so when loading a 24 bit value, it will load the least significant byte first. So it will pop and read in order L, H and U. So we push AF and then we add one to the stack pointer. This should, assuming I figured things out correctly, overwrite the AF and leave the A on the stack. Now, we push HL and then we pop HLU. We should now be in a good position to actually make the call. So we need to set E to zero. We need to load the file handling to C. Load the system call number into A. Make the system call. So there is our code. I am not even slightly convinced that that's right. Does it at least build? No. I had forgotten that you cannot add stuff to the stack pointer. Okay, there is actually a safer way to do this that we define the three-mic buffer to write our value to it and then load it back as a 24 bit operation. Nope, that doesn't work. Turns out that you can't read a 24 bit value from a 16 bit address with any degree of ease. You have to compute a 24 bit address based on the content of the MB register and stuff like that. Okay, I think I have a plan. So as of line 1904, we have the high bit, the high byte in A and the low bytes in HL. We want to keep the low bytes in HL. We want the A to go into U. So we are going to do a long push of HL. That will write HLU onto the stack. We then copy the new stack pointer into HL. That gives us a pointer to that three byte value. So we now increment twice. HL is now pointing at the high byte. So we then write A to it. So we should now be able to do a long pop of HL which reads all three values in the right order into HLU. And then we should be set up to actually make our call. Now, because I have no faith in my ability to get this right, I am actually going to do some debugging code. And a closed square bracket. So every time we do a seek, it will print the offset it has seeked sort to. So now we want to try and implement our read routine. It goes 17, 20. So we want to do a read. Clearly the first thing we are going to want to do is seek to the sector. Now, just a double check the documentation. The only thing we should need to do to actually make this happen is we want to load the DMA address into HL. That's the destination. We want to load the number of bytes we're going to read into DE. System call number is 1A. Do the system call. And then we actually need to set the return value correctly. Checking what that is. Returns A equals 0 for okay, 1 for error. A equals 0 return. So there is our read routine. So the only thing to do now is to see if it builds, which it doesn't. I've been doing a lot of 6502 assembly recently. Okay, let's try that. What's this complaining about? The same thing again. 2010 LDHL, SP. Yeah, getting values out of SP is annoying on the Z80. I believe that what you've got to do. Yeah, load 0 into HL, add SP on to 0. Okay, so the only thing to do now is to try it and see what happens. Well, that hasn't worked. I can tell by the number of seeks that it's actually trying to read the directory. But it seems to be going to 8585 every time. I know why it's done that. I have put the value in the wrong register. It hasn't worked for a different reason. But our tracing is wrong for reasonable reasons. PX8 wants the value in C rather than A. I suspect that my maths are wrong and it's seeking to entirely the wrong place. And that's why it's not working. Yeah, that looks plausible. Okay, FEB3 is all horribly wrong. Well, if you look at lines 1900 and 1910, I think it's pretty obvious what's wrong, which is just I clearly managed to overwrite a rather important line of code, which would be this. So we load current track into A, put it into H, load tracks per sector into A, put it into L, multiply, add on the current sector. I will note that this code is limited to 256 tracks. Let's give this another go. Still wrong. Interesting. I hope that that 24-bit push is not using some weird stack pointer. I finally made it work. It turns out there'll be two problems. One was actually loading the 24-bit value into HLU. I went through about four different ways of doing it, none of which worked, until I eventually settled on this one. If you look at line 2030, that is a long HL load instruction that loads three constant bytes into HLU. I then self-modify that instruction in lines 2020-10. Well, it's fine. The other issue was that my maths were indeed wrong. I was using the wrong rotate instruction. RRC is an 8-bit rotate, RR is a 9-bit rotate, and I needed the 9-bit rotate. So it was taking a bit off the bottom of the register and sticking it back on on the top, which is not what I wanted. I've also discovered that the whole thing is now big enough that I've had to claim another page of memory. Only just. I'm 10 bytes over. So hopefully once I get rid of the debugging code, then I will be able to shrink it again. But anyway, if we load it and we run it, you see it is now loading sectors that are 128 bytes apart. If I hit return a few times, we get the CPM-A prompt. And in fact, commands do work sort of, but there's something wrong with the way the terminal status is read. So everything is one character late. So if I do DIR, return, return, has actually done something. It's printed a dot, which isn't right. But let's fix that console thing. I know what that is. Well, I think I know what that is. And that is const. Where is it? There we go. I think I need to change that to 160 to X or A. Instead of returning that there is always a character ready, we now return that there is never a character ready. So let's just give that a go. Hey, it is one byte shorter. Yep, there's our A prompt, DIR. Right, well, it's given a reasonable attempt to load the directory. It hasn't worked, but you can see it's scanning those sectors. So it is possible that my attempts to make HLU work are in fact not working and it's reading the wrong sector from the file. Or my load code is wrong and it is not reading at all. Let me take out some of that debugging code just in case that that is upsetting HLU. It hasn't helped. I'm just wondering what there is we can do. Try running a command. Okay, that has not actually tried to load anything. It's clearly failing to load the directory. I suppose we should check to see if the loading is failing, which we're currently not doing. What does MOS return for FREED? Number of bytes read in DEU. So given that we asked for 128, if that value in DE is not 128, then something is wrong. So we load HL with 128 and now want to subtract. I have a vague memory that 6502 and 780 use different carry flags because we'd have to do that. I'm not quite sure how to initialize the carry. I think I want to clear the carry flag. So this is testing to see if the result is 0. If it is 0 with good return, otherwise it's an error. I think that's right. What does it do? Right, well, it has failed with an error. That may be a correct error that has actually failed, or it may have been that I've just got my arithmetic wrong again. So that hasn't actually helped. Wait a minute. There is no reset call there. It's not actually doing the work. I may have just removed it by accident, but it may have never been in there in the first place. Let's actually just print the result. Run. And now we get an A prompt, but we still don't get anything in the directory. Okay, now you can see it's returning 80, so that is correct. Confused. I fixed the comparison. Firstly, by remembering that subtracting is the same as adding a negative value, so I don't have to fiddle about the carry flag. I can just, if you look at line 1760, load HL with minus 128 and add it. The other was discovering that CCF does not clear the carry flag. CCF invert the carry flag. So that was why it was behaving weirdly. Now it still doesn't work, but it's better. So it's clearly reading something wrong. Let's take a look at what it has actually read. So what we do is we load HL with the current EMA address, load it into C, print it. So every time you read the sector, it will write the first byte of that sector. What is going to come out? Zeroes. That is not very plausible. The first byte of a CPM directory sector should contain the user code of the file, which will normally be zero, or an E5 if that directory entry is unused. And it is very unlikely that all the directory entries in the directory are unused. So I think this is either reading the wrong data or failing to read at all. When we call read, HL contains the buffer, DE contains the size, C should contain the file handle that should be preserved from call seek to sector. In fact, we know that's fine because we are getting back the correct value. We do have the right system call number. So many bugs. Okay, I figured out what was going on after looking at the MOS source code, which is that, where did I put that read routine? When you call the fread system call, like I'm doing there at line 1751, MOS is not converting the 16-bit pointers passed from Z80 mode to the 24-bit pointers used by MOS internally. When you're using 24-bit mode. So the destination that we're passing in from DMA ends up being just wrong because the bottom two bytes are set, but the top byte is set arbitrarily. So it is doing the read. It's then going somewhere random in memory. I'm actually surprised the machine hasn't crashed. So we're going to have to do some more variable fixing up. We have this piece of code in seek to sector where we are patching up a long load. So I think we're going to make that a bit more generic and make a helper routine. I'm going to put it here. What it's going to do is you pass in HL and A and then it will set HLU to A being the high byte and HL being the low byte. So this is just going to be the basically the same code, slightly rearranged. So HL wants to be low. So it goes here. The code above line 2017-2018 is actually also doing the shift needed. So when I shift the sector number right by one byte, I'm actually adding a byte on on the right. That's A. So A is the low byte and HL are the high bytes, which is not the same as what this routine will do. Two, three, four, two, A. HLU, instant. Two, one, six, five is going to be, that's actually going to be the same as 2100. But add 2166, 2167, red. So there is our set HLU routine. Now, do I want to change the seek to routine? We would have to shuffle the registers around. So that is going to be three register moves. So that's three bytes plus the call. That's another three bytes. That's six bytes. So yes, it is worth it. So four bytes of shuffling. It's still worth it though. So we are going to move. A goes to C temporarily. A, which is going to be the high byte, gets H. H was going to be the middle byte, gets L. And L, which is the low byte, gets the old value of A, which is C. And then we just do call set HLU. And I can get rid of 2090. And 2100. So that is the new code. Let me just check that builds, which it does. Let's take a look at that read routine. So we have to set HL and DE. So for reasons, we are going to do it the other way around. So we load DE with 128. A is zero set HLU. Okay. So that will set DE to be the three byte length. Now we want to swap DE and HL. Okay. So then we load HL with current DMA. We want to load A with the special MB register, which I now remember this assembler won't know about. Let me go look up the encoding. The encoding is ED60. So ED60. And I've also discovered while doing that the EX may not swap DE and HL. So we're going to revisit this. There's not a lot of good documentation on the EZ80. So it actually occurs to me that we don't need to use set HLU to load that 128 simply because it's a constant. We can just do A, I think the prefix for this is 49. I'll double check. Load DE with 128, high byte zero. So that should give me a 24-bit constant load with no fuss. And then we just call MOSFREED. What that's doing is we take the 16-bit address out of current DMA. We then patch in the current MB, which is the high byte of the address that's normally invisible when you're in Z80 mode. And then we load the constant 24-bit 128 value. 5B, not 4B. So renumber and build it. Does it build? It builds. And let's see what it does. All zeros. Great. So that's still not working. The LDA, MB instruction does nothing when you're in Z80 mode. I mean, it says it's literally a two-cycle NOOP. So having had a hunt around, I can't find any way to figure out where my program has been loaded. Luckily, it appears that MOS always loads things at a fixed address. So let's just try setting the fixed address. Seeing what happens, I will be kind of surprised. Huh, that worked. Those E5s are empty directory entries. And there is our directory with programs in it. Right, what do we get? Does look like it worked. Let's see if we can list a file. That's worked. It's paging through as I press the key. Can we run programs? Well, the obvious one to run is this. It does work. However, I know that BBCBasic uses CON status a great deal for just doing stuff with. And if you don't actually have a proper implementation, it doesn't do anything. But yes, we're now running the CPM version of BBCBasic on top of CPM, on top of MOS, which we've assembled using the MOS version of BBCBasic. Okay, well, let's put in the right routine. So the right routine is actually going to be identical to the read routine, except that it calls a different MOS system call. They both have the same API. So in fact, we can probably comment this out somewhere. So let's just change this to prepare read write. 1, 7, 6, 1, read, 1, 7, 6, 2, 3 is going to be our new readie. Here we call prepare read write. Okay. And in fact, we can do more than that because we're just going to fall through. So now the right code is call prepare read write, lda, 1b, MOSF write, reset 8, check read write result. Okay. So load run. So we should now be able to modify the disk. I'm just trying to think of what we can actually do. Well, I happen to know that ZATE is a console-based debugger, but it needs configuration for the terminal. So it's not going to work on this system until I tweak it. So I should just be able to do that. And it's gone, which is nice. Which is nice. Let's start up the stat work. Looks like stat doesn't work. Okay. Let me try that one more time. I see that ZATE has gone, which is nice. Let's just try dump. And dump does also not work. Also does not work. Interesting. BBC basic loaded. So let's try loading BBC basic again. BBC basic works. So it should just be able to type into it. Well, how about the assembler? Very interesting. Now, I know that some of these programs are written in C. BBC basic is not BBC basic machine code program. Stat and dump and copy are all in C. I... QE is... I wrote that myself as a VI like editor. I don't know about asm 80. Camel 80 is not. So let's try that. Okay. Well, that works. This is a fourth interpreter. Got that one again. Plus dot. Oh, that's interesting. When I press the shift key, I get a zero result coming in from getcha. That shouldn't happen. It crashed. Okay. There's clearly some kind of corruption issue going on. The CPM is pretty robust. I mean, there's not enough there to go wrong. I mean, I've basically finished the port apart from, you know, the bug fixing asm 80. Yeah, that doesn't work either. I think I know what's going on. When I did the warm boot code, line 1010, I did LDHL, W boot E. That is, I pointed the BIOS vector at the bottom of memory to the warm boot routine. I think that this is hopelessly wrong because any user program that wants to call the BIOS is going to assume that this thing is pointing at the entry in the jump table. So that it can read the address out of that jump instruction, subtract three, and get at the jump table. So I think any program that's been trying to call the BIOS has been crashing because it's been fetching the wrong address from there. So let's just change that to vbase plus three, which I think should be correct. And try that again. So, oh, I put some tracing in. Okay, let's try QE. I have actually gone and ported, hopefully, QE and Z80 to work on this terminal. That has in fact not fixed anything. Fantastic. So I think I have found something. What you're seeing running is DDT. It's the original CPM debugger dating from back to about 1977. It only supports the 8080 instruction set, not the Z80 instruction set. It's got different mnemonics, so it's kind of weird, but it does work and it loads, luckily. So, oh, it just crashed. Okay, let's try that again, shall we? It's very fragile and I think I know why. And now it's hung anyway, rather than fiddled with it. M-R-S.com. Okay, now if I tell it to dump memory starting at zero, this is what appears at the very beginning of RAM. And it's not right. In particular, at the very beginning of RAM, there needs to be a jump instruction to the BIOS. And I see a C3 and a C3-00. So that's expecting the BIOS to be at address C3. That doesn't seem right. Later on, I do see at address 5, C3-00-DF. That looks right for the BDOS entry point. Actually, it doesn't look right. I would expect that to be something plus 6. So I'd expect to see C3-06-DF. Although now I think of it, it is quite possible that DDT has claimed the BIOS. So what is actually at D3? One, two, three. I think U is unassembled. So if I do UD3, that has not actually worked. But anyway, that BIOS jump, C3-C3-00, is just plain wrong. So let's take a quick look at the BIOS code. Because if that is wrong, then that explains the symptoms. Because any program that tries to use the BIOS will read the address there and completely fail. So there in the warm boot code, I see it load C3-A, copy it to BDOS call, load FBase plus 6, and copy that to BDOS call plus 1. Then we load the jump instruction to BIOS call, which is at 0, load BBase plus 3 into HL, and write A again into BIOS call plus 1. Okay, that explains a few things. Let's change that to that, right. I'm not entirely convinced that the memory tester works, but let's just try it. Bioslocated FDOO, correct, top of memory, yeah. I got this tool, by the way, of a 1980 archive of useful public domain-ish software. Well, I assume it's now testing memory. Okay, let me try a different program. Let's try stat instead. And of course it fails. There's obviously something else wrong. Control C is supposed to trigger CPM to reload the BIOS of disk. So if I do it now, and it crashes, that's not right. So that indicates that something's wrong. Let me go and look up the DDT documentation, because it's interesting. And let's see exactly what resetting the system does. Okay, L is disassembled. So if I do L0, there we go. Jump to FDO3, not not jump to DFOO. Yes, that's where DDT's loaded itself. So yeah, that's perfectly normal. DDT's claimed the BIOS vector. X allows us to modify program state. So let's try XP equals 000. Okay, that's changed the program counter to zero. So I now should be able to do T. For traced program execution. And that's showing me the instruction we're currently at and telling me that it's about to go to FDO3. Okay, FD3A, this is our warm boot routine. It is now showing 8080 op codes. So this is our warm boot routine. Stores C3 at five. EFO six is the BDOS address. And I have a feeling that as soon as this writes, it's going to crash DDT or not. Okay, that's writing FDO3 as the BIOS address. It's now going to load the BDOS CCP at e700 and 1600 bytes long system call one go. Oops. It seems to be trying to dump the entire memory space as machine code. That seems peculiar. Now one interesting thing that occurs to me is that I believe I've forgotten all about the stack again. When you do a warm boot, it of course overwrites the CCP and BDOS. And the CCP and BDOS both have their stack in that memory. So it's overwriting the stack it's running off and that's never going to go well. So I think that what we need to do, this could be why it was crashing when trying to do a warm boot. We want to set another ephemeral stack. I tend to put them here. There is empty space from address 80 to address 100 hex. It's used to pass command line parameters to programs and programs will tend to use it as a buffer, but it should be completely fine for use here. It's only being used for the warm boot routine as soon as we enter the CCP it's going to switch stacks. This is probably not going to help the programs crashing, but it could help control C. Incidentally, I'm not really sure how this just loading CPM on top of basic is working, because it's loading it in the same place as basic. So weird. Okay, control C. There you go, warm boot. Right, so let's try. Okay, and as I thought that hangs. So that is whatever else is wrong, but at least warm boots work, which means that quitting programs should be reasonably robust. Okay, so after reloading, let us just try debugging stat.com with DDT. So this is the beginning of stat. You see the first thing it's doing is loading the BDoS address. It's doing this in order to find where the top of memory is. So we trace that, execute that instruction, and we see it's loaded DF. So that's telling it that DF00 is the top of memory and is then going to look to see whether it's got enough. I think it's been a while since I've looked at this code. So C is DF. Yeah, it's looking to see if it's got enough memory. So now it's going to do some various initialization things, probably initialize the, yeah, it'll be initializing the BSS of the program. I'm not sure DDT supports breakpoints. Yes, it does. And the syntax is super weird. Go comma 11f. Let's try that. It did it. Okay, so it should have initialized its thing. Oh, it's going around the loop again. One, two, one, two, three, four, there. Right, now it's getting the address of a stack frame. The stack is at 100. Shouldn't it have set a stack? Yeah, there it is. Now it's setting a stack. And if I list one, two, E, yeah, this was compiled from C into 8080 machine code by the Amsterdam compiler kit, and it's not a very good compiler. But that's okay, because the 8080 isn't a very good processor. So who knows what this is doing? One, three, one. Okay, so it's clearly crashing somewhere inside one, six. That subroutine, which is probably the main program, to be honest. Well, yeah, control C obviously does nothing. I think I'm going to have to look into this offline and not drag you through lots of laborious machine code debugging. But as an intro to DDT, which is ancient but still amazingly useful, anyway, I'll take a look. Well, that took about three minutes. And I know what the problem is. What you're looking at is some of the startup code of AC program. The 8080 mnemonics are like a pig to understand. But the interesting bit is that stacks instruction. What that's actually doing is storing A to the address in DE. DE, which is just labeled D on the 8080, contains the value eight. What this is doing is it's installing a bunch of resets for use in the compiled C code to make the C faster. I know this because I wrote it. The problem is reset eight is the one used to make system calls to MOS. So of course, as soon as it does that, it overwrite the reset that's already there and suddenly you're unable to make MOS calls. So that's nice. There is a way around this. We don't actually have to use the resets. Let me just attempt to quit out of this. The only people who are going to be using the resets to MOS is us, the BIOS. So rather than whenever we want to do something, we just call, we must find a piece of code. There we go. Line 1280, con out. It's printing a character. It puts the character in A and it calls reset 10 hex. The thing is the code out reset 10 hex is actually this stuff here. All it's doing is it's making a, we're looking at lines 230 and 240. It's making a long reset call to MOS. So rather than doing reset 10 to go to this code that then does the extended reset to go to MOS, we can just do the extended reset directly in the code. And all we need to do is stick a bunch of def b4 nines in front of every reset in the program. Now, let me see. Some versions of EBC basics allowed you to search. I wonder if this will do it. It does. Fantastic. So if you look for reset, there we go. There are all the resets we're using. If I renumber to make sure this space, 489, def b4 9, 589, def b4 9, 1089, def b4 9, 229, def b4 9. Okay, that should be done. I've also done the remaining four entry points, which is for the printer output, the punched tape writer, and the punched tape reader. Yes, really, CPM is that old. They don't do anything on this. It's traditional and modern-ish CPM systems to hook them up to a serial port. And in fact, the Aegon Lite does have one, so we could, but I'm just not going to do that for now. So let's run it. What are we doing for space? F, F, O, E. Yeah, we need to scrounge up the 14 bytes, and then we could get a whole 256 bytes of memory back. All right, let's run it. Okay, let's try dump. Cannot open source file. It is working. Dump is the hex dumper. Oh, it likes 80 columns. Great. Stat, there is our disk. We have lots of space free, because I'm using a 720k disk image. I think we should be able to do stat star dot star. Details of all our files in terms of how much space they're using. Okay, let's try the big one, QE. That's not quite right. So, shift A for append. This is a VI-like text editor, which I wrote for CPM. It's very small, and it is clearly not working right, but it is mostly working. Yeah, I think my reverse text on off is a little dysfunctional, so let me go and deal with that. So, here we are looking at the slightly fixed QE. It's not quite right. There's something very odd going wrong with the terminal emulation, which I need to look into, but it does sort of function. So, let us type some code. It's a VI clone, so if I do shift A, it goes into insert mode. And we are going to, this is 8080 instructions. Load HL with a constant. Load register C with a magic number. Call another magic number. That's the BDOS entry point. And return, find a label, followed by a carriage return, followed by a dollar sign, because for some reason, CPM uses dollar signs to terminate strings, which is kind of annoying if you want to write out a dollar sign. Save it. Quit. So, now we should be able to type our file. There we go, which is hopefully what I typed. So, that has assembled it. Yes, I wrote that assembler. It's terrible. And it's generated a hello.bin, which I can actually dump using the dump tool. And yeah, it wants 80 columns, but that is a program. It's got the wrong extension, so let's rename it. Yes, CPM's rename uses the parameters the wrong way around. And now we can run it. And it has said hello world. It's also produced some garbage, and I don't know why. I think that's an assembler bug rather than anything else, actually. I can verify that. We've got DDT, the debugger. No, we don't. I regenerate the disk image, and it doesn't have DDT on it anymore. Oh, well. Anyway, that's a demonstration of writing a program and assembling it using CPM. Anyway, so that is a thorough tour of coding for the Aegon Lite using only the internal tools, i.e. doing it the hard way. If I was actually writing programs for this for real, I would cross compile from a PC. It's so much easier, but the tools are there and you can use them. I would have to describe the system as quite flaky. A few other things that weren't on camera went wrong, such as the fact that the various graphics demos written in basic worked yesterday, but don't now, and I haven't done anything with the board. So that is interesting. And I found, I think, at least three bugs in MOS in the space of 24 hours, which I have, of course, reported. So we'll see about that. But it's an interesting device. If you want to do Z80 programming, and you actually want a modern computer that works rather than a 50-year-old computer, which is a project because you keep having to replace bits, I think that you could do worse than one of these. It's got keyboard interface, reasonable graphics, not too good graphics. It's got basic support for things like sprites, but it's all scaled down sufficiently that you can plausibly drive it from a Z80 machine. Yeah, it's a nice system. I has been very interesting actually coding this for it. I will, of course, polish it all up and upload it to GitHub at some point, as and when I get the energy. So I'm going to call it here. As always, I hope you've enjoyed this video. Please let me know what you think in the comments.