In [1]:
from IPython.core.display import HTML
HTML(open('../css/custom.css', 'r').read())
Out[1]:
Note: If you're reading this as a static HTML page, you can also get it as an executable Jupyter notebook here.
Pulse width modulators (PWMs) output a repetitive waveform that is high for a set percentage of the interval and low for the remainder. One of their uses is to generate a quasi-analog signal using only a digital output pin. This makes them useful for doing things like varying the brightness of an LED by adjusting the amount of time the signal is high and the LED is on.
If you've used PWMs before on a microcontroller, you know what a headache it is to set all the control bits to select the clock source, pulse durations, and so forth. Surprisingly, PWMs are actually a bit easier with FPGAs. Let's take a look.
A very simple PWM consists of a counter and a comparator. The counter is incremented by a clock signal and its value is compared to a threshold. When the counter is less than the threshold, the PWM output is high, otherwise it's low. So the higher the threshold, the longer the output is on. This on-off pulsing repeats every time the counter rolls over and begins again at zero.
Here's the MyHDL code for a simple PWM:
In [2]:
from pygmyhdl import *
@chunk
def pwm_simple(clk_i, pwm_o, threshold):
'''
Inputs:
clk_i: PWM changes state on the rising edge of this clock input.
threshold: Bit-length determines counter width and value determines when output goes low.
Outputs:
pwm_o: PWM output starts and stays high until counter > threshold and then output goes low.
'''
cnt = Bus(len(threshold), name='cnt') # Create a counter with the same number of bits as the threshold.
# Here's the sequential logic for incrementing the counter. We've seen this before!
@seq_logic(clk_i.posedge)
def cntr_logic():
cnt.next = cnt + 1
# Combinational logic that drives the PWM output high when the counter is less than the threshold.
@comb_logic
def output_logic():
pwm_o.next = cnt < threshold # cnt<threshold evaluates to either True (1) or False (0).
Now I'll simulate it:
In [3]:
initialize()
# Create signals and attach them to the PWM.
clk = Wire(name='clk')
pwm = Wire(name='pwm')
threshold = Bus(3, init_val=3) # Use a 3-bit threshold with a value of 3.
pwm_simple(clk, pwm, threshold)
# Pulse the clock and look at the PWM output.
clk_sim(clk, num_cycles=24)
show_waveforms(start_time=13, tock=True)
The simulation was set up with a three-bit threshold having a value of three. The counter has the same number of bits as the threshold, so it will cycle over the values 0, 1, 2, $\ldots$, 6, 7, 0, 1, $\ldots$. From the waveforms, you can see the PWM output is on for three out of every eight clock cycles.
One characteristic of this PWM is the total pulse duration (both on and off portions) is restricted to being a power-of-two clock cycles. The next PWM will remove that limitation.
We can make the PWM more general with allowable intervals that are not powers of 2 by adding another comparator. This comparator watches the counter value and rolls it back to zero once it reaches a given value. The comparator is implemented with a small addition to the sequential logic of the simple PWM as follows:
In [4]:
@chunk
def pwm_less_simple(clk_i, pwm_o, threshold, duration):
'''
Inputs:
clk_i: PWM changes state on the rising edge of this clock input.
threshold: Determines when output goes low.
duration: The length of the total pulse duration as determined by the counter.
Outputs:
pwm_o: PWM output starts and stays high until counter > threshold and then output goes low.
'''
# The log2 of the pulse duration determines the number of bits needed
# in the counter. The log2 value is rounded up to the next integer value.
import math
length = math.ceil(math.log(duration, 2))
cnt = Bus(length, name='cnt')
# Augment the counter with a comparator to adjust the pulse duration.
@seq_logic(clk_i.posedge)
def cntr_logic():
cnt.next = cnt + 1
# Reset the counter to zero once it reaches one less than the desired duration.
# So if the duration is 3, the counter will count 0, 1, 2, 0, 1, 2...
if cnt == duration-1:
cnt.next = 0
@comb_logic
def output_logic():
pwm_o.next = cnt < threshold
Now test the PWM with a non-power of 2 interval:
In [5]:
initialize()
clk = Wire(name='clk')
pwm = Wire(name='pwm')
pwm_less_simple(clk, pwm, threshold=3, duration=5)
clk_sim(clk, num_cycles=15)
show_waveforms()
The simulation shows the PWM pulse duration is five clock cycles with the output being high for three cycles and low for the other two.
But there's a problem if the threshold is changed while the PWM is operating. This can cause glitches under the wrong conditions. To demonstrate this, a PWM with a pulse duration of ten cycles will have its threshold changed from three to eight during the middle of a pulse by the simulation test bench shown below:
In [6]:
initialize()
clk = Wire(name='clk')
pwm = Wire(name='pwm')
threshold = Bus(4, name='threshold')
pwm_less_simple(clk, pwm, threshold, 10)
def test_bench(num_cycles):
clk.next = 0
threshold.next = 3 # Start with threshold of 3.
yield delay(1)
for cycle in range(num_cycles):
clk.next = 0
# Raise the threshold to 8 after 15 cycles.
if cycle >= 14:
threshold.next = 8
yield delay(1)
clk.next = 1
yield delay(1)
# Simulate for 20 clocks and show a specific section of the waveforms.
simulate(test_bench(20))
show_waveforms(tick=True, start_time=19)
At time $t = 20$, a pulse begins. The PWM output is high for three clock cycles until $t = 26$ and then goes low. At $t = 29$, the threshold increases from 3 to 8, exceeding the current counter value. This makes the PWM output go high again and it stays there until the counter reaches the new threshold. So there is a glitch from $t = 29$ to $t = 36$.
It's usually the case that every new problem can be fixed by adding a bit more hardware. This case is no different.
The glitch in the last PWM could have been avoided if we didn't allow the thresold to change willy-nilly during a pulse. We can prevent this by adding a register that stores the threshold and doesn't allow it to change until the current pulse ends and a new one begins.
In [7]:
@chunk
def pwm_glitchless(clk_i, pwm_o, threshold, interval):
import math
length = math.ceil(math.log(interval, 2))
cnt = Bus(length)
threshold_r = Bus(length, name='threshold_r') # Create a register to hold the threshold value.
@seq_logic(clk_i.posedge)
def cntr_logic():
cnt.next = cnt + 1
if cnt == interval-1:
cnt.next = 0
threshold_r.next = threshold # The threshold only changes at the end of a pulse.
@comb_logic
def output_logic():
pwm_o.next = cnt < threshold_r
Now we can test it using the previous test bench:
In [8]:
initialize()
clk = Wire(name='clk')
pwm = Wire(name='pwm')
threshold = Bus(4, name='threshold')
pwm_glitchless(clk, pwm, threshold, 10)
simulate(test_bench(22))
show_waveforms(tick=True, start_time=19)
See? No more glitches! As before, the threshold changes at $t = 29$ but the threshold register doesn't change until $t = 40$ when the pulse ends.
So which one of these PWMs is "better"? The metric I'll use here is the amount of FPGA resources each one uses.
First, I'll synthesize and compile the simple PWM with an eight-bit threshold:
In [9]:
threshold = Bus(8)
toVerilog(pwm_simple, clk, pwm, threshold)
!yosys -q -p "synth_ice40 -blif pwm_simple.blif" pwm_simple.v
!arachne-pnr -d 1k pwm_simple.blif -o pwm_simple.asc
Looking at the stats, the simple PWM uses eight D flip-flops (3 DFF
and 5 CARRY,DFF
) which
is expected when using an eight-bit threshold.
It also uses sixteen carry circuits (10 CARRY
, 5 CARRY,DFF
and 1 CARRY PASS
) since the counter
and the comparator are both eight bits wide and each bit needs a carry circuit.
So this all looks reasonable.
In total, the simple PWM uses 32 logic cells.
For the PWM with non power-of-two pulse duration, I'll use the same eight-bit threshold but set the duration to 227:
In [10]:
toVerilog(pwm_less_simple, clk, pwm, threshold, 227)
!yosys -q -p "synth_ice40 -blif pwm_less_simple.blif" pwm_less_simple.v
!arachne-pnr -d 1k pwm_less_simple.blif -o pwm_less_simple.asc
Note that the number of D flip-flops and carry circuits has stayed the same, but the total number of logic cells consumed has risen to 36. This PWM performs an additional equality comparison of the counter and the pulse duration input, both of which are eight bits wide for a total of sixteen bits. This computation can be done with four 4-input LUTs (plus another LUT to combine the outputs), so an increase of four logic cells is reasonable.
Finally, here are the stats for the glitchless PWM:
In [11]:
toVerilog(pwm_glitchless, clk, pwm, threshold, 227)
!yosys -q -p "synth_ice40 -blif pwm_glitchless.blif" pwm_glitchless.v
!arachne-pnr -d 1k pwm_glitchless.blif -o pwm_glitchless.asc
The glitchless PWM adds another eight-bit register to store the threshold, so the total
number of flip-flops has risen to sixteen (11 DFF
and 5 CARRY,DFF
).
Because some of these flip-flops can be combined into logic cells which weren't using
their DFFs, the total number of logic cells consumed only rises by five to 41.
So the final tally is:
PWM Type | LCs | DFFs | Carrys |
---|---|---|---|
Simple | 32 | 8 | 16 |
Non $2^N$ | 36 | 8 | 16 |
Glitchless | 41 | 16 | 16 |
Resource usage is only one metric that affects your choice of a PWM. For some non-precision applications, such as varying the intensity of an LED's brightness, a simple PWM is fine and will save space in the FPGA. For more demanding applications, like motor control, you might opt for the glitchless PWM and take the hit on the number of LCs consumed.
It would be a shame to go through all this work and then never do anything fun with it! Let's make a demo that gradually brightens and darkens an LED on the iCEstick board (instead of snapping it on and off like our previous examples).
The basic idea is to generate a triangular ramp using a counter that repetitively increments from 0 to $N$ and then decrements back to 0. Then connect the upper bits of the counter to the threshold input of a simple PWM and connect an LED to the output. As the counter ramps up and down, the threshold will increase and decrease and the LED intensity will wax and wane.
Here's the sequential logic for the ramp generator:
In [12]:
@chunk
def ramp(clk_i, ramp_o):
'''
Inputs:
clk_i: Clock input.
Outputs:
ramp_o: Multi-bit amplitude of ramp.
'''
# Delta is the increment (+1) or decrement (-1) for the counter.
delta = Bus(len(ramp_o))
@seq_logic(clk_i.posedge)
def logic():
# Add delta to the current ramp value to get the next ramp value.
ramp_o.next = ramp_o + delta
# When the ramp reaches the bottom, set delta to +1 to start back up the ramp.
if ramp_o == 1:
delta.next = 1
# When the ramp reaches the top, set delta to -1 to start back down the ramp.
elif ramp_o == ramp_o.max-2:
delta.next = -1
# After configuring the FPGA, the delta register is set to zero.
# Set it to +1 and set the ramp value to +1 to get things going.
elif delta == 0:
delta.next = 1
ramp_o.next = 1
You might ask why delta
is changed from -1 to +1 when ramp_o
is one instead of waiting until it is zero.
The reason is that the new value of delta
doesn't take effect until the next clock cycle.
Therefore, if ramp_o
was allowed to hit 0 before making the change,
the current -1 value in delta
would decrement ramp_o
to -1.
This would correspond to a positive value of 255 (maximum
intensity for the LED) if ramp_o
was eight bits wide.
Changing delta
one cycle early prevents this mishap.
The same logic applies at the other end of the ramp as well (i.e., flip delta
when the
counter is just below its maximum value).
When the bitstream is loaded, the FPGA also clears all its registers.
This means delta
and ramp_o
will both be zero with the result that nothing will happen.
Therefore, if delta
is ever seen to be zero, then delta
and ramp_o
will both
be set to values that will get the triangle ramp going.
The previous two paragraphs illustrate an important principle: logic can be a tricky thing. At times, it appears to defy logic. But it could never do that. Because it's logic.
With the ramp generator done, I can combine it with a simple PWM to complete the LED wax-wane demo:
In [13]:
@chunk
def wax_wane(clk_i, led_o, length):
rampout = Bus(length, name='ramp') # Create the triangle ramp counter register.
ramp(clk_i, rampout) # Generate the ramp.
pwm_simple(clk_i, led_o, rampout.o[length:length-4]) # Use the upper 4 ramp bits to drive the PWM threshold
Now I'll simulate it using a six-bit ramp counter:
In [14]:
initialize()
clk = Wire(name='clk')
led = Wire(name='led')
wax_wane(clk, led, 6) # Set ramp counter to 6 bits: 0, 1, 2, ..., 61, 62, 63, 62, 61, ..., 2, 1, 0, ...
clk_sim(clk, num_cycles=180)
t = 110 # Look in the middle of the simulation to see if anything is happening.
show_waveforms(tick=True, start_time=t, stop_time=t+40)
The simulation doesn't show us much, but there are two items worth mentioning:
0x3F
) and then starts back down.At this point, I could do more simulation in an attempt to get more information. But for a non-critical application like this demo, I'll just go build it and see what happens!
First, I'll generate the Verilog from the MyHDL code. The ramp counter has to be wide enough to ensure I can see the LED wax-wane cycle. If I set the counter width to 23 bits, it will take $2^{23}$ cycles of the 12 MHz clock to go from zero to its maximum value. Then it will take the same number of cycles to go back to zero. This translates into $2^{24} \textrm{ / 12,000,000 Hz} = $ 1.4 seconds. That seems good.
In [15]:
toVerilog(wax_wane, clk, led, 23)
Out[15]:
Next, I have to assign some pins for the clock input and the LED output (LED D1):
In [16]:
with open('wax_wane.pcf', 'w') as pcf:
pcf.write(
'''
set_io clk_i 21
set_io led_o 99
'''
)
Finally, I can compile the Verilog code and pin assignments into a bitstream and download it into the iCEstick:
In [17]:
!yosys -q -p "synth_ice40 -blif wax_wane.blif" wax_wane.v
!arachne-pnr -q -d 1k -p wax_wane.pcf wax_wane.blif -o wax_wane.asc
!icepack wax_wane.asc wax_wane.bin
!iceprog wax_wane.bin
After the download is complete, you should be rewarded with a waxing/waning LED display:
Just to recap: