Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Right way to express a bundle of Signals? #211

Closed
RobertBaruch opened this issue Sep 19, 2019 · 13 comments
Closed

Right way to express a bundle of Signals? #211

RobertBaruch opened this issue Sep 19, 2019 · 13 comments
Labels

Comments

@RobertBaruch
Copy link

My use case is that for formal verification, I need to define a big bundle of Signals which are used for keeping track of state. This bundle goes to many modules. Some modules will set some of those signals, and other modules will set other signals. Kind of like this:

from nmigen import *
from nmigen.asserts import *
from nmigen.hdl.ast import *
from nmigen.back import pysim


# The "big bundle of Signals"
class EmbeddedThing(object):
    def __init__(self):
        self.thing = Signal(16)
        # Imagine lots and lots more signals here.

    def ports(self):
        return [self.thing]


class ThingUnderTest(Elaboratable):
    def __init__(self, embedded=None):
        self.input = Signal(16)
        self.output = Signal(16)
        self.embedded = embedded

    def ports(self):
        return [self.input, self.output]

    def elaborate(self, platform):
        m = Module()
        m.submodules.bottom = bottom = BottomModule(embedded=embedded)

        m.d.comb += self.output.eq(self.input + bottom.output)

        return m


# This module sets a signal in the big bundle.
class BottomModule(Elaboratable):
    def __init__(self, embedded=None):
        self.output = Signal(16)
        self.embedded = embedded

    def elaborate(self, platform):
        m = Module()

        if self.embedded != None:
            m.d.comb += self.output.eq(1)
            m.d.pos += self.embedded.thing.eq(self.output)
        else:
            m.d.comb += self.output.eq(0)

        return m


if __name__ == "__main__":
    m = Module()

    embedded = EmbeddedThing()
    m.submodules.testmod = testmod = ThingUnderTest(embedded=embedded)

    m.d.comb += testmod.input.eq(0x9876)

    ports = testmod.ports()
    ports.extend(embedded.ports())

    with pysim.Simulator(
            m, vcd_file=open("top_thing.vcd", "w"), traces=ports) as sim:
        sim.add_clock(1e-9, domain="pos")

        def process():
            # initial reset
            yield

        sim.add_sync_process(process(), domain="pos")
        sim.run()

This is fine, but in top_thing.vcd, EmbeddedThing's thing signal ends up listed under the bottom module. I can only imagine that's because the first time thing appears in the LHS in the AST is in bottom.

bottom

Rather than have EmbeddedThing's signals scattered all over the various modules that happen to set them, I'd prefer to have the signals all in one place, preferably top, which is where the signals were defined.

So (a) am I defining a bundle of Signals correctly and using them correctly, and (b) how do I get the signals all in one place?

@whitequark
Copy link
Contributor

Note that passing traces to Simulator does absolutely nothing unless you also pass gtkw_file.

Should your EmbeddedThing not be a Record?

@whitequark
Copy link
Contributor

whitequark commented Sep 19, 2019

I can only imagine that's because the first time thing appears in the LHS in the AST is in bottom.

This is essentially correct.

Rather than have EmbeddedThing's signals scattered all over the various modules that happen to set them, I'd prefer to have the signals all in one place, preferably top, which is where the signals were defined.

I'm afraid there's no way to do this, because there is no concept of "a module/fragment where the signal was defined" in nMigen. There can't be, for that matter! There's no link between "the constructor where Signal was called" and "the fragment that elaboratable ended up as".

But, if you pass gtkw_file, then you'll get all of your signals pre-populated in the set of traces if you open the .gtkw file instead of the .vcd file, since you're passing traces with all of them collected.

@RobertBaruch
Copy link
Author

Should your EmbeddedThing not be a Record?

Couldn't find a way to define an array of Records in a Record.

@whitequark
Copy link
Contributor

Oh yeah, that's not currently a thing. (Maybe it should be.)

@RobertBaruch
Copy link
Author

RobertBaruch commented Sep 19, 2019

But, if you pass gtkw_file, then you'll get all of your signals pre-populated in the set of traces if you open the .gtkw file instead of the .vcd file, since you're passing traces with all of them collected.

Hmm, well, what I'm writing is a CPU, and there will be lots and lots and lots of signals that I generally don't want to see. It would likely be just as hard to find the signals I'm interested in among all those other signals.

Is there maybe a different thing I should be doing? Maybe making a module holding the formal signals instead and have an interface that sets those signals rather than setting the signals themselves in the various modules making up the CPU?

@whitequark
Copy link
Contributor

Is there maybe a different thing I should be doing? Maybe making a module holding the formal signals instead and have an interface that sets those signals rather than setting the signals themselves in the various modules making up the CPU?

This seems like a decent workaround to me.

@RobertBaruch
Copy link
Author

Will try it, thanks!

@RobertBaruch
Copy link
Author

Can we get examples of the use of Record, including connect?

@whitequark
Copy link
Contributor

@RobertBaruch Have you looked at examples/basic/gpio.py and nmigen/test/test_hdl_rec.py? If that's not sufficient I'll expand the examples.

@RobertBaruch
Copy link
Author

RobertBaruch commented Sep 20, 2019

They don't seem to explain how connect is used. The basic use of Record seems clear -- it's a bundle of Signals. But there is a significant amount of code having to do with directionality, much of which is used in connect.

From inspection, it seems that connect wires up a Record to bits and pieces of other Records, possibly or-ing them together. I'd definitely appreciate an example or two for connect. I mean, sure, I could stumble around and figure it out, eventually. But an example would go a long way :)

@RobertBaruch
Copy link
Author

Ah, I think I have a self-contained example.

from nmigen import *
from nmigen.cli import main
from nmigen.asserts import *
from nmigen.hdl.rec import *

cableLayout = Layout([
    ("data", 8, DIR_FANIN),
    ("select1", 1, DIR_FANOUT),
    ("select2", 1, DIR_FANOUT),
])


class CableBundle(Record):
    def __init__(self):
        super().__init__(cableLayout)


class Memory(Elaboratable):
    def __init__(self, selection, value):
        self.cable = CableBundle()

        self.selection = selection
        self.value = value

    def elaborate(self, platform):
        m = Module()

        if self.selection == 1:
            selector = self.cable.select1
        else:
            selector = self.cable.select2

        with m.If(selector):
            m.d.comb += self.cable.data.eq(self.value)
        with m.Else():
            m.d.comb += self.cable.data.eq(0)

        return m


class HelloRecords(Elaboratable):
    def __init__(self):
        self.cable = CableBundle()
        self.data = Signal(8)

    def elaborate(self, platform):
        m = Module()

        m.submodules.mem1 = mem1 = Memory(1, 0x42)
        m.submodules.mem2 = mem2 = Memory(2, 0xBA)

        m.d.comb += self.cable.connect(mem1.cable, mem2.cable)
        m.d.comb += self.data.eq(self.cable.data)

        return m


from nmigen.back import pysim
from nmigen.hdl.ast import Tick

if __name__ == "__main__":
    m = Module()
    sync = ClockDomain()

    m.submodules.hello = hello = HelloRecords()
    m.domains.sync = sync

    main(m, ports=[hello.data, hello.cable.select1, hello.cable.select2])

    with pysim.Simulator(m, vcd_file=open("hello_records.vcd", "w")) as sim:
        sim.add_clock(1e-9)

        def process():
            yield hello.cable.select1.eq(0)
            yield hello.cable.select2.eq(0)
            yield Tick()
            yield hello.cable.select1.eq(1)
            yield Tick()
            yield hello.cable.select1.eq(0)
            yield hello.cable.select2.eq(0)
            yield Tick()
            yield hello.cable.select2.eq(1)
            yield Tick()
            yield hello.cable.select1.eq(0)
            yield hello.cable.select2.eq(0)
            yield Tick()

        sim.add_process(process())
        sim.run()

@whitequark
Copy link
Contributor

But there is a significant amount of code having to do with directionality, much of which is used in connect.

Gotcha. I'll make sure to improve that documentation. The basic idea is that... imagine you are wiring up a bunch of control/status registers, for example. They would all be on a single bus, and each endpoint bus would be represented with a Record:

csr_layout = Layout([
    ("addr",   16, DIR_FANOUT),
    ("r_data", 32, DIR_FANIN),
    ("r_en",   1,  DIR_FANOUT),
    ("w_data", 32, DIR_FANOUT),
    ("w_en",   1,  DIR_FANOUT),
])


class Register(Elaboratable):
    def __init__(self, addr):
        self.bus  = Record(csr_layout)
        self.addr = addr
        self.data = Signal(32)

    def elaborate(self, platform):
        m = Module()
        with m.If(self.bus.addr == self.addr):
            with m.If(self.bus.r_en):
                m.d.comb += self.bus.r_data.eq(self.data)
            with m.If(self.bus.w_en):
                m.d.sync += self.data.eq(self.bus.w_data)
        return m


class CPU(Elaboratable):
    def __init__(self, regsters):
        self.registers = registers

    def elaborate(self, platform):
        csr_bus = Record(csr_layout)

        m = Module()
        m.d.comb += [
            csr_bus.connect(reg.bus for reg in self.registers),
            csr_bus.addr.eq(...),
            ...
        ]

In this case, address and control signals are routed out to every register unmodified; write data is also routed to every register; and read data is routed from every register using a tree of OR'd signals. This maps nicely to a LUT-based architecture because it is cheaper than using multiplexers. However, it requires each device on the bus to keep every DIR_FANIN field "undriven" (that is, at zero), which is fortunately the default reset value, so simply not doing any combinatorial assignment to the bus endpoint does the trick.

@RobertBaruch
Copy link
Author

Got it, makes sense. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants