First FPGA Project - Getting Fancy with PWM
Creating a Wave
We now need to get all eight LEDs pulsing out of sync with each other to make the wave effect. We can do this by setting the initial value of the counter to something different for each one.
DFFs have a parameter named INIT. This parameter allows us to set a value that will be assigned to the DFF upon configuration of the FPGA or reset if the DFF has a reset signal. By default, INIT is set to 0.
We can create a parameter in our pulse module called INITIAL_VALUE and pass this along to the DFF.
module pulse #(
INITIAL_VALUE = 0 : INITIAL_VALUE >= 0 && INITIAL_VALUE < $pow(2,9)
)(
input clk, // clock
input rst, // reset
output led // output to led
) {
.clk(clk) {
.rst(rst) {
pwm pwm(#WIDTH(8), #DIV(11)); // PWM component
dff ctr[27](#INIT(c{INITIAL_VALUE, 18b0})); // counter
}
}
always {
led = pwm.pulse; // connect the PWM output to the led
ctr.d = ctr.q + 1; // increment the counter
pwm.update = 1; // always update
// connect the triangular waveform to the PWM module
pwm.value = ctr.q[ctr.WIDTH-2-:pwm.value.WIDTH] ^ pwm.value.WIDTHx{ctr.q[ctr.WIDTH-1]};
}
}
Since we are only using 9 bits to determine the value of the PWM duty cycle, I made the INITIAL_VALUE parameter only accept 9 bit values. I then had to pad the value with 18 0s to make it the full 27 bits wide of our counter.
We want the offsets to be evenly spaced across the eight LEDs so each one should be 512/8 = 64 apart.
We can modify the top-level module to instantiate eight of these with different INITIAL_VALUEs.
module au_top (
input clk, // 100MHz clock
input rst_n, // reset button (active low)
output led [8], // 8 user controllable LEDs
input usb_rx, // USB->Serial input
output usb_tx // USB->Serial output
) {
sig rst; // reset signal
.clk(clk) {
// The reset conditioner is used to synchronize the reset signal to the FPGA
// clock. This ensures the entire FPGA comes out of reset at the same time.
reset_conditioner reset_cond;
.rst(rst) {
pulse pulse1(#INITIAL_VALUE(0));
pulse pulse2(#INITIAL_VALUE(64));
pulse pulse3(#INITIAL_VALUE(64*2));
pulse pulse4(#INITIAL_VALUE(64*3));
pulse pulse5(#INITIAL_VALUE(64*4));
pulse pulse6(#INITIAL_VALUE(64*5));
pulse pulse7(#INITIAL_VALUE(64*6));
pulse pulse8(#INITIAL_VALUE(64*7));
}
}
always {
reset_cond.in = ~rst_n; // input raw inverted reset signal
rst = reset_cond.out; // conditioned reset
led = c{pulse8.led, pulse7.led, pulse6.led, pulse5.led,
pulse4.led, pulse3.led, pulse2.led, pulse1.led};
usb_tx = usb_rx; // echo the serial data
}
}
If you build and load this onto your board you will see the LED wave pattern we’ve been working towards!
But wait! We aren’t done yet. There are a lot of optimizations we could make to this.
First, count how many counters we have running. Each PWM component has its own counter and each of our pulse modules has another counter. That means we have 16 counters.
Each of the eight PWM counters are identical. They are all the same size, have the same initial value, and all are free-running. The same is true for our pulse counters except they have different initial values.
However, we can get around that too by taking advantage of the circular nature of binary numbers. If we have a free running counter and add a constant value to it, it will seem as if it was just another counter offset by that value. This means our 8 counters could be replaced by one counter and seven adders. Keep in mind that every counter already has an adder in it so this saves all the extra DFFs.
We can combine all the counters together into a single free-running counter.
Finally, we don’t need the glitch free operation of the PWM component since we can ensure that the compare value will only ever update upon overflow of the PWM’s counter as it’ll be using the same counter, just more-significant bits.
All of this is neatly packed away into the PWM Wave component under LED Effects in the component library.
module wave #(
CTR_LEN = 25 : CTR_LEN >= 9
)(
input clk, // clock
input rst, // reset
output out[8] // LED output
) {
// counter
dff ctr[CTR_LEN](.clk(clk),.rst(rst));
var i; // for loop var
sig acmp[8]; // intermediate value
sig result[9]; // intermediate value
always {
// increment the counter
ctr.d = ctr.q +1;
// for each output
for (i = 0; i < 8; i++) {
// take the top bits of the counter and
// offset differently them for each output
result = ctr.q[CTR_LEN-1-:9] + i * 8d64;
// if the MSB is 1
if (result[8])
// invert the result to count down
acmp = ~result[7:0];
else // otherwise
// leave it alone to count up
acmp = result[7:0];
// PWM output
out[i] = acmp > ctr.q[7:0];
}
}
}
This module uses some advanced features to make it so compact, namely sig, var, and for loops.
The type var is used to hold a variable. This is a value that shouldn’t actually show up in your design and is used almost exclusively to hold the index value in for loops.
The type sig is used to hold a signal. Basically, these are just a renaming of some other value. They can’t save data like a dff can, but they can be used as placeholders for values to clean up your code.
For loops take the same form seen in many programming languages: for (initialization; check; operation) {...}
. The initialization sets the loop var to an initial value. The check is used to set a continuation condition and the operation is “performed” each iteration. Note that the ++
and --
syntax for incrementing and decrementing is only available on var types in this context.
Remember that for loops are really nothing other than a compact way to write something repetitive. The tools must be able to unroll them so they need to have a constant number of iterations.
The first line in the for loop takes advantage of the circular nature we talked about before by adding i * 64 to the counter’s value and naming it result.
Sig types in an always block can be read and written and their values are whatever they were last assigned. So after assigning result the value of the offset counter, we can now use it.
The sig acmp is used to hold the value we will compare with the counter for the PWM signal.
The module could have used the same XOR trick we used before but instead uses an if else statement to accomplish the same thing. In practice these will be implemented the same by the tools and I’d argue the if else format is easier to read. The XOR trick was used before as it was a good way to demonstrate some new syntax.
Finally, each bit of out is assigned depending on the comparison to the first eight bits of the counter. This module also neglects a prescaler on the PWM signal so it isn’t as power efficient as before, but the difference is probably pretty negligible.
We can put this into the top-level module and get the LEDs to wave.
module au_top (
input clk, // 100MHz clock
input rst_n, // reset button (active low)
output led [8], // 8 user controllable LEDs
input usb_rx, // USB->Serial input
output usb_tx // USB->Serial output
) {
sig rst; // reset signal
.clk(clk) {
// The reset conditioner is used to synchronize the reset signal to the FPGA
// clock. This ensures the entire FPGA comes out of reset at the same time.
reset_conditioner reset_cond;
.rst(rst) {
wave wave;
}
}
always {
reset_cond.in = ~rst_n; // input raw inverted reset signal
rst = reset_cond.out; // conditioned reset
led = wave.out; // wave the LEDs
usb_tx = usb_rx; // echo the serial data
}
}
This is the exact pattern used in the demo file shipped on the board.
This tutorial’s conclusion may feel a little underwhelming since all of this could have been accomplished simply by adding the wave component to your project and adding two lines to instantiate and connect it. However, hopefully this journey has taught you a bit more about creating FPGA projects.
There is still a lot to cover but you should now be comfortable with the basics to start tinkering.