CIRCT 23.0.0git
Loading...
Searching...
No Matches
dma.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5from __future__ import annotations
6
7from pycde.common import Clock, Input, InputChannel, OutputChannel, Reset
8from pycde.constructs import ControlReg, Mux, NamedWire, Wire
9from pycde.module import modparams, generator
10from pycde.types import Bits, Channel, StructType, Type, UInt
11from pycde.support import clog2
12from pycde import esi
13
14
15@modparams
16def OneItemBuffersToHost(client_type: Type):
17 """Create a simple, non-performant DMA-based channel communication. Protocol:
18
19 1) Host sends address of buffer address via MMIO write.
20 2) Device writes data on channel with a byte '1' to said buffer address.
21 3) Host polls the last byte in buffer for '1'.
22 4) Data is copied out of buffer, last byte is set to '0', goto 1.
23
24 Future improvement: support more than one buffer at once."""
25
26 class OneItemBuffersToHost(esi.EngineModule):
27
28 @property
29 def TypeName(self):
30 return "OneItemBuffersToHost"
31
32 clk = Clock()
33 rst = Reset()
34 # The channel whose messages we are sending to the host.
35 input_channel = InputChannel(client_type)
36
37 # Since we cannot produce service requests (shortcoming of the ESI
38 # compiler), the module (usually a service implementation) must issue the
39 # service requests for us then connect the input ports.
40 mmio = Input(esi.MMIO.read_write.type)
41 # Compute the byte-aligned width of client_data. The DMA protocol
42 # requires the valid flag to be the last byte of the transfer, which
43 # only works when client_data is byte-aligned.
44 client_data_bitwidth = client_type.bitwidth
45 client_data_padded_bits = ((client_data_bitwidth + 7) // 8) * 8
46 client_data_needs_pad = (client_data_padded_bits != client_data_bitwidth)
47 # When padding is needed, widen client_data to Bits so the struct is
48 # byte-aligned. Otherwise keep the original type.
49 client_data_wire_type = Bits(
50 client_data_padded_bits) if client_data_needs_pad else client_type
51 xfer_data_type = StructType([("valid", Bits(8)),
52 ("client_data", client_data_wire_type)])
53 hostmem_write = Input(esi.HostMem.write_req_bundle_type(xfer_data_type))
54
55 @generator
56 def build(ports):
57 clk = ports.clk
58 rst = ports.rst
59
60 # Set up the MMIO interface to receive the buffer locations.
61 mmio_resp_chan = Wire(Channel(Bits(64)))
62 mmio_rw = ports.mmio
63 mmio_cmd_chan_raw = mmio_rw.unpack(data=mmio_resp_chan)['cmd']
64 mmio_cmd_chan, mmio_cmd_fork_resp = mmio_cmd_chan_raw.fork(clk, rst)
65
66 # Create a response channel which always responds with 0.
67 mmio_resp_data = NamedWire(Bits(64)(0), "mmio_resp_data")
68 mmio_resp_chan.assign(
69 mmio_cmd_fork_resp.transform(lambda _: mmio_resp_data))
70
71 # Create a mailbox for each register. Overkill for one register, but may
72 # be useful later on.
73 _, _, mmio_cmd = mmio_cmd_chan.snoop()
74 num_sinks = 2
75 mmio_offset_words = NamedWire((mmio_cmd.offset.as_bits()[3:]).as_uint(),
76 "mmio_offset_words")
77 addr_above = mmio_offset_words >= UInt(32)(num_sinks)
78 addr_is_zero = mmio_offset_words == UInt(32)(0)
79 force_to_null = NamedWire(addr_above | ~addr_is_zero | mmio_cmd.write,
80 "force_to_null")
81 cmd_sink_sel = Mux(force_to_null,
82 Bits(clog2(num_sinks))(0),
83 mmio_offset_words.as_bits()[:clog2(num_sinks)])
84 mmio_data_only_chan = mmio_cmd_chan.transform(lambda m: m.data)
85 mailbox_names = ["null", "buffer_loc"]
86 demuxed = esi.ChannelDemux(mmio_data_only_chan, cmd_sink_sel, num_sinks)
87 mailbox_mod = esi.Mailbox(Bits(64))
88 mailboxes = [
89 mailbox_mod(clk=clk,
90 rst=rst,
91 input=c,
92 instance_name="mailbox_" + name)
93 for name, c in zip(mailbox_names, demuxed)
94 ]
95 [_, buffer_loc] = mailboxes
96
97 # Join the buffer location channel with the input channel and send a write
98 # message to hostmem.
99 next_buffer_loc_chan = buffer_loc.output
100 hostwr_type = esi.HostMem.write_req_channel_type(
101 OneItemBuffersToHost.xfer_data_type)
102 hostwr_joined = Channel.join(next_buffer_loc_chan, ports.input_channel)
103
104 def _pad_client_data(raw):
105 """Widen the client data to byte-aligned Bits for the DMA struct."""
106 if not OneItemBuffersToHost.client_data_needs_pad:
107 return raw
108 # bitcast to Bits of original width, then zero-extend.
109 return raw.bitcast(Bits(
110 OneItemBuffersToHost.client_data_bitwidth)).pad_or_truncate(
111 OneItemBuffersToHost.client_data_padded_bits)
112
113 hostwr = hostwr_joined.transform(lambda joined: hostwr_type({
114 "address": joined.a.as_uint(),
115 "tag": 0,
116 "data": {
117 "valid": 1,
118 "client_data": _pad_client_data(joined.b)
119 },
120 }))
121 ports.hostmem_write.unpack(req=hostwr)
122
123 return OneItemBuffersToHost
124
125
126def OneItemBuffersFromHost(client_type: Type):
127 """Create a simple, non-performant DMA-base from host communication channel.
128
129 1) Host sends address of buffer address via MMIO write to register 0x08.
130 2) Host sends address of completion address via MMIO write to register 0x10.
131 3) Device reads data from said buffer and sends down the channel. Only keeps
132 one transfer outstanding by buffering the read response locally before
133 forwarding it.
134 4) Device writes '1' to the first byte of the completion buffer to signal that
135 the transfer is done.
136 """
137
138 class OneItemBuffersFromHost(esi.EngineModule):
139
140 @property
141 def TypeName(self):
142 return "OneItemBuffersFromHost"
143
144 clk = Clock()
145 rst = Reset()
146
147 # The channel whose messages we are sending to the host.
148 output_channel = OutputChannel(client_type)
149
150 # Since we cannot produce service requests (shortcoming of the ESI
151 # compiler), the module (usually a service implementation) must issue the
152 # service requests for us then connect the input ports.
153 mmio = Input(esi.MMIO.read_write.type)
154 xfer_data_type = Bits(8)
155 hostmem_write = Input(esi.HostMem.write_req_bundle_type(xfer_data_type))
156 hostmem_read = Input(esi.HostMem.read_bundle_type(client_type))
157
158 @generator
159 def build(ports):
160 clk = ports.clk
161 rst = ports.rst
162
163 # Set up the MMIO interface to receive the buffer locations.
164 mmio_resp_chan = Wire(Channel(Bits(64)))
165 mmio_rw = ports.mmio
166 mmio_cmd_chan_raw = mmio_rw.unpack(data=mmio_resp_chan)['cmd']
167 mmio_cmd_chan, mmio_cmd_fork_resp = mmio_cmd_chan_raw.fork(clk, rst)
168
169 # Create a response channel which always responds with 0.
170 mmio_resp_data = NamedWire(Bits(64)(0), "mmio_resp_data")
171 mmio_resp_chan.assign(
172 mmio_cmd_fork_resp.transform(lambda _: mmio_resp_data))
173
174 # Route the MMIO command
175 _, _, mmio_cmd = mmio_cmd_chan.snoop()
176 mailbox_names = ["null", "buffer_loc", "completion_addr"]
177 num_sinks = len(mailbox_names)
178 mmio_offset_words = NamedWire((mmio_cmd.offset.as_bits()[3:]).as_uint(),
179 "mmio_offset_words")
180 addr_above = mmio_offset_words >= UInt(32)(num_sinks)
181 addr_is_zero = mmio_offset_words == UInt(32)(0)
182 force_to_null = NamedWire(addr_above | ~addr_is_zero | mmio_cmd.write,
183 "force_to_null")
184 cmd_sink_sel = Mux(force_to_null,
185 Bits(clog2(num_sinks))(0),
186 mmio_offset_words.as_bits()[:clog2(num_sinks)])
187 mmio_data_only_chan = mmio_cmd_chan.transform(lambda m: m.data)
188 demuxed = esi.ChannelDemux(mmio_data_only_chan, cmd_sink_sel, num_sinks)
189 # Mailbox is intentionally always-ready and overwrite-on-write: if
190 # software updates either address too early, the new value replaces the
191 # stale one instead of backpressuring MMIO.
192 mailbox_mod = esi.Mailbox(Bits(64))
193 mailboxes = [
194 mailbox_mod(clk=clk,
195 rst=rst,
196 input=c,
197 instance_name="mailbox_" + name)
198 for name, c in zip(mailbox_names, demuxed)
199 ]
200 [_, buffer_loc, completion_loc] = mailboxes
201 buffer_loc_for_read = buffer_loc.output
202
203 output_chan = Wire(Channel(client_type))
204 ports.output_channel = output_chan
205 output_buf_ready = Wire(Bits(1), name="output_buf_ready")
206
207 # Only keep one outstanding transfer. Once we can buffer a response
208 # locally, issue the host read even if the downstream consumer has not
209 # raised ready yet.
210 read_req_accept = Wire(Bits(1), name="read_req_accept")
211 buffer_loc_data, buffer_loc_valid = buffer_loc_for_read.unwrap(
212 read_req_accept)
213 read_req_valid = NamedWire(buffer_loc_valid & output_buf_ready,
214 "read_req_valid")
215 read_req, read_req_ready = Channel(esi.HostMem.ReadReqType).wrap(
216 esi.HostMem.ReadReqType({
217 "address": buffer_loc_data.as_uint(),
218 "tag": 0,
219 }), read_req_valid)
220 read_req_xact = NamedWire(read_req_valid & read_req_ready,
221 "read_req_xact")
222 read_req_accept.assign(read_req_xact)
223
224 # Buffer the read response locally so forwarding the data and issuing the
225 # completion write do not both need the shared HostMem write path in the
226 # same cycle.
227 read_resp = ports.hostmem_read.unpack(req=read_req)['resp']
228 output_ready = Wire(Bits(1), name="output_ready")
229 output_valid_reset = Wire(Bits(1), name="output_valid_reset")
230 read_resp_ready = Wire(Bits(1), name="read_resp_ready")
231 read_resp_msg, read_resp_valid = read_resp.unwrap(read_resp_ready)
232 read_resp_xact = NamedWire(read_resp_valid & read_resp_ready,
233 "read_resp_xact")
234 output_valid = ControlReg(
235 clk=clk,
236 rst=rst,
237 asserts=[read_resp_xact],
238 resets=[output_valid_reset],
239 name="output_valid",
240 )
241 output_data = read_resp_msg.data.reg(clk=clk,
242 rst=rst,
243 ce=read_resp_xact,
244 name="output_data")
245 output_buf_ready.assign((~output_valid | output_ready).as_bits())
246 read_resp_ready.assign(output_buf_ready)
247 output_valid_reset.assign(output_valid & output_ready)
248 output_buf_chan, output_chan_ready = Channel(client_type).wrap(
249 output_data, output_valid)
250 output_chan.assign(output_buf_chan)
251 output_ready.assign(output_chan_ready)
252
253 # Once the data has been read into the local buffer, issue the completion
254 # write independently of when the downstream consumer accepts that data.
255 completion_pending_reset = Wire(Bits(1), name="completion_pending_reset")
256 completion_pending = ControlReg(
257 clk=clk,
258 rst=rst,
259 asserts=[read_resp_xact],
260 resets=[completion_pending_reset],
261 name="completion_pending",
262 )
263
264 write_ch_type = esi.HostMem.write_req_channel_type(
265 OneItemBuffersFromHost.xfer_data_type)
266 completion_loc_ready = Wire(Bits(1), name="completion_loc_ready")
267 completion_loc_data, completion_loc_valid = completion_loc.output.unwrap(
268 completion_loc_ready)
269 completion_write_valid = NamedWire(
270 completion_loc_valid & completion_pending, "completion_write_valid")
271 write_done_chan, completion_write_ready = Channel(write_ch_type).wrap(
272 write_ch_type({
273 "address": completion_loc_data.as_uint(),
274 "tag": 0,
275 "data": Bits(8)(1)
276 }), completion_write_valid)
277 completion_write_xact = NamedWire(
278 completion_write_valid & completion_write_ready,
279 "completion_write_xact")
280 completion_loc_ready.assign(completion_write_xact)
281 completion_pending_reset.assign(completion_write_xact)
282
283 # Unpack the write port, but ignore the write confirmation.
284 ports.hostmem_write.unpack(req=write_done_chan)
285
286 return OneItemBuffersFromHost
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))