diff options
author | Linus Torvalds <torvalds@linux-foundation.org> | 2019-09-19 14:14:28 -0700 |
---|---|---|
committer | Linus Torvalds <torvalds@linux-foundation.org> | 2019-09-19 14:14:28 -0700 |
commit | 32b90daf5cafdda62b1ce62d0b7445fa9107babf (patch) | |
tree | a22ae3217a1f8c32e28c70d878f830b99b4a113b /drivers/platform | |
parent | c6cfaf4f86d9d15e5541adb3bb899d0b80f89ec7 (diff) | |
parent | 4c1fde5077dcad1a2a10a6a0883c8f94326c4971 (diff) |
Merge tag 'tag-chrome-platform-for-v5.4' of git://git.kernel.org/pub/scm/linux/kernel/git/chrome-platform/linux
Pull chrome platform updates from Benson Leung:
"CrOS EC / MFD Migration:
- Move cros_ec core driver from mfd into chrome platform.
Wilco EC:
- Add batt_ppid_info command to Wilco telemetry driver.
CrOS EC:
- cros_ec_rpmsg : Add support to inform EC of suspend/resume status
- cros_ec_rpmsg : Fix race condition on probe failed
- cros_ec_chardev : Add a poll handler to receive MKBP events
Misc:
- bugfixes in cros_usbpd_logger and cros_ec_ishtp"
* tag 'tag-chrome-platform-for-v5.4' of git://git.kernel.org/pub/scm/linux/kernel/git/chrome-platform/linux:
platform/chrome: cros_usbpd_logger: null check create_singlethread_workqueue
platform/chrome: cros_ec_chardev: Add a poll handler to receive MKBP events
platform/chrome: cros_ec_rpmsg: Fix race with host command when probe failed
platform/chrome: chromeos_tbmc: Report wake events
mfd: cros_ec: Use mfd_add_hotplug_devices() helper
mfd: cros_ec: Add convenience struct to define autodetectable CrOS EC subdevices
mfd: cros_ec: Add convenience struct to define dedicated CrOS EC MCUs
mfd: cros_ec: Use kzalloc and cros_ec_cmd_xfer_status helper
mfd / platform: cros_ec: Reorganize platform and mfd includes
mfd / platform: cros_ec: Rename config to a better name
mfd: cros_ec: Switch to use the new cros-ec-chardev driver
mfd / platform: cros_ec: Miscellaneous character device to talk with the EC
mfd / platform: cros_ec: Move cros-ec core driver out from MFD
mfd / platform: cros_ec: Handle chained ECs as platform devices
platform/chrome: cros_ec_rpmsg: Add host command AP sleep state support
platform/chrome: chromeos_laptop: drop checks of NULL-safe functions
platform/chrome: wilco_ec: Add batt_ppid_info command to telemetry driver
Diffstat (limited to 'drivers/platform')
20 files changed, 894 insertions, 66 deletions
diff --git a/drivers/platform/chrome/Kconfig b/drivers/platform/chrome/Kconfig index 970679d0b6f6..ee5f08ea57b6 100644 --- a/drivers/platform/chrome/Kconfig +++ b/drivers/platform/chrome/Kconfig @@ -3,6 +3,16 @@ # Platform support for Chrome OS hardware (Chromebooks and Chromeboxes) # +config MFD_CROS_EC + tristate "Platform support for Chrome hardware (transitional)" + select CHROME_PLATFORMS + select CROS_EC + select CONFIG_MFD_CROS_EC_DEV + depends on X86 || ARM || ARM64 || COMPILE_TEST + help + This is a transitional Kconfig option and will be removed after + everyone enables the parts individually. + menuconfig CHROME_PLATFORMS bool "Platform support for Chrome hardware" depends on X86 || ARM || ARM64 || COMPILE_TEST @@ -50,9 +60,22 @@ config CHROMEOS_TBMC To compile this driver as a module, choose M here: the module will be called chromeos_tbmc. +config CROS_EC + tristate "ChromeOS Embedded Controller" + select CROS_EC_PROTO + depends on X86 || ARM || ARM64 || COMPILE_TEST + help + If you say Y here you get support for the ChromeOS Embedded + Controller (EC) providing keyboard, battery and power services. + You also need to enable the driver for the bus you are using. The + protocol for talking to the EC is defined by the bus driver. + + To compile this driver as a module, choose M here: the + module will be called cros_ec. + config CROS_EC_I2C tristate "ChromeOS Embedded Controller (I2C)" - depends on MFD_CROS_EC && I2C + depends on CROS_EC && I2C help If you say Y here, you get support for talking to the ChromeOS @@ -62,7 +85,7 @@ config CROS_EC_I2C config CROS_EC_RPMSG tristate "ChromeOS Embedded Controller (rpmsg)" - depends on MFD_CROS_EC && RPMSG && OF + depends on CROS_EC && RPMSG && OF help If you say Y here, you get support for talking to the ChromeOS EC through rpmsg. This uses a simple byte-level protocol with a @@ -74,7 +97,7 @@ config CROS_EC_RPMSG config CROS_EC_ISHTP tristate "ChromeOS Embedded Controller (ISHTP)" - depends on MFD_CROS_EC + depends on CROS_EC depends on INTEL_ISH_HID help If you say Y here, you get support for talking to the ChromeOS EC @@ -87,7 +110,7 @@ config CROS_EC_ISHTP config CROS_EC_SPI tristate "ChromeOS Embedded Controller (SPI)" - depends on MFD_CROS_EC && SPI + depends on CROS_EC && SPI ---help--- If you say Y here, you get support for talking to the ChromeOS EC @@ -97,7 +120,7 @@ config CROS_EC_SPI config CROS_EC_LPC tristate "ChromeOS Embedded Controller (LPC)" - depends on MFD_CROS_EC && ACPI && (X86 || COMPILE_TEST) + depends on CROS_EC && ACPI && (X86 || COMPILE_TEST) help If you say Y here, you get support for talking to the ChromeOS EC over an LPC bus, including the LPC Microchip EC (MEC) variant. @@ -123,10 +146,21 @@ config CROS_KBD_LED_BACKLIGHT To compile this driver as a module, choose M here: the module will be called cros_kbd_led_backlight. +config CROS_EC_CHARDEV + tristate "ChromeOS EC miscdevice" + depends on MFD_CROS_EC_DEV + default MFD_CROS_EC_DEV + help + This driver adds file operations support to talk with the + ChromeOS EC from userspace via a character device. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_chardev. + config CROS_EC_LIGHTBAR tristate "Chromebook Pixel's lightbar support" - depends on MFD_CROS_EC_CHARDEV - default MFD_CROS_EC_CHARDEV + depends on MFD_CROS_EC_DEV + default MFD_CROS_EC_DEV help This option exposes the Chromebook Pixel's lightbar to userspace. @@ -136,8 +170,8 @@ config CROS_EC_LIGHTBAR config CROS_EC_VBC tristate "ChromeOS EC vboot context support" - depends on MFD_CROS_EC_CHARDEV && OF - default MFD_CROS_EC_CHARDEV + depends on MFD_CROS_EC_DEV && OF + default MFD_CROS_EC_DEV help This option exposes the ChromeOS EC vboot context nvram to userspace. @@ -147,8 +181,8 @@ config CROS_EC_VBC config CROS_EC_DEBUGFS tristate "Export ChromeOS EC internals in DebugFS" - depends on MFD_CROS_EC_CHARDEV && DEBUG_FS - default MFD_CROS_EC_CHARDEV + depends on MFD_CROS_EC_DEV && DEBUG_FS + default MFD_CROS_EC_DEV help This option exposes the ChromeOS EC device internals to userspace. @@ -158,8 +192,8 @@ config CROS_EC_DEBUGFS config CROS_EC_SYSFS tristate "ChromeOS EC control and information through sysfs" - depends on MFD_CROS_EC_CHARDEV && SYSFS - default MFD_CROS_EC_CHARDEV + depends on MFD_CROS_EC_DEV && SYSFS + default MFD_CROS_EC_DEV help This option exposes some sysfs attributes to control and get information from ChromeOS EC. diff --git a/drivers/platform/chrome/Makefile b/drivers/platform/chrome/Makefile index fd0af05cc14c..477ec3d1d1c9 100644 --- a/drivers/platform/chrome/Makefile +++ b/drivers/platform/chrome/Makefile @@ -6,6 +6,7 @@ CFLAGS_cros_ec_trace.o:= -I$(src) obj-$(CONFIG_CHROMEOS_LAPTOP) += chromeos_laptop.o obj-$(CONFIG_CHROMEOS_PSTORE) += chromeos_pstore.o obj-$(CONFIG_CHROMEOS_TBMC) += chromeos_tbmc.o +obj-$(CONFIG_CROS_EC) += cros_ec.o obj-$(CONFIG_CROS_EC_I2C) += cros_ec_i2c.o obj-$(CONFIG_CROS_EC_ISHTP) += cros_ec_ishtp.o obj-$(CONFIG_CROS_EC_RPMSG) += cros_ec_rpmsg.o @@ -14,6 +15,7 @@ cros_ec_lpcs-objs := cros_ec_lpc.o cros_ec_lpc_mec.o obj-$(CONFIG_CROS_EC_LPC) += cros_ec_lpcs.o obj-$(CONFIG_CROS_EC_PROTO) += cros_ec_proto.o cros_ec_trace.o obj-$(CONFIG_CROS_KBD_LED_BACKLIGHT) += cros_kbd_led_backlight.o +obj-$(CONFIG_CROS_EC_CHARDEV) += cros_ec_chardev.o obj-$(CONFIG_CROS_EC_LIGHTBAR) += cros_ec_lightbar.o obj-$(CONFIG_CROS_EC_VBC) += cros_ec_vbc.o obj-$(CONFIG_CROS_EC_DEBUGFS) += cros_ec_debugfs.o diff --git a/drivers/platform/chrome/chromeos_laptop.c b/drivers/platform/chrome/chromeos_laptop.c index 7abbb6167766..8723bcf10c93 100644 --- a/drivers/platform/chrome/chromeos_laptop.c +++ b/drivers/platform/chrome/chromeos_laptop.c @@ -838,18 +838,14 @@ static void chromeos_laptop_destroy(const struct chromeos_laptop *cros_laptop) i2c_dev = &cros_laptop->i2c_peripherals[i]; info = &i2c_dev->board_info; - if (i2c_dev->client) - i2c_unregister_device(i2c_dev->client); - - if (info->properties) - property_entries_free(info->properties); + i2c_unregister_device(i2c_dev->client); + property_entries_free(info->properties); } for (i = 0; i < cros_laptop->num_acpi_peripherals; i++) { acpi_dev = &cros_laptop->acpi_peripherals[i]; - if (acpi_dev->properties) - property_entries_free(acpi_dev->properties); + property_entries_free(acpi_dev->properties); } kfree(cros_laptop->i2c_peripherals); diff --git a/drivers/platform/chrome/chromeos_tbmc.c b/drivers/platform/chrome/chromeos_tbmc.c index ce259ec9f990..d1cf8f3463ce 100644 --- a/drivers/platform/chrome/chromeos_tbmc.c +++ b/drivers/platform/chrome/chromeos_tbmc.c @@ -47,6 +47,7 @@ static __maybe_unused int chromeos_tbmc_resume(struct device *dev) static void chromeos_tbmc_notify(struct acpi_device *adev, u32 event) { + acpi_pm_wakeup_event(&adev->dev); switch (event) { case 0x80: chromeos_tbmc_query_switch(adev, adev->driver_data); @@ -90,6 +91,7 @@ static int chromeos_tbmc_add(struct acpi_device *adev) dev_err(dev, "cannot register input device\n"); return ret; } + device_init_wakeup(dev, true); return 0; } diff --git a/drivers/platform/chrome/cros_ec.c b/drivers/platform/chrome/cros_ec.c new file mode 100644 index 000000000000..fd77e6fa74c2 --- /dev/null +++ b/drivers/platform/chrome/cros_ec.c @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ChromeOS EC multi-function device + * + * Copyright (C) 2012 Google, Inc + * + * The ChromeOS EC multi function device is used to mux all the requests + * to the EC device for its multiple features: keyboard controller, + * battery charging and regulator control, firmware update. + */ + +#include <linux/of_platform.h> +#include <linux/interrupt.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/platform_data/cros_ec_commands.h> +#include <linux/platform_data/cros_ec_proto.h> +#include <linux/suspend.h> +#include <asm/unaligned.h> + +#define CROS_EC_DEV_EC_INDEX 0 +#define CROS_EC_DEV_PD_INDEX 1 + +static struct cros_ec_platform ec_p = { + .ec_name = CROS_EC_DEV_NAME, + .cmd_offset = EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_EC_INDEX), +}; + +static struct cros_ec_platform pd_p = { + .ec_name = CROS_EC_DEV_PD_NAME, + .cmd_offset = EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX), +}; + +static irqreturn_t ec_irq_thread(int irq, void *data) +{ + struct cros_ec_device *ec_dev = data; + bool wake_event = true; + int ret; + + ret = cros_ec_get_next_event(ec_dev, &wake_event); + + /* + * Signal only if wake host events or any interrupt if + * cros_ec_get_next_event() returned an error (default value for + * wake_event is true) + */ + if (wake_event && device_may_wakeup(ec_dev->dev)) + pm_wakeup_event(ec_dev->dev, 0); + + if (ret > 0) + blocking_notifier_call_chain(&ec_dev->event_notifier, + 0, ec_dev); + return IRQ_HANDLED; +} + +static int cros_ec_sleep_event(struct cros_ec_device *ec_dev, u8 sleep_event) +{ + int ret; + struct { + struct cros_ec_command msg; + union { + struct ec_params_host_sleep_event req0; + struct ec_params_host_sleep_event_v1 req1; + struct ec_response_host_sleep_event_v1 resp1; + } u; + } __packed buf; + + memset(&buf, 0, sizeof(buf)); + + if (ec_dev->host_sleep_v1) { + buf.u.req1.sleep_event = sleep_event; + buf.u.req1.suspend_params.sleep_timeout_ms = + EC_HOST_SLEEP_TIMEOUT_DEFAULT; + + buf.msg.outsize = sizeof(buf.u.req1); + if ((sleep_event == HOST_SLEEP_EVENT_S3_RESUME) || + (sleep_event == HOST_SLEEP_EVENT_S0IX_RESUME)) + buf.msg.insize = sizeof(buf.u.resp1); + + buf.msg.version = 1; + + } else { + buf.u.req0.sleep_event = sleep_event; + buf.msg.outsize = sizeof(buf.u.req0); + } + + buf.msg.command = EC_CMD_HOST_SLEEP_EVENT; + + ret = cros_ec_cmd_xfer(ec_dev, &buf.msg); + + /* For now, report failure to transition to S0ix with a warning. */ + if (ret >= 0 && ec_dev->host_sleep_v1 && + (sleep_event == HOST_SLEEP_EVENT_S0IX_RESUME)) { + ec_dev->last_resume_result = + buf.u.resp1.resume_response.sleep_transitions; + + WARN_ONCE(buf.u.resp1.resume_response.sleep_transitions & + EC_HOST_RESUME_SLEEP_TIMEOUT, + "EC detected sleep transition timeout. Total slp_s0 transitions: %d", + buf.u.resp1.resume_response.sleep_transitions & + EC_HOST_RESUME_SLEEP_TRANSITIONS_MASK); + } + + return ret; +} + +int cros_ec_register(struct cros_ec_device *ec_dev) +{ + struct device *dev = ec_dev->dev; + int err = 0; + + BLOCKING_INIT_NOTIFIER_HEAD(&ec_dev->event_notifier); + + ec_dev->max_request = sizeof(struct ec_params_hello); + ec_dev->max_response = sizeof(struct ec_response_get_protocol_info); + ec_dev->max_passthru = 0; + + ec_dev->din = devm_kzalloc(dev, ec_dev->din_size, GFP_KERNEL); + if (!ec_dev->din) + return -ENOMEM; + + ec_dev->dout = devm_kzalloc(dev, ec_dev->dout_size, GFP_KERNEL); + if (!ec_dev->dout) + return -ENOMEM; + + mutex_init(&ec_dev->lock); + + err = cros_ec_query_all(ec_dev); + if (err) { + dev_err(dev, "Cannot identify the EC: error %d\n", err); + return err; + } + + if (ec_dev->irq) { + err = devm_request_threaded_irq(dev, ec_dev->irq, NULL, + ec_irq_thread, IRQF_TRIGGER_LOW | IRQF_ONESHOT, + "chromeos-ec", ec_dev); + if (err) { + dev_err(dev, "Failed to request IRQ %d: %d", + ec_dev->irq, err); + return err; + } + } + + /* Register a platform device for the main EC instance */ + ec_dev->ec = platform_device_register_data(ec_dev->dev, "cros-ec-dev", + PLATFORM_DEVID_AUTO, &ec_p, + sizeof(struct cros_ec_platform)); + if (IS_ERR(ec_dev->ec)) { + dev_err(ec_dev->dev, + "Failed to create CrOS EC platform device\n"); + return PTR_ERR(ec_dev->ec); + } + + if (ec_dev->max_passthru) { + /* + * Register a platform device for the PD behind the main EC. + * We make the following assumptions: + * - behind an EC, we have a pd + * - only one device added. + * - the EC is responsive at init time (it is not true for a + * sensor hub). + */ + ec_dev->pd = platform_device_register_data(ec_dev->dev, + "cros-ec-dev", + PLATFORM_DEVID_AUTO, &pd_p, + sizeof(struct cros_ec_platform)); + if (IS_ERR(ec_dev->pd)) { + dev_err(ec_dev->dev, + "Failed to create CrOS PD platform device\n"); + platform_device_unregister(ec_dev->ec); + return PTR_ERR(ec_dev->pd); + } + } + + if (IS_ENABLED(CONFIG_OF) && dev->of_node) { + err = devm_of_platform_populate(dev); + if (err) { + platform_device_unregister(ec_dev->pd); + platform_device_unregister(ec_dev->ec); + dev_err(dev, "Failed to register sub-devices\n"); + return err; + } + } + + /* + * Clear sleep event - this will fail harmlessly on platforms that + * don't implement the sleep event host command. + */ + err = cros_ec_sleep_event(ec_dev, 0); + if (err < 0) + dev_dbg(ec_dev->dev, "Error %d clearing sleep event to ec", + err); + + dev_info(dev, "Chrome EC device registered\n"); + + return 0; +} +EXPORT_SYMBOL(cros_ec_register); + +int cros_ec_unregister(struct cros_ec_device *ec_dev) +{ + if (ec_dev->pd) + platform_device_unregister(ec_dev->pd); + platform_device_unregister(ec_dev->ec); + + return 0; +} +EXPORT_SYMBOL(cros_ec_unregister); + +#ifdef CONFIG_PM_SLEEP +int cros_ec_suspend(struct cros_ec_device *ec_dev) +{ + struct device *dev = ec_dev->dev; + int ret; + u8 sleep_event; + + sleep_event = (!IS_ENABLED(CONFIG_ACPI) || pm_suspend_via_firmware()) ? + HOST_SLEEP_EVENT_S3_SUSPEND : + HOST_SLEEP_EVENT_S0IX_SUSPEND; + + ret = cros_ec_sleep_event(ec_dev, sleep_event); + if (ret < 0) + dev_dbg(ec_dev->dev, "Error %d sending suspend event to ec", + ret); + + if (device_may_wakeup(dev)) + ec_dev->wake_enabled = !enable_irq_wake(ec_dev->irq); + + disable_irq(ec_dev->irq); + ec_dev->was_wake_device = ec_dev->wake_enabled; + ec_dev->suspended = true; + + return 0; +} +EXPORT_SYMBOL(cros_ec_suspend); + +static void cros_ec_report_events_during_suspend(struct cros_ec_device *ec_dev) +{ + while (ec_dev->mkbp_event_supported && + cros_ec_get_next_event(ec_dev, NULL) > 0) + blocking_notifier_call_chain(&ec_dev->event_notifier, + 1, ec_dev); +} + +int cros_ec_resume(struct cros_ec_device *ec_dev) +{ + int ret; + u8 sleep_event; + + ec_dev->suspended = false; + enable_irq(ec_dev->irq); + + sleep_event = (!IS_ENABLED(CONFIG_ACPI) || pm_suspend_via_firmware()) ? + HOST_SLEEP_EVENT_S3_RESUME : + HOST_SLEEP_EVENT_S0IX_RESUME; + + ret = cros_ec_sleep_event(ec_dev, sleep_event); + if (ret < 0) + dev_dbg(ec_dev->dev, "Error %d sending resume event to ec", + ret); + + if (ec_dev->wake_enabled) { + disable_irq_wake(ec_dev->irq); + ec_dev->wake_enabled = 0; + } + /* + * Let the mfd devices know about events that occur during + * suspend. This way the clients know what to do with them. + */ + cros_ec_report_events_during_suspend(ec_dev); + + + return 0; +} +EXPORT_SYMBOL(cros_ec_resume); + +#endif + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS EC core driver"); diff --git a/drivers/platform/chrome/cros_ec_chardev.c b/drivers/platform/chrome/cros_ec_chardev.c new file mode 100644 index 000000000000..74ded441bb50 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_chardev.c @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Miscellaneous character driver for ChromeOS Embedded Controller + * + * Copyright 2014 Google, Inc. + * Copyright 2019 Google LLC + * + * This file is a rework and part of the code is ported from + * drivers/mfd/cros_ec_dev.c that was originally written by + * Bill Richardson. + */ + +#include <linux/init.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/mfd/cros_ec.h> +#include <linux/miscdevice.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/platform_data/cros_ec_chardev.h> +#include <linux/platform_data/cros_ec_commands.h> +#include <linux/platform_data/cros_ec_proto.h> +#include <linux/platform_device.h> +#include <linux/poll.h> +#include <linux/slab.h> +#include <linux/types.h> +#include <linux/uaccess.h> + +#define DRV_NAME "cros-ec-chardev" + +/* Arbitrary bounded size for the event queue */ +#define CROS_MAX_EVENT_LEN PAGE_SIZE + +struct chardev_data { + struct cros_ec_dev *ec_dev; + struct miscdevice misc; +}; + +struct chardev_priv { + struct cros_ec_dev *ec_dev; + struct notifier_block notifier; + wait_queue_head_t wait_event; + unsigned long event_mask; + struct list_head events; + size_t event_len; +}; + +struct ec_event { + struct list_head node; + size_t size; + u8 event_type; + u8 data[0]; +}; + +static int ec_get_version(struct cros_ec_dev *ec, char *str, int maxlen) +{ + static const char * const current_image_name[] = { + "unknown", "read-only", "read-write", "invalid", + }; + struct ec_response_get_version *resp; + struct cros_ec_command *msg; + int ret; + + msg = kzalloc(sizeof(*msg) + sizeof(*resp), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_GET_VERSION + ec->cmd_offset; + msg->insize = sizeof(*resp); + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + snprintf(str, maxlen, + "Unknown EC version, returned error: %d\n", + msg->result); + goto exit; + } + + resp = (struct ec_response_get_version *)msg->data; + if (resp->current_image >= ARRAY_SIZE(current_image_name)) + resp->current_image = 3; /* invalid */ + + snprintf(str, maxlen, "%s\n%s\n%s\n%s\n", CROS_EC_DEV_VERSION, + resp->version_string_ro, resp->version_string_rw, + current_image_name[resp->current_image]); + + ret = 0; +exit: + kfree(msg); + return ret; +} + +static int cros_ec_chardev_mkbp_event(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *_notify) +{ + struct chardev_priv *priv = container_of(nb, struct chardev_priv, + notifier); + struct cros_ec_device *ec_dev = priv->ec_dev->ec_dev; + struct ec_event *event; + unsigned long event_bit = 1 << ec_dev->event_data.event_type; + int total_size = sizeof(*event) + ec_dev->event_size; + + if (!(event_bit & priv->event_mask) || + (priv->event_len + total_size) > CROS_MAX_EVENT_LEN) + return NOTIFY_DONE; + + event = kzalloc(total_size, GFP_KERNEL); + if (!event) + return NOTIFY_DONE; + + event->size = ec_dev->event_size; + event->event_type = ec_dev->event_data.event_type; + memcpy(event->data, &ec_dev->event_data.data, ec_dev->event_size); + + spin_lock(&priv->wait_event.lock); + list_add_tail(&event->node, &priv->events); + priv->event_len += total_size; + wake_up_locked(&priv->wait_event); + spin_unlock(&priv->wait_event.lock); + + return NOTIFY_OK; +} + +static struct ec_event *cros_ec_chardev_fetch_event(struct chardev_priv *priv, + bool fetch, bool block) +{ + struct ec_event *event; + int err; + + spin_lock(&priv->wait_event.lock); + if (!block && list_empty(&priv->events)) { + event = ERR_PTR(-EWOULDBLOCK); + goto out; + } + + if (!fetch) { + event = NULL; + goto out; + } + + err = wait_event_interruptible_locked(priv->wait_event, + !list_empty(&priv->events)); + if (err) { + event = ERR_PTR(err); + goto out; + } + + event = list_first_entry(&priv->events, struct ec_event, node); + list_del(&event->node); + priv->event_len -= sizeof(*event) + event->size; + +out: + spin_unlock(&priv->wait_event.lock); + return event; +} + +/* + * Device file ops + */ +static int cros_ec_chardev_open(struct inode *inode, struct file *filp) +{ + struct miscdevice *mdev = filp->private_data; + struct cros_ec_dev *ec_dev = dev_get_drvdata(mdev->parent); + struct chardev_priv *priv; + int ret; + + priv = kzalloc(sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->ec_dev = ec_dev; + filp->private_data = priv; + INIT_LIST_HEAD(&priv->events); + init_waitqueue_head(&priv->wait_event); + nonseekable_open(inode, filp); + + priv->notifier.notifier_call = cros_ec_chardev_mkbp_event; + ret = blocking_notifier_chain_register(&ec_dev->ec_dev->event_notifier, + &priv->notifier); + if (ret) { + dev_err(ec_dev->dev, "failed to register event notifier\n"); + kfree(priv); + } + + return ret; +} + +static __poll_t cros_ec_chardev_poll(struct file *filp, poll_table *wait) +{ + struct chardev_priv *priv = filp->private_data; + + poll_wait(filp, &priv->wait_event, wait); + + if (list_empty(&priv->events)) + return 0; + + return EPOLLIN | EPOLLRDNORM; +} + +static ssize_t cros_ec_chardev_read(struct file *filp, char __user *buffer, + size_t length, loff_t *offset) +{ + char msg[sizeof(struct ec_response_get_version) + + sizeof(CROS_EC_DEV_VERSION)]; + struct chardev_priv *priv = filp->private_data; + struct cros_ec_dev *ec_dev = priv->ec_dev; + size_t count; + int ret; + + if (priv->event_mask) { /* queued MKBP event */ + struct ec_event *event; + + event = cros_ec_chardev_fetch_event(priv, length != 0, + !(filp->f_flags & O_NONBLOCK)); + if (IS_ERR(event)) + return PTR_ERR(event); + /* + * length == 0 is special - no IO is done but we check + * for error conditions. + */ + if (length == 0) + return 0; + + /* The event is 1 byte of type plus the payload */ + count = min(length, event->size + 1); + ret = copy_to_user(buffer, &event->event_type, count); + kfree(event); + if (ret) /* the copy failed */ + return -EFAULT; + *offset = count; + return count; + } + + /* + * Legacy behavior if no event mask is defined + */ + if (*offset != 0) + return 0; + + ret = ec_get_version(ec_dev, msg, sizeof(msg)); + if (ret) + return ret; + + count = min(length, strlen(msg)); + + if (copy_to_user(buffer, msg, count)) + return -EFAULT; + + *offset = count; + return count; +} + +static int cros_ec_chardev_release(struct inode *inode, struct file *filp) +{ + struct chardev_priv *priv = filp->private_data; + struct cros_ec_dev *ec_dev = priv->ec_dev; + struct ec_event *event, *e; + + blocking_notifier_chain_unregister(&ec_dev->ec_dev->event_notifier, + &priv->notifier); + + list_for_each_entry_safe(event, e, &priv->events, node) { + list_del(&event->node); + kfree(event); + } + kfree(priv); + + return 0; +} + +/* + * Ioctls + */ +static long cros_ec_chardev_ioctl_xcmd(struct cros_ec_dev *ec, void __user *arg) +{ + struct cros_ec_command *s_cmd; + struct cros_ec_command u_cmd; + long ret; + + if (copy_from_user(&u_cmd, arg, sizeof(u_cmd))) + return -EFAULT; + + if (u_cmd.outsize > EC_MAX_MSG_BYTES || + u_cmd.insize > EC_MAX_MSG_BYTES) + return -EINVAL; + + s_cmd = kmalloc(sizeof(*s_cmd) + max(u_cmd.outsize, u_cmd.insize), + GFP_KERNEL); + if (!s_cmd) + return -ENOMEM; + + if (copy_from_user(s_cmd, arg, sizeof(*s_cmd) + u_cmd.outsize)) { + ret = -EFAULT; + goto exit; + } + + if (u_cmd.outsize != s_cmd->outsize || + u_cmd.insize != s_cmd->insize) { + ret = -EINVAL; + goto exit; + } + + s_cmd->command += ec->cmd_offset; + ret = cros_ec_cmd_xfer(ec->ec_dev, s_cmd); + /* Only copy data to userland if data was received. */ + if (ret < 0) + goto exit; + + if (copy_to_user(arg, s_cmd, sizeof(*s_cmd) + s_cmd->insize)) + ret = -EFAULT; +exit: + kfree(s_cmd); + return ret; +} + +static long cros_ec_chardev_ioctl_readmem(struct cros_ec_dev *ec, + void __user *arg) +{ + struct cros_ec_device *ec_dev = ec->ec_dev; + struct cros_ec_readmem s_mem = { }; + long num; + + /* Not every platform supports direct reads */ + if (!ec_dev->cmd_readmem) + return -ENOTTY; + + if (copy_from_user(&s_mem, arg, sizeof(s_mem))) + return -EFAULT; + + num = ec_dev->cmd_readmem(ec_dev, s_mem.offset, s_mem.bytes, + s_mem.buffer); + if (num <= 0) + return num; + + if (copy_to_user((void __user *)arg, &s_mem, sizeof(s_mem))) + return -EFAULT; + + return num; +} + +static long cros_ec_chardev_ioctl(struct file *filp, unsigned int cmd, + unsigned long arg) +{ + struct chardev_priv *priv = filp->private_data; + struct cros_ec_dev *ec = priv->ec_dev; + + if (_IOC_TYPE(cmd) != CROS_EC_DEV_IOC) + return -ENOTTY; + + switch (cmd) { + case CROS_EC_DEV_IOCXCMD: + return cros_ec_chardev_ioctl_xcmd(ec, (void __user *)arg); + case CROS_EC_DEV_IOCRDMEM: + return cros_ec_chardev_ioctl_readmem(ec, (void __user *)arg); + case CROS_EC_DEV_IOCEVENTMASK: + priv->event_mask = arg; + return 0; + } + + return -ENOTTY; +} + +static const struct file_operations chardev_fops = { + .open = cros_ec_chardev_open, + .poll = cros_ec_chardev_poll, + .read = cros_ec_chardev_read, + .release = cros_ec_chardev_release, + .unlocked_ioctl = cros_ec_chardev_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = cros_ec_chardev_ioctl, +#endif +}; + +static int cros_ec_chardev_probe(struct platform_device *pdev) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pdev->dev.parent); + struct cros_ec_platform *ec_platform = dev_get_platdata(ec_dev->dev); + struct chardev_data *data; + + /* Create a char device: we want to create it anew */ + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->ec_dev = ec_dev; + data->misc.minor = MISC_DYNAMIC_MINOR; + data->misc.fops = &chardev_fops; + data->misc.name = ec_platform->ec_name; + data->misc.parent = pdev->dev.parent; + + dev_set_drvdata(&pdev->dev, data); + + return misc_register(&data->misc); +} + +static int cros_ec_chardev_remove(struct platform_device *pdev) +{ + struct chardev_data *data = dev_get_drvdata(&pdev->dev); + + misc_deregister(&data->misc); + + return 0; +} + +static struct platform_driver cros_ec_chardev_driver = { + .driver = { + .name = DRV_NAME, + }, + .probe = cros_ec_chardev_probe, + .remove = cros_ec_chardev_remove, +}; + +module_platform_driver(cros_ec_chardev_driver); + |