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 for stage.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,
        }
}
1

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.