Game development for NES in C. Chapter 24. Appendix 2 - working with memory banks

Published on March 25, 2018

Game development for NES in C. Chapter 24. Appendix 2 - working with memory banks

Original author: Nesdoug
  • Transfer
  • Tutorial
The final part of the cycle. In this chapter, we will look at working with the MMC3 mapper using examples
<<< previous Source We didn’t use switching memory banks before, but now it’s time to learn the MMC3 mapper. Without a mapper, you can use 32 kilobytes of PRG ROM for code and 8 kilobytes of CHR ROM for graphics. Mapper allows you to get around this barrier.

image




We will keep in mind the release of our game on a real cartridge. [Manual] (http://kevtris.org/mappers/mmc3/) claims that we have the following options:

- Up to 64K PRG, 64K CHR
- Up to 512K PRG, 64K CHR
- Up to 512K PRG, VRAM
- Up to 512K PRG , 256K CHR
- Up to 128K PRG, 64K CHR, 8K CHR RAM The

list is not complete. Choose the most compact format, 64 / 64k. You must specify this in the header of the cartridge image so that the emulator knows about it. Image format documentation is available on the wiki :

INES Header
.byte $4e,$45,$53,$1a
.byte $04 ; = 4 x 0х4000 байт PRG ROM
.byte $08 ; = 8 x 0х2000 байт CHR ROM
.byte $40 ; = маппер №4 - MMC3



Next, you need to register the memory banks in .cfg:

Fragment nes.cfg
#Адреса банков ROM:
#все по адресу $8000, потому что они будут подставляться туда маппером
PRG0: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG1: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG2: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG3: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG4: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG5: start = $a000, size = $2000, file = %O ,fill = yes, define = yes;
PRG6: start = $c000, size = $2000, file = %O ,fill = yes, define = yes;
PRG7: start = $e000, size = $1ffa, file = %O ,fill = yes, define = yes;

# Вектора прерываний в хвосте ROM
VECTORS: start = $fffa, size = $6, file = %O, fill = yes;




All memory banks will be loaded at the same address $ 8000. The executable code will be in the last non-reloadable bank, and it can be placed at any address. Memory allocation is the most difficult when working with the mapper, here you need to be careful.

Segments must be registered in the config:
nes.cfg
SEGMENTS {
HEADER: load = HEADER, type = ro;
CODE0: load = PRG0, type = ro, define = yes;
CODE1: load = PRG1, type = ro, define = yes;
CODE2: load = PRG2, type = ro, define = yes;
CODE3: load = PRG3, type = ro, define = yes;
CODE4: load = PRG4, type = ro, define = yes;
CODE5: load = PRG5, type = ro, define = yes;
CODE6: load = PRG6, type = ro, define = yes;
STARTUP: load = PRG7, type = ro, define = yes;
CODE: load = PRG7, type = ro, define = yes;
VECTORS: load = VECTORS, type = ro;
CHARS: load = CHR, type = rw;

BSS: load = RAM, type = bss, define = yes;
HEAP: load = RAM, type = bss, optional = yes;
ZEROPAGE: load = ZP, type = zp;
#OAM: load = OAM1, type = bss, define = yes;
}



The OAM segment is not used in this example.

And now we’ll write something noticeable in each bank and see how it fits in the ROM file. For example, take the words Bank0, Bank1, and so on. These words will be displayed on the screen, switching banks with the Start button.

Placement of a variable in the desired bank is done through the PRAGMA directive:
lesson19.c
#pragma rodata-name (“CODE0”)
#pragma code-name (“CODE0”)
const unsigned char TEXT1[]={
“Bank0”};

#pragma rodata-name (“CODE1”)
#pragma code-name (“CODE1”)
const unsigned char TEXT2[]={
“Bank1”};

#pragma rodata-name (“CODE2”)
#pragma code-name (“CODE2”)
const unsigned char TEXT3[]={
“Bank2”};




When you click Start, the memory bank at the addresses $ 8000- $ 9FFF is switched off, and the first 5 bytes are displayed on the screen
Output text from the bank
void Draw_Bank_Num(void){ // функция вывода на экран
PPU_ADDRESS = 0x20;
PPU_ADDRESS = 0xa6;
for (index = 0;index < 5;++index){
PPU_DATA = TEXT1[index];
}
PPU_ADDRESS = 0;
PPU_ADDRESS = 0;
}




TEXT1 is determined at the compilation stage and at the start of the console points to the zero bank. If you change the bank, this address will remain unchanged, and in any case, the text from the addresses $ 8000-8004 will be displayed. Banks switch like this:
Bank switching
if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){
++PRGbank;
if (PRGbank > 7) PRGbank = 0;
*((unsigned char*)0x8000) = 6; // переключить банк PRG по адресу $8000
*((unsigned char*)0x8001) = PRGbank;
Draw_Bank_Num(); //вывод текста из нового банка



The address $ 8000 belongs to ROM, but the record is intercepted there by the mapper. Next is the bank number for loading. Details as usual in the [wiki] (http://wiki.nesdev.com/w/index.php/MMC3):

A bit of confusion is caused by the random equality of the addresses of the beginning of the bank and the service register of the mapper. We can transfer the bank to the addresses $ A000- $ BFFF:

*((unsigned char*)0x8000) = 7; // Адрес начала банка PRG - $A000
*((unsigned char*)0x8001) = which_PRG_bank;


But the management registers still remain at the addresses of $ 8000 and $ 8001.

I also added the initialization code to the beginning of main (). This point is not documented, but apparently, after RESET, the correct loading of only the last bank is guaranteed at the addresses $ E000- $ FFFF. All of our initialization code should only be located there.

This scheme of working with memory banks (when their beginning is fixed at one address) is very inconvenient. Usually, an array with pointers to data structures and functions is stored at the beginning of each bank. Then you can go into them indirect transitions, or a faster focus with the stack . There Assembler, but worth it.

In any case, I want to add a background scroll with parallax. To do this, every 4 frames, switch the CHR ROM bank to the PPU memory area - the tiles will be picked up from there. MMC3 splits CHR ROM into banks of 64 tiles, this is 0x400 bytes. We will make an animated waterfall, in each set of tiles they will be shifted by 1 pixel - when changing banks, an animation will turn out.

image

Link to the source code, the next frame is shown on the Start button:
Dropbox
Github

MMC3 also knows how to count the lines displayed on the TV. This is usually done through a zero sprite, but it works once per frame - sometimes more is needed. To simulate background parallax, we will change the scroll position every 20 lines. MMC3 will cause interrupts at the right moments, and in its handler the scroll will be set to the desired position. The handler is written in assembler, because when working with C, you can accidentally damage the stack when calling the function http://www.cc65.org/faq.php#IntHandlers .

At the start of the prefix, interrupts are turned off, they must be included in main ().

asm (“cli”); // Включить прерывания


Pointers in the interrupt vector at the end of the reset.s file should point to the correct handlers. Now you can set up line counting:

*((unsigned char*)0xe000) = 1; // Выключить MMC3 IRQ
*((unsigned char*)0xc000) = 20; // Вызвать прерывание через 20 строк
*((unsigned char*)0xc001) = 20;
*((unsigned char*)0xe001) = 1; // Снова включить MMC3 IRQ


Apparently, the first line is not taken into account, because the interrupt is triggered after 21 lines.

It is also highly desirable to pull the horizontal scroll during a very short H-blank period - the time the beam travels to the beginning of the line. If this is not taken into account, there will be a slight image distortion. If you know where to look, it is noticeable in many games.

The MMC3 interrupt fires exactly in the H-blank, but its duration is not enough to go to the handler. So I put there a simple loop that waits about 100 beats until the next H-blank. This point may not be accurately processed by some emulators. Real games do not wait for the next line and do a scroll shift in the area with a solid fill. After the scroll shift, wait for the next 20 lines, and repeat again.

If you want to see it with your own eyes, fix the loop restriction in the handler. A shift of literally 1 repetition will be seen - the H-blank is really so short.

image

The start still switches banks, but here it is not noticeable.

Dropbox
Github

If you are too lazy to tinker with recompilation, then the gif is:

image

The timing cycle is shortened by 1 revolution - the scroll changes a few pixels to the end of the line. Distortion is visible at the right end of the bottom line of each horizontal layer. It changes every frame, so that everything dances on the screen. If the interruption works in the middle of the line, then it will be very bad.

This scroll operation allows you to realize the parallax effect. The query 'NES parallax scrolling' on YouTube will give clear examples. Again, note that in most games, background layers are separated by a solid fill.