Introduction

Documentation Status Discord Build Status

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.

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:
        obj.i2c_device.write(self.buffer, end=1, stop=False)
        obj.i2c_device.readinto(self.buffer, start=1)
    return bool(self.buffer[1] & self.bit_mask)

def __set__(self, obj, value):
    with obj.i2c_device:
        obj.i2c_device.write(self.buffer, end=1, stop=False)
        obj.i2c_device.readinto(self.buffer, 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.

Contributing

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

Building locally

To build this library locally you’ll need to install the circuitpython-build-tools package.

python3 -m venv .env
source .env/bin/activate
pip install circuitpython-build-tools

Once installed, make sure you are in the virtual environment:

source .env/bin/activate

Then run the build:

circuitpython-build-bundles --filename_prefix adafruit-circuitpython-register --library_location .

Sphinx documentation

Sphinx is used to build the documentation based on rST files and comments in the code. First, install dependencies (feel free to reuse the virtual environment from above):

python3 -m venv .env
source .env/bin/activate
pip install Sphinx sphinx-rtd-theme

Now, once you have the virtual environment activated:

cd docs
sphinx-build -E -W -b html . _build/html

This will output the documentation to docs/_build/html. Open the index.html in your browser to view them. It will also (due to -W) error out on any warning like Travis will. This is a good way to locally verify it will pass.

Table of Contents

Simple tests

Ensure your device works with this simple test.

examples/rwbit.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from board import SCL, SDA
from busio import I2C
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_bit import RWBit

DEVICE_ADDRESS = 0x68 # device address of DS3231 board
A_DEVICE_REGISTER = 0x0E # control register on the DS3231 board

class DeviceControl: #pylint: disable-msg=too-few-public-methods
    def __init__(self, i2c):
        self.i2c_device = i2c # self.i2c_device required by RWBit class

    flag1 = RWBit(A_DEVICE_REGISTER, 0) # bit 0 of the control register
    flag2 = RWBit(A_DEVICE_REGISTER, 1) # bit 1
    flag3 = RWBit(A_DEVICE_REGISTER, 7) # bit 7

# The follow is for I2C communications
comm_port = I2C(SCL, SDA)
device = I2CDevice(comm_port, DEVICE_ADDRESS)
flags = DeviceControl(device)

# set the bits in the device
flags.flag1 = False
flags.flag2 = True
flags.flag3 = False
# display the device values for the bits
print("flag1: {}; flag2: {}; flag3: {}".format(flags.flag1, flags.flag2, flags.flag3))

# toggle the bits
flags.flag1 = not flags.flag1
flags.flag2 = not flags.flag2
flags.flag3 = not flags.flag3
# display the device values for the bits
print("flag1: {}; flag2: {}; flag3: {}".format(flags.flag1, flags.flag2, flags.flag3))
examples/rwbits.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from board import SCL, SDA
from busio import I2C
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_bits import RWBits

DEVICE_ADDRESS = 0x39 # device address of APDS9960 board
A_DEVICE_REGISTER_1 = 0xA2 # a control register on the APDS9960 board
A_DEVICE_REGISTER_2 = 0xA3 # another control register on the APDS9960 board

class DeviceControl: #pylint: disable-msg=too-few-public-methods
    def __init__(self, i2c):
        self.i2c_device = i2c # self.i2c_device required by RWBit class

    setting1 = RWBits(2, A_DEVICE_REGISTER_1, 6) # 2 bits: bits 6 & 7
    setting2 = RWBits(2, A_DEVICE_REGISTER_2, 5) # 2 bits: bits 5 & 6

# The follow is for I2C communications
comm_port = I2C(SCL, SDA)
device = I2CDevice(comm_port, DEVICE_ADDRESS)
settings = DeviceControl(device)

# set the bits in the device
settings.setting1 = 0
settings.setting2 = 3
# display the device values for the bits
print("setting1: {}; setting2: {}".format(settings.setting1, settings.setting2))

# toggle the bits
settings.setting1 = 3
settings.setting2 = 0
# display the device values for the bits
print("setting1: {}; setting2: {}".format(settings.setting1, settings.setting2))
examples/struct.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from board import SCL, SDA
from busio import I2C
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_struct import Struct

DEVICE_ADDRESS = 0x40  # device address of PCA9685 board
A_DEVICE_REGISTER = 0x06 # PWM 0 control register on the PCA9685 board

class DeviceControl: #pylint: disable-msg=too-few-public-methods
    def __init__(self, i2c):
        self.i2c_device = i2c # self.i2c_device required by Struct class

    tuple_of_numbers = Struct(A_DEVICE_REGISTER, "<HH") # 2 16-bit numbers

# The follow is for I2C communications
comm_port = I2C(SCL, SDA)
device = I2CDevice(comm_port, DEVICE_ADDRESS)
registers = DeviceControl(device)

# set the bits in the device
registers.tuple_of_numbers = (0, 0x00FF)
# display the device values for the bits
print("register 1: {}; register 2: {}".format(*registers.tuple_of_numbers))

# toggle the bits
registers.tuple_of_numbers = (0x1000, 0)
# display the device values for the bits
print("register 1: {}; register 2: {}".format(*registers.tuple_of_numbers))
examples/unarystruct.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from board import SCL, SDA
from busio import I2C
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_struct import UnaryStruct

DEVICE_ADDRESS = 0x74  # device address of PCA9685 board
A_DEVICE_REGISTER_1 = 0x00 # Configuration register on the is31fl3731 board
A_DEVICE_REGISTER_2 = 0x03 # Auto Play Control Register 2 on the is31fl3731 board

class DeviceControl: #pylint: disable-msg=too-few-public-methods
    def __init__(self, i2c):
        self.i2c_device = i2c # self.i2c_device required by UnaryStruct class

    register1 = UnaryStruct(A_DEVICE_REGISTER_1, "<B") # 8-bit number
    register2 = UnaryStruct(A_DEVICE_REGISTER_2, "<B") # 8-bit number

# The follow is for I2C communications
comm_port = I2C(SCL, SDA)
device = I2CDevice(comm_port, DEVICE_ADDRESS)
registers = DeviceControl(device)

# set the bits in the device
registers.register1 = 1 << 3 | 2
registers.register2 = 32
# display the device values for the bits
print("register 1: {}; register 2: {}".format(registers.register1, registers.register2))

# toggle the bits
registers.register1 = 2 << 3 | 5
registers.register2 = 60
# display the device values for the bits
print("register 1: {}; register 2: {}".format(registers.register1, registers.register2))

Module Reference

I2C

i2c_bit - Single bit registers

adafruit_register.i2c_bit

Single bit registers

  • Author(s): Scott Shawcroft
class adafruit_register.i2c_bit.ROBit(register_address, bit, register_width=1)[source]

Single bit register that is read only. Subclass of RWBit.

Values are bool

Parameters:
  • register_address (int) – The register address to read the bit from
  • bit (type) – The bit index within the byte at register_address
  • register_width (int) – The number of bytes in the register. Defaults to 1.
class adafruit_register.i2c_bit.RWBit(register_address, bit, register_width=1)[source]

Single bit register that is readable and writeable.

Values are bool

Parameters:
  • register_address (int) – The register address to read the bit from
  • bit (type) – The bit index within the byte at register_address
  • register_width (int) – The number of bytes in the register. Defaults to 1.

i2c_bits - Multi bit registers

adafruit_register.i2c_bits

Multi bit registers

  • Author(s): Scott Shawcroft
class adafruit_register.i2c_bits.ROBits(num_bits, register_address, lowest_bit, register_width=1)[source]

Multibit register (less than a full byte) that is read-only. This must be within a byte register.

Values are int between 0 and 2 ** num_bits - 1.

Parameters:
  • num_bits (int) – The number of bits in the field.
  • register_address (int) – The register address to read the bit from
  • lowest_bit (type) – The lowest bits index within the byte at register_address
  • register_width (int) – The number of bytes in the register. Defaults to 1.
class adafruit_register.i2c_bits.RWBits(num_bits, register_address, lowest_bit, register_width=1)[source]

Multibit register (less than a full byte) that is readable and writeable. This must be within a byte register.

Values are int between 0 and 2 ** num_bits - 1.

Parameters:
  • num_bits (int) – The number of bits in the field.
  • register_address (int) – The register address to read the bit from
  • lowest_bit (type) – The lowest bits index within the byte at register_address
  • register_width (int) – The number of bytes in the register. Defaults to 1.

i2c_struct - Generic structured registers based on struct

adafruit_register.i2c_struct

Generic structured registers based on struct

  • Author(s): Scott Shawcroft
class adafruit_register.i2c_struct.ROUnaryStruct(register_address, struct_format)[source]

Arbitrary single value structure register that is read-only.

Values map to the first value in the defined struct. See struct module documentation for struct format string and its possible value types.

Parameters:
  • register_address (int) – The register address to read the bit from
  • struct_format (type) – The struct format string for this register.
class adafruit_register.i2c_struct.Struct(register_address, struct_format)[source]

Arbitrary structure register that is readable and writeable.

Values are tuples that map to the values in the defined struct. See struct module documentation for struct format string and its possible value types.

Parameters:
  • register_address (int) – The register address to read the bit from
  • struct_format (type) – The struct format string for this register.
class adafruit_register.i2c_struct.UnaryStruct(register_address, struct_format)[source]

Arbitrary single value structure register that is readable and writeable.

Values map to the first value in the defined struct. See struct module documentation for struct format string and its possible value types.

Parameters:
  • register_address (int) – The register address to read the bit from
  • struct_format (type) – The struct format string for this register.

i2c_bcd_datetime - Binary Coded Decimal date and time register

adafruit_register.i2c_bcd_datetime

Binary Coded Decimal date and time register

  • Author(s): Scott Shawcroft
class adafruit_register.i2c_bcd_datetime.BCDDateTimeRegister(register_address, weekday_first=True, weekday_start=1)[source]

Date and time register using binary coded decimal structure.

The byte order of the register must* be: second, minute, hour, weekday, day (1-31), month, year (in years after 2000).

  • Setting weekday_first=False will flip the weekday/day order so that day comes first.

Values are time.struct_time

Parameters:
  • register_address (int) – The register address to start the read
  • weekday_first (bool) – True if weekday is in a lower register than the day of the month (1-31)
  • weekday_start (int) – 0 or 1 depending on the RTC’s representation of the first day of the week

i2c_bcd_alarm - Binary Coded Decimal alarm register

adafruit_register.i2c_bcd_alarm

Binary Coded Decimal alarm register

  • Author(s): Scott Shawcroft
class adafruit_register.i2c_bcd_alarm.BCDAlarmTimeRegister(register_address, has_seconds=True, weekday_shared=True, weekday_start=1)[source]

Alarm date and time register using binary coded decimal structure.

The byte order of the registers must* be: [second], minute, hour, day, weekday. Each byte must also have a high enable bit where 1 is disabled and 0 is enabled.

  • If weekday_shared is True, then weekday and day share a register.
  • If has_seconds is True, then there is a seconds register.

Values are a tuple of (time.struct_time, str) where the struct represents a date and time that would alarm. The string is the frequency:

  • “secondly”, once a second (only if alarm has_seconds)
  • “minutely”, once a minute when seconds match (if alarm doesn’t seconds then when seconds = 0)
  • “hourly”, once an hour when tm_min and tm_sec match
  • “daily”, once a day when tm_hour, tm_min and tm_sec match
  • “weekly”, once a week when tm_wday, tm_hour, tm_min, tm_sec match
  • “monthly”, once a month when tm_mday, tm_hour, tm_min, tm_sec match
Parameters:
  • register_address (int) – The register address to start the read
  • has_seconds (bool) – True if the alarm can happen minutely.
  • weekday_shared (bool) – True if weekday and day share the same register
  • weekday_start (int) – 0 or 1 depending on the RTC’s representation of the first day of the week (Monday)

SPI

Coming soon!

Indices and tables