Wednesday, October 9, 2024

ALPP 03-05 -- Hexadecimal Output on the 6800, without General Divide

Hexadecimal Output on the 6800,
without General Divide

(Title Page/Index)

 

We've now done binary output the easy way on all four processors ending with the 68000. 

I say the easy way, because we didn't use a general divide, and we didn't use an intermediate buffer. Octal and hexadecimal can be done similarly, letting us avoid developing a general division routine. Really, any power of two as a radix can use this shortcut on binary CPUs.

Not that division is hard, it's just a bit arcane, and even many professional software engineers seem to be allergic to arcane. So I'm going to introduce it in stages.

Truth told, as I mentioned somewhere in the last four chapters, shifting is division and multiplication by powers of two.

Look at it this way, assuming decimal radix:

 123456789.0
      × 10
-----------------
1234567890.0 

and

 123456789.0
      ÷ 10
-----------------
  12345678.90 

In decimal radix -- base ten -- multiplication by ten is shifting the number one column to the left. Which is the same thing as shifting the decimal point one column to the right.

And dividing by ten is shifting the number one column to the right, or shifting the decimal one column to the left.

This works in binary, as well.

1011010.0two × 10two == 10110100.0two

and

1011010.0two ÷ 10two == 101101.0two

So shifting left was multiplying by 2? 

Exactly. And the left bit fell out into the Carry, and we grabbed it and shoved it out on what became the next left column of the output number.

Or we could say it this way -- multiplying by two in an 8-bit register is the same as dividing by 128. 

But then you have to see the Carry bit as the register receiving the result, which may be a little hard to visualize.

Oh, let's do a sanity check (and slip in the hexadecimal for later reference):

10two = 2ten or 2sixteen
1011010.0two = 90ten or 5Asixteen
10110100.0two = 180ten or B4sixteen
101101.0two = 45ten or 2Dsixteen

Are we satisfied about shifting left 1 bit being the same as multiplying by 2, and shifting 1 bit right being the same thing as dividing by 2? And multiple shifts being multiplying and dividing by powers of 2?

No? 

Maybe?

Well, let's move ahead. Shifting right four bits would be dividing by 24, or by sixteen.

Four bits of an eight-bit byte is a four-bit nybble, right?

So, when working with an eight-bit byte, a nybble (preferably properly aligned as the low or high nybble) is a hexadecimal column.

If we want the more-significant nybble, or the left-hand column of a byte, we could shift it left four bits, one bit at a time, catching each carry and rotating that left into another byte further left.

Or we could just shift it right four bits, bringing a 0 bit in on each sift.

A logical shift right brings zeroes into the high bits, so that would work perfectly.

And the less-significant nybble is already in place, all we have to do is mask off the high nybble. That can be done with the bit-AND operator: 

1AND1==1
1AND0==0
0AND1==0
0AND0==0

We've got enough of the theory of how this is going to work, let's write it:

* simple 8-bit hexadecimal output for 6800
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
	EXP	rt_rig01_6800.asm
****************
* Program code:
*
ASC0	EQU	'0	; Some assemblers won't handle 'c constants well.
ASC9	EQU	'9
ASCA	EQU	'A
ASCXGAP	EQU	ASCA-ASC9-1	; Gap between '9' and 'A' for hexadecimal
*
* Mask off and convert the nybble in B to ASCII numeric,
* including hexadecimals
OUTH4	ANDB	#$0F	; mask it off
	ADDB	#ASC0	; Add the ASCII for '0'
	CMPB	#ASC9	; Greater than '9'?
	BLS	OUTH4D	; no, output as is.
	ADDB	#ASCXGAP	; Adjust it to 'A' - 'F'
OUTH4D	CLRA
	JSR	PPSHD
	JSR	OUTC
	RTS
*
* Output an 8-bit byte in hexadecimal,
* byte as a 16-bit parameter on PSP.
OUTHX8	LDX	PSP
	LDAB	1,X	; get the byte
	LSRB
	LSRB
	LSRB
	LSRB
	BSR	OUTH4
	LDX	PSP
	LDAB	1,X
	BSR	OUTH4
	LDX	PSP
	INX
	INX
	STX	PSP
	RTS
*
HEADLN	FCB	CR,LF	; Put message at beginning of line
	FCC	"Outputting $5A in binary and hex: "	; 
	FCB	CR,LF,NUL	; Put the binary output on a new line
CHEX	FCC	": $"
	FCB	NUL
*
*
PGSTRT	LDX	#HEADLN
	JSR	PPSHX
	JSR	OUTS
	CLRA
	LDAB	#$5A	; byte to output
	JSR	PPSHD
	JSR	OUTB8
	LDX	#CHEX
	JSR	PPSHX
	JSR	OUTS	
	LDAB	#$5A	; byte to output
	JSR	PPSHD
	JSR	OUTHX8
	JSR	OUTNWLN
	RTS
*
	END	ENTRY

As usual, save it as something like

outhx8_6800.asm

We could use the same rigging as for the bit output, but I've moved the bit output (with a little more optimization) to the rigging:

* A simple run-time framework inclusion for 6800
* providing parameter stack and local base
* Version 00.00.01
* 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
* If we didn't mind leaving the popped value in limbo 
* beyond the top of the stack, we could avoid the temporary:
*PPOPX	LDX	PSP
*	INX
*	INX
*	STX	PSP
*	DEX
*	DEX
*	LDX	0,X
*	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.
*
* Output the 8-bit number on the stack in binary (base two).
* For consistency, we are passing the byte as 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 (B is preserved in BIOS
OUTB8L	LSL	1,X	; Get the leftmost bit.
	BCS	OUTB81
OUTB80	LDAA	#'0
	BRA	OUTB8B
OUTB81	LDAA	#'1
OUTB8B	JSR	OUTCV
	DECB	; count
	BNE	OUTB8L	; loop if not Zero
	INX		; drop parameter bytes
	INX
	STX	PSP
	RTS
*
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.

As before, save this in the same directory under the name the inclusion directive specifies --

rt_rig01_6800.s

Assemble it as you did the last with

$ asm68c -l2 outhx8_6800.asm > outhx8_6800.list

and go through the processes for simulation on EXORsim. 

Before we move on, go ahead and move the hexadecimal routine into your framework rigging. Optimize it if you feel inclined, leave it as-is if you don't. 

If you don't see any optimizations, that's okay. Early optimization can cause problems down the road, and we are not fighting the tight development environments now that we had back in the early 1980s.

Make sure it works, of course, preferably before you try any optimizations. Make sure the stacks stay balanced, too.

Other than stack usage, accidentally switching registers in mid-flight is another likely place to go wrong.

If you can't get it to work, step carefully through it, looking at registers at every step. And be careful not to fall asleep. :-/ If you still can't get it to work, move on to the 6801 code. Sometimes moving forward helps you see things you missed. And I'll show you what I've done, later.

The 6801 and 6809 code have no surprises, and I suppose I could have left them both as exercises. Pasting them in here would make this too long, at any rate.

Oh, but I had to do them myself, so I might as well post the results. Go take a look at the 6801 hexadecimal code. It really didn't change much.


(Title Page/Index)

 

Tuesday, October 8, 2024

ALPP 03-04 -- Binary Output on the 68000, Left-to-right; Framework-by-include

 Binary Output on the 68000,
Left-to-right;
Framework-by-include

(Title Page/Index)

 

Having seen what we can do with binary output on the 6809, let's see what it looks like on the 68000. 

I'm having a hard time coming up with good opening lines. Maybe I should just put a link to the last chapter at top left like I've done in my -- attempts at -- novels. Heh.

Hey, the hardest part of assembly language is staying awake while you're doing all the repetitious stuff.  

In a desperate attempt to make this interesting, I'll point out that shifting a single byte in a register 1 bit on the 68000 requires a 2-byte opcode, as opposed to a 1-byte opcode on the 6800/6801 and 6809. 

Of course, that same 2-byte opcode on a 68000, with just a little variation in a couple of bit fields, can shift an entire 32-bit register up to 8 bits. Doing that on a 680X requires repeating a 6-byte sequence 8 times. 

Which is done more often? We'll see. But in this code, it's a single byte, 1 bit.

Don't forget to compare the source with the source for the 6809. While you're at it, you might even open up the code for the 6801 and 6800 to compare the progression.

***********************************************************************
* simple 8-bit binary output for 68000
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024

	OPT LIST,SYMTAB	; Options we want for the stand-alone assembler.
	OPT DEBUG	; We want labels for debugging.
	OUTPUT
***********************************************************************

	INCLUDE	rt_rig68K.s

	MACHINE MC68000	; because there are a lot the assembler can do.


****************
* Program code:
*
* Output a 0
OUT0	MOVE.L	#'0',-(A6)
OUT01	BSR.W	OUTC
	RTS
*
* Output a 1 
OUT1	MOVE.L	#'1',-(A6)
	BRA.S	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 lowest-order byte
* of a 32-bit word.
OUTB8	MOVE.L	D5,-(A7)	; save working register	
	MOVE.L	(A6),D5	; shift on memory is 16-bit
	MOVE.W	#8,(A6)	; 8 bits to output, borrow parameter high word.
OUTB8L	LSL.B	#1,D5	; Get the leftmost bit of the lowest byte.
	BCS.S	OUTB81
OUTB80	BSR.S	OUT0
	BRA.S	OUTB8D
OUTB81	BSR.S	OUT1
OUTB8D	SUBQ.W	#1,(A6)
	BNE.S	OUTB8L	; loop if not Zero
	SUBQ.L	#NATWID,A6	; drop parameter character
	MOVE.L	(A7)+,D5	; MOVEM.W would sign extend
	RTS
*
HEADLN	DC.B	CR,LF	; Put message at beginning of line
	DC.B	"Outputting $5A in binary:"	; 
	DC.B	CR,LF,NUL	; Put the binary output on a new line
*
*
*
*
PGSTRT	LEA	HEADLN(PC),A0
	MOVE.L	A0,-(A6)
	BSR.W	OUTS
	MOVE.L	#$5A,-(A6)	; byte to output
	BSR.W	OUTB8
	BSR.W	OUTNWLN
	RTS
*
	END	ENTRY

Save that as something like

outb8_68K.s

[JMR202410152008 IMPORTANT NOTE! There is a bug in this source. See below the rigging for more information.]

You might notice in the framework that having a lot of registers and being able to work on 32 bits at a time makes the code feel a bit less streamlined than the 6809 code. I certainly get that sense from it. Maybe it's just me.

***********************************************************************
* A simple run-time framework inclusion for 68000
* providing parameter stack and local base
* Version 00.00.00
* Joel Matthew Rees, October 2024

	MACHINE MC68000	; because there are a lot the assembler can do.
***********************************************************************
*
* Essential control codes
LF	EQU	$0A	; line feed
CR	EQU	$0D	; carriage return
NUL	EQU	0


* Essential BIOS/OS:
*
* We will not be using these:
*GEMDOSTRAP	EQU	1
*GEMprintstr	EQU	9	; PRINT LINE in some docs
*
BIOSTRAP	EQU	13
bconout		EQU	3
devscrkbd	EQU	2

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


*********************************
* Per-process local static area:
*	ORG	$20000	; TOS just gives you what it gives you.
	EVEN
LOCBAS	EQU	*	; here pointer, local static base starts here.
ENTRY	BRA.W	START
	NOP		; buffers to 4 byte even
	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
* A7 will, of course, be our return address stack pointer
* A6 will be our parameter stack pointer
* A5 will be our local static base.
* A4 not yet dedicated, should be preserved if used.

* A0, A1, A2 are assumed volatile in BIOS.
* A3 is used here (volatile).

* D6, D7 are used here (volatile).
* D0, D1, D2 are assumed volatile in BIOS.
* D3, D4, and D5 should be preserved if used.
* 

* In this code, we handle the parameter stack directly.
* Items on the parameter stack
* are assumed to be full natural width (NATWID bytes),
* as a simplifying assumption.

*			  room for something
GAP1	DS.L	58	; to even 256 bytes, not much here yet
*
	DS.L	2	; a little bumper space
SSTKLIM	DS.L	16	; 16 levels of call, max
* 			; 68000 is pre-dec (decrement-before-store) push
SSTKBAS	DS.L	2	; a little bumper space
PSTKLIM	DS.L	32	; roughly 16 levels of call at two parameters per call
PSTKBAS	DS.L	2	; bumper space -- parameter stack is pre-dec


******************************
* Maintain local static area:
INITRT	MOVE.L	(A7)+,A0	; get the return address
	LEA	LOCBAS(PC),A3	; temporary local base (pseudo-DP)
	MOVEM.L	A4-A7,A4SAVE-LOCBAS(A3)	; Store away what the BIOS gives us.
	MOVE	A3,A5			; set up our local base (pseudo-DP)
	LEA	SSTKBAS-LOCBAS(A5),A7	; set up our return stack
	LEA	PSTKBAS-LOCBAS(A5),A6	; set up our parameter stack
	JMP	(A0)		; return via A0


*****************
* Low level I/O:
* This will be a focus point in porting.

* driver level code to output a new line
OUTNWLN	MOVEQ.L	#CR,D7
	BSR.S	OUTCV
	MOVEQ.L	#LF,D7
	BSR.S	OUTCV
	RTS

** Output character on stack, preserving D7
*OUTC	MOVE.L	D7,-(A6)	; Preserve D7
*	CLR.W	D7		; clear character high byte.
*	MOVE.B	NATWID*2-1(A6),D7	; Get the character, high byte cleared
*	BSR.S	OUTCV
*	MOVE.L	(A6),D7		; restore D7
*	ADDQ.L	2*NATWID,A6	; drop saved D7 and character
*	RTS

* OUTC and OUTCV are two entry points to the same routine.
OUTC	CLR.W	D7		; clear character high byte.
	MOVE.B	NATWID-1(A6),D7	; Get the character, high byte cleared
	ADDQ.L	#NATWID,A6	; drop it from parameter stack before falling through.
* Falls through!
* Common low-level hook: call with the character in D7. 
* This bit of glue calls the BIOS.
OUTCV	MOVE.W	D7,-(A7)	; Push the character on A7 where bconout wants it.
	MOVE.W	#devscrkbd,-(A7)	; push the device number
	MOVE.W	#bconout,-(A7)		; push the BIOS routine selector
	TRAP	#BIOSTRAP		; call into the BIOS
	ADDQ.L	#6,A7			; deallocate the BIOS parameters when done
	RTS
* Return from TRAP is RTE,not RTS.
* Can't rob TRAP's return.

* Output NUL-terminated string of bytes.
* Very low-level, no guard rails on length.
* A3 is preserved by Hatari BIOS.
OUTS	MOVE.L	(A6)+,A3	; get the string pointer
OUTSL	CLR.L	D7		; Prepare the high bytes.
	MOVE.B	(A3)+,D7	; get the byte, update the pointer
	BEQ.S	OUTDN		; if NUL, leave before outputting the character.
	BSR.S	OUTCV	; use OUTCV directly
	BRA.S	OUTSL	; next character
OUTDN	RTS


******************************
* intermediate-level library:
*
* 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 ***


************************************
* Start run-time, call program.
* Expects program to define PGSTRT:
*
START	BSR.W	INITRT
	NOP		; place to set breakpoint
*
	BSR.W	PGSTRT
*
DONE	NOP		; place to set breakpoint
	MOVEM.L	A4SAVE-LOCBAS(A5),A4-A7	; restore the monitor's A4-A7
	NOP
	NOP		; landing pad
	NOP
	NOP
* One way to return to the OS or other calling program
	CLR.W	-(A7)	; there should be enough room on the caller's stack
	TRAP	#1	;	quick exit

Save this in the same directory, as under the name the inclusion directive specifies --

rt_rig68K.s

Given those names, you can assemble it with 

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

and go through the processes for simulation on Hatari.

Pulling OUT0 and OUT1 into OUTB8 should be fairly straightforward, but using registers instead of the parameter stack, and then the parameter stack instead of registers might mean you have to think just a tad -- to satisfy yourself that it might be that easy.

And I'm going to leave that as an exercise for the reader. (Is that mean of me? ;-)

[JMR202410141655 addendum:]

LoL. There's a (subtle?) bug in here that I found when testing the INKEY step code. I typed in a few keys, the program properly reported the character and its binary and hexadecimal representations until I had typed quite a few, and -- BOOF!!

I got an alignment violation or address error violation or something.

You don't really notice these kinds bugs unless you execute a piece of code several times. So we wouldn't see it here. But it's one the kinds of bugs I've been warning about as we go.

See if you can find it, either by examining the code or by tracing through it, or by asking yourself, 

What kind of bug is likely to work for a little while, then cause some sort of addressing violation error?

I think I need to go back and add a deliberate debugging example, including an example debugging this one. And also a chapter introducing the sort of debugging tool that would make this bug easier to swat.

[JMR202410251124 note: So it turns out there are more than one bug, and it was kind of fortuitous that the address violation bug happened. See the debugging sessions.]

[JMR202410141655 addendum END.]

Does getting hexadecimal output on the 6800 sound like a good next project? 

Yes?

I'm glad you think so. Let's do it.


(Title Page/Index)

 

 

 

 

ALPP 03-03 -- Binary Output on the 6809, Left-to-right; Framework-by-include (LWTools)

Binary Output on the 6809,
Left-to-right;
Framework-by-include
(LWTools)

(Title Page/Index)

 

We've done binary output on the 6801, so let's do it on the 6809. Sure it's getting monotonous, but that's what it takes to understand the differences between the processors. More to the point, I don't know any other way to explain the discipline I'm using, except in steps.

The binary output routines in their less-optimized form don't make a lot of obvious use of the 6809's unique features; at least, they don't stand out until you consider how things are being done directly by the 6809's native instructions rather than subroutines:
* simple 8-bit binary output for 6809
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
	OPT	6809
	INCLUDE	rt_rig6809.asm
****************
* Program code:
*
* Output a 0
OUT0	LDB	#'0
OUT01	PSHU	D
	LBSR	OUTC
	RTS
*
* Output a 1 
OUT1	LDB	#'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	LDB	#8	; 8 bits
	STB	0,U	; Borrow the upper byte of the parameter.
OUTB8L	LSL	1,U	; Get the leftmost bit of the lower byte.
	BCS	OUTB81
OUTB80	BSR	OUT0
	BRA	OUTB8D
OUTB81	BSR	OUT1
OUTB8D	DEC	,U
	BNE	OUTB8L	; loop if not Zero
	LEAU	2,U	; drop parameter bytes
	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	LEAX	HEADLN,PCR
	PSHU	X
	LBSR	OUTS
	LDD	#$5A	; byte to output
	PSHU	D
	LBSR	OUTB8
	LBSR	OUTNWLN
	RTS
*
	END	ENTRY

You'll want to save that as something like

outb8_6809.asm

You'll note that the inclusion directive is now INCLUDE, not EXP. We won't be using asm68c on the 6809 source. I never got that far, and Lost Wizard does a really good job with LWTools, anyway. 

(William Astle kept OPT as a synonym for PRAGMA, to help compatibility with other assemblers.)

As was somewhat the case with the 6801, the framework rigging makes much more clear use of the 6809's features:

* A simple run-time framework inclusion for 6809
* 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	$2000	; MDOS says this is a good place for usr stuff
	SETDP	$20	; some other assemblers
*	SETDP	$2000	; EXORsim
LOCBAS	EQU	*	; here pointer, local static base starts here.
ENTRY	JMP	START
	NOP
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
*			  room for something
* Not much here
*
	SETDP	0	; Not yet set up
	ORG	$2100	; Give the DP room.
	RMB	2	; a little bumper space
SSTKLIM	RMB	31	; 16 levels of call, max
*			; 6809 is pre-dec (decrement-before-store) push
SSTKBAS	RMB	2	; a little bumper space
PSTKLIM	RMB	64	; 16 levels of call at two parameters per call
PSTKBAS	RMB	2	; bumper space -- parameter stack is pre-dec
*
*
INITRT	TFR	DP,A
	CLRB
	TFR	D,Y		; save old DP base for a moment
	LEAX	LOCBAS,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
*
*
OUTNWLN	LDA	#CR	; driver level code to output a new line
	BSR	OUTCV
	LDA	#LF
	BSR	OUTCV
	RTS
*
* No need for PPOPD, PPUSHD, PPOPX, or PPUSHX in 6809 code.
*
* We can handle the parameter stack directly.
* Preserves B.
OUTC	LDA	1,U	; Get the character in A where XOUTCH wants it.
	LEAU	2,U	; Got it in A, now drop it from the stack.
	BSR	OUTCV	; output via monitor ROM
	RTS
*
OUTCV	JMP	XOUTCH	; driver code for outputting a character
*
* And we can handle the parameter stack directly here, too.
OUTS	PULU	X	; get the string pointer
OUTSL	LDA	,X+	; get the byte, update the pointer
	BEQ	OUTDN	; if NUL, leave
	BSR	OUTCV	; use the same call OUTC uses.
	BRA	OUTSL	; next character
OUTDN	RTS
*
*
******************************
* intermediate-level library:
*
* 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.
*
*
************************************
* Start run-time, call program.
* Expects program to define PGSTRT:
*
START	LBSR	INITRT
*
	LBSR	PGSTRT
*
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		; remember to set a breakpoint here!
	NOP		; landing pad
	NOP
	NOP
	JMP	[$FFFE]	; alternatively, jmp through reset vector
*
* 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 this in the same directory as

rt_rig6809.asm

since that's what the inclusion directive says it is.

Since asm68c won't assemble that, we'll need something else. I recommend William Astle's very professional LWTools, as I mentioned above, found 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 the executables 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. 

You can use the command line

$ lwasm outb8_6809.asm

to assemble the code, and the object code, if there were no errors in copying, will be saved as 

a.out

in a format that EXORsim doesn't recognize. And you won't get a listing. To get s1-s9 (srec) object code and listing to the terminal, use

$ lwasm --format=srec -o outb8_6809.s19 --list outb8_6809.asm

I find it useful to put the symbol table in the listing with the "-s" option and save the listing to a file, so I use the command line

$ lwasm --format=srec -o outb8_6809.s19 --list=outb8_6809.list -s outb8_6809.asm

If you get a clean assembly, you can open the object file 

outb8_6809.s19

in a text editor and select ...

Okay, this is a little tricky. EXORsim doesn't accept the S0 header record (first line) or the S5 count record (second to last line), so we have to delete those lines before we can select all of the rest to paste it in. 

No big deal, right?

Again, you may have to hit return again at the end, to bring it back to the % prompt:

$ ./exor09 --mon
Load facts file 'facts09'
'exbug09.bin' loaded.
  EXBUG09-2.1 detected
'mdos09.dsk' opened for drive 0 (double sided)

OSLOAD...

Hit Ctrl-C for simulator command line.  Starting simulation...

>         0 A=00 B=00 X=0000 Y=0000 U=0000 S=00FF P=00 --------            0020: 86 10        LDA #$10                   

6809 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% l 
S10720007E21AF1278
S10D21661FB85F1F02308DFE911FA9
S1132170101F8B109F06338CEB351010DF04328C4C
S1132180A06E84860D8D0C860A8D0839A6413342D3
S11321908D01397EF0183710A68027048DF520F8BC
S11321A039EC42E3C1EDC439EC42A3C1EDC43917A3
S11321B0FFB417004E10DE04DC061F8B121212123D
S11321C06E9FFFFEC630360617FFC139C63120F6B2
S11321D0C608E740684125048DEA20028DEE6AC4F2
S11321E026F23342390D0A4F757470757474696E32
S11321F0672024354120696E2062696E6172793AE4
S11322000D0A00308CDF361017FF8BCC005A3606CF
S10A221017FFBD17FF6D3934
S9032000DC
PC set to 2000
181 bytes loaded
No checksum errors.
% 

 Refer to the listing file, look around, step through a bit, set a breakpoint, and run it:

% u 21AF
21AF: 17 FFB4             LBSR $2166
21B2: 17 004E             LBSR $2203
21B5: 10DE 04             LDS $04
21B8: DC 06               LDD $06
21BA: 1F 8B               TFR A,DP
21BC: 12                  NOP 
21BD: 12                  NOP 
21BE: 12                  NOP 
21BF: 12                  NOP 
21C0: 6E 9F FFFE           JMP [$fffe]
21C4: C6 30               LDB #$30
21C6: 36                  PSHU B,A
21C8: 17 FFC1             LBSR $218c
21CB: 39                  RTS 
21CC: C6 31               LDB #$31
21CE: 20 F6               BRA $21c6
21D0: C6 08               LDB #$08
21D2: E7 40               STB 0,U
21D4: 68 41               ASL 1,U
21D6: 25 04               BCS $21dc
21D8: 8D EA               BSR $21c4
21DA: 20 02               BRA $21de
% b 21BD
Breakpoint set at 21BD
% s 2000

          0 A=00 B=00 X=0000 Y=0000 U=0000 S=00FF P=00 --------            2000: 7E 21AF      JMP $21af                  
>         1 A=00 B=00 X=0000 Y=0000 U=0000 S=00FF P=00 --------            21AF: 17 FFB4      LBSR $2166                 

6809 Monitor: Ctrl-C to exit, 'c' to continue, or type 'help'
% r
PC=21AF A=00 B=00 X=0000 Y=0000 U=0000 SP=FF DP=00 CC=00
% c 


Breakpoint!

Outputting $5A in binary:
01011010
       1323 A=00 B=00 X=2203 Y=0000 U=2164 S=00FF P=00 -----Z--            21BC: 12           NOP                        
>      1324 A=00 B=00 X=2203 Y=0000 U=2164 S=00FF P=00 -----Z--            21BD: 12           NOP                        

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

The same optimization that we made for the 6800 and 6801 will work here. except you have to edit LDAA to LDA, etc., remove the references to PSP and trade them for ,U, etc. I should leave that for you to figure out, but, maybe not:

* 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	LDB	#8	; 8 bits
	STB	,U	; Borrow the upper byte of the parameter.
OUTB8L	LSL	1,U	; Get the leftmost bit of the lower byte.
	BCS	OUTB81
OUTB80	LDA	#'0
	BRA	OUTB8B
OUTB81	LDA	#'1
OUTB8B	JSR	OUTCV
	DEC	,U
	BNE	OUTB8L	; loop if not Zero
	LEAU	2,U	; drop parameter bytes
	RTS

Doesn't having U to reference the parameter stack and make references to parameters (and variables) clear without having to dodge the return address and look through stack frames make it so much easier to see what you're doing?

I'm getting excited again. Sorry.

Stack frames. I guess I'll have to show you how those work, but not yet.

The 68000 code should be just different enough that we don't fall asleep getting there. 

(Title Page/Index)

 

 

 

 

Monday, October 7, 2024

ALPP 03-02 -- Binary Output on the 6801, Left-to-right; Framework-by-include

Binary Output on the 6801,
Left-to-right;
Framework-by-include

(Title Page/Index)

 

We've done binary output on the 6800, installing a new separate assembler in the process, and and learning a little about how to use inclusion files in the process. 

Time to do it again on the 6801. Go ahead and check the changes in the code to optimize it for the 6801, and review the process for using my asm68c assembler. 

There are not a lot of changes in the code. The most visible might be the ability to save X off to the return address stack instead of temporaries in the direct page. After that is just being able to load and store 16 bits at a time through the D double accumulator, including immediate 16-bit values.

One thing essential is to enable the 6801 op-codes for asm68c. That is done by the 

OPT 6801

in the line before the one including the framework rigging with the EXP pseudo-op.

It's tempting to use this as an opportunity to clean up the binary output routine, but it's clean enough and I don't want to confuse you about reasons why the code should change. So this chapter will end up as primarily practice on the older simulator in EXORsim6801.

* simple 8-bit binary output for 6801
* using parameter stack,
* with test frame
* Joel Matthew Rees, October 2024
*
	OPT	6801
	EXP	rt_rig6801.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
	LDD	#$5A	; byte to output
	JSR	PPSHD
	JSR	OUTB8
	JSR	OUTNWLN
	RTS
*
	END	ENTRY
The framework rigging makes a bit more use of the 6801:
* A simple run-time framework inclusion for 6801
* 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
*
SCRLIN	EQU	$40	; More than a screen full of lines.
*
* 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
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
	PULX		; pop return address to X
	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	; get value for X
	PSHX		; push it out of the way
	LDX	PSP
	INX
	INX
	STX	PSP
	PULX		; and bring it back.
	RTS
*
* Trashes A,B;
* X points to X value just pushed -- PSP top of stack -- at end
PPSHX	PSHX	; push X out of the way
	PULA
	PULB	; Falls through:
*
* X points to PSP top of stack at end
PPSHD	LDX	PSP
	DEX
	DEX
	STX	PSP
	STD	0,X
	RTS
*
* X points to PSP top of stack at end
PPOPD	LDX	PSP
	LDD	0,X
	INX
	INX
	STX	PSP
	RTS
*
* Keep this around in comments, 
* for reference for when we want to referance tables in code.
* 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
*
WAKETRM	LDAB	#SCRLIN
WAKETML	JSR	OUTNWLN
	DECB
	BNE	WAKETML
	RTS
*
******************************
* intermediate-level library:
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit sum
ADD16	LDX	PSP
	LDD	2,X	; left
	ADDD	0,X	; right
	STD	2,X	; sum
	INX		; adjust parameter stack
	INX
	STX	PSP
	RTS
*
* input parameters:
*   16-bit left, right
* output parameter:
*   16-bit difference
SUB16	LDX	PSP
	LDD	2,X	; left
	SUBD	0,X	; right
	STD	2,X	; difference
	INX		; adjust parameter stack
	INX
	STX	PSP
	RTS
*
*
************************************
* Start run-time, call program.
* Expects program to define PGSTRT:
*
START	JSR	INITRT
	JSR	WAKETRM
*
	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.

Why so little use of the 6801's improvements?

This one doesn't need the extensions so much, but, really, allowing us to work with the X register without saving it to static temporaries is one of those small things that is huge for controllers that have to make heavy use of interrupts.

Save it as

rt_rig6801.asm

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

outb8_6801.asm

Then use the command line

asm68c -l2 outb8_6801.asm

 to assemble the code. 

If the listing doesn't seem to show obvious errors, use 

asm68c -l2 outb8_6801.asm > outb8_6801.list

to redirect the listing, and open the listing and the S1/S9 object .

outb8_6800.x

in a text editor. 

Remember to fix any errors in the assembly language source file, not the listing file!

Get a session of EXORsim6801 running in another terminal window with the --6801 option with

exor --6801 --mon

or 

./exor --6801 --mon

As with the 6800 session, at the EXORsim monitor's % prompt, hit the "l" (for "load") and hit return, and it will wait for you to paste the s1/s9 object code in. Select it all and copy and paste. Hit return one more time for good measure. This time, however, you have to hit CTRL-C to break out of the load mode.

You may have to hit return again at the end, to bring it back to the % prompt:

Type 'help'
% l
S10700807E212101B7
S109008400000000000072
S105008A000070
S11320007E2121010000000000000000000000000B
S113201000000000000000000000000000000000BC
S113202000000000000000000000000000000000AC
S1132030000000000000000000000000000000009C
S109204000000000000096
S11320460000000000000000000000000000000086
S11320560000000000000000000000000000000076
S11320660000000000000000000000000000000066
S11320760000000000000000000000000000000056
S11320860000000000000000000000000000000046
S11320960000000000000000000000000000000036
S10720A60000000032
S11320AACE20A8DF84CE2000DF86389F8A8E206562
S10520BA6E00B2
S11020BCDE84EE003CDE840808DF84383941
S10620C93C32336F
S10C20CCDE840909DF84ED00390A
S10C20D5DE84EC000808DF843904
S10C20DE860D8D0C860A8D08396B
S10A20E7BD20D5178D01395E
S10620EE7EF01865
S11020F1BD20BCA60027058DF40820F7399A
S10C20FEC640BD20DE5A26FA3961
S1102107DE84EC02E300ED020808DF8439F9
S1102114DE84EC02A300ED020808DF84392C
S1092121BD20AABD20FE52
S1062127BD21795A
S10E212A9E8A01010101FEFFFE6E0011
S10C2135C630BD20CCBD20E73901
S107213EC63120F58D
S1132142DE84C608E700680125048DE720028DECD1
S10C21526A0026F20808DF843952
S113215B0D0A4F757470757474696E67202435415C
S111216B20696E2062696E6172793A0D0A0075
S1132179CE215BBD20C9BD20F1CC005ABD20CCBD08
S10921892142BD20DE39F5
S903218F4C
PC set to 218f

Unknown record on line 41
% 

Again, unassemble the code, get memory dumps, and otherwise and satisfy yourself it's all there. Then step through it and do any necessary debugging:

% r
PC=218F A=00 B=00 X=0000 SP=FF8A CC=C0
% s 80
          0 A=00 B=00 X=0000 SP=FF8A ------          0080: 7E 21 21 JMP 2121  EA=2121        
>         1 A=00 B=00 X=0000 SP=FF8A ------          2121: BD 20 AA JSR 20AA                 


Type 'help'
% c


[... big empty gap ...]


Outputting $5A in binary:
01011010
:9
EXBUG 1.1

This time, instead of a pause, I've had the framework load the reset vector and jump in, which will start EXBUG and mess up what you have in memory. If you don't want that, copy the pause code in, or simply set a breakpoint and scroll back until you confirm that you got the binary output expected.

I'll note I had trouble getting the terminal's attention. If you can't get the terminal to respond, leave me a note. If enough people leave me a notes about it, I'll see about bringing the 6801 simulator into a fork of Joe's current code or something.

For the record, the minor optimization I mentioned at the end of the 6800 version of this can be brought straight into this code without change, of course, but doesn't benefit from the 6801's extensions. Give it at try. Copy the optimized OUTB8 over the OUT0, OUT1, and OUTB8 code above and test it.

One more thing I might mention is the 6801's LSLD and LSRD instructions that shift the whole 16-bit wide Double accumulator left or right one bit in one instruction. If you don't need the Zero flag, they are equivalent to an 8-bit SHift followed by a ROtate in the same direction, only saving 1 byte and 1 cycle. If you do need the Z flag, then, of course, it saves you some additional testing and branching, as I have mentioned about ADDs and SUBtracts on the 6800 vs. 6801. 

As another optional exercise, you might want to try using the 16-bit shifts to output 16 bits at a time. As another, you might want to try adding a parameter to insert an arbitrary character, say a colon or a space, between each 8-bit half, and consider whether it would be easier or clearer to do it 16 bits at a time or use the existing 8-bit routines to do the top half first and then the bottom.

Doing this all over on the 6809 may get old, but we do want to see how much of the 6809's fancy stuff we can use. 


(Title Page/Index)

 

 

 

 

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 LWTools:  https://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)