-
Notifications
You must be signed in to change notification settings - Fork 25
register accessor layer and SPI support #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
dc6d647
register accessor layer and SPI support
FoamyGuy 4532b05
repo and version meta vars
FoamyGuy 77983d4
I2C accessor implementation, repo and version meta vars
FoamyGuy a5e9569
Merge remote-tracking branch 'foamyguy/accessor_layer' into accessor_…
FoamyGuy 9bdc1f8
docstrings. implement bool vs int logic in i2c write
FoamyGuy 5196696
move read current value out of accessor into Bit(s). Support multibyt…
FoamyGuy c850754
pass address to read/write Accessor functions. Fix start/end indexes …
FoamyGuy 5039f29
SPI start/end fix for multibyte address. big endian multibyte address…
FoamyGuy b42f50e
split address and data buffers. Address buffer inside Accessor, data …
FoamyGuy 262e989
only use full_buffer for i2c.write(). Split SPI write into two instea…
FoamyGuy f4861a7
add protocol details to both Accessor docstrings. Remove buffer lengt…
FoamyGuy 4b553b4
add i2c comms details
FoamyGuy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, explain the protocol over I2C that this supports.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.