Beginner's Journey in Super Nintendo Development


Despite my ability, that being of a computer programmer, being one which is insufficient to a degree to which it would trigger several mental cataclysms in people of fields totally unrelated to computer programming, causing them to throw several, maybe a few dozen, rotten tomatoes at my face, I have decided that, for my own personal obsession, I would begin programming the Super Nintendo. Now, why would I choose such "antique" hardware to further progress my programming skills into shit-dom? Well, to put it simply, I plan to never get hired as a computer programmer, thus I have the freedom to be as experienced or as delirious in the field as I want.

You may notice (and, by the way, good on you for noticing!) that this site's... "aspect", is relatively dull. No magic buttons, or aero effects -- I saw one site which had a very faithful recreation of it. Safe to say, I was wildly impressed that sites can do that! -- or custom cursors, or, really, anything even slightly interesting, apart from maybe the custom Windows XP borders and the dark blue gradient background. You will notice this dullness as we go on in this matter of writing for the SNES as well.

You may have noticed the little "--" symbol, and have gotten a little jolt. Was this page written by a robot? Or one of those Artificial Intelligence chatbots that can speak like a rotten business man? These are all fair questions. And I will answer you directly: firstly, have you seen a business man that actively throws jabs of self-deprecation about anything he does? Well, maybe there are a few on the planet. But I will tell you this, most business men look up to bigger business men, or rival them. You'd best bet that their ego is in an exponential increase. To become a bigger business man, you jerk your ego against the one you want to rival. So, technically, I would be dethroned the second I would step in position as some C.E.O. Not that I'd want to, anyway. It's a job that would probably give me profound emotional disgust. No offense to the C.E.O's that do manage things in an ethical way, although I'm not sure how many of those there would actually be. Secondly, do not fret, Artificial Intelligence has no place in my writings about my mental illnesses upon things or people or the world. As far as I'm concerned, I don't even know the name of the symbol, or the character code for it.

I have named this blog entry as "Beginner's Journey in Super Nintendo Development", if the massive text at the top of this page and the blog entry name on the home-page did not indulge you into the idea, somehow. The point of this is that I am a beginner and that, despite my unrelenting will to want to know everything, I will never know everything. Or anything, really. All I know are some premises that happen to work in the Universe of which I am a part of. I can't speak for other Universes, however. I can't speak for whether other Universes exist, either. How would one even know? Do you just... walk in into an intermediary chamber, something like a Multi-Universal Hub, and declare yourself to be a visitor, a guest who that Universe will have to accomodate its own part of the chamber for? Do you know whether it will be kind to you, be guest-friendly, or was it not expecting any guests? Will it be an awkward meeting between two beings that have no conception of one another except for that moment in which they first perceived each other? Would they even perceive each other? Well, technically that Multi-Universal Hub would probably take care of that issue. Engineers are wildly imaginative by profession. Oh, crumbs. This is all working to some result that has nothing to do with what you came here to read about. I hope you'll forgive me.


The Super Nintendo. Came out when it came out, late 80s-ish, and died off when it died off, late 90s-ish. Its popularity probably decreased as people flocked to the Nintendo 64, I reckon. I think the Original Gameboy came out around the same time as the Super Nintendo, and had a longer lifespan, going as far as the 2000s! Seems even a system with a Picture Processing Unit that could read 16-bit color, with 4 available backgrounds and a whopping 128 sprites (in 1 mode) plus a pseudo-3-dimensional mode (Mode 7) really could not beat a little gray box running Super Mario Land, a game with 4 total colors. Handheld consoles really did not have to be technically impressive in terms of what they could do to implode minds.

Do not get me wrong: the Gameboy was very impressive, at the time, for what it could fit in a small box. It is normal and expected that it would have suffered some small sacrifices in terms of what it could do graphically or sonically.

And there were some more graphically/sonically impressive ones. There was the Lynx from Atari, there was some handheld by SEGA (spoiler: I found out that it's called a Game Gear), they made a little Sonic game whose music sounded quite tinkly. Like a urination session, but if the toilet was yay-high. I haven't read a little of anything about those systems, but from the looks, they did have color. Lynx, I think, had sampled sound. Do not quote me on this. Anyway, despite those more technically impressive attempts, Nintendo dominated the "greater" handheld market, along with Tiger on the "penny stocks" handheld market.

Ahem.

Nintendo made a new Gameboy called the Gameboy Color. Well, it's a Gameboy. With color. Not really anything different sound-wise but, to be fair, a sound update I don't think was entirely necessary. It became "iconic", so to speak, the Gameboy sound. So they didn't change it.

As a small data-sheet for you all pointed to my own bodily structure, my own brain seems to have a habit of rambling. I hope you will eagerly find a trick to shut me up. But it'll be a while until you do find it, or if anyone cares to find it. Maybe you do prefer me talking about things that you and I find no case, currently, where we care about them. That is because the premises actually lead to the case where I care about them, and you do not! Pah! An EXCLUSIVE OR operation! George Boole was not a stupid man, I tell you, and neither were the people who constructed relay devices. I should probably read about relay devices. There was a point where I was interested in Ladder Logic, which I've read operates with relay logic quite a lot, however that obsession ended after 61 minutes.

Nevertheless, I shall begin, for I believe that if I am to be afraid of this hardware, I'll never end up making anything! And I do want to make things for it.


THUS, I shall explain.

The Super Nintendo consists of a CPU called the Ricoh-5A22. It's based on the "WDC-65816 CPU", courtesy of Wikipedia naming this for me. The photo which shows the microprocessor as-is has these following text strings imprinted on it:

"W65C816S8PG-14"
" SA0150A "

Now, you may be asking, what do these two strings mean? Well, there's the processor name in it, "W65C816", but what about everything else? Well, I am delighted to tell you, dear reader, that I do not know either. I could guess that "PG-14" stands for something like 'Programmable-14', and not the UK's PG rating system. I'll tell you this with all integrity and with the hope that you believe me with full confidence: there is nothing explicit about programming a processor. Well, unless you wish to make Atari sex games. Which, if you do, that's... a choice. A perfectly respectable choice. There is nothing wrong with that. Just do not show it to anyone under the age of 18. If you do, you have lost all integrity, and you have lost all legal ability for free speech, I think. Well, I don't know the laws, but I do wish it was the case.

Anywho, I presume that you do not care about any of this in the current circumstance, so I'll continue with more pressing matters.

The 65C816 is similar to the 65C02, which is an update to the 6502. If you have programmed for the 6502, this processor's language will be very familiar. Well, basically identical, only for a few changes:

-The registers are now 16-bit! That's right, 16-bit registers on a 6502-like processor. Now, these are permanently 16-bit as in container size, but you can switch from 16-bit to 8-bit any time you like in the program. The modes for 16-bit or 8-bit just represent how you want the registers to process data, either byte-sized (8-bit or 1-byte) or word-sized (16-bit or 2-byte).

-You have a few new instructions.
REP |immediate value|
SEP |immediate value|
INA
DEA
STZ |memory address|

Well, these are the new ones I've learnt. I'm pretty sure there's a few more, but these are the ones that I have found necessary for now.

In terms of work registers, you will deal with the same stuff as on a 6502. You have an "A" register, representing the register where you can do arithmetic operations, and two "pointer" registers, "X" and "Y". I call them "pointer" registers because you can use them as offsets to point to different offsets (word repetition be damned!) of a certain memory address to grab byte-sized or word-sized values from there. Be advised that memory addresses are still processed through the work registers as word-sized addresses, thus $0000, $1234, $9999, and they are written in hexadecimal. Thus, you have the "beautiful" counting system: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, and F. It may be tricky at first, but trust me that, as you write more and more in this system, you will begin to naturally count in it. Not that you'd want to count in it naturally, I've yet to see people who do enjoy calculating in hexadecimal notation. But you know, if you want to.

In terms of the instructions, REP clears the processor status in terms of the bits which are 1 and 0 in your immediate value. SEP sets the processor status in the same way with your immediate value. Clearing just means turning a bit to 0 regardless of what it was before, and setting means to turn a bit to 1 regardless of what it was before. So, if you have a processor status of 12, which is 00010010, if you wanna turn those 1s to 0s, you do REP #%00010010, or you can do REP #%11111111, or REP #$FF respectively, to clear out every bit, and then you can set manually what bits you want with SEP. But since this specific example just clears out everything, you don't have to set anything to 1.

I do love examples that don't turn out the way you expect. A.k.a: faulty examples. Your frustration must be palpable.

On the next instruction, we have INA. If you've programmed a 6502 before, you would know that there are three little instructions, INX, INY, and INC |memory address|. INX increments the X register, incrementing being an addition of 1 to whatever value you had in the register, INY, which increments the Y register, and INC increments any writeable and accessible memory address. I say accessible because maybe you would deal with a system which has a bank system, which means it has a lot of memory addresses which simply cannot be accessed by a 16-bit value. Which means you'd have to switch banks, do a write, then switch back. Oh, I am burdening your working memory with a lot of stuff that I should talk about later, and not now, I believe. Anyway, for INA, since there is an A register, take the wildest guess at what INA does.

...

That's right, it increments the A register. If you have guessed that correctly before I had told you the answer, you may be eligible for compensation by none other than the Antarctican Government, with a check of just under 1,000,000 EUR. Just under. Think about it...

You may wonder about why they did not have an instruction such as INA on the original 6502? Well, the reason would be beyond my mental faculties, therefore I will attribute it to some idea of profound genius. "It was not a necessary feature, and we got with it reasonably well", a begrudged, old, mustached engineer with 365 years of experience would say... Oh, crumbs, yet again, because they did end up redesigning the 6502 and added this instruction anyway, on the 65C02. So, maybe... not such an idea of genius to leave it out.

Would you believe me If I said that this brokenhearted, 365-year old engineer would also be rotating 365 degrees every 3.65 seconds? I wonder if he would have jets of cosmic matter shooting out the top and bottom of his body, like a pulsar? I wonder whether the density of this reasonable man would be a density such that a teaspoon of his brain would equal the weight of the entire planet Jupiter? Maybe the Moon? Either way, such a hypothetical man would probably have to be outside of our Solar System. Imagine the gravitational waves this man would produce through his sole existence! In about just 4 years, it probably would have disrupted the already chaotic nature of our Solar System. It would probably throw the Earth off-balance from its orbit, spiralling its temperature either to TOO HOT or -275 degrees Celsius. Or 0 Kelvin. Or something, something Farenheit.

This man would have to, by necessity, be located in Interstellar space. Oh, to be recorded peacefully pulsing away by some Radio Telescope on Earth. The dream life... frozen, possibly with collapsed lungs... dead for 365 years... talking about the INA instruction being an unnecessary bloat upon the perfection that would be the 6502 processor...

You know, there were some people in the past speaking of Pulsars as possible "light-house" points. I believe that, on the Golden Record of the Voyager spacecrafts, one of the two, has a map which locates the Sun using the closest pulsars to our Solar System. Or was it just normal stars...? I don't remember. It would be cooler if it was just pulsars. Well, technically, pulsars would probably be better for the job, since they can be a lot brighter than the closest normal stars around our Solar System, considering they're, well, they're red-dwarfs. Not exactly prime candidates for "bright coordinate pointers". Pulsars would also be highly recognizable because they... pulse. Compared to normal stars which do not pulse. I'd choose them too, if I was a scientist.

Back to our little old engineer, if he was in our Solar System, before he would get to even program for the 6502, or the 65C02, or the 65C816, to get to even complain about the "INA" instruction, he would have, most assuredly, already placed the Earth in jeopardy, either burning like Hell, or freezing like... Heaven? Well, no, Heaven wouldn't be frozen, and I'd say it was never described as such. Maybe in Dante's Inferno, there was something about the perception of time not being necessary, thus time "felt frozen." Oh, that's just my memory of Wendigoon's video on Dante's Inferno. But as far as I am concerned, it was never described, in either Biblical texts, or literary works based on the Bible, as a literal frozen place. It would be a cool idea, a frozen Heaven. Anyway, this engineer would have had to be an engineer already for around "the year of the birth of computing" minus 365. So, maybe 1938 - 365 which would equal around... well, he'd have to have started being here from the year 1573. Bloody Christ! If we assume that it would take 4 years to destabilize the Earth, then in 1577, we'd have either one of the Apocalyptic cases. Well, technically, he couldn't even be a computer engineer for 365 years if, somewhat improbably, he does not destabilize the planet. The field of computing, if we assume that 1938 is its start with the "BOMBA" computer used to decrypt the Enigma code, built by the Polish, and we do 2026-1938, we get around 88 years. Well, what a bummer, isn't it? Ok... new calculation:

2026 - 88 - 365 = 1573... hm. I don't know why I calculated this. This would, obviously, give me the exact same result.

Ok, I guess he'd need around 365 years of general engineering experience since 1573, and another 88 years for computer engineering and programming experience. Which means that, if we are to assume he was not thrown into engineering child labor "programs", that he started at a relatively decent age, around 15 years old. So, add 15 years from 453 (which was our result of 365+88), and we'd get 468 years that this man would have been to be totally alive for. Which means he would have been born in 1573-15=1558. Which, I swear that it sounds like a year where something very important happened. I will look up "important things that happened in 1558", and looks like Wikipedia has something already.

Oh, wow, it has the whole year. And it looks like it is the year in which Elizabeth I is selected to reign as Queen of England on the 17th of November after Queen Mary, the previous queen, dies of uterine cancer at 42. Well, Elizabeth I's reign starts less than 3 weeks after December 7th, it seems.

There's also explorer Anthony Jenkinson which noticed, after traveling from Moscow to Astrakhan and Bukhara, that the Amu Darya had started flowing into the Aral Sea. I'd say this fact is a bit more interesting for me than which one gets the holy royal crown. Now I'm just reciting Wikipedia entries. If you were expecting just SNES development logs, well, you are sadly mistaken. This is my blog, and I get to say what goes on it. If you were expecting an apology, you may stop looking for one, for it is not here. I guess that would make me a good royal. Wink wink.

Anyway, back to what you may actually be interested to. The Super Nintendo.

I am going to assume from this point on that you are familiar with the 6502. If you are not, you may visit 6502.org to learn about its instruction set and so on. If you are familiar, I will press on.

The memory layout of the Super Nintendo Entertainment System (really stretching out the name) follows a bank structure, similar, I've heard, to the NES. Each bank, being a number from $00 to $FF (thus a 1-byte-sized value), has a list of addresses ranging from $0000 to $FFFF (a 2-byte-sized value, or a word). Memory addresses, thus, tend to look like, if you take their long form (bank and address), as $000000, the two nibbles (or the byte) on the most left (nibble being the cute name for hexadecimal digits) being the bank number, and the four nibbles (or the 2 bytes or word) on the most right being the memory address. For example, a long address like $7E0000 would be read as:

Bank 7E, Address 0000.

Funnily enough, this bank is used (as in I use it) as the Zero Page, where I can hold variables which I consider important.

If you wish for a look at the entire memory map, I recommend this SNESDEV link. It shows the banks on the top, and the general markers for the different parts of memory.

When I began reading the memory map, initially I was confused. I first believed that the numbers on top of the picture were the actual accessible memory addresses, and that the numbers on the left were the bank numbers. This was obviously contradicted as soon as I read 3 more words on the SNESDEV wiki, when they mentioned bank numbers, and every number there was always a 1-byte value. Have I gotten it wrong? Have I, with my stupendous 3 functional neurons, gotten this simple fact wrong? Rest assured that I, of course, have. Eventually, I understood my foolish, foolish mistake, and had gotten to reading about more interesting things. Who am I kidding, this console's technical details are heavily interesting from top to bottom. Did you know that the SNES CPU, the Ricoh-5A22, runs at 3.5 MHz? As in 3.5 MegaHertz? It is actually slower that the SEGA Genesis/Mega Drive's CPU, the 68000, which was 7.5 MHz. I could talk all about the 68000 when I first started programming for the Amiga, well, programming in big quotations, but that's... for another blog post in the future.

Thus, we begin. And how does one begin? Well, if you plan to code your SNES program in C, well, look on the same wiki for a C compiler for the SNES. There probably is one. In my case, I've turned to just writing my game directly in 65C816 Assembly. So I had to, naturally, use an Assembler. 64tass was one of the links on the SNES Dev wiki and, so far, I have not regretted it. You may plead with me to not torture myself, to not be a masochistic machine. "Oh, MusicPlumber, do not enter Hell and scream in woe, for there are gentler fires to be groomed by elsewhere..." But I will turn your offer down, for I do enjoy working in 65C816 Assembly. That is either my Stockholm Syndrome speaking or a genuine enjoyment for pain and, at this point, whether I can tell the difference between the two is irrelevant, for the enjoyment exists.

This specific page will, of course, explain the process that I went through in 65C816 Assembly, using 64tass, to make a simple, functional SNES ROM. This will use the BSNES Emulator as a Debugger. I've heard that Mesen is a little more accurate, but BSNES will do for now.

First things first, an SNES ROM has, usually, a header. The SNES DEV wiki mentions that actual SNES hardware does not need the header in the ROM, however emulators require it be present, as indicators of what pieces of hardware are necessary for emulation.

The header starts at memory address $FFC0, and ends at $FFE0 which... if you do some quick math... ends up being around 32 bytes. The first 21 bytes represent the name of the game. Literally, in ASCII. The name of it. Now, the name may be smaller than 21 readablecharacters. But that's fine, because you just put spaces to fill up that 21 byte requirement. The previous 11 bytes represent some auxiliary stuff, but it seems that an important thing is the checksum. Now, I don't have any SNES hardware, so I'm doing this with an emulator.

The header I've put is this:


; --------------\. ; SNES HEADER | ; --------------/' * = $FFC0 ; Header starts at $FFC0 .text "Very good game :) " .byte $20 ; Selecting LOROM .byte $00 ; Cartridge type, 00: no extra ram, 01: extra RAM with battery .byte $08 ; ROM Size, $08 means 256 KB .byte $00 ; Backup ROM size, none, thus $00 .byte $01 ; American ROM Identifier ($00 is Japan, $01 is America, $02 is PAL or Europe, I guess) .byte $00 ; Publisher ID, dunno what I should have put here so I just put $00. ???? .byte $00 ; ROM revision number .word $00 ; Checksum, I guess? .word $FFFF ; Another part of checksum, I guess.


Now, I myself have no certain confirmation that it would work on real hardware, except that BSNES runs it fine, snes9x runs it fine. Haven't tried Mesen, but I'll try it eventually. And, since BSNES is an emulator that is built for accuracy, I do have a probable confirmation that it would run on real hardware, as far as I'm concerned.

Next comes the Vector Interrupt Table. This part is actually completely necessary for code execution and, well, interrupts. Otherwise, you can run the code all you want, but if you don't have this table, the SNES will start running your code, unbeknownst to you, at $0000, which isn't cleared, and is full of random bullshit. If you're lucky, you may stop the debugger just as it catches a part of your code. I do hope that you, dear intellectual or non-intellectual, do not base your own hopes on that little problem to correctly run your code. So, write the damn table. The table starts from $FFE0, immediately after the header, and ends at $FFFF. It's mostly filled with 0s, formatted in 16 words. The last 2 words are the most important. The penultimate word represents the memory address of where your code starts, and the last word represents an interrupt address, which you can leave as 0 for now. Since it starts immediately after the header, and if you've filled out the header correctly, you don't really need to add a directive for a certain address because the VIT (Vector Interrupt Table) will start exactly where it's supposed to, aka at $FFE0.


; ----------------------\.
; VECTOR INTERRUPT TABLE |
' ----------------------/'

.word $00, $00, $00, $00
.word $00, $00, $00, $00
.word $00, $00, $00, $00
.word $00, $00, |your code starting label|, $00


After one has finished filling these up, well, you can start to actually write a program. Just make sure to write the starting label of your code in that second-to-last word from the VIT.

...Oh, I hope you didn't think we immediately start to write cool code yet, did you? The SNES doesn't have any preprogrammed section. Everything is either done by hardware, which is relatively empty in terms of actual CPU instructions, or it is not done at all. There is no BIOS, there is nothing at all. There is nothing set up for you. Everything is, well, up to you. If the registers, the stack, the PPU, isn't already set up and cleaned up by your program to deliver a different program, well, you're going to have a lot of trouble. So, what does one need to do?

Well, first you have to clear the emulation flag. The SNES starts in 65C02 mode, 8-bit mode, and you have to set it to 16-bit mode so that you can actually access the 16-bit features of the 65C816. Since it's a kind of register, but is unaccessible through normal means, what you have to do is clear the carry register, then exchange the carry register's data with the emulation register's data. Thus, you get two states like this:

1 C=|random value|, E=1 - Carry would be at some random value
2 C=0, E=1 - After clearing the Carry
3 C=1, E=0 - After exchange

Now, after you do this, you've got to also direct your assembler, to tell it that you switched to 16-bit registers. It doesn't just know, of course. You do that with this directive: .xi

If you don't do that, you'll quickly run into the problem that it will not assemble at all, because it will think you're trying to fit 16-bit values in 8-bit registers, even if you have done the switch in the code. The reason it doesn't just guess, I guess (guess guess guess...), is because 64tass assembles for many different 6502 variants. It's not gonna keep track for every individual way of the processors of what it switches, and stuff :P

Then, what you do is set the pointer registers (X and Y) to 16-bit, and keep the accumulator (A) as 8-bit. Now, when I read this in the default init code provided by the SNES Dev wiki (on which my Init code is written from), I thought that I did want all registers to be 16-bit. "More bits = better", right? But no. In practice, you will absolutely need a way to quickly write an 8-bit value to some memory places, 16-bit values to others. But even so, for communicating with the hardware (aka PPU, APU, etc), you'll mostly use 8-bit values. And, because the accumulator is the one register on which you can actually do arithmetic, it's good to keep it 8-bit. Now, why set the pointer registers to 16-bit, in contrast? Well, if you think about it, if you have a memory address like $1000, you can access, say, $FFFF by having one of those pointer registers set to #$EFFF, and then doing something like:

LDA $1000,X (if #$EFFF was put in X)

Now, sure, it's not the most technically justified decision through this reason which I found just 3 seconds ago (courtesy of being an idiot, I guess), but so far, it's the only one I can think of. It just allows you to access more memory without more beforehand memory calculation. Well, in your head. The process would be pretty much identical, as far as I'm concerned. So, there's really not much waste there.

There is definitely a more deeply technical reason for it, but I won't even bother to derive it, for it is 6 in the morning as I write this.

Anyway, you set the accumulator to 8-bit and the pointer registers to 16-bit. You do that by clearing the processor status register with REP #$ff, and you set the processor status register to this binary value: 00100100, or #$24 in hexadecimal. So the instruction afterward is SEP #$24. Now, my memory is faulty, currently, so either the 3rd bit from the right is the one that sets the accumulator to 8-bit, and the 6th bit from the right sets the pointer registers to 16-bit, or it's the other way around. Either way, it does the job.

Now, the stack. In the original Init Code, the instruction sequence was something like:

LDA #STACK_BOTTOM
TXS

Now, the understanding is clear. Load the value, which is the stack bottom value (an assembler directive), into the A register, and then transfer whatever is in A (which is the stack bottom value, duh :3) into the S register, which is the stack pointer register, I assume in good faith. Now, that specific page never actually specified what #STACK_BOTTOM is. So how am I supposed to find out? Well... I don't know. So I just put #$FFFF in place of what was at #STACK_BOTTOM. Whether this ends up overwriting my code in memory eventually, only the time when I push anything or pop anything to the stack will tell. God be with the blessed, XOXO

Here are the next two instructions. And, yes, I say this with full confidence that I have no idea what these do. Something important, definitely, otherwise they wouldn't be here. So, we have

PEA 0
PLD

According to the 65c816 documentation on 6502.org (really good site, I recommend it), PEA means "Push Effective Address". Which, since it has 0 next to it, I guess it pushes the address 0....? Or something? Then, PLD means "Pull Direct Register". What's the Direct Register? I don't know. There is the one before it called "Pull Data Bank Register", but that one is PLB. PLD, I have no idea where that pulls to. But it does pull the value 0, since that was what was pushed earlier. So, good on you, "Direct Register". You are now equal to 0. Congratulations! My most heartfelt felicitations.

The next one is a more understandable one. Well, one of the instructions, at least.

PHK
PLB

I've seen the K register in the debugger, and I think it's maybe the databank register. But the thing is, this code wouldn't make sense if that was the case, because you'd be pushing the value of the databank register, and... pulling it back. It's the same value, so... K is definitely something different. Since it is probably a value that the bank needs to be set to, there's something about K that is bank-like, but is not the same as the other bank. You know, like B. Moving on...

Now, the next two instructions. You disable interrupts by setting $4200 to 0, and you disable HDMA by setting $420c to 0. So:

STZ $4200
STZ $420c

You disable the screen afterwards, and turn on the forced VBLANK, so you can edit VRAM, CGRAM, and the PPU registers, well, I think that that is the reason:

LDA #$8F
STA $2100

Next, you set registers $2181, $2182, and $2183, to 0. According to the original init code, these 3 instructions set the Work-RAM address to 0. The original init code names these as WMADDL, WMADDM, and WMADDH, which I could only interpret as "Work-RAM Address Low", "Middle", and "High". If that's the case, then that would be a 3-byte address! Wow! That is pretty lengthy. 24-bit!

STZ $2181
STZ $2182
STZ $2183

Next, you set register $4300 (being DMAP0, which I could only translate as Direct Memory Access Processor 0, I guess) to value #$08, which apparently means to set it to a "Fixed Address Transfer to a Byte Register" mode. Then, you logically AND the value of WMDATA, aka the number of register WMDATA which is... if I check my notes, uhhh, register $2180, with the value of #$FF. Which is, well, if you do a quick AND-mask glance at #$2180 and #$FF, you see that $FF is basically $00FF, which means you get #$80. So you end up storing the value #$80 into register $4301, which is BBAD0. Not BAD, BBAD0. I don't know what BBAD0 means linguistically, but it does say "DMA transfer to WMDATA", which I guess means it sets the transfer register to WMDATA. Which, as you'll see later, a lot of these registers increment automatically. And, I guess this one does too.

LDA #$08
STA $4300
LDA #$80
STA $4301

The next few instructions would have been:

LDX #.loword(WorkRamResetByte)
STX A1T0
LDA #.bankbyte(WorkRamResetByte)
STX A1B0

But I when I first copied the Init code, I replaced them with:

STZ $4302
STZ $4304

But now that I come to think of it, I have no idea what WorkRamResetByte is supposed to be. Is it supposed to be 0? Is it, perhaps some specific, weird little number that the Work-RAM needs to be set to? Apart from 0? I'll just assume it's 0 for now. The first instruction I wrote may be fine, possibly, but the second, I think I'd probably have to do a sort of bank-transfer. Maybe. Take the current value from either K or B, and use that.

Wait... Low word, and bank byte... FLASHBACK.....!!!!:

"Bank 7E, Address 0000."

That's right! ".loword", as in Low-word, means the Address, probably, and ".bankbyte" means... the bank byte! Right, so it probably is that. It could probably be written as:

LDX WorkRamResetByte
STX $4302
LDA #$80
STA $4304
...
;Later in the data declarations
WorkRamResetByte: .byte $00

It's probably that. And, well, after I checked the debugger, I was confused before why the Work-RAM was full of garbage values. Now it's not! Seems it worked. Good stuff!