Cartridges for the IBM PC

Cartridges? For the IBM PC? There’s no such thing!

Well, there kinda is… The IBM PCjr has two cartridge slots. Now, one may argue about whether the PCjr is actually an IBM PC compatible or not, but let’s look at how the PCjr implements this first.

For that, we can check the IBM PCjr Technical Reference, page 2-107 onward. Apparently a cartridge merely contains a ROM, which needs to have a certain header in order to be detected. It starts with the bytes 55h, AAh, and then a length-field (in blocks of 512 bytes). Then 3 bytes where you can store a jump to your initialization routine entry-point. The byte after that, at offset 6, will indicate what type of cartridge it is.

Three types of cartridge are supported:

  1. “Initial Program Loadable” (IPL): The cartridge will register an INT 18h handler for itself at initialization. INT 18h will be called after the BIOS POST is done, and no bootable device was found. This is the mechanism by which BASIC is normally started on an IBM PC or PCjr. BASIC is then the ‘initial program’ that is loaded. When the type-byte at offset 6 is set to 0, the cartridge is expected to be an IPL-type.
  2. DOS Command Extensions: From offset 6 onwards, a list of commands is stored: first a length-byte indicating the length of the command. Then the ASCII-string of the command itself, followed by a (word) offset to the code that implements the command, according to the manual. However, that seems to be an error. Instead, DOS will jump directly to the position directly after the command-string. You have 3 bytes to put a jump to the actual code, similar to the main initialization entry-point (DOS creates a process for you, so you can just use ah=4Ch to exit back to DOS from your command). The list of commands is terminated by an entry with a length of 0.
  3. BASIC cartridges: Instead of the 3-byte jump at offset 3, you have the sequence: CBh, 55h, AAh. The type-byte at offset 6 will be set to 0. Then at offset 7, there will be a byte that is either FEh for a protected BASIC program, or FFh for unprotected.

After this initial header, the code (either x86 machine code or BASIC) will be stored, and at the end of the image, there will be two bytes that contain a 16-bit CRC code. We will get back to that later.

But first… did you see what they did there? With the 3 types? They tricked us! Because if you consider a DOS command image with no commands, then technically it is the same as an IPL-type. What you do in the initialization code is pure semantics. And the CBh byte for the BASIC cartridge is actually a RETF instruction. So that is technically an empty initialization function. Apparently the BIOS can still call into it, it will just return immediately. The 55h and AAh bytes are then additional bytes that BASIC can check for at a later point. Likewise it seems that the DOS commands are actually scanned by DOS at a later part in the boot sequence, not during BIOS initialization. So what this appears to boil down to is that the BIOS merely checks for valid ROMs by scanning for the 55h AAh marker, then checking the length and verifying the CRC value, and if that checks out, it will do a far-call into offset 3 to call the initialization routine of the ROM.

But we can trick them as well, can’t we? Yes, it would seem that although the jump at offset 3 should technically be jumping into an initialization function, and then RETF back to the BIOS initialization, in practice various cartridges actually jump directly into the program code, and just never return to the BIOS at all. This fundamentally changes how the cartridge works: If you just use the initialization routine to install an INT 18h handler, then the cartridge code will only be executed when no bootable device is found (so only when you have not inserted a bootable floppy, and you have no other boot device, such as a HDD installed). If you start your application code directly from the init function, then it will execute regardless of whether other boot devices are available.

If you compare this cartridge system with the BIOS extension system of the regular IBM PC and compatibles, you’ll find that it works in basically the same way. You can find it in the IBM PC Technical Reference, page 5-13 onward. That is, the PC BIOS does not appear to support DOS command extensions or BASIC extensions via ROM, but an IPL-type ROM for PCjr is very similar to a PC BIOS extension. The initial 55h, AAh bytes and the length-value are stored in the same way. And in both cases the BIOS will also do a far-call to offset 3. The PCjr simply extended this mechanism by jumping over the optional DOS commands that can be stored, starting at offset 6.

Also, where the PCjr uses a CRC16 to verify the ROM, the PC BIOS uses a simple checksum (just adding all bytes together, modulo 100h). But the idea is the same: after processing all bytes in the ROM, the CRC16 or checksum should be 0, so you should add a value at the end of your ROM to make sure it gets to 0. That will make it somewhat difficult to make a single ROM that works on both types of machines (it would need to pass both the CRC16 and the checksum-test), but you can trivially make both a PC and a PCjr version out of the same codebase, by only adjusting the CRC/checksum-value for the specific target before writing it to a ROM.

The address range of the ROMs is also different: the PC BIOS extensions use the segment range C000h-F400h, where the PCjr cartridge uses segment range D000h-F000h (allowing to add up to 128k of ROM space via two cartridges). In both cases, the ROM area is scanned at an interval of 2k blocks. So a new ROM image (indicated by the 55h AAh marker) can occur at every 2k boundary. The PCjr simply treats ROMs at D000h and above as cartridge ROMs (and as such expects CRC16), where the rest are assumed to be standard BIOS extensions (with a checksum).

So in theory you can create a ROM containing an application for a regular IBM PC as well, and stick it onto an ISA card, as a makeshift ‘cartridge’. In practice it’s just less convenient as you cannot easily insert and remove it. That might explain why the support for DOS command or BASIC expansion ROMs was never added to regular PCs, and as such, remains a PCjr-exclusive.

However, the cartridge port on the PCjr is basically just a cut-down ISA slot. In theory you could create an ISA card with a PCjr-like cartridge slot in the bracket at the back, so that you can insert cartridges with ROMs that way. And once you can easily insert and remove ROMs like that, it makes sense to create ROMs that contain games or other software.

Why cartridges/ROMs?

Is there a point to using cartridges/ROMs, you may ask? Can’t you just load your software from a floppy or harddisk? Well yes, there is. Roughly there are two advantages to storing software in ROM:

  1. The ROM is mapped into a separate part of the address space, so your code and data can be accessed directly from the ROM, leaving your RAM free for other things.
  2. Unlike the dynamic RAM in your machine, the ROM does not need to be refreshed. As such, when reading from ROM, you do not incur any waitstates. This makes ROM slightly faster than RAM.

For a regular PC this is not such a big issue, as they generally have lots of RAM anyway, and a regular PC refreshes memory only once every 18 IO cycles (roughly every 15 microseconds).

For a PCjr however, things are different. A standard PCjr has only 64k or 128k of memory, and this is shared with the video circuit. So it reserves 16k to 32k of video memory from this pool. The 64k models were sold without a floppy drive, as DOS would not be much use in the remaining 48k of memory. Even 128k is still quite cramped for DOS. So if you could store your application in ROM, you can make more of the system RAM.

And PCjr shares its main memory between the CPU and the video chip. This means you don’t get a memory refresh cycle once every 18 IO cycles. Instead, the video circuit will access memory once every 4 IO cycles, generating a wait state that blocks the CPU from accessing memory. Effectively this makes the CPU run code much slower from system RAM than from ROM or expansion RAM. Putting code and data in a ROM cartridge will fix this performance disadvantage of the PCjr, making it run as fast as a regular PC (or actually slightly faster).

Lotus 1-2-3 was released on two cartridges for the PCjr at the time, as the application itself was already 128k in size. It was still a DOS application, the cartridge made use of the DOS command extension mechanism to register ‘123’ as a command, so it would start from the cartridge when you entered ‘123’ at the DOS prompt. That was a clever way to make the most of the PCjr. It ran from DOS, so you would have access to floppy storage without any issues. It saved precious system memory by storing the application in ROM. And the ROM also made the code run faster than if it were a regular DOS application running from RAM.

So let’s see what it takes to make our own cartridge.

The CRC check

Let’s look closer at how the CRC check works. We are going to need it in order to pass the validation check in the BIOS, so it will actually run our code from the ROM.

On page 2-109, the Technical Reference points to the ‘crc_check’ routine in the BIOS listing, to see how this works. We can find this CRC routine on page A-106:

So the CRC check basically scans the entire ROM image (including all headers), and the check passes if the resulting CRC value is 0.

On page 2-108, the manual says that you should put ‘CRC Bytes’ at the Last 2 Addresses’ of your ROM image. What they mean to say is that the CRC should yield 0. But the CRC is just a polynomial calculated over the ROM contents, so it can be anything, depending on the contents. In order to get it to 0, you need to add extra ‘dummy bytes’ to get the polynomial to reach 0 at the end. And since this is a 16-bit CRC, two bytes are enough to compensate.

But if you study the code, you see that it simply calculates the CRC over the entire area, and doesn’t specifically treat the last two bytes as something special. This means that theoretically you can place them anywhere in your image. In fact, in the rare case that the CRC already reaches 0 without adding extra bytes, you can just leave them out altogether.

However, in practice, it is easiest to place them at the end. Namely, how do you generate these two bytes? Well, one way is to just bruteforce all options (there are only 65536 possibilities), until you find one. But if you have to calculate the CRC over the entire ROM every time, this will be relatively inefficient. CRC is an incremental algorithm however. That is, you start out with an initial value (which by default is FFFFh as you can see in the code), and then add on to that.

So if you want to bruteforce the last two bytes of a ROM image of N bytes, you simply calculate the CRC up to N-2 bytes first. Then you use this CRC as the initial value, and calculate a CRC for the last two bytes. So you only have to calculate up to 65536 CRCs of 2 bytes in order to find the right combination. Even an 8088 at 4.77 MHz can do that almost instantly.

Writing the code

It’s somewhat tricky to write actual ROM code, because the usual programming environments are aimed at DOS, and as such want to generate either a .COM or a .EXE file. Generating a valid ROM image is not directly possible with most programming environments. A .COM file is pretty close to a raw binary image though: it is a single segment of up to 64k in size, containing both code and data. Obviously you have to avoid any dependencies on DOS in the .COM file, so you probably need to disable any runtime libraries. In which case the main difference with a raw binary is that a .COM file starts at offset 100h.

This is another DOS-dependency, as DOS creates a Program Segment Prefix (PSP) block for every process that it starts. This block is 256 bytes long, and the .COM file is loaded directly after it, in the same segment. DOS will then call the code at offset 100h as your entry point.

In a cartridge we do not have a PSP, but we do have a header, with a jump into the actual code. This header is much smaller than 256 bytes. So we could create a cartridge header with a jump to 100h, then pad up to offset 100h, and then append the .COM file there.

However, I chose to use MASM, and in that case you specifically have to place a ‘org 100h’ directive to make the code start at offset 100h. If you omit that directive, it will start the code (or data) at offset 0. This means I can actually place the header inside my assembly code, and place my code and data directly after the cartridge header. I created a template like so:

.Model Tiny

.code
; Cartridge header
marker dw 0AA55h
len db 0
jmp InitROM
; List of DOS commands
org 6	
db 0

; Start of ROM data

InitROM PROC
...
retf
InitROM ENDP

...

END

The generated .COM file will then start with the header, and the code will correctly assume that the image is loaded at offset 0 in the segment, not offset 100h. There are just three problems left:

  1. The length-field is left at 0, because it should be specified in blocks of 512 bytes. While it is possible to do simple addition and subtraction with labels in MASM to get the size of a block of code or data, it is not possible to divide the size by 512. So we cannot let the assembler generate the correct length in the image automatically.
  2. The image size needs to be aligned to blocks of 512 bytes. The assembler can only do alignment up to 256 bytes (the size of a ‘page’).
  3. The image needs to generate a CRC value of 0, which as mentioned above, will require adding additional bytes to adjust the CRC.

I have created a simple tool for this, based on the actual BIOS code. It checks if the length-field matches with the actual size of the binary. If there is a match, it assumes the image is already prepared for a cartridge, and it will calculate the CRC for the image, and report whether it is 0 or not.

If the size is a mismatch, it will assume the image needs to be prepared. It will first add two bytes to the size, to make room for the CRC adjustment. Then it will align the size up to the next 512-byte boundary, and write the size into the header. Then it will calculate the CRC for the entire image, save for the last two bytes. The final step is to bruteforce the last two bytes to find a value that gives a CRC result of 0. These are then written to the image, and the result should be a valid cartridge image.

I will probably put this tool in the DOS SDK when it has matured a bit. If you are curious, other people have also created cartridges, and released tools, for example, this entry on Hackaday by SHAOS.

For completeness, this more elaborate template demonstrates an initialization function, which registers an int 18h handler, and implements two DOS commands:

.8086
.Model Tiny

; Macro to make sure the jump to the command always takes 3 bytes
CARTPROC MACRO target
	LOCAL start

	start EQU $
	
	jmp target	; This must be a 3-byte field, so a relative jump with 16-bit offset, or else pad with a nop before the next command
	org (start+3)
ENDM

; Macro to add a DOS Command to the header
DOSCOMMAND MACRO cmd, target
	LOCAL str

	db SIZEOF str	; Length of command string
	str db cmd	; Command must be uppercase

	CARTPROC target	; Add 3-byte jump code to command proc
ENDM

.code
; Cartridge Header
marker dw 0AA55h
len db 0
jmp InitROM
org 6

; DOS Commands
DOSCOMMAND "TEST1", test1
DOSCOMMAND "TEST2", test2

; End DOS Commands
db 0

; Zero-terminated string in DS:SI
Print PROC
	; Get current video mode, so current page is set in BH
	mov ah, 0Fh
	int 10h
	
	lodsb
	test al, al
	je done

outputString:
		; Output char to TTY
		mov ah, 0Eh
		int 10h
		
		lodsb
		test al, al
		jne outputString
done:
	ret
Print ENDP

; Start of ROM data
InitROM PROC
	mov si, offset initString
	call Print

	; Set INT18h handler
	pushf
	cli
	
	push ds
	xor ax, ax
	mov ds, ax
	
	mov ds:[18h*4 + 0], offset EntryPoint
	mov ds:[18h*4 + 2], cs
				
	pop ds
	popf		
	
	retf
InitROM ENDP
		
EntryPoint PROC
	sti

	push cs
	pop ds

	mov si, offset entryString
	call Print
	
	iret
EntryPoint ENDP

initString db "Cartridge Initialization",0
entryString db "Cartridge EntryPoint",0

test1str db "test1",0
test2str db "test2",0

test1 PROC
	push ds
	push cs
	pop ds

	mov si, offset test1str
	call Print
	
	pop ds
	
	mov ax, 4C00h
	int 21h
test1 ENDP

test2 PROC
	push ds
	push cs
	pop ds

	mov si, offset test2str
	call Print
	
	pop ds
	
	mov ax, 4C00h
	int 21h
test2 ENDP

.data

END

That gives you all the possible functionality, aside from BASIC stuff. You could combine everything in a single cartridge as this template demonstrates, but the idea is to just show the different techniques, so you can pick and choose. If you just want a DOS cartridge, you don’t need the initialization and int 18h handlers. Just a retf at initialization is good enough.

And the int 18h setup is only for when you want the cartridge to start only when no boot device is found. So you can control whether or not to run the cartridge by inserting a bootable floppy. If you want the cartridge to always run when inserted, just put your code directly in the inititalization.

Running the cartridge code

Now that we know how to create a cartridge image, how do we actually run and test it? The easiest way to do a quick test is to use an emulator. A file format for PCjr cartridges exists, originally developed for the Tand-Em emulator (which, as the name implies, was originally a Tandy 1000 emulator, but the PCjr is of course a close relative). A utility by the name of JRipCart was developed to extract PCjr cartridges to files with the extension .JRC, for use in emulators. The JRC format is very simple. It’s just a header of 512 bytes in size, with some metadata about the ROM, which is prepended to a raw cartridge image. You can easily make the header yourself, and then use the copy-command to append the two files into a new .JRC file:

copy /b HEADER.JRC+CARTRIDGE.BIN CARTRIDGE.JRC

A popular PCjr emulator with support for .JRC is DOSBox. You can load a .JRC file via the ‘BOOT’ command when you are using the PCjr machine configuration, similar to how you’d boot a floppy image. Note that DOSBox support is very limited, and the current release at time of writing, .074-3, does not actually verify the length field or the CRC, so it does not tell you whether or not your cartridge ROM is fully valid. It merely loads the ROM and executes the code from offset 3.

But of course we would also want to run it on actual hardware. There are various ways to do that. You can go the ‘classic’ way of creating an actual PCB with ROMs on it. You can find the schematics online from various projects, including one from SHAOS, or from Raphnet. The SHAOS-design is true to the original PCjr cartridges, and uses 32k EPROMs. So you need two 32k EPROMs for a 64k cartridge. Raphnet uses a design based around a more modern EPROM type which is available in 64k, so a single EPROM is enough (although it requires a 74LS08 IC for some glue-logic).

Speaking of Raphnet, he has also developed an interesting PCjr cartridge, which allows you to use an SD-card as a harddisk. That is especially interesting since the cartridge port is a read-only interface. So the design uses a clever hack to allow writing to the device as well.

However, this design currently does not allow you to store cartridge images on the SD-card, and load them as ROMs, as far as I understand. There is another device available, which can do exactly that, the BackBit Pro cartridge:

I bought one of these, for two reasons:

  1. I don’t currently have the tools to (re)program EPROMs.
  2. Using an SD-card is more convenient than having to reprogram EPROMs and putting them on a PCB everytime during development.

I suppose once you’ve developed and tested your cartridge image, you can always have a ‘real’ EPROM cartridge made from it later.

The BackBit Pro cartridge is a very interesting device, in that it is a ‘universal’ cartridge, that you plug into a suitable adapter for your target system. There is quite a selection of adapters available, and last year, a PCjr adapter has been added:

It works in a way similar to other devices that simulate cartridges, floppy drives and such via SD-cards, USB drives and whatnot: When you boot up the system (or press the reset button), a menu pops up, which allows you to browse the SD-card and select a ROM file. The system then reboots and runs the cartridge.

The BackBit Pro has a few other tricks up its sleeve. Aside from emulating cartridges, it can also be used to load alternative BIOSes (these were a thing on the PCjr, for example, to make it appear like a regular PC for better compatibility, or to skip the REALLY slow memory test when you have a large memory expansion). And it can load .COM files or floppy images directly (great for booter games).

The current version only seems to support raw cartridge images, which have to have the .BIN extension. It does not appear to support .JRC files yet. But that’s not a big deal, as it’s trivial to convert a .JRC to a raw image: just cut off the first 512 bytes.

I also had some issues with my jr-IDE installed: the BackBit Pro selection screen would not start. You would see it change the border colour to its characteristic pink colour, but then the system froze. Other than that I didn’t encounter any problems, so I can certainly recommend the BackBit Pro for cartridge development, or just as a nice tool for running software on the PCjr.

Update (5-6-2023): The BackBit Pro has received a firmware update (2.1.2), which solves the issue with the jr-IDE. Apparently the issue was that the jr-IDE switched to 80-column mode, which also enables a different page of video memory. So the BackBit Pro was actually working, it was just updating a part of VRAM that was not visible at the time. It now switches back to 40-column mode and a known page of display, which fixes the issue. I think in general it’s a good idea not to make assumptions about what mode and display page you’re in, when writing BIOS or cartridge ROMs. I have found some issues with using the BIOS int 10h TTY output from a cartridge as well.

The firmware also adds support for .JRC files, so you don’t need to convert them before copying them to your SD-card. Excellent work!

Anyway, hope this has been a helpful primer into the world of PC/PCjr cartridges and BIOS extension ROMs.

This entry was posted in Hardware news, Oldskool/retro programming and tagged , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

1 Response to Cartridges for the IBM PC

  1. Pingback: IBM PC的墨盒 - 偏执的码农

Leave a comment