Skip to content

Commit 636acd4

Browse files
committed
Add BiDf and utilities
1 parent d616f5c commit 636acd4

File tree

8 files changed

+567
-0
lines changed

8 files changed

+567
-0
lines changed

LICENSES/BSD-2-Clause.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Copyright (c) <year> <owner>.
2+
3+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4+
5+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
7+
8+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

bittide-extra/bittide-extra.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ library
174174
Project.Chan
175175
Project.Handle
176176
Protocols.Axi4.Extra
177+
Protocols.BiDf
177178
Protocols.Df.Extra
178179
Protocols.Extra
179180
Protocols.Spi
@@ -190,6 +191,7 @@ test-suite unittests
190191
other-modules:
191192
Tests.Clash.Cores.Xilinx.Xpm.Cdc.Extra
192193
Tests.Numeric.Extra
194+
Tests.Protocols.BiDf
193195
Tests.Protocols.Df.Extra
194196

195197
build-depends:
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
-- SPDX-FileCopyrightText: 2026 Google LLC
2+
--
3+
-- SPDX-License-Identifier: Apache-2.0
4+
5+
-- | Bi-directional request/response-style 'Df' channels.
6+
module Protocols.BiDf (
7+
BiDf,
8+
9+
-- * Conversion
10+
fromDfs,
11+
toDfs,
12+
fromBiDf,
13+
toBiDf,
14+
15+
-- * Trivial combinators
16+
void,
17+
loopback,
18+
19+
-- * Mapping
20+
map,
21+
bimap,
22+
23+
-- * Fan-in
24+
fanin,
25+
26+
-- * Memories
27+
fromBlockRam,
28+
fromBlockRamWithMask,
29+
30+
-- * Debugging
31+
trace,
32+
traceSignal,
33+
) where
34+
35+
import Clash.Prelude hiding (map, traceSignal)
36+
import Data.Typeable (Typeable)
37+
import Protocols
38+
39+
import qualified Protocols.Df as Df
40+
import qualified Protocols.Df.Extra as Df
41+
42+
{- | A 'Protocol' allowing requests to be passed downstream, with corresponding
43+
responses being passed back upstream. Responses are provided in the order that
44+
their corresponding requests were submitted.
45+
46+
*Correctness conditions*
47+
48+
- The response channel must not produce a value before the request channel
49+
has produced a value. The response may be produced in the same cycle the
50+
request is acknowledged (but see the law about a combinational path
51+
below).
52+
53+
- Each request must be paired with exactly one response.
54+
55+
- Responses must be issued in the order that their corresponding requests arrived.
56+
57+
- Both the request and response channels must obey usual 'Df' correctness
58+
conditions.
59+
60+
- There must not be a combinational path from the request channel to the
61+
response channel.
62+
-}
63+
type BiDf dom req resp =
64+
(Df dom req, Reverse (Df dom resp))
65+
66+
-- | Convert a circuit of 'Df's to a 'BiDf' circuit.
67+
toBiDf ::
68+
Circuit (Df dom req) (Df dom resp) ->
69+
Circuit (BiDf dom req resp) ()
70+
toBiDf c = circuit $ \bidf -> do
71+
resp <- c -< req
72+
req <- toDfs -< (bidf, resp)
73+
idC -< ()
74+
75+
-- | Convert a 'BiDf' circuit to a circuit of 'Df's.
76+
fromBiDf ::
77+
Circuit (BiDf dom req resp) () ->
78+
Circuit (Df dom req) (Df dom resp)
79+
fromBiDf c = circuit $ \req -> do
80+
(biDf, resp) <- fromDfs -< req
81+
c -< biDf
82+
idC -< resp
83+
84+
-- | Convert a pair of a request and response 'Df`s into a 'BiDf'.
85+
toDfs :: Circuit (BiDf dom req resp, Df dom resp) (Df dom req)
86+
toDfs = fromSignals $ \(~((reqData, respAck), respData), reqAck) ->
87+
(((reqAck, respData), respAck), reqData)
88+
89+
-- | Convert a 'BiDf' into a pair of request and response 'Df`s.
90+
fromDfs :: Circuit (Df dom req) (BiDf dom req resp, Df dom resp)
91+
fromDfs = fromSignals $ \(reqData, ~((reqAck, respData), respAck)) ->
92+
(reqAck, ((reqData, respAck), respData))
93+
94+
-- | Ignore all requests, never providing responses.
95+
void :: (KnownDomain dom, HiddenReset dom) => Circuit (BiDf dom req resp') ()
96+
void = circuit $ \biDf -> do
97+
req <- toDfs -< (biDf, resp)
98+
resp <- Df.empty -< ()
99+
Df.void -< req
100+
101+
-- | Return mapped requests as responses.
102+
loopback ::
103+
(HiddenClockResetEnable dom, NFDataX req) =>
104+
(req -> resp) ->
105+
Circuit (BiDf dom req resp) ()
106+
loopback f = circuit $ \biDf -> do
107+
req <- toDfs -< (biDf, resp)
108+
resp <- Df.map f <| Df.registerFwd -< req
109+
idC -< ()
110+
111+
-- | Map requests and responses of a 'BiDf' using separate `Df` circuits.
112+
map ::
113+
Circuit (Df dom req) (Df dom req') ->
114+
Circuit (Df dom resp) (Df dom resp') ->
115+
Circuit (BiDf dom req resp') (BiDf dom req' resp)
116+
map mapReq mapResp = circuit $ \bidf -> do
117+
req <- toDfs -< (bidf, resp')
118+
req' <- mapReq -< req
119+
resp' <- mapResp -< resp
120+
(bidf', resp) <- fromDfs -< req'
121+
idC -< bidf'
122+
123+
-- | Map both requests and responses.
124+
bimap ::
125+
(req -> req') ->
126+
(resp -> resp') ->
127+
Circuit (BiDf dom req resp') (BiDf dom req' resp)
128+
bimap f g = map (Df.map f) (Df.map g)
129+
130+
{- | Merge a number of 'BiDf's, preferring requests from the last channel.
131+
TODO: Check why this does not work if we insert delays on the individual `Df` channels of the
132+
right hand side `BiDf`s. See `Tests.Protocols.BiDf.prop_fanin`.
133+
-}
134+
fanin ::
135+
forall n dom req resp.
136+
( KnownNat n
137+
, 1 <= n
138+
, NFDataX req
139+
, NFDataX resp
140+
, HiddenClockResetEnable dom
141+
) =>
142+
Circuit (Vec n (BiDf dom req resp)) (BiDf dom req resp)
143+
fanin = fromSignals $ \(upFwds, (reqAck, respData)) ->
144+
let
145+
(reqDatas, respAcks) = unzip upFwds
146+
147+
((reqAcks, respAck), (respDatas, reqData)) =
148+
toSignals fanin' ((reqDatas, respData), (respAcks, reqAck))
149+
in
150+
(zip reqAcks respDatas, (reqData, respAck))
151+
where
152+
fanin' ::
153+
Circuit
154+
(Vec n (Df dom req), Df dom resp)
155+
(Vec n (Df dom resp), Df dom req)
156+
fanin' = circuit $ \(reqs, resp0) -> do
157+
[fwd0, fwd1] <-
158+
Df.fanout
159+
<| Df.roundrobinCollect @n Df.Parallel
160+
<| repeatWithIndexC (\i -> Df.map (\x -> (i, x)))
161+
-< reqs
162+
163+
(activeN, Fwd rdy) <- Df.skid <| Df.map fst -< fwd1
164+
req1 <- Df.stallNext rdy -< req0
165+
resps <- Df.route <| Df.zip -< (activeN, resp0)
166+
req0 <- Df.map snd -< fwd0
167+
idC -< (resps, req1)
168+
169+
{- | Copy a circuit /n/ times, providing access to the index of each replica.
170+
If looking for a circuit that turns a single channel into multiple, check out
171+
'Protocols.Df.fanout'.
172+
-}
173+
repeatWithIndexC ::
174+
forall n a b.
175+
(KnownNat n) =>
176+
(Index n -> Circuit a b) ->
177+
Circuit (Vec n a) (Vec n b)
178+
repeatWithIndexC f =
179+
Circuit (unzip . zipWith g indicesI . uncurry zip)
180+
where
181+
g i = case f i of Circuit f' -> f'
182+
183+
{- | Creates a `Df` wrapper around a block RAM primitive that supports byte enables for
184+
its write channel. Writes are always acked immediately, reads receive backpressure
185+
based on the outgoing `Df` channel.
186+
187+
This component assumes that the blockram primitive produces a result one cycle after the read
188+
address is provided and that it can be stalled by deasserting the enable signal.
189+
190+
TODO: Use `DSignal` for primitive to enforce the timing assumption on a type level.
191+
-}
192+
fromBlockRamWithMask ::
193+
(KnownDomain dom, HiddenClock dom, HiddenReset dom, Num addr, NFDataX addr, KnownNat words) =>
194+
( Enable dom ->
195+
Signal dom addr ->
196+
Signal dom (Maybe (addr, BitVector (words * 8))) ->
197+
Signal dom (BitVector words) ->
198+
Signal dom (BitVector (words * 8))
199+
) ->
200+
Circuit
201+
( BiDf dom addr (BitVector (words * 8))
202+
, Df dom (addr, BitVector words, BitVector (words * 8))
203+
)
204+
()
205+
fromBlockRamWithMask primitive = circuit $ \(bidf, writeData) -> do
206+
readAddress <- toDfs -< (bidf, readData)
207+
readData <- Df.fromBlockRamWithMask primitive -< (readAddress, writeData)
208+
idC -< ()
209+
210+
{- | Creates a `Df` wrapper around a block RAM primitive. Writes are always acked
211+
immediately, reads receive backpressure based on the outgoing `Df` channel.
212+
213+
This component assumes that the blockram primitive produces a result one cycle after the read
214+
address is provided and that it can be stalled by deasserting the enable signal.
215+
216+
TODO: Use `DSignal` for primitive to enforce the timing assumption on a type level.
217+
-}
218+
fromBlockRam ::
219+
forall dom addr words.
220+
(KnownDomain dom, HiddenClock dom, HiddenReset dom, KnownNat words, Num addr, NFDataX addr) =>
221+
( Enable dom ->
222+
Signal dom addr ->
223+
Signal dom (Maybe (addr, BitVector (words * 8))) ->
224+
Signal dom (BitVector (words * 8))
225+
) ->
226+
Circuit
227+
( BiDf dom addr (BitVector (words * 8))
228+
, Df dom (addr, BitVector (words * 8))
229+
)
230+
()
231+
fromBlockRam primitive = circuit $ \(bidf, writeData) -> do
232+
readAddress <- toDfs -< (bidf, readData)
233+
readData <- Df.fromBlockRam primitive -< (readAddress, writeData)
234+
idC -< ()
235+
236+
{- | `BiDf` version of `traceShowId`, introduces no state or logic of any form. Only prints when
237+
there is data available on the input side. Prints available data, clock cycle count in the
238+
relevant domain, and the corresponding Ack.
239+
-}
240+
trace ::
241+
(KnownDomain dom, ShowX req, NFDataX req, ShowX resp, NFDataX resp) =>
242+
String ->
243+
Circuit (BiDf dom req resp) (BiDf dom req resp)
244+
trace msg = map (Df.trace (msg <> "_request")) (Df.trace (msg <> "_response"))
245+
246+
{- | `BiDf` version of `Clash.Debug.traceSignal` based on `Df.traceSignal`.
247+
Signal names:
248+
- left request forward: name_request_fwd
249+
- left request backward: name_request_bwd
250+
- right response forward: name_response_fwd
251+
- right response backward: name_response_bwd
252+
-}
253+
traceSignal ::
254+
( KnownDomain dom
255+
, ShowX req
256+
, NFDataX req
257+
, BitPack req
258+
, Typeable req
259+
, ShowX resp
260+
, NFDataX resp
261+
, BitPack resp
262+
, Typeable resp
263+
) =>
264+
String ->
265+
Circuit (BiDf dom req resp) (BiDf dom req resp)
266+
traceSignal name = map (Df.traceSignal (name <> "_request")) (Df.traceSignal (name <> "_response"))

bittide-extra/src/Protocols/Df/Extra.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import qualified Clash.Explicit.Prelude as E
1616
import qualified Clash.Explicit.Signal.Delayed as ED
1717
import qualified Clash.Explicit.Signal.Delayed.Extra as ED
1818
import qualified Clash.Signal.Delayed as D
19+
import qualified Data.Maybe as Maybe
1920
import qualified Debug.Trace as Debug
2021
import qualified Protocols.Df as Df
2122

@@ -234,6 +235,39 @@ bypassFifo SNat fifoCircuit = circuit $ \inp -> do
234235
in
235236
(nextState, ((inpAck, fifoOutAck), (out, fifoIn)))
236237

238+
{- | Will stall the next incoming transaction until the `Bool` is `True`. If it becomes `False`
239+
while a transaction is being processed it will not be affected, but the next transaction will
240+
be blocked until it is `True` again.
241+
-}
242+
stallNext ::
243+
forall dom a.
244+
(HiddenClockResetEnable dom, NFDataX a) =>
245+
-- | Blocks when False
246+
Signal dom Bool ->
247+
Circuit (Df dom a) (Df dom a)
248+
stallNext rdyS = circuit $ \req -> do
249+
ckt -< (req, Fwd rdyS)
250+
where
251+
ckt :: Circuit (Df dom a, CSignal dom Bool) (Df dom a)
252+
ckt = Circuit goS
253+
254+
goS ((datS, rdyS'), ack) = ((ackOutS, ()), datOutS)
255+
where
256+
(ackOutS, datOutS) = unbundle $ mealy go False $ bundle $ (bundle (datS, rdyS'), ack)
257+
258+
go offering ((dat, rdy), Ack ackIn) = (nextOffering, (ackOut, datOut))
259+
where
260+
passThrough = offering || rdy
261+
datOut
262+
| passThrough = dat
263+
| otherwise = Nothing
264+
265+
nextOffering
266+
| Maybe.isJust dat && passThrough = not ackIn
267+
| otherwise = False
268+
269+
ackOut = Ack (passThrough && ackIn)
270+
237271
{- | `Df` version of `traceShowId`, introduces no state or logic of any form. Only prints when
238272
there is data available on the input side. Prints available data, clock cycle count in the
239273
relevant domain, and the corresponding Ack.

bittide-extra/tests/unittests/Main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Test.Tasty.Hedgehog (HedgehogTestLimit (..))
1212

1313
import qualified Tests.Clash.Cores.Xilinx.Xpm.Cdc.Extra
1414
import qualified Tests.Numeric.Extra
15+
import qualified Tests.Protocols.BiDf
1516
import qualified Tests.Protocols.Df.Extra
1617

1718
setDefaultHedgehogTestLimit :: HedgehogTestLimit -> HedgehogTestLimit
@@ -24,6 +25,7 @@ tests =
2425
"tests"
2526
[ Tests.Numeric.Extra.tests
2627
, Tests.Clash.Cores.Xilinx.Xpm.Cdc.Extra.tests
28+
, Tests.Protocols.BiDf.tests
2729
, Tests.Protocols.Df.Extra.tests
2830
]
2931

0 commit comments

Comments
 (0)