diff --git a/build/Makefile b/build/Makefile index 190ce88..1197ca7 100644 --- a/build/Makefile +++ b/build/Makefile @@ -5,6 +5,7 @@ # source distribution. .PHONY: images f4pga buildroot litex clone help attach hardware-image \ + install-software openFPGALoader pytftp \ buildroot-image upsilon-hardware.tar.gz upsilon-opensbi.tar.gz upsilon-buildroot.tar.gz ###### Images @@ -12,6 +13,15 @@ images: hardware-image buildroot-image opensbi-image +###### Install Software + +install-software: openFPGALoader + +openFPGALoader: + git clone https://github.com/trabucayre/openFPGALoader + mkdir openFPGALoader/build + cd openFPGALoader/build && cmake .. + cd openFPGALoader/build && cmake --build . ###### Containers @@ -38,6 +48,8 @@ hardware-get: docker cp upsilon-hardware:/home/user/upsilon/gateware/build/digilent_arty/gateware/digilent_arty.bit ../boot/ docker cp upsilon-hardware:/home/user/upsilon/gateware/arty.dtb ../boot/ docker cp upsilon-hardware:/home/user/upsilon/gateware/mmio.py ../boot/ + docker cp upsilon-hardware:/home/user/upsilon/gateware/csr.json ../boot/ + docker cp upsilon-hardware:/home/user/upsilon/gateware/csr_bitwidth.json ../boot/ hardware-clean: -docker container stop upsilon-hardware -docker container rm upsilon-hardware @@ -112,6 +124,9 @@ buildroot-clean: ###### Execute +OPENFPGALOADER=./openFPGALoader/build/openFPGALoader +flash: + ${OPENFPGALOADER} -c digilent ../boot/digilent_arty.bit tftp: cd ../boot && py3tftp --host 192.168.1.100 -p 6969 -v diff --git a/client/noise_test.py b/client/noise_test.py index d7e54e5..408c369 100644 --- a/client/noise_test.py +++ b/client/noise_test.py @@ -6,22 +6,55 @@ For license terms, refer to the files in `doc/copying` in the Upsilon source distribution. """ -from pssh.clients import SSHClient +from pssh.clients import SSHClient # require parallel-ssh import numpy as np import matplotlib.pyplot as plt import pandas as pd import sys def sign_extend(value, bits): + """ + Interpret ``value`` as a twos-complement integer of ``bits`` length. + + :param value: Twos-complement integer with finite bit width. + :param bits: Bit length of ``value``. + :return: ``value`` converted to a Python integer. + """ + + # Check the sign bit of the integer. is_signed = (value >> (bits - 1)) & 1 == 1 + # If not signed, just return the integer. if not is_signed: return value + # Otherwise, + # 1. Do an explicit twos-complement negation + # 2. Mask all the non-sign bits + # This returns the positive value as a standard Python integer. + # Then the function negates the positive integer to get the negative + # one back. return -((~value + 1) & ((1 << (bits - 1)) - 1)) +################### +# Boilerplate +################### + +# Start a SSH connection to the server. client = SSHClient('192.168.1.50', user='root', pkey='~/.ssh/upsilon_key') +# Upload the script. client.scp_send('../linux/noise_test.py', '/root/noise_test.py') +# Run the script. out = client.run_command('micropython noise_test.py') +################ +# Script Handler +################ +""" +The ramp script outputs a list of lines, each with two values separated by one +space. The first value is the DAC setting, the second value is the ADC setting. +This script gets all of those values, averages them by DAC value, and plots +it. +""" + current_dac = None current_adc = [] x_ax = [] diff --git a/doc/docker.md b/doc/docker.md index bab82d9..d6860aa 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -11,17 +11,15 @@ Change directory to `build`. ## Installing OpenFPGALoader -Install [openFPGALoader][1]. This utility entered the Ubuntu repositories -in 23.04. Install and compile it if you do not have it. Install the udev rule -so that admin access is not required to load FPGA bitstreams. +Install [openFPGALoader][1]. If this program is not in your repositories, +run `make openFPGALoader` to fetch and install the program. [1]: https://trabucayre.github.io/openFPGALoader/index.html ## Setup Rootless Docker Docker allows you to run programs in containers, which are isolated -environments. Upsilon development (at the Maglab) uses Docker for -reproducibility: the environment can be set up automatically, and re-setup +environments. Build environments can be set up automatically, and re-setup whenever needed. If you have issues with docker, try adding to `~/.config/docker/daemon.json` @@ -33,7 +31,7 @@ If you have issues with docker, try adding to `~/.config/docker/daemon.json` ## Download and Install Python3 -Install `python3-venv` (or `python3-virtualenv`) and `python3-pip`. +Install `python3` and `python3-pip`. ## Clone External Repositories @@ -46,7 +44,7 @@ Plug in your router/switch to an ethernet port on your computer. If your computer is usually wired to the network, you will need another ethernet port (a PCI card is ideal, but a USB-Ethernet port works). -Set the ethernet port to static ip `192.168.1.100/24`, netmask 255.255.255.0, +Set the ethernet port to static ip `192.168.1.100/24`, netmask `255.255.255.0`, gateway `192.168.1.1`. Make sure this is not the default route. Make sure to adjust your firewall to allow traffic on the 192.168.1.0/24 range. @@ -82,9 +80,9 @@ launch the TFTP server. Keep this terminal open. ## Flash FPGA -Plug in your FPGA into the USB slot. Then run - - openFPGALoader -c digilent upsilon/boot/digilent_arty.bit +Plug in your FPGA into the USB slot. If you have installed openFPGALoader +by your package manager, run `make OPENFPGALOADER=openfpgaloader flash`. +If you installed it using `make openFPGALoader`, then just run `make flash`. In a second you should see messages in the TFTP terminal. This means your controller is sucessfully connected to your computer. @@ -113,9 +111,12 @@ Wait about a minute for Linux to boot. If you cannot access the FPGA through SSH, you can launch a shell through UART. +You will need to install [LiteX](https://github.com/enjoy-digital/litex). +Download and run `litex_setup.py`. + Run `litex_term /dev/ttyUSB1`. You should get messages in the window with the TFTP server that the FPGA has connected to the server. Eventually you -will get a login prompt: you have sucessfully loaded Upsilon onto your FPGA. +will get a login prompt (username `root` password `upsilon`). ## Copy Library diff --git a/gateware/csr2mp.py b/gateware/csr2mp.py index 3171b56..e3d4997 100644 --- a/gateware/csr2mp.py +++ b/gateware/csr2mp.py @@ -11,6 +11,7 @@ # # TODO: Devicetree? +import collections import argparse import json import sys @@ -19,9 +20,9 @@ class MMIORegister: def __init__(self, name, read_only=False, number=1, exntype=None): """ Describes a MMIO register. - :param name: The name of the MMIO register. This name must be the - same as the pin name used in ``csr.json``, except for any - numerical suffix. + :param name: The name of the MMIO register, excluding the prefix + defining its module (i.e. ``base_``) and excluding its + numerical suffix. :param read_only: True if the register is read only. Defaults to ``False``. :param number: The number of MMIO registers with the same name @@ -33,121 +34,181 @@ class MMIORegister: self.number = number self.exntype = exntype -def mmio_factory(dac_num, exntype): - def f(name, read_only=False): - return MMIORegister(name, read_only, numer=dac_num, exntype=exntype) - return f - + # These are filled in by the CSR file. + self.size = None -class MicroPythonCSRGenerator: - def __init__(self, csrjson, bitwidthjson, registers, outf): +def mmio_factory(num, exntype): + """ + Return a function that simplifies the creation of instances of + :py:class:`MMIORegister` with the same number and exception type. + + :param num: Number of registers with the same name (minus suffix). + :param exntype: MicroPython exception type. + :return: A function ``f(name, read_only=False)``. Each argument is + the same as the one in the initializer of py:class:`MMIORegister`. + """ + def f(name, read_only=False): + return MMIORegister(name, read_only, number=num, exntype=exntype) + return f + +class CSRHandler: + """ + Class that wraps the CSR file and fills in registers with information + from those files. + """ + def __init__(self, csrjson, bitwidthjson, registers): """ - This class generates a MicroPython wrapper for MMIO registers. + Reads in the CSR files. :param csrjson: Filename of a LiteX "csr.json" file. :param bitwidthjson: Filename of an Upsilon "bitwidthjson" file. - :param registers: A list of + :param registers: A list of :py:class:`MMIORegister`s. + :param outf: Output file. """ self.registers = registers self.csrs = json.load(open(csrjson)) self.bws = json.load(open(bitwidthjson)) - self.file = outf - def get_reg(self, name, num=None): - """ Get the base address of the register. """ - if num is None: - regname = f"base_{name}" + def update_reg(self, reg): + """ + Fill in size information from bitwidth json file. + + :param reg: The register. + :raises Exception: When the bit width exceeds 64. + """ + b = self.bws[reg.name] + if b <= 8: + reg.size = 8 + elif b <= 16: + reg.size = 16 + elif b <= 32: + reg.size = 32 + elif b <= 64: + reg.size = 64 else: - regname = f"base_{name}_{num}" + raise Exception(f"unsupported width {b} in {reg.name}") + + def get_reg_addr(self, reg, num=None): + """ + Get address of register. + + :param reg: The register. + :param num: Select which register number. Registers without + numerical suffixes require ``None``. + :return: The address. + """ + if num is None: + regname = f"base_{reg.name}" + else: + regname = f"base_{reg.name}_{num}" return self.csrs["csr_registers"][regname]["addr"] - def get_accessor(self, name, num=None): - """ Return a list of Micropython machine.mem accesses that can - be used to read/write to a MMIO register. +class InterfaceGenerator: + """ + Interface for file generation. Implement the unimplemented functions + to generate a CSR interface for another language. + """ - Since the Micropython API only supports accesses up to the - natural word size of the processor, multiple accesses must be made - for 64 bit registers. + def __init__(self, csr, outf): """ - b = self.bws[name] - if b <= 8: - return [f'machine.mem8[{self.get_reg(name,num)}]'] - elif b <= 16: - return [f'machine.mem16[{self.get_reg(name,num)}]'] - elif b <= 32: - return [f'machine.mem32[{self.get_reg(name,num)}]'] - elif b <= 64: - return [f'machine.mem32[{self.get_reg(name,num)}]', - f'machine.mem32[{self.get_reg(name,num) + 4}]'] - else: - raise Exception('unsupported width', b) + :param CSRHandler csr: + :param FileIO outf: + """ + self.outf = outf + self.csr = csr def print(self, *args): - print(*args, end='', file=self.file) - - def print_write_register(self, indent, varname, name, num): - acc = self.get_accessor(name,num) - if len(acc) == 1: - self.print(f'{indent}{acc[0]} = {varname}\n') - else: - assert len(acc) == 2 - self.print(f'{indent}{acc[0]} = {varname} & 0xFFFFFFFF\n') - self.print(f'{indent}{acc[1]} = {varname} >> 32\n') - - def print_read_register(self, indent, varname, name, num): - acc = self.get_accessor(name,num) - if len(acc) == 1: - self.print(f'{indent}return {acc[0]}\n') - else: - assert len(acc) == 2 - self.print(f'{indent}return {acc[0]} | ({acc[1]} << 32)\n') - - def print_fun(self, optype, reg, pfun): - """Print out a read/write function for an MMIO register. - - :param optype: is set to "read" or "write" (the string). - :param reg: is the dictionary containing the register info. - :param pfun: is set to {self.print_write_register} or {self.print_read_register} """ - name = reg['name'] - regnum = reg['total'] - exntype = reg['exntype]' + Print to the file specified in the initializer and without + newlines. + """ + print(*args, end='', file=self.outf) - self.print(f'def {optype}_{name}(') + def fun(self, reg, optype): + """ Print function for reads/writes to register. """ + pass + def header(self): + """ Print header of file. """ + pass + + def print_file(self): + self.print(self.header()) + for r in self.csr.registers: + self.print(self.fun(r, "read")) + if not r.read_only: + self.print(self.fun(r, "write")) + +class MicropythonGenerator(InterfaceGenerator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_accessor(self, reg, num): + addr = self.csr.get_reg_addr(reg, num) + if reg.size in [8, 16, 32]: + return [f"machine.mem{reg.size}[{addr}]"] + return [f"machine.mem32[{addr}]", f"machine.mem32[{addr + 4}]"] + + def print_write_register(self, indent, varname, reg, num): + acc = self.get_accessor(reg, num) + if len(acc) == 1: + return f'{indent}{acc[0]} = {varname}\n' + else: + assert len(acc) == 2 + return f'{indent}{acc[0]} = {varname} & 0xFFFFFFFF\n' + \ + f'{indent}{acc[1]} = {varname} >> 32\n' + + def print_read_register(self, indent, varname, reg, num): + acc = self.get_accessor(reg, num) + if len(acc) == 1: + return f'{indent}return {acc[0]}\n' + else: + assert len(acc) == 2 + return f'{indent}return {acc[0]} | ({acc[1]} << 32)\n' + + def fun(self, reg, optype): + rs = "" + def a(s): + nonlocal rs + rs = rs + s + a(f'def {optype}_{reg.name}(') printed_argument = False if optype == 'write': - self.print('val') + a('val') printed_argument = True - - if regnum != 1: - if printed_argument: - self.print(', ') - self.print('num') - self.print('):\n') - - if regnum == 1: - pfun('\t', 'val', name, None) + pfun = self.print_write_register else: - for i in range(0,regnum): - if i == 0: - self.print(f'\tif ') - else: - self.print(f'\telif ') - self.print(f'num == {i}:\n') - pfun('\t\t', 'val', name, i) - self.print(f'\telse:\n') - self.print(f'\t\traise {exntype}(regnum)\n') - self.print('\n') + pfun = self.print_read_register - def print_file(self): - self.print('import machine\n') - self.print('class InvalidDACException(Exception):\n\tpass\n') - self.print('class InvalidADCException(Exception):\n\tpass\n') - for reg in self.registers: - self.print_fun('read', reg, self.print_read_register) - if not reg['read_only']: - self.print_fun('write', reg, self.print_write_register) + if reg.number != 1: + if printed_argument: + a(', ') + a('num') + a('):\n') + + if reg.number == 1: + a(pfun('\t', 'val', reg, None)) + else: + for i in range(0,reg.number): + if i == 0: + a(f'\tif ') + else: + a(f'\telif ') + a(f'num == {i}:\n') + a(pfun('\t\t', 'val', reg, i)) + a(f'\telse:\n') + a(f'\t\traise {r.exntype}(regnum)\n') + a('\n') + + return rs + + def header(self): + return """import machine +class InvalidDACException(Exception): + pass +class InvalidADCException(Exception): + pass +""" if __name__ == "__main__": dac_num = 8 @@ -172,14 +233,8 @@ if __name__ == "__main__": MMIORegister("cl_word_in"), MMIORegister("cl_start_cmd"), MMIORegister("cl_finish_cmd", read_only=True), - -# {"read_only": False, "name": "wf_arm", "total": dac_num}, -# {"read_only": False, "name": "wf_halt_on_finish", "total": dac_num}, -# {"read_only": True, "name": "wf_finished", "total": dac_num}, -# {"read_only": True, "name": "wf_running", "total": dac_num}, -# {"read_only": False, "name": "wf_time_to_wait", "total": dac_num}, -# {"read_only": False, "name": "wf_refresh_start", "total": dac_num}, -# {"read_only": True, "name": "wf_refresh_finished", "total": dac_num}, -# {"read_only": False, "name": "wf_start_addr", "total": dac_num}, - ] - MicroPythonCSRGenerator("csr.json", "csr_bitwidth.json", registers, sys.stdout).print_file() + ] + csrh = CSRHandler(sys.argv[1], sys.argv[2], registers) + for r in registers: + csrh.update_reg(r) + MicropythonGenerator(csrh, sys.stdout).print_file()