As it turns out, implementing a PWM controller is as easy as I imagined it to be. It takes only a handful lines of code to get it to work and even those aren’t particularly complex. But let’s start from the beginning: How does my hardware PWM controller work?
Well, it takes a timer’s counter value and a set value. If the set value is less than the counter value, the PWM output is high.
Yes. That’s it. Super easy, right? In fact, if we don’t count the boilerplate code that’s required to define a module’s interface, it’s only a single line of code:
module pwm #(parameter BITS = 4) ( input [BITS - 1 : 0] counter, input [BITS - 1 : 0] pwm_set, output wire out ); // the very complicated code for this module: assign out = counter < pwm_set; endmodule
Of course I don’t want just one PWM channel, I want several of those. So I created another module called
pwm_controller which is using a generate loop to create a few pwm channels. It also accepts a
channel_enable vector, which can be used to tristate each channel. Might be useful or a complete waste of time. At this point I don’t really know yet. It was so little effort to add, I didn’t even think about it. Coming from C++, I kind of hope that if I ever pass in a constant „all 1“ vector, the synthesizer will optimize this subcircuit away. I’m not sure that’s how Verilog works, but to be honest my asic designs aren’t particularly complex or performance oriented. Even if it isn’t optimized, I just don’t really care enough. I guess what I’m trying to say is: It’s good enough for me either way.
Enough talking. Here’s the
module pwm_controller #(parameter CHANNELS = 3, parameter BITS = 16) ( input clk, rst, input [BITS - 1 : 0] counter, input [CHANNELS - 1 : 0] channel_enable, input [BITS - 1 : 0] set_values [CHANNELS - 1 : 0], output wire [CHANNELS - 1 : 0] outputs ); for(genvar i = 0; i < CHANNELS; i = i + 1) begin wire out_tmp; pwm #(.BITS(BITS)) channel(.counter(counter), .pwm_set(set_values[i]), .out(out_tmp)); assign outputs[i] = channel_enable[i] ? out_tmp : 1'dz; end endmodule
5 lines if we don’t count the boilerplate. As I said before, the for loop generates a few
pwm channels. Each output is then assigned to
outputs if the given channel is enabled. Otherwise the channel is tristated/disconnected. The eagle-eyed among you might notice that if I set
counter = 255 and
pwm_set = 255 there will always be a single clock cycle where the output is off. Meaning I can never reach 100% duty cycle if
counter is the maxiumum value for a given bitwidth. I’m not sure if „real“ pwm implementations out there by ST, Microchip, TI and the likes behave the same. I want 100% duty cycle and the simplest way I can achieve that is by simply having my timer count to 2n – 2 instead of 2n – 1. Then I can set
pwm_set to 2n – 1 and everything will be hunky dory.
Here are my results, this time using a GTKWave screenshot instead of a Wavedrom diagram:
I’m really starting to like verilog and hardware development in general. Seeing how much you can achieve with so little is actually really impressive. A few years ago I would have imagined PWM controllers (and a lot of other peripherals) to be a couple orders of magnitude more complex, but now they seem like really tame beasts.
At this point I’m not sure yet what I’ll try next, but I think I might try my luck with the uart receiver part. Or maybe I’ll try to write a testbench that can perform automatic tests. So far all my testing has been entirely done by hand using
$dumpvars and GTKWave.