diff --git a/.gitignore b/.gitignore index 5616c66..5b1922a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ build/opensbi/ build/upsilon/ swic/*.bin swic/*.elf +swic/*mmio.h +linux/*mpy +linux/mmio.py +build/venv diff --git a/build/Makefile b/build/Makefile index b5bc7f4..aa83144 100644 --- a/build/Makefile +++ b/build/Makefile @@ -23,6 +23,12 @@ openFPGALoader: cd openFPGALoader/build && cmake .. cd openFPGALoader/build && cmake --build . +#### Local pip + +venv-create: + python3 -m venv venv + . venv/bin/activate && pip install mpy-cross + ###### Containers ### Hardware container @@ -51,8 +57,8 @@ hardware-get: docker cp upsilon-hardware:/home/user/upsilon/gateware/csr.json ../boot/ docker cp upsilon-hardware:/home/user/upsilon/gateware/soc_subregions.json ../boot/ docker cp upsilon-hardware:/home/user/upsilon/gateware/pico0.json ../boot/ - docker cp upsilon-hardware:/home/user/upsilon/gateware/mmio.py ../boot/ - docker cp upsilon-hardware:/home/user/upsilon/gateware/pico0_mmio.h ../boot/ + docker cp upsilon-hardware:/home/user/upsilon/gateware/mmio.py ../linux/ + docker cp upsilon-hardware:/home/user/upsilon/gateware/pico0_mmio.h ../swic/ hardware-clean: -docker container stop upsilon-hardware -docker container rm upsilon-hardware diff --git a/linux/Makefile b/linux/Makefile new file mode 100644 index 0000000..44eecd2 --- /dev/null +++ b/linux/Makefile @@ -0,0 +1,9 @@ +.PHONY: all + +.SUFFIXES: .mpy .py + +MPY=picorv32.mpy registers.mpy spi.mpy waveform.mpy mmio.mpy + +all: $(MPY) +.py.mpy: + . ../build/venv/bin/activate && mpy-cross $< diff --git a/linux/picorv32.py b/linux/picorv32.py index 8d8066f..4071be9 100644 --- a/linux/picorv32.py +++ b/linux/picorv32.py @@ -2,6 +2,13 @@ from registers import * class PicoRV32(Immutable): def __init__(self, ram, params, ram_pi): + """ + :param ram: Instance of FlatArea containing the executable space + of the PicoRV32. + :param params: Instance of RegisterRegion. This register region + contains CPU register information, the enable/disable bit, etc. + :param ram_pi: Register that controls ram read/write access. + """ super().__init__() self.ram = ram @@ -11,19 +18,25 @@ class PicoRV32(Immutable): self.make_immutable() def load(self, filename, force=False): + """ Load file (as bytes) into PicoRV32. + :param filename: File to load. + :param force: If True, turn off the PicoRV32 even if it's running. + """ 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()) + self.ram.mem8.load(f.read()) def enable(self): + """ Start the PicoRV32. """ self.ram_pi.v = 1 self.params.enable.v = 1 def dump(self): + """ Dump all status information about the PicoRV32. """ return self.params.dump() def test_pico(pico, filename, cl_I): diff --git a/linux/registers.py b/linux/registers.py index 25362d1..e2f7473 100644 --- a/linux/registers.py +++ b/linux/registers.py @@ -1,53 +1,134 @@ +# Copyright 2024 (C) Peter McGoron +# +# This file is a part of Upsilon, a free and open source software project. +# For license terms, refer to the files in `doc/copying` in the Upsilon +# source distribution. import machine class Immutable: + """ Makes attributes immutable after calling ``make_immutable``. """ + def __init__(self): - super().__setattr__("_has_init", False) + self._has_init = False def make_immutable(self): self._has_init = True def __setattr__(self, name, val): + # If the immutable class has not been initialized, then hasattr + # will return False, and setattr will work as normal. if hasattr(self, "_has_init") and self._has_init: raise NameError(f'{name}: {self.__class__.__name__} is immutable') + + # Call standard setattr to set class attribute super().__setattr__(name, val) -class FlatArea(Immutable): - def __init__(self, origin, num_words): - super().__init__() +class Accessor(Immutable): + """ Wraps accesses to a memory region, allowing for byte or word level + access. + """ + + _accessor = None + """ Object used to access memory directly. This is either machine.mem8 + or machine.mem32. + """ + + _ind_conv = None + """ Integer used to convert from addressing in the unit_size to byte + addressing. This is 1 for ``unit_size=8`` and 4 for ``unit_size=32``. + """ + + def __init__(self, origin, unit_size, size_in_units): + """ + :param origin: Origin of the memory region. + :param unit_size: The size in bits of the chunks read by this class. + Acceptable values are 8 (byte-size) and 32 (word-size). + :param size_in_units: The accessable size of the memory region in + units of the specified unit_size. + """ self.origin = origin - self.num_words = num_words + self.unit_size = unit_size - self.make_immutable() + if unit_size == 8: + self._accessor = machine.mem8 + self._ind_conv = 1 + elif unit_size == 32: + self._accessor = machine.mem32 + self._ind_conv = 4 + else: + raise Exception("Accessor can only take unit size 8 or 32") + + self.size_in_units = size_in_units 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] + if i < 0 or i >= self.size_in_units: + raise IndexError(f"Index {i} out of bounds of {self.size_in_units}") + return self._accessor[self.origin + self._ind_conv*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 + if i < 0 or i >= self.size_in_units: + raise IndexError(f"Index {i} out of bounds of {self.size_in_units}") + self._accessor[self.origin + self._ind_conv*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})") + def load(self, arr, start=0): + """ Load an array into this memory location. - for num, b in enumerate(arr): + :param arr: Array where each value in the array can be fit into an + integer of bitsize unit_size. + :param start: What offset in the memory region to start writing data + to. + """ + for num,b in enumerate(arr,start=start): self[num] = b - for num, b in enumerate(arr): + for num,b in enumerate(arr,start=start): 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)] + """ Return an array containing the values in the memory region. """ + return [self[i] for i in range(0, self.size_in_units)] + +class FlatArea(Immutable): + """ RAM region. RAM regions have no registers inside of them and can be + accessed at byte-level granularity. + """ + + mem8 = None + """ Instance of Accessor for byte-level access. """ + + mem32 = None + """ Instance of Accessor for word-level access. """ + + def __init__(self, origin, num_words): + """ + :param origin: Origin of the memory region. + :param num_words: Number of accessable words in the memory region. + """ + super().__init__() + + self.mem8 = Accessor(origin, 8, num_words*4) + self.mem32 = Accessor(origin, 32, num_words) + + self.make_immutable() class Register(Immutable): + """ Wraps a single register that has a maxmimum bitlength of 1 word. + + Accesses to registers are done using the ``v`` attribute. Writes to + ``v`` will write to the underlying memory area, and reads of ``v`` + will read the underlying value. + """ + + loc = None + """ Location of the register in memory. """ + def __init__(self, loc, **kwargs): + """ + This class accepts keyword arguments, which are placed in the + register object as attributes. This can be used to document if the + register is read-only, etc. + """ super().__init__() self.loc = loc @@ -65,7 +146,24 @@ class Register(Immutable): machine.mem32[self.loc] = newval class RegisterRegion(Immutable): + """ Holds multiple registers that are in the same Wishbone bus region. + The registers are attributes of the object and are set at instantiation + time. + """ + + _names = None + """ List of names of registers in the register region. """ + + _origin = None + """ Origin of the memory region containing the registers. """ + def __init__(self, origin, **regs): + """ + :param origin: Origin of the memory region containing the registers. + :param regs: Dictionary of registers that are placed in the object + as attributes. + """ + super().__init__() self._origin = origin @@ -77,4 +175,7 @@ class RegisterRegion(Immutable): self.make_immutable() def dump(self): + """ Return a dictionary containing the values of all the registers + in the region. + """ return {n:getattr(self,n).v for n in self._names} diff --git a/linux/waveform.py b/linux/waveform.py index a022cfc..d9c8f00 100644 --- a/linux/waveform.py +++ b/linux/waveform.py @@ -9,20 +9,35 @@ class Waveform(Immutable): self.make_immutable() - def run_waveform(self, wf, timer, timer_spacing, do_loop): + def run(self, wf, timer_spacing, do_loop = False): + """ Start waveform with signal. + + :param wf: Array of integers that describe the waveform. + These are twos-complement 20-bit integers. + :param timer_spacing: The amount of time to wait between + points on the waveform. + :param do_loop: If True, the waveform will repeat. + """ + self.stop() + + self.ram_pi.v = 0 + self.ram.mem32.load(wf) + + self.regs.wform_width.v = len(wf) + self.regs.timer_spacing.v = timer_spacing + + self.regs.do_loop.v = do_loop + self.ram_pi.v = 1 + self.regs.run.v = 1 + + def stop(self): + """ Stop the waveform and wait until it is ready. """ 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 + def dump(self): + """ Dump contents of control registers. """ + return self.regs.dump()