← All articles

MSX Programming

Saving to MSX FlashROM Cartridges

Tested Z80 routines and practical setup guidance for ASCII16-X, MegaFlashROM SCC+ SD, Carnivore2, and compatible AMD/JEDEC-style NOR Flash cartridges.

1. Why FlashROM saving is confusing

A Flash cartridge looks like ROM during normal execution, but compatible NOR Flash chips recognize special write sequences as commands. The CPU is still performing ordinary memory writes; it is the Flash chip that recognizes the address/value pattern and switches into program or erase mode.

The same basic AMD/JEDEC command set is used by the Flash chips found in ASCII16-X, MegaFlashROM SCC+ SD and Carnivore2. The complicated part is usually not the program byte command itself. The complications are mapper setup, sector geometry, cartridge write protection, and knowing what the Flash returns while an operation is still in progress.

2. The most important rule: Flash changes 1 → 0

An erased NOR Flash byte contains FFh. Programming can clear any of those 1 bits to 0. Programming cannot restore a cleared 0 bit back to 1. Restoring 0 → 1 requires erasing the entire sector containing the byte.

FFh → F0h → C0h → 00h    ; valid without erasing
FFh → F0h → F8h           ; invalid: some 0 bits would need to become 1

A useful software test before programming a byte is:

(old_value AND new_value) = new_value

If that expression is true, the update requires only 1 → 0 transitions and can be programmed without erasing. If it is false, the containing sector must be erased first.

Writing 00h consumes all eight available 1-bits in that byte. After that, the byte cannot be changed to any other value until its sector is erased.

3. Execute Flash-writing code from RAM

Do not execute the program/erase routine from the FlashROM that is being placed into command/program/erase mode. Copy the writer into normal MSX RAM and call it there. A location such as C000hE000h is convenient if it is available to the game.

This also avoids problems when the mapper banks being modified are the same banks from which code would otherwise be fetched.

4. Standard command sequences

Byte program

AAh → unlock address 1
55h → unlock address 2
A0h → unlock address 1
data → target Flash address

Sector erase

AAh → unlock address 1
55h → unlock address 2
80h → unlock address 1
AAh → unlock address 1
55h → unlock address 2
30h → any address inside the target sector

In the tested mapper arrangement below, those MSX-visible command locations are 4AAAh and 4555h. The target address must, of course, be in the Flash sector and bank you actually intend to modify.

5. Tested Z80 routines

The following routines are based on the code used and tested by Max Iwamoto with ASCII16-X, MegaFlashROM and Carnivore2, including the Illusion City ROM build.

; ------------------------------------------------------------
; FLASH PROGRAM
;
; HL = source in RAM
; DE = destination address in mapped Flash
; BC = number of bytes
;
; Return:
;   Cy = 0 success
;   Cy = 1 Flash operation failed
; ------------------------------------------------------------

_flash_program:
        LD      A,AAh           ; Unlock
        LD      (4AAAh),A
        LD      A,55h
        LD      (4555h),A
        LD      A,A0h           ; Byte-program command
        LD      (4AAAh),A

        LD      A,(HL)
        LD      (DE),A
        CALL    _check_flash_status
        RET     C               ; Error

        INC     HL
        INC     DE
        DEC     BC
        LD      A,B
        OR      C
        JR      NZ,_flash_program
        RET


; ------------------------------------------------------------
; FLASH SECTOR ERASE
;
; Example below targets a sector visible at 8000h.
;
; BC = 8030h is deliberately chosen because:
;   BC itself is an address inside the target sector
;   B = 80h = erase-setup command
;   C = 30h = sector-erase confirmation command
;
; Return:
;   Cy = 0 success
;   Cy = 1 Flash operation failed
; ------------------------------------------------------------

_flash_erase:
        LD      HL,4AAAh
        LD      DE,4555h
        LD      BC,8030h        ; Address inside target sector
                                ; B=80h, C=30h

        LD      (HL),L          ; AAh -> 4AAAh
        LD      A,E             ; A = 55h
        LD      (DE),A          ; 55h -> 4555h
        LD      (HL),B          ; 80h erase setup

        LD      (HL),L          ; AAh again
        LD      (DE),A          ; 55h again

        LD      A,C             ; 30h
        LD      (BC),A          ; Sector erase confirm

        LD      A,FFh           ; Expected result after erase
        LD      D,B
        LD      E,C             ; DE = address being polled

; ------------------------------------------------------------
; FLASH STATUS
;
; A  = expected final value
; DE = Flash address to poll
;
; Cy = 0 success
; Cy = 1 failure
; ------------------------------------------------------------

_check_flash_status:
        PUSH    BC
        LD      C,A

.loop:
        LD      A,(DE)          ; Flash status while busy
        XOR     C
        JP      P,.exit         ; DQ7 matches expected: finished

        XOR     C               ; Restore actual status value
        AND     20h             ; DQ5: exceeded timing limits?
        JR      Z,.loop

        ; DQ5 became 1. Re-read DQ7 once because the operation
        ; may have finished at about the same instant.
        LD      A,(DE)
        XOR     C
        JP      P,.exit

        SCF                     ; Cy=1: operation failed
        LD      A,F0h
        LD      (DE),A          ; Reset Flash to read-array mode

.exit:
        POP     BC
        RET

Why BC=8030h?

This is a code-size trick, not a Flash requirement. 8030h is simply an address inside the target sector, but its high and low bytes are also exactly the two erase command values needed later:

B = 80h     ; erase setup
C = 30h     ; sector erase confirm

The final 30h does not need to be written to the first byte of the sector. Writing it anywhere inside the target sector identifies which sector the Flash should erase.

6. What DQ7 and DQ5 actually mean

While a program or erase operation is active, a read from the target address is not necessarily the normal stored ROM byte. The Flash temporarily returns operation status.

BitMeaning while an embedded operation is active
DQ7Returns the inverse of the expected DQ7 value while busy. When DQ7 matches the expected final value, the operation has completed.
DQ6Toggles between successive reads while an operation is active. Some Flash routines use this as an alternate polling method.
DQ5“Exceeded timing limits.” Indicates that the Flash could not complete the requested operation normally.

The routine uses:

LD      A,(DE)
XOR     C
JP      P,.exit

After XOR C, bit 7 is zero when DQ7 equals the expected DQ7. JP P therefore detects successful completion without needing a separate mask and compare.

If DQ7 still does not match, the routine restores the original status byte and checks DQ5:

XOR     C
AND     20h
JR      Z,.loop

If DQ5 becomes set, the routine reads DQ7 one more time. That second check matters because completion and the observation of DQ5 can happen at nearly the same time. If DQ7 still does not match, the routine returns carry set and writes F0h to return the Flash to normal read-array mode.

If a debugger repeatedly shows FFh during a supposed programming operation, do not assume that the polling routine is wrong. It often means the command sequence was never accepted: the wrong mapper is active, the wrong bank is mapped, the command addresses do not reach the intended physical Flash locations, or write access is disabled.

7. Flash sector geometry: bottom-boot, top-boot and uniform

Flash documentation uses the word sector or erase block for the smallest area that can be erased independently. These are physical regions inside the Flash chip. They are not MSX 16 KB pages and they are not mapper banks. To avoid three different meanings for “page,” this article calls them Flash sectors.

The three layouts you are likely to encounter

Physical layoutSmall sectorsRest of the chipPractical consequence
Bottom-bootThe lowest physical 64 KB is split into eight 8 KB sectors.Ordinary 64 KB sectors.Useful for firmware components, but software must not assume every sector is 64 KB at the bottom of the chip.
Top-bootThe highest physical 64 KB is split into eight 8 KB sectors.Ordinary 64 KB sectors.The irregular sectors are at the opposite end of the chip.
UniformNone.Every erase sector is normally 64 KB.The simplest layout, but software should still verify the actual device when it depends on exact geometry.
Comparison of bottom-boot, uniform, and top-boot Flash sector layouts
Common 8 MB NOR Flash layouts. “Bottom” means the lowest physical chip addresses; it does not mean MSX Page 0. The M29W640GB used by Carnivore2 is a bottom-boot device with eight 8 KB sectors in its first physical 64 KB, followed by 64 KB sectors.

The letters in a Flash part number often identify the geometry. For example, the Micron/Numonyx M29W640GB is the bottom-boot version and the M29W640GT is the top-boot version; uniform variants use 64 KB sectors throughout. The ASCII16-X specification describes the common bottom-boot arrangement used by its reference cartridge: the first eight sectors are 8 KB and the remaining sectors are 64 KB.

If your program genuinely needs to discover the installed device, use the chip’s CFI query and decode the erase-region descriptors. If maximum compatibility is more important than using every last byte, the simpler rule is to reserve ordinary 64 KB sectors and avoid making the save format depend on any 8 KB boot-sector arrangement.

Why the physical bottom of a multifunction cartridge is usually not game space

On a simple cartridge containing only one ROM, physical Flash address zero may also be the beginning of that ROM. On a multifunction or multi-ROM cartridge, that assumption is unsafe. The cartridge needs its own firmware, directory records, BIOS images, recovery data, or ROM-disk data somewhere in Flash.

Carnivore2 documents this precisely. Its first physical 64 KB is the eight-sector bottom-boot area and contains Boot Menu code, directory entries, and Boot Menu data. The following 64 KB blocks contain the IDE BIOS and FMPAC BIOS; ordinary ROM-image storage begins later. Those are cartridge-owned regions, not spare sectors for a game. See the Carnivore2 technical description for the physical block map.

MegaFlashROM SCC+ SD also keeps cartridge-managed components in Flash, including the SD kernel, disk ROM, optional ROM disk, and recovery program. OPFXSD manages those areas and the placement of loaded ROMs. A game should therefore treat its mapped ROM image as its address space and should not assume that physical chip offset 000000h belongs to it.

Do not erase a sector merely because it is physically near the bottom of the chip. On Carnivore2 that region contains the boot menu and directory. On MegaFlashROM, OPFXSD-managed Flash can contain the SD kernel, disk ROM, ROM disk, and recovery software. Erasing the wrong physical sector can damage the cartridge installation rather than just the game.

Multiple ROMs: the physical location can move while the logical location stays the same

A game normally addresses Flash relative to its own ROM image: logical bank 0, logical bank 1, and so on. The cartridge loader chooses a physical image base in the Flash chip and configures the mapper so those logical banks reach the correct physical blocks.

If more ROMs are installed, deleted, or reorganized, the same game image may begin at a different physical Flash block. Its internal layout does not change. A save sector reserved at a fixed logical offset near the end of the ROM remains at that same offset inside the image; only its absolute chip address changes.

Diagram showing a fixed logical save-sector offset translated to a relocated physical Flash sector
The game keeps the same logical ROM map. The cartridge’s directory, RCP/configuration, and mapper supply the physical image base. This is why self-writing code should work through the mapped ROM banks instead of hard-coding an absolute Flash block number.
physical Flash address = cartridge-selected image base
                       + logical offset inside the ROM image

This relocation only works safely when the cartridge configuration maps writes to the same Flash image that it maps for reads. MegaFlashROM must launch the title in its self-writing mode, and Carnivore2 must use an RCP whose relevant bank engines permit writes. The ROM should reserve its save sectors as part of the ROM image so the loader moves the game and its save area together.

Portable recommendation: reserve one or two ordinary 64 KB sectors near the logical end of the ROM image. Keep those sectors inside the image, use mapped ROM-relative banks to reach them, and let the cartridge loader determine the physical Flash location. Avoid cartridge firmware areas and do not depend on the special 8 KB boot sectors.

8. One address space, four MSX pages — how all cartridges fit together

Before looking at MegaFlashROM, Carnivore2 or ASCII16-X, it helps to separate three different ideas that are often mixed together: the MSX CPU page, the ROM mapper bank, and the physical FlashROM sector. They are related, but they are not the same thing.

The MSX standard: four 16 KB CPU pages

The Z80 has a 64 KB address space. MSX divides that address space into four standard 16 KB pages:

MSX nameZ80 address rangeMeaning
MSX Page 00000h–3FFFhFirst 16 KB of the CPU address space
MSX Page 14000h–7FFFhNormal lower cartridge ROM window
MSX Page 28000h–BFFFhNormal upper cartridge ROM window
MSX Page 3C000h–FFFFhNormally system/game RAM

These names should be our primary terminology throughout this article. “Page 1” always means 4000h–7FFFh, regardless of which cartridge is being used. The MSX slot hardware also selects which slot is visible independently in each of these four pages.

Important terminology: an MSX page is a fixed 16 KB CPU address range. A mapper bank is a configurable piece of ROM/Flash that is made visible somewhere in that address space. A Flash sector is a physical erase unit inside the Flash chip. Do not use these three terms interchangeably.

Where an ASCII16 game normally lives

A conventional ASCII16 cartridge uses the middle 32 KB of the Z80 address space: MSX Page 1 (4000h–7FFFh) and MSX Page 2 (8000h–BFFFh). Mapper writes select which 16 KB portions of a larger ROM appear in those two CPU pages.

That is why Flash-saving code for an ASCII16-style game will normally issue its Flash commands and data writes through addresses in Page 1 and/or Page 2. The physical save sector may be near the end of a multi-megabyte FlashROM, but the CPU still reaches that sector through one of these 16 KB windows after mapping the appropriate ROM bank.

Flash writing adds another layer

The Flash chip does not care that the programmer calls an address “MSX Page 1” or “MSX Page 2.” It sees the physical Flash address produced by the cartridge mapper. To program or erase Flash, every write in the command sequence must actually reach the Flash chip: the unlock writes, command writes, and final data or sector-confirm write.

This gives us a useful mental model:

Z80 address
    ↓
MSX Page 0 / 1 / 2 / 3
    ↓
cartridge mapper window
    ↓
selected ROM/Flash bank
    ↓
physical Flash address / erase sector

For example, a game may map a save area located near the end of a 4 MB ROM into 8000h–BFFFh. The CPU is writing through MSX Page 2, even though the physical Flash address is several megabytes from the beginning of the chip.

How the three cartridge solutions differ

MegaFlashROM SCC+ SDCarnivore2ASCII16-X
MSX CPU pagesStill the standard MSX Page 0–3 address ranges.Still the standard MSX Page 0–3 address ranges.Still the standard MSX Page 0–3 address ranges.
Typical ASCII16 game accessPage 1 4000h–7FFFh and Page 2 8000h–BFFFh.Page 1 and Page 2 for the standard ASCII16 RCP.Page 1 and Page 2.
Extra permission before self-writingThe ROM must be installed in a write-capable configuration; for OPFXSD this is the reason for using the appropriate /W setup for a self-writing title.Write permission is controlled per Carnivore2 bank by bit 4 of RnMult. The RCP decides which banks—and therefore which CPU windows—accept writes.No Carnivore2 RCP layer. The ASCII16-X mapper/Flash implementation directly provides the Flash behavior expected by the ROM.
Flash protocolThe game can use the same compatible AMD/JEDEC-style program/erase command logic when the underlying Flash implementation supports it. The cartridge-specific job is making sure those writes actually reach the intended Flash bank.

Carnivore2 terminology: MSX Page is not Carnivore2 Bank

This is the easiest place to get confused. Carnivore2 has four configurable hardware Banks 1–4. Those numbers do not mean MSX Pages 0–3. Each Carnivore2 bank has a BnAdrD value that specifies where that bank appears in the Z80 address space.

For the standard Carnivore2 ASCII16 configuration the relationship is:

MSX standard terminologyCPU rangeCarnivore2 ASCII16 terminology
MSX Page 00000h–3FFFhCarnivore2 Bank 4
MSX Page 14000h–7FFFhCarnivore2 Bank 1
MSX Page 28000h–BFFFhCarnivore2 Bank 2
MSX Page 3C000h–FFFFhCarnivore2 Bank 3
Therefore “Carnivore2 Bank 4” does not mean “MSX Page 3.” In the standard ASCII16 RCP, Carnivore2 Bank 4 is placed at 0000h–3FFFh, which is MSX Page 0. The mapping can be different in another RCP because BnAdrD is configurable.

Our RCP utility therefore shows both names. It should say, for example, “MSX Page 1 — 4000h–7FFFh — Carnivore2 Bank 1”, rather than presenting “Bank 1” by itself. When an existing RCP is dropped into the utility, the displayed MSX address range is decoded from that RCP instead of assuming the standard ASCII16 arrangement.

What should normally be writable?

For a typical ASCII16/ASCII16-X game, start with MSX Page 1 (4000h–7FFFh) and MSX Page 2 (8000h–BFFFh), because those are the normal cartridge ROM windows. On the standard Carnivore2 ASCII16 RCP these are Carnivore2 Banks 1 and 2.

But do not turn on pages mechanically. Look at the addresses used by the actual Flash routine. If any unlock, command, program or erase-confirm write goes through another CPU window, the Carnivore2 bank covering that window must also permit writes. Conversely, if a page is never used for Flash writes, leaving it protected is preferable.

This also explains why the same game-side Flash routine can be made to work across MegaFlashROM, Carnivore2 and ASCII16-X. The MSX-side addresses and Flash command protocol can remain the same; what changes is how each cartridge maps those addresses to Flash and whether it permits the writes.

9. MegaFlashROM SCC+ SD

MegaFlashROM adds a very important extra requirement: a ROM can be launched with its Flash area effectively read-only.

When flashing a self-saving game with OPFXSD, use the /W option.
OPFXSD GAME.ROM /W

According to information provided by Manuel Pazos, OPFXSD normally makes the launched ROM read-only. The /W parameter is required when the running ROM needs to program its own FlashROM area. OPFXSD can also recognize specific ROM checksums and automatically apply the equivalent write-enabled setup for known titles.

For a released game, it is therefore useful to document the required /W parameter prominently. It may also be possible to have the title recognized by OPFXSD so users do not need to remember the option manually.

10. Carnivore2: choose exactly where Flash writes are allowed

Carnivore2 has four independently configurable mapper banks. Each bank has an RnMult byte, and bit 4 controls whether CPU writes are passed to that bank: 0 = protected, 1 = writable.

The bank number itself does not permanently mean a fixed Z80 page. BnAdrD selects where the bank appears in the CPU address space, while the low three bits of RnMult select its size. Therefore an RCP editor must decode the actual CPU window instead of assuming R1/R2/R3/R4 always mean 0000h/4000h/8000h/C000h.

For a normal ASCII16 ROM

The documented Carnivore2 ASCII16 preset places its four 16 KB banks as follows:

C2 bankZ80 CPU windowRecommended write setting
Bank 14000h–7FFFhUsually enable
Bank 28000h–BFFFhUsually enable
Bank 3C000h–FFFFhNormally protect
Bank 40000h–3FFFhNormally protect
For the usual ASCII16 self-saving game, enabling 4000h–7FFFh and 8000h–BFFFh is the sensible default. These are the normal cartridge ROM windows. But this is a convention, not a Carnivore2 hardware limitation.

Why you may need more than the page containing the save data

The complete Flash command sequence has to reach the Flash chip. That includes the unlock writes, command writes, and the final program-data or sector-erase confirmation write. If part of your command sequence goes through 4000h–7FFFh and another part goes through 8000h–BFFFh, both windows must be writable.

You can also deliberately use Flash through 0000h–3FFFh or C000h–FFFFh. In that case enable the corresponding bank too. The safest rule is simple: enable writes only for every CPU window your Flash routine actually writes through, and leave the rest protected.

What changes inside an RCP

The RCP contains six bytes for each bank: RnMask, RnAddr, RnReg, RnMult, BnMaskR, and BnAdrD. To enable writes without disturbing the mapper setup, set only bit 4:

RnMult = RnMult OR 10h      ; enable writes
RnMult = RnMult AND EFh     ; disable writes

For an existing RCP this matters: the other RnMult bits control whether the bank is active, Flash/RAM selection, mirroring and bank size. They must be preserved.

Embedded Carnivore2 RCP editor / generator

Drop an existing .RCP to edit it. If you do not load a file, choose a mapper and create a new one from the documented Carnivore2 defaults. Imported files are kept intact; the tool changes only the four RnMult write-enable bits.

Drop an existing Carnivore2 .RCP here
or click to choose a file. Leave empty to create a new RCP.
Advanced: decoded bank bytes

Imported files preserve every original byte, including bytes beyond the documented 30-byte RCP structure. Only offsets 04h, 0Ah, 10h and 16h are changed by these write-enable controls.

Official RCP layout: byte 00h = mapper character; bank records occupy 01h–18h; R1Mult/R2Mult/R3Mult/R4Mult are at 04h/0Ah/10h/16h; 19h–1Ch contain Mconf, CardMDR, PosSiz and RstRun; 1Dh is reserved and normally FFh.

11. ASCII16-X and openMSX

ASCII16-X was designed with direct Flash programming in mind. There is no OPFXSD-style launch option required for the cartridge itself. The game still needs to map the correct Flash banks, execute the Flash routine from RAM, and target a valid sector.

One ASCII16-X-specific detail is that mapper registers are also visible in bank 2. For Flash command/data writes you should avoid addresses in B000h–BFFFh; use the equivalent locations through the appropriate 7000h–7FFFh mapping instead.

openMSX

When testing, make absolutely sure the ROM is configured as ASCII16X, not ordinary ASCII16. A recent real-world debugging case looked exactly like a failed Flash command sequence: the debugger kept returning normal ROM data because the launch script had accidentally hardcoded the normal ASCII16 mapper. Once changed to ASCII16-X, the same programming code worked as expected.

Modern openMSX versions emulate the ASCII16-X Flash behavior and timing, making them useful for testing the exact same DQ7/DQ5 logic used on hardware. Final testing on real cartridges is still recommended.

12. A practical cross-cartridge save design

A robust arrangement is to reserve two ordinary 64 KB sectors near the end of the ROM and alternate between them.

  1. Keep the currently valid save in sector A.
  2. Leave sector B erased and ready.
  3. When an update eventually requires 0 → 1 transitions, write the new complete save image into sector B.
  4. Verify the new data.
  5. Only after verification, treat sector B as current.
  6. Erase old sector A so it is ready for the next update.
  7. Swap the roles next time.

For small state changes such as achievements, flags or counters, you can often avoid an erase altogether by using fresh 1-bits and only programming 1 → 0. A log-style format can append new records until a sector fills, then compact into the alternate sector.

For maximum cartridge compatibility, do not make your save format depend on the first chip-specific 8 KB sectors just to save space. A 64 KB reservation is inexpensive in a multi-megabyte ROM and substantially simplifies hardware compatibility.

13. Troubleshooting checklist

SymptomCheck
Writes appear to do nothing on MegaFlashROMWas the ROM programmed/launched with OPFXSD /W?
Writes appear to do nothing on Carnivore2Does the ROM's Carnivore2 configuration have the relevant bank's write-enable bit set?
Works on hardware but not openMSXVerify the mapper is ASCII16X, not ASCII16. Use a recent openMSX version.
Polling immediately sees FFhThe command may not have been accepted. Check mapper/bank setup, unlock addresses and write protection.
DQ5 error when programmingCheck whether the requested new byte attempts any 0 → 1 transition. If so, erase the sector first.
Code crashes during erase/programMake sure the actual Flash writer and polling routines execute from RAM.
Works on one Flash cart but not anotherCheck sector geometry. Avoid assumptions about 8 KB boot sectors; use ordinary 64 KB sectors.
Wrong sector erasedVerify which physical Flash bank is mapped at the MSX address receiving the final 30h.

References and further reading

Article assembled from the public ASCII-X development discussion, the tested Max Iwamoto Flash routines, explanations by Max Iwamoto and Laurens Holst (Grauw), MegaFlashROM information from Manuel Pazos, and public Carnivore2 technical documentation. Flash programming always carries a risk of corrupting data if the wrong bank or sector is selected; test with disposable ROM images/hardware configurations first.