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