Wednesday, February 3, 2021

[PC-88] C framework for the NEC PC8801 - Beginnings

So, you wanna make a game for an old Japanese computer virtually nobody in the west cares about? 

Welcome!

I built this from my own knowledge of Python, C, and z80 assembly. (Thanks to the SDCC core!) I had to start from scratch, but a very large portion of the knowledge I gained came from these two Japanese-only sites on PC88 assembly:

Bookworm's Library

Maroon Youkan

I will attempt to translate and explain what I've learned as best I can. If you have questions and are serious about PC-88 C development, consider joining the @RetroDevDiscord and helping the few of us there are. 

I use the Windows 10/x64 distribution of SDCC, version 4.0.0. Details on the build process further down.

PC88-C on Github

PC88-C base file list:

src\main.c
src\pc88-c.h
tools\hex2bin.py
tools\hexer.py
tools\maked88.py
tools\png288.py
ipl.bin
*makepc88.bat

(*makepc88.bat is the primary build script.)

MAIN.C
As with most C projects, main.c contains the project code. SDCC generally only likes one primary C file at a time, so try to put all your code in main.c or in files included by main.c.

Due to my unfamiliarity with the inner workings of SDCC, void main() must be the first actual code entry in the built file. The crt0-equivalent, IPL.BIN, points directly to code start at $1000, which is where the autoloader targets (further information below).

PC88-C.H
Most of the command documentation, explanation on registers, etc. is in this file. There is still a lot that needs to be documented, but a lot of information is here. Overview:


These are fairly self-explanatory. String should allow you to define character arrays as you are accustomed. bit() allows you to get bit values without doing the hex in your head.



PlanarBitmap is a bitmap definition required for DrawPlanarBitmap(). 'r', 'g' and 'b' are pointers to raw image data for each of the color planes, and 'w' and 'h' are width (in tiles, or in pixels divided by 8) and and height (in pixels). 

The PC-88's video memory is divided into three, one-bit RGB color planes. The original PC-88 models, referred to as 'V1' mode, had to access the RGB planes independently, one at a time. The resulting 3-bit color is displayed on the screen. By default, plane 0 is blue, plane 1 is red, and plane 2 is green. So, to get a "cyan" color, you write a 1 to the blue and green planes, and a 0 to the red plane.

You can change the default palette values for certain effects, such as simulated foreground- and background-layers by limiting overall palette color. This technique is explained in part 3, and functions well with 1-bit software sprites.

However, given the 4MHz CPU speed of the V1 models and the relatively slow bus speed, this is not effective for high fidelity games. V2 mode came not much later, and the ALU expansion module.

The ALU (Arithmetic Logic Unit, thanks rikkles) is the key to improving game performance. With the control of three extra registers, you can write to all 3 VRAM planes simultaneously. The explanation given of utilizing the ALU was extremely complex and took me several days to fully understand, so don't feel bad if it doesn't click right away.

Before getting into that, here are the function outlines:

static inline void putchr(u8 c) (and putchr40)
Puts a character on the screen at location global SCREEN_POINTER. SCREEN_POINTER must be initialized in main() at SCREEN_TXT_OFFSET (or your desired location) before using this function or print().

void print(String str) (and print40)
Prints a string to SCREEN_POINTER

u8 ReadIOReg(u8 r)
Returns the value in a given I/O register, if that register can be read. See the H file for detailed definitions.

void SetIOReg(u8 r, u8 v)
Sets the value of I/O register 'r' to single byte value 'v'.

void SetTextAttribute(u8 x, u8 y, u8 attr)
Adds an attribute byte-pair to row 'y', beginning at position 'x'. The text attribute macros are explained below.

void ClearAttributeRam()
Resets the entire screen's text attributes to their default (80/Color mode) defaults.

void SetCursorPos(u8 x, u8 y) (and SetCursorPos40)
Moves SCREEN_POINTER to 'x', 'y' on-screen, where x=(0, 79) and y=(0, 25)

void Wait_VBLANK()
Proper Vblank ASM routine. Waits for VBL signal from the CRTC, then waits until its clear before returning to ensure we are *inside* vertical blank. 

void DrawPlaneBMP(const u8* img, u8 plane, u16 x, u16 y, u8 w, u8 h)
Draws raw, single color plane image data 'img', to color plane 'plane', of width/8 and height 'w' and 'h', at pixel offset 'x', 'y'. Warning, not pixel perfect - x offset is tied to 8-pixel boundary. This is macroed three times with DrawPlanarBMP, defined below. This will toggle the GVRAM planes for you, but SETBANK_MAINRAM() must be called afterwards to re-enable the Main RAM page.

void SetPixel(u16 x, u8 y, u8 c)
Pixel-perfect plot a single pixel at 'x', 'y' of color 'c', where bits 0-2 of 'c' represent the VRAM color planes. Default colors are macroed with the prefix CLR_.
SetPixel() will toggle GVRAM dependant on the color, but SETBANK_MAINRAM() must be called afterwards to re-enable the Main RAM page. Note that to access individual GVRAM pages, expanded GVRAM must be off.  

bool GetKeyDown(u8 SCANCODE)
Returns true if the macro 'SCANCODE', prefixed by KB_, is presently down; else returns false.

static inline void EnableALU()
static inline void DisableALU()
Sets I/O register 0x32 to 0xC9 if enabled and 0x89 (IPL defaults) if disabled. Must be called after calling ExpandedGVRAM_On(). (V2 only)

static inline void ExpandedGVRAM_On()
static inline void ExpandedGVRAM_Off()

Sets I/O register 0x35 to 0x80 if enabled and 0 if disabled. This is required before calling EnableALU(), otherwise writing through V2 mode (via the ALU) will not work. V2 only. Note that on boot, Expanded GVRAM is off.

void DiskLoad(u8* dest, u8 srcTrack, u8 srcSector, u8 numSecs, u8 drive) __naked 
Same assembly routine as is in IPL.BIN. e.g.
DiskLoad((u8*)0x4000, 1, 7, 40, 0);
Loads 40*256 bytes from track 1, sector 7, to RAM at 0x4000. 

void __init()
Sets up the screen pointer and calls main(). Should generally be left alone. :)

Macro definitions:

#define SetBGColor(c) SetIOReg(0x52, c << 4);
#define SetBorderColor(c) SetIOReg(0x52, c); // PC88mk2 and prior

Sets the background color for color text mode. Border color function was removed from most later models.

#define SETBANK_BLUE() SetIOReg(0x5c, 0xff);
#define SETBANK_RED() SetIOReg(0x5d, 0xff);
#define SETBANK_GREEN() SetIOReg(0x5e, 0xff);
#define SETBANK_MAINRAM() SetIOReg(0x5f, 0xff);
Toggles GVRAM banks over 0xC000 ~ 0xFFFF. SETBANK_MAINRAM() must be active during normal program execution - having any VRAM bank active can slow programs down.

#define DrawPlanarBitmap(pb, x, y) 
Macro for drawing a PlanarBitmap struct (V1 mode).

#define COLORMODE_SET(color, semigraphic) 
Defines a SET type attribute in Color Text mode, where 'color' is 0-7 and 'semigraphic' is 0 or 1.

#define COLORMODE_ATTR(underline, upperline, reverse, blink, hidden) 
Defines an ATTR type attribute in Color Text mode, where all parameters are either 0 or 1.

#define BWMODE_ATTR(underline, upperline, reverse, blink, hidden) 
#define ATTR_BW_SEMIGRAPHIC 0b10011000
Defines an attribute for B&W Text mode. For B&W mode, use the ATTR_BW_SEMIGRAPHIC macro to enable semigraphic mode.

#define IRQ_OFF __asm di __endasm;
#define IRQ_ON __asm ei __endasm;
#define HALT __asm halt __endasm;
#define BREAKPOINT HALT 
Convenience macros. When swapping RAM/GVRAM pages and reading/writing IO ports, remember to disable IRQs. HALT and BREAKPOINT are simply easy ways to aid in debugging without a full debugger.

HEX2BIN.PY
Takes the place of hex2bin.exe. This is taken from Intel's official open source library. Requires Python 3 and python module "intelhex", obtainable via 'pip install intelhex'. This is already integrated in makepc88.bat.

HEXER.PY
Simple tool of convenience - overwrites 1 byte in the given file with the given value, e.g.
 python3 tools/hexer.py ipl.bin 0x2f 0x50
Will change the number of sectors loaded by the autoloader in IPL.BIN (the byte located at 0x2f) to 0x50. This tool is not utilized in the chain, but is there for ease of use.

MAKED88.PY
Replaces the eponymous D88SAVER.EXE. D88SAVER was taken from the above websites and was useful in generating a blank d88 file, and injecting files into it (including IPL.BIN). This serves the same purpose, and is used in the same way - with the added feature that it will create the disk file passed as argument if it does not already exist.
(Note this only supports 2D, or 375kB disks for now.)
This is integrated into makepc88.bat. 

PNG288.PY
Converts a standard indexed PNG to its corresponding R, G and B bitplanes, then writes them to C-style header files in const char array format. The header file byte data can then be drawn directly by DrawPlaneBMP(). Requires the modules 'Pillow' and 'numpy'.

IPL.BIN
This file is assembled using ASW assembler from ipl.z80 and disk.z80. It contains a short routine that sets up the screen and stack pointer, and has a minimal disk access routine for loading from floppy. 

Boot process on the PC-88 is roughly:
- Is the 'boot from floppy' dipswitch on?
-- If YES, copy the 256 bytes from cylinder/head/record 0/0/1 from the inserted disk into RAM at 0xC000. Then, jp $C000.
--If NO, check the TERMINAL and BASIC dipswitches and boot to ROM.

The 256 bytes within IPL.BIN are therefore called from within RAM at org $C000. This is clearly not enough to run an entire game, so the routine then copies N bytes, the value of which is located at offset [0x2F] in the IPL, to the location at offset [0x38-0x39].
By default, [0x2F] = 0x4F and [0x38-0x39] = 0x00 0x10, meaning 79 sectors (2f = 79 * 256 bytes, or ~20kB) is copied from disk to RAM starting at little-endian address $1000. Feel free to change these using hexer.py if you don't have ASW to re-assemble.

MAKEPC88.BAT
Performs the following:
- Deletes app.d88
- Creates a blank 2D d88 with maked88.py
- sdcc -mz80 --code-loc 0x1000 --stack-loc 0x0080 --data-loc 0x0100 --fomit-frame-pointer --no-std-crt0 src/main.c
- Converts the resulting IHX to BIN format using hex2bin.py
- Inserts IPL.BIN and the resultant MAIN.BIN into app.d88 at sectors 0 0 1 and 0 0 2
- Prints a (rough) outline of the memory map
- If "main.bin" (or the resultant filename) is passed as an argument, it will inform you if the file is larger than the default number of sectors copied in by the autoloader. (change line 6 in the bat, set usedsec=nn if you change this value in the IPL). 
- Launches the emulator (you must change this to your own emulator path)

This finalizes the explanation of the files included in the repository.

Part 2 will cover basic text, pixel and bitmap drawing in both V1 and V2 modes!

No comments:

Post a Comment