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:
parent
223d2f98c6
commit
2e98c0229d
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
205
gateware/soc.py
205
gateware/soc.py
|
@ -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")
|
||||
|
|
205
gateware/swic.py
205
gateware/swic.py
|
@ -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()
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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))
|
||||
|
|
@ -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()
|
|
@ -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}
|
|
@ -0,0 +1,6 @@
|
|||
from registers import *
|
||||
|
||||
class SPI(RegisterRegion):
|
||||
def __init__(self, spiwid, origin, **regs):
|
||||
self.spiwid = spiwid
|
||||
super().__init__(origin, **regs)
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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]))
|
|
@ -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 (;;) ;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue