1:Isle Display
Published 01 Aug 2025 (DRAFT)
The first hardware we build for our computer is a display controller. A display controller generates the low-level signals we need to drive a screen, be it a computer display or television.
If you're new to the project, read Isle FPGA Computer for an introduction.
Space and Time
A screen is a miniature universe with its own space and time.
Seen from afar, a screen shows a smooth two-dimensional image. Up close, it breaks up into many individual blocks of colour: red, green, and blue. We hide this complexity behind the abstract idea of a pixel: the smallest part of the image we can control. A typical screen has millions of pixels; even 640x480 has more than 300,000.
Screens create the illusion of movement by updating many times every second. At 60 Hz, a 1920x1080 HD television draws 124 million pixels every second! The need to quickly handle so much data is a big part of the challenge of working with graphics at a hardware level.
Display Interfaces
Display connectors and cabling vary, but VGA, DVI, HDMI, and DisplayPort have similar data designs. There are three channels for colour (red, green, and blue), horizontal, and vertical sync signals. There may also be audio and configuration data, but that's not important right now.
The red, green, and blue channels carry the colour of each pixel in turn. A screen begins a new line when it receives a horizontal sync and a new frame on a vertical sync. The sync signals are part of blanking intervals.
Blanking intervals allow time for the electron gun in cathode ray tubes (CRTs) to move to the following line (horizontal retrace) or the top of the screen (vertical retrace). Modern digital displays have retained the blanking intervals and repurposed them to transmit audio and other data.
Display Timings
A display mode is defined by its display timings. Standard timings are set by VESA and the CTA.
In this chapter, we'll use 640x480 at 60Hz, because almost all displays support it. However, Isle supports multiple display modes, and we'll introduce other modes in the next chapter.
Parameter | Horizontal | Vertical |
---|---|---|
Active Pixels | 640 | 480 |
Front Porch | 16 | 10 |
Sync Width | 96 | 2 |
Back Porch | 48 | 33 |
Total Blanking | 160 | 45 |
Total Pixels | 800 | 525 |
Sync Polarity | neg | neg |
The blanking interval has three parts: front porch, sync, and back porch. The front porch occurs before the sync signal, and the back porch after.
Including blanking, a 640x480 display has a total of 800 × 525 pixels.
The refresh rate is 60 Hz, so the total number of pixels per second is:
800 × 525 × 60 = 25,200,000
Therefore, we need a pixel clock of 25.2 MHz. The pixel clock is also known as the dot clock.
Driving a Display
Having selected our display timings, we're ready to create a video signal. There are four stages:
- Pixel Clock
- Display Sync Signals
- Painting Graphics
- Video Signal Output
Pixel Clock
We know we need a frequency of 25.2 MHz for 640x480, but how to reach it?
FPGAs include phase-locked loops (PLLs) to generate custom clock frequencies. Alas, there isn't a standard way to configure a PLL; we need a vendor-specific design. I've created clock generation modules for the Lattice ECP5 and Xilinx XC7 FPGAs found on our target dev boards. These modules generate two related clocks for TMDS encoding (discussed later):
- ECP5: arch/ecp5/clock2_gen.v
- XC7: arch/xc7/clock2_gen.v
I have a post covering ECP5 FPGA Clock Generation, if you want to learn more.
For other FPGA architectures, you'll need to consult your vendor documentation. If you can't reach 25.2 MHz exactly, 25 MHz or thereabouts should be fine.
Display Sync Signals
Next, we can generate sync signals from our pixel clock and display timings. We also want to get the current screen position to know when to paint things (dx,dy). We do both of these things with our display controller: gfx/display.v (ref doc). The main part of the Verilog module is shown below.
module display #(
parameter CORDW=0, // signed coordinate width (bits)
parameter MODE=0 // display mode (see above for supported modes)
) (
input wire clk_pix, // pixel clock
input wire rst_pix, // reset in pixel clock domain
output reg signed [CORDW-1:0] hres, // horizontal resolution (pixels)
output reg signed [CORDW-1:0] vres, // vertical resolution (lines)
output reg signed [CORDW-1:0] dx, // horizontal display position
output reg signed [CORDW-1:0] dy, // vertical display position
output reg hsync, // horizontal sync
output reg vsync, // vertical sync
output reg de, // data enable (low in blanking)
output reg frame_start, // high for one cycle at frame start
output reg line_start // high for one cycle at line start
);
reg signed [CORDW-1:0] x, y; // uncorrected display position (1 cycle early)
// timing registers
reg signed [CORDW-1:0] h_sta, hs_sta, hs_end, ha_sta, ha_end;
reg signed [CORDW-1:0] v_sta, vs_sta, vs_end, va_sta, va_end;
reg h_pol, v_pol; // sync polarity (0:neg, 1:pos)
// generate horizontal and vertical sync with correct polarity
always @(posedge clk_pix) begin
hsync <= h_pol ? (x >= hs_sta && x < hs_end) : ~(x >= hs_sta && x < hs_end);
vsync <= v_pol ? (y >= vs_sta && y < vs_end) : ~(y >= vs_sta && y < vs_end);
if (rst_pix) begin
hsync <= h_pol ? 0 : 1;
vsync <= v_pol ? 0 : 1;
end
end
// control signals
always @(posedge clk_pix) begin
de <= (y >= va_sta && x >= ha_sta);
frame_start <= (y == v_sta && x == h_sta);
line_start <= (x == h_sta);
if (rst_pix) begin
de <= 0;
frame_start <= 0;
line_start <= 0;
end
end
// calculate horizontal and vertical display position
always @(posedge clk_pix) begin
if (x == ha_end) begin // last pixel on line?
x <= h_sta;
y <= (y == va_end) ? v_sta : y + 1; // last line on display?
end else begin
x <= x + 1;
end
if (rst_pix) begin
x <= h_sta;
y <= v_sta;
end
end
// delay display position to match sync and control signals
always @(posedge clk_pix) begin
dx <= x;
dy <= y;
if (rst_pix) begin
dx <= h_sta;
dy <= v_sta;
end
end
// display timings
always @(posedge clk_pix) begin
case (MODE)
default: begin // 640 x 480 - default
hres <= 640; // horizontal resolution
vres <= 480; // vertical resolution
h_pol <= 0; // horizontal sync polarity (0:neg, 1:pos)
h_sta <= -160; // horizontal start (horizontal blanking)
hs_sta <= -144; // sync start (after front porch)
hs_end <= -48; // sync end
ha_sta <= 0; // active start
ha_end <= 639; // active end
v_pol <= 0; // vertical sync polarity (0:neg, 1:pos)
v_sta <= -45; // vertical start (vertical blanking)
vs_sta <= -35; // sync start (after front porch)
vs_end <= -33; // sync end
va_sta <= 0; // active start
va_end <= 479; // active end
end
// ... other modes hidden for brevity
endcase
end
endmodule
Coordinates
Display coordinates are signed 16-bit values; an (x,y) pair fitting into a 32-bit word. The top-left visible pixel is at (0,0) with the Y-coordinate increasing down the screen. Blanking occurs at negative coordinates, so we have time to prepare at the start of a line or frame. This can be hard to understand in the abstract, but it will become clear when we start painting graphics.

Painting Graphics
Without any memory to store bitmap graphics (yet), we're racing the beam, generating the colour of each pixel when the display needs it. Perhaps the simplest thing we can do is define a square using the coordinates from the display controller:
// define a square with display coordinates
reg square;
always @(*) begin
square = (dx > 220 && dx < 420) && (dy > 140 && dy < 340);
end
// paint colour: white inside square, blue outside
reg [BPC-1:0] paint_r, paint_g, paint_b;
always @(*) begin
paint_r = (square) ? 'h1F : 'h02;
paint_g = (square) ? 'h1F : 'h06;
paint_b = (square) ? 'h1F : 'h0E;
end
To understand the paint colours, you need to know that Isle uses 15-bit colour (RGB555). Each colour has 5 bits and a range of 0-31, or 0x0-0x1F in hex. See Isle display modes for more detail on 15-bit colour.
The following screen capture shows our square design running in Verilator/SDL simulation.

Video Signal Output
To get our graphics onto the screen, we need to encode them into a suitable format and output them from the FPGA. Isle generates DVI signals that are upwardly compatible with HDMI. DVI and HDMI use transition-minimized differential signaling (TMDS) to encode each 8-bit colour channel as a robust 10-bit value for transmission as a high-frequency serial signal.
I am writing a separate post that goes into the details of DVI and TMDS, and for analogue fans, I'm writing a guide to generating VGA signals. But for now, I'll summarise the DVI modules:
- Common
- gfx/tmds_encoder - TMDS Encoder (DVI)
- ECP5
- arch/ecp5/dvi_generator - DVI output with tmds_encoder and ODDRX1F
- XC7
- arch/xc7/dvi_generator - DVI output with tmds_encoder and oserdes_10b
- arch/xc7/oserdes_10b - 10:1 Output Serializer with OSERDESE2
- arch/xc7/tmds_out - TMDS Signal Output with OBUFDS
The Verilator/SDL simulation doesn't use DVI; it writes the display signal to a texture for rendering.
Top Level
Bringing our designs together, we have our first root Isle module book/ch01/ch01_square.v:
module ch01_square #(
parameter BPC=5, // bits per colour channel
parameter CORDW=16, // signed coordinate width (bits)
parameter DISPLAY_MODE=0 // display mode (see display.v for modes)
) (
input wire clk, // system clock
input wire rst, // reset
output reg signed [CORDW-1:0] disp_x, // horizontal display position
output reg signed [CORDW-1:0] disp_y, // vertical display position
output reg disp_hsync, // horizontal display sync
output reg disp_vsync, // vertical display sync
output reg disp_de, // display data enable
output reg disp_frame, // high for one cycle at frame start
output reg [BPC-1:0] disp_r, // red display channel
output reg [BPC-1:0] disp_g, // green display channel
output reg [BPC-1:0] disp_b // blue display channel
);
//
// Display Controller
//
wire signed [CORDW-1:0] dx, dy;
wire hsync, vsync, de;
wire frame_start;
display #(
.CORDW(CORDW),
.MODE(DISPLAY_MODE)
) display_inst (
.clk_pix(clk),
.rst_pix(rst),
.hres(),
.vres(),
.dx(dx),
.dy(dy),
.hsync(hsync),
.vsync(vsync),
.de(de),
.frame_start(frame_start),
.line_start()
);
//
// Painting
//
// define a square with display coordinates
reg square;
always @(*) begin
square = (dx > 220 && dx < 420) && (dy > 140 && dy < 340);
end
// paint colour: white inside square, blue outside
reg [BPC-1:0] paint_r, paint_g, paint_b;
always @(*) begin
paint_r = (square) ? 'h1F : 'h02;
paint_g = (square) ? 'h1F : 'h06;
paint_b = (square) ? 'h1F : 'h0E;
end
//
// Display Output
//
// register display signals
always @(posedge clk) begin
disp_x <= dx;
disp_y <= dy;
disp_hsync <= hsync;
disp_vsync <= vsync;
disp_de <= de;
disp_frame <= frame_start;
disp_r <= (de) ? paint_r : 'h00; // paint colour but black in blanking
disp_g <= (de) ? paint_g : 'h00;
disp_b <= (de) ? paint_b : 'h00;
end
endmodule
This module creates an instance of the display controller, draws a square, and outputs the display signals.
But this isn't the top module and doesn't do the DVI signal generation. Instead, each board has its own top module with architecture and board-specific configuration. Keeping architecture-specific designs to the top module reduces code duplication and makes supporting multiple dev boards manageable.
- Lakritz: boards/lakritz/top_ch01.v
- Nexys Video: boards/nexys_video/ch01/top_ch01.v
- ULX3S: boards/ulx3s/top_ch01.v
- Verilator: boards/verilator/top_ch01.v
You'll find clock, DVI generation and board display signal handling in these short top modules.
Build & Program
Let's build this first, very simple iteration on Isle.
I've included a Makefile, constraints, and build instructions for each board:
You can open an issue if you have problems building Isle or spot a mistake on the blog. However, bear in mind I'm a team of one working in my spare time. Your board's forum or Discord channel is usually the best place to get help.
Try the hitomezashi (一目刺し) stitch pattern, book/ch01/ch01_pattern.v, by replacing the ch01_square instance in your board's top. You can find more ideas for Racing the Beam from my previous FPGA Graphics series.
In the next chapter (coming soon), we'll be adding vram and talking bitmap graphics.

Next step: Bitmap Graphics (forthcoming) or Isle Index
You can sponsor me to support Isle development and get early access to new chapters and designs.