Using Tasks

Tasks are the fundamental unit of behavior in a DV-Flow specification. Tasks accept data from tasks that they depend on, and produce data to be used by dependent tasks. Tasks may have local parameters to control operation.

The implementation of a task is required to perform three key tasks:

  • Compute dependencies – Specifically, determine whether the task must perform work. Tasks typically save a ‘memento’ with the system that is used by future evaluations of the task.

  • Perform work – The task implementation must perform the desired data transformation when required. Usually this involves invoking another tool, such as a compiler. When work is performed, the task may produce markers to communicate the location of errors, warnings, or other key information.

  • Produce output – Tasks are required to pass data output data to their successor tasks independent of whether the task performed a data transformation.

Most tasks are built on top of another existing task or tasks.These ‘derived’ tasks either modifiy the parameters of the base task to cause it to behave as desired, or compose multiple tasks together to achieve the desired functionality.

Specifying the Base Task

Each defined task has a unique identity. Typically, though, the required behavior is very similar to that of an existing task. As a consequence, it is very common to define a task in terms of another. This is done via the uses clause.

tasks:
- name: PrintHello
  uses: std.Message

The example above creates a new task named PrintHello that inherits the parameters and implementation of the existing std.Message task.

Specializing Task Parameters

The simplest way to leverage existing tasks is to customize the value of parameters that control the behavior of the base task. Let’s take a look at the std.Message task. This task displays a user-specified message string. By default, the message is empty.

tasks:
- name: PrintHello
  uses: std.Message
  with:
    msg: Hello, World!

The example above creates a new task named PrintHello that overrides the value of the msg parameter. This causes the implementation of std.Message to print ‘Hello, World!’.

Specifying Dependencies

Most tasks operate on data produced by other tasks. The needs clause specifies the tasks that must complete before this task can execute.

tasks:
- name: rtlfiles
  uses: std.FileSet
  with:
    type: "verilogSource"
    base: "rtl"
    include: "*.v"
- name: sim
  uses: hdlsim.vlt.SimImage
  needs: [rtlfiles]
  with:
    top: [mytop]
- name: run
  uses: hdlsim.vlt.SimRun
  needs: [sim]

The example above shows three tasks that:

  • Gather verilog source files from the rtl directory

  • Compile the files into a simulation image

  • Run the simulation image

Data passes between each pair of steps above:

  • rtlfiles outputs a list of sources files required to create a simulation image

  • The sim-image task passes a path to the simulation image to the task that runs it

In addition, one step cannot proceed until the proceeding step has completed. These scheduling and dataflow requirements are captured using needs relationships.

Tasks and Dataflow

Task needs relationships specify dataflow between tasks in addition to scheduling dependencies. A task provides two controls over what input data is provided to the task implementation (if present) and what inputs are forwarded to the output.

../_images/task_dataflow.excalidraw.svg

The two controls are:

  • consumes - Specifies what input data will be passed to the implementation

    • all - All input data is passed to the implementation

    • none - No input data is passed to the implementation

    • pattern - Inputs matching a pattern are passed to the implementation

  • passthrough - Specifies what inputs are passed to the task output

    • all - All input data is passed to the output

    • none - No input data is passed to the output

    • unused - Inputs that are not consumed are passed to the output

    • pattern - Inputs matching a pattern are passed to the output

The default value of passthrough is always unused. The default value for the consumes` parameter depends on the task type:

  • Shell or Python Implementation: all

  • DataItem or No Implementation: none

Using DataItem Tasks

Tasks generally produce output data. That output data may be produced by the task’s programming-language implementation (ie Python) when computation of some form is required. When no computation is required, there is concise way to produce a data item: define a task that uses a data type as the base. This is most commonly done when producing a data item that captures a set of tool options.

tasks:
- name: SimOptionsTrace
  uses: hdlsim.SimElabArgs
  with:
    args: [--trace-fst]

- name: sim_img
  uses: hdlsim.vlt.SimImage
  needs: [SimOptionsTrace]

The SimOptionsTrace task above will produce a single hdlsim.SimElabArgs data item that instructs the Verilator simulator to enable dumping a FST-format waveform file.

This has the same effect as adding –trace-fst directly on the sim_img task, but provides more flexibility:

  • Tasks can be defined with commonly-used sets of options

  • Conditional task execution can be used to select between option sets

Using Compound Tasks

Compound tasks allow a task to be defined in terms of a set of subtasks. In many cases, this allows more-advanced behavior to be created without the need to provide a programming-language implementation for the task.

tasks:
- name: CreateSVFiles
  rundir: inherit
  body:
  - name: mod1
    uses: std.CreateFile
    with:
        filename: mod1.sv
        content: |
            module mod1;
            endmodule
  - name: mod2
    uses: std.CreateFile
    with:
        filename: mod2.sv
        content: |
            module mod2;
            endmodule
  - name: getFiles
    uses: std.FileSet
    passthrough: none
    needs: [mod1, mod2]
    with:
        type: "verilogSource"
        base: "."
        include: "*.sv"

The compound task above uses the std.CreateFile task to create two SystmeVerilog files. We then want to pass both files on to all tasks that depend on CreateSVFiles.

This is accomplished by:

  • Specifying that all tasks use the same run directory via the

    rundir: inherit clause.

  • Causing the getFiles to depend on the file-creation tasks

Conditional Tasks

By default, all tasks on the needs dependency path from the root task(s) will be executed. In some cases, though, it is desirable to only execute tasks under specific circumstances. The iff property of tasks supports this use model.

package:
  name: my_ip
  with:
    debug_level:
      type: int
      value: 0

  tasks:
  - name: SimOptions
    uses: hdlsim.SimElabArgs
    body:
    - name: SimOptionsDebug
      uses: hdlsim.SimElabArgs
      iff: ${{ debug_level > 0 }}
      with:
        args: [--trace-fst]

  - name: sim_img
    uses: hdlsim.vlt.SimImage
    needs: [SimOptions]

The example above uses conditional execution to customize elaboration options. When the debug_level parameter’s value is greater than 0, the SimOptionsDebug task sends the –trace-fst option to the simulator. Otherwise, no additional arguments are provided.