The next step in getting Veronica’s VGA board up and running is setting up a way to share the contents of VRAM between the CPU and the GPU (yes, I’m calling it a GPU because it is convenient- complain all you want, local pedants). I considered a lot of different ways to do this, mostly involving elaborate setups of TTL logic and such to control access to VRAM. In the end, what I’m going to try is actually leveraging an advantage I have by using the AVR the way I am. I’m going to solve the problem with a mix of hardware and software. Doing as much as possible on the software side reduces the chip count substantially.
All along, my intent has been to make the graphics board less like an Apple //, and more like a Nintendo. The graphics board will be doing all the heavy lifting of rendering pixels, text, sprites, etc. The CPU will only be uploading data as part of a setup process, then issuing small commands periodically to move elements, change properties, etc. This is very much the model of modern GPUs, whereby the CPU mostly just issues 3D API calls to apply transforms and such to geometry. Rarely, the CPU uploads texture or geometry data. Data almost never goes down the other way, and when this is permitted, the whole pipeline is stalled.
Perhaps it’s my background in 3D game engine design, but this approach makes a lot of sense to me, and is what I’ll be using, even though Veronica will only be rendering very simple 2D elements. It allows me to make the connection to the video board very simple, and relieves the CPU of 90% of the load of keeping up the graphics. It’s unlikely a 6502 could do a decent job of keeping up a VGA display on its own. The displays of the 1Mhz era were lower resolution and had very low bit-per-pixel counts for a reason. Well, it was also to save frame-buffer memory, which was very expensive. Today we have the luxury of cheap memory, though ironically I’m paying more than market value in order to get DIPs, which only hobbyists use anymore.
Alright, down to business. My basic approach is to have a command-buffer area using the upper 3k of VRAM that is currently unused. I have 64k, and my frame buffer is only 61440 bytes. The system will map that upper 3K into the same area of the 6502’s memory, which is conveniently also mostly available (save a few bytes for reset vectors and such). So, when an address is requested in the VRAM command buffer range, it will trigger the VGA board to write those bytes into VRAM instead of system ram. The VGA board will then respond to the new contents of the command buffer, as needed. In this way, there will be a fairly wide communication pipeline between what are essentially completely isolated subsystems.
I’m getting this running in small steps. First, the system needs a way to know the CPU wants access to VRAM. For that, I’m going to use an external interrupt on the AVR. This interrupt will be triggered when an address in the command buffer range is detected on the address bus. I haven’t used external interrupts on the AVR before, so the first step is getting that working. I wired up a button that would signal the interrupt, and wrote a handler to light up my debug LED on the VGA dev board:
The code to set up this external interrupt is very simple. As usual (for me), this is avr-gcc code.
Note that only specific input pins can be used for this. Fortunately, one of the options is pin 2 on port B, which I happened to have available. Kismet!
With that small victory in hand, it’s time to get ready to share the VRAM bus. For that, I’ll need physical access to it. I opted to add some headers to the dev board to allow me to access the bus. I didn’t plan ahead very well at all with this dev board, so there was no convenient place to hook in these headers. I had to patch in with a mess of ribbon cable. Cue facepalm.
A quick test after adding those headers revealed an amusing problem.
It looks as though we lost half our horizontal resolution. I can think of two causes for this- either the pixel clock slowed down (highly unlikely), or a couple of address lines are shorted (embarrassingly likely). Even though I inspected all my new connections for shorts, the evidence suggests they need checking again. Sure enough the lowest two address lines had a microscopic solder bridge between them that I missed with my earlier inspection. A quick swipe with the iron, and Veronica is back to 256 x 240.
Another new problem appeared that is less amusing. The noise is back. The new headers are acting like little radio antennas on my bus, and the monitor is now having intermittent problems locking to the signal. Perhaps more bypass/decoupling caps on the board would help, and I should add some in the final design. In the meantime however, I rigged up an EMI shield by covering both sides of some aluminum foil with clear tape, and covering the board with it. Sounds nuts, but it worked like a dream. All noise issues were eliminated instantly. If I remove the foil, the noise comes back. It’s completely reproducible. I’m actually kind of shocked this worked.
Next I want to set up the interface between the VRAM bus and Veronica’s system bus. That will be a set of 74HC541 buffers which are controlled by the AVR. The algorithm is:
Because the AVR is running at twenty times the clock speed of the 6502, it can execute this process so fast that the CPU won’t even notice. The interrupt handler is less than 20 clocks long, so this entire process happens within the space of a 6502 clock pulse.
Importantly, the external interrupts are higher priority than the timer interrupts on the AVR. The latter is bit-banging the VGA signal, so we need to be careful about when that is interrupted. I modified the VGA generator code to disable external interrupts while pixels and sync pulses are being generated. If a CPU request occurs during this time, the interrupt flag will be set in the AVR, and it will be handled as soon as external interrupts are re-enabled. This may cause issues on the CPU side, as the system currently assumes the CPU’s write requests will be handled instantly. I may need to latch the CPU data with a hardware register to hold it until the interrupt can be handled, or perhaps provide the CPU with a signal to know when it’s okay to write to VRAM. We’ll see how this goes.
To test the process, I set up the buffers on a breadboard.
Next, I patched those in to the dev board, then hooked up my hex input device to simulate the CPU trying to drive the bus.
Finally, I needed the AVR code to handle the interrupts. Here’s the relevant part. The “cpuBusAllow” signal connects to the /OE lines on the ‘541 buffers. The “vramOE” signal connects to the /OE pin on the VRAM. The “vramWrite” signal controls the /W pin on the VRAM.
So, with this set up, and my Aluminum Foil EMI Shield Of Awesomeness in place, I should actually be able to write to VRAM using the hex switches and the push-button (which is still configured to simulate the CPU address decode which triggers the interrupt).
Here’s video of me walking through VRAM and firing the interrupt. The data bus is being driven with the value for a white pixel. If you look closely at the lower part of the ‘V’ on the display, you can see a white line being formed as I push bytes into VRAM with the interrupt. You may need to switch the video to high quality in order to see the white line.
How about that- I’ve made the world’s most tedious paint program.
That’s a good start, and I’m confident this approach is going to work. Stay tuned to see how I get along with the rest of this interface.
Housekeeping Note: I’ve been using pastebin to share my code samples on this site, because it’s a nice easy way to format them, and makes it easy for people to copy the code to their own machines. As of this writing, pastebin seems to be under DDoS attack, which I’ve learned happens periodically. They seem to have some enemies. For this post, I’ve switched to gist on github. If you have trouble reading the old code samples, let me know and I’ll switch them to gist as well. If gist works well for everyone, I’ll continue to use it from now on.