News
Documents
Source Code
Downloads
Message Board


Sega Megadrive Assembly Programming Basics

Copyright, Lewis Bassett, December 2005


Chapter 4 - Assembly Language

Section 5 - Subroutines and Branching

So far we've assumed that our programs are going to run from beginning to end, with each instruction executed consecutively. But what if we want our programs to make desicions? What if we want to split our programs up into different sections? For this we use branch instructions.

Inside the processor is yet another special register, called the Program Counter, or PC for short. The Program Counter holds the address of the next instruction to be executed. Normally, whenever an instruction is being executed, the processor will automatically add to the Program Counter, so that it points to the next instruction.

Branch instructions tell the processor to branch to another piece of code at a different place, by writing a new value into the Program Counter. There are many different variations, and I'm going to explain all the main ones, starting with BRA and JMP.


BRA - Branch and JMP - Jump

These two instructions are very similar to eachother. They basically tell the processor to jump to a set of instructions, stored at a certain address.

Here's an example:

	jmp	$0230;
The above instruction tells the processor to jump to the code stored at location $0230.

JMP instructions jump to an actual address.

The other instruction I mentioned is the BRA instruction. BRA instructions work like JMP instructions, except they use a relative address. That is, they either subtract, or add an address onto the Program Counter.

Here's an example of a BRA instruction:

	bra	-$20
The above instruction jumps to the code stored $20 spaces backwards.

BRA and JMP instructions are unconditional branches, which means that the processor will always branch to the addresses they provide, no matter what.


Code Labels

As you can see, using BRA and JMP instructions to jump to addresses of code is very messy, and practically impossible. Most programmers never use addresses for branch instructions. Instead we can use code labels.

A code label is simple a descriptive label that is used to mark a specific line of code. When the program is assembled, the assembler replaces all of the code labels with addresses, so we can even use code labels just to mark different sections of code too!

To use a code label, first you need to put a label on the line of code you want to jump to:

	AddRegisters:
 		move.b	#1,	d0;
		move.b	#8,	d1;
		add.b	d0,	d1;
Note that code labels must be on the edge of the page, with NO tabs or spaces before or inside the label. This is important, otherwise the assembler will mistake the code label for an instruction, and an error will be returned.

Now, if we wanted to skip some code, and tell the processor to execute the instruction at the 'AddRegisters' section, we simply use the code label instead of the address:

		bra	AddRegisters;
Notice that if we use code labels, we don't have to worry about whether the address is relative to the current possition, or whether it's just a plane normal address; the processor takes care of that for us, so we can use BRA or JMP instructions in exactly the same way!

We can use code labels and branch instructions to make loops:

	Loop:
		add.b	d0,	d1;
		bra	Loop;
This loop will be executed endlessly, since the processor will keep jumping back, executing the code and jumping back again. Loops are useful, since they keep control of the processor and stop it from going off and doing random things. As I explain some more different types of branch instructions, I'll demostrate how loops like this can be used to make very useful and intelligent programs.

Code labels can also be used to mark data definitions:

	Numbers:
		dc.b	01;
		dc.b	02;
		dc.b	05;
Remember, at the beginning of this chapter I explained how the DC directive is used to tell the assembler that you want to define some constant data. This data can be put in sections and marked with code labels, so that other parts of the program can use them.


LEA - Load Effective Address

The LEA instruction is used to put the address of a code or data segment into an address register, so that it can be used as a variable, or part of other more complex functions. Note, that although we can refer to code labels anywhere in the program, we can't alter them inside the program or perform any functions on them. Using the LEA instruction, we can do this.

Assume we had the following data segment:

	EvenNumbers:
		dc.b	02
		dc.b	04
		dc.b	06
		dc.b	08
		dc.b	10
		dc.b	12
		dc.b	14
		dc.b	16

First, the code label is put into an address register:
		lea	EvenNumbers(pc),	a1;
Notice that the code label 'EvenNumbers' has '(pc)' directly after it. PC refers to the Program Counter, and 'EvenNumbers(pc)' refers to the address of 'EvenNumbers' relative to the current value of the PC, ie, the address of the current instruction.

Now, anytime we want to refer to 'EvenNumbers', we can use the address register a1 with indirect addressing:

		bra	(a1);
We can also manipulate the new address, just like we can with any other data stored that's stored in an address register:
		move.b	(a1)+,	$ff0000;
The above instruction copied the first byte from the location which a1 points to, our 'EvenNumbers' data, copies it to the Megadrive main RAM and then increments the address in a1, ie, it points to the next number stored at 'EvenNumbers'.

The full use of the LEA instruction will be demostrated in the next few chapters, as we start to actually program the Megadrive.


Subroutines

A subroutine is a section of code which can be called at anytime, often more than once. When a subroutine is finished, the program will continue running from exactly where it left off. Subroutines are a very tidy way of programming, since it keeps the different parts of a program seperate from eachother.

Before we can use a subroutine, we need to make it. Subroutines can be written as part of your main sourcefile, or as a different file, in which case an 'include' statement is used to insert it into the file. The advantage of using a different file to store your subroutine is that it can be used by more than one program. Subroutines that are used in this way are called modules, and are generally included into the end of the main source file.

Here's a simple subroutine:

	AddRegisters:
		add.b	d1,	d0;
		add.b	d2,	d0;
		add.b	d3,	d0;
		add.b	d4,	d0;
		add.b	d5,	d0;
		add.b	d6,	d0;
		add.b	d7,	d0;

		rts
This subroutine adds the contents of all the registers to d0. Notice the addition of a new instruction: 'rts'. RTS is short for ReTurn from Subroutine, and is used to tell the processor to continue from where it left off. This instruction is essential, as once we've finished with our subroutine, we'll want to continue running the rest of our code from where we left.

To use our subroutine, we use either one of two new instructions: BSR - Branch to SubRoutine, or JSR - Jump to SubRoutine. BRA or JMP will not work, since the processor needs to know that it's branching to a subroutine, so it can save the current address and use it for the RTS instruction.

To run our 'AddRegisters' subroutine, we use this line:

		bsr	AddRegisters;
or
		jsr	AddRegisters;
The differences between JSR and BSR are the same as the differences between JMP and BRA, that is, BSR uses a relative address and JSR uses a definate address.

If we were to store the subroutine in a different file called 'AddRegs.asm', we would need to include the file at the end of the program.

		include	"AddRegs.asm";
Now, we can branch to the 'AddRegisters' subroutine anytime using the BSR instruction as usual.

It's also possible to have subroutines inside of subroutines. This practise is very common, espcially in larger programs.


DBRA - Decrement and Branch

DBRA instructions are used for loops where the ammount of times the processor needs to loop is known beforehand and specified. DBRA instructions use one data register to store the ammound of loops to perform. Each loop, the register specified is decremented, and when the value is zero, the processor won't loop anymore.

Here's a demonstration:

		move.b	#06,	d1;

	Loop:
		add	#$01,	d2;
		dbra	d1,	Loop;	
First of all, the number of times we wish to loop is written into a register, in this case d1. This is done outside the loop. Next, a label is used to mark where we're going to be branching to eachtime we loop, and some instruction are written on the lines below. Finally, a DBRA instruction is used, with d1 specified as register to be tested and decremented, and the code label 'Loop' is inserted after the comma seperator.

Note, DBRA instructions finish when the register is equal to zero, so the loop above actually executes seven times, even though the number six is copied into register d1.

DBRA instructions are most commonly used with LEA instructions to move large ammounts of predefined data. Here's a list of numbers that we're going to need:

	Numbers:
		dc.b	45;
		dc.b	67;
		dc.b	32;
		dc.b	23;
For this example, we're going to move all four numbers into the Megadrive's main RAM, starting at address $FF000E.
		move.b	#3,		d1;
		lea	Numbers(pc),	a1;
		move.l	#$00ff000e,	a2;

	MoveData:
		move.b	(a1)+,		(a2)+
		dbra	d1,		MoveData;
Before the start of the loop, the ammount of times to loop (4, written as 3) is copied into register d1, the relative address of the start of the 'Numbers' data segment is loaded into register a1, and the RAM address we're copying the data to ($ff000e) is written to register a2.

A code label to mark the start of the loop is inserted next, followed by a line which moves the data from the 'Numbers' segment, into the RAM address, and then increments both addresses afterwards. This is crucial, since next time round the loop, the address registers will need to point to the next number in the 'Numbers' segment, and the next location in RAM. In fact, alot of major errors in programs usually result from programmers forgetting to increment the addresses properly. Finally, the number in d1 is decremented, and the loop is executed another 3 more times until d1 is equal to zero, and all four of the numbers are copied into RAM.

This method of copying data is very useful. Rather than using multiple move instructions, which makes the code very messy and very difficult to change, this method is used to make the code much more flexable. The real life use of this type of programming will become clear as we progress onto writing real Megadrive code.


Conditional Branches

Conditional branches work very closly with the Condition Code Register. They are used in the same way as JMP or BRA commands, however, they must always follow logical, CMP or TST instructions. Conditional branches work by checking all the relevant flags in the CCR. If they are set, it branches to the code label specified, just like a regular branch instruction. If the relevant flag isn't set, the condition branch instruction is ignored, and the program continues funning where it is.

There are many different varieties of conditional branches. I'll explain the following: BEQ, BNE, BGE and BLE.


BEQ - Branch if EQual

The BEQ instruction checks the zero flag. If it is set, the branch will be performed, since if two numbers which are equal are compared, the result will always be zero.

Here's an example:

	move.b	#$02,	d1;
	cmpi.b	#$02,	d1;
	beq	NewCode;
The number two is copied into register d1, and then d1 is compared to the number 2. Because they are equal, the zero flag is set. The BEQ instruction reads the zero flag, and since it's set, it branches to the code label 'NewCode'.

Here's another example:

	move.b	#$04,	d1;
	move.b	#$03,	d2;
	cmp.b	d1,	d2;
	beq	NewCode;
Here, the numbers four and three are copied into registers d1 and d2, and are compared to eachother. Since a compare instruction subtracts the source from the operand, the result is minus one, so the zero flag is not set. Instead the negative flag is set, but the BEQ instruction won't be executed.

BEQ instructions can also be used after a TST instruction. It will only branch if the location tested has the value of zero.

Example:

	move.l	#00000000,	d1;
	tst.l	d1;
	beq	NewCode;
Since we've filled d1 with zeros, the result of the TST instruction is zero, so the BEQ instruction is executed and the processor branches to the 'NewCode' label.


BNE - Branch if Not Equal

The BNE instruction is opposite of the BEQ instruction. It too checks the zero flag, however, BNE instructions will only be executed if the zero flag is NOT set, since if the result of a CMP instruction is not zero, the operands aren't equal.

Here's the first BEQ instruction example again, this time with a BNE instruction:

	move.b	#$02,	d1;
	cmpi.b	#$02,	d1;
	bne	NewCode;
Since the two numbers are the same, the CMP instruction sets the zero flag. The BNE instruction checks the zero flag, and since it's clear, the processor doesn't branch to the 'NewCode' label.

Here's the second example again:

	move.b	#$04,	d1;
	move.b	#$03,	d2;
	cmp.b	d1,	d2;
	bne	NewCode;
This time, the two numbers compared are not equal. Because the zero flag is not set, the BNE instruction is executed and the processor branches to the 'NewCode' label.

Here's an example with a TST instruction:

	move.b	#1,	d0;
	tst.l	d0;
	bne	NewCode;
In the above example, the number one is written to register d0. Register d0 is then tested using a TST instruction, and since the result is not zero, the BNE instruction is executed and the processor branches.


BGE - Branch if Greater than or Equal to

The way that this instruction checks the zero and negative flags is quite a complicated process, so I won't explain it.

When a CMP instruction is performed, one of the possible outcomes is 'greater than'. If this is the case, a BGE instruction will be executed. Check this example:

	move.b	#23,	d0;
	move.b	#34,	d1;
	cmp.b	d0,	d1;
	bge	SomeNewCode;
Since d1 is larger than d0, the CMP instruction returns the outcome as 'greater than', and the BGE instruction is executed, branching to the code label 'SomeNewCode'.

Here's another example:

	move.b	#2,	d0;
	move.b	#1,	d1;
	cmp.b	d0,	d1;
	bge	SomeNewCode;
D1 is actually smaller than d0, so the CMP instruction actually returns the outcome as 'less than', therefore the BGE instruction is not executed, and program continues as usual without branching.


BLE - Branch if Less than or Equal to

The BLE instruction is the exact opposite of the BGE instruction, since it only branches when the returned outcome is 'less than'.

Here's the first BGE example, but with a BLE instruction instead:

	move.b	#23,	d0;
	move.b	#34,	d1;
	cmp.b	d0,	d1;
	ble	SomeNewCode;
Since, when d1 is compared to d0, the recorded outcome is 'greater than', the BLE instruction is not executed, and the program doesn't branch.

Here's the second example:

	move.b	#2,	d0;
	move.b	#1,	d1;
	cmp.b	d0,	d1;
	ble	SomeNewCode;
In this example, registers d1 and d0 are compared. Since d1 is less than d0, the returned outcome is 'less than', and the BLE instruction is executed. The processor branches to the label 'SomeNewCode'.

That's all of the main branch instructions, and their different uses explained. Of course, there are many more different varieties of these instructions which I have left out. Details of these instructions can be found in the M68000 Programmer's Reference.

In the final section of this chapter, I'll explain a few more useful instructions, before we move onto the Megadrive ROM format.



Chapter 4 - Section 4 Contents Chapter 4 - Section 6


Designed & maintained by Lewis AS Bassett
SEGA, Megadrive, Genesis, Sonic the Hedgehog, etc are all owned by Sega Enterprises Ltd