VG64 Project: Adding a Second Monitor to the Commodore 64

After coming up with the idea of ​​adding a second display to the Commodore 64, I quickly implemented this project. All the hardware fits into a standard size cartridge (including the DE-15 connector). Video output compatible with VGA (31 kHz).

Inside the cartridge is 128KB SRAM for the framebuffer and a simple 1-bit DAC.

TL; DR

This is what the board looks like inside the cartridge. Download source can be here

Programming interface

The cartridge can be placed anywhere in the 64KB address space, including I / O1 or I / O2. There is Verilog code to present either in a window in the @EXPROM framebuffer, which will take up 8KB of Basic memory, or a register-based approach that saves RAM.

In the examples shown, I / O1 at $ DE00 is used for the control registers. You may want to change the example provided if there is a conflict with some other addon (second SID chip, etc.). In general, there is support for a special token that allows you to avoid conflicts, but I do not have additional software that causes these conflicts.

Registers

IOBASE = token
IOBASE + 1 = lsb address
IOBASE + 2 = msb address
IOBASE + 3 = data

The framebuffer is linear and easy to use, like the native C64 bitmap modes. At SRAM, it starts at $ 00,000.

Video output

Regardless of the selected mode, video is output with a pixel rate of 25 MHz thanks to the built-in 100 MHz generator. This parameter is close to the 25.175 MHz standard for a 640×480 screen at 60 Hz FPS. Accordingly, any display I connected showed the image correctly and without problems. The vertical and horizontal sync and blanking areas are set to the correct polarity and length to trigger this mode. There are two possible interpretations of framebuffer data: high resolution 640×480 1 bit per pixel mode and low resolution multicolor mode 320×480. Both modes are palette direct.

Iron

The hardware is pretty simple: 3.3V regulator, CPLD, oscillator and SRAM. SRAM spends half of its time responding to the host and half of its time loading pixel data. The CPLD used here, the Xilinx 95144XL, is 5V stable, so it sits on the C64 expansion bus, although it is powered by a 3.3V regulator along with the rest of the hardware.

Almost all CPLD resources are used. I was hoping to fit one hardware sprite for the pointer, but there just wasn’t room for that.

For those who will print coolers, the STL model has everything you need, and in the C64 style.

The important point is that you need a JTAG programmer to load the bitstream into the CPLD.

And one more thing – the cartridge does not work with the Ultimate 64 board. Moreover, installing a cartridge on this board may damage the cartridge. But everything works with all versions of C64, C128 and C64 Reloaded boards. I don’t know for sure if the cartridge is compatible with all versions of C64 or C128 released by Commodore, but I don’t see any problems.

Specifications

  • 4-layer PCB. Gerber files included. The bevel on the edge adds significantly to the cost, so just sand it by hand (be sure to do this, otherwise you might damage the female contacts).
  • Cartridge housing top and bottom. STL files included.
  • Aluminum Polarized Capacitor, 22uF, 6.6mm
  • Snap action switch, such as pn 430156043726 if you need a reset button for your computer.
  • Connectors .1 ”
  • resistors 0603: 2 499R, 3 300R, 2 30R
  • capacitors 0603: 10 0.1 μF, 7 0.01 μF
  • 2 LEDs 3.2×1.6 (useful for debugging, but not required)
  • XC95144XL-5TQ100C CPLD (speed not important)
  • JEDEC 128kx8 SO Async SRAM a la AS6C1008-55PCN (not slower)
  • High density straight angled VGA with holes, DE15 female

Verilog

I used Xilinx ISE 14.5 as I could not find an open source toolkit for these CPLDs. If anyone has such information, please share.

Pixel packing

In high definition mode, each bit corresponds to one pixel. 1 = white, 0 = black. Addresses move from (0,0) at the top-left most visible position to the bottom-right (639 479), column by column, then line. Bit 7 in each byte is the first pixel.
In multicolor mode, pixels are output at the same rate as in monochrome mode, but each color channel has a different resolution. Green is 1/2 pixel rate and red and blue are 1/4 pixel rate. Matching a bit pattern to a color channel is byte-by-byte (fragmentary) and is:

G0 G1 G2 G3 R0 R1 B0 B1

Whereas the screen representation of each byte of the framebuffer looks like this:

R0 R0 R0 R0 R1 R1 R1 R1
G0 G0 G1 G1 G2 G2 G3 G3
B0 B0 B0 B0 B1 B1 B1 B1

Converting images for display using ImageMagick, monochrome:

convert input.tiff -resize 640×480 -colors 2 -depth 1 output.mono

Color Mode:

convert input.tiff + dither -posterize 2 -resize 640×480 output.tiff
convert output.tiff -separate channel% d.png

The code is written in Python – this option seemed to me the simplest:

from PIL import Image
from array import *
import numpy as np

ir = Image.open("channel0.png")
ig = Image.open("channel1.png")
ib = Image.open("channel2.png")

ir = ir.resize((640,480))
ig = ig.resize((640,480))
ib = ib.resize((640,480))

r = ir.load()
g = ig.load()
b = ib.load()

arr=np.zeros((480,80,8))
out=np.zeros((480,640))

for y in range(0,480):
        for x in range(0,80):

                # 0 1 2 3 is green level
                # 4 5 is red level
                # 6 7 is blue level

                # GREEN
        
                arr[y][x][0]=(g[x*8+0,y]+g[x*8+1,y])/2
                arr[y][x][1]=(g[x*8+2,y]+g[x*8+3,y])/2
                arr[y][x][2]=(g[x*8+4,y]+g[x*8+5,y])/2
                arr[y][x][3]=(g[x*8+6,y]+g[x*8+7,y])/2

                # RED

                arr[y][x][4]=(r[x*8+0,y]+r[x*8+1,y]+r[x*8+2,y]+r[x*8+3,y])/4
                arr[y][x][5]=(r[x*8+4,y]+r[x*8+5,y]+r[x*8+6,y]+r[x*8+7,y])/4

                #BLUE

                arr[y][x][6]=(b[x*8+0,y]+b[x*8+1,y]+b[x*8+2,y]+b[x*8+3,y])/4
                arr[y][x][7]=(b[x*8+4,y]+b[x*8+5,y]+b[x*8+6,y]+b[x*8+7,y])/4

for y in range(0,480):
        for x in range(0,80):
                for bit in range(0,8):

                        arr[y][x][bit] = int(round(round(arr[y][x][bit])/255))

newfile=open("output.bin","wb")

for y in range(0,480):
        for x in range(0,80):

                out[y][x] = int(arr[y][x][0] + arr[y][x][1]*2 + arr[y][x][2]*4 + arr[y][x][3]*8 
+ arr[y][x][4]*16 + arr[y][x][5]*32 + arr[y][x][6]*64 + arr[y][x][7]*128)

                newfile.write(out[y][x].astype(np.ubyte))

newfile.close()

Demo video:

We collect and solder:

From this link you can download all files necessary for work.

Similar Posts

Leave a Reply