Actions, Triggers and Conditional Bindings
A very common pattern in hardware design is the coupling of data to a "valid" signal. Because it is so common, and there's no hardware overhead to implementing it, SUS provides a syntactic sugar for it named action and trigger.
An action can be thought of as a sort-of method on a submodule. It can take any number of inputs, and produce any number of outputs. When the action is "called", the effect is that the "valid" wire associated with the action (of the same name as the action itself) is set to "1". Important to know is that since an action still represents a physical connection on a module, it can only be used once per cycle. Don't try to call an action in a for-loop and expect it to be "executed" multiple times.
/// Declaration of Actions
module memory#(T, int DEPTH) {
T[DEPTH] mem
action write: int#(FROM: 0, TO: DEPTH) wr_addr, T wr_data {
mem[wr_addr] = wr_data
}
action read: int#(FROM: 0, TO: DEPTH) rd_addr -> T rd_data {
rd_data = mem[rd_addr]
}
}
/// Is equivalent to
module memory#(T, int DEPTH) {
T[DEPTH] mem
input bool write
input int#(FROM: 0, TO: DEPTH) wr_addr
input T wr_data
when write {
mem[wr_addr] = wr_data
}
input bool read
input int#(FROM: 0, TO: DEPTH) rd_addr
output T rd_data
when read {
rd_data = mem[rd_addr]
}
}
/// Calling of Actions
module use_memory {
memory#(T: type bool[20], DEPTH: 5) mem
state int cur_idx
// Really this should also be an action
bool do_read
output bool[20] dout
when do_read {
dout = mem.read(cur_idx) // <<<===
cur_idx = (cur_idx + 1) % 5
}
}
Triggers
Triggers are like reverse actions. In this case it is the submodule that wishes to invoke an action on its parent module, not dissimilar to a callback. In the case of triggers, it is the submodule itself that "calls" the trigger, thereby setting the trigger's "valid" bit to 1 as with actions.
/// Declaration of triggers
module iterator#(int MAX) {
state int cur
action start {
cur = 0
}
trigger iter : int v, bool last
when cur != MAX {
iter(cur, cur == MAX - 1)
cur = (cur + 1) % MAX
}
}
In the parent module, we can react to the submodule's trigger using a "conditional binding", marked with <<<===.
/// Use of triggers and conditional bindings
module use_iter {
int[6] terms = [5, 7, -9, 6, 5, 2]
state int total
iterator#(MAX: 6) iter
action sum_up {
total = 0
iter.start()
}
trigger done : int sum
when iter.iter : int idx, bool last { // <<<===
int new_total = (total + terms[idx]) % 256
when last {
done(new_total)
}
total = new_total
}
}
To recap
- An
actionis for the parent module to signal one of its submodules - A
triggeris for the submodule to signal its parent