When working with UVM, the phrase "you gotta use the factory!" gets drilled into our heads constantly. This is because in object oriented programming (OOP) extendability doesn't come for free. We need to plan for it by structuring our code in certain ways. Why the factory is important, though, and how it helps us achieve that goal may be kind of difficult for a beginner to understand (I know I was pretty confused back in the day). The UVM user guide doesn't explain things all that well, either. Note that I haven't cracked it open in a while, so apologies if everything I'm going to talk about in this post is already presented crystal clearly over there.
This is a companion discussion topic for the original entry at https://verificationgentleman.netlify.app/2015/07/15/rambling-about-uvm-factory-overrides-per-type.html
I don’t know about AOP and e but whatever you have given example from that it’s seems that e is simple to write. Here i want to add that in UVM for this case/feature (Back to Back read without delay etc…) no need to use factory. We can simple write a test case with full random and then extend this test case and turn on/off randomization and constrain as per required feature, so no need to write extra sequences.
W.r.t. writing a test with full random, this is exactly the thing we did in the beginning (the test that starts the main sequence). I don’t really get your point with extending the test case and turning randomization on/off. Could you please elaborate?
Say for example we want to test read without delay (back to back reads). For this one first of all extend test2 from test1 and remove all code from the end_of_elaboration_phase function. (As per my understanding some time “default_sequence” is overhead, so i used to start sequence with “start” function). Now turn off randomization (using built in SV function) of variables “direction” and “delay” and assign value. (In our case direction = READ and delay = NONE). So now when we start sequence (same one as in test1, no need specific sequence) using start function in test2, test2 have direction = READ and delay = NONE and size = RANDOM_VALUE.
That might work for that case, if you create the item first and make it a public sequence field (you’ll notice now it’s a temporary variable inside the body() task).
But what if you have a more complicated constraint? Something like “when doing a read, select with 50% probability between NONE and SHORT delays”. There you can’t go without randomization.
Also, regarding ‘default_sequence’, I’m starting to favor it more and more, because this way I can put the ‘raise/drop_objection’ stuff inside ‘pre/post_body()’ and it will get handled automatically. No need to raise/drop from the test.
I observed and experienced that “start” method giving more convenience then “default_sequence”. However i have seen people who test all things using “default_sequence”. I always code “‘raise/drop_objection’” in “pre/post_body()” even in case of “start” method.
I was always under the impression that pre/post_body() isn’t called when using start, but I see now by looking at the UVM source code that I was wrong. This means ‘start(…)’ is the much better choice, because there are no string based paths to fiddle around with. Everything is compile time safe. You have converted me!
Good reading, I learned a thing or too. thanks
In general - I try and look at the factory as a “solver” and not as a architectural pattern, I.e. I will use it only in the test layer, or at the integration layer - but would rather the functionality be later be integrated into the main code.
In the above example - I would not keep the functionality in the test, but will later push it into the sequence library.
This will insure that my sequence lib. will be more interesting and I will get more coverage randomly.
In the same way, if I get an VIP and find an issue in the integration, I would use factory override to solve it. But will try to push the new behavior to be native handled in the VIP for the next “drop” (if possible). This will insure that my desired behavior will be checked and covered by the VIP. In addition, Factory require me to be very sensitive to the naming, arch. etc. of the VIP - and this mean that my env. will be fragile (E.g.: in case the VIP will do internal changes). In other words: Factory breaks “OOP” (In a way, so does AOP)
Care to expand on what you mean by pushing functionality from the tests to the sequence library? If you mean that you would move the sequence items (with the added constraints) to the sequence library, then stay tuned for the next post. I still don’t see a way of getting rid of factory use, however, if you want to achieve the same degree of “direction” of randomization in certain tests.
By “pushing” I mean: adding control knobs to the basic sequence that will randomly be selected to achieve the more directed behavior. (It does not have to be on the basic sequence, it can be on one of the derived sequences. A good sequences lib.,IMHO, has layers. Starting from very basic and “fully random” and up-to specific (virtual) flow/Arch level scenarios)
I do not intend to get rid of factory, and in the test layer, it is a very common and useful way to get the desired behavior with minimum change to the env. However - in many cases - the scenario developed in the test can be used randomly with other scenarios, so it will be good if it will be part of the randomly achieved sequences in a higher level random test.
This means that in some cases - I will perhaps write the sequence as part of the test ( and use factory to apply it) - but I will later refactor the test and “push” the sequence into the seq. lib.
In general - I tend to add a high level test that randomly select sequences and cross then with other sequences from the lib.
this ensure that any legal random scenario can happen in concurrency with other legal scenario. ( and from time to time I will hit it in large scale regressions)
I will anyway stay tuned to your next post
I just realized I already wrote about the topic of how to handle many constraints in an intelligent way: http://blog.verificationgentleman.com/2015/03/mixing-in-constraints.html
I have so many posts it’s hard to keep track of them all
W.r.t. layered sequence libs, I love them too. I intend to write about that topic too. In any case, this post wasn’t about writing new sequences or layering thereof. It was about fiddling with the behavior of one existing sequence (generate 10 to 20 transfers). Adding control knobs to the sequence to select any of these scenarios (fast reads, slow writes, byte, etc.) randomly would be possible, but kind of bad for maintenance. Also, what if you write a second sequence (generate 10 to 20 writes/read pairs)? Do you add the same control knobs to that one too? That would mean duplication.
I have a question. In case you have instantiated an object inside the overridden driver and you want to assign this handle to another component say monitor, how would you do that? During compilation, only the base type driver is created in the agent.
class base_driver extends uvm_driver;
class overridden_driver extends base_driver;
class monitor extends uvm_monitor;
I want to do something like:
monitor_h.mon_obj_h = overriden_driver_h.drv_obj_h;
Assume, the necessary create or new is done.
You have to understand that the type an object is defined to isn’t necessarily the type it has. During compilation, no objects are created, that happens during run time. At compile time, the compiler knows that the driver is of type ‘base_driver’. If we’re actually creating an ‘overridden_driver’ instead of a ‘base_driver’ we need to let the compiler know so we can use the new fields. We do this using $cast(…).