- Starter Code
- Related Class Materials
- Introduction to FPGA Development
- Dynamic Counter Control System
- Checkoff
- Bonus: utilizing the 7 Segment
- Summary
50.002 Computation Structures
Information Systems Technology and Design
Singapore University of Technology and Design
Lab 2: Combinational and Sequential Logic with FPGA
Starter Code
There’s no starter code for this lab. You simply need to have Alchitry Labs V2 installed. You don’t need Vivado for this lab, we are only going to run simulations.
Related Class Materials
The lecture notes on sequential logic and logic synthesis are closely related to this lab.
Introduction to FPGA Development
In this chapter, we’ll explore combinational and sequential logic using FPGAs.
Unlike software simulations, FPGAs let you directly program hardware, giving you a real-world, hands-on understanding of how digital systems work.
FPGAs offer:
- Flexibility: Easily reprogram and experiment with different designs.
- Real-time Feedback: See your circuits in action, running in parallel.
You have been given the Alchitry Au FPGA development kit, which includes I/O ports for easy connections to external devices. It’s a cost-effective option compared to full-scale Xilinx boards, making it ideal for education.
To program the FPGA, we’ll use the Lucid V2 hardware description language (HDL), which is simpler (in syntax) than traditional HDLs like Verilog, making it easier to learn. Alchitry Labs will be our development environment (IDE), converting Lucid into Verilog for synthesis in Vivado.
Why not Vivado + Verilog?
Vivado is the industry-standard tool for FPGAs. However, both Vivado and Verilog have steep learning curves, especially for beginners. It is not suitable for 50.002 because we need to cover a lot of foundational topics—ranging from MOSFETs and transistors to operating systems. Alchitry Labs, will handle the conversion to Verilog in the background, so you can focus on learning the core concepts without diving into the complexities of Verilog or Vivado.
When you “program” an FPGA, you are configuring its logic cells and connections to implement your desired digital circuit or design. This is different from programming software, as you are actually defining hardware behavior within the FPGA.
Create New Project
Create a new project with Blinker Demo as a template. Choose Alchitry Au. This is our FPGA development board.
There a few Lucid (.luc
) source files created:
alchitry_top.luc
: This is like the “main” file. You can interact with I/O (LEDs, buttons, switches) defined in the constraint file.blinker.luc
: this is a custom module containg the logic of LED blinkingreset_conditioner.luc
: this is a module obtained from Alchitry component library. It synchronizes the reset button so that all modules in the FPGA can receive the reset signal at the same time.Constraint files: alchitry.acf
: a constraint file maps the FPGA I/O pin with a logical name that you can define inalchitry_top.luc
, so that you can define the logic to control these I/Os.
Module
This section briefly explains basic Lucid V2 syntax. Lucid V2 is a great language to program simple behavior on the FPGA which is sufficient for 50.002.
You are strongly recommended to give the official reference guide a read to obtain further information.
Modules are the core building blocks of any Lucid project. They encapsulate specific functionality, allowing you to design complex circuits by breaking them into smaller, manageable components. Each module can have parameters and ports that define how it interacts with other parts of the design. The general structure for a module declaration in Lucid is:
module module_name #(
// optional parameter list
)(
// port list
) {
// module body
}
By organizing your design into modules, you create reusable, testable blocks that can be combined to form larger systems.
For instance, the blinker.luc
module is written as follows:
module blinker (
input clk, // clock
input rst, // reset
output blink // output to LED
) {
dff counter[26](.clk(clk), .rst(rst))
always {
blink = counter.q[25]
counter.d = counter.q + 1
}
}
It is designed to create a blinking signal that can drive an LED, toggling it on and off at a set interval. It uses a clock signal (clk
) and a reset signal (rst
) to control the timing of the blink.
Port List
- Inputs:
clk
: 1 bit clock signal, which drives the timing of the module.- By default, the FPGA supplies 100MHz clock (defined in the constraint file)
rst
: 1 bit reset signal, which sets the counter back to its initial state when triggered.
- Output:
blink
: 1 bit output signal that drives the LED, toggling it on and off.
Module Body (module instances)
- Counter: The module uses a 26-bit flip-flop-based counter (
dff counter[26]
), which increments its value on each clock cycle. When instantiating thedff
module, we connect theclk
andrst
input signal to the dff’s port. This is so that counter’s (dff) state is updated based on the clock (clk
) and can be reset using the reset signal (rst
).
Module Body (always block: internal logic)
- Blink Control:
- The blink signal is assigned the value of the 25th bit of the counter. This effectively divides the clock by \(2^{25}\), slowing down the frequency to a human-visible rate for blinking.
Dividing 100 MHz by \(2^{25}\) results in a frequency of approximately 2.98 Hz, meaning that the signal toggles 3 times per second. Each time the signal toggles, it goes from on to off or off to on. Since this toggle happens 3 times per second, the LED will change its state (on to off, or off to on) at that rate. Therefore, the blink will turn on and off once every 0.67 seconds, with both the on and off states taking about 0.33 seconds each.
- Incrementing the Counter:
- The counter’s value (
counter.d
) is updated by adding 1 to the current value (counter.q + 1
), causing it to increment on each clock cycle.
- The counter’s value (
With this logic, blinker
module uses a 26-bit counter to divide the clock signal down, and the 25th bit is used to toggle the blink
output. As the counter increments, the blink
signal toggles between high and low, creating a blinking effect that can be used to drive an LED.
The always
Block: Unlike Sequential Programming
The always block
In hardware description languages like Lucid, the
always
block represents continuous and parallel execution, unlike traditional sequential programming where instructions are executed one after the other. It is used to define connections and relationships between different signals happening in parallel, rather than executing sequential instructions like in software programming.
In the blinker
module, the always
block ensures that certain operations happen simultaneously on every clock cycle:
blink = counter.q[25]
constantly updates theblink
output to match the 25th bit of the counter.counter.d = counter.q + 1
continuously increments the counter by 1 on each clock cycle.- The counter increments on each clock cycle.
- The blink output is continuously updated based on the 25th bit of the counter.
- Both operations (updating blink and incrementing counter) happen in parallel on every clock edge.
- No explicit loop is needed—everything updates with the clock signal.
This is inherently different from sequential programming where instructions are executed in a specific order, one after another. In sequential programming, the logic is executed step-by-step, and you would need an explicit loop to simulate the continuous behavior. Here’s the same functionality in Python:
def blinker():
counter = 0
blink = 0
while True:
blink = (counter >> 25) & 1 # Update blink based on the 25th bit of the counter
counter += 1 # Increment the counter
time.sleep(1 / clock_frequency) # Simulate clock delay
In the code above:
- The counter is incremented manually in each iteration of the loop.
- The blink value is recalculated after every iteration based on the 25th bit of the counter.
- This code runs in a sequential loop, step by step, with a delay (time.sleep()) to simulate the clock.
- Operations are sequential: first, the counter increments, then the blink value is updated, then the process pauses before repeating.
Timing
In hardware, the timing is inherently tied to the clock signal, which runs as long as the circuit is powered. In software, you need to introduce artificial delays (e.g.,
time.sleep()
) to mimic clock behavior.
Let’s compare that again with Lucid’s/HDL always
block:
- Both lines of code are evaluated in parallel on each clock cycle.
- The operations happen in a non-blocking manner, meaning the update of the counter and the assignment to the
blink
output are done concurrently, not one after the other.
This parallelism is key to understanding how digital logic works—everything inside an FPGA is happening at the same time, driven by the clock, unlike the step-by-step flow of traditional programming.
alchitry_top
This is the main file that interfaces with the I/O ports.
module alchitry_top (
input clk, // 100MHz clock
input rst_n, // reset button (active low)
output led[8], // 8 user controllable LEDs
input usb_rx, // USB->Serial input
output usb_tx // USB->Serial output
)
The blinker
module is instantiated here and then connected to led
, which is the 8 LEDs connected to the side of the Alchitry Au Board. The reset button is also situated there. The input clk
is powered by the FPGA, defaulted to 100MHz
.
You can ignore
usb_rx
andusb_tx
. You are not required to use them in this course.
Simulate
Alchitry Lab V2 offers the feature of simulation with a much slower clock (1000Hz) to test your design before spending a few minutes synthesizing it. You can click the Simulate button to view the simulator:
Right now you won’t see the LED blinking because the simulation clock runs on 1000Hz clock instead of 100MHz. We are dividing 1000Hz clock by \(2^{25}\), which results in a frequency of 0.0000298 Hz, or about 1 toggle every 33,554 seconds (around 9.32 hours). We need to change the value “25” into something else more suitable for the simulator such as 9 so that it divides the 1000Hz clock by \(2^{9}\): \(\frac{1000}{2^{9}} = 1.95 Hz\), or 1 toggle every 0.5 seconds.
Add this line of code in blinker.luc
:
always {
blink = counter.q[$is_sim() ? 9 : 25] // set value conditionally
counter.d = counter.q + 1
}
This means that we will use the value 25
when we synthesize the code for the FPGA, and the value 9
during simulation.
You should now see the LED blinks in the simulation:
Dynamic Counter Control System
Our goal in this section is to create a Dynamic Counter Control System . Here’s its behavior:
- The dynamic counter system begins counting from 0.
- The counter increases by a set value, which starts at 1 but can be adjusted.
- You can increase or decrease the amount by which the counter increments.
- The speed at which the counter adds the value can be controlled, allowing for faster or slower counting.
- The system can be started, paused, or reset at any time, and its behavior can be modified based on user inputs.
Here’s some constraints:
- The counter’s value (
SIZE
bits) should be clamped to zero and shall not become negative. It should not have positive overflow either. - The counter shouldn’t be increased by a value larger than
SIZE-1
or smaller than-SIZE
The gif below summarizes the expected output:
We need the following components to build this counter system:
- Adder (combinational logic device): to compute value of the counter and check for overflow or negative value
- Several DFFs (sequential logic device): to hold the counter, incremeter, and speed control pointer
- FSM (sequential logic device): to control the overall operation of the counter system, including starting, stopping, and adjusting the speed or increment values based on button inputs
- Multiplexers (combinational logic device): to select between different inputs (e.g., choosing when to add or subtract from the incrementer, or when to increase or decrease the counter speed).
Each section below elaborates how to do it in Lucid and how to test your code in the Simulator. Create a new project with Base Project as your template. Then follow this guide.
Task 1: Adder
An adder unit is a digital circuit used to perform addition of binary numbers.
- Adders are fundamental building blocks in arithmetic logic units (ALUs), processors, and many other digital systems.
- They are essential for executing operations such as addition, subtraction (using two’s complement), multiplication, and division in digital circuits.
- This adder unit can be set to perform addition or subtraction, and check for overflow or negative value.
An adder is a combinational logic device. This means that its output depends solely on the current inputs, and it does not have any memory or feedback loops.
Create an adder module in your project.
- This module requires a parameter list:
SIZE
: determines the size of the adder
- In the port list:
- Define the inputs:
a
andb
(SIZE
bit each),subtract
(1 bit,0
to add,1
to sub) - Define the output:
s
(SIZE
bit, computing the add/sub output),v
(1 bit, indicating overflow),n
(1 bit, indicating negative output)
- Define the inputs:
- In the module body:
- There’s no need to instantiate any module
- Write the adder logic in the
always
block
Hints
The output s
should be set to a+b
or a-b
depending on the value of subtract
. You can use the if-statement for this. if-statement are equivalent to multiplexers.
-
and+
operators are called expressions in HDL.- More precisely, they are arithmetic expressions
- Lucid supports similar expressions to Verilog and common programming languages. Give this reference guide a read.
To compute v
, you should consider how overflow happens:
- Positive overflow: the result
s
is out of range of the positive number - Negative overflow: the result
s
is out of range of the negative number
For instance:
// assume SIZE = 4
a = 4b1000 // -8
b = 4b1111 // -1
subtract = 0
s
becomes 4b0111
(7) when it’s supposed to be 5b10111
(-9
). This is negative overflow.
Computing n
is straightforward: connect it to the MSB of s
. However, since s
is an output port of the adder, you can’t connect an output port to another output port:
n = s[SIZE-1] // NOT ALLOWED
You can “name” an intermediary signal using the sig
type. This is the format:
sig sig_name
always {
sig_name = expression
}
For instance, you can do this:
sig addition_result[SIZE]
always{
addition_result = a + b
s = addition_result
n = addition_result[SIZE-1]
}
The statements are evaluated top-down. This means lower statements take precedent over higher ones. For more information, read the reference guide on always blocks.
For example, if you have accidentally set s
to be 0
at the bottom of the always block, it will be set as 0
regardless of what you set above. Remember that the always
block defines connections that are evaluated in parallel in each clock cycle.
always{
s = a + b
// other logic
s = 0 // s will always be zero
}
Test With I/O & Simulate
The I/O shield comes with 24 dip switches, 24 io led, 4 7-segment units, and 5 io buttons as follows:
To access it, add the IO constraints from the component library. Open the component library window:
Then select the Io constraint:
Then add the access ports at alchitry_top
:
module alchitry_top (
input clk, // 100MHz clock
input rst_n, // reset button (active low)
output led[8], // 8 user controllable LEDs
input usb_rx, // USB->Serial input
output usb_tx, // USB->Serial output
output io_led[3][8], // LEDs on IO Shield
output io_segment[8], // 7-segment LEDs on IO Shield
output io_select[4], // Digit select on IO Shield
input io_button[5], // 5 buttons on IO Shield
input io_dip[3][8] // DIP switches on IO Shield
)
What the .acf
file do is to give a logical name to the FPGA pins so that we can program its behavior. For instance, B28
is used as an input pin with a pulldown internal resistor set. This means that if we don’t supply a high voltage at pin B28
, it will register as 0
(digital low). Activating this input requires a high voltage supply, and thus we also call this input as being active high.
The pins without pulldown
are output pins. You can physically connect this to LED bulbs via the Br Board. Our Br board has custom schematic that you can find here. Here’s a copy of it:
Here’s where each LED/button are situated:
The 7 Segment
io_select
uses a hot encoding to select which of the 4 segments to light up, andio_segment
uses hot encoding to control which of the 8 LEDs within the selected segment are illuminated. This means that only one segment can be illuminated at a time.
io_select
rapidly switches between the 4 segments to give the illusion that all segments are illuminated simultaneously, even though only one segment is lit at any given time.
Right now your alchitry_top
should contain some errors because you have not set the output of the io_led
, io_segment
, and io_select
. Add the following code into the always
block:
io_led = 3x{ {8h00} }
io_select = 4hF
io_segment = 8h00
When you press simulate, you should see the io_shield
being shown stacked on top of the fpga.
With this, you can test your adder. Instantiate an N-bit adder in your alchitry_top:
const SIZE = 8 // set SIZE to any number you want
adder adder(#SIZE(SIZE))
Then connect its I/O in the always
block:
adder.a = io_dip[0]
adder.b = io_dip[1]
adder.subtract = io_dip[2][0]
io_led[0] = adder.s
led[2:0] = c{adder.z, adder.v, adder.n} // signal concatenation
When you click simulate, you can manually test the adder using the io dips and observe its output at the io_led and led.
Concatenation and Duplication
Concatenation provides a way to merge two or more arrays. It takes the form
c{ expr1, expr2, ... }
where allexpr
are arrays or bits.Duplication provides a way to concatenate a single value many times with itself. It takes the form
const_expr x{ expr }
whereconst_expr
is a constant expression indicating how many times to duplicateexpr
.For instance,
io_led = 3x{ {8h00} }
is equivalent to a two dimensional array:{8h00, 8h00, 8h00}
(3 by 8 bits).
Task 2: dff
From our lectures, we learned that A dff is a sequential logic circuit that captures the input value (d
) on the rising edge of the clock and stores it as the output (q
), synchronizing the stored value with the clock signal.
Recap
The dff updates its output
q
with the inputd
on every rising clock edge.
We need 6 dffs to hold the following value:
- The state of the counter: increase, decrease, faster, slower, or idle
- The current value of the counter
- The current value of the increment (how much to increment the counter with)
- The speed control pointer (how fast to increment the counter)
- The state of the slow clock (enabled/disabled)
- A counter dff (to generate slow clock signal)
The FSM diagram for this machine is roughly as follows. Let’s work towards creating it in alchitry_top.luc
:
You will formally learn about FSM next week during lecture. We shall tap on abstracted FSM knowledge obtained from DDW 10.020 for now.
Generate the Slow Clock Signal
You should tackle the simplest feature first: to generate the slow clock signal. The FPGA clock is 100MHz, which is too fast for the human eye. We can create a slow_clock
using two dffs: the slow clock dff and the speed control pointer dff.
The FPGA clock runs at 100MHz and if we start our counter with this clock, it will increase too fast for the human eye to catch. We can create a slow clock using two dffs:
counter
dff: an N bit dff which value is always increased by 1 each time the FPGA clock risesspeed_pointer
dff: alog2(N)
bit dff which points to certain bit of thecounter
dff, effectively producing a slow clock signal
This is also known as a frequency divider. You have seen this in the blinker project. Generate this signal in your project now. You can instantiate the dffs as such in lucid:
sig slow_clock
const SIZE = 8 // set SIZE to any value you want
const SLOW_CLOCK_DEFAULT_SPEED = $is_sim() ? 8 : 28 // put 8 for sim, 28 for hardware
.clk(clk){
.rst(rst){
dff counter[SIZE]
dff speed_pointer[$clog2(SIZE)](#INIT(SLOW_CLOCK_DEFAULT_SPEED))
}
}
We use connection blocks to connect clk
and rst
signal required by both dffs. Later in the always body, you should set the .d
port input of each dff:
always{
counter.d = counter.q + 1 // by default: always increment by 1
speed_pointer.d = speed_pointer.q // by default: leave it unchanged
}
Edge Detector
You need to pass the slow_clock
signal through an edge_detector
so that you will increment/decrement the counter exactly once each time the slow clock rises. Alchitry comes with edge detector as a standard component. Add the edge_detector
under Pulses:
You can then use the edge detector as follows:
.clk(clk){
edge_detector slow_clock_edge(#RISE(1), #FALL(0))
}
always{
slow_clock_edge.in = slow_clock
}
Toggle the counter
The next thing to do is to start/stop the counter. Since we use the toggle feature (using io button 0 to start and stop), we need to rely on some external value: slow clock enabled. This value should be stored in a 1-bit dff. This dff’s output then determine whether we shall feed in actual slow_clock
to the edge detector or a 0
signal (to stop it):
.clk(clk){
.rst(rst){
dff slow_clock_enable(#INIT(0))
}
}
always{
// default connection
slow_clock_enable.d = slow_clock_enable.q
// this is a mux deciding whether we stop the clock or set it with certain speed
case (slow_clock_enable.q){
0:
slow_clock_edge.in = 0 // always don't increment or decrement
1:
slow_clock_edge.in = slow_clock
default:
slow_clock_edge.in = 0
}
}
Case
The case statement provides a way to cleanly write a group of conditional statements that depend on the value of a single expression. Case statements are also equivalent to multiplexers.
Task 3: FSM
Up until this point, we have created 3 dffs: counter
, speed_pointer
and slow_clock_enable
. It is now time to create the FSM dff to hold our 8 states. We can use enum
in Lucid to name the states conveniently:
enum States {
RUN,
STOP,
FASTER,
SLOWER,
INCREASE,
DECREASE,
UPDATE,
IDLE
}
Then we create a 3-bit dff to “hold” these states. While we’re at it, create the remaining two dffs that hold the current counter value and the increment value:
dff states[$width(States)](#INIT(States.IDLE))
dff current_value[SIZE](#INIT(0))
dff current_delta[$clog2(SIZE)+1](#INIT(1)) // increment is initially 1, maxed at SIZE
In the always block, we can then decide what to do depending on states.q
. For instance, in States.RUN
and States.STOP
we change the value of slow_clock_enable
dff accordingly to start/stop the clock signal entering the edge detector.
always{
// default: no change in dff values
// these are placed at the top of the always block, may be overriden later
states.d = states.q
current_delta.d = current_delta.q
current_value.d = current_value.q
// other code
case(states.q){
States.IDLE:
// watch out for button presses / slow clock signal, transition to different states accordingly
// this takes precedence, placed as the first clause
if (slow_clock_edge.out)
{
states.d = States.UPDATE
}
else ... (other triggers such as button presses)
States.UPDATE:
// simple update, did not consider overflow or negative value
current_value.d = current_delta.q // adds current_value dff by current_delta
states.d = States.IDLE
States.RUN:
slow_clock_enable.d = 1
states.d = States.IDLE
States.SLOWER:
// clamp to 31st bit at max
if (speed_pointer.q < SIZE-1){
speed_pointer.d = speed_pointer.q + 1
}
states.d = States.IDLE
States.DECREASE:
// simple subtraction, did not consider negative overflow
current_delta.d = current_delta.q - 1
states.d = States.IDLE
// other states
}
}
Note how each state always returns to IDLE in the next FPGA clock cycle. This arrangement makes the FSM neat and easy to debug.
Task 4: Process Button Presses
We cannot simply do this in the IDLE state:
case(states.q){
States.IDLE:
// watch out for button presses / slow clock signal, transition to different states accordingly
// this takes precedence, placed as the first clause
if (slow_clock_edge.out)
{
states.d = States.UPDATE
}
else if(io_button[1]){
if (~|slow_clock_enable.q){ // if slow_clock is not currently enabled, run
states.d = States.RUN
}
else{
states.d = States.STOP
}
}
else if(io_button[0]){
states.d = States.FASTER
}
// other clauses
Managing button presses
We can’t use button presses directly to trigger the FSM because of the significant speed difference between human reactions and the FPGA clock (100 MHz). When a button, such as
io_button[i]
, is pressed, it will remain in the1
state for many clock cycles, as a typical button press lasts milliseconds, while each clock cycle occurs in nanoseconds. As a result, the FSM will rapidly toggle between states (e.g., betweenRUN
andSTOP
) multiple times during a single press, instead of transitioning just once as intended.
We need to use edge detectors to detect a down press (falling edge), as well as button conditioner to clean the signal.
Button Conditioner
Button Conditioner: This module will synchronize and debounce a button input so that you can reliably tell when it is pressed
Add Button Conditioner component to your project, then use it as follows:
const CLK_FREQ = $is_sim() ? 1000 : 10000000 // put 1000 only for sim, 10M on hardware
.clk{
// instantiate 5 edge detectors, one for each button
edge_detector io_button_edge[5](#RISE(5x1), #FALL(5x0))
// instantiate 5 conditioners, one for each button
button_conditioner io_button_cond[5](#CLK_FREQ(5x))
.rst(rst){
// other code
}
}
always{
// condition the io buttons, then take **rising** edges only
io_button_cond.in = io_button
io_button_edge.in = io_button_cond.out
}
Now you can use io_button_edge[i].out
to trigger state transition during the IDLE
state. Complete your FSM logic for state: IDLE, STOP, FASTER
, and INCREASE
.
Task 5: Test the simple FSM with I/O
You can set the io_led
to see the contents of the current_value
dff and current_delta
dff to check if you have set the states correctly:
led = c{slow_clock, 2b0, speed_pointer.q} // led[7] shows the `slow_clock` signal, led[5:0] shows the content of speed_pointer dff
io_led = {c{2b0,current_delta.q} ,current_value.q[SIZE-1:SIZE-8], current_value.q[7:0]}
You should observe the following:
current_value
is increased/decreased bycurrent_delta
steadily as perslow_clock
signal whenio_button[1]
is toggled (center io button)- When the counter is enabled, pressing
io_button[0]
(up button) orio_button[1]
(down button) will increase/decrease the speed of the counter. The value ofspeed_pointer
in `led{5:0} should change accordingly - Pressing
io_button[3]
(left button) orio_button[4]
will increase or decrease the value ofcurrent_delta
accordingly. When enabled, thecurrent_value
should increase/decrease bycurrent_delta
At this point we have a working dynamic counter but the two constraints are not yet met:
- The counter’s value (
SIZE
bits) should be clamped to zero and shall not become negative. It should not have positive overflow either.- This means
current_value
dff should not be made negative, even if we attempt to decrease it
- This means
- The counter shouldn’t be increased by a value larger than
SIZE-1
or smaller than-SIZE
- This means
current_delta
dff should not overflow
- This means
Task 6: Incorporate the constraints
Like any software projects, we should add the constraints only after we get the basic functionality going.
We can utilise the adder
unit we create earlier to check if increasing or decreasing the value of current_value
and current_delta
will violate the two constraints above. Here’s one example in the state INCREASE
:
// instantiation
adder delta_adder(#SIZE($clog2(SIZE)+1))
// always block
States.INCREASE:
// check overflow
delta_adder.a = current_delta.q
delta_adder.b = 1
// only update current_delta if it does not overflow
if(~delta_adder.v){
current_delta.d = delta_adder.out
}
states.d = States.IDLE
Sign extension
You might be tempted to do the following to test if updating current_value
will result in positive overflow:
// instantiation
adder value_adder(#SIZE(SIZE))
// always block
States.UPDATE:
value_adder.a = current_value.q // current_value is of size SIZE
value_adder.b = delta_adder.q // delta_adder is of size $clog2(SIZE)+1
// check for overflow and negative value
if (~adder.n & ~adder.v){
// update current_value
current_value.d = adder.out
}
However, since the number of bits of delta_adder
and current_value
is not the same (log2(SIZE)
vs SIZE
), you won’t have the intended behavior when checking for overflow or negative value.
For example, if delta_adder.q
is -1
: 111
and current_value.q
is 8h0
, adding the two of them results in: 8h0 + 8h07 = 8h07
which is not the desired value of 0 + -1 = -1
.
Sign extension is the process of increasing the bit-width of a signed binary number while preserving its value and sign. When extending the bit-width of a number, the most significant bit (MSB), which represents the sign in two’s complement representation, is replicated in the new bits. This ensures that the value and sign of the number remain consistent.
For example:
- A 4-bit signed number
1010
(which represents -6 in two’s complement) can be sign-extended to 8 bits as11111010
, keeping the value as -6.
This is crucial when performing arithmetic operations between values of different bit-widths to maintain correctness in signed operations.
You would need to sign extend current_delta
. That is to take the MSB of current_delta
and extend it to make up to SIZE
bits, which is the size of current_value
. You can do this using concatenation and duplication in Lucid.
Checkoff
You can work as your 1D group and obtain checkoffs from your TA by next lab session. You may demonstrate the simulation over Teams call or in-person. You can also build the project and demonstrate it on your FPGA.
You should demonstrate the following behavior:
- The counter can be started and stopped at any time
current_delta
can be increased or decreased, and not overflowcurrent_value
will not suffer from positive overflow, and clamped at 0 (will not go negative)- You can vary the speed of the counter
Bonus: utilizing the 7 Segment
You can display the value of the counter using the 7 segment. The problem is that you need to convert the binary value of current_value
into decimal value.
Alchitry has a component to do this called Binary to Decimal. Add this to your project:
We also need a 7 segment controller to multiplex it. Create a new file called multi_seven_seg
with the following content:
module multi_seven_seg #(
DIGITS = 4 : DIGITS > 0,
DIV = 16 : DIV >= 0
)(
input clk, // clock
input rst, // reset
input values[DIGITS][4], // values to show
output seg[7], // LED segments
output sel[DIGITS] // Digit select
) {
// number of bits required to store DIGITS-1
const DIGIT_BITS = $clog2(DIGITS)
.clk(clk), .rst(rst) {
counter ctr (#DIV(DIV), #SIZE(DIGIT_BITS), #TOP(DIGITS-1))
}
seven_seg seg_dec // segment decoder
decoder digit_dec (#WIDTH(DIGIT_BITS)) // digit decoder
always {
seg_dec.char = values[ctr.value] // select the value for the active digit
seg = seg_dec.segs // output the decoded value
digit_dec.in = ctr.value // decode active digit to one-hot
sel = digit_dec.out // output the active digit
}
}
Now we are ready to use it in alchitry_top
:
const SEVEN_SEG_DIV = $is_sim() ? 3 : 16
// instantiation
.clk(clk){
.rst(rst){
multi_seven_seg seg(#DIV(SEVEN_SEG_DIV))
}
}
bin_to_dec decimal_renderer(#DIGITS(4), #LEADING_ZEROS(1))
// always block
decimal_renderer.value = current_value.q // convert the binary output to decimal value
seg.values = decimal_renderer.digits // plug the decimal digits into 7seg controller
io_segment = ~seg.seg
io_select = ~seg.sel
Summary
The goal of this lab is to give you an overview of crucial Lucid syntaxes. It is important that you give the reference guide a read yourself, and read the components library for more samples.
Here’s what we have covered in this lab thus far:
- Basic Alchitry lab navigation: creating a new project, adding components, running simulation
- Module structure: parameters, module body (instantiation), and
always
block (logic implementation) - How
always
block works in HDL - Accessing I/O via
alchitry_top
- Constraint file: purpose and relationship with the I/O pins
- Understanding active
low
(pullup
) vs activehigh
(pulldown
)
- Understanding active
- Creating a basic module: adder (combinational logic device). In doing this, we learn a few things about Lucid’s syntax:
- Parameter type:
SIZE
for the adder sig
type- Statement evaluation in Lucid: **top-down **
- Array concatenation and duplication
- Parameter type:
- Creating and using
dff
- Slowing down the default 100 MHz FPGA clock by creating a frequency divider: a basic counter
dff
and a speed pointerdff
- FPGA Clock (100MHz) and Simulation Clock (1000Hz) is different
- Instantiating several modules at once with connection blocks
- Implementing multiplexers using if-statement or case-statement
- Creating FSM using
enum
anddff
(sequential logic device) - Using inbuilt functions:
$clog2()
,$isSim()
,$width()
- Cleaning up button press signals using
button_conditioner
andedge_detector
components - Using inbuilt 7-segment
Ensure that you revisit this lab if any of the points above still sound unfamiliar to you. Afterwards, you need to give these tutorials (made by the original author) a read and try on your own time. Consult your instructor/TAs if you have any enquiries. It is imperative that you start early and not let your doubts snowball!