eee:fpga:calc_pipeline

Calc 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

  1. 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.

  1. module pipeline (
  2. input wire clk,
  3. input wire [7:0] x,
  4. output wire [9:0] y
  5. );
  6.  
  7. reg [9:0] stage1;
  8. reg [9:0] stage2;
  9. reg [9:0] stage3;
  10.  
  11. always @ (posedge clk) begin
  12. stage1 <= x + 1;
  13. stage2 <= stage1 * 2;
  14. stage3 <= stage2 + 3;
  15. end
  16.  
  17. assign y = stage3;
  18. endmodule

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

  1. `timescale 1ns / 1ps
  2.  
  3. module pipeline_tb;
  4.  
  5. reg clk;
  6. reg [7:0] x;
  7.  
  8. wire [9:0] y;
  9.  
  10. pipeline dut (
  11. .clk(clk),
  12. .x(x),
  13. .y(y)
  14. );
  15. endmodule

Here the block

  1. pipeline dut (
  2. .clk(clk),
  3. .x(x),
  4. .y(y)
  5. );

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.

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.

  1. initial begin
  2. clk = 0;
  3.  
  4. forever begin
  5. #10 clk = ~clk;
  6. end
  7. end

This block gives clk an initial value 0, and toggle every 10 unit. The unit is defined by the top definition

  1. `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.

  1. initial begin
  2. clk = 0;
  3. end
  4.  
  5. always begin
  6. #10 clk = ~clk;
  7. end

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.

  1. initial begin
  2. x = 0;
  3.  
  4. @(negedge clk);
  5. x = 10;
  6.  
  7. @(negedge clk);
  8. x = 20;
  9.  
  10. @(negedge clk);
  11. x = 30;
  12.  
  13. @(negedge clk);
  14. x = 40;
  15. end

Multiple initial or always blocks are executed in parallel in simulation.

It will be a good idea to stop the simulation in some time

  1. initial begin
  2. #200;
  3. $finish;
  4. end

The overall testbench file should be like this

pipeline_tb.v
  1. `timescale 1ns / 1ps
  2.  
  3. module pipeline_tb;
  4.  
  5. reg clk;
  6. reg [7:0] x;
  7.  
  8. wire [9:0] y;
  9.  
  10. pipeline dut (
  11. .clk(clk),
  12. .x(x),
  13. .y(y)
  14. );
  15.  
  16. initial begin
  17. clk = 0;
  18. end
  19.  
  20. always begin
  21. #10 clk = ~clk;
  22. end
  23.  
  24. initial begin
  25. x = 0;
  26.  
  27. @(negedge clk);
  28. x = 10;
  29.  
  30. @(negedge clk);
  31. x = 20;
  32.  
  33. @(negedge clk);
  34. x = 30;
  35.  
  36. @(negedge clk);
  37. x = 40;
  38. end
  39.  
  40. initial begin
  41. #200;
  42. $finish;
  43. end
  44.  
  45. 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.

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
  1. module pipeline (
  2. input wire clk,
  3. input wire [7:0] x,
  4. input wire in_valid,
  5. output wire [9:0] y,
  6. output wire out_valid
  7. );
  8.  
  9. reg [9:0] stage1;
  10. reg [9:0] stage2;
  11. reg [9:0] stage3;
  12.  
  13. reg valid1;
  14. reg valid2;
  15. reg valid3;
  16.  
  17. always @ (posedge clk) begin
  18. stage1 <= x + 1;
  19. stage2 <= stage1 * 2;
  20. stage3 <= stage2 + 3;
  21.  
  22. valid1 <= in_valid;
  23. valid2 <= valid1;
  24. valid3 <= valid2;
  25. end
  26.  
  27. assign y = stage3;
  28. assign out_valid = valid3;
  29.  
  30. endmodule
pipeline_tb.v
  1. `timescale 1ns / 1ps
  2.  
  3. module pipeline_tb;
  4.  
  5. reg clk;
  6. reg [7:0] x;
  7. reg valid_in;
  8.  
  9. wire [9:0] y;
  10. wire valid_out;
  11.  
  12. pipeline dut (
  13. .clk(clk),
  14. .x(x),
  15. .in_valid(valid_in),
  16. .y(y),
  17. .out_valid(valid_out)
  18. );
  19.  
  20. initial begin
  21. clk = 0;
  22. end
  23.  
  24. always begin
  25. #10 clk = ~clk;
  26. end
  27.  
  28. initial begin
  29. x = 0;
  30. valid_in = 0;
  31.  
  32. @(negedge clk);
  33. x = 10;
  34. valid_in = 1;
  35.  
  36. @(negedge clk);
  37. x = 99;
  38. valid_in = 0;
  39.  
  40. @(negedge clk);
  41. x = 20;
  42. valid_in = 1;
  43.  
  44. @(negedge clk);
  45. x = 88;
  46. valid_in = 0;
  47.  
  48. @(negedge clk);
  49. x = 30;
  50. valid_in = 1;
  51. end
  52.  
  53. initial begin
  54. #200;
  55. $finish;
  56. end
  57.  
  58. endmodule

Valid go together with data. If the input is valid, then output will be valid also, and invalid input will give invalid output.

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,

  1. assign ready3 = ~valid3 | out_ready;

And in the always block, stage 3 related codes should be enclosure with the judgement of ready3, i.e.

  1. if (ready3) begin
  2. stage3 <= stage2 + 3;
  3. valid3 <= valid2;
  4. 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
  1. module pipeline (
  2. input wire clk,
  3. input wire [7:0] x,
  4. input wire in_valid,
  5. output wire in_ready,
  6. output wire [9:0] y,
  7. output wire out_valid,
  8. input wire out_ready
  9. );
  10.  
  11. reg [9:0] stage1;
  12. reg valid1;
  13. wire ready1;
  14.  
  15. reg [9:0] stage2;
  16. reg valid2;
  17. wire ready2;
  18.  
  19. reg [9:0] stage3;
  20. reg valid3;
  21. wire ready3;
  22.  
  23. initial begin
  24. valid1 = 0;
  25. valid2 = 0;
  26. valid3 = 0;
  27. end
  28.  
  29. assign ready3 = ~valid3 | out_ready;
  30. assign ready2 = ~valid2 | ready3;
  31. assign ready1 = ~valid1 | ready2;
  32. assign in_ready = ready1;
  33.  
  34. always @ (posedge clk) begin
  35. if (ready1) begin
  36. stage1 <= x + 1;
  37. valid1 <= in_valid;
  38. end
  39.  
  40. if (ready2) begin
  41. stage2 <= stage1 * 2;
  42. valid2 <= valid1;
  43. end
  44.  
  45. if (ready3) begin
  46. stage3 <= stage2 + 3;
  47. valid3 <= valid2;
  48. end
  49. end
  50.  
  51. assign y = stage3;
  52. assign out_valid = valid3;
  53.  
  54. endmodule

The testbench file can be written that

pipeline_tb.v
  1. `timescale 1ns / 1ps
  2.  
  3. module pipeline_tb;
  4.  
  5. reg clk;
  6.  
  7. reg [7:0] x;
  8. reg valid_in;
  9. wire ready_in;
  10.  
  11. wire [9:0] y;
  12. wire valid_out;
  13. reg ready_out;
  14.  
  15. pipeline dut (
  16. .clk(clk),
  17.  
  18. .x(x),
  19. .in_valid(valid_in),
  20. .in_ready(ready_in),
  21.  
  22. .y(y),
  23. .out_valid(valid_out),
  24. .out_ready(ready_out)
  25. );
  26.  
  27. initial begin
  28. clk = 0;
  29. end
  30.  
  31. always begin
  32. #10 clk = ~clk;
  33. end
  34.  
  35. initial begin
  36. x = 0;
  37. valid_in = 0;
  38.  
  39. @(posedge clk);
  40. while (!ready_in)
  41. @(posedge clk);
  42.  
  43. @(negedge clk);
  44. x = 10;
  45. valid_in = 1;
  46.  
  47. @(posedge clk);
  48. while (!ready_in)
  49. @(posedge clk);
  50.  
  51. @(negedge clk);
  52. x = 99;
  53. valid_in = 0;
  54.  
  55. @(posedge clk);
  56. while (!ready_in)
  57. @(posedge clk);
  58.  
  59. @(negedge clk);
  60. x = 20;
  61. valid_in = 1;
  62.  
  63. @(posedge clk);
  64. while (!ready_in)
  65. @(posedge clk);
  66.  
  67. @(negedge clk);
  68. x = 88;
  69. valid_in = 0;
  70.  
  71. @(posedge clk);
  72. while (!ready_in)
  73. @(posedge clk);
  74.  
  75. @(negedge clk);
  76. x = 30;
  77. valid_in = 1;
  78.  
  79. @(posedge clk);
  80. while (!ready_in)
  81. @(posedge clk);
  82.  
  83. @(negedge clk);
  84. x = 77;
  85. valid_in = 0;
  86.  
  87. @(posedge clk);
  88. while (!ready_in)
  89. @(posedge clk);
  90.  
  91. @(negedge clk);
  92. x = 40;
  93. valid_in = 1;
  94.  
  95. @(posedge clk);
  96. while (!ready_in)
  97. @(posedge clk);
  98.  
  99. @(negedge clk);
  100. x = 66;
  101. valid_in = 0;
  102.  
  103. @(posedge clk);
  104. while (!ready_in)
  105. @(posedge clk);
  106.  
  107. @(negedge clk);
  108. x = 50;
  109. valid_in = 1;
  110.  
  111. @(posedge clk);
  112. while (!ready_in)
  113. @(posedge clk);
  114.  
  115. @(negedge clk);
  116. x = 60;
  117. valid_in = 1;
  118. end
  119.  
  120. initial begin
  121. ready_out = 1;
  122.  
  123. repeat (5) @(negedge clk);
  124. ready_out = 0;
  125.  
  126. repeat (5) @(negedge clk);
  127. ready_out = 1;
  128.  
  129. repeat (2) @(negedge clk);
  130. ready_out = 0;
  131.  
  132. repeat (2) @(negedge clk);
  133. ready_out = 1;
  134. end
  135.  
  136. initial begin
  137. #400;
  138. $finish;
  139. end
  140.  
  141. 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.

  • eee/fpga/calc_pipeline.txt
  • 最后更改: 2026/08/22 11:59
  • xiaobenmao