Linux Audio

Check our new training course

Loading...
/*
 * Copyright (c) 2020 Richard Osterloh
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT vishay_vcnl4040

#include "vcnl4040.h"
#include <zephyr/pm/device.h>
#include <zephyr/sys/__assert.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
#include <stdlib.h>

LOG_MODULE_REGISTER(vcnl4040, CONFIG_SENSOR_LOG_LEVEL);

int vcnl4040_read(const struct device *dev, uint8_t reg, uint16_t *out)
{
	const struct vcnl4040_config *config = dev->config;
	struct vcnl4040_data *data = dev->data;
	uint8_t buff[2] = { 0 };
	int ret = 0;

	ret = i2c_write_read(data->i2c, config->i2c_address,
			     &reg, sizeof(reg), buff, sizeof(buff));

	if (!ret)
		*out = sys_get_le16(buff);

	return ret;
}

int vcnl4040_write(const struct device *dev, uint8_t reg, uint16_t value)
{
	const struct vcnl4040_config *config = dev->config;
	struct vcnl4040_data *data = dev->data;
	struct i2c_msg msg;
	int ret;
	uint8_t buff[3];

	sys_put_le16(value, &buff[1]);

	buff[0] = reg;

	msg.buf = buff;
	msg.flags = 0;
	msg.len = sizeof(buff);

	ret = i2c_transfer(data->i2c, &msg, 1, config->i2c_address);

	if (ret < 0) {
		LOG_ERR("write block failed");
		return ret;
	}

	return 0;
}

static int vcnl4040_sample_fetch(const struct device *dev,
				 enum sensor_channel chan)
{
	struct vcnl4040_data *data = dev->data;
	int ret = 0;

#ifdef CONFIG_VCNL4040_ENABLE_ALS
	__ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL ||
			chan == SENSOR_CHAN_PROX ||
			chan == SENSOR_CHAN_LIGHT);
#else
	__ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_PROX);
#endif
	k_sem_take(&data->sem, K_FOREVER);

	if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_PROX) {
		ret = vcnl4040_read(dev, VCNL4040_REG_PS_DATA,
				    &data->proximity);
		if (ret < 0) {
			LOG_ERR("Could not fetch proximity");
			goto exit;
		}
	}
#ifdef CONFIG_VCNL4040_ENABLE_ALS
	if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_LIGHT) {
		ret = vcnl4040_read(dev, VCNL4040_REG_ALS_DATA,
				    &data->light);
		if (ret < 0) {
			LOG_ERR("Could not fetch ambient light");
		}
	}
#endif
exit:
	k_sem_give(&data->sem);

	return ret;
}

static int vcnl4040_channel_get(const struct device *dev,
				enum sensor_channel chan,
				struct sensor_value *val)
{
	struct vcnl4040_data *data = dev->data;
	int ret = 0;

	k_sem_take(&data->sem, K_FOREVER);

	switch (chan) {
	case SENSOR_CHAN_PROX:
		val->val1 = data->proximity;
		val->val2 = 0;
		break;

#ifdef CONFIG_VCNL4040_ENABLE_ALS
	case SENSOR_CHAN_LIGHT:
		val->val1 = data->light * data->sensitivity;
		val->val2 = 0;
		break;
#endif

	default:
		ret = -ENOTSUP;
	}

	k_sem_give(&data->sem);

	return ret;
}

static int vcnl4040_proxy_setup(const struct device *dev)
{
	const struct vcnl4040_config *config = dev->config;
	uint16_t conf = 0;

	if (vcnl4040_read(dev, VCNL4040_REG_PS_MS, &conf)) {
		LOG_ERR("Could not read proximity config");
		return -EIO;
	}

	/* Set LED current */
	conf |= config->led_i << VCNL4040_LED_I_POS;

	if (vcnl4040_write(dev, VCNL4040_REG_PS_MS, conf)) {
		LOG_ERR("Could not write proximity config");
		return -EIO;
	}

	if (vcnl4040_read(dev, VCNL4040_REG_PS_CONF, &conf)) {
		LOG_ERR("Could not read proximity config");
		return -EIO;
	}

	/* Set PS_HD */
	conf |= VCNL4040_PS_HD_MASK;
	/* Set duty cycle */
	conf |= config->led_dc << VCNL4040_PS_DUTY_POS;
	/* Set integration time */
	conf |= config->proxy_it << VCNL4040_PS_IT_POS;
	/* Clear proximity shutdown */
	conf &= ~VCNL4040_PS_SD_MASK;

	if (vcnl4040_write(dev, VCNL4040_REG_PS_CONF, conf)) {
		LOG_ERR("Could not write proximity config");
		return -EIO;
	}

	return 0;
}

#ifdef CONFIG_VCNL4040_ENABLE_ALS
static int vcnl4040_ambient_setup(const struct device *dev)
{
	const struct vcnl4040_config *config = dev->config;
	struct vcnl4040_data *data = dev->data;
	uint16_t conf = 0;

	if (vcnl4040_read(dev, VCNL4040_REG_ALS_CONF, &conf)) {
		LOG_ERR("Could not read proximity config");
		return -EIO;
	}

	/* Set ALS integration time */
	conf |= config->als_it << VCNL4040_ALS_IT_POS;
	/* Clear ALS shutdown */
	conf &= ~VCNL4040_ALS_SD_MASK;

	if (vcnl4040_write(dev, VCNL4040_REG_ALS_CONF, conf)) {
		LOG_ERR("Could not write proximity config");
		return -EIO;
	}

	/*
	 * scale the lux depending on the value of the integration time
	 * see page 8 of the VCNL4040 application note:
	 * https://www.vishay.com/docs/84307/designingvcnl4040.pdf
	 */
	switch (config->als_it) {
	case VCNL4040_AMBIENT_INTEGRATION_TIME_80MS:
		data->sensitivity = 0.12;
		break;
	case VCNL4040_AMBIENT_INTEGRATION_TIME_160MS:
		data->sensitivity = 0.06;
		break;
	case VCNL4040_AMBIENT_INTEGRATION_TIME_320MS:
		data->sensitivity = 0.03;
		break;
	case VCNL4040_AMBIENT_INTEGRATION_TIME_640MS:
		data->sensitivity = 0.015;
		break;
	default:
		data->sensitivity = 1.0;
		LOG_WRN("Cannot set ALS sensitivity from ALS_IT=%d",
			config->als_it);
		break;
	}

	return 0;
}
#endif

#ifdef CONFIG_PM_DEVICE
static int vcnl4040_pm_action(const struct device *dev,
			      enum pm_device_action action)
{
	int ret = 0;
	uint16_t ps_conf;

	ret = vcnl4040_read(dev, VCNL4040_REG_PS_CONF, &ps_conf);
	if (ret < 0)
		return ret;
#ifdef CONFIG_VCNL4040_ENABLE_ALS
	uint16_t als_conf;

	ret = vcnl4040_read(dev, VCNL4040_REG_ALS_CONF, &als_conf);
	if (ret < 0)
		return ret;
#endif
	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		/* Clear proximity shutdown */
		ps_conf &= ~VCNL4040_PS_SD_MASK;

		ret = vcnl4040_write(dev, VCNL4040_REG_PS_CONF,
					ps_conf);
		if (ret < 0)
			return ret;
#ifdef CONFIG_VCNL4040_ENABLE_ALS
		/* Clear als shutdown */
		als_conf &= ~VCNL4040_ALS_SD_MASK;

		ret = vcnl4040_write(dev, VCNL4040_REG_ALS_CONF,
					als_conf);
		if (ret < 0)
			return ret;
#endif
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		/* Set proximity shutdown bit 0 */
		ps_conf |= VCNL4040_PS_SD_MASK;

		ret = vcnl4040_write(dev, VCNL4040_REG_PS_CONF,
					ps_conf);
		if (ret < 0)
			return ret;
#ifdef CONFIG_VCNL4040_ENABLE_ALS
		/* Clear als shutdown bit 0 */
		als_conf |= VCNL4040_ALS_SD_MASK;

		ret = vcnl4040_write(dev, VCNL4040_REG_ALS_CONF,
					als_conf);
		if (ret < 0)
			return ret;
#endif
		break;
	default:
		return -ENOTSUP;
	}

	return ret;
}
#endif

static int vcnl4040_init(const struct device *dev)
{
	const struct vcnl4040_config *config = dev->config;
	struct vcnl4040_data *data = dev->data;
	uint16_t id;

	/* Get the I2C device */
	data->i2c = device_get_binding(config->i2c_name);
	if (data->i2c == NULL) {
		LOG_ERR("Could not find I2C device");
		return -EINVAL;
	}

	/* Check device id */
	if (vcnl4040_read(dev, VCNL4040_REG_DEVICE_ID, &id)) {
		LOG_ERR("Could not read device id");
		return -EIO;
	}

	if (id != VCNL4040_DEFAULT_ID) {
		LOG_ERR("Incorrect device id (%d)", id);
		return -EIO;
	}

	if (vcnl4040_proxy_setup(dev)) {
		LOG_ERR("Failed to setup proximity functionality");
		return -EIO;
	}

#ifdef CONFIG_VCNL4040_ENABLE_ALS
	if (vcnl4040_ambient_setup(dev)) {
		LOG_ERR("Failed to setup ambient light functionality");
		return -EIO;
	}
#endif

	k_sem_init(&data->sem, 0, K_SEM_MAX_LIMIT);

#if CONFIG_VCNL4040_TRIGGER
	if (vcnl4040_trigger_init(dev)) {
		LOG_ERR("Could not initialise interrupts");
		return -EIO;
	}
#endif

	k_sem_give(&data->sem);

	LOG_DBG("Init complete");

	return 0;
}

static const struct sensor_driver_api vcnl4040_driver_api = {
	.sample_fetch = vcnl4040_sample_fetch,
	.channel_get = vcnl4040_channel_get,
#ifdef CONFIG_VCNL4040_TRIGGER
	.attr_set = vcnl4040_attr_set,
	.trigger_set = vcnl4040_trigger_set,
#endif
};

static const struct vcnl4040_config vcnl4040_config = {
	.i2c_name = DT_INST_BUS_LABEL(0),
	.i2c_address = DT_INST_REG_ADDR(0),
#ifdef CONFIG_VCNL4040_TRIGGER
#if DT_INST_NODE_HAS_PROP(0, int_gpios)
	.gpio_name = DT_INST_GPIO_LABEL(0, int_gpios),
	.gpio_pin = DT_INST_GPIO_PIN(0, int_gpios),
	.gpio_flags = DT_INST_GPIO_FLAGS(0, int_gpios),
#else
	.gpio_name = NULL,
	.gpio_pin = 0,
	.gpio_flags = 0,
#endif
#endif
	.led_i = DT_INST_ENUM_IDX(0, led_current),
	.led_dc = DT_INST_ENUM_IDX(0, led_duty_cycle),
	.als_it = DT_INST_ENUM_IDX(0, als_it),
	.proxy_it = DT_INST_ENUM_IDX(0, proximity_it),
	.proxy_type = DT_INST_ENUM_IDX(0, proximity_trigger),
};

static struct vcnl4040_data vcnl4040_data;

PM_DEVICE_DT_INST_DEFINE(0, vcnl4040_pm_action);

DEVICE_DT_INST_DEFINE(0, vcnl4040_init,
	      PM_DEVICE_DT_INST_GET(0), &vcnl4040_data, &vcnl4040_config,
	      POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, &vcnl4040_driver_api);