From 3bdd1ae65922bd8862a0eda249d86104b4c02e1b Mon Sep 17 00:00:00 2001 From: Thomas Cort Date: Mon, 19 Aug 2013 09:54:20 -0400 Subject: [PATCH] sht21: driver for the SHT21 humidity & temp sensor Change-Id: Ia71168e394a7b260019e74973db6c9d75d3d4482 --- commands/MAKEDEV/MAKEDEV.sh | 11 +- distrib/sets/lists/minix/md.evbarm | 1 + drivers/Makefile | 2 +- drivers/sht21/Makefile | 14 + drivers/sht21/README.txt | 67 ++++ drivers/sht21/sht21.c | 618 +++++++++++++++++++++++++++++ etc/system.conf | 5 + include/minix/dmap.h | 3 + 8 files changed, 719 insertions(+), 2 deletions(-) create mode 100644 drivers/sht21/Makefile create mode 100644 drivers/sht21/README.txt create mode 100644 drivers/sht21/sht21.c diff --git a/commands/MAKEDEV/MAKEDEV.sh b/commands/MAKEDEV/MAKEDEV.sh index 5322a87cb..df4f9a0e4 100644 --- a/commands/MAKEDEV/MAKEDEV.sh +++ b/commands/MAKEDEV/MAKEDEV.sh @@ -31,7 +31,8 @@ case $#:$1 in eepromb2s54 eepromb2s55 eepromb2s56 eepromb2s57 \ eepromb3s50 eepromb3s51 eepromb3s52 eepromb3s53 \ eepromb3s54 eepromb3s55 eepromb3s56 eepromb3s57 \ - tsl2550b1s39 tsl2550b2s39 tsl2550b3s39 + tsl2550b1s39 tsl2550b2s39 tsl2550b3s39 \ + sht21b1s40 sht21b2s40 sht21b3s40 ;; 0:|1:-\?) cat >&2 <&2 ex=1 diff --git a/distrib/sets/lists/minix/md.evbarm b/distrib/sets/lists/minix/md.evbarm index 679cdce36..d301ca958 100644 --- a/distrib/sets/lists/minix/md.evbarm +++ b/distrib/sets/lists/minix/md.evbarm @@ -111,6 +111,7 @@ ./usr/sbin/i2c minix-sys ./usr/sbin/lan8710a minix-sys ./usr/sbin/random minix-sys +./usr/sbin/sht21 minix-sys ./usr/sbin/tda19988 minix-sys ./usr/sbin/tps65217 minix-sys ./usr/sbin/tps65950 minix-sys diff --git a/drivers/Makefile b/drivers/Makefile index 6f6e220b0..05c8c131b 100644 --- a/drivers/Makefile +++ b/drivers/Makefile @@ -24,7 +24,7 @@ SUBDIR= ahci amddev atl2 at_wini audio dec21140A dp8390 dpeth \ .if ${MACHINE_ARCH} == "earm" SUBDIR= cat24c256 fb gpio i2c mmc lan8710a log readclock \ - tda19988 tps65217 tps65950 tsl2550 tty random + sht21 tda19988 tps65217 tps65950 tsl2550 tty random .endif .endif # ${MKIMAGEONLY} != "yes" diff --git a/drivers/sht21/Makefile b/drivers/sht21/Makefile new file mode 100644 index 000000000..03d248d4b --- /dev/null +++ b/drivers/sht21/Makefile @@ -0,0 +1,14 @@ +# Makefile for the sht21 humidity and temp sensor found on the Weather Cape. +PROG= sht21 +SRCS= sht21.c + +DPADD+= ${LIBI2CDRIVER} ${LIBCHARDRIVER} ${LIBSYS} ${LIBTIMERS} +LDADD+= -li2cdriver -lchardriver -lsys -ltimers + +MAN= + +BINDIR?= /usr/sbin + +CPPFLAGS+= -I${NETBSDSRCDIR} + +.include diff --git a/drivers/sht21/README.txt b/drivers/sht21/README.txt new file mode 100644 index 000000000..fd3cba305 --- /dev/null +++ b/drivers/sht21/README.txt @@ -0,0 +1,67 @@ +SHT21 Driver (Relative Humidity and Temperature Sensor) +======================================================= + +Overview +-------- + +This is the driver for the relative humidity and temperature sensor commonly +found on the WeatherCape expansion board for the BeagleBone. + +Interface +--------- + +This driver implements the character device interface. It supports reading +through /dev/sht21b{1,3}s40. When read from, it returns a string containing +a data label, a colon, and the sensor value. + +Example output of `cat /dev/sht21b3s40`: + +TEMPERATURE : 35.014 +HUMIDITY : 25.181 + +Temperature is expressed in Celsius (a.k.a. centigrade). Valid values are +-40.000 to 125.000. + +Humidity is expressed as a percentage. Valid values are 0.000 to 100.000. + +Limitations +----------- + +Intense activity causes the chip to heat up, affecting the temperature reading. +In order to prevent the chip from self-heating more than 0.1C, the sensor +values will only be read once per second. Subsequent reads within the same +second will return cached temperature and humidity values. + +The measurement resolution is configurable in the chip, but this driver just +uses the default maximum resolutions (12-bit for Humidity, 14-bit for +temperature). It could probably be implemented with an ioctl() or by passing +an argument via the service command, but it doesn't seem too useful at this +time. See the data sheet for the trade-off between faster conversion time and +lower resolution. + +In testing, the temperature sensor reported a value several degrees higher +than an indoor thermometer placed nearby. It doesn't appear to be a bug in the +driver as the Linux driver reports similar temperature. Additionally, the +BMP085 temperature sensor on the same cape reports a temperature about 2 +degrees lower than the SHT21. This could be due to heat produced by the +BeagleBone heating the cape slightly or maybe just a bad chip on the test +board. + +Testing the Code +---------------- + +The driver should have been started by a script in /etc/rc.capes/ If not, +this is how you start up an instance: + +cd /dev && MAKEDEV sht21b3s40 +/bin/service up /usr/sbin/sht21 -label sht21.3.40 -dev /dev/sht21b3s40 \ + -args 'bus=3 address=0x40' + +Getting the sensor value: + +cat /dev/sht21b3s40 + +Killing an instance: + +/bin/service down sht21.3.40 + diff --git a/drivers/sht21/sht21.c b/drivers/sht21/sht21.c new file mode 100644 index 000000000..cb9f327d2 --- /dev/null +++ b/drivers/sht21/sht21.c @@ -0,0 +1,618 @@ +/* Driver for the SHT21 Relative Humidity and Temperature Sensor */ + +#include +#include +#include +#include +#include +#include + +#include + +/* + * Device Commands + */ + +/* + * The trigger commands start a measurement. 'Hold' ties up the bus while the + * measurement is being performed while 'no hold' requires the driver to poll + * the chip until the data is ready. Hold is faster and requires less message + * passing while no hold frees up the bus while the measurement is in progress. + * The worst case conversion times are 85 ms for temperature and 29 ms for + * humidity. Typical conversion times are about 75% of the worst case times. + * + * The driver uses the 'hold' versions of the trigger commands. + */ +#define CMD_TRIG_T_HOLD 0xe3 +#define CMD_TRIG_RH_HOLD 0xe5 +#define CMD_TRIG_T_NOHOLD 0xf3 +#define CMD_TRIG_RH_NOHOLD 0xf5 + +/* Read and write the user register contents */ +#define CMD_WR_USR_REG 0xe6 +#define CMD_RD_USR_REG 0xe7 + +/* Resets the chip */ +#define CMD_SOFT_RESET 0xfe + +/* Status bits included in the measurement need to be masked in calculation */ +#define STATUS_BITS_MASK 0x0003 + +/* + * The user register has some reserved bits that the device changes over + * time. The driver must preserve the value of those bits when writing to + * the user register. + */ +#define USR_REG_RESERVED_MASK ((1<<3)|(1<<4)|(1<<5)) + +/* End of Battery flag is set when the voltage drops below 2.25V. */ +#define USR_REG_EOB_MASK (1<<6) + +/* When powered up and communicating, the register should have only the + * 'Disable OTP Reload' bit set + */ +#define EXPECTED_PWR_UP_TEST_VAL (1<<1) + +/* Define some constants for the different sensor types on the chip. */ +enum sht21_sensors +{ SHT21_T, SHT21_RH }; + +/* logging - use with log_warn(), log_info(), log_debug(), log_trace(), etc */ +static struct log log = { + .name = "sht21", + .log_level = LEVEL_INFO, + .log_func = default_log +}; + +/* device slave address is fixed at 0x40 */ +static i2c_addr_t valid_addrs[2] = { + 0x40, 0x00 +}; + +/* Buffer to store output string returned when reading from device file. */ +#define BUFFER_LEN 64 +char buffer[BUFFER_LEN + 1]; + +/* the bus that this device is on (counting starting at 1) */ +static uint32_t bus; + +/* slave address of the device */ +static i2c_addr_t address; + +/* endpoint for the driver for the bus itself. */ +static endpoint_t bus_endpoint; + +/* Sampling causes self-heating. To limit the self-heating to < 0.1C, the + * data sheet suggests limiting sampling to 2 samples per second. Since + * the driver samples temperature and relative humidity at the same time, + * it's measure function does at most 1 pair of samples per second. It uses + * this timestamp to see if a measurement was taken less than 1 second ago. + */ +static time_t last_sample_time = 0; + +/* + * Cache temperature and relative humidity readings. These values are returned + * when the last_sample_time == current_time to keep the chip activity below + * 10% to help prevent self-heating. + */ +static int32_t cached_t = 0.0; +static int32_t cached_rh = 0.0; + +/* + * An 8-bit CRC is used to validate the readings. + */ +#define CRC8_POLYNOMIAL 0x131 +#define CRC8_INITIAL_CRC 0x00 + +/* main driver functions */ +static int sht21_init(void); +static int soft_reset(void); +static int usr_reg_read(uint8_t * usr_reg_val); +static int sensor_read(enum sht21_sensors sensor, int32_t * measurement); +static int measure(void); + +/* CRC functions */ +static uint8_t crc8(uint8_t crc, uint8_t byte); +static int checksum(uint8_t * bytes, int nbytes, uint8_t expected_crc); + +/* libchardriver callbacks */ +static struct device *sht21_prepare(dev_t UNUSED(dev)); +static int sht21_transfer(endpoint_t endpt, int opcode, u64_t position, + iovec_t * iov, unsigned nr_req, endpoint_t UNUSED(user_endpt), + unsigned int UNUSED(flags)); +static int sht21_other(message * m); + +/* SEF functions */ +static int sef_cb_lu_state_save(int); +static int lu_state_restore(void); +static int sef_cb_init(int type, sef_init_info_t * info); +static void sef_local_startup(void); + +/* Entry points to this driver from libchardriver. */ +static struct chardriver sht21_tab = { + .cdr_open = do_nop, + .cdr_close = do_nop, + .cdr_ioctl = nop_ioctl, + .cdr_prepare = sht21_prepare, + .cdr_transfer = sht21_transfer, + .cdr_cleanup = nop_cleanup, + .cdr_alarm = nop_alarm, + .cdr_cancel = nop_cancel, + .cdr_select = nop_select, + .cdr_other = sht21_other +}; + +static struct device sht21_device = { + .dv_base = 0, + .dv_size = 0 +}; + +/* + * Sends the chip a soft reset command and waits 15 ms for the chip to reset. + */ +static int +soft_reset(void) +{ + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Write to chip */ + ioctl_exec.iie_op = I2C_OP_WRITE_WITH_STOP; + ioctl_exec.iie_addr = address; + + /* No command bytes for writing to this chip */ + ioctl_exec.iie_cmdlen = 0; + + /* Set the byte to write */ + ioctl_exec.iie_buf[0] = CMD_SOFT_RESET; + ioctl_exec.iie_buflen = 1; + + r = i2cdriver_exec(bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "soft_reset() failed (r=%d)\n", r); + return -1; + } + + /* soft reset takes up to 15 ms to complete. */ + micro_delay(15000); + + log_debug(&log, "Soft Reset Complete\n"); + + return OK; +} + +/* + * Obtain the contents of the usr register and store it in usr_reg_val. + */ +static int +usr_reg_read(uint8_t * usr_reg_val) +{ + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + if (usr_reg_val == NULL) { + log_warn(&log, "usr_reg_read() called with NULL pointer\n"); + return -1; + } + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Read from chip */ + ioctl_exec.iie_op = I2C_OP_READ_WITH_STOP; + ioctl_exec.iie_addr = address; + + /* Send the read from user register command */ + ioctl_exec.iie_cmd[0] = CMD_RD_USR_REG; + ioctl_exec.iie_cmdlen = 1; + + /* Read the register contents into iie_buf */ + ioctl_exec.iie_buflen = 1; + + r = i2cdriver_exec(bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "usr_reg_read() failed (r=%d)\n", r); + return -1; + } + + *usr_reg_val = ioctl_exec.iie_buf[0]; + + log_trace(&log, "Read 0x%x from USR_REG\n", *usr_reg_val); + + return OK; +} + +/* + * Performs a soft reset and reads the contents of the user register to ensure + * that the chip is in a good state and working properly. + */ +static int +sht21_init(void) +{ + int r; + uint8_t usr_reg_val; + + r = soft_reset(); + if (r != OK) { + return -1; + } + + r = usr_reg_read(&usr_reg_val); + if (r != OK) { + return -1; + } + + /* Check for End of Battery flag. */ + if ((usr_reg_val & USR_REG_EOB_MASK) == USR_REG_EOB_MASK) { + log_warn(&log, "End of Battery Alarm\n"); + return -1; + } + + /* Check that the non-reserved bits are in the default state. */ + if ((usr_reg_val & ~USR_REG_RESERVED_MASK) != EXPECTED_PWR_UP_TEST_VAL) { + log_warn(&log, "USR_REG has non-default values after reset\n"); + log_warn(&log, "Expected 0x%x | Actual 0x%x", + EXPECTED_PWR_UP_TEST_VAL, + (usr_reg_val & ~USR_REG_RESERVED_MASK)); + return -1; + } + + return OK; +} + +/* + * Read from the sensor, check the CRC, convert the ADC value into the final + * representation, and store the result in measurement. + */ +static int +sensor_read(enum sht21_sensors sensor, int32_t * measurement) +{ + int r; + uint8_t cmd; + uint8_t val_hi, val_lo; + uint16_t val; + uint8_t expected_crc; + minix_i2c_ioctl_exec_t ioctl_exec; + + switch (sensor) { + case SHT21_T: + cmd = CMD_TRIG_T_HOLD; + break; + case SHT21_RH: + cmd = CMD_TRIG_RH_HOLD; + break; + default: + log_warn(&log, "sensor_read() called with bad sensor type.\n"); + return -1; + } + + if (measurement == NULL) { + log_warn(&log, "sensor_read() called with NULL pointer\n"); + return -1; + } + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Read from chip */ + ioctl_exec.iie_op = I2C_OP_READ_WITH_STOP; + ioctl_exec.iie_addr = address; + + /* Send the trigger command */ + ioctl_exec.iie_cmd[0] = cmd; + ioctl_exec.iie_cmdlen = 1; + + /* Read the results */ + ioctl_exec.iie_buflen = 3; + + r = i2cdriver_exec(bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "sensor_read() failed (r=%d)\n", r); + return -1; + } + + expected_crc = ioctl_exec.iie_buf[2]; + + r = checksum(ioctl_exec.iie_buf, 2, expected_crc); + if (r != OK) { + return -1; + } + + val_hi = ioctl_exec.iie_buf[0]; + val_lo = ioctl_exec.iie_buf[1]; + val = ((val_hi << 8) | val_lo); + + val &= ~STATUS_BITS_MASK; /* clear status bits */ + + log_debug(&log, "Read VAL:0x%x CRC:0x%x\n", val, expected_crc); + + /* Convert the ADC value to the actual value. */ + if (cmd == CMD_TRIG_T_HOLD) { + *measurement = (int32_t) + ((-46.85 + ((175.72 / 65536) * ((float) val))) * 1000.0); + log_debug(&log, "Measured Temperature %d mC\n", *measurement); + } else if (cmd == CMD_TRIG_RH_HOLD) { + *measurement = + (int32_t) ((-6.0 + + ((125.0 / 65536) * ((float) val))) * 1000.0); + log_debug(&log, "Measured Humidity %d m%%\n", *measurement); + } + + return OK; +} + +static int +measure(void) +{ + int r; + time_t sample_time; + int32_t t, rh; + + log_debug(&log, "Taking a measurement..."); + + sample_time = time(NULL); + if (sample_time == last_sample_time) { + log_debug(&log, "measure() called too soon, using cache.\n"); + return OK; + } + + r = sensor_read(SHT21_T, &t); + if (r != OK) { + return -1; + } + + r = sensor_read(SHT21_RH, &rh); + if (r != OK) { + return -1; + } + + /* save measured values */ + cached_t = t; + cached_rh = rh; + last_sample_time = time(NULL); + + log_debug(&log, "Measurement completed\n"); + + return OK; +} + +/* + * Return an updated checksum for the given crc and byte. + */ +static uint8_t +crc8(uint8_t crc, uint8_t byte) +{ + int i; + + crc ^= byte; + + for (i = 0; i < 8; i++) { + + if ((crc & 0x80) == 0x80) { + crc = (crc << 1) ^ CRC8_POLYNOMIAL; + } else { + crc <<= 1; + } + } + + return crc; +} + +/* + * Compute the CRC of an array of bytes and compare it to expected_crc. + * If the computed CRC matches expected_crc, then return OK, otherwise EINVAL. + */ +static int +checksum(uint8_t * bytes, int nbytes, uint8_t expected_crc) +{ + int i; + uint8_t crc; + + crc = CRC8_INITIAL_CRC; + + log_debug(&log, "Checking CRC\n"); + + for (i = 0; i < nbytes; i++) { + crc = crc8(crc, bytes[i]); + } + + if (crc == expected_crc) { + log_debug(&log, "CRC OK\n"); + return OK; + } else { + log_warn(&log, + "Bad CRC -- Computed CRC: 0x%x | Expected CRC: 0x%x\n", + crc, expected_crc); + return EINVAL; + } +} + +static struct device * +sht21_prepare(dev_t UNUSED(dev)) +{ + return &sht21_device; +} + +static int +sht21_transfer(endpoint_t endpt, int opcode, u64_t position, + iovec_t * iov, unsigned nr_req, endpoint_t UNUSED(user_endpt), + unsigned int UNUSED(flags)) +{ + int bytes, r; + + r = measure(); + if (r != OK) { + return EIO; + } + + memset(buffer, '\0', BUFFER_LEN + 1); + snprintf(buffer, BUFFER_LEN, "%-16s: %d.%03d\n%-16s: %d.%03d\n", + "TEMPERATURE", cached_t / 1000, cached_t % 1000, "HUMIDITY", + cached_rh / 1000, cached_rh % 1000); + + log_trace(&log, "%s", buffer); + + bytes = strlen(buffer) - position < iov->iov_size ? + strlen(buffer) - position : iov->iov_size; + + if (bytes <= 0) { + return OK; + } + + switch (opcode) { + case DEV_GATHER_S: + r = sys_safecopyto(endpt, (cp_grant_id_t) iov->iov_addr, 0, + (vir_bytes) (buffer + position), bytes); + iov->iov_size -= bytes; + break; + default: + return EINVAL; + } + + return r; +} + +static int +sht21_other(message * m) +{ + int r; + + switch (m->m_type) { + case NOTIFY_MESSAGE: + if (m->m_source == DS_PROC_NR) { + log_debug(&log, + "bus driver changed state, update endpoint\n"); + i2cdriver_handle_bus_update(&bus_endpoint, bus, + address); + } + r = OK; + break; + default: + log_warn(&log, "Invalid message type (0x%x)\n", m->m_type); + r = EINVAL; + break; + } + + return r; +} + +static int +sef_cb_lu_state_save(int UNUSED(state)) +{ + ds_publish_u32("bus", bus, DSF_OVERWRITE); + ds_publish_u32("address", address, DSF_OVERWRITE); + return OK; +} + +static int +lu_state_restore(void) +{ + /* Restore the state. */ + u32_t value; + + ds_retrieve_u32("bus", &value); + ds_delete_u32("bus"); + bus = (int) value; + + ds_retrieve_u32("address", &value); + ds_delete_u32("address"); + address = (int) value; + + return OK; +} + +static int +sef_cb_init(int type, sef_init_info_t * UNUSED(info)) +{ + int r; + + if (type == SEF_INIT_LU) { + /* Restore the state. */ + lu_state_restore(); + } + + /* look-up the endpoint for the bus driver */ + bus_endpoint = i2cdriver_bus_endpoint(bus); + if (bus_endpoint == 0) { + log_warn(&log, "Couldn't find bus driver.\n"); + return EXIT_FAILURE; + } + + /* claim the device */ + r = i2cdriver_reserve_device(bus_endpoint, address); + if (r != OK) { + log_warn(&log, "Couldn't reserve device 0x%x (r=%d)\n", + address, r); + return EXIT_FAILURE; + } + + r = sht21_init(); + if (r != OK) { + log_warn(&log, "Device Init Failed\n"); + return EXIT_FAILURE; + } + + if (type != SEF_INIT_LU) { + + /* sign up for updates about the i2c bus going down/up */ + r = i2cdriver_subscribe_bus_updates(bus); + if (r != OK) { + log_warn(&log, "Couldn't subscribe to bus updates\n"); + return EXIT_FAILURE; + } + + i2cdriver_announce(bus); + log_debug(&log, "announced\n"); + } + + return OK; +} + +static void +sef_local_startup(void) +{ + /* + * Register init callbacks. Use the same function for all event types + */ + sef_setcb_init_fresh(sef_cb_init); + sef_setcb_init_lu(sef_cb_init); + sef_setcb_init_restart(sef_cb_init); + + /* + * Register live update callbacks. + */ + /* Agree to update immediately when LU is requested in a valid state. */ + sef_setcb_lu_prepare(sef_cb_lu_prepare_always_ready); + /* Support live update starting from any standard state. */ + sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid_standard); + /* Register a custom routine to save the state. */ + sef_setcb_lu_state_save(sef_cb_lu_state_save); + + /* Let SEF perform startup. */ + sef_startup(); +} + +int +main(int argc, char *argv[]) +{ + int r; + + env_setargs(argc, argv); + + r = i2cdriver_env_parse(&bus, &address, valid_addrs); + if (r < 0) { + log_warn(&log, "Expecting -args 'bus=X address=0xYY'\n"); + log_warn(&log, "Example -args 'bus=1 address=0x40'\n"); + return EXIT_FAILURE; + } else if (r > 0) { + log_warn(&log, + "Invalid slave address for device, expecting 0x40\n"); + return EXIT_FAILURE; + } + + sef_local_startup(); + + chardriver_task(&sht21_tab, CHARDRIVER_SYNC); + + return 0; +} diff --git a/etc/system.conf b/etc/system.conf index d37739e5a..e46d183eb 100644 --- a/etc/system.conf +++ b/etc/system.conf @@ -641,6 +641,11 @@ service tsl2550 ipc SYSTEM RS DS i2c; }; +service sht21 +{ + ipc SYSTEM RS DS i2c; +}; + service vbox { system diff --git a/include/minix/dmap.h b/include/minix/dmap.h index 73ef35c22..9a1c2f5a3 100644 --- a/include/minix/dmap.h +++ b/include/minix/dmap.h @@ -69,6 +69,9 @@ enum dev_style { STYLE_NDEV, STYLE_DEV, STYLE_DEVA, STYLE_TTY, STYLE_CTTY, #define TSL2550B1S39_MAJOR 47 /* 47 = /dev/tsl2550b1s39 (tsl2550) */ #define TSL2550B2S39_MAJOR 48 /* 48 = /dev/tsl2550b2s39 (tsl2550) */ #define TSL2550B3S39_MAJOR 49 /* 49 = /dev/tsl2550b3s39 (tsl2550) */ +#define SHT21B1S40_MAJOR 50 /* 50 = /dev/sht21b1s40 (sht21) */ +#define SHT21B2S40_MAJOR 51 /* 51 = /dev/sht21b2s40 (sht21) */ +#define SHT21B3S40_MAJOR 52 /* 52 = /dev/sht21b3s40 (sht21) */ /* Minor device numbers for memory driver. */ -- 2.44.0