This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

CA65/LD65: putting common code in several banks

CA65/LD65: putting common code in several banks
by on (#66990)
Hoping someone has a suggestion on how to do this elegantly as I can't fathom it out.

I want to split my editor over two 16k banks (MMC1) but there are some common routines and tables that need to be accessed whichever of the two PRG banks the editor is currently in.

My idea was to duplicate these common elements in both editor banks at the same address so you can just call them from anywhere in the editor. Problem is, the labels end up being duplicated too and you get errors of course.

However, I'm unsure about how to go about it. I was trying to mess around with .export and .import but I got myself in a lot of knots. I was hoping there'd be a simple way to do it in the memory configuration file for ld65.

Any advice would be very welcome. :)

by on (#66991)
If the code is location-independent you could compile it separately and incbin the resulting binary at all the places where you need it.

Another solution would be to create macros of them (essentially making all local labels anonymous).

by on (#66994)
Compile and link it separately, and write out a map file. In your main program, incbin the result in its own SEGMENT. Write a native program (in C, Perl, Python, etc.) to translate the map file into a file that the main program can include.

by on (#66995)
I just tried this and it works well, assuming I'm understanding the question. First, assemble each segment separately, so that only the exported symbols can clash. Then just include the duplicated code in each segment but don't export its names. No incbin or anything. Full source (uses 16-byte segments to make it easy to see in hex editor): ca65_segments.zip

I noticed Neil mentioned he wants the controller routines at the SAME addresses in each. To achieve this, include them at the beginning of the segments, so they always assemble the same in each.

Code:
; controller.s
    read_joy:
        ...
        rts

; common.s
    .export table
   
    .segment "COMMON"
    table:
        ...

; seg1.s
    .export foo
    .import table
   
    .segment "SEG1"
       
        .include "controller.s"
       
    foo:
        ...
        jsr read_joy
        ...
        lda table,x
        ...
        rts

; seg2.s
    .export bar
    .import table
   
    .segment "SEG2"
       
        .include "controller.s"
       
    bar:
        ...
        jsr read_joy
        ...
        lda table,x
        ...
        rts

; build
ca65 common.s
ca65 seg1.s
ca65 seg2.s
ld65 common.o seg1.o seg2.o

by on (#66997)
Thanks for the help and ideas.

With blargg's example I managed to get the code building but have ran into another problem. All of my ZP/RAM vars are defined in one .h file and so I now need to .export everything so the other 'modules' can use the vars.

The only option seems to be to .export every variable and then .import the definitions (or use the auto-import switch for ld65?) but as I've got about 600 variables defined, if there's a less labour-intestive way to avoid having to change the whole .h to to export everything, I'd really appreciate some info.

Or could I turn the .h file into a 'library'?

:)

by on (#66999)
Hello neilbaldwin,

I also use ca65 / ld65 for my nes game development. While I don't have the same routine in multiple ROM banks at the same address, I do have fixed locations for all of my ZP (and non ZP) global variables. (ps- I'm using a MMC1 based design).

I don't know if the source to my project would help you or not, but you (or anyone else) are free to look at it. Of particular interest for this thread would be "src/kernel/globals.s" and "src/include/globals.inc".

https://svn.ecoligames.com/svn/nes-game/trunk

by on (#67000)
neilbaldwin wrote:
All of my ZP/RAM vars are defined in one .h file and so I now need to .export everything so the other 'modules' can use the vars.

The typical pattern in C is to "define" the variables (reserve space for them) in a single .c file and "extern declare" them in a .h file that .c files include:
Code:
/* stuff.h */
extern unsigned short foo;
extern unsigned char bar[128];

/* stuff.c */
#include "stuff.h"
unsigned short foo;
unsigned char bar[128];

This works in ca65 too, as the cc65 system has pretty much the same linkage model as C, apart from the difference between 1-byte and 2-byte pointers:
Code:
; stuff.h
.globalzp foo
.global bar

; stuff.s
.include "stuff.h"
.segment "ZEROPAGE"
foo: .res 2
.segment "BSS"
bar: .res 128


Quote:
The only option seems to be to .export every variable and then .import the definitions (or use the auto-import switch for ld65?) but as I've got about 600 variables defined, if there's a less labour-intestive way to avoid having to change the whole .h to to export everything

If you know sed, you can write a one-liner to extract a list of variables and prepend .global or .globalzp as needed. If you don't, you can do the same thing using the regular expression support built into Perl or Python.

by on (#67008)
Groan....

So, I've type ".export" at the front of all my variables and now I'm using the corresponding ".import" in another file to access them but I seem to be getting a lot of "range errors" e.g

cpx #keyBufferEnd-keyBufferStart

where keyBufferStart and keyBufferEnd are addresses in main RAM.

I'm guessing the resulting arithmetic operation is returning a number that exceeds 8bits but how do I know and more importantly, how do I force it to be correct?

Pllllllease don't tell me I've wasted about 3 hours today trying to make this work :S

by on (#67009)
neilbaldwin wrote:
Groan....

Pllllllease don't tell me I've wasted about 3 hours today trying to make this work :S


No matter what solution you choose, you have not wasted 3 hours. At best you've used 3 hours to solve your problem. At worse you've used three hours to learn something :)

by on (#67012)
Maybe you could try cpx #<(keyBufferEnd-keyBufferStart) to force the lower 8 bits. What a pain, though.

edit - I think I remember doing something like this by putting the duplicated data inside a .scope, but with another label before it (outside the scope) to reference the thing.

But I've done all sorts of weird things with LD65 to get around this sort of stuff, inserting the duplicate data is easy once everything is just anonymous binary data. I think this can be done in the linker, you can make a segment and just output it to the file multiple times. Segment sizes have to be adjusted manually, though.

I didn't know that for a while with LD65 (I guess because 32kB was usually plenty), that having everything output to the same file will simply build up the entire file in the order you link it.

by on (#67013)
Memblers wrote:
Maybe you could try cpx #<(keyBufferEnd-keyBufferStart) to force the lower 8 bits. What a pain, though.

edit - I think I remember doing something like this by putting the duplicated data inside a .scope, but with another label before it (outside the scope) to reference the thing.

But I've done all sorts of weird things with LD65 to get around this sort of stuff, inserting the duplicate data is easy once everything is just anonymous binary data. I think this can be done in the linker, you can make a segment and just output it to the file multiple times. Segment sizes have to be adjusted manually, though.

I didn't know that for a while with LD65 (I guess because 32kB was usually plenty), that having everything output to the same file will simply build up the entire file in the order you link it.


Yeah, I fixed that cpx just like that. Horrible though.

I managed to get the ROM to build but now it doesn't run (black screen).

Something is wrong somewhere but jesus, it could be anything at this stage :(

by on (#67017)
I usually just used .global and .globalzp, rather than import/export.

by on (#67021)
Like mentioned above, every new trial will bring some experience in the end so I wouldn't see it as a waste of time. If that was the case, I would be crying a lot over all the re-factoring I did recently ;)

But right now, the problem is maybe you changed too much thing without knowing the impact of that change. In assembler, this can sometime be lethal (I experienced it many time ;;^_^). My suggestion, although abstract, would be that you first roll back to a previous version that work and do the operation on a few variables that you know would have a visual impact right away if they're not working properly. During that phase, don't focus on the code sharing but on the variable sharing only, which seem to be the one causing issue. Once you figure out the cause, you can reproduce the same thing with another group of variable. You should do it gradually since maybe you did some miss (typo or something) and now you cannot figure out where it is. Save every interim revision until the end, just in case something goes wrong during the process.

I had a few issue like that when converting the FT driver. I was doing it in one shot and couldn't figure out where did I miss. Doing it gradually should help.

It may not be the answer that you're looking for but maybe the process could help in the end figuring out what went wrong.

by on (#67034)
Here's a doc and example code I came up with that cleanly solves what I understand to be the problem: ca65_duped_bank_data.zip

by on (#67042)
It's always something stupid isn't it....

In all the code reorganising I'd put the audio engine in the wrong bank so the CPU was executing garbage instead of initialising the APU :)

Well at least it seems to work now, even though my code now looks like a china shop in the wake of a visit by Mr Bull.

Oh and blargg's last post is a really nice elegant solution so after all my .export/.import typing, I'm probably going to scrap it and do it his way.

by on (#67043)
Be cautious of my solutions, because they tend towards extra levels of wrappings that can obscure things without a corresponding benefit. Evaluate them critically and don't get distracted by how good they might seem at first. This is coming from someone who's been taken by them many times, only to scrap them for something simpler later :)

by on (#67047)
blargg wrote:
Be cautious of my solutions, because they tend towards extra levels of wrappings that can obscure things without a corresponding benefit. Evaluate them critically and don't get distracted by how good they might seem at first. This is coming from someone who's been taken by them many times, only to scrap them for something simpler later :)


You do do obscure very well :)

by on (#67048)
OK, I'm getting somewhere now with the .global and .globalzp command (thanks for mentioning that reaper, it didn't seem all that useful until blargg explained it to me :))

by on (#67049)
Aaaaaaand once again I spoke too soon :S

So the project is compiling and running again now so I thought it was time to try to include the bits of common code/tables in two different banks.

I have an include (.h) file now that contains the addresses of the routines in that code as .globals.

Where I include the .h file and the common code it relates to in the first bank it compiles OK. However in the second bank, I get several ld5 errors;

"Warning : Duplicate external identifier : 'xxxxxxxxx'

which all relate to the routine addresses that are in my .globals list.

Can any see the (probably obvious) problem?

by on (#67051)
And to round the morning off, answering my own questions again LOL

So I split the routine addresses for the common code into their own .globals list and then didn't include that list in the code for the bank where the second version of the common code resides.

A little messier than I'd have liked but it seems to work. Can now jump around in either bank and the common routines keep it all intact.

I need some coffee now, couldn't sleep because this thing was on my mind and I woke up at 5.45 am to try to fix it. :S

:)

by on (#67052)
neilbaldwin wrote:
So I split the routine addresses for the common code into their own .globals list and then didn't include that list in the code for the bank where the second version of the common code resides. [emphasis mine]

Exactly. I guess the doc I most recently posted a link to wasn't clear on this.

by on (#67053)
blargg wrote:
neilbaldwin wrote:
So I split the routine addresses for the common code into their own .globals list and then didn't include that list in the code for the bank where the second version of the common code resides. [emphasis mine]

Exactly. I guess the doc I most recently posted a link to wasn't clear on this.


Now I've re-read it, you identified and worked around the problem in your example. I was probably too tired to realise.

Thanks blargg, help and guidance much appreciated.

:)

by on (#67057)
neilbaldwin wrote:
So, I've type ".export" at the front of all my variables and now I'm using the corresponding ".import" in another file to access them but I seem to be getting a lot of "range errors" e.g

cpx #keyBufferEnd-keyBufferStart

I handle this differently:
Code:
; stuff.h
KEY_BUFFER_LEN = 32
.global keyBufferStart

; stuff.s
.include "stuff.h"
.segment "BSS"
keyBufferStart: .res KEY_BUFFER_LEN

;elsewhere.s
.include "stuff.h"
[...]
  cpx #KEY_BUFFER_LEN

by on (#67060)
tepples wrote:
neilbaldwin wrote:
So, I've type ".export" at the front of all my variables and now I'm using the corresponding ".import" in another file to access them but I seem to be getting a lot of "range errors" e.g

cpx #keyBufferEnd-keyBufferStart

I handle this differently:
Code:
; stuff.h
KEY_BUFFER_LEN = 32
.global keyBufferStart

; stuff.s
.include "stuff.h"
.segment "BSS"
keyBufferStart: .res KEY_BUFFER_LEN

;elsewhere.s
.include "stuff.h"
[...]
  cpx #KEY_BUFFER_LEN


Sorry Tepples, mine was probably a bad example. The actual CPX instruction that it was failing on was in a routine that cleared a certain range of variables. So I set an arbitrary label at the start and one at the end and then in the clearing routine just do the CPX with #end-start so that I can add more vars to that group and know that they'll be cleared appropriately.

Having said that, your example did give me a good idea to change some of my constants etc.

:)

by on (#67091)
Using a macro like I outline in the code, tepples' example becomes even simpler to maintain. The macro even automatically defines a second constant, keyBufferSize:

Code:
; stuff.h
.include "macros.inc"
bss_global keyBuffer,32

; stuff.s
.include "stuff.h"

; globals.s
DEFINE_GLOBALS = 1
.include "stuff.h"
... any other includes that define globals ...

;elsewhere.s
.include "stuff.h"
[...]
  cpx #keyBufferSize
  bcs too_big
  lda keyBuffer,x

by on (#67094)
I am not the original poster, and I don't intend to hijack this thread nor his question.... but, I have a question relating to the definition and usage of global variables.

(ps- I am an accomplished C/C++/asm programmer w/ 20+ years exp)

What is wrong with simply using ".globalzp" declarations in a common header file, and then ".word" (or .byte or whatever) in a single ".s" file?

This is analogous to using "int xx = 99;" in a .c file and "extern int xx;" in a .h file (that is included by every .c file, including the one containing 'int xx=99;').

The assembler will automatically declare the proper imports and exports in each ".o" file, and the linker will automagically resolve them.

What is gained by wrapping it all up in macros? It seems like an extra (un-needed) layer of abstraction to me.

Maybe I misunderstood the original question.

Was it a concern for the exact absolute address that the linker would assign to each global, or a concern for syntax of getting ".global / .globalzp" to work at all?

by on (#67098)
Exactly, you can use .global as "extern", and .word/.res as the definition in one source file. Unlike with C, though, there's no type information, so for example a buffer size must be stated as a separate constant, whereas in C you could use sizeof. The proposed macro in my previous message (for which I couldn't implement the sizeof part due to ca65 bullshit bugs) was sort of like the attempts in C to have a single file of globals, with a preprocessor flag that either makes it merely declare them, or actually define them.

See my earlier warning about my approaches :)

So, I agree; .global is the way to go unless you like experimenting with the language more. Before this thread I only knew about .export and .import, which are much more tedious to use. Of course I assemble most of my programs as a single source file, so don't ever even export any names.