drivers: input: add an analog-axis driver

Add an input driver to read data from an analog device, such as a
thumbstick, connected to an ADC channel, and report it as an input
device.

Signed-off-by: Fabio Baltieri <fabiobaltieri@google.com>
This commit is contained in:
Fabio Baltieri 2023-12-16 21:13:57 +00:00 committed by Carles Cufí
parent e5b3231354
commit bd8cee8683
11 changed files with 688 additions and 0 deletions

View file

@ -96,3 +96,8 @@ Input Event Definitions
*********************** ***********************
.. doxygengroup:: input_events .. doxygengroup:: input_events
Analog Axis API Reference
*************************
.. doxygengroup:: input_analog_axis

View file

@ -4,6 +4,8 @@ zephyr_library()
zephyr_library_property(ALLOW_EMPTY TRUE) zephyr_library_property(ALLOW_EMPTY TRUE)
# zephyr-keep-sorted-start # zephyr-keep-sorted-start
zephyr_library_sources_ifdef(CONFIG_INPUT_ANALOG_AXIS input_analog_axis.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_ANALOG_AXIS_SETTINGS input_analog_axis_settings.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CAP1203 input_cap1203.c) zephyr_library_sources_ifdef(CONFIG_INPUT_CAP1203 input_cap1203.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CST816S input_cst816s.c) zephyr_library_sources_ifdef(CONFIG_INPUT_CST816S input_cst816s.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_ESP32_TOUCH_SENSOR input_esp32_touch_sensor.c) zephyr_library_sources_ifdef(CONFIG_INPUT_ESP32_TOUCH_SENSOR input_esp32_touch_sensor.c)

View file

@ -6,6 +6,7 @@ if INPUT
menu "Input drivers" menu "Input drivers"
# zephyr-keep-sorted-start # zephyr-keep-sorted-start
source "drivers/input/Kconfig.analog_axis"
source "drivers/input/Kconfig.cap1203" source "drivers/input/Kconfig.cap1203"
source "drivers/input/Kconfig.cst816s" source "drivers/input/Kconfig.cst816s"
source "drivers/input/Kconfig.esp32" source "drivers/input/Kconfig.esp32"

View file

@ -0,0 +1,43 @@
# Copyright 2023 Google LLC
# SPDX-License-Identifier: Apache-2.0
config INPUT_ANALOG_AXIS
bool "ADC based analog axis input driver"
default y
depends on DT_HAS_ANALOG_AXIS_ENABLED
depends on ADC
depends on MULTITHREADING
help
ADC based analog axis input driver
if INPUT_ANALOG_AXIS
config INPUT_ANALOG_AXIS_THREAD_STACK_SIZE
int "Stack size for the analog axis thread"
default 762
help
Size of the stack used for the analog axis thread.
config INPUT_ANALOG_AXIS_THREAD_PRIORITY
int "Priority for the analog axis thread"
default 0
help
Priority level of the analog axis thread.
config INPUT_ANALOG_AXIS_SETTINGS
bool "Analog axis settings support"
default y
depends on SETTINGS
help
Settings support for the analog axis driver, exposes a
analog_axis_calibration_save() function to save the calibration into
settings and load them automatically on startup.
config INPUT_ANALOG_AXIS_SETTINGS_MAX_AXES
int "Maximum number of axes supported in the settings."
default 8
help
Maximum number of axes that can have calibration value saved in
settings.
endif

View file

@ -0,0 +1,271 @@
/*
* Copyright 2023 Google LLC
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT analog_axis
#include <stdlib.h>
#include <zephyr/device.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/input/input.h>
#include <zephyr/input/input_analog_axis.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
LOG_MODULE_REGISTER(analog_axis, CONFIG_INPUT_LOG_LEVEL);
struct analog_axis_channel_config {
struct adc_dt_spec adc;
int16_t out_min;
int16_t out_max;
uint16_t axis;
bool invert;
};
struct analog_axis_channel_data {
int last_out;
};
struct analog_axis_config {
uint32_t poll_period_ms;
const struct analog_axis_channel_config *channel_cfg;
struct analog_axis_channel_data *channel_data;
struct analog_axis_calibration *calibration;
const uint8_t num_channels;
};
struct analog_axis_data {
struct k_mutex cal_lock;
analog_axis_raw_data_t raw_data_cb;
struct k_timer timer;
struct k_thread thread;
K_KERNEL_STACK_MEMBER(thread_stack,
CONFIG_INPUT_ANALOG_AXIS_THREAD_STACK_SIZE);
};
int analog_axis_num_axes(const struct device *dev)
{
const struct analog_axis_config *cfg = dev->config;
return cfg->num_channels;
}
int analog_axis_calibration_get(const struct device *dev,
int channel,
struct analog_axis_calibration *out_cal)
{
const struct analog_axis_config *cfg = dev->config;
struct analog_axis_data *data = dev->data;
struct analog_axis_calibration *cal = &cfg->calibration[channel];
if (channel >= cfg->num_channels) {
return -EINVAL;
}
k_mutex_lock(&data->cal_lock, K_FOREVER);
memcpy(out_cal, cal, sizeof(struct analog_axis_calibration));
k_mutex_unlock(&data->cal_lock);
return 0;
}
void analog_axis_set_raw_data_cb(const struct device *dev, analog_axis_raw_data_t cb)
{
struct analog_axis_data *data = dev->data;
k_mutex_lock(&data->cal_lock, K_FOREVER);
data->raw_data_cb = cb;
k_mutex_unlock(&data->cal_lock);
}
int analog_axis_calibration_set(const struct device *dev,
int channel,
struct analog_axis_calibration *new_cal)
{
const struct analog_axis_config *cfg = dev->config;
struct analog_axis_data *data = dev->data;
struct analog_axis_calibration *cal = &cfg->calibration[channel];
if (channel >= cfg->num_channels) {
return -EINVAL;
}
k_mutex_lock(&data->cal_lock, K_FOREVER);
memcpy(cal, new_cal, sizeof(struct analog_axis_calibration));
k_mutex_unlock(&data->cal_lock);
return 0;
}
static void analog_axis_loop(const struct device *dev)
{
const struct analog_axis_config *cfg = dev->config;
struct analog_axis_data *data = dev->data;
int16_t bufs[cfg->num_channels];
int32_t out;
struct adc_sequence sequence = {
.buffer = bufs,
.buffer_size = sizeof(bufs),
};
const struct analog_axis_channel_config *axis_cfg_0 = &cfg->channel_cfg[0];
int err;
int i;
adc_sequence_init_dt(&axis_cfg_0->adc, &sequence);
for (i = 0; i < cfg->num_channels; i++) {
const struct analog_axis_channel_config *axis_cfg = &cfg->channel_cfg[i];
sequence.channels |= BIT(axis_cfg->adc.channel_id);
}
err = adc_read(axis_cfg_0->adc.dev, &sequence);
if (err < 0) {
LOG_ERR("Could not read (%d)", err);
return;
}
k_mutex_lock(&data->cal_lock, K_FOREVER);
for (i = 0; i < cfg->num_channels; i++) {
const struct analog_axis_channel_config *axis_cfg = &cfg->channel_cfg[i];
struct analog_axis_channel_data *axis_data = &cfg->channel_data[i];
struct analog_axis_calibration *cal = &cfg->calibration[i];
int16_t in_range = cal->in_max - cal->in_min;
int16_t out_range = axis_cfg->out_max - axis_cfg->out_min;
int32_t raw_val = bufs[i];
if (axis_cfg->invert) {
raw_val *= -1;
}
if (data->raw_data_cb != NULL) {
data->raw_data_cb(dev, i, raw_val);
}
LOG_DBG("%s: ch %d: raw_val: %d", dev->name, i, raw_val);
out = CLAMP((raw_val - cal->in_min) * out_range / in_range + axis_cfg->out_min,
axis_cfg->out_min, axis_cfg->out_max);
if (cal->out_deadzone > 0) {
int16_t center = DIV_ROUND_CLOSEST(
axis_cfg->out_max + axis_cfg->out_min, 2);
if (abs(out - center) < cal->out_deadzone) {
out = center;
}
}
if (axis_data->last_out != out) {
input_report_abs(dev, axis_cfg->axis, out, true, K_FOREVER);
}
axis_data->last_out = out;
}
k_mutex_unlock(&data->cal_lock);
}
static void analog_axis_thread(void *arg1, void *arg2, void *arg3)
{
const struct device *dev = arg1;
const struct analog_axis_config *cfg = dev->config;
struct analog_axis_data *data = dev->data;
int err;
int i;
for (i = 0; i < cfg->num_channels; i++) {
const struct analog_axis_channel_config *axis_cfg = &cfg->channel_cfg[i];
if (!adc_is_ready_dt(&axis_cfg->adc)) {
LOG_ERR("ADC controller device not ready");
return;
}
err = adc_channel_setup_dt(&axis_cfg->adc);
if (err < 0) {
LOG_ERR("Could not setup channel #%d (%d)", i, err);
return;
}
}
k_timer_init(&data->timer, NULL, NULL);
k_timer_start(&data->timer,
K_MSEC(cfg->poll_period_ms), K_MSEC(cfg->poll_period_ms));
while (true) {
analog_axis_loop(dev);
k_timer_status_sync(&data->timer);
}
}
static int analog_axis_init(const struct device *dev)
{
struct analog_axis_data *data = dev->data;
k_tid_t tid;
k_mutex_init(&data->cal_lock);
tid = k_thread_create(&data->thread, data->thread_stack,
K_KERNEL_STACK_SIZEOF(data->thread_stack),
analog_axis_thread, (void *)dev, NULL, NULL,
CONFIG_INPUT_ANALOG_AXIS_THREAD_PRIORITY,
0, K_NO_WAIT);
if (!tid) {
LOG_ERR("thread creation failed");
return -ENODEV;
}
k_thread_name_set(&data->thread, dev->name);
return 0;
}
#define ANALOG_AXIS_CHANNEL_CFG_DEF(node_id) \
{ \
.adc = ADC_DT_SPEC_GET(node_id), \
.out_min = (int16_t)DT_PROP(node_id, out_min), \
.out_max = (int16_t)DT_PROP(node_id, out_max), \
.axis = DT_PROP(node_id, zephyr_axis), \
.invert = DT_PROP(node_id, invert), \
}
#define ANALOG_AXIS_CHANNEL_CAL_DEF(node_id) \
{ \
.in_min = (int16_t)DT_PROP(node_id, in_min), \
.in_max = (int16_t)DT_PROP(node_id, in_max), \
.out_deadzone = DT_PROP(node_id, out_deadzone), \
}
#define ANALOG_AXIS_INIT(inst) \
static const struct analog_axis_channel_config analog_axis_channel_cfg_##inst[] = { \
DT_INST_FOREACH_CHILD_STATUS_OKAY_SEP(inst, ANALOG_AXIS_CHANNEL_CFG_DEF, (,)) \
}; \
\
static struct analog_axis_channel_data \
analog_axis_channel_data_##inst[ARRAY_SIZE(analog_axis_channel_cfg_##inst)]; \
\
static struct analog_axis_calibration \
analog_axis_calibration##inst[ARRAY_SIZE(analog_axis_channel_cfg_##inst)] = { \
DT_INST_FOREACH_CHILD_STATUS_OKAY_SEP( \
inst, ANALOG_AXIS_CHANNEL_CAL_DEF, (,)) \
}; \
\
static const struct analog_axis_config analog_axis_cfg_##inst = { \
.poll_period_ms = DT_INST_PROP(inst, poll_period_ms), \
.channel_cfg = analog_axis_channel_cfg_##inst, \
.channel_data = analog_axis_channel_data_##inst, \
.calibration = analog_axis_calibration##inst, \
.num_channels = ARRAY_SIZE(analog_axis_channel_cfg_##inst), \
}; \
\
static struct analog_axis_data analog_axis_data_##inst; \
\
DEVICE_DT_INST_DEFINE(inst, analog_axis_init, NULL, \
&analog_axis_data_##inst, &analog_axis_cfg_##inst, \
POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, NULL);
DT_INST_FOREACH_STATUS_OKAY(ANALOG_AXIS_INIT)

View file

@ -0,0 +1,111 @@
/*
* Copyright 2023 Google LLC
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <zephyr/device.h>
#include <zephyr/input/input_analog_axis.h>
#include <zephyr/input/input_analog_axis_settings.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/printk.h>
LOG_MODULE_REGISTER(analog_axis_settings, CONFIG_INPUT_LOG_LEVEL);
#define ANALOG_AXIS_SETTINGS_PATH_MAX 32
#define MAX_AXES CONFIG_INPUT_ANALOG_AXIS_SETTINGS_MAX_AXES
static void analog_axis_calibration_log(const struct device *dev)
{
struct analog_axis_calibration cal;
int i;
for (i = 0; i < analog_axis_num_axes(dev); i++) {
analog_axis_calibration_get(dev, i, &cal);
LOG_INF("%s: ch: %d min: %d max: %d deadzone: %d",
dev->name, i, cal.in_min, cal.in_max, cal.out_deadzone);
}
}
static int analog_axis_calibration_load(const char *key, size_t len_rd,
settings_read_cb read_cb, void *cb_arg)
{
const struct device *dev;
struct analog_axis_calibration cal[MAX_AXES];
int axes;
char dev_name[ANALOG_AXIS_SETTINGS_PATH_MAX];
const char *next;
int nlen;
ssize_t len;
nlen = settings_name_next(key, &next);
if (nlen + 1 > sizeof(dev_name)) {
LOG_ERR("Setting name too long: %d", nlen);
return -EINVAL;
}
memcpy(dev_name, key, nlen);
dev_name[nlen] = '\0';
dev = device_get_binding(dev_name);
if (dev == NULL) {
LOG_ERR("Cannot restore: device %s not available", dev_name);
return -ENODEV;
}
len = read_cb(cb_arg, cal, sizeof(cal));
if (len < 0) {
LOG_ERR("Data restore error: %d", len);
}
axes = analog_axis_num_axes(dev);
if (len != sizeof(struct analog_axis_calibration) * axes) {
LOG_ERR("Invalid settings data length: %d, expected %d",
len, sizeof(struct analog_axis_calibration) * axes);
return -EIO;
}
for (int i = 0; i < axes; i++) {
analog_axis_calibration_set(dev, i, &cal[i]);
}
analog_axis_calibration_log(dev);
return 0;
}
SETTINGS_STATIC_HANDLER_DEFINE(analog_axis, "aa-cal", NULL,
analog_axis_calibration_load, NULL, NULL);
int analog_axis_calibration_save(const struct device *dev)
{
struct analog_axis_calibration cal[MAX_AXES];
int axes;
char path[ANALOG_AXIS_SETTINGS_PATH_MAX];
int ret;
analog_axis_calibration_log(dev);
ret = snprintk(path, sizeof(path), "aa-cal/%s", dev->name);
if (ret < 0) {
return -EINVAL;
}
axes = analog_axis_num_axes(dev);
for (int i = 0; i < axes; i++) {
analog_axis_calibration_get(dev, i, &cal[i]);
}
ret = settings_save_one(path, &cal[0],
sizeof(struct analog_axis_calibration) * axes);
if (ret < 0) {
LOG_ERR("Settings save errord: %d", ret);
return ret;
}
return 0;
}

View file

@ -0,0 +1,90 @@
# Copyright 2023 Google LLC
# SPDX-License-Identifier: Apache-2.0
description: |
ADC based analog axis input device
Implement an input device generating absolute axis events by periodically
reading from some ADC channels.
Example configuration:
#include <zephyr/dt-bindings/input/input-event-codes.h>
analog_axis {
compatible = "analog-axis";
poll-period-ms = <15>;
axis-x {
io-channels = <&adc 0>;
out-deadzone = <8>;
in-min = <100>;
in-max = <800>;
zephyr,axis = <INPUT_ABS_X>;
};
};
compatible: "analog-axis"
include: base.yaml
properties:
poll-period-ms:
type: int
default: 15
description: |
How often to get new ADC samples for the various configured axes in
milliseconds. Defaults to 15ms if unspecified.
child-binding:
properties:
io-channels:
type: phandle-array
required: true
description: |
ADC IO channel to use.
out-min:
type: int
default: 0
description: |
Minimum value to output on input events. Defaults to 0 if unspecified.
out-max:
type: int
default: 255
description: |
Maximum value to output on input events. Defaults to 255 if
unspecified.
out-deadzone:
type: int
default: 0
description: |
Deadzone for the output center value. If specified output values
between the center of the range plus or minus this value will be
reported as center. Defaults to 0, no deadzone.
in-min:
type: int
required: true
description: |
Input value that corresponds to the minimum output value.
in-max:
type: int
required: true
description: |
Input value that corresponds to the maximum output value.
zephyr,axis:
type: int
required: true
description: |
The input code for the axis to report for the device, typically any of
INPUT_ABS_*.
invert:
type: boolean
description: |
If set, invert the raw ADC value before processing it. Useful for
differential channels.

View file

@ -0,0 +1,97 @@
/*
* Copyright 2023 Google LLC
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_INCLUDE_INPUT_ANALOG_AXIS_H_
#define ZEPHYR_INCLUDE_INPUT_ANALOG_AXIS_H_
#include <stdint.h>
#include <zephyr/device.h>
/**
* @brief Analog axis API
* @defgroup input_analog_axis Analog axis API
* @ingroup io_interfaces
* @{
*/
/**
* @brief Analog axis calibration data structure.
*
* Holds the calibration data for a single analog axis. Initial values are set
* from the devicetree and can be changed by the applicatoin in runtime using
* @ref analog_axis_calibration_set and @ref analog_axis_calibration_get.
*/
struct analog_axis_calibration {
/** Input value that corresponds to the minimum output value. */
int16_t in_min;
/** Input value that corresponds to the maximum output value. */
int16_t in_max;
/** Output value deadzone relative to the output range. */
uint16_t out_deadzone;
};
/**
* @brief Analog axis raw data callback.
*
* @param dev Analog axis device.
* @param channel Channel number.
* @param raw_val Raw value for the channel.
*/
typedef void (*analog_axis_raw_data_t)(const struct device *dev,
int channel, int16_t raw_val);
/**
* @brief Set a raw data callback.
*
* Set a callback to receive raw data for the specified analog axis device.
* This is meant to be use in the application to acquire the data to use for
* calibration. Set cb to NULL to disable the callback.
*
* @param dev Analog axis device.
* @param cb An analog_axis_raw_data_t callback to use, NULL disable.
*/
void analog_axis_set_raw_data_cb(const struct device *dev, analog_axis_raw_data_t cb);
/**
* @brief Get the number of defined axes.
*
* @retval n The number of defined axes for dev.
*/
int analog_axis_num_axes(const struct device *dev);
/**
* @brief Get the axis calibration data.
*
* @param dev Analog axis device.
* @param channel Channel number.
* @param cal Pointer to an analog_axis_calibration structure that is going to
* get set with the current calibration data.
*
* @retval 0 If successful.
* @retval -EINVAL If the specified channel is not valid.
*/
int analog_axis_calibration_get(const struct device *dev,
int channel,
struct analog_axis_calibration *cal);
/**
* @brief Set the axis calibration data.
*
* @param dev Analog axis device.
* @param channel Channel number.
* @param cal Pointer to an analog_axis_calibration structure with the new
* calibration data
*
* @retval 0 If successful.
* @retval -EINVAL If the specified channel is not valid.
*/
int analog_axis_calibration_set(const struct device *dev,
int channel,
struct analog_axis_calibration *cal);
/** @} */
#endif /* ZEPHYR_INCLUDE_INPUT_ANALOG_AXIS_H_ */

View file

@ -0,0 +1,33 @@
/*
* Copyright 2023 Google LLC
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_INCLUDE_INPUT_ANALOG_AXIS_SETTINGS_H_
#define ZEPHYR_INCLUDE_INPUT_ANALOG_AXIS_SETTINGS_H_
#include <stdint.h>
#include <zephyr/device.h>
/**
* @addtogroup input_analog_axis
* @{
*/
/**
* @brief Save the calibration data.
*
* Save the calibration data permanently on the specifided device, requires the
* the @ref settings subsystem to be configured and initialized.
*
* @param dev Analog axis device.
*
* @retval 0 If successful.
* @retval -errno In case of any other error.
*/
int analog_axis_calibration_save(const struct device *dev);
/** @} */
#endif /* ZEPHYR_INCLUDE_INPUT_ANALOG_AXIS_SETTINGS_H_ */

View file

@ -9,6 +9,22 @@
#address-cells = <1>; #address-cells = <1>;
#size-cells = <1>; #size-cells = <1>;
test_adc: adc@adc0adc0 {
compatible = "vnd,adc";
reg = <0xadc0adc0 0x1000>;
#io-channel-cells = <1>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_VDD_1";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
};
};
test_gpio: gpio@0 { test_gpio: gpio@0 {
compatible = "vnd,gpio"; compatible = "vnd,gpio";
gpio-controller; gpio-controller;
@ -71,6 +87,20 @@
idle-timeout-ms = <200>; idle-timeout-ms = <200>;
}; };
analog_axis {
compatible = "analog-axis";
axis-x {
io-channels = <&test_adc 0>;
out-min = <(-127)>;
out-max = <127>;
out-deadzone = <8>;
in-min = <(-100)>;
in-max = <100>;
zephyr,axis = <0>;
invert;
};
};
longpress: longpress { longpress: longpress {
input = <&longpress>; input = <&longpress>;
compatible = "zephyr,input-longpress"; compatible = "zephyr,input-longpress";

View file

@ -19,3 +19,8 @@ tests:
drivers.input.kbd_16_bit: drivers.input.kbd_16_bit:
extra_configs: extra_configs:
- CONFIG_INPUT_KBD_MATRIX_16_BIT_ROW=y - CONFIG_INPUT_KBD_MATRIX_16_BIT_ROW=y
drivers.input.analog_axis:
extra_configs:
- CONFIG_ADC=y
- CONFIG_SETTINGS=y