Building an OSVVM Co-simulation VC
Introduction
With the 2026.01 release of OSVVM a new verification component (or VC) was added to support the PCI Express (PCIe) bus protocol. OSVVM already has a comprehensive suite of VCs for both memory mapped address bus protocols, such as AXI, and streaming protocols, such as Ethernet. The PCIe VC extends OSVVM’s supported protocols as part of its ongoing development. However, there is something different about the PCIe VC that sets it apart from the existing VCs. That is that it’s a co-simulation VC—the protocol is modelled in C and the OSVVM co-simulation features have been used to interface this model to the VHDL domain in a component that looks like any other VC.
As the PCIe VC was the first co-simulation VC, the methods used to integrate the C model into such a component were invented for this purpose. However, these methods can now be used to integrate other C models that generate protocol transactions at the signal bit level. In this article I want to document this process using the PCIe VC as the example case so that others can do so with their own available models, or ones they may wish to construct.
What is an OSVVM VC?
Before diving into a co-simulation VC it might be prudent to define what an OSVVM VC is in general and how it is used within the OSVVM verification environment.
The philosophy of OSVVM is that a test program generates transactions that are independent of any particular protocol. These are known as model independent transactions, or MITs. A common aim of these protocols is to move data from a source to a target over the bus, but there are two types of general protocol—namely, memory (or I/O) mapped address busses and streaming busses. The former is characterized by having an address to define a location in a memory or I/O space, and the latter an address-less point-to-point communication. Some modern protocols have point-to-point connections but still have addresses for a memory space. These we’ll consider as address buses as they still behave like an old school memory mapped bus, just via a switching network. Also worth noting, for both types of protocols, is that a transaction has a source and destination—or a requestor and completer, or manager and subordinate, or whatever you to call it. For address busses there is usually a unidirectional order from a requestor to a completer, such as AXI memory mapped bus, but not always. Streaming interfaces, such as ethernet, have a more symmetrical arrangement, with transmission and reception ports at each end, and requests able to flow in both directions.
OSVVM makes these distinctions and provides VCs in one of the two categories and, as we’ll see, defines machine independent transaction signals for both. For address bus MITs, it also makes distinction between a requestor and a completer.
The general Structure of a VC
A VC is a VHDL component that is instantiated in the test bench. It will have a clock that may be part of the protocol signaling, and (often) a reset and is likely to have some generics—perhaps for defining timings and to set a model name for identification in logs. Then there will be two main port groups. One group will be an MIT group, with streaming VCs usually having a pair of MIT ports and address bus VCs a single MIT port. The other group will be a port for the specific protocol signalling. The diagram below shows the component for the AXI4-Lite manager VC:

MIT Record Types
In the diagram for an example VC, the MIT port is an AddressBusRecType signal, and this will be true for all address bus VCs, whatever the specific protocol being modelled is. For streaming VCs, the MIT signal type will be StreamRecType. These two record types have a few things in common, but or not interchangeable. The definition for the AddressBusRecType is shown below:
type AddressBusRecType is record Rdy : RdyType ; Ack : AckType ; Operation : AddressBusOperationType ; Address : std_logic_vector_max_c ; AddrWidth : integer_max ; DataToModel : std_logic_vector_max_c ; DataFromModel : std_logic_vector_max_c ; DataWidth : integer_max ; WriteBurstFifo : ScoreboardIdType ; ReadBurstFifo : ScoreboardIdType ; Params : ModelParametersIDType ; StatusMsgOn : boolean_max ; IntToModel : integer_max ; IntFromModel : integer_max ; BoolToModel : boolean_max ; BoolFromModel : boolean_max ; TimeToModel : time_max ; TimeFromModel : time_max ; Options : integer_max ; end record AddressBusRecType ;
The StreamBusRecType is similar, but has no address fields, has a ParamToModel and ParamFromModel in the Data group but no DataWidth, a single burst fifo and no StatusMsgOn field.
The Rdy and Ack signals are used to handshake a transaction request between the test program procedure calls and the VC and the operation specifies what type of transaction is requested, such as WRITE_OP or READ_OP, but also things like WAIT_FOR_CLOCK, SET_MODEL_OPTIONS and GET_TRANSACTION_COUNT, as well as many others.
The address and its width are specified, and a pair data fields to send write data to the model and return read data from the model. These are for single cycle word transfers only (bytes, half-words, words etc.) up to the data width. For burst data transfers, write and read burst FIFOs are provided.
The Params field is used to reference an array of parameters which can be of mixed types for the elements. This is used to send multiple parameter attributes along with a transaction operation, specific to a VC for it to interpret.
The StatusMsgOn boolean flag is used to force the VC printing status messages, regardless of other settings, to aid debugging.
The next group of fields are for transferring other types of data to and from VC for integer, boolean and time types. Finally, the Options field allows configuration (or inspection) of a VC’s configuration parameters—usually when SET_MODEL_OPTIONS or GET_MODEL_OPTIONS is the transaction operation.
Note that operations that do not result in a transaction on the bus, like waiting for a clock or a transaction to complete do, result in zero simulation time advancing. For example, SET_MODEL_OPTIONS.
Driving the MIT signal on a VC
We’ve discussed the operations that an MIT signal can have, along with other settings, but how do we construct the settings on the record signal to pass to a VC to instigate the transaction? OSVVM provides a set of VHDL procedures that can be called from a test process that sets the values of an MIT signal, such as one of AddressBusRecType. All will have the MIT signal as the first argument, followed by a set of appropriate arguments to fill in the appropriate fields for the operation. For example, the Write procedure’s prototype definition is:
------------------------------------------------------------ procedure Write ( -- Blocking Write Transaction. ------------------------------------------------------------ signal TransactionRec : InOut AddressBusRecType ; iAddr : In std_logic_vector ; iData : In std_logic_vector ; StatusMsgOn : In boolean := false } ;
There are a rich set of procedures provided, ranging from basic reads and write, to split transactions, random data generation variants and checking variants. The table below shows just those procedures for AddressBusRecType manager signals, but there are also a set for subordinate transactions and a set of common directive transactions. There are also a set of procedures for the StreamRecType.

Note that the right hand column shows the value of the Operation field setting of the record for the procedure in the left hand column. Where there is no operation given, these procedures work on the signal itself, such as pushing to or popping from the FIFO referenced by IDs to burst FIFOs. All the available procedures for driving MIT signals are documented in the Address Bus Model Independent Transactions User Guide and the Stream Model Independent Transactions User Guide.
The normal VHDL VCs’ job is to interpret received MIT transaction operations, translate to protocol specific operations and return status and, if appropriate, data. For non-transaction commands, it does the MIT interpretation, but returns data and status from internal state, without a protocol transaction being generated.
OSVVM provide ready made VCs, but custom VCs can be implemented for custom bus protocols or for standard protocols not provided in the library. Creating one’s own VHDL VC is documented in the OSVVM Verification Component Developer’s Guide
OSVVM Co-simulation
We’ve looked at what a VC is, what the MIT signals are and their types, and how to drive them with the library’s set of VHDL procedures. Before moving on to a co-simulation VC, a brief summary of OSVVM’s co-simulation features is in order so that the scene is set to use these features to create a co-simulation VC.
Co-simulation features were introduced into OSVVM at revision 2023.01 and allow C or C++ programs to be natively compiled on the host machine (that’s running the simulation) as shared objects, loaded by the simulator and run in a thread with access to the logic simulation through a provided API. Thus, the program is running as part of the simulation, rather than as a separate process requiring inter-process communication.
As discussed earlier, VHDL procedures are provided to drive model independent transaction (or MIT) signals that can be connected to a Verification Component (or VC) that turns these generic transactions into protocol specific signalling such as AXI for example, that can then drive a design under test. These procedures can be for memory mapped address bus type protocols or streaming type protocols—and be for manager or subordinate connections.
With co-simulation, the functionality traditionally provided by the VHDL procedures is mapped to equivalent API methods in the C or C++ domain. The programs are free running but, when an API method is called, the underlying mechanism synchronises the program with the simulation and the generated transaction.
There can be multiple running co-simulation programs, driving multiple MIT signals, and so each source of transactions needs a unique ID known, for historical reasons, as a “node” number. The “main” entry point for a given node’s user program has the form of VUserMain<n>, where <n> is the node number. Thus, multiple different programs can be run and interact with the OSVVM simulation via separate MIT signals.
The diagram below shows a classic OSVVM setup. This has a test control block with, in this example, a manager process with a set of calls to VHDL procedures that drive an MIT signal (called ManagerRec) which is an address bus record type (as opposed to a stream record type). This drives a verification component—such as one for AXI4—that then drives a DUT’s subordinate bus. A similar setup could have a subordinate process with an MIT signal connected to a manager bus on the DUT via a VC—or there could be both.

The second diagram below shows the same setup for co-simulation. The OSVVM VHDL procedures calls are replaced with, firstly, a call to a VHDL procedure for the node, called CoSimInit, which initialises the co-simulation interface then loads and runs the node’s user program. To drive a transaction on the MIT signal, a call to a CoSimTrans VHDL procedure is made, with the MIT signal as a parameter. This fetches and executes the next transaction from the co-simulation program instigated when it calls an API method. It isn’t necessary, but calls to CoSimTrans would normally be done in a loop, as shown, but can be mixed and matched with other OSVVM procedure calls.

User program and Co-Simulation API
A co-simulation user program is a VUserMain entry point function for a given node, in lieu of a “main”, and the node number is passed in as an argument. An object for the co-simulation API is created—the example below shows an API for a memory mapped address bus (OsvvmCoSim), but this could have been for a streaming interface (OsvvmCosimStream) if driving, say, an ethernet VC. A simple example is shown below:

From this point, any of the API methods available can be called, and the example shows some of the simplest for single word reads and writes and burst accesses. But access to all the different mapped OSVVM procedures shown earlier are available from the user program. The example is simple but, of course, hierarchy and structure to any level of complexity can be added from the VUserMain function, and libraries linked, just as for any C++ program. OSVVM also provides scripts to help compile the user code easily for OSVVM co-simulation.
One last thing to note from the example is the “tick” method. This allows time to pass in the simulation without having to do a transaction—it uses the OSVVM’s WaitForClock procedure—handy for introducing delay between transactions. The first argument specifies the number of cycles (as used by the clock on the MIT signal being driven), but a second argument can indicate to the test bench logic that a program has completed, and a third that there has been an error with a non-zero error code.
The example above shows some basic transactions, but the co-simulation API maps all the available VHDL procedure operations to equivalent functions (for C) or methods (for C++), as documented in the OSVVM Co-simulation Framework document.
OSVVM provides scripting help for ease of compiling the co-simulation code and details can be found in the co-simulation framework document referenced in the last paragraph, and more details for linking external libraries have been given in a previous article I wrote on the subject.
Types of OSVVM Co-simulation Programs
Since we’re now in the C/C++ domain, we can write any type of program to run. These could, at the simplest level, be test programs to replace VHDL test processes, but with access to all the libraries resources available to a C or C++ program—one of its key advantages.
If relevant C or C++ libraries are available, these could be used to generate or process quite complex data more easily than in VHDL or be used as reference models for DUT output data for comparison.
For co-development of embedded software with logic IP, an instruction set simulator model of a processor could be the running program. OSVVM co-simulation does actually provide an integrated RISC-V model, from the rv32 ISS project, and examples of its use for this purpose.
Or, less obviously perhaps, the running program might be a model that can generate a bus protocol’s transaction data at the clocked signal level, and this is what the PCIe VC model does. The rest of the article will look at how this was done and how other such C and C++ models, either pre-existing or newly created, can be integrated to create an OSVVM VC.
Characteristics of a Co-simulation VC
The overriding aim here is that a co-simulation VC should be used in exactly the same way as a VHDL VC as much as possible. The user of the VC should not need to write co-simulation software. However, software to implement the protocol and interface to MIT signals is required, obviously, but must be provided as part of OSVVM. As we shall see, there does need to be a trivial top level VUserMain program to bind a particular node to the PCIe software—but even here this is provided as a template that a user can bind to their particular chosen node for the VC.
The VHDL Component
With this in mind we can say that the VHDL component will have the same characteristics as a VHDL VC, namely that it will have an MIT port signal and protocol interface signals. Whether the MIT is address bus or streaming will depend on the protocol, and whether it has separate clock and reset will also depend on whether these signals form part of the protocol or not.
We will be using the PCIe co-simulation VC as a case study, and the diagram below shows its component definition by way of example.

This looks remarkably similar to the Axi4LiteManager VC shown earlier, by design. There are clock and reset signals and an AddressBusRecType MIT port, suitable for the PCIe protocol. Protocol specific ports are provided via in and out ports of LinkType—an unconstrained type that defines the number of lanes by the connection array size and whether a PIPE interface or an 8b10 interface by each element’s with (9 or 10 bits respectively). There are also a couple of clock related output signals that, although not part of the PCIe protocol are, none-the-less, PCIe specific signals.
For the generics, it has a MODEL_ID_NAME, like the AX4-Lite VC, and some protocol specific generics for static configuration of the model. The only visible feature that is co-simulation related is that there is a NODE_NUM generic to set the model’s unique node ID.
The diagram also shows some other “hidden” features that aren’t on the component’s entity. It happens that the PCIe C model has features to display decoded and formatted PCIe link output for debugging purposes, and this is controlled by reading an external ContDisps.hex file to define when and how much information is displayed. It also shows a shared object, VProc.so, which contains the co-simulation code, including the library to connect the PCIe C model, and is loaded by the simulation at run time. This VProc.so library will load the VUSer.so library containing the (trivial) user code and the PCIe C model. These are compiled by OSVVM scripting, so little user intervention is required.
So, the co-simulation VC looks, to all intents and purposes, like any other VC. We have yet to define what the internals of the VHDL component need to be, but we have defined the entity and component, which is a good start.
The Protocol C Model Characteristics
If a co-simulation VC is being constructed, this assumes that either a C or C++ model already exists, or that one is to be constructed. To be suitable for use in creating a co-simulation VC it needs to have certain straightforward characteristics. Firstly, it must have a C or C++ API to instigate the generation of the protocol signalling data. Secondly, it must have functions or methods to generate output signal data and read input signal data to the resolution of a clock cycle. If the protocol allows static idle periods (i.e. ones where idle output data is not continually being transmitted and received, and the signals don’t change) it should leave the signals in an idle state until the next call. This should be all that’s needed.
Using the PCIe C model example, it has a rich API to allow a calling program to generate all the different transaction types for the transaction layer, data link layer and physical layer, with idle output in-between. The tables below summarise the main transaction generation C API functions (there are C++ equivalents in an API class), but there are many more variants of these and also other functions for configuration and internal model state inspection and modification. The full list is documented in the PCIe Virtual Host Model Test Component.

The above, then, provides software means to generate transactions on the protocol bus. At the signalling end, the model generates output values and reads inputs by calling a single external function called VWrite (for historical reasons) with the following prototype definition:
int VWrite (const unsigned idx, const unsigned data, const int delta, const unsigned linknum);
Here an index is supplied that selects a particular signal output to update, along with data with which to update the selected signal. It also has a “delta” argument. This flags to the external program that more data is to come for this cycle, allowing for updating output that is wider than the data supplied in a single call to the function. A characteristic of this particular function is that the index also addresses an equivalent input signal and this should be returned to the model. For PCIe, the indexes from 0 to 15 address up to 16 lanes, and at each clock cycle the function is called with output data for each active lane and returns the value on the equivalent input lane. On all but the last update/read for the cycle, delta will be set. The final argument is for a link number to allow the same code to drive multiple PCIe links and selecting which link to update.
The PCIe model uses VWrite as a write and read call, but it is not necessary and a model could have separate read function. The main defining characteristics must be to indicate a signal to update and read (or partial signal, if wider than the supplied data) with some sort of index, and to flag that no more updates are forthcoming for the current cycle. Even this last could be a separate function call.
And this is all the interfacing that a model needs to be useful for generating a new co-simulation VC (with the actual protocol signal generation and processing a given). The API needs connecting to the MIT interface, and the signal function needs connecting to the protocol signal ports—we’ll look at this later.
PCIe model as an Endpoint
As mentioned before, the model defaults to being an uplink, but it does have some Endpoint capabilities as well.
It has an internal sparse memory model which has a full 64-bit range. This can act as a target for all memory TLPs and, when enabled, these TLPs access this memory rather than being passed to the user registered callback for processing. The API also provides back door methods to access the memory model, allowing data to be pre-loaded, or external IP PCIe accesses to be inspected and checked.
A type 0 configuration space is also available which, by default, acts as a simple memory, much like the memory model, that configuration space TLPs can target. Like the memory model, this space can be written and read but there is also a shadow mask space which can be written to mark bits as read-only. It can’t yet handle things like write-one-to-clear.
With the internal memories enabled, the model can automatically generate completions to non-posted TLPs accessing them but, if disabled, the arriving TLPs are passed to the user callback and this must generate the completions.
Connecting the Dots
In the previous sections we have defined the nature of a co-simulation VC component’s entity, with MIT and protocol specifics ports and a set of generics. We’ve also looked at what characteristics the C or C++ model should have. We now need to connect the software model to the VHDL component to make a unified verification component. The diagram below shows the general architecture of the PCIe VC, showing the connection from HDL to the C model.

In the diagram is a PcieModel VHDL component with the two main interfaces for the MIT signal and the PCIe signals. There is some internal logic to connect these signals to a foreign procedure to pass information between the logic and C/C++ domains. The OsvvmCosim layer is the OSVVM co-simulation software and there is a VUserMain entry point for the program running for the VC’s node. This simply calls a pcieVcInterface layer with a run method to start the model executing. The pcieVcIinterface software makes calls to the OsvvmCosim layer, via a thin OsvvmPcieAdapater layer, to read and write to the HDL to fetch MIT requests and return status and data. It also has access to the PCIe C model’s API to instigate transactions. The PCIe C model calls the external VWrite method to update and read PCIe signals. This is implemented in an OsvvmPcieAdapter layer to map to calls to the OsvvmCosim software layer’s transWrite method that connects to a VTrans foreign procedure. This procedure is the lowest level co-simulation call which is normally hidden within a higher layer procedure such as CoSimTrans or CoSimStream etc. but is used within the model for direct co-simulation connection.
The VC HDL
We have a choice here about how to tackle processing received transaction requests over the MIT port. In general, there needs to be a decode of the MIT operation and a connection to calling the C model’s API. There are two choices that offer themselves: decode in the HDL or decode in the software. If the MIT is decoded in the VHDL there still needs to be an interface to the software, though this could now more nearly match the model’s API, abstracting away MIT details that are specific to OSVVM. If the software is to decode MIT operations then some interface layer needs to be written in C/C++ to map between requests and API calls.
The PCIe VC decodes in software with the co-simulation philosophy of moving computation from the HDL to C/C++ domain as quickly as possible as it is likely that this processing will be considerably faster. In addition, the HDL is much simplified with MIT processing simply handling the received requests and passing them straight on to the software.
The VHDL pseudo-code fragment below shows the general structure of a co-simulation VC’s main process.

Like normal VCs, the main process of a co-simulation VC is a loop to fetch and process MIT requests, with a WaitForTransaction call to handshake new transactions over the MIT signal, and a case statement to select on the operation. The difference for the co-simulation VC is that, instead of decoding the operation, it makes a call to an OSVVM foreign procedure, VTrans, which returns an address (VPAddr) to use as an index to access various state in the VHDL—one of which is to read the MIT signal’s Operation value. The case statement now decodes on the VPAddr to do reads and writes of various kinds. Read state is returned to VTrans via a RdData variable updated in the case statement and passed into VTrans at the next loop iteration. The VPData and VPDataHi variables are updated by VTrans and used to update state, often by being converted to vectors via a WrData variable.
There are four categories of state access available to the pcieVcInterface software:
-
- Reading the values of the VC’s generics
- Reading and writing of the protocol signals
- Reading and writing the MIT signal fields
- Acknowledging the MIT request and waiting for a new one
The first of these allows the PCIe interface software to access the generics of the VHDL component to configure the C model based on these. The second type updates and fetches the protocol signals. This will be instigated from the C model but via the PCIe interface layer. The third is where the PCIe interface is fetching MIT state to read the transaction operation and any relevant accompanying parameters, and to return data and state. The final type is for handshaking the current MIT request. This is done with a call to the OSVVM procedure WaitForTransaction which acknowledges the current transaction and waits for a new one. When the PCIe interface software has completed processing a transaction, it will access this to ensure that at the next loop iteration, a new transaction will be waiting for processing.
Delta cycle processing is done by flagging whether a delta cycle via the VTrans VPOp argument. If a read or asynchronous write it is assumed that it is a delta cycle access. Referencing back to the last diagram above, the OsvvmPcieAdpater software inspects the delta flag passed in by the model when it calls VWrite and calls the appropriate OsvvmCosim API method to make this condition true for all delta cycle accesses. In the HDL, after the case statement, the logic waits for a clock rising edge if not a delta cycle access. Note that for the PCIe VC all the accesses will be delta cycle accesses except for the last PCIe signal update. This is likely to be true of any co-simulation VC.
The PCIe Interface Software
The PCIe co-simulation VC links the VHDL component to the PCIe C model through the VTrans foreign interface, the OSVVM co-simulation software and the OsvvmPcieAdapter layer. The pcieVcInterface layer provides a single run method to be called once to start it running. This takes on the role that would normally be done in a VC’s VHDL.
The pseudo-code below shows the general structure of the pcieVcInterface run method.

On calling this method, the PCIe model is initialised, and the model configured from any settings on the VC component’s generics and brought out of electrical idle. It will wait for reset to be deasserted before proceeding further.
The main body of the method is a loop to process MIT requests and acknowledge them when complete. It does a blocking call to get the next transactions and inspects the transaction operation (e.g. read, burst write, set model option etc.). The operation is decoded in a switch statement and there are four main operation types: model option update and read, a TLP request, a wait for clock, and extended operation, which has sub-categories on the option field of the MIT signal to select waiting for a completion to arrive over the PCIe link in a blocking or non-blocking way.
The TLP request types make the appropriate calls to the model’s API and, for non-posted requests, wait for completions to arrive on an RX queue. It will extract the status and data (if any) and update the MIT signals fields and FIFO as appropriate, before popping the received completion off the front of the queue.
Under the extended operations, arriving TLP requests can be waited for on an REQ queue, or tried to see if one has arrived and, if so, status and data extracted and the MIT fields and FIFO updated accordingly. The transaction at the front of the request buffer is then popped.
At the iteration of the loop, the active MIT request is acknowledged and a new one fetched.
The two queues mentioned are updated by a callback function registered by pcieVcInterface. When completions arrive, status and data are extracted and placed at the back of an RX queue. Likewise, when TLP requests arrive, status and data are extracted and placed at the back of a REQ queue.
The main role of the OsvvmPcieAdapter layer is to provide the VWrite function of the PCIe model to update and read PCIe signals, but it also provides a VRead and 64-bit versions of these. The pcieVcInterface also uses these functions access the VHDL to read and write the MIT signal fields, fetch generic values, and some other ancillary functions that I shan’t mention here. It is these functions that connect with the VTrans foreign procedure.
Pseudo-code for the input handling callback function is shown below

Driving the VC
The VHDL test code can configure the model with a standard call to the OSVVM SetModelOptions procedure. It can also generate PCIe transactions using the standard OSVVM procedures for driving an address bus record type, such as PushFifo, Write or ReadBurst etc., though this requires, in many cases, multiple calls to generate one PCIe transaction due to the nature and complexity of the protocol. Therefore, a package is provided, with a set of helper procedures, to implement single call transaction generation.
The details are not important here, but the available PCIe procedures are shown in the table below, with manager and subordinate type calls.

Validating the VC
Having created a new co-simulation VC, as for any new VC, it needs validating.
Self-consistency Testing
If the VC is symmetrical, such as a UART TX/RX pair for example or, if not symmetrical, there are two VCs for a requestor and completer, then the first stage of validation might be self-consistency. The VC can be set up in a back-to-back arrangement to serve as a target for each other. Vectors can be run to drive transactions over the VCs with the receiving VC validating expected activity.
The diagram below shows one of the PCIe co-simulation test benches in the OSVVM repository with the VCs in a back to back arrangement, where one VC is configured as upstream, and the other as an endpoint. They connect their PCIe signals to a passthru component, which simply connects the link across, but acts like a DUT, with split out PCIe signal ports for up and down links. The VCs are driven by test code in an upstream process and a downstream process using the PCIe helper procedures discussed earlier.

Self-consistency checking can capture any logic contradictions and is useful for bring up of new VCs, but it does not mean that the implementation of the protocol meets the specification or standard and more needs to be done.
Use of Third Party IP Simulation Models
A better way of validating the VC is to use a third-party implementation. This may be in the form of a separately implemented internally developed model or logic implementation, or the use of an external piece of IP.
For the PCIe VC, a test bench was created that utilises Altera’s PCIe interface implementation for the Cyclone V family of devices. In the diagram below is shown the setup of the demo of a practical use of the PCIe VC, where the VC drives the PCIe interface of Altera’s Cyclone V Hard IP for PCI Express.
In this example test bench, the PCIe VC is driven by an OSVVM manager process over the MIT signal. The VC connects to the Altera IP, wrapped in some SystemVerilog to abstract away details of the IP’s simulation model and parameters, and also to demonstrate using the VHDL PCIe VC with other HDL languages. The Altera IP’s output is a master Avalon bus, which is converted to AXI4Lite to drive an AX4Lite subordinate VC—here standing in for a DUT block or subsystem—and then passes data to the OSVVM subordinate process.
The Altera IP and PCIe VC model are configured in PIPE mode, with a single 9-bit lane pair. The demo’s test trains the link from idle to L0 power up, initialises DLL flow control for virtual channel 0, enumerates the configuration space and then reads and displays some of the main PCI registers, before executing some memory writes and reads to the AXI4Lite VC, which passes these onto a subordinate test process, validating their integrity. The diagram below summarises the setup for this test.

The test isn’t meant, of course, to verify Altera’s IP, but is an example for verifying new IP that needs to interface with the 3rd party PCIe interface. Of course, it could also be used to drive a new PCIe interface design as well.
Let’s have a quick look at some of the various outputs of this example test. Firstly this is a straight waveform showing part of the link training LTSSM states. Here is shown the link from Config.LinkAccept to L0 “Link Up” state. The signal LtssmState is a diagnostic output of the Altera IP.

At this point in the simulation the IP is ready for DLL flow control initialisation of virtual channel 0. I won’t show logs for this step as it’s not very informative. After flow control, the test goes through an enumeration. Some log fragments are shown below.

Firstly, it writes all ones to BAR0, which was configures for a 4K space and a read-back shows this and also indicates it a 32-bit BAR. BAR0 is then set for an address (actually to 0). The link is than enabled with a write to the PCI control register.
The test than goes through all the PCI registers to dump and decode their values. The beginning of this is shown and a couple of things highlighted that confirm the test correctly read the values configured in the IP, such as the vendor ID and the device class.
After enumeration it does some memory read and write transfers over PCIe and validates them. The log fragment below shows a memory write and read using the PCIe model’s displink formatted output and it is configured to display DLL and TL packets. We can see the memory write and memory read TLPs, but also the DLL acknowledges and a flow control update
Conclusions
The current PCIe VC provides the ability to generate all the transaction layer packets, but with the physical and data link layer traffic generated automatically—internally to the model. This simplifies the VC’s use, but plans are in the works to expose the DLL and Physical layer packet generation API that the C model already has, as VHDL PCIe procedures, to allow for more complex packet output and exception and failure mode generation.
The C model is in continuous development, but folding updates to OSVVM is a simple matter of generating new libraries and headers, as the model is used unmodified. Work is started on support for more LTSSM states (such as Hot Reset, Disabled, L2 etc.) and investigation on the required work to support GEN3 and GEN4.
So, to conclude, the PCIe VC, despite being co-simulation based, is basically used in the same way as any other OSVVM’s VC but also has some PCIe specific helper procedures to simplify its use for the more complex protocol.
The methods developed to generate the co-simulation PCIe VC can also be used to generate other complex protocol VCs if C models exist, or where this might be a simpler path than a VHDL VC implementation.
So, the OSVVM co-simulation features have allowed the interfacing of a C PCIe model to an OSVVM VHDL environment as a VC, with all of its inherent verification advantages, but OSVVM co-simulation is not limited to this, or even just writing tests in C or C++. With a well-defined and efficient bridge between the two domains, all of the C and C++ world becomes available to a complex but easy to use VHDL verification environment, We’ve looked at PCIe and how that can be used to develop PCIe and associated IP, but the possibilities are also much broader than this, and I’m sure some people reading this can think of other use cases. None-the less, the new PCIe VC adds to OSVVM’s ever growing support for different and more complex protocols integrated with its rich and easy to use verification functionality, and the methods used in its creation mapped to creating other co-simulation VCs for complex protocols.