Skip to content

Session 5: RTL Design & Verification

Assignment 1 — Write Verilog for the core module

The core module is pwm_channel.v — a single PWM channel that takes a rotary encoder input and generates a PWM signal to control LED brightness.

The module has three internal blocks:

  • Encoder reader — detects rising edges on enc_a, increments or decrements the duty cycle register depending on enc_b direction
  • 8-bit counter — counts from 0 to 255 continuously
  • Comparator — sets pwm_out HIGH when counter < duty value

The duty cycle starts at 128 (50% brightness) after reset. Turning the encoder clockwise increases brightness, counter-clockwise decreases it.

module pwm_channel (
  input  clk, rst_n,
  input  enc_a, enc_b,
  output reg pwm_out
);
  reg [7:0] counter, duty;
  reg enc_a_prev;

  // Encoder reader
  always @(posedge clk) begin
    if (!rst_n) duty <= 8'd128;
    else if (enc_a && !enc_a_prev)
      duty <= enc_b ? duty + 1 : duty - 1;
    enc_a_prev <= enc_a;
  end

  // 8-bit counter
  always @(posedge clk)
    counter <= counter + 1;

  // PWM comparator
  always @(*)
    pwm_out = (counter < duty);
endmodule

iverilog compile OK

Assignment 2 — Create a top-level wrapper

rgb_top.v instantiates three pwm_channel modules — one for each color (Red, Green, Blue). Each channel receives its own encoder inputs and produces its own PWM output. All three share the same clock and reset.

module rgb_top (
  input  clk, rst_n,
  input  enc_r_a, enc_r_b,
  input  enc_g_a, enc_g_b,
  input  enc_b_a, enc_b_b,
  output pwm_r, pwm_g, pwm_b
);
  pwm_channel ch_red   (.clk(clk), .rst_n(rst_n), .enc_a(enc_r_a), .enc_b(enc_r_b), .pwm_out(pwm_r));
  pwm_channel ch_green (.clk(clk), .rst_n(rst_n), .enc_a(enc_g_a), .enc_b(enc_g_b), .pwm_out(pwm_g));
  pwm_channel ch_blue  (.clk(clk), .rst_n(rst_n), .enc_a(enc_b_a), .enc_b(enc_b_b), .pwm_out(pwm_b));
endmodule

iverilog top-level compile OK

Assignment 3 — Simulate with testbench and examine waveforms

I wrote a testbench rgb_top_tb.v that applies reset, then simulates encoder turns for the red and green channels. Compiled and ran with iverilog and vvp, then opened waveforms in GTKWave.

iverilog -o rgb_sim rgb_top_tb.v rgb_top.v pwm_channel.v && vvp rgb_sim
gtkwave rgb_waves.vcd

The waveforms show:

  • rst_n goes HIGH after 40 ns — reset works correctly
  • pwm_r, pwm_g, pwm_b all show PWM signals at 50% duty cycle (duty=128 out of 255)

GTKWave — clk, rst_n, pwm_r, pwm_g, pwm_b

Assignment 4 — Run linter and fix warnings

verilator --lint-only -Wall pwm_channel.v

Result: zero warnings. The code is clean.

Verilator — 0 warnings

What I learned

Writing RTL is closer to describing hardware behavior than writing software. There are no loops that execute over time — everything is either combinational (instant) or sequential (triggered by a clock edge).

The 8-bit counter wrapping from 255 back to 0 is not a bug — it is how PWM works. The duty cycle value stays fixed while the counter cycles continuously. The ratio of time spent HIGH vs LOW is the brightness.

Three identical modules sharing a clock is the simplest form of parallelism in hardware. All three channels update simultaneously on every clock edge — something that would require threads or interrupts in software.