A PWM module for FPGAs that supports dead band. A VHDL project that generates two opposite PWM signals with a dead band. You can change the duty cycle with a rotary encoder. |
When you drive half-bridge designs, you need a control signal for both transistors in the circuit.
These signals need to be each other's opposite, because you close one transistor when you drive the other.
At the switching time, you introduce a tiny bit of dead time, to allow one transistor to properly shut before the other opens.
If you don't allow for this stabilisation period, your transistors will get hot and the magic smoke will eventually (sooner rather than later) escape.
I've made a VHDL module that generates these complementary signals, including a configurable deadband.
You decide in your design what the frequency and deadband is.
You can then freely change the duty cycle. The FPGA takes care that the dead time is guaranteed.
PWM VHDL module
My design is 100% based on the Xess XuLA2 PWM library. I've added the complementary signal and introduced that delay for rising edges of both outputs.
entity PwmDeadBand is port ( clk_i : in std_logic; -- Input clock. duty_i : in std_logic_vector; -- Duty-cycle input. band_i : in natural; -- number of clock-ticks to keep both signals low before rising edge pwmA_o : out std_logic; -- PWM output. pwmB_o : out std_logic -- PWM output inverse. ); end entity;
duty_i is a register that holds the desired duty cycle. It's configurable - I've set it to 8 bits.
Your clock speed is dependent on the size of this register. The clock that you present on the clk_i input of the module will be divided by 2^(number of bits). In this case, 2^8 -> 256. For the standard 12 MHz clock of the XuLA2, the PWM module will beat at 47 kHz. There are ways to increase the clock frequency in the FPGA and the XuLA2 libs have support for that. |
The dead band, in clock ticks, is passed via the band_i pin. The two complementary signals appear on pwmA_o and pwmB_o.
In the constraint file of your project, you assign that to physical Spartan-6 pins:
# PM1 connections for the pwm outputs net pwmA_o loc=m16; net pwmB_o loc=k16;
If you use a StickIt! motherboard, you get the signals at PM1, pin D4 and D6.
If you tap them from the XuLA2 directly, they are chan4 and chan6 on the expansion header.
The implementation is just an extension of what the original Xess library does.
We introduce an additional channel that's HI when the other is LO,, and vice versa.
And we hold off driving any of these channels high until we've waited duty_i clock ticks.
architecture arch of PwmDeadBand is constant MAX_DUTY_C : std_logic_vector(duty_i'range) := (duty_i'range => ONE); signal timer_r : natural range 0 to 2**duty_i'length-1; begin process(clk_i) begin if rising_edge(clk_i) then pwmA_o <= LO; timer_r <= timer_r + 1; if timer_r >= band_i + TO_INTEGER(unsigned(duty_i)) then pwmB_o <= HI; end if; if timer_r < TO_INTEGER(unsigned(duty_i)) then pwmB_o <= LO; if timer_r >= band_i then pwmA_o <= HI; end if; end if; end if; end process; end architecture;
That's really all for the PWM module. (afterthought in 2021: the line 02 may not be needed. MAX_DUTY_C is not used and the timer naturally iterates from 0 to maximum in this design)
Rotary Encoder module
This section is very short: read the previous blog post.
Patching it together
Also easy. We just have to wire the register that holds the value of the Rotary Encoder to the one that's driving the PWM module.
I just use the same register. That's the simplest way to do this.
entity Rotary_Pwm is Port ( clk_i : in STD_LOGIC; rotEncA_i : in std_logic; -- Rotary encoder phase 1 output. rotEncB_i : in std_logic; -- Rotary encoder phase 2 output. pwmA_o : out STD_LOGIC; pwmB_o : out STD_LOGIC ); end Rotary_Pwm; architecture Behavioral of Rotary_Pwm is signal accumulator_s : std_logic_vector(7 downto 0) := "01111111"; -- 50% begin u0 : PwmDeadBand port map ( clk_i => clk_i, duty_i => accumulator_s, band_i => 16, pwmA_o => pwmA_o, pwmB_o => pwmB_o ); u1 : RotaryEncoderWithCounter generic map (ALLOW_ROLLOVER_G => true, INITIAL_CNT_G => 127) port map ( clk_i => clk_i, a_i => rotEncA_i, b_i => rotEncB_i, cnt_o => accumulator_s ); end Behavioral;
So the handover point is the 8-bit register accumulator_s. When you turn the encoder, it changes the value of the register to reflect your action.
In real-time, the duty cycle of the PWM module adapts. There's not a single clock tick between event and action.
Connections
I've used PM1 on the StickIt! motherboard, and used these pins:
PM1 pin | XuLA2 pin | Spartan-6 pin | pin name | direction | function |
---|---|---|---|---|---|
6 - +3V3 | +3.3V | +3.3V | out | pull-up power for encoder | |
5 - GND | GND | GND | out | ground for PWM and Rotary Encoder | |
4 - D6 | CHAN6 | K16 | pwmB_o | out | PWM complementary signal B |
3 - D4 | CHAN4 | M16 | pwmA_o | out | PWM complementary signal A |
2 - D2 | CHAN2 | R16 | rotEncB_i | in | Rotary Encoder pin B |
1 - D0 | CHAN0 | R7 | rotEncA_i | in | Rotary Encoder pin A |
If you want to wire the encoder inputs or pwm outputs to other pins, you only have to change the constraint file.
The project is attached, together with the PWM library.
I'm going to use this to drive my GaN experiment board. You?
Top Comments