diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-08-30 09:22:00 -0700 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-08-30 09:22:00 -0700 |
| commit | 034dd340b08be1f2f0477ad16131d609f9dbd53c (patch) | |
| tree | 2536f4b2d7893ccd916b5abdf3c454ad6edc03c2 /drivers/extcon | |
| download | linux-stable-034dd340b08be1f2f0477ad16131d609f9dbd53c.tar.gz linux-stable-034dd340b08be1f2f0477ad16131d609f9dbd53c.zip | |
Merge tag 'trace-v7.3-2' of git://git.kernel.org/pub/scm/linux/kernel/git/trace/linux-tracegrafted
Pull tracing fixes from Steven Rostedt:
- Fix error output of boot instance creation failure
Currently if a boot instance creation fails, instead of printing out
the name of the instance that failed, it prints "(null)". That is
because it prints "cur_str" that had already been processed by
strsep(). Print the saved name instead.
While at it, print the error code of the failure.
- Fix use-after-free for same named historgrams
Histograms can be named so that they can be used in multiple events.
But if the named histogram has a variable attached, the second event
that uses the named histogram which duplicates it and needs to free
the original after duplication leaves the old variable in place and
still visible. If another histogram uses than variable, it will use
the stale one which will try to reference the freed duplicate
histogram and crash the kernel.
Free the duplicate variables along with the duplicated histogram
data.
- Check return value of kthread_run() in event self test
The events self tests uses a kthread for testing but does not check
if it succeeded in creating a kthread. If the kthread creation were
to fail, the code will still try to call kthread_stop() on the error
returned.
- Fix race between reading trace_pipe and updating subbuffer size
If a user is reading the trace_pipe file at the same time they update
the ring buffer sub-buffer size, can cause the trace_pipe read to
read stale data. Add trace_access_lock() around updating the ring
buffer sub-buffer size.
- Fix eventfs_inode on failure path in creation of the events directory
In the creation of the "events" directory, if after allocating the
eventfs_inode a failure is detected, it calls cleanup_ei() which
calls free_ei(). The free_ei() will test if eventfs_inode being freed
has no children. It is a bug if it does. But on the failure case of
the creation of the "events" directory, the children lists have not
yet been initialized and the free will trigger a warning because
list_empty() on an uninitialized list returns false.
Move the initialization into init_ei() where it makes more sense and
makes sure that a created eventfs_inode has its lists initialized
upon creation.
- Check return value of kthread_run() in ftrace direct sample code
The sample code that shows how to use the ftrace direct calls does
not test the return of kthread_run() to see if it succeeds. Return a
failure if the kthread_run() doesn't succeed.
- Clear user events state on fork in case of alloc failure
On fork, the child gets a pointer to the parent's user events state.
It makes a copy of it then updates the child's pointer to it. But if
the allocation fails, the duplication function leaves the child with
a pointer to its parent's descriptor. When the child cleans up its
data, it will free the parent's descriptor while the parent is still
using it.
In the duplication function, set the child's user_event_mm to NULL
before testing if the allocation succeeded, and when it exits it will
not free the parent's descriptor.
- Fix retry exhaustion in simple ring buffer reader swap
simple_ring_buffer_swap_reader_page() starts with retry set to 8 and
post-decrements it only after a failed link replacement. On the final
attempt, a successful replacement leaves retry at zero, while a
failed replacement leaves it at -1.
But the check for success expects the retry value to be non-zero and
exits with an error on zero. This is the opposite result. Fix it.
- Fail nicely when the remote swap_reader_page() returns an error
Currently, if the swap_reader_page() of a remote buffer fails, it
triggers a WARN_ON_ONCE() and continues normally. Instead, have it
exit with an error and a pr_warn() print instead of a full WARNING.
* tag 'trace-v7.3-2' of git://git.kernel.org/pub/scm/linux/kernel/git/trace/linux-trace:
ring-buffer: Stop remote reader update when page swap fails
tracing: Fix retry exhaustion in simple ring buffer reader swap
tracing/user_events: Clear copied tracing state before fork duplication
samples/ftrace: Fix kthread_stop() on ERR_PTR in ftrace-direct-multi-modify
samples/ftrace: Fix kthread_stop() on ERR_PTR in ftrace-direct-modify
eventfs: Initialize ei->children and ei->list in init_ei()
tracing: Fix use-after-free in trace_pipe read on sub-buffer order change
tracing: Fix crash passing ERR_PTR to kthread_stop()
tracing: Fix use-after-free with same-name named triggers
tracing: Fix logged instance name on creation failure
Diffstat (limited to 'drivers/extcon')
31 files changed, 15741 insertions, 0 deletions
diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig new file mode 100644 index 000000000..68d9df7d2 --- /dev/null +++ b/drivers/extcon/Kconfig @@ -0,0 +1,231 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig EXTCON + tristate "External Connector Class (extcon) support" + help + Say Y here to enable external connector class (extcon) support. + This allows monitoring external connectors by userspace + via sysfs and uevent and supports external connectors with + multiple states; i.e., an extcon that may have multiple + cables attached. For example, an external connector of a device + may be used to connect an HDMI cable and a AC adaptor, and to + host USB ports. Many of 30-pin connectors including PDMI are + also good examples. + +if EXTCON + +comment "Extcon Device Drivers" + +config EXTCON_ADC_JACK + tristate "ADC Jack extcon support" + depends on IIO + help + Say Y here to enable extcon device driver based on ADC values. + +config EXTCON_AXP288 + tristate "X-Power AXP288 EXTCON support" + depends on MFD_AXP20X && USB_SUPPORT && X86 && ACPI && IOSF_MBI + select USB_ROLE_SWITCH + help + Say Y here to enable support for USB peripheral detection + and USB MUX switching by X-Power AXP288 PMIC. + +config EXTCON_FSA9480 + tristate "FSA9480 EXTCON Support" + depends on INPUT && I2C + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the Fairchild Semiconductor + FSA9480 microUSB switch and accessory detector chip. The FSA9480 is a USB + port accessory detector and switch. The FSA9480 is fully controlled using + I2C and enables USB data, stereo and mono audio, video, microphone + and UART data to use a common connector port. + +config EXTCON_GPIO + tristate "GPIO extcon support" + depends on GPIOLIB || COMPILE_TEST + help + Say Y here to enable GPIO based extcon support. Note that GPIO + extcon supports single state per extcon instance. + +config EXTCON_INTEL_INT3496 + tristate "Intel INT3496 ACPI device extcon driver" + depends on GPIOLIB && ACPI && (X86 || COMPILE_TEST) + help + Say Y here to enable extcon support for USB OTG ports controlled by + an Intel INT3496 ACPI device. + + This ACPI device is typically found on Intel Baytrail or Cherrytrail + based tablets, or other Baytrail / Cherrytrail devices. + +config EXTCON_INTEL_CHT_WC + tristate "Intel Cherrytrail Whiskey Cove PMIC extcon driver" + depends on INTEL_SOC_PMIC_CHTWC + depends on USB_SUPPORT + depends on POWER_SUPPLY + select USB_ROLE_SWITCH + help + Say Y here to enable extcon support for charger detection / control + on the Intel Cherrytrail Whiskey Cove PMIC. + +config EXTCON_INTEL_MRFLD + tristate "Intel Merrifield Basin Cove PMIC extcon driver" + depends on INTEL_SOC_PMIC_MRFLD + help + Say Y here to enable extcon support for charger detection / control + on the Intel Merrifield Basin Cove PMIC. + +config EXTCON_LC824206XA + tristate "LC824206XA extcon Support" + depends on I2C + depends on POWER_SUPPLY + help + Say Y here to enable support for the ON Semiconductor LC824206XA + microUSB switch and accessory detector chip. The LC824206XA is a USB + port accessory detector and switch. The LC824206XA is fully controlled + using I2C and enables USB data, stereo and mono audio, video, + microphone and UART data to use a common connector port. + +config EXTCON_MAX14577 + tristate "Maxim MAX14577/77836 EXTCON Support" + depends on MFD_MAX14577 + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the MUIC device of + Maxim MAX14577/77836. The MAX14577/77836 MUIC is a USB port accessory + detector and switch. + +config EXTCON_MAX3355 + tristate "Maxim MAX3355 USB OTG EXTCON Support" + depends on GPIOLIB || COMPILE_TEST + help + If you say yes here you get support for the USB OTG role detection by + MAX3355. The MAX3355 chip integrates a charge pump and comparators to + enable a system with an integrated USB OTG dual-role transceiver to + function as an USB OTG dual-role device. + +config EXTCON_MAX77693 + tristate "Maxim MAX77693 EXTCON Support" + depends on MFD_MAX77693 && INPUT + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the MUIC device of + Maxim MAX77693 PMIC. The MAX77693 MUIC is a USB port accessory + detector and switch. + +config EXTCON_MAX77843 + tristate "Maxim MAX77843 EXTCON Support" + depends on MFD_MAX77843 + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the MUIC device of + Maxim MAX77843. The MAX77843 MUIC is a USB port accessory + detector add switch. + +config EXTCON_MAX8997 + tristate "Maxim MAX8997 EXTCON Support" + depends on MFD_MAX8997 + select IRQ_DOMAIN + help + If you say yes here you get support for the MUIC device of + Maxim MAX8997 PMIC. The MAX8997 MUIC is a USB port accessory + detector and switch. + +config EXTCON_MAX14526 + tristate "Maxim MAX14526 EXTCON Support" + depends on I2C + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the Maxim MAX14526 + MUIC device. The MAX14526 MUIC is a USB port accessory + detector and switch. The MAX14526 is designed to simplify + interface requirements on portable devices by multiplexing + common inputs (USB, UART, Microphone, Stereo Audio and + Composite Video) on a single micro/mini USB connector. + +config EXTCON_PALMAS + tristate "Palmas USB EXTCON support" + depends on MFD_PALMAS + help + Say Y here to enable support for USB peripheral and USB host + detection by palmas usb. + +config EXTCON_PTN5150 + tristate "NXP PTN5150 CC LOGIC USB EXTCON support" + depends on I2C && (GPIOLIB || COMPILE_TEST) + depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH + depends on TYPEC || !TYPEC + select REGMAP_I2C + help + Say Y here to enable support for USB peripheral and USB host + detection by NXP PTN5150 CC (Configuration Channel) logic chip. + +config EXTCON_QCOM_SPMI_MISC + tristate "Qualcomm USB extcon support" + depends on ARCH_QCOM || COMPILE_TEST + help + Say Y here to enable SPMI PMIC based USB cable detection + support on Qualcomm PMICs such as PM8941. + +config EXTCON_RT8973A + tristate "Richtek RT8973A EXTCON support" + depends on I2C + select IRQ_DOMAIN + select REGMAP_I2C + select REGMAP_IRQ + help + If you say yes here you get support for the MUIC device of + Richtek RT8973A. The RT8973A is a USB port accessory detector + and switch that is optimized to protect low voltage system + from abnormal high input voltage (up to 28V). + +config EXTCON_SM5502 + tristate "Silicon Mitus SM5502/SM5504/SM5703 EXTCON support" + depends on I2C + select IRQ_DOMAIN + select REGMAP_I2C + select REGMAP_IRQ + help + If you say yes here you get support for the MUIC device of + Silicon Mitus SM5502. The SM5502 is a USB port accessory + detector and switch. + +config EXTCON_USB_GPIO + tristate "USB GPIO extcon support" + depends on GPIOLIB || COMPILE_TEST + help + Say Y here to enable GPIO based USB cable detection extcon support. + Used typically if GPIO is used for USB ID pin detection. + +config EXTCON_USBC_CROS_EC + tristate "ChromeOS Embedded Controller EXTCON support" + depends on CROS_EC + help + Say Y here to enable USB Type C cable detection extcon support when + using Chrome OS EC based USB Type-C ports. + +config EXTCON_USBC_TUSB320 + tristate "TI TUSB320 USB-C extcon support" + depends on I2C && TYPEC + select REGMAP_I2C + depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH + help + Say Y here to enable support for USB Type C cable detection extcon + support using a TUSB320. + +config EXTCON_RTK_TYPE_C + tristate "Realtek RTD SoC extcon Type-C Driver" + depends on ARCH_REALTEK || COMPILE_TEST + depends on TYPEC + select USB_COMMON + help + Say Y here to enable extcon support for USB Type C cable detection + when using the Realtek RTD SoC USB Type-C port. + The DHC (Digital Home Hub) RTD series SoC contains a type c module. + This driver will detect the status of the type-c port. + +endif diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile new file mode 100644 index 000000000..6482f2bfd --- /dev/null +++ b/drivers/extcon/Makefile @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0 + +# Makefile for external connector class (extcon) devices +# + +obj-$(CONFIG_EXTCON) += extcon-core.o +extcon-core-objs += extcon.o devres.o +obj-$(CONFIG_EXTCON_ADC_JACK) += extcon-adc-jack.o +obj-$(CONFIG_EXTCON_AXP288) += extcon-axp288.o +obj-$(CONFIG_EXTCON_FSA9480) += extcon-fsa9480.o +obj-$(CONFIG_EXTCON_GPIO) += extcon-gpio.o +obj-$(CONFIG_EXTCON_INTEL_INT3496) += extcon-intel-int3496.o +obj-$(CONFIG_EXTCON_INTEL_CHT_WC) += extcon-intel-cht-wc.o +obj-$(CONFIG_EXTCON_INTEL_MRFLD) += extcon-intel-mrfld.o +obj-$(CONFIG_EXTCON_LC824206XA) += extcon-lc824206xa.o +obj-$(CONFIG_EXTCON_MAX14577) += extcon-max14577.o +obj-$(CONFIG_EXTCON_MAX3355) += extcon-max3355.o +obj-$(CONFIG_EXTCON_MAX77693) += extcon-max77693.o +obj-$(CONFIG_EXTCON_MAX77843) += extcon-max77843.o +obj-$(CONFIG_EXTCON_MAX8997) += extcon-max8997.o +obj-$(CONFIG_EXTCON_MAX14526) += extcon-max14526.o +obj-$(CONFIG_EXTCON_PALMAS) += extcon-palmas.o +obj-$(CONFIG_EXTCON_PTN5150) += extcon-ptn5150.o +obj-$(CONFIG_EXTCON_QCOM_SPMI_MISC) += extcon-qcom-spmi-misc.o +obj-$(CONFIG_EXTCON_RT8973A) += extcon-rt8973a.o +obj-$(CONFIG_EXTCON_SM5502) += extcon-sm5502.o +obj-$(CONFIG_EXTCON_USB_GPIO) += extcon-usb-gpio.o +obj-$(CONFIG_EXTCON_USBC_CROS_EC) += extcon-usbc-cros-ec.o +obj-$(CONFIG_EXTCON_USBC_TUSB320) += extcon-usbc-tusb320.o +obj-$(CONFIG_EXTCON_RTK_TYPE_C) += extcon-rtk-type-c.o diff --git a/drivers/extcon/devres.c b/drivers/extcon/devres.c new file mode 100644 index 000000000..f9d52e8ec --- /dev/null +++ b/drivers/extcon/devres.c @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/extcon/devres.c - EXTCON device's resource management + * + * Copyright (C) 2016 Samsung Electronics + * Author: Chanwoo Choi <cw00.choi@samsung.com> + */ + +#include "extcon.h" + +static int devm_extcon_dev_match(struct device *dev, void *res, void *data) +{ + struct extcon_dev **r = res; + + if (WARN_ON(!r || !*r)) + return 0; + + return *r == data; +} + +static void devm_extcon_dev_release(struct device *dev, void *res) +{ + extcon_dev_free(*(struct extcon_dev **)res); +} + + +static void devm_extcon_dev_unreg(struct device *dev, void *res) +{ + extcon_dev_unregister(*(struct extcon_dev **)res); +} + +struct extcon_dev_notifier_devres { + struct extcon_dev *edev; + unsigned int id; + struct notifier_block *nb; +}; + +static void devm_extcon_dev_notifier_unreg(struct device *dev, void *res) +{ + struct extcon_dev_notifier_devres *this = res; + + extcon_unregister_notifier(this->edev, this->id, this->nb); +} + +static void devm_extcon_dev_notifier_all_unreg(struct device *dev, void *res) +{ + struct extcon_dev_notifier_devres *this = res; + + extcon_unregister_notifier_all(this->edev, this->nb); +} + +/** + * devm_extcon_dev_allocate - Allocate managed extcon device + * @dev: the device owning the extcon device being created + * @supported_cable: the array of the supported external connectors + * ending with EXTCON_NONE. + * + * This function manages automatically the memory of extcon device using device + * resource management and simplify the control of freeing the memory of extcon + * device. + * + * Returns the pointer memory of allocated extcon_dev if success + * or ERR_PTR(err) if fail + */ +struct extcon_dev *devm_extcon_dev_allocate(struct device *dev, + const unsigned int *supported_cable) +{ + struct extcon_dev **ptr, *edev; + + ptr = devres_alloc(devm_extcon_dev_release, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + edev = extcon_dev_allocate(supported_cable); + if (IS_ERR(edev)) { + devres_free(ptr); + return edev; + } + + edev->dev.parent = dev; + + *ptr = edev; + devres_add(dev, ptr); + + return edev; +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_allocate); + +/** + * devm_extcon_dev_free() - Resource-managed extcon_dev_unregister() + * @dev: the device owning the extcon device being created + * @edev: the extcon device to be freed + * + * Free the memory that is allocated with devm_extcon_dev_allocate() + * function. + */ +void devm_extcon_dev_free(struct device *dev, struct extcon_dev *edev) +{ + WARN_ON(devres_release(dev, devm_extcon_dev_release, + devm_extcon_dev_match, edev)); +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_free); + +/** + * devm_extcon_dev_register() - Resource-managed extcon_dev_register() + * @dev: the device owning the extcon device being created + * @edev: the extcon device to be registered + * + * this function, that extcon device is automatically unregistered on driver + * detach. Internally this function calls extcon_dev_register() function. + * To get more information, refer that function. + * + * If extcon device is registered with this function and the device needs to be + * unregistered separately, devm_extcon_dev_unregister() should be used. + * + * Returns 0 if success or negaive error number if failure. + */ +int devm_extcon_dev_register(struct device *dev, struct extcon_dev *edev) +{ + struct extcon_dev **ptr; + int ret; + + ptr = devres_alloc(devm_extcon_dev_unreg, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return -ENOMEM; + + ret = extcon_dev_register(edev); + if (ret) { + devres_free(ptr); + return ret; + } + + *ptr = edev; + devres_add(dev, ptr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_register); + +/** + * devm_extcon_dev_unregister() - Resource-managed extcon_dev_unregister() + * @dev: the device owning the extcon device being created + * @edev: the extcon device to unregistered + * + * Unregister extcon device that is registered with devm_extcon_dev_register() + * function. + */ +void devm_extcon_dev_unregister(struct device *dev, struct extcon_dev *edev) +{ + WARN_ON(devres_release(dev, devm_extcon_dev_unreg, + devm_extcon_dev_match, edev)); +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_unregister); + +/** + * devm_extcon_register_notifier() - Resource-managed extcon_register_notifier() + * @dev: the device owning the extcon device being created + * @edev: the extcon device + * @id: the unique id among the extcon enumeration + * @nb: a notifier block to be registered + * + * This function manages automatically the notifier of extcon device using + * device resource management and simplify the control of unregistering + * the notifier of extcon device. + * + * Note that the second parameter given to the callback of nb (val) is + * "old_state", not the current state. The current state can be retrieved + * by looking at the third pameter (edev pointer)'s state value. + * + * Returns 0 if success or negaive error number if failure. + */ +int devm_extcon_register_notifier(struct device *dev, struct extcon_dev *edev, + unsigned int id, struct notifier_block *nb) +{ + struct extcon_dev_notifier_devres *ptr; + int ret; + + ptr = devres_alloc(devm_extcon_dev_notifier_unreg, sizeof(*ptr), + GFP_KERNEL); + if (!ptr) + return -ENOMEM; + + ret = extcon_register_notifier(edev, id, nb); + if (ret) { + devres_free(ptr); + return ret; + } + + ptr->edev = edev; + ptr->id = id; + ptr->nb = nb; + devres_add(dev, ptr); + + return 0; +} +EXPORT_SYMBOL(devm_extcon_register_notifier); + +/** + * devm_extcon_unregister_notifier() + * - Resource-managed extcon_unregister_notifier() + * @dev: the device owning the extcon device being created + * @edev: the extcon device + * @id: the unique id among the extcon enumeration + * @nb: a notifier block to be registered + */ +void devm_extcon_unregister_notifier(struct device *dev, + struct extcon_dev *edev, unsigned int id, + struct notifier_block *nb) +{ + WARN_ON(devres_release(dev, devm_extcon_dev_notifier_unreg, + devm_extcon_dev_match, edev)); +} +EXPORT_SYMBOL(devm_extcon_unregister_notifier); + +/** + * devm_extcon_register_notifier_all() + * - Resource-managed extcon_register_notifier_all() + * @dev: the device owning the extcon device being created + * @edev: the extcon device + * @nb: a notifier block to be registered + * + * This function manages automatically the notifier of extcon device using + * device resource management and simplify the control of unregistering + * the notifier of extcon device. To get more information, refer that function. + * + * Returns 0 if success or negaive error number if failure. + */ +int devm_extcon_register_notifier_all(struct device *dev, struct extcon_dev *edev, + struct notifier_block *nb) +{ + struct extcon_dev_notifier_devres *ptr; + int ret; + + ptr = devres_alloc(devm_extcon_dev_notifier_all_unreg, sizeof(*ptr), + GFP_KERNEL); + if (!ptr) + return -ENOMEM; + + ret = extcon_register_notifier_all(edev, nb); + if (ret) { + devres_free(ptr); + return ret; + } + + ptr->edev = edev; + ptr->nb = nb; + devres_add(dev, ptr); + + return 0; +} +EXPORT_SYMBOL(devm_extcon_register_notifier_all); + +/** + * devm_extcon_unregister_notifier_all() + * - Resource-managed extcon_unregister_notifier_all() + * @dev: the device owning the extcon device being created + * @edev: the extcon device + * @nb: a notifier block to be registered + */ +void devm_extcon_unregister_notifier_all(struct device *dev, + struct extcon_dev *edev, + struct notifier_block *nb) +{ + WARN_ON(devres_release(dev, devm_extcon_dev_notifier_all_unreg, + devm_extcon_dev_match, edev)); +} +EXPORT_SYMBOL(devm_extcon_unregister_notifier_all); diff --git a/drivers/extcon/extcon-adc-jack.c b/drivers/extcon/extcon-adc-jack.c new file mode 100644 index 000000000..e735f43dc --- /dev/null +++ b/drivers/extcon/extcon-adc-jack.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/extcon/extcon-adc-jack.c + * + * Analog Jack extcon driver with ADC-based detection capability. + * + * Copyright (C) 2016 Samsung Electronics + * Chanwoo Choi <cw00.choi@samsung.com> + * + * Copyright (C) 2012 Samsung Electronics + * MyungJoo Ham <myungjoo.ham@samsung.com> + * + * Modified for calling to IIO to get adc by <anish.singh@samsung.com> + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/device.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/workqueue.h> +#include <linux/iio/consumer.h> +#include <linux/extcon/extcon-adc-jack.h> +#include <linux/extcon-provider.h> + +/** + * struct adc_jack_data - internal data for adc_jack device driver + * @dev: The device structure associated with the adc_jack. + * @edev: extcon device. + * @cable_names: list of supported cables. + * @adc_conditions: list of adc value conditions. + * @num_conditions: size of adc_conditions. + * @irq: irq number of attach/detach event (0 if not exist). + * @handling_delay: interrupt handler will schedule extcon event + * handling at handling_delay jiffies. + * @handler: extcon event handler called by interrupt handler. + * @chan: iio channel being queried. + * @wakeup_source: Indicates if the device can wake up the system. + */ +struct adc_jack_data { + struct device *dev; + struct extcon_dev *edev; + + const unsigned int **cable_names; + struct adc_jack_cond *adc_conditions; + int num_conditions; + + int irq; + unsigned long handling_delay; /* in jiffies */ + struct delayed_work handler; + + struct iio_channel *chan; + bool wakeup_source; +}; + +static void adc_jack_handler(struct work_struct *work) +{ + struct adc_jack_data *data = container_of(to_delayed_work(work), + struct adc_jack_data, + handler); + struct adc_jack_cond *def; + int ret, adc_val; + int i; + + ret = iio_read_channel_raw(data->chan, &adc_val); + if (ret < 0) { + dev_err(data->dev, "read channel() error: %d\n", ret); + return; + } + + /* Get state from adc value with adc_conditions */ + for (i = 0; i < data->num_conditions; i++) { + def = &data->adc_conditions[i]; + if (def->min_adc <= adc_val && def->max_adc >= adc_val) { + extcon_set_state_sync(data->edev, def->id, true); + return; + } + } + + /* Set the detached state if adc value is not included in the range */ + for (i = 0; i < data->num_conditions; i++) { + def = &data->adc_conditions[i]; + extcon_set_state_sync(data->edev, def->id, false); + } +} + +static irqreturn_t adc_jack_irq_thread(int irq, void *_data) +{ + struct adc_jack_data *data = _data; + + queue_delayed_work(system_power_efficient_wq, + &data->handler, data->handling_delay); + return IRQ_HANDLED; +} + +static int adc_jack_probe(struct platform_device *pdev) +{ + struct adc_jack_data *data; + struct adc_jack_pdata *pdata = dev_get_platdata(&pdev->dev); + int i, err = 0; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (!pdata->cable_names) { + dev_err(&pdev->dev, "error: cable_names not defined.\n"); + return -EINVAL; + } + + data->dev = &pdev->dev; + data->edev = devm_extcon_dev_allocate(&pdev->dev, pdata->cable_names); + if (IS_ERR(data->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + if (!pdata->adc_conditions) { + dev_err(&pdev->dev, "error: adc_conditions not defined.\n"); + return -EINVAL; + } + data->adc_conditions = pdata->adc_conditions; + + /* Check the length of array and set num_conditions */ + for (i = 0; data->adc_conditions[i].id != EXTCON_NONE; i++); + data->num_conditions = i; + + data->chan = devm_iio_channel_get(&pdev->dev, pdata->consumer_channel); + if (IS_ERR(data->chan)) + return PTR_ERR(data->chan); + + data->handling_delay = msecs_to_jiffies(pdata->handling_delay_ms); + data->wakeup_source = pdata->wakeup_source; + + INIT_DEFERRABLE_WORK(&data->handler, adc_jack_handler); + + platform_set_drvdata(pdev, data); + + err = devm_extcon_dev_register(&pdev->dev, data->edev); + if (err) + return err; + + data->irq = platform_get_irq(pdev, 0); + if (data->irq < 0) + return -ENODEV; + + err = request_any_context_irq(data->irq, adc_jack_irq_thread, + pdata->irq_flags, pdata->name, data); + + if (err < 0) { + dev_err(&pdev->dev, "error: irq %d\n", data->irq); + return err; + } + + if (data->wakeup_source) + device_init_wakeup(&pdev->dev, 1); + + adc_jack_handler(&data->handler.work); + return 0; +} + +static void adc_jack_remove(struct platform_device *pdev) +{ + struct adc_jack_data *data = platform_get_drvdata(pdev); + + if (data->wakeup_source) + device_init_wakeup(&pdev->dev, false); + free_irq(data->irq, data); + cancel_work_sync(&data->handler.work); +} + +#ifdef CONFIG_PM_SLEEP +static int adc_jack_suspend(struct device *dev) +{ + struct adc_jack_data *data = dev_get_drvdata(dev); + + cancel_delayed_work_sync(&data->handler); + if (device_may_wakeup(data->dev)) + enable_irq_wake(data->irq); + + return 0; +} + +static int adc_jack_resume(struct device *dev) +{ + struct adc_jack_data *data = dev_get_drvdata(dev); + + if (device_may_wakeup(data->dev)) + disable_irq_wake(data->irq); + + return 0; +} +#endif /* CONFIG_PM_SLEEP */ + +static SIMPLE_DEV_PM_OPS(adc_jack_pm_ops, + adc_jack_suspend, adc_jack_resume); + +static struct platform_driver adc_jack_driver = { + .probe = adc_jack_probe, + .remove = adc_jack_remove, + .driver = { + .name = "adc-jack", + .pm = &adc_jack_pm_ops, + }, +}; + +module_platform_driver(adc_jack_driver); + +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_DESCRIPTION("ADC Jack extcon driver"); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("IIO_CONSUMER"); diff --git a/drivers/extcon/extcon-axp288.c b/drivers/extcon/extcon-axp288.c new file mode 100644 index 000000000..19856ddda --- /dev/null +++ b/drivers/extcon/extcon-axp288.c @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * extcon-axp288.c - X-Power AXP288 PMIC extcon cable detection driver + * + * Copyright (c) 2017-2018 Hans de Goede <hdegoede@redhat.com> + * Copyright (C) 2015 Intel Corporation + * Author: Ramakrishna Pallala <ramakrishna.pallala@intel.com> + */ + +#include <linux/acpi.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/io.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/property.h> +#include <linux/notifier.h> +#include <linux/extcon-provider.h> +#include <linux/regmap.h> +#include <linux/mfd/axp20x.h> +#include <linux/usb/role.h> +#include <linux/workqueue.h> + +#include <asm/cpu_device_id.h> +#include <asm/intel-family.h> +#include <asm/iosf_mbi.h> + +/* Power source status register */ +#define PS_STAT_VBUS_TRIGGER BIT(0) +#define PS_STAT_BAT_CHRG_DIR BIT(2) +#define PS_STAT_VBUS_ABOVE_VHOLD BIT(3) +#define PS_STAT_VBUS_VALID BIT(4) +#define PS_STAT_VBUS_PRESENT BIT(5) + +/* BC module global register */ +#define BC_GLOBAL_RUN BIT(0) +#define BC_GLOBAL_DET_STAT BIT(2) +#define BC_GLOBAL_DBP_TOUT BIT(3) +#define BC_GLOBAL_VLGC_COM_SEL BIT(4) +#define BC_GLOBAL_DCD_TOUT_MASK (BIT(6)|BIT(5)) +#define BC_GLOBAL_DCD_TOUT_300MS 0 +#define BC_GLOBAL_DCD_TOUT_100MS 1 +#define BC_GLOBAL_DCD_TOUT_500MS 2 +#define BC_GLOBAL_DCD_TOUT_900MS 3 +#define BC_GLOBAL_DCD_DET_SEL BIT(7) + +/* BC module vbus control and status register */ +#define VBUS_CNTL_DPDM_PD_EN BIT(4) +#define VBUS_CNTL_DPDM_FD_EN BIT(5) +#define VBUS_CNTL_FIRST_PO_STAT BIT(6) + +/* BC USB status register */ +#define USB_STAT_BUS_STAT_MASK (BIT(3)|BIT(2)|BIT(1)|BIT(0)) +#define USB_STAT_BUS_STAT_SHIFT 0 +#define USB_STAT_BUS_STAT_ATHD 0 +#define USB_STAT_BUS_STAT_CONN 1 +#define USB_STAT_BUS_STAT_SUSP 2 +#define USB_STAT_BUS_STAT_CONF 3 +#define USB_STAT_USB_SS_MODE BIT(4) +#define USB_STAT_DEAD_BAT_DET BIT(6) +#define USB_STAT_DBP_UNCFG BIT(7) + +/* BC detect status register */ +#define DET_STAT_MASK (BIT(7)|BIT(6)|BIT(5)) +#define DET_STAT_SHIFT 5 +#define DET_STAT_SDP 1 +#define DET_STAT_CDP 2 +#define DET_STAT_DCP 3 + +enum axp288_extcon_reg { + AXP288_PS_STAT_REG = 0x00, + AXP288_PS_BOOT_REASON_REG = 0x02, + AXP288_BC_GLOBAL_REG = 0x2c, + AXP288_BC_VBUS_CNTL_REG = 0x2d, + AXP288_BC_USB_STAT_REG = 0x2e, + AXP288_BC_DET_STAT_REG = 0x2f, +}; + +enum axp288_extcon_irq { + VBUS_FALLING_IRQ = 0, + VBUS_RISING_IRQ, + MV_CHNG_IRQ, + BC_USB_CHNG_IRQ, + EXTCON_IRQ_END, +}; + +static const unsigned int axp288_extcon_cables[] = { + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_CDP, + EXTCON_CHG_USB_DCP, + EXTCON_USB, + EXTCON_NONE, +}; + +struct axp288_extcon_info { + struct device *dev; + struct regmap *regmap; + struct regmap_irq_chip_data *regmap_irqc; + struct usb_role_switch *role_sw; + struct work_struct role_work; + int irq[EXTCON_IRQ_END]; + struct extcon_dev *edev; + struct extcon_dev *id_extcon; + struct notifier_block id_nb; + unsigned int previous_cable; + bool vbus_attach; +}; + +static const struct x86_cpu_id cherry_trail_cpu_ids[] = { + X86_MATCH_VFM(INTEL_ATOM_AIRMONT, NULL), + {} +}; + +/* Power up/down reason string array */ +static const char * const axp288_pwr_up_down_info[] = { + "Last wake caused by user pressing the power button", + "Last wake caused by a charger insertion", + "Last wake caused by a battery insertion", + "Last wake caused by SOC initiated global reset", + "Last wake caused by cold reset", + "Last shutdown caused by PMIC UVLO threshold", + "Last shutdown caused by SOC initiated cold off", + "Last shutdown caused by user pressing the power button", +}; + +/* + * Decode and log the given "reset source indicator" (rsi) + * register and then clear it. + */ +static void axp288_extcon_log_rsi(struct axp288_extcon_info *info) +{ + unsigned int val, i, clear_mask = 0; + unsigned long bits; + int ret; + + ret = regmap_read(info->regmap, AXP288_PS_BOOT_REASON_REG, &val); + if (ret < 0) { + dev_err(info->dev, "failed to read reset source indicator\n"); + return; + } + + bits = val & GENMASK(ARRAY_SIZE(axp288_pwr_up_down_info) - 1, 0); + for_each_set_bit(i, &bits, ARRAY_SIZE(axp288_pwr_up_down_info)) + dev_dbg(info->dev, "%s\n", axp288_pwr_up_down_info[i]); + clear_mask = bits; + + /* Clear the register value for next reboot (write 1 to clear bit) */ + regmap_write(info->regmap, AXP288_PS_BOOT_REASON_REG, clear_mask); +} + +/* + * The below code to control the USB role-switch on devices with an AXP288 + * may seem out of place, but there are 2 reasons why this is the best place + * to control the USB role-switch on such devices: + * 1) On many devices the USB role is controlled by AML code, but the AML code + * only switches between the host and none roles, because of Windows not + * really using device mode. To make device mode work we need to toggle + * between the none/device roles based on Vbus presence, and this driver + * gets interrupts on Vbus insertion / removal. + * 2) In order for our BC1.2 charger detection to work properly the role + * mux must be properly set to device mode before we do the detection. + */ + +/* Returns the id-pin value, note pulled low / false == host-mode */ +static bool axp288_get_id_pin(struct axp288_extcon_info *info) +{ + enum usb_role role; + + if (info->id_extcon) + return extcon_get_state(info->id_extcon, EXTCON_USB_HOST) <= 0; + + /* We cannot access the id-pin, see what mode the AML code has set */ + role = usb_role_switch_get_role(info->role_sw); + return role != USB_ROLE_HOST; +} + +static void axp288_usb_role_work(struct work_struct *work) +{ + struct axp288_extcon_info *info = + container_of(work, struct axp288_extcon_info, role_work); + enum usb_role role; + bool id_pin; + int ret; + + id_pin = axp288_get_id_pin(info); + if (!id_pin) + role = USB_ROLE_HOST; + else if (info->vbus_attach) + role = USB_ROLE_DEVICE; + else + role = USB_ROLE_NONE; + + ret = usb_role_switch_set_role(info->role_sw, role); + if (ret) + dev_err(info->dev, "failed to set role: %d\n", ret); +} + +static bool axp288_get_vbus_attach(struct axp288_extcon_info *info) +{ + int ret, pwr_stat; + + ret = regmap_read(info->regmap, AXP288_PS_STAT_REG, &pwr_stat); + if (ret < 0) { + dev_err(info->dev, "failed to read vbus status\n"); + return false; + } + + return !!(pwr_stat & PS_STAT_VBUS_VALID); +} + +static int axp288_handle_chrg_det_event(struct axp288_extcon_info *info) +{ + int ret, stat, cfg; + u8 chrg_type; + unsigned int cable = info->previous_cable; + bool vbus_attach = false; + + ret = iosf_mbi_block_punit_i2c_access(); + if (ret < 0) + return ret; + + vbus_attach = axp288_get_vbus_attach(info); + if (!vbus_attach) + goto no_vbus; + + /* Check charger detection completion status */ + ret = regmap_read(info->regmap, AXP288_BC_GLOBAL_REG, &cfg); + if (ret < 0) + goto dev_det_ret; + if (cfg & BC_GLOBAL_DET_STAT) { + dev_dbg(info->dev, "can't complete the charger detection\n"); + goto dev_det_ret; + } + + ret = regmap_read(info->regmap, AXP288_BC_DET_STAT_REG, &stat); + if (ret < 0) + goto dev_det_ret; + + chrg_type = (stat & DET_STAT_MASK) >> DET_STAT_SHIFT; + + switch (chrg_type) { + case DET_STAT_SDP: + dev_dbg(info->dev, "sdp cable is connected\n"); + cable = EXTCON_CHG_USB_SDP; + break; + case DET_STAT_CDP: + dev_dbg(info->dev, "cdp cable is connected\n"); + cable = EXTCON_CHG_USB_CDP; + break; + case DET_STAT_DCP: + dev_dbg(info->dev, "dcp cable is connected\n"); + cable = EXTCON_CHG_USB_DCP; + break; + default: + dev_warn(info->dev, "unknown (reserved) bc detect result\n"); + cable = EXTCON_CHG_USB_SDP; + } + +no_vbus: + iosf_mbi_unblock_punit_i2c_access(); + + extcon_set_state_sync(info->edev, info->previous_cable, false); + if (info->previous_cable == EXTCON_CHG_USB_SDP) + extcon_set_state_sync(info->edev, EXTCON_USB, false); + + if (vbus_attach) { + extcon_set_state_sync(info->edev, cable, vbus_attach); + if (cable == EXTCON_CHG_USB_SDP) + extcon_set_state_sync(info->edev, EXTCON_USB, + vbus_attach); + + info->previous_cable = cable; + } + + if (info->role_sw && info->vbus_attach != vbus_attach) { + info->vbus_attach = vbus_attach; + /* Setting the role can take a while */ + queue_work(system_long_wq, &info->role_work); + } + + return 0; + +dev_det_ret: + iosf_mbi_unblock_punit_i2c_access(); + + if (ret < 0) + dev_err(info->dev, "failed to detect BC Mod\n"); + + return ret; +} + +static int axp288_extcon_id_evt(struct notifier_block *nb, + unsigned long event, void *param) +{ + struct axp288_extcon_info *info = + container_of(nb, struct axp288_extcon_info, id_nb); + + /* We may not sleep and setting the role can take a while */ + queue_work(system_long_wq, &info->role_work); + + return NOTIFY_OK; +} + +static irqreturn_t axp288_extcon_isr(int irq, void *data) +{ + struct axp288_extcon_info *info = data; + int ret; + + ret = axp288_handle_chrg_det_event(info); + if (ret < 0) + dev_err(info->dev, "failed to handle the interrupt\n"); + + return IRQ_HANDLED; +} + +static int axp288_extcon_enable(struct axp288_extcon_info *info) +{ + int ret = 0; + + ret = iosf_mbi_block_punit_i2c_access(); + if (ret < 0) + return ret; + + regmap_update_bits(info->regmap, AXP288_BC_GLOBAL_REG, + BC_GLOBAL_RUN, 0); + /* Enable the charger detection logic */ + regmap_update_bits(info->regmap, AXP288_BC_GLOBAL_REG, + BC_GLOBAL_RUN, BC_GLOBAL_RUN); + + iosf_mbi_unblock_punit_i2c_access(); + + return ret; +} + +static void axp288_put_role_sw(void *data) +{ + struct axp288_extcon_info *info = data; + + cancel_work_sync(&info->role_work); + usb_role_switch_put(info->role_sw); +} + +static int axp288_extcon_find_role_sw(struct axp288_extcon_info *info) +{ + const struct software_node *swnode; + struct fwnode_handle *fwnode; + + if (!x86_match_cpu(cherry_trail_cpu_ids)) + return 0; + + swnode = software_node_find_by_name(NULL, "intel-xhci-usb-sw"); + if (!swnode) + return -EPROBE_DEFER; + + fwnode = software_node_fwnode(swnode); + info->role_sw = usb_role_switch_find_by_fwnode(fwnode); + fwnode_handle_put(fwnode); + + return info->role_sw ? 0 : -EPROBE_DEFER; +} + +static int axp288_extcon_probe(struct platform_device *pdev) +{ + struct axp288_extcon_info *info; + struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); + struct device *dev = &pdev->dev; + struct acpi_device *adev; + int ret, i, pirq; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->regmap = axp20x->regmap; + info->regmap_irqc = axp20x->regmap_irqc; + info->previous_cable = EXTCON_NONE; + INIT_WORK(&info->role_work, axp288_usb_role_work); + info->id_nb.notifier_call = axp288_extcon_id_evt; + + platform_set_drvdata(pdev, info); + + ret = axp288_extcon_find_role_sw(info); + if (ret) + return ret; + + if (info->role_sw) { + ret = devm_add_action_or_reset(dev, axp288_put_role_sw, info); + if (ret) + return ret; + + adev = acpi_dev_get_first_match_dev("INT3496", NULL, -1); + if (adev) { + info->id_extcon = extcon_get_extcon_dev(acpi_dev_name(adev)); + acpi_dev_put(adev); + if (IS_ERR(info->id_extcon)) + return PTR_ERR(info->id_extcon); + + dev_info(dev, "controlling USB role\n"); + } else { + dev_info(dev, "controlling USB role based on Vbus presence\n"); + } + } + + ret = iosf_mbi_block_punit_i2c_access(); + if (ret < 0) + return ret; + + info->vbus_attach = axp288_get_vbus_attach(info); + + axp288_extcon_log_rsi(info); + + iosf_mbi_unblock_punit_i2c_access(); + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + axp288_extcon_cables); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + return PTR_ERR(info->edev); + } + + /* Register extcon device */ + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return ret; + } + + for (i = 0; i < EXTCON_IRQ_END; i++) { + pirq = platform_get_irq(pdev, i); + if (pirq < 0) + return pirq; + + info->irq[i] = regmap_irq_get_virq(info->regmap_irqc, pirq); + if (info->irq[i] < 0) { + dev_err(&pdev->dev, + "failed to get virtual interrupt=%d\n", pirq); + ret = info->irq[i]; + return ret; + } + + ret = devm_request_threaded_irq(&pdev->dev, info->irq[i], + NULL, axp288_extcon_isr, + IRQF_ONESHOT | IRQF_NO_SUSPEND, + pdev->name, info); + if (ret) { + dev_err(&pdev->dev, "failed to request interrupt=%d\n", + info->irq[i]); + return ret; + } + } + + if (info->id_extcon) { + ret = devm_extcon_register_notifier_all(dev, info->id_extcon, + &info->id_nb); + if (ret) + return ret; + } + + /* Make sure the role-sw is set correctly before doing BC detection */ + if (info->role_sw) { + queue_work(system_long_wq, &info->role_work); + flush_work(&info->role_work); + } + + /* Start charger cable type detection */ + ret = axp288_extcon_enable(info); + if (ret < 0) + return ret; + + devm_device_init_wakeup(dev); + platform_set_drvdata(pdev, info); + + return 0; +} + +static int __maybe_unused axp288_extcon_suspend(struct device *dev) +{ + struct axp288_extcon_info *info = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) + enable_irq_wake(info->irq[VBUS_RISING_IRQ]); + + return 0; +} + +static int __maybe_unused axp288_extcon_resume(struct device *dev) +{ + struct axp288_extcon_info *info = dev_get_drvdata(dev); + + /* + * Wakeup when a charger is connected to do charger-type + * connection and generate an extcon event which makes the + * axp288 charger driver set the input current limit. + */ + if (device_may_wakeup(dev)) + disable_irq_wake(info->irq[VBUS_RISING_IRQ]); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(axp288_extcon_pm_ops, axp288_extcon_suspend, + axp288_extcon_resume); + +static const struct platform_device_id axp288_extcon_table[] = { + { .name = "axp288_extcon" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, axp288_extcon_table); + +static struct platform_driver axp288_extcon_driver = { + .probe = axp288_extcon_probe, + .id_table = axp288_extcon_table, + .driver = { + .name = "axp288_extcon", + .pm = &axp288_extcon_pm_ops, + }, +}; +module_platform_driver(axp288_extcon_driver); + +MODULE_AUTHOR("Ramakrishna Pallala <ramakrishna.pallala@intel.com>"); +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_DESCRIPTION("X-Powers AXP288 extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-fsa9480.c b/drivers/extcon/extcon-fsa9480.c new file mode 100644 index 000000000..a031eb091 --- /dev/null +++ b/drivers/extcon/extcon-fsa9480.c @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * extcon-fsa9480.c - Fairchild Semiconductor FSA9480 extcon driver + * + * Copyright (c) 2019 Tomasz Figa <tomasz.figa@gmail.com> + * + * Loosely based on old fsa9480 misc-device driver. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/types.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/bitops.h> +#include <linux/interrupt.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/kobject.h> +#include <linux/extcon-provider.h> +#include <linux/irqdomain.h> +#include <linux/regmap.h> + +/* FSA9480 I2C registers */ +#define FSA9480_REG_DEVID 0x01 +#define FSA9480_REG_CTRL 0x02 +#define FSA9480_REG_INT1 0x03 +#define FSA9480_REG_INT2 0x04 +#define FSA9480_REG_INT1_MASK 0x05 +#define FSA9480_REG_INT2_MASK 0x06 +#define FSA9480_REG_ADC 0x07 +#define FSA9480_REG_TIMING1 0x08 +#define FSA9480_REG_TIMING2 0x09 +#define FSA9480_REG_DEV_T1 0x0a +#define FSA9480_REG_DEV_T2 0x0b +#define FSA9480_REG_BTN1 0x0c +#define FSA9480_REG_BTN2 0x0d +#define FSA9480_REG_CK 0x0e +#define FSA9480_REG_CK_INT1 0x0f +#define FSA9480_REG_CK_INT2 0x10 +#define FSA9480_REG_CK_INTMASK1 0x11 +#define FSA9480_REG_CK_INTMASK2 0x12 +#define FSA9480_REG_MANSW1 0x13 +#define FSA9480_REG_MANSW2 0x14 +#define FSA9480_REG_END 0x15 + +/* Control */ +#define CON_SWITCH_OPEN (1 << 4) +#define CON_RAW_DATA (1 << 3) +#define CON_MANUAL_SW (1 << 2) +#define CON_WAIT (1 << 1) +#define CON_INT_MASK (1 << 0) +#define CON_MASK (CON_SWITCH_OPEN | CON_RAW_DATA | \ + CON_MANUAL_SW | CON_WAIT) + +/* Device Type 1 */ +#define DEV_USB_OTG 7 +#define DEV_DEDICATED_CHG 6 +#define DEV_USB_CHG 5 +#define DEV_CAR_KIT 4 +#define DEV_UART 3 +#define DEV_USB 2 +#define DEV_AUDIO_2 1 +#define DEV_AUDIO_1 0 + +#define DEV_T1_USB_MASK (DEV_USB_OTG | DEV_USB) +#define DEV_T1_UART_MASK (DEV_UART) +#define DEV_T1_CHARGER_MASK (DEV_DEDICATED_CHG | DEV_USB_CHG) + +/* Device Type 2 */ +#define DEV_AV 14 +#define DEV_TTY 13 +#define DEV_PPD 12 +#define DEV_JIG_UART_OFF 11 +#define DEV_JIG_UART_ON 10 +#define DEV_JIG_USB_OFF 9 +#define DEV_JIG_USB_ON 8 + +#define DEV_T2_USB_MASK (DEV_JIG_USB_OFF | DEV_JIG_USB_ON) +#define DEV_T2_UART_MASK (DEV_JIG_UART_OFF | DEV_JIG_UART_ON) +#define DEV_T2_JIG_MASK (DEV_JIG_USB_OFF | DEV_JIG_USB_ON | \ + DEV_JIG_UART_OFF | DEV_JIG_UART_ON) + +/* + * Manual Switch + * D- [7:5] / D+ [4:2] + * 000: Open all / 001: USB / 010: AUDIO / 011: UART / 100: V_AUDIO + */ +#define SW_VAUDIO ((4 << 5) | (4 << 2)) +#define SW_UART ((3 << 5) | (3 << 2)) +#define SW_AUDIO ((2 << 5) | (2 << 2)) +#define SW_DHOST ((1 << 5) | (1 << 2)) +#define SW_AUTO ((0 << 5) | (0 << 2)) + +/* Interrupt 1 */ +#define INT1_MASK (0xff << 0) +#define INT_DETACH (1 << 1) +#define INT_ATTACH (1 << 0) + +/* Interrupt 2 mask */ +#define INT2_MASK (0x1f << 0) + +/* Timing Set 1 */ +#define TIMING1_ADC_500MS (0x6 << 0) + +struct fsa9480_usbsw { + struct device *dev; + struct regmap *regmap; + struct extcon_dev *edev; + u16 cable; +}; + +static const unsigned int fsa9480_extcon_cable[] = { + EXTCON_USB_HOST, + EXTCON_USB, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_ACA, + EXTCON_JACK_LINE_OUT, + EXTCON_JACK_VIDEO_OUT, + EXTCON_JIG, + + EXTCON_NONE, +}; + +static const u64 cable_types[] = { + [DEV_USB_OTG] = BIT_ULL(EXTCON_USB_HOST), + [DEV_DEDICATED_CHG] = BIT_ULL(EXTCON_USB) | BIT_ULL(EXTCON_CHG_USB_DCP), + [DEV_USB_CHG] = BIT_ULL(EXTCON_USB) | BIT_ULL(EXTCON_CHG_USB_SDP), + [DEV_CAR_KIT] = BIT_ULL(EXTCON_USB) | BIT_ULL(EXTCON_CHG_USB_SDP) + | BIT_ULL(EXTCON_JACK_LINE_OUT), + [DEV_UART] = BIT_ULL(EXTCON_JIG), + [DEV_USB] = BIT_ULL(EXTCON_USB) | BIT_ULL(EXTCON_CHG_USB_SDP), + [DEV_AUDIO_2] = BIT_ULL(EXTCON_JACK_LINE_OUT), + [DEV_AUDIO_1] = BIT_ULL(EXTCON_JACK_LINE_OUT), + [DEV_AV] = BIT_ULL(EXTCON_JACK_LINE_OUT) + | BIT_ULL(EXTCON_JACK_VIDEO_OUT), + [DEV_TTY] = BIT_ULL(EXTCON_JIG), + [DEV_PPD] = BIT_ULL(EXTCON_JACK_LINE_OUT) | BIT_ULL(EXTCON_CHG_USB_ACA), + [DEV_JIG_UART_OFF] = BIT_ULL(EXTCON_JIG), + [DEV_JIG_UART_ON] = BIT_ULL(EXTCON_JIG), + [DEV_JIG_USB_OFF] = BIT_ULL(EXTCON_USB) | BIT_ULL(EXTCON_JIG), + [DEV_JIG_USB_ON] = BIT_ULL(EXTCON_USB) | BIT_ULL(EXTCON_JIG), +}; + +/* Define regmap configuration of FSA9480 for I2C communication */ +static bool fsa9480_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case FSA9480_REG_INT1_MASK: + return true; + default: + break; + } + return false; +} + +static const struct regmap_config fsa9480_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .volatile_reg = fsa9480_volatile_reg, + .max_register = FSA9480_REG_END, +}; + +static int fsa9480_write_reg(struct fsa9480_usbsw *usbsw, int reg, int value) +{ + int ret; + + ret = regmap_write(usbsw->regmap, reg, value); + if (ret < 0) + dev_err(usbsw->dev, "%s: err %d\n", __func__, ret); + + return ret; +} + +static int fsa9480_read_reg(struct fsa9480_usbsw *usbsw, int reg) +{ + int ret, val; + + ret = regmap_read(usbsw->regmap, reg, &val); + if (ret < 0) { + dev_err(usbsw->dev, "%s: err %d\n", __func__, ret); + return ret; + } + + return val; +} + +static int fsa9480_read_irq(struct fsa9480_usbsw *usbsw, int *value) +{ + u8 regs[2]; + int ret; + + ret = regmap_bulk_read(usbsw->regmap, FSA9480_REG_INT1, regs, 2); + if (ret < 0) + dev_err(usbsw->dev, "%s: err %d\n", __func__, ret); + + *value = regs[1] << 8 | regs[0]; + return ret; +} + +static void fsa9480_handle_change(struct fsa9480_usbsw *usbsw, + u16 mask, bool attached) +{ + while (mask) { + int dev = fls64(mask) - 1; + u64 cables = cable_types[dev]; + + while (cables) { + int cable = fls64(cables) - 1; + + extcon_set_state_sync(usbsw->edev, cable, attached); + cables &= ~BIT_ULL(cable); + } + + mask &= ~BIT_ULL(dev); + } +} + +static void fsa9480_detect_dev(struct fsa9480_usbsw *usbsw) +{ + int val1, val2; + u16 val; + + val1 = fsa9480_read_reg(usbsw, FSA9480_REG_DEV_T1); + val2 = fsa9480_read_reg(usbsw, FSA9480_REG_DEV_T2); + if (val1 < 0 || val2 < 0) { + dev_err(usbsw->dev, "%s: failed to read registers", __func__); + return; + } + val = val2 << 8 | val1; + + dev_info(usbsw->dev, "dev1: 0x%x, dev2: 0x%x\n", val1, val2); + + /* handle detached cables first */ + fsa9480_handle_change(usbsw, usbsw->cable & ~val, false); + + /* then handle attached ones */ + fsa9480_handle_change(usbsw, val & ~usbsw->cable, true); + + usbsw->cable = val; +} + +static irqreturn_t fsa9480_irq_handler(int irq, void *data) +{ + struct fsa9480_usbsw *usbsw = data; + int intr = 0; + + /* clear interrupt */ + fsa9480_read_irq(usbsw, &intr); + if (!intr) + return IRQ_NONE; + + /* device detection */ + fsa9480_detect_dev(usbsw); + + return IRQ_HANDLED; +} + +static int fsa9480_probe(struct i2c_client *client) +{ + struct fsa9480_usbsw *info; + int ret; + + if (!client->irq) { + dev_err(&client->dev, "no interrupt provided\n"); + return -EINVAL; + } + + info = devm_kzalloc(&client->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + info->dev = &client->dev; + + i2c_set_clientdata(client, info); + + /* External connector */ + info->edev = devm_extcon_dev_allocate(info->dev, + fsa9480_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(info->dev, "failed to allocate memory for extcon\n"); + ret = -ENOMEM; + return ret; + } + + ret = devm_extcon_dev_register(info->dev, info->edev); + if (ret) { + dev_err(info->dev, "failed to register extcon device\n"); + return ret; + } + + info->regmap = devm_regmap_init_i2c(client, &fsa9480_regmap_config); + if (IS_ERR(info->regmap)) { + ret = PTR_ERR(info->regmap); + dev_err(info->dev, "failed to allocate register map: %d\n", + ret); + return ret; + } + + /* ADC Detect Time: 500ms */ + fsa9480_write_reg(info, FSA9480_REG_TIMING1, TIMING1_ADC_500MS); + + /* configure automatic switching */ + fsa9480_write_reg(info, FSA9480_REG_CTRL, CON_MASK); + + /* unmask interrupt (attach/detach only) */ + fsa9480_write_reg(info, FSA9480_REG_INT1_MASK, + INT1_MASK & ~(INT_ATTACH | INT_DETACH)); + fsa9480_write_reg(info, FSA9480_REG_INT2_MASK, INT2_MASK); + + ret = devm_request_threaded_irq(info->dev, client->irq, NULL, + fsa9480_irq_handler, + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + "fsa9480", info); + if (ret) { + dev_err(info->dev, "failed to request IRQ\n"); + return ret; + } + + devm_device_init_wakeup(info->dev); + fsa9480_detect_dev(info); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int fsa9480_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + if (device_may_wakeup(&client->dev) && client->irq) + enable_irq_wake(client->irq); + + return 0; +} + +static int fsa9480_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + if (device_may_wakeup(&client->dev) && client->irq) + disable_irq_wake(client->irq); + + return 0; +} +#endif + +static const struct dev_pm_ops fsa9480_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(fsa9480_suspend, fsa9480_resume) +}; + +static const struct i2c_device_id fsa9480_id[] = { + { "fsa9480" }, + {} +}; +MODULE_DEVICE_TABLE(i2c, fsa9480_id); + +static const struct of_device_id fsa9480_of_match[] = { + { .compatible = "fcs,fsa9480", }, + { .compatible = "fcs,fsa880", }, + { .compatible = "ti,tsu6111", }, + { }, +}; +MODULE_DEVICE_TABLE(of, fsa9480_of_match); + +static struct i2c_driver fsa9480_i2c_driver = { + .driver = { + .name = "fsa9480", + .pm = &fsa9480_pm_ops, + .of_match_table = fsa9480_of_match, + }, + .probe = fsa9480_probe, + .id_table = fsa9480_id, +}; + +static int __init fsa9480_module_init(void) +{ + return i2c_add_driver(&fsa9480_i2c_driver); +} +subsys_initcall(fsa9480_module_init); + +static void __exit fsa9480_module_exit(void) +{ + i2c_del_driver(&fsa9480_i2c_driver); +} +module_exit(fsa9480_module_exit); + +MODULE_DESCRIPTION("Fairchild Semiconductor FSA9480 extcon driver"); +MODULE_AUTHOR("Tomasz Figa <tomasz.figa@gmail.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-gpio.c b/drivers/extcon/extcon-gpio.c new file mode 100644 index 000000000..4105df74f --- /dev/null +++ b/drivers/extcon/extcon-gpio.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * extcon_gpio.c - Single-state GPIO extcon driver based on extcon class + * + * Copyright (C) 2008 Google, Inc. + * Author: Mike Lockwood <lockwood@android.com> + * + * Modified by MyungJoo Ham <myungjoo.ham@samsung.com> to support extcon + * (originally switch class is supported) + */ + +#include <linux/devm-helpers.h> +#include <linux/extcon-provider.h> +#include <linux/gpio/consumer.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +/** + * struct gpio_extcon_data - A simple GPIO-controlled extcon device state container. + * @edev: Extcon device. + * @work: Work fired by the interrupt. + * @debounce_jiffies: Number of jiffies to wait for the GPIO to stabilize, from the debounce + * value. + * @gpiod: GPIO descriptor for this external connector. + * @extcon_id: The unique id of specific external connector. + * @debounce: Debounce time for GPIO IRQ in ms. + * @check_on_resume: Boolean describing whether to check the state of gpio + * while resuming from sleep. + */ +struct gpio_extcon_data { + struct extcon_dev *edev; + struct delayed_work work; + unsigned long debounce_jiffies; + struct gpio_desc *gpiod; + unsigned int extcon_id; + unsigned long debounce; + bool check_on_resume; +}; + +static void gpio_extcon_work(struct work_struct *work) +{ + int state; + struct gpio_extcon_data *data = + container_of(to_delayed_work(work), struct gpio_extcon_data, + work); + + state = gpiod_get_value_cansleep(data->gpiod); + extcon_set_state_sync(data->edev, data->extcon_id, state); +} + +static irqreturn_t gpio_irq_handler(int irq, void *dev_id) +{ + struct gpio_extcon_data *data = dev_id; + + queue_delayed_work(system_power_efficient_wq, &data->work, + data->debounce_jiffies); + return IRQ_HANDLED; +} + +static int gpio_extcon_probe(struct platform_device *pdev) +{ + struct gpio_extcon_data *data; + struct device *dev = &pdev->dev; + unsigned long irq_flags; + int irq; + int ret; + + data = devm_kzalloc(dev, sizeof(struct gpio_extcon_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + /* + * FIXME: extcon_id represents the unique identifier of external + * connectors such as EXTCON_USB, EXTCON_DISP_HDMI and so on. extcon_id + * is necessary to register the extcon device. But, it's not yet + * developed to get the extcon id from device-tree or others. + * On later, it have to be solved. + */ + if (data->extcon_id > EXTCON_NONE) + return -EINVAL; + + data->gpiod = devm_gpiod_get(dev, "extcon", GPIOD_IN); + if (IS_ERR(data->gpiod)) + return PTR_ERR(data->gpiod); + irq = gpiod_to_irq(data->gpiod); + if (irq <= 0) + return irq; + + /* + * It is unlikely that this is an acknowledged interrupt that goes + * away after handling, what we are looking for are falling edges + * if the signal is active low, and rising edges if the signal is + * active high. + */ + if (gpiod_is_active_low(data->gpiod)) + irq_flags = IRQF_TRIGGER_FALLING; + else + irq_flags = IRQF_TRIGGER_RISING; + + /* Allocate the memory of extcon devie and register extcon device */ + data->edev = devm_extcon_dev_allocate(dev, &data->extcon_id); + if (IS_ERR(data->edev)) { + dev_err(dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + ret = devm_extcon_dev_register(dev, data->edev); + if (ret < 0) + return ret; + + ret = devm_delayed_work_autocancel(dev, &data->work, gpio_extcon_work); + if (ret) + return ret; + + /* + * Request the interrupt of gpio to detect whether external connector + * is attached or detached. + */ + ret = devm_request_any_context_irq(dev, irq, + gpio_irq_handler, irq_flags, + pdev->name, data); + if (ret < 0) + return ret; + + platform_set_drvdata(pdev, data); + /* Perform initial detection */ + gpio_extcon_work(&data->work.work); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int gpio_extcon_resume(struct device *dev) +{ + struct gpio_extcon_data *data; + + data = dev_get_drvdata(dev); + if (data->check_on_resume) + queue_delayed_work(system_power_efficient_wq, + &data->work, data->debounce_jiffies); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(gpio_extcon_pm_ops, NULL, gpio_extcon_resume); + +static struct platform_driver gpio_extcon_driver = { + .probe = gpio_extcon_probe, + .driver = { + .name = "extcon-gpio", + .pm = &gpio_extcon_pm_ops, + }, +}; + +module_platform_driver(gpio_extcon_driver); + +MODULE_AUTHOR("Mike Lockwood <lockwood@android.com>"); +MODULE_DESCRIPTION("GPIO extcon driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-intel-cht-wc.c b/drivers/extcon/extcon-intel-cht-wc.c new file mode 100644 index 000000000..99a9dfc62 --- /dev/null +++ b/drivers/extcon/extcon-intel-cht-wc.c @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Extcon charger detection driver for Intel Cherrytrail Whiskey Cove PMIC + * Copyright (C) 2017 Hans de Goede <hdegoede@redhat.com> + * + * Based on various non upstream patches to support the CHT Whiskey Cove PMIC: + * Copyright (C) 2013-2015 Intel Corporation. All rights reserved. + */ + +#include <linux/extcon-provider.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/mfd/intel_soc_pmic.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/regmap.h> +#include <linux/regulator/consumer.h> +#include <linux/slab.h> +#include <linux/usb/role.h> + +#include "extcon-intel.h" + +#define CHT_WC_PHYCTRL 0x5e07 + +#define CHT_WC_CHGRCTRL0 0x5e16 +#define CHT_WC_CHGRCTRL0_CHGRRESET BIT(0) +#define CHT_WC_CHGRCTRL0_EMRGCHREN BIT(1) +#define CHT_WC_CHGRCTRL0_EXTCHRDIS BIT(2) +#define CHT_WC_CHGRCTRL0_SWCONTROL BIT(3) +#define CHT_WC_CHGRCTRL0_TTLCK BIT(4) +#define CHT_WC_CHGRCTRL0_CCSM_OFF BIT(5) +#define CHT_WC_CHGRCTRL0_DBPOFF BIT(6) +#define CHT_WC_CHGRCTRL0_CHR_WDT_NOKICK BIT(7) + +#define CHT_WC_CHGRCTRL1 0x5e17 +#define CHT_WC_CHGRCTRL1_FUSB_INLMT_100 BIT(0) +#define CHT_WC_CHGRCTRL1_FUSB_INLMT_150 BIT(1) +#define CHT_WC_CHGRCTRL1_FUSB_INLMT_500 BIT(2) +#define CHT_WC_CHGRCTRL1_FUSB_INLMT_900 BIT(3) +#define CHT_WC_CHGRCTRL1_FUSB_INLMT_1500 BIT(4) +#define CHT_WC_CHGRCTRL1_FTEMP_EVENT BIT(5) +#define CHT_WC_CHGRCTRL1_OTGMODE BIT(6) +#define CHT_WC_CHGRCTRL1_DBPEN BIT(7) + +#define CHT_WC_USBSRC 0x5e29 +#define CHT_WC_USBSRC_STS_MASK GENMASK(1, 0) +#define CHT_WC_USBSRC_STS_SUCCESS 2 +#define CHT_WC_USBSRC_STS_FAIL 3 +#define CHT_WC_USBSRC_TYPE_SHIFT 2 +#define CHT_WC_USBSRC_TYPE_MASK GENMASK(5, 2) +#define CHT_WC_USBSRC_TYPE_NONE 0 +#define CHT_WC_USBSRC_TYPE_SDP 1 +#define CHT_WC_USBSRC_TYPE_DCP 2 +#define CHT_WC_USBSRC_TYPE_CDP 3 +#define CHT_WC_USBSRC_TYPE_ACA 4 +#define CHT_WC_USBSRC_TYPE_SE1 5 +#define CHT_WC_USBSRC_TYPE_MHL 6 +#define CHT_WC_USBSRC_TYPE_FLOATING 7 +#define CHT_WC_USBSRC_TYPE_OTHER 8 +#define CHT_WC_USBSRC_TYPE_DCP_EXTPHY 9 + +#define CHT_WC_CHGDISCTRL 0x5e2f +#define CHT_WC_CHGDISCTRL_OUT BIT(0) +/* 0 - open drain, 1 - regular push-pull output */ +#define CHT_WC_CHGDISCTRL_DRV BIT(4) +/* 0 - pin is controlled by SW, 1 - by HW */ +#define CHT_WC_CHGDISCTRL_FN BIT(6) + +#define CHT_WC_PWRSRC_IRQ 0x6e03 +#define CHT_WC_PWRSRC_IRQ_MASK 0x6e0f +#define CHT_WC_PWRSRC_STS 0x6e1e +#define CHT_WC_PWRSRC_VBUS BIT(0) +#define CHT_WC_PWRSRC_DC BIT(1) +#define CHT_WC_PWRSRC_BATT BIT(2) +#define CHT_WC_PWRSRC_USBID_MASK GENMASK(4, 3) +#define CHT_WC_PWRSRC_USBID_SHIFT 3 +#define CHT_WC_PWRSRC_RID_ACA 0 +#define CHT_WC_PWRSRC_RID_GND 1 +#define CHT_WC_PWRSRC_RID_FLOAT 2 + +#define CHT_WC_VBUS_GPIO_CTLO 0x6e2d +#define CHT_WC_VBUS_GPIO_CTLO_OUTPUT BIT(0) +#define CHT_WC_VBUS_GPIO_CTLO_DRV_OD BIT(4) +#define CHT_WC_VBUS_GPIO_CTLO_DIR_OUT BIT(5) + +enum cht_wc_mux_select { + MUX_SEL_PMIC = 0, + MUX_SEL_SOC, +}; + +static const unsigned int cht_wc_extcon_cables[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_CDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_ACA, + EXTCON_NONE, +}; + +struct cht_wc_extcon_data { + struct device *dev; + struct regmap *regmap; + struct extcon_dev *edev; + struct usb_role_switch *role_sw; + struct regulator *vbus_boost; + struct power_supply *psy; + enum power_supply_usb_type usb_type; + unsigned int previous_cable; + bool usb_host; + bool vbus_boost_enabled; +}; + +static int cht_wc_extcon_get_id(struct cht_wc_extcon_data *ext, int pwrsrc_sts) +{ + switch ((pwrsrc_sts & CHT_WC_PWRSRC_USBID_MASK) >> CHT_WC_PWRSRC_USBID_SHIFT) { + case CHT_WC_PWRSRC_RID_GND: + return INTEL_USB_ID_GND; + case CHT_WC_PWRSRC_RID_FLOAT: + return INTEL_USB_ID_FLOAT; + /* + * According to the spec. we should read the USB-ID pin ADC value here + * to determine the resistance of the used pull-down resister and then + * return RID_A / RID_B / RID_C based on this. But all "Accessory + * Charger Adapter"s (ACAs) which users can actually buy always use + * a combination of a charging port with one or more USB-A ports, so + * they should always use a resistor indicating RID_A. But the spec + * is hard to read / badly-worded so some of them actually indicate + * they are a RID_B ACA evnen though they clearly are a RID_A ACA. + * To workaround this simply always return INTEL_USB_RID_A, which + * matches all the ACAs which users can actually buy. + */ + case CHT_WC_PWRSRC_RID_ACA: + return INTEL_USB_RID_A; + default: + return INTEL_USB_ID_FLOAT; + } +} + +static int cht_wc_extcon_get_charger(struct cht_wc_extcon_data *ext, + bool ignore_errors) +{ + int ret, usbsrc, status; + unsigned long timeout; + + /* Charger detection can take upto 600ms, wait 800ms max. */ + timeout = jiffies + msecs_to_jiffies(800); + do { + ret = regmap_read(ext->regmap, CHT_WC_USBSRC, &usbsrc); + if (ret) { + dev_err(ext->dev, "Error reading usbsrc: %d\n", ret); + return ret; + } + + status = usbsrc & CHT_WC_USBSRC_STS_MASK; + if (status == CHT_WC_USBSRC_STS_SUCCESS || + status == CHT_WC_USBSRC_STS_FAIL) + break; + + msleep(50); /* Wait a bit before retrying */ + } while (time_before(jiffies, timeout)); + + if (status != CHT_WC_USBSRC_STS_SUCCESS) { + if (!ignore_errors) { + if (status == CHT_WC_USBSRC_STS_FAIL) + dev_warn(ext->dev, "Could not detect charger type\n"); + else + dev_warn(ext->dev, "Timeout detecting charger type\n"); + } + + /* Safe fallback */ + usbsrc = CHT_WC_USBSRC_TYPE_SDP << CHT_WC_USBSRC_TYPE_SHIFT; + } + + usbsrc = (usbsrc & CHT_WC_USBSRC_TYPE_MASK) >> CHT_WC_USBSRC_TYPE_SHIFT; + switch (usbsrc) { + default: + dev_warn(ext->dev, + "Unhandled charger type %d, defaulting to SDP\n", + ret); + ext->usb_type = POWER_SUPPLY_USB_TYPE_SDP; + return EXTCON_CHG_USB_SDP; + case CHT_WC_USBSRC_TYPE_SDP: + case CHT_WC_USBSRC_TYPE_FLOATING: + case CHT_WC_USBSRC_TYPE_OTHER: + ext->usb_type = POWER_SUPPLY_USB_TYPE_SDP; + return EXTCON_CHG_USB_SDP; + case CHT_WC_USBSRC_TYPE_CDP: + ext->usb_type = POWER_SUPPLY_USB_TYPE_CDP; + return EXTCON_CHG_USB_CDP; + case CHT_WC_USBSRC_TYPE_DCP: + case CHT_WC_USBSRC_TYPE_DCP_EXTPHY: + case CHT_WC_USBSRC_TYPE_MHL: /* MHL2+ delivers upto 2A, treat as DCP */ + ext->usb_type = POWER_SUPPLY_USB_TYPE_DCP; + return EXTCON_CHG_USB_DCP; + case CHT_WC_USBSRC_TYPE_ACA: + ext->usb_type = POWER_SUPPLY_USB_TYPE_ACA; + return EXTCON_CHG_USB_ACA; + } +} + +static void cht_wc_extcon_set_phymux(struct cht_wc_extcon_data *ext, u8 state) +{ + int ret; + + ret = regmap_write(ext->regmap, CHT_WC_PHYCTRL, state); + if (ret) + dev_err(ext->dev, "Error writing phyctrl: %d\n", ret); +} + +static void cht_wc_extcon_set_5v_boost(struct cht_wc_extcon_data *ext, + bool enable) +{ + int ret, val; + + /* + * The 5V boost converter is enabled through a gpio on the PMIC, since + * there currently is no gpio driver we access the gpio reg directly. + */ + val = CHT_WC_VBUS_GPIO_CTLO_DRV_OD | CHT_WC_VBUS_GPIO_CTLO_DIR_OUT; + if (enable) + val |= CHT_WC_VBUS_GPIO_CTLO_OUTPUT; + + ret = regmap_write(ext->regmap, CHT_WC_VBUS_GPIO_CTLO, val); + if (ret) + dev_err(ext->dev, "Error writing Vbus GPIO CTLO: %d\n", ret); +} + +static void cht_wc_extcon_set_otgmode(struct cht_wc_extcon_data *ext, + bool enable) +{ + unsigned int val = enable ? CHT_WC_CHGRCTRL1_OTGMODE : 0; + int ret; + + ret = regmap_update_bits(ext->regmap, CHT_WC_CHGRCTRL1, + CHT_WC_CHGRCTRL1_OTGMODE, val); + if (ret) + dev_err(ext->dev, "Error updating CHGRCTRL1 reg: %d\n", ret); + + if (ext->vbus_boost && ext->vbus_boost_enabled != enable) { + if (enable) + ret = regulator_enable(ext->vbus_boost); + else + ret = regulator_disable(ext->vbus_boost); + + if (ret) + dev_err(ext->dev, "Error updating Vbus boost regulator: %d\n", ret); + else + ext->vbus_boost_enabled = enable; + } +} + +static void cht_wc_extcon_enable_charging(struct cht_wc_extcon_data *ext, + bool enable) +{ + unsigned int val = enable ? 0 : CHT_WC_CHGDISCTRL_OUT; + int ret; + + ret = regmap_update_bits(ext->regmap, CHT_WC_CHGDISCTRL, + CHT_WC_CHGDISCTRL_OUT, val); + if (ret) + dev_err(ext->dev, "Error updating CHGDISCTRL reg: %d\n", ret); +} + +/* Small helper to sync EXTCON_CHG_USB_SDP and EXTCON_USB state */ +static void cht_wc_extcon_set_state(struct cht_wc_extcon_data *ext, + unsigned int cable, bool state) +{ + extcon_set_state_sync(ext->edev, cable, state); + if (cable == EXTCON_CHG_USB_SDP) + extcon_set_state_sync(ext->edev, EXTCON_USB, state); +} + +static void cht_wc_extcon_pwrsrc_event(struct cht_wc_extcon_data *ext) +{ + int ret, pwrsrc_sts, id; + unsigned int cable = EXTCON_NONE; + /* Ignore errors in host mode, as the 5v boost converter is on then */ + bool ignore_get_charger_errors = ext->usb_host; + enum usb_role role; + + ext->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + + ret = regmap_read(ext->regmap, CHT_WC_PWRSRC_STS, &pwrsrc_sts); + if (ret) { + dev_err(ext->dev, "Error reading pwrsrc status: %d\n", ret); + return; + } + + id = cht_wc_extcon_get_id(ext, pwrsrc_sts); + if (id == INTEL_USB_ID_GND) { + cht_wc_extcon_enable_charging(ext, false); + cht_wc_extcon_set_otgmode(ext, true); + + /* The 5v boost causes a false VBUS / SDP detect, skip */ + goto charger_det_done; + } + + cht_wc_extcon_set_otgmode(ext, false); + cht_wc_extcon_enable_charging(ext, true); + + /* Plugged into a host/charger or not connected? */ + if (!(pwrsrc_sts & CHT_WC_PWRSRC_VBUS)) { + /* Route D+ and D- to PMIC for future charger detection */ + cht_wc_extcon_set_phymux(ext, MUX_SEL_PMIC); + goto set_state; + } + + ret = cht_wc_extcon_get_charger(ext, ignore_get_charger_errors); + if (ret >= 0) + cable = ret; + +charger_det_done: + /* Route D+ and D- to SoC for the host or gadget controller */ + cht_wc_extcon_set_phymux(ext, MUX_SEL_SOC); + +set_state: + if (cable != ext->previous_cable) { + cht_wc_extcon_set_state(ext, cable, true); + cht_wc_extcon_set_state(ext, ext->previous_cable, false); + ext->previous_cable = cable; + } + + ext->usb_host = ((id == INTEL_USB_ID_GND) || (id == INTEL_USB_RID_A)); + extcon_set_state_sync(ext->edev, EXTCON_USB_HOST, ext->usb_host); + + if (ext->usb_host) + role = USB_ROLE_HOST; + else if (pwrsrc_sts & CHT_WC_PWRSRC_VBUS) + role = USB_ROLE_DEVICE; + else + role = USB_ROLE_NONE; + + /* Note: this is a no-op when ext->role_sw is NULL */ + ret = usb_role_switch_set_role(ext->role_sw, role); + if (ret) + dev_err(ext->dev, "Error setting USB-role: %d\n", ret); + + if (ext->psy) + power_supply_changed(ext->psy); +} + +static irqreturn_t cht_wc_extcon_isr(int irq, void *data) +{ + struct cht_wc_extcon_data *ext = data; + int ret, irqs; + + ret = regmap_read(ext->regmap, CHT_WC_PWRSRC_IRQ, &irqs); + if (ret) { + dev_err(ext->dev, "Error reading irqs: %d\n", ret); + return IRQ_NONE; + } + + cht_wc_extcon_pwrsrc_event(ext); + + ret = regmap_write(ext->regmap, CHT_WC_PWRSRC_IRQ, irqs); + if (ret) { + dev_err(ext->dev, "Error writing irqs: %d\n", ret); + return IRQ_NONE; + } + + return IRQ_HANDLED; +} + +static int cht_wc_extcon_sw_control(struct cht_wc_extcon_data *ext, bool enable) +{ + int ret, mask, val; + + val = enable ? 0 : CHT_WC_CHGDISCTRL_FN; + ret = regmap_update_bits(ext->regmap, CHT_WC_CHGDISCTRL, + CHT_WC_CHGDISCTRL_FN, val); + if (ret) + dev_err(ext->dev, + "Error setting sw control for CHGDIS pin: %d\n", + ret); + + mask = CHT_WC_CHGRCTRL0_SWCONTROL | CHT_WC_CHGRCTRL0_CCSM_OFF; + val = enable ? mask : 0; + ret = regmap_update_bits(ext->regmap, CHT_WC_CHGRCTRL0, mask, val); + if (ret) + dev_err(ext->dev, "Error setting sw control: %d\n", ret); + + return ret; +} + +static int cht_wc_extcon_find_role_sw(struct cht_wc_extcon_data *ext) +{ + const struct software_node *swnode; + struct fwnode_handle *fwnode; + + swnode = software_node_find_by_name(NULL, "intel-xhci-usb-sw"); + if (!swnode) + return -EPROBE_DEFER; + + fwnode = software_node_fwnode(swnode); + ext->role_sw = usb_role_switch_find_by_fwnode(fwnode); + fwnode_handle_put(fwnode); + + return ext->role_sw ? 0 : -EPROBE_DEFER; +} + +static void cht_wc_extcon_put_role_sw(void *data) +{ + struct cht_wc_extcon_data *ext = data; + + usb_role_switch_put(ext->role_sw); +} + +/* Some boards require controlling the role-sw and Vbus based on the id-pin */ +static int cht_wc_extcon_get_role_sw_and_regulator(struct cht_wc_extcon_data *ext) +{ + int ret; + + ret = cht_wc_extcon_find_role_sw(ext); + if (ret) + return ret; + + ret = devm_add_action_or_reset(ext->dev, cht_wc_extcon_put_role_sw, ext); + if (ret) + return ret; + + /* + * On x86/ACPI platforms the regulator <-> consumer link is provided + * by platform_data passed to the regulator driver. This means that + * this info is not available before the regulator driver has bound. + * Use devm_regulator_get_optional() to avoid getting a dummy + * regulator and wait for the regulator to show up if necessary. + */ + ext->vbus_boost = devm_regulator_get_optional(ext->dev, "vbus"); + if (IS_ERR(ext->vbus_boost)) { + ret = PTR_ERR(ext->vbus_boost); + if (ret == -ENODEV) + ret = -EPROBE_DEFER; + + return dev_err_probe(ext->dev, ret, "getting Vbus regulator"); + } + + return 0; +} + +static int cht_wc_extcon_psy_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct cht_wc_extcon_data *ext = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = ext->usb_type; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = ext->usb_type ? 1 : 0; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_property cht_wc_extcon_psy_props[] = { + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc cht_wc_extcon_psy_desc = { + .name = "cht_wcove_pwrsrc", + .type = POWER_SUPPLY_TYPE_USB, + .usb_types = BIT(POWER_SUPPLY_USB_TYPE_SDP) | + BIT(POWER_SUPPLY_USB_TYPE_CDP) | + BIT(POWER_SUPPLY_USB_TYPE_DCP) | + BIT(POWER_SUPPLY_USB_TYPE_ACA) | + BIT(POWER_SUPPLY_USB_TYPE_UNKNOWN), + .properties = cht_wc_extcon_psy_props, + .num_properties = ARRAY_SIZE(cht_wc_extcon_psy_props), + .get_property = cht_wc_extcon_psy_get_prop, +}; + +static int cht_wc_extcon_register_psy(struct cht_wc_extcon_data *ext) +{ + struct power_supply_config psy_cfg = { .drv_data = ext }; + + ext->psy = devm_power_supply_register(ext->dev, + &cht_wc_extcon_psy_desc, + &psy_cfg); + return PTR_ERR_OR_ZERO(ext->psy); +} + +static int cht_wc_extcon_probe(struct platform_device *pdev) +{ + struct intel_soc_pmic *pmic = dev_get_drvdata(pdev->dev.parent); + struct cht_wc_extcon_data *ext; + unsigned long mask = ~(CHT_WC_PWRSRC_VBUS | CHT_WC_PWRSRC_USBID_MASK); + int pwrsrc_sts, id; + int irq, ret; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + ext = devm_kzalloc(&pdev->dev, sizeof(*ext), GFP_KERNEL); + if (!ext) + return -ENOMEM; + + ext->dev = &pdev->dev; + ext->regmap = pmic->regmap; + ext->previous_cable = EXTCON_NONE; + + /* Initialize extcon device */ + ext->edev = devm_extcon_dev_allocate(ext->dev, cht_wc_extcon_cables); + if (IS_ERR(ext->edev)) + return PTR_ERR(ext->edev); + + switch (pmic->cht_wc_model) { + case INTEL_CHT_WC_GPD_WIN_POCKET: + /* + * When a host-cable is detected the BIOS enables an external 5v boost + * converter to power connected devices there are 2 problems with this: + * 1) This gets seen by the external battery charger as a valid Vbus + * supply and it then tries to feed Vsys from this creating a + * feedback loop which causes aprox. 300 mA extra battery drain + * (and unless we drive the external-charger-disable pin high it + * also tries to charge the battery causing even more feedback). + * 2) This gets seen by the pwrsrc block as a SDP USB Vbus supply + * Since the external battery charger has its own 5v boost converter + * which does not have these issues, we simply turn the separate + * external 5v boost converter off and leave it off entirely. + */ + cht_wc_extcon_set_5v_boost(ext, false); + break; + case INTEL_CHT_WC_LENOVO_YOGABOOK1: + case INTEL_CHT_WC_LENOVO_YT3_X90: + /* Do this first, as it may very well return -EPROBE_DEFER. */ + ret = cht_wc_extcon_get_role_sw_and_regulator(ext); + if (ret) + return ret; + /* + * The bq25890 used here relies on this driver's BC-1.2 charger + * detection, and the bq25890 driver expect this info to be + * available through a parent power_supply class device which + * models the detected charger (idem to how the Type-C TCPM code + * registers a power_supply classdev for the connected charger). + */ + ret = cht_wc_extcon_register_psy(ext); + if (ret) + return ret; + break; + case INTEL_CHT_WC_XIAOMI_MIPAD2: + ret = cht_wc_extcon_get_role_sw_and_regulator(ext); + if (ret) + return ret; + break; + default: + break; + } + + /* Enable sw control */ + ret = cht_wc_extcon_sw_control(ext, true); + if (ret) + goto disable_sw_control; + + /* Disable charging by external battery charger */ + cht_wc_extcon_enable_charging(ext, false); + + /* Register extcon device */ + ret = devm_extcon_dev_register(ext->dev, ext->edev); + if (ret) { + dev_err(ext->dev, "Error registering extcon device: %d\n", ret); + goto disable_sw_control; + } + + ret = regmap_read(ext->regmap, CHT_WC_PWRSRC_STS, &pwrsrc_sts); + if (ret) { + dev_err(ext->dev, "Error reading pwrsrc status: %d\n", ret); + goto disable_sw_control; + } + + /* + * If no USB host or device connected, route D+ and D- to PMIC for + * initial charger detection + */ + id = cht_wc_extcon_get_id(ext, pwrsrc_sts); + if (id != INTEL_USB_ID_GND) + cht_wc_extcon_set_phymux(ext, MUX_SEL_PMIC); + + /* Get initial state */ + cht_wc_extcon_pwrsrc_event(ext); + + ret = devm_request_threaded_irq(ext->dev, irq, NULL, cht_wc_extcon_isr, + IRQF_ONESHOT, pdev->name, ext); + if (ret) { + dev_err(ext->dev, "Error requesting interrupt: %d\n", ret); + goto disable_sw_control; + } + + /* Unmask irqs */ + ret = regmap_write(ext->regmap, CHT_WC_PWRSRC_IRQ_MASK, mask); + if (ret) { + dev_err(ext->dev, "Error writing irq-mask: %d\n", ret); + goto disable_sw_control; + } + + platform_set_drvdata(pdev, ext); + + return 0; + +disable_sw_control: + cht_wc_extcon_sw_control(ext, false); + return ret; +} + +static void cht_wc_extcon_remove(struct platform_device *pdev) +{ + struct cht_wc_extcon_data *ext = platform_get_drvdata(pdev); + + cht_wc_extcon_sw_control(ext, false); +} + +static const struct platform_device_id cht_wc_extcon_table[] = { + { .name = "cht_wcove_pwrsrc" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, cht_wc_extcon_table); + +static struct platform_driver cht_wc_extcon_driver = { + .probe = cht_wc_extcon_probe, + .remove = cht_wc_extcon_remove, + .id_table = cht_wc_extcon_table, + .driver = { + .name = "cht_wcove_pwrsrc", + }, +}; +module_platform_driver(cht_wc_extcon_driver); + +MODULE_DESCRIPTION("Intel Cherrytrail Whiskey Cove PMIC extcon driver"); +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-intel-int3496.c b/drivers/extcon/extcon-intel-int3496.c new file mode 100644 index 000000000..7d16d5b7d --- /dev/null +++ b/drivers/extcon/extcon-intel-int3496.c @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel INT3496 ACPI device extcon driver + * + * Copyright (c) 2016 Hans de Goede <hdegoede@redhat.com> + * + * Based on android x86 kernel code which is: + * + * Copyright (c) 2014, Intel Corporation. + * Author: David Cohen <david.a.cohen@linux.intel.com> + */ + +#include <linux/acpi.h> +#include <linux/devm-helpers.h> +#include <linux/extcon-provider.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regulator/consumer.h> + +#define INT3496_GPIO_USB_ID 0 +#define INT3496_GPIO_VBUS_EN 1 +#define INT3496_GPIO_USB_MUX 2 +#define DEBOUNCE_TIME msecs_to_jiffies(50) + +struct int3496_data { + struct device *dev; + struct extcon_dev *edev; + struct delayed_work work; + struct gpio_desc *gpio_usb_id; + struct gpio_desc *gpio_vbus_en; + struct gpio_desc *gpio_usb_mux; + struct regulator *vbus_boost; + int usb_id_irq; + bool vbus_boost_enabled; +}; + +static const unsigned int int3496_cable[] = { + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +static const struct acpi_gpio_params id_gpios = { INT3496_GPIO_USB_ID, 0, false }; +static const struct acpi_gpio_params vbus_gpios = { INT3496_GPIO_VBUS_EN, 0, false }; +static const struct acpi_gpio_params mux_gpios = { INT3496_GPIO_USB_MUX, 0, false }; + +static const struct acpi_gpio_mapping acpi_int3496_default_gpios[] = { + /* + * Some platforms have a bug in ACPI GPIO description making IRQ + * GPIO to be output only. Ask the GPIO core to ignore this limit. + */ + { "id-gpios", &id_gpios, 1, ACPI_GPIO_QUIRK_NO_IO_RESTRICTION }, + { "vbus-gpios", &vbus_gpios, 1 }, + { "mux-gpios", &mux_gpios, 1 }, + { }, +}; + +static void int3496_set_vbus_boost(struct int3496_data *data, bool enable) +{ + int ret; + + if (IS_ERR_OR_NULL(data->vbus_boost)) + return; + + if (data->vbus_boost_enabled == enable) + return; + + if (enable) + ret = regulator_enable(data->vbus_boost); + else + ret = regulator_disable(data->vbus_boost); + + if (ret == 0) + data->vbus_boost_enabled = enable; + else + dev_err(data->dev, "Error updating Vbus boost regulator: %d\n", ret); +} + +static void int3496_do_usb_id(struct work_struct *work) +{ + struct int3496_data *data = + container_of(work, struct int3496_data, work.work); + int id = gpiod_get_value_cansleep(data->gpio_usb_id); + + /* id == 1: PERIPHERAL, id == 0: HOST */ + dev_dbg(data->dev, "Connected %s cable\n", id ? "PERIPHERAL" : "HOST"); + + /* + * Peripheral: set USB mux to peripheral and disable VBUS + * Host: set USB mux to host and enable VBUS + */ + if (!IS_ERR(data->gpio_usb_mux)) + gpiod_direction_output(data->gpio_usb_mux, id); + + if (!IS_ERR(data->gpio_vbus_en)) + gpiod_direction_output(data->gpio_vbus_en, !id); + else + int3496_set_vbus_boost(data, !id); + + extcon_set_state_sync(data->edev, EXTCON_USB_HOST, !id); +} + +static irqreturn_t int3496_thread_isr(int irq, void *priv) +{ + struct int3496_data *data = priv; + + /* Let the pin settle before processing it */ + mod_delayed_work(system_percpu_wq, &data->work, DEBOUNCE_TIME); + + return IRQ_HANDLED; +} + +static int int3496_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct int3496_data *data; + int ret; + + if (has_acpi_companion(dev)) { + ret = devm_acpi_dev_add_driver_gpios(dev, acpi_int3496_default_gpios); + if (ret) { + dev_err(dev, "can't add GPIO ACPI mapping\n"); + return ret; + } + } + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = dev; + ret = devm_delayed_work_autocancel(dev, &data->work, int3496_do_usb_id); + if (ret) + return ret; + + data->gpio_usb_id = + devm_gpiod_get(dev, "id", GPIOD_IN | GPIOD_FLAGS_BIT_NONEXCLUSIVE); + if (IS_ERR(data->gpio_usb_id)) { + ret = PTR_ERR(data->gpio_usb_id); + dev_err(dev, "can't request USB ID GPIO: %d\n", ret); + return ret; + } + + data->usb_id_irq = gpiod_to_irq(data->gpio_usb_id); + if (data->usb_id_irq < 0) { + dev_err(dev, "can't get USB ID IRQ: %d\n", data->usb_id_irq); + return data->usb_id_irq; + } + + data->gpio_vbus_en = devm_gpiod_get(dev, "vbus", GPIOD_ASIS); + if (IS_ERR(data->gpio_vbus_en)) { + dev_dbg(dev, "can't request VBUS EN GPIO\n"); + data->vbus_boost = devm_regulator_get_optional(dev, "vbus"); + } + + data->gpio_usb_mux = devm_gpiod_get(dev, "mux", GPIOD_ASIS); + if (IS_ERR(data->gpio_usb_mux)) + dev_dbg(dev, "can't request USB MUX GPIO\n"); + + /* register extcon device */ + data->edev = devm_extcon_dev_allocate(dev, int3496_cable); + if (IS_ERR(data->edev)) + return -ENOMEM; + + ret = devm_extcon_dev_register(dev, data->edev); + if (ret < 0) { + dev_err(dev, "can't register extcon device: %d\n", ret); + return ret; + } + + ret = devm_request_threaded_irq(dev, data->usb_id_irq, + NULL, int3496_thread_isr, + IRQF_SHARED | IRQF_ONESHOT | + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING, + dev_name(dev), data); + if (ret < 0) { + dev_err(dev, "can't request IRQ for USB ID GPIO: %d\n", ret); + return ret; + } + + /* process id-pin so that we start with the right status */ + queue_delayed_work(system_percpu_wq, &data->work, 0); + flush_delayed_work(&data->work); + + platform_set_drvdata(pdev, data); + + return 0; +} + +static const struct acpi_device_id int3496_acpi_match[] = { + { "INT3496" }, + { } +}; +MODULE_DEVICE_TABLE(acpi, int3496_acpi_match); + +static const struct platform_device_id int3496_ids[] = { + { .name = "intel-int3496" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, int3496_ids); + +static struct platform_driver int3496_driver = { + .driver = { + .name = "intel-int3496", + .acpi_match_table = int3496_acpi_match, + }, + .probe = int3496_probe, + .id_table = int3496_ids, +}; + +module_platform_driver(int3496_driver); + +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_DESCRIPTION("Intel INT3496 ACPI device extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-intel-mrfld.c b/drivers/extcon/extcon-intel-mrfld.c new file mode 100644 index 000000000..7246d704f --- /dev/null +++ b/drivers/extcon/extcon-intel-mrfld.c @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * extcon driver for Basin Cove PMIC + * + * Copyright (c) 2019, Intel Corporation. + * Author: Andy Shevchenko <andriy.shevchenko@linux.intel.com> + */ + +#include <linux/extcon-provider.h> +#include <linux/interrupt.h> +#include <linux/mfd/intel_soc_pmic.h> +#include <linux/mfd/intel_soc_pmic_mrfld.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +#include "extcon-intel.h" + +#define BCOVE_USBIDCTRL 0x19 +#define BCOVE_USBIDCTRL_ID BIT(0) +#define BCOVE_USBIDCTRL_ACA BIT(1) +#define BCOVE_USBIDCTRL_ALL (BCOVE_USBIDCTRL_ID | BCOVE_USBIDCTRL_ACA) + +#define BCOVE_USBIDSTS 0x1a +#define BCOVE_USBIDSTS_GND BIT(0) +#define BCOVE_USBIDSTS_RARBRC_MASK GENMASK(2, 1) +#define BCOVE_USBIDSTS_RARBRC_SHIFT 1 +#define BCOVE_USBIDSTS_NO_ACA 0 +#define BCOVE_USBIDSTS_R_ID_A 1 +#define BCOVE_USBIDSTS_R_ID_B 2 +#define BCOVE_USBIDSTS_R_ID_C 3 +#define BCOVE_USBIDSTS_FLOAT BIT(3) +#define BCOVE_USBIDSTS_SHORT BIT(4) + +#define BCOVE_CHGRIRQ_ALL (BCOVE_CHGRIRQ_VBUSDET | BCOVE_CHGRIRQ_DCDET | \ + BCOVE_CHGRIRQ_BATTDET | BCOVE_CHGRIRQ_USBIDDET) + +#define BCOVE_CHGRCTRL0 0x4b +#define BCOVE_CHGRCTRL0_CHGRRESET BIT(0) +#define BCOVE_CHGRCTRL0_EMRGCHREN BIT(1) +#define BCOVE_CHGRCTRL0_EXTCHRDIS BIT(2) +#define BCOVE_CHGRCTRL0_SWCONTROL BIT(3) +#define BCOVE_CHGRCTRL0_TTLCK BIT(4) +#define BCOVE_CHGRCTRL0_BIT_5 BIT(5) +#define BCOVE_CHGRCTRL0_BIT_6 BIT(6) +#define BCOVE_CHGRCTRL0_CHR_WDT_NOKICK BIT(7) + +struct mrfld_extcon_data { + struct device *dev; + struct regmap *regmap; + struct extcon_dev *edev; + unsigned int status; + unsigned int id; +}; + +static const unsigned int mrfld_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_CDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_ACA, + EXTCON_NONE, +}; + +static int mrfld_extcon_clear(struct mrfld_extcon_data *data, unsigned int reg, + unsigned int mask) +{ + return regmap_update_bits(data->regmap, reg, mask, 0x00); +} + +static int mrfld_extcon_set(struct mrfld_extcon_data *data, unsigned int reg, + unsigned int mask) +{ + return regmap_update_bits(data->regmap, reg, mask, 0xff); +} + +static int mrfld_extcon_sw_control(struct mrfld_extcon_data *data, bool enable) +{ + unsigned int mask = BCOVE_CHGRCTRL0_SWCONTROL; + struct device *dev = data->dev; + int ret; + + if (enable) + ret = mrfld_extcon_set(data, BCOVE_CHGRCTRL0, mask); + else + ret = mrfld_extcon_clear(data, BCOVE_CHGRCTRL0, mask); + if (ret) + dev_err(dev, "can't set SW control: %d\n", ret); + return ret; +} + +static int mrfld_extcon_get_id(struct mrfld_extcon_data *data) +{ + struct regmap *regmap = data->regmap; + unsigned int id; + bool ground; + int ret; + + ret = regmap_read(regmap, BCOVE_USBIDSTS, &id); + if (ret) + return ret; + + if (id & BCOVE_USBIDSTS_FLOAT) + return INTEL_USB_ID_FLOAT; + + switch ((id & BCOVE_USBIDSTS_RARBRC_MASK) >> BCOVE_USBIDSTS_RARBRC_SHIFT) { + case BCOVE_USBIDSTS_R_ID_A: + return INTEL_USB_RID_A; + case BCOVE_USBIDSTS_R_ID_B: + return INTEL_USB_RID_B; + case BCOVE_USBIDSTS_R_ID_C: + return INTEL_USB_RID_C; + } + + /* + * PMIC A0 reports USBIDSTS_GND = 1 for ID_GND, + * but PMIC B0 reports USBIDSTS_GND = 0 for ID_GND. + * Thus we must check this bit at last. + */ + ground = id & BCOVE_USBIDSTS_GND; + switch ('A' + BCOVE_MAJOR(data->id)) { + case 'A': + return ground ? INTEL_USB_ID_GND : INTEL_USB_ID_FLOAT; + case 'B': + return ground ? INTEL_USB_ID_FLOAT : INTEL_USB_ID_GND; + } + + /* Unknown or unsupported type */ + return INTEL_USB_ID_FLOAT; +} + +static int mrfld_extcon_role_detect(struct mrfld_extcon_data *data) +{ + unsigned int id; + bool usb_host; + int ret; + + ret = mrfld_extcon_get_id(data); + if (ret < 0) + return ret; + + id = ret; + + usb_host = (id == INTEL_USB_ID_GND) || (id == INTEL_USB_RID_A); + extcon_set_state_sync(data->edev, EXTCON_USB_HOST, usb_host); + + return 0; +} + +static int mrfld_extcon_cable_detect(struct mrfld_extcon_data *data) +{ + struct regmap *regmap = data->regmap; + unsigned int status, change; + int ret; + + /* + * It seems SCU firmware clears the content of BCOVE_CHGRIRQ1 + * and makes it useless for OS. Instead we compare a previously + * stored status to the current one, provided by BCOVE_SCHGRIRQ1. + */ + ret = regmap_read(regmap, BCOVE_SCHGRIRQ1, &status); + if (ret) + return ret; + + change = status ^ data->status; + if (!change) + return -ENODATA; + + if (change & BCOVE_CHGRIRQ_USBIDDET) { + ret = mrfld_extcon_role_detect(data); + if (ret) + return ret; + } + + data->status = status; + + return 0; +} + +static irqreturn_t mrfld_extcon_interrupt(int irq, void *dev_id) +{ + struct mrfld_extcon_data *data = dev_id; + int ret; + + ret = mrfld_extcon_cable_detect(data); + + mrfld_extcon_clear(data, BCOVE_MIRQLVL1, BCOVE_LVL1_CHGR); + + return ret ? IRQ_NONE: IRQ_HANDLED; +} + +static int mrfld_extcon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct intel_soc_pmic *pmic = dev_get_drvdata(dev->parent); + struct regmap *regmap = pmic->regmap; + struct mrfld_extcon_data *data; + unsigned int status; + unsigned int id; + int irq, ret; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = dev; + data->regmap = regmap; + + data->edev = devm_extcon_dev_allocate(dev, mrfld_extcon_cable); + if (IS_ERR(data->edev)) + return PTR_ERR(data->edev); + + ret = devm_extcon_dev_register(dev, data->edev); + if (ret < 0) + return dev_err_probe(dev, ret, "can't register extcon device\n"); + + ret = devm_request_threaded_irq(dev, irq, NULL, mrfld_extcon_interrupt, + IRQF_ONESHOT | IRQF_SHARED, pdev->name, + data); + if (ret) + return dev_err_probe(dev, ret, "can't register IRQ handler\n"); + + ret = regmap_read(regmap, BCOVE_ID, &id); + if (ret) + return dev_err_probe(dev, ret, "can't read PMIC ID\n"); + + data->id = id; + + ret = mrfld_extcon_sw_control(data, true); + if (ret) + return ret; + + /* Get initial state */ + mrfld_extcon_role_detect(data); + + /* + * Cached status value is used for cable detection, see comments + * in mrfld_extcon_cable_detect(), we need to sync cached value + * with a real state of the hardware. + */ + regmap_read(regmap, BCOVE_SCHGRIRQ1, &status); + data->status = status; + + mrfld_extcon_clear(data, BCOVE_MIRQLVL1, BCOVE_LVL1_CHGR); + mrfld_extcon_clear(data, BCOVE_MCHGRIRQ1, BCOVE_CHGRIRQ_ALL); + + mrfld_extcon_set(data, BCOVE_USBIDCTRL, BCOVE_USBIDCTRL_ALL); + + platform_set_drvdata(pdev, data); + + return 0; +} + +static void mrfld_extcon_remove(struct platform_device *pdev) +{ + struct mrfld_extcon_data *data = platform_get_drvdata(pdev); + + mrfld_extcon_sw_control(data, false); +} + +static const struct platform_device_id mrfld_extcon_id_table[] = { + { .name = "mrfld_bcove_pwrsrc" }, + {} +}; +MODULE_DEVICE_TABLE(platform, mrfld_extcon_id_table); + +static struct platform_driver mrfld_extcon_driver = { + .driver = { + .name = "mrfld_bcove_pwrsrc", + }, + .probe = mrfld_extcon_probe, + .remove = mrfld_extcon_remove, + .id_table = mrfld_extcon_id_table, +}; +module_platform_driver(mrfld_extcon_driver); + +MODULE_AUTHOR("Andy Shevchenko <andriy.shevchenko@linux.intel.com>"); +MODULE_DESCRIPTION("extcon driver for Intel Merrifield Basin Cove PMIC"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-intel.h b/drivers/extcon/extcon-intel.h new file mode 100644 index 000000000..0ad645ec7 --- /dev/null +++ b/drivers/extcon/extcon-intel.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Header file for Intel extcon hardware + * + * Copyright (C) 2019 Intel Corporation. All rights reserved. + */ + +#ifndef __EXTCON_INTEL_H__ +#define __EXTCON_INTEL_H__ + +enum extcon_intel_usb_id { + INTEL_USB_ID_OTG, + INTEL_USB_ID_GND, + INTEL_USB_ID_FLOAT, + INTEL_USB_RID_A, + INTEL_USB_RID_B, + INTEL_USB_RID_C, +}; + +#endif /* __EXTCON_INTEL_H__ */ diff --git a/drivers/extcon/extcon-lc824206xa.c b/drivers/extcon/extcon-lc824206xa.c new file mode 100644 index 000000000..56938748a --- /dev/null +++ b/drivers/extcon/extcon-lc824206xa.c @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ON Semiconductor LC824206XA Micro USB Switch driver + * + * Copyright (c) 2024 Hans de Goede <hansg@kernel.org> + * + * ON Semiconductor has an "Advance Information" datasheet available + * (ENA2222-D.PDF), but no full datasheet. So there is no documentation + * available for the registers. + * + * This driver is based on the register info from the extcon-fsa9285.c driver, + * from the Lollipop Android sources for the Lenovo Yoga Tablet 2 (Pro) + * 830 / 1050 / 1380 models. Note despite the name this is actually a driver + * for the LC824206XA not the FSA9285. The Android sources can be downloaded + * from Lenovo's support page for these tablets, filename: + * yoga_tab_2_osc_android_to_lollipop_201505.rar. + */ + +#include <linux/bits.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/extcon-provider.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/regulator/consumer.h> +#include <linux/workqueue.h> + +/* + * Register defines as mentioned above there is no datasheet with register + * info, so this may not be 100% accurate. + */ +#define REG00 0x00 +#define REG00_INIT_VALUE 0x01 + +#define REG_STATUS 0x01 +#define STATUS_OVP BIT(0) +#define STATUS_DATA_SHORT BIT(1) +#define STATUS_VBUS_PRESENT BIT(2) +#define STATUS_USB_ID GENMASK(7, 3) +#define STATUS_USB_ID_GND 0x80 +#define STATUS_USB_ID_ACA 0xf0 +#define STATUS_USB_ID_FLOAT 0xf8 + +/* + * This controls the DP/DM muxes + other switches, + * meaning of individual bits is unknown. + */ +#define REG_SWITCH_CONTROL 0x02 +#define SWITCH_STEREO_MIC 0xc8 +#define SWITCH_USB_HOST 0xec +#define SWITCH_DISCONNECTED 0xf8 +#define SWITCH_USB_DEVICE 0xfc + +/* 5 bits? ADC 0x10 GND, 0x1a-0x1f ACA, 0x1f float */ +#define REG_ID_PIN_ADC_VALUE 0x03 + +/* Masks for all 3 interrupt registers */ +#define INTR_ID_PIN_CHANGE BIT(0) +#define INTR_VBUS_CHANGE BIT(1) +/* Both of these get set after a continuous mode ADC conversion */ +#define INTR_ID_PIN_ADC_INT1 BIT(2) +#define INTR_ID_PIN_ADC_INT2 BIT(3) +/* Charger type available in reg 0x09 */ +#define INTR_CHARGER_DET_DONE BIT(4) +#define INTR_OVP BIT(5) + +/* There are 7 interrupt sources, bit 6 use is unknown (OCP?) */ +#define INTR_ALL GENMASK(6, 0) + +/* Unmask interrupts this driver cares about */ +#define INTR_MASK \ + (INTR_ALL & ~(INTR_ID_PIN_CHANGE | INTR_VBUS_CHANGE | INTR_CHARGER_DET_DONE)) + +/* Active (event happened and not cleared yet) interrupts */ +#define REG_INTR_STATUS 0x04 + +/* + * Writing a 1 to a bit here clears it in INTR_STATUS. These bits do NOT + * auto-reset to 0, so these must be set to 0 manually after clearing. + */ +#define REG_INTR_CLEAR 0x05 + +/* Interrupts which bit is set to 1 here will not raise the HW IRQ */ +#define REG_INTR_MASK 0x06 + +/* ID pin ADC control, meaning of individual bits is unknown */ +#define REG_ID_PIN_ADC_CTRL 0x07 +#define ID_PIN_ADC_AUTO 0x40 +#define ID_PIN_ADC_CONTINUOUS 0x44 + +#define REG_CHARGER_DET 0x08 +#define CHARGER_DET_ON BIT(0) +#define CHARGER_DET_CDP_ON BIT(1) +#define CHARGER_DET_CDP_VAL BIT(2) + +#define REG_CHARGER_TYPE 0x09 +#define CHARGER_TYPE_UNKNOWN 0x00 +#define CHARGER_TYPE_DCP 0x01 +#define CHARGER_TYPE_SDP_OR_CDP 0x04 +#define CHARGER_TYPE_QC 0x06 + +#define REG10 0x10 +#define REG10_INIT_VALUE 0x00 + +struct lc824206xa_data { + struct work_struct work; + struct i2c_client *client; + struct extcon_dev *edev; + struct power_supply *psy; + struct regulator *vbus_boost; + unsigned int usb_type; + unsigned int cable; + unsigned int previous_cable; + u8 switch_control; + u8 previous_switch_control; + bool vbus_ok; + bool vbus_boost_enabled; + bool fastcharge_over_miclr; +}; + +static const unsigned int lc824206xa_cables[] = { + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_CDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_ACA, + EXTCON_CHG_USB_FAST, + EXTCON_NONE, +}; + +/* read/write reg helpers to add error logging to smbus byte functions */ +static int lc824206xa_read_reg(struct lc824206xa_data *data, u8 reg) +{ + int ret; + + ret = i2c_smbus_read_byte_data(data->client, reg); + if (ret < 0) + dev_err(&data->client->dev, "Error %d reading reg 0x%02x\n", ret, reg); + + return ret; +} + +static int lc824206xa_write_reg(struct lc824206xa_data *data, u8 reg, u8 val) +{ + int ret; + + ret = i2c_smbus_write_byte_data(data->client, reg, val); + if (ret < 0) + dev_err(&data->client->dev, "Error %d writing reg 0x%02x\n", ret, reg); + + return ret; +} + +static int lc824206xa_get_id(struct lc824206xa_data *data) +{ + int ret; + + ret = lc824206xa_write_reg(data, REG_ID_PIN_ADC_CTRL, ID_PIN_ADC_CONTINUOUS); + if (ret) + return ret; + + ret = lc824206xa_read_reg(data, REG_ID_PIN_ADC_VALUE); + + lc824206xa_write_reg(data, REG_ID_PIN_ADC_CTRL, ID_PIN_ADC_AUTO); + + return ret; +} + +static void lc824206xa_set_vbus_boost(struct lc824206xa_data *data, bool enable) +{ + int ret; + + if (data->vbus_boost_enabled == enable) + return; + + if (enable) + ret = regulator_enable(data->vbus_boost); + else + ret = regulator_disable(data->vbus_boost); + + if (ret == 0) + data->vbus_boost_enabled = enable; + else + dev_err(&data->client->dev, "Error updating Vbus boost regulator: %d\n", ret); +} + +static void lc824206xa_charger_detect(struct lc824206xa_data *data) +{ + int charger_type, ret; + + charger_type = lc824206xa_read_reg(data, REG_CHARGER_TYPE); + if (charger_type < 0) + return; + + dev_dbg(&data->client->dev, "charger type 0x%02x\n", charger_type); + + switch (charger_type) { + case CHARGER_TYPE_UNKNOWN: + data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + /* Treat as SDP */ + data->cable = EXTCON_CHG_USB_SDP; + data->switch_control = SWITCH_USB_DEVICE; + break; + case CHARGER_TYPE_SDP_OR_CDP: + data->usb_type = POWER_SUPPLY_USB_TYPE_SDP; + data->cable = EXTCON_CHG_USB_SDP; + data->switch_control = SWITCH_USB_DEVICE; + + ret = lc824206xa_write_reg(data, REG_CHARGER_DET, + CHARGER_DET_CDP_ON | CHARGER_DET_ON); + if (ret < 0) + break; + + msleep(100); + ret = lc824206xa_read_reg(data, REG_CHARGER_DET); + if (ret >= 0 && (ret & CHARGER_DET_CDP_VAL)) { + data->usb_type = POWER_SUPPLY_USB_TYPE_CDP; + data->cable = EXTCON_CHG_USB_CDP; + } + + lc824206xa_write_reg(data, REG_CHARGER_DET, CHARGER_DET_ON); + break; + case CHARGER_TYPE_DCP: + data->usb_type = POWER_SUPPLY_USB_TYPE_DCP; + data->cable = EXTCON_CHG_USB_DCP; + if (data->fastcharge_over_miclr) + data->switch_control = SWITCH_STEREO_MIC; + else + data->switch_control = SWITCH_DISCONNECTED; + break; + case CHARGER_TYPE_QC: + data->usb_type = POWER_SUPPLY_USB_TYPE_DCP; + data->cable = EXTCON_CHG_USB_DCP; + data->switch_control = SWITCH_DISCONNECTED; + break; + default: + dev_warn(&data->client->dev, "Unknown charger type: 0x%02x\n", charger_type); + break; + } +} + +static void lc824206xa_work(struct work_struct *work) +{ + struct lc824206xa_data *data = container_of(work, struct lc824206xa_data, work); + bool vbus_boost_enable = false; + int status, id; + + status = lc824206xa_read_reg(data, REG_STATUS); + if (status < 0) + return; + + dev_dbg(&data->client->dev, "status 0x%02x\n", status); + + data->vbus_ok = (status & (STATUS_VBUS_PRESENT | STATUS_OVP)) == STATUS_VBUS_PRESENT; + + /* Read id pin ADC if necessary */ + switch (status & STATUS_USB_ID) { + case STATUS_USB_ID_GND: + case STATUS_USB_ID_FLOAT: + break; + default: + /* Happens when the connector is inserted slowly, log at dbg level */ + dev_dbg(&data->client->dev, "Unknown status 0x%02x\n", status); + fallthrough; + case STATUS_USB_ID_ACA: + id = lc824206xa_get_id(data); + dev_dbg(&data->client->dev, "RID 0x%02x\n", id); + switch (id) { + case 0x10: + status = STATUS_USB_ID_GND; + break; + case 0x18 ... 0x1e: + status = STATUS_USB_ID_ACA; + break; + case 0x1f: + status = STATUS_USB_ID_FLOAT; + break; + default: + dev_warn(&data->client->dev, "Unknown RID 0x%02x\n", id); + return; + } + } + + /* Check for out of spec OTG charging hubs, treat as ACA */ + if ((status & STATUS_USB_ID) == STATUS_USB_ID_GND && + data->vbus_ok && !data->vbus_boost_enabled) { + dev_info(&data->client->dev, "Out of spec USB host adapter with Vbus present, not enabling 5V output\n"); + status = STATUS_USB_ID_ACA; + } + + switch (status & STATUS_USB_ID) { + case STATUS_USB_ID_ACA: + data->usb_type = POWER_SUPPLY_USB_TYPE_ACA; + data->cable = EXTCON_CHG_USB_ACA; + data->switch_control = SWITCH_USB_HOST; + break; + case STATUS_USB_ID_GND: + data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + data->cable = EXTCON_USB_HOST; + data->switch_control = SWITCH_USB_HOST; + vbus_boost_enable = true; + break; + case STATUS_USB_ID_FLOAT: + /* When fast charging with Vbus > 5V, OVP will be set */ + if (data->fastcharge_over_miclr && + data->switch_control == SWITCH_STEREO_MIC && + (status & STATUS_OVP)) { + data->cable = EXTCON_CHG_USB_FAST; + break; + } + + if (data->vbus_ok) { + lc824206xa_charger_detect(data); + } else { + data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + data->cable = EXTCON_NONE; + data->switch_control = SWITCH_DISCONNECTED; + } + break; + } + + lc824206xa_set_vbus_boost(data, vbus_boost_enable); + + if (data->switch_control != data->previous_switch_control) { + lc824206xa_write_reg(data, REG_SWITCH_CONTROL, data->switch_control); + data->previous_switch_control = data->switch_control; + } + + if (data->cable != data->previous_cable) { + extcon_set_state_sync(data->edev, data->previous_cable, false); + extcon_set_state_sync(data->edev, data->cable, true); + data->previous_cable = data->cable; + } + + power_supply_changed(data->psy); +} + +static irqreturn_t lc824206xa_irq(int irq, void *_data) +{ + struct lc824206xa_data *data = _data; + int intr_status; + + intr_status = lc824206xa_read_reg(data, REG_INTR_STATUS); + if (intr_status < 0) + intr_status = INTR_ALL; /* Should never happen, clear all */ + + dev_dbg(&data->client->dev, "interrupt 0x%02x\n", intr_status); + + lc824206xa_write_reg(data, REG_INTR_CLEAR, intr_status); + lc824206xa_write_reg(data, REG_INTR_CLEAR, 0); + + schedule_work(&data->work); + return IRQ_HANDLED; +} + +/* + * Newer charger (power_supply) drivers expect the max input current to be + * provided by a parent power_supply device for the charger chip. + */ +static int lc824206xa_psy_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct lc824206xa_data *data = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = data->vbus_ok && !data->vbus_boost_enabled; + break; + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = data->usb_type; + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + switch (data->usb_type) { + case POWER_SUPPLY_USB_TYPE_DCP: + case POWER_SUPPLY_USB_TYPE_ACA: + val->intval = 2000000; + break; + case POWER_SUPPLY_USB_TYPE_CDP: + val->intval = 1500000; + break; + default: + val->intval = 500000; + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_property lc824206xa_psy_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_CURRENT_MAX, +}; + +static const struct power_supply_desc lc824206xa_psy_desc = { + .name = "lc824206xa-charger-detect", + .type = POWER_SUPPLY_TYPE_USB, + .usb_types = BIT(POWER_SUPPLY_USB_TYPE_SDP) | + BIT(POWER_SUPPLY_USB_TYPE_CDP) | + BIT(POWER_SUPPLY_USB_TYPE_DCP) | + BIT(POWER_SUPPLY_USB_TYPE_ACA) | + BIT(POWER_SUPPLY_USB_TYPE_UNKNOWN), + .properties = lc824206xa_psy_props, + .num_properties = ARRAY_SIZE(lc824206xa_psy_props), + .get_property = lc824206xa_psy_get_prop, +}; + +static int lc824206xa_probe(struct i2c_client *client) +{ + struct power_supply_config psy_cfg = { }; + struct device *dev = &client->dev; + struct lc824206xa_data *data; + int ret; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->client = client; + INIT_WORK(&data->work, lc824206xa_work); + data->cable = EXTCON_NONE; + data->previous_cable = EXTCON_NONE; + data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + /* Some designs use a custom fast-charge protocol over the mic L/R inputs */ + data->fastcharge_over_miclr = + device_property_read_bool(dev, "onnn,enable-miclr-for-dcp"); + + data->vbus_boost = devm_regulator_get(dev, "vbus"); + if (IS_ERR(data->vbus_boost)) + return dev_err_probe(dev, PTR_ERR(data->vbus_boost), + "getting regulator\n"); + + /* Init */ + ret = lc824206xa_write_reg(data, REG00, REG00_INIT_VALUE); + ret |= lc824206xa_write_reg(data, REG10, REG10_INIT_VALUE); + msleep(100); + ret |= lc824206xa_write_reg(data, REG_INTR_CLEAR, INTR_ALL); + ret |= lc824206xa_write_reg(data, REG_INTR_CLEAR, 0); + ret |= lc824206xa_write_reg(data, REG_INTR_MASK, INTR_MASK); + ret |= lc824206xa_write_reg(data, REG_ID_PIN_ADC_CTRL, ID_PIN_ADC_AUTO); + ret |= lc824206xa_write_reg(data, REG_CHARGER_DET, CHARGER_DET_ON); + if (ret) + return -EIO; + + /* Initialize extcon device */ + data->edev = devm_extcon_dev_allocate(dev, lc824206xa_cables); + if (IS_ERR(data->edev)) + return PTR_ERR(data->edev); + + ret = devm_extcon_dev_register(dev, data->edev); + if (ret) + return dev_err_probe(dev, ret, "registering extcon device\n"); + + psy_cfg.drv_data = data; + data->psy = devm_power_supply_register(dev, &lc824206xa_psy_desc, &psy_cfg); + if (IS_ERR(data->psy)) + return dev_err_probe(dev, PTR_ERR(data->psy), "registering power supply\n"); + + ret = devm_request_threaded_irq(dev, client->irq, NULL, lc824206xa_irq, + IRQF_TRIGGER_LOW | IRQF_ONESHOT, + KBUILD_MODNAME, data); + if (ret) + return dev_err_probe(dev, ret, "requesting IRQ\n"); + + /* Sync initial state */ + schedule_work(&data->work); + return 0; +} + +static const struct i2c_device_id lc824206xa_i2c_ids[] = { + { "lc824206xa" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lc824206xa_i2c_ids); + +static struct i2c_driver lc824206xa_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, + .probe = lc824206xa_probe, + .id_table = lc824206xa_i2c_ids, +}; + +module_i2c_driver(lc824206xa_driver); + +MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>"); +MODULE_DESCRIPTION("LC824206XA Micro USB Switch driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-max14526.c b/drivers/extcon/extcon-max14526.c new file mode 100644 index 000000000..bf8997827 --- /dev/null +++ b/drivers/extcon/extcon-max14526.c @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <linux/device.h> +#include <linux/devm-helpers.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/extcon-provider.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/pm.h> +#include <linux/regmap.h> + +/* I2C addresses of MUIC internal registers */ +#define MAX14526_DEVICE_ID 0x00 +#define MAX14526_ID 0x02 + +/* CONTROL_1 register masks */ +#define MAX14526_CONTROL_1 0x01 +#define ID_2P2 BIT(6) +#define ID_620 BIT(5) +#define ID_200 BIT(4) +#define VLDO BIT(3) +#define SEMREN BIT(2) +#define ADC_EN BIT(1) +#define CP_EN BIT(0) + +/* CONTROL_2 register masks */ +#define MAX14526_CONTROL_2 0x02 +#define INTPOL BIT(7) +#define INT_EN BIT(6) +#define MIC_LP BIT(5) +#define CP_AUD BIT(4) +#define CHG_TYPE BIT(1) +#define USB_DET_DIS BIT(0) + +/* SW_CONTROL register masks */ +#define MAX14526_SW_CONTROL 0x03 +#define SW_DATA 0x00 +#define SW_UART 0x01 +#define SW_AUDIO 0x02 +#define SW_OPEN 0x07 + +/* INT_STATUS register masks */ +#define MAX14526_INT_STAT 0x04 +#define CHGDET BIT(7) +#define MR_COMP BIT(6) +#define SENDEND BIT(5) +#define V_VBUS BIT(4) + +/* STATUS register masks */ +#define MAX14526_STATUS 0x05 +#define CPORT BIT(7) +#define CHPORT BIT(6) +#define C1COMP BIT(0) + +enum max14526_idno_resistance { + MAX14526_GND, + MAX14526_24KOHM, + MAX14526_56KOHM, + MAX14526_100KOHM, + MAX14526_130KOHM, + MAX14526_180KOHM, + MAX14526_240KOHM, + MAX14526_330KOHM, + MAX14526_430KOHM, + MAX14526_620KOHM, + MAX14526_910KOHM, + MAX14526_OPEN +}; + +enum max14526_field_idx { + VENDOR_ID, CHIP_REV, /* DEVID */ + DM, DP, /* SW_CONTROL */ + MAX14526_N_REGMAP_FIELDS +}; + +static const struct reg_field max14526_reg_field[MAX14526_N_REGMAP_FIELDS] = { + [VENDOR_ID] = REG_FIELD(MAX14526_DEVICE_ID, 4, 7), + [CHIP_REV] = REG_FIELD(MAX14526_DEVICE_ID, 0, 3), + [DM] = REG_FIELD(MAX14526_SW_CONTROL, 0, 2), + [DP] = REG_FIELD(MAX14526_SW_CONTROL, 3, 5), +}; + +struct max14526_data { + struct i2c_client *client; + struct extcon_dev *edev; + + struct regmap *regmap; + struct regmap_field *rfield[MAX14526_N_REGMAP_FIELDS]; + + int last_state; + int cable; +}; + +enum max14526_muic_modes { + MAX14526_OTG = MAX14526_GND, /* no power */ + MAX14526_MHL = MAX14526_56KOHM, /* no power */ + MAX14526_OTG_Y = MAX14526_GND | V_VBUS, + MAX14526_MHL_CHG = MAX14526_GND | V_VBUS | CHGDET, + MAX14526_NONE = MAX14526_OPEN, + MAX14526_USB = MAX14526_OPEN | V_VBUS, + MAX14526_CHG = MAX14526_OPEN | V_VBUS | CHGDET, +}; + +static const unsigned int max14526_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_FAST, + EXTCON_DISP_MHL, + EXTCON_NONE, +}; + +static int max14526_ap_usb_mode(struct max14526_data *priv) +{ + struct device *dev = &priv->client->dev; + int ret; + + /* Enable USB Path */ + ret = regmap_field_write(priv->rfield[DM], SW_DATA); + if (ret) + return ret; + + ret = regmap_field_write(priv->rfield[DP], SW_DATA); + if (ret) + return ret; + + /* Enable 200K, Charger Pump and ADC */ + ret = regmap_write(priv->regmap, MAX14526_CONTROL_1, + ID_200 | ADC_EN | CP_EN); + if (ret) + return ret; + + dev_dbg(dev, "AP USB mode set\n"); + + return 0; +} + +static irqreturn_t max14526_interrupt(int irq, void *dev_id) +{ + struct max14526_data *priv = dev_id; + struct device *dev = &priv->client->dev; + int state, ret; + + /* + * Upon an MUIC IRQ (MUIC_INT_N falls), wait at least 70ms + * before reading INT_STAT and STATUS. After the reads, + * MUIC_INT_N returns to high (but the INT_STAT and STATUS + * contents will be held). + */ + msleep(100); + + ret = regmap_read(priv->regmap, MAX14526_INT_STAT, &state); + if (ret) + dev_err(dev, "failed to read MUIC state %d\n", ret); + + if (state == priv->last_state) + return IRQ_HANDLED; + + /* Detach previous device */ + extcon_set_state_sync(priv->edev, priv->cable, false); + + switch (state) { + case MAX14526_USB: + priv->cable = EXTCON_USB; + break; + + case MAX14526_CHG: + priv->cable = EXTCON_CHG_USB_FAST; + break; + + case MAX14526_OTG: + case MAX14526_OTG_Y: + priv->cable = EXTCON_USB_HOST; + break; + + case MAX14526_MHL: + case MAX14526_MHL_CHG: + priv->cable = EXTCON_DISP_MHL; + break; + + case MAX14526_NONE: + default: + priv->cable = EXTCON_NONE; + break; + } + + extcon_set_state_sync(priv->edev, priv->cable, true); + + priv->last_state = state; + + return IRQ_HANDLED; +} + +static const struct regmap_config max14526_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = MAX14526_STATUS, +}; + +static int max14526_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct max14526_data *priv; + int ret, dev_id, rev, i; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->client = client; + i2c_set_clientdata(client, priv); + + priv->regmap = devm_regmap_init_i2c(client, &max14526_regmap_config); + if (IS_ERR(priv->regmap)) + return dev_err_probe(dev, PTR_ERR(priv->regmap), "cannot allocate regmap\n"); + + for (i = 0; i < MAX14526_N_REGMAP_FIELDS; i++) { + priv->rfield[i] = devm_regmap_field_alloc(dev, priv->regmap, + max14526_reg_field[i]); + if (IS_ERR(priv->rfield[i])) + return dev_err_probe(dev, PTR_ERR(priv->rfield[i]), + "cannot allocate regmap field\n"); + } + + /* Detect if MUIC version is supported */ + ret = regmap_field_read(priv->rfield[VENDOR_ID], &dev_id); + if (ret) + return dev_err_probe(dev, ret, "failed to read MUIC ID\n"); + + regmap_field_read(priv->rfield[CHIP_REV], &rev); + + if (dev_id == MAX14526_ID) + dev_info(dev, "detected MAX14526 MUIC with id 0x%x, rev 0x%x\n", dev_id, rev); + else + dev_err_probe(dev, -EINVAL, "MUIC vendor id 0x%X is not recognized\n", dev_id); + + priv->edev = devm_extcon_dev_allocate(dev, max14526_extcon_cable); + if (IS_ERR(priv->edev)) + return dev_err_probe(dev, (IS_ERR(priv->edev)), + "failed to allocate extcon device\n"); + + ret = devm_extcon_dev_register(dev, priv->edev); + if (ret < 0) + return dev_err_probe(dev, ret, "failed to register extcon device\n"); + + ret = max14526_ap_usb_mode(priv); + if (ret < 0) + return dev_err_probe(dev, ret, "failed to set AP USB mode\n"); + + regmap_write_bits(priv->regmap, MAX14526_CONTROL_2, INT_EN, INT_EN); + regmap_write_bits(priv->regmap, MAX14526_CONTROL_2, USB_DET_DIS, (u32)~USB_DET_DIS); + + ret = devm_request_threaded_irq(dev, client->irq, NULL, &max14526_interrupt, + IRQF_ONESHOT | IRQF_SHARED, client->name, priv); + if (ret) + return dev_err_probe(dev, ret, "failed to register IRQ\n"); + + irq_wake_thread(client->irq, priv); + + return 0; +} + +static int max14526_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct max14526_data *priv = i2c_get_clientdata(client); + + irq_wake_thread(client->irq, priv); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(max14526_pm_ops, NULL, max14526_resume); + +static const struct of_device_id max14526_match[] = { + { .compatible = "maxim,max14526" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, max14526_match); + +static const struct i2c_device_id max14526_id[] = { + { "max14526" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max14526_id); + +static struct i2c_driver max14526_driver = { + .driver = { + .name = "max14526", + .of_match_table = max14526_match, + .pm = &max14526_pm_ops, + }, + .probe = max14526_probe, + .id_table = max14526_id, +}; +module_i2c_driver(max14526_driver); + +MODULE_AUTHOR("Svyatoslav Ryhel <clamor95@gmail.com>"); +MODULE_DESCRIPTION("MAX14526 extcon driver to support MUIC"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-max14577.c b/drivers/extcon/extcon-max14577.c new file mode 100644 index 000000000..5476f48ed --- /dev/null +++ b/drivers/extcon/extcon-max14577.c @@ -0,0 +1,803 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// extcon-max14577.c - MAX14577/77836 extcon driver to support MUIC +// +// Copyright (C) 2013,2014 Samsung Electronics +// Chanwoo Choi <cw00.choi@samsung.com> +// Krzysztof Kozlowski <krzk@kernel.org> + +#include <linux/devm-helpers.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/mfd/max14577.h> +#include <linux/mfd/max14577-private.h> +#include <linux/extcon-provider.h> + +#define DELAY_MS_DEFAULT 17000 /* unit: millisecond */ + +enum max14577_muic_adc_debounce_time { + ADC_DEBOUNCE_TIME_5MS = 0, + ADC_DEBOUNCE_TIME_10MS, + ADC_DEBOUNCE_TIME_25MS, + ADC_DEBOUNCE_TIME_38_62MS, +}; + +enum max14577_muic_status { + MAX14577_MUIC_STATUS1 = 0, + MAX14577_MUIC_STATUS2 = 1, + MAX14577_MUIC_STATUS_END, +}; + +/** + * struct max14577_muic_irq + * @irq: the index of irq list of MUIC device. + * @name: the name of irq. + * @virq: the virtual irq to use irq domain + */ +struct max14577_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max14577_muic_irq max14577_muic_irqs[] = { + { MAX14577_IRQ_INT1_ADC, "muic-ADC" }, + { MAX14577_IRQ_INT1_ADCLOW, "muic-ADCLOW" }, + { MAX14577_IRQ_INT1_ADCERR, "muic-ADCError" }, + { MAX14577_IRQ_INT2_CHGTYP, "muic-CHGTYP" }, + { MAX14577_IRQ_INT2_CHGDETRUN, "muic-CHGDETRUN" }, + { MAX14577_IRQ_INT2_DCDTMR, "muic-DCDTMR" }, + { MAX14577_IRQ_INT2_DBCHG, "muic-DBCHG" }, + { MAX14577_IRQ_INT2_VBVOLT, "muic-VBVOLT" }, +}; + +static struct max14577_muic_irq max77836_muic_irqs[] = { + { MAX14577_IRQ_INT1_ADC, "muic-ADC" }, + { MAX14577_IRQ_INT1_ADCLOW, "muic-ADCLOW" }, + { MAX14577_IRQ_INT1_ADCERR, "muic-ADCError" }, + { MAX77836_IRQ_INT1_ADC1K, "muic-ADC1K" }, + { MAX14577_IRQ_INT2_CHGTYP, "muic-CHGTYP" }, + { MAX14577_IRQ_INT2_CHGDETRUN, "muic-CHGDETRUN" }, + { MAX14577_IRQ_INT2_DCDTMR, "muic-DCDTMR" }, + { MAX14577_IRQ_INT2_DBCHG, "muic-DBCHG" }, + { MAX14577_IRQ_INT2_VBVOLT, "muic-VBVOLT" }, + { MAX77836_IRQ_INT2_VIDRM, "muic-VIDRM" }, +}; + +struct max14577_muic_info { + struct device *dev; + struct max14577 *max14577; + struct extcon_dev *edev; + int prev_cable_type; + int prev_chg_type; + u8 status[MAX14577_MUIC_STATUS_END]; + + struct max14577_muic_irq *muic_irqs; + unsigned int muic_irqs_num; + bool irq_adc; + bool irq_chg; + struct work_struct irq_work; + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + int path_usb; + int path_uart; +}; + +enum max14577_muic_cable_group { + MAX14577_CABLE_GROUP_ADC = 0, + MAX14577_CABLE_GROUP_CHG, +}; + +/* Define supported accessory type */ +enum max14577_muic_acc_type { + MAX14577_MUIC_ADC_GROUND = 0x0, + MAX14577_MUIC_ADC_SEND_END_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S1_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S2_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S3_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S4_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S5_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S6_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S7_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S8_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S9_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S10_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S11_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S12_BUTTON, + MAX14577_MUIC_ADC_RESERVED_ACC_1, + MAX14577_MUIC_ADC_RESERVED_ACC_2, + MAX14577_MUIC_ADC_RESERVED_ACC_3, + MAX14577_MUIC_ADC_RESERVED_ACC_4, + MAX14577_MUIC_ADC_RESERVED_ACC_5, + MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE2, + MAX14577_MUIC_ADC_PHONE_POWERED_DEV, + MAX14577_MUIC_ADC_TTY_CONVERTER, + MAX14577_MUIC_ADC_UART_CABLE, + MAX14577_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX14577_MUIC_ADC_FACTORY_MODE_USB_OFF, + MAX14577_MUIC_ADC_FACTORY_MODE_USB_ON, + MAX14577_MUIC_ADC_AV_CABLE_NOLOAD, + MAX14577_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF, + MAX14577_MUIC_ADC_FACTORY_MODE_UART_ON, + MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE1, /* with Remote and Simple Ctrl */ + MAX14577_MUIC_ADC_OPEN, +}; + +static const unsigned int max14577_extcon_cable[] = { + EXTCON_USB, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_FAST, + EXTCON_CHG_USB_SLOW, + EXTCON_CHG_USB_CDP, + EXTCON_JIG, + EXTCON_NONE, +}; + +/* + * max14577_muic_set_debounce_time - Set the debounce time of ADC + * @info: the instance including private data of max14577 MUIC + * @time: the debounce time of ADC + */ +static int max14577_muic_set_debounce_time(struct max14577_muic_info *info, + enum max14577_muic_adc_debounce_time time) +{ + u8 ret; + + switch (time) { + case ADC_DEBOUNCE_TIME_5MS: + case ADC_DEBOUNCE_TIME_10MS: + case ADC_DEBOUNCE_TIME_25MS: + case ADC_DEBOUNCE_TIME_38_62MS: + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_MUIC_REG_CONTROL3, + CTRL3_ADCDBSET_MASK, + time << CTRL3_ADCDBSET_SHIFT); + if (ret) { + dev_err(info->dev, "failed to set ADC debounce time\n"); + return ret; + } + break; + default: + dev_err(info->dev, "invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +}; + +/* + * max14577_muic_set_path - Set hardware line according to attached cable + * @info: the instance including private data of max14577 MUIC + * @value: the path according to attached cable + * @attached: the state of cable (true:attached, false:detached) + * + * The max14577 MUIC device share outside H/W line among a varity of cables + * so, this function set internal path of H/W line according to the type of + * attached cable. + */ +static int max14577_muic_set_path(struct max14577_muic_info *info, + u8 val, bool attached) +{ + u8 ctrl1, ctrl2 = 0; + int ret; + + /* Set open state to path before changing hw path */ + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_MUIC_REG_CONTROL1, + CLEAR_IDBEN_MICEN_MASK, CTRL1_SW_OPEN); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl1 = val; + else + ctrl1 = CTRL1_SW_OPEN; + + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_MUIC_REG_CONTROL1, + CLEAR_IDBEN_MICEN_MASK, ctrl1); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl2 |= CTRL2_CPEN_MASK; /* LowPwr=0, CPEn=1 */ + else + ctrl2 |= CTRL2_LOWPWR_MASK; /* LowPwr=1, CPEn=0 */ + + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_REG_CONTROL2, + CTRL2_LOWPWR_MASK | CTRL2_CPEN_MASK, ctrl2); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + dev_dbg(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +/* + * max14577_muic_get_cable_type - Return cable type and check cable state + * @info: the instance including private data of max14577 MUIC + * @group: the path according to attached cable + * @attached: store cable state and return + * + * This function check the cable state either attached or detached, + * and then divide precise type of cable according to cable group. + * - max14577_CABLE_GROUP_ADC + * - max14577_CABLE_GROUP_CHG + */ +static int max14577_muic_get_cable_type(struct max14577_muic_info *info, + enum max14577_muic_cable_group group, bool *attached) +{ + int cable_type = 0; + int adc; + int chg_type; + + switch (group) { + case MAX14577_CABLE_GROUP_ADC: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[MAX14577_MUIC_STATUS1] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type) for handling cable when cable is + * detached. + */ + if (adc == MAX14577_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX14577_MUIC_ADC_OPEN; + } else { + *attached = true; + + cable_type = info->prev_cable_type = adc; + } + break; + case MAX14577_CABLE_GROUP_CHG: + /* + * Read charger type to check cable type and decide cable state + * according to type of charger cable. + */ + chg_type = info->status[MAX14577_MUIC_STATUS2] & + STATUS2_CHGTYP_MASK; + chg_type >>= STATUS2_CHGTYP_SHIFT; + + if (chg_type == MAX14577_CHARGER_TYPE_NONE) { + *attached = false; + + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX14577_CHARGER_TYPE_NONE; + } else { + *attached = true; + + /* + * Check current cable state/cable type and store cable + * type(info->prev_chg_type) for handling cable when + * charger cable is detached. + */ + cable_type = info->prev_chg_type = chg_type; + } + + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max14577_muic_jig_handler(struct max14577_muic_info *info, + int cable_type, bool attached) +{ + int ret = 0; + u8 path = CTRL1_SW_OPEN; + + dev_dbg(info->dev, + "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_OFF: /* ADC_JIG_USB_OFF */ + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_ON: /* ADC_JIG_USB_ON */ + /* PATH:AP_USB */ + path = CTRL1_SW_USB; + break; + case MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF: /* ADC_JIG_UART_OFF */ + /* PATH:AP_UART */ + path = CTRL1_SW_UART; + break; + default: + dev_err(info->dev, "failed to detect %s jig cable\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + ret = max14577_muic_set_path(info, path, attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_JIG, attached); + + return 0; +} + +static int max14577_muic_adc_handler(struct max14577_muic_info *info) +{ + int cable_type; + bool attached; + int ret = 0; + + /* Check accessory state which is either detached or attached */ + cable_type = max14577_muic_get_cable_type(info, + MAX14577_CABLE_GROUP_ADC, &attached); + + dev_dbg(info->dev, + "external connector is %s (adc:0x%02x, prev_adc:0x%x)\n", + attached ? "attached" : "detached", cable_type, + info->prev_cable_type); + + switch (cable_type) { + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_ON: + case MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF: + /* JIG */ + ret = max14577_muic_jig_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX14577_MUIC_ADC_GROUND: + case MAX14577_MUIC_ADC_SEND_END_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S3_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S7_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S9_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S10_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S12_BUTTON: + case MAX14577_MUIC_ADC_RESERVED_ACC_1: + case MAX14577_MUIC_ADC_RESERVED_ACC_2: + case MAX14577_MUIC_ADC_RESERVED_ACC_3: + case MAX14577_MUIC_ADC_RESERVED_ACC_4: + case MAX14577_MUIC_ADC_RESERVED_ACC_5: + case MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE2: + case MAX14577_MUIC_ADC_PHONE_POWERED_DEV: + case MAX14577_MUIC_ADC_TTY_CONVERTER: + case MAX14577_MUIC_ADC_UART_CABLE: + case MAX14577_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX14577_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX14577_MUIC_ADC_CEA936A_TYPE2_CHG: + case MAX14577_MUIC_ADC_FACTORY_MODE_UART_ON: + case MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE1: + /* + * This accessory isn't used in general case if it is specially + * needed to detect additional accessory, should implement + * proper operation when this accessory is attached/detached. + */ + dev_info(info->dev, + "accessory is %s but it isn't used (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s accessory (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max14577_muic_chg_handler(struct max14577_muic_info *info) +{ + int chg_type; + bool attached; + int ret = 0; + + chg_type = max14577_muic_get_cable_type(info, + MAX14577_CABLE_GROUP_CHG, &attached); + + dev_dbg(info->dev, + "external connector is %s(chg_type:0x%x, prev_chg_type:0x%x)\n", + attached ? "attached" : "detached", + chg_type, info->prev_chg_type); + + switch (chg_type) { + case MAX14577_CHARGER_TYPE_USB: + /* PATH:AP_USB */ + ret = max14577_muic_set_path(info, info->path_usb, attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_USB, attached); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + break; + case MAX14577_CHARGER_TYPE_DEDICATED_CHG: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + attached); + break; + case MAX14577_CHARGER_TYPE_DOWNSTREAM_PORT: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_CDP, + attached); + break; + case MAX14577_CHARGER_TYPE_SPECIAL_500MA: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SLOW, + attached); + break; + case MAX14577_CHARGER_TYPE_SPECIAL_1A: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_FAST, + attached); + break; + case MAX14577_CHARGER_TYPE_NONE: + case MAX14577_CHARGER_TYPE_DEAD_BATTERY: + break; + default: + dev_err(info->dev, + "failed to detect %s accessory (chg_type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + return -EINVAL; + } + + return 0; +} + +static void max14577_muic_irq_work(struct work_struct *work) +{ + struct max14577_muic_info *info = container_of(work, + struct max14577_muic_info, irq_work); + int ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + ret = max14577_bulk_read(info->max14577->regmap, + MAX14577_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return; + } + + if (info->irq_adc) { + ret = max14577_muic_adc_handler(info); + info->irq_adc = false; + } + if (info->irq_chg) { + ret = max14577_muic_chg_handler(info); + info->irq_chg = false; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +/* + * Sets irq_adc or irq_chg in max14577_muic_info and returns 1. + * Returns 0 if irq_type does not match registered IRQ for this device type. + */ +static int max14577_parse_irq(struct max14577_muic_info *info, int irq_type) +{ + switch (irq_type) { + case MAX14577_IRQ_INT1_ADC: + case MAX14577_IRQ_INT1_ADCLOW: + case MAX14577_IRQ_INT1_ADCERR: + /* + * Handle all of accessory except for + * type of charger accessory. + */ + info->irq_adc = true; + return 1; + case MAX14577_IRQ_INT2_CHGTYP: + case MAX14577_IRQ_INT2_CHGDETRUN: + case MAX14577_IRQ_INT2_DCDTMR: + case MAX14577_IRQ_INT2_DBCHG: + case MAX14577_IRQ_INT2_VBVOLT: + /* Handle charger accessory */ + info->irq_chg = true; + return 1; + default: + return 0; + } +} + +/* + * Sets irq_adc or irq_chg in max14577_muic_info and returns 1. + * Returns 0 if irq_type does not match registered IRQ for this device type. + */ +static int max77836_parse_irq(struct max14577_muic_info *info, int irq_type) +{ + /* First check common max14577 interrupts */ + if (max14577_parse_irq(info, irq_type)) + return 1; + + switch (irq_type) { + case MAX77836_IRQ_INT1_ADC1K: + info->irq_adc = true; + return 1; + case MAX77836_IRQ_INT2_VIDRM: + /* Handle charger accessory */ + info->irq_chg = true; + return 1; + default: + return 0; + } +} + +static irqreturn_t max14577_muic_irq_handler(int irq, void *data) +{ + struct max14577_muic_info *info = data; + int i, irq_type = -1; + bool irq_parsed; + + /* + * We may be called multiple times for different nested IRQ-s. + * Including changes in INT1_ADC and INT2_CGHTYP at once. + * However we only need to know whether it was ADC, charger + * or both interrupts so decode IRQ and turn on proper flags. + */ + for (i = 0; i < info->muic_irqs_num; i++) + if (irq == info->muic_irqs[i].virq) + irq_type = info->muic_irqs[i].irq; + + switch (info->max14577->dev_type) { + case MAXIM_DEVICE_TYPE_MAX77836: + irq_parsed = max77836_parse_irq(info, irq_type); + break; + case MAXIM_DEVICE_TYPE_MAX14577: + default: + irq_parsed = max14577_parse_irq(info, irq_type); + break; + } + + if (!irq_parsed) { + dev_err(info->dev, "muic interrupt: irq %d occurred, skipped\n", + irq_type); + return IRQ_HANDLED; + } + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static int max14577_muic_detect_accessory(struct max14577_muic_info *info) +{ + int ret = 0; + int adc; + int chg_type; + bool attached; + + mutex_lock(&info->mutex); + + /* Read STATUSx register to detect accessory */ + ret = max14577_bulk_read(info->max14577->regmap, + MAX14577_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return ret; + } + + adc = max14577_muic_get_cable_type(info, MAX14577_CABLE_GROUP_ADC, + &attached); + if (attached && adc != MAX14577_MUIC_ADC_OPEN) { + ret = max14577_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + chg_type = max14577_muic_get_cable_type(info, MAX14577_CABLE_GROUP_CHG, + &attached); + if (attached && chg_type != MAX14577_CHARGER_TYPE_NONE) { + ret = max14577_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + mutex_unlock(&info->mutex); + + return 0; +} + +static void max14577_muic_detect_cable_wq(struct work_struct *work) +{ + struct max14577_muic_info *info = container_of(to_delayed_work(work), + struct max14577_muic_info, wq_detcable); + + max14577_muic_detect_accessory(info); +} + +static int max14577_muic_probe(struct platform_device *pdev) +{ + struct max14577 *max14577 = dev_get_drvdata(pdev->dev.parent); + struct max14577_muic_info *info; + int delay_jiffies; + int cable_type; + bool attached; + int ret; + int i; + u8 id; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->max14577 = max14577; + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + ret = devm_work_autocancel(&pdev->dev, &info->irq_work, + max14577_muic_irq_work); + if (ret) + return ret; + + switch (max14577->dev_type) { + case MAXIM_DEVICE_TYPE_MAX77836: + info->muic_irqs = max77836_muic_irqs; + info->muic_irqs_num = ARRAY_SIZE(max77836_muic_irqs); + break; + case MAXIM_DEVICE_TYPE_MAX14577: + default: + info->muic_irqs = max14577_muic_irqs; + info->muic_irqs_num = ARRAY_SIZE(max14577_muic_irqs); + } + + /* Support irq domain for max14577 MUIC device */ + for (i = 0; i < info->muic_irqs_num; i++) { + struct max14577_muic_irq *muic_irq = &info->muic_irqs[i]; + int virq = 0; + + virq = regmap_irq_get_virq(max14577->irq_data, muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max14577_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + max14577_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + return PTR_ERR(info->edev); + } + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return ret; + } + + /* Default h/w line path */ + info->path_usb = CTRL1_SW_USB; + info->path_uart = CTRL1_SW_UART; + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + + /* Set initial path for UART when JIG is connected to get serial logs */ + ret = max14577_bulk_read(info->max14577->regmap, + MAX14577_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "Cannot read STATUS registers\n"); + return ret; + } + cable_type = max14577_muic_get_cable_type(info, MAX14577_CABLE_GROUP_ADC, + &attached); + if (attached && cable_type == MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF) + max14577_muic_set_path(info, info->path_uart, true); + + /* Check revision number of MUIC device*/ + ret = max14577_read_reg(info->max14577->regmap, + MAX14577_REG_DEVICEID, &id); + if (ret < 0) { + dev_err(&pdev->dev, "failed to read revision number\n"); + return ret; + } + dev_info(info->dev, "device ID : 0x%x\n", id); + + /* Set ADC debounce time */ + max14577_muic_set_debounce_time(info, ADC_DEBOUNCE_TIME_25MS); + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, max14577_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + delay_jiffies); + + return ret; +} + +static const struct platform_device_id max14577_muic_id[] = { + { "max14577-muic", MAXIM_DEVICE_TYPE_MAX14577, }, + { "max77836-muic", MAXIM_DEVICE_TYPE_MAX77836, }, + { } +}; +MODULE_DEVICE_TABLE(platform, max14577_muic_id); + +static const struct of_device_id of_max14577_muic_dt_match[] = { + { .compatible = "maxim,max14577-muic", + .data = (void *)MAXIM_DEVICE_TYPE_MAX14577, }, + { .compatible = "maxim,max77836-muic", + .data = (void *)MAXIM_DEVICE_TYPE_MAX77836, }, + { }, +}; +MODULE_DEVICE_TABLE(of, of_max14577_muic_dt_match); + +static struct platform_driver max14577_muic_driver = { + .driver = { + .name = "max14577-muic", + .of_match_table = of_max14577_muic_dt_match, + }, + .probe = max14577_muic_probe, + .id_table = max14577_muic_id, +}; + +module_platform_driver(max14577_muic_driver); + +MODULE_DESCRIPTION("Maxim 14577/77836 Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>, Krzysztof Kozlowski <krzk@kernel.org>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:extcon-max14577"); diff --git a/drivers/extcon/extcon-max3355.c b/drivers/extcon/extcon-max3355.c new file mode 100644 index 000000000..d687d178a --- /dev/null +++ b/drivers/extcon/extcon-max3355.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Maxim Integrated MAX3355 USB OTG chip extcon driver + * + * Copyright (C) 2014-2015 Cogent Embedded, Inc. + * Author: Sergei Shtylyov <sergei.shtylyov@cogentembedded.com> + */ + +#include <linux/extcon-provider.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/platform_device.h> + +struct max3355_data { + struct extcon_dev *edev; + struct gpio_desc *id_gpiod; + struct gpio_desc *shdn_gpiod; +}; + +static const unsigned int max3355_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +static irqreturn_t max3355_id_irq(int irq, void *dev_id) +{ + struct max3355_data *data = dev_id; + int id = gpiod_get_value_cansleep(data->id_gpiod); + + if (id) { + /* + * ID = 1 means USB HOST cable detached. + * As we don't have event for USB peripheral cable attached, + * we simulate USB peripheral attach here. + */ + extcon_set_state_sync(data->edev, EXTCON_USB_HOST, false); + extcon_set_state_sync(data->edev, EXTCON_USB, true); + } else { + /* + * ID = 0 means USB HOST cable attached. + * As we don't have event for USB peripheral cable detached, + * we simulate USB peripheral detach here. + */ + extcon_set_state_sync(data->edev, EXTCON_USB, false); + extcon_set_state_sync(data->edev, EXTCON_USB_HOST, true); + } + + return IRQ_HANDLED; +} + +static int max3355_probe(struct platform_device *pdev) +{ + struct max3355_data *data; + struct gpio_desc *gpiod; + int irq, err; + + data = devm_kzalloc(&pdev->dev, sizeof(struct max3355_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + gpiod = devm_gpiod_get(&pdev->dev, "id", GPIOD_IN); + if (IS_ERR(gpiod)) { + dev_err(&pdev->dev, "failed to get ID_OUT GPIO\n"); + return PTR_ERR(gpiod); + } + data->id_gpiod = gpiod; + + gpiod = devm_gpiod_get(&pdev->dev, "maxim,shdn", GPIOD_OUT_HIGH); + if (IS_ERR(gpiod)) { + dev_err(&pdev->dev, "failed to get SHDN# GPIO\n"); + return PTR_ERR(gpiod); + } + data->shdn_gpiod = gpiod; + + data->edev = devm_extcon_dev_allocate(&pdev->dev, max3355_cable); + if (IS_ERR(data->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return PTR_ERR(data->edev); + } + + err = devm_extcon_dev_register(&pdev->dev, data->edev); + if (err < 0) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return err; + } + + irq = gpiod_to_irq(data->id_gpiod); + if (irq < 0) { + dev_err(&pdev->dev, "failed to translate ID_OUT GPIO to IRQ\n"); + return irq; + } + + err = devm_request_threaded_irq(&pdev->dev, irq, NULL, max3355_id_irq, + IRQF_ONESHOT | IRQF_NO_SUSPEND | + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING, + pdev->name, data); + if (err < 0) { + dev_err(&pdev->dev, "failed to request ID_OUT IRQ\n"); + return err; + } + + platform_set_drvdata(pdev, data); + + /* Perform initial detection */ + max3355_id_irq(irq, data); + + return 0; +} + +static void max3355_remove(struct platform_device *pdev) +{ + struct max3355_data *data = platform_get_drvdata(pdev); + + gpiod_set_value_cansleep(data->shdn_gpiod, 0); +} + +static const struct of_device_id max3355_match_table[] = { + { .compatible = "maxim,max3355", }, + { } +}; +MODULE_DEVICE_TABLE(of, max3355_match_table); + +static struct platform_driver max3355_driver = { + .probe = max3355_probe, + .remove = max3355_remove, + .driver = { + .name = "extcon-max3355", + .of_match_table = max3355_match_table, + }, +}; + +module_platform_driver(max3355_driver); + +MODULE_AUTHOR("Sergei Shtylyov <sergei.shtylyov@cogentembedded.com>"); +MODULE_DESCRIPTION("Maxim MAX3355 extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-max77693.c b/drivers/extcon/extcon-max77693.c new file mode 100644 index 000000000..2c567e0b7 --- /dev/null +++ b/drivers/extcon/extcon-max77693.c @@ -0,0 +1,1280 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// extcon-max77693.c - MAX77693 extcon driver to support MAX77693 MUIC +// +// Copyright (C) 2012 Samsung Electrnoics +// Chanwoo Choi <cw00.choi@samsung.com> + +#include <linux/devm-helpers.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/input.h> +#include <linux/interrupt.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/mfd/max77693.h> +#include <linux/mfd/max77693-common.h> +#include <linux/mfd/max77693-private.h> +#include <linux/extcon-provider.h> +#include <linux/regmap.h> +#include <linux/irqdomain.h> + +#define DEV_NAME "max77693-muic" +#define DELAY_MS_DEFAULT 20000 /* unit: millisecond */ + +/* + * Default value of MAX77693 register to bring up MUIC device. + * If user don't set some initial value for MUIC device through platform data, + * extcon-max77693 driver use 'default_init_data' to bring up base operation + * of MAX77693 MUIC device. + */ +static struct max77693_reg_data default_init_data[] = { + { + /* STATUS2 - [3]ChgDetRun */ + .addr = MAX77693_MUIC_REG_STATUS2, + .data = MAX77693_STATUS2_CHGDETRUN_MASK, + }, { + /* INTMASK1 - Unmask [3]ADC1KM,[0]ADCM */ + .addr = MAX77693_MUIC_REG_INTMASK1, + .data = INTMASK1_ADC1K_MASK + | INTMASK1_ADC_MASK, + }, { + /* INTMASK2 - Unmask [0]ChgTypM */ + .addr = MAX77693_MUIC_REG_INTMASK2, + .data = INTMASK2_CHGTYP_MASK, + }, { + /* INTMASK3 - Mask all of interrupts */ + .addr = MAX77693_MUIC_REG_INTMASK3, + .data = 0x0, + }, { + /* CDETCTRL2 */ + .addr = MAX77693_MUIC_REG_CDETCTRL2, + .data = CDETCTRL2_VIDRMEN_MASK + | CDETCTRL2_DXOVPEN_MASK, + }, +}; + +enum max77693_muic_adc_debounce_time { + ADC_DEBOUNCE_TIME_5MS = 0, + ADC_DEBOUNCE_TIME_10MS, + ADC_DEBOUNCE_TIME_25MS, + ADC_DEBOUNCE_TIME_38_62MS, +}; + +struct max77693_muic_info { + struct device *dev; + struct max77693_dev *max77693; + struct extcon_dev *edev; + int prev_cable_type; + int prev_cable_type_gnd; + int prev_chg_type; + int prev_button_type; + u8 status[2]; + + int irq; + struct work_struct irq_work; + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; + + /* Button of dock device */ + struct input_dev *dock; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + int path_usb; + int path_uart; +}; + +enum max77693_muic_cable_group { + MAX77693_CABLE_GROUP_ADC = 0, + MAX77693_CABLE_GROUP_ADC_GND, + MAX77693_CABLE_GROUP_CHG, + MAX77693_CABLE_GROUP_VBVOLT, +}; + +enum max77693_muic_charger_type { + MAX77693_CHARGER_TYPE_NONE = 0, + MAX77693_CHARGER_TYPE_USB, + MAX77693_CHARGER_TYPE_DOWNSTREAM_PORT, + MAX77693_CHARGER_TYPE_DEDICATED_CHG, + MAX77693_CHARGER_TYPE_APPLE_500MA, + MAX77693_CHARGER_TYPE_APPLE_1A_2A, + MAX77693_CHARGER_TYPE_DEAD_BATTERY = 7, +}; + +/** + * struct max77693_muic_irq + * @irq: the index of irq list of MUIC device. + * @name: the name of irq. + * @virq: the virtual irq to use irq domain + */ +struct max77693_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max77693_muic_irq muic_irqs[] = { + { MAX77693_MUIC_IRQ_INT1_ADC, "muic-ADC" }, + { MAX77693_MUIC_IRQ_INT1_ADC_LOW, "muic-ADCLOW" }, + { MAX77693_MUIC_IRQ_INT1_ADC_ERR, "muic-ADCError" }, + { MAX77693_MUIC_IRQ_INT1_ADC1K, "muic-ADC1K" }, + { MAX77693_MUIC_IRQ_INT2_CHGTYP, "muic-CHGTYP" }, + { MAX77693_MUIC_IRQ_INT2_CHGDETREUN, "muic-CHGDETREUN" }, + { MAX77693_MUIC_IRQ_INT2_DCDTMR, "muic-DCDTMR" }, + { MAX77693_MUIC_IRQ_INT2_DXOVP, "muic-DXOVP" }, + { MAX77693_MUIC_IRQ_INT2_VBVOLT, "muic-VBVOLT" }, + { MAX77693_MUIC_IRQ_INT2_VIDRM, "muic-VIDRM" }, + { MAX77693_MUIC_IRQ_INT3_EOC, "muic-EOC" }, + { MAX77693_MUIC_IRQ_INT3_CGMBC, "muic-CGMBC" }, + { MAX77693_MUIC_IRQ_INT3_OVP, "muic-OVP" }, + { MAX77693_MUIC_IRQ_INT3_MBCCHG_ERR, "muic-MBCCHG_ERR" }, + { MAX77693_MUIC_IRQ_INT3_CHG_ENABLED, "muic-CHG_ENABLED" }, + { MAX77693_MUIC_IRQ_INT3_BAT_DET, "muic-BAT_DET" }, +}; + +/* Define supported accessory type */ +enum max77693_muic_acc_type { + MAX77693_MUIC_ADC_GROUND = 0x0, + MAX77693_MUIC_ADC_SEND_END_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S1_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S2_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S3_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S4_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S5_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S6_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S7_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S8_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S9_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S10_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S11_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S12_BUTTON, + MAX77693_MUIC_ADC_RESERVED_ACC_1, + MAX77693_MUIC_ADC_RESERVED_ACC_2, + MAX77693_MUIC_ADC_RESERVED_ACC_3, + MAX77693_MUIC_ADC_RESERVED_ACC_4, + MAX77693_MUIC_ADC_RESERVED_ACC_5, + MAX77693_MUIC_ADC_CEA936_AUDIO, + MAX77693_MUIC_ADC_PHONE_POWERED_DEV, + MAX77693_MUIC_ADC_TTY_CONVERTER, + MAX77693_MUIC_ADC_UART_CABLE, + MAX77693_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX77693_MUIC_ADC_FACTORY_MODE_USB_OFF, + MAX77693_MUIC_ADC_FACTORY_MODE_USB_ON, + MAX77693_MUIC_ADC_AV_CABLE_NOLOAD, + MAX77693_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF, + MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON, + MAX77693_MUIC_ADC_AUDIO_MODE_REMOTE, + MAX77693_MUIC_ADC_OPEN, + + /* + * The below accessories have same ADC value so ADCLow and + * ADC1K bit is used to separate specific accessory. + */ + /* ADC|VBVolot|ADCLow|ADC1K| */ + MAX77693_MUIC_GND_USB_HOST = 0x100, /* 0x0| 0| 0| 0| */ + MAX77693_MUIC_GND_USB_HOST_VB = 0x104, /* 0x0| 1| 0| 0| */ + MAX77693_MUIC_GND_AV_CABLE_LOAD = 0x102,/* 0x0| 0| 1| 0| */ + MAX77693_MUIC_GND_MHL = 0x103, /* 0x0| 0| 1| 1| */ + MAX77693_MUIC_GND_MHL_VB = 0x107, /* 0x0| 1| 1| 1| */ +}; + +/* + * MAX77693 MUIC device support below list of accessories(external connector) + */ +static const unsigned int max77693_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_FAST, + EXTCON_CHG_USB_SLOW, + EXTCON_CHG_USB_CDP, + EXTCON_DISP_MHL, + EXTCON_JIG, + EXTCON_DOCK, + EXTCON_NONE, +}; + +/* + * max77693_muic_set_debounce_time - Set the debounce time of ADC + * @info: the instance including private data of max77693 MUIC + * @time: the debounce time of ADC + */ +static int max77693_muic_set_debounce_time(struct max77693_muic_info *info, + enum max77693_muic_adc_debounce_time time) +{ + int ret; + + switch (time) { + case ADC_DEBOUNCE_TIME_5MS: + case ADC_DEBOUNCE_TIME_10MS: + case ADC_DEBOUNCE_TIME_25MS: + case ADC_DEBOUNCE_TIME_38_62MS: + /* + * Don't touch BTLDset, JIGset when you want to change adc + * debounce time. If it writes other than 0 to BTLDset, JIGset + * muic device will be reset and loose current state. + */ + ret = regmap_write(info->max77693->regmap_muic, + MAX77693_MUIC_REG_CTRL3, + time << MAX77693_CONTROL3_ADCDBSET_SHIFT); + if (ret) { + dev_err(info->dev, "failed to set ADC debounce time\n"); + return ret; + } + break; + default: + dev_err(info->dev, "invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +}; + +/* + * max77693_muic_set_path - Set hardware line according to attached cable + * @info: the instance including private data of max77693 MUIC + * @value: the path according to attached cable + * @attached: the state of cable (true:attached, false:detached) + * + * The max77693 MUIC device share outside H/W line among a varity of cables + * so, this function set internal path of H/W line according to the type of + * attached cable. + */ +static int max77693_muic_set_path(struct max77693_muic_info *info, + u8 val, bool attached) +{ + int ret; + unsigned int ctrl1, ctrl2 = 0; + + if (attached) + ctrl1 = val; + else + ctrl1 = MAX77693_CONTROL1_SW_OPEN; + + ret = regmap_update_bits(info->max77693->regmap_muic, + MAX77693_MUIC_REG_CTRL1, COMP_SW_MASK, ctrl1); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl2 |= MAX77693_CONTROL2_CPEN_MASK; /* LowPwr=0, CPEn=1 */ + else + ctrl2 |= MAX77693_CONTROL2_LOWPWR_MASK; /* LowPwr=1, CPEn=0 */ + + ret = regmap_update_bits(info->max77693->regmap_muic, + MAX77693_MUIC_REG_CTRL2, + MAX77693_CONTROL2_LOWPWR_MASK | MAX77693_CONTROL2_CPEN_MASK, + ctrl2); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + dev_info(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +/* + * max77693_muic_get_cable_type - Return cable type and check cable state + * @info: the instance including private data of max77693 MUIC + * @group: the path according to attached cable + * @attached: store cable state and return + * + * This function check the cable state either attached or detached, + * and then divide precise type of cable according to cable group. + * - MAX77693_CABLE_GROUP_ADC + * - MAX77693_CABLE_GROUP_ADC_GND + * - MAX77693_CABLE_GROUP_CHG + * - MAX77693_CABLE_GROUP_VBVOLT + */ +static int max77693_muic_get_cable_type(struct max77693_muic_info *info, + enum max77693_muic_cable_group group, bool *attached) +{ + int cable_type = 0; + int adc; + int adc1k; + int adclow; + int vbvolt; + int chg_type; + + switch (group) { + case MAX77693_CABLE_GROUP_ADC: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & MAX77693_STATUS1_ADC_MASK; + adc >>= MAX77693_STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type) for handling cable when cable is + * detached. + */ + if (adc == MAX77693_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX77693_MUIC_ADC_OPEN; + } else { + *attached = true; + + cable_type = info->prev_cable_type = adc; + } + break; + case MAX77693_CABLE_GROUP_ADC_GND: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & MAX77693_STATUS1_ADC_MASK; + adc >>= MAX77693_STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type/_gnd) for handling cable when cable + * is detached. + */ + if (adc == MAX77693_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type_gnd; + info->prev_cable_type_gnd = MAX77693_MUIC_ADC_OPEN; + } else { + *attached = true; + + adclow = info->status[0] & MAX77693_STATUS1_ADCLOW_MASK; + adclow >>= MAX77693_STATUS1_ADCLOW_SHIFT; + adc1k = info->status[0] & MAX77693_STATUS1_ADC1K_MASK; + adc1k >>= MAX77693_STATUS1_ADC1K_SHIFT; + + vbvolt = info->status[1] & MAX77693_STATUS2_VBVOLT_MASK; + vbvolt >>= MAX77693_STATUS2_VBVOLT_SHIFT; + + /** + * [0x1|VBVolt|ADCLow|ADC1K] + * [0x1| 0| 0| 0] USB_HOST + * [0x1| 1| 0| 0] USB_HSOT_VB + * [0x1| 0| 1| 0] Audio Video cable with load + * [0x1| 0| 1| 1] MHL without charging cable + * [0x1| 1| 1| 1] MHL with charging cable + */ + cable_type = ((0x1 << 8) + | (vbvolt << 2) + | (adclow << 1) + | adc1k); + + info->prev_cable_type = adc; + info->prev_cable_type_gnd = cable_type; + } + + break; + case MAX77693_CABLE_GROUP_CHG: + /* + * Read charger type to check cable type and decide cable state + * according to type of charger cable. + */ + chg_type = info->status[1] & MAX77693_STATUS2_CHGTYP_MASK; + chg_type >>= MAX77693_STATUS2_CHGTYP_SHIFT; + + if (chg_type == MAX77693_CHARGER_TYPE_NONE) { + *attached = false; + + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77693_CHARGER_TYPE_NONE; + } else { + *attached = true; + + /* + * Check current cable state/cable type and store cable + * type(info->prev_chg_type) for handling cable when + * charger cable is detached. + */ + cable_type = info->prev_chg_type = chg_type; + } + + break; + case MAX77693_CABLE_GROUP_VBVOLT: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & MAX77693_STATUS1_ADC_MASK; + adc >>= MAX77693_STATUS1_ADC_SHIFT; + chg_type = info->status[1] & MAX77693_STATUS2_CHGTYP_MASK; + chg_type >>= MAX77693_STATUS2_CHGTYP_SHIFT; + + if (adc == MAX77693_MUIC_ADC_OPEN + && chg_type == MAX77693_CHARGER_TYPE_NONE) + *attached = false; + else + *attached = true; + + /* + * Read vbvolt field, if vbvolt is 1, + * this cable is used for charging. + */ + vbvolt = info->status[1] & MAX77693_STATUS2_VBVOLT_MASK; + vbvolt >>= MAX77693_STATUS2_VBVOLT_SHIFT; + + cable_type = vbvolt; + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max77693_muic_dock_handler(struct max77693_muic_info *info, + int cable_type, bool attached) +{ + int ret = 0; + int vbvolt; + bool cable_attached; + unsigned int dock_id; + + dev_info(info->dev, + "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX77693_MUIC_ADC_RESERVED_ACC_3: /* Dock-Smart */ + /* + * Check power cable whether attached or detached state. + * The Dock-Smart device need surely external power supply. + * If power cable(USB/TA) isn't connected to Dock device, + * user can't use Dock-Smart for desktop mode. + */ + vbvolt = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_VBVOLT, &cable_attached); + if (attached && !vbvolt) { + dev_warn(info->dev, + "Cannot detect external power supply\n"); + return 0; + } + + /* + * Notify Dock/MHL state. + * - Dock device include three type of cable which + * are HDMI, USB for mouse/keyboard and micro-usb port + * for USB/TA cable. Dock device need always exteranl + * power supply(USB/TA cable through micro-usb cable). Dock + * device support screen output of target to separate + * monitor and mouse/keyboard for desktop mode. + * + * Features of 'USB/TA cable with Dock device' + * - Support MHL + * - Support external output feature of audio + * - Support charging through micro-usb port without data + * connection if TA cable is connected to target. + * - Support charging and data connection through micro-usb port + * if USB cable is connected between target and host + * device. + * - Support OTG(On-The-Go) device (Ex: Mouse/Keyboard) + */ + ret = max77693_muic_set_path(info, info->path_usb, attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_DOCK, attached); + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, attached); + goto out; + case MAX77693_MUIC_ADC_AUDIO_MODE_REMOTE: /* Dock-Desk */ + dock_id = EXTCON_DOCK; + break; + case MAX77693_MUIC_ADC_AV_CABLE_NOLOAD: /* Dock-Audio */ + dock_id = EXTCON_DOCK; + if (!attached) { + extcon_set_state_sync(info->edev, EXTCON_USB, false); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + false); + } + break; + default: + dev_err(info->dev, "failed to detect %s dock device\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + /* Dock-Car/Desk/Audio, PATH:AUDIO */ + ret = max77693_muic_set_path(info, MAX77693_CONTROL1_SW_AUDIO, + attached); + if (ret < 0) + return ret; + extcon_set_state_sync(info->edev, dock_id, attached); + +out: + return 0; +} + +static int max77693_muic_dock_button_handler(struct max77693_muic_info *info, + int button_type, bool attached) +{ + struct input_dev *dock = info->dock; + unsigned int code; + + switch (button_type) { + case MAX77693_MUIC_ADC_REMOTE_S3_BUTTON-1 + ... MAX77693_MUIC_ADC_REMOTE_S3_BUTTON+1: + /* DOCK_KEY_PREV */ + code = KEY_PREVIOUSSONG; + break; + case MAX77693_MUIC_ADC_REMOTE_S7_BUTTON-1 + ... MAX77693_MUIC_ADC_REMOTE_S7_BUTTON+1: + /* DOCK_KEY_NEXT */ + code = KEY_NEXTSONG; + break; + case MAX77693_MUIC_ADC_REMOTE_S9_BUTTON: + /* DOCK_VOL_DOWN */ + code = KEY_VOLUMEDOWN; + break; + case MAX77693_MUIC_ADC_REMOTE_S10_BUTTON: + /* DOCK_VOL_UP */ + code = KEY_VOLUMEUP; + break; + case MAX77693_MUIC_ADC_REMOTE_S12_BUTTON-1 + ... MAX77693_MUIC_ADC_REMOTE_S12_BUTTON+1: + /* DOCK_KEY_PLAY_PAUSE */ + code = KEY_PLAYPAUSE; + break; + default: + dev_err(info->dev, + "failed to detect %s key (adc:0x%x)\n", + attached ? "pressed" : "released", button_type); + return -EINVAL; + } + + input_event(dock, EV_KEY, code, attached); + input_sync(dock); + + return 0; +} + +static int max77693_muic_adc_ground_handler(struct max77693_muic_info *info) +{ + int cable_type_gnd; + int ret = 0; + bool attached; + + cable_type_gnd = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC_GND, &attached); + + switch (cable_type_gnd) { + case MAX77693_MUIC_GND_USB_HOST: + case MAX77693_MUIC_GND_USB_HOST_VB: + /* USB_HOST, PATH: AP_USB */ + ret = max77693_muic_set_path(info, MAX77693_CONTROL1_SW_USB, + attached); + if (ret < 0) + return ret; + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, attached); + break; + case MAX77693_MUIC_GND_AV_CABLE_LOAD: + /* Audio Video Cable with load, PATH:AUDIO */ + ret = max77693_muic_set_path(info, MAX77693_CONTROL1_SW_AUDIO, + attached); + if (ret < 0) + return ret; + extcon_set_state_sync(info->edev, EXTCON_USB, attached); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + break; + case MAX77693_MUIC_GND_MHL: + case MAX77693_MUIC_GND_MHL_VB: + /* MHL or MHL with USB/TA cable */ + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, attached); + break; + default: + dev_err(info->dev, "failed to detect %s cable of gnd type\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + return 0; +} + +static int max77693_muic_jig_handler(struct max77693_muic_info *info, + int cable_type, bool attached) +{ + int ret = 0; + u8 path = MAX77693_CONTROL1_SW_OPEN; + + dev_info(info->dev, + "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_OFF: /* ADC_JIG_USB_OFF */ + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_ON: /* ADC_JIG_USB_ON */ + /* PATH:AP_USB */ + path = MAX77693_CONTROL1_SW_USB; + break; + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF: /* ADC_JIG_UART_OFF */ + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON: /* ADC_JIG_UART_ON */ + /* PATH:AP_UART */ + path = MAX77693_CONTROL1_SW_UART; + break; + default: + dev_err(info->dev, "failed to detect %s jig cable\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + ret = max77693_muic_set_path(info, path, attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_JIG, attached); + + return 0; +} + +static int max77693_muic_adc_handler(struct max77693_muic_info *info) +{ + int cable_type; + int button_type; + bool attached; + int ret = 0; + + /* Check accessory state which is either detached or attached */ + cable_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC, &attached); + + dev_info(info->dev, + "external connector is %s (adc:0x%02x, prev_adc:0x%x)\n", + attached ? "attached" : "detached", cable_type, + info->prev_cable_type); + + switch (cable_type) { + case MAX77693_MUIC_ADC_GROUND: + /* USB_HOST/MHL/Audio */ + max77693_muic_adc_ground_handler(info); + break; + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_ON: + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF: + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON: + /* JIG */ + ret = max77693_muic_jig_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX77693_MUIC_ADC_RESERVED_ACC_3: /* Dock-Smart */ + case MAX77693_MUIC_ADC_AUDIO_MODE_REMOTE: /* Dock-Desk */ + case MAX77693_MUIC_ADC_AV_CABLE_NOLOAD: /* Dock-Audio */ + /* + * DOCK device + * + * The MAX77693 MUIC device can detect total 34 cable type + * except of charger cable and MUIC device didn't define + * specfic role of cable in the range of from 0x01 to 0x12 + * of ADC value. So, can use/define cable with no role according + * to schema of hardware board. + */ + ret = max77693_muic_dock_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX77693_MUIC_ADC_REMOTE_S3_BUTTON: /* DOCK_KEY_PREV */ + case MAX77693_MUIC_ADC_REMOTE_S7_BUTTON: /* DOCK_KEY_NEXT */ + case MAX77693_MUIC_ADC_REMOTE_S9_BUTTON: /* DOCK_VOL_DOWN */ + case MAX77693_MUIC_ADC_REMOTE_S10_BUTTON: /* DOCK_VOL_UP */ + case MAX77693_MUIC_ADC_REMOTE_S12_BUTTON: /* DOCK_KEY_PLAY_PAUSE */ + /* + * Button of DOCK device + * - the Prev/Next/Volume Up/Volume Down/Play-Pause button + * + * The MAX77693 MUIC device can detect total 34 cable type + * except of charger cable and MUIC device didn't define + * specfic role of cable in the range of from 0x01 to 0x12 + * of ADC value. So, can use/define cable with no role according + * to schema of hardware board. + */ + if (attached) + button_type = info->prev_button_type = cable_type; + else + button_type = info->prev_button_type; + + ret = max77693_muic_dock_button_handler(info, button_type, + attached); + if (ret < 0) + return ret; + break; + case MAX77693_MUIC_ADC_SEND_END_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX77693_MUIC_ADC_RESERVED_ACC_1: + case MAX77693_MUIC_ADC_RESERVED_ACC_2: + case MAX77693_MUIC_ADC_RESERVED_ACC_4: + case MAX77693_MUIC_ADC_RESERVED_ACC_5: + case MAX77693_MUIC_ADC_CEA936_AUDIO: + case MAX77693_MUIC_ADC_PHONE_POWERED_DEV: + case MAX77693_MUIC_ADC_TTY_CONVERTER: + case MAX77693_MUIC_ADC_UART_CABLE: + case MAX77693_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX77693_MUIC_ADC_CEA936A_TYPE2_CHG: + /* + * This accessory isn't used in general case if it is specially + * needed to detect additional accessory, should implement + * proper operation when this accessory is attached/detached. + */ + dev_info(info->dev, + "accessory is %s but it isn't used (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s accessory (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max77693_muic_chg_handler(struct max77693_muic_info *info) +{ + int chg_type; + int cable_type_gnd; + int cable_type; + bool attached; + bool cable_attached; + int ret = 0; + + chg_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_CHG, &attached); + + dev_info(info->dev, + "external connector is %s(chg_type:0x%x, prev_chg_type:0x%x)\n", + attached ? "attached" : "detached", + chg_type, info->prev_chg_type); + + switch (chg_type) { + case MAX77693_CHARGER_TYPE_USB: + case MAX77693_CHARGER_TYPE_DEDICATED_CHG: + case MAX77693_CHARGER_TYPE_NONE: + /* Check MAX77693_CABLE_GROUP_ADC_GND type */ + cable_type_gnd = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC_GND, + &cable_attached); + switch (cable_type_gnd) { + case MAX77693_MUIC_GND_MHL: + case MAX77693_MUIC_GND_MHL_VB: + /* + * MHL cable with USB/TA cable + * - MHL cable include two port(HDMI line and separate + * micro-usb port. When the target connect MHL cable, + * extcon driver check whether USB/TA cable is + * connected. If USB/TA cable is connected, extcon + * driver notify state to notifiee for charging battery. + * + * Features of 'USB/TA with MHL cable' + * - Support MHL + * - Support charging through micro-usb port without + * data connection + */ + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + attached); + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, + cable_attached); + break; + } + + /* Check MAX77693_CABLE_GROUP_ADC type */ + cable_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC, + &cable_attached); + switch (cable_type) { + case MAX77693_MUIC_ADC_AV_CABLE_NOLOAD: /* Dock-Audio */ + /* + * Dock-Audio device with USB/TA cable + * - Dock device include two port(Dock-Audio and micro- + * usb port). When the target connect Dock-Audio device, + * extcon driver check whether USB/TA cable is connected + * or not. If USB/TA cable is connected, extcon driver + * notify state to notifiee for charging battery. + * + * Features of 'USB/TA cable with Dock-Audio device' + * - Support external output feature of audio. + * - Support charging through micro-usb port without + * data connection. + */ + extcon_set_state_sync(info->edev, EXTCON_USB, + attached); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + + if (!cable_attached) + extcon_set_state_sync(info->edev, EXTCON_DOCK, + cable_attached); + break; + case MAX77693_MUIC_ADC_RESERVED_ACC_3: /* Dock-Smart */ + /* + * Dock-Smart device with USB/TA cable + * - Dock-Desk device include three type of cable which + * are HDMI, USB for mouse/keyboard and micro-usb port + * for USB/TA cable. Dock-Smart device need always + * exteranl power supply(USB/TA cable through micro-usb + * cable). Dock-Smart device support screen output of + * target to separate monitor and mouse/keyboard for + * desktop mode. + * + * Features of 'USB/TA cable with Dock-Smart device' + * - Support MHL + * - Support external output feature of audio + * - Support charging through micro-usb port without + * data connection if TA cable is connected to target. + * - Support charging and data connection through micro- + * usb port if USB cable is connected between target + * and host device + * - Support OTG(On-The-Go) device (Ex: Mouse/Keyboard) + */ + ret = max77693_muic_set_path(info, info->path_usb, + attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_DOCK, + attached); + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, + attached); + break; + } + + /* Check MAX77693_CABLE_GROUP_CHG type */ + switch (chg_type) { + case MAX77693_CHARGER_TYPE_NONE: + /* + * When MHL(with USB/TA cable) or Dock-Audio with USB/TA + * cable is attached, muic device happen below two irq. + * - 'MAX77693_MUIC_IRQ_INT1_ADC' for detecting + * MHL/Dock-Audio. + * - 'MAX77693_MUIC_IRQ_INT2_CHGTYP' for detecting + * USB/TA cable connected to MHL or Dock-Audio. + * Always, happen eariler MAX77693_MUIC_IRQ_INT1_ADC + * irq than MAX77693_MUIC_IRQ_INT2_CHGTYP irq. + * + * If user attach MHL (with USB/TA cable and immediately + * detach MHL with USB/TA cable before MAX77693_MUIC_IRQ + * _INT2_CHGTYP irq is happened, USB/TA cable remain + * connected state to target. But USB/TA cable isn't + * connected to target. The user be face with unusual + * action. So, driver should check this situation in + * spite of, that previous charger type is N/A. + */ + break; + case MAX77693_CHARGER_TYPE_USB: + /* Only USB cable, PATH:AP_USB */ + ret = max77693_muic_set_path(info, info->path_usb, + attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_USB, + attached); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + break; + case MAX77693_CHARGER_TYPE_DEDICATED_CHG: + /* Only TA cable */ + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + attached); + break; + } + break; + case MAX77693_CHARGER_TYPE_DOWNSTREAM_PORT: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_CDP, + attached); + break; + case MAX77693_CHARGER_TYPE_APPLE_500MA: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SLOW, + attached); + break; + case MAX77693_CHARGER_TYPE_APPLE_1A_2A: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_FAST, + attached); + break; + case MAX77693_CHARGER_TYPE_DEAD_BATTERY: + break; + default: + dev_err(info->dev, + "failed to detect %s accessory (chg_type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + return -EINVAL; + } + + return 0; +} + +static void max77693_muic_irq_work(struct work_struct *work) +{ + struct max77693_muic_info *info = container_of(work, + struct max77693_muic_info, irq_work); + int irq_type = -1; + int i, ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) + if (info->irq == muic_irqs[i].virq) + irq_type = muic_irqs[i].irq; + + ret = regmap_bulk_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return; + } + + switch (irq_type) { + case MAX77693_MUIC_IRQ_INT1_ADC: + case MAX77693_MUIC_IRQ_INT1_ADC_LOW: + case MAX77693_MUIC_IRQ_INT1_ADC_ERR: + case MAX77693_MUIC_IRQ_INT1_ADC1K: + /* + * Handle all of accessory except for + * type of charger accessory. + */ + ret = max77693_muic_adc_handler(info); + break; + case MAX77693_MUIC_IRQ_INT2_CHGTYP: + case MAX77693_MUIC_IRQ_INT2_CHGDETREUN: + case MAX77693_MUIC_IRQ_INT2_DCDTMR: + case MAX77693_MUIC_IRQ_INT2_DXOVP: + case MAX77693_MUIC_IRQ_INT2_VBVOLT: + case MAX77693_MUIC_IRQ_INT2_VIDRM: + /* Handle charger accessory */ + ret = max77693_muic_chg_handler(info); + break; + case MAX77693_MUIC_IRQ_INT3_EOC: + case MAX77693_MUIC_IRQ_INT3_CGMBC: + case MAX77693_MUIC_IRQ_INT3_OVP: + case MAX77693_MUIC_IRQ_INT3_MBCCHG_ERR: + case MAX77693_MUIC_IRQ_INT3_CHG_ENABLED: + case MAX77693_MUIC_IRQ_INT3_BAT_DET: + break; + default: + dev_err(info->dev, "muic interrupt: irq %d occurred\n", + irq_type); + mutex_unlock(&info->mutex); + return; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +static irqreturn_t max77693_muic_irq_handler(int irq, void *data) +{ + struct max77693_muic_info *info = data; + + info->irq = irq; + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static const struct regmap_config max77693_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, +}; + +static int max77693_muic_detect_accessory(struct max77693_muic_info *info) +{ + int ret = 0; + int adc; + int chg_type; + bool attached; + + mutex_lock(&info->mutex); + + /* Read STATUSx register to detect accessory */ + ret = regmap_bulk_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return ret; + } + + adc = max77693_muic_get_cable_type(info, MAX77693_CABLE_GROUP_ADC, + &attached); + if (attached && adc != MAX77693_MUIC_ADC_OPEN) { + ret = max77693_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + chg_type = max77693_muic_get_cable_type(info, MAX77693_CABLE_GROUP_CHG, + &attached); + if (attached && chg_type != MAX77693_CHARGER_TYPE_NONE) { + ret = max77693_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + mutex_unlock(&info->mutex); + + return 0; +} + +static void max77693_muic_detect_cable_wq(struct work_struct *work) +{ + struct max77693_muic_info *info = container_of(to_delayed_work(work), + struct max77693_muic_info, wq_detcable); + + max77693_muic_detect_accessory(info); +} + +static int max77693_muic_probe(struct platform_device *pdev) +{ + struct max77693_dev *max77693 = dev_get_drvdata(pdev->dev.parent); + struct max77693_platform_data *pdata = dev_get_platdata(max77693->dev); + struct max77693_muic_info *info; + struct max77693_reg_data *init_data; + int num_init_data; + int delay_jiffies; + int cable_type; + bool attached; + int ret; + int i; + unsigned int id; + + info = devm_kzalloc(&pdev->dev, sizeof(struct max77693_muic_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->max77693 = max77693; + if (info->max77693->regmap_muic) { + dev_dbg(&pdev->dev, "allocate register map\n"); + } else { + info->max77693->regmap_muic = devm_regmap_init_i2c( + info->max77693->i2c_muic, + &max77693_muic_regmap_config); + if (IS_ERR(info->max77693->regmap_muic)) { + ret = PTR_ERR(info->max77693->regmap_muic); + dev_err(max77693->dev, + "failed to allocate register map: %d\n", ret); + return ret; + } + } + + /* Register input device for button of dock device */ + info->dock = devm_input_allocate_device(&pdev->dev); + if (!info->dock) { + dev_err(&pdev->dev, "%s: failed to allocate input\n", __func__); + return -ENOMEM; + } + info->dock->name = "max77693-muic/dock"; + info->dock->phys = "max77693-muic/extcon"; + info->dock->dev.parent = &pdev->dev; + + __set_bit(EV_REP, info->dock->evbit); + + input_set_capability(info->dock, EV_KEY, KEY_VOLUMEUP); + input_set_capability(info->dock, EV_KEY, KEY_VOLUMEDOWN); + input_set_capability(info->dock, EV_KEY, KEY_PLAYPAUSE); + input_set_capability(info->dock, EV_KEY, KEY_PREVIOUSSONG); + input_set_capability(info->dock, EV_KEY, KEY_NEXTSONG); + + ret = input_register_device(info->dock); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot register input device error(%d)\n", + ret); + return ret; + } + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + ret = devm_work_autocancel(&pdev->dev, &info->irq_work, + max77693_muic_irq_work); + if (ret) + return ret; + + /* Support irq domain for MAX77693 MUIC device */ + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) { + struct max77693_muic_irq *muic_irq = &muic_irqs[i]; + int virq; + + virq = regmap_irq_get_virq(max77693->irq_data_muic, + muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max77693_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + max77693_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + return PTR_ERR(info->edev); + } + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return ret; + } + + /* Initialize MUIC register by using platform data or default data */ + if (pdata && pdata->muic_data) { + init_data = pdata->muic_data->init_data; + num_init_data = pdata->muic_data->num_init_data; + } else { + init_data = default_init_data; + num_init_data = ARRAY_SIZE(default_init_data); + } + + for (i = 0; i < num_init_data; i++) { + regmap_write(info->max77693->regmap_muic, + init_data[i].addr, + init_data[i].data); + } + + if (pdata && pdata->muic_data) { + struct max77693_muic_platform_data *muic_pdata + = pdata->muic_data; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + if (muic_pdata->path_uart) + info->path_uart = muic_pdata->path_uart; + else + info->path_uart = MAX77693_CONTROL1_SW_UART; + + if (muic_pdata->path_usb) + info->path_usb = muic_pdata->path_usb; + else + info->path_usb = MAX77693_CONTROL1_SW_USB; + + /* + * Default delay time for detecting cable state + * after certain time. + */ + if (muic_pdata->detcable_delay_ms) + delay_jiffies = + msecs_to_jiffies(muic_pdata->detcable_delay_ms); + else + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } else { + info->path_usb = MAX77693_CONTROL1_SW_USB; + info->path_uart = MAX77693_CONTROL1_SW_UART; + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } + + /* Set initial path for UART when JIG is connected to get serial logs */ + ret = regmap_bulk_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + return ret; + } + cable_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC, &attached); + if (attached && (cable_type == MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON || + cable_type == MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF)) + max77693_muic_set_path(info, info->path_uart, true); + + /* Check revision number of MUIC device*/ + ret = regmap_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_ID, &id); + if (ret < 0) { + dev_err(&pdev->dev, "failed to read revision number\n"); + return ret; + } + dev_info(info->dev, "device ID : 0x%x\n", id); + + /* Set ADC debounce time */ + max77693_muic_set_debounce_time(info, ADC_DEBOUNCE_TIME_25MS); + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, max77693_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + delay_jiffies); + + return ret; +} + +static const struct of_device_id of_max77693_muic_dt_match[] = { + { .compatible = "maxim,max77693-muic", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, of_max77693_muic_dt_match); + +static struct platform_driver max77693_muic_driver = { + .driver = { + .name = DEV_NAME, + .of_match_table = of_max77693_muic_dt_match, + }, + .probe = max77693_muic_probe, +}; + +module_platform_driver(max77693_muic_driver); + +MODULE_DESCRIPTION("Maxim MAX77693 Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:max77693-muic"); diff --git a/drivers/extcon/extcon-max77843.c b/drivers/extcon/extcon-max77843.c new file mode 100644 index 000000000..2ae9f7f1a --- /dev/null +++ b/drivers/extcon/extcon-max77843.c @@ -0,0 +1,971 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// extcon-max77843.c - Maxim MAX77843 extcon driver to support +// MUIC(Micro USB Interface Controller) +// +// Copyright (C) 2015 Samsung Electronics +// Author: Jaewon Kim <jaewon02.kim@samsung.com> + +#include <linux/extcon-provider.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/mfd/max77693-common.h> +#include <linux/mfd/max77843-private.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/workqueue.h> + +#define DELAY_MS_DEFAULT 15000 /* unit: millisecond */ + +enum max77843_muic_status { + MAX77843_MUIC_STATUS1 = 0, + MAX77843_MUIC_STATUS2, + MAX77843_MUIC_STATUS3, + + MAX77843_MUIC_STATUS_NUM, +}; + +struct max77843_muic_info { + struct device *dev; + struct max77693_dev *max77843; + struct extcon_dev *edev; + + struct mutex mutex; + struct work_struct irq_work; + struct delayed_work wq_detcable; + + u8 status[MAX77843_MUIC_STATUS_NUM]; + int prev_cable_type; + int prev_chg_type; + int prev_gnd_type; + + bool irq_adc; + bool irq_chg; +}; + +enum max77843_muic_cable_group { + MAX77843_CABLE_GROUP_ADC = 0, + MAX77843_CABLE_GROUP_ADC_GND, + MAX77843_CABLE_GROUP_CHG, +}; + +enum max77843_muic_adc_debounce_time { + MAX77843_DEBOUNCE_TIME_5MS = 0, + MAX77843_DEBOUNCE_TIME_10MS, + MAX77843_DEBOUNCE_TIME_25MS, + MAX77843_DEBOUNCE_TIME_38_62MS, +}; + +/* Define accessory cable type */ +enum max77843_muic_accessory_type { + MAX77843_MUIC_ADC_GROUND = 0, + MAX77843_MUIC_ADC_SEND_END_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S1_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S2_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S3_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S4_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S5_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S6_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S7_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S8_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S9_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S10_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S11_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S12_BUTTON, + MAX77843_MUIC_ADC_RESERVED_ACC_1, + MAX77843_MUIC_ADC_RESERVED_ACC_2, + MAX77843_MUIC_ADC_RESERVED_ACC_3, /* SmartDock */ + MAX77843_MUIC_ADC_RESERVED_ACC_4, + MAX77843_MUIC_ADC_RESERVED_ACC_5, + MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE2, + MAX77843_MUIC_ADC_PHONE_POWERED_DEV, + MAX77843_MUIC_ADC_TTY_CONVERTER, + MAX77843_MUIC_ADC_UART_CABLE, + MAX77843_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX77843_MUIC_ADC_FACTORY_MODE_USB_OFF, + MAX77843_MUIC_ADC_FACTORY_MODE_USB_ON, + MAX77843_MUIC_ADC_AV_CABLE_NOLOAD, + MAX77843_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF, + MAX77843_MUIC_ADC_FACTORY_MODE_UART_ON, + MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE1, + MAX77843_MUIC_ADC_OPEN, + + /* + * The below accessories should check + * not only ADC value but also ADC1K and VBVolt value. + */ + /* Offset|ADC1K|VBVolt| */ + MAX77843_MUIC_GND_USB_HOST = 0x100, /* 0x1| 0| 0| */ + MAX77843_MUIC_GND_USB_HOST_VB = 0x101, /* 0x1| 0| 1| */ + MAX77843_MUIC_GND_MHL = 0x102, /* 0x1| 1| 0| */ + MAX77843_MUIC_GND_MHL_VB = 0x103, /* 0x1| 1| 1| */ +}; + +/* Define charger cable type */ +enum max77843_muic_charger_type { + MAX77843_MUIC_CHG_NONE = 0, + MAX77843_MUIC_CHG_USB, + MAX77843_MUIC_CHG_DOWNSTREAM, + MAX77843_MUIC_CHG_DEDICATED, + MAX77843_MUIC_CHG_SPECIAL_500MA, + MAX77843_MUIC_CHG_SPECIAL_1A, + MAX77843_MUIC_CHG_SPECIAL_BIAS, + MAX77843_MUIC_CHG_RESERVED, + MAX77843_MUIC_CHG_GND, + MAX77843_MUIC_CHG_DOCK, +}; + +static const unsigned int max77843_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_CDP, + EXTCON_CHG_USB_FAST, + EXTCON_CHG_USB_SLOW, + EXTCON_DISP_MHL, + EXTCON_DOCK, + EXTCON_JIG, + EXTCON_NONE, +}; + +struct max77843_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max77843_muic_irq max77843_muic_irqs[] = { + { MAX77843_MUIC_IRQ_INT1_ADC, "MUIC-ADC" }, + { MAX77843_MUIC_IRQ_INT1_ADCERROR, "MUIC-ADC_ERROR" }, + { MAX77843_MUIC_IRQ_INT1_ADC1K, "MUIC-ADC1K" }, + { MAX77843_MUIC_IRQ_INT2_CHGTYP, "MUIC-CHGTYP" }, + { MAX77843_MUIC_IRQ_INT2_CHGDETRUN, "MUIC-CHGDETRUN" }, + { MAX77843_MUIC_IRQ_INT2_DCDTMR, "MUIC-DCDTMR" }, + { MAX77843_MUIC_IRQ_INT2_DXOVP, "MUIC-DXOVP" }, + { MAX77843_MUIC_IRQ_INT2_VBVOLT, "MUIC-VBVOLT" }, + { MAX77843_MUIC_IRQ_INT3_VBADC, "MUIC-VBADC" }, + { MAX77843_MUIC_IRQ_INT3_VDNMON, "MUIC-VDNMON" }, + { MAX77843_MUIC_IRQ_INT3_DNRES, "MUIC-DNRES" }, + { MAX77843_MUIC_IRQ_INT3_MPNACK, "MUIC-MPNACK"}, + { MAX77843_MUIC_IRQ_INT3_MRXBUFOW, "MUIC-MRXBUFOW"}, + { MAX77843_MUIC_IRQ_INT3_MRXTRF, "MUIC-MRXTRF"}, + { MAX77843_MUIC_IRQ_INT3_MRXPERR, "MUIC-MRXPERR"}, + { MAX77843_MUIC_IRQ_INT3_MRXRDY, "MUIC-MRXRDY"}, +}; + +static const struct regmap_config max77843_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = MAX77843_MUIC_REG_END, +}; + +static const struct regmap_irq max77843_muic_irq[] = { + /* INT1 interrupt */ + { .reg_offset = 0, .mask = MAX77843_MUIC_ADC, }, + { .reg_offset = 0, .mask = MAX77843_MUIC_ADCERROR, }, + { .reg_offset = 0, .mask = MAX77843_MUIC_ADC1K, }, + + /* INT2 interrupt */ + { .reg_offset = 1, .mask = MAX77843_MUIC_CHGTYP, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_CHGDETRUN, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_DCDTMR, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_DXOVP, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_VBVOLT, }, + + /* INT3 interrupt */ + { .reg_offset = 2, .mask = MAX77843_MUIC_VBADC, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_VDNMON, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_DNRES, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MPNACK, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXBUFOW, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXTRF, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXPERR, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXRDY, }, +}; + +static const struct regmap_irq_chip max77843_muic_irq_chip = { + .name = "max77843-muic", + .status_base = MAX77843_MUIC_REG_INT1, + .unmask_base = MAX77843_MUIC_REG_INTMASK1, + .num_regs = 3, + .irqs = max77843_muic_irq, + .num_irqs = ARRAY_SIZE(max77843_muic_irq), +}; + +static int max77843_muic_set_path(struct max77843_muic_info *info, + u8 val, bool attached, bool nobccomp) +{ + struct max77693_dev *max77843 = info->max77843; + int ret = 0; + unsigned int ctrl1, ctrl2; + + if (attached) + ctrl1 = val; + else + ctrl1 = MAX77843_MUIC_CONTROL1_SW_OPEN; + if (nobccomp) { + /* Disable BC1.2 protocol and force manual switch control */ + ctrl1 |= MAX77843_MUIC_CONTROL1_NOBCCOMP_MASK; + } + + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL1, + MAX77843_MUIC_CONTROL1_COM_SW | + MAX77843_MUIC_CONTROL1_NOBCCOMP_MASK, + ctrl1); + if (ret < 0) { + dev_err(info->dev, "Cannot switch MUIC port\n"); + return ret; + } + + if (attached) + ctrl2 = MAX77843_MUIC_CONTROL2_CPEN_MASK; + else + ctrl2 = MAX77843_MUIC_CONTROL2_LOWPWR_MASK; + + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL2, + MAX77843_MUIC_CONTROL2_LOWPWR_MASK | + MAX77843_MUIC_CONTROL2_CPEN_MASK, ctrl2); + if (ret < 0) { + dev_err(info->dev, "Cannot update lowpower mode\n"); + return ret; + } + + dev_dbg(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +static void max77843_charger_set_otg_vbus(struct max77843_muic_info *info, + bool on) +{ + struct max77693_dev *max77843 = info->max77843; + unsigned int cnfg00; + + if (on) + cnfg00 = MAX77843_CHG_OTG_MASK | MAX77843_CHG_BOOST_MASK; + else + cnfg00 = MAX77843_CHG_ENABLE | MAX77843_CHG_BUCK_MASK; + + regmap_update_bits(max77843->regmap_chg, MAX77843_CHG_REG_CHG_CNFG_00, + MAX77843_CHG_MODE_MASK, cnfg00); +} + +static int max77843_muic_get_cable_type(struct max77843_muic_info *info, + enum max77843_muic_cable_group group, bool *attached) +{ + int adc, chg_type, cable_type, gnd_type; + + adc = info->status[MAX77843_MUIC_STATUS1] & + MAX77843_MUIC_STATUS1_ADC_MASK; + adc >>= MAX77843_MUIC_STATUS1_ADC_SHIFT; + + switch (group) { + case MAX77843_CABLE_GROUP_ADC: + if (adc == MAX77843_MUIC_ADC_OPEN) { + *attached = false; + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX77843_MUIC_ADC_OPEN; + } else { + *attached = true; + cable_type = info->prev_cable_type = adc; + } + break; + case MAX77843_CABLE_GROUP_CHG: + chg_type = info->status[MAX77843_MUIC_STATUS2] & + MAX77843_MUIC_STATUS2_CHGTYP_MASK; + + /* Check GROUND accessory with charger cable */ + if (adc == MAX77843_MUIC_ADC_GROUND) { + if (chg_type == MAX77843_MUIC_CHG_NONE) { + /* + * The following state when charger cable is + * disconnected but the GROUND accessory still + * connected. + */ + *attached = false; + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77843_MUIC_CHG_NONE; + } else { + + /* + * The following state when charger cable is + * connected on the GROUND accessory. + */ + *attached = true; + cable_type = MAX77843_MUIC_CHG_GND; + info->prev_chg_type = MAX77843_MUIC_CHG_GND; + } + break; + } + + if (adc == MAX77843_MUIC_ADC_RESERVED_ACC_3) { /* SmartDock */ + if (chg_type == MAX77843_MUIC_CHG_NONE) { + *attached = false; + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77843_MUIC_CHG_NONE; + } else { + *attached = true; + cable_type = MAX77843_MUIC_CHG_DOCK; + info->prev_chg_type = MAX77843_MUIC_CHG_DOCK; + } + break; + } + + if (chg_type == MAX77843_MUIC_CHG_NONE) { + *attached = false; + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77843_MUIC_CHG_NONE; + } else { + *attached = true; + cable_type = info->prev_chg_type = chg_type; + } + break; + case MAX77843_CABLE_GROUP_ADC_GND: + if (adc == MAX77843_MUIC_ADC_OPEN) { + *attached = false; + cable_type = info->prev_gnd_type; + info->prev_gnd_type = MAX77843_MUIC_ADC_OPEN; + } else { + *attached = true; + + /* + * Offset|ADC1K|VBVolt| + * 0x1| 0| 0| USB-HOST + * 0x1| 0| 1| USB-HOST with VB + * 0x1| 1| 0| MHL + * 0x1| 1| 1| MHL with VB + */ + /* Get ADC1K register bit */ + gnd_type = (info->status[MAX77843_MUIC_STATUS1] & + MAX77843_MUIC_STATUS1_ADC1K_MASK); + + /* Get VBVolt register bit */ + gnd_type |= (info->status[MAX77843_MUIC_STATUS2] & + MAX77843_MUIC_STATUS2_VBVOLT_MASK); + gnd_type >>= MAX77843_MUIC_STATUS2_VBVOLT_SHIFT; + + /* Offset of GND cable */ + gnd_type |= MAX77843_MUIC_GND_USB_HOST; + cable_type = info->prev_gnd_type = gnd_type; + } + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max77843_muic_adc_gnd_handler(struct max77843_muic_info *info) +{ + int ret, gnd_cable_type; + bool attached; + + gnd_cable_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC_GND, &attached); + dev_dbg(info->dev, "external connector is %s (gnd:0x%02x)\n", + attached ? "attached" : "detached", gnd_cable_type); + + switch (gnd_cable_type) { + case MAX77843_MUIC_GND_USB_HOST: + case MAX77843_MUIC_GND_USB_HOST_VB: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_USB, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, attached); + max77843_charger_set_otg_vbus(info, attached); + break; + case MAX77843_MUIC_GND_MHL_VB: + case MAX77843_MUIC_GND_MHL: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_OPEN, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, attached); + break; + default: + dev_err(info->dev, "failed to detect %s accessory(gnd:0x%x)\n", + attached ? "attached" : "detached", gnd_cable_type); + return -EINVAL; + } + + return 0; +} + +static int max77843_muic_jig_handler(struct max77843_muic_info *info, + int cable_type, bool attached) +{ + int ret; + u8 path = MAX77843_MUIC_CONTROL1_SW_OPEN; + + dev_dbg(info->dev, "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_ON: + path = MAX77843_MUIC_CONTROL1_SW_USB; + break; + case MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF: + path = MAX77843_MUIC_CONTROL1_SW_UART; + break; + default: + return -EINVAL; + } + + ret = max77843_muic_set_path(info, path, attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_JIG, attached); + + return 0; +} + +static int max77843_muic_dock_handler(struct max77843_muic_info *info, + bool attached) +{ + int ret; + + dev_dbg(info->dev, "external connector is %s (adc: 0x10)\n", + attached ? "attached" : "detached"); + + ret = max77843_muic_set_path(info, MAX77843_MUIC_CONTROL1_SW_USB, + attached, attached); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, attached); + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, attached); + extcon_set_state_sync(info->edev, EXTCON_DOCK, attached); + + return 0; +} + +static int max77843_muic_adc_handler(struct max77843_muic_info *info) +{ + int ret, cable_type; + bool attached; + + cable_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC, &attached); + + dev_dbg(info->dev, + "external connector is %s (adc:0x%02x, prev_adc:0x%x)\n", + attached ? "attached" : "detached", cable_type, + info->prev_cable_type); + + switch (cable_type) { + case MAX77843_MUIC_ADC_RESERVED_ACC_3: /* SmartDock */ + ret = max77843_muic_dock_handler(info, attached); + if (ret < 0) + return ret; + break; + case MAX77843_MUIC_ADC_GROUND: + ret = max77843_muic_adc_gnd_handler(info); + if (ret < 0) + return ret; + break; + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_ON: + case MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF: + ret = max77843_muic_jig_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX77843_MUIC_ADC_SEND_END_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S3_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S7_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S9_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S10_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S12_BUTTON: + case MAX77843_MUIC_ADC_RESERVED_ACC_1: + case MAX77843_MUIC_ADC_RESERVED_ACC_2: + case MAX77843_MUIC_ADC_RESERVED_ACC_4: + case MAX77843_MUIC_ADC_RESERVED_ACC_5: + case MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE2: + case MAX77843_MUIC_ADC_PHONE_POWERED_DEV: + case MAX77843_MUIC_ADC_TTY_CONVERTER: + case MAX77843_MUIC_ADC_UART_CABLE: + case MAX77843_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX77843_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX77843_MUIC_ADC_CEA936A_TYPE2_CHG: + case MAX77843_MUIC_ADC_FACTORY_MODE_UART_ON: + case MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE1: + case MAX77843_MUIC_ADC_OPEN: + dev_err(info->dev, + "accessory is %s but it isn't used (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s accessory (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max77843_muic_chg_handler(struct max77843_muic_info *info) +{ + int ret, chg_type, gnd_type; + bool attached; + + chg_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_CHG, &attached); + + dev_dbg(info->dev, + "external connector is %s(chg_type:0x%x, prev_chg_type:0x%x)\n", + attached ? "attached" : "detached", + chg_type, info->prev_chg_type); + + switch (chg_type) { + case MAX77843_MUIC_CHG_USB: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_USB, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_USB, attached); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + break; + case MAX77843_MUIC_CHG_DOWNSTREAM: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_OPEN, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_CDP, + attached); + break; + case MAX77843_MUIC_CHG_DEDICATED: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_OPEN, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + attached); + break; + case MAX77843_MUIC_CHG_SPECIAL_500MA: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_OPEN, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SLOW, + attached); + break; + case MAX77843_MUIC_CHG_SPECIAL_1A: + ret = max77843_muic_set_path(info, + MAX77843_MUIC_CONTROL1_SW_OPEN, + attached, false); + if (ret < 0) + return ret; + + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_FAST, + attached); + break; + case MAX77843_MUIC_CHG_GND: + gnd_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC_GND, &attached); + + /* Charger cable on MHL accessory is attach or detach */ + if (gnd_type == MAX77843_MUIC_GND_MHL_VB) + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + true); + else if (gnd_type == MAX77843_MUIC_GND_MHL) + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + false); + break; + case MAX77843_MUIC_CHG_DOCK: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, attached); + break; + case MAX77843_MUIC_CHG_NONE: + break; + default: + dev_err(info->dev, + "failed to detect %s accessory (chg_type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + + max77843_muic_set_path(info, MAX77843_MUIC_CONTROL1_SW_OPEN, + attached, false); + return -EINVAL; + } + + return 0; +} + +static void max77843_muic_irq_work(struct work_struct *work) +{ + struct max77843_muic_info *info = container_of(work, + struct max77843_muic_info, irq_work); + struct max77693_dev *max77843 = info->max77843; + int ret = 0; + + mutex_lock(&info->mutex); + + ret = regmap_bulk_read(max77843->regmap_muic, + MAX77843_MUIC_REG_STATUS1, info->status, + MAX77843_MUIC_STATUS_NUM); + if (ret) { + dev_err(info->dev, "Cannot read STATUS registers\n"); + mutex_unlock(&info->mutex); + return; + } + + if (info->irq_adc) { + ret = max77843_muic_adc_handler(info); + if (ret) + dev_err(info->dev, "Unknown cable type\n"); + info->irq_adc = false; + } + + if (info->irq_chg) { + ret = max77843_muic_chg_handler(info); + if (ret) + dev_err(info->dev, "Unknown charger type\n"); + info->irq_chg = false; + } + + mutex_unlock(&info->mutex); +} + +static irqreturn_t max77843_muic_irq_handler(int irq, void *data) +{ + struct max77843_muic_info *info = data; + int i, irq_type = -1; + + for (i = 0; i < ARRAY_SIZE(max77843_muic_irqs); i++) + if (irq == max77843_muic_irqs[i].virq) + irq_type = max77843_muic_irqs[i].irq; + + switch (irq_type) { + case MAX77843_MUIC_IRQ_INT1_ADC: + case MAX77843_MUIC_IRQ_INT1_ADCERROR: + case MAX77843_MUIC_IRQ_INT1_ADC1K: + info->irq_adc = true; + break; + case MAX77843_MUIC_IRQ_INT2_CHGTYP: + case MAX77843_MUIC_IRQ_INT2_CHGDETRUN: + case MAX77843_MUIC_IRQ_INT2_DCDTMR: + case MAX77843_MUIC_IRQ_INT2_DXOVP: + case MAX77843_MUIC_IRQ_INT2_VBVOLT: + info->irq_chg = true; + break; + case MAX77843_MUIC_IRQ_INT3_VBADC: + case MAX77843_MUIC_IRQ_INT3_VDNMON: + case MAX77843_MUIC_IRQ_INT3_DNRES: + case MAX77843_MUIC_IRQ_INT3_MPNACK: + case MAX77843_MUIC_IRQ_INT3_MRXBUFOW: + case MAX77843_MUIC_IRQ_INT3_MRXTRF: + case MAX77843_MUIC_IRQ_INT3_MRXPERR: + case MAX77843_MUIC_IRQ_INT3_MRXRDY: + break; + default: + dev_err(info->dev, "Cannot recognize IRQ(%d)\n", irq_type); + break; + } + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static void max77843_muic_detect_cable_wq(struct work_struct *work) +{ + struct max77843_muic_info *info = container_of(to_delayed_work(work), + struct max77843_muic_info, wq_detcable); + struct max77693_dev *max77843 = info->max77843; + int chg_type, adc, ret; + bool attached; + + mutex_lock(&info->mutex); + + ret = regmap_bulk_read(max77843->regmap_muic, + MAX77843_MUIC_REG_STATUS1, info->status, + MAX77843_MUIC_STATUS_NUM); + if (ret) { + dev_err(info->dev, "Cannot read STATUS registers\n"); + goto err_cable_wq; + } + + adc = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC, &attached); + if (attached && adc != MAX77843_MUIC_ADC_OPEN) { + ret = max77843_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect accessory\n"); + goto err_cable_wq; + } + } + + chg_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_CHG, &attached); + if (attached && chg_type != MAX77843_MUIC_CHG_NONE) { + ret = max77843_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger accessory\n"); + goto err_cable_wq; + } + } + +err_cable_wq: + mutex_unlock(&info->mutex); +} + +static int max77843_muic_set_debounce_time(struct max77843_muic_info *info, + enum max77843_muic_adc_debounce_time time) +{ + struct max77693_dev *max77843 = info->max77843; + int ret; + + switch (time) { + case MAX77843_DEBOUNCE_TIME_5MS: + case MAX77843_DEBOUNCE_TIME_10MS: + case MAX77843_DEBOUNCE_TIME_25MS: + case MAX77843_DEBOUNCE_TIME_38_62MS: + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL4, + MAX77843_MUIC_CONTROL4_ADCDBSET_MASK, + time << MAX77843_MUIC_CONTROL4_ADCDBSET_SHIFT); + if (ret < 0) { + dev_err(info->dev, "Cannot write MUIC regmap\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +} + +static int max77843_init_muic_regmap(struct max77693_dev *max77843) +{ + int ret; + + max77843->i2c_muic = i2c_new_dummy_device(max77843->i2c->adapter, + I2C_ADDR_MUIC); + if (IS_ERR(max77843->i2c_muic)) { + dev_err(&max77843->i2c->dev, + "Cannot allocate I2C device for MUIC\n"); + return PTR_ERR(max77843->i2c_muic); + } + + i2c_set_clientdata(max77843->i2c_muic, max77843); + + max77843->regmap_muic = devm_regmap_init_i2c(max77843->i2c_muic, + &max77843_muic_regmap_config); + if (IS_ERR(max77843->regmap_muic)) { + ret = PTR_ERR(max77843->regmap_muic); + goto err_muic_i2c; + } + + ret = regmap_add_irq_chip(max77843->regmap_muic, max77843->irq, + IRQF_TRIGGER_LOW | IRQF_ONESHOT | IRQF_SHARED, + 0, &max77843_muic_irq_chip, &max77843->irq_data_muic); + if (ret < 0) { + dev_err(&max77843->i2c->dev, "Cannot add MUIC IRQ chip\n"); + goto err_muic_i2c; + } + + return 0; + +err_muic_i2c: + i2c_unregister_device(max77843->i2c_muic); + + return ret; +} + +static int max77843_muic_probe(struct platform_device *pdev) +{ + struct max77693_dev *max77843 = dev_get_drvdata(pdev->dev.parent); + struct max77843_muic_info *info; + unsigned int id; + int cable_type; + bool attached; + int i, ret; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->max77843 = max77843; + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + /* Initialize i2c and regmap */ + ret = max77843_init_muic_regmap(max77843); + if (ret) { + dev_err(&pdev->dev, "Failed to init MUIC regmap\n"); + return ret; + } + + /* Turn off auto detection configuration */ + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL4, + MAX77843_MUIC_CONTROL4_USBAUTO_MASK | + MAX77843_MUIC_CONTROL4_FCTAUTO_MASK, + CONTROL4_AUTO_DISABLE); + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + max77843_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "Failed to allocate memory for extcon\n"); + ret = PTR_ERR(info->edev); + goto err_muic_irq; + } + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "Failed to register extcon device\n"); + goto err_muic_irq; + } + + /* Set ADC debounce time */ + max77843_muic_set_debounce_time(info, MAX77843_DEBOUNCE_TIME_25MS); + + /* Set initial path for UART when JIG is connected to get serial logs */ + ret = regmap_bulk_read(max77843->regmap_muic, + MAX77843_MUIC_REG_STATUS1, info->status, + MAX77843_MUIC_STATUS_NUM); + if (ret) { + dev_err(info->dev, "Cannot read STATUS registers\n"); + goto err_muic_irq; + } + cable_type = max77843_muic_get_cable_type(info, MAX77843_CABLE_GROUP_ADC, + &attached); + if (attached && cable_type == MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF) + max77843_muic_set_path(info, MAX77843_MUIC_CONTROL1_SW_UART, + true, false); + + /* Check revision number of MUIC device */ + ret = regmap_read(max77843->regmap_muic, MAX77843_MUIC_REG_ID, &id); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to read revision number\n"); + goto err_muic_irq; + } + dev_info(info->dev, "MUIC device ID : 0x%x\n", id); + + /* Support virtual irq domain for max77843 MUIC device */ + INIT_WORK(&info->irq_work, max77843_muic_irq_work); + + /* Clear IRQ bits before request IRQs */ + ret = regmap_bulk_read(max77843->regmap_muic, + MAX77843_MUIC_REG_INT1, info->status, + MAX77843_MUIC_STATUS_NUM); + if (ret) { + dev_err(&pdev->dev, "Failed to Clear IRQ bits\n"); + goto err_muic_irq; + } + + for (i = 0; i < ARRAY_SIZE(max77843_muic_irqs); i++) { + struct max77843_muic_irq *muic_irq = &max77843_muic_irqs[i]; + int virq = 0; + + virq = regmap_irq_get_virq(max77843->irq_data_muic, + muic_irq->irq); + if (virq <= 0) { + ret = -EINVAL; + goto err_muic_irq; + } + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max77843_muic_irq_handler, IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "Failed to request irq (IRQ: %d, error: %d)\n", + muic_irq->irq, ret); + goto err_muic_irq; + } + } + + /* Detect accessory after completing the initialization of platform */ + INIT_DELAYED_WORK(&info->wq_detcable, max77843_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, + &info->wq_detcable, msecs_to_jiffies(DELAY_MS_DEFAULT)); + + return 0; + +err_muic_irq: + regmap_del_irq_chip(max77843->irq, max77843->irq_data_muic); + i2c_unregister_device(max77843->i2c_muic); + + return ret; +} + +static void max77843_muic_remove(struct platform_device *pdev) +{ + struct max77843_muic_info *info = platform_get_drvdata(pdev); + struct max77693_dev *max77843 = info->max77843; + + cancel_work_sync(&info->irq_work); + regmap_del_irq_chip(max77843->irq, max77843->irq_data_muic); + i2c_unregister_device(max77843->i2c_muic); +} + +static const struct platform_device_id max77843_muic_id[] = { + { "max77843-muic", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(platform, max77843_muic_id); + +static const struct of_device_id of_max77843_muic_dt_match[] = { + { .compatible = "maxim,max77843-muic", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, of_max77843_muic_dt_match); + +static struct platform_driver max77843_muic_driver = { + .driver = { + .name = "max77843-muic", + .of_match_table = of_max77843_muic_dt_match, + }, + .probe = max77843_muic_probe, + .remove = max77843_muic_remove, + .id_table = max77843_muic_id, +}; + +static int __init max77843_muic_init(void) +{ + return platform_driver_register(&max77843_muic_driver); +} +subsys_initcall(max77843_muic_init); + +MODULE_DESCRIPTION("Maxim MAX77843 Extcon driver"); +MODULE_AUTHOR("Jaewon Kim <jaewon02.kim@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-max8997.c b/drivers/extcon/extcon-max8997.c new file mode 100644 index 000000000..9cddf08e0 --- /dev/null +++ b/drivers/extcon/extcon-max8997.c @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// extcon-max8997.c - MAX8997 extcon driver to support MAX8997 MUIC +// +// Copyright (C) 2012 Samsung Electronics +// Donggeun Kim <dg77.kim@samsung.com> + +#include <linux/devm-helpers.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/kobject.h> +#include <linux/mfd/max8997.h> +#include <linux/mfd/max8997-private.h> +#include <linux/extcon-provider.h> +#include <linux/irqdomain.h> + +#define DEV_NAME "max8997-muic" +#define DELAY_MS_DEFAULT 20000 /* unit: millisecond */ + +enum max8997_muic_adc_debounce_time { + ADC_DEBOUNCE_TIME_0_5MS = 0, /* 0.5ms */ + ADC_DEBOUNCE_TIME_10MS, /* 10ms */ + ADC_DEBOUNCE_TIME_25MS, /* 25ms */ + ADC_DEBOUNCE_TIME_38_62MS, /* 38.62ms */ +}; + +struct max8997_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max8997_muic_irq muic_irqs[] = { + { MAX8997_MUICIRQ_ADCError, "muic-ADCERROR" }, + { MAX8997_MUICIRQ_ADCLow, "muic-ADCLOW" }, + { MAX8997_MUICIRQ_ADC, "muic-ADC" }, + { MAX8997_MUICIRQ_VBVolt, "muic-VBVOLT" }, + { MAX8997_MUICIRQ_DBChg, "muic-DBCHG" }, + { MAX8997_MUICIRQ_DCDTmr, "muic-DCDTMR" }, + { MAX8997_MUICIRQ_ChgDetRun, "muic-CHGDETRUN" }, + { MAX8997_MUICIRQ_ChgTyp, "muic-CHGTYP" }, + { MAX8997_MUICIRQ_OVP, "muic-OVP" }, + { MAX8997_PMICIRQ_CHGINS, "pmic-CHGINS" }, + { MAX8997_PMICIRQ_CHGRM, "pmic-CHGRM" }, +}; + +/* Define supported cable type */ +enum max8997_muic_acc_type { + MAX8997_MUIC_ADC_GROUND = 0x0, + MAX8997_MUIC_ADC_MHL, /* MHL*/ + MAX8997_MUIC_ADC_REMOTE_S1_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S2_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S3_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S4_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S5_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S6_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S7_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S8_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S9_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S10_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S11_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S12_BUTTON, + MAX8997_MUIC_ADC_RESERVED_ACC_1, + MAX8997_MUIC_ADC_RESERVED_ACC_2, + MAX8997_MUIC_ADC_RESERVED_ACC_3, + MAX8997_MUIC_ADC_RESERVED_ACC_4, + MAX8997_MUIC_ADC_RESERVED_ACC_5, + MAX8997_MUIC_ADC_CEA936_AUDIO, + MAX8997_MUIC_ADC_PHONE_POWERED_DEV, + MAX8997_MUIC_ADC_TTY_CONVERTER, + MAX8997_MUIC_ADC_UART_CABLE, + MAX8997_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX8997_MUIC_ADC_FACTORY_MODE_USB_OFF, /* JIG-USB-OFF */ + MAX8997_MUIC_ADC_FACTORY_MODE_USB_ON, /* JIG-USB-ON */ + MAX8997_MUIC_ADC_AV_CABLE_NOLOAD, /* DESKDOCK */ + MAX8997_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX8997_MUIC_ADC_FACTORY_MODE_UART_OFF, /* JIG-UART */ + MAX8997_MUIC_ADC_FACTORY_MODE_UART_ON, /* CARDOCK */ + MAX8997_MUIC_ADC_AUDIO_MODE_REMOTE, + MAX8997_MUIC_ADC_OPEN, /* OPEN */ +}; + +enum max8997_muic_cable_group { + MAX8997_CABLE_GROUP_ADC = 0, + MAX8997_CABLE_GROUP_ADC_GND, + MAX8997_CABLE_GROUP_CHG, + MAX8997_CABLE_GROUP_VBVOLT, +}; + +enum max8997_muic_usb_type { + MAX8997_USB_HOST, + MAX8997_USB_DEVICE, +}; + +enum max8997_muic_charger_type { + MAX8997_CHARGER_TYPE_NONE = 0, + MAX8997_CHARGER_TYPE_USB, + MAX8997_CHARGER_TYPE_DOWNSTREAM_PORT, + MAX8997_CHARGER_TYPE_DEDICATED_CHG, + MAX8997_CHARGER_TYPE_500MA, + MAX8997_CHARGER_TYPE_1A, + MAX8997_CHARGER_TYPE_DEAD_BATTERY = 7, +}; + +struct max8997_muic_info { + struct device *dev; + struct i2c_client *muic; + struct extcon_dev *edev; + int prev_cable_type; + int prev_chg_type; + u8 status[2]; + + int irq; + struct work_struct irq_work; + struct mutex mutex; + + struct max8997_muic_platform_data *muic_pdata; + enum max8997_muic_charger_type pre_charger_type; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + int path_usb; + int path_uart; +}; + +static const unsigned int max8997_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_FAST, + EXTCON_CHG_USB_SLOW, + EXTCON_CHG_USB_CDP, + EXTCON_DISP_MHL, + EXTCON_DOCK, + EXTCON_JIG, + EXTCON_NONE, +}; + +/* + * max8997_muic_set_debounce_time - Set the debounce time of ADC + * @info: the instance including private data of max8997 MUIC + * @time: the debounce time of ADC + */ +static int max8997_muic_set_debounce_time(struct max8997_muic_info *info, + enum max8997_muic_adc_debounce_time time) +{ + int ret; + + switch (time) { + case ADC_DEBOUNCE_TIME_0_5MS: + case ADC_DEBOUNCE_TIME_10MS: + case ADC_DEBOUNCE_TIME_25MS: + case ADC_DEBOUNCE_TIME_38_62MS: + ret = max8997_update_reg(info->muic, + MAX8997_MUIC_REG_CONTROL3, + time << CONTROL3_ADCDBSET_SHIFT, + CONTROL3_ADCDBSET_MASK); + if (ret) { + dev_err(info->dev, "failed to set ADC debounce time\n"); + return ret; + } + break; + default: + dev_err(info->dev, "invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +}; + +/* + * max8997_muic_set_path - Set hardware line according to attached cable + * @info: the instance including private data of max8997 MUIC + * @value: the path according to attached cable + * @attached: the state of cable (true:attached, false:detached) + * + * The max8997 MUIC device share outside H/W line among a varity of cables, + * so this function set internal path of H/W line according to the type of + * attached cable. + */ +static int max8997_muic_set_path(struct max8997_muic_info *info, + u8 val, bool attached) +{ + int ret; + u8 ctrl1, ctrl2 = 0; + + if (attached) + ctrl1 = val; + else + ctrl1 = CONTROL1_SW_OPEN; + + ret = max8997_update_reg(info->muic, + MAX8997_MUIC_REG_CONTROL1, ctrl1, COMP_SW_MASK); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl2 |= CONTROL2_CPEN_MASK; /* LowPwr=0, CPEn=1 */ + else + ctrl2 |= CONTROL2_LOWPWR_MASK; /* LowPwr=1, CPEn=0 */ + + ret = max8997_update_reg(info->muic, + MAX8997_MUIC_REG_CONTROL2, ctrl2, + CONTROL2_LOWPWR_MASK | CONTROL2_CPEN_MASK); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + dev_info(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +/* + * max8997_muic_get_cable_type - Return cable type and check cable state + * @info: the instance including private data of max8997 MUIC + * @group: the path according to attached cable + * @attached: store cable state and return + * + * This function check the cable state either attached or detached, + * and then divide precise type of cable according to cable group. + * - MAX8997_CABLE_GROUP_ADC + * - MAX8997_CABLE_GROUP_CHG + */ +static int max8997_muic_get_cable_type(struct max8997_muic_info *info, + enum max8997_muic_cable_group group, bool *attached) +{ + int cable_type = 0; + int adc; + int chg_type; + + switch (group) { + case MAX8997_CABLE_GROUP_ADC: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type) for handling cable when cable is + * detached. + */ + if (adc == MAX8997_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX8997_MUIC_ADC_OPEN; + } else { + *attached = true; + + cable_type = info->prev_cable_type = adc; + } + break; + case MAX8997_CABLE_GROUP_CHG: + /* + * Read charger type to check cable type and decide cable state + * according to type of charger cable. + */ + chg_type = info->status[1] & STATUS2_CHGTYP_MASK; + chg_type >>= STATUS2_CHGTYP_SHIFT; + + if (chg_type == MAX8997_CHARGER_TYPE_NONE) { + *attached = false; + + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX8997_CHARGER_TYPE_NONE; + } else { + *attached = true; + + /* + * Check current cable state/cable type and store cable + * type(info->prev_chg_type) for handling cable when + * charger cable is detached. + */ + cable_type = info->prev_chg_type = chg_type; + } + + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max8997_muic_handle_usb(struct max8997_muic_info *info, + enum max8997_muic_usb_type usb_type, bool attached) +{ + int ret = 0; + + ret = max8997_muic_set_path(info, info->path_usb, attached); + if (ret < 0) { + dev_err(info->dev, "failed to update muic register\n"); + return ret; + } + + switch (usb_type) { + case MAX8997_USB_HOST: + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, attached); + break; + case MAX8997_USB_DEVICE: + extcon_set_state_sync(info->edev, EXTCON_USB, attached); + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + break; + default: + dev_err(info->dev, "failed to detect %s usb cable\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + return 0; +} + +static int max8997_muic_handle_dock(struct max8997_muic_info *info, + int cable_type, bool attached) +{ + int ret = 0; + + ret = max8997_muic_set_path(info, CONTROL1_SW_AUDIO, attached); + if (ret) { + dev_err(info->dev, "failed to update muic register\n"); + return ret; + } + + switch (cable_type) { + case MAX8997_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX8997_MUIC_ADC_FACTORY_MODE_UART_ON: + extcon_set_state_sync(info->edev, EXTCON_DOCK, attached); + break; + default: + dev_err(info->dev, "failed to detect %s dock device\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + return 0; +} + +static int max8997_muic_handle_jig_uart(struct max8997_muic_info *info, + bool attached) +{ + int ret = 0; + + /* switch to UART */ + ret = max8997_muic_set_path(info, info->path_uart, attached); + if (ret) { + dev_err(info->dev, "failed to update muic register\n"); + return ret; + } + + extcon_set_state_sync(info->edev, EXTCON_JIG, attached); + + return 0; +} + +static int max8997_muic_adc_handler(struct max8997_muic_info *info) +{ + int cable_type; + bool attached; + int ret = 0; + + /* Check cable state which is either detached or attached */ + cable_type = max8997_muic_get_cable_type(info, + MAX8997_CABLE_GROUP_ADC, &attached); + + switch (cable_type) { + case MAX8997_MUIC_ADC_GROUND: + ret = max8997_muic_handle_usb(info, MAX8997_USB_HOST, attached); + if (ret < 0) + return ret; + break; + case MAX8997_MUIC_ADC_MHL: + extcon_set_state_sync(info->edev, EXTCON_DISP_MHL, attached); + break; + case MAX8997_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX8997_MUIC_ADC_FACTORY_MODE_USB_ON: + ret = max8997_muic_handle_usb(info, + MAX8997_USB_DEVICE, attached); + if (ret < 0) + return ret; + break; + case MAX8997_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX8997_MUIC_ADC_FACTORY_MODE_UART_ON: + ret = max8997_muic_handle_dock(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX8997_MUIC_ADC_FACTORY_MODE_UART_OFF: + ret = max8997_muic_handle_jig_uart(info, attached); + break; + case MAX8997_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S3_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S7_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S9_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S10_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S12_BUTTON: + case MAX8997_MUIC_ADC_RESERVED_ACC_1: + case MAX8997_MUIC_ADC_RESERVED_ACC_2: + case MAX8997_MUIC_ADC_RESERVED_ACC_3: + case MAX8997_MUIC_ADC_RESERVED_ACC_4: + case MAX8997_MUIC_ADC_RESERVED_ACC_5: + case MAX8997_MUIC_ADC_CEA936_AUDIO: + case MAX8997_MUIC_ADC_PHONE_POWERED_DEV: + case MAX8997_MUIC_ADC_TTY_CONVERTER: + case MAX8997_MUIC_ADC_UART_CABLE: + case MAX8997_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX8997_MUIC_ADC_CEA936A_TYPE2_CHG: + case MAX8997_MUIC_ADC_AUDIO_MODE_REMOTE: + /* + * This cable isn't used in general case if it is specially + * needed to detect additional cable, should implement + * proper operation when this cable is attached/detached. + */ + dev_info(info->dev, + "cable is %s but it isn't used (type:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s unknown cable (type:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max8997_muic_chg_handler(struct max8997_muic_info *info) +{ + int chg_type; + bool attached; + int adc; + + chg_type = max8997_muic_get_cable_type(info, + MAX8997_CABLE_GROUP_CHG, &attached); + + switch (chg_type) { + case MAX8997_CHARGER_TYPE_NONE: + break; + case MAX8997_CHARGER_TYPE_USB: + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + if ((adc & STATUS1_ADC_MASK) == MAX8997_MUIC_ADC_OPEN) { + max8997_muic_handle_usb(info, + MAX8997_USB_DEVICE, attached); + } + break; + case MAX8997_CHARGER_TYPE_DOWNSTREAM_PORT: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_CDP, + attached); + break; + case MAX8997_CHARGER_TYPE_DEDICATED_CHG: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_DCP, + attached); + break; + case MAX8997_CHARGER_TYPE_500MA: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SLOW, + attached); + break; + case MAX8997_CHARGER_TYPE_1A: + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_FAST, + attached); + break; + default: + dev_err(info->dev, + "failed to detect %s unknown chg cable (type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + return -EINVAL; + } + + return 0; +} + +static void max8997_muic_irq_work(struct work_struct *work) +{ + struct max8997_muic_info *info = container_of(work, + struct max8997_muic_info, irq_work); + int irq_type = 0; + int i, ret; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) + if (info->irq == muic_irqs[i].virq) + irq_type = muic_irqs[i].irq; + + ret = max8997_bulk_read(info->muic, MAX8997_MUIC_REG_STATUS1, + 2, info->status); + if (ret) { + dev_err(info->dev, "failed to read muic register\n"); + mutex_unlock(&info->mutex); + return; + } + + switch (irq_type) { + case MAX8997_MUICIRQ_ADCError: + case MAX8997_MUICIRQ_ADCLow: + case MAX8997_MUICIRQ_ADC: + /* Handle all of cable except for charger cable */ + ret = max8997_muic_adc_handler(info); + break; + case MAX8997_MUICIRQ_VBVolt: + case MAX8997_MUICIRQ_DBChg: + case MAX8997_MUICIRQ_DCDTmr: + case MAX8997_MUICIRQ_ChgDetRun: + case MAX8997_MUICIRQ_ChgTyp: + case MAX8997_PMICIRQ_CHGINS: + case MAX8997_PMICIRQ_CHGRM: + /* Handle charger cable */ + ret = max8997_muic_chg_handler(info); + break; + case MAX8997_MUICIRQ_OVP: + break; + default: + dev_info(info->dev, "misc interrupt: irq %d occurred\n", + irq_type); + mutex_unlock(&info->mutex); + return; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +static irqreturn_t max8997_muic_irq_handler(int irq, void *data) +{ + struct max8997_muic_info *info = data; + + dev_dbg(info->dev, "irq:%d\n", irq); + info->irq = irq; + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static int max8997_muic_detect_dev(struct max8997_muic_info *info) +{ + int ret = 0; + int adc; + int chg_type; + bool attached; + + mutex_lock(&info->mutex); + + /* Read STATUSx register to detect accessory */ + ret = max8997_bulk_read(info->muic, + MAX8997_MUIC_REG_STATUS1, 2, info->status); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return ret; + } + + adc = max8997_muic_get_cable_type(info, MAX8997_CABLE_GROUP_ADC, + &attached); + if (attached && adc != MAX8997_MUIC_ADC_OPEN) { + ret = max8997_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect ADC cable\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + chg_type = max8997_muic_get_cable_type(info, MAX8997_CABLE_GROUP_CHG, + &attached); + if (attached && chg_type != MAX8997_CHARGER_TYPE_NONE) { + ret = max8997_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger cable\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + mutex_unlock(&info->mutex); + + return 0; +} + +static void max8997_muic_detect_cable_wq(struct work_struct *work) +{ + struct max8997_muic_info *info = container_of(to_delayed_work(work), + struct max8997_muic_info, wq_detcable); + int ret; + + ret = max8997_muic_detect_dev(info); + if (ret < 0) + dev_err(info->dev, "failed to detect cable type\n"); +} + +static int max8997_muic_probe(struct platform_device *pdev) +{ + struct max8997_dev *max8997 = dev_get_drvdata(pdev->dev.parent); + struct max8997_platform_data *pdata = dev_get_platdata(max8997->dev); + struct max8997_muic_info *info; + int delay_jiffies; + int cable_type; + bool attached; + int ret, i; + + info = devm_kzalloc(&pdev->dev, sizeof(struct max8997_muic_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->muic = max8997->muic; + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, max8997_muic_irq_work); + ret = devm_work_autocancel(&pdev->dev, &info->irq_work, + max8997_muic_irq_work); + if (ret) + return ret; + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) { + struct max8997_muic_irq *muic_irq = &muic_irqs[i]; + unsigned int virq = 0; + + virq = irq_create_mapping(max8997->irq_domain, muic_irq->irq); + if (!virq) + return -EINVAL; + + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max8997_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* External connector */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, max8997_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + return PTR_ERR(info->edev); + } + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return ret; + } + + if (pdata && pdata->muic_pdata) { + struct max8997_muic_platform_data *muic_pdata + = pdata->muic_pdata; + + /* Initialize registers according to platform data */ + for (i = 0; i < muic_pdata->num_init_data; i++) { + max8997_write_reg(info->muic, + muic_pdata->init_data[i].addr, + muic_pdata->init_data[i].data); + } + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + if (muic_pdata->path_uart) + info->path_uart = muic_pdata->path_uart; + else + info->path_uart = CONTROL1_SW_UART; + + if (muic_pdata->path_usb) + info->path_usb = muic_pdata->path_usb; + else + info->path_usb = CONTROL1_SW_USB; + + /* + * Default delay time for detecting cable state + * after certain time. + */ + if (muic_pdata->detcable_delay_ms) + delay_jiffies = + msecs_to_jiffies(muic_pdata->detcable_delay_ms); + else + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } else { + info->path_uart = CONTROL1_SW_UART; + info->path_usb = CONTROL1_SW_USB; + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } + + /* Set initial path for UART when JIG is connected to get serial logs */ + ret = max8997_bulk_read(info->muic, MAX8997_MUIC_REG_STATUS1, + 2, info->status); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + return ret; + } + cable_type = max8997_muic_get_cable_type(info, + MAX8997_CABLE_GROUP_ADC, &attached); + if (attached && cable_type == MAX8997_MUIC_ADC_FACTORY_MODE_UART_OFF) + max8997_muic_set_path(info, info->path_uart, true); + + /* Set ADC debounce time */ + max8997_muic_set_debounce_time(info, ADC_DEBOUNCE_TIME_25MS); + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, max8997_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + delay_jiffies); + + return 0; +} + +static struct platform_driver max8997_muic_driver = { + .driver = { + .name = DEV_NAME, + }, + .probe = max8997_muic_probe, +}; + +module_platform_driver(max8997_muic_driver); + +MODULE_DESCRIPTION("Maxim MAX8997 Extcon driver"); +MODULE_AUTHOR("Donggeun Kim <dg77.kim@samsung.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:max8997-muic"); diff --git a/drivers/extcon/extcon-palmas.c b/drivers/extcon/extcon-palmas.c new file mode 100644 index 000000000..d339b8680 --- /dev/null +++ b/drivers/extcon/extcon-palmas.c @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Palmas USB transceiver driver + * + * Copyright (C) 2013 Texas Instruments Incorporated - https://www.ti.com + * Author: Graeme Gregory <gg@slimlogic.co.uk> + * Author: Kishon Vijay Abraham I <kishon@ti.com> + * Based on twl6030_usb.c + * Author: Hema HK <hemahk@ti.com> + */ + +#include <linux/devm-helpers.h> +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/err.h> +#include <linux/mfd/palmas.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/gpio/consumer.h> +#include <linux/workqueue.h> + +#define USB_GPIO_DEBOUNCE_MS 20 /* ms */ + +static const unsigned int palmas_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +static void palmas_usb_wakeup(struct palmas *palmas, int enable) +{ + if (enable) + palmas_write(palmas, PALMAS_USB_OTG_BASE, PALMAS_USB_WAKEUP, + PALMAS_USB_WAKEUP_ID_WK_UP_COMP); + else + palmas_write(palmas, PALMAS_USB_OTG_BASE, PALMAS_USB_WAKEUP, 0); +} + +static irqreturn_t palmas_vbus_irq_handler(int irq, void *_palmas_usb) +{ + struct palmas_usb *palmas_usb = _palmas_usb; + struct extcon_dev *edev = palmas_usb->edev; + unsigned int vbus_line_state; + + palmas_read(palmas_usb->palmas, PALMAS_INTERRUPT_BASE, + PALMAS_INT3_LINE_STATE, &vbus_line_state); + + if (vbus_line_state & PALMAS_INT3_LINE_STATE_VBUS) { + if (palmas_usb->linkstat != PALMAS_USB_STATE_VBUS) { + palmas_usb->linkstat = PALMAS_USB_STATE_VBUS; + extcon_set_state_sync(edev, EXTCON_USB, true); + dev_dbg(palmas_usb->dev, "USB cable is attached\n"); + } else { + dev_dbg(palmas_usb->dev, + "Spurious connect event detected\n"); + } + } else if (!(vbus_line_state & PALMAS_INT3_LINE_STATE_VBUS)) { + if (palmas_usb->linkstat == PALMAS_USB_STATE_VBUS) { + palmas_usb->linkstat = PALMAS_USB_STATE_DISCONNECT; + extcon_set_state_sync(edev, EXTCON_USB, false); + dev_dbg(palmas_usb->dev, "USB cable is detached\n"); + } else { + dev_dbg(palmas_usb->dev, + "Spurious disconnect event detected\n"); + } + } + + return IRQ_HANDLED; +} + +static irqreturn_t palmas_id_irq_handler(int irq, void *_palmas_usb) +{ + unsigned int set, id_src; + struct palmas_usb *palmas_usb = _palmas_usb; + struct extcon_dev *edev = palmas_usb->edev; + + palmas_read(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_LATCH_SET, &set); + palmas_read(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_SRC, &id_src); + + if ((set & PALMAS_USB_ID_INT_SRC_ID_GND) && + (id_src & PALMAS_USB_ID_INT_SRC_ID_GND)) { + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_LATCH_CLR, + PALMAS_USB_ID_INT_EN_HI_CLR_ID_GND); + palmas_usb->linkstat = PALMAS_USB_STATE_ID; + extcon_set_state_sync(edev, EXTCON_USB_HOST, true); + dev_dbg(palmas_usb->dev, "USB-HOST cable is attached\n"); + } else if ((set & PALMAS_USB_ID_INT_SRC_ID_FLOAT) && + (id_src & PALMAS_USB_ID_INT_SRC_ID_FLOAT)) { + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_LATCH_CLR, + PALMAS_USB_ID_INT_EN_HI_CLR_ID_FLOAT); + palmas_usb->linkstat = PALMAS_USB_STATE_DISCONNECT; + extcon_set_state_sync(edev, EXTCON_USB_HOST, false); + dev_dbg(palmas_usb->dev, "USB-HOST cable is detached\n"); + } else if ((palmas_usb->linkstat == PALMAS_USB_STATE_ID) && + (!(set & PALMAS_USB_ID_INT_SRC_ID_GND))) { + palmas_usb->linkstat = PALMAS_USB_STATE_DISCONNECT; + extcon_set_state_sync(edev, EXTCON_USB_HOST, false); + dev_dbg(palmas_usb->dev, "USB-HOST cable is detached\n"); + } else if ((palmas_usb->linkstat == PALMAS_USB_STATE_DISCONNECT) && + (id_src & PALMAS_USB_ID_INT_SRC_ID_GND)) { + palmas_usb->linkstat = PALMAS_USB_STATE_ID; + extcon_set_state_sync(edev, EXTCON_USB_HOST, true); + dev_dbg(palmas_usb->dev, "USB-HOST cable is attached\n"); + } + + return IRQ_HANDLED; +} + +static void palmas_gpio_id_detect(struct work_struct *work) +{ + int id; + struct palmas_usb *palmas_usb = container_of(to_delayed_work(work), + struct palmas_usb, + wq_detectid); + struct extcon_dev *edev = palmas_usb->edev; + + if (!palmas_usb->id_gpiod) + return; + + id = gpiod_get_value_cansleep(palmas_usb->id_gpiod); + + if (id) { + extcon_set_state_sync(edev, EXTCON_USB_HOST, false); + dev_dbg(palmas_usb->dev, "USB-HOST cable is detached\n"); + } else { + extcon_set_state_sync(edev, EXTCON_USB_HOST, true); + dev_dbg(palmas_usb->dev, "USB-HOST cable is attached\n"); + } +} + +static irqreturn_t palmas_gpio_id_irq_handler(int irq, void *_palmas_usb) +{ + struct palmas_usb *palmas_usb = _palmas_usb; + + queue_delayed_work(system_power_efficient_wq, &palmas_usb->wq_detectid, + palmas_usb->sw_debounce_jiffies); + + return IRQ_HANDLED; +} + +static void palmas_enable_irq(struct palmas_usb *palmas_usb) +{ + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_VBUS_CTRL_SET, + PALMAS_USB_VBUS_CTRL_SET_VBUS_ACT_COMP); + + if (palmas_usb->enable_id_detection) { + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_CTRL_SET, + PALMAS_USB_ID_CTRL_SET_ID_ACT_COMP); + + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_EN_HI_SET, + PALMAS_USB_ID_INT_EN_HI_SET_ID_GND | + PALMAS_USB_ID_INT_EN_HI_SET_ID_FLOAT); + } + + if (palmas_usb->enable_vbus_detection) + palmas_vbus_irq_handler(palmas_usb->vbus_irq, palmas_usb); + + /* cold plug for host mode needs this delay */ + if (palmas_usb->enable_id_detection) { + msleep(30); + palmas_id_irq_handler(palmas_usb->id_irq, palmas_usb); + } +} + +static int palmas_usb_probe(struct platform_device *pdev) +{ + struct palmas *palmas = dev_get_drvdata(pdev->dev.parent); + struct palmas_usb_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct device_node *node = pdev->dev.of_node; + struct palmas_usb *palmas_usb; + int status; + + if (!palmas) { + dev_err(&pdev->dev, "failed to get valid parent\n"); + return -EINVAL; + } + + palmas_usb = devm_kzalloc(&pdev->dev, sizeof(*palmas_usb), GFP_KERNEL); + if (!palmas_usb) + return -ENOMEM; + + if (node && !pdata) { + palmas_usb->wakeup = of_property_read_bool(node, "ti,wakeup"); + palmas_usb->enable_id_detection = of_property_read_bool(node, + "ti,enable-id-detection"); + palmas_usb->enable_vbus_detection = of_property_read_bool(node, + "ti,enable-vbus-detection"); + } else { + palmas_usb->wakeup = true; + palmas_usb->enable_id_detection = true; + palmas_usb->enable_vbus_detection = true; + + if (pdata) + palmas_usb->wakeup = pdata->wakeup; + } + + palmas_usb->id_gpiod = devm_gpiod_get_optional(&pdev->dev, "id", + GPIOD_IN); + if (IS_ERR(palmas_usb->id_gpiod)) + return dev_err_probe(&pdev->dev, PTR_ERR(palmas_usb->id_gpiod), + "failed to get id gpio\n"); + + palmas_usb->vbus_gpiod = devm_gpiod_get_optional(&pdev->dev, "vbus", + GPIOD_IN); + if (IS_ERR(palmas_usb->vbus_gpiod)) + return dev_err_probe(&pdev->dev, PTR_ERR(palmas_usb->vbus_gpiod), + "failed to get id gpio\n"); + + if (palmas_usb->enable_id_detection && palmas_usb->id_gpiod) { + palmas_usb->enable_id_detection = false; + palmas_usb->enable_gpio_id_detection = true; + } + + if (palmas_usb->enable_vbus_detection && palmas_usb->vbus_gpiod) { + palmas_usb->enable_vbus_detection = false; + palmas_usb->enable_gpio_vbus_detection = true; + } + + if (palmas_usb->enable_gpio_id_detection) { + u32 debounce; + + if (of_property_read_u32(node, "debounce-delay-ms", &debounce)) + debounce = USB_GPIO_DEBOUNCE_MS; + + status = gpiod_set_debounce(palmas_usb->id_gpiod, + debounce * 1000); + if (status < 0) + palmas_usb->sw_debounce_jiffies = msecs_to_jiffies(debounce); + } + + status = devm_delayed_work_autocancel(&pdev->dev, + &palmas_usb->wq_detectid, + palmas_gpio_id_detect); + if (status) + return status; + + palmas->usb = palmas_usb; + palmas_usb->palmas = palmas; + + palmas_usb->dev = &pdev->dev; + + palmas_usb_wakeup(palmas, palmas_usb->wakeup); + + platform_set_drvdata(pdev, palmas_usb); + + palmas_usb->edev = devm_extcon_dev_allocate(&pdev->dev, + palmas_extcon_cable); + if (IS_ERR(palmas_usb->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + status = devm_extcon_dev_register(&pdev->dev, palmas_usb->edev); + if (status) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return status; + } + + if (palmas_usb->enable_id_detection) { + palmas_usb->id_otg_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_ID_OTG_IRQ); + palmas_usb->id_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_ID_IRQ); + status = devm_request_threaded_irq(palmas_usb->dev, + palmas_usb->id_irq, + NULL, palmas_id_irq_handler, + IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | + IRQF_ONESHOT, + "palmas_usb_id", palmas_usb); + if (status < 0) { + dev_err(&pdev->dev, "can't get IRQ %d, err %d\n", + palmas_usb->id_irq, status); + return status; + } + } else if (palmas_usb->enable_gpio_id_detection) { + palmas_usb->gpio_id_irq = gpiod_to_irq(palmas_usb->id_gpiod); + if (palmas_usb->gpio_id_irq < 0) { + dev_err(&pdev->dev, "failed to get id irq\n"); + return palmas_usb->gpio_id_irq; + } + status = devm_request_threaded_irq(&pdev->dev, + palmas_usb->gpio_id_irq, + NULL, + palmas_gpio_id_irq_handler, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + "palmas_usb_id", + palmas_usb); + if (status < 0) { + dev_err(&pdev->dev, + "failed to request handler for id irq\n"); + return status; + } + } + + if (palmas_usb->enable_vbus_detection) { + palmas_usb->vbus_otg_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_VBUS_OTG_IRQ); + palmas_usb->vbus_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_VBUS_IRQ); + status = devm_request_threaded_irq(palmas_usb->dev, + palmas_usb->vbus_irq, NULL, + palmas_vbus_irq_handler, + IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | + IRQF_ONESHOT, + "palmas_usb_vbus", palmas_usb); + if (status < 0) { + dev_err(&pdev->dev, "can't get IRQ %d, err %d\n", + palmas_usb->vbus_irq, status); + return status; + } + } else if (palmas_usb->enable_gpio_vbus_detection) { + /* remux GPIO_1 as VBUSDET */ + status = palmas_update_bits(palmas, + PALMAS_PU_PD_OD_BASE, + PALMAS_PRIMARY_SECONDARY_PAD1, + PALMAS_PRIMARY_SECONDARY_PAD1_GPIO_1_MASK, + (1 << PALMAS_PRIMARY_SECONDARY_PAD1_GPIO_1_SHIFT)); + if (status < 0) { + dev_err(&pdev->dev, "can't remux GPIO1\n"); + return status; + } + + palmas_usb->vbus_otg_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_VBUS_OTG_IRQ); + palmas_usb->gpio_vbus_irq = gpiod_to_irq(palmas_usb->vbus_gpiod); + if (palmas_usb->gpio_vbus_irq < 0) { + dev_err(&pdev->dev, "failed to get vbus irq\n"); + return palmas_usb->gpio_vbus_irq; + } + status = devm_request_threaded_irq(&pdev->dev, + palmas_usb->gpio_vbus_irq, + NULL, + palmas_vbus_irq_handler, + IRQF_TRIGGER_FALLING | + IRQF_TRIGGER_RISING | + IRQF_ONESHOT, + "palmas_usb_vbus", + palmas_usb); + if (status < 0) { + dev_err(&pdev->dev, + "failed to request handler for vbus irq\n"); + return status; + } + } + + palmas_enable_irq(palmas_usb); + /* perform initial detection */ + if (palmas_usb->enable_gpio_vbus_detection) + palmas_vbus_irq_handler(palmas_usb->gpio_vbus_irq, palmas_usb); + palmas_gpio_id_detect(&palmas_usb->wq_detectid.work); + device_set_wakeup_capable(&pdev->dev, true); + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int palmas_usb_suspend(struct device *dev) +{ + struct palmas_usb *palmas_usb = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) { + if (palmas_usb->enable_vbus_detection) + enable_irq_wake(palmas_usb->vbus_irq); + if (palmas_usb->enable_gpio_vbus_detection) + enable_irq_wake(palmas_usb->gpio_vbus_irq); + if (palmas_usb->enable_id_detection) + enable_irq_wake(palmas_usb->id_irq); + if (palmas_usb->enable_gpio_id_detection) + enable_irq_wake(palmas_usb->gpio_id_irq); + } + return 0; +} + +static int palmas_usb_resume(struct device *dev) +{ + struct palmas_usb *palmas_usb = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) { + if (palmas_usb->enable_vbus_detection) + disable_irq_wake(palmas_usb->vbus_irq); + if (palmas_usb->enable_gpio_vbus_detection) + disable_irq_wake(palmas_usb->gpio_vbus_irq); + if (palmas_usb->enable_id_detection) + disable_irq_wake(palmas_usb->id_irq); + if (palmas_usb->enable_gpio_id_detection) + disable_irq_wake(palmas_usb->gpio_id_irq); + } + + /* check if GPIO states changed while suspend/resume */ + if (palmas_usb->enable_gpio_vbus_detection) + palmas_vbus_irq_handler(palmas_usb->gpio_vbus_irq, palmas_usb); + palmas_gpio_id_detect(&palmas_usb->wq_detectid.work); + + return 0; +}; +#endif + +static SIMPLE_DEV_PM_OPS(palmas_pm_ops, palmas_usb_suspend, palmas_usb_resume); + +static const struct of_device_id of_palmas_match_tbl[] = { + { .compatible = "ti,palmas-usb", }, + { .compatible = "ti,palmas-usb-vid", }, + { .compatible = "ti,twl6035-usb", }, + { .compatible = "ti,twl6035-usb-vid", }, + { /* end */ } +}; + +static struct platform_driver palmas_usb_driver = { + .probe = palmas_usb_probe, + .driver = { + .name = "palmas-usb", + .of_match_table = of_palmas_match_tbl, + .pm = &palmas_pm_ops, + }, +}; + +module_platform_driver(palmas_usb_driver); + +MODULE_ALIAS("platform:palmas-usb"); +MODULE_AUTHOR("Graeme Gregory <gg@slimlogic.co.uk>"); +MODULE_DESCRIPTION("Palmas USB transceiver driver"); +MODULE_LICENSE("GPL"); +MODULE_DEVICE_TABLE(of, of_palmas_match_tbl); diff --git a/drivers/extcon/extcon-ptn5150.c b/drivers/extcon/extcon-ptn5150.c new file mode 100644 index 000000000..eca1b140a --- /dev/null +++ b/drivers/extcon/extcon-ptn5150.c @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// extcon-ptn5150.c - PTN5150 CC logic extcon driver to support USB detection +// +// Based on extcon-sm5502.c driver +// Copyright (c) 2018-2019 by Vijai Kumar K +// Author: Vijai Kumar K <vijaikumar.kanagarajan@gmail.com> +// Copyright (c) 2020 Krzysztof Kozlowski <krzk@kernel.org> + +#include <linux/bitfield.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/extcon-provider.h> +#include <linux/gpio/consumer.h> +#include <linux/usb/role.h> +#include <linux/usb/typec_mux.h> + +/* PTN5150 registers */ +#define PTN5150_REG_DEVICE_ID 0x01 +#define PTN5150_REG_CONTROL 0x02 +#define PTN5150_REG_INT_STATUS 0x03 +#define PTN5150_REG_CC_STATUS 0x04 +#define PTN5150_REG_CON_DET 0x09 +#define PTN5150_REG_VCONN_STATUS 0x0a +#define PTN5150_REG_RESET 0x0b +#define PTN5150_REG_INT_MASK 0x18 +#define PTN5150_REG_INT_REG_STATUS 0x19 +#define PTN5150_REG_END PTN5150_REG_INT_REG_STATUS + +#define PTN5150_DFP_ATTACHED 0x1 +#define PTN5150_UFP_ATTACHED 0x2 + +/* Define PTN5150 MASK/SHIFT constant */ +#define PTN5150_REG_DEVICE_ID_VERSION GENMASK(7, 3) +#define PTN5150_REG_DEVICE_ID_VENDOR GENMASK(2, 0) + +#define PTN5150_POLARITY_CC1 0x1 +#define PTN5150_POLARITY_CC2 0x2 + +#define PTN5150_REG_CC_PORT_ATTACHMENT GENMASK(4, 2) +#define PTN5150_REG_CC_POLARITY GENMASK(1, 0) +#define PTN5150_REG_CC_VBUS_DETECTION BIT(7) +#define PTN5150_REG_INT_CABLE_ATTACH_MASK BIT(0) +#define PTN5150_REG_INT_CABLE_DETACH_MASK BIT(1) + +struct ptn5150_info { + struct device *dev; + struct extcon_dev *edev; + struct i2c_client *i2c; + struct regmap *regmap; + struct gpio_desc *int_gpiod; + struct gpio_desc *vbus_gpiod; + int irq; + struct work_struct irq_work; + struct mutex mutex; + struct typec_switch *orient_sw; + struct usb_role_switch *role_sw; +}; + +/* List of detectable cables */ +static const unsigned int ptn5150_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +static const struct regmap_config ptn5150_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = PTN5150_REG_END, +}; + +static void ptn5150_check_state(struct ptn5150_info *info) +{ + enum typec_orientation orient = TYPEC_ORIENTATION_NONE; + unsigned int port_status, reg_data, vbus; + enum usb_role usb_role = USB_ROLE_NONE; + int ret; + + ret = regmap_read(info->regmap, PTN5150_REG_CC_STATUS, ®_data); + if (ret) { + dev_err(info->dev, "failed to read CC STATUS %d\n", ret); + return; + } + + orient = FIELD_GET(PTN5150_REG_CC_POLARITY, reg_data); + switch (orient) { + case PTN5150_POLARITY_CC1: + orient = TYPEC_ORIENTATION_NORMAL; + break; + case PTN5150_POLARITY_CC2: + orient = TYPEC_ORIENTATION_REVERSE; + break; + default: + orient = TYPEC_ORIENTATION_NONE; + break; + } + + ret = typec_switch_set(info->orient_sw, orient); + if (ret) + dev_err(info->dev, "failed to set orientation: %d\n", ret); + + port_status = FIELD_GET(PTN5150_REG_CC_PORT_ATTACHMENT, reg_data); + + switch (port_status) { + case PTN5150_DFP_ATTACHED: + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, false); + gpiod_set_value_cansleep(info->vbus_gpiod, 0); + extcon_set_state_sync(info->edev, EXTCON_USB, true); + usb_role = USB_ROLE_DEVICE; + break; + case PTN5150_UFP_ATTACHED: + extcon_set_state_sync(info->edev, EXTCON_USB, false); + vbus = FIELD_GET(PTN5150_REG_CC_VBUS_DETECTION, reg_data); + if (vbus) + gpiod_set_value_cansleep(info->vbus_gpiod, 0); + else + gpiod_set_value_cansleep(info->vbus_gpiod, 1); + + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, true); + usb_role = USB_ROLE_HOST; + break; + default: + break; + } + + if (usb_role) { + ret = usb_role_switch_set_role(info->role_sw, usb_role); + if (ret) + dev_err(info->dev, "failed to set %s role: %d\n", + usb_role_string(usb_role), ret); + } +} + +static void ptn5150_irq_work(struct work_struct *work) +{ + struct ptn5150_info *info = container_of(work, + struct ptn5150_info, irq_work); + int ret = 0; + unsigned int int_status; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + /* Clear interrupt. Read would clear the register */ + ret = regmap_read(info->regmap, PTN5150_REG_INT_STATUS, &int_status); + if (ret) { + dev_err(info->dev, "failed to read INT STATUS %d\n", ret); + mutex_unlock(&info->mutex); + return; + } + + if (int_status) { + unsigned int cable_attach; + + cable_attach = int_status & PTN5150_REG_INT_CABLE_ATTACH_MASK; + if (cable_attach) { + ptn5150_check_state(info); + } else { + extcon_set_state_sync(info->edev, + EXTCON_USB_HOST, false); + extcon_set_state_sync(info->edev, + EXTCON_USB, false); + gpiod_set_value_cansleep(info->vbus_gpiod, 0); + + ret = usb_role_switch_set_role(info->role_sw, + USB_ROLE_NONE); + if (ret) + dev_err(info->dev, + "failed to set none role: %d\n", + ret); + + ret = typec_switch_set(info->orient_sw, + TYPEC_ORIENTATION_NONE); + if (ret) + dev_err(info->dev, + "failed to set orientation: %d\n", ret); + } + } + + /* Clear interrupt. Read would clear the register */ + ret = regmap_read(info->regmap, PTN5150_REG_INT_REG_STATUS, + &int_status); + if (ret) { + dev_err(info->dev, + "failed to read INT REG STATUS %d\n", ret); + mutex_unlock(&info->mutex); + return; + } + + mutex_unlock(&info->mutex); +} + + +static irqreturn_t ptn5150_irq_handler(int irq, void *data) +{ + struct ptn5150_info *info = data; + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static int ptn5150_init_dev_type(struct ptn5150_info *info) +{ + unsigned int reg_data, vendor_id, version_id; + int ret; + + ret = regmap_read(info->regmap, PTN5150_REG_DEVICE_ID, ®_data); + if (ret) { + dev_err(info->dev, "failed to read DEVICE_ID %d\n", ret); + return -EINVAL; + } + + vendor_id = FIELD_GET(PTN5150_REG_DEVICE_ID_VENDOR, reg_data); + version_id = FIELD_GET(PTN5150_REG_DEVICE_ID_VERSION, reg_data); + dev_dbg(info->dev, "Device type: version: 0x%x, vendor: 0x%x\n", + version_id, vendor_id); + + /* Clear any existing interrupts */ + ret = regmap_read(info->regmap, PTN5150_REG_INT_STATUS, ®_data); + if (ret) { + dev_err(info->dev, + "failed to read PTN5150_REG_INT_STATUS %d\n", + ret); + return -EINVAL; + } + + ret = regmap_read(info->regmap, PTN5150_REG_INT_REG_STATUS, ®_data); + if (ret) { + dev_err(info->dev, + "failed to read PTN5150_REG_INT_REG_STATUS %d\n", ret); + return -EINVAL; + } + + return 0; +} + +static void ptn5150_work_sync_and_put(void *data) +{ + struct ptn5150_info *info = data; + + cancel_work_sync(&info->irq_work); + usb_role_switch_put(info->role_sw); + typec_switch_put(info->orient_sw); +} + +static int ptn5150_i2c_probe(struct i2c_client *i2c) +{ + struct device *dev = &i2c->dev; + struct device_node *np = i2c->dev.of_node; + struct fwnode_handle *connector; + struct ptn5150_info *info; + int ret; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&i2c->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + i2c_set_clientdata(i2c, info); + + info->dev = &i2c->dev; + info->i2c = i2c; + info->vbus_gpiod = devm_gpiod_get(&i2c->dev, "vbus", GPIOD_OUT_LOW); + if (IS_ERR(info->vbus_gpiod)) { + ret = PTR_ERR(info->vbus_gpiod); + if (ret == -ENOENT) { + dev_info(dev, "No VBUS GPIO, ignoring VBUS control\n"); + info->vbus_gpiod = NULL; + } else { + return dev_err_probe(dev, ret, "failed to get VBUS GPIO\n"); + } + } + + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, ptn5150_irq_work); + + info->regmap = devm_regmap_init_i2c(i2c, &ptn5150_regmap_config); + if (IS_ERR(info->regmap)) { + return dev_err_probe(info->dev, PTR_ERR(info->regmap), + "failed to allocate register map\n"); + } + + if (i2c->irq > 0) { + info->irq = i2c->irq; + } else { + info->int_gpiod = devm_gpiod_get(&i2c->dev, "int", GPIOD_IN); + if (IS_ERR(info->int_gpiod)) { + return dev_err_probe(dev, PTR_ERR(info->int_gpiod), + "failed to get INT GPIO\n"); + } + + info->irq = gpiod_to_irq(info->int_gpiod); + if (info->irq < 0) { + dev_err(dev, "failed to get INTB IRQ\n"); + return info->irq; + } + } + + ret = devm_request_threaded_irq(dev, info->irq, NULL, + ptn5150_irq_handler, + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + i2c->name, info); + if (ret < 0) { + dev_err(dev, "failed to request handler for INTB IRQ\n"); + return ret; + } + + /* Allocate extcon device */ + info->edev = devm_extcon_dev_allocate(info->dev, ptn5150_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(info->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + + /* Register extcon device */ + ret = devm_extcon_dev_register(info->dev, info->edev); + if (ret) { + dev_err(info->dev, "failed to register extcon device\n"); + return ret; + } + + extcon_set_property_capability(info->edev, EXTCON_USB, + EXTCON_PROP_USB_VBUS); + extcon_set_property_capability(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_VBUS); + extcon_set_property_capability(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY); + + /* Initialize PTN5150 device and print vendor id and version id */ + ret = ptn5150_init_dev_type(info); + if (ret) + return -EINVAL; + + connector = device_get_named_child_node(dev, "connector"); + if (connector) { + info->orient_sw = fwnode_typec_switch_get(connector); + if (IS_ERR(info->orient_sw)) + return dev_err_probe(info->dev, PTR_ERR(info->orient_sw), + "failed to get orientation switch\n"); + } + + info->role_sw = usb_role_switch_get(info->dev); + if (!info->role_sw && connector) + info->role_sw = fwnode_usb_role_switch_get(connector); + if (IS_ERR(info->role_sw)) + return dev_err_probe(info->dev, PTR_ERR(info->role_sw), + "failed to get role switch\n"); + + ret = devm_add_action_or_reset(dev, ptn5150_work_sync_and_put, info); + if (ret) + return ret; + + /* + * Update current extcon state if for example OTG connection was there + * before the probe + */ + mutex_lock(&info->mutex); + ptn5150_check_state(info); + mutex_unlock(&info->mutex); + + return 0; +} + +static int ptn5150_resume(struct device *dev) +{ + struct i2c_client *i2c = to_i2c_client(dev); + struct ptn5150_info *info = i2c_get_clientdata(i2c); + + /* Need to check possible pending interrupt events */ + schedule_work(&info->irq_work); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(ptn5150_pm_ops, NULL, ptn5150_resume); + +static const struct of_device_id ptn5150_dt_match[] = { + { .compatible = "nxp,ptn5150" }, + { }, +}; +MODULE_DEVICE_TABLE(of, ptn5150_dt_match); + +static const struct i2c_device_id ptn5150_i2c_id[] = { + { "ptn5150" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ptn5150_i2c_id); + +static struct i2c_driver ptn5150_i2c_driver = { + .driver = { + .name = "ptn5150", + .pm = pm_sleep_ptr(&ptn5150_pm_ops), + .of_match_table = ptn5150_dt_match, + }, + .probe = ptn5150_i2c_probe, + .id_table = ptn5150_i2c_id, +}; +module_i2c_driver(ptn5150_i2c_driver); + +MODULE_DESCRIPTION("NXP PTN5150 CC logic Extcon driver"); +MODULE_AUTHOR("Vijai Kumar K <vijaikumar.kanagarajan@gmail.com>"); +MODULE_AUTHOR("Krzysztof Kozlowski <krzk@kernel.org>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-qcom-spmi-misc.c b/drivers/extcon/extcon-qcom-spmi-misc.c new file mode 100644 index 000000000..3c522c9c9 --- /dev/null +++ b/drivers/extcon/extcon-qcom-spmi-misc.c @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * extcon-qcom-spmi-misc.c - Qualcomm USB extcon driver to support USB ID + * and VBUS detection based on extcon-usb-gpio.c. + * + * Copyright (C) 2016 Linaro, Ltd. + * Stephen Boyd <stephen.boyd@linaro.org> + */ + +#include <linux/devm-helpers.h> +#include <linux/extcon-provider.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +#define USB_ID_DEBOUNCE_MS 5 /* ms */ + +struct qcom_usb_extcon_info { + struct extcon_dev *edev; + int id_irq; + int vbus_irq; + struct delayed_work wq_detcable; + unsigned long debounce_jiffies; +}; + +static const unsigned int qcom_usb_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +static void qcom_usb_extcon_detect_cable(struct work_struct *work) +{ + bool state = false; + int ret; + union extcon_property_value val; + struct qcom_usb_extcon_info *info = container_of(to_delayed_work(work), + struct qcom_usb_extcon_info, + wq_detcable); + + if (info->id_irq > 0) { + /* check ID and update cable state */ + ret = irq_get_irqchip_state(info->id_irq, + IRQCHIP_STATE_LINE_LEVEL, &state); + if (ret) + return; + + if (!state) { + val.intval = true; + extcon_set_property(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_SS, val); + } + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, !state); + } + + if (info->vbus_irq > 0) { + /* check VBUS and update cable state */ + ret = irq_get_irqchip_state(info->vbus_irq, + IRQCHIP_STATE_LINE_LEVEL, &state); + if (ret) + return; + + if (state) { + val.intval = true; + extcon_set_property(info->edev, EXTCON_USB, + EXTCON_PROP_USB_SS, val); + } + extcon_set_state_sync(info->edev, EXTCON_USB, state); + } +} + +static irqreturn_t qcom_usb_irq_handler(int irq, void *dev_id) +{ + struct qcom_usb_extcon_info *info = dev_id; + + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + info->debounce_jiffies); + + return IRQ_HANDLED; +} + +static int qcom_usb_extcon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct qcom_usb_extcon_info *info; + int ret; + + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->edev = devm_extcon_dev_allocate(dev, qcom_usb_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + ret = devm_extcon_dev_register(dev, info->edev); + if (ret < 0) { + dev_err(dev, "failed to register extcon device\n"); + return ret; + } + + ret = extcon_set_property_capability(info->edev, + EXTCON_USB, EXTCON_PROP_USB_SS); + ret |= extcon_set_property_capability(info->edev, + EXTCON_USB_HOST, EXTCON_PROP_USB_SS); + if (ret) { + dev_err(dev, "failed to register extcon props rc=%d\n", + ret); + return ret; + } + + info->debounce_jiffies = msecs_to_jiffies(USB_ID_DEBOUNCE_MS); + + ret = devm_delayed_work_autocancel(dev, &info->wq_detcable, + qcom_usb_extcon_detect_cable); + if (ret) + return ret; + + info->id_irq = platform_get_irq_byname_optional(pdev, "usb_id"); + if (info->id_irq > 0) { + ret = devm_request_threaded_irq(dev, info->id_irq, NULL, + qcom_usb_irq_handler, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + pdev->name, info); + if (ret < 0) { + dev_err(dev, "failed to request handler for ID IRQ\n"); + return ret; + } + } + + info->vbus_irq = platform_get_irq_byname_optional(pdev, "usb_vbus"); + if (info->vbus_irq > 0) { + ret = devm_request_threaded_irq(dev, info->vbus_irq, NULL, + qcom_usb_irq_handler, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + pdev->name, info); + if (ret < 0) { + dev_err(dev, "failed to request handler for VBUS IRQ\n"); + return ret; + } + } + + if (info->id_irq < 0 && info->vbus_irq < 0) { + dev_err(dev, "ID and VBUS IRQ not found\n"); + return -EINVAL; + } + + platform_set_drvdata(pdev, info); + devm_device_init_wakeup(dev); + + /* Perform initial detection */ + qcom_usb_extcon_detect_cable(&info->wq_detcable.work); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int qcom_usb_extcon_suspend(struct device *dev) +{ + struct qcom_usb_extcon_info *info = dev_get_drvdata(dev); + int ret = 0; + + if (device_may_wakeup(dev)) { + if (info->id_irq > 0) + ret = enable_irq_wake(info->id_irq); + if (info->vbus_irq > 0) + ret = enable_irq_wake(info->vbus_irq); + } + + return ret; +} + +static int qcom_usb_extcon_resume(struct device *dev) +{ + struct qcom_usb_extcon_info *info = dev_get_drvdata(dev); + int ret = 0; + + if (device_may_wakeup(dev)) { + if (info->id_irq > 0) + ret = disable_irq_wake(info->id_irq); + if (info->vbus_irq > 0) + ret = disable_irq_wake(info->vbus_irq); + } + + return ret; +} +#endif + +static SIMPLE_DEV_PM_OPS(qcom_usb_extcon_pm_ops, + qcom_usb_extcon_suspend, qcom_usb_extcon_resume); + +static const struct of_device_id qcom_usb_extcon_dt_match[] = { + { .compatible = "qcom,pm8941-misc", }, + { } +}; +MODULE_DEVICE_TABLE(of, qcom_usb_extcon_dt_match); + +static struct platform_driver qcom_usb_extcon_driver = { + .probe = qcom_usb_extcon_probe, + .driver = { + .name = "extcon-pm8941-misc", + .pm = &qcom_usb_extcon_pm_ops, + .of_match_table = qcom_usb_extcon_dt_match, + }, +}; +module_platform_driver(qcom_usb_extcon_driver); + +MODULE_DESCRIPTION("QCOM USB ID extcon driver"); +MODULE_AUTHOR("Stephen Boyd <stephen.boyd@linaro.org>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-rt8973a.c b/drivers/extcon/extcon-rt8973a.c new file mode 100644 index 000000000..19bb49f13 --- /dev/null +++ b/drivers/extcon/extcon-rt8973a.c @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * extcon-rt8973a.c - Richtek RT8973A extcon driver to support USB switches + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + * Author: Chanwoo Choi <cw00.choi@samsung.com> + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/input.h> +#include <linux/interrupt.h> +#include <linux/irqdomain.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/extcon-provider.h> + +#include "extcon-rt8973a.h" + +#define DELAY_MS_DEFAULT 20000 /* unit: millisecond */ + +struct muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +struct reg_data { + u8 reg; + u8 mask; + u8 val; + bool invert; +}; + +struct rt8973a_muic_info { + struct device *dev; + struct extcon_dev *edev; + + struct i2c_client *i2c; + struct regmap *regmap; + + struct regmap_irq_chip_data *irq_data; + struct muic_irq *muic_irqs; + unsigned int num_muic_irqs; + int irq; + bool irq_attach; + bool irq_detach; + bool irq_ovp; + bool irq_otp; + struct work_struct irq_work; + + struct reg_data *reg_data; + unsigned int num_reg_data; + bool auto_config; + + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; +}; + +/* Default value of RT8973A register to bring up MUIC device. */ +static struct reg_data rt8973a_reg_data[] = { + { + .reg = RT8973A_REG_CONTROL1, + .mask = RT8973A_REG_CONTROL1_ADC_EN_MASK + | RT8973A_REG_CONTROL1_USB_CHD_EN_MASK + | RT8973A_REG_CONTROL1_CHGTYP_MASK + | RT8973A_REG_CONTROL1_SWITCH_OPEN_MASK + | RT8973A_REG_CONTROL1_AUTO_CONFIG_MASK + | RT8973A_REG_CONTROL1_INTM_MASK, + .val = RT8973A_REG_CONTROL1_ADC_EN_MASK + | RT8973A_REG_CONTROL1_USB_CHD_EN_MASK + | RT8973A_REG_CONTROL1_CHGTYP_MASK, + .invert = false, + }, + { /* sentinel */ } +}; + +/* List of detectable cables */ +static const unsigned int rt8973a_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_DCP, + EXTCON_JIG, + EXTCON_NONE, +}; + +/* Define OVP (Over Voltage Protection), OTP (Over Temperature Protection) */ +enum rt8973a_event_type { + RT8973A_EVENT_ATTACH = 1, + RT8973A_EVENT_DETACH, + RT8973A_EVENT_OVP, + RT8973A_EVENT_OTP, +}; + +/* Define supported accessory type */ +enum rt8973a_muic_acc_type { + RT8973A_MUIC_ADC_OTG = 0x0, + RT8973A_MUIC_ADC_AUDIO_SEND_END_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S1_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S2_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S3_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S4_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S5_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S6_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S7_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S8_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S9_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S10_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S11_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S12_BUTTON, + RT8973A_MUIC_ADC_RESERVED_ACC_1, + RT8973A_MUIC_ADC_RESERVED_ACC_2, + RT8973A_MUIC_ADC_RESERVED_ACC_3, + RT8973A_MUIC_ADC_RESERVED_ACC_4, + RT8973A_MUIC_ADC_RESERVED_ACC_5, + RT8973A_MUIC_ADC_AUDIO_TYPE2, + RT8973A_MUIC_ADC_PHONE_POWERED_DEV, + RT8973A_MUIC_ADC_UNKNOWN_ACC_1, + RT8973A_MUIC_ADC_UNKNOWN_ACC_2, + RT8973A_MUIC_ADC_TA, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB, + RT8973A_MUIC_ADC_UNKNOWN_ACC_3, + RT8973A_MUIC_ADC_UNKNOWN_ACC_4, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART, + RT8973A_MUIC_ADC_UNKNOWN_ACC_5, + RT8973A_MUIC_ADC_OPEN = 0x1f, + + /* + * The below accessories has same ADC value (0x1f). + * So, Device type1 is used to separate specific accessory. + */ + /* |---------|--ADC| */ + /* | [7:5]|[4:0]| */ + RT8973A_MUIC_ADC_USB = 0x3f, /* | 001|11111| */ +}; + +/* List of supported interrupt for RT8973A */ +static struct muic_irq rt8973a_muic_irqs[] = { + { RT8973A_INT1_ATTACH, "muic-attach" }, + { RT8973A_INT1_DETACH, "muic-detach" }, + { RT8973A_INT1_CHGDET, "muic-chgdet" }, + { RT8973A_INT1_DCD_T, "muic-dcd-t" }, + { RT8973A_INT1_OVP, "muic-ovp" }, + { RT8973A_INT1_CONNECT, "muic-connect" }, + { RT8973A_INT1_ADC_CHG, "muic-adc-chg" }, + { RT8973A_INT1_OTP, "muic-otp" }, + { RT8973A_INT2_UVLO, "muic-uvlo" }, + { RT8973A_INT2_POR, "muic-por" }, + { RT8973A_INT2_OTP_FET, "muic-otp-fet" }, + { RT8973A_INT2_OVP_FET, "muic-ovp-fet" }, + { RT8973A_INT2_OCP_LATCH, "muic-ocp-latch" }, + { RT8973A_INT2_OCP, "muic-ocp" }, + { RT8973A_INT2_OVP_OCP, "muic-ovp-ocp" }, +}; + +/* Define interrupt list of RT8973A to register regmap_irq */ +static const struct regmap_irq rt8973a_irqs[] = { + /* INT1 interrupts */ + { .reg_offset = 0, .mask = RT8973A_INT1_ATTACH_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_DETACH_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_CHGDET_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_DCD_T_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_OVP_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_CONNECT_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_ADC_CHG_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_OTP_MASK, }, + + /* INT2 interrupts */ + { .reg_offset = 1, .mask = RT8973A_INT2_UVLOT_MASK,}, + { .reg_offset = 1, .mask = RT8973A_INT2_POR_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OTP_FET_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OVP_FET_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OCP_LATCH_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OCP_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OVP_OCP_MASK, }, +}; + +static const struct regmap_irq_chip rt8973a_muic_irq_chip = { + .name = "rt8973a", + .status_base = RT8973A_REG_INT1, + .mask_base = RT8973A_REG_INTM1, + .num_regs = 2, + .irqs = rt8973a_irqs, + .num_irqs = ARRAY_SIZE(rt8973a_irqs), +}; + +/* Define regmap configuration of RT8973A for I2C communication */ +static bool rt8973a_muic_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case RT8973A_REG_INTM1: + case RT8973A_REG_INTM2: + return true; + default: + break; + } + return false; +} + +static const struct regmap_config rt8973a_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .volatile_reg = rt8973a_muic_volatile_reg, + .max_register = RT8973A_REG_END, +}; + +/* Change DM_CON/DP_CON/VBUSIN switch according to cable type */ +static int rt8973a_muic_set_path(struct rt8973a_muic_info *info, + unsigned int con_sw, bool attached) +{ + int ret; + + /* + * Don't need to set h/w path according to cable type + * if Auto-configuration mode of CONTROL1 register is true. + */ + if (info->auto_config) + return 0; + + if (!attached) + con_sw = DM_DP_SWITCH_UART; + + switch (con_sw) { + case DM_DP_SWITCH_OPEN: + case DM_DP_SWITCH_USB: + case DM_DP_SWITCH_UART: + ret = regmap_update_bits(info->regmap, RT8973A_REG_MANUAL_SW1, + RT8973A_REG_MANUAL_SW1_DP_MASK | + RT8973A_REG_MANUAL_SW1_DM_MASK, + con_sw); + if (ret < 0) { + dev_err(info->dev, + "cannot update DM_CON/DP_CON switch\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Unknown DM_CON/DP_CON switch type (%d)\n", + con_sw); + return -EINVAL; + } + + return 0; +} + +static int rt8973a_muic_get_cable_type(struct rt8973a_muic_info *info) +{ + unsigned int adc, dev1; + int ret, cable_type; + + /* Read ADC value according to external cable or button */ + ret = regmap_read(info->regmap, RT8973A_REG_ADC, &adc); + if (ret) { + dev_err(info->dev, "failed to read ADC register\n"); + return ret; + } + cable_type = adc & RT8973A_REG_ADC_MASK; + + /* Read Device 1 reigster to identify correct cable type */ + ret = regmap_read(info->regmap, RT8973A_REG_DEV1, &dev1); + if (ret) { + dev_err(info->dev, "failed to read DEV1 register\n"); + return ret; + } + + switch (adc) { + case RT8973A_MUIC_ADC_OPEN: + if (dev1 & RT8973A_REG_DEV1_USB_MASK) + cable_type = RT8973A_MUIC_ADC_USB; + else if (dev1 & RT8973A_REG_DEV1_DCPORT_MASK) + cable_type = RT8973A_MUIC_ADC_TA; + else + cable_type = RT8973A_MUIC_ADC_OPEN; + break; + default: + break; + } + + return cable_type; +} + +static int rt8973a_muic_cable_handler(struct rt8973a_muic_info *info, + enum rt8973a_event_type event) +{ + static unsigned int prev_cable_type; + unsigned int con_sw = DM_DP_SWITCH_UART; + int ret, cable_type; + unsigned int id; + bool attached = false; + + switch (event) { + case RT8973A_EVENT_ATTACH: + cable_type = rt8973a_muic_get_cable_type(info); + attached = true; + break; + case RT8973A_EVENT_DETACH: + cable_type = prev_cable_type; + attached = false; + break; + case RT8973A_EVENT_OVP: + case RT8973A_EVENT_OTP: + dev_warn(info->dev, + "happen Over %s issue. Need to disconnect all cables\n", + event == RT8973A_EVENT_OVP ? "Voltage" : "Temperature"); + cable_type = prev_cable_type; + attached = false; + break; + default: + dev_err(info->dev, + "Cannot handle this event (event:%d)\n", event); + return -EINVAL; + } + prev_cable_type = cable_type; + + switch (cable_type) { + case RT8973A_MUIC_ADC_OTG: + id = EXTCON_USB_HOST; + con_sw = DM_DP_SWITCH_USB; + break; + case RT8973A_MUIC_ADC_TA: + id = EXTCON_CHG_USB_DCP; + con_sw = DM_DP_SWITCH_OPEN; + break; + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB: + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB: + id = EXTCON_JIG; + con_sw = DM_DP_SWITCH_USB; + break; + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART: + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART: + id = EXTCON_JIG; + con_sw = DM_DP_SWITCH_UART; + break; + case RT8973A_MUIC_ADC_USB: + id = EXTCON_USB; + con_sw = DM_DP_SWITCH_USB; + break; + case RT8973A_MUIC_ADC_OPEN: + return 0; + case RT8973A_MUIC_ADC_UNKNOWN_ACC_1: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_2: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_3: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_4: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_5: + dev_warn(info->dev, + "Unknown accessory type (adc:0x%x)\n", cable_type); + return 0; + case RT8973A_MUIC_ADC_AUDIO_SEND_END_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S1_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S2_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S3_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S4_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S5_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S6_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S7_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S8_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S9_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S10_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S11_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S12_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_TYPE2: + dev_warn(info->dev, + "Audio device/button type (adc:0x%x)\n", cable_type); + return 0; + case RT8973A_MUIC_ADC_RESERVED_ACC_1: + case RT8973A_MUIC_ADC_RESERVED_ACC_2: + case RT8973A_MUIC_ADC_RESERVED_ACC_3: + case RT8973A_MUIC_ADC_RESERVED_ACC_4: + case RT8973A_MUIC_ADC_RESERVED_ACC_5: + case RT8973A_MUIC_ADC_PHONE_POWERED_DEV: + return 0; + default: + dev_err(info->dev, + "Cannot handle this cable_type (adc:0x%x)\n", + cable_type); + return -EINVAL; + } + + /* Change internal hardware path(DM_CON/DP_CON) */ + ret = rt8973a_muic_set_path(info, con_sw, attached); + if (ret < 0) + return ret; + + /* Change the state of external accessory */ + extcon_set_state_sync(info->edev, id, attached); + if (id == EXTCON_USB) + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + + return 0; +} + +static void rt8973a_muic_irq_work(struct work_struct *work) +{ + struct rt8973a_muic_info *info = container_of(work, + struct rt8973a_muic_info, irq_work); + int ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + /* Detect attached or detached cables */ + if (info->irq_attach) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_ATTACH); + info->irq_attach = false; + } + + if (info->irq_detach) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_DETACH); + info->irq_detach = false; + } + + if (info->irq_ovp) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_OVP); + info->irq_ovp = false; + } + + if (info->irq_otp) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_OTP); + info->irq_otp = false; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +static irqreturn_t rt8973a_muic_irq_handler(int irq, void *data) +{ + struct rt8973a_muic_info *info = data; + int i, irq_type = -1; + + for (i = 0; i < info->num_muic_irqs; i++) + if (irq == info->muic_irqs[i].virq) + irq_type = info->muic_irqs[i].irq; + + switch (irq_type) { + case RT8973A_INT1_ATTACH: + info->irq_attach = true; + break; + case RT8973A_INT1_DETACH: + info->irq_detach = true; + break; + case RT8973A_INT1_OVP: + info->irq_ovp = true; + break; + case RT8973A_INT1_OTP: + info->irq_otp = true; + break; + case RT8973A_INT1_CHGDET: + case RT8973A_INT1_DCD_T: + case RT8973A_INT1_CONNECT: + case RT8973A_INT1_ADC_CHG: + case RT8973A_INT2_UVLO: + case RT8973A_INT2_POR: + case RT8973A_INT2_OTP_FET: + case RT8973A_INT2_OVP_FET: + case RT8973A_INT2_OCP_LATCH: + case RT8973A_INT2_OCP: + case RT8973A_INT2_OVP_OCP: + default: + dev_dbg(info->dev, + "Cannot handle this interrupt (%d)\n", irq_type); + break; + } + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static void rt8973a_muic_detect_cable_wq(struct work_struct *work) +{ + struct rt8973a_muic_info *info = container_of(to_delayed_work(work), + struct rt8973a_muic_info, wq_detcable); + int ret; + + /* Notify the state of connector cable or not */ + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_ATTACH); + if (ret < 0) + dev_warn(info->dev, "failed to detect cable state\n"); +} + +static void rt8973a_init_dev_type(struct rt8973a_muic_info *info) +{ + unsigned int data, vendor_id, version_id; + int i, ret; + + /* To test I2C, Print version_id and vendor_id of RT8973A */ + ret = regmap_read(info->regmap, RT8973A_REG_DEVICE_ID, &data); + if (ret) { + dev_err(info->dev, + "failed to read DEVICE_ID register: %d\n", ret); + return; + } + + vendor_id = ((data & RT8973A_REG_DEVICE_ID_VENDOR_MASK) >> + RT8973A_REG_DEVICE_ID_VENDOR_SHIFT); + version_id = ((data & RT8973A_REG_DEVICE_ID_VERSION_MASK) >> + RT8973A_REG_DEVICE_ID_VERSION_SHIFT); + + dev_info(info->dev, "Device type: version: 0x%x, vendor: 0x%x\n", + version_id, vendor_id); + + /* Initiazle the register of RT8973A device to bring-up */ + for (i = 0; i < info->num_reg_data; i++) { + u8 reg = info->reg_data[i].reg; + u8 mask = info->reg_data[i].mask; + u8 val = 0; + + if (info->reg_data[i].invert) + val = ~info->reg_data[i].val; + else + val = info->reg_data[i].val; + + regmap_update_bits(info->regmap, reg, mask, val); + } + + /* Check whether RT8973A is auto switching mode or not */ + ret = regmap_read(info->regmap, RT8973A_REG_CONTROL1, &data); + if (ret) { + dev_err(info->dev, + "failed to read CONTROL1 register: %d\n", ret); + return; + } + + data &= RT8973A_REG_CONTROL1_AUTO_CONFIG_MASK; + if (data) { + info->auto_config = true; + dev_info(info->dev, + "Enable Auto-configuration for internal path\n"); + } +} + +static int rt8973a_muic_i2c_probe(struct i2c_client *i2c) +{ + struct device_node *np = i2c->dev.of_node; + struct rt8973a_muic_info *info; + int i, ret, irq_flags; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&i2c->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + i2c_set_clientdata(i2c, info); + + info->dev = &i2c->dev; + info->i2c = i2c; + info->irq = i2c->irq; + info->muic_irqs = rt8973a_muic_irqs; + info->num_muic_irqs = ARRAY_SIZE(rt8973a_muic_irqs); + info->reg_data = rt8973a_reg_data; + info->num_reg_data = ARRAY_SIZE(rt8973a_reg_data); + + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, rt8973a_muic_irq_work); + + info->regmap = devm_regmap_init_i2c(i2c, &rt8973a_muic_regmap_config); + if (IS_ERR(info->regmap)) { + ret = PTR_ERR(info->regmap); + dev_err(info->dev, "failed to allocate register map: %d\n", + ret); + return ret; + } + + /* Support irq domain for RT8973A MUIC device */ + irq_flags = IRQF_TRIGGER_FALLING | IRQF_ONESHOT | IRQF_SHARED; + ret = regmap_add_irq_chip(info->regmap, info->irq, irq_flags, 0, + &rt8973a_muic_irq_chip, &info->irq_data); + if (ret != 0) { + dev_err(info->dev, "failed to add irq_chip (irq:%d, err:%d)\n", + info->irq, ret); + return ret; + } + + for (i = 0; i < info->num_muic_irqs; i++) { + struct muic_irq *muic_irq = &info->muic_irqs[i]; + int virq = 0; + + virq = regmap_irq_get_virq(info->irq_data, muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(info->dev, virq, NULL, + rt8973a_muic_irq_handler, + IRQF_NO_SUSPEND | IRQF_ONESHOT, + muic_irq->name, info); + if (ret) { + dev_err(info->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Allocate extcon device */ + info->edev = devm_extcon_dev_allocate(info->dev, rt8973a_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(info->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + + /* Register extcon device */ + ret = devm_extcon_dev_register(info->dev, info->edev); + if (ret) { + dev_err(info->dev, "failed to register extcon device\n"); + return ret; + } + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, rt8973a_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + msecs_to_jiffies(DELAY_MS_DEFAULT)); + + /* Initialize RT8973A device and print vendor id and version id */ + rt8973a_init_dev_type(info); + + return 0; +} + +static void rt8973a_muic_i2c_remove(struct i2c_client *i2c) +{ + struct rt8973a_muic_info *info = i2c_get_clientdata(i2c); + + regmap_del_irq_chip(info->irq, info->irq_data); +} + +static const struct of_device_id rt8973a_dt_match[] = { + { .compatible = "richtek,rt8973a-muic" }, + { }, +}; +MODULE_DEVICE_TABLE(of, rt8973a_dt_match); + +#ifdef CONFIG_PM_SLEEP +static int rt8973a_muic_suspend(struct device *dev) +{ + struct i2c_client *i2c = to_i2c_client(dev); + struct rt8973a_muic_info *info = i2c_get_clientdata(i2c); + + enable_irq_wake(info->irq); + + return 0; +} + +static int rt8973a_muic_resume(struct device *dev) +{ + struct i2c_client *i2c = to_i2c_client(dev); + struct rt8973a_muic_info *info = i2c_get_clientdata(i2c); + + disable_irq_wake(info->irq); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(rt8973a_muic_pm_ops, + rt8973a_muic_suspend, rt8973a_muic_resume); + +static const struct i2c_device_id rt8973a_i2c_id[] = { + { "rt8973a", TYPE_RT8973A }, + { } +}; +MODULE_DEVICE_TABLE(i2c, rt8973a_i2c_id); + +static struct i2c_driver rt8973a_muic_i2c_driver = { + .driver = { + .name = "rt8973a", + .pm = &rt8973a_muic_pm_ops, + .of_match_table = rt8973a_dt_match, + }, + .probe = rt8973a_muic_i2c_probe, + .remove = rt8973a_muic_i2c_remove, + .id_table = rt8973a_i2c_id, +}; + +static int __init rt8973a_muic_i2c_init(void) +{ + return i2c_add_driver(&rt8973a_muic_i2c_driver); +} +subsys_initcall(rt8973a_muic_i2c_init); + +MODULE_DESCRIPTION("Richtek RT8973A Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-rt8973a.h b/drivers/extcon/extcon-rt8973a.h new file mode 100644 index 000000000..0d0d69fca --- /dev/null +++ b/drivers/extcon/extcon-rt8973a.h @@ -0,0 +1,199 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * rt8973a.h + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + */ + +#ifndef __LINUX_EXTCON_RT8973A_H +#define __LINUX_EXTCON_RT8973A_H + +enum rt8973a_types { + TYPE_RT8973A, +}; + +/* RT8973A registers */ +enum rt8973A_reg { + RT8973A_REG_DEVICE_ID = 0x1, + RT8973A_REG_CONTROL1, + RT8973A_REG_INT1, + RT8973A_REG_INT2, + RT8973A_REG_INTM1, + RT8973A_REG_INTM2, + RT8973A_REG_ADC, + RT8973A_REG_RSVD_1, + RT8973A_REG_RSVD_2, + RT8973A_REG_DEV1, + RT8973A_REG_DEV2, + RT8973A_REG_RSVD_3, + RT8973A_REG_RSVD_4, + RT8973A_REG_RSVD_5, + RT8973A_REG_RSVD_6, + RT8973A_REG_RSVD_7, + RT8973A_REG_RSVD_8, + RT8973A_REG_RSVD_9, + RT8973A_REG_MANUAL_SW1, + RT8973A_REG_MANUAL_SW2, + RT8973A_REG_RSVD_10, + RT8973A_REG_RSVD_11, + RT8973A_REG_RSVD_12, + RT8973A_REG_RSVD_13, + RT8973A_REG_RSVD_14, + RT8973A_REG_RSVD_15, + RT8973A_REG_RESET, + + RT8973A_REG_END, +}; + +/* Define RT8973A MASK/SHIFT constant */ +#define RT8973A_REG_DEVICE_ID_VENDOR_SHIFT 0 +#define RT8973A_REG_DEVICE_ID_VERSION_SHIFT 3 +#define RT8973A_REG_DEVICE_ID_VENDOR_MASK (0x7 << RT8973A_REG_DEVICE_ID_VENDOR_SHIFT) +#define RT8973A_REG_DEVICE_ID_VERSION_MASK (0x1f << RT8973A_REG_DEVICE_ID_VERSION_SHIFT) + +#define RT8973A_REG_CONTROL1_INTM_SHIFT 0 +#define RT8973A_REG_CONTROL1_AUTO_CONFIG_SHIFT 2 +#define RT8973A_REG_CONTROL1_I2C_RST_EN_SHIFT 3 +#define RT8973A_REG_CONTROL1_SWITCH_OPEN_SHIFT 4 +#define RT8973A_REG_CONTROL1_CHGTYP_SHIFT 5 +#define RT8973A_REG_CONTROL1_USB_CHD_EN_SHIFT 6 +#define RT8973A_REG_CONTROL1_ADC_EN_SHIFT 7 +#define RT8973A_REG_CONTROL1_INTM_MASK (0x1 << RT8973A_REG_CONTROL1_INTM_SHIFT) +#define RT8973A_REG_CONTROL1_AUTO_CONFIG_MASK (0x1 << RT8973A_REG_CONTROL1_AUTO_CONFIG_SHIFT) +#define RT8973A_REG_CONTROL1_I2C_RST_EN_MASK (0x1 << RT8973A_REG_CONTROL1_I2C_RST_EN_SHIFT) +#define RT8973A_REG_CONTROL1_SWITCH_OPEN_MASK (0x1 << RT8973A_REG_CONTROL1_SWITCH_OPEN_SHIFT) +#define RT8973A_REG_CONTROL1_CHGTYP_MASK (0x1 << RT8973A_REG_CONTROL1_CHGTYP_SHIFT) +#define RT8973A_REG_CONTROL1_USB_CHD_EN_MASK (0x1 << RT8973A_REG_CONTROL1_USB_CHD_EN_SHIFT) +#define RT8973A_REG_CONTROL1_ADC_EN_MASK (0x1 << RT8973A_REG_CONTROL1_ADC_EN_SHIFT) + +#define RT9873A_REG_INTM1_ATTACH_SHIFT 0 +#define RT9873A_REG_INTM1_DETACH_SHIFT 1 +#define RT9873A_REG_INTM1_CHGDET_SHIFT 2 +#define RT9873A_REG_INTM1_DCD_T_SHIFT 3 +#define RT9873A_REG_INTM1_OVP_SHIFT 4 +#define RT9873A_REG_INTM1_CONNECT_SHIFT 5 +#define RT9873A_REG_INTM1_ADC_CHG_SHIFT 6 +#define RT9873A_REG_INTM1_OTP_SHIFT 7 +#define RT9873A_REG_INTM1_ATTACH_MASK (0x1 << RT9873A_REG_INTM1_ATTACH_SHIFT) +#define RT9873A_REG_INTM1_DETACH_MASK (0x1 << RT9873A_REG_INTM1_DETACH_SHIFT) +#define RT9873A_REG_INTM1_CHGDET_MASK (0x1 << RT9873A_REG_INTM1_CHGDET_SHIFT) +#define RT9873A_REG_INTM1_DCD_T_MASK (0x1 << RT9873A_REG_INTM1_DCD_T_SHIFT) +#define RT9873A_REG_INTM1_OVP_MASK (0x1 << RT9873A_REG_INTM1_OVP_SHIFT) +#define RT9873A_REG_INTM1_CONNECT_MASK (0x1 << RT9873A_REG_INTM1_CONNECT_SHIFT) +#define RT9873A_REG_INTM1_ADC_CHG_MASK (0x1 << RT9873A_REG_INTM1_ADC_CHG_SHIFT) +#define RT9873A_REG_INTM1_OTP_MASK (0x1 << RT9873A_REG_INTM1_OTP_SHIFT) + +#define RT9873A_REG_INTM2_UVLO_SHIFT 1 +#define RT9873A_REG_INTM2_POR_SHIFT 2 +#define RT9873A_REG_INTM2_OTP_FET_SHIFT 3 +#define RT9873A_REG_INTM2_OVP_FET_SHIFT 4 +#define RT9873A_REG_INTM2_OCP_LATCH_SHIFT 5 +#define RT9873A_REG_INTM2_OCP_SHIFT 6 +#define RT9873A_REG_INTM2_OVP_OCP_SHIFT 7 +#define RT9873A_REG_INTM2_UVLO_MASK (0x1 << RT9873A_REG_INTM2_UVLO_SHIFT) +#define RT9873A_REG_INTM2_POR_MASK (0x1 << RT9873A_REG_INTM2_POR_SHIFT) +#define RT9873A_REG_INTM2_OTP_FET_MASK (0x1 << RT9873A_REG_INTM2_OTP_FET_SHIFT) +#define RT9873A_REG_INTM2_OVP_FET_MASK (0x1 << RT9873A_REG_INTM2_OVP_FET_SHIFT) +#define RT9873A_REG_INTM2_OCP_LATCH_MASK (0x1 << RT9873A_REG_INTM2_OCP_LATCH_SHIFT) +#define RT9873A_REG_INTM2_OCP_MASK (0x1 << RT9873A_REG_INTM2_OCP_SHIFT) +#define RT9873A_REG_INTM2_OVP_OCP_MASK (0x1 << RT9873A_REG_INTM2_OVP_OCP_SHIFT) + +#define RT8973A_REG_ADC_SHIFT 0 +#define RT8973A_REG_ADC_MASK (0x1f << RT8973A_REG_ADC_SHIFT) + +#define RT8973A_REG_DEV1_OTG_SHIFT 0 +#define RT8973A_REG_DEV1_SDP_SHIFT 2 +#define RT8973A_REG_DEV1_UART_SHIFT 3 +#define RT8973A_REG_DEV1_CAR_KIT_TYPE1_SHIFT 4 +#define RT8973A_REG_DEV1_CDPORT_SHIFT 5 +#define RT8973A_REG_DEV1_DCPORT_SHIFT 6 +#define RT8973A_REG_DEV1_OTG_MASK (0x1 << RT8973A_REG_DEV1_OTG_SHIFT) +#define RT8973A_REG_DEV1_SDP_MASK (0x1 << RT8973A_REG_DEV1_SDP_SHIFT) +#define RT8973A_REG_DEV1_UART_MASK (0x1 << RT8973A_REG_DEV1_UART_SHIFT) +#define RT8973A_REG_DEV1_CAR_KIT_TYPE1_MASK (0x1 << RT8973A_REG_DEV1_CAR_KIT_TYPE1_SHIFT) +#define RT8973A_REG_DEV1_CDPORT_MASK (0x1 << RT8973A_REG_DEV1_CDPORT_SHIFT) +#define RT8973A_REG_DEV1_DCPORT_MASK (0x1 << RT8973A_REG_DEV1_DCPORT_SHIFT) +#define RT8973A_REG_DEV1_USB_MASK (RT8973A_REG_DEV1_SDP_MASK \ + | RT8973A_REG_DEV1_CDPORT_MASK) + +#define RT8973A_REG_DEV2_JIG_USB_ON_SHIFT 0 +#define RT8973A_REG_DEV2_JIG_USB_OFF_SHIFT 1 +#define RT8973A_REG_DEV2_JIG_UART_ON_SHIFT 2 +#define RT8973A_REG_DEV2_JIG_UART_OFF_SHIFT 3 +#define RT8973A_REG_DEV2_JIG_USB_ON_MASK (0x1 << RT8973A_REG_DEV2_JIG_USB_ON_SHIFT) +#define RT8973A_REG_DEV2_JIG_USB_OFF_MASK (0x1 << RT8973A_REG_DEV2_JIG_USB_OFF_SHIFT) +#define RT8973A_REG_DEV2_JIG_UART_ON_MASK (0x1 << RT8973A_REG_DEV2_JIG_UART_ON_SHIFT) +#define RT8973A_REG_DEV2_JIG_UART_OFF_MASK (0x1 << RT8973A_REG_DEV2_JIG_UART_OFF_SHIFT) + +#define RT8973A_REG_MANUAL_SW1_DP_SHIFT 2 +#define RT8973A_REG_MANUAL_SW1_DM_SHIFT 5 +#define RT8973A_REG_MANUAL_SW1_DP_MASK (0x7 << RT8973A_REG_MANUAL_SW1_DP_SHIFT) +#define RT8973A_REG_MANUAL_SW1_DM_MASK (0x7 << RT8973A_REG_MANUAL_SW1_DM_SHIFT) +#define DM_DP_CON_SWITCH_OPEN 0x0 +#define DM_DP_CON_SWITCH_USB 0x1 +#define DM_DP_CON_SWITCH_UART 0x3 +#define DM_DP_SWITCH_OPEN ((DM_DP_CON_SWITCH_OPEN << RT8973A_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_OPEN << RT8973A_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_USB ((DM_DP_CON_SWITCH_USB << RT8973A_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_USB << RT8973A_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_UART ((DM_DP_CON_SWITCH_UART << RT8973A_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_UART << RT8973A_REG_MANUAL_SW1_DM_SHIFT)) + +#define RT8973A_REG_MANUAL_SW2_FET_ON_SHIFT 0 +#define RT8973A_REG_MANUAL_SW2_JIG_ON_SHIFT 2 +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_SHIFT 3 +#define RT8973A_REG_MANUAL_SW2_FET_ON_MASK (0x1 << RT8973A_REG_MANUAL_SW2_FET_ON_SHIFT) +#define RT8973A_REG_MANUAL_SW2_JIG_ON_MASK (0x1 << RT8973A_REG_MANUAL_SW2_JIG_ON_SHIFT) +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_MASK (0x1 << RT8973A_REG_MANUAL_SW2_BOOT_SW_SHIFT) +#define RT8973A_REG_MANUAL_SW2_FET_ON 0 +#define RT8973A_REG_MANUAL_SW2_FET_OFF 0x1 +#define RT8973A_REG_MANUAL_SW2_JIG_OFF 0 +#define RT8973A_REG_MANUAL_SW2_JIG_ON 0x1 +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_ON 0 +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_OFF 0x1 + +#define RT8973A_REG_RESET_SHIFT 0 +#define RT8973A_REG_RESET_MASK (0x1 << RT8973A_REG_RESET_SHIFT) +#define RT8973A_REG_RESET 0x1 + +/* RT8973A Interrupts */ +enum rt8973a_irq { + /* Interrupt1*/ + RT8973A_INT1_ATTACH, + RT8973A_INT1_DETACH, + RT8973A_INT1_CHGDET, + RT8973A_INT1_DCD_T, + RT8973A_INT1_OVP, + RT8973A_INT1_CONNECT, + RT8973A_INT1_ADC_CHG, + RT8973A_INT1_OTP, + + /* Interrupt2*/ + RT8973A_INT2_UVLO, + RT8973A_INT2_POR, + RT8973A_INT2_OTP_FET, + RT8973A_INT2_OVP_FET, + RT8973A_INT2_OCP_LATCH, + RT8973A_INT2_OCP, + RT8973A_INT2_OVP_OCP, + + RT8973A_NUM, +}; + +#define RT8973A_INT1_ATTACH_MASK BIT(0) +#define RT8973A_INT1_DETACH_MASK BIT(1) +#define RT8973A_INT1_CHGDET_MASK BIT(2) +#define RT8973A_INT1_DCD_T_MASK BIT(3) +#define RT8973A_INT1_OVP_MASK BIT(4) +#define RT8973A_INT1_CONNECT_MASK BIT(5) +#define RT8973A_INT1_ADC_CHG_MASK BIT(6) +#define RT8973A_INT1_OTP_MASK BIT(7) +#define RT8973A_INT2_UVLOT_MASK BIT(0) +#define RT8973A_INT2_POR_MASK BIT(1) +#define RT8973A_INT2_OTP_FET_MASK BIT(2) +#define RT8973A_INT2_OVP_FET_MASK BIT(3) +#define RT8973A_INT2_OCP_LATCH_MASK BIT(4) +#define RT8973A_INT2_OCP_MASK BIT(5) +#define RT8973A_INT2_OVP_OCP_MASK BIT(6) + +#endif /* __LINUX_EXTCON_RT8973A_H */ diff --git a/drivers/extcon/extcon-rtk-type-c.c b/drivers/extcon/extcon-rtk-type-c.c new file mode 100644 index 000000000..82b60b927 --- /dev/null +++ b/drivers/extcon/extcon-rtk-type-c.c @@ -0,0 +1,1795 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * * extcon-rtk-type-c.c - Realtek Extcon Type C driver + * + * Copyright (C) 2023 Realtek Semiconductor Corporation + * + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_irq.h> +#include <linux/io.h> +#include <linux/interrupt.h> +#include <linux/syscalls.h> +#include <linux/suspend.h> +#include <linux/debugfs.h> +#include <linux/extcon.h> +#include <linux/extcon-provider.h> +#include <linux/sys_soc.h> +#include <linux/nvmem-consumer.h> +#include <linux/gpio/consumer.h> +#include <linux/usb/otg.h> +#include <linux/usb/typec.h> + +struct cc_param { + u32 rp_4p7k_code; + u32 rp_36k_code; + u32 rp_12k_code; + u32 rd_code; + u32 ra_code; + u32 vref_2p6v; + u32 vref_1p23v; + u32 vref_0p8v; + u32 vref_0p66v; + u32 vref_0p4v; + u32 vref_0p2v; + u32 vref_1_1p6v; + u32 vref_0_1p6v; +}; + +struct type_c_cfg { + int parameter_ver; /* Parameter version */ + int cc_dfp_mode; + struct cc_param cc1_param; + struct cc_param cc2_param; + + u32 debounce_val; + bool use_defalut_parameter; +}; + +struct type_c_data { + void __iomem *reg_base; + struct device *dev; + struct extcon_dev *edev; + + u32 irq; + + /* rd control GPIO only for rtd1295 */ + struct gpio_desc *rd_ctrl_gpio_desc; + + /* Parameters */ + struct type_c_cfg *type_c_cfg; + u32 dfp_mode_rp_en; + u32 ufp_mode_rd_en; + u32 cc1_code; + u32 cc2_code; + u32 cc1_vref; + u32 cc2_vref; + u32 debounce; /* 1b,1us 7f,4.7us */ + + /* type_c state */ + int connect_change; +#define CONNECT_CHANGE 1 +#define CONNECT_NO_CHANGE 0 + int cc_mode; /* cc is host or device */ +#define IN_HOST_MODE 0x10 +#define IN_DEVICE_MODE 0x20 + int is_attach; +#define IN_ATTACH 1 +#define TO_ATTACH 1 +#define IN_DETACH 0 +#define TO_DETACH 0 + int at_cc1; +#define AT_CC1 1 +#define AT_CC2 0 + + u32 int_status; + u32 cc_status; + /* protect the data member */ + spinlock_t lock; + struct delayed_work delayed_work; + + bool rd_en_at_first; + + struct dentry *debug_dir; + + struct typec_port *port; +}; + +/* Type C register offset */ +#define USB_TYPEC_CTRL_CC1_0 0x0 +#define USB_TYPEC_CTRL_CC1_1 0x4 +#define USB_TYPEC_CTRL_CC2_0 0x8 +#define USB_TYPEC_CTRL_CC2_1 0xC +#define USB_TYPEC_STS 0x10 +#define USB_TYPEC_CTRL 0x14 +#define USB_DBUS_PWR_CTRL 0x18 + +#define ENABLE_CC1 0x1 +#define ENABLE_CC2 0x2 +#define DISABLE_CC 0x0 + +/* Bit mapping USB_TYPEC_CTRL_CC1_0 and USB_TYPEC_CTRL_CC2_0 */ +#define PLR_EN BIT(29) +#define CC_SWITCH_MASK (BIT(29) | BIT(28) | BIT(27)) +#define CC_CODE_MASK (0xfffff << 7) +#define rp4pk_code(val) ((0x1f & (val)) << 22) +#define code_rp4pk(val) (((val) >> 22) & 0x1f) +#define rp36k_code(val) ((0x1f & (val)) << 17) +#define code_rp36k(val) (((val) >> 17) & 0x1f) +#define rp12k_code(val) ((0x1f & (val)) << 12) +#define code_rp12k(val) (((val) >> 12) & 0x1f) +#define rd_code(val) ((0x1f & (val)) << 7) +#define code_rd(val) (((val) >> 7) & 0x1f) +#define dfp_mode(val) ((0x3 & (val)) << 5) +#define EN_RP4P7K BIT(4) +#define EN_RP36K BIT(3) +#define EN_RP12K BIT(2) +#define EN_RD BIT(1) +#define EN_CC_DET BIT(0) + +#define CC_MODE_UFP 0x0 +#define CC_MODE_DFP_USB 0x1 +#define CC_MODE_DFP_1_5 0x2 +#define CC_MODE_DFP_3_0 0x3 + +/* + * PARAMETER_V0: + * Realtek Kylin rtd1295 + * Realtek Hercules rtd1395 + * Realtek Thor rtd1619 + * Realtek Hank rtd1319 + * Realtek Groot rtd1312c + * PARAMETER_V1: + * Realtek Stark rtd1619b + * Realtek Parker rtd1319d + * Realtek Danvers rtd1315e + */ +enum parameter_version { + PARAMETER_V0 = 0, + PARAMETER_V1 = 1, +}; + +/* Bit mapping USB_TYPEC_CTRL_CC1_1 and USB_TYPEC_CTRL_CC2_1 */ +#define V0_vref_2p6v(val) ((0xf & (val)) << 26) /* Bit 29 for groot */ +#define V0_vref_1p23v(val) ((0xf & (val)) << 22) +#define V0_vref_0p8v(val) ((0xf & (val)) << 18) +#define V0_vref_0p66v(val) ((0xf & (val)) << 14) +#define V0_vref_0p4v(val) ((0x7 & (val)) << 11) +#define V0_vref_0p2v(val) ((0x7 & (val)) << 8) +#define V0_vref_1_1p6v(val) ((0xf & (val)) << 4) +#define V0_vref_0_1p6v(val) ((0xf & (val)) << 0) + +#define V0_decode_2p6v(val) (((val) >> 26) & 0xf) /* Bit 29 for groot */ +#define V0_decode_1p23v(val) (((val) >> 22) & 0xf) +#define V0_decode_0p8v(val) (((val) >> 18) & 0xf) +#define V0_decode_0p66v(val) (((val) >> 14) & 0xf) +#define V0_decode_0p4v(val) (((val) >> 11) & 0x7) +#define V0_decode_0p2v(val) (((val) >> 8) & 0x7) +#define V0_decode_1_1p6v(val) (((val) >> 4) & 0xf) +#define V0_decode_0_1p6v(val) (((val) >> 0) & 0xf) + +/* new Bit mapping USB_TYPEC_CTRL_CC1_1 and USB_TYPEC_CTRL_CC2_1 */ +#define V1_vref_2p6v(val) ((0xf & (val)) << 28) +#define V1_vref_1p23v(val) ((0xf & (val)) << 24) +#define V1_vref_0p8v(val) ((0xf & (val)) << 20) +#define V1_vref_0p66v(val) ((0xf & (val)) << 16) +#define V1_vref_0p4v(val) ((0xf & (val)) << 12) +#define V1_vref_0p2v(val) ((0xf & (val)) << 8) +#define V1_vref_1_1p6v(val) ((0xf & (val)) << 4) +#define V1_vref_0_1p6v(val) ((0xf & (val)) << 0) + +#define V1_decode_2p6v(val) (((val) >> 28) & 0xf) +#define V1_decode_1p23v(val) (((val) >> 24) & 0xf) +#define V1_decode_0p8v(val) (((val) >> 20) & 0xf) +#define V1_decode_0p66v(val) (((val) >> 16) & 0xf) +#define V1_decode_0p4v(val) (((val) >> 12) & 0xf) +#define V1_decode_0p2v(val) (((val) >> 8) & 0xf) +#define V1_decode_1_1p6v(val) (((val) >> 4) & 0xf) +#define V1_decode_0_1p6v(val) (((val) >> 0) & 0xf) + +/* Bit mapping USB_TYPEC_STS */ +#define DET_STS 0x7 +#define CC1_DET_STS (DET_STS) +#define CC2_DET_STS (DET_STS << 3) +#define DET_STS_RA 0x1 +#define DET_STS_RD 0x3 +#define DET_STS_RP 0x1 +#define CC1_DET_STS_RA (DET_STS_RA) +#define CC1_DET_STS_RD (DET_STS_RD) +#define CC1_DET_STS_RP (DET_STS_RP) +#define CC2_DET_STS_RA (DET_STS_RA << 3) +#define CC2_DET_STS_RD (DET_STS_RD << 3) +#define CC2_DET_STS_RP (DET_STS_RP << 3) + +/* Bit mapping USB_TYPEC_CTRL */ +#define CC2_INT_EN BIT(11) +#define CC1_INT_EN BIT(10) +#define CC2_INT_STS BIT(9) +#define CC1_INT_STS BIT(8) +#define DEBOUNCE_TIME_MASK 0xff +#define DEBOUNCE_EN BIT(0) +#define ENABLE_TYPE_C_DETECT (CC1_INT_EN | CC2_INT_EN) +#define ALL_CC_INT_STS (CC1_INT_STS | CC2_INT_STS) + +/* Parameter */ +#define DETECT_TIME 50 /* ms */ + +static const unsigned int usb_type_c_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +enum usb_data_roles { + DR_NONE, + DR_HOST, + DR_DEVICE, +}; + +static const struct soc_device_attribute rtk_soc_kylin[] = { + { .family = "Realtek Kylin", }, + { /* empty */ } +}; + +static int rtd129x_switch_type_c_plug_config(struct type_c_data *type_c, + int dr_mode, int cc) +{ + void __iomem *reg = type_c->reg_base + USB_TYPEC_CTRL_CC1_0; + int val_cc; + +#define TYPE_C_EN_SWITCH BIT(29) +#define TYPE_C_TXRX_SEL (BIT(28) | BIT(27)) +#define TYPE_C_SWITCH_MASK (TYPE_C_EN_SWITCH | TYPE_C_TXRX_SEL) +#define TYPE_C_ENABLE_CC1 TYPE_C_EN_SWITCH +#define TYPE_C_ENABLE_CC2 (TYPE_C_EN_SWITCH | TYPE_C_TXRX_SEL) +#define TYPE_C_DISABLE_CC ~TYPE_C_SWITCH_MASK + + val_cc = readl(reg); + val_cc &= ~TYPE_C_SWITCH_MASK; + + if (cc == DISABLE_CC) { + val_cc &= TYPE_C_DISABLE_CC; + } else if (cc == ENABLE_CC1) { + val_cc |= TYPE_C_ENABLE_CC1; + } else if (cc == ENABLE_CC2) { + val_cc |= TYPE_C_ENABLE_CC2; + } else { + dev_err(type_c->dev, "%s: Error cc setting cc=0x%x\n", __func__, cc); + return -EINVAL; + } + writel(val_cc, reg); + + /* waiting cc stable for enable/disable */ + mdelay(1); + + dev_dbg(type_c->dev, "%s: cc=0x%x val_cc=0x%x usb_typec_ctrl_cc1_0=0x%x\n", + __func__, cc, val_cc, readl(reg)); + + return 0; +} + +static inline void switch_type_c_plug_config(struct type_c_data *type_c, + int dr_mode, int cc) +{ + int ret = 0; + + if (soc_device_match(rtk_soc_kylin)) + ret = rtd129x_switch_type_c_plug_config(type_c, dr_mode, cc); + + if (ret < 0) + dev_err(type_c->dev, "%s: Error set type c plug config\n", + __func__); +} + +static void switch_type_c_dr_mode(struct type_c_data *type_c, int dr_mode, int cc) +{ + bool is_host = false; + bool is_device = false; + bool polarity = false; + bool vbus = false; + bool ss = true; + + switch_type_c_plug_config(type_c, dr_mode, cc); + if (cc == ENABLE_CC2) + polarity = true; + + switch (dr_mode) { + case USB_DR_MODE_HOST: + is_host = true; + break; + case USB_DR_MODE_PERIPHERAL: + is_device = true; + vbus = true; + break; + default: + dev_dbg(type_c->dev, "%s dr_mode=%d ==> no host or device\n", + __func__, dr_mode); + break; + } + + dev_dbg(type_c->dev, "%s is_host=%d is_device=%d vbus=%d polarity=%d\n", + __func__, is_host, is_device, vbus, polarity); + + /* for EXTCON_USB device mode */ + extcon_set_state(type_c->edev, EXTCON_USB, is_device); + extcon_set_property(type_c->edev, EXTCON_USB, + EXTCON_PROP_USB_VBUS, + (union extcon_property_value)(int)vbus); + extcon_set_property(type_c->edev, EXTCON_USB, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)(int)polarity); + extcon_set_property(type_c->edev, EXTCON_USB, + EXTCON_PROP_USB_SS, + (union extcon_property_value)(int)ss); + + /* for EXTCON_USB_HOST host mode */ + extcon_set_state(type_c->edev, EXTCON_USB_HOST, is_host); + extcon_set_property(type_c->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_VBUS, + (union extcon_property_value)(int)vbus); + extcon_set_property(type_c->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)(int)polarity); + extcon_set_property(type_c->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_SS, + (union extcon_property_value)(int)ss); + + /* sync EXTCON_USB and EXTCON_USB_HOST */ + extcon_sync(type_c->edev, EXTCON_USB); + extcon_sync(type_c->edev, EXTCON_USB_HOST); + + if (type_c->port) { + switch (dr_mode) { + case USB_DR_MODE_HOST: + typec_set_data_role(type_c->port, TYPEC_HOST); + typec_set_pwr_role(type_c->port, TYPEC_SOURCE); + break; + case USB_DR_MODE_PERIPHERAL: + typec_set_data_role(type_c->port, TYPEC_DEVICE); + typec_set_pwr_role(type_c->port, TYPEC_SINK); + break; + default: + dev_dbg(type_c->dev, "%s unknown dr_mode=%d\n", + __func__, dr_mode); + break; + } + } +} + +/* connector attached/detached */ +static int connector_attached(struct type_c_data *type_c, u32 cc, int dr_mode) +{ + void __iomem *reg = type_c->reg_base + USB_TYPEC_CTRL; + + cancel_delayed_work(&type_c->delayed_work); + + switch_type_c_dr_mode(type_c, dr_mode, cc); + + writel(ENABLE_TYPE_C_DETECT | readl(reg), reg); + + return 0; +} + +static int connector_detached(struct type_c_data *type_c, u32 cc, int dr_mode) +{ + void __iomem *reg = type_c->reg_base + USB_TYPEC_CTRL; + + writel(~ENABLE_TYPE_C_DETECT & readl(reg), reg); + + switch_type_c_dr_mode(type_c, 0, cc); + + schedule_delayed_work(&type_c->delayed_work, msecs_to_jiffies(DETECT_TIME)); + + return 0; +} + +/* detect host device switch */ +static int __detect_host_device(struct type_c_data *type_c, u32 rp_or_rd_en) +{ + struct device *dev = type_c->dev; + void __iomem *reg_base = type_c->reg_base; + u32 cc1_config, cc2_config, default_ctrl; + u32 cc1_switch = 0; + + default_ctrl = readl(reg_base + USB_TYPEC_CTRL) & DEBOUNCE_TIME_MASK; + writel(default_ctrl, reg_base + USB_TYPEC_CTRL); + + cc1_config = readl(reg_base + USB_TYPEC_CTRL_CC1_0); + cc2_config = readl(reg_base + USB_TYPEC_CTRL_CC2_0); + + cc1_config &= ~EN_CC_DET; + cc2_config &= ~EN_CC_DET; + writel(cc1_config, reg_base + USB_TYPEC_CTRL_CC1_0); + writel(cc2_config, reg_base + USB_TYPEC_CTRL_CC2_0); + + if (soc_device_match(rtk_soc_kylin)) + cc1_switch = cc1_config & CC_SWITCH_MASK; + + cc1_config &= CC_CODE_MASK; + cc1_config |= rp_or_rd_en | cc1_switch; + cc2_config &= CC_CODE_MASK; + cc2_config |= rp_or_rd_en; + writel(cc2_config, reg_base + USB_TYPEC_CTRL_CC2_0); + writel(cc1_config, reg_base + USB_TYPEC_CTRL_CC1_0); + + /* For kylin to disable external rd control gpio */ + if (soc_device_match(rtk_soc_kylin)) { + struct gpio_desc *gpio = type_c->rd_ctrl_gpio_desc; + + if (gpio && gpiod_direction_output(gpio, 1)) + dev_err(dev, "%s ERROR set rd_ctrl_gpio_desc fail\n", __func__); + } + + cc1_config |= EN_CC_DET; + cc2_config |= EN_CC_DET; + writel(cc1_config, reg_base + USB_TYPEC_CTRL_CC1_0); + writel(cc2_config, reg_base + USB_TYPEC_CTRL_CC2_0); + + return 0; +} + +static int detect_device(struct type_c_data *type_c) +{ + return __detect_host_device(type_c, type_c->dfp_mode_rp_en); +} + +static int detect_host(struct type_c_data *type_c) +{ + return __detect_host_device(type_c, type_c->ufp_mode_rd_en); +} + +static int host_device_switch_detection(struct type_c_data *type_c) +{ + if (type_c->cc_mode == IN_HOST_MODE) { + type_c->cc_mode = IN_DEVICE_MODE; + detect_host(type_c); + } else { + type_c->cc_mode = IN_HOST_MODE; + detect_device(type_c); + } + + return 0; +} + +static int detect_type_c_state(struct type_c_data *type_c) +{ + struct device *dev = type_c->dev; + void __iomem *reg_base = type_c->reg_base; + u32 int_status, cc_status, cc_status_check; + unsigned long flags; + + spin_lock_irqsave(&type_c->lock, flags); + + int_status = readl(reg_base + USB_TYPEC_CTRL); + cc_status = readl(reg_base + USB_TYPEC_STS); + + type_c->connect_change = CONNECT_NO_CHANGE; + + switch (type_c->cc_mode | type_c->is_attach) { + case IN_HOST_MODE | IN_ATTACH: + if (((cc_status & CC1_DET_STS) == CC1_DET_STS) && type_c->at_cc1 == AT_CC1) { + dev_dbg(dev, "IN host mode and cc1 device detach (cc_status=0x%x)", + cc_status); + type_c->is_attach = TO_DETACH; + type_c->connect_change = CONNECT_CHANGE; + } else if (((cc_status & CC2_DET_STS) == CC2_DET_STS) && + type_c->at_cc1 == AT_CC2) { + dev_dbg(dev, "IN host mode and cc2 device detach (cc_status=0x%x)", + cc_status); + type_c->is_attach = TO_DETACH; + type_c->connect_change = CONNECT_CHANGE; + } + break; + case IN_HOST_MODE | IN_DETACH: + cc_status_check = readl(reg_base + USB_TYPEC_STS); + if (cc_status_check != (CC1_DET_STS | CC2_DET_STS)) { + if (in_interrupt()) { + /* Add delay time to avoid capacitive effect of cable. */ + mdelay(300); + } else { + spin_unlock_irqrestore(&type_c->lock, flags); + /* Add delay time to avoid capacitive effect of cable. */ + msleep(300); + spin_lock_irqsave(&type_c->lock, flags); + } + cc_status_check = readl(reg_base + USB_TYPEC_STS); + } + if (cc_status != cc_status_check) { + dev_warn(dev, "IN_HOST_MODE: cc_status (0x%x) != cc_status_check (0x%x)\n", + cc_status, cc_status_check); + cc_status = readl(reg_base + USB_TYPEC_STS); + } + + if ((cc_status & CC1_DET_STS) == CC1_DET_STS_RD) { + dev_dbg(dev, "IN host mode and cc1 device attach (cc_status=0x%x)", + cc_status); + type_c->is_attach = TO_ATTACH; + type_c->at_cc1 = AT_CC1; + type_c->connect_change = CONNECT_CHANGE; + } else if ((cc_status & CC2_DET_STS) == CC2_DET_STS_RD) { + dev_dbg(dev, "In host mode and cc2 device attach (cc_status=0x%x)", + cc_status); + type_c->is_attach = TO_ATTACH; + type_c->at_cc1 = AT_CC2; + type_c->connect_change = CONNECT_CHANGE; + } + break; + case IN_DEVICE_MODE | IN_ATTACH: + if ((cc_status & CC1_DET_STS) < CC1_DET_STS_RP || + (cc_status & CC2_DET_STS) < CC2_DET_STS_RP) { + /* Add a sw debounce to filter cc signal sent from apple pd adapter */ + mdelay(5); + cc_status_check = readl(reg_base + USB_TYPEC_STS); + + if (cc_status != cc_status_check) { + dev_dbg(dev, "IN_DEVICE_MODE: cc_status (0x%x) != cc_status_check (0x%x) maybe use a pd adapter\n", + cc_status, cc_status_check); + cc_status = cc_status_check; + } + } + + if ((cc_status & CC1_DET_STS) < CC1_DET_STS_RP && type_c->at_cc1 == AT_CC1) { + dev_dbg(dev, "IN device mode and cc1 host disconnect (cc_status=0x%x)", + cc_status); + type_c->is_attach = TO_DETACH; + type_c->connect_change = CONNECT_CHANGE; + } else if ((cc_status & CC2_DET_STS) < CC2_DET_STS_RP && + type_c->at_cc1 == AT_CC2) { + dev_dbg(dev, "IN device mode and cc2 host disconnect (cc_status=0x%x)", + cc_status); + type_c->is_attach = TO_DETACH; + type_c->connect_change = CONNECT_CHANGE; + } + break; + case IN_DEVICE_MODE | IN_DETACH: + cc_status_check = readl(reg_base + USB_TYPEC_STS); + if (cc_status_check != 0x0) { + if (in_interrupt()) { + /* Add delay time to avoid capacitive effect of cable. */ + mdelay(300); + } else { + spin_unlock_irqrestore(&type_c->lock, flags); + /* Add delay time to avoid capacitive effect of cable. */ + msleep(300); + spin_lock_irqsave(&type_c->lock, flags); + } + cc_status_check = readl(reg_base + USB_TYPEC_STS); + } + + if (cc_status != cc_status_check) { + dev_warn(dev, "IN_DEVICE_MODE: cc_status (0x%x) != cc_status_check (0x%x)\n", + cc_status, cc_status_check); + cc_status = readl(reg_base + USB_TYPEC_STS); + } + + if ((cc_status & CC1_DET_STS) >= CC1_DET_STS_RP) { + dev_dbg(dev, "IN device mode and cc1 host connect (cc_status=0x%x)", + cc_status); + type_c->at_cc1 = AT_CC1; + type_c->is_attach = TO_ATTACH; + type_c->connect_change = CONNECT_CHANGE; + } else if ((cc_status & CC2_DET_STS) >= CC2_DET_STS_RP) { + dev_dbg(dev, "IN device mode and cc2 host connect (cc_status=0x%x)", + cc_status); + type_c->at_cc1 = AT_CC2; + type_c->is_attach = TO_ATTACH; + type_c->connect_change = CONNECT_CHANGE; + } + break; + default: + dev_err(dev, "error host or device mode (cc_mode=%d, is_attach=%d) ", + type_c->cc_mode, type_c->is_attach); + } + + type_c->int_status = int_status; + type_c->cc_status = cc_status; + + spin_unlock_irqrestore(&type_c->lock, flags); + return 0; +} + +static void host_device_switch(struct work_struct *work) +{ + struct type_c_data *type_c = container_of(work, struct type_c_data, + delayed_work.work); + struct device *dev = type_c->dev; + unsigned long flags; + int connect_change = 0; + int cc_mode = 0; + int is_attach = 0; + int at_cc1 = 0; + + spin_lock_irqsave(&type_c->lock, flags); + if (type_c->connect_change) + connect_change = type_c->connect_change; + spin_unlock_irqrestore(&type_c->lock, flags); + + if (!connect_change) + detect_type_c_state(type_c); + + spin_lock_irqsave(&type_c->lock, flags); + if (type_c->connect_change) { + connect_change = type_c->connect_change; + cc_mode = type_c->cc_mode; + is_attach = type_c->is_attach; + at_cc1 = type_c->at_cc1; + type_c->connect_change = CONNECT_NO_CHANGE; + } else { + host_device_switch_detection(type_c); + + schedule_delayed_work(&type_c->delayed_work, msecs_to_jiffies(DETECT_TIME)); + } + spin_unlock_irqrestore(&type_c->lock, flags); + + if (!connect_change) + return; + + dev_dbg(dev, "%s: usb cable connection change\n", __func__); + if (cc_mode == IN_HOST_MODE) { + if (is_attach && at_cc1) + connector_attached(type_c, ENABLE_CC1, USB_DR_MODE_HOST); + else if (is_attach && !at_cc1) + connector_attached(type_c, ENABLE_CC2, USB_DR_MODE_HOST); + else + connector_detached(type_c, DISABLE_CC, USB_DR_MODE_HOST); + } else if (cc_mode == IN_DEVICE_MODE) { + if (is_attach && at_cc1) + connector_attached(type_c, ENABLE_CC1, USB_DR_MODE_PERIPHERAL); + else if (is_attach && !at_cc1) + connector_attached(type_c, ENABLE_CC2, USB_DR_MODE_PERIPHERAL); + else + connector_detached(type_c, DISABLE_CC, USB_DR_MODE_PERIPHERAL); + } else { + dev_err(dev, "Error: IN unknown mode %d to %s at %s (cc_status=0x%x)\n", + cc_mode, is_attach ? "attach" : "detach", + at_cc1 ? "cc1" : "cc2", type_c->cc_status); + } + dev_info(dev, "Connection change OK: IN %s mode to %s at %s (cc_status=0x%x)\n", + cc_mode == IN_HOST_MODE ? "host" : "device", + is_attach ? "attach" : "detach", + at_cc1 ? "cc1" : "cc2", type_c->cc_status); +} + +static irqreturn_t type_c_detect_irq(int irq, void *__data) +{ + struct type_c_data *type_c = (struct type_c_data *)__data; + struct device *dev = type_c->dev; + void __iomem *reg = type_c->reg_base + USB_TYPEC_CTRL; + unsigned long flags; + + detect_type_c_state(type_c); + + spin_lock_irqsave(&type_c->lock, flags); + + if (type_c->connect_change) { + dev_dbg(dev, "%s: IN %s mode to %s (at %s interrupt) int_status=0x%x, cc_status=0x%x", + __func__, + type_c->cc_mode == IN_HOST_MODE ? "host" : "device", + type_c->is_attach ? "attach" : "detach", + type_c->at_cc1 ? "cc1" : "cc2", + type_c->int_status, type_c->cc_status); + + /* clear interrupt status */ + writel(~ALL_CC_INT_STS & readl(reg), reg); + + cancel_delayed_work(&type_c->delayed_work); + schedule_delayed_work(&type_c->delayed_work, msecs_to_jiffies(0)); + } else { + static int local_count; + + /* if no connect_change, we keep the status to avoid status lose */ + if (local_count++ > 10) { + /* clear interrupt status */ + writel(~ALL_CC_INT_STS & readl(reg), reg); + local_count = 0; + } + } + + spin_unlock_irqrestore(&type_c->lock, flags); + + return IRQ_HANDLED; +} + +static int type_c_port_dr_set(struct typec_port *port, + enum typec_data_role role) +{ + struct type_c_data *type_c = typec_get_drvdata(port); + u32 enable_cc; + unsigned long flags; + + spin_lock_irqsave(&type_c->lock, flags); + enable_cc = type_c->at_cc1 ? ENABLE_CC1 : ENABLE_CC2; + spin_unlock_irqrestore(&type_c->lock, flags); + + if (role == TYPEC_HOST) + switch_type_c_dr_mode(type_c, USB_DR_MODE_HOST, enable_cc); + else if (role == TYPEC_DEVICE) + switch_type_c_dr_mode(type_c, USB_DR_MODE_PERIPHERAL, enable_cc); + else + switch_type_c_dr_mode(type_c, 0, DISABLE_CC); + + return 0; +} + +static const struct typec_operations type_c_port_ops = { + .dr_set = type_c_port_dr_set, +}; + +#ifdef CONFIG_DEBUG_FS +static int type_c_parameter_show(struct seq_file *s, void *unused) +{ + struct type_c_data *type_c = s->private; + struct type_c_cfg *type_c_cfg = type_c->type_c_cfg; + struct cc_param *cc_param; + unsigned long flags; + + spin_lock_irqsave(&type_c->lock, flags); + + seq_printf(s, "cc_dfp_mode %s\n", + ({ char *tmp; + switch (type_c_cfg->cc_dfp_mode) { + case CC_MODE_DFP_USB: + tmp = "CC_MODE_DFP_USB"; break; + case CC_MODE_DFP_1_5: + tmp = "CC_MODE_DFP_1_5"; break; + case CC_MODE_DFP_3_0: + tmp = "CC_MODE_DFP_3_0"; break; + default: + tmp = "?"; break; + } tmp; })); + + seq_printf(s, "dfp_mode_rp_en 0x%x\n", type_c->dfp_mode_rp_en); + seq_printf(s, "ufp_mode_rd_en 0x%x\n", type_c->ufp_mode_rd_en); + seq_printf(s, "cc1_code 0x%x\n", type_c->cc1_code); + seq_printf(s, "cc2_code 0x%x\n", type_c->cc2_code); + seq_printf(s, "cc1_vref 0x%x\n", type_c->cc1_vref); + seq_printf(s, "cc2_vref 0x%x\n", type_c->cc2_vref); + seq_printf(s, "debounce 0x%x\n", type_c->debounce); + seq_puts(s, "\n"); + + cc_param = &type_c_cfg->cc1_param; + seq_puts(s, "cc1_param:\n"); + seq_printf(s, " rp_4p7k_code 0x%x\n", cc_param->rp_4p7k_code); + seq_printf(s, " rp_36k_code 0x%x\n", cc_param->rp_36k_code); + seq_printf(s, " rp_12k_code 0x%x\n", cc_param->rp_12k_code); + seq_printf(s, " rd_code 0x%x\n", cc_param->rd_code); + seq_printf(s, " vref_2p6v 0x%x\n", cc_param->vref_2p6v); + seq_printf(s, " vref_1p23v 0x%x\n", cc_param->vref_1p23v); + seq_printf(s, " vref_0p8v 0x%x\n", cc_param->vref_0p8v); + seq_printf(s, " vref_0p66v 0x%x\n", cc_param->vref_0p66v); + seq_printf(s, " vref_0p4v 0x%x\n", cc_param->vref_0p4v); + seq_printf(s, " vref_0p2v 0x%x\n", cc_param->vref_0p2v); + seq_printf(s, " vref_1_1p6v 0x%x\n", cc_param->vref_1_1p6v); + seq_printf(s, " vref_0_1p6v 0x%x\n", cc_param->vref_0_1p6v); + + cc_param = &type_c_cfg->cc2_param; + seq_puts(s, "cc2_param:\n"); + seq_printf(s, " rp_4p7k_code 0x%x\n", cc_param->rp_4p7k_code); + seq_printf(s, " rp_36k_code 0x%x\n", cc_param->rp_36k_code); + seq_printf(s, " rp_12k_code 0x%x\n", cc_param->rp_12k_code); + seq_printf(s, " rd_code 0x%x\n", cc_param->rd_code); + seq_printf(s, " vref_2p6v 0x%x\n", cc_param->vref_2p6v); + seq_printf(s, " vref_1p23v 0x%x\n", cc_param->vref_1p23v); + seq_printf(s, " vref_0p8v 0x%x\n", cc_param->vref_0p8v); + seq_printf(s, " vref_0p66v 0x%x\n", cc_param->vref_0p66v); + seq_printf(s, " vref_0p4v 0x%x\n", cc_param->vref_0p4v); + seq_printf(s, " vref_0p2v 0x%x\n", cc_param->vref_0p2v); + seq_printf(s, " vref_1_1p6v 0x%x\n", cc_param->vref_1_1p6v); + seq_printf(s, " vref_0_1p6v 0x%x\n", cc_param->vref_0_1p6v); + + spin_unlock_irqrestore(&type_c->lock, flags); + + return 0; +} + +static int type_c_parameter_open(struct inode *inode, struct file *file) +{ + return single_open(file, type_c_parameter_show, inode->i_private); +} + +static const struct file_operations type_c_parameter_fops = { + .open = type_c_parameter_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int type_c_status_show(struct seq_file *s, void *unused) +{ + struct type_c_data *type_c = s->private; + unsigned long flags; + + spin_lock_irqsave(&type_c->lock, flags); + + seq_printf(s, "In %s mode %s at %s (cc_status=0x%x)\n", + type_c->cc_mode == IN_HOST_MODE ? "host" : "device", + type_c->is_attach ? "attach" : "detach", + type_c->at_cc1 ? "cc1" : "cc2", type_c->cc_status); + + seq_printf(s, "Read Register (type_c_ctrl_cc1_0=0x%x)\n", + readl(type_c->reg_base + 0x0)); + seq_printf(s, "Read Register (type_c_ctrl_cc1_1=0x%x)\n", + readl(type_c->reg_base + 0x4)); + seq_printf(s, "Read Register (type_c_ctrl_cc2_0=0x%x)\n", + readl(type_c->reg_base + 0x8)); + seq_printf(s, "Read Register (type_c_ctrl_cc2_1=0x%x)\n", + readl(type_c->reg_base + 0xc)); + seq_printf(s, "Read Register (type_c_status=0x%x)\n", + readl(type_c->reg_base + 0x10)); + seq_printf(s, "Read Register (type_c_ctrl=0x%x)\n", + readl(type_c->reg_base + 0x14)); + + spin_unlock_irqrestore(&type_c->lock, flags); + + return 0; +} + +static int type_c_status_open(struct inode *inode, struct file *file) +{ + return single_open(file, type_c_status_show, inode->i_private); +} + +static const struct file_operations type_c_status_fops = { + .open = type_c_status_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static inline void create_debug_files(struct type_c_data *type_c) +{ + type_c->debug_dir = debugfs_create_dir("type_c", usb_debug_root); + + debugfs_create_file("parameter", 0444, type_c->debug_dir, type_c, + &type_c_parameter_fops); + + debugfs_create_file("status", 0444, type_c->debug_dir, type_c, + &type_c_status_fops); +} + +static inline void remove_debug_files(struct type_c_data *type_c) +{ + debugfs_remove_recursive(type_c->debug_dir); +} +#else +static inline void create_debug_files(struct type_c_data *type_c) { } +static inline void remove_debug_files(struct type_c_data *type_c) { } +#endif /* CONFIG_DEBUG_FS */ + +/* Init and probe */ + +static inline s8 get_value(s8 value) +{ + return (((s8)value & 0x8) ? (-(s8)(0x7 & value)) : ((s8)(value))); +} + +static int __updated_type_c_parameter_by_efuse(struct type_c_data *type_c) +{ + struct type_c_cfg *type_c_cfg = type_c->type_c_cfg; + struct cc_param *cc_param; + struct nvmem_cell *cell; + s8 cc1_4p7k = 0; + s8 cc1_12k = 0; + s8 cc1_0p2v = 0; + s8 cc1_0p8v = 0; + s8 cc1_2p6v = 0; + s8 cc1_0p66v = 0; + s8 cc1_1p23v = 0; + s8 cc2_4p7k = 0; + s8 cc2_12k = 0; + s8 cc2_0p2v = 0; + s8 cc2_0p8v = 0; + s8 cc2_2p6v = 0; + s8 cc2_0p66v = 0; + s8 cc2_1p23v = 0; + + cell = nvmem_cell_get(type_c->dev, "usb-cal"); + if (IS_ERR(cell)) { + dev_warn(type_c->dev, "%s failed to get usb-cal: %ld\n", + __func__, PTR_ERR(cell)); + } else { + unsigned char *buf; + size_t buf_size; + int value_size = 4; + int value_mask = (BIT(value_size) - 1); + + buf = nvmem_cell_read(cell, &buf_size); + if (!IS_ERR(buf)) { + cc1_0p2v = get_value((buf[0] >> value_size * 0) & value_mask); + cc1_0p8v = get_value((buf[0] >> value_size * 1) & value_mask); + cc1_2p6v = get_value((buf[1] >> value_size * 0) & value_mask); + cc1_0p66v = get_value((buf[1] >> value_size * 1) & value_mask); + cc1_1p23v = get_value((buf[2] >> value_size * 0) & value_mask); + + cc2_0p2v = get_value((buf[3] >> value_size * 0) & value_mask); + cc2_0p8v = get_value((buf[3] >> value_size * 1) & value_mask); + cc2_2p6v = get_value((buf[4] >> value_size * 0) & value_mask); + cc2_0p66v = get_value((buf[4] >> value_size * 1) & value_mask); + cc2_1p23v = get_value((buf[5] >> value_size * 0) & value_mask); + + cc1_4p7k = get_value((buf[6] >> value_size * 0) & value_mask); + cc1_12k = get_value((buf[6] >> value_size * 1) & value_mask); + cc2_4p7k = get_value((buf[7] >> value_size * 0) & value_mask); + cc2_12k = get_value((buf[7] >> value_size * 1) & value_mask); + + kfree(buf); + } + nvmem_cell_put(cell); + } + + dev_dbg(type_c->dev, "check efuse cc1_4p7k=%d cc1_12k=%d cc2_4p7k=%d cc2_12k=%d\n", + cc1_4p7k, cc1_12k, cc2_4p7k, cc2_12k); + dev_dbg(type_c->dev, "check efuse cc1_0p2v=%d cc1_0p8v=%d cc1_2p6v=%d cc1_0p66v=%d cc1_1p23v=%d\n", + cc1_0p2v, cc1_0p8v, cc1_2p6v, cc1_0p66v, cc1_1p23v); + dev_dbg(type_c->dev, "check efuse cc2_0p2v=%d cc2_0p8v=%d cc2_2p6v=%d cc2_0p66v=%d cc2_1p23v=%d\n", + cc2_0p2v, cc2_0p8v, cc2_2p6v, cc2_0p66v, cc2_1p23v); + + cc_param = &type_c_cfg->cc1_param; + cc_param->rp_4p7k_code = cc_param->rp_4p7k_code + cc1_4p7k; + cc_param->rp_12k_code = cc_param->rp_12k_code + cc1_12k; + + cc_param->vref_1p23v = cc_param->vref_1p23v + cc1_1p23v; + cc_param->vref_0p66v = cc_param->vref_0p66v + cc1_0p66v; + cc_param->vref_2p6v = cc_param->vref_2p6v + cc1_2p6v; + cc_param->vref_0p8v = cc_param->vref_0p8v + cc1_0p8v; + cc_param->vref_0p2v = cc_param->vref_0p2v + cc1_0p2v; + + cc_param = &type_c_cfg->cc2_param; + cc_param->rp_4p7k_code = cc_param->rp_4p7k_code + cc2_4p7k; + cc_param->rp_12k_code = cc_param->rp_12k_code + cc2_12k; + + cc_param->vref_1p23v = cc_param->vref_1p23v + cc2_1p23v; + cc_param->vref_0p66v = cc_param->vref_0p66v + cc2_0p66v; + cc_param->vref_2p6v = cc_param->vref_2p6v + cc2_2p6v; + cc_param->vref_0p8v = cc_param->vref_0p8v + cc2_0p8v; + cc_param->vref_0p2v = cc_param->vref_0p2v + cc2_0p2v; + + return 0; +} + +static int __updated_type_c_parameter_by_efuse_v2(struct type_c_data *type_c) +{ + struct type_c_cfg *type_c_cfg = type_c->type_c_cfg; + struct cc_param *cc_param; + struct nvmem_cell *cell; + s8 cc1_4p7k = 0; + s8 cc1_12k = 0; + s8 cc1_0p2v = 0; + s8 cc1_0p8v = 0; + s8 cc1_2p6v = 0; + s8 cc1_0p66v = 0; + s8 cc1_1p23v = 0; + s8 cc2_4p7k = 0; + s8 cc2_12k = 0; + s8 cc2_0p2v = 0; + s8 cc2_0p8v = 0; + s8 cc2_2p6v = 0; + s8 cc2_0p66v = 0; + s8 cc2_1p23v = 0; + + cell = nvmem_cell_get(type_c->dev, "usb-type-c-cal"); + if (IS_ERR(cell)) { + dev_warn(type_c->dev, "%s failed to get usb-type-c-cal: %ld\n", + __func__, PTR_ERR(cell)); + } else { + unsigned char *buf; + size_t buf_size; + int value_size = 0; + int value_mask = (BIT(value_size) - 1); + + buf = nvmem_cell_read(cell, &buf_size); + if (!IS_ERR(buf)) { + value_size = 5; + value_mask = (BIT(value_size) - 1); + cc1_4p7k = buf[0] & value_mask; + cc1_12k = buf[1] & value_mask; + cc2_4p7k = buf[2] & value_mask; + cc2_12k = buf[3] & value_mask; + + value_size = 4; + value_mask = (BIT(value_size) - 1); + cc1_0p2v = (buf[4] >> value_size * 0) & value_mask; + cc1_0p66v = (buf[4] >> value_size * 1) & value_mask; + cc1_0p8v = (buf[5] >> value_size * 0) & value_mask; + cc1_1p23v = (buf[5] >> value_size * 1) & value_mask; + cc1_2p6v = (buf[6] >> value_size * 0) & value_mask; + + cc2_0p2v = (buf[6] >> value_size * 1) & value_mask; + cc2_0p66v = (buf[7] >> value_size * 0) & value_mask; + cc2_0p8v = (buf[7] >> value_size * 1) & value_mask; + cc2_1p23v = (buf[8] >> value_size * 0) & value_mask; + cc2_2p6v = (buf[8] >> value_size * 1) & value_mask; + + kfree(buf); + } + nvmem_cell_put(cell); + } + + dev_dbg(type_c->dev, "check efuse v2 cc1_4p7k=%d cc1_12k=%d cc2_4p7k=%d cc2_12k=%d\n", + cc1_4p7k, cc1_12k, cc2_4p7k, cc2_12k); + dev_dbg(type_c->dev, "check efuse v2 cc1_0p2v=%d cc1_0p8v=%d cc1_2p6v=%d cc1_0p66v=%d cc1_1p23v=%d\n", + cc1_0p2v, cc1_0p8v, cc1_2p6v, cc1_0p66v, cc1_1p23v); + dev_dbg(type_c->dev, "check efuse v2 cc2_0p2v=%d cc2_0p8v=%d cc2_2p6v=%d cc2_0p66v=%d cc2_1p23v=%d\n", + cc2_0p2v, cc2_0p8v, cc2_2p6v, cc2_0p66v, cc2_1p23v); + + cc_param = &type_c_cfg->cc1_param; + if (cc1_4p7k) + cc_param->rp_4p7k_code = cc1_4p7k; + if (cc1_12k) + cc_param->rp_12k_code = cc1_12k; + + if (cc1_1p23v) + cc_param->vref_1p23v = cc1_1p23v; + if (cc1_0p66v) + cc_param->vref_0p66v = cc1_0p66v; + if (cc1_2p6v) + cc_param->vref_2p6v = cc1_2p6v; + if (cc1_0p8v) + cc_param->vref_0p8v = cc1_0p8v; + if (cc1_0p2v) + cc_param->vref_0p2v = cc1_0p2v; + + cc_param = &type_c_cfg->cc2_param; + if (cc2_4p7k) + cc_param->rp_4p7k_code = cc2_4p7k; + if (cc2_12k) + cc_param->rp_12k_code = cc2_12k; + + if (cc2_1p23v) + cc_param->vref_1p23v = cc2_1p23v; + if (cc2_0p66v) + cc_param->vref_0p66v = cc2_0p66v; + if (cc2_2p6v) + cc_param->vref_2p6v = cc2_2p6v; + if (cc2_0p8v) + cc_param->vref_0p8v = cc2_0p8v; + if (cc2_0p2v) + cc_param->vref_0p2v = cc2_0p2v; + + return 0; +} + +static void get_default_type_c_parameter(struct type_c_data *type_c) +{ + void __iomem *reg; + int val; + + type_c->dfp_mode_rp_en = dfp_mode(CC_MODE_DFP_3_0) | EN_RP4P7K; + type_c->ufp_mode_rd_en = EN_RD; + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC1_0; + val = readl(reg); + type_c->cc1_code = CC_CODE_MASK & val; + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC2_0; + val = readl(reg); + type_c->cc2_code = CC_CODE_MASK & val; + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC1_1; + val = readl(reg); + type_c->cc1_vref = val; + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC2_1; + val = readl(reg); + type_c->cc2_vref = val; + + reg = type_c->reg_base + USB_TYPEC_CTRL; + val = readl(reg); + type_c->debounce = DEBOUNCE_TIME_MASK & val; +} + +static int setup_type_c_parameter(struct type_c_data *type_c) +{ + struct type_c_cfg *type_c_cfg = type_c->type_c_cfg; + struct cc_param *cc_param; + struct soc_device_attribute rtk_soc_efuse_v1[] = { + { .family = "Realtek Phoenix",}, + { .family = "Realtek Kylin",}, + { .family = "Realtek Hercules",}, + { .family = "Realtek Thor",}, + { .family = "Realtek Hank",}, + { .family = "Realtek Groot",}, + { .family = "Realtek Stark",}, + { .family = "Realtek Parker",}, + { /* empty */ } + }; + + if (type_c_cfg->use_defalut_parameter) { + get_default_type_c_parameter(type_c); + return 0; + } + + if (soc_device_match(rtk_soc_efuse_v1)) + __updated_type_c_parameter_by_efuse(type_c); + else + __updated_type_c_parameter_by_efuse_v2(type_c); + + /* + * UFP rd vref_ufp : 1p23v, 0p66v, 0p2v + * DFP_USB rp36k vref_dfp_usb: 0_1p6v, 0p2v, unused + * DFP_1.5 rp12k vref_dfp_1_5: 1_1p6v, 0p4v, 0p2v + * DFP_3.0 rp4p7k vref_dfp_3_0: 2p6v, 0p8v, 0p2v + */ + + switch (type_c_cfg->cc_dfp_mode) { + case CC_MODE_DFP_USB: + type_c->dfp_mode_rp_en = dfp_mode(CC_MODE_DFP_USB) | EN_RP36K; + break; + case CC_MODE_DFP_1_5: + type_c->dfp_mode_rp_en = dfp_mode(CC_MODE_DFP_1_5) | EN_RP12K; + break; + case CC_MODE_DFP_3_0: + type_c->dfp_mode_rp_en = dfp_mode(CC_MODE_DFP_3_0) | EN_RP4P7K; + break; + default: + dev_err(type_c->dev, "%s: unknown cc_dfp_mode %d\n", + __func__, type_c_cfg->cc_dfp_mode); + } + + type_c->ufp_mode_rd_en = EN_RD; + + cc_param = &type_c_cfg->cc1_param; + type_c->cc1_code = rp4pk_code(cc_param->rp_4p7k_code) | + rp36k_code(cc_param->rp_36k_code) | + rp12k_code(cc_param->rp_12k_code) | + rd_code(cc_param->rd_code); + + if (type_c_cfg->parameter_ver == PARAMETER_V0) + type_c->cc1_vref = V0_vref_2p6v(cc_param->vref_2p6v) | + V0_vref_1p23v(cc_param->vref_1p23v) | + V0_vref_0p8v(cc_param->vref_0p8v) | + V0_vref_0p66v(cc_param->vref_0p66v) | + V0_vref_0p4v(cc_param->vref_0p4v) | + V0_vref_0p2v(cc_param->vref_0p2v) | + V0_vref_1_1p6v(cc_param->vref_1_1p6v) | + V0_vref_0_1p6v(cc_param->vref_0_1p6v); + else if (type_c_cfg->parameter_ver == PARAMETER_V1) + type_c->cc1_vref = V1_vref_2p6v(cc_param->vref_2p6v) | + V1_vref_1p23v(cc_param->vref_1p23v) | + V1_vref_0p8v(cc_param->vref_0p8v) | + V1_vref_0p66v(cc_param->vref_0p66v) | + V1_vref_0p4v(cc_param->vref_0p4v) | + V1_vref_0p2v(cc_param->vref_0p2v) | + V1_vref_1_1p6v(cc_param->vref_1_1p6v) | + V1_vref_0_1p6v(cc_param->vref_0_1p6v); + else + dev_err(type_c->dev, "%s: unknown parameter_ver %d\n", + __func__, type_c_cfg->parameter_ver); + + cc_param = &type_c_cfg->cc2_param; + type_c->cc2_code = rp4pk_code(cc_param->rp_4p7k_code) + | rp36k_code(cc_param->rp_36k_code) + | rp12k_code(cc_param->rp_12k_code) + | rd_code(cc_param->rd_code); + + if (type_c_cfg->parameter_ver == PARAMETER_V0) + type_c->cc2_vref = V0_vref_2p6v(cc_param->vref_2p6v) | + V0_vref_1p23v(cc_param->vref_1p23v) | + V0_vref_0p8v(cc_param->vref_0p8v) | + V0_vref_0p66v(cc_param->vref_0p66v) | + V0_vref_0p4v(cc_param->vref_0p4v) | + V0_vref_0p2v(cc_param->vref_0p2v) | + V0_vref_1_1p6v(cc_param->vref_1_1p6v) | + V0_vref_0_1p6v(cc_param->vref_0_1p6v); + else if (type_c_cfg->parameter_ver == PARAMETER_V1) + type_c->cc2_vref = V1_vref_2p6v(cc_param->vref_2p6v) | + V1_vref_1p23v(cc_param->vref_1p23v) | + V1_vref_0p8v(cc_param->vref_0p8v) | + V1_vref_0p66v(cc_param->vref_0p66v) | + V1_vref_0p4v(cc_param->vref_0p4v) | + V1_vref_0p2v(cc_param->vref_0p2v) | + V1_vref_1_1p6v(cc_param->vref_1_1p6v) | + V1_vref_0_1p6v(cc_param->vref_0_1p6v); + else + dev_err(type_c->dev, "%s: unknown parameter_ver %d\n", + __func__, type_c_cfg->parameter_ver); + + type_c->debounce = (type_c_cfg->debounce_val << 1) | DEBOUNCE_EN; + + return 0; +} + +static int extcon_rtk_type_c_init(struct type_c_data *type_c) +{ + struct device *dev = type_c->dev; + unsigned long flags; + void __iomem *reg; + int val; + + spin_lock_irqsave(&type_c->lock, flags); + + /* set parameter */ + reg = type_c->reg_base + USB_TYPEC_CTRL_CC1_0; + val = readl(reg); + val = (~CC_CODE_MASK & val) | (type_c->cc1_code & CC_CODE_MASK); + writel(val, reg); + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC2_0; + val = readl(reg); + val = (~CC_CODE_MASK & val) | (type_c->cc2_code & CC_CODE_MASK); + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC1_1; + writel(type_c->cc1_vref, reg); + + reg = type_c->reg_base + USB_TYPEC_CTRL_CC2_1; + writel(type_c->cc2_vref, reg); + + reg = type_c->reg_base + USB_TYPEC_CTRL; + val = readl(reg); + val = (~DEBOUNCE_TIME_MASK & val) | (type_c->debounce & DEBOUNCE_TIME_MASK); + + dev_info(dev, "First check USB_DR_MODE_PERIPHERAL"); + type_c->cc_mode = IN_DEVICE_MODE; + type_c->is_attach = IN_DETACH; + type_c->connect_change = CONNECT_NO_CHANGE; + + detect_host(type_c); + + spin_unlock_irqrestore(&type_c->lock, flags); + + schedule_delayed_work(&type_c->delayed_work, msecs_to_jiffies(0)); + + if (!type_c->port) { + struct typec_capability typec_cap = { }; + struct fwnode_handle *fwnode; + const char *buf; + int ret; + + typec_cap.revision = USB_TYPEC_REV_1_0; + typec_cap.prefer_role = TYPEC_NO_PREFERRED_ROLE; + typec_cap.driver_data = type_c; + typec_cap.ops = &type_c_port_ops; + + fwnode = device_get_named_child_node(dev, "connector"); + if (!fwnode) + return -EINVAL; + + ret = fwnode_property_read_string(fwnode, "power-role", &buf); + if (ret) { + dev_err(dev, "power-role not found: %d\n", ret); + return ret; + } + + ret = typec_find_port_power_role(buf); + if (ret < 0) + return ret; + typec_cap.type = ret; + + ret = fwnode_property_read_string(fwnode, "data-role", &buf); + if (ret) { + dev_err(dev, "data-role not found: %d\n", ret); + return ret; + } + + ret = typec_find_port_data_role(buf); + if (ret < 0) + return ret; + typec_cap.data = ret; + + type_c->port = typec_register_port(type_c->dev, &typec_cap); + if (IS_ERR(type_c->port)) + return PTR_ERR(type_c->port); + } + + return 0; +} + +static int extcon_rtk_type_c_edev_register(struct type_c_data *type_c) +{ + struct device *dev = type_c->dev; + int ret = 0; + + type_c->edev = devm_extcon_dev_allocate(dev, usb_type_c_cable); + if (IS_ERR(type_c->edev)) { + dev_err(dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + ret = devm_extcon_dev_register(dev, type_c->edev); + if (ret < 0) { + dev_err(dev, "failed to register extcon device\n"); + return ret; + } + + extcon_set_property_capability(type_c->edev, EXTCON_USB, + EXTCON_PROP_USB_VBUS); + extcon_set_property_capability(type_c->edev, EXTCON_USB, + EXTCON_PROP_USB_TYPEC_POLARITY); + extcon_set_property_capability(type_c->edev, EXTCON_USB, + EXTCON_PROP_USB_SS); + + extcon_set_property_capability(type_c->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_VBUS); + extcon_set_property_capability(type_c->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY); + extcon_set_property_capability(type_c->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_SS); + + return ret; +} + +static int extcon_rtk_type_c_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct type_c_data *type_c; + const struct type_c_cfg *type_c_cfg; + int ret = 0; + + type_c = devm_kzalloc(dev, sizeof(*type_c), GFP_KERNEL); + if (!type_c) + return -ENOMEM; + + type_c->reg_base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(type_c->reg_base)) + return PTR_ERR(type_c->reg_base); + + type_c->dev = dev; + + type_c->irq = irq_of_parse_and_map(pdev->dev.of_node, 0); + if (type_c->irq <= 0) { + dev_err(&pdev->dev, "Type C driver with no IRQ. Check %s setup!\n", + dev_name(&pdev->dev)); + ret = -ENODEV; + goto err; + } + + ret = devm_request_irq(dev, type_c->irq, type_c_detect_irq, + IRQF_SHARED, "type_c_detect", type_c); + + spin_lock_init(&type_c->lock); + + type_c->rd_ctrl_gpio_desc = NULL; + if (soc_device_match(rtk_soc_kylin)) { + struct gpio_desc *gpio; + + gpio = fwnode_gpiod_get_index(of_fwnode_handle(dev->of_node), + "realtek,rd-ctrl-gpios", + 0, GPIOD_OUT_HIGH, "rd-ctrl-gpio"); + if (IS_ERR(gpio)) { + dev_err(dev, "Error rd_ctrl-gpios no found (err=%d)\n", + (int)PTR_ERR(gpio)); + } else { + type_c->rd_ctrl_gpio_desc = gpio; + dev_dbg(dev, "%s get rd-ctrl-gpios (id=%d) OK\n", + __func__, desc_to_gpio(gpio)); + } + } + + type_c_cfg = of_device_get_match_data(dev); + if (!type_c_cfg) { + dev_err(dev, "type_c config are not assigned!\n"); + ret = -EINVAL; + goto err; + } + + type_c->type_c_cfg = devm_kzalloc(dev, sizeof(*type_c_cfg), GFP_KERNEL); + if (!type_c->type_c_cfg) + return -ENOMEM; + + memcpy(type_c->type_c_cfg, type_c_cfg, sizeof(*type_c_cfg)); + + if (setup_type_c_parameter(type_c)) { + dev_err(dev, "ERROR: %s to setup type c parameter!!", __func__); + ret = -EINVAL; + goto err; + } + + INIT_DELAYED_WORK(&type_c->delayed_work, host_device_switch); + + ret = extcon_rtk_type_c_init(type_c); + if (ret) { + dev_err(dev, "%s failed to init type_c\n", __func__); + goto err; + } + + platform_set_drvdata(pdev, type_c); + + ret = extcon_rtk_type_c_edev_register(type_c); + + create_debug_files(type_c); + + return 0; + +err: + dev_err(&pdev->dev, "%s: Probe fail, %d\n", __func__, ret); + + return ret; +} + +static void extcon_rtk_type_c_remove(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct type_c_data *type_c = dev_get_drvdata(dev); + u32 default_ctrl; + unsigned long flags; + + remove_debug_files(type_c); + + if (type_c->port) { + typec_unregister_port(type_c->port); + type_c->port = NULL; + } + + cancel_delayed_work_sync(&type_c->delayed_work); + flush_delayed_work(&type_c->delayed_work); + WARN_ON_ONCE(delayed_work_pending(&type_c->delayed_work)); + + spin_lock_irqsave(&type_c->lock, flags); + /* disable interrupt */ + default_ctrl = readl(type_c->reg_base + USB_TYPEC_CTRL) & + DEBOUNCE_TIME_MASK; + writel(default_ctrl, type_c->reg_base + USB_TYPEC_CTRL); + + /* disable cc detect, rp, rd */ + writel(PLR_EN, type_c->reg_base + USB_TYPEC_CTRL_CC1_0); + writel(0, type_c->reg_base + USB_TYPEC_CTRL_CC2_0); + + spin_unlock_irqrestore(&type_c->lock, flags); + + if (type_c->rd_ctrl_gpio_desc) + gpiod_put(type_c->rd_ctrl_gpio_desc); + type_c->rd_ctrl_gpio_desc = NULL; + + free_irq(type_c->irq, type_c); +} + +static const struct type_c_cfg rtd1295_type_c_cfg = { + .parameter_ver = PARAMETER_V0, + .cc_dfp_mode = CC_MODE_DFP_3_0, + .cc1_param = { .rp_4p7k_code = 0xb, + .rp_36k_code = 0x17, + .rp_12k_code = 0x10, + .rd_code = 0, + .ra_code = 0, + .vref_2p6v = 0x0, + .vref_1p23v = 0x0, + .vref_0p8v = 0x3, + .vref_0p66v = 0x0, + .vref_0p4v = 0x0, + .vref_0p2v = 0x4, + .vref_1_1p6v = 0, + .vref_0_1p6v = 0 }, + .cc2_param = { .rp_4p7k_code = 0xc, + .rp_36k_code = 0x17, + .rp_12k_code = 0x12, + .rd_code = 0, + .ra_code = 0, + .vref_2p6v = 0x2, + .vref_1p23v = 0x0, + .vref_0p8v = 0x3, + .vref_0p66v = 0x0, + .vref_0p4v = 0x0, + .vref_0p2v = 0x5, + .vref_1_1p6v = 0, + .vref_0_1p6v = 0 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1395_type_c_cfg = { + .parameter_ver = PARAMETER_V0, + .cc_dfp_mode = CC_MODE_DFP_3_0, + .cc1_param = { .rp_4p7k_code = 0xc, + .rp_36k_code = 0xb, + .rp_12k_code = 0xe, + .rd_code = 0x10, + .ra_code = 0x0, + .vref_2p6v = 0x0, + .vref_1p23v = 0x1, + .vref_0p8v = 0x0, + .vref_0p66v = 0x0, + .vref_0p4v = 0x3, + .vref_0p2v = 0x0, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0xb, + .rp_36k_code = 0x9, + .rp_12k_code = 0xe, + .rd_code = 0xf, + .ra_code = 0x0, + .vref_2p6v = 0x1, + .vref_1p23v = 0x3, + .vref_0p8v = 0x3, + .vref_0p66v = 0x2, + .vref_0p4v = 0x3, + .vref_0p2v = 0x2, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1619_type_c_cfg = { + .parameter_ver = PARAMETER_V0, + .cc_dfp_mode = CC_MODE_DFP_3_0, + .cc1_param = { .rp_4p7k_code = 0xc, + .rp_36k_code = 0xf, + .rp_12k_code = 0xe, + .rd_code = 0x11, + .ra_code = 0x0, + .vref_2p6v = 0x5, + .vref_1p23v = 0x7, + .vref_0p8v = 0xa, + .vref_0p66v = 0xa, + .vref_0p4v = 0x3, + .vref_0p2v = 0x2, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0xc, + .rp_36k_code = 0xf, + .rp_12k_code = 0xe, + .rd_code = 0xf, + .ra_code = 0x0, + .vref_2p6v = 0x5, + .vref_1p23v = 0x8, + .vref_0p8v = 0xa, + .vref_0p66v = 0xa, + .vref_0p4v = 0x3, + .vref_0p2v = 0x2, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1319_type_c_cfg = { + .parameter_ver = PARAMETER_V0, + .cc_dfp_mode = CC_MODE_DFP_1_5, + .cc1_param = { .rp_4p7k_code = 0x9, + .rp_36k_code = 0xe, + .rp_12k_code = 0x9, + .rd_code = 0x9, + .ra_code = 0x7, + .vref_2p6v = 0x3, + .vref_1p23v = 0x7, + .vref_0p8v = 0x7, + .vref_0p66v = 0x6, + .vref_0p4v = 0x2, + .vref_0p2v = 0x3, + .vref_1_1p6v = 0x4, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0x8, + .rp_36k_code = 0xe, + .rp_12k_code = 0x9, + .rd_code = 0x9, + .ra_code = 0x7, + .vref_2p6v = 0x3, + .vref_1p23v = 0x7, + .vref_0p8v = 0x7, + .vref_0p66v = 0x6, + .vref_0p4v = 0x3, + .vref_0p2v = 0x3, + .vref_1_1p6v = 0x6, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1312c_type_c_cfg = { + .parameter_ver = PARAMETER_V0, + .cc_dfp_mode = CC_MODE_DFP_1_5, + .cc1_param = { .rp_4p7k_code = 0xe, + .rp_36k_code = 0xc, + .rp_12k_code = 0xc, + .rd_code = 0xa, + .ra_code = 0x3, + .vref_2p6v = 0xa, + .vref_1p23v = 0x7, + .vref_0p8v = 0x7, + .vref_0p66v = 0x7, + .vref_0p4v = 0x4, + .vref_0p2v = 0x4, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0xe, + .rp_36k_code = 0xc, + .rp_12k_code = 0xc, + .rd_code = 0xa, + .ra_code = 0x3, + .vref_2p6v = 0xa, + .vref_1p23v = 0x7, + .vref_0p8v = 0x7, + .vref_0p66v = 0x7, + .vref_0p4v = 0x4, + .vref_0p2v = 0x4, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1619b_type_c_cfg = { + .parameter_ver = PARAMETER_V1, + .cc_dfp_mode = CC_MODE_DFP_1_5, + .cc1_param = { .rp_4p7k_code = 0xf, + .rp_36k_code = 0xf, + .rp_12k_code = 0xf, + .rd_code = 0xf, + .ra_code = 0x7, + .vref_2p6v = 0x9, + .vref_1p23v = 0x7, + .vref_0p8v = 0x9, + .vref_0p66v = 0x8, + .vref_0p4v = 0x7, + .vref_0p2v = 0x9, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0xf, + .rp_36k_code = 0xf, + .rp_12k_code = 0xf, + .rd_code = 0xf, + .ra_code = 0x7, + .vref_1p23v = 0x7, + .vref_0p8v = 0x9, + .vref_0p66v = 0x8, + .vref_0p4v = 0x7, + .vref_0p2v = 0x8, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1319d_type_c_cfg = { + .parameter_ver = PARAMETER_V1, + .cc_dfp_mode = CC_MODE_DFP_1_5, + .cc1_param = { .rp_4p7k_code = 0xe, + .rp_36k_code = 0x3, + .rp_12k_code = 0xe, + .rd_code = 0xf, + .ra_code = 0x6, + .vref_2p6v = 0x7, + .vref_1p23v = 0x7, + .vref_0p8v = 0x8, + .vref_0p66v = 0x7, + .vref_0p4v = 0x7, + .vref_0p2v = 0x7, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0xe, + .rp_36k_code = 0x3, + .rp_12k_code = 0xe, + .rd_code = 0xf, + .ra_code = 0x6, + .vref_2p6v = 0x7, + .vref_1p23v = 0x7, + .vref_0p8v = 0x8, + .vref_0p66v = 0x7, + .vref_0p4v = 0x7, + .vref_0p2v = 0x8, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct type_c_cfg rtd1315e_type_c_cfg = { + .parameter_ver = PARAMETER_V1, + .cc_dfp_mode = CC_MODE_DFP_1_5, + .cc1_param = { .rp_4p7k_code = 0xe, + .rp_36k_code = 0x3, + .rp_12k_code = 0xe, + .rd_code = 0xf, + .ra_code = 0x6, + .vref_2p6v = 0x7, + .vref_1p23v = 0x7, + .vref_0p8v = 0x8, + .vref_0p66v = 0x7, + .vref_0p4v = 0x7, + .vref_0p2v = 0x7, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .cc2_param = { .rp_4p7k_code = 0xe, + .rp_36k_code = 0x3, + .rp_12k_code = 0xe, + .rd_code = 0xf, + .ra_code = 0x6, + .vref_2p6v = 0x7, + .vref_1p23v = 0x7, + .vref_0p8v = 0x8, + .vref_0p66v = 0x7, + .vref_0p4v = 0x7, + .vref_0p2v = 0x8, + .vref_1_1p6v = 0x7, + .vref_0_1p6v = 0x7 }, + .debounce_val = 0x7f, /* 1b,1us 7f,4.7us */ + .use_defalut_parameter = false, +}; + +static const struct of_device_id extcon_rtk_type_c_match[] = { + { .compatible = "realtek,rtd1295-type-c", .data = &rtd1295_type_c_cfg }, + { .compatible = "realtek,rtd1312c-type-c", .data = &rtd1312c_type_c_cfg }, + { .compatible = "realtek,rtd1315e-type-c", .data = &rtd1315e_type_c_cfg }, + { .compatible = "realtek,rtd1319-type-c", .data = &rtd1319_type_c_cfg }, + { .compatible = "realtek,rtd1319d-type-c", .data = &rtd1319d_type_c_cfg }, + { .compatible = "realtek,rtd1395-type-c", .data = &rtd1395_type_c_cfg }, + { .compatible = "realtek,rtd1619-type-c", .data = &rtd1619_type_c_cfg }, + { .compatible = "realtek,rtd1619b-type-c", .data = &rtd1619b_type_c_cfg }, + {}, +}; +MODULE_DEVICE_TABLE(of, extcon_rtk_type_c_match); + +#ifdef CONFIG_PM_SLEEP +static int extcon_rtk_type_c_prepare(struct device *dev) +{ + struct type_c_data *type_c = dev_get_drvdata(dev); + u32 default_ctrl; + unsigned long flags; + + cancel_delayed_work_sync(&type_c->delayed_work); + flush_delayed_work(&type_c->delayed_work); + WARN_ON_ONCE(delayed_work_pending(&type_c->delayed_work)); + + spin_lock_irqsave(&type_c->lock, flags); + /* disable interrupt */ + default_ctrl = readl(type_c->reg_base + USB_TYPEC_CTRL) & + DEBOUNCE_TIME_MASK; + writel(default_ctrl, type_c->reg_base + USB_TYPEC_CTRL); + + /* disable cc detect, rp, rd */ + writel(PLR_EN, type_c->reg_base + USB_TYPEC_CTRL_CC1_0); + writel(0, type_c->reg_base + USB_TYPEC_CTRL_CC2_0); + + spin_unlock_irqrestore(&type_c->lock, flags); + + return 0; +} + +static void extcon_rtk_type_c_complete(struct device *dev) +{ + /* nothing */ +} + +static int extcon_rtk_type_c_suspend(struct device *dev) +{ + /* nothing */ + + return 0; +} + +static int extcon_rtk_type_c_resume(struct device *dev) +{ + struct type_c_data *type_c = dev_get_drvdata(dev); + int ret; + + ret = extcon_rtk_type_c_init(type_c); + if (ret) { + dev_err(dev, "%s failed to init type_c\n", __func__); + return ret; + } + + return 0; +} + +static const struct dev_pm_ops extcon_rtk_type_c_pm_ops = { + SET_LATE_SYSTEM_SLEEP_PM_OPS(extcon_rtk_type_c_suspend, extcon_rtk_type_c_resume) + .prepare = extcon_rtk_type_c_prepare, + .complete = extcon_rtk_type_c_complete, +}; + +#define DEV_PM_OPS (&extcon_rtk_type_c_pm_ops) +#else +#define DEV_PM_OPS NULL +#endif /* CONFIG_PM_SLEEP */ + +static struct platform_driver extcon_rtk_type_c_driver = { + .probe = extcon_rtk_type_c_probe, + .remove = extcon_rtk_type_c_remove, + .driver = { + .name = "extcon-rtk-type_c", + .of_match_table = extcon_rtk_type_c_match, + .pm = DEV_PM_OPS, + }, +}; + +module_platform_driver(extcon_rtk_type_c_driver); + +MODULE_DESCRIPTION("Realtek Extcon Type C driver"); +MODULE_AUTHOR("Stanley Chang <stanley_chang@realtek.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-sm5502.c b/drivers/extcon/extcon-sm5502.c new file mode 100644 index 000000000..c8c4b9ef7 --- /dev/null +++ b/drivers/extcon/extcon-sm5502.c @@ -0,0 +1,855 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * extcon-sm5502.c - Silicon Mitus SM5502 extcon drvier to support USB switches + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + * Author: Chanwoo Choi <cw00.choi@samsung.com> + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/irqdomain.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/extcon-provider.h> + +#include "extcon-sm5502.h" + +#define DELAY_MS_DEFAULT 17000 /* unit: millisecond */ + +struct muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +struct reg_data { + u8 reg; + unsigned int val; + bool invert; +}; + +struct sm5502_muic_info { + struct device *dev; + struct extcon_dev *edev; + + struct i2c_client *i2c; + struct regmap *regmap; + + const struct sm5502_type *type; + struct regmap_irq_chip_data *irq_data; + int irq; + bool irq_attach; + bool irq_detach; + struct work_struct irq_work; + + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; +}; + +struct sm5502_type { + struct muic_irq *muic_irqs; + unsigned int num_muic_irqs; + const struct regmap_irq_chip *irq_chip; + + struct reg_data *reg_data; + unsigned int num_reg_data; + + unsigned int otg_dev_type1; + int (*parse_irq)(struct sm5502_muic_info *info, int irq_type); +}; + +/* Default value of SM5502 register to bring up MUIC device. */ +static struct reg_data sm5502_reg_data[] = { + { + .reg = SM5502_REG_RESET, + .val = SM5502_REG_RESET_MASK, + .invert = true, + }, { + .reg = SM5502_REG_CONTROL, + .val = SM5502_REG_CONTROL_MASK_INT_MASK, + .invert = false, + }, { + .reg = SM5502_REG_INTMASK1, + .val = SM5502_REG_INTM1_KP_MASK + | SM5502_REG_INTM1_LKP_MASK + | SM5502_REG_INTM1_LKR_MASK, + .invert = true, + }, { + .reg = SM5502_REG_INTMASK2, + .val = SM5502_REG_INTM2_VBUS_DET_MASK + | SM5502_REG_INTM2_REV_ACCE_MASK + | SM5502_REG_INTM2_ADC_CHG_MASK + | SM5502_REG_INTM2_STUCK_KEY_MASK + | SM5502_REG_INTM2_STUCK_KEY_RCV_MASK + | SM5502_REG_INTM2_MHL_MASK, + .invert = true, + }, +}; + +/* Default value of SM5504 register to bring up MUIC device. */ +static struct reg_data sm5504_reg_data[] = { + { + .reg = SM5502_REG_RESET, + .val = SM5502_REG_RESET_MASK, + .invert = true, + }, { + .reg = SM5502_REG_INTMASK1, + .val = SM5504_REG_INTM1_ATTACH_MASK + | SM5504_REG_INTM1_DETACH_MASK, + .invert = false, + }, { + .reg = SM5502_REG_INTMASK2, + .val = SM5504_REG_INTM2_RID_CHG_MASK + | SM5504_REG_INTM2_UVLO_MASK + | SM5504_REG_INTM2_POR_MASK, + .invert = true, + }, { + .reg = SM5502_REG_CONTROL, + .val = SM5502_REG_CONTROL_MANUAL_SW_MASK + | SM5504_REG_CONTROL_CHGTYP_MASK + | SM5504_REG_CONTROL_USBCHDEN_MASK + | SM5504_REG_CONTROL_ADC_EN_MASK, + .invert = true, + }, +}; + +/* List of detectable cables */ +static const unsigned int sm5502_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_DCP, + EXTCON_NONE, +}; + +/* Define supported accessory type */ +enum sm5502_muic_acc_type { + SM5502_MUIC_ADC_GROUND = 0x0, + SM5502_MUIC_ADC_SEND_END_BUTTON, + SM5502_MUIC_ADC_REMOTE_S1_BUTTON, + SM5502_MUIC_ADC_REMOTE_S2_BUTTON, + SM5502_MUIC_ADC_REMOTE_S3_BUTTON, + SM5502_MUIC_ADC_REMOTE_S4_BUTTON, + SM5502_MUIC_ADC_REMOTE_S5_BUTTON, + SM5502_MUIC_ADC_REMOTE_S6_BUTTON, + SM5502_MUIC_ADC_REMOTE_S7_BUTTON, + SM5502_MUIC_ADC_REMOTE_S8_BUTTON, + SM5502_MUIC_ADC_REMOTE_S9_BUTTON, + SM5502_MUIC_ADC_REMOTE_S10_BUTTON, + SM5502_MUIC_ADC_REMOTE_S11_BUTTON, + SM5502_MUIC_ADC_REMOTE_S12_BUTTON, + SM5502_MUIC_ADC_RESERVED_ACC_1, + SM5502_MUIC_ADC_RESERVED_ACC_2, + SM5502_MUIC_ADC_RESERVED_ACC_3, + SM5502_MUIC_ADC_RESERVED_ACC_4, + SM5502_MUIC_ADC_RESERVED_ACC_5, + SM5502_MUIC_ADC_AUDIO_TYPE2, + SM5502_MUIC_ADC_PHONE_POWERED_DEV, + SM5502_MUIC_ADC_TTY_CONVERTER, + SM5502_MUIC_ADC_UART_CABLE, + SM5502_MUIC_ADC_TYPE1_CHARGER, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB, + SM5502_MUIC_ADC_AUDIO_VIDEO_CABLE, + SM5502_MUIC_ADC_TYPE2_CHARGER, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART, + SM5502_MUIC_ADC_AUDIO_TYPE1, + SM5502_MUIC_ADC_OPEN = 0x1f, + + /* + * The below accessories have same ADC value (0x1f or 0x1e). + * So, Device type1 is used to separate specific accessory. + */ + /* |---------|--ADC| */ + /* | [7:5]|[4:0]| */ + SM5502_MUIC_ADC_AUDIO_TYPE1_FULL_REMOTE = 0x3e, /* | 001|11110| */ + SM5502_MUIC_ADC_AUDIO_TYPE1_SEND_END = 0x5e, /* | 010|11110| */ + /* |Dev Type1|--ADC| */ + SM5502_MUIC_ADC_GROUND_USB_OTG = 0x80, /* | 100|00000| */ + SM5502_MUIC_ADC_OPEN_USB = 0x5f, /* | 010|11111| */ + SM5502_MUIC_ADC_OPEN_TA = 0xdf, /* | 110|11111| */ + SM5502_MUIC_ADC_OPEN_USB_OTG = 0xff, /* | 111|11111| */ +}; + +/* List of supported interrupt for SM5502 */ +static struct muic_irq sm5502_muic_irqs[] = { + { SM5502_IRQ_INT1_ATTACH, "muic-attach" }, + { SM5502_IRQ_INT1_DETACH, "muic-detach" }, + { SM5502_IRQ_INT1_KP, "muic-kp" }, + { SM5502_IRQ_INT1_LKP, "muic-lkp" }, + { SM5502_IRQ_INT1_LKR, "muic-lkr" }, + { SM5502_IRQ_INT1_OVP_EVENT, "muic-ovp-event" }, + { SM5502_IRQ_INT1_OCP_EVENT, "muic-ocp-event" }, + { SM5502_IRQ_INT1_OVP_OCP_DIS, "muic-ovp-ocp-dis" }, + { SM5502_IRQ_INT2_VBUS_DET, "muic-vbus-det" }, + { SM5502_IRQ_INT2_REV_ACCE, "muic-rev-acce" }, + { SM5502_IRQ_INT2_ADC_CHG, "muic-adc-chg" }, + { SM5502_IRQ_INT2_STUCK_KEY, "muic-stuck-key" }, + { SM5502_IRQ_INT2_STUCK_KEY_RCV, "muic-stuck-key-rcv" }, + { SM5502_IRQ_INT2_MHL, "muic-mhl" }, +}; + +/* Define interrupt list of SM5502 to register regmap_irq */ +static const struct regmap_irq sm5502_irqs[] = { + /* INT1 interrupts */ + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_ATTACH_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_DETACH_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_KP_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_LKP_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_LKR_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_OVP_EVENT_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_OCP_EVENT_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_OVP_OCP_DIS_MASK, }, + + /* INT2 interrupts */ + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_VBUS_DET_MASK,}, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_REV_ACCE_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_ADC_CHG_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_STUCK_KEY_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_STUCK_KEY_RCV_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_MHL_MASK, }, +}; + +static const struct regmap_irq_chip sm5502_muic_irq_chip = { + .name = "sm5502", + .status_base = SM5502_REG_INT1, + .mask_base = SM5502_REG_INTMASK1, + .num_regs = 2, + .irqs = sm5502_irqs, + .num_irqs = ARRAY_SIZE(sm5502_irqs), +}; + +/* List of supported interrupt for SM5504 */ +static struct muic_irq sm5504_muic_irqs[] = { + { SM5504_IRQ_INT1_ATTACH, "muic-attach" }, + { SM5504_IRQ_INT1_DETACH, "muic-detach" }, + { SM5504_IRQ_INT1_CHG_DET, "muic-chg-det" }, + { SM5504_IRQ_INT1_DCD_OUT, "muic-dcd-out" }, + { SM5504_IRQ_INT1_OVP_EVENT, "muic-ovp-event" }, + { SM5504_IRQ_INT1_CONNECT, "muic-connect" }, + { SM5504_IRQ_INT1_ADC_CHG, "muic-adc-chg" }, + { SM5504_IRQ_INT2_RID_CHG, "muic-rid-chg" }, + { SM5504_IRQ_INT2_UVLO, "muic-uvlo" }, + { SM5504_IRQ_INT2_POR, "muic-por" }, + { SM5504_IRQ_INT2_OVP_FET, "muic-ovp-fet" }, + { SM5504_IRQ_INT2_OCP_LATCH, "muic-ocp-latch" }, + { SM5504_IRQ_INT2_OCP_EVENT, "muic-ocp-event" }, + { SM5504_IRQ_INT2_OVP_OCP_EVENT, "muic-ovp-ocp-event" }, +}; + +/* Define interrupt list of SM5504 to register regmap_irq */ +static const struct regmap_irq sm5504_irqs[] = { + /* INT1 interrupts */ + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_ATTACH_MASK, }, + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_DETACH_MASK, }, + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_CHG_DET_MASK, }, + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_DCD_OUT_MASK, }, + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_OVP_MASK, }, + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_CONNECT_MASK, }, + { .reg_offset = 0, .mask = SM5504_IRQ_INT1_ADC_CHG_MASK, }, + + /* INT2 interrupts */ + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_RID_CHG_MASK,}, + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_UVLO_MASK, }, + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_POR_MASK, }, + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_OVP_FET_MASK, }, + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_OCP_LATCH_MASK, }, + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_OCP_EVENT_MASK, }, + { .reg_offset = 1, .mask = SM5504_IRQ_INT2_OVP_OCP_EVENT_MASK, }, +}; + +static const struct regmap_irq_chip sm5504_muic_irq_chip = { + .name = "sm5504", + .status_base = SM5502_REG_INT1, + .mask_base = SM5502_REG_INTMASK1, + .num_regs = 2, + .irqs = sm5504_irqs, + .num_irqs = ARRAY_SIZE(sm5504_irqs), +}; + +/* Define regmap configuration of SM5502 for I2C communication */ +static bool sm5502_muic_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case SM5502_REG_INTMASK1: + case SM5502_REG_INTMASK2: + return true; + default: + break; + } + return false; +} + +static const struct regmap_config sm5502_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .volatile_reg = sm5502_muic_volatile_reg, + .max_register = SM5502_REG_END, +}; + +/* Change DM_CON/DP_CON/VBUSIN switch according to cable type */ +static int sm5502_muic_set_path(struct sm5502_muic_info *info, + unsigned int con_sw, unsigned int vbus_sw, + bool attached) +{ + int ret; + + if (!attached) { + con_sw = DM_DP_SWITCH_OPEN; + vbus_sw = VBUSIN_SWITCH_OPEN; + } + + switch (con_sw) { + case DM_DP_SWITCH_OPEN: + case DM_DP_SWITCH_USB: + case DM_DP_SWITCH_AUDIO: + case DM_DP_SWITCH_UART: + ret = regmap_update_bits(info->regmap, SM5502_REG_MANUAL_SW1, + SM5502_REG_MANUAL_SW1_DP_MASK | + SM5502_REG_MANUAL_SW1_DM_MASK, + con_sw); + if (ret < 0) { + dev_err(info->dev, + "cannot update DM_CON/DP_CON switch\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Unknown DM_CON/DP_CON switch type (%d)\n", + con_sw); + return -EINVAL; + } + + switch (vbus_sw) { + case VBUSIN_SWITCH_OPEN: + case VBUSIN_SWITCH_VBUSOUT: + case VBUSIN_SWITCH_MIC: + case VBUSIN_SWITCH_VBUSOUT_WITH_USB: + ret = regmap_update_bits(info->regmap, SM5502_REG_MANUAL_SW1, + SM5502_REG_MANUAL_SW1_VBUSIN_MASK, + vbus_sw); + if (ret < 0) { + dev_err(info->dev, + "cannot update VBUSIN switch\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Unknown VBUS switch type (%d)\n", vbus_sw); + return -EINVAL; + } + + return 0; +} + +/* Return cable type of attached or detached accessories */ +static unsigned int sm5502_muic_get_cable_type(struct sm5502_muic_info *info) +{ + unsigned int cable_type, adc, dev_type1; + int ret; + + /* Read ADC value according to external cable or button */ + ret = regmap_read(info->regmap, SM5502_REG_ADC, &adc); + if (ret) { + dev_err(info->dev, "failed to read ADC register\n"); + return ret; + } + + /* + * If ADC is SM5502_MUIC_ADC_GROUND(0x0), external cable hasn't + * connected with to MUIC device. + */ + cable_type = adc & SM5502_REG_ADC_MASK; + + switch (cable_type) { + case SM5502_MUIC_ADC_GROUND: + ret = regmap_read(info->regmap, SM5502_REG_DEV_TYPE1, + &dev_type1); + if (ret) { + dev_err(info->dev, "failed to read DEV_TYPE1 reg\n"); + return ret; + } + + if (dev_type1 == info->type->otg_dev_type1) { + cable_type = SM5502_MUIC_ADC_GROUND_USB_OTG; + } else { + dev_dbg(info->dev, + "cannot identify the cable type: adc(0x%x), dev_type1(0x%x)\n", + adc, dev_type1); + return -EINVAL; + } + break; + case SM5502_MUIC_ADC_SEND_END_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S1_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S2_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S3_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S4_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S5_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S6_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S7_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S8_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S9_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S10_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S11_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S12_BUTTON: + case SM5502_MUIC_ADC_RESERVED_ACC_1: + case SM5502_MUIC_ADC_RESERVED_ACC_2: + case SM5502_MUIC_ADC_RESERVED_ACC_3: + case SM5502_MUIC_ADC_RESERVED_ACC_4: + case SM5502_MUIC_ADC_RESERVED_ACC_5: + case SM5502_MUIC_ADC_AUDIO_TYPE2: + case SM5502_MUIC_ADC_PHONE_POWERED_DEV: + case SM5502_MUIC_ADC_TTY_CONVERTER: + case SM5502_MUIC_ADC_UART_CABLE: + case SM5502_MUIC_ADC_TYPE1_CHARGER: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB: + case SM5502_MUIC_ADC_AUDIO_VIDEO_CABLE: + case SM5502_MUIC_ADC_TYPE2_CHARGER: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART: + break; + case SM5502_MUIC_ADC_AUDIO_TYPE1: + /* + * Check whether cable type is + * SM5502_MUIC_ADC_AUDIO_TYPE1_FULL_REMOTE + * or SM5502_MUIC_ADC_AUDIO_TYPE1_SEND_END + * by using Button event. + */ + break; + case SM5502_MUIC_ADC_OPEN: + ret = regmap_read(info->regmap, SM5502_REG_DEV_TYPE1, + &dev_type1); + if (ret) { + dev_err(info->dev, "failed to read DEV_TYPE1 reg\n"); + return ret; + } + + if (dev_type1 == info->type->otg_dev_type1) { + cable_type = SM5502_MUIC_ADC_OPEN_USB_OTG; + break; + } + + switch (dev_type1) { + case SM5502_REG_DEV_TYPE1_USB_SDP_MASK: + cable_type = SM5502_MUIC_ADC_OPEN_USB; + break; + case SM5502_REG_DEV_TYPE1_DEDICATED_CHG_MASK: + cable_type = SM5502_MUIC_ADC_OPEN_TA; + break; + default: + dev_dbg(info->dev, + "cannot identify the cable type: adc(0x%x)\n", + adc); + return -EINVAL; + } + break; + default: + dev_err(info->dev, + "failed to identify the cable type: adc(0x%x)\n", adc); + return -EINVAL; + } + + return cable_type; +} + +static int sm5502_muic_cable_handler(struct sm5502_muic_info *info, + bool attached) +{ + static unsigned int prev_cable_type = SM5502_MUIC_ADC_GROUND; + unsigned int cable_type = SM5502_MUIC_ADC_GROUND; + unsigned int con_sw = DM_DP_SWITCH_OPEN; + unsigned int vbus_sw = VBUSIN_SWITCH_OPEN; + unsigned int id; + int ret; + + /* Get the type of attached or detached cable */ + if (attached) + cable_type = sm5502_muic_get_cable_type(info); + else + cable_type = prev_cable_type; + prev_cable_type = cable_type; + + switch (cable_type) { + case SM5502_MUIC_ADC_OPEN_USB: + id = EXTCON_USB; + con_sw = DM_DP_SWITCH_USB; + vbus_sw = VBUSIN_SWITCH_VBUSOUT_WITH_USB; + break; + case SM5502_MUIC_ADC_OPEN_TA: + id = EXTCON_CHG_USB_DCP; + con_sw = DM_DP_SWITCH_OPEN; + vbus_sw = VBUSIN_SWITCH_VBUSOUT; + break; + case SM5502_MUIC_ADC_GROUND_USB_OTG: + case SM5502_MUIC_ADC_OPEN_USB_OTG: + id = EXTCON_USB_HOST; + con_sw = DM_DP_SWITCH_USB; + vbus_sw = VBUSIN_SWITCH_OPEN; + break; + default: + dev_dbg(info->dev, + "cannot handle this cable_type (0x%x)\n", cable_type); + return 0; + } + + /* Change internal hardware path(DM_CON/DP_CON, VBUSIN) */ + ret = sm5502_muic_set_path(info, con_sw, vbus_sw, attached); + if (ret < 0) + return ret; + + /* Change the state of external accessory */ + extcon_set_state_sync(info->edev, id, attached); + if (id == EXTCON_USB) + extcon_set_state_sync(info->edev, EXTCON_CHG_USB_SDP, + attached); + + return 0; +} + +static void sm5502_muic_irq_work(struct work_struct *work) +{ + struct sm5502_muic_info *info = container_of(work, + struct sm5502_muic_info, irq_work); + int ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + /* Detect attached or detached cables */ + if (info->irq_attach) { + ret = sm5502_muic_cable_handler(info, true); + info->irq_attach = false; + } + if (info->irq_detach) { + ret = sm5502_muic_cable_handler(info, false); + info->irq_detach = false; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +/* + * Sets irq_attach or irq_detach in sm5502_muic_info and returns 0. + * Returns -ESRCH if irq_type does not match registered IRQ for this dev type. + */ +static int sm5502_parse_irq(struct sm5502_muic_info *info, int irq_type) +{ + switch (irq_type) { + case SM5502_IRQ_INT1_ATTACH: + info->irq_attach = true; + break; + case SM5502_IRQ_INT1_DETACH: + info->irq_detach = true; + break; + case SM5502_IRQ_INT1_KP: + case SM5502_IRQ_INT1_LKP: + case SM5502_IRQ_INT1_LKR: + case SM5502_IRQ_INT1_OVP_EVENT: + case SM5502_IRQ_INT1_OCP_EVENT: + case SM5502_IRQ_INT1_OVP_OCP_DIS: + case SM5502_IRQ_INT2_VBUS_DET: + case SM5502_IRQ_INT2_REV_ACCE: + case SM5502_IRQ_INT2_ADC_CHG: + case SM5502_IRQ_INT2_STUCK_KEY: + case SM5502_IRQ_INT2_STUCK_KEY_RCV: + case SM5502_IRQ_INT2_MHL: + default: + break; + } + + return 0; +} + +static int sm5504_parse_irq(struct sm5502_muic_info *info, int irq_type) +{ + switch (irq_type) { + case SM5504_IRQ_INT1_ATTACH: + info->irq_attach = true; + break; + case SM5504_IRQ_INT1_DETACH: + info->irq_detach = true; + break; + case SM5504_IRQ_INT1_CHG_DET: + case SM5504_IRQ_INT1_DCD_OUT: + case SM5504_IRQ_INT1_OVP_EVENT: + case SM5504_IRQ_INT1_CONNECT: + case SM5504_IRQ_INT1_ADC_CHG: + case SM5504_IRQ_INT2_RID_CHG: + case SM5504_IRQ_INT2_UVLO: + case SM5504_IRQ_INT2_POR: + case SM5504_IRQ_INT2_OVP_FET: + case SM5504_IRQ_INT2_OCP_LATCH: + case SM5504_IRQ_INT2_OCP_EVENT: + case SM5504_IRQ_INT2_OVP_OCP_EVENT: + default: + break; + } + + return 0; +} + +static irqreturn_t sm5502_muic_irq_handler(int irq, void *data) +{ + struct sm5502_muic_info *info = data; + int i, irq_type = -1, ret; + + for (i = 0; i < info->type->num_muic_irqs; i++) + if (irq == info->type->muic_irqs[i].virq) + irq_type = info->type->muic_irqs[i].irq; + + ret = info->type->parse_irq(info, irq_type); + if (ret < 0) { + dev_warn(info->dev, "cannot handle is interrupt:%d\n", + irq_type); + return IRQ_HANDLED; + } + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static void sm5502_muic_detect_cable_wq(struct work_struct *work) +{ + struct sm5502_muic_info *info = container_of(to_delayed_work(work), + struct sm5502_muic_info, wq_detcable); + int ret; + + /* Notify the state of connector cable or not */ + ret = sm5502_muic_cable_handler(info, true); + if (ret < 0) + dev_warn(info->dev, "failed to detect cable state\n"); +} + +static void sm5502_init_dev_type(struct sm5502_muic_info *info) +{ + unsigned int reg_data, vendor_id, version_id; + int i, ret; + + /* To test I2C, Print version_id and vendor_id of SM5502 */ + ret = regmap_read(info->regmap, SM5502_REG_DEVICE_ID, ®_data); + if (ret) { + dev_err(info->dev, + "failed to read DEVICE_ID register: %d\n", ret); + return; + } + + vendor_id = ((reg_data & SM5502_REG_DEVICE_ID_VENDOR_MASK) >> + SM5502_REG_DEVICE_ID_VENDOR_SHIFT); + version_id = ((reg_data & SM5502_REG_DEVICE_ID_VERSION_MASK) >> + SM5502_REG_DEVICE_ID_VERSION_SHIFT); + + dev_info(info->dev, "Device type: version: 0x%x, vendor: 0x%x\n", + version_id, vendor_id); + + /* Initiazle the register of SM5502 device to bring-up */ + for (i = 0; i < info->type->num_reg_data; i++) { + unsigned int val = 0; + + if (!info->type->reg_data[i].invert) + val |= ~info->type->reg_data[i].val; + else + val = info->type->reg_data[i].val; + regmap_write(info->regmap, info->type->reg_data[i].reg, val); + } +} + +static int sm5022_muic_i2c_probe(struct i2c_client *i2c) +{ + struct device_node *np = i2c->dev.of_node; + struct sm5502_muic_info *info; + int i, ret, irq_flags; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&i2c->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + i2c_set_clientdata(i2c, info); + + info->dev = &i2c->dev; + info->i2c = i2c; + info->irq = i2c->irq; + info->type = device_get_match_data(info->dev); + if (!info->type) + return -EINVAL; + if (!info->type->parse_irq) { + dev_err(info->dev, "parse_irq missing in struct sm5502_type\n"); + return -EINVAL; + } + + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, sm5502_muic_irq_work); + + info->regmap = devm_regmap_init_i2c(i2c, &sm5502_muic_regmap_config); + if (IS_ERR(info->regmap)) { + ret = PTR_ERR(info->regmap); + dev_err(info->dev, "failed to allocate register map: %d\n", + ret); + return ret; + } + + /* Support irq domain for SM5502 MUIC device */ + irq_flags = IRQF_TRIGGER_FALLING | IRQF_ONESHOT | IRQF_SHARED; + ret = devm_regmap_add_irq_chip(info->dev, info->regmap, info->irq, + irq_flags, 0, info->type->irq_chip, + &info->irq_data); + if (ret != 0) { + dev_err(info->dev, "failed to request IRQ %d: %d\n", + info->irq, ret); + return ret; + } + + for (i = 0; i < info->type->num_muic_irqs; i++) { + struct muic_irq *muic_irq = &info->type->muic_irqs[i]; + int virq = 0; + + virq = regmap_irq_get_virq(info->irq_data, muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(info->dev, virq, NULL, + sm5502_muic_irq_handler, + IRQF_NO_SUSPEND | IRQF_ONESHOT, + muic_irq->name, info); + if (ret) { + dev_err(info->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Allocate extcon device */ + info->edev = devm_extcon_dev_allocate(info->dev, sm5502_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(info->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + + /* Register extcon device */ + ret = devm_extcon_dev_register(info->dev, info->edev); + if (ret) { + dev_err(info->dev, "failed to register extcon device\n"); + return ret; + } + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, sm5502_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + msecs_to_jiffies(DELAY_MS_DEFAULT)); + + /* Initialize SM5502 device and print vendor id and version id */ + sm5502_init_dev_type(info); + + return 0; +} + +static const struct sm5502_type sm5502_data = { + .muic_irqs = sm5502_muic_irqs, + .num_muic_irqs = ARRAY_SIZE(sm5502_muic_irqs), + .irq_chip = &sm5502_muic_irq_chip, + .reg_data = sm5502_reg_data, + .num_reg_data = ARRAY_SIZE(sm5502_reg_data), + .otg_dev_type1 = SM5502_REG_DEV_TYPE1_USB_OTG_MASK, + .parse_irq = sm5502_parse_irq, +}; + +static const struct sm5502_type sm5504_data = { + .muic_irqs = sm5504_muic_irqs, + .num_muic_irqs = ARRAY_SIZE(sm5504_muic_irqs), + .irq_chip = &sm5504_muic_irq_chip, + .reg_data = sm5504_reg_data, + .num_reg_data = ARRAY_SIZE(sm5504_reg_data), + .otg_dev_type1 = SM5504_REG_DEV_TYPE1_USB_OTG_MASK, + .parse_irq = sm5504_parse_irq, +}; + +static const struct of_device_id sm5502_dt_match[] = { + { .compatible = "siliconmitus,sm5502-muic", .data = &sm5502_data }, + { .compatible = "siliconmitus,sm5504-muic", .data = &sm5504_data }, + { .compatible = "siliconmitus,sm5703-muic", .data = &sm5502_data }, + { }, +}; +MODULE_DEVICE_TABLE(of, sm5502_dt_match); + +#ifdef CONFIG_PM_SLEEP +static int sm5502_muic_suspend(struct device *dev) +{ + struct i2c_client *i2c = to_i2c_client(dev); + struct sm5502_muic_info *info = i2c_get_clientdata(i2c); + + enable_irq_wake(info->irq); + + return 0; +} + +static int sm5502_muic_resume(struct device *dev) +{ + struct i2c_client *i2c = to_i2c_client(dev); + struct sm5502_muic_info *info = i2c_get_clientdata(i2c); + + disable_irq_wake(info->irq); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(sm5502_muic_pm_ops, + sm5502_muic_suspend, sm5502_muic_resume); + +static const struct i2c_device_id sm5502_i2c_id[] = { + { "sm5502", (kernel_ulong_t)&sm5502_data }, + { "sm5504", (kernel_ulong_t)&sm5504_data }, + { "sm5703-muic", (kernel_ulong_t)&sm5502_data }, + { } +}; +MODULE_DEVICE_TABLE(i2c, sm5502_i2c_id); + +static struct i2c_driver sm5502_muic_i2c_driver = { + .driver = { + .name = "sm5502", + .pm = &sm5502_muic_pm_ops, + .of_match_table = sm5502_dt_match, + }, + .probe = sm5022_muic_i2c_probe, + .id_table = sm5502_i2c_id, +}; + +static int __init sm5502_muic_i2c_init(void) +{ + return i2c_add_driver(&sm5502_muic_i2c_driver); +} +subsys_initcall(sm5502_muic_i2c_init); + +MODULE_DESCRIPTION("Silicon Mitus SM5502 Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-sm5502.h b/drivers/extcon/extcon-sm5502.h new file mode 100644 index 000000000..9c04315d4 --- /dev/null +++ b/drivers/extcon/extcon-sm5502.h @@ -0,0 +1,354 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * sm5502.h + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + */ + +#ifndef __LINUX_EXTCON_SM5502_H +#define __LINUX_EXTCON_SM5502_H + +/* SM5502 registers */ +enum sm5502_reg { + SM5502_REG_DEVICE_ID = 0x01, + SM5502_REG_CONTROL, + SM5502_REG_INT1, + SM5502_REG_INT2, + SM5502_REG_INTMASK1, + SM5502_REG_INTMASK2, + SM5502_REG_ADC, + SM5502_REG_TIMING_SET1, + SM5502_REG_TIMING_SET2, + SM5502_REG_DEV_TYPE1, + SM5502_REG_DEV_TYPE2, + SM5502_REG_BUTTON1, + SM5502_REG_BUTTON2, + SM5502_REG_CAR_KIT_STATUS, + SM5502_REG_RSVD1, + SM5502_REG_RSVD2, + SM5502_REG_RSVD3, + SM5502_REG_RSVD4, + SM5502_REG_MANUAL_SW1, + SM5502_REG_MANUAL_SW2, + SM5502_REG_DEV_TYPE3, + SM5502_REG_RSVD5, + SM5502_REG_RSVD6, + SM5502_REG_RSVD7, + SM5502_REG_RSVD8, + SM5502_REG_RSVD9, + SM5502_REG_RESET, + SM5502_REG_RSVD10, + SM5502_REG_RESERVED_ID1, + SM5502_REG_RSVD11, + SM5502_REG_RSVD12, + SM5502_REG_RESERVED_ID2, + SM5502_REG_RSVD13, + SM5502_REG_OCP, + SM5502_REG_RSVD14, + SM5502_REG_RSVD15, + SM5502_REG_RSVD16, + SM5502_REG_RSVD17, + SM5502_REG_RSVD18, + SM5502_REG_RSVD19, + SM5502_REG_RSVD20, + SM5502_REG_RSVD21, + SM5502_REG_RSVD22, + SM5502_REG_RSVD23, + SM5502_REG_RSVD24, + SM5502_REG_RSVD25, + SM5502_REG_RSVD26, + SM5502_REG_RSVD27, + SM5502_REG_RSVD28, + SM5502_REG_RSVD29, + SM5502_REG_RSVD30, + SM5502_REG_RSVD31, + SM5502_REG_RSVD32, + SM5502_REG_RSVD33, + SM5502_REG_RSVD34, + SM5502_REG_RSVD35, + SM5502_REG_RSVD36, + SM5502_REG_RESERVED_ID3, + + SM5502_REG_END, +}; + +/* Define SM5502 MASK/SHIFT constant */ +#define SM5502_REG_DEVICE_ID_VENDOR_SHIFT 0 +#define SM5502_REG_DEVICE_ID_VERSION_SHIFT 3 +#define SM5502_REG_DEVICE_ID_VENDOR_MASK (0x3 << SM5502_REG_DEVICE_ID_VENDOR_SHIFT) +#define SM5502_REG_DEVICE_ID_VERSION_MASK (0x1f << SM5502_REG_DEVICE_ID_VERSION_SHIFT) + +#define SM5502_REG_CONTROL_MASK_INT_SHIFT 0 +#define SM5502_REG_CONTROL_WAIT_SHIFT 1 +#define SM5502_REG_CONTROL_MANUAL_SW_SHIFT 2 +#define SM5502_REG_CONTROL_RAW_DATA_SHIFT 3 +#define SM5502_REG_CONTROL_SW_OPEN_SHIFT 4 +#define SM5502_REG_CONTROL_MASK_INT_MASK (0x1 << SM5502_REG_CONTROL_MASK_INT_SHIFT) +#define SM5502_REG_CONTROL_WAIT_MASK (0x1 << SM5502_REG_CONTROL_WAIT_SHIFT) +#define SM5502_REG_CONTROL_MANUAL_SW_MASK (0x1 << SM5502_REG_CONTROL_MANUAL_SW_SHIFT) +#define SM5502_REG_CONTROL_RAW_DATA_MASK (0x1 << SM5502_REG_CONTROL_RAW_DATA_SHIFT) +#define SM5502_REG_CONTROL_SW_OPEN_MASK (0x1 << SM5502_REG_CONTROL_SW_OPEN_SHIFT) + +#define SM5504_REG_CONTROL_CHGTYP_SHIFT 5 +#define SM5504_REG_CONTROL_USBCHDEN_SHIFT 6 +#define SM5504_REG_CONTROL_ADC_EN_SHIFT 7 +#define SM5504_REG_CONTROL_CHGTYP_MASK (0x1 << SM5504_REG_CONTROL_CHGTYP_SHIFT) +#define SM5504_REG_CONTROL_USBCHDEN_MASK (0x1 << SM5504_REG_CONTROL_USBCHDEN_SHIFT) +#define SM5504_REG_CONTROL_ADC_EN_MASK (0x1 << SM5504_REG_CONTROL_ADC_EN_SHIFT) + +#define SM5502_REG_INTM1_ATTACH_SHIFT 0 +#define SM5502_REG_INTM1_DETACH_SHIFT 1 +#define SM5502_REG_INTM1_KP_SHIFT 2 +#define SM5502_REG_INTM1_LKP_SHIFT 3 +#define SM5502_REG_INTM1_LKR_SHIFT 4 +#define SM5502_REG_INTM1_OVP_EVENT_SHIFT 5 +#define SM5502_REG_INTM1_OCP_EVENT_SHIFT 6 +#define SM5502_REG_INTM1_OVP_OCP_DIS_SHIFT 7 +#define SM5502_REG_INTM1_ATTACH_MASK (0x1 << SM5502_REG_INTM1_ATTACH_SHIFT) +#define SM5502_REG_INTM1_DETACH_MASK (0x1 << SM5502_REG_INTM1_DETACH_SHIFT) +#define SM5502_REG_INTM1_KP_MASK (0x1 << SM5502_REG_INTM1_KP_SHIFT) +#define SM5502_REG_INTM1_LKP_MASK (0x1 << SM5502_REG_INTM1_LKP_SHIFT) +#define SM5502_REG_INTM1_LKR_MASK (0x1 << SM5502_REG_INTM1_LKR_SHIFT) +#define SM5502_REG_INTM1_OVP_EVENT_MASK (0x1 << SM5502_REG_INTM1_OVP_EVENT_SHIFT) +#define SM5502_REG_INTM1_OCP_EVENT_MASK (0x1 << SM5502_REG_INTM1_OCP_EVENT_SHIFT) +#define SM5502_REG_INTM1_OVP_OCP_DIS_MASK (0x1 << SM5502_REG_INTM1_OVP_OCP_DIS_SHIFT) + +#define SM5502_REG_INTM2_VBUS_DET_SHIFT 0 +#define SM5502_REG_INTM2_REV_ACCE_SHIFT 1 +#define SM5502_REG_INTM2_ADC_CHG_SHIFT 2 +#define SM5502_REG_INTM2_STUCK_KEY_SHIFT 3 +#define SM5502_REG_INTM2_STUCK_KEY_RCV_SHIFT 4 +#define SM5502_REG_INTM2_MHL_SHIFT 5 +#define SM5502_REG_INTM2_VBUS_DET_MASK (0x1 << SM5502_REG_INTM2_VBUS_DET_SHIFT) +#define SM5502_REG_INTM2_REV_ACCE_MASK (0x1 << SM5502_REG_INTM2_REV_ACCE_SHIFT) +#define SM5502_REG_INTM2_ADC_CHG_MASK (0x1 << SM5502_REG_INTM2_ADC_CHG_SHIFT) +#define SM5502_REG_INTM2_STUCK_KEY_MASK (0x1 << SM5502_REG_INTM2_STUCK_KEY_SHIFT) +#define SM5502_REG_INTM2_STUCK_KEY_RCV_MASK (0x1 << SM5502_REG_INTM2_STUCK_KEY_RCV_SHIFT) +#define SM5502_REG_INTM2_MHL_MASK (0x1 << SM5502_REG_INTM2_MHL_SHIFT) + +#define SM5504_REG_INTM1_ATTACH_SHIFT 0 +#define SM5504_REG_INTM1_DETACH_SHIFT 1 +#define SM5504_REG_INTM1_CHG_DET_SHIFT 2 +#define SM5504_REG_INTM1_DCD_OUT_SHIFT 3 +#define SM5504_REG_INTM1_OVP_EVENT_SHIFT 4 +#define SM5504_REG_INTM1_CONNECT_SHIFT 5 +#define SM5504_REG_INTM1_ADC_CHG_SHIFT 6 +#define SM5504_REG_INTM1_ATTACH_MASK (0x1 << SM5504_REG_INTM1_ATTACH_SHIFT) +#define SM5504_REG_INTM1_DETACH_MASK (0x1 << SM5504_REG_INTM1_DETACH_SHIFT) +#define SM5504_REG_INTM1_CHG_DET_MASK (0x1 << SM5504_REG_INTM1_CHG_DET_SHIFT) +#define SM5504_REG_INTM1_DCD_OUT_MASK (0x1 << SM5504_REG_INTM1_DCD_OUT_SHIFT) +#define SM5504_REG_INTM1_OVP_EVENT_MASK (0x1 << SM5504_REG_INTM1_OVP_EVENT_SHIFT) +#define SM5504_REG_INTM1_CONNECT_MASK (0x1 << SM5504_REG_INTM1_CONNECT_SHIFT) +#define SM5504_REG_INTM1_ADC_CHG_MASK (0x1 << SM5504_REG_INTM1_ADC_CHG_SHIFT) + +#define SM5504_REG_INTM2_RID_CHG_SHIFT 0 +#define SM5504_REG_INTM2_UVLO_SHIFT 1 +#define SM5504_REG_INTM2_POR_SHIFT 2 +#define SM5504_REG_INTM2_OVP_FET_SHIFT 4 +#define SM5504_REG_INTM2_OCP_LATCH_SHIFT 5 +#define SM5504_REG_INTM2_OCP_EVENT_SHIFT 6 +#define SM5504_REG_INTM2_OVP_OCP_EVENT_SHIFT 7 +#define SM5504_REG_INTM2_RID_CHG_MASK (0x1 << SM5504_REG_INTM2_RID_CHG_SHIFT) +#define SM5504_REG_INTM2_UVLO_MASK (0x1 << SM5504_REG_INTM2_UVLO_SHIFT) +#define SM5504_REG_INTM2_POR_MASK (0x1 << SM5504_REG_INTM2_POR_SHIFT) +#define SM5504_REG_INTM2_OVP_FET_MASK (0x1 << SM5504_REG_INTM2_OVP_FET_SHIFT) +#define SM5504_REG_INTM2_OCP_LATCH_MASK (0x1 << SM5504_REG_INTM2_OCP_LATCH_SHIFT) +#define SM5504_REG_INTM2_OCP_EVENT_MASK (0x1 << SM5504_REG_INTM2_OCP_EVENT_SHIFT) +#define SM5504_REG_INTM2_OVP_OCP_EVENT_MASK (0x1 << SM5504_REG_INTM2_OVP_OCP_EVENT_SHIFT) + +#define SM5502_REG_ADC_SHIFT 0 +#define SM5502_REG_ADC_MASK (0x1f << SM5502_REG_ADC_SHIFT) + +#define SM5502_REG_TIMING_SET1_KEY_PRESS_SHIFT 4 +#define SM5502_REG_TIMING_SET1_KEY_PRESS_MASK (0xf << SM5502_REG_TIMING_SET1_KEY_PRESS_SHIFT) +#define TIMING_KEY_PRESS_100MS 0x0 +#define TIMING_KEY_PRESS_200MS 0x1 +#define TIMING_KEY_PRESS_300MS 0x2 +#define TIMING_KEY_PRESS_400MS 0x3 +#define TIMING_KEY_PRESS_500MS 0x4 +#define TIMING_KEY_PRESS_600MS 0x5 +#define TIMING_KEY_PRESS_700MS 0x6 +#define TIMING_KEY_PRESS_800MS 0x7 +#define TIMING_KEY_PRESS_900MS 0x8 +#define TIMING_KEY_PRESS_1000MS 0x9 +#define SM5502_REG_TIMING_SET1_ADC_DET_SHIFT 0 +#define SM5502_REG_TIMING_SET1_ADC_DET_MASK (0xf << SM5502_REG_TIMING_SET1_ADC_DET_SHIFT) +#define TIMING_ADC_DET_50MS 0x0 +#define TIMING_ADC_DET_100MS 0x1 +#define TIMING_ADC_DET_150MS 0x2 +#define TIMING_ADC_DET_200MS 0x3 +#define TIMING_ADC_DET_300MS 0x4 +#define TIMING_ADC_DET_400MS 0x5 +#define TIMING_ADC_DET_500MS 0x6 +#define TIMING_ADC_DET_600MS 0x7 +#define TIMING_ADC_DET_700MS 0x8 +#define TIMING_ADC_DET_800MS 0x9 +#define TIMING_ADC_DET_900MS 0xA +#define TIMING_ADC_DET_1000MS 0xB + +#define SM5502_REG_TIMING_SET2_SW_WAIT_SHIFT 4 +#define SM5502_REG_TIMING_SET2_SW_WAIT_MASK (0xf << SM5502_REG_TIMING_SET2_SW_WAIT_SHIFT) +#define TIMING_SW_WAIT_10MS 0x0 +#define TIMING_SW_WAIT_30MS 0x1 +#define TIMING_SW_WAIT_50MS 0x2 +#define TIMING_SW_WAIT_70MS 0x3 +#define TIMING_SW_WAIT_90MS 0x4 +#define TIMING_SW_WAIT_110MS 0x5 +#define TIMING_SW_WAIT_130MS 0x6 +#define TIMING_SW_WAIT_150MS 0x7 +#define TIMING_SW_WAIT_170MS 0x8 +#define TIMING_SW_WAIT_190MS 0x9 +#define TIMING_SW_WAIT_210MS 0xA +#define SM5502_REG_TIMING_SET2_LONG_KEY_SHIFT 0 +#define SM5502_REG_TIMING_SET2_LONG_KEY_MASK (0xf << SM5502_REG_TIMING_SET2_LONG_KEY_SHIFT) +#define TIMING_LONG_KEY_300MS 0x0 +#define TIMING_LONG_KEY_400MS 0x1 +#define TIMING_LONG_KEY_500MS 0x2 +#define TIMING_LONG_KEY_600MS 0x3 +#define TIMING_LONG_KEY_700MS 0x4 +#define TIMING_LONG_KEY_800MS 0x5 +#define TIMING_LONG_KEY_900MS 0x6 +#define TIMING_LONG_KEY_1000MS 0x7 +#define TIMING_LONG_KEY_1100MS 0x8 +#define TIMING_LONG_KEY_1200MS 0x9 +#define TIMING_LONG_KEY_1300MS 0xA +#define TIMING_LONG_KEY_1400MS 0xB +#define TIMING_LONG_KEY_1500MS 0xC + +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE1_SHIFT 0 +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE2_SHIFT 1 +#define SM5502_REG_DEV_TYPE1_USB_SDP_SHIFT 2 +#define SM5502_REG_DEV_TYPE1_UART_SHIFT 3 +#define SM5502_REG_DEV_TYPE1_CAR_KIT_CHARGER_SHIFT 4 +#define SM5502_REG_DEV_TYPE1_USB_CHG_SHIFT 5 +#define SM5502_REG_DEV_TYPE1_DEDICATED_CHG_SHIFT 6 +#define SM5502_REG_DEV_TYPE1_USB_OTG_SHIFT 7 +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE1_MASK (0x1 << SM5502_REG_DEV_TYPE1_AUDIO_TYPE1_SHIFT) +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE1__MASK (0x1 << SM5502_REG_DEV_TYPE1_AUDIO_TYPE2_SHIFT) +#define SM5502_REG_DEV_TYPE1_USB_SDP_MASK (0x1 << SM5502_REG_DEV_TYPE1_USB_SDP_SHIFT) +#define SM5502_REG_DEV_TYPE1_UART_MASK (0x1 << SM5502_REG_DEV_TYPE1_UART_SHIFT) +#define SM5502_REG_DEV_TYPE1_CAR_KIT_CHARGER_MASK (0x1 << SM5502_REG_DEV_TYPE1_CAR_KIT_CHARGER_SHIFT) +#define SM5502_REG_DEV_TYPE1_USB_CHG_MASK (0x1 << SM5502_REG_DEV_TYPE1_USB_CHG_SHIFT) +#define SM5502_REG_DEV_TYPE1_DEDICATED_CHG_MASK (0x1 << SM5502_REG_DEV_TYPE1_DEDICATED_CHG_SHIFT) +#define SM5502_REG_DEV_TYPE1_USB_OTG_MASK (0x1 << SM5502_REG_DEV_TYPE1_USB_OTG_SHIFT) + +#define SM5504_REG_DEV_TYPE1_USB_OTG_SHIFT 0 +#define SM5504_REG_DEV_TYPE1_USB_OTG_MASK (0x1 << SM5504_REG_DEV_TYPE1_USB_OTG_SHIFT) + +#define SM5502_REG_DEV_TYPE2_JIG_USB_ON_SHIFT 0 +#define SM5502_REG_DEV_TYPE2_JIG_USB_OFF_SHIFT 1 +#define SM5502_REG_DEV_TYPE2_JIG_UART_ON_SHIFT 2 +#define SM5502_REG_DEV_TYPE2_JIG_UART_OFF_SHIFT 3 +#define SM5502_REG_DEV_TYPE2_PPD_SHIFT 4 +#define SM5502_REG_DEV_TYPE2_TTY_SHIFT 5 +#define SM5502_REG_DEV_TYPE2_AV_CABLE_SHIFT 6 +#define SM5502_REG_DEV_TYPE2_JIG_USB_ON_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_USB_ON_SHIFT) +#define SM5502_REG_DEV_TYPE2_JIG_USB_OFF_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_USB_OFF_SHIFT) +#define SM5502_REG_DEV_TYPE2_JIG_UART_ON_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_UART_ON_SHIFT) +#define SM5502_REG_DEV_TYPE2_JIG_UART_OFF_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_UART_OFF_SHIFT) +#define SM5502_REG_DEV_TYPE2_PPD_MASK (0x1 << SM5502_REG_DEV_TYPE2_PPD_SHIFT) +#define SM5502_REG_DEV_TYPE2_TTY_MASK (0x1 << SM5502_REG_DEV_TYPE2_TTY_SHIFT) +#define SM5502_REG_DEV_TYPE2_AV_CABLE_MASK (0x1 << SM5502_REG_DEV_TYPE2_AV_CABLE_SHIFT) + +#define SM5502_REG_MANUAL_SW1_VBUSIN_SHIFT 0 +#define SM5502_REG_MANUAL_SW1_DP_SHIFT 2 +#define SM5502_REG_MANUAL_SW1_DM_SHIFT 5 +#define SM5502_REG_MANUAL_SW1_VBUSIN_MASK (0x3 << SM5502_REG_MANUAL_SW1_VBUSIN_SHIFT) +#define SM5502_REG_MANUAL_SW1_DP_MASK (0x7 << SM5502_REG_MANUAL_SW1_DP_SHIFT) +#define SM5502_REG_MANUAL_SW1_DM_MASK (0x7 << SM5502_REG_MANUAL_SW1_DM_SHIFT) +#define VBUSIN_SWITCH_OPEN 0x0 +#define VBUSIN_SWITCH_VBUSOUT 0x1 +#define VBUSIN_SWITCH_MIC 0x2 +#define VBUSIN_SWITCH_VBUSOUT_WITH_USB 0x3 +#define DM_DP_CON_SWITCH_OPEN 0x0 +#define DM_DP_CON_SWITCH_USB 0x1 +#define DM_DP_CON_SWITCH_AUDIO 0x2 +#define DM_DP_CON_SWITCH_UART 0x3 +#define DM_DP_SWITCH_OPEN ((DM_DP_CON_SWITCH_OPEN <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_OPEN <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_USB ((DM_DP_CON_SWITCH_USB <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_USB <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_AUDIO ((DM_DP_CON_SWITCH_AUDIO <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_AUDIO <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_UART ((DM_DP_CON_SWITCH_UART <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_UART <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) + +#define SM5502_REG_RESET_MASK (0x1) + +/* SM5502 Interrupts */ +enum sm5502_irq { + /* INT1 */ + SM5502_IRQ_INT1_ATTACH, + SM5502_IRQ_INT1_DETACH, + SM5502_IRQ_INT1_KP, + SM5502_IRQ_INT1_LKP, + SM5502_IRQ_INT1_LKR, + SM5502_IRQ_INT1_OVP_EVENT, + SM5502_IRQ_INT1_OCP_EVENT, + SM5502_IRQ_INT1_OVP_OCP_DIS, + + /* INT2 */ + SM5502_IRQ_INT2_VBUS_DET, + SM5502_IRQ_INT2_REV_ACCE, + SM5502_IRQ_INT2_ADC_CHG, + SM5502_IRQ_INT2_STUCK_KEY, + SM5502_IRQ_INT2_STUCK_KEY_RCV, + SM5502_IRQ_INT2_MHL, + + SM5502_IRQ_NUM, +}; + +#define SM5502_IRQ_INT1_ATTACH_MASK BIT(0) +#define SM5502_IRQ_INT1_DETACH_MASK BIT(1) +#define SM5502_IRQ_INT1_KP_MASK BIT(2) +#define SM5502_IRQ_INT1_LKP_MASK BIT(3) +#define SM5502_IRQ_INT1_LKR_MASK BIT(4) +#define SM5502_IRQ_INT1_OVP_EVENT_MASK BIT(5) +#define SM5502_IRQ_INT1_OCP_EVENT_MASK BIT(6) +#define SM5502_IRQ_INT1_OVP_OCP_DIS_MASK BIT(7) +#define SM5502_IRQ_INT2_VBUS_DET_MASK BIT(0) +#define SM5502_IRQ_INT2_REV_ACCE_MASK BIT(1) +#define SM5502_IRQ_INT2_ADC_CHG_MASK BIT(2) +#define SM5502_IRQ_INT2_STUCK_KEY_MASK BIT(3) +#define SM5502_IRQ_INT2_STUCK_KEY_RCV_MASK BIT(4) +#define SM5502_IRQ_INT2_MHL_MASK BIT(5) + +/* SM5504 Interrupts */ +enum sm5504_irq { + /* INT1 */ + SM5504_IRQ_INT1_ATTACH, + SM5504_IRQ_INT1_DETACH, + SM5504_IRQ_INT1_CHG_DET, + SM5504_IRQ_INT1_DCD_OUT, + SM5504_IRQ_INT1_OVP_EVENT, + SM5504_IRQ_INT1_CONNECT, + SM5504_IRQ_INT1_ADC_CHG, + + /* INT2 */ + SM5504_IRQ_INT2_RID_CHG, + SM5504_IRQ_INT2_UVLO, + SM5504_IRQ_INT2_POR, + SM5504_IRQ_INT2_OVP_FET, + SM5504_IRQ_INT2_OCP_LATCH, + SM5504_IRQ_INT2_OCP_EVENT, + SM5504_IRQ_INT2_OVP_OCP_EVENT, + + SM5504_IRQ_NUM, +}; + +#define SM5504_IRQ_INT1_ATTACH_MASK BIT(0) +#define SM5504_IRQ_INT1_DETACH_MASK BIT(1) +#define SM5504_IRQ_INT1_CHG_DET_MASK BIT(2) +#define SM5504_IRQ_INT1_DCD_OUT_MASK BIT(3) +#define SM5504_IRQ_INT1_OVP_MASK BIT(4) +#define SM5504_IRQ_INT1_CONNECT_MASK BIT(5) +#define SM5504_IRQ_INT1_ADC_CHG_MASK BIT(6) +#define SM5504_IRQ_INT2_RID_CHG_MASK BIT(0) +#define SM5504_IRQ_INT2_UVLO_MASK BIT(1) +#define SM5504_IRQ_INT2_POR_MASK BIT(2) +#define SM5504_IRQ_INT2_OVP_FET_MASK BIT(4) +#define SM5504_IRQ_INT2_OCP_LATCH_MASK BIT(5) +#define SM5504_IRQ_INT2_OCP_EVENT_MASK BIT(6) +#define SM5504_IRQ_INT2_OVP_OCP_EVENT_MASK BIT(7) + +#endif /* __LINUX_EXTCON_SM5502_H */ diff --git a/drivers/extcon/extcon-usb-gpio.c b/drivers/extcon/extcon-usb-gpio.c new file mode 100644 index 000000000..e35fd1f69 --- /dev/null +++ b/drivers/extcon/extcon-usb-gpio.c @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/extcon/extcon-usb-gpio.c - USB GPIO extcon driver + * + * Copyright (C) 2015 Texas Instruments Incorporated - https://www.ti.com + * Author: Roger Quadros <rogerq@ti.com> + */ + +#include <linux/extcon-provider.h> +#include <linux/gpio/consumer.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/irq.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/workqueue.h> +#include <linux/pinctrl/consumer.h> + +#define USB_GPIO_DEBOUNCE_MS 20 /* ms */ + +struct usb_extcon_info { + struct device *dev; + struct extcon_dev *edev; + + struct gpio_desc *id_gpiod; + struct gpio_desc *vbus_gpiod; + int id_irq; + int vbus_irq; + + unsigned long debounce_jiffies; + struct delayed_work wq_detcable; +}; + +static const unsigned int usb_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +/* + * "USB" = VBUS and "USB-HOST" = !ID, so we have: + * Both "USB" and "USB-HOST" can't be set as active at the + * same time so if "USB-HOST" is active (i.e. ID is 0) we keep "USB" inactive + * even if VBUS is on. + * + * State | ID | VBUS + * ---------------------------------------- + * [1] USB | H | H + * [2] none | H | L + * [3] USB-HOST | L | H + * [4] USB-HOST | L | L + * + * In case we have only one of these signals: + * - VBUS only - we want to distinguish between [1] and [2], so ID is always 1. + * - ID only - we want to distinguish between [1] and [4], so VBUS = ID. +*/ +static void usb_extcon_detect_cable(struct work_struct *work) +{ + int id, vbus; + struct usb_extcon_info *info = container_of(to_delayed_work(work), + struct usb_extcon_info, + wq_detcable); + + /* check ID and VBUS and update cable state */ + id = info->id_gpiod ? + gpiod_get_value_cansleep(info->id_gpiod) : 1; + vbus = info->vbus_gpiod ? + gpiod_get_value_cansleep(info->vbus_gpiod) : id; + + /* at first we clean states which are no longer active */ + if (id) + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, false); + if (!vbus) + extcon_set_state_sync(info->edev, EXTCON_USB, false); + + if (!id) { + extcon_set_state_sync(info->edev, EXTCON_USB_HOST, true); + } else { + if (vbus) + extcon_set_state_sync(info->edev, EXTCON_USB, true); + } +} + +static irqreturn_t usb_irq_handler(int irq, void *dev_id) +{ + struct usb_extcon_info *info = dev_id; + + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + info->debounce_jiffies); + + return IRQ_HANDLED; +} + +static int usb_extcon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + struct usb_extcon_info *info; + int ret; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = dev; + info->id_gpiod = devm_gpiod_get_optional(&pdev->dev, "id", GPIOD_IN); + info->vbus_gpiod = devm_gpiod_get_optional(&pdev->dev, "vbus", + GPIOD_IN); + + if (!info->id_gpiod && !info->vbus_gpiod) { + dev_err(dev, "failed to get gpios\n"); + return -ENODEV; + } + + if (IS_ERR(info->id_gpiod)) + return PTR_ERR(info->id_gpiod); + + if (IS_ERR(info->vbus_gpiod)) + return PTR_ERR(info->vbus_gpiod); + + info->edev = devm_extcon_dev_allocate(dev, usb_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + ret = devm_extcon_dev_register(dev, info->edev); + if (ret < 0) { + dev_err(dev, "failed to register extcon device\n"); + return ret; + } + + if (info->id_gpiod) + ret = gpiod_set_debounce(info->id_gpiod, + USB_GPIO_DEBOUNCE_MS * 1000); + if (!ret && info->vbus_gpiod) + ret = gpiod_set_debounce(info->vbus_gpiod, + USB_GPIO_DEBOUNCE_MS * 1000); + + if (ret < 0) + info->debounce_jiffies = msecs_to_jiffies(USB_GPIO_DEBOUNCE_MS); + + INIT_DELAYED_WORK(&info->wq_detcable, usb_extcon_detect_cable); + + if (info->id_gpiod) { + info->id_irq = gpiod_to_irq(info->id_gpiod); + if (info->id_irq < 0) { + dev_err(dev, "failed to get ID IRQ\n"); + return info->id_irq; + } + + ret = devm_request_threaded_irq(dev, info->id_irq, NULL, + usb_irq_handler, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + pdev->name, info); + if (ret < 0) { + dev_err(dev, "failed to request handler for ID IRQ\n"); + return ret; + } + } + + if (info->vbus_gpiod) { + info->vbus_irq = gpiod_to_irq(info->vbus_gpiod); + if (info->vbus_irq < 0) { + dev_err(dev, "failed to get VBUS IRQ\n"); + return info->vbus_irq; + } + + ret = devm_request_threaded_irq(dev, info->vbus_irq, NULL, + usb_irq_handler, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + pdev->name, info); + if (ret < 0) { + dev_err(dev, "failed to request handler for VBUS IRQ\n"); + return ret; + } + } + + platform_set_drvdata(pdev, info); + device_set_wakeup_capable(&pdev->dev, true); + + /* Perform initial detection */ + usb_extcon_detect_cable(&info->wq_detcable.work); + + return 0; +} + +static void usb_extcon_remove(struct platform_device *pdev) +{ + struct usb_extcon_info *info = platform_get_drvdata(pdev); + + cancel_delayed_work_sync(&info->wq_detcable); + device_init_wakeup(&pdev->dev, false); +} + +#ifdef CONFIG_PM_SLEEP +static int usb_extcon_suspend(struct device *dev) +{ + struct usb_extcon_info *info = dev_get_drvdata(dev); + int ret = 0; + + if (device_may_wakeup(dev)) { + if (info->id_gpiod) { + ret = enable_irq_wake(info->id_irq); + if (ret) + return ret; + } + if (info->vbus_gpiod) { + ret = enable_irq_wake(info->vbus_irq); + if (ret) { + if (info->id_gpiod) + disable_irq_wake(info->id_irq); + + return ret; + } + } + } + + if (!device_may_wakeup(dev)) + pinctrl_pm_select_sleep_state(dev); + + return ret; +} + +static int usb_extcon_resume(struct device *dev) +{ + struct usb_extcon_info *info = dev_get_drvdata(dev); + int ret = 0; + + if (!device_may_wakeup(dev)) + pinctrl_pm_select_default_state(dev); + + if (device_may_wakeup(dev)) { + if (info->id_gpiod) { + ret = disable_irq_wake(info->id_irq); + if (ret) + return ret; + } + if (info->vbus_gpiod) { + ret = disable_irq_wake(info->vbus_irq); + if (ret) { + if (info->id_gpiod) + enable_irq_wake(info->id_irq); + + return ret; + } + } + } + + queue_delayed_work(system_power_efficient_wq, + &info->wq_detcable, 0); + + return ret; +} +#endif + +static SIMPLE_DEV_PM_OPS(usb_extcon_pm_ops, + usb_extcon_suspend, usb_extcon_resume); + +static const struct of_device_id usb_extcon_dt_match[] = { + { .compatible = "linux,extcon-usb-gpio", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, usb_extcon_dt_match); + +static const struct platform_device_id usb_extcon_platform_ids[] = { + { .name = "extcon-usb-gpio", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(platform, usb_extcon_platform_ids); + +static struct platform_driver usb_extcon_driver = { + .probe = usb_extcon_probe, + .remove = usb_extcon_remove, + .driver = { + .name = "extcon-usb-gpio", + .pm = &usb_extcon_pm_ops, + .of_match_table = usb_extcon_dt_match, + }, + .id_table = usb_extcon_platform_ids, +}; + +module_platform_driver(usb_extcon_driver); + +MODULE_AUTHOR("Roger Quadros <rogerq@ti.com>"); +MODULE_DESCRIPTION("USB GPIO extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-usbc-cros-ec.c b/drivers/extcon/extcon-usbc-cros-ec.c new file mode 100644 index 000000000..a2ef710b0 --- /dev/null +++ b/drivers/extcon/extcon-usbc-cros-ec.c @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: GPL-2.0 +// ChromeOS Embedded Controller extcon +// +// Copyright (C) 2017 Google, Inc. +// Author: Benson Leung <bleung@chromium.org> + +#include <linux/extcon-provider.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of.h> +#include <linux/platform_data/cros_ec_commands.h> +#include <linux/platform_data/cros_ec_proto.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/sched.h> + +struct cros_ec_extcon_info { + struct device *dev; + struct extcon_dev *edev; + + int port_id; + + struct cros_ec_device *ec; + + struct notifier_block notifier; + + unsigned int dr; /* data role */ + bool pr; /* power role (true if VBUS enabled) */ + bool dp; /* DisplayPort enabled */ + bool mux; /* SuperSpeed (usb3) enabled */ + unsigned int power_type; +}; + +static const unsigned int usb_type_c_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_DISP_DP, + EXTCON_NONE, +}; + +enum usb_data_roles { + DR_NONE, + DR_HOST, + DR_DEVICE, +}; + +/** + * cros_ec_pd_command() - Send a command to the EC. + * @info: pointer to struct cros_ec_extcon_info + * @command: EC command + * @version: EC command version + * @outdata: EC command output data + * @outsize: Size of outdata + * @indata: EC command input data + * @insize: Size of indata + * + * Return: 0 on success, <0 on failure. + */ +static int cros_ec_pd_command(struct cros_ec_extcon_info *info, + unsigned int command, + unsigned int version, + void *outdata, + unsigned int outsize, + void *indata, + unsigned int insize) +{ + struct cros_ec_command *msg; + int ret; + + msg = kzalloc_flex(*msg, data, max(outsize, insize)); + if (!msg) + return -ENOMEM; + + msg->version = version; + msg->command = command; + msg->outsize = outsize; + msg->insize = insize; + + if (outsize) + memcpy(msg->data, outdata, outsize); + + ret = cros_ec_cmd_xfer_status(info->ec, msg); + if (ret >= 0 && insize) + memcpy(indata, msg->data, insize); + + kfree(msg); + return ret; +} + +/** + * cros_ec_usb_get_power_type() - Get power type info about PD device attached + * to given port. + * @info: pointer to struct cros_ec_extcon_info + * + * Return: power type on success, <0 on failure. + */ +static int cros_ec_usb_get_power_type(struct cros_ec_extcon_info *info) +{ + struct ec_params_usb_pd_power_info req; + struct ec_response_usb_pd_power_info resp; + int ret; + + req.port = info->port_id; + ret = cros_ec_pd_command(info, EC_CMD_USB_PD_POWER_INFO, 0, + &req, sizeof(req), &resp, sizeof(resp)); + if (ret < 0) + return ret; + + return resp.type; +} + +/** + * cros_ec_usb_get_pd_mux_state() - Get PD mux state for given port. + * @info: pointer to struct cros_ec_extcon_info + * + * Return: PD mux state on success, <0 on failure. + */ +static int cros_ec_usb_get_pd_mux_state(struct cros_ec_extcon_info *info) +{ + struct ec_params_usb_pd_mux_info req; + struct ec_response_usb_pd_mux_info resp; + int ret; + + req.port = info->port_id; + ret = cros_ec_pd_command(info, EC_CMD_USB_PD_MUX_INFO, 0, + &req, sizeof(req), + &resp, sizeof(resp)); + if (ret < 0) + return ret; + + return resp.flags; +} + +/** + * cros_ec_usb_get_role() - Get role info about possible PD device attached to a + * given port. + * @info: pointer to struct cros_ec_extcon_info + * @polarity: pointer to cable polarity (return value) + * + * Return: role info on success, -ENOTCONN if no cable is connected, <0 on + * failure. + */ +static int cros_ec_usb_get_role(struct cros_ec_extcon_info *info, + bool *polarity) +{ + struct ec_params_usb_pd_control pd_control; + struct ec_response_usb_pd_control_v1 resp; + int ret; + + pd_control.port = info->port_id; + pd_control.role = USB_PD_CTRL_ROLE_NO_CHANGE; + pd_control.mux = USB_PD_CTRL_MUX_NO_CHANGE; + pd_control.swap = USB_PD_CTRL_SWAP_NONE; + ret = cros_ec_pd_command(info, EC_CMD_USB_PD_CONTROL, 1, + &pd_control, sizeof(pd_control), + &resp, sizeof(resp)); + if (ret < 0) + return ret; + + if (!(resp.enabled & PD_CTRL_RESP_ENABLED_CONNECTED)) + return -ENOTCONN; + + *polarity = resp.polarity; + + return resp.role; +} + +/** + * cros_ec_pd_get_num_ports() - Get number of EC charge ports. + * @info: pointer to struct cros_ec_extcon_info + * + * Return: number of ports on success, <0 on failure. + */ +static int cros_ec_pd_get_num_ports(struct cros_ec_extcon_info *info) +{ + struct ec_response_usb_pd_ports resp; + int ret; + + ret = cros_ec_pd_command(info, EC_CMD_USB_PD_PORTS, + 0, NULL, 0, &resp, sizeof(resp)); + if (ret < 0) + return ret; + + return resp.num_ports; +} + +static const char *cros_ec_usb_role_string(unsigned int role) +{ + return role == DR_NONE ? "DISCONNECTED" : + (role == DR_HOST ? "DFP" : "UFP"); +} + +static const char *cros_ec_usb_power_type_string(unsigned int type) +{ + switch (type) { + case USB_CHG_TYPE_NONE: + return "USB_CHG_TYPE_NONE"; + case USB_CHG_TYPE_PD: + return "USB_CHG_TYPE_PD"; + case USB_CHG_TYPE_PROPRIETARY: + return "USB_CHG_TYPE_PROPRIETARY"; + case USB_CHG_TYPE_C: + return "USB_CHG_TYPE_C"; + case USB_CHG_TYPE_BC12_DCP: + return "USB_CHG_TYPE_BC12_DCP"; + case USB_CHG_TYPE_BC12_CDP: + return "USB_CHG_TYPE_BC12_CDP"; + case USB_CHG_TYPE_BC12_SDP: + return "USB_CHG_TYPE_BC12_SDP"; + case USB_CHG_TYPE_OTHER: + return "USB_CHG_TYPE_OTHER"; + case USB_CHG_TYPE_VBUS: + return "USB_CHG_TYPE_VBUS"; + case USB_CHG_TYPE_UNKNOWN: + return "USB_CHG_TYPE_UNKNOWN"; + default: + return "USB_CHG_TYPE_UNKNOWN"; + } +} + +static bool cros_ec_usb_power_type_is_wall_wart(unsigned int type, + unsigned int role) +{ + switch (type) { + /* FIXME : Guppy, Donnettes, and other chargers will be miscategorized + * because they identify with USB_CHG_TYPE_C, but we can't return true + * here from that code because that breaks Suzy-Q and other kinds of + * USB Type-C cables and peripherals. + */ + case USB_CHG_TYPE_PROPRIETARY: + case USB_CHG_TYPE_BC12_DCP: + return true; + case USB_CHG_TYPE_PD: + case USB_CHG_TYPE_C: + case USB_CHG_TYPE_BC12_CDP: + case USB_CHG_TYPE_BC12_SDP: + case USB_CHG_TYPE_OTHER: + case USB_CHG_TYPE_VBUS: + case USB_CHG_TYPE_UNKNOWN: + case USB_CHG_TYPE_NONE: + default: + return false; + } +} + +static int extcon_cros_ec_detect_cable(struct cros_ec_extcon_info *info, + bool force) +{ + struct device *dev = info->dev; + int role, power_type; + unsigned int dr = DR_NONE; + bool pr = false; + bool polarity = false; + bool dp = false; + bool mux = false; + bool hpd = false; + + power_type = cros_ec_usb_get_power_type(info); + if (power_type < 0) { + dev_err(dev, "failed getting power type err = %d\n", + power_type); + return power_type; + } + + role = cros_ec_usb_get_role(info, &polarity); + if (role < 0) { + if (role != -ENOTCONN) { + dev_err(dev, "failed getting role err = %d\n", role); + return role; + } + dev_dbg(dev, "disconnected\n"); + } else { + int pd_mux_state; + + dr = (role & PD_CTRL_RESP_ROLE_DATA) ? DR_HOST : DR_DEVICE; + pr = (role & PD_CTRL_RESP_ROLE_POWER); + pd_mux_state = cros_ec_usb_get_pd_mux_state(info); + if (pd_mux_state < 0) + pd_mux_state = USB_PD_MUX_USB_ENABLED; + + dp = pd_mux_state & USB_PD_MUX_DP_ENABLED; + mux = pd_mux_state & USB_PD_MUX_USB_ENABLED; + hpd = pd_mux_state & USB_PD_MUX_HPD_IRQ; + + dev_dbg(dev, + "connected role 0x%x pwr type %d dr %d pr %d pol %d mux %d dp %d hpd %d\n", + role, power_type, dr, pr, polarity, mux, dp, hpd); + } + + /* + * When there is no USB host (e.g. USB PD charger), + * we are not really a UFP for the AP. + */ + if (dr == DR_DEVICE && + cros_ec_usb_power_type_is_wall_wart(power_type, role)) + dr = DR_NONE; + + if (force || info->dr != dr || info->pr != pr || info->dp != dp || + info->mux != mux || info->power_type != power_type) { + bool host_connected = false, device_connected = false; + + dev_dbg(dev, "Type/Role switch! type = %s role = %s\n", + cros_ec_usb_power_type_string(power_type), + cros_ec_usb_role_string(dr)); + info->dr = dr; + info->pr = pr; + info->dp = dp; + info->mux = mux; + info->power_type = power_type; + + if (dr == DR_DEVICE) + device_connected = true; + else if (dr == DR_HOST) + host_connected = true; + + extcon_set_state(info->edev, EXTCON_USB, device_connected); + extcon_set_state(info->edev, EXTCON_USB_HOST, host_connected); + extcon_set_state(info->edev, EXTCON_DISP_DP, dp); + extcon_set_property(info->edev, EXTCON_USB, + EXTCON_PROP_USB_VBUS, + (union extcon_property_value)(int)pr); + extcon_set_property(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_VBUS, + (union extcon_property_value)(int)pr); + extcon_set_property(info->edev, EXTCON_USB, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)(int)polarity); + extcon_set_property(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)(int)polarity); + extcon_set_property(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)(int)polarity); + extcon_set_property(info->edev, EXTCON_USB, + EXTCON_PROP_USB_SS, + (union extcon_property_value)(int)mux); + extcon_set_property(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_SS, + (union extcon_property_value)(int)mux); + extcon_set_property(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_USB_SS, + (union extcon_property_value)(int)mux); + extcon_set_property(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_DISP_HPD, + (union extcon_property_value)(int)hpd); + + extcon_sync(info->edev, EXTCON_USB); + extcon_sync(info->edev, EXTCON_USB_HOST); + extcon_sync(info->edev, EXTCON_DISP_DP); + + } else if (hpd) { + extcon_set_property(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_DISP_HPD, + (union extcon_property_value)(int)hpd); + extcon_sync(info->edev, EXTCON_DISP_DP); + } + + return 0; +} + +static int extcon_cros_ec_event(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *_notify) +{ + struct cros_ec_extcon_info *info; + struct cros_ec_device *ec; + u32 host_event; + + info = container_of(nb, struct cros_ec_extcon_info, notifier); + ec = info->ec; + + host_event = cros_ec_get_host_event(ec); + if (host_event & (EC_HOST_EVENT_MASK(EC_HOST_EVENT_PD_MCU) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_USB_MUX))) { + extcon_cros_ec_detect_cable(info, false); + return NOTIFY_OK; + } + + return NOTIFY_DONE; +} + +static int extcon_cros_ec_probe(struct platform_device *pdev) +{ + struct cros_ec_extcon_info *info; + struct cros_ec_device *ec = dev_get_drvdata(pdev->dev.parent); + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + int numports, ret; + + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = dev; + info->ec = ec; + + if (np) { + u32 port; + + ret = of_property_read_u32(np, "google,usb-port-id", &port); + if (ret < 0) { + dev_err(dev, "Missing google,usb-port-id property\n"); + return ret; + } + info->port_id = port; + } else { + info->port_id = pdev->id; + } + + numports = cros_ec_pd_get_num_ports(info); + if (numports < 0) { + dev_err(dev, "failed getting number of ports! ret = %d\n", + numports); + return numports; + } + + if (info->port_id >= numports) { + dev_err(dev, "This system only supports %d ports\n", numports); + return -ENODEV; + } + + info->edev = devm_extcon_dev_allocate(dev, usb_type_c_cable); + if (IS_ERR(info->edev)) { + dev_err(dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + ret = devm_extcon_dev_register(dev, info->edev); + if (ret < 0) { + dev_err(dev, "failed to register extcon device\n"); + return ret; + } + + extcon_set_property_capability(info->edev, EXTCON_USB, + EXTCON_PROP_USB_VBUS); + extcon_set_property_capability(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_VBUS); + extcon_set_property_capability(info->edev, EXTCON_USB, + EXTCON_PROP_USB_TYPEC_POLARITY); + extcon_set_property_capability(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY); + extcon_set_property_capability(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_USB_TYPEC_POLARITY); + extcon_set_property_capability(info->edev, EXTCON_USB, + EXTCON_PROP_USB_SS); + extcon_set_property_capability(info->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_SS); + extcon_set_property_capability(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_USB_SS); + extcon_set_property_capability(info->edev, EXTCON_DISP_DP, + EXTCON_PROP_DISP_HPD); + + info->dr = DR_NONE; + info->pr = false; + + platform_set_drvdata(pdev, info); + + /* Get PD events from the EC */ + info->notifier.notifier_call = extcon_cros_ec_event; + ret = blocking_notifier_chain_register(&info->ec->event_notifier, + &info->notifier); + if (ret < 0) { + dev_err(dev, "failed to register notifier\n"); + return ret; + } + + /* Perform initial detection */ + ret = extcon_cros_ec_detect_cable(info, true); + if (ret < 0) { + dev_err(dev, "failed to detect initial cable state\n"); + goto unregister_notifier; + } + + return 0; + +unregister_notifier: + blocking_notifier_chain_unregister(&info->ec->event_notifier, + &info->notifier); + return ret; +} + +static void extcon_cros_ec_remove(struct platform_device *pdev) +{ + struct cros_ec_extcon_info *info = platform_get_drvdata(pdev); + + blocking_notifier_chain_unregister(&info->ec->event_notifier, + &info->notifier); +} + +#ifdef CONFIG_PM_SLEEP +static int extcon_cros_ec_suspend(struct device *dev) +{ + return 0; +} + +static int extcon_cros_ec_resume(struct device *dev) +{ + int ret; + struct cros_ec_extcon_info *info = dev_get_drvdata(dev); + + ret = extcon_cros_ec_detect_cable(info, true); + if (ret < 0) + dev_err(dev, "failed to detect cable state on resume\n"); + + return 0; +} + +static const struct dev_pm_ops extcon_cros_ec_dev_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(extcon_cros_ec_suspend, extcon_cros_ec_resume) +}; + +#define DEV_PM_OPS (&extcon_cros_ec_dev_pm_ops) +#else +#define DEV_PM_OPS NULL +#endif /* CONFIG_PM_SLEEP */ + +#ifdef CONFIG_OF +static const struct of_device_id extcon_cros_ec_of_match[] = { + { .compatible = "google,extcon-usbc-cros-ec" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, extcon_cros_ec_of_match); +#endif /* CONFIG_OF */ + +static struct platform_driver extcon_cros_ec_driver = { + .driver = { + .name = "extcon-usbc-cros-ec", + .of_match_table = of_match_ptr(extcon_cros_ec_of_match), + .pm = DEV_PM_OPS, + }, + .remove = extcon_cros_ec_remove, + .probe = extcon_cros_ec_probe, +}; + +module_platform_driver(extcon_cros_ec_driver); + +MODULE_DESCRIPTION("ChromeOS Embedded Controller extcon driver"); +MODULE_AUTHOR("Benson Leung <bleung@chromium.org>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-usbc-tusb320.c b/drivers/extcon/extcon-usbc-tusb320.c new file mode 100644 index 000000000..920b03421 --- /dev/null +++ b/drivers/extcon/extcon-usbc-tusb320.c @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * drivers/extcon/extcon-tusb320.c - TUSB320 extcon driver + * + * Copyright (C) 2020 National Instruments Corporation + * Author: Michael Auchter <michael.auchter@ni.com> + */ + +#include <linux/bitfield.h> +#include <linux/extcon-provider.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/regmap.h> +#include <linux/usb/typec.h> +#include <linux/usb/typec_altmode.h> +#include <linux/usb/role.h> +#include <linux/irq.h> + +#define TUSB320_REG8 0x8 +#define TUSB320_REG8_CURRENT_MODE_ADVERTISE GENMASK(7, 6) +#define TUSB320_REG8_CURRENT_MODE_ADVERTISE_USB 0x0 +#define TUSB320_REG8_CURRENT_MODE_ADVERTISE_15A 0x1 +#define TUSB320_REG8_CURRENT_MODE_ADVERTISE_30A 0x2 +#define TUSB320_REG8_CURRENT_MODE_DETECT GENMASK(5, 4) +#define TUSB320_REG8_CURRENT_MODE_DETECT_DEF 0x0 +#define TUSB320_REG8_CURRENT_MODE_DETECT_MED 0x1 +#define TUSB320_REG8_CURRENT_MODE_DETECT_ACC 0x2 +#define TUSB320_REG8_CURRENT_MODE_DETECT_HI 0x3 +#define TUSB320_REG8_ACCESSORY_CONNECTED GENMASK(3, 1) +#define TUSB320_REG8_ACCESSORY_CONNECTED_NONE 0x0 +#define TUSB320_REG8_ACCESSORY_CONNECTED_AUDIO 0x4 +#define TUSB320_REG8_ACCESSORY_CONNECTED_ACHRG 0x5 +#define TUSB320_REG8_ACCESSORY_CONNECTED_DBGDFP 0x6 +#define TUSB320_REG8_ACCESSORY_CONNECTED_DBGUFP 0x7 +#define TUSB320_REG8_ACTIVE_CABLE_DETECTION BIT(0) + +#define TUSB320_REG9 0x9 +#define TUSB320_REG9_ATTACHED_STATE GENMASK(7, 6) +#define TUSB320_REG9_CABLE_DIRECTION BIT(5) +#define TUSB320_REG9_INTERRUPT_STATUS BIT(4) + +#define TUSB320_REGA 0xa +#define TUSB320L_REGA_DISABLE_TERM BIT(0) +#define TUSB320_REGA_I2C_SOFT_RESET BIT(3) +#define TUSB320_REGA_MODE_SELECT_SHIFT 4 +#define TUSB320_REGA_MODE_SELECT_MASK 0x3 + +#define TUSB320L_REGA0_REVISION 0xa0 + +enum tusb320_attached_state { + TUSB320_ATTACHED_STATE_NONE, + TUSB320_ATTACHED_STATE_DFP, + TUSB320_ATTACHED_STATE_UFP, + TUSB320_ATTACHED_STATE_ACC, +}; + +enum tusb320_mode { + TUSB320_MODE_PORT, + TUSB320_MODE_UFP, + TUSB320_MODE_DFP, + TUSB320_MODE_DRP, +}; + +struct tusb320_priv; + +struct tusb320_ops { + int (*set_mode)(struct tusb320_priv *priv, enum tusb320_mode mode); + int (*get_revision)(struct tusb320_priv *priv, unsigned int *revision); +}; + +struct tusb320_priv { + struct device *dev; + struct regmap *regmap; + struct extcon_dev *edev; + struct tusb320_ops *ops; + enum tusb320_attached_state state; + struct typec_port *port; + struct typec_capability cap; + enum typec_port_type port_type; + enum typec_pwr_opmode pwr_opmode; + struct fwnode_handle *connector_fwnode; + struct usb_role_switch *role_sw; +}; + +static const char * const tusb_attached_states[] = { + [TUSB320_ATTACHED_STATE_NONE] = "not attached", + [TUSB320_ATTACHED_STATE_DFP] = "downstream facing port", + [TUSB320_ATTACHED_STATE_UFP] = "upstream facing port", + [TUSB320_ATTACHED_STATE_ACC] = "accessory", +}; + +static const unsigned int tusb320_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +static int tusb320_check_signature(struct tusb320_priv *priv) +{ + static const char sig[] = { '\0', 'T', 'U', 'S', 'B', '3', '2', '0' }; + unsigned val; + int i, ret; + + for (i = 0; i < sizeof(sig); i++) { + ret = regmap_read(priv->regmap, sizeof(sig) - 1 - i, &val); + if (ret < 0) + return ret; + if (val != sig[i]) { + dev_err(priv->dev, "signature mismatch!\n"); + return -ENODEV; + } + } + + return 0; +} + +static int tusb320_set_mode(struct tusb320_priv *priv, enum tusb320_mode mode) +{ + int ret; + + /* Mode cannot be changed while cable is attached */ + if (priv->state != TUSB320_ATTACHED_STATE_NONE) + return -EBUSY; + + /* Write mode */ + ret = regmap_write_bits(priv->regmap, TUSB320_REGA, + TUSB320_REGA_MODE_SELECT_MASK << TUSB320_REGA_MODE_SELECT_SHIFT, + mode << TUSB320_REGA_MODE_SELECT_SHIFT); + if (ret) { + dev_err(priv->dev, "failed to write mode: %d\n", ret); + return ret; + } + + return 0; +} + +static int tusb320l_set_mode(struct tusb320_priv *priv, enum tusb320_mode mode) +{ + int ret; + + /* Disable CC state machine */ + ret = regmap_write_bits(priv->regmap, TUSB320_REGA, + TUSB320L_REGA_DISABLE_TERM, 1); + if (ret) { + dev_err(priv->dev, + "failed to disable CC state machine: %d\n", ret); + return ret; + } + + /* Write mode */ + ret = regmap_write_bits(priv->regmap, TUSB320_REGA, + TUSB320_REGA_MODE_SELECT_MASK << TUSB320_REGA_MODE_SELECT_SHIFT, + mode << TUSB320_REGA_MODE_SELECT_SHIFT); + if (ret) { + dev_err(priv->dev, "failed to write mode: %d\n", ret); + goto err; + } + + msleep(5); +err: + /* Re-enable CC state machine */ + ret = regmap_write_bits(priv->regmap, TUSB320_REGA, + TUSB320L_REGA_DISABLE_TERM, 0); + if (ret) + dev_err(priv->dev, + "failed to re-enable CC state machine: %d\n", ret); + + return ret; +} + +static int tusb320_reset(struct tusb320_priv *priv) +{ + int ret; + + /* Set mode to default (follow PORT pin) */ + ret = priv->ops->set_mode(priv, TUSB320_MODE_PORT); + if (ret && ret != -EBUSY) { + dev_err(priv->dev, + "failed to set mode to PORT: %d\n", ret); + return ret; + } + + /* Perform soft reset */ + ret = regmap_write_bits(priv->regmap, TUSB320_REGA, + TUSB320_REGA_I2C_SOFT_RESET, 1); + if (ret) { + dev_err(priv->dev, + "failed to write soft reset bit: %d\n", ret); + return ret; + } + + /* Wait for chip to go through reset */ + msleep(95); + + return 0; +} + +static int tusb320l_get_revision(struct tusb320_priv *priv, unsigned int *revision) +{ + return regmap_read(priv->regmap, TUSB320L_REGA0_REVISION, revision); +} + +static struct tusb320_ops tusb320_ops = { + .set_mode = tusb320_set_mode, +}; + +static struct tusb320_ops tusb320l_ops = { + .set_mode = tusb320l_set_mode, + .get_revision = tusb320l_get_revision, +}; + +static int tusb320_set_adv_pwr_mode(struct tusb320_priv *priv) +{ + u8 mode; + + if (priv->pwr_opmode == TYPEC_PWR_MODE_USB) + mode = TUSB320_REG8_CURRENT_MODE_ADVERTISE_USB; + else if (priv->pwr_opmode == TYPEC_PWR_MODE_1_5A) + mode = TUSB320_REG8_CURRENT_MODE_ADVERTISE_15A; + else if (priv->pwr_opmode == TYPEC_PWR_MODE_3_0A) + mode = TUSB320_REG8_CURRENT_MODE_ADVERTISE_30A; + else /* No other mode is supported. */ + return -EINVAL; + + return regmap_write_bits(priv->regmap, TUSB320_REG8, + TUSB320_REG8_CURRENT_MODE_ADVERTISE, + FIELD_PREP(TUSB320_REG8_CURRENT_MODE_ADVERTISE, + mode)); +} + +static int tusb320_port_type_set(struct typec_port *port, + enum typec_port_type type) +{ + struct tusb320_priv *priv = typec_get_drvdata(port); + + if (type == TYPEC_PORT_SRC) + return priv->ops->set_mode(priv, TUSB320_MODE_DFP); + else if (type == TYPEC_PORT_SNK) + return priv->ops->set_mode(priv, TUSB320_MODE_UFP); + else if (type == TYPEC_PORT_DRP) + return priv->ops->set_mode(priv, TUSB320_MODE_DRP); + else + return priv->ops->set_mode(priv, TUSB320_MODE_PORT); +} + +static const struct typec_operations tusb320_typec_ops = { + .port_type_set = tusb320_port_type_set, +}; + +static void tusb320_extcon_irq_handler(struct tusb320_priv *priv, u8 reg) +{ + int state, polarity; + + state = FIELD_GET(TUSB320_REG9_ATTACHED_STATE, reg); + polarity = !!(reg & TUSB320_REG9_CABLE_DIRECTION); + + dev_dbg(priv->dev, "attached state: %s, polarity: %d\n", + tusb_attached_states[state], polarity); + + extcon_set_state(priv->edev, EXTCON_USB, + state == TUSB320_ATTACHED_STATE_UFP); + extcon_set_state(priv->edev, EXTCON_USB_HOST, + state == TUSB320_ATTACHED_STATE_DFP); + extcon_set_property(priv->edev, EXTCON_USB, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)polarity); + extcon_set_property(priv->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY, + (union extcon_property_value)polarity); + extcon_sync(priv->edev, EXTCON_USB); + extcon_sync(priv->edev, EXTCON_USB_HOST); + + priv->state = state; +} + +static void tusb320_typec_irq_handler(struct tusb320_priv *priv, u8 reg9) +{ + struct typec_port *port = priv->port; + struct device *dev = priv->dev; + int typec_mode; + enum usb_role usb_role; + enum typec_role pwr_role; + enum typec_data_role data_role; + u8 state, mode, accessory; + int ret, reg8; + bool ori; + + ret = regmap_read(priv->regmap, TUSB320_REG8, ®8); + if (ret) { + dev_err(dev, "error during reg8 i2c read, ret=%d!\n", ret); + return; + } + + ori = reg9 & TUSB320_REG9_CABLE_DIRECTION; + typec_set_orientation(port, ori ? TYPEC_ORIENTATION_REVERSE : + TYPEC_ORIENTATION_NORMAL); + + state = FIELD_GET(TUSB320_REG9_ATTACHED_STATE, reg9); + accessory = FIELD_GET(TUSB320_REG8_ACCESSORY_CONNECTED, reg8); + + switch (state) { + case TUSB320_ATTACHED_STATE_DFP: + typec_mode = TYPEC_MODE_USB2; + usb_role = USB_ROLE_HOST; + pwr_role = TYPEC_SOURCE; + data_role = TYPEC_HOST; + break; + case TUSB320_ATTACHED_STATE_UFP: + typec_mode = TYPEC_MODE_USB2; + usb_role = USB_ROLE_DEVICE; + pwr_role = TYPEC_SINK; + data_role = TYPEC_DEVICE; + break; + case TUSB320_ATTACHED_STATE_ACC: + /* + * Accessory detected. For debug accessories, just make some + * qualified guesses as to the role for lack of a better option. + */ + if (accessory == TUSB320_REG8_ACCESSORY_CONNECTED_AUDIO || + accessory == TUSB320_REG8_ACCESSORY_CONNECTED_ACHRG) { + typec_mode = TYPEC_MODE_AUDIO; + usb_role = USB_ROLE_NONE; + pwr_role = TYPEC_SINK; + data_role = TYPEC_DEVICE; + break; + } else if (accessory == + TUSB320_REG8_ACCESSORY_CONNECTED_DBGDFP) { + typec_mode = TYPEC_MODE_DEBUG; + pwr_role = TYPEC_SOURCE; + usb_role = USB_ROLE_HOST; + data_role = TYPEC_HOST; + break; + } else if (accessory == + TUSB320_REG8_ACCESSORY_CONNECTED_DBGUFP) { + typec_mode = TYPEC_MODE_DEBUG; + pwr_role = TYPEC_SINK; + usb_role = USB_ROLE_DEVICE; + data_role = TYPEC_DEVICE; + break; + } + + dev_warn(priv->dev, "unexpected ACCESSORY_CONNECTED state %d\n", + accessory); + + fallthrough; + default: + typec_mode = TYPEC_MODE_USB2; + usb_role = USB_ROLE_NONE; + pwr_role = TYPEC_SINK; + data_role = TYPEC_DEVICE; + break; + } + + typec_set_vconn_role(port, pwr_role); + typec_set_pwr_role(port, pwr_role); + typec_set_data_role(port, data_role); + typec_set_mode(port, typec_mode); + usb_role_switch_set_role(priv->role_sw, usb_role); + + mode = FIELD_GET(TUSB320_REG8_CURRENT_MODE_DETECT, reg8); + if (mode == TUSB320_REG8_CURRENT_MODE_DETECT_DEF) + typec_set_pwr_opmode(port, TYPEC_PWR_MODE_USB); + else if (mode == TUSB320_REG8_CURRENT_MODE_DETECT_MED) + typec_set_pwr_opmode(port, TYPEC_PWR_MODE_1_5A); + else if (mode == TUSB320_REG8_CURRENT_MODE_DETECT_HI) + typec_set_pwr_opmode(port, TYPEC_PWR_MODE_3_0A); + else /* Charge through accessory */ + typec_set_pwr_opmode(port, TYPEC_PWR_MODE_USB); +} + +static irqreturn_t tusb320_state_update_handler(struct tusb320_priv *priv, + bool force_update) +{ + unsigned int reg; + + if (regmap_read(priv->regmap, TUSB320_REG9, ®)) { + dev_err(priv->dev, "error during i2c read!\n"); + return IRQ_NONE; + } + + if (!force_update && !(reg & TUSB320_REG9_INTERRUPT_STATUS)) + return IRQ_NONE; + + tusb320_extcon_irq_handler(priv, reg); + + /* + * Type-C support is optional. Only call the Type-C handler if a + * port had been registered previously. + */ + if (priv->port) + tusb320_typec_irq_handler(priv, reg); + + regmap_write(priv->regmap, TUSB320_REG9, reg); + + return IRQ_HANDLED; +} + +static irqreturn_t tusb320_irq_handler(int irq, void *dev_id) +{ + struct tusb320_priv *priv = dev_id; + + return tusb320_state_update_handler(priv, false); +} + +static const struct regmap_config tusb320_regmap_config = { + .reg_bits = 8, + .val_bits = 8, +}; + +static int tusb320_extcon_probe(struct tusb320_priv *priv) +{ + int ret; + + priv->edev = devm_extcon_dev_allocate(priv->dev, tusb320_extcon_cable); + if (IS_ERR(priv->edev)) { + dev_err(priv->dev, "failed to allocate extcon device\n"); + return PTR_ERR(priv->edev); + } + + ret = devm_extcon_dev_register(priv->dev, priv->edev); + if (ret < 0) { + dev_err(priv->dev, "failed to register extcon device\n"); + return ret; + } + + extcon_set_property_capability(priv->edev, EXTCON_USB, + EXTCON_PROP_USB_TYPEC_POLARITY); + extcon_set_property_capability(priv->edev, EXTCON_USB_HOST, + EXTCON_PROP_USB_TYPEC_POLARITY); + + return 0; +} + +static int tusb320_typec_probe(struct i2c_client *client, + struct tusb320_priv *priv) +{ + struct fwnode_handle *connector; + const char *cap_str; + int ret; + + /* The Type-C connector is optional, for backward compatibility. */ + connector = device_get_named_child_node(&client->dev, "connector"); + if (!connector) + return 0; + + /* Type-C connector found. */ + ret = typec_get_fw_cap(&priv->cap, connector); + if (ret) + goto err_put; + + priv->port_type = priv->cap.type; + + /* This goes into register 0x8 field CURRENT_MODE_ADVERTISE */ + if (!fwnode_property_read_string(connector, "typec-power-opmode", + &cap_str)) { + ret = typec_find_pwr_opmode(cap_str); + if (ret < 0) + goto err_put; + priv->pwr_opmode = ret; + + /* Initialize the hardware with the devicetree settings. */ + ret = tusb320_set_adv_pwr_mode(priv); + if (ret) + goto err_put; + } + + priv->cap.revision = USB_TYPEC_REV_1_1; + priv->cap.accessory[0] = TYPEC_ACCESSORY_AUDIO; + priv->cap.accessory[1] = TYPEC_ACCESSORY_DEBUG; + priv->cap.orientation_aware = true; + priv->cap.driver_data = priv; + priv->cap.ops = &tusb320_typec_ops; + priv->cap.fwnode = connector; + + priv->port = typec_register_port(&client->dev, &priv->cap); + if (IS_ERR(priv->port)) { + ret = PTR_ERR(priv->port); + goto err_put; + } + + /* Find any optional USB role switch that needs reporting to */ + priv->role_sw = fwnode_usb_role_switch_get(connector); + if (IS_ERR(priv->role_sw)) { + ret = PTR_ERR(priv->role_sw); + goto err_unreg; + } + + priv->connector_fwnode = connector; + + return 0; + +err_unreg: + typec_unregister_port(priv->port); + +err_put: + fwnode_handle_put(connector); + + return ret; +} + +static void tusb320_typec_remove(struct tusb320_priv *priv) +{ + usb_role_switch_put(priv->role_sw); + typec_unregister_port(priv->port); + fwnode_handle_put(priv->connector_fwnode); +} + +static int tusb320_probe(struct i2c_client *client) +{ + struct tusb320_priv *priv; + const void *match_data; + unsigned int revision; + int ret; + u32 irq_trigger_type = IRQF_TRIGGER_FALLING; + struct irq_data *irq_d; + + priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = &client->dev; + i2c_set_clientdata(client, priv); + + priv->regmap = devm_regmap_init_i2c(client, &tusb320_regmap_config); + if (IS_ERR(priv->regmap)) + return PTR_ERR(priv->regmap); + + ret = tusb320_check_signature(priv); + if (ret) + return ret; + + match_data = device_get_match_data(&client->dev); + if (!match_data) + return -EINVAL; + + priv->ops = (struct tusb320_ops*)match_data; + + if (priv->ops->get_revision) { + ret = priv->ops->get_revision(priv, &revision); + if (ret) + dev_warn(priv->dev, + "failed to read revision register: %d\n", ret); + else + dev_info(priv->dev, "chip revision %d\n", revision); + } + + ret = tusb320_extcon_probe(priv); + if (ret) + return ret; + + ret = tusb320_typec_probe(client, priv); + if (ret) + return ret; + + /* update initial state */ + tusb320_state_update_handler(priv, true); + + /* Reset chip to its default state */ + ret = tusb320_reset(priv); + if (ret) + dev_warn(priv->dev, "failed to reset chip: %d\n", ret); + else + /* + * State and polarity might change after a reset, so update + * them again and make sure the interrupt status bit is cleared. + */ + tusb320_state_update_handler(priv, true); + + irq_d = irq_get_irq_data(client->irq); + if (irq_d) + irq_trigger_type = irqd_get_trigger_type(irq_d); + + ret = devm_request_threaded_irq(priv->dev, client->irq, NULL, + tusb320_irq_handler, + IRQF_ONESHOT | irq_trigger_type, + client->name, priv); + if (ret) + tusb320_typec_remove(priv); + + return ret; +} + +static void tusb320_remove(struct i2c_client *client) +{ + struct tusb320_priv *priv = i2c_get_clientdata(client); + + tusb320_typec_remove(priv); +} + +static const struct of_device_id tusb320_extcon_dt_match[] = { + { .compatible = "ti,tusb320", .data = &tusb320_ops, }, + { .compatible = "ti,tusb320l", .data = &tusb320l_ops, }, + { } +}; +MODULE_DEVICE_TABLE(of, tusb320_extcon_dt_match); + +static struct i2c_driver tusb320_extcon_driver = { + .probe = tusb320_probe, + .remove = tusb320_remove, + .driver = { + .name = "extcon-tusb320", + .of_match_table = tusb320_extcon_dt_match, + }, +}; + +static int __init tusb320_init(void) +{ + return i2c_add_driver(&tusb320_extcon_driver); +} +subsys_initcall(tusb320_init); + +static void __exit tusb320_exit(void) +{ + i2c_del_driver(&tusb320_extcon_driver); +} +module_exit(tusb320_exit); + +MODULE_AUTHOR("Michael Auchter <michael.auchter@ni.com>"); +MODULE_DESCRIPTION("TI TUSB320 extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon.c b/drivers/extcon/extcon.c new file mode 100644 index 000000000..98d85cc11 --- /dev/null +++ b/drivers/extcon/extcon.c @@ -0,0 +1,1488 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/extcon/extcon.c - External Connector (extcon) framework. + * + * Copyright (C) 2015 Samsung Electronics + * Author: Chanwoo Choi <cw00.choi@samsung.com> + * + * Copyright (C) 2012 Samsung Electronics + * Author: Donggeun Kim <dg77.kim@samsung.com> + * Author: MyungJoo Ham <myungjoo.ham@samsung.com> + * + * based on android/drivers/switch/switch_class.c + * Copyright (C) 2008 Google, Inc. + * Author: Mike Lockwood <lockwood@android.com> + */ + +#include <linux/module.h> +#include <linux/types.h> +#include <linux/idr.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/err.h> +#include <linux/of.h> +#include <linux/slab.h> +#include <linux/sysfs.h> + +#include "extcon.h" + +#define SUPPORTED_CABLE_MAX 32 + +static const struct __extcon_info { + unsigned int type; + unsigned int id; + const char *name; + +} extcon_info[] = { + [EXTCON_NONE] = { + .type = EXTCON_TYPE_MISC, + .id = EXTCON_NONE, + .name = "NONE", + }, + + /* USB external connector */ + [EXTCON_USB] = { + .type = EXTCON_TYPE_USB, + .id = EXTCON_USB, + .name = "USB", + }, + [EXTCON_USB_HOST] = { + .type = EXTCON_TYPE_USB, + .id = EXTCON_USB_HOST, + .name = "USB-HOST", + }, + + /* Charging external connector */ + [EXTCON_CHG_USB_SDP] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_SDP, + .name = "SDP", + }, + [EXTCON_CHG_USB_DCP] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_DCP, + .name = "DCP", + }, + [EXTCON_CHG_USB_CDP] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_CDP, + .name = "CDP", + }, + [EXTCON_CHG_USB_ACA] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_ACA, + .name = "ACA", + }, + [EXTCON_CHG_USB_FAST] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_FAST, + .name = "FAST-CHARGER", + }, + [EXTCON_CHG_USB_SLOW] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_SLOW, + .name = "SLOW-CHARGER", + }, + [EXTCON_CHG_WPT] = { + .type = EXTCON_TYPE_CHG, + .id = EXTCON_CHG_WPT, + .name = "WPT", + }, + [EXTCON_CHG_USB_PD] = { + .type = EXTCON_TYPE_CHG | EXTCON_TYPE_USB, + .id = EXTCON_CHG_USB_PD, + .name = "PD", + }, + + /* Jack external connector */ + [EXTCON_JACK_MICROPHONE] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_MICROPHONE, + .name = "MICROPHONE", + }, + [EXTCON_JACK_HEADPHONE] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_HEADPHONE, + .name = "HEADPHONE", + }, + [EXTCON_JACK_LINE_IN] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_LINE_IN, + .name = "LINE-IN", + }, + [EXTCON_JACK_LINE_OUT] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_LINE_OUT, + .name = "LINE-OUT", + }, + [EXTCON_JACK_VIDEO_IN] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_VIDEO_IN, + .name = "VIDEO-IN", + }, + [EXTCON_JACK_VIDEO_OUT] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_VIDEO_OUT, + .name = "VIDEO-OUT", + }, + [EXTCON_JACK_SPDIF_IN] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_SPDIF_IN, + .name = "SPDIF-IN", + }, + [EXTCON_JACK_SPDIF_OUT] = { + .type = EXTCON_TYPE_JACK, + .id = EXTCON_JACK_SPDIF_OUT, + .name = "SPDIF-OUT", + }, + + /* Display external connector */ + [EXTCON_DISP_HDMI] = { + .type = EXTCON_TYPE_DISP, + .id = EXTCON_DISP_HDMI, + .name = "HDMI", + }, + [EXTCON_DISP_MHL] = { + .type = EXTCON_TYPE_DISP, + .id = EXTCON_DISP_MHL, + .name = "MHL", + }, + [EXTCON_DISP_DVI] = { + .type = EXTCON_TYPE_DISP, + .id = EXTCON_DISP_DVI, + .name = "DVI", + }, + [EXTCON_DISP_VGA] = { + .type = EXTCON_TYPE_DISP, + .id = EXTCON_DISP_VGA, + .name = "VGA", + }, + [EXTCON_DISP_DP] = { + .type = EXTCON_TYPE_DISP | EXTCON_TYPE_USB, + .id = EXTCON_DISP_DP, + .name = "DP", + }, + [EXTCON_DISP_HMD] = { + .type = EXTCON_TYPE_DISP | EXTCON_TYPE_USB, + .id = EXTCON_DISP_HMD, + .name = "HMD", + }, + [EXTCON_DISP_CVBS] = { + .type = EXTCON_TYPE_DISP, + .id = EXTCON_DISP_CVBS, + .name = "CVBS", + }, + [EXTCON_DISP_EDP] = { + .type = EXTCON_TYPE_DISP, + .id = EXTCON_DISP_EDP, + .name = "EDP", + }, + + /* Miscellaneous external connector */ + [EXTCON_DOCK] = { + .type = EXTCON_TYPE_MISC, + .id = EXTCON_DOCK, + .name = "DOCK", + }, + [EXTCON_JIG] = { + .type = EXTCON_TYPE_MISC, + .id = EXTCON_JIG, + .name = "JIG", + }, + [EXTCON_MECHANICAL] = { + .type = EXTCON_TYPE_MISC, + .id = EXTCON_MECHANICAL, + .name = "MECHANICAL", + }, + + { /* sentinel */ } +}; + +/** + * struct extcon_cable - An internal data for an external connector. + * @edev: the extcon device + * @cable_index: the index of this cable in the edev + * @attr_g: the attribute group for the cable + * @attr_name: "name" sysfs entry + * @attr_state: "state" sysfs entry + * @attrs: the array pointing to attr_name and attr_state for attr_g + * @usb_propval: the array of USB connector properties + * @chg_propval: the array of charger connector properties + * @jack_propval: the array of jack connector properties + * @disp_propval: the array of display connector properties + * @usb_bits: the bit array of the USB connector property capabilities + * @chg_bits: the bit array of the charger connector property capabilities + * @jack_bits: the bit array of the jack connector property capabilities + * @disp_bits: the bit array of the display connector property capabilities + */ +struct extcon_cable { + struct extcon_dev *edev; + int cable_index; + + struct attribute_group attr_g; + struct device_attribute attr_name; + struct device_attribute attr_state; + + struct attribute *attrs[3]; /* to be fed to attr_g.attrs */ + + union extcon_property_value usb_propval[EXTCON_PROP_USB_CNT]; + union extcon_property_value chg_propval[EXTCON_PROP_CHG_CNT]; + union extcon_property_value jack_propval[EXTCON_PROP_JACK_CNT]; + union extcon_property_value disp_propval[EXTCON_PROP_DISP_CNT]; + + DECLARE_BITMAP(usb_bits, EXTCON_PROP_USB_CNT); + DECLARE_BITMAP(chg_bits, EXTCON_PROP_CHG_CNT); + DECLARE_BITMAP(jack_bits, EXTCON_PROP_JACK_CNT); + DECLARE_BITMAP(disp_bits, EXTCON_PROP_DISP_CNT); +}; + +static struct class *extcon_class; + +static DEFINE_IDA(extcon_dev_ids); +static LIST_HEAD(extcon_dev_list); +static DEFINE_MUTEX(extcon_dev_list_lock); + +static int check_mutually_exclusive(struct extcon_dev *edev, u32 new_state) +{ + int i; + + if (!edev->mutually_exclusive) + return 0; + + for (i = 0; edev->mutually_exclusive[i]; i++) { + int weight; + u32 correspondants = new_state & edev->mutually_exclusive[i]; + + /* calculate the total number of bits set */ + weight = hweight32(correspondants); + if (weight > 1) + return i + 1; + } + + return 0; +} + +static int find_cable_index_by_id(struct extcon_dev *edev, const unsigned int id) +{ + int i; + + /* Find the index of extcon cable in edev->supported_cable */ + for (i = 0; i < edev->max_supported; i++) { + if (edev->supported_cable[i] == id) + return i; + } + + return -EINVAL; +} + +static int get_extcon_type(unsigned int prop) +{ + switch (prop) { + case EXTCON_PROP_USB_MIN ... EXTCON_PROP_USB_MAX: + return EXTCON_TYPE_USB; + case EXTCON_PROP_CHG_MIN ... EXTCON_PROP_CHG_MAX: + return EXTCON_TYPE_CHG; + case EXTCON_PROP_JACK_MIN ... EXTCON_PROP_JACK_MAX: + return EXTCON_TYPE_JACK; + case EXTCON_PROP_DISP_MIN ... EXTCON_PROP_DISP_MAX: + return EXTCON_TYPE_DISP; + default: + return -EINVAL; + } +} + +static bool is_extcon_attached(struct extcon_dev *edev, unsigned int index) +{ + return !!(edev->state & BIT(index)); +} + +static bool is_extcon_changed(struct extcon_dev *edev, int index, + bool new_state) +{ + int state = !!(edev->state & BIT(index)); + return (state != new_state); +} + +static bool is_extcon_property_supported(unsigned int id, unsigned int prop) +{ + int type; + + /* Check whether the property is supported or not. */ + type = get_extcon_type(prop); + if (type < 0) + return false; + + /* Check whether a specific extcon id supports the property or not. */ + return !!(extcon_info[id].type & type); +} + +static int is_extcon_property_capability(struct extcon_dev *edev, + unsigned int id, int index,unsigned int prop) +{ + struct extcon_cable *cable; + int type, ret; + + /* Check whether the property is supported or not. */ + type = get_extcon_type(prop); + if (type < 0) + return type; + + cable = &edev->cables[index]; + + switch (type) { + case EXTCON_TYPE_USB: + ret = test_bit(prop - EXTCON_PROP_USB_MIN, cable->usb_bits); + break; + case EXTCON_TYPE_CHG: + ret = test_bit(prop - EXTCON_PROP_CHG_MIN, cable->chg_bits); + break; + case EXTCON_TYPE_JACK: + ret = test_bit(prop - EXTCON_PROP_JACK_MIN, cable->jack_bits); + break; + case EXTCON_TYPE_DISP: + ret = test_bit(prop - EXTCON_PROP_DISP_MIN, cable->disp_bits); + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static void init_property(struct extcon_dev *edev, unsigned int id, int index) +{ + unsigned int type = extcon_info[id].type; + struct extcon_cable *cable = &edev->cables[index]; + + if (EXTCON_TYPE_USB & type) + memset(cable->usb_propval, 0, sizeof(cable->usb_propval)); + if (EXTCON_TYPE_CHG & type) + memset(cable->chg_propval, 0, sizeof(cable->chg_propval)); + if (EXTCON_TYPE_JACK & type) + memset(cable->jack_propval, 0, sizeof(cable->jack_propval)); + if (EXTCON_TYPE_DISP & type) + memset(cable->disp_propval, 0, sizeof(cable->disp_propval)); +} + +static ssize_t state_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int i, count = 0; + struct extcon_dev *edev = dev_get_drvdata(dev); + + if (edev->max_supported == 0) + return sysfs_emit(buf, "%u\n", edev->state); + + for (i = 0; i < edev->max_supported; i++) { + count += sysfs_emit_at(buf, count, "%s=%d\n", + extcon_info[edev->supported_cable[i]].name, + !!(edev->state & BIT(i))); + } + + return count; +} +static DEVICE_ATTR_RO(state); + +static ssize_t name_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct extcon_dev *edev = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%s\n", edev->name); +} +static DEVICE_ATTR_RO(name); + +static ssize_t cable_name_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct extcon_cable *cable = container_of(attr, struct extcon_cable, + attr_name); + int i = cable->cable_index; + + return sysfs_emit(buf, "%s\n", + extcon_info[cable->edev->supported_cable[i]].name); +} + +static ssize_t cable_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct extcon_cable *cable = container_of(attr, struct extcon_cable, + attr_state); + + int i = cable->cable_index; + + return sysfs_emit(buf, "%d\n", + extcon_get_state(cable->edev, cable->edev->supported_cable[i])); +} + +/** + * extcon_sync() - Synchronize the state for an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * + * Note that this function send a notification in order to synchronize + * the state and property of an external connector. + * + * Returns 0 if success or error number if fail. + */ +int extcon_sync(struct extcon_dev *edev, unsigned int id) +{ + char name_buf[120]; + char state_buf[120]; + char *prop_buf; + char *envp[3]; + int env_offset = 0; + int length; + int index; + int state; + unsigned long flags; + + if (!edev) + return -EINVAL; + + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + spin_lock_irqsave(&edev->lock, flags); + state = !!(edev->state & BIT(index)); + spin_unlock_irqrestore(&edev->lock, flags); + + /* + * Call functions in a raw notifier chain for the specific one + * external connector. + */ + raw_notifier_call_chain(&edev->nh[index], state, edev); + + /* + * Call functions in a raw notifier chain for the all supported + * external connectors. + */ + raw_notifier_call_chain(&edev->nh_all, state, edev); + + spin_lock_irqsave(&edev->lock, flags); + /* This could be in interrupt handler */ + prop_buf = (char *)get_zeroed_page(GFP_ATOMIC); + if (!prop_buf) { + /* Unlock early before uevent */ + spin_unlock_irqrestore(&edev->lock, flags); + + dev_err(&edev->dev, "out of memory in extcon_set_state\n"); + kobject_uevent(&edev->dev.kobj, KOBJ_CHANGE); + + return -ENOMEM; + } + + length = name_show(&edev->dev, NULL, prop_buf); + if (length > 0) { + if (prop_buf[length - 1] == '\n') + prop_buf[length - 1] = 0; + snprintf(name_buf, sizeof(name_buf), "NAME=%s", prop_buf); + envp[env_offset++] = name_buf; + } + + length = state_show(&edev->dev, NULL, prop_buf); + if (length > 0) { + if (prop_buf[length - 1] == '\n') + prop_buf[length - 1] = 0; + snprintf(state_buf, sizeof(state_buf), "STATE=%s", prop_buf); + envp[env_offset++] = state_buf; + } + envp[env_offset] = NULL; + + /* Unlock early before uevent */ + spin_unlock_irqrestore(&edev->lock, flags); + kobject_uevent_env(&edev->dev.kobj, KOBJ_CHANGE, envp); + free_page((unsigned long)prop_buf); + + return 0; +} +EXPORT_SYMBOL_GPL(extcon_sync); + +/** + * extcon_get_state() - Get the state of an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * + * Returns 0 if success or error number if fail. + */ +int extcon_get_state(struct extcon_dev *edev, const unsigned int id) +{ + int index, state; + unsigned long flags; + + if (!edev) + return -EINVAL; + + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + spin_lock_irqsave(&edev->lock, flags); + state = is_extcon_attached(edev, index); + spin_unlock_irqrestore(&edev->lock, flags); + + return state; +} +EXPORT_SYMBOL_GPL(extcon_get_state); + +/** + * extcon_set_state() - Set the state of an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @state: the new state of an external connector. + * the default semantics is true: attached / false: detached. + * + * Note that this function set the state of an external connector without + * a notification. To synchronize the state of an external connector, + * have to use extcon_set_state_sync() and extcon_sync(). + * + * Returns 0 if success or error number if fail. + */ +int extcon_set_state(struct extcon_dev *edev, unsigned int id, bool state) +{ + unsigned long flags; + int index, ret = 0; + + if (!edev) + return -EINVAL; + + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + spin_lock_irqsave(&edev->lock, flags); + + /* Check whether the external connector's state is changed. */ + if (!is_extcon_changed(edev, index, state)) + goto out; + + if (check_mutually_exclusive(edev, + (edev->state & ~BIT(index)) | (state & BIT(index)))) { + ret = -EPERM; + goto out; + } + + /* + * Initialize the value of extcon property before setting + * the detached state for an external connector. + */ + if (!state) + init_property(edev, id, index); + + /* Update the state for an external connector. */ + if (state) + edev->state |= BIT(index); + else + edev->state &= ~(BIT(index)); +out: + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_set_state); + +/** + * extcon_set_state_sync() - Set the state of an external connector with sync. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @state: the new state of external connector. + * the default semantics is true: attached / false: detached. + * + * Note that this function set the state of external connector + * and synchronize the state by sending a notification. + * + * Returns 0 if success or error number if fail. + */ +int extcon_set_state_sync(struct extcon_dev *edev, unsigned int id, bool state) +{ + int ret; + + ret = extcon_set_state(edev, id, state); + if (ret < 0) + return ret; + + return extcon_sync(edev, id); +} +EXPORT_SYMBOL_GPL(extcon_set_state_sync); + +/** + * extcon_get_property() - Get the property value of an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @prop: the property id indicating an extcon property + * @prop_val: the pointer which store the value of extcon property + * + * Note that when getting the property value of external connector, + * the external connector should be attached. If detached state, function + * return 0 without property value. Also, the each property should be + * included in the list of supported properties according to extcon type. + * + * Returns 0 if success or error number if fail. + */ +int extcon_get_property(struct extcon_dev *edev, unsigned int id, + unsigned int prop, + union extcon_property_value *prop_val) +{ + struct extcon_cable *cable; + unsigned long flags; + int index, ret = 0; + + *prop_val = (union extcon_property_value){0}; + + if (!edev) + return -EINVAL; + + /* Check whether the property is supported or not */ + if (!is_extcon_property_supported(id, prop)) + return -EINVAL; + + /* Find the cable index of external connector by using id */ + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + spin_lock_irqsave(&edev->lock, flags); + + /* Check whether the property is available or not. */ + if (!is_extcon_property_capability(edev, id, index, prop)) { + spin_unlock_irqrestore(&edev->lock, flags); + return -EPERM; + } + + /* + * Check whether the external connector is attached. + * If external connector is detached, the user can not + * get the property value. + */ + if (!is_extcon_attached(edev, index)) { + spin_unlock_irqrestore(&edev->lock, flags); + return 0; + } + + cable = &edev->cables[index]; + + /* Get the property value according to extcon type */ + switch (prop) { + case EXTCON_PROP_USB_MIN ... EXTCON_PROP_USB_MAX: + *prop_val = cable->usb_propval[prop - EXTCON_PROP_USB_MIN]; + break; + case EXTCON_PROP_CHG_MIN ... EXTCON_PROP_CHG_MAX: + *prop_val = cable->chg_propval[prop - EXTCON_PROP_CHG_MIN]; + break; + case EXTCON_PROP_JACK_MIN ... EXTCON_PROP_JACK_MAX: + *prop_val = cable->jack_propval[prop - EXTCON_PROP_JACK_MIN]; + break; + case EXTCON_PROP_DISP_MIN ... EXTCON_PROP_DISP_MAX: + *prop_val = cable->disp_propval[prop - EXTCON_PROP_DISP_MIN]; + break; + default: + ret = -EINVAL; + break; + } + + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_get_property); + +/** + * extcon_set_property() - Set the property value of an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @prop: the property id indicating an extcon property + * @prop_val: the pointer including the new value of extcon property + * + * Note that each property should be included in the list of supported + * properties according to the extcon type. + * + * Returns 0 if success or error number if fail. + */ +int extcon_set_property(struct extcon_dev *edev, unsigned int id, + unsigned int prop, + union extcon_property_value prop_val) +{ + struct extcon_cable *cable; + unsigned long flags; + int index, ret = 0; + + if (!edev) + return -EINVAL; + + /* Check whether the property is supported or not */ + if (!is_extcon_property_supported(id, prop)) + return -EINVAL; + + /* Find the cable index of external connector by using id */ + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + spin_lock_irqsave(&edev->lock, flags); + + /* Check whether the property is available or not. */ + if (!is_extcon_property_capability(edev, id, index, prop)) { + spin_unlock_irqrestore(&edev->lock, flags); + return -EPERM; + } + + cable = &edev->cables[index]; + + /* Set the property value according to extcon type */ + switch (prop) { + case EXTCON_PROP_USB_MIN ... EXTCON_PROP_USB_MAX: + cable->usb_propval[prop - EXTCON_PROP_USB_MIN] = prop_val; + break; + case EXTCON_PROP_CHG_MIN ... EXTCON_PROP_CHG_MAX: + cable->chg_propval[prop - EXTCON_PROP_CHG_MIN] = prop_val; + break; + case EXTCON_PROP_JACK_MIN ... EXTCON_PROP_JACK_MAX: + cable->jack_propval[prop - EXTCON_PROP_JACK_MIN] = prop_val; + break; + case EXTCON_PROP_DISP_MIN ... EXTCON_PROP_DISP_MAX: + cable->disp_propval[prop - EXTCON_PROP_DISP_MIN] = prop_val; + break; + default: + ret = -EINVAL; + break; + } + + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_set_property); + +/** + * extcon_set_property_sync() - Set property of an external connector with sync. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @prop: the property id indicating an extcon property + * @prop_val: the pointer including the new value of extcon property + * + * Note that when setting the property value of external connector, + * the external connector should be attached. The each property should + * be included in the list of supported properties according to extcon type. + * + * Returns 0 if success or error number if fail. + */ +int extcon_set_property_sync(struct extcon_dev *edev, unsigned int id, + unsigned int prop, + union extcon_property_value prop_val) +{ + int ret; + + ret = extcon_set_property(edev, id, prop, prop_val); + if (ret < 0) + return ret; + + return extcon_sync(edev, id); +} +EXPORT_SYMBOL_GPL(extcon_set_property_sync); + +/** + * extcon_get_property_capability() - Get the capability of the property + * for an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @prop: the property id indicating an extcon property + * + * Returns 1 if the property is available or 0 if not available. + */ +int extcon_get_property_capability(struct extcon_dev *edev, unsigned int id, + unsigned int prop) +{ + int index; + + if (!edev) + return -EINVAL; + + /* Check whether the property is supported or not */ + if (!is_extcon_property_supported(id, prop)) + return -EINVAL; + + /* Find the cable index of external connector by using id */ + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + return is_extcon_property_capability(edev, id, index, prop); +} +EXPORT_SYMBOL_GPL(extcon_get_property_capability); + +/** + * extcon_set_property_capability() - Set the capability of the property + * for an external connector. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @prop: the property id indicating an extcon property + * + * Note that this function set the capability of the property + * for an external connector in order to mark the bit in capability + * bitmap which mean the available state of the property. + * + * Returns 0 if success or error number if fail. + */ +int extcon_set_property_capability(struct extcon_dev *edev, unsigned int id, + unsigned int prop) +{ + struct extcon_cable *cable; + int index, type, ret = 0; + + if (!edev) + return -EINVAL; + + /* Check whether the property is supported or not. */ + if (!is_extcon_property_supported(id, prop)) + return -EINVAL; + + /* Find the cable index of external connector by using id. */ + index = find_cable_index_by_id(edev, id); + if (index < 0) + return index; + + type = get_extcon_type(prop); + if (type < 0) + return type; + + cable = &edev->cables[index]; + + switch (type) { + case EXTCON_TYPE_USB: + __set_bit(prop - EXTCON_PROP_USB_MIN, cable->usb_bits); + break; + case EXTCON_TYPE_CHG: + __set_bit(prop - EXTCON_PROP_CHG_MIN, cable->chg_bits); + break; + case EXTCON_TYPE_JACK: + __set_bit(prop - EXTCON_PROP_JACK_MIN, cable->jack_bits); + break; + case EXTCON_TYPE_DISP: + __set_bit(prop - EXTCON_PROP_DISP_MIN, cable->disp_bits); + break; + default: + ret = -EINVAL; + } + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_set_property_capability); + +/** + * extcon_get_extcon_dev() - Get the extcon device instance from the name. + * @extcon_name: the extcon name provided with extcon_dev_register() + * + * Return the pointer of extcon device if success or ERR_PTR(err) if fail. + * NOTE: This function returns -EPROBE_DEFER so it may only be called from + * probe() functions. + */ +struct extcon_dev *extcon_get_extcon_dev(const char *extcon_name) +{ + struct extcon_dev *sd; + + if (!extcon_name) + return ERR_PTR(-EINVAL); + + mutex_lock(&extcon_dev_list_lock); + list_for_each_entry(sd, &extcon_dev_list, entry) { + if (!strcmp(sd->name, extcon_name)) + goto out; + } + sd = ERR_PTR(-EPROBE_DEFER); +out: + mutex_unlock(&extcon_dev_list_lock); + return sd; +} +EXPORT_SYMBOL_GPL(extcon_get_extcon_dev); + +/** + * extcon_register_notifier() - Register a notifier block to get notified by + * any state changes from the extcon. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @nb: a notifier block to be registered + * + * Note that the second parameter given to the callback of nb (val) is + * the current state of an external connector and the third pameter + * is the pointer of extcon device. + * + * Returns 0 if success or error number if fail. + */ +int extcon_register_notifier(struct extcon_dev *edev, unsigned int id, + struct notifier_block *nb) +{ + unsigned long flags; + int ret, idx; + + if (!edev || !nb) + return -EINVAL; + + idx = find_cable_index_by_id(edev, id); + if (idx < 0) + return idx; + + spin_lock_irqsave(&edev->lock, flags); + ret = raw_notifier_chain_register(&edev->nh[idx], nb); + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_register_notifier); + +/** + * extcon_unregister_notifier() - Unregister a notifier block from the extcon. + * @edev: the extcon device + * @id: the unique id indicating an external connector + * @nb: a notifier block to be registered + * + * Returns 0 if success or error number if fail. + */ +int extcon_unregister_notifier(struct extcon_dev *edev, unsigned int id, + struct notifier_block *nb) +{ + unsigned long flags; + int ret, idx; + + if (!edev || !nb) + return -EINVAL; + + idx = find_cable_index_by_id(edev, id); + if (idx < 0) + return idx; + + spin_lock_irqsave(&edev->lock, flags); + ret = raw_notifier_chain_unregister(&edev->nh[idx], nb); + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_unregister_notifier); + +/** + * extcon_register_notifier_all() - Register a notifier block for all connectors. + * @edev: the extcon device + * @nb: a notifier block to be registered + * + * Note that this function registers a notifier block in order to receive + * the state change of all supported external connectors from extcon device. + * And the second parameter given to the callback of nb (val) is + * the current state and the third pameter is the pointer of extcon device. + * + * Returns 0 if success or error number if fail. + */ +int extcon_register_notifier_all(struct extcon_dev *edev, + struct notifier_block *nb) +{ + unsigned long flags; + int ret; + + if (!edev || !nb) + return -EINVAL; + + spin_lock_irqsave(&edev->lock, flags); + ret = raw_notifier_chain_register(&edev->nh_all, nb); + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_register_notifier_all); + +/** + * extcon_unregister_notifier_all() - Unregister a notifier block from extcon. + * @edev: the extcon device + * @nb: a notifier block to be registered + * + * Returns 0 if success or error number if fail. + */ +int extcon_unregister_notifier_all(struct extcon_dev *edev, + struct notifier_block *nb) +{ + unsigned long flags; + int ret; + + if (!edev || !nb) + return -EINVAL; + + spin_lock_irqsave(&edev->lock, flags); + ret = raw_notifier_chain_unregister(&edev->nh_all, nb); + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_unregister_notifier_all); + +static struct attribute *extcon_attrs[] = { + &dev_attr_state.attr, + &dev_attr_name.attr, + NULL, +}; +ATTRIBUTE_GROUPS(extcon); + +static int create_extcon_class(void) +{ + if (extcon_class) + return 0; + + extcon_class = class_create("extcon"); + if (IS_ERR(extcon_class)) + return PTR_ERR(extcon_class); + extcon_class->dev_groups = extcon_groups; + + return 0; +} + +static void extcon_dev_release(struct device *dev) +{ +} + +static const char *muex_name = "mutually_exclusive"; +static void dummy_sysfs_dev_release(struct device *dev) +{ +} + +/* + * extcon_dev_allocate() - Allocate the memory of extcon device. + * @supported_cable: the array of the supported external connectors + * ending with EXTCON_NONE. + * + * Note that this function allocates the memory for extcon device + * and initialize default setting for the extcon device. + * + * Returns the pointer memory of allocated extcon_dev if success + * or ERR_PTR(err) if fail. + */ +struct extcon_dev *extcon_dev_allocate(const unsigned int *supported_cable) +{ + struct extcon_dev *edev; + + if (!supported_cable) + return ERR_PTR(-EINVAL); + + edev = kzalloc_obj(*edev); + if (!edev) + return ERR_PTR(-ENOMEM); + + edev->max_supported = 0; + edev->supported_cable = supported_cable; + + return edev; +} + +/* + * extcon_dev_free() - Free the memory of extcon device. + * @edev: the extcon device + */ +void extcon_dev_free(struct extcon_dev *edev) +{ + kfree(edev); +} +EXPORT_SYMBOL_GPL(extcon_dev_free); + +/** + * extcon_alloc_cables() - alloc the cables for extcon device + * @edev: extcon device which has cables + * + * Returns 0 if success or error number if fail. + */ +static int extcon_alloc_cables(struct extcon_dev *edev) +{ + int index; + char *str; + struct extcon_cable *cable; + + if (!edev) + return -EINVAL; + + if (!edev->max_supported) + return 0; + + edev->cables = kzalloc_objs(*edev->cables, edev->max_supported); + if (!edev->cables) + return -ENOMEM; + + for (index = 0; index < edev->max_supported; index++) { + cable = &edev->cables[index]; + + str = kasprintf(GFP_KERNEL, "cable.%d", index); + if (!str) { + for (index--; index >= 0; index--) { + cable = &edev->cables[index]; + kfree(cable->attr_g.name); + } + + kfree(edev->cables); + return -ENOMEM; + } + + cable->edev = edev; + cable->cable_index = index; + cable->attrs[0] = &cable->attr_name.attr; + cable->attrs[1] = &cable->attr_state.attr; + cable->attrs[2] = NULL; + cable->attr_g.name = str; + cable->attr_g.attrs = cable->attrs; + + sysfs_attr_init(&cable->attr_name.attr); + cable->attr_name.attr.name = "name"; + cable->attr_name.attr.mode = 0444; + cable->attr_name.show = cable_name_show; + + sysfs_attr_init(&cable->attr_state.attr); + cable->attr_state.attr.name = "state"; + cable->attr_state.attr.mode = 0444; + cable->attr_state.show = cable_state_show; + } + + return 0; +} + +/** + * extcon_alloc_muex() - alloc the mutual exclusive for extcon device + * @edev: extcon device + * + * Returns 0 if success or error number if fail. + */ +static int extcon_alloc_muex(struct extcon_dev *edev) +{ + char *name; + int index; + + if (!edev) + return -EINVAL; + + if (!(edev->max_supported && edev->mutually_exclusive)) + return 0; + + /* Count the size of mutually_exclusive array */ + for (index = 0; edev->mutually_exclusive[index]; index++) + ; + + edev->attrs_muex = kzalloc_objs(*edev->attrs_muex, index + 1); + if (!edev->attrs_muex) + return -ENOMEM; + + edev->d_attrs_muex = kzalloc_objs(*edev->d_attrs_muex, index); + if (!edev->d_attrs_muex) { + kfree(edev->attrs_muex); + return -ENOMEM; + } + + for (index = 0; edev->mutually_exclusive[index]; index++) { + name = kasprintf(GFP_KERNEL, "0x%x", + edev->mutually_exclusive[index]); + if (!name) { + for (index--; index >= 0; index--) + kfree(edev->d_attrs_muex[index].attr.name); + + kfree(edev->d_attrs_muex); + kfree(edev->attrs_muex); + return -ENOMEM; + } + sysfs_attr_init(&edev->d_attrs_muex[index].attr); + edev->d_attrs_muex[index].attr.name = name; + edev->d_attrs_muex[index].attr.mode = 0000; + edev->attrs_muex[index] = &edev->d_attrs_muex[index].attr; + } + edev->attr_g_muex.name = muex_name; + edev->attr_g_muex.attrs = edev->attrs_muex; + + return 0; +} + +/** + * extcon_alloc_groups() - alloc the groups for extcon device + * @edev: extcon device + * + * Returns 0 if success or error number if fail. + */ +static int extcon_alloc_groups(struct extcon_dev *edev) +{ + int index; + + if (!edev) + return -EINVAL; + + if (!edev->max_supported) + return 0; + + edev->extcon_dev_type.groups = kzalloc_objs(*edev->extcon_dev_type.groups, + edev->max_supported + 2); + if (!edev->extcon_dev_type.groups) + return -ENOMEM; + + edev->extcon_dev_type.name = dev_name(&edev->dev); + edev->extcon_dev_type.release = dummy_sysfs_dev_release; + + for (index = 0; index < edev->max_supported; index++) + edev->extcon_dev_type.groups[index] = &edev->cables[index].attr_g; + + if (edev->mutually_exclusive) + edev->extcon_dev_type.groups[index] = &edev->attr_g_muex; + + edev->dev.type = &edev->extcon_dev_type; + + return 0; +} + +/** + * extcon_dev_register() - Register an new extcon device + * @edev: the extcon device to be registered + * + * Among the members of edev struct, please set the "user initializing data" + * do not set the values of "internal data", which are initialized by + * this function. + * + * Note that before calling this funciton, have to allocate the memory + * of an extcon device by using the extcon_dev_allocate(). And the extcon + * dev should include the supported_cable information. + * + * Returns 0 if success or error number if fail. + */ +int extcon_dev_register(struct extcon_dev *edev) +{ + int ret, index; + + ret = create_extcon_class(); + if (ret < 0) + return ret; + + if (!edev || !edev->supported_cable) + return -EINVAL; + + for (index = 0; edev->supported_cable[index] != EXTCON_NONE; index++); + + edev->max_supported = index; + if (index > SUPPORTED_CABLE_MAX) { + dev_err(&edev->dev, + "exceed the maximum number of supported cables\n"); + return -EINVAL; + } + + edev->dev.class = extcon_class; + edev->dev.release = extcon_dev_release; + + edev->name = dev_name(edev->dev.parent); + if (IS_ERR_OR_NULL(edev->name)) { + dev_err(&edev->dev, + "extcon device name is null\n"); + return -EINVAL; + } + + ret = ida_alloc(&extcon_dev_ids, GFP_KERNEL); + if (ret < 0) + return ret; + + edev->id = ret; + + ret = extcon_alloc_cables(edev); + if (ret < 0) + goto err_alloc_cables; + + ret = extcon_alloc_muex(edev); + if (ret < 0) + goto err_alloc_muex; + + ret = extcon_alloc_groups(edev); + if (ret < 0) + goto err_alloc_groups; + + spin_lock_init(&edev->lock); + if (edev->max_supported) { + edev->nh = kzalloc_objs(*edev->nh, edev->max_supported); + if (!edev->nh) { + ret = -ENOMEM; + goto err_alloc_nh; + } + } + + for (index = 0; index < edev->max_supported; index++) + RAW_INIT_NOTIFIER_HEAD(&edev->nh[index]); + + RAW_INIT_NOTIFIER_HEAD(&edev->nh_all); + + dev_set_drvdata(&edev->dev, edev); + dev_set_name(&edev->dev, "extcon%d", edev->id); + edev->state = 0; + + ret = device_register(&edev->dev); + if (ret) { + put_device(&edev->dev); + goto err_dev; + } + + mutex_lock(&extcon_dev_list_lock); + list_add(&edev->entry, &extcon_dev_list); + mutex_unlock(&extcon_dev_list_lock); + + return 0; + +err_dev: + if (edev->max_supported) + kfree(edev->nh); +err_alloc_nh: + if (edev->max_supported) + kfree(edev->extcon_dev_type.groups); +err_alloc_groups: + if (edev->max_supported && edev->mutually_exclusive) { + for (index = 0; edev->mutually_exclusive[index]; index++) + kfree(edev->d_attrs_muex[index].attr.name); + kfree(edev->d_attrs_muex); + kfree(edev->attrs_muex); + } +err_alloc_muex: + for (index = 0; index < edev->max_supported; index++) + kfree(edev->cables[index].attr_g.name); + if (edev->max_supported) + kfree(edev->cables); +err_alloc_cables: + ida_free(&extcon_dev_ids, edev->id); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_dev_register); + +/** + * extcon_dev_unregister() - Unregister the extcon device. + * @edev: the extcon device to be unregistered. + * + * Note that this does not call kfree(edev) because edev was not allocated + * by this class. + */ +void extcon_dev_unregister(struct extcon_dev *edev) +{ + int index; + + if (!edev) + return; + + mutex_lock(&extcon_dev_list_lock); + list_del(&edev->entry); + mutex_unlock(&extcon_dev_list_lock); + + if (!get_device(&edev->dev)) { + dev_err(&edev->dev, "Failed to unregister extcon_dev\n"); + return; + } + + device_unregister(&edev->dev); + + ida_free(&extcon_dev_ids, edev->id); + + if (edev->mutually_exclusive && edev->max_supported) { + for (index = 0; edev->mutually_exclusive[index]; + index++) + kfree(edev->d_attrs_muex[index].attr.name); + kfree(edev->d_attrs_muex); + kfree(edev->attrs_muex); + } + + for (index = 0; index < edev->max_supported; index++) + kfree(edev->cables[index].attr_g.name); + + if (edev->max_supported) { + kfree(edev->extcon_dev_type.groups); + kfree(edev->cables); + kfree(edev->nh); + } + + put_device(&edev->dev); +} +EXPORT_SYMBOL_GPL(extcon_dev_unregister); + +#ifdef CONFIG_OF + +/* + * extcon_find_edev_by_node - Find the extcon device from devicetree. + * @node : OF node identifying edev + * + * Return the pointer of extcon device if success or ERR_PTR(err) if fail. + */ +struct extcon_dev *extcon_find_edev_by_node(struct device_node *node) +{ + struct extcon_dev *edev; + + mutex_lock(&extcon_dev_list_lock); + list_for_each_entry(edev, &extcon_dev_list, entry) + if (edev->dev.parent && device_match_of_node(edev->dev.parent, node)) + goto out; + edev = ERR_PTR(-EPROBE_DEFER); +out: + mutex_unlock(&extcon_dev_list_lock); + + return edev; +} + +/* + * extcon_get_edev_by_phandle - Get the extcon device from devicetree. + * @dev : the instance to the given device + * @index : the index into list of extcon_dev + * + * Return the pointer of extcon device if success or ERR_PTR(err) if fail. + */ +struct extcon_dev *extcon_get_edev_by_phandle(struct device *dev, int index) +{ + struct device_node *node, *np = dev_of_node(dev); + struct extcon_dev *edev; + + if (!np) { + dev_dbg(dev, "device does not have a device node entry\n"); + return ERR_PTR(-EINVAL); + } + + node = of_parse_phandle(np, "extcon", index); + if (!node) { + dev_dbg(dev, "failed to get phandle in %pOF node\n", np); + return ERR_PTR(-ENODEV); + } + + edev = extcon_find_edev_by_node(node); + of_node_put(node); + + return edev; +} + +#else + +struct extcon_dev *extcon_find_edev_by_node(struct device_node *node) +{ + return ERR_PTR(-ENOSYS); +} + +struct extcon_dev *extcon_get_edev_by_phandle(struct device *dev, int index) +{ + return ERR_PTR(-ENOSYS); +} + +#endif /* CONFIG_OF */ + +EXPORT_SYMBOL_GPL(extcon_find_edev_by_node); +EXPORT_SYMBOL_GPL(extcon_get_edev_by_phandle); + +/** + * extcon_get_edev_name() - Get the name of the extcon device. + * @edev: the extcon device + */ +const char *extcon_get_edev_name(struct extcon_dev *edev) +{ + return !edev ? NULL : edev->name; +} +EXPORT_SYMBOL_GPL(extcon_get_edev_name); + +static int __init extcon_class_init(void) +{ + return create_extcon_class(); +} +module_init(extcon_class_init); + +static void __exit extcon_class_exit(void) +{ + class_destroy(extcon_class); +} +module_exit(extcon_class_exit); + +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_DESCRIPTION("External Connector (extcon) framework"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon.h b/drivers/extcon/extcon.h new file mode 100644 index 000000000..946182687 --- /dev/null +++ b/drivers/extcon/extcon.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __LINUX_EXTCON_INTERNAL_H__ +#define __LINUX_EXTCON_INTERNAL_H__ + +#include <linux/extcon-provider.h> + +/** + * struct extcon_dev - An extcon device represents one external connector. + * @name: The name of this extcon device. Parent device name is + * used if NULL. + * @supported_cable: Array of supported cable names ending with EXTCON_NONE. + * If supported_cable is NULL, cable name related APIs + * are disabled. + * @mutually_exclusive: Array of mutually exclusive set of cables that cannot + * be attached simultaneously. The array should be + * ending with 0 or be NULL (no mutually exclusive cables). + * For example, if it is {0x7, 0x30, 0}, then, + * {0, 1}, {0, 1, 2}, {0, 2}, {1, 2}, or {4, 5} cannot + * be attached simulataneously. {0x7, 0} is equivalent to + * {0x3, 0x6, 0x5, 0}. If it is {0xFFFFFFFF, 0}, there + * can be no simultaneous connections. + * @dev: Device of this extcon. + * @id: Unique device ID of this extcon. + * @state: Attach/detach state of this extcon. Do not provide at + * register-time. + * @nh_all: Notifier for the state change events for all supported + * external connectors from this extcon. + * @nh: Notifier for the state change events from this extcon + * @entry: To support list of extcon devices so that users can + * search for extcon devices based on the extcon name. + * @lock: Protects device state and serialises device registration + * @max_supported: Internal value to store the number of cables. + * @extcon_dev_type: Device_type struct to provide attribute_groups + * customized for each extcon device. + * @cables: Sysfs subdirectories. Each represents one cable. + * + * In most cases, users only need to provide "User initializing data" of + * this struct when registering an extcon. In some exceptional cases, + * optional callbacks may be needed. However, the values in "internal data" + * are overwritten by register function. + */ +struct extcon_dev { + /* Optional user initializing data */ + const char *name; + const unsigned int *supported_cable; + const u32 *mutually_exclusive; + + /* Internal data. Please do not set. */ + struct device dev; + unsigned int id; + struct raw_notifier_head nh_all; + struct raw_notifier_head *nh; + struct list_head entry; + int max_supported; + spinlock_t lock; /* could be called by irq handler */ + u32 state; + + /* /sys/class/extcon/.../cable.n/... */ + struct device_type extcon_dev_type; + struct extcon_cable *cables; + + /* /sys/class/extcon/.../mutually_exclusive/... */ + struct attribute_group attr_g_muex; + struct attribute **attrs_muex; + struct device_attribute *d_attrs_muex; +}; + +#endif /* __LINUX_EXTCON_INTERNAL_H__ */ |
