diff --git a/README.rst b/README.rst index d4c4e05..50ab249 100644 --- a/README.rst +++ b/README.rst @@ -94,9 +94,22 @@ Usage Example .. code-block:: python + import time import board import adafruit_qmc5883p + i2c = board.I2C() + + sensor = adafruit_qmc5883p.QMC5883P(i2c) + + while True: + mag_x, mag_y, mag_z = sensor.magnetic + + print(f"X:{mag_x:2.3f}, Y:{mag_y:2.3f}, Z:{mag_z:2.3f} G") + print("") + + time.sleep(1) + Documentation ============= API documentation for this library can be found on `Read the Docs `_. diff --git a/adafruit_qmc5883p.py b/adafruit_qmc5883p.py index b494e19..b898142 100644 --- a/adafruit_qmc5883p.py +++ b/adafruit_qmc5883p.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,302 @@ **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 QMC5883P - Triple Axis Magnetometer `_" **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 +import struct +import time + +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 +from micropython import const + +try: + from typing import Tuple + + import busio +except ImportError: + pass __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_QMC5883P.git" + +# I2C Address +_DEFAULT_ADDR = const(0x2C) + +# Registers +_CHIPID = const(0x00) +_XOUT_LSB = const(0x01) +_XOUT_MSB = const(0x02) +_YOUT_LSB = const(0x03) +_YOUT_MSB = const(0x04) +_ZOUT_LSB = const(0x05) +_ZOUT_MSB = const(0x06) +_STATUS = const(0x09) +_CONTROL1 = const(0x0A) +_CONTROL2 = const(0x0B) + +# Operating modes +MODE_SUSPEND = const(0x00) +MODE_NORMAL = const(0x01) +MODE_SINGLE = const(0x02) +MODE_CONTINUOUS = const(0x03) + +# Output data rates +ODR_10HZ = const(0x00) +ODR_50HZ = const(0x01) +ODR_100HZ = const(0x02) +ODR_200HZ = const(0x03) + +# Over sample ratios +OSR_8 = const(0x00) +OSR_4 = const(0x01) +OSR_2 = const(0x02) +OSR_1 = const(0x03) + +# Downsample ratios +DSR_1 = const(0x00) +DSR_2 = const(0x01) +DSR_4 = const(0x02) +DSR_8 = const(0x03) + +# Field ranges +RANGE_30G = const(0x00) +RANGE_12G = const(0x01) +RANGE_8G = const(0x02) +RANGE_2G = const(0x03) + +# Set/Reset modes +SETRESET_ON = const(0x00) +SETRESET_SETONLY = const(0x01) +SETRESET_OFF = const(0x02) + +# LSB per Gauss for each range +_LSB_PER_GAUSS = {RANGE_30G: 1000.0, RANGE_12G: 2500.0, RANGE_8G: 3750.0, RANGE_2G: 15000.0} + + +class QMC5883P: + """Driver for the QMC5883P 3-axis magnetometer. + + :param ~busio.I2C i2c_bus: The I2C bus the QMC5883P is connected to. + :param int address: The I2C address of the device. Defaults to :const:`0x3C` + """ + + # Register definitions using adafruit_register + _chip_id = ROUnaryStruct(_CHIPID, " None: + self.i2c_device = I2CDevice(i2c_bus, address) + + # Check chip ID + if self._chip_id != 0x80: + raise RuntimeError("Failed to find QMC5883P chip") + + # Initialize with default settings + self.mode = MODE_NORMAL + self.data_rate = ODR_50HZ + self.oversample_ratio = OSR_4 + self.downsample_ratio = DSR_2 + self.range = RANGE_8G + self.setreset_mode = SETRESET_ON + + @property + def magnetic(self) -> Tuple[float, float, float]: + """The magnetic field measured in microteslas (uT). + + :return: A 3-tuple of X, Y, Z axis values in microteslas + """ + # Wait for data ready + while not self.data_ready: + time.sleep(0.001) + + # Read all 6 bytes at once + buf = bytearray(6) + with self.i2c_device as i2c: + i2c.write_then_readinto(bytes([_XOUT_LSB]), buf) + + # Unpack as signed 16-bit integers + raw_x, raw_y, raw_z = struct.unpack(" Tuple[int, int, int]: + """The raw magnetic field sensor values as signed 16-bit integers. + + :return: A 3-tuple of X, Y, Z axis raw values + """ + # Wait for data ready + while not self._data_ready: + time.sleep(0.001) + + # Read all 6 bytes at once + buf = bytearray(6) + with self.i2c_device as i2c: + i2c.write_then_readinto(bytes([_XOUT_LSB]), buf) + + # Unpack as signed 16-bit integers + return struct.unpack(" int: + """The operating mode of the sensor. + + Options are: + - MODE_SUSPEND (0x00): Suspend mode + - MODE_NORMAL (0x01): Normal mode + - MODE_SINGLE (0x02): Single measurement mode + - MODE_CONTINUOUS (0x03): Continuous mode + """ + return self._mode + + @mode.setter + def mode(self, value: int) -> None: + if value not in {MODE_SUSPEND, MODE_NORMAL, MODE_SINGLE, MODE_CONTINUOUS}: + raise ValueError("Invalid mode") + self._mode = value + + @property + def data_rate(self) -> int: + """The output data rate in Hz. + + Options are: + - ODR_10HZ (0x00): 10 Hz + - ODR_50HZ (0x01): 50 Hz + - ODR_100HZ (0x02): 100 Hz + - ODR_200HZ (0x03): 200 Hz + """ + return self._odr + + @data_rate.setter + def data_rate(self, value: int) -> None: + if value not in {ODR_10HZ, ODR_50HZ, ODR_100HZ, ODR_200HZ}: + raise ValueError("Invalid output data rate") + self._odr = value + + @property + def oversample_ratio(self) -> int: + """The over sample ratio. + + Options are: + - OSR_8 (0x00): Over sample ratio = 8 + - OSR_4 (0x01): Over sample ratio = 4 + - OSR_2 (0x02): Over sample ratio = 2 + - OSR_1 (0x03): Over sample ratio = 1 + """ + return self._osr + + @oversample_ratio.setter + def oversample_ratio(self, value: int) -> None: + if value not in {OSR_8, OSR_4, OSR_2, OSR_1}: + raise ValueError("Invalid oversample ratio") + self._osr = value + + @property + def downsample_ratio(self) -> int: + """The downsample ratio. + + Options are: + - DSR_1 (0x00): Downsample ratio = 1 + - DSR_2 (0x01): Downsample ratio = 2 + - DSR_4 (0x02): Downsample ratio = 4 + - DSR_8 (0x03): Downsample ratio = 8 + """ + return self._dsr + + @downsample_ratio.setter + def downsample_ratio(self, value: int) -> None: + if value not in {DSR_1, DSR_2, DSR_4, DSR_8}: + raise ValueError("Invalid downsample ratio") + self._dsr = value + + @property + def range(self) -> int: + """The magnetic field range. + + Options are: + - RANGE_30G (0x00): ±30 Gauss range + - RANGE_12G (0x01): ±12 Gauss range + - RANGE_8G (0x02): ±8 Gauss range + - RANGE_2G (0x03): ±2 Gauss range + """ + return self._range + + @range.setter + def range(self, value: int) -> None: + if value not in {RANGE_30G, RANGE_12G, RANGE_8G, RANGE_2G}: + raise ValueError("Invalid range") + self._range = value + + @property + def setreset_mode(self) -> int: + """The set/reset mode. + + Options are: + - SETRESET_ON (0x00): Set and reset on + - SETRESET_SETONLY (0x01): Set only on + - SETRESET_OFF (0x02): Set and reset off + """ + return self._setreset + + @setreset_mode.setter + def setreset_mode(self, value: int) -> None: + if value not in {SETRESET_ON, SETRESET_SETONLY, SETRESET_OFF}: + raise ValueError("Invalid set/reset mode") + self._setreset = value + + def soft_reset(self) -> None: + """Perform a soft reset of the chip.""" + self._reset = True + time.sleep(0.05) # Wait 50ms for reset to complete + + # Verify chip ID after reset + if self._chip_id != 0x80: + raise RuntimeError("Chip ID invalid after reset") + + def self_test(self) -> bool: + """Perform self-test of the chip. + + :return: True if self-test passed, False otherwise + """ + self._selftest = True + time.sleep(0.005) # Wait 5ms for self-test to complete + + # Check if self-test bit auto-cleared (indicates completion) + return not self._selftest diff --git a/docs/conf.py b/docs/conf.py index f9f21fe..762bb47 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ # 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"] autodoc_preserve_defaults = True diff --git a/docs/index.rst b/docs/index.rst index fc7e013..b069775 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. + Adafruit QMC5883P - Triple Axis Magnetometer 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 QMC5883P - Triple Axis Magnetometer - STEMMA QT .. toctree:: :caption: Other Links diff --git a/examples/qmc5883p_simpletest.py b/examples/qmc5883p_simpletest.py index 42772ff..a1dc86d 100644 --- a/examples/qmc5883p_simpletest.py +++ b/examples/qmc5883p_simpletest.py @@ -1,4 +1,33 @@ -# 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 + +"""QMC5333P Simple Test""" + +import time + +import board + +import adafruit_qmc5883p + +i2c = board.I2C() +# i2c = board.STEMMA_I2C() + +sensor = adafruit_qmc5883p.QMC5883P(i2c) + +# configure sensor settings +# defaults to MODE_NORMAL, ODR_50HZ, RANGE_8G + +# sensor.mode = adafruit_qmc5883p.MODE_CONTINUOUS +# sensor.data_rate = adafruit_qmc5883p.ODR_10HZ +# sensor.range = adafruit_qmc5883p.RANGE_2G + +print("QMC5883P Magnetometer Test") +print("-" * 40) + +while True: + mag_x, mag_y, mag_z = sensor.magnetic + + print(f"X:{mag_x:2.3f}, Y:{mag_y:2.3f}, Z:{mag_z:2.3f} G") + + time.sleep(1) diff --git a/requirements.txt b/requirements.txt index cf750ec..7284723 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,3 @@ Adafruit-Blinka adafruit-circuitpython-busdevice adafruit-circuitpython-register -n