In this lab, you will learn about what the clock is in terms of Verilog code, and how to effectively use it. This includes how to synchronize elements of Verilog code using the clock, coding procedurally to create memory elements with the clock, and using procedural coding to create simple state machines to create simple operating systems for the WebFPGA.
What is the Clock?WARNING: In this lab, you will be running code on the TI RSLK Max Robotics kit with the WebFPGA attached. Power your FPGA through USB Power ONLY, not through battery power.
Up until this point, our Verilog logic has been combinational in nature. That means that it is constantly using whatever value is present to determine the output of the logic. Think of Lab 2; the light on the WebFPGA or the RSLK only turned on when you held the button down. What if we wanted Verilog to be able to remember a value stored in a register, and iterate on it? There are a couple of ways to implement memory in Verilog, but the easiest, most controlled way requires a way to synchronize these devices across the entire Verilog design - A Clock!
The clock is a digital waveform signal that oscillates between outputting high (1) and low (0) in a fixed amount of time, called the period. See the image below for a visual example of a clock:
The clock signal in this example starts low, then goes high. When the clock goes from low to high, we call that a rising or positive edge; similarly, when the clock goes from high to low, we call it a falling or negative edge. Most elements in our Verilog code that use the clock will act and update on either the rising or falling edge of the clock. The time between rising edges of the clock is called the period. See below for that concept visualized with the WebFPGA clock:
The WebFPGA has an included clock signal, called WF_CLK. We will use this as the clock in our Verilog code. As you can see above, it has a period of 62.5 nanoseconds. 1 divided by the period is the frequency; using this math, we say the WebFPGA clock has a frequency of 16MHz. Knowing this, we can derive specific timing in our Verilog programs. For example, if we increment a variable every rising clock edge, after that counter increments 16000000 times, we know that 1 second has passed.
- How many times would that counter increment in 24 hours with a 16MHz clock?
This information is all good and well, but how would we implement a counter like this in Verilog?
Procedural LogicThe key to using the clock effectively in Verilog coding is a new type of input/variable we can define in Verilog, called a register. See below for a simple example of how to make an output a reg:
module fpga_top(input wire, output reg B);
endmodule
Reg type inputs can’t be assigned like wires. Instead, we have to assign them using something called an always@ block. There are 2 main ways we can implement an always@ block. One mimics the sequential logic that you’re already familiar with, see below:
module fpga_top(input wire A, output reg B);
always@(*) begin
B <= A;
end
endmodule;
There are many important things to learn from the above code snippet. First, the always@ block has a star in its parentheses. The contents of a parentheses in an always@ block denote when the values within the block are updated. The star means that the always@ block will update whenever the variables on the right side of the <= signs change. In the above example, that means it only updates when A changes. You may have also noticed that the <= symbol I used in place of an = symbol. This is a non-blocking statement, and it will be explained later. And finally, when using logic blocks like always@ or if statements, you must use “begin” and “end” to bookmark the beginning and end of the logic block if there is more than 1 line of Verilog between them; think of them as your curly brackets from a programming language like C.
To create logic blocks that are timed to a clock, we have to use an always@ statement that updates every time the clock either has a rising edge or a falling edge. Generally, we use the rising edge of the clock to synchronize our logic. See the snippet below for an example.
module fpga_top(input wire A, input wire WF_CLK, output reg B);
always@(posedge WF_CLK) begin
B <= A;
end
endmodule;
Let’s look at a quick example of why non-blocking statements are important. Look at the code snippet below:
always@(posedge WF_CLK) begin
B <= A
C <= B
end
Let’s say before the code above executes, A is equal to 1, B is equal to 2, and C is equal to 3. After the first statement executes, B would be equal to 1. After the second statement executes, what do you think it would be equal to?
The answer is that C would be equal to 2. Non-blocking statements don’t update the variables contained within the statement until it is finished. Essentially, C gets assigned to the previous value of B before all variables are updated at the same time. Let’s contrast this with the blocking version of this code block.
always@(posedge WF_CLK) begin
B = A
C = B
end
After this code block resolves, B would be equal to 1, and C would also be equal to 1. As a rule of thumb, if you’re using an always@(posedge) statement, use non-blocking, and if you’re using an always@(*) block, use blocking statements.
Now that we have the basics of non-blocking statements and always@ blocks, let's add if statements and make the RSLK and FPGA actually do something. Let’s look at a simple example that uses the clock to iterate a counter and turn the LED on and off depending on the value of the counter. Can you figure out what the interval that the LED will be turning on and off on?
// Pin Map
// @MAP_IO motorL_pwm 0
// @MAP_IO motorL_dir 1
// @MAP_IO motorL_en 2
// @MAP_IO motorR_pwm 3
// @MAP_IO motorR_dir 4
// @MAP_IO motorR_en 5
// @MAP_IO ledFL 17
// @MAP_IO ledFR 18
// @MAP_IO ledBL 19
// @MAP_IO ledBR 20
module fpga_top(
input wire WF_BUTTON,
input wire WF_CLK,
output reg ledFL,
output reg ledFR,
output reg ledBL,
output reg ledBR,
output reg WF_LED,
output wire motorL_pwm,
output wire motorL_dir,
output wire motorL_en,
output wire motorR_pwm,
output wire motorR_dir,
output wire motorR_en
);
reg [25:0] counter;
assign motorL_pwm = 0;
assign motorL_dir = 0;
assign motorL_en = 0;
assign motorR_pwm = 0;
assign motorR_dir = 0;
assign motorR_en = 0;
always@(posedge WF_CLK)begin
if(counter == 32000000)
begin
WF_LED <= ~WF_LED;
counter <= 0;
end
else
begin
counter <= counter + 1;
end
end
endmodule;
Notice that in the above code block, we have implemented memory in our Verilog code! Every clock cycle, it either stores the iterated version of the counter value, or flips the LED on/off if the counter value is high enough, then resets it. Counters are a great way to perform precise maneuvers and operations with your car, since you can time it down to nanoseconds (at least on the code side). Obviously there is still some delay to actually, say, turn on some LEDs (or eventually, the motors), but nonetheless timers like these will be a powerful tool in your RSLK coding toolkit.
State MachinesUp until this point, we’ve been coding the car to essentially function in a single state. This section of the lab is going to introduce state machines, an added layer of complexity on to Verilog programming. State machines allow our code to perform actions sequentially, like a flow chart. The Verilog code will complete a set of actions, then make a decision about what set of actions it will undertake next. Let’s start with a simple example. The code snippet below implements the same functionality as the last code snippet, but it uses state machines instead. We also provide a state diagram (essentially the flowchart I mentioned earlier) for the code, showing how the code progresses through the logic.
// Pin Map
// @MAP_IO motorL_pwm 0
// @MAP_IO motorL_dir 1
// @MAP_IO motorL_en 2
// @MAP_IO motorR_pwm 3
// @MAP_IO motorR_dir 4
// @MAP_IO motorR_en 5
// @MAP_IO ledFL 17
// @MAP_IO ledFR 18
// @MAP_IO ledBL 19
// @MAP_IO ledBR 20
module fpga_top(
input wire WF_BUTTON,
input wire WF_CLK,
output reg ledFL,
output reg ledFR,
output reg ledBL,
output reg ledBR,
output reg WF_LED,
output wire motorL_pwm,
output wire motorL_dir,
output wire motorL_en,
output wire motorR_pwm,
output wire motorR_dir,
output wire motorR_en
);
localparam s0 = 2'b00;
localparam s1 = 2'b01;
localparam s2 = 2'b10;
reg [1:0] current_state;
reg [1:0] next_state;
reg [25:0] counter1;
reg counter1_enabled;
reg [25:0] counter2;
reg counter2_enabled;
assign motorL_pwm = 0;
assign motorL_dir = 0;
assign motorL_en = 0;
assign motorR_pwm = 0;
assign motorR_dir = 0;
assign motorR_en = 0;
always@(posedge WF_CLK)begin
current_state <= next_state;
end
always @ (posedge WF_CLK)
begin
if(counter1_enabled)begin
counter1 <= counter1 + 1;
end
else
begin
counter1 <= 0;
end
end
always @ (posedge WF_CLK)
begin
if(counter2_enabled)begin
counter2 <= counter2 + 1;
end
else
begin
counter2 <= 0;
end
end
always@(*) begin
casex(current_state)
s0: begin
WF_LED = 0;
counter1_enabled = 0;
counter2_enabled = 0;
next_state = s1;
end
s1: begin
WF_LED = 0;
counter1_enabled = 1;
counter2_enabled = 0;
if(counter1 == 32000000)
begin
next_state = s2;
end
else
begin
next_state = s1;
end
end
s2:
begin
WF_LED = 1;
counter1_enabled = 0;
counter2_enabled = 1;
if(counter2 == 32000000)
begin
next_state = s1;
end
else
begin
next_state = s2;
end
end
endcase
end
endmodule
The core brain of the state machine is the casex statement. We denote the beginning of the casex statement with the variable that determines the current state and we denote the end of the possible cases with “endcase”. Depending on the value of “current_state”, different blocks of code will execute. We call these blocks of code “states”. The state determines what logic the Verilog executes, and what the next state the Verilog will transition into. We use a new type of Verilog statement in the above code to make the state machines easier to read; these are called parameters. Essentially, when the code is synthesized, all the parameters will be replaced with what we defined the names as. In the above snippet, we use parameters to represent the states in the case statement/next_state/current_state; s0 represents binary 00, s1 represents binary 01, s2 represents binary 10. Knowing this, let’s break down what each of the states does.
State 0 is the reset state. Neither counter is enabled, and the LED is on. The state machine always progresses to state 1 out of this state. This is the state that the FPGA will enter when it first powers on, or when the red reset button is pressed.
State 1 enables counter 1, turns counter 2 off and zeroes it out, and turns the LED on. It then checks what the value of counter 1 is. If counter 1 is equal to 32000000, then that means 2 seconds have passed, and it is time to progress to state 2. If not, then we stay in state 1.
State 2 enables counter 2, turns off and zeroes out counter 1, and turns the LED off. To see which state to go into next, it checks the value of counter 2. If counter 2 equals 32000000, then we transition back to state 1; if it isn't, we stay in state 2.
Take note that in all 3 states, we make sure that every variable has been assigned. If a variable gets assigned in one state but not in another state, that creates a memory element, which is NOT the intention of the state machine! We don’t want to remember what a given variable was set to in a previous state; we want to assign every variable in every possible state.
Can you take the state machine above and expand/modify it so that:
- It takes 5 seconds to move between states
- Instead of turning the WebFPGA LED on and off, it turns on the following sequence of LEDs on, 1 LED at a time (so all others are off when one is on), repeating itself forever: Front Left, Back Left, Back Right, Front Right
- It uses as few timers/states as possible (this is more of a challenge goal rather than a strict requirement. How efficient can you make it?)?
Congratulations! You now have a working knowledge of how to use the clock to synchronize your Verilog programs, how to use procedural logic to create combinational and sequential logic, and how to combine the two to create simple state machines. State machines are an incredibly powerful tool for creating behavior patterns for the RSLK kit, and will be the foundation of most future Verilog coding we do.
Comments