mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
8.9 KiB
363 lines
8.9 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Power supply driver for ChromeOS EC based Peripheral Device Charger. |
|
* |
|
* Copyright 2020 Google LLC. |
|
*/ |
|
|
|
#include <linux/module.h> |
|
#include <linux/notifier.h> |
|
#include <linux/platform_data/cros_ec_commands.h> |
|
#include <linux/platform_data/cros_ec_proto.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/power_supply.h> |
|
#include <linux/slab.h> |
|
#include <linux/stringify.h> |
|
#include <linux/types.h> |
|
#include <asm/unaligned.h> |
|
|
|
#define DRV_NAME "cros-ec-pchg" |
|
#define PCHG_DIR_PREFIX "peripheral" |
|
#define PCHG_DIR_NAME PCHG_DIR_PREFIX "%d" |
|
#define PCHG_DIR_NAME_LENGTH \ |
|
sizeof(PCHG_DIR_PREFIX __stringify(EC_PCHG_MAX_PORTS)) |
|
#define PCHG_CACHE_UPDATE_DELAY msecs_to_jiffies(500) |
|
|
|
struct port_data { |
|
int port_number; |
|
char name[PCHG_DIR_NAME_LENGTH]; |
|
struct power_supply *psy; |
|
struct power_supply_desc psy_desc; |
|
int psy_status; |
|
int battery_percentage; |
|
int charge_type; |
|
struct charger_data *charger; |
|
unsigned long last_update; |
|
}; |
|
|
|
struct charger_data { |
|
struct device *dev; |
|
struct cros_ec_dev *ec_dev; |
|
struct cros_ec_device *ec_device; |
|
int num_registered_psy; |
|
struct port_data *ports[EC_PCHG_MAX_PORTS]; |
|
struct notifier_block notifier; |
|
}; |
|
|
|
static enum power_supply_property cros_pchg_props[] = { |
|
POWER_SUPPLY_PROP_STATUS, |
|
POWER_SUPPLY_PROP_CHARGE_TYPE, |
|
POWER_SUPPLY_PROP_CAPACITY, |
|
POWER_SUPPLY_PROP_SCOPE, |
|
}; |
|
|
|
static int cros_pchg_ec_command(const struct charger_data *charger, |
|
unsigned int version, |
|
unsigned int command, |
|
const void *outdata, |
|
unsigned int outsize, |
|
void *indata, |
|
unsigned int insize) |
|
{ |
|
struct cros_ec_dev *ec_dev = charger->ec_dev; |
|
struct cros_ec_command *msg; |
|
int ret; |
|
|
|
msg = kzalloc(sizeof(*msg) + max(outsize, insize), GFP_KERNEL); |
|
if (!msg) |
|
return -ENOMEM; |
|
|
|
msg->version = version; |
|
msg->command = ec_dev->cmd_offset + command; |
|
msg->outsize = outsize; |
|
msg->insize = insize; |
|
|
|
if (outsize) |
|
memcpy(msg->data, outdata, outsize); |
|
|
|
ret = cros_ec_cmd_xfer_status(charger->ec_device, msg); |
|
if (ret >= 0 && insize) |
|
memcpy(indata, msg->data, insize); |
|
|
|
kfree(msg); |
|
return ret; |
|
} |
|
|
|
static const unsigned int pchg_cmd_version = 1; |
|
|
|
static bool cros_pchg_cmd_ver_check(const struct charger_data *charger) |
|
{ |
|
struct ec_params_get_cmd_versions_v1 req; |
|
struct ec_response_get_cmd_versions rsp; |
|
int ret; |
|
|
|
req.cmd = EC_CMD_PCHG; |
|
ret = cros_pchg_ec_command(charger, 1, EC_CMD_GET_CMD_VERSIONS, |
|
&req, sizeof(req), &rsp, sizeof(rsp)); |
|
if (ret < 0) { |
|
dev_warn(charger->dev, |
|
"Unable to get versions of EC_CMD_PCHG (err:%d)\n", |
|
ret); |
|
return false; |
|
} |
|
|
|
return !!(rsp.version_mask & BIT(pchg_cmd_version)); |
|
} |
|
|
|
static int cros_pchg_port_count(const struct charger_data *charger) |
|
{ |
|
struct ec_response_pchg_count rsp; |
|
int ret; |
|
|
|
ret = cros_pchg_ec_command(charger, 0, EC_CMD_PCHG_COUNT, |
|
NULL, 0, &rsp, sizeof(rsp)); |
|
if (ret < 0) { |
|
dev_warn(charger->dev, |
|
"Unable to get number or ports (err:%d)\n", ret); |
|
return ret; |
|
} |
|
|
|
return rsp.port_count; |
|
} |
|
|
|
static int cros_pchg_get_status(struct port_data *port) |
|
{ |
|
struct charger_data *charger = port->charger; |
|
struct ec_params_pchg req; |
|
struct ec_response_pchg rsp; |
|
struct device *dev = charger->dev; |
|
int old_status = port->psy_status; |
|
int old_percentage = port->battery_percentage; |
|
int ret; |
|
|
|
req.port = port->port_number; |
|
ret = cros_pchg_ec_command(charger, pchg_cmd_version, EC_CMD_PCHG, |
|
&req, sizeof(req), &rsp, sizeof(rsp)); |
|
if (ret < 0) { |
|
dev_err(dev, "Unable to get port.%d status (err:%d)\n", |
|
port->port_number, ret); |
|
return ret; |
|
} |
|
|
|
switch (rsp.state) { |
|
case PCHG_STATE_RESET: |
|
case PCHG_STATE_INITIALIZED: |
|
case PCHG_STATE_ENABLED: |
|
default: |
|
port->psy_status = POWER_SUPPLY_STATUS_UNKNOWN; |
|
port->charge_type = POWER_SUPPLY_CHARGE_TYPE_NONE; |
|
break; |
|
case PCHG_STATE_DETECTED: |
|
port->psy_status = POWER_SUPPLY_STATUS_CHARGING; |
|
port->charge_type = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; |
|
break; |
|
case PCHG_STATE_CHARGING: |
|
port->psy_status = POWER_SUPPLY_STATUS_CHARGING; |
|
port->charge_type = POWER_SUPPLY_CHARGE_TYPE_STANDARD; |
|
break; |
|
case PCHG_STATE_FULL: |
|
port->psy_status = POWER_SUPPLY_STATUS_FULL; |
|
port->charge_type = POWER_SUPPLY_CHARGE_TYPE_NONE; |
|
break; |
|
} |
|
|
|
port->battery_percentage = rsp.battery_percentage; |
|
|
|
if (port->psy_status != old_status || |
|
port->battery_percentage != old_percentage) |
|
power_supply_changed(port->psy); |
|
|
|
dev_dbg(dev, |
|
"Port %d: state=%d battery=%d%%\n", |
|
port->port_number, rsp.state, rsp.battery_percentage); |
|
|
|
return 0; |
|
} |
|
|
|
static int cros_pchg_get_port_status(struct port_data *port, bool ratelimit) |
|
{ |
|
int ret; |
|
|
|
if (ratelimit && |
|
time_is_after_jiffies(port->last_update + PCHG_CACHE_UPDATE_DELAY)) |
|
return 0; |
|
|
|
ret = cros_pchg_get_status(port); |
|
if (ret < 0) |
|
return ret; |
|
|
|
port->last_update = jiffies; |
|
|
|
return ret; |
|
} |
|
|
|
static int cros_pchg_get_prop(struct power_supply *psy, |
|
enum power_supply_property psp, |
|
union power_supply_propval *val) |
|
{ |
|
struct port_data *port = power_supply_get_drvdata(psy); |
|
|
|
switch (psp) { |
|
case POWER_SUPPLY_PROP_STATUS: |
|
case POWER_SUPPLY_PROP_CAPACITY: |
|
case POWER_SUPPLY_PROP_CHARGE_TYPE: |
|
cros_pchg_get_port_status(port, true); |
|
break; |
|
default: |
|
break; |
|
} |
|
|
|
switch (psp) { |
|
case POWER_SUPPLY_PROP_STATUS: |
|
val->intval = port->psy_status; |
|
break; |
|
case POWER_SUPPLY_PROP_CAPACITY: |
|
val->intval = port->battery_percentage; |
|
break; |
|
case POWER_SUPPLY_PROP_CHARGE_TYPE: |
|
val->intval = port->charge_type; |
|
break; |
|
case POWER_SUPPLY_PROP_SCOPE: |
|
val->intval = POWER_SUPPLY_SCOPE_DEVICE; |
|
break; |
|
default: |
|
return -EINVAL; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int cros_pchg_event(const struct charger_data *charger, |
|
unsigned long host_event) |
|
{ |
|
int i; |
|
|
|
for (i = 0; i < charger->num_registered_psy; i++) |
|
cros_pchg_get_port_status(charger->ports[i], false); |
|
|
|
return NOTIFY_OK; |
|
} |
|
|
|
static int cros_ec_notify(struct notifier_block *nb, |
|
unsigned long queued_during_suspend, |
|
void *data) |
|
{ |
|
struct cros_ec_device *ec_dev = data; |
|
struct charger_data *charger = |
|
container_of(nb, struct charger_data, notifier); |
|
u32 host_event; |
|
|
|
if (ec_dev->event_data.event_type != EC_MKBP_EVENT_PCHG || |
|
ec_dev->event_size != sizeof(host_event)) |
|
return NOTIFY_DONE; |
|
|
|
host_event = get_unaligned_le32(&ec_dev->event_data.data.host_event); |
|
|
|
if (!(host_event & EC_MKBP_PCHG_DEVICE_EVENT)) |
|
return NOTIFY_DONE; |
|
|
|
return cros_pchg_event(charger, host_event); |
|
} |
|
|
|
static int cros_pchg_probe(struct platform_device *pdev) |
|
{ |
|
struct device *dev = &pdev->dev; |
|
struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent); |
|
struct cros_ec_device *ec_device = ec_dev->ec_dev; |
|
struct power_supply_desc *psy_desc; |
|
struct charger_data *charger; |
|
struct power_supply *psy; |
|
struct port_data *port; |
|
struct notifier_block *nb; |
|
int num_ports; |
|
int ret; |
|
int i; |
|
|
|
charger = devm_kzalloc(dev, sizeof(*charger), GFP_KERNEL); |
|
if (!charger) |
|
return -ENOMEM; |
|
|
|
charger->dev = dev; |
|
charger->ec_dev = ec_dev; |
|
charger->ec_device = ec_device; |
|
|
|
ret = cros_pchg_port_count(charger); |
|
if (ret <= 0) { |
|
/* |
|
* This feature is enabled by the EC and the kernel driver is |
|
* included by default for CrOS devices. Don't need to be loud |
|
* since this error can be normal. |
|
*/ |
|
dev_info(dev, "No peripheral charge ports (err:%d)\n", ret); |
|
return -ENODEV; |
|
} |
|
|
|
if (!cros_pchg_cmd_ver_check(charger)) { |
|
dev_err(dev, "EC_CMD_PCHG version %d isn't available.\n", |
|
pchg_cmd_version); |
|
return -EOPNOTSUPP; |
|
} |
|
|
|
num_ports = ret; |
|
if (num_ports > EC_PCHG_MAX_PORTS) { |
|
dev_err(dev, "Too many peripheral charge ports (%d)\n", |
|
num_ports); |
|
return -ENOBUFS; |
|
} |
|
|
|
dev_info(dev, "%d peripheral charge ports found\n", num_ports); |
|
|
|
for (i = 0; i < num_ports; i++) { |
|
struct power_supply_config psy_cfg = {}; |
|
|
|
port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL); |
|
if (!port) |
|
return -ENOMEM; |
|
|
|
port->charger = charger; |
|
port->port_number = i; |
|
snprintf(port->name, sizeof(port->name), PCHG_DIR_NAME, i); |
|
|
|
psy_desc = &port->psy_desc; |
|
psy_desc->name = port->name; |
|
psy_desc->type = POWER_SUPPLY_TYPE_BATTERY; |
|
psy_desc->get_property = cros_pchg_get_prop; |
|
psy_desc->external_power_changed = NULL; |
|
psy_desc->properties = cros_pchg_props; |
|
psy_desc->num_properties = ARRAY_SIZE(cros_pchg_props); |
|
psy_cfg.drv_data = port; |
|
|
|
psy = devm_power_supply_register(dev, psy_desc, &psy_cfg); |
|
if (IS_ERR(psy)) |
|
return dev_err_probe(dev, PTR_ERR(psy), |
|
"Failed to register power supply\n"); |
|
port->psy = psy; |
|
|
|
charger->ports[charger->num_registered_psy++] = port; |
|
} |
|
|
|
if (!charger->num_registered_psy) |
|
return -ENODEV; |
|
|
|
nb = &charger->notifier; |
|
nb->notifier_call = cros_ec_notify; |
|
ret = blocking_notifier_chain_register(&ec_dev->ec_dev->event_notifier, |
|
nb); |
|
if (ret < 0) |
|
dev_err(dev, "Failed to register notifier (err:%d)\n", ret); |
|
|
|
return 0; |
|
} |
|
|
|
static struct platform_driver cros_pchg_driver = { |
|
.driver = { |
|
.name = DRV_NAME, |
|
}, |
|
.probe = cros_pchg_probe |
|
}; |
|
|
|
module_platform_driver(cros_pchg_driver); |
|
|
|
MODULE_LICENSE("GPL"); |
|
MODULE_DESCRIPTION("ChromeOS EC peripheral device charger"); |
|
MODULE_ALIAS("platform:" DRV_NAME);
|
|
|