tegrakernel/kernel/nvidia/drivers/misc/nvs/nvs_proximity.c

646 lines
24 KiB
C

/* Copyright (c) 2014-2017, NVIDIA CORPORATION. All rights reserved.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
/* The NVS = NVidia Sensor framework */
/* This common NVS proximity module allows, along with the NVS IIO common
* module, a proximity driver to offload the code interacting with IIO and
* proximity reporting, and just have code that interacts with the HW.
* The commonality between this module and the NVS ALS driver is the
* nvs_proximity structure. It is expected that the NVS proximity driver will:
* - call nvs_proximity_enable when the device is enabled for initialization.
* - read the HW and place the value in nvs_proximity.hw
* - call nvs_proximity_read
* - depending on the nvs_proximity_read return value:
* - -1 = poll HW using nvs_proximity.poll_delay_ms delay.
* - 0 = if interrupt driven, do nothing or resume regular polling
* - 1 = set new thresholds using the nvs_proximity.hw_thresh_lo/hi
* Reporting the distance is handled within this module.
* See nvs_proximity.h for nvs_proximity structure details.
*/
/* NVS proximity drivers can be configured for binary output. If the max_range
* and resolution settings in the device tree is set for 1.0, the driver
* will configure the rest of the settings so that a 1 is reported for
* "far away" and 0 for "near". The low threshold is typically set for maximum
* range allowing the minimal LED drive power to determine the actual range.
* If proximity binary output is disabled, the driver will then require the
* interpolation calibration for reporting actual distances.
*/
/* The NVS HAL will use the IIO scale and offset sysfs attributes to modify the
* data using the following formula: (data * scale) + offset
* A scale value of 0 disables scale.
* A scale value of 1 puts the NVS HAL into calibration mode where the scale
* and offset are read everytime the data is read to allow realtime calibration
* of the scale and offset values to be used in the device tree parameters.
* Keep in mind the data is buffered but the NVS HAL will display the data and
* scale/offset parameters in the log. See calibration steps below.
*/
/* Because the proximity HW can use dynamic resolution depending on the
* distance range, configuration threshold values are HW based. In other
* words, the threshold will automatically scale based on the resolution.
*/
/* If the NVS proximity driver is not configured for binary output, then
* there are two calibration mechanisms that can be used:
* Method 1 is required if the driver is using dynamic resolution since the
* resolution cannot be read by the NVS HAL on every data value read due to
* buffering. So instead.a mechanism allows floating point to be calculated
* here in the kernel by shifting up to integer the floating point significant
* amount. This allows real-time resolution changes without the NVS HAL having
* to synchronize to the actual resolution for each datum. The scale.fval must
* be a 10 base value, e.g. 0.1, 0.01, ... 0.000001, etc. as the significant
* amount. The NVS HAL will then convert the value to float by multiplying the
* integer float-data with scale.
* Method 1:
* This method uses interpolation and requires a low and high uncalibrated
* value along with the corresponding low and high calibrated values. The
* uncalibrated values are what is read from the sensor in the steps below.
* The corresponding calibrated values are what the correct value should be.
* All values are programmed into the device tree settings.
* 1. Read scale sysfs attribute. This value will need to be written back.
* 2. Disable device.
* 3. Write 1 to the scale sysfs attribute.
* 4. Enable device.
* 5. The NVS HAL will announce in the log that calibration mode is enabled and
* display the data along with the scale and offset parameters applied.
* 6. Write the scale value read in step 1 back to the scale sysfs attribute.
* 7. Put the device into a state where the data read is a low value.
* 8. Note the values displayed in the log. Separately measure the actual
* value. The value from the sensor will be the uncalibrated value and the
* separately measured value will be the calibrated value for the current
* state (low or high values).
* 9. Put the device into a state where the data read is a high value.
* 10. Repeat step 8.
* 11. Enter the values in the device tree settings for the device. Both
* calibrated and uncalibrated values will be the values before scale and
* offset are applied.
* The proximity sensor has the following device tree parameters for this:
* proximity_uncalibrated_lo
* proximity_calibrated_lo
* proximity_uncalibrated_hi
* proximity_calibrated_hi
*
* An NVS proximity driver may support a simplified version of method 1 that
* can be used in realtime:
* At step 8, write the calibrated value to the in_proximity_threshold_low
* attribute. When in calibration mode this value will be written to the
* proximity_calibrated_lo and the current proximity written to
* proximity_uncalibrated_lo internal to the driver.
* If this is after step 9, then use the in_proximity_threshold_high
* attribute.
* Note that the calibrated value must be the value before the scale and offset
* is applied. For example, if the calibrated proximity reading is 123.4 cm,
* and the in_proximity_scale is normally 0.01, then the value entered is 12340
* which will be 123.4 cm when the scale is applied at the HAL layer.
* To confirm the realtime values and see what the driver used for uncalibrated
* values, do the following at the adb prompt in the driver space:
* # echo 5 > nvs
* # cat nvs
* This will be a partial dump of the sensor's configuration structure that
* will show the calibrated and uncalibrated values. For example:
* ...
* uncal_lo=1
* uncal_hi=96346
* cal_lo=230
* cal_hi=1888000
* thresh_lo=10
* thresh_hi=10
* ...
* If the thresholds have changed instead of the calibration settings, then
* the driver doesn't support this feature.
* In order to display raw values, interpolation, that uses the calibration
* values, is not executed by the driver when in calibration mode, so to test,
* disable and reenable the device to exit calibration mode and test the new
* calibration values.
*
* Method 2 can only be used if dynamic resolution is not used by the HW
* driver. The data passed up to the HAL is the HW value read so that the HAL
* can multiply the HW value with the scale (resolution).
* As a baseline, scale would be the same value as the static resolution.
* Method 2:
* 1. Disable device.
* 2. Write 1 to the scale sysfs attribute.
* 3. Enable device.
* 4. The NVS HAL will announce in the log that calibration mode is enabled and
* display the data along with the scale and offset parameters applied.
* 5. Write to scale and offset sysfs attributes as needed to get the data
* modified as desired.
* 6. Disabling the device disables calibration mode.
* 7. Set the new scale and offset parameters in the device tree:
* proximity_scale_ival = the integer value of the scale.
* proximity_scale_fval = the floating value of the scale.
* proximity_offset_ival = the integer value of the offset.
* proximity_offset_fval = the floating value of the offset.
* The values are in the NVS_FLOAT_SIGNIFICANCE_ format (see nvs.h).
*/
/* If the NVS proximity driver is configured for binary output, then
* interpolation is not used and the thresholds are used to trigger either the
* 0 or 1 output. To calibrate the thresholds:
* 1. Disable device.
* 2. Write 1 to the scale sysfs attribute.
* 3. Enable device.
* 4. The NVS HAL will announce in the log that calibration mode is enabled and
* display the HW proximity data.
* 5. Move an object (your hand) through the proximity range. Note the HW
* value when the object is at a point that the output should be 0. This
* will be the high threshold value. Move the object away from the sensor
* and note the HW value where the output should change to 1. This will be
* the low threshold value.
* NOTE: Proximity typically works by reading the reflected IR light from an
* LED. The more light reflected, the higher the HW value and the closer
* the object is. Because of this, the thresholds appear to be reversed
* to the output, but keep in mind, the thresholds are HW based, so low
* threshold means low HW value regardless of the actual output.
* NOTE: If greater range is needed, modify the LED output strength if the
* proximity HW supports it. This will be a DT configuration option that
* is specific to the driver and HW.
* 6. Enter the threshold values in the device tree settings for the device.
* The proximity sensor has the following device tree parameters for this:
* proximity_threshold_lo
* proximity_threshold_hi
*/
/* If the NVS proximity driver is not configured for binary output, then the
* thresholds are used for hysterysis. The threshold settings are HW based and
* allow a window around the last reported HW value. For example, if the low
* threshold is set to 10 and the high threshold set to 20, if the proximity HW
* value is 100, the proximity won't be reported again until the proximity HW
* value is either < 90 or > than 120.
* The low/high threshold values are typically the same, but they can be
* configured so that proximity changes at a different rate based on the
* direction of change.
* Use the calibration methods for a steady output of data to get an idea of
* the debounce desired.
* NOTE: If both configuration thresholds are 0, then thresholds are disabled.
* NOTE: An NVS feature is the use of the report_count configuration variable,
* proximity_report_count in DT (see nvs.h). This allows additional
* reporting of proximity a set amount of times while still within the
* threshold window.
*/
/* If the NVS proximity driver is configured for binary output, then the
* thresholds are absolute HW values. If not configured for binary output,
* then the thresholds are relative HW values to set a trigger window around
* the last read HW value.
*/
#include <linux/module.h>
#include <linux/of.h>
#include <linux/version.h>
#include <linux/nvs_proximity.h>
#define NVS_PROXIMITY_VERSION (102)
#define NVS_FS_NANO NVS_FLOAT_SIGNIFICANCE_NANO
#define NVS_FS_MICRO NVS_FLOAT_SIGNIFICANCE_MICRO
ssize_t nvs_proximity_dbg(struct nvs_proximity *np, char *buf)
{
ssize_t t;
t = snprintf(buf, PAGE_SIZE, "%s v.%u:\n",
__func__, NVS_PROXIMITY_VERSION);
t += snprintf(buf + t, PAGE_SIZE - t, "timestamp=%lld\n",
np->timestamp);
t += snprintf(buf + t, PAGE_SIZE - t, "timestamp_report=%lld\n",
np->timestamp_report);
t += snprintf(buf + t, PAGE_SIZE - t, "proximity=%u\n", np->proximity);
t += snprintf(buf + t, PAGE_SIZE - t, "hw=%u\n", np->hw);
t += snprintf(buf + t, PAGE_SIZE - t, "hw_mask=%x\n", np->hw_mask);
t += snprintf(buf + t, PAGE_SIZE - t, "hw_thresh_lo=%u\n",
np->hw_thresh_lo);
t += snprintf(buf + t, PAGE_SIZE - t, "hw_thresh_hi=%u\n",
np->hw_thresh_hi);
t += snprintf(buf + t, PAGE_SIZE - t, "hw_limit_lo=%x\n",
np->hw_limit_lo);
t += snprintf(buf + t, PAGE_SIZE - t, "hw_limit_hi=%x\n",
np->hw_limit_hi);
t += snprintf(buf + t, PAGE_SIZE - t, "thresh_valid_lo=%x\n",
np->thresh_valid_lo);
t += snprintf(buf + t, PAGE_SIZE - t, "thresh_valid_hi=%x\n",
np->thresh_valid_hi);
t += snprintf(buf + t, PAGE_SIZE - t, "thresholds_valid=%x\n",
np->thresholds_valid);
t += snprintf(buf + t, PAGE_SIZE - t, "calibration_en=%x\n",
np->calibration_en);
t += snprintf(buf + t, PAGE_SIZE - t, "dynamic_resolution_dis=%x\n",
np->dynamic_resolution_dis);
t += snprintf(buf + t, PAGE_SIZE - t,
"proximity_reverse_range_dis=%x\n",
np->proximity_reverse_range_dis);
t += snprintf(buf + t, PAGE_SIZE - t, "proximity_binary_en=%x\n",
np->proximity_binary_en);
t += snprintf(buf + t, PAGE_SIZE - t, "proximity_binary_hw=%x\n",
np->proximity_binary_hw);
t += snprintf(buf + t, PAGE_SIZE - t, "poll_delay_ms=%u\n",
np->poll_delay_ms);
t += snprintf(buf + t, PAGE_SIZE - t, "delay_us=%u\n", np->delay_us);
t += snprintf(buf + t, PAGE_SIZE - t, "report=%u\n", np->report);
return t;
}
EXPORT_SYMBOL_GPL(nvs_proximity_dbg);
static void nvs_proximity_interpolate(int x1, s64 x2, int x3,
int y1, u32 *y2, int y3)
{
s64 dividend;
s64 divisor;
/* y2 = ((x2 - x1)(y3 - y1)/(x3 - x1)) + y1 */
divisor = (x3 - x1);
if (!divisor) {
*y2 = (u32)x2;
return;
}
dividend = (x2 - x1) * (y3 - y1);
if (dividend < 0) {
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 3, 0)
dividend = abs64(dividend);
#else
dividend = abs(dividend);
#endif
do_div(dividend, divisor);
dividend = 0 - dividend;
} else {
do_div(dividend, divisor);
}
dividend += y1;
if (dividend < 0)
dividend = 0;
*y2 = (u32)dividend;
}
static int nvs_proximity_poll_delay(struct nvs_proximity *np, int ret,
unsigned int poll_delay,
bool report_delay_min)
{
if (report_delay_min)
poll_delay = np->delay_us;
if ((poll_delay < np->cfg->delay_us_min) || np->calibration_en)
poll_delay = np->cfg->delay_us_min;
np->poll_delay_ms = poll_delay / 1000;
if (np->report || np->calibration_en)
ret = RET_POLL_NEXT; /* poll for next sample */
return ret;
}
/**
* nvs_proximity_read - called after HW is read and written to
* np.
* @np: the common structure between driver and common module.
*
* This will handle the conversion of HW to distance value,
* reporting, calculation of thresholds and poll time.
*
* Returns: -1 = Error and/or polling is required for next
* sample regardless of being interrupt driven.
* 0 = Do nothing. Value has not changed for reporting
* and same threshold values if interrupt driven.
* If not interrupt driven use poll_delay_ms.
* 1 = New HW thresholds are needed.
* If not interrupt driven use poll_delay_ms.
*/
int nvs_proximity_read(struct nvs_proximity *np)
{
u64 hw_distance;
u64 calc_i;
u64 calc_f;
s64 calc;
s64 timestamp_diff;
s64 delay;
bool report_delay_min = true;
unsigned int poll_delay = 0;
unsigned int thresh_lo;
unsigned int thresh_hi;
int ret;
if (np->calibration_en)
/* always report without report_delay_min */
np->report = np->cfg->report_n;
if (np->report < np->cfg->report_n) { /* always report first sample */
/* calculate elapsed time for allowed report rate */
timestamp_diff = np->timestamp - np->timestamp_report;
delay = (s64)np->delay_us * 1000;
if (timestamp_diff < delay) {
/* data changes are happening faster than allowed to
* report so we poll for the next data at an allowed
* rate with interrupts disabled.
*/
delay -= timestamp_diff;
do_div(delay, 1000); /* ns => us */
poll_delay = delay;
report_delay_min = false;
}
}
if (np->proximity_binary_hw) {
/* this device has proximity binary HW (HW reads 0 or 1)
* so just report value if changed
*/
ret = RET_NO_CHANGE;
if (np->hw != np->proximity) {
np->proximity = np->hw;
np->report = np->cfg->report_n;
}
if (np->calibration_en)
np->report = np->cfg->report_n;
if (np->report && report_delay_min) {
np->report--;
np->timestamp_report = np->timestamp;
np->handler(np->nvs_st, &np->proximity,
np->timestamp_report);
ret = RET_HW_UPDATE;
}
return nvs_proximity_poll_delay(np, ret, poll_delay,
report_delay_min);
}
/* threshold flags */
thresh_lo = np->cfg->thresh_lo;
thresh_hi = np->cfg->thresh_hi;
if (thresh_lo < np->hw_mask) {
np->thresh_valid_lo = true;
} else {
np->thresh_valid_lo = false;
thresh_lo = 0;
}
if (thresh_hi < np->hw_mask) {
np->thresh_valid_hi = true;
} else {
np->thresh_valid_hi = false;
thresh_hi = 0;
}
if (np->thresh_valid_lo && np->thresh_valid_hi)
np->thresholds_valid = true;
else
np->thresholds_valid = false;
/* limit flags */
if ((np->hw < thresh_lo) || (np->hw == 0))
np->hw_limit_lo = true;
else
np->hw_limit_lo = false;
if (np->proximity_binary_en) {
if (np->hw > thresh_hi)
np->hw_limit_hi = true;
else
np->hw_limit_hi = false;
} else {
if ((np->hw == np->hw_mask) || (np->hw >
(np->hw_mask - thresh_hi)))
np->hw_limit_hi = true;
else
np->hw_limit_hi = false;
}
ret = RET_NO_CHANGE;
if (np->proximity_binary_en) {
/* proximity has binary threshold */
if (!np->thresholds_valid) {
/* Invalid thresholds is an NVS feature that forces
* polling. However, with this binary mechanism,
* thresholds are required. So although the feature
* is somewhat crippled, we make it work by setting
* the trigger in the middle of the HW range.
*/
thresh_lo = np->hw_mask / 2;
thresh_hi = thresh_lo;
np->report = np->cfg->report_n;
}
if (np->hw < np->hw_thresh_lo) {
np->proximity = 1;
np->report = np->cfg->report_n;
/* disable lower threshold */
np->hw_thresh_lo = 0;
/* enable upper threshold */
np->hw_thresh_hi = thresh_hi;
} else if (np->hw > np->hw_thresh_hi) {
np->proximity = 0;
np->report = np->cfg->report_n;
/* disable upper threshold */
np->hw_thresh_hi = np->hw_mask;
/* enable lower threshold */
np->hw_thresh_lo = thresh_lo;
}
if (np->calibration_en)
np->proximity = np->hw;
if (np->report && report_delay_min) {
np->report--;
np->timestamp_report = np->timestamp;
np->handler(np->nvs_st, &np->proximity,
np->timestamp_report);
ret = RET_HW_UPDATE;
}
} else {
/* reporting and thresholds */
if (np->thresholds_valid) {
if (np->hw < np->hw_thresh_lo)
np->report = np->cfg->report_n;
else if (np->hw > np->hw_thresh_hi)
np->report = np->cfg->report_n;
} else {
/* report everything if no thresholds */
np->report = np->cfg->report_n;
}
if (np->report && report_delay_min) {
np->report--;
np->timestamp_report = np->timestamp;
if (np->proximity_reverse_range_dis)
hw_distance = np->hw;
else
/* reverse the value in the range */
hw_distance = np->hw_mask - np->hw;
/* distance = HW * (resolution *
* NVS_FLOAT_SIGNIFICANCE_) / scale
*/
calc_i = hw_distance;
calc_f = 0;
if (np->cfg->scale.fval &&
!np->dynamic_resolution_dis) {
/* The mechanism below allows floating point to
* be calculated here in the kernel by shifting
* up to integer the floating point significant
* amount.
* The nl->cfg->scale.fval must be a 10 base
* value, e.g. 0.1, 0.01, ... 0.000001, etc.
* The significance is calculated as:
* s = (NVS_FLOAT_SIGNIFICANCE_* / scale.fval)
* so that proximity = HW * resolution * s
* The NVS HAL will then convert the value to
* float by multiplying the data with scale.
*/
if (np->cfg->resolution.fval) {
calc_f = hw_distance *
np->cfg->resolution.fval;
do_div(calc_f, np->cfg->scale.fval);
}
if (np->cfg->resolution.ival) {
if (np->cfg->float_significance)
calc_i = NVS_FS_NANO;
else
calc_i = NVS_FS_MICRO;
do_div(calc_i, np->cfg->scale.fval);
calc_i *= hw_distance *
np->cfg->resolution.ival;
}
}
calc = (s64)(calc_i + calc_f);
if (np->calibration_en)
/* when in calibration mode just return calc */
np->proximity = (u32)calc;
else
/* get calibrated value */
nvs_proximity_interpolate(np->cfg->uncal_lo,
calc,
np->cfg->uncal_hi,
np->cfg->cal_lo,
&np->proximity,
np->cfg->cal_hi);
/* report proximity */
np->handler(np->nvs_st, &np->proximity,
np->timestamp_report);
if ((np->thresholds_valid) && !np->report) {
/* calculate low threshold */
calc = (s64)np->hw;
calc -= thresh_lo;
if (calc < 0)
/* low threshold is disabled */
np->hw_thresh_lo = 0;
else
np->hw_thresh_lo = calc;
/* calculate high threshold */
calc = np->hw + thresh_hi;
if (calc > np->hw_mask)
/* high threshold is disabled */
np->hw_thresh_hi = np->hw_mask;
else
np->hw_thresh_hi = calc;
ret = RET_HW_UPDATE;
}
}
}
return nvs_proximity_poll_delay(np, ret, poll_delay, report_delay_min);
}
EXPORT_SYMBOL_GPL(nvs_proximity_read);
/**
* nvs_proximity_enable - called when the proximity sensor is
* enabled.
* @np: the common structure between driver and common module.
*
* This inititializes the np NVS variables.
*
* Returns 0 on success or a negative error code.
*/
int nvs_proximity_enable(struct nvs_proximity *np)
{
if (!np->cfg->report_n)
np->cfg->report_n = 1;
np->report = np->cfg->report_n;
np->timestamp_report = 0;
np->hw_thresh_hi = 0;
np->hw_thresh_lo = -1;
np->proximity = 1;
if (np->cfg->resolution.ival == 1 && !np->cfg->resolution.fval &&
np->cfg->max_range.ival == 1 && !np->cfg->max_range.fval)
np->proximity_binary_en = true;
else
np->proximity_binary_en = false;
if (np->cfg->scale.ival == 1 && !np->cfg->scale.fval)
np->calibration_en = true;
else
np->calibration_en = false;
if (np->delay_us)
np->poll_delay_ms = np->delay_us * 1000;
else
np->poll_delay_ms = np->cfg->delay_us_min * 1000;
if (np->hw_mask == 1)
np->proximity_binary_hw = true;
return 0;
}
EXPORT_SYMBOL_GPL(nvs_proximity_enable);
/**
* nvs_proximity_of_dt - called during system boot for
* configuration from device tree.
* @np: the common structure between driver and common module.
* @dn: device node pointer.
* @dev_name: device name string. Typically a string to
* "proximity" or NULL.
*
* Returns 0 on success or a negative error code.
*
* Driver must initialize variables if no success.
*/
int nvs_proximity_of_dt(struct nvs_proximity *np, const struct device_node *dn,
const char *dev_name)
{
s32 binary_hw = -1;
char str[256];
int ret;
if (np->cfg)
np->cfg->flags |= SENSOR_FLAG_ON_CHANGE_MODE;
if (dn == NULL)
return -EINVAL;
if (dev_name == NULL)
dev_name = NVS_PROXIMITY_STRING;
ret = snprintf(str, sizeof(str), "%s_binary_hw", dev_name);
if (ret > 0)
of_property_read_s32(dn, str, &binary_hw);
if (binary_hw > 0)
np->proximity_binary_hw = true;
else if (!binary_hw)
np->proximity_binary_hw = false;
return 0;
}
EXPORT_SYMBOL_GPL(nvs_proximity_of_dt);
/**
* nvs_proximity_threshold_calibrate_lo - runtime mechanism to
* modify calibrated/uncalibrated low value.
* @np: the common structure between driver and common module.
*
* NOTE: If not in calibration mode then thresholds are modified
* instead.
*/
void nvs_proximity_threshold_calibrate_lo(struct nvs_proximity *np, int lo)
{
if (np->calibration_en) {
np->cfg->uncal_lo = np->proximity;
np->cfg->cal_lo = lo;
} else {
np->cfg->thresh_lo = lo;
}
}
EXPORT_SYMBOL_GPL(nvs_proximity_threshold_calibrate_lo);
/**
* nvs_proximity_threshold_calibrate_hi - runtime mechanism to
* modify calibrated/uncalibrated high value.
* @nl: the common structure between driver and common module.
*
* NOTE: If not in calibration mode then thresholds are modified
* instead.
*/
void nvs_proximity_threshold_calibrate_hi(struct nvs_proximity *np, int hi)
{
if (np->calibration_en) {
np->cfg->uncal_hi = np->proximity;
np->cfg->cal_hi = hi;
} else {
np->cfg->thresh_hi = hi;
}
}
EXPORT_SYMBOL_GPL(nvs_proximity_threshold_calibrate_hi);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("NVidia Sensor proximity module");
MODULE_AUTHOR("NVIDIA Corporation");