unify region generation and added SPI to PicoRV32

This commit is contained in:
Peter McGoron 2024-02-27 03:48:22 +00:00
parent 6f61d7db7a
commit da1e9238ab
9 changed files with 197 additions and 267 deletions

View File

@ -49,6 +49,7 @@ hardware-get:
docker cp upsilon-hardware:/home/user/upsilon/gateware/build/digilent_arty/gateware/digilent_arty.bit ../boot/
docker cp upsilon-hardware:/home/user/upsilon/gateware/arty.dtb ../boot/
docker cp upsilon-hardware:/home/user/upsilon/gateware/csr.json ../boot/
docker cp upsilon-hardware:/home/user/upsilon/gateware/soc_subregions.json ../boot/
docker cp upsilon-hardware:/home/user/upsilon/gateware/pico0.json ../boot/
docker cp upsilon-hardware:/home/user/upsilon/gateware/mmio.py ../boot/
hardware-clean:

View File

@ -34,13 +34,39 @@ The ``machine`` module contains arrays called ``mem8``, ``mem16``, and ``mem32``
They are used to directly access memory locations on the main CPU bus. Note
that ``mem32`` accesses must be word aligned.
Locations of interest are included as constants in the ``mmio`` module, which
is generated from memory map JSON files (``csr.json`` and others). At some
point there will be a standard library that wraps these accesses as functions.
-------------------
Accessing Registers
-------------------
----------------
At the lowest level, a program will write to and read from "registers." These
registers control the operations of various parts of the system.
The main bus has two register buses: "CSR" (which is the LiteX default), and
custom Wishbone code. CSR register information is in the ``csr.json`` file.
Wishbone bus registers are allocated with regions that are specified in
``csr.json``, while the actual registers inside that region are located in
``soc_subregions.json``. These should be automatically dumped to the Micropython
file ``mmio.py`` for easy usage.
====================
System Within a Chip
====================
Systems Within a Chip (**SWiCs**) are CPUs that are controlled by the main CPU
but run seperately (they have their own registers, RAM, etc.) They can be
programmed and controlled through Micropython.
The SWiC is a RV32IMC core. Code for the SWiC needs to be compiled for a start
address of ``0x10000`` and a IRQ handler at ``0x10010``. The default length of
the SWiC region is ``0x1000`` bytes.
Each core is given the name ``pico0``, ``pico1``, etc. The regions of each CPU
are stored in ``pico0.json``, ``pico1.json``, etc. The system used to control
slave access to the CPU bus is a CSR (and should be in ``mmio.py``).
================
Computer Control
----------------
================
Micropython code can be loaded manually with SSH but this gets cumbersome.
Python scripts on the controlling computer connected to the Upsilon FPGA can

View File

@ -15,7 +15,7 @@ csr.json build/digilent_arty/digilent_arty.bit: soc.py
python3 soc.py
mmio.py: csr.json csr2mp.py
python3 csr2mp.py csr.json > mmio.py
python3 csr2mp.py > mmio.py
clean:
rm -rf build csr.json arty.dts arty.dtb mmio.py

View File

@ -13,17 +13,22 @@
import collections
import argparse
import json
import sys
import mmio_descr
with open(sys.argv[1], 'rt') as f:
j = json.load(f)
with open('csr.json', 'rt') as f:
csrs = json.load(f)
print("from micropython import const")
for key in j["csr_registers"]:
for key in csrs["csr_registers"]:
if key.startswith("pico0"):
print(f'{key} = const({j["csr_registers"][key]["addr"]})')
print(f'{key} = const({csrs["csr_registers"][key]["addr"]})')
print(f'pico0_ram = const({j["memories"]["pico0_ram"]["base"]})')
print(f'pico0_dbg_reg = const({j["memories"]["pico0_dbg_reg"]["base"]})')
with open('soc_subregions.json', 'rt') as f:
subregions = json.load(f)
for key in subregions:
if subregions[key] is None:
print(f'{key} = const({csrs["memories"][key]["base"]})')
else:
print(f'{key}_base = const({csrs["memorys"][key]["base"]})')
print(f'{key} = {subregions[key].__repr__()}')

View File

@ -10,8 +10,51 @@ from litex.soc.interconnect.wishbone import Interface
from util import *
class SPIMaster(Module):
AD5791_PARAMS = {
"polarity" :0,
"phase" :1,
"spi_cycle_half_wait" : 10,
"ss_wait" : 5,
"enable_miso" : 1,
"enable_mosi" : 1,
"spi_wid" : 24,
}
LT_ADC_PARAMS = {
"polarity" : 1,
"phase" : 0,
"spi_cycle_half_wait" : 5,
"ss_wait" : 60,
"enable_mosi" : 0,
}
width = 0x10
registers = {
"finished_or_ready": {
"origin" : 0,
"width" : 4,
"rw": False,
},
"arm" : {
"origin": 4,
"width": 4,
"rw": True,
},
"from_slave": {
"origin": 8,
"width": 4,
"rw": False,
},
"to_slave": {
"origin": 0xC,
"width": 4,
"rw": True
},
}
""" Wrapper for the SPI master verilog code. """
def __init__(self, rst, miso, mosi, sck, ss,
def __init__(self, rst, miso, mosi, sck, ss_L,
polarity = 0,
phase = 0,
ss_wait = 1,
@ -28,8 +71,8 @@ class SPIMaster(Module):
: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>.
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
@ -39,7 +82,6 @@ class SPIMaster(Module):
"""
self.bus = Interface(data_width = 32, address_width=32, addressing="byte")
self.addr_space_size = 0x10
self.comb += [
self.bus.err.eq(0),
@ -62,7 +104,7 @@ class SPIMaster(Module):
i_miso = miso,
o_mosi = mosi,
o_sck_wire = sck,
o_ss_L = ss,
o_ss_L = ss_L,
i_wb_cyc = self.bus.cyc,
i_wb_stb = self.bus.stb,

View File

@ -1,221 +0,0 @@
import textwrap
class Descr:
def __init__(self, name, blen, rwperm, num, descr):
"""
:param name: Name of the pin without numerical suffix.
:param blen: Bit length of the pin.
:param doc: Restructured text documentation of the register.
:param num: The amount of registers of the same type.
:param read_only: A string that must be either "read-only" or "write-write".
"""
self.name = name
self.blen = blen
self.doc = textwrap.dedent(descr)
self.num = num
self.rwperm = rwperm
@classmethod
def from_dict(cls, jsdict, name):
return cls(name, jsdict[name]["len"], jsdict[name]["ro"], jsdict[name]["num"], jsdict[name]["doc"])
def store_to_dict(self, d):
d[self.name] = {
"len": self.blen,
"doc": self.doc,
"num": self.num,
"ro": ro
}
registers = [
Descr("adc_sel", 3, "read-write", 8, """\
Select which on-FPGA SPI master controls the ADC.
Valid settings:
* ``0``: ADC is controlled by MMIO registers.
* ``0b10``: ADC is controlled by MMIO registers, but conversion is
disabled. This is used to flush output from an out-of-sync ADC.
* ``0b100``: ADC 0 only. ADC is controlled by control loop."""),
Descr("adc_finished", 1, "read-only", 8, """\
Signals that an ADC master has finished an SPI cycle.
Values:
* ``0``: MMIO master is either not armed or currently in a
SPI transfer.
* ``1``: MMIO master has finished.
This flag is on only when ``adc_arm`` is high. The flag does not
mean that data has been received successfully, only that the master
has finished it's SPI transfer."""),
Descr("adc_arm", 1, "read-write", 8, """\
Start a DAC master SPI transfer.
If ``adc_arm`` is raised from and the master is currently not in a SPI
transfer, the SPI master will start an SPI transfer and write data
into ``adc_recv_buf``.
If ``adc_arm`` is raised while the master is in an SPI transfer,
nothing changes.
If ``adc_arm`` is lowered while the master is in an SPI transfer,
nothing changes. The SPI cycle will continue to execute and it will
continue to write data to ``adc_recv_buf``.
If the SPI transfer finishes and ``adc_arm`` is still set to
1, then ``adc_finished`` is raised to 1. If ``adc_arm`` is lowered
in this state, then ``adc_finished`` is lowered.
Linear Technologies ADCs must not have their SPI transfers
interrupted. The transfer can be interrupted by
1. Interrupt the signal physically (i.e. pulling out cable connecting
the FPGA to the ADC)
2. Reset of the ADC master
3. Reset of the FPGA
4. Switching ``adc_sel`` to the control loop
If the ADC is interrupted then it will be in an unknown transfer
state. To recover from an unknown transfer state, set ``adc_sel``
to ``0b10`` and run a SPI transfer cycle. This will run the SPI
clock and flush the ADC buffer. The only other way is to power-cycle
the ADC.
If ``adc_sel`` is not set to 0 then the transfer will proceed
as normal, but no data will be received from the ADC."""),
Descr("adc_recv_buf", 18, "read-only", 8, """\
ADC Master receive buffer.
This buffer is stable if there is no ADC transfer caused by ``adc_arm``
is in process.
This register only changes if an SPI transfer is triggered by the MMIO
registers. SPI transfers by other masters will not affect this register.
buffer."""),
Descr("dac_sel", 2, "read-write", 8, """\
Select which on-FPGA SPI master controls the DAC.
Valid settings:
* ``0``: DAC is controlled by MMIO registers.
* ``0b10``: DAC 0 only. DAC is controlled by control loop."""),
Descr("dac_finished", 1, "read-only", 8, """\
Signals that the DAC master has finished transmitting data.
Values:
* ``0``: MMIO master is either not armed or currently in a
SPI transfer.
* ``1``: MMIO master has finished transmitting.
This flag is on only when ``dac_arm`` is high. The flag does not
mean that data has been received or transmitted successfully, only that
the master has finished it's SPI transfer."""),
Descr("dac_arm", 1, "read-write", 8, """\
Start a DAC master SPI transfer.
If ``dac_arm`` is raised from and the master is currently not in a SPI
transfer, the SPI master reads from the ``dac_send_buf`` register and sends
it over the wire to the DAC, while reading data from the DAC into
``dac_recv_buf``.
If ``dac_arm`` is raised while the master is in an SPI transfer,
nothing changes.
If ``dac_arm`` is lowered while the master is in an SPI transfer,
nothing changes. The SPI cycle will continue to execute and it will
continue to write data to ``dac_recv_buf``.
If the SPI transfer finishes and ``dac_arm`` is still set to
1, then ``dac_finished`` is raised to 1. If ``dac_arm`` is lowered
in this state, then ``dac_finished`` is lowered.
Analog Devices DACs can have their SPI transfers interrupted without
issue. However it is currently not possible to interrupt SPI transfers
in software without resetting the entire device.
If ``dac_sel`` is set to another master then the transfer will proceed
as normal, but no data will be sent to or received from the DAC."""),
Descr("dac_recv_buf", 24, "read-only", 8, """\
DAC master receive buffer.
This buffer is stable if there is no DAC transfer caused by ``dac_arm``
is in process.
This register only changes if an SPI transfer is triggered by the MMIO
registers. SPI transfers by other masters will not affect this register.
buffer."""),
Descr("dac_send_buf", 24, "read-write", 8, """\
DAC master send buffer.
Fill this buffer with a 24 bit Analog Devices DAC command. Updating
this buffer does not start an SPI transfer. To send data to the DAC,
fill this buffer and raise ``dac_arm``.
The DAC copies this buffer into an internal register when writing data.
Modifying this buffer during a transfer does not disrupt an in-process
transfer."""),
Descr("cl_assert_change", 1, "read-write", 1, """\
Flush parameter changes to control loop.
When this bit is raised from low to high, this signals the control
loop that it should read in new values from the MMIO registers.
While the bit is raised high, the control loop will read the constants
at most once.
When this bit is raised from high to low before ``cl_change_made``
is asserted by the control loop, nothing happens."""),
Descr("cl_change_made", 1, "read-only", 1, """\
Signal from the control loop that the parameters have been applied.
This signal goes high only while ``cl_assert_change`` is high. No
change will be applied afterwards while both are high."""),
Descr("cl_in_loop", 1, "read-only", 1, """\
This bit is high if the control loop is running."""),
Descr("cl_run_loop_in", 1, "read-write", 1, """\
Set this bit high to start the control loop."""),
Descr("cl_setpt_in", 18, "read-write", 1, """\
Setpoint of the control loop.
This is a twos-complement number in ADC units.
This is a parameter: see ``cl_assert_change``."""),
Descr("cl_P_in", 64, "read-write", 1, """\
Proportional parameter of the control loop.
This is a twos-complement fixed point number with 21 whole
bits and 43 fractional bits. This is applied to the error
in DAC units.
This is a parameter: see ``cl_assert_change``."""),
Descr("cl_I_in", 64, "read-write", 1, """\
Integral parameter of the control loop.
This is a twos-complement fixed point number with 21 whole
bits and 43 fractional bits. This is applied to the error
in DAC units.
This is a parameter: see ``cl_assert_change``."""),
Descr("cl_delay_in", 16, "read-write", 1, """\
Delay parameter of the loop.
This is an unsigned number denoting the number of cycles
the loop should wait between loop executions.
This is a parameter: see ``cl_assert_change``."""),
Descr("cl_cycle_count", 18, "read-only", 1, """\
Delay parameter of the loop.
This is an unsigned number denoting the number of cycles
the loop should wait between loop executions."""),
Descr("cl_z_pos", 20, "read-only", 1, """\
Control loop DAC Z position.
"""),
Descr("cl_z_measured", 18, "read-only", 1, """\
Control loop ADC Z position.
"""),
]

View File

@ -21,18 +21,23 @@ added manually and there is no sanity checking.
class BasicRegion:
""" Simple class for storing a RAM region. """
def __init__(self, origin, size, bus=None):
def __init__(self, origin, size, bus=None, registers=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.
:param registers: Dictionary where keys are names of addressable
areas in the region, values have "offset" and "width", and
optionally other parameters that help with describing the
subregion.
"""
self.origin = origin
self.size = size
self.bus = bus
self.registers = registers
def decoder(self):
"""
@ -56,7 +61,7 @@ class BasicRegion:
return lambda addr: addr[rightbits:32] == (self.origin >> rightbits)
def to_dict(self):
return {"origin" : self.origin, "size": self.size}
return {"origin" : self.origin, "size": self.size, "registers": self.registers}
def __str__(self):
return str(self.to_dict())

View File

@ -63,6 +63,7 @@ from liteeth.phy.mii import LiteEthPHYMII
from util import *
from swic import *
from extio import *
from region import BasicRegion
"""
Keep this diagram up to date! This is the wiring diagram from the ADC to
@ -104,6 +105,9 @@ io = [
# ("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")),
@ -149,6 +153,10 @@ class UpsilonSoC(SoCCore):
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):
self.bus.add_slave(name, bus, region)
self.soc_subregions[name] = registers
def add_blockram(self, name, size, connect_now=True):
mod = SRAM(size)
self.add_module(name, mod)
@ -163,24 +171,49 @@ class UpsilonSoC(SoCCore):
self.add_module(name, mod)
return mod
def add_picorv32(self, name, size=0x1000, origin=0x10000, param_origin=0x100000):
def add_picorv32(self, name, size=0x1000, origin=0x10000):
pico = PicoRV32(name, origin, origin+0x10)
self.add_module(name, pico)
self.bus.add_slave(name + "_dbg_reg", pico.debug_reg_read.bus,
SoCRegion(origin=None, size=pico.debug_reg_read.width, cached=False))
self.add_slave_with_registers(name + "_dbg_reg", pico.debug_reg_read.bus,
SoCRegion(origin=None, size=pico.debug_reg_read.width, cached=False),
pico.debug_reg_read.registers)
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",
BasicRegion(origin=origin, size=size, bus=ram_iface.buses[1]))
self.bus.add_slave(name + "_ram", ram_iface.buses[0],
SoCRegion(origin=None, size=size, cached=True))
self.add_slave_with_registers(name + "_ram", ram_iface.buses[0],
SoCRegion(origin=None, size=size, cached=True),
None)
def picorv32_add_cl(self, name, param_origin=0x100000):
pico = self.get_module(name)
param_iface = pico.add_cl_params(param_origin, name + "_cl.json")
self.bus.add_slave(name + "_cl", param_iface,
SoCRegion(origin=None, size=size, cached=False))
def add_AD5791(self, name, **kwargs):
args = SPIMaster.AD5791_PARAMS
args.update(kwargs)
spi = SPIMaster(**args)
self.add_module(name, spi)
return spi
def add_LT_adc(self, name, **kwargs):
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"])
spi = SPIMaster(**args)
self.add_module(name, spi)
return spi
def __init__(self,
variant="a7-100",
local_ip="192.168.2.50",
@ -199,6 +232,11 @@ class UpsilonSoC(SoCCore):
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 = {}
"""
These source files need to be sorted so that modules
that rely on another module come later. For instance,
@ -263,16 +301,49 @@ class UpsilonSoC(SoCCore):
# Add pins
platform.add_extension(io)
self.submodules.spi0 = SPIMaster(
platform.request("module_reset"),
platform.request("dac_miso_0"),
platform.request("dac_mosi_0"),
platform.request("dac_sck_0"),
platform.request("dac_ss_L_0"),
)
self.bus.add_slave("spi0", self.spi0.bus, SoCRegion(origin=None, size=self.spi0.addr_space_size, cached=False))
# Add control loop DACs and ADCs.
self.add_picorv32("pico0")
# XXX: I don't have the time to restructure my code to make it
# elegant, that comes when things work
module_reset = platform.request("module_reset")
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.add_preemptive_interface("dac0_PI", 2, self.dac0)
self.add_slave_with_registers("dac0", self.dac0_PI.buses[0],
SoCRegion(origin=None, size=self.dac0.width, cached=False),
self.dac0.registers)
self.pico0.mmap.add_region("dac0",
BasicRegion(origin=0x200000, size=self.dac0.width,
bus=self.dac0_PI.buses[1],
registers=self.dac0.registers))
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.add_preemptive_interface("adc0_PI", 2, self.adc0)
self.add_slave_with_registers("adc0", self.adc0_PI.buses[0],
SoCRegion(origin=None, size=self.adc0.width, cached=False),
self.adc0.registers)
self.pico0.mmap.add_region("adc0",
BasicRegion(origin=0x300000, size=self.adc0.width,
bus=self.adc0_PI.buses[1],
registers=self.adc0.registers))
def do_finalize(self):
with open('soc_subregions.json', 'wt') as f:
import json
json.dump(self.soc_subregions, f)
def main():
from config import config

View File

@ -182,20 +182,22 @@ class RegisterInterface(LiteXModule):
bus_logic(self.mainbus, main_case)
bus_logic(self.picobus, pico_case)
def dump_json(self, filename):
""" Dump description of offsets to JSON file. """
d = {}
# Generate addresses
self.addresses = {}
for reg, off in self.registers:
d[reg.name] = {
self.addresses[reg.name] = {
"width" : reg.width,
"direction" : reg.direction,
"offset": off
}
import json
with open(filename, 'wt') as f:
json.dump(d, f)
class RegisterRead(LiteXModule):
pico_registers = {
"ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "t1", "t2",
"s0/fp", "s1", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2",
"s3", "s4", "s5", "s6", "s7", "t3", "t4", "t5", "t6",
}
registers = {name: {"origin" : num * 4, "size" : 4, "rw": False} for num, name in enumerate(pico_registers)}
""" Inspect PicoRV32 registers via Wishbone bus. """
def __init__(self):
self.regs = [Signal(32) for i in range(1,32)]
@ -233,7 +235,7 @@ class PicoRV32(LiteXModule):
:param origin: Origin of the region for the PicoRV32.
:param dumpname: File to dump offsets within the region (common to
both Pico and Main CPU).
:return: Interface used by main cpu to control variables.
:return: Parameter module (used for accessing metadata).
"""
params = RegisterInterface(
SpecialRegister.from_tuples(
@ -245,9 +247,9 @@ class PicoRV32(LiteXModule):
("zpos", "PW", 32),
))
self.add_module("cl_params", params)
self.mmap.add_region("cl_params", BasicRegion(origin, params.width, params.picobus))
self.mmap.add_region("cl_params", BasicRegion(origin, params.width, params.picobus, params.addresses))
params.dump_json(dumpname)
return params.mainbus
return params
def __init__(self, name, start_addr=0x10000, irq_addr=0x10010, stackaddr=0x100FF):
self.name = name
@ -292,7 +294,6 @@ class PicoRV32(LiteXModule):
self.d_dat_w.status.eq(mem_wdata),
]
# NOTE: need to compile to these extact instructions
self.specials += Instance("picorv32",
p_COMPRESSED_ISA = 1,
p_ENABLE_MUL = 1,