top of page

RAM pagination


Introduction

In this article, we will discuss how to work with more than 64K of RAM memory and do so easily by taking advantage of the benefits offered by MSX-DOS2.


What is RAM Paging?

The Z80 processor can only address 64Kb of memory. To overcome this limitation, the designers of the MSX system came up with the idea of creating 4 slots that the Z80 could access, and then dividing each of these slots into 4 subslots. These subslots are what would be presented to the Z80 to provide more memory. Later, memory mappers were introduced, allowing the assignment of subslots within a slot to different memory segments. The memory mappers took control of these segments, thereby breaking the 64Kb memory limit of the MSX. These memory segments are what we call a RAM page, and their control is known as RAM paging.


MSXDOS2 contains a series of calls to its BIOS (BDOS) responsible for handling all this memory management, saving us from the challenge of knowing the memory layout for each MSX model. In Fusion-C, we will use functions like InitRamMapperInfo, which initializes the memory mapping, and _GetRamMapperBaseTable, which updates the data in the MAPPERINFOBLOCK table. This table contains information about the status of paged memory, including used and free pages.

If you'd like to read another explanation of RAM paging, you can refer to this post by Javier Lavandeira, which I find to be very well-explained.


What will we do in the code?

To test paging, we will load files into different memory pages, place these pages in segment 2 of the Z80, which corresponds to address 0x8000, and read their values to verify that they have been loaded correctly. We will then change the page and read the value again. This is very similar to the example provided in Fusion-C, but unlike that example, we will load an entire file into the chosen memory page instead of changing just one value.


Why do we make it more complicated? The truth is that C generates larger files than if you were to program in assembly language optimally. So, you quickly fill a 16K segment. If you can create 16K modules that have specific functionality, you can load them into a memory page, and the main program manager running on the main page can call them. In other words, modular programming. For example, the game "bricks" uses this strategy to load the initial and final game animations, each of which is a module of less than 16K. "PantInic.c" and "PantFin.c" are the C files that allow compiling and testing them, while "PantInic.c_mod" and "PantFin.c_mod" are the corresponding compiled binaries of the previous ones, removing the common functions already present in the main function and adding an adaptation function that provides access to these modules (the previous files can be found in gitlab).


This also allows for parallel team programming since you can load and develop the modules independently.


Explanation of the code

The program is located in the Gitlab repository with the name "caBinMap.c" and consists of:

As always, the first lines (up to 8) contain the libraries we will use. The first variable we encounter (line 10) is the description of the DOS file, the FCB, explained in more detail in the article "Loading Images on MSX." The BufferPagina variable is the 16KB buffer where the bytes read from the disk will be loaded. It is defined with the directive __at 0x8000 to instruct the linker to place this variable at address 0x8000. This address coincides with the start of page 2 of the Z80.

Finally, we have the table variable, which will contain information about the DOS memory pages. This information is what we print in the printRamMapperStatus function. Each time we call it, it will display on the screen the slot number, the used 16KB segments, the free segments, the ones we have already requested, and the ones being used by the user.


Next, we have the FT_SetName function (which is the same as what we saw in "Loading Images on MSX") and its accompanying error control function, FT_errorHandler. To complete the disk reading functions, we have the FT_LoadBin function at line 78. It closely resembles FT_LoadSc5Image (from "Loading Images on MSX"), but here it only loads the data into the buffer without the need for a subsequent call to HMMC to transfer data from RAM to VRAM. At line 90, we load the entire file into page 2, at address 0x8000, where the BufferPagina variable is stored.


At line 95, we have the main function, which starts by defining the variables to be used. We have a pointer p that will be used to read different memory positions, a status variable of type SEGMENTSTATUS that will store information about the requested segment, and three char variables named segmentId0, segmentId1, and initialSegment that will store the segment number, returned by SEGMENTSTATUS.


At line 102, we initialize the DOS2 memory paging by calling its BIOS with a value of 4, which is the device ID (deviceID) for the memory pager. We clear the screen with Cls and print the status of the pager, obtaining:

At line 109, we request the allocation of a segment using the command AllocateSegment(0,0), where the first 0 indicates that it should be a User Segment, and the second 0 indicates automatic allocation. We save the return value of the function in status and the assigned segment in segmentId0, then print this information on the screen.

We repeat the same process for another segment and store the information in segmentId1. What we see on the screen at this point is:

Here we can observe that the number of User Segments has increased to 2, and the number of Free Segments (unassigned segments) has reduced to 3.


We clear the screen and read the memory address 0x8000, storing it in the variable p (line 123). We save the segment that was in page 2 as initialSegment and print the original segment and the value we read from p of this original segment.


Between lines 130 and 134, we place the first reserved segment (segmentId0) into page 2 and read its value. We store the value 0xDD at address 0x8000 and print this value. Finally, we print the value of the segment in page 2 obtained with the Get_PN() function.


This is how the screen looks at this point:

The first segment that was in memory was #1, which had a value of 0xCD. This value depends on the values that were in RAM at that moment; we didn't set a specific value. We change to segment 4, where there was a value of 0x01, and we replace it with 0xDD. Finally, we check that the value we obtain with Get_PN(2) is the same as what we set using Set_PN(2).


Between lines 142 and 155, we do something similar, but this time with segmentId1. First, we print the segment number of segmentId1 to the screen and then move this segment to page 2 of the Z80. We print the value at address 0x8000, overwrite it in memory, and read it again. In line 148, we load the binary file "16k_2.bin," which is a 16K file where each byte has a value of 2, and we check the values at addresses 0x8000 and 0x800A, which should both show 2. Finally, we verify that the segment read by Get_PN is the same as what we set with Set_PN. This is what appears on our screen:

Between lines 157 and 161, we once again place segmentId0 in page 2 of the Z80 and read the value 0xDD, which is what we had stored in line 133 of our code.


The following lines up to 171 do the same as before, but this time, we load a binary file ("16k_1.bin") that only contains the value 1, and we load it into segmentId0. We then put back segmentId1, which contained the values of #2 for 16K, and read two positions, verifying that they still have the value #2 (if we were using the openmsx-debugger, we could see that segmentId1 is filled with 1s). This is what appears on the screen:

At lines 173-180, we retrieve the original segment in page 2 (initialSegment) and verify that it still has the same value we saw in the 3rd screen (at the line with "Before setting segments #1"), which is 0xCD. As you can see in the screenshot:

Finally, we release the different segments and print the Mapper's status, resulting in the following output on the screen:

As we can see, there have been no errors, so the free segments are back to 5, and the used segments are at 0.


Summary and utilities of paging

I wanted to show how to load 16K blocks because in the game Bricks, it's used to load the initial and final animations. Since the code isn't very optimized, it quickly occupied 32K of program space. That's when the idea arose to split the application: the main loop takes up 32K and is responsible for managing different states: presentation, player selection, gameplay, and final presentation. It also handles the game part. The initial and final animations are each less than 16K, and as explained at the beginning, they were developed independently and then the functions that were already in the main loop were removed: file loading, music interrupts, etc. These modules are loaded into page 2 of the Z80. The last page (page 3) is used by the music driver to transcribe an MBM file loaded into memory and configure the OPLL to play the music.


How do we compile these files to be loaded into page 2?

If we compile the previous files as usual, SDCC will place all functions starting from address 0x106. That's why we change the options to --code-loc 0x8000 (instead of the usual 0x106, as explained in the Introduction post on this blog).


Once we have the functions at the addresses of page 2, we need to load this binary in a way similar to what we did in this tutorial but store it in an array at address 0x8000. The commands that do this are:

__at 0x8000 char pag2[0x4000];
FT_openFile("PantIni.bMo");
FcbRead(&file, pag2, 0x4000);

With this, we obtain the MSX loaded with all the opcodes at address 0x8000.


In the module file, there must be a function that calls the main function of the module. In the case of Bricks, it was:

void CallMain_asm()__naked {
  __asm
      call _animacio_pantalla_inicial
      ret
 __endasm;
 }

Finally, you have to call this function in the main module. To do this, you need to investigate the .map file generated to find the address of the previous function and call it directly. Assuming the address was 0x8EA6, you would call it like this:

__asm call #0x8ea6 __endasm;

Maybe it's not clear enough, and a more detailed post should be made. If so, don't hesitate to let us know.


Finally, some homework. Paging also allows us to create a simple application to check the system's RAM. With the information from the RamMapperBaseTable, we can quickly determine the amount of RAM visible to DOS and check if all bytes can be written and read. Who's up for developing it in C?


Click here to see the example in action.



Comments


bottom of page