aboutsummaryrefslogtreecommitdiffstats
path: root/drivers/platform/chrome/wilco_ec
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/platform/chrome/wilco_ec')
-rw-r--r--drivers/platform/chrome/wilco_ec/Kconfig18
-rw-r--r--drivers/platform/chrome/wilco_ec/Makefile6
-rw-r--r--drivers/platform/chrome/wilco_ec/core.c26
-rw-r--r--drivers/platform/chrome/wilco_ec/debugfs.c12
-rw-r--r--drivers/platform/chrome/wilco_ec/event.c581
-rw-r--r--drivers/platform/chrome/wilco_ec/mailbox.c21
-rw-r--r--drivers/platform/chrome/wilco_ec/properties.c132
-rw-r--r--drivers/platform/chrome/wilco_ec/sysfs.c156
-rw-r--r--drivers/platform/chrome/wilco_ec/telemetry.c450
9 files changed, 1372 insertions, 30 deletions
diff --git a/drivers/platform/chrome/wilco_ec/Kconfig b/drivers/platform/chrome/wilco_ec/Kconfig
index fd29cbfd3d5d..89007b0bc743 100644
--- a/drivers/platform/chrome/wilco_ec/Kconfig
+++ b/drivers/platform/chrome/wilco_ec/Kconfig
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-only
config WILCO_EC
tristate "ChromeOS Wilco Embedded Controller"
- depends on ACPI && X86 && CROS_EC_LPC && CROS_EC_LPC_MEC
+ depends on ACPI && X86 && CROS_EC_LPC
help
If you say Y here, you get support for talking to the ChromeOS
Wilco EC over an eSPI bus. This uses a simple byte-level protocol
@@ -19,3 +19,19 @@ config WILCO_EC_DEBUGFS
manipulation and allow for testing arbitrary commands. This
interface is intended for debug only and will not be present
on production devices.
+
+config WILCO_EC_EVENTS
+ tristate "Enable event forwarding from EC to userspace"
+ depends on WILCO_EC
+ help
+ If you say Y here, you get support for the EC to send events
+ (such as power state changes) to userspace. The EC sends the events
+ over ACPI, and a driver queues up the events to be read by a
+ userspace daemon from /dev/wilco_event using read() and poll().
+
+config WILCO_EC_TELEMETRY
+ tristate "Enable querying telemetry data from EC"
+ depends on WILCO_EC
+ help
+ If you say Y here, you get support to query EC telemetry data from
+ /dev/wilco_telem0 using write() and then read().
diff --git a/drivers/platform/chrome/wilco_ec/Makefile b/drivers/platform/chrome/wilco_ec/Makefile
index 063e7fb4ea17..bc817164596e 100644
--- a/drivers/platform/chrome/wilco_ec/Makefile
+++ b/drivers/platform/chrome/wilco_ec/Makefile
@@ -1,6 +1,10 @@
# SPDX-License-Identifier: GPL-2.0
-wilco_ec-objs := core.o mailbox.o
+wilco_ec-objs := core.o mailbox.o properties.o sysfs.o
obj-$(CONFIG_WILCO_EC) += wilco_ec.o
wilco_ec_debugfs-objs := debugfs.o
obj-$(CONFIG_WILCO_EC_DEBUGFS) += wilco_ec_debugfs.o
+wilco_ec_events-objs := event.o
+obj-$(CONFIG_WILCO_EC_EVENTS) += wilco_ec_events.o
+wilco_ec_telem-objs := telemetry.o
+obj-$(CONFIG_WILCO_EC_TELEMETRY) += wilco_ec_telem.o
diff --git a/drivers/platform/chrome/wilco_ec/core.c b/drivers/platform/chrome/wilco_ec/core.c
index 05e1e2be1c91..3724bf4b77c6 100644
--- a/drivers/platform/chrome/wilco_ec/core.c
+++ b/drivers/platform/chrome/wilco_ec/core.c
@@ -52,9 +52,7 @@ static int wilco_ec_probe(struct platform_device *pdev)
ec->dev = dev;
mutex_init(&ec->mailbox_lock);
- /* Largest data buffer size requirement is extended data response */
- ec->data_size = sizeof(struct wilco_ec_response) +
- EC_MAILBOX_DATA_SIZE_EXTENDED;
+ ec->data_size = sizeof(struct wilco_ec_response) + EC_MAILBOX_DATA_SIZE;
ec->data_buffer = devm_kzalloc(dev, ec->data_size, GFP_KERNEL);
if (!ec->data_buffer)
return -ENOMEM;
@@ -89,8 +87,28 @@ static int wilco_ec_probe(struct platform_device *pdev)
goto unregister_debugfs;
}
+ ret = wilco_ec_add_sysfs(ec);
+ if (ret < 0) {
+ dev_err(dev, "Failed to create sysfs entries: %d", ret);
+ goto unregister_rtc;
+ }
+
+ /* Register child device that will be found by the telemetry driver. */
+ ec->telem_pdev = platform_device_register_data(dev, "wilco_telem",
+ PLATFORM_DEVID_AUTO,
+ ec, sizeof(*ec));
+ if (IS_ERR(ec->telem_pdev)) {
+ dev_err(dev, "Failed to create telemetry platform device\n");
+ ret = PTR_ERR(ec->telem_pdev);
+ goto remove_sysfs;
+ }
+
return 0;
+remove_sysfs:
+ wilco_ec_remove_sysfs(ec);
+unregister_rtc:
+ platform_device_unregister(ec->rtc_pdev);
unregister_debugfs:
if (ec->debugfs_pdev)
platform_device_unregister(ec->debugfs_pdev);
@@ -102,6 +120,8 @@ static int wilco_ec_remove(struct platform_device *pdev)
{
struct wilco_ec_device *ec = platform_get_drvdata(pdev);
+ wilco_ec_remove_sysfs(ec);
+ platform_device_unregister(ec->telem_pdev);
platform_device_unregister(ec->rtc_pdev);
if (ec->debugfs_pdev)
platform_device_unregister(ec->debugfs_pdev);
diff --git a/drivers/platform/chrome/wilco_ec/debugfs.c b/drivers/platform/chrome/wilco_ec/debugfs.c
index f163476d080d..8d65a1e2f1a3 100644
--- a/drivers/platform/chrome/wilco_ec/debugfs.c
+++ b/drivers/platform/chrome/wilco_ec/debugfs.c
@@ -16,14 +16,14 @@
#define DRV_NAME "wilco-ec-debugfs"
-/* The 256 raw bytes will take up more space when represented as a hex string */
-#define FORMATTED_BUFFER_SIZE (EC_MAILBOX_DATA_SIZE_EXTENDED * 4)
+/* The raw bytes will take up more space when represented as a hex string */
+#define FORMATTED_BUFFER_SIZE (EC_MAILBOX_DATA_SIZE * 4)
struct wilco_ec_debugfs {
struct wilco_ec_device *ec;
struct dentry *dir;
size_t response_size;
- u8 raw_data[EC_MAILBOX_DATA_SIZE_EXTENDED];
+ u8 raw_data[EC_MAILBOX_DATA_SIZE];
u8 formatted_data[FORMATTED_BUFFER_SIZE];
};
static struct wilco_ec_debugfs *debug_info;
@@ -124,12 +124,6 @@ static ssize_t raw_write(struct file *file, const char __user *user_buf,
msg.response_data = debug_info->raw_data;
msg.response_size = EC_MAILBOX_DATA_SIZE;
- /* Telemetry commands use extended response data */
- if (msg.type == WILCO_EC_MSG_TELEMETRY_LONG) {
- msg.flags |= WILCO_EC_FLAG_EXTENDED_DATA;
- msg.response_size = EC_MAILBOX_DATA_SIZE_EXTENDED;
- }
-
ret = wilco_ec_mailbox(debug_info->ec, &msg);
if (ret < 0)
return ret;
diff --git a/drivers/platform/chrome/wilco_ec/event.c b/drivers/platform/chrome/wilco_ec/event.c
new file mode 100644
index 000000000000..dba3d445623f
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/event.c
@@ -0,0 +1,581 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ACPI event handling for Wilco Embedded Controller
+ *
+ * Copyright 2019 Google LLC
+ *
+ * The Wilco Embedded Controller can create custom events that
+ * are not handled as standard ACPI objects. These events can
+ * contain information about changes in EC controlled features,
+ * such as errors and events in the dock or display. For example,
+ * an event is triggered if the dock is plugged into a display
+ * incorrectly. These events are needed for telemetry and
+ * diagnostics reasons, and for possibly alerting the user.
+
+ * These events are triggered by the EC with an ACPI Notify(0x90),
+ * and then the BIOS reads the event buffer from EC RAM via an
+ * ACPI method. When the OS receives these events via ACPI,
+ * it passes them along to this driver. The events are put into
+ * a queue which can be read by a userspace daemon via a char device
+ * that implements read() and poll(). The event queue acts as a
+ * circular buffer of size 64, so if there are no userspace consumers
+ * the kernel will not run out of memory. The char device will appear at
+ * /dev/wilco_event{n}, where n is some small non-negative integer,
+ * starting from 0. Standard ACPI events such as the battery getting
+ * plugged/unplugged can also come through this path, but they are
+ * dealt with via other paths, and are ignored here.
+
+ * To test, you can tail the binary data with
+ * $ cat /dev/wilco_event0 | hexdump -ve '1/1 "%x\n"'
+ * and then create an event by plugging/unplugging the battery.
+ */
+
+#include <linux/acpi.h>
+#include <linux/cdev.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/idr.h>
+#include <linux/io.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/spinlock.h>
+#include <linux/uaccess.h>
+#include <linux/wait.h>
+
+/* ACPI Notify event code indicating event data is available. */
+#define EC_ACPI_NOTIFY_EVENT 0x90
+/* ACPI Method to execute to retrieve event data buffer from the EC. */
+#define EC_ACPI_GET_EVENT "QSET"
+/* Maximum number of words in event data returned by the EC. */
+#define EC_ACPI_MAX_EVENT_WORDS 6
+#define EC_ACPI_MAX_EVENT_SIZE \
+ (sizeof(struct ec_event) + (EC_ACPI_MAX_EVENT_WORDS) * sizeof(u16))
+
+/* Node will appear in /dev/EVENT_DEV_NAME */
+#define EVENT_DEV_NAME "wilco_event"
+#define EVENT_CLASS_NAME EVENT_DEV_NAME
+#define DRV_NAME EVENT_DEV_NAME
+#define EVENT_DEV_NAME_FMT (EVENT_DEV_NAME "%d")
+static struct class event_class = {
+ .owner = THIS_MODULE,
+ .name = EVENT_CLASS_NAME,
+};
+
+/* Keep track of all the device numbers used. */
+#define EVENT_MAX_DEV 128
+static int event_major;
+static DEFINE_IDA(event_ida);
+
+/* Size of circular queue of events. */
+#define MAX_NUM_EVENTS 64
+
+/**
+ * struct ec_event - Extended event returned by the EC.
+ * @size: Number of 16bit words in structure after the size word.
+ * @type: Extended event type, meaningless for us.
+ * @event: Event data words. Max count is %EC_ACPI_MAX_EVENT_WORDS.
+ */
+struct ec_event {
+ u16 size;
+ u16 type;
+ u16 event[0];
+} __packed;
+
+#define ec_event_num_words(ev) (ev->size - 1)
+#define ec_event_size(ev) (sizeof(*ev) + (ec_event_num_words(ev) * sizeof(u16)))
+
+/**
+ * struct ec_event_queue - Circular queue for events.
+ * @capacity: Number of elements the queue can hold.
+ * @head: Next index to write to.
+ * @tail: Next index to read from.
+ * @entries: Array of events.
+ */
+struct ec_event_queue {
+ int capacity;
+ int head;
+ int tail;
+ struct ec_event *entries[0];
+};
+
+/* Maximum number of events to store in ec_event_queue */
+static int queue_size = 64;
+module_param(queue_size, int, 0644);
+
+static struct ec_event_queue *event_queue_new(int capacity)
+{
+ struct ec_event_queue *q;
+
+ q = kzalloc(struct_size(q, entries, capacity), GFP_KERNEL);
+ if (!q)
+ return NULL;
+
+ q->capacity = capacity;
+
+ return q;
+}
+
+static inline bool event_queue_empty(struct ec_event_queue *q)
+{
+ /* head==tail when both full and empty, but head==NULL when empty */
+ return q->head == q->tail && !q->entries[q->head];
+}
+
+static inline bool event_queue_full(struct ec_event_queue *q)
+{
+ /* head==tail when both full and empty, but head!=NULL when full */
+ return q->head == q->tail && q->entries[q->head];
+}
+
+static struct ec_event *event_queue_pop(struct ec_event_queue *q)
+{
+ struct ec_event *ev;
+
+ if (event_queue_empty(q))
+ return NULL;
+
+ ev = q->entries[q->tail];
+ q->entries[q->tail] = NULL;
+ q->tail = (q->tail + 1) % q->capacity;
+
+ return ev;
+}
+
+/*
+ * If full, overwrite the oldest event and return it so the caller
+ * can kfree it. If not full, return NULL.
+ */
+static struct ec_event *event_queue_push(struct ec_event_queue *q,
+ struct ec_event *ev)
+{
+ struct ec_event *popped = NULL;
+
+ if (event_queue_full(q))
+ popped = event_queue_pop(q);
+ q->entries[q->head] = ev;
+ q->head = (q->head + 1) % q->capacity;
+
+ return popped;
+}
+
+static void event_queue_free(struct ec_event_queue *q)
+{
+ struct ec_event *event;
+
+ while ((event = event_queue_pop(q)) != NULL)
+ kfree(event);
+
+ kfree(q);
+}
+
+/**
+ * struct event_device_data - Data for a Wilco EC device that responds to ACPI.
+ * @events: Circular queue of EC events to be provided to userspace.
+ * @queue_lock: Protect the queue from simultaneous read/writes.
+ * @wq: Wait queue to notify processes when events are available or the
+ * device has been removed.
+ * @cdev: Char dev that userspace reads() and polls() from.
+ * @dev: Device associated with the %cdev.
+ * @exist: Has the device been not been removed? Once a device has been removed,
+ * writes, reads, and new opens will fail.
+ * @available: Guarantee only one client can open() file and read from queue.
+ *
+ * There will be one of these structs for each ACPI device registered. This data
+ * is the queue of events received from ACPI that still need to be read from
+ * userspace, the device and char device that userspace is using, a wait queue
+ * used to notify different threads when something has changed, plus a flag
+ * on whether the ACPI device has been removed.
+ */
+struct event_device_data {
+ struct ec_event_queue *events;
+ spinlock_t queue_lock;
+ wait_queue_head_t wq;
+ struct device dev;
+ struct cdev cdev;
+ bool exist;
+ atomic_t available;
+};
+
+/**
+ * enqueue_events() - Place EC events in queue to be read by userspace.
+ * @adev: Device the events came from.
+ * @buf: Buffer of event data.
+ * @length: Length of event data buffer.
+ *
+ * %buf contains a number of ec_event's, packed one after the other.
+ * Each ec_event is of variable length. Start with the first event, copy it
+ * into a persistent ec_event, store that entry in the queue, move on
+ * to the next ec_event in buf, and repeat.
+ *
+ * Return: 0 on success or negative error code on failure.
+ */
+static int enqueue_events(struct acpi_device *adev, const u8 *buf, u32 length)
+{
+ struct event_device_data *dev_data = adev->driver_data;
+ struct ec_event *event, *queue_event, *old_event;
+ size_t num_words, event_size;
+ u32 offset = 0;
+
+ while (offset < length) {
+ event = (struct ec_event *)(buf + offset);
+
+ num_words = ec_event_num_words(event);
+ event_size = ec_event_size(event);
+ if (num_words > EC_ACPI_MAX_EVENT_WORDS) {
+ dev_err(&adev->dev, "Too many event words: %zu > %d\n",
+ num_words, EC_ACPI_MAX_EVENT_WORDS);
+ return -EOVERFLOW;
+ }
+
+ /* Ensure event does not overflow the available buffer */
+ if ((offset + event_size) > length) {
+ dev_err(&adev->dev, "Event exceeds buffer: %zu > %d\n",
+ offset + event_size, length);
+ return -EOVERFLOW;
+ }
+
+ /* Point to the next event in the buffer */
+ offset += event_size;
+
+ /* Copy event into the queue */
+ queue_event = kmemdup(event, event_size, GFP_KERNEL);
+ if (!queue_event)
+ return -ENOMEM;
+ spin_lock(&dev_data->queue_lock);
+ old_event = event_queue_push(dev_data->events, queue_event);
+ spin_unlock(&dev_data->queue_lock);
+ kfree(old_event);
+ wake_up_interruptible(&dev_data->wq);
+ }
+
+ return 0;
+}
+
+/**
+ * event_device_notify() - Callback when EC generates an event over ACPI.
+ * @adev: The device that the event is coming from.
+ * @value: Value passed to Notify() in ACPI.
+ *
+ * This function will read the events from the device and enqueue them.
+ */
+static void event_device_notify(struct acpi_device *adev, u32 value)
+{
+ struct acpi_buffer event_buffer = { ACPI_ALLOCATE_BUFFER, NULL };
+ union acpi_object *obj;
+ acpi_status status;
+
+ if (value != EC_ACPI_NOTIFY_EVENT) {
+ dev_err(&adev->dev, "Invalid event: 0x%08x\n", value);
+ return;
+ }
+
+ /* Execute ACPI method to get event data buffer. */
+ status = acpi_evaluate_object(adev->handle, EC_ACPI_GET_EVENT,
+ NULL, &event_buffer);
+ if (ACPI_FAILURE(status)) {
+ dev_err(&adev->dev, "Error executing ACPI method %s()\n",
+ EC_ACPI_GET_EVENT);
+ return;
+ }
+
+ obj = (union acpi_object *)event_buffer.pointer;
+ if (!obj) {
+ dev_err(&adev->dev, "Nothing returned from %s()\n",
+ EC_ACPI_GET_EVENT);
+ return;
+ }
+ if (obj->type != ACPI_TYPE_BUFFER) {
+ dev_err(&adev->dev, "Invalid object returned from %s()\n",
+ EC_ACPI_GET_EVENT);
+ kfree(obj);
+ return;
+ }
+ if (obj->buffer.length < sizeof(struct ec_event)) {
+ dev_err(&adev->dev, "Invalid buffer length %d from %s()\n",
+ obj->buffer.length, EC_ACPI_GET_EVENT);
+ kfree(obj);
+ return;
+ }
+
+ enqueue_events(adev, obj->buffer.pointer, obj->buffer.length);
+ kfree(obj);
+}
+
+static int event_open(struct inode *inode, struct file *filp)
+{
+ struct event_device_data *dev_data;
+
+ dev_data = container_of(inode->i_cdev, struct event_device_data, cdev);
+ if (!dev_data->exist)
+ return -ENODEV;
+
+ if (atomic_cmpxchg(&dev_data->available, 1, 0) == 0)
+ return -EBUSY;
+
+ /* Increase refcount on device so dev_data is not freed */
+ get_device(&dev_data->dev);
+ stream_open(inode, filp);
+ filp->private_data = dev_data;
+
+ return 0;
+}
+
+static __poll_t event_poll(struct file *filp, poll_table *wait)
+{
+ struct event_device_data *dev_data = filp->private_data;
+ __poll_t mask = 0;
+
+ poll_wait(filp, &dev_data->wq, wait);
+ if (!dev_data->exist)
+ return EPOLLHUP;
+ if (!event_queue_empty(dev_data->events))
+ mask |= EPOLLIN | EPOLLRDNORM | EPOLLPRI;
+ return mask;
+}
+
+/**
+ * event_read() - Callback for passing event data to userspace via read().
+ * @filp: The file we are reading from.
+ * @buf: Pointer to userspace buffer to fill with one event.
+ * @count: Number of bytes requested. Must be at least EC_ACPI_MAX_EVENT_SIZE.
+ * @pos: File position pointer, irrelevant since we don't support seeking.
+ *
+ * Removes the first event from the queue, places it in the passed buffer.
+ *
+ * If there are no events in the the queue, then one of two things happens,
+ * depending on if the file was opened in nonblocking mode: If in nonblocking
+ * mode, then return -EAGAIN to say there's no data. If in blocking mode, then
+ * block until an event is available.
+ *
+ * Return: Number of bytes placed in buffer, negative error code on failure.
+ */
+static ssize_t event_read(struct file *filp, char __user *buf, size_t count,
+ loff_t *pos)
+{
+ struct event_device_data *dev_data = filp->private_data;
+ struct ec_event *event;
+ ssize_t n_bytes_written = 0;
+ int err;
+
+ /* We only will give them the entire event at once */
+ if (count != 0 && count < EC_ACPI_MAX_EVENT_SIZE)
+ return -EINVAL;
+
+ spin_lock(&dev_data->queue_lock);
+ while (event_queue_empty(dev_data->events)) {
+ spin_unlock(&dev_data->queue_lock);
+ if (filp->f_flags & O_NONBLOCK)
+ return -EAGAIN;
+
+ err = wait_event_interruptible(dev_data->wq,
+ !event_queue_empty(dev_data->events) ||
+ !dev_data->exist);
+ if (err)
+ return err;
+
+ /* Device was removed as we waited? */
+ if (!dev_data->exist)
+ return -ENODEV;
+ spin_lock(&dev_data->queue_lock);
+ }
+ event = event_queue_pop(dev_data->events);
+ spin_unlock(&dev_data->queue_lock);
+ n_bytes_written = ec_event_size(event);
+ if (copy_to_user(buf, event, n_bytes_written))
+ n_bytes_written = -EFAULT;
+ kfree(event);
+
+ return n_bytes_written;
+}
+
+static int event_release(struct inode *inode, struct file *filp)
+{
+ struct event_device_data *dev_data = filp->private_data;
+
+ atomic_set(&dev_data->available, 1);
+ put_device(&dev_data->dev);
+
+ return 0;
+}
+
+static const struct file_operations event_fops = {
+ .open = event_open,
+ .poll = event_poll,
+ .read = event_read,
+ .release = event_release,
+ .llseek = no_llseek,
+ .owner = THIS_MODULE,
+};
+
+/**
+ * free_device_data() - Callback to free the event_device_data structure.
+ * @d: The device embedded in our device data, which we have been ref counting.
+ *
+ * This is called only after event_device_remove() has been called and all
+ * userspace programs have called event_release() on all the open file
+ * descriptors.
+ */
+static void free_device_data(struct device *d)
+{
+ struct event_device_data *dev_data;
+
+ dev_data = container_of(d, struct event_device_data, dev);
+ event_queue_free(dev_data->events);
+ kfree(dev_data);
+}
+
+static void hangup_device(struct event_device_data *dev_data)
+{
+ dev_data->exist = false;
+ /* Wake up the waiting processes so they can close. */
+ wake_up_interruptible(&dev_data->wq);
+ put_device(&dev_data->dev);
+}
+
+/**
+ * event_device_add() - Callback when creating a new device.
+ * @adev: ACPI device that we will be receiving events from.
+ *
+ * This finds a free minor number for the device, allocates and initializes
+ * some device data, and creates a new device and char dev node.
+ *
+ * The device data is freed in free_device_data(), which is called when
+ * %dev_data->dev is release()ed. This happens after all references to
+ * %dev_data->dev are dropped, which happens once both event_device_remove()
+ * has been called and every open()ed file descriptor has been release()ed.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int event_device_add(struct acpi_device *adev)
+{
+ struct event_device_data *dev_data;
+ int error, minor;
+
+ minor = ida_alloc_max(&event_ida, EVENT_MAX_DEV-1, GFP_KERNEL);
+ if (minor < 0) {
+ error = minor;
+ dev_err(&adev->dev, "Failed to find minor number: %d\n", error);
+ return error;
+ }
+
+ dev_data = kzalloc(sizeof(*dev_data), GFP_KERNEL);
+ if (!dev_data) {
+ error = -ENOMEM;
+ goto free_minor;
+ }
+
+ /* Initialize the device data. */
+ adev->driver_data = dev_data;
+ dev_data->events = event_queue_new(queue_size);
+ if (!dev_data->events) {
+ kfree(dev_data);
+ error = -ENOMEM;
+ goto free_minor;
+ }
+ spin_lock_init(&dev_data->queue_lock);
+ init_waitqueue_head(&dev_data->wq);
+ dev_data->exist = true;
+ atomic_set(&dev_data->available, 1);
+
+ /* Initialize the device. */
+ dev_data->dev.devt = MKDEV(event_major, minor);
+ dev_data->dev.class = &event_class;
+ dev_data->dev.release = free_device_data;
+ dev_set_name(&dev_data->dev, EVENT_DEV_NAME_FMT, minor);
+ device_initialize(&dev_data->dev);
+
+ /* Initialize the character device, and add it to userspace. */
+ cdev_init(&dev_data->cdev, &event_fops);
+ error = cdev_device_add(&dev_data->cdev, &dev_data->dev);
+ if (error)
+ goto free_dev_data;
+
+ return 0;
+
+free_dev_data:
+ hangup_device(dev_data);
+free_minor:
+ ida_simple_remove(&event_ida, minor);
+ return error;
+}
+
+static int event_device_remove(struct acpi_device *adev)
+{
+ struct event_device_data *dev_data = adev->driver_data;
+
+ cdev_device_del(&dev_data->cdev, &dev_data->dev);
+ ida_simple_remove(&event_ida, MINOR(dev_data->dev.devt));
+ hangup_device(dev_data);
+
+ return 0;
+}
+
+static const struct acpi_device_id event_acpi_ids[] = {
+ { "GOOG000D", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(acpi, event_acpi_ids);
+
+static struct acpi_driver event_driver = {
+ .name = DRV_NAME,
+ .class = DRV_NAME,
+ .ids = event_acpi_ids,
+ .ops = {
+ .add = event_device_add,
+ .notify = event_device_notify,
+ .remove = event_device_remove,
+ },
+ .owner = THIS_MODULE,
+};
+
+static int __init event_module_init(void)
+{
+ dev_t dev_num = 0;
+ int ret;
+
+ ret = class_register(&event_class);
+ if (ret) {
+ pr_err(DRV_NAME ": Failed registering class: %d\n", ret);
+ return ret;
+ }
+
+ /* Request device numbers, starting with minor=0. Save the major num. */
+ ret = alloc_chrdev_region(&dev_num, 0, EVENT_MAX_DEV, EVENT_DEV_NAME);
+ if (ret) {
+ pr_err(DRV_NAME ": Failed allocating dev numbers: %d\n", ret);
+ goto destroy_class;
+ }
+ event_major = MAJOR(dev_num);
+
+ ret = acpi_bus_register_driver(&event_driver);
+ if (ret < 0) {
+ pr_err(DRV_NAME ": Failed registering driver: %d\n", ret);
+ goto unregister_region;
+ }
+
+ return 0;
+
+unregister_region:
+ unregister_chrdev_region(MKDEV(event_major, 0), EVENT_MAX_DEV);
+destroy_class:
+ class_unregister(&event_class);
+ ida_destroy(&event_ida);
+ return ret;
+}
+
+static void __exit event_module_exit(void)
+{
+ acpi_bus_unregister_driver(&event_driver);
+ unregister_chrdev_region(MKDEV(event_major, 0), EVENT_MAX_DEV);
+ class_unregister(&event_class);
+ ida_destroy(&event_ida);
+}
+
+module_init(event_module_init);
+module_exit(event_module_exit);
+
+MODULE_AUTHOR("Nick Crews <ncrews@chromium.org>");
+MODULE_DESCRIPTION("Wilco EC ACPI event driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:" DRV_NAME);
diff --git a/drivers/platform/chrome/wilco_ec/mailbox.c b/drivers/platform/chrome/wilco_ec/mailbox.c
index 7fb58b487963..ced1f9f3dcee 100644
--- a/drivers/platform/chrome/wilco_ec/mailbox.c
+++ b/drivers/platform/chrome/wilco_ec/mailbox.c
@@ -119,7 +119,6 @@ static int wilco_ec_transfer(struct wilco_ec_device *ec,
struct wilco_ec_response *rs;
u8 checksum;
u8 flag;
- size_t size;
/* Write request header, then data */
cros_ec_lpc_io_bytes_mec(MEC_IO_WRITE, 0, sizeof(*rq), (u8 *)rq);
@@ -148,21 +147,11 @@ static int wilco_ec_transfer(struct wilco_ec_device *ec,
return -EIO;
}
- /*
- * The EC always returns either EC_MAILBOX_DATA_SIZE or
- * EC_MAILBOX_DATA_SIZE_EXTENDED bytes of data, so we need to
- * calculate the checksum on **all** of this data, even if we
- * won't use all of it.
- */
- if (msg->flags & WILCO_EC_FLAG_EXTENDED_DATA)
- size = EC_MAILBOX_DATA_SIZE_EXTENDED;
- else
- size = EC_MAILBOX_DATA_SIZE;
-
/* Read back response */
rs = ec->data_buffer;
checksum = cros_ec_lpc_io_bytes_mec(MEC_IO_READ, 0,
- sizeof(*rs) + size, (u8 *)rs);
+ sizeof(*rs) + EC_MAILBOX_DATA_SIZE,
+ (u8 *)rs);
if (checksum) {
dev_dbg(ec->dev, "bad packet checksum 0x%02x\n", rs->checksum);
return -EBADMSG;
@@ -173,9 +162,9 @@ static int wilco_ec_transfer(struct wilco_ec_device *ec,
return -EBADMSG;
}
- if (rs->data_size != size) {
- dev_dbg(ec->dev, "unexpected packet size (%u != %zu)",
- rs->data_size, size);
+ if (rs->data_size != EC_MAILBOX_DATA_SIZE) {
+ dev_dbg(ec->dev, "unexpected packet size (%u != %u)",
+ rs->data_size, EC_MAILBOX_DATA_SIZE);
return -EMSGSIZE;
}
diff --git a/drivers/platform/chrome/wilco_ec/properties.c b/drivers/platform/chrome/wilco_ec/properties.c
new file mode 100644
index 000000000000..e69682c95ea2
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/properties.c
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2019 Google LLC
+ */
+
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/string.h>
+#include <linux/unaligned/le_memmove.h>
+
+/* Operation code; what the EC should do with the property */
+enum ec_property_op {
+ EC_OP_GET = 0,
+ EC_OP_SET = 1,
+};
+
+struct ec_property_request {
+ u8 op; /* One of enum ec_property_op */
+ u8 property_id[4]; /* The 32 bit PID is stored Little Endian */
+ u8 length;
+ u8 data[WILCO_EC_PROPERTY_MAX_SIZE];
+} __packed;
+
+struct ec_property_response {
+ u8 reserved[2];
+ u8 op; /* One of enum ec_property_op */
+ u8 property_id[4]; /* The 32 bit PID is stored Little Endian */
+ u8 length;
+ u8 data[WILCO_EC_PROPERTY_MAX_SIZE];
+} __packed;
+
+static int send_property_msg(struct wilco_ec_device *ec,
+ struct ec_property_request *rq,
+ struct ec_property_response *rs)
+{
+ struct wilco_ec_message ec_msg;
+ int ret;
+
+ memset(&ec_msg, 0, sizeof(ec_msg));
+ ec_msg.type = WILCO_EC_MSG_PROPERTY;
+ ec_msg.request_data = rq;
+ ec_msg.request_size = sizeof(*rq);
+ ec_msg.response_data = rs;
+ ec_msg.response_size = sizeof(*rs);
+
+ ret = wilco_ec_mailbox(ec, &ec_msg);
+ if (ret < 0)
+ return ret;
+ if (rs->op != rq->op)
+ return -EBADMSG;
+ if (memcmp(rq->property_id, rs->property_id, sizeof(rs->property_id)))
+ return -EBADMSG;
+
+ return 0;
+}
+
+int wilco_ec_get_property(struct wilco_ec_device *ec,
+ struct wilco_ec_property_msg *prop_msg)
+{
+ struct ec_property_request rq;
+ struct ec_property_response rs;
+ int ret;
+
+ memset(&rq, 0, sizeof(rq));
+ rq.op = EC_OP_GET;
+ put_unaligned_le32(prop_msg->property_id, rq.property_id);
+
+ ret = send_property_msg(ec, &rq, &rs);
+ if (ret < 0)
+ return ret;
+
+ prop_msg->length = rs.length;
+ memcpy(prop_msg->data, rs.data, rs.length);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(wilco_ec_get_property);
+
+int wilco_ec_set_property(struct wilco_ec_device *ec,
+ struct wilco_ec_property_msg *prop_msg)
+{
+ struct ec_property_request rq;
+ struct ec_property_response rs;
+ int ret;
+
+ memset(&rq, 0, sizeof(rq));
+ rq.op = EC_OP_SET;
+ put_unaligned_le32(prop_msg->property_id, rq.property_id);
+ rq.length = prop_msg->length;
+ memcpy(rq.data, prop_msg->data, prop_msg->length);
+
+ ret = send_property_msg(ec, &rq, &rs);
+ if (ret < 0)
+ return ret;
+ if (rs.length != prop_msg->length)
+ return -EBADMSG;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(wilco_ec_set_property);
+
+int wilco_ec_get_byte_property(struct wilco_ec_device *ec, u32 property_id,
+ u8 *val)
+{
+ struct wilco_ec_property_msg msg;
+ int ret;
+
+ msg.property_id = property_id;
+
+ ret = wilco_ec_get_property(ec, &msg);
+ if (ret < 0)
+ return ret;
+ if (msg.length != 1)
+ return -EBADMSG;
+
+ *val = msg.data[0];
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(wilco_ec_get_byte_property);
+
+int wilco_ec_set_byte_property(struct wilco_ec_device *ec, u32 property_id,
+ u8 val)
+{
+ struct wilco_ec_property_msg msg;
+
+ msg.property_id = property_id;
+ msg.data[0] = val;
+ msg.length = 1;
+
+ return wilco_ec_set_property(ec, &msg);
+}
+EXPORT_SYMBOL_GPL(wilco_ec_set_byte_property);
diff --git a/drivers/platform/chrome/wilco_ec/sysfs.c b/drivers/platform/chrome/wilco_ec/sysfs.c
new file mode 100644
index 000000000000..3b86a21005d3
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/sysfs.c
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Sysfs properties to view and modify EC-controlled features on Wilco devices.
+ * The entries will appear under /sys/bus/platform/devices/GOOG000C:00/
+ *
+ * See Documentation/ABI/testing/sysfs-platform-wilco-ec for more information.
+ */
+
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/sysfs.h>
+
+#define CMD_KB_CMOS 0x7C
+#define SUB_CMD_KB_CMOS_AUTO_ON 0x03
+
+struct boot_on_ac_request {
+ u8 cmd; /* Always CMD_KB_CMOS */
+ u8 reserved1;
+ u8 sub_cmd; /* Always SUB_CMD_KB_CMOS_AUTO_ON */
+ u8 reserved3to5[3];
+ u8 val; /* Either 0 or 1 */
+ u8 reserved7;
+} __packed;
+
+#define CMD_EC_INFO 0x38
+enum get_ec_info_op {
+ CMD_GET_EC_LABEL = 0,
+ CMD_GET_EC_REV = 1,
+ CMD_GET_EC_MODEL = 2,
+ CMD_GET_EC_BUILD_DATE = 3,
+};
+
+struct get_ec_info_req {
+ u8 cmd; /* Always CMD_EC_INFO */
+ u8 reserved;
+ u8 op; /* One of enum get_ec_info_op */
+} __packed;
+
+struct get_ec_info_resp {
+ u8 reserved[2];
+ char value[9]; /* __nonstring: might not be null terminated */
+} __packed;
+
+static ssize_t boot_on_ac_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct wilco_ec_device *ec = dev_get_drvdata(dev);
+ struct boot_on_ac_request rq;
+ struct wilco_ec_message msg;
+ int ret;
+ u8 val;
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret < 0)
+ return ret;
+ if (val > 1)
+ return -EINVAL;
+
+ memset(&rq, 0, sizeof(rq));
+ rq.cmd = CMD_KB_CMOS;
+ rq.sub_cmd = SUB_CMD_KB_CMOS_AUTO_ON;
+ rq.val = val;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.type = WILCO_EC_MSG_LEGACY;
+ msg.request_data = &rq;
+ msg.request_size = sizeof(rq);
+ ret = wilco_ec_mailbox(ec, &msg);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static DEVICE_ATTR_WO(boot_on_ac);
+
+static ssize_t get_info(struct device *dev, char *buf, enum get_ec_info_op op)
+{
+ struct wilco_ec_device *ec = dev_get_drvdata(dev);
+ struct get_ec_info_req req = { .cmd = CMD_EC_INFO, .op = op };
+ struct get_ec_info_resp resp;
+ int ret;
+
+ struct wilco_ec_message msg = {
+ .type = WILCO_EC_MSG_LEGACY,
+ .request_data = &req,
+ .request_size = sizeof(req),
+ .response_data = &resp,
+ .response_size = sizeof(resp),
+ };
+
+ ret = wilco_ec_mailbox(ec, &msg);
+ if (ret < 0)
+ return ret;
+
+ return scnprintf(buf, PAGE_SIZE, "%.*s\n", (int)sizeof(resp.value),
+ (char *)&resp.value);
+}
+
+static ssize_t version_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return get_info(dev, buf, CMD_GET_EC_LABEL);
+}
+
+static DEVICE_ATTR_RO(version);
+
+static ssize_t build_revision_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return get_info(dev, buf, CMD_GET_EC_REV);
+}
+
+static DEVICE_ATTR_RO(build_revision);
+
+static ssize_t build_date_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return get_info(dev, buf, CMD_GET_EC_BUILD_DATE);
+}
+
+static DEVICE_ATTR_RO(build_date);
+
+static ssize_t model_number_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return get_info(dev, buf, CMD_GET_EC_MODEL);
+}
+
+static DEVICE_ATTR_RO(model_number);
+
+
+static struct attribute *wilco_dev_attrs[] = {
+ &dev_attr_boot_on_ac.attr,
+ &dev_attr_build_date.attr,
+ &dev_attr_build_revision.attr,
+ &dev_attr_model_number.attr,
+ &dev_attr_version.attr,
+ NULL,
+};
+
+static struct attribute_group wilco_dev_attr_group = {
+ .attrs = wilco_dev_attrs,
+};
+
+int wilco_ec_add_sysfs(struct wilco_ec_device *ec)
+{
+ return sysfs_create_group(&ec->dev->kobj, &wilco_dev_attr_group);
+}
+
+void wilco_ec_remove_sysfs(struct wilco_ec_device *ec)
+{
+ sysfs_remove_group(&ec->dev->kobj, &wilco_dev_attr_group);
+}
diff --git a/drivers/platform/chrome/wilco_ec/telemetry.c b/drivers/platform/chrome/wilco_ec/telemetry.c
new file mode 100644
index 000000000000..94cdc166c840
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/telemetry.c
@@ -0,0 +1,450 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Telemetry communication for Wilco EC
+ *
+ * Copyright 2019 Google LLC
+ *
+ * The Wilco Embedded Controller is able to send telemetry data
+ * which is useful for enterprise applications. A daemon running on
+ * the OS sends a command to the EC via a write() to a char device,
+ * and can read the response with a read(). The write() request is
+ * verified by the driver to ensure that it is performing only one
+ * of the whitelisted commands, and that no extraneous data is
+ * being transmitted to the EC. The response is passed directly
+ * back to the reader with no modification.
+ *
+ * The character device will appear as /dev/wilco_telemN, where N
+ * is some small non-negative integer, starting with 0. Only one
+ * process may have the file descriptor open at a time. The calling
+ * userspace program needs to keep the device file descriptor open
+ * between the calls to write() and read() in order to preserve the
+ * response. Up to 32 bytes will be available for reading.
+ *
+ * For testing purposes, try requesting the EC's firmware build
+ * date, by sending the WILCO_EC_TELEM_GET_VERSION command with
+ * argument index=3. i.e. write [0x38, 0x00, 0x03]
+ * to the device node. An ASCII string of the build date is
+ * returned.
+ */
+
+#include <linux/cdev.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/module.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/uaccess.h>
+
+#define TELEM_DEV_NAME "wilco_telem"
+#define TELEM_CLASS_NAME TELEM_DEV_NAME
+#define DRV_NAME TELEM_DEV_NAME
+#define TELEM_DEV_NAME_FMT (TELEM_DEV_NAME "%d")
+static struct class telem_class = {
+ .owner = THIS_MODULE,
+ .name = TELEM_CLASS_NAME,
+};
+
+/* Keep track of all the device numbers used. */
+#define TELEM_MAX_DEV 128
+static int telem_major;
+static DEFINE_IDA(telem_ida);
+
+/* EC telemetry command codes */
+#define WILCO_EC_TELEM_GET_LOG 0x99
+#define WILCO_EC_TELEM_GET_VERSION 0x38
+#define WILCO_EC_TELEM_GET_FAN_INFO 0x2E
+#define WILCO_EC_TELEM_GET_DIAG_INFO 0xFA
+#define WILCO_EC_TELEM_GET_TEMP_INFO 0x95
+#define WILCO_EC_TELEM_GET_TEMP_READ 0x2C
+#define WILCO_EC_TELEM_GET_BATT_EXT_INFO 0x07
+
+#define TELEM_ARGS_SIZE_MAX 30
+
+/**
+ * struct wilco_ec_telem_request - Telemetry command and arguments sent to EC.
+ * @command: One of WILCO_EC_TELEM_GET_* command codes.
+ * @reserved: Must be 0.
+ * @args: The first N bytes are one of telem_args_get_* structs, the rest is 0.
+ */
+struct wilco_ec_telem_request {
+ u8 command;
+ u8 reserved;
+ u8 args[TELEM_ARGS_SIZE_MAX];
+} __packed;
+
+/*
+ * The following telem_args_get_* structs are embedded within the |args| field
+ * of wilco_ec_telem_request.
+ */
+
+struct telem_args_get_log {
+ u8 log_type;
+ u8 log_index;
+} __packed;
+
+/*
+ * Get a piece of info about the EC firmware version:
+ * 0 = label
+ * 1 = svn_rev
+ * 2 = model_no
+ * 3 = build_date
+ * 4 = frio_version
+ */
+struct telem_args_get_version {
+ u8 index;
+} __packed;
+
+struct telem_args_get_fan_info {
+ u8 command;
+ u8 fan_number;
+ u8 arg;
+} __packed;
+
+struct telem_args_get_diag_info {
+ u8 type;
+ u8 sub_type;
+} __packed;
+
+struct telem_args_get_temp_info {
+ u8 command;
+ u8 index;
+ u8 field;
+ u8 zone;
+} __packed;
+
+struct telem_args_get_temp_read {
+ u8 sensor_index;
+} __packed;
+
+struct telem_args_get_batt_ext_info {
+ u8 var_args[5];
+} __packed;
+
+/**
+ * check_telem_request() - Ensure that a request from userspace is valid.
+ * @rq: Request buffer copied from userspace.
+ * @size: Number of bytes copied from userspace.
+ *
+ * Return: 0 if valid, -EINVAL if bad command or reserved byte is non-zero,
+ * -EMSGSIZE if the request is too long.
+ *
+ * We do not want to allow userspace to send arbitrary telemetry commands to
+ * the EC. Therefore we check to ensure that
+ * 1. The request follows the format of struct wilco_ec_telem_request.
+ * 2. The supplied command code is one of the whitelisted commands.
+ * 3. The request only contains the necessary data for the header and arguments.
+ */
+static int check_telem_request(struct wilco_ec_telem_request *rq,
+ size_t size)
+{
+ size_t max_size = offsetof(struct wilco_ec_telem_request, args);
+
+ if (rq->reserved)
+ return -EINVAL;
+
+ switch (rq->command) {
+ case WILCO_EC_TELEM_GET_LOG:
+ max_size += sizeof(struct telem_args_get_log);
+ break;
+ case WILCO_EC_TELEM_GET_VERSION:
+ max_size += sizeof(struct telem_args_get_version);
+ break;
+ case WILCO_EC_TELEM_GET_FAN_INFO:
+ max_size += sizeof(struct telem_args_get_fan_info);
+ break;
+ case WILCO_EC_TELEM_GET_DIAG_INFO:
+ max_size += sizeof(struct telem_args_get_diag_info);
+ break;
+ case WILCO_EC_TELEM_GET_TEMP_INFO:
+ max_size += sizeof(struct telem_args_get_temp_info);
+ break;
+ case WILCO_EC_TELEM_GET_TEMP_READ:
+ max_size += sizeof(struct telem_args_get_temp_read);
+ break;
+ case WILCO_EC_TELEM_GET_BATT_EXT_INFO:
+ max_size += sizeof(struct telem_args_get_batt_ext_info);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return (size <= max_size) ? 0 : -EMSGSIZE;
+}
+
+/**
+ * struct telem_device_data - Data for a Wilco EC device that queries telemetry.
+ * @cdev: Char dev that userspace reads and polls from.
+ * @dev: Device associated with the %cdev.
+ * @ec: Wilco EC that we will be communicating with using the mailbox interface.
+ * @available: Boolean of if the device can be opened.
+ */
+struct telem_device_data {
+ struct device dev;
+ struct cdev cdev;
+ struct wilco_ec_device *ec;
+ atomic_t available;
+};
+
+#define TELEM_RESPONSE_SIZE EC_MAILBOX_DATA_SIZE
+
+/**
+ * struct telem_session_data - Data that exists between open() and release().
+ * @dev_data: Pointer to get back to the device data and EC.
+ * @request: Command and arguments sent to EC.
+ * @response: Response buffer of data from EC.
+ * @has_msg: Is there data available to read from a previous write?
+ */
+struct telem_session_data {
+ struct telem_device_data *dev_data;
+ struct wilco_ec_telem_request request;
+ u8 response[TELEM_RESPONSE_SIZE];
+ bool has_msg;
+};
+
+/**
+ * telem_open() - Callback for when the device node is opened.
+ * @inode: inode for this char device node.
+ * @filp: file for this char device node.
+ *
+ * We need to ensure that after writing a command to the device,
+ * the same userspace process reads the corresponding result.
+ * Therefore, we increment a refcount on opening the device, so that
+ * only one process can communicate with the EC at a time.
+ *
+ * Return: 0 on success, or negative error code on failure.
+ */
+static int telem_open(struct inode *inode, struct file *filp)
+{
+ struct telem_device_data *dev_data;
+ struct telem_session_data *sess_data;
+
+ /* Ensure device isn't already open */
+ dev_data = container_of(inode->i_cdev, struct telem_device_data, cdev);
+ if (atomic_cmpxchg(&dev_data->available, 1, 0) == 0)
+ return -EBUSY;
+
+ get_device(&dev_data->dev);
+
+ sess_data = kzalloc(sizeof(*sess_data), GFP_KERNEL);
+ if (!sess_data) {
+ atomic_set(&dev_data->available, 1);
+ return -ENOMEM;
+ }
+ sess_data->dev_data = dev_data;
+ sess_data->has_msg = false;
+
+ nonseekable_open(inode, filp);
+ filp->private_data = sess_data;
+
+ return 0;
+}
+
+static ssize_t telem_write(struct file *filp, const char __user *buf,
+ size_t count, loff_t *pos)
+{
+ struct telem_session_data *sess_data = filp->private_data;
+ struct wilco_ec_message msg = {};
+ int ret;
+
+ if (count > sizeof(sess_data->request))
+ return -EMSGSIZE;
+ if (copy_from_user(&sess_data->request, buf, count))
+ return -EFAULT;
+ ret = check_telem_request(&sess_data->request, count);
+ if (ret < 0)
+ return ret;
+
+ memset(sess_data->response, 0, sizeof(sess_data->response));
+ msg.type = WILCO_EC_MSG_TELEMETRY;
+ msg.request_data = &sess_data->request;
+ msg.request_size = sizeof(sess_data->request);
+ msg.response_data = sess_data->response;
+ msg.response_size = sizeof(sess_data->response);
+
+ ret = wilco_ec_mailbox(sess_data->dev_data->ec, &msg);
+ if (ret < 0)
+ return ret;
+ if (ret != sizeof(sess_data->response))
+ return -EMSGSIZE;
+
+ sess_data->has_msg = true;
+
+ return count;
+}
+
+static ssize_t telem_read(struct file *filp, char __user *buf, size_t count,
+ loff_t *pos)
+{
+ struct telem_session_data *sess_data = filp->private_data;
+
+ if (!sess_data->has_msg)
+ return -ENODATA;
+ if (count > sizeof(sess_data->response))
+ return -EINVAL;
+
+ if (copy_to_user(buf, sess_data->response, count))
+ return -EFAULT;
+
+ sess_data->has_msg = false;
+
+ return count;
+}
+
+static int telem_release(struct inode *inode, struct file *filp)
+{
+ struct telem_session_data *sess_data = filp->private_data;
+
+ atomic_set(&sess_data->dev_data->available, 1);
+ put_device(&sess_data->dev_data->dev);
+ kfree(sess_data);
+
+ return 0;
+}
+
+static const struct file_operations telem_fops = {
+ .open = telem_open,
+ .write = telem_write,
+ .read = telem_read,
+ .release = telem_release,
+ .llseek = no_llseek,
+ .owner = THIS_MODULE,
+};
+
+/**
+ * telem_device_free() - Callback to free the telem_device_data structure.
+ * @d: The device embedded in our device data, which we have been ref counting.
+ *
+ * Once all open file descriptors are closed and the device has been removed,
+ * the refcount of the device will fall to 0 and this will be called.
+ */
+static void telem_device_free(struct device *d)
+{
+ struct telem_device_data *dev_data;
+
+ dev_data = container_of(d, struct telem_device_data, dev);
+ kfree(dev_data);
+}
+
+/**
+ * telem_device_probe() - Callback when creating a new device.
+ * @pdev: platform device that we will be receiving telems from.
+ *
+ * This finds a free minor number for the device, allocates and initializes
+ * some device data, and creates a new device and char dev node.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int telem_device_probe(struct platform_device *pdev)
+{
+ struct telem_device_data *dev_data;
+ int error, minor;
+
+ /* Get the next available device number */
+ minor = ida_alloc_max(&telem_ida, TELEM_MAX_DEV-1, GFP_KERNEL);
+ if (minor < 0) {
+ error = minor;
+ dev_err(&pdev->dev, "Failed to find minor number: %d", error);
+ return error;
+ }
+
+ dev_data = kzalloc(sizeof(*dev_data), GFP_KERNEL);
+ if (!dev_data) {
+ ida_simple_remove(&telem_ida, minor);
+ return -ENOMEM;
+ }
+
+ /* Initialize the device data */
+ dev_data->ec = dev_get_platdata(&pdev->dev);
+ atomic_set(&dev_data->available, 1);
+ platform_set_drvdata(pdev, dev_data);
+
+ /* Initialize the device */
+ dev_data->dev.devt = MKDEV(telem_major, minor);
+ dev_data->dev.class = &telem_class;
+ dev_data->dev.release = telem_device_free;
+ dev_set_name(&dev_data->dev, TELEM_DEV_NAME_FMT, minor);
+ device_initialize(&dev_data->dev);
+
+ /* Initialize the character device and add it to userspace */;
+ cdev_init(&dev_data->cdev, &telem_fops);
+ error = cdev_device_add(&dev_data->cdev, &dev_data->dev);
+ if (error) {
+ put_device(&dev_data->dev);
+ ida_simple_remove(&telem_ida, minor);
+ return error;
+ }
+
+ return 0;
+}
+
+static int telem_device_remove(struct platform_device *pdev)
+{
+ struct telem_device_data *dev_data = platform_get_drvdata(pdev);
+
+ cdev_device_del(&dev_data->cdev, &dev_data->dev);
+ put_device(&dev_data->dev);
+ ida_simple_remove(&telem_ida, MINOR(dev_data->dev.devt));
+
+ return 0;
+}
+
+static struct platform_driver telem_driver = {
+ .probe = telem_device_probe,
+ .remove = telem_device_remove,
+ .driver = {
+ .name = DRV_NAME,
+ },
+};
+
+static int __init telem_module_init(void)
+{
+ dev_t dev_num = 0;
+ int ret;
+
+ ret = class_register(&telem_class);
+ if (ret) {
+ pr_err(DRV_NAME ": Failed registering class: %d", ret);
+ return ret;
+ }
+
+ /* Request the kernel for device numbers, starting with minor=0 */
+ ret = alloc_chrdev_region(&dev_num, 0, TELEM_MAX_DEV, TELEM_DEV_NAME);
+ if (ret) {
+ pr_err(DRV_NAME ": Failed allocating dev numbers: %d", ret);
+ goto destroy_class;
+ }
+ telem_major = MAJOR(dev_num);
+
+ ret = platform_driver_register(&telem_driver);
+ if (ret < 0) {
+ pr_err(DRV_NAME ": Failed registering driver: %d\n", ret);
+ goto unregister_region;
+ }
+
+ return 0;
+
+unregister_region:
+ unregister_chrdev_region(MKDEV(telem_major, 0), TELEM_MAX_DEV);
+destroy_class:
+ class_unregister(&telem_class);
+ ida_destroy(&telem_ida);
+ return ret;
+}
+
+static void __exit telem_module_exit(void)
+{
+ platform_driver_unregister(&telem_driver);
+ unregister_chrdev_region(MKDEV(telem_major, 0), TELEM_MAX_DEV);
+ class_unregister(&telem_class);
+ ida_destroy(&telem_ida);
+}
+
+module_init(telem_module_init);
+module_exit(telem_module_exit);
+
+MODULE_AUTHOR("Nick Crews <ncrews@chromium.org>");
+MODULE_DESCRIPTION("Wilco EC telemetry driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:" DRV_NAME);