CIRCT 23.0.0git
Loading...
Searching...
No Matches
FirMemLowering.cpp
Go to the documentation of this file.
1//===- FirMemLowering.cpp - FirMem lowering utilities ---------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "FirMemLowering.h"
11#include "circt/Support/Path.h"
12#include "mlir/IR/Threading.h"
13#include "llvm/ADT/MapVector.h"
14#include "llvm/Support/Debug.h"
15#include "llvm/Support/Path.h"
16
17using namespace circt;
18using namespace hw;
19using namespace seq;
20using llvm::MapVector;
21
22#define DEBUG_TYPE "lower-seq-firmem"
23
24/// Return the lowest common ancestor directory of all memory ops provided.
25static Attribute computeCommonOutputFile(ArrayRef<seq::FirMemOp> memOps) {
26 auto getDirectory = [](seq::FirMemOp op) -> StringRef {
27 if (auto file = op->getAttrOfType<hw::OutputFileAttr>("output_file"))
28 return file.getDirectory();
29 return "";
30 };
31
32 SmallString<64> commonDir(getDirectory(memOps.front()));
33 for (auto memOp : memOps.drop_front()) {
34 if (commonDir.empty())
35 break;
36 makeCommonDirectoryPrefix(commonDir, getDirectory(memOp));
37 }
38
39 if (commonDir.empty())
40 return {};
41 return hw::OutputFileAttr::getAsDirectory(memOps.front()->getContext(),
42 commonDir);
43}
44
46 : context(circuit.getContext()), circuit(circuit) {
49
50 // For each module, assign an index. Use it to identify the insertion point
51 // for the generated ops.
52 for (auto [index, module] : llvm::enumerate(circuit.getOps<HWModuleOp>()))
53 moduleIndex[module] = index;
54}
55
56/// Collect the memories in a list of HW modules.
58FirMemLowering::collectMemories(ArrayRef<HWModuleOp> modules) {
59 // For each module in the list populate a separate vector of `FirMemOp`s in
60 // that module. This allows for the traversal of the HW modules to be
61 // parallelized.
62 using ModuleMemories = SmallVector<std::pair<FirMemConfig, FirMemOp>, 0>;
63 SmallVector<ModuleMemories> memories(modules.size());
64
65 mlir::parallelFor(context, 0, modules.size(), [&](auto idx) {
66 // TODO: Check if this module is in the DUT hierarchy.
67 // bool isInDut = state.isInDUT(module);
68 HWModuleOp(modules[idx]).walk([&](seq::FirMemOp op) {
69 memories[idx].push_back({collectMemory(op), op});
70 });
71 });
72
73 // Group the gathered memories by unique `FirMemConfig` details.
75 for (auto [module, moduleMemories] : llvm::zip(modules, memories))
76 for (auto [summary, memOp] : moduleMemories)
77 grouped[summary].push_back(memOp);
78
79 return grouped;
80}
81
82/// Trace a value through wires to its original definition.
83static Value lookThroughWires(Value value) {
84 while (value) {
85 if (auto wireOp = value.getDefiningOp<WireOp>()) {
86 value = wireOp.getInput();
87 continue;
88 }
89 break;
90 }
91 return value;
92}
93
94/// Determine the exact parametrization of the memory that should be generated
95/// for a given `FirMemOp`.
97 FirMemConfig cfg;
98 cfg.dataWidth = op.getType().getWidth();
99 cfg.depth = op.getType().getDepth();
100 cfg.readLatency = op.getReadLatency();
101 cfg.writeLatency = op.getWriteLatency();
102 cfg.maskBits = op.getType().getMaskWidth().value_or(1);
103 cfg.readUnderWrite = op.getRuw();
104 cfg.writeUnderWrite = op.getWuw();
105 if (auto init = op.getInitAttr()) {
106 cfg.initFilename = init.getFilename();
107 cfg.initIsBinary = init.getIsBinary();
108 cfg.initIsInline = init.getIsInline();
109 }
110 cfg.outputFile = op.getOutputFileAttr();
111 if (auto prefix = op.getPrefixAttr())
112 cfg.prefix = prefix.getValue();
113 // TODO: Handle modName (maybe not?)
114 // TODO: Handle groupID (maybe not?)
115
116 // Count the read, write, and read-write ports, and identify the clocks
117 // driving the write ports.
119 for (auto *user : op->getUsers()) {
120 if (isa<FirMemReadOp>(user))
121 ++cfg.numReadPorts;
122 else if (isa<FirMemWriteOp>(user))
123 ++cfg.numWritePorts;
124 else if (isa<FirMemReadWriteOp>(user))
125 ++cfg.numReadWritePorts;
126
127 // Assign IDs to the values used as clock. This allows later passes to
128 // easily detect which clocks are effectively driven by the same value.
129 if (isa<FirMemWriteOp, FirMemReadWriteOp>(user)) {
130 auto clock = lookThroughWires(user->getOperand(2));
131 cfg.writeClockIDs.push_back(
132 clockValues.insert({clock, clockValues.size()}).first->second);
133 }
134 }
135
136 return cfg;
137}
138
140 if (!schemaOp) {
141 // Create or re-use the generator schema.
142 for (auto op : circuit.getOps<hw::HWGeneratorSchemaOp>()) {
143 if (op.getDescriptor() == "FIRRTL_Memory") {
144 schemaOp = op;
145 break;
146 }
147 }
148 if (!schemaOp) {
149 auto builder = OpBuilder::atBlockBegin(circuit.getBody());
150 std::array<StringRef, 14> schemaFields = {
151 "depth", "numReadPorts",
152 "numWritePorts", "numReadWritePorts",
153 "readLatency", "writeLatency",
154 "width", "maskGran",
155 "readUnderWrite", "writeUnderWrite",
156 "writeClockIDs", "initFilename",
157 "initIsBinary", "initIsInline"};
158 schemaOp = hw::HWGeneratorSchemaOp::create(
159 builder, circuit.getLoc(), "FIRRTLMem", "FIRRTL_Memory",
160 builder.getStrArrayAttr(schemaFields));
161 }
162 }
163 return FlatSymbolRefAttr::get(schemaOp);
164}
165
166/// Create the `HWModuleGeneratedOp` for a single memory parametrization.
167HWModuleGeneratedOp
169 ArrayRef<seq::FirMemOp> memOps) {
170 auto schemaSymRef = getOrCreateSchema();
171
172 // Identify the first module which uses the memory configuration.
173 // Insert the generated module before it.
174 HWModuleOp insertPt;
175 for (auto memOp : memOps) {
176 auto parent = memOp->getParentOfType<HWModuleOp>();
177 if (!insertPt || moduleIndex[parent] < moduleIndex[insertPt])
178 insertPt = parent;
179 }
180
181 OpBuilder builder(context);
182 builder.setInsertionPoint(insertPt);
183
184 // Pick a name for the memory. Honor the optional prefix and try to include
185 // the common part of the names of the memory instances that use this
186 // configuration. The resulting name is of the form:
187 //
188 // <prefix>_<commonName>_<depth>x<width>
189 //
190 StringRef baseName = "";
191 bool firstFound = false;
192 for (auto memOp : memOps) {
193 if (auto memName = memOp.getName()) {
194 if (!firstFound) {
195 baseName = *memName;
196 firstFound = true;
197 continue;
198 }
199 unsigned idx = 0;
200 for (; idx < memName->size() && idx < baseName.size(); ++idx)
201 if ((*memName)[idx] != baseName[idx])
202 break;
203 baseName = baseName.take_front(idx);
204 }
205 }
206 baseName = baseName.rtrim('_');
207
208 SmallString<32> nameBuffer;
209 nameBuffer += mem.prefix;
210 if (!baseName.empty()) {
211 nameBuffer += baseName;
212 } else {
213 nameBuffer += "mem";
214 }
215 nameBuffer += "_";
216 (Twine(mem.depth) + "x" + Twine(mem.dataWidth)).toVector(nameBuffer);
217 auto name = builder.getStringAttr(globalNamespace.newName(nameBuffer));
218
219 LLVM_DEBUG(llvm::dbgs() << "Creating " << name << " for " << mem.depth
220 << " x " << mem.dataWidth << " memory\n");
221
222 bool withMask = mem.maskBits > 1;
223 SmallVector<hw::PortInfo> ports;
224
225 // Common types used for memory ports.
226 Type clkType = ClockType::get(context);
227 Type bitType = IntegerType::get(context, 1);
228 Type dataType = IntegerType::get(context, std::max((size_t)1, mem.dataWidth));
229 Type maskType = IntegerType::get(context, mem.maskBits);
230 Type addrType =
231 IntegerType::get(context, std::max(1U, llvm::Log2_64_Ceil(mem.depth)));
232
233 // Helper to add an input port.
234 size_t inputIdx = 0;
235 auto addInput = [&](StringRef prefix, size_t idx, StringRef suffix,
236 Type type) {
237 ports.push_back({{builder.getStringAttr(prefix + Twine(idx) + suffix), type,
238 ModulePort::Direction::Input},
239 inputIdx++});
240 };
241
242 // Helper to add an output port.
243 size_t outputIdx = 0;
244 auto addOutput = [&](StringRef prefix, size_t idx, StringRef suffix,
245 Type type) {
246 ports.push_back({{builder.getStringAttr(prefix + Twine(idx) + suffix), type,
247 ModulePort::Direction::Output},
248 outputIdx++});
249 };
250
251 // Helper to add the ports common to read, read-write, and write ports.
252 auto addCommonPorts = [&](StringRef prefix, size_t idx) {
253 addInput(prefix, idx, "_addr", addrType);
254 addInput(prefix, idx, "_en", bitType);
255 addInput(prefix, idx, "_clk", clkType);
256 };
257
258 // Add the read ports.
259 for (size_t i = 0, e = mem.numReadPorts; i != e; ++i) {
260 addCommonPorts("R", i);
261 addOutput("R", i, "_data", dataType);
262 }
263
264 // Add the read-write ports.
265 for (size_t i = 0, e = mem.numReadWritePorts; i != e; ++i) {
266 addCommonPorts("RW", i);
267 addInput("RW", i, "_wmode", bitType);
268 addInput("RW", i, "_wdata", dataType);
269 addOutput("RW", i, "_rdata", dataType);
270 if (withMask)
271 addInput("RW", i, "_wmask", maskType);
272 }
273
274 // Add the write ports.
275 for (size_t i = 0, e = mem.numWritePorts; i != e; ++i) {
276 addCommonPorts("W", i);
277 addInput("W", i, "_data", dataType);
278 if (withMask)
279 addInput("W", i, "_mask", maskType);
280 }
281
282 // Mask granularity is the number of data bits that each mask bit can
283 // guard. By default it is equal to the data bitwidth.
284 auto genAttr = [&](StringRef name, Attribute attr) {
285 return builder.getNamedAttr(name, attr);
286 };
287 auto genAttrUI32 = [&](StringRef name, uint32_t value) {
288 return genAttr(name, builder.getUI32IntegerAttr(value));
289 };
290 NamedAttribute genAttrs[] = {
291 genAttr("depth", builder.getI64IntegerAttr(mem.depth)),
292 genAttrUI32("numReadPorts", mem.numReadPorts),
293 genAttrUI32("numWritePorts", mem.numWritePorts),
294 genAttrUI32("numReadWritePorts", mem.numReadWritePorts),
295 genAttrUI32("readLatency", mem.readLatency),
296 genAttrUI32("writeLatency", mem.writeLatency),
297 genAttrUI32("width", mem.dataWidth),
298 genAttrUI32("maskGran", mem.dataWidth / mem.maskBits),
299 genAttr("readUnderWrite",
300 seq::RUWAttr::get(builder.getContext(), mem.readUnderWrite)),
301 genAttr("writeUnderWrite",
302 seq::WUWAttr::get(builder.getContext(), mem.writeUnderWrite)),
303 genAttr("writeClockIDs", builder.getI32ArrayAttr(mem.writeClockIDs)),
304 genAttr("initFilename", builder.getStringAttr(mem.initFilename)),
305 genAttr("initIsBinary", builder.getBoolAttr(mem.initIsBinary)),
306 genAttr("initIsInline", builder.getBoolAttr(mem.initIsInline))};
307
308 // Combine the locations of all actual `FirMemOp`s to be the location of the
309 // generated memory.
310 Location loc = FirMemOp(memOps.front()).getLoc();
311 if (memOps.size() > 1) {
312 SmallVector<Location> locs;
313 for (auto memOp : memOps)
314 locs.push_back(memOp.getLoc());
315 loc = FusedLoc::get(context, locs);
316 }
317
318 // Create the module.
319 auto genOp =
320 hw::HWModuleGeneratedOp::create(builder, loc, schemaSymRef, name, ports,
321 StringRef{}, ArrayAttr{}, genAttrs);
322
323 // Put the memory in the lowest common ancestor directory.
324 if (auto outputFile = computeCommonOutputFile(memOps))
325 genOp->setAttr("output_file", outputFile);
326
327 return genOp;
328}
329
330/// Replace all `FirMemOp`s in an HW module with an instance of the
331/// corresponding generated module.
333 HWModuleOp module,
334 ArrayRef<std::tuple<FirMemConfig *, HWModuleGeneratedOp, FirMemOp>> mems) {
335 LLVM_DEBUG(llvm::dbgs() << "Lowering " << mems.size() << " memories in "
336 << module.getName() << "\n");
337
338 DenseMap<unsigned, Value> constOneOps;
339 auto constOne = [&](unsigned width = 1) {
340 auto it = constOneOps.try_emplace(width, Value{});
341 if (it.second) {
342 auto builder = OpBuilder::atBlockBegin(module.getBodyBlock());
343 it.first->second = hw::ConstantOp::create(
344 builder, module.getLoc(), builder.getIntegerType(width), 1);
345 }
346 return it.first->second;
347 };
348 auto valueOrOne = [&](Value value, unsigned width = 1) {
349 return value ? value : constOne(width);
350 };
351
352 for (auto [config, genOp, memOp] : mems) {
353 LLVM_DEBUG(llvm::dbgs() << "- Lowering " << memOp.getName() << "\n");
354 SmallVector<Value> inputs;
355 SmallVector<Value> outputs;
356
357 auto addInput = [&](Value value) { inputs.push_back(value); };
358 auto addOutput = [&](Value value) { outputs.push_back(value); };
359
360 // Add the read ports.
361 for (auto *op : memOp->getUsers()) {
362 auto port = dyn_cast<FirMemReadOp>(op);
363 if (!port)
364 continue;
365 addInput(port.getAddress());
366 addInput(valueOrOne(port.getEnable()));
367 addInput(port.getClk());
368 addOutput(port.getData());
369 }
370
371 // Add the read-write ports.
372 for (auto *op : memOp->getUsers()) {
373 auto port = dyn_cast<FirMemReadWriteOp>(op);
374 if (!port)
375 continue;
376 addInput(port.getAddress());
377 addInput(valueOrOne(port.getEnable()));
378 addInput(port.getClk());
379 addInput(port.getMode());
380 addInput(port.getWriteData());
381 addOutput(port.getReadData());
382 if (config->maskBits > 1)
383 addInput(valueOrOne(port.getMask(), config->maskBits));
384 }
385
386 // Add the write ports.
387 for (auto *op : memOp->getUsers()) {
388 auto port = dyn_cast<FirMemWriteOp>(op);
389 if (!port)
390 continue;
391 addInput(port.getAddress());
392 addInput(valueOrOne(port.getEnable()));
393 addInput(port.getClk());
394 addInput(port.getData());
395 if (config->maskBits > 1)
396 addInput(valueOrOne(port.getMask(), config->maskBits));
397 }
398
399 // Create the module instance.
400 StringRef memName = "mem";
401 if (auto name = memOp.getName(); name && !name->empty())
402 memName = *name;
403 ImplicitLocOpBuilder builder(memOp.getLoc(), memOp);
404 auto instOp = hw::InstanceOp::create(
405 builder, genOp, builder.getStringAttr(memName + "_ext"), inputs,
406 ArrayAttr{}, memOp.getInnerSymAttr());
407 for (auto [oldOutput, newOutput] : llvm::zip(outputs, instOp.getResults()))
408 oldOutput.replaceAllUsesWith(newOutput);
409
410 // Carry attributes over from the `FirMemOp` to the `InstanceOp`.
411 auto defaultAttrNames = memOp.getAttributeNames();
412 for (auto namedAttr : memOp->getAttrs())
413 if (!llvm::is_contained(defaultAttrNames, namedAttr.getName()))
414 instOp->setAttr(namedAttr.getName(), namedAttr.getValue());
415
416 // Get rid of the `FirMemOp`.
417 for (auto *user : llvm::make_early_inc_range(memOp->getUsers()))
418 user->erase();
419 memOp.erase();
420 }
421}
static std::unique_ptr< Context > context
static Attribute computeCommonOutputFile(ArrayRef< seq::FirMemOp > memOps)
Return the lowest common ancestor directory of all memory ops provided.
static Value lookThroughWires(Value value)
Trace a value through wires to its original definition.
static std::vector< mlir::Value > toVector(mlir::ValueRange range)
UniqueConfigs collectMemories(ArrayRef< hw::HWModuleOp > modules)
Groups memories by their kind from the whole design.
void lowerMemoriesInModule(hw::HWModuleOp module, ArrayRef< MemoryConfig > mems)
Lowers a group of memories from the same module.
hw::HWGeneratorSchemaOp schemaOp
FirMemLowering(ModuleOp circuit)
hw::HWModuleGeneratedOp createMemoryModule(FirMemConfig &mem, ArrayRef< seq::FirMemOp > memOps)
Creates the generated module for a given configuration.
FlatSymbolRefAttr getOrCreateSchema()
Find the schema or create it if it does not exist.
DenseMap< hw::HWModuleOp, size_t > moduleIndex
FirMemConfig collectMemory(seq::FirMemOp op)
Determine the exact parametrization of the memory that should be generated for a given FirMemOp.
void add(mlir::ModuleOp module)
Definition Namespace.h:48
StringRef newName(const Twine &name)
Return a unique name, derived from the input name, and add the new name to the internal namespace.
Definition Namespace.h:87
void addDefinitions(mlir::Operation *top)
Populate the symbol cache with all symbol-defining operations within the 'top' operation.
Definition SymCache.cpp:23
create(data_type, value)
Definition hw.py:433
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
void makeCommonDirectoryPrefix(llvm::SmallVectorImpl< char > &a, StringRef b)
Truncate a in place to the longest common directory prefix of a and b, ensuring that the result ends ...
Definition Path.cpp:36
Definition hw.py:1
Definition seq.py:1
The configuration of a FIR memory.
SmallVector< int32_t, 1 > writeClockIDs