reorganize litex code
This commit is contained in:
parent
f5b14d51ab
commit
3785e3498d
|
@ -0,0 +1,38 @@
|
|||
Copyright 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.
|
||||
|
||||
***************************************************
|
||||
|
||||
====================
|
||||
System Within a Chip
|
||||
====================
|
||||
|
||||
A *system within a chip* (SWiC) is a SoC within a SoC. Upsilon has the
|
||||
capability to add SWiCs that can be controlled by the main CPU. The CPU for
|
||||
the SWiC is the PicoRV32, which is a RISC-V RVI32 core with optional C, M
|
||||
extensions and custom-made non-branching instructions.
|
||||
|
||||
----------------
|
||||
Main CPU Control
|
||||
----------------
|
||||
|
||||
The main CPU controls the SWiC using two methods:
|
||||
|
||||
1. LiteX CSR Registers. This is for simple things where the main CPU should
|
||||
have 100% control at all times, like starting and stopping the CPU.
|
||||
2. *Preemptive interfaces* (PI), which sit in front of a Wishbone slave. The
|
||||
main CPU has a LiteX CSR register which selects the Wishbone master the
|
||||
Wishbone slave connects to.
|
||||
|
||||
In usual operation the SWiC RAM sits behind a PI. Before the SWiC starts, the
|
||||
main CPU switches the PI to itself, fills the RAM with the program, and then
|
||||
switches it back to the SWiC. When the SWiC starts, the SWiC has read-write
|
||||
access to the RAM. PIs are also used for connecting to external IO (like SPI).
|
||||
|
||||
In the future, there will be master-read-slave-write interfaces to transfer
|
||||
bulk memory from the SWiC to the main CPU (for instances, raster scanning).
|
||||
|
||||
See /gateware/soc.py for implementation notes.
|
|
@ -0,0 +1,75 @@
|
|||
# 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.
|
||||
|
||||
from migen import *
|
||||
from litex.soc.interconnect.wishbone import Interface
|
||||
|
||||
from util import *
|
||||
|
||||
class SPIMaster(Module):
|
||||
""" Wrapper for the SPI master verilog code. """
|
||||
def __init__(self, rst, miso, mosi, sck, ss,
|
||||
polarity = 0,
|
||||
phase = 0,
|
||||
ss_wait = 1,
|
||||
enable_miso = 1,
|
||||
enable_mosi = 1,
|
||||
spi_wid = 24,
|
||||
spi_cycle_half_wait = 1,
|
||||
):
|
||||
"""
|
||||
:param rst: Reset signal.
|
||||
:param miso: MISO signal.
|
||||
:param mosi: MOSI signal.
|
||||
:param sck: SCK signal.
|
||||
:param ss: SS signal.
|
||||
:param phase: Phase of SPI master. This phase is not the standard
|
||||
SPI phase because it is defined in terms of the rising edge, not
|
||||
the leading edge. See <https://software.mcgoron.com/peter/spi>
|
||||
:param polarity: See <https://software.mcgoron.com/peter/spi>.
|
||||
:param enable_miso: If ``False``, the module does not read data
|
||||
from MISO into a register.
|
||||
:param enable_mosi: If ``False``, the module does not write data
|
||||
to MOSI from a register.
|
||||
:param spi_wid: Verilog parameter: see file.
|
||||
:param spi_cycle_half_wait: Verilog parameter: see file.
|
||||
"""
|
||||
|
||||
self.bus = Interface(data_width = 32, address_width=32, addressing="word")
|
||||
self.addr_space_size = 0x10
|
||||
|
||||
self.comb += [
|
||||
self.bus.err.eq(0),
|
||||
]
|
||||
|
||||
self.specials += Instance("spi_master_ss_wb",
|
||||
p_SS_WAIT = ss_wait,
|
||||
p_SS_WAIT_TIMER_LEN = minbits(ss_wait),
|
||||
p_CYCLE_HALF_WAIT = spi_cycle_half_wait,
|
||||
p_TIMER_LEN = minbits(spi_cycle_half_wait),
|
||||
p_WID = spi_wid,
|
||||
p_WID_LEN = minbits(spi_wid),
|
||||
p_ENABLE_MISO = enable_miso,
|
||||
p_ENABLE_MOSI = enable_mosi,
|
||||
p_POLARITY = polarity,
|
||||
p_PHASE = phase,
|
||||
|
||||
i_clk = ClockSignal(),
|
||||
i_rst_L = rst,
|
||||
i_miso = miso,
|
||||
o_mosi = mosi,
|
||||
o_sck_wire = sck,
|
||||
o_ss_L = ss,
|
||||
|
||||
i_wb_cyc = self.bus.cyc,
|
||||
i_wb_stb = self.bus.stb,
|
||||
i_wb_we = self.bus.we,
|
||||
i_wb_sel = self.bus.sel,
|
||||
i_wb_addr = self.bus.adr,
|
||||
i_wb_dat_w = self.bus.dat_w,
|
||||
o_wb_ack = self.bus.ack,
|
||||
o_wb_dat_r = self.bus.dat_r,
|
||||
)
|
|
@ -0,0 +1,83 @@
|
|||
# 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.
|
||||
|
||||
from migen import *
|
||||
from litex.soc.interconnect.wishbone import Decoder
|
||||
|
||||
from util import *
|
||||
|
||||
"""
|
||||
LiteX has an automatic Wishbone bus generator that has a lot of quality of life
|
||||
features, like overlap checking, relocation, multiple masters, etc.
|
||||
|
||||
It doesn't work when the main SoC bus is also using the bus generator, so this
|
||||
module implements a basic Wishbone bus generator. All locations have to be
|
||||
added manually and there is no sanity checking.
|
||||
"""
|
||||
|
||||
class BasicRegion:
|
||||
""" Simple class for storing a RAM region. """
|
||||
def __init__(self, origin, size, bus=None):
|
||||
"""
|
||||
:param origin: Positive integer denoting the start location
|
||||
of the memory region.
|
||||
:param size: Size of the memory region. This must be of the form
|
||||
(2**N - 1).
|
||||
:param bus: Instance of a wishbone bus interface.
|
||||
"""
|
||||
|
||||
self.origin = origin
|
||||
self.size = size
|
||||
self.bus = bus
|
||||
|
||||
def decoder(self):
|
||||
"""
|
||||
Wishbone decoder generator. The decoder looks at the high
|
||||
bits of the address to check what bits are passed to the
|
||||
slave device.
|
||||
|
||||
Examples:
|
||||
|
||||
Location 0x10000 has 0xFFFF of address space.
|
||||
origin = 0x10000, rightbits = 16.
|
||||
|
||||
Location 0x10000 has 0xFFF of address space.
|
||||
origin = 0x10000, rightbits = 12.
|
||||
|
||||
Location 0x100000 has 0x1F of address space.
|
||||
origin = 0x100000, rightbits = 5.
|
||||
"""
|
||||
rightbits = minbits(self.size-1)
|
||||
print(self.origin, self.origin >> rightbits)
|
||||
return lambda addr: addr[rightbits:32] == (self.origin >> rightbits)
|
||||
|
||||
def to_dict(self):
|
||||
return {"origin" : self.origin, "size": self.size}
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_dict())
|
||||
|
||||
class MemoryMap:
|
||||
""" Stores the memory map of an embedded core. """
|
||||
def __init__(self):
|
||||
self.regions = {}
|
||||
|
||||
def add_region(self, name, region):
|
||||
assert name not in self.regions
|
||||
self.regions[name] = region
|
||||
|
||||
def dump_mmap(self, jsonfile):
|
||||
with open(jsonfile, 'wt') as f:
|
||||
import json
|
||||
json.dump({k : self.regions[k].to_dict() for k in self.regions}, f)
|
||||
|
||||
def bus_submodule(self, masterbus):
|
||||
""" Return a module that decodes the masterbus into the
|
||||
slave devices according to their origin and start positions. """
|
||||
slaves = [(self.regions[n].decoder(), self.regions[n].bus)
|
||||
for n in self.regions]
|
||||
return Decoder(masterbus, slaves, register=False)
|
||||
|
335
gateware/soc.py
335
gateware/soc.py
|
@ -60,11 +60,9 @@ from litedram.modules import MT41K128M16
|
|||
from litedram.frontend.dma import LiteDRAMDMAReader
|
||||
from liteeth.phy.mii import LiteEthPHYMII
|
||||
|
||||
from math import log2, floor
|
||||
|
||||
def minbits(n):
|
||||
""" Return the amount of bits necessary to store n. """
|
||||
return floor(log2(n) + 1)
|
||||
from util import *
|
||||
from swic import *
|
||||
from extio import *
|
||||
|
||||
"""
|
||||
Keep this diagram up to date! This is the wiring diagram from the ADC to
|
||||
|
@ -93,6 +91,8 @@ 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")),
|
||||
|
@ -111,318 +111,6 @@ io = [
|
|||
# ("test_clock", 0, Pins("P18"), IOStandard("LVCMOS33"))
|
||||
]
|
||||
|
||||
class PreemptiveInterface(Module, AutoCSR):
|
||||
""" A preemptive interface is a manually controlled Wishbone interface
|
||||
that stands between multiple masters (potentially interconnects) and a
|
||||
single slave. A master controls which master (or interconnect) has access
|
||||
to the slave. This is to avoid bus contention by having multiple buses. """
|
||||
|
||||
def __init__(self, masters_len, slave):
|
||||
"""
|
||||
:param masters_len: The amount of buses accessing this slave. This number
|
||||
must be greater than one.
|
||||
:param slave: The slave device. This object must have an Interface object
|
||||
accessable as ``bus``.
|
||||
"""
|
||||
|
||||
assert masters_len > 1
|
||||
self.buses = []
|
||||
self.master_select = CSRStorage(masters_len, name='master_select', description='RW bitstring of which master interconnect to connect to')
|
||||
self.slave = slave
|
||||
|
||||
for i in range(masters_len):
|
||||
# Add the slave interface each master interconnect sees.
|
||||
self.buses.append(Interface(data_width=32, address_width=32, addressing="byte"))
|
||||
|
||||
"""
|
||||
Construct a combinatorial case statement. In verilog, the if
|
||||
statment would look like
|
||||
|
||||
always @ (*) case (master_select)
|
||||
1: begin
|
||||
// Bus assignments...
|
||||
end
|
||||
2: begin
|
||||
// Bus assignments...
|
||||
end
|
||||
// more cases:
|
||||
default:
|
||||
// assign everything to master 0
|
||||
end
|
||||
|
||||
The If statement in Migen (Python HDL) is an object with a method
|
||||
called "ElseIf" and "Else", that return objects with the specified
|
||||
case attached. Instead of directly writing an If statement into
|
||||
the combinatorial block, the If statement is constructed in a
|
||||
for loop.
|
||||
|
||||
Avoiding latches:
|
||||
Left hand sign (assignment) is always an input.
|
||||
"""
|
||||
|
||||
def assign_for_case(current_case):
|
||||
asn = [ ]
|
||||
|
||||
for j in range(masters_len):
|
||||
if current_case == j:
|
||||
asn += [
|
||||
self.slave.bus.adr.eq(self.buses[j].adr),
|
||||
self.slave.bus.dat_w.eq(self.buses[j].dat_w),
|
||||
self.slave.bus.cyc.eq(self.buses[j].cyc),
|
||||
self.slave.bus.stb.eq(self.buses[j].stb),
|
||||
self.slave.bus.we.eq(self.buses[j].we),
|
||||
self.slave.bus.sel.eq(self.buses[j].sel),
|
||||
self.buses[j].dat_r.eq(self.slave.bus.dat_r),
|
||||
self.buses[j].ack.eq(self.slave.bus.ack),
|
||||
]
|
||||
else:
|
||||
asn += [
|
||||
self.buses[j].dat_r.eq(0),
|
||||
self.buses[j].ack.eq(self.buses[j].cyc & self.buses[j].stb),
|
||||
]
|
||||
return asn
|
||||
|
||||
cases = {"default": assign_for_case(0)}
|
||||
for i in range(1, masters_len):
|
||||
cases[i] = assign_for_case(i)
|
||||
|
||||
self.comb += Case(self.master_select.storage, cases)
|
||||
|
||||
class SPIMaster(Module):
|
||||
""" Wrapper for the SPI master verilog code. """
|
||||
def __init__(self, rst, miso, mosi, sck, ss,
|
||||
polarity = 0,
|
||||
phase = 0,
|
||||
ss_wait = 1,
|
||||
enable_miso = 1,
|
||||
enable_mosi = 1,
|
||||
spi_wid = 24,
|
||||
spi_cycle_half_wait = 1,
|
||||
):
|
||||
"""
|
||||
:param rst: Reset signal.
|
||||
:param miso: MISO signal.
|
||||
:param mosi: MOSI signal.
|
||||
:param sck: SCK signal.
|
||||
:param ss: SS signal.
|
||||
:param phase: Phase of SPI master. This phase is not the standard
|
||||
SPI phase because it is defined in terms of the rising edge, not
|
||||
the leading edge. See <https://software.mcgoron.com/peter/spi>
|
||||
:param polarity: See <https://software.mcgoron.com/peter/spi>.
|
||||
:param enable_miso: If ``False``, the module does not read data
|
||||
from MISO into a register.
|
||||
:param enable_mosi: If ``False``, the module does not write data
|
||||
to MOSI from a register.
|
||||
:param spi_wid: Verilog parameter: see file.
|
||||
:param spi_cycle_half_wait: Verilog parameter: see file.
|
||||
"""
|
||||
|
||||
self.bus = Interface(data_width = 32, address_width=32, addressing="word")
|
||||
self.region = SoCRegion(size=0x10, cached=False)
|
||||
|
||||
self.comb += [
|
||||
self.bus.err.eq(0),
|
||||
]
|
||||
|
||||
self.specials += Instance("spi_master_ss_wb",
|
||||
p_SS_WAIT = ss_wait,
|
||||
p_SS_WAIT_TIMER_LEN = minbits(ss_wait),
|
||||
p_CYCLE_HALF_WAIT = spi_cycle_half_wait,
|
||||
p_TIMER_LEN = minbits(spi_cycle_half_wait),
|
||||
p_WID = spi_wid,
|
||||
p_WID_LEN = minbits(spi_wid),
|
||||
p_ENABLE_MISO = enable_miso,
|
||||
p_ENABLE_MOSI = enable_mosi,
|
||||
p_POLARITY = polarity,
|
||||
p_PHASE = phase,
|
||||
|
||||
i_clk = ClockSignal(),
|
||||
i_rst_L = rst,
|
||||
i_miso = miso,
|
||||
o_mosi = mosi,
|
||||
o_sck_wire = sck,
|
||||
o_ss_L = ss,
|
||||
|
||||
i_wb_cyc = self.bus.cyc,
|
||||
i_wb_stb = self.bus.stb,
|
||||
i_wb_we = self.bus.we,
|
||||
i_wb_sel = self.bus.sel,
|
||||
i_wb_addr = self.bus.adr,
|
||||
i_wb_dat_w = self.bus.dat_w,
|
||||
o_wb_ack = self.bus.ack,
|
||||
o_wb_dat_r = self.bus.dat_r,
|
||||
)
|
||||
|
||||
#TODO: Generalize CSR stuff
|
||||
class ControlLoopParameters(Module, AutoCSR):
|
||||
""" Interface for the Linux CPU to write parameters to the CPU
|
||||
and for the CPU to write data back to the CPU without complex
|
||||
locking mechanisms.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.cl_I = CSRStorage(32, description='Integral parameter')
|
||||
self.cl_P = CSRStorage(32, description='Proportional parameter')
|
||||
self.deltaT = CSRStorage(32, description='Wait parameter')
|
||||
self.setpt = CSRStorage(32, description='Setpoint')
|
||||
self.zset = CSRStatus(32, description='Set Z position')
|
||||
self.zpos = CSRStatus(32, description='Measured Z position')
|
||||
|
||||
self.bus = Interface(data_width = 32, address_width = 32, addressing="word")
|
||||
self.width = 0x20
|
||||
self.sync += [
|
||||
If(self.bus.cyc == 1 and self.bus.stb == 1 and self.bus.ack == 0,
|
||||
Case(self.bus.adr[0:4], {
|
||||
0x0: self.bus.dat_r.eq(self.cl_I.storage),
|
||||
0x4: self.bus.dat_r.eq(self.cl_P.storage),
|
||||
0x8: self.bus.dat_r.eq(self.deltaT.storage),
|
||||
0xC: self.bus.dat_r.eq(self.setpt.storage),
|
||||
0x10: If(self.bus.we,
|
||||
self.zset.status.eq(self.bus.dat_w)
|
||||
).Else(
|
||||
self.bus.dat_r.eq(self.zset.status)
|
||||
),
|
||||
0x14: If(self.bus.we,
|
||||
self.zpos.status.eq(self.bus.dat_w),
|
||||
).Else(
|
||||
self.bus.dat_r.eq(self.zpos.status)
|
||||
),
|
||||
"default": self.bus.dat_r.eq(0xdeadbeef),
|
||||
}),
|
||||
self.bus.ack.eq(1),
|
||||
).Else(
|
||||
self.bus.ack.eq(0),
|
||||
)
|
||||
]
|
||||
|
||||
class BasicRegion:
|
||||
""" Simple class for storing a RAM region. """
|
||||
def __init__(self, origin, size, bus=None):
|
||||
self.origin = origin
|
||||
self.size = size
|
||||
self.bus = bus
|
||||
|
||||
def decoder(self):
|
||||
"""
|
||||
Wishbone decoder generator. The decoder looks at the high
|
||||
bits of the address to check what bits are passed to the
|
||||
slave device.
|
||||
|
||||
Examples:
|
||||
|
||||
Location 0x10000 has 0xFFFF of address space.
|
||||
origin = 0x10000, rightbits = 16.
|
||||
|
||||
Location 0x10000 has 0xFFF of address space.
|
||||
origin = 0x10000, rightbits = 12.
|
||||
|
||||
Location 0x100000 has 0x1F of address space.
|
||||
origin = 0x100000, rightbits = 5.
|
||||
"""
|
||||
rightbits = minbits(self.size-1)
|
||||
print(self.origin, self.origin >> rightbits)
|
||||
return lambda addr: addr[rightbits:32] == (self.origin >> rightbits)
|
||||
|
||||
def to_dict(self):
|
||||
return {"origin" : self.origin, "size": self.size}
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_dict())
|
||||
|
||||
class MemoryMap:
|
||||
""" Stores the memory map of an embedded core. """
|
||||
def __init__(self):
|
||||
self.regions = {}
|
||||
|
||||
def add_region(self, name, region):
|
||||
assert name not in self.regions
|
||||
self.regions[name] = region
|
||||
|
||||
def dump_json(self, jsonfile):
|
||||
with open(jsonfile, 'wt') as f:
|
||||
import json
|
||||
json.dump({k : self.regions[k].to_dict() for k in self.regions}, f)
|
||||
|
||||
def bus_submodule(self, masterbus):
|
||||
""" Return a module that decodes the masterbus into the
|
||||
slave devices according to their origin and start positions. """
|
||||
slaves = [(self.regions[n].decoder(), self.regions[n].bus)
|
||||
for n in self.regions]
|
||||
return Decoder(masterbus, slaves, register=False)
|
||||
|
||||
class PicoRV32(Module, AutoCSR):
|
||||
def add_ram(self, name, width, origin):
|
||||
mod = SRAM(width)
|
||||
self.submodules += mod
|
||||
|
||||
self.mmap.add_region(name, BasicRegion(width, origin, mod.bus))
|
||||
|
||||
def add_params(self, origin):
|
||||
self.submodules.params = params = ControlLoopParameters()
|
||||
self.mmap.add_region('params', BasicRegion(origin, params.width, params.bus))
|
||||
|
||||
def __init__(self, name, start_addr=0x10000, irq_addr=0x10010):
|
||||
self.mmap = MemoryMap()
|
||||
self.name = name
|
||||
|
||||
self.masterbus = Interface(data_width=32, address_width=32, addressing="byte")
|
||||
|
||||
self.resetpin = CSRStorage(1, name="enable", description="PicoRV32 enable")
|
||||
|
||||
self.trap = CSRStatus(8, name="trap", description="Trap condition")
|
||||
self.d_adr = CSRStatus(32)
|
||||
self.d_dat_w = CSRStatus(32)
|
||||
self.dbg_insn_addr = CSRStatus(32)
|
||||
self.dbg_insn_opcode = CSRStatus(32)
|
||||
|
||||
self.comb += [
|
||||
self.d_adr.status.eq(self.masterbus.adr),
|
||||
self.d_dat_w.status.eq(self.masterbus.dat_w),
|
||||
]
|
||||
|
||||
# NOTE: need to compile to these extact instructions
|
||||
self.specials += Instance("picorv32_wb",
|
||||
p_COMPRESSED_ISA = 1,
|
||||
p_ENABLE_MUL = 1,
|
||||
p_PROGADDR_RESET=start_addr,
|
||||
p_PROGADDR_IRQ =irq_addr,
|
||||
p_REGS_INIT_ZERO = 1,
|
||||
o_trap = self.trap.status,
|
||||
|
||||
i_wb_rst_i = ~self.resetpin.storage,
|
||||
i_wb_clk_i = ClockSignal(),
|
||||
o_wbm_adr_o = self.masterbus.adr,
|
||||
o_wbm_dat_o = self.masterbus.dat_r,
|
||||
i_wbm_dat_i = self.masterbus.dat_w,
|
||||
o_wbm_we_o = self.masterbus.we,
|
||||
o_wbm_sel_o = self.masterbus.sel,
|
||||
o_wbm_stb_o = self.masterbus.stb,
|
||||
i_wbm_ack_i = self.masterbus.ack,
|
||||
o_wbm_cyc_o = self.masterbus.cyc,
|
||||
|
||||
o_pcpi_valid = Signal(),
|
||||
o_pcpi_insn = Signal(32),
|
||||
o_pcpi_rs1 = Signal(32),
|
||||
o_pcpi_rs2 = Signal(32),
|
||||
i_pcpi_wr = 0,
|
||||
i_pcpi_wait = 0,
|
||||
i_pcpi_rd = 0,
|
||||
i_pcpi_ready = 0,
|
||||
|
||||
i_irq = 0,
|
||||
o_eoi = Signal(32),
|
||||
|
||||
o_trace_valid = Signal(),
|
||||
o_trace_data = Signal(36),
|
||||
o_debug_state = Signal(2),
|
||||
|
||||
o_dbg_insn_addr = self.dbg_insn_addr.status,
|
||||
o_dbg_insn_opcode = self.dbg_insn_opcode.status,
|
||||
)
|
||||
|
||||
def do_finalize(self):
|
||||
self.mmap.dump_json(self.name + ".json")
|
||||
self.submodules.decoder = self.mmap.bus_submodule(self.masterbus)
|
||||
|
||||
# Clock and Reset Generator
|
||||
# I don't know how this works, I only know that it does.
|
||||
class _CRG(Module):
|
||||
|
@ -462,9 +150,8 @@ class UpsilonSoC(SoCCore):
|
|||
self.add_constant(f"{ip_name}{seg_num}", int(ip_byte))
|
||||
|
||||
def add_blockram(self, name, size, connect_now=True):
|
||||
assert not hasattr(self.submodules, name)
|
||||
mod = SRAM(size)
|
||||
setattr(self.submodules, name, mod)
|
||||
self.add_module(name, mod)
|
||||
|
||||
if connect_now:
|
||||
self.bus.add_slave(name, mod.bus,
|
||||
|
@ -472,19 +159,17 @@ class UpsilonSoC(SoCCore):
|
|||
return mod
|
||||
|
||||
def add_preemptive_interface(self, name, size, slave):
|
||||
assert not hasattr(self.submodules, name)
|
||||
mod = PreemptiveInterface(size, slave)
|
||||
setattr(self.submodules, name, mod)
|
||||
self.add_module(name, mod)
|
||||
return mod
|
||||
|
||||
def add_picorv32(self, name, size=0x1000, origin=0x10000, param_origin=0x100000):
|
||||
assert not hasattr(self.submodules, name)
|
||||
pico = PicoRV32(name, origin, origin+0x10)
|
||||
setattr(self.submodules, name, pico)
|
||||
self.add_module(name, pico)
|
||||
|
||||
ram = self.add_blockram(name + "_ram", size=size, connect_now=False)
|
||||
ram_iface = self.add_preemptive_interface(name + "ram_iface", 2, ram)
|
||||
pico.mmap.add_region("main",
|
||||
pico.add_region("main",
|
||||
BasicRegion(origin=origin, size=size, bus=ram_iface.buses[1]))
|
||||
|
||||
self.bus.add_slave(name + "_ram", ram_iface.buses[0],
|
||||
|
@ -573,7 +258,7 @@ class UpsilonSoC(SoCCore):
|
|||
platform.request("dac_sck_0"),
|
||||
platform.request("dac_ss_L_0"),
|
||||
)
|
||||
self.bus.add_slave("spi0", self.spi0.bus, self.spi0.region)
|
||||
self.bus.add_slave("spi0", self.spi0.bus, SoCRegion(origin=None, size=self.spi0.addr_space_size, cached=False))
|
||||
|
||||
self.add_picorv32("pico0")
|
||||
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
# 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.
|
||||
|
||||
from migen import *
|
||||
from litex.soc.interconnect.csr import CSRStorage, CSRStatus
|
||||
from litex.soc.interconnect.wishbone import Interface, SRAM, Decoder
|
||||
from litex.gen import LiteXModule
|
||||
from region import *
|
||||
|
||||
class PreemptiveInterface(LiteXModule):
|
||||
""" A preemptive interface is a manually controlled Wishbone interface
|
||||
that stands between multiple masters (potentially interconnects) and a
|
||||
single slave. A master controls which master (or interconnect) has access
|
||||
to the slave. This is to avoid bus contention by having multiple buses. """
|
||||
|
||||
def __init__(self, masters_len, slave):
|
||||
"""
|
||||
:param masters_len: The amount of buses accessing this slave. This number
|
||||
must be greater than one.
|
||||
:param slave: The slave device. This object must have an Interface object
|
||||
accessable as ``bus``.
|
||||
"""
|
||||
|
||||
assert masters_len > 1
|
||||
self.buses = []
|
||||
self.master_select = CSRStorage(masters_len, name='master_select', description='RW bitstring of which master interconnect to connect to')
|
||||
self.slave = slave
|
||||
|
||||
for i in range(masters_len):
|
||||
# Add the slave interface each master interconnect sees.
|
||||
self.buses.append(Interface(data_width=32, address_width=32, addressing="byte"))
|
||||
|
||||
"""
|
||||
Construct a combinatorial case statement. In verilog, the case
|
||||
statment would look like
|
||||
|
||||
always @ (*) case (master_select)
|
||||
1: begin
|
||||
// Assign slave to master 1,
|
||||
// Assign all other masters to dummy ports
|
||||
end
|
||||
2: begin
|
||||
// Assign slave to master 2,
|
||||
// Assign all other masters to dummy ports
|
||||
end
|
||||
// more cases:
|
||||
default: begin
|
||||
// assign slave to master 0
|
||||
// Assign all other masters to dummy ports
|
||||
end
|
||||
|
||||
Case statement is a dictionary, where each key is either the
|
||||
number to match or "default", which is the default case.
|
||||
|
||||
Avoiding latches:
|
||||
Left hand sign (assignment) is always an input.
|
||||
"""
|
||||
|
||||
def assign_for_case(current_case):
|
||||
asn = [ ]
|
||||
|
||||
for j in range(masters_len):
|
||||
if current_case == j:
|
||||
""" Assign all inputs (for example, slave reads addr
|
||||
from master) to outputs. """
|
||||
asn += [
|
||||
self.slave.bus.adr.eq(self.buses[j].adr),
|
||||
self.slave.bus.dat_w.eq(self.buses[j].dat_w),
|
||||
self.slave.bus.cyc.eq(self.buses[j].cyc),
|
||||
self.slave.bus.stb.eq(self.buses[j].stb),
|
||||
self.slave.bus.we.eq(self.buses[j].we),
|
||||
self.slave.bus.sel.eq(self.buses[j].sel),
|
||||
self.buses[j].dat_r.eq(self.slave.bus.dat_r),
|
||||
self.buses[j].ack.eq(self.slave.bus.ack),
|
||||
]
|
||||
else:
|
||||
""" Master bus will always read 0 when they are not
|
||||
selected, and writes are ignored. They will still
|
||||
get a response to avoid timeouts. """
|
||||
asn += [
|
||||
self.buses[j].dat_r.eq(0),
|
||||
self.buses[j].ack.eq(self.buses[j].cyc & self.buses[j].stb),
|
||||
]
|
||||
return asn
|
||||
|
||||
cases = {"default": assign_for_case(0)}
|
||||
for i in range(1, masters_len):
|
||||
cases[i] = assign_for_case(i)
|
||||
|
||||
self.comb += Case(self.master_select.storage, cases)
|
||||
|
||||
#TODO: Generalize CSR stuff
|
||||
class ControlLoopParameters(LiteXModule):
|
||||
""" Interface for the Linux CPU to write parameters to the CPU
|
||||
and for the CPU to write data back to the CPU without complex
|
||||
locking mechanisms.
|
||||
|
||||
This is a hack and will be replaced later with a more general
|
||||
memory region.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.cl_I = CSRStorage(32, description='Integral parameter')
|
||||
self.cl_P = CSRStorage(32, description='Proportional parameter')
|
||||
self.deltaT = CSRStorage(32, description='Wait parameter')
|
||||
self.setpt = CSRStorage(32, description='Setpoint')
|
||||
self.zset = CSRStatus(32, description='Set Z position')
|
||||
self.zpos = CSRStatus(32, description='Measured Z position')
|
||||
|
||||
self.bus = Interface(data_width = 32, address_width = 32, addressing="word")
|
||||
self.width = 0x20
|
||||
self.sync += [
|
||||
If(self.bus.cyc == 1 and self.bus.stb == 1 and self.bus.ack == 0,
|
||||
Case(self.bus.adr[0:4], {
|
||||
0x0: self.bus.dat_r.eq(self.cl_I.storage),
|
||||
0x4: self.bus.dat_r.eq(self.cl_P.storage),
|
||||
0x8: self.bus.dat_r.eq(self.deltaT.storage),
|
||||
0xC: self.bus.dat_r.eq(self.setpt.storage),
|
||||
0x10: If(self.bus.we,
|
||||
self.zset.status.eq(self.bus.dat_w)
|
||||
).Else(
|
||||
self.bus.dat_r.eq(self.zset.status)
|
||||
),
|
||||
0x14: If(self.bus.we,
|
||||
self.zpos.status.eq(self.bus.dat_w),
|
||||
).Else(
|
||||
self.bus.dat_r.eq(self.zpos.status)
|
||||
),
|
||||
"default": self.bus.dat_r.eq(0xdeadbeef),
|
||||
}),
|
||||
self.bus.ack.eq(1),
|
||||
).Else(
|
||||
self.bus.ack.eq(0),
|
||||
)
|
||||
]
|
||||
|
||||
class PicoRV32(LiteXModule, MemoryMap):
|
||||
def add_params(self, origin):
|
||||
params = ControlLoopParameters()
|
||||
self.add_module("params", params)
|
||||
self.add_region('params', BasicRegion(origin, params.width, params.bus))
|
||||
|
||||
def __init__(self, name, start_addr=0x10000, irq_addr=0x10010):
|
||||
MemoryMap.__init__(self)
|
||||
self.name = name
|
||||
|
||||
self.masterbus = Interface(data_width=32, address_width=32, addressing="byte")
|
||||
|
||||
self.resetpin = CSRStorage(1, name="enable", description="PicoRV32 enable")
|
||||
|
||||
self.trap = CSRStatus(8, name="trap", description="Trap condition")
|
||||
self.d_adr = CSRStatus(32)
|
||||
self.d_dat_w = CSRStatus(32)
|
||||
self.dbg_insn_addr = CSRStatus(32)
|
||||
self.dbg_insn_opcode = CSRStatus(32)
|
||||
|
||||
self.comb += [
|
||||
self.d_adr.status.eq(self.masterbus.adr),
|
||||
self.d_dat_w.status.eq(self.masterbus.dat_w),
|
||||
]
|
||||
|
||||
# NOTE: need to compile to these extact instructions
|
||||
self.specials += Instance("picorv32_wb",
|
||||
p_COMPRESSED_ISA = 1,
|
||||
p_ENABLE_MUL = 1,
|
||||
p_PROGADDR_RESET=start_addr,
|
||||
p_PROGADDR_IRQ =irq_addr,
|
||||
p_REGS_INIT_ZERO = 1,
|
||||
o_trap = self.trap.status,
|
||||
|
||||
i_wb_rst_i = ~self.resetpin.storage,
|
||||
i_wb_clk_i = ClockSignal(),
|
||||
o_wbm_adr_o = self.masterbus.adr,
|
||||
o_wbm_dat_o = self.masterbus.dat_w,
|
||||
i_wbm_dat_i = self.masterbus.dat_r,
|
||||
o_wbm_we_o = self.masterbus.we,
|
||||
o_wbm_sel_o = self.masterbus.sel,
|
||||
o_wbm_stb_o = self.masterbus.stb,
|
||||
i_wbm_ack_i = self.masterbus.ack,
|
||||
o_wbm_cyc_o = self.masterbus.cyc,
|
||||
|
||||
o_pcpi_valid = Signal(),
|
||||
o_pcpi_insn = Signal(32),
|
||||
o_pcpi_rs1 = Signal(32),
|
||||
o_pcpi_rs2 = Signal(32),
|
||||
i_pcpi_wr = 0,
|
||||
i_pcpi_wait = 0,
|
||||
i_pcpi_rd = 0,
|
||||
i_pcpi_ready = 0,
|
||||
|
||||
i_irq = 0,
|
||||
o_eoi = Signal(32),
|
||||
|
||||
o_trace_valid = Signal(),
|
||||
o_trace_data = Signal(36),
|
||||
o_debug_state = Signal(2),
|
||||
|
||||
o_dbg_insn_addr = self.dbg_insn_addr.status,
|
||||
o_dbg_insn_opcode = self.dbg_insn_opcode.status,
|
||||
)
|
||||
|
||||
def do_finalize(self):
|
||||
self.dump_mmap(self.name + ".json")
|
||||
self.submodules.decoder = self.bus_submodule(self.masterbus)
|
|
@ -0,0 +1,11 @@
|
|||
# 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.
|
||||
|
||||
def minbits(n):
|
||||
from math import log2, floor
|
||||
""" Return the amount of bits necessary to store n. """
|
||||
return floor(log2(n) + 1)
|
||||
|
Loading…
Reference in New Issue