RTL module inputs / outputs

I just wanted to follow up on our August 5th discussion on inputs / outputs in RTL dialect modules. To refresh everyone’s memory, a few snippets from the meeting notes:

Aug 5, 2020

  • Introductions from a few new people!
  • Continue discussion about directionality in MLIR type system.

[ … ]

  • Should EmitVerilog.cpp support FIRRTL bundle types?
    • This would be really great!
    • How do we handle bidirectional bundle? Do we emit them as inout ports, do we split them into two unidirectional pieces?
    • inout is problematic in many tools, so we cannot necessarily just lower to them.
  • Should RTL dialect support flip?
    • Stephen’s perspective: RTL is a very logically pure thing that supports synchronous circuits; doesn’t support connect.

[ … ]

  • Current proposal is to have a rather generic type of ‘module’ operation that:
      1. has a graph region, and can be instantiated in graph regions
      1. primarily uses operation operands to represent module inputs and operation results to represent module outputs
      1. Also support ‘inout’ operands marked with an attribute that are accessed through ‘connect’-like operation
  • Alternatively, the RTL dialect could have a restricted module operation that supports 1+2, but not 3. The Verilog dialect would then have a different, less restricted module.
  • Open question: Does Verilog dialect module concept want a graph region, or an SSACFG region?

Did we ever finalize this decision? I would prefer the “alternative” option where rtl.module has its arguments represent inputs, its results as outputs, and no support for inout.

I’m not sure that it’s finalized, but RTL modules and instances were added a little while back: https://github.com/llvm/circt/pull/76. The current implementation isn’t exactly the “alternative” option you mentioned, but it’s close. I think the main difference is right now both module inputs and outputs are specified in the operation’s operands, rather than using the operation’s results for outputs.

Yeah, I saw that. The downside of that approach is that we then have to use rtl.connect operations to wire them up, which I think is totally unnecessary. It is the way it’s done in SystemVerilog and (I think) the FIRRTL dialect, but I agree with @stephenneuendorffer in that the rtl dialect shouldn’t use have to use connect.

I agree, and I know @clattner mentioned the same in the PR comments. Not sure if anyone is planning on following that up though. @amaleewilson were you going to look into that?

I’m not necessarily against the suggestion that ‘connect’ exist in the dialect solely to support in/out connections, as long as ‘inputs’ and ‘outputs’ don’t have to use it and transformations don’t have to support it. I expect a common canonicalization will be to convert in/out connections to explicit inputs and outputs and don’t see a good reason to force this to happen outside of the regular ‘RTL’ dialect.

I see @clattner’s comment in that PR:

lattner on Sep 5


This is fine as a first step, but I’d love to see the rtl.instance op take inputs as arguments and produce outputs as results. ‘inout’ should be handled like inputs for rtl.instance.

Does this mean that we should eliminate output as being valid on an argument? I’m in favor of that – why provide two different ways to do something.

Also, I think if a direction isn’t specified for an argument it should default to input. I see inout as not being used often.

It would be great to move outputs strictly to the results of rtl.instance, and keep the arguments for the inputs/inouts.

Actually this inout business keeps on bothering me for LLHD as well. The only thing that makes inout port special is that you can drive a value onto it, but then get a different value back due to drive conflict resolution:

llhd.entity @foo (%sig : "inout" l12$) {
    %0 = llhd.const 42 : l12  # lN = nine-valued logic
    %1 = llhd.time 1ns : time
    llhd.drv %sig, %0 after %1 : (l12$, l12, time)
    %2 = llhd.prb %sig : l12$
    # %2 can read back as 0xXXX instead of 42 due

I think we could model this behaviour by allocating an argument and a result for inout signals in the LLHD and RTL dialects. The result is what you actually drive, and the argument shows you what the final value on the wire is after drive conflict resolution. We could then have a llhd.resolve_conflicts or rtl.resolve_conflicts op which takes a bunch of driven values as arguments, and provides the final resolved value of the wire, and add that to the graph where appropriate (i.e. wherever we connect inouts). That would allow the RTL dialect to get by without any direction annotation. Any thoughts?

I’m starting to think that the only thing that distinguishes the Structural LLHD and RTL dialects will be prb/drv versus args/results. Which is awesome, because we can share almost all ops and infrastructure, and conversion between the two should be trivial. Then the chain would generally be SV -> Behav. LLHD -> Struct. LLHD -> RTL for synthesizable code, and just SV -> Behav. LLHD for testbenches.

Why do we have to support inout? As far as I can tell (or remember) the only (common) use case is for IO pins, which really only occur at the edge of designs and are very low-level. I don’t think it’s important that we model them.

Are there other relatively common use cases? Are they common enough to justify the contortions we’re going through to support inout? Should we remove it for now and if we find a compelling use case add it (or something like it) back once we understand the use case?

Here is the use case:

  1. If we want to instantiate a foreign module (Black Box) inside CIRCT / RTL component.
  2. Block Box has Verilog implementation with inout ports.
  3. Instance is nested deep into CIRCT / RTL hierarchy.
  4. It is CIRCT / RTL task to wire this inout port to the top or other Black Box as inout

Unless you property handle inout ports, how would you do that?

Personally: I wouldn’t :slight_smile: I think I would prefer to use the Verilog dialect to do that, or an IP instantiation dialect. That said: I don’t feel particularly wedded to this: I think that most transformations on the RTL dialect could simply ignore such inout wires and not change them. If you want to include them interesting optimizations, I think they should be canonicalized to separate inputs and outputs first. As long as such signals are only used to connect to black boxes, I don’t think they will get in the way too much.

+1. I would prefer not having to look at an attribute and ignore it in transformation passes.

Why does that module need an inout? What does it need to connect to? There could be a variety of reasons but I would think that most likely it needs to control an external pin. In this case, (if it needs to be in the RTL dialect) I would have some sort of compile-time-side-effectful Op to connect up through the module hierarchy. The SystemVerilog dialect would just have to scan for that operation and plumb it up. (Having to plumb global signals around is one of my pet peeves with SV.) This solution could actually work for “interior” connections as well. I think there are a bunch of different ways to skin this cat depending on the specific requirements.

Hi all,

Sorry for the delay, I fell behind on this thread. Responding to a couple of upthread questions:

Yes, completely agreed. I’d love to move outputs to be “result like”. At that point, you just have input and inout left, so we can just use a nullary “is inout” attribute to optimize for the normal case.

I do think it is important to support inout in the RTL dialect, particularly because directionality is only a hint in verilog. Being able to model connects seems really important here. I guess we could move rtl.connect to SV dialect if there were a reason to do so though. @stephenneuendorffer do you think there is a good reason to do that?

I see what you’re going for, but I don’t think this will compose well. This would be awkward because we’ll want to be able to associate these together, and we’ll want to produce inout semantics in verilog directly. Also, when we start talking about higher level types (e.g. the stuff in the firrtl system that can include flips etc) we’ll want to talk about an aggregate that is bidirectional, but which can desugar/lower into smaller units that have strict directionality. Being able to represent the bidirectional thing as one value is useful/important for this.


Yeah that’s actually a good point. Now coming to think of it, it might also be desirable to do this in LLHD. As in, keeping the exact port layout, including port directions.