Handles, objects and constructors
Tell a handle from the object it points to, give every object a valid starting state, and create one object per transaction.
- Lesson
- 4.1
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
A generator builds one transaction object before its loop. Inside the loop it sets the ID and address, pushes the handle onto the scoreboard's expected queue and sends it to the driver. After four iterations every entry in the queue shows ID 3, because all four entries point at the same object. The first response, for ID 0, is reported as a mismatch against an expectation that no longer exists.
The first entry changes in every iteration, because it is the same object the loop keeps editing.
A class describes objects
A class is a type that bundles data with the code that works on it. Its variables are called properties, and its functions and tasks are called methods.
A transaction class for the read scoreboard might hold an ID, an address and the expected data, plus methods to print and compare them. Each transaction in the test is then one object of that class.
Declaring a class creates no objects. You create each object while the test runs, as many as the traffic needs.
A handle names an object
A variable of class type is a handle: a reference to an object, not the object itself. axi_txn tr; declares a handle whose starting value is null, which means it refers to nothing.
tr = new(5, 'h40); allocates an object, runs its constructor with those arguments, and makes tr refer to it. Only then does tr.id have somewhere to live.
Using a null handle is a run-time error. Reading or writing tr.id while tr is null stops the simulation with a message such as Null pointer dereferenced, so test tr == null wherever a handle might not have been set yet.
The constructor sets the starting state
The constructor is a function named new with no return type. It runs once for each object, right after allocation, and its job is to leave the object in a valid state. Unlike other methods, new cannot be declared virtual.
Give it arguments for the values every object must have, with defaults where a sensible default exists. Inside it, this.id names the property when an argument has the same name, as in this.id = id;.
A class without a constructor still gets one. That implicit constructor leaves every property at its declared initial value, or at the default for its type: 0 for int, x for logic, null for a handle.
Assignment copies the handle
Assigning one handle to another copies the reference and nothing else. After b = a;, both names refer to the same object, so a change made through b is visible through a.
a = new(7, 'h40);
One object exists, and
arefers to it.a object 1 (id 7)b = a;
No new object. Both handles refer to object 1.
a object 1 (id 7)b object 1b.id = 9;
The change goes to object 1, so
a.idreads 9 as well.a object 1 (id 9)b object 1b = new(3, 'h80);
A second object. Now the handles refer to different objects.
a object 1 (id 9)b object 2 (id 3)a = null;
No handle refers to object 1 any more, so the simulator frees it automatically.
a nullb object 2 (id 3)
The == operator on handles asks whether they refer to the same object. Two separate objects with identical properties still compare unequal; comparing their contents needs a method of your own.
Handles in queues and arrays
A queue such as axi_txn exp_q [$]; holds handles, not objects. Pushing tr stores a second reference to the object tr refers to, and the queue does not get a copy of its own.
So a change made through tr after the push is visible through the queue entry, and a change made through the entry is visible through tr. The scoreboard and the generator are sharing one object.
An array of handles starts with every element null, and you must create an object for each element before using it. A mailbox, covered in Module 3, also passes handles in exactly this way.
One object per transaction
The generator in the opening story called new once, outside its loop. Every push stored another handle to that single object, and the queue ended up with four names for one transaction.
Construct once, reuse
tr = new();
for (int i = 0; i < 4; i++) begin
tr.id = i;
exp_q.push_back(tr); // same object every time
end
All four entries refer to one object, which ends with ID 3.
Construct per transaction
for (int i = 0; i < 4; i++) begin
tr = new(i);
exp_q.push_back(tr); // a new object each time
end
Four objects with IDs 0, 1, 2 and 3.
The same rule applies to everything that hands a transaction to someone else, such as a mailbox put or a scoreboard write. Once you have handed over a handle, build a new object for the next transaction instead of editing the old one.
Try it
Changing b.id changed a.id, because both handles referred to one object. Two separate objects with the same field values still compare unequal, because == compares handles.
The first queue holds four handles to one object, so it shows the last ID four times. Add tr.id = 99; after the second loop and predict which queue changes.
before new: tr is null = 1 after b = a: a.id=9 b.id=9 same object=1 two objects with equal fields: same object=0 new outside the loop: IDs 3 3 3 3 new inside the loop: IDs 0 1 2 3Check yourself
Remember
- A handle refers to an object; a new handle is
nulluntilnewcreates an object for it. - The constructor
newruns once per object and should leave it in a valid state. - Assigning a handle copies the reference, so both names then change the same object.
==on handles asks whether they refer to the same object, not whether the contents match.- Build a new object for each transaction you hand to another component.
Practice
- Construct a valid packet Implement · Easy