/ 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