/* 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) { struct lis2mdl_data *lis2mdl = dev->data; 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(lis2mdl->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) { struct lis2mdl_data *lis2mdl = dev->data; uint8_t i; int16_t offset[3]; for (i = 0U; i < 3; i++) { offset[i] = sys_cpu_to_le16(val->val1); val++; } return lis2mdl_mag_user_offset_set(lis2mdl->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; val->val1 = drv_data->temp_sample / 100; val->val2 = (drv_data->temp_sample % 100) * 10000; } 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; int rc = 0; rc = lis2mdl_operating_mode_set(lis2mdl->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(lis2mdl->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 *const config = dev->config; int16_t raw_mag[3]; int rc = 0; if (config->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] = sys_le16_to_cpu(raw_mag[0]); lis2mdl->mag[1] = sys_le16_to_cpu(raw_mag[1]); lis2mdl->mag[2] = sys_le16_to_cpu(raw_mag[2]); if (config->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 beed 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] += sys_le16_to_cpu(raw_mag[0]); lis2mdl->mag[1] += sys_le16_to_cpu(raw_mag[1]); lis2mdl->mag[2] += sys_le16_to_cpu(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(lis2mdl->ctx, raw_mag); if (rc) { LOG_ERR("Failed to read sample"); return rc; } lis2mdl->mag[0] = sys_le16_to_cpu(raw_mag[0]); lis2mdl->mag[1] = sys_le16_to_cpu(raw_mag[1]); lis2mdl->mag[2] = sys_le16_to_cpu(raw_mag[2]); } return 0; } static int lis2mdl_sample_fetch_temp(const struct device *dev) { struct lis2mdl_data *lis2mdl = dev->data; int16_t raw_temp; int32_t temp; /* fetch raw temperature sample */ if (lis2mdl_temperature_raw_get(lis2mdl->ctx, &raw_temp) < 0) { LOG_ERR("Failed to read sample"); return -EIO; } /* formula is temp = 25 + (temp / 8) C */ temp = (sys_le16_to_cpu(raw_temp) & 0x8FFF); lis2mdl->temp_sample = 2500 + (temp * 100) / 8; 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 const struct sensor_driver_api 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_interface(const struct device *dev) { const struct lis2mdl_config *const config = dev->config; struct lis2mdl_data *lis2mdl = dev->data; lis2mdl->bus = device_get_binding(config->master_dev_name); if (!lis2mdl->bus) { LOG_ERR("Could not get pointer to %s device", config->master_dev_name); return -EINVAL; } return config->bus_init(dev); } static const struct lis2mdl_config lis2mdl_dev_config = { .master_dev_name = DT_INST_BUS_LABEL(0), .single_mode = DT_INST_PROP(0, single_mode), .cancel_offset = DT_INST_PROP(0, cancel_offset), #ifdef CONFIG_LIS2MDL_TRIGGER .gpio_name = DT_INST_GPIO_LABEL(0, irq_gpios), .gpio_pin = DT_INST_GPIO_PIN(0, irq_gpios), .gpio_flags = DT_INST_GPIO_FLAGS(0, irq_gpios), #endif /* CONFIG_LIS2MDL_TRIGGER */ #if DT_ANY_INST_ON_BUS_STATUS_OKAY(spi) .bus_init = lis2mdl_spi_init, .spi_conf.frequency = DT_INST_PROP(0, spi_max_frequency), .spi_conf.operation = (SPI_OP_MODE_MASTER | SPI_MODE_CPOL | SPI_MODE_CPHA | SPI_WORD_SET(8) | SPI_LINES_SINGLE), .spi_conf.slave = DT_INST_REG_ADDR(0), #if DT_INST_SPI_DEV_HAS_CS_GPIOS(0) .gpio_cs_port = DT_INST_SPI_DEV_CS_GPIOS_LABEL(0), .cs_gpio = DT_INST_SPI_DEV_CS_GPIOS_PIN(0), .cs_gpio_flags = DT_INST_SPI_DEV_CS_GPIOS_FLAGS(0), .spi_conf.cs = &lis2mdl_data.cs_ctrl, #else .spi_conf.cs = NULL, #endif #elif DT_ANY_INST_ON_BUS_STATUS_OKAY(i2c) .bus_init = lis2mdl_i2c_init, .i2c_slv_addr = DT_INST_REG_ADDR(0), #else #error "BUS MACRO NOT DEFINED IN DTS" #endif }; static int lis2mdl_init(const struct device *dev) { struct lis2mdl_data *lis2mdl = dev->data; const struct lis2mdl_config *const config = dev->config; uint8_t wai; int rc = 0; lis2mdl->dev = dev; if (lis2mdl_init_interface(dev)) { return -EINVAL; } /* check chip ID */ if (lis2mdl_device_id_get(lis2mdl->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(lis2mdl->ctx, PROPERTY_ENABLE) < 0) { LOG_ERR("s/w reset failed"); return -EIO; } k_busy_wait(100); #if CONFIG_LIS2MDL_SPI_FULL_DUPLEX /* After s/w reset set SPI 4wires again if the case */ if (lis2mdl_spi_mode_set(lis2mdl->ctx, LIS2MDL_SPI_4_WIRE) < 0) { return -EIO; } #endif /* enable BDU */ if (lis2mdl_block_data_update_set(lis2mdl->ctx, PROPERTY_ENABLE) < 0) { LOG_ERR("setting bdu failed"); return -EIO; } /* Set Output Data Rate */ if (lis2mdl_data_rate_set(lis2mdl->ctx, LIS2MDL_ODR_10Hz)) { LOG_ERR("set odr failed"); return -EIO; } if (config->cancel_offset) { /* Set offset cancellation, common for both single and * and continuous mode. */ if (lis2mdl_set_rst_mode_set(lis2mdl->ctx, LIS2MDL_SENS_OFF_CANC_EVERY_ODR)) { LOG_ERR("reset sensor mode failed"); return -EIO; } } /* Enable temperature compensation */ if (lis2mdl_offset_temp_comp_set(lis2mdl->ctx, PROPERTY_ENABLE)) { LOG_ERR("enable temp compensation failed"); return -EIO; } if (config->cancel_offset && config->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(lis2mdl->ctx, PROPERTY_ENABLE); if (rc) { LOG_ERR("Set offset cancelaltion failed"); return rc; } } if (config->single_mode) { /* Set drdy on pin 7 */ rc = lis2mdl_drdy_on_pin_set(lis2mdl->ctx, 1); if (rc) { LOG_ERR("set drdy on pin failed!"); return rc; } /* Reboot sensor after setting the configuration registers */ rc = lis2mdl_boot_set(lis2mdl->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(lis2mdl->ctx, LIS2MDL_CONTINUOUS_MODE); if (rc) { LOG_ERR("set continuos mode failed"); return rc; } } #ifdef CONFIG_PM_DEVICE lis2mdl->power_state = DEVICE_PM_ACTIVE_STATE; #endif #ifdef CONFIG_LIS2MDL_TRIGGER if (lis2mdl_init_interrupt(dev) < 0) { LOG_ERR("Failed to initialize interrupts"); return -EIO; } #endif return 0; } #ifdef CONFIG_PM_DEVICE static int lis2mdl_set_power_state(struct lis2mdl_data *lis2mdl, const struct lis2mdl_config *const config, uint32_t new_state) { int status = 0; if (new_state == DEVICE_PM_ACTIVE_STATE) { if (config->single_mode) { status = lis2mdl_operating_mode_set(lis2mdl->ctx, LIS2MDL_SINGLE_TRIGGER); } else { status = lis2mdl_operating_mode_set(lis2mdl->ctx, LIS2MDL_CONTINUOUS_MODE); } if (status) { LOG_ERR("Power up failed"); } lis2mdl->power_state = DEVICE_PM_ACTIVE_STATE; LOG_DBG("State changed to active"); } else { __ASSERT_NO_MSG(new_state == DEVICE_PM_LOW_POWER_STATE || new_state == DEVICE_PM_SUSPEND_STATE || new_state == DEVICE_PM_OFF_STATE); status = lis2mdl_operating_mode_set(lis2mdl->ctx, LIS2MDL_POWER_DOWN); if (status) { LOG_ERR("Power down failed"); } lis2mdl->power_state = new_state; LOG_DBG("State changed to inactive"); } return status; } static int lis2mdl_pm_control(const struct device *dev, uint32_t ctrl_command, void *context, device_pm_cb cb, void *arg) { struct lis2mdl_data *lis2mdl = dev->data; const struct lis2mdl_config *const config = dev->config; uint32_t current_state = lis2mdl->power_state; int status = 0; uint32_t new_state; switch (ctrl_command) { case DEVICE_PM_SET_POWER_STATE: new_state = *((const uint32_t *)context); if (new_state != current_state) { status = lis2mdl_set_power_state(lis2mdl, config, new_state); } break; case DEVICE_PM_GET_POWER_STATE: *((uint32_t *)context) = current_state; break; default: LOG_ERR("Got unknown power management control command"); status = -EINVAL; } if (cb) { cb(dev, status, context, arg); } return status; } #endif /* CONFIG_PM_DEVICE */ DEVICE_DT_INST_DEFINE(0, lis2mdl_init, lis2mdl_pm_control, &lis2mdl_data, &lis2mdl_dev_config, POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, &lis2mdl_driver_api);