-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
drivers: input: add 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>
- Loading branch information
1 parent
94829f4
commit 867bc3c
Showing
8 changed files
with
492 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# 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 | ||
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. | ||
|
||
endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,354 @@ | ||
/* | ||
* 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/settings/settings.h> | ||
#include <zephyr/sys/util.h> | ||
|
||
LOG_MODULE_REGISTER(analog_axis, CONFIG_INPUT_LOG_LEVEL); | ||
|
||
#define ANALOG_AXIS_SETTINGS_PATH_MAX 32 | ||
|
||
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); | ||
}; | ||
|
||
static void analog_axis_calibration_log(const struct device *dev) | ||
{ | ||
const struct analog_axis_config *cfg = dev->config; | ||
int i; | ||
|
||
for (i = 0; i < cfg->num_channels; i++) { | ||
struct analog_axis_calibration *cal = &cfg->calibration[i]; | ||
|
||
LOG_INF("%s: ch: %d min: %d max: %d deadzone: %d", | ||
dev->name, i, cal->in_min, cal->in_max, cal->out_deadzone); | ||
} | ||
} | ||
|
||
#ifdef CONFIG_SETTINGS | ||
|
||
int analog_axis_calibratation_load(const char *key, size_t len_rd, | ||
settings_read_cb read_cb, void *cb_arg) | ||
{ | ||
const struct device *dev; | ||
const struct analog_axis_config *cfg; | ||
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; | ||
} | ||
|
||
cfg = dev->config; | ||
len = read_cb(cb_arg, cfg->calibration, | ||
sizeof(struct analog_axis_calibration) * cfg->num_channels); | ||
if (len < 0) { | ||
LOG_ERR("Data restore error: %d", len); | ||
} | ||
|
||
analog_axis_calibration_log(dev); | ||
|
||
return 0; | ||
} | ||
|
||
SETTINGS_STATIC_HANDLER_DEFINE(analog_axis, "aa-cal", NULL, | ||
analog_axis_calibratation_load, NULL, NULL); | ||
|
||
int analog_axis_calibratation_save(const struct device *dev) | ||
{ | ||
const struct analog_axis_config *cfg = dev->config; | ||
struct analog_axis_data *data = dev->data; | ||
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; | ||
} | ||
|
||
k_mutex_lock(&data->cal_lock, K_FOREVER); | ||
ret = settings_save_one(path, cfg->calibration, | ||
sizeof(struct analog_axis_calibration) * cfg->num_channels); | ||
k_mutex_unlock(&data->cal_lock); | ||
|
||
return ret; | ||
} | ||
|
||
#else | ||
|
||
int analog_axis_calibratation_save(const struct device *dev) | ||
{ | ||
return -ENOSYS; | ||
} | ||
|
||
#endif /* CONFIG_SETTINGS */ | ||
|
||
int analog_axis_calibratation_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_calibratation_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 (!device_is_ready(axis_cfg->adc.dev)) { | ||
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) |
Oops, something went wrong.