Tasks
A task is an atomic sequential entity. It has private state,
ports, optional constants, and the special functions setup and
loop. Every clock cycle, the task executes part of its body.
A task is synchronous by default: clock and reset are implicit. Combinational tasks and multi-clock tasks are configured through the properties block.
Anatomy
A task body holds four kinds of declaration:
task T {
// 1. State variables - persist across cycles, become registers.
short i = 0;
bool started = false;
// 2. Constants - compile-time values, explicit const required.
const short LENGTH_PRE = 6;
// 3. Ports - interface to the outside.
in push u8 data;
out push bool done;
// 4. Functions - including the special setup() and loop().
void setup() { /* runs once after reset */ }
void loop() { /* runs every cycle, repeatedly */ }
}Constants inside a task require the explicit const keyword. In
a bundle the keyword is optional
because bundle members are implicitly constant.
setup is optional. If declared, it runs once after reset, then
control passes to loop. loop runs repeatedly: after the body
finishes, it starts again.
Cycle-accurate execution
Statements inside setup or loop execute sequentially within
the same cycle. A cycle break ends the current cycle and
starts the next. The compiler turns each task into a finite-state
machine where each state is one execution rule, and each rule
takes exactly one cycle.
Explicit cycle breaks
Two statements introduce cycle breaks explicitly.
| Statement | Effect |
|---|---|
fence | End the current cycle, resume on the next. |
idle(n) | End the current cycle and stay idle for n cycles before resuming. n is a compile-time constant. |
A task with a fence between two writes takes two cycles per
loop iteration:
task TwoCycle {
out push u8 value;
u8 v;
void loop() {
value.write(v); // cycle 1
fence;
v = v + 1; // cycle 2
}
}Implicit cycle breaks
The compiler inserts a cycle break automatically in three situations:
- Reading the same port twice - the second read must happen on a later cycle.
- Writing the same port twice - same reason.
- Before each iteration of a sequential loop, so each iteration consumes at least one cycle.
This for loop runs for 16 cycles, with one msg.read() per
cycle and a final cycle to leave the loop:
for (t = 0; t < 16; t++) {
W[t] = msg.read();
}The compiler refuses to schedule two operations in the same cycle when an implicit break would otherwise be required. There is no hidden parallelism.
Port synchronization
A read() on a push, stream, or confirm port blocks: the
surrounding rule defers until the data arrives. Reading several
such ports in one rule is conjunctive, so the rule fires only
when every read completes in the same cycle. port.available()
tests for data without consuming it.
in push u3 a, b;
out u6 product;
void loop() {
product.write(a.read() * b.read()); // fires when a AND b valid
}See Declarations for the full qualifier reference.
Parameters
A task is parameterized by its constants. Declare a const with a
default value, then override it per instance. Each distinct set of
argument values produces its own specialized module.
task Widget {
const int W = 8; // a parameter, default 8
out uint<W> q; // its width depends on W
uint<W> reg;
void setup() { reg = 0; }
void loop() { reg = (uint<W>)(reg + 1); q.write(reg); }
}Use a parameter anywhere a compile-time value is allowed: a type width
(uint<W>), an array size (uint<8> mem[W]), or any constant
expression. A network overrides parameters by name when it instantiates
the task.
w8 = new Widget(); // W defaults to 8 -> uint<8>
w16 = new Widget({W: 16}); // W = 16 -> uint<16>The compiler generates one specialized module per distinct argument
set, so w8 and w16 are independent hardware. An unspecified
parameter keeps its default. This is the same mechanism the standard
library uses for size and width on its FIFOs and RAMs.
A worked example, parameterized by depth:
task Accumulator {
const int DEPTH = 4;
uint<8> mem[DEPTH]; // array sized by the parameter
out uint<32> total;
void setup() {
uint<8> i;
for (i = 0; i < DEPTH; i = (uint<8>)(i + 1)) { mem[i] = i; }
}
void loop() {
uint<32> sum = 0;
uint<8> i;
for (i = 0; i < DEPTH; i = (uint<8>)(i + 1)) { sum = (uint<32>)(sum + mem[i]); }
total.write(sum);
}
}
acc4 = new Accumulator(); // DEPTH 4
acc16 = new Accumulator({DEPTH: 16}); // DEPTH 16, a 4x-larger arrayAngle-bracket shorthand
Parameters can also be declared and supplied with the familiar angle-bracket syntax. Declare the formals after the task name, then pass them positionally when you instantiate:
task Cell<int W = 8, int EXPECT = 0xFF> {
uint<W> reg;
void setup() { reg = (uint<W>)EXPECT; }
...
}
c4 = new Cell<4, 0xF>(); // W = 4, EXPECT = 0xF
c8 = new Cell<8, 0xFF>(); // W = 8, EXPECT = 0xFF
cd = new Cell(); // both defaults: W = 8, EXPECT = 0xFFThis is exactly the same parameter mechanism - task Cell<int W = 8> is
shorthand for a const int W = 8 parameter, and new Cell<4>() is
shorthand for new Cell({W: 4}). The two forms are interchangeable; you
can even mix them (new Cell<4>({EXPECT: 0xF})), and a named argument
wins if it overlaps a positional one. Positional arguments bind to the
formals in declaration order; supplying fewer is fine (the rest keep
their defaults), but supplying more than the task declares is an error.
Type arguments and parameter defaults are constant arithmetic
expressions (+, -, *, /, %). Comparison and shift operators
(<, >, <<, >>) are not allowed inside the angle brackets - they
would collide with the closing >; precompute the value into a const
if you need them.
Parameterized ports
A port's width can be a parameter, so the same task specializes into instances with different interface widths. Wire each producer to a consumer of the same width:
task Producer {
const int W = 8;
out push uint<W> o;
void setup() { o.write((uint<W>)0xFF); }
}
task Consumer {
const int W = 8;
in push uint<W> i;
void setup() { uint<W> v = i.read; }
}
network Pipelines {
p4 = new Producer({W: 4}); // 4-bit port
c4 = new Consumer({W: 4});
p8 = new Producer({W: 8}); // 8-bit port
c8 = new Consumer({W: 8});
c4.reads(p4.o); // 4-bit -> 4-bit
c8.reads(p8.o); // 8-bit -> 8-bit
}Connected ports must have the same width. Wiring a 4-bit output to
an 8-bit input - for example a generic instance specialized to the wrong
width - is an error, not a silent zero-extend or truncation. The check
applies to fixed-width ports too (u8 into uint<4>).
For widths derived from a size, use sizeof, which returns the number
of bits needed to hold a value (the clog2 you would write in other
HDLs):
const int DEPTH = 16;
const int AW = sizeof(DEPTH - 1); // 4 - bits to index 16 entries
out push uint<AW> addr; // a 4-bit address portRules and limits
- A parameter must have a default. An uninitialized
constis rejected. - An argument key must name a
constof the task. A misspelled key like{Wdith: 16}is an error, not a silent fallback to the default. - Arguments may be named (
{W: 16}) or positional (Widget<16>); see the angle-bracket shorthand above. A named argument wins if it overlaps a positional one. - Connected ports must have matching widths. A width mismatch on a connection is an error, not an implicit resize.
External tasks
An external task declares a signature in C⏚ but provides its implementation in Verilog, VHDL, or another HDL. The C⏚ compiler instantiates and wires it like any other task, but emits no code for its body.
package com.acme;
task Queue {
properties {
clocks: ["din_clock", "dout_clock"],
implementation: {
type: "external",
file: "../../../rtl/Queue.v",
dependencies: ["../../../rtl/Ram.v"]
}
}
const int depth = 2, width = 16;
in push uint<width> din;
out push uint<width> dout;
out push bool ack;
}External tasks have:
- No
setup, noloop, no function bodies. - Constants that are passed through as instantiation parameters.
- An
implementationproperty pointing at the HDL file and any dependencies.
The compiler emits scripts that include the listed dependencies
in declaration order, before the task itself. Paths are absolute
or relative to the folder containing the .cg file.
Instantiation overrides the constants:
queue = new Queue({depth: 32, width: 8});Next: Networks for composing tasks into hierarchies.