Implementation of a VGA Graphics Card on FPGA Following Ben Eater's Principles
Developers familiar with HDL will appreciate the shift from simulations in Turing Complete to real hardware implementation. Drawing on Ben Eater's ideas from his video courses on discrete logic, this project builds a simple graphics card on an FPGA. It uses the Altera EP4CE15F23C8N chip with 15,000 logic elements, connected to VGA via standard 800x600@60Hz timings. The 50 MHz system clock is divided by 5 to produce 10 MHz, matching the original project.
The ClockDivider module handles the clock division:
module ClockDivider
(
input wire clk,
output reg clk_out
);
reg [2:0] DATA;
always @(posedge clk)
begin
if(DATA == 3'd4)
begin
DATA <= 3'd0;
clk_out <= 1'd1;
end
else
begin
clk_out <= 1'd0;
DATA <= DATA + 1'd1;
end
end
endmodule
Verilog Features for FPGA Development
Verilog stands out from imperative languages due to its parallel execution. Variables fall into two categories: reg (which hold state) and wire (simple wires without storage). Assignment operators produce different hardware circuits:
- assign for combinational logic:
wire a,b,c;
assign c = a & b;
- always @(...) = (blocking) for sequential execution within a block.
- always @(posedge clk) <= (non-blocking) for clocked registers:
reg A;
always @(posedge clk)
begin
A <= ~A;
end
Module parameters are defined using #() with default values and passed by name during instantiation. The `define directive is discouraged due to preprocessor issues.
Counters for VGA Timings
The horizontal counter tracks pixels in a line:
module HorizontalCounter
#(
parameter ENDSTRING = 0
)
(
input wire clk,
output reg [8:0] DATA
);
always @(posedge clk )
begin
if(DATA == ENDSTRING) DATA <= 9'd0;
else DATA <= DATA + 1'd1;
end
endmodule
The vertical counter increments at the end of each line:
module VerticalCounter
#(
parameter ENDFRAME = 0,
parameter ENDSTRING = 0
)
(
input wire clk,
input reg [8:0] Hor_count,
output reg [10:0] DATA
);
always @(posedge clk )
begin
if(DATA == ENDFRAME) DATA <= 11'd0;
else if (Hor_count == ENDSTRING) DATA <= DATA + 1'd1;
end
endmodu
Signals like Hsync, Vsync, Hblank, and Vblank are asserted based on timings from tinyvga.com (front and back porch values are specified in case statements for H_count and V_count).
Generating Video Data
The visible area has a resolution of 100x75 pixels with 2-bit color depth (4 levels per RGB channel, packed into 6 bits). Images are prepared in Photoshop: resize to 100x75, convert to Indexed Color with a custom palette (0x00/0x55/0xaa/0xff), and export as 8-bit BMP.
A C++ utility converts the BMP into a Verilog memory array mem[]:
#include <windows.h>
#include <iostream>
// ... (full code parsera BMP with SetPixCollor for mappinga palitry in 6-bitnye values)
The SetPixCollor function encodes RGB values into bits (R2R1 G2G1 B2B1). The output mem_block.txt is loaded into an FPGA ROM, addressed using Hor_count[6:0] and V_count[6:0].
- Load BMP and validate headers (bfType=0x4D42, biBitCount=8).
- Read the RGBQUAD[256] palette.
- Parse pixels and map them to 6-bit values.
- Generate Verilog code along with an ASCII visualization.
Integration and Debugging
Assembly is done in Quartus Prime: modules are instantiated with parameters like ENDSTRING=799 and ENDFRAME=624. USB Blaster needs custom firmware for reliable operation. The Quartus simulator stands in for traditional debuggers, showing oscillograms of signals like clk_out, H_count, and RGB.
Key points:
- Verilog's parallelism calls for non-blocking assignments when working with registers.
- VGA timings are critical: 10 MHz pixel clock for 800x600@60Hz.
- 2-bit color keeps the ROM simple (7500x6 bits) while maintaining gradients.
- FPGA setup is economical: a minimal board with no extras + standard VGA cables.
- Debugging happens entirely in simulation, avoiding runtime issues.
— Editorial Team
No comments yet.