minix/drivers/sht21/sht21.c

534 lines
12 KiB
C
Raw Normal View History

/* Driver for the SHT21 Relative Humidity and Temperature Sensor */
#include <minix/ds.h>
#include <minix/drivers.h>
#include <minix/i2c.h>
#include <minix/i2cdriver.h>
#include <minix/chardriver.h>
#include <minix/log.h>
#include <time.h>
/*
* 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 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
};
/*
* 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;
/* Perform a soft-reset */
r = i2creg_raw_write8(bus_endpoint, address, CMD_SOFT_RESET);
if (r != OK) {
return -1;
}
/* soft reset takes up to 15 ms to complete. */
micro_delay(15000);
log_debug(&log, "Soft Reset Complete\n");
r = i2creg_read8(bus_endpoint, address, CMD_RD_USR_REG, &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;
uint16_t val;
uint8_t bytes[2];
uint32_t val32;
uint8_t expected_crc;
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;
}
r = i2creg_read24(bus_endpoint, address, cmd, &val32);
if (r != OK) {
log_warn(&log, "sensor_read() failed (r=%d)\n", r);
return -1;
}
expected_crc = val32 & 0xff;
val = (val32 >> 8) & 0xffff;
bytes[0] = (val >> 8) & 0xff;
bytes[1] = val & 0xff;
r = checksum(bytes, 2, expected_crc);
if (r != OK) {
return -1;
}
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;
}