How do I do in CA65 things I do in ASM6?

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: How do I do in CA65 things I do in ASM6?

Post by tokumaru »

Oh, you're right! This is really starting to make sense to me! :D
User avatar
rainwarrior
Posts: 8731
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How to do in CA65 things I do in ASM6

Post by rainwarrior »

Oops, did a little test, and I think I overstated the ability of .res, ha ha. Again, .res needs to be constant at assemble time, but it also needs to be constant in the first pass, it seems, which puts a few more requirements on us. The whole bank needs to be in one assembly unit, this way (not necessarily in the same file, but all files must be combined into one unit with .include). Everything for the ROM except the fixed code must go in one big segment. In this example, the segment is named "BANK".

Code: Select all

.segment "BANK"
bank_start: ; this must be the first line in the BANK segment so that its total size can be calculated

; other code can go here

; the FIXED code can go anywhere, as long as appears before the padding at the end of BANK which appears below
; the critical thing is that FIXED_SIZE must be known before we get to the padding, so that we can calculate a size for padding
.segment "FIXED"
fixed_start:
; fixed code here
.assert * = $FFFA, error, "Vectors misaligned"
.word vec0, vec1, vec2
FIXED_SIZE = * - fixed_start

; finally the padding at the end of BANK segment
; this must appear after FIXED_SIZE is defined
.segment "BANK"
BANK_SIZE = * - bank_start
.res $10000 - (FIXED_SIZE + BANK_SIZE) ; generate padding
Trying to do the alignment via padding rather than the linker ends up requiring you to use a single SEGMENT and a single assembly for the whole bank, but maybe that's okay for you because that's how other assemblers do things anyway.
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by thefox »

Two things to keep in mind about ca65:

- It doesn't know where the code is going to be placed (unless you explicitly state it with .org), but it needs to know the size of the data/code that it has generated. So stuff like .res $1234-* won't work in relocatable ("non-org") mode.
- It's a single-pass assembler, so if you try to reference labels that haven't been defined yet, it may or may not work, depending on the case:

Code: Select all

BlockStart:
nop
nop
BlockEnd:
; Fine, the value is known at this point.
.res BlockEnd-BlockStart

Code: Select all

; Error, need to know value (affects amount of generated data)
.res BlockEnd-BlockStart
BlockStart:
nop
nop
BlockEnd:

Code: Select all

; Fine, value not known but doesn't affect amount of generated data, so
; actual calculation can be deferred.
.byte BlockEnd-BlockStart
BlockStart:
nop
nop
BlockEnd:
Sometimes it would be nice if it could resolve expressions like that (in cases that are resolvable, i.e. no recursive definitions and so on), but usually it's not a deal breaker.
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: How to do in CA65 things I do in ASM6

Post by tokumaru »

rainwarrior wrote:Again, .res needs to be constant at assemble time, but it also needs to be constant in the first pass, it seems, which puts a few more requirements on us.
Thanks for verifying this. Honestly though, this isn't such a big issue, so if the whole thing gets too awkward I might just manually define the size of the fixed block and adjust later, to make proper use of the assembler's capabilities. The main reason I decided to try CA65 is that I want to use segments so I can group relevant pieces of code regardless of where in the ROM they'll end up. In ASM6 I have to always mind the order in which code is written and/or files are included, so everything ends up where it should, and this is getting annoying to maintain because each sub-system has parts scattered everywhere.
thefox wrote:It's a single-pass assembler
Will keep that in mind, thanks.

Anyway, here's another question: How can I keep track of the indices of the different program banks? For example, if I placed a subroutine in a segment called "BANK13", how can I know which bank to switch to in order to call that subroutine without having to manually type the number 13 to switch banks? The goal is to not have to track down references to that bank in case I decide to move the routine to a different bank later on. In ASM6, where I do everything linearly, I just have a symbol incremented every time a new bank starts, and I copy the value from that symbol to another symbol I can use for switching banks (e.g. AUDIO_BANK = CURRENT-BANK).
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: How to do in CA65 things I do in ASM6

Post by thefox »

tokumaru wrote:Anyway, here's another question: How can I keep track of the indices of the different program banks? For example, if I placed a subroutine in a segment called "BANK13", how can I know which bank to switch to in order to call that subroutine without having to manually type the number 13 to switch banks? The goal is to not have to track down references to that bank in case I decide to move the routine to a different bank later on. In ASM6, where I do everything linearly, I just have a symbol incremented every time a new bank starts, and I copy the value from that symbol to another symbol I can use for switching banks (e.g. AUDIO_BANK = CURRENT-BANK).
There's an attribute "bank" that can be specified for memory areas in the linker configuration. It's an arbitrary 32-bit number that can be retrieved from symbols with .bank in source code.

E.g. if you have a memory area "PRG13" that specifies "bank=13", and you have a segment "BANK13" that you place in "PRG13", then:

Code: Select all

.segment "BANK13"
.proc foo
  rts
.endproc

; ...
; somewhere else:

lda #.lobyte( .bank( foo ) ) ; loads 13 (have to use .lobyte since it's a 32-bit number)
jsr mapBank
jsr foo
This works across file boundaries (the correct bank is substituted at link time).
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
tepples
Posts: 22705
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: How to do in CA65 things I do in ASM6

Post by tepples »

tokumaru wrote:
FIXED: load = PRG, type = ro, start = $FF90;
Again, you have to manually adjust the start depending on how much fixed stuff you have? There's no way of saying "align this arbitrarily sized block to the end of the bank"? Maybe it's possible in a hackish way, but that's actually one of the things I'm trying to avoid.
Correct. I've never seen a linker that allows "right justifying" code, or specifying the location based on the address of the end of a segment rather than based on the start of a segment. Do you want me to go request this feature in ld65 on the cc65 project's issue tracker on GitHub?
Would it be acceptable for me to use hundreds of .export statements in a bank that contains patterns for hundreds of animation frames so I can create pointers to that data in the main engine in order to access it during vblank?
Yes. I did something like this for level data in a recent project.
That's the exact kind of thing I want to avoid... I want my code to be flexible, so I don't have to edit several lines of code every time I resize a variable or rename something.
Then I guess I've elicited another possible ld65 feature request from you: "union segments". Consider RAM segments A and D that are usable at all times and RAM segments B and C are not usable simultaneously. You want to lay out BSS segments B and C at the end of BSS segment A, wherever that might end up, and BSS segment D that starts at the maximum of the end of segments B and C, wherever that might end up.

Code: Select all

,---------------.
|               |
|       A       |
|               |
+-------+-------+
|       |       |
|   B   |       |
|       |   C   |
`-------+       |
        |       |
,-------+-------+
|               |
|       D       |
|               |
`---------------'
rainwarrior wrote:These are additional constraints you're dropping on me now after the fact.
tokumaDRW?
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: How to do in CA65 things I do in ASM6

Post by tokumaru »

thefox wrote:There's an attribute "bank" that can be specified for memory areas in the linker configuration.
Ah, that's perfect! That's the kind of thing I'm looking for, built-in functionalities to do things that required convoluted hacks in other assemblers.
tepples wrote:Do you want me to go request this feature in ld65 on the cc65 project's issue tracker on GitHub?
If you feel like this might be useful to a reasonable number of users, then by all means, go ahead. I personally wouldn't feel comfortable making feature requests at this point, since I'm not really acquainted with the software yet.
Would it be acceptable for me to use hundreds of .export statements
Yes. I did something like this for level data in a recent project.
Good to know that this isn't such an alien thing to do, but I'd still try to avoid it, if possible.
"union segments"
Sounds interesting.
tokumaDRW?
Oh man, I'll try harder to not come off like that. I definitely didn't make up things after the fact though, since all of this stuff is already implemented and working in ASM6. I might have failed to provide all the details though, so sorry about that! :wink:
User avatar
rainwarrior
Posts: 8731
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How to do in CA65 things I do in ASM6

Post by rainwarrior »

tokumaru wrote:Would it be acceptable for me to use hundreds of .export statements
There's really no problem with using hundreds of import/export/global directives, if you want to. It's not like they're "expensive", it's just another symbol in a table somewhere. The linker just looks 'em up and sticks the data where it needs to go.

From a code design standpoint, there's probably something to be said for interfaces that are well contained, so that everything that belongs together is in the same translation unit, and your cross-unit communication (import/export) is minimal. However, you've got a real program to write, and if lots of linked symbols seems like it would help, there's no pressing reason to avoid it.
tepples wrote:Do you want me to go request this feature in ld65 on the cc65 project's issue tracker on GitHub?
The strongest use case I can think of for "align to end" is DPCM samples. It's hard to let them resize to fit your data automatically, but also make room for stuff pushing up from below. As an alternative to "align to end", a "start at or above" feature would be just as good to meet its requirements, I think.
tepples wrote:"union segments"
I can't really think of a compelling reason a feature like this would be needed. Yes, it would apply to tokumaru's specific request, well, but that needs that request represents can be satisfied in other ways, so I can't think of a more practical reason to want this feature. Actually, even "align to end" would be a good solution in this case, i.e. if you align all your temporary pages to end, you no longer need everything in the same translation unit for that ".res" to represent the shared space. You could just protect the bottom edge of your temporary space with a link-time .assert, and both regions could just grow toward the middle safely.
tokumaru wrote:
tepples wrote:tokumaDRW?
Oh man, I'll try harder to not come off like that.
I certainly wasn't trying to make that comparison. I was merely explaining that if you had given me the additional constraints up front, I could have easily given you a solution that met them.
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by thefox »

As long as we're talking about new features and what the linker can't do currently, when I was toying around with FME-7 I noticed this one case that the linker can't really handle: Let's say you have an 8 KB bank, and you want to map it at two different addresses in the memory space at (possibly) different times. So, for example, you have some code that should go at $8000, and another unrelated piece of code (or DMC sample) that should go at $C200, and you want to place those in the same bank. Since you can specify only one base address for the memory area, this can't be done.

Not sure what kind of a new feature would be the best way to model that type of a thing. Possibly another set of definitions for banks within the output file, and multiple memory areas could output data into the same bank (and the actual base address of a memory area would depend on how much stuff is in the bank by the time the memory area gets added there).

Probably not a big deal in practice, unless you're really short of space, but still kind of annoying that it's not at all possible, AFAIK (without .org).
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
User avatar
rainwarrior
Posts: 8731
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by rainwarrior »

thefox wrote:Since you can specify only one base address for the memory area, this can't be done.
Segments have a "load" attribute (where it goes in MEMORY block) and "run" attribute (where it is expected to be run from, i.e. controls PC). By default "run" inherits "load", but otherwise you can use this to run a segment in a different MEMORY block than it gets stored in.

The only limitation I can think of is that you'd really need to start the segment at a specific address, so that you can line it up on both sides. Normally this feature is intended for run-from-RAM code, so the intention is that the linker will give you some defined symbols telling you where to place the code in RAM. With ROM, obviously that's not flexible anymore, so you'd have to specify it in the segment itself, I think.
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by thefox »

rainwarrior wrote:
thefox wrote:Since you can specify only one base address for the memory area, this can't be done.
Segments have a "load" attribute (where it goes in MEMORY block) and "run" attribute (where it is expected to be run from, i.e. controls PC). By default "run" inherits "load", but otherwise you can use this to run a segment in a different MEMORY block than it gets stored in.
Thanks for the comment. I actually think it helped me figure out a good solution for this:

Code: Select all

# linker.cfg
MEMORY {
    # The actual bank that goes to the file.
    PRG0:
        start=$0000, # Address doesn't matter since it won't be used.
        size=$2000,
        fill=yes,
        file=%O;

    # A dummy memory area for data going to $8000, not written to file.
    PRG0_8000:
        start=$8000,
        size=$2000,
        define=yes,
        file="";

    # Another dummy area for data going to $C000, not written to file.
    # Start address and size depends on how much data got placed in PRG0_8000.
    PRG0_C000:
        start=$C000+(__PRG0_8000_LAST__-__PRG0_8000_START__),
        size=$2000-(__PRG0_8000_LAST__-__PRG0_8000_START__),
        file="";
}

SEGMENTS {
    CODE0_8000: load=PRG0, run=PRG0_8000, type=ro;
    CODE0_C000: load=PRG0, run=PRG0_C000, type=ro;
}

Code: Select all

.segment "CODE0_8000"
.proc at8000
    jmp *
.endproc

.segment "CODE0_C000"
.proc atC000
    jmp *
.endproc
Result:

Code: Select all

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000  4C 00 80 4C 03 C0 00 00 00 00 00 00 00 00 00 00  L.€L.À..........
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
...
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
User avatar
rainwarrior
Posts: 8731
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by rainwarrior »

Interesting, that looks very useful!

I did not know about the LAST symbol, or that it could be considered a constant you could use for subsequent MEMORY blocks. I'd always run into issues trying to use generated values like that, but I'd always tried to do it through SEGMENTS; I guess the linker goes through the MEMORY blocks one at a time, in order, with each block's symbols generated before trying to resolve the next one?
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by thefox »

rainwarrior wrote:Interesting, that looks very useful!

I did not know about the LAST symbol, or that it could be considered a constant you could use for subsequent MEMORY blocks. I'd always run into issues trying to use generated values like that, but I'd always tried to do it through SEGMENTS; I guess the linker goes through the MEMORY blocks one at a time, in order, with each block's symbols generated before trying to resolve the next one?
Yeah I was not sure at first whether I would be able to use it there either. I'm glad it works. I'm not sure if there's an actual good reason why it doesn't work in segment definitions. There's a chance it's a bug.
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: How do I do in CA65 things I do in ASM6?

Post by tokumaru »

How about scratchpad variables? In ASM6, I reserve 32 bytes in ZP that subroutines can use however they want. Then, for each subroutine, I use an .enum to define variables in that area (to keep in the spirit of not typing addresses manually, which is error-prone and annoying to maintain):

Code: Select all

	.enum Scratchpad
	RowPointer .dsb 2
	BlockCount .dsb 1
	.ende
Any way to achieve similar behavior in ca65, without having to create hundreds of overlapping segments, one for each subroutine?
tepples
Posts: 22705
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: How do I do in CA65 things I do in ASM6?

Post by tepples »

thefox wrote:I noticed this one case that the linker can't really handle: Let's say you have an 8 KB bank, and you want to map it at two different addresses in the memory space at (possibly) different times. So, for example, you have some code that should go at $8000, and another unrelated piece of code (or DMC sample) that should go at $C200, and you want to place those in the same bank. Since you can specify only one base address for the memory area, this can't be done.
If you don't want to use multiple MEMORY areas or run addresses for segments in a bank, possibly because they're [tokumaru]not automatic[/tokumaru], I've thought of a third way. You can leave it at $8000, and then in your sample address table entry macro, map anything in $8000-$BFFF to $C000-$DFFF.

The reuse of the __*_LAST__ symbol looks interesting as well. If it works, it'd resolve one of the feature requests above. Is there a counterpart to GREATEST() or MAX() function in ld65 config language?
  • Union segments part I (multiple overlapping MEMORY areas starting after a specified previous MEMORY area, for BSS specific to one of several mutually exclusive parts of a program): I'll need to make a demo of resolving this with __*_LAST__
  • Union segments part II (MEMORY area starting after the longest of multiple overlapping MEMORY areas): Not resolved yet
  • SEGMENT aligned to end within its MEMORY area: Not resolved yet
I wonder whether scratchpad variables in $0000-$001F could be defined with a .struct.
Post Reply