Skip to content

alu_division

William Moore edited this page Apr 5, 2024 · 15 revisions

ALU Division

Introduction

ALU Division is a small example of a complete Bathtub environment. This example is included in the DVCon U.S. 2024 paper, “Gherkin Implementation in SystemVerilog Brings Agile Behavior-Driven Development to UVM.” The paper contains:

  • The ALU Division Gherkin feature file
  • Two step definitions
  • The Bathtub UVM test

This sample environment contains just enough testbench to validate that the code samples in the paper are functional. Still, this example may be instructive.

EDA Playground

This complete example is available as an EDA Playground at bathtub_test_paper. It is open freely for viewing, but registration is required to run it.

Most the files from EDA Playground have been downloaded and committed to the GitHub repository here: https://github.com/williaml33moore/bathtub/tree/main/examples/alu_division. EDA Playground downloads the entire playground--input and output files--in a single directory called results. The entire results directory (minus a few unnecessary files) is committed in the repository for simplicity. We do not recommend storing everything in a single directory like this on an actual ASIC project.

Gherkin Feature File

The Bathtub flow begins with a Gherkin feature file. Presumably teammates gather to discuss the behavior of a new feature, and formulate their findings in a plain text file like this. The ALU Division feature file is alu_division.feature:

# This Gherkin feature file's name is alu_division.feature

Feature: Arithmetic Logic Unit division operations

    The arithmetic logic unit performs integer division.

    Scenario: In integer division, the remainder is discarded
        Given operand A is 15 and operand B is 4
        When the ALU performs the division operation
        Then the result should be 3
        And the DIV_BY_ZERO flag should be clear

    Scenario: Attempting to divide by zero results in an error
        Given operand A is 10 and operand B is 0
        When the ALU performs the division operation
        Then the DIV_BY_ZERO flag should be raised

It's a functional, but very small sample that illustrates only the basics of the Gherkin language.

Comments begin with #. This comment helpfully demonstrates the convention that feature filenames end with the suffix “.feature.”

The Feature: line provides a title for the high-level feature. A feature file can have only one Feature: keyword.

The block following Feature: is free-form text for documentation purposes. It can span multiple lines.

A Scenario: is a concrete example of a discrete behavior, comprised of steps. The text is a description for documentation purposes.

The Given step is declarative and sets up this division’s operands.

The When step indicates the procedural operation being tested.

The Then step asserts the expected outcome. It is traditional to use the auxiliary verb “should” in Gherkin Then steps (“should be clear”) but it carries the same meaning as stronger verbs like “must” or “shall” in that it is considered an error if the assertion fails.

And is an alias for the preceding keyword that makes the scenario more readable than repeating the keyword Then. Gherkin has additional syntactic sugar step keywords But and *; the asterisk allows the user to create bullet lists of steps. Scenarios may have any number of Given, When, Then, And, But, and * steps in any combination.

The second scenario is an error case. Note that some lines are common between the two scenarios, differing only in literal values, illustrating that steps can be parameterized and reused across behaviors.

Leading and trailing whitespace is insignificant, but the prevailing convention is to indent keyword lines consistently.

Gherkin syntax consists solely of the keywords at the beginning of each line. The text following each keyword is arbitrary, not subject to any syntactic or grammatical requirements.

RTL Design

The feature file describes what our RTL should do. The RTL file is design.sv. This is very simple Verilog code for a combinational arithmetic logic unit that can add, subtract, multiply, and divide two 16-bit integers. It's not intended to be synthesizable; it only needs to satisfy our sample feature file. The file contains a Verilog interface and a module.

Interface

Here is the interface:

interface alu_if ();

  logic[15:0] operand_a;
  logic[15:0] operand_b;
  logic[3:0] operation;
  logic[31:0] result;
  logic[31:0] status;

    modport dut (
    input operand_a,
    input operand_b,
    input operation,
    output result,
    output status
    );

    modport driver (
    output operand_a,
    output operand_b,
    output operation,
    input result,
    input status
    );

    modport monitor (
    input operand_a,
    input operand_b,
    input operation,
    input result,
    input status
    );
endinterface : alu_if

This interface has modports for the DUT, the testbench driver, and a testbench monitor. This example doesn't have a monitor, but the modport is included for symmetry.

The DUT's interface is:

logic[15:0] operand_a:
Input operand A

logic[15:0] operand_b:
Input operand B

logic[3:0] operation:
Input opcode (0 = add, 1 = subtract, 2 = multiply, 3 = divide)

logic[31:0] result:
Output result

logic[31:0] status:
Outpus status flags (bit[0] = OKAY, bit[1] = DIV_BY_ZERO error, bit[2] = invalid opcode error)

Module

Here is the module:

module alu (alu_if i);

  localparam logic[3:0]
  OP_ADD = 0,
  OP_SUBTRACT = 1,
  OP_MULTIPLY = 2,
  OP_DIVIDE = 3;
  
  localparam int unsigned
  	STATUS_OKAY = 0,
  	STATUS_DIV_BY_ZERO = 1,
    STATUS_OP_ERR = 2;
  
  always_comb begin
    i.dut.result = 0;
    i.dut.status = 0;
    case (i.dut.operation)
      OP_ADD : begin
        i.dut.result = i.dut.operand_a + i.dut.operand_b;
   		i.dut.status[STATUS_OKAY] = 1;
      end
      
      OP_SUBTRACT : begin
        i.dut.result = i.dut.operand_a - i.dut.operand_b;
   		i.dut.status[STATUS_OKAY] = 1;
      end
      
      OP_MULTIPLY : begin
        i.dut.result = i.dut.operand_a * i.dut.operand_b;
   		i.dut.status[STATUS_OKAY] = 1;
      end
      
      OP_DIVIDE : begin
        if (i.dut.operand_b != 0) begin
          i.dut.result = i.dut.operand_a / i.dut.operand_b;
   		  i.dut.status[STATUS_OKAY] = 1;
        end
        else begin
          i.dut.result = 0;
          i.dut.status[STATUS_DIV_BY_ZERO] = 1;
        end
      end
      
      default : begin

        i.dut.result = -1;
        i.dut.status[STATUS_OP_ERR] = 1;
      end
        
    endcase
  end
endmodule : alu

It's simple combinational logic that continuously outputs an arithmetic result and status based on the operand and opcode inputs.

Testbench

Now that we have a DUT and its interface, we can build a testbench.

The testbench is in testbench.sv. This file consists of:

  • A testbench top module
  • A UVM environment class
  • A UVM virtual sequencer class
  • A UVM virtual sequence base class, compatible with the virtual sequencer
  • A UVM test

Following the familiar refrain, everything is as minimal as possible with the sole goal of validating the examples in the paper. This is not representative of best practices for a working verification environment.

Testbench Top Module

module top();

  import uvm_pkg::*;
  
  typedef enum {ADD=0, SUBTRACT=1, MULTIPLY=2, DIVIDE=3} op_type;
  typedef enum {OKAY=0, DIV_BY_ZERO=1, OP_ERR=2} status_type;

  alu_if vif();
  alu dut(vif);

  initial begin
    $timeformat(0, 3, "s", 20);
    uvm_config_db#(virtual alu_if)::set(uvm_coreservice_t::get().get_root(), "top", "vif", vif);
    run_test();
  end

...

`include "alu_step_definition.svh"

endmodule : top

The top module of the simulation instantiates the DUT module and its interface, and passes the interface to the module.

The initial block stores the interface virtually in the UVM configuration database using uvm_root as context. Then it calls run_test().

The file which declares our step definitions, alu_step_definition.svh, is included near the bottom of the module. Adding these step definitions is the only change to the top module required for Bathtub.

UVM Environment Class

Module top defines a UVM environment class for the ALU, extending base class uvm_env:

  class alu_env extends uvm_env;
    `uvm_component_utils(alu_env)

    alu_sequencer alu_vseqr;
    virtual alu_if vif;

    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction : new

    virtual function void build_phase(uvm_phase phase);
      alu_vseqr = alu_sequencer::type_id::create("alu_vseqr", this);
    endfunction : build_phase

    virtual function void connect_phase(uvm_phase phase);
      bit ok;

      ok = uvm_config_db#(virtual alu_if)::get(uvm_coreservice_t::get().get_root(), "top", "vif", vif);
      assert (ok);
      assert_vif_not_null : assert (vif != null);
      alu_vseqr.vif = vif;
    endfunction : connect_phase

  endclass : alu_env

The build phase instantiates our virtual sequencer, alu_vseqr, described in the following section.

The connect phase retrieves the virtual interface from the UVM configuration database and passes it to the virtual sequencer.

The environment has no dependencies on Bathtub. This environment could be used unchanged from a "normal" test bench without Bathtub.