Well, it is time to continue this little blog series and finally get to the point of them - actually dumping gameboy games! Make sure to read my previous 2 blog entries to catch up and what I did so far.
Now that I have a consistent and easy way to get to a point in pokemon yellow where I can install my own code into any memory address I want, it was finally time to see if my original idea was even going to work, that idea is to dump games using the audio headphone output of a gameboy color.
Yep, I am not joking about that, sending the game through the beeps and boops the gameboy is able to produce was my idea all along, there is a reason I had "insane" in my title
Getting bytes into audio form can be done in many ways, but my particular idea was to make use of the volume register of the gameboy to send distinct pulses that I can record, detect and then evaluate on a PC to retrieve the ROM back into its original form. While there are up to 8 volumes (3 bits) per channel, I decided to only make use of 4 volumes (2 bits), because we are still talking about recording a gameboy here so I figured it'd be difficult enough to distinctly detect those 4 volumes.
So I started making some very basic code by literally writing the instruction bytes in a hex editor, at this point in time I did not even bother writing and compiling machine code and I just went straight to the bytes I wanted.
The code started out like this:
All that does is restart the audio to clear any previous game state, then enable the square audio channel with a very high frequency so I get fast spikes and lastly wait for me to press start to begin sending whatever was inserted into the cartridge slot.
The sending started by first sending those 4 volumes on both left and right channel as a form of calibration:
This of course will make it much easier for my receiver on PC to convert the pulses back into bytes. The very first test send of those volumes I recorded looks like this:
As you can see, those 4 volumes are very clearly visible, so this was a great first start and gave me hope that this will be all doable.Warning: Spoilers inside!
Now after that comes the actual ROM data, 1 byte is made up of 8 bits, meaning in 1 pulse I can send half a byte, 2 bits per channel make up 4 bits, meaning 2 pulses make up 8 bits, one byte. The good thing about that actually is that half a byte is also known as a nibble, and the gameboy specifically has a function to swap both nibbles of a byte around, making this code very fast and efficient to extract 4 bits and send them:
Once I have those 4 bits, I have to still convert them to a volume value I can set the volume register to, so for that I made something called a lookup table, basically that is a list of values that get picked depending on whatever bits are set on the input:
This lookup table takes those 4 bits as input and gives me a byte where I already set the volume for both left and right channels depending on what 4 bits were set, I then write that value into the volume register, enable the square channel I used at the time, let the cpu just do nothing for a while and then disabled the square channel again. That process then just was repeated for the other half of the byte, the next byte then got read, repeat.
Do that process for the whole cart and you got yourself a full game dump!
With this very first test version I never made any full dump though because it would have been very, very slow, in fact I calculated something around 249 bytes a second only, so it would've taken over an hour to actually send over the 1mb of pokemon yellow, so I immediately reduced the delay in my pulses down to something a little better, giving me a speed of about 0.9kb/s.
The very first dump of my pokemon yellow cart with that speed took about 19 minutes and its hash indeed matched the hash of a known valid ROM so this was already pretty great! Now I did also focus on making this usable with very generic base recording settings of 44100hz 16 bit, which if we have a quick look of that volume test pulse from the last image how that looks now:
Makes those pulses pretty close together but still easily detectable. I did however push this together even more in my next test version, going up to a incredible 1.7kb/s, making a 1mb dump take about 10 minutes, but well, just have a quick look at the pulses:Warning: Spoilers inside!
Yeah thats not looking too good anymore, and indeed at this point it started having issues and sometimes not managing to produce a valid ROM anymore, because the small points where the volume goes up and down now sometimes shifted into a bad position where it enabled the volume at a point where the top peak was exactly at the corners of it, so it was not properly picked up anymore by my receiver.Warning: Spoilers inside!
So this leads me more into my current design from where I went away from the original square channel I used over to the wave channel, which purely on accident I discovered to be incredibly stable at outputting a smooth volume, which led me to speed this up one last time to about 2.2kb/s, meaning now 1mb actually only takes 7 minutes and 50 seconds, which I think is a pretty excellent time, with that even the biggest cartridge size, 8mb, only takes a little over an hour, just as much as my very first prototype send would've taken for 1mb!
Lets also have a quick look of the volume pulses now:
As you can see, it is pretty easy to see what volume is what now, the only thing I had to change in my receiver was the way I now picked up "silent" areas, you see before that blue line in between pulses was always around the 0 marker, but now because every pulse always goes into the positive side, that "silent" line goes into the negative line simply because of filters automatically trying to center the signal, so now instead of checking against 0 I just subtract the "silent" part from the top peak read to get the absolute peak value, this method turns out to be very consistent and I managed to easily dump 12 carts I had laying around without a single issue or hash fail! Also I since added some statistic printouts on my receiver application to see how much the volume levels it measured deviate from the originally measured peaks and all those values are always incredibly close to the original calibration, so I think as it is right now things are very good.Warning: Spoilers inside!
At this point I also re-did my whole set of emulator inputs for installation because I felt the original method to get to code execution was rather ugly and took too long, the previous video I showed took 22 minutes and 32 seconds to finally show a textbox with text I wrote on screen, so now knowing a bit more about pokemon yellow I cut out quite a few things that were not needed, this led me to make this new setup:
Which, spoiler, only takes 15 minutes and 25 seconds to show a textbox with text I wrote! So just by better planning I was able to save over 7 minutes which I felt like was pretty good now, thats where my setup is now.
With this new setup and everything I felt like it was finally time to also add some form of graphics to my dumper, so far it has been showing nothing on screen and only waited for you to press start and then started sending whatever it read back from where the cart is supposed to be, no matter if it was there or not. Because there was still all of the games font in VRAM, I finally wrote some routines that clear the screen and then display some of my own text using the leftover font, which right now looks like this:
Once you press start, it will then attempt to read some bytes from the addresses where the cart is supposed to be, those bytes always have to be the same on every cart for the gameboy to accept it, else it will just freeze on the gameboy logo, so its a great way for us to also check those to make sure everything is good, all done in this tiny bit of code:Warning: Spoilers inside!
If everything is good it will then ask you to press A to start sending the cart, at which point you can record it on a PC of course. If one of the bytes does not match, it will print an error message instead and ask you to press A to return to the insert cartridge screen again so you can make sure the cart is inserted/clean to be read. Once you do accept it and its sending it also displays that as a message of course:
and once its done, it will ask you to press A again to return to the insert cartridge screen if you want to directly dump another one, or you can just turn off the gameboy of course. So there we have it, the sender broken down into what it is right now.Warning: Spoilers inside!
Now for the receiver side, I really dont feel like going into too much detail on that because it was a pain to write it up into a good working state, right now it takes a simple .wav file as input, also it specifically has to be normalized in audacity before exporting because of my hardcoded values for what volume level counts as "silence" and what count as "peak".
It basically takes a set amount of samples where it thinks should be silence, a peak and then silence again, compares the samples against some hardcoded values that I just decided should be big/small enough to count as silence or a peak, and from there it writes that read byte into a new .gbc file until the recorded .wav file is done being read. Also in the end it prints out crc32, md5 and sha1 hashes of the file it just wrote, all that output looks like this:
In this particular instance I converted my dump of tetris dx over into a .gbc file, and if we just quickly compare the hashes against the no-intro set you'll find that indeed, this dump was valid:Warning: Spoilers inside!
Also of course if you want to read that long receiver code for yourself:
Oh I should mention, this entire project, sender, receiver and installer is on my github, so you can go explore everything of course:
So there you have it, my current state of this crazy project, as of right now to use it you need a gameboy player with some form of modded gamecube with a sd gecko to use gameboy interface to install the dumper onto the german version of pokemon yellow, in the future I would like to of course also support at least the english version as well, it all just so happens to be for the german one because well, I am from germany so thats the cart I have laying around here. Also it most likely can be done for many other pokemon games as well, not just generation 1 games but also generation 2 games have exploits to install code into as well.
The sender I would also like to extend further to include save dumping, that should also be very easily doable, it is not in there right now because my current installer can only install up to 636 bytes of code into the pokemon save and well, my current dumper code just so happens to also be 636 bytes, yep, I already had to fight to even get everything crammed in. That said I did look a bit more at the save today and saw that I can safely extend that space up to 1122 bytes, I just have to update my installer for that and then I should be able to get to that feature as well.
Anyways that is it for now, hope this was interesting to at least some, thanks for reading.
You need to be logged in to comment