NEC PC-9801/9821 Development Tools

The NEC PC-98 (PC-9801 and PC-9821) are are a range of x86 compatible desktop computers from NEC in the mid to late 80's, and early to mid 90's. They were very popular within Japan.

The earlier PC-9801 is approximately equivalent to the IBM PC XT class machines; 8086 (or rather the NEC Vxx equivalent), with the PC-9821 being aligned with the IBM AT, 386 and above. They both have some level of MS-DOS/Windows compatibility, but they are very definitely their own platform, with their own ports of popular applications and, more importantly, games :-)

This page is my attempt to try and collate the available material that exists, in English, for development tools for those platforms.

This is mostly in Japanese, of course, but contains very useful information on the hardware structure of the machine, as well as the different BIOS calls and memory map.

These documents do focus more on the earlier PC-9801 machines somewhat.

- pc-9800technicaldatabookbios.pdf - pc-9800technicaldatabookhardware.pdf

- pc-9801programmersbible.pdf

Other reference websites:

Of particular interest is this diagram (from here) which shows the matrix of the various graphics options of the PC-98 series:

You can see that the 256 colour mode is available on the PC-9821 range, but its functionality is split; planar and packed-pixel/chunky. Fortunately packed-pixel is the easiest to work with (and closest to IBM PC style VGA), where one byte is one pixel, and is available on all of the PC-9821 range.

The options for C compilers for the PC-98 are limited; although it the PC-98 runs a version of MS-DOS, it isn't a PC compatible in the strictest sense of the word:

  • Different BIOS interface
  • Different set of interrupts
  • Different memory map
  • Different types of peripherals
  • Different sound hardware
  • Different video hardware

There's no ISA bus, no VGA, or anything like that. So the hardware interface side of things is quite different. You can run well behaved DOS applications, but only if they use the DOS api and don't hit the hardware directly; so nothing that does video or sound, for example.

These compilers were written to run directly on the hardware itself, compatability with current C standards and syntax is likely to be low, but they are well proven (most of them were previously commercial packages):

C Compilers
C++ Compilers

There doesn't appear to be a specific PC-98 target for GCC (either native, or as a cross-compiler option), but there does exist some support in DJGPP (the port of GCC to MS-DOS).

Firstly, this is all of the disparate legacy DJGPP stuff that was released years ago. Unfortunately there isn't a single, self-contained distribution containing all of the necessary parts for a working build/runtime environment:

DJGPP 1.12

DJGPP 2.03

There exists a patch to add in PC-98 support to DJGPP 2.03, but it's a patch against both the source code/headers, and a binary diff against the version of libc.a included with DJGPP, which is unusual, to say the least. A translated description of the patch is linked here.

Note: If you just want a fully working DJGPP build environment and don't want to know how to set it up from scratch, skip to the end step to download a pre-build archive.

This is how you build the patch:

  1. Unzip DJGPP 2.03 binaries (into ./djgpp/)
  2. Unzip DJGPP 2.03 source (into ./djsrc/)
  3. Unzip DJGPP 2.03 libc patch, which contains dj203980.dif and dj203980.ldf
  4. Convert patch file to unix line breaks and apply patch
$ cd djsrc
djsrc$ dos2unix ../dj203980.dif
dos2unix: converting file ../dj203980.dif to Unix format...
djsrc$ patch -p0 -b --binary < ../dj203980.dif
checking file include/libc/pc9800.h
checking file src/libc/bios/b_time.c
checking file src/libc/bios/bioscom.c
checking file src/libc/bios/biosdisk.c
checking file src/libc/bios/biosequi.c
checking file src/libc/bios/bioskey.c
checking file src/libc/bios/biosmem.c
checking file src/libc/bios/biosprin.c
checking file src/libc/bios/biostime.c
checking file src/libc/compat/time/rawclock.c
checking file src/libc/crt0/crt1.c
Hunk #2 succeeded at 20 with fuzz 1 (offset 1 line).
Hunk #3 succeeded at 33 (offset 1 line).
Hunk #4 succeeded at 86 (offset 26 lines).
Hunk #5 succeeded at 141 (offset 26 lines).
checking file src/libc/dos/dos/delay.c
checking file src/libc/dos/io/flushdc.c
checking file src/libc/go32/dpmiexcp.c
Hunk #2 FAILED at 22.
Hunk #3 succeeded at 107 (offset 1 line).
Hunk #4 FAILED at 193.
Hunk #5 succeeded at 519 with fuzz 2 (offset 1 line).
Hunk #6 succeeded at 553 (offset 1 line).
Hunk #7 succeeded at 570 (offset 1 line).
2 out of 7 hunks FAILED
checking file src/libc/pc_hw/co80/conio.c
checking file src/libc/pc_hw/co80/conio98.c
checking file src/libc/pc_hw/co80/scattrib.c
checking file src/libc/pc_hw/co80/scclear.c
checking file src/libc/pc_hw/co80/sccols.c
checking file src/libc/pc_hw/co80/scgetc.c
checking file src/libc/pc_hw/co80/scgetch.c
checking file src/libc/pc_hw/co80/scmode.c
checking file src/libc/pc_hw/co80/scputc.c
checking file src/libc/pc_hw/co80/scputs.c
checking file src/libc/pc_hw/co80/scretr.c
checking file src/libc/pc_hw/co80/scrows.c
checking file src/libc/pc_hw/co80/scsetc.c
checking file src/libc/pc_hw/co80/scupdate.c
checking file src/libc/pc_hw/co80/scupdl.c
checking file src/libc/pc_hw/co80/scvbell.c
checking file src/libc/pc_hw/kb/getkey.c
checking file src/libc/pc_hw/kb/getxkey.c
checking file src/libc/pc_hw/kb/kbhit.c
checking file src/libc/pc_hw/mono/mono.c
checking file src/libc/pc_hw/sound/sound.c
checking file src/libc/pc_hw/timer/clock.c
checking file src/libc/pc_hw/timer/uclock.c
checking file src/libc/posix/termios/tcflush.c
checking file src/libc/posix/termios/tminit.c
checking file src/libc/posix/unistd/getpid.c

Here's a zip of the source tree with the patch applied:

You then have to copy the the new ./include/libc/pc9800.h header to the place where you unzipped the DJGPP 2.03 binaries; there should already be an ./include/libc/ tree as part of the install, just drop the file in there. You can then either build a new libc.a from the updated source files, above, or patch the existing libc.a in the existing DJGPP 2.03 binary install directory.

The install notes for the patch suggest using the ldf tool to apply dj203980.ldf, but try as I might, I simply cannot get this patch to apply properly - it either complains that the timestamp or size of the original DJGPP 2.03 libc.a is incorrect… and if you force the patch, you end up with a libc.a with the same md5sum as the original one.

So, back to the drawing board and try to get DJGPP to compile itself, with the patched source tree from above…

So we install the following packages into, say, ./djgpp/:

Startup Dosbox, set your PATH variable to the directory where the above packages are installed to and change to the DJGPP source tree that you patched, and run make all, for example:

gcc ... -c file1.c ...
gcc ... -c file2.c ...
gcc ... -c file3.c ...

You then wait. And wait. And wait some more.

Seriously, even at 100% cycles, this takes a long, long time. I/O on Dosbox isn't it's strongpoint, and this is a fairly worst-case scenario. As long as the compile process starts, and your Dosbox session has enough RAM (I set it to 64MB), it should be okay. Just leave it for a a good half hour and check on it later.

One more issue that raises its head at this point is that several of the DJGPP source files are named longer than the traditional 8+3 characters allowed in DOS… (this appears to be mainly relate to src/libm/math/*.c), so the associate Makefile for that subirectory needs to be modified to refer to the truncated names.

DJGPP 2.03 End Step

So you end up with a built tree of DJGPP source code, and a magic libc.a containing those patched PC-98 specific functions.

Simply copy over the libc.a to ./djgpp/lib/ and remember that any code you compile and link to libc.a on this setup will be for a PC-98 target, not standard PC. The archive below contains everything, already setup and ready to go:

The modified system calls that the patch creates are detailed ./djgpp/include/libc/pc9800.h, a direct link to that file is included below:

 *  PC9800.H : djgpp v2.03 libc for PC-AT/PC-9800 PATCH Rel.0
 *  Copyright (C) 1997-2000 takas / tantan / Wataru Kaneko / Naoki Takano
#ifndef __PC9800_H_
#define __PC9800_H_
#include <bios.h>
#include <time.h>
#define PCAT	0x00	/* IBM PC-AT and Compatible */
#define DOSV	0x01	/* IBM PC-AT and Compatible with DOS/V */
#define PC98	0x10	/* NEC PC-9800 Series */
#define PC98H	0x11	/* NEC PC-H98 Series - No Support */
#define ISPCAT(mtype)	((mtype & 0xf0) == PCAT)
#define ISPC98(mtype)	((mtype & 0xf0) == PC98)
#define PC98_KEYS	0x6C
#define TXA_98AT(txa)	conv_98at_txatbl[(unsigned char)(txa)] /* at to 98 */
#define TXA_AT98(txa)	conv_at98_txatbl[(unsigned char)(txa)] /* 98 to at */
/* src/libc/bios/b_time.c */
unsigned _bios_timeofday_at(unsigned _cmd, unsigned long *_timeval);
unsigned _bios_timeofday_98(unsigned _cmd, unsigned long *_timeval);
/* src/libc/bios/bioscom.c */
int bioscom_at(int cmd, char data, int port);
int bioscom_98(int cmd, char data, int port);
unsigned char conv_98at_com_mode(int data);
int conv_at98_com_status(int cx);
/* src/libc/bios/biosdisk.c */
int biosdisk_at(int _cmd, int _drive, int _head, int _track, int _sector,
		int _nsects, void *_buffer);
int biosdisk_98(int _cmd, int _drive, int _head, int _track, int _sector,
		int _nsects, void *_buffer);
unsigned _bios_disk_at(unsigned _cmd, struct _diskinfo_t *_di);
unsigned _bios_disk_98(unsigned _cmd, struct _diskinfo_t *_di);
/* src/libc/bios/biosequi.c */
int biosequip_at(void);
int biosequip_98(void);
/* src/libc/bios/bioskey.c */
int bioskey_at(int cmd);
int bioskey_98(int cmd);
int conv_at98_bioskey(unsigned char ah);
/* src/libc/bios/biosmem.c */
int biosmemory_at(void);
int biosmemory_98(void);
/* src/libc/bios/biosprin.c */
int biosprint_at(int _cmd, int _byte, int _port);
int biosprint_98(int _cmd, int _byte, int _port);
/* src/libc/bios/biostime.c */
long biostime_at(int _cmd, long _newtime);
long biostime_98(int _cmd, long _newtime);
/* src/libc/crt0/crt1.c */
extern int __crt0_mtype;
/* src/libc/dos/delay.c */
void delay_at(unsigned msec);
void delay_98(unsigned msec);
/* src/libc/pc_hw/co80/conio.c */
int puttext_at(int c, int r, int c2, int r2, void *buf);
int puttext_98(int c, int r, int c2, int r2, void *buf);
int gettext_at(int c, int r, int c2, int r2, void *buf);
int gettext_98(int c, int r, int c2, int r2, void *buf);
void textmode_at(int mode);
void textmode_98(int mode);
void _setcursortype_at(int type);
void _setcursortype_98(int type);
void fillrow(int row, int left, int right, int fill);
void clrscr_at(void);
void clrscr_98(void);
void insline_at(void);
void insline_98(void);
void insline_dv(void);
void delline_at(void);
void delline_98(void);
void delline_dv(void);
int cputs_at(const char *s);
int cputs_98(const char *s);
int cputs_dv(const char *s);
void _set_screen_lines_at(int nlines);
void _set_screen_lines_98(int nlines);
void blinkvideo_at(void);
void blinkvideo_98(void);
void intensevideo_at(void);
void intensevideo_98(void);
void UpdateDOSV(int row, int col, int chars);
/* src/libc/pc_hw/co80/scattrib.c */
extern unsigned char conv_98at_txatbl[]; /* at to 98 */
extern unsigned char conv_at98_txatbl[]; /* 98 to at */
/* src/libc/pc_hw/co80/scclear.c */
void ScreenClear_at(void);
void ScreenClear_98(void);
/* src/libc/pc_hw/co80/sccols.c */
int ScreenCols_at(void);
int ScreenCols_98(void);
/* src/libc/pc_hw/co80/scgetc.c */
void ScreenGetCursor_at(int *_row, int *_col);
void ScreenGetCursor_98(int *_row, int *_col);
/* src/libc/pc_hw/co80/scgetch.c */
void ScreenGetChar_at(int *_ch, int *_attr, int _x, int _y);
void ScreenGetChar_98(int *_ch, int *_attr, int _x, int _y);
/* src/libc/pc_hw/co80/scmode.c */
int ScreenMode_at(void);
int ScreenMode_98(void);
/* src/libc/pc_hw/co80/scputc.c */
void ScreenPutChar_at(int _ch, int _attr, int _x, int _y);
void ScreenPutChar_98(int _ch, int _attr, int _x, int _y);
/* src/libc/pc_hw/co80/scputs.c */
void ScreenPutString_at(const char *_ch, int _attr, int _x, int _y);
void ScreenPutString_98(const char *_ch, int _attr, int _x, int _y);
/* src/libc/pc_hw/co80/scretr.c */
void ScreenRetrieve_at(void *_virtual_screen);
void ScreenRetrieve_98(void *_virtual_screen);
/* src/libc/pc_hw/co80/scrows.c */
int ScreenRows_at(void);
int ScreenRows_98(void);
/* src/libc/pc_hw/co80/scsetc.c */
void ScreenSetCursor_at(int _row, int _col);
void ScreenSetCursor_98(int _row, int _col);
/* src/libc/pc_hw/co80/scupdate.c */
void ScreenUpdate_at(const void *_virtual_screen);
void ScreenUpdate_98(const void *_virtual_screen);
/* src/libc/pc_hw/co80/scupdl.c */
void ScreenUpdateLine_at(const void *_virtual_screen_line, int _row);
void ScreenUpdateLine_98(const void *_virtual_screen_line, int _row);
/* src/libc/pc_hw/co80/scvbell.c */
void ScreenVisualBell_at(void);
void ScreenVisualBell_98(void);
/* src/libc/pc_hw/kb/getkey.c */
int getkey_at(void);
int getkey_98(void);
int conv_at98_pchwkey(unsigned char ah, unsigned char xflag);
/* src/libc/pc_hw/kb/getxkey.c */
int getxkey_at(void);
int getxkey_98(void);
/* src/libc/pc_hw/kb/kbhit.c */
int kbhit_at(void);
int kbhit_98(void);
/* src/libc/pc_hw/mono/mono.c */
void _mono_clear_at(void);
void _mono_clear_98(void);
void _mono_putc_at(int ch);
void _mono_putc_98(int ch);
/* src/libc/pc_hw/sound/sound.c */
void sound_at(int freq);
void sound_98(int freq);
/* src/libc/pc_hw/clock/clock.c */
clock_t clock_at(void);
clock_t clock_98(void);
/* src/libc/pc_hw/clock/uclock.c */
uclock_t uclock_at(void);
uclock_t uclock_98(void);

DJGPP 2.05 (and later

There isn't a useable version of DJGPP greater than 2.03 at this time, but there are several ongoing projects that are trying to get one working:

Supporting Toolset

Most of the standard GNU tools (binutils, diffutils, gawk, sed, make, bison, patch etc.) are already included in the v2.03 development environment archive above, so should not be needed.

However, they can all be downloaded seperately from:

Be sure not to use any of the versions found outside the deleted sub-folder, as these will almost certainly be too new to work with the version of libc and other supporting libraries included in the environment targetting the PC-98.

DJGPP aims to be be as compatible as possible with mainstream libc functionality (i.e ANSI C), but it's a DOS target at the end of the day, so some things will be different.

Here's the full libc reference manual:

A sample makefile is included below:

# Names of the compiler and friends
AS 		= $(APP_BASE)\as.exe
CC 		= $(APP_BASE)\gcc.exe
LD 		= $(APP_BASE)\ld.exe
OBJCOPY 	= $(APP_BASE)\objcopy.exe
STRIP 		= $(APP_BASE)\strip.exe

# libraries and paths
LIBS	 	=
INCLUDES 	=          

# Compiler flags
ASM_FLAGS 	= -mcpu=i486 -march=i486 
CFLAGS 		= -mcpu=i486 -march=i486 -O3

# What our application is named
TARGET	= sample.exe

all: $(TARGET)

OBJFILES = sample.o 

	gcc.exe $(LDFLAGS) $(OBJFILES) $(LIBS) -o $(TARGET)
	strip.exe $(TARGET)

sample.o: sample.c
	gcc.exe $(CFLAGS) -c sample.c -o sample.o
	del $(OBJFILES)
	del $(TARGET)

You also need to distribute with your application the DJGPP go32-v2.exe DOS extender, and a DPMI server, of which for the PC-9821 there are relatively few. Apart from the fact that there is one distributed along with PC-98 MS-DOS 5.00!!!

You can of course optimise for -mcpu=i586 and -mcpu=pentium, but they're a subset of the PC-9821 range. If you want your code to run on PC-9801 to PC-9821, your only real option is -mcpu=i386, though you may want to run two passes of your application to produce (for example) a generic 386 binary and one tuned for Pentium.

More optimisation options and compile-time flags are available on the GCC documentation pages.

Here's a very trivial piece of code that uses the new PC98 system calls to identify whether the application is running on PC MS-DOS, or on PC-98xx MS-DOS:

#include <stdio.h>
#include <libc/pc9800.h>
int main(){
	int m;
	m = __crt0_mtype;
		printf("Hello, PC world\n");
		printf("Hello, PC98 world\n");
	return 0;

Here's the code compiling and running on Dosbox:

… and the exact same binary running on Neko Project II Kai (PC-98 emulator):

First of all, DPMI32.exe (from the PC-98 DOS 5.00 disks) DPMI.EXE (from the MS-DOS 6.22 distribution) and go32-v2.exe (which we built from the patched sources, above) work on real PC-9821 hardware, just as they do within the NekoProject II Kai emulator, as long as you have a VCPI service running (either EMM386, VEMM486 or similar), the result is the same.

So boot with typical memory managers in place:

… and then try to start DPMI server and the DJGPP go32 DOS extender:

So, we have the same behaviour as on the emulated PC-98 system, and the emulated IBM PC system

However, I struggle to get make.exe to actually run a Makefile. No matter what I try to do, make just will not detect the presence of a makefile in the current directory.

Given a directory of files like this:

… a simple 'make' command should find MAKEFILE, right?:


Even explicitly specifing the file, like:

make.exe -f MAKEFILE

doesn't seem to work, because if I go the belts-and-braces approach and rename MAKEFILE to something sane in the DOS world like MAKEFILE.TXT, and then capture the output:

You can see that make never actually finds the file. There's definitely something strange going on with the way the make.exe is interacting with the files on the PC-98 filesystem is being accessed, compared to the same make.exe accessing files within Dosbox.

It's not the entire GNU toolchain, because calling gcc.exe and other commands manually works:

But not a single variation of the make incantation will make it find and run the makefile:

Argh!!!! What is going on?

Disaster, as I've now found that the same stuff works fine within the emulator:

So, I figured out that there's something broken in the DJGPP build of make 3.71, if instead you use make 4.3, it works perfectly, both in the emulator and on a FAT32 filesystem on the real hardware:

The upside is that nothing else needs changing, just the single make.exe binary.

Here's an updated version of the GCC + DJGPP development environment, included the updated version of make:

Note that this version also includes renamed libreadline.a (libread.a), libstdc++ (libc++.a) and libhistory (libhist.a) libraries, as the defaults supplied with DJGPP are over the 8+3 filename length of non-Windows 95 systems.

Go forth, create code for another obsolete computer platform, and be merry!

Or, alternatively, have a look at my set of real-world programming examples for the NEC PC-9821 and its graphics hardware.

  • blog/pc98_devtools.txt
  • Last modified: 2020/08/22 10:34
  • by john