upsilon/gateware/soc.py

481 lines
20 KiB
Python

"""
##########################################################################
# Portions of this file incorporate code licensed under the
# BSD 2-Clause License.
#
# Copyright (c) 2014-2022 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2013-2014 Sebastien Bourdeauducq <sb@m-labs.hk>
# 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>
# BSD 2-Clause License
#
# Copyright (c) Copyright 2012-2022 Enjoy-Digital.
# Copyright (c) Copyright 2012-2022 / LiteX-Hub community.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
##########################################################################
# Copyright 2023-2024 (C) Peter McGoron
#
# This file is a part of Upsilon, a free and open source software project.
# For license terms, refer to the files in `doc/copying` in the Upsilon
# source distribution.
"""
# There is nothing fundamental about the Arty A7(35|100)T to this
# design, but another eval board will require some porting.
from migen import *
import litex_boards.platforms.digilent_arty as board_spec
from litex.soc.integration.builder import Builder
from litex.build.generic_platform import IOStandard, Pins, Subsignal
from litex.soc.integration.soc_core import SoCCore
from litex.soc.integration.soc import SoCRegion, SoCBusHandler, SoCIORegion
from litex.soc.cores.clock import S7PLL, S7IDELAYCTRL
from litex.soc.interconnect.csr import AutoCSR, Module, CSRStorage, CSRStatus
from litex.soc.interconnect.wishbone import Interface, SRAM, Decoder
from litedram.phy import s7ddrphy
from litedram.modules import MT41K128M16
from litedram.frontend.dma import LiteDRAMDMAReader
from liteeth.phy.mii import LiteEthPHYMII
from util import *
from swic import *
from extio import *
from region import BasicRegion
import json
"""
Keep this diagram up to date! This is the wiring diagram from the ADC to
the named Verilog pins.
Refer to `A7-constraints.xdc` for pin names.
DAC: SS MOSI MISO SCK
0: 1 2 3 4 (PMOD A top, right to left)
1: 1 2 3 4 (PMOD A bottom, right to left)
2: 1 2 3 4 (PMOD B top, right to left)
3: 0 1 2 3 (Analog header)
4: 0 1 2 3 (PMOD C top, right to left)
5: 4 5 6 8 (Analog header)
6: 1 2 3 4 (PMOD D top, right to left)
7: 1 2 3 4 (PMOD D bottom, right to left)
Outer chip header (C=CONV, K=SCK, D=SDO, XX=not connected)
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
C4 K4 D4 C5 K5 D5 XX XX C6 K6 D6 C7 K7 D7 XX XX
C0 K0 D0 C1 K1 D1 XX XX C2 K2 D2 C3 K3 D3
0 1 2 3 4 5 6 7 8 9 10 11 12 13
The `io` list maps hardware pins to names used by the SoC
generator. These pins are then connected to Verilog modules.
If there is more than one pin in the Pins string, the resulting
name will be a vector of pins.
TODO: generate declaratively from constraints file.
"""
io = [
# ("differntial_output_low", 0, Pins("J17 J18 K15 J15 U14 V14 T13 U13 B6 E5 A3"), IOStandard("LVCMOS33")),
("dac_ss_L_0", 0, Pins("G13"), IOStandard("LVCMOS33")),
("dac_mosi_0", 0, Pins("B11"), IOStandard("LVCMOS33")),
("dac_miso_0", 0, Pins("A11"), IOStandard("LVCMOS33")),
("dac_sck_0", 0, Pins("D12"), IOStandard("LVCMOS33")),
# ("dac_ss_L", 0, Pins("G13 D13 E15 F5 U12 D7 D4 E2"), IOStandard("LVCMOS33")),
# ("dac_mosi", 0, Pins("B11 B18 E16 D8 V12 D5 D3 D2"), IOStandard("LVCMOS33")),
# ("dac_miso", 0, Pins("A11 A18 D15 C7 V10 B7 F4 H2"), IOStandard("LVCMOS33")),
# ("dac_sck", 0, Pins("D12 K16 C15 E7 V11 E6 F3 G2"), IOStandard("LVCMOS33")),
("adc_conv_0", 0, Pins("V15"), IOStandard("LVCMOS33")),
("adc_sck_0", 0, Pins("U16"), IOStandard("LVCMOS33")),
("adc_sdo_0", 0, Pins("P14"), IOStandard("LVCMOS33")),
# ("adc_conv", 0, Pins("V15 T11 N15 U18 U11 R10 R16 U17"), IOStandard("LVCMOS33")),
# ("adc_sck", 0, Pins("U16 R12 M16 R17 V16 R11 N16 T18"), IOStandard("LVCMOS33")),
# ("adc_sdo", 0, Pins("P14 T14 V17 P17 M13 R13 N14 R18"), IOStandard("LVCMOS33")),
("module_reset", 0, Pins("D9"), IOStandard("LVCMOS33")),
# ("test_clock", 0, Pins("P18"), IOStandard("LVCMOS33"))
]
# Clock and Reset Generator
# I don't know how this works, I only know that it does.
class _CRG(Module):
def __init__(self, platform, sys_clk_freq, with_dram, rst_pin):
self.rst = Signal()
self.clock_domains.cd_sys = ClockDomain()
self.clock_domains.cd_eth = ClockDomain()
if with_dram:
self.clock_domains.cd_sys4x = ClockDomain()
self.clock_domains.cd_sys4x_dqs = ClockDomain()
self.clock_domains.cd_idelay = ClockDomain()
# Clk/Rst.
clk100 = platform.request("clk100")
rst = ~rst_pin if rst_pin is not None else 0
# PLL.
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_eth, 25e6)
self.comb += platform.request("eth_ref_clk").eq(self.cd_eth.clk)
platform.add_false_path_constraints(self.cd_sys.clk, pll.clkin) # Ignore sys_clk to pll.clkin path created by SoC's rst.
if with_dram:
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)
# IdelayCtrl.
if with_dram:
self.submodules.idelayctrl = S7IDELAYCTRL(self.cd_idelay)
class UpsilonSoC(SoCCore):
def add_ip(self, ip_str, ip_name):
# The IP of the FPGA and the IP of the TFTP server are stored as
# "constants" which turn into preprocessor defines.
# They are IPv4 addresses that are split into octets. So the local
# ip is LOCALIP1, LOCALIP2, etc.
for seg_num, ip_byte in enumerate(ip_str.split('.'),start=1):
self.add_constant(f"{ip_name}{seg_num}", int(ip_byte))
def add_slave_with_registers(self, name, bus, region, registers):
""" Add a bus slave, and also add its registers to the subregions
dictionary. """
self.bus.add_slave(name, bus, region)
self.soc_subregions[name] = registers
def add_preemptive_interface_for_slave(self, name, slave_bus, slave_width, slave_registers, addressing="word"):
""" Add a PreemptiveInterface in front of a Wishbone Slave interface.
:param name: Name of the module and the Wishbone bus region.
:param slave_bus: Instance of Wishbone.Interface.
:param slave_width: Width of the region.
:param slave_registers: Register inside the bus region.
:return: The PI module.
"""
pi = PreemptiveInterface(slave_bus, addressing=addressing, name=name)
self.add_module(name, pi)
self.add_slave_with_registers(name, pi.add_master("main"),
SoCRegion(origin=None, size=slave_width, cached=False),
slave_registers)
def f(csrs):
# CSRs are not case-folded, but Wishbone memory regions are!!
return f'{name} = Register({csrs["csr_registers"][name + "_master_select"]["addr"]})'
self.mmio_closures.append(f)
self.pre_finalize.append(lambda : pi.pre_finalize(name + "_main_PI.json"))
return pi
def add_blockram(self, name, size):
""" Add a blockram module to the system. """
mod = SRAM(size)
self.add_module(name, mod)
pi = self.add_preemptive_interface_for_slave(name + "_PI", mod.bus,
size, None, "word")
def f(csrs):
return f'{name} = FlatArea({csrs["memories"][name.lower() + "_pi"]["base"]}, {size})'
self.mmio_closures.append(f)
return mod, pi
def add_picorv32(self, name, size=0x1000, origin=0x10000, param_origin=0x100000):
""" Add a PicoRV32 core.
:param name: Name of the PicoRV32 module in the Main CPU.
:param size: Size of the PicoRV32 RAM region.
:param origin: Start position of the PicoRV32.
:param param_origin: Origin of the PicoRV32 param region in the PicoRV32
memory.
"""
# Add PicoRV32 core
pico = PicoRV32(name, origin, origin+0x10, param_origin)
self.add_module(name, pico)
# Attach registers to main CPU at pre-finalize time.
def pre_finalize():
pico.params.pre_finalize()
self.add_slave_with_registers(name + "_params", pico.params.firstbus,
SoCRegion(origin=None, size=pico.params.width, cached=False),
pico.params.public_registers)
pico.mmap.add_region("params",
BasicRegion(origin=pico.param_origin, size=pico.params.width, bus=pico.params.secondbus,
registers=pico.params.public_registers))
self.pre_finalize.append(pre_finalize)
# Add a Block RAM for the PicoRV32 toexecute from.
ram, ram_pi = self.add_blockram(name + "_ram", size=size)
# Add this at the end so the Blockram declaration comes before this one
def f(csrs):
param_origin = csrs["memories"][f'{name.lower()}_params']["base"]
return f'{name}_params = RegisterRegion({param_origin}, {pico.params.mmio(param_origin)})\n' \
+ f'{name} = PicoRV32({name}_ram, {name}_params, {name}_ram_PI)'
self.mmio_closures.append(f)
# Allow access from the PicoRV32 to the Block RAM.
pico.mmap.add_region("main",
BasicRegion(origin=origin, size=size, bus=ram_pi.add_master(name)))
def picorv32_add_cl(self, name):
""" Add a register area containing the control loop parameters to the
PicoRV32.
"""
pico = self.get_module(name)
params = pico.add_cl_params()
def picorv32_add_pi(self, name, region_name, pi_name, origin, width, registers):
""" Add a PreemptiveInterface master to a PicoRV32 MemoryMap region.
:param name: Name of the PicoRV32 module.
:param region_name: Name of the region in the PicoRV32 MMAP.
:param pi_name: Name of the PreemptiveInterface module in the main CPU.
:param origin: Origin of the memory region in the PicoRV32.
:param width: Width of the region in the PicoRV32.
:param registers: Registers of the region.
"""
pico = self.get_module(name)
pi = self.get_module(pi_name)
pico.mmap.add_region(region_name,
BasicRegion(origin=origin, size=width,
bus=pi.add_master(name), registers=registers))
def add_spi_master(self, name, **kwargs):
spi = SPIMaster(**kwargs)
self.add_module(name, spi)
pi = self.add_preemptive_interface_for_slave(name + "_PI", spi.bus,
spi.width, spi.public_registers, "byte")
def f(csrs):
wid = kwargs["spi_wid"]
origin = csrs["memories"][name.lower() + "_pi"]['base']
return f'{name} = SPI({wid}, {origin}, {spi.mmio(origin)})'
self.mmio_closures.append(f)
return spi, pi
def add_AD5791(self, name, **kwargs):
""" Adds an AD5791 SPI master to the SoC.
:return: A tuple of the SPI master module and the PI module.
"""
args = SPIMaster.AD5791_PARAMS
args.update(kwargs)
return self.add_spi_master(name, **args)
def add_LT_adc(self, name, **kwargs):
""" Adds a Linear Technologies ADC SPI master to the SoC.
:return: A tuple of the SPI master module and the PI module.
"""
args = SPIMaster.LT_ADC_PARAMS
args.update(kwargs)
args["mosi"] = Signal()
# SPI Master brings ss_L low when converting and keeps it high
# when idle. The ADC is the opposite, so invert the signal here.
conv_high = Signal()
self.comb += conv_high.eq(~kwargs["ss_L"])
return self.add_spi_master(name, **args)
def add_waveform(self, name, ram_len, **kwargs):
kwargs['counter_max_wid'] = minbits(ram_len)
wf = Waveform(**kwargs)
self.add_module(name, wf)
pi = self.add_preemptive_interface_for_slave(name + "_PI",
wf.slavebus, wf.width, wf.public_registers, "byte")
bram, bram_pi = self.add_blockram(name + "_ram", ram_len)
wf.add_ram(bram_pi.add_master(name), ram_len)
def f(csrs):
origin = csrs["memories"][name.lower() + "_pi"]["base"]
return f'{name} = RegisterRegion({origin}, {wf.mmio(origin)})'
self.mmio_closures.append(f)
return wf, pi
def __init__(self,
variant="a7-100",
local_ip="192.168.2.50",
remote_ip="192.168.2.100",
tftp_port=6969):
"""
:param variant: Arty A7 variant. Accepts "a7-35" or "a7-100".
:param local_ip: The IP that the BIOS will use when transmitting.
:param remote_ip: The IP that the BIOS will use when retreving
the Linux kernel via TFTP.
:param tftp_port: Port that the BIOS uses for TFTP.
"""
sys_clk_freq = int(100e6)
platform = board_spec.Platform(variant=variant, toolchain="f4pga")
rst = platform.request("cpu_reset")
self.submodules.crg = _CRG(platform, sys_clk_freq, True, rst)
# The SoC won't know the origins until LiteX sorts out all the
# memory regions, so they go into a dictionary directly instead
# of through MemoryMap.
self.soc_subregions = {}
# The SoC generates a Python module containing information about
# how to access registers from Micropython. This is a list of
# closures that print the code that will be placed in the module.
self.mmio_closures = []
# This is a list of closures that are run "pre-finalize", which
# is before the do_finalize() function is called.
self.pre_finalize = []
"""
These source files need to be sorted so that modules
that rely on another module come later. For instance,
`control_loop` depends on `control_loop_math`, so
control_loop_math.v comes before control_loop.v
If you want to add a new verilog file to the design, look at the
modules that it refers to and place it the files with those modules.
Since Yosys doesn't support modern Verilog, only put preprocessed
(if applicable) files here.
"""
platform.add_source("rtl/picorv32/picorv32.v")
platform.add_source("rtl/spi/spi_master_preprocessed.v")
platform.add_source("rtl/spi/spi_master_ss.v")
platform.add_source("rtl/waveform/waveform.v")
# SoCCore does not have sane defaults (no integrated rom)
SoCCore.__init__(self,
clk_freq=sys_clk_freq,
toolchain="symbiflow",
platform = platform,
bus_standard = "wishbone",
ident = f"Arty-{variant} F4PGA LiteX VexRiscV Zephyr - Upsilon",
bus_data_width = 32,
bus_address_width = 32,
bus_timeout = int(1e6),
cpu_type = "vexriscv_smp",
cpu_count = 1,
cpu_variant="linux",
integrated_rom_size=0x20000,
integrated_sram_size = 0x2000,
csr_data_width=32,
csr_address_width=14,
csr_paging=0x800,
csr_ordering="big",
timer_uptime = True)
# This initializes the connection to the physical DRAM interface.
self.submodules.ddrphy = s7ddrphy.A7DDRPHY(platform.request("ddram"),
memtype = "DDR3",
nphases = 4,
sys_clk_freq = sys_clk_freq)
# Synchronous dynamic ram. This is what controls all access to RAM.
# This houses the "crossbar", which negotiates all RAM accesses to different
# modules, including the verilog interfaces (waveforms etc.)
self.add_sdram("sdram",
phy = self.ddrphy,
module = MT41K128M16(sys_clk_freq, "1:4"),
l2_cache_size = 8192
)
# Initialize Ethernet
self.submodules.ethphy = LiteEthPHYMII(
clock_pads = platform.request("eth_clocks"),
pads = platform.request("eth"))
self.add_ethernet(phy=self.ethphy, dynamic_ip=True)
# Initialize network information
self.add_ip(local_ip, "LOCALIP")
self.add_ip(remote_ip, "REMOTEIP")
self.add_constant("TFTP_SERVER_PORT", tftp_port)
# Add pins
platform.add_extension(io)
module_reset = platform.request("module_reset")
# Add control loop DACs and ADCs.
self.add_picorv32("pico0")
self.picorv32_add_cl("pico0")
# Add waveform generator.
self.add_waveform("wf0", 4096)
self.picorv32_add_pi("pico0", "wf0", "wf0_PI", 0x400000, self.wf0.width, self.wf0.public_registers)
self.add_AD5791("dac0",
rst=module_reset,
miso=platform.request("dac_miso_0"),
mosi=platform.request("dac_mosi_0"),
sck=platform.request("dac_sck_0"),
ss_L=platform.request("dac_ss_L_0"),
)
self.picorv32_add_pi("pico0", "dac0", "dac0_PI", 0x200000, self.dac0.width, self.dac0.public_registers)
self.wf0.add_spi(self.dac0_PI.add_master("wf0"))
self.add_LT_adc("adc0",
rst=module_reset,
miso=platform.request("adc_sdo_0"),
sck=platform.request("adc_sck_0"),
ss_L=platform.request("adc_conv_0"),
spi_wid=18,
)
self.picorv32_add_pi("pico0", "adc0", "adc0_PI", 0x300000, self.adc0.width, self.adc0.public_registers)
# Run pre-finalize
for f in self.pre_finalize:
f()
def do_finalize(self):
with open('soc_subregions.json', 'wt') as f:
regions = self.soc_subregions.copy()
for k in regions:
if regions[k] is not None:
regions[k] = {name : reg._to_dict() for name, reg in regions[k].items()}
json.dump(regions, f)
def generate_main_cpu_include(closures, csr_file):
""" Generate Micropython include from a JSON file. """
with open('mmio.py', 'wt') as out:
print("from registers import *", file=out)
print("from waveform import *", file=out)
print("from picorv32 import *", file=out)
print("from spi import *", file=out)
with open(csr_file, 'rt') as f:
csrs = json.load(f)
for f in closures:
print(f(csrs), file=out)
from config import config
soc =UpsilonSoC(**config)
builder = Builder(soc, csr_json="csr.json", compile_software=True, compile_gateware=True)
builder.build()
generate_main_cpu_include(soc.mmio_closures, "csr.json")