A task is a named routine you declare once and call as many times as you need, and it has one property that defines it: a task may consume simulation time. Delays, waits, and clock edges are all legal inside a task, which makes it the natural container for stimulus — drive a value, hold it, release it, all wrapped behind one call. Functions, which you will meet properly soon, are the opposite: instant, time-free computation, and a delay inside one is illegal. When the job involves time, the job belongs in a task.
A task receives its inputs as copies. When you call it, the values you pass are copied into the task's own storage, and whatever the task does with those copies, the caller's variables do not move. That is passing by value, and it is the default because it is safe: a task cannot accidentally reach back and disturb its caller. When you genuinely need a task to update a caller's variable, the language provides passing by reference, which hands the task the variable itself rather than a copy. Reach for it only when the task's purpose is to give something back; for driving stimulus, copies are exactly what you want.
Look at any beginner testbench and you will find the same block of stimulus pasted three, five, ten times with small edits. The cost is not aesthetics. Ten pasted copies means ten places a bug can live and ten edits every time the protocol changes, and sooner or later one copy drifts from the others and the testbench lies to you. Refactoring the repetition into one task changes the arithmetic: one place to fix, one place to read, and every call guaranteed to behave the same way. This is why reusable tasks are the first step toward a maintainable testbench, and it is the step this lab makes you take.
Here is the trap that catches people years into the job. A task declared in a module is static by default: there is exactly one set of storage for its arguments and locals, shared by every call. Call the task, let it wait, and call it again while the first call is still waiting, and the second call overwrites the shared storage out from under the first. When the first call wakes and reads its arguments, it reads the second call's values. Nothing errors; the output is simply wrong, and the corruption only appears when calls overlap, which is exactly when you are least likely to suspect the task. The fix is one word: declare the task automatic, and every call gets its own private storage, making overlapping calls fully independent. In this lab, your task will be called twice at the same moment by code we provide, and the output will show you, at one exact line, whether your calls stayed independent. This whole mechanism rests on one earlier idea — a waiting call sleeps while time advances — because the corruption depends on the first call still being asleep when the second one runs.
Part one: the testbench below drives three pulses with the same block of code pasted three times. Replace the repetition with a single task named drive_pulse, taking a value and a duration, that sets the bus, waits the duration, and prints the completion line, then call it three times. Part two is already written for you and must not be modified: it calls your task twice at the same moment with different values. Declare your task so overlapping calls stay independent. Run and submit. The engine checks the full printed sequence, and if the overlapping calls corrupted each other, it will show you the exact line where one call reported the other call's value.
Weekly intelligence on VLSI, AI for EDA, and chip careers. 20,000+ engineers already inside.