ASIC from scratch – Part 2: PWM

Veröffentlicht von

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 pwm_controller code:

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:

Fig. 1: PWM Output waveform displayed in GTKWave

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.

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.