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.
Share your thoughts with @WillFlux on Mastodon or Twitter. If you like what I do, sponsor me. 🙏
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?
If you enjoyed this post, please sponsor me. Sponsors help me create more FPGA and RISC-V projects for everyone, and they get early access to blog posts and source code. 🙏
Check out my other FPGA demos or the FPGA graphics tutorials.