 It is time to do some more coding. What I've got here, and at this point I'm pointing off to the right where you can't see me, is a Alpha Smart Dana device. This, and I'm showing you a picture on screen now, is a laptop-like device from... I can't remember when. It's technically a PalmOS tablet. It's got a full-sized keyboard, a decent screen, massive battery life, and it runs off a 33 MHz 68000. Out of the box, and indeed at all, it runs PalmOS via a flash chip. It works fine. However, I recently discovered this, which is the manual for the system on a chip that's based around, and discovered that it is highly hackable. And in the previous video in what I hope to be a series, I took the lid off and figured out the pinout to the debug port, and actually got it working in bootstrap mode here. So I am now capable, with the appropriate software, of bypassing the entire ROM of the device and starting the thing up in debug mode and downloading and running code onto it via a serial port. So I need some software to do this. I've done the hardware side. Now I actually need to write a client that talks this protocol. So that is what I'm going to do today. Now I've got the datasheet here. Over here I have the development environment set up via a very nearly set up. Let's just make this thing work. Yeah, I should do fine. So that I can write code and it will automatically compile and everything. Over here I have a disassembly of the processor's bootstrap ROM. This is the excellent Gidra reverse engineering tool, which is the best 68000 disassembler I'm aware of. The bootstrap code is about 190 bytes of machine code that live on the processor itself. And to get at it you ground a pin on the processor and hit the reset button and it boots into this. So with this I can tell what it's doing. You see there's not a lot of it. And the actual protocol itself is relatively well documented. It only does two things. It downloads data into the machine and it runs stuff. So I'm going to try and put together a rather more flexible tool that does things like reading data as well. I've done this before for other devices. I'm going to copy big chunks from one of those. So this is the main from a program I wrote in 2005 which talks to the Amstrad personal organizer, Amstrad e-mailer bootloader for doing basically precisely this. And as this has got all the code in it for talking to the serial port and so on and using sub commands and argument parsing, I'll just use that. So first let's just trim out the stuff we don't need. When you've got one board rate this thing starts up at 19.2. We should be able to change that later. Don't want that. Don't want that. Don't want either of those. The processor itself is I did say it was a 68000 derivative but it is called the Dragon Ball 68 VZ 328. It's compatible with the vanilla 68000 so it's got no memory management unit but it runs up to 33 megahertz which is pretty fast for a 68000. Wow, I wrote this in 2005. So we don't need any of that. Actually we don't want any of that. We do want port. Okay, so these are all the commands I did for PBLQ. I don't think I'm going to get through all of these. I just want to do read and write for the time being and this is going to be interesting for reasons I will explain later. So let's just do those and that. We don't want less. We don't want checksum. Okay, so powers options we... E no longer exists. P does exist. F and S exist. Packet size no. Here we go. So we've got ping term. We've got no bless. We've got execute. We've got no checksum. Read write and we don't have read flash and write flash. Okay, so let's just create our globals.h. Did I actually remember to add any of this stuff? I did not. So this program is in pretty vade C++ which is one of my least favorite languages but it's kind of the best one for this job. Okay, I need some more stuff stolen from PBLQ. Utils which has got standard routines for displaying various things and getting the simple timers. Good. We now need to add this to our build tool. So my preferred build tool these days is basically defined as anything but make. What I seem to have settled on is the ninja build tool which is very fast and flexible but has got a lousy input language and a shell script which is this one for actually generating the files used by ninja. We do have a utils.cc. Oh right, you can't do that with a build library. So yeah, the shell script is pretty simple but it does understand most C things. I think that's all you want. Okay, better. So let's steal some more things including some definitions. Okay, so log on is the routine which is going to set up the connection to the bootstrap and just generally make sure that we are correctly talking to the board. And now we want a bunch of these for our sub commands. So we've got write, read, exec, execute is called rather. Yeah, I wrote this in 2005. I can't really remember how any of it works. Oh yes, we want a version which we're going to set here for simplicity. Ah, dodgy term. Yes, dodgy term is a very simple serial terminal because that is something that you nearly always want to have on this kind of machine. So we're probably going to want serial.c which will contain this. Add this to our list of source files and now we need to make it work. So this contains all the code for talking to the serial port including some protocol-based stuff. This is setting up the connection to the Amstrad e-mailer. So we don't want any of that. This actually speaks both to the e-mailer 2 and the e3 protocol, neither of which we care about. So log on. We connect to the serial port. We'll take this out, but we might want to put it back again much later and then this code is supposed to talk to the e-mailer just to see if it works. We don't have any of that. Then change the board rate to much faster one. We don't want that. Here's the serial terminal. Okay. Yes, I have stopped defining my own byte types in the 16 years since I wrote this. That's terrifying. Oh, yeah. And we want to strip out nearly all the sync stuff. So I will just stop that out for the time being. I'll just log on to find a packet there. Also in. We don't want that. It appears to have written this in C89. Yeah. I think we got rid of... We did get rid of slow board rate. Yes, let's just put this here. That's the board rate at which the Daner's bootstrap loader initializes. Changing it is complicated, and we'll get onto that later, but let's make the rest of it work first. Okay. So it is now connecting. We want to add read and write, which we will copy from PPLQ. Why is there no read in there? So this is... Okay, let's say we don't want to write to Flash, or rather we do want to write to Flash, but we haven't done that bit yet. All this code is going to be different because it's a different protocol. Okay. Now where is read? Apparently I put it in the checksum command. Why did I do that? Oh, I remember how this works. Oh, yeah. Yeah. The Amstrad emailer doesn't... The bootstrap on that doesn't actually support reading back data, but it does support getting a checksum. So what I do is I check some ever-increasing areas of memory, and then based on the two checksums, I can compute what the byte is supposed to be. Actually, I seem to be checksumming one byte at a time. It is ruinously slow, but it does work. So instead of doing that, we're going to copy our write. It's essentially the same operation, and actually implementing this is going to be a nightmare, and that should do it. Okay, and where is our write? I just want to stick in an error here to see it doesn't work. Oh, we haven't done execute. So the way these all work is there's a function that does the work, and there's a function that pauses the thing the user provided. The idea is that these are executable by other parts of the system to make it reasonably modular, which we do want. We'll get there eventually. So we want to pause. The argument contains one. Here we go. The command list for execute contains one value, which is the start address, which we get here. The second value must be null because otherwise there are too many provided, so that makes the syntax check easy. And we just stick this in here. Should be an address. This should be s. Good. All right, so if we run it and let's just change... Oh, yeah, that will be in details. Okay. So the first thing we're going to do is ping, which does absolutely nothing because we didn't do any synchronized code. Now, what we want to do is when the user calls ping, which is just used to make sure that the board is responding, that just calls logon and does nothing else. Logon is here in this code. This opens the serial port, sets it up, and then synchronizes, which is when we poke the board to make sure that it is responding correctly. Now, if I fire up my own serial terminal at the right address... Okay, so this is the bootstrap loader running. It's just echoing out all my stuff. If I now push the reset button, nothing happens but the board has reset. Now we send one byte... One byte? Ah. We have to send the right byte, which in this case is return, and the board responds with a at sign, which indicates the bootstrap loader is ready and running. And I can show you the code for this. Bootloader entry here. This then sets up the serial port. Here we have a loop that just spins looking for data on either of the two UARTs. Now, once the user... Once it has received a byte, it then goes to here, and this instruction is the one that sends the at sign, which is hex 40, to the designated UART. So let me think... Okay, so I'm typing capital A's, and the first one comes out with an at sign, and then they're all echoed. The at sign itself is echoed. So if I reset, then type an at sign, I get the first at sign indicating that the board is ready. Let me type another one, and we get an at sign indicating that it's echoed back the at sign and sent it. So what we're going to do is we have to keep poking the board before it will respond. So let us... Do we have read and write byte commands? We do. That will wait forever. I don't think we want to use those. Those are used for actually sending and receiving data. So we just want to first write a byte. We want to repeatedly send an at sign and then try to read back a result. And we're just going to keep doing this until we get an at sign. So first we do the right if it fails... Actually no, we don't care about the result. Are we opening this in non-blocking mode? Yes, we are opening it in non-blocking mode. So whether this works or not, it will return immediately. We're now going to wait back for some data. We're now going to read one byte of data. And again, we don't care about the return value. If we read back an at sign, then we have successfully synchronized. I'm thinking that we do actually... Let's actually just check this. And everything else we don't need anymore. Okay. I think that is our synchronization. If we run the ping command, it worked. Okay, let's hit the reset button. The reason why that worked is because it output the... As the board is already in bootstrap mode, therefore we sent the at sign and got back the echo. So I press the reset button. Now we do it again. And it sent the at sign and the board was activated. So we are now talking to the bootstrap loader. We can start doing more interesting things. Now... We could immediately start work on right and send the... Where's the bootstrap mode docks? And using the bootstrap loader's own protocol so that in order to write data, you send it a record containing the address you wanted to write to the number of bytes and the data. But we're not going to do that. The reason we're not going to do that is because this is an example of what you actually send to the board. And you notice it's all in ASCII. The data you send it is in hex nibbles. This text on the right is all ignored. Because it's all hex nibbles, this means that we're wasting half the bandwidth of the rather slow serial connection. So we are going to do better than that. So in order to read and write stuff, what we're actually going to do is to upload a fragment of 68,000 machine code that implements a much more efficient protocol. But to do that, we are going to have to implement code that actually speaks this protocol so we can upload the machine code programs. So let's add in a file for the library that's going to do that. So the protocol only has two operations. There's execute and there's write, which are going to be implemented by these. So let's put these in. Let's open a file. Let's put these in here. Let's steal our headers from main. Here. Okay. This is, I mean, because it's all hex, it's incredibly easy to actually implement. So we are just going to the text record into a string. I should probably have imported the C++ format library, but I haven't. Did I implement? No. Sometimes in the standard utils, I put in a printf to a string or such like routine. Actually, let's do that. Printf to a C++ string. So we're going to be using sprintf to count the number of bytes, printf. If you do an sprintf to null, then it doesn't do anything other than count. Turn value of snprintf. Yeah, I think that'll work. We'll test it and see. So now we create a string that is big enough. So vsnprintf. So we are going to write to our C++ string. Only allow this many bytes. App and app. Turn s. Did I mention C++ isn't my favorite language? We have to include the standard library string here because you can't forward declare this string there. So in fact, if we go back to utils, we do not want to import string there. That's not right. I think I've got the syntax dvsnprintf wrong. No string size format list. String size format list. So what's that complaining about? That's interesting. Am I confusing my C++ containers? I think I might be. Yeah. Okay, let's create a string and then set the size to byte resize. That's better. Okay. All right, so let's go over here to be record again. We are going to, we want to write out string to the serial port. So where are our helpers? Send byte and receive byte. And no, we actually don't want a receive byte. We just want send. And we do want send byte. At some point, we should probably make the debug tracing here a little bit nicer, but this will do for now. Okay. Oops. Throwing my mouse across the desk. Okay. So send a printf. You want the address in capital hex, followed by 00, followed by a new line. That's all there is for that. Now for this, so this 00 here is the number of bytes to send. And if it's zero, this means just execute from that address. So you can't send zero bytes. So if we do a write and the user asks for no bytes, just do nothing. So now we are going to the address to write to, followed by the number of bytes, but no new line. And now we send a single byte and a new line. So hopefully that works. Well, we need to wait to test it. So in order to test it, we're going to have to, well, I can fake up some code easily enough to write something to the board. But getting anything back might be trickier. Hang on a second. I can write to the UART. So that will transmit a character. Okay, the bootstrap loader is running. Now the UART is, this is the way to get the addresses here. I believe we're using UART 1. So we want to write to the low byte of that address, 906. So if I just do write to 906, that's the wrong address. Okay, let's do that again. Write to the low byte of the 16 bit value at 906. And this is a big engine system. So the low byte is second, one byte, and we want to send a 50. Wow, it didn't even wait for a return. But it worked. It sent a P. That is what hex50 is an ASCII. So the bootstrap is actually doing what I expected is. So now let's just write some code. Actually, I think it's worth doing this in synchronize. So let us send P because why not? And I do actually need to prototype that. So this will just be a bit more. Okay, I bet that segmentation fault is due to my printf routine. Actually, we couldn't test that. The point of doing this is to just be a little bit more robust about making sure that the bootstrap loader is present and behaving. Do I want SN printf? They write at most size bytes, including the terminating null byte. Down here we've got a thing on the return value. Return value functions SN printf and BSN printf, including the terminating null byte. So we don't want that because the C++ string is doing that for us. Turn value of size or more means that the output was truncated. Yeah, okay, that should work. Reset. Do we get any verbose tracing? We do not. Okay, that has failed somehow. So let's fire up the debugger. So we step through the code to make sure there's nothing in the serial port. Write our at sign. Wait for data. Wait a minute. Why is there a break there? We don't want a break there. We want to read back the at sign. That was incorrect. When does poll return zero? Our return value zero indicates a timeout where you actually want to go back to the beginning again. And let's make sure the buffers are flushed before we... That is simply not working for whatever reason. So we set the serial port. Flush the buffers. Write our at sign. We did not get any result. Flush the buffers. Right, we're not getting anything out at all. So when things like this happen, let's just do that to see what happens. So we are writing the at sign, but nothing is happening. TTYS0, really. Okay, that's better. It has got to this point because it's read back the... read back something it doesn't understand. That is better. Got a 4-6 instead of 5-0. Well, 5-0 is P. 4-6 is F. Why did we get an F? That honestly sounds like it is echoing back part of the B record. You can actually see that using the minus view option. I didn't enable speutracing. Speutracing causes all the serial stuff to log verbously everything that's happened. Okay, so F, F, F, F. 5 F, that's correct. 393037 is the address we've asked to send. 00 should be the count, which should be 1. Why are we getting zeros there? It's actually called execute rather than write. Actually, these are not zero nibbles, they are zero bytes. That should never happen. I think something's gone wrong with my string again. Once we've done this, then we can get on to the interesting bit of the job, which is writing the 68000 machine code blobs that we're going to upload. Okay, B record, write. Count is 1, indeed. Okay, 9 bytes, that is 8, 9, 10. I misread that man page, didn't I? It said not including the functions that write at most size bytes, including the terminating null byte. You see, this should be the length of this string is 10. But size is not the same as the return value. Return the number of characters printed excluding the null byte. See, this is why I wanted to use the format library, which does all this for you. Okay, so this sent, yes. So, here are our Fs, 9070150, return, and then we get back the 46, which is a capital F, yes. So, let me just stick a leading new line on both of these just to see if we need to reset the state machine. Any value less than 0x30, I believe, is considered a record separator by the bootstrap loader. One of the nice things about Gidra is it does a reasonable job of trying to turn your machine code back into C. So, here is the code that actually deals with bytes it reads back from the serial port. So, if you send a value that is smaller than the ASCII value for 0, then it jumps to 5a here, which is the actual main loop. Let's just rename that. Now, it can't possibly be the case that we're sending stuff too quickly. So, we are sending F, too many serial devices attached to my system. Okay, we are sending FFFF9070150, outcomes of P. So, it works if I do it through the serial terminal, but it doesn't work here. Actually, we don't want to send terminating new lines. Of course, every character you send gets echoed back. Right, so, that's why we're getting the 4.6. So, in fact, what we need to do here in our send routine is make sure that we get echoed back the right thing. Okay, so, now you can see that we are sending our Fs and then getting back our Fs. So, this has, we've got as far as 5, 0, and now it's stuck there waiting for the response. Let's try that serial terminal again. Okay, FFFF90701, now 5, 0. Okay, the P came after the echoed back 0 I sent. So, again, this should work. So, let's try putting back those leading new lines again to, again, reset the state machine in the bootstrap. Bingo, and it works. Good, I have to say the performance is not so hot that we'll increase the board rate later. And we're not intending to do anything bandwidth intensive using the B record protocol anyway. That is actually extremely not brilliant. I should be able to do better than that. I wonder if we are somehow forcing a weight before every byte. So, let's just try that with S trace. We are indeed, we're calling pole, but pole should return as soon as any data shows up. So, ah, it'll do. Okay, so we have now, we've now got to the point where we're synchronizing to the board. So, let's just check stuff in. Right, now the next job. Oh, actually, we can do one thing, which is command execute here isn't really simple. We're just going to do B record, execute address. Job done. Now, let's go and look at the debug ROM again. You see, because the bootstrap is invoked before anything happens on the chip, including running any code from ROM, you don't necessarily get any useful stuff like, you know, RAM working. So, the bootloader comes with either 32 or 64 bytes of data living at this address. So, let's say it's 32, so that's 1632. And you can see it's immediately followed by a jump to the main loop again. What have we got? Yeah. So, it's possible that we can overwrite this data with our own code here. The purpose of this is to allow you to upload chunks of machine code, which you can then execute. So, we're going to put our routines in this. Now, we don't have very much space to work with, which is going to be a little bit interesting. Now, I have the 68000 assembler, so we are going to use this to generate our routines. So, the first piece of code is going to be right. Just trying to remember how a new assembler works. I may have to take a break and do some reading up on this. The syntaxes of assemblers are always different. But we want to basically... This is just going to be a simple stub that does nothing. We want to jump to main loop here, which is at 5a. So, that has successfully written. We should now have a 68020 object file. So, I should be able to dump that. And indeed, we have a jump instruction. So, we need to now integrate this into our build system. So, what we are going to end up doing is... Each stub is going to be a single source file. We need to assemble it, link it to the right address, which is ffffc0. And then turn it into a char array that we can include in our program so that we can upload it to the board. So, now, can I compile this with... I'm just thinking the best way to do this. Can I compile that with GCC? I think I can. This gives us rather saner semantics when it comes to the flags. So, that is the... Now, I do need a LD. I need to be able to call the linker directly rather than doing it through the compiler driver, which is generally the preferred way to do it. So, our build stub function is going to take the... Just take the path of a source file. I'm just thinking about how to do this. Let's just go with that for the time being. So, now, we can say, source equals source stub string 1.s. That will work. Don't care about that. We want to build it with that. We don't have any dependencies. We probably want to do something about that later. Do indeed not have the C++ compiler. So, that needs to be cc68k... Actually, this should work out the dependencies for me. This ends up being cc. And then this also needs to be cc. Okay, so that should have assembled the file into there. So, let's just disassemble it. There you go. Right. The next job is to turn it into a... ...includable header. And, luckily, we already have a rule for doing this. Bid in code. So... Now, we can't turn that into... Well, it will work. It just won't do the right thing. It has built. The problem is that it's turned the entire object file into the static array, rather than just the code itself, which is not what we want. So, we now need to be able to link it. So, we're going to add a link68k. So, this is going to be LD68k, LD68k flags. I don't want libs there. 60.k. We want to give it... I was actually thinking that we'd want to give it a actual linker script, but I think we can get away with that one, because if we can specify the basic address layout using command line flags. So, this is going to be... Nope, LD generates elf files. Therefore, we always have to go through the elf stage. So, that's going to be like so, except we now want to specify flags. And then build rule should be link68k. There we go. We cannot find entry symbol start defaulting to a thing. So, let's go to our stub, and we're going to need to declare that global, probably. Okay. So, that has now successfully linked a thing. It just hasn't done anything useful, because it's linked it to another relocatable file with no actual work done. So, can we put... Is there a way to specify the segment? Here we go. So, what we want to do is a text is going to go to, and data will go immediately after text, which automatically, which is fine. I think that might be it. Okay. So, what do we get if we dump this? Well, it's put our code in the right place, which is a good thing. I don't think LD may be configured to support more than one kind of object file. That would be nice. Will that work? That does not appear to have done anything useful. I think we're going to have to call obj copy to convert it into an actual binary. Yeah, I think that command is just not working in this setup. Oh, wait, that's input format. Is there an output format? O-format. O-format binary. I don't think that's done anything, to be honest. So, what has that actually done? So, it should have written a thing there, but hasn't. Okay, let's just stop fiddling with this, and let's just use obj dump as well. So, we're going to actually emit an L file, which does work, that generated the appropriate thing, and we're going to have to now add another build rule to turn it into a binary. Just check that exists. And in fact, I might be able to skip a stage, because I can actually tell obj copy to turn this into a... Ah, I can tell obj copy to turn a binary into a linkable object, but this is turning an L file into a binary. And let's just keep using xxt for the... turning it into an array stage, because life's too short to fiddle with obj copy. So, we call bin. And that is stupid. Why am I doing it like that? So, this is going to be build d.bin from obj copy 68k. The input file is d.elf, and the flags are going to be oformat... output target. So, I want to change the target or the format. I think I want to change the format, so I'm going to have to figure out how to do this. This is why I don't like using obj copy. It's just a pain. Okay, minus o binary. So, make sure... Okay, we need to add a rule here. So, at this point, people may be asking why I am insisting on doing it like this, rather than using make. The answer is essentially, because I want it to work. Make has some serious, serious problems, particularly when it comes to parallel builds. Well, that seems to have done the right thing. It's almost impossible to write correct make files. That hasn't generated a... Why is that not... It has not generated the bin file. It has generated an h file. I don't think that's the right one. Well, that worked. Ah! I know what I did wrong. There are the files. Yes, I want to... I didn't put an obj here in the right places. So, that's going to be this, and this is the only bit that's referring to a source file becomes this. I spent way too much time struggling with make, trying to get parallel builds to work. And while this is a fairly simple program that doesn't need it, I've been thoroughly burnt by make's inability to, for example, have multiple output files from a single rule. And most make files only work accidentally, which is why anyone who's used make's first reaction to weird things going on with the build is to do a make clean, which works, but they shouldn't have to. In fact, we don't want a source there. We just want it like that. There we go. And there is our bin file containing four bytes, which is correct, and there is our h file containing practically nothing. And this is actually going to be stub. Okay, onstubs.write.h. So, this generates this array, which we can easily import in here. So, now we have our 68,000 machine code in the correct form that we can just upload it. So, let's just try a thing just to see if it works. Now, we know that we can send bytes to the serial port by using this kind of instruction. So, that we could just do moves.bq2fff907, which I believe was our UTX, which was, here we go, 907, and we should probably common these out into a single file. So, that is very much shorter than I was expecting. See, this should be two instructions. stub.write.h See, this is five words of data, and that's four bytes. That's not right. Bin is the right size. Yeah, okay. Because I put that initial stub on the end, that's not going to work. Let's just put it there for simplicity. And that's now the wrong file name. Right, and now we should have, there we go, and there is our data. So, this should write a value to the UART and then jump back into the bootstrap routine. So, in write.c here, we can write our routine to the bootstrap, and then run it, and then see whether we got the right thing out. And we have not included... Okay, we did that wrong there. So, what we actually want to do is to include the header that we generated, which we can do like that. So, now we have our stub embedded into our program, and when we do a write, it will upload it to the board and run it. And we should get back a queue. So, let us try that. And these values are garbage.c equals 51. That's not a queue. But, well, it ran it, which is nice. But why did it not do what I was expecting? Okay, let's turn on more tracing again. So, this was it synchronizing. So, you can see here, it's uploading our byte to 907, 1 byte, 50, and then we get back the 50 it wrote. Here, we are uploading the bootstrap routine. Here, we are telling it that we want to execute code. So, ff... Well, yeah, that's c0 to there, followed by 2 zeros, which is the instruction to tell it to execute. And then we get a 51... Hang on. 51 queue it worked. Okay, that's nice. So, we now have maybe either 32 or 64 bytes worth of space to work with. And in fact, I've done this wrong. I don't want to work on write first. I want to do read first. Because read is the thing that we can't do using the bootstrap. Yeah. And in fact, the first thing I want to do is to dump the bootloader. Because the bootloader I've got here in Ghidra actually comes from different device. And I know it's possible for the bootstrap loader to be customized. So, let's just add the read stub. Let me go to read.cc Code.obj Stubs.readstub.h Is that going to work? That works. Let's just copy this code. And then we want to... Actually, let's open our output file first. And we're not using PBLQ's timer code. All we're doing is reading bytes one at a time. So this is straightforward. So we want to read a byte. I can never remember the order of the... F puts the B to Fp. Do some progress. Like so. So what that will do, hopefully, is run the piece of code in the stub. Now, there's no way to pass in the start address and length yet. Yeah, also, this needs changing. And it will download and run the stub and copy it back whatever it gets up to. It needs to know how long the... Yeah, it needs to know how long the data block is. And length. So in fact, we should be able to use that very same stub. Is that going to work? That's not going to work. We need to change that one. So we should be able to now... Oh, this is going to be bad. Let's turn that off. So let us read these two are garbage. This one is in use. It's created a file, but it was empty. So length was apparently zero. And length is not zero, apparently. I bet the time was zero. Let's just... If I run that in Valgrind, it will not give me a stack trace. Yeah, this is a division by zero error here. So... Has that read our one byte? It's read our one byte. So we need to just make sure that this doesn't cause division by zero errors if the time is too short. And the simplest way to do that is to just turn the time into a floating point value. This machine is quite a lot faster than the machine I wrote PBLQ on. Because this is... So we now want to return fractional seconds rather than an integer value. So that has worked. It's returned one fewer byte than it should. That's because we do the read. Then we write the message. We have an incremented count yet. It can't get incremented between loops. Do I care enough to fix this? Yeah, let's just do it properly. Okay, so... I'm not sure why that is so garbage. So this is bytes divided by the number of seconds. Did I miscalculate the number of seconds? Ah, that's because this is a floating point number. Again, another good reason to want to use the format library. Right. A whole 40k per second for reading one byte, which I'm sure is thoroughly representative. Let's see if I can truncate that. Okay. And in fact, let's just do... How to do nested functions in C++. It's one of the few good things about the language. Okay. Now, let's do some more stub work. Read.s. I never opened read.s. So we want to read values from memory and write them to the serial port. Now, we do need to be careful about what we do with registers. I think the Bootstrap documentation tells you which registers you can access. CPU registers D0 to D6 and A0 are used by the Bootloader program. Really. I think that's incorrect because A1 here is actually pointing at the UART. So if we overwrite it, then we will screw up the Bootstrap's ability to talk to the UART. We can get round that by actually jumping to here that will reset the Bootloader completely, which should be fine, provided we don't want to change the board rate. But until I know that this represents the same Bootloader that I got on the real machine, then I'm not going to touch it. Okay. So we want to put... Don't think A2 or UPP are used by anything. Okay. This is the start of the Bootloader ROM. We're going to put that into A2. So we are going to read a byte from the address at A2 and put it and output it to the serial port. That will increment A2. We now need to compare A2 against 0. I think the easiest way to do that is to... So we're not auto-incrementing A2 anymore. Instead, we're using ADDQ to do it. And then we can say that... We know we've finished when A2 is 0, because that's like rolled over. Actually, actually... Actually, I'm not going to do it like that. I think we've got the space. I'm going to put the number of bytes we want to write into D7. Is D7 used anywhere? It's used there. Why? What's it doing with it? I don't see any other references to D7, to be honest. I think that instruction is doing nothing. Good. Well, let's just use D7 for the count. So we do want that auto-increment. This becomes subq1 from D7. Then we are going to branch if said. I think that's right. To loop. And then we're going to exit. Missing operand 0 assumed. We might have to put that into a register. Let's just see whether that still doesn't like this. It's possible that this syntax is not supported by the GNU assembler. Because this is using odd things. Yeah, I think... Yeah, I'm just going to go look that up. Okay, yes, the assembler syntax is not the same one as used by Ghidra here. It's apparently the Motorola or MIT syntax. Anyway, this is how it works. Registers have percent signs on them. Luckily, this still works. It's BNE rather than BZ. BZ was wrong anyway. We want to branch if not Z. And this is what our code looks like. However, we aren't ready yet. Because we don't want to write until the UART is accepting bytes. So where did I put my documentation? Universal. We need to check to see whether the UART write buffer is full. And if it's full, we wait until... Well, it's not full. So we were looking for the TX status register. There we go. UX transmitter register, 16 bits wide. This is the high, half, and this is big endian. So this is in the lower address. And this is the data part. So this is at 907 and this is at 906. And we want to... Here we go. We want the transmitter FIFO available bit. So that's bit 13. So we're going to do B test. You want to check bit 13 of the 16th value at that address. And if it is 0 to say that there is no space available, we loop. So BQ loop. B test W. That is the right instruction, surely. B test. Do I have to read the value into a register first? This does actually seem to be doing just that. So... And it's using D5 for it. So let's just do move word into D5. B test 13 D5. Interesting. Do I need to put W on? No? So I don't think that's doing what I want it to do. No, actually that will work. So 68,000 instructions. You specify how big the value you want it to operate on is. So that's what these W, Ls, and Bs are. L means a long 32-bit value. W means a word, a 16-bit value. So this is loading... Actually just put in... So this is loading the value at Tx into D5. But it's doing a 16-bit access. However, B test here, which is operating on a register, is I think going to be a 32-bit value. Is there a B version? There's a B version, but there's no W version. But there is an L version. Okay, yeah. But that's okay because this is the bit number. And of course bit 0 to 15 have just been loaded by this. I believe that this will 0 extend bit 17 and up. But we don't care about that and this isn't looking at them. So that's fine. That should work. Our code is a little bit long. You see it's just a bit over our 32 bytes. So can I turn this into a shorter loop? Yes we can. Say that these are all short jumps. It should do that automatically, to be honest. And I think I can probably do that. No, that's got to be a jump. I'm sure it's possible to make that work somehow. Because the address that we're jumping to is only just a little bit above start. But of course the assemble doesn't know that. So that is fitting in our 32 bytes. So I think that we can make that work. So let's try it. And then we should be getting back 512 bytes. So start is ignored, filenames not ignored. Length is ignored by this code. But we want the read code here to use the right length. So okay. Did we get anything? Well we got 512 bytes of data. But I recognize this data and it's all wrong. So what we've got here is in fact the boot ROM. Not the boot ROM, the flash. Verifying this was there was one of the things I was going to do when I got this working. This is... Ah! No? Oh no, I thought I scrolled to the wrong place. This looks like the stuff down here at E0. It's E6000FF78. E6000FF78. So I think it's just read 512 bytes from the wrong place. So did we get our stub wrong? Yes we did. That should be that. At least it worked. It's not quick but it worked. It's not resetting. Hit the button. Okay. That's interesting. That suggests that I've managed to confuse the bootloader. So maybe it is using those registers. Right. This is more what I expected. The internal bootstrap ROM. For some reason the top 256 bytes are empty. You can see that here in this version. The actual bootstrap loader starts at F00. 256 bytes in with 43F8. I'm not going to compare this by hand. But this is definitely looking like the code I was expecting. I still don't know whether this stuff here is mutable. Whether it's RAM and not just ROM. But now that we have this thing working we can find out. But first I want to actually get parameters passed in. The way we are going to do that is we're going to want to patch the instructions in the... Actually, yeah, we're going to patch the addresses in these instructions. But we want to do that in memory before we send it to the board. We could just do another couple of B-record writes to update these addresses. But that will be slow so let's just do it locally. These are going to be placeholder values. Let's turn them into something that the assembler isn't going to try to do anything clever with. And of course they are big endian. That was going to work. And sign char to const char star. That loads read it into a stub. We now want to write these two values into the string at a particular offset. So this is annoying so let's produce another utility for it. So this is going to be the high byte. And of course these will all be implicitly cast to bytes. So the address is going to be at offset two. The length is at offset eight. So this is going to be... Now this must be the length so this is going to be stub size. And this is the address. This is going to be address of the first char. So this should now read 512 bytes from address zero. And then hit the reset button. And what did we get? That does indeed look like 512 bytes from the beginning of the address space containing the boot ROM. So notice ff00 to begin with. So let's do that. And hit reset. And it skipped the first byte. Right that's worked. We are now reading data from the serial port. Slowly. Let's just read 8k and see how that goes. So... Well that is the maximum speed that the serial port can sustain. It's not brilliant like 2k a second but... And we have 16k of boot ROM. Fabulous. Alright let's take a look at that stub. Probably doesn't want me using D5 to be honest. So this is the code where it decides what to do after reading each pair of hex values. D6 here appears to decide whether to execute or wait for more data. D5 appears to be used as the temporary. We could use D1. D0 appears to be something important. But D1 is reset right at the beginning of the main loop. So let's try D1. So reset the board. Read. Let's try that again. Nope the board is still confused. One thing that might be worth trying is to see whether that compiles. It does compile. But I'm not sure whether it's good. Yeah we can do this. So the reason for the minus 2 is that this is doing a 32 bit read of the value here. And because this is big endian and we want the 2 bytes in the TX register to show up in the bottom half of the intermediate value. We have to decrement the address by 2. So this is actually going to be reading from whatever's immediately above TX. Which I believe is 9.6.9.4. So that's going to read the Rx register as well. Which I believe will have that will have a side effect. In that I believe that it will remove a value from the FIFO. Yeah I don't think we want to do that. So we're going to want to stick with this. But it is definitely confused somehow. So what registers are we... I didn't see an A2 anywhere. So this is the code that actually executes whatever it was that you've been asked to do. The layout of this stuff is very strange. So this came from doexecute here. Which what is that doing? This is moving D4 to D0 but it's testing the value of D4. So if it is 0 then actually do the execution. Otherwise continue on with this code. Yeah I don't know why this is so unhappy. But we can try this. And we need to reset. Anyway the board did it's thing. So if we try that again that did not confuse the board. The board was fine and it did it's thing. So if we put that here that will then write 1 byte. Which it did. Try that again. Yep that's fine. So I've now run out of things that could be going wrong. Okay we're touching all the registers and everything is fine. And now it's confused. So I have to hit the reset button for it does anything. It's possible that something has misassembled but it looks fine to me. And it is jumping to 5A. Which is main loop here. And actually let us ffffeo512. And we need to reset the board. Okay we now have a bootloader ROM. So we're going to go over here to Ghidra. And okay so try to remember how this works now. So if you start up Ghidra in the blank workspace. Import the file bootloader to the raw binary. And it's 68000. Ghidra doesn't actually know about stock 68000. So I'll just tell it it's a 68020. And this is starting at ffffe00. And we tell it to analyze as much as it can. And it's done, so it'll never mind. I just hit the D key to tell it to disassemble from this address. So we'll just create a function. And you see here is the decompilation. This all looks very familiar to be honest. Yeah here is our main loop. Here is the temporary buffer. Which goes down to E0. And then here is the branch to the main loop. And here are a lot of knobs. So this actually... Ah I was wondering what all this stuff was. But of course it's the program I just entered in. Which is this you know 2470. 2470. And here is the source address. Here's the next instruction. Here is the length. And then here is the program that actually does the work. So actually I should be able to tell it to assemble. Disassemble that. There we go. So this is the UART. Where's that got a W on the end? Is that saying that... That's doing a byte access? So a word access rather? I know why it's confused. This is not a jump instruction. We have in fact run out of space. That's fine. So replace that with a knob. Yeah so it's hitting this. It's got an invalid opcode. So it's... In fact the first two bytes will be a for a jump. But the remainder has gone. Right this actually proves that we only have 32 bytes of space. Because our attempt to overwrite this has failed. So replace that with a knob. And now when we reach the end of the loop. It will just fall through to this branch instruction. So now we should be able to do this. Okay it's confused. Wait for a reset. And done. Do it again. And it's happy. Okay we actually want this to be a little bit different. So you want this to be a bit more robust. So what we're going to do is... Our stub is always going to be 32 bytes long. And we want to fill it with knobs before... And then we will... And we want to make sure that it's always 32 bytes long. And contains... It's padded at the end with knobs. So we'll neatly fill out to this point here. That means we don't need to bother with a return instruction on the end of the stub. We just fall off the bottom of the program. And everything works. So this is also a utils. This is going to be... Okay so... If stub.size is greater than 32... Is too large. And while stub.size is less than 32... We want to write a knob which is a 4e and a 7 1. So that's... This is terrible code but... Okay is this going to work? Okay... Yeah no trouble. Alright. So that's our read stub done. We now want to do the write stub. This is going to be a little trickier to test because we don't have any RAM yet. But this is going to be reading from... I don't need that. So here we have the address and length. We wish to wait for... URX. We want to wait to see if data is ready which is handily in the same bit. So the set that code will work. If... If there is no data loop. Now our data has already is already in the low byte of D1. So we should be able to say that. And this is a... This should write the low byte of D1 to the address here and return. So... What is that assembled into? It's quite short. Hopefully that's correct. Decrement D7 and... So I don't believe we'll be able to write to... Anything. But let's give it a try anyway. Let's write... You know just random stuff to address 0. But I haven't actually written any of the... The server side code. So it's not going to do anything. Okay. Let's actually copy all of that. Stick it in here. We get our write stub. Our length is... We need to calculate that. We use this code. So we execute. And instead of reading bytes we write them. So... Like that. Dead. Okay. Right. It's doing its thing. And now if we read this back... And that's interesting. The board crashed. We sent it the right number of bytes. And as you see nothing's happened. The only place we've got to write to... Is our little chunk of memory that our stub is inside. Which is this. Is this still going to crash? Yeah. It's definitely unhappy. So... It's the same code. I don't know why it would be unhappy. So if I now tell it to... Write an empty file. And reset. Okay. But it did do its thing. That is working. All the same reasoning behind the register numbers should be valid. Unfortunately we can't actually read back what it's written in. Because we can't read anything back without replacing the stub. So that's dc, dd. And we've got ef which is our knob. We just have to do put an explicit knob in. Actually that's our shorter. If we put three knobs in it should now be too big to easier. And let's just check to make sure that doesn't work. Yup. Stub is too big. The knob and the board is confused. So it's not the knobs that are the problem. And it is successfully writing the data. It's not... well, it's trying to write the data to ROMs. It's not working. But it's not hitting an exception or an access fault. Otherwise we wouldn't see any progress at all. And I'm curious to know where that access exception came from. A floating point exception. Integer divide. Oh, it's because the length is zero. Let's switch that to a floating point number. Yeah, like that garbage. But that's fine when you're asking it to do something stupid. Well, that's wrong for a start. Okay, that's good. So let's just make the same change in read. So are we still fine with reading? Well, that's interesting. We're not. I know why. It's because the... my stubs... they read or write before decrementing the... decrementing and comparing the length. So if we actually read some data, yeah. So if you try to... if you tell it you want to read or write zero, it just... it decrements the number to minus one. Note that it's not zero and just loops forever. Okay. So that's working. Let's try writing bytes. Okay. The only problem was the stubs. So the issue is that we would need to put our comparison test here. It's slightly bigger than it was, but this is fitting just. So what this is doing is as the code flows down on entry to the loop, our length was the last thing loaded. So the zero flag is set from this. When we... A few more changes. The next time through the loop, the last thing that touched D7 and set the zero flag was this sub-Q. The branch instruction here does not affect the status flags. So that should work. So let's try reading zero. Did something. Okay. That is working. And let's try reading 100 bytes. Let's see if we can do the same thing for our right stub. So this is branch to exit if zero. We are not branching to loop when we want to wait for data because that is going to execute this BeQB again and the Z flag has been set from the test here. So we don't want to do that. And then this wants to be a braby loop. So what does this look like? Okay. That's just fitting again. Let's try writing. This is writing 100 bytes. Yes. And again. Yes. Make sure that this is now empty. Write. Yes. Write. Yes. Okay. It's fine. This is working. Okay. I wonder if it's worth trying to do high speed transfers by changing the board rate. The issue with this is that once we've changed the board rate then using the old board rate won't work. The way I did it with PBLQ is there was a retry option. So the first thing you would do in a script was to do ping without the retry. This would connect to the device and switch board rate from the slow board rate to the fast board rate. Subsequent invocations would use the retry option that would skip the sync thing. It would assume that the device was already set up and in the fast board rate. It was not terribly satisfactory to tell the truth. One thing I could do is to try and ping the board with both board rates to see which one works. Actually changing board rate is exciting. There's actually some code here to do it. Here we are changing the speed of communication. The problem is that the board rate control on the board is a two byte value. And B records change one byte at a time. So you change the first byte of the two byte register and the register value changes and now your board rate is wrong. So the second byte isn't read correctly. So the instructions here tell you you want to change the register value from 0126 to 0038. You first change it to 0026 which changes you from 19.2 kiloboard to 38.4 kiloboard. Then you wish for another B record command to change it to 0038 that takes you to 115.2 kiloboard. That's not brilliant. But now we've got the stubs we can do better than that because we can upload a chunk of machine code that changes the board rate in one go. That's straightforward. So I think the first thing we want to do is to just read back what the board rate is set to which we can do now, of course, which is at 902.902. We were at 907. Yeah, beyond your one. So 902, board control register. If 902, two bytes. And that is 0126. The reason why I wanted to know that is because there's actually several different potential clock chips that can be used. We are 32.768 or 38.4 and it will auto detect and set the board rate appropriately. So let's add another stub. So it's called 902.001. So this is straightforward. We just want to load our 38. What's the value? Which is 0038. Prescalar of 38.0 divide. And I think there's a table somewhere. Here we go. 115.2 is prescalar of 38.0. And divider of 1. Really? So why was 0038? So this is 902. This is 903. No. Yes. So 0038. The low byte is this end. So 38 is 0011. This is 3F. So shouldn't the divider be being set? I'm going to trust this table rather than the bootstrap mode code because the bootstrap mode code is full of typos. So if we want the divider to be 1 then this is 138. And we want that to go into Uboard 1. There we go. And it will be padded with knobs. Assuming this works, we can actually go to faster board. So we actually want to do that here. So after we've waited for the device to be reset we want to steal our stub handler code. We don't want that. Fastmode stub.h Okay. So we now want to write it and execute it. And now we want to switch board rates. Which we do with this. Okay. So let's read 8K's beginning of memory. See if this actually works. Yeah, that failed to ping. So that's wedged. Reset. Okay. So let's... Now, what's that done? C0. Okay, this is it uploading the bootstrap. This is it running the bootstrap. It looks like we are still in 19.2 kiloboard. So this didn't work. Now we should get confused bootstrap when... Right. I know what's happening, which is we have sent the byte to tell it to do the execute. And it's done it. And then because the board rate's been changed the received byte here is corrupted. So what we are going to do is wait for the transmit buffer to empty. Then change the board rate. So the transmit buffer is in read. So we want to load. We want to read transmit buffer into D1. We then want to test... We want to test for FIFO empty, which is bit 15. A branch if it is not empty to entry. So we'll spin there until it's... Let's also put... Change that to the right name. Okay, let's try that. I'll just hit the reset button first. It didn't help. Should have. Bit 15. FIFO empty. Branch if it's zero to start. Zero means it's not empty. So we'll only get to here when it is in fact empty. Then we change the board rate. Okay, let's take a look at this code then. Now 902 is a Uboard 1. So this is the Bootstrap code itself setting up the UART with 02. And if we look at our table, that does not look right. The value I read was not that. The value I read was 38, as described in the Bootstrap mode here. It was 0126. So where's 0126 coming from? Here. So it's obviously doing something with it set to a magic board rate value. So at this point A1 has been set to the address of the UART, which is going to be 9-0-0 in this case for UART 1 or whatever the address is for UART 2. So what's this 7? Oh, that's the transmit. That's transmitting the at sign. So this is clearly thinking it's going to change the board rate and then immediately send something. This is the piece of code that sets up A1 for the other UART. Unfortunately, Gidger insists on displaying all these things signed so addresses are difficult to pass. That's better. F423 is quarter E select register. This FBOB is actually not documented. Oh, hang on. It is documented. It's the watchdog timer. FBOA is the watchdog. So yeah, never mind. This is UART 1. Keep putting the label in the wrong thing. Can I turn this into a pointer? Apparently I can't. Anyway, that's not actually helping. So these are the RX registers. F904, this is URX2. So this is writing something to the TX register. But it's a byte that is writing. So that's going to show up in the control part of the register. The high end. So what's it actually doing? E8. So E is 1110. 8 is 10000. Okay, so setting the top three bits does nothing because these are all read only. All it's doing is saying turn off control flow. That's not very useful. Okay, well, let's try some no op code. Changing speed of communications. 126 is 19.2 kiloboard. So let's just do this. And let's go back to serial. And the bit where we actually try to change the board rate. Let's just do that. And this is actually now working. I wonder if I was actually getting that board rate there wrong. Let's try putting that back. And 38. Now, this seems to think that 115.2 is 0038. So that's worth a try. Nope. It thinks that 0026 is 38.4. So I think this is actually changing the board. But you know what? I am going to do something of it. Let's just not bother checking that. It may be that it'll sort itself out. And the ping that happens next is going to deal with this. So this wanted to be 138. Oh, let's try that. Okay. It's now pinging the board, but the board is not receiving. We should be sending peas constantly until something comes back. So let's turn on speed tracing again. And see what this is doing. Because it may be odd, and it does it once. Oh, that's interesting and a little worrying. So it's mostly working. See, send 46, get 46. Send 39, get 39. Send 30, get 3C. 3737, 303C again. 35303C. Let's just set that back to 38.4. Fastmode.s becomes the word 38. Let's try that. 4666. That's like epically corrupted. Again, I'm just wondering about these tables. So let's see if there's another value. 1, 2, 6. Oh, hang on. Oh, I just set it to stupidly high speed. Okay, let's just... 1, 2, 6 is the correct value for this. 1, 2, 6. According to that table at any rate. Okay, that is working. It's faster now. So clearly somebody is wrong about the zero port values. Though it's not like the performance was better. Interesting. So 57.6 is 238. It's not working. So is this just my dodgy wiring? I mean, I do have... I have some parts on order and I am going to repair the wiring. But that seems unlikely to be honest. Wait a minute. 1, 2, 6. 1, 2, 6. That's 38.4. But this is what was being set for 19.2. I may think our clock speed isn't at 33 MHz and therefore that table is invalid. Let's try 0, 0, 2, 6. Change this to 38.4. 100. We change this to 0, 2, 6. Reset. But it's not happy. Okay, I have a logic analyzer and that is clearly the thing that I need to get out in order to figure out what's going on here. And I don't think that will have made a difference. So let's put that back to 0, 1, 2, 6. Change this back to 19, 2, 0. So that is happy. Yeah, I don't know what's going on there. If the system uses a 32.768 kHz external crystal, the board register is initialised to 0, 1, 2, 6. After 19.2 BPS set up, assuming the system clock is set to 16 MHz. Don't suppose there was another table here with... Yeah, and this is for 33 MHz. So I need to double everything. In other words, decrement the divider by 1. That gives 0, 0, 2, 6 for 38.4. But we tried that and it didn't work. That just presumably gets garbage. I don't know actually. This does seem to be reading and writing the right things. New line. F, F, F, F, F, 9, 0, 7, 0, 1, 5, 0. But then we never get anything back. But these reads have worked and produced the right thing. Oh, I know. So it is because this is not checking to see if the TX buffer is empty. We need another stub. Source, Stubs, Ping.js. So this is... Wait until the TX buffer has a free space. Reply back with a B. So here in our serial code, instead of doing this, we have to get out another stub. So we can clean this up. Actually create a class for Stubs and not have to do all this horrible stuff. Add with nops, ping stub, execute it. I don't want to change speed anymore. And receive a byte. Let's see what that does. Fabulous. So this has done the execute. Oh, hang on. I think I just told it to wait forever. No, that's right. So we should be getting back the P. I mean, it's successfully changed board rate and does seem to be working. Okay, that is receiving back garbage. I think something is not working at that speed. So we're going to have to stick to 38.4. Is that a comment? No is... Hang on, I can do that. Yep, that's a comment. That still doesn't explain why this isn't working. Do it's easy to echoing back the... Echoing stuff back correctly. But then the receive byte here is not receiving anything. Are we correctly writing to the... We'll actually move for a start. But it should be a synonym and not make a difference. Well, we could just try it without the ping code at all. So we reset. Okay, right. That's what I expected was to happen and the same thing we have here. It's uploaded the stub, it's called the stub, but we're not getting anything back. Now it's possible that the transmitter is in some kind of state, but that seems unlikely to me to be honest because like it's echoing back the data. And the code's not complicated. Where is it? So we read a byte. We check to see whether a byte was actually received. Otherwise we loop. Then we write the byte straight back to the TX register to send it out. Yeah, that's all there is to it. Yeah, this was my code here, so I can't rely on that being anything resembling correct. I am very confused. So what happens if I leave it at 19.2? Does this work? Yes, it does. So our ping is doing the right thing. So our execute waits for a reply back from the board. So the board's transmit buffer should be empty when execute returns. We call CF set speed, which will presumably flush the buffers. Yeah, we are then successfully transferring the stub to the device and as far as I can tell executing it, but nothing happens. I wonder if the... Is the stub not making it to the board correctly? Once again, turn on dispute tracing. Well, they all look like pairs except for that one. That's not a pair. Okay, I just think that what's happening is the transmission's being corrupted at anything faster than 19.2 kiloboard. That's bizarre. It's got to be better than that. Okay, well, let's just go with this then. Actually, before I save it, let's just, so this should hang, and let's put this stuff back. It sounds like that the transfers are unreliable enough that my transfer routines probably also want to do a checksum. That'll be interesting. I need to try and make a stub containing a checksum. We don't really have a lot of space to work with. Yeah. Okay, now let's comment this out and change that, and now it works. Possibly I need to crank the thing up to 33 megahertz. It seems unlikely though. That would involve looking up all the system clock stuff, clock generation code, but, well, I now have a functional tool. All the bits should work, including the serial terminal. It's not, but it should. I'll look into that later. That's less of a priority. We can read, we can write, we can probably execute, but as I can't actually try, don't have any RAM to transfer programs into, I can't test that. So that is a good start. We can now dump the ROM very, very slowly. I already have a copy of the ROM for this thing. It may be possible, depending on how the flash chip works, to re-flash it very, very slowly, and possibly with corrupt transfers. So that's actually a good place to stop. I think the next thing to do is to try and set up the DRAM, and I'll just add another command for that. But I think that's good. I don't think we need this anymore, to be honest. This is for dynamic board rates. Now it's still being used there. Never mind, I'll leave it. Okay, well, I do not have a GitHub repository for this set up yet, but I will do that shortly and push it. I think that makes a good far too long video. So I'm going to leave it there. This is now a pretty useful tool, and I'm sure I'll have fun exploring what's inside this machine. I hope you enjoyed this video, and as always, please let me know what you think in the comments.