/* ST Microelectronics LIS2MDL 3-axis magnetometer sensor * * Copyright (c) 2018-2019 STMicroelectronics * * SPDX-License-Identifier: Apache-2.0 * * Datasheet: * https://www.st.com/resource/en/datasheet/lis2mdl.pdf */ #define DT_DRV_COMPAT st_lis2mdl #include #include #include #include #include #include #include "lis2mdl.h" /* Based on the data sheet, the maximum turn-on time is ("9.4 ms + 1/ODR") when * offset cancellation is on. But in the single mode the ODR is not dependent on * the configured value in Reg A. It is dependent on the frequency of the I2C * signal. The slowest value we could measure by I2C frequency of 100000HZ was * 13 ms. So we chose 20 ms. */ #define SAMPLE_FETCH_TIMEOUT_MS 20 struct lis2mdl_data lis2mdl_data; LOG_MODULE_REGISTER(LIS2MDL, CONFIG_SENSOR_LOG_LEVEL); #ifdef CONFIG_LIS2MDL_MAG_ODR_RUNTIME static int lis2mdl_set_odr(const struct device *dev, const struct sensor_value *val) { const struct lis2mdl_config *cfg = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&cfg->ctx; lis2mdl_odr_t odr; switch (val->val1) { case 10: odr = LIS2MDL_ODR_10Hz; break; case 20: odr = LIS2MDL_ODR_20Hz; break; case 50: odr = LIS2MDL_ODR_50Hz; break; case 100: odr = LIS2MDL_ODR_100Hz; break; default: return -EINVAL; } if (lis2mdl_data_rate_set(ctx, odr)) { return -EIO; } return 0; } #endif /* CONFIG_LIS2MDL_MAG_ODR_RUNTIME */ static int lis2mdl_set_hard_iron(const struct device *dev, enum sensor_channel chan, const struct sensor_value *val) { const struct lis2mdl_config *cfg = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&cfg->ctx; uint8_t i; int16_t offset[3]; for (i = 0U; i < 3; i++) { offset[i] = val->val1; val++; } return lis2mdl_mag_user_offset_set(ctx, offset); } static void lis2mdl_channel_get_mag(const struct device *dev, enum sensor_channel chan, struct sensor_value *val) { int32_t cval; int i; uint8_t ofs_start, ofs_stop; struct lis2mdl_data *lis2mdl = dev->data; struct sensor_value *pval = val; switch (chan) { case SENSOR_CHAN_MAGN_X: ofs_start = ofs_stop = 0U; break; case SENSOR_CHAN_MAGN_Y: ofs_start = ofs_stop = 1U; break; case SENSOR_CHAN_MAGN_Z: ofs_start = ofs_stop = 2U; break; default: ofs_start = 0U; ofs_stop = 2U; break; } for (i = ofs_start; i <= ofs_stop; i++) { cval = lis2mdl->mag[i] * 1500; pval->val1 = cval / 1000000; pval->val2 = cval % 1000000; pval++; } } /* read internal temperature */ static void lis2mdl_channel_get_temp(const struct device *dev, struct sensor_value *val) { struct lis2mdl_data *drv_data = dev->data; /* formula is temp = 25 + (temp / 8) C */ val->val1 = 25 + drv_data->temp_sample / 8; val->val2 = (drv_data->temp_sample % 8) * 1000000 / 8; } static int lis2mdl_channel_get(const struct device *dev, enum sensor_channel chan, struct sensor_value *val) { switch (chan) { case SENSOR_CHAN_MAGN_X: case SENSOR_CHAN_MAGN_Y: case SENSOR_CHAN_MAGN_Z: case SENSOR_CHAN_MAGN_XYZ: lis2mdl_channel_get_mag(dev, chan, val); break; case SENSOR_CHAN_DIE_TEMP: lis2mdl_channel_get_temp(dev, val); break; default: LOG_ERR("Channel not supported"); return -ENOTSUP; } return 0; } static int lis2mdl_config(const struct device *dev, enum sensor_channel chan, enum sensor_attribute attr, const struct sensor_value *val) { switch (attr) { #ifdef CONFIG_LIS2MDL_MAG_ODR_RUNTIME case SENSOR_ATTR_SAMPLING_FREQUENCY: return lis2mdl_set_odr(dev, val); #endif /* CONFIG_LIS2MDL_MAG_ODR_RUNTIME */ case SENSOR_ATTR_OFFSET: return lis2mdl_set_hard_iron(dev, chan, val); default: LOG_ERR("Mag attribute not supported"); return -ENOTSUP; } return 0; } static int lis2mdl_attr_set(const struct device *dev, enum sensor_channel chan, enum sensor_attribute attr, const struct sensor_value *val) { switch (chan) { case SENSOR_CHAN_ALL: case SENSOR_CHAN_MAGN_X: case SENSOR_CHAN_MAGN_Y: case SENSOR_CHAN_MAGN_Z: case SENSOR_CHAN_MAGN_XYZ: return lis2mdl_config(dev, chan, attr, val); default: LOG_ERR("attr_set() not supported on %d channel", chan); return -ENOTSUP; } return 0; } static int get_single_mode_raw_data(const struct device *dev, int16_t *raw_mag) { struct lis2mdl_data *lis2mdl = dev->data; const struct lis2mdl_config *cfg = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&cfg->ctx; int rc = 0; rc = lis2mdl_operating_mode_set(ctx, LIS2MDL_SINGLE_TRIGGER); if (rc) { LOG_ERR("set single mode failed"); return rc; } if (k_sem_take(&lis2mdl->fetch_sem, K_MSEC(SAMPLE_FETCH_TIMEOUT_MS))) { LOG_ERR("Magnetometer data not ready within %d ms", SAMPLE_FETCH_TIMEOUT_MS); return -EIO; } /* fetch raw data sample */ rc = lis2mdl_magnetic_raw_get(ctx, raw_mag); if (rc) { LOG_ERR("Failed to read sample"); return rc; } return 0; } static int lis2mdl_sample_fetch_mag(const struct device *dev) { struct lis2mdl_data *lis2mdl = dev->data; const struct lis2mdl_config *cfg = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&cfg->ctx; int16_t raw_mag[3]; int rc = 0; if (cfg->single_mode) { rc = get_single_mode_raw_data(dev, raw_mag); if (rc) { LOG_ERR("Failed to read raw data"); return rc; } lis2mdl->mag[0] = raw_mag[0]; lis2mdl->mag[1] = raw_mag[1]; lis2mdl->mag[2] = raw_mag[2]; if (cfg->cancel_offset) { /* The second measurement is needed when offset * cancellation is enabled in the single mode. Then the * average of the first measurement done above and this * one would be the final value. This process is not * needed in continuous mode since it has been taken * care by lis2mdl itself automatically. Please refer * to the application note for more details. */ rc = get_single_mode_raw_data(dev, raw_mag); if (rc) { LOG_ERR("Failed to read raw data"); return rc; } lis2mdl->mag[0] += raw_mag[0]; lis2mdl->mag[1] += raw_mag[1]; lis2mdl->mag[2] += raw_mag[2]; lis2mdl->mag[0] /= 2; lis2mdl->mag[1] /= 2; lis2mdl->mag[2] /= 2; } } else { /* fetch raw data sample */ rc = lis2mdl_magnetic_raw_get(ctx, raw_mag); if (rc) { LOG_ERR("Failed to read sample"); return rc; } lis2mdl->mag[0] = raw_mag[0]; lis2mdl->mag[1] = raw_mag[1]; lis2mdl->mag[2] = raw_mag[2]; } return 0; } static int lis2mdl_sample_fetch_temp(const struct device *dev) { struct lis2mdl_data *lis2mdl = dev->data; const struct lis2mdl_config *cfg = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&cfg->ctx; int16_t raw_temp; /* fetch raw temperature sample */ if (lis2mdl_temperature_raw_get(ctx, &raw_temp) < 0) { LOG_ERR("Failed to read sample"); return -EIO; } lis2mdl->temp_sample = raw_temp; return 0; } static int lis2mdl_sample_fetch(const struct device *dev, enum sensor_channel chan) { switch (chan) { case SENSOR_CHAN_MAGN_X: case SENSOR_CHAN_MAGN_Y: case SENSOR_CHAN_MAGN_Z: case SENSOR_CHAN_MAGN_XYZ: lis2mdl_sample_fetch_mag(dev); break; case SENSOR_CHAN_DIE_TEMP: lis2mdl_sample_fetch_temp(dev); break; case SENSOR_CHAN_ALL: lis2mdl_sample_fetch_mag(dev); lis2mdl_sample_fetch_temp(dev); break; default: return -ENOTSUP; } return 0; } static DEVICE_API(sensor, lis2mdl_driver_api) = { .attr_set = lis2mdl_attr_set, #if CONFIG_LIS2MDL_TRIGGER .trigger_set = lis2mdl_trigger_set, #endif .sample_fetch = lis2mdl_sample_fetch, .channel_get = lis2mdl_channel_get, }; static int lis2mdl_init(const struct device *dev) { struct lis2mdl_data *lis2mdl = dev->data; const struct lis2mdl_config *cfg = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&cfg->ctx; uint8_t wai; int rc = 0; lis2mdl->dev = dev; if (cfg->spi_4wires) { /* Set SPI 4wires if it's the case */ if (lis2mdl_spi_mode_set(ctx, LIS2MDL_SPI_4_WIRE) < 0) { return -EIO; } } /* check chip ID */ if (lis2mdl_device_id_get(ctx, &wai) < 0) { return -EIO; } if (wai != LIS2MDL_ID) { LOG_ERR("Invalid chip ID: %02x", wai); return -EINVAL; } /* reset sensor configuration */ if (lis2mdl_reset_set(ctx, PROPERTY_ENABLE) < 0) { LOG_ERR("s/w reset failed"); return -EIO; } k_busy_wait(100); if (cfg->spi_4wires) { /* After s/w reset set SPI 4wires again if the case */ if (lis2mdl_spi_mode_set(ctx, LIS2MDL_SPI_4_WIRE) < 0) { return -EIO; } } /* enable BDU */ if (lis2mdl_block_data_update_set(ctx, PROPERTY_ENABLE) < 0) { LOG_ERR("setting bdu failed"); return -EIO; } /* Set Output Data Rate */ if (lis2mdl_data_rate_set(ctx, LIS2MDL_ODR_10Hz)) { LOG_ERR("set odr failed"); return -EIO; } if (cfg->cancel_offset) { /* Set offset cancellation, common for both single and * and continuous mode. */ if (lis2mdl_set_rst_mode_set(ctx, LIS2MDL_SENS_OFF_CANC_EVERY_ODR)) { LOG_ERR("reset sensor mode failed"); return -EIO; } } /* Enable temperature compensation */ if (lis2mdl_offset_temp_comp_set(ctx, PROPERTY_ENABLE)) { LOG_ERR("enable temp compensation failed"); return -EIO; } if (cfg->cancel_offset && cfg->single_mode) { /* Set OFF_CANC_ONE_SHOT bit. This setting is only needed in * the single-mode when offset cancellation is enabled. */ rc = lis2mdl_set_rst_sensor_single_set(ctx, PROPERTY_ENABLE); if (rc) { LOG_ERR("Set offset cancellation failed"); return rc; } } if (cfg->single_mode) { /* Set drdy on pin 7 */ rc = lis2mdl_drdy_on_pin_set(ctx, 1); if (rc) { LOG_ERR("set drdy on pin failed!"); return rc; } /* Reboot sensor after setting the configuration registers */ rc = lis2mdl_boot_set(ctx, 1); if (rc) { LOG_ERR("Reboot failed."); return rc; } k_sem_init(&lis2mdl->fetch_sem, 0, 1); } else { /* Set device in continuous mode */ rc = lis2mdl_operating_mode_set(ctx, LIS2MDL_CONTINUOUS_MODE); if (rc) { LOG_ERR("set continuous mode failed"); return rc; } } #ifdef CONFIG_LIS2MDL_TRIGGER if (cfg->trig_enabled) { if (lis2mdl_init_interrupt(dev) < 0) { LOG_ERR("Failed to initialize interrupts"); return -EIO; } } #endif return 0; } #ifdef CONFIG_PM_DEVICE static int lis2mdl_pm_action(const struct device *dev, enum pm_device_action action) { const struct lis2mdl_config *config = dev->config; stmdev_ctx_t *ctx = (stmdev_ctx_t *)&config->ctx; int status = 0; switch (action) { case PM_DEVICE_ACTION_RESUME: if (config->single_mode) { status = lis2mdl_operating_mode_set(ctx, LIS2MDL_SINGLE_TRIGGER); } else { status = lis2mdl_operating_mode_set(ctx, LIS2MDL_CONTINUOUS_MODE); } if (status) { LOG_ERR("Power up failed"); } LOG_DBG("State changed to active"); break; case PM_DEVICE_ACTION_SUSPEND: status = lis2mdl_operating_mode_set(ctx, LIS2MDL_POWER_DOWN); if (status) { LOG_ERR("Power down failed"); } LOG_DBG("State changed to inactive"); break; default: return -ENOTSUP; } return status; } #endif /* CONFIG_PM_DEVICE */ #if DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) == 0 #warning "LIS2MDL driver enabled without any devices" #endif /* * Device creation macro, shared by LIS2MDL_DEFINE_SPI() and * LIS2MDL_DEFINE_I2C(). */ #define LIS2MDL_DEVICE_INIT(inst) \ PM_DEVICE_DT_INST_DEFINE(inst, lis2mdl_pm_action); \ \ SENSOR_DEVICE_DT_INST_DEFINE(inst, \ lis2mdl_init, \ PM_DEVICE_DT_INST_GET(inst), \ &lis2mdl_data_##inst, \ &lis2mdl_config_##inst, \ POST_KERNEL, \ CONFIG_SENSOR_INIT_PRIORITY, \ &lis2mdl_driver_api); /* * Instantiation macros used when a device is on a SPI bus. */ #ifdef CONFIG_LIS2MDL_TRIGGER #define LIS2MDL_CFG_IRQ(inst) \ .trig_enabled = true, \ .gpio_drdy = GPIO_DT_SPEC_INST_GET(inst, irq_gpios) #else #define LIS2MDL_CFG_IRQ(inst) #endif /* CONFIG_LIS2MDL_TRIGGER */ #define LIS2MDL_CONFIG_COMMON(inst) \ .cancel_offset = DT_INST_PROP(inst, cancel_offset), \ .single_mode = DT_INST_PROP(inst, single_mode), \ COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, irq_gpios), \ (LIS2MDL_CFG_IRQ(inst)), ()) #define LIS2MDL_SPI_OPERATION (SPI_WORD_SET(8) | \ SPI_OP_MODE_MASTER | \ SPI_MODE_CPOL | \ SPI_MODE_CPHA) \ #define LIS2MDL_CONFIG_SPI(inst) \ { \ STMEMSC_CTX_SPI(&lis2mdl_config_##inst.stmemsc_cfg), \ .stmemsc_cfg = { \ .spi = SPI_DT_SPEC_INST_GET(inst, \ LIS2MDL_SPI_OPERATION, \ 0), \ }, \ .spi_4wires = DT_INST_PROP(inst, duplex) == \ SPI_FULL_DUPLEX, \ LIS2MDL_CONFIG_COMMON(inst) \ } /* * Instantiation macros used when a device is on an I2C bus. */ #define LIS2MDL_CONFIG_I2C(inst) \ { \ STMEMSC_CTX_I2C(&lis2mdl_config_##inst.stmemsc_cfg), \ .stmemsc_cfg = { \ .i2c = I2C_DT_SPEC_INST_GET(inst), \ }, \ LIS2MDL_CONFIG_COMMON(inst) \ } /* * Main instantiation macro. Use of COND_CODE_1() selects the right * bus-specific macro at preprocessor time. */ #define LIS2MDL_DEFINE(inst) \ static struct lis2mdl_data lis2mdl_data_##inst; \ static const struct lis2mdl_config lis2mdl_config_##inst = \ COND_CODE_1(DT_INST_ON_BUS(inst, spi), \ (LIS2MDL_CONFIG_SPI(inst)), \ (LIS2MDL_CONFIG_I2C(inst))); \ LIS2MDL_DEVICE_INIT(inst) DT_INST_FOREACH_STATUS_OKAY(LIS2MDL_DEFINE)