Verilog has significant limitations regarding global declarations, especially functions and user-defined types.

  • Local Scope: In Verilog, all objects declared within a module are local to that module. This means that any function or task must be re-declared in each module where it is needed, leading to redundant code and increased maintenance efforts.
  • Hierarchical References: While Verilog allows hierarchical references to access module objects from other modules, these references are only for verification purposes and do not represent actual hardware behavior. Consequently, they are not synthesizable.
  • User-Defined Types: There is often a need to use user-defined types in multiple modules. However, without a mechanism for global declarations, these types must also be redefined in each module, further contributing to code duplication.

SystemVerilog Package

A SystemVerilog package offers a way to store and share data, methods, properties, and parameters that can be reused across multiple modules, interfaces, or programs. Packages have explicitly defined scopes that exist at the same level as top-level modules, allowing all parameters and enumerations to be referenced within this scope.


package <package_name>;
  // Typedef declarations
  // Function/Task definitions
  // ...
endpackage

A package is a separate namespace and is not embedded inside a Verilog module. By placing definitions and declarations between package and endpackage, you can prevent clutter in the global namespace.

Note ! Package cannot contain hierarchical references except those created within the package or imported.

Package Example


package my_pkg;

    // Create typedef declarations that can be reused in multiple modules
	typedef enum bit [1:0] { RED, YELLOW, GREEN, RSVD } e_signal;

	typedef struct { bit [3:0]   signal_id;
                     bit         active;
                     bit [1:0]   timeout;
                   } e_sig_param;

    // Create function and task defintions that can be reused
    // Note that it will be a 'static' method if the keyword 'automatic' is not used
	function int calc_parity ();
      $display ("Called from somewhere");
   	endfunction
    
endpackage

Package Import

A SystemVerilog package can be imported into the current scope using the keyword import followed by the scope resolution operator ::, enabling the use of their items.


import <package_name>::*; // Imports all items
import <package_name>::<item_name>; // Imports specific item

Wildcard Import

When package items are imported using a wildcard, only the items that are actually utilized in the module or interface are imported. Any definitions and declarations in the package that are not referenced remain unimported. So, an import essentially means that it is made visible.


// Import the package defined above to use e_signal
import my_pkg::*;

class myClass;
	e_signal 	m_signal;
endclass

module tb;
	myClass cls;

	initial begin
		cls          = new();
		cls.m_signal = GREEN;
		$display ("m_signal = %s", cls.m_signal.name());
		calc_parity();
	end
endmodule
 Simulation Log
ncsim> run
m_signal = GREEN
Called from somewhere
ncsim: *W,RNQUIE: Simulation is complete.

Note that the package had to be imported for the compiler to recognize where GREEN is defined. Had the package not been imported, then you'll get compiler errors like shown below.

 Simulation Log
	e_signal 	m_signal;
	       |
ncvlog: *E,NOIPRT (testbench.sv,15|8): Unrecognized declaration 'e_signal' could be an unsupported keyword, a spelling mistake or missing instance port list '()' [SystemVerilog].
		cls.m_signal = GREEN;
		                 |
ncvlog: *E,UNDIDN (testbench.sv,23|19): 'GREEN': undeclared identifier [12.5(IEEE)].

Import Specific Items

Instead of importing all the definitions inside a package, you can also import them individually if you know exactly what is used in your piece of code. But, it is seen as an overhead especially when more members are accessed from the package.


import my_pkg::GREEN;
import my_pkg::e_signal;
import my_pkg::common;

Omission of either one of the three import statements will result in a compiler error simply because it does not know where they are defined unless its imported.


// Although e_signal is made visible, enumerated labels will not be made visible
import my_pkg::e_signal;

// Import each enumerated label instead, if you are not importing using wildcard ::*
import my_pkg::RED;
import my_pkg::GREEN;
import my_pkg::YELLOW;
// etc

Note that when importing specific items, all enumerated labels will have to be individually imported.

Search Order

Local definitions and declarations within a module or interface have priority over any wildcard import. Similarly, an import that explicitly names specific items from a package will also take precedence over a wildcard import. A wildcard import essentially extends the search rules to include the package when resolving an identifier. Software tools will first look for local declarations (following Verilog's search rules within a module), then in any packages imported with a wildcard, and finally in SystemVerilog’s $unit declaration space.

Consider the example below where the same definitions exist, one at the top level and the other via an imported package.


package my_pkg;
	typedef enum bit { READ, WRITE } e_rd_wr;
endpackage	

import my_pkg::*;

module tb;
  typedef enum bit { WRITE, READ } e_wr_rd;

	initial begin
        e_wr_rd  	opc1 = READ;
        e_rd_wr  	opc2 = READ;
      $display ("READ1 = %0d READ2 = %0d ", opc1, opc2);
	end
endmodule

Note that even though my_pkg was imported, e_rd_wr variable opc2 got the value of READ as 1, which implies that the enum value inside the package is not considered.

 Simulation Log
ncsim> run
READ1 = 1 READ2 = 1 
ncsim: *W,RNQUIE: Simulation is complete.

In order for the simulator to apply value from the package, the package name should be explicitly mentioned using the :: operator as shown below.


module tb;	
	initial begin
        e_wr_rd  	opc1 = READ;
        e_rd_wr  	opc2 = my_pkg::READ;
      $display ("READ1 = %0d READ2 = %0d ", opc1, opc2);
	end
endmodule
 Simulation Log
ncsim> run
READ1 = 1 READ2 = 0 
ncsim: *W,RNQUIE: Simulation is complete.

Nested Package Reference

Packages can also be imported into another package.


// Define a new package called X
package X;
  byte    lb     = 8'h10;
  int     word   = 32'hcafe_face;
  string  name = "X";

  function void display();
    $display("pkg=%s lb=0x%0h word=0x%0h", name, lb, word);
  endfunction 
endpackage

// Define a new package called Y, use variable value inside X within Y
package Y;
  import X::*;

  byte    lb   = 8'h10 + X::lb;
  string  name = "Y";

  function void display();
    $display("pkg=%s lb=0x%0h word=0x%0h", name, lb, word);
  endfunction 
endpackage

// Define a new package called Z, use variable value inside Y within Z
package Z;
  import Y::*;  

  byte    lb   = 8'h10 + Y::lb;
  string  name = "Z";
  
  function void display();
    // Note that 'word' exists in package X and not in Y, but X
    // is not directly imported here in Z, so the statement below 
    // will result in a compilation error
    //$display("pkg=%s lb=0x%0h word=0x%0h", name, lb, word);  // ERROR !
    
    $display("pkg=%s lb=0x%0h", name, lb);
  endfunction 
endpackage

module tb;
  // import only Z package
  import Z::*;
  
  initial begin
    X::display();   // Direct reference X package
    Y::display();   // Direct reference Y package
    display();      // Taken from Z package because of import
  end 
endmodule

Note in the log below that each package got compiled into its own namespace, and variables in local scope has precedence over imported scope. For example, display function in package Y used variables in Y instead of X .

 Simulation Log
	Top level design units:
		X
		Y
		Z
		tb
Loading snapshot worklib.tb:sv .................... Done
...
xcelium> run
pkg=X lb=0x10 word=0xcafeface
pkg=Y lb=0x20 word=0xcafeface
pkg=Z lb=0x30
xmsim: *W,RNQUIE: Simulation is complete.

Design Notes

The synthesizable constructs that a package can include are:

  • Definitions of parameters, local parameters and constant variables
  • User-defined types using typedef
  • Fully automatic definitions of tasks and functions
  • Import statements for other packages

Let's define a package to store enumerated values that represent functions of an ALU block.


// Define the package for ALU instructions
package alu_pkg;

typedef enum logic [2:0] {
    ADD = 3'b000,
    SUB = 3'b001,
    AND = 3'b010,
    OR  = 3'b011,
    XOR = 3'b100
} alu_op_t;

endpackage

Now, a wildcard import is done to access all declarations inside the package, within the module.


// Import everything inside the package containing ALU operations
import alu_pkg::*;

module alu (
    input logic [2:0]   opcode,  // ALU operation code
    input logic [31:0]  a,       // First operand
    input logic [31:0]  b,       // Second operand
    output logic [31:0] result   // Result of the operation
);

always_comb begin
    // Enumerations defined in 'alu_pkg' can be directly used here
    // because they have been imported
    case (opcode)
        ADD: result = a + b;   // Perform addition
        SUB: result = a - b;   // Perform subtraction
        AND: result = a & b;   // Perform bitwise AND
        OR:  result = a | b;   // Perform bitwise OR
        XOR: result = a ^ b;   // Perform bitwise XOR
        default: result = 32'b0; // Default case (optional)
    endcase
end
endmodule

In the following example, an import is not specifically called but a direct reference to declaration inside the package is made using the scope resolution operator ::.


// Note that package import is not done here

module alu (
    input logic [2:0]   opcode,  // ALU operation code
    input logic [31:0]  a,       // First operand
    input logic [31:0]  b,       // Second operand
    output logic [31:0] result   // Result of the operation
);

always_comb begin
    // Enumerations defined in 'alu_pkg' is directly referenced using ::
    case (opcode)
        alu_pkg::ADD  : result = a + b;   // Perform addition
        alu_pkg::SUB  : result = a - b;   // Perform subtraction
        alu_pkg::AND  : result = a & b;   // Perform bitwise AND
        alu_pkg::OR   : result = a | b;   // Perform bitwise OR
        alu_pkg::XOR  : result = a ^ b;   // Perform bitwise XOR
        default       : result = 32'b0;   // Default case (optional)
    endcase
end

When a module calls a task or function defined in a package, synthesis will replicate the functionality as if the task or function were defined directly within the module. For tasks and functions in a package to be synthesizable, they must be declared as automatic and cannot include static variables.

Synthesis does not support variable declarations within packages. In simulation, a variable in a package is shared across all modules that import it, allowing one module to write to the variable and another module to access the updated value. This form of inter-module communication, which bypasses module ports, is not synthesizable.