|  | // SPDX-License-Identifier: GPL-2.0+ | 
|  | /* | 
|  | * Fast-charge control for Apple "MFi" devices | 
|  | * | 
|  | * Copyright (C) 2019 Bastien Nocera <hadess@hadess.net> | 
|  | */ | 
|  |  | 
|  | /* Standard include files */ | 
|  | #include <linux/module.h> | 
|  | #include <linux/power_supply.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/usb.h> | 
|  |  | 
|  | MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>"); | 
|  | MODULE_DESCRIPTION("Fast-charge control for Apple \"MFi\" devices"); | 
|  | MODULE_LICENSE("GPL"); | 
|  |  | 
|  | #define TRICKLE_CURRENT_MA		0 | 
|  | #define FAST_CURRENT_MA			2500 | 
|  |  | 
|  | #define APPLE_VENDOR_ID			0x05ac	/* Apple */ | 
|  |  | 
|  | /* The product ID is defined as starting with 0x12nn, as per the | 
|  | * "Choosing an Apple Device USB Configuration" section in | 
|  | * release R9 (2012) of the "MFi Accessory Hardware Specification" | 
|  | * | 
|  | * To distinguish an Apple device, a USB host can check the device | 
|  | * descriptor of attached USB devices for the following fields: | 
|  | * ■ Vendor ID: 0x05AC | 
|  | * ■ Product ID: 0x12nn | 
|  | * | 
|  | * Those checks will be done in .match() and .probe(). | 
|  | */ | 
|  |  | 
|  | static const struct usb_device_id mfi_fc_id_table[] = { | 
|  | { .idVendor = APPLE_VENDOR_ID, | 
|  | .match_flags = USB_DEVICE_ID_MATCH_VENDOR }, | 
|  | {}, | 
|  | }; | 
|  |  | 
|  | MODULE_DEVICE_TABLE(usb, mfi_fc_id_table); | 
|  |  | 
|  | /* Driver-local specific stuff */ | 
|  | struct mfi_device { | 
|  | struct usb_device *udev; | 
|  | struct power_supply *battery; | 
|  | struct power_supply_desc battery_desc; | 
|  | int charge_type; | 
|  | }; | 
|  |  | 
|  | static int apple_mfi_fc_set_charge_type(struct mfi_device *mfi, | 
|  | const union power_supply_propval *val) | 
|  | { | 
|  | int current_ma; | 
|  | int retval; | 
|  | __u8 request_type; | 
|  |  | 
|  | if (mfi->charge_type == val->intval) { | 
|  | dev_dbg(&mfi->udev->dev, "charge type %d already set\n", | 
|  | mfi->charge_type); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | switch (val->intval) { | 
|  | case POWER_SUPPLY_CHARGE_TYPE_TRICKLE: | 
|  | current_ma = TRICKLE_CURRENT_MA; | 
|  | break; | 
|  | case POWER_SUPPLY_CHARGE_TYPE_FAST: | 
|  | current_ma = FAST_CURRENT_MA; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | request_type = USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE; | 
|  | retval = usb_control_msg(mfi->udev, usb_sndctrlpipe(mfi->udev, 0), | 
|  | 0x40, /* Vendor‐defined power request */ | 
|  | request_type, | 
|  | current_ma, /* wValue, current offset */ | 
|  | current_ma, /* wIndex, current offset */ | 
|  | NULL, 0, USB_CTRL_GET_TIMEOUT); | 
|  | if (retval) { | 
|  | dev_dbg(&mfi->udev->dev, "retval = %d\n", retval); | 
|  | return retval; | 
|  | } | 
|  |  | 
|  | mfi->charge_type = val->intval; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int apple_mfi_fc_get_property(struct power_supply *psy, | 
|  | enum power_supply_property psp, | 
|  | union power_supply_propval *val) | 
|  | { | 
|  | struct mfi_device *mfi = power_supply_get_drvdata(psy); | 
|  |  | 
|  | dev_dbg(&mfi->udev->dev, "prop: %d\n", psp); | 
|  |  | 
|  | switch (psp) { | 
|  | case POWER_SUPPLY_PROP_CHARGE_TYPE: | 
|  | val->intval = mfi->charge_type; | 
|  | break; | 
|  | case POWER_SUPPLY_PROP_SCOPE: | 
|  | val->intval = POWER_SUPPLY_SCOPE_DEVICE; | 
|  | break; | 
|  | default: | 
|  | return -ENODATA; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int apple_mfi_fc_set_property(struct power_supply *psy, | 
|  | enum power_supply_property psp, | 
|  | const union power_supply_propval *val) | 
|  | { | 
|  | struct mfi_device *mfi = power_supply_get_drvdata(psy); | 
|  | int ret; | 
|  |  | 
|  | dev_dbg(&mfi->udev->dev, "prop: %d\n", psp); | 
|  |  | 
|  | ret = pm_runtime_get_sync(&mfi->udev->dev); | 
|  | if (ret < 0) { | 
|  | pm_runtime_put_noidle(&mfi->udev->dev); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | switch (psp) { | 
|  | case POWER_SUPPLY_PROP_CHARGE_TYPE: | 
|  | ret = apple_mfi_fc_set_charge_type(mfi, val); | 
|  | break; | 
|  | default: | 
|  | ret = -EINVAL; | 
|  | } | 
|  |  | 
|  | pm_runtime_mark_last_busy(&mfi->udev->dev); | 
|  | pm_runtime_put_autosuspend(&mfi->udev->dev); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int apple_mfi_fc_property_is_writeable(struct power_supply *psy, | 
|  | enum power_supply_property psp) | 
|  | { | 
|  | switch (psp) { | 
|  | case POWER_SUPPLY_PROP_CHARGE_TYPE: | 
|  | return 1; | 
|  | default: | 
|  | return 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | static enum power_supply_property apple_mfi_fc_properties[] = { | 
|  | POWER_SUPPLY_PROP_CHARGE_TYPE, | 
|  | POWER_SUPPLY_PROP_SCOPE | 
|  | }; | 
|  |  | 
|  | static const struct power_supply_desc apple_mfi_fc_desc = { | 
|  | .name                   = "apple_mfi_fastcharge", | 
|  | .type                   = POWER_SUPPLY_TYPE_BATTERY, | 
|  | .properties             = apple_mfi_fc_properties, | 
|  | .num_properties         = ARRAY_SIZE(apple_mfi_fc_properties), | 
|  | .get_property           = apple_mfi_fc_get_property, | 
|  | .set_property           = apple_mfi_fc_set_property, | 
|  | .property_is_writeable  = apple_mfi_fc_property_is_writeable | 
|  | }; | 
|  |  | 
|  | static bool mfi_fc_match(struct usb_device *udev) | 
|  | { | 
|  | int idProduct; | 
|  |  | 
|  | idProduct = le16_to_cpu(udev->descriptor.idProduct); | 
|  | /* See comment above mfi_fc_id_table[] */ | 
|  | return (idProduct >= 0x1200 && idProduct <= 0x12ff); | 
|  | } | 
|  |  | 
|  | static int mfi_fc_probe(struct usb_device *udev) | 
|  | { | 
|  | struct power_supply_config battery_cfg = {}; | 
|  | struct mfi_device *mfi = NULL; | 
|  | char *battery_name; | 
|  | int err; | 
|  |  | 
|  | if (!mfi_fc_match(udev)) | 
|  | return -ENODEV; | 
|  |  | 
|  | mfi = kzalloc(sizeof(struct mfi_device), GFP_KERNEL); | 
|  | if (!mfi) | 
|  | return -ENOMEM; | 
|  |  | 
|  | battery_name = kasprintf(GFP_KERNEL, "apple_mfi_fastcharge_%d-%d", | 
|  | udev->bus->busnum, udev->devnum); | 
|  | if (!battery_name) { | 
|  | err = -ENOMEM; | 
|  | goto err_free_mfi; | 
|  | } | 
|  |  | 
|  | mfi->battery_desc = apple_mfi_fc_desc; | 
|  | mfi->battery_desc.name = battery_name; | 
|  |  | 
|  | battery_cfg.drv_data = mfi; | 
|  |  | 
|  | mfi->charge_type = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; | 
|  | mfi->battery = power_supply_register(&udev->dev, | 
|  | &mfi->battery_desc, | 
|  | &battery_cfg); | 
|  | if (IS_ERR(mfi->battery)) { | 
|  | dev_err(&udev->dev, "Can't register battery\n"); | 
|  | err = PTR_ERR(mfi->battery); | 
|  | goto err_free_name; | 
|  | } | 
|  |  | 
|  | mfi->udev = usb_get_dev(udev); | 
|  | dev_set_drvdata(&udev->dev, mfi); | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | err_free_name: | 
|  | kfree(battery_name); | 
|  | err_free_mfi: | 
|  | kfree(mfi); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static void mfi_fc_disconnect(struct usb_device *udev) | 
|  | { | 
|  | struct mfi_device *mfi; | 
|  |  | 
|  | mfi = dev_get_drvdata(&udev->dev); | 
|  | if (mfi->battery) | 
|  | power_supply_unregister(mfi->battery); | 
|  | kfree(mfi->battery_desc.name); | 
|  | dev_set_drvdata(&udev->dev, NULL); | 
|  | usb_put_dev(mfi->udev); | 
|  | kfree(mfi); | 
|  | } | 
|  |  | 
|  | static struct usb_device_driver mfi_fc_driver = { | 
|  | .name =		"apple-mfi-fastcharge", | 
|  | .probe =	mfi_fc_probe, | 
|  | .disconnect =	mfi_fc_disconnect, | 
|  | .id_table =	mfi_fc_id_table, | 
|  | .match =	mfi_fc_match, | 
|  | .generic_subclass = 1, | 
|  | }; | 
|  |  | 
|  | static int __init mfi_fc_driver_init(void) | 
|  | { | 
|  | return usb_register_device_driver(&mfi_fc_driver, THIS_MODULE); | 
|  | } | 
|  |  | 
|  | static void __exit mfi_fc_driver_exit(void) | 
|  | { | 
|  | usb_deregister_device_driver(&mfi_fc_driver); | 
|  | } | 
|  |  | 
|  | module_init(mfi_fc_driver_init); | 
|  | module_exit(mfi_fc_driver_exit); |