0% found this document useful (0 votes)
364 views87 pages

SEGA Mega Drive - Assembly Workshop

The document discusses an upcoming assembly programming workshop for the SEGA Mega Drive console. It provides details on what will be covered in the workshop, including an overview of the Mega Drive system, assembly language basics, and how to set up tools to write and debug assembly code for the console. The document also includes refreshers on binary and hexadecimal numbering systems.

Uploaded by

dotemacs
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
364 views87 pages

SEGA Mega Drive - Assembly Workshop

The document discusses an upcoming assembly programming workshop for the SEGA Mega Drive console. It provides details on what will be covered in the workshop, including an overview of the Mega Drive system, assembly language basics, and how to set up tools to write and debug assembly code for the console. The document also includes refreshers on binary and hexadecimal numbering systems.

Uploaded by

dotemacs
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 87

SEGA Mega Drive

Assembly Programming Workshop


Matt Phillips - bigevilcorporation.co.uk
Tanglewood is a brand new game for the SEGA Mega Drive, programmed in pure 68000 assembly
language. If you found these slides useful, please consider backing it on Kickstarter!
Who is this workshop for?
● Those who have never written a line of assembly before
● Those with little or no understanding of how a CPU works internally
● Some higher level programming experience is required
○ Variables, functions, numbers (signed and unsigned), pointers, logic
● Some higher level debugging experience is required
○ Breakpoints, stepping, watch windows, swearing
● A basic understanding of binary and hexadecimal numbering systems
○ There will be a short refresher to jog your memory
● How to run DOSbox and use the DOS command line
What will be covered?
● The SEGA Mega Drive - what’s inside it?
● The binary and hex numbering systems - a quick refresher
● How a CPU works (from a programmer’s perspective)
● 68000 assembly language - the basics
● Getting your assemblers, emulators and debuggers working
● Initialising the SEGA Mega Drive
● The SEGA Mega Drive display processor
● Make the screen pink!
● Upload graphics tiles
● “Hello, world!”
● Port Crysis 3
What will you need?
● A computer
● A text editor
● DOSbox
● An assembler
○ ASM68K.EXE
○ SNASM68K.EXE
● A SEGA Mega Drive emulator with built-in debugger
○ Windows/Linux: Regen (debug version) from SonicRetro.org
○ Mac: KGens
● The Mega Drive ROM header template + support files
○ From bigevilcorporation.co.uk
What Nintendon’t
SEGA Mega Drive internals

● Motorola 68000 main CPU (7.61 mhz)


● Zilog Z80 slave CPU (from Master System)
● 64kb main memory
● Yamaha YM7101 VDP (Video Display Processor)
● 64kb dedicated video memory
● Yamaha YM2612 FM audio chip
● Texas Instruments SN76489 audio chip (from Master System)
Binary - a quick refresher
● 2-base numbering system
● 1 bit = either 0 or 1
● Bit positions represent squared increments:

LSB MSB
128 64 32 16 8 4 2 1
-------------------------------
0 1 0 0 0 1 0 1 = 1 + 4 + 64 = 69

● Shift left = x2
● Shift right = ÷2
● 1 byte = 8 bits
● Further reading: look up “Two’s Complement” for signed numbers
Hexadecimal - a quick refresher
● 16-base numbering system
● 0-9 then A-F
● 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 2 3 4 5 6 7 8 9 A B C D E F
● 0x08 = 8
● 0x0C = 12
● 0x12 = 18
● 0xFF = 255 (unsigned) or -1 (signed byte)
● 0x18FE = 6398
● 1 hex digit = 4 bits = 1 nybble
What can a CPU do?
● Crunch numbers
● Takes one or more numbers + an operation, produces an output
● Literally, that’s all
● Give it the numbers 6 and 12, tell it to add, it dumps out 18
● Basic CPU operations:
○ Move data around - to/from memory, or other chips and devices
○ Add, subtract, multiply, divide numbers
○ Logic AND/OR/NOT/XOR
○ Binary shift
○ Compare numbers
○ Negate numbers
What CAN’T a CPU do?
● printf()
○ It doesn’t know what a font is
○ It doesn’t know what a character is
○ It doesn’t know what a string is
○ It doesn’t know what a display is
● sqrt()
○ High level maths algorithm
● GetKey(uint charcode)
○ What’s a keyboard?
● DrawTriangle(const VertexBuff& verts)
○ No knowledge of structures, 3D maths, frustums, cameras

All higher level concepts, built with ADD/SUB/MUL/DIV/AND/OR/etc


Raw 68000 assembly
● No standard libraries
○ Build your own printf(), memset(), file reading routines, exception handlers
● No hardware libraries
○ DirectInput? OpenGL? Forget it
● Interface directly with the CPU, GPU, sound chip, I/O ports
○ Prepare to make it crash
○ A lot
● Read/write directly to RAM
○ No virtual memory, paging
● Handle your own interrupts
○ Oh, this is fun
How to we tell the CPU what to do?

● That’s a drawing, not a photo


How to we tell the CPU what to do?

● Registers are very tiny (4 bytes) of very fast memory inside the CPU
● Programmer moves numbers to the registers for the CPU to operate on
How to we tell the CPU what to do?

6 12
● Move 6 to register 1, then move 12 to register 2
● Tell the CPU to ADD
● CPU computes the result, dumps it to register 2
The Mega Drive CPU (Motorola 68000)

● 8 x 32-bit general purpose registers (mainly for arithmetic)


● 8 x 32-bit address registers (“pointers”, sort of)
● Program Counter (address of current line of code)
● Status Register (state of the CPU after the last instruction)
68000 assembly - basic syntax
MOVE.B #6, d0
MOVE.B #12, d1
ADD.B d0, d1

● MOVE literal number 6 to register d0


● MOVE literal number 12 to register d1
● ADD register d0 to register d1
● d1 contains result (hopefully 18)
● .B means “byte size” - will move and add 8 bits only (values from -128 to +127)
68000 assembly - basic syntax
● MOVE.B = move byte (8 bits, 0x00 - 0xFF)

● MOVE.W = move word (16 bits, 0x0000 - 0xFFFF)

● MOVE.L = move longword (32 bits, 0x00000000 - 0xFFFFFFFF)


68000 assembly - basic syntax
● ADD.B - add 1 byte, wraps from 127 to -128

● ADD.W - add 2 bytes, wraps from 32767 to -32768

● ADD.L - add 4 bytes, wraps from 2147483647 to -2147483648

● Sizes have other side effects for MUL, DIV (anything that doesn’t rely on
Two’s Complement for the computation) - read the Instruction Set
● For example:
○ DIVS.W will perform 16-bit division, leaving remainder in upper word
○ DIVS.L will perform 32-bit division
○ MULU.W will perform 16-bit multiplication, with 32-bit result
○ MULU.L will perform 32-bit multiplication, with 32-bit result
68000 assembly - basic syntax
MOVE.W #3, d0
MOVE.W #4, d1
MULS.W d0, d1
● MOVE literal number 3 to register d0 writing to bottom word only
● MOVE literal number 4 to register d1 writing to bottom word only
● MUL (signed) register d0 by register d1 leaving a word-sized result
● d1 contains result (hopefully 12) in bottom word (upper word will remain unchanged)
Let’s write our first program!
● Create a new text file (MS-DOS friendly filename, .ASM extension)
○ Label the start of ROM:
ROM_Start
○ Include the globals, Mega Drive ROM header, and interrupts (includes begin with a TAB):
include ‘GLOBALS.ASM’
include ‘HEADER.ASM’
include ‘INTERPTS.ASM’
○ Label the entry point:
CPU_EntryPoint
○ Add instructions (all instructions begin with a TAB):
MOVE.B #6, d0
MOVE.B #12, d1
ADD.B d0, d1
○ Label the end of ROM:
ROM_End
Let’s write our first program!
Let’s assemble our first ROM!
● Open DOSbox
● Mount your dev directory as a drive:
○ MOUNT C C:\SEGADEV
● Switch to drive:
○ C:
● Assemble the ROM:
SNASM68K.EXE /p filename.asm,romname.bin

● Getting errors? Whitespace + quote type (‘) is important


Let’s debug it!
● Open emulator
● Load ROM
● Open the 68000 debugger
● Set breakpoint at address 0x020C (CPU_EntryPoint)
● Reset (TAB in Regen)
● Step through and watch registers
Regen’s 68000 debugger Registers

Code
Step

Breakpoint
(PC type)
Opcodes - Basic arithmetic
○ ADD.W #7, d1 ; add 7 to value in d1
○ SUB.W #6, d1 ; subtract 6 from value in d1
○ MULS.W #4, d1 ; multiply value in d1 by 4
○ DIVS.W d0, d1 ; divide value in d1 by value in d0 (better not be 0!)
○ NEG.W d1 ; negate value in d1
Quick challenge! Player update logic
● The player is jumping!
● Let’s write the Update() routine
● Start with these initial values in any registers, then calculate the new player
coordinates:

position = (128, 50)


velocity = (22, 45)
gravity = -10
Quick challenge! Player update logic

● 0x0096 (150) in d0
● 0x0055 (85) in d1
Opcodes - Branching
● BRA = BRAnch (to never return, think ‘goto’ in C/BASIC)
● Use labels!

MOVE.W #8, d0
BRA SomeOtherCode
MOVE.L #10, d1
MOVE.B #3, d2
SomeOtherCode:
MOVE.W #6, d1
MOVE.B #2, d3
Opcodes - Branching
● BSR = Branch to SubRoutine (to return later, think FunctionCall() in C)
● RTS = ReTurn from Subroutine (think ‘return’ in C)

MOVE.B #8, d0
MOVE.B #4, d1
BSR AddBytes
MOVE.B #6, d0
BSR AddBytes

AddBytes:
ADD.B d0, d1
RTS
Opcodes - Branching
● DBRA = Decrement and BRAnch
● Decrements a register, then branches if it is >= 0
● Looping!

MOVE.W #9, d0 ; Counter (word size, -1) to d0


Loop: ; Start of loop
ADD.W #1, d1 ; Do something mundane
DBRA d0, Loop ; Loop until d0 < 0
Quick challenge! Player update logic
● Put your player update code into a subroutine
● Execute it 5 times
● Halt the CPU when finished:
STOP #0x2700
● Careful not to stomp on registers still in use!
● Tip: Define constant gravity using EQU (EQUate)
gravity equ 10
Quick challenge! Player update logic

● 0x00EE in d0
● 0x00E1 in d1
● 0xFFFF in count register
Opcodes - Comparison
● To make logical decisions, we need to compare two numbers
● How does the CPU determine if 12 is greater than 4?
● Subtract 12 from 4, will it go negative?
● Enter the Carry Flag!
● Status Register (sr) holds state of last CPU operation
● If Carry Flag is 1, the last operation resulted in a negative number (value A
was greater than value B)
● If Z flag is 1, the last operation resulted in 0 (value A is equal to value B)
● Some opcodes to help us
Opcodes - Comparison
First:

● CMP - CoMPare two registers


○ Internally performs a subtraction, without writing the result
○ Just sets Carry/Z flags

And then:

● BEQ - Branch if EQual


○ Branches to specified destination if Z flag set
● BLT - Branch if Less Than
○ Branches to specified destination if Carry flag is set
Comparison - if/then/else/endif example
MOVE.W #12, d0 ; 12 to d0
MOVE.W #4, d1 ; 4 to d1
CMP.W d0, d1 ; Compare d0 and d1 (“IF”)
BLT LessThan ; If d0 less than d1, branch
MOVE.W d1, d0 ; Do something B (“ELSE”)
BRA End ; Skip if case
LessThan: ; Label (“THEN”)
ADD.W #1, d1 ; Do something A
End: ; (“END IF”)
Quick challenge! Player update logic
● Define Floor as a constant
● Clamp player Y position to floor height

Player Y Position = 30
Player Y Velocity = 0
Floor Height = 10
Opcodes - Comparison
● BEQ - Branch if EQual
● BNE - Branch if Not Equal
● BLT - Branch if Less Than
● BGT - Branch if Greater Than
● BGE - Branch if Greater than or Equal
● BLE - Branch if Less than or Equal
Quick challenge! Player update logic

0x00EE (238) in d0
0x000A (10) in d1
SEGA Mega Drive
Working with Memory
Address registers
Address registers
● Best described as “on-CPU pointers”
● Hold addresses in memory (or other hardware, more later)
● Increment, decrement, offset (good for structures), “dereference”
Defining data in ROM
● Code and data = all just bytes
● Packed into the same ROM address space
● Uses same label syntax (just marks an address)

array_of_bytes:
dc.b 0,1,2,3,4,5,6,7,8

walk_animation_frames:
dc.w 0,1,2,3,2,1

level_select_cheat:
dc.b 0x19,0x65,0x09,0x17
Reading data using address registers
● Load address into register
● Dereference value
● Increment register
● Repeat

move.l #level_select_cheat, a0 ; Move address to a0

move.l (a0)+, d0 ; Move long to d0, post-increment address

move.w (a0)+, d1 ; Word size read + word size increment


move.b (a0)+, d2 ; Byte size read + byte size increment
move.b (a0)+, d3
SEGA Mega Drive
The Video Display Processor
The Mega Drive Video Display Processor
● 320x240 pixel resolution (PAL)
● Tile-based display chip
○ Tiles are 8x8 pixels
● 4x 16 colour palettes
● 2x scrolling tile maps
● 64 sprites
○ Up to 4x4 tiles in size each
● 64KB dedicated video memory (VRAM)
● 128 bytes dedicated colour memory (CRAM)
VDP registers

● VDP has 24 registers


● Not general purpose, very specific roles
○ r0 = display on/off
○ r5 = address of sprite table
○ r7 = background colour
● Cannot be written to like 68000 registers
VDP registers
About that ‘dc.b’
● dc tells the assembler that the following is data, not code
● dc.b, dc.w, dc.l
● Assembler just copies data into the ROM untouched
● We’ll be using dc to define tables, tiles, and palettes inline with code
● Be mindful of alignment - pad structures to longword length
The TMSS
● Before we continue, there’s a small obstacle
● SEGA introduced the TMSS - Trade Mark Security System - to model 1+
● Can’t use the VDP without turning it on
● But if you turn it on…
The TMSS
● Don’t let the word ‘security’ put you off, it’s very easy to enable
● It wasn’t created to provide a technical challenge
● It was created to provide a legal challenge
Programming the VDP
● Control Port at 0x00C00004 - MOVE commands to it (word size)
● Data Port at 0x00C00000 - MOVE data to it (word size)
● To write a register:
○ Command 0x8
○ ...followed by register number: 0x87
○ ...followed by value: 0x8701
○ Sets register 7 (background colour) to value 01 (palette 0, colour 1)
○ Move 0x8701 to VDP control port address
○ MOVE.W #0x8701, 0x00C00004
○ MOVE.W #0x8701, vdp_control
● To initialise the VDP, we write all 24 registers
Initialising the VDP
Initialising the VDP
include ‘VDP.ASM’

bsr VDP_WriteTMSS
bsr VDP_LoadRegisters

● Tip: Verify registers with VDP debugger in emulator


● Depending on where you included VDP.ASM, your entry point may have
changed address
● Tip: Generate an LST file to help:
Palettes
● VDP supports up to 4 palettes at once
● 16 colours per palette (colour 0 is always transparent)
● 9 bits per pixel
● Colour as a word (16 bits):
0000 BBB0 GGG0 RRR0
● Red = 0x000E
● Green = 0x00E0
● Blue = 0x0E00
Palettes
Copying palettes to CRAM
● Poke VDP control port to prepare it for accepting data
● Write data to VDP data port
● Palette 0 lives at CRAM address 0x0000
● Unfortunately, not as simple as writing registers
Writing to VRAM/CRAM
● Control port byte pattern:
CCAA AAAA AAAA AAAA 0000 0000 CCCC 00AA
● Destination address:
--DC BA98 7654 3210 ---- ---- ---- --FE
● Commands:
○ 100000 – VRAM Write
○ 110000 – CRAM Write
● There are more, all in good time
● So, to write to VRAM address 0xC000:
1--- ---- ---- ---- ---- ---- ---- ----
--00 0000 0000 0000 ---- ---- ---- --11
1000 0000 0000 0000 0000 0000 0000 0011
MOVE.L #0x80000003, vdp_control
Writing to VRAM/CRAM
● A bit complex? Don’t worry about it
● Source and destination known at compile time, so…
● We’ll be using a macro!

SetVRAMWrite: macro addr


move.l #(vdp_cmd_vram_write)|((\addr)&$3FFF)<<16|(\addr)>>14, vdp_control
endm

SetCRAMWrite: macro addr


move.l #(vdp_cmd_cram_write)|((\addr)&$3FFF)<<16|(\addr)>>14, vdp_control
endm
Writing to VRAM/CRAM
● Data port accepts longwords, except it doesn’t
● If you try to MOVE.L to it, it will be split up into two word writes
● Don’t worry about it, autoincrement to the rescue!
● VDP register 15 is set to 2, so the destination address will increment by 2 for
every word written

MOVE.L (a0)+, vdp_data


Copying palettes to CRAM
● Define a palette:
Copying palettes to CRAM
● Tell VDP control port we’re about to write to CRAM address 0x0000:

SetCRAMWrite 0x0000

● Get palette source address ready:

move.l #Palette, a0

● Write all 16 words to data port:


As promised, set the background pink!
● VDP register 7 holds the background colour
● Pink is palette 0, colour 6:

● Remember - command, register, value:


0xCRVV

MOVE.W #0x8706, vdp_control


Graphics tiles
● VDP is tile based
● Tiles are 8x8 pixels in size
● 1 nybble (4 bits) per pixel
● Each nybble holds colour index
● If colour 0 is transparency, 1 is red:

● 8x8x4 = 256 bits = 32 bytes


● Don’t forget 1 is black in our palette
so adjust this example accordingly
Maps
● Each tile has an index (0 - 1200, with current register settings)
● VDP has 2 scrolling maps (Plane A and Plane B)
● Map split into 64x32 cells (with current register settings)
● Each cell holds a tile index
Map cells
● If ‘H’ tile was at address 0x0020 (tiles are 32 bytes), it will have tile ID 1
● To set cell 3,1 to H, set it to 1 in the map
Moving tiles to VRAM
● Same method as palette, but VRAM instead of CRAM
● Set control port to write to VRAM
● Loop over data, copying to data port
● All cells are set to 0, so we can test by uploading our ‘H’ to 0x0000
Moving tiles to VRAM
Moving tiles to VRAM
Setting cell values
● Each cell is 1 word in size
● Plane A’s cell data is at 0xC000 (with current register settings)
● LPPH VTTT TTTT TTTT

L = low/high plane
P = Palette index
H = Horizontal flip
V = Vertical flip
T = Tile ID
Setting cell values
● So, let’s move ‘H’ to tile ID 1 (change address to 0x0020)
● Then set first cell (table at address 0xC000) to 1:

SetVRAMWrite 0xC000
move.w #0x0001, vdp_data
Setting cell values
● Planes are 64 cells wide (with current register settings)
● So to increment Y coord, add 0x0080 to cell address (64 in words)
● Address for cell 3,1 = 0xC086
● Address for cell 12,8 = 0xC418
Hello, world!
● Design your font!
● H, E, L, O, W, R, D
● Note the spacing
Hello, world!
● Upload tiles: NumChars*8 longwords (remember -1 for loop counter!)
● Calculate coords for each tile
● Mul X by 2 (2 bytes per cell)
● Mul Y by 128 (64 cells wide, 2 bytes per cell)
● Add to 0xC000
Hello, world!
Shameless plug

tanglewoodgame.com
Made in pure 68000 assembly ;)
Need help making tiles, maps, sprite sheets?

Beehive, at BigEvilCorporation github


Bonus Slides
Are you still here?
Bonus slides - Sprites
● 64 hardware sprites
● Drawn at arbitrary coordinates
● Sprite plane: 512x512, with 128 pixel border
● Use same tiles from same address space as plane cells
● Up to 4x4 tiles per sprite
● Column major ordering
● Sprite table stored in VRAM (address 0xF000 with current register settings)
Bonus slides - Sprites
One sprite table entry:

0000 = 1x1
0001 = 1x2
0010 = 1x3
0011 = 1x4
0100 = 2x1
1000 = 3x1
1100 = 4x1
1111 = 4x4
Bonus slides - Sprites
● Just write entry to sprite table:

● For subsequent sprites, take care of the Link field


Bonus slides - Sprites
Bonus slides - Allocating RAM
● 64KB system RAM
○ From 0x00FF0000 to 0x00FFFFFF
● Programmer has access to all of it, unrestricted, at any time
○ But beware of where the stack lives
● Dynamic allocations (malloc()/free()/new/delete) inappropriate
○ Speed
○ Overhead (headers/footers)
○ Complexity (debugging, leaks)
○ 64KB is small enough to manage
● Recommended: Create a memory map
● SNASM assembler provides “RS” to help manage structures
Bonus slides - A humble memory map

● This just defines labels for offsets from 0x00FF0000 in a tidy structure
Bonus slides - Fetching from/storing to RAM
MOVE.W #10, player_x ; Move 10 to 0x00FF0006
MOVE.W (player_health), d0 ; Move value at 0x00FF0004 to d0
Bonus slides - Structures
rsset 0
player_x rs.w 1
player_y rs.w 1
player_vel_x rs.w 1
player_vel_y rs.w 1
player_health rs.w 1
;----------------------
player_struct_sz rs.w 0

● Re-usable structure (offsets from 0) - reserve player_struct_sz in memory map


Bonus slides - Structures
rsset ram_start
player_1 rs.b (player_struct_sz)
player_2 rs.b (player_struct_sz)

● To reference structure in memory:

lea player_1, a0 ; Load Effective Address


lea player_2, a1

sub.w #attack_damage, player_health(a0)


move.w #jump_vel, player_vel_y(a1)

You might also like