PCJr Cartridge with battery-backed S-RAM
Posted: Sun Jan 23, 2022 9:47 pm
Howdy folks!
As I've been reading up on the IBM PCJr (both the criticism of the day and modern looks at the system), it seems that the primary re-occurring theme of both then and now is a focus on the IBM PC: what it lacks in comparison to a PC, how to modify it to make it a somewhat useable PC, etc. I can't really argue with that (and in hindsight I suspect that was for the best), but it did seem pretty interesting to me that the actual base PCJr "platform" itself seems to have never really been exercised with a practical example. Almost everything run on it is PC DOS software after the system has been modified to 640K, and I think other than running the cartridge games from launch, BASIC-ally(
) nothing runs on a base PCJr.
I wanted to take a step back and start a thought experiment of what choices IBM and Microsoft would have had if they had decided to try and save the platform, with a focus on the base PCJr as a driver for some hardware design projects to do in my spare time. Looking at other cartridge-based systems, I think there were three major "Force-Multipliers" when it comes to the complexity of the games that ran on them, and wanted to look at each of them:
1.) Save games (and extra RAM)
2.) Banking to support larger amounts of memory than the cartridge address window supports
3.) Co-processors
All three of these run into a one-way wall on the PCJr: the PCJr does not support writing data to the cartridge port, only reading (system sends an address and a chipselect, the cartridge reads that address/chip select and writes data, and system reads that data). As cartridge based systems evolved, most of them added a read/write signal that the system also sends to the cartridge which the PCJr lacks.
Which would be the end of it, if it wasn't for a technicality: the PCJr can't signal a write or send data over the data lines but most definitely does send some information (the address it wants to read). A slightly more complex cartridge design could use part of a read address or a sequence of reads to special addresses as a write. Raphnet's modern SD-Cart JR proves (via existence) that it's possible (and the period Integrity Technology Clock Cartridge likely does something similar). The SD-Cart JR technically completely solves 1 & 2 with essentially infinite free space, but also does have tradeoffs for speed and system RAM usage (and of course SD cards came to the market a little late for the PCJr).
Goal #1:
Design and test a prototype cartridge that has a program ROM, a battery-backed S-RAM to save data, and simple LS style logic that, while still relatively expensive, are close-ish to having been a feasible option as the platform games hypothetically got more advanced. Sorta. Squint a little if you need to
.
Components (everything except the PCB I pulled from parts I had lying around):
1 PCB
1 ROM chip
1 S-RAM chip
1 LS373 Octal latch
1 LS00 NAND gate
2 Diodes
6 pull-up/pull-down resistors
4 capacitors (bypass/bulk)
1 CR2025 (game-boy save battery)
API:
1.) Read ROM
Having the CPU read from the D0000h-D7FFFh address space (CS2) simply has the CPU retrieve the expected value from the ROM (in my prototype I just have an 8K ROM so it is mirrored multiple times)
2.) Read RAM
Having the CPU read from the E0000h-E7FFFh address space (CS4) has two affects:
a.) Actually performs the intended read: CPU retrieves the expected value from the RAM (in my prototype I have a 128K RAM, only the upper-most 32K is used)
b.) Stores (caches) the lower 8-bits of the address in the LS373 Octal Latch
3.) Writing RAM
Having the CPU read from the E8000h-EFFFFh address space (CS5) has the LS373 output the last cached data onto the data bus which:
a.) CPU ends up retrieving the cached data (which is a dummy read)
b.) More importantly, CS5 is wired to the RAM's Write pin so the LS373 output on the databus is also written to RAM
Behavior:
Cartridge ROM Read: should behave like any ROM: set the data segment and read into registers with no wait-states. I believe this is more or less the fastest possible speed that the base PCJr is capable of retrieving data at. You can read bytes or words. Copying data from ROM to System RAM likely needs DS/ES addressing (extra bytes for encoding) and the base PCJr System RAM is shared with the video card (extra wait-states, though I would guess if you timed a transfer during V-Blank you could get the wait-states to a minimum?)
Cartridge RAM Read: same as the ROM: set the data segment and read into registers with no wait-states, copying to System RAM. You can read bytes or words.
Cartridge RAM Write: very slow (I haven't done any timing / transfer rate calculations yet). Essentially at a minimum you're doing reads to two different addresses. My proof of concept is meant more fore readability / obvious behavior than it is for efficiency, but you're definitely going to be shuffling segments or adding 08000h. There is no writing a "Word" in little-endian style natively either, it's byte-by-byte so if you want to read data as words you need to write the data accordingly.
Boot:
Because the PCJr BIOS reads every 2K in cartridge space looking for cartridges to boot, and reads in the E8000h-EFFFFh trigger "write" circuitry, action must be taken to protect the Cartridge RAM. If you intend to let the BIOS finish booting (using the int18h entry), the init code must also modify the PCJr BIOS ROM counter so that it thinks it already read the cartridge space. My init function looks like:
With the primary goal being just "Save-game" data, the write-speed being pretty slow/inefficient wouldn't necessarily be an issue. You can also use the save RAM area as graphics decompression area: decompression would be slow but once the area's graphics are decompressed you'd have full speed read-access to them. In the end, you'd have the flexibility to use 32K of System RAM to double buffer the screen, and have 32K of Cartridge RAM (very slow-write, fast-read) and ~30ish K of System RAM (slow/medium write, slow/medium read) to juggle graphics and game data.
Proof of concept seems to work, though with some of the components sub-optimal I do need to exercise it a bit more (example, the random diodes I grabbed have a voltage drop that is a bit too high for this application, so it may be surviving based on stored energy in the capacitors in the short term, need to see if it retains the data over a few days. I've also only exercised the first few bytes). It's also a bit longer than a normal cartridge but a swap to surface mount (or smarter routing) would fix that. As most of the cartridge games I've seen on the system tend to look like older Atari type games, I included a pair of photos of mockups I did when first experimenting to show what GameBoy graphics might look like [running on my full PCJr]. My guess is even with page flipping and copying from cartridge to video ram during V-Blank the lack of hardware scrolling support would make something like that pretty unlikely, but game-boy graphics in a CGA palette at "high" resolution does look pretty cool.
Next steps for me:
SW - write a more robust proof of concept usage that is a little more game-like and more completely verify the cartridge is working as designed
HW - Next proof of concept is to try and make a bank-switching circuit to allow for larger cartridges
PS. Is there a summary list out there of software that actually uses the PCJr specific audio and graphics features without modification?
As I've been reading up on the IBM PCJr (both the criticism of the day and modern looks at the system), it seems that the primary re-occurring theme of both then and now is a focus on the IBM PC: what it lacks in comparison to a PC, how to modify it to make it a somewhat useable PC, etc. I can't really argue with that (and in hindsight I suspect that was for the best), but it did seem pretty interesting to me that the actual base PCJr "platform" itself seems to have never really been exercised with a practical example. Almost everything run on it is PC DOS software after the system has been modified to 640K, and I think other than running the cartridge games from launch, BASIC-ally(
I wanted to take a step back and start a thought experiment of what choices IBM and Microsoft would have had if they had decided to try and save the platform, with a focus on the base PCJr as a driver for some hardware design projects to do in my spare time. Looking at other cartridge-based systems, I think there were three major "Force-Multipliers" when it comes to the complexity of the games that ran on them, and wanted to look at each of them:
1.) Save games (and extra RAM)
2.) Banking to support larger amounts of memory than the cartridge address window supports
3.) Co-processors
All three of these run into a one-way wall on the PCJr: the PCJr does not support writing data to the cartridge port, only reading (system sends an address and a chipselect, the cartridge reads that address/chip select and writes data, and system reads that data). As cartridge based systems evolved, most of them added a read/write signal that the system also sends to the cartridge which the PCJr lacks.
Which would be the end of it, if it wasn't for a technicality: the PCJr can't signal a write or send data over the data lines but most definitely does send some information (the address it wants to read). A slightly more complex cartridge design could use part of a read address or a sequence of reads to special addresses as a write. Raphnet's modern SD-Cart JR proves (via existence) that it's possible (and the period Integrity Technology Clock Cartridge likely does something similar). The SD-Cart JR technically completely solves 1 & 2 with essentially infinite free space, but also does have tradeoffs for speed and system RAM usage (and of course SD cards came to the market a little late for the PCJr).
Goal #1:
Design and test a prototype cartridge that has a program ROM, a battery-backed S-RAM to save data, and simple LS style logic that, while still relatively expensive, are close-ish to having been a feasible option as the platform games hypothetically got more advanced. Sorta. Squint a little if you need to
Components (everything except the PCB I pulled from parts I had lying around):
1 PCB
1 ROM chip
1 S-RAM chip
1 LS373 Octal latch
1 LS00 NAND gate
2 Diodes
6 pull-up/pull-down resistors
4 capacitors (bypass/bulk)
1 CR2025 (game-boy save battery)
API:
1.) Read ROM
Having the CPU read from the D0000h-D7FFFh address space (CS2) simply has the CPU retrieve the expected value from the ROM (in my prototype I just have an 8K ROM so it is mirrored multiple times)
2.) Read RAM
Having the CPU read from the E0000h-E7FFFh address space (CS4) has two affects:
a.) Actually performs the intended read: CPU retrieves the expected value from the RAM (in my prototype I have a 128K RAM, only the upper-most 32K is used)
b.) Stores (caches) the lower 8-bits of the address in the LS373 Octal Latch
3.) Writing RAM
Having the CPU read from the E8000h-EFFFFh address space (CS5) has the LS373 output the last cached data onto the data bus which:
a.) CPU ends up retrieving the cached data (which is a dummy read)
b.) More importantly, CS5 is wired to the RAM's Write pin so the LS373 output on the databus is also written to RAM
Behavior:
Cartridge ROM Read: should behave like any ROM: set the data segment and read into registers with no wait-states. I believe this is more or less the fastest possible speed that the base PCJr is capable of retrieving data at. You can read bytes or words. Copying data from ROM to System RAM likely needs DS/ES addressing (extra bytes for encoding) and the base PCJr System RAM is shared with the video card (extra wait-states, though I would guess if you timed a transfer during V-Blank you could get the wait-states to a minimum?)
Cartridge RAM Read: same as the ROM: set the data segment and read into registers with no wait-states, copying to System RAM. You can read bytes or words.
Cartridge RAM Write: very slow (I haven't done any timing / transfer rate calculations yet). Essentially at a minimum you're doing reads to two different addresses. My proof of concept is meant more fore readability / obvious behavior than it is for efficiency, but you're definitely going to be shuffling segments or adding 08000h. There is no writing a "Word" in little-endian style natively either, it's byte-by-byte so if you want to read data as words you need to write the data accordingly.
Code: Select all
;; al = data byte to write
;; bx = "Lower 32K" version of the RAM address, returns bx + 1 for slightly easier sequential writes
push ds
push bx
;; Cache Data
mov bx,ram_seg_read ; ram_set_read = 0E000h
mov ds,bx
mov bh,00h ;; bh can technically be anything less than 80h
mov bl,al
mov al,[bx] ; dummy read to cache the data
;; Write Data
mov bx,ram_seg_write ; ram_set_read = 0E800h
mov ds,bx
pop bx
mov al,[bx]
inc bx
pop ds
iret
Because the PCJr BIOS reads every 2K in cartridge space looking for cartridges to boot, and reads in the E8000h-EFFFFh trigger "write" circuitry, action must be taken to protect the Cartridge RAM. If you intend to let the BIOS finish booting (using the int18h entry), the init code must also modify the PCJr BIOS ROM counter so that it thinks it already read the cartridge space. My init function looks like:
Code: Select all
INIT PROC FAR
; The BIOS pushes DX (the current ROM block being checked for signatures) to the stack
; and then does a call (far) (which pushes the segment and offset before jumping)
; Assumes your init didn't push additional data to the stack
mov bp,sp ; Set the base pointer
mov ax,0F000h ;
mov [bp+4],ax ; Set the "current ROM block" to be after the end of the cartridge
; Set up the interrupt vector to boot into the cartridge
mov ax,0000h
mov es,ax ; Set ES to 0000h (the interrupt vector segment)
mov ax,cs
mov es:int18seg_loc,ax ; Move the current Code Segment to the Interrupt Vector 2nd 2 bytes
mov ax,offset MAIN
mov es:int18off_loc,ax ; Move the Main Offset to the Interrupt Vector 1st 2 bytes
ret
INIT ENDP
With the primary goal being just "Save-game" data, the write-speed being pretty slow/inefficient wouldn't necessarily be an issue. You can also use the save RAM area as graphics decompression area: decompression would be slow but once the area's graphics are decompressed you'd have full speed read-access to them. In the end, you'd have the flexibility to use 32K of System RAM to double buffer the screen, and have 32K of Cartridge RAM (very slow-write, fast-read) and ~30ish K of System RAM (slow/medium write, slow/medium read) to juggle graphics and game data.
Proof of concept seems to work, though with some of the components sub-optimal I do need to exercise it a bit more (example, the random diodes I grabbed have a voltage drop that is a bit too high for this application, so it may be surviving based on stored energy in the capacitors in the short term, need to see if it retains the data over a few days. I've also only exercised the first few bytes). It's also a bit longer than a normal cartridge but a swap to surface mount (or smarter routing) would fix that. As most of the cartridge games I've seen on the system tend to look like older Atari type games, I included a pair of photos of mockups I did when first experimenting to show what GameBoy graphics might look like [running on my full PCJr]. My guess is even with page flipping and copying from cartridge to video ram during V-Blank the lack of hardware scrolling support would make something like that pretty unlikely, but game-boy graphics in a CGA palette at "high" resolution does look pretty cool.
Next steps for me:
SW - write a more robust proof of concept usage that is a little more game-like and more completely verify the cartridge is working as designed
HW - Next proof of concept is to try and make a bank-switching circuit to allow for larger cartridges
PS. Is there a summary list out there of software that actually uses the PCJr specific audio and graphics features without modification?