Monday, August 31, 2015

A state of the art SystemVerilog simulation environment consists of two separate worlds. There is the static world, where interface and modules (including the DUT) exist. The other one, the dynamic world, consists of object instances that make up the testbench. The most widespread way to connect these two together is using the virtual interface construct, which is just a pompous way of describing a "reference" to an interface. I will assume that everybody is well acquainted with this concept.

A very big problem that exists with this approach is that it creates a very tight coupling between the interfaces and the classes that are supposed to interact with them. Because of this, for example, it becomes very cumbersome to handle parameterized interfaces, since any interface parameters need to be propagated to the coupled classes too.

There is an alternative to using virtual interfaces, as this paper shows. It builds on an older paper which first championed using abstract classes to define how to operate on signals. An interface would declare a concrete class that implements the real signal accesses. The paper was written in a time before OVM or UVM, when bus functional models (BFMs) were all the rage, hence it proposes offloading the logic to drive signals into these classes. Other authors have recognized the potential of this approach and built on the BFM idea, showing how to incorporate it into OVM and, more recently, UVM environments. While I do like the idea of dumping virtual interfaces and using the abstract class based approach, I disagree with giving these classes too much responsibility. Read on and you'll understand why.

The introduction might seem rather dry, but a concrete example will illustrate things better. Let's look at a simple APB master driver. The interface it's supposed to drive would look like this:

The clocking block helps enforce synchronicity to the clock and signal access control. The PREADY and PRDATA signals are only tagged as inputs, since the master is only supposed to sample the values that get driven by the slave. Normally, a master contains an array of PSELx signals, but for this example I want to keep things simple.

The first thing an APB UVC needs is a transaction class to model a transfer:

And there we have it: one APB master driver that can handle the vanilla protocol. Now let's say that we want to slightly modify this protocol to use only one bidirectional data bus. Instead PWDATA and PRDATA we'll have a PDATA signal:

I've had to make PDATA an inout wire port, because when I defined it as a simple wire inside the interface my simulator complained later on when I tried to drive it from a testbench instantiating the interface. Since it isn't possible to drive wires directly from procedural statements (i.e. from classes), we'll need an intermediary signal to drive, hence the PDATA_drive signal. The input clockvar will sample the wire, to give us the resolved value of the bus. The output clockvar will drive the intermediary signal synchronously. Normally it should have been possible to also call the output clockvarPDATA, but the simulator got confused, even though the syntax is legal as per the LRM.

This new protocol is almost exactly the same as the standard one, with the minor difference that we need to drive PDATA as 'z (high impedance) during idle phases. Only when driving a write transfer would we drive real data onto it. This suggests that we should be able to easily tweak the vanilla master driver to achieve this behavior. Unfortunately, the type of the vif field, virtual vgm_apb_master_interface, makes it impossible to extend this driver to use our new interface. This is the tight coupling I was talking about at the beginning of this post.

The only thing we could reuse from the vanilla APB UVC is the transfer class. The driver we'd have to implement again by copying and pasting the original code and patching it. Here's a summary of the changes we'd need to make:

We'd need to change very little. First, the vif field should have the new type. Second, we'd need to float PDATA after reset and after a transfer. Third we'd need to update the drive_access_phase(...) task to use the PDATAclockvars instead of PWDATA and PRDATA. Aside from this, everything else would stay the same. Clipboard-based inheritance at its finest!

In the ideal case, we should have been able to leverage the vanilla driver implementation and use OOP techniques to make the very few required tweaks, for example in a subclass. That's the whole reason verification evolved to look more and more like software programming, to allow us, among others, to easily deal with such situations.

The root of all evil is that the driver had to know the type of the (virtual) interface it was driving. The abstract class approach presented in the papers from above can do away with such coupling (you did browse them at least, didn't you?). Instead of the driver getting a virtual interface, it will delegate the task of actually driving the signals to a proxy object. Such a proxy class will contain accessor functions for each of the signals:

Notice that this class is defined as virtual, meaning that it's "incomplete" (the method definitions are missing) and as such can't be instantiated. A concrete class which actually performs signal accesses has to be defined inside the interface we want to drive:

I avoided talking about the topics of reset and clock. For synchronization, the proxy should also provide tasks to wait for a clock edge and for the beginning and end of reset. I'll skip the implementations of these since they're pretty trivial (but you can find them in the full example on SourceForge). You might have also noticed that our get/set_*(...) functions all reference clocking block variables. This means we won't be able to asynchronously drive signals to their idle values when reset is asserted. Providing asynchronous versions of the methods seem like overkill, so what we could do is define a reset() function inside the proxy to take care of this:

Now we've gotten rid of any references to any virtual interface inside our driver by adding another layer of indirection between it and the actual interface it's supposed to be driving. Was it all worth the extra complexity? Let's see...

We were promised that if we decouple the driver from the interface all sorts of good things will happen. Let's go back to the tristate APB variant. First, we'll need to declare the concrete interface proxy that can talk to the vgm_apb_tri_master_interface:

Aside from an extra line in the reset() function and telling the data accessors to operate on PDATA instead of PWDATA/PRDATA, the method definitions stay the same. This means that whenever the driver will try to read or write data it will also indirectly be working with PDATA. Even though the set/get_*_data(...) functions now do different things, the driver doesn't care and nor does it need to. The code we already have will work directly with the new interface. Now that's polymorphism right there! We do need to make one small alteration to float PDATA after a transfer has taken place. This is easily done in a subclass:

And that's it. No duplication of driver functionality. Just one extra statement. The doubled up code we have in both concrete proxy classes to actually access the signals is a necessary evil. That code is pretty much boilerplate and as long as the signals don't change, neither should it.

Now if we would have gone for the BFM approach mentioned in the papers (I hope you read them by now since I kept going back to them), where the proxy classes would have known more about the protocol, such as how to perform a transfer, we would have needed to re-implement these functions in both concrete classes. This is exactly the situation we wanted to avoid if you remember the initial implementations of the two drivers. Granted, we could have built such high level functions out of calls to get_* and set_*, but then again this is exactly what we're doing inside the driver.

After seeing how to implement interface polymorphism, let's tackle extendability. Let's now imagine that we need to implement another slight modification to the APB protocol that supports narrow accesses. AHB connoisseurs should be familiar with this concept. We'll do this by adding another signal, PSIZE, that defines how wide a transfer is, BYTE, HALFWORD or WORD. This new interface will look like this:

We'll need to extend our transfer class to add this extra field. The width of the transfer will impose constraints on the address. At the same time, when transferring bytes or half words, it doesn't make sense to randomize the entire data field:

Now that we have all our building blocks in place, we can start writing the new master driver. As before, we can extend the vanilla master driver and add the new behavior. Since the new driver needs access to the get/set_size(...) functions inside the proxy, the if_proxy field of the base driver won't suffice, since it is of type vgm_apb::master_interface_proxy. We'll need a new field of the extended proxy type:

We'll need to make sure that when calling set_interface_proxy(...), the caller passes us a proxy object of the new type, hence the cast. An even better approach would have been to re-declare the function's argument as having the type vgm_apb_ext::master_interface_proxy, which is a subclass of the original type so it can be substituted for it. This way we wouldn't need the cast and we'd get extra compile time safety. This is possible in other programming languages, but isn't allowed by the SystemVerilog LRM. As a compromise we can define a new set_ext_interface_proxy(...).

Now it's time to implement the driving logic. First off we need to drive PSIZE to the value contained in the transfer we get from the sequencer. We'll need to cast the transfer to the new type so we can access the size field. After we have that, driving PSIZE is just one call to set_size(...) away:

For the access phase, things get trickier when handing the data. Those of you who've worked with the AHB protocol know that the data busses are split into byte lanes. Depending on the size of the transfer and on its address, different lanes are active:

Since our bus is 32 bit wide, we can drive four bytes at the same time. This means we need two address bits to differentiate between which byte or half word we are driving. For example, for a byte access to address 0x10 the data will be placed in the BYTE0 lane, for a byte access to address 0x5a the data will be placed in the BYTE2 lane and so on. The concept is similar for half words, while words always occupy the full bus.

In our transfer class, the data field only contains the payload that we want to send. The driver must place this payload onto the appropriate lane(s). Our standard APB implementation didn't do anything like this; it directly drove PWDATA with the contents of the data field. If we perform this alignment on it before calling the base drive_access_phase(...) task we'll achieve our desired effect:

I didn't like the idea of dirtying the data field of the transfer we got from the sequencer so I implemented this alignment and dispatch on a copy. At the same, when coming back from a read transfer, the transfer will get updated with value extracted from PRDATA.

There's not more to it than that. If you want to go through the code yourself, you can find the full examples (in both the virtual interface and abstract class flavors) on SourceForge.

We've seen that with just a few slight tweaks we were able to add a new signal to the protocol. This is something that wouldn't have been possible to do as cleanly by using virtual interfaces. When I say "cleanly" I mean that we've kept strictly separate interfaces for each of the protocol variants. What we wouldn't like to see is some monstrous monolithic interface that contains all signals for all possible variants (i.e. PWDATA/PRDATA, PDATA and PSIZE in our case) and then detailed instructions on how to connect it when wanting to use a specific flavor of the protocol (e.g. for vanilla APB float PDATA and drive PSIZE to 'h3). This is unfortunately the way that things end up getting implemented when trying to support multiple variations of the same protocol.

The downside to this approach is that it requires more code, at least at a first glance. We need to maintain multiple interfaces and there is a lot of duplication when defining the concrete interface proxies for each. This should be a one-off thing, though, and it is effort that's well invested, since it should significantly improve usability. We intend to use our UVCs much longer than it takes to develop them, so any boost in making more user friendly will pay off in the long run.

Drastically reducing the coupling between the static interface and dynamic driver also allows for new protocol variants to be implemented much faster and with much more synergy, leading to better separation of concerns and much cleaner code that is easier to maintain. If you have to maintain multiple flavours of a certain protocol, then I definitely recommend you give them a try!

About

I am a Verification Engineer at Infineon Technologies, where I get the chance to work with both e and SystemVerilog.
I started the Verification Gentleman blog to store solutions to small (and big) problems I've faced in my day to day work. I want to share them with the community in the hope that they may be useful to someone else.