6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Sun Nov 24, 2024 6:06 am

All times are UTC




Post new topic Reply to topic  [ 10 posts ] 
Author Message
PostPosted: Sun Jun 20, 2010 6:40 pm 
Offline

Joined: Sun Jun 20, 2010 6:31 pm
Posts: 7
What's the preferred method of passing parameters to assembly subroutines?

I thought of using the stack but since the subroutine address is pushed there too, I don't that that'll work. Reserving memory locations just for different subroutines seemed wasteful. I thought of making my own kind of stack for my data, but it seemed a bit overkill.

I'm new to the 6502 and assembly programming in general, having spent my career writing in C++ and high level languages. It certainly requires a totally different mindset to work at the CPU level :)


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Sun Jun 20, 2010 7:08 pm 
Offline

Joined: Sat Jan 04, 2003 10:03 pm
Posts: 1706
The 65816 is quite adept at passing parameters on the stack.

The 6502, however, not so much. It can be done, but it's a lot of work. Most 6502 OSes use "control blocks," which are basically parameter blocks in arbitrary memory which you then pass an address to in CPU registers or dedicated zero-page locations (e.g., GEOS, Atari's 8-bit OS, etc). The advantage of this is that you control how and where these blocks are allocated; the routine doesn't care. But, once it has an address, it's free to access the structure.

An additional benefit of this style of parameter passing is that you can embed parameter blocks inline to your code:

Code:
jsr I_MyRoutine
.db parametersGoHere
...
jsr nextInstructionHere


I_MyRoutine would then grab the return address off the stack, fix up the return address so that RTS does the right thing, then jumps to MyRoutine (note lack of I_ prefix) with the parameter block address in tow. GEOS uses this technique a lot.

Hope this helps.


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Sun Jun 20, 2010 7:29 pm 
Offline

Joined: Sun Jun 20, 2010 6:31 pm
Posts: 7
Hi kc5tja! Thanks for your reply. I'm really interested in ideas for 6502 since I'd like to work with NES-level hardware.

I'm afraid I don't quite 100% understand the idea of control blocks. A control block be just a section of memory reserved for parameters passing? And used by many routines or is just unique to that one routine?

In your example, would I call I_MyRoutine or MyRoutine?

Could you give another example, maybe something a little more concrete?

Thanks!
Shawn


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Mon Jun 21, 2010 12:35 am 
Offline

Joined: Sat Jan 04, 2003 10:03 pm
Posts: 1706
Let's start with the basics -- the concept of parameter versus control blocks.

I'm going to assume you're familiar with DOS or Unix, where you have a function available "open" to a file.

Code:
/* In C syntax, you might see something like this: */

int fileHandle;

fileHandle = open("/tmp/whatever", O_READONLY);
if(fileHandle == -1)
  perror("Cannot open file because: ");


We might express this in 6502 assembly language like so:

Code:
  LDA #<openParams
  LDX #>openParams
  JSR _open
  STA fileHandle
  BPL 1$
  LDA #<errorMsg
  LDX #>errorMsg
  JSR _perror
1$:
  ; ...etc...

fileHandle:
  .db 0

openParams:
  .dw filename, O_READONLY

errorMsg:
  .db "Couldn't open file because: ",0

filename:
  .db "/tmp/whatever",0


In this case, the data structure defined by "openParams" is a "parameter block." It's used to pass inputs (and sometimes outputs for sophisticated-enough routines) to the library function you're calling. Note that parameter blocks look just like stack frames in C, except that they don't reside in the stack. They can sit anywhere in memory, and can even be re-used for other purposes if you can prove that nothing else will call into the OS or library while using the structure address.

Once inside the open() call, you need to save the pointer to the parameter block:

Code:
_open:
  STA param
  STX param+1
;
; work, work, work,
;
  LDY #0
  LDA (param),Y
  STA filenameLow
  INY
  LDA (param),Y
  STA filenameHigh
;
; etc...
;


The difference between a parameter block and a control block is one of how much state resides in the block of memory. For example, suppose our OS keeps track of device ID using a 16-bit number, starting sector number (16-bit), current sector number (16-bit), byte offset (16-bit) within that sector. (For example, this would be plenty sufficient to work with a FAT filesystem.) Then we'd need to expand our code accordingly:

Code:
openParams:
  .dw filename, O_READONLY ; these are inputs to the open() call
  .dw 0, 0, 0, 0           ; these are maintenance state variables used by other filesystem calls


This no longer serves just the purpose of parameters, but now also has control information (used by the OS). Thus, it's now a control block. The up-side of this is that you save memory (no need for handle tables, etc.), but the down-side is that you directly expose the implementation details of your library or OS. Thus, whenever you want to change your OS, you almost certainly will break application compatibility (what happens if you don't use FAT? NTFS has no concept of a "starting block" for a file, for example, but rather "extents belonging to forks").

One should, ideally, minimize the use of control blocks, unless you can guarantee for all time that you have all the state you'll ever need. Experience maintaining software suggests this never is the case, though, which is why MS-DOS evolved from file control blocks (FCBs) from it's CP/M pedigree to using more Unix-like file handles starting with 2.0. It also allowed MS-DOS to support subdirectories, which FCB's cannot handle, since FCBs (as CP/M implemented them) embedded the entire filename (11 characters) within, instead of pointing to the filename.

Note that passing a bunch of parameters on the stack implies you're building a parameter block, and you're passing the address to this parameter block using the stack pointer register. :-)


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Mon Jun 21, 2010 3:31 pm 
Offline

Joined: Thu Jul 26, 2007 4:46 pm
Posts: 105
I'd be tempted to do things RISC style. Designate, say, the first 32 bytes of memory as "pseudo-registers", then say that some of these bytes are used to pass parameters (Say, 0-15 in that order), some are available as callee scratch (Maybe 16-23, in addition to the parameters) and some will be preserved across function calls (Say 24 - 31). You might specify that return values are in 0, or you might say that they're in A:X (i.e. low byte A, high byte X for 16-bit values)

How much memory you give to this block is, of course, up to you; as is how you save them (Though if you want to allow recursion, you might decide to push them onto the stack.

This provides you with quick parameter access and, of course, consistency.

An example, using a trivial 16-bit add method, using the method outlined above and returns in A:X:

Code:
add:
  LDA $0
  CLC
  ADC $2
  STA $0
  LDA $1
  ADC $3
  STA $1
  RTS


Its often said that the 6502 zero page is like having lots of hardware registers; it is certainly possible to use them like this, and it is easy to see why some people consider the 6502 the first RISC architecture.

The important thing is that, whatever you come up with, it should be consistent and standardized. True, a standardized mechanism brings with it some inefficiencies (Such as potentially needless shuffling), but on the other hand also brings the benefit of not having to remember how every single routine takes parameters.


Top
 Profile  
Reply with quote  
PostPosted: Mon Jun 21, 2010 3:33 pm 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8514
Location: Midwestern USA
shawnleblanc wrote:
I thought of using the stack but since the subroutine address is pushed there too, I don't that that'll work.

Parameter passing via the stack with the 6502 is problematic due to the fact that only the accumulator can push or pull the stack. It's easier with the 65C02 because .X and .Y can also be pushed and pulled. So, with the C02, you could pull the return address with .X and .Y, pull and store the parameters one at a time with .A (accumulator), adjust the return address by incrementing .X and .Y for as many parameters as pulled, and then push .X and .Y (in reverse order) to reset the return address.

That said, you also have to be mindful of the 256 byte size of the 65(C)02 stack. If you push a lot of parameters and nest a lot of subroutines, you could end up wrapping the stack. It's easier with the 65C816, as kc5tja noted, due to the availability of stack relative addressing, as well as a 16 bit stack pointer.

The method I use when more than three parameters have to be passed into a subroutine is to use a statically allocated data table. I then pass the address of the data table to the subroutine for processing:

Code:
        ldx #<datatab
        ldy #>datatab
        jsr mysub
...
datatab .byte filenum                 ;file number
        .byte "/etc/default/tcp.conf" ;file name


Here I'm pasing data needed to open a file. Obviously, the data table could be overwritten during runtime with any data, even though I specified static data in my code.

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


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Mon Jun 21, 2010 6:33 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8546
Location: Southern California
Shawn, although the 6502 lacks the stack-relative addressing modes that the 65816 has, the job can still be done by saving X (if you don't want to lose it), then doing TSX, then for example LDA $104,X etc., where the address is $100 plus how far into the stack you want to reach for a given operation. When done, restore the old X value if necessary. [Edit, many years later: I posted a treatise about 6502 stacks at http://wilsonminesco.com/stacks/ which cover this and many other aspects in depth.]

What is done in Forth on the 6502 however is to have two stacks, using the native stack in page 1 for a return stack, and having a data stack in page 0. The 6502 does make it rather practical to have gobs of stacks if you wish; but using ZP for the Forth data stack works out very efficiently, using X as the pointer. ZP instructions are shorter and you have more addressing modes. You seldom have to save the contents of X and use X for something else. It's almost like it was intended for this purpose. I've been using Forth for 19 years on the 65c02, with programs up to 10,000 lines with interrupts serviced in high-level Forth while NMIs interrupted those ISRs for service in assembly, and I've never gotten anywhere near running out of space on either stack except with the rare error situation that wouldn't be satisfied with any amount. In my experience, even splitting page 0 and page 1 into three parts each for multitasking would be entirely reasonable. More could be done, with caution.

When you know you're accessing the stacks constantly but don't know what the maximum depth is you're using, the tendency is to go overboard and keep upping your estimation, "just to be sure". I did this for years myself, and finally decided to do some tests to find out. I filled the stack areas with a constant value (maybe it was 00-- I don't remember), ran a heavyish application with all the interrupts going too, did compiling, assembling, and interpreting while running other things in the background on interrupts, and after awhile looked to see how much of the stack area had been written on. As I posted above, it wasn't really much-- less than 20% of each of the two pages.

_________________
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?


Last edited by GARTHWILSON on Wed Jun 23, 2010 9:41 pm, edited 1 time in total.

Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Wed Jun 23, 2010 4:28 pm 
Offline

Joined: Sat Jan 04, 2003 10:03 pm
Posts: 1706
OwenS wrote:
I'd be tempted to do things RISC style. Designate, say, the first 32 bytes of memory as "pseudo-registers", then say that some of these bytes are used to pass parameters (Say, 0-15 in that order), some are available as callee scratch (Maybe 16-23, in addition to the parameters) and some will be preserved across function calls (Say 24 - 31). You might specify that return values are in 0, or you might say that they're in A:X (i.e. low byte A, high byte X for 16-bit values)


The problem with this is the overhead involved with pushing and pulling these "pseudo-registers" to/from the stack. You also need conventions on who saves what pseudo-registers and/or under what circumstances too.

Definitely doable -- I've seen it done before, and using pointers to parameter or control blocks sort of implies this approach too. I just prefer to minimize the amount of code I have to write. (Now, if writing in a high-level language, let the compiler do all the work. ;) )


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Wed Jun 23, 2010 8:56 pm 
Offline

Joined: Thu Jul 26, 2007 4:46 pm
Posts: 105
Its pretty much the best option if you want methods to be re-entrant. It would be better to be able to allocate space for data preserved across calls on the stack, but one must admit the 6502's abilities are seriously lacking in this department.


Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: Thu Jun 24, 2010 5:47 am 
Offline

Joined: Sat Jan 04, 2003 10:03 pm
Posts: 1706
If you can allocate memory dynamically, you can put your activation records in such allocated memory. This gives you the opportunity to do all sorts of neat stuff, including but not limited to full continuations (c.f. Scheme) and Smalltalk-style code blocks. And, it still supports reentrancy.

But, in 64K of space, it's not likely going to happen, especially if you want performance. ;D


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

All times are UTC


Who is online

Users browsing this forum: No registered users and 6 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: