Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

FPGA-Based Breathing LED Using PWM Control

Tech May 16 1

A breathing LED produces a gradual brightening and dimming effect by continuously varying its brightness. In digital hardware, this is accomplished through Pulse Width Modulation (PWM) where the duty cycle is swept from minimum to maximum and back. An FPGA implementation typically splits the design into a PWM generator and a brightness controller that adjusts the duty cycle over time.

PWM Signal Generation

A PWM generator can be built with a free-running counter and a compare register. The counter defines the PWM period. While counting, the output is driven low if the count value lies below the compare threshold, and high otherwise. The duty cycle changes when the compare value is updated. The following diagram illustrates this mechanism:

Hardware Setup

Four LEDs (LED0–LED3) are connected with their cathodes to the collectors of S8050 NPN transistors. The anodes are tied to 3.3V, and the transistor bases are driven by FPGA I/O pins. The FPGA's 1.5V logic level is insufficient to drive the LEDs directly, so the transistors serve as current amplifiers. A high output from the FPGA turns on the transistor, lighting the LED; a low output turns it off. Only LED0 is used in this experiment.

Architecture

The design contains two main blocks: a PWM generator and a breath controller. The breath controller computes a time-varying duty cycle and feeds it to the PWM generaotr, which produces the actual waveform that drives the LED.


RTL Implementation

PWM Generator Module

This module accepts a period value and a duty cycle input, and outputs a PWM waveform. The period is constrained to be at least 2 to guarantee proper operation. The duty cycle is translated into a comparison threshold. A cycle counter runs from 0 up to period_max - 1, wrapping to zero. While the counter is below the threshold, the output is logic 0; otherwise it is logic 1.

`timescale 1ns / 1ns

module pwm_gen
(
    input  wire        clk,
    input  wire        rst_n,
    input  wire [31:0] period_max,
    input  wire [31:0] duty_val,
    output reg         pwm_out
);

    wire [31:0] safe_period = (period_max < 2) ? 2 : period_max;
    wire [31:0] compare_th  = (duty_val < safe_period) ? (safe_period - duty_val) : 0;

    reg [31:0] tick_cnt;

    always @(posedge clk) begin
        if (!rst_n)
            tick_cnt <= 0;
        else if (tick_cnt < safe_period - 1)
            tick_cnt <= tick_cnt + 1;
        else
            tick_cnt <= 0;
    end

    always @(posedge clk) begin
        if (!rst_n)
            pwm_out <= 1'b0;
        else
            pwm_out <= (tick_cnt < compare_th) ? 1'b0 : 1'b1;
    end

endmodule

Breath Controller Module

The breath controller adjusts the duty cycle incrementally. It uses three counters:

  • A period counter that paces the duty cycle updates.
  • A step counter that tracks the number of brightness steps applied in one direction.
  • A direction flag (inc_not_dec) that toggles when the duty cycle reaches its maximum or minimum.

The brightness range is divided into BRIGHT_LEVELS discrete steps. At each update interval, the duty cycle is increased or decreased by STEP_SIZE. When step_cnt reaches BRIGHT_LEVELS - 1, the direction is reversed.

`timescale 1ns / 1ns

module breath_ctrl #(
    parameter PWM_PERIOD     = 100_000,
    parameter BREATH_CYCLE   = 100_000_000,
    parameter BRIGHT_LEVELS  = 100
) (
    input  wire        clk,
    input  wire        rst_n,
    output reg  [31:0] duty_cycle
);

    localparam STEP_SIZE   = PWM_PERIOD / BRIGHT_LEVELS;
    localparam TICK_LIMIT  = BREATH_CYCLE / BRIGHT_LEVELS;

    reg [31:0] pace_cnt;
    reg [31:0] step_cnt;
    reg        inc_not_dec;   // 1: increase, 0: decrease

    // Pacing counter
    always @(posedge clk) begin
        if (!rst_n)
            pace_cnt <= 0;
        else if (pace_cnt < TICK_LIMIT - 1)
            pace_cnt <= pace_cnt + 1;
        else
            pace_cnt <= 0;
    end

    // Duty cycle update
    always @(posedge clk) begin
        if (!rst_n)
            duty_cycle <= 0;
        else if (pace_cnt == TICK_LIMIT - 1) begin
            if (inc_not_dec && (PWM_PERIOD - duty_cycle) >= STEP_SIZE)
                duty_cycle <= duty_cycle + STEP_SIZE;
            else if (!inc_not_dec && (duty_cycle >= STEP_SIZE))
                duty_cycle <= duty_cycle - STEP_SIZE;
        end
    end

    // Step counter
    always @(posedge clk) begin
        if (!rst_n)
            step_cnt <= 0;
        else if (pace_cnt == TICK_LIMIT - 1) begin
            if (step_cnt < BRIGHT_LEVELS - 1)
                step_cnt <= step_cnt + 1;
            else
                step_cnt <= 0;
        end
    end

    // Direction control
    always @(posedge clk) begin
        if (!rst_n)
            inc_not_dec <= 1'b1;
        else if (pace_cnt == TICK_LIMIT - 1) begin
            if (step_cnt == BRIGHT_LEVELS - 1)
                inc_not_dec <= ~inc_not_dec;
        end
    end

endmodule

Top-Level Integration

The top module instantiates the breath controller and the PWM generator, wiring the dynamic duty cycle value to the PWM module. A parameterizable period, breath cycle length, and number of brightness levels allow easy tuning.

`timescale 1ns / 1ns

module top_breathing_led #(
    parameter PWM_PERIOD    = 100_000,
    parameter BREATH_CYCLE  = 100_000_000,
    parameter BRIGHT_LEVELS = 100
) (
    input  wire clk,
    input  wire rst_n,
    output wire led
);

    wire [31:0] duty_cycle;

    breath_ctrl #(
        .PWM_PERIOD    (PWM_PERIOD),
        .BREATH_CYCLE  (BREATH_CYCLE),
        .BRIGHT_LEVELS (BRIGHT_LEVELS)
    ) u_ctrl (
        .clk        (clk),
        .rst_n      (rst_n),
        .duty_cycle (duty_cycle)
    );

    pwm_gen u_pwm (
        .clk        (clk),
        .rst_n      (rst_n),
        .period_max (PWM_PERIOD),
        .duty_val   (duty_cycle),
        .pwm_out    (led)
    );

endmodule

Verification

PWM Generator Testbench

A simple testbench toggles the duty cycle through several values while monitoring the PWM output. A 10 ns clock is used.

`timescale 1ns / 1ns

module tb_pwm_gen;

    reg       clk;
    reg       rst_n;
    reg [31:0] duty_val;
    wire      pwm_wire;

    pwm_gen dut (
        .clk       (clk),
        .rst_n     (rst_n),
        .period_max(32'd10),
        .duty_val  (duty_val),
        .pwm_out   (pwm_wire)
    );

    always #10 clk = ~clk;

    initial begin
        clk = 0;
        rst_n = 0;
        duty_val = 0;
        #200 rst_n = 1;
        repeat (10) begin
            #1000 duty_val = duty_val + 1;
        end
        $finish;
    end

endmodule

Breath Controller Testbench

The breath controller is verified by observing the duty cycle waveform. The parameters are scaled down for simulation speed.

`timescale 1ns / 1ns

module tb_breath_ctrl;

    reg       clk;
    reg       rst_n;
    wire [31:0] duty_cycle;

    breath_ctrl #(
        .PWM_PERIOD   (100),
        .BREATH_CYCLE (10_000),
        .BRIGHT_LEVELS(10)
    ) dut (
        .clk       (clk),
        .rst_n     (rst_n),
        .duty_cycle(duty_cycle)
    );

    always #10 clk = ~clk;

    initial begin
        clk = 0;
        rst_n = 0;
        #200 rst_n = 1;
        #200_000 $finish;
    end

endmodule

Full System Testbench

The top-level testbench exercises the integrated design with scaled-down parameters.

`timescale 1ns / 1ns

module tb_top;

    reg  clk;
    reg  rst_n;
    wire led;

    top_breathing_led #(
        .PWM_PERIOD   (10),
        .BREATH_CYCLE (1000),
        .BRIGHT_LEVELS(10)
    ) dut (
        .clk   (clk),
        .rst_n (rst_n),
        .led   (led)
    );

    always #10 clk = ~clk;

    initial begin
        clk = 0;
        rst_n = 0;
        #200 rst_n = 1;
        #50_000 $finish;
    end

endmodule

Constraints

The design is targeted at a device with a 50 MHz input clock. Pin assignments for clock, reset, and LED0 are given in the XDC file:

# Clock constraint: 50 MHz
create_clock -period 20.000 -name sys_clk [get_ports clk]

# I/O placement
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS15} [get_ports clk]
set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS15} [get_ports rst_n]
set_property -dict {PACKAGE_PIN V9 IOSTANDARD LVCMOS15} [get_ports led]
Tags: FPGAverilog

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.