refactor csr2mp and docker Makefile

This commit is contained in:
Peter McGoron 2023-06-26 15:49:20 -04:00
parent f30f6f1ad5
commit 130e1775ac
4 changed files with 220 additions and 116 deletions

View File

@ -5,6 +5,7 @@
# source distribution. # source distribution.
.PHONY: images f4pga buildroot litex clone help attach hardware-image \ .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 buildroot-image upsilon-hardware.tar.gz upsilon-opensbi.tar.gz upsilon-buildroot.tar.gz
###### Images ###### Images
@ -12,6 +13,15 @@
images: hardware-image buildroot-image opensbi-image 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 ###### 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/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/arty.dtb ../boot/
docker cp upsilon-hardware:/home/user/upsilon/gateware/mmio.py ../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: hardware-clean:
-docker container stop upsilon-hardware -docker container stop upsilon-hardware
-docker container rm upsilon-hardware -docker container rm upsilon-hardware
@ -112,6 +124,9 @@ buildroot-clean:
###### Execute ###### Execute
OPENFPGALOADER=./openFPGALoader/build/openFPGALoader
flash:
${OPENFPGALOADER} -c digilent ../boot/digilent_arty.bit
tftp: tftp:
cd ../boot && py3tftp --host 192.168.1.100 -p 6969 -v cd ../boot && py3tftp --host 192.168.1.100 -p 6969 -v

View File

@ -6,22 +6,55 @@ For license terms, refer to the files in `doc/copying` in the Upsilon
source distribution. source distribution.
""" """
from pssh.clients import SSHClient from pssh.clients import SSHClient # require parallel-ssh
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pandas as pd import pandas as pd
import sys import sys
def sign_extend(value, bits): 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 is_signed = (value >> (bits - 1)) & 1 == 1
# If not signed, just return the integer.
if not is_signed: if not is_signed:
return value 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)) 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') 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') client.scp_send('../linux/noise_test.py', '/root/noise_test.py')
# Run the script.
out = client.run_command('micropython noise_test.py') 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_dac = None
current_adc = [] current_adc = []
x_ax = [] x_ax = []

View File

@ -11,17 +11,15 @@ Change directory to `build`.
## Installing OpenFPGALoader ## Installing OpenFPGALoader
Install [openFPGALoader][1]. This utility entered the Ubuntu repositories Install [openFPGALoader][1]. If this program is not in your repositories,
in 23.04. Install and compile it if you do not have it. Install the udev rule run `make openFPGALoader` to fetch and install the program.
so that admin access is not required to load FPGA bitstreams.
[1]: https://trabucayre.github.io/openFPGALoader/index.html [1]: https://trabucayre.github.io/openFPGALoader/index.html
## Setup Rootless Docker ## Setup Rootless Docker
Docker allows you to run programs in containers, which are isolated Docker allows you to run programs in containers, which are isolated
environments. Upsilon development (at the Maglab) uses Docker for environments. Build environments can be set up automatically, and re-setup
reproducibility: the environment can be set up automatically, and re-setup
whenever needed. whenever needed.
If you have issues with docker, try adding to `~/.config/docker/daemon.json` 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 ## Download and Install Python3
Install `python3-venv` (or `python3-virtualenv`) and `python3-pip`. Install `python3` and `python3-pip`.
## Clone External Repositories ## 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 computer is usually wired to the network, you will need another ethernet
port (a PCI card is ideal, but a USB-Ethernet port works). 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 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. 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 ## Flash FPGA
Plug in your FPGA into the USB slot. Then run Plug in your FPGA into the USB slot. If you have installed openFPGALoader
by your package manager, run `make OPENFPGALOADER=openfpgaloader flash`.
openFPGALoader -c digilent upsilon/boot/digilent_arty.bit 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 In a second you should see messages in the TFTP terminal. This means your
controller is sucessfully connected to your computer. 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 If you cannot access the FPGA through SSH, you can launch a shell through
UART. 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 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 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 ## Copy Library

View File

@ -11,6 +11,7 @@
# #
# TODO: Devicetree? # TODO: Devicetree?
import collections
import argparse import argparse
import json import json
import sys import sys
@ -19,9 +20,9 @@ class MMIORegister:
def __init__(self, name, read_only=False, number=1, exntype=None): def __init__(self, name, read_only=False, number=1, exntype=None):
""" """
Describes a MMIO register. Describes a MMIO register.
:param name: The name of the MMIO register. This name must be the :param name: The name of the MMIO register, excluding the prefix
same as the pin name used in ``csr.json``, except for any defining its module (i.e. ``base_``) and excluding its
numerical suffix. numerical suffix.
:param read_only: True if the register is read only. Defaults to :param read_only: True if the register is read only. Defaults to
``False``. ``False``.
:param number: The number of MMIO registers with the same name :param number: The number of MMIO registers with the same name
@ -33,121 +34,181 @@ class MMIORegister:
self.number = number self.number = number
self.exntype = exntype self.exntype = exntype
def mmio_factory(dac_num, exntype): # These are filled in by the CSR file.
def f(name, read_only=False): self.size = None
return MMIORegister(name, read_only, numer=dac_num, exntype=exntype)
return f
class MicroPythonCSRGenerator: def mmio_factory(num, exntype):
def __init__(self, csrjson, bitwidthjson, registers, outf): """
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 csrjson: Filename of a LiteX "csr.json" file.
:param bitwidthjson: Filename of an Upsilon "bitwidthjson" 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.registers = registers
self.csrs = json.load(open(csrjson)) self.csrs = json.load(open(csrjson))
self.bws = json.load(open(bitwidthjson)) self.bws = json.load(open(bitwidthjson))
self.file = outf
def get_reg(self, name, num=None): def update_reg(self, reg):
""" Get the base address of the register. """ """
if num is None: Fill in size information from bitwidth json file.
regname = f"base_{name}"
: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: 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"] return self.csrs["csr_registers"][regname]["addr"]
def get_accessor(self, name, num=None): class InterfaceGenerator:
""" Return a list of Micropython machine.mem accesses that can """
be used to read/write to a MMIO register. 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 def __init__(self, csr, outf):
natural word size of the processor, multiple accesses must be made
for 64 bit registers.
""" """
b = self.bws[name] :param CSRHandler csr:
if b <= 8: :param FileIO outf:
return [f'machine.mem8[{self.get_reg(name,num)}]'] """
elif b <= 16: self.outf = outf
return [f'machine.mem16[{self.get_reg(name,num)}]'] self.csr = csr
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)
def print(self, *args): 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'] Print to the file specified in the initializer and without
regnum = reg['total'] newlines.
exntype = reg['exntype]' """
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 printed_argument = False
if optype == 'write': if optype == 'write':
self.print('val') a('val')
printed_argument = True printed_argument = True
pfun = self.print_write_register
if regnum != 1:
if printed_argument:
self.print(', ')
self.print('num')
self.print('):\n')
if regnum == 1:
pfun('\t', 'val', name, None)
else: else:
for i in range(0,regnum): pfun = self.print_read_register
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')
def print_file(self): if reg.number != 1:
self.print('import machine\n') if printed_argument:
self.print('class InvalidDACException(Exception):\n\tpass\n') a(', ')
self.print('class InvalidADCException(Exception):\n\tpass\n') a('num')
for reg in self.registers: a('):\n')
self.print_fun('read', reg, self.print_read_register)
if not reg['read_only']: if reg.number == 1:
self.print_fun('write', reg, self.print_write_register) 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__": if __name__ == "__main__":
dac_num = 8 dac_num = 8
@ -172,14 +233,8 @@ if __name__ == "__main__":
MMIORegister("cl_word_in"), MMIORegister("cl_word_in"),
MMIORegister("cl_start_cmd"), MMIORegister("cl_start_cmd"),
MMIORegister("cl_finish_cmd", read_only=True), MMIORegister("cl_finish_cmd", read_only=True),
]
# {"read_only": False, "name": "wf_arm", "total": dac_num}, csrh = CSRHandler(sys.argv[1], sys.argv[2], registers)
# {"read_only": False, "name": "wf_halt_on_finish", "total": dac_num}, for r in registers:
# {"read_only": True, "name": "wf_finished", "total": dac_num}, csrh.update_reg(r)
# {"read_only": True, "name": "wf_running", "total": dac_num}, MicropythonGenerator(csrh, sys.stdout).print_file()
# {"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()