Skip to main content

CircuitPython data descriptor classes to represent hardware registers on I2C and SPI devices.

Project description

Introduction

Documentation Status Discord Build Status Code Style: Ruff

This library provides a variety of data descriptor class for Adafruit CircuitPython that makes it really simple to write a device drivers for a I2C and SPI register based devices. Data descriptors act like basic attributes from the outside which makes using them really easy to use.

Dependencies

This driver depends on:

Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading the Adafruit library and driver bundle.

Installing from PyPI

On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally from PyPI. To install for current user:

pip3 install adafruit-circuitpython-register

To install system-wide (this may be required in some cases):

sudo pip3 install adafruit-circuitpython-register

To install in a virtual environment in your current project:

mkdir project-name && cd project-name
python3 -m venv .venv
source .venv/bin/activate
pip3 install adafruit-circuitpython-register

Usage Example

Creating a driver

Creating a driver with the register library is really easy. First, import the register modules you need from the available modules:

from adafruit_register import i2c_bit
from adafruit_bus_device import i2c_device

Next, define where the bit is located in the device’s memory map:

class HelloWorldDevice:
    """Device with two bits to control when the words 'hello' and 'world' are lit."""

    hello = i2c_bit.RWBit(0x0, 0x0)
    """Bit to indicate if hello is lit."""

    world = i2c_bit.RWBit(0x1, 0x0)
    """Bit to indicate if world is lit."""

Lastly, we need to add an i2c_device member of type I2CDevice that manages sharing the I2C bus for us. Make sure the name is exact, otherwise the registers will not be able to find it. Also, make sure that the i2c device implements the busio.I2C interface.

def __init__(self, i2c, device_address=0x0):
    self.i2c_device = i2c_device.I2CDevice(i2c, device_address)

Thats it! Now we have a class we can use to talk to those registers:

import busio
from board import *

with busio.I2C(SCL, SDA) as i2c:
    device = HelloWorldDevice(i2c)
    device.hello = True
    device.world = True

Adding register types

Adding a new register type is a little more complicated because you need to be careful and minimize the amount of memory the class will take. If you don’t, then a driver with five registers of your type could take up five times more extra memory.

First, determine whether the new register class should go in an existing module or not. When in doubt choose a new module. The more finer grained the modules are, the fewer extra classes a driver needs to load in.

Here is the start of the RWBit class:

class RWBit:
    """
    Single bit register that is readable and writeable.

    Values are `bool`

    :param int register_address: The register address to read the bit from
    :param type bit: The bit index within the byte at ``register_address``
    """
    def __init__(self, register_address, bit):
        self.bit_mask = 1 << bit
        self.buffer = bytearray(2)
        self.buffer[0] = register_address

The first thing done is writing an RST formatted class comment that explains the functionality of the register class and any requirements of the register layout. It also documents the parameters passed into the constructor (__init__) which configure the register location in the device map. It does not include the device address or the i2c object because its shared on the device class instance instead. That way if you have multiple of the same device on the same bus, the register classes will be shared.

In __init__ we only use two member variable because each costs 8 bytes of memory plus the memory for the value. And remember this gets multiplied by the number of registers of this type in a driver! Thats why we pack both the register address and data byte into one bytearray. We could use two byte arrays of size one but each MicroPython object is 16 bytes minimum due to the garbage collector. So, by sharing a byte array we keep it to the 16 byte minimum instead of 32 bytes. Each memoryview also costs 16 bytes minimum so we avoid them too.

Another thing we could do is allocate the bytearray only when we need it. This has the advantage of taking less memory up front but the cost of allocating it every access and risking it failing. If you want to add a version of Foo that lazily allocates the underlying buffer call it FooLazy.

Ok, onward. To make a data descriptor we must implement __get__ and __set__.

def __get__(self, obj, objtype=None):
    with obj.i2c_device as i2c:
        i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1)
    return bool(self.buffer[1] & self.bit_mask)

def __set__(self, obj, value):
    with obj.i2c_device as i2c:
        i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1)
        if value:
            self.buffer[1] |= self.bit_mask
        else:
            self.buffer[1] &= ~self.bit_mask
        obj.i2c_device.write(self.buffer)

As you can see, we have two places to get state from. First, self stores the register class members which locate the register within the device memory map. Second, obj is the driver class that uses the register class which must by definition provide a I2CDevice compatible object as i2c_device. This object does two thing for us:

  1. Waits for the bus to free, locks it as we use it and frees it after.

  2. Saves the device address and other settings so we don’t have to.

Note that we take heavy advantage of the start and end parameters to the i2c functions to slice the buffer without actually allocating anything extra. They function just like self.buffer[start:end] without the extra allocation.

Thats it! Now you can use your new register class like the example above. Just remember to keep the number of members to a minimum because the class may be used a bunch of times.

Documentation

API documentation for this library can be found on Read the Docs.

For information on building library documentation, please check out this guide.

Contributing

Contributions are welcome! Please read our Code of Conduct before contributing to help this project stay welcoming.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adafruit_circuitpython_register-1.11.1.tar.gz (31.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file adafruit_circuitpython_register-1.11.1.tar.gz.

File metadata

File hashes

Hashes for adafruit_circuitpython_register-1.11.1.tar.gz
Algorithm Hash digest
SHA256 45c6b6de5e65efe584375ea1bd6964866dab0d5e5b52ede49e906abd28876989
MD5 ff4d38c84113a053fb0968f8410ec90c
BLAKE2b-256 f214dde5accb99a72460f750037373b9df9a06d9f6915c0891433ca09b26a1a5

See more details on using hashes here.

File details

Details for the file adafruit_circuitpython_register-1.11.1-py3-none-any.whl.

File metadata

File hashes

Hashes for adafruit_circuitpython_register-1.11.1-py3-none-any.whl
Algorithm Hash digest
SHA256 40b792f149a28f047c46a7c280313ad8384d75439354737ab1e88f107a2251f6
MD5 a72391b001cca88c27d55a1a02e298df
BLAKE2b-256 96b0a847f66cbd60c9222fb2b16ebb6277d9e48e1cd5f3282835e4e84785ea28

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page