The GameEngine is a project I've been working on intermittently for the last five or so years. It's an excuse to learn FPGA programming, a fun exercise in making my own fast Basic interpreter, and a demonstration of how you can build a flexible GPU system with relatively low-end components. Functionally, it's a modern retrocomputer, a machine inspired by the 8-bit computers that I grew up with in the 80s, but built with modern components. The current version uses USB peripherals, stores files on a SD card, and outputs HDMI video, but acts like a text interface boot-to-BASIC computer similar to classic machines from Commodore, Texas Instruments, or Apple.
This project’s inspiration came from multiple sources. It was partly inspired by the Commander X16 project, which got me thinking about how I would build the kind of classic computer I would have liked to have while growing up. It was also inspired by looking at many other homebrew 8-bit computers, and noticing that they were at best producing VGA video output. HDMI output was apparently too difficult to do in a homebrew system, at least while still using discrete logic. This started me wondering what the minimal hardware system that could generate HDMI video would be. I figured that a FPGA would be required, but that was fine with me as I was looking to get more FPGA programming experience anyway.
My initial idea was to develop a custom computer system, a board containing a 6502 CPU, RAM and ROM, and a FPGA on a dedicated daughterboard plugged into a standard DIP socket. The FPGA would generate video and sound output, and would also take over much of the glue logic functions, generating clock and reset, address decoding for the RAM and ROM, possibly keyboard handling and maybe even smart interrupt and DMA functions. But first I needed to demonstrate the basic video function with a devkit board.
The really low-end FPGAs from Lattice and Microchip were too small to do what I wanted, while the powerful high-end ones from companies like AMD were expensive and at the time had very long lead times. I decided on the mid-range FPGAs from Efinix, as they were relatively inexpensive yet had enough LUTs and on-chip RAM to do what I needed. For this project I chose a development board based around the Efinix T20F256C3.
The T20F256C3 does not have a built-in HDMI driver, but it does have multiple LVDS output channels. LVDS signals don’t quite match the required voltage levels of the HDMI data channels, but a simple RC network was enough to get them close enough to work. I built up an adaptor board to plug into the devkit and output close-enough HDMI video signals.
The circuit built up on breadboard, plugged into the FPGA devboard.
The ideal thing to do would be to use an actual HDMI signal driver chip to get the proper voltage levels and drive strengths, but I didn't want to bother with having to set up an entire breakout board for that. The simple RC network seems to work well enough, even if the signal levels here only marginally meet the HDMI signal specifications.
My first goal was to see if I could simply display a static color bar image. HDMI digital video output consists of four differential channels: red, green, blue, and clock. The color channels are transmitted as ten bits per pixel data signals. The fourth channel is simply a clock signal which cycles once per pixel. Horizontal and vertical sync are added to the blue channel, and additionally audio data can be included during the sync period on the red and green channels.
I was aiming for a 640x480 pixel image, the baseline screen image that all HDMI monitors should accept. With horizontal and vertical sync and blanking periods, each screen consists of 800x525 pixels, or 420,000 pixel clocks per frame. A 60hz update rate would require a 25.2mhz pixel clock. Unfortunately the FPGA I was using couldn’t easily generate this exact clock rate from the clock signal on the devkit, but it could easily make 25mhz, which would give me a frame rate of 59.524hz. This seems to be close enough for the monitor to recognize.
The data on the red, green, and blue channels is TMDS encoded, which converts the data from 8 bits of raw color information to a 10 bit signal encoded to balance voltage levels and minimize transitions. The blue channel also has some additional encoding for indicating horizontal and vertical sync signals. For this part I simply grabbed some open source TMDS encoder code and pasted it into the project, rather than bothering to write my own from scratch. Thanks to Scott Larson for creating and publishing that code for me to use.
I wrote up a simple timing block to count through the horizontal and vertical pixels, and generate the blanking and sync signals as needed. A simple case statement based on the horizontal position was used to generate color signals to the encoders.
Color bars. I had the red and blue channels swapped here, but that was an easy fix.
At this point, I had a valid video output from the FPGA, with surprisingly little code. Next step was to see if I could make it display a static bitmap image.
The video output at this point was a 640x480 pixel, 24 bit color image. Storing that image at full resolution and color depth would require over 900KB of RAM. The Efinix T20F256C3 FPGA only has about 100KB of block memory, so I would have to make some sacrifices to get a raw bitmap image to fit.
First thing I did was halve the resolution. Doubling up each pixel horizontally and vertically meant I only had to store 320x240 pixels in RAM. This was about the graphics resolution I was aiming for with the final design anyway.
The second step was to switch to 8 bit indexed color. I created a palette lookup table, three 256x8 bit memory banks holding the red, green, and blue values of each indexed color. Instead of holding raw color values, each byte in the bitmap image would have the index of a color to use from the palette RAMs.
256 color demonstration, using the standard ANSI color table.
I created a quick Python script to take a raw image, find the closest color in the palette, and then output a raw memory file. I even added a feature to dither between two colors when the raw color in the image was roughly between two choices.
With the reduced resolution and indexed color, the memory required for the image was reduced to only 75K, which would fit in the block ram of the FPGA. I created a single block RAM to hold the image, with its contents initialized from the memory file created previously, and replaced my simple color bar generator with a lookup function to fetch the required pixel from the block RAM. This worked, and I now had a simple FPGA code to display a static image over HDMI.
No comments:
Post a Comment