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