OSVVM: Verification Data Structures & Singletons
In reply to a LinkedIn post that regurgitated the old statement that VHDL is verbose, I replied, “With the VHDL-2008 update, Verilog is more verbose than VHDL.”
This led my old friend Cliff Cummings and I to trade a few VHDL vs. System Verilog jabs. One of the things he brought up as concise SystemVerilog verification capabilities are singletons and verification data structures. Rather than give a short answer of how to do this in VHDL+OSVVM, I thought I would give an article length answer with examples would be better.
OSVVM Verification Data Structures
With respect to verification data structures, OSVVM has implemented data structures for FIFOs and Scoreboards (ScoreboardGenericPkg), Memory Models (MemoryPkg), Coverage Modeling (CoveragePkg), and Error Tracking and Message Filtering (AlertLogPkg).
All of OSVVM’s data structures use VHDL’s protected types to allow us to implement the data structure inside the package and allow access to the object to have a larger scope than a single process. With a shared variable an object can reach beyond an architecture – either with external names or by locating the declaration of the shared variable in a package.
Singletons in VHDL
A singleton is a programming pattern where a shared variable is hidden in a package body and can only be accessed via a package defined API. A singleton hides the protected type declaration by also putting it in the package body so as to ensure that there is only one instance (the shared variable) of this protected type.
OSVVM’s Error Tracking and Messaging filtering capability in AlertLogPkg uses a singleton. The protected type declaration, protected type body, and shared variable declaration body are all hidden away inside the package body. Only the user API can interact with the shared variable and protected type.
What is unique about OSVVM’s Error Tracking and Message filtering capability is that we needed the ability to either have a simple, single error tracking bin and message filtering control – or – have a separate error tracking bin and message filtering control per verification component. The separate error tracking and message filtering can be further broken down so that a verification component has individual error counting and message control for each class of error or message – such as one for each separate protocol and data checker, or one for the read channel and one for the write channel.
Inside AlertLogPkg’s protected type is a resizable array of error tracking and message filtering bins. A constructor, hidden in the protected type, allocates these bins and returns a descriptor with which to access them. This constructor is accessed using the method GetAlertLogID. The benefit of all this is that while the internals of the package may be complicated, the user API is simple.
If we are using separate error tracking bins and message filtering controls, we start by declaring an object of the ID type (AlertLogIDType). It may be a signal or variable. Later we will also discuss the possibility of using a constant.
signal ModelID : AlertLogIDType ;
Then in our code we call GetAlertLogID to construct (add and initialize) an additional AlertLog bin (one set of error tracking bins and message filtering controls).
ModelID <= GetAlertLogID("Model_1") ;
An error is signaled with an Alert (similar to VHDL’s Assert). To associate it with a particular bin, we use the AlertLogID in the call. Error indicates it is an ERROR (and is the default if not specified). Just like VHDL’s assert, we also support FAILURE and WARNING.
AlertIf(ModelID, A > 5, "A > 5", Error) ;
When the condition is true, the following message will be printed and the ErrorCount field of the bin associated with ModelID is incremented.
%% Alert ERROR In Model_1, A > 5
For simple testbenches were we do not need separate bins, we simply call AlertIf without ModelID as shown below. Hence in simple cases we skip the signal declaration and the call to GetAlertLogID, and just use the default bin.
AlertIf(A > 5, "A > 5", Error) ;
Its corresponding output looses the context of Model_1.
%% Alert ERROR A > 5
OSVVM’s AlertLogPkg also has Log for message filtering and AffirmIf for data checking (which can generates Log PASSED when it passes or Alert ERROR when it fails). To learn more about this capability, see the AlertLogPkg_user_guide.pdf in our documentation repository: https://github.com/OSVVM/Documentation.
OSVVM Data Structures prior to release 2021.06
From January 2015 to June of 2021, the AlertLogPkg was the only singleton implemented in the OSVVM library. The other data structures, such as CoveragePkg used a protected types API.
For a simple ALU, we may wish to use Functional Coverage to show that each of the 8 sources from one input of the ALU has been used with each of the 8 sources from the other input to the ALU. This results in a cartesian cross (of 8 x 8) – which is the simplest thing that is done with functional coverage.
Using the CoveragePkg protected type API to model functional coverage results in the following code.
architecture Test2 of tb is shared variable ACov : CovPType ; -- Declare begin TestProc : process variable RV : RandomPType ; variable Src1, Src2 : integer ; begin -- create coverage model ACov.AddCross( GenBin(0,7), GenBin(0,7) ); -- Model while not ACov.IsCovered loop -- Done? (Src1, Src2) := GetRandPoint(ACov) ; -- Intelligent Cov Rand DoAluOp(TRec, Src1, Src2) ; -- Transaction ACov.ICover( (Src1, Src2) ) ; -- Accumulate end loop ; ACov.WriteBin ; -- Report EndStatus(. . . ) ; end process ;
For more on this example, see ProtectedTypes_Old/CoveragePkg_user_guide.pdf in the documentation repository: https://github.com/OSVVM/Documentation.
Many find the calling format of protected type methods awkward. The call to AddCross is prefixed by the coverage object name: ACov.AddCross(…). This did not bother me too much, but it has been lingering in the back of my mind.
What does bother me about protected types is that they can only be held by a shared variable, and hence, passing them around is awkward. Sure, you can work around this by putting them in a package or using an external name.
In addition, without VHDL-2019 you cannot create an array of protected types – such as an array of scoreboards. You can emulate arrays internal to the protected type and OSVVM does this for arrays of scoreboards inside of the ScoreboardGenericPkg, but this only works for a single dimension – what about a matrix of scoreboards. What then?
The answer is singletons.
Singleton’s for ALL OSVVM Data Structures
In releases 2021.06 and 2021.07, ScoreboardGenericPkg, MemoryPkg, and CoveragePkg (as well as few internal packages) were refactored significantly to add a shared variable to the protected type body and a user API to interact with the shared variable – both of these following the singleton pattern.
Like AlertLogPkg, inside the protected type is a resizable array of things – specifically Scoreboards, Memories, and Coverage Models. For each of these the constructor hidden in the protected type and allocates “a thing” and returns a descriptor with which to access it. This constructor is accessed using the method NewID (common name for all of them). Again all of the complexity is hidden behind a simple user API. An additional benefit is that improvements to the protected type internal data structure can be made without impacting the user API.
Another benefit of the singleton approach is that the descriptor is a regular type and we can (and do) create arrays of it. For Scoreboards, we have already defined 1D and 2D arrays and a corresponding NewID. Example below shows how to use an array of scoreboards. For details see Scoreboard_user_guide.pdf and Scoreboard_QuickRef.pdf
signal SB : ScoreboardIDArrayType(1 to 3); . . . -- Create 3 indexed scoreboards SB <= NewID("SB", 3); wait for 0 ns ; -- Push values into scoreboards Push(SB(1), X"11") ; Push(SB(2), X"21") ; Push(SB(3), X"13") ; . . . -- In another process check values Check(SB(3), X"13") ; Check(SB(2), X"21") ; Check(SB(1), X"11") ;
If you need to pass the value through a signal interface, you can do that too. With these updates, the AXI4 Full and AxiStream verification components have moved their burst FIFOs (or at least the descriptors for the burst FIFOs) to the OSVVM Model Independent Interface (a record). For details see AXI4_VC_user_guide.pdf or AxiStream_user_guide.pdf.
Currently for Memory Models and AlertLogIDs, calling NewID/GetAlertLogID with a name that was registered by a previous call to NewID/GetAlertLogID will return the ID for that name rather than creating a new ID. As a result, to access an object we only need to know its name – no external names and no need to pass the object around. This look up by name capability will be implemented for CoveragePkg and ScoreboardGenericPkg in a future release.
With the singleton, the functional coverage example from the last section, is updated as follows:
architecture Test2 of tb is signal ACov : CoverageIDType ; -- Declare ID begin TestProc : process variable RV : RandomPType ; variable Src1, Src2 : integer ; begin -- create coverage model AddCross(ACov, GenBin(0,7), GenBin(0,7) ); -- Model loop (Src1, Src2) := GetRandPoint(ACov) ; -- Intelligent Cov Rand DoAluOp(TRec, Src1, Src2) ; -- Transaction ICover(ACov, (Src1, Src2) ) ; -- Accumulate exit when IsCovered(ACov) ; -- Done? end loop ; WriteBin(ACov) ; -- Report ReportAlerts ; end process ;
For more on this example, see CoveragePkg_user_guide.pdf in the documentation repository: https://github.com/OSVVM/Documentation.
By using a singleton and making it accessible by name, we make it easier to split the functional coverage model across different models – such as a test sequencer and a monitor (a verification component that observes an interface).
This example shows that for simple cartesian cross coverage, VHDL is as concise as SystemVerilog. That is where the similarity ends. When it is not a simple cartesian cross, SystemVerilog capability is limited by its declarative nature. OTOH, OSVVM functional coverage is captured in sequential code – incrementally. Hence, if the coverage is not a cartesian cross, this is as simple as writing the code with all of the capability of VHDL’s sequential statements (if, case, while, for, …). Conditional coverage is just an “if” statement – and OSVVM coverage models, or pieces of them, can be passed via generics.
Going further OSVVM implements our signature Intelligent Coverage Randomization™, which does a random walk across the functional coverage model to generate stimulus. This is a light weight version of an intelligent testbench tool built into CoveragePkg – which is free open source. Intelligent Coverage Randomization, which can generate full coverage in O(n) steps, has a performance (speed) benefit over constrained random (including SystemVerilog with its solvers) which randomization theory tells us it takes O(n* log n). See the CoveragePkg_user_guide for more details.
What about the other Data Structures Cliff Mentioned
Cliff mentioned Dynamic arrays, associative arrays, queues, and mailboxes. Queues are the same as FIFOs and we already have those in OSVVM’s ScoreboardGenericPkg.
Associative arrays, also know as dictionaries, are a candidate for future implementation in the OSVVM library. One use model we have heard is implementing memories, however, OSVVM already has a purpose built sparse memory data structure in MemoryPkg that better addresses the needs of memory modeling (and includes memory File IO capability)
Internally OSVVM has a minor usage for dictionaries for faster searching of names, however, these searches currently are one time events, so the return on investment does not make this a priority implementation. As a result, we are looking for a compelling verification use model for dictionaries.
If you would like to share a use model or report an issue with OSVVM, a good place to log it is at: https://github.com/OSVVM/OsvvmLibraries/issues.
Dynamic arrays sound interesting. Our scoreboard models use a dynamic array internally. For example, the std_logic_vector instance of our Scoreboard, ScoreboardPkg_slv.vhd, allows you to push and pop any sized of std_logic_vector. For example, in the following we push 8 bits and then 16 bits, however, take care to first pop 8 bits and then 16 bits.
signal FIFO : ScoreboardIDType ; . . . FIFO <= NewID(“FIFO1”) ; wait for 0 ns ; push(FIFO, X”AB”) ; push(FIFO, X”ABCD”) ; . . . -- in some other process A_slv8 := pop(FIFO) ; B_slv16 := pop(FIFO) ;
Dynamic arrays would be easy enough to add to OSVVM if anyone is willing to share a compelling use model.
Mailboxes need blocking capability. To implement this, we will need some very simple VHDL-2019 features. Make sure to ask your vendors to implement VHDL-2019 feature that allows impure functions to have parameters that are inout and out. It would also be helpful to have VHDL-2019 interfaces, but we can implement the capability without this feature.
In early 2022, we plan on experimenting with Mailboxes and other features that require blocking using Aldec Riviera-PRO and any other simulator that has already implemented the necessary parts of VHDL-2019.
Moving Toward First Class Data Structures
OSVVM data structures differ from first class data structures since they do not call the constructor (NewID) in the declaration of the descriptor (ID). The code would certainly be more compact if we did this. In VHDL-202X, the IEEE VHDL WG has a proposal to remove this restriction (see https://gitlab.com/IEEE-P1076/VHDL-Issues/-/issues/75). This will allow us to do the following:
constant FIFO : ScoreboardIDType := NewID(“FIFO1”) ;
It should be noted that the following tools currently allow this to be done: Aldec ActiveHDL/RivieraPRO, Siemens EDA ModelSim/QuestaSim, GHDL, and Synopsys VCS. So I do not expect resistance in having this change accepted.
I work on VHDL standards, so I prefer my examples to have strict language compliance, but also share this kind of information so you can make the decision as to whether you want to abuse the language/tools or not.
VHDL, OSVVM, and Standards
If you are using VHDL as your design language, OSVVM is the right answer for your verification methodology. We apply our deep knowledge of VHDL – gained through design, verification, and standardization experience to provide you with easy to use, language compliant solutions. And we continue to lead the way and push the boundaries of what is capable.
OSVVM is an IEEE Open Source Project. Join us in further enhancing OSVVM. Pull requests are welcome.
The OSVVM developers are also part of the IEEE VHDL standards community. Our intent is to help make sure the next version of the language has the features we need to take the next step forward in verification methodology capability.