In SystemVerilog, macros provide a way to define reusable piece of code that can be inserted anywhere in your source code. SystemVerilog macros are very similar to c language macros. Macros essentially substitutes a name with a piece of code.
From my early days with SystemVerilog to more than a decade of building test benches with it, I have a special respect towards macros. This simple looking construct is extremely useful when needed and can also cause disastrous consequences if mishandled.
In this article, I will explore the world of SystemVerilog macros, their benefits, pitfalls and best practices.
Structure of a Macro
A macro in SystemVerilog is defined with `define keyword. An example macro looks like this:
`define NUM_OF_DRIVERS 10
int num_of_drivers = `NUM_OF_DRIVERS;
The first line defines a macro called NUM_OF_DRIVERS and it has to be substituted with 10 when used anywhere in source code. The second line shows how to use a macro.
There are several variations on how you can define and use the macros which will be discussed a later in this article. For now, let us understand basics of macros by using this simple example.
When you compile your source code, a program called pre-processor is started first. Pre-processor will parse the source code and extracts all the macro definitions and when any of these definitions are found in your source code with a backtick then it will simply substitutes the define with the text that you declared during the definition.
In this example when pre-processor sees the first line, it will extract the definition of NUM_OF_DRIVERS and when it sees the second line, it will simply replace the NUM_OF_DRIVERS with 10.
Once pre-processor is done with all its substitutions, the compiler is invoked. Notice that the compiler is seeing a different code (after substitution) than what you wrote. It sees the code as below:
int num_of_drivers = 10;
This simple substitution may not look like much but it will be very useful. The usefulness can only be judged when we understand all the forms in which macros can be defines.
Parameterised Macros
Macros can take parameters. Parameters are useful in customising the macro when using it. It is very similar to a function. An example is shown below.
`define ADD(x,y) x + y
int a = `ADD(3,4); // 3 + 4 is substituted here
A simple macro to add two numbers called ADD. When you are using this macro you can pass parameters. Note that pre-processor still substitutes what ever you write in the definition but this time you can customize each substitution based on the parameters passed at the time of using the macro.
Default values to parameters cab also be defined. When arguments are missing the macro will take the default value for the parameter of the macro. The same example is rewritten with default parameter values.
`define ADD(x, y = 6) x + y
int a = `ADD(3); // 3 + 6 is substituted here
In this example code, the parameter x is mandatory parameter that must be passed but y is optional as it is defined with default argument value of 6.
Multiline Macros
You can also specify a bunch of code under a macro. This can be done by using multi-line macros. An example will give you a better picture.
`define DISPLAY_PARAMETERS \
initial begin \
$display("Parameters are as follows.."); \
end
module top();
`DISPLAY_PARAMETERS
endmodule : top
In this example, we defined a multi-line macro called DISPLAY_PARAMTERS which can substitute an entire initial block with its contents.
Notice the back slash at the end of each line in the macro. It is this back slash that indicates that the macro is spanning multiple lines. You can have as many lines as you want so long as they end with back slash except for the last line.
Macros for conditional compilation
Sometimes you might need to include a piece of source code or exclude a piece of source code conditionally. This can be achieved by defining a macro without any substitution text and using it using conditional compilation directives. Again, an example will be better to discuss further.
`define DEBUG
module test();
`ifdef DEBUG
initial $display("Debug mode is enabled");
`else
initial $display("Debug mode is disabled");
`endif
endmodule : test
Observe that the macro DEBUG does not have any substitution text. This means you are just defining a macro called DEBUG and later you can use a conditional compilation directive ifdef to check if the DEBUG is defined or not.
Compiler will see an initial block based on whether you define DEBUG or not. This is called conditional compilation. There is another directives to check if the macro is not defined ifndef (if not defined).
Predefined Macros
SystemVerilog language comes with some predefined macros. Few examples are `__FILE__ and `__LINE__. You might be familiar with these macros if you have experience with Universal Verification Methodology (UVM).
Report functions of UVM uses these predefined macros to denote current file and line number. Now that you know different forms of the macros. In the next section, we will talk about different use cases and potential benefits of the macros.
Benefits and Potential Use Cases of Macros
1. Declaring global constants
Simplest use case that we can think of is to declare constants with macros. Of course, there are many ways you can declare constants in SystemVerilog. There are keywords like parameter, const, local param, etc to help is declare constants but macros provide one advantage.
Macros are global to project that means they don’t have a scope. You can declare all your constants at one common place with macros and all the files in the project can reference it as macros are simply a substitution mechanism.
It is easy to change any constant in a common place to reflect in entire project. Macros are also type unaware which means that you simply provide a text for substitution regardless of type of the data.
This means you can declare constants consistently in same format regardless of the data type you are dealing with. Both lack of scope and lack of type awareness can be disadvantage also depending on how you use the macro. We will discuss this later when talking about the ‘Challenges’
2. Guard macros
A common use case that I see everywhere is to use macros to guard against recompilation of file by mistake. A combination of conditional compilation directive and macro can give us guarantee to compile a file only once.
Typical guard macros looks like this.
`ifndef FILE_NAME
`define FILE_NAME
// content of the file goes here
`endif
When the EDA tool parses the file the macro FILE_NAME is not defined so it enters the branch and defines the macro first and then continues to compile the file.
Imagine, if the file is encountered again by the compiler by mistake then the macro is already defined so it skips reading the file until endif
3. Conditional compilation
Conditional compilation is unique use case for macros. If you want to change the structure of the code with a qualifier then conditional compilation is a best choice.
For example, you developed a test bench at block level and same test bench can be used in chip level with few changes in the structure of the test bench like connections, modes, components etc.
You can have a configuration field to choose the test bench to rearrange to chip or block level. What if the structural changes involve connection to DUT? or some parts of the DUT should not be instantiated in block or in chip.
In this case conditional compilation can be very useful. A hypothetical example is shown below.
DUT dut(
`ifdef BLOCK_BENCH
.axi_intf(axi_intf)
`else
.axi_intf(chip.axi_intf)
`endif
)
We are deciding which interface to be connected to the DUT based on a macro definition. The ability to pass a macro definition from the command line is available in all EDA vendors.
So, we can choose to run a block level bench or a chip level bench with a simple command line define is a huge advantage.
4. Reusability
There are several benefits of using macros. One well known advantage is reusability. Every one knows that the reusability is a key attribute and there is no need for convincing about it.
In general the reusability is achieved by functions, tasks, classes, modules etc. in [[SystemVerilog]]. If the code is repetitive and simple enough then macros are excellent choice.
However, there is a difference between the function and macro when it comes to reuse. If you declare and use a function then the functionality is reused several times by calling the function where as if you define a macro and call a macro then you are literally pasting the code again and again at many places.
Macros should not be used for functional reuse or in other words your macros are never an alternative to functions, tasks, classes or modules.
Macros are general meant to provide a piece of code template that you can customize and use in many places. Typically these pieces of code are often boiler plate code that you need to keep repeated at many places.
Couple of exampled might help you understand.
`define uvm_object_constructor(object_name) \
function new(string name = object_name); \
super.new(name) \
endfunction : new
class my_object extends uvm_object;
`uvm_object_utils(my_object)
`uvm_object_constructor(my_object)
endclass : my_object
In the above example, uvm_object_constructor macro is created to place and empty UVM object constructor.
When you are declaring your custom UVM objects and want to place an empty constructor then no need to repeat the same code again and again. Just use the macro.
There are plenty of such macro examples are available in UVM. Another example is to a have access methods for properties. See the example below.
`define protected_member(data_type, property_name) \
protected data_type property_name; \
virtual function void set_``property_name``(``data_type`` property_name``) \
endfunction
class my_object extends uvm_object;
`protected_member(int, data_length)
endclass : my_object
The protected_member macro will declare a protected member of class with given data type and name and then creates a set method to reach the protected method.
Only set is shown here but you should easily extend it to get as well. When you are declaring members of the class if any of them are protected and need access methods then simply use the macro.
Challenges with Macros
1. Lack of Type
Macros do not perform type checking. This can lead to unexpected behavior or difficult-to-diagnose bugs because the compiler does not check the types of the arguments passed to the macro.
There is no way to enforce type on a macro. For example, you created a macro that expected an integer parameter but if the user gives a string parameter, there is nothing you can do except debugging a weird error for couple of days.
A macro once defined can take any type value so always keep in mind that the macros are substitutions and nothing else.
2. Substitution issues
Macros perform simple text substitution, which can lead to errors if not used carefully. For example, improper use of parentheses can lead to incorrect evaluation of expressions.
This is very common pitfall that every one goes through their career. Following is an example to show how important the parenthesis are, when macros are used.
`define SQUARE(x) x * x
int result = `SQUARE(1 + 2); // Expands to 1 + 2 * 1 + 2, which is 5, not 9.
If you don’t have parenthesis and your macro is dealing with numeric values. It can cause issues with precedence of evaluation.
In the above example, when you pass the parameter to macro as 1+2 you might expect the answer to be 9 but the actual substitution might look different and due to the precedence rules the answer might not align with your expectation.
If you use parenthesis then the answer would be 9.
3. Lack of scope
Macros do not have scope, meaning they can unintentionally affect global variables and cause naming conflicts. This can make the code harder to understand and maintain.
4. Debugging difficulties
Debugging macro-related issues can be challenging because the preprocessor replaces macros before the actual compilation.
Errors in macros can result in cryptic error messages that point to the expanded code rather than the macro definition.
`define INCREMENT(x) x = x + 1
int a = 5; `INCREMENT(a)
// Expands to a = a + 1, which is fine.
int b = 5; `INCREMENT(b + 1)
// Expands to b + 1 = b + 1 + 1, which is invalid syntax.
5. Maintainability
Macros can make the code more complex and harder to maintain. Changes to macro definitions need to be made carefully, as they affect all instances where the macro is used.
Macros can lead to unintended consequences if they inadvertently modify global state or interact with other macros in unexpected ways.
Best Practices
1. Don’t abuse the Macros
Think twice before deciding to use macros. Some substitutions are trivial and are ok to use but when you are writing multiline macros with parameters then stop and evaluate if it is really needed.
2. Use parenthesis
Always use parentheses around macro parameters and the entire macro body to ensure correct evaluation order.
3. Centralised Definition
Define all macros at centralised file or location for the entire project. Declaring macros across files in your project will give you tough time debugging.
4. Clear and meaningful naming
Use clear and distinct names for macros to avoid naming conflicts and improve readability.
5. Documentation
Document macros clearly to explain their purpose and usage, making it easier for others (and your future self) to understand the code.
Closing Thoughts
Macros in SystemVerilog are simple in appearance but extremely powerful when used correctly. They can make your testbench cleaner, more configurable, and more reusable, especially when dealing with repetitive boilerplate code or structural differences between block-level and chip-level environments. At the same time, their lack of type checking, global scope, and blind substitution can easily lead to bugs that are very hard to track down.
Understanding how the preprocessor works and following a few best practices such as using parentheses, meaningful naming, and limiting macro usage to the right scenarios can help you get the best out of this feature without falling into the common traps.
Like many tools in verification, macros shine when used with intention. With a clear understanding of their behavior and limitations, they can become a valuable part of your SystemVerilog toolbox.
Nice ,One request Can you please post for the coverage same and with AI intergaration on the tool