Using Multiple OSVVM Verification Components
Several times now I have been asked how to use multiple OSVVM VC, like a UART, in a simulation. This should be simple, however, there are a couple of VHDL and OSVVM got-yas.
For the 2022.10 release, I prepared a basic example that instances 16 UartTx and UartRx components. The good thing about writing test cases like this is that it reveals further opportunities for improvements. As I wrote this article, I was also working on the 2022.11 release. 2022.11 is now released – and this article was updated slightly to reflect this.
If you are not already familiar with OSVVM, you may wish to read OSVVM Structured Testbench Framework and OSVVM Test Writers User Guide. You will find these in the directory OsvvmLibraries/Documentation.
There are three interesting aspects to this problem: the record array type, the test harness, and the test sequencer. Examples of the test harness and test sequencer are in the directory OsvvmLibraries/UART/testbench_multiple_uarts.
Types
When using multiple VC of the same type, it is convenient to use an array type. Release 2022.11 defines StreamRecArrayType in StreamTransactionPkg as follows.
type StreamRecArrayType is array (integer range <>) of StreamRecType ;
UartTbPkg (2022.11) creates the subtype UartRecArrayType to add constraints to StreamRecArrayType as shown below:
subtype UartRecArrayType is StreamRecArrayType(open)( DataToModel (UartTb_DataType'range), ParamToModel (UartTb_ErrorModeType'range), DataFromModel (UartTb_DataType'range), ParamFromModel(UartTb_ErrorModeType'range) ) ;
Note the above types generate issues for GHDL and reports have been filed.
Test Harness
This system is just a demonstration system that connects each UartTx to a UartRx. Pictorially it is as shown below. In a real system, the UartTx would send to the DUT and the UartRx would receive from the DUT.
The test harness uses a “for generate” to create the instances. This is shown below.
signal UartTxRec : UartRecArrayType (1 to NUM_UARTS) ; signal UartRxRec : UartRecArrayType (1 to NUM_UARTS) ; . . . begin . . . GenerateUartInstances : for GEN_UART in 1 to NUM_UARTS generate begin ------------------------------------------------------------ UartTx_1 : UartTx ------------------------------------------------------------ generic map ( MODEL_ID_NAME => "UartTx_" & to_string(GEN_UART) ) port map ( TransRec => UartTxRec(GEN_UART), SerialDataOut => SerialData(GEN_UART) ) ; ------------------------------------------------------------ UartRx_1 : UartRx ------------------------------------------------------------ generic map ( MODEL_ID_NAME => "UartRx_" & to_string(GEN_UART) ) port map ( TransRec => UartRxRec(GEN_UART), SerialDataIn => SerialData(GEN_UART) ) ; end generate GenerateUartInstances ;
There are two got-yas that happened that resulted in either AlertLogIDs or FIFOs being shared among all UartTx (and likewise for all UartRx). The simple fix for both of these is to specify a unique MODEL_ID_NAME generic value for each instance (as shown in the code example).
Sharing of AlertLogIDs is a result of using “for generate”. Inside the “for generate” the instance label for all UartTx ends with UartTx_1. Without the MODEL_ID_NAME being set, the AlertLogName for all 16 of the UartTx VC is “UartTx_1”, and as a result, the AlertLogIDs are all the same.
Sharing of FIFOs is a result of the AlertLogID being the same and default settings in NewID (prior to 2022.11). Sharing of FIFOs resulted in mangled transactions. For OSVVM VC, without the MODEL_ID_NAME being set, this issue started happening with OSVVM release 2022.02 and ended with 2022.05.
Test Sequencer – Multiple Processes
OSVVM codes its test cases in the test sequencer (which I typically name TestCtrl). My preferred use model is to create a separate process for each independent interface. This works particularly well with applications where the interface stimulus is generated using randomization.
The following is the TbUart_MultipleProcess_1.vhd example (2022.11). It uses “for generate” to generate 16 UartTxProc and 16 UartRxProc. Data is transmitted in UartTxProc by calling the Send transaction. Data is received in UartRxProc by calling the Get transaction. A scoreboard is used to check the transactions.
architecture MultipleProcess_1 of TestCtrl is . . . use osvvm_uart.ScoreboardPkg_Uart.all ; signal RxScoreboard : osvvm_uart.ScoreboardPkg_Uart.ScoreboardIdArrayType(1 to NUM_UARTS) ; . . . begin . . . GenerateUartHandlers : for GEN_UART in 1 to NUM_UARTS generate signal RxActive : boolean := TRUE ; begin ------------------------------------------------------------ UartTxProc : process ------------------------------------------------------------ variable TxStim : UartStimType ; variable RvCtrl, RvData : RandomPType ; begin RvCtrl.InitSeed(RvCtrl'INSTANCE_NAME) ; RvData.InitSeed(RvData'INSTANCE_NAME) ; wait for 0 ns ; wait for 0 ns ; TransmitLoop : while RxActive loop -- Burst of Transactions for i in 1 to RvCtrl.RandInt(1, 20) loop TxStim.Error := RvData.DistSlv((70,10,10,6,1,1,1,1), 3) ; if TxStim.Error >= 4 then TxStim.Data := RvData.RandSlv(11,25,8); -- Break Error elsif TxStim.Error <= 1 then TxStim.Data := RvData.RandSlv(0,255,8); -- Normal & Parity Error else TxStim.Data := RvData.RandSlv(1,255,8); -- Stop end if ; Push(RxScoreboard(GEN_UART), TxStim) ; Send(UartTxRec(GEN_UART), TxStim.Data, TxStim.Error) ; exit when not RxActive ; end loop ; -- Idle between bursts WaitForClock(UartTxRec(GEN_UART), RvCtrl.RandInt(1, 5)); end loop TransmitLoop ; WaitForBarrier(TestDone) ; wait ; end process UartTxProc ; ------------------------------------------------------------ UartRxProc : process ------------------------------------------------------------ variable ReceivedVal : UartStimType ; variable RvCtrl : RandomPType ; begin RvCtrl.InitSeed(RvCtrl'INSTANCE_NAME) ; wait for 0 ns ; wait for 0 ns ; ReceiveLoop : while TestActive or not Empty(RxScoreboard(GEN_UART)) loop -- Burst of Get Transactions for i in 1 to RvCtrl.RandInt(1, 20) loop Get(UartRxRec(GEN_UART), ReceivedVal.Data, ReceivedVal.Error) ; Check(RxScoreboard(GEN_UART), ReceivedVal ) ; exit when not TestActive ; end loop ; RxActive <= TestActive ; WaitForClock(UartRxRec(GEN_UART), RvCtrl.RandInt(1, 5)); end loop ; -- Idle between bursts WaitForBarrier(TestDone) ; wait ; end process UartRxProc ; end generate GenerateUartHandlers ;
Test Sequencer – Single Process
While my preference is to use multiple processes to dispatch stimulus, OSVVM certainly supports using a single process to dispatch stimulus. The following is the TbUart_SingleProcess_1.vhd (2022.11) example.
------------------------------------------------------------ CentralTestProc : process ------------------------------------------------------------ begin SendAsync(UartTxRec( 1), Data => X"01", Param => "000") ; SendAsync(UartTxRec( 2), Data => X"02", Param => "000") ; SendAsync(UartTxRec( 3), Data => X"03", Param => "000") ; SendAsync(UartTxRec( 4), Data => X"04", Param => "000") ; . . . Check(UartRxRec( 1), Data => X"01", Param => "000") ; Check(UartRxRec( 2), Data => X"02", Param => "000") ; Check(UartRxRec( 3), Data => X"03", Param => "000") ; Check(UartRxRec( 4), Data => X"04", Param => "000") ; . . . end process CentralTestProc ;
The downside of a single process dispatch approach is that if you need to do a Read (address bus) or Get (streaming) type transaction to inspect a value, the process needs to block (ie: wait for data to be available). Blocking on a transaction to one interface, prevents other interfaces from simultaneously doing a read or get transaction. While there are appropriate non-blocking transactions in OSVVM, such as ReadAddressAsync + TryReadData or TryGet, doing this while also interacting with other VC is going to be an interesting challenge. Using a separate process for these type of test cases allows a process to block without preventing other interfaces from simultaneously doing a read or get transaction at the same time.
Indexing Signal Parameters while Looping
The next step of improvement to the above process is to use a loop. However, VHDL requires that the actual to a formal signal parameter have a static signal name. Unfortunately, this is violated if a signal parameter is indexed using the loop variable. More information on this issue is in VHDL Issue 275. My expectation is this limitation will be addressed in the next revision of VHDL (because I will help make it so). Note this restriction does not apply to formal parameters that are constants or variables.
OSVVM 2022.11 creates an array version of streaming transactions (in StreamTransactionArrayPkg) to work around the "static signal name" issue. The following is the TbUart_SingleProcessLoop_1.vhd example (2022.11). For all stream transactions, there is a form that supports arrays of transaction records as shown in the example below. It is simple, instead of indexing the UartTxRec (or UartRxRec), call the transaction with the Index as the second parameter immediately following UartTxRec (or UartRxRec) below.
------------------------------------------------------------ CentralTestProc : process ------------------------------------------------------------ begin wait for 0 ns ; wait for 0 ns ; for i in 1 to 16 loop SendAsync(UartTxRec, i, to_slv(i, 8), "000") ; end loop ; for i in 1 to 16 loop Check(UartRxRec, i, to_slv(i, 8), "000") ; end loop ; WaitForBarrier(TestDone) ; wait ; end process CentralTestProc ;
Using the above requires that UartTxRec and UartRxRec be of the type StreamRecArrrayType which is defined in StreamTransactionPkg (2022.11 and shown below).
type StreamRecArrayType is array (integer range <>) of StreamRecType ;
Preventing Unwanted FIFO Sharing (also Coverage and Memory)
When sharing of data structures was introduced for the FIFO/Scoreboard, Coverage Model, and Memory data structures, it was expected that using a name and the ParentID would be sufficient to distinguish the data structures in separate VC, but also allow easy access to them when appropriate. Hence, the default setting NAME_AND_PARENT_ELSE_PRIVATE was created as the default to search by the NAME and ParentID in the search when the ParentID is specified.
Unfortunately iterating across verification components with a for generate causes the naming to be problematic. As a result, 2022.11 changed the default to PRIVATE_NAME so that sharing will not happen by default. It is just too easy to introduce an issue that is challenging to find. It is better to require that sharing be activated in a more explicit manner.
Until adopting 2022.11, you can specify PRIVATE_NAME in calls to NewID as shown below.
architecture model of UartTx is . . . signal TransmitFifo : osvvm.ScoreboardPkg_slv.ScoreboardIDType ; . . . begin Initialize : process variable ID : AlertLogIDType ; begin ID := NewID(MODEL_INSTANCE_NAME) ; ModelID <= ID ; TransmitFifo <= NewID("TransmitFifo", ID, ReportMode => DISABLED, Search => PRIVATE_NAME) ; wait ; end process Initialize ;
Creating a Good AlertLogName for a VC
The "for generate" instance name issue changed another practice in OSVVM. All OSVVM VC now have a MODEL_ID_NAME generic (2022.05). The AlertLogID for the VC is created as follows. This is a recommended practice for all VC (so they can support multiple instances with "for generate").
entity UartTx is generic ( MODEL_ID_NAME : string := "" ; . . . ) ; port ( . . . ) ; -- Use MODEL_ID_NAME if defined, otherwise, derive a name from UartTx'PATH_NAME constant MODEL_INSTANCE_NAME : string := IfElse(MODEL_ID_NAME'length > 0, MODEL_ID_NAME, to_lower(PathTail(UartTx'PATH_NAME))) ; end UartTx ; architecture model of UartTx is signal ModelID : AlertLogIDType ; . . . begin Initialize : process variable ID : AlertLogIDType ; begin ID := NewID(MODEL_INSTANCE_NAME) ; ModelID <= ID ; . . .
For VHDL-202X we are also working on allowing calls to NewID in a constant declaration as shown below. This would replace both the signal declaration and the Initialize process. VHDL does not currently allow calling NewID in a declaration since protected type methods are not permitted to be called in a declaration region and internally NewID calls a protected type method. It should be noted that most vendors already allow you to do this. This is an important change for VHDL as it makes singleton data structures (such as those defined in OSVVM) work in the same fashion as language defined types.
constant ModelID : AlertLogIDType := NewID(MODEL_INSTANCE_NAME) ;
Summary
One of OSVVM’s many advantages over other VHDL Verification Methodologies is that we are also the same VHDL experts who have helped develop VHDL standards. This makes us very skilled at finding solutions that work with the language and are VHDL compliant. It also means that when the best we can do is a work around, we are motivated to help update the standard and make it more capable.