|
| 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")) |
0 commit comments