/ adafruit_icm20x.py
adafruit_icm20x.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2020 Bryan Siepert for Adafruit Industries
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_icm20x`
 24  ================================================================================
 25  
 26  Library for the ST ICM20X Motion Sensor Family
 27  
 28  * Author(s): Bryan Siepert
 29  
 30  Implementation Notes
 31  --------------------
 32  
 33  **Hardware:**
 34  
 35  * Adafruit's ICM20649 Breakout: https://adafruit.com/product/4464
 36  * Adafruit's ICM20948 Breakout: https://adafruit.com/product/4554
 37  
 38  **Software and Dependencies:**
 39  
 40  * Adafruit CircuitPython firmware for the supported boards:
 41    https://circuitpython.org/downloads
 42  
 43  
 44  * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
 45  * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
 46  """
 47  
 48  __version__ = "0.0.0-auto.0"
 49  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ICM20X.git"
 50  # Common imports; remove if unused or pylint will complain
 51  from time import sleep
 52  import adafruit_bus_device.i2c_device as i2c_device
 53  
 54  from adafruit_register.i2c_struct import UnaryStruct, ROUnaryStruct, Struct
 55  from adafruit_register.i2c_bit import RWBit, ROBit
 56  from adafruit_register.i2c_bits import RWBits
 57  
 58  # pylint: disable=bad-whitespace
 59  _ICM20649_DEFAULT_ADDRESS = 0x68  # icm20649 default i2c address
 60  _ICM20948_DEFAULT_ADDRESS = 0x69  # icm20649 default i2c address
 61  _ICM20649_DEVICE_ID = 0xE1  # Correct content of WHO_AM_I register
 62  _ICM20948_DEVICE_ID = 0xEA  # Correct content of WHO_AM_I register
 63  
 64  # Functions using these bank-specific registers are responsible for ensuring
 65  # that the correct bank is set
 66  # Bank 0
 67  _ICM20X_WHO_AM_I = 0x00  # device_id register
 68  _ICM20X_REG_BANK_SEL = 0x7F  # register bank selection register
 69  _ICM20X_PWR_MGMT_1 = 0x06  # primary power management register
 70  _ICM20X_ACCEL_XOUT_H = 0x2D  # first byte of accel data
 71  _ICM20X_GYRO_XOUT_H = 0x33  # first byte of accel data
 72  _ICM20X_I2C_MST_STATUS = 0x17  # I2C Master Status bits
 73  _ICM20948_EXT_SLV_SENS_DATA_00 = 0x3B
 74  
 75  _ICM20X_USER_CTRL = 0x03  # User Control Reg. Includes I2C Master
 76  _ICM20X_LP_CONFIG = 0x05  # Low Power config
 77  _ICM20X_REG_INT_PIN_CFG = 0xF  # Interrupt config register
 78  _ICM20X_REG_INT_ENABLE_0 = 0x10  # Interrupt enable register 0
 79  _ICM20X_REG_INT_ENABLE_1 = 0x11  # Interrupt enable register 1
 80  
 81  # Bank 2
 82  _ICM20X_GYRO_SMPLRT_DIV = 0x00
 83  _ICM20X_GYRO_CONFIG_1 = 0x01
 84  _ICM20X_ACCEL_SMPLRT_DIV_1 = 0x10
 85  _ICM20X_ACCEL_SMPLRT_DIV_2 = 0x11
 86  _ICM20X_ACCEL_CONFIG_1 = 0x14
 87  
 88  
 89  # Bank 3
 90  
 91  _ICM20X_I2C_MST_ODR_CONFIG = 0x0  # Sets ODR for I2C master bus
 92  _ICM20X_I2C_MST_CTRL = 0x1  # I2C master bus config
 93  _ICM20X_I2C_MST_DELAY_CTRL = 0x2  # I2C master bus config
 94  _ICM20X_I2C_SLV0_ADDR = 0x3  # Sets I2C address for I2C master bus slave 0
 95  _ICM20X_I2C_SLV0_REG = 0x4  # Sets register address for I2C master bus slave 0
 96  _ICM20X_I2C_SLV0_CTRL = 0x5  # Controls for I2C master bus slave 0
 97  _ICM20X_I2C_SLV0_DO = 0x6  # Sets I2C master bus slave 0 data out
 98  
 99  _ICM20X_I2C_SLV4_ADDR = 0x13  # Sets I2C address for I2C master bus slave 4
100  _ICM20X_I2C_SLV4_REG = 0x14  # Sets register address for I2C master bus slave 4
101  _ICM20X_I2C_SLV4_CTRL = 0x15  # Controls for I2C master bus slave 4
102  _ICM20X_I2C_SLV4_DO = 0x16  # Sets I2C master bus slave 4 data out
103  _ICM20X_I2C_SLV4_DI = 0x17  # Sets I2C master bus slave 4 data in
104  
105  _ICM20X_UT_PER_LSB = 0.15  # mag data LSB value (fixed)
106  _ICM20X_RAD_PER_DEG = 0.017453293  # Degrees/s to rad/s multiplier
107  
108  G_TO_ACCEL = 9.80665
109  # pylint: enable=bad-whitespace
110  class CV:
111      """struct helper"""
112  
113      @classmethod
114      def add_values(cls, value_tuples):
115          """Add CV values to the class"""
116          cls.string = {}
117          cls.lsb = {}
118  
119          for value_tuple in value_tuples:
120              name, value, string, lsb = value_tuple
121              setattr(cls, name, value)
122              cls.string[value] = string
123              cls.lsb[value] = lsb
124  
125      @classmethod
126      def is_valid(cls, value):
127          """Validate that a given value is a member"""
128          return value in cls.string
129  
130  
131  class AccelRange(CV):
132      """Options for ``accelerometer_range``"""
133  
134      pass  # pylint: disable=unnecessary-pass
135  
136  
137  class GyroRange(CV):
138      """Options for ``gyro_data_range``"""
139  
140      pass  # pylint: disable=unnecessary-pass
141  
142  
143  class GyroDLPFFreq(CV):
144      """Options for ``gyro_dlpf_cutoff``"""
145  
146      pass  # pylint: disable=unnecessary-pass
147  
148  
149  class AccelDLPFFreq(CV):
150      """Options for ``accel_dlpf_cutoff``"""
151  
152      pass  # pylint: disable=unnecessary-pass
153  
154  
155  class ICM20X:  # pylint:disable=too-many-instance-attributes
156      """Library for the ST ICM-20X Wide-Range 6-DoF Accelerometer and Gyro Family
157  
158  
159          :param ~busio.I2C i2c_bus: The I2C bus the ICM20X is connected to.
160          :param address: The I2C slave address of the sensor
161  
162      """
163  
164      # Bank 0
165      _device_id = ROUnaryStruct(_ICM20X_WHO_AM_I, ">B")
166      _bank_reg = UnaryStruct(_ICM20X_REG_BANK_SEL, ">B")
167      _reset = RWBit(_ICM20X_PWR_MGMT_1, 7)
168      _sleep_reg = RWBit(_ICM20X_PWR_MGMT_1, 6)
169      _low_power_en = RWBit(_ICM20X_PWR_MGMT_1, 5)
170      _clock_source = RWBits(3, _ICM20X_PWR_MGMT_1, 0)
171  
172      _raw_accel_data = Struct(_ICM20X_ACCEL_XOUT_H, ">hhh")  # ds says LE :|
173      _raw_gyro_data = Struct(_ICM20X_GYRO_XOUT_H, ">hhh")
174  
175      _lp_config_reg = UnaryStruct(_ICM20X_LP_CONFIG, ">B")
176  
177      _i2c_master_cycle_en = RWBit(_ICM20X_LP_CONFIG, 6)
178      _accel_cycle_en = RWBit(_ICM20X_LP_CONFIG, 5)
179      _gyro_cycle_en = RWBit(_ICM20X_LP_CONFIG, 4)
180  
181      # Bank 2
182      _gyro_dlpf_enable = RWBits(1, _ICM20X_GYRO_CONFIG_1, 0)
183      _gyro_range = RWBits(2, _ICM20X_GYRO_CONFIG_1, 1)
184      _gyro_dlpf_config = RWBits(3, _ICM20X_GYRO_CONFIG_1, 3)
185  
186      _accel_dlpf_enable = RWBits(1, _ICM20X_ACCEL_CONFIG_1, 0)
187      _accel_range = RWBits(2, _ICM20X_ACCEL_CONFIG_1, 1)
188      _accel_dlpf_config = RWBits(3, _ICM20X_ACCEL_CONFIG_1, 3)
189  
190      # this value is a 12-bit register spread across two bytes, big-endian first
191      _accel_rate_divisor = UnaryStruct(_ICM20X_ACCEL_SMPLRT_DIV_1, ">H")
192      _gyro_rate_divisor = UnaryStruct(_ICM20X_GYRO_SMPLRT_DIV, ">B")
193      AccelDLPFFreq.add_values(
194          (
195              (
196                  "DISABLED",
197                  -1,
198                  "Disabled",
199                  None,
200              ),  # magical value that we will use do disable
201              ("FREQ_246_0HZ_3DB", 1, 246.0, None),
202              ("FREQ_111_4HZ_3DB", 2, 111.4, None),
203              ("FREQ_50_4HZ_3DB", 3, 50.4, None),
204              ("FREQ_23_9HZ_3DB", 4, 23.9, None),
205              ("FREQ_11_5HZ_3DB", 5, 11.5, None),
206              ("FREQ_5_7HZ_3DB", 6, 5.7, None),
207              ("FREQ_473HZ_3DB", 7, 473, None),
208          )
209      )
210      GyroDLPFFreq.add_values(
211          (
212              (
213                  "DISABLED",
214                  -1,
215                  "Disabled",
216                  None,
217              ),  # magical value that we will use do disable
218              ("FREQ_196_6HZ_3DB", 0, 196.6, None),
219              ("FREQ_151_8HZ_3DB", 1, 151.8, None),
220              ("FREQ_119_5HZ_3DB", 2, 119.5, None),
221              ("FREQ_51_2HZ_3DB", 3, 51.2, None),
222              ("FREQ_23_9HZ_3DB", 4, 23.9, None),
223              ("FREQ_11_6HZ_3DB", 5, 11.6, None),
224              ("FREQ_5_7HZ_3DB", 6, 5.7, None),
225              ("FREQ_361_4HZ_3DB", 7, 361.4, None),
226          )
227      )
228  
229      @property
230      def _bank(self):
231          return self._bank_reg >> 4
232  
233      @_bank.setter
234      def _bank(self, value):
235          self._bank_reg = value << 4
236  
237      def __init__(self, i2c_bus, address):
238  
239          self.i2c_device = i2c_device.I2CDevice(i2c_bus, address)
240          self._bank = 0
241          if not self._device_id in [_ICM20649_DEVICE_ID, _ICM20948_DEVICE_ID]:
242              raise RuntimeError("Failed to find an ICM20X sensor - check your wiring!")
243          self.reset()
244          self.initialize()
245  
246      def initialize(self):
247          """Configure the sensors with the default settings. For use after calling `reset()`"""
248  
249          self._sleep = False
250          self.accelerometer_range = AccelRange.RANGE_8G  # pylint: disable=no-member
251          self.gyro_range = GyroRange.RANGE_500_DPS  # pylint: disable=no-member
252  
253          self.accelerometer_data_rate_divisor = 20  # ~53.57Hz
254          self.gyro_data_rate_divisor = 10  # ~100Hz
255  
256      def reset(self):
257          """Resets the internal registers and restores the default settings"""
258          self._bank = 0
259  
260          sleep(0.005)
261          self._reset = True
262          sleep(0.005)
263          while self._reset:
264              sleep(0.005)
265  
266      @property
267      def _sleep(self):
268          self._bank = 0
269          sleep(0.005)
270          self._sleep_reg = False
271          sleep(0.005)
272  
273      @_sleep.setter
274      def _sleep(self, sleep_enabled):
275          self._bank = 0
276          sleep(0.005)
277          self._sleep_reg = sleep_enabled
278          sleep(0.005)
279  
280      @property
281      def acceleration(self):
282          """The x, y, z acceleration values returned in a 3-tuple and are in m / s ^ 2."""
283          self._bank = 0
284          raw_accel_data = self._raw_accel_data
285          sleep(0.005)
286  
287          x = self._scale_xl_data(raw_accel_data[0])
288          y = self._scale_xl_data(raw_accel_data[1])
289          z = self._scale_xl_data(raw_accel_data[2])
290  
291          return (x, y, z)
292  
293      @property
294      def gyro(self):
295          """The x, y, z angular velocity values returned in a 3-tuple and are in degrees / second"""
296          self._bank = 0
297          raw_gyro_data = self._raw_gyro_data
298          x = self._scale_gyro_data(raw_gyro_data[0])
299          y = self._scale_gyro_data(raw_gyro_data[1])
300          z = self._scale_gyro_data(raw_gyro_data[2])
301  
302          return (x, y, z)
303  
304      def _scale_xl_data(self, raw_measurement):
305          sleep(0.005)
306          return raw_measurement / AccelRange.lsb[self._cached_accel_range] * G_TO_ACCEL
307  
308      def _scale_gyro_data(self, raw_measurement):
309          return (
310              raw_measurement / GyroRange.lsb[self._cached_gyro_range]
311          ) * _ICM20X_RAD_PER_DEG
312  
313      @property
314      def accelerometer_range(self):
315          """Adjusts the range of values that the sensor can measure, from +/- 4G to +/-30G
316          Note that larger ranges will be less accurate. Must be an `AccelRange`"""
317          return self._cached_accel_range
318  
319      @accelerometer_range.setter
320      def accelerometer_range(self, value):  # pylint: disable=no-member
321          if not AccelRange.is_valid(value):
322              raise AttributeError("range must be an `AccelRange`")
323          self._bank = 2
324          sleep(0.005)
325          self._accel_range = value
326          sleep(0.005)
327          self._cached_accel_range = value
328          self._bank = 0
329  
330      @property
331      def gyro_range(self):
332          """Adjusts the range of values that the sensor can measure, from 500 Degrees/second to 4000
333          degrees/s. Note that larger ranges will be less accurate. Must be a `GyroRange`"""
334          return self._cached_gyro_range
335  
336      @gyro_range.setter
337      def gyro_range(self, value):
338          if not GyroRange.is_valid(value):
339              raise AttributeError("range must be a `GyroRange`")
340  
341          self._bank = 2
342          sleep(0.005)
343          self._gyro_range = value
344          sleep(0.005)
345          self._cached_gyro_range = value
346          self._bank = 0
347          sleep(0.100)  # needed to let new range settle
348  
349      @property
350      def accelerometer_data_rate_divisor(self):
351          """The divisor for the rate at which accelerometer measurements are taken in Hz
352  
353          Note: The data rates are set indirectly by setting a rate divisor according to the
354          following formula: ``accelerometer_data_rate = 1125/(1+divisor)``
355  
356          This function sets the raw rate divisor.
357          """
358          self._bank = 2
359          raw_rate_divisor = self._accel_rate_divisor
360          sleep(0.005)
361          self._bank = 0
362          # rate_hz = 1125/(1+raw_rate_divisor)
363          return raw_rate_divisor
364  
365      @accelerometer_data_rate_divisor.setter
366      def accelerometer_data_rate_divisor(self, value):
367          # check that value <= 4095
368          self._bank = 2
369          sleep(0.005)
370          self._accel_rate_divisor = value
371          sleep(0.005)
372  
373      @property
374      def gyro_data_rate_divisor(self):
375          """The divisor for the rate at which gyro measurements are taken in Hz
376  
377          Note: The data rates are set indirectly by setting a rate divisor according to the
378          following formula: ``gyro_data_rate = 1100/(1+divisor)``
379  
380          This function sets the raw rate divisor.
381          """
382  
383          self._bank = 2
384          raw_rate_divisor = self._gyro_rate_divisor
385          sleep(0.005)
386          self._bank = 0
387          # rate_hz = 1100/(1+raw_rate_divisor)
388          return raw_rate_divisor
389  
390      @gyro_data_rate_divisor.setter
391      def gyro_data_rate_divisor(self, value):
392          # check that value <= 255
393          self._bank = 2
394          sleep(0.005)
395          self._gyro_rate_divisor = value
396          sleep(0.005)
397  
398      def _accel_rate_calc(self, divisor):  # pylint:disable=no-self-use
399          return 1125 / (1 + divisor)
400  
401      def _gyro_rate_calc(self, divisor):  # pylint:disable=no-self-use
402          return 1100 / (1 + divisor)
403  
404      @property
405      def accelerometer_data_rate(self):
406          """The rate at which accelerometer measurements are taken in Hz
407  
408          Note: The data rates are set indirectly by setting a rate divisor according to the
409          following formula: ``accelerometer_data_rate = 1125/(1+divisor)``
410  
411          This function does the math to find the divisor from a given rate but it will not be
412          exactly as specified.
413          """
414          return self._accel_rate_calc(self.accelerometer_data_rate_divisor)
415  
416      @accelerometer_data_rate.setter
417      def accelerometer_data_rate(self, value):
418          if value < self._accel_rate_calc(4095) or value > self._accel_rate_calc(0):
419              raise AttributeError(
420                  "Accelerometer data rate must be between 0.27 and 1125.0"
421              )
422          self.accelerometer_data_rate_divisor = value
423  
424      @property
425      def gyro_data_rate(self):
426          """The rate at which gyro measurements are taken in Hz
427  
428          Note: The data rates are set indirectly by setting a rate divisor according to the
429          following formula: ``gyro_data_rate = 1100/(1+divisor)``
430          This function does the math to find the divisor from a given rate but it will not
431          be exactly as specified.
432          """
433          return self._gyro_rate_calc(self.gyro_data_rate_divisor)
434  
435      @gyro_data_rate.setter
436      def gyro_data_rate(self, value):
437          if value < self._gyro_rate_calc(4095) or value > self._gyro_rate_calc(0):
438              raise AttributeError("Gyro data rate must be between 4.30 and 1100.0")
439  
440          divisor = round(((1125.0 - value) / value))
441          self.gyro_data_rate_divisor = divisor
442  
443      @property
444      def accel_dlpf_cutoff(self):
445          """The cutoff frequency for the accelerometer's digital low pass filter. Signals
446          above the given frequency will be filtered out. Must be an ``AccelDLPFCutoff``.
447          Use AccelDLPFCutoff.DISABLED to disable the filter
448  
449          **Note** readings immediately following setting a cutoff frequency will be
450          inaccurate due to the filter "warming up" """
451          self._bank = 2
452          return self._accel_dlpf_config
453  
454      @accel_dlpf_cutoff.setter
455      def accel_dlpf_cutoff(self, cutoff_frequency):
456          if not AccelDLPFFreq.is_valid(cutoff_frequency):
457              raise AttributeError("accel_dlpf_cutoff must be an `AccelDLPFFreq`")
458          self._bank = 2
459          # check for shutdown
460          if cutoff_frequency is AccelDLPFFreq.DISABLED:  # pylint: disable=no-member
461              self._accel_dlpf_enable = False
462              return
463          self._accel_dlpf_enable = True
464          self._accel_dlpf_config = cutoff_frequency
465  
466      @property
467      def gyro_dlpf_cutoff(self):
468          """The cutoff frequency for the gyro's digital low pass filter. Signals above the
469          given frequency will be filtered out. Must be a ``GyroDLPFFreq``. Use
470          GyroDLPFCutoff.DISABLED to disable the filter
471  
472          **Note** readings immediately following setting a cutoff frequency will be
473          inaccurate due to the filter "warming up" """
474          self._bank = 2
475          return self._gyro_dlpf_config
476  
477      @gyro_dlpf_cutoff.setter
478      def gyro_dlpf_cutoff(self, cutoff_frequency):
479          if not GyroDLPFFreq.is_valid(cutoff_frequency):
480              raise AttributeError("gyro_dlpf_cutoff must be a `GyroDLPFFreq`")
481          self._bank = 2
482          # check for shutdown
483          if cutoff_frequency is GyroDLPFFreq.DISABLED:  # pylint: disable=no-member
484              self._gyro_dlpf_enable = False
485              return
486          self._gyro_dlpf_enable = True
487          self._gyro_dlpf_config = cutoff_frequency
488  
489      @property
490      def _low_power(self):
491          self._bank = 0
492          return self._low_power_en
493  
494      @_low_power.setter
495      def _low_power(self, enabled):
496          self._bank = 0
497          self._low_power_en = enabled
498  
499  
500  class ICM20649(ICM20X):
501      """Library for the ST ICM-20649 Wide-Range 6-DoF Accelerometer and Gyro.
502  
503          :param ~busio.I2C i2c_bus: The I2C bus the ICM20649 is connected to.
504          :param address: The I2C slave address of the sensor
505  
506      """
507  
508      def __init__(self, i2c_bus, address=_ICM20649_DEFAULT_ADDRESS):
509  
510          AccelRange.add_values(
511              (
512                  ("RANGE_4G", 0, 4, 8192),
513                  ("RANGE_8G", 1, 8, 4096.0),
514                  ("RANGE_16G", 2, 16, 2048),
515                  ("RANGE_30G", 3, 30, 1024),
516              )
517          )
518  
519          GyroRange.add_values(
520              (
521                  ("RANGE_500_DPS", 0, 500, 65.5),
522                  ("RANGE_1000_DPS", 1, 1000, 32.8),
523                  ("RANGE_2000_DPS", 2, 2000, 16.4),
524                  ("RANGE_4000_DPS", 3, 4000, 8.2),
525              )
526          )
527          super().__init__(i2c_bus, address)
528  
529  
530  # https://www.y-ic.es/datasheet/78/SMDSW.020-2OZ.pdf page 19
531  _AK09916_WIA1 = 0x00
532  _AK09916_WIA2 = 0x01
533  _AK09916_ST1 = 0x10
534  _AK09916_HXL = 0x11
535  _AK09916_HXH = 0x12
536  _AK09916_HYL = 0x13
537  _AK09916_HYH = 0x14
538  _AK09916_HZL = 0x15
539  _AK09916_HZH = 0x16
540  _AK09916_ST2 = 0x18
541  _AK09916_CNTL2 = 0x31
542  _AK09916_CNTL3 = 0x32
543  
544  
545  class MagDataRate(CV):
546      """Options for ``magnetometer_data_rate``"""
547  
548      pass  # pylint: disable=unnecessary-pass
549  
550  
551  class ICM20948(ICM20X):  # pylint:disable=too-many-instance-attributes
552      """Library for the ST ICM-20948 Wide-Range 6-DoF Accelerometer and Gyro.
553  
554          :param ~busio.I2C i2c_bus: The I2C bus the ICM20948 is connected to.
555          :param address: The I2C slave address of the sensor
556      """
557  
558      _slave_finished = ROBit(_ICM20X_I2C_MST_STATUS, 6)
559  
560      # mag data is LE
561      _raw_mag_data = Struct(_ICM20948_EXT_SLV_SENS_DATA_00, "<hhhh")
562  
563      _bypass_i2c_master = RWBit(_ICM20X_REG_INT_PIN_CFG, 1)
564      _i2c_master_control = UnaryStruct(_ICM20X_I2C_MST_CTRL, ">B")
565      _i2c_master_enable = RWBit(_ICM20X_USER_CTRL, 5)  # TODO: use this in sw reset
566      _i2c_master_reset = RWBit(_ICM20X_USER_CTRL, 1)
567  
568      _slave0_addr = UnaryStruct(_ICM20X_I2C_SLV0_ADDR, ">B")
569      _slave0_reg = UnaryStruct(_ICM20X_I2C_SLV0_REG, ">B")
570      _slave0_ctrl = UnaryStruct(_ICM20X_I2C_SLV0_CTRL, ">B")
571      _slave0_do = UnaryStruct(_ICM20X_I2C_SLV0_DO, ">B")
572  
573      _slave4_addr = UnaryStruct(_ICM20X_I2C_SLV4_ADDR, ">B")
574      _slave4_reg = UnaryStruct(_ICM20X_I2C_SLV4_REG, ">B")
575      _slave4_ctrl = UnaryStruct(_ICM20X_I2C_SLV4_CTRL, ">B")
576      _slave4_do = UnaryStruct(_ICM20X_I2C_SLV4_DO, ">B")
577      _slave4_di = UnaryStruct(_ICM20X_I2C_SLV4_DI, ">B")
578  
579      def __init__(self, i2c_bus, address=_ICM20948_DEFAULT_ADDRESS):
580          AccelRange.add_values(
581              (
582                  ("RANGE_2G", 0, 2, 16384),
583                  ("RANGE_4G", 1, 4, 8192),
584                  ("RANGE_8G", 2, 8, 4096.0),
585                  ("RANGE_16G", 3, 16, 2048),
586              )
587          )
588          GyroRange.add_values(
589              (
590                  ("RANGE_250_DPS", 0, 250, 131.0),
591                  ("RANGE_500_DPS", 1, 500, 65.5),
592                  ("RANGE_1000_DPS", 2, 1000, 32.8),
593                  ("RANGE_2000_DPS", 3, 2000, 16.4),
594              )
595          )
596  
597          # https://www.y-ic.es/datasheet/78/SMDSW.020-2OZ.pdf page 9
598          MagDataRate.add_values(
599              (
600                  ("SHUTDOWN", 0x0, "Shutdown", None),
601                  ("SINGLE", 0x1, "Single", None),
602                  ("RATE_10HZ", 0x2, 10, None),
603                  ("RATE_20HZ", 0x4, 20, None),
604                  ("RATE_50HZ", 0x6, 50, None),
605                  ("RATE_100HZ", 0x8, 100, None),
606              )
607          )
608          super().__init__(i2c_bus, address)
609          self._magnetometer_init()
610  
611      # A million thanks to the SparkFun folks for their library that I pillaged to write this method!
612      # See their Python library here:
613      # https://github.com/sparkfun/Qwiic_9DoF_IMU_ICM20948_Py
614      @property
615      def _mag_configured(self):
616          success = False
617          for _i in range(5):
618              success = self._mag_id() is not None
619  
620              if success:
621                  return True
622              self._reset_i2c_master()
623              # i2c master stuck, try resetting
624          return False
625  
626      def _reset_i2c_master(self):
627          self._bank = 0
628          self._i2c_master_reset = True
629  
630      def _magnetometer_enable(self):
631  
632          self._bank = 0
633          sleep(0.100)
634          self._bypass_i2c_master = False
635          sleep(0.005)
636  
637          # no repeated start, i2c master clock = 345.60kHz
638          self._bank = 3
639          sleep(0.100)
640          self._i2c_master_control = 0x17
641          sleep(0.100)
642  
643          self._bank = 0
644          sleep(0.100)
645          self._i2c_master_enable = True
646          sleep(0.020)
647  
648      def _magnetometer_init(self):
649          self._magnetometer_enable()
650          self.magnetometer_data_rate = (
651              MagDataRate.RATE_100HZ  # pylint: disable=no-member
652          )
653  
654          if not self._mag_configured:
655              return False
656  
657          self._setup_mag_readout()
658  
659          return True
660  
661      # set up slave0 for reading into the bank 0 data registers
662      def _setup_mag_readout(self):
663          self._bank = 3
664          self._slave0_addr = 0x8C
665          sleep(0.005)
666          self._slave0_reg = 0x11
667          sleep(0.005)
668          self._slave0_ctrl = 0x89  # enable
669          sleep(0.005)
670  
671      def _mag_id(self):
672          return self._read_mag_register(0x01)
673  
674      @property
675      def magnetic(self):
676          """The current magnetic field strengths onthe X, Y, and Z axes in uT (micro-teslas)"""
677  
678          self._bank = 0
679          full_data = self._raw_mag_data
680          sleep(0.005)
681  
682          x = full_data[0] * _ICM20X_UT_PER_LSB
683          y = full_data[1] * _ICM20X_UT_PER_LSB
684          z = full_data[2] * _ICM20X_UT_PER_LSB
685  
686          return (x, y, z)
687  
688      @property
689      def magnetometer_data_rate(self):
690          """The rate at which the magenetometer takes measurements to update its output registers"""
691          # read mag DR register
692          self._read_mag_register(_AK09916_CNTL2)
693  
694      @magnetometer_data_rate.setter
695      def magnetometer_data_rate(self, mag_rate):
696          # From https://www.y-ic.es/datasheet/78/SMDSW.020-2OZ.pdf page 9
697  
698          # "When user wants to change operation mode, transit to Power-down mode first and then
699          # transit to other modes. After Power-down mode is set, at least 100 microsectons (Twait)
700          # is needed before setting another mode"
701          if not MagDataRate.is_valid(mag_rate):
702              raise AttributeError("range must be an `MagDataRate`")
703          self._write_mag_register(
704              _AK09916_CNTL2, MagDataRate.SHUTDOWN  # pylint: disable=no-member
705          )
706          sleep(0.001)
707          self._write_mag_register(_AK09916_CNTL2, mag_rate)
708  
709      def _read_mag_register(self, register_addr, slave_addr=0x0C):
710          self._bank = 3
711  
712          slave_addr |= 0x80  # set top bit for read
713  
714          self._slave4_addr = slave_addr
715          sleep(0.005)
716          self._slave4_reg = register_addr
717          sleep(0.005)
718          self._slave4_ctrl = (
719              0x80  # enable, don't raise interrupt, write register value, no delay
720          )
721          sleep(0.005)
722          self._bank = 0
723  
724          finished = False
725          for _i in range(100):
726              finished = self._slave_finished
727              if finished:  # bueno!
728                  break
729              sleep(0.010)
730  
731          if not finished:
732              return None
733  
734          self._bank = 3
735          mag_register_data = self._slave4_di
736          sleep(0.005)
737          return mag_register_data
738  
739      def _write_mag_register(self, register_addr, value, slave_addr=0x0C):
740          self._bank = 3
741  
742          self._slave4_addr = slave_addr
743          sleep(0.005)
744          self._slave4_reg = register_addr
745          sleep(0.005)
746          self._slave4_do = value
747          sleep(0.005)
748          self._slave4_ctrl = (
749              0x80  # enable, don't raise interrupt, write register value, no delay
750          )
751          sleep(0.005)
752          self._bank = 0
753  
754          finished = False
755          for _i in range(100):
756              finished = self._slave_finished
757              if finished:  # bueno!
758                  break
759              sleep(0.010)
760  
761          return finished