Saturday, September 7, 2024

ALPP 02-04 -- Foothold! (Split Stacks ... barely on the Beach) -- 68000

Foothold!
(Split Stacks ... barely on the Beach)
68000

(Title Page/Index)

 

In the last chapter, we took a bit of a closer look at how to use many of the 6809's key improvements over the 6800 and 6801. In this chapter, we'll look at using similar features in the 68000.

Again, you may want to compare the code in separate browser windows or even on separate monitors.

Where the 6809 gives us either two 8-bit accumulators or one 16-bit accumulator, the 68000 gives us a monstrous eight -- D0 ~ D7. Where the 6809 gives us four or five generally indexable registers -- X, Y, U, S, and PC, the 68000 gives us eight or ten -- A0 ~ A6, system and user A7, and PC.

Anyway, let's take a look at how OUTC can change on the 68000. Somewhat randomly, I am choosing to use A6 as the the parameter stack pointer. OUTC can use the 68000's pop equivalent instructions:

* In these examples, we'll assume that items on the parameter stack
* are full natural width (NATWID bytes), as a simplifying assumption.
OUTC	MOVEM.L	(A6)+,D7	; Get the character, all 32 bits
	AND.W	#$00FF,D7		; clear character high byte.
	MOVEM.W	D7,-(A7)	; Put it where bconout wants it, on the A7 stack.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS

Note that this looks like a lot more code than the 6809 code, but most of that is for the BIOS call. Where the call into the monitor ROM on the 6809 was one instruction, the BIOS call here is four, including the final deallocation. Also, in the 6809 monitor code, we did not need to clear the high byte, and here we do. 

So it would be essentially the same number of instructions if we were comparing like calls with like.

We could clear the high byte after we move it where bconout expects it:

OUTC	MOVEM.L	(A6)+,D7		; Get the character, all 32 bits.
	MOVEM.W	D7,-(A7)	; Put 16 bits of it where bconout wants it.
	CLR.B	(A7)	; Clear the high byte on the A7 stack.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS

Or we could clear D7 before we get just the byte:

* We can handle the parameter stack directly.
OUTC	CLR.W	D7		; clear the high byte for the character.
	MOVE.B	NATWID-1(A6),D7	; Get the character.
	ADDQ.L	#NATWID,A6	; drop it from parameter stack.
	MOVEM.W	D7,-(A7)	; Put it where bconout wants it, on the A7 stack.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS

What if we even want to preserve D7? (We probably don't, but ...):

OUTC	LEA	NATWID/2(A6),A6	; get the high 16 bits out of the way
	MOVE.W	(A6)+,-(A7)	; move the character from one stack to the other
	CLR.B	(A7)		; clear character high byte, as bconout wants it.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS

When you want to adjust an address or other value by small amounts, the ADDQ instruction is usually the Quickest. Think of it as INC by anything from 1 to 8. 

Load Effective Address using constant offset also works, similar to the 6809.

Again, we have PC relative source addresses to help keep the code easy to move from place to place. Here, too, the assembler figures out the offset from the label, so you use the label in the source code, but the offset is assembled to the object code.

Again, if you haven't already, open up separate browser windows for the introduction to split stacks on the 6809 so you can compare.

Of course, we don't need any equivalent to the PPUSHD and PPOPD routines. I've used the MOVEM instruction in the above examples, to let you know MOVEM is there. Also, MOVEM does not alter the CPU processor state flags, like a straight MOVE would. But a straight MOVE with auto-increment or -decrement can be used if you only have one register to push or pop.

MOVEM, like the 6809 PSH and PUL, can move Multiple registers with a single instruction.

One word of warning about using MOVEM as the default POP instruction -- Even though it doesn't affect the flags, it does sign-extend 16-bit values loaded into data registers. So you'll need to keep that in mind.

The 68000 routine to set up the stack pointers looks a lot like the 6809 routine::

INISTKS	LEA	PSTKBAS(PC),A6	; Set up the parameter stack
	MOVEM.L	(A7),A0		; 68000 lets us do this -- return address in A0
	MOVE.L	A7,D7		; Save what the monitor gave us.
	LEA	SSTKBAS(PC),A7	; Move to our own stack
	MOVEM.L	D7,-(A7)	; Save monitor's stack pointer on ours
	JMP	(A0)	; return via A0

When we're done, we can restore the monitor's stack pointer with a simple load:

DONE	MOVEM.L	(A7),A7		; restore the BIOS stack pointer

Let's look at the complete code to test character output:

	OPT LIST,SYMTAB	; Options we want for the stand-alone assembler.
	MACHINE MC68000	; because there are a lot the assembler can do.
	OPT DEBUG	; We want labels for debugging.
	OUTPUT
***********************************************************************

*
NATWID	EQU	4	; 4 bytes in the CPU's natural integer


BIOSTRAP	EQU	13
bconout		EQU	3
devscrkbd	EQU	2


	EVEN	
ENTRY	JMP	START
	EVEN
* No need for PSP in 68000 --
* A6 will be our parameter stack pointer
*
* No need for SSAVE, either. We will keep SSAVE in an unused register.
*
SSTKLIM	DS.L	16	; 16 levels of call, max
* 68000 is pre-dec (decrement before store) push
SSTKBAS	DS.L	1	; a little bumper space
PSTKLIM	DS.L	16*2	; 16 levels of call at two parameters per call
PSTKBAS	DS.L	1	; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.)


* MOVEM is the designated PUSH/POP for the 68000, sort-of.
* Usually it is used to save and restore multiple registers in one instruction.
* It can also be used to avoid messing with the processor state flags.
* We're using it to bring it to yor attention.
* Otherwise, for only one register, you will usually use a normal MOVE.
* Unless you want to preserve flags.
INISTKS	LEA	PSTKBAS(PC),A6	; Set up the parameter stack
	MOVEM.L	(A7),A0		; 68000 lets us do this -- return address in A0
	MOVE.L	A7,D7		; Save what the monitor gave us.
	LEA	SSTKBAS(PC),A7	; Move to our own stack
	MOVEM.L	D7,-(A7)	; Save monitor's stack pointer on ours
	JMP	(A0)	; return via A0

* No need for PPOPD or PPUSHD in 68000 code.

* We can handle the parameter stack directly.
* In this example, we'll assume that items on the parameter stack
* are full natural width (NATWID bytes), as a simplifying assumption.
OUTC	CLR.W	D7		; clear the high byte for the character.
	MOVE.B	NATWID-1(A6),D7	; Get the character.
	ADDQ.L	#NATWID,A6	; drop it from parameter stack.
	MOVEM.W	D7,-(A7)	; Put it where bconout wants it, on the A7 stack.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS
	
*
START	BSR.W	INISTKS		; 68000 lets us do medium range relative branches
*
	MOVE.L	#'H',-(A6)	; push character to be output at natural width
	BSR.W	OUTC
*
DONE	MOVEM.L	(A7),A7		; restore the BIOS stack pointer
	NOP
	NOP
* One way to return to the OS or other calling program
	clr.w	-(sp)	; there should be enough room on the caller's stack
	trap	#1		;	quick exit

And that's it. 

Here again, line buffering requires you to (c)ontinue the code through the return to the TOS command line, to see the character output.

Save the code as something like "out_H_2s.s" in the directory where you're doing the 68000 Atari ST work and use vasm to assemble it, using a command something like

$ vasmm68k_mot -Ftos -no-opt -o OUT_H_2s.PRG -L out_H_2s.lst out_H_2s.s

Refer back to Hello World for 68000 example in the H68000's Hello World chapter if you don't remember how to invoke the debugger, set breakpoints, and run it.

Unlike the 6800/6801 and 6809, the 68000 has an exception to break on undefined op-codes. But you don't want to depend on garbage in memory being undefined op-codes, so do set breakpoints.

And now, again, really straightforward (once you've seen the above), the string output test routine:
* First 68000 Foothold on the split stack beach,
* by Joel Matthew Rees September 2024, Copyright 2024 -- All rights reserved.
*
	OPT LIST,SYMTAB	; Options we want for the stand-alone assembler.
	MACHINE MC68000	; because there are a lot the assembler can do.
	OPT DEBUG	; We want labels for debugging.
	OUTPUT
***********************************************************************

* We will not be using these:
*GEMDOSTRAP	EQU	1
*GEMprintstr	EQU	9	; PRINT LINE in some docs

BIOSTRAP	EQU	13
bconout		EQU	3
devscrkbd	EQU	2

LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0

* Opinions may vary about the natural integer.
NATWID	EQU	4	; 4 bytes in the CPU's natural integer

	EVEN	
ENTRY	BRA.W	START

* No need for PSP in 68000 --
* A6 will be our parameter stack pointer
*
* No need for SSAVE, either. We will keep SSAVE in an unused register.
*
SSTKLIM	DS.L	16	; 16 levels of call, max
* 68000 is pre-dec (decrement before store) push
SSTKBAS	DS.L	1	; a little bumper space
PSTKLIM	DS.L	16*2	; 16 levels of call at two parameters per call
PSTKBAS	DS.L	1	; bumper space -- parameter stack is pre-dec
* (But this example doesn't use half of that number of levels.)
*
HELLO	DC.B	CR,LF	; Put message at beginning of line
	DC.B	"SEKAI YO, YAI!"	; Whatever the user wants here.
	DC.B	CR,LF,NUL	; Put the debugger's output on a new line.

* MOVEM is the designated PUSH/POP for the 68000, sort-of.
* Usually it is used to save and restore multiple registers in one instruction.
* It can also be used to avoid messing with the processor state flags.
* We're using it to bring it to yor attention.
* Otherwise, for only one register, you will usually use a normal MOVE.
* Unless you want to preserve flags.
	EVEN
INISTKS	LEA	PSTKBAS(PC),A6	; Set up the parameter stack
	MOVEM.L	(A7),A0		; 68000 lets us do this -- return address in A0
	MOVE.L	A7,D7		; Save what the monitor gave us.
	LEA	SSTKBAS(PC),A7	; Move to our own stack
	MOVEM.L	D7,-(A7)	; Save monitor's stack pointer on ours
	JMP	(A0)	; return via A0

* No need for PPOPD or PPUSHD in 68000 code.

* We can handle the parameter stack directly.
* In this example, we'll assume that items on the parameter stack
* are full natural width (NATWID bytes), as a simplifying assumption.
OUTC	CLR.W	D7		; clear character high byte.
	MOVE.B	NATWID-1(A6),D7	; Get the character, high byte cleared
	ADDQ.L	#NATWID,A6	; drop it from parameter stack.
	BSR.S	OUTCV		; Can't just fall through if we're going to share.
	RTS
*
* Return from TRAP is RTE,not RTS,
* therefore, this must be called as a subroutine, not just jumped to.
* Common hook: call with the character on A7 stack. 
* This bit of glue calls the BIOS.
OUTCV	MOVE.W	D7,-(A7)	; Push the character on A7 where bconout wants it.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS

*
* And we can handle the parameter stack directly here, too.
* A3 is preserved by Hatari BIOS.
OUTS	MOVEM.L	(A6)+,A3	; get the string pointer
OUTSL	CLR.W	D7		; Prepare the high byte.
	MOVE.B	(A3)+,D7	; get the byte, update the pointer
	BEQ.S	OUTDN		; if NUL, leave before pushing the character.
	BSR.S	OUTCV	; use the same call OUTC uses.
	BRA.S	OUTSL	; next character
OUTDN	RTS
*
* All the newlines we need are in the string.


*
START	BSR.W	INISTKS		; 68000 lets us do medium range relative branches
*
	LEA	HELLO(PC),A0	; push address of string to be output
	MOVEM.L	A0,-(A6)	; A0 is not preserved by Hatari BIOS, by the way.
	BSR.W	OUTS		; put it on the screen,
DONE	MOVE.L	(A7),A7		; restore previous user stack pointer
	NOP
	NOP
* One way to return to the OS or other calling program
	clr.w	-(sp)	; there should be enough room on the caller's stack
	trap	#1		;	quick exit

Note again, all the uses of the auto-increment index mode, not just in the string output. 

Note also the choices of index registers. 

In particular, I got trapped in forgetting that A0 is volatile across BIOS calls and had to debug some code before putting it up here. Can't use it and expect it to be there after a BIOS call, but we can use it for scratch calculations and temporarily holding addresses when we don't call a subroutine between setting it and using it. (This is information in the Atari manuals.)

Also note that we don't put a few BIOS parameters on the A7 stack and then jump into a subroutine that puts more on the A7 stack, because that would require moving return addresses out of the way. I got bitten by this, trying to push the character on A7 before calling OUTCV and pushing the BIOS call selectors on A7. (Remember what I said about using just one stack?)

And the 68000 also makes it is quite easy to have OUTS call OUTC rather than the common code subroutine, like this:

OUTS	MOVEM.L	(A6)+,A3	; get the string pointer
OUTSL	CLR.L	D7		; Prepare the high byte.
	MOVE.B	(A3)+,D7	; get the byte, update the pointer
	BEQ.S	OUTDN		; if NUL, leave before pushing the character.
	MOVE.L	D7,-(A6)	; push it on the parameter stack.
	BSR.S	OUTC	; use OUTC itself.
	BRA.S	OUTSL	; next character
OUTDN	RTS

Here we can get a peek at one of the differences between the 6809 and the 68000. 

Concatenating low bytes and high bytes is easy on the 6809. Not so easy on the 68000 -- so much so that when you need to clear the higher part of an integer on the 68000, it's generally best to clear a free register before loading the lower part of the integer.

Like the 6809, it takes just a little extra code to call OUTC rather than the common subroutine, but, like the 6809, it can make it easier to manage character output related code, and, like the 6809, separating the common code into a shorter subroutine may be useful.

Which way do I recommend? It depends, and I'll tell you more about that as we go.

Again, lightly tested, should work for you. Play with the code a little while we're here. And think about checking whether the stacks are in balance.

After you've played with it a bit, let's start looking at doing integer arithmetic at byte level on the three 8-bit processors.

 

(Title Page/Index)

 

 

No comments:

Post a Comment