diff --git a/README.rst b/README.rst index 6f46fad..a6a35f6 100644 --- a/README.rst +++ b/README.rst @@ -94,8 +94,19 @@ Usage Example .. code-block:: python + import time + import board import adafruit_as5600 + i2c = board.I2C() + sensor = adafruit_as5600.AS5600(i2c) + + while True: + print(f"Raw angle: {sensor.raw_angle}") + print(f"Scaled angle: {sensor.angle}") + print(f"Magnitude: {sensor.magnitude}") + time.sleep(2) + Documentation ============= API documentation for this library can be found on `Read the Docs `_. diff --git a/adafruit_as5600.py b/adafruit_as5600.py index 9e0334b..2e621e2 100644 --- a/adafruit_as5600.py +++ b/adafruit_as5600.py @@ -1,4 +1,3 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries # # SPDX-License-Identifier: MIT @@ -16,22 +15,275 @@ Implementation Notes **Hardware:** -.. todo:: Add links to any specific hardware product page(s), or category page(s). - Use unordered list & hyperlink rST inline format: "* `Link Text `_" +* `Adafruit AS5600 Magnetic Angle Sensor - STEMMA QT `_ **Software and Dependencies:** * Adafruit CircuitPython firmware for the supported boards: https://circuitpython.org/downloads -.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies - based on the library's use of either. - -# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +* Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register """ -# imports +from adafruit_bus_device.i2c_device import I2CDevice +from adafruit_register.i2c_bit import ROBit, RWBit +from adafruit_register.i2c_bits import RWBits +from adafruit_register.i2c_struct import ROUnaryStruct, UnaryStruct +from micropython import const + +try: + from typing import Optional + + from busio import I2C +except ImportError: + pass __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_AS5600.git" + +# I2C Address +_ADDR = const(0x36) + +# Register addresses +_ZMCO = const(0x00) # ZMCO register (burn count) +_ZPOS_H = const(0x01) # Zero position high byte +_MPOS_H = const(0x03) # Maximum position high byte +_MANG_H = const(0x05) # Maximum angle high byte +_CONF_L = const(0x08) # Configuration register low byte +_CONF_H = const(0x07) # Configuration register high byte +_STATUS = const(0x0B) # Status register +_RAWANGLE_H = const(0x0C) # Raw angle high byte +_ANGLE_H = const(0x0E) # Scaled angle high byte +_AGC = const(0x1A) # Automatic Gain Control register +_MAGNITUDE_H = const(0x1B) # Magnitude high byte +_BURN = const(0xFF) # Burn command register + +# Power mode constants +POWER_MODE_NOM = const(0x00) # Normal mode (default) +POWER_MODE_LPM1 = const(0x01) # Low power mode 1 +POWER_MODE_LPM2 = const(0x02) # Low power mode 2 +POWER_MODE_LPM3 = const(0x03) # Low power mode 3 + +# Hysteresis constants +HYSTERESIS_OFF = const(0x00) # Hysteresis off (default) +HYSTERESIS_1LSB = const(0x01) # 1 LSB hysteresis +HYSTERESIS_2LSB = const(0x02) # 2 LSB hysteresis +HYSTERESIS_3LSB = const(0x03) # 3 LSB hysteresis + +# Output stage constants +OUTPUT_STAGE_ANALOG_FULL = const(0x00) # Analog (0% to 100%) +OUTPUT_STAGE_ANALOG_REDUCED = const(0x01) # Analog (10% to 90%) +OUTPUT_STAGE_DIGITAL_PWM = const(0x02) # Digital PWM +OUTPUT_STAGE_RESERVED = const(0x03) # Reserved + +# PWM frequency constants +PWM_FREQ_115HZ = const(0x00) # 115 Hz (default) +PWM_FREQ_230HZ = const(0x01) # 230 Hz +PWM_FREQ_460HZ = const(0x02) # 460 Hz +PWM_FREQ_920HZ = const(0x03) # 920 Hz + +# Slow filter constants +SLOW_FILTER_16X = const(0x00) # 16x (default) +SLOW_FILTER_8X = const(0x01) # 8x +SLOW_FILTER_4X = const(0x02) # 4x +SLOW_FILTER_2X = const(0x03) # 2x + +# Fast filter threshold constants +FAST_FILTER_SLOW_ONLY = const(0x00) # Slow filter only (default) +FAST_FILTER_6LSB = const(0x01) # 6 LSB +FAST_FILTER_7LSB = const(0x02) # 7 LSB +FAST_FILTER_9LSB = const(0x03) # 9 LSB +FAST_FILTER_18LSB = const(0x04) # 18 LSB +FAST_FILTER_21LSB = const(0x05) # 21 LSB +FAST_FILTER_24LSB = const(0x06) # 24 LSB +FAST_FILTER_10LSB = const(0x07) # 10 LSB + + +class AS5600: + """Driver for the AS5600 12-bit contactless position sensor. + + :param ~busio.I2C i2c_bus: The I2C bus the AS5600 is connected to. + :param int address: The I2C device address. Defaults to :const:`_ADDR` + """ + + _zmco = ROUnaryStruct(_ZMCO, "B") # Read-only burn count + + # 12-bit position registers (stored as 16-bit big-endian) + _zpos = UnaryStruct(_ZPOS_H, ">H") + _mpos = UnaryStruct(_MPOS_H, ">H") + _mang = UnaryStruct(_MANG_H, ">H") + _rawangle = ROUnaryStruct(_RAWANGLE_H, ">H") + _angle = ROUnaryStruct(_ANGLE_H, ">H") + + # 8-bit registers + agc: int = ROUnaryStruct(_AGC, "B") + """The current AGC (Automatic Gain Control) value. + Range is 0-255 in 5V mode, 0-128 in 3.3V mode.""" + _magnitude = ROUnaryStruct(_MAGNITUDE_H, ">H") + + # Status register bits + min_gain_overflow: bool = ROBit(_STATUS, 3) # MH (magnet too strong) + """True if AGC minimum gain overflow occurred (magnet too strong).""" + max_gain_overflow: bool = ROBit(_STATUS, 4) # ML (magnet too weak) + """True if AGC maximum gain overflow occurred (magnet too weak).""" + magnet_detected: bool = ROBit(_STATUS, 5) # MD (magnet detected) + """True if a magnet is detected, otherwise False""" + + # Configuration bits + _power_mode = RWBits(2, _CONF_L, 0) + _hysteresis = RWBits(2, _CONF_L, 2) + _output_stage = RWBits(2, _CONF_L, 4) + _pwm_freq = RWBits(2, _CONF_L, 6) + _slow_filter = RWBits(2, _CONF_H, 0) + _fast_filter = RWBits(3, _CONF_H, 2) + watchdog: bool = RWBit(_CONF_H, 5) # Bit 13 of the 16-bit config register + """Enable or disable the watchdog timer.""" + + def __init__(self, i2c: I2C, address: int = _ADDR) -> None: + try: + self.i2c_device = I2CDevice(i2c, address) + # Check if we can communicate with the device + self.watchdog = False + self.power_mode = POWER_MODE_NOM + self.hysteresis = HYSTERESIS_OFF + self.slow_filter = SLOW_FILTER_16X + self.fast_filter_threshold = FAST_FILTER_SLOW_ONLY + self.z_position = 0 + self.m_position = 4095 + self.max_angle = 4095 + except ValueError: + raise ValueError(f"No I2C device found at address 0x{address:02X}") + + @property + def zm_count(self) -> int: + """The number of times ZPOS and MPOS have been permanently burned (0-3). + This is read-only.""" + return self._zmco & 0x03 + + @property + def z_position(self) -> int: + """The zero position (start position) as a 12-bit value (0-4095).""" + return self._zpos & 0x0FFF + + @z_position.setter + def z_position(self, value: int) -> None: + """Set the zero position (start position) as a 12-bit value (0-4095).""" + if not 0 <= value <= 4095: + raise ValueError("z_position must be between 0 and 4095") + self._zpos = value & 0x0FFF + + @property + def m_position(self) -> int: + """The maximum position (stop position) as a 12-bit value (0-4095).""" + return self._mpos & 0x0FFF + + @m_position.setter + def m_position(self, value: int) -> None: + """Set the maximum position (stop position) as a 12-bit value (0-4095).""" + if not 0 <= value <= 4095: + raise ValueError("m_position must be between 0 and 4095") + self._mpos = value & 0x0FFF + + @property + def max_angle(self) -> int: + """The maximum angle range as a 12-bit value (0-4095). + This represents 0-360 degrees.""" + return self._mang & 0x0FFF + + @max_angle.setter + def max_angle(self, value: int) -> None: + """Set the maximum angle range as a 12-bit value (0-4095). + This represents 0-360 degrees.""" + if not 0 <= value <= 4095: + raise ValueError("max_angle must be between 0 and 4095") + self._mang = value & 0x0FFF + + @property + def raw_angle(self) -> int: + """The raw angle reading as a 12-bit value (0-4095). + This is unscaled and unmodified by ZPOS/MPOS/MANG settings.""" + return self._rawangle & 0x0FFF + + @property + def angle(self) -> int: + """The scaled angle reading as a 12-bit value (0-4095). + This is scaled according to ZPOS/MPOS/MANG settings.""" + return self._angle & 0x0FFF + + @property + def magnitude(self) -> int: + """The magnitude value from the CORDIC processor (0-4095).""" + return self._magnitude & 0x0FFF + + @property + def power_mode(self) -> int: + """The power mode setting. Use POWER_MODE_* constants.""" + return self._power_mode + + @power_mode.setter + def power_mode(self, value: int) -> None: + """Set the power mode. Use POWER_MODE_* constants.""" + if not 0 <= value <= 3: + raise ValueError("Invalid power mode") + self._power_mode = value + + @property + def hysteresis(self) -> int: + """The hysteresis setting. Use HYSTERESIS_* constants.""" + return self._hysteresis + + @hysteresis.setter + def hysteresis(self, value: int) -> None: + """Set the hysteresis. Use HYSTERESIS_* constants.""" + if not 0 <= value <= 3: + raise ValueError("Invalid hysteresis setting") + self._hysteresis = value + + @property + def output_stage(self) -> int: + """The output stage configuration. Use OUTPUT_STAGE_* constants.""" + return self._output_stage + + @output_stage.setter + def output_stage(self, value: int) -> None: + """Set the output stage configuration. Use OUTPUT_STAGE_* constants.""" + if not 0 <= value <= 3: + raise ValueError("Invalid output stage setting") + self._output_stage = value + + @property + def pwm_frequency(self) -> int: + """The PWM frequency setting. Use PWM_FREQ_* constants.""" + return self._pwm_freq + + @pwm_frequency.setter + def pwm_frequency(self, value: int) -> None: + """Set the PWM frequency. Use PWM_FREQ_* constants.""" + if not 0 <= value <= 3: + raise ValueError("Invalid PWM frequency setting") + self._pwm_freq = value + + @property + def slow_filter(self) -> int: + """The slow filter setting. Use SLOW_FILTER_* constants.""" + return self._slow_filter + + @slow_filter.setter + def slow_filter(self, value: int) -> None: + """Set the slow filter. Use SLOW_FILTER_* constants.""" + if not 0 <= value <= 3: + raise ValueError("Invalid slow filter setting") + self._slow_filter = value + + @property + def fast_filter_threshold(self) -> int: + """The fast filter threshold setting. Use FAST_FILTER_* constants.""" + return self._fast_filter + + @fast_filter_threshold.setter + def fast_filter_threshold(self, value: int) -> None: + """Set the fast filter threshold. Use FAST_FILTER_* constants.""" + if not 0 <= value <= 7: + raise ValueError("Invalid fast filter threshold setting") + self._fast_filter = value diff --git a/docs/conf.py b/docs/conf.py index 14d8dd9..abdeaa6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ extensions = [ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -# autodoc_mock_imports = ["digitalio", "busio"] +autodoc_mock_imports = ["digitalio", "busio", "adafruit_register"] autodoc_preserve_defaults = True diff --git a/docs/examples.rst b/docs/examples.rst index 786f936..7102d13 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -6,3 +6,12 @@ Ensure your device works with this simple test. .. literalinclude:: ../examples/as5600_simpletest.py :caption: examples/as5600_simpletest.py :linenos: + +Full test +---------- + +Full test of the library + +.. literalinclude:: ../examples/as5600_fulltest.py + :caption: examples/as5600_fulltest.py + :linenos: diff --git a/docs/index.rst b/docs/index.rst index f167d01..68057b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,14 +24,12 @@ Table of Contents .. toctree:: :caption: Tutorials -.. todo:: Add any Learn guide links here. If there are none, then simply delete this todo and leave - the toctree above for use later. + Learn Guide: .. toctree:: :caption: Related Products -.. todo:: Add any product links here. If there are none, then simply delete this todo and leave - the toctree above for use later. + Adafruit AS5600 Magnetic Angle Sensor .. toctree:: :caption: Other Links diff --git a/examples/as5600_fulltest.py b/examples/as5600_fulltest.py new file mode 100644 index 0000000..eb66f2d --- /dev/null +++ b/examples/as5600_fulltest.py @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +Full library testing example for the Adafruit AS5600 CircuitPython library + +This example tests all functionality of the AS5600 magnetic angle sensor +""" + +import time + +import board + +import adafruit_as5600 + +# Initialize I2C and AS5600 +i2c = board.I2C() # uses board.SCL and board.SDA +as5600 = adafruit_as5600.AS5600(i2c) + +print("Adafruit AS5600 Full Test") +print("AS5600 found!") +print() + +# Test zm_count property +zm_count = as5600.zm_count +print(f"ZM Count (burn count): {zm_count}") + +# Test z_position property +z_pos = as5600.z_position +print(f"Z Position: {z_pos}") + +# Test setting z_position (XOR current value with 0xADA to change it) +test_pos = (z_pos ^ 0xADA) & 0x0FFF # XOR with 0xADA and keep within 12-bit range +print(f"Setting Z Position to {test_pos} (0x{test_pos:03X})... ") +try: + as5600.z_position = test_pos + print("Success") + new_z_pos = as5600.z_position + print(f"New Z Position: {new_z_pos} (0x{new_z_pos:03X})") +except Exception as e: + print(f"Failed: {e}") + +# Test m_position property +m_pos = as5600.m_position +print(f"M Position: {m_pos}") + +# Test setting m_position (XOR current value with 0xBEE) +test_m_pos = (m_pos ^ 0xBEE) & 0x0FFF +print(f"Setting M Position to {test_m_pos} (0x{test_m_pos:03X})... ") +try: + as5600.m_position = test_m_pos + print("Success") + new_m_pos = as5600.m_position + print(f"New M Position: {new_m_pos} (0x{new_m_pos:03X})") +except Exception as e: + print(f"Failed: {e}") + +# Test max_angle property +max_angle = as5600.max_angle +print(f"Max Angle: {max_angle}") + +# Test setting max_angle (XOR current value with 0xCAB) +test_max_angle = (max_angle ^ 0xCAB) & 0x0FFF +print(f"Setting Max Angle to {test_max_angle} (0x{test_max_angle:03X})... ") +try: + as5600.max_angle = test_max_angle + print("Success") + new_max_angle = as5600.max_angle + print(f"New Max Angle: {new_max_angle} (0x{new_max_angle:03X})") +except Exception as e: + print(f"Failed: {e}") + +# Test watchdog property +print("Turning on watchdog... ") +try: + as5600.watchdog = True + print("Success") + print(f"Watchdog status: {'ENABLED' if as5600.watchdog else 'DISABLED'}") +except Exception as e: + print(f"Failed: {e}") + +print("Turning off watchdog...") +try: + as5600.watchdog = False + print("Success") + print(f"Watchdog status: {'ENABLED' if as5600.watchdog else 'DISABLED'}") +except Exception as e: + print(f"Failed: {e}") + +# Test power_mode property +print("Setting power mode...") +try: + as5600.power_mode = adafruit_as5600.POWER_MODE_NOM + print("Success") + mode = as5600.power_mode + print("Power mode: ") + if mode == adafruit_as5600.POWER_MODE_NOM: + print("Normal") + elif mode == adafruit_as5600.POWER_MODE_LPM1: + print("Low Power Mode 1") + elif mode == adafruit_as5600.POWER_MODE_LPM2: + print("Low Power Mode 2") + elif mode == adafruit_as5600.POWER_MODE_LPM3: + print("Low Power Mode 3") +except Exception as e: + print(f"Failed: {e}") + +# Test hysteresis property +print("Setting hysteresis...") +try: + as5600.hysteresis = adafruit_as5600.HYSTERESIS_OFF + print("Success") + hysteresis = as5600.hysteresis + print("Hysteresis: ") + if hysteresis == adafruit_as5600.HYSTERESIS_OFF: + print("OFF") + elif hysteresis == adafruit_as5600.HYSTERESIS_1LSB: + print("1 LSB") + elif hysteresis == adafruit_as5600.HYSTERESIS_2LSB: + print("2 LSB") + elif hysteresis == adafruit_as5600.HYSTERESIS_3LSB: + print("3 LSB") +except Exception as e: + print(f"Failed: {e}") + +# Test output_stage property +print("Setting output stage...") +try: + as5600.output_stage = adafruit_as5600.OUTPUT_STAGE_ANALOG_FULL + print("Success") + output_stage = as5600.output_stage + print("Output stage: ") + if output_stage == adafruit_as5600.OUTPUT_STAGE_ANALOG_FULL: + print("Analog Full (0% to 100%)") + elif output_stage == adafruit_as5600.OUTPUT_STAGE_ANALOG_REDUCED: + print("Analog Reduced (10% to 90%)") + elif output_stage == adafruit_as5600.OUTPUT_STAGE_DIGITAL_PWM: + print("Digital PWM") + elif output_stage == adafruit_as5600.OUTPUT_STAGE_RESERVED: + print("Reserved") +except Exception as e: + print(f"Failed: {e}") + +# Test pwm_frequency property +print("Setting PWM frequency...") +try: + as5600.pwm_frequency = adafruit_as5600.PWM_FREQ_115HZ + print("Success") + pwm_freq = as5600.pwm_frequency + print("PWM frequency: ") + if pwm_freq == adafruit_as5600.PWM_FREQ_115HZ: + print("115 Hz") + elif pwm_freq == adafruit_as5600.PWM_FREQ_230HZ: + print("230 Hz") + elif pwm_freq == adafruit_as5600.PWM_FREQ_460HZ: + print("460 Hz") + elif pwm_freq == adafruit_as5600.PWM_FREQ_920HZ: + print("920 Hz") +except Exception as e: + print(f"Failed: {e}") + +# Test slow_filter property +print("Setting slow filter to 16x (options: 16X=0, 8X=1, 4X=2, 2X=3)... ") +try: + as5600.slow_filter = adafruit_as5600.SLOW_FILTER_16X + print("Success") + slow_filter = as5600.slow_filter + print("Slow filter: ") + if slow_filter == adafruit_as5600.SLOW_FILTER_16X: + print("16x") + elif slow_filter == adafruit_as5600.SLOW_FILTER_8X: + print("8x") + elif slow_filter == adafruit_as5600.SLOW_FILTER_4X: + print("4x") + elif slow_filter == adafruit_as5600.SLOW_FILTER_2X: + print("2x") +except Exception as e: + print(f"Failed: {e}") + +# Test fast_filter_threshold property +print("Setting fast filter threshold... ") +try: + as5600.fast_filter_threshold = adafruit_as5600.FAST_FILTER_SLOW_ONLY + print("Success") + fast_thresh = as5600.fast_filter_threshold + print("Fast filter threshold: ", end="") + if fast_thresh == adafruit_as5600.FAST_FILTER_SLOW_ONLY: + print("Slow filter only") + elif fast_thresh == adafruit_as5600.FAST_FILTER_6LSB: + print("6 LSB") + elif fast_thresh == adafruit_as5600.FAST_FILTER_7LSB: + print("7 LSB") + elif fast_thresh == adafruit_as5600.FAST_FILTER_9LSB: + print("9 LSB") + elif fast_thresh == adafruit_as5600.FAST_FILTER_18LSB: + print("18 LSB") + elif fast_thresh == adafruit_as5600.FAST_FILTER_21LSB: + print("21 LSB") + elif fast_thresh == adafruit_as5600.FAST_FILTER_24LSB: + print("24 LSB") + elif fast_thresh == adafruit_as5600.FAST_FILTER_10LSB: + print("10 LSB") +except Exception as e: + print(f"Failed: {e}") + +# Reset position settings to defaults +print("\nResetting position settings to defaults...") +as5600.z_position = 0 +as5600.m_position = 4095 +as5600.max_angle = 4095 + +print("\nStarting continuous angle reading...") +print("=" * 80) + +# Continuously read and display angle values +while True: + # Get angle readings + raw_angle = as5600.raw_angle + angle = as5600.angle + + # Build output string + output = f"Raw: {raw_angle:4d} (0x{raw_angle:03X}) | Scaled: {angle:4d} (0x{angle:03X})" + + # Check status conditions + if as5600.magnet_detected: + output += " | Magnet: YES" + else: + output += " | Magnet: NO " + + if as5600.min_gain_overflow: + output += " | MH: magnet too strong" + + if as5600.max_gain_overflow: + output += " | ML: magnet too weak" + + # Get AGC and Magnitude values + agc = as5600.agc + magnitude = as5600.magnitude + output += f" | AGC: {agc:3d} | Mag: {magnitude:4d}" + + print(output) + time.sleep(2) diff --git a/examples/as5600_simpletest.py b/examples/as5600_simpletest.py index 42772ff..3b15de7 100644 --- a/examples/as5600_simpletest.py +++ b/examples/as5600_simpletest.py @@ -1,4 +1,28 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries # -# SPDX-License-Identifier: Unlicense +# SPDX-License-Identifier: MIT +"""AS5600 Simple Test""" + +import time + +import board + +import adafruit_as5600 + +i2c = board.I2C() +sensor = adafruit_as5600.AS5600(i2c) + +while True: + # Read angle values + if sensor.magnet_detected: + if sensor.max_gain_overflow is True: + print("Magnet is too weak") + if sensor.min_gain_overflow is True: + print("Magnet is too strong") + print(f"Raw angle: {sensor.raw_angle}") + print(f"Scaled angle: {sensor.angle}") + print(f"Magnitude: {sensor.magnitude}") + else: + print("Waiting for magnet..") + print() + time.sleep(2)