| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Support for QST QMC6308 3-Axis Magnetic Sensor on I2C bus. |
| * |
| * Copyright (C) 2026 Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> |
| * |
| * Datasheet available at |
| * <https://qstcorp.com/upload/pdf/202202/13-52-15%20QMC6308%20Datasheet%20Rev.%20F(1).pdf> |
| */ |
| |
| #include <linux/array_size.h> |
| #include <linux/bitfield.h> |
| #include <linux/bits.h> |
| #include <linux/cleanup.h> |
| #include <linux/delay.h> |
| #include <linux/dev_printk.h> |
| #include <linux/err.h> |
| #include <linux/i2c.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/regmap.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/time.h> |
| #include <linux/types.h> |
| |
| #include <asm/byteorder.h> |
| |
| #include <linux/iio/iio.h> |
| |
| #define QMC6308_REG_ID 0x00 |
| #define QMC6308_REG_X_LSB 0x01 |
| #define QMC6308_REG_STATUS 0x09 |
| #define QMC6308_REG_CTRL1 0x0A |
| #define QMC6308_REG_CTRL2 0x0B |
| #define QMC6308_REG_CTRL3 0x0D |
| #define QMC6308_REG_CTRL4 0x29 |
| |
| #define QMC6308_CHIP_ID 0x80 |
| |
| /* Control register 1 */ |
| #define QMC6308_MODE_MASK GENMASK(1, 0) |
| #define QMC6308_ODR_MASK GENMASK(3, 2) |
| #define QMC6308_OSR1_MASK GENMASK(5, 4) |
| #define QMC6308_OSR2_MASK GENMASK(7, 6) |
| |
| #define QMC6308_MODE_SUSPEND 0x00 |
| #define QMC6308_MODE_NORMAL 0x01 |
| |
| #define QMC6308_ODR_10HZ 0x00 |
| #define QMC6308_ODR_50HZ 0x01 |
| #define QMC6308_ODR_100HZ 0x02 |
| #define QMC6308_ODR_200HZ 0x03 |
| |
| #define QMC6308_OSR1_8 0x00 |
| #define QMC6308_OSR1_4 0x01 |
| #define QMC6308_OSR1_2 0x02 |
| #define QMC6308_OSR1_1 0x03 |
| |
| /* Control register 2 */ |
| #define QMC6308_SET_RESET_MASK GENMASK(1, 0) |
| #define QMC6308_RNG_MASK GENMASK(3, 2) |
| #define QMC6308_SELF_TEST BIT(6) |
| #define QMC6308_SOFT_RST BIT(7) |
| |
| #define QMC6308_SET_RESET_ON 0x00 |
| |
| #define QMC6308_RNG_30G 0x00 |
| #define QMC6308_RNG_12G 0x01 |
| #define QMC6308_RNG_8G 0x02 |
| #define QMC6308_RNG_2G 0x03 |
| |
| /* Status register */ |
| #define QMC6308_STATUS_DRDY BIT(0) |
| #define QMC6308_STATUS_OVFL BIT(1) |
| |
| /* Power-on completion time (datasheet Table 7) */ |
| #define QMC6308_POR_US 250 |
| |
| #define QMC6308_AUTOSUSPEND_DELAY_MS 500 |
| |
| struct qmc6308_data { |
| struct regmap *regmap; |
| /* Protect data->range/odr/osr and serialize measurements */ |
| struct mutex mutex; |
| struct iio_mount_matrix orientation; |
| u8 range; |
| u8 odr; |
| u8 osr; |
| }; |
| |
| enum qmc6308_axis { |
| QMC6308_AXIS_X, |
| QMC6308_AXIS_Y, |
| QMC6308_AXIS_Z, |
| }; |
| |
| static const int qmc6308_odr_avail[] = { |
| [QMC6308_ODR_10HZ] = 10, |
| [QMC6308_ODR_50HZ] = 50, |
| [QMC6308_ODR_100HZ] = 100, |
| [QMC6308_ODR_200HZ] = 200, |
| }; |
| |
| static const int qmc6308_osr1_avail[] = { |
| [QMC6308_OSR1_8] = 8, |
| [QMC6308_OSR1_4] = 4, |
| [QMC6308_OSR1_2] = 2, |
| [QMC6308_OSR1_1] = 1, |
| }; |
| |
| /* |
| * Sensitivity is 1000/2500/3750/15000 LSB/Gauss for the |
| * +-30/12/8/2 Gauss ranges respectively. |
| */ |
| static const int qmc6308_scales[][2] = { |
| [QMC6308_RNG_30G] = { 0, 1000000 }, |
| [QMC6308_RNG_12G] = { 0, 400000 }, |
| [QMC6308_RNG_8G] = { 0, 266667 }, |
| [QMC6308_RNG_2G] = { 0, 66667 }, |
| }; |
| |
| static int qmc6308_set_mode(struct qmc6308_data *data, unsigned int mode) |
| { |
| return regmap_update_bits(data->regmap, QMC6308_REG_CTRL1, |
| QMC6308_MODE_MASK, |
| FIELD_PREP(QMC6308_MODE_MASK, mode)); |
| } |
| |
| static int qmc6308_take_measurement(struct iio_dev *indio_dev, int index, |
| int *val) |
| { |
| struct qmc6308_data *data = iio_priv(indio_dev); |
| struct device *dev = regmap_get_device(data->regmap); |
| unsigned int status; |
| __le16 buf[3]; |
| int ret; |
| |
| PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dev, pm); |
| ret = PM_RUNTIME_ACQUIRE_ERR(&pm); |
| if (ret) { |
| dev_err(dev, "Failed to power on (%d)\n", ret); |
| return ret; |
| } |
| |
| guard(mutex)(&data->mutex); |
| |
| /* |
| * Reading the status register clears DRDY, which is why the poll |
| * and the data read stay under one mutex hold. A runtime resume |
| * clears DRDY too, so a sample converted before the last suspend |
| * is never returned here. |
| * |
| * The timeout is 50ms of headroom over the slowest ODR (10Hz). |
| */ |
| ret = regmap_read_poll_timeout(data->regmap, QMC6308_REG_STATUS, |
| status, (status & QMC6308_STATUS_DRDY), |
| 2 * USEC_PER_MSEC, |
| 150 * USEC_PER_MSEC); |
| if (ret) |
| return ret; |
| |
| ret = regmap_bulk_read(data->regmap, QMC6308_REG_X_LSB, buf, |
| sizeof(buf)); |
| if (ret) |
| return ret; |
| |
| if (status & QMC6308_STATUS_OVFL) |
| return -ERANGE; |
| |
| *val = (s16)le16_to_cpu(buf[index]); |
| |
| return 0; |
| } |
| |
| static int qmc6308_read_raw(struct iio_dev *indio_dev, |
| const struct iio_chan_spec *chan, |
| int *val, int *val2, long mask) |
| { |
| struct qmc6308_data *data = iio_priv(indio_dev); |
| int ret; |
| |
| switch (mask) { |
| case IIO_CHAN_INFO_RAW: |
| ret = qmc6308_take_measurement(indio_dev, chan->address, val); |
| if (ret) |
| return ret; |
| return IIO_VAL_INT; |
| case IIO_CHAN_INFO_SCALE: { |
| guard(mutex)(&data->mutex); |
| |
| *val = qmc6308_scales[data->range][0]; |
| *val2 = qmc6308_scales[data->range][1]; |
| |
| return IIO_VAL_INT_PLUS_NANO; |
| } |
| case IIO_CHAN_INFO_SAMP_FREQ: { |
| guard(mutex)(&data->mutex); |
| |
| *val = qmc6308_odr_avail[data->odr]; |
| |
| return IIO_VAL_INT; |
| } |
| case IIO_CHAN_INFO_OVERSAMPLING_RATIO: { |
| guard(mutex)(&data->mutex); |
| |
| *val = qmc6308_osr1_avail[data->osr]; |
| |
| return IIO_VAL_INT; |
| } |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int qmc6308_write_raw(struct iio_dev *indio_dev, |
| const struct iio_chan_spec *chan, |
| int val, int val2, long mask) |
| { |
| struct qmc6308_data *data = iio_priv(indio_dev); |
| unsigned int status; |
| unsigned int i; |
| int ret; |
| |
| switch (mask) { |
| case IIO_CHAN_INFO_SCALE: { |
| if (val != 0) |
| return -EINVAL; |
| |
| for (i = 0; i < ARRAY_SIZE(qmc6308_scales); i++) { |
| if (val2 == qmc6308_scales[i][1]) |
| break; |
| } |
| if (i == ARRAY_SIZE(qmc6308_scales)) |
| return -EINVAL; |
| |
| guard(mutex)(&data->mutex); |
| |
| ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL2, |
| QMC6308_RNG_MASK, |
| FIELD_PREP(QMC6308_RNG_MASK, i)); |
| if (ret) |
| return ret; |
| |
| data->range = i; |
| |
| /* |
| * The data registers still hold (and DRDY still |
| * advertises) a sample converted at the previous range; |
| * discard it so that a read does not pair old-range data |
| * with the new scale. A conversion already in flight may |
| * still complete at the old range, so this narrows the |
| * window rather than closing it. The range change itself |
| * took effect, so only log a failure here: an error |
| * would mislead userspace about an effective write. |
| */ |
| ret = regmap_read(data->regmap, QMC6308_REG_STATUS, |
| &status); |
| if (ret) |
| dev_warn(regmap_get_device(data->regmap), |
| "Failed to discard stale sample (%d)\n", ret); |
| |
| return 0; |
| } |
| case IIO_CHAN_INFO_SAMP_FREQ: { |
| for (i = 0; i < ARRAY_SIZE(qmc6308_odr_avail); i++) { |
| if (val == qmc6308_odr_avail[i]) |
| break; |
| } |
| if (i == ARRAY_SIZE(qmc6308_odr_avail)) |
| return -EINVAL; |
| |
| guard(mutex)(&data->mutex); |
| |
| ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL1, |
| QMC6308_ODR_MASK, |
| FIELD_PREP(QMC6308_ODR_MASK, i)); |
| if (ret) |
| return ret; |
| |
| data->odr = i; |
| |
| return 0; |
| } |
| case IIO_CHAN_INFO_OVERSAMPLING_RATIO: { |
| for (i = 0; i < ARRAY_SIZE(qmc6308_osr1_avail); i++) { |
| if (val == qmc6308_osr1_avail[i]) |
| break; |
| } |
| if (i == ARRAY_SIZE(qmc6308_osr1_avail)) |
| return -EINVAL; |
| |
| guard(mutex)(&data->mutex); |
| |
| ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL1, |
| QMC6308_OSR1_MASK, |
| FIELD_PREP(QMC6308_OSR1_MASK, i)); |
| if (ret) |
| return ret; |
| |
| data->osr = i; |
| |
| return 0; |
| } |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int qmc6308_read_avail(struct iio_dev *indio_dev, |
| struct iio_chan_spec const *chan, |
| const int **vals, int *type, int *length, |
| long mask) |
| { |
| switch (mask) { |
| case IIO_CHAN_INFO_SAMP_FREQ: |
| *vals = qmc6308_odr_avail; |
| *type = IIO_VAL_INT; |
| *length = ARRAY_SIZE(qmc6308_odr_avail); |
| return IIO_AVAIL_LIST; |
| case IIO_CHAN_INFO_OVERSAMPLING_RATIO: |
| *vals = qmc6308_osr1_avail; |
| *type = IIO_VAL_INT; |
| *length = ARRAY_SIZE(qmc6308_osr1_avail); |
| return IIO_AVAIL_LIST; |
| case IIO_CHAN_INFO_SCALE: |
| *vals = (const int *)qmc6308_scales; |
| *type = IIO_VAL_INT_PLUS_NANO; |
| *length = ARRAY_SIZE(qmc6308_scales) * 2; |
| return IIO_AVAIL_LIST; |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int qmc6308_write_raw_get_fmt(struct iio_dev *indio_dev, |
| struct iio_chan_spec const *chan, |
| long mask) |
| { |
| switch (mask) { |
| case IIO_CHAN_INFO_SCALE: |
| return IIO_VAL_INT_PLUS_NANO; |
| default: |
| return IIO_VAL_INT; |
| } |
| } |
| |
| static const struct iio_mount_matrix * |
| qmc6308_get_mount_matrix(const struct iio_dev *indio_dev, |
| const struct iio_chan_spec *chan) |
| { |
| struct qmc6308_data *data = iio_priv(indio_dev); |
| |
| return &data->orientation; |
| } |
| |
| static const struct iio_chan_spec_ext_info qmc6308_ext_info[] = { |
| IIO_MOUNT_MATRIX(IIO_SHARED_BY_DIR, qmc6308_get_mount_matrix), |
| { } |
| }; |
| |
| static const struct iio_info qmc6308_info = { |
| .read_raw = qmc6308_read_raw, |
| .write_raw = qmc6308_write_raw, |
| .read_avail = qmc6308_read_avail, |
| .write_raw_get_fmt = qmc6308_write_raw_get_fmt, |
| }; |
| |
| static int qmc6308_init(struct qmc6308_data *data) |
| { |
| struct regmap *map = data->regmap; |
| unsigned int reg; |
| int ret; |
| |
| ret = regmap_read(map, QMC6308_REG_ID, ®); |
| if (ret) |
| return ret; |
| |
| /* Allow unknown IDs so that fallback compatibles work */ |
| if (reg != QMC6308_CHIP_ID) |
| dev_warn(regmap_get_device(map), |
| "Unknown chip id: 0x%02x, continuing\n", reg); |
| |
| /* The SOFT_RST bit is not auto-cleared and must be written back 0 */ |
| ret = regmap_write(map, QMC6308_REG_CTRL2, QMC6308_SOFT_RST); |
| if (ret) |
| return ret; |
| |
| /* |
| * The datasheet gives no soft-reset completion figure; reuse the |
| * power-on time as a conservative bound. |
| */ |
| fsleep(QMC6308_POR_US); |
| |
| data->range = QMC6308_RNG_30G; |
| data->odr = QMC6308_ODR_50HZ; |
| data->osr = QMC6308_OSR1_8; |
| |
| ret = regmap_write(map, QMC6308_REG_CTRL2, |
| FIELD_PREP(QMC6308_SET_RESET_MASK, |
| QMC6308_SET_RESET_ON) | |
| FIELD_PREP(QMC6308_RNG_MASK, data->range)); |
| if (ret) |
| return ret; |
| |
| /* OSR2 (second-stage filter) set to its power-on default of 0 */ |
| return regmap_write(map, QMC6308_REG_CTRL1, |
| FIELD_PREP(QMC6308_MODE_MASK, |
| QMC6308_MODE_NORMAL) | |
| FIELD_PREP(QMC6308_ODR_MASK, data->odr) | |
| FIELD_PREP(QMC6308_OSR1_MASK, data->osr) | |
| FIELD_PREP(QMC6308_OSR2_MASK, 0)); |
| } |
| |
| static void qmc6308_power_down_action(void *priv) |
| { |
| struct qmc6308_data *data = priv; |
| |
| if (!pm_runtime_status_suspended(regmap_get_device(data->regmap))) |
| qmc6308_set_mode(data, QMC6308_MODE_SUSPEND); |
| } |
| |
| static bool qmc6308_volatile_reg(struct device *dev, unsigned int reg) |
| { |
| return reg >= QMC6308_REG_X_LSB && reg <= QMC6308_REG_STATUS; |
| } |
| |
| static bool qmc6308_writable_reg(struct device *dev, unsigned int reg) |
| { |
| switch (reg) { |
| case QMC6308_REG_CTRL1: |
| case QMC6308_REG_CTRL2: |
| case QMC6308_REG_CTRL3: |
| case QMC6308_REG_CTRL4: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| static const struct regmap_config qmc6308_regmap_config = { |
| .reg_bits = 8, |
| .val_bits = 8, |
| .max_register = QMC6308_REG_CTRL4, |
| .cache_type = REGCACHE_MAPLE, |
| .volatile_reg = qmc6308_volatile_reg, |
| .writeable_reg = qmc6308_writable_reg, |
| }; |
| |
| #define QMC6308_CHANNEL(_axis) \ |
| { \ |
| .type = IIO_MAGN, \ |
| .modified = 1, \ |
| .channel2 = IIO_MOD_##_axis, \ |
| .address = QMC6308_AXIS_##_axis, \ |
| .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ |
| .info_mask_shared_by_type = \ |
| BIT(IIO_CHAN_INFO_SCALE) | \ |
| BIT(IIO_CHAN_INFO_SAMP_FREQ) | \ |
| BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \ |
| .info_mask_shared_by_type_available = \ |
| BIT(IIO_CHAN_INFO_SCALE) | \ |
| BIT(IIO_CHAN_INFO_SAMP_FREQ) | \ |
| BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \ |
| .ext_info = qmc6308_ext_info, \ |
| } |
| |
| static const struct iio_chan_spec qmc6308_channels[] = { |
| QMC6308_CHANNEL(X), |
| QMC6308_CHANNEL(Y), |
| QMC6308_CHANNEL(Z), |
| }; |
| |
| static int qmc6308_probe(struct i2c_client *client) |
| { |
| struct device *dev = &client->dev; |
| struct qmc6308_data *data; |
| struct iio_dev *indio_dev; |
| struct regmap *map; |
| int ret; |
| |
| indio_dev = devm_iio_device_alloc(dev, sizeof(*data)); |
| if (!indio_dev) |
| return -ENOMEM; |
| |
| i2c_set_clientdata(client, indio_dev); |
| |
| map = devm_regmap_init_i2c(client, &qmc6308_regmap_config); |
| if (IS_ERR(map)) |
| return dev_err_probe(dev, PTR_ERR(map), |
| "regmap initialization failed\n"); |
| |
| ret = devm_regulator_get_enable(dev, "vdd"); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "Failed to enable VDD regulator\n"); |
| |
| fsleep(QMC6308_POR_US); |
| |
| data = iio_priv(indio_dev); |
| data->regmap = map; |
| |
| ret = devm_mutex_init(dev, &data->mutex); |
| if (ret) |
| return ret; |
| |
| ret = iio_read_mount_matrix(dev, &data->orientation); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "Failed to read mount matrix\n"); |
| |
| indio_dev->name = "qmc6308"; |
| indio_dev->info = &qmc6308_info; |
| indio_dev->channels = qmc6308_channels; |
| indio_dev->num_channels = ARRAY_SIZE(qmc6308_channels); |
| indio_dev->modes = INDIO_DIRECT_MODE; |
| |
| ret = qmc6308_init(data); |
| if (ret) |
| return dev_err_probe(dev, ret, "qmc6308 init failed\n"); |
| |
| ret = pm_runtime_set_active(dev); |
| if (ret) |
| return ret; |
| |
| ret = devm_add_action_or_reset(dev, qmc6308_power_down_action, data); |
| if (ret) |
| return ret; |
| |
| pm_runtime_use_autosuspend(dev); |
| pm_runtime_set_autosuspend_delay(dev, QMC6308_AUTOSUSPEND_DELAY_MS); |
| |
| ret = devm_pm_runtime_enable(dev); |
| if (ret) |
| return ret; |
| |
| return devm_iio_device_register(dev, indio_dev); |
| } |
| |
| static int qmc6308_runtime_suspend(struct device *dev) |
| { |
| struct iio_dev *indio_dev = dev_get_drvdata(dev); |
| struct qmc6308_data *data = iio_priv(indio_dev); |
| |
| return qmc6308_set_mode(data, QMC6308_MODE_SUSPEND); |
| } |
| |
| static int qmc6308_runtime_resume(struct device *dev) |
| { |
| struct iio_dev *indio_dev = dev_get_drvdata(dev); |
| struct qmc6308_data *data = iio_priv(indio_dev); |
| unsigned int status; |
| int ret; |
| |
| ret = qmc6308_set_mode(data, QMC6308_MODE_NORMAL); |
| if (ret) |
| return ret; |
| |
| /* |
| * DRDY may still be set for a sample converted before the last |
| * suspend; reading the status register clears it so the next |
| * measurement waits for fresh data. |
| */ |
| ret = regmap_read(data->regmap, QMC6308_REG_STATUS, &status); |
| if (ret) { |
| /* Best effort to leave the chip in a consistent state */ |
| qmc6308_set_mode(data, QMC6308_MODE_SUSPEND); |
| } |
| |
| return ret; |
| } |
| |
| static DEFINE_RUNTIME_DEV_PM_OPS(qmc6308_pm_ops, qmc6308_runtime_suspend, |
| qmc6308_runtime_resume, NULL); |
| |
| static const struct of_device_id qmc6308_match[] = { |
| { .compatible = "qstcorp,qmc6308" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, qmc6308_match); |
| |
| static const struct i2c_device_id qmc6308_id[] = { |
| { .name = "qmc6308" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(i2c, qmc6308_id); |
| |
| static struct i2c_driver qmc6308_driver = { |
| .driver = { |
| .name = "qmc6308", |
| .of_match_table = qmc6308_match, |
| .pm = pm_ptr(&qmc6308_pm_ops), |
| }, |
| .id_table = qmc6308_id, |
| .probe = qmc6308_probe, |
| }; |
| module_i2c_driver(qmc6308_driver); |
| |
| MODULE_DESCRIPTION("QST QMC6308 3-Axis Magnetic Sensor driver"); |
| MODULE_AUTHOR("Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>"); |
| MODULE_LICENSE("GPL"); |