Project F

Rasterbars

Published

This FPGA demo effect renders four animated rasterbars. I created this effect with benjamin.computer for All You Need, a Chapterhouse prod released at Revision 2022.

For an introduction to rasterbars and other simple graphics effects, check out Racing the Beam.

The design was originally for a custom Artix-7 dev board, but this version 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. 🙏

FPGA Demo

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/rasterbars/

New to FPGA graphics design? Start with Beginning FPGA Graphics.

Demo Structure

I’ve written a separate post on the workings of the FPGA Sine Lookup Table.

Rasterbar Rendering

Explanation to follow.

Simple Rasterbar

module rasterbar (
    input  wire logic clk,                      // clock
    input  wire logic start,                    // start control
    input  wire logic line,                     // signal new screen line
    input  wire logic [11:0] base_colr,         // base bar colour
    input  wire logic [3:0] colr_steps,         // bar colour steps
    input  wire logic [3:0] colr_lines,         // lines of each colour
    output      logic [11:0] bar_colr,          // bar draw colour
    output      logic drawing,                  // bar is drawing
    output      logic done                      // bar drawing is complete
    );

    // NB. We don't (yet) register colr_steps and colr_lines

    logic bar_inc;  // increasing (or decreasing) brightness
    logic [3:0] cnt_step;  // count colour steps in each bar
    logic [3:0] cnt_line;  // count lines of each colour
    
    // generate bar colours
    always_ff @(posedge clk) begin
        if (start) begin  // reset colour at start of bar
            bar_colr <= base_colr;
            bar_inc <= 1;  // start by increasing brightness
            cnt_step <= 0;
            cnt_line <= 0;
            drawing <= 1;
            done <= 0;
        end else if (line) begin  // on each screen line
            if (cnt_line == colr_lines-1) begin  // colour complete
                cnt_line <= 0;
                if (cnt_step == colr_steps-1) begin  // switch increase/decrease
                    if (bar_inc == 1) begin
                        bar_inc <= 0;
                        cnt_step <= 0;
                    end else begin
                        drawing <= 0;
                        done <= 1;
                        bar_colr <= 12'h000;
                    end
                end else begin
                    bar_colr <= (bar_inc) ? bar_colr + 12'h111 : bar_colr - 12'h111;
                    cnt_step <= cnt_step + 1;
                end
            end else cnt_line <= cnt_line + 1;
        end
    end
endmodule

Render Multiple Rasterbars

module render_rasterbars #(
    parameter CORDW=16,      // signed coordinate width
    parameter VCENTER=180,   // vertical centre of raster render
    parameter COLR_LINES=3,  // lines of each colour
    parameter SIN_FILE="",   // sine table ROM .mem file
    parameter SIN_SHIFT=0    // right-shift sine values
    ) (
    input  wire logic clk,                    // clock
    input  wire logic start,                  // start control
    input  wire logic line,                   // line signal
    input  wire logic signed [CORDW-1:0] sy,  // vertical position
    output      logic [11:0] bar_colr,        // colour to draw
    output      logic bar_up                  // bar is drawing up
    );

    // sine table
    localparam SIN_DEPTH=64;  // entires in sine ROM 0°-90°
    localparam SIN_WIDTH=8;   // width of sine ROM data
    localparam SIN_ADDRW=$clog2(4*SIN_DEPTH);   // full table -180° to +180°
    logic [SIN_ADDRW-1:0] sin_id, sin_offs;
    logic signed [CORDW-1:0] sin_data;  // sign extend data to match coords
    sine_table #(
        .ROM_DEPTH(SIN_DEPTH),
        .ROM_WIDTH(SIN_WIDTH),
        .ROM_FILE(SIN_FILE)
    ) sine_table_inst (
        .id(sin_id + sin_offs),
        .data(sin_data)
    );

    // raster A
    localparam BASE_COLR_A  = 12'h126;  // bar start colour (blue)
    localparam STEPS_A      = 10;  // colours steps in each bar
    logic [11:0] bar_colr_a;
    logic bar_drawing_a;
    logic signed [CORDW-1:0] bar_y_a;       // screen line to draw at
    logic signed [CORDW-1:0] bar_y_a_prev;  // previous line for direction detection
    logic bar_up_a;  // bar is moving up the screen?

    rasterbar raster_a (
        .clk,
        .start(sy == bar_y_a),
        .line,
        .base_colr(BASE_COLR_A),
        .colr_steps(STEPS_A),
        .colr_lines(COLR_LINES),
        .bar_colr(bar_colr_a),
        .drawing(bar_drawing_a),
        /* verilator lint_off PINCONNECTEMPTY */
        .done()
        /* verilator lint_on PINCONNECTEMPTY */
    );

    // raster B
    localparam BASE_COLR_B  = 12'h640;  // bar start colour (gold)
    localparam STEPS_B      =  10;      // colours steps in each bar
    logic [11:0] bar_colr_b;
    logic bar_drawing_b;
    logic signed [CORDW-1:0] bar_y_b;       // screen line to draw at
    logic signed [CORDW-1:0] bar_y_b_prev;  // previous line for direction detection
    logic bar_up_b;  // bar is moving up the screen?

    rasterbar raster_b (
        .clk,
        .start(sy == bar_y_b),
        .line,
        .base_colr(BASE_COLR_B),
        .colr_steps(STEPS_B),
        .colr_lines(COLR_LINES),
        .bar_colr(bar_colr_b),
        .drawing(bar_drawing_b),
        /* verilator lint_off PINCONNECTEMPTY */
        .done()
        /* verilator lint_on PINCONNECTEMPTY */
    );

    // raster C
    localparam BASE_COLR_C  = 12'h610;  // bar start colour (red)
    localparam STEPS_C      =  10;      // colours steps in each bar
    logic [11:0] bar_colr_c;
    logic bar_drawing_c;
    logic signed [CORDW-1:0] bar_y_c;       // screen line to draw at
    logic signed [CORDW-1:0] bar_y_c_prev;  // previous line for direction detection
    logic bar_up_c;  // bar is moving up the screen?

    rasterbar raster_c (
        .clk,
        .start(sy == bar_y_c),
        .line,
        .base_colr(BASE_COLR_C),
        .colr_steps(STEPS_C),
        .colr_lines(COLR_LINES),
        .bar_colr(bar_colr_c),
        .drawing(bar_drawing_c),
        /* verilator lint_off PINCONNECTEMPTY */
        .done()
        /* verilator lint_on PINCONNECTEMPTY */
    );

    // raster D
    localparam BASE_COLR_D  = 12'h145;  // bar start colour (greenish)
    localparam STEPS_D      =  10;      // colours steps in each bar
    logic [11:0] bar_colr_d;
    logic bar_drawing_d;
    logic signed [CORDW-1:0] bar_y_d;       // screen line to draw at
    logic signed [CORDW-1:0] bar_y_d_prev;  // previous line for direction detection
    logic bar_up_d;  // bar is moving up the screen?

    rasterbar raster_d (
        .clk,
        .start(sy == bar_y_d),
        .line,
        .base_colr(BASE_COLR_D),
        .colr_steps(STEPS_D),
        .colr_lines(COLR_LINES),
        .bar_colr(bar_colr_d),
        .drawing(bar_drawing_d),
        /* verilator lint_off PINCONNECTEMPTY */
        .done()
        /* verilator lint_on PINCONNECTEMPTY */
    );

    // update bar positions with sine table
    enum {INIT, BAR_A, BAR_B, BAR_C, BAR_D, DONE} state;
    always_ff @(posedge clk) begin
        case (state)
            INIT: begin
                state <= BAR_A;
                sin_id <= sin_id + 1;
                sin_offs <= 0;
            end
            BAR_A: begin
                state <= BAR_B;
                bar_y_a <= VCENTER + (sin_data >>> SIN_SHIFT);
                bar_y_a_prev <= bar_y_a;
                sin_offs <= 64;  // offset for bar B: 64/256
            end
            BAR_B: begin
                state <= BAR_C;
                bar_y_b <= VCENTER + (sin_data >>> SIN_SHIFT);
                bar_y_b_prev <= bar_y_b;
                sin_offs <= 128;  // offset for bar C: 128/256
            end
            BAR_C: begin
                state <= BAR_D;
                bar_y_c <= VCENTER + (sin_data >>> SIN_SHIFT);
                bar_y_c_prev <= bar_y_c;
                sin_offs <= 192;  // offset for bar D: 192/256
            end
            BAR_D: begin
                state <= DONE;
                bar_y_d <= VCENTER + (sin_data >>> SIN_SHIFT);
                bar_y_d_prev <= bar_y_d;
            end
            default: if (start) state <= INIT;
        endcase
    end

    // bar rising or falling?
    always_comb begin
        bar_up_a = (bar_y_a < bar_y_a_prev);
        bar_up_b = (bar_y_b < bar_y_b_prev);
        bar_up_c = (bar_y_c < bar_y_c_prev);
        bar_up_d = (bar_y_d < bar_y_d_prev);
    end

    always_ff @(posedge clk) begin
        if (bar_drawing_a && bar_up_a) bar_colr <= bar_colr_a;
        else if (bar_drawing_b && bar_up_b) bar_colr <= bar_colr_b;
        else if (bar_drawing_c && bar_up_c) bar_colr <= bar_colr_c;
        else if (bar_drawing_d && bar_up_d) bar_colr <= bar_colr_d;
        else if (bar_drawing_a) bar_colr <= bar_colr_a;
        else if (bar_drawing_b) bar_colr <= bar_colr_b;
        else if (bar_drawing_c) bar_colr <= bar_colr_c;
        else if (bar_drawing_d) bar_colr <= bar_colr_d;
        else bar_colr <= 12'h000;
    end

    always_ff @(posedge clk) begin
        if ((bar_drawing_a && bar_up_a) || (bar_drawing_b && bar_up_b) ||
            (bar_drawing_c && bar_up_c) || (bar_drawing_d && bar_up_d))
            bar_up <= 1;
        else bar_up <= 0;
    end
endmodule

This is a quick design for a demo, not an example of good hardware design practice! For one thing, I’m not proud of that huge if-else statement to determine bar drawing order!

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 demos, FPGA graphics tutorials, and guide to FPGA sine lookup tables.