Showing posts with label subtraction. Show all posts
Showing posts with label subtraction. Show all posts

Sunday, October 6, 2024

ALPP 03-0X -- Binary (Base Two) Output on the 6800 with Pseudo-code Mixed in

I decided it was a little early to try to teach you to hand-compile code. 

Go here for binary output on the 6800: https://joels-programming-fun.blogspot.com/2024/10/alpp-03-01-binary-output-6800-left-to-right-framework-by-include.html.

Binary (Base Two) Output
on the 6800
with Pseudo-code Mixed in

(Title Page/Index)

 

Okay, I used 16-bit math as an excuse to show you in some detail three ways to pass parameters at run-time, and now we've worked our way through that on the 68000.

It's getting to be tedious, relying on the debugger for seeing what's going on in the program, isn't it?

So, how about we look at ways to get binary numeric output on the terminal screen?

Binary's easy. All you do is look at the bits and spit them out. Something like this, in an abstract pseudo-language that looks a little like C:

void output0(void)
{
    putchar( '0' );
}


void output1(void)
{
    putchar( '1' );
}

void outbinary_8bit( unsigned int value )
{
    unsigned byte count;
    unsigned localvalue = value;

    for ( count = 8; count > 0; --count )
    {
        unsigned int carry = 0x80 & localvalue;
        localvalue <<= 1;
        if ( carry )
        {
            output1();
        }
        else 
        {
            output0();
        }
    }
}

Let's hand-compile that for the 6800:

* void output0(void)
* {
* Output a 0
OUT0	PSHA
	PSHB
*    putchar( '0' );
	LDAA	#'0
	JSR	OUTCH
	PULB
	PULA
	RTS
* }
*
* void output1(void)
* {
* Output a 1 
OUT1	PSHA
	PSHB
*    putchar( '1' );
	LDAA	#'1
	JSR	OUTCH
	PULB
	PULA
	RTS
* }
*
*
* void outbinary_8bit( unsigned int byte )
* {
* Output the 8-bit binary (base two) number on the stack.
* For consistency, we are passing the byte in a 16-bit word.
OUTB8	LDX	PSP
*    unsigned byte count;
*    unsigned localvalue = value;
	LDAB	1,X
*    for ( count = 8; count > 0; --count )
	LDAA	#8	; 8 bits
*    {
*        unsigned int carry = 0x80 & localvalue;
*        localvalue <<= 1;
OUTB8L	LSRB		; Get the leftmost bit.
*        if ( carry )
	BCS	OUTB81
*        {
*            output1();
OUTB80	BSR	OUT1
*        }
	BRA	OUTB8L
*        else 
*        {
*           output0();
OUTB81	BSR	OUT0
*        }
	DECA
	BHI	OUTB8L	; branch if Carry clear and not Zero
*    }
	INX
	INX
	STX	PSP
* }

That's actually a little cluttered, but you can see the correspondence between the pseudo-C and the assembly language code.

You'll need some setup and tear-down code to make it actually work, and a test frame to call it, but that should work. Let's see if it does.

 

 

.


(Title Page/Index)

 

 

 

 

ALPP 02-12 -- On the Beach with Parameters -- 16/32-bit Arithmetic on the 68000

On the Beach with Parameters --
16/32-bit Arithmetic
on the 68000

(Title Page/Index)

 

So. 

Three different 8-bit processors, three different modes each for passing parameters at run-time. And the direct page for statically allocated variables. 

You thought I was showing you how to add and subtract? Well, yeah, that, too.

In all of this, the ancient 6809 still really looks impressive, if it weren't for the apparent simplicity and efficiency of static allocation on that descendant of the 6801 that management wants to compare it with, the 68HC11.

Apparent simplicity and efficiency. Danger! Danger! Will Robinson!

Heh. Okay, that's too dramatic, but the hidden dangers are there and show up when you have a project that suddenly outgrows the single-process-with-a-few-concurrent-subtasks (-threads) model that works reasonably well on the 6801 and even better on the 68HC11. 

And successful projects do grow. You want them to grow, don't you?

But, on the other hand, if you engineer every project for maximum growth, you have dozens of projects that die from over-engineering. So, ... 

Anyway, Motorola never extended the 6809 the way it should have been, and then  Hitachi did some random extensions that they hid (reference the 6309 CPU). 

So the next step up from the 68HC11 became the 68000, and this chapter is about where in the memory map parameters and variables on the 68000 should go.

(Two reasons I haven't been treating the 68HC11 in these tutorials -- 

  • (1) I haven't been able to find a good open source/libre simulator. And 
  • (2) The 68HC11 run-time model is going to be really close to the 6801 model. And 
  • (3) using the Y register well and appropriately requires careful analysis of the target application. In some cases, for instance, it could be an effective parameter stack pointer. In others, you would not want to do that.

What? Was that three reasons, not two? You may be right about that. :)

(The 6309? Similar. 

  • XRoar does the 6309, too, but Ciaran hasn't got single-step debugging in there, and I haven't been able to help him with that. It would take me a month for to get properly into his source code, to feel confident I was doing it right, plus a week or four to get the results properly debugged. 
  • And the run-time model that the 6309 needs would either be identical to the 6809's or just enough different to confuse us. 
  • Using the 6309's extensions wisely is not a topic for tutorials. Some are no-brainers, some are not, and some look like no-brainers but aren't.

Sigh. Ancient industry wars and their fallout. 8-| )

I should, for completeness, show you how absolute addressing on the 68000 consumes a lot of code space (comparatively speaking) for those 32-bit addresses, by doing this twice (not quite what I did for the 6809).  But I won't. It should be obvious that absolute addressing is similar to absolute addressing on the 8-bit CPUs, but at double the address width.

So I'm just going to point out that 32-bit absolute addresses are big and leave it up to you to figure out. (Almost, I'll talk a little more about it when we're done here.)

Well, let's put up a small wall of text here.

Comparing the 68000 to the 6809, the 68000 has pretty much everything the 6809 has, only bigger and more (as if the 68000 were designed and laid out in Texas ;-).

(Pretty much. No memory indirection, and no 8-bit addressing. 16-bit, yes, but, ... oh, there is 8-bit addressing, but you end up using it with a second index, and it's not cheaper than 16-bit.)

If we use the DP as a process-local base pointer, we can just allocate one of the address registers for that. And the offsets will be 16-bit instead of 8-bit. <:-)

So, whatever use we intend for the 6809's DP register, it can be done by a spare address register on the 68000. Sort-of -- but with big offsets. 

More address space is good! -- especially now that memory is cheap. Lots of memory means you can keep a lot more useful stuff in memory.

Except we need to note that the offsets (displacements, Motorola calls them) are signed offsets. Not offset 0 ($0000) to 65535 ($FFFF) from our address register doing DP duty, rather, offset -32768 (-$8000) to +32767 ($7FFF). Sigh.

And, as I parenthesized, the 68000 doesn't do memory indirection (which we haven't used yet). To indirect through a pointer in memory, the 68000 has to load the pointer into an (intermediary) address register (which it conveniently has enough of). It's not fatal, but it's sometimes inconvenient.

But the 6809's DP register doesn't directly support memory indirection, either. So that's a wash relative to the process local static allocation area. 

So the LEA instruction will be available on the 68000 for process local static variables, where it isn't on the 6809's DP ( -- the real reason for my habit of complaining about not having the DP mode duplicated in the 6809's index mode post-byte. :-/).

Whatever address register you replace DP with, you can use the 68000's full range of indexing capabilities on it.

So I'm going to allocate a 68000 address register for use as a local base pointer, for roughly the equivalent of the use I have made of the 6809's DP in the last chapter. Keep that in mind when you compare the code -- similar, but not the same.

Other things to pay attention to -- 

  • MOVEM (MOVE Multiple) is, as you might remember, intended for saving and restoring register sets in a single instruction, like the 6809's PSHU/S and PULU/S. So it doesn't have any effect on the registers, which is very convenient. Differently from the 6809's push and pop instructions, MOVEM can be used without increment or decrementing an address register, which is also convenient. 
  •  But you need to understand that MOVEM.W to a register sign-extends the 16-bit value loaded, even though it doesn't affect the flags.
  • MOVE (but not MOVEM) instructions can proceed memory-to-memory, without passing through a data or address register. 
  • ADDs and SUBtracts cannot operate memory-to-memory, but can operate register-to-memory or even immediate-to-memory, in addition to the usual memory to register. 
  • Be sure you check the number of bytes and kinds of object code produced by the various addressing modes.

With that much said, I think the comments -- along with comparing it to the 6809 code -- are sufficient, so here's the code for the parameter stack version:

	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
***********************************************************************
*
* 16-bit addition and subtraction for 68000 on parameter stack,
* with test code
* Joel Matthew Rees, October 2024
*
NATWID	EQU	4	; 4 bytes in the CPU's natural integer
*
*
	EVEN
LB_ADDR	EQU	*
ENTRY	BRA.W	START
	NOP		; A little buffer zone.
	NOP
A4SAVE	DS.L	1	; a place to keep A4 to A7 so we can return clean
A5SAVE	DS.L	1	; using it as pseudo-DP
A6SAVE	DS.L	1	; using it as PSP
A7SAVE	DS.L	1	; SP
FINAL1	DS.L	1	; 32-bit final result in process-local variable
FINAL2	DS.L	1	; another final result
FINAL3	DS.L	1	; yet another final result
	DS.W	1	; gap
FINAL16	DS.W	1	; 16-bit final result
GAP1	DS.L	54	; gap, make it an even 256 bytes.
*
*
	DS.L	1	; a little bumper space
SSTKLIM	DS.L	16	; 16 levels of call, max
* 			; 68000 is pre-dec (pre-store-decrement) push
SSTKBAS	DS.L	1	; a little bumper space
PSTKLIM	DS.L	32	; roughly 16 levels of call at two parameters per call
PSTKBAS	DS.L	1	; bumper space -- parameter stack is pre-dec
*
*
INISTKS	MOVE.L	(A7)+,A0	; get the return address
	LEA	A4SAVE(PC),A3
	MOVEM.L	A4-A7,(A3)	; Store away what the BIOS gives us.
	LEA	LB_ADDR(PC),A5	; set up our local base (pseudo-DP)
	LEA	SSTKBAS(PC),A7	; set up our return stack
	LEA	PSTKBAS(PC),A6	; set up our parameter stack
	JMP	(A0)		; return via A3
*
*
* PPOP and PPUSH are completely unnecessary, 
* but if we had to have them, here's one way to do it:
*PPOP16	MOVE.W	(A6)+,D7
*	RTS
*
*PPSH16	MOVE.W	D7,-(A6)
*	RTS
*
* Or, of course,
*PPOP16	MOVEM.W	(A6)+,D7	; movem to sign extend it.
*	RTS
*
*PPSH16	MOVEM.W	D7,-(A6)	; movem just because
*	RTS
*
*
* Don't need LD16I.
* If we needed it, it could look like this, but we don't.
*
* You could use it like this:
*	BSR.W	LD16I	; load D7 immediate
*	DC.W	$1234	; "immediate" 16-bit value to load
*	BSR	SOMEWHERE ; or some other executable code.
*
* LD16I	MOVE.L	(A7)+,A0	; point to the instruction stream
*	MOVE.W	(A0),D7	; from instruction stream
*	JMP	2(A0)	; return to the byte after the constant.
*
* But use
*	MOVE.W	#1234,D7	; 16 bits!
* instead.
*
* And if we need to index ROMmed tables or such, 
* we have something much better for that, too:
*
* TABLE	DC.B	SOMETHING
*	...
*	EVEN
*	...
* 	LEA	TABLE(PC),A0
*
*
* We often will not need these, but we'll go ahead and define them:
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	MOVE.W	(A6)+,D7	; right (16-bit only)
	ADD.W	D7,(A6)		; add to left
	RTS			; *** all flags valid!! ***
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	MOVE.W	(A6)+,D7	; right (16-bit only)
	SUB.W	D7,(A6)		; subtract from left
	RTS			; *** all flags valid!! ***
*
* input parameters:
*   32-bit left, right
* output parameter:
*   32-bit sum
ADD32	MOVE.L	(A6)+,D7	; right 
	ADD.L	D7,(A6)		; add to left
	RTS			; *** all flags valid!! ***
*
* input parameters:
*   32-bit left, right
* output parameter:
*   32-bit difference
SUB32	MOVE.L	(A6)+,D7	; right 
	SUB.L	D7,(A6)		; subtract from left
	RTS			; *** all flags valid!! ***
*
* input parameters:
*   16-bit unsigned left, right
* output parameter:
*   32-bit sum
ADD16L	CLR.L	D7
	MOVE.W	2(A6),D7	; left (no sign extension)
	CLR.L	D6
	MOVE.W	(A6),D6		; right (no sign extension)
	ADD.L	D6,D7		; 32-bit sum
	MOVE.L	D7,(A6)		; 32-bit result on stack
	RTS			; *** X, N, Z valid ***
*
* input parameters:
*   16-bit left, right
* output parameter:
*   32-bit signed difference
SUB16L	CLR.L	D7
	MOVE.W	2(A6),D7	; left (no sign extension)
	CLR.L	D6
	MOVE.W	(A6),D6		; right (no sign extension)
	SUB.L	D6,D7		; 32-bit difference
	MOVE.L	D7,(A6)		; 32-bit result on stack
	RTS			; *** X, N, Z valid ***
*
*
* Let's use what we have:
START	BSR.W	INISTKS
*
	MOVE.W	#$1234,-(A6)
	MOVE.W	#$CDEF,-(A6)
	BSR.W	ADD16	; result should be $E023
	MOVE.W	#$8765,-(A6)
	BSR.W	SUB16	; result should be $58BE
	MOVE.W	(A6)+,FINAL16-LB_ADDR(A5)	; store the result
*
*	The 32-bit math and the unsigned 16-bit widened to 32 bit math 
*	are left as exercises.
*
DONE	MOVEM.L	A4SAVE-LB_ADDR(A5),A4-A7	; restore the monitor's A4-A7
	NOP
	NOP		; landing pad

And I am serious about the exercises for the reader, I think. I mean, you should have seen enough to be able to pick the numbers to use for testing and add the code yourself by now. (I hope.) Leave me a note in the comments if you have problems. 

Let's try that disparaged combined stack version now. Again, I think reading the comments and comparing the code with the 6809 code will be sufficient explanation:

	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
***********************************************************************
*
* 16-bit addition and subtraction for 68000 on return stack,
* with test code
* Joel Matthew Rees, October 2024
*
NATWID	EQU	4	; 4 bytes in the CPU's natural integer
*
*
	EVEN
LB_ADDR	EQU	*
ENTRY	BRA.W	START
	NOP		; A little buffer zone.
	NOP
A4SAVE	DS.L	1	; a place to keep A4 to A7 so we can return clean
A5SAVE	DS.L	1	; using it as pseudo-DP
A6SAVE	DS.L	1	; save A6 anyway.
A7SAVE	DS.L	1	; SP
FINAL1	DS.L	1	; 32-bit final result in process-local variable
FINAL2	DS.L	1	; another final result
FINAL3	DS.L	1	; yet another final result
	DS.W	1	; gap
FINAL16	DS.W	1	; 16-bit final result
GAP1	DS.L	54	; gap, make it an even 256 bytes.
*
	DS.L	1	; a little bumper space
SSTKLIM	DS.L	16	; 16 levels of call, max
* 			; 68000 is pre-dec (pre-store-decrement) push
SSTKBAS	DS.L	1	; a little bumper space
*
*
INISTKS	MOVEM.L	(A7)+,A0	; get the return address
	LEA	A4SAVE(PC),A3
	MOVEM.L	A4-A7,(A3)	; Store away what the BIOS gives us.
	LEA	LB_ADDR(PC),A5	; set up our local base (pseudo-DP)
	LEA	SSTKBAS(PC),A7	; set up our return stack
	JMP	(A0)		; return via A3
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	MOVE.L	(A7)+,A0	; Get the return address out of the way
	MOVE.W	(A7)+,D7	; right (16-bit only)
	ADD.W	D7,(A7)		; add to left
	JMP	(A0)		; return, *** all flags valid!! ***
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	MOVE.L	(A7)+,A0	; Get the return address out of the way
	MOVE.W	(A7)+,D7	; right 
	SUB.W	D7,(A7)		; subtract from left
	JMP	(A0)		; return, *** all flags valid!! ***
*
* input parameters:
*   32-bit left, right
* output parameter:
*   32-bit sum
ADD32	MOVE.L	(A7)+,A0	; Get the return address.
	MOVE.L	(A7)+,D7	; right
	ADD.L	D7,(A7)		; add to left
	JMP	(A0)		; return, *** all flags valid!! ***
*
* JFTR, something like this should also work:
* ADD32	MOVE.L	(A7)+,D5/D6/D7
*	MOVE.L	D5,A0
*	ADD.L	D6,D7
*	MOVE.L	D7,-(A7)
*	JMP	(A0)		; return, *** all flags valid!! ***
*
* input parameters:
*   32-bit left, right
* output parameter:
*   32-bit difference
SUB32	MOVE.L	(A7)+,A0	; Get the return address.
	MOVE.L	(A7)+,D7	; right
	SUB.L	D7,(A7)		; subtract from left
	JMP	(A0)		; return, *** all flags valid!! ***
*
* input parameters:
*   16-bit unsigned left, right
* output parameter:
*   32-bit sum
ADD16L	MOVE.L	(A7)+,A0	; Get the return address.
	CLR.L	D7
	MOVE.W	2(A7),D7	; left (no sign extension)
	CLR.L	D6
	MOVE.W	(A7),D6		; right (no sign extension)
	ADD.L	D6,D7		; 32-bit sum
	MOVE.L	D7,(A7)		; 32-bit result on stack
	JMP	(A0)		; return, *** all flags valid!! ***
*
* input parameters:
*   16-bit left, right
* output parameter:
*   32-bit signed difference
SUB16L	MOVE.L	(A7)+,A0	; Get the return address.
	CLR.L	D7
	MOVE.W	2(A7),D7	; left (no sign extension)
	CLR.L	D6
	MOVE.W	(A7),D6		; right (no sign extension)
	SUB.L	D6,D7		; 32-bit difference
	MOVE.L	D7,(A7)		; 32-bit result on stack
	JMP	(A0)		; return, *** all flags valid!! ***
*
*
START	BSR.W	INISTKS
*
	MOVE.W	#$1234,-(A7)
	MOVE.W	#$CDEF,-(A7)
	BSR.W	ADD16	; result should be $E023
	MOVE.W	#$8765,-(A7)
	BSR.W	SUB16	; result should be $58BE
	MOVE.W	(A7)+,FINAL16-LB_ADDR(A5)	; store the result
*
*	The 32-bit math and the unsigned 16-bit widened to 32 bit math 
*	done as exercises in the parameter stack version
* 	should work here, too.
*
DONE	MOVEM.L	A4SAVE-LB_ADDR(A5),A4-A7	; restore the monitor's A4-A7
	NOP
	NOP		; landing pad

No surprises, no revelations. 

But do check that the 32-bit problems you worked out for the split-stack discipline don't break on the combined stack discipline, or, if they do, make sure you can fix them.

Up until now, we haven't really tried to do anything like direct page mode on the 68000, just used absolute/extended mode.

As I mentioned at the top of this chapter, the 68000 does have abbreviated addressing modes. Addresses in the first 64K (cough) ...

That's not right. Let's try that again. 

Addresses within 32K of address zero can use the short absolute form, which takes only 16-bits of address -- 2 bytes of address after the 2 bytes of op-code. 

Yeah, yeah, yeah, that's absolute addresses from -32768 to +32767, or -$8000 to +$7FFF, written in 32 bits as a signed integer, 

$FFFF8000 to $00007FFF

they can be given in short absolute as 

$8000 to $7FFF

But BIOS and TOS use more than 64 K at the bottom of the address space (addresses $0000 to $7FFF).  And addresses at the top of address space aren't implemented in the Atari ST. So the short address absolute mode isn't going to be much direct use to us.

In the 6809, we can move the DP past the range used in Disk I/O and MDOS on the EXORciser/EXORsim, and we did that. Moved it to $2000.

If pick, arbitrarily, A5 for a substitute for the 6809 DP register -- or, more correctly, as a base for the per-process variable space -- we can use short 16-bit constant (signed) offsets to access that space.

If we insist on (signed) 8-bit offsets, we can (again arbitrarily) designate A5+D3 as the base, loading D3 with zero and accessing 0 to 127 (positive) offsets with that address mode, but it still takes a full 16-bits to specify the addressing mode and the offset. (And remembering that 128 to 255 are going to actually be negative offset -128 to -1.)

With full 16-bit offsets, the range

-$8000 to +$7FFF (-32768 to 32767)

from the base address can be accessed. But negative offsets become tricky to work with, so it's probably best to consider it 0 to 32767 except in certain special cases. 32767 is an awful lot of room anyway.

(Motorola calls signed offsets "displacements", to help us, I suppose, remember they are signed.) 

Full 32-bit constant offsets were not available until the 68020 and beyond. If you needed them, you load the offset constant into a data register or a second address register as I mentioned above when talking about 8-bit offsets.

So, different from the 6809 DP in a number of ways, but it does allow us to set up a base for per-process variables.

Here's some code for addition and subtraction using statically allocated parameter variables based off A5 as a near-equivalent to DP in providing a base for per-process statically allocated variable space. 

Concerning my comments on consistency, yeah, it seems kind of ridiculous to bother with offsetting the 32-bit parameter variables by 2 so that the 16-bit parameters go into the low word portion of the variable in RAM, when there won't be any other code that accesses those parameters. But it doesn't cost anything at run-time, and it keeps the source consistent, and the hardest thing about statically-allocated parameters is keeping their use consistent.

It's worth the effort if you have to use statically allocated parameters.

Accessing a variable without being conscious of its size is way up there among ways to blow up your code silently.

	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
***********************************************************************
*
* 16-bit addition and subtraction for 68000 via per-process are
* scratch pad,
* with test code
* Joel Matthew Rees, October 2024
*
NATWID	EQU	4	; 4 bytes in the CPU's natural integer
*
*
	EVEN
LB_ADDR	EQU	*
ENTRY	BRA.W	START
	NOP		; A little buffer zone.
	NOP
A4SAVE	DS.L	1	; a place to keep A4 to A7 so we can return clean
A5SAVE	DS.L	1	; using it as pseudo-DP
A6SAVE	DS.L	1	; save A6 anyway.
A7SAVE	DS.L	1	; SP
FINAL1	DS.L	1	; 32-bit final result in process-local variable
FINAL2	DS.L	1	; another final result
FINAL3	DS.L	1	; yet another final result
	DS.W	1	; gap
FINAL16	DS.W	1	; 16-bit final result
*
* parameter/scratch area for leaf functions only:
* ** When using statically allocated parameters,
* you want to reuse them.
* ** And when reusing statically allocated parameters,
* you absolutely want to use them consistently.
* ** The assembler may not handle implicit offsets 
* like 6809 assemblers handle DP, 
* so you need to calculate the offsets yourself.
NLFT	DS.L	1	; binary operator left side parameter
NRT	DS.L	1	; binary operator right side parameter
NRES	DS.L	1	; unary/binary operator result
NTEMP	DS.L	1	; general scratch register for 
NPAR	EQU	NLFT	; unary operator parameter
NSCRAT	EQU	NLFT	; 
*
GAP1	DS.L	50	; gap, make it an even 256 bytes.
*
	DS.L	1	; a little bumper space
SSTKLIM	DS.L	16	; roughly 16 levels of call, max
*			; 68000 is pre-dec (pre-store-decrement) push
SSTKBAS	DS.L	1	; a little bumper space
*
*
INISTKS	MOVEM.L	(A7)+,A0	; get the return address
	LEA	A4SAVE(PC),A3
	MOVEM.L	A4-A7,(A3)	; Store away what the BIOS gives us.
	LEA	LB_ADDR(PC),A5	; set up our local base (pseudo-DP)
	LEA	SSTKBAS(PC),A7	; set up our return stack
	JMP	(A0)		; return via A3
*
*
* Don't need PPOP and PPSH, but wait 'til we need SCRATCHPUSH!
*
*
* input parameters:
*   16-bit left in low word of NLFT,
*   16-bit right in low word of NRT
* output parameter:
*   17-bit sum in all 32 bits of NRES
ADD16	CLR.L	D7	; for an entirely valid result
	MOVE.W	NLFT+2-LB_ADDR(A5),D7	; low word
	ADD.W	NRT+2-LB_ADDR(A5),D7	; low word
	MOVE.W	D7,NRES+2-LB_ADDR(A5)	; sum
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	CLR.L	D7	; for an entirely valid result
	MOVE.W	NLFT+2-LB_ADDR(A5),D7	; low word
	SUB.W	NRT+2-LB_ADDR(A5),D7	; low word
	MOVE.W	D7,NRES+2-LB_ADDR(A5)	; difference
	RTS
*
*
START	BSR.W	INISTKS
*
	MOVE.W	#$1234,NLFT+2-LB_ADDR(A5)
	MOVE.W	#$CDEF,NRT+2-LB_ADDR(A5)
	BSR.W	ADD16	; result should be $E023
	MOVE.W	NRES+2-LB_ADDR(A5),NLFT+2-LB_ADDR(A5)
	MOVE.W	#$8765,NRT+2-LB_ADDR(A5)
	BSR.W	SUB16	; result should be $58BE
	MOVE.W	NRES+2-LB_ADDR(A5),FINAL16-LB_ADDR(A5)
*
* Repeat, with native instructions:
	MOVE.W	#$1234,D7
	ADD.W	#$CDEF,D7
	SUB.W	#$8765,D7
*
*	The 32-bit math and the unsigned 16-bit widened to 32 bit math 
*	are left as exercises.
*
DONE	MOVEM.L	A4SAVE-LB_ADDR(A5),A4-A7	; restore the monitor's A4-A7
	NOP
	NOP		; landing pad

I think it's time to start looking at getting numeric output -- probably before we look at multiplication and division, even though we'll need multiplication and division for decimal base output. Let's try binary output on the 6800 if you're ready to jump ahead.

Except, we are using the stack enough to start talking about balancing the stack and checking it, things we will need to know to debug our mistakes pretty soon.


(Title Page/Index)


Thursday, October 3, 2024

ALPP 02-11 -- On the Beach with Parameters -- 16-bit Arithmetic on the 6809 with Direct Page Moved

On the Beach with Parameters --
16-bit Arithmetic
on the 6809
with Direct Page Moved

(Title Page/Index)

 

Having worked through three different ways to pass parameters at run-time on the 6809, we remembered that the 6809 has the direct page register. Let's use it, repeating the three ways to pass parameters.

Why?

Because I want to focus on that idea of moving the direct page before doing this all on the 68000.

These are very minor changes to the parameter stack and combined stack versions, but the changes are more significant (if still minor) for the statically allocated parameters (in the direct page) version. When you step through, pay attention to the direct page register, and to the object code when and the actual address accessed when using the direct page mode to access variables in the direct page -- the SSAVE variable (and the new DPSAVE and FINAL variables) and the parameter variables themselves in the "direct page" version.

I've been abbreviating my references, by the way, in a way that I should not have, referring to the statically allocated parameters as direct page parameters or some such. This makes sense on the 6809, and sort-of makes sense on the 6800/6801, but it doesn't map directly to the 68000, and won't map directly to processors without a direct page. 

And we want to think carefully how we map the concept to the 6800/8601.

Understanding what we're doing here will help when we move on to the 68000, and, later, if someone picks up other processors.

Let's look first at the separate parameter stack version, starting with the declarations. Where we declared SSAVE in page zero to this point, we're declaring it out in page $20 now.

	ORG	$2000	; MDOS says this is a good place for usr stuff.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
*
ENTRY	LBRA	START
	NOP		; Just want even addressed pointers for no reason.
SSAVE	RMB	2	; a place to keep S so we can return clean
DPSAVE	RMB	2	; a place to keep DP so we can return clean
FINAL	RMB	2	; Final result in DP variable (to show we can)

The SETDP declarations here should not be necessary for the assembler. I put them here more as comments, to indicate to the human reader that we intend to set the DP to point here. 

And, as I've noted, different assemblers have different semantics for the SETDP declarative. The ones I generally use just take the page number, but EXORsim's assembler wants the whole base address. 

I've added a DPSAVE to save the DP we get from the monitor.

I've also added a FINAL variable to store the final result in, just as a kind of interpretive demonstration.

And that's it. After that, I move up to page $21 to declare the stacks, to show that the stacks don't have to be in the direct page. They can be if there's room, but I don't want anyone thinking they have to be.

	SETDP	0	; Not yet set up
	ORG	$2100	; Give the DP room.
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; 16 levels of call, max
* 			; 6809 is pre-dec (pre-store-decrement) 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

Following the stack declarations is the stack initialization routine, where we fairly carefully get the monitor's DP and put it in Y, then calculate out the page number by relative addressing and move the base address from X to D, where we can access the page number in A and TransFeR it to DP.

And then we SETDP for the duration of the source, until we restore DP at the end.

Once DP is set and declared, I use the direct page variables to save the DP and S that we get from the monitor ROM. When you check the code, you'll see that the addresses are given in short form, as offsets from the base address that DP points to.

INISTKS	TFR	DP,A
	CLRB
	TFR	D,Y		; save old DP base for a moment
	LEAX	ENTRY,PCR	; Set up new DP base
	TFR	X,D
	TFR	A,DP		; Now we can access DP variables correctly.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
	STY	DPSAVE		; technically only need to save high byte
	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X

You might be wondering whether a full 16-bit DP base register might have been more reasonable. I think so, myself. It would have allowed better granularity for locating whatever you put in the direct page. 

I assume that Motorola was planning on the shorter DP using less resources in the CPU and fewer cycles in the DP relative accesses. I'm not sure it worked out that way. DP accesses cost as much as short offset indexed register accesses.

(And you hear me again muttering about the lack of DP mode in the index mode postbyte.)

From there until just before DONE, the rest of the source code is the same, and the effects are in accesses to variables in the direct page, which now access them in page $21 instead of page $00.

Just before the DONE label, I've stored the result in FINAL, and then at DONE I restore the stack pointer and direct page base that the monitor gave us, and that's that.

	LDD	,U++	; load the result into A:B
	STD	FINAL
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	LDD	DPSAVE	; restore the monitor DP
	TFR	A,DP
	SETDP	0	; For lack of a better way to set it.
	NOP
	NOP		; landing pad

Here's the full source for the parameter stack version:

* 16-bit addition and subtraction for 6809 on parameter stack
* using the direct page,
* with test code
* Joel Matthew Rees, October 2024
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$2000	; MDOS says this is a good place for usr stuff.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
*
ENTRY	LBRA	START
	NOP		; Just want even addressed pointers for no reason.
SSAVE	RMB	2	; a place to keep S so we can return clean
DPSAVE	RMB	2	; a place to keep DP so we can return clean
FINAL	RMB	2	; Final result in DP variable (to show we can)
*
*
	SETDP	0	; Not yet set up
	ORG	$2100	; Give the DP room.
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; 16 levels of call, max
* 			; 6809 is pre-dec (pre-store-decrement) 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
*
*
INISTKS	TFR	DP,A
	CLRB
	TFR	D,Y		; save old DP base for a moment
	LEAX	ENTRY,PCR	; Set up new DP base
	TFR	X,D
	TFR	A,DP		; Now we can access DP variables correctly.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
	STY	DPSAVE		; technically only need to save high byte
	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X
*
* PPOP and PPUSH are completely unnecessary, 
* but if we had to have them, here's one way to do it:
*PPOP16	LDD	,U++
*	RTS
*
*PPSH16	STD	,--U
*	RTS
*
* Or, of course,
*PPOP16	PULU	A,B
*	RTS
*
*PPSH16	PSHU	A,B
*	RTS
*
*
* Don't need LD16I.
* If we needed it, it could look like this, but we don't.
*
* You could use it like this:
*	LBSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	BSR	SOMEWHERE ; or some other executable code.
*
* LD16I	PULS	X	; point to the instruction stream
*	LDD	,X	; from instruction stream
*	JMP	2,X	; return to the byte after the constant.
*
* But use
*	LDD	#1234	; 16 bits!
* instead.
*
* And if we need to index ROMmed tables or such, 
* we have something much better for that, too:
*
* TABLE	FCB	SOMETHING
*	...
* 	LEAX	TABLE,PCR
*
*
* We often will not need these, but we'll go ahead and define them:
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	2,U	; left 
	ADDD	,U++	; right
	STD	,U	; sum (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets cleared.
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	2,U	; left
	SUBD	,U++	; right
	STD	,U	; difference (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets cleared.
*
*
* Let's use what we have:
START	LBSR	INISTKS
*
	LDD	#$1234
	PSHU	A,B
	LDD	#$CDEF
	PSHU	A,B
	LBSR	ADD16	; result should be $E023
	LDD	#$8765
	PSHU	A,B
	LBSR	SUB16	; result should be $58BE
	LDD	,U++	; load the result into A:B
	STD	FINAL
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	LDD	DPSAVE	; restore the monitor DP
	TFR	A,DP
	SETDP	0	; For lack of a better way to set it.
	NOP
	NOP		; landing pad

And, basically, the changes are the same, except for one less stack to set up, for the combined stack version that I keep disparaging (so that you understand that I don't think it's the way things should be done):

* 16-bit addition and subtraction for 6809 on return stack
* using the direct page,
* with test code
* Joel Matthew Rees, October 2024
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$2000	; MDOS says this is a good place for usr stuff.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
*
ENTRY	LBRA	START
	NOP		; Just want even addressed pointers for no reason.
SSAVE	RMB	2	; a place to keep S so we can return clean
DPSAVE	RMB	2	; a place to keep DP so we can return clean
FINAL	RMB	2	; Final result in DP variable (to show we can)
*
*
	SETDP	0	; Not yet set up
	ORG	$2100	; Give the DP room.
	RMB	2	; a little bumper space
SSTKLIM	RMB	96	; (64+32) roughly 16 levels of call, max
* 			; 6809 is pre-dec (pre-store-decrement) push
SSTKBAS	RMB	2	; a little bumper space
*
*
INISTK	TFR	DP,A
	CLRB
	TFR	D,Y		; save old DP base for a moment
	LEAX	ENTRY,PCR	; Set up new DP base
	TFR	X,D
	TFR	A,DP		; Now we can access DP variables correctly.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
	STY	DPSAVE		; technically only need to save high byte
	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	PULS	X	; get return address out of the way
	LDD	2,S	; left 
	ADDD	,S++	; right
	STD	,S	; sum (N, Z, & C flags should be correct)
	JMP	,X	; return
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	PULS	X	; get return address out of the way
	LDD	2,S	; left 
	SUBD	,S++	; right
	STD	,S	; sum (N, Z, & C flags should be correct)
	JMP	,X	; return
*
*
START	LBSR	INISTK
*
	LDD	#$1234
	PSHS	A,B
	LDD	#$CDEF
	PSHS	A,B
	LBSR	ADD16	; result should be $E023
	LDD	#$8765
	PSHS	A,B
	LBSR	SUB16	; result should be $58BE
	LDD	,S++	; load the result into A:B
	STD	FINAL
*
DONE	LDS	SSAVE,PCR	; restore the monitor stack pointer
	LDD	DPSAVE	; restore the monitor DP
	TFR	A,DP
	SETDP	0	; For lack of a better way to set it.
	NOP
	NOP		; landing pad

[EDIT JMR202510059924:]

See the edits in the above code from the version of this that does not move the direct page, for the mistake I made while dancing around the return address. The code above is fixed now.

[END EDIT JMR202510059924.]

And the changes really are basically the same for the DP version, where we expect to see the most effect. I've included the statically allocated (scratch) parameter variables in the direct page because that's basically where such parameters should go, in the use of the DP that I am promoting here:

* 16-bit addition and subtraction for 6809 via DP scratch pad
* using the direct page,
* with test code
* Joel Matthew Rees, October 2024
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
* Blank line will end assembly.
	ORG	$2000	; MDOS says this is a good place for usr stuff.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
*
ENTRY	LBRA	START
	NOP		; Just want even addressed pointers for no reason.
SSAVE	RMB	2	; a place to keep S so we can return clean
DPSAVE	RMB	2	; a place to keep DP so we can return clean
FINAL	RMB	2	; Final result in DP variable (to show we can)
* parameter/scratch area for leaf functions only:
NLFT	RMB	2	; binary operator left side parameter
NRT	RMB	2	; binary operator right side parameter
NRES	RMB	2	; unary/binary operator result
NTEMP	RMB	2	; general scratch register for 
NPAR	EQU	NLFT	; unary operator parameter
NSCRAT	EQU	NLFT	; 
*
*
	SETDP	0	; Not yet set up
	ORG	$2100	; Give the DP room.
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; roughly 16 levels of call, max
*			; 6809 is pre-dec (pre-store-decrement) push
SSTKBAS	RMB	2	; a little bumper space
*
*
INISTK	TFR	DP,A
	CLRB
	TFR	D,Y		; save old DP base for a moment
	LEAX	ENTRY,PCR	; Set up new DP base
	TFR	X,D
	TFR	A,DP		; Now we can access DP variables correctly.
*	SETDP	$20	; some other assemblers
	SETDP	$2000	; EXORsim
	STY	DPSAVE		; technically only need to save high byte
	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X
*
*
* Don't need PPOP and PPSH, but wait 'til we need SCRPSH!
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	NLFT
	ADDD	NRT
ADD16S	STD	NRES	; sum
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	NLFT
	SUBD	NRT
	STD	NRES	; difference
	RTS
* Stealing code would only save 1 byte.
*
*
START	LBSR	INISTK
*
	LDD	#$1234
	STD	NLFT
	LDD	#$CDEF
	STD	NRT
	LBSR	ADD16	; result should be $E023
	LDD	NRES
	STD	NLFT
	LDD	#$8765
	STD	NRT
	LBSR	SUB16	; result should be $58BE
	LDD	NRES
	STD	FINAL
*
* Repeat, with native instructions:
	LDD	#$1234
	ADDD	#$CDEF
	SUBD	#$8765
*
DONE	LDS	SSAVE,PCR	; restore the monitor stack pointer
	LDD	DPSAVE	; restore the monitor DP
	TFR	A,DP
	SETDP	0	; For lack of a better way to set it.
	NOP
	NOP		; landing pad

What should go in the direct page? Different people have different ideas.

For my part, the monitor ROM should point DP to where the principle I/O registers are, perhaps, when it is accessing them, and otherwise point it to where the monitor's statically allocated variables are.

Then, every process should point DP to its own statically allocated variables, both global to the process and local to the individual functions of the process. This allows a certain degree of actual separation of process variable spaces.

For the record, if the monitor is able to handle allocation of the direct page and the stacks, the monitor itself should set them up for the processes and the processes should not have to save them. This would provide the greatest separation. 

And now we can begin to see what the point of all my ramblings about stacks and such is -- logical separation of  access to variables by whether they are statically (globally) allocated or dynamically (locally) allocated.

Can we do something like a local static allocation area for the 6800/6801?

Well, if we have a local base (LB?) pointer somewhat analogous to the PSP parameter stack pointer, most likely declared (and allocated) right there with the PSP, we could get such a thing, but, as with the cost of the software stack, it would come at a small cost. We'd have to load it into X every time we need it, wiping out whatever pointer was in X, and thrashing X even more. 

But such a local base pointer would not need the maintenance PSP needs, which means it would not cost as much to use.

Another option would be to have an area in the page zero direct page of the 6800/6801, probably adjacent to the PSP, which the multi-tasking OS or monitor would copy to private space when switching processes.

I'll try to talk about both those options when we have a better opportunity.

So. Why not just use the 6801?

Yeah. If you have a hardware app with a very small number of concurrent processes, the 6801 isn't really a bad option, no worse than the Z-80, maybe a little better.

Let's take a look at all this on the 68000.

(Title Page/Index)

 

 

Wednesday, October 2, 2024

ALPP 02-10 -- On the Beach with Parameters -- 16-bit Arithmetic on the 6809

On the Beach with Parameters --
16-bit Arithmetic
on the 6809

(Title Page/Index)

 

And now we've worked through three different ways to pass parameters at run-time on the 6801.

So what does the 6809 do for us?

The declarations from the 6800/6801 code we borrowed from the improved Hello World examples change in small ways, as does the initialization code.

PSP is now the U register, so we don't need a variable for it. We could actually get rid of everything in the DP, since SSAVE really doesn't need to be in the DP, but we'll keep it this way to be consistent.

The JMP at NOENTRY can be exchanged for a long branch, and I like that better. It allows us to make the code from NOENTRY up relocatable without load-time patching. So I'm going ahead and doing it. 

The return stack is now pre-decrement push, so the declarations for it change from the 6800/6801 code.

The initialization code really doesn't change, even though I am now using Load Effective Address instructions in PC-relative mode, which keeps the initialization code relocatable without patch-up. 

Push and pop on both the U stack (which we are using for parameters) and the S stack (the return address stack) are part of the native instruction set and fully encode in two bytes, so using PPUSH and PPOP routines would actually be de-optimizing in both terms of code size and cycle counts. We do want to note that load and store instructions (LDD/STD) affect the flags, where the push and pop instructions (PSHU/S and PULU/S) do not.

	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
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	LBRA	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; 16 levels of call, max
* 			; 6809 is pre-dec (pre-store-decrement) 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
*
*
INISTKS	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X
*
* PPOP and PPUSH are completely unnecessary, 
* but if we had to have them, here's one way to do it:
*PPOP16	LDD	,U++
*	RTS
*
*PPSH16	STD	,--U
*	RTS
*
* Or, of course,
*PPOP16	PULU	A,B
*	RTS
*
*PPSH16	PSHU	A,B
*	RTS

Since the 6809, like the 6801, has LDD, we don't need a LD16I instruction, Huzzah!

We can do similar things if necessary

* Don't need LD16I.
* If we needed it, it could look like this, but we don't.
*
* You could use it like this:
*	LBSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	BSR	SOMEWHERE ; or some other executable code.
*
* LD16I	PULS	X	; point to instruction stream
*	LDD	,X	; from instruction stream
*	JMP	2,X	; return to the byte after the constant.
*
* But use
*	LDD	#1234	; 16 bits!
* instead.
*
* And if we need to index ROMmed tables or such, 
* we have something much better for that, too:
*
* TABLE	FCB	SOMETHING
*	...
* 	LEAX	TABLE,PCR

When we need to load addresses to work on them, we can now use the LEA instructions instead of loading the address as an immediate into D.

Cool stuff, huh?

And, if we refer back to Wozniak's Sweet 16 virtual machine, we find that the 6809 instruction set and addressing modes basically implement everything that Sweet 16 gave the 6502 (and more), as native, full speed instructions, with compact encodings.

Is that exciting? Or does it get boring? 

Boring can be good, sometimes.

Well, one caveat. Motorola did not include DP-relative in the index mode post-byte, so indirecting through direct-page pointers requires loading the pointer into an index register. And getting the effective address for variables in the direct page requires just a little computation:

* Indirecting through DP variables --
* instead of
*	LDD	[<DP_PTR]
* use an intermediate index register
	LDX	<DP_PTR
	LDD	,X
*
* Loading effective address of DP variables --
* instead of 
* 	LEAX	<DP_VAR
* calculate it something like
	TFR	DP,A
	LDB	#DP_VAR-DP_BASE
	TFR	D,X

Bummer! Right?

Okay, the world is not our perfect oyster yet. We're not taking a huge hit, we can deal with it.

How do the addition and subtraction subroutines fare?

Oh, wow!

* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	2,U	; left 
	ADDD	,U++	; right
	STD	,U	; sum (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets cleared.
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	2,U	; left
	SUBD	,U++	; right
	STD	,U	; difference (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets cleared.

Stack maintenance basically disappears into the meat of the function. In fact, we look at that and wonder if we really need to call those routines any more. No more than six bytes to in-line them, as compared to three bytes to call them.

Sometimes we won't bother calling them.

AND THERE's MORE in those comments!

Again, even without the  

	TFR	CC,A

which the 6809 replaces TPA with, and without any bit twiddling or even much care about code ordering, the Zero, Negative, and Carry flags are right there for the caller to use. oVerflow still gets cleared. If we need it, we'll probably just use the instructions in-line.

Okay, putting the test frame for the 6809 together, with comments on what went away:

* 16-bit addition and subtraction for 6809 on parameter stack,
* with test code
* Joel Matthew Rees, October 2024
*
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.
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	LBRA	START
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; 16 levels of call, max
* 			; 6809 is pre-dec (pre-store-decrement) 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
*
*
INISTKS	LEAU	PSTKBAS,PCR	; Set up the parameter stack
	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X
*
* PPOP and PPUSH are completely unnecessary, 
* but if we had to have them, here's one way to do it:
*PPOP16	LDD	,U++
*	RTS
*
*PPSH16	STD	,--U
*	RTS
*
* Or, of course,
*PPOP16	PULU	A,B
*	RTS
*
*PPSH16	PSHU	A,B
*	RTS
*
*
* Don't need LD16I.
* If we needed it, it could look like this, but we don't.
*
* You could use it like this:
*	LBSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	BSR	SOMEWHERE ; or some other executable code.
*
* LD16I	PULS	X	; point to the instruction stream
*	LDD	,X	; from instruction stream
*	JMP	2,X	; return to the byte after the constant.
*
* But use
*	LDD	#1234	; 16 bits!
* instead.
*
* And if we need to index ROMmed tables or such, 
* we have something much better for that, too:
*
* TABLE	FCB	SOMETHING
*	...
* 	LEAX	TABLE,PCR
*
*
* We often will not need these, but we'll go ahead and define them:
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	2,U	; left 
	ADDD	,U++	; right
	STD	,U	; sum (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets cleared.
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	2,U	; left
	SUBD	,U++	; right
	STD	,U	; difference (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets cleared.
*
*
* Let's use what we have:
START	LBSR	INISTKS
*
	LDD	#$1234
	PSHU	A,B
	LDD	#$CDEF
	PSHU	A,B
	LBSR	ADD16	; result should be $E023
	LDD	#$8765
	PSHU	A,B
	LBSR	SUB16	; result should be $58BE
	LDD	,U++	; load the result into A:B
*
DONE	LDS	SSAVE,PCR	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

You know the drill. Step through it, try other constants. Convince yourself that you'd rather use the 6809 than even the 6801, when you're trying to get work done.

(Why didn't Motorola release the 6809 as an SOC core like it did the 6801? ブツブツブツ)

And now we're going to see some revelations about the single interleaved stack discipline I keep disparaging:

* 16-bit addition and subtraction for 6809 on return stack,
* with test code
* Joel Matthew Rees, October 2024
*
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.
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	LBRA	START
	NOP		; bump to aligned
	RMB	2	; a little bumper space
SSTKLIM	RMB	96	; (64+32) roughly 16 levels of call, max
* 			; 6809 is pre-dec (pre-store-decrement) push
SSTKBAS	RMB	2	; a little bumper space
*
*
INISTKS	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X	; return via X
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	PULS	X	; get return address out of the way
	LDD	2,S	; left 
	ADDD	,S++	; right
	STD	,S	; sum (N, Z, & C flags should be correct)
	JMP	,X	; return
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	PULS	X	; get return address out of the way
	LDD	2,S	; left 
	SUBD	,S++	; right
	STD	,S	; difference (N, Z, & C flags should be correct)
	JMP	,X	; return
*
*
START	LBSR	INISTKS
*
	LDD	#$1234
	PSHS	A,B
	LDD	#$CDEF
	PSHS	A,B
	LBSR	ADD16	; result should be $E023
	LDD	#$8765
	PSHS	A,B
	LBSR	SUB16	; result should be $58BE
	LDD	,S++	; load the result into A:B
*
DONE	LDS	SSAVE,PCR	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

You're looking at me and saying,

What revelations?????? That looks almost identical to the code for the split stack!!

Well, that should be a revelation. On the 6809, the only cost for using a separate parameter stack is the cost of declaring the stack space and initializing it, and then we don't have to fuss with the return address in the middle of our parameters any more.

In this example we don't really see how much we gain, but at least we can see that there's no real cost -- on a processor like the 6809.

No real cost except the allocation, and so many engineers have thought the allocation was the biggest hurdle. It seems to be a losing battle, doesn't it. Let's soldier on.

[EDIT JMR202410042358:]

Almost identical, indeed.

Case in point of how easy it is to mess up your code when you are dancing around the return address to get to your parameters and local variables.

While working on the equivalent code to the above for the 68000, I realized that I had failed to de-allocate the stack before or on return from the ADD16 and SUB16 routines here. Here's what I had written:

* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	4,S	; left 
	ADDD	2,S	; right
	STD	2,S	; sum (N, Z, & C flags should be correct)
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	4,S	; left 
	SUBD	2,S	; right
	STD	2,S	; sum (N, Z, & C flags should be correct)
	RTS

I had the offsets correct, you see? No problem there. Or, I thought so. I had successfully avoided overwriting the return address, but now the result was out of place and in the way, and the stack had one of the input parameters still live on it after the return. This is a good way to overflow the stack and in various ways screw up the calculations.

But so many engineers think that they won't do this. Or, rather, that they can write their compilers to keep them from doing it. 

And it would be nice if you would believe me for this, but I'm sure I'm going to have to present stronger evidence than my mistakes to really convince you.

[END EDIT JMR202410042358.]

How is the scratch area in DP version going to look?

* 16-bit addition and subtraction for 6809 via DP scratch pad,
* with test code
* Joel Matthew Rees, October 2024
*
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.
	SETDP	0
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
SSAVE	RMB	2	; a place to keep S so we can return clean
* parameter/scratch area for leaf functions only:
NLFT	RMB	2	; binary operator left side parameter
NRT	RMB	2	; binary operator right side parameter
NRES	RMB	2	; unary/binary operator result
NTEMP	RMB	2	; general scratch register for 
NPAR	EQU	NLFT	; unary operator parameter
NSCRAT	EQU	NLFT	; 
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	LBRA	START
	NOP		; bump to aligned
	RMB	2	; a little bumper space
SSTKLIM	RMB	32	; roughly 16 levels of call, max
*			; 6809 is pre-dec (pre-store-decrement) push
SSTKBAS	RMB	2	; a little bumper space
*
*
INISTKS	PULS	X		; get return address
	STS	SSAVE		; Save what the monitor gave us.
	LEAS	SSTKBAS,PCR	; Move to our own stack
	JMP	,X		; return via X
*
*
* Don't need PPOP and PPSH, but wait 'til we need SCRPSH!
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	NLFT
	ADDD	NRT
ADD16S	STD	NRES	; sum
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	NLFT
	SUBD	NRT
	STD	NRES	; difference
	RTS
* Stealing code would only save 1 byte.
*
*
START	LBSR	INISTKS
*
	LDD	#$1234
	STD	NLFT
	LDD	#$CDEF
	STD	NRT
	LBSR	ADD16	; result should be $E023
	LDD	NRES
	STD	NLFT
	LDD	#$8765
	STD	NRT
	LBSR	SUB16	; result should be $58BE
	LDD	NRES
*
* Repeat, with native instructions:
	LDD	#$1234
	ADDD	#$CDEF
	SUBD	#$8765
*
DONE	LDS	SSAVE,PCR	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Now, if it weren't for the LBSR calls instead of the JSR calls, that would look just like the 6801 code! (Almost.) Why do we even need any stack at all?

Yeah! Why not just write

	LDD	#$1234
	ADDD	#$CDEF
	SUBD	#$8765

??

Why not just use the 6801?

Patience. We will get there. 

You know, I could have shown extended mode addressing vs. direct-page mode on each of these processors. That would be four modes, which would have been maybe too many. 

And the only difference between the absolute/extended mode and direct page mode for the 6800 and 6801 would have been the number of bytes for addresses for the parameter stack pointer and scratch registers.

There's another difference on the 6809, however. The DP register lets us move the direct page away from page zero. But ... really, for this example, that would not have been meaningful. We could have deliberately moved DP, but unless you were watching really closely as you stepped through, you might not have noticed. 

If the concept intrigues you, give it a try. The SETDP directive will be useful.

Some assemblers expect the SETDP to be given just the high byte of the base address, but the EXORsim assembler expects the whole base address (and warns if it is not on an even 256-byte boundary).

I will show how to use DP later.

I changed my mind. I know you wanted to explore it yourself. You can, of course. 

But I'm going ahead and showing you how to use DP before we move on to the 68000. There are concepts there I want to reference when I show you the 68000 code.

 

(Title Page/Index)

 

 

ALPP 02-09 -- On the Beach with Parameters -- 16-bit Arithmetic on the 6801

On the Beach with Parameters --
16-bit Arithmetic
on the 6801

(Title Page/Index)

 

So we've worked through three different ways to pass parameters at run-time on the 6800.

Now let's see how the 6801 extensions to the 6800 come into play with all of that.

The declarations from code we borrowed from the improved Hello World examples don't really change compared with the 6800 code, but the stack initialization and pushes and pops get some improvements from PULX and LDD/STD:
	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
	PULX		; get return address
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
PPOP16	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
PPSH16	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS

What about LD16I?

We now have the LDD instruction to explicitly load immediate values to the A:B pair like this:

VALUE	EQU	$1234
	...
	LDD	#VALUE

Of course, we can even load address to the A:B pair like this

BUFFER	RMB	80	; text buffer
	...
	LDD	#BUFFER

So we don't need LD16I at all! Hoorah, hoorah! 

If we needed it, it would be much cleaner to write, but we don't!

* Don't need LD16I.
* If we needed it, it would look like this, but we don't.
*
* You could use it like this:
*	JSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	JSR	SOMEWHERE ; or some other executable code.
*
* LD16I	PULX		; point to the instruction stream
*	LDD	0,X	; from instruction stream
*	JMP	2,X	; return to the byte after the constant.
*
* But use
*	LDD	#1234	; 16 bits!
* instead.

What for are you looking at me strange like that again? 

(cough)

Actually, remembering this little bit of syntactic sugar may come in handy down the road, for such things as pointing to tables of constants kept in the code itself.

And that's part of the rest of the story on that little snippet. We look forward to using it.

Anyway, referring back to Wozniak's Sweet 16 virtual machine, we find that key elements of Sweet 16's 16-bit functionality are present in the 6801's native instruction set, and what remains is dead simple to implement. Combined with the 6801's new direct page mode for JSR, we could even make a really nifty and clean 16-bit relative BRanch Always. More fun than a barrel of monkeys. Later.

How can we improve our addition and subtraction subroutines?

* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDX	PSP
	LDD	2,X	; left 
	ADDD	0,X	; right
	INX		; adjust parameter stack first
	INX
	STX	PSP
	STD	0,X	; sum (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets walked on.
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDX	PSP
	LDD	2,X	; left
	SUBD	0,X	; right
	INX		; adjust parameter stack first
	INX
	STX	PSP
	STD	0,X	; difference (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets walked on.

That speeds things up a bit, but, surprisingly, what sticks out most is that maintaining the software stack now well outweighs the meat of the function.

Bummer.

On the other hand, we will often find ourselves directly using the new 16-bit wide ADDD and SUBD instructions instead of calling these routines.

BUT THERE's MORE!

Notice those comments. Careful organization of the  code allows us to keep the Zero, Negative, and Carry flags for the caller to use. oVerflow gets walked on. If we need it, we could preserve it with some TPA and bit twiddling and TAP, like we did in the 6800 code, but, really, we'd just use the ADDD and SUBD instructions directly if we need the oVerflow flag.

(Or, really, any of the flags, but, please be patient with this. There is a madness to my methods. Or something.)

So, here's the complete test frame for software parameter stack on the 6801:

* 16-bit addition and subtraction for 6801 on parameter stack,
* with test code
* Joel Matthew Rees, October 2024
*
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
	PULX		; get return address
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
PPOP16	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
PPSH16	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS
*
* Don't need LD16I.
* If we needed it, it would look like this, but we don't.
*
* You could use it like this:
*	JSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	JSR	SOMEWHERE ; or some other executable code.
*
* LD16I	PULX		; point to the instruction stream
*	LDD	0,X	; from instruction stream
*	JMP	2,X	; return to the byte after the constant.
*
* But use
*	LDD	#1234	; 16 bits!
* instead.
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDX	PSP
	LDD	2,X	; left 
	ADDD	0,X	; right
	INX		; adjust parameter stack first
	INX
	STX	PSP
	STD	0,X	; sum (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets walked on.
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDX	PSP
	LDD	2,X	; left
	SUBD	0,X	; right
	INX		; adjust parameter stack first
	INX
	STX	PSP
	STD	0,X	; difference (N, Z, & C flags should be correct)
	RTS
* Flags: Specifically,
*        N and Z get set correctly by the final store double;
*        C should make it through manipulating X and storing D.
*        V gets walked on.
*
*
START	JSR	INISTKS
*
	LDD	#$1234
	JSR	PPSH16
	LDD	#$CDEF
	JSR	PPSH16
	JSR	ADD16	; result should be $E023
	LDD	#$8765
	JSR	PPSH16
	JSR	SUB16	; result should be $58BE
	LDX	PSP
	LDD	0,X	; load the result into A:B
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Make sure you've copied everything correctly, step through it, try other constants. Convince yourself that you'd rather use the 6801 than the 6800.

(Why didn't Motorola release the 6801 core in a package that could be dropped into a socket for the 6800? Yeah, yeah, I was the unpaying customer with great demands.)

And let's see how it might look with the single interleaved stack discipline I keep disparaging:

* 16-bit addition and subtraction for 6801 on return stack,
* with test code
* Joel Matthew Rees, October 2024
*
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.
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
	NOP		; bump to aligned
	RMB	2	; a little bumper space
SSTKLIM	RMB	95	; (64+31) roughly 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
*
*
INISTKS	PULX		; Get return address.
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	TSX
	LDD	4,X	; left
	ADDD	2,X	; right
ADD16S	STD	4,X	; sum
	LDX	0,X	; return address before we deallocate it
	INS		; drop return address
	INS
	INS		; drop right-hand addend
	INS
	JMP	0,X	; return
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	TSX
	LDD	4,X	; left
	SUBD	2,X	; right
	BRA	ADD16S	; Steal code.
* Could steal code this way in the parameter stack example, as well.
*
*
START	JSR	INISTKS
*
	LDD	#$1234
	PSHB		; push in correct order
	PSHA
	LDD	#$CDEF
	PSHB
	PSHA
	JSR	ADD16	; result should be $E023
	LDD	#$8765
	PSHB
	PSHA
	JSR	SUB16	; result should be $58BE
	PULA
	PULB
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Again, being able to use the native push and pop instructions seems to clean up the code significantly.

But we are still playing dodgy games avoiding the return address, and those games will still tend to keep you too amused late at night.

 And lets try it using a scratch area in the DP to pass values in and out:

* 16-bit addition and subtraction for 6801 via scratch pad,
* with test code
* Joel Matthew Rees, October 2024
*
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.
SSAVE	RMB	2	; a place to keep S so we can return clean
* parameter/scratch area for leaf functions only:
NLFT	RMB	2	; binary operator left side parameter
NRT	RMB	2	; binary operator right side parameter
NRES	RMB	2	; unary/binary operator result
NTEMP	RMB	2	; general scratch register for 
NPAR	EQU	NLFT	; unary operator parameter
NSCRAT	EQU	NLFT	; 
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	NOP		; bump to aligned
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; roughly 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
*
*
INISTKS	PULX		; get return address
	STS	SSAVE	; Save what the monitor gave us.
	LDS	#SSTKBAS	; Move to our own stack
	JMP	0,X	; return via X
*
*
* Don't need PPOP and PPSH
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDD	NLFT
	ADDD	NRT
ADD16S	STD	NRES	; sum
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDD	NLFT
	SUBD	NRT
	STD	NRES	; difference
	RTS
* Stealing code would only save 1 byte.
*
*
START	JSR	INISTKS
*
	LDD	#$1234
	STD	NLFT
	LDD	#$CDEF
	STD	NRT
	JSR	ADD16	; result should be $E023
	LDD	NRES
	STD	NLFT
	LDD	#$8765
	STD	NRT
	JSR	SUB16	; result should be $58BE
	LDD	NRES
*
* Repeat, with native instructions:
	LDD	#$1234
	ADDD	#$CDEF
	SUBD	#$8765
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

Dramatic?

But still, all of that? Just to write the equivalent of

	LDD	#$1234
	ADDD	#$CDEF
	SUBD	#$8765

??

Yeah, I jest. Again, there are things you cannot reduce to constants at design- or compile-time.

But, even though it appears dramatic, you might be able to see a trend here. Let's see how that trend continues on the 6809.


(Title Page/Index)

 

 

Sunday, September 29, 2024

ALPP 02-08 -- On the Beach with Parameters -- 16-bit Arithmetic on the 6800

On the Beach with Parameters --
16-bit Arithmetic
on the 6800

(Title Page/Index)

 

So we pretty much snuck the meat of the 16-bit arithmetic in already didn't we? We were passing byte parameters in and widening them in the last note and the previous two chapters, but we were doing 16-bit math.

Parameters. Oh, yeah. Those.

I wrote a couple of walls of text about parameters, and then decided I should show you code instead, or at least first. (Yes. Again.)

Let's define some library-style functions to add and subtract on the 6800, using the split stack parameter passing paradigm I keep talking about. Then I can philosophize a wall of text and maybe not put everyone to sleep. 

We'll borrow this code from the improved Hello World examples, to declare the stack pointers and set the stacks up, and to push and pop both accumulators:

	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
*
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

We want to be able to load immediate values to the A:B pair. Some assemblers would allow us to load them something like this:

VALUE	EQU	$1234
	...
	LDAA	#VALUE/256
	LDAB	#VALUE-VALUE/256

Some will even allow loading an address like this

BUFFER	RMB	80	; text buffer
	...
	LDAA	#BUFFER/256
	LDAB	#BUFFER-VALUE/256

But the one we are presently using in EXORsim will not do either. -- at this time.

Even assemblers that allow the former may not allow the latter, under the assumption that addresses should never be divided or multiplied in legitimate code. Treating addresses like integers has traditionally been considered evidence of operator error on the programmer's part, and many assemblers will complain if you do.

We could go looking for an assembler that will do what we want, but for now we want a workaround. (And some people think the following run-time "syntactic sugar" makes code more "readable", anyway.)

*
* Load a constant from the instruction stream into A:B, 
* continue execution after the constant.
* This is not self-modifying code, even though it feels like a trick
* and is playing with the return stack and instruction stream 
* in ways we wouldn't think we wanted to think we should.
* Call it a "necessary" bit of run-time syntactic sugar.
*
* Use it like this:
*	JSR	LD16I	; load A:B immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	JSR	SOMEWHERE ; or some other executable code.
*
LD16I	TSX		; point to top of return address stack
	LDX	0,X	; point into the instruction stream
	LDAA	0,X	; high byte from instruction stream
	LDAB	1,X	; low byte from instruction stream
	INS		; drop the return address we don't need
	INS
	JMP	2,X	; return to the byte after the constant.

What are you looking at me like that for? Yeah, this little bit of code to enable some syntactic sugar looks really strange when the concept of a return address is still fuzzy in your mind. And it seems so unnecessary. It takes space to define, the call takes as much space in code as the pair of immediate loads it replaces. WHY????

Well, if you study virtual machines like, for instance, the fig Forth run-time (the code for LIT), or Steve Wozniak's Sweet 16 VM that supplied 16-bit routines for some Apple II software, you recognize what it's doing. If you have a VM, it can be a way to save some bytes of object code, but what we're really trading is management time for runtime.

At a cost of a few cycles of (your) runtime, I can avoid the trouble of chasing done the bug in EXORsim, getting Joe H. Allen's attention, potentially discussing whether addresses should be allowed to have division done on them, etc., or, in the alternative, fixing it myself and forking the code like I did for the odd-ball EXORsim6801.

And you thought optimization was simple code size vs. speed. :)

Now we will define our addition and subtraction subroutines:

* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDX	PSP
	LDAB	3,X	; left low
	LDAA	2,X	; left high
	ADDB	1,X	; right low
	ADCA	0,X	; right high, with carry
	STAB	3,X	; sum low
	STAA	2,X	; sum high
	INX		; adjust parameter stack
	INX
	STX	PSP
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDX	PSP
	LDAB	3,X	; left low
	LDAA	2,X	; left high
	SUBB	1,X	; right low
	SBCA	0,X	; right high, with borrow
	STAB	3,X	; difference low
	STAA	2,X	; difference high
	INX		; adjust parameter stack
	INX
	STX	PSP
	RTS

If you're wondering whether the processor flags are correct after all that, only the Carry flag makes it through the stack pointer update unscathed. Moreover, if you're watching, you should notice that the Zero flag does not show whether the entire 16 bits of the result are zero, only one byte at a time, the high byte last here.

We can sort of fix the flags, something like this (untested):

SUB16F	LDX	PSP
	LDAB	3,X	; left low
	LDAA	2,X	; left high
	SUBB	1,X	; right low
	SBCA	0,X	; right high, with borrow
	STAB	3,X	; difference low
	STAA	2,X	; difference high
* In this version, we will set the flags almost as if it were SUBD:
	TPA		; save the flags
	ANDA	#$FB	; clear the Z flag in the copy
	STAA	0,X	; re-use this byte to save the copied flags
	ORAB	2,X	; OR low byte with high to set the correct Z flag
	TPA
	ANDA	#$04	; clear all but Z
	ORAA	0,X	; combine corrected Z with copied flags
	PSHA		; which is worse? return stack or DP?
	INX		; adjust parameter stack before restoring the flags
	INX
	STX	PSP
	PULA		; get the flags back
	TAP		; replace the flags
	RTS

Wow, that's a lot of code! And we would want to test it thoroughly before using it for anything important. (It should work, but ...)

Pay particular attention to the order things are done: 

  1. We save the best copy of the flags.
  2. Before we update the stack pointer, we borrow some of the stack space that is no longer in use to calculate what the flags should have been.
  3. Then we save the corrected flags to the safest place we can think of. 
  4. Before updating the CPU flags, we update the stack pointer, so that updating the stack pointer will not thrash the flags we just calculated.
  5. Then we get the flags back and restore them in the CPU.
  6. RTS does not affect the flags. (This is a deliberate design decision by the CPU architects.)

So you can see how it could be done -- but we usually don't need all the flags corrected. (And, in fact, we didn't clear the Half-carry flag!) 

I'll show some alternate approaches later.

Let's put this all together with some test code. You'll want to pay close attention to what happens in the CPU when it executes each of the new routines, but especially LD16I.

* 16-bit addition and subtraction for 6800 on parameter stack, with test code
* Joel Matthew Rees, October 2024
*
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
*
PPOP16	LDX	PSP
	LDAA	0,X
	LDAB	1,X
	INX
	INX
	STX	PSP
	RTS
*
PPSH16	LDX	PSP
	DEX
	DEX
	STX	PSP
	STAA	0,X
	STAB	1,X
	RTS
*
* Load a constant from the instruction stream into A:B, 
* continue execution after the constant.
* This is not self-modifying code, even though it feels like a trick
* and is playing with the return stack and instruction stream 
* in ways we wouldn't think we wanted to think we should.
* Call it a "necessary" bit of run-time syntactic sugar.
*
* Use it like this:
*	JSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	JSR	SOMEWHERE ; or some other executable code.
*
LD16I	TSX		; point to top of return address stack
	LDX	0,X	; point into the instruction stream
	LDAA	0,X	; high byte from instruction stream
	LDAB	1,X	; low byte from instruction stream
	INS		; drop the return address we don't need
	INS
	JMP	2,X	; return to the byte after the constant.
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDX	PSP
	LDAB	3,X	; left low
	LDAA	2,X	; left high
	ADDB	1,X	; right low
	ADCA	0,X	; right high, with carry
	STAB	3,X	; sum low
	STAA	2,X	; sum high
	INX		; adjust parameter stack before restoring the flags
	INX
	STX	PSP
*
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDX	PSP
	LDAB	3,X	; left low
	LDAA	2,X	; left high
	SUBB	1,X	; right low
	SBCA	0,X	; right high, with borrow
	STAB	3,X	; difference low
	STAA	2,X	; difference high
	INX		; adjust parameter stack before restoring the flags
	INX
	STX	PSP
	RTS
*
*
START	JSR	INISTKS
*
	JSR	LD16I
	FDB	$1234	; (FDB seems to want a comment.)
	JSR	PPSH16
	JSR	LD16I
	FDB	$CDEF	; (FDB seems to want a comment.)
	JSR	PPSH16
	JSR	ADD16	; result should be $E023
	JSR	LD16I
	FDB	$8765	; (FDB seems to want a comment.)
	JSR	PPSH16
	JSR	SUB16	; result should be $58BE
	LDX	PSP
	LDAB	1,X	; load the result into A:B
	LDAA	0,X
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

If something doesn't work, go back and make sure you've copied everything correctly.

Once you've stepped through it, you might want to try other constants. 

Before we move on to equivalent code for the 6801, let's compare how it would look with an interleaved (combined) parameter and return stack -- you know, the single stack discipline I keep disparaging. Here's a comparable test frame for the single stack:

* 16-bit addition and subtraction for 6800 on return stack,
* with test code
* Joel Matthew Rees, October 2024
*
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.
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
	NOP		; bump to aligned
	RMB	2	; a little bumper space
SSTKLIM	RMB	95	; (64+31) roughly 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
*
*
INISTKS	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
*
*
* Don't need PPOP and PPSH
*
* Load a constant from the instruction stream into A:B, 
* continue execution after the constant.
* This is not self-modifying code, even though it feels like a trick
* and is playing with the return stack and instruction stream 
* in ways we wouldn't think we wanted to think we should.
* Call it a "necessary" bit of run-time syntactic sugar.
*
* Use it like this:
*	JSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	JSR	SOMEWHERE ; or some other executable code.
*
LD16I	TSX		; point to top of return address stack
	LDX	0,X	; point into the instruction stream
	LDAA	0,X	; high byte from instruction stream
	LDAB	1,X	; low byte from instruction stream
	INS		; drop the return address we don't need
	INS
	JMP	2,X	; return to the byte after the constant.
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	TSX
	LDAB	5,X	; left low
	LDAA	4,X	; left high
	ADDB	3,X	; right low
	ADCA	2,X	; right high, with carry
ADD16S	STAB	5,X	; sum low
	STAA	4,X	; sum high
	LDX	0,X	; before we deallocate it
	INS		; drop return address
	INS
	INS		; drop right-hand addend
	INS
	JMP	0,X	; return
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	TSX
	LDAB	5,X	; left low
	LDAA	4,X	; left high
	SUBB	3,X	; right low
	SBCA	2,X	; right high, with borrow
	BRA	ADD16S	; Steal code.
* Could steal code this way in the parameter stack example, as well.
*
*
START	JSR	INISTKS
*
	JSR	LD16I
	FDB	$1234	; (FDB seems to want a comment.)
	PSHB		; push in correct order
	PSHA
	JSR	LD16I
	FDB	$CDEF	; (FDB seems to want a comment.)
	PSHB
	PSHA
	JSR	ADD16	; result should be $E023
	JSR	LD16I
	FDB	$8765	; (FDB seems to want a comment.)
	PSHB
	PSHA
	JSR	SUB16	; result should be $58BE
	PULA
	PULB
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

On casual inspection and quick step-through, it looks simpler. It definitely runs faster. 

Being able to use the processor's native PSH/PULA/B instructions instead of the PPUSH/PPOP routines definitely seems to be a plus.

But deeper inspection reveals some tricky games dodging the return address, games that, if you get them wrong, crash the program in amusing ways just when you really didn't want to be amused.

I know you don't believe me, but hold on to your doubts for a moment.

For further reference, here's a comparable set of routines and test code that uses a scratch area in the DP to pass values in  and out. You could call this using direct page static globals as pseudo-registers:

* 16-bit addition and subtraction for 6800 via scratch pad,
* with test code
* Joel Matthew Rees, October 2024
*
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.
SSAVE	RMB	2	; a place to keep S so we can return clean
* parameter/scratch area for leaf functions only:
NLFT	RMB	2	; binary operator left side parameter
NRT	RMB	2	; binary operator right side parameter
NRES	RMB	2	; unary/binary operator result
NTEMP	RMB	2	; general scratch register for 
NPAR	EQU	NLFT	; unary operator parameter
NSCRAT	EQU	NLFT	; 
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
NOENTRY	JMP	START
	NOP		; bump to aligned
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; roughly 16 levels of call, max
SSTKBAS	RMB	1	; 6800 is post-dec (post-store-decrement) push
	RMB	2	; a little bumper space
*
*
INISTKS	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
*
*
* Don't need PPOP and PPSH
*
* Load a constant from the instruction stream into A:B, 
* continue execution after the constant.
* This is not self-modifying code, even though it feels like a trick
* and is playing with the return stack and instruction stream 
* in ways we wouldn't think we wanted to think we should.
* Call it a "necessary" bit of run-time syntactic sugar.
*
* Use it like this:
*	JSR	LD16I	; load D immediate
*	FDB	$1234	; "immediate" 16-bit value to load
*	JSR	SOMEWHERE ; or some other executable code.
*
LD16I	TSX		; point to top of return address stack
	LDX	0,X	; point into the instruction stream
	LDAA	0,X	; high byte from instruction stream
	LDAB	1,X	; low byte from instruction stream
	INS		; drop the return address we don't need
	INS
	JMP	2,X	; return to the byte after the constant.
*
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDAB	NLFT+1	; low
	LDAA	NLFT	; high
	ADDB	NRT+1	; low
	ADCA	NRT	; high, with carry
ADD16S	STAB	NRES+1	; sum low
	STAA	NRES	; sum high
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDAB	NLFT+1	; low
	LDAA	NLFT	; high
	SUBB	NRT+1	; low
	SBCA	NRT	; high, with borrow
	BRA	ADD16S	; Steal code (5 bytes for 2)
* Could steal code this way in the parameter stack example, as well.
*
*
START	JSR	INISTKS
*
	JSR	LD16I
	FDB	$1234	; (FDB seems to want a comment.)
	STAB	NLFT+1
	STAA	NLFT
	JSR	LD16I
	FDB	$CDEF	; (FDB seems to want a comment.)
	STAB	NRT+1
	STAA	NRT
	JSR	ADD16	; result should be $E023
	LDAB	NRES+1
	LDAA	NRES
	STAB	NLFT+1
	STAA	NLFT
	JSR	LD16I
	FDB	$8765	; (FDB seems to want a comment.)
	STAB	NRT+1
	STAA	NRT
	JSR	SUB16	; result should be $58BE
	LDAB	NRES+1
	LDAA	NRES
	NOP
	NOP
* Repeat, without all the pushing and popping and jumping around:
	LDAB	#$34
	LDAA	#$12
	ADDB	#$EF
	ADCA	#$CD
	SUBB	#$65
	SBCA	#$87
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP
	NOP		; landing pad

This probably looks even simpler. 

It's not easy to see how complicated this becomes, how quickly, with a small test program like this, but it will become plain shortly (for some definition of shortly and some definition of plain).

And you should be looking at those six lines of code right before the DONE label and scratching your head. 

All of that? Just to write the equivalent of the following?

	LDAB	#$34
	LDAA	#$12
	ADDB	#$EF
	ADCA	#$CD
	SUBB	#$65
	SBCA	#$87

That is essentially what the test frame does. But, of course, we didn't write all of that just to write the test frame. We wrote it to allow us to do things well beyond what the test frame does.

And, in the cynical point of view, waste some of the applications' run-time cycles to reduce the design-time burden. 

But, no, not just that. There are things you cannot reduce to constants at design- or compile-time.

Things will become a bit clearer after we get a look at this for the 6801, 6809, and 68000, after we get a look at reading keys from the keyboard, and maybe a bit of other prep so we can take up a simple project to prove that we can make something directly useful from assembly language.

In the meantime, let's get a look at how this looks on the 6801.


(Title Page/Index)

 

ALPP 02-XX -- Missed the Beach with Parameters -- 16-bit Arithmetic on the 6800, 6801, and 6809

This attempt at a chapter hit a wall of text and went somewhere south. Keeping it for records.
You probably want to go here, instead: https://joels-programming-fun.blogspot.com/2024/09/alpp-02-08-on-beach-with-parameters-16-bit-arithmetic-6800.html

On the Beach with Parameters --
16-bit Arithmetic
on the 6800, 6801, and 6809

(Title Page/Index)

 

So we snuck the 16-bit arithmetic in already didn't we? We were passing byte parameters in and widening them in the last note and the previous two chapters.

Parameters. 

(Wall of text warning!)

Those were the numbers we were feeding in, via variables such as the ones labeled NLFT and NRT.

In software engineering, the parameters become, not just the numbers/values passed in to an automaton, but the variables that are the means of passing them in to the defined software function that implements an automaton. 

A mathematical function is an abstract object that defines an abstract automaton's behavior, often invoking an abstract algorithm. 

But a software function is a concrete implementation of a mathematical function, with the various sorts of limits that implementation imply. The implementation is via a procedural implementation of the algorithm, again with limits implied.

A mathematical parameter is a value which affects the operation of an automaton, including numeric or abstract values which are "input" into the automaton function.

A software parameter is also the device by which a particular parameter value is input, and we often include, since the output devices have a lot in common with the input devices, output devices as parameters.

In other words, result variables are similar to parameter variables in so many ways, it can be hard to distinguish them except by knowing that one is input and one is output, and it can be useful to consider them of the same class of object, objects used for moving data between parts of the code.

Scratch, or temporary, variables are also of the same class of object.

Hopefully, as we work through some of the descriptions of the mechanisms, I can make it clear which I'm talking about.

So far, except for the Hello World chapters, we've been allocating our parameter variables statically and globally. They are "static" because they persist from beginning to end, and before and after, really. And they are "global" because they are visible and known in all parts of the code.

When it's just a few functions, it's not hard to keep track of statically allocated globals, but, when it's hundreds or thousands of functions (or hundreds of thousands), tracking all those variables and their names can get a bit confusing. And the bytes of program space they use can get prohibitive.

And you can often have way too much fun finding out that you've given two (or more) variables the same name, so that when you think you're changing the parameters to one function, you're actually coincidentally changing the parameters to another at the same time. And either function or both misbehaves.

And,  because all those variables consume all that RAM space, you tend to try to share variables between functions that you know shouldn't be in-flight at the same time, and then you forget, and fail to keep them from being in-process at the same time, and things get even more confusing.

So it's actually an important tool to have a means of defining parameter and variable names that don't need to be global as "local" to a function -- that is, you make their names invisible outside the function in which they are defined and used.Unfortunately, although some assemblers provide means for making definitions in some sense local, the syntax, and, too often, the semantics of those tools are not shared between the standard assemblers for each CPU.

If we keep our functions and our projects small, we don't actually need to have parameter names that the assembler sees at all. Sure, they can help when the definition mechanism doesn't get in the way, but we can work around not having them. Comments can suffice.

(With large projects where we can justify the heavy use of optimizers and mechanical correctness analysis, we will want named and characterized parameters, but we will not be tackling such large projects in this tutorial.)

Gack. I did not want to do one of these walls of text here. But we need it. One more point and then we can get back to code.

It's even more important to have a means of physically (so to speak) allocating and accessing parameters and variables that don't need to persist between invocations of a function, such that they don't even exist when the function is not in-process. 

These parameters have traditionally been called "dynamic", since that seems to indicate their dynamic mode of existence. ("Ephemeral" just sounds a little too ghostly, I guess.)

And that is what we are going to talk about in this chapter.

In the Hello World chapters, we saw three ways to allocate and access parameters that are not static in persistence. Well, two and a half, maybe.

One is by passing them via CPU registers, and, if we need to keep them safe, we push them before we call other function routines, and pop them back when we're done. (This is the half-method.)

A second is by putting them directly on the same stack as the return addresses, being careful to keep the pushes and the pops balanced, and being just as careful not to overwrite the return addresses, or, at least, try to be careful. 

Often, in our efforts to be careful, we construct something called a "stack frame", which is time-consuming and, well, clunky. 

Clunky is not necessarily bad, but it can be, and often is.

A third is by passing them via a separate parameter stack. 

Separate parameter stacks inherently avoid the conflicts with the return addresses, and you can even construct stack frames which are not clunky when you have a separate parameter stack.

The separate parameter stack requires additional maintenance, and many engineers eschew the additional maintenance as if the return stack didn't require maintenance.

Assuming the return address stack doesn't require maintenance is a very dangerous practice.

Conversely, maintaining the return address stack properly is about half-way to maintaining the separate parameter stack properly. It's a matter of keeping track of how many calls deep your code can go where and when, and how many bytes are needed at each level. It sounds intractible, but it isn't really.

Heh. The final point was another wall of text, and you're thinking I've forgotten about 16-bit arithmetic. But, since we've done that, I'm going to use getting them done properly as an opportunity to show how to pass parameters.

Ack. One more low wall-of-text. Sorry.

When a programmer wants to call a subroutine, he or she usually doesn't want to think too deeply about how the subroutine does its job. Just pass the parameters in the appropriate places, call it, and use the results.

On the other hand, the optimizer (whether mechanical or human) wants to know what's going on inside, make some efficiency judgements based on some given criteria, and decide whether to actually call the code or pull the non-parameter-handling code into the caller routine, in-line.

In high-level languages, this pulling code in is often called in-lining, but at the assembly language level, it's often called macro-expansion, because of something many assemblers have called macro definition (which we probably need to look at sometime).

In this chapter, I'm going to show how to define functions that implement 16-bit addition and subtraction, passing the parameters on the separate parameter stack. When we've looked at that, we can take quick detours to look at the other two approaches, which we will do in other chapters.

 

 

While we're here, subtraction on the 6800 is a bit more complicated, as I alluded to above, because we do, in fact, usually want to have the Z flag tell the whole story.  

To see what I mean, let's convert the straight 16-bit addition source for the 6800 from above to subtraction:

NLFT	FDB	132	; 132 in two bytes, high byte zero
NRT	FDB	188	; 188 in two bytes, high byte zero
RES	RMB	2	; 2-byte result
	...
	LDAB	NLFT+1	; Get the left low byte.
	LDAA	NLFT	; Get the left high byte.
	SUBB	NRT+1	; Subtract the right low byte.
	SBCA	NRT	; Subtract the right high byte.
	STAB	RES+1	; Store the result low byte.
	STAA	RES	; Store the result high byte.

The carry/borrow flag is unaffected by storing the result, so it's okay. (The H Half-carry and  V oVerflow flags, we haven't talked about, so we won't at this point.) Since we stored the less significant byte and then the more significant byte, in that order, the N sign bit flag reflects the high bit of the A accumulator, or the more significant byte, which is correct.

So branch on carry set or clear, and branch on plus/minus branches will both work after the second store.

But the Z Zero flag only represents the final STAA instruction, so it only tells us whether the high byte is zero or not, which is almost never what we want to know.  

Subtraction is often used to compare two numbers. If the result is positive, the left side is greater. If the result is negative, the right side is greater. And if the result is zero, both sides were equal.

With what we have, the Carry flag being set tells us that the right side was greater. But the Carry flag being clear tells us that either the left side was greater or the two numbers were equal.

As with adding, we can fix that by OR-ing the two bytes, which, in this case, since the result is both in memory and in the accumulators, is straightforward, just use the ORAA on the low byte at RES+1, then you can branch on zero and know that both bytes are represented in the Z flag. (We'll talk more about this later.) So, for example,

NLFT	FDB	132	; 132 in two bytes, high byte zero
NRT	FDB	188	; 188 in two bytes, high byte zero
RES	RMB	2	; 2-byte result
FLAGS	RMB	1	; temporary for the flags
	...
	LDAB	NLFT+1	; Get the left low byte.
	LDAA	NLFT	; Get the left high byte.
	SUBB	NRT+1	; Subtract the right low byte.
	SBCA	NRT	; Subtract the right high byte.
	STAB	RES+1	; Store the result low byte.
	STAA	RES	; Store the result high byte.
* Usually, you don't want to go to all this trouble!
	TPA		; get the flags in A
	ANDA	#$FB	; clear the Z flag
	STAA	FLAGS
	ORAB	RES	; Set the correct Z flag
	TPA
	ANDA	#$04	; clear all but Z
	ORAA	FLAGS	; combine the flags
	TAP		; replace the flags
* Now all branches work as they should.




At some point, I need to talk about where the scratch RAM should be and why, but this should be enough for this note, I guess.


I don't recommend using the return stack, but we'll look at it:

NL1	FCB	34	; just an arbitrary small number
NR1	FCB	66	; another arbitrary small number
RES1	RMB	2	; 2-byte result
C1	EQU	RES1	; To look at the carry from the sum.
R1	EQU	RES1+1	; To look at the the eight bit sum.
	...
	LDB	NR1	; Get the addend (right side).
	CLRA		; Clear storage for high byte.
	PSHS	A,B	; pushed in right order
	LDB	NL1	; Get the augend. A still clear.
	ADDD	,S++	; add the right side and pop it.
	STD	RES1	; Save the 9 bit result in 16 bits.
If we have the parameter stack set up, we can use that, instead:
NL1	FCB	34	; just an arbitrary small number
NR1	FCB	66	; another arbitrary small number
RES1	RMB	2	; 2-byte result
C1	EQU	RES1	; To look at the carry from the sum.
R1	EQU	RES1+1	; To look at the the eight bit sum.
	...
	LDB	NR1	; Get the addend (right side).
	CLRA		; Clear storage for high byte.
	PSHU	A,B	; pushed in right order
	LDB	NL1	; Get the augend. A still clear.
	ADDD	,U++	; add the right side and pop it.
	STD	RES1	; Save the 9 bit result in 16 bits.

The subtraction version would simply replace ADDD with SUBD, and it would be done.

Do you see the meta-similarities between the 6809 and 68000? 

Did I mention before that the 6809 is not the predecessor to the 68000, nor is it 68000-lite? They were developed in parallel, taking the 6800 as a springboard, referring to a study Motorola made of code for the 6800, looking for ways to relieve bottlenecks in code and improve efficiency, with the two projects heading slightly different directions. 

Oh, and the 6801 project actually began while they were getting silicon on the 6809, so the 6801 is actually a third direction, which Motorola followed up on with the very-well received 68HC11. Ah, the things that could have been.

I know, I've mentioned this before. I'm sure I have. I harp on it too much.








(Title Page/Index)