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)

 

 

No comments:

Post a Comment