nmigen-learning/fpga/idea_nmain.md

6.7 KiB

The current nmigen.cli.main() can:

  • Can display a help text when asked (not done by default)
  • Has the ability to elaborate a design
  • Has the ability to simulate the design for x cycles with no input

I've worked out a few use-cases i've seen others have, and ones i've run into myself:

What could we want form a new main()?

  • It should display the help text by default (better discoverability)

  • It should have the ability to elaborate the design

    • Perhaps provide the ability to set design parameters from the CLI
    • Ability to choose among multiple target platforms provided to main()
      • Perhaps even provide a mocked/generic platform instead of None, with arbitrary clocks and resources available
  • It should have the ability to synth+PnR a design and program it

    • No more using platform.build(top, do_program=True) instead of main()
    • It should provide the option to program in the CLI interface, instead of hardcoding it
      • "program" may also differ between platform. Options to just program in RAM or to write to the SPI flash could also be needed
    • Have the option to print the list of files generated
    • It should provide the option to print the synthesis and PnR output, or silence it
    • Have the ability to avoid rebuilding the bitfile, and instead just flash the current one on disk
    • Have the ability to override the default "build/" folder from the CLI
    • Have the ability to set overrides (the kwargs in platform.build)
      • at least print a list of the supported override environ variables
  • Have the ability to run a simulation of the design

    • Perhaps with the ability to chose among multiple simulation processes/functions provided to main()?
  • Give the user the ability to provide binary blobs to be flashed to the SPI flash (see #)

  • It could be extendable with plugins

    • A plugin system could extend all nmigen design, instead of requiring the author to add the plugin
  • Have the ability to run a simulation of the design

    • Perhaps with the ability to chose among multiple simulation processes/functions provided to main()?

Should a new main function be accessible from python?

If you publish your design as a python module, it should be possible to just import it and convert it to verilog/ilang. This culd be useful when using systems like litex. Therefore i suggest the a new main have a pattern like this:

main = nmigen.main(MyDesign(), ...)
if __name__ == "__main__":
    main.run_cli()

, making a main object accessible from within python.

Should a new main function provide an interface to run tests?

Testing is currently done through unittest or pytest. There is however the issue of not immediately knowing which test_*.py file covers the module you're currently looking at.

How does the current "simulate" action differ from tests?

Assert() is currently being added to the simulator (see #427), blurring the line between a pure simulation and a unit test. Should the "simulate" action be renamed? Merged? Leave it as is?

Should we change and break the old main(), or make a new one

Even though the backend API is not declared fully stable yet, breaking old code sucks. The current main could therefore be deprecated and a new one then be added in a different location or under a different name. (nmain is cute)

While on the topic of platform overrides:

The ability to set platform overrides during elaboration could be usefull: PLL module for example would then be able to add the proper timing constraints. (Vivado is able to infer these constraints, but nextpnr do not to my knowledge)

How could such a new main look like?

this mess
class MyModule(Elaboratable):
    def __init__(self, param: int):
        self.param = param
        self.out = Signal()
    ...

# the largest issue is the lack of a consistent CLI interface
# to expect from a given nmigen .py file
if __name__ == "__main__":
    top = MyModule(param=4) # `param` is hardcoded. This may also result in a UnusedElaboratable warning

    if sys.argv[1] = "build":
        if sys.argv[2] = "icebreaker":
            plat = ICEBreakerPlatform()
            plat.add_resources(...)

        elif sys.argv[2] = "fomu"
            plat = FomuHackerPlatform()
            plat.add_resources(...)
        else:
            exit(1)

        # `do_program` is usually hard-coded.
        # The folder "build" should also be configurable from CLI
        plat.build(top, do_program=True)

    elif sys.argv[1] == "test": # "simulate" is already taken by main()
        # logic for multiple simulations to select from is manually made
        sim = Simulator(top)

        def my_proc():
            yield ...
        sim.add_sync_process(my_proc)

        with sim.write_vcd("uart.vcd", "uart.gtkw"): # hardcoded filenames, never printed to user
            sim.run()

    else:
        main(top, ports=[ top.out ]) # '-h' doesn't mention the two modes defined above

could become
class MyModule(Elaboratable):
    def __init__(self, param):
        self.param = param
        self.out = Signal()
    ...

if __name__ == "__main__":
    platforms, simulations = [], [] # using lists makes the nMain interface more transparent

    # supporting multiple platforms encourages designing the
    # top-level module in a platform-agnostic fashion

    @platforms.append
    def icebreaker(): # nMain infers the name from function.__name__
        plat = ICEBreakerPlatform()
        plat.add_resources(...)
        return plat

    @platforms.append
    def fomu():
        plat = FomuHackerPlatform()
        plat.add_resources(...)
        return plat

    @simulations.append
    def sim_the_foo(sim): # sim = Simulator(design)
        sim.add_clock(1e-6)

        @sim.add_sync_process
        def my_proc():
            yield ...
        # `sim` could then be automatically run by nMain, with configurable wavefile output

    nMain(
        name          = "blinker",           # Current default is "top", perhaps default to design.__name__ instead?
        design        = MyModule,            # Pass a class itself instead of an instance, avoids UnusedElaboratable warnings
        design_kwargs = {"param": 4}         # Optional, should be possible to override in CLI
        ports         = lambda x: [ x.out ], # Optional due to platform.request() being a thing.
                                             # Perhaps all Signals defined in __init__ could be inferred as ports
                                             # by inspecting it with dir() and getattr() before calling .elaborate()?
        platforms     = platforms,           # Optional
        simulations   = simulations,         # Optional
    )