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)

 

 

 

 

No comments:

Post a Comment