0
param word 0

function
    ...
    lda param,x


scr1 byte "some text"
     byte 0

So how do I pass "scr1" to function as "param"? I know it's a memory address, so it doesn't fit into 1 byte registers. What's the best way to do this?

EDIT:

Thanks for the answers! The solution posted below works great, but this uses both X and Y registers. What if "function" calls KERNAL routines which rely on X or Y or my own code needs X for something etc? In these case this won't work as expected I guess?

I'm totally new to assembler, so I'm confused about many things.

Basically I wanted to pass the address, because I wanted to avoid code duplication. I have a simple function which prints a string to the screen like so:

        ldx #$00
copy_text
        lda scr1,x
        beq done
        sta screen_start,x
        inx
        bne copy_text
done
        rts

But this works only with scr1. If wanna print other stuff, I need to duplicate the code which seems to be wasteful.

Is that acceptable in assembler? I mean, in C or any other language you would just write a reusable function or method. But in assembler this seems to be very hard to do, because there are only 3 registers and they are used by many things.

What's the best way to overcome this limitation?

user2297996
  • 1,382
  • 3
  • 18
  • 28
  • Does c64 have a way to hold an address in a pair of registers such that you can deref the pointer? If so, pass in that register-pair. – Peter Cordes Sep 29 '21 at 04:16
  • Yes, there is. I remember the Zero Page used for this. I don't know exactly but it was about putting the 16bit address to Zero Page as two consecutive bytes and loading the address with Zero Page Indexed Indirect mode or something like that. – the kamilz Sep 29 '21 at 10:38
  • For a function that needs the incoming arg registers for something else, save those values somewhere and reload them later. Just like normal for modern CPUs with more registers. [Although with so few regs, you probably don't want to consider any of them call-preserved](https://stackoverflow.com/questions/9268586/what-are-callee-and-caller-saved-registers/56178078#56178078). Or make up a different calling convention that passes args in memory. – Peter Cordes Sep 29 '21 at 16:09
  • Welcome to the constraints of 8-bit assembly. "In C or any other language you would just write a reusable function or method" any language once compiled will boil down to using a fixed number of registers. "But this uses both X and Y registers. What if "function" calls KERNAL routines" for the example below X and Y are only passed at the beginning; after you've stored them elsewhere you can do what you need to do with X and Y. Complex assembly will be a lot of storing stuff in ZP and temp variables and stack manipulation to make things work. What you're experiencing is considered "normal". – tendim Sep 29 '21 at 18:42
  • @tendim OK thanks for the clarification. I"m still a bit confused about how tmp variables work in this language. Is there any good book / tutorial you would recommend for beginner? I'm not new to programming, only to assemblers. Thanks in advance! – user2297996 Sep 30 '21 at 05:06
  • In assembly there are no variables; variables are a high-level language construct. A variable is just a space in memory, but in assembly you have to manage all of that. So if you want to say `A$="foo"`, you need to define a space in memory (say $C000), and then copy "foo" to that memory. My personal go-to for learning is COMPUTE's _Machine Language for Beginners_ by Richard Mansfield. Very old, but completely relevant for what you are trying to do. It takes you from the ground up, and walks you through how to build a large program (you actually create your own assembler in the book!). – tendim Oct 01 '21 at 15:21

1 Answers1

1

There are a few ways to do this.

zpb = $fb

function = *
   stx zpb+0  
   sty zpb+1  ; note after you've stored .X and .Y in zero page they can be re-used for other purposes.
   ...
   ldy #$00
   lda (zpb),y
   ...

caller = *
    ldx #<scr1
    ldy #>scr1
    jsr function

Alternatively you can play with the stack

zpb = $fb

function = * 
   pla            ; save current stack pointers for later
   sta temp+0
   pla
   sta temp+1
   pla            ; pull low byte off first
   sta zpb+0
   pla            ; now pull off high byte
   sta zpb+1       
   lda temp+1     ; restore stack pointers so RTS works as expected
   pha
   lda temp+0
   pha
   ...
   ldy #$00
   lda (zpb),y
   ...
temp .byte 0,0

caller = *
    lda #>scr1   ; note we push high byte first since stack is FILO
    pha
    lda #<scr1
    pha
    jsr function

Machine language by its own on the 6502 only has the three registers, so your options are usually to pass values in those registers, or, use those registers to point to a larger set of data that you can then access elsewhere. The indirect zero page is your best friend in this regard since you can then store pointers in zero page and then access them indirectly.

tendim
  • 447
  • 3
  • 10
  • I don't know 6502 very well, but even the first way looks like some extra overhead in the callee. Can the caller pass something that's ready to use, e.g. store the pointer in a location it owns in the zero page, and pass the zero-page offset as a register arg? That would depend on 6502 having an addressing mode that takes a zero-page address in a register, and does memory-indirect load or store through it. IDK if that exists, but if so, it might be interesting to have the caller own the zero-page pointer location instead of the callee having to dump reg args to a spot it owns. – Peter Cordes Sep 29 '21 at 13:30
  • What overhead are you seeing though? If the callee has to store it in zero page, they are still performing at least four operations (two loads, two stores, into a ZP address). Here they only perform two operations (load .X/.Y with the address). This is also a design consideration: should ZP be owned by the callee or the caller? "store the pointer in a location it owns in the zero page, and pass the zero-page offset as a register arg?" afaik no such instruction exists for x502; I'd love for someone to correct me on this! Addressing modes: http://www.emulator101.com/6502-addressing-modes.html – tendim Sep 29 '21 at 13:52
  • If the caller's zero-page pointer is call-preserved, the caller can reuse the same pointer across multiple calls to this or other functions, without either caller or callee having to do more stores of it. You're only showing a single call-site and a single callee. Although now that I think about it, many functions do need to increment their pointer arg if it's to an array. Maybe still usable for a struct with fixed offsets, but there probably isn't an addressing mode that can index relative to a zero-page pointer selected by a register. – Peter Cordes Sep 29 '21 at 14:01
  • Thanks for the addressing mode link. `LDA ($0,X)` should work to let a callee use a zero-page address passed in `x`. But that can only access the one byte pointed-to, not other bytes nearby. `LDA ($86), Y` needs a hard-coded (direct) zero-page address, not `LDA (X), Y`. Still, if the callee is allowed to modify the zero-page pointer with other instructions, they can do that between uses of use `LDA ($0,X)`. But for looping over an array, that's much worse than your idea of dumping to a fixed zero-page location which would enable `LDA ($86), Y` or whatever in loops. – Peter Cordes Sep 29 '21 at 14:08
  • 1
    @PeterCordes: Another option, for functions that are stored in RAM and wouldn't need to use a pointer too many times would be to have a function receive the address of a zero-page pointer in a register, and begin with stores that patch all of the places that use it. Not practical in a function uses the pointer in many places, but quick and easy if it's only used once or twice. – supercat Oct 01 '21 at 18:54
  • @supercat: If you're considering self-modifying code, would you maybe skip the zero-page entirely and patch a load and/or store in a loop to use `STA $1000,Y` absolute indexed addressing? (I assume a 3-byte opcode + address instruction is faster than a 2-byte instruction that has to fetch an extra 2 bytes of pointer data before using it.) Then you'd have to pass a full 16-bit address in a pair of regs, though. – Peter Cordes Oct 01 '21 at 19:52
  • @PeterCordes: That can also be a good approach. Using `(zp),y` mode costs a cycle for each access, but is likely to yield more compact code. – supercat Oct 01 '21 at 20:04
  • @supercat: Oh, just one cycle net saving? I was expecting bigger savings from not having to turn around and use one load result as an AGU input for another, if that took multiple microcode steps or something. Probably not worth it in many cases then, except for really long running loops. – Peter Cordes Oct 01 '21 at 20:15
  • @PeterCordes: When using the `(ind),y` addressing modes, address `ind` will be fetched on the second cycle. During the next cycle, the contents of address `ind` will be fetched while the ALU computes `ind+1`. Then the contents of `ind+1` will be fetched while the ALU adds `y` to the value fetched from `ind`. On the next cycle, the CPU will form an address using the ALU result and the byte fetched from `ind+1` while the ALU adds one to that newly-fetched value. Finally, if required, the CPU will add a memory cycle using the ALU value instead of the previous high byte if appropriate. – supercat Oct 01 '21 at 20:24
  • @PeterCordes: The only common CPU where address arithmetic is really bad is the Z80, which is limited by a 4-bit ALU. What's ironic is that while the 8080 has an 8-bit ALU, the Z80's 4-bit ALU doesn't really hurt the performance of most 8080 instructions. It has a much larger adverse impact on the performance of instructions that were added to the Z80 instruction set. – supercat Oct 01 '21 at 22:34