Castle Drawing
In this FPGA demo, we use multiple shapes (rectangles, triangles, circles) to render a simple picture of a castle. We don’t use any software or CPU, just shape rasterization and finite state machines. This Verilog design runs on the Digilent Arty A7 or as a Verilator/SDL simulation on your computer.
Building the Demo
Find the Verilog source and build instructions in the projf-explore git repo:
https://github.com/projf/projf-explore/tree/main/demos/castle-drawing
To learn about hardware drawing, check out my tutorial on Lines and Triangles and 2D Shapes.
Demo Structure
- Top module with display interfaces and background drawing
- Arty (XC7): xc7/top_castle.sv
- Verilator/SDL: sim/top_castle.sv
- Castle render module: render_castle.sv (see below)
- Drawing routines from Project F library
Castle Rendering
The castle render module is a mix of rectangles, triangles and circles drawn with a finite state machine. The background sky and grass are not drawn into the framebuffer but are rendered directly based on the screen position (see next section).
module render_castle #(
parameter CORDW=16, // signed coordinate width (bits)
parameter CIDXW=4, // colour index width (bits)
parameter SCALE=1 // drawing scale: 1=320x180, 2=640x360, 4=1280x720
) (
input wire logic clk, // clock
input wire logic rst, // reset
input wire logic oe, // output enable
input wire logic start, // start drawing
output logic signed [CORDW-1:0] x, // horizontal draw position
output logic signed [CORDW-1:0] y, // vertical draw position
output logic [CIDXW-1:0] cidx, // pixel colour
output logic drawing, // actively drawing
output logic done // drawing is complete (high for one tick)
);
localparam RBOW_CNT=8; // number of shapes in rainbow
localparam SHAPE_CNT=19; // number of shapes in castle
logic [$clog2(RBOW_CNT)-1:0] rbow_id; // rainbow shape identifier
logic [$clog2(SHAPE_CNT)-1:0] shape_id; // castle shape identifier
logic signed [CORDW-1:0] vx0, vy0, vx1, vy1, vx2, vy2; // shape coords
logic signed [CORDW-1:0] vr0; // circle radius
logic signed [CORDW-1:0] x_tri, y_tri; // triangle framebuffer coords
logic signed [CORDW-1:0] x_rect, y_rect; // rectangle framebuffer coords
logic signed [CORDW-1:0] x_circle, y_circle; // circle framebuffer coords
logic draw_done; // combined done signal
logic draw_start_tri, drawing_tri, draw_done_tri; // drawing triangle
logic draw_start_rect, drawing_rect, draw_done_rect; // drawing rectangle
logic draw_start_circle, drawing_circle, draw_done_circle; // drawing circle
// draw state machine
enum {IDLE, INIT_RBOW, DRAW_RBOW, INIT_CASTLE, DRAW_CASTLE, DONE} state;
always_ff @(posedge clk) begin
case (state)
INIT_RBOW: begin
state <= DRAW_RBOW;
draw_start_circle <= 1;
vx0 <= 368;
vy0 <= 160;
vr0 <= 180 - 5 * rbow_id;
cidx <= (rbow_id == RBOW_CNT-1) ? 'h0 : 'h2 + rbow_id;
end
DRAW_RBOW: begin
draw_start_circle <= 0;
if (draw_done) begin
if (rbow_id == RBOW_CNT-1) begin
state <= INIT_CASTLE;
end else begin
rbow_id <= rbow_id + 1;
state <= INIT_RBOW;
end
end
end
INIT_CASTLE: begin // register coordinates and colour (Sweetie 16 palette)
state <= DRAW_CASTLE;
case (shape_id)
'd0: begin // main building
draw_start_rect <= 1;
vx0 <= 60; vy0 <= 75;
vx1 <= 190; vy1 <= 130;
cidx <= 'hE; // dark grey
end
'd1: begin // drawbridge
draw_start_rect <= 1;
vx0 <= 110; vy0 <= 110;
vx1 <= 140; vy1 <= 130;
cidx <= 'hF; // blue-grey
end
'd2: begin // drawbridge arch
draw_start_circle <= 1;
vx0 <= 125; vy0 <= 110;
vr0 <= 15;
cidx <= 'hF; // blue-grey
end
'd3: begin // left tower
draw_start_rect <= 1;
vx0 <= 40; vy0 <= 50;
vx1 <= 60; vy1 <= 130;
cidx <= 'hE; // dark grey
end
'd4: begin // middle tower
draw_start_rect <= 1;
vx0 <= 110; vy0 <= 45;
vx1 <= 140; vy1 <= 75;
cidx <= 'hE; // dark grey
end
'd5: begin // right tower
draw_start_rect <= 1;
vx0 <= 190; vy0 <= 50;
vx1 <= 210; vy1 <= 130;
cidx <= 'hE; // dark grey
end
'd6: begin // left roof
draw_start_tri <= 1;
vx0 <= 50; vy0 <= 35;
vx1 <= 65; vy1 <= 50;
vx2 <= 35; vy2 <= 50;
cidx <= 'h2; // red
end
'd7: begin // middle roof
draw_start_tri <= 1;
vx0 <= 125; vy0 <= 25;
vx1 <= 145; vy1 <= 45;
vx2 <= 105; vy2 <= 45;
cidx <= 'h2; // red
end
'd8: begin // right roof
draw_start_tri <= 1;
vx0 <= 200; vy0 <= 35;
vx1 <= 215; vy1 <= 50;
vx2 <= 185; vy2 <= 50;
cidx <= 'h2; // red
end
'd9: begin // left window
draw_start_rect <= 1;
vx0 <= 46; vy0 <= 55;
vx1 <= 54; vy1 <= 70;
cidx <= 'hF; // blue-grey
end
'd10: begin // middle window
draw_start_rect <= 1;
vx0 <= 120; vy0 <= 50;
vx1 <= 130; vy1 <= 70;
cidx <= 'hF; // blue-grey
end
'd11: begin // right window
draw_start_rect <= 1;
vx0 <= 196; vy0 <= 55;
vx1 <= 204; vy1 <= 70;
cidx <= 'hF; // blue-grey
end
'd12: begin // battlement 1
draw_start_rect <= 1;
vx0 <= 63; vy0 <= 67;
vx1 <= 72; vy1 <= 75;
cidx <= 'hE; // dark grey
end
'd13: begin // battlement 2
draw_start_rect <= 1;
vx0 <= 80; vy0 <= 67;
vx1 <= 89; vy1 <= 75;
cidx <= 'hE; // dark grey
end
'd14: begin // battlement 3
draw_start_rect <= 1;
vx0 <= 97; vy0 <= 67;
vx1 <= 106; vy1 <= 75;
cidx <= 'hE; // dark grey
end
'd15: begin // battlement 4
draw_start_rect <= 1;
vx0 <= 144; vy0 <= 67;
vx1 <= 153; vy1 <= 75;
cidx <= 'hE; // dark grey
end
'd16: begin // battlement 5
draw_start_rect <= 1;
vx0 <= 161; vy0 <= 67;
vx1 <= 170; vy1 <= 75;
cidx <= 'hE; // dark grey
end
'd17: begin // battlement 6
draw_start_rect <= 1;
vx0 <= 178; vy0 <= 67;
vx1 <= 187; vy1 <= 75;
cidx <= 'hE; // dark grey
end
default: begin // clear ground
draw_start_rect <= 1;
vx0 <= 0; vy0 <= 131;
vx1 <= 319; vy1 <= 179;
cidx <= 'h0; // background (transparent)
end
endcase
end
DRAW_CASTLE: begin
draw_start_tri <= 0;
draw_start_rect <= 0;
draw_start_circle <= 0;
if (draw_done) begin
if (shape_id == SHAPE_CNT-1) begin
state <= DONE;
end else begin
shape_id <= shape_id + 1;
state <= INIT_CASTLE;
end
end
end
DONE: state <= DONE;
default: if (start) state <= INIT_RBOW; // IDLE
endcase
if (rst) state <= IDLE;
end
draw_triangle_fill #(.CORDW(CORDW)) draw_triangle_inst (
.clk,
.rst,
.start(draw_start_tri),
.oe,
.x0(vx0 * SCALE),
.y0(vy0 * SCALE),
.x1(vx1 * SCALE),
.y1(vy1 * SCALE),
.x2(vx2 * SCALE),
.y2(vy2 * SCALE),
.x(x_tri),
.y(y_tri),
.drawing(drawing_tri),
.busy(),
.done(draw_done_tri)
);
draw_rectangle_fill #(.CORDW(CORDW)) draw_rectangle_inst (
.clk,
.rst,
.start(draw_start_rect),
.oe,
.x0(vx0 * SCALE),
.y0(vy0 * SCALE),
.x1(vx1 * SCALE),
.y1(vy1 * SCALE),
.x(x_rect),
.y(y_rect),
.drawing(drawing_rect),
.busy(),
.done(draw_done_rect)
);
draw_circle_fill #(.CORDW(CORDW)) draw_circle_inst (
.clk,
.rst,
.start(draw_start_circle),
.oe,
.x0(vx0 * SCALE),
.y0(vy0 * SCALE),
.r0(vr0 * SCALE),
.x(x_circle),
.y(y_circle),
.drawing(drawing_circle),
.busy(),
.done(draw_done_circle)
);
// output coordinates for the active shape
always_ff @(posedge clk) begin
x <= drawing_tri ? x_tri : (drawing_rect ? x_rect : x_circle);
y <= drawing_tri ? y_tri : (drawing_rect ? y_rect : y_circle);
end
// drawing and done apply to all drawing types
always_ff @(posedge clk) begin
drawing <= drawing_tri || drawing_rect || drawing_circle;
draw_done <= draw_done_tri || draw_done_rect || draw_done_circle;
end
// doesn't need delaying one cycle because it's based on draw_done
always_ff @(posedge clk) done <= (state == DONE);
endmodule
Rendering Background
The background is rendered in the top module but will move to its own module in time.
// background colour (sy ignores 16:9 letterbox)
logic [COLRW-1:0] bg_colr;
always_ff @(posedge clk_pix) begin
if (line) begin
if (sy == 0) bg_colr <= 12'h000;
else if (sy == 60) bg_colr <= 12'h239;
else if (sy == 140) bg_colr <= 12'h24A;
else if (sy == 195) bg_colr <= 12'h25B;
else if (sy == 230) bg_colr <= 12'h26C;
else if (sy == 260) bg_colr <= 12'h27D;
else if (sy == 285) bg_colr <= 12'h29E;
else if (sy == 305) bg_colr <= 12'h2BF;
else if (sy == 322) bg_colr <= 12'h370; // below castle (2x pix)
else if (sy == 420) bg_colr <= 12'h000;
end
end
What’s Next?
Check out my other FPGA demos or the FPGA graphics tutorials.
Get in touch on Mastodon, Bluesky, or X. If you enjoy my work, please sponsor me. 🙏