[PATCH v1 3/3] misc: Add meta cld driver

From: Delphine CC Chiu
Date: Tue Jan 17 2023 - 04:47:04 EST


Add support for meta control-logic-device driver. The CLD manages the
server system power squence and other state such as host-power-state,
uart-selection and presense-slots. The baseboard management controller
(BMC) can access the CLD through I2C.

The version 1 of CLD driver is supported. The registers number, name
and mode of CLD can be defined in dts file for version 1. The driver
exports the filesystem following the dts setting.

Signed-off-by: Delphine CC Chiu <Delphine_CC_Chiu@xxxxxxxxxx>
Tested-by: Bonnie Lo <Bonnie_Lo@xxxxxxxxxx>
---
MAINTAINERS | 6 +
drivers/misc/Kconfig | 9 +
drivers/misc/Makefile | 1 +
drivers/misc/control-logic-device.c | 443 ++++++++++++++++++++++++++++
4 files changed, 459 insertions(+)
create mode 100644 drivers/misc/control-logic-device.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 7483853880b6..46e250a2c334 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13388,6 +13388,12 @@ T: git git://linuxtv.org/media_tree.git
F: Documentation/devicetree/bindings/media/amlogic,gx-vdec.yaml
F: drivers/staging/media/meson/vdec/

+META CPLD DRIVER
+M: Delphine CC Chiu <Delphine_CC_Chiu@xxxxxxxxxx>
+L: linux-i2c@xxxxxxxxxxxxxxx
+S: Maintained
+F: Documentation/devicetree/bindings/misc/meta,control-logic-device.txt
+
METHODE UDPU SUPPORT
M: Vladimir Vid <vladimir.vid@xxxxxxxxxx>
S: Maintained
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 358ad56f6524..21d3bf820438 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -496,6 +496,15 @@ config VCPU_STALL_DETECTOR

If you do not intend to run this kernel as a guest, say N.

+config CONTROL_LOGIC_DEVICE
+ bool "Meta Control Logic Device Driver"
+ depends on I2C && REGMAP
+ help
+ Say yes here to add support for the Meta CLD device driver. The dirver
+ is used to monitor CLD register value and notify the application if
+ value is changed. Application also can access the CLD register value
+ through this driver.
+
source "drivers/misc/c2port/Kconfig"
source "drivers/misc/eeprom/Kconfig"
source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index ac9b3e757ba1..6fcdd575a143 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -62,3 +62,4 @@ obj-$(CONFIG_HI6421V600_IRQ) += hi6421v600-irq.o
obj-$(CONFIG_OPEN_DICE) += open-dice.o
obj-$(CONFIG_GP_PCI1XXXX) += mchp_pci1xxxx/
obj-$(CONFIG_VCPU_STALL_DETECTOR) += vcpu_stall_detector.o
+obj-$(CONFIG_CONTROL_LOGIC_DEVICE) += control-logic-device.o
diff --git a/drivers/misc/control-logic-device.c b/drivers/misc/control-logic-device.c
new file mode 100644
index 000000000000..07113dcb0836
--- /dev/null
+++ b/drivers/misc/control-logic-device.c
@@ -0,0 +1,443 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022 Meta Inc.
+ */
+
+ #include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/regmap.h>
+#include <linux/sysfs.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/stddef.h>
+#include <linux/kthread.h>
+
+#define CLD_LABEL_LEN 64 /* label = "name:mode:offset" */
+#define to_cld_reg(x) container_of(x, struct cld_data, bin)
+
+struct cld_register_info {
+ const char *name;
+ umode_t mode;
+ u8 reg;
+ u8 value;
+};
+
+struct cld_data {
+ struct list_head list;
+ struct bin_attribute bin;
+ struct kernfs_node *kn;
+ struct cld_register_info info;
+};
+
+struct cld_client {
+ struct bin_attribute new_register_bin;
+ struct bin_attribute delet_register_bin;
+ struct regmap *regmap;
+ struct device *dev;
+ struct cld_data *data;
+ struct task_struct *task;
+ size_t num_attributes;
+};
+
+struct meta_cld_of_ops {
+ int (*load_registers)(struct cld_client *cld);
+};
+
+static struct regmap_config cld_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .cache_type = REGCACHE_NONE,
+};
+
+static ssize_t cld_register_read(struct file *flip, struct kobject *kobj,
+ struct bin_attribute *attr, char *buf,
+ loff_t pos, size_t count)
+{
+ int ret;
+ unsigned int value;
+ struct device *dev = kobj_to_dev(kobj);
+ struct cld_client *cld = dev_get_drvdata(dev);
+ struct cld_data *data = to_cld_reg(attr);
+
+ ret = regmap_read(cld->regmap, data->info.reg, &value);
+ if (ret != 0)
+ return ret;
+
+ if ((data->info.mode & 0400) == 0400)
+ data->info.value = value;
+
+ snprintf(buf, sizeof(value), "%d\n", value);
+
+ return strlen(buf);
+}
+
+static ssize_t cld_register_write(struct file *flip, struct kobject *kobj,
+ struct bin_attribute *attr, char *buf,
+ loff_t pos, size_t count)
+{
+ int ret;
+ unsigned long val;
+ struct device *dev = kobj_to_dev(kobj);
+ struct cld_client *cld = dev_get_drvdata(dev);
+ struct cld_data *data = to_cld_reg(attr);
+
+ ret = kstrtoul(buf, 0, &val);
+ if (ret)
+ goto out;
+
+ if (val >= BIT(8)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ ret = regmap_write(cld->regmap, data->info.reg, val);
+ if (ret)
+ goto out;
+
+out:
+ return ret ? ret : strlen(buf);
+}
+
+static int cld_bin_register(struct cld_register_info info,
+ struct cld_client *cld)
+{
+ int ret;
+ struct cld_data *data, *pos;
+ struct bin_attribute *bin;
+ size_t length;
+
+ list_for_each_entry(pos, &cld->data->list, list) {
+ length = (strlen(info.name) > strlen(pos->info.name)) ?
+ strlen(info.name):strlen(pos->info.name);
+ if (!strncasecmp(info.name, pos->info.name, length))
+ return -EEXIST;
+ }
+
+ data = devm_kzalloc(cld->dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ bin = &data->bin;
+ data->info = info;
+
+ sysfs_bin_attr_init(bin);
+ bin->attr.name = kstrdup(info.name, GFP_KERNEL);
+ if (!bin->attr.name)
+ return -EINVAL;
+ bin->attr.mode = info.mode;
+ bin->read = cld_register_read;
+ bin->write = cld_register_write;
+ bin->size = 1;
+ ret = sysfs_create_bin_file(&cld->dev->kobj, bin);
+ if (ret) {
+ dev_err(cld->dev, "%s: failed to create file: %d\n",
+ info.name, ret);
+ goto out;
+ }
+
+ data->kn = kernfs_find_and_get(cld->dev->kobj.sd, bin->attr.name);
+ if (!data->kn) {
+ sysfs_remove_bin_file(&cld->dev->kobj, bin);
+ ret = -EFAULT;
+ goto out;
+ }
+
+ list_add_tail(&data->list, &cld->data->list);
+ ++cld->num_attributes;
+out:
+ return ret;
+}
+
+static int cld_bin_unregister(struct cld_register_info info,
+ struct cld_client *cld)
+{
+ size_t length;
+ struct cld_data *pos;
+
+ list_for_each_entry(pos, &cld->data->list, list) {
+ length = (strlen(info.name) > strlen(pos->info.name)) ?
+ strlen(info.name):strlen(pos->info.name);
+ if ((!strncasecmp(info.name, pos->info.name, length)) &&
+ (info.mode == pos->info.mode) &&
+ (info.reg == pos->info.reg)) {
+ kernfs_put(pos->kn);
+ sysfs_remove_bin_file(&cld->dev->kobj,
+ &pos->bin);
+ list_del(&pos->list);
+ devm_kfree(cld->dev, pos);
+ --cld->num_attributes;
+ return 0;
+ }
+ }
+
+ dev_err(cld->dev, "%s: cannot match cld data\n", info.name);
+ return -EINVAL;
+}
+
+static int cld_parse_label(char *label, struct cld_register_info *info)
+{
+ char name[CLD_LABEL_LEN] = { 0 };
+ char *mode_str, *reg_str;
+ int rc, i;
+ unsigned long reg;
+ umode_t mode;
+ size_t length;
+ struct {
+ char *mode;
+ int value;
+ } options[] = {
+ {"rw", 0600},
+ {"r", 0400},
+ {"w", 0200},
+ };
+
+ strncpy(name, label, CLD_LABEL_LEN);
+
+ mode_str = strchr(name, ':');
+ if (!mode_str)
+ return -EINVAL;
+ *mode_str++ = '\0';
+
+ reg_str = strchr(mode_str, ':');
+ if (!reg_str)
+ return -EINVAL;
+ *reg_str++ = '\0';
+
+ if ((*name == '\0') || (*mode_str == '\0') || (*reg_str == '\0'))
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(options); i++) {
+ length = (strlen(options[i].mode) > strlen(mode_str)) ?
+ strlen(options[i].mode):strlen(mode_str);
+ if (strncasecmp(mode_str, options[i].mode, length) == 0) {
+ rc = kstrtoul(reg_str, 0, &reg);
+ if (rc)
+ return -EINVAL;
+ mode = options[i].value;
+ break;
+ }
+ }
+ if ((i >= ARRAY_SIZE(options)) || (reg >= BIT(8)))
+ return -EINVAL;
+
+ info->name = kstrdup(name, GFP_KERNEL);
+ if (!info->name)
+ return -EINVAL;
+ info->reg = reg;
+ info->mode = mode;
+ return 0;
+}
+
+static int cld_load_registers(struct cld_client *cld, const char *property)
+{
+ const char *label;
+ int count, ret, i;
+ struct cld_register_info info;
+ struct device *dev = cld->dev;
+
+ count = of_property_count_strings(dev->of_node, property);
+ if (count <= 0)
+ return -EINVAL;
+
+ for (i = 0; i < count; i++) {
+ ret = of_property_read_string_index(dev->of_node, property, i,
+ &label);
+ if (ret) {
+ dev_err(dev, ": failed to get label for index %d\n",
+ i);
+ continue;
+ }
+
+ ret = cld_parse_label((char *)label, &info);
+ if (ret) {
+ dev_err(cld->dev, "%s: failed to parse label\n",
+ label);
+ continue;
+ }
+ cld_bin_register(info, cld);
+ }
+ return (count == cld->num_attributes) ? 0 : (-EINVAL);
+}
+
+static int meta_cld_of_v1_load_registers(struct cld_client *cld)
+{
+ int ret;
+
+ ret = cld_load_registers(cld, "registers-map");
+ if (ret)
+ dev_dbg(cld->dev, "failed to load registers\n");
+
+ return 0;
+}
+
+static ssize_t cld_sysfs_new_register(struct file *filp, struct kobject *kobj,
+ struct bin_attribute *attr,
+ char *buf, loff_t pos, size_t count)
+{
+ int ret;
+ struct device *dev = kobj_to_dev(kobj);
+ struct cld_client *cld = dev_get_drvdata(dev);
+ struct cld_register_info info;
+
+ ret = cld_parse_label(buf, &info);
+ if (ret) {
+ dev_err(cld->dev, "failed to parse label\n");
+ goto out;
+ }
+ ret = cld_bin_register(info, cld);
+
+out:
+ return (!ret) ? count : ret;
+}
+
+static ssize_t cld_sysfs_delete_register(struct file *filp,
+ struct kobject *kobj,
+ struct bin_attribute *attr,
+ char *buf, loff_t pos, size_t count)
+{
+ int ret;
+ struct device *dev = kobj_to_dev(kobj);
+ struct cld_client *cld = dev_get_drvdata(dev);
+ struct cld_register_info info;
+
+ ret = cld_parse_label(buf, &info);
+ if (ret) {
+ dev_err(cld->dev, "failed to parse label\n");
+ goto out;
+ }
+
+ ret = cld_bin_unregister(info, cld);
+
+out:
+ return (!ret) ? count : ret;
+}
+
+static int cld_monitor_thread(void *client)
+{
+ struct cld_client *cld = client;
+ unsigned int value;
+ int ret;
+ struct cld_data *pos;
+
+ while (!kthread_should_stop()) {
+ list_for_each_entry(pos, &cld->data->list, list) {
+ if ((pos->info.mode & 0400) == 0400) {
+ ret = regmap_read(cld->regmap, pos->info.reg,
+ &value);
+ if (ret)
+ continue;
+ if (pos->info.value != value) {
+ pos->info.value = value;
+ kernfs_notify(pos->kn);
+ }
+ }
+ }
+ msleep(50);
+ }
+ return 0;
+}
+
+static int cld_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ int ret = 0;
+ const struct meta_cld_of_ops *ops;
+ struct device *dev = &client->dev;
+ struct cld_client *cld;
+
+ cld = devm_kzalloc(dev, sizeof(*cld), GFP_KERNEL);
+ if (!cld) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ cld->dev = dev;
+ cld->num_attributes = 0;
+ cld->regmap = devm_regmap_init_i2c(client, &cld_regmap_config);
+ cld->data = devm_kzalloc(cld->dev, sizeof(struct cld_data),
+ GFP_KERNEL);
+ if (!cld->data) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ INIT_LIST_HEAD(&cld->data->list);
+
+ sysfs_bin_attr_init(&cld->new_register_bin);
+ cld->new_register_bin.attr.name = "new_register";
+ cld->new_register_bin.attr.mode = 0200;
+ cld->new_register_bin.write = cld_sysfs_new_register;
+ ret = sysfs_create_bin_file(&dev->kobj, &cld->new_register_bin);
+ if (ret)
+ goto out;
+
+ sysfs_bin_attr_init(&cld->delet_register_bin);
+ cld->delet_register_bin.attr.name = "delete_register";
+ cld->delet_register_bin.attr.mode = 0200;
+ cld->delet_register_bin.write = cld_sysfs_delete_register;
+ ret = sysfs_create_bin_file(&dev->kobj, &cld->delet_register_bin);
+ if (ret)
+ goto out;
+
+ ops = of_device_get_match_data(cld->dev);
+ if (!ops) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ ret = ops->load_registers(cld);
+
+ cld->task = kthread_run(cld_monitor_thread, (void *)cld,
+ "register_monitor");
+ if (IS_ERR(cld->task)) {
+ ret = PTR_ERR(cld->task);
+ cld->task = NULL;
+ goto out;
+ }
+
+ dev_set_drvdata(dev, cld);
+
+out:
+ return ret;
+}
+
+static int cld_remove(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct cld_client *cld = dev_get_drvdata(dev);
+
+ if (cld->task) {
+ kthread_stop(cld->task);
+ cld->task = NULL;
+ }
+
+ devm_kfree(dev, cld);
+ return 0;
+}
+
+static const struct meta_cld_of_ops of_v1_ops = {
+ .load_registers = meta_cld_of_v1_load_registers,
+};
+
+static const struct of_device_id cld_of_match[] = {
+ {
+ .compatible = "meta,control-logic-device-v1",
+ .data = &of_v1_ops,
+ },
+ {}
+};
+
+MODULE_DEVICE_TABLE(of, cld_of_match)
+
+static struct i2c_driver cld = {
+ .driver = { .name = "cld-driver",
+ .of_match_table = of_match_ptr(cld_of_match) },
+ .probe = cld_probe,
+ .remove = cld_remove,
+};
+
+module_i2c_driver(cld);
+
+MODULE_AUTHOR("Ren Chen <ren_chen@xxxxxxxxxx>");
+MODULE_DESCRIPTION("Meta control logic device driver");
+MODULE_LICENSE("GPL");
--
2.17.1