Electronics Tinkerer
Projects 'N Stuff

Lecture 2: State Machines and Parameterization

Last Update: 2023-11-04 You’ve likely heard the term “state machine” before, but what exactly is one? Usually when we talk about state machines in control logic, we are referring to a Finite State Machine (FSM). This is a circuit which has some logic to store its current “state” and some logic to compute the next state. The counter from lecture 1 is a simple example of a synchronous state machine: there is a count register, consisting of several flip flops, which stores the current value (state) and there is an adder to compute the next count value (the next state). On a specific clock edge (posedge), the state register updates to the next computed state value. The next state of the counter from lecture 1 relies entirely on its current state. That is, there is no way to externally influence its operation. In general, very few state machines follow this deign approach simply because this limits what we can do with a state machine. Listing 1 shows a modified version of the counter which adds a reset input. This allows an external circuit to influence the behavior of the FSM.
module counter_resettable (
        input clk, rst,
        output [3:0] count
    );

    reg [3:0] count_internal;

    assign count = count_internal;

    always @(posedge clk) begin
        if (rst) begin
            count_internal <= 4 'b0;
        end
        else begin
            count_internal <= count_internal + 4 'b0001;
        end
    end
endmodule
Listing 1: 4-bit counter with synchronous reset Notice how there is now a check for the “rst” signal in the main “always” block? When this signal is asserted high, the if’s condition evaluates to true and the “count_internal” net is reset to 0. When the signal is not asserted, the else clause is evaluated, and the counter goes about its standard behavior of incrementing once per clock cycle. Keep in mind that this if/else strucure is only checked on the rising edge of the clock. If the “rst” signal is asserted at any other time, it has no effect on the counter’s operation. This is what is known as a “synchronous reset.” It is also possible to create an “asynchronous reset.”
module counter_resettable_async (
        input clk, rst,
        output [3:0] count
    );

    reg [3:0] count_internal;

    assign count = count_internal;

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            count_internal <= 4 'b0;
        end
        else begin
            count_internal <= count_internal + 4 'b0001;
        end
    end
endmodule
Listing 2: 4-bit counter with asynchronous reset Listing 2 shows an almost identical counter except that it models an asynchronous reset. This means that when the circuit sees a leading edge on the reset signal, the always block’s body is run. In this case since we have an active high reset, we have picked the positive edge of the reset line to trigger the reset operation. (We could have just as easily picked the negative edge for an active low reset.) Despite having two trigger events in the sensitivity list, it should not be expected that this block miscounts. Since the reset edge matches the polarity of the reset signal, when the always block’s body is run the reset input will be high, which matches the if condition. Additionally, if the module receives a positive edge on the clock while reset is asserted, the if condition is still true and the counter will stay in a reset state.

Parameterization

Taking a short tangent from our discussion on state machines, now is a good time to discuss a Verilog design technique called “parameterization.” This is similar in concept to how you might “#define” a value in C. It allows the designer to give a name to a constant but parameters also allow this constant to be changed on a per-instance basis. Listing 3 shows our counter with a synchronous reset but this time, we used a parameter to set the width of the counter.
module counter_resettable_param 
    #(
        parameter WIDTH = 4
    ) (
        input clk, rst,
        output [WIDTH-1:0] count
    );

    reg [WIDTH-1:0] count_internal;

    assign count = count_internal;

    always @(posedge clk) begin
        if (rst) begin
            count_internal <= {WIDTH{1'b0}};
        end
        else begin
            count_internal <= count_internal + {{(WIDTH-1){1'b0}}, 1'b1};
        end
    end
endmodule
Listing 3: Parameterized counter with synchronous reset There are a number of new elements here. Let’s start with the new part of the module signature. In lecture 1, it was mentioned that the module definition can contain a parameter list. In Listing 3, this is the “#( parameter WIDTH = 4 )” part. This declares a parameter called “WIDTH” which takes the default value of 4. We will see how to override this default later. A parameter is a runtime constant and must be determinable during compilation/elaboration. Multiple parameters can be specified by placing a comma between their definitions withing the “#( … )” list. We then use this parameter to specify the width of the count output port. Since the lowest bit index of the “count” vector is 0 (and not 1) we need to subtract 1 from the WIDTH to get the correct width for this vector. (A declaration of [3:0] is 4 bits while [4:0] is 5 bits.) Similarly, the “count_internal” vector is parameterized in its width. The next change is the reset value expression for “count_internal.” This makes use of a “replication operator.” The general syntax for a replication is “{ repeat { value/net } }” It concatenates the “value/net” portion of the expression “repeat” number of times. In the counter, this means that the “1'b0” value is replicated WIDTH times. With the default WIDTH, this results in a constant of 4'b0000. The last change is in the count increment line. Using a similar syntax to replication, we can concatenate two (or more) nets/values. Concatenation takes the form of “{ MSB, …, LSB }” where MSB is the most significant bits and LSB is the least significant bits. If you are following closely, you should have noticed that the “MSB” portion of the count increment value is just the replication of 1'b0 by (WIDTH - 1) times, concatenated with a 1'b1 in the LSB position. This ensures that the value to be added is the same width as count_internal. (This is, admittedly a bit pedantic. You could have much more easily used a simple “+ 1'b1” and gotten the same result.)

State Diagrams

We’re already said that the counter is a basic form of state machine. When we start to get into more complicated state machines, we often rely on a tool to visualize how we “transition” between different states. This is called a “State Diagram.” A diagram showing the states and transitions for a 2-bit counter with synchronous reset. Figure 1: State machine for a 2-bit counter with synchronous reset Figure 1 shows the state machine for the parameterized counter with synchronous reset when we set the WIDTH parameter to 2. Each bubble in the diagram represents one possible state. The state name is indicated in the center of the bubble. Since we have constructed a counter, it makes most sense to just write the count’s value for each name. Going between the bubbles are arrows. These indicate the state the machine will transition to on the next clock cycle. For instance, from state 00, there is an arrow which feeds into state 01. Most of the arrows have a condition written next to them. This condition is usually the values of the inputs required to make that transition occur. The transition from 00 to 01 must occur only if the “rst” input is 0. If “rst” is 1, the transition line coming out of state 00 simply loops back to state 00 and the state does not change. This logic follows for the remaining conditional lines. One transition which is not conditionally marked is the transition from 11 to 00. This is because regardless of the reset input’s value, the next state will always be 00 (11 + 1 = 00 due to the counter rolling over).

Design Example: Button Debouncing

Many design applications involve reading some form of user input. The simplest method is just reading a button. Buttons in an ideal sense are very niece to work with since they can be wired to give a binary value which can be read in. Without going into the details of logic levels and the like, we will assume that < 50% of our supply is a logic 0 and > 50% is a logic 1. From this specification, a circuit like Figure 2 can be created. A schematic showing a normally open push button connected to power and a pull-down resistor connected to ground. The button and resistor are connected in series and the connection node is connected to an input pin on a digital circuit. Figure 2: Button input schematic This is about the simplest circuit you can get for an input. When the button is not pressed, the two contacts are disconnected (a N.O. - normally open type switch). When the button is pressed, the contacts are connected and the switch conducts, connecting the digital input pin to 3.3V. The purpose of the resistor is to ensure that while the switch is open, there is a known voltage on the pin. Without the resistor to “pull down” the voltage to 0V when the switch is open, we cannot predict the voltage on the digital input pin (this condition is called a “floating input” and digital circuits really do not like this). This can lead to odd behavior of our digital circuits. Waveform of the input pin when the button is pushed showing a nice square edge at the moment that the button is pushed. Figure 3: Ideal button waveform Figure 3 shows that at the moment the button is pressed, the output voltage jumps straight to 3.3V. Unfortunately, our buttons do not come from Yost Manufacturing, and thus are not ideal. As a result, when the button contacts make contact, they bounce a few times, meaning the resulting waveform looks something like Figure 4. If this were fed to the clock input of the counter, it might count multiple pulses per press. Waveform of the input pin when the button is pushed showing a noisy rise at the moment that the button is pushed. Figure 4: Real button waveform If we are sampling the input to the digital circuit slowly enough, we might entirely miss this noisy edge and there would be no issues. There is an alternative approach, though: we construct a state machine which allows us to sample the input at some fast rate (say, 10 MHz) while still avoiding the issues of multiple edges. Let’s say that a particular button is known to bounce for a maximum of 10 ms when closed but experience no bounce when it is opened. The design can start with an “initial” state. This is the default state that the state machine waits in when it is idle or reset. We know that the default state of the button is to be open, so it makes sense to use the initial state as “button not pushed.” From here, there is only one place to go: “button pushed.” As soon as the state machine sees a high value, it transitions to the “button pushed” state. On first look, it might make sense to use a low input value to transition back to “button not pushed.” Here’s the flow diagram for this design: A state diagram for our 2-state input debounce state machine. Figure 5: First attempt at a debounce state machine The trouble with this design is that the state machine will be running faster than the frequency of the button’s bounce. As a result, it might flip between the “button not pushed” and “button pushed” states a few times during the noisy transition. This can be fixed, however, by the addition of a delay before returning to the “button not pushed” state. To do this, we can add a third state: “button released timeout.” When in the “button pushed” state and the button is released, the state machine can set a timeout in a counter and transition to the “button released timeout” state. While in the “button released timeout” state, each clock cycle will be used to increment the timer’s value until a timeout. When the timeout occurs, the machine transitions back to “button not pushed.” If at any time during the timeout the button is pushed again, the machine will transition back to “button pushed,” in effect resetting the timeout. A state diagram for our 3-state input debounce state machine. Figure 6: Corrected debounce state machine

Converting to a Verilog model

The first step to creating a new Verilog design is to start off with the module declaration:
module button_debounce
  #(
    parameter DELAY_MS = 50, // In ms
    parameter CLK_FREQ = 50_000_000 // In hertz
    )
   (
    input  clk, rst, 
    input  btn_in,
    output btn_out
    );

endmodule
Listing 4: Button debounce module signature The declaration includes two parameters for specifying the length of the release delay timeout (DELAY_MS) and the frequency of the system clock (CLK_FREQ). Using these two values, we can compute the timeout count value for use in the “button released” state. Next are the system clock and reset inputs, followed by the raw button input signal “btn_in” and the debounced “btn_out” output signal. Next, we compute the number of cycles needed to achieve the specified delay as well as the width of register needed to hold this value. Additionally, two other values need to be calculated: the counter’s reset value and the value to load into the delay counter when restarting the delay timeout (in “button pushed”). Notice that these are “localparam” style parameters. These can only be set within the module and cannot be modified (directly) from outside of the module like standard “parameter”s.
   // Calculate the count value needed for button release delay
   localparam DELAY_COUNT = (CLK_FREQ * 1000) / DELAY_MS;
   localparam DELAY_WIDTH = $clog2(DELAY_COUNT);
   localparam [DELAY_WIDTH:0] DELAY_RESET_VAL = {1'b1, {DELAY_WIDTH{1'b0}}};
   localparam [DELAY_WIDTH:0] DC_LOAD_VAL = DELAY_RESET_VAL - DELAY_COUNT;
Listing 5: Delay calculations The “DELAY_RESET_VAL” and “DC_LOAD_VAL” are both one bit wider than the count needs to be. Why? This allows us to reset the counter with a 1 in the MSb and the rest all 0’s. The circuit can read this MSb to determine if the counter is reset without the need for a wide input NOR gate. This can potentially save on propagation delay, resulting in a faster circuit. (In CMOS technology, gates typically can have a maximum of four inputs before multiple gates need to be cascaded to achieve a desired input count.) To ensure that the delay counter ends with its MSb set, the load value for starting the counter is simply the number of cycles to count subtracted from this “maximum” value. Listing 6 defines the signals needed for the counter, delay timeout detection, and state storage. It also defines the values that will be used to represent each state. The states are encoded using “one hot encoding,” where in each state, only a single bit is set. This is commonly used in high speed state machines since minimal logic is required to move the bit around during transitions.
   reg [DELAY_WIDTH:0] count, next_count;
   wire                delay_complete;
   reg [2:0]           state, next_state;

   localparam S_BTN_NOT_PUSH = 3'b001,
              S_BTN_PUSH     = 3'b010,
              S_BTN_RELEASED = 3'b100;

   assign delay_complete = count[DELAY_WIDTH];
Listing 6: State variables and definitions Finally comes the state machine. Listing 7 shows the logic to advance the state as well as the basic structure of the next state logic. The state advancement logic is fairly simple: it synthesizes two sets of flip flops which latch the next state and next count values each clock cycle. In contrast, the next state logic is purely combinational. It implements a reset and computation of the next state based on the current state. To ensure that this is combinational, we must make sure that any LHS value which is assigned within the “always @(*)” block is assigned a value in every execution path. This is why in “next_count” and “next_state” are assigned a default value of “count” and “state,” respectively, before the “case” statement. (It’s also why “btn_out” is assigned a default value of 0). Remember that doing multiple assignments to a net within the same block will not result in a logic glitch due to the scheduler’s behavior: the blocking assignments are executed in the order they are written and the current time step is not completed until all updates for that time step have been processed.
   always @(posedge clk) begin
      state <= next_state;
      count <= next_count;
   end

   // Next state logic
   always @(*) begin
      btn_out = 1'b0;
      // Handle reset
      if (rst) begin
         next_state = S_BTN_NOT_PUSH;
         next_count = DELAY_RESET_VAL;
      end
      // If reset is not active, follow the state machine logic
      else begin
         // Make sure that every path through the always block
         // assigns a value to the nets that this block drives.
         // If we don't do this, latches will be synthesized,
         // resulting in headaches.
         next_count = count;
         next_state = state;
         case (state)
            // Button is not pushed, idle
            S_BTN_NOT_PUSH : begin
               if (btn_in) begin
                  next_state = S_BTN_PUSH;
               end
            end
            // Button is currently held down,
            // set up the timer
            S_BTN_PUSH : begin
               btn_out = 1'b1;
               next_count = DC_LOAD_VAL;

               // If button is released, start the timeout
               if (!btn_in) begin
                  next_state = S_BTN_RELEASED;
               end
            end
            // Button was held down but has been released
            // recently.
            S_BTN_RELEASED : begin
               btn_out = 1'b1;
               next_count = count - 1'b1;
               // If the button is pressed, restart the timeout
               if (btn_in) begin
                  next_state = S_BTN_PUSH;
               end
               // If the timeout delay is over, reset
               else if (delay_complete) begin
                  next_state = S_BTN_NOT_PUSH;
               end
            end
         endcase
      end
   end
Listing 7: State machine logic Finally, we can put this button debouncer into Quartus (to be discussed next week) and using it’s RTL viewer for state machines, see the flow diagram for the above code: Quartus’s generated state machine diagram for the button debouncer Figure 7: Quartus’s generated state machine diagram for the button debouncer The complete debounce code can be found here: button-debounce.v One last note: the button input is not synchronized to our clock domain, that is, it could transition at any time, relative to the active edge of the clock. As a result, the setup time may be violated and the state machine’s behavior somewhat unpredictable. We will show one way to solve this next week…
That's all!
© 2021-2024 — Electronics Tinkerer — All rights reserved.
Top | Home | Privacy | Terms of Service