In many cases, it may be necessary to instantiate objects which are essentially the same, but may differ in some detail, for instance the address and data widths may differ between otherwise identical types of transactions. This information needs to be provided when the code is compiled and so cannot be set when the object is constructed. Another situation might be where channels between 2 components need to be modelled to carry different transactions, but otherwise have identical operations. In these situations, it is very easy to end up with needlessly duplicated code that increases the maintenance burden, or with extra code to help handle the problem (up and down casting classes, for instance).
Classes can be declared with parameters to specify either a value (such as a data width) or a type (such as the FIFO element type). Using parameters, the code for the class is written in a more generic style and it is important to remember that the type of the class is only established by a combination of the class name and a set of actual parameters.
As an example, consider the following declaration for a message:
class Message #(int ADDR_W = 16, DATA_W = 8); logic [DATA_W-1:0] data; logic [ADDR_W-1:0] address; ... endclass: Message Message#(32, 32) m1; // or: #(.ADDR_W(32), .DATA_W(32)) Message#(16, 16) m2;
The generic declaration of Message is not itself a datatype - the declaration, Message #(32, 32), though, is and is referred to as a specialization of the class.
The following code would then be illegal because m1 and m2 are different types - the combination of the unadorned name and the actual parameters is different for m1 and m2.
initial begin m1 = new(); m2 = t1; // illegal ... end
Creating generic classes through parameterization becomes even more useful when using a type parameter:
class Transaction; ... endclass: Transaction class Channel #(type Tr = Transaction); ... task put(input Tr trans); ... task get(output Tr trans); ... endclass
Here it can be seen how the channel code can be reused with different types of transaction. Note that the actual transaction type must be a descendent class of Transaction - an alternative is to declare the generic Transaction with a default type of, say, bit.
class Channel #(type Tr = bit); ... endclass typedef Channel #(APB_trans) APB_chan;
The use of typedef in the snippet, above, creates an equivalent declaration and avoids the need to repeat the complete specialization wherever it may be used.
Parameterized classes can extend unadorned classes as well as be extended by them and with interesting results. With the parameterized declaration for Channel above where the default type for Tr is bit, we can do the following:
|class Chan1 #(type P=real) extends Channel;||The default type for Tr is still bit and declares a type parameter for Chan1 with default type of real|
|class Chan2 #(type P=real) extends Channel#(integer);||Similar to the previous example, but the default type for Tr is now integer|
|class Chan3 #(type P=real) extends Channel#(P);||The default type for Chan3 is still real (P) and the Tr type parameter is the same (real)|
|class Chan4 #(type P=Channel#(Transaction) extends P;||The default type for Tr is Transaction|
The last form in the table above has been used in SystemVerilog (and particularly in UVM) to add functionality dynamically without distorting or having to modify the class hierarchy. For instance, an established base class (such as the Channel class above) may need to be extended with certain functionality, such as implementing checks and reports after the simulation is completed. Several approaches could be taken, but it is desirable to minimise the resulting code maintenance burden. Take a look at the mix-in class tutorial for some ideas about using this technique.