ASIC from scratch – Part 1: First Module

Veröffentlicht von

So… how do you even start with ASIC development? Well, first of all you need a few tools. Notably an editor, a way to simulate your designs and – at least if you like to debug your designs comfortably – a way to display waveforms. Additionally I sprinkled in a few tools for documentation purposes. I prefer open source tools wherever possible, so I ended up with these:

  • VSCode, my editor
  • Icarus Verilog for simulation
  • GTKWave for viewing waveforms
  • wavedrom for rendering timing diagrams
  • Lucidchart for block diagrams (unfortunately I couldn’t find a open-source alternative that was as easy to use)

It’s a surprisingly simple toolchain, but so far I haven’t missed anything. Maybe that’s just my lack of experience speaking. I guess we’ll see how it all worked out at the end of this blog series. Obviously once I actually run the tapeout I’ll use additional tools for synthesis, static timing analysis, etc.

High Level Architecture

Before writing any module it makes sense to have a general idea of the ASIC’s structure. It doesn’t need to be set in stone yet, but it should at least not change too dramatically throughout the next few weeks. But an image says more than a thousand words, so here you go:

Fig. 1: General overview of the system architecture

As you can see, there are 4 big, grey blocks. The first block is the UART, which is responsible for receiveing configuration commands, relaying them to the control unit and sending back information about the system. Next up is the control unit. It’s the brains of the operation, as such it keeps track of all configuration and changes the timer’s control registers. The timer block contains 3 timers: Timer 1 is for time keeping, it increments every clock cycle and is configured to trigger once every second, but can be configured to trigger more ore less often. Timer 2 contains the value to be displayed. It counts from 0 to 9999 (i.e. the highest value displayable on a 4 digit display (*)). Timer 3 is a utility timer used by the 3 PWM channels. Why 3 PWM channels, you say? 🤷‍♂ why not?

First Module – Timers ⏰

Obviously timers are a major part of the design, so I decided to begin with the timer module. I wanted it to be as flexible as possible, so I ended up with the following requirements:

  • up-counting mode, down-counting mode, up/down mode (flip direction at both ends)
  • disable counting entirely
  • variable/parametrized bit-width (I might want to use different widths for each timer, particularly timer 2, which only needs to count to 9999 (or at most 0xFFFF, if I add a hex counting mode))

To satisfy all these requirements I decided on this module interface:

module timer #(parameter BITS = 4)(
	input rst, pulse,
	input count_dir,
	input count_mode,
	input [BITS - 1 : 0] reload_value,
	output wire [BITS - 1 : 0] counter
);

Like most modules it has a reset signal rst and a clock signal called pulse. My logic behind naming it „pulse“ instead of „clk“ was that it isn’t necessarily a clock signal. Later on I might connect it to a quadrature signal or hardware pushbutton. Calling that signal clock felt wrong to me.

There’s also a count_dir which specifies whether the counter should count up (count_dir = 1) or down (count_dir = 0).

count_mode is used to configure whether the counter value should reset when it reaches the end (count_mode = 0), or if it should flip counting direction (count_mode = 1). Obviously if the latter is configured the count_dir value will be ignored.

Lastly there is reload_value which serves a dual purpose as either the maximum value to count to (inclusive, only in upcounting mode), or the value to which the counter is reset once it reaches 0 in downcounting mode.

The output should be self-explanatory: It’s simply the current counter value.

Timer Behavior

The basic timer behavior is handled by a 3 cased if-statment.

// Check if either end of count was reached
if (dir && counter_reg == reload_value) begin
	// we reached the maximum counter value, we don't want to count up any further

	// check if we should reset to max or flip direction
	if(count_mode) begin
		counter_reg <= 0;
	end else begin
		counter_reg = counter_reg - 1;
		updown_dir <= ~updown_dir;
	end
end else if (!dir && counter_reg == 0) begin
	// we reached the minimum counter value, we don't want to count down any further

	// check if we should reset to max or flip direction
	if(count_mode) begin
		counter_reg <= reload_value;
	end else begin
		counter_reg = counter_reg + 1;
		updown_dir <= ~updown_dir;
	end
end else begin
	// we're not at either end yet, so just keep on counting
	if(dir)
		counter_reg <= counter_reg + 1;
	else
		counter_reg <= counter_reg - 1;
end

In terms of behavior of our module we have 3 cases we need to consider:

1. We aren’t at either end, i.e. counter is neither 0 nor reload_value. In this case we simply keep going in whichever direction we’re already going. This is the most boring case, because not much is happening.
2. We’re counting up and we’ve reached the reload_value. Depending on the count_mode we either reset our counter value back to 0 or flip the counting direction.
3. We’re counting down and we’re at 0. Like before we either reset to reload_value or flip the counting direction.

There are a few other edge cases which are probably best explained with the full code:

module timer #(parameter BITS = 4)(
	input rst, pulse,
	input count_dir,
	input count_mode,
	input [BITS - 1 : 0] reload_value,
	output reg [BITS - 1 : 0] counter
);

reg updown_dir;

// count mode = 1 means up counting OR down counting mode (depending on externally supplied direction)
// count mode = 0 means up AND down counting mode
wire dir = count_mode ? count_dir : updown_dir;

always @(posedge pulse) begin
	if(rst) begin
		// In up counting mode it should start at 0, in downcounting mode it should start at max
		//counter <= dir ? 0 : reload_value;
		counter <= 0;
		updown_dir <= 1;
	end else begin
		// Check if either end of count was reached
		if (dir && counter == reload_value) begin
			// we reached the maximum counter value, we don't want to count up any further

			// check if we should reset to max or flip direction
			if(count_mode) begin
				counter <= 0;
			end else begin
				counter = counter - 1;
				updown_dir <= ~updown_dir;
			end
		end else if (!dir && counter == 0) begin
			// we reached the minimum counter value, we don't want to count down any further

			// check if we should reset to max or flip direction
			if(count_mode) begin
				counter <= reload_value;
			end else begin
				counter = counter + 1;
				updown_dir <= ~updown_dir;
			end
		end else begin
			// we're not at either end yet, so just keep on counting
			if(dir)
				counter <= counter + 1;
			else
				counter <= counter - 1;
		end
	end
end

endmodule

Notably I used a intermediate wire with some logic for the actual direction to use, since there are two possible sources. Only one of both is valid at any given time. In up-down-counting mode the direction must be managed by the timer itself, while in up-counting mode (or down-counting mode) the counting direction is supplied externally. This is done using a wire so no clock cycle is wasted storing the correct direction and the direction is available immediately. The counting direction used by up-down-counting mode is stored in a separate register called updown_dir.

Finally, here is what the output looks like in up-down mode. count_dir is undefined, because it isn’t used in this mode.

Fig. 2: Output of the timer module in up-down-counting mode

All in all it’s a refreshingly simple module. I suppose my ASIC endeavour won’t stay that simple though and I’m wondering just how complex it will get towards the end. Anyways, that’s it for now. Next up I’ll try to work on the PWM module, conceptually that seems quite simple, too…. famous last words

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.