My adventure with implementing a Chip-8 emulator in Rust...
MMU, scheduler, tasks, etc.
I first considered writing a Chip-8 emulator maybe around 2 or 1 and a half years ago. I had way less experience and I really didn't do any systems programming, or knew anything about computer architecture, so it was a daring project, but still I hopped down to my computer and started doing and I ultimately... failed. Yeah, so I abandoned it very quickly revisiting it a few times, but never really doing anything.
But suddenly last year (2022) I had the sudden urge to learn Rust. Which has became one of my favourite languages since then. And I started thinking about what could I do with it after doing a number of smaller projects, and ultimately stumbled upon the Chip-8 emulator, I even thought that it would be easier since I had some experience with it (I had no experience with it.).
So I started working on it.
I started working on it around October/November maybe and quickly built up a base for the emulator with SDL2 (regretted it later), based on Cowgod's Chip-8 Technical Reference. The Chip-8 isn't a really complex system, especially if you look at other consoles (if you can call Chip-8 a console) like the GameBoy, but I really think you can have some fun with it, if you want to.
After implementing IO and some instructions I kinda got bored of the project for a bit and decided to pursue something else for a while, until eventually coming back to it in the new year (2023), and implementing the rest of the instructions and stumbling upon the foreshadowed SDL2 issue.
The issue with SDL2 was that it cleared the backbuffer after every frame, so you had to rerender the sprites every single frame, but rerendering every frame made the whole emulator very laggy and slow for some reason even on a fast machine, I'm sure there are work arounds, but I didn't find any in the Rust wrapper for it, so I decided to ditch SDL2 and work with something much much smaller, namely minifb. And then it was time to add sound, which was honestly really easy with rodio.
Lastly I implemented the remaining instructions, and then fixed bugs, patches and misbehaving instructions.
And it was done, so be sure to check it out :)
Well, the idea is basically: write an operating system for the Raspberry Pi Zero W (version 1) to mess around with and build stuff on it.
My journey through operating systems development starts quite earlier with a few unsuccessful attempts, and some tutorials, which sent me to the realm of boredom. I didn't really know what I was doing and well... that's still the case, at least sometimes, but that's how we learn, no? No? I hope so. Anyways, this time around I was like I want to build something bare metal on this RPiZ, so I was like lets start building, in Rust, now that didn't really work out since Rust doesn't have support for the ARMv6Z architecture, which, you know the RPiZ is based on. Although I did get some things working (a blinking LED) everything about it seemed to be quite unstable, and that with a single line of code I could just crash the whole thing. So I switched to good old C, now by this time I was pretty sure I wanted to build an operating system to go under this project of mine, so that's what I started doing, I got an LED working, wrote some memutils, and started work on the MMU.
Now the MMU may sound intimidating at first, and it kind of is, but once you learn how it works, it's actually quite simple and understandable, for the most part at least, it does involve quite a bit of bit manipulation, but that just comes with the job. Now I stumbled upon some great writeups about this namely this repository. It was pretty helpful in helping me understand the MMU. But I did encounter some problems, namely the adding an MMU section takes way too long for some reason so long in fact I could probably leave the thing on for an hour and it still wouldn't get to the LED blinking part of the program problem. And that is where I am now. So I guess stay tuned for a solution maybe, I suspect it has something to do with the write buffer, but I'm not entirely sure. Anyways, see ya.
The AP bits (a.k.a. the access permission bits), turns out telling the MMU in the first-level descriptor that no one can access that area of memory is a pretty bad idea, if you want to access that area of memory. Anyways, definitely didn't take 4 days to figure that out... pfft, never. So, new dilemma: second-level descriptors and where to put them. I really want to put them on a kernel heap, but obviously, we need a kernel heap to do that, sooo...
Creating a heap isn't actually the most difficult thing out there, but creating a well-performing heap isn't actually that easy either. Now, since this is a hobby OS, we probably aren't going to be using the most cutting edge technologies, but I do want something that is quite performant, and doesn't fragment the memory too much, since we are already starting out with only 512 MiB of memory, and we definitely need it for a lot more things too.
There's a variety of different heap designs, but something that I think we can start out with, is to think about where the heap table is going to be, now if you don't really know what the heap is, it's essentially an area of memory, of which we can cut certain sized blocks, and can give them out to a (part of a) program for use for a while, and once that (part of the) program finishes using that memory, we can get it back. But we obviously need to keep track of which block of memory is free to give out, and which block is being used, so that we don't accidentally give multiple programs the same area of memory to work with, that's where the heap table comes in to play.
My main dilemma here, is where to put the heap table, and how should it build up? Something we could do, is that the heap actually just points to the first block header, and then the block header defines smaller blocks inside itself, which it will keep track of, and we could technically store all this information at the beginning or the end of the block of memory. But the problem with that, else than the fact it takes up heap space, is that it offsets the starting addresses of the blocks we give out, by it's own size, which is bad, especially if you're trying to be also able to allocate memory aligned to some number, but that's a subject I'll want to tackle once we're done with this. Now we could put the block header at the end of the block, but then obviously the next set of blocks would be offset by the size of our block header, so I guess that's the thing I'm gonna think about for a while now, anyways, I'll be back, see y'all. :)
Solution is that I realized it's enough if the heap sections (that's what I call them now) aren't aligned, it's not actually a problem, since if we align after the heap sections header by the block size, all the blocks will be aligned anyways, so it will just simply work, I'm going with a bitmap type heap, similar to what's described in this OSDev page, but with some different algorithms and implementations. I have encountered a new problem though, to align to a variable, we need to get the remainder of the full size of the header and the block size we align by. But since we're building for the ARMv6-Z architecture, we are in some trouble, because the ARMv6-Z architecture (similar to other older ARM architectures) doesn't have a division instruction, which means we can't really divide by anything else, other than constants, or maybe we can, I am quite unsure actually, I see a lot of contradicting opinions. Anyways, see y'all, I'll try and figure this out.
Nevermind the last thing, all I needed to do was link libgcc for it to (according to what I read) emulate division, which is quite an easy solution, literally five characters: -lgcc
. I'll be back on how heap implementation goes, see y'all! :)