Skip to content

Commit

Permalink
regmap: Create a regio library to allow exporting tools along with re…
Browse files Browse the repository at this point in the history
…gmap

This change makes regmap a sub-package (in the Python sense) of a new top-level
regio package. This allows distributing the existing regio* tools in a way that
is consistent with standard Python distribution pactices. In-tree regio* stubs
have been created to provide backwards compatibility (the stubs are not used by
the distribution). All jinja2 templates are included in the distribution and are
installed along side the Python sources.

The source tree has been shuffled to conform to the distribution tool's needs.
  • Loading branch information
jranger-es-net committed Jan 17, 2024
1 parent 56275d8 commit 703c9aa
Show file tree
Hide file tree
Showing 55 changed files with 1,140 additions and 1,050 deletions.
14 changes: 11 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
[tool.poetry]
name = "regmap"
name = "regio"
version = "0.1.0"
description = "Library for defining and programmatically accessing registers"
description = "Library and tools for defining and programmatically accessing registers"
authors = []
packages = [{include = "regmap"}]
packages = [{include = "regio", from = "src"}]
include = ["src/regio/tools/templates"]

[tool.poetry.dependencies]
python = "^3.8"
Expand All @@ -22,6 +23,13 @@ shells = [
"ptpython",
]

[tool.poetry.scripts]
regio = "regio.tools.io:main"
regio-elaborate = "regio.tools.elaborate:main"
regio-flatten = "regio.tools.flatten:main"
regio-generate = "regio.tools.generate:main"
regio-info = "regio.tools.info:main"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
320 changes: 6 additions & 314 deletions regio
Original file line number Diff line number Diff line change
@@ -1,319 +1,11 @@
#!/usr/bin/env python3

import click
from pathlib import Path
import mmap
import pathlib
import sys
import struct
import os
import re

from yaml import load, dump
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper

struct_width_map = {
8 : "@B",
16 : "@H",
32 : "@I",
64 : "@L",
}

# Note: It is essential to use a memoryview here rather than relying on
# the struct module. Using the struct module definitely results in double
# reads to a register, possibly triggering side effects due to the
# extra read.
# See: https://stackoverflow.com/a/53492789
def build_reg_cast(mem, offset, reg):
addr = offset + reg['offset']
width = reg['width']
access = reg['access']
count = reg['count']

# Create a sub-memoryview of just the bytes we're interested in
reg_bytes = mem[addr:addr+((width>>3) * count)]
# Cast the memview to the correct width
reg_cast = reg_bytes.cast(struct_width_map[width])

return reg_cast

def update_field(reg_val, fld, fld_val):
shift = fld['offset']
mask = (1 << fld['width']) - 1

reg_val &= ~(mask << shift)
reg_val |= ((fld_val & mask) << shift)

return reg_val

SYSFS_BUS_PCI_DEVICES = "/sys/bus/pci/devices"

@click.command()
@click.option('-s', '--select',
help="Select a specific PCIe device (domain:bus:device.function)",
default="0000:d8:00.0",
show_default=True)
@click.option('-b', '--bar',
help="Select which PCIe bar to use for IO",
default=2,
show_default=True)
@click.option('-r', '--regmap',
help="Path to fully elaborated regmap yaml file for this device",
default='/usr/share/esnet-smartnic/esnet-smartnic-top-ir.yaml',
show_default=True,
type=click.File('r'))
@click.argument('register')
def main(select, bar, regmap, register):

regmap = load(regmap, Loader=Loader)

if not "toplevel" in regmap:
print("ERROR: No toplevel defined in regmap. Are you sure that's a regmap file?")
sys.exit(1)

toplevel = regmap['toplevel']
if not "bars" in toplevel:
print("ERROR: No bars defined in toplevel. Are you sure that's a regmap file?")
sys.exit(1)

bars = toplevel['bars']
if not bar in bars:
print("ERROR: Bar {} is not defined in regmap (only {} are defined)".format(bar, ", ".join(["{:d}".format(b) for b in bars])))
sys.exit(1)

device_path = Path(SYSFS_BUS_PCI_DEVICES) / select
if not device_path.exists():
print("ERROR: Selected device does not exist. Is the FPGA loaded and the PCIe bus rescanned?")
sys.exit(1)

resource_path = Path(SYSFS_BUS_PCI_DEVICES) / select / "resource{:d}".format(bar)
if not resource_path.exists():
print("ERROR: Selected bar does not exist for the selected device. Is the FPGA loaded?")
sys.exit(1)

with resource_path.open('r+b') as f:
m = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ | mmap.PROT_WRITE)
mv = memoryview(m)

from itertools import chain, repeat

# Check if we have an assignment
lhs, rhs, *_ = chain(register.split('='), repeat(None, 5))

blk_name, reg_expr, fld_name, *_ = chain(lhs.split('.'), repeat(None, 5))

if reg_expr is not None:
# Check if we have an index or slice
matches = re.match(r'^(?P<reg_name>[^\[\]]+)'
r'(\[(?P<index>[0-9]+)\]|'
r'\[((?P<slice_min>[0-9]+)?:)?(?P<slice_max>[0-9]+)?(:(?P<slice_inc>[0-9]+)?)?\])?$', reg_expr)
if matches is None:
print("ERROR: Unable to parse requested register expression '{}'".format(reg_expr))
sys.exit(1)

reg_name = matches.group('reg_name')
index = matches.group('index')
slice_min = matches.group('slice_min')
slice_max = matches.group('slice_max')
slice_inc = matches.group('slice_inc')

# Convert any slice parameters to ints
if index is not None:
slice_min = int(index)
slice_max = int(index) + 1
slice_inc = 1
else:
if slice_min is not None:
slice_min = int(slice_min)
if slice_max is not None:
slice_max = int(slice_max)
if slice_inc is not None:
slice_inc = int(slice_inc)
else:
reg_name = None
index = None
slice_min = None
slice_max = None
slice_inc = None

if blk_name is None:
print("ERROR: No block name specified in {}".format(lhs))
sys.exit(1)

# Build up the block name to block instance map
region_map = {}
for i, v in enumerate(bars[bar]['decoder']['regions']):
# skip any padding blocks
if 'anon' in v:
continue

if 'name' in v:
region_map.update({ v['name'] : i })
else:
region_map.update({ v['block']['name'] : i })

if not blk_name in region_map:
print("ERROR: Bar {} does not contain a block called {}".format(bar, blk_name))
sys.exit(1)

region = bars[bar]['decoder']['regions'][region_map[blk_name]]
blk = region['block']

if reg_name is not None:
# Build up the register name to register instance map
reg_map = {v['name'] : i for (i, v) in enumerate(blk['regs'])}

# Make sure we are referring to a valid register name in this block
if reg_name not in reg_map:
print("ERROR: Block {} does not contain a register called {}".format(blk_name, reg_name))
sys.exit(1)

reg = blk['regs'][reg_map[reg_name]]
else:
reg = None

if fld_name is not None:
# Build up the field name to field instance map for this register
fld_map = {v['name'] : i for (i, v) in enumerate(reg['fields'])}

# Make sure we are referring to a valid field name in this register
if fld_name not in fld_map:
print("ERROR: Register {} does not contain a field called {}".format(reg_name, fld_name))
sys.exit(1)

fld = reg['fields'][fld_map[fld_name]]
else:
fld = None

# Validate additional constraints for assignments
if rhs is not None:
# Assignments must include at least a register name
if reg is None:
print("ERROR: Assignments must provide at least a register name")
sys.exit(1)

# Try to convert the rhs to an int directly
if type(rhs) is str:
try:
rhs = int(rhs, 0)
except ValueError:
pass

# Try to convert the rhs to an int via a field enum
if type(rhs) is str:
if fld is not None and 'enum_hex' in fld:
# Build up the enum name to value instance map for this field
enum_map = {v : k for k, v in fld['enum_hex'].items()}
if rhs not in enum_map:
print("ERROR: Field {} does not contain an enum called {}".format(fld_name, rhs))
sys.exit(1)

# Convert the enum to a value
if type(enum_map[rhs]) is str:
rhs = int(enum_map[rhs], 16)
else:
rhs = enum_map[rhs]

if type(rhs) is not int:
print("ERROR: Can't convert {} to a value".format(rhs))
sys.exit(1)

#
# Ready to do a write operation
#
reg_cast = build_reg_cast(mv, region['offset'], reg)

for reg_index in range(*slice(slice_min, slice_max, slice_inc).indices(len(reg_cast))):
if fld is not None:
# Need to do a read + modify before writing the register back
v = update_field(reg_cast[reg_index], fld, rhs)
else:
# Use the rhs value directly since we're doing an entire register write
v = rhs

# perform the write to the register
reg_cast[reg_index] = v

sys.exit(0)

#
# We're just printing out blocks/registers/fields
#

def print_fields(reg, v, only_fld):
width_map = {
8 : "02x",
16 : "04x",
32 : "08x",
64 : "016x",
}

width_map_no_zeros = {
8 : " 2x",
16 : " 4x",
32 : " 8x",
64 : " 16x",
}

offset = reg['computed_width']
for fld in reversed(reg['fields']):
if only_fld is not None and fld['name'] != only_fld['name']:
continue
start = offset - 1
end = offset - fld['width']
reg_fmt = width_map[reg['width']]
fld_fmt = width_map_no_zeros[reg['width']]
shift = fld['offset']
mask = (1 << fld['width']) - 1
v_masked = v & (mask << shift)
v_f = (v >> shift) & mask
fld_name = fld['name']
print(f' {v_masked:{reg_fmt}} [{start:-2d}:{end:-2d}] {v_f:{fld_fmt}} {fld_name}')
offset -= fld['width']

def print_reg(parent_base_addr, reg, reg_index, v, only_fld):
reg_base_addr = parent_base_addr + reg['offset']

if reg['count'] == 1:
# not an array, use format without an index notation
print(" {: 8X}: {:08x} {}".format(reg_base_addr,
v,
reg['name']))
else:
# is an array, include the index notation
print(" {: 8X}: {:08x} {}[{:d}]".format(reg_base_addr+(reg_index*(reg['width']>>3)),
v,
reg['name'],
reg_index))
if 'fields' in reg:
print_fields(reg, v, only_fld)

if reg_name is not None:
reg_cast = build_reg_cast(mv, region['offset'], reg)

for reg_index in range(*slice(slice_min, slice_max, slice_inc).indices(len(reg_cast))):
print_reg(region['offset'], reg, reg_index, reg_cast[reg_index], fld)
else:
# Print out all of the defined registers in the block
print("[{}]".format(blk_name))
for reg in blk['regs']:
offset = region['offset'] + reg['offset']
access = reg['access']

if reg['access'] == "none":
continue
elif reg['access'] == "wo":
print(" {: 8x}: -------- {}".format(offset, reg['name']))
continue
else:
reg_cast = build_reg_cast(mv, region['offset'], reg)

for reg_index in range(*slice(slice_min, slice_max, slice_inc).indices(len(reg_cast))):
print_reg(region['offset'], reg, reg_index, reg_cast[reg_index], fld)

if __name__ == "__main__":
main(auto_envvar_prefix='REGIO')

# Force the search path to find the local regio library.
sys.path.insert(1, str(pathlib.Path(__file__).parent / 'src'))
from regio.tools.io import main

if __name__ == '__main__':
sys.exit(main())
Loading

0 comments on commit 703c9aa

Please sign in to comment.