6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Sat Nov 23, 2024 7:28 am

All times are UTC




Post new topic Reply to topic  [ 8 posts ] 
Author Message
PostPosted: Sat Sep 24, 2022 7:11 pm 
Offline

Joined: Wed Jun 29, 2022 2:15 am
Posts: 44
Over on http://forum.6502.org/viewtopic.php?f=2&t=7304 I asked whether syntax would have more assembly coders think of zero page as registers instead of byte-saving global. That %Rnnn syntax led me to another syntax-based solution to another common 6502 coding question, how best to pass parameters to subroutines.

The 1980s and 1990s CPUs mostly solved this with a addressing mode relative to the stack pointer. FORTH implementation in the 8-bit CPUs often manage a second “data stack”. A zero page pointer is the obvious place to do this on the 6502.

I came up with something inspired by that, but simper. My solution is a new syntax in the assembler. The general form is %%n.mm, and the mapping from that to addresses is inspired by the $Cn00 memory layout on the Apple II.

%%n where n =0-7, with the lowest level subroutines using 7, the next lowest 6, then 5, and the top-level application using 0. (I’ve yet to use 2, 3, or 4 so 8 layers is probably too many).

%%n.mm where mm is a decimal values, 1-15. I use the shortcut %%n to refer to %%n.0

For example, my PrintString low-level subroutine expects the pointer to the string in %%7, as a two-byte address. CompareString has two parameters, %%7 and %%7.2, both two byte addresses.

In my Apple II4 project, I map this syntax to start at $D000 (after the I/O space and after the screen buffers), but the assembler can map in anywhere in memory.

The result is basically a manual data stack. Yes, it seems to waste space, but on a typical stack you waste the yet-used space where I’m wasting the in-between space. The benefit is that I know exactly what address the parameters for every subroutine should be, making debugging a lot simpler.

Plus there is no push or pull and thus no way to get into a state where the return address or current parameters are not where they are expected to be. The stack pointer just deals with JSR/RTS and the parameters only get set, never unset.

Plus, every so often my code makes a sequence of low-level subroutine calls and as the first parameter to all of those is always %%7, in those cases I can just set %%7 once.

The result of all this feels a lot like a manual version of what a C compiler does, minus the pushing and pulling. I’m manually looking up the list of parameters and %%addresses of the subroutine I’m calling, mustering the parameters in place, then JSR. Then inside any subroutine it’s just LDA %%n.mm to grab the value.

And with the manually set “levels” there is never a conflict.

Anyone have a similar solution? Or something better?


Top
 Profile  
Reply with quote  
PostPosted: Sat Sep 24, 2022 7:28 pm 
Online
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8544
Location: Southern California
It sounds like you'd run into the problem that a subroutine doesn't know how many levels of subroutines deep it is, meaning it wouldn't know which set of address variables to use. One time it's called, it might be only the second one down, and another time, it might be the fourth one, etc.. You mention Forth's data stack. You don't have to use Forth to use a ZP data stack though, and it can prevent a lot of problems. I address this in my treatise of 6502 stacks (plural, not just the page-1 hardware stack), starting especially in section 4, "Virtual stacks and various ways to implement them," then section 5, "Stack addressing, both hardware and virtual, plus tricks," section 6, "Parameter-passing methods," section 14, "Local variables & environments," and section 15, "Recursion."

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
PostPosted: Sun Sep 25, 2022 4:23 pm 
Offline

Joined: Wed Jun 29, 2022 2:15 am
Posts: 44
GARTHWILSON wrote:
[color=#000000]It sounds like you'd run into the problem that a subroutine doesn't know how many levels of subroutines deep it is, meaning it wouldn't know which set of address variables to use. One time it's called, it might be only the second one down, and another time, it might be the fourth one, etc..

I’m not setting the levels at runtime, I’m setting them in the architecture of the code at assembly time.

Or in other words, I know which subroutines are the lowest level, as they don’t call any others. Those use %%7. PrintChar, ClearScreen, CompareStrings, etc. The next level up can call “down” to that level or within the same level, and thus are assigned %%6. In my system that is FileOpen, FileRead, FileClose, etc.

I’m using %%0 at the top level of the command line, with a Unix-like ARGN as %%0, the name of the command as %%0.1, and the other parameters as %%0.n. That then leaves the %%1 level for subroutines that implement the commands. I’ve yet to need %%2-5 but only because my programs so far have been relatively simple.

Yes, I read your post on stacks, and it inspired this simpler, manual stack.

Mine is unarguable less flexible but makes debugging easier and faster. I’m a long-time C coder and jumping back to assembly I’m often debugging one char errors like a missing # or $ or before this %% syntax, a subroutine stomping on a memory location.


Top
 Profile  
Reply with quote  
PostPosted: Sun Sep 25, 2022 7:12 pm 
Online
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8544
Location: Southern California
Ah, so you're kind of going at it the opposite direction from what I was thinking. Still, you'd have to reserve the maximum number of bytes each level might require, because there's no chance of re-assigning it at runtime like a stack does continually. That permanently assigned memory becomes kind of like a toothbrush which is only for one person even though it mostly sits unused. Later updates could cause a lot of bugs, where for example you find you need to expand a routine and make it call another level down which wasn't originally expected, so now it could be level 3 instead of level 2. Things get shuffled around and somewhere a loose end will slip your attention and one routine will be stepping on the variables of another. You're free to do it any way you like of course, but I don't think you're making debugging easier or faster at all. I've been down that road, and later found I could have made life a lot easier for myself if I had known back then what I know now about stacks. There is however a related thing in Forth which is the scratchpad area of about eight bytes, called N; but it's only used by primitives (ie, words defined in assembly language), and each primitive that uses it must be completely finished with it when it exits, and N is never used to pass parameters. Almost zero primitives have any JSRs in them either.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
PostPosted: Sun Sep 25, 2022 8:28 pm 
Offline

Joined: Sun Nov 08, 2009 1:56 am
Posts: 411
Location: Minnesota
It's been a while, but I think I did something similar for a UCSD p-code interpreter. There was dedicated zero-page scratch area, two 16-bit data and two 16-bit addr (which was almost all I ever needed - except integer math needed at least one more 16-bit data), that any subroutine could use as long as it "knew" that area could be trashed if it called another subroutine. That could be used for passing parameters as well, and if more were needed, they could be passed on the stack and removed to a "safe" area by the callee.

The lowest Level-0 routines were not allowed to call anything while active. Level-1 could call Level-0 (and would, else they would not be Level-1). Level-2 could call Level-1 and Level-0. Basically the idea was that at any level calls could be made to any lower level, but never upward. The collection of these made up the layer that separated the virtual p-machine from the hardware it was running on. I think the levels topped out at Level-3, but I seem to recall thinking that any more were going to be increasing difficult to manage. But that may have been partly because it getting harder to find "private" zero-page memory both protected from lower level routines and small enough to not miss when not in use.


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 07, 2022 2:56 am 
Offline

Joined: Wed Jun 29, 2022 2:15 am
Posts: 44
GARTHWILSON wrote:
[color=#000000]Still, you'd have to reserve the maximum number of bytes each level might require, because there's no chance of re-assigning it at runtime like a stack does continually.

Yes. But with a typical stack you have to reserve the maximum number of bytes you think you'll need, as pushing past the end (or wrapping) is usually catastrophic.

So it's really just another version of optimizing compile time vs. runtime. Your choice probably uses less memory, but remember, I have a 24-bit 6502 with plenty of memory. My pseudo-stack uses $D000-$D7FF to cover eight levels of parameters, 256 bytes per level. That is huge compared to the 256 bytes of the JSR/RTS stack, and given I'm only using four levels I'm wasting 4x more than that built-in stack, but it's 2K of the 256K of my system and thus less than 1%.

And again, the tradeoff is that I know at all times where a parameter is supposed to be. It has saved me hours of debugging by looking at the listing, dumping the address, and seeing that the value isn't where it should be.

GARTHWILSON wrote:
[color=#000000] Later updates could cause a lot of bugs, where for example you find you need to expand a routine and make it call another level down which wasn't originally expected, so now it could be level 3 instead of level 2.

True, but with 256 bytes per level, so far there is always plenty of extra space. So worst case I could add a half level, putting the parameters of the squeezed-in subroutine as %%n.127, %%n.126, ... downward instead of upward, highly unlikely to clash with the subroutine making the call unless it expects dozens of parameters.


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 07, 2022 4:03 am 
Online
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8544
Location: Southern California
65LUN02 wrote:
GARTHWILSON wrote:
Still, you'd have to reserve the maximum number of bytes each level might require, because there's no chance of re-assigning it at runtime like a stack does continually.

Yes. But with a typical stack you have to reserve the maximum number of bytes you think you'll need, as pushing past the end (or wrapping) is usually catastrophic.

Not at all. There is no such trouble in Forth, and you can do things the same way in assembly without using Forth. Any routine can use a number of stack cells or bytes that's different each time it is called. It also does not need to return the same number as it received as parameters, as long as the caller and callee agree on either the number of bytes or cells passed in each direction, or a method to communicate how many, in situations where it may vary.

Quote:
So it's really just another version of optimizing compile time vs. runtime. Your choice probably uses less memory, but remember, I have a 24-bit 6502 with plenty of memory.

Having memory to burn does have its advantages.

Quote:
And again, the tradeoff is that I know at all times where a parameter is supposed to be. It has saved me hours of debugging by looking at the listing, dumping the address, and seeing that the value isn't where it should be.

If you're debugging and do a dump, the stack pointer value should be part of that. Picture a ship, and you know the propeller or the keel or other part are so many feet below the waterline. As long as the water is deep enough to avoid hitting the ocean floor, you don't care how deep it is. Same with a stack. You don't have to know how deep the stack is when you're in a particular routine. You know that certain data are "so many feet below the water line," to make the analogy. The "waterline" is the stack pointer, and that's the reference you go by, regardless of how deep the stack is. You'll know a parameter is, for example, 8 bytes deep in the stack, regardless of how many bytes other pending routines have reserved, and whether it's different each time the routine runs. You don't have to know that it's 3145 feet above the ocean floor one time and 511 feet another time and 16,239 feet another time.

You are, of course, free to do things any way you like; and sometimes we do things in an inefficient way for legitimate reasons, like because we just haven't yet dived into a more-efficient way, like using discrete logic if we haven't gotten into programmable logic yet, or even for the fun of it, like storing data on audio tape, or for historical reasons, like wanting to limit oneself to the NMOS 6502 instruction set. It is clear however that you are not yet familiar with the power and flexibility of stacks. I would encourage re-reading, and maybe reading yet again, the 6502 stacks treatise. I'm sure it leaves room for improvement to make it more understandable; but I know it's quite long, but I struggle to see what I could remove. Videos would undoubtedly help. Maybe someday I'll get up to speed on that.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 07, 2022 7:17 am 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8507
Location: Midwestern USA
65LUN02 wrote:
That %Rnnn syntax led me to another syntax-based solution to another common 6502 coding question, how best to pass parameters to subroutines.

Here's how I do it in one of my programming library functions.

The getkey() function accepts a keystroke that is optionally filtered for certain parts of the ASCII “alphabet.” The data needed for filtering can be in a structured table somewhere in RAM or can be in a table formed from stack pushes immediately before the call. In either case, the size of the filter table is variable to 127 maximum entries, which means, of course, this function cannot know in advance how many parameters it will be processing or how much stack space will be used for any given call.

Incidentally, this function also includes somewhat-arcane code that causes keyboard input to time out in the event the user takes no action. It makes use of the alarm() function in the firmware of my POC units. Also, the advantages of using the 65C816 instead of the 65C02 become quite clear with this sort of application. :D

Attachment:
getkey.asm [15.67 KiB]
Downloaded 40 times

_________________
x86?  We ain't got no x86.  We don't NEED no stinking x86!


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 8 posts ] 

All times are UTC


Who is online

Users browsing this forum: No registered users and 11 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: