/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "lis3dh.h" #include "registers.h" #include "drivers/i2c.h" #include "drivers/legacy/accel.h" #include "system/logging.h" #include "util/math.h" #include "util/size.h" //! @file accel_config.c //! procedures for dealing with I2C communication with the accel struct I2CCommand { uint8_t register_address; uint8_t value; }; // // Boiler plate functions for talking over i2c // static uint8_t prv_read_reg(uint8_t address) { uint8_t reg; lis3dh_read(address, 1, ®); return (reg); } static bool prv_write_reg(uint8_t address, uint8_t value) { return (lis3dh_write(address, 1, &value)); } static bool send_i2c_commands(struct I2CCommand* commands, int num_commands) { for (int i = 0; i < num_commands; ++i) { bool result = prv_write_reg(commands[i].register_address, commands[i].value); if (!result) { return false; } } return true; } void lis3dh_enable_fifo(void) { uint8_t ctrl_reg5 = prv_read_reg(LIS3DH_CTRL_REG5); ctrl_reg5 |= FIFO_EN; prv_write_reg(LIS3DH_CTRL_REG5, ctrl_reg5); } void lis3dh_disable_fifo(void) { uint8_t ctrl_reg5 = prv_read_reg(LIS3DH_CTRL_REG5); ctrl_reg5 &= ~FIFO_EN; prv_write_reg(LIS3DH_CTRL_REG5, ctrl_reg5); } bool lis3dh_is_fifo_enabled(void) { uint8_t ctrl_reg5 = prv_read_reg(LIS3DH_CTRL_REG5); return (ctrl_reg5 & FIFO_EN); } void lis3dh_disable_click(void) { uint8_t ctrl_reg3 = prv_read_reg(LIS3DH_CTRL_REG3); ctrl_reg3 &= ~I1_CLICK; prv_write_reg(LIS3DH_CTRL_REG3, ctrl_reg3); } void lis3dh_enable_click(void) { uint8_t ctrl_reg3 = prv_read_reg(LIS3DH_CTRL_REG3); ctrl_reg3 |= I1_CLICK; prv_write_reg(LIS3DH_CTRL_REG3, ctrl_reg3); } // // Accel config Getter/Setters // void lis3dh_set_interrupt_axis(AccelAxisType axis, bool double_click) { // get the current state of the registers uint8_t reg_1 = prv_read_reg(LIS3DH_CTRL_REG1); // clear the axis-enable bits reg_1 = reg_1 & ~(0x1 | 0x2 | 0x4); uint8_t click_cfg = 0; switch (axis) { case ACCEL_AXIS_X: reg_1 |= 0x01; click_cfg = 0x01; break; case ACCEL_AXIS_Y: reg_1 |= 0x02; click_cfg = 0x04; break; case ACCEL_AXIS_Z: reg_1 |= 0x04; click_cfg = 0x10; break; default: PBL_LOG(LOG_LEVEL_ERROR, "Unknown axis"); } if (double_click) { click_cfg <<= 1; } if ((!prv_write_reg(LIS3DH_CTRL_REG1, reg_1)) || (!prv_write_reg(LIS3DH_CLICK_CFG, click_cfg))) { PBL_LOG(LOG_LEVEL_ERROR, "Failed to write axis selection"); } } uint8_t lis3dh_get_click_window() { return prv_read_reg(LIS3DH_TIME_WINDOW); } void lis3dh_set_click_window(uint8_t window) { if (!prv_write_reg(LIS3DH_TIME_WINDOW, MIN(window, LIS3DH_MAX_CLICK_WINDOW))) { PBL_LOG(LOG_LEVEL_ERROR, "Failed to write click latency"); } } uint8_t lis3dh_get_click_latency() { return prv_read_reg(LIS3DH_TIME_LATENCY); } void lis3dh_set_click_latency(uint8_t latency) { if (!prv_write_reg(LIS3DH_TIME_LATENCY, MIN(latency, LIS3DH_MAX_CLICK_LATENCY))) { PBL_LOG(LOG_LEVEL_ERROR, "Failed to write click latency"); } } uint8_t lis3dh_get_interrupt_threshold() { return prv_read_reg(LIS3DH_CLICK_THS); } void lis3dh_set_interrupt_threshold(uint8_t threshold) { if (!prv_write_reg(LIS3DH_CLICK_THS, MIN(threshold, LIS3DH_MAX_THRESHOLD))) { PBL_LOG(LOG_LEVEL_ERROR, "Failed to set interrupt threshold"); } } uint8_t lis3dh_get_interrupt_time_limit() { return prv_read_reg(LIS3DH_TIME_LIMIT); } void lis3dh_set_interrupt_time_limit(uint8_t time_limit) { if (!prv_write_reg(LIS3DH_TIME_LIMIT, MIN(time_limit, LIS3DH_MAX_TIME_LIMIT))) { PBL_LOG(LOG_LEVEL_ERROR, "Failed to set interrupt time limit"); } } bool lis3dh_set_fifo_wtm(uint8_t wtm) { uint8_t fifo_ctrl_reg = prv_read_reg(LIS3DH_FIFO_CTRL_REG); fifo_ctrl_reg &= ~THR_MASK; fifo_ctrl_reg |= (wtm & THR_MASK); return (prv_write_reg(LIS3DH_FIFO_CTRL_REG, fifo_ctrl_reg)); } uint8_t lis3dh_get_fifo_wtm(void) { uint8_t fifo_ctrl_reg = prv_read_reg(LIS3DH_FIFO_CTRL_REG); return (fifo_ctrl_reg & THR_MASK); } AccelSamplingRate accel_get_sampling_rate(void) { uint8_t odr = ODR_MASK & prv_read_reg(LIS3DH_CTRL_REG1); if (odr == (ODR2 | ODR0)) { return (ACCEL_SAMPLING_100HZ); } else if (odr == ODR2) { return (ACCEL_SAMPLING_50HZ); } else if (odr == (ODR1 | ODR0)) { return (ACCEL_SAMPLING_25HZ); } else if (odr == (ODR1)) { return (ACCEL_SAMPLING_10HZ); } else { PBL_LOG(LOG_LEVEL_ERROR, "Unrecognized ODR value %d", odr); return (0); } } bool accel_set_sampling_rate(AccelSamplingRate rate) { uint8_t odr; switch (rate) { case ACCEL_SAMPLING_100HZ: odr = ODR2 | ODR0; break; case ACCEL_SAMPLING_50HZ: odr = ODR2; break; case ACCEL_SAMPLING_25HZ: odr = ODR1 | ODR0; break; case ACCEL_SAMPLING_10HZ: odr = ODR1; break; default: PBL_LOG(LOG_LEVEL_ERROR, "Unsupported sampling rate %d", rate); return (false); } uint8_t ctrl_reg_1 = prv_read_reg(LIS3DH_CTRL_REG1); ctrl_reg_1 &= ~ODR_MASK; ctrl_reg_1 |= (odr & ODR_MASK); //TODO: fix hack below (enabling axes after lis3dh_power_down) ctrl_reg_1 |= (Xen | Yen | Zen); //enable x, y and z axis bool res = prv_write_reg(LIS3DH_CTRL_REG1, ctrl_reg_1); // Update the click limit based on sampling frequency uint8_t time_limit = rate * LIS3DH_TIME_LIMIT_MULT / LIS3DH_TIME_LIMIT_DIV; lis3dh_set_interrupt_time_limit(time_limit); PBL_LOG(LOG_LEVEL_DEBUG, "setting click time limit to 0x%x", lis3dh_get_interrupt_time_limit()); // Update click latency uint8_t time_latency = rate * LIS3DH_TIME_LATENCY_MULT / LIS3DH_TIME_LATENCY_DIV; lis3dh_set_click_latency(time_latency); PBL_LOG(LOG_LEVEL_DEBUG, "setting click time latency to 0x%x", lis3dh_get_click_latency()); // Update click window uint32_t time_window = rate * LIS3DH_TIME_WINDOW_MULT / LIS3DH_TIME_WINDOW_DIV; time_window = MIN(time_window, 0xff); lis3dh_set_click_window(time_window); PBL_LOG(LOG_LEVEL_DEBUG, "setting click time window to 0x%x", lis3dh_get_click_window()); return (res); } Lis3dhScale accel_get_scale(void) { uint8_t fs = FS_MASK & prv_read_reg(LIS3DH_CTRL_REG4); if (fs == (FS0 | FS1)) { return (LIS3DH_SCALE_16G); } else if (fs == FS1) { return (LIS3DH_SCALE_8G); } else if (fs == FS0) { return (LIS3DH_SCALE_4G); } else if (fs == 0) { return (LIS3DH_SCALE_2G); } else { PBL_LOG(LOG_LEVEL_ERROR, "Unrecognized FS value %d", fs); return (LIS3DH_SCALE_UNKNOWN); } } bool accel_set_scale(Lis3dhScale scale) { uint8_t fs; switch (scale) { case LIS3DH_SCALE_16G: fs = (FS0 | FS1); break; case LIS3DH_SCALE_8G: fs = FS1; break; case LIS3DH_SCALE_4G: fs = FS0; break; case LIS3DH_SCALE_2G: fs = 0; break; default: PBL_LOG(LOG_LEVEL_ERROR, "Unsupported scale %d", scale); return (false); } uint8_t ctrl_reg_4 = prv_read_reg(LIS3DH_CTRL_REG4); ctrl_reg_4 &= ~FS_MASK; ctrl_reg_4 |= (fs & FS_MASK); bool res = prv_write_reg(LIS3DH_CTRL_REG4, ctrl_reg_4); lis3dh_set_interrupt_threshold(scale * LIS3DH_THRESHOLD_MULT / LIS3DH_THRESHOLD_DIV); PBL_LOG(LOG_LEVEL_DEBUG, "setting click threshold to 0x%x", lis3dh_get_interrupt_threshold()); return (res); } bool lis3dh_set_fifo_mode(uint8_t mode) { uint8_t fifo_ctrl_reg = prv_read_reg(LIS3DH_FIFO_CTRL_REG); fifo_ctrl_reg &= ~MODE_MASK; fifo_ctrl_reg |= (mode & MODE_MASK); return (prv_write_reg(LIS3DH_FIFO_CTRL_REG, fifo_ctrl_reg)); } uint8_t lis3dh_get_fifo_mode(void) { uint8_t fifo_ctrl_reg = prv_read_reg(LIS3DH_FIFO_CTRL_REG); return (fifo_ctrl_reg & MODE_MASK); } //! Configure the accel to run "Self Test 0". See S3.2.2 of the accel datasheet for more information bool lis3dh_enter_self_test_mode(SelfTestMode mode) { uint8_t reg4 = 0x8; switch (mode) { case SELF_TEST_MODE_ONE: reg4 |= 0x2; break; case SELF_TEST_MODE_TWO: reg4 |= (0x2 | 0x4); break; default: break; } struct I2CCommand test_mode_config[] = { { LIS3DH_CTRL_REG1, 0x9f }, { LIS3DH_CTRL_REG3, 0x00 }, { LIS3DH_CTRL_REG4, reg4 } }; return send_i2c_commands(test_mode_config, ARRAY_LENGTH(test_mode_config)); } void lis3dh_exit_self_test_mode(void) { lis3dh_config_set_defaults(); } // // Boot-time config // //! Ask the accel for a 8-bit value that's programmed into the IC at the //! factory. Useful as a sanity check to make sure everything came up properly. bool lis3dh_sanity_check(void) { uint8_t whoami = prv_read_reg(LIS3DH_WHO_AM_I); PBL_LOG(LOG_LEVEL_DEBUG, "Read accel whomai byte 0x%x, expecting 0x%x", whoami, LIS3DH_WHOAMI_BYTE); return (whoami == LIS3DH_WHOAMI_BYTE); } bool lis3dh_config_set_defaults() { // Follow the startup sequence from AN3308 struct I2CCommand accel_init_commands[] = { { LIS3DH_CTRL_REG1, (ODR1 | ODR0 | Zen | Yen | Xen) }, // 25MHz, Enable X,Y,Z Axes { LIS3DH_CTRL_REG2, 0x00 }, { LIS3DH_CTRL_REG3, I1_WTM }, // FIFO Watermark on INT1 { LIS3DH_CTRL_REG4, (BDU | FS0 | HR) }, // Block Read, +/- 4g sensitivity { LIS3DH_CTRL_REG6, I2_CLICK }, // Click on INT2 { LIS3DH_INT1_THS, 0x20 }, // inertial threshold (MAX 0x7f) { LIS3DH_INT1_DURATION, 0x10 }, // interrupt duration (units of 1/(update frequency) [See CTRL_REG1]) { LIS3DH_INT1_CFG, 0x00 }, // no inertial interrupts // click threshold (MAX 0x7f) { LIS3DH_CLICK_THS, LIS3DH_SCALE_4G * LIS3DH_THRESHOLD_MULT / LIS3DH_THRESHOLD_DIV }, // click time limit (units of 1/(update frequency) [See CTRL_REG1]) { LIS3DH_TIME_LIMIT, ACCEL_DEFAULT_SAMPLING_RATE * LIS3DH_TIME_LIMIT_MULT / LIS3DH_TIME_LIMIT_DIV}, { LIS3DH_CLICK_CFG, (XS | YS | ZS) }, // single click detection on the X axis { LIS3DH_FIFO_CTRL_REG, MODE_BYPASS | 0x19 }, // BYPASS MODE and 25 samples per interrupt // time latency, ie "debounce time" after the first of a double click // (units of 1/(update frequency) [See CTRL_REG1]) { LIS3DH_TIME_LATENCY, ACCEL_DEFAULT_SAMPLING_RATE * LIS3DH_TIME_LATENCY_MULT / LIS3DH_TIME_LATENCY_DIV }, // max time allowed between clicks for a double click (end to start) // (units of 1/(update frequency) [See CTRL_REG1]) { LIS3DH_TIME_WINDOW, ACCEL_DEFAULT_SAMPLING_RATE * LIS3DH_TIME_WINDOW_MULT / LIS3DH_TIME_WINDOW_DIV} }; if (!send_i2c_commands(accel_init_commands, ARRAY_LENGTH(accel_init_commands))) { accel_stop(); PBL_LOG(LOG_LEVEL_WARNING, "Failed to initialize accelerometer"); return false; } return true; }