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:
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.
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
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
Finally, here is what the output looks like in up-down mode.
count_dir is undefined, because it isn’t used in this 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