drivers: soc: qcom: Add driver for FSA4480 I2C device

Add driver support for FSA4480 I2C device that
is used for switching orientation of USB-C analog
and for display.

Change-Id: Iac392c82a72fe5de469fda094059ef7691772ca8
Signed-off-by: Phani Kumar Uppalapati <phaniu@codeaurora.org>
This commit is contained in:
Phani Kumar Uppalapati 2018-01-17 11:14:16 -08:00 committed by Gerrit - the friendly Code Review server
parent 5dfea24b2f
commit 96e8162852
5 changed files with 519 additions and 0 deletions

View File

@ -0,0 +1,18 @@
Qualcomm Technologies, Inc.
Fairchild FSA4480 Device
This device is used for switching orientation of USB-C analog
and for display. It uses I2C communication to set the registers
to configure the switches inside the FSA4480 chip to change
orientation and also to set SBU1/SBU2 connections of USB-C.
Required properties:
- compatible: Should be "qcom,fsa4480-i2c".
- reg: I2C device address of the device
Example:
fsa4480: fsa4480@43 {
compatible = "qcom,fsa4480-i2c";
reg = <0x43>;
};

View File

@ -495,6 +495,16 @@ config APSS_CORE_EA
Platform specific power aware driver to provide power
and temperature information to the scheduler.
config QCOM_FSA4480_I2C
bool "Fairchild FSA4480 chip with I2C"
select REGMAP_I2C
depends on I2C
help
Support for the Fairchild FSA4480 IC switch chip controlled
using I2C. This driver provides common support
for accessing the device, switching between USB and Audio
modes, changing orientation.
if MSM_PM
menuconfig MSM_IDLE_STATS
bool "Collect idle statistics"

View File

@ -59,3 +59,4 @@ obj-$(CONFIG_QCOM_QDSS_BRIDGE) += qdss_bridge.o
obj-$(CONFIG_MSM_EVENT_TIMER) += event_timer.o
obj-$(CONFIG_MSM_IDLE_STATS) += lpm-stats.o
obj-$(CONFIG_QTI_RPM_STATS_LOG) += rpm_stats.o
obj-$(CONFIG_QCOM_FSA4480_I2C) += fsa4480-i2c.o

View File

@ -0,0 +1,440 @@
/* Copyright (c) 2018, The Linux Foundation. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/power_supply.h>
#include <linux/regmap.h>
#include <linux/i2c.h>
#include <linux/soc/qcom/fsa4480-i2c.h>
#define FSA4480_I2C_NAME "fsa4480-driver"
#define FSA4480_SWITCH_SETTINGS 0x04
#define FSA4480_SWITCH_CONTROL 0x05
#define FSA4480_SLOW_L 0x08
#define FSA4480_SLOW_R 0x09
#define FSA4480_SLOW_MIC 0x0A
#define FSA4480_SLOW_SENSE 0x0B
#define FSA4480_SLOW_GND 0x0C
#define FSA4480_DELAY_L_R 0x0D
#define FSA4480_DELAY_L_MIC 0x0E
#define FSA4480_DELAY_L_SENSE 0x0F
#define FSA4480_DELAY_L_AGND 0x10
#define FSA4480_RESET 0x1E
struct fsa4480_priv {
struct regmap *regmap;
struct device *dev;
struct power_supply *usb_psy;
struct notifier_block psy_nb;
bool usbc_force_pr_mode;
atomic_t usbc_mode;
struct work_struct usbc_analog_work;
struct blocking_notifier_head fsa4480_notifier;
};
struct fsa4480_reg_val {
u16 reg;
u8 val;
};
static const struct regmap_config fsa4480_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = FSA4480_RESET,
};
static const struct fsa4480_reg_val fsa_reg_i2c_defaults[] = {
{FSA4480_SLOW_L, 0x00},
{FSA4480_SLOW_R, 0x00},
{FSA4480_SLOW_MIC, 0x00},
{FSA4480_SLOW_SENSE, 0x00},
{FSA4480_SLOW_GND, 0x00},
{FSA4480_DELAY_L_R, 0x00},
{FSA4480_DELAY_L_MIC, 0x00},
{FSA4480_DELAY_L_SENSE, 0x00},
{FSA4480_DELAY_L_AGND, 0x09},
};
static void fsa4480_usbc_restore_default(struct fsa4480_priv *fsa_priv)
{
if (!fsa_priv->regmap) {
dev_err(fsa_priv->dev, "%s: regmap invalid\n", __func__);
return;
}
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_SETTINGS, 0x80);
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_CONTROL, 0x18);
/* FSA4480 chip hardware requirement */
usleep_range(50, 55);
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_SETTINGS, 0xF8);
}
static int fsa4480_usbc_event_changed(struct notifier_block *nb,
unsigned long evt, void *ptr)
{
int ret;
union power_supply_propval mode;
struct fsa4480_priv *fsa_priv =
container_of(nb, struct fsa4480_priv, psy_nb);
struct device *dev;
if (!fsa_priv)
return -EINVAL;
dev = fsa_priv->dev;
if (!dev)
return -EINVAL;
if ((struct power_supply *)ptr != fsa_priv->usb_psy ||
evt != PSY_EVENT_PROP_CHANGED)
return 0;
ret = power_supply_get_property(fsa_priv->usb_psy,
POWER_SUPPLY_PROP_TYPEC_MODE, &mode);
if (ret) {
dev_err(dev, "%s: Unable to read USB TYPEC_MODE: %d\n",
__func__, ret);
return ret;
}
dev_dbg(dev, "%s: USB change event received, supply mode %d, usbc mode %d, expected %d\n",
__func__, mode.intval, fsa_priv->usbc_mode.counter,
POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER);
switch (mode.intval) {
case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER:
case POWER_SUPPLY_TYPEC_NONE:
if (atomic_read(&(fsa_priv->usbc_mode)) == mode.intval)
break; /* filter notifications received before */
atomic_set(&(fsa_priv->usbc_mode), mode.intval);
dev_dbg(dev, "%s: queueing usbc_analog_work\n",
__func__);
schedule_work(&fsa_priv->usbc_analog_work);
break;
default:
break;
}
return ret;
}
/*
* fsa4480_reg_notifier - register notifier block with fsa driver
*
* @nb - notifier block of fsa4480
* @node - phandle node to fsa4480 device
*
* Returns 0 on success, or error code
*/
int fsa4480_reg_notifier(struct notifier_block *nb,
struct device_node *node)
{
int rc = 0;
struct i2c_client *client = of_find_i2c_device_by_node(node);
struct fsa4480_priv *fsa_priv;
if (!client)
return -EINVAL;
fsa_priv = (struct fsa4480_priv *)i2c_get_clientdata(client);
if (!fsa_priv)
return -EINVAL;
rc = blocking_notifier_chain_register
(&fsa_priv->fsa4480_notifier, nb);
if (rc)
return rc;
/*
* as part of the init sequence check if there is a connected
* USB C analog adapter
*/
dev_dbg(fsa_priv->dev, "%s: verify if USB adapter is already inserted\n",
__func__);
rc = fsa4480_usbc_event_changed(&fsa_priv->psy_nb,
PSY_EVENT_PROP_CHANGED,
fsa_priv->usb_psy);
return rc;
}
EXPORT_SYMBOL(fsa4480_reg_notifier);
/*
* fsa4480_unreg_notifier - unregister notifier block with fsa driver
*
* @nb - notifier block of fsa4480
* @node - phandle node to fsa4480 device
*
* Returns 0 on pass, or error code
*/
int fsa4480_unreg_notifier(struct notifier_block *nb,
struct device_node *node)
{
struct i2c_client *client = of_find_i2c_device_by_node(node);
struct fsa4480_priv *fsa_priv;
if (!client)
return -EINVAL;
fsa_priv = (struct fsa4480_priv *)i2c_get_clientdata(client);
if (!fsa_priv)
return -EINVAL;
fsa4480_usbc_restore_default(fsa_priv);
return blocking_notifier_chain_unregister
(&fsa_priv->fsa4480_notifier, nb);
}
EXPORT_SYMBOL(fsa4480_unreg_notifier);
/*
* fsa4480_switch_event - configure FSA switch position based on event
*
* @node - phandle node to fsa4480 device
* @event - fsa_function enum
*
* Returns int on whether the switch happened or not
*/
int fsa4480_switch_event(struct device_node *node,
enum fsa_function event)
{
int switch_control = 0;
struct i2c_client *client = of_find_i2c_device_by_node(node);
struct fsa4480_priv *fsa_priv;
if (!client)
return -EINVAL;
fsa_priv = (struct fsa4480_priv *)i2c_get_clientdata(client);
if (!fsa_priv)
return -EINVAL;
if (!fsa_priv->regmap)
return -EINVAL;
regmap_read(fsa_priv->regmap, FSA4480_SWITCH_CONTROL, &switch_control);
switch (event) {
case FSA_MIC_GND_SWAP:
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_SETTINGS, 0x80);
if ((switch_control & 0x07) == 0x07)
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_CONTROL,
0x00);
else
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_CONTROL,
0x07);
/* FSA4480 chip hardware requirement */
usleep_range(50, 55);
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_SETTINGS, 0x1F);
break;
default:
break;
}
return 0;
}
EXPORT_SYMBOL(fsa4480_switch_event);
static int fsa4480_usbc_analog_setup_switches
(struct fsa4480_priv *fsa_priv, bool active)
{
int rc = 0;
union power_supply_propval pval;
dev_dbg(fsa_priv->dev, "%s: setting GPIOs active = %d\n",
__func__, active);
memset(&pval, 0, sizeof(pval));
if (active) {
pval.intval = POWER_SUPPLY_TYPEC_PR_SOURCE;
if (power_supply_set_property(fsa_priv->usb_psy,
POWER_SUPPLY_PROP_TYPEC_POWER_ROLE, &pval))
dev_err(fsa_priv->dev,
"%s: force PR_SOURCE mode unsuccessful\n",
__func__);
else
fsa_priv->usbc_force_pr_mode = true;
/* activate switches */
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_SETTINGS, 0x80);
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_CONTROL, 0x00);
/* FSA4480 chip hardware requirement */
usleep_range(50, 55);
regmap_write(fsa_priv->regmap, FSA4480_SWITCH_SETTINGS, 0x1F);
/* notify call chain on event */
blocking_notifier_call_chain(&fsa_priv->fsa4480_notifier,
POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER, NULL);
} else {
/* notify call chain on event */
blocking_notifier_call_chain(&fsa_priv->fsa4480_notifier,
POWER_SUPPLY_TYPEC_NONE, NULL);
/* deactivate switches */
fsa4480_usbc_restore_default(fsa_priv);
if (fsa_priv->usbc_force_pr_mode) {
pval.intval = POWER_SUPPLY_TYPEC_PR_DUAL;
if (power_supply_set_property(fsa_priv->usb_psy,
POWER_SUPPLY_PROP_TYPEC_POWER_ROLE, &pval))
dev_err(fsa_priv->dev,
"%s: force PR_DUAL unsuccessful\n",
__func__);
fsa_priv->usbc_force_pr_mode = false;
}
}
return rc;
}
static void fsa4480_usbc_analog_work_fn(struct work_struct *work)
{
struct fsa4480_priv *fsa_priv =
container_of(work, struct fsa4480_priv, usbc_analog_work);
if (!fsa_priv) {
pr_err("%s: fsa container invalid\n", __func__);
return;
}
fsa4480_usbc_analog_setup_switches(fsa_priv,
atomic_read(&(fsa_priv->usbc_mode)) != POWER_SUPPLY_TYPEC_NONE);
}
static void fsa4480_update_reg_defaults(struct regmap *regmap)
{
u8 i;
for (i = 0; i < ARRAY_SIZE(fsa_reg_i2c_defaults); i++)
regmap_write(regmap, fsa_reg_i2c_defaults[i].reg,
fsa_reg_i2c_defaults[i].val);
}
static int fsa4480_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
struct fsa4480_priv *fsa_priv;
int rc = 0;
fsa_priv = devm_kzalloc(&i2c->dev, sizeof(*fsa_priv),
GFP_KERNEL);
if (!fsa_priv)
return -ENOMEM;
fsa_priv->dev = &i2c->dev;
fsa_priv->usb_psy = power_supply_get_by_name("usb");
if (!fsa_priv->usb_psy) {
rc = -EPROBE_DEFER;
dev_dbg(fsa_priv->dev,
"%s: could not get USB psy info: %d\n",
__func__, rc);
goto err_data;
}
fsa_priv->regmap = devm_regmap_init_i2c(i2c, &fsa4480_regmap_config);
if (IS_ERR_OR_NULL(fsa_priv->regmap)) {
dev_err(fsa_priv->dev, "%s: Failed to initialize regmap: %d\n",
__func__, rc);
if (!fsa_priv->regmap) {
rc = -EINVAL;
goto err_supply;
}
rc = PTR_ERR(fsa_priv->regmap);
goto err_supply;
}
fsa4480_update_reg_defaults(fsa_priv->regmap);
fsa_priv->psy_nb.notifier_call = fsa4480_usbc_event_changed;
fsa_priv->psy_nb.priority = 0;
rc = power_supply_reg_notifier(&fsa_priv->psy_nb);
if (rc) {
dev_err(fsa_priv->dev, "%s: power supply reg failed: %d\n",
__func__, rc);
goto err_supply;
}
i2c_set_clientdata(i2c, fsa_priv);
INIT_WORK(&fsa_priv->usbc_analog_work,
fsa4480_usbc_analog_work_fn);
fsa_priv->fsa4480_notifier.rwsem =
(struct rw_semaphore)__RWSEM_INITIALIZER
((fsa_priv->fsa4480_notifier).rwsem);
fsa_priv->fsa4480_notifier.head = NULL;
return 0;
err_supply:
power_supply_put(fsa_priv->usb_psy);
err_data:
devm_kfree(&i2c->dev, fsa_priv);
return rc;
}
static int fsa4480_remove(struct i2c_client *i2c)
{
struct fsa4480_priv *fsa_priv =
(struct fsa4480_priv *)i2c_get_clientdata(i2c);
if (!fsa_priv)
return -EINVAL;
fsa4480_usbc_restore_default(fsa_priv);
/* deregister from PMI */
power_supply_unreg_notifier(&fsa_priv->psy_nb);
power_supply_put(fsa_priv->usb_psy);
dev_set_drvdata(&i2c->dev, NULL);
return 0;
}
static const struct of_device_id fsa4480_i2c_dt_match[] = {
{
.compatible = "qcom,fsa4480-i2c",
},
{}
};
static struct i2c_driver fsa4480_i2c_driver = {
.driver = {
.name = FSA4480_I2C_NAME,
.of_match_table = fsa4480_i2c_dt_match,
},
.probe = fsa4480_probe,
.remove = fsa4480_remove,
};
static int __init fsa4480_init(void)
{
int rc;
rc = i2c_add_driver(&fsa4480_i2c_driver);
if (rc)
pr_err("fsa4480: Failed to register I2C driver: %d\n", rc);
return rc;
}
module_init(fsa4480_init);
static void __exit fsa4480_exit(void)
{
i2c_del_driver(&fsa4480_i2c_driver);
}
module_exit(fsa4480_exit);
MODULE_DESCRIPTION("FSA4480 I2C driver");
MODULE_LICENSE("GPL v2");

View File

@ -0,0 +1,50 @@
/* Copyright (c) 2018, The Linux Foundation. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#ifndef FSA4480_I2C_H
#define FSA4480_I2C_H
#include <linux/of.h>
#include <linux/notifier.h>
enum fsa_function {
FSA_MIC_GND_SWAP,
};
#ifdef CONFIG_QCOM_FSA4480_I2C
int fsa4480_switch_event(struct device_node *node,
enum fsa_function event);
int fsa4480_reg_notifier(struct notifier_block *nb,
struct device_node *node);
int fsa4480_unreg_notifier(struct notifier_block *nb,
struct device_node *node);
#else
static inline int fsa4480_switch_event(struct device_node *node,
enum fsa_function event)
{
return 0;
}
static inline int fsa4480_reg_notifier(struct notifier_block *nb,
struct device_node *node)
{
return 0;
}
static inline int fsa4480_unreg_notifier(struct notifier_block *nb,
struct device_node *node)
{
return 0;
}
#endif /* CONFIG_QCOM_FSA4480_I2C */
#endif /* FSA4480_I2C_H */