Porting a Core, DeMiSTified – Part 5 – 2021-03-07
I’m not going to go into great depth regarding the substitute controller module which I’m using for this project, since I’ve covered much of that ground in previous posts (see the previous series about the ZPU-based control module, the TG68_MiniSoC project and also EightThirtyTwoDemos) – in all cases I have a soft CPU with a number of memory-mapped hardware registers, and I’m following much the same pattern this time around.
Briefly, I have a UART for serial debugging output, an SPI chip select register which allows one of several SPI channels to be selected, an SPI data transfer register, a simple interrupt controller, PS/2 keyboard and a simple timer in the form of a millisecond counter. I also have a joystick input register, since the controller will be responsible for relaying joystick events into the guest core.
So having created a CPU and peripheral module which we can connect to the guest core, all that remains is porting the firmware, yes?
Well…. no.
The MiST firmware is approximately 240k of ARM code, and the cores I want to port typically have only a handful of block RAMs left. The Master System core, for instance, has only 7k of block RAM free (with a bit of careful persuasion we can increase this to 12k) – so I have to choose between attempting to squeeze the MiST firmware down to one twentieth of its original size, or I have to make invasive changes to each and every core to add an extra port to the SDRAM controller, find a spare region in the memory map and upload the firmware at boot.
I will no doubt have to take the latter option for cores that require hard drive support – Archimedes springs to mind – but for simpler console or arcade cores… maybe I can fit enough firmware into 12k to do what’s needed? Or, more realistically, write a massively simplified barebones firmware which does just enough to support the cores being run.
So what services will our guest cores require from the firmware?
- ROM upload – for this we require a way of selecting files, and a routine to send the file, byte by byte, over SPI.
- Joystick data. The firmware needs to read the status of any genuine joysticks connected to the host hardware, and forward them to the guest core over SPI. We also need to merge the events generated by any joystick emulation we might want to provide.
- A method of reading core-specific options from the core, presenting them to the user and allowing them to be changed.
For extra credit we can think about saving settings to the SD card, saving non-volatile RAM and suchlike, but for now we’ll concentrate on the basic functionality.
The guest core provides an on-screen display overlay with a framebuffer of sorts. We can upload data to this framebuffer using the SPI interface – the same one as is used for the SD-card, but instead of asserting the SD-card’s CS line, we assert the SPI_SS3 signal instead. We can send commands to show and hide the overlay, and to upload a row of data into the framebuffer. The data organisation in the framebuffer is a bit strange – it’s actually like driving a dot-matrix printer, in that each byte contains a vertical stack of eight pixels, so a stream of data sent to the OSD draws a complete line of text in the framebuffer.
Speaking of framebuffers – the previous control module I added to the One Chip MSX and PC Engine cores way back when contained its own font, and the core would upload characters to the OSD buffer. Since this time round we’re uploading raw pixels, we’re going to need a font.
There is a suitable font in the Minimig project (which is where this OSD design originated) – in the charrom.h file within the firmware. This font has been floating around in various FPGA projects for some time – but Minimig is where it originated. So let’s take a look:
// *character font
unsigned char charfont[256][8] =
{
OK straight away that’s bad news – we have an array of 256 characters, each 8 bytes long, so that’s 2k of data right there. Given how tight memory is for this project, that’s not going to fly.
Luckily, apart from a few special characters, the first 32 entries are all zeros, and better still, the entire second half of the font table is empty, so what I’ve chosen to do is remove entries 0-31, include entries 32 to 127 as normal, then include a few special characters starting at character 128. In return from the inconvenience of having to subtract 32 from a character value before looking up its font entry, we can bring the size of the font down to 832 bytes. That’s still a lot when the target is only 12k, but it will do for now. If I get stuck size-wise I can look at doing something more complicated; the rightmost byte of most characters is zero, for instance – but there’s a danger that doing anything too clever with this table will require more bytes of code to unscramble than is saved by the reduction in static data.
A number of commands are defined for the OSD module – mostly a legacy from the original Minimig – but most are ignored in MiST cores; the ones that we actually use are OSDCMDDISABLE (0x40), OSDCMDENABLE (0x41), and OSDCMDWRITE (0x2n, where n is the row number).
So between thee commands, the font, and the SPI interface we have all we need to control the OSD – but we need to know which options to display to the user – and these vary from core to core.
One of the other modules with which we can communicate over SPI is the user_io block (selected when CONF_DATA0 is low). This is where we’ll send joystick events, and it can also be used to configure the scandoubler and set some other video options. It also handles a core-specific “status” field which contains the kinds of things that would be configured by DIP switches on real hardware. Finally, it can send a configuration string in the other direction.
I have to admit my first reaction to this config string system was that it would be painful to deal with. I’ve since changed my mind – it’s actually a remarkably compact way to represent a menu!
Let’s take a look at the NES core’s config string:
parameter CONF_STR = {
"NES;NESFDSNSF;",
"F,BIN,Load FDS BIOS;",
So first we have the name of the core. On MiST this is displayed in the side stripe on the left hand side of the menu. I haven’t implemented that yet, but may do in future if there’s enough RAM left after everything else is implemented…
The next field contains a list of three-character file extensions which define the types of ROM files this core understands. These will simply be used for pattern matching in the file selector; however we have to keep track of which one matches, since the core can process files differently depending upon their type. When uploading a ROM we can set an “index” parameter, which in this example would be 0 for NES files, 1 for FDS files and 2 for NSF files.
Then we have an “F” entry, which works similarly to the previous line; it adds a file selector with a specific purpose to the menu – in this case to load an FDS BIOS (whatever that might be!)
Now we have some options:
"O12,System Type,NTSC,PAL,Dendy;",
"O34,Scanlines,OFF,25%,50%,75%;",
"O5,Joystick swap,OFF,ON;",
"O6,Invert mirroring,OFF,ON;",
"O7,Hide overscan,OFF,ON;",
"O8,Palette,FCEUX,Unsaturated-V6;",
"O9B,Disk side,Auto,A,B,C,D;"
In each case, following the letter “O” we have one or more hexadecimal digits which indicate which bits in the status field the options will control.
For instance, O5 will control a simple on/off signal on bit 5 of the status word, whereas O34 indicates a 2 bit field in bits 3 and 4 which will cycle through four values. O9B indicates a three-bit field in bits 9 to 11 of the status word, cycling through as many as eight options – but only five options have been supplied in this case.
"T0,Reset;",
"V,v2.0-test1;"
}
Finally, a Toggle – which acts similarly to an Option, except it changes state only briefly when selected, before returning to its previous state – like a push-button, hence its use here for reset.
Last, and certainly least since I’m not going to bother parsing it unless I turn out to have plenty of RAM left after all, is the core’s version.
So that’s all we have to worry about regarding the config string? Nope…
Here’s a chunk of config string from a different core:
"P1,Video options;",
"P1O12,Scandoubler Fx,None,CRT 25%,CRT 50%,CRT 75%;",
"P1O3,Overscan,Hidden,Visible;",
"P1O4,Border Color,Original,Black;"
Yes, that’s right – config strings can define submenus too! In this case the “P1,” line defines the submenu’s name, and the “P1O…” lines behave exactly as per the previously described “O…” lines, except for the fact that they’re displayed as part of a submenu, not the toplevel menu.
Next time I’ll look at the ROM upload process, and how a bi-directional signal can spoil our day.