CC65 - Inline assembly return values?

Programming the 6502 microprocessor and its relatives in assembly and other languages.
Post Reply
GinDiamond
Posts: 39
Joined: 12 Feb 2022

CC65 - Inline assembly return values?

Post by GinDiamond »

I want to make a "getch" function in C for my sym-1, and I think I can get it by doing:

Code: Select all

__asm__ ("JSR $8A5A")
However, I'm not really quite sure how to pull the value from the A register to return it? I know that the return value gets placed in A/X/sreg, but
if I put it in a void function, would this work by doing

Code: Select all

void getch()
{
    __asm__ ("JSR $8A5A");
}
...
char c;
getch();
__asm__ ("STA %b", c);
I don't seem to be able to get this to work.
User avatar
Yuri
Posts: 372
Joined: 28 Feb 2023
Location: Texas

Re: CC65 - Inline assembly return values?

Post by Yuri »

GinDiamond wrote:
I want to make a "getch" function in C for my sym-1, and I think I can get it by doing:

Code: Select all

__asm__ ("JSR $8A5A")
Not sure what would exactly lie at address $8A5A for you, but I wouldn't rely on a hard coded memory address like this. There's no telling where the assembler/compiler might move a function to when you compile the next time. If this is a function written in assembly that you're calling from C, it would probably be a whole lot easier to export that function as a symbol the C compiler can see and then just call that.
Quote:
However, I'm not really quite sure how to pull the value from the A register to return it? I know that the return value gets placed in A/X/sreg, but
if I put it in a void function, would this work by doing

Code: Select all

void getch()
{
    __asm__ ("JSR $8A5A");
}
...
char c;
getch();
__asm__ ("STA %b", c);
I don't seem to be able to get this to work.
So the thing here is that the compiler assumes that outside of your inline assembly it can do whatever it wants with the registers (A or otherwise). Looking in the documentation it doesn't appear that you can define a naked function which would strip out the function preamble/cleanup; the way that it looks like they suggest you get around this is to use a variable to temporarily hold it.

Code: Select all

char c;

void getch(void)
{
    __asm__("JSR $8A5A");
    __asm__("STA %b", c);
}
This looks more like what you're probably trying to do, however, this seems less than ideal to me. Esp if you've already written the function in an assembly file. Tell the CA65 assembler to export that symbol as getch and then import from your c-code:

In ASM

Code: Select all

.export getch

getch:
   ; Your code for getch() here.
   ; Leave the character in the A register before return, that's where the compiler will expect the return value
   rts
In C:

Code: Select all

int getch(); // Define a prototype, the linker will find this for you now that it's exported from ASM

char c = getch();
Hope this helps!

Edit:
C sometimes like to put an underscore on function names. So you may need to define it as _getch in the assembly.
GinDiamond
Posts: 39
Joined: 12 Feb 2022

Re: CC65 - Inline assembly return values?

Post by GinDiamond »

Hey, I'm sorry I wasn't as clear as I could be.
$8A5A is a subroutine in the SYM-1 ROM that waits and grabs an incoming byte from the serial port and puts it in the Accumulator.

I also am not quite sure what you mean about making an asm file with my function in it, however, this seems like what I should do for sure. How would I link that with a command?
User avatar
Yuri
Posts: 372
Joined: 28 Feb 2023
Location: Texas

Re: CC65 - Inline assembly return values?

Post by Yuri »

GinDiamond wrote:
Hey, I'm sorry I wasn't as clear as I could be.
$8A5A is a subroutine in the SYM-1 ROM that waits and grabs an incoming byte from the serial port and puts it in the Accumulator.

I also am not quite sure what you mean about making an asm file with my function in it, however, this seems like what I should do for sure. How would I link that with a command?
Okay, getting clearer picture here. Didn't realize you were using a prebuilt ROM; in that case the above method could work fine, though I would ditch the global variable:

Code: Select all

int getch(void)
{
    char c;
    __asm__ ("JSR $8A5A");
    __asm__ ("STA %b", c);
    return c;
}
Still not what I'd consider the most ideal, but you're using a C compiler, there's going to be a lot of boiler plate code that the compiler adds.

With something like the SYM-1 (which I just gave myself a crash course on), you can't grantee that every ROM function is written to interface with the CC65 compiler. getch() is such a simple function you might be able to just link it in with some fancy stuff with the linker, but I don't know how to do that off the top of my head.


As to the question of how to link assembly with C:

You can create an assembly file (usually the extension is .asm or .s) and then assemble it using the CA65 assembler which comes with the CC65 compiler.

You would join multiple files together with a linker step, most people wrap this up with a Makefile or some other such system for building multiple files.

A simple Makefile might look like so:

Code: Select all

OBJS=asm1.o asm2.o cfile1.o cfile2.o

# Assemble .s files into .o files
.s.o:
    ca65 --cpu 65c02 -I include -o $@ $<

# Compile .c files into .o files
.c.o:
    cc65 --cpu 65c02 -I include -o $@ $<

# Generate your program from the above created .o files.
myprogram.bin: $(OBJS)
    ld65 $(OBJS) -o myprogram.bin
CC65 doesn't come with Make on it's own, you'll have to get and install Make for whatever platform you're running on. And each flavor of Make has it's own little quarks. (I would recommend GNU Make simply because it has probably the most extensive documentation on the net)

If you look at the CC65 docs I believe they talk about how you can roll a Makefile that works with their tools.

If you want you can try dissecting the ROM I've been slowly coding up which has a Makefile, but it is purely in assembly, no C.
(It does use the CA65 assembler though)

https://github.com/Kelmar/Lemon

Good Luck!
hmn
Posts: 21
Joined: 07 May 2017

Re: CC65 - Inline assembly return values?

Post by hmn »

See also here: Calling assembly functions from C.
Yuri wrote:

Code: Select all

int getch(void)
{
    char c;
    __asm__ ("JSR $8A5A");
    __asm__ ("STA %b", c);
    return c;
}
That does not compile due to "%b" requiring a literal. Since "c" is a local variable, I don't think it can be accessed easily here. It should work by making it global:

Code: Select all

int getch(void)
{
  static char c;
  __asm__ ("JSR $8A5A");
  __asm__ ("STA %v", c);
  return c;
}
This compiles to

Code: Select all

; ---------------------------------------------------------------
; int __near__ getch (void)
; ---------------------------------------------------------------

.segment	"CODE"

.proc	_getch: near

.segment	"BSS"

L0002:
	.res	1,$00

.segment	"CODE"

	jsr     $8A5A
	sta     L0002
	ldx     #$00
	rts

.endproc
Note that X gets zeroed, as per the calling convention described in the documentation linked above - you'd have to do that in the external ASM function as well.

Of course the inline ASM is not optimal due to the useless STA and write-only variable.

Interestingly, when changing the return type to "char", the generated code contains an additional redundant load:

Code: Select all

; ---------------------------------------------------------------
; unsigned char __near__ getch8 (void)
; ---------------------------------------------------------------

.segment	"CODE"

.proc	_getch8: near

.segment	"BSS"

L0009:
	.res	1,$00

.segment	"CODE"

	jsr     $8A5A
	sta     L0009
	ldx     #$00
	lda     L0009
	rts

.endproc
User avatar
Yuri
Posts: 372
Joined: 28 Feb 2023
Location: Texas

Re: CC65 - Inline assembly return values?

Post by Yuri »

hmn wrote:
See also here: Calling assembly functions from C.

That does not compile due to "%b" requiring a literal. Since "c" is a local variable, I don't think it can be accessed easily here. It should work by making it global:
Not surprising, I hadn't tried compiling it myself. Good to know a static variable works as well. Wouldn't be thread safe, but I doubt anyone is doing complex multi threaded code on the 6502.


Quote:
...

Note that X gets zeroed, as per the calling convention described in the documentation linked above - you'd have to do that in the external ASM function as well.
Correct, because the return is a 16-bit integer not a pure byte. Hence the -1 to indicate an end of file or other error for getch(), otherwise -1 would turn into 255, which is a valid character.

In my Lemon code I take advantage of being able to check the fact that I can check the status flags and use the carry bit as a quick way to indicate if a return value is valid or not.
Quote:
Of course the inline ASM is not optimal due to the useless STA and write-only variable.
Yep, exactly. Let alone all the other boiler plate stack build up and unwinding that the compiler may (or may not) add on top of that.
Quote:
Interestingly, when changing the return type to "char", the generated code contains an additional redundant load:

Code: Select all

; ---------------------------------------------------------------
; unsigned char __near__ getch8 (void)
; ---------------------------------------------------------------

.segment	"CODE"

.proc	_getch8: near

.segment	"BSS"

L0009:
	.res	1,$00

.segment	"CODE"

	jsr     $8A5A
	sta     L0009
	ldx     #$00
	lda     L0009
	rts

.endproc
Correct, there's a lot of stuff the compiler just does that seems to not make sense from a human perspective. I'm sure from the context of how the compiler keeps track of what registers are in use and are not, it might make more sense, but yea, it seems to do weird stuff like that.

There's a discussion here were we go into some of the weird things the compilers sometimes does.

This is the reason why I decided to switch to pure ASM for Lemon, the compiler was just adding too much fluff, and my goal is to fit it into the 8KByte ROM I have.
GinDiamond
Posts: 39
Joined: 12 Feb 2022

Re: CC65 - Inline assembly return values?

Post by GinDiamond »

This is all very very interesting, I'll have to use this in future projects!

Turns out, the default libraries of the sym-1 seem to be really really good to start with, so I'm not really creating anything groundbreaking or even faster/smaller by writing getchar() myself. However, it seems writing my own memset may be an interesting idea.
Post Reply