Lab 5. Instruction Decoder
The control unit (CU) is one of the core blocks of a processor, responsible for decoding instructions and generating control signals for all processor blocks. In this course, the role of the CU (with some caveats) is played by the instruction decoder.
Objective
Describe an instruction decoder block for a single-cycle RISC-V processor in SystemVerilog.
Preparation Materials
- Instruction encoding formats of the
RV32Ibase instruction set. - Theory on control and status registers.
- Differences between blocking and non-blocking assignments.
Workflow
- Study the microarchitecture of the processor core being implemented.
- Understand the logic for generating control signals for all instruction types.
- Study the instruction decoder signal descriptions.
- Study the supported RISC-V instruction set and encoding schemes.
- Study the SystemVerilog constructs to be used for describing the decoder (#tools).
- Implement the instruction decoder in SystemVerilog (#task).
- Verify correct operation using the verification environment.
Proposed RISC-V Processor Microarchitecture
Fig. 1 shows the microarchitecture of the RISC-V processor core being implemented.
The architecture shown is not the assignment for this lab — it merely illustrates how the instruction decoder implemented here will be connected and used later.
Figure 1. Microarchitecture of the future processor core.
The proposed microarchitecture is similar to the CYBERcobra processor microarchitecture from Lab 4, but with several changes.
First, the processor's inputs and outputs have changed:
- The instruction memory has been moved outside the processor, so the processor now has the
instr_addr_oandinstr_iports; - The processor also has data memory interface signals:
mem_addr_o— external memory address;mem_req_o— request to access external memory;mem_size_o— data size for memory access;mem_we_o— write enable signal for external memory;mem_wd_o— data to write to external memory;mem_rd_i— data read from external memory. These signals are used when executing load/store instructions.
- The processor also has a
stall_iinput that halts the program counter update.
Additionally, two new modules have appeared: Interrupt Controller and Control Status Registers. These modules will provide interrupt support in the processor system.
ALU operand sources have also been added: the program counter, various constants from instructions, and microarchitectural constants — requiring these signals to be multiplexed.
The write-back sources to the register file have also changed, and now include:
- the result of an ALU operation;
- data read from external memory;
- data from the control and status registers module.
To control the expanded set of multiplexers, the data memory interface, and the new modules, a dedicated unit is needed — the Control Unit (CU). In this microarchitecture, the control unit logic is not separated into its own module; it is only highlighted in blue on the diagram. For the most part, the instruction decoder serves as the control unit in the proposed microarchitecture.
Instruction Decoder Signal Descriptions
The list of instruction decoder ports and their descriptions is given in Table 1.
| Signal Name | Description |
|---|---|
| fetched_instr_i | Instruction to be decoded |
| a_sel_o | Multiplexer control signal for selecting the first ALU operand |
| b_sel_o | Multiplexer control signal for selecting the second ALU operand |
| alu_op_o | ALU operation |
| csr_op_o | CSR module operation |
| csr_we_o | CSR write enable |
| mem_req_o | Memory access request (part of the memory interface) |
| mem_we_o | Memory write enable signal (when zero, a read is performed) |
| mem_size_o | Control signal for selecting data transfer size during memory read/write (part of memory interface) |
| wb_sel_o | Multiplexer control signal for selecting data to write back to the register file |
| gpr_we_o | Register file write enable signal |
| branch_o | Signal indicating a conditional branch instruction |
| jal_o | Signal indicating an unconditional jump instruction jal |
| jalr_o | Signal indicating an unconditional jump instruction jalr |
| mret_o | Signal indicating a return-from-trap instruction mret |
| illegal_instr_o | Signal indicating an illegal instruction |
Table 1. Instruction decoder port descriptions.
This module has only one input: fetched_instr_i — the instruction currently being decoded. All other signals are module outputs, which can be grouped into several classes.
Operation Code Signals
This class includes signals that tell an individual functional block which operation to perform. There are two such blocks: the ALU and the Control and Status Registers module. The ALU can perform one of 16 operations (introduced in Lab 2), and this signal is used to select which one. You are not yet familiar with the CSR module that appeared in the microarchitecture, but for now it is enough to understand that it can also perform one of several operations and requires a dedicated signal for that purpose.
The operation code signal class therefore includes:
alu_op_o,csr_op_o.
For convenience, the possible values of these signals are defined as parameters in the alu_opcodes_pkg and csr_pkg packages respectively.
Execute-Stage and Write-Back Multiplexer Control Signals
This class includes signals controlling the multiplexers located on the right side of the diagram:
a_sel_o,b_sel_o,wb_sel_o.
The a_sel_o and b_sel_o signals determine the source of data for ALU operands a_i and b_i respectively. For example, if both operands should come from the register file, the value 0 must be applied to both multiplexer control inputs.
The wb_sel_o signal determines the write-back data source for the register file: either the result of an ALU operation, data read from data memory, or data from the control and status registers module.
Memory Interface
Data memory is used to store and access information required for program execution. Although data memory and the register file both store data, their roles are different: the register file stores data that is being actively processed (within a few instructions), while data memory holds all other information that does not fit in the register file due to its limited size.
The instruction decoder will use the following signals to interact with the data memory subsystem:
mem_req_o— this signal must be asserted to 1 whenever a memory access is required (read or write);mem_we_o— this signal must be asserted to 1 when writing to memory (0 for reads);mem_size_o— this signal specifies the data transfer size (possible values are listed in Table 2). For convenience, these values are defined as parameters in thedecoder_pkgpackage.
| Parameter | mem_size_o Value |
Description |
|---|---|---|
| LDST_B | 3'd0 | Signed 8-bit value |
| LDST_H | 3'd1 | Signed 16-bit value |
| LDST_W | 3'd2 | 32-bit value |
| LDST_BU | 3'd4 | Unsigned 8-bit value |
| LDST_HU | 3'd5 | Unsigned 16-bit value |
Table 2. Values of the mem_size_o signal for different data transfer sizes.
These signals are sufficient for the main memory to understand whether it is being accessed at a given moment, whether a read or write is required, and what size of data is being transferred.
Write Enable Signals
This category includes two single-bit signals:
gpr_we_o— register file write enable signal (General Purpose Registers, GPR);csr_we_o— write enable signal for the control and status registers module.
Program Counter Control Signals
This category includes single-bit signals that indicate an instruction related to a program counter update is being executed:
branch_o— signal indicating a conditional branch instruction;jal_o— signal indicating an unconditional jump instructionjal;jalr_o— signal indicating an unconditional jump instructionjalr;mret_o— signal indicating a return-from-trap instructionmret.
Illegal Instruction Signal
A signal that must be set to 1 when an instruction arrives that is not in the processor's supported instruction list.
This is not the only action the decoder must take in such a situation. Let us examine in detail what should happen when an illegal instruction arrives.
Illegal Instruction Handling
There are many reasons why an unsupported instruction might reach the processor for execution, including:
- a compilation error: either a bug in the compiler itself or compilation with incorrect parameters;
- a hardware fault (e.g., a memory malfunction);
- a deliberately inserted unsupported instruction (e.g., to exploit a vulnerability);
- an instruction that is actually supported by the processor but requires a higher privilege level and therefore cannot be executed.
When an unsupported instruction arrives, the control unit must ensure system stability. In the simplest case, the instruction must be skipped while preserving the processor's so-called architectural state — that is, the values of all elements that characterize the current system state. These elements include the register file contents, main memory contents, CSR contents, etc. The program counter value is also part of the architectural state; however, in the context of skipping an instruction while preserving the architectural state, its value must be changed — otherwise the system would be stuck in an infinite loop (an unchanged counter would keep pointing to the same instruction that is not supposed to modify the architectural state).
In other words, when an illegal instruction arrives, the control unit (whose role is largely played by the decoder in our system) must ensure that nothing in the system changes except the program counter. The signals that affect the architectural state are:
mem_req_o,mem_we_o,gpr_we_o,csr_we_o,branch_o,jal_o,jalr_o,mret_o.
That is, all write requests, memory accesses, and any program counter "jumps" must be disabled.
Let us now determine exactly which instructions our processor must support.
Supported RISC-V Instruction Set and Encoding Schemes
All RISC-V architecture instructions can be broadly divided into three categories.
- Computational instructions (operations performed on the ALU with the result written to the register file). These are primarily instructions:
- using two registers as operands;
- using a register and an immediate operand from the instruction (a constant).
- Memory access instructions:
- loading from main memory into the register file;
- storing data from the register file to main memory.
- Control instructions:
- Conditional branches
- Unconditional jumps
- System instructions
- access to control and status registers;
- system calls and return from interrupt handler.
Table 3 shows a fragment from the RISC-V specification. The upper portion lists 6 instruction encoding formats: R, I, S, B, U, and J (format descriptions are given in Table 4). Below that is a list of all instructions with specific field values corresponding to the encoding format of each instruction type.
rd denotes the 5-bit destination register address (register destination), rs1 and rs2 are 5-bit source register addresses (register source), imm is an immediate operand (a constant encoded directly in the instruction), with bit positions and ordering indicated in square brackets. Note that immediate operands have different widths in different encoding formats, and their bits are arranged differently. Immediate operands of all types are interpreted as signed values and require sign extension [1, p. 23]. The exception is 5-bit immediate operands used in CSR instructions.
Table 3. Base instruction set from the RISC-V specification [1, p. 554], the standard Zicsr extension [1, p. 556], and the privileged instruction mret [2, p. 51].
| Encoding | Description |
|---|---|
| R-type | Arithmetic and logical operations on two registers, with the result written to a third (the destination register may be the same as one of the source registers) |
| I-type | Instructions with a 12-bit immediate operand |
| S-type | Store instructions (write to memory) |
| B-type | Branch instructions |
| U-type | Instructions with a 20-bit "long" immediate operand, shifted left by 12 |
| J-type | The single jal instruction, performing an unconditional jump relative to the current program counter |
Table 4. RISC-V ISA instruction encoding format descriptions.
RISC-V Instruction Decoding
As described in the preparation materials, instruction decoding begins with the opcode field (operation code). This field identifies a group of instructions of the same type. The instruction is then further specified (for most encoding types) through the func3 and func7 fields (when present). Note that the positions of these fields are the same across all instruction types (see the upper portion of Table 3).
The rs1/rs2/imm and rd fields are not needed by the decoder and are used directly for register addressing and constant specification.
Some instructions have no variable fields (for example, the ECALL instruction in Table 3). Such instructions must be checked in their entirety (the full 32-bit value must match).
Table 5 lists all opcodes of the instructions we are implementing. The opcodes shown are 5 bits because the 2 least significant bits of the full 7-bit opcode must always be 11 for all instructions we are implementing. If this is not the case, the entire instruction is already illegal and requires no further decoding.
For convenience, the opcode values are defined as parameters in the decoder_pkg package.
| Parameter | Opcode | Operation Group Description | Short Notation |
|---|---|---|---|
| OP | 01100 | Write the ALU result of rs1 and rs2 to rd |
rd = alu_op(rs1, rs2) |
| OP_IMM | 00100 | Write the ALU result of rs1 and imm to rd |
rd = alu_op(rs1, imm) |
| LUI | 01101 | Write the U-type immediate operand imm_u to rd |
rd = imm << 12 |
| LOAD | 00000 | Write memory data at address rs1+imm to rd |
rd = Mem[rs1 + imm] |
| STORE | 01000 | Write rs2 data to memory at address rs1+imm |
Mem[rs1 + imm] = rs2 |
| BRANCH | 11000 | Increment the program counter by imm if the comparison of rs1 and rs2 is true |
if cmp_op(rs1, rs2) then PC += imm |
| JAL | 11011 | Write the next program counter value to rd, increment the program counter by imm |
rd = PC + 4; PC += imm |
| JALR | 11001 | Write the next program counter value to rd, set the program counter to rs1+imm |
rd = PC + 4; PC = rs1+imm |
| AUIPC | 00101 | Write the sum of the U-type immediate operand imm_u and the program counter to rd |
rd = PC + (imm << 12) |
| MISC-MEM | 00011 | Perform no operation | - |
| SYSTEM | 11100 | Write the csr value to rd. Update csr using rs1. (or execute mret/ecall/ebreak) |
csr = csr_op(rs1); rd = csr |
Table 5. Opcode descriptions.
SYSTEM Instructions
SYSTEM instructions are used to access system functions and may require privileged access. These instructions can be divided into two classes:
- Access to Control and Status Registers (CSR)
- All other instructions (possibly from the privileged instruction set)
To support interrupts in the future, we need to decode instructions of both classes.
Access to the control and status registers is performed using the six instructions of the standard Zicsr extension. Each of these instructions (if its fields are legal) performs a write to the CSR and to the register file (the Control Status Registers and Register File blocks in Fig. 1 respectively).
Additionally, to return control to the main instruction stream, an additional SYSTEM instruction from the privileged instruction set is needed: MRET.
The instructions listed above are "extensions" — they were added on top of the standard instruction set to provide the functionality required by our system. However, there are two more SYSTEM instructions that we must be able to decode, since they are part of the standard instruction set.
The ECALL and EBREAK instructions trigger an exception. Exceptions and interrupts will be covered in detail in Lab 10; for now, it is enough to know that in our processor system all exceptions will be implemented by asserting 1 on the illegal_instr_o signal.
MISC-MEM Instructions
In the RISC-V base instruction set, the MISC-MEM operations include FENCE, FENCE.TSO, and PAUSE (combined into a single FENCE instruction in Table 5). In the processor core being implemented, this instruction must not cause any state changes. The FENCE instruction in RISC-V is needed when working with multiple hardware threads, or "harts" (hart — "hardware thread"). It helps synchronize data access between them. RISC-V uses a relaxed memory model, which allows threads to observe operations of other threads, but not necessarily in the order they appear in program code. A FENCE instruction placed between two read and/or write instructions guarantees that other threads will see the first instruction before the second. FENCE implementation is optional in RISC-V, and in our case it is not necessary since the system does not involve multiple hardware threads. This instruction must be implemented as a NOP (no operation).
Table 6 shows the instructions from Table 3 with their types, opcode, func3, func7 field values, functional descriptions, and usage examples.
Table 6. Extended description of RV32IZicsr instructions.
Important
Note the
slli,srli, andsraiinstructions (constant-shift operations). These instructions use a slightly modified I* encoding format. The I encoding format provides a 12-bit immediate. Shifting a 32-bit number by more than 31 makes no sense. Only 5 bits are needed to encode the value 31. Therefore, only 5 bits of the 12-bit immediate are used for the shift amount (denoted as theshamtfield, short for shift amount), while the remaining 7 bits are unused. Crucially (what a coincidence!), these 7 bits occupy exactly the same position as thefunc7field in other instructions. Therefore, to avoid wasting this portion of the field inslli,srli, andsrai— which use the I format — it is treated as thefunc7field. This is precisely why in Lab 2 we used only the 5 least significant bits of operandB— to discard the portion of the immediate that serves asfunc7in shift operations.
Important
Also note the
ecall,ebreak, andmretinstructions. All of these are I-type instructions with afunc3field equal to zero. From a decoding standpoint, they appear to be the same instruction type with differentimmvalues. However, in this specific case (SYSTEM_OPCODE andfunc3 == 0), these instructions must be treated as a full 32-bit match (see Table 3).
Control Signal Generation
As discussed earlier, the instruction decoder in a processor is responsible for converting an instruction into the set of control signals required to execute it. For each instruction in Table 3, the decoder must assign a specific value to each of the outputs listed in Table 1.
Example: to execute the instruction that writes 32-bit data from the register file to external memory (the sw instruction), the decoder must direct two operands (a base address and an offset) to the ALU along with the ALU operation code (addition) to compute the write address. The base address comes from the register file, and the offset is the immediate operand of the S-type instruction. To compute the write address, the decoder must assert the following output values:
a_sel_o = 2'd0,b_sel_o = 3'd3,alu_op_o = ALU_ADD.
(see Figure 1).
Additionally, for the memory write operation itself, the decoder must generate the memory interface control signals (memory access request, data transfer size, and write enable):
mem_req_o = 1'b1,mem_size_o = LDST_W(see Table 2),mem_we_o = 1'b1.
Although the signals described above are the key ones for a memory write operation, this does not mean that the remaining decoder output signals can take arbitrary values.
Since the sw operation is not a jump instruction, the jal_o, jalr_o, branch_o, and mret_o signals must be zero (otherwise the processor would jump, which the sw instruction does not imply). Similarly, since nothing should be written to the register file or CSRs during a memory write, gpr_we_o and csr_we_o must also be zero.
In other words, it is critically important to track all output signals that affect the processor's architectural state and that are not explicitly involved in the current instruction.
The wb_sel signal, however, can take any value (since the register file write enable is zero, it does not matter what the write-back source is — nothing will be written to the register file regardless).
Of course, when implementing the instruction decoder module, it would be impractical to specify values for all 14 outputs for each of the 47 instructions, especially since many outputs have the same value for all instructions sharing the same opcode. It is most convenient to describe them grouped by opcode.
Table 7 lists the instruction decoder output signals and the instruction groups for which these outputs can take a non-zero value.
| Signal Name | Description | Opcodes for Which a Non-Zero Value Is Possible (see Table 6) |
|---|---|---|
| a_sel_o | Multiplexer control signal for selecting the first ALU operand | All except MISC_MEM and SYSTEM |
| b_sel_o | Multiplexer control signal for selecting the second ALU operand | All except MISC_MEM and SYSTEM |
| alu_op_o | ALU operation | All except MISC_MEM and SYSTEM |
| csr_op_o | CSR module operation | SYSTEM only |
| csr_we_o | CSR write enable | SYSTEM only |
| mem_req_o | Memory access request (part of the memory interface) | LOAD and STORE |
| mem_we_o | Memory write enable signal (when zero, a read is performed) | STORE only |
| mem_size_o | Control signal for selecting data transfer size (part of memory interface) | LOAD and STORE |
| gpr_we_o | Register file write enable signal | All except STORE, BRANCH, MISC_MEM |
| wb_sel_o | Multiplexer control signal for selecting write-back data | All except STORE, BRANCH, MISC_MEM |
| illegal_instr_o | Signal indicating an illegal instruction | All except JAL, LUI, AUIPC |
| branch_o | Signal indicating a conditional branch instruction | BRANCH only |
| jal_o | Signal indicating an unconditional jump instruction jal | JAL only |
| jalr_o | Signal indicating an unconditional jump instruction jalr | JALR only |
| mret_o | Signal indicating a return-from-trap instruction mret | SYSTEM only |
Table 7. Instruction decoder port descriptions.
The decoder must assert illegal_instr_o in the following cases:
- the two least significant bits of the opcode are not equal to
11; - the
opcodefield value does not match any known opcode, and therefore the operation is undefined; - the
func3orfunc7fields contain invalid values for the given opcode.
Furthermore, since the microarchitecture shown in Fig. 1 supports only one exception (via the illegal_instr_o signal), this signal must also be asserted in the case of:
- an
ECALL/EBREAKinstruction.
Tools
SystemVerilog is a hardware description language. Using this language, a designer either tells a synthesizer what hardware to produce, or tells a simulator how to verify that hardware. A synthesizer is a program that builds a digital device from logic elements based on the description provided. The synthesizer inside Vivado needs to be told what is expected of it. For instance, to ask directions from a Spanish speaker you need to speak Spanish — otherwise they cannot help. And if you speak Spanish well, you can likely phrase the same question in multiple ways. SystemVerilog works the same way — the same device can be described by different code, but the synthesis result may be identical. However, two semantically equivalent descriptions can sometimes synthesize into different hardware that is functionally identical yet may differ in performance, for example. Similarly, the same language constructs can be used to synthesize different digital elements.
The decoder is a combinational circuit. This means that the same input values will always produce the same output values.
Combinational circuits can be described in various ways — for example, using the continuous assignment operator assign. The case construct works well for describing a decoder; it synthesizes not into a multiplexer but into a combinational circuit with optimal critical-path characteristics. In the pre-HDL era, designers had to construct enormous truth tables and Karnaugh maps to find optimal implementations. Today, the synthesizer handles this task automatically — it finds the most efficient solution from the device description.
The difference from a multiplexer implementation is that here the right-hand side of every assignment is always a constant. This effectively becomes a way to describe a truth table. Such code is easy to edit and navigate.
Consider Listing 1. Inside the always_comb block, default values are specified before the case construct. This eliminates the need to specify all signals inside every case handler — only those that differ from the default value need to be listed. The example implements a combinational circuit that, when control_signal == 4'b1100, asserts c = 1'b0 (a value different from its default). Signal a is not modified, so it does not appear in the corresponding handler. If sub_signal == 1'b0, then b equals 1 and d equals 0. If sub_signal == 1'b1, the reverse is true — b equals 0 and d equals 1.
module example (
input logic [3:0] control_signal,
input logic sub_signal,
output logic a, b, c, d
);
parameter logic [3:0] SOME_PARAM = 4'b1100;
always_comb begin
a = 1'b0; // default values
b = 1'b0; // note that blocking assignment is used
c = 1'b1; // inside the always_comb block
d = 1'b0;
case(control_signal)
// ... some other combinations
SOME_PARAM: begin // if control_signal equals SOME_PARAM
c = 1'b0;
case (sub_signal)
1'b0: b = 1'b1; // if sub_signal is 1'b0
1'b1: d = 1'b1; // if sub_signal is 1'b1
endcase
end
// ... some other handlers
default: begin // since not all values of
a = 1'b0; // control_signal are listed, a default
b = 1'b0; // is required to prevent the case
c = 1'b1; // construct from inferring a latch
d = 1'b0;
end
endcase
end
endmodule
Listing 1. Example decoder description.
Keep in mind that the default values specified at the beginning of the always_comb block can only be used this way with blocking assignments (which should be used exclusively in combinational blocks).
Furthermore, the use of nested case blocks is only justified when implementing a decoder block (i.e., when all right-hand side values in assignments are constants, not other signals). When describing a multiplexer, nested case blocks may synthesize into a cascade of multiplexers, which will negatively affect the timing characteristics of the circuit.
Assignment
Implement the instruction decoder module for a single-cycle RISC-V processor in SystemVerilog, following the proposed microarchitecture. The module prototype is given in Listing 2.
module decoder (
input logic [31:0] fetched_instr_i,
output logic [1:0] a_sel_o,
output logic [2:0] b_sel_o,
output logic [4:0] alu_op_o,
output logic [2:0] csr_op_o,
output logic csr_we_o,
output logic mem_req_o,
output logic mem_we_o,
output logic [2:0] mem_size_o,
output logic gpr_we_o,
output logic [1:0] wb_sel_o,
output logic illegal_instr_o,
output logic branch_o,
output logic jal_o,
output logic jalr_o,
output logic mret_o
);
import decoder_pkg::*;
endmodule
Listing 2. Instruction decoder prototype.
Depending on coding style, the module may span over a hundred lines of code, but this does not make it difficult to implement. At its core, the decoder is simply a large case block describing which signals take which values under which conditions. The work requires attention, some patience, and a clear understanding of what is being done. There will almost certainly be bugs that need to be fixed. Bugs are normal — those who do nothing make no mistakes — and fixing them provides invaluable development experience. The implementation may feel tedious at times, but by the end of Lab 7, the satisfaction from the result will show that it was worth it.
Steps
- Carefully study the instruction decoder output signals and how they control the functional blocks of the processor core shown in Fig. 1, as well as the instruction types. If you have questions, consult the instructor.
- Add the file
alu_opcodes_pkg.svto the project'sDesign Sources(if it has not already been added during Lab 2), as well as the filescsr_pkg.svanddecoder_pkg.sv. These files contain parameters that will be useful when describing the decoder. - Describe the instruction decoder module with the same name and ports as specified in the task.
- For convenience, it is recommended to first create
opcode,func3, andfunc7signals and assign them the corresponding bits of the input instruction signal. - The module can be described in many ways: each output signal can be described through its own combinational logic in a separate
caseblock, but the simplest approach is to describe all signals using nestedcaseblocks inside a singlealways_combblock. - Inside the
always_combblock, before thecaseblock, you can specify default values for all output signals. This is not the same as thedefaultbranch in acaseblock. Here you can describe the states that will be most commonly used; a signal assignment then only needs to appear where an instruction requires a value different from the default. - You can then describe the main
caseblock, which determines the operation type from the opcode. - Once the operation type is identified, you can determine the specific operation from the
func3andfunc7fields (if present for that type). - Remember that if at any stage (type identification or specific operation identification) a field value arrives that is not defined in the ISA, the
illegal_instr_osignal must be asserted. - In the case of an illegal instruction, you must guarantee that no conditional or unconditional jump occurs, and that nothing is written to external memory, the register file, or the CSRs. It does not matter what the ALU executes or what data is selected at the write-back multiplexer. What matters is that no write actually occurs to any of these units (consider which signal values are needed to ensure this).
- For convenience, it is recommended to first create
- Verify the module using the verification environment provided in the file
lab_05.tb_decoder.sv. After the first run, you may encounter a large number of error messages. You must investigate these errors on the waveform and fix them in your module.- Before starting simulation, make sure the correct top-level module is selected in
Simulation Sources.
- Before starting simulation, make sure the correct top-level module is selected in
- This lab does not include verification on the FPGA board.

