josuah.net | panoramix-labs.fr

git | cv | links | quotes | ascii | tgtimes | gopher | mail

Sequential signals may hide combinational ones

Verilog is a hardware language with a syntax similar to C, but very different semantics: signals, circuits, and registers for storing the state.

Registers and sequential logic

                 ┌─────┐
some circuit  >──┤D   Q├──>  some circuit
                 │     │     updated one clock edge later
               ┌─>     │
               │ └─────┘
             clock

They use the <= assignment operator to build-up sequential logic. Like software variables, they can hold values across multiple clock cycles. An assignment to a register (on D) gets read (on Q) on the next clock cycle.

reg r;
always_ff @(posedge clk) begin
  r <= 1;             └── This is the clock connected to all registers
end    └── This is the input signal D fed into the register

wire w = r;
         └── This is the output signal Q read from the register

Wires, signals, and combinational logic

some circuit  >───────────────>  some circuit
                the_wire_name

Most verilog expressions gets converted to a circuit of gates implementing it. The = assignment used in various contexts permit to attach an end of a wire to a name, a label. The always_comb block can only have =, no <=, and implement combinational logic.

input i;
wire [7:0] w;
always_comb begin
  w = i ^ 8'b10101010;
end

How combinational logic appear

But what if complex expressions are assigned to a register instead of a wire? The signal coming into the register may be represented as an expression itself, and expressions are combinational logic. The flip-flop-only always_ff contains, in fact, a mix of combinational and sequential logic.

The restriction in always_ff is that adding extra combinational logic is forbidden (no = operator) but combinational logic feeding registers can still occur. It helps with preventing <= being typoed into =.

input i;
reg [7:0] r;
always_ff @(posedge clk) begin
  r <= i ^ 8'b10101010;
end

This is not a problem per-se, just a remark.

The LowRISC approach

The above permits us to explain the approach LowRISC is taking. The team described it in their style guide for SystemVerilog:

It is done by coupling a always_ff that applies the changes to registers, with always_comb that build-up the next values. Each register is having:

On each clock, this happens alone in a block: something_q <= something_d; and the rest is purely combinational.

In other words, the sequential operation have been isolated, by giving the input signal a name (something_d here).

wire something_d;
reg [7:0] something_q;
always_ff @(posedge clk) begin
  // apply the changes to the registers
  something_q <= something_d;
end

always_comb begin
  // by default, the value stays the same as the register previous value
  // this was happening under the hood in the previous examples, it is now
  // explicit
  something_d = someting_q;

  // for giving something_q another value, something_d can be changed with
  // in the combinational logic
  if (i > 3) begin
    something_d = something_d + 1;
  end
end

Extra convention

In addition to LowRISC guideline, I also use this convention: When declaring signals with logic, the _d wire comes first, followed by _q and _q2 etc. That way, there is one line per signal, including its past state. This help with reading code faster:

logic ack_d, ack_q, ack_q2;
logic state_d, state_q;
logic counter_q;

In practice?

Inconvenients:

Advantages:

It is also frequent to have a reset signal that puts all registers to a default value. The LowRISC approach makes it convenient to integrate it within the always_ff block, introducing a little bit of combinational logic, without much effect.

always_ff @(posedge clk) begin
  if (rst) begin
    something0_q <= 0;
    something1_q <= 0;
    something2_q <= 0;
    something3_q <= 0;
  end else begin
    something0_q <= something0_d;
    something1_q <= something1_d;
    something2_q <= something2_d;
    something3_q <= something3_d;
  end
end

In a real world example, I would have been using logic instead of wire and reg, the _q and _d helping with making the distinction. I would also likely use rst_ni instead of rst and clk_i instead of clk.

Also sometimes the whole design is so trivial that a separate always_comb would be an overkill.

https://lowrisc.org/

https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md