A question that i asked when i joined into the Sonic Hacking was about porting SRAM to keep the progress in the game. Theres a way to add it in the github disassemblies?
I wrote a guide on this 5 years ago, but I figured it was worth updating since my knowledge has grown and so I can add a checksum which was pretty requested at the time I made my original guide. I will make a disclaimer though that I haven't tested this code myself, but I'm sure others will let me know if there's something I messed up, and I'll be happy to edit this reply accordingly. Step 1 - Header We first need to let the console know that our game uses SRAM. This is easy enough. After the RAM range in the header is the SRAM type and range. In a Hivebrain disassembly it looks like this: Code (Text): SRAMSupport: dc.l $20202020 ; change to $5241E020 to create SRAM dc.l $20202020 ; SRAM start dc.l $20202020 ; SRAM end And in a GitHub disassembly it looks like this: Code (Text): if EnableSRAM=1 dc.b $52, $41, $A0+(BackupSRAM<<6)+(AddressSRAM<<3), $20 ; SRAM support else dc.l $20202020 endif dc.l $20202020 ; SRAM start ($200001) dc.l $20202020 ; SRAM end ($20xxxx) I really don't like the GitHub version's implementation of "EnableSRAM", it's misleading in that it only changes this part of the header, rather than doing any actual SRAM work as one may expect, and it turns what could be a simple byte value into an unnecessary equation... So with those grievences, we're just going to ignore it and write it my way . We'll change the first line of $20202020 to our SRAM type which will look like this: Code (Text): dc.b "RA", $XX, $20 The XX is the actual type and everything around it is just formatting. Here's the types you may choose from: $A0 - No saving, 16-bit values ($WWWW, $WWWW...) $B0 - No saving, even 8-bit values ($BB, $FF, $BB, $FF...) $B8 - No saving, odd 8-bit values ($FF, $BB, $FF, $BB...) $E0 - Saving, 16-bit values ($WWWW, $WWWW...) $F0 - Saving, even 8-bit values ($BB, $FF, $BB, $FF...) $F8 - Saving, odd 8-bit values. ($FF, $BB, $FF, $BB...) Wait- Why would we not want SRAM to save? Well SRAM is still RAM, so you can also use it as a form of external RAM if you're running low on internal RAM, neat, right?! But I assume that's not what you're looking for here. You want S3K styled saving. So we'll do it like S3K, which opts for the odd 8-bit saving type, $F8. The next 2 lines of $20202020 will be our for SRAM range. This will start from $200001 and can go up to $20FFFF, but you can also set the end range to be however many bytes you actually intend on using. But if you're lazy then just copy this: Code (Text): dc.l $200001 dc.l $20FFFF And with that we've now told the console that this is a game that uses SRAM. So we can start writing code, but first you need to know something. Step 2 - $A130F1 A concept you'll need to grasp about the Mega Drive (And the 68k as a whole) is the address bus. Think of the address bus as like a big pool of bytes where everything on the console lies. $000000-$3FFFFF is the ROM, The $A00000 is the Z80/I/O area, $C00000 is the VDP area, $FF0000-$FFFFFF is CPU RAM, and relevant to what we want is the SRAM area, which we defined as $200001-$20FFFF. And maybe you noticed a problem, but this is inside the ROM range of $000000-$3FFFFF. And we can't write to the ROM area, the RO means read only after all. So what do we do about this? Well, $A130F1 is a flag that will switch the console from treating the $200000 area as ROM into SRAM so we can write to it. However, this is not necessary if your ROM is less than 2 megabytes, because then the ROM space does not reach to the $200000 area. And chances are if you haven't severely expanded Sonic 1's ROM with say, a speed over space optimization like ROM chunks, then your ROM is probably only 512kb-1mb and you don't have to worry about it. But it is worth keeping in mind, if your ROM is 2 megabytes or larger, before you work with SRAM you need to set $A130F1 to 1 and then set it back to 0 when you're done. With that said, let's start writing code. Step 3 - Initialization Now you can just start writing and reading to SRAM from here, but that is dangerous! Why? Well we never initialized our SRAM. So on initial boot, there may be garbage data, or later on the data could get corrupted, and you could start loading bad values that may point you to an invalid level ID or something like that, which crashes the game. So let's first check if our SRAM is valid before we do anything with it. We're going to use 2 forms of validation for our save data. A header so we know if the save data has been initialized at all, or if our save data belongs to this game. Then we will use a checksum to validate that the actual data within is alright and hasn't been corrupted. If everything checks out, we move on, if not then we reset the data. We'll make some equates before we write any code here though so we can keep track of our data. And for our example, we'll save the player's level, score, lives, and continues. (I'll let you figure out the special stage and emerald count on your own (aka I'm lazy)) This is important because we also need to know where our data ends. Code (Text): sram_header equ $200001 sram_checksum equ $200009 sram_start equ $20000D sram_zone equ $20000D sram_act equ $20000F sram_score equ $200011 sram_lives equ $200019 sram_conts equ $20001B sram_end equ $20001B Now we can make our initialization function. First let's do our header check, this is a quick and easy check to see if real save data exists at all: Code (Text): SRAM_Check: ; move.b #1,($A130F1).l ; Enable reading from SRAM >2MB only!!! lea (sram_header).l,a0 ; Load the SRAM header. move.l #"SRAM",d0 ; Get the string we expect. movep.l (a0)+,d1 ; Get the data in the header. cmp.l d1,d0 ; Check if the data matches our string. bne.s SRAM_Init ; If not we need to initialize SRAM. I'd recommend making the "SRAM" string something unique to your hack in the event of another hack using the same code, so there isn't a potential of data contamination. Also! You may have noticed an instruction you haven't seen before, movep, this will be a very handy instruction when working with 8-bit SRAM and 16-bit or 32-bit values since the data is located on every other byte. It only takes word (.w) and long (.l) operand sizes. And it will send the source operand to every other byte of the destination operand (or the other way around since we're reading). So what we just did was take the data from SRAM which looks like this: Code (Text): $83, $FF, $82, $FF, $65, $FF, $77, $FF And just grabbed these bytes: Code (Text): $83, $82, $65, $77 Also keep this in mind for your future use. Now let's handle the checksum! Let's grab the checksum value and set our loop count (This is why we needed to know where our data ends): Code (Text): movep.w (a0)+,d0 ; Get the checksum value. moveq #0,d1 move.b #((sram_end-sram_start)/2)-1,d1 ; Set loop count. moveq #0,d2 And then the actual loop: Code (Text): @Loop: add.b (a0)+,d2 ; Add to checksum value. adda.w #1,a0 ; Skip a byte. dbf d1,@Loop ; Loop Then lastly our check, which is also where our validation ends: Code (Text): cmp.w d2,d0 ; Does our checksum match? bne.s SRAM_Init ; If not we need to initialize SRAM. ; At this point, we've validated our data! ; clr.b ($A130F1).l ; Disable reading from SRAM >2MB only!!! rts Next up, we'll write our reset function. We first correct our header to the string you chose, then initialize our values to whatever we want, and lastly, recalculate the checksum. You can really do this in your own way, I'd recommend a table so you can effectively set up the variables with multiple slots, and you can also precalculate the checksum to save processing time. But just for this example we'll do things simple and modular. Code (Text): SRAM_Init: movep.l #"SRAM",(sram_header).l ; Set our header. move.b #0,(sram_zone).l ; Set zone to Green Hill. move.b #0,(sram_act).l ; Set act to Act 1. movep.l #0,(sram_score).l ; Set score to 0. move.b #3,(sram_lives).l ; Set lives to 3. move.b #0,(sram_conts).l ; Set continues to 0. lea (sram_start).l,a0 ; Get the start of SRAM data. moveq #0,d0 moveq #0,d1 move.b #((sram_end-sram_start)/2)-1,d1 ; Set loop count. @Loop: add.b (a0)+,d0 ; Add to checksum value. adda.w #1,a0 ; Skip a byte. dbf d1,@Loop ; Loop movep.w d0,(sram_checksum).l ; Apply checksum. ; clr.b ($A130F1).l ; Disable reading from SRAM >2MB only!!! rts Then to wrap up initialization we just need to make a call to SRAM_Check, which we'll place near the other hardware initialization calls. Code (Text): ; GitHub .clearRAM: move.l d7,(a6)+ dbf d6,.clearRAM ; clear RAM ($0000-$FDFF) bsr.w VDPSetupGame bsr.w DACDriverLoad bsr.w JoypadInit bsr.w SRAM_Check ; <-- Here move.b #id_Sega,(v_gamemode).w ; set Game Mode to Sega Screen ; Hivebrain GameClrRAM: move.l d7,(a6)+ dbf d6,GameClrRAM ; fill RAM ($0000-$FDFF) with $0 bsr.w VDPSetupGame bsr.w SoundDriverLoad bsr.w JoypadInit bsr.w SRAM_Check ; <-- Here move.b #0,($FFFFF600).w ; set Game Mode to Sega Screen Step 4 - Reading and Writing Now with initialization all done, we can apply SRAM into the game! First we'll read it when we load up a level. So let's go to PlayLevel and replace these lines: Code (Text): ; GitHub move.b #3,(v_lives).w ; set lives to 3 moveq #0,d0 move.w d0,(v_rings).w ; clear rings move.l d0,(v_time).w ; clear time move.l d0,(v_score).w ; clear score move.b d0,(v_lastspecial).w ; clear special stage number move.b d0,(v_emeralds).w ; clear emeralds move.l d0,(v_emldlist).w ; clear emeralds move.l d0,(v_emldlist+4).w ; clear emeralds move.b d0,(v_continues).w ; clear continues ; Hivebrain move.b #3,($FFFFFE12).w ; set lives to 3 moveq #0,d0 move.w d0,($FFFFFE20).w ; clear rings move.l d0,($FFFFFE22).w ; clear time move.l d0,($FFFFFE26).w ; clear score move.b d0,($FFFFFE16).w ; clear special stage number move.b d0,($FFFFFE57).w ; clear emeralds move.l d0,($FFFFFE58).w ; clear emeralds move.l d0,($FFFFFE5C).w ; clear emeralds move.b d0,($FFFFFE18).w ; clear continues With this: Code (Text): ; GitHub movep.w (sram_zone).l,d0 move.w d0,(v_zone).w ; set level move.b (sram_lives).l,d0 move.b d0,(v_lives).w ; set lives moveq #0,d0 move.w d0,(v_rings).w ; clear rings move.l d0,(v_time).w ; clear time movep.l (sram_score).l,d1 move.l d1,(v_score).w ; set score move.b d0,(v_lastspecial).w ; clear special stage number move.b d0,(v_emeralds).w ; clear emeralds move.l d0,(v_emldlist).w ; clear emeralds move.l d0,(v_emldlist+4).w ; clear emeralds move.b (sram_conts).l,d1 move.b d1,(v_continues).w ; set continues ; Hivebrain movep.w (sram_zone).l,d0 move.w d0,($FFFFFE10).w ; set level move.b (sram_lives).l,d0 move.b d0,($FFFFFE12).w ; set lives moveq #0,d0 move.w d0,($FFFFFE20).w ; clear rings move.l d0,($FFFFFE22).w ; clear time movep.l (sram_score).l,d1 move.l d1,($FFFFFE26).w ; set score move.b d0,($FFFFFE16).w ; clear special stage number move.b d0,($FFFFFE57).w ; clear emeralds move.l d0,($FFFFFE58).w ; clear emeralds move.l d0,($FFFFFE5C).w ; clear emeralds move.b (sram_conts).l,d1 move.b d1,($FFFFFE18).w ; set continues This is an oversimplification of things, and you will have to apply it to things like entering the special stage directly through the level select. But it will work from just directly pressing start on the title screen. Don't forget about the 2mb limit! I didn't include it this time around so you will remember to do it yourself. Next, let's save our save data. We'll make this a subroutine since we'll also need to recalculate the checksum and it's better to just section it off than making a mess of the place we want to call it from. Code (Text): ; GitHub SRAM_Save: move.w (v_zone).w,d1 ; Grab all the values we wanna save move.l (v_score).w,d2 move.b (v_lives).w,d3 move.b (v_continues).w,d4 movep.w d1,(sram_zone).l ; And save the values! movep.l d2,(sram_score).l move.b d3,(sram_lives).l move.b d4,(sram_conts).l lea (sram_start).l,a0 ; Get the start of SRAM data. moveq #0,d0 moveq #0,d1 move.b #((sram_end-sram_start)/2)-1,d1 ; Set loop count. @Loop: add.b (a0)+,d0 ; Add to checksum value. adda.w #1,a0 ; Skip a byte. dbf d1,@Loop ; Loop movep.w d0,(sram_checksum).l ; Apply checksum. rts ; Hivebrain SRAM_Save: move.w ($FFFFFE10).w,d1 ; Grab all the values we wanna save move.l ($FFFFFE26).w,d2 move.b ($FFFFFE12).w,d3 move.b ($FFFFFE18).w,d4 movep.w d1,(sram_zone).l ; And save the values! movep.l d2,(sram_score).l move.b d3,(sram_lives).l move.b d4,(sram_conts).l lea (sram_start).l,a0 ; Get the start of SRAM data. moveq #0,d0 moveq #0,d1 move.b #((sram_end-sram_start)/2)-1,d1 ; Set loop count. @Loop: add.b (a0)+,d0 ; Add to checksum value. adda.w #1,a0 ; Skip a byte. dbf d1,@Loop ; Loop movep.w d0,(sram_checksum).l ; Apply checksum. rts I'd recommend calling SRAM_Save in _incObj/3A Got Through Card.asm at Got_NextLevel (GitHub) or Obj3A_NextLevel (Hivebrain) just after the next level ID gets set just before the tst instruction. Thaaat's just about everything I think. Again if someone smarter than me catches something I did wrong please let me know! I want this to be as in depth and beginner friendly as possible but I just don't have the time to test and double check at the moment. But I hope this helps!
EDIT: could a moderator pin this thread actually? This seems like a very pin worthy thread, cooljerk's link and Kilo's explanations are right up there for a thread worth remembering and linking. I want to expand on the above post. First of all, the above post is really good, you should definitely read it, I don't want my post to shadow it, mine will be more technical and on the circuit side. Also, this isn't meant to be one-up-manship, because one flaw of all posts/sites explaining SRAM leave one vital thing out, including Sik's page (plutiedev.com linked by Cooljerk, which again, is definitely worth a read). Why is SRAM only on odd addresses? I want to draw your attention to Devon's post here, this is worth a good read and it's related. As Devon correctly mentions, there are 16 data lines, he also mentioned the 68k "only having 23 address lines", this is also related. The 68k having a 23-bit address has bits 1 to 23, but no bit 0, so all addresses are even. You cannot access offset 000001, it's impossible. Instead, you access offset 000000 as a word, so addresses 000000 and 000001 are collected together as two bytes. If you load a byte from offset 000001, then the CPU is actually accessing word 000000 and 000001 together, and disgarding the upper byte. This goes into explaining the odd address error exception with the 68k. Accessing a word on odd addresses would mean accessing two words and disgarding two bytes. For example, if you wanted to load the word from offset 000001, then you need to access word 000000 - 000001 and 000002 - 000003, disgard the upper byte of the first word, and the lower byte of the second word, and patch them together. This is what the later 680X0 CPU versions do, and there's a penalty for it, as it'll cost you twice as many read cycles (as per Devon's post). If you've ever looked at a Mega Drive circuit board, you might notice something very weird about the RAM: ...there's two. Why? There's a data sheet right here https://www.alldatasheet.com/datasheet-pdf/pdf/113668/TOSHIBA/TC51832SPL-10.html You can see this particular RAM chip has 8 x I/O data pins (I/O1-I/O8), it's an 8-bit RAM chip. Since the 68k accesses data in words (16-bits), how do you hook up an 8-bit RAM chip? Well once again, there's two of them. The idea is, one chip will hold the odd bytes, the other chip will hold the even bytes, so when you write a word to address E00000 (or commonly FF0000), the lower word address 0000 pins (1 to 15) are setup on both RAM chips address pins (0 to 14), the upper 8 data pins of the 68k are connected to one RAM chip, and the lower 8 data pins are connected to the second RAM chip. So... When you write a word (two bytes) to offset FF0000 - FF0001, you're writing a byte to 0000 of one RAM chip, and 0000 of the other RAM chip, When you write a word (two bytes) to offset FF0002 - FF0003, you're writing a byte to 0001 of one RAM chip, and 0001 of the other RAM chip, When you write a word (two bytes) to offset FF0004 - FF0005, you're writing a byte to 0002 of one RAM chip, and 0002 of the other RAM chip, When you write a word (two bytes) to offset FFFFFE - FFFFFF, you're writing a byte to 7FFF of one RAM chip, and 7FFF of the other RAM chip, It's performing a single 16-bit write, but it's split up between the two RAM chips. If you write a single byte, only 8-pins will be active during the transfer, the upper or lower 8-bits will be disgarded. The same may very well be said for ROM cartridges, and it also explains why you get those odd SMD (Super Magic Drive) format ROM file rips, where it seems to be interlaced (even bytes on the first half of the ROM file, and odd bytes on the second half of the ROM file), it'll very likely be reading one ROM chip of the even offsets, and then reading the other ROM chip of the odd offsets. So when it comes to cheap SRAM, they often splash out on only one chip, and it'll be similar to the RAM chip you see in the data sheet above in the sense that it's only an 8-bit chip. Often they hook the address lines (1 upto 20) to the address lines of the SRAM directly, so accessing 200000-1 will access 00000 of SRAM, accessing 200002-3 will access 00001 of SRAM, only the lower 8-bit data lines are hooked up, the upper 8-bits are simply disgarded, hence writing a word to offset 00000 of SRAM only the lower byte will reach the SRAM chip. Hence the instruction movep which is specifically designed for peripheral devices of 8-bit origin, as it's assumed you'd hook up 8-bit devices to the CPU in the same fashion as the SRAM chip, a very cheap solution. It's why many I/O sections of the Mega Drive (including controller ports) are accessed on odd addresses. The Z80 is a special case, where bits 1 to 12 are hooked up to the Z80's RAM bits 1 to 12 (not 0 to 11), and the UDS/LDS (Upper/Lower Data Strobe) pins of the 68k are hooked up to bit 0 in a specific way: This helps narrow the 68k to access Z80 RAM bytes precisely in parallel with 68k address, A00000 will access Z80 RAM 0000, A00001 will access Z80 RAM 0001, A01FFF will access Z80 RAM 1FFF, etc, but of course, a word write will yeild one of the bytes disgarded. This exclusive detail is what's often missing when talking about peripheral access on the 68k, SRAM is a common example, it's always explained odd address only, but never why. Header format Again, not to one-up Kilo, he did an excellent job of explaining the header format, especially with the limited knowledge provided, but I would like to fix up and add to it, having read the above about "$XX, $20", I'd like to expand on this. Think of XX and 20 as binary: 1E1TT000 DDD0000 1 = Keep 1. 0 = Keep 0. E = Erase (1 No | 0 Yes). TT = Read/Write Type (00 both | 01 serial EEPROMS (RAMs with 4-bit data bus, etc...) | 10 even | 11 odd). DDD = Device type (001 SRAM | 010 EEPROM | rest = unused). Don't ask me where I got this info, I have hundreds of PDF files over the past 20 years, and I cannot begin to explain where I acquired all of them... I mean, feel free to be skeptical, and by all means challenge the above, it could always been wrong, but I'm feeling rather solid about this.
I will only add that the header itself is nearly a pure formality, the console itself doesn't care about it other than TMSS checking that "SEGA" (or " SEGA") is there at $00100, and CPU using the first 8 bytes of the ROM as stack and code start addresses. It really only aids the emulators and flashcarts that can use the info to enable/disable certain features, and not all of them even care about what is in the header. As far as actual cartridge hardware goes, unless the game is larger than 2MB, even the $A130xx write is not necessary. The write to $A130xx pulses the !TIME signal on cartslot which is used by games with larger than 2MB ROMs to capture D0 from databus to control if $200000...$3FFFFF is RAM or ROM (with the entire area mirrored as main RAM in its $E00000...FFFFFF area). Games typically use $A130F1 as the address but the cartridges themselves don't do any finer address decode and respond to all the addresses between $A13000...$A130FF, with $xxxx xxx0=ROM and %xxxx xxx1=RAM as data. Only the lowest data bit matters. Games that have 2MB or less of ROM don't have this banking mechanism at all and the RAM is always available in the $200000...$3FFFFF area. Some Sega documentation also refers to write protect bit, but this is present only on some devboards and not found in any of the game cartridge hardware. In context of Sonic games, write to $A150xx is needed since there's an oncart banker that has to switch between S&K ROM and backup memory in the second 2MB and even without S&K one has to write to the mapper to access save memory. Sonic3 uses an 8bit FeRAM chip (on odd addresses as all the commercial games) and not an SRAM+battery. This is fancy, especially in the 90s when this memory was still new. Sometimes the memory goes bad due to physical nature of it but there's even a fix for that. The game uses only one quarter of the space in the chip, with top two address lines going to a fixed state. To revive a bad Sonic3 cart that no longer saves you only have to separate one of these address pins and connect it to a different state. This is also a reason why the game works on a shadow copy of the save memory in the console RAM, which is to avoid doing writes to the actual FeRAM chip since there are limited number of writes that the chip can handle. For normal SRAM + battery things, there are no such problems to avoid and you can use the memory even as addition to console's own RAM if you needed more storage.
You know, I never really thought about not having odd access would mean the lowest bit wasn't connected. Does that mean the 68020 an onward are technically a 31-bit bus? Re: Interleaved 8-bit ram -- the Dreamcast does the same thing! The memory is two 32-bit SDRAM chips, read together as one 64-bit value. Anyone know if the Saturn does something similar?
68020 has all the address bits present on the bus, but because it uses different bus cycles where the size of bus can dynamically change according to what talks back. It supports mixed 8, 16 and 32bit accesses and the bus interface automatically splits things up as it talks to the external world according to what that world is wanting. But since the CPU really does have a 32bit data bus, all the smaller accesses reduce bandwidth since the bus is held up and gives only half or quarter of the data width. Saturn uses 16bit memories in it and they go in pairs on the CPU bus. PCs used to use 4 bit memories that were parallelled to fill the 8/16/3/64buses. Since SDRAM days, the memory chips are mostly 8bit wide but you perhaps can see narrower chips on the ginormous server RAM sticks.
Apparently, the same splitting of data between two 8-bit RAM chips is also used for VRAM when its unused 128kb mode is enabled. Two questions: I heard on a Discord server before that supposedly, SRAM can exist at say, $300000, but emulators might struggle with recognizing this. Is this actually true? Are flashcarts known to be able to handle 16-bit SRAM properly, as they do with 8-bit SRAM?
When I had my Everdrive X5 a while ago, it didn't play ball with 16-bit SRAM, only odd 8-bit SRAM worked. Haven't tested it on my Mega Everdrive Pro, though. Technically, you can have all kinds of configuration for SRAM (and other stuff like the Virtua Processor), it depends on what the hardware on the cartridge does. Emulators don't exactly have that luxury, as they would need to be updated to emulate such configurations.
Since no commercial game used 16bit SRAM, no flashcart supports it either, none I am aware of anyway. It is just extra cost for no benefits for vast majority of users. I now recall there are a few games that are 3MB in size and use SRAM without the A130xx banking mechanism, with SRAM being in the $300000...3FFFFF range. But I don't know which games, there shouldn't be many 3MB games with saving...
I'm hoping some commercial cartridge in the future does supports dual 16-bit SRAM, or else I won't be able to run programs from SRAM on my operating system... I'm to recall BlastEm supports word, and up to the 2MB mark, and assumes this based on the header, not sure about Fusion or Regen, but those both limit the range to a small amount, not even close to 1MB, so it probably wouldn't be useful if they could support word.
I vaguely recall that I found emulators of the time only supported up to 64KB, that's in the 2005...2010 range when I was most active in the MD dev things. I have no clue what modern options can provide... There is one cartridge that does have 16bit RAM, whole 4MBytes of it even lol (but no ROM of any sort). I made such a cart for Chilly Willy long ago, for 32X+MCD business although it turned out that 32X cannot actually write to ROM area, only read from it... which made the thing a whole lot less useful than originally hoped for. Many of the modern flashcarts are actually using RAM for their ROM storage, and in theory one can turn off the write protection and gain access to as much RAM as one wishes that way...