NES targets
The NES is a particularly challenging compiler target, due to the extremely large number of configurations supported by the various "mapper" ASICs on its cartridges.
The generic nes
target contains only functionality that is generically applicable across mapper chips. Individual mappers each have their own derived target:
Number | Name | Target |
---|---|---|
000 | NROM | nes-nrom
|
001 | MMC1 | nes-mmc1
|
002 | UNROM | nes-unrom
|
003 | CNROM | nes-cnrom
|
004 | MMC3 | nes-mmc3
|
028 | Action 53 | nes-action53
|
030 | UNROM-512 | nes-unrom-512
|
For example, to build a C++ file in a project targeting MMC1, you would use a command like:
$ mos-nes-mmc1-clang++ main.cpp -Os -o main.nes
Mapper Basics[edit | edit source]
The NESdev Wiki has a comprehensive guide on NES Mappers. Here are the basics you'll need to get up and running with a mapper using llvm-mos.
TLDR: What is a mapper?[edit | edit source]
Mapper chips most commonly allow a cartridge have a lot more memory than the NES would otherwise support. They do this by letting you redirect a region of the NES' memory space (a bank) to a different place in your cartridge's memory. Some offer advanced features like synchronizing code with a particular scan line of the screen drawing. This allows for special effects like having a HUD at the bottom of the screen or doing certain kinds of parallax effects. The most basic mappers simply allow you to include an extra RAM chip on the cartridge, with or without battery-backup.
To control the mapper, a game will usually "write" to specific addresses in the ROM. No data is written since the memory is read-only, but the mapper chip "sees" this attempt and responds to the data that is being "written".
PPU Banking[edit | edit source]
The NES allocates an 8KiB address space for CHR-ROM, from 0x0000
to 0x1FFF
. Conceptually these are split into two separate 4KiB regions which can be used for sprite data, background data, or both. So you might have the title screen using the lower 4KiB of ROM for both backgrounds and sprites, but then the main game switches to the upper bank for the sprites.
bank_bg(0); // Use lower half of CHR-ROM for backgrounds
bank_spr(1); // Use upper half of CHR-ROM for sprites
Note: This form of banking is built into the NES and does not require any mapper. The rest of this article is referring to mapper-based banking when it uses "banking" without qualification.
CHR-ROM Banking[edit | edit source]
CHR-ROM banking allows you to select a different region of the physical memory in your cartridge to map to some region of the 8 KiB CHR-ROM. For example, with MMC1, you can have up to 128 KiB of CHR-ROM in your cartidge/image and select any 4 KiB bank using a 5-bit address (0-31).
// Map PPU CHR-ROM to the 3rd and 4th 4KiB regions of physical ROM
set_chr_bank_0(2);
set_chr_bank_1(3);
PRG-RAM[edit | edit source]
The addresses available for cartridges to use in the PRG-ROM space range from 0x4020
to 0xFFFF
. The simplest cartridges would connect a 32 KiB ROM chip and enable or disable it based on the highest bit of the memory address being read. Using the address range between 0x4020
and 0x7FFF
is a bit more complicated since it takes more bits of the address to distinguish it from other ranges. It is significantly easier to use the 0x6000
-0x7FFF
range, so a lot of cartridges included an 8KiB RAM chip behind a simple logic gate to check the upper bits of the address. This area is often called Work RAM (WRAM).
Depending on the mapper you may need to call a function to enable WRAM. E.g. with MMC3 you would do:
set_wram_mode(WRAM_ON);
PRG-RAM Banking[edit | edit source]
This gets a bit more complicated [ TODO ]
PRG-ROM Banking[edit | edit source]
This gets quite a bit more complicated [ TODO ]
Mapper/iNES Header Configuration[edit | edit source]
As far as is possible, mappers are configured by assigning symbol values. These control the contents of the iNES 2.0 header in the resulting binary, as well as the addresses assigned to program sections. Some of the following values have logical sizes less than the full 32-bits available for symbol values. In such cases, only the lowest order bits are considered. For example, __prg_nvram_size_raw
corresponds to the high nibble of header field 10. This means that the lowest nibble of __prg_nvram_size_raw
is mapped to the high nibble of header field 10.
You can set these symbols with the macros in ines.h
or manually - using inline assembly or a separate assembly file. For example:
// main.c
#include <ines.h>
MAPPER_CHR_ROM_KB(0);
MAPPER_CHR_RAM_KB(8);
// main.c
asm(".globl __chr_rom_size\n"
"__chr_rom_size = 0\n"
".globl __chr_ram_size\n"
"__chr_ram_size = 8\n");
; config.s
.global __prg_rom_size, __chr_rom_size, __prg_ram_size
.global __mirroring, __four_screen
; Kilobytes
__prg_rom_size = 512
__chr_rom_size = 256
__prg_ram_size = 8
; Flags
__mirroring = 1 ; horizontal mirroring
Symbol | Description |
---|---|
__prg_rom_size
|
KiB of PRG-ROM |
__chr_rom_size
|
KiB of CHR-ROM |
__prg_ram_size
|
KiB of PRG-RAM |
__prg_nvram_size
|
KiB of PRG-NVRAM |
__chr_ram_size
|
KiB of CHR-RAM |
__chr_nvram_size
|
KiB of CHR-NVRAM |
__mirroring
|
Bit 0 of field 6 |
__battery
|
Bit 1 of field 6 |
__trainer
|
Bit 2 of field 6 |
__four_screen
|
Bit 3 of field 6 |
__mapper
|
Mapper number. |
__console_type
|
Low two bits of field 7 |
__timing
|
Low two bits of field 12 |
__ppu_type
|
Low nibble of byte 13 when Vs. System |
__hw_type
|
High nibble of byte 13 when Vs. System |
__extended_console_type
|
Low nibble of byte 13 when Extended Console Type |
__misc_roms
|
Low two bits of byte 14 |
__default_expansion_device
|
Low six bits of byte 15 |
__prg_rom_size_raw
|
The most significant nibble is the lower nibble of field 9, and the least significant byte is field 4. Overrides __prg_rom_size .
|
__chr_rom_size_raw
|
The most significant nibble is the upper nibble of field 9, and the least significant byte is field 5. Overrides __chr_rom_size .
|
__prg_ram_size_raw
|
Raw value of low 4 bits of header field 10. Overrides __prg_ram_size .
|
__prg_nvram_size_raw
|
Raw value of high 4 bits of header field 10. Overrides __prg_nvram_size .
|
__chr_ram_size_raw
|
Raw value of low 4 bits of header field 11. Overrides __chr_ram_size .
|
__chr_nvram_size_raw
|
Raw value of high 4 bits of header field 11. Overrides __chr_nvram_size .
|
Linker layout[edit | edit source]
Sections[edit | edit source]
PRG-ROM[edit | edit source]
If PRG-ROM banking is disabled, then data can be placed in PRG-ROM using the regular .rodata
C sections. Otherwise, data can be placed into a specific PRG-ROM bank using section .prg_rom_<bankno>
or any section that begins with .prg_rom_<bankno>.
. The load addresses of PRG-ROM banks begin at 0x01000000
. This allows the bottom 24-bits to represent a logical PRG-ROM address space, while the high byte being 0x01
indicates that the address is PRG-ROM.
CHR-ROM[edit | edit source]
If CHR-ROM banking is disabled, then data can be placed in CHR-ROM using the .chr_rom
section or any section that begins with .chr_rom.
. Otherwise, data can be placed into a specific CHR-ROM bank using section .chr_rom_<bankno>
or any section that begins with .chr_rom_<bankno>.
. The logical addresses of CHR-ROM banks begins at 0x02000000
. This allows the bottom 24-bits to represent a logical CHR-ROM address space, while the highest byte being 0x02
indicates that the address is CHR-ROM.
PRG-RAM[edit | edit source]
If PRG-RAM banking is disabled, then data can be placed in PRG-RAM using the .prg_ram
section or any section that begins with .prg_ram.
. Otherwise, variables can be placed into a specific PRG-RAM bank using section .prg_ram_<bankno>
or any section that begins with .prg_rom_<bankno>.
. The PRG-RAM is not initialized at program start, so any initializers given in C or data provided in assembly are ignored.
INCLUDE Configuration[edit | edit source]
In some cases, the linker script semantics are insufficiently powerful to configure the linker alone. In this case, linker scripts can be composed by `INCLUDE`-ing script files to build up a custom linker script (rather than using the default). The following script files are common to all targets.
You can either include these in your own projects linker script, or pass -Tfilename
to the linker. For example:
# makefile
LDFLAGS = -lneslib -lnesdoug -Tcommon.ld -Tc-in-prg-ram.ld -Tprg-rom-banked-mode-0
Name | Description |
---|---|
common.ld
|
Functionality common to all linker scripts for a given target. Must be included. Included in default linker script. |
c-in-ram.ld
|
Place the writable C sections (static/global variables and such) into NES RAM. Exactly one c-in- script must be included. Included in default linker script.
|
ELF address space[edit | edit source]
On the NES target, the ELF address space is split into three sections:
Type (bits 31-24) | Bits | Name | Values |
---|---|---|---|
0x00 (CPU)
|
23 - 16 | Bank | 0x00 .. 0xFF
|
15 - 0 | Address | 0x0000 .. 0xFFFF
Matches 16-bit address for the CPU | |
0x01 (CHR)
|
23 - 0 | Address | 0x000000 .. 0xFFFFFF
|
Mapper-specific information[edit | edit source]
NROM, CNROM[edit | edit source]
INCLUDE Configuration[edit | edit source]
Name | Description |
---|---|
c-in-prg-ram.ld
|
Place the writable C sections (static/global variables and such) into PRG-RAM instead of system RAM. |
MMC1[edit | edit source]
The 512KiB PRG-ROM mode and PRG-ROM bank modes other than 3 are not yet supported, since we don't yet have a general mechanism for copying arbitrary code into all banks to simulate a fixed bank at the end of the address space.
INCLUDE Configuration[edit | edit source]
Name | Description |
---|---|
c-in-prg-ram-0.ld
|
Place the writable C sections (static/global variables and such) into PRG-RAM instead of system RAM. |
prg-rom-banked.ld
|
Up to 256KiB of banked PRG-ROM. |
prg-rom-fixed.ld
|
Up to 32KiB of fixed PRG-ROM. |
MMC3[edit | edit source]
By default, the mapper is configured such that 24KiB of PRG-ROM is fixed, while a 8KiB bank is available at 0x8000. This can be altered by INCLUDEing alternative linker scripts.
In banking mode 0, the even banks are available at 0x8000, and the odd banks are available at 0xa000. In banking mode 1, the even banks are available at 0xc000, and the odd banks at 0xa000. This assignment can be changed on a per-bank basis by defining the symbol __prg_rom_NN
, where NN
is the bank number.
INCLUDE Configuration[edit | edit source]
Name | Description |
---|---|
c-in-prg-ram.ld
|
Place the writable C sections (static/global variables and such) into PRG-RAM instead of system RAM. |
prg-rom-banked-8.ld
|
One mappable 8KiB bank at 0x8000, and 3 contiguous fixed banks (24 KiB total) starting at 0xa000. |
prg-rom-banked-mode-0.ld
|
Two separately mappable 8KiB banks at 0x8000 and 0xa000, and 2 contiguous fixed banks starting at 0xc000. |
prg-rom-banked-mode-1.ld
|
Two separately mappable 8KiB banks at 0xc000 and 0xa000, the next-to-last bank fixed at 0x8000, and the last bank fixed at 0xe000. |
prg-rom-fixed.ld
|
Up to 32KiB of fixed PRG-ROM. |
Action 53[edit | edit source]
This mapper is the primary target for the NES Game Jam "NESDEV Compo." Because the common developer use case for this mapper is to make a ROM that's a part of a multi-cart, the defaults for this target are set for that purpose. The defaults are as follows: 64KiB PRG-ROM, 32KiB CHR-RAM, with 16KiB banking is available at 0x8000. This can be altered by changing the standard iNES configuration symbols, but adds another symbol __supervisor_outer_bank
which will be ORed with the value written to $80 if you need to change the switchable bank to 0xc000.