I've been working on a VGA interface for my SBC and I've finally managed to make some progress, so I thought I'd share and ask for some advice. In this post, I wanted to talk a bit about design choices and theory of operation, and then I'll write a follow-up post with some questions around a couple bugs that are stumping me a little bit.
The schematic is attached below. For now, I've decided to forego an attempt at interleaving RAM access between the CPU and the pixel clock circuit, and instead relegate CPU memory access to the blanking interval. I thought this was going to be much worse than it's turned out to be, so for now it's “good enough.”
The interface's operation is pretty simple. Borrowing from George's approach, five four-bit counters (U1-U5) are fed a 25.175MHz clock and use a 64kB EEPROM to generate the signals needed to synchronize the system, including horizontal and vertical sync, and three different blanking signals (I'll explain this in a bit). The counters are also buffered (U6-U8) so that they can share access to the main memory (U16) with a PIC18, which acts as a GPU of sorts (the idea being that my SBC can talk to it through a 6522 and ask it to perform advanced commands like, say, drawing primitives and managing sprites). The PIC, in turn, controls four '574 octal latches (U12-U15) that it uses to access the RAM's address and data buses. When it wants to take control of the bus, it brings /VD_SEL low, which turns on the latches and turns off the counter buffers.
The output of the RAM chip feeds to another '574 (U10), which is latched to the falling edge of the 320px per line clock, and whose output enable is tied to the blanking signal coming out of the EEPROM; the '574 is then connected to a resistor ladder, which goes out to the VGA connector, giving 8bpp colour.
There are two additional blanking signals coming out of the EEPROM. The first, /MCU_BLANK, is shortened by a few pixels so that the PIC can always just check whether it is in a blanking interval and have enough time to complete a pixel write cycle. If I were to rely on the regular blanking interval, there would be no way of telling whether there is enough time to write a pixel before control needs to go back to the counter because a new row has begun, and thus the code would always have to wait for the beginning of a new blanking interval before accessing the memory, which is very wasteful. The other blanking interval is triggered only on vsync, with the idea that it can be tied to the interrupt line on the SBC to help sync graphics updates with the monitor's frame rate.
Here's what this circuit looks like on a breadboard. Behold the nest of rats! (Bonus PIC18 hanging at the bottom because I ran out of space.)
I'm sure this can be simplified a great deal, and I would love everyone's feedback on improving it, but it seems to work quite well overall; the picture is very stable, and there is only a minimal amount of jailbars that I'm not too worried about. I haven't really sorted out the SBC interface yet, but here's a video of the PIC driving it (apologies for the poor quality… it really does look pretty good in real life):
I'm not going to make a PCB out of this, because there are a couple of improvements that I still want to make. The first is to reduce the resolution to 320x240, which is more era-appropriate and cuts the amount of memory that needs to be pushed around in half. The second is making access from the PIC to the video memory's data bus bidirectional, so that the memory can be used to store useful data, like, say, sprite information. There should be enough pins left on the MCU to make this possible without adding another buffer, and it seems like a worthwhile addition.
A big bummer here is that I had to use an SMD part for the RAM… there just doesn't seem to be anything that's fast enough and comes in a DIP package…