Showing posts with label discipline. Show all posts
Showing posts with label discipline. Show all posts

Wednesday, September 11, 2024

ALPP 02-XX -- Introduction to Address Math on the 6800, 6801, 6809, and 68000

This attempt at a chapter went sideways. Keeping it for the records.
Go here, instead: https://joels-programming-fun.blogspot.com/2024/09/alpp-02-05-introduction-to-byte-arithmetic-6800-6801-6809.html

Introduction to Address Math
on the 6800, 6801, 6809, and 68000

(Title Page/Index)

 

In the last chapter, we saw how the 68000 provides similar advanced addressing features to the 6809's.

Now we want to take an introductory look at general address math on all four processors.

Why do you need to do address math?

We've seen one of the use cases, allocating and deallocating space on the stacks. It's also necessary when allocating and deallocating space for variables and other stuff that needs to stick around between calls to various routines.

It's also useful when accessing distinct fields within structured variables and other objects, and when linking such objects together.

The 6800 doesn't have much in the way of address math. INX and DEX are about it. So you have to use the more general integer math, which only comes in byte size on the 6800.

So you have to synthesize the general math, along with some specific useful cases of general math. That's what we'll look at first.

You've noticed by now that there is a carry bit in the 6800's processor state. It's the least significant bit of the state flags, which is convenient when trying to find out its state. But we don't need to know that for combining two 8-bit adds to make a 16-bit add, because we have the ADd Carry instruction that adds the carry bit leftover from the last operation in along with its specified operands.

ADD and ADC are binary operands. Binary operands on 680X and 680XX family CPUs specify one accumulator or register as both a source and a target, and one memory operand as the second source -- thus, in the case of ADD and ADC, add something in memory to a register.

So you can use the Carry bit to chain the addition of bytes together, pretty much the way you use the carry bit in ordinary arithmetic to chain columns together. 

(If you want, you can consider the addition to be to numbers in base 256, and think of each byte as a column. If that does anything for you. If not, forget I mentioned it.)

Here's how to add two 16-bit numbers on the 6800 when both are in memory, either in the direct page or in the extended absolute addressing area:

* Add two 16-bit numbers in memory and store them in memory:
*
ENTRY	JMP	ADDM16
*
N1	FDB	57738	; just an arbitrary number
N2	FDB	17715	; another arbitrary number
RES	RMB	3	; Enough room to hold a carry from the high bytes.
*
ADDM16	LDAB	N1+1	; do the low byte (right columns) first.
	ADDB	N2+1	; Sets the Carry flag if the result is too big to fit.
	STAB	RES+2	; LoaD and STore do not affect the Carry flag.
	LDAA	N1	; Now do the high bytes (left columns).
	ADCA	N2	; ADd in the Carry from the low bytes as well.
	STAA	RES+1	; Save the high byte away.
	LDAB	#0	; CLR would clear the carry.
	ROLB		; Move the carry in
	STAB	RES	; Save the carry in the low bit of the highest byte.
	NOP
	NOP

This is not the way we usually do this, but it is useful for stepping through and watching the columns get added together. 

There is no real reason for using A for the high byte. B could be used instead and, other than that, the code would not change. Using B only would have the potential advantage of preserving A.

This code would work as is on the 6801 and 6809, with the proviso that you would use LDA for LDAA and STA for STAA on the 6809. But, of course, it could be done 16 bits at once on both:

* Add two 16-bit numbers in memory and store them in memory:
*
ENTRY	JMP	ADDM16
*
N1	FDB	57738	; just an arbitrary number
N2	FDB	17715	; another arbitrary number
RES	RMB	3	; Enough room to hold a carry from the high bytes.
*
ADDM16	LDD	N1	; Both bytes at once
	ADDD	N2	; Sets the Carry flag if the result is too big to fit.
	STD	RES+1	; LoaD and STore do not affect the Carry flag.
	LDAB	#0	; CLR would clear the carry.
	ROLB		; Move the carry in
	STAB	RES	; Save the carry in the low bit of the highest byte.
	NOP
	NOP

... using STA instead of STAA at the end for the 6809 code.

As a reader challenge, work the 6809 code out. (Hardly worth calling a challenge, but do it anyway.)

And, especially if you don't feel comfortable that you know what's happening, take the time to play with the code

On the 68000, of course we can do it a byte at a time or 16 bits at once, or ... 

Wait a minute. 

If we do it a byte at a time or even 16 bits at once, things get a little weird with the 68000. It's intended as a 16-bit/32-bit processor, and it kind of shows in places, particularly when using 24-bit results and other odd sizes.

In the 68000, the Carry function is split up between the X flag, used for eXtending multiple precision additions and subtractions, and the C flag, used for testing and branching. Instead of ADC and SBC, there is ADDX and SUBX, and ADDX and SUBX operate only on registers or in predecrement mode on numeric strings in memory. (Interestingly, the 68000 adds a CMPM instruction, to operate only in postincrement mode on numeric strings in memory.)

And then there is the even address requirements for the 68000. If you store or load by 16 bits or 32, you have to access memory on an even (divisible by 2) boundary.

Sounds wonky?

Yes and no. Let's look at what that means for the 16+1 bit math from above:

* Add two 16-bit numbers in memory and store them in memory:
* Both of these are messy because we are doing odd addresses.

Fix these***

*
ENTRY	JMP	ADDM16W
*
	EVEN
N1	DC.W	57738	; just an arbitrary number
N2	DC.W	17715	; another arbitrary number
RES	DS.B	3	; Enough room to hold a carry from the high bytes.
*
	EVEN
ADDM16W	CLR.B	D6	; pre-clear the carry byte
	MOVE.W	N1,D7	; One 16 bit word at a time
	ADD.W	N2,D7	; Sets the Carry and eXtend flags on carry.
	ROXL.B	#1,D6	; Save the eXtended carry in X first
	MOVE.B	D6,RES	; and move it into place (rotate on memory is word only).
	MOVE.B	D7,RES+2	; Save result low byte. Does not affect the X flag.
	ASR.W	#8,D7	; bring the high byte down
	MOVE.B	D7,RES+1	; because we can't save a 16-bit word to an odd address
	NOP
	NOP
	CLR.W	RES	; clear the result so we can do it again.
	CLR.B	RES+2
ADDM16	LEA	RES+3,A3	; point one beyond RES, same as MOVE #RES+3,A3
	LEA	N2+2,A2		; point one beyond N2, same as MOVE #N2+2,A2
	MOVE.B	-(A2),-(A3)	; copy low byte
	MOVE.B	-(A2),-(A3)	; copy high byte
	ADDQ.L	#2,A3		; back up on RES (100% sure clears X)
	LEA	N1+2,A2		; Actually, it was already there
	CLR.B	D7		; pre-clear the carry byte
	ADDX.B	-(A2),-(A3)	; add low bytes directly in memory
	ADDX.B	-(A2),-(A3)	; and high bytes directly in memory
	ROXL.B	#1,D7		; Save the eXtended carry in X
	MOVE.B	D7,-(A3)	; and move it into place (rotate on memory is word only).
	NOP
	NOP

So it takes more instructions than we want to think it would take -- because of the odd alignment issues. 

(FWIW, certain later members of the 680X0 family relax the alignment require about memory access, but the instruction set does not get dressed-out to fill in certain gaps for byte-level operands. So with those processors, we could improve the number of instructions a little, but not completely to the degree the 6809 allows. I won't look at those CPUs at this point, we have more important things to do.)

But, wait! (There's more ...)

Addresses on a 68000 are not 16 bit. 

Well, unless your runtime is only using a 64 K page within the total memory space or something like that. But that's a story for another time.

And, addresses don't use the carry bit. It's modular math. They just wrap around.

(Wraparound is useful when adding negative offsets, but requires some care in pointer comparison.) 

Hmm.  The 6800 and 6801 code is easy enough to read through and remove the part extracting and saving the carry, but the 68000 code needs another look.

* Add two 16-bit numbers in memory and store them in memory:
* Much less messy with even addresses.
*
ENTRY	JMP	ADDM16W
*
	EVEN
N1	DC.W	57738	; just an arbitrary number
N2	DC.W	17715	; another arbitrary number
RES	DS.B	2	; No carry, keep it aligned
*
	EVEN
ADDM16W	CLR.B	D6	; pre-clear the carry byte
	MOVE.W	N1,D7	; One 16 bit word at a time
	ADD.W	N2,D7	; Add it.
	MOVE.W	D7,RES	; and store the result non-destructively.
	NOP
	NOP
	CLR.W	RES	; clear the result so we can do it again.
ADDM16B	LEA	RES+2,A3	; point one beyond RES, same as MOVE #RES+3,A3
	LEA	N2+2,A2		; point one beyond N2, same as MOVE #N2+2,A2
	MOVE.W	-(A2),-(A3)	; copy all at once
	ADDQ.L	#2,A3		; back up on RES, probably (100% sure) clears X
	LEA	N1+2,A2		; Actually, it was already there
* All this setup just so we can do this,
* one byte at a time to show how to chain adds on the 68000:
	ADDX.B	-(A2),-(A3)	; add low bytes directly in memory
	ADDX.B	-(A2),-(A3)	; and high bytes directly in memory
* Yes, ADDX can be done .W and .L widths, as well.
	NOP
	NOP


 

 


 

(Title Page/Index)

 

 

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)

 

 

Sunday, September 1, 2024

ALPP 02-03 -- Foothold! (Split Stacks ... barely on the Beach) -- 6809

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

(Title Page/Index)

 

The differences we saw between the 6800 and the 6801 were relatively minor, but not unimportant. The very small advances in the 6801 make it significantly easier to keep the CPU responsive and stable while handling interrupts and other kinds of concurrency.

And now we finally get a look at some of the important differences between the 6800 / 6801, and the 6809. It may be a little mind-boggling. You really want to open up separate browser windows and compare the code for each, preferably on separate monitors. 

Even if you don't quite follow what's going on, compare the code. The more times you see this from different points of view, the easier it gets.

Again, as I have said, one major purpose of the split stack is to separate return addresses from parameters and local variables, producing somewhat more robust, less difficult to debug code. 

And, I'm going to repeat myself (again?), you probably won't believe me yet, but the split stack also simplifies and streamlines the call interface for the subroutines and procedures you write. 

Let's start taking a look at why. 

The 6809 includes the same ability to concatenate A and B that the 6801 has. (Historically, the 6809's design actually came first.)

The 6809 also gives us two more index registers, or four, depending on how you count, adding Y and U to X and S. Where in the 6800/6801, only X can take an offset and be used as a general index register, all four, plus PC, can take an offset in the 6809. Additionally, U also specifically supports pushes and pops, and we shall be using it instead of PSP for the parameter stack. 

(We'll talk about the 6809's partially indexable DP and how to use it well later.)

Enough sales talk, let's take a look at how OUTC can change on the 6809.

OUTC will use the 6809's pop instruction (PULU) for U:

* Walks on contents of B.
OUTC	PULU	D	; Get the character, 
	TFR	B,A	; Put it where XOUTCH wants it.
	JSR	XOUTCH	; output via monitor ROM
	RTS

Or we could do two PULU A instructions and preserve B.

* Does not Walk on B.
OUTC	PULU	A	; Get the high byte out of the way, 
	PULU	A	; Get the character where XOUTCH wants it.
	JSR	XOUTCH	; output via monitor ROM
	RTS

But the 6809 also provides a way to access the U stack directly:

* Preserves B.
OUTC	LDA	1,U	; Get the character in A where XOUTCH wants it.
	LEAU	2,U	; Got it in A, now drop it from the stack.
	JSR	XOUTCH	; output via monitor ROM
	RTS

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

OUTC	PSHS	A	; Save A
	LDA	1,U	; Get the character in A where XOUTCH wants it.
	LEAU	2,U	; Got it in A, now drop it from the stack.
	JSR	XOUTCH	; output via monitor ROM
	PULS	A,PC	; Pop and return in one instruction.

Load Effective Address shows up again, this time replacing a separate INCrement instruction for U, or, rather, replacing what would have been two INU instructions if the 6809 had them. 

For source registers other than PC, LEA adds the signed offset to the specified index register, in this case +2 to U.

For PC relative source addresses (which we will see again below), 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.

(If you have a macro assembler, you can define INU as LEAU 1,U, but that's rather wasteful if you use it more than once in a row.)

And there are several other interesting ways we could do this.

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

Note that X is not used, no need to push and pop it, it's preserved. 

Note also that PPUSHD and PPOPD simply go away (along with PPUSHX and PPOPX), replaced by the 6809's more flexible PSHU and PULU instructions.

The 6809 gives us some really wonderful options for setting up the stack pointers. Not only can we pop the return address directly to X like we can on the 6801, but we can save the monitor's stack pointer on our own stack, eliminating yet another global variable:

INISTKS	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X	; 6809 lets us do this -- return address in X
	TFR	S,Y	; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	PSHS	Y	; Save monitor's stack pointer on ours
	JMP	,X	; return via X

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

DONE	LDS	,S	; restore the monitor stack pointer

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

* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
* No need for PSP in 6809 --
* U will be our parameter stack pointer,
* And we will save S on our own stack.
*
	ORG	$2000	; MDOS says this is a good place for user stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; 16 levels of call, max
* 6809 is pre-dec (decrement before store) push
SSTKBAS	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.)
*
*
INISTKS	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X	; 6809 lets us do this -- return address in X
	TFR	S,Y	; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	PSHS	Y	; Save monitor's stack pointer on ours
	JMP	,X	; return via X
*
* No need for PPOPD or PPUSHD in 6809 code.
*
* We can handle the parameter stack directly.
* Preserves B.
OUTC	LDA	1,U	; Get the character in A where XOUTCH wants it.
	LEAU	2,U	; Got it in A, now drop it from the stack.
	JSR	XOUTCH	; output via monitor ROM
	RTS
*
*
START	LBSR	INISTKS
*
	LDD	#'H
	PSHU	D
	LBSR	OUTC	; 6809 lets us do long relative branches
*
DONE	LDS	,S	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

And that's it.

Change back to the directory for JHAllen's unmodified EXORsim and run an instance of EXORsim 6809, assemble the source, set a breakpoint at the landing pad NOPs, and run it. (Refer back to Hello World for 6809 in the 8-bit Hello World chapter if you don't remember how to do this.) 

Do set the breakpoint. 

EXORsim is nice enough to break on undefined opcodes, but, with the 6809, zero is a defined opcode -- the direct-page form of the NEGate instruction. So it won't stop, It will wander off and will eventually start executing the monitor or something and end up jumping into MDOS or something, which will be confusing.

Be aware, the actual 6800 is not nice enough to break on undefined opcodes. So if you were getting lazy about breakpoints on the 6800 and 6801, remember you can't do that on real hardware.

This is so short that I almost want to do the 68000 code here in the same chapter and be done with it. But I want you to be able to compare the 68000 code to the 6809 code side-by-side, so we'll keep that separate.

So, it's time to look at the string routines?

Really straightforward.

* First 6809 Foothold on the split stack beach,
* by Joel Matthew Rees September 2024, Copyright 2024 -- All rights reserved.
*
* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0
*
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
* We will save S on our own stack.
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; 16 levels of call, max
* 6809 is pre-dec (decrement before store) push
SSTKBAS	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
HELLO	FCB	CR,LF	; Put message at beginning of line
	FCC	"Ashi-ba ga dekita!"	; Whatever the user wants here.
	FCB	CR,LF,NUL	; Put the debugger's output on a new line.
*
*
INISTKS	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X	; 6809 lets us do this -- return address in X
	TFR	S,Y	; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	PSHS	Y	; Save monitor's stack pointer on ours
	JMP	,X	; return via X
*
*
* No need for PPOPD, PPUSHD, PPOPX, or PPUSHX in 6809 code.
*
* We can handle the parameter stack directly.
* Preserves B.
OUTC	LDA	1,U	; Get the character in A where XOUTCH wants it.
	LEAU	2,U	; Got it in A, now drop it from the stack.
	BSR	OUTCV	; output via monitor ROM
	RTS
*
OUTCV	JMP	XOUTCH	; common hook
*
* And we can handle the parameter stack directly here, too.
OUTS	PULU	X	; get the string pointer
OUTSL	LDA	,X+	; get the byte, update the pointer
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	BRA	OUTSL	; next character
OUTDN	RTS
*
* All the newlines we need are in the string.
*
*
START	LBSR	INISTKS
*
	LEAX	HELLO,PCR
	PSHU	X
	LBSR	OUTS	; Our string has its own CR/LF this time around.
*
DONE	LDS	,S	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Note the use of the auto-increment index mode in the string output routine. 

Before we leave, I think I should point out something I briefly mentioned in the 6800 and 6801 code examples. It is quite easy to have OUTS call OUTC, like this:

OUTS	PULU	X	; get the string pointer
OUTSL	LDA	,X+	; get the byte, update the pointer
	BEQ	OUTDN	; if NUL, leave
	PSHU	A	; push the low byte of the character
	CLR	,-U	; push the high byte of zero for OUTC
	BSR	OUTC	; use OUTC itself.
	BRA	OUTSL	; next character
OUTDN	RTS

We can't just CLeaR B and PuSH D on U, because that will be in the wrong byte order. But the predecrement mode CLR works nicely for putting the high byte of zero in the right place.

We do need to be careful to check that the stack remains balanced after the parameter pushing and popping, particular since it can be a little distracting that it's happening in a loop.

You can see that takes a bit of extra code and ends up pushing and popping a little redundantly. 

On the 6800 and 6801, that's going to be a full call and return penalty to let PPUSHD and PPOPD handle it. On the 6809, it's not nearly as much of a penalty, since the 6809 provides the extra stack, PSHU, PULU, and the predecrement addressing modes. But it's still redundant code that takes room in the object and takes cycles to execute.

On the other hand, having OUTS call OUTC allows all character output issues to be taken care of in OUTC.

As a compromise, I'm having both OUTS and OUTC call OUTCV. 

(You might think a branch would work instead of a call, stealing the monitor's RTS, and, indeed, it should work, it often does, and it has been done in existing, functioning code. But there's a certain lack of clarity in doing so, and I have other things I want you to be thinking about.)

Again, lightly tested, should work for you. Play with the code a little while we're here. And think about how to make sure the stacks have stayed in balance.

(A note, while we're looking at it: From early code examples from Motorola and even the CPU's order of saving registers on interrupt, it appears that the original intent in the design of the 6800 might have been that A would be the low byte and B the overflow -- high -- byte, when used together -- and that, for some reason, that intent was inverted in the 6801 and 6809. That may be the reason the monitor looks for the character in A instead of B.)

After you're done playing a bit, let's move on to getting this foothold on the 68000.


(Title Page/Index)


Saturday, August 31, 2024

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

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

(Title Page/Index)

 

The 6801 does not seem to have a lot to add for improving our foothold text output routines on the 6800, but it does offer a little, and what it offers is important. You might not think it enough to warrant a separate chapter, but the last chapter was a bit long and deep, and it will make good review of the 6801. And ...

... as I have said, one major purpose of the split stack is to separate return addresses from parameters and local variables, producing somewhat more robust, less difficult to debug code. 

And, you probably won't believe me yet, but the split stack also simplifies and streamlines the call interface for the subroutines and procedures you write. Promise. It's not quite visible yet, but you shall see it soon.

Maybe not quite yet, but soon. 

The 6801 doesn't really give us anything to improve our 6800 version of OUTC that handles the stack directly:
OUTC	LDX	PSP	; get the parameter stack pointer
	LDAA	1,X	; get the low byte where the EXbug's 7-bit character should be.
	INX		; drop the passed character off the stack
	INX
	STX	PSP	; update the stack pointer
	JSR	XOUTCH	; output A via monitor ROM
	RTS

... unless you want to save X across the use of the routine, of course, but that slightly breaks our promise not to use the return address stack for other purposes:

OUTC	PSHX
	LDX	PSP	; get the parameter stack pointer
	LDAA	1,X	; get the low byte where the EXbug's 7-bit character should be.
	INX		; drop the passed character off the stack
	INX
	STX	PSP	; update the stack pointer
	JSR	XOUTCH	; output A via monitor ROM
	PULX
	RTS

It does, however, give us something for some minor improvements to the parameter push and pop:

PPOPD	LDX	PSP
	LDD	0,X	; We can do both A & B in one instruction now.
	INX
	INX
	STX	PSP
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X	; Double accumulator here, too -- both at once.
	RTS

Here, too, If we wanted to make the push and pop less intrusive to the use of X, we could use the 6801's PSHX and POPX, again slightly breaking our promise not to use the return stack for anything but return addresses:

PPOPD	PSHX
	LDX	PSP
	LDD	0,X	; We can do both A & B in one instruction now.
	INX
	INX
	STX	PSP
	PULX
	RTS
*
PPUSHD	PSHX
	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X	; Double accumulator here, too -- both at once.
	PULX
	RTS

No changes in OUTC using PPUSHD and PPOPD. We'll have it call our slightly improved push and pop, but you can't tell from OUTC.

OUTC	JSR	PPOPD	; get the character in B
	TBA		; put it where XOUTCH wants it.
	JSR	XOUTCH	; output via monitor ROM
	RTS

Calling PPUSHD might not change any:

	CLRA	; 1 byte, 2 cycles
	LDAB	#'H	; 2 bytes, 2 cycles
	JSR	PPUSHD
	JSR	OUTC

Or it might:

	LDD	#'H	; 3-bytes, 3 cycles
	JSR	PPUSHD
	JSR	OUTC

If you've got two monitors, you may want to open up the 6800 code on the other one so you can see what the differences between the code here and the code there are. Even if you have only one monitor, you may want to use separate browser windows.

There are only minor changes in the overall program (here, not saving X across the parameter pushes and pops):
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
	ORG	$2000	; MDOS says this is a good place for user stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.) * * INISTKS LDX #PSTKBAS ; Set up the parameter stack STX PSP PULX ; 6801 lets us do this -- return address in X STS SSAVE ; Save what the monitor gave us. LDS #SSTKBAS ; Move to our own stack JMP 0,X ; return via X * PPOPD LDX PSP LDD 0,X INX INX STX PSP RTS * PPUSHD LDX PSP DEX DEX STX PSP STD 0,X RTS * * OUTC JSR PPOPD ; get the character in B TBA ; put it where XOUTCH wants it. JSR XOUTCH ; output via monitor ROM RTS * * START JSR INISTKS LDD #'H JSR PPUSHD JSR OUTC * DONE LDS SSAVE ; restore the monitor stack pointer NOP NOP ; landing pad

This should run, but, as we discovered in the last set of examples, it's going to be hard to test on EXORsim6801. (It should be testable on, say, XRoar's MC-10 emulation, if you have already figured out how to use that with assembly language. I plan on talking about XRoar later.)

So, let's modularize the terminal initialization and the pause and add them to this code and see what happens with EXORsim6801:

* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
	ORG	$2000	; MDOS says this is a good place for user stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.)
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	PULX		; 6801 lets us do this -- return address in X
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
PPOPD	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS
*
*
OUTC	JSR	PPOPD	; get the character in B
	TBA		; put it where XOUTCH wants it.
	JSR	XOUTCH	; output via monitor ROM
	RTS
*
* Output a newline combination
OUTNEWL	LDD	#CR
	JSR	PPUSHD
	JSR	OUTC
	LDD	#LF
	JSR	PPUSHD
	JSR	OUTC
	RTS
*
* Takes a visible character to print on each line as a parameter.
* Using the simpler run-time model, lots of pushes and pops:
INITRM	LDD	#40	; screen full of lines
	JSR	PPUSHD
INITRL	BSR	OUTNEWL
	LDX	PSP
	DEC	1,X	; the count
	BNE	INITRL
	JSR	PPOPD	; drop the count
	RTS
*
* This is not the best timing LOOP for all purposes.
* Also, timing LOOPs are usually not the correct solution.
* Usually.
PAUSE	LDAA	#5	; adjust this for your workstation.
	CLRB
	LDX	#0
PLOOP	DEX		; 3~
* If not 0 after dec, go back and dec again
	BNE	PLOOP	; 3~, tot 6 : 65536 times => 393216~ 
	DECB		; 2~, tot 393218~
	BNE	PLOOP	; 3~, tot 393221~ : times 256 => 100664576~
* At 1 MHz, that should be about 100 seconds.
* But the emulator seems to be running a bit fast.
* It's about 2 seconds on my workstation.
	DECA		; 100664578~
	BNE	PLOOP	; 100664581~ : times 5 => 503322905
	RTS		; total of about 10 seconds on my WS
*
START	JSR	INISTKS
	JSR	INITRM	; get the terminal's attention
*
	LDD	#'H
	JSR	PPUSHD
	JSR	OUTC
*
	JSR	OUTNEWL	; make a clear line for the breakpoint output
	JSR	PAUSE
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

This has been lightly tested and it works. Give it a try.

You'll note that, just before the pause, I've added a call to the new line routine. Without that, the line buffering for the terminal emulation in the older EXORsim seems to hold onto the output character until it gets swallowed in the the traceback.

You may be thinking that this code should be optimizable by quite a bit. It really is not worth optimizing, in terms of the code, but walking through optimization to show what can be done while keeping it readable and clean is a useful exercise in understanding both the CPU and the optimization processes. Be careful to read the comments.

* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
	ORG	$2000	; MDOS says this is a good place for user stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.)
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	PULX		; 6801 lets us do this -- return address in X
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
PPOPD	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS
*
*
* Let's handle the parameter stack directly:
OUTC	LDX	PSP
	LDAA	1,X	; Get the character in A where XOUTCH wants it.
	INX		; Got it in A, now drop it from the stack.
	INX
	STX	PSP
OUTCRB	JSR	XOUTCH	; output via monitor ROM
	RTS
*
* Output a newline combination
OUTNEWL	LDAA	#CR
	BSR	OUTCRB
	LDAA	#LF
	BRA	OUTCRB	; Rob OUTC's RTS
*
* Takes a visible character to print on each line as a parameter.
* Since we can see OUTCRB does not walk on B,
* we'll put the count in B instead of on the parameter stack.
INITRM	LDAB	#40	; screen full of lines
INITRL	BSR	OUTNEWL
	DECB		; the count
	BNE	INITRL
	RTS
* Note that if the code OUTNEWL or INITRM get separated from OUTC, 
* it will be easy to forget what we are relying on --
* making it easy to introduce bugs down the road.
*
* This is not the best timing LOOP for all purposes.
* Also, timing LOOPs are usually not the correct solution.
* Usually.
PAUSE	LDAA	#5	; adjust this for your workstation.
	CLRB
	LDX	#0
PLOOP	DEX		; 3~
* If not 0 after dec, go back and dec again
	BNE	PLOOP	; 3~, tot 6 : 65536 times => 393216~ 
	DECB		; 2~, tot 393218~
	BNE	PLOOP	; 3~, tot 393221~ : times 256 => 100664576~
* At 1 MHz, that should be about 100 seconds.
* But the emulator seems to be running a bit fast.
* It's about 2 seconds on my workstation.
	DECA		; 100664578~
	BNE	PLOOP	; 100664581~ : times 5 => 503322905
	RTS		; total of about 10 seconds on my WS
*
* Let's keep this part high-level.
START	JSR	INISTKS
	JSR	INITRM	; get the terminal's attention
*
	LDD	#'H
	JSR	PPUSHD
	JSR	OUTC
*
	JSR	OUTNEWL	; make a clear line break for the breakpoint output
	JSR	PAUSE
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

I noted in the comments that when code you're relying on in your optimizations gets moved away from where you are optimizing for it, it's really easy to forget what's going on and add bugs to your code. It's a concept really worth remembering, especially when trying to decide whether you want to optimize or not.

Now, let's turn our attention the the string output routines, and give them a similar treatment. We'll go ahead and put the new-line and pause code in, to avoid spending too much time going over the same ground.

First, let's look at handling the parameter stack via subroutine only:

	OPT	6801
* First 6800 Foothold on the split stack beach,
* by Joel Matthew Rees August 2024, Copyright 2024 -- All rights reserved.
*
* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0
*
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
HELLO	FCB	CR,LF	; Put message at beginning of line
	FCC	"Ashi-ba ga dekita!"	; Whatever the user wants here.
	FCB	CR,LF,NUL	; Put the debugger's output on a new line.
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	PULX		; 6801 lets us do this -- return address in X
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
PPOPD	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
PPOPX	BSR	PPOPD
	PSHB		; keep the order straight
	PSHA		; high byte low in memory
	PULX		; Got X, return address is back on top.
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS
*
PPUSHX	PSHX
	PULA		; keep the order straight!
	PULB
	BRA	PPUSHD	; Rob the return
*
OUTC	JSR	PPOPD	; get the character in B
	TBA		; put it where XOUTCH wants it.
	BSR	OUTCV	; output A via monitor ROM
	RTS
*
OUTCV	JMP	XOUTCH	; common hook
*
OUTS	JSR	PPOPX	; get the string pointer
OUTSL	LDAA	0,X	; get the byte out there
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	INX		; point to the next
	BRA	OUTSL	; next character
OUTDN	RTS
*
*
* Output a newline combination
OUTNEWL	LDD	#CR
	JSR	PPUSHD
	JSR	OUTC
	LDD	#LF
	JSR	PPUSHD
	JSR	OUTC
	RTS
*
* Using the simpler run-time model, lots of pushes and pops:
INITRM	LDD	#40	; screen full of lines
	JSR	PPUSHD
INITRL	BSR	OUTNEWL
	LDX	PSP
	DEC	1,X	; the count
	BNE	INITRL
	JSR	PPOPD	; drop the count
	RTS
*
* This is not the best timing LOOP for all purposes.
* Also, timing LOOPs are usually not the correct solution.
* Usually.
PAUSE	LDAA	#5	; adjust this for your workstation.
	CLRB
	LDX	#0
PLOOP	DEX		; 3~
* If not 0 after dec, go back and dec again
	BNE	PLOOP	; 3~, tot 6 : 65536 times => 393216~ 
	DECB		; 2~, tot 393218~
	BNE	PLOOP	; 3~, tot 393221~ : times 256 => 100664576~
* At 1 MHz, that should be about 100 seconds.
* But the emulator seems to be running a bit fast.
* It's about 2 seconds on my workstation.
	DECA		; 100664578~
	BNE	PLOOP	; 100664581~ : times 5 => 503322905
	RTS		; total of about 10 seconds on my WS
*
*
START	JSR	INISTKS
	JSR	INITRM
*
	LDX	#HELLO	; There are other ways to push the address.
	JSR	PPUSHX
	JSR	OUTS	; Our string has its own CR/LF this time around.
*
	JSR	PAUSE
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Now let's look at handling the stack a little more directly in places where it might make sense to do so:

	OPT	6801
* First 6800 Foothold on the split stack beach,
* by Joel Matthew Rees August 2024, Copyright 2024 -- All rights reserved.
*
* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0
*
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
HELLO	FCB	CR,LF	; Put message at beginning of line
	FCC	"Ashi-ba ga dekita!"	; Whatever the user wants here.
	FCB	CR,LF,NUL	; Put the debugger's output on a new line.
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	PULX		; 6801 lets us do this -- return address in X
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
PPOPD	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
PPOPX	BSR	PPOPD
	PSHB		; keep the order straight
	PSHA		; high byte low in memory
	PULX		; Got X, return address is back on top.
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS
*
PPUSHX	PSHX
	PULA		; keep the order straight!
	PULB
	BRA	PPUSHD	; Rob the return
*
* Let's handle the parameter stack directly:
OUTC	LDX	PSP
	LDAA	1,X	; Get the character in A where XOUTCH wants it.
	INX		; Got it in A, now drop it from the stack.
	INX
	STX	PSP
	BSR	OUTCV	; output via monitor ROM hook
	RTS
*
OUTCV	JMP	XOUTCH	; common hook
*
* Because of the conflict in using X for two purposes,
* Handling the stack directly will use quite a bit of code.
* So we won't do that here.
OUTS	JSR	PPOPX	; get the string pointer
OUTSL	LDAA	0,X	; get the byte out there
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	INX		; point to the next
	BRA	OUTSL	; next character
OUTDN	RTS
*
*
* Output a newline combination
OUTNEWL	LDAA	#CR
	BSR	OUTCV
	LDAA	#LF
	BSR	OUTCV	; Can't rob OUTC's RTS
	RTS		; Should be able to rob XOUTCH's RTS,
* but don't really want to rely on things I can't see nearby.
*
* Since we can see OUTCRB does not walk on B,
* we'll put the count in B instead of on the parameter stack.
INITRM	LDAB	#40	; screen full of lines
INITRL	BSR	OUTNEWL
	DECB		; the count
	BNE	INITRL
	RTS
* Note that if the code OUTNEWL or INITRM get separated from OUTC, 
* it will be easy to forget what we are relying on --
* making it easy to introduce bugs down the road.
*
* This is not the best timing LOOP for all purposes.
* Also, timing LOOPs are usually not the correct solution.
* Usually.
PAUSE	LDAA	#5	; adjust this for your workstation.
	CLRB
	LDX	#0
PLOOP	DEX		; 3~
* If not 0 after dec, go back and dec again
	BNE	PLOOP	; 3~, tot 6 : 65536 times => 393216~ 
	DECB		; 2~, tot 393218~
	BNE	PLOOP	; 3~, tot 393221~ : times 256 => 100664576~
* At 1 MHz, that should be about 100 seconds.
* But the emulator seems to be running a bit fast.
* It's about 2 seconds on my workstation.
	DECA		; 100664578~
	BNE	PLOOP	; 100664581~ : times 5 => 503322905
	RTS		; total of about 10 seconds on my WS
*
*
START	JSR	INISTKS
	JSR	INITRM
*
	LDX	#HELLO	; There are other ways to push the address.
	JSR	PPUSHX
	JSR	OUTS	; Our string has its own CR/LF this time around.
*
	JSR	PAUSE
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Both of these are lightly tested and should work. 

Oddly, I seem to have forgotten to handle the stack more directly in the main routine, from START. Probably too focused on the INITRM and PAUSE routines or something. If you want to give it a try, it would look something like this:

START	JSR	INISTKS
	JSR	INITRM
*
	LDX	#HELLO	; There are other ways to push the address.
	PSHX
	PULA		; watch the order!
	PULB
	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	JSR	OUTS	; Our string has its own CR/LF this time around.
*
	JSR	PAUSE

You may be thinking about how much these "minor" improvements will make the 6801 easier to keep responsive and stable than the 6800 at interrupt time and such. I hope so.

Not that you can't do it on the 6800, but it's trickier, especially when trying to keep it responsive. It's going to be slower in general on the 6800 to make sure it's done right.

I also hope you're thinking about having  OUTS call OUTC instead of the shared OUTCV. We'll talk about that after we've looked out how we do this on the 6809.

And I hope you are thinking about how to check that the stacks have stayed in balance.

I think it's time to see a little bit of what gets people excited about the 6809.


(Title Page/Index)

 

Saturday, August 17, 2024

ALPP 02-01 -- Foothold! (Split Stacks ... barely on the Beach) -- 6800

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

(Title Page/Index)

 

We've had a look at getting characters out on the screen on the EXORciser and Atari ST emulators. Maybe you also took a quick detour through my debugging puzzles. Maybe you've even read my meanderings about studying other processors.

Anyway, we've had a look at using the resources a monitor or a BIOS can provide to help us use a CPU to put out strings of text. But the EXbug monitor ROM and the Atari ST BIOS both use different disciplines than the discipline I promised to show you. So we don't yet really have that foothold on the beach we are attacking.

Now, we could pervert our simulators and go completely raw on them, replacing the monitor, BIOS, DOS, etc. with code that uses that discipline, but we want some cooked code to take with us when we do -- and a lot more experience. 

Yes, at some point we'll do a little bit-twiddling and BLiTting. But first we'll use the character I/O that the monitor and BIOS provide, and prepare some interesting code for when we get there. 

If we limit our use of the existing monitor and BIOS code to the character I/O routines, we make it simpler to move the code to other targets.

To that end, our project for this chapter is to write a bit of glue code for character and text/string terminal I/O on the EXORciser under EXbug, to connect between the split stack discipline I use and the single-stack discipline used in EXbug. Later, we'll extend to using persistent store -- disk I/O. There also, we'll limit our use of code from the other disciplines, to make it easier to port the code we produce to other targets.

Why split stack? 

What is split stack? 

It's going to be easier to show than to explain, but a little hand-waving at the start might help, so I'll give it a try. See (as I wave my hands), it's like this ...

Unless you went spelunking in the EXbug monitor ROM, you may not have noticed it in there. But the PSH (push) and PUL (pop) instructions on the 6800 and 6801 push and pop registers -- on the same stack as the return address. This is the case on most currently commercially successful CPUs. 

Most currently successful CPUs -- the typical run-time models implemented on even the 6809 and 68000, which provide push and pop instructions for multiple stacks, tend to support and use only a single stack.

(I realize this is jumping in a little deep all of a sudden, but we need to get a look at where we are going. It will become clear after a few more topics.)

A single stack is all fine and good, better than none, anyway, but what happens when you forget what you pushed on and try to return to a parameter (in other words, interpreted as an address)? Or when you pop one too many bytes of parameter off and try to return to an address that is part of a parameter and part of something else? 

Yes, things blow up. Or freeze. Or both. Or, worse, silently fail and leave you with hidden corruption in your data. Or, somebody with a far too clever and sneaky bent leaves too much data in your network buffers, overwrites a return address on your stack, and starts remote controlling your computer to do nefarious things like reveal your credit card numbers and send mail in your name to your friends to lure them into traps.

No, that's not being too alarmist, although the split stack is not a complete and perfect cure for all bad coding woes.

What the split stack can do is keep the return address separate from the parameters and local variables, so that if you screw up in popping, pushing, dropping, etc., at least your code eventually returns where it might have been supposed to be instead of somewhere completely irrelevant. It returns in a bad state, yes, but in a somewhat controlled state.

I'm getting too excited about this. Let's look at some ways of doing the glue code:

OUTC	LDX	PSP	; get the parameter stack pointer
	LDAA	1,X	; get the low byte where the EXbug's 7-bit character should be.
	INX		; drop the passed character off the stack
	INX
	STX	PSP	; update the stack pointer
	JSR	XOUTCH	; output A via monitor ROM
	RTS

You were wondering what we were going to use for a parameter stack pointer, weren't you? The 6800 has only one stack, after all, and it is the return address stack I was just getting excited about (not) using.

What we're going to do is implement a software stack. The variable PSP will be its stack pointer, and we can load it into X to use it.

Of course, if we have to maintain it every time we use it, we may find it a bit unwieldy to use. So we can borrow from the Forth playbook and define a couple of routines to do that for us: 

  • Parameter POP Double accumulator and 
  • Parameter PUSH Double accumulator

like this:

PPOPD	LDX	PSP
	LDAA	0,X
	LDAB	1,X
	INX
	INX
	STX	PSP
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STAA	0,X
	STAB	1,X
	RTS

Under the split-stack discipline, parameters are usually passed on the stack. These two routines will be a little different from that, in that they will use the accumulators to pass the value being pushed or popped. 

We can use PPOPD in our new OUTC as follows. It uses a few more cycles, but the OUTC routine will take fewer bytes than the first version I showed above:

OUTC	JSR	PPOPD	; get the character in B
	TBA		; put it where XOUTCH wants it.
	JSR	XOUTCH	; output via monitor ROM
	RTS

And we can pass the character in using PPUSHD as follows;

	CLRA
	LDAB	#'H
	JSR	PPUSHD
	JSR	OUTC

You may be wondering what happens to X. And, if you think about it, A and B.

Under this approach, if you have something important in the index register or the accumulators, you have to save it before calling routines like these. We could use another global variable to save X while we are using for something like this, but we would then need to worry (more) about interrupts and re-entrancy. (Recursion, we can explicitly disallow.) That is, the more such globals we use the more we have to worry about them.

On the 6801, we could save X by using PSHX to push it to the return address stack, which on the one hand brings us back toward those problems I mentioned about mixing return addresses with saved registers, but on the other hand avoids the interrupt-time issues of globally referenced variables. On the 6809 and 68000, we have enough index registers and addressing modes to not have to resort to this kind of game, and we'll look at those when we have got reasonable solutions to this for the 6800 and 6801.

Let's see what this looks like using PPUSHD and PPOPD as above:

* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
	ORG	$2000	; MDOS says this is a good place for user stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
* (But this example only uses two levels.) * * INISTKS LDX #PSTKBAS ; Set up the parameter stack STX PSP TSX ; point to return address LDX 0,X ; return address in X INS ; drop the return pointer on stack INS STS SSAVE ; Save what the monitor gave us. LDS #SSTKBAS ; Move to our own stack JMP 0,X ; return via X * PPOPD LDX PSP LDAA 0,X LDAB 1,X INX INX STX PSP RTS * PPUSHD LDX PSP DEX DEX STX PSP STAA 0,X STAB 1,X RTS * * OUTC JSR PPOPD ; get the character in B TBA ; put it where XOUTCH wants it. JSR XOUTCH ; output via monitor ROM RTS * * START JSR INISTKS CLRA LDAB #'H JSR PPUSHD JSR OUTC * DONE LDS SSAVE ; restore the monitor stack pointer NOP NOP ; landing pad

I've lightly tested it, it should run. Go ahead and give it a try. Check previous chapters if you've forgotten something. 

And remember the (h)elp command if there's something you want to try but don't know how. No promises, but there are things in EXORsim I haven't explained yet that might be helpful.

Now, just for completeness, here's the same thing, but letting the code handle PSP directly, instead of by subroutines, per the first example:

* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	TSX		; point to return address
	LDX	0,X	; return address in X
	INS		; drop the return pointer on stack
	INS
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
OUTC	LDX	PSP	; get the parameter stack pointer
	LDAA	1,X	; get the low byte where the EXbug's 7-bit character should be.
	INX		; drop the passed character off the stack
	INX
	STX	PSP	; update the stack pointer
	JSR	XOUTCH	; output A via monitor ROM
	RTS
*
*
START	JSR	INISTKS
	LDX	PSP
	DEX
	DEX
	STX	PSP
	CLR	0,X
	LDAB	#'H
	STAB	1,X
	JSR	OUTC
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

About the use of the addresses down at $80, that's in the 6800's direct page. (It's called page zero on some other CPUs.) 

The direct page is in the same overall address space as everything else in the 6800, but you can address it in either of two ways when using binary memory-to-register instructions, specifically, either extended addressing or direct page addressing. Direct page addressing is shorter and quicker.

To explain, if we have a 2-byte pointer variable at address $0080, we can load it into X it with either 

FE 00 80            LDX >$0080

or

DE 80               LDX <$0080

Using the right angle brackets (greater-than and less-than) symbols to force extended mode and direct mode addressing only works on some assemblers. It does not work on EXORsim's interactive mode assembler. Don't get hung up on that, just remember that EXORsim's assembler chooses direct page for you when it can.

Let's look closely at the object code for the two different op-codes:

  • $FE is the op-code for extended mode LDX.
    It has a 2-byte address field, so it takes 3 bytes to encode and runs in 5 cycles.
  • $DE is the op-code for direct-page mode addressing LDX.
    It has a 1-byte address field, so it takes 2 bytes to encode and runs in 4 cycles.

So if we keep a virtual stack pointer down there, we can save some bytes and cycles referencing it. 

In other words, with PSP allocated in the direct page, it's just a tad bit faster to load and store, and just a tad byte shorter code, as well. 

Of course, then you have to be careful to make sure that putting PSP down there won't conflict with other stuff down there, kind of like being careful that using U on the 6809 or A6 on the 68000 as a parameter stack pointer won't conflict with the way the OS uses those registers. 

Yeah, there is software that uses variables that will conflict with our variables at $80, but I assume we aren't using those at the same time we're doing this. If so, we can move our variables a bit higher, maybe to $90.

For a quick detour, how about if we did like everyone else and passed the character on the return address stack? PSHB and PSHA are pretty quick.

NO_OUTC	PULA	; Get the high byte out of the way, we think.
	PULA	; get the low byte where the EXbug's 7-bit character should be, we think.
	JSR	XOUTCH	; output A via monitor ROM, we think
	RTS		; we think

Keeeewelll! Why didn't we just do this in the first place?!!?!

(Cough.)

Talk about trying to output the low byte of the return address instead of the character passed, and trying to return to the character that was supposed to be the parameter as if it were an address -- $0048.

Dang. Okay, what about this?

HMMOUTC	PULX	; get the return address out of the way.
	PULA	; Get the high byte out of the way.
	PULA	; get the low byte where the EXbug's 7-bit character should be.
	JSR	XOUTCH	; output A via monitor ROM
	JMP	0,X	; return through X (XOUTCH preserves X, doesn't it?)

That would actually work on the 6801, since the 6801 has a PULX. But the 6800 does not.

AAAARRRRGGGHHHH!!!!!!! NO FAIR!!

Heh. Okay, let's try something that would actually work on the 6800.

OUTC1S	TSX	; Point to the return address on the stack.
	LDAA	3,X	; Skip the return address and high byte.
	LDX	0,X	; get the return address
	INS		; get rid of what we no longer need
	INS		; bump S past the return address
	INS
	INS		; past the character passed in
	JSR	XOUTCH	; output A via monitor ROM
	JMP	0,X	; return through X

Now, unless you have already read up about how TSX works, you should be wondering why the offsets in that are not one off. 

Motorola, in their wisdom, made TSX and TXS to adjust addresses between X and S so that the pre-decrement push nature of the 6800 S is hidden away. Cool, and convenient, but it bites you when you forget and save S to memory and then try to load it to X for some reason.

But you still have to do stuff to maintain the stack if you put parameters on the return stack on the 6800.

Okay, back from the detour. Let's look at getting strings output. I know, I know, there's already a lot to chew on in this chapter, but we're trying to build a beachhead, and we need a foothold. We aren't there yet. 

We want a routine to output a string with some kind of terminator. EXbug used EOT ($04) as a terminator. The Atari ST's DOS calls used a NUL ($00).

The programming language C uses a NUL as a terminator for many of its standard library functions. There are some problems in doing so, but it's an easy function to define. Point to the string, grab characters in sequence and send them to the output device until we grab a NUL.

This requires learning how to repeat sections of code in a loop, and we'll show how to do that with conditional and absolute branches. (We've already had a soft introduction in the workarounds for EXORsim6801.)

The instruction mnemonic BEQ means Branch if EQual to zero after many instructions.

After a CoMPare instruction or a SUBtract instruction, it means Branch if the two operands of the previous compare or subtract were EQual -- in other words, if the difference is zero.

BNE means Branch if Not Equal to zero. Or, after a compare or subtract, Branch if Not Equal.

Mnemonics can be a little dicey.

Some instructions have more or less unfortunate mnemonics. At least, I might wish the engineers had chosen "BR" instead of "BRA" for BRanch Always. Oh, well. Motorola was not the first to use the mnemonic by any means. 

It's best not to get too hung up on linguistic infelicities.

In a sort of pseudo-code, the string output code might be written something like

Point to first character of the string.
Do in a loop:
	Get the character.
	If it is not NUL, 
		output the character.
until you get to a NUL.

Brainstorming --

If we load the string address into A and B and push it on the parameter stack, we know how to access it as a local variable now, don't we? 

No?

Yes. Have a look:

	LDX	#HELLO
	STX	XWORK
	LDAA	XWORK	; there are other ways to do this.
	LDAB	XWORK+1
	JSR	PPUSHD	; local variable to point into the string
	LDX	PSP	; point to the local variable

So we got the address of the string onto the parameter stack, and we grabbed the top of the parameter stack and that points to the local copy of the pointer to the string.

But then what? The moment we use X to point to the string, we've lost our pointer to the local variables. And if we load our pointer to the local variable, we've lost our pointer to the string. 

To get a little better view of what we want to do, let's see if we can handle the parameter stack directly and write a string output routine something like this:

OUTS	LDX	PSP	; get the parameter stack pointer
* Point to first character of the string:
	LDX	0,X	; point to the string
* Do in a loop:
* 	Get the character.
OUTSL	LDAA	0,X	; get the byte that's out there
* 	If it is not NUL, 
	BEQ	OUTDN	; if NUL, leave
* 		output the character.
	JSR	XOUTCH	; Call through EXbug
	INX		; point to the next
* until you get to a NUL.
	BRA	OUTSL	; do the next character
OUTDN	LDX	PSP	; drop pointer from parameter stack
	INX
	INX
	STX	PSP
	RTS

So, to recap, we

  • get the pointer to the top of the parameter stack;
  • get the pointer to the string, leave PSP as it was;
  • (The label OUTSL marks the beginning of the loop.)
  • load accumulator A via the pointer to the string,
    setting the Z flag if it's a NUL;
  • if the Z flag is set,
    branch out of the loop, to label OUTDN;
  • increment the pointer to the string;
  • branch unconditionally to OUTSL,
    the beginning of the loop, to go again.
  • (The label OUTDN marks the first instruction location
    after the loop body.)

And after the loop is complete, at label OUTDN, we

  • get the pointer to the top of the parameter stack back;
  • increment it twice; and
  • update the top of the stack
    to drop the pointer from the stack.

It looks like it should work.

Here's the complete test program, which light testing shows does work:

* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0	; ASCII NUL
*
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
* XWORK must not be used by any routine that calls another routine!
* It must also not be used for values with long duration.
XWORK	RMB	2	; a place to work on X for very short calcualations.
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
HELLO	FCB	CR,LF	; Put message at beginning of line
	FCC	"Ashi-gakari ga dekita!"	; Whatever the user wants here.
	FCB	CR,LF,NUL	; Put the debugger's output on a new line.
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	TSX		; point to return address
	LDX	0,X	; return address in X
	INS		; drop the return pointer on stack
	INS
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
OUTC	LDX	PSP	; get the parameter stack pointer
	LDAA	1,X	; get the low byte where the EXbug's 7-bit character should be.
	INX		; drop the passed character off the stack
	INX
	STX	PSP	; update the stack pointer
	BSR	OUTCV	; output A via monitor ROM
	RTS
*
OUTCV	JMP	XOUTCH	; Centralize the calls into the monitor.
*
OUTS	LDX	PSP	; get the parameter stack pointer
	LDX	0,X	; get the string pointer
OUTSL	LDAA	0,X	; get the byte out there
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	INX		; point to the next
	BRA	OUTSL	; next character
OUTDN	LDX	PSP	; drop pointer from parameter stack
	INX
	INX
	STX	PSP
	RTS
*
*
START	JSR	INISTKS
	LDX	#HELLO	; Other assemblers allow splitting addresses in half.
	STX	XWORK
	LDAA	XWORK
	LDAB	XWORK+1
	LDX	PSP
	DEX
	DEX
	STX	PSP
	STAA	0,X
	STAB	1,X
	JSR	OUTS
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Note that I am centralizing the call into the monitor ROM so that, if you use this code on another platform, you only need to change the address that OUTCV jumps to. 

Also, the FCC directive, Form Constant Character, is used for making strings. Many assemblers will allow strings to be assembled with FCB, but some will require FCC instead.

Now, if we want to cut this up into re-usable subroutines, like PPUSHD and PPOPD, we probably want to define PPUSHX and PPOPX. If we are careful, PPUSHX and PPOPX can re-use PPUSHD and PPOPD.

* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0	; ASCII NUL
*
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
PSP	RMB	2	; parameter stack pointer
SSAVE	RMB	2	; a place to keep S so we can return clean
*
* XWORK must not be used by any routine that calls another routine!
* XWORK must also not be used for values with long duration.
XWORK	RMB	2	; a place to work on X for very short calcualations.
* More accurately, it must not be in use when a routine that uses it 
* calls another routine.
*
* XSTKSV is strictly for PPUSHX and PPOPX
XSTKSV	RMB	2	; a place to keep X for pushing and popping.
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
HELLO	FCB	CR,LF	; Put message at beginning of line
	FCC	"Ashi-gakari ga dekita!"	; Whatever the user wants here.
	FCB	CR,LF,NUL	; Put the debugger's output on a new line.
*
*
INISTKS	LDX	#PSTKBAS	; Set up the parameter stack
	STX	PSP
	TSX		; point to return address
	LDX	0,X	; return address in X
	INS		; drop the return pointer on stack
	INS
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
PPOPD	LDX	PSP
	LDAA	0,X
	LDAB	1,X
	INX
	INX
	STX	PSP
	RTS
*
PPOPX	BSR	PPOPD
	STAA	XSTKSV
	STAB	XSTKSV+1
	LDX	XSTKSV
	RTS
*
PPUSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STAA	0,X
	STAB	1,X
	RTS
*
PPUSHX	STX	XSTKSV
	LDAA	XSTKSV
	LDAB	XSTKSV+1
	BRA	PPUSHD	; rob RTS
*
OUTC	JSR	PPOPD	; get the character in B
	TBA		; put it where XOUTCH wants it.
	BSR	OUTCV	; output A via monitor ROM
	RTS
*
OUTCV	JMP	XOUTCH
*
OUTS	JSR	PPOPX	; get the string pointer
OUTSL	LDAA	0,X	; get the byte out there
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	INX		; point to the next
	BRA	OUTSL	; next character
OUTDN	RTS
*
*
START	JSR	INISTKS
	LDX	#HELLO	; There are other ways to push the address.
	JSR	PPUSHX
	JSR	OUTS
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

How does that work? 

We hide the resource conflicts in the pushes and pops of the glue subroutines.

And, incidentally, here we can see that, when the code is close, we can use branches instead of jumps, including Branch to SubRoutine instead of Jump to SubRoutine.

Branches on the 6800 and 6801 use byte offsets instead of 2-byte absolute addresses, and are limited to a range of -128 to +127 from the address after the offset. This saves a byte and a cycle. 

Also, branch offsets are not absolute addresses, but that is not as important as we might wish it were -- not until the 6809 and 68000 (which needs a chapter or two of its own to explain, way down the road).

It's worth noting here that, if we intend to use the code above to make our own monitor or BIOS, we'll need to consider a number of things we're ignoring here, such as what to do during interrupts -- and how to make sure the stacks have remained in balance.

And, with that, I think we are ready to see how the 6801 improves things just a little.


(Title Page/Index)