VIP Central


NVMe VIP Architecture: Host Features

In my last post, I covered a basic NVMe VIP test-case including some basic setup, sending a command and receiving a completion. Here, we’ll look at a few more NVMe commands, touching on some of the features and capabilities of the VIP.

Here’s where you can learn more about Synopsys VC Verification IP for NVMe and for PCIe.

A (Reminder) View of the VIP

We overviewed this briefly last time. This time we’ll go into a bit more depth, so we will continue to refer to this diagram:


The NVMe VIP provides a set of feature to assist in testing. These include randomizations, feature snooping, simplified PRP and data buffer handling, memory fencing and built-in score-boarding. We’ll look at each of these in turn with another example.

Continuing our Test Case…

Following up on our “trivial test-case” from the last post (again, we are not showing some of the task arguments or checking errors), let’s take a look at a few more commands to get our NVMe test case rolling.

Just a reminder: the tasks that start with the word Script are NVMe commands. The others (that don’t start with Script), are a VIP status/control/configuration task.

// We will assume that the PCIe stack is setup and running
bit [63:0] base_addr = 32’h0001_0000;  // Ctlr BAR base addr
dword_t    num_q_entries, ctlr_id;

// Tell the host where the controller has its base address
AllocateControllerID(base_addr, ctlr_id, status);
num_q_entries = 2;

// Create the Admin Completion and Submission Queues
ScriptCreateAdminCplQ(ctlr_id, num_q_entries, status);
ScriptCreateAdminSubQ(ctlr_id, num_q_entries, status);

// Send an “Identify Controller” Command
data_buf_t #(dword_t) identify_buffer;      // identify data
identify_buffer = new(1024);
ScriptIdentify(ctlr_id, 0, identify_buffer, 0, status);

We ended our last sample with a call to Identify Controller. Now, continuing at that point, we read bytes 519:516 to get the number of valid namespace IDs. We hand that to the host VIP with the SetNumNamespaces() call. Note that we had to byte-swap the (little-endian) data returned in the Identify Controller buffer.

int num_ns, nsid, blk_size_pow2, blk_size_in_bytes;
bit [63:0] ns_size_in_blks;
feature_identifier_t feature_id;
nvme_feature_t set_feature;

// We’ll grab the Number of valid namespaces (NN) from the
// identify buffer. Note index converted from bytes to dword.
num_ns = ByteSwap32(identify_buffer[516 >> 2]); // bytes 519:516

// Tell the VIP how many active NSIDs the controller has
SetNumNamespaces(ctlr_id, num_ns, status);

Next we read the information for one of the namespaces (Namespace ID=1). Note that we “cheated” a bit here, as we should have walked all the valid namespaces. For the example we’ll just assume we have only NSID=1. Although the Identify calls don’t take a PRP list, their host memory buffer can have an offset. If this is desired, select the argument “use_offset=1”. The actual offset is randomized via the constraints MIN/MAX_PRP_DWORD_OFFSET_VAR.

// Now send an “Identify Namespace” command for nsid=1
nsid = 1;
use_offset = 1;            // Randomize buffer offset
ScriptIdentify(ctlr_id, nsid, identify_buffer,
               use_offset, status);

// Pull information from format[0]
blk_size_pow2 = ByteSwap32(identify_buffer.GetData(128 >> 2)));
blk_size_pow2 = (blk_size_pow2 >> 16) & 32’hff;  // dword[23:16]
blk_size_in_bytes = 1 << blk_size_pow2;          // Convert
ns_size_in_blks = ByteSwap64({identify_buffer.GetData(8 >> 2),
                              identify_buffer.GetData(12 >> 2)});

// Before we create queues, we need to configure the num queues
// on the controller.
feature_id = FEATURE_NUM_QUEUES;
set_feature = new(feature_id);

Once the Identify Namespace returns, we now have both the block size and the namespace size. We set the number of requested queues with Set Features. Via the VIP’s feature snooping, this will (transparently) set the VIP with the current number of supported submission and completion queues (for later checking and error injection support.)

The next steps format our namespace (using Format 0 in the Identify Namespace data structure). We then update the VIP view of the namespace information. The VIP needs this namespace information to keep a per-namespace scoreboard.

set_features.SetNumCplQ(2);     // Request number of sub &
set_features.SetNumSubQ(3);     // cpl queues
// Call Set Features command to set the queues on the ctlr
ScriptSetFeatures(ctlr_id, set_features, …, status);

// Note that Set Features Number of Queues command need not
// return the same amount of queues that were requested. We can
// check by examining set_features.GetNumCplQ() and
// GetNumSubQ(), but in this case we’ll just trust it…
// Format the Namespace
sec_erase = 0;        // Don’t use secure erase
pi_md_settings = 0;   // Don’t use prot info or metadata
format_number = 0;    // From Identify NS data structure
ScriptFormatNVM(ctlr_id, nsid, sec_erase, pi_md_settings,
                format_number, …, status);

// Tell the VIP about this NS
SetNamespaceInfo(ctlr_id, nsid, blk_size_in_bytes,
                 ns_size_in_blks, md_bytes_per_blk,
pi_md_settings, 0, status);

We next create a pair of I/O queues. Since the submission queue requires its companion completion queue to be passed along with it, we create the completion queue first. Note that queue creation routines take an argument contig. If contig is set, the queue will be placed in contiguous memory, otherwise a PRP list will be created for that queue. In addition to creating the actual queue, the VIP creates a fence around the queue to verify memory accesses to the queues. Attempts from the controller to (for example) read from a completion queue will be flagged as an invalid access attempt. The actual queue IDs are randomized (within both legal and user-configurable constraints).

// Create the I/O Queues
num_q_entries = 10;
contig = 1;           // Contiguous queue
ScriptCreateIOCplQ(ctlr_id, num_q_entries,
contig, …, cplq_id, …, status);
contig = 0;           // PRP-based queue
ScriptCreateIOSubQ(ctlr_id, num_q_entries,
contig, cplq_id …, subq_id, …, status);

Once we have I/O queues created, we can start doing I/O. Using the ScriptWrite() and ScriptRead() calls, we send data to the controller and immediately retrieve that same data back. The underlying data structure of the data (in host memory) is built automatically by the VIP. Note the use_offset argument (as with our queue creation tasks) to control whether we generate PRP and PRP List offsets (controlled by MIN/MAX_PRP_DWORD_OFFSET_VAR and MIN/MAX_PRP_LIST_DWORD_OFFSET respectively). Due to our built in score-boarding, we don’t have to compare the data read from that written, the VIP is checking data returned against its shadow copy that is tracking successful VIP writes to the controller.

// Do our I/O write then read with a random LBA/length
data_buf_t #(dword_t) wbuf, rbuf; // Write/Read Data buffers
num_blks = RandBetween(1, ns_size_in_blks);
lba = RandBetween(0, ns_size_in_blks – num_blks);
num_dwords = (blk_size_in_bytes / 4) * num_blks;
wbuf = new(num_dwords);
for (int idx = 0 ; idx < num_dwords ; idx++) // Fill the buffer
   wbuf.SetData(idx, { 16’hdada, idx[15:0] } );
ScriptWrite(ctlr_id, subq_id, lba, nsid, wbuf, num_blks,
            use_offset, …, status);

// We’ll read the same LBA since we know it’s been written
   ScriptRead(ctlr_id, subq_id, lba, nsid, rbuf, num_blks,
              use_offset, …, status);
// Do what you’d like with the rbuf (that’s the data we just read).

We’re Done!

Hopefully that’s gotten us through most of the basics. You should have a good feel for the operation of the VIP. Again, many of these tasks have more arguments allowing more control and error injection, but our goal is to get through without dealing with the more esoteric features. If you have the VIP handy, feel free to walk through the examples: they should look quite familiar.

In my next post, we will look into actually testing a controller, especially going into features like error injection.

As always, thanks for joining us. See you again soon.

Authored by Eric Peterson

Here’s where you can learn more about Synopsys VC Verification IP for NVMe and for PCIe.