Skip to content
Brainless-SoftwarePublic

About

Python classes for accessing registers on embedded devices

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

from timeit import dummy_src_name

RGSCore

License - MIT PyPI Latest Release pytest Documentation

Python library that provides classes for representing, defining and accessing registers in the embedded devices.

To add this library to your project do

uv add rgscore

Library purpose

  1. To provide python API to define registers and serialize it into persistent json store/file.
  2. To provide access from Python to serialized form of register definitions (aka. addrMap json).
  3. Together with other libraries, such as i2c-api and i2c-drivers, to provide read and write access from Python to registers in live devices.
  4. Provide intuitive interactive API for interfacing with live devices.

Overall structure

To fulfill above goals this library defines following classes

  • Register - defines/holds name, address, description, in-memory bitarray storage, reset value and a model of fields (list of FieldDef objects).
  • FieldDef - holds register field definition which includes field name, bit-range in the larger register bitarray, numeric type, r/w flag, description and optional enum values that maps numeric value held in a register field to symbolic names.
  • EnumValue - symbolic name corresponding to a numeric value in the field. Also includes description.
  • RLink - abstract class that connects registers to physical hardware. Requires implementation that drives low level communication to live device.
  • RLinkI2C - implementation of RLink suitable for linking classes in this library to physical devices over I2C bus.
  • AddrMap - grouping of registers ensuring their uniqueness within addrMap. Primary class for (de)serialization to/from json file.

Defining registers programmatically

Registers are grouped into ordered collections known as addrMap's, represented by AddrMap class. When registers are added to the addrMap this ensures that they are unique (both, with regard to register names and addresses) within a given addrMap. Registers can be added to the addrMap at the time of construction or later by calling AddrMap::add(...) method.

from rgscore import Register, AddrMap, FieldDef, EnumValue

addrMap = AddrMap(registers=[
    Register(
        bit_len=8,
        name="Foo",
        address=0x01,
        desc="Register Foo",
        model=[FieldDef.value_of("a@[3:0]U4.1#ro")]
    )
])

addrMap.add(Register(
    bit_len=8,
    name="Bar",
    address=0x02,
    model=[
        FieldDef.value_of("b@[3:0]U4.0#ro").with_description("field b").with_enum(
            [EnumValue("START", 1, "Starting..."), EnumValue("STOP", 0, "Stoping...")]
        )
    ],
))

AddrMap ensures that registers do not have overlapping names or addresses.

Class Register holds

  • name - optional human-readable name.
  • address - optional integer address within larger address space.
  • desc - optional description.
  • reset - reset value (default is 0x0)
  • bit_len - bit width.
  • address_bus_width_bytes - width of the address bus (default is 1).
  • model - optional model of register fields, i.e. an array of FieldDef instances.
  • link_provider - optional prover of RLink, object that links this register to the physical hardware.

If model is None, then model with just one field spanning the whole of the register width is autogenerated with numerical type of unsigned int.

When adding it to the AddrMap it must have an address. If name is not present, then adding it to the AddrMap will autogenerate name in the format R${hex_address}.

Class FieldDef can be created using string description by calling FieldDef.value_of(...). Where description is

<field_name>@<bit_range><num_type>

or

<field_name>@<bit_range><num_type>#<rw_flag>

where <bit_range> is [<end_offset>,<start_offset>]

<num_type> is fixed point number definition in simplified Q_(number_format), i.e. U or S (for unsigned or signed) followed by bit-width, then dot, and then fraction.

Flag <rw_flag> is optional and can be "rw" (default) or "ro".

Description can be added by calling with_description(...) and enum values with_enum(...)

Loading addrMap from json file

Instance of AddrMap can writen down into json file and read back like so

from pathlib import Path
from rgscore import AddrMap

addrMap = AddrMap(...)

# write into a file
addrMapFilePath = Path.home() / "addrMap.json"
addrMap.save_to_json_file(addrMapFilePath)

# load from a file
addrMap = AddrMap.new_instance_from_file(addrMapFilePath.read_text())

Loading addrMap from URL

AddrMap class can be instantiated with the URL that points to addrMap json file.

from rgscore import AddrMap

addrMap = AddrMap.new_instance_from_url("https://.../addrMap.json")

By default, this will cache downloaded json file at $HOME/.rgscore/cache/ directory, so that subsequent calls to this function will load addrMap from cache. Cache can be refreshed if force_reload=True argument is set.

To change cache location you can set RGSCORE_CACHE_DIR environment variable to another folder.

Accessing registers on physical devices

To access physical hardware you need to create instance of RLink that links AddrMap (and therefore registers) to live chip over some physical bus. Below is an example of doing that over I2C bus. This requires you to bring in two more libraries, i2c-api and i2c-drivers.

from i2c_drivers import I2CMasterI2CDriver

from rgscore import AddrMap, RLinkI2C

addrMap = AddrMap.new_instance_from_file("addrMap-BMP581.json", link=RLinkI2C(I2CMasterI2CDriver(), 0x47))

odr_config = addrMap.get_register_by_name("ODR_CONFIG")
odr_config.read()  # read from the chip at I2C address 0x47
print("pwr_mode", odr_config.get_field_value("pwr_mode"))

# set new pwr_mode and write this register to the chip
odr_config.set_field_value("pwr_mode", 1)
odr_config.write()

Interactive API

API described above is more applicable to developers writing applications on top of this library. For electrical engineers using this library there are two other API end points, dut (aka. Device Under Test) and regList. The first one dut is for accessing live registers values, and second one regLsit holds minimal necessary subset of register and fields definitions.

Consider an example of accessing device BMP581. We assume that it is present on the I2C bus at address 0x47. For this chip we do have address map json file at https://raw.githubusercontent.com/Brainless-Software/rgscore/refs/heads/master/devices/AddrMap_BMP581.json

from i2c_drivers import I2CMasterI2CDriver

from rgscore import AddrMap, RLinkI2C, read, write

addrMap = AddrMap.new_instance_from_url(
    "https://raw.githubusercontent.com/Brainless-Software/rgscore/refs/heads/master/devices/AddrMap_BMP581.json", 
    link=RLinkI2C(I2CMasterI2CDriver(), 0x47)
)

dut = addrMap.dut
regList = addrMap.regList

For dut (instance of DeviceUnderTest class) following rules apply:

  1. dut.$register_name returns a register (instance of HardwareRegister)
  2. dut.$register_name.$field_name returns value of a field in this register.
  3. dut.$register_name.$field_name = $number | EnumValue assigns value to this field.
  4. read(dut.$register_name) reads register value from the connected hardware device.
  5. write(dut.$register_name) writes register value to the connected hardware device.

For regList (instance of RegList class) following rules apply:

  1. regList.$register_name returns FieldModelAccessor class which in turn allows one to access fields.
  2. regList.$register_name.$field_name return object to access associated enum values if any.
  3. regList.$register_name() returns list of field names.
  4. regList.$register_name.$field_name() returns list of enum values if any.
  5. regList.$register_name.$field_name($number) returns associated enum value of $number matches any enum or just that number if no enums defined otherwise None.

The json file loaded above has entries for the following registers

from rgscore import AddrMap, Register, EnumValue, FieldDef

Register(
    bit_len=8, 
    address=0x7E,
    name="CMD",
    reset=0,
    model=[
        FieldDef.value_of("cmd@[7:0]U8.0#rw").with_enum([
            EnumValue("nvm_write_en", 0xA0), EnumValue("soft_reset", 0xB6)
        ])
    ]
)

We can then issue soft reset like so

dut.CMD.cmd = regList.CMD.cmd.soft_reset  # set `cmd` field to 182 (soft_reset enum value) 
write(dut.CMD)  # write this register to the device.
time.sleep(0.002) # reset takes 2 milliseconds

Note that you can also set CMD.cmd by simply supplying a number

dut.CMD.cmd = 0xB6

Accessing regList.CMD.cmd returns object with all enum values. But calling regList.CMD.cmd() returns list of all enums for this field.

print(regList.CMD.cmd())

Prints

[nvm_write_en(160.0), soft_reset(182.0)]

And calling register returns list of field names

print(regList.CMD())

Prints

['cmd']

About

Python classes for accessing registers on embedded devices

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages