Monday, October 7, 2024

ALPP 03-01 -- Binary Output on the 6800, Left-to-right; Framework-by-include (asm68c)

Binary Output on the 6800,
Left-to-right;
Framework-by-include
(asm68c)

(Title Page/Index)

 

We just worked through three ways to pass parameters at run-time on the 68000 -- and I think it's getting a little tedious to rely on the debugger alone for seeing what's going on in the program.

So, how about we look at ways to get numeric output?

Binary output's easy. All you do is look at the bits and spit them out, something like this:

* simple 8-bit binary output for 6800
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
	EXP	rt_rig6800.asm
****************
* Program code:
*
* Output a 0
OUT0	LDAB	#'0
OUT01	JSR	PPSHD
	JSR	OUTC
	RTS
*
* Output a 1 
OUT1	LDAB	#'1
	BRA	OUT01
* Rob code, shave a couple of bytes, waste a few cycles.
*
* Output the 8-bit binary (base two) number on the stack.
* For consistency, we are passing the byte in the low-order byte
* of a 16-bit word.
OUTB8	LDX	PSP	; parameter is at 0,X (low byte at 1,X)
	LDAB	#8	; 8 bits
	STAB	0,X	; Borrow the upper byte of the parameter.
OUTB8L	LSL	1,X	; Get the leftmost bit.
	BCS	OUTB81
OUTB80	BSR	OUT0
	BRA	OUTB8D
OUTB81	BSR	OUT1
OUTB8D	DEC	0,X
	BNE	OUTB8L	; loop if not Zero
	INX		; drop parameter bytes
	INX
	STX	PSP
	RTS
*
HEADLN	FCB	CR,LF	; Put message at beginning of line
	FCC	"Outputting $5A in binary:"	; 
	FCB	CR,LF,NUL	; Put the binary output on a new line
*
*
*
*
PGSTRT	LDX	#HEADLN
	JSR	PPSHX
	JSR	OUTS
	CLRA
	LDAB	#$5A	; byte to output
	JSR	PPSHD
	JSR	OUTB8
	JSR	OUTNWLN
	RTS
*
	END	ENTRY

Well, it's not the most efficient expression of the algorithm, but I think it's reasonable. Separate functions for outputting 0 and 1 seems excessive, but the tail parts are common and we can have one steal code from the other.

Getting the bits is just a matter of shifting them into the carry. Shift left is, by convention of the cultures in which the dominant computer technology developed, shifting the most significant bit out of the register (and thus into Carry). Motorola CPUs (along with most others) do not distinguish between arithmetic and logical shifts going left, and we don't have any reason to care, either.

Why shift left? 

When we write, we usually write the most significant first on the left, and then proceed writing to the right. And that's how it proceeds here. You put the most significant bit in Carry by shifting the bits to the left.

Can we do it from right-to-left, instead? Shifting right on bit is essentially dividing by two, isn't it? 

Yeah, but then we need a buffer and extra logic, and I'm avoiding that for now.

Finally, where is all the missing code? And what's that EXP pseudo-operator?

EXP is an abbreviation of "expand", and it means to expand (thus, include) the named file in the place of the line EXP is on. And that file contains all the missing code, all the useful bits from the preceding chapters.

Here it is:

* A simple run-time framework inclusion for 6800
* providing parameter stack and local base
* Version 00.00.00
* Joel Matthew Rees, October 2024
*
* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0
*
* Essential monitor ROM routines
XOUTCH	EQU	$F018
*
NATWID	EQU	2	; 2 bytes in the CPU's natural integer
*
*
	ORG	$80	; MDOS and EXbug docs say it should be okay here.
ENTRY	JMP	START
	NOP		; Just want even addressed pointers for no reason.
*
* These are the page zero context variables that must be
* saved and restored on process context switch.
* They must never be accessed except in leaf routines:
PSP	RMB	2	; parameter stack pointer
LBP	RMB	2	; local static variable base pointer
XSTKWK	RMB	2	; for stashing X during stack work 
XWORK	RMB	2	; for stashing X during other very short operations
*
SSAVE	RMB	2	; a place to keep S so we can return clean
* End of page zero context variables.
*
*
	ORG	$2000	; MDOS says this is a good place for usr stuff
LOCBAS	EQU	*	; here pointer, local static base starts here.
NOENTRY	JMP	START
	NOP
	RMB	64	; room for something
	RMB	2	; a little bumper space
* Not much here
*
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
*
*
INITRT	LDX	#PSTKBAS	; Set up the run-time environment
	STX	PSP
	LDX	#LOCBAS
	STX	LBP
	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
*
*
*********************
* Low-level library:
*
* Only alters X
PPOPX	LDX	PSP
	LDX	0,X
	STX	XSTKWK
	LDX	PSP
	INX
	INX
	STX	PSP
	LDX	XSTKWK
	RTS
*
* Trashes A,B;
* X points to X value just pushed -- PSP top of stack -- at end
PPSHX	STX	XSTKWK
	LDAA	XSTKWK
	LDAB	XSTKWK+1	; Falls through
* X points to PSP top of stack at end
PPSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STAA	0,X
	STAB	1,X
	RTS
*
*
* X points to PSP top of stack at end
PPOPD	LDX	PSP
	LDAA	0,X
	LDAB	1,X
	INX
	INX
	STX	PSP
	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.
*
OUTNWLN	LDAA	#CR	; driver level code to output a new line
	BSR	OUTCV
	LDAA	#LF
	BSR	OUTCV
	RTS
*
OUTC	JSR	PPOPD	; get the character in B
	TBA		; put it where XOUTCH wants it.
	BSR	OUTCV	; output A via monitor ROM
	RTS
*
OUTCV	JMP	XOUTCH	; driver code for outputting a character
*
OUTS	JSR	PPOPX	; get the string pointer
OUTSL	LDAA	0,X	; get the byte out there
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	INX		; point to the next
	BRA	OUTSL	; next character
OUTDN	RTS
*
*
******************************
* intermediate-level library:
*
* 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
	INX
	STX	PSP
	RTS
*
*
************************************
* Start run-time, call program.
* Expects program to define PGSTRT:
*
START	JSR	INITRT
*
	JSR	PGSTRT
*
DONE	LDS	SSAVE	; restore the monitor stack pointer
	NOP		; remember to set a breakpoint here!
	NOP		; landing pad
	NOP
	NOP
	LDX	$FFFE	; alternatively, get reset vector
	JMP	0,X	; and reboot through it
*
* Anyway, if running in EXORsim,
* Ctrl-C should bring you back to EXORsim monitor, 
* but not necessarily to your program in a runnable state.

Save it as

rt_rig6800.asm

so that the bit output code can find it and include it.

But, wait. Can EXORsim's interactive assembler do that?

Good question. Pasting it into an assembly session, it just says, "huh" at that line, even if I copy the file into EXORsim's execution director.

It looks like we need to graduate to a separate assembler. And I happen to have one for the 6800 and 6801. It's ancient, but it does the job well enough, and it accepts the EXP pseudo-operator. 

I call it asm68c, which can be confusing, because there's another unrelated product out there by someone else with the same name. This is the canonical repository for my assembler:

https://sourceforge.net/projects/asm68c/

Ignore the big green download button and find the link to the Git repository a bit below and to the right. Click the Git link and then click the

code

link that shows in the pop-up menu. (Ignore a68c-Code. I need to do something about that abortive branch sometime. I promise I will. Just ignore it. Don't click it. It's old code. Sorry.)

You can either download a snapshot (.zip format, but it'll do.) or use git to clone it. You probably don't want to unpack or clone it in the same directory that you're working on the tutorial in, however. Find a good out-of-the-way place to build it.

Scroll down the browser listing of the source code tree and you'll see the README file. I give rough instructions there for building it, but they work. I'll summarize them here. Just go to the top directory after you clone or unpack it and give it the 

make

command, if you have a compatible make in your system (which, after building EXORsim, I think you must). That will build and test it.

Or the simple compile command

cc -Wall -o asm68c *.c

should do the job as well. Simply compiling won't attempt to assemble the test files and compare output, but it should be fine.

Move, copy, or link the 

asm68c

executable to your local executables directory and make sure the permissions are right and you should be good to go. Or you can invoke by the full path. Either way works.

You should be able to type the 

asm68c -?

command at the terminal command line and get a listing of command-line options, most of which you should ignore, at least for now.

Once it's built and the executable is installed in an appropriate place and running, make sure you've saved the run-time rigging code as 

rt_rig6800.asm

and the binary output function and test code in the same directory, as 

outb8_6800.asm

Then the command line

asm68c -l2 outb8_6800.asm

should assemble the code, spitting a listing file to your screen on the second pass, and leaving the s1/s9 object code in a file named

outb8_6800.x

which you can open with a text editor. Or you can redirect the assembler listing to a file with 

asm68c -l2 outb8_6800.asm > outb8_6800.list

Actually, you do want to redirect the listing to a file, because you'll want to refer to it while stepping through the code and debugging it. But there is something satisfying about watching the assembler listing scroll up the screen ...

Heh. (cough) anywho

If you aren't getting the listing, either you've downloaded the old code or you haven't given the "-l2" option correctly. That's a hyphen, a little el, and a 2. It means, "list from the 2nd pass."

Check the listing for errors. If the line after it says pass 2 is done says no syntax errors found, it should be good, but you should use your text editor's search function to make sure that no "error"s or "warning"s show up anywhere.

Fix the errors in the assembly language source file, not the listing file!

(I can't tell you how many times I've found myself editing the listing and wondering why it doesn't change the compiled code. 8-*)

If there are no errors, you can open the file

outb8_6800.x

select the whole file and copy it, open another terminal session wherever you usually run EXORsim, run the 6800 version with 

exor --mon

if it's in your executable path, or if you are running it where you built it (like I do), with 

./exor --mon

and get a 6800 session going. At the EXORsim monitor's % prompt, hit the el key ("l" for "load") and hit return, and it will wait for you to paste the s1/s9 object code in. You may have to hit return again at the end, to bring it back to the % prompt:

6800 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% l
S10700807E213C019C                                         
S10B0084000000000000000070
S105008C00006E
S11320007E213C01000000000000000000000000F0
S113201000000000000000000000000000000000BC
S113202000000000000000000000000000000000AC
S1132030000000000000000000000000000000009C
S109204000000000000096
S11320460000000000000000000000000000000086
S11320560000000000000000000000000000000076
S11320660000000000000000000000000000000066
S11320760000000000000000000000000000000056
S11320860000000000000000000000000000000046
S11320960000000000000000000000000000000036
S10720A60000000032
S11320AACE20A8DF84CE2000DF8630EE0031319FB7
S10920BA8C8E20656E000F
S11220C0DE84EE00DF88DE840808DF84DE8839E2
S10920CFDF889688D68923
S10E20D5DE840909DF84A700E701395D
S10E20E0DE84A600E6010808DF843956
S10E20EB30EE00A600E60131316E0269
S10C20F6860D8D0C860A8D083953
S10A20FFBD20E0178D01393B
S10621067EF0184C
S1102109BD20C0A60027058DF40820F7397D
S1132116DE84E603A602EB01A900E703A70208088A
S1062126DF843916
S1132129DE84E603A602E001A200E703A702080889
S1062139DF843903
S106213CBD20AA15
S106213FBD21912A
S10E21429E8C01010101FEFFFE6E00F7
S10C214DC630BD20D5BD20FF39C8
S1072156C63120F575
S113215ADE84C608E700680125048DE720028DECB9
S10C216A6A0026F20808DF84393A
S11321730D0A4F757470757474696E672024354144
S111218320696E2062696E6172793A0D0A005D
S1132191CE2173BD20CFBD21094FC65ABD20D5BD67
S10921A1215ABD20F639AD
S90321A734
PC set to 21a7
437 bytes loaded
No checksum errors.
% 

From there you can unassemble the code, get memory dumps, and so forth, comparing what's in memory to what shows in the listing. When you're satisfied it all got safely put where it's supposed to be, proceed to step through and debug as usual:

% s 80

          0 A=00 B=00 X=0000 SP=00FF ------          0080: 7E 21 3C JMP 213C  EA=213C        
>         1 A=00 B=00 X=0000 SP=00FF ------          213C: BD 20 AA JSR 20AA                 


6800 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% r
PC=213C A=00 B=00 X=0000 SP=FF CC=C0
% b 2145
Breakpoint set at 2145
% c


Breakpoint!

Outputting $5A in binary:
01011010
        951 A=0A B=30 X=20A8 SP=00FF ------          2144: 01       NOP                      
>       952 A=0A B=30 X=20A8 SP=00FF ------          2145: 01       NOP                      

6800 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% 

At least, that's what you should be able to get once you have it all set up correctly.

One of the things I need to fix in my assembler is to provide an option for making EXORsim compatible "facts" files from the symbol table. Until I do, you'll need to look the label addresses up in the assembler listing. 

Just for reference, OUT0 and OUT1 can be absorbed into OUTB8, for a slight optimization, as below. (I've hinted at a further optimization in the comments.)

* simple 8-bit binary output for 6800, slightly optimized
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
	EXP	rt_rig6800.asm
****************
* Program code:
*
* Output the 8-bit binary (base two) number on the stack.
* For consistency, we are passing the byte in the low-order byte
* of a 16-bit word.
OUTB8	LDX	PSP	; parameter is at 0,X (low byte at 1,X)
	LDAB	#8	; 8 bits
	STAB	0,X	; Borrow the upper byte of the parameter.
OUTB8L	LSL	1,X	; Get the leftmost bit.
	BCS	OUTB81
OUTB80	LDAA	#'0
	BRA	OUTB8B
OUTB81	LDAA	#'1
OUTB8B	JSR	OUTCV
	DEC	0,X	; B is actually preserved, but this also works.
	BNE	OUTB8L	; loop if not Zero
	INX		; drop parameter bytes
	INX
	STX	PSP
	RTS
*
HEADLN	FCB	CR,LF	; Put message at beginning of line
	FCC	"Outputting $5A in binary:"	; 
	FCB	CR,LF,NUL	; Put the binary output on a new line
*
*
*
*
PGSTRT	LDX	#HEADLN
	JSR	PPSHX
	JSR	OUTS
	CLRA
	LDAB	#$5A	; byte to output
	JSR	PPSHD
	JSR	OUTB8
	JSR	OUTNWLN
	RTS
*
	END	ENTRY

And that's enough for one chapter. After you've played around a bit more with this, let's do it on the 6801

Speaking of tedious, I'm running out of time to repeat this tutorial for parameters on the return stack and for static parameters. You've seen enough to do those yourself at this point.

If you are interested,  please give it a try. 

I'm going to set those aside, except possibly where it comes time to talk about stack frames

If you do it yourself, you'll learn a lot. 

I think it will also convince you how things that look simple often aren't, especially when you don't have automated tools helping you track what's being done where. 

I think you'll quickly see how the return address really does get tangled up in your variables and parameters when you do that, so much so that stack frames become de rigueur rather quickly. As I say, I may do one more example where we can see stack frames start turning into a bottleneck. Maybe.

And you'll also see how quickly static parameters tend to devolve into either making lots of little stacks or doing lots of copying to the return address stack, which demonstrates why you really want to make as few variables statically allocated as possible in the first place -- although it does turn out that a few statically allocated variables are necessary in pretty much any real application. 

On, on to the 6801.


(Title Page/Index)

 

 

 

 

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 03-XX -- More Tools -- LWTools for 6809 and asm68c for 6800/6801

Decided to split the binary output chapters up and talk about the tools there. So this chapter will get absorbed into two of those. 

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

And go here for my discussion of using LWToolshttps://joels-programming-fun.blogspot.com/2024/10/alpp-03-03-binary-output-6809-left-to-right-framework-by-include.html.

 

More Tools
LWTools for 6809
and asm68c for 6800/6801

(Title Page/Index)

 

Again, I'll assume that you have EXORsim, Hatari, and EXORsim6801 running under *nix or Cygwin environment. If you've been following along, you must have, or you must have something equivalent set up and running.

EXORsim's built-in interactive assembler is great for small stuff, but our number of lines of code has already gone over a hundred, and when we have that many lines of code we really want a separate assembler.

William Astle (Lost Wizard) put together a very professional assembler, along with related tools for the 6809 and 6309, called LWTools. His support site is here:

http://www.lwtools.ca/

Instructions to build it are on that page, and there are links to the manual as well.

Building it is straightforward, and it supports standard techniques for changing the install directory. I was able to tell it to install to my user-local executables directory without fuss.

When you get through the install with no errors, you should be able to give it the command

$ lwasm --help

and it should list out various ways to call it. 

I've put together a much less professional assembler for the 6800 and 6801 which I call asm68c. (Just so you know, there's another project out there called asm68c which is not an assembler and has nothing to do with me. Don't ask me about it.)

The source code page you want is "code", not "a68c-code". (I really should do something about a68c-code, it was a blind alley, and I don't remember why I didn't just delete it.)

Anyway, the README file will be displayed under the master tree listing when you go to the source code, and it has some basic instructions for building and testing it. Parts of the README are a little out-of-date. But it should be enough to let you work on the tutorial sources.

While I have gone to the trouble of providing a make file for it, I've left the question of installing it up to you. It's one executable, so it's just one file to copy or link into your user local executables directory or wherever you want to run it from.

When you get it to compile without errors, try running it with 

$ asm68c -?

and it should respond with a bunch of stuff that I need to remind myself about. (Sorry) But there should be enough in there to remind you how to give it your source file name and how to tell it what kind of object to output and where to save the object, etc. (Yeah, the help listing is a bit out-of-date, too.)

Source to test it on?

 

(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)