/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "drivers/vibe.h" #include "board/board.h" #include "console/prompt.h" #include "drivers/gpio.h" #include "drivers/periph_config.h" #include "drivers/pmic.h" #include "drivers/pwm.h" #include "drivers/timer.h" #include "kernel/util/stop.h" #include "system/logging.h" #include "system/passert.h" #include "util/math.h" #include "services/common/analytics/analytics.h" #include "services/common/battery/battery_monitor.h" #include "services/common/battery/battery_state.h" #include "services/common/analytics/analytics.h" #include //! Make a resolution of 100. Working in integer duty cycles on the following ranges: //! //! For a 2-direction, rotating vibe (PWM actuates an H-Bridge): //! [0, 49] : Full-strength reverse rotation to zero-strength reverse rotation. //! 50 : No rotation strength //! [51, 100] : Zero-strength forward rotation to full-strength forward rotation. //! //! For a 1-direction vibe: //! 0 : No vibration strength. //! [1, 100] : Zero strength vibration to full-strength vibration. //! //! This must be an even value so that a half-way point exists as an edge between an equal number of //! clock cycles on either side. #define PWM_TIMER_UPDATE_PERIOD (100) // Operating frequency of DRV2603 is in the [10, 250] kHz range. #define PWM_OUTPUT_FREQUENCY_HZ (22 * 1000) // Count clock needs to run at least as fast as the (update period * output frequency) #define PWM_TIMER_FREQUENCY_HZ (PWM_TIMER_UPDATE_PERIOD * PWM_OUTPUT_FREQUENCY_HZ) // 50% duty cycle means not vibrating. #define PWM_DUTY_CYCLE_OFF (PWM_TIMER_UPDATE_PERIOD / 2) #define PWM_DUTY_CYCLE_FULL (PWM_TIMER_UPDATE_PERIOD) static uint8_t s_vibe_duty_cycle = PWM_DUTY_CYCLE_FULL; static bool s_initialized = false; void vibe_init(void) { if (s_initialized) { return; } periph_config_acquire_lock(); if (BOARD_CONFIG_VIBE.options & ActuatorOptions_Ctl) { gpio_output_init(&BOARD_CONFIG_VIBE.ctl, GPIO_OType_PP, GPIO_Speed_2MHz); gpio_output_set(&BOARD_CONFIG_VIBE.ctl, false); s_initialized = true; } if (BOARD_CONFIG_VIBE.options & ActuatorOptions_Pwm) { pwm_init(&BOARD_CONFIG_VIBE.pwm, PWM_TIMER_UPDATE_PERIOD, PWM_TIMER_FREQUENCY_HZ); s_initialized = true; } if (BOARD_CONFIG_VIBE.options & ActuatorOptions_HBridge) { PBL_ASSERTN(BOARD_CONFIG_VIBE.options & ActuatorOptions_Pwm); } periph_config_release_lock(); } //! Enables / disables the PWM timer used for vibe control. //! Note: assumes the timer peripheral is enabled static void prv_vibe_pwm_enable(bool on) { pwm_enable(&BOARD_CONFIG_VIBE.pwm, on); static bool stop_mode_disabled = false; if (stop_mode_disabled != on) { if (on) { stop_mode_disable(InhibitorVibes); } else { stop_mode_enable(InhibitorVibes); } stop_mode_disabled = on; } } static uint16_t prv_get_vsys_mv(void) { if (battery_get_charge_state().is_plugged) { // Plugged in, use Vsys rather than Vbat return pmic_get_vsys(); } // Not plugged in, use latest battery reading return battery_state_get_voltage(); } static uint32_t prv_vibe_get_pwm_duty_cycle(int8_t strength) { if (BOARD_CONFIG_VIBE.options & ActuatorOptions_HBridge) { // Scale from -100..100 (strength) to 0..100 (duty cycle) return ((PWM_DUTY_CYCLE_FULL - PWM_DUTY_CYCLE_OFF) * strength / VIBE_STRENGTH_MAX) + PWM_DUTY_CYCLE_OFF; } else { // Treat "reverse" rotation strength as if it were "forward" strength. uint32_t duty_cycle = ABS(strength); // Scale the duty cycle given the current battery voltage if (BOARD_CONFIG_VIBE.vsys_scale > 0) { const uint16_t vsys_mv = prv_get_vsys_mv(); PBL_ASSERTN(vsys_mv > 0); duty_cycle = (BOARD_CONFIG_VIBE.vsys_scale * duty_cycle) / vsys_mv; } return duty_cycle; } } static void prv_vibe_raw_ctl(bool on) { if (BOARD_CONFIG_VIBE.options & ActuatorOptions_Pwm) { const uint32_t duty_cycle = (on) ? s_vibe_duty_cycle : PWM_DUTY_CYCLE_OFF; prv_vibe_pwm_enable(on); pwm_set_duty_cycle(&BOARD_CONFIG_VIBE.pwm, s_vibe_duty_cycle); } if (BOARD_CONFIG_VIBE.options & ActuatorOptions_Ctl) { gpio_output_set(&BOARD_CONFIG_VIBE.ctl, on); } } void vibe_set_strength(int8_t strength) { uint8_t duty_cycle = prv_vibe_get_pwm_duty_cycle(strength); s_vibe_duty_cycle = MIN(duty_cycle, PWM_DUTY_CYCLE_FULL); } void vibe_ctl(bool on) { if (!s_initialized) { return; } if (on && battery_monitor_critical_lockout()) { on = false; } static bool s_on = false; if (on && !s_on) { analytics_inc(ANALYTICS_DEVICE_METRIC_VIBRATOR_ON_COUNT, AnalyticsClient_System); analytics_stopwatch_start(ANALYTICS_APP_METRIC_VIBRATOR_ON_TIME, AnalyticsClient_App); analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_VIBRATOR_ON_TIME, AnalyticsClient_System); } else if (!on && s_on) { analytics_stopwatch_stop(ANALYTICS_APP_METRIC_VIBRATOR_ON_TIME); analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_VIBRATOR_ON_TIME); } s_on = on; PBL_LOG(LOG_LEVEL_DEBUG, "Vibe status <%s>", on ? "on" : "off"); prv_vibe_raw_ctl(on); } void vibe_force_off(void) { if (!s_initialized) { return; } prv_vibe_raw_ctl(false); } int8_t vibe_get_braking_strength(void) { if (BOARD_CONFIG_VIBE.options & ActuatorOptions_HBridge) { // We support the full -100..100 range, send it all the way backwards return VIBE_STRENGTH_MIN; } else { // We only support the 0..100 range, just ask it to turn off return VIBE_STRENGTH_OFF; } } void command_vibe_ctl(const char *arg) { int strength = atoi(arg); const bool out_of_bounds = ((strength < 0) || (strength > VIBE_STRENGTH_MAX)); const bool not_a_number = (strength == 0 && arg[0] != '0'); if (out_of_bounds || not_a_number) { prompt_send_response("Invalid argument"); return; } vibe_set_strength(strength); const bool turn_on = strength != 0; vibe_ctl(turn_on); prompt_send_response("OK"); }