1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * sgp30.c - Support for Sensirion SGP Gas Sensors |
4 | * |
5 | * Copyright (C) 2018 Andreas Brauchli <andreas.brauchli@sensirion.com> |
6 | * |
7 | * I2C slave address: 0x58 |
8 | * |
9 | * Datasheets: |
10 | * https://www.sensirion.com/file/datasheet_sgp30 |
11 | * https://www.sensirion.com/file/datasheet_sgpc3 |
12 | * |
13 | * TODO: |
14 | * - baseline support |
15 | * - humidity compensation |
16 | * - power mode switching (SGPC3) |
17 | */ |
18 | |
19 | #include <linux/crc8.h> |
20 | #include <linux/delay.h> |
21 | #include <linux/kthread.h> |
22 | #include <linux/module.h> |
23 | #include <linux/mod_devicetable.h> |
24 | #include <linux/mutex.h> |
25 | #include <linux/i2c.h> |
26 | #include <linux/iio/iio.h> |
27 | #include <linux/iio/sysfs.h> |
28 | |
29 | #define SGP_WORD_LEN 2 |
30 | #define SGP_CRC8_POLYNOMIAL 0x31 |
31 | #define SGP_CRC8_INIT 0xff |
32 | #define SGP_CRC8_LEN 1 |
33 | #define SGP_CMD(cmd_word) cpu_to_be16(cmd_word) |
34 | #define SGP_CMD_DURATION_US 12000 |
35 | #define SGP_MEASUREMENT_DURATION_US 50000 |
36 | #define SGP_CMD_LEN SGP_WORD_LEN |
37 | #define SGP_CMD_MAX_BUF_SIZE (SGP_CMD_LEN + 2 * SGP_WORD_LEN) |
38 | #define SGP_MEASUREMENT_LEN 2 |
39 | #define SGP30_MEASURE_INTERVAL_HZ 1 |
40 | #define SGPC3_MEASURE_INTERVAL_HZ 2 |
41 | #define SGP_VERS_PRODUCT(data) ((((data)->feature_set) & 0xf000) >> 12) |
42 | #define SGP_VERS_RESERVED(data) ((((data)->feature_set) & 0x0800) >> 11) |
43 | #define SGP_VERS_GEN(data) ((((data)->feature_set) & 0x0600) >> 9) |
44 | #define SGP_VERS_ENG_BIT(data) ((((data)->feature_set) & 0x0100) >> 8) |
45 | #define SGP_VERS_MAJOR(data) ((((data)->feature_set) & 0x00e0) >> 5) |
46 | #define SGP_VERS_MINOR(data) (((data)->feature_set) & 0x001f) |
47 | |
48 | DECLARE_CRC8_TABLE(sgp_crc8_table); |
49 | |
50 | enum sgp_product_id { |
51 | SGP30 = 0, |
52 | SGPC3, |
53 | }; |
54 | |
55 | enum sgp30_channel_idx { |
56 | SGP30_IAQ_TVOC_IDX = 0, |
57 | SGP30_IAQ_CO2EQ_IDX, |
58 | SGP30_SIG_ETOH_IDX, |
59 | SGP30_SIG_H2_IDX, |
60 | }; |
61 | |
62 | enum sgpc3_channel_idx { |
63 | SGPC3_IAQ_TVOC_IDX = 10, |
64 | SGPC3_SIG_ETOH_IDX, |
65 | }; |
66 | |
67 | enum sgp_cmd { |
68 | SGP_CMD_IAQ_INIT = SGP_CMD(0x2003), |
69 | SGP_CMD_IAQ_MEASURE = SGP_CMD(0x2008), |
70 | SGP_CMD_GET_FEATURE_SET = SGP_CMD(0x202f), |
71 | SGP_CMD_GET_SERIAL_ID = SGP_CMD(0x3682), |
72 | |
73 | SGP30_CMD_MEASURE_SIGNAL = SGP_CMD(0x2050), |
74 | |
75 | SGPC3_CMD_MEASURE_RAW = SGP_CMD(0x2046), |
76 | }; |
77 | |
78 | struct sgp_version { |
79 | u8 major; |
80 | u8 minor; |
81 | }; |
82 | |
83 | struct sgp_crc_word { |
84 | __be16 value; |
85 | u8 crc8; |
86 | } __attribute__((__packed__)); |
87 | |
88 | union sgp_reading { |
89 | u8 start; |
90 | struct sgp_crc_word raw_words[4]; |
91 | }; |
92 | |
93 | enum _iaq_buffer_state { |
94 | IAQ_BUFFER_EMPTY = 0, |
95 | IAQ_BUFFER_DEFAULT_VALS, |
96 | IAQ_BUFFER_VALID, |
97 | }; |
98 | |
99 | struct sgp_data { |
100 | struct i2c_client *client; |
101 | struct task_struct *iaq_thread; |
102 | struct mutex data_lock; |
103 | unsigned long iaq_init_start_jiffies; |
104 | unsigned long iaq_defval_skip_jiffies; |
105 | u16 product_id; |
106 | u16 feature_set; |
107 | unsigned long measure_interval_jiffies; |
108 | enum sgp_cmd iaq_init_cmd; |
109 | enum sgp_cmd measure_iaq_cmd; |
110 | enum sgp_cmd measure_gas_signals_cmd; |
111 | union sgp_reading buffer; |
112 | union sgp_reading iaq_buffer; |
113 | enum _iaq_buffer_state iaq_buffer_state; |
114 | }; |
115 | |
116 | struct sgp_device { |
117 | unsigned long product_id; |
118 | const struct iio_chan_spec *channels; |
119 | int num_channels; |
120 | }; |
121 | |
122 | static const struct sgp_version supported_versions_sgp30[] = { |
123 | { |
124 | .major = 1, |
125 | .minor = 0, |
126 | }, |
127 | }; |
128 | |
129 | static const struct sgp_version supported_versions_sgpc3[] = { |
130 | { |
131 | .major = 0, |
132 | .minor = 4, |
133 | }, |
134 | }; |
135 | |
136 | static const struct iio_chan_spec sgp30_channels[] = { |
137 | { |
138 | .type = IIO_CONCENTRATION, |
139 | .channel2 = IIO_MOD_VOC, |
140 | .modified = 1, |
141 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
142 | .address = SGP30_IAQ_TVOC_IDX, |
143 | }, |
144 | { |
145 | .type = IIO_CONCENTRATION, |
146 | .channel2 = IIO_MOD_CO2, |
147 | .modified = 1, |
148 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
149 | .address = SGP30_IAQ_CO2EQ_IDX, |
150 | }, |
151 | { |
152 | .type = IIO_CONCENTRATION, |
153 | .channel2 = IIO_MOD_ETHANOL, |
154 | .modified = 1, |
155 | .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), |
156 | .address = SGP30_SIG_ETOH_IDX, |
157 | }, |
158 | { |
159 | .type = IIO_CONCENTRATION, |
160 | .channel2 = IIO_MOD_H2, |
161 | .modified = 1, |
162 | .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), |
163 | .address = SGP30_SIG_H2_IDX, |
164 | }, |
165 | }; |
166 | |
167 | static const struct iio_chan_spec sgpc3_channels[] = { |
168 | { |
169 | .type = IIO_CONCENTRATION, |
170 | .channel2 = IIO_MOD_VOC, |
171 | .modified = 1, |
172 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
173 | .address = SGPC3_IAQ_TVOC_IDX, |
174 | }, |
175 | { |
176 | .type = IIO_CONCENTRATION, |
177 | .channel2 = IIO_MOD_ETHANOL, |
178 | .modified = 1, |
179 | .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), |
180 | .address = SGPC3_SIG_ETOH_IDX, |
181 | }, |
182 | }; |
183 | |
184 | static const struct sgp_device sgp_devices[] = { |
185 | [SGP30] = { |
186 | .product_id = SGP30, |
187 | .channels = sgp30_channels, |
188 | .num_channels = ARRAY_SIZE(sgp30_channels), |
189 | }, |
190 | [SGPC3] = { |
191 | .product_id = SGPC3, |
192 | .channels = sgpc3_channels, |
193 | .num_channels = ARRAY_SIZE(sgpc3_channels), |
194 | }, |
195 | }; |
196 | |
197 | /** |
198 | * sgp_verify_buffer() - verify the checksums of the data buffer words |
199 | * |
200 | * @data: SGP data |
201 | * @buf: Raw data buffer |
202 | * @word_count: Num data words stored in the buffer, excluding CRC bytes |
203 | * |
204 | * Return: 0 on success, negative error otherwise. |
205 | */ |
206 | static int sgp_verify_buffer(const struct sgp_data *data, |
207 | union sgp_reading *buf, size_t word_count) |
208 | { |
209 | size_t size = word_count * (SGP_WORD_LEN + SGP_CRC8_LEN); |
210 | int i; |
211 | u8 crc; |
212 | u8 *data_buf = &buf->start; |
213 | |
214 | for (i = 0; i < size; i += SGP_WORD_LEN + SGP_CRC8_LEN) { |
215 | crc = crc8(table: sgp_crc8_table, pdata: &data_buf[i], SGP_WORD_LEN, |
216 | SGP_CRC8_INIT); |
217 | if (crc != data_buf[i + SGP_WORD_LEN]) { |
218 | dev_err(&data->client->dev, "CRC error\n" ); |
219 | return -EIO; |
220 | } |
221 | } |
222 | |
223 | return 0; |
224 | } |
225 | |
226 | /** |
227 | * sgp_read_cmd() - reads data from sensor after issuing a command |
228 | * The caller must hold data->data_lock for the duration of the call. |
229 | * @data: SGP data |
230 | * @cmd: SGP Command to issue |
231 | * @buf: Raw data buffer to use |
232 | * @word_count: Num words to read, excluding CRC bytes |
233 | * @duration_us: Time taken to sensor to take a reading and data to be ready. |
234 | * |
235 | * Return: 0 on success, negative error otherwise. |
236 | */ |
237 | static int sgp_read_cmd(struct sgp_data *data, enum sgp_cmd cmd, |
238 | union sgp_reading *buf, size_t word_count, |
239 | unsigned long duration_us) |
240 | { |
241 | int ret; |
242 | struct i2c_client *client = data->client; |
243 | size_t size = word_count * (SGP_WORD_LEN + SGP_CRC8_LEN); |
244 | u8 *data_buf; |
245 | |
246 | ret = i2c_master_send(client, buf: (const char *)&cmd, SGP_CMD_LEN); |
247 | if (ret != SGP_CMD_LEN) |
248 | return -EIO; |
249 | usleep_range(min: duration_us, max: duration_us + 1000); |
250 | |
251 | if (word_count == 0) |
252 | return 0; |
253 | |
254 | data_buf = &buf->start; |
255 | ret = i2c_master_recv(client, buf: data_buf, count: size); |
256 | if (ret < 0) |
257 | return ret; |
258 | if (ret != size) |
259 | return -EIO; |
260 | |
261 | return sgp_verify_buffer(data, buf, word_count); |
262 | } |
263 | |
264 | /** |
265 | * sgp_measure_iaq() - measure and retrieve IAQ values from sensor |
266 | * The caller must hold data->data_lock for the duration of the call. |
267 | * @data: SGP data |
268 | * |
269 | * Return: 0 on success, -EBUSY on default values, negative error |
270 | * otherwise. |
271 | */ |
272 | |
273 | static int sgp_measure_iaq(struct sgp_data *data) |
274 | { |
275 | int ret; |
276 | /* data contains default values */ |
277 | bool default_vals = !time_after(jiffies, data->iaq_init_start_jiffies + |
278 | data->iaq_defval_skip_jiffies); |
279 | |
280 | ret = sgp_read_cmd(data, cmd: data->measure_iaq_cmd, buf: &data->iaq_buffer, |
281 | SGP_MEASUREMENT_LEN, SGP_MEASUREMENT_DURATION_US); |
282 | if (ret < 0) |
283 | return ret; |
284 | |
285 | data->iaq_buffer_state = IAQ_BUFFER_DEFAULT_VALS; |
286 | |
287 | if (default_vals) |
288 | return -EBUSY; |
289 | |
290 | data->iaq_buffer_state = IAQ_BUFFER_VALID; |
291 | |
292 | return 0; |
293 | } |
294 | |
295 | static void sgp_iaq_thread_sleep_until(const struct sgp_data *data, |
296 | unsigned long sleep_jiffies) |
297 | { |
298 | const long IAQ_POLL = 50000; |
299 | |
300 | while (!time_after(jiffies, sleep_jiffies)) { |
301 | usleep_range(min: IAQ_POLL, max: IAQ_POLL + 10000); |
302 | if (kthread_should_stop() || data->iaq_init_start_jiffies == 0) |
303 | return; |
304 | } |
305 | } |
306 | |
307 | static int sgp_iaq_threadfn(void *p) |
308 | { |
309 | struct sgp_data *data = (struct sgp_data *)p; |
310 | unsigned long next_update_jiffies; |
311 | int ret; |
312 | |
313 | while (!kthread_should_stop()) { |
314 | mutex_lock(&data->data_lock); |
315 | if (data->iaq_init_start_jiffies == 0) { |
316 | ret = sgp_read_cmd(data, cmd: data->iaq_init_cmd, NULL, word_count: 0, |
317 | SGP_CMD_DURATION_US); |
318 | if (ret < 0) |
319 | goto unlock_sleep_continue; |
320 | data->iaq_init_start_jiffies = jiffies; |
321 | } |
322 | |
323 | ret = sgp_measure_iaq(data); |
324 | if (ret && ret != -EBUSY) { |
325 | dev_warn(&data->client->dev, |
326 | "IAQ measurement error [%d]\n" , ret); |
327 | } |
328 | unlock_sleep_continue: |
329 | next_update_jiffies = jiffies + data->measure_interval_jiffies; |
330 | mutex_unlock(lock: &data->data_lock); |
331 | sgp_iaq_thread_sleep_until(data, sleep_jiffies: next_update_jiffies); |
332 | } |
333 | |
334 | return 0; |
335 | } |
336 | |
337 | static int sgp_read_raw(struct iio_dev *indio_dev, |
338 | struct iio_chan_spec const *chan, int *val, |
339 | int *val2, long mask) |
340 | { |
341 | struct sgp_data *data = iio_priv(indio_dev); |
342 | struct sgp_crc_word *words; |
343 | int ret; |
344 | |
345 | switch (mask) { |
346 | case IIO_CHAN_INFO_PROCESSED: |
347 | mutex_lock(&data->data_lock); |
348 | if (data->iaq_buffer_state != IAQ_BUFFER_VALID) { |
349 | mutex_unlock(lock: &data->data_lock); |
350 | return -EBUSY; |
351 | } |
352 | words = data->iaq_buffer.raw_words; |
353 | switch (chan->address) { |
354 | case SGP30_IAQ_TVOC_IDX: |
355 | case SGPC3_IAQ_TVOC_IDX: |
356 | *val = 0; |
357 | *val2 = be16_to_cpu(words[1].value); |
358 | ret = IIO_VAL_INT_PLUS_NANO; |
359 | break; |
360 | case SGP30_IAQ_CO2EQ_IDX: |
361 | *val = 0; |
362 | *val2 = be16_to_cpu(words[0].value); |
363 | ret = IIO_VAL_INT_PLUS_MICRO; |
364 | break; |
365 | default: |
366 | ret = -EINVAL; |
367 | break; |
368 | } |
369 | mutex_unlock(lock: &data->data_lock); |
370 | break; |
371 | case IIO_CHAN_INFO_RAW: |
372 | mutex_lock(&data->data_lock); |
373 | if (chan->address == SGPC3_SIG_ETOH_IDX) { |
374 | if (data->iaq_buffer_state == IAQ_BUFFER_EMPTY) |
375 | ret = -EBUSY; |
376 | else |
377 | ret = 0; |
378 | words = data->iaq_buffer.raw_words; |
379 | } else { |
380 | ret = sgp_read_cmd(data, cmd: data->measure_gas_signals_cmd, |
381 | buf: &data->buffer, SGP_MEASUREMENT_LEN, |
382 | SGP_MEASUREMENT_DURATION_US); |
383 | words = data->buffer.raw_words; |
384 | } |
385 | if (ret) { |
386 | mutex_unlock(lock: &data->data_lock); |
387 | return ret; |
388 | } |
389 | |
390 | switch (chan->address) { |
391 | case SGP30_SIG_ETOH_IDX: |
392 | *val = be16_to_cpu(words[1].value); |
393 | ret = IIO_VAL_INT; |
394 | break; |
395 | case SGPC3_SIG_ETOH_IDX: |
396 | case SGP30_SIG_H2_IDX: |
397 | *val = be16_to_cpu(words[0].value); |
398 | ret = IIO_VAL_INT; |
399 | break; |
400 | default: |
401 | ret = -EINVAL; |
402 | break; |
403 | } |
404 | mutex_unlock(lock: &data->data_lock); |
405 | break; |
406 | default: |
407 | return -EINVAL; |
408 | } |
409 | |
410 | return ret; |
411 | } |
412 | |
413 | static int sgp_check_compat(struct sgp_data *data, |
414 | unsigned int product_id) |
415 | { |
416 | struct device *dev = &data->client->dev; |
417 | const struct sgp_version *supported_versions; |
418 | u16 ix, num_fs; |
419 | u16 product, generation, major, minor; |
420 | |
421 | /* driver does not match product */ |
422 | generation = SGP_VERS_GEN(data); |
423 | if (generation != 0) { |
424 | dev_err(dev, |
425 | "incompatible product generation %d != 0" , generation); |
426 | return -ENODEV; |
427 | } |
428 | |
429 | product = SGP_VERS_PRODUCT(data); |
430 | if (product != product_id) { |
431 | dev_err(dev, "sensor reports a different product: 0x%04x\n" , |
432 | product); |
433 | return -ENODEV; |
434 | } |
435 | |
436 | if (SGP_VERS_RESERVED(data)) |
437 | dev_warn(dev, "reserved bit is set\n" ); |
438 | |
439 | /* engineering samples are not supported: no interface guarantees */ |
440 | if (SGP_VERS_ENG_BIT(data)) |
441 | return -ENODEV; |
442 | |
443 | switch (product) { |
444 | case SGP30: |
445 | supported_versions = supported_versions_sgp30; |
446 | num_fs = ARRAY_SIZE(supported_versions_sgp30); |
447 | break; |
448 | case SGPC3: |
449 | supported_versions = supported_versions_sgpc3; |
450 | num_fs = ARRAY_SIZE(supported_versions_sgpc3); |
451 | break; |
452 | default: |
453 | return -ENODEV; |
454 | } |
455 | |
456 | major = SGP_VERS_MAJOR(data); |
457 | minor = SGP_VERS_MINOR(data); |
458 | for (ix = 0; ix < num_fs; ix++) { |
459 | if (major == supported_versions[ix].major && |
460 | minor >= supported_versions[ix].minor) |
461 | return 0; |
462 | } |
463 | dev_err(dev, "unsupported sgp version: %d.%d\n" , major, minor); |
464 | |
465 | return -ENODEV; |
466 | } |
467 | |
468 | static void sgp_init(struct sgp_data *data) |
469 | { |
470 | data->iaq_init_cmd = SGP_CMD_IAQ_INIT; |
471 | data->iaq_init_start_jiffies = 0; |
472 | data->iaq_buffer_state = IAQ_BUFFER_EMPTY; |
473 | switch (SGP_VERS_PRODUCT(data)) { |
474 | case SGP30: |
475 | data->measure_interval_jiffies = SGP30_MEASURE_INTERVAL_HZ * HZ; |
476 | data->measure_iaq_cmd = SGP_CMD_IAQ_MEASURE; |
477 | data->measure_gas_signals_cmd = SGP30_CMD_MEASURE_SIGNAL; |
478 | data->product_id = SGP30; |
479 | data->iaq_defval_skip_jiffies = 15 * HZ; |
480 | break; |
481 | case SGPC3: |
482 | data->measure_interval_jiffies = SGPC3_MEASURE_INTERVAL_HZ * HZ; |
483 | data->measure_iaq_cmd = SGPC3_CMD_MEASURE_RAW; |
484 | data->measure_gas_signals_cmd = SGPC3_CMD_MEASURE_RAW; |
485 | data->product_id = SGPC3; |
486 | data->iaq_defval_skip_jiffies = |
487 | 43 * data->measure_interval_jiffies; |
488 | break; |
489 | } |
490 | } |
491 | |
492 | static const struct iio_info sgp_info = { |
493 | .read_raw = sgp_read_raw, |
494 | }; |
495 | |
496 | static const struct of_device_id sgp_dt_ids[] = { |
497 | { .compatible = "sensirion,sgp30" , .data = &sgp_devices[SGP30] }, |
498 | { .compatible = "sensirion,sgpc3" , .data = &sgp_devices[SGPC3] }, |
499 | { } |
500 | }; |
501 | |
502 | static int sgp_probe(struct i2c_client *client) |
503 | { |
504 | const struct i2c_device_id *id = i2c_client_get_device_id(client); |
505 | const struct sgp_device *match_data; |
506 | struct device *dev = &client->dev; |
507 | struct iio_dev *indio_dev; |
508 | struct sgp_data *data; |
509 | int ret; |
510 | |
511 | indio_dev = devm_iio_device_alloc(parent: dev, sizeof_priv: sizeof(*data)); |
512 | if (!indio_dev) |
513 | return -ENOMEM; |
514 | |
515 | match_data = i2c_get_match_data(client); |
516 | |
517 | data = iio_priv(indio_dev); |
518 | i2c_set_clientdata(client, data: indio_dev); |
519 | data->client = client; |
520 | crc8_populate_msb(table: sgp_crc8_table, SGP_CRC8_POLYNOMIAL); |
521 | mutex_init(&data->data_lock); |
522 | |
523 | /* get feature set version and write it to client data */ |
524 | ret = sgp_read_cmd(data, cmd: SGP_CMD_GET_FEATURE_SET, buf: &data->buffer, word_count: 1, |
525 | SGP_CMD_DURATION_US); |
526 | if (ret < 0) |
527 | return ret; |
528 | |
529 | data->feature_set = be16_to_cpu(data->buffer.raw_words[0].value); |
530 | |
531 | ret = sgp_check_compat(data, product_id: match_data->product_id); |
532 | if (ret) |
533 | return ret; |
534 | |
535 | indio_dev->info = &sgp_info; |
536 | indio_dev->name = id->name; |
537 | indio_dev->modes = INDIO_DIRECT_MODE; |
538 | indio_dev->channels = match_data->channels; |
539 | indio_dev->num_channels = match_data->num_channels; |
540 | |
541 | sgp_init(data); |
542 | |
543 | ret = devm_iio_device_register(dev, indio_dev); |
544 | if (ret) { |
545 | dev_err(dev, "failed to register iio device\n" ); |
546 | return ret; |
547 | } |
548 | |
549 | data->iaq_thread = kthread_run(sgp_iaq_threadfn, data, |
550 | "%s-iaq" , data->client->name); |
551 | |
552 | return 0; |
553 | } |
554 | |
555 | static void sgp_remove(struct i2c_client *client) |
556 | { |
557 | struct iio_dev *indio_dev = i2c_get_clientdata(client); |
558 | struct sgp_data *data = iio_priv(indio_dev); |
559 | |
560 | if (data->iaq_thread) |
561 | kthread_stop(k: data->iaq_thread); |
562 | } |
563 | |
564 | static const struct i2c_device_id sgp_id[] = { |
565 | { "sgp30" , (kernel_ulong_t)&sgp_devices[SGP30] }, |
566 | { "sgpc3" , (kernel_ulong_t)&sgp_devices[SGPC3] }, |
567 | { } |
568 | }; |
569 | |
570 | MODULE_DEVICE_TABLE(i2c, sgp_id); |
571 | MODULE_DEVICE_TABLE(of, sgp_dt_ids); |
572 | |
573 | static struct i2c_driver sgp_driver = { |
574 | .driver = { |
575 | .name = "sgp30" , |
576 | .of_match_table = sgp_dt_ids, |
577 | }, |
578 | .probe = sgp_probe, |
579 | .remove = sgp_remove, |
580 | .id_table = sgp_id, |
581 | }; |
582 | module_i2c_driver(sgp_driver); |
583 | |
584 | MODULE_AUTHOR("Andreas Brauchli <andreas.brauchli@sensirion.com>" ); |
585 | MODULE_AUTHOR("Pascal Sachs <pascal.sachs@sensirion.com>" ); |
586 | MODULE_DESCRIPTION("Sensirion SGP gas sensors" ); |
587 | MODULE_LICENSE("GPL v2" ); |
588 | |