| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright 2024 Google LLC |
| * |
| * This driver provides the ability to control GPIOs on the Chrome OS EC. |
| * There isn't any direction control, and setting values on GPIOs is only |
| * possible when the system is unlocked. |
| */ |
| |
| #include <linux/bitops.h> |
| #include <linux/device.h> |
| #include <linux/errno.h> |
| #include <linux/gpio/driver.h> |
| #include <linux/kernel.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/module.h> |
| #include <linux/platform_data/cros_ec_commands.h> |
| #include <linux/platform_data/cros_ec_proto.h> |
| #include <linux/platform_device.h> |
| #include <linux/property.h> |
| #include <linux/slab.h> |
| |
| /* Prefix all names to avoid collisions with EC <-> AP nets */ |
| static const char cros_ec_gpio_prefix[] = "EC:"; |
| |
| /* Setting gpios is only supported when the system is unlocked */ |
| static void cros_ec_gpio_set(struct gpio_chip *gc, unsigned int gpio, int val) |
| { |
| const char *name = gc->names[gpio] + strlen(cros_ec_gpio_prefix); |
| struct cros_ec_device *cros_ec = gpiochip_get_data(gc); |
| struct ec_params_gpio_set params = { |
| .val = val, |
| }; |
| int ret; |
| ssize_t copied; |
| |
| copied = strscpy(params.name, name, sizeof(params.name)); |
| if (copied < 0) |
| return; |
| |
| ret = cros_ec_cmd(cros_ec, 0, EC_CMD_GPIO_SET, ¶ms, |
| sizeof(params), NULL, 0); |
| if (ret < 0) |
| dev_err(gc->parent, "error setting gpio%d (%s) on EC: %d\n", gpio, name, ret); |
| } |
| |
| static int cros_ec_gpio_get(struct gpio_chip *gc, unsigned int gpio) |
| { |
| const char *name = gc->names[gpio] + strlen(cros_ec_gpio_prefix); |
| struct cros_ec_device *cros_ec = gpiochip_get_data(gc); |
| struct ec_params_gpio_get params; |
| struct ec_response_gpio_get response; |
| int ret; |
| ssize_t copied; |
| |
| copied = strscpy(params.name, name, sizeof(params.name)); |
| if (copied < 0) |
| return -EINVAL; |
| |
| ret = cros_ec_cmd(cros_ec, 0, EC_CMD_GPIO_GET, ¶ms, |
| sizeof(params), &response, sizeof(response)); |
| if (ret < 0) { |
| dev_err(gc->parent, "error getting gpio%d (%s) on EC: %d\n", gpio, name, ret); |
| return ret; |
| } |
| |
| return response.val; |
| } |
| |
| #define CROS_EC_GPIO_INPUT BIT(8) |
| #define CROS_EC_GPIO_OUTPUT BIT(9) |
| |
| static int cros_ec_gpio_get_direction(struct gpio_chip *gc, unsigned int gpio) |
| { |
| const char *name = gc->names[gpio] + strlen(cros_ec_gpio_prefix); |
| struct cros_ec_device *cros_ec = gpiochip_get_data(gc); |
| struct ec_params_gpio_get_v1 params = { |
| .subcmd = EC_GPIO_GET_INFO, |
| .get_info.index = gpio, |
| }; |
| struct ec_response_gpio_get_v1 response; |
| int ret; |
| |
| ret = cros_ec_cmd(cros_ec, 1, EC_CMD_GPIO_GET, ¶ms, |
| sizeof(params), &response, sizeof(response)); |
| if (ret < 0) { |
| dev_err(gc->parent, "error getting direction of gpio%d (%s) on EC: %d\n", gpio, name, ret); |
| return ret; |
| } |
| |
| if (response.get_info.flags & CROS_EC_GPIO_INPUT) |
| return GPIO_LINE_DIRECTION_IN; |
| |
| if (response.get_info.flags & CROS_EC_GPIO_OUTPUT) |
| return GPIO_LINE_DIRECTION_OUT; |
| |
| return -EINVAL; |
| } |
| |
| /* Query EC for all gpio line names */ |
| static int cros_ec_gpio_init_names(struct cros_ec_device *cros_ec, struct gpio_chip *gc) |
| { |
| struct ec_params_gpio_get_v1 params = { |
| .subcmd = EC_GPIO_GET_INFO, |
| }; |
| struct ec_response_gpio_get_v1 response; |
| int ret, i; |
| /* EC may not NUL terminate */ |
| size_t name_len = strlen(cros_ec_gpio_prefix) + sizeof(response.get_info.name) + 1; |
| ssize_t copied; |
| const char **names; |
| char *str; |
| |
| names = devm_kcalloc(gc->parent, gc->ngpio, sizeof(*names), GFP_KERNEL); |
| if (!names) |
| return -ENOMEM; |
| gc->names = names; |
| |
| str = devm_kcalloc(gc->parent, gc->ngpio, name_len, GFP_KERNEL); |
| if (!str) |
| return -ENOMEM; |
| |
| /* Get gpio line names one at a time */ |
| for (i = 0; i < gc->ngpio; i++) { |
| params.get_info.index = i; |
| ret = cros_ec_cmd(cros_ec, 1, EC_CMD_GPIO_GET, ¶ms, |
| sizeof(params), &response, sizeof(response)); |
| if (ret < 0) { |
| dev_err_probe(gc->parent, ret, "error getting gpio%d info\n", i); |
| return ret; |
| } |
| |
| names[i] = str; |
| copied = scnprintf(str, name_len, "%s%s", cros_ec_gpio_prefix, |
| response.get_info.name); |
| if (copied < 0) |
| return copied; |
| |
| str += copied + 1; |
| } |
| |
| return 0; |
| } |
| |
| /* Query EC for number of gpios */ |
| static int cros_ec_gpio_ngpios(struct cros_ec_device *cros_ec) |
| { |
| struct ec_params_gpio_get_v1 params = { |
| .subcmd = EC_GPIO_GET_COUNT, |
| }; |
| struct ec_response_gpio_get_v1 response; |
| int ret; |
| |
| ret = cros_ec_cmd(cros_ec, 1, EC_CMD_GPIO_GET, ¶ms, |
| sizeof(params), &response, sizeof(response)); |
| if (ret < 0) |
| return ret; |
| |
| return response.get_count.val; |
| } |
| |
| static int cros_ec_gpio_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct device *parent = dev->parent; |
| struct cros_ec_dev *ec_dev = dev_get_drvdata(parent); |
| struct cros_ec_device *cros_ec = ec_dev->ec_dev; |
| struct gpio_chip *gc; |
| int ngpios; |
| int ret; |
| |
| /* Use the fwnode from the protocol device, e.g. cros-ec-spi */ |
| device_set_node(dev, dev_fwnode(cros_ec->dev)); |
| |
| ngpios = cros_ec_gpio_ngpios(cros_ec); |
| if (ngpios < 0) { |
| dev_err_probe(dev, ngpios, "error getting gpio count\n"); |
| return ngpios; |
| } |
| |
| gc = devm_kzalloc(dev, sizeof(*gc), GFP_KERNEL); |
| if (!gc) |
| return -ENOMEM; |
| |
| gc->ngpio = ngpios; |
| gc->parent = dev; |
| ret = cros_ec_gpio_init_names(cros_ec, gc); |
| if (ret) |
| return ret; |
| |
| gc->can_sleep = true; |
| gc->label = dev_name(dev); |
| gc->base = -1; |
| gc->set = cros_ec_gpio_set; |
| gc->get = cros_ec_gpio_get; |
| gc->get_direction = cros_ec_gpio_get_direction; |
| |
| return devm_gpiochip_add_data(dev, gc, cros_ec); |
| } |
| |
| static const struct platform_device_id cros_ec_gpio_id[] = { |
| { "cros-ec-gpio", 0 }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(platform, cros_ec_gpio_id); |
| |
| static struct platform_driver cros_ec_gpio_driver = { |
| .probe = cros_ec_gpio_probe, |
| .driver = { |
| .name = "cros-ec-gpio", |
| }, |
| .id_table = cros_ec_gpio_id, |
| }; |
| module_platform_driver(cros_ec_gpio_driver); |
| |
| MODULE_DESCRIPTION("ChromeOS EC GPIO Driver"); |
| MODULE_LICENSE("GPL"); |