fpga-pwm-control-intensidad-servomotor

PWM on FPGA: LED and Servo Motor Control

  • 6 min

A PWM signal is a periodic digital wave whose high time we can vary to control the energy delivered to a load.

We have seen how to make binary (digital) signals. But what if we want the LED to shine at 50%? Or if we want to move a robotic arm to a specific 45-degree position?

To regulate an LED or a servo, we don’t need to generate a continuous analog voltage. We can use PWM (Pulse Width Modulation) from a digital output.

Today we are going to design our own PWM generator from scratch in Verilog.

What is PWM?

The concept is to trick the eye (or the motor) through speed. If we turn a signal on and off very quickly, the physical world perceives the average energy.

  • Period (T): The total time of one cycle (on + off).
  • Frequency (f = 1/T): How many cycles occur per second.
  • Duty Cycle: The percentage of time the signal is HIGH (1).
  • If the Duty Cycle is 100%, we have a constant 3.3V (Maximum brightness).
  • If the Duty Cycle is 50%, the output spends half the time at 3.3V and the other half at 0V. A slow load or a filter perceives an average value of approximately 1.65V.
  • If the Duty Cycle is 0%, we have 0V (Off).

On Arduino you use analogWrite(pin, 128).

On an FPGA, we build the circuit that performs that analogWrite. We can generate many channels in parallel, limited by the available pins, logic, and timing.

Hardware Implementation

Generating a PWM on an FPGA is incredibly simple. We only need two pieces we already know:

  1. A Counter that continuously increases (sawtooth wave).
  2. A Comparator that decides whether the output is 0 or 1.

The logic is:

“While the counter value is LESS than my desired value (Duty), keep the output at 1. If it exceeds it, set it to 0.”

Generic PWM Generator

Let’s create a parameterizable module so we can reuse it.

module pwm_generator #(
    parameter N_BITS = 8 // 8-bit resolution (0-255)
)(
    input wire clk,
    input wire rst,
    input wire [N_BITS-1 : 0] duty, // Desired "power" value
    output reg pwm_out
);

    reg [N_BITS-1 : 0] counter = 0;

    always @(posedge clk) begin
        if (rst) begin
            counter <= 0;
            pwm_out  <= 0;
        end else begin
            // 1. Increment the counter (Automatic overflow when reaching 255)
            counter <= counter + 1;

            // 2. Comparator
            if (counter < duty)
                pwm_out <= 1'b1;
            else
                pwm_out <= 1'b0;
        end
    end

endmodule
Copied!

Breathing Effect for an LED

Let’s test our module. We will make an LED smoothly increase and decrease its intensity, like the sleep light on a Mac.

To do this, we need to instantiate our pwm_generator and feed its duty input with another slower counter that goes up and down (a triangular wave).

module breathing_led (
    input wire clk,      // 12 MHz
    output wire led
);

    // 1. Slow counter to vary the intensity
    // It needs to be slow so the eye can see the change.
    // We'll use a large counter and take the upper bits.
    reg [23:0] slow_counter = 0;
    
    always @(posedge clk) begin
        slow_counter <= slow_counter + 1;
    end

    // Trick: Use the high bits of the slow counter as the duty value.
    // To make a ramp up/down effect (triangular) requires a bit more logic,
    // but for a sawtooth effect (ramp up and snap off) this works directly:
    wire [7:0] brightness_level = slow_counter[23:16];

    // 2. PWM Instance
    pwm_generator #( .N_BITS(8) ) my_pwm (
        .clk(clk),
        .rst(1'b0),
        .duty(brightness_level),
        .pwm_out(led)
    );

endmodule
Copied!

PWM Frequency:

With a 12 MHz clock and 8 bits (256 steps), the frequency is 12,000,000 / 256 = 46,875 Hz. This is much faster than the eye can discern and is suitable for dimming an LED.

Servo Motor Control

Hobby servomotors (those little blue SG90 motors) are controlled by PWM, but they are very demanding regarding timing. They don’t work with just any frequency.

Many hobby servos use approximately:

  1. Period: Around 20 ms (50 Hz).
  2. High Pulse (t_on): Typically lasts between 1 ms and 2 ms.
    • 1.0 ms: one extreme.
    • 1.5 ms: center position.
    • 2.0 ms: the other extreme.

The Mathematical Challenge

Our clock runs at 12 MHz. A 20 ms period contains 12,000,000 × 0.020 = 240,000 cycles, so we need a counter that goes from 0 to 239,999. And we need to set the output to 1 only during the first X cycles:

  • For center (1.5 ms): 12,000,000 × 0.0015 = 18,000 cycles.

Actual timings and angles depend on each servo. Consult its datasheet before approaching the mechanical limits.

Servo Driver in Verilog

Let’s create a specific module, because the simple pwm_generator we made earlier (based on natural overflow) doesn’t give us the exact control over the 50 Hz frequency.

module servo_driver (
    input wire clk,             // 12 MHz
    input wire [7:0] position,  // Input from 0 to 255 (0 = 1ms, 255 = 2ms)
    output reg servo_pin
);

    // Physical parameters
    localparam CLK_HZ = 12000000;
    localparam PRESCALER_MAX = 240000; // 20ms at 12MHz

    // Pulse limits
    localparam PULSE_MIN = 12000; // 1ms (12000 cycles)
    localparam PULSE_MAX = 24000; // 2ms (24000 cycles)
    
    // Main period counter (0 to 239,999)
    reg [17:0] period_counter = 0;

    // Calculate the desired pulse width
    // Map (0-255) to the range (12000-24000)
    // Formula: width = MIN + (position * (MAX-MIN) / 255)
    wire [17:0] pulse_width;
    assign pulse_width = PULSE_MIN + (position * (PULSE_MAX - PULSE_MIN) / 255);

    always @(posedge clk) begin
        // 1. Period Control (50Hz)
        if (period_counter == PRESCALER_MAX - 1) begin
            period_counter <= 0;
        end else begin
            period_counter <= period_counter + 1;
        end

        // 2. Pulse Generation
        if (period_counter < pulse_width) begin
            servo_pin <= 1'b1;
        end else begin
            servo_pin <= 1'b0;
        end
    end

endmodule
Copied!

We have just created a hardware driver whose period does not depend on a software loop. Its precision is limited by the clock, integer rounding, and output timing, but other modules in the design do not delay the pulse as long as timing constraints are met.