| /* |
| * Copyright 2022 The Chromium OS Authors |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT zephyr_usb_c_vbus_adc |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(usbc_vbus_adc, CONFIG_USBC_LOG_LEVEL); |
| |
| #include <zephyr/device.h> |
| #include <zephyr/sys/util.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/drivers/adc.h> |
| #include <zephyr/drivers/usb_c/usbc_pd.h> |
| #include <zephyr/drivers/usb_c/usbc_vbus.h> |
| #include <soc.h> |
| #include <stddef.h> |
| |
| #include "usbc_vbus_adc_priv.h" |
| |
| /** |
| * @brief Reads and returns VBUS measured in mV |
| * |
| * @retval 0 on success |
| * @retval -EIO on failure |
| */ |
| static int adc_vbus_measure(const struct device *dev, int *meas) |
| { |
| const struct usbc_vbus_config *const config = dev->config; |
| struct usbc_vbus_data *data = dev->data; |
| int value; |
| int ret; |
| |
| __ASSERT(meas != NULL, "ADC VBUS meas must not be NULL"); |
| |
| ret = adc_read(config->adc_channel.dev, &data->sequence); |
| if (ret != 0) { |
| LOG_INF("ADC reading failed with error %d.", ret); |
| return ret; |
| } |
| |
| value = data->sample; |
| ret = adc_raw_to_millivolts_dt(&config->adc_channel, &value); |
| if (ret != 0) { |
| LOG_INF("Scaling ADC failed with error %d.", ret); |
| return ret; |
| } |
| |
| if (config->full_ohm > 0) { |
| /* VBUS is scaled down though a voltage divider */ |
| value = (value * 1000) / ((config->output_ohm * 1000) / config->full_ohm); |
| } |
| *meas = value; |
| |
| return 0; |
| } |
| |
| /** |
| * @brief Checks if VBUS is at a particular level |
| * |
| * @retval true if VBUS is at the level voltage, else false |
| */ |
| static bool adc_vbus_check_level(const struct device *dev, |
| enum tc_vbus_level level) |
| { |
| int meas; |
| int ret; |
| |
| ret = adc_vbus_measure(dev, &meas); |
| if (ret) { |
| return false; |
| } |
| |
| switch (level) { |
| case TC_VBUS_SAFE0V: |
| return (meas < PD_V_SAFE_0V_MAX_MV); |
| case TC_VBUS_PRESENT: |
| return (meas >= PD_V_SAFE_5V_MIN_MV); |
| case TC_VBUS_REMOVED: |
| return (meas < TC_V_SINK_DISCONNECT_MAX_MV); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * @brief Sets pin to discharge VBUS |
| * |
| * @retval 0 on success |
| * @retval -EIO on failure |
| * @retval -ENOENT if enable pin isn't defined |
| */ |
| static int adc_vbus_discharge(const struct device *dev, |
| bool enable) |
| { |
| const struct usbc_vbus_config *const config = dev->config; |
| const struct gpio_dt_spec *gcd = &config->discharge_gpios; |
| int ret = -ENOENT; |
| |
| if (gcd->port) { |
| ret = gpio_pin_set_dt(gcd, enable); |
| } |
| return ret; |
| } |
| |
| /** |
| * @brief Sets pin to enable VBUS measurments |
| * |
| * @retval 0 on success |
| * @retval -EIO on failure |
| * @retval -ENOENT if enable pin isn't defined |
| */ |
| static int adc_vbus_enable(const struct device *dev, |
| bool enable) |
| { |
| const struct usbc_vbus_config *const config = dev->config; |
| const struct gpio_dt_spec *gcp = &config->power_gpios; |
| int ret = -ENOENT; |
| |
| if (gcp->port) { |
| ret = gpio_pin_set_dt(gcp, enable); |
| } |
| return ret; |
| } |
| |
| /** |
| * @brief Initializes the ADC VBUS Driver |
| * |
| * @retval 0 on success |
| * @retval -EIO on failure |
| */ |
| static int adc_vbus_init(const struct device *dev) |
| { |
| const struct usbc_vbus_config *const config = dev->config; |
| struct usbc_vbus_data *data = dev->data; |
| const struct gpio_dt_spec *gcp = &config->power_gpios; |
| const struct gpio_dt_spec *gcd = &config->discharge_gpios; |
| int ret; |
| |
| /* Configure VBUS Measurement enable pin if defined */ |
| if (gcp->port) { |
| ret = device_is_ready(gcp->port); |
| if (ret < 0) { |
| LOG_ERR("%s: device not ready", gcp->port->name); |
| return ret; |
| } |
| ret = gpio_pin_configure_dt(gcp, GPIO_OUTPUT_INACTIVE); |
| if (ret < 0) { |
| LOG_ERR("Failed to control feed %s.%u: %d", |
| gcp->port->name, gcp->pin, ret); |
| return ret; |
| } |
| } |
| |
| /* Configure VBUS Discharge pin if defined */ |
| if (gcd->port) { |
| ret = device_is_ready(gcd->port); |
| if (ret == false) { |
| LOG_ERR("%s: device not ready", gcd->port->name); |
| return ret; |
| } |
| ret = gpio_pin_configure_dt(gcd, GPIO_OUTPUT_INACTIVE); |
| if (ret < 0) { |
| LOG_ERR("Failed to control feed %s.%u: %d", |
| gcd->port->name, gcd->pin, ret); |
| return ret; |
| } |
| |
| } |
| |
| data->sequence.buffer = &data->sample; |
| data->sequence.buffer_size = sizeof(data->sample); |
| |
| ret = adc_channel_setup_dt(&config->adc_channel); |
| if (ret < 0) { |
| LOG_INF("Could not setup channel (%d)\n", ret); |
| return ret; |
| } |
| |
| ret = adc_sequence_init_dt(&config->adc_channel, &data->sequence); |
| if (ret < 0) { |
| LOG_INF("Could not init sequence (%d)\n", ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static const struct usbc_vbus_driver_api driver_api = { |
| .measure = adc_vbus_measure, |
| .check_level = adc_vbus_check_level, |
| .discharge = adc_vbus_discharge, |
| .enable = adc_vbus_enable |
| }; |
| |
| BUILD_ASSERT(DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) > 0, |
| "No compatible USB-C VBUS Measurement instance found"); |
| |
| #define DRIVER_INIT(inst) \ |
| static struct usbc_vbus_data drv_data_##inst; \ |
| static const struct usbc_vbus_config drv_config_##inst = { \ |
| .output_ohm = DT_INST_PROP(inst, output_ohms), \ |
| .full_ohm = DT_INST_PROP_OR(inst, full_ohms, 0), \ |
| .adc_channel = ADC_DT_SPEC_INST_GET(inst), \ |
| .discharge_gpios = GPIO_DT_SPEC_INST_GET_OR(inst, discharge_gpios, {}), \ |
| .power_gpios = GPIO_DT_SPEC_INST_GET_OR(inst, power_gpios, {}), \ |
| }; \ |
| DEVICE_DT_INST_DEFINE(inst, \ |
| &adc_vbus_init, \ |
| NULL, \ |
| &drv_data_##inst, \ |
| &drv_config_##inst, \ |
| POST_KERNEL, \ |
| CONFIG_USBC_INIT_PRIORITY, \ |
| &driver_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(DRIVER_INIT) |