Skip to content

Session5 - RTL Design

(Mon Mar 2) Miles stone!

Homework

1. Write Verilog for your project’s core module (aim for 10-30 lines to start)

To refine the customized Verilog code of the previous homework, I used AI. The prompt was “Is it correct?” AI said, “Good code”, but I don’t know why. This Code is difficult and curious, it means I try harder astronomically.

Verilog - hello_morse.v

// ============================================================================
// LED Color Buzzer Morse Beacon - Kyunghee Yoo - ChatGPT - Microelectronics 2026 - hello_morse.v
// ============================================================================
//
// HOW IT WORKS:
//   1. Chip drives a strip of WS2812 (NeoPixel) LEDs
//   2. All LEDs flash together to send Morse code
//   3. Press button to change colors
//   4. Beep when LEDs are flashing (during dots and dashes) 
//
// WHAT YOU'LL LEARN:
//   - Precise timing for serial protocols (WS2812)
//   - State machines for sequencing
//   - Encoding data (ASCII to Morse)
//   - Tone PWM generator
//   - Buzzer Control Logic
//
// MORSE CODE TIMING:
//   - Dot  = 1 unit
//   - Dash = 3 units
//   - Gap between dots/dashes = 1 unit
//   - Gap between letters = 3 units
//   - Gap between words = 7 units
//
// ABOUT WS2812 LEDs:
//   These "smart" LEDs have a built-in chip. You send color data one bit
//   at a time using precise pulse widths:
//
//   Sending a '0' bit:        Sending a '1' bit:
//   ┌──┐                      ┌────────┐
//   │   │                       │               │
//   └──┴──────────      └────────┴────
//   0.4µs HIGH, 0.8µs LOW     0.8µs HIGH, 0.4µs LOW
//
//   Each LED needs 24 bits: 8 green, 8 red, 8 blue (yes, GRB order!)
//
// OTHER USES FOR THIS CODE:
//   - VU meter / level display (map input to number of lit LEDs)
//   - Battery gauge
//   - Progress bar
//   - Temperature indicator
//   - Binary counter display
//
// ============================================================================

`timescale 1ns/1ps

module hello_morse #(
    parameter CLK_FREQ = 50_000_000,  // Clock speed in Hz
    parameter NUM_LEDS = 8            // Number of LEDs in the strip
)(
    input  wire clk,
    input  wire rst_n,
    input  wire btn_color,   // Button to cycle colors
    output wire led_data,    // Data output to WS2812 strip
    output wire buzzer_pwm   // 600 Hz piezo buzzer output
);

    // ========================================================================
    // MESSAGE MEMORY
    // ========================================================================

    localparam MSG_LEN = 5;
    reg [7:0] message [0:MSG_LEN-1];

    initial begin
        message[0] = "H";  // ....
        message[1] = "E";  // .
        message[2] = "L";  // .-..
        message[3] = "L";  // .-..
        message[4] = "O";  // ---
    end

    // ========================================================================
    // MORSE CODE LOOKUP TABLE
    // Format: {length[3:0], pattern[7:0]}
    // Pattern is sent LSB first. 0 = dot, 1 = dash.
    // ========================================================================

    function [11:0] get_morse;
        input [7:0] ch;
        begin
            case (ch)
                "A": get_morse = {4'd2, 8'b00000010};  // .-
                "B": get_morse = {4'd4, 8'b00000001};  // -...
                "C": get_morse = {4'd4, 8'b00000101};  // -.-.
                "D": get_morse = {4'd3, 8'b00000001};  // -..
                "E": get_morse = {4'd1, 8'b00000000};  // .
                "F": get_morse = {4'd4, 8'b00000100};  // ..-.
                "G": get_morse = {4'd3, 8'b00000011};  // --.
                "H": get_morse = {4'd4, 8'b00000000};  // ....
                "I": get_morse = {4'd2, 8'b00000000};  // ..
                "J": get_morse = {4'd4, 8'b00001110};  // .---
                "K": get_morse = {4'd3, 8'b00000101};  // -.-
                "L": get_morse = {4'd4, 8'b00000010};  // .-..
                "M": get_morse = {4'd2, 8'b00000011};  // --
                "N": get_morse = {4'd2, 8'b00000001};  // -.
                "O": get_morse = {4'd3, 8'b00000111};  // ---
                "P": get_morse = {4'd4, 8'b00000110};  // .--.
                "Q": get_morse = {4'd4, 8'b00001011};  // --.-
                "R": get_morse = {4'd3, 8'b00000010};  // .-.
                "S": get_morse = {4'd3, 8'b00000000};  // ...
                "T": get_morse = {4'd1, 8'b00000001};  // -
                "U": get_morse = {4'd3, 8'b00000100};  // ..-
                "V": get_morse = {4'd4, 8'b00001000};  // ...-
                "W": get_morse = {4'd3, 8'b00000110};  // .--
                "X": get_morse = {4'd4, 8'b00001001};  // -..-
                "Y": get_morse = {4'd4, 8'b00001101};  // -.--
                "Z": get_morse = {4'd4, 8'b00000011};  // --..
                " ": get_morse = {4'd0, 8'b00000000};  // word gap marker
                default: get_morse = {4'd0, 8'b00000000};
            endcase
        end
    endfunction

    // ========================================================================
    // MORSE TIMING
    // ========================================================================

    localparam UNIT_TIME = CLK_FREQ / 10;  // 100 ms per unit
    localparam DOT_TIME  = UNIT_TIME;
    localparam DASH_TIME = UNIT_TIME * 3;
    localparam SYM_GAP   = UNIT_TIME;
    localparam CHAR_GAP  = UNIT_TIME * 3;
    localparam WORD_GAP  = UNIT_TIME * 7;

    // ========================================================================
    // BUZZER CONFIGURATION
    // ========================================================================

    localparam BUZZER_FREQ = 600;
    localparam PWM_PERIOD  = CLK_FREQ / BUZZER_FREQ;

    reg [31:0] buzzer_counter;
    reg        buzzer_tone;

    // ========================================================================
    // BUTTON / COLOR CONTROL
    // ========================================================================

    wire btn_pressed;

    debounce #(.CLK_FREQ(CLK_FREQ)) debounce_inst (
        .clk(clk),
        .rst_n(rst_n),
        .btn_raw(btn_color),
        .btn_pressed(btn_pressed)
    );

    reg [1:0] color_mode;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            color_mode <= 2'd0;
        else if (btn_pressed)
            color_mode <= color_mode + 1'b1;
    end

    reg [23:0] on_color;
    reg [23:0] off_color;

    always @(*) begin
        case (color_mode)
            2'd0: begin on_color = 24'h00FF00; off_color = 24'h000000; end  // Red in GRB
            2'd1: begin on_color = 24'hFF0000; off_color = 24'h000000; end  // Green in GRB
            2'd2: begin on_color = 24'h0000FF; off_color = 24'h000000; end  // Blue in GRB
            2'd3: begin on_color = 24'hFFFFFF; off_color = 24'h050500; end  // White / dim off
            default: begin on_color = 24'h00FF00; off_color = 24'h000000; end
        endcase
    end

    // ========================================================================
    // MORSE CODE STATE MACHINE
    // ========================================================================

    localparam MS_LOAD      = 3'd0;
    localparam MS_SYMBOL    = 3'd1;
    localparam MS_SYM_GAP   = 3'd2;
    localparam MS_CHAR_GAP  = 3'd3;
    localparam MS_WORD_GAP  = 3'd4;
    localparam MS_RESTART   = 3'd5;

    reg [2:0]  morse_state;
    reg [3:0]  char_idx;
    reg [11:0] morse_data;
    reg [2:0]  symbol_idx;
    reg [31:0] morse_timer;
    reg        leds_on;

    wire [3:0] morse_len = morse_data[11:8];
    wire       is_dash   = morse_data[symbol_idx];

    wire [11:0] curr_char_morse = get_morse(message[char_idx]);
    wire [3:0]  curr_char_len   = curr_char_morse[11:8];
    wire        curr_is_space   = (message[char_idx] == " ");
    wire        next_char_is_space =
        (char_idx < MSG_LEN-1) && (message[char_idx+1] == " ");

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            morse_state <= MS_LOAD;
            char_idx    <= 0;
            morse_data  <= 0;
            symbol_idx  <= 0;
            morse_timer <= 0;
            leds_on     <= 0;
        end else begin
            case (morse_state)

                MS_LOAD: begin
                    leds_on     <= 1'b0;
                    morse_data  <= curr_char_morse;
                    symbol_idx  <= 3'd0;
                    morse_timer <= 32'd0;

                    if (curr_is_space || curr_char_len == 0)
                        morse_state <= MS_WORD_GAP;
                    else
                        morse_state <= MS_SYMBOL;
                end

                MS_SYMBOL: begin
                    leds_on     <= 1'b1;
                    morse_timer <= morse_timer + 1'b1;

                    if (( is_dash && (morse_timer >= DASH_TIME - 1)) ||
                        (!is_dash && (morse_timer >= DOT_TIME  - 1))) begin
                        leds_on     <= 1'b0;
                        morse_timer <= 32'd0;

                        if (symbol_idx + 1 >= morse_len) begin
                            if (char_idx >= MSG_LEN - 1)
                                morse_state <= MS_RESTART;
                            else if (next_char_is_space)
                                morse_state <= MS_WORD_GAP;
                            else
                                morse_state <= MS_CHAR_GAP;
                        end else begin
                            symbol_idx  <= symbol_idx + 1'b1;
                            morse_state <= MS_SYM_GAP;
                        end
                    end
                end

                MS_SYM_GAP: begin
                    leds_on     <= 1'b0;
                    morse_timer <= morse_timer + 1'b1;

                    if (morse_timer >= SYM_GAP - 1) begin
                        morse_timer <= 32'd0;
                        morse_state <= MS_SYMBOL;
                    end
                end

                MS_CHAR_GAP: begin
                    leds_on     <= 1'b0;
                    morse_timer <= morse_timer + 1'b1;

                    if (morse_timer >= CHAR_GAP - 1) begin
                        morse_timer <= 32'd0;
                        char_idx    <= char_idx + 1'b1;
                        morse_state <= MS_LOAD;
                    end
                end

                MS_WORD_GAP: begin
                    leds_on     <= 1'b0;
                    morse_timer <= morse_timer + 1'b1;

                    if (morse_timer >= WORD_GAP - 1) begin
                        morse_timer <= 32'd0;

                        if (curr_is_space) begin
                            // currently sitting on a space: move to next char
                            if (char_idx >= MSG_LEN - 1) begin
                                morse_state <= MS_RESTART;
                            end else begin
                                char_idx    <= char_idx + 1'b1;
                                morse_state <= MS_LOAD;
                            end
                        end else begin
                            // current char ended, next one is a space: skip space too
                            if (char_idx + 2 >= MSG_LEN) begin
                                morse_state <= MS_RESTART;
                            end else begin
                                char_idx    <= char_idx + 2;
                                morse_state <= MS_LOAD;
                            end
                        end
                    end
                end

                MS_RESTART: begin
                    leds_on     <= 1'b0;
                    morse_timer <= morse_timer + 1'b1;

                    if (morse_timer >= WORD_GAP - 1) begin
                        morse_timer <= 32'd0;
                        char_idx    <= 4'd0;
                        morse_state <= MS_LOAD;
                    end
                end

                default: begin
                    morse_state <= MS_LOAD;
                    char_idx    <= 4'd0;
                    morse_timer <= 32'd0;
                    leds_on     <= 1'b0;
                end
            endcase
        end
    end
    // ========================================================================
    // BUZZER DRIVER - 600 HZ SQUARE WAVE
    // ========================================================================

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            buzzer_counter <= 0;
            buzzer_tone    <= 0;
        end else begin
            if (buzzer_counter >= (PWM_PERIOD / 2 - 1)) begin
                buzzer_counter <= 0;
                buzzer_tone    <= ~buzzer_tone;
            end else begin
                buzzer_counter <= buzzer_counter + 1'b1;
            end
        end
    end

    assign buzzer_pwm = leds_on ? buzzer_tone : 1'b0;

    // ========================================================================
    // WS2812 DRIVER
    // ========================================================================

    localparam T0H    = CLK_FREQ / 2_500_000;   // 0.4 us
    localparam T0L    = CLK_FREQ / 1_250_000;   // 0.8 us
    localparam T1H    = CLK_FREQ / 1_250_000;   // 0.8 us
    localparam T1L    = CLK_FREQ / 2_500_000;   // 0.4 us
    localparam TRESET = CLK_FREQ / 20_000;      // 50 us

    localparam WS_RESET = 2'd0;
    localparam WS_LOAD  = 2'd1;
    localparam WS_HIGH  = 2'd2;
    localparam WS_LOW   = 2'd3;

    localparam LED_IDX_W = (NUM_LEDS <= 1) ? 1 : $clog2(NUM_LEDS);

    reg [1:0] ws_state;
    reg [LED_IDX_W-1:0] led_idx;
    reg [4:0] bit_idx;
    reg [23:0] pixel_data;
    reg [15:0] timer;
    reg data_out;

    wire [23:0] current_color = leds_on ? on_color : off_color;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            ws_state   <= WS_RESET;
            led_idx    <= {LED_IDX_W{1'b0}};
            bit_idx    <= 5'd0;
            pixel_data <= 24'd0;
            timer      <= 16'd0;
            data_out   <= 1'b0;
        end else begin
            case (ws_state)

                WS_RESET: begin
                    data_out <= 1'b0;
                    timer <= timer + 1'b1;
                    if (timer >= TRESET - 1) begin
                        timer   <= 16'd0;
                        led_idx <= {LED_IDX_W{1'b0}};
                        ws_state <= WS_LOAD;
                    end
                end

                WS_LOAD: begin
                    pixel_data <= current_color;
                    bit_idx    <= 5'd23;
                    timer      <= 16'd0;
                    ws_state   <= WS_HIGH;
                end

                WS_HIGH: begin
                    data_out <= 1'b1;
                    timer <= timer + 1'b1;

                    if (pixel_data[bit_idx]) begin
                        if (timer >= T1H - 1) begin
                            timer <= 16'd0;
                            ws_state <= WS_LOW;
                        end
                    end else begin
                        if (timer >= T0H - 1) begin
                            timer <= 16'd0;
                            ws_state <= WS_LOW;
                        end
                    end
                end

                WS_LOW: begin
                    data_out <= 1'b0;
                    timer <= timer + 1'b1;

                    if (pixel_data[bit_idx]) begin
                        if (timer >= T1L - 1) begin
                            timer <= 16'd0;
                            if (bit_idx == 0) begin
                                if (led_idx >= NUM_LEDS - 1)
                                    ws_state <= WS_RESET;
                                else begin
                                    led_idx <= led_idx + 1'b1;
                                    ws_state <= WS_LOAD;
                                end
                            end else begin
                                bit_idx <= bit_idx - 1'b1;
                                ws_state <= WS_HIGH;
                            end
                        end
                    end else begin
                        if (timer >= T0L - 1) begin
                            timer <= 16'd0;
                            if (bit_idx == 0) begin
                                if (led_idx >= NUM_LEDS - 1)
                                    ws_state <= WS_RESET;
                                else begin
                                    led_idx <= led_idx + 1'b1;
                                    ws_state <= WS_LOAD;
                                end
                            end else begin
                                bit_idx <= bit_idx - 1'b1;
                                ws_state <= WS_HIGH;
                            end
                        end
                    end
                end

                default: begin
                    ws_state <= WS_RESET;
                end
            endcase
        end
    end

    assign led_data = data_out;

endmodule

Testbench - hello_morse_tb.v

// ============================================================================
// Morse Beacon Testbench
// ============================================================================
//
// This testbench verifies the Morse code beacon by:
//   1. Capturing the serial data sent to the LED strip
//   2. Decoding the WS2812 protocol to extract color values
//   3. Detecting dots, dashes, and gaps in the Morse output
//   4. Observing buzzer activity during Morse transmission
//
// MORSE TIMING RECAP:
//   - Dot  = 1 unit (LEDs on)
//   - Dash = 3 units (LEDs on)
//   - Gap between symbols = 1 unit (LEDs off)
//   - Gap between letters = 3 units (LEDs off)
//   - Gap between words   = 7 units (LEDs off)
//
// HOW TO RUN:
//   iverilog -g2012 -o sim.vvp morse_beacon.v debounce.v morse_beacon_tb.v
//   vvp sim.vvp
//   gtkwave morse_beacon_tb.vcd
//
// ============================================================================

`timescale 1ns/1ps

module morse_beacon_tb;

    // ========================================================================
    // Testbench Signals
    // ========================================================================

    reg  clk;
    reg  rst_n;
    reg  btn_color;
    wire led_data;
    wire buzzer_pwm;

    // ========================================================================
    // DUT Instantiation
    // ========================================================================
    //
    // Use 10 MHz for simulation to reduce runtime.
    // 10 MHz => 100 ns clock period
    // UNIT_TIME = CLK_FREQ / 10 = 1,000,000 cycles = 100 ms

    morse_beacon #(
        .CLK_FREQ(10_000_000),
        .NUM_LEDS(8)
    ) dut (
        .clk        (clk),
        .rst_n      (rst_n),
        .btn_color  (btn_color),
        .led_data   (led_data),
        .buzzer_pwm (buzzer_pwm)
    );

    // ========================================================================
    // Clock Generation
    // ========================================================================

    initial begin
        clk = 1'b0;
        forever #50 clk = ~clk;   // 10 MHz clock
    end

    // ========================================================================
    // WS2812 Protocol Decoder
    // ========================================================================
    //
    // Decode serial LED data to recover one 24-bit GRB value per LED.

    time rise_time;
    time fall_time;
    real high_time_us;

    reg [23:0] rx_data;
    reg [4:0]  rx_bit;
    integer    led_count;
    integer    frame_count;

    reg [23:0] last_color;

    initial begin
        rx_bit      = 0;
        rx_data     = 24'd0;
        led_count   = 0;
        frame_count = 0;
        rise_time   = 0;
        fall_time   = 0;
        high_time_us = 0.0;
        last_color  = 24'h000000;
    end

    // Record rising edge time
    always @(posedge led_data) begin
        rise_time = $time;
    end

    // Decode bit on falling edge
    always @(negedge led_data) begin
        fall_time    = $time;
        high_time_us = (fall_time - rise_time) / 1000.0; // ns -> us

        // WS2812: about 0.8us high = 1, about 0.4us high = 0
        if (high_time_us > 0.6)
            rx_data[23 - rx_bit] = 1'b1;
        else
            rx_data[23 - rx_bit] = 1'b0;

        rx_bit = rx_bit + 1;

        if (rx_bit >= 24) begin
            // Use the first LED in the frame as Morse ON/OFF indicator
            if (led_count == 0) begin
                if ((rx_data != 24'h000000) && (last_color == 24'h000000)) begin
                    $display("[%0t ns] LEDs ON   (G=%02h R=%02h B=%02h)",
                             $time, rx_data[23:16], rx_data[15:8], rx_data[7:0]);
                end
                else if ((rx_data == 24'h000000) && (last_color != 24'h000000)) begin
                    $display("[%0t ns] LEDs OFF", $time);
                end

                last_color = rx_data;
            end

            rx_bit   = 0;
            led_count = led_count + 1;

            if (led_count >= 8) begin
                led_count   = 0;
                frame_count = frame_count + 1;
            end
        end
    end

    // ========================================================================
    // Optional buzzer monitor
    // ========================================================================

    always @(posedge buzzer_pwm) begin
        $display("[%0t ns] Buzzer activity detected", $time);
    end

    // ========================================================================
    // Main Test Sequence
    // ========================================================================

    initial begin
        $dumpfile("morse_beacon_tb.vcd");
        $dumpvars(0, morse_beacon_tb);

        rst_n     = 1'b0;
        btn_color = 1'b0;

        $display("");
        $display("===========================================");
        $display("Morse Beacon Testbench");
        $display("===========================================");
        $display("Message: HELLO");
        $display("Expected Morse: .... . .-.. .-.. ---");
        $display("");
        $display("Watching LED transitions and buzzer activity...");
        $display("Simulation clock = 10 MHz");
        $display("Morse unit time  = 100 ms");
        $display("");

        // release reset
        #10_000;
        rst_n = 1'b1;

        // --------------------------------------------------------------------
        // Watch initial Morse output
        // --------------------------------------------------------------------

        #500_000_000;  // 500 ms

        // --------------------------------------------------------------------
        // Test color change button
        // --------------------------------------------------------------------

        $display("");
        $display("Pressing color button...");

        btn_color = 1'b1;
        #30_000_000;   // 30 ms, long enough for debounce
        btn_color = 1'b0;

        // Wait and observe new color frames
        #200_000_000;

        // Press again
        $display("Pressing color button again...");

        btn_color = 1'b1;
        #30_000_000;
        btn_color = 1'b0;

        #200_000_000;

        // --------------------------------------------------------------------
        // Finish
        // --------------------------------------------------------------------

        $display("");
        $display("===========================================");
        $display("Test complete!");
        $display("Frames captured: %0d", frame_count);
        $display("===========================================");
        $finish;
    end

endmodule

2. Integrate with any provided library modules (e.g., debounce, UART, PWM) — create a top-level wrapper

AI prompt: create a top-level wrapper ? A top-level wrapper is a small module that connects your design to the FPGA board pins (clock, button, LEDs, buzzer). It instantiates your morse_beacon module and wires it to the hardware.

The design integrates an external debounce module to clean the push-button input before color selection. Other functions, such as Morse encoding, buzzer generation, and WS2812 LED driving, are implemented directly inside the top-level morse_beacon module.

library module - debounce.v

// ============================================================================
// Button Debouncer Module
// ============================================================================
//
// PROBLEM:
// Mechanical buttons do not switch cleanly. When pressed or released,
// the contacts bounce for a short time, causing multiple rapid transitions.
//
//   Ideal press:          Real press with bounce:
//
//   ─────┐                ─────┐ ┌┐ ┌┐ ┌─┐
//        │                     │ ││ ││ │ │
//        └────────             └─┘└─┘└─┘ └────────
//
// SOLUTION:
// 1. Synchronize the asynchronous input to the system clock.
// 2. Wait until the input remains different from the current stable state
//    for a fixed amount of time.
// 3. Accept the new stable state only after that delay.
// 4. Output a one-clock-cycle pulse when a valid press is detected.
//
// ============================================================================

`timescale 1ns/1ps

module debounce #(
    // ========================================================================
    // Parameters
    // ========================================================================
    parameter integer CLK_FREQ    = 50_000_000, // Clock frequency in Hz
    parameter integer DEBOUNCE_MS = 20          // Stable time required in ms
)(
    // ========================================================================
    // Ports
    // ========================================================================
    input  wire clk,         // System clock
    input  wire rst_n,       // Active-low reset
    input  wire btn_raw,     // Raw asynchronous button input
    output reg  btn_pressed  // One-clock pulse on valid rising press
);

    // ========================================================================
    // Counter size calculation
    // ========================================================================
    // Example:
    //   50 MHz clock, 20 ms debounce
    //   COUNT_MAX = 50,000,000 / 1000 * 20 = 1,000,000 cycles

    localparam integer COUNT_MAX = (CLK_FREQ / 1000) * DEBOUNCE_MS;
    localparam integer CTR_WIDTH = (COUNT_MAX <= 1) ? 1 : $clog2(COUNT_MAX + 1);

    // ========================================================================
    // Internal registers
    // ========================================================================

    reg                    btn_sync0;   // Synchronizer stage 1
    reg                    btn_sync1;   // Synchronizer stage 2
    reg                    btn_state;   // Current debounced stable state
    reg [CTR_WIDTH-1:0]    counter;     // Stability counter

    // ========================================================================
    // Main logic
    // ========================================================================

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            btn_sync0   <= 1'b0;
            btn_sync1   <= 1'b0;
            btn_state   <= 1'b0;
            btn_pressed <= 1'b0;
            counter     <= {CTR_WIDTH{1'b0}};
        end else begin
            // ----------------------------------------------------------------
            // Step 1: Synchronize raw input to avoid metastability
            // ----------------------------------------------------------------
            btn_sync0 <= btn_raw;
            btn_sync1 <= btn_sync0;

            // Default output: no pulse
            btn_pressed <= 1'b0;

            // ----------------------------------------------------------------
            // Step 2: Debounce
            // ----------------------------------------------------------------
            // If synchronized input matches the stable state,
            // reset the counter.
            //
            // If it differs, count how long it stays different.
            // Once it remains different long enough, accept the new state.
            if (btn_sync1 == btn_state) begin
                counter <= {CTR_WIDTH{1'b0}};
            end else begin
                if (counter >= CTR_WIDTH'(COUNT_MAX - 1)) begin
                    btn_state <= btn_sync1;
                    counter   <= {CTR_WIDTH{1'b0}};

                    // --------------------------------------------------------
                    // Step 3: Generate one-cycle pulse on rising edge only
                    // --------------------------------------------------------
                    // If the new stable state is 1, that means the button
                    // has been validly pressed.
                    if (btn_sync1)
                        btn_pressed <= 1'b1;
                end else begin
                    counter <= counter + 1'b1;
                end
            end
        end
    end

endmodule

3. Simulate with a testbench and examine waveforms in GTKWave

iverilog .v, .vvp, gtkwave _tb.vcd

# 1 Compile simulation iverilog -g2012 -o hello_morse.vvp hello_morse.v hello_morse_tb.v debounce.v

# 2 Run simulation vvp hello_morse.vvp

Command Meaning
iverilog compile Verilog
-g2012 use Verilog-2012 standard
-o ***.vvp create simulation file named ***.vvp
vvp ***.vvp run simulation
gtkwave ***.vcd view waveform

# 3 View waveform gtkwave hello_morse_tb.vcd

4. Run linter (verilator –lint-only) and fix any warnings

# Lint check
verilator --lint-only -Wall hello_morse.v debounce.v

Lint Warning

%Warning-WIDTHEXPAND: debounce.v:93:29: Operator GTE expects 32 bits on the LHS, but LHS's VARREF 'counter' generates 20 bits.
                                      : ... note: In instance 'hello_morse.debounce_inst'
   93 |                 if (counter >= COUNT_MAX - 1) begin
      |                             ^~
                      ... For warning description see https://verilator.org/warn/WIDTHEXPAND?v=5.044
                      ... Use "/* verilator lint_off WIDTHEXPAND */" and lint_on around source to disable this message.
%Warning-WIDTHEXPAND: hello_morse.v:191:38: Bit extraction of var[11:0] requires 4 bit index, not 3 bits.
                                          : ... note: In instance 'hello_morse'
  191 |     wire       is_dash   = morse_data[symbol_idx];
      |                                      ^
%Warning-WIDTHTRUNC: hello_morse.v:193:52: Bit extraction of array[4:0] requires 3 bit index, not 4 bits.
                                         : ... note: In instance 'hello_morse'
  193 |     wire [11:0] curr_char_morse = get_morse(message[char_idx]);
      |                                                    ^
                     ... For warning description see https://verilator.org/warn/WIDTHTRUNC?v=5.044
                     ... Use "/* verilator lint_off WIDTHTRUNC */" and lint_on around source to disable this message.
%Warning-WIDTHTRUNC: hello_morse.v:195:43: Bit extraction of array[4:0] requires 3 bit index, not 4 bits.
                                         : ... note: In instance 'hello_morse'
  195 |     wire        curr_is_space   = (message[char_idx] == " ");
      |                                           ^
%Warning-WIDTHEXPAND: hello_morse.v:373:31: Operator GTE expects 32 or 26 bits on the LHS, but LHS's VARREF 'timer' generates 16 bits.
                                          : ... note: In instance 'hello_morse'
  373 |                     if (timer >= TRESET - 1) begin
      |                               ^~
%Warning-WIDTHEXPAND: hello_morse.v:392:35: Operator GTE expects 32 or 26 bits on the LHS, but LHS's VARREF 'timer' generates 16 bits.
                                          : ... note: In instance 'hello_morse'
  392 |                         if (timer >= T1H - 1) begin
      |                                   ^~
%Warning-WIDTHEXPAND: hello_morse.v:397:35: Operator GTE expects 32 or 26 bits on the LHS, but LHS's VARREF 'timer' generates 16 bits.
                                          : ... note: In instance 'hello_morse'
  397 |                         if (timer >= T0H - 1) begin
      |                                   ^~
%Warning-WIDTHEXPAND: hello_morse.v:412:45: Operator GTE expects 32 or 4 bits on the LHS, but LHS's VARREF 'led_idx' generates 3 bits.
                                          : ... note: In instance 'hello_morse'
  412 |                                 if (led_idx >= NUM_LEDS - 1)
      |                                             ^~
%Warning-WIDTHEXPAND: hello_morse.v:409:35: Operator GTE expects 32 or 26 bits on the LHS, but LHS's VARREF 'timer' generates 16 bits.
                                          : ... note: In instance 'hello_morse'
  409 |                         if (timer >= T1L - 1) begin
      |                                   ^~
%Warning-WIDTHEXPAND: hello_morse.v:427:45: Operator GTE expects 32 or 4 bits on the LHS, but LHS's VARREF 'led_idx' generates 3 bits.
                                          : ... note: In instance 'hello_morse'
  427 |                                 if (led_idx >= NUM_LEDS - 1)
      |                                             ^~
%Warning-WIDTHEXPAND: hello_morse.v:424:35: Operator GTE expects 32 or 26 bits on the LHS, but LHS's VARREF 'timer' generates 16 bits.
                                          : ... note: In instance 'hello_morse'
  424 |                         if (timer >= T0L - 1) begin
      |                                   ^~
%Error: Exiting due to 11 warning(s)

Lint Fixing

Generated by AI

hello_morse.v

// ============================================================================ // LED Color Buzzer Morse Beacon - Kyunghee Yoo - Microelectronics 2026 // hello_morse.v -Lint // ============================================================================

`timescale 1ns/1ps

module hello_morse #( parameter integer CLK_FREQ = 50_000_000, // Clock speed in Hz parameter integer NUM_LEDS = 8 // Number of LEDs in the strip )( input wire clk, input wire rst_n, input wire btn_color, // Button to cycle colors output wire led_data, // Data output to WS2812 strip output wire buzzer_pwm // 600 Hz piezo buzzer output );

// ========================================================================
// MESSAGE MEMORY
// ========================================================================

localparam integer MSG_LEN     = 5;
localparam integer CHAR_IDX_W  = (MSG_LEN <= 1) ? 1 : $clog2(MSG_LEN);
localparam [CHAR_IDX_W-1:0] LAST_CHAR      = MSG_LEN - 1;
localparam [CHAR_IDX_W-1:0] ONE_CHAR       = 1;
localparam [CHAR_IDX_W-1:0] TWO_CHARS      = 2;
localparam [CHAR_IDX_W-1:0] NEXT_LAST_CHAR = MSG_LEN - 2;

reg [7:0] message [0:MSG_LEN-1];

initial begin
    message[0] = "H";  // ....
    message[1] = "E";  // .
    message[2] = "L";  // .-..
    message[3] = "L";  // .-..
    message[4] = "O";  // ---
end

// ========================================================================
// MORSE CODE LOOKUP TABLE
// Format: {length[3:0], pattern[7:0]}
// Pattern is sent LSB first. 0 = dot, 1 = dash.
// ========================================================================

function [11:0] get_morse;
    input [7:0] ch;
    begin
        case (ch)
            "A": get_morse = {4'd2, 8'b00000010};  // .-
            "B": get_morse = {4'd4, 8'b00000001};  // -...
            "C": get_morse = {4'd4, 8'b00000101};  // -.-.
            "D": get_morse = {4'd3, 8'b00000001};  // -..
            "E": get_morse = {4'd1, 8'b00000000};  // .
            "F": get_morse = {4'd4, 8'b00000100};  // ..-.
            "G": get_morse = {4'd3, 8'b00000011};  // --.
            "H": get_morse = {4'd4, 8'b00000000};  // ....
            "I": get_morse = {4'd2, 8'b00000000};  // ..
            "J": get_morse = {4'd4, 8'b00001110};  // .---
            "K": get_morse = {4'd3, 8'b00000101};  // -.-
            "L": get_morse = {4'd4, 8'b00000010};  // .-..
            "M": get_morse = {4'd2, 8'b00000011};  // --
            "N": get_morse = {4'd2, 8'b00000001};  // -.
            "O": get_morse = {4'd3, 8'b00000111};  // ---
            "P": get_morse = {4'd4, 8'b00000110};  // .--.
            "Q": get_morse = {4'd4, 8'b00001011};  // --.-
            "R": get_morse = {4'd3, 8'b00000010};  // .-.
            "S": get_morse = {4'd3, 8'b00000000};  // ...
            "T": get_morse = {4'd1, 8'b00000001};  // -
            "U": get_morse = {4'd3, 8'b00000100};  // ..-
            "V": get_morse = {4'd4, 8'b00001000};  // ...-
            "W": get_morse = {4'd3, 8'b00000110};  // .--
            "X": get_morse = {4'd4, 8'b00001001};  // -..-
            "Y": get_morse = {4'd4, 8'b00001101};  // -.--
            "Z": get_morse = {4'd4, 8'b00000011};  // --..
            " ": get_morse = {4'd0, 8'b00000000};  // word gap marker
            default: get_morse = {4'd0, 8'b00000000};
        endcase
    end
endfunction

// ========================================================================
// MORSE TIMING
// ========================================================================

localparam integer UNIT_TIME = CLK_FREQ / 10;  // 100 ms per unit
localparam integer DOT_TIME  = UNIT_TIME;
localparam integer DASH_TIME = UNIT_TIME * 3;
localparam integer SYM_GAP   = UNIT_TIME;
localparam integer CHAR_GAP  = UNIT_TIME * 3;
localparam integer WORD_GAP  = UNIT_TIME * 7;

// ========================================================================
// BUZZER CONFIGURATION
// ========================================================================

localparam integer BUZZER_FREQ = 600;
localparam integer PWM_PERIOD  = CLK_FREQ / BUZZER_FREQ;

reg [31:0] buzzer_counter;
reg        buzzer_tone;

// ========================================================================
// BUTTON / COLOR CONTROL
// ========================================================================

wire btn_pressed;

debounce #(
    .CLK_FREQ(CLK_FREQ)
) debounce_inst (
    .clk(clk),
    .rst_n(rst_n),
    .btn_raw(btn_color),
    .btn_pressed(btn_pressed)
);

reg [1:0] color_mode;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        color_mode <= 2'd0;
    else if (btn_pressed)
        color_mode <= color_mode + 2'd1;
end

reg [23:0] on_color;
reg [23:0] off_color;

always @(*) begin
    case (color_mode)
        2'd0: begin on_color = 24'h00FF00; off_color = 24'h000000; end  // Red in GRB
        2'd1: begin on_color = 24'hFF0000; off_color = 24'h000000; end  // Green in GRB
        2'd2: begin on_color = 24'h0000FF; off_color = 24'h000000; end  // Blue in GRB
        2'd3: begin on_color = 24'hFFFFFF; off_color = 24'h050500; end  // White / dim off
        default: begin on_color = 24'h00FF00; off_color = 24'h000000; end
    endcase
end

// ========================================================================
// MORSE CODE STATE MACHINE
// ========================================================================

localparam [2:0] MS_LOAD      = 3'd0;
localparam [2:0] MS_SYMBOL    = 3'd1;
localparam [2:0] MS_SYM_GAP   = 3'd2;
localparam [2:0] MS_CHAR_GAP  = 3'd3;
localparam [2:0] MS_WORD_GAP  = 3'd4;
localparam [2:0] MS_RESTART   = 3'd5;

reg [2:0]             morse_state;
reg [CHAR_IDX_W-1:0]  char_idx;
reg [11:0]            morse_data;
reg [3:0]             symbol_idx;
reg [31:0]            morse_timer;
reg                   leds_on;

wire [3:0] morse_len = morse_data[11:8];
wire       is_dash   = morse_data[symbol_idx];

wire [11:0] curr_char_morse = get_morse(message[char_idx]);
wire [3:0]  curr_char_len   = curr_char_morse[11:8];
wire        curr_is_space   = (message[char_idx] == " ");
wire        next_char_is_space =
    (char_idx < LAST_CHAR) && (message[char_idx + ONE_CHAR] == " ");

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        morse_state <= MS_LOAD;
        char_idx    <= {CHAR_IDX_W{1'b0}};
        morse_data  <= 12'd0;
        symbol_idx  <= 4'd0;
        morse_timer <= 32'd0;
        leds_on     <= 1'b0;
    end else begin
        case (morse_state)

            MS_LOAD: begin
                leds_on     <= 1'b0;
                morse_data  <= curr_char_morse;
                symbol_idx  <= 4'd0;
                morse_timer <= 32'd0;

                if (curr_is_space || (curr_char_len == 4'd0))
                    morse_state <= MS_WORD_GAP;
                else
                    morse_state <= MS_SYMBOL;
            end

            MS_SYMBOL: begin
                leds_on     <= 1'b1;
                morse_timer <= morse_timer + 32'd1;

                if (( is_dash && (morse_timer >= DASH_TIME - 1)) ||
                    (!is_dash && (morse_timer >= DOT_TIME  - 1))) begin
                    leds_on     <= 1'b0;
                    morse_timer <= 32'd0;

                    if ((symbol_idx + 4'd1) >= {1'b0, morse_len}) begin
                        if (char_idx >= LAST_CHAR)
                            morse_state <= MS_RESTART;
                        else if (next_char_is_space)
                            morse_state <= MS_WORD_GAP;
                        else
                            morse_state <= MS_CHAR_GAP;
                    end else begin
                        symbol_idx  <= symbol_idx + 4'd1;
                        morse_state <= MS_SYM_GAP;
                    end
                end
            end

            MS_SYM_GAP: begin
                leds_on     <= 1'b0;
                morse_timer <= morse_timer + 32'd1;

                if (morse_timer >= SYM_GAP - 1) begin
                    morse_timer <= 32'd0;
                    morse_state <= MS_SYMBOL;
                end
            end

            MS_CHAR_GAP: begin
                leds_on     <= 1'b0;
                morse_timer <= morse_timer + 32'd1;

                if (morse_timer >= CHAR_GAP - 1) begin
                    morse_timer <= 32'd0;
                    char_idx    <= char_idx + ONE_CHAR;
                    morse_state <= MS_LOAD;
                end
            end

            MS_WORD_GAP: begin
                leds_on     <= 1'b0;
                morse_timer <= morse_timer + 32'd1;

                if (morse_timer >= WORD_GAP - 1) begin
                    morse_timer <= 32'd0;

                    if (curr_is_space) begin
                        if (char_idx >= LAST_CHAR) begin
                            morse_state <= MS_RESTART;
                        end else begin
                            char_idx    <= char_idx + ONE_CHAR;
                            morse_state <= MS_LOAD;
                        end
                    end else begin
                        if (char_idx >= NEXT_LAST_CHAR) begin
                            morse_state <= MS_RESTART;
                        end else begin
                            char_idx    <= char_idx + TWO_CHARS;
                            morse_state <= MS_LOAD;
                        end
                    end
                end
            end

            MS_RESTART: begin
                leds_on     <= 1'b0;
                morse_timer <= morse_timer + 32'd1;

                if (morse_timer >= WORD_GAP - 1) begin
                    morse_timer <= 32'd0;
                    char_idx    <= {CHAR_IDX_W{1'b0}};
                    morse_state <= MS_LOAD;
                end
            end

            default: begin
                morse_state <= MS_LOAD;
                char_idx    <= {CHAR_IDX_W{1'b0}};
                morse_timer <= 32'd0;
                leds_on     <= 1'b0;
            end
        endcase
    end
end

// ========================================================================
// BUZZER DRIVER - 600 HZ SQUARE WAVE
// ========================================================================

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        buzzer_counter <= 32'd0;
        buzzer_tone    <= 1'b0;
    end else begin
        if (buzzer_counter >= (PWM_PERIOD / 2 - 1)) begin
            buzzer_counter <= 32'd0;
            buzzer_tone    <= ~buzzer_tone;
        end else begin
            buzzer_counter <= buzzer_counter + 32'd1;
        end
    end
end

assign buzzer_pwm = leds_on ? buzzer_tone : 1'b0;

// ========================================================================
// WS2812 DRIVER
// ========================================================================

localparam [15:0] T0H    = CLK_FREQ / 2_500_000;   // 0.4 us
localparam [15:0] T0L    = CLK_FREQ / 1_250_000;   // 0.8 us
localparam [15:0] T1H    = CLK_FREQ / 1_250_000;   // 0.8 us
localparam [15:0] T1L    = CLK_FREQ / 2_500_000;   // 0.4 us
localparam [15:0] TRESET = CLK_FREQ / 20_000;      // 50 us

localparam [1:0] WS_RESET = 2'd0;
localparam [1:0] WS_LOAD  = 2'd1;
localparam [1:0] WS_HIGH  = 2'd2;
localparam [1:0] WS_LOW   = 2'd3;

localparam integer LED_IDX_W = (NUM_LEDS <= 1) ? 1 : $clog2(NUM_LEDS);
localparam [LED_IDX_W-1:0] LAST_LED = NUM_LEDS - 1;

reg [1:0]             ws_state;
reg [LED_IDX_W-1:0]   led_idx;
reg [4:0]             bit_idx;
reg [23:0]            pixel_data;
reg [15:0]            timer;
reg                   data_out;

wire [23:0] current_color = leds_on ? on_color : off_color;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        ws_state   <= WS_RESET;
        led_idx    <= {LED_IDX_W{1'b0}};
        bit_idx    <= 5'd0;
        pixel_data <= 24'd0;
        timer      <= 16'd0;
        data_out   <= 1'b0;
    end else begin
        case (ws_state)

            WS_RESET: begin
                data_out <= 1'b0;
                timer    <= timer + 16'd1;
                if (timer >= (TRESET - 16'd1)) begin
                    timer    <= 16'd0;
                    led_idx  <= {LED_IDX_W{1'b0}};
                    ws_state <= WS_LOAD;
                end
            end

            WS_LOAD: begin
                pixel_data <= current_color;
                bit_idx    <= 5'd23;
                timer      <= 16'd0;
                ws_state   <= WS_HIGH;
            end

            WS_HIGH: begin
                data_out <= 1'b1;
                timer    <= timer + 16'd1;

                if (pixel_data[bit_idx]) begin
                    if (timer >= (T1H - 16'd1)) begin
                        timer    <= 16'd0;
                        ws_state <= WS_LOW;
                    end
                end else begin
                    if (timer >= (T0H - 16'd1)) begin
                        timer    <= 16'd0;
                        ws_state <= WS_LOW;
                    end
                end
            end

            WS_LOW: begin
                data_out <= 1'b0;
                timer    <= timer + 16'd1;

                if (pixel_data[bit_idx]) begin
                    if (timer >= (T1L - 16'd1)) begin
                        timer <= 16'd0;
                        if (bit_idx == 5'd0) begin
                            if (led_idx >= LAST_LED)
                                ws_state <= WS_RESET;
                            else begin
                                led_idx  <= led_idx + 1'b1;
                                ws_state <= WS_LOAD;
                            end
                        end else begin
                            bit_idx   <= bit_idx - 5'd1;
                            ws_state  <= WS_HIGH;
                        end
                    end
                end else begin
                    if (timer >= (T0L - 16'd1)) begin
                        timer <= 16'd0;
                        if (bit_idx == 5'd0) begin
                            if (led_idx >= LAST_LED)
                                ws_state <= WS_RESET;
                            else begin
                                led_idx  <= led_idx + 1'b1;
                                ws_state <= WS_LOAD;
                            end
                        end else begin
                            bit_idx   <= bit_idx - 5'd1;
                            ws_state  <= WS_HIGH;
                        end
                    end
                end
            end

            default: begin
                ws_state <= WS_RESET;
            end
        endcase
    end
end

assign led_data = data_out;

endmodule

debounce.v

// ============================================================================ // Button Debouncer Module - Lint // ============================================================================

`timescale 1ns/1ps

module debounce #( parameter integer CLK_FREQ = 50_000_000, parameter integer DEBOUNCE_MS = 20 )( input wire clk, input wire rst_n, input wire btn_raw, output reg btn_pressed );

localparam integer COUNT_MAX = (CLK_FREQ / 1000) * DEBOUNCE_MS;
localparam integer CTR_WIDTH = (COUNT_MAX <= 1) ? 1 : $clog2(COUNT_MAX + 1);
localparam [CTR_WIDTH-1:0] COUNT_MAX_M1 = COUNT_MAX - 1;

reg                 btn_sync0;
reg                 btn_sync1;
reg                 btn_state;
reg [CTR_WIDTH-1:0] counter;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        btn_sync0   <= 1'b0;
        btn_sync1   <= 1'b0;
        btn_state   <= 1'b0;
        btn_pressed <= 1'b0;
        counter     <= {CTR_WIDTH{1'b0}};
    end else begin
        btn_sync0   <= btn_raw;
        btn_sync1   <= btn_sync0;
        btn_pressed <= 1'b0;

        if (btn_sync1 == btn_state) begin
            counter <= {CTR_WIDTH{1'b0}};
        end else begin
            if (counter >= COUNT_MAX_M1) begin
                btn_state <= btn_sync1;
                counter   <= {CTR_WIDTH{1'b0}};

                if (btn_sync1)
                    btn_pressed <= 1'b1;
            end else begin
                counter <= counter + 1'b1;
            end
        end
    end
end

endmodule

Compile, Run, waveform

Class Note

0. Key Concepts: Flip-Flops and Latches

  • A flip-flop (Good) is like a light switch. It captures its input only at a specific moment — the clock edge: Predictable(Output only changes at clock edges) & Synchronous(All flip-flops update together)
  • A latch (usually bad) is “transparent” — it passes input to output whenever enabled (Unpredictable timing)

1. What is RTL?

RTL (Register-Transfer Level) describes digital circuits in terms of: Registers (flip-flops that store data), Combinational logic(transforms data between registers), Transfers (data moving) e.g. always @(posedge clk) y <= a + b;

2. Verilog Basics

Module Structure

module my_module (
    input  wire       clk,      // Clock
    input  wire       rst_n,    // Reset (active low)
    input  wire [7:0] data_in,  // 8-bit input
    output reg  [7:0] data_out  // 8-bit output
);

    // Internal signals
    wire [7:0] intermediate;    // Continuous connections
    reg  [7:0] stored;    // Store values 

    // Logic goes here...

endmodule

Number Formats

3. Combinational Logic

Combinational logic has no memory - output depends only on current inputs.

Method 1: Continuous Assignment

// Simple gates
assign y = a & b;           // AND
assign y = a | b;           // OR
assign y = a ^ b;           // XOR
assign y = ~a;              // NOT

// Arithmetic
assign sum = a + b;
assign diff = a - b;

// Conditional (mux)
assign y = sel ? a : b;     // if sel then a else b

Method 2: Always Block (combinational)

always @(*) begin           // * means "all inputs"
    case (sel)
        2'b00: y = a;
        2'b01: y = b;
        2'b10: y = c;
        2'b11: y = d;
    endcase
end

In combinational always blocks, assign to ALL outputs in ALL branches, or you’ll create a latch!

4. Sequential Logic

Sequential logic has memory - uses flip-flops triggered by a clock. Filp-Flop Pattern

5. State Machines

A Finite State Machine (FSM) is a circuit that moves between states based on inputs.

FSM Template

6. Testbenches

A testbench is Verilog code that tests your design. Testbench Template

7. Simulation & Debugging

# Compile
iverilog -o sim.vvp my_module.v my_module_tb.v

# Run
vvp sim.vvp

# View waveforms
gtkwave waves.vcd

# Debug Prints
$display("Value of x = %d", x);       // Print once
$monitor("x=%d y=%d", x, y);          // Print on any change
$time                                  // Current simulation time

# Linting (Check for common errors)
verilator --lint-only my_module.v

A self-checking testbench automatically verifies correctness instead of requiring you to manually inspect waveforms.

cocotb

8. Clock Domain Crossing

When signals cross between different clock domains, you must handle metastability or your design will fail randomly.

Synchronization Handshake Reset

9. Advanced Constructs

generate lets you create multiple instances or conditional hardware: Used in: pocket_synth.v for key synchronizers.

$clog2 - Calculate bits needed for a value: Used in: uart_tx.v, debounce.v for counter sizing.

Localparam vs Parameter

10. Common Mistakes

Unintentional Latches, Mixing Blocking/Non-Blocking, Incomplete Sensitivity List

11. Lint Warnings

Linting catches bugs

  • Recommended Resources Hardware abstraction https://github.com/siliconcompiler/lambdalib
  • EDA abstraction https://github.com/siliconcompiler/siliconcompiler
  • Open source https://github.com/aolofsson/awesome-opensource-hardware
  • Game using Verilog https://www.hackster.io/Mayukhmali_Das/lemmings-game-using-verilog-and-fpga-544cf7