Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions adafruit_register/register_accessor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SPDX-FileCopyrightText: Copyright (c) 2022 Max Holliday
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_register.register_accessor`
====================================================

SPI and I2C Register Accessor classes.

* Author(s): Max Holliday
* Adaptation by Tim Cocks
"""

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git"

try:
from typing import Union

from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_bus_device.spi_device import SPIDevice
except ImportError:
pass


class RegisterAccessor:
"""
Subclasses of this class will be used to provide read/write interface to registers
over different bus types.

:param int address_width: The width of the register addresses in bytes. Defaults to 1.
:param bool lsb_first: Is the first byte we read from the bus the LSB? Defaults to true
"""

address_width = None

def __init__(self, address_width=1, lsb_first=True):
self.address_width = address_width
self.address_buffer = bytearray(address_width)
self.lsb_first = lsb_first

def _pack_address_into_buffer(self, address):
if self.lsb_first:
# Little-endian: least significant byte first
for address_byte_i in range(self.address_width):
self.address_buffer[address_byte_i] = (address >> (address_byte_i * 8)) & 0xFF
else:
# Big-endian: most significant byte first
big_endian_address = address.to_bytes(self.address_width, byteorder="big")
for address_byte_i in range(self.address_width):
self.address_buffer[address_byte_i] = big_endian_address[address_byte_i]


class SPIRegisterAccessor(RegisterAccessor):
"""
RegisterAccessor class for SPI bus transport. Provides interface to read/write
registers over SPI. This class automatically handles the R/W bit by setting the
highest bit of the address to 1 when reading and 0 when writing. For multi-byte
addresses the R/W bit will be set as the highest bit in the first byte of the
address.

:param SPIDevice spi_device: The SPI bus device to communicate over.
:param int address_width: The number of bytes in the address
"""

def __init__(self, spi_device: SPIDevice, address_width: int = 1, lsb_first=True):
super().__init__(address_width, lsb_first)
self.spi_device = spi_device

def _shift_rw_cmd_bit_into_first_byte(self, bit_value):
if bit_value not in {0, 1}:
raise ValueError("bit_value must be 0 or 1")

# Clear the MSB (set bit 7 to 0)
cleared_byte = self.address_buffer[0] & 0x7F
# Set the MSB to the desired bit value
self.address_buffer[0] = cleared_byte | (bit_value << 7)

def read_register(self, address: int, buffer: bytearray):
"""
Read register value over SPIDevice.

:param int address: The register address to read.
:param bytearray buffer: Buffer that will be used to read register data into.
:return: None
"""

self._pack_address_into_buffer(address)
self._shift_rw_cmd_bit_into_first_byte(1)
with self.spi_device as spi:
spi.write(self.address_buffer)
spi.readinto(buffer)

def write_register(
self,
address: int,
buffer: bytearray,
):
"""
Write register value over SPIDevice.

:param int address: The register address to read.
:param bytearray buffer: Buffer that will be written to the register.
:return: None
"""
self._pack_address_into_buffer(address)
self._shift_rw_cmd_bit_into_first_byte(0)
with self.spi_device as spi:
spi.write(self.address_buffer)
spi.write(buffer)


class I2CRegisterAccessor(RegisterAccessor):
"""
RegisterAccessor class for I2C bus transport. Provides interface to read/write
registers over I2C. This class uses `adafruit_bus_device.I2CDevice` for
communication. I2CDevice automatically handles the R/W bit by setting
the lowest bit of the device address to 1 for reading and 0 for writing.
Device address & r/w bit will be written first, followed by register address,
then the data will be written or read.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, explain the protocol over I2C that this supports.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also talk about the relationship between address and data.

:param I2CDevice i2c_device: I2C device to communicate over
:param int address_width: The number of bytes in the address
"""

def __init__(self, i2c_device: I2CDevice, address_width: int = 1, lsb_first=True):
super().__init__(address_width, lsb_first)
self.i2c_device = i2c_device

# buffer that will hold address + data for write operations, will grow as needed
self._full_buffer = bytearray(address_width + 1)

def read_register(self, address: int, buffer: bytearray):
"""
Read register value over I2CDevice.

:param int address: The register address to read.
:param bytearray buffer: Buffer that will be used to read register data into.
:return: None
"""

self._pack_address_into_buffer(address)
with self.i2c_device as i2c:
i2c.write_then_readinto(self.address_buffer, buffer)

def write_register(self, address: int, buffer: bytearray):
"""
Write register value over I2CDevice.

:param int address: The register address to read.
:param bytearray buffer: Buffer of data that will be written to the register.
:return: None
"""
# grow full buffer if needed
if self.address_width + len(buffer) > len(self._full_buffer):
self._full_buffer = bytearray(self.address_width + len(buffer))

# put address into full buffer
self._pack_address_into_buffer(address)
self._full_buffer[: self.address_width] = self.address_buffer

# put data into full buffer
self._full_buffer[self.address_width : self.address_width + len(buffer)] = buffer

with self.i2c_device as i2c:
i2c.write(self._full_buffer, end=self.address_width + len(buffer))
80 changes: 80 additions & 0 deletions adafruit_register/register_bit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_register.register_bit`
====================================================

Single bit registers that use RegisterAccessor

* Author(s): Tim Cocks

"""

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git"


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 int bit: The bit index within the byte at ``register_address``
:param int register_width: The number of bytes in the register. Defaults to 1.
:param bool lsb_first: Is the first byte we read from spi the LSB? Defaults to true

"""

def __init__(
self, register_address: int, bit: int, register_width: int = 1, lsb_first: bool = True
):
self.bit_mask = 1 << (bit % 8) # the bitmask *within* the byte!

self.address = register_address

self.buffer = bytearray(register_width)

self.lsb_first = lsb_first
self.bit_index = bit
if lsb_first:
self.byte = bit // 8 # Little-endian: bit 0 in first register byte
else:
self.byte = register_width - 1 - (bit // 8) # Big-endian: bit 0 in last register byte

def __get__(self, obj, objtype=None):
# read data from register
obj.register_accessor.read_register(self.address, self.buffer)

# check specified bit and return boolean
return bool(self.buffer[self.byte] & self.bit_mask)

def __set__(self, obj, value):
# read current data from register
obj.register_accessor.read_register(self.address, self.buffer)

# update current data with new value
if value:
self.buffer[self.byte] |= self.bit_mask
else:
self.buffer[self.byte] &= ~self.bit_mask

# write updated data to register
obj.register_accessor.write_register(self.address, self.buffer)


class ROBit(RWBit):
"""Single bit register that is read only. Subclass of `RWBit`.

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``
:param int register_width: The number of bytes in the register. Defaults to 1.

"""

def __set__(self, obj, value):
raise AttributeError()
117 changes: 117 additions & 0 deletions adafruit_register/register_bits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_register.register_bits`
====================================================

Multi bit registers

* Author(s): Tim Cocks

"""

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git"


class RWBits:
"""
Multibit register that is readable and writeable.

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

:param int num_bits: The number of bits in the field.
:param int register_address: The register address to read the bit from
:param int lowest_bit: The lowest bits index within the byte at ``register_address``
:param int register_width: The number of bytes in the register. Defaults to 1.
:param bool lsb_first: Is the first byte we read from the bus the LSB? Defaults to true
:param bool signed: If True, the value is a "two's complement" signed value.
If False, it is unsigned.

"""

# pylint: disable=too-many-arguments
def __init__(
self,
num_bits: int,
register_address: int,
lowest_bit: int,
register_width: int = 1,
lsb_first: bool = True,
signed: bool = False,
):
self.bit_mask = ((1 << num_bits) - 1) << lowest_bit

if self.bit_mask >= 1 << (register_width * 8):
raise ValueError("Cannot have more bits than register size")
self.lowest_bit = lowest_bit

self.address = register_address
self.buffer = bytearray(register_width)

self.lsb_first = lsb_first
self.sign_bit = (1 << (num_bits - 1)) if signed else 0

def __get__(self, obj, objtype=None):
# read data from register
obj.register_accessor.read_register(self.address, self.buffer)

# read the bytes into a single variable
reg = 0
order = range(len(self.buffer) - 1, -1, -1)
if not self.lsb_first:
order = reversed(order)
for i in order:
reg = (reg << 8) | self.buffer[i]

# extract integer value from specified bits
result = (reg & self.bit_mask) >> self.lowest_bit

# If the value is signed and negative, convert it
if result & self.sign_bit:
result -= 2 * self.sign_bit

return result

def __set__(self, obj, value):
# read current data from register
obj.register_accessor.read_register(self.address, self.buffer)

# shift in integer value to register data
reg = 0
order = range(len(self.buffer) - 1, -1, -1)
if not self.lsb_first:
order = reversed(order)
for i in order:
reg = (reg << 8) | self.buffer[i]
shifted_value = value << self.lowest_bit
reg &= ~self.bit_mask # mask off the bits we're about to change
reg |= shifted_value # then or in our new value

# put data from reg back into buffer
for i in reversed(order):
self.buffer[i] = reg & 0xFF
reg >>= 8

# write updated data buffer to the register
obj.register_accessor.write_register(self.address, self.buffer)


class ROBits(RWBits):
"""
Multibit register that is read-only.

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

:param int num_bits: The number of bits in the field.
:param int register_address: The register address to read the bit from
:param int lowest_bit: The lowest bits index within the byte at ``register_address``
:param int register_width: The number of bytes in the register. Defaults to 1.
:param bool lsb_first: Is the first byte we read from the bus the LSB? Defaults to true
:param bool signed: If True, the value is a "two's complement" signed value.
If False, it is unsigned.
"""

def __set__(self, obj, value):
raise AttributeError()
Loading