Verification Martial Arts: A Verification Methodology Blog

Archive for April, 2010

Why do the SystemC guys use TLM-2.0?

Posted by John Aynsley on 29th April 2010

JohnAynsley John Aynsley, CTO, Doulos

Since this is the Verification Martial Arts blog, I have focused so far on features of VMM 1.2 itself. But some of you may be wondering why all the fuss about TLM-2.0 anyway? Why is TLM-2.0 used in the SystemC domain?

I guess I should first give a quick summary of how and why SystemC is used. That’s easy. SystemC is a C++ class library with an open-source implementation, and it is used as “glue” to stick together component models when building system-level simulations or software virtual platforms (explained below). SystemC has Verilog-like features such as modules, ports, processes, events, time, and concurrency, so it is conceivable that SystemC could be used in place of an HDL. Indeed, hardware synthesis from SystemC is a fast-growing area. However, the primary use case for SystemC today is to create wrappers for existing behavioral models, which could be plain C/C++, in order to bring them into a concurrent simulation environment.

A software virtual platform is a software model of a hardware platform used for application software development. Today, such platforms typically include multiple processor cores, on-chip busses, memories, and a range of digital and analog hardware peripherals. The virtual platform would typically include an instruction set simulator for each processor core, and transaction-level models for the various busses, memories and peripherals, many of which will be intellectual property (IP) reused from previous projects or bought in from an external supplier.

The SystemC TLM-2.0 standard is targeted at the integration of transaction-level component models around an on-chip communication mechanism, specifically a memory-mapped bus. When you gather component models from multiple sources you need them to play together, but at the transaction level, using SystemC alone is insufficient to ensure interoperability. There are just too many degrees of freedom when writing a SystemC communication wrapper to ensure that two models will talk to each other off-the-shelf. TLM-2.0 provides a standardized modeling interface between transaction-level models of components that communicate over a memory-mapped bus, such that any two TLM-2.0-compliant models can be made to talk to each other.

In order to fulfil its purpose, the primary focus of the SystemC TLM-2.0 standard is on speed and interoperability. Speed means being able to execute application software at as close to full speed as possible and TLM-2.0 sets very aggressive simulation performance goals in this respect. Interoperability means being able to integrate models from different sources with a minimum of engineering effort, and in the case of integrating models that use different bus protocols, to do so without sacrificing any simulation speed.

So finally back to VMM. It turns out that the specific features of TLM-2.0 used to achieve speed and interoperability do not exactly translate into the SystemVerilog verification environment, where the speed goals are less aggressive and there is not such a singular focus on memory-mapped bus modeling. But, as I described in a previous post on this blog, there are still significant benefits to be gained from using a standard transaction-level interface within VMM, both for its intrinsic benefits and in particular when it comes to interacting with virtual platforms that exploit the TLM-2.0 standard.

Posted in Interoperability, Optimization/Performance, SystemC/C/C++, Transaction Level Modeling (TLM), VMM 1.2 | 2 Comments »

Verification in the trenches: Chaining tests using VMM1.2

Posted by Ambar Sarkar on 26th April 2010

Dr. Ambar Sarkar, Chief Verification Technologist, Paradigm Works Inc.

Have you been frustrated by how long it takes to run the initial phases of a test before it gets to those that really matter? And it gets worse when you need to run a number of these tests. I often see projects where about 80% of the test execution time is used up in these initial phases.

While you do need to execute these initial phases, repeating them for each test does not contribute to any additional verification. Ideally, you would like to run the initial phases for a group of related tests just once, just to a point where you are ready to send real traffic or transactions. The phases that follow should be executed once per test, with the environment rolled back to an appropriate state before the next test starts executing.

The diagram below shows how you may want chain two testsTEST1 and TEST2, where you want to use the same configuration for both.


VMM1.2 supports this model of test execution for implicitly phased environments.

First the steps.

Step 1. Tag a test as concatenable and specify the rewind-to phase.?

If you are using implicit phasing of your environment, this is done simply by using the macro that announces the tests as concatenable, along with the phase to roll_back to.

class TEST1 extends vmm_test;
//Macro to indicate which phase to rollback to
`vmm_test_concatenate(configure_test)

endclass

class TEST2 extends vmm_test;
//Macro to indicate which phase to rollback to
`vmm_test_concatenate(configure_test)

endclass

Step 2. Invoke it.

This is done by simply specifying the command line parameters:

//Command line arguments to run the example
./simv +vmm_test=TEST1+TEST2

You also have the option of specifying all tests in a file or just chain all the tests.

So what are the gotchas?

The underlying VMM implementation (and you can read the gory details of how this is done using the three timelines: pre, top, and post in the VMM user guide) takes care of synchronizing all the test phases so that the tests can be effectively chained. However, the user still has to implement any application specific cleanup in between tests to restore the environment. This seems fair enough, and the cleanup phase is the perfect phase to implement that.

However, there is one limitation when considering chained tests. You cannot use vmm_test::set_config() in concatenated tests, which is the place to perform class-factory overrides for the environment components. This is not a limitation of the implementation however, but just how class factory overrides work. Note that you can still use class factory to override your traffic scenarios, just not the environment components.

First, a test can only run after the environment is created. Second, a class factory override for the environment can work only if it is called before the environment is created. This means that after the first test is executed, you cannot override the environment setting using class factory override, unless you are willing to rebuild the environment from scratch in between. Clearly, this rebuild per test run will negate any benefits of chaining. Since vmm_test::set_config is explicitly used to reconfigure environments using the class factory method, it is not allowed when tests are chained.

This article is the 5th in the Verification in the trenches series. Hope you found this article useful. If you would like to hear about any other related topic, please comment or drop me a line at ambar.sarkar@paradigm-works.com. Also, if you are starting out fresh, please check out the free VMM1.2 environment generator at http://resourceworks.paradigm-works.com/svftg/vmm .

Posted in Creating tests, VMM 1.2 | No Comments »

Combining TLM Ports with VMM Channels in VMM 1.2

Posted by John Aynsley on 23rd April 2010

JohnAynsley

John Aynsley, CTO, Doulos

In previous posts I described the TLM ports and exports introduced with VMM 1.2. Of course, the vmm_channel still remains one of the primary communication mechanisms in VMM. In this article I explore how TLM ports can be used with VMM channels.

TLM ports and exports support a style of communication between transactors in which a b_transport method made from a producer is implemented within a consumer, thereby allowing a transaction to be passed directly from producer to consumer without any intervening channel. On the other hand, a vmm_channel sits as a buffer between a producer and a consumer, with the producer putting transactions into the tail of the channel and the consumer getting transactions from the head of the channel. The channel deliberately isolates the producer from the consumer.

Each style of communication has its advantages. The b_transport style of communication is fast, direct, and the end-of-life of the transaction is handled independently from the contents and behavior of the transaction object. On the other hand, vmm_channel provides a lot more functionality and flexibility, including the ability to process transactions while they remain in the channel and to support a range of synchronization models between producer and consumer.
In VMM 1.2, support for TLM ports and exports is now built into the vmm_channel class. It is possible to bind a TLM port to a VMM channel such that the channel provides the implementation of b_transport. The goal is to get the best of both worlds: the clean semantics of a b_transport call in the producer, and the convenience of using the active slot in the vmm_channel in the consumer.
An example is shown below. First we need a transaction class and a corresponding channel type:

class my_tx extends vmm_data;   // User-defined transaction

typedef vmm_channel_typed #(my_tx) my_channel;

The producer sends transactions using a b_transport call, knowing that by the time the call returns, the transaction will have been completed:

class producer extends vmm_xactor;

vmm_tlm_b_transport_port #(producer, my_tx) m_port;

my_tx tx;

m_port.b_transport(tx, delay);

The consumer manipulates the transaction while leaving it in the active slot of a vmm_channel and executing the standard notifications:

class consumer extends vmm_xactor;

my_channel m_chan;

my_tx tx;

m_chan.activate(tx);   // Peek at the transaction in the channel
m_chan.start();        // Notification vmm_data::STARTED

m_chan.complete();     // Notification vmm_data::ENDED
m_chan.remove();       // Unblock the producer

The channel is instantiated and connected in the surrounding environment:

class tb_env extends vmm_group;

my_channel  m_tx_chan;
producer    m_producer;
consumer    m_consumer;

virtual function void build_ph;

m_producer = new( “m_producer”, this );
m_consumer = new( “m_consumer”, this );

m_tx_chan  = new( “my_channel”, “m_tx_chan” );
m_tx_chan.reconfigure(1);

endfunction

function void connect_ph();

vmm_connect #(.D(my_tx))::tlm_bind( m_tx_chan, m_producer.m_port,
vmm_tlm::TLM_BLOCKING_EXPORT );
m_consumer.m_chan = m_tx_chan;

There are two key points to note in the above. Firstly, the channel is reconfigured to have a full level of 1. This ensures that the blocking transport call does indeed block. If the full level is greater than 1, the first call to b_transport will return immediately before the transaction has completed, which would defeat the purpose.

Secondly, the transport port and the vmm_channel are bound together using the vmm_connect utility. This connect utility must be used when binding VMM TLM objects to channels, and can also used in order to bind TLM ports and exports of differing interface types (e.g. a blocking port to a non-blocking export). The third argument to tlm_bind indicates that the connection is being made from the port in the producer to a blocking export within the channel. I will discuss other uses for this method in later posts.

Posted in Communication, Interoperability, Reuse, Transaction Level Modeling (TLM), VMM 1.2 | No Comments »

Changing Functionality: The Factory Service #3

Posted by JL Gray on 21st April 2010

by Jason Sprott

Jason Sprott is CTO at Verilab.

In Factory Service #1 and in Factory Service #2 we discussed what a Factory is and what it can do for us. In this post we’ll take a look at the two general types of objects we might want to replace using the Factory. Typically objects in testbenches fall into two categories:

  • Dynamic: These are objects that get created and garbage collected spuriously during normal operation. An example might be a randomized transaction generated by a scenario, or even the scenario itself.
  • Structural: These objects get created once at the beginning and live throughout the simulation. An example might be vmm_xactor or vmm_group.

The location within the VMM Phasing scheme where the factory override performed is different between the two.  A factory override has to be done before any of the objects affected by the override are instantiated (using <class>::create_instance() ). Structural components in the testbench are typically instantiated earlier than dynamic ones.

In the case of overriding dynamic objects, we need to perform the override before the test starts running the simulation. A good place to perform dynamic object overrides is in the vmm_test::configure_test_ph()method. This is executed in the test phase (see “Constructing and Controlling Environments > Composing Implicitly Phased Environments/Sub-Environments” section of the VMM User Guide), before test execution is started by the vmm_test::start_of_sim_ph() method. So no dynamic objects are likely to have been created. The following example shows where we override a transaction in a testcase:

class my_dynamic_factory_test extends vmm_test;

virtual function void configure_test_ph();

my_trans::override_with_new(“@%*”,
custom_trans::this_type(),
log, `__FILE__, `__LINE__);
endfunction:configure_test_ph

endclass

Structural object overrides on the other hand potentially give us a bit of a problem, because these objects are instantiated in the pre-pest “build” phase. Performing the factory override in vmm_test::configure_test() would be too late, as it happens after the objects have already been instantiated. Instead structural object factory overrides must be performed in vmm_test::set_config(), which is executed before the Pre-Test timeline, as long as test concatenation is not being done. The following is an example where we override a VIP (encapsulated in a vmm_group) in the testcase:

class my_structural_factory_test extends vmm_test;

virtual function void set_config();

my_vip::override_with_new(“@%*”,
my_extended_vip::this_type(),
log, `__FILE__, `__LINE__);
endfunction:set_config

endclass

The figure below illustrates the phases where overrides should be implemented, with respect to the associated objects types:

image

Another thing to be aware of when using a Factory is test concatenation. In general, if you want to be able to concatenate a test, it’s not a good idea to change components in the environment using factory overrides. Reprogramming the environment, either structural or dynamic objects, will affect subsequent concatenated tests. If the changes can be undone, in the cleanup phase of the test for example, it may be OK, but changes to structural objects are difficult to undo. Any change to the environment may affect the validity of a test. For this reason the VMM will not execute vmm_test::set_config() if test concatenation is being performed.  That might not be enough though. For reuse, considering test concatenation is an important point test developers and users of the tests need to be aware of. Factory overrides incompatible with test concatenation will not necessarily cause a noticeable side affect, or failure, so misuse could go unnoticed. This is not a problem of the Factory; it’s just a consideration when using factory overrides with concatenation of tests.

Posted in Configuration, Reuse, VMM 1.2, VMM infrastructure | No Comments »

Changing Functionality: The Factory Service #2

Posted by JL Gray on 19th April 2010

by Jason Sprott

Jason Sprott is CTO at Verilab

In a previous post we took a look at what the VMM Factory is and what it could do for us. In this post, we take a look at the problem the Factory solves. In order to understand when we should use a Factory, we really need to understand a bit more about the problem it solves, so we can spot situations where a Factory might be appropriate. Let’s start by looking at how we might do things in SystemVerilog without using VMM Factories. Typically we might instantiate a class of type Foo like this:

Foo my_foo;

my_foo = new(…);

The problem with that is my_foo will always create an object of type Foo, no matter what.  We can’t change that. We might use this class Foo in lots of places around our testbench code. If we want to replace Foo with a new class that maybe adds more variables, or replaces a method, we could have a problem. We’d have to go around our testbench looking for everywhere Foo is used, to see if there was a way it could be replaced. Anywhere Foo is constructed using new(), it’s is highly likely to involve modifying the original code. This is very undesirable, especially if the code may be part of a tested IP library. In general, modifying the original source is a Bad Thing.

In VMM we can avoid this problem using a different way to create an instance of the class with the factory. It would look something like this:

Foo my foo;

my_foo = Foo::create_instance(…);

Although this looks similar to an instantiation with new(), something much more sophisticated  is at play. If we factory enable the class Foo, we never call new()to instantiate it again. We now call the Factory’s static method for generating instances of the object create_instance(). Since the method is static, it can be called without having an instance of Foo, therefore it can be used to create itself. What’s the point?

Now that we’ve encapsulated the task of creating an instance in a method, we can change what that method does. We can tell create_instance() to return something different. This might be a new type (with some modifications), derived from Foo, or the same type populated with different values for the variables. What’s more, we can do this easily anywhere Foo is used in the code. We can pick specific instances to be replaced, or replace multiple instances globally.

The two methods used to reprogram the factory are:
•    create_with_new() – tells the factory, when creating new instances, to return a brand new instance of the type specified.
•    create_with_copy() – tells the factory, when creating new instances,  to return a copy of instance specified.

This replacement can be done anywhere in the code, as long as it happens before the instances you want to replace are created. Let’s say a test needs to replace our type Foo, with a new class, FooWithAttitude, derived from Foo. Here’s what that might look like:

class my_factory_test extends vmm_test;

virtual function void configure_test_ph();
Foo::override_with_new(         // Foo’s factory is being overridden
“@%*”,                        // instances matching this pattern will be replaced
FooWithAttitude::this_type(), // they will be replaced with this type of class
log, `__FILE__, `__LINE__);   // some generic log and debug stuff
endfunction:configure_test_ph

endclass:my_factory_test

As can be seen, the Factory override uses regular expression pattern matching to specify which instances will be targeted. The syntax is expressive enough to indentify single instances, multiple instances (e.g. by hierarchy), or all instances (as in this example). More information on the pattern matching syntax can be found in the “Common Infrastructure and Services > Simple Match Patterns” section of the VMM User Guide.

This next example shows how the factory can be programmed to return a copy of the class we’ve modified with some values.

class test_read_back2back extends vmm_test;

virtual function void configure_test_ph();
FooBusTrans tr = new();        // create a template for the override copy
tr.address = ‘habcd_1234;      // special value we want to set up for override
tr.address.rand_mode(0);       // you might want to protect value during randomization
FooBusTrans::override_with_copy(
“@top:foobus0:*”,           // instances matching this pattern will be replaced
tr,                         // they will be replaced by a copy of this instance
log, `__FILE__, `__LINE__); // some generic log and debug stuff
endfunction

endclass

The above example  shows a Factory override in the configure test phase of the simulation timeline. Exactly when a Factory replacement is done is quite important. As previously mentioned, the replacement has to be done before any instances of the class have been created. This depends on the type of class being replaced. For example, dynamic objects (such as transactions), created many times during normal operation of the testbench, are likely to be created after the test has started. However, structural objects (such as instances of a VIP), are likely to be created once when the testbench is built. The details of where to put Factory overrides is covered in another post.

Posted in Configuration, Reuse, VMM 1.2, VMM infrastructure | No Comments »

Changing Functionality: The Factory Service #1

Posted by JL Gray on 16th April 2010

by Jason Sprott

Jason Sprott is CTO at Verilab.

Building a testbench using SystemVerilog, an object-oriented testbench language, does not automatically make the end solution reusable, or easily extensible. In SystemVerilog we can certainly implement the kind of object-oriented principles and design patterns that enable reuse, but this requires significant programming skills and experience in understanding the requirements of building substantial reusable software solutions. Also, when users inevitably need to change the original functionality, the mechanisms in place to allow those changes would have to be well documented and understood.

Two common changes we might need to make are:
•    Swap one type of class with another (which may replace or add new constraints, variables and methods).
•    Add new functionality to an existing method without replacing it

Fortunately the VMM provides some standard solutions for handling these types of changes. This post takes a look at how the Factory Service helps with the first of the two requirements, swapping classes.

There are many cases where we might want to swap one class for another in a testbench. For example, a typical requirement in constrained random testbenches is to replace one randomizable class with derived version, adding or modifying constraints. We might also have various derived versions of VIP, supporting slightly different versions of a protocol, or injecting different types of errors. When we first develop some VIP, or a testbench, it’s almost impossible to predict what people will want and need from our solution in the future. We can however provide users with a standard way to replace our implementation with something slightly different. This could be a derived version (extension with additions or modification), of what we originally implemented, or a copy with modified values.

Although this sounds quite trivial, testbenches that do not have this capability can be very hard to change without modifying the original source. Changing an original implementation to add new functionality is undesirable and sometimes impossible. Access to original source code is quite often restricted, or at least controlled. The original code may be well tested and proven, and changing the original source could affect other users of the code. Testbench components implementing the Factory are easily replaced at runtime without modifying the original source. Such modification can even be done on a per test basis if required.

However, we don’t get a Factory for free; there’s a bit of up-front thought required. We have to care enough about this capability to implement a Factory in the first place, obviously. This seems obvious, but in fact many times I come across a class that was in dire need of a factory that wasn’t there, the only reason was that the original developer didn’t think to put one in. I try not to judge too harshly, because we’ve all developed code that didn’t quite meet down-steam requirements at some point in time. Theres’s also a bit of effort (a small amount of additional coding) required. So why should we bother?

The VMM Factory Service (Factory) is a standard mechanism for changing functionality by replacing classes. VMM has always recommended the use of Factories, but The Factory Service was introduced in VMM 1.2 to ease implementation. The Factory provides the following API and utilities:
•    Short-hand macros to make implementing a Factory simple. The macros implement the Factory API methods for a given class.
•    API method to create instances of the class – replacing the use of the new()constructor method.
•    API method to change the instance generated by the Factory to a new derived type.
•    API method to change the instance generated by the Factory to a copy of a class of the same type
•    Specific and regular expression based selection of component factories to override

As an example, imagine a Bus VIP. The VIP has quite a lot going on inside. It has a master, a slave, some monitors, coverage and checking. All use a particular transaction type. So if we wanted to replace that transaction with a derived type, to maybe inject some errors, it would affect multiple components in the VIP. If any of the components using the transaction create an instance using new(), there’s a pretty good chance we cannot replace the transaction type without modifying that source. This may not be an option, or at the very least not desirable.

If we use the Factory, we can not only do the replacement, we can choose which instances of the VIP in a testbench we want to perform the replacement on. Maybe not all nodes on the bus have to inject errors. In our test, or new testbench environment, we might decide to say something like:
•    When creating new classes of type Foo, instead instantiate my new class FooWithAttitude. Using the Factory in VMM that might look like:
Foo::override_with_new(         // Foo’s factory is being overridden
“@%*”,                        // match all instances
FooWithAttitude::this_type(), // they will be replaced with this type of class
log, `__FILE__, `__LINE__);   // some generic log and debug stuff

Or, maybe we want to make sure some variables in a configuration object are replaced with something specific. We can make sure a copy of a class is instantiated, with some specific values set. We might decide to say something like:
•    When creating new classes of type Foo,  instead insatiate my copy of that class (in this case we have created our own instance, tr), with some values set and randomization turned off:

FooBusTrans::override_with_copy(
“@top:foobus0:*”,           // instances matching this pattern will be replaced
tr,                         // they will be replaced by a copy of this instance
log, `__FILE__, `__LINE__); // some generic log and debug stuff

To understand when we should use a Factory, we really need to understand a bit more about the problem it solves. We’ll discuss this in another post.

Posted in Configuration, Reuse, VMM 1.2, VMM infrastructure | No Comments »

The Benefits of using TLM within VMM 1.2

Posted by John Aynsley on 14th April 2010

JohnAynsley
John Aynsley, CTO, Doulos

In this post I am going to highlight the benefits of using the TLM-2.0 standard within VMM. In discussing this issue, I am motivated in part by the current activity within the Accellera VIP Technical Subcommittee on the development of a Universal Verification Methodology, or UVM, which is to take input from both VMM and OVM. As I write, the VIP TSC is wrestling with the issue of which TLM features should be included in UVM.

As I remarked in an earlier post, VMM has always used transaction-level communication, but based on the vmm_channel rather than on a wider industry standard. Now that multi-language simulation environments are so common, it makes sense to consider using formal standards for transaction-level communication across the language boundary. But is that the only reason that TLM-2.0 was introduced into VMM? No, not quite. Let us look at four benefits that TLM-2.0 can bring to VMM:

Benefit 1 – Direct communication. The SystemC TLM standard supports direct communication between producers and consumers using a single function call, rather than having communication mediated through a channel. Learning to use this new style of communication involves a shift in mindset for anyone familiar only with the vmm_channel-style of communication, but in many circumstances it is worth the effort. Put simply, removing the intermediate channel reduces the number of function calls, the number of events, and the number of processes, all of which can speed up simulation. More specifically, the behavior of the consumer can be executed directly within the function call made from the producer, thus avoiding the need to context-switch between multiple processes.

Benefit 2 – Simple completion model. The SystemC TLM-2.0 standard carefully defines the conditions for the completion of each transaction such that producers and consumers can tell when a transaction is complete without having to understand and track the protocol-specific details of the communication. In some situations this can help decouple the producer and consumer transactors behaviorally, making the overall verification environment more robust. (On the other hand, there are other situations where it is better to have an intermediate channel. VMM supports both approaches: direct TLM function calls and mediated communication.)

Benefit 3 – Simple type-safe connections. Making TLM connections has been likened to plumbing. As long as the pipes and connectors are the same size, you simply make the proper joints and water will flow. In the case of SystemC and SystemVerilog, if you get it wrong you get a compile-time error, which is a good thing. But if you make connections of the correct type, there is a good chance that the transaction-level communication is going to work as expected. This helps make transactors more reusable.

Benefit 4 – Better interoperability. There are now solutions for passing transactions across the language boundary using the TLM-2.0 standard. For example, there is the VCS TLI (Transaction Level Interface), which allows transactions to be passed in either direction between SystemVerilog and SystemC. vmm_channel can be a great solution for internal use within a VMM environment, but does not help with communication with other languages.

So, although vmm_channel works fine in many situations, there are benefits to be gained from using the new TLM-2.0-inspired features introduced with VMM 1.2 where appropriate.

Posted in Transaction Level Modeling (TLM) | 1 Comment »

Bruce Kenner: Programmer

Posted by Andrew Piziali on 8th April 2010

by Andrew Piziali and Gary Stringham

Once upon a time in a cubicle far, far away there was a brilliant software engineer named Bruce Kenner. He had designed an elegant compiler for a processor with an exposed pipeline, requiring compiler-scheduled instructions for a new instruction set architecture. No, this was not the pedestrian VLIW machine you may be familiar with but a machine having a programmer specified number of delay slots following each branch instruction. In order to manage resource hazards, Bruce had furnished the compiler with an oracle that had a full pipeline model of the processor. The oracle advised the scheduler about resources and their availability.Orca Compiler Flow

Despite Bruce’s obvious talent, his business card simply read “Bruce Kenner: Programmer.”Since Bruce was a software engineer that designed and implemented exceedingly complex machinery, we might ask what is the role of the software engineer in the verification process. How did Bruce verify the oracle, scheduler and compiler he was designing? What role in general does the software engineer play in the verification of a modern system? To address these questions and others, I asked embedded software developer Gary Stringham to join me in this discussion. Gary is the founder and president of Gary Stringham & Associates, LLC, specializing in embedded systems development.

The typical SoC today contains a dozen or more processors of various flavors: general purpose, DSP, graphics, audio, encryption, etc. These processors are distinguished from their digital hardware brethren in that they are generally pre-verified cores that faithfully perform whatever tasks the programmer specifies. Hence, the DUV (design under verification) becomes the code written by the programmer rather than the hardware on which it executes. We must demonstrate this code implementation preserves the original intent of the architect. To do so requires the contribution of the software engineer to the verification process. We illustrate the cost of bringing the software engineer into the design cycle too late with this story from Gary’s experience. He writes:

I was having trouble getting my device driver to work with a block in an SoC for a printer so I went to the hardware engineer for help. We studied what my device driver was doing and it seemed fine. Then, we compared it to what the corresponding verification test was doing and both were writing the same values to the same registers. However, when we examined the device driver more closely, we discovered that it was programming the registers in a different order than the test case so that it exposed a problem.

I had my reasons for writing those registers in the way that I did, having to do with the overall architecture of the software. This was based upon the order that the device driver received information during the printing process. The hardware engineer had no rhyme nor reason to write to the registers in the order he did. He did not—nor was he expected to—know the nuances of the software architecture that required the driver to do it the way it did. But, with the order he happened to write those registers in his verification test, he obscured a defect. At this point in the design cycle the SoC was already in silicon so I figured out a work-around in my device driver.

Now, in this particular case, there was no reason to believe there would be order sensitivity of register writes; the driver was setting up some configuration registers before launching the task. But, there was a sensitivity. If I, the software engineer, had been involved months earlier with the design of the verification test suite, I might have suggested to write the registers in the order I needed the device driver to do it and it would have exposed the order-dependent error.

With this in mind, let’s examine the software engineering roles we recommend. The initial role of the software engineer should be collaborating with the system architect to ensure that required hardware resources are available for the algorithms delegated to the software.[1] Parametric specifications and version dependencies should also be examined and verified. Often the software engineer is the only one intimately familiar with the software limitations of legacy blocks reused in the design. Hence, they need to examine these in light of the requirements of the current design. What space/time/performance/power trade-offs become apparent as various hardware components are considered?

Next, the software engineer should review early specifications with an eye toward implementation challenges. What requirements and features lead to code complexities that might jeopardize the implementation or verification schedule? Are obscure use cases addressed? What ambiguities exist in the specification that could lead to different interpretations by the design, verification and software engineers? What hardware resources do software engineers need to debug problems, such as debug hooks and ports?

At the same time, the software engineer should be contributing to the verification planning process[2] during either top-down or bottom-up specification analysis. During the more common top-down analysis, where each designer explains their understanding of the features and their interactions, the software engineer will be asking questions that aid the extraction of design features requiring software implementation. Likewise, the software engineer will be answering questions posed by verification engineers and designers about their understanding of the specification. As the verification plan comes together, the software engineer should be a periodic reviewer.

Finally, during hardware/software integration, the software engineer plays an integral role, having a first-hand understanding (usually!) of their code and its intended behavior. Both the designer and software engineer will reference the specification and verification plan to disambiguate results. Each will learn from the other as they observe their respective components contribute to system features.

Summarizing, the software engineer must be involved early and throughout the design cycle to ensure design intent is preserved, yet properly partitioned between hardware and software, in the final implementation. Make sure software engineering is represented during the specification and verification planning processes, all the way through final system integration to maximize your potential for success. Or, as one of my earliest finger(1) .plan files (you remember those, right?) used to read: “Fully functional first pass silicon.”

——————-
[1] Hardware/Firmware Interface Design: Best Practices for Improving Embedded Systems Development, Stringham, Elsevier, 2010

[2] ESL Design and Verification, Bailey, Martin and Piziali, Elsevier, 2007 Read the rest of this entry »

Posted in Interoperability, Reuse, Verification Planning & Management | No Comments »

Transactor generator with VMM technology for efficient usage of CPU resources.

Posted by Oded Edelstein on 5th April 2010

OdedEdelsteinPic

Oded Edelstein – Founder and CEO of SolidVer

Background:

Many network designs require an efficient transactor generator
to cover DUT functionality.

In a random test we would like to cover all scenarios,
but also to use the CPU mostly on cases which push the design to its edge.

In this VMM example, I will demonstrate 3 cases, and solutions for a better
usage of CPU resources.

Cases:

Case A – In network designs packet size can vary between 40 Bytes, for small packets
and 10KBytes, for large MTU packets.
A test which is based on the number of packets(Transactions), might be very short
or very long depends on the total size of packets. The long tests scenarios can
be covered in a separate random test.

Case B – Some network designs forward packets to different channels(queues) with
different levels of bandwidth support. Random generation of channel
number does not cover many cases (e.g. filling a certain queue with packets),
since the probability that the same channel will be chosen one after the other,
in a system with many channels is very low.

Case C – In some projects, the transactor generates many packets to all queues
while some queues are randomly configured to a low bandwidth. This causes the
test to be very long, until all packets are being forwarded.
At the beginning of the test, the DUT is very busy – almost every cycle, it gets data.
But after the high bandwidth queues got all packets, the low bandwidth queues
continue slowly to get packets, until all packets have been forwarded.
Now, most of the DUT queues are empty and the DUT is using only a small
portion of its performance ability. That has no added value for coverage.

Solutions:

The following code example, shows a simple solution for the above cases.
The solution is based on the following techniques:

1. Test length is defined based on sum of packets size, instead of the number of
packets(transactions).

2. Add the random test a basic case where a number of packets are send  to the same channel
one after the other.
Low probability cases need to be identified and added as case inside
the random test (cases inside directed tests are not good enough for a good coverage).

3. No packets are generated in advance for all queues.
Packets are randomly generated and driven, on the fly, only to available queues.
From my experience, random tests can generate all parameters, and a good coverage can be achieved.
At the same time, a much better coverage can be achieved if idle periods, which consume
CPU during the test are identified and handle correctly.

Code Example:

//——————————————————————————————————
//
//  packet.sv
//
//——————————————————————————————————
class packet extends vmm_data;

rand byte payload[];
rand int packet_size;
rand int channel_num;

static int cnt;

constraint c_payload_size { payload.size == packet_size; }

constraint c_pkt_size_dist { packet_size  dist { 40:= 20,
[41:200]:= 50,
[200:2000]:= 5,
[2000:10000]:= 1,
10000 := 1};}

`vmm_data_member_begin(packet)
`vmm_data_member_scalar_array(payload, DO_ALL)
`vmm_data_member_scalar(packet_size, DO_ALL)
`vmm_data_member_scalar(channel_numm, DO_ALL)
`vmm_data_member_end(packet)

endclass : packet

//—————————————————————————–
// VMM Macros – Channel and Atomic Generator
//—————————————————————————–
`vmm_channel(packet)
`vmm_atomic_gen(packet, “Packet atomic generator”)

//——————————————————————————————————
//  End file packet.sv
//——————————————————————————————————

//——————————————————————————————————
//
//  bfm_master.sv
//
//————————————————————————————————————

//
// SUM_PACKETS_SIZE_IN_TEST : The sum of packets size in bytes that the BFM will drive the DUT.
// We used sum of packets size, to define test length, instead of number of packets, since packet
// size distribution could randomly vary between 40 bytes (small packets) – 10KByes (Large MTU packets).
// This could cause for some seeds to be very long, with no significant added value for coverage.
// These long scenarios were tested in a separate random test.
//
`define SUM_PACKETS_SIZE_IN_TEST 10000000 // 10MB

//
// In this example The DUT gets a packet with a channel number.
// The DUT holds a sperate FIFO for every channel.
//
`define MAX_NUMBER_OF_CAHNNELS   16
class bfm_master extends vmm_xactor;

vmm_log log;

// Packet Transaction channels
//
packet_channel    packet_chan;

//
// The DUT will send the BFM back pressure signal, separately for every channel,
// when the channel FIFO, inside the DUT is full.
// avail_channel_list – Holds a bit for every channel, The BFM can drive packets only on channels
//                      which are not back pressured.
//
bit [(`MAX_NUMBER_OF_CAHNNELS-1):0] avail_channel_list;
int done = 0;

extern function new (string instance,
integer stream_id,
packet_channel packet_chan);
extern function int generate_stream_size();
extern function int generate_avail_channel();

extern virtual task main();
extern virtual task drive_packet(packet packet_trans);

endclass: bfm_master

function bfm_master::new(string instance,
integer stream_id,
packet_channel packet_chan);

super.new(“BFM MASTER”, instance, stream_id);
log = new(“BFM MASTER”, “BFM MASTER”);
if (packet_chan == null) packet_chan = new(“BFM MASTER INPUT CHANNEL”, instance);
this.packet_chan = packet_chan;

endfunction: new

//—————————————————————————–
// main() – Main task for driving packets.
//—————————————————————————–

task bfm_master::drive_packet(packet packet_trans);
// drive the packet …
endtask: drive_packet

function int bfm_master::generate_avail_channel();
….
endfunction

function int bfm_master::generate_stream_size();
….
endfunction
task bfm_master::main();

//
// The sum of packets that the BFM will drive the DUT. when this value is
// above SUM_PACKETS_SIZE_IN_TEST the BFM will stop driving packets
//
int sum_packet_data_sent = 0;

//
// The channel on which the DUT will drive packets. This channel should not be back pressured
// while driving packets
//
int channel_num;

//
// packet_stream_size
// The number of packets that will be sent one after the other to the same channel.
// The idea behind this variable is to get a good coverage for cases where number
// of packets are sent to the same channel one after the other, to quickly fill the FIFO.
// Otherwise the BFM will generate statistically every time, a different
// channel. The probability that the same channel will be generated one after the other
// is very low. e.g. Statistically the probability that the same channel will be
// generated 5, 10, or 20 times, one after the other, is 16 power 5, 16 power 10 or
// 16 power 20, which is a very low probability.
//

int packet_stream_size;

// Counter for the number of packets that were driven in the test.
//
int packet_cnt = 0;
int i;
packet packet_trans;
super.main();
while(sum_packet_data_sent < `SUM_PACKETS_SIZE_IN_TEST) begin
// gen random channel from avail_channel_list;
channel_num = generate_avail_channel();
packet_stream_size =  generate_stream_size();

for(i = 0; i < packet_stream_size; i++ ) begin
this.wait_if_stopped_or_empty(this.packet_chan);
if(avail_channel_list[channel_num] == 1) begin
packet_chan.get(packet_trans);
packet_trans.channel_num = channel_num;
drive_packet(packet_trans);
sum_packet_data_sent = sum_packet_data_sent + packet_trans.packet_size;
packet_cnt++;
`vmm_note(log, $psprintf(“drive packet = %0d  size = %0d  channel = %0d  stream index = %0d  sum = %0d “,
packet_cnt, packet_trans.packet_size, channel_num, i, sum_packet_data_sent));
end
else begin
break;
end
end // for loop
end// end while loop
done = 1;

endtask: main

//——————————————————————————————————
//
//  End file bfm_master.sv
//
//——————————————————————————————————

Posted in Automation, Modeling, Optimization/Performance | 1 Comment »

VMM Smart Log in DVT

Posted by Cristian Amitroaie on 1st April 2010

In this blog, I’d like to cover two cool features that are coming up with the DVT Integrated Development Environment (IDE). The 1st one is the possibility to color VMM log occurrences, the 2nd one a possibility to hyperlink these occurrences to the VMM source code.

Here is a snapshot showing VMM logs being colored and hyperlinked to source code:

vlogdt-vmm-smart-log

Not only providing hyperlinks from VMM log to source code, DVT also provides hyperlinks to VCS compilation/simulation errors and warnings. To enable these features, all you have to do is to define the right parameters in DVT Run Configuration window. For instance, you can specify the compilation/simulation commands and turn VMM log filtering from the corresponding tab.

APB_Run_Configuration

This is just one among many other VMM features that are available in DVT.

For more details see www.dvteclipse.com.

Posted in Debug, Tools & 3rd Party interfaces, VMM | No Comments »