@quartz struct Mac
@in a::Bits{16}, b::Bits{16}, c::Bits{16}, d::Bits{16}
@out y::Bits{33}
@out valid::Bool
p::Pipeline{2,Bits{33}}
end
@on Mac posedge(clk) begin
p ← Bits{33}(a) * b + Bits{33}(c) * d # two multiplies and an add, over two cycles
y ← coalesce(p, 0) # the latest result, or 0 before the first
valid ← isnew(p)
endPipelines
Pipeline{K,T}: a computation spread over K cycles
Some arithmetic will not fit in one clock. A wide multiply, a sum of many terms, a popcount over a long register: the logic is too deep for the clock period, and the fix is to register it in the middle — a pipeline. Writing pipelines by hand means deciding where the cuts go, declaring a register for every value that crosses one, and keeping the stages in step. A Pipeline{K,T} does that for you.
Writing to a pipeline
Write an expression into the pipeline and the result comes out K cycles later. Here the multiplies land in the first stage and the add in the second; the result written at edge 1 is readable at edge 3, and copied to y at edge 4. The compiler cuts the traced expression into K stages of roughly equal depth, and every value that crosses a cut becomes a register.
let m = Mac()
for i in 1:5
m = step(m; a=Bits{16}(i), b=Bits{16}(10), c=Bits{16}(1), d=Bits{16}(i))
@info "edge $i: y = $(Int(m.y)), valid = $(m.valid)"
end
end[ Info: edge 1: y = 0, valid = false [ Info: edge 2: y = 0, valid = false [ Info: edge 3: y = 0, valid = false [ Info: edge 4: y = 11, valid = true [ Info: edge 5: y = 22, valid = true
Reading from a pipeline
- Read bare,
pis the most recent output, ormissingbefore the first one has arrived — socoalesce(p, 0)gives a value either way. isnew(p)is true on the cycle a new result appears.isready(p)is true when a result is out and no write is still on its way through, so whatpgives is the result of the last write.
Write to a pipeline every cycle and you get a result every cycle, K cycles late. Write to it occasionally and isnew tells you when to look.
Seeing the cuts
stages shows what the compiler did:
stages(Mac, :p)Pipeline p :: Pipeline{2,Bits{33}} of Mac path cost 99, cut into 2 stages
stage 1 cost 66
in a, b, c, d
t1 = Bits{33}(a) 33-bit [0]
t2 = t1 * b 33-bit [66]
t3 = Bits{33}(c) 33-bit [0]
t4 = t3 * d 33-bit [66]
out t2 (33), t4 (33) 66 flops
stage 2 cost 33
in t2, t4
p = t2 + t4 33-bit [33]
out p (33) 33 flops
output p_out (33), p_valid (2), p_hasout (1), p_isnew (1) 37 flops
total 99 + 37 = 136 flops
Each stage lists what it takes in, what it computes, what it registers, and the cost of its longest path; the last line is what the whole pipeline costs in registers. stages(Mac) reports every pipeline in the module.
A single operation cannot be split. If K is larger than the computation can use, the extra stages only delay, the report says so, and writing the Verilog warns:
@quartz struct TooDeep
@in a::Bits{8}, b::Bits{8}
s::Pipeline{3,Bits{9}}
end
@on TooDeep posedge(clk) s ← Bits{9}(a) + b
stages(TooDeep, :s)Pipeline s :: Pipeline{3,Bits{9}} of TooDeep path cost 9, cut into 3 stages
stage 1 (computes nothing) cost 0
in a, b
t1 = Bits{9}(a) 9-bit [0]
out b (8), t1 (9) 17 flops
stage 2 (computes nothing) cost 0
in b, t1
out b (8), t1 (9) 17 flops
stage 3 cost 9
in b, t1
s = t1 + b 9-bit [9]
out s (9) 9 flops
output s_out (9), s_valid (3), s_hasout (1), s_isnew (1) 14 flops
total 43 + 14 = 57 flops
warning: pipeline s of TooDeep: stage 1 of 3 computes nothing; 3 stages is more than the computation can use
warning: pipeline s of TooDeep: stage 2 of 3 computes nothing; 3 stages is more than the computation can use
The Verilog contains the stage registers and an always block per stage, plus a valid bit that travels with the data — with the cuts chosen by measuring the depth of the logic. Change the expression and the cuts move with it.
What goes in a pipeline
The expression written into a pipeline may use registers, inputs, locals and helper functions, like any other expression. What it may not do is depend on its own output: a feedback path through a pipeline is a loop with K cycles of delay, and that is a state machine, not a pipeline. Write the feedback as ordinary registers around the pipeline instead.
Next
Multicycle paths: the other answer to slow logic, when the inputs are slower still.