Bus widths should now all be correct. (addresses and sizes in amaranth take into account the granularity selection, so granularity acts like two extra bits on the bus address line) Also don't ignore a warning about undriven resets. Turns out that can gate *everything* off in your design.
210 lines
7.5 KiB
Python
210 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from amaranth import *
|
|
from amaranth.sim import *
|
|
from amaranth_boards import colorlight_i9
|
|
from amaranth_soc.wishbone import *
|
|
|
|
from minerva.core import Minerva
|
|
|
|
from typing import List
|
|
from argparse import ArgumentParser
|
|
from pathlib import Path
|
|
import unittest
|
|
import sys
|
|
import os
|
|
|
|
from memory import *
|
|
from led import *
|
|
from eth import *
|
|
|
|
import i2c
|
|
import test_i2c
|
|
import uart
|
|
|
|
# To change clock domain of a module:
|
|
# new_thing = DomainRenamer("new_clock")(MyElaboratable())
|
|
|
|
|
|
# TODO clean this up, generate binary here, maybe even run cargo build
|
|
def load_firmware_for_mem() -> List[int]:
|
|
with open('../firmware/fw.bin', 'rb') as f:
|
|
# Stored as little endian, LSB first??
|
|
data = f.read()
|
|
out = []
|
|
assert(len(data) % 4 == 0)
|
|
for i in range(int(len(data) / 4)):
|
|
out.append(int.from_bytes(data[i*4:i*4+4], byteorder='little'))
|
|
|
|
return out
|
|
|
|
class Core(Elaboratable):
|
|
def __init__(self, clk25, led_signal, eth_interface):
|
|
self.count = Signal(64)
|
|
self.cpu = Minerva(reset_address=0x01000000, with_debug=False)
|
|
self.arbiter = Arbiter(addr_width=30, data_width=32, granularity=8)
|
|
self.decoder = Decoder(addr_width=30, data_width=32, granularity=8, features=["err"])
|
|
self.clk25 = clk25
|
|
self.led_signal = led_signal
|
|
self.eth_interface = eth_interface
|
|
|
|
def elaborate(self, platform):
|
|
m = Module()
|
|
m.submodules.cpu = self.cpu
|
|
m.submodules.arbiter = self.arbiter
|
|
m.submodules.decoder = self.decoder
|
|
|
|
# Create main and sampling clock, using PLL and 25MHz input clock
|
|
if platform is not None:
|
|
platform.add_file("pll.v", open("pll.v", "r").read())
|
|
sys_clk = Signal()
|
|
sample_clk = Signal()
|
|
pll = Instance(
|
|
"pll",
|
|
i_clkin=self.clk25,
|
|
o_clkout0=sys_clk,
|
|
o_clkout1=sample_clk,
|
|
)
|
|
m.submodules.pll = pll
|
|
|
|
# Create new clock domains
|
|
m.domains += ClockDomain("sync")
|
|
m.domains += ClockDomain("sample")
|
|
m.d.comb += ClockSignal("sync").eq(sys_clk)
|
|
m.d.comb += ClockSignal("sample").eq(sample_clk)
|
|
|
|
# Add clock constraints
|
|
if platform is not None:
|
|
platform.add_clock_constraint(sys_clk, 50e6)
|
|
platform.add_clock_constraint(sample_clk, 10e6)
|
|
|
|
# Connect ibus and dbus together for simplicity for now
|
|
minerva_wb_features = ["cti", "bte", "err"]
|
|
self.ibus = Interface(addr_width=30, data_width=32, granularity=8, features=minerva_wb_features)
|
|
# Note this is a set of statements! without assigning to the comb domain, this will do nothing
|
|
m.d.comb += self.cpu.ibus.connect(self.ibus)
|
|
self.arbiter.add(self.ibus)
|
|
|
|
self.dbus = Interface(addr_width=30, data_width=32, granularity=8, features=minerva_wb_features)
|
|
m.d.comb += self.cpu.dbus.connect(self.dbus) # Don't use .eq, use .connect, which will appropriately assign signals
|
|
# using .eq() gave me Multiple Driven errors
|
|
self.arbiter.add(self.dbus)
|
|
# TODO do something with interrupts
|
|
# These are interrupts headed into the CPU
|
|
self.interrupts = Signal(32)
|
|
m.d.comb += [
|
|
# Used for mtime registers, which are memory-mapped (not CSR), so I would have to implement.
|
|
# If I cared.
|
|
self.cpu.timer_interrupt.eq(0),
|
|
# Ostensibly exposed to us so we can interrupt one hart (CPU in this context) from another, we don't need this.
|
|
self.cpu.software_interrupt.eq(0),
|
|
# External interrupt lines, would be for any interrupts I implemented w/ a custom interrupt controller.
|
|
# Most likely I'll map a few lines to some peripherals, won't do anything fancy
|
|
self.cpu.external_interrupt.eq(self.interrupts),
|
|
]
|
|
|
|
|
|
fw = load_firmware_for_mem()
|
|
|
|
# Hook up memory space
|
|
self.rom = ROM(fw)
|
|
m.submodules.rom = self.rom
|
|
# Problem: not sure to handle how we do byte vs word addressing properly
|
|
# So doing this shift is a bit of a hacky way to impl anything
|
|
start, _stop, _step = self.decoder.add(self.rom, addr=0x01000000)
|
|
print(f"ROM added at 0x{start:08x}")
|
|
|
|
self.ram = RAM()
|
|
m.submodules.ram = self.ram
|
|
start, _stop, _step = self.decoder.add(self.ram)
|
|
print(f"RAM added at 0x{start:08x}")
|
|
|
|
self.led = LEDPeripheral(self.led_signal)
|
|
m.submodules.led = self.led
|
|
start, _stop, _step = self.decoder.add(self.led)
|
|
print(f"LED added at 0x{start:08x}")
|
|
|
|
m.submodules.uart = uart.UART(10e6)
|
|
|
|
# Ethernet
|
|
self.eth = LiteEth(self.eth_interface)
|
|
m.submodules.eth = self.eth
|
|
start, _stop, _step = self.decoder.add(self.eth, addr=0x02000000)
|
|
print(f"LiteETH added at 0x{start:08x}")
|
|
|
|
# Connect arbiter to decoder
|
|
m.d.comb += self.arbiter.bus.connect(self.decoder.bus)
|
|
|
|
# Counter
|
|
#m.d.sync += self.count.eq(self.count + 1)
|
|
#with m.If(self.count >= 50000000):
|
|
# m.d.sync += self.count.eq(0)
|
|
# m.d.sync += led.eq(~led)
|
|
|
|
return m
|
|
|
|
class SoC(Elaboratable):
|
|
def __init__(self):
|
|
pass
|
|
|
|
def elaborate(self, platform):
|
|
if platform is not None:
|
|
clk25 = platform.request("clk25")
|
|
led_signal = platform.request("led")
|
|
ethernet_interface = platform.request("eth_rgmii", 1)
|
|
else:
|
|
# platform is None in simulation, so provide harnesses for required signals
|
|
clk25 = Signal() # TODO unsure if this will work in sim
|
|
led_signal = Signal()
|
|
ethernet_interface = Record(rgmii_layout)
|
|
|
|
return Core(clk25, led_signal, ethernet_interface)
|
|
|
|
|
|
# TODO add structure to add regression tests
|
|
def run_sim():
|
|
dut = SoC()
|
|
sim = Simulator(dut)
|
|
|
|
def proc():
|
|
for i in range(1000):
|
|
yield Tick()
|
|
|
|
sim.add_clock(1e-6)
|
|
sim.add_sync_process(proc)
|
|
with sim.write_vcd('sim.vcd', gtkw_file='sim.gtkw'):
|
|
sim.reset()
|
|
sim.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = ArgumentParser(description="ARVP Sonar Acquisition FPGA gateware.")
|
|
args.add_argument("--build", action="store_true", help="Build bitstream.")
|
|
args.add_argument("--gen-debug-verilog", action="store_true", help="Save debug verilog.")
|
|
# TODO maybe allow an optional arg to specify an individual test to run?
|
|
args.add_argument("--test", action="store_true", help="Run RTL test suite and report results.")
|
|
args.add_argument("--save-vcd", action="store_true", help="Save VCD waveforms from test(s).")
|
|
args.add_argument("--sim", action="store_true", help="Run overall simulation")
|
|
args = args.parse_args()
|
|
|
|
if args.build:
|
|
# Overrides are available via AMARANTH_<override_variable_name> env variable, or kwarg
|
|
# TODO fix platform so I don't have to manually specify MDIO signal
|
|
colorlight_i9.Colorlight_i9_Platform().build(SoC(), debug_verilog=args.gen_debug_verilog, nextpnr_opts="--router router1", add_preferences="LOCATE COMP \"top.eth.core.rgmii_eth_mdio\" SITE \"P5\";\n")
|
|
|
|
if args.test:
|
|
if args.save_vcd:
|
|
if not Path("vcd_out").exists():
|
|
os.mkdir("vcd_out")
|
|
|
|
os.environ["TEST_SAVE_VCD"] = "YES"
|
|
|
|
# Super hacky... why am I doing this
|
|
test_modules = [mod for mod in sys.modules if mod.startswith("test_")]
|
|
for mod in test_modules:
|
|
unittest.main(module=mod, argv=[sys.argv[0]])
|
|
|
|
if args.sim:
|
|
run_sim()
|
|
|