Wow. A lot of time has passed since the last post. I have been busy though. I have built the bones of an ARM-based game console. I'll describe how I got to this point (a pointless spinning cube) over the last one and half years of sporadic effort.
A spinning cube...
STM32F4 Discovery Board
Firstly, though here are my influences. You should certainly check out these amazing projects - they might inspire you to go down this crazy path.
Lazarus64: http://lucidscience.com/pro-lazarus-64%20prototype-1.aspx
Uzebox: http://belogic.com/uzebox/
Bitbox: http://bitboxconsole.blogspot.com.au/
Maximite: http://geoffg.net/maximite.html
A Quick History
Take 1: The AVR-based Approach
Originally I wanted to build a Z80 based computer with nice 8-bit AVR based GPU - a software GPU. The idea for the GPU came after reading about the Lazarus64 project. Brad (from LucidScience) managed to breadboard an ATMEGA324P, 2 SRAMs, switching logic and delay lines for NTSC colour generation. It had the nice feature that it could switch which SRAM was connected to the CPU to achieve a hardware based double-buffered frame buffer.I started thinking how it would be neat to use a VGA output since VGA is simple to generate. I wanted to have a widescreen 16:9 resolution so that my pixels would be square on any modern TV. I settled on 480x270 which is nearly 16:9 and fits in a 128K SRAM.
I progressed to a reasonably advanced state with the circuit and PCB layout. My design had two AVRs each with their own SRAM. They would simply take turns rendering. Another goal was to prototype at home and this meant I wanted a single-side board that I could make on my Zen Toolworks CNC router. This constraint basically killed this design. Well, in theory the design was sound but in practise it killed my patience due to the complexity involved in making changes.
Take 2: The AVR+FPGA-based Approach
I thought it might be easier to place a small FPGA as the centre of the design as hub for the SRAM, AVR and VGA port. I bought this:
Altera Cyclone II
I still think this was a good move. I could plug in pieces of my computer in chunks as I built them. I set about making an SRAM adapter board on my CNC that would plug into the board above. This contained the finest pitch routing I have ever attempted on my CNC and necessitated the use of a probe to correct for slight deviations in height in the PCB blank. Here is the result:
For those interested, I used the following probing software with my Eagle, PCBGcode and LinuxCNC setup: AutoLeveller. It is a terrific piece of software.
The board above took a 10ns ISSI 512KB SRAM in a TSOP-44 package. I think the board turned out really well. I soldered it up, plugged it in and toasted both my SRAM and a couple of pins on my FPGA. That is what happens when you have a solder bridge underneath the SRAM. I should have checked it I know. I didn't. Anyway I ordered another FPGA board.
So, it was time time to write some VHDL code. Since I had an FPGA, I thought I might as well move the VGA signal generation to it. I had not written VHDL since university some 15 years ago. Once I remembered that I was not supposed to be writing code but rather describing hardware, things went more smoothly. Fortunately I found some VGA generation VHDL that I modified and plugged in appropriate 50MHz clock for a nice 800x600 mode:
LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY vga_controller IS
GENERIC(
h_pulse : INTEGER := 120; --horizontal sync pulse width in pixels
h_bp : INTEGER := 64; --horizontal back porch width in pixels
h_pixels : INTEGER := 800; --horizontal display width in pixels
h_fp : INTEGER := 56; --horizontal front porch width in pixels
h_pol : STD_LOGIC := '1'; --horizontal sync pulse polarity (1 = positive, 0 = negative)
v_pulse : INTEGER := 6; --vertical sync pulse width in rows
v_bp : INTEGER := 23; --vertical back porch width in rows
v_pixels : INTEGER := 600; --vertical display width in rows
v_fp : INTEGER := 37; --vertical front porch width in rows
v_pol : STD_LOGIC := '1'); --vertical sync pulse polarity (1 = positive, 0 = negative)
PORT(
pixel_clk : IN STD_LOGIC; --pixel clock at frequency of VGA mode being used
reset_n : IN STD_LOGIC; --active low asynchronous reset
h_sync : OUT STD_LOGIC; --horizontal sync pulse
v_sync : OUT STD_LOGIC; --vertical sync pulse
disp_ena : OUT STD_LOGIC; --display enable ('1' = display time, '0' = blanking time)
column : OUT INTEGER; --horizontal pixel coordinate
row : OUT INTEGER; --vertical pixel coordinate
n_blank : OUT STD_LOGIC; --direct blacking output to DAC
n_sync : OUT STD_LOGIC); --sync-on-green output to DAC
END vga_controller;
ARCHITECTURE behavior OF vga_controller IS
CONSTANT h_period : INTEGER := h_pulse + h_bp + h_pixels + h_fp; --total number of pixel clocks in a row
CONSTANT v_period : INTEGER := v_pulse + v_bp + v_pixels + v_fp; --total number of rows in column
BEGIN
n_blank <= '1'; --no direct blanking
n_sync <= '0'; --no sync on green
PROCESS(pixel_clk, reset_n)
VARIABLE h_count : INTEGER RANGE 0 TO h_period - 1 := 0; --horizontal counter (counts the columns)
VARIABLE v_count : INTEGER RANGE 0 TO v_period - 1 := 0; --vertical counter (counts the rows)
BEGIN
IF(reset_n = '0') THEN --reset asserted
h_count := 0; --reset horizontal counter
v_count := 0; --reset vertical counter
h_sync <= NOT h_pol; --deassert horizontal sync
v_sync <= NOT v_pol; --deassert vertical sync
disp_ena <= '0'; --disable display
column <= 0; --reset column pixel coordinate
row <= 0; --reset row pixel coordinate
ELSIF(pixel_clk'EVENT AND pixel_clk = '1') THEN
--counters
IF(h_count < h_period - 1) THEN --horizontal counter (pixels)
h_count := h_count + 1;
ELSE
h_count := 0;
IF(v_count < v_period - 1) THEN --veritcal counter (rows)
v_count := v_count + 1;
ELSE
v_count := 0;
END IF;
END IF;
--horizontal sync signal
IF(h_count < h_pixels + h_fp OR h_count > h_pixels + h_fp + h_pulse) THEN
h_sync <= NOT h_pol; --deassert horizontal sync pulse
ELSE
h_sync <= h_pol; --assert horizontal sync pulse
END IF;
--vertical sync signal
IF(v_count < v_pixels + v_fp OR v_count > v_pixels + v_fp + v_pulse) THEN
v_sync <= NOT v_pol; --deassert vertical sync pulse
ELSE
v_sync <= v_pol; --assert vertical sync pulse
END IF;
--set pixel coordinates
IF(h_count < h_pixels) THEN --horizontal display time
column <= h_count; --set horizontal pixel coordinate
END IF;
IF(v_count < v_pixels) THEN --vertical display time
row <= v_count; --set vertical pixel coordinate
END IF;
--set display enable output
IF(h_count < h_pixels AND v_count < v_pixels) THEN --display time
disp_ena <= '1'; --enable display
ELSE --blanking time
disp_ena <= '0'; --disable display
END IF;
END IF;
END PROCESS;
END behavior;
This produced a a stable 800x600 image which I took the worst photo in world of below:
Anyway, it was time for the wheels to fall off this idea too. So, how did this one fail? I changed the timings to generate my desired 480x270 mode and plugged the contraption into the VGA port on my TV. The result: NOTHING.
So it turns out that TVs are far more pickier about modes that PC LCD monitors. Both my Sony LCD and Pioneer Plasma will accept 640x480, 800x600, 1920x1080 and other popular modes. 480x270 didn't work on either. I decided at this point to have bit of rest.
Take 3: The ATMega328 and SVideo/Composite-based Approach
About mid-2014 I started becoming interested in the Uzebox and started wondering if I could do something similar. The Uzebox is game console. It was around this time that I started becoming interested in retro gaming and the Uzebox is all about retro gaming. It still amazes me that Minecraft (well Mojang) was sold for $2.5 billion to Microsoft . Minecraft had pixelated graphics by design. Pixel art can be very compelling clearly and I think that same spirit is found within the amazing Uzebox community. Go and check out the Uzebox Forum - amazing stuff going on in there.So my requirements were beginning to change at this point to something like the following:
- Must be largely a single-chip design except for the TV encoder
- Must support SNES game pads which are available cheaply on eBay
- Must support 256 colours
- Must support a 16:9 resolution
- Must have sufficient resolution to be fun on a 42-inch screen but not too high that it isn't fun to make graphics for.
- Must support audio output of some form.
- Must support the ability to execute from RAM.
The Uzebox uses a Analog Device AD725 PAL/NTSC encoder chip to produce a 4:3 video signal from the AVR. I wanted to do a 16:9 widescreen mode but I remembered that the first DVD players (pre-HDMI) supported widescreen modes over composite video. I dug into it the timings more and then realised that because PAL is analog, I can push out as many pixels as I like per line to achieve a widescreen mode. Well, you are limited by the bandwidth of the AD724 and the TV's decoder though. In practise I think this is around 4-5 MHz for PAL so my mode is achievable. Essentially a 8-10MHz pixel clock is the upper limit. In another post I might elaborate on the why the pixel clock can be double but take my word for now.
I started to look at other modes that might fit nicely in a PAL timing window and found that 416x234 required a nice 8MHz pixel clock that is exactly half of an Arduino clock frequency. I had an Arduino sitting around so I though I might hook them up. Unfortunately I couldn't work out how to achieve 8MHz with an external SRAM. I felt the Arduino's 2K RAM wasn't enough to make the kinds of games I wanted to make. Unfortunately there isn't enough time to address an SRAM and read data from it during scan out.
So then after reading about the PIC32-based Maximite and always having an interest in MIPS since learning about it at university I bought a PIC32 dev board called a UBW32. I mad a small board for the AD724 (like the AD725) and ended up with this monstrosity:
Note that the AD724 is on the backside as it an SMD device
More to follow in another post...