Mapper implementation MMC1 assembler 6502 nes

Games not using mappers on the NES are limited to 16kb PRG ROM (code storage) and 8kb CHR ROM (graphics storage). With the development of game development on the NES, the question arose of how to increase these restrictions and mapper chips came to the rescue. What are mappers, we will analyze today and how to use them in your code.

What are mappers?

Mappers are microcircuits that are able to replace the data supplied to the outputs (ports) in the cartridge. For example, a hypothetical simplified mapper is able to switch between two memory chips on a cartridge and connect them to port pins in the console. Then we get two banks of memory with a code of 16Kb, in general, already 32Kb.

Mapper MMC1

This is an ASIC (Application Specific Integrated Circuit) capable of switching PRG memory banks and, accordingly, CHR. And also change the nametable mirroring on the fly. This mapper has several options for using SNROM, SOROM, SZROM, SXROM, and so on, the main difference between them is the number of switched memory banks, and the presence / absence of RAM for PRG / CHR. Many battery-powered savegames such as The Legend of Zelda used RAM to save and restore the game state so that changes were not lost and a battery was built in to power the RAM. But for my purposes, I chose the SLROM configuration, without RAM memory and with a total of 128kb of memory for graphics and program. The same mapper configuration was in Chip ‘n Dale Rescue Rangers.

Memory banks

The mapper contains the following memory banks:

  1. $6000 – $7FFF – RAM is not used in our configuration

  2. $8000 – $BFFF – 16kb PRG ROM bank, switchable, fixed on first page by default

  3. $C000 – $FFFF – 16kb PRG ROM bank, switchable, fixed on last page

  4. $0000 – $0FFF – 4kb CHR ROM bank switchable

  5. $1000 – $1FFF – 4kb CHR ROM bank switchable

Mapper ports

The mapper has serial ports for its control, which means that it is necessary to sequentially write 1 or 0 to such a port. The mapper has the following registers:

  • Load register – $8000 – $FFFF

  • Control register – $8000 – $9FFF

  • CHR Bank 0 – $A000 – $BFFF

  • CHR bank 1 – $С000 – $DFFF

  • PRG bank – $E000 – $FFFF

Headings ines

First of all, in order for the mapper to work correctly in our project, we need to write the following headers


.segment "HEADER"
	.byt "NES",$1A
	.byt 8 				; 8 x 16kB PRG block. 128kb
	.byt 16 			; 16 x 8kB CHR block. 128kb
	.byt 17              
	.byt 02             ; mapper

Thus, we tell the emulators that it will use the MMC1 mapper with a 128kb configuration for graphics and code.

ld65 linker configuration

Added 8 ROM pages and 16 ROM pages with graphics
MEMORY {
  HEADER: start=$00, size=$10, fill=yes, fillval=$00;
  ZEROPAGE: start=$10, size=$ff;
  STACK: start=$0100, size=$0100;
  OAMBUFFER: start=$0200, size=$0100;
  RAM: start=$0300, size=$0500;

  ROM1: start=$8000, size=$4000, fill=yes, fillval=$ff;
  ROM2: start=$8000, size=$4000, fill=yes, fillval=$ff;
  ROM3: start=$8000, size=$4000, fill=yes, fillval=$ff;
  ROM4: start=$8000, size=$4000, fill=yes, fillval=$ff;
  ROM5: start=$8000, size=$4000, fill=yes, fillval=$ff;
  ROM6: start=$8000, size=$4000, fill=yes, fillval=$ff;
  ROM7: start=$8000, size=$4000, fill=yes, fillval=$ff;

  ROM: start=$C000, size=$4000, fill=yes, fillval=$ff;
  # 16 CHR ROM
  CHRROM0: start=$0000, size=$1000;
  CHRROM1: start=$0000, size=$1000;
  CHRROM2: start=$0000, size=$1000;
  CHRROM3: start=$0000, size=$1000;
  CHRROM4: start=$0000, size=$1000;
  CHRROM5: start=$0000, size=$1000;
  CHRROM6: start=$0000, size=$1000;
  CHRROM7: start=$0000, size=$1000;
  CHRROM8: start=$0000, size=$1000;
  CHRROM9: start=$0000, size=$1000;
  CHRROM10: start=$0000, size=$1000;
  CHRROM11: start=$0000, size=$1000;
  CHRROM12: start=$0000, size=$1000;
  CHRROM13: start=$0000, size=$1000;
  CHRROM14: start=$0000, size=$1000;
  CHRROM15: start=$0000, size=$1000;
  CHRROM_1: start=$1000, size=$1000;
}

SEGMENTS {
  HEADER: load=HEADER, type=ro, align=$10;
  ZEROPAGE: load=ZEROPAGE, type=zp;
  STACK: load=STACK, type=bss, optional=yes;
  OAM: load=OAMBUFFER, type=bss, optional=yes;
  BSS: load=RAM, type=bss, optional=yes;
  DMC: load=ROM, type=ro, align=64, optional=yes;
  CODE: load=ROM, type=ro, align=$0100;

  CODE_1: load=ROM1, type=ro, align=$0100;
  CODE_2: load=ROM2, type=ro, align=$0100;
  CODE_3: load=ROM3, type=ro, align=$0100;
  CODE_4: load=ROM4, type=ro, align=$0100;
  CODE_5: load=ROM5, type=ro, align=$0100;
  CODE_6: load=ROM6, type=ro, align=$0100;
  CODE_7: load=ROM7, type=ro, align=$0100;

  RODATA: load=ROM, type=ro, align=$0100;
  VECTORS: load=ROM, type=ro, start=$FFFA;

  CHR0: load=CHRROM0, type=ro, align=16, optional=yes;
  CHR1: load=CHRROM1, type=ro, align=16, optional=yes;
  CHR2: load=CHRROM2, type=ro, align=16, optional=yes;
  CHR3: load=CHRROM3, type=ro, align=16, optional=yes;
  CHR4: load=CHRROM4, type=ro, align=16, optional=yes;
  CHR5: load=CHRROM5, type=ro, align=16, optional=yes;

  CHR_1: load=CHRROM_1, type=ro, align=16, optional=yes;
}

In fact, this item was one of the most difficult, you need to understand 2 simple things:

  1. The bank contains pages and the pages have the same memory bank address

  2. Banks must be filled with $ff

  3. All interrupt vectors must be located in a fixed page, I have $C000 which actually was, and the reset procedure should also be in a fixed memory area

After that, we must add the missing new sections to our project file and we can compile. True, at startup, there may not be graphics.

Graphic arts

To do this, we need to split our 8kb chr file into two files of 4096 bytes each, I did this using the split command

 split -b4096 test.chr

rename the resulting files in accordance with our wishes, and import them into the CHR page segments.

.segment "CHR0"
	.incbin "test_1_1.chr"
.segment "CHR1"
    .incbin "test_1_2.chr"
.segment "CHR2"
	.incbin "test_2_1.chr"
.segment "CHR3"
    .incbin "test_2_2.chr"

Approximately it should turn out like this, now we compile our project and the graphics that were before the migration to the MMC1 mapper from NROM will appear.

Important note

Before moving on to working with the mapper, you need to know a few things:

  1. Registers have a pointer to the order in which bits are written; to reset it, you only need to write 7 bits to the port before working with it.

  2. The least significant bit (0th from right to left) is taken as the value for writing to the serial port

  3. When writing to any range address (which I mentioned above), the value will be redirected to the starting address of the register

Mapper control functions

Based on the comments above, I wrote general procedures for managing the mapper

Writing to register controll
.proc writeToMapper
    STA $8000         ; first data bit
    LSR A             ; shift to next bit
    STA $8000         ; second data bit
    LSR A             ; etc
    STA $8000
    LSR A
    STA $8000
    LSR A
    STA $8000         ; config bits written here, takes effect immediately

    RTS
.endproc

This procedure is needed to initialize the mapper and change the mirroring

Writing to CHR 0
.proc changeChrZerro
    STA $A000         ; first data bit
    LSR A             ; shift to next bit
    STA $A000         ; second data bit
    LSR A             ; etc
    STA $A000
    LSR A
    STA $A000
    LSR A
    STA $A000         ; config bits written here, takes effect immediately

    RTS
.endproc
CHR register 1

.proc changeChrFirst
    STA $C000         ; first data bit
    LSR A             ; shift to next bit
    STA $C000         ; second data bit
    LSR A             ; etc
    STA $C000
    LSR A
    STA $C000
    LSR A
    STA $C000         ; config bits written here, takes effect immediately

    RTS
.endproc
PRG page change
.proc changePrgBank
    STA $C000         ; first data bit
    LSR A             ; shift to next bit
    STA $C000         ; second data bit
    LSR A             ; etc
    STA $C000
    LSR A
    STA $C000
    LSR A
    STA $C000

    RTS
.endproc

Next, consider the reset procedure for this mapper

hidden text
.proc resetMapper
    LDA #$80
    STA $8000

    RTS
.endproc

.proc resetMapperProcedure
    INC resetMapper ; тут в порт $8000 будет записан %1000 0001

    RTS
.endproc

If we turn to the documentation, we will see that in order to initialize the mapper, it is necessary to write the value %1000 0001 to port $8000 where:

  • Bit 7 locks memory bank $C000 – $FFFF

  • 0th bit enables shift mode, least significant bit as value

We call reset on our reload vector.

Change CHR page

We have prepared procedures for communicating with mapper registers in order to switch the chr page, we just need to do the following:

LDA #%00000000 ; номер страницы по порядку 0 и 1 будут страницами 0 и 1 соответственно
JSR changeChrZerro

In this case, the specified page will become parrent table 0, and the next parent table 1

The next function is to change the mirroring, so I set the vertical mirroring

.proc setVerticalMirror
    JSR resetMapper ; для начала 7 бит должен быть 1 что бы сбросить счетчик
    LDA #%00001110    ; 8KB CHR, 16KB PRG, $8000-BFFF swappable, vertical mirroring
    JSR writeToMapper ; записываем в маппер значение

    RTS
.endproc

How about horizontal mirroring?

.proc setHorizontalMirror
    JSR resetMapper
    LDA #%00001111    ; 8KB CHR, 16KB PRG, $8000-BFFF swappable, vertical mirroring
    JSR writeToMapper

    RTS
.endproc

Pretty simple, don’t you agree? And the last page change PRG

.proc changePrgToFirst
    LDA #%00000000 ; порядковый номер страницы 4 бита десятичное число 0-15
    JSR setPrgBank
    RTS
.endproc

Code organization

For myself, I chose the following code organization, all functions related to levels will be stored in their pages. And the common code like the animation of the hero of some enemies, the background loading functions, the palette, the attributes will be in a common fixed memory page.

As a conclusion

As a conclusion, I would like to say that this is my first acquaintance with MMC1. And in principle, if you figure it out, everything is not so difficult. I also want to say that this linker configuration works on the emulator, but it has not yet been tested on real hardware, but in the near future I want to assemble a cartridge for testing with rewritable memory, or purchase a flash cartridge. I shortened the theory a little because you can walk around and around one port for a long time and not understand anything.

useful links

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *