Dynamic Pipelines
For conditionally executing pipelines, an enable condition can be specified on the reg statement. If this condition is false, the old value of all pipeline registers for this stage will be held, rather than being updated to the new values.
The stall condition is specified as follows
pipeline(1) pipe(clk: clock, condition: bool, x: bool) -> bool {
reg[condition];
x
}
where condition
is a boolean expression which when true
updates all the registers for this stage, and when false
the register content is undefined1.
The above code is compiled down to the equivalent of
entity pipe(clk: clock, condition: bool, x: bool) -> bool {
reg(clk) condition_s1 = if condition {condition} else {condition_s1}
reg(clk) x_s1 = if condition {x} else {x_s1}
x_s1
}
Pipeline enable conditions propagate to stages above the enabled stage, in order to make sure that values are not flushed. This means that in the following code
pipeline(1) pipe(clk: clock, x: bool) -> bool {
reg;
reg[inst check_condition()];
reg;
x
}
the first two stages will be disabled and keep their old value when
check_condition
returns false
while the registers in the final stage will
update unconditionally.
If several conditions are used, they are combined, i.e. in
pipeline(1) pipe(clk: clock, x: bool) -> bool {
reg;
reg[inst check_condition()];
reg;
reg[inst check_other_condition()];
reg;
x
}
the first two stages will update only if both check_condition()
and
check_other_condition()
are true
, and the next two registers are only going
to update if check_other_condition
is true.
stage.ready
and stage.valid
In some cases it is necessary to check if a stage will be updated on the next
cycle (ready) or if the values in the current cycle are valid. This is done
using stage.valid
and stage.ready
.
stage.ready
is true
if the registers directly following the statement will
update their values this cycle, i.e. if the condition of it and all downstream
registers are met.
stage.valid
is true
if the values in the current stage were
enabled, i.e. if none of the conditions for any registers
this value flowed
through were false
.
NOTE:
stage.valid
currently initializes as undefined, and needs time to propagate through the pipeline. It is up to the user to ensure that a reset signal is asserted long enough forstage.valid
to stabilize.
Example: Processor
This is part of a processor that stalls the pipeline in order to allow 3 cycles for a load instruction.
The program_counter
entity takes a signal indicating whether it should count up, or stall.
This signal is set to stage.ready
, to ensure that if the downstream registers don't accept new instructions, the program counter will stall.
pipeline(5) cpu(clk: clock) -> bool {
let pc = program_counter$(clk, stall: stage.ready)
reg;
let insn = inst(1) program_memory(clk)
let stall = stage(+1).is_load || stage(+2).is_load || stage(+3).is_load;
reg[stall];
let is_load = is_load(insn);
reg;
let alu_out = alu(insn);
reg;
reg;
let regfile_write = if stage.valid && insn_writes(insn) {Some(alu)} else {None()}
true // NOTE: Dummy output, we need to return something
}
the last line where regfile_write
is set uses stage.valid
to ensure that
results of an instruction are only written for valid signals, not signals
being undefined due to a stalled register.
Example: Latency Insensitive Interface
A common design method in hardware is to use a ready/valid interface. Here, a
downstream unit can communicate that it is ready to receive data by asserting a
ready
signal, and upstream unit indicate that their data is valid using a
valid
signal. If both ready
and valid
are set, the upstream unit hands
over a piece of data to the downstream unit.
What follows is an example of a pipelined multiplier that propagates a
ready/valid signal from its downstream unit to its upstream unit
struct port Rv<T> {
data: &T,
valid: &bool,
ready: &mut bool
}
pipeline(4) mul(clk: clock, a: Rv<int<16>>, b: Rv<int<16>>) -> Rv<int<32>> {
let product = a*b;
set a.ready = stage.ready;
set b.ready = stage.ready;
reg[*a.valid && *b.valid];
reg;
reg;
let downstream_ready = inst new_mut_wire();
reg[inst read_mut_wire(downstream_ready)];
Rv {
data: &product,
valid: &stage.valid,
ready: downstream_ready,
}
}
Currently, the implementation holds the previous value of the register,
which will also be done in hardware. However, this might change to setting
the value to X
for easier debugging, and to give more optimization
opportunities for the synthesis tool.