Author: Craig Maiman, Principal RTL Design Consultant
Welcome to the RTL Design Success series — Part 2 of 9
Once you have settled on a micro-architecture then it is time to write the specification describing it. I have always put a lot of effort into the specification to make sure it is clear, complete, and unambiguous. That effort up-front makes for easier work down the line.
It should be complete enough that any software person that must write code to interface to the hardware could do so without asking the designer any questions. Also, it should be complete enough that if another person were writing the RTL to implement it, they should similarly be able to do it without assistance.
The following are some examples of what should be clearly addressed by the micro-architecture specification:
- Micro-architecture overview
- Top-level functionality in detail
- Sub-block functionality
- Software API (e.g., memory maps, registers, functionality, reset state, etc.)
- Interfaces (signals, protocols, etc.)
- Resets (hard and soft)
- Asynchronous interfaces
- Test modes
- Performance, power, area
- Target device and cost estimates if applicable
- Verification requirements (e.g., functionality to test, including corner cases)
- References & Standards
Using higher-level abstractions in your coding can be pay many dividends in your development cycle including coding efficiency, readability, debug, ease of code changes and maintenance down the road. While it takes time up-front to set up coding abstractions, it more than pays off once that’s done.
The simplest abstraction would be the commonly used enumerated type. While enumerated types is typically used in state machines, that’s certainly not the only place it can be used. It can also be used for any bus you expect to carry control-oriented non-arithmetic information such as a request, acknowledgment, response, status, etc.
An example of a state machine enumerated type would be as follows:
Enumerated types are used by most designers, so we won’t spend any more time on them in this paper.
A more interesting abstraction in SystemVerilog is Structs, which many designers will be familiar with from any C coding they have done.
A simple example of a Struct definition, which you would put a SystemVerilog Package file (see here on how to define a package and import it into logic modules), would be as follows:
This Struct is just a collection of simple logic signals and can be used for communication between logic within a Module or used in interfaces between Modules. Now, instead of having to list all those signals in the interface list of multiple Modules and up and down a hierarchy, you just specify a “signal” of type opb_buf_req_data_type. If you need to make changes to this interface, you only need to modify the Package definition and not all the interface definitions (of course, you may still need to modify the source and destination logic).
The various parts of the declaration are as follows:
Typedef: Defines a type so you can declare variables of that type
Struct: A structure consisting of components of various types.
Packed: The default is “unpacked” which means the simulator can store components of the Struct however it sees fit. To force the Struct to be contiguous bits, you must specify packed. This also allows for the designer to be able to calculate the size of the struct in bits. For example, if you need to size FIFOs or memories that the Struct might be wired to.
The bits will be stored as follows: The first listed component of the Struct will be the high-order bits and the last component will be the low-order bits (though, usually this doesn’t matter to the designer).
An example of a more complex Struct would be as follows:
This Struct example not only has logic signals and enumerated types, but it also has another Struct! It has:
- Valid bit – buf_valid, a single bit logic signal
- Request – buf_req, an enumerated type indicating the type of request (e.g., Read, Add, Remove, etc.)
- Source – buf_src, an enumerated type indicating the source of the request (e.g., Transmit, Receive, etc.)
- Address – buf_addr, a logic type bus
- Data – buf_data, a Struct (shown previously), which contains various information used in the Request.
After I initially defined this Struct and was testing the logic in simulation, I found bugs which required adding various information in the data portion of the Struct. All I had to do was make the changes in the package file and the source and destination logic. No changes where required in any of the interface definitions.
The interface was defined as follows:
So much simpler than if I had had to spell out all the signals embedded in the Structs. Also consider using a “Interface” type if you have multiple interfaces with the same interface.
It is also easy to use the Structs in your logic. Here’s an example of assigning signals to Struct fields (this snippet just shows one case value, not the whole case statement):
This is a complicated example because the source and destination in this case are of different Struct definitions. While they have many similar fields, there are differences, so it required a different Struct type for each. In this case you must do the assigns field-by-field. If the two (opb_bug_wdata and opb_buf_req_fifo_q2) were of the same Struct type, then the assignment could simply be opb_bug_wdata <= opb_buf_req_fifo_q2 and the whole structure is assigned!
Another thing to note about the above case statement is the “unique” case statement. Unique indicates that the values listed below (only one shown here) are mutually exclusive (two or more will never be true at the same time). If you don’t indicate that they are “unique” it will be interpreted as priority, which will result in more complex logic. Just make sure you have all the possible values! If not, it could result in indeterminate outputs (put a default value if necessary to cover all values not specifically listed). See more about this here.
An example showing how to test fields of a Struct:
In this example we’re testing a field of a Struct by checking if it is equal to a value of an enumerated type (which of course matches the field definition type used in the Struct definition).
Here is a 3rd example of using Structs in logic:
In this example we’re testing various fields of a Struct in a case statement to determine the next state to go to in a state machine. It some cases we are testing against an enumerated type value and for other fields just comparing against constants. You can see how readable this becomes when using all the abstractions.
Using Structs like the above examples shows great dividends in simulation too. Many simulation wave viewers will show all the field values if you hover the cursor over a Struct-defined signal. The “signal” will be one line of wave in the view, but hovering will produce a dropdown from the cursor showing all the field values. Much more compact and readable than having all the signals taking up many lines of the wave view.
Here’s an example: