i2c: Congatec Board Controller i2c bus driver
authorThomas Richard <thomas.richard@bootlin.com>
Tue, 1 Oct 2024 11:53:29 +0000 (13:53 +0200)
committerLee Jones <lee@kernel.org>
Wed, 9 Oct 2024 15:32:08 +0000 (16:32 +0100)
Add i2c support for the Congatec Board Controller.

The Board Controller has two i2c busses, a General Purpose bus and a Power
Management bus.

Acked-by: Andi Shyti <andi.shyti@kernel.org>
Signed-off-by: Thomas Richard <thomas.richard@bootlin.com>
Link: https://lore.kernel.org/r/20241001-congatec-board-controller-v3-3-39ceceed5c47@bootlin.com
Signed-off-by: Lee Jones <lee@kernel.org>
drivers/i2c/busses/Kconfig
drivers/i2c/busses/Makefile
drivers/i2c/busses/i2c-cgbc.c [new file with mode: 0644]

index 6b3ba7e..4977abc 100644 (file)
@@ -535,6 +535,16 @@ config I2C_CBUS_GPIO
          This driver can also be built as a module.  If so, the module
          will be called i2c-cbus-gpio.
 
+config I2C_CGBC
+       tristate "Congatec I2C Controller"
+       depends on MFD_CGBC
+       help
+         This driver supports the 2 I2C interfaces on the Congatec Board
+         Controller.
+
+         This driver can also be built as a module. If so, the module will
+         be called i2c-cgbc.ko.
+
 config I2C_CPM
        tristate "Freescale CPM1 or CPM2 (MPC8xx/826x)"
        depends on CPM1 || CPM2
index ecc07c5..a6bcbf2 100644 (file)
@@ -50,6 +50,7 @@ obj-$(CONFIG_I2C_BCM2835)     += i2c-bcm2835.o
 obj-$(CONFIG_I2C_BCM_IPROC)    += i2c-bcm-iproc.o
 obj-$(CONFIG_I2C_CADENCE)      += i2c-cadence.o
 obj-$(CONFIG_I2C_CBUS_GPIO)    += i2c-cbus-gpio.o
+obj-$(CONFIG_I2C_CGBC)         += i2c-cgbc.o
 obj-$(CONFIG_I2C_CPM)          += i2c-cpm.o
 obj-$(CONFIG_I2C_DAVINCI)      += i2c-davinci.o
 obj-$(CONFIG_I2C_DESIGNWARE_CORE)                      += i2c-designware-core.o
diff --git a/drivers/i2c/busses/i2c-cgbc.c b/drivers/i2c/busses/i2c-cgbc.c
new file mode 100644 (file)
index 0000000..eba0b20
--- /dev/null
@@ -0,0 +1,406 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Congatec Board Controller I2C busses driver
+ *
+ * Copyright (C) 2024 Bootlin
+ * Author: Thomas Richard <thomas.richard@bootlin.com>
+ */
+
+#include <linux/i2c.h>
+#include <linux/iopoll.h>
+#include <linux/mfd/cgbc.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+#define CGBC_I2C_PRIMARY_BUS_ID        0
+#define CGBC_I2C_PM_BUS_ID     4
+
+#define CGBC_I2C_CMD_START     0x40
+#define CGBC_I2C_CMD_STAT      0x48
+#define CGBC_I2C_CMD_DATA      0x50
+#define CGBC_I2C_CMD_SPEED     0x58
+
+#define CGBC_I2C_STAT_IDL      0x00
+#define CGBC_I2C_STAT_DAT      0x01
+#define CGBC_I2C_STAT_BUSY     0x02
+
+#define CGBC_I2C_START 0x80
+#define CGBC_I2C_STOP  0x40
+
+#define CGBC_I2C_LAST_ACK  0x80    /* send ACK on last read byte */
+
+/*
+ * Reference code defines 1kHz as min freq and 6.1MHz as max freq.
+ * But in practice, the board controller limits the frequency to 1MHz, and the
+ * 1kHz is not functional (minimal working freq is 50kHz).
+ * So use these values as limits.
+ */
+#define CGBC_I2C_FREQ_MIN_HZ   50000   /* 50 kHz */
+#define CGBC_I2C_FREQ_MAX_HZ   1000000 /* 1 MHz */
+
+#define CGBC_I2C_FREQ_UNIT_1KHZ                0x40
+#define CGBC_I2C_FREQ_UNIT_10KHZ       0x80
+#define CGBC_I2C_FREQ_UNIT_100KHZ      0xC0
+
+#define CGBC_I2C_FREQ_UNIT_MASK                0xC0
+#define CGBC_I2C_FREQ_VALUE_MASK       0x3F
+
+#define CGBC_I2C_READ_MAX_LEN  31
+#define CGBC_I2C_WRITE_MAX_LEN 32
+
+#define CGBC_I2C_CMD_HEADER_SIZE       4
+#define CGBC_I2C_CMD_SIZE              (CGBC_I2C_CMD_HEADER_SIZE + CGBC_I2C_WRITE_MAX_LEN)
+
+enum cgbc_i2c_state {
+       CGBC_I2C_STATE_DONE = 0,
+       CGBC_I2C_STATE_INIT,
+       CGBC_I2C_STATE_START,
+       CGBC_I2C_STATE_READ,
+       CGBC_I2C_STATE_WRITE,
+       CGBC_I2C_STATE_ERROR,
+};
+
+struct i2c_algo_cgbc_data {
+       u8              bus_id;
+       unsigned long   read_maxtime_us;
+};
+
+struct cgbc_i2c_data {
+       struct device           *dev;
+       struct cgbc_device_data *cgbc;
+       struct i2c_adapter      adap;
+       struct i2c_msg          *msg;
+       int                     nmsgs;
+       int                     pos;
+       enum cgbc_i2c_state     state;
+};
+
+struct cgbc_i2c_transfer {
+       u8 bus_id;
+       bool start;
+       bool stop;
+       bool last_ack;
+       u8 read;
+       u8 write;
+       u8 addr;
+       u8 data[CGBC_I2C_WRITE_MAX_LEN];
+};
+
+static u8 cgbc_i2c_freq_to_reg(unsigned int bus_frequency)
+{
+       u8 reg;
+
+       if (bus_frequency <= 10000)
+               reg = CGBC_I2C_FREQ_UNIT_1KHZ | (bus_frequency / 1000);
+       else if (bus_frequency <= 100000)
+               reg = CGBC_I2C_FREQ_UNIT_10KHZ | (bus_frequency / 10000);
+       else
+               reg = CGBC_I2C_FREQ_UNIT_100KHZ | (bus_frequency / 100000);
+
+       return reg;
+}
+
+static unsigned int cgbc_i2c_reg_to_freq(u8 reg)
+{
+       unsigned int freq = reg & CGBC_I2C_FREQ_VALUE_MASK;
+       u8 unit = reg & CGBC_I2C_FREQ_UNIT_MASK;
+
+       if (unit == CGBC_I2C_FREQ_UNIT_100KHZ)
+               return freq * 100000;
+       else if (unit == CGBC_I2C_FREQ_UNIT_10KHZ)
+               return freq * 10000;
+       else
+               return freq * 1000;
+}
+
+static int cgbc_i2c_get_status(struct i2c_adapter *adap)
+{
+       struct i2c_algo_cgbc_data *algo_data = adap->algo_data;
+       struct cgbc_i2c_data *i2c = i2c_get_adapdata(adap);
+       struct cgbc_device_data *cgbc = i2c->cgbc;
+       u8 cmd = CGBC_I2C_CMD_STAT | algo_data->bus_id;
+       u8 status;
+       int ret;
+
+       ret = cgbc_command(cgbc, &cmd, sizeof(cmd), NULL, 0, &status);
+       if (ret)
+               return ret;
+
+       return status;
+}
+
+static int cgbc_i2c_set_frequency(struct i2c_adapter *adap,
+                                 unsigned int bus_frequency)
+{
+       struct i2c_algo_cgbc_data *algo_data = adap->algo_data;
+       struct cgbc_i2c_data *i2c = i2c_get_adapdata(adap);
+       struct cgbc_device_data *cgbc = i2c->cgbc;
+       u8 cmd[2], data;
+       int ret;
+
+       if (bus_frequency > CGBC_I2C_FREQ_MAX_HZ ||
+           bus_frequency < CGBC_I2C_FREQ_MIN_HZ) {
+               dev_info(i2c->dev, "invalid frequency %u, using default\n", bus_frequency);
+               bus_frequency = I2C_MAX_STANDARD_MODE_FREQ;
+       }
+
+       cmd[0] = CGBC_I2C_CMD_SPEED | algo_data->bus_id;
+       cmd[1] = cgbc_i2c_freq_to_reg(bus_frequency);
+
+       ret = cgbc_command(cgbc, &cmd, sizeof(cmd), &data, 1, NULL);
+       if (ret)
+               return dev_err_probe(i2c->dev, ret,
+                                    "Failed to initialize I2C bus %s",
+                                    adap->name);
+
+       cmd[1] = 0x00;
+
+       ret = cgbc_command(cgbc, &cmd, sizeof(cmd), &data, 1, NULL);
+       if (ret)
+               return dev_err_probe(i2c->dev, ret,
+                                    "Failed to get I2C bus frequency");
+
+       bus_frequency = cgbc_i2c_reg_to_freq(data);
+
+       dev_dbg(i2c->dev, "%s is running at %d Hz\n", adap->name, bus_frequency);
+
+       /*
+        * The read_maxtime_us variable represents the maximum time to wait
+        * for data during a read operation. The maximum amount of data that
+        * can be read by a command is CGBC_I2C_READ_MAX_LEN.
+        * Therefore, calculate the max time to properly size the timeout.
+        */
+       algo_data->read_maxtime_us = (BITS_PER_BYTE + 1) * CGBC_I2C_READ_MAX_LEN
+               * USEC_PER_SEC / bus_frequency;
+
+       return 0;
+}
+
+static unsigned int cgbc_i2c_xfer_to_cmd(struct cgbc_i2c_transfer xfer, u8 *cmd)
+{
+       int i = 0;
+
+       cmd[i++] = CGBC_I2C_CMD_START | xfer.bus_id;
+
+       cmd[i] = (xfer.start) ? CGBC_I2C_START : 0x00;
+       if (xfer.stop)
+               cmd[i] |= CGBC_I2C_STOP;
+       cmd[i++] |= (xfer.start) ? xfer.write + 1 : xfer.write;
+
+       cmd[i++] = (xfer.last_ack) ? (xfer.read | CGBC_I2C_LAST_ACK) : xfer.read;
+
+       if (xfer.start)
+               cmd[i++] = xfer.addr;
+
+       if (xfer.write > 0)
+               memcpy(&cmd[i], &xfer.data, xfer.write);
+
+       return i + xfer.write;
+}
+
+static int cgbc_i2c_xfer_msg(struct i2c_adapter *adap)
+{
+       struct i2c_algo_cgbc_data *algo_data = adap->algo_data;
+       struct cgbc_i2c_data *i2c = i2c_get_adapdata(adap);
+       struct cgbc_device_data *cgbc = i2c->cgbc;
+       struct i2c_msg *msg = i2c->msg;
+       u8 cmd[CGBC_I2C_CMD_SIZE];
+       int ret, max_len, len, i;
+       unsigned int cmd_len;
+       u8 cmd_data;
+
+       struct cgbc_i2c_transfer xfer = {
+               .bus_id = algo_data->bus_id,
+               .addr = i2c_8bit_addr_from_msg(msg),
+       };
+
+       if (i2c->state == CGBC_I2C_STATE_DONE)
+               return 0;
+
+       ret = cgbc_i2c_get_status(adap);
+
+       if (ret == CGBC_I2C_STAT_BUSY)
+               return -EBUSY;
+       else if (ret < 0)
+               goto err;
+
+       if (i2c->state == CGBC_I2C_STATE_INIT ||
+           (i2c->state == CGBC_I2C_STATE_WRITE && msg->flags & I2C_M_RD))
+               xfer.start = true;
+
+       i2c->state = (msg->flags & I2C_M_RD) ? CGBC_I2C_STATE_READ : CGBC_I2C_STATE_WRITE;
+
+       max_len = (i2c->state == CGBC_I2C_STATE_READ) ?
+               CGBC_I2C_READ_MAX_LEN : CGBC_I2C_WRITE_MAX_LEN;
+
+       if (msg->len - i2c->pos > max_len) {
+               len = max_len;
+       } else {
+               len = msg->len - i2c->pos;
+
+               if (i2c->nmsgs == 1)
+                       xfer.stop = true;
+       }
+
+       if (i2c->state == CGBC_I2C_STATE_WRITE) {
+               xfer.write = len;
+               xfer.read = 0;
+
+               for (i = 0; i < len; i++)
+                       xfer.data[i] = msg->buf[i2c->pos + i];
+
+               cmd_len = cgbc_i2c_xfer_to_cmd(xfer, &cmd[0]);
+
+               ret = cgbc_command(cgbc, &cmd, cmd_len, NULL, 0, NULL);
+               if (ret)
+                       goto err;
+       } else if (i2c->state == CGBC_I2C_STATE_READ) {
+               xfer.write = 0;
+               xfer.read = len;
+
+               if (i2c->nmsgs > 1 || msg->len - i2c->pos > max_len)
+                       xfer.read |= CGBC_I2C_LAST_ACK;
+
+               cmd_len = cgbc_i2c_xfer_to_cmd(xfer, &cmd[0]);
+               ret = cgbc_command(cgbc, &cmd, cmd_len, NULL, 0, NULL);
+               if (ret)
+                       goto err;
+
+               ret = read_poll_timeout(cgbc_i2c_get_status, ret,
+                                       ret != CGBC_I2C_STAT_BUSY, 0,
+                                       2 * algo_data->read_maxtime_us, false, adap);
+               if (ret < 0)
+                       goto err;
+
+               cmd_data = CGBC_I2C_CMD_DATA | algo_data->bus_id;
+               ret = cgbc_command(cgbc, &cmd_data, sizeof(cmd_data),
+                                  msg->buf + i2c->pos, len, NULL);
+               if (ret)
+                       goto err;
+       }
+
+       if (len == (msg->len - i2c->pos)) {
+               i2c->msg++;
+               i2c->nmsgs--;
+               i2c->pos = 0;
+       } else {
+               i2c->pos += len;
+       }
+
+       if (i2c->nmsgs == 0)
+               i2c->state = CGBC_I2C_STATE_DONE;
+
+       return 0;
+
+err:
+       i2c->state = CGBC_I2C_STATE_ERROR;
+       return ret;
+}
+
+static int cgbc_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
+                        int num)
+{
+       struct cgbc_i2c_data *i2c = i2c_get_adapdata(adap);
+       unsigned long timeout = jiffies + HZ;
+       int ret;
+
+       i2c->state = CGBC_I2C_STATE_INIT;
+       i2c->msg = msgs;
+       i2c->nmsgs = num;
+       i2c->pos = 0;
+
+       while (time_before(jiffies, timeout)) {
+               ret = cgbc_i2c_xfer_msg(adap);
+               if (i2c->state == CGBC_I2C_STATE_DONE)
+                       return num;
+
+               if (i2c->state == CGBC_I2C_STATE_ERROR)
+                       return ret;
+
+               if (ret == 0)
+                       timeout = jiffies + HZ;
+       }
+
+       i2c->state = CGBC_I2C_STATE_ERROR;
+       return -ETIMEDOUT;
+}
+
+static u32 cgbc_i2c_func(struct i2c_adapter *adap)
+{
+       return I2C_FUNC_I2C | (I2C_FUNC_SMBUS_EMUL & ~(I2C_FUNC_SMBUS_QUICK));
+}
+
+static const struct i2c_algorithm cgbc_i2c_algorithm = {
+       .master_xfer    = cgbc_i2c_xfer,
+       .functionality  = cgbc_i2c_func,
+};
+
+static struct i2c_algo_cgbc_data cgbc_i2c_algo_data[] = {
+       { .bus_id = CGBC_I2C_PRIMARY_BUS_ID },
+       { .bus_id = CGBC_I2C_PM_BUS_ID },
+};
+
+static const struct i2c_adapter cgbc_i2c_adapter[] = {
+       {
+               .owner          = THIS_MODULE,
+               .name           = "Congatec General Purpose I2C adapter",
+               .class          = I2C_CLASS_DEPRECATED,
+               .algo           = &cgbc_i2c_algorithm,
+               .algo_data      = &cgbc_i2c_algo_data[0],
+               .nr             = -1,
+       },
+       {
+               .owner          = THIS_MODULE,
+               .name           = "Congatec Power Management I2C adapter",
+               .class          = I2C_CLASS_DEPRECATED,
+               .algo           = &cgbc_i2c_algorithm,
+               .algo_data      = &cgbc_i2c_algo_data[1],
+               .nr             = -1,
+       },
+};
+
+static int cgbc_i2c_probe(struct platform_device *pdev)
+{
+       struct cgbc_device_data *cgbc = dev_get_drvdata(pdev->dev.parent);
+       struct cgbc_i2c_data *i2c;
+       int ret;
+
+       i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);
+       if (!i2c)
+               return -ENOMEM;
+
+       i2c->cgbc = cgbc;
+       i2c->dev = &pdev->dev;
+       i2c->adap = cgbc_i2c_adapter[pdev->id];
+       i2c->adap.dev.parent = i2c->dev;
+       i2c_set_adapdata(&i2c->adap, i2c);
+       platform_set_drvdata(pdev, i2c);
+
+       ret = cgbc_i2c_set_frequency(&i2c->adap, I2C_MAX_STANDARD_MODE_FREQ);
+       if (ret)
+               return ret;
+
+       return i2c_add_numbered_adapter(&i2c->adap);
+}
+
+static void cgbc_i2c_remove(struct platform_device *pdev)
+{
+       struct cgbc_i2c_data *i2c = platform_get_drvdata(pdev);
+
+       i2c_del_adapter(&i2c->adap);
+}
+
+static struct platform_driver cgbc_i2c_driver = {
+       .driver = {
+               .name = "cgbc-i2c",
+       },
+       .probe          = cgbc_i2c_probe,
+       .remove_new     = cgbc_i2c_remove,
+};
+
+module_platform_driver(cgbc_i2c_driver);
+
+MODULE_DESCRIPTION("Congatec Board Controller I2C Driver");
+MODULE_AUTHOR("Thomas Richard <thomas.richard@bootlin.com>");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:cgbc_i2c");