1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * ams-iaq-core.c - Support for AMS iAQ-Core VOC sensors |
4 | * |
5 | * Copyright (C) 2015, 2018 |
6 | * Author: Matt Ranostay <matt.ranostay@konsulko.com> |
7 | */ |
8 | |
9 | #include <linux/module.h> |
10 | #include <linux/mod_devicetable.h> |
11 | #include <linux/mutex.h> |
12 | #include <linux/init.h> |
13 | #include <linux/i2c.h> |
14 | #include <linux/iio/iio.h> |
15 | |
16 | #define AMS_IAQCORE_DATA_SIZE 9 |
17 | |
18 | #define AMS_IAQCORE_VOC_CO2_IDX 0 |
19 | #define AMS_IAQCORE_VOC_RESISTANCE_IDX 1 |
20 | #define AMS_IAQCORE_VOC_TVOC_IDX 2 |
21 | |
22 | struct ams_iaqcore_reading { |
23 | __be16 co2_ppm; |
24 | u8 status; |
25 | __be32 resistance; |
26 | __be16 voc_ppb; |
27 | } __attribute__((__packed__)); |
28 | |
29 | struct ams_iaqcore_data { |
30 | struct i2c_client *client; |
31 | struct mutex lock; |
32 | unsigned long last_update; |
33 | |
34 | struct ams_iaqcore_reading buffer; |
35 | }; |
36 | |
37 | static const struct iio_chan_spec ams_iaqcore_channels[] = { |
38 | { |
39 | .type = IIO_CONCENTRATION, |
40 | .channel2 = IIO_MOD_CO2, |
41 | .modified = 1, |
42 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
43 | .address = AMS_IAQCORE_VOC_CO2_IDX, |
44 | }, |
45 | { |
46 | .type = IIO_RESISTANCE, |
47 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
48 | .address = AMS_IAQCORE_VOC_RESISTANCE_IDX, |
49 | }, |
50 | { |
51 | .type = IIO_CONCENTRATION, |
52 | .channel2 = IIO_MOD_VOC, |
53 | .modified = 1, |
54 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
55 | .address = AMS_IAQCORE_VOC_TVOC_IDX, |
56 | }, |
57 | }; |
58 | |
59 | static int ams_iaqcore_read_measurement(struct ams_iaqcore_data *data) |
60 | { |
61 | struct i2c_client *client = data->client; |
62 | int ret; |
63 | |
64 | struct i2c_msg msg = { |
65 | .addr = client->addr, |
66 | .flags = client->flags | I2C_M_RD, |
67 | .len = AMS_IAQCORE_DATA_SIZE, |
68 | .buf = (char *) &data->buffer, |
69 | }; |
70 | |
71 | ret = i2c_transfer(adap: client->adapter, msgs: &msg, num: 1); |
72 | |
73 | return (ret == AMS_IAQCORE_DATA_SIZE) ? 0 : ret; |
74 | } |
75 | |
76 | static int ams_iaqcore_get_measurement(struct ams_iaqcore_data *data) |
77 | { |
78 | int ret; |
79 | |
80 | /* sensor can only be polled once a second max per datasheet */ |
81 | if (!time_after(jiffies, data->last_update + HZ)) |
82 | return 0; |
83 | |
84 | ret = ams_iaqcore_read_measurement(data); |
85 | if (ret < 0) |
86 | return ret; |
87 | |
88 | data->last_update = jiffies; |
89 | |
90 | return 0; |
91 | } |
92 | |
93 | static int ams_iaqcore_read_raw(struct iio_dev *indio_dev, |
94 | struct iio_chan_spec const *chan, int *val, |
95 | int *val2, long mask) |
96 | { |
97 | struct ams_iaqcore_data *data = iio_priv(indio_dev); |
98 | int ret; |
99 | |
100 | if (mask != IIO_CHAN_INFO_PROCESSED) |
101 | return -EINVAL; |
102 | |
103 | mutex_lock(&data->lock); |
104 | ret = ams_iaqcore_get_measurement(data); |
105 | |
106 | if (ret) |
107 | goto err_out; |
108 | |
109 | switch (chan->address) { |
110 | case AMS_IAQCORE_VOC_CO2_IDX: |
111 | *val = 0; |
112 | *val2 = be16_to_cpu(data->buffer.co2_ppm); |
113 | ret = IIO_VAL_INT_PLUS_MICRO; |
114 | break; |
115 | case AMS_IAQCORE_VOC_RESISTANCE_IDX: |
116 | *val = be32_to_cpu(data->buffer.resistance); |
117 | ret = IIO_VAL_INT; |
118 | break; |
119 | case AMS_IAQCORE_VOC_TVOC_IDX: |
120 | *val = 0; |
121 | *val2 = be16_to_cpu(data->buffer.voc_ppb); |
122 | ret = IIO_VAL_INT_PLUS_NANO; |
123 | break; |
124 | default: |
125 | ret = -EINVAL; |
126 | } |
127 | |
128 | err_out: |
129 | mutex_unlock(lock: &data->lock); |
130 | |
131 | return ret; |
132 | } |
133 | |
134 | static const struct iio_info ams_iaqcore_info = { |
135 | .read_raw = ams_iaqcore_read_raw, |
136 | }; |
137 | |
138 | static int ams_iaqcore_probe(struct i2c_client *client) |
139 | { |
140 | struct iio_dev *indio_dev; |
141 | struct ams_iaqcore_data *data; |
142 | |
143 | indio_dev = devm_iio_device_alloc(parent: &client->dev, sizeof_priv: sizeof(*data)); |
144 | if (!indio_dev) |
145 | return -ENOMEM; |
146 | |
147 | data = iio_priv(indio_dev); |
148 | i2c_set_clientdata(client, data: indio_dev); |
149 | data->client = client; |
150 | |
151 | /* so initial reading will complete */ |
152 | data->last_update = jiffies - HZ; |
153 | mutex_init(&data->lock); |
154 | |
155 | indio_dev->info = &ams_iaqcore_info; |
156 | indio_dev->name = dev_name(dev: &client->dev); |
157 | indio_dev->modes = INDIO_DIRECT_MODE; |
158 | |
159 | indio_dev->channels = ams_iaqcore_channels; |
160 | indio_dev->num_channels = ARRAY_SIZE(ams_iaqcore_channels); |
161 | |
162 | return devm_iio_device_register(&client->dev, indio_dev); |
163 | } |
164 | |
165 | static const struct i2c_device_id ams_iaqcore_id[] = { |
166 | { "ams-iaq-core" , 0 }, |
167 | { } |
168 | }; |
169 | MODULE_DEVICE_TABLE(i2c, ams_iaqcore_id); |
170 | |
171 | static const struct of_device_id ams_iaqcore_dt_ids[] = { |
172 | { .compatible = "ams,iaq-core" }, |
173 | { } |
174 | }; |
175 | MODULE_DEVICE_TABLE(of, ams_iaqcore_dt_ids); |
176 | |
177 | static struct i2c_driver ams_iaqcore_driver = { |
178 | .driver = { |
179 | .name = "ams-iaq-core" , |
180 | .of_match_table = ams_iaqcore_dt_ids, |
181 | }, |
182 | .probe = ams_iaqcore_probe, |
183 | .id_table = ams_iaqcore_id, |
184 | }; |
185 | module_i2c_driver(ams_iaqcore_driver); |
186 | |
187 | MODULE_AUTHOR("Matt Ranostay <matt.ranostay@konsulko.com>" ); |
188 | MODULE_DESCRIPTION("AMS iAQ-Core VOC sensors" ); |
189 | MODULE_LICENSE("GPL v2" ); |
190 | |