diff --git a/drivers/iio/humidity/Kconfig b/drivers/iio/humidity/Kconfig index 54f11f000..b54cbd6b3 100644 --- a/drivers/iio/humidity/Kconfig +++ b/drivers/iio/humidity/Kconfig @@ -143,4 +143,14 @@ config SI7020 To compile this driver as a module, choose M here: the module will be called si7020. +config AHT20 + tristate "ASAIR AHT20 temperature and humidity sensor" + depends on I2C + help + If you say yes here you get support for the ASAIR AHT20 + temperature and humidity sensor. + + This driver can also be built as a module. If so, the module + will be called aht20. + endmenu diff --git a/drivers/iio/humidity/Makefile b/drivers/iio/humidity/Makefile index 34b3dc749..230a0ea68 100644 --- a/drivers/iio/humidity/Makefile +++ b/drivers/iio/humidity/Makefile @@ -20,5 +20,6 @@ obj-$(CONFIG_HTS221_SPI) += hts221_spi.o obj-$(CONFIG_HTU21) += htu21.o obj-$(CONFIG_SI7005) += si7005.o obj-$(CONFIG_SI7020) += si7020.o +obj-$(CONFIG_AHT20) += aht20.o ccflags-y += -I$(srctree)/drivers/iio/common/hid-sensors diff --git a/drivers/iio/humidity/aht20.c b/drivers/iio/humidity/aht20.c new file mode 100644 index 000000000..3f4283cd4 --- /dev/null +++ b/drivers/iio/humidity/aht20.c @@ -0,0 +1,780 @@ +/* + * AHT20 I2C temperature and humidity sensor driver + * + * Copyright (c) 2025 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "aht20" +#define DRIVER_VERSION "1.0.0" + +/* Default I2C address (can be overridden by device tree) */ +#define AHT20_DEFAULT_ADDR 0x70 + +/* Commands */ +#define AHT20_CMD_INIT 0xBE +#define AHT20_CMD_MEASURE 0xAC +#define AHT20_CMD_SOFTRESET 0xBA +#define AHT20_CMD_STATUS 0x71 + +/* Status bits */ +#define AHT20_STATUS_BUSY BIT(7) +#define AHT20_STATUS_CALIBRATED BIT(3) + +/* Timing constants */ +#define AHT20_POWER_ON_DELAY_MS 20 +#define AHT20_SOFT_RESET_DELAY_MS 20 +#define AHT20_MEASURE_DELAY_MS 80 +#define AHT20_CALIBRATION_DELAY_MS 10 + +/* Data length */ +#define AHT20_DATA_LEN 7 + +/* Channel definitions */ +#define AHT20_CHANNEL_TEMP 0 +#define AHT20_CHANNEL_HUMIDITY 1 +#define AHT20_CHANNEL_COUNT 2 + +/* Scale factors */ +#define AHT20_TEMP_SCALE_NANO 244141 /* 200 / 2^20 * 10^9 */ +#define AHT20_TEMP_OFFSET (-50 * 1000000000LL) +#define AHT20_HUMIDITY_SCALE_NANO 95367 /* 100 / 2^20 * 10^9 */ + +struct aht20_data { + struct i2c_client *client; + struct mutex lock; + struct regulator *vdd; + + struct iio_trigger *trig; + struct delayed_work measure_work; + bool triggered_measurement; + + /* Last reading */ + u32 raw_temp; + u32 raw_humidity; + u8 status; + bool valid; + + /* Calibration data */ + bool calibrated; + int64_t timestamp; + + /* For triggered buffer */ + struct { + s32 temp; + s32 humidity; + s64 ts; + } scan; + + /* Device specific data */ + u8 i2c_addr; /* Store the actual I2C address */ +}; + +/** + * aht20_read_data() - read raw data from sensor + */ +static int aht20_read_data(struct i2c_client *client, u8 *buf, int len) +{ + // struct aht20_data *data = i2c_get_clientdata(client); + struct i2c_msg msgs[2] = { + { + .addr = client->addr, + .flags = 0, + .len = 0, + .buf = NULL, + }, + { + .addr = client->addr, + .flags = I2C_M_RD, + .len = len, + .buf = buf, + } + }; + int ret; + + /* Use the stored address for debugging/logging purposes */ + dev_info(&client->dev, "Reading from I2C address 0x%02x\n", client->addr); + + ret = i2c_transfer(client->adapter, msgs, 2); + if (ret < 0) { + dev_err(&client->dev, "I2C read failed: %d\n", ret); + return ret; + } + + return (ret == 2) ? 0 : -EIO; +} + +/** + * aht20_write_cmd() - write command to sensor + */ +static int aht20_write_cmd(struct i2c_client *client, u8 *buf, int len) +{ + //struct aht20_data *data = i2c_get_clientdata(client); + struct i2c_msg msg = { + .addr = client->addr, + .flags = 0, + .len = len, + .buf = buf, + }; + int ret; + + dev_info(&client->dev, "Writing to I2C address 0x%02x\n", client->addr); + + ret = i2c_transfer(client->adapter, &msg, 1); + if (ret < 0) { + dev_err(&client->dev, "I2C write failed: %d\n", ret); + return ret; + } + + return (ret == 1) ? 0 : -EIO; +} + +/** + * aht20_soft_reset() - perform soft reset + */ +static int aht20_soft_reset(struct i2c_client *client) +{ + u8 cmd = AHT20_CMD_SOFTRESET; + int ret; + + dev_info(&client->dev, "Sending soft reset command\n"); + + ret = aht20_write_cmd(client, &cmd, 1); + if (ret < 0) + return ret; + + msleep(AHT20_SOFT_RESET_DELAY_MS); + return 0; +} + +/** + * aht20_read_status() - read status register + */ +static int aht20_read_status(struct i2c_client *client, u8 *status) +{ + int ret; + + ret = aht20_read_data(client, status, 1); + if (ret < 0) + return ret; + + dev_dbg(&client->dev, "Status register: 0x%02x\n", *status); + return 0; +} + +/** + * aht20_init_sensor() - initialize sensor + */ +static int aht20_init_sensor(struct i2c_client *client) +{ + struct aht20_data *data = i2c_get_clientdata(client); + u8 cmd[3] = {AHT20_CMD_INIT, 0x08, 0x00}; + u8 status; + int ret; + + dev_info(&client->dev, "Initializing AHT20 at I2C address 0x%02x\n", + client->addr); + + /* Power on delay */ + msleep(AHT20_POWER_ON_DELAY_MS); + + /* Soft reset */ + ret = aht20_soft_reset(client); + if (ret < 0) { + dev_err(&client->dev, "aht20 soft reset fail\n"); + return ret; + } + + /* Check calibration status */ + ret = aht20_read_status(client, &status); + if (ret < 0) + return ret; + + if (!(status & AHT20_STATUS_CALIBRATED)) { + dev_info(&client->dev, "Calibrating sensor\n"); + + /* Send calibration command */ + ret = aht20_write_cmd(client, cmd, 3); + if (ret < 0) + return ret; + + msleep(AHT20_CALIBRATION_DELAY_MS); + data->calibrated = true; + dev_info(&client->dev, "Calibration complete\n"); + } else { + dev_info(&client->dev, "Sensor already calibrated\n"); + data->calibrated = true; + } + + return 0; +} + +/** + * aht20_calc_crc() - calculate CRC8 + */ +static u8 aht20_calc_crc(const u8 *data, u8 len) +{ + u8 i, bit; + u8 crc = 0xFF; + + for (i = 0; i < len; i++) { + crc ^= data[i]; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc <<= 1; + } + } + + return crc; +} + +/** + * aht20_read_measurement() - read temperature and humidity + */ +static int aht20_read_measurement(struct i2c_client *client, + u32 *raw_temp, u32 *raw_humidity) +{ + // struct aht20_data *data = i2c_get_clientdata(client); + u8 cmd[3] = {AHT20_CMD_MEASURE, 0x33, 0x00}; + u8 data_buf[AHT20_DATA_LEN]; + u8 crc; + int ret; + + dev_dbg(&client->dev, "Starting measurement\n"); + + /* Send measurement command */ + ret = aht20_write_cmd(client, cmd, 3); + if (ret < 0) + return ret; + + /* Wait for measurement to complete */ + msleep(AHT20_MEASURE_DELAY_MS); + + /* Read measurement data */ + ret = aht20_read_data(client, data_buf, AHT20_DATA_LEN); + if (ret < 0) + return ret; + + /* Check if sensor is busy */ + if (data_buf[0] & AHT20_STATUS_BUSY) { + dev_warn(&client->dev, "Sensor busy, retrying\n"); + return -EAGAIN; + } + + /* Verify CRC */ + crc = aht20_calc_crc(data_buf, 6); + if (crc != data_buf[6]) { + dev_err(&client->dev, "CRC error: expected 0x%02x, got 0x%02x\n", + crc, data_buf[6]); + return -EIO; + } + + /* Extract humidity data (20 bits) */ + *raw_humidity = ((u32)data_buf[1] << 16) | + ((u32)data_buf[2] << 8) | + data_buf[3]; + *raw_humidity >>= 4; + + /* Extract temperature data (20 bits) */ + *raw_temp = ((u32)data_buf[3] & 0x0F) << 16 | + ((u32)data_buf[4] << 8) | + data_buf[5]; + + dev_info(&client->dev, "Raw humidity: 0x%05x, Raw temperature: 0x%05x\n", + *raw_humidity, *raw_temp); + + return 0; +} + +/** + * aht20_read_raw() - IIO read_raw callback + */ +static int aht20_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int *val, int *val2, long mask) +{ + struct aht20_data *data = iio_priv(indio_dev); + struct i2c_client *client = data->client; + int ret = 0; + + dev_dbg(&client->dev, "aht20_read_raw enter ... %ld, type:%d\n", mask, chan->type); + + mutex_lock(&data->lock); + + switch (mask) { + case IIO_CHAN_INFO_RAW: + /* Trigger a new measurement */ + ret = aht20_read_measurement(client, + &data->raw_temp, + &data->raw_humidity); + if (ret < 0) { + mutex_unlock(&data->lock); + return ret; + } + + data->valid = true; + data->timestamp = iio_get_time_ns(indio_dev); + + switch (chan->type) { + case IIO_TEMP: + *val = (s32)data->raw_temp; + ret = IIO_VAL_INT; + dev_dbg(&client->dev, "Temperature raw: %d (0x%x)\n", + *val, data->raw_temp); + break; + case IIO_HUMIDITYRELATIVE: + *val = (s32)data->raw_humidity; + ret = IIO_VAL_INT; + dev_dbg(&client->dev, "Humidity raw: %d (0x%x)\n", + *val, data->raw_humidity); + break; + default: + ret = -EINVAL; + break; + } + break; + + case IIO_CHAN_INFO_SCALE: + switch (chan->type) { + case IIO_TEMP: + *val = AHT20_TEMP_SCALE_NANO / 1000; + *val2 = AHT20_TEMP_SCALE_NANO % 1000 * 1000; + ret = IIO_VAL_INT_PLUS_NANO; + break; + case IIO_HUMIDITYRELATIVE: + *val = 0; + *val2 = AHT20_HUMIDITY_SCALE_NANO; + ret = IIO_VAL_INT_PLUS_NANO; + break; + default: + ret = -EINVAL; + break; + } + break; + + case IIO_CHAN_INFO_OFFSET: + if (chan->type == IIO_TEMP) { + *val = AHT20_TEMP_OFFSET / 1000000000LL; + *val2 = AHT20_TEMP_OFFSET % 1000000000LL; + ret = IIO_VAL_INT_PLUS_NANO; + } else { + ret = -EINVAL; + } + break; + + case IIO_CHAN_INFO_SAMP_FREQ: + /* Fixed measurement time ~80ms */ + *val = 12; /* ~12 Hz */ + *val2 = 0; + ret = IIO_VAL_INT; + break; + + default: + ret = -EINVAL; + break; + } + + mutex_unlock(&data->lock); + + if (ret >= 0 && mask == IIO_CHAN_INFO_RAW) { + dev_dbg(&client->dev, "Successfully read channel type %d: %d\n", + chan->type, *val); + } + return ret; +} + +/** + * aht20_write_raw() - IIO write_raw callback + */ +static int aht20_write_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int val, int val2, long mask) +{ + /* This sensor doesn't support configuration */ + return -EINVAL; +} + +/** + * aht20_read_avail() - IIO read_avail callback + */ +static int aht20_read_avail(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + const int **vals, int *type, int *length, + long mask) +{ + static const int temp_scale_avail[] = { AHT20_TEMP_SCALE_NANO }; + static const int humidity_scale_avail[] = { AHT20_HUMIDITY_SCALE_NANO }; + static const int sample_freq_avail[] = { 12 }; + + switch (mask) { + case IIO_CHAN_INFO_SCALE: + *type = IIO_VAL_INT_PLUS_NANO; + *length = 1; + if (chan->type == IIO_TEMP) { + *vals = temp_scale_avail; + } else { + *vals = humidity_scale_avail; + } + return IIO_AVAIL_LIST; + + case IIO_CHAN_INFO_SAMP_FREQ: + *type = IIO_VAL_INT; + *length = 1; + *vals = sample_freq_avail; + return IIO_AVAIL_LIST; + + default: + return -EINVAL; + } +} + +/** + * aht20_validate_trigger() - Validate trigger + */ +static int aht20_validate_trigger(struct iio_dev *indio_dev, + struct iio_trigger *trig) +{ + struct aht20_data *data = iio_priv(indio_dev); + + if (data->trig != trig) + return -EINVAL; + + return 0; +} + +/** + * aht20_trigger_handler() - Trigger handler for buffer + */ +static irqreturn_t aht20_trigger_handler(int irq, void *p) +{ + struct iio_poll_func *pf = p; + struct iio_dev *indio_dev = pf->indio_dev; + struct aht20_data *data = iio_priv(indio_dev); + int ret; + + mutex_lock(&data->lock); + + /* Perform measurement */ + ret = aht20_read_measurement(data->client, + &data->raw_temp, + &data->raw_humidity); + if (ret < 0) { + mutex_unlock(&data->lock); + goto out; + } + + data->valid = true; + data->timestamp = iio_get_time_ns(indio_dev); + + /* Convert raw values to processed values */ + /* Temperature: T = raw * 200 / 2^20 - 50 */ + data->scan.temp = ((s64)data->raw_temp * 200000000000LL >> 20) - 50000000000LL; + + /* Humidity: H = raw * 100 / 2^20 */ + data->scan.humidity = (s64)data->raw_humidity * 100000000000LL >> 20; + data->scan.ts = data->timestamp; + + /* Push to buffer */ + iio_push_to_buffers_with_timestamp(indio_dev, &data->scan, + data->timestamp); + + mutex_unlock(&data->lock); + +out: + iio_trigger_notify_done(indio_dev->trig); + return IRQ_HANDLED; +} + +/** + * aht20_measurement_work() - Work function for periodic measurements + */ +static void aht20_measurement_work(struct work_struct *work) +{ + struct aht20_data *data = container_of(to_delayed_work(work), + struct aht20_data, + measure_work); + struct iio_dev *indio_dev = i2c_get_clientdata(data->client); + int ret; + + mutex_lock(&data->lock); + + ret = aht20_read_measurement(data->client, + &data->raw_temp, + &data->raw_humidity); + if (ret == 0) { + data->valid = true; + data->timestamp = iio_get_time_ns(indio_dev); + + /* Store in buffer if triggered */ + if (data->triggered_measurement) { + data->scan.temp = ((s64)data->raw_temp * 200000000000LL >> 20) - 50000000000LL; + data->scan.humidity = (s64)data->raw_humidity * 100000000000LL >> 20; + data->scan.ts = data->timestamp; + + iio_push_to_buffers_with_timestamp(indio_dev, + &data->scan, + data->timestamp); + } + } + + mutex_unlock(&data->lock); + + /* Schedule next measurement */ + if (data->triggered_measurement) { + schedule_delayed_work(&data->measure_work, + msecs_to_jiffies(AHT20_MEASURE_DELAY_MS)); + } +} + +/** + * aht20_buffer_preenable() - Buffer preenable callback + */ +static int aht20_buffer_preenable(struct iio_dev *indio_dev) +{ + struct aht20_data *data = iio_priv(indio_dev); + + data->triggered_measurement = true; + schedule_delayed_work(&data->measure_work, + msecs_to_jiffies(AHT20_MEASURE_DELAY_MS)); + return 0; +} + +/** + * aht20_buffer_postdisable() - Buffer postdisable callback + */ +static int aht20_buffer_postdisable(struct iio_dev *indio_dev) +{ + struct aht20_data *data = iio_priv(indio_dev); + + data->triggered_measurement = false; + cancel_delayed_work_sync(&data->measure_work); + return 0; +} + +static const struct iio_buffer_setup_ops aht20_buffer_setup_ops = { + .preenable = aht20_buffer_preenable, + // .postenable = iio_triggered_buffer_postenable, + // .predisable = iio_triggered_buffer_predisable, + .postdisable = aht20_buffer_postdisable, +}; + +/* Channel definitions */ +static const struct iio_chan_spec aht20_channels[] = { + { + .type = IIO_TEMP, + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | + BIT(IIO_CHAN_INFO_SCALE) | + BIT(IIO_CHAN_INFO_OFFSET) | + BIT(IIO_CHAN_INFO_SAMP_FREQ), + .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) | + BIT(IIO_CHAN_INFO_OFFSET), + .info_mask_shared_by_type_available = + BIT(IIO_CHAN_INFO_SCALE) | + BIT(IIO_CHAN_INFO_SAMP_FREQ), + .scan_index = AHT20_CHANNEL_TEMP, + .scan_type = { + .sign = 's', + .realbits = 32, + .storagebits = 32, + .endianness = IIO_CPU, + }, + }, + { + .type = IIO_HUMIDITYRELATIVE, + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | + BIT(IIO_CHAN_INFO_SCALE) | + BIT(IIO_CHAN_INFO_SAMP_FREQ), + .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), + .info_mask_shared_by_type_available = + BIT(IIO_CHAN_INFO_SCALE) | + BIT(IIO_CHAN_INFO_SAMP_FREQ), + .scan_index = AHT20_CHANNEL_HUMIDITY, + .scan_type = { + .sign = 's', + .realbits = 32, + .storagebits = 32, + .endianness = IIO_CPU, + }, + }, + IIO_CHAN_SOFT_TIMESTAMP(2), +}; + +/* IIO info structure */ +static const struct iio_info aht20_info = { + .read_raw = aht20_read_raw, + .write_raw = aht20_write_raw, + .read_avail = aht20_read_avail, + .validate_trigger = aht20_validate_trigger, +}; + +/* Device tree match table */ +static const struct of_device_id aht20_of_match[] = { + { .compatible = "asair,aht20" }, + { } +}; +MODULE_DEVICE_TABLE(of, aht20_of_match); + +/* I2C device ID table */ +static const struct i2c_device_id aht20_id[] = { + { "aht20", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, aht20_id); + +/** + * aht20_probe() - probe function + */ +static int aht20_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct iio_dev *indio_dev; + struct aht20_data *data; + int ret; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) + return -ENODEV; + + /* Get I2C address from device tree */ + if (!client->addr) { + dev_err(dev, "I2C address not specified\n"); + return -EINVAL; + } + + dev_info(dev, "Probing AHT20 at I2C address 0x%02x\n", client->addr); + + /* Allocate IIO device */ + indio_dev = devm_iio_device_alloc(dev, sizeof(*data)); + if (!indio_dev) + return -ENOMEM; + + data = iio_priv(indio_dev); + data->client = client; + data->i2c_addr = client->addr; /* Store the address */ + mutex_init(&data->lock); + + i2c_set_clientdata(client, indio_dev); + + /* Get power supply (optional) */ + data->vdd = devm_regulator_get_optional(dev, "vdd"); + if (IS_ERR(data->vdd)) { + if (PTR_ERR(data->vdd) != -ENODEV) { + ret = PTR_ERR(data->vdd); + dev_err(dev, "Failed to get regulator: %d\n", ret); + return ret; + } + /* No regulator specified, assume always powered */ + data->vdd = NULL; + dev_info(dev, "No regulator specified, assuming always powered\n"); + } + + /* Enable power if regulator exists */ + if (data->vdd) { + ret = regulator_enable(data->vdd); + if (ret < 0) { + dev_err(dev, "Failed to enable regulator: %d\n", ret); + return ret; + } + dev_info(dev, "Regulator enabled\n"); + } + + /* Initialize sensor */ + ret = aht20_init_sensor(client); + if (ret < 0) { + dev_err(dev, "Failed to initialize AHT20: %d\n", ret); + goto err_disable_reg; + } + + /* Initialize delayed work */ + INIT_DELAYED_WORK(&data->measure_work, aht20_measurement_work); + + /* Setup IIO device */ + indio_dev->name = dev_name(dev); + indio_dev->modes = INDIO_DIRECT_MODE; + indio_dev->info = &aht20_info; + indio_dev->channels = aht20_channels; + indio_dev->num_channels = ARRAY_SIZE(aht20_channels); + + /* Setup buffer */ + ret = devm_iio_triggered_buffer_setup(dev, indio_dev, + iio_pollfunc_store_time, + aht20_trigger_handler, + &aht20_buffer_setup_ops); + if (ret < 0) { + dev_err(dev, "Failed to setup buffer: %d\n", ret); + goto err_disable_reg; + } + + /* Register IIO device */ + ret = devm_iio_device_register(dev, indio_dev); + if (ret < 0) { + dev_err(dev, "Failed to register IIO device: %d\n", ret); + goto err_disable_reg; + } + + dev_info(dev, "AHT20 temperature/humidity sensor registered at 0x%02x\n", + client->addr); + return 0; + +err_disable_reg: + regulator_disable(data->vdd); + return ret; +} + +/** + * aht20_remove() - remove function + */ +static void aht20_remove(struct i2c_client *client) +{ + struct iio_dev *indio_dev = i2c_get_clientdata(client); + struct aht20_data *data = iio_priv(indio_dev); + + /* Cancel any pending work */ + cancel_delayed_work_sync(&data->measure_work); + + /* Disable power */ + regulator_disable(data->vdd); + + dev_info(&client->dev, "AHT20 removed from I2C address 0x%02x\n", + client->addr); +} + +static struct i2c_driver aht20_driver = { + .driver = { + .name = DRIVER_NAME, + .of_match_table = aht20_of_match, + }, + .probe = aht20_probe, + .remove = aht20_remove, + .id_table = aht20_id, +}; + +module_i2c_driver(aht20_driver); + +MODULE_AUTHOR("Xiaochuan Mao"); +MODULE_DESCRIPTION("AHT20 I2C temperature and humidity sensor driver"); +MODULE_LICENSE("GPL v2"); +MODULE_VERSION(DRIVER_VERSION);