Upsilon standard library; integrate waveform; overhaul code generation

1. Add a new Upsilon MicroPython standard library in the linux/
   subdirectory. This puts all the submodules into classes with methods
   for ease of access.
2. Totally rewrite mmio.py code generation. Instead of just dumping
   registers, the build system now instantiates classes which
   encapsulate the module in question.
3. Split the PicoRV32 special register interface away from the PicoRV32.
   It is now the PeekPokeInterface, which will be used in the future to
   implement register control for Waveform and SPI.
4. Integrate Waveform into the design. Has not been tested yet.
This commit is contained in:
Peter McGoron 2024-03-11 04:31:30 +00:00
parent 223d2f98c6
commit 2e98c0229d
18 changed files with 597 additions and 558 deletions

View File

@ -67,23 +67,25 @@ these are registers that reside in the memory region. The keys of the registers
are
1. ``origin``: offset of the register from the beginning of the memory.
2. ``size``: Size of the register in bytes. Right now this is always ``4``,
2. ``bitwidth``: Size of the register in bits. Right now cannot be more than ``32``.
even if the writable space is smaller. Always access with words.
3. ``rw``: True if writable and False if not. Sometimes this is not there
because the writable might be dynamic or must be inferred from other
properties.
4. ``direction``: Specifcally for registers that are Main-write-Pico-read
or Main-read-Pico-write. ``PR`` denotes Pico-read, ``PW`` denotes Pico-write.
4. ``direction``: For registers inside a ``PeekPokeInterface``, ``1`` for
writable by the Main CPU, ``2`` for writable by SWiC, and blank for read-only.
``pico0.json`` (and other PicoRV32 JSON files) are JSON objects whose keys are
memory regions. Their values are objects with keys:
1. ``origin``: Absolute position of the memory region.
2. ``width``: Width of the memory region in bytes.
2. ``bitwidth``: Width of the memory region in bits.
3. ``registers``: Either ``null`` (uniform region, like above), or an object
whose keys are the names of registers in the region. The values of these
keys have the same interpretation as ``soc_subregions.json`` above.
A read only register is not necessarily constant!
====================
System Within a Chip
====================
@ -177,71 +179,33 @@ the instructions). To copy the machine code to ``test.bin``, execute::
riscv64-unknown-elf-objcopy -O binary -j .text test.elf test.bin
This code can now be loaded into the PicoRV32's ram. The next code sections
will be written in Micropython.
The standard library has ``load()`` as a method for each PicoRV32 instance.
First include the Micropython MMIO locations::
First import the SoC memory locations::
from mmio import *
Next import ``machine``. You will almost always need this library.::
Then load the file (the file needs to be uploaded to the SoC)::
import machine
pico0.load(filename)
Next import the PicoRV32 standard library. (Currently it is hard-coded for pico0,
this might change if more CPUs are required.)::
Fill in any registers::
import picorv32
pico0.regs.cl_I = 115200
Next load the code into Micropython::
Then run it::
with open('test.bin', 'rb') as f:
prog = f.read()
pico0.enable()
This assumes that ``test.bin`` has been uploaded to the FPGA. We now turn off
the CPU and switch control of the RAM to the main CPU::
To inspect how the core is running, use dump::
machine.mem32[pico0_enable] = 0
machine.mem32[pico0ram_iface_master_select] = 0
from pprint import pprint
pprint(pico0.dump())
Both of these values default to ``0`` on startup.
Now the program can be loaded into the PicoRV32's ram::
assert len(prog) < 0xFFF
for offset, b in enumerate(prog, start=pico0_ram):
machine.mem8[offset] = b
The first line of the code makes sure that the code will overflow from the
RAM region and write other parts of memory. (TODO: this is hardcoded, and
the size of the region should also be written to memory.)
The loop goes though each byte of the program and writes it to the RAM,
starting at the beginning of the RAM. ``enumerate`` is just a fancy Python way
of a for loop with an increasing counter.
As a sanity test, check that the program was written correctly::
for i in range(len(prog)):
assert machine.mem8[pico_ram + i] == prog[i]
This can detect overwrite errors (a write to a read-only area will silently
fail) and cache mismatch errors.
After the program is loaded, the CPU can finally be started::
machine.mem32[pico0ram_iface_master_select] = 1
assert machine.mem8[pico0_ram] == 0
machine.mem32[pico0_enable] = 1
The first line switches control of the RAM to the PicoRV32. The second line
checks if the switch worked. If this line fails, most likely the preemptive
interface is not properly connected to the PicoRV32 (or my code is buggy).
The final line starts the CPU.
The state of the CPU can be inspected using ``picorv32.dump()``. This will
tell you if the CPU is in a trap state and what registers the CPU is currently
reading.
This will tell you about all the memory mapped registers, all the PicoRV32
registers, the program counter, etc. It also includes the ``trap`` condition,
which is an integer whose values are defined in ``picorv32.v``. ``0`` indicate
normal execution (or stopped).
================
Computer Control

View File

@ -147,6 +147,39 @@ The Wishbone cache is very confusing and causes custom Wishbone bus code to
not work properly. Since a lot of this memory is volatile you should never
enable the cache (possible exception: SRAM).
---------------------------------------------------------
Working Around LiteX using pre_finalize and mmio_closures
---------------------------------------------------------
LiteX runs code prior to calling ``finalize()``, such as CSR allocation,
that makes it very difficult to write procedural code without preallocating
lengths.
Upsilon solves this with an ugly hack called ``pre_finalize``, which runs at
the end of the SoC main module instantiation. All pre_finalize functions are
put into a list which is run with no arguments and with their return result
ignored.
``pre_finalize`` calls are usually due to ``PreemptiveInterface``, which uses
CSR registers.
There is another ugly hack, ``mmio_closures``, which is used to generate the
``mmio.py`` library. The ``mmio.py`` library groups together relevant memory
regions and registers into instances of MicroPython classes. The only good
way to do this is to generate the code for ``mmio.py`` at instantiation time,
but the origin of each memory region is not known at instantiation time. The
functions have to be delayed until after memory locations are allocated, but
there is no hook in LiteX to do that, and the only interface I can think of
that one can use to look at the origins is ``csr.json``.
The solution is a list of closures that return strings that will be put into
``mmio.py``. They take one argument, ``csrs``, the ``csr.json`` file as a
Python dictionary. The closures use the memory location origin in ``csrs``
to generate code with the correct offsets.
Note that the ``csr.json`` file casefolds the memory locations into lowercase
but keeps CSR registers as-is.
====================
System Within a Chip
====================
@ -313,3 +346,9 @@ I overrode finalize and now things are broken
Each Migen module has a ``finalize()`` function inherited from the class. This
does code generation and calls ``do_finalize()``, which is a user-defined
function.
=========
TODO List
=========
Pseudo CSR bus for the main CPU?

View File

@ -15,45 +15,51 @@ class Waveform(LiteXModule):
by reading from RAM. """
public_registers = {
"run" : {
"origin": 0,
"size": 4,
"rw": True,
},
"cntr": {
"origin": 0x4,
"size": 4,
"rw": False,
},
"do_loop": {
"origin": 0x8,
"size" : 4,
"rw" : True,
},
"finished_or_ready": {
"origin": 0xC,
"size" : 4,
"rw" : False,
},
"wform_size": {
"origin": 0x10,
"size": 4,
"rw" : True,
},
"timer": {
"origin": 0x14,
"size" : 4,
"rw" : False,
},
"timer_spacing": {
"origin" : 0x18,
"size" : 4,
"rw" : True,
}
"run" : Register(
origin=0,
bitwidth=1,
rw=True,
),
"cntr": Register(
origin=0x4,
bitwidth=16,
rw=False,
),
"do_loop": Register(
origin=0x8,
bitwidth= 1,
rw= True,
),
"finished_or_ready": Register(
origin=0xC,
bitwidth= 2,
rw= False,
),
"wform_width": Register(
origin=0x10,
bitwidth=16,
rw= True,
),
"timer": Register(
origin=0x14,
bitwidth= 16,
rw= False,
),
"timer_spacing": Register(
origin= 0x18,
bitwidth= 16,
rw= True,
)
}
width = 0x20
def mmio(self, origin):
r = ""
for name, reg in self.public_registers.items():
r += f'{name} = Register(loc={origin + reg.origin}, bitwidth={reg.bitwidth}, rw={reg.rw}),'
return r
def __init__(self,
ram_start_addr = 0,
spi_start_addr = 0x10000000,
@ -80,7 +86,7 @@ class Waveform(LiteXModule):
timer = Signal(timer_wid)
timer_spacing = Signal(timer_wid)
self.comb += If(b.cyc & b.stb & ~b.ack,
self.sync += If(b.cyc & b.stb & ~b.ack,
Case(b.adr, {
0x0: If(b.we,
run.eq(b.dat_w[0]),
@ -188,43 +194,49 @@ class SPIMaster(Module):
# armed and finished with a transmission.
# The second bit is the "ready" bit, when the master is
# not armed and ready to be armed.
"ready_or_finished": {
"origin" : 0,
"width" : 4,
"rw": False,
},
"ready_or_finished": Register(
origin= 0,
bitwidth= 2,
rw=False,
),
# One bit to initiate a transmission cycle. Transmission
# cycles CANNOT be interrupted.
"arm" : {
"origin": 4,
"width": 4,
"rw": True,
},
"arm" : Register(
origin=4,
bitwidth=1,
rw=True,
),
# Data sent from the SPI slave.
"from_slave": {
"origin": 8,
"width": 4,
"rw": False,
},
"from_slave": Register(
origin=8,
bitwidth=32,
rw=False,
),
# Data sent to the SPI slave.
"to_slave": {
"origin": 0xC,
"width": 4,
"rw": True
},
"to_slave": Register(
origin=0xC,
bitwidth=32,
rw=True
),
# Same as ready_or_finished, but halts until ready or finished
# goes high. Dangerous, might cause cores to hang!
"wait_ready_or_finished": {
"origin": 0x10,
"width": 4,
"rw" : False,
},
"wait_ready_or_finished": Register(
origin=0x10,
bitwidth=2,
rw= False,
),
}
def mmio(self, origin):
r = ""
for name, reg in self.public_registers.items():
r += f'{name} = Register(loc={origin + reg.origin},bitwidth={reg.bitwidth},rw={reg.rw}),'
return r
""" Wrapper for the SPI master verilog code. """
def __init__(self, rst, miso, mosi, sck, ss_L,
polarity = 0,

View File

@ -19,6 +19,24 @@ module implements a basic Wishbone bus generator. All locations have to be
added manually and there is no sanity checking.
"""
class Register:
""" Register describes a register in a memory region. It must have an
origin and a bit width.
Register stores all fields as attributes.
"""
def __init__(self, origin, bitwidth, **kwargs):
self.origin = origin
self.bitwidth = bitwidth
# Assign all values in kwargs as attributes.
self.__dict__.update(kwargs)
def _to_dict(self):
""" This function has an underscore in front of it in order
for it to not get picked up in this comprehension. """
return {k: getattr(self,k) for k in dir(self) if not k.startswith("_")}
class BasicRegion:
""" Simple class for storing a RAM region. """
def __init__(self, origin, size, bus=None, registers=None):
@ -29,9 +47,7 @@ class BasicRegion:
(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.
areas in the region, values are instances of Register.
"""
self.origin = origin
@ -61,7 +77,12 @@ class BasicRegion:
return lambda addr: addr[rightbits:32] == (self.origin >> rightbits)
def to_dict(self):
return {"origin" : self.origin, "width": self.size, "registers": self.registers}
return {
"origin" : self.origin,
"width": self.size,
"registers": {k:v._to_dict() for k,v in self.registers.items()}
if self.registers is not None else None
}
def __str__(self):
return str(self.to_dict())
@ -115,5 +136,124 @@ class MemoryMap(LiteXModule):
def do_finalize(self):
slaves = [(self.regions[n].decoder(), self.adapt(self.regions[n].bus))
for n in self.regions]
# TODO: timeout using InterconnectShared?
self.submodules.decoder = Decoder(self.masterbus, slaves, register=True)
class PeekPokeInterface(LiteXModule):
""" Module that exposes registers to two Wishbone masters.
Registers can be written to by at most one CPU. Some of them are
read-only for both.
NOTE: The interface only accepts up to 32 bit registers and does not
respect wstrb. All writes will be interpreted as word writes.
"""
def __init__(self):
self.firstbus = Interface(data_width = 32, address_width = 32, addressing="byte")
self.secondbus = Interface(data_width = 32, address_width = 32, addressing="byte")
# If an address is added, this is the next memory location
self.next_register_loc = 0
# Register description
self.public_registers = {}
# Migen signals
self.signals = {}
self.has_pre_finalize = False
def mmio(self, origin):
r = ""
for name, reg in self.public_registers.items():
can_write = True if reg.can_write == "1" else False
r += f'{name} = Register(loc={origin + reg.origin}, bitwidth={reg.bitwidth}, rw={can_write}),'
return r
def add_register(self, name, can_write, bitwidth, sig=None):
""" Add a register to the memory area.
:param name: Name of the register in the description.
:param bitwidth: Width of the register in bits.
:param can_write: Which CPU can write to it. One of "1", "2" or
empty (none).
"""
if self.has_pre_finalize:
raise Exception("Cannot add register after pre finalization")
if sig is None:
sig = Signal(bitwidth)
if name in self.public_registers:
raise NameError(f"Register {name} already allocated")
self.public_registers[name] = Register(
origin=self.next_register_loc,
bitwidth=bitwidth,
can_write=can_write,
)
self.signals[name] = sig
# Each location is padded in memory space to 32 bits.
# Push every 32 bits to a new memory location.
while bitwidth > 0:
self.next_register_loc += 0x4
bitwidth -= 32
def pre_finalize(self):
second_case = {"default": self.secondbus.dat_r.eq(0xFACADE)}
first_case = {"default": self.firstbus.dat_r.eq(0xEDACAF)}
if self.has_pre_finalize:
raise Exception("Cannot pre_finalize twice")
self.has_pre_finalize = True
for name in self.public_registers:
sig = self.signals[name]
reg = self.public_registers[name]
if reg.bitwidth > 32:
raise Exception("Registers larger than 32 bits are not supported")
def write_case(bus):
return If(bus.we,
sig.eq(bus.dat_w),
).Else(
bus.dat_r.eq(sig)
)
def read_case(bus):
return bus.dat_r.eq(sig)
if reg.can_write == "2":
second_case[reg.origin] = write_case(self.secondbus)
first_case[reg.origin] = read_case(self.firstbus)
elif reg.can_write == "1":
second_case[reg.origin] = read_case(self.secondbus)
first_case[reg.origin] = write_case(self.firstbus)
elif reg.can_write == "":
second_case[reg.origin] = read_case(self.secondbus)
first_case[reg.origin] = read_case(self.firstbus)
else:
raise Exception("Invalid can_write: ", reg.can_write)
self.width = round_up_to_pow_2(self.next_register_loc)
# The width is a power of 2 (0b1000...). This bitlen is the
# number of bits to read, starting from 0.
bitlen = (self.width - 1).bit_length()
def bus_logic(bus, cases):
self.sync += If(bus.cyc & bus.stb & ~bus.ack,
Case(bus.adr[0:bitlen], cases),
bus.ack.eq(1)
).Elif(~bus.cyc,
bus.ack.eq(0))
bus_logic(self.firstbus, first_case)
bus_logic(self.secondbus, second_case)
def do_finalize(self):
if not self.has_pre_finalize:
raise Exception("pre_finalize required")

View File

@ -176,9 +176,16 @@ class UpsilonSoC(SoCCore):
"""
pi = PreemptiveInterface(slave_bus, addressing=addressing, name=name)
self.add_module(name, pi)
self.add_slave_with_registers(name, pi.add_master(),
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):
@ -189,36 +196,88 @@ class UpsilonSoC(SoCCore):
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):
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)
pico = PicoRV32(name, origin, origin+0x10, param_origin)
self.add_module(name, pico)
# Attach the register region to the main CPU.
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.public_registers)
# 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()))
BasicRegion(origin=origin, size=size, bus=ram_pi.add_master(name)))
def picorv32_add_cl(self, name, param_origin=0x100000):
def picorv32_add_cl(self, name):
""" Add a register area containing the control loop parameters to the
PicoRV32.
:param param_origin: The origin of the parameters in the PicoRV32's
address space. """
"""
pico = self.get_module(name)
params = pico.add_cl_params(param_origin, name + "_cl.json")
self.add_slave_with_registers(name + "_cl", params.mainbus,
SoCRegion(origin=None, size=params.width, cached=False),
params.public_registers)
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.
@ -227,13 +286,7 @@ class UpsilonSoC(SoCCore):
"""
args = SPIMaster.AD5791_PARAMS
args.update(kwargs)
spi = SPIMaster(**args)
self.add_module(name, spi)
pi = self.add_preemptive_interface_for_slave(name + "_PI", spi.bus,
spi.width, spi.public_registers, "byte")
return spi, pi
return self.add_spi_master(name, **args)
def add_LT_adc(self, name, **kwargs):
""" Adds a Linear Technologies ADC SPI master to the SoC.
@ -249,19 +302,23 @@ class UpsilonSoC(SoCCore):
conv_high = Signal()
self.comb += conv_high.eq(~kwargs["ss_L"])
spi = SPIMaster(**args)
self.add_module(name, spi)
return self.add_spi_master(name, **args)
pi = self.add_preemptive_interface_for_slave(name + "_PI", spi.bus,
spi.width, spi.public_registers, "byte")
return spi, pi
def add_waveform(self, name):
wf = Waveform()
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,
@ -287,6 +344,15 @@ class UpsilonSoC(SoCCore):
# 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,
@ -351,23 +417,15 @@ class UpsilonSoC(SoCCore):
# 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")
self.pico0.mmap.add_region("wf0",
BasicRegion(origin=0x400000, size=self.wf0.width,
bus=self.wf0_PI.add_master(),
registers=self.wf0.public_registers))
# Waveform generator RAM storage
self.add_blockram("wf0_ram", 4096)
self.wf0.add_ram(self.wf0_ram_PI.add_master(), 4096)
module_reset = platform.request("module_reset")
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,
@ -376,11 +434,8 @@ class UpsilonSoC(SoCCore):
sck=platform.request("dac_sck_0"),
ss_L=platform.request("dac_ss_L_0"),
)
self.pico0.mmap.add_region("dac0",
BasicRegion(origin=0x200000, size=self.dac0.width,
bus=self.dac0_PI.add_master(),
registers=self.dac0.public_registers))
self.wf0.add_spi(self.dac0_PI.add_master())
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,
@ -389,51 +444,37 @@ class UpsilonSoC(SoCCore):
ss_L=platform.request("adc_conv_0"),
spi_wid=18,
)
self.pico0.mmap.add_region("adc0",
BasicRegion(origin=0x300000, size=self.adc0.width,
bus=self.adc0_PI.add_master(),
registers=self.adc0.public_registers))
self.picorv32_add_pi("pico0", "adc0", "adc0_PI", 0x300000, self.adc0.width, self.adc0.public_registers)
# Pre-finalizations. Very bad hacks.
self.pico0_ram_PI.pre_finalize()
self.dac0_PI.pre_finalize()
self.adc0_PI.pre_finalize()
self.wf0_ram_PI.pre_finalize()
self.wf0_PI.pre_finalize()
# Run pre-finalize
for f in self.pre_finalize:
f()
def do_finalize(self):
with open('soc_subregions.json', 'wt') as f:
json.dump(self.soc_subregions, 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(csr_file):
def generate_main_cpu_include(closures, csr_file):
""" Generate Micropython include from a JSON file. """
with open('mmio.py', 'wt') as out:
print("from micropython import const", file=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 key in csrs["csr_registers"]:
if key.startswith("pico0"):
print(f'{key} = const({csrs["csr_registers"][key]["addr"]})', file=out)
for f in closures:
print(f(csrs), file=out)
with open('soc_subregions.json', 'rt') as f:
subregions = json.load(f)
from config import config
soc =UpsilonSoC(**config)
builder = Builder(soc, csr_json="csr.json", compile_software=True, compile_gateware=True)
builder.build()
for key in subregions:
if subregions[key] is None:
print(f'{key} = const({csrs["memories"][key.lower()]["base"]})', file=out)
else:
print(f'{key}_base = const({csrs["memories"][key.lower()]["base"]})', file=out)
print(f'{key} = {subregions[key].__repr__()}', file=out)
def main():
from config import config
soc =UpsilonSoC(**config)
builder = Builder(soc, csr_json="csr.json", compile_software=True)
builder.build()
generate_main_cpu_include("csr.json")
if __name__ == "__main__":
main()
generate_main_cpu_include(soc.mmio_closures, "csr.json")

View File

@ -40,21 +40,26 @@ class PreemptiveInterface(LiteXModule):
self.buses = []
self.name = name
self.master_names = []
self.pre_finalize_done = False
def add_master(self):
def add_master(self, name):
""" Adds a new master bus to the PI.
:return: The interface to the bus.
:param name: Name associated with this master.
"""
if self.pre_finalize_done:
raise Exception(self.name + ": Attempted to modify PreemptiveInterface after pre-finalize")
self.master_names.append(name)
iface = Interface(data_width=32, address_width=32, addressing=self.addressing)
self.buses.append(iface)
return iface
def pre_finalize(self):
def pre_finalize(self, dump_name):
# NOTE: DUMB HACK! CSR bus logic is NOT generated when inserted at do_finalize time!
if self.pre_finalize_done:
@ -64,6 +69,12 @@ class PreemptiveInterface(LiteXModule):
masters_len = len(self.buses)
if masters_len > 1:
self.master_select = CSRStorage(masters_len, name='master_select', description='RW bitstring of which master interconnect to connect to')
# FIXME: Implement PreemptiveInterfaceController module to limit proliferation
# of JSON files
with open(dump_name, 'wt') as f:
import json
json.dump(self.master_names, f)
def do_finalize(self):
if not self.pre_finalize_done:
@ -137,126 +148,6 @@ class PreemptiveInterface(LiteXModule):
else:
self.comb += Case(self.master_select.storage, cases)
class SpecialRegister:
""" Special registers used for small bits of communiciation. """
def __init__(self, name, direction, width):
"""
:param name: Name of the register, seen in mmio.py.
:param direction: One of "PR" (pico-read main-write) or "PW" (pico-write main-read).
:param width: Width in bits, from 0 exclusive to 32 inclusive.
"""
assert direction in ["PR", "PW"]
assert 0 < width and width <= 32
self.name = name
self.direction = direction
self.width = width
def from_tuples(*tuples):
return [SpecialRegister(*args) for args in tuples]
class RegisterInterface(LiteXModule):
""" Defines "registers" that are either exclusively CPU-write Pico-read
or CPU-read pico-write. These registers are stored as flip-flops. """
# TODO: Add no-write registers that are ready only for both ends.
# Also make more flexible signal sizes.
def __init__(self, registers):
"""
:param special_registers: List of instances of SpecialRegister.
"""
self.picobus = Interface(data_width = 32, address_width = 32, addressing="byte")
self.mainbus = Interface(data_width = 32, address_width = 32, addressing="byte")
pico_case = {"default": self.picobus.dat_r.eq(0xFACADE)}
main_case = {"default": self.picobus.dat_r.eq(0xEDACAF)}
# Tuple list of (SpecialRegister, offset)
self.registers = [(reg, num*0x4) for num, reg in enumerate(registers)]
for reg, off in self.registers:
# Round up the width of the stored signal to a multiple of 8.
wid = round_up_to_word(reg.width)
sig = Signal(wid)
def make_write_case(target_bus):
""" Function to handle write selection for ``target_bus``. """
writes = []
if wid >= 8:
writes.append(If(target_bus.sel[0],
sig[0:8].eq(target_bus.dat_w[0:8])))
if wid >= 16:
writes.append(If(target_bus.sel[1],
sig[8:16].eq(target_bus.dat_w[8:16])))
if wid >= 32:
writes.append(If(target_bus.sel[2],
sig[16:24].eq(target_bus.dat_w[16:24])))
writes.append(If(target_bus.sel[3],
sig[24:32].eq(target_bus.dat_w[24:32])))
return writes
if reg.direction == "PR":
pico_case[off] = self.picobus.dat_r.eq(sig)
main_case[off] = If(self.mainbus.we,
*make_write_case(self.mainbus)).Else(
self.mainbus.dat_r.eq(sig))
else:
main_case[off] = self.mainbus.dat_r.eq(sig)
pico_case[off] = If(self.picobus.we,
*make_write_case(self.picobus)).Else(
self.picobus.dat_r.eq(sig))
self.width = round_up_to_pow_2(sum([off for _, off in self.registers]))
# Since array indices are exclusive in python (unlike in Verilog),
# use the bit length of the power of 2, not the bit length of the
# maximum addressable value.
bitlen = self.width.bit_length()
def bus_logic(bus, cases):
self.sync += If(bus.cyc & bus.stb & ~bus.ack,
Case(bus.adr[0:bitlen], cases),
bus.ack.eq(1)
).Elif(~bus.cyc,
bus.ack.eq(0))
bus_logic(self.mainbus, main_case)
bus_logic(self.picobus, pico_case)
# Generate addresses
self.public_registers = {}
for reg, off in self.registers:
self.public_registers[reg.name] = {
"width" : reg.width,
"direction" : reg.direction,
"origin": off,
}
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",
}
public_registers = {name: {"origin" : num * 4, "width" : 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)]
self.bus = Interface(data_width = 32, address_width = 32, addressing="byte")
self.width = 0x100
cases = {"default": self.bus.dat_r.eq(0xdeaddead)}
for i, reg in enumerate(self.regs):
cases[i*0x4] = self.bus.dat_r.eq(reg)
# CYC -> transfer in progress
# STB -> data is valid on the input lines
self.sync += [
If(self.bus.cyc & self.bus.stb & ~self.bus.ack,
Case(self.bus.adr[0:7], cases),
self.bus.ack.eq(1),
).Elif(self.bus.cyc != 1,
self.bus.ack.eq(0)
)
]
def gen_pico_header(pico_name):
""" Generate PicoRV32 C header for this CPU from JSON file. """
import json
@ -284,44 +175,41 @@ def gen_pico_header(pico_name):
# Copyright (c) 2018 William D. Jones <thor0505@comcast.net>
# SPDX-License-Identifier: BSD-2-Clause
class PicoRV32(LiteXModule):
def add_cl_params(self, origin, dumpname):
def add_cl_params(self):
""" Add parameter region for control loop variables. Dumps the
region information to a JSON file `dumpname`.
: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: Parameter module (used for accessing metadata).
"""
params = RegisterInterface(
SpecialRegister.from_tuples(
("cl_I", "PR", 32),
("cl_P", "PR", 32),
("deltaT", "PR", 32),
("setpt", "PR", 32),
("zset", "PW", 32),
("zpos", "PW", 32),
))
self.add_module("cl_params", params)
self.mmap.add_region("cl_params", BasicRegion(origin, params.width, params.picobus, params.public_registers))
return params
def __init__(self, name, start_addr=0x10000, irq_addr=0x10010, stackaddr=0x100FF):
self.params.add_register("cl_I", "1", 32)
self.params.add_register("cl_P", "1", 32)
self.params.add_register("deltaT", "1", 32)
self.params.add_register("setpt", "1", 32)
self.params.add_register("zset", "2", 32)
self.params.add_register("zpos", "2", 32)
def __init__(self, name, start_addr=0x10000, irq_addr=0x10010, stackaddr=0x100FF, param_origin=0x100000):
self.name = name
self.masterbus = Interface(data_width=32, address_width=32, addressing="byte")
self.mmap = MemoryMap(self.masterbus)
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.params = PeekPokeInterface()
self.param_origin = param_origin
self.params.add_register("enable", "1", 1)
self.params.add_register("trap", "", 8)
self.params.add_register("debug_adr", "", 32)
self.params.add_register("dat_w", "", 32)
self.params.add_register("pc", "", 32)
self.params.add_register("opcode", "", 32)
self.debug_reg_read = RegisterRead()
reg_args = {}
for i in range(1,32):
reg_args[f"o_dbg_reg_x{i}"] = self.debug_reg_read.regs[i-1]
for num, reg in enumerate(["ra", "sp", "gp", "tp", "t0", "t1", "t2",
"s0_fp", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7",
"s2", "s3", "s4", "s5", "s6", "s7", "t3",
"t4", "t5", "t6",], start=1):
self.params.add_register(reg, "", 32)
reg_args[f"o_dbg_reg_x{num}"] = self.params.signals[reg]
mem_valid = Signal()
mem_instr = Signal()
@ -342,11 +230,8 @@ class PicoRV32(LiteXModule):
self.masterbus.bte.eq(0),
mem_ready.eq(self.masterbus.ack),
mem_rdata.eq(self.masterbus.dat_r),
]
self.comb += [
self.d_adr.status.eq(mem_addr),
self.d_dat_w.status.eq(mem_wdata),
self.params.signals["debug_adr"].eq(mem_addr),
self.params.signals["dat_w"].eq(mem_wdata),
]
self.specials += Instance("picorv32",
@ -356,7 +241,7 @@ class PicoRV32(LiteXModule):
p_PROGADDR_RESET=start_addr,
p_PROGADDR_IRQ =irq_addr,
p_STACKADDR = stackaddr,
o_trap = self.trap.status,
o_trap = self.params.signals["trap"],
o_mem_valid = mem_valid,
o_mem_instr = mem_instr,
@ -368,7 +253,7 @@ class PicoRV32(LiteXModule):
i_mem_rdata = mem_rdata,
i_clk = ClockSignal(),
i_resetn = self.resetpin.storage,
i_resetn = self.params.signals["enable"],
o_mem_la_read = Signal(),
o_mem_la_write = Signal(),
@ -391,13 +276,13 @@ class PicoRV32(LiteXModule):
o_trace_valid = Signal(),
o_trace_data = Signal(36),
o_dbg_insn_addr = self.dbg_insn_addr.status,
o_dbg_insn_opcode = self.dbg_insn_opcode.status,
o_dbg_insn_addr = self.params.signals["pc"],
o_dbg_insn_opcode = self.params.signals["opcode"],
**reg_args
)
def do_finalize(self):
self.mmap.finalize()
self.mmap.dump_mmap(self.name + ".json")
gen_pico_header(self.name)
self.mmap.finalize()

View File

@ -15,7 +15,7 @@ def round_up_to_pow_2(n):
# If n is a power of 2, then n - 1 has a smaller bit length than n.
# If n is not a power of 2, then n - 1 has the same bit length.
l = (n - 1).bit_length()
return 1 << (l + 1)
return 1 << l
def round_up_to_word(n):
""" Round up to 8, 16, or 32. """

View File

@ -1,48 +0,0 @@
# Copyright 2023 (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.
#
# Upsilon Micropython Standard Library.
from mmio import *
# Write a 20 bit twos-complement value to a DAC.
def dac_write_volt(val, num):
"""
Write a 20 bit twos-complement value to a DAC.
:param val: Two's complement 20 bit integer. The number is bitmasked
to the appropriate length, so negative Python integers are also
accepted. This DOES NOT check if the integer actually fits in 20
bits.
:param num: DAC number.
:raises Exception:
"""
write_dac_send_buf(1 << 20 | (val & 0xFFFFF), num)
write_dac_arm(1, num)
write_dac_arm(0, num)
# Read a register from a DAC.
def dac_read_reg(val, num):
write_dac_send_buf(1 << 23 | val, num)
write_dac_arm(1, num)
write_dac_arm(0, num)
return read_dac_recv_buf(num)
# Initialize a DAC by setting it's output value to 0, and
# removing the output restriction from the settings register.
def dac_init(num):
write_dac_sel(0,num)
dac_write_volt(0, num)
write_dac_send_buf(1 << 21 | 1 << 1, num)
write_dac_arm(1, num)
write_dac_arm(0, num)
return dac_read_reg(1 << 21, num)
# Read a value from an ADC.
def adc_read(num):
write_adc_arm(1, num)
write_adc_arm(0, num)
return read_adc_recv_buf(num)

View File

@ -1,31 +0,0 @@
from mmio import *
from comm import *
from sys import argv
# The DAC always have to be init and reset
dac_init(0)
write_dac_sel(1 << 1, 0)
write_adc_sel(2 << 1, 0)
def cl_cmd_write(cmd, val):
write_cl_word_in(val)
write_cl_cmd(1 << 7 | cmd)
write_cl_start_cmd(1)
while not read_cl_finish_cmd():
print('aa')
write_cl_start_cmd(0)
def cl_cmd_read(cmd):
write_cl_cmd(cmd)
write_cl_start_cmd(1)
while not read_cl_finish_cmd():
print('aa')
write_cl_start_cmd(0)
return read_cl_word_out()
cl_cmd_write(2, int(argv[1])) # Setpoint
cl_cmd_write(3, int(argv[2])) # P
cl_cmd_write(4, int(argv[3])) # I
cl_cmd_write(8, int(argv[4])) # Delay
cl_cmd_write(1, 1)

View File

@ -1,10 +0,0 @@
from comm import *
from time import sleep_ms
dac_init(0)
write_adc_sel(0,0)
for i in range(-300,300):
dac_write_volt(i, 0)
for j in range(0,20):
print(i, adc_read(0))

35
linux/picorv32.py Normal file
View File

@ -0,0 +1,35 @@
from registers import *
class PicoRV32(Immutable):
def __init__(self, ram, params, ram_pi):
super().__init__()
self.ram = ram
self.ram_pi = ram_pi
self.params = params
self.make_immutable()
def load(self, filename, force=False):
if not force and self.params.enable == 1:
raise Exception("PicoRV32 RAM cannot be modified while running")
self.params.enable.v = 0
self.ram_pi.v = 0
with open(filename, 'rb') as f:
self.ram.load(f.read())
def enable(self):
self.ram_pi.v = 1
self.params.enable.v = 1
def dump(self):
return self.params.dump()
def test_pico(pico, filename, cl_I):
pico.params.cl_I.v = cl_I
pico.load(filename, force=True)
pico.enable()
return pico.dump()

80
linux/registers.py Normal file
View File

@ -0,0 +1,80 @@
import machine
class Immutable:
def __init__(self):
super().__setattr__("_has_init", False)
def make_immutable(self):
self._has_init = True
def __setattr__(self, name, val):
if hasattr(self, "_has_init") and self._has_init:
raise NameError(f'{name}: {self.__class__.__name__} is immutable')
super().__setattr__(name, val)
class FlatArea(Immutable):
def __init__(self, origin, num_words):
super().__init__()
self.origin = origin
self.num_words = num_words
self.make_immutable()
def __getitem__(self, i):
if i < 0 or i >= self.num_words*4:
raise IndexError(f"Index {i} out of bounds of {self.num_words}")
return machine.mem8[self.origin + i]
def __setitem__(self, i, v):
if i < 0 or i >= self.num_words*4:
raise IndexError(f"Index {i} out of bounds of {self.num_words}")
machine.mem8[self.origin + i] = v
def load(self, arr):
l = len(arr)
if l >= self.num_words:
raise IndexError(f"{l} is too large for ram region ({self.num_words})")
for num, b in enumerate(arr):
self[num] = b
for num, b in enumerate(arr):
if self[num] != b:
raise MemoryError(f"{num}: {self[num]} != {b}")
def dump(self):
o = self.origin
return [machine.mem32[o + i*4] for i in range(0,self.num_words)]
class Register(Immutable):
def __init__(self, loc, **kwargs):
super().__init__()
self.loc = loc
for k in kwargs:
setattr(self, k, kwargs[k])
self.make_immutable()
@property
def v(self):
return machine.mem32[self.loc]
@v.setter
def v(self, newval):
machine.mem32[self.loc] = newval
class RegisterRegion(Immutable):
def __init__(self, origin, **regs):
super().__init__()
self._origin = origin
self._names = [r for r in regs]
for r, reg in regs.items():
setattr(self, r, reg)
self.make_immutable()
def dump(self):
return {n:getattr(self,n).v for n in self._names}

6
linux/spi.py Normal file
View File

@ -0,0 +1,6 @@
from registers import *
class SPI(RegisterRegion):
def __init__(self, spiwid, origin, **regs):
self.spiwid = spiwid
super().__init__(origin, **regs)

View File

@ -1,41 +0,0 @@
from micropython import const
import machine
from time import sleep_us
dac_sel = const(4026531844)
dac_arm = const(4026531852)
dac_fin = const(4026531848)
dac_from = const(4026531856)
dac_to = const(4026531860)
adc_fin = const(4026531864)
adc_arm = const(4026531868)
adc_dat = const(4026531872)
adc_sel = const(4026531840)
machine.mem8[dac_sel] = 1
def dac_comm(val):
machine.mem32[dac_to] = val
machine.mem8[dac_arm] = 1
while machine.mem8[dac_fin] == 0:
pass
machine.mem8[dac_arm] = 0
def dac_read(val):
dac_comm(1 << 23 | val)
dac_comm(0)
v = bin(machine.mem32[dac_from])
print(v, len(v) - 2)
# dac_comm(0b11010010001)
dac_comm(1 << 22 | 1 << 2)
dac_comm(1 << 21 | (1 << 1))
dac_read(1 << 21)
machine.mem8[adc_sel] = 1
def adc_read():
machine.mem8[adc_arm] = 1
sleep_us(5)
machine.mem8[adc_arm] = 0
return machine.mem32[adc_dat]

28
linux/waveform.py Normal file
View File

@ -0,0 +1,28 @@
from registers import *
class Waveform(Immutable):
def __init__(self, ram, ram_pi, regs):
super().__init__()
self.ram = ram
self.ram_pi = ram_pi
self.regs = regs
self.make_immutable()
def run_waveform(self, wf, timer, timer_spacing, do_loop):
self.regs.run = 0
self.regs.do_loop = 0
while self.regs.finished_or_ready == 0:
pass
self.ram_pi.v = 0
self.ram.load(wf)
self.regs.wform_width.v = len(wf)
self.regs.timer.v = timer
self.regs.timer_spacing.v = timer_spacing
self.regs.do_loop.v = do_loop
self.ram_pi.v = 1
self.regs.run.v = 1

View File

@ -1,23 +0,0 @@
import machine
from mmio import *
def read_file(filename):
with open(filename, 'rb') as f:
return f.read()
def run_program(prog, cl_I):
# Reset PicoRV32
machine.mem32[pico0_enable] = 0
machine.mem32[pico0ram_iface_master_select] = 0
offset = pico0_ram
for b in prog:
machine.mem8[offset] = b
offset += 1
for i in range(len(prog)):
assert machine.mem8[pico0_ram + i] == prog[i]
machine.mem32[pico0ram_iface_master_select] = 1
assert machine.mem8[pico0_ram] == 0
machine.mem32[pico0_enable] = 1

View File

@ -1,37 +0,0 @@
import machine
from mmio import *
trapcode = [
"normal",
"illegal rs1",
"illegal rs2",
"misalligned word",
"misalligned halfword",
"misalligned instruction",
"ebreak",
]
reg_names = [
"zero", "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",
]
def u(n):
# Converts possibly signed number to unsigned 32 bit.
return hex(n & 0xFFFFFFFF)
# XXX: Currently hardcoded for Pico0. Future versions should accept a
# class that has fields.
def dump():
print("Running:", "yes" if machine.mem32[pico0_enable] else "no")
print("Trap status:", trapcode[machine.mem32[pico0_trap]])
print("Bus address:", u(machine.mem32[pico0_d_adr]))
print("Bus write value:", u(machine.mem32[pico0_d_dat_w]))
print("Instruction address:", u(machine.mem32[pico0_dbg_insn_addr]))
print("Opcode:", u(machine.mem32[pico0_dbg_insn_opcode]))
# Skip the zero register, since it's always zero.
for num, name in enumerate(reg_names[1:],start=1):
print(name + ":", u(machine.mem32[pico0_dbg_reg + 0x4*num]))

View File

@ -1,10 +1,9 @@
#include <stdint.h>
#include "../boot/pico0_mmio.h"
void _start(void)
{
volatile uint32_t *write = (volatile uint32_t *)(0x100000 + 0x10);
volatile uint32_t *read = (volatile uint32_t *)( 0x100000 + 0x0);
*write = *read;
*PARAMS_ZPOS = *PARAMS_CL_I;
for (;;) ;
}