Page 1 of 1

Let's Write a Mega Drive Homebrew ROM!

Posted: Sun Nov 20, 2016 11:24 pm
by Dandaman955
Hey. Basically, I've had an MD homebrew guide I've been meaning to upload for a year now, that I never got around to doing. Since this forum could use a little something new, I've decided to upload it here instead. Bear in mind that this was written over a year ago, and while I've skimmed it, fixing any little mistakes I had made, I don't think it's perfect. If you spot any, point it out to me and I'll fix them. The first part is a bit formulaic because there really isn't another way to restructure initialisation code on the MD, but the next few parts will mix it up a bit. Any suggestions you have for writing a part 2, I will take on board. Enjoy!


Right, as promised, a homebrew tutorial for the Mega Drive. Had to split it up into parts because it's big enough as it is with the initialisation alone, and no way in hell am I going to get this finished in a reasonable time if I didn't. Started this on the 4th of November and I've been chipping away at it ever since, so there may be a few things out of date/sentences that sound like they were strung together. Enjoy! :]

So, to be completely honest with you, I wasn't particularly pleased to see the amount of homebrew ROMs submitted to the Sega Homebrew Competition this year. A few people have said that they would've submitted if they would've known how to create one, so I'm writing this tutorial in the hopes that there may be some more interest in Mega Drive homebrew ROM development. This guide will explain some concepts such as the MD header, sprite tables, DMA, SRAM etc.

For starters, I'll assume you have a basic knowledge of 68K commands and VDP/Z80/etc. commands. If you don't, then use these links as a reference:

http://mrjester.hapisan.com/04_MC68/

^ A 68K guide written by MarkeyJester, that explains it from a beginner's perspective, and should cover the commands that you will be using.

http://md.squee.co/VDP

^ A guide that will cover everything you will need to know about the VDP, etc.

You will need an assembler. I'm going to be using ASM68K, which can be found in any of the Sonic disassemblies. Copy it over, along with build.bat, remove all the lines except for pause, and change it so that it looks like this:

Code: Select all

@echo off

ASM68K /o op+ /o os+ /o ow+ /o oz+ /o oaq+ /o osq+ /o omq+ /p /o ae- Source.asm, ROM.bin
pause

This means that you will need to save your file as Source.asm. If you want, you can look up the manual and add your own commands to it, rather than copying them over from Sonic 1.

You will also need a debugging emulator. Regen or Exodus will do. Kega works fine for casual play, and can sometimes pick up on stuff that Regen doesn't, so keep that in mind.

Also note that I will supply the template code at the end of each tutorial, so you know what's going on.



PART 1 - THE HEADER

Right, to start with, the MD has a vector table which contains various pointers, such as the system stack, the entry point (where the system jumps to when it's turned on/reset), various interrupt pointers, etc. Let's have a look at our template vector table:

Code: Select all

StartofROM:
Vectors:                                                                     
                dc.l $FFFE00, Entrypoint, BusError, AddressError
		dc.l IllegalInstr,ZeroDivide,ChkInstr,TrapvInstr
		dc.l PrivilegeViolation,Trace,Line1010Emu,Line1111Emu
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ExtInt,    ErrorTrap
                dc.l HInt,      ErrorTrap, VInt,      ErrorTrap
                dc.l Trap0, 	Trap1, 	   Trap2,     Trap3
                dc.l Trap4, 	Trap5, 	   Trap6,     Trap7
                dc.l Trap8, 	Trap9, 	   TrapA,     TrapB
                dc.l TrapC, 	TrapD, 	   TrapE,     TrapF
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap

This is the vector table taken from Sonic 1, with a few modifications to make it more readable and to explain some stuff. For starters, let's have a look at the first 6 rows of pointers:

Code: Select all

                dc.l $FFFE00, Entrypoint, BusError, AddressError
		dc.l IllegalInstr,ZeroDivide,ChkInstr,TrapvInstr
		dc.l PrivilegeViolation,Trace,Line1010Emu,Line1111Emu
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap

The first pointer is the system stack. That's the area in memory where most of the return addresses are stored for specific jumps, in addition to any data the user pushes to it. Set any address you want for that (Note that the stack generally works backwards). Next up is the entry point, the routine that is jumped to when the machine is turned on. Your init code should be stored here. The bus error exception isn't used on the Mega Drive, but it's what happens when the program counter goes outside of a ROM address ($000000-$3FFFFF) or a RAM address (Generally $FF0000-$FFFFFF). Address error is called when there is a word/longword access to an odd address, whether it's a ROM or RAM address. Illegal instruction is triggered when the MD passes through an instruction that isn't recognised, or is triggered by the 'illegal' opcode. It's a similar case with Line1010Emu and Line1111Emu, where it jumps to if there is an opcode that begins with $AXXX (1010) or $FXXX (1111). Zero divide occurs when a divide by 0 happens. CHK instruction happens when the result of the CHK opcode returning true (dn<0, d0>value), and trapvinstruction happens when the trapv opcode activates (If V=1, trap). Privilege violation happens if supervisor mode isn't set on the status register and certain instructions are used, and trace occurs after each instruction once the trace bit is set on the status register, bit like a hardware debugger. AFAIK, it doesn't work on the MD. Just bear in mind that you don't really need a routine for the error handlers if you don't want to: Replace the first 3 rows with dc.l's to ErrorTrap if not (Do note that we will be covering error handlers in a later part). The following 3 rows after the error exceptions are reserved. Next up:

Code: Select all

                dc.l ErrorTrap, ErrorTrap, ExtInt, ErrorTrap
                dc.l HInt,      ErrorTrap, VInt,      ErrorTrap

These pointers are all interrupt routines. They increase by level, so the first one up there is level 0 and the last one is level 7. Only interrupts 2, 4 and 6 are used on the MD, so that's what we'll be looking at. The first one we'll be looking at is the level 2 interrupt. This is an external interrupt, generally used by things such as light guns. Next up you've got the HBlank interrupt (level 4), which is triggered when the CRT beam has drawn a line on the screen and is turned off while retracing its way back, only slightly lower. Sonic games use the routine to display the water palette; using more colours on screen than there should be. Finally we have the VBlank interrupt (level 6). This is triggered when the CRT beam hits the bottom right of the screen, and is turned off, returning to the top left. This is the best time to update CRAM, sprite tables, run object code, DMA etc. Next up:

Code: Select all


                dc.l Trap0, 	Trap1, 	   Trap2,     Trap3
                dc.l Trap4, 	Trap5, 	   Trap6,     Trap7
                dc.l Trap8, 	Trap9, 	   TrapA,     TrapB
                dc.l TrapC, 	TrapD, 	   TrapE,     TrapF

These are called when the trap opcode is used. Depending on what number you trap, it will trigger the exception based on that number, i.e. trap #0 will trigger the first routine. And finally:

Code: Select all

 

                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap

These are also reserved. That pretty much covers the vector table. Now we're moving on to the next part of the header. While you don't have to strictly follow this format (with the exception of 'SEGA' or ' SEGA' at least being written to address $100, seen at Console, although the 'MEGA DRIVE' or 'GENESIS' isn't necessary after it. This is necessary for the TMSS.), this is how most of the official games are labelled. Once again, this is taken from Sonic 1, with a few corrections and changes:

Code: Select all


Console:	dc.b 'SEGA MEGA DRIVE ' ; Hardware system ID ($10 chars long)
Date:		dc.b '(C)D955 2015.OCT' ; Release date ($10 chars long)
Title_Local:	dc.b 'YOUR HOMEBREW GAME                              ' ; Domestic name (Ensure $30 chars long!)
Title_Int:	dc.b 'YOUR HOMEBREW GAME                              ' ; International name (Ensure $30 chars long!)
Serial:		dc.b 'GM 00000000-00'   ; Serial/version number ($E chars long)
Checksum:	dc.w 0			; Pre-calculated checksum value.
		dc.b 'J               ' ; I/O support ($10 chars long)
RomStartLoc:	dc.l StartOfRom		; ROM start
RomEndLoc:	dc.l EndOfRom-1		; ROM end
RamStartLoc:	dc.l $FF0000		; RAM start
RamEndLoc:	dc.l $FFFFFF		; RAM end
SRAMSupport:	dc.l $20202020		; change to $5241F820 to create	SRAM
		dc.l $20202020		; SRAM start
		dc.l $20202020		; SRAM end
Modem:		dc.b "            "	; Modem support ($C chars long)
Notes:		dc.b "                                        "	; Notes ($28 chars long)
Region:		dc.b 'JUE             ' ; Region ($10 chars long: Vector table ($100) and Header ($100) should add to $200!)

Firstly, it's important to stress that address $100 (First 4 (or 5) bytes of the 'Hardware system ID') must contain the string 'SEGA' or ' SEGA' for the TMSS bootstrap ROM. So you can write anything after that, for the most part. SRAM creation will be covered later on.

PART 2 - INITIALISATION

Now, you're going to want to create some labels for your vectors. If you don't plan on using any of the error exception routines (Bus error to Line1111Emu), you can replace them with a branch to ErrorTrap (as mentioned earlier), which will be our generic error handler. Making error handlers will be covered later, but until then, let's look at ErrorTrap:

Code: Select all


; Generic error handler.
ErrorTrap:
                bra.s   *                         ; Loop on this instruction forever.


If you want, you can use rte instead, but it will cause Regen to crash eventually when an address error is hit, due to the way bus/address errors are handled compared to the other exceptions. What this routine does is freeze the game when this instruction is hit, as it is looping forever, nothing updates. Next:

Code: Select all


Hint:
                rte                               ; Return from the exception.

Vint:
		rte				  ; Return from the exception. 

We'll be returning to these in another part, but for now they can remain blank. If they are hit they will ReTurn from the Exception. Next up, we're going to create our entry point label, along with a few initialisation checks:

Code: Select all


EntryPoint:
                move    #$2700,sr               ; Disable interrupts.
                tst.l   ($A10008).l             ; Test Port A & B control.
		bne.s   PortA_Ok		; If it's okay, branch.
		tst.w   ($A1000C).l		; Test Port C control.
PortA_Ok:
		bne.w   PortC_Ok		; If Port A/B/C are okay, branch.
		*Continue init*
PortC_Ok:

What the above instructions do are prevent any interrupt from running, test some I/O ports to see if the MD is being turned on, or reset, and if it is reset, branch to PortC_Ok (More on this later). After that, inbetween the branch to Port C and the Port C label (Continue init):

Code: Select all


                move.b  ($A10001).l,d0          ; Load the version register data into d0.
		andi.b  #$F,d0			; Get hardware version bits only.
		beq.s   SkipSecurity		; If it's a non-TMSS model, branch.
		move.l  #'SEGA',($A14000).l	; Satisfy the TMSS.
SkipSecurity:

What the above instructions do are test to see if it's an MD model without the Trademark Security System (The 'PRODUCED BY OR UNDER LICENCE BY SEGA ENTERPRISES LTD.' screen thing), and if it isn't, branch as the address is apparently reserved. If it is a TMSS model, the 'SEGA' text string (Or just $53454741, same thing) needs to be written to I/O port $A14000, before the VDP is accessed, or the MD will lock up. In addition, as mentioned before, 'SEGA' or ' SEGA' need to be written to address $100, but that is covered by writing 'SEGA MEGA DRIVE ' or 'SEGA GENESIS ' anyway. Now, we need to clear all kinds of RAM (NOTE: THERE ARE MORE EFFICIENT WAYS OF DOING THIS! This is for an easy-to-read example, and should be left to the viewer to figure out a more efficient way, such as loading the values from a table like in Sonic 1). Firstly, we'll set up our VDP register values.

Contrary to what I just said, I will be loading the register values from a table, to prevent wasting a huge amount of space, although it can be optimised much further. Take a look at http://md.squee.co/VDP for a detailed explanation of the registers I've used (I will only be noting the important bits), but this is what I've done:

Code: Select all


                move.w  #VDPRegSetup_End-VDPRegSetup/2-1,d0 ; Get the full length of the registers.
		lea     VDPRegSetup,a0			    ; Load the table address.	
VDPRegInit:		
                move.w  (a0)+,($C00004).l		    ; Write the first register value into a0.
		dbf     d0,VDPRegInit			    ; Repeat until all VDP registers are initialised.	
		*Continue init*

VDPRegSetup:  
                dc.w    $8004                      ; Register $00 - Disable HBlank.
                dc.w    $8134                      ; Register $01 - Enable VBlank and DMA, disable the display.
                dc.w    $8230                      ; Register $02 - Plane A nametable location - $C000.
                dc.w    $8328                      ; Register $03 - Window plane nametable location $A000.
                dc.w    $8407                      ; Register $04 - Plane B nametable location - $E000.
                dc.w    $857C                      ; Register $05 - Sprite attribute table location - $F800.
                dc.w    $8600                      ; Register $06 - 128kb VRAM mode register - Unused on stock MD.
                dc.w    $8700                      ; Register $07 - Background colour - Palette line 0, entry 0.
                dc.w    $8800                      ; Register $08 - HScroll register, unused on mode 5 (MD).
                dc.w    $8900                      ; Register $09 - VScroll register, unused on mode 5 (MD).
                dc.w    $8A00                      ; Register $0A - H-INT counter - 0.
                dc.w    $8B00                      ; Register $0B - Full screen scrolling, no level 2 interrupt.
                dc.w    $8C81                      ; Register $0C - Use a 40 tile-wide display.
                dc.w    $8D3F                      ; Register $0D - HScroll nametable location - $FC00.
                dc.w    $8E00                      ; Register $0E - 128kb VRAM mode register - Unused on stock MD.
                dc.w    $8F02                      ; Register $0F - VDP auto increment - $02.
                dc.w    $9001                      ; Register $10 - 64x32 plane size.
                dc.w    $9100                      ; Register $11 - Window plane horizontal position - 0.
                dc.w    $9200                      ; Register $12 - Window plane vertical position - 0.
                dc.w    $93FF                      ; Register $13 - DMA length - $XXFF.
                dc.w    $94FF                      ; Register $14 - DMA length - $FFFF
                dc.w    $9500                      ; Register $15 - DMA low byte - $XXXX00
                dc.w    $9600                      ; Register $16 - DMA mid byte - $XX0000
                dc.w    $9780                      ; Register $17 - Set to DMA fill mode, rather than 68k to VRAM.
VDPRegSetup_End:

So now that you have the VDP registers set up, it's time to clear some RAM. To start with, we will clear CRAM, VSRAM and VRAM; colour RAM, vertical scroll RAM and video RAM.

Code: Select all


		lea     ($C00000).l,a0			; Load the VDP data port to a0 (incidentally, control port can be loaded by writing to 4(a0) instead of to (a0)).
		move.l  #$C0000000,4(a0)		; Set the VDP to CRAM write, address $0000.
		moveq   #0,d0				; Clear d0 - This is the register that we're going to be using to clear RAM.
                moveq   #$3F,d1				; Set to clear 64 palettes - This is also the register we're going to be using for dbf loop lengths.
@ClrCRAMLoop:		
		move.w  d0,(a0)				; Clear one palette (palettes are a word in size).
		dbf     d1,@ClrCRAMLoop			; Repeat for all 64 palettes.
		move.l  #$40000010,4(a0)		; Set the VDP to VSRAM write, address $0000.
		moveq   #$27,d1				; Set to clear $50 bytes ($27 for /2 due to word writes, and -1 due to the dbf loop).
@ClrVSRAMLoop
		move.w  d0,(a0)				; Clear 4 bytes of VSRAM.
		dbf	d1,@ClrVSRAMLoop		; Repeat until it is fully cleared.
		
Right. While the comments above just basically clear CRAM and VSRAM, we're going to be clearing VRAM a little bit differently, although you can clear it differently to above. VDP register $17's bitfield is set to the DMA's fill mode, so when the VDP is set to DMA, whatever value is written to the data port, is repeated for the whole DMA length (Registers $13 & $14, in this case $FFFF; the whole of VRAM). So:

Code: Select all


                move.l  #$40000080,4(a0)                ; Set the VDP to VRAM DMA, address $0000.
		move.w  #$8F01,4(a0)			; Set the VDP to increment the VRAM address 1 byte with every write to the data port.
		move.w  #0,(a0)				; Clear two bytes of VRAM; start DMA.

Now, unlike regular 68K to VRAM DMA, with DMA fill/copy, the 68k isn't frozen, so we're going to need to loop the 68K until it is finished or we could end up corrupting VRAM when it comes across another VDP write. How we do this is using the VDP's own status register. You can copy the control port value to an address register, which basically gives you the VDP status. The bit we're looking for is the second bit, bit 1. This bit is set when DMA is running, but it's only useful for fills and copies, due to the 68K being frozen with the other mode. So...

Code: Select all


@CheckVRAMDMAClear:
                move.w  4(a0),d1                        ; Load the VDP status register value into d1.
		btst    #1,d1				; Is DMA still running?
		bne.s   @CheckVRAMDMAClear		; If it still is, loop.
		move.w  #$8F02,4(a0)			; Set the VDP address to increment 2 bytes per data port write.

Next up, we have the Z80, the Mega Drive's sub-processor. This mainly controls most of the sound hardware, so we will be using it later. In the meantime, we will just leave it on a loop. To write code to the Z80, you must first send it a bus request to stop it (and reset it if need be). This is done by writing $100 to the I/O ports $A11100 and $A11200, respectively. Afterwards, you'll then be able to use the Z80's RAM, ranging from $A00000-$A01FFF, and any of the sound hardware that is mapped to the Z80, if need be.

Code: Select all


		move.w  #$100,($A11100).l               ; Stop the Z80.
		move.w  #$100,($A11200).l		; Deassert Z80 reset.
@WaitforZ80:
		btst    d0,($A11100).l			; Has the Z80 stopped?
		bne.s	@WaitforZ80			; If it hasn't, branch.
		lea     Z80Data(pc),a0			; Load the Z80 init code.
		lea     ($A00000).l,a1			; Load the start of Z80 RAM into a1.
		moveq   #Z80Data_End-Z80Data-1,d1	; Load the length of the code, -1 for the dbf loop.
@InitZ80Loop:
		move.b  (a0)+,(a1)+			; Write a byte of data to Z80 RAM (IMPORTANT! Can only be written to as bytes!).
		dbf     d1,@InitZ80Loop			; Repeat until completely written.
		move.w  #0,($A11200).l			; Reset the Z80: Run the code from $0000 in Z80 RAM.
		move.w  #0,($A11100).l			; Start the Z80.
		move.w  #$100,($A11200).l		; Deassert Z80 reset.

...And underneath VDPRegSetup_End, put this:

Code: Select all


Z80Data:
                dc.b    $F3       ; di    - Disable interrupts.
		dc.b	$ED, $56  ; im 1  - Only interrupt mode used. Interrupts occur at address $38 in Z80 RAM.
		dc.b	$18, $FE  ; jr -2 - Loop repeatedly.
		dc.b	$00	  ; nop   - Do nothing. Written to prevent odd addresses for the 68K.
Z80Data_End:

So, as you can see, the Z80 has its own assembly language in comparison to 68K. All this does is ensure that it loops on the spot while we work with the 68K. Now to set up the PSG registers:

Code: Select all


		move.b  #$9F,($C00011).l                ; Mute PSG channel 1.
		move.b  #$BF,($C00011).l                ; Mute PSG channel 2.
		move.b  #$DF,($C00011).l                ; Mute PSG channel 3.
		move.b  #$FF,($C00011).l                ; Mute PSG noise channel.

We'll be covering these in a later part, but basically, we've set the volume by setting the right nybble as $F (off, PSG registers work from a low value being high, to a high value being low, so $F will be off). Next:

Code: Select all


                movea.l #0,a6                           ; Set a6 to $0.
		move.l  a6,usp				; Set the USP to 0.
		move.w  #$3FFF,d1			; Set to clear $10000 bytes of RAM.
@ClrRAMLoop:
		move.l  d0,-(a6)			; Overflow into RAM section, clear RAM.
		dbf	d1,@ClrRAMLoop			; Repeat until fully cleared.

Basically, what's happening here is that we've set the usp to 0 and we're clearing RAM, by clearing data backwards. Since a6 is XX000000 (24-bit addresses on the 68K), it rolls backwards to XXFFFFFC, which is $FFFFFC, in the RAM address range. We clear RAM from there. Now, underneath PortC_Ok, add this:

Code: Select all


PortC_Ok:
		move.w  ($C00004).l,d0                  ; Move the VDP status register value into d0.
                btst    #1,d0			        ; Is DMA still running after reset?
		bne.s   PortC_Ok         		; If it is, loop.
		tst.w   ($C00004).l			; Clear the VDP's command-pending flag.

Firstly, the check for DMA is for when the console is reset, to make sure it's waited out before any VDP accesses. The next instruction clears the VDP's internal command-pending flag, which is set when it is expecting the lower 16 bits of the VDP command. Since the game can be reset in between those times, it could cause VDP miswrites.

Code: Select all


		move.b  #$40,($A10009).l		; Set /TH to read as an output in port A; Game is considered 'initialised':-
							; Will branch to PortC_Ok when reset.
		move.b  #$40,($A1000B).l		; Set /TH to read as an output in port B.
		move.b	#$40,($A1000D).l		; Set /TH to read as an output in port C.
		movem.l (a6),d0-a6			; Clear all registers.

Remember how I said I'd be coming back to what those port A/B/C checks did? This is it. It waits for a value to be written to these, and the code generally treats the game as initialised once these values are set. This also sets /TH as an output, which is needed for controllers. More on that in another part. This next instruction writes the data stored in a6 (The address will be $FF0000, a cleared RAM section) into all the registers except the stack, clearing the registers.

Now, branch over your table and there we go, we have our init code sorted! Join us for part 2, where we will be covering other things, such as art, sprites, palettes, sound and other stuff in depth!

Want some homework? Optimise the code I just gave you. Here's the full code, just in case it's been copied down wrong. DO NOTE that I've repointed a lot of error exception/ interrupt vectors to 'ErrorTrap', simply because I'll be programming them at a later date:

==Appendix:==

Code: Select all


StartofROM:
Vectors:                                                                     
                dc.l $FFFE00, Entrypoint,  ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l HInt,      ErrorTrap, VInt,      ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
                dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
Console:	dc.b 'SEGA MEGA DRIVE ' ; Hardware system ID ($10 chars long)
Date:		dc.b '(C)D955 2015.OCT' ; Release date ($10 chars long)
Title_Local:	dc.b 'YOUR HOMEBREW GAME                              ' ; Domestic name (Ensure $30 chars long!)
Title_Int:	dc.b 'YOUR HOMEBREW GAME                              ' ; International name (Ensure $30 chars long!)
Serial:		dc.b 'GM 00000000-00'   ; Serial/version number ($E chars long)
Checksum:	dc.w 0			; Pre-calculated checksum value.
		dc.b 'J               ' ; I/O support ($10 chars long)
RomStartLoc:	dc.l StartOfRom		; ROM start
RomEndLoc:	dc.l EndOfRom-1		; ROM end
RamStartLoc:	dc.l $FF0000		; RAM start
RamEndLoc:	dc.l $FFFFFF		; RAM end
SRAMSupport:	dc.l $20202020		; change to $5241F820 to create	SRAM
		dc.l $20202020		; SRAM start
		dc.l $20202020		; SRAM end
Modem:		dc.b "            "	; Modem support ($C chars long)
Notes:		dc.b "                                        "	; Notes ($28 chars long)
Region:		dc.b 'JUE             ' ; Region ($10 chars long: Vector table ($100) and Header ($100) should add to $200!)

; =============================================================================

; Generic error handler.
ErrorTrap:
                bra.s   *                         	; Loop on this instruction forever.

Hint:
                rte                                     ; Return from the exception.

Vint:
		rte				        ; Return from the exception. 

EntryPoint:
                tst.l   ($A10008).l                     ; Test Port A & B control.
		bne.s   PortA_Ok			; If it's okay, branch.
		tst.w   ($A1000C).l			; Test Port C control.
PortA_Ok:
		bne.w   PortC_Ok			; If Port A/B/C are okay, branch.
                move.b  ($A10001).l,d0          	; Load the version register data into d0.
		andi.b  #$F,d0				; Get hardware version bits only.
		beq.s   SkipSecurity			; If it's a non-TMSS model, branch.
		move.l  #'SEGA',($A14000).l	        ; Satisfy the TMSS.
SkipSecurity:
                move.w  #VDPRegSetup_End-VDPRegSetup/2-1,d0 ; Get the full length of the registers.
		lea     VDPRegSetup,a0			; Load the table address.	
VDPRegInit:		
                move.w  (a0)+,($C00004).l		; Write the first register value into a0.
		dbf     d0,VDPRegInit			; Repeat until all VDP registers are initialised.	
		lea     ($C00000).l,a0			; Load the VDP data port to a0 (incidentally, control port can be loaded by writing to 4(a0) instead of to (a0)).
		move.l  #$C0000000,4(a0)		; Set the VDP to CRAM write, address $0000.
		moveq   #0,d0				; Clear d0 - This is the register that we're going to be using to clear RAM.
                moveq   #$1F,d1				; Set to clear 64 palettes ($40/2 for long writes) - This is also the register we're going to be using for dbf loop lengths.
@ClrCRAMLoop:		
		move.l  d0,(a0)				; Clear two palettes (palettes are a word in size).
		dbf     d1,@ClrCRAMLoop			; Repeat for all 64 palettes.
		move.l  #$40000010,4(a0)		; Set the VDP to VSRAM write, address $0000.
		moveq   #$13,d1				; Set to clear $50 bytes ($13 for /4 due to longword writes, and -1 due to the dbf loop).
@ClrVSRAMLoop
		move.l  d0,(a0)				; Clear 4 bytes of VSRAM.
		dbf	d1,@ClrVSRAMLoop		; Repeat until it is fully cleared.
                move.l  #$40000080,4(a0)                ; Set the VDP to VRAM DMA, address $0000.
		move.w  #$8F01,4(a0)			; Set the VDP to increment the VRAM address 1 byte with every write to the data port.
		move.w  #0,(a0)				; Clear two bytes of VRAM; start DMA.
@CheckVRAMDMAClear:
                move.w  4(a0),d1                        ; Load the VDP status register value into d1.
		btst    #1,d1				; Is DMA still running?
		bne.s   @CheckVRAMDMAClear		; If it still is, loop.
		move.w  #$8F02,4(a0)			; Set the VDP address to increment 2 bytes per data port write.
		move.w  #$100,($A11100).l               ; Stop the Z80.
		move.w  #$100,($A11200).l		; Deassert Z80 reset.
@WaitforZ80:
		btst    d0,($A11100).l			; Has the Z80 stopped?
		bne.s	@WaitforZ80			; If it hasn't, branch.
		lea     Z80Data(pc),a0			; Load the Z80 init code.
		lea     ($A00000).l,a1			; Load the start of Z80 RAM into a1.
		moveq   #Z80Data_End-Z80Data-1,d1	; Load the length of the code, -1 for the dbf loop.
@InitZ80Loop:
		move.b  (a0)+,(a1)+			; Write a byte of data to Z80 RAM (IMPORTANT! Can only be written to as bytes!).
		dbf     d1,@InitZ80Loop			; Repeat until completely written.
		move.w  #0,($A11200).l			; Reset the Z80: Run the code from $0000 in Z80 RAM.
		move.w  #0,($A11100).l			; Start the Z80.
		move.w  #$100,($A11200).l		; Deassert Z80 reset.
		move.b  #$9F,($C00011).l                ; Mute PSG channel 1.
		move.b  #$BF,($C00011).l                ; Mute PSG channel 2.
		move.b  #$DF,($C00011).l                ; Mute PSG channel 3.
		move.b  #$FF,($C00011).l                ; Mute PSG noise channel.
                movea.l #0,a6                           ; Set a6 to $0.
		move.l  a6,usp				; Set the USP to 0.
		move.w  #$3FFF,d1			; Set to clear $10000 bytes of RAM.
@ClrRAMLoop:
		move.l  d0,-(a6)			; Overflow into RAM section, clear RAM.
		dbf	d1,@ClrRAMLoop			; Repeat until fully cleared.
PortC_Ok:
		move.w  ($C00004).l,d0                  ; Move the VDP status register value into d0.
                btst    #1,d0			        ; Is DMA still running after reset?
		bne.s   PortC_Ok         		; If it is, loop.
		tst.w   ($C00004).l			; Clear the VDP's command-pending flag. 
		    
		move.b  #$40,($A10009).l		; Set /TH to read as an output in port A; Game is considered 'initialised':-
							; Will branch to PortC_Ok when reset.
		move.b  #$40,($A1000B).l		; Set /TH to read as an output in port B.
		move.b	#$40,($A1000D).l		; Set /TH to read as an output in port C.
		movem.l (a6),d0-a6			; Clear all registers.
		bra.w   GameProgram			; Start the game!

; ==============================================================================================

VDPRegSetup:  
                dc.w    $8004                      ; Register $00 - Disable HBlank.
                dc.w    $8134                      ; Register $01 - Enable VBlank and DMA, disable the display.
                dc.w    $8230                      ; Register $02 - Plane A nametable location - $C000.
                dc.w    $8328                      ; Register $03 - Window plane nametable location $A000.
                dc.w    $8407                      ; Register $04 - Plane B nametable location - $E000.
                dc.w    $857C                      ; Register $05 - Sprite attribute table location - $F800.
                dc.w    $8600                      ; Register $06 - 128kb VRAM mode register - Unused on stock MD.
                dc.w    $8700                      ; Register $07 - Background colour - Palette line 0, entry 0.
                dc.w    $8800                      ; Register $08 - HScroll register, unused on mode 5 (MD).
                dc.w    $8900                      ; Register $09 - VScroll register, unused on mode 5 (MD).
                dc.w    $8A00                      ; Register $0A - H-INT counter - 0.
                dc.w    $8B00                      ; Register $0B - Full screen scrolling, no level 2 interrupt.
                dc.w    $8C81                      ; Register $0C - Use a 40 tile-wide display.
                dc.w    $8D3F                      ; Register $0D - HScroll nametable location - $FC00.
                dc.w    $8E00                      ; Register $0E - 128kb VRAM mode register - Unused on stock MD.
                dc.w    $8F02                      ; Register $0F - VDP auto increment - $02.
                dc.w    $9001                      ; Register $10 - 64x32 plane size.
                dc.w    $9100                      ; Register $11 - Window plane horizontal position - 0.
                dc.w    $9200                      ; Register $12 - Window plane vertical position - 0.
                dc.w    $93FF                      ; Register $13 - DMA length - $XXFF.
                dc.w    $94FF                      ; Register $14 - DMA length - $FFFF
                dc.w    $9500                      ; Register $15 - DMA low byte - $XXXX00
                dc.w    $9600                      ; Register $16 - DMA mid byte - $XX0000
                dc.w    $9780                      ; Register $17 - Set to DMA fill mode, rather than 68k to VRAM.
VDPRegSetup_End:

Z80Data:
                dc.b    $F3       ; di    - Disable interrupts.
		dc.b	$ED, $56  ; im 1  - Only interrupt mode used.
		dc.b	$18, $FE  ; jr -2 - Loop repeatedly.
		dc.b	$00	  ; nop   - Do nothing. Written to prevent odd addresses for the 68K.
Z80Data_End:

; ==============================================================================================

GameProgram:
                bra.s   *                     ; See you in the next part!

EndofROM:



                END
                

Re: Let's Write a Mega Drive Homebrew ROM!

Posted: Mon Nov 21, 2016 7:30 am
by LazloPsylus
First, Domestic and Overseas name need to be $30 bytes long as per standard. Would not recommend trying to shorten things in that area due to it causing some other issues later on. Header is expected to be $200 bytes, with Vector table taking up $100 and the metadata taking up the remaining. In fact, may want to make note of the standards for the sizes of each of those fields.

Second, I've considered setting up a sort of "Homebrewer DB" for assigning shortcodes to devs so that you can fill in the release date and devcode field with something meaningful. Anyone like?

Re: Let's Write a Mega Drive Homebrew ROM!

Posted: Mon Nov 21, 2016 4:31 pm
by Dandaman955
It is $30 bytes, it's just the weird formatting when copying over from Notepad to the message board. I'll see if I can fix that. The dbs sound quite interesting as well; Saves manually counting them.

EDIT: Okay Xeal, I'll try code tags.

Re: Let's Write a Mega Drive Homebrew ROM!

Posted: Mon Nov 21, 2016 7:19 pm
by LazloPsylus
Forum formatting will be forum formatting, eh? Stupid "smart" formatting to ruin what you're doing and make sure it's not at all what you actually meant.