Calc Pipeline
Pipeline
Consider the expression that
$$ y = (x+1) \times 2 + 3 $$
where the input is $x$ and output is $y$.
We would like to keep calculating what $x$ is feed in. It can be written that
assign y = (x+1)*2+3;
Assuming that each operation takes a time of $t$ to be stabilized, then the output $y$ is stable after $3t$. Each calculation takes $3t$, while in the time period $0$ to $t$, multiply and the second add is doing nothing, and $t$ to $2t$, the 2 adder is doing nothing, so for $2t$ to $3t$. 2/3 of hardware time is wasted.
But it could be in parallel. When data moves to multiplier, it's a good time for $x$ to feed in the next value and to be processed by adder. When moving to the final adder, the result from first adder can go to multiplier, and $x$ can give new value in. In this way, it takes $3t$ for each calculation, but at $4t$ we can get the second result instead of $6t$, and hardware is fully utilized.
This way is called pipeline. It does not fasten a single datum, but start to proceed consequent datum at the same time.
For both cases, latency is the identical $3t$, but throughput is 0.33 result/cycle and 1 result/cycle correspondingly.
module pipeline ( input wire clk, input wire [7:0] x, output wire [9:0] y ); reg [9:0] stage1; reg [9:0] stage2; reg [9:0] stage3; always @ (posedge clk) begin stage1 <= x + 1; stage2 <= stage1 * 2; stage3 <= stage2 + 3; end assign y = stage3; endmodule
Instantiate
Since the pipeline is fast to be observed by eye, testbench simulation could be used to observe it.
Create a new simulation source named pipeline_tb.v with the content
`timescale 1ns / 1ps module pipeline_tb; reg clk; reg [7:0] x; wire [9:0] y; pipeline dut ( .clk(clk), .x(x), .y(y) ); endmodule
Here the block
pipeline dut ( .clk(clk), .x(x), .y(y) );
instantiated the module pipeline defined before with a name dut (design under test). .clk refers the clk signal defined by the module, while the clk in bracket connects to outside clk signal. The naming of them not necessarily to be the same. Writing like .clk(system_clock_signal) is totally ok as long as the signal system_clock_signal exists.
Clock
However, there is no physical crystal or clock signal in our testbench, and it is supposed to be generated by us. Some statements are allowed in testbench file to do so. The forever can be used within initial block to keep executing, and # allows wait for some simulation timestamps.
initial begin clk = 0; forever begin #10 clk = ~clk; end end
This block gives clk an initial value 0, and toggle every 10 unit. The unit is defined by the top definition
`timescale 1ns / 1ps
That unit is 1ns and precision is 1ps.
Alternatively, always can be used as replacement of forever, but it should be put outside initial block.
initial begin clk = 0; end always begin #10 clk = ~clk; end
Feeding Values
Similar to clk, the input x also need to be generated by us.
The circuit take x values at every posedge, thus the value of x should not be changed at this time. In real world, there will be 2 specifications, setup time and hold time, to define how long the time x should be there before the posedge, as well as how long the time x should still there after the posedge.
Instead of counting the time manually, we can change x in negedge of clk.
initial begin x = 0; @(negedge clk); x = 10; @(negedge clk); x = 20; @(negedge clk); x = 30; @(negedge clk); x = 40; end
Multiple initial or always blocks are executed in parallel in simulation.
Finish
It will be a good idea to stop the simulation in some time
initial begin #200; $finish; end
Simulation Run
The overall testbench file should be like this
- pipeline_tb.v
- `timescale 1ns / 1ps
- module pipeline_tb;
- reg clk;
- reg [7:0] x;
- wire [9:0] y;
- pipeline dut (
- .clk(clk),
- .x(x),
- .y(y)
- );
- initial begin
- clk = 0;
- end
- always begin
- #10 clk = ~clk;
- end
- initial begin
- x = 0;
- @(negedge clk);
- x = 10;
- @(negedge clk);
- x = 20;
- @(negedge clk);
- x = 30;
- @(negedge clk);
- x = 40;
- end
- initial begin
- #200;
- $finish;
- end
- endmodule
Simulation can be started by simulation - run simulation - run behavioral simulation.
After that, waveform page will be shown. To add internal signals, expand the instance and right click signals to add. To change radix (default is hexadecimal), right click a signal.
The result is as below. Values are in decimal.
Valid Pipeline
The output y is X which is undetermined until the third posedge giving the result for the input 0. But we need to let downstream know the output there is invalid data. Also, the upstream should let us know the input data is valid or not. The valid attribute should go through our circuit together with data synchronized.
We can modify our project that
- pipeline.v
- module pipeline (
- input wire clk,
- input wire [7:0] x,
- input wire in_valid,
- output wire [9:0] y,
- output wire out_valid
- );
- reg [9:0] stage1;
- reg [9:0] stage2;
- reg [9:0] stage3;
- reg valid1;
- reg valid2;
- reg valid3;
- always @ (posedge clk) begin
- stage1 <= x + 1;
- stage2 <= stage1 * 2;
- stage3 <= stage2 + 3;
- valid1 <= in_valid;
- valid2 <= valid1;
- valid3 <= valid2;
- end
- assign y = stage3;
- assign out_valid = valid3;
- endmodule
- pipeline_tb.v
- `timescale 1ns / 1ps
- module pipeline_tb;
- reg clk;
- reg [7:0] x;
- reg valid_in;
- wire [9:0] y;
- wire valid_out;
- pipeline dut (
- .clk(clk),
- .x(x),
- .in_valid(valid_in),
- .y(y),
- .out_valid(valid_out)
- );
- initial begin
- clk = 0;
- end
- always begin
- #10 clk = ~clk;
- end
- initial begin
- x = 0;
- valid_in = 0;
- @(negedge clk);
- x = 10;
- valid_in = 1;
- @(negedge clk);
- x = 99;
- valid_in = 0;
- @(negedge clk);
- x = 20;
- valid_in = 1;
- @(negedge clk);
- x = 88;
- valid_in = 0;
- @(negedge clk);
- x = 30;
- valid_in = 1;
- end
- initial begin
- #200;
- $finish;
- end
- endmodule
Valid go together with data. If the input is valid, then output will be valid also, and invalid input will give invalid output.
Valid-Ready
Assuming that downstream device has limited processing ability and might unable to accept our output interemittently, data may get lost during transmission. It's important to have a ready signal from downstream to letting us know if they are able to take our values. A data transmission should be considered complete only if both valid and ready are true. When valid is true but ready is false, we have to hold the data until downstream is ready.
Consider the simplified connection
up down data -------> valid -------> ready <-------
and below values at posedge
data valid ready (1) 10 1 1 (2) 20 1 0 (3) 20 1 0 (4) 20 1 1 (5) 30 1 1 (6) 40 0 0
There's data transfer only at (1), (4), and (5). The data is on hold at (2) and (3) due to downstream not ready.
But a false ready does not means the whole line need to be stopped. At (6), although the downstream is not ready, the data is invalid and we can dispose it immediately instead of holding it. A new valid value can be standby to wait downstream ready. This also applicable for internal registers stage1 and stage2.
To accomplish this, each stage must have its own ready signal defined.
Let's consider stage3, or y first. To move data into stage3, either the downstream already take the data, or the data inside is invalid. That is,
assign ready3 = ~valid3 | out_ready;
And in the always block, stage 3 related codes should be enclosure with the judgement of ready3, i.e.
if (ready3) begin stage3 <= stage2 + 3; valid3 <= valid2; end
No else block needed as no action means to keep the value already.
It's similar for stage2 and stage1.
For simulation simplicity, valid registers are initialized with initial. A reset signal will be introduced later for explicit runtime initialization.
- pipeline.v
- module pipeline (
- input wire clk,
- input wire [7:0] x,
- input wire in_valid,
- output wire in_ready,
- output wire [9:0] y,
- output wire out_valid,
- input wire out_ready
- );
- reg [9:0] stage1;
- reg valid1;
- wire ready1;
- reg [9:0] stage2;
- reg valid2;
- wire ready2;
- reg [9:0] stage3;
- reg valid3;
- wire ready3;
- initial begin
- valid1 = 0;
- valid2 = 0;
- valid3 = 0;
- end
- assign ready3 = ~valid3 | out_ready;
- assign ready2 = ~valid2 | ready3;
- assign ready1 = ~valid1 | ready2;
- assign in_ready = ready1;
- always @ (posedge clk) begin
- if (ready1) begin
- stage1 <= x + 1;
- valid1 <= in_valid;
- end
- if (ready2) begin
- stage2 <= stage1 * 2;
- valid2 <= valid1;
- end
- if (ready3) begin
- stage3 <= stage2 + 3;
- valid3 <= valid2;
- end
- end
- assign y = stage3;
- assign out_valid = valid3;
- endmodule
The testbench file can be written that
- pipeline_tb.v
- `timescale 1ns / 1ps
- module pipeline_tb;
- reg clk;
- reg [7:0] x;
- reg valid_in;
- wire ready_in;
- wire [9:0] y;
- wire valid_out;
- reg ready_out;
- pipeline dut (
- .clk(clk),
- .x(x),
- .in_valid(valid_in),
- .in_ready(ready_in),
- .y(y),
- .out_valid(valid_out),
- .out_ready(ready_out)
- );
- initial begin
- clk = 0;
- end
- always begin
- #10 clk = ~clk;
- end
- initial begin
- x = 0;
- valid_in = 0;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 10;
- valid_in = 1;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 99;
- valid_in = 0;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 20;
- valid_in = 1;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 88;
- valid_in = 0;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 30;
- valid_in = 1;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 77;
- valid_in = 0;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 40;
- valid_in = 1;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 66;
- valid_in = 0;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 50;
- valid_in = 1;
- @(posedge clk);
- while (!ready_in)
- @(posedge clk);
- @(negedge clk);
- x = 60;
- valid_in = 1;
- end
- initial begin
- ready_out = 1;
- repeat (5) @(negedge clk);
- ready_out = 0;
- repeat (5) @(negedge clk);
- ready_out = 1;
- repeat (2) @(negedge clk);
- ready_out = 0;
- repeat (2) @(negedge clk);
- ready_out = 1;
- end
- initial begin
- #400;
- $finish;
- end
- endmodule
It can be seen that invalid data bubble is allowed to be eliminated when downstream not ready.
Downstream getting values:
090 ns 25 210 ns 45 230 ns 65 290 ns 85 310 ns 105 330 ns 125 (and onwards posedge)
Note: A transfer happens at a rising edge when both valid and ready are high immediately before that edge. The waveform displayed after the edge may already contain the next state. Therefore, do not determine whether a transfer occurred only by looking at the values displayed after the edge.





