5

A similar question was asked here but I read it and didn't really understand the solution.

What I'm trying to do here is make an NES ROM that uses Mapper 24, aka Konami VRC6 Version 1 (the one used by the Japanese Castlevania III with the extra sound channels.) I've had trouble getting this mapper to work in the past, but I've managed to get it running properly on NESASM3 and ASM6. However, ca65 poses a challenge since it doesn't really use .org to assign areas of code. I've created my own .cfg file for this mapper as well as the actual code but neither seems to work properly. The only thing that gets added to the rom is the palette data, which is supposed to be in the .rodata segment (assigned to address $A000) but gets placed at $E000 where my reset handler is supposed to go.

If you're not familiar with the VRC6, it has a fixed ROM bank in $E000, and a 16k switchable bank from $8000-$BFFF, and a 8k switchable bank from $C000-DFFF. This is the explanation that the NESDev Wiki gives, which is sort of misleading, because technically all banks are 8k, it's just that if you set the 16k bank to bank n, it displays bank n at $8000-$9fff and bank n+1 at $A000-$BFFF. Once I figured this out I got it working on other assemblers.

TL;DR:

  • How do I set up multiple banks on an NES ROM using ca65/ld65?
  • Since the banks can be slotted in at will at runtime, how can I tell ca65 that a particular segment is intended to be part of switchable ROM?
  • How do I get the fixed section of ROM to go to $E000 where it belongs?

debugger showing the palette data in the wrong segment

Here's my config file:

# ca65 configuration for Konami VRC6 Type 1 (Akumajou Densetsu)

# Defines areas where code/data is put into memory during linking
# fill=yes forces area to be padded to specified size in output

# Based on the following template:
#
# Linker script for NROM-128 games
# Copyright 2010-2014 Damian Yerrick
#
# Copying and distribution of this file, with or without
# modification, are permitted in any medium without royalty
# provided the copyright notice and this notice are preserved.
# This file is offered as-is, without any warranty.
#
MEMORY
{
    # 2K RAM in NES
    ZP:      start =     0, size =  $100, type = rw, file = %O;
    # Skip the $100 bytes for stack
    WRAM:    start =  $200, size =  $600, type = rw, file = %O;
    
    # Pseudo area for iNES header
    HEADER:  start = $7FF0, size =    16, type = ro, fill = yes, file = %O;
    
    # ROM
    PRGBANK_0:  start = $8000, size = $2000, type = ro, fill = yes, file = %O;
    PRGBANK_1:  start = $A000, size = $2000, type = ro, fill = yes, file = %O;
    FIXED:      start = $E000, size = $1FFA, type = ro, fill = yes, file = %O;
    VECTORS:    start = $FFFA, size =     6, type = ro, fill = yes, file = %O;
    
    # CHR_ROM
    CHR_ROM:   start =     0, size = $2000, type = ro, fill = yes, file = %O;
}

# Defines named segments you refer to in assembler, and sets up order
# of data in output file
# align=$100 allows use of .align directive with a value up to $100
SEGMENTS
{
    ZEROPAGE: load = ZP,      type = zp;
    BSS:      load = WRAM,    type = bss;
    HEADER:   load = HEADER, type = ro;
    CODE:     load = PRGBANK_0, type = ro;
    RODATA:   load = PRGBANK_1, type = ro;
    STARTUP:  load = FIXED, type = ro;
    VECTORS:  load = VECTORS,type = ro;
    CHR:      load = CHR_ROM, type = ro, align = 16, optional = yes;
}

And here is my actual code.


.include "VasmCompatibility.inc"
.include "VRC6_Memory_Map.inc"
.include "C:\cc65\asminc\nes.inc"

; iNES header
.segment "HEADER"

INES_MAPPER = 24
INES_MIRROR = 0 ; 0 = horizontal mirroring, 1 = vertical mirroring
INES_SRAM   = 0 ; 1 = battery backed SRAM at $6000-7FFF
INES_PRG    = 1 ; 16k PRG-ROM bank count
INES_CHR    = 1 ; 8k CHR-ROM file count

.byte 'N', 'E', 'S', $1A ; ID
.byte INES_PRG ; 16k PRG bank count
.byte INES_CHR ; 8k CHR bank count
.byte INES_MIRROR | (INES_SRAM << 1) | ((INES_MAPPER & $f) << 4)
.byte (INES_MAPPER & %11110000)
.byte $0, $0, $0, $0, $0, $0, $0, $0 ; padding




.zeropage


.bss


.segment "CODE"


.segment "RODATA"

palette:
  .BYTE $0D,$29,$1A,$0D,  $0D,$36,$17,$0D,  $0D,$30,$21,$0D,  $0D,$29,$27,$16   ;;background palette
  .BYTE $0D,$1C,$15,$14,  $0D,$02,$38,$3C,  $0D,$1C,$15,$14,  $0D,$02,$38,$3C   ;;sprite palette

.segment "STARTUP"
reset_handler:
    ;reset code goes here
nmi_handler:
    RTI

irq_handler:
    RTI

.segment "VECTORS"
.addr nmi_handler, reset_handler, irq_handler


.segment "CHR"
.incbin "chicago_oblique_aka_ff6_font.chr"
3
  • To start with, it would be helpful if you reduce all of the code down to the issue at hand, wouldn't it? Also, keep in mind that CA65 is not meant to output binary for some weird format by default - nor are we. So please add information about the way that format requires these sections to be layed out. In fact, chances are high that, if you just write up your source and how the result is supposed to look side by side you may already notice how to do this.
    – Raffzahn
    Aug 25, 2021 at 21:50
  • Ok, I removed the reset code, but I left in the palette data because it seems to get written into $E000 even though I told it to go to $A000. As far as the sections go, as I mentioned before, the fixed bank must start at address $E000. The mapper supports anywhere from 2 to 16 8k PRG-ROM banks, which at startup are slotted in as follows: Bank 0 is loaded at $8000. Bank 1 is loaded at $A000. These two make up the 16k switchable bank. Then, Bank 0 is loaded again at $C000. This is the 8k switchable bank. Aug 25, 2021 at 21:58
  • Please take a step back. CA65 does not know anything about a mapper or a banks or bank addresses. More so, it does not output into Banks or whatsoever. It creates an output file structured the way described in the config file. Not more not less. So first thing is to understand what the file for your NES-ROM (or the emulator using it) has to be. Then building a config handling that. I have no idea how that 'ROM is structured, but it wouldn't surprise me if the addresses to be used are not load addresses but relative to ROM/file start. So translate this by hand or let CA65 do it.
    – Raffzahn
    Aug 25, 2021 at 22:14

1 Answer 1

4

How do I set up multiple banks on an NES ROM using ca65/ld65?

Do you have a clear description how that ROM structured? Or, nowadays it's more like, how the 'ROM' file structure for that system (and configuration) is?

CA65 does not know, nor do I (And I'm not going to dissect emulators now).

Since the banks can be slotted in at will at runtime, how can I tell ca65 that a particular segment is intended to be part of switchable ROM?

By giving a segment a certain target address, so the linker will relocate it to that address (range) before outputting.

How do I get the fixed section of ROM to go to $E000 where it belongs?

Well, that again depends on the hardware (or it's emulation), so you got to know how were it has to end up, as you must tell LD65 about it.


Without knowing about that specific system configuration it's hard to give an exact how-to. Also, I guess you do not just want some magic file provided, but understand how it works. 'cause if not you would have simply taken an example for exactly this configuration, right?

So let's take a step back and look how a configuration file describes the way the various chunks (segments) produced by the assembler (or C compiler) is moved into one (or more) binary files by the linker.

[Hint, it's all described in the manual page, but let's dive into anyway]

The LD65 linker understands a number of sections in the config file:

  • MEMORY describes the memory structure of the target system. After all, with a 6500 CPU everything is memory. What areas are there, can they be written to, and so on. It crates an image of the hardware.
  • SEGMENTS describes how the various segments your program consists of are assigned to the memory regions defined with the memory section.
  • FILES adds information about various output file structures handling and assignes optional file types.
  • FORMAT may specify parameters for the file format.
  • FEATURES allows to handle certain features provided by CA65 for code generation.
  • SYMBOLS allows the definition of symbolic values to be used in address calculation when linking.

Sound's a lot, but for 98% of all projects the MEMORY and SEGMENTS section is all you'll need - much like in the config file you most likely used as template.


Let's assume we have a strange 6500 based game system.

[Structures used in further are based in part on said example, but targeted at generic configurations.Still, similarities with existing systems are no accident]

That game system features some RAM in low memory and a 32 KiB program (ROM) area at the upper end.

What MEMORY does is to describe the memory layout this machine:

MEMORY {
  ZP:     start = $0000, size = $0100, type = rw, file="";
  RAM:    start = $0300, size = $0500, type = rw, file="";
  ROM:    start = $8000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF;
}

So there we have the linker's view of the system:

  • 256 bytes RAM (marked as read/write) at 0, called (guess what) 'ZP',
  • 1.25 KiB RAM (RW) starting at $300, named 'RAM', and a
  • 32 KiB ROM (markes as read only) starting at $8000, named 'ROM'

A remarkable fact her is the file attribute.

  • file="" as used for the two RAM section means that we do not want to see these regions in any output file (*1), while
  • file=%O lets the output be punched (*2) into a file using the default name (a.out or as given by command line)

fill=yes and fillval=$FF as used for 'ROM' means that if all segments put in that region, still not fill it, it should be filled up with the value given.

So now the LD65 knows how the address space of the 6500 looks like and is able to fit the segments provided by the assembler. So next is to tell LD65 which segment is to be liked into which memory region and in what sequence they are to be outputted. This is what SEGMENTS establishes:

SEGMENTS {
  ZEROPAGE: load = ZP,  type = zp;
  BSS:      load = RAM, type = bss, align = $100,
  MAIN:     load = ROM, type = ro,  align = $100;
  SEGA:     load = ROM, type = ro,  align = $100;
  SEGB:     load = ROM, type = ro,  align = $100;
}

This list tells what segments found in the assembled source should be put into wich memory regions and in what sequence. After that these regions will be outputted to whatever file was given - or not for regions with file="".

Since 'ZP' as well as 'RAM' will not be outputted (file="" in MEMORY), the resulting binary will be consist of

  • Content of segment 'MAIN',
  • filled up with $FF until the next page boundary due align = $100,
  • followed by 'SEGA',
  • again filled up to page boundary,
  • followed by 'SEGB' and
  • the remaining ROM (until 32 KiB) are filled as well

All filling is due the 'fill=yes'.

After the linker is done, we get a nice binary of 32 KiB. Ready to be burned into an EPROM or some more modern cartridge.

Then again, if we want to run that file in an emulator, we need to produce a file which can be understood by that emulator. Most likely this requires some header. I bet there are tools to create them, but why not let assembler and linker do that job? All we need to do is

  • create some pseudo memory region,
  • have the assembler create useful header data,
  • assign a segment to this region and
  • make sure it gets outputted before real ROM content

A header of 16 bytes will need a region within 'MEMORY' like this:

HDR:    start =     0, size =    16, type = ro, file = %O, fill=yes, fillval=$00;

The segment in some assembler source will most likely be just constants:

    .segment "HEADER"
    .byte    $02

And to get it out into the binary, this segment needs to be put into 'SEGMENTS' as very first entry:

  HEADER:   load = HDR,  type = ro;

Et voila, we receive the same file with the 16 byte header added our emulator wants.


So far a nice development, but we're writing the most advanced game ever for that console, so 32 KiB isn't enough. But we're lucky, there is some kind of banking logic (mapper in gamer terms) to page ROM into the address space using 32 KiB chunks from a game modules ROM chip.

Cool. We only need to make up a ROM image. To start with, we need to create a memory region for every possible ROM bank. In our case 4 of them:

[Now point we leave the example used by the OP, as that was only for a non banking system]

MEMORY {
  ZP:     start = $0000, size = $0100, type = rw, file="";
  RAM:    start = $0300, size = $0500, type = rw, file="";
  ROM0:   start = $8000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF;
  ROM1:   start = $8000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF;
  ROM2:   start = $8000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF;
  ROM3:   start = $8000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF;
}

'MEMORY' now describes a system that has 4 ROM with 32 KiB each all at the same address. Next step is to assign whatever segments our program has to these ROM pages:

SEGMENTS {
  HEADER:   load = HDR,  type = ro;
  ZEROPAGE: load = ZP,   type = zp;
  BSS:      load = RAM,  type = bss, align = $100, define = yes;
  MAIN:     load = ROM0, type = ro,  align = $100;
  OVL1:     load = ROM0, type = ro,  align = $100;
  OVL2:     load = ROM1, type = ro,  align = $100;
  OVL3:     load = ROM2, type = ro,  align = $100;
  OVL4:     load = ROM4, type = ro,  align = $100;
}

Rather straight forward, isn't it? The result is a 256 KiB (+16 Byte Header) binary file, ready to be loaded with 'the' emulator.

Important here maybe that of course various code segments can be combined into a ROM bank. The Linker will warn if they overflow the available space. No matter how much each is filled with useful code and data they are always trimmed to 32 KiB each, as mandated by their entry in 'MEMORY'.


Long story short: Describe your systems layout in 'MEMORY' and assign whatever segments your code produces to memory regions in 'SEGMENTS' and their sequence within the generated binary file.

And yes, there are many more details to consider when doing a game using more than a single bank, but that's not the linkers business - he only arranges and relocates and segments into a file.


*1 - Important hint: When doing a ROM not every segment created during assembly and linking must end up in that ROM (real, physical ROM that is).

*2 - Ya greenhorns might call that writing into files - real men create punch card stacks :))

*3 - When it comes to emulator files (often as well called ROMs), additional segments might be needed which are not to be found in real ROM. Like headers used by that emulator.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .