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)

 

No comments:

Post a Comment