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.

PWM

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 Simple PWM

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)


<class 'myhdl.StopSimulation'>: No more events

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.

A Less-Simple PWM

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()


<class 'myhdl.StopSimulation'>: No more events

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)


<class 'myhdl.StopSimulation'>: No more events

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.

A Glitch-less PWM

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)


<class 'myhdl.StopSimulation'>: No more events

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.

Which is Better?

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


c:\python35-32\lib\site-packages\ipykernel_launcher.py:2: UserWarning: 
    toVerilog(): Deprecated usage: See http://dev.myhdl.org/meps/mep-114.html
  
seed: 1
device: 1k
read_chipdb +/chipdb-1k.bin...
  supported packages: cb121, cb132, cb81, cm121, cm36, cm49, cm81, qn84, swg16tr, tq144, vq100
read_blif pwm_simple.blif...
prune...
instantiate_io...
pack...

After packing:
IOs          10 / 96
GBs          0 / 8
  GB_IOs     0 / 8
LCs          32 / 1280
  DFF        3
  CARRY      10
  CARRY, DFF 5
  DFF PASS   1
  CARRY PASS 1
BRAMs        0 / 16
WARMBOOTs    0 / 1
PLLs         0 / 1

place_constraints...
promote_globals...
  promoted clk_i$2, 8 / 8
  promoted 1 nets
    1 clk
  1 globals
    1 clk
realize_constants...
  realized 1
place...
  initial wire length = 649
  at iteration #50: temp = 6.39439, wire length = 287
  at iteration #100: temp = 3.11838, wire length = 199
  at iteration #150: temp = 0.226164, wire length = 96
  final wire length = 93

After placement:
PIOs       6 / 96
PLBs       7 / 160
BRAMs      0 / 16

  place time 0.13s
route...
  pass 1, 0 shared.

After routing:
span_4     28 / 6944
span_12    11 / 1440

  route time 0.04s
write_txt 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


c:\python35-32\lib\site-packages\ipykernel_launcher.py:1: UserWarning: 
    toVerilog(): Deprecated usage: See http://dev.myhdl.org/meps/mep-114.html
  """Entry point for launching an IPython kernel.
seed: 1
device: 1k
read_chipdb +/chipdb-1k.bin...
  supported packages: cb121, cb132, cb81, cm121, cm36, cm49, cm81, qn84, swg16tr, tq144, vq100
read_blif pwm_less_simple.blif...
prune...
instantiate_io...
pack...

After packing:
IOs          10 / 96
GBs          0 / 8
  GB_IOs     0 / 8
LCs          36 / 1280
  DFF        3
  CARRY      10
  CARRY, DFF 5
  DFF PASS   1
  CARRY PASS 1
BRAMs        0 / 16
WARMBOOTs    0 / 1
PLLs         0 / 1

place_constraints...
promote_globals...
  promoted clk_i$2, 8 / 8
  promoted $abc$247$n1, 9 / 9
  promoted 2 nets
    1 sr/we
    1 clk
  2 globals
    1 sr/we
    1 clk
realize_constants...
  realized 1
place...
  initial wire length = 663
  at iteration #50: temp = 8.65406, wire length = 289
  at iteration #100: temp = 3.09376, wire length = 200
  at iteration #150: temp = 0.786488, wire length = 118
  final wire length = 96

After placement:
PIOs       7 / 96
PLBs       7 / 160
BRAMs      0 / 16

  place time 0.17s
route...
  pass 1, 0 shared.

After routing:
span_4     30 / 6944
span_12    15 / 1440

  route time 0.05s
write_txt 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


c:\python35-32\lib\site-packages\ipykernel_launcher.py:1: UserWarning: 
    toVerilog(): Deprecated usage: See http://dev.myhdl.org/meps/mep-114.html
  """Entry point for launching an IPython kernel.
seed: 1
device: 1k
read_chipdb +/chipdb-1k.bin...
  supported packages: cb121, cb132, cb81, cm121, cm36, cm49, cm81, qn84, swg16tr, tq144, vq100
read_blif pwm_glitchless.blif...
prune...
instantiate_io...
pack...

After packing:
IOs          10 / 96
GBs          0 / 8
  GB_IOs     0 / 8
LCs          41 / 1280
  DFF        11
  CARRY      10
  CARRY, DFF 5
  DFF PASS   8
  CARRY PASS 2
BRAMs        0 / 16
WARMBOOTs    0 / 1
PLLs         0 / 1

place_constraints...
promote_globals...
  promoted clk_i$2, 16 / 16
  promoted $abc$305$n3, 8 / 8
  promoted $abc$305$n1, 8 / 8
  promoted 3 nets
    1 sr/we
    1 cen/wclke
    1 clk
  3 globals
    1 sr/we
    1 cen/wclke
    1 clk
realize_constants...
  realized 1
place...
  initial wire length = 481
  at iteration #50: temp = 7.78865, wire length = 287
  at iteration #100: temp = 3.25659, wire length = 189
  at iteration #150: temp = 0.747164, wire length = 106
  at iteration #200: temp = 0.000924944, wire length = 82
  final wire length = 82

After placement:
PIOs       7 / 96
PLBs       15 / 160
BRAMs      0 / 16

  place time 0.17s
route...
  pass 1, 0 shared.

After routing:
span_4     16 / 6944
span_12    12 / 1440

  route time 0.03s
write_txt 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.

Demo Time!

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)


<class 'myhdl.StopSimulation'>: No more events

The simulation doesn't show us much, but there are two items worth mentioning:

  1. The triangle ramp increments to its maximum value (0x3F) and then starts back down.
  2. The PWM output appears to be outputing its maximum level (it's on for 15 of its 16 cycles).

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)


c:\python35-32\lib\site-packages\ipykernel_launcher.py:1: UserWarning: 
    toVerilog(): Deprecated usage: See http://dev.myhdl.org/meps/mep-114.html
  """Entry point for launching an IPython kernel.
Out[15]:
[[<myhdl._always_seq._AlwaysSeq at 0x5aa1e30>],
 [<myhdl._always_comb._AlwaysComb at 0x5aa1db0>,
  <myhdl._always_seq._AlwaysSeq at 0x5aa1f50>]]

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


init..
cdone: high
reset..
cdone: low
flash ID: 0x20 0xBA 0x16 0x10 0x00 0x00 0x23 0x12 0x67 0x21 0x13 0x00 0x49 0x00 0x34 0x04 0x11 0x11 0x20 0x31
file size: 32220
erase 64kB sector at 0x000000..
programming..
reading..
VERIFY OK
cdone: high
Bye.

After the download is complete, you should be rewarded with a waxing/waning LED display:

Summary

Just to recap:

  • We found out what PWMs are.
  • We designed a few different types.
  • We simulated them.
  • We saw how much of the FPGA each type uses.
  • We built a demo using a PWM to drive an LED at varying intensities.
  • We admired our demo and congratulated ourselves on a day well spent.