====== 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
`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.
{{.:pasted:20260822-083725.png?300}}
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.
{{.:pasted:20260822-084147.png?300}}{{.:pasted:20260822-084247.png?300}}
The result is as below. Values are in decimal.
{{.:pasted:20260822-084636.png}}
===== 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
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
`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
{{.:pasted:20260822-090349.png}}
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.
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
`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.
{{.:pasted:20260822-114337.png}}
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.