minix/drivers/tsl2550/tsl2550.c

483 lines
11 KiB
C
Raw Normal View History

/* Driver for the TSL2550 Ambient Light 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 <minix/type.h>
#include <minix/spin.h>
/*
* Device Commands
*/
#define CMD_PWR_DOWN 0x00
#define CMD_PWR_UP 0x03
#define CMD_EXT_RANGE 0x1d
#define CMD_NORM_RANGE 0x18
#define CMD_READ_ADC0 0x43
#define CMD_READ_ADC1 0x83
/* When powered up and communicating, the register should have this value */
#define EXPECTED_PWR_UP_TEST_VAL 0x03
/* Maximum Lux value in Standard Mode */
#define MAX_LUX_STD_MODE 1846
/* Bit Masks for ADC Data */
#define ADC_VALID_MASK (1<<7)
#define ADC_CHORD_MASK ((1<<6)|(1<<5)|(1<<4))
#define ADC_STEP_MASK ((1<<3)|(1<<2)|(1<<1)|(1<<0))
#define ADC_VAL_IS_VALID(x) ((x & ADC_VALID_MASK) == ADC_VALID_MASK)
#define ADC_VAL_TO_CHORD_BITS(x) ((x & ADC_CHORD_MASK) >> 4)
#define ADC_VAL_TO_STEP_BITS(x) (x & ADC_STEP_MASK)
/* logging - use with log_warn(), log_info(), log_debug(), log_trace(), etc */
static struct log log = {
.name = "tsl2550",
.log_level = LEVEL_INFO,
.log_func = default_log
};
/* The slave address is hardwired to 0x39 and cannot be changed. */
static i2c_addr_t valid_addrs[2] = {
0x39, 0x00
};
/* Buffer to store output string returned when reading from device file. */
#define BUFFER_LEN 32
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;
/* main driver functions */
static int tsl2550_init(void);
static int adc_read(int adc, uint8_t * val);
static int measure_lux(uint32_t * lux);
/* libchardriver callbacks */
static struct device *tsl2550_prepare(dev_t UNUSED(dev));
static int tsl2550_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 tsl2550_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 tsl2550_tab = {
.cdr_open = do_nop,
.cdr_close = do_nop,
.cdr_ioctl = nop_ioctl,
.cdr_prepare = tsl2550_prepare,
.cdr_transfer = tsl2550_transfer,
.cdr_cleanup = nop_cleanup,
.cdr_alarm = nop_alarm,
.cdr_cancel = nop_cancel,
.cdr_select = nop_select,
.cdr_other = tsl2550_other
};
static struct device tsl2550_device = {
.dv_base = 0,
.dv_size = 0
};
/*
* These two lookup tables and the formulas used in measure_lux() are from
* 'TAOS INTELLIGENT OPTO SENSOR DESIGNER'S NOTEBOOK' Number 9
* 'Simplified TSL2550 Lux Calculation for Embedded and Micro Controllers'.
*
* The tables and formulas eliminate the need for floating point math and
* functions from libm. It also speeds up the calculations.
*/
/* Look up table for converting ADC values to ADC counts */
static const uint32_t adc_counts_lut[128] = {
0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
16, 18, 20, 22, 24, 26, 28, 30,
32, 34, 36, 38, 40, 42, 44, 46,
49, 53, 57, 61, 65, 69, 73, 77,
81, 85, 89, 93, 97, 101, 105, 109,
115, 123, 131, 139, 147, 155, 163, 171,
179, 187, 195, 203, 211, 219, 227, 235,
247, 263, 279, 295, 311, 327, 343, 359,
375, 391, 407, 423, 439, 455, 471, 487,
511, 543, 575, 607, 639, 671, 703, 735,
767, 799, 831, 863, 895, 927, 959, 991,
1039, 1103, 1167, 1231, 1295, 1359, 1423, 1487,
1551, 1615, 1679, 1743, 1807, 1871, 1935, 1999,
2095, 2223, 2351, 2479, 2607, 2735, 2863, 2991,
3119, 3247, 3375, 3503, 3631, 3759, 3887, 4015
};
/* Look up table of scaling factors */
static const uint32_t ratio_lut[129] = {
100, 100, 100, 100, 100, 100, 100, 100,
100, 100, 100, 100, 100, 100, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 98, 98, 98, 98, 98,
98, 98, 97, 97, 97, 97, 97, 96,
96, 96, 96, 95, 95, 95, 94, 94,
93, 93, 93, 92, 92, 91, 91, 90,
89, 89, 88, 87, 87, 86, 85, 84,
83, 82, 81, 80, 79, 78, 77, 75,
74, 73, 71, 69, 68, 66, 64, 62,
60, 58, 56, 54, 52, 49, 47, 44,
42, 41, 40, 40, 39, 39, 38, 38,
37, 37, 37, 36, 36, 36, 35, 35,
35, 35, 34, 34, 34, 34, 33, 33,
33, 33, 32, 32, 32, 32, 32, 31,
31, 31, 31, 31, 30, 30, 30, 30,
30
};
static int
measure_lux(uint32_t * lux)
{
int r;
uint8_t adc0_val, adc1_val;
uint32_t adc0_cnt, adc1_cnt;
uint32_t ratio;
r = adc_read(0, &adc0_val);
if (r != OK) {
return -1;
}
r = adc_read(1, &adc1_val);
if (r != OK) {
return -1;
}
/* Look up the adc count, drop the MSB to put in range 0-127. */
adc0_cnt = adc_counts_lut[adc0_val & ~ADC_VALID_MASK];
adc1_cnt = adc_counts_lut[adc1_val & ~ADC_VALID_MASK];
/* default scaling factor */
ratio = 128;
/* calculate ratio - avoid div by 0, ensure cnt1 <= cnt0 */
if ((adc0_cnt != 0) && (adc1_cnt <= adc0_cnt)) {
ratio = (adc1_cnt * 128 / adc0_cnt);
}
/* ensure ratio isn't outside ratio_lut[] */
if (ratio > 128) {
ratio = 128;
}
/* calculate lux */
*lux = ((adc0_cnt - adc1_cnt) * ratio_lut[ratio]) / 256;
/* range check */
if (*lux > MAX_LUX_STD_MODE) {
*lux = MAX_LUX_STD_MODE;
}
return OK;
}
static int
adc_read(int adc, uint8_t * val)
{
int r;
spin_t spin;
if (adc != 0 && adc != 1) {
log_warn(&log, "Invalid ADC number %d, expected 0 or 1.\n",
adc);
return EINVAL;
}
if (val == NULL) {
log_warn(&log, "Read called with a NULL pointer.\n");
return EINVAL;
}
*val = (adc == 0) ? CMD_READ_ADC0 : CMD_READ_ADC1;
/* Select the ADC to read from */
r = i2creg_raw_write8(bus_endpoint, address, *val);
if (r != OK) {
log_warn(&log, "Failed to write ADC read command.\n");
return -1;
}
*val = 0;
/* Repeatedly read until the value is valid (i.e. the conversion
* finishes). Depending on the timing, the data sheet says this
* could take up to 400ms.
*/
spin_init(&spin, 400000);
do {
r = i2creg_raw_read8(bus_endpoint, address, val);
if (r != OK) {
log_warn(&log, "Failed to read ADC%d value.\n", adc);
return -1;
}
if (ADC_VAL_IS_VALID(*val)) {
return OK;
}
} while (spin_check(&spin));
/* Final read attempt. If the bus was really busy with other requests
* and the timing of things happened in the worst possible case,
* there is a chance that the loop above only did 1 read (slightly
* before 400 ms) and left the loop. To ensure there is a final read
* at or after the 400 ms mark, we try one last time here.
*/
r = i2creg_raw_read8(bus_endpoint, address, val);
if (r != OK) {
log_warn(&log, "Failed to read ADC%d value.\n", adc);
return -1;
}
if (ADC_VAL_IS_VALID(*val)) {
return OK;
} else {
log_warn(&log, "ADC%d never returned a valid result.\n", adc);
return EIO;
}
}
static int
tsl2550_init(void)
{
int r;
uint8_t val;
/* Power on the device */
r = i2creg_raw_write8(bus_endpoint, address, CMD_PWR_UP);
if (r != OK) {
log_warn(&log, "Power-up command failed.\n");
return -1;
}
/* Read power on test value */
r = i2creg_raw_read8(bus_endpoint, address, &val);
if (r != OK) {
log_warn(&log, "Failed to read power on test value.\n");
return -1;
}
/* Check power on test value */
if (val != EXPECTED_PWR_UP_TEST_VAL) {
log_warn(&log, "Bad test value. Got 0x%x, expected 0x%x\n",
val, EXPECTED_PWR_UP_TEST_VAL);
return -1;
}
/* Set range to normal */
r = i2creg_raw_write8(bus_endpoint, address, CMD_NORM_RANGE);
if (r != OK) {
log_warn(&log, "Normal range command failed.\n");
return -1;
}
return OK;
}
static struct device *
tsl2550_prepare(dev_t UNUSED(dev))
{
return &tsl2550_device;
}
static int
tsl2550_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;
uint32_t lux;
r = measure_lux(&lux);
if (r != OK) {
return EIO;
}
memset(buffer, '\0', BUFFER_LEN + 1);
snprintf(buffer, BUFFER_LEN, "%-16s: %d\n", "ILLUMINANCE", lux);
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
tsl2550_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 = tsl2550_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=0x39'\n");
return EXIT_FAILURE;
} else if (r > 0) {
log_warn(&log,
"Invalid slave address for device, expecting 0x39\n");
return EXIT_FAILURE;
}
sef_local_startup();
chardriver_task(&tsl2550_tab, CHARDRIVER_SYNC);
return 0;
}