Difference between revisions of "Create a mapped ROM"
From MSX Game Library
(→Banked call) |
|||
(12 intermediate revisions by the same user not shown) | |||
Line 66: | Line 66: | ||
A sample program (<tt>s_mapper</tt>) is available to demonstrate the use of ROM mappers. | A sample program (<tt>s_mapper</tt>) is available to demonstrate the use of ROM mappers. | ||
− | In this example, we create a 128 KB mapped ROM in ASCII-8 format (16 segments). The first 4 segments are defined in the main program and 2 others in separate files. | + | In this example, we create a 128 KB mapped ROM in ASCII-8 format (16 segments). The first 4 segments are defined in the main program (32 KB) and 2 others in separate files. |
Sample files: | Sample files: | ||
Line 79: | Line 79: | ||
* <tt>s_mapper.c</tt> will be compile as a 32 KB plain ROM (which represents the first 4 segments). | * <tt>s_mapper.c</tt> will be compile as a 32 KB plain ROM (which represents the first 4 segments). | ||
* Then, the Build Tool will search for source files for segment #5 to #15 starting with <tt>s_mapper_</tt>. 3 files extensions are supported: <tt>.c</tt>, <tt>.asm</tt> and <tt>.s</tt>. | * Then, the Build Tool will search for source files for segment #5 to #15 starting with <tt>s_mapper_</tt>. 3 files extensions are supported: <tt>.c</tt>, <tt>.asm</tt> and <tt>.s</tt>. | ||
− | ** <tt>s_mapper_s4_b2.c</tt> will be find, compile and added to link list. | + | ** <tt>s_mapper_s4_b2.c</tt> will be find, compile to be visible in bank 2 (from 0x8000) and added to link list. |
− | ** <tt>s_mapper_s5_b3.asm</tt> will be find, compile and added to link list. Assembler segment source file must include the <tt>.area</tt> directive with the segment name <tt>_SEG{num}</tt> (<tt>_SEG5</tt> here). | + | ** <tt>s_mapper_s5_b3.asm</tt> will be find, compile to be visible in bank 3 (from 0xA000) and added to link list. Assembler segment source file must include the <tt>.area</tt> directive with the segment name <tt>_SEG{num}</tt> (<tt>_SEG5</tt> here). |
* All the source will be linked together (and symbols will be "resolved"). | * All the source will be linked together (and symbols will be "resolved"). | ||
* The final binary file will be create by [[MSXhex]], putting each segment at its final location in the ROM. | * The final binary file will be create by [[MSXhex]], putting each segment at its final location in the ROM. | ||
Line 135: | Line 135: | ||
BankedFunc(); // automatic switch to segment #7 then restore previous segment | BankedFunc(); // automatic switch to segment #7 then restore previous segment | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | ⚠️ <u>Note</u>: Banked functions seem simple to use at first glance, but I don't recommend using them as they make segment switching invisible and can be a source of many bugs. Making bank switches by hand has the big advantage of making them explicit in the code, so you don't lose sight of this sensitive mechanism. | ||
+ | |||
+ | === Raw binary data === | ||
+ | |||
+ | MSXgl also provides a means of adding raw binary data directly to segments. This data is not compiled (faster) and is simply added to the final ROM, automatically placing it in the right place. | ||
+ | |||
+ | All you have to do is define the <tt>RawFiles</tt> parameter in your project configuration (<tt>project_setting.js</tt>) with an array of the type: | ||
+ | <syntaxhighlight lang="js"> | ||
+ | //-- List of raw data files to be added to final binary (array). Each entry must be in the following format: { offset:0x0000, file:"myfile.bin" } | ||
+ | RawFiles = [ | ||
+ | { segment: 2, file:"content/music1.vgm"}, | ||
+ | { segment: 3, file:"content/music2.vgm" }, | ||
+ | { offset: 0x18000, file:"content/image.bin" }, | ||
+ | { page: 4, file:"content/video.bin" } | ||
+ | ]; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | The first parameter of each table entry can be any of 3 types: | ||
+ | * <tt>offset</tt>: a 32-bit offset (decimal or hexadecimal) from the start of the ROM. | ||
+ | * <tt>segment</tt>: segment number for mapped ROMs (Build tool use mapper's bank size to determine the final position). | ||
+ | * <tt>page</tt>: page number (16 KB) for mapped or plain ROM. | ||
+ | |||
+ | The sample <tt>s_vgm</tt> showcase the use this option. | ||
=== Initial slot configuration === | === Initial slot configuration === | ||
− | When your <tt>main()</tt> function start, the mapped-ROM cartridge slot is selected in | + | When your <tt>main()</tt> function start, the mapped-ROM cartridge slot is selected in pages #1 and #2 of the memory space. |
− | <img style=" | + | <img style="width:400px; margin:0.5em;" src="https://raw.githubusercontent.com/aoineko-fr/MSXgl/main/engine/doc/img/target/rom_slot_ascii8_128.png" /> |
− | <img style=" | + | <img style="width:400px; margin:0.5em;" src="https://raw.githubusercontent.com/aoineko-fr/MSXgl/main/engine/doc/img/target/rom_slot_ascii16_128.png" /> |
Also, the banks are initially setup as: | Also, the banks are initially setup as: | ||
Line 154: | Line 178: | ||
* Bank #0 (4000h-7FFFh): Segment #0 (contain the ROM header) | * Bank #0 (4000h-7FFFh): Segment #0 (contain the ROM header) | ||
* Bank #1 (8000h-BFFFh): Segment #1 | * Bank #1 (8000h-BFFFh): Segment #1 | ||
+ | |||
+ | === NEO mapper === | ||
+ | [[NEO mapper]] works on the same principle as the old mappers, but has more bank switching registers (covering page 0) and registers are 16-bit instead of 8-bit. | ||
+ | |||
+ | When your <tt>main()</tt> function start, the mapped-ROM cartridge slot is selected in pages #0, #1 and #2 of the memory space. | ||
+ | |||
+ | <img style="width:400px; margin:0.5em;" src="https://raw.githubusercontent.com/aoineko-fr/MSXgl/main/engine/doc/mapper_neo8.png" /> | ||
+ | <img style="width:400px; margin:0.5em;" src="https://raw.githubusercontent.com/aoineko-fr/MSXgl/main/engine/doc/mapper_neo16.png" /> | ||
+ | |||
+ | NEO-8: | ||
+ | * Bank #0 (0000h-1FFFh): Segment #4 (contain the ISR) | ||
+ | * Bank #1 (2000h-3FFFh): Segment #5 | ||
+ | * Bank #2 (4000h-5FFFh): Segment #0 (contain the ROM header) | ||
+ | * Bank #3 (6000h-7FFFh): Segment #1 | ||
+ | * Bank #4 (8000h-9FFFh): Segment #2 | ||
+ | * Bank #5 (A000h-BFFFh): Segment #3 | ||
+ | |||
+ | For 16 KB mappers: | ||
+ | * Bank #0 (0000h-3FFFh): Segment #2 (contain the ISR) | ||
+ | * Bank #1 (4000h-7FFFh): Segment #0 (contain the ROM header) | ||
+ | * Bank #2 (8000h-BFFFh): Segment #1 | ||
+ | |||
+ | |||
+ | See the [[NEO mapper|NEO mapper specifications]] for more details. | ||
== Advice == | == Advice == | ||
Line 190: | Line 238: | ||
For example, the music player could be in a segment and switch to bank #2 only when you need to decode a music frame. | For example, the music player could be in a segment and switch to bank #2 only when you need to decode a music frame. | ||
− | == | + | === Warning === |
− | + | ⚠️ If your main program is larger than the size of one segment, it will span several segments. And since SDCC can add low-level functions anywhere in the space occupied by your main program, you can end up with critical functions (whose calls you don't control) ending up in a segment that you want to switch. And if you do, your program will crash. | |
− | + | For example, if you use ASCII-16 mapper format and your main program is bigger than 16 KB, part of it will ends up to segment #1 visible through bank #1. If you switch bank #1 to another segment, you will lose visibility to potentially critical functions. | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | It is therefore strongly recommended that you keep the main programme below the size of the segments that you can keep always visible to the Z80: 16 KB for a 16 KB mapper, and 8, 16 or 24 KB for an 8 KB mapper (depending on whether you keep 1, 2 or 3 segments always visible). | |
− | |||
− | |||
− |
Latest revision as of 23:00, 8 September 2024
To create cartridges larger than 64 KB, MSXGL allows to use 6 types of ROM mappers. This documentation explain how to use the 4 classic ones: ASCII-8, ASCII-16, Konami, and Konami with SCC.
Most of the information also apply to NEO mapper (NEO-8 and NEO-16) but for further detail, see the dedicated section.
Contents
Principles
A mapped ROM is visible in pages 1 and 2 of the Z80 memory space (address 4000h-7FFFh and 8000h-BFFFh).
This 32 KB space is divided into either 4 banks (or sub-pages) of 8 KB or 2 banks of 16 KB.
This is why mappers are often categorized as 8 KB mapper and 16 KB mapper.
ROMs using these mappers can be for example 128 KB, 256 KB, 512 KB, 1 MB, 2 MB or 4 MB in size (4 MB, only for 16 KB mapper).
The total content of the ROM is divided into blocks of the mapper size (8 or 16 KB). These blocks are called segments.
The concept of using mappers is to associate a given segment with a bank to make it visible to the Z80.
By changing the segments visible in each bank during the course of the program, we can thus access the entire contents of the ROM.
128 KB ROM using an 8 KB mapper (ASCII-8) in slot 1. The ROM is composed of 16 segments, visible to the Z80 through 4 banks. In the example above, segment #0 is visible in bank #0, segment #1 in bank #1, segment #4 in bank #2 and segment #12 in bank #3.
To change the visible segment in a bank, you just have to write the segment number to a given address (different depending on the mappers).
Write in a ROM, yes, you read it right!
In fact, mapped ROMs have special mechanisms (mappers) that intercept write signals at certain addresses and use the written value to select the segment.
Using MSXgl, you don't need to know the addresses or write because you only need to use the SET_BANK_SEGMENT(bank, seg) macro which takes care of associating a segment to a bank.
The supported mappers are:
Target | Description |
---|---|
ROM_ASCII8 | ASCII-8: 8 KB segments for a total of 64 KB to 2 MB |
ROM_ASCII16 | ASCII-16: 16 KB segments for a total of 64 KB to 4 MB |
ROM_KONAMI | Konami MegaROM (aka Konami4) 8 KB segments for a total of 64 KB to 2 MB |
ROM_KONAMI_SCC | Konami MegaROM SCC (aka Konami5): 8 KB segments for a total of 64 KB to 2 MB |
More details on the different mappers:
Note: "MegaROM" refers to a ROM of 128 KB or more. Even though they are not widely supported, there is nothing to prevent you from creating a 64 KB ROM using a mapper. We therefore prefer to use the term Mapped ROM here rather than MegaROM.
How to
Setup
First, you need to decide which mapper you want to use and what size you need.
If you have no idea, start with a 128 KB ROM using ASCII-8 mapper.
The choice of ROM is made in the configuration file of the build tool (project_config.js).
Example:
target = "ROM_ASCII8"; ROMSize = 128;
See the details of the configuration of the Build Tool: Targets#Mapped_ROM_program.
Example
A sample program (s_mapper) is available to demonstrate the use of ROM mappers.
In this example, we create a 128 KB mapped ROM in ASCII-8 format (16 segments). The first 4 segments are defined in the main program (32 KB) and 2 others in separate files.
Sample files:
- projects/
- samples/
- s_mapper.c (main program)
- s_mapper.js (build tool configuration)
- s_mapper_s4_b2.c (segment #4 C source)
- s_mapper_s5_b3.asm (segment #5 assembler source)
- samples/
In the Build Tool:
- s_mapper.c will be compile as a 32 KB plain ROM (which represents the first 4 segments).
- Then, the Build Tool will search for source files for segment #5 to #15 starting with s_mapper_. 3 files extensions are supported: .c, .asm and .s.
- s_mapper_s4_b2.c will be find, compile to be visible in bank 2 (from 0x8000) and added to link list.
- s_mapper_s5_b3.asm will be find, compile to be visible in bank 3 (from 0xA000) and added to link list. Assembler segment source file must include the .area directive with the segment name _SEG{num} (_SEG5 here).
- All the source will be linked together (and symbols will be "resolved").
- The final binary file will be create by MSXhex, putting each segment at its final location in the ROM.
Symbols
Segment symbols (tables or functions name) can be accessed from any other sources.
No more need to know the address of a data in a segment of the mapper; it is enough to call it by its name (which also allows the C compiler to validate the data type).
It is therefore also much easier to accumulate data in a segment since it is only necessary to put the data one after the other; the address of the arrays being resolved at link time.
The switching of the segments remains the responsibility of the programmer.
Example:
// File: mygame_s5_b3.c // Segment #5 (bank #3) u8 MyData[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
// File: mygame.c // Main program extern u8 MyData[]; // Tell the compiler this data exists somewhere else. SET_BANK_SEGMENT(3, 5); // Switch segment visible through bank #3. u8 s = MyData[1];
Banked call
SDCC banked calls (using the __banked directive) can be used to call a function in a segment and have an automatic bank switching to this segment before call, and then, a restoration of the previous segment after function returns. The only limitation is that SDCC only returns the segment number (and not the bank number) to the default trampoline functions. So, to make it simple (and efficient), MSXgl only allow (for the moment) a switch on one of a mapped-ROM's bank.
- For 8 KB mapper : bank #2 (8000h-9FFFh)
- For 16 KB mapper : bank #1 (8000h-BFFFh)
To activate this feature, you need to add set BankedCall=true; in your "project_config.js".
Example:
// File: mygame_s7_b2.c // Segment #7 (bank #2) u8 LocalFunc() { return 2; } u8 BankedFunc() __banked { return LocalFunc()+1; };
// File: mygame.c // Main program u8 BankedFunc() __banked; // Prototype to tell the compiler this function exists somewhere else. BankedFunc(); // automatic switch to segment #7 then restore previous segment
⚠️ Note: Banked functions seem simple to use at first glance, but I don't recommend using them as they make segment switching invisible and can be a source of many bugs. Making bank switches by hand has the big advantage of making them explicit in the code, so you don't lose sight of this sensitive mechanism.
Raw binary data
MSXgl also provides a means of adding raw binary data directly to segments. This data is not compiled (faster) and is simply added to the final ROM, automatically placing it in the right place.
All you have to do is define the RawFiles parameter in your project configuration (project_setting.js) with an array of the type:
//-- List of raw data files to be added to final binary (array). Each entry must be in the following format: { offset:0x0000, file:"myfile.bin" } RawFiles = [ { segment: 2, file:"content/music1.vgm"}, { segment: 3, file:"content/music2.vgm" }, { offset: 0x18000, file:"content/image.bin" }, { page: 4, file:"content/video.bin" } ];
The first parameter of each table entry can be any of 3 types:
- offset: a 32-bit offset (decimal or hexadecimal) from the start of the ROM.
- segment: segment number for mapped ROMs (Build tool use mapper's bank size to determine the final position).
- page: page number (16 KB) for mapped or plain ROM.
The sample s_vgm showcase the use this option.
Initial slot configuration
When your main() function start, the mapped-ROM cartridge slot is selected in pages #1 and #2 of the memory space.
Also, the banks are initially setup as:
For 8 KB mappers:
- Bank #0 (4000h-5FFFh): Segment #0 (contain the ROM header)
- Bank #1 (6000h-7FFFh): Segment #1
- Bank #2 (8000h-9FFFh): Segment #2
- Bank #3 (A000h-BFFFh): Segment #3
For 16 KB mappers:
- Bank #0 (4000h-7FFFh): Segment #0 (contain the ROM header)
- Bank #1 (8000h-BFFFh): Segment #1
NEO mapper
NEO mapper works on the same principle as the old mappers, but has more bank switching registers (covering page 0) and registers are 16-bit instead of 8-bit.
When your main() function start, the mapped-ROM cartridge slot is selected in pages #0, #1 and #2 of the memory space.
NEO-8:
- Bank #0 (0000h-1FFFh): Segment #4 (contain the ISR)
- Bank #1 (2000h-3FFFh): Segment #5
- Bank #2 (4000h-5FFFh): Segment #0 (contain the ROM header)
- Bank #3 (6000h-7FFFh): Segment #1
- Bank #4 (8000h-9FFFh): Segment #2
- Bank #5 (A000h-BFFFh): Segment #3
For 16 KB mappers:
- Bank #0 (0000h-3FFFh): Segment #2 (contain the ISR)
- Bank #1 (4000h-7FFFh): Segment #0 (contain the ROM header)
- Bank #2 (8000h-BFFFh): Segment #1
See the NEO mapper specifications for more details.
Advice
The simplest way to start with mapped-ROM is to use a 8 KB mapper (ASCII-8 for example) and to keep the first 3 segments always visible from the first 3 banks (the initial state), and then use the last bank (#3) to switch between all other segments.
This allows to have a 24 KB program (3 segments) always visible by the Z80 and to switch only the segments that contain the data, when needed.
Let's say you have a 24 KB (segment #0, #1 and #2) program and 3 data segments containing: sprites (segment #4), background tiles (segment #5) and music (segment #6).
Your pseudo code could be something like this:
// File: mygame.c ... // Init VRAM data SET_BANK_SEGMENT(3, 4); // Segment #4 visible through bank #3 LoadSpriteInVRAM(); SET_BANK_SEGMENT(3, 5); // Segment #5 visible through bank #3 LoadTilesInVRAM(); ... // Main loop while(1) { WaitForVSynch(); SET_BANK_SEGMENT(3, 6); // Segment #6 visible through bank #3 PlayMusic(); ... }
To go further, the next step could be, for example, to use bank #2 (the third one) to put specific code that doesn't need to be always visible by your main program.
For example, the music player could be in a segment and switch to bank #2 only when you need to decode a music frame.
Warning
⚠️ If your main program is larger than the size of one segment, it will span several segments. And since SDCC can add low-level functions anywhere in the space occupied by your main program, you can end up with critical functions (whose calls you don't control) ending up in a segment that you want to switch. And if you do, your program will crash. For example, if you use ASCII-16 mapper format and your main program is bigger than 16 KB, part of it will ends up to segment #1 visible through bank #1. If you switch bank #1 to another segment, you will lose visibility to potentially critical functions.
It is therefore strongly recommended that you keep the main programme below the size of the segments that you can keep always visible to the Z80: 16 KB for a 16 KB mapper, and 8, 16 or 24 KB for an 8 KB mapper (depending on whether you keep 1, 2 or 3 segments always visible).