Lab3_Multi-Output Circuits: Encoders, Decoders, and Memories

Introduction

Boolean expressions are used to output a Boolean function of a number of variables. Dataflow constructs like assign can be used to model such functions. Some circuits have multiple outputs and multiple inputs. In this lab, you will design encoders and decoders and read-only memories.

Multi-output Decoder Circuits

Decoders are combinatorial circuits that have multiple outputs. They are widely used in memory chips to select one of the words addressed by the address input. For example, an 8-word memory will have a three-bit address input. The decoder will decode the 3-bit address and generate a select line for one of the eight words corresponding to the input address. The 3-to-8 decoder symbol and the truth table are shown below.

drawing

Such circuits, also known as binary decoders, can be modeled using dataflow statements as only each output is true for a unique input combination.

Part3-1-1

Design a 3-to-8-line decoder. Use dataflow modeling constructs.

Create and add the Verilog module, naming it decoder_3to8_dataflow.v, that defines the 3-to-8 line decoder with three-bit input x and 8-bit output y. Use dataflow modeling constructs.

lab3_1_1.v


// Define the module with a 3-bit input x and an 8-bit output y.
module lab3_1_1(
    input [2:0] x,   // 3-bit input used to select one of the eight outputs
    output [7:0] y   // 8-bit output where only one bit is set to 1 based on input x
);

    // Implement the 3-to-8 line decoder functionality using dataflow modeling.
    // Each line of y is set to 1 only when x matches the corresponding binary value.
    // Otherwise, the line is set to 0.
    assign y[0] = (x == 3'b000) ? 1'b1 : 1'b0; // y[0] is high if x is 000
    assign y[1] = (x == 3'b001) ? 1'b1 : 1'b0; // y[1] is high if x is 001
    assign y[2] = (x == 3'b010) ? 1'b1 : 1'b0; // y[2] is high if x is 010
    assign y[3] = (x == 3'b011) ? 1'b1 : 1'b0; // y[3] is high if x is 011
    assign y[4] = (x == 3'b100) ? 1'b1 : 1'b0; // y[4] is high if x is 100
    assign y[5] = (x == 3'b101) ? 1'b1 : 1'b0; // y[5] is high if x is 101
    assign y[6] = (x == 3'b110) ? 1'b1 : 1'b0; // y[6] is high if x is 110
    assign y[7] = (x == 3'b111) ? 1'b1 : 1'b0; // y[7] is high if x is 111

endmodule

Now we can see the Schematic under the RTL ANALYSIS part like below:

drawing

Then we can run Simulation to check the code of the lab3_1_1 module.

decoder_3to8_dataflow_tb.v

// Define the testbench module with no ports as it's a top-level entity
module decoder_3to8_dataflow_tb();
    
    // Declare input signals as reg type to drive them with procedural assignments
    reg [2:0] x;
    // Declare output signals as wire since they will be driven by the instance of DUT (Device Under Test)
    wire [7:0] y;
    // Declare an integer for use in the for-loop
    integer k;
    
    // Instantiate the Device Under Test (DUT) with named port mapping
    lab3_1_1 DUT (.x(x), .y(y));   
 
    // Initial block to define the sequence of test cases
    initial begin
      // Initialize input
      x = 0; // Start with input 0
      
      // Loop through all possible inputs from 0 to 7
      for (k = 0; k < 8; k = k + 1) begin
		#5 x = k; // Assign the value of k to x and wait for 5-time units
      end
      #10; // Wait for 10 time units before ending the simulation
      
      // Optional: Add $finish to end the simulation explicitly.
      // Some simulation environments automatically end when there are no more events,
      // but others require $finish to terminate the simulation.
      $finish;
    end

    // Optional: Monitor changes in variables
    // Display changes in input and output to observe the test results
    initial begin
        $monitor("Time = %t, Input x = %b, Output y = %b", $time, x, y);
    end

endmodule

We can run Simulation to check the code by clicking the Run Simulation under the SIMULATION and choosing the first Run Behavioral Simulation.

drawing

Then, we can click on the Run synthesis under the SYNTHESIS and Run implementation under the IMPLEMENTATION. We should add the appropriate board-related master XDC file to the project and edit it to include the related pins like the below:

drawing

Because the board has four LEDs, so we can also use the RGB LED(LD5 and LD4).

drawing

Click on the Generate Bitstream under the PROGRAM AND DEBUG, and when writing bitstream is complete, we can download the file to the board and verify the lab.

drawing
drawing

Part3-1-2

Design and implement a popular IC, 74138, functionality using dataflow modeling and the decoder you used in 1-1. The IC symbol and truth table are given below.

drawing

X = don’t care

Note that this is very similar to the one you had created in 1-1; it has additional control (Enable) signals G1, /G2A, and /G2B. These enable signals to simplify decoding in some systems.

Create and add the Verilog module, named decoder_74138_dataflow, instantiating the model you had developed in 1-1. Add additional logic by using the dataflow modeling constructs to model the desired functionality.

lab3_1_2.v

module Lab3_1_2(
    input [2:0] x,        // 3-bit input
    input G1,             // Active-high enable
    input G2A_n, G2B_n,   // Active-low enables
    output [7:0] y        // 8-bit output
);

    // Internal signal for combined enable condition
    wire enable;
    wire [7:0] y_data;  
    // The decoder is enabled only if G1 is high and both G2A_n and G2B_n are low
    assign enable = G1 & ~G2A_n & ~G2B_n;

    // Instantiate the basic 3-to-8 decoder, passing the enable condition
    // The outputs are only asserted if the enable condition is true
    lab3_1_1 basic_decoder (
        .x(x),
        .y(y_data)
    );

    // Modify the output based on the enable condition using dataflow constructs
    // If 'enable' is not asserted, all outputs should be low.
    assign y[0] = enable & y_data[0];
    assign y[1] = enable & y_data[1];
    assign y[2] = enable & y_data[2];
    assign y[3] = enable & y_data[3];
    assign y[4] = enable & y_data[4];
    assign y[5] = enable & y_data[5];
    assign y[6] = enable & y_data[6];
    assign y[7] = enable & y_data[7];

endmodule

Then, we can run Simulation to check the code of the lab3_1_2 module.

decoder_74138_dataflow_tb.v

// Define the testbench module with no ports, as it's a top-level entity
module decoder_74138_dataflow_tb();
    
    // Declare input signals as reg type to drive them with procedural assignments
    reg [2:0] x;        // 3-bit address input
    reg g1, g2a_n, g2b_n; // Enable inputs for the decoder, g1 is active high, g2a_n and g2b_n are active low
    
    // Declare output signals as wire since they will be driven by the instance of DUT (Device Under Test)
    wire [7:0] y;      // 8-bit output representing the decoded line
    
    // Declare an integer for use in the for-loop
    integer k;
    
    // Instantiate the Device Under Test (DUT) with the named port mapping
    Lab3_1_2 DUT (.G1(g1), .G2A_n(g2a_n), .G2B_n(g2b_n), .x(x), .y(y));   
 
    // Initial block to define the sequence of test cases
    initial begin
      // Test all possible input combinations under different enable configurations
      // Set initial conditions: Decoder disabled due to g2a_n and g2b_n being high
      x = 0; g1 = 0; g2a_n = 1; g2b_n = 1;
      // Iterate through all input combinations
      for (k = 0; k < 8; k = k + 1)
		#5 x = k;  // Change address input every 5 time units
      #10; // Wait for 10 time units before changing enable conditions
      
      // Enable the decoder with G1 high and G2A_n low
      x = 0; g1 = 1; g2a_n = 0; g2b_n = 1;
      for (k = 0; k < 8; k = k + 1)
		#5 x = k;  
      #10; // Wait and switch to another enable condition
      
      // Enable the decoder with G1 low and G2B_n low
      x = 0; g1 = 0; g2a_n = 1; g2b_n = 0;
      for (k = 0; k < 8; k = k + 1)
		#5 x = k;  
      #10;
      
      // Fully enable the decoder with both G1 high and G2B_n low (G2A_n also low for typical 74138 behavior)
      x = 0; g1 = 1; g2a_n = 0; g2b_n = 0;
      for (k = 0; k < 8; k = k + 1)
		#5 x = k;  // Test all combinations when fully enabled
      #10; // Wait before ending simulation
      
      // End the simulation
      $finish;
    end

    // Optional: Monitor changes in variables
    // This helps in debugging and observing the behavior of the decoder under different enabled signals
    initial begin
        $monitor("Time = %t, Enable: G1=%b, G2A_n=%b, G2B_n=%b, Input x = %b, Output y = %b", $time, g1, g2a_n, g2b_n, x, y);
    end

endmodule

We can run Simulation to check the code by clicking the Run Simulation under the SIMULATION and choosing the first Run Behavioral Simulation.

drawing

Multi-output Encoder Circuits

The Encoder circuit converts information from one format (code) to another for standardization, speed, secrecy, security, or saving space by shrinking size. In digital circuits, encoding information may reduce size and/or prioritize functions. Examples of widely used encoder circuits include priority encoders, Huffman encoders, etc.

Part3-2-1

Design an 8-to-3 priority encoder whose truth table is given below. Use behavioral modeling.

drawing

Create and add the Verilog module with v and en_in_n input; y, en_out, and gs output. The v input will be 8-bit data inputs (labeled 0 to 7 in the table), en_in_n input will be one bit (E1), y output will be 3-bit (A2, A1, A0), en_out will be one bit output (GS), and en_out will be one bit output (E0).

lab3_2_1.v

module lab3_2_1(
    input [7:0] v,      // 8-bit data inputs representing the binary code
    input en_in_n,      // Active low enable input indicating whether the encoder should operate
    output reg [2:0] y, // 3-bit output representing the encoded value
    output reg en_out,  // Active low enable output indicating valid output (E0)
    output reg gs       // Group Select, active low when any input is active
);

    // Combine enable signal and inputs into a single 9-bit vector for easier case handling
    wire [8:0] inputs;
    assign inputs = {en_in_n, v}; // Combine enable input with data inputs for case evaluation
    
    // Behavioral block to determine the output based on the inputs
always@(*)
    begin
     case (inputs)
          // Check for encoder being disabled or no input active
            9'b1xxxxxxxx: // If enable is high (active low logic), the encoder is disabled
                        begin
                            y = 3'b111;
                            en_out = 1;
                            gs = 1;
                        end
            9'b011111111:
                        begin
                            y = 3'b111;
                            en_out = 0;
                            gs = 1;
                        end
            9'b0xxxxxxx0:
                        begin
                            y = 3'b000;
                            en_out = 1;
                            gs = 0;
                        end
            9'b0xxxxxx01:
                        begin
                            y = 3'b001;
                            en_out = 1;
                            gs = 0;
                        end    
            9'b0xxxxx011:
                        begin
                            y = 3'b010;
                            en_out = 1;
                            gs = 0;
                        end           
            9'b0xxxx0111:
                        begin
                            y = 3'b011;
                            en_out = 1;
                            gs = 0;
                        end
            9'b0xxx01111:
                        begin
                            y = 3'b111;
                            en_out = 1;
                            gs = 0;
                        end            
            9'b0xx011111:
                        begin
                            y = 3'b100;
                            en_out = 1;
                            gs = 0;
                        end            
            9'b0xx011111:
                        begin
                            y = 3'b101;
                            en_out = 1;
                            gs = 0;
                        end             
            9'b0x0111111:
                        begin
                            y = 3'b110;
                            en_out = 1;
                            gs = 0;
                        end            
            9'b001111111:
                        begin
                            y = 3'b111;
                            en_out = 1;
                            gs = 0;
                        end             
             default:  begin
                            y = 3'b000;
                            en_out = 0;
                            gs = 0;
                        end              
        endcase
    
    end
endmodule

Read-Only Memories

Read-only memories (ROM) consist of interconnected arrays to store an array of binary information. Once the binary information is stored, it can be read at any time but cannot be altered. Large ROMs are typically used to store programs and/or data that will not change by the other circuitry in the system. Small ROMs can be used to implement combinatorial circuits. A ROM uses a decoder, similar to one designed in 1-1 earlier, to address a particular location.

A ROM will have m address input pins and n information output pins to store 2^m words information, each word being n bit in length. The content is accessed by placing an address, and the content of the corresponding word is read at the output pins.

In Verilog HDL, memories can be defined as a two-dimensional array using reg data type, as illustrated below:

reg [3:0] MY_ROM [15:0]; 

where reg is data type, MY_ROM is a 16x4 memory with 16 locations each location being 4-bit wide. If the memory is to be modeled as read-only then two things must happen: (i) memory should only be read and not written into, and (ii) memory should somehow be initialized with the desired content. Verilog HDL provides a system task, called $readmemb, to initialize memory with content. Following is an example of the definition and usage of 4x2 ROM.


module ROM_4x2 (ROM_data, ROM_addr);
 output [1:0] ROM_data;
 input [1:0] ROM_addr;
 reg [1:0] ROM [3:0]; // defining 4x2 ROM
 assign ROM_data = ROM[ROM_addr]; // reading ROM content at the ROM_addr address

 initial $readmemb ("ROM_data.txt", ROM, 0, 3); // load ROM content from ROM_data.txt file
//please correct the absolute path to the ROW_data.txt file, the above is mine and will not work for yours

endmodule 

The ROM_data.txt file, for this example, should be present in the same directory where the model is defined (since no directory path is given), and may have 8 or less lines such as:

 10
 01
 11
 00 

Note that if the number of lines is less than the size of the ROM, the unspecified locations will be initialized with 0s. Also, note that there is another system task available, called $readmembh, which allows the data file to be written using hexadecimal symbols.

For adding text files: Select “Add sources” from the Project Navigator > Add or create Design sources > Add files In the Add source file window, select “File of type” as “All files”.

For the example, we can also run a simulation to verify.

tb.v

// Define the testbench module with no ports as it's a top-level entity
module ROM_4x2_tb;

   //  Declare the ROM address and data lines as wires and regs appropriately
    reg [1:0] ROM_addr;          // Address lines for the ROM as input
    wire [1:0] ROM_data;         // Data lines from the ROM as output

    // Instantiate the Device Under Test (DUT) with the named port mapping
    ROM_4x2 DUT(
        .ROM_data(ROM_data), 
        .ROM_addr(ROM_addr)
    );

    // Initial block to define the sequence of test cases
    initial begin
        // Initialize ROM address
        ROM_addr = 2'd0; 
        #10; // Wait for ten time units
        
        // Apply different addresses and observe the data
        ROM_addr = 2'd1; 
        #10; // Change address every 10 time units to simulate reading different ROM locations
        
        ROM_addr = 2'd2;
        #10;
        
        ROM_addr = 2'd3;
        #10; // Wait after the last address to observe the last set of data
        
        // Add more cases if necessary
        
        // End of simulation
//        $finish;
    end

    // Optional: Monitor changes in variables
    initial begin
        $monitor("Time = %t, Address = %b, Data = %b", $time, ROM_addr, ROM_data);
    end

endmodule

The result is shown below:

drawing

Part3-3-1

Design a 2-bit comparator that compares two 2-bit numbers and asserts outputs indicating whether the decimal equivalent of word A is less than, greater than, or equal to that of word B. You will model ROM and use the $readmemb task.

Create and add the Verilog module with two inputs (a, b) and three outputs (lt, gt, and eq) using ROM and $readmemb system task.

lab3_3_1.v

module lab3_3_1(
    input [1:0] a, // 2-bit input a
    input [1:0] b, // 2-bit input b
    output reg Lt, // a < b
    output reg Eq, // a == b
    output reg Gt  // a > b
);

    reg [2:0] ROM[15:0]; // Define a ROM with enough space
    wire [3:0] addr; // Combined address from A and B

    assign addr = {a, b}; // Combine A and B to form ROM address

   initial begin
        $readmemb("C:/all/Verilog_labs/lab3/project_2/ROM_data.txt", ROM); // Load ROM content from file
// please correct the absolute path to the ROW_data.txt file, the above is mine and will not work for yours
    end

    always @(*) begin
        case (ROM[addr])
            3'b001: begin // a == b
                Lt = 0;
                Eq = 1;
                Gt = 0;
            end
            3'b010: begin // a > b
                Lt = 0;
                Eq = 0;
                Gt = 1;
            end
            3'b100: begin // a < b
                Lt = 1;
                Eq = 0;
                Gt = 0;
            end
            default: begin // Default case for safety
                Lt = 0;
                Eq = 0;
                Gt = 0;
            end
        endcase
    end

endmodule



Create and add a text file that describes the design output.

drawing

a and b are concatenated to form a 4-bit address (addr), which is used to access a specific location in the ROM like below:

ROM_data.txt

001
100
100
100
010
001
100
100
010
010
001
100
010
010
010
001

Create the logic in a document and save it as a .txt file. Then click on Add Sources under the Flow Navigator. Select the Add or create design sources and click next. Click the green plus button and add the file. Add the .txt file and click finish.

We can run Simulation to check the code by clicking the Run Simulation under SIMULATION and choosing the first Run Behavioral Simulation.

ROM_comparator_tb.v

// Testbench for the ROM-based 2-bit comparator
module ROM_comparator_tb;

    // Testbench signals
    reg [1:0] a, b;      // Inputs to the comparator (2-bit each)
    wire Lt, Eq, Gt;     // Outputs from the comparator: Less than, Equal, Greater than

    // Instantiate the Device Under Test (DUT)
    lab3_3_1 DUT(
        .a(a),           // Connect input 'a' to the DUT
        .b(b),           // Connect input 'b' to the DUT
        .Lt(Lt),         // Connect 'Lt' output from the DUT
        .Eq(Eq),         // Connect 'Eq' output from the DUT
        .Gt(Gt)          // Connect 'Gt' output from the DUT
    );

    integer i;  // Loop counter variable

    // Test sequence
    initial begin
        // Display header
        $display("Time: %t | a: %b, b: %b | Lt: %b, Eq: %b, Gt: %b", 
                 $time, a, b, Lt, Eq, Gt);

        // Loop to test all possible combinations of inputs 'a' and 'b'
        for (i = 0; i < 16; i = i + 1) begin 
            #5;               // Small delay to stabilize input changes
            {a, b} = i;       // Assign 'i' value to combined inputs 'a' and 'b'
            #5;               // Wait for the combinational logic to produce output
        end

        // Stop the simulation after all combinations have been tested
        #20 $stop;
    end

The simulation result is shown below:

drawing

If we want to implement it on the board, we need to change the code and add one another IP like below:

drawing

Double-click on and have a setting like the below:

drawing
drawing
drawing
drawing

And then we can find the IP instantiation template below:

drawing

lab3_3_1_board.v

// This module simulates a comparison between two 2-bit numbers using a ROM
module lab3_3_1(
    input clk,           // Clock input for synchronous operations
    input [1:0] a,       // First 2-bit input for comparison
    input [1:0] b,       // Second 2-bit input for comparison
    output reg Lt,       // Output flag, set if 'a' is less than 'b'
    output reg Eq,       // Output flag, set if 'a' equals 'b'
    output reg Gt        // Output flag, set if 'a' is greater than 'b'
);

    // Combined address for ROM: concatenates inputs 'a' and 'b'
    wire [3:0] addr_reg; // Combines 'a' and 'b' to form the ROM address

    // Output from ROM, stores comparison results based on 'a' and 'b'
    wire [2:0] douta;    // Data output from ROM based on address 'addr_reg'

    // Instance of ROM block
    rom_data rom (
      .clka(clk),        // Provides the system clock to the ROM
      .addra(addr_reg),  // Supplies the address to ROM
      .douta(douta)      // Retrieves the comparison result from ROM
    );

    // Assign block to combine 'a' and 'b' into a single address
    // This part was initially inside an always block, but for a combinational approach, we use 'assign'
    assign addr_reg = {a, b}; // Combines 'a' and 'b' to form the address without waiting for the clock edge

    // Combinational logic to set comparison flags based on ROM's output
    // This block decides which of the flags (Lt, Eq, Gt) should be set based on the value of 'douta' from the ROM
    always @(*) begin // Triggered on any change of 'douta'
        case (douta)
            3'b001: begin // ROM output for 'a == b'
                Lt = 0;
                Eq = 1;
                Gt = 0;
            end
            3'b010: begin // ROM output for 'a < b'
                Lt = 1;
                Eq = 0;
                Gt = 0;
            end
            3'b100: begin // ROM output for 'a > b'
                Lt = 0;
                Eq = 0;
                Gt = 1;
            end
            default: begin // Safety default case to handle unexpected ROM outputs
                Lt = 0;
                Eq = 0;
                Gt = 0;
            end
        endcase
    end
    
endmodule

ROM_data.coe

memory_initialization_radix=2;  
memory_initialization_vector=
001,
100,
100,
100,
010,
001,
100,
100,
010,
010,
001,
100,
010,
010,
010,
001;

tb.v


// Testbench for lab3_3_1: Compares two 2-bit numbers and sets output flags
module lab3_3_1_tb;

    // Define testbench-specific signals
    reg clk;             // Clock signal used for synchronizing the DUT
    reg [1:0] a, b;      // 2-bit inputs to be compared by the DUT
    wire Lt, Eq, Gt;     // Outputs from the DUT representing comparison results

    // Instantiate the Device Under Test (DUT)
    lab3_3_1 DUT(
        .clk(clk),       // Connect the testbench clock to the DUT clock
        .a(a),           // Connect 'a' input of the testbench to the DUT
        .b(b),           // Connect 'b' input of the testbench to the DUT
        .Lt(Lt),         // Connect 'Lt' output of the DUT to the testbench
        .Eq(Eq),         // Connect 'Eq' output of the DUT to the testbench
        .Gt(Gt)          // Connect 'Gt' output of the DUT to the testbench
    );

    // Clock generation process
    initial begin
        clk = 1;                     // Initialize the clock signal
        forever #5 clk = ~clk;       // Toggle the clock every 5-time units to generate a periodic signal
    end

    // Test sequence process
    initial begin
        // Delay to ensure the DUT is properly initialized before starting the tests
        #5; // Initial delay to synchronize with clock

        // Loop through all possible combinations of 'a' and 'b'
        for (i = 0; i < 16; i = i + 1) begin
            {a, b} = i;  // Assign new values to 'a' and 'b'
            #10;         // Wait for one clock cycle between assignments to allow DUT to process
        end
    end

    // Monitoring process: Prints the values of inputs and outputs at every simulation step
    initial begin
        $monitor("Time: %t | a: %b, b: %b | Lt: %b, Eq: %b, Gt: %b",
                 $time, a, b, Lt, Eq, Gt); // Print the time and current signal values
    end

endmodule

We can also run the simulation and see the result below:

drawing

Then we can click on the Run synthesis under the SYNTHESIS and Run implementation under the IMPLEMENTATION. We should add the appropriate board-related master XDC file to the project and edit it to include the related pins like the below:

drawing

Generate the bitstream and program device like Lab1.

Then you can press the button on the board, and you can see the LED is on like below:

drawing
drawing
drawing

Part3-3-2

Implement a 2-bit by 2-bit multiplier using a ROM. Output the product in binary on four LEDs.

Create and add the Verilog module with two 2-bit inputs (a, b), and a 4-bit product output using ROM and $readmemb system task.

lab3_3_2.v

module lab3_3_2(
    input [1:0] a,
    input [1:0] b,
    output [3:0] product
);

    // Calculate the address for ROM based on inputs 'a' and 'b'
    wire [3:0] address;
    assign address = {a, b};

    // ROM for storing pre-computed products, 16x4 bits
    reg [3:0] ROM[15:0];

    // Initialize ROM with pre-computed products using $readmemb system task
    initial begin
	$readmemb("C:/all/Verilog_labs/lab3/project_2/ROM_data.txt", ROM); // Load ROM content from file
// please correct the absolute path to the ROW_data.txt file, the above is mine and will not work for yours
   end
   assign product = ROM[address];
    // Retrieve product from ROM based on the current address
  
endmodule

ROM_data.txt

0000 
0000 
0000
0000 
0000 
0001 
0010 
0011
0000 
0010 
0100 
0110 
0000 
0011 
0110 
1001

tb_2.v

// Testbench for 2-bit multiplier using ROM
module tb_2;

    // Define testbench-specific signals
    reg [1:0] a, b;      // Inputs to the multiplier
    wire [3:0] product;  // Output from the multiplier (4-bit product)

    // Instantiate the Device Under Test (DUT)
    lab3_3_2 DUT(
        .a(a),           // Map testbench input 'a' to DUT input 'a'
        .b(b),           // Map testbench input 'b' to DUT input 'b'
        .product(product) // Map DUT output 'product' to testbench wire 'product'
    );

    // Test sequence for checking all possible combinations of inputs
    initial begin
        // Initialize inputs to zero
        a = 2'b00; b = 2'b00;
        #10; // Wait for 10 time units between input changes for the DUT to process

        // Iterate through all possible combinations of 'a' and 'b'
        // The following code blocks manually set every possible combination of inputs
        a = 2'b00; b = 2'b01; #10;
        a = 2'b00; b = 2'b10; #10;
        a = 2'b00; b = 2'b11; #10;

        a = 2'b01; b = 2'b00; #10;
        a = 2'b01; b = 2'b01; #10;
        a = 2'b01; b = 2'b10; #10;
        a = 2'b01; b = 2'b11; #10;

        a = 2'b10; b = 2'b00; #10;
        a = 2'b10; b = 2'b01; #10;
        a = 2'b10; b = 2'b10; #10;
        a = 2'b10; b = 2'b11; #10;

        a = 2'b11; b = 2'b00; #10;
        a = 2'b11; b = 2'b01; #10;
        a = 2'b11; b = 2'b10; #10;
        a = 2'b11; b = 2'b11; #10;

        // Note: The $finish command can be uncommented to end the simulation after the last test case.
        // $finish;
    end

    // Monitoring process for changes in important signals
    // This prints the values of 'a', 'b', and 'product' at every time step where they change
    initial begin
        $monitor("Time = %t: a = %b, b = %b, Product = %b", $time, a, b, product);
    end

    // Additional: Reading and displaying ROM data for debugging purposes
    // This block is not directly related to testing the DUT but can be useful for verifying ROM contents
    reg [3:0] RAM [0:15]; // Temporary storage to display ROM contents
    initial $readmemb("ROM_data.txt", RAM); // Read contents of the ROM data file into RAM
    integer i;
    initial begin
        $display("ROM data:"); // Display header for ROM data section
        for (i = 0; i < 16; i = i + 1) begin
            $display("%d: %h", i, RAM[i]); // Display each line of ROM data with its index
        end
    end

endmodule

We can also run the simulation and see the result below:

drawing

Add the appropriate board-related master XDC file to the project and edit it to include the related pins, assigning a to btn3-btn2, b to btn1-btn0, and product to LED3-LED0, you can refer to the Part-3-3-1

Conclusion

In this lab, you learned how to model multiple output circuits such as decoders, encoders, and ROM. You also learned how to use a system task $readmemb to initialize ROM memory. There are more system tasks that the language supports and you will learn some of them in the next lab.