2021-06-16 11:51:24 -07:00
// 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>
2022-01-26 10:04:10 -08:00
# include <asm/unaligned.h>
2021-06-16 11:51:24 -07:00
# 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 ;
2022-06-07 23:03:44 +08:00
msg = kzalloc ( struct_size ( msg , data , max ( outsize , insize ) ) , GFP_KERNEL ) ;
2021-06-16 11:51:24 -07:00
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 )
{
2022-01-26 10:04:10 -08:00
struct cros_ec_device * ec_dev = data ;
2021-06-16 11:51:24 -07:00
struct charger_data * charger =
container_of ( nb , struct charger_data , notifier ) ;
2022-01-26 10:04:10 -08:00
u32 host_event ;
2021-06-16 11:51:24 -07:00
2022-01-26 10:04:10 -08:00
if ( ec_dev - > event_data . event_type ! = EC_MKBP_EVENT_PCHG | |
ec_dev - > event_size ! = sizeof ( host_event ) )
2021-06-16 11:51:24 -07:00
return NOTIFY_DONE ;
2022-01-26 10:04:10 -08:00
host_event = get_unaligned_le32 ( & ec_dev - > event_data . data . host_event ) ;
2021-06-16 11:51:24 -07:00
2022-01-26 10:04:10 -08:00
if ( ! ( host_event & EC_MKBP_PCHG_DEVICE_EVENT ) )
2021-06-16 11:51:24 -07:00
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 ) ;