#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0-or-later

#  Copyright (C) 2015 Robert Jordens <jordens@gmail.com>

import unittest

import migen as mg
import migen.build.generic_platform as mb
from migen.genlib import io
from migen.build import xilinx


"""
This migen script produces proxy bitstreams to allow programming SPI flashes
behind FPGAs.

Bitstream binaries built with this script are available at:
https://github.com/jordens/bscan_spi_bitstreams

A JTAG2SPI transfer consists of:

1. an arbitrary number of 0 bits (from BYPASS registers in front of the
   JTAG2SPI DR)
2. a marker bit (1) indicating the start of the JTAG2SPI transaction
3. 32 bits (big endian) describing the length of the SPI transaction
4. a number of SPI clock cycles (corresponding to 3.) with CS_N asserted
5. an arbitrary number of cycles (to shift MISO/TDO data through subsequent
   BYPASS registers)

Notes:

* The JTAG2SPI DR is 1 bit long (due to different sampling edges of
  {MISO,MOSI}/{TDO,TDI}).
* MOSI is TDI with half a cycle delay.
* TDO is MISO with half a cycle delay.
* CAPTURE-DR needs to be performed before SHIFT-DR on the BYPASSed TAPs in
  JTAG chain to clear the BYPASS registers to 0.

https://github.com/m-labs/migen
"""


class JTAG2SPI(mg.Module):
    def __init__(self, spi=None, bits=32):
        self.jtag = mg.Record([
            ("sel", 1),
            ("shift", 1),
            ("capture", 1),
            ("tck", 1),
            ("tdi", 1),
            ("tdo", 1),
        ])
        self.cs_n = mg.TSTriple()
        self.clk = mg.TSTriple()
        self.mosi = mg.TSTriple()
        self.miso = mg.TSTriple()

        # # #

        self.cs_n.o.reset = mg.Constant(1)
        self.mosi.o.reset_less = True
        bits = mg.Signal(bits, reset_less=True)
        head = mg.Signal(max=len(bits), reset=len(bits) - 1)
        self.clock_domains.cd_sys = mg.ClockDomain()
        self.submodules.fsm = mg.FSM("IDLE")
        if spi is not None:
            self.specials += [
                    self.cs_n.get_tristate(spi.cs_n),
                    self.mosi.get_tristate(spi.mosi),
                    self.miso.get_tristate(spi.miso),
            ]
            if hasattr(spi, "clk"):  # 7 Series drive it fixed
                self.specials += self.clk.get_tristate(spi.clk)
                # self.specials += io.DDROutput(1, 0, spi.clk, self.clk.o)
        self.comb += [
                self.cd_sys.rst.eq(self.jtag.sel & self.jtag.capture),
                self.cd_sys.clk.eq(self.jtag.tck),
                self.cs_n.oe.eq(self.jtag.sel),
                self.clk.oe.eq(self.jtag.sel),
                self.mosi.oe.eq(self.jtag.sel),
                self.miso.oe.eq(0),
                # Do not suppress CLK toggles outside CS_N asserted.
                # Xilinx USRCCLK0 requires three dummy cycles to do anything
                # https://www.xilinx.com/support/answers/52626.html
                # This is fine since CS_N changes only on falling CLK.
                self.clk.o.eq(~self.jtag.tck),
                self.jtag.tdo.eq(self.miso.i),
        ]
        # Latency calculation (in half cycles):
        # 0 (falling TCK, rising CLK):
        #   JTAG adapter: set TDI
        # 1 (rising TCK, falling CLK):
        #   JTAG2SPI: sample TDI -> set MOSI
        #   SPI: set MISO
        # 2 (falling TCK, rising CLK):
        #   SPI: sample MOSI
        #   JTAG2SPI (BSCAN primitive): sample MISO -> set TDO
        # 3 (rising TCK, falling CLK):
        #   JTAG adapter: sample TDO
        self.fsm.act("IDLE",
                mg.If(self.jtag.tdi & self.jtag.sel & self.jtag.shift,
                    mg.NextState("HEAD")
                )
        )
        self.fsm.act("HEAD",
                mg.If(head == 0,
                    mg.NextState("XFER")
                )
        )
        self.fsm.act("XFER",
                mg.If(bits == 0,
                    mg.NextState("IDLE")
                ),
        )
        self.sync += [
                self.mosi.o.eq(self.jtag.tdi),
                self.cs_n.o.eq(~self.fsm.ongoing("XFER")),
                mg.If(self.fsm.ongoing("HEAD"),
                    bits.eq(mg.Cat(self.jtag.tdi, bits)),
                    head.eq(head - 1)
                ),
                mg.If(self.fsm.ongoing("XFER"),
                    bits.eq(bits - 1)
                )
        ]


class JTAG2SPITest(unittest.TestCase):
    def setUp(self):
        self.bits = 8
        self.dut = JTAG2SPI(bits=self.bits)

    def test_instantiate(self):
        pass

    def test_initial_conditions(self):
        def check():
            yield
            self.assertEqual((yield self.dut.cs_n.oe), 0)
            self.assertEqual((yield self.dut.mosi.oe), 0)
            self.assertEqual((yield self.dut.miso.oe), 0)
            self.assertEqual((yield self.dut.clk.oe), 0)
        mg.run_simulation(self.dut, check())

    def test_enable(self):
        def check():
            yield self.dut.jtag.sel.eq(1)
            yield self.dut.jtag.shift.eq(1)
            yield
            self.assertEqual((yield self.dut.cs_n.oe), 1)
            self.assertEqual((yield self.dut.mosi.oe), 1)
            self.assertEqual((yield self.dut.miso.oe), 0)
            self.assertEqual((yield self.dut.clk.oe), 1)
        mg.run_simulation(self.dut, check())

    def run_seq(self, tdi, tdo, spi=None):
        yield self.dut.jtag.sel.eq(1)
        yield
        yield self.dut.jtag.shift.eq(1)
        for di in tdi:
            yield self.dut.jtag.tdi.eq(di)
            yield
            tdo.append((yield self.dut.jtag.tdo))
            if spi is not None:
                v = []
                for k in "cs_n clk mosi miso".split():
                    t = getattr(self.dut, k)
                    v.append("{}>".format((yield t.o)) if (yield t.oe)
                            else "<{}".format((yield t.i)))
                spi.append(" ".join(v))
        yield self.dut.jtag.sel.eq(0)
        yield
        yield self.dut.jtag.shift.eq(0)
        yield

    def test_shift(self):
        bits = 8
        data = 0x81
        tdi = [0, 0, 1]  # dummy from BYPASS TAPs and marker
        tdi += [((bits - 1) >> j) & 1 for j in range(self.bits - 1, -1, -1)]
        tdi += [(data >> j) & 1 for j in range(bits)]
        tdi += [0, 0, 0, 0]  # dummy from BYPASS TAPs
        tdo = []
        spi = []
        mg.run_simulation(self.dut, self.run_seq(tdi, tdo, spi))
        # print(tdo)
        for l in spi:
            print(l)


class Spartan3(mg.Module):
    macro = "BSCAN_SPARTAN3"
    toolchain = "ise"

    def __init__(self, platform):
        platform.toolchain.bitgen_opt += " -g compress -g UnusedPin:Pullup"
        self.submodules.j2s = j2s = JTAG2SPI(platform.request("spiflash"))
        self.specials += [
                mg.Instance(
                    self.macro,
                    o_SHIFT=j2s.jtag.shift, o_SEL1=j2s.jtag.sel,
                    o_CAPTURE=j2s.jtag.capture,
                    o_DRCK1=j2s.jtag.tck,
                    o_TDI=j2s.jtag.tdi, i_TDO1=j2s.jtag.tdo,
                    i_TDO2=0),
        ]
        platform.add_period_constraint(j2s.jtag.tck, 6)


class Spartan3A(Spartan3):
    macro = "BSCAN_SPARTAN3A"


class Spartan6(mg.Module):
    toolchain = "ise"

    def __init__(self, platform):
        platform.toolchain.bitgen_opt += " -g compress -g UnusedPin:Pullup"
        self.submodules.j2s = j2s = JTAG2SPI(platform.request("spiflash"))
        # clk = mg.Signal()
        self.specials += [
                mg.Instance(
                    "BSCAN_SPARTAN6", p_JTAG_CHAIN=1,
                    o_SHIFT=j2s.jtag.shift, o_SEL=j2s.jtag.sel,
                    o_CAPTURE=j2s.jtag.capture,
                    o_DRCK=j2s.jtag.tck,
                    o_TDI=j2s.jtag.tdi, i_TDO=j2s.jtag.tdo),
                # mg.Instance("BUFG", i_I=clk, o_O=j2s.jtag.tck)
        ]
        platform.add_period_constraint(j2s.jtag.tck, 6)


class Series7(mg.Module):
    toolchain = "vivado"

    def __init__(self, platform):
        platform.toolchain.bitstream_commands.extend([
            "set_property BITSTREAM.GENERAL.COMPRESS True [current_design]",
            "set_property BITSTREAM.CONFIG.UNUSEDPIN Pullnone [current_design]"
        ])
        self.submodules.j2s = j2s = JTAG2SPI(platform.request("spiflash"))
        # clk = mg.Signal()
        self.specials += [
                mg.Instance(
                    "BSCANE2", p_JTAG_CHAIN=1,
                    o_SHIFT=j2s.jtag.shift, o_SEL=j2s.jtag.sel,
                    o_CAPTURE=j2s.jtag.capture,
                    o_DRCK=j2s.jtag.tck,
                    o_TDI=j2s.jtag.tdi, i_TDO=j2s.jtag.tdo),
                mg.Instance(
                    "STARTUPE2", i_CLK=0, i_GSR=0, i_GTS=0,
                    i_KEYCLEARB=0, i_PACK=1,
                    i_USRCCLKO=j2s.clk.o, i_USRCCLKTS=~j2s.clk.oe,
                    i_USRDONEO=1, i_USRDONETS=1),
                # mg.Instance("BUFG", i_I=clk, o_O=j2s.jtag.tck)
        ]
        platform.add_period_constraint(j2s.jtag.tck, 6)
        try:
            self.comb += [
                    platform.request("user_sma_gpio_p").eq(j2s.cs_n.i),
                    platform.request("user_sma_gpio_n").eq(j2s.clk.o),
                    platform.request("user_sma_clock_p").eq(j2s.mosi.o),
                    platform.request("user_sma_clock_n").eq(j2s.miso.i),
            ]
        except mb.ConstraintError:
            pass


class Ultrascale(mg.Module):
    toolchain = "vivado"

    def __init__(self, platform):
        platform.toolchain.bitstream_commands.extend([
            "set_property BITSTREAM.GENERAL.COMPRESS True [current_design]",
            "set_property BITSTREAM.CONFIG.UNUSEDPIN Pullnone [current_design]",
        ])
        self.submodules.j2s0 = j2s0 = JTAG2SPI()
        self.submodules.j2s1 = j2s1 = JTAG2SPI(platform.request("spiflash"))
        di = mg.Signal(4)
        self.comb += mg.Cat(j2s0.mosi.i, j2s0.miso.i).eq(di)
        self.specials += [
                mg.Instance("BSCANE2", p_JTAG_CHAIN=1,
                    o_SHIFT=j2s0.jtag.shift, o_SEL=j2s0.jtag.sel,
                    o_CAPTURE=j2s0.jtag.capture,
                    o_DRCK=j2s0.jtag.tck,
                    o_TDI=j2s0.jtag.tdi, i_TDO=j2s0.jtag.tdo),
                mg.Instance("BSCANE2", p_JTAG_CHAIN=2,
                    o_SHIFT=j2s1.jtag.shift, o_SEL=j2s1.jtag.sel,
                    o_CAPTURE=j2s1.jtag.capture,
                    o_DRCK=j2s1.jtag.tck,
                    o_TDI=j2s1.jtag.tdi, i_TDO=j2s1.jtag.tdo),
                mg.Instance("STARTUPE3", i_GSR=0, i_GTS=0,
                    i_KEYCLEARB=0, i_PACK=1,
                    i_USRDONEO=1, i_USRDONETS=1,
                    i_USRCCLKO=mg.Mux(j2s0.clk.oe, j2s0.clk.o, j2s1.clk.o),
                    i_USRCCLKTS=~(j2s0.clk.oe | j2s1.clk.oe),
                    i_FCSBO=j2s0.cs_n.o, i_FCSBTS=~j2s0.cs_n.oe,
                    o_DI=di,
                    i_DO=mg.Cat(j2s0.mosi.o, j2s0.miso.o, 0, 0),
                    i_DTS=mg.Cat(~j2s0.mosi.oe, ~j2s0.miso.oe, 1, 1))
        ]
        platform.add_period_constraint(j2s0.jtag.tck, 6)
        platform.add_period_constraint(j2s1.jtag.tck, 6)


class XilinxBscanSpi(xilinx.XilinxPlatform):
    packages = {
        # (package-speedgrade, id): [cs_n, clk, mosi, miso, *pullups]
        ("cp132", 1): ["M2", "N12", "N2", "N8"],
        ("fg320", 1): ["U3", "U16", "T4", "N10"],
        ("fg320", 2): ["V3", "U16", "T11", "V16"],
        ("fg484", 1): ["Y4", "AA20", "AB14", "AB20"],
        ("fgg484", 1): ["Y4", "AA20", "AB14", "AB20"],
        ("fgg400", 1): ["Y2", "Y19", "W12", "W18"],
        ("ftg256", 1): ["T2", "R14", "P10", "T14"],
        ("ft256", 1): ["T2", "R14", "P10", "T14"],
        ("fg400", 1): ["Y2", "Y19", "W12", "W18"],
        ("cs484", 1): ["U7", "V17", "V13", "W17"],
        ("qg144-2", 1): ["P38", "P70", "P64", "P65", "P62", "P61"],
        ("cpg196-2", 1): ["P2", "N13", "P11", "N11", "N10", "P10"],
        ("cpg236-1", 1): ["K19", None, "D18", "D19", "G18", "F18"],
        ("csg484-2", 1): ["AB5", "W17", "AB17", "Y17", "V13", "W13"],
        ("csg324-2", 1): ["V3", "R15", "T13", "R13", "T14", "V14"],
        ("csg324-1", 1): ["L13", None, "K17", "K18", "L14", "M14"],
        ("fbg484-1", 1): ["T19", None, "P22", "R22", "P21", "R21"],
        ("fbg484-1", 2): ["L16", None, "H18", "H19", "G18", "F19"],
        ("fbg676-1", 1): ["C23", None, "B24", "A25", "B22", "A22"],
        ("ffg901-1", 1): ["V26", None, "R30", "T30", "R28", "T28"],
        ("ffg900-1", 1): ["U19", None, "P24", "R25", "R20", "R21"],
        ("ffg1156-1", 1): ["V30", None, "AA33", "AA34", "Y33", "Y34"],
        ("ffg1157-1", 1): ["AL33", None, "AN33", "AN34", "AK34", "AL34"],
        ("ffg1158-1", 1): ["C24", None, "A23", "A24", "B26", "A26"],
        ("ffg1926-1", 1): ["AK33", None, "AN34", "AN35", "AJ34", "AK34"],
        ("fhg1761-1", 1): ["AL36", None, "AM36", "AN36", "AJ36", "AJ37"],
        ("flg1155-1", 1): ["AL28", None, "AE28", "AF28", "AJ29", "AJ30"],
        ("flg1932-1", 1): ["V32", None, "T33", "R33", "U31", "T31"],
        ("flg1926-1", 1): ["AK33", None, "AN34", "AN35", "AJ34", "AK34"],

        ("ffva1156-2-e", 1): ["G26", None, "M20", "L20", "R21", "R22"],
        ("ffva1156-2-e", "sayma"): ["K21", None, "M20", "L20", "R21", "R22"],
    }

    pinouts = {
        # bitstreams are named by die, package does not matter, speed grade
        # should not matter.
        #
        # chip: (package, id, standard, class)
        "xc3s100e": ("cp132", 1, "LVCMOS33", Spartan3),
        "xc3s1200e": ("fg320", 1, "LVCMOS33", Spartan3),
        "xc3s1400a": ("fg484", 1, "LVCMOS33", Spartan3A),
        "xc3s1400an": ("fgg484", 1, "LVCMOS33", Spartan3A),
        "xc3s1600e": ("fg320", 1, "LVCMOS33", Spartan3),
        "xc3s200a": ("fg320", 2, "LVCMOS33", Spartan3A),
        "xc3s200an": ("ftg256", 1, "LVCMOS33", Spartan3A),
        "xc3s250e": ("cp132", 1, "LVCMOS33", Spartan3),
        "xc3s400a": ("fg320", 2, "LVCMOS33", Spartan3A),
        "xc3s400an": ("fgg400", 1, "LVCMOS33", Spartan3A),
        "xc3s500e": ("cp132", 1, "LVCMOS33", Spartan3),
        "xc3s50a": ("ft256", 1, "LVCMOS33", Spartan3A),
        "xc3s50an": ("ftg256", 1, "LVCMOS33", Spartan3A),
        "xc3s700a": ("fg400", 1, "LVCMOS33", Spartan3A),
        "xc3s700an": ("fgg484", 1, "LVCMOS33", Spartan3A),
        "xc3sd1800a": ("cs484", 1, "LVCMOS33", Spartan3A),
        "xc3sd3400a": ("cs484", 1, "LVCMOS33", Spartan3A),

        "xc6slx100": ("csg484-2", 1, "LVCMOS33", Spartan6),
        "xc6slx100t": ("csg484-2", 1, "LVCMOS33", Spartan6),
        "xc6slx150": ("csg484-2", 1, "LVCMOS33", Spartan6),
        "xc6slx150t": ("csg484-2", 1, "LVCMOS33", Spartan6),
        "xc6slx16": ("cpg196-2", 1, "LVCMOS33", Spartan6),
        "xc6slx25": ("csg324-2", 1, "LVCMOS33", Spartan6),
        "xc6slx25t": ("csg324-2", 1, "LVCMOS33", Spartan6),
        "xc6slx45": ("csg324-2", 1, "LVCMOS33", Spartan6),
        "xc6slx45t": ("csg324-2", 1, "LVCMOS33", Spartan6),
        "xc6slx4": ("cpg196-2", 1, "LVCMOS33", Spartan6),
        "xc6slx4t": ("qg144-2", 1, "LVCMOS33", Spartan6),
        "xc6slx75": ("csg484-2", 1, "LVCMOS33", Spartan6),
        "xc6slx75t": ("csg484-2", 1, "LVCMOS33", Spartan6),
        "xc6slx9": ("cpg196-2", 1, "LVCMOS33", Spartan6),
        "xc6slx9t": ("qg144-2", 1, "LVCMOS33", Spartan6),

        "xc7a100t": ("csg324-1", 1, "LVCMOS25", Series7),
        "xc7a15t": ("cpg236-1", 1, "LVCMOS25", Series7),
        "xc7a200t": ("fbg484-1", 1, "LVCMOS25", Series7),
        "xc7a35t": ("cpg236-1", 1, "LVCMOS25", Series7),
        "xc7a50t": ("cpg236-1", 1, "LVCMOS25", Series7),
        "xc7a75t": ("csg324-1", 1, "LVCMOS25", Series7),
        "xc7k160t": ("fbg484-1", 2, "LVCMOS25", Series7),
        "xc7k325t": ("fbg676-1", 1, "LVCMOS25", Series7),
        "xc7k325t-debug": ("ffg900-1", 1, "LVCMOS25", Series7),
        "xc7k355t": ("ffg901-1", 1, "LVCMOS25", Series7),
        "xc7k410t": ("fbg676-1", 1, "LVCMOS25", Series7),
        "xc7k420t": ("ffg1156-1", 1, "LVCMOS25", Series7),
        "xc7k480t": ("ffg1156-1", 1, "LVCMOS25", Series7),
        "xc7k70t": ("fbg484-1", 2, "LVCMOS25", Series7),
        "xc7v2000t": ("fhg1761-1", 1, "LVCMOS18", Series7),
        "xc7v585t": ("ffg1157-1", 1, "LVCMOS18", Series7),
        "xc7vh580t": ("flg1155-1", 1, "LVCMOS18", Series7),
        "xc7vh870t": ("flg1932-1", 1, "LVCMOS18", Series7),
        "xc7vx1140t": ("flg1926-1", 1, "LVCMOS18", Series7),
        "xc7vx330t": ("ffg1157-1", 1, "LVCMOS18", Series7),
        "xc7vx415t": ("ffg1157-1", 1, "LVCMOS18", Series7),
        "xc7vx485t": ("ffg1157-1", 1, "LVCMOS18", Series7),
        "xc7vx550t": ("ffg1158-1", 1, "LVCMOS18", Series7),
        "xc7vx690t": ("ffg1157-1", 1, "LVCMOS18", Series7),
        "xc7vx980t": ("ffg1926-1", 1, "LVCMOS18", Series7),

        "xcku040": ("ffva1156-2-e", 1, "LVCMOS18", Ultrascale),
        "xcku040-sayma": ("ffva1156-2-e", "sayma", "LVCMOS18", Ultrascale),
    }

    def __init__(self, device, pins, std, toolchain="ise"):
        ios = [self.make_spi(0, pins, std, toolchain)]
        if device == "xc7k325t-ffg900-1":  # debug
            ios += [
                ("user_sma_clock_p", 0, mb.Pins("L25"), mb.IOStandard("LVCMOS25")),
                ("user_sma_clock_n", 0, mb.Pins("K25"), mb.IOStandard("LVCMOS25")),
                ("user_sma_gpio_p", 0, mb.Pins("Y23"), mb.IOStandard("LVCMOS25")),
                ("user_sma_gpio_n", 0, mb.Pins("Y24"), mb.IOStandard("LVCMOS25")),
            ]
        xilinx.XilinxPlatform.__init__(self, device, ios, toolchain=toolchain)

    @staticmethod
    def make_spi(i, pins, std, toolchain):
        pu = "PULLUP" if toolchain == "ise" else "PULLUP TRUE"
        pd = "PULLDOWN" if toolchain == "ise" else "PULLDOWN TRUE"
        cs_n, clk, mosi, miso = pins[:4]
        io = ["spiflash", i,
            mb.Subsignal("cs_n", mb.Pins(cs_n), mb.Misc(pu)),
            mb.Subsignal("mosi", mb.Pins(mosi), mb.Misc(pu)),
            mb.Subsignal("miso", mb.Pins(miso), mb.Misc(pu)),
            mb.IOStandard(std),
            ]
        if clk:
            io.append(mb.Subsignal("clk", mb.Pins(clk), mb.Misc(pd)))
        for i, p in enumerate(pins[4:]):
            io.append(mb.Subsignal("pullup{}".format(i), mb.Pins(p),
                                mb.Misc(pu)))
        return io

    @classmethod
    def make(cls, target, errors=False):
        pkg, id, std, Top = cls.pinouts[target]
        pins = cls.packages[(pkg, id)]
        device = target.split("-", 1)[0]
        platform = cls("{}-{}".format(device, pkg), pins, std, Top.toolchain)
        top = Top(platform)
        name = "bscan_spi_{}".format(target)
        try:
            platform.build(top, build_name=name)
        except Exception as e:
            print(("ERROR: xilinx_bscan_spi build failed "
                  "for {}: {}").format(target, e))
            if errors:
                raise


if __name__ == "__main__":
    import argparse
    import multiprocessing
    p = argparse.ArgumentParser(description="build bscan_spi bitstreams "
                                "for openocd jtagspi flash driver")
    p.add_argument("device", nargs="*",
                   default=sorted(list(XilinxBscanSpi.pinouts)),
                   help="build for these devices (default: %(default)s)")
    p.add_argument("-p", "--parallel", default=1, type=int,
                   help="number of parallel builds (default: %(default)s)")
    args = p.parse_args()
    pool = multiprocessing.Pool(args.parallel)
    pool.map(XilinxBscanSpi.make, args.device, chunksize=1)
