diff --git a/doc/hardware/peripherals/haptics.rst b/doc/hardware/peripherals/haptics.rst new file mode 100644 index 0000000000000..337d792eda883 --- /dev/null +++ b/doc/hardware/peripherals/haptics.rst @@ -0,0 +1,26 @@ +.. _haptics_api: + +Haptics +####### + +Overview +******** + +The haptics API allows for the control of haptic driver devices for the +purposes of performing haptic feedback events. + +During a haptic feedback event the haptic device drives a signal to an +actuator. The source of the haptic event signal varies depending on the +capabilities of the haptic device. + +Some examples of haptic signal sources are analog signals, preprogrammed +(ROM) wavetables, synthesized (RAM) wavetables, and digital audio streams. + +Additionally, haptic driver devices often offer controls for adjusting and +tuning the drive signal to meet the electrical requirements of their respective +actuators. + +API Reference +************* + +.. doxygengroup:: haptics_interface diff --git a/doc/hardware/peripherals/index.rst b/doc/hardware/peripherals/index.rst index e3e85254e6c92..aad9e9e781085 100644 --- a/doc/hardware/peripherals/index.rst +++ b/doc/hardware/peripherals/index.rst @@ -31,6 +31,7 @@ Peripherals fuel_gauge.rst gnss.rst gpio.rst + haptics.rst hwinfo.rst i2c_eeprom_target.rst i3c.rst diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index 9b6ebd22057eb..a0c7d2cb88fa8 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -42,6 +42,7 @@ add_subdirectory_ifdef(CONFIG_FPGA fpga) add_subdirectory_ifdef(CONFIG_FUEL_GAUGE fuel_gauge) add_subdirectory_ifdef(CONFIG_GNSS gnss) add_subdirectory_ifdef(CONFIG_GPIO gpio) +add_subdirectory_ifdef(CONFIG_HAPTICS haptics) add_subdirectory_ifdef(CONFIG_HWINFO hwinfo) add_subdirectory_ifdef(CONFIG_HWSPINLOCK hwspinlock) add_subdirectory_ifdef(CONFIG_I2C i2c) diff --git a/drivers/Kconfig b/drivers/Kconfig index 5c0029d78a739..cfa8d08b6a246 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -35,6 +35,7 @@ source "drivers/fpga/Kconfig" source "drivers/fuel_gauge/Kconfig" source "drivers/gnss/Kconfig" source "drivers/gpio/Kconfig" +source "drivers/haptics/Kconfig" source "drivers/hwinfo/Kconfig" source "drivers/hwspinlock/Kconfig" source "drivers/i2c/Kconfig" diff --git a/drivers/haptics/CMakeLists.txt b/drivers/haptics/CMakeLists.txt new file mode 100644 index 0000000000000..ca81bffad6b26 --- /dev/null +++ b/drivers/haptics/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() +zephyr_syscall_header(${ZEPHYR_BASE}/include/zephyr/drivers/haptics.h) + +zephyr_library_sources_ifdef(CONFIG_HAPTICS_DRV2605 drv2605.c) +zephyr_library_sources_ifdef(CONFIG_USERSPACE haptics_handlers.c) diff --git a/drivers/haptics/Kconfig b/drivers/haptics/Kconfig new file mode 100644 index 0000000000000..4a932d9b21279 --- /dev/null +++ b/drivers/haptics/Kconfig @@ -0,0 +1,24 @@ +# Copyright 2024 Cirrus Logic, Inc. +# +# SPDX-License-Identifier: Apache-2.0 + +menuconfig HAPTICS + bool "Haptic feedback drivers" + help + Enable haptics driver configuration. + +if HAPTICS + +module = HAPTICS +module-str = haptics +source "subsys/logging/Kconfig.template.log_config" + +config HAPTICS_INIT_PRIORITY + int "Haptic driver init priority" + default 90 + help + Haptic driver initialization priority. + +source "drivers/haptics/Kconfig.drv2605" + +endif # HAPTICS diff --git a/drivers/haptics/Kconfig.drv2605 b/drivers/haptics/Kconfig.drv2605 new file mode 100644 index 0000000000000..6a6b89aee0a3f --- /dev/null +++ b/drivers/haptics/Kconfig.drv2605 @@ -0,0 +1,11 @@ +# Copyright 2024 Cirrus Logic, Inc. +# SPDX-License-Identifier: Apache-2.0 + +config HAPTICS_DRV2605 + bool "DRV2605 Haptics Driver" + default y + depends on DT_HAS_TI_DRV2605_ENABLED + select I2C + help + Enable I2C-based driver for the Texas Instruments DRV2605 Haptics + Driver. diff --git a/drivers/haptics/drv2605.c b/drivers/haptics/drv2605.c new file mode 100644 index 0000000000000..3427894bec919 --- /dev/null +++ b/drivers/haptics/drv2605.c @@ -0,0 +1,621 @@ +/* + * Copyright 2024 Cirrus Logic, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + * + * DRV2605 Datasheet: https://www.ti.com/lit/gpn/drv2605 + */ + +#define DT_DRV_COMPAT ti_drv2605 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(DRV2605, CONFIG_HAPTICS_LOG_LEVEL); + +#define DRV2605_REG_STATUS 0x0 +#define DRV2605_DEVICE_ID GENMASK(7, 5) +#define DRV2605_DEVICE_ID_DRV2605 0x3 +#define DRV2605_DEVICE_ID_DRV2605L 0x7 +#define DRV2605_DIAG_RESULT BIT(3) +#define DRV2605_FB_STS BIT(2) +#define DRV2605_OVER_TEMP BIT(1) +#define DRV2605_OC_DETECT BIT(0) + +#define DRV2605_REG_MODE 0x1 +#define DRV2605_DEV_RESET BIT(7) +#define DRV2605_STANDBY BIT(6) +#define DRV2605_MODE GENMASK(2, 0) + +#define DRV2605_REG_RT_PLAYBACK_INPUT 0x2 + +#define DRV2605_REG_UNNAMED 0x3 +#define DRV2605_HI_Z_OUTPUT BIT(4) +#define DRV2605_LIBRARY_SEL GENMASK(2, 0) + +#define DRV2605_REG_WAVEFORM_SEQUENCER 0x4 +#define DRV2605_WAIT BIT(7) +#define DRV2605_WAV_FRM_SEQ GENMASK(6, 0) + +#define DRV2605_REG_GO 0xc +#define DRV2605_GO BIT(0) + +#define DRV2605_REG_OVERDRIVE_TIME_OFFSET 0xd + +#define DRV2605_REG_SUSTAIN_TIME_OFFSET_POS 0xe + +#define DRV2605_REG_SUSTAIN_TIME_OFFSET_NEG 0xf + +#define DRV2605_REG_BRAKE_TIME_OFFSET 0x10 + +#define DRV2605_TIME_STEP_MS 5 + +#define DRV2605_REG_AUDIO_TO_VIBE_CONTROL 0x11 +#define DRV2605_ATH_PEAK_TIME GENMASK(3, 2) +#define DRV2605_ATH_FILTER GENMASK(1, 0) + +#define DRV2605_REG_AUDIO_TO_VIBE_MIN_INPUT_LEVEL 0x12 + +#define DRV2605_REG_AUDIO_TO_VIBE_MAX_INPUT_LEVEL 0x13 + +#define DRV2605_ATH_INPUT_STEP_UV (1800000 / 255) + +#define DRV2605_REG_AUDIO_TO_VIBE_MIN_OUTPUT_DRIVE 0x14 + +#define DRV2605_REG_AUDIO_TO_VIBE_MAX_OUTPUT_DRIVE 0x15 + +#define DRV2605_ATH_OUTPUT_DRIVE_PCT (100 * 255) + +#define DRV2605_REG_RATED_VOLTAGE 0x16 + +#define DRV2605_REG_OVERDRIVE_CLAMP_VOLTAGE 0x17 + +#define DRV2605_REG_AUTO_CAL_COMP_RESULT 0x18 + +#define DRV2605_REG_AUTO_CAL_BACK_EMF_RESULT 0x19 + +#define DRV2605_REG_FEEDBACK_CONTROL 0x1a +#define DRV2605_N_ERM_LRA BIT(7) +#define DRV2605_FB_BRAKE_FACTOR GENMASK(6, 4) +#define DRV2605_LOOP_GAIN GENMASK(3, 2) +#define DRV2605_BEMF_GAIN GENMASK(1, 0) + +#define DRV2605_REG_CONTROL1 0x1b +#define DRV2605_STARTUP_BOOST BIT(7) +#define DRV2605_AC_COUPLE BIT(5) +#define DRV2605_DRIVE_TIME GENMASK(4, 0) + +#define DRV2605_REG_CONTROL2 0x1c +#define DRV2605_BIDIR_INPUT BIT(7) +#define DRV2605_BRAKE_STABILIZER BIT(6) +#define DRV2605_SAMPLE_TIME GENMASK(5, 4) +#define DRV2605_BLANKING_TIME GENMASK(3, 2) +#define DRV2605_IDISS_TIME GENMASK(1, 0) + +#define DRV2605_REG_CONTROL3 0x1d +#define DRV2605_NG_THRESH GENMASK(7, 6) +#define DRV2605_ERM_OPEN_LOOP BIT(5) +#define DRV2605_SUPPLY_COMP_DIS BIT(4) +#define DRV2605_DATA_FORMAT_RTP BIT(3) +#define DRV2605_LRA_DRIVE_MODE BIT(2) +#define DRV2605_N_PWM_ANALOG BIT(1) +#define DRV2605_LRA_OPEN_LOOP BIT(0) + +#define DRV2605_REG_CONTROL4 0x1e +#define DRV2605_ZERO_CROSSING_TIME GENMASK(7, 6) +#define DRV2605_AUTO_CAL_TIME GENMASK(5, 4) +#define DRV2605_OTP_STATUS BIT(2) +#define DRV2605_OTP_PROGRAM BIT(0) + +#define DRV2605_REG_BATT_VOLTAGE_MONITOR 0x21 +#define DRV2605_VBAT_STEP_UV (5600000 / 255) + +#define DRV2605_REG_LRA_RESONANCE_PERIOD 0x22 + +#define DRV2605_POWER_UP_DELAY_US 250 + +enum drv2605_pm_state { + DRV2605_PM_STATE_SHUTDOWN, + DRV2605_PM_STATE_STANDBY, + DRV2605_PM_STATE_ACTIVE, +}; + +struct drv2605_config { + struct i2c_dt_spec i2c; + struct gpio_dt_spec en_gpio; + struct gpio_dt_spec in_trig_gpio; + uint8_t feedback_brake_factor; + uint8_t loop_gain; + uint8_t rated_voltage; + uint8_t overdrive_clamp_voltage; + uint8_t auto_cal_time; + uint8_t drive_time; + bool actuator_mode; +}; + +struct drv2605_data { + const struct device *dev; + struct k_work rtp_work; + const struct drv2605_rtp_data *rtp_data; + enum drv2605_mode mode; +}; + +static inline int drv2605_haptic_config_audio(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + struct drv2605_data *data = dev->data; + int ret; + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_CONTROL3, DRV2605_N_PWM_ANALOG, + DRV2605_N_PWM_ANALOG); + if (ret < 0) { + return ret; + } + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_CONTROL1, DRV2605_AC_COUPLE, + DRV2605_AC_COUPLE); + if (ret < 0) { + return ret; + } + + data->mode = DRV2605_MODE_AUDIO_TO_VIBE; + + return 0; +} + +static inline int drv2605_haptic_config_pwm_analog(const struct device *dev, const bool analog) +{ + const struct drv2605_config *config = dev->config; + struct drv2605_data *data = dev->data; + uint8_t value = 0; + int ret; + + if (analog) { + value = DRV2605_N_PWM_ANALOG; + } + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_CONTROL3, DRV2605_N_PWM_ANALOG, + value); + if (ret < 0) { + return ret; + } + + data->mode = DRV2605_MODE_PWM_ANALOG_INPUT; + + return 0; +} + +static void drv2605_rtp_work_handler(struct k_work *work) +{ + struct drv2605_data *data = CONTAINER_OF(work, struct drv2605_data, rtp_work); + const struct drv2605_rtp_data *rtp_data = data->rtp_data; + const struct drv2605_config *config = data->dev->config; + int i; + + for (i = 0; i < rtp_data->size; i++) { + i2c_reg_write_byte_dt(&config->i2c, DRV2605_REG_RT_PLAYBACK_INPUT, + rtp_data->rtp_input[i]); + k_usleep(rtp_data->rtp_hold_us[i]); + } +} + +static inline int drv2605_haptic_config_rtp(const struct device *dev, + const struct drv2605_rtp_data *rtp_data) +{ + const struct drv2605_config *config = dev->config; + struct drv2605_data *data = dev->data; + int ret; + + data->rtp_data = rtp_data; + + ret = i2c_reg_write_byte_dt(&config->i2c, DRV2605_REG_RT_PLAYBACK_INPUT, 0); + if (ret < 0) { + return ret; + } + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_MODE, + (uint8_t)DRV2605_MODE_RTP); + if (ret < 0) { + return ret; + } + + data->mode = DRV2605_MODE_RTP; + + return 0; +} + +static inline int drv2605_haptic_config_rom(const struct device *dev, + const struct drv2605_rom_data *rom_data) +{ + const struct drv2605_config *config = dev->config; + uint8_t reg_addr = DRV2605_REG_WAVEFORM_SEQUENCER; + struct drv2605_data *data = dev->data; + int i, ret; + + switch (rom_data->trigger) { + case DRV2605_MODE_INTERNAL_TRIGGER: + case DRV2605_MODE_EXTERNAL_EDGE_TRIGGER: + case DRV2605_MODE_EXTERNAL_LEVEL_TRIGGER: + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_MODE, + (uint8_t)rom_data->trigger); + if (ret < 0) { + return ret; + } + + data->mode = rom_data->trigger; + break; + default: + return -EINVAL; + } + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_UNNAMED, DRV2605_LIBRARY_SEL, + (uint8_t)rom_data->library); + if (ret < 0) { + return ret; + } + + for (i = 0; i < DRV2605_WAVEFORM_SEQUENCER_MAX; i++) { + ret = i2c_reg_write_byte_dt(&config->i2c, reg_addr, rom_data->seq_regs[i]); + if (ret < 0) { + return ret; + } + + reg_addr++; + + if (rom_data->seq_regs[i] == 0U) { + break; + } + } + + ret = i2c_reg_write_byte_dt(&config->i2c, DRV2605_REG_OVERDRIVE_TIME_OFFSET, + rom_data->overdrive_time); + if (ret < 0) { + return ret; + } + + ret = i2c_reg_write_byte_dt(&config->i2c, DRV2605_REG_SUSTAIN_TIME_OFFSET_POS, + rom_data->sustain_pos_time); + if (ret < 0) { + return ret; + } + + ret = i2c_reg_write_byte_dt(&config->i2c, DRV2605_REG_SUSTAIN_TIME_OFFSET_NEG, + rom_data->sustain_neg_time); + if (ret < 0) { + return ret; + } + + ret = i2c_reg_write_byte_dt(&config->i2c, DRV2605_REG_BRAKE_TIME_OFFSET, + rom_data->brake_time); + if (ret < 0) { + return ret; + } + + return 0; +} + +int drv2605_haptic_config(const struct device *dev, enum drv2605_haptics_source source, + const union drv2605_config_data *config_data) +{ + switch (source) { + case DRV2605_HAPTICS_SOURCE_ROM: + return drv2605_haptic_config_rom(dev, config_data->rom_data); + case DRV2605_HAPTICS_SOURCE_RTP: + return drv2605_haptic_config_rtp(dev, config_data->rtp_data); + case DRV2605_HAPTICS_SOURCE_AUDIO: + return drv2605_haptic_config_audio(dev); + case DRV2605_HAPTICS_SOURCE_PWM: + return drv2605_haptic_config_pwm_analog(dev, false); + case DRV2605_HAPTICS_SOURCE_ANALOG: + return drv2605_haptic_config_pwm_analog(dev, true); + default: + return -ENOTSUP; + } + + return 0; +} + +static inline int drv2605_edge_mode_event(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + int ret; + + ret = gpio_pin_set_dt(&config->in_trig_gpio, 1); + if (ret < 0) { + return ret; + } + + return gpio_pin_set_dt(&config->in_trig_gpio, 0); +} + +static int drv2605_stop_output(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + struct drv2605_data *data = dev->data; + uint8_t value; + int ret; + + switch (data->mode) { + case DRV2605_MODE_DIAGNOSTICS: + case DRV2605_MODE_AUTO_CAL: + ret = i2c_reg_read_byte_dt(&config->i2c, DRV2605_REG_GO, &value); + if (ret < 0) { + return ret; + } + + if (FIELD_GET(DRV2605_GO, value)) { + LOG_DBG("Playback mode: %d is uninterruptible", data->mode); + return -EBUSY; + } + + break; + case DRV2605_MODE_INTERNAL_TRIGGER: + return i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_GO, DRV2605_GO, 0); + case DRV2605_MODE_EXTERNAL_EDGE_TRIGGER: + return drv2605_edge_mode_event(dev); + case DRV2605_MODE_EXTERNAL_LEVEL_TRIGGER: + return gpio_pin_set_dt(&config->in_trig_gpio, 0); + case DRV2605_MODE_PWM_ANALOG_INPUT: + case DRV2605_MODE_AUDIO_TO_VIBE: + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_MODE, + (uint8_t)DRV2605_MODE_INTERNAL_TRIGGER); + if (ret < 0) { + return ret; + } + + return i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_GO, DRV2605_GO, 0); + case DRV2605_MODE_RTP: + return k_work_cancel(&data->rtp_work); + default: + return -ENOTSUP; + } + + return 0; +} + +static int drv2605_start_output(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + struct drv2605_data *data = dev->data; + + switch (data->mode) { + case DRV2605_MODE_DIAGNOSTICS: + case DRV2605_MODE_AUTO_CAL: + case DRV2605_MODE_INTERNAL_TRIGGER: + return i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_GO, DRV2605_GO, DRV2605_GO); + case DRV2605_MODE_EXTERNAL_EDGE_TRIGGER: + return drv2605_edge_mode_event(dev); + case DRV2605_MODE_EXTERNAL_LEVEL_TRIGGER: + return gpio_pin_set_dt(&config->in_trig_gpio, 1); + case DRV2605_MODE_AUDIO_TO_VIBE: + case DRV2605_MODE_PWM_ANALOG_INPUT: + return i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_MODE, + (uint8_t)data->mode); + case DRV2605_MODE_RTP: + return k_work_submit(&data->rtp_work); + default: + return -ENOTSUP; + } + + return 0; +} + +#ifdef CONFIG_PM_DEVICE +static int drv2605_pm_action(const struct device *dev, enum pm_device_action action) +{ + const struct drv2605_config *config = dev->config; + + switch (action) { + case PM_DEVICE_ACTION_RESUME: + return i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_STANDBY, 0); + case PM_DEVICE_ACTION_SUSPEND: + return i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_STANDBY, + DRV2605_STANDBY); + case PM_DEVICE_ACTION_TURN_OFF: + if (config->en_gpio.port != NULL) { + gpio_pin_set_dt(&config->en_gpio, 0); + } + + break; + case PM_DEVICE_ACTION_TURN_ON: + if (config->en_gpio.port != NULL) { + gpio_pin_set_dt(&config->en_gpio, 1); + } + + break; + default: + return -ENOTSUP; + } + + return 0; +} +#endif + +static int drv2605_hw_config(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + uint8_t mask, value; + int ret; + + value = FIELD_PREP(DRV2605_N_ERM_LRA, config->actuator_mode) | + FIELD_PREP(DRV2605_FB_BRAKE_FACTOR, config->feedback_brake_factor) | + FIELD_PREP(DRV2605_LOOP_GAIN, config->loop_gain); + + mask = DRV2605_N_ERM_LRA | DRV2605_FB_BRAKE_FACTOR | DRV2605_LOOP_GAIN; + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_FEEDBACK_CONTROL, mask, value); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int drv2605_reset(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + int retries = 5, ret; + uint8_t value; + + i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_STANDBY, 0); + + ret = i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_DEV_RESET, + DRV2605_DEV_RESET); + if (ret < 0) { + return ret; + } + + k_msleep(100); + + while (retries > 0) { + i2c_reg_read_byte_dt(&config->i2c, DRV2605_REG_MODE, &value); + + if ((value & DRV2605_DEV_RESET) == 0U) { + i2c_reg_update_byte_dt(&config->i2c, DRV2605_REG_MODE, DRV2605_STANDBY, 0); + return 0; + } + + retries--; + + k_usleep(10000); + } + + return -ETIMEDOUT; +} + +static int drv2605_check_devid(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + uint8_t value; + int ret; + + ret = i2c_reg_read_byte_dt(&config->i2c, DRV2605_REG_STATUS, &value); + if (ret < 0) { + return ret; + } + + value = FIELD_GET(DRV2605_DEVICE_ID, value); + + switch (value) { + case DRV2605_DEVICE_ID_DRV2605: + case DRV2605_DEVICE_ID_DRV2605L: + break; + default: + LOG_ERR("Invalid device ID found"); + return -ENOTSUP; + } + + LOG_DBG("Found DRV2605, DEVID: 0x%x", value); + + return 0; +} + +static int drv2605_gpio_config(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + int ret; + + if (config->en_gpio.port != NULL) { + if (!gpio_is_ready_dt(&config->en_gpio)) { + return -ENODEV; + } + + ret = gpio_pin_configure_dt(&config->en_gpio, GPIO_OUTPUT_ACTIVE); + if (ret < 0) { + return ret; + } + } + + if (config->in_trig_gpio.port != NULL) { + if (!gpio_is_ready_dt(&config->in_trig_gpio)) { + return -ENODEV; + } + + ret = gpio_pin_configure_dt(&config->in_trig_gpio, GPIO_OUTPUT_INACTIVE); + if (ret < 0) { + return ret; + } + } + + return 0; +} + +static int drv2605_init(const struct device *dev) +{ + const struct drv2605_config *config = dev->config; + struct drv2605_data *data = dev->data; + int ret; + + data->dev = dev; + + k_usleep(DRV2605_POWER_UP_DELAY_US); + + if (!i2c_is_ready_dt(&config->i2c)) { + return -ENODEV; + } + + k_work_init(&data->rtp_work, drv2605_rtp_work_handler); + + ret = drv2605_gpio_config(dev); + if (ret < 0) { + LOG_DBG("Failed to allocate GPIOs: %d", ret); + return ret; + } + + ret = drv2605_check_devid(dev); + if (ret < 0) { + return ret; + } + + ret = drv2605_reset(dev); + if (ret < 0) { + LOG_DBG("Failed to reset device: %d", ret); + return ret; + } + + ret = drv2605_hw_config(dev); + if (ret < 0) { + LOG_DBG("Failed to configure device: %d", ret); + return ret; + } + + return 0; +} + +static const struct haptics_driver_api drv2605_driver_api = { + .start_output = &drv2605_start_output, + .stop_output = &drv2605_stop_output, +}; + +#define HAPTICS_DRV2605_DEFINE(inst) \ + \ + static const struct drv2605_config drv2605_config_##inst = { \ + .i2c = I2C_DT_SPEC_INST_GET(inst), \ + .en_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, ti_en_gpios, {}), \ + .in_trig_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, ti_in_trig_gpios, {}), \ + .feedback_brake_factor = DT_INST_ENUM_IDX_OR(inst, ti_feedback_brake_factor, 3), \ + .loop_gain = DT_INST_ENUM_IDX_OR(inst, ti_loop_gain, 2), \ + .actuator_mode = DT_INST_ENUM_IDX_OR(inst, ti_actuator_mode, 0), \ + }; \ + \ + static struct drv2605_data drv2605_data_##inst = { \ + .mode = DRV2605_MODE_INTERNAL_TRIGGER, \ + }; \ + \ + PM_DEVICE_DT_INST_DEFINE(inst, drv2605_pm_action); \ + \ + DEVICE_DT_INST_DEFINE(inst, drv2605_init, PM_DEVICE_DT_INST_GET(inst), \ + &drv2605_data_##inst, &drv2605_config_##inst, POST_KERNEL, \ + CONFIG_HAPTICS_INIT_PRIORITY, &drv2605_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(HAPTICS_DRV2605_DEFINE) diff --git a/drivers/haptics/haptics_handlers.c b/drivers/haptics/haptics_handlers.c new file mode 100644 index 0000000000000..ad83f28d9fa6d --- /dev/null +++ b/drivers/haptics/haptics_handlers.c @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Cirrus Logic, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +static inline int z_vrfy_haptics_start_output(const struct device *dev) +{ + Z_OOPS(Z_SYSCALL_DRIVER_HAPTICS(dev, start_output)); + + return z_impl_haptics_start_output(dev); +} + +#include + +static inline int z_vrfy_haptics_stop_output(const struct device *dev) +{ + Z_OOPS(Z_SYSCALL_DRIVER_HAPTICS(dev, stop_output)); + + z_impl_haptics_stop_output(dev); +} + +#include diff --git a/dts/bindings/haptics/ti,drv2605.yaml b/dts/bindings/haptics/ti,drv2605.yaml new file mode 100644 index 0000000000000..0b71cc007a63e --- /dev/null +++ b/dts/bindings/haptics/ti,drv2605.yaml @@ -0,0 +1,35 @@ +# Copyright (c) 2024 Cirrus Logic, Inc. +# SPDX-License-Identifier: Apache-2.0 + +description: | + DRV2605 Haptic Driver for ERM and LRA with built-in library and smart-loop + architecture. + +compatible: "ti,drv2605" + +include: i2c-device.yaml + +properties: + actuator-mode: + type: string + enum: + - "ERM" + - "LRA" + feedback-brake-factor: + type: string + enum: + - "1X" + - "2X" + - "3X" + - "4X" + - "6X" + - "8X" + - "16X" + - "DISABLED" + loop-gain: + type: string + enum: + - "LOW" + - "MEDIUM" + - "HIGH" + - "VERY_HIGH" diff --git a/include/zephyr/drivers/haptics.h b/include/zephyr/drivers/haptics.h new file mode 100644 index 0000000000000..bc76959f549cf --- /dev/null +++ b/include/zephyr/drivers/haptics.h @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Cirrus Logic, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_DRIVERS_HAPTICS_H_ +#define ZEPHYR_INCLUDE_DRIVERS_HAPTICS_H_ + +/** + * @brief Haptics Interface + * @defgroup haptics_interface Haptics Interface + * @ingroup io_interfaces + * @{ + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @typedef haptics_stop_output_t + * @brief Set the haptic device to stop output + * @param dev Pointer to the device structure for haptic device instance + */ +typedef int (*haptics_stop_output_t)(const struct device *dev); + +/** + * @typedef haptics_start_output_t + * @brief Set the haptic device to start output for a playback event + */ +typedef int (*haptics_start_output_t)(const struct device *dev); + +/** + * @brief Haptic device API + */ +__subsystem struct haptics_driver_api { + haptics_start_output_t start_output; + haptics_stop_output_t stop_output; +}; + +/** + * @brief Set the haptic device to start output for a playback event + * + * @param dev Pointer to the device structure for haptic device instance + * + * @retval 0 if successful + * @retval <0 if failed + */ +__syscall int haptics_start_output(const struct device *dev); + +static inline int z_impl_haptics_start_output(const struct device *dev) +{ + const struct haptics_driver_api *api = (const struct haptics_driver_api *)dev->api; + + return api->start_output(dev); +} + +/** + * @brief Set the haptic device to stop output for a playback event + * + * @param dev Pointer to the device structure for haptic device instance + * + * @retval 0 if successful + * @retval <0 if failed + */ +__syscall int haptics_stop_output(const struct device *dev); + +static inline int z_impl_haptics_stop_output(const struct device *dev) +{ + const struct haptics_driver_api *api = (const struct haptics_driver_api *)dev->api; + + return api->stop_output(dev); +} + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#include + +#endif /* ZEPHYR_INCLUDE_DRIVERS_HAPTICS_H_ */ diff --git a/include/zephyr/drivers/haptics/drv2605.h b/include/zephyr/drivers/haptics/drv2605.h new file mode 100644 index 0000000000000..ada27d096dfd7 --- /dev/null +++ b/include/zephyr/drivers/haptics/drv2605.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Cirrus Logic, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_DRIVERS_HAPTICS_DRV2605_H_ +#define ZEPHYR_INCLUDE_DRIVERS_HAPTICS_DRV2605_H_ + +#include +#include + +#define DRV2605_WAVEFORM_SEQUENCER_MAX 8 + +enum drv2605_library { + DRV2605_LIBRARY_EMPTY = 0, + DRV2605_LIBRARY_TS2200_A, + DRV2605_LIBRARY_TS2200_B, + DRV2605_LIBRARY_TS2200_C, + DRV2605_LIBRARY_TS2200_D, + DRV2605_LIBRARY_TS2200_E, + DRV2605_LIBRARY_LRA, +}; + +enum drv2605_mode { + DRV2605_MODE_INTERNAL_TRIGGER = 0, + DRV2605_MODE_EXTERNAL_EDGE_TRIGGER, + DRV2605_MODE_EXTERNAL_LEVEL_TRIGGER, + DRV2605_MODE_PWM_ANALOG_INPUT, + DRV2605_MODE_AUDIO_TO_VIBE, + DRV2605_MODE_RTP, + DRV2605_MODE_DIAGNOSTICS, + DRV2605_MODE_AUTO_CAL, +}; + +/** + * @brief DRV2605 haptic driver signal sources + */ +enum drv2605_haptics_source { + /** The playback source is device ROM */ + DRV2605_HAPTICS_SOURCE_ROM, + /** The playback source is the RTP buffer */ + DRV2605_HAPTICS_SOURCE_RTP, + /** The playback source is audio */ + DRV2605_HAPTICS_SOURCE_AUDIO, + /** The playback source is a PWM signal */ + DRV2605_HAPTICS_SOURCE_PWM, + /** The playback source is an analog signal */ + DRV2605_HAPTICS_SOURCE_ANALOG, +}; + +struct drv2605_rom_data { + enum drv2605_mode trigger; + enum drv2605_library library; + uint8_t seq_regs[DRV2605_WAVEFORM_SEQUENCER_MAX]; + uint8_t overdrive_time; + uint8_t sustain_pos_time; + uint8_t sustain_neg_time; + uint8_t brake_time; +}; + +struct drv2605_rtp_data { + size_t size; + uint32_t *rtp_hold_us; + uint8_t *rtp_input; +}; + +union drv2605_config_data { + struct drv2605_rom_data *rom_data; + struct drv2605_rtp_data *rtp_data; +}; + +/** + * @brief Configure the DRV2605 device for a particular signal source + * + * @param dev Pointer to the device structure for haptic device instance + * @param source The type of haptic signal source desired + * @param config_data Pointer to the configuration data union for the source + * + * @retval 0 if successful + * @retval -ENOTSUP if the signal source is not supported + * @retval <0 if failed + */ +int drv2605_haptic_config(const struct device *dev, enum drv2605_haptics_source source, + const union drv2605_config_data *config_data); + +#endif diff --git a/samples/drivers/haptics/drv2605/CMakeLists.txt b/samples/drivers/haptics/drv2605/CMakeLists.txt new file mode 100644 index 0000000000000..786a1dc9fc821 --- /dev/null +++ b/samples/drivers/haptics/drv2605/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(drv2605) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/drivers/haptics/drv2605/README.rst b/samples/drivers/haptics/drv2605/README.rst new file mode 100644 index 0000000000000..4d6ca198b3432 --- /dev/null +++ b/samples/drivers/haptics/drv2605/README.rst @@ -0,0 +1,36 @@ +.. zephyr:code-sample:: drv2605 + :name: DRV2605 Haptic Driver + :relevant-api: haptics_interface + + Drive an LRA using the DRV2605 haptic driver chip. + +Overview +******** + +This sample demonstrates how to configure a ROM playback event on the DRV2605 and executes playback +of a tapping rhythmic pattern. + +Building and Running +******************** + +Build the application for the :ref:`nucleo_f401re_board` board, and connect a DRV2605 haptic driver +on the bus I2C1 at the address 0x5A. + +.. zephyr-app-commands:: + :zephyr-app: samples/drivers/haptics/drv2605 + :board: nucleo_f401re + :goals: build + :compact: + +For flashing the application, refer to the Flashing section of the :ref:`nucleo_f401re_board` board +documentation. + +.. code-block:: none + + *** Booting Zephyr OS build v3.7.0-5-g3b4f8d80850e *** + [00:00:00.103,000] main: Found DRV2605 device drv2605@5a + +References +********** + +- DRV2605 Datasheet: https://www.ti.com/lit/ds/symlink/drv2605.pdf diff --git a/samples/drivers/haptics/drv2605/boards/nucleo_f401re.overlay b/samples/drivers/haptics/drv2605/boards/nucleo_f401re.overlay new file mode 100644 index 0000000000000..324d32f6ff4cd --- /dev/null +++ b/samples/drivers/haptics/drv2605/boards/nucleo_f401re.overlay @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Cirrus Logic, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +&arduino_i2c { + status = "okay"; + clock-frequency = ; + + haptic: drv2605@5a { + compatible = "ti,drv2605"; + reg = <0x5a>; + status = "okay"; + + actuator-mode = "LRA"; + loop-gain = "HIGH"; + feedback-brake-factor = "2X"; + }; +}; diff --git a/samples/drivers/haptics/drv2605/prj.conf b/samples/drivers/haptics/drv2605/prj.conf new file mode 100644 index 0000000000000..32ef5e97454ea --- /dev/null +++ b/samples/drivers/haptics/drv2605/prj.conf @@ -0,0 +1,2 @@ +CONFIG_LOG=y +CONFIG_HAPTICS=y diff --git a/samples/drivers/haptics/drv2605/src/main.c b/samples/drivers/haptics/drv2605/src/main.c new file mode 100644 index 0000000000000..37e3369fa06a8 --- /dev/null +++ b/samples/drivers/haptics/drv2605/src/main.c @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Cirrus Logic, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_LEVEL 4 + +LOG_MODULE_REGISTER(main); + +static struct drv2605_rom_data rom_data = { + .library = DRV2605_LIBRARY_LRA, + .brake_time = 0, + .overdrive_time = 0, + .sustain_neg_time = 0, + .sustain_pos_time = 0, + .trigger = DRV2605_MODE_INTERNAL_TRIGGER, + .seq_regs[0] = 1, + .seq_regs[1] = 10 | 0x80, + .seq_regs[2] = 2, + .seq_regs[3] = 10 | 0x80, + .seq_regs[4] = 3, + .seq_regs[5] = 10 | 0x80, + .seq_regs[6] = 4, +}; + +int main(void) +{ + const struct device *dev = DEVICE_DT_GET(DT_NODELABEL(haptic)); + union drv2605_config_data config_data = {}; + + int ret; + + if (!dev) { + LOG_ERR("DRV2605 device not found"); + return -ENODEV; + } else if (!device_is_ready(dev)) { + LOG_ERR("DRV2605 device %s is not ready", dev->name); + return -EIO; + } + + LOG_INF("Found DRV2605 device %s", dev->name); + + config_data.rom_data = &rom_data; + + ret = drv2605_haptic_config(dev, DRV2605_HAPTICS_SOURCE_ROM, &config_data); + if (ret < 0) { + LOG_ERR("Failed to configure ROM event: %d", ret); + return ret; + } + + ret = haptics_start_output(dev); + if (ret < 0) { + LOG_ERR("Failed to start ROM event: %d", ret); + return ret; + } + + return 0; +} diff --git a/tests/drivers/build_all/haptics/CMakeLists.txt b/tests/drivers/build_all/haptics/CMakeLists.txt new file mode 100644 index 0000000000000..9bc0a6e2b1c59 --- /dev/null +++ b/tests/drivers/build_all/haptics/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright (c) 2024 Cirrus Logic, Inc. +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(build_all) + +target_sources(app PRIVATE src/main.c) diff --git a/tests/drivers/build_all/haptics/app.overlay b/tests/drivers/build_all/haptics/app.overlay new file mode 100644 index 0000000000000..a80b0cf280573 --- /dev/null +++ b/tests/drivers/build_all/haptics/app.overlay @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Cirrus Logic, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + + #include + +/ { + test { + #address-cells = <1>; + #size-cells = <1>; + + test_i2c: i2c@11112222 { + #address-cells = <1>; + #size-cells = <0>; + compatible = "vnd,i2c"; + reg = <0x11112222 0x1000>; + status = "okay"; + clock-frequency = ; + + #include "i2c.dtsi" + }; + }; +}; diff --git a/tests/drivers/build_all/haptics/i2c.dtsi b/tests/drivers/build_all/haptics/i2c.dtsi new file mode 100644 index 0000000000000..2038347e11ada --- /dev/null +++ b/tests/drivers/build_all/haptics/i2c.dtsi @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 Cirrus Logic, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/**************************************** + * PLEASE KEEP REG ADDRESSES SEQUENTIAL * + ***************************************/ + +drv2605@0 { + compatible = "ti,drv2605"; + reg = <0x0>; + status = "okay"; +}; diff --git a/tests/drivers/build_all/haptics/prj.conf b/tests/drivers/build_all/haptics/prj.conf new file mode 100644 index 0000000000000..db62174a0dd11 --- /dev/null +++ b/tests/drivers/build_all/haptics/prj.conf @@ -0,0 +1 @@ +CONFIG_HAPTICS=y diff --git a/tests/drivers/build_all/haptics/src/main.c b/tests/drivers/build_all/haptics/src/main.c new file mode 100644 index 0000000000000..c619685d45007 --- /dev/null +++ b/tests/drivers/build_all/haptics/src/main.c @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024 Cirrus Logic, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +int main(void) +{ + return 0; +} diff --git a/tests/drivers/build_all/haptics/testcase.yaml b/tests/drivers/build_all/haptics/testcase.yaml new file mode 100644 index 0000000000000..97d0dd67b55f8 --- /dev/null +++ b/tests/drivers/build_all/haptics/testcase.yaml @@ -0,0 +1,11 @@ +# Copyright (c) 2024 Cirrus Logic, Inc. +# SPDX-License-Identifier: Apache-2.0 + +tests: + drivers.haptics.build: + tags: + - drivers + - haptics + build_only: true + platform_allow: + - native_sim