Showing posts with label scratch registers. Show all posts
Showing posts with label scratch registers. Show all posts

Sunday, September 29, 2024

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

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

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

(Title Page/Index)

 

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

Parameters. 

(Wall of text warning!)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

 

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

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

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

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

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

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

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

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

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

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




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


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

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

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

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

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

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

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








(Title Page/Index)

 

 

Saturday, September 28, 2024

ALPP 02-07 -- A Note on Byte Widening and Scratch Registers on the 6800, 6801, and 6809

A Note on Byte Widening and Scratch Registers
on the 6800, 6801, and 6809

(Title Page/Index)

 

I briefly mentioned, in the introduction to byte arithmetic chapter on the 6800, 6801, and 6809, the necessity of expanding byte data to 16-bit to use the 6801 and 6809's ADDD and SUBD instructions. I gave a more concrete example of byte widening in the 68000's chapter on byte arithmetic that we just finished.

In the 68000, the data registers are plenty wide enough. All we need to do, in the case of unsigned byte widening, is to make sure that, before we load the byte, enough of the register is cleared out to hold the target width.

In the case of signed byte widening for the 68000, we can use the sign-EXTend instruction after loading the byte value, which we will take a look at later.

(Other 16-bit and wider CPUs have load instructions that automatically sign-extend or zero-extend registers on load, instead.)

Let's look at how we can widen a byte on the 6800.

Hmm? What? The 6800 doesn't have a 16-bit add, you say?

Well, yeah, but, (mutter, mumble, ...) actually, when we cleared A out before we used ADCA #0 and SBCA #0, we were effectively performing a byte widening. We just used an immediate zero byte as the high byte of the widened source.

Why are you looking at me like that?

Sigh. Okay, remember, add with carry (ADC) allows us to expand the width of the ADD instruction. So we can synthesize a sixteen-bit add quite easily:

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

And there's your 16-bit add. Just takes 6 instructions. >:-)

Comparing that to the 6801 and 6809, same declarations as above:

NLFT	FDB	132	; 132 in two bytes, high byte zero
NRT	FDB	188	; 188 in two bytes, high byte zero
RES	RMB	2	; 2-byte result
	...
	LDD	NLFT	; Get the left 16-bit word.
	ADDD	NRT	; Add the right 16-bit word.
	STD	RES	; Store the result 16-bit word.

3 instructions.

Okay, it takes a few more than 6 on the 6800 if we need to get the flags perfectly right, but we don't usually need to get the flags perfect for addition. Hardly ever, really. The Carry is correct, and so is the Negative. The Zero only reflects the high byte, which is wrong, but we have ways to handle that. So we're good for most purposes on the 6800 -- for addition. 

If we need to test for zero results, we can check that Carry is clear first, then OR the result low and high bytes together. If carry was set, we know the result was $10000 or greater, so, non-zero. ORing the low and high bytes together will leave non-zero if either was non-zero, so that completes the test. (I can make it sound simple, right? It's simple here because we have two copies of the result, one in memory and one in the accumulators. There is more to this, but I don't want to get too distracted. We'll come back to it.)

And in another point of fact, because loads and stores don't affect the carry flag, we could do all that math in 6 instructions just using a single accumulator instead of using both.

So, let's take that as a jumping-off point, declare the values we're adding as bytes again, and expand before adding. We'll start by declaring a byte of scratch RAM,  preferably in the direct page:

SCRTCH	RMB	1	; preferably in DP
	...
NLFT	FCB	132	; 132 in one byte
NRT	FCB	188	; 188 in one byte
RES	RMB	2	; 2-byte result
	...
	CLRA		; A high byte of zero
	STAA	SCRTCH	; Another high byte of zero
	LDAB	NLFT	; Get the left byte.
	ADDB	NRT	; Add the right byte.
	ADCA	SCRTCH	; Add the widening bytes.
	STAB	RES+1	; Store the result low byte.
	STAA	RES	; Store the result high byte.

So, do you believe me now?

Let's look at that on the 6801 (and 6809, by translating LDAB to LDB):

SCRTCH	RMB	2	; preferably in DP
	...
NLFT	FCB	132	; 132 in one byte
NRT	FCB	188	; 188 in one byte
RES	RMB	2	; 2-byte result
	...
	CLRA		; A high byte of zero
	LDAB	NRT
	STD	SCRTCH	; widened 16-bit word
	LDAB	NLFT	; Get the left byte, A still clear.
	ADDD	SCRTCH	; Add the widened 16-bit word.
	STD	RES	; Store the result high byte.

Because the D register operators need 16-bit operands, widening for the 6801 and 6809 requires two bytes of scratch RAM instead of one -- unless you're doing the math one byte at  a time, in which case you can still use the immediate zero argument to ADC as the source.

The above sources are slightly incomplete, so you might need to edit them a little to check what I am saying. Not much, but a little.

Subtraction is pretty similar, although we do care more about the flags after subtraction. 

By widening before we add or subtract, the addition and subtraction sequences can be made quite parallel. You might want to simply substitute SUBtract for ADD and SuBtract with Carry for ADd with Carry in the above code to see what happens.

I keep trying to go a bit further on the flags and such, but that is a distraction.

One thing that I need to make clear before we go, where the 68000 with lots of accumulator-equivalet data registers shows how natural byte widening is, having only one 16-bit accumulator makes byte widening on the 6801 and 6809 use scratch registers.

And I'm thinking we can now, after you've played with the subtraction code, move forward to doing proper 16-bit arithmetic. 

>:->


(Title Page/Index)