enjoy-digital / litex

Build your hardware, easily!
Other
2.98k stars 565 forks source link

yosys+nextpnr toolchain support #1174

Closed suarezvictor closed 2 years ago

suarezvictor commented 2 years ago

Done synthesis with yosys+nextpnr on a Xilinx Artix-7 FPGA (no vivado, no symbiflow). Tested with a FemtoRV CPU at 50MHz. It still requires manual removal of unised FD primitives intances in the generated verilog (the ones giving a warning in Vivado[1]), and some small tweaking on the soc generating script posted below.

command used: ./digilent_arty_basic.py --cpu-type=femtorv --build --toolchain=yosys+nextpnr

Here's the makefile generator

#
# This file is part of LiteX.
#
# Copyright (c) 2020 Antmicro <www.antmicro.com>
# Copyright (c) 2020 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2022 Victor Suarez Rovere <suarezvictor@gmail.com>
# SPDX-License-Identifier: BSD-2-Clause

import os
import subprocess
import sys
import math
from typing import NamedTuple, Union, List
import re
from shutil import which

from migen.fhdl.structure import _Fragment, wrap, Constant
from migen.fhdl.specials import Instance

from litex.build.generic_platform import *
from litex.build.xilinx.vivado import _xdc_separator, _format_xdc, _build_xdc
from litex.build import tools
from litex.build.xilinx import common

def _unwrap(value):
    return value.value if isinstance(value, Constant) else value

# Makefile -----------------------------------------------------------------------------------------

class _MakefileGenerator:
    class Var(NamedTuple):
        name: str
        value: Union[str, List[str]] = ""

    class Rule(NamedTuple):
        target: str
        prerequisites: List[str] = []
        commands: List[str] = []
        phony: bool = False

    def __init__(self, ast):
        self.ast = ast

    def generate(self):
        makefile = []
        for entry in self.ast:
            if isinstance(entry, str):
                makefile.append(entry)
            elif isinstance(entry, self.Var):
                if not entry.value:
                    makefile.append(f"{entry.name} :=")
                elif isinstance(entry.value, list):
                    indent = " " * (len(entry.name) + len(" := "))
                    line = f"{entry.name} := {entry.value[0]}"
                    for value in entry.value[1:]:
                        line += " \\"
                        makefile.append(line)
                        line = indent + value
                    makefile.append(line)
                elif isinstance(entry.value, str):
                    makefile.append(f"{entry.name} := {entry.value}")
                else:
                    raise
            elif isinstance(entry, self.Rule):
                makefile.append("")
                if entry.phony:
                    makefile.append(f".PHONY: {entry.target}")
                makefile.append(" ".join([f"{entry.target}:", *entry.prerequisites]))
                for cmd in entry.commands:
                    makefile.append(f"\t{cmd}")

        return "\n".join(makefile)

def _run_make():
    make_cmd = ["make", "-j1"]

    if which("nextpnr-xilinx") is None:
        msg = "Unable to find Yosys+Nextpnr toolchain, please:\n"
        msg += "- Add Yosys and Nextpnr tools to your $PATH."
        raise OSError(msg)

    if tools.subprocess_call_filtered(make_cmd, common.colors) != 0:
        raise OSError("Error occured during yosys or nextpnr script execution.")

# YosysNextpnrToolchain -------------------------------------------------------------------------------

class YosysNextpnrToolchain:
    attr_translate = {
        #"keep":            ("dont_touch", "true"),
        #"no_retiming":     ("dont_touch", "true"),
        #"async_reg":       ("async_reg",  "true"),
        #"mr_ff":           ("mr_ff",      "true"), # user-defined attribute
        #"ars_ff1":         ("ars_ff1",    "true"), # user-defined attribute
        #"ars_ff2":         ("ars_ff2",    "true"), # user-defined attribute
        #"no_shreg_extract": None
    }

    def __init__(self):
        self.clocks = dict()
        self.false_paths = set()
        self.symbiflow_device = None
        self.bitstream_device = None
        self._partname = None

    def _check_properties(self, platform):
        if not self.symbiflow_device:
            try:
                self.symbiflow_device = {
                    # FIXME: fine for now since only a few devices are supported, do more clever device re-mapping.
                    "xc7a35ticsg324-1L" : "xc7a35t",
                    "xc7a100tcsg324-1"  : "xc7a35t",
                }[platform.device]
            except KeyError:
                raise ValueError(f"symbiflow_device is not specified")
        if not self.bitstream_device:
            try:
                # bitstream_device points to a directory in prjxray database
                # available bitstream_devices: artix7, kintex7, zynq7
                self.bitstream_device = {
                    "xc7a": "artix7", # xc7a35t, xc7a50t, xc7a100t, xc7a200t
                }[platform.device[:4]]
            except KeyError:
                raise ValueError(f"Unsupported device: {platform.device}")
        # FIXME: prjxray-db doesn't have xc7a35ticsg324-1L - use closest replacement
        self._partname = {
            "xc7a35ticsg324-1L" : "xc7a35tcsg324-1",
            "xc7a100tcsg324-1"  : "xc7a100tcsg324-1",
            "xc7a200t-sbg484-1" : "xc7a200tsbg484-1",
        }.get(platform.device, platform.device)

    def _generate_makefile(self, platform, build_name):
        Var = _MakefileGenerator.Var
        Rule = _MakefileGenerator.Rule

        makefile = _MakefileGenerator([
            "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n",
            Var("TOP", build_name),
            Var("PARTNAME", self._partname),
            Var("DEVICE", self.symbiflow_device),
            Var("BITSTREAM_DEVICE", self.bitstream_device),
            "",
            Var("DB_DIR", "/usr/share/nextpnr/prjxray-db"), #FIXME: resolve path
            Var("CHIPDB_DIR", "/usr/share/nextpnr/xilinx-chipdb"), #FIXME: resolve path
            "",
            Var("VERILOG", [f for f,language,_ in platform.sources if language in ["verilog", "system_verilog"]]),
            Var("MEM_INIT", [f"{name}" for name in os.listdir() if name.endswith(".init")]),
            Var("SDC", f"{build_name}.sdc"),
            Var("XDC", f"{build_name}.xdc"),
            Var("ARTIFACTS", [
                    "$(TOP).fasm", "$(TOP).frames", 
                    "*.bit", "*.fasm", "*.json", "*.log", "*.rpt",
                    "constraints.place"
                ]),

            Rule("all", ["$(TOP).bit"], phony=True),
            Rule("$(TOP).json", ["$(VERILOG)", "$(MEM_INIT)", "$(XDC)"], commands=[
                    #"symbiflow_synth -t $(TOP) -v $(VERILOG) -d $(BITSTREAM_DEVICE) -p $(PARTNAME) -x $(XDC) > /dev/null"
                    'yosys -p "synth_xilinx -flatten -abc9 -nobram -arch xc7 -top $(TOP); write_json $(TOP).json" $(VERILOG) > /dev/null'
                ]),
            Rule("$(TOP).fasm", ["$(TOP).json"], commands=[
                    #"symbiflow_write_fasm -e $(TOP).eblif -d $(DEVICE) > /dev/null"
                    'nextpnr-xilinx --chipdb $(CHIPDB_DIR)/$(DEVICE).bin --xdc $(XDC) --json $(TOP).json --write $(TOP)_routed.json --fasm $(TOP).fasm > /dev/null'
                ]),
            Rule("$(TOP).frames", ["$(TOP).fasm"], commands=[
                    'fasm2frames.py --part $(PARTNAME) --db-root $(DB_DIR)/$(BITSTREAM_DEVICE) $(TOP).fasm > $(TOP).frames'
                ]),
            Rule("$(TOP).bit", ["$(TOP).frames"], commands=[
                    #"symbiflow_write_bitstream -d $(BITSTREAM_DEVICE) -f $(TOP).fasm -p $(PARTNAME) -b $(TOP).bit > /dev/null"
                    'xc7frames2bit --part_file $(DB_DIR)/$(BITSTREAM_DEVICE)/$(PARTNAME)/part.yaml --part_name $(PARTNAME) --frm_file $(TOP).frames --output_file $(TOP).bit > /dev/null'
                ]),
            Rule("clean", phony=True, commands=[
                    "rm -f $(ARTIFACTS)"
                ]),
        ])

        tools.write_to_file("Makefile", makefile.generate())

    def _build_clock_constraints(self, platform):
        platform.add_platform_command(_xdc_separator("Clock constraints"))
        #for clk, period in sorted(self.clocks.items(), key=lambda x: x[0].duid):
        #    platform.add_platform_command(
        #        "create_clock -period " + str(period) +
        #        " {clk}", clk=clk)
        pass #clock constraints not supported

    def _fix_instance(self, instance):
        pass

    def build(self, platform, fragment,
        build_dir  = "build",
        build_name = "top",
        run        = True,
        enable_xpm = False,
        **kwargs):

        self._check_properties(platform)

        # Create build directory
        os.makedirs(build_dir, exist_ok=True)
        cwd = os.getcwd()
        os.chdir(build_dir)

        # Finalize design
        if not isinstance(fragment, _Fragment):
            fragment = fragment.get_fragment()
        platform.finalize(fragment)

        # toolchain-specific fixes
        for instance in fragment.specials:
            if isinstance(instance, Instance):
                self._fix_instance(instance)

        # Generate timing constraints
        self._build_clock_constraints(platform)

        # Generate verilog
        v_output = platform.get_verilog(fragment, name=build_name, **kwargs)
        named_sc, named_pc = platform.resolve_signals(v_output.ns)
        v_file = build_name + ".v"
        v_output.write(v_file)
        platform.add_source(v_file)

        self._generate_makefile(
            platform   = platform,
            build_name = build_name
        )

        # Generate design constraints
        tools.write_to_file(build_name + ".xdc", _build_xdc(named_sc, named_pc))

        if run:
            _run_make()

        os.chdir(cwd)

        return v_output.ns

    def add_period_constraint(self, platform, clk, period):
        clk.attr.add("keep")
        period = math.floor(period*1e3)/1e3 # round to lowest picosecond
        if clk in self.clocks:
            if period != self.clocks[clk]:
                raise ValueError("Clock already constrained to {:.2f}ns, new constraint to {:.2f}ns"
                    .format(self.clocks[clk], period))
        self.clocks[clk] = period

    def add_false_path_constraint(self, platform, from_, to):
        # FIXME: false path constraints are currently not supported by the symbiflow toolchain
        return

def symbiflow_build_args(parser):
    pass

def symbiflow_build_argdict(args):
    return dict()

and the SoC generator I used for testing

#!/usr/bin/env python3

#
# This file is part of LiteX-Boards.
#
# Copyright (c) 2015-2019 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2020 Antmicro <www.antmicro.com>
# Copyright (c) 2022 Victor Suarez Rovere <suarezvictor@gmail.com>
# SPDX-License-Identifier: BSD-2-Clause

import os
import argparse

from migen import *

from litex_boards.platforms import arty
from litex.build.xilinx.vivado import vivado_build_args, vivado_build_argdict

from litex.soc.cores.clock import *
from litex.soc.integration.soc import SoCRegion
from litex.soc.integration.soc_core import *
from litex.soc.integration.builder import *
from litex.soc.cores.led import LedChaser
from litex.soc.cores.gpio import GPIOTristate

from litedram.modules import MT41K128M16
from litedram.phy import s7ddrphy

from liteeth.phy.mii import LiteEthPHYMII

# CRG ----------------------------------------------------------------------------------------------

class _CRG(Module):
    def __init__(self, platform, sys_clk_freq, with_rst=True):
        self.rst = Signal()
        self.clock_domains.cd_sys       = ClockDomain()
        self.clock_domains.cd_sys4x     = ClockDomain(reset_less=True)
        self.clock_domains.cd_sys4x_dqs = ClockDomain(reset_less=True)
        self.clock_domains.cd_idelay    = ClockDomain()
        self.clock_domains.cd_eth       = ClockDomain()

        # # #

        clk100 = platform.request("clk100")
        rst    = ~platform.request("cpu_reset") if with_rst else 0

        self.submodules.pll = pll = S7PLL(speedgrade=-1)
        self.comb += pll.reset.eq(rst | self.rst)
        pll.register_clkin(clk100, 100e6)
        pll.create_clkout(self.cd_sys,       sys_clk_freq)
        pll.create_clkout(self.cd_sys4x,     4*sys_clk_freq)
        pll.create_clkout(self.cd_sys4x_dqs, 4*sys_clk_freq, phase=90)
        pll.create_clkout(self.cd_idelay,    200e6)
        pll.create_clkout(self.cd_eth,       25e6)
        platform.add_false_path_constraints(self.cd_sys.clk, pll.clkin) # Ignore sys_clk to pll.clkin path created by SoC's rst.

        #self.submodules.idelayctrl = S7IDELAYCTRL(self.cd_idelay)

        self.comb += platform.request("eth_ref_clk").eq(self.cd_eth.clk)

# BaseSoC ------------------------------------------------------------------------------------------

class BaseSoC(SoCCore):
    def __init__(self, variant="a7-35", toolchain="vivado", sys_clk_freq=int(100e6),
                 with_ethernet=False, with_etherbone=False, eth_ip="192.168.1.50",
                 eth_dynamic_ip=False, ident_version=True, with_led_chaser=True, with_jtagbone=True,
                 with_spi_flash=False, with_pmod_gpio=False, **kwargs):
        platform = arty.Platform(variant=variant, toolchain=toolchain, set_properties=False)

        # SoCCore ----------------------------------------------------------------------------------
        SoCCore.__init__(self, platform, sys_clk_freq,
            ident          = "LiteX SoC on Arty A7",
            ident_version  = ident_version,
            **kwargs)

        # CRG --------------------------------------------------------------------------------------
        self.submodules.crg = _CRG(platform, sys_clk_freq)

        # DDR3 SDRAM -------------------------------------------------------------------------------
        if False: #not self.integrated_main_ram_size:
            self.submodules.ddrphy = s7ddrphy.A7DDRPHY(platform.request("ddram"),
                memtype        = "DDR3",
                nphases        = 4,
                sys_clk_freq   = sys_clk_freq)
            self.add_sdram("sdram",
                phy           = self.ddrphy,
                module        = MT41K128M16(sys_clk_freq, "1:4"),
                l2_cache_size = kwargs.get("l2_size", 8192)
            )

        # Ethernet / Etherbone ---------------------------------------------------------------------
        if with_ethernet or with_etherbone:
            self.submodules.ethphy = LiteEthPHYMII(
                clock_pads = self.platform.request("eth_clocks"),
                pads       = self.platform.request("eth"))
            if with_ethernet:
                self.add_ethernet(phy=self.ethphy, dynamic_ip=eth_dynamic_ip)
            if with_etherbone:
                self.add_etherbone(phy=self.ethphy, ip_address=eth_ip)

        # Jtagbone ---------------------------------------------------------------------------------
        if with_jtagbone:
            self.add_jtagbone()

        # SPI Flash --------------------------------------------------------------------------------
        if with_spi_flash:
            from litespi.modules import S25FL128L
            from litespi.opcodes import SpiNorFlashOpCodes as Codes
            self.add_spi_flash(mode="4x", module=S25FL128L(Codes.READ_1_1_4), rate="1:2", with_master=True)

        # Leds -------------------------------------------------------------------------------------
        if with_led_chaser:
            self.submodules.leds = LedChaser(
                pads         = platform.request_all("user_led"),
                sys_clk_freq = sys_clk_freq)

        # GPIOs ------------------------------------------------------------------------------------
        if with_pmod_gpio:
            platform.add_extension(arty.raw_pmod_io("pmoda"))
            self.submodules.gpio = GPIOTristate(platform.request("pmoda"))

# Build --------------------------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(description="LiteX SoC on Arty A7")
    parser.add_argument("--toolchain",           default="vivado",                 help="FPGA toolchain (vivado or symbiflow).")
    parser.add_argument("--build",               action="store_true",              help="Build bitstream.")
    parser.add_argument("--load",                action="store_true",              help="Load bitstream.")
    parser.add_argument("--variant",             default="a7-35",                  help="Board variant (a7-35 or a7-100).")
    parser.add_argument("--sys-clk-freq",        default=50e6,                    help="System clock frequency.")
    ethopts = parser.add_mutually_exclusive_group()
    ethopts.add_argument("--with-ethernet",      action="store_true",              help="Enable Ethernet support.")
    ethopts.add_argument("--with-etherbone",     action="store_true",              help="Enable Etherbone support.")
    parser.add_argument("--eth-ip",              default="192.168.1.50", type=str, help="Ethernet/Etherbone IP address.")
    parser.add_argument("--eth-dynamic-ip",      action="store_true",              help="Enable dynamic Ethernet IP addresses setting.")
    sdopts = parser.add_mutually_exclusive_group()
    sdopts.add_argument("--with-spi-sdcard",     action="store_true",              help="Enable SPI-mode SDCard support.")
    sdopts.add_argument("--with-sdcard",         action="store_true",              help="Enable SDCard support.")
    parser.add_argument("--sdcard-adapter",      type=str,                         help="SDCard PMOD adapter (digilent or numato).")
    parser.add_argument("--no-ident-version",    action="store_false",             help="Disable build time output.")
    parser.add_argument("--with-jtagbone",       action="store_true",              help="Enable JTAGbone support.")
    parser.add_argument("--with-spi-flash",      action="store_true",              help="Enable SPI Flash (MMAPed).")
    parser.add_argument("--with-pmod-gpio",      action="store_true",              help="Enable GPIOs through PMOD.") # FIXME: Temporary test.
    builder_args(parser)
    soc_core_args(parser)
    vivado_build_args(parser)
    args = parser.parse_args()

    assert not (args.with_etherbone and args.eth_dynamic_ip)

    soc = BaseSoC(
        variant        = args.variant,
        toolchain      = args.toolchain,
        sys_clk_freq   = int(float(args.sys_clk_freq)),
        with_ethernet  = args.with_ethernet,
        with_etherbone = args.with_etherbone,
        eth_ip         = args.eth_ip,
        eth_dynamic_ip = args.eth_dynamic_ip,
        ident_version  = args.no_ident_version,
        with_jtagbone  = args.with_jtagbone,
        with_spi_flash = args.with_spi_flash,
        with_pmod_gpio = args.with_pmod_gpio,
        **soc_core_argdict(args)
    )
    if args.sdcard_adapter == "numato":
        soc.platform.add_extension(arty._numato_sdcard_pmod_io)
    else:
        soc.platform.add_extension(arty._sdcard_pmod_io)
    if args.with_spi_sdcard:
        soc.add_spi_sdcard()
    if args.with_sdcard:
        soc.add_sdcard()

    builder = Builder(soc, **builder_argdict(args))
    builder_kwargs = vivado_build_argdict(args) if args.toolchain == "vivado" else {}
    builder.build(**builder_kwargs, run=args.build)

    if args.load:
        prog = soc.platform.create_programmer()
        prog.load_bitstream(os.path.join(builder.gateware_dir, soc.build_name + ".bit"))

if __name__ == "__main__":
    main()

[1] The involved flip-flops are these:

WARNING: [DRC PLHOLDVIO-2] Non-Optimal connections which could lead to hold violations: A LUT clk100_inst is driving clock pin of 8 cells. This could lead to large hold time violations. Involved cells are:
FD, FD_1, FD_2, FD_3, FD_4, FD_5, FD_6, and FD_7
gsomlo commented 2 years ago

Interesting! What version of nextpnr did you use? I didn't notice the "upstream" version (https://github.com/yosyshq/nextpnr) advertising Xilinx support, did I miss anything? @gatecat

I'm packaging nextpnr for Fedora, and would love nothing more than to update to a version that has xilinx support :)

suarezvictor commented 2 years ago

I use this version:

$ nextpnr-xilinx --version
nextpnr-xilinx -- Next Generation Place and Route (Version cd8b15db)

I was following this installation tutorial: https://github.com/BrunoLevy/learn-fpga/blob/master/FemtoRV/TUTORIALS/toolchain_arty.md

And made some tweaks: https://github.com/BrunoLevy/learn-fpga/issues/54

enjoy-digital commented 2 years ago

Thanks @suarezvictor, very interesting. As we did with Symbiflow, we could use the Arty as the experimental board for it and add required tweaks to it if needed. (Once the tweaks are not longer required we could extend support to other boards). Would you mind doing a PR with your work? I could look at avoiding the manual changes you had to do. Having this in place will be a good way to enable users to test it and make fast progress all together.

suarezvictor commented 2 years ago

Yes I'll be quite glad to prepare the PR and join efforts

suarezvictor commented 2 years ago

See PR here, with cleanups: https://github.com/enjoy-digital/litex/pull/1180

suarezvictor commented 2 years ago

RapidWright can be an option to this toolchain too, as used in the following example: https://github.com/gatecat/nextpnr-xilinx/blob/xilinx-upstream/xilinx/examples/blinky/blinky.sh

enjoy-digital commented 2 years ago

We can now close this since https://github.com/enjoy-digital/litex/pull/1180 has been merged. Thanks @suarezvictor.