From 845aabfe6528684d732b02c973449c099ad1fbee Mon Sep 17 00:00:00 2001 From: Thomas Cort Date: Mon, 19 Aug 2013 06:57:27 -0400 Subject: [PATCH] tsl2550: driver for the TSL2550 light sensor Change-Id: I9e1c87132404509ffec8bf22a8c6cc993df1aa73 --- commands/MAKEDEV/MAKEDEV.sh | 10 +- distrib/sets/lists/minix/md.evbarm | 1 + drivers/Makefile | 2 +- drivers/tsl2550/Makefile | 14 + drivers/tsl2550/README.txt | 46 +++ drivers/tsl2550/tsl2550.c | 567 +++++++++++++++++++++++++++++ etc/system.conf | 5 + include/minix/dmap.h | 4 + 8 files changed, 647 insertions(+), 2 deletions(-) create mode 100644 drivers/tsl2550/Makefile create mode 100644 drivers/tsl2550/README.txt create mode 100644 drivers/tsl2550/tsl2550.c diff --git a/commands/MAKEDEV/MAKEDEV.sh b/commands/MAKEDEV/MAKEDEV.sh index e5eecf5d9..5322a87cb 100644 --- a/commands/MAKEDEV/MAKEDEV.sh +++ b/commands/MAKEDEV/MAKEDEV.sh @@ -30,7 +30,8 @@ case $#:$1 in eepromb2s50 eepromb2s51 eepromb2s52 eepromb2s53 \ eepromb2s54 eepromb2s55 eepromb2s56 eepromb2s57 \ eepromb3s50 eepromb3s51 eepromb3s52 eepromb3s53 \ - eepromb3s54 eepromb3s55 eepromb3s56 eepromb3s57 + eepromb3s54 eepromb3s55 eepromb3s56 eepromb3s57 \ + tsl2550b1s39 tsl2550b2s39 tsl2550b3s39 ;; 0:|1:-\?) cat >&2 <&2 ex=1 diff --git a/distrib/sets/lists/minix/md.evbarm b/distrib/sets/lists/minix/md.evbarm index bcd293634..679cdce36 100644 --- a/distrib/sets/lists/minix/md.evbarm +++ b/distrib/sets/lists/minix/md.evbarm @@ -114,5 +114,6 @@ ./usr/sbin/tda19988 minix-sys ./usr/sbin/tps65217 minix-sys ./usr/sbin/tps65950 minix-sys +./usr/sbin/tsl2550 minix-sys ./usr/tests/minix-posix/mod minix-sys ./usr/tests/minix-posix/test63 minix-sys diff --git a/drivers/Makefile b/drivers/Makefile index 6c9f811ef..6f6e220b0 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 tty random + tda19988 tps65217 tps65950 tsl2550 tty random .endif .endif # ${MKIMAGEONLY} != "yes" diff --git a/drivers/tsl2550/Makefile b/drivers/tsl2550/Makefile new file mode 100644 index 000000000..19896dc45 --- /dev/null +++ b/drivers/tsl2550/Makefile @@ -0,0 +1,14 @@ +# Makefile for the tsl2550 ambient light sensor found on the Weather Cape. +PROG= tsl2550 +SRCS= tsl2550.c + +DPADD+= ${LIBI2CDRIVER} ${LIBCHARDRIVER} ${LIBSYS} ${LIBTIMERS} +LDADD+= -li2cdriver -lchardriver -lsys -ltimers + +MAN= + +BINDIR?= /usr/sbin + +CPPFLAGS+= -I${NETBSDSRCDIR} + +.include diff --git a/drivers/tsl2550/README.txt b/drivers/tsl2550/README.txt new file mode 100644 index 000000000..a7bb7576f --- /dev/null +++ b/drivers/tsl2550/README.txt @@ -0,0 +1,46 @@ +TSL2550 Driver (Ambient Light Sensor) +===================================== + +Overview +-------- + +This is the driver for the ambient light sensor commonly found on the +WeatherCape expansion board for the BeagleBone. + +Interface +--------- + +This driver implements the character device interface. It supports reading +through /dev/tsl2550b{1,3}s39. When read from, it returns a string containing +a data label, a colon, and the sensor value. + +Example output of `cat /dev/tsl2550b3s39`: + +ILLUMINANCE : 830 + +Illuminance is expressed in lux. Valid values are 0 to 1846. + +Limitations +----------- + +Extended mode isn't implemented. Normal mode should be sufficient for most +applications. + +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 tsl2550b3s39 +/bin/service up /usr/sbin/tsl2550 -label tsl2550.3.39 -dev /dev/tsl2550b3s39 \ + -args 'bus=3 address=0x39' + +Getting the sensor value: + +cat /dev/tsl2550b3s39 + +Killing an instance: + +/bin/service down tsl2550.3.39 + diff --git a/drivers/tsl2550/tsl2550.c b/drivers/tsl2550/tsl2550.c new file mode 100644 index 000000000..06cc31d7c --- /dev/null +++ b/drivers/tsl2550/tsl2550.c @@ -0,0 +1,567 @@ +/* Driver for the TSL2550 Ambient Light Sensor */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * 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; + +/* register access functions */ +static int reg_read(uint8_t * val); +static int reg_write(uint8_t val); + +/* 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 = reg_write(*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 = reg_read(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 = reg_read(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 +reg_read(uint8_t * val) +{ + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + if (val == NULL) { + log_warn(&log, "Read called with a NULL pointer.\n"); + return EINVAL; + } + + 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; + + /* No register address to write */ + ioctl_exec.iie_cmdlen = 0; + + /* Read one byte */ + ioctl_exec.iie_buflen = 1; + + r = i2cdriver_exec(bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "reg_read() failed (r=%d)\n", r); + return -1; + } + + *val = ioctl_exec.iie_buf[0]; + + log_trace(&log, "Read 0x%x from reg\n", *val); + + return OK; +} + +static int +reg_write(uint8_t val) +{ + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + switch (val) { + case CMD_PWR_DOWN: + case CMD_PWR_UP: + case CMD_EXT_RANGE: + case CMD_NORM_RANGE: + case CMD_READ_ADC0: + case CMD_READ_ADC1: + /* Command is valid */ + break; + default: + log_warn(&log, + "reg_write() called with invalid command 0x%x\n", val); + return EINVAL; + } + + 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] = val; + ioctl_exec.iie_buflen = 1; + + r = i2cdriver_exec(bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "reg_write() failed (r=%d)\n", r); + return -1; + } + + log_trace(&log, "Wrote 0x%x to reg\n", val); + + return OK; +} + +static int +tsl2550_init(void) +{ + int r; + uint8_t val; + + /* Power on the device */ + r = reg_write(CMD_PWR_UP); + if (r != OK) { + log_warn(&log, "Power-up command failed.\n"); + return -1; + } + + /* Read power on test value */ + r = reg_read(&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 = reg_write(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; +} diff --git a/etc/system.conf b/etc/system.conf index 47045888f..d37739e5a 100644 --- a/etc/system.conf +++ b/etc/system.conf @@ -636,6 +636,11 @@ service tps65950 ipc SYSTEM RS DS i2c readclock.drv; }; +service tsl2550 +{ + ipc SYSTEM RS DS i2c; +}; + service vbox { system diff --git a/include/minix/dmap.h b/include/minix/dmap.h index 5adc17cbd..73ef35c22 100644 --- a/include/minix/dmap.h +++ b/include/minix/dmap.h @@ -66,6 +66,10 @@ enum dev_style { STYLE_NDEV, STYLE_DEV, STYLE_DEVA, STYLE_TTY, STYLE_CTTY, #define EEPROMB3S55_MAJOR 44 /* 44 = /dev/eepromb3s55 (cat24c256) */ #define EEPROMB3S56_MAJOR 45 /* 45 = /dev/eepromb3s56 (cat24c256) */ #define EEPROMB3S57_MAJOR 46 /* 46 = /dev/eepromb3s57 (cat24c256) */ +#define TSL2550B1S39_MAJOR 47 /* 47 = /dev/tsl2550b1s39 (tsl2550) */ +#define TSL2550B2S39_MAJOR 48 /* 48 = /dev/tsl2550b2s39 (tsl2550) */ +#define TSL2550B3S39_MAJOR 49 /* 49 = /dev/tsl2550b3s39 (tsl2550) */ + /* Minor device numbers for memory driver. */ # define RAM_DEV_OLD 0 /* minor device for /dev/ram */