diff --git a/adafruit_register/register_accessor.py b/adafruit_register/register_accessor.py new file mode 100644 index 0000000..31ba309 --- /dev/null +++ b/adafruit_register/register_accessor.py @@ -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. + + :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)) diff --git a/adafruit_register/register_bit.py b/adafruit_register/register_bit.py new file mode 100644 index 0000000..04604e5 --- /dev/null +++ b/adafruit_register/register_bit.py @@ -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() diff --git a/adafruit_register/register_bits.py b/adafruit_register/register_bits.py new file mode 100644 index 0000000..37522b8 --- /dev/null +++ b/adafruit_register/register_bits.py @@ -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() diff --git a/examples/register_accessor_multibyte_address_test.py b/examples/register_accessor_multibyte_address_test.py new file mode 100644 index 0000000..2bbcc8c --- /dev/null +++ b/examples/register_accessor_multibyte_address_test.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT +""" +Verify functionality of multibyte address registers. + +Rrequires OV5640 camera connected. +Default pins are for RPi Pico + Adafruit PiCowbell Camera Breakout +""" + +import board +import busio +import digitalio +from adafruit_bus_device.i2c_device import I2CDevice + +from adafruit_register.register_accessor import I2CRegisterAccessor +from adafruit_register.register_bits import ROBits + +I2C_ADDRESS = 0x3C +REG_CHIP_ID_HIGH = 0x300A + + +class OV5640Tester: + chip_id = ROBits(16, REG_CHIP_ID_HIGH, 0, register_width=2, lsb_first=False) + + def __init__(self, i2c): + try: + i2c_device = I2CDevice(i2c, I2C_ADDRESS) + self.register_accessor = I2CRegisterAccessor( + i2c_device, address_width=2, lsb_first=False + ) + except ValueError: + raise ValueError(f"No I2C device found.") + + +if __name__ == "__main__": + print("construct bus") + i2c = busio.I2C(board.GP5, board.GP4) + print("construct camera") + reset = digitalio.DigitalInOut(board.GP14) + + ov5640 = OV5640Tester(i2c) + print(hex(ov5640.chip_id))