The 12 most commonly used CPU instructions.

The Motorola 68000 CPU has about 50–60 basic instructions, each with variants that bring the total to around 150. If you’re coding for the 68080 CPU, you can multiply those numbers significantly. Luckily, you can get pretty far with just a few of the basic ones, and an understanding of variants. In this article, I’ll share just 12 commands that are all you need to build complete projects.

You only need 12 commands to code in assembler.

Even though the 68000 is a 16-bit CPU, moving at most 16 bits of memory in one go, it has a 32-bit internal architecture and appears as a 32-bit processor from a programmer’s perspective. From the 68020 onward, the CPUs in this series are true 32-bit. Instructions that read or write data have variants indicating the size of the data being processed. This is done by adding a suffix to the command.

instruction.bByte – 8 bits
instruction.wWord – 16 bits
instruction.lLong – 32 bits

Be careful to use the correct size when writing your code. Getting this wrong is one of the most common mistakes and can lead to tricky bugs. If you write a byte-sized value into a register, you have no idea what’s in the upper 24 bits. Reading that register as a long afterward might just give you a random number.

Here’s the list of the 12 most common instructions along with a couple of usage examples for each.

move
lea
add
sub
cmp
tst
btst
beq (and other branches)
dbf
jsr
bsr
rts

MOVE – The move command is used to move data to and from memory and registers.

InstructionExampleDescription
move.bmove.b #255,d0Write the number 255 into the lower 8 bits of d0
move.wmove.w #$4ff,d0Write the hexadecimal number 4ff into the lower 16-bits of d0
move.lmove.l a0,d0move the address stored in a0 into d0
move.l (a0),d0Move the content that a0 points to into d0.
move.l (a0)+,d0Same as above, but modify a0 to point to the next address.
move.l (a0),(a1)move the data that a0 point to, into the address that a1 points to.
moveqmoveq #200,d0Move the number 200 into d0 with a quick command, but ensure the number is 8-bit. The operation still performs a 32-bit write to the register.
movea.lmovea.l d0,a0move the content of d0 into address register a0
movea.la0,a1move the content of a0 into a1
movemmovem.l d0-d2/a6,-(sp)Move multiple. This example stores d0,d1,d2, and a6 on the stack.

The key takeaways are:

  • # is used to indicate a hardcoded number to be written; otherwise, it represents an address.
  • The $ symbol is used to indicate that the number is in hexadecimal format.
  • An address register in parentheses refers to the data stored at the address contained in the register.
  • MOVEA is used when the destination is an address register, but most assemblers will automatically convert MOVE to MOVEA during assembly.

LEA – Load Effective Address

The LEA command creates a pointer to an address and stores it into an address register.

Example

lea mydata,a0

mydata: dc.l 1000,2000,3000,0

Here we have a data section with some numbers. The LEA command will load the a0 register with the address where these numbers are stored. A movea.l mydata,a0, on the other hand, would place the actual number 1000 into a0. This is an important distinction.

Add – Mathematical addition.

ExampleDescription
add.l #50,d0Add the content of d0 with 50
add.w #50,d0Same as above, but only writes the lower 16-bits.
addq.l #5,d0Add the content of d0 with 5. Quick command that only works with numbers between 1 and 8.
add.l d0,d1Add the content of d0 to d1. result is stored in d1
adda.l #4,a0Adding 4 to the address stored in a0. a0’s pointer have now been increased with 4 bytes.

Sub – Mathematical substraction.

ExampleDescription
sub.l #50,d0Substracts the content of d0 with 50
sub.w #50,d0Same as above, but only writes the lower 16-bits.
subq.l #5,d0Substracts the content of d0 with 5. Quick command that only works with numbers between 1 and 8.
sub.l d0,d1Substracts the content of d0 to d1. result is stored in d1
suba.l #4,a0Substracts 4 to the address stored in a0. a0’s pointer have now been decreased with 4 bytes.

Tip: If you want to clear an address register, you can indeed use:

movea.l #0,a0

However, the same result can be achieved using sub, which would be quicker for the CPU to execute.

suba.l a0,a0

CMP – Compare two values and sets status flag

ExampleDescription
cmp.b #100,d0Check if the lower 8 bits of d0 contain the value 100.
cmp.l #100,d0Check if d0 (all 32 bits) contain the value 100.
cmp.l d0,d1Check if the content of d0 is the same as in d1.
cmpa.l a0,a1Check if content of a0 is the same as in a1
cmpm.l (a0),(a1)Check if the data at the location a0 points to is the same as the data at the location a1 points to.
cmpi.l #100,(a0)Check if the data at the location a0 points to is 100

CMP instructions are paired with branch commands like beq, bne, and others to handle conditions correctly.

Start:
	cmp.b	#'A',d0		        ; Does d0 contain the letter A?
	beq	.Afound	        	; Branch if equal to .Afound
		
	cmp.b 	#'B',d0		        ; Does d0 contain the letter B?
	beq	.BFound 		; Branch if equal to .BFound
		
	....
	....
		
.done
	rts				; Return from subroutine
		
		
.Afound	moveq	#0,d0
	bra	.done	        	;Branch always

.Bfound	moveq	#1,d0
	bra	.done    		; Branch always
		

TST – Test for 0 and set status flag

ExampleDescription
tst.l d0Does d0 contain 0?
tst.b (a0)Does the byte a0 points to contain 0?

TST is faster than using CMP to test for zero. Note that TST can not test an address register.

BTST – Bit Test – Test single bit for value 0

ExampleDescription
btst #7,d0Does bit number 7 in d0 contain 0?
btst #20,d0Does bit number 20 in d0 contain 0?
btst #6,$bfe001Does bit number 6 at address $bfe001 contain 0 ?

Important: When testing a bit in a data register, you can use a number between 0 and 31. When testing directly in memory, it’s an 8-bit operation, so you’re checking the byte at the given address. In that case, you should use a number between 0 and 7.

Branches – A branch jumps to a specific location when a condition is met.

ExampleDescription
beqBranch if equal to
bneBranch if not equal to
bgtBranch if greater than
bgeBranch if greater or equal to
bltBranch if less than
bleBranch if less or equal to
braBranch always
cmp.l d0,d1		; Compare d0 to d1
beq   .isEqual		; Jump if d0 and d1 have the same values
bgt   .isGreater	; Jump if d0 have a greater value than d1 does
blt   .isLess		; Jump if d0 have a lower value than d1 does
bne   .isNotEqual	; Jump if d0 and d1 are not equal.

DBF – Decrement and Branch if False.

This is your go-to command for creating loops that run a specific number of times. DBF decreases the data register by one and branches to the label if the register is not -1.

	moveq	#9,d0		; Loop 10 times (0-9)
.loop:
	; ... your code ...
	dbf	d0,.loop	; d0 = d0 -1, branch if d0 >= 0

JSR – Jump to Subroutine

This command lets you jump to a subroutine and return to the same point once it’s finished. In AmigaOS, it’s widely used for calling OS functions.

BSR – Branch to Subroutine

BSR works just like JSR, but it’s a more efficient command that can only jump up to 32KB from the current location. It’s commonly used to jump to your own subroutines within your code.

RTS – Return from Subroutine

RTS returns from a subroutine called by either JSR or BSR.

Start:	
	moveq	#100,d0
	move.l	#5000,d1
	bsr	AddNumbers
	....				; d0 now contains the value 5100
	rts				; Exit this program


	; AddNumbers
	; INPUT: d0,d1 = numbers to add
	; OUTPUT: d0 = result
AddNumbers:
	add.l	d0,d1			; Adds d0 to d1
	move.l	d1,d0			; Move result to d0
	rts				; Return to line after the calling BSR