summaryrefslogtreecommitdiffstats
path: root/drivers/platform/chrome/wilco_ec
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-11 08:27:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-11 08:27:49 +0000
commitace9429bb58fd418f0c81d4c2835699bddf6bde6 (patch)
treeb2d64bc10158fdd5497876388cd68142ca374ed3 /drivers/platform/chrome/wilco_ec
parentInitial commit. (diff)
downloadlinux-ace9429bb58fd418f0c81d4c2835699bddf6bde6.tar.xz
linux-ace9429bb58fd418f0c81d4c2835699bddf6bde6.zip
Adding upstream version 6.6.15.upstream/6.6.15
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/platform/chrome/wilco_ec')
-rw-r--r--drivers/platform/chrome/wilco_ec/Kconfig38
-rw-r--r--drivers/platform/chrome/wilco_ec/Makefile11
-rw-r--r--drivers/platform/chrome/wilco_ec/core.c169
-rw-r--r--drivers/platform/chrome/wilco_ec/debugfs.c283
-rw-r--r--drivers/platform/chrome/wilco_ec/event.c578
-rw-r--r--drivers/platform/chrome/wilco_ec/keyboard_leds.c203
-rw-r--r--drivers/platform/chrome/wilco_ec/mailbox.c215
-rw-r--r--drivers/platform/chrome/wilco_ec/properties.c135
-rw-r--r--drivers/platform/chrome/wilco_ec/sysfs.c250
-rw-r--r--drivers/platform/chrome/wilco_ec/telemetry.c471
10 files changed, 2353 insertions, 0 deletions
diff --git a/drivers/platform/chrome/wilco_ec/Kconfig b/drivers/platform/chrome/wilco_ec/Kconfig
new file mode 100644
index 0000000000..49e8530ca0
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/Kconfig
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config WILCO_EC
+ tristate "ChromeOS Wilco Embedded Controller"
+ depends on X86 || COMPILE_TEST
+ depends on ACPI && CROS_EC_LPC && LEDS_CLASS
+ 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
+ with a checksum.
+
+ To compile this driver as a module, choose M here: the
+ module will be called wilco_ec.
+
+config WILCO_EC_DEBUGFS
+ tristate "Enable raw access to EC via debugfs"
+ depends on WILCO_EC
+ help
+ If you say Y here, you get support for sending raw commands to
+ the Wilco EC via debugfs. These commands do not do any byte
+ 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
new file mode 100644
index 0000000000..ecb3145cab
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/Makefile
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0
+
+wilco_ec-objs := core.o keyboard_leds.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
new file mode 100644
index 0000000000..d6a994bdc1
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/core.c
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Core driver for Wilco Embedded Controller
+ *
+ * Copyright 2018 Google LLC
+ *
+ * This is the entry point for the drivers that control the Wilco EC.
+ */
+
+#include <linux/acpi.h>
+#include <linux/device.h>
+#include <linux/ioport.h>
+#include <linux/module.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/platform_device.h>
+
+#include "../cros_ec_lpc_mec.h"
+
+#define DRV_NAME "wilco-ec"
+
+static struct resource *wilco_get_resource(struct platform_device *pdev,
+ int index)
+{
+ struct device *dev = &pdev->dev;
+ struct resource *res;
+
+ res = platform_get_resource(pdev, IORESOURCE_IO, index);
+ if (!res) {
+ dev_dbg(dev, "Couldn't find IO resource %d\n", index);
+ return res;
+ }
+
+ return devm_request_region(dev, res->start, resource_size(res),
+ dev_name(dev));
+}
+
+static int wilco_ec_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct wilco_ec_device *ec;
+ int ret;
+
+ ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL);
+ if (!ec)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, ec);
+ ec->dev = dev;
+ mutex_init(&ec->mailbox_lock);
+
+ 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;
+
+ /* Prepare access to IO regions provided by ACPI */
+ ec->io_data = wilco_get_resource(pdev, 0); /* Host Data */
+ ec->io_command = wilco_get_resource(pdev, 1); /* Host Command */
+ ec->io_packet = wilco_get_resource(pdev, 2); /* MEC EMI */
+ if (!ec->io_data || !ec->io_command || !ec->io_packet)
+ return -ENODEV;
+
+ /* Initialize cros_ec register interface for communication */
+ cros_ec_lpc_mec_init(ec->io_packet->start,
+ ec->io_packet->start + EC_MAILBOX_DATA_SIZE);
+
+ /*
+ * Register a child device that will be found by the debugfs driver.
+ * Ignore failure.
+ */
+ ec->debugfs_pdev = platform_device_register_data(dev,
+ "wilco-ec-debugfs",
+ PLATFORM_DEVID_AUTO,
+ NULL, 0);
+
+ /* Register a child device that will be found by the RTC driver. */
+ ec->rtc_pdev = platform_device_register_data(dev, "rtc-wilco-ec",
+ PLATFORM_DEVID_AUTO,
+ NULL, 0);
+ if (IS_ERR(ec->rtc_pdev)) {
+ dev_err(dev, "Failed to create RTC platform device\n");
+ ret = PTR_ERR(ec->rtc_pdev);
+ goto unregister_debugfs;
+ }
+
+ /* Set up the keyboard backlight LEDs. */
+ ret = wilco_keyboard_leds_init(ec);
+ if (ret < 0) {
+ dev_err(dev,
+ "Failed to initialize keyboard LEDs: %d\n",
+ ret);
+ goto unregister_rtc;
+ }
+
+ ret = wilco_ec_add_sysfs(ec);
+ if (ret < 0) {
+ dev_err(dev, "Failed to create sysfs entries: %d\n", ret);
+ goto unregister_rtc;
+ }
+
+ /* Register child device to be found by charger config driver. */
+ ec->charger_pdev = platform_device_register_data(dev, "wilco-charger",
+ PLATFORM_DEVID_AUTO,
+ NULL, 0);
+ if (IS_ERR(ec->charger_pdev)) {
+ dev_err(dev, "Failed to create charger platform device\n");
+ ret = PTR_ERR(ec->charger_pdev);
+ goto remove_sysfs;
+ }
+
+ /* 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 unregister_charge_config;
+ }
+
+ return 0;
+
+unregister_charge_config:
+ platform_device_unregister(ec->charger_pdev);
+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);
+ return ret;
+}
+
+static int wilco_ec_remove(struct platform_device *pdev)
+{
+ struct wilco_ec_device *ec = platform_get_drvdata(pdev);
+
+ platform_device_unregister(ec->telem_pdev);
+ platform_device_unregister(ec->charger_pdev);
+ wilco_ec_remove_sysfs(ec);
+ platform_device_unregister(ec->rtc_pdev);
+ if (ec->debugfs_pdev)
+ platform_device_unregister(ec->debugfs_pdev);
+ return 0;
+}
+
+static const struct acpi_device_id wilco_ec_acpi_device_ids[] = {
+ { "GOOG000C", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(acpi, wilco_ec_acpi_device_ids);
+
+static struct platform_driver wilco_ec_driver = {
+ .driver = {
+ .name = DRV_NAME,
+ .acpi_match_table = wilco_ec_acpi_device_ids,
+ },
+ .probe = wilco_ec_probe,
+ .remove = wilco_ec_remove,
+};
+
+module_platform_driver(wilco_ec_driver);
+
+MODULE_AUTHOR("Nick Crews <ncrews@chromium.org>");
+MODULE_AUTHOR("Duncan Laurie <dlaurie@chromium.org>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("ChromeOS Wilco Embedded Controller driver");
+MODULE_ALIAS("platform:" DRV_NAME);
diff --git a/drivers/platform/chrome/wilco_ec/debugfs.c b/drivers/platform/chrome/wilco_ec/debugfs.c
new file mode 100644
index 0000000000..7a13f13b16
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/debugfs.c
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * debugfs attributes for Wilco EC
+ *
+ * Copyright 2019 Google LLC
+ *
+ * See Documentation/ABI/testing/debugfs-wilco-ec for usage.
+ */
+
+#include <linux/ctype.h>
+#include <linux/debugfs.h>
+#include <linux/fs.h>
+#include <linux/module.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/platform_device.h>
+
+#define DRV_NAME "wilco-ec-debugfs"
+
+/* 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];
+ u8 formatted_data[FORMATTED_BUFFER_SIZE];
+};
+static struct wilco_ec_debugfs *debug_info;
+
+/**
+ * parse_hex_sentence() - Convert a ascii hex representation into byte array.
+ * @in: Input buffer of ascii.
+ * @isize: Length of input buffer.
+ * @out: Output buffer.
+ * @osize: Length of output buffer, e.g. max number of bytes to parse.
+ *
+ * An valid input is a series of ascii hexadecimal numbers, separated by spaces.
+ * An example valid input is
+ * " 00 f2 0 000076 6 0 ff"
+ *
+ * If an individual "word" within the hex sentence is longer than MAX_WORD_SIZE,
+ * then the sentence is illegal, and parsing will fail.
+ *
+ * Return: Number of bytes parsed, or negative error code on failure.
+ */
+static int parse_hex_sentence(const char *in, int isize, u8 *out, int osize)
+{
+ int n_parsed = 0;
+ int word_start = 0;
+ int word_end;
+ int word_len;
+ /* Temp buffer for holding a "word" of chars that represents one byte */
+ #define MAX_WORD_SIZE 16
+ char tmp[MAX_WORD_SIZE + 1];
+ u8 byte;
+
+ while (word_start < isize && n_parsed < osize) {
+ /* Find the start of the next word */
+ while (word_start < isize && isspace(in[word_start]))
+ word_start++;
+ /* reached the end of the input before next word? */
+ if (word_start >= isize)
+ break;
+
+ /* Find the end of this word */
+ word_end = word_start;
+ while (word_end < isize && !isspace(in[word_end]))
+ word_end++;
+
+ /* Copy to a tmp NULL terminated string */
+ word_len = word_end - word_start;
+ if (word_len > MAX_WORD_SIZE)
+ return -EINVAL;
+ memcpy(tmp, in + word_start, word_len);
+ tmp[word_len] = '\0';
+
+ /*
+ * Convert from hex string, place in output. If fails to parse,
+ * just return -EINVAL because specific error code is only
+ * relevant for this one word, returning it would be confusing.
+ */
+ if (kstrtou8(tmp, 16, &byte))
+ return -EINVAL;
+ out[n_parsed++] = byte;
+
+ word_start = word_end;
+ }
+ return n_parsed;
+}
+
+/* The message type takes up two bytes*/
+#define TYPE_AND_DATA_SIZE ((EC_MAILBOX_DATA_SIZE) + 2)
+
+static ssize_t raw_write(struct file *file, const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ char *buf = debug_info->formatted_data;
+ struct wilco_ec_message msg;
+ u8 request_data[TYPE_AND_DATA_SIZE];
+ ssize_t kcount;
+ int ret;
+
+ if (count > FORMATTED_BUFFER_SIZE)
+ return -EINVAL;
+
+ kcount = simple_write_to_buffer(buf, FORMATTED_BUFFER_SIZE, ppos,
+ user_buf, count);
+ if (kcount < 0)
+ return kcount;
+
+ ret = parse_hex_sentence(buf, kcount, request_data, TYPE_AND_DATA_SIZE);
+ if (ret < 0)
+ return ret;
+ /* Need at least two bytes for message type and one byte of data */
+ if (ret < 3)
+ return -EINVAL;
+
+ msg.type = request_data[0] << 8 | request_data[1];
+ msg.flags = 0;
+ msg.request_data = request_data + 2;
+ msg.request_size = ret - 2;
+ memset(debug_info->raw_data, 0, sizeof(debug_info->raw_data));
+ msg.response_data = debug_info->raw_data;
+ msg.response_size = EC_MAILBOX_DATA_SIZE;
+
+ ret = wilco_ec_mailbox(debug_info->ec, &msg);
+ if (ret < 0)
+ return ret;
+ debug_info->response_size = ret;
+
+ return count;
+}
+
+static ssize_t raw_read(struct file *file, char __user *user_buf, size_t count,
+ loff_t *ppos)
+{
+ int fmt_len = 0;
+
+ if (debug_info->response_size) {
+ fmt_len = hex_dump_to_buffer(debug_info->raw_data,
+ debug_info->response_size,
+ 16, 1, debug_info->formatted_data,
+ sizeof(debug_info->formatted_data),
+ true);
+ /* Only return response the first time it is read */
+ debug_info->response_size = 0;
+ }
+
+ return simple_read_from_buffer(user_buf, count, ppos,
+ debug_info->formatted_data, fmt_len);
+}
+
+static const struct file_operations fops_raw = {
+ .owner = THIS_MODULE,
+ .read = raw_read,
+ .write = raw_write,
+ .llseek = no_llseek,
+};
+
+#define CMD_KB_CHROME 0x88
+#define SUB_CMD_H1_GPIO 0x0A
+#define SUB_CMD_TEST_EVENT 0x0B
+
+struct ec_request {
+ u8 cmd; /* Always CMD_KB_CHROME */
+ u8 reserved;
+ u8 sub_cmd;
+} __packed;
+
+struct ec_response {
+ u8 status; /* 0 if allowed */
+ u8 val;
+} __packed;
+
+static int send_ec_cmd(struct wilco_ec_device *ec, u8 sub_cmd, u8 *out_val)
+{
+ struct ec_request rq;
+ struct ec_response rs;
+ struct wilco_ec_message msg;
+ int ret;
+
+ memset(&rq, 0, sizeof(rq));
+ rq.cmd = CMD_KB_CHROME;
+ rq.sub_cmd = sub_cmd;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.type = WILCO_EC_MSG_LEGACY;
+ msg.request_data = &rq;
+ msg.request_size = sizeof(rq);
+ msg.response_data = &rs;
+ msg.response_size = sizeof(rs);
+ ret = wilco_ec_mailbox(ec, &msg);
+ if (ret < 0)
+ return ret;
+ if (rs.status)
+ return -EIO;
+
+ *out_val = rs.val;
+
+ return 0;
+}
+
+/**
+ * h1_gpio_get() - Gets h1 gpio status.
+ * @arg: The wilco EC device.
+ * @val: BIT(0)=ENTRY_TO_FACT_MODE, BIT(1)=SPI_CHROME_SEL
+ */
+static int h1_gpio_get(void *arg, u64 *val)
+{
+ int ret;
+
+ ret = send_ec_cmd(arg, SUB_CMD_H1_GPIO, (u8 *)val);
+ if (ret == 0)
+ *val &= 0xFF;
+ return ret;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_h1_gpio, h1_gpio_get, NULL, "0x%02llx\n");
+
+/**
+ * test_event_set() - Sends command to EC to cause an EC test event.
+ * @arg: The wilco EC device.
+ * @val: unused.
+ */
+static int test_event_set(void *arg, u64 val)
+{
+ u8 ret;
+
+ return send_ec_cmd(arg, SUB_CMD_TEST_EVENT, &ret);
+}
+
+/* Format is unused since it is only required for get method which is NULL */
+DEFINE_DEBUGFS_ATTRIBUTE(fops_test_event, NULL, test_event_set, "%llu\n");
+
+/**
+ * wilco_ec_debugfs_probe() - Create the debugfs node
+ * @pdev: The platform device, probably created in core.c
+ *
+ * Try to create a debugfs node. If it fails, then we don't want to change
+ * behavior at all, this is for debugging after all. Just fail silently.
+ *
+ * Return: 0 always.
+ */
+static int wilco_ec_debugfs_probe(struct platform_device *pdev)
+{
+ struct wilco_ec_device *ec = dev_get_drvdata(pdev->dev.parent);
+
+ debug_info = devm_kzalloc(&pdev->dev, sizeof(*debug_info), GFP_KERNEL);
+ if (!debug_info)
+ return 0;
+ debug_info->ec = ec;
+ debug_info->dir = debugfs_create_dir("wilco_ec", NULL);
+ debugfs_create_file("raw", 0644, debug_info->dir, NULL, &fops_raw);
+ debugfs_create_file("h1_gpio", 0444, debug_info->dir, ec,
+ &fops_h1_gpio);
+ debugfs_create_file("test_event", 0200, debug_info->dir, ec,
+ &fops_test_event);
+
+ return 0;
+}
+
+static int wilco_ec_debugfs_remove(struct platform_device *pdev)
+{
+ debugfs_remove_recursive(debug_info->dir);
+
+ return 0;
+}
+
+static struct platform_driver wilco_ec_debugfs_driver = {
+ .driver = {
+ .name = DRV_NAME,
+ },
+ .probe = wilco_ec_debugfs_probe,
+ .remove = wilco_ec_debugfs_remove,
+};
+
+module_platform_driver(wilco_ec_debugfs_driver);
+
+MODULE_ALIAS("platform:" DRV_NAME);
+MODULE_AUTHOR("Nick Crews <ncrews@chromium.org>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("Wilco EC debugfs driver");
diff --git a/drivers/platform/chrome/wilco_ec/event.c b/drivers/platform/chrome/wilco_ec/event.c
new file mode 100644
index 0000000000..a40f60bcef
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/event.c
@@ -0,0 +1,578 @@
+// 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 = {
+ .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[];
+} __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[];
+};
+
+/* 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 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 void 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);
+}
+
+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/keyboard_leds.c b/drivers/platform/chrome/wilco_ec/keyboard_leds.c
new file mode 100644
index 0000000000..6ce9c67820
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/keyboard_leds.c
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Keyboard backlight LED driver for the Wilco Embedded Controller
+ *
+ * Copyright 2019 Google LLC
+ *
+ * Since the EC will never change the backlight level of its own accord,
+ * we don't need to implement a brightness_get() method.
+ */
+
+#include <linux/device.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/slab.h>
+
+#define WILCO_EC_COMMAND_KBBL 0x75
+#define WILCO_KBBL_MODE_FLAG_PWM BIT(1) /* Set brightness by percent. */
+#define WILCO_KBBL_DEFAULT_BRIGHTNESS 0
+
+struct wilco_keyboard_leds {
+ struct wilco_ec_device *ec;
+ struct led_classdev keyboard;
+};
+
+enum wilco_kbbl_subcommand {
+ WILCO_KBBL_SUBCMD_GET_FEATURES = 0x00,
+ WILCO_KBBL_SUBCMD_GET_STATE = 0x01,
+ WILCO_KBBL_SUBCMD_SET_STATE = 0x02,
+};
+
+/**
+ * struct wilco_keyboard_leds_msg - Message to/from EC for keyboard LED control.
+ * @command: Always WILCO_EC_COMMAND_KBBL.
+ * @status: Set by EC to 0 on success, 0xFF on failure.
+ * @subcmd: One of enum wilco_kbbl_subcommand.
+ * @reserved3: Should be 0.
+ * @mode: Bit flags for used mode, we want to use WILCO_KBBL_MODE_FLAG_PWM.
+ * @reserved5to8: Should be 0.
+ * @percent: Brightness in 0-100. Only meaningful in PWM mode.
+ * @reserved10to15: Should be 0.
+ */
+struct wilco_keyboard_leds_msg {
+ u8 command;
+ u8 status;
+ u8 subcmd;
+ u8 reserved3;
+ u8 mode;
+ u8 reserved5to8[4];
+ u8 percent;
+ u8 reserved10to15[6];
+} __packed;
+
+/* Send a request, get a response, and check that the response is good. */
+static int send_kbbl_msg(struct wilco_ec_device *ec,
+ struct wilco_keyboard_leds_msg *request,
+ struct wilco_keyboard_leds_msg *response)
+{
+ struct wilco_ec_message msg;
+ int ret;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.type = WILCO_EC_MSG_LEGACY;
+ msg.request_data = request;
+ msg.request_size = sizeof(*request);
+ msg.response_data = response;
+ msg.response_size = sizeof(*response);
+
+ ret = wilco_ec_mailbox(ec, &msg);
+ if (ret < 0) {
+ dev_err(ec->dev,
+ "Failed sending keyboard LEDs command: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int set_kbbl(struct wilco_ec_device *ec, enum led_brightness brightness)
+{
+ struct wilco_keyboard_leds_msg request;
+ struct wilco_keyboard_leds_msg response;
+ int ret;
+
+ memset(&request, 0, sizeof(request));
+ request.command = WILCO_EC_COMMAND_KBBL;
+ request.subcmd = WILCO_KBBL_SUBCMD_SET_STATE;
+ request.mode = WILCO_KBBL_MODE_FLAG_PWM;
+ request.percent = brightness;
+
+ ret = send_kbbl_msg(ec, &request, &response);
+ if (ret < 0)
+ return ret;
+
+ if (response.status) {
+ dev_err(ec->dev,
+ "EC reported failure sending keyboard LEDs command: %d\n",
+ response.status);
+ return -EIO;
+ }
+
+ return 0;
+}
+
+static int kbbl_exist(struct wilco_ec_device *ec, bool *exists)
+{
+ struct wilco_keyboard_leds_msg request;
+ struct wilco_keyboard_leds_msg response;
+ int ret;
+
+ memset(&request, 0, sizeof(request));
+ request.command = WILCO_EC_COMMAND_KBBL;
+ request.subcmd = WILCO_KBBL_SUBCMD_GET_FEATURES;
+
+ ret = send_kbbl_msg(ec, &request, &response);
+ if (ret < 0)
+ return ret;
+
+ *exists = response.status != 0xFF;
+
+ return 0;
+}
+
+/**
+ * kbbl_init() - Initialize the state of the keyboard backlight.
+ * @ec: EC device to talk to.
+ *
+ * Gets the current brightness, ensuring that the BIOS already initialized the
+ * backlight to PWM mode. If not in PWM mode, then the current brightness is
+ * meaningless, so set the brightness to WILCO_KBBL_DEFAULT_BRIGHTNESS.
+ *
+ * Return: Final brightness of the keyboard, or negative error code on failure.
+ */
+static int kbbl_init(struct wilco_ec_device *ec)
+{
+ struct wilco_keyboard_leds_msg request;
+ struct wilco_keyboard_leds_msg response;
+ int ret;
+
+ memset(&request, 0, sizeof(request));
+ request.command = WILCO_EC_COMMAND_KBBL;
+ request.subcmd = WILCO_KBBL_SUBCMD_GET_STATE;
+
+ ret = send_kbbl_msg(ec, &request, &response);
+ if (ret < 0)
+ return ret;
+
+ if (response.status) {
+ dev_err(ec->dev,
+ "EC reported failure sending keyboard LEDs command: %d\n",
+ response.status);
+ return -EIO;
+ }
+
+ if (response.mode & WILCO_KBBL_MODE_FLAG_PWM)
+ return response.percent;
+
+ ret = set_kbbl(ec, WILCO_KBBL_DEFAULT_BRIGHTNESS);
+ if (ret < 0)
+ return ret;
+
+ return WILCO_KBBL_DEFAULT_BRIGHTNESS;
+}
+
+static int wilco_keyboard_leds_set(struct led_classdev *cdev,
+ enum led_brightness brightness)
+{
+ struct wilco_keyboard_leds *wkl =
+ container_of(cdev, struct wilco_keyboard_leds, keyboard);
+ return set_kbbl(wkl->ec, brightness);
+}
+
+int wilco_keyboard_leds_init(struct wilco_ec_device *ec)
+{
+ struct wilco_keyboard_leds *wkl;
+ bool leds_exist;
+ int ret;
+
+ ret = kbbl_exist(ec, &leds_exist);
+ if (ret < 0) {
+ dev_err(ec->dev,
+ "Failed checking keyboard LEDs support: %d\n", ret);
+ return ret;
+ }
+ if (!leds_exist)
+ return 0;
+
+ wkl = devm_kzalloc(ec->dev, sizeof(*wkl), GFP_KERNEL);
+ if (!wkl)
+ return -ENOMEM;
+
+ wkl->ec = ec;
+ wkl->keyboard.name = "platform::kbd_backlight";
+ wkl->keyboard.max_brightness = 100;
+ wkl->keyboard.flags = LED_CORE_SUSPENDRESUME;
+ wkl->keyboard.brightness_set_blocking = wilco_keyboard_leds_set;
+ ret = kbbl_init(ec);
+ if (ret < 0)
+ return ret;
+ wkl->keyboard.brightness = ret;
+
+ return devm_led_classdev_register(ec->dev, &wkl->keyboard);
+}
diff --git a/drivers/platform/chrome/wilco_ec/mailbox.c b/drivers/platform/chrome/wilco_ec/mailbox.c
new file mode 100644
index 0000000000..0f98358ea8
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/mailbox.c
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Mailbox interface for Wilco Embedded Controller
+ *
+ * Copyright 2018 Google LLC
+ *
+ * The Wilco EC is similar to a typical ChromeOS embedded controller.
+ * It uses the same MEC based low-level communication and a similar
+ * protocol, but with some important differences. The EC firmware does
+ * not support the same mailbox commands so it is not registered as a
+ * cros_ec device type.
+ *
+ * Most messages follow a standard format, but there are some exceptions
+ * and an interface is provided to do direct/raw transactions that do not
+ * make assumptions about byte placement.
+ */
+
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/io.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/platform_device.h>
+
+#include "../cros_ec_lpc_mec.h"
+
+/* Version of mailbox interface */
+#define EC_MAILBOX_VERSION 0
+
+/* Command to start mailbox transaction */
+#define EC_MAILBOX_START_COMMAND 0xda
+
+/* Version of EC protocol */
+#define EC_MAILBOX_PROTO_VERSION 3
+
+/* Number of header bytes to be counted as data bytes */
+#define EC_MAILBOX_DATA_EXTRA 2
+
+/* Maximum timeout */
+#define EC_MAILBOX_TIMEOUT HZ
+
+/* EC response flags */
+#define EC_CMDR_DATA BIT(0) /* Data ready for host to read */
+#define EC_CMDR_PENDING BIT(1) /* Write pending to EC */
+#define EC_CMDR_BUSY BIT(2) /* EC is busy processing a command */
+#define EC_CMDR_CMD BIT(3) /* Last host write was a command */
+
+/**
+ * wilco_ec_response_timed_out() - Wait for EC response.
+ * @ec: EC device.
+ *
+ * Return: true if EC timed out, false if EC did not time out.
+ */
+static bool wilco_ec_response_timed_out(struct wilco_ec_device *ec)
+{
+ unsigned long timeout = jiffies + EC_MAILBOX_TIMEOUT;
+
+ do {
+ if (!(inb(ec->io_command->start) &
+ (EC_CMDR_PENDING | EC_CMDR_BUSY)))
+ return false;
+ usleep_range(100, 200);
+ } while (time_before(jiffies, timeout));
+
+ return true;
+}
+
+/**
+ * wilco_ec_checksum() - Compute 8-bit checksum over data range.
+ * @data: Data to checksum.
+ * @size: Number of bytes to checksum.
+ *
+ * Return: 8-bit checksum of provided data.
+ */
+static u8 wilco_ec_checksum(const void *data, size_t size)
+{
+ u8 *data_bytes = (u8 *)data;
+ u8 checksum = 0;
+ size_t i;
+
+ for (i = 0; i < size; i++)
+ checksum += data_bytes[i];
+
+ return checksum;
+}
+
+/**
+ * wilco_ec_prepare() - Prepare the request structure for the EC.
+ * @msg: EC message with request information.
+ * @rq: EC request structure to fill.
+ */
+static void wilco_ec_prepare(struct wilco_ec_message *msg,
+ struct wilco_ec_request *rq)
+{
+ memset(rq, 0, sizeof(*rq));
+ rq->struct_version = EC_MAILBOX_PROTO_VERSION;
+ rq->mailbox_id = msg->type;
+ rq->mailbox_version = EC_MAILBOX_VERSION;
+ rq->data_size = msg->request_size;
+
+ /* Checksum header and data */
+ rq->checksum = wilco_ec_checksum(rq, sizeof(*rq));
+ rq->checksum += wilco_ec_checksum(msg->request_data, msg->request_size);
+ rq->checksum = -rq->checksum;
+}
+
+/**
+ * wilco_ec_transfer() - Perform actual data transfer.
+ * @ec: EC device.
+ * @msg: EC message data for request and response.
+ * @rq: Filled in request structure
+ *
+ * Context: ec->mailbox_lock should be held while using this function.
+ * Return: number of bytes received or negative error code on failure.
+ */
+static int wilco_ec_transfer(struct wilco_ec_device *ec,
+ struct wilco_ec_message *msg,
+ struct wilco_ec_request *rq)
+{
+ struct wilco_ec_response *rs;
+ u8 checksum;
+ u8 flag;
+
+ /* Write request header, then data */
+ cros_ec_lpc_io_bytes_mec(MEC_IO_WRITE, 0, sizeof(*rq), (u8 *)rq);
+ cros_ec_lpc_io_bytes_mec(MEC_IO_WRITE, sizeof(*rq), msg->request_size,
+ msg->request_data);
+
+ /* Start the command */
+ outb(EC_MAILBOX_START_COMMAND, ec->io_command->start);
+
+ /* For some commands (eg shutdown) the EC will not respond, that's OK */
+ if (msg->flags & WILCO_EC_FLAG_NO_RESPONSE) {
+ dev_dbg(ec->dev, "EC does not respond to this command\n");
+ return 0;
+ }
+
+ /* Wait for it to complete */
+ if (wilco_ec_response_timed_out(ec)) {
+ dev_dbg(ec->dev, "response timed out\n");
+ return -ETIMEDOUT;
+ }
+
+ /* Check result */
+ flag = inb(ec->io_data->start);
+ if (flag) {
+ dev_dbg(ec->dev, "bad response: 0x%02x\n", flag);
+ return -EIO;
+ }
+
+ /* Read back response */
+ rs = ec->data_buffer;
+ checksum = cros_ec_lpc_io_bytes_mec(MEC_IO_READ, 0,
+ sizeof(*rs) + EC_MAILBOX_DATA_SIZE,
+ (u8 *)rs);
+ if (checksum) {
+ dev_dbg(ec->dev, "bad packet checksum 0x%02x\n", rs->checksum);
+ return -EBADMSG;
+ }
+
+ if (rs->result) {
+ dev_dbg(ec->dev, "EC reported failure: 0x%02x\n", rs->result);
+ return -EBADMSG;
+ }
+
+ if (rs->data_size != EC_MAILBOX_DATA_SIZE) {
+ dev_dbg(ec->dev, "unexpected packet size (%u != %u)\n",
+ rs->data_size, EC_MAILBOX_DATA_SIZE);
+ return -EMSGSIZE;
+ }
+
+ if (rs->data_size < msg->response_size) {
+ dev_dbg(ec->dev, "EC didn't return enough data (%u < %zu)\n",
+ rs->data_size, msg->response_size);
+ return -EMSGSIZE;
+ }
+
+ memcpy(msg->response_data, rs->data, msg->response_size);
+
+ return rs->data_size;
+}
+
+/**
+ * wilco_ec_mailbox() - Send EC request and receive EC response.
+ * @ec: EC device.
+ * @msg: EC message data for request and response.
+ *
+ * On entry msg->type, msg->request_size, and msg->request_data should all be
+ * filled in. If desired, msg->flags can be set.
+ *
+ * If a response is expected, msg->response_size should be set, and
+ * msg->response_data should point to a buffer with enough space. On exit
+ * msg->response_data will be filled.
+ *
+ * Return: number of bytes received or negative error code on failure.
+ */
+int wilco_ec_mailbox(struct wilco_ec_device *ec, struct wilco_ec_message *msg)
+{
+ struct wilco_ec_request *rq;
+ int ret;
+
+ dev_dbg(ec->dev, "type=%04x flags=%02x rslen=%zu rqlen=%zu\n",
+ msg->type, msg->flags, msg->response_size, msg->request_size);
+
+ mutex_lock(&ec->mailbox_lock);
+ /* Prepare request packet */
+ rq = ec->data_buffer;
+ wilco_ec_prepare(msg, rq);
+
+ ret = wilco_ec_transfer(ec, msg, rq);
+ mutex_unlock(&ec->mailbox_lock);
+
+ return ret;
+
+}
+EXPORT_SYMBOL_GPL(wilco_ec_mailbox);
diff --git a/drivers/platform/chrome/wilco_ec/properties.c b/drivers/platform/chrome/wilco_ec/properties.c
new file mode 100644
index 0000000000..c2bf4c95c5
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/properties.c
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2019 Google LLC
+ */
+
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <asm/unaligned.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 0000000000..893c59dde3
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/sysfs.c
@@ -0,0 +1,250 @@
+// 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/device.h>
+#include <linux/kernel.h>
+#include <linux/platform_data/wilco-ec.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+#include <linux/types.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_USB_CHARGE 0x39
+
+enum usb_charge_op {
+ USB_CHARGE_GET = 0,
+ USB_CHARGE_SET = 1,
+};
+
+struct usb_charge_request {
+ u8 cmd; /* Always CMD_USB_CHARGE */
+ u8 reserved;
+ u8 op; /* One of enum usb_charge_op */
+ u8 val; /* When setting, either 0 or 1 */
+} __packed;
+
+struct usb_charge_response {
+ u8 reserved;
+ u8 status; /* Set by EC to 0 on success, other value on failure */
+ u8 val; /* When getting, set by EC to either 0 or 1 */
+} __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 sysfs_emit(buf, "%.*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 int send_usb_charge(struct wilco_ec_device *ec,
+ struct usb_charge_request *rq,
+ struct usb_charge_response *rs)
+{
+ struct wilco_ec_message msg;
+ int ret;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.type = WILCO_EC_MSG_LEGACY;
+ msg.request_data = rq;
+ msg.request_size = sizeof(*rq);
+ msg.response_data = rs;
+ msg.response_size = sizeof(*rs);
+ ret = wilco_ec_mailbox(ec, &msg);
+ if (ret < 0)
+ return ret;
+ if (rs->status)
+ return -EIO;
+
+ return 0;
+}
+
+static ssize_t usb_charge_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct wilco_ec_device *ec = dev_get_drvdata(dev);
+ struct usb_charge_request rq;
+ struct usb_charge_response rs;
+ int ret;
+
+ memset(&rq, 0, sizeof(rq));
+ rq.cmd = CMD_USB_CHARGE;
+ rq.op = USB_CHARGE_GET;
+
+ ret = send_usb_charge(ec, &rq, &rs);
+ if (ret < 0)
+ return ret;
+
+ return sprintf(buf, "%d\n", rs.val);
+}
+
+static ssize_t usb_charge_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct wilco_ec_device *ec = dev_get_drvdata(dev);
+ struct usb_charge_request rq;
+ struct usb_charge_response rs;
+ 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_USB_CHARGE;
+ rq.op = USB_CHARGE_SET;
+ rq.val = val;
+
+ ret = send_usb_charge(ec, &rq, &rs);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static DEVICE_ATTR_RW(usb_charge);
+
+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_usb_charge.attr,
+ &dev_attr_version.attr,
+ NULL,
+};
+
+static const 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 0000000000..54708aa6c7
--- /dev/null
+++ b/drivers/platform/chrome/wilco_ec/telemetry.c
@@ -0,0 +1,471 @@
+// 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 allowlisted 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 = {
+ .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 WILCO_EC_TELEM_GET_BATT_PPID_INFO 0x8A
+
+#define TELEM_ARGS_SIZE_MAX 30
+
+/*
+ * 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;
+
+struct telem_args_get_batt_ppid_info {
+ u8 always1; /* Should always be 1 */
+} __packed;
+
+/**
+ * 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;
+ union {
+ u8 buf[TELEM_ARGS_SIZE_MAX];
+ struct telem_args_get_log get_log;
+ struct telem_args_get_version get_version;
+ struct telem_args_get_fan_info get_fan_info;
+ struct telem_args_get_diag_info get_diag_info;
+ struct telem_args_get_temp_info get_temp_info;
+ struct telem_args_get_temp_read get_temp_read;
+ struct telem_args_get_batt_ext_info get_batt_ext_info;
+ struct telem_args_get_batt_ppid_info get_batt_ppid_info;
+ } args;
+} __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 allowlisted 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(rq->args.get_log);
+ break;
+ case WILCO_EC_TELEM_GET_VERSION:
+ max_size += sizeof(rq->args.get_version);
+ break;
+ case WILCO_EC_TELEM_GET_FAN_INFO:
+ max_size += sizeof(rq->args.get_fan_info);
+ break;
+ case WILCO_EC_TELEM_GET_DIAG_INFO:
+ max_size += sizeof(rq->args.get_diag_info);
+ break;
+ case WILCO_EC_TELEM_GET_TEMP_INFO:
+ max_size += sizeof(rq->args.get_temp_info);
+ break;
+ case WILCO_EC_TELEM_GET_TEMP_READ:
+ max_size += sizeof(rq->args.get_temp_read);
+ break;
+ case WILCO_EC_TELEM_GET_BATT_EXT_INFO:
+ max_size += sizeof(rq->args.get_batt_ext_info);
+ break;
+ case WILCO_EC_TELEM_GET_BATT_PPID_INFO:
+ if (rq->args.get_batt_ppid_info.always1 != 1)
+ return -EINVAL;
+
+ max_size += sizeof(rq->args.get_batt_ppid_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;
+
+ stream_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;
+ memset(&sess_data->request, 0, sizeof(sess_data->request));
+ 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\n", 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);
+ ida_simple_remove(&telem_ida, MINOR(dev_data->dev.devt));
+ put_device(&dev_data->dev);
+
+ 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\n", 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\n", 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);