The existing bcm283x I2C driver relies on a hardcoded clock rate
which breaks I2C communication on the BCM2711 (Raspberry Pi 4),
where the VPU core clock is dynamically scaled by the firmware.

This patch completely refactors the driver (based on Linux kernel's
i2c-bcm2835.c) to bring it up to standard:
- Queries the actual core clock via BCM2835 Mailbox property tags.
- Calculates the CDIV divider dynamically based on the firmware clock.
- Adds the "brcm,bcm2711-i2c" compatible string.
- Improves FIFO drain/fill and hardware timeout polling.

Signed-off-by: Giau Nguyen <[email protected]>
---
 MAINTAINERS               |   1 +
 drivers/i2c/Kconfig       |  10 +
 drivers/i2c/Makefile      |   1 +
 drivers/i2c/i2c-bcm2835.c | 411 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 423 insertions(+)
 create mode 100644 drivers/i2c/i2c-bcm2835.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 8c6c0c2a4bc..3260024a736 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -215,6 +215,7 @@ F:  arch/arm/dts/bcm283*
 F:     arch/arm/mach-bcm283x/
 F:     board/raspberrypi/
 F:     drivers/gpio/bcm2835_gpio.c
+F:     drivers/i2c/i2c-bcm2835.c
 F:     drivers/mmc/bcm2835_sdhci.c
 F:     drivers/mmc/bcm2835_sdhost.c
 F:     drivers/serial/serial_bcm283x_mu.c
diff --git a/drivers/i2c/Kconfig b/drivers/i2c/Kconfig
index 52067fa7c1f..19bd60ecdac 100644
--- a/drivers/i2c/Kconfig
+++ b/drivers/i2c/Kconfig
@@ -782,6 +782,16 @@ config SYS_I2C_XILINX_XIIC
        help
          Support for Xilinx AXI I2C controller.
 
+config SYS_I2C_BCM2835
+       bool "BCM2835 I2C controller"
+       depends on DM_I2C
+       help
+         Enable support for the Broadcom BCM2835/BCM2711 I2C controller.
+         This driver provides standard I2C support for Raspberry Pi
+         devices, including the newer Raspberry Pi 4. It dynamically
+         calculates the clock divider based on the VPU core clock
+         queried via the firmware mailbox.
+
 config SYS_I2C_IHS
         bool "gdsys IHS I2C driver"
         depends on DM_I2C
diff --git a/drivers/i2c/Makefile b/drivers/i2c/Makefile
index bebd728e7da..e2c252cf16c 100644
--- a/drivers/i2c/Makefile
+++ b/drivers/i2c/Makefile
@@ -55,6 +55,7 @@ obj-$(CONFIG_SYS_I2C_UNIPHIER) += i2c-uniphier.o
 obj-$(CONFIG_SYS_I2C_UNIPHIER_F) += i2c-uniphier-f.o
 obj-$(CONFIG_SYS_I2C_VERSATILE) += i2c-versatile.o
 obj-$(CONFIG_SYS_I2C_XILINX_XIIC) += xilinx_xiic.o
+obj-$(CONFIG_SYS_I2C_BCM2835) += i2c-bcm2835.o
 obj-$(CONFIG_TEGRA186_BPMP_I2C) += tegra186_bpmp_i2c.o
 
 obj-$(CONFIG_$(XPL_)I2C_MUX) += muxes/
diff --git a/drivers/i2c/i2c-bcm2835.c b/drivers/i2c/i2c-bcm2835.c
new file mode 100644
index 00000000000..93d19cb8e5a
--- /dev/null
+++ b/drivers/i2c/i2c-bcm2835.c
@@ -0,0 +1,411 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * BCM2835 I2C controller driver
+ * (C) Copyright 2026
+ *
+ * Author: Giau Nguyen <[email protected]>
+ * Based on Linux driver: drivers/i2c/busses/i2c-bcm2835.c
+ * Assisted by AI
+ */
+
+#include <log.h>
+#include <asm/io.h>
+#include <asm/arch/mbox.h>
+#include <dm.h>
+#include <dm/device_compat.h>
+#include <i2c.h>
+#include <memalign.h>
+#include <time.h>
+#include <linux/bitops.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/types.h>
+
+#define BCM2835_I2C_C          0x0
+#define BCM2835_I2C_S          0x4
+#define BCM2835_I2C_DLEN       0x8
+#define BCM2835_I2C_A          0xc
+#define BCM2835_I2C_FIFO       0x10
+#define BCM2835_I2C_DIV                0x14
+#define BCM2835_I2C_DEL                0x18
+/*
+ * 16-bit field for the number of SCL cycles to wait after rising SCL
+ * before deciding the slave is not responding. 0 disables the
+ * timeout detection.
+ */
+#define BCM2835_I2C_CLKT       0x1c
+
+#define BCM2835_I2C_C_READ     BIT(0)
+#define BCM2835_I2C_C_CLEAR    BIT(4) /* bits 4 and 5 both clear */
+#define BCM2835_I2C_C_ST       BIT(7)
+#define BCM2835_I2C_C_INTD     BIT(8)
+#define BCM2835_I2C_C_INTT     BIT(9)
+#define BCM2835_I2C_C_INTR     BIT(10)
+#define BCM2835_I2C_C_I2CEN    BIT(15)
+
+#define BCM2835_I2C_S_TA       BIT(0)
+#define BCM2835_I2C_S_DONE     BIT(1)
+#define BCM2835_I2C_S_TXW      BIT(2)
+#define BCM2835_I2C_S_RXR      BIT(3)
+#define BCM2835_I2C_S_TXD      BIT(4)
+#define BCM2835_I2C_S_RXD      BIT(5)
+#define BCM2835_I2C_S_TXE      BIT(6)
+#define BCM2835_I2C_S_RXF      BIT(7)
+#define BCM2835_I2C_S_ERR      BIT(8)
+#define BCM2835_I2C_S_CLKT     BIT(9)
+#define BCM2835_I2C_S_LEN      BIT(10) /* Fake bit for SW error reporting */
+
+#define BCM2835_I2C_FEDL_SHIFT 16
+#define BCM2835_I2C_REDL_SHIFT 0
+
+#define BCM2835_I2C_CDIV_MIN   0x0002
+#define BCM2835_I2C_CDIV_MAX   0xFFFE
+
+#define BCM2835_I2C_CLKT_TOUT_MS 35
+#define BCM2835_I2C_XFER_TIMEOUT_MS 1000
+
+struct bcm2835_i2c_priv {
+       void __iomem *regs;
+       ulong input_rate;
+       size_t msg_buf_remaining;
+       u8 *msg_buf;
+       struct i2c_msg *curr_msg;
+       int msg_err;
+       int num_msgs;
+};
+
+struct bcm2835_i2c_clock_msg {
+       struct bcm2835_mbox_hdr hdr;
+       struct bcm2835_mbox_tag_get_clock_rate get_clock_rate;
+       u32 end_tag;
+};
+
+static int bcm2835_i2c_get_input_rate(ulong *rate)
+{
+       ALLOC_CACHE_ALIGN_BUFFER(struct bcm2835_i2c_clock_msg, msg, 1);
+       int ret;
+
+       BCM2835_MBOX_INIT_HDR(msg);
+       BCM2835_MBOX_INIT_TAG(&msg->get_clock_rate, GET_CLOCK_RATE);
+       msg->get_clock_rate.body.req.clock_id = BCM2835_MBOX_CLOCK_ID_CORE;
+
+       ret = bcm2835_mbox_call_prop(BCM2835_MBOX_PROP_CHAN, &msg->hdr);
+       if (ret)
+               return ret;
+
+       *rate = msg->get_clock_rate.body.resp.rate_hz;
+       return *rate ? 0 : -EINVAL;
+}
+
+static inline void bcm2835_i2c_writel(struct bcm2835_i2c_priv *i2c_dev,
+                                     u32 reg, u32 val)
+{
+       writel(val, i2c_dev->regs + reg);
+}
+
+static inline u32 bcm2835_i2c_readl(struct bcm2835_i2c_priv *i2c_dev, u32 reg)
+{
+       return readl(i2c_dev->regs + reg);
+}
+
+static void bcm2835_fill_txfifo(struct bcm2835_i2c_priv *i2c_dev)
+{
+       u32 val;
+
+       while (i2c_dev->msg_buf_remaining) {
+               val = bcm2835_i2c_readl(i2c_dev, BCM2835_I2C_S);
+               if (!(val & BCM2835_I2C_S_TXD))
+                       break;
+               bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_FIFO,
+                                  *i2c_dev->msg_buf);
+               i2c_dev->msg_buf++;
+               i2c_dev->msg_buf_remaining--;
+       }
+}
+
+static void bcm2835_drain_rxfifo(struct bcm2835_i2c_priv *i2c_dev)
+{
+       u32 val;
+
+       while (i2c_dev->msg_buf_remaining) {
+               val = bcm2835_i2c_readl(i2c_dev, BCM2835_I2C_S);
+               if (!(val & BCM2835_I2C_S_RXD))
+                       break;
+               *i2c_dev->msg_buf = bcm2835_i2c_readl(i2c_dev,
+                                                     BCM2835_I2C_FIFO);
+               i2c_dev->msg_buf++;
+               i2c_dev->msg_buf_remaining--;
+       }
+}
+
+static int bcm2835_i2c_calc_divider(unsigned long rate,
+                               unsigned long parent_rate)
+{
+       int divider = DIV_ROUND_UP(parent_rate, rate);
+
+       /*
+        * Per the datasheet, the register is always interpreted as an even
+        * number, by rounding down. In other words, the LSB is ignored. So,
+        * if the LSB is set, increment the divider to avoid any issue.
+        */
+       if (divider & 1)
+               divider++;
+       if ((divider < BCM2835_I2C_CDIV_MIN) ||
+           (divider > BCM2835_I2C_CDIV_MAX))
+               return -EINVAL;
+
+       return divider;
+}
+
+static int bcm2835_i2c_set_rate(struct udevice *bus, unsigned int rate)
+{
+       struct bcm2835_i2c_priv *i2c_dev = dev_get_priv(bus);
+       ulong parent_rate = i2c_dev->input_rate;
+       u32 redl, fedl;
+       u32 clk_tout;
+       int divider;
+
+       if (!rate || !parent_rate)
+               return -EINVAL;
+
+       divider = bcm2835_i2c_calc_divider(rate, parent_rate);
+       if (divider < 0)
+               return divider;
+
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_DIV, divider);
+
+       /*
+        * Number of core clocks to wait after falling edge before
+        * outputting the next data bit.  Note that both FEDL and REDL
+        * can't be greater than CDIV/2.
+        */
+       fedl = max_t(u32, divider / 16, 1);
+
+       /*
+        * Number of core clocks to wait after rising edge before
+        * sampling the next incoming data bit.
+        */
+       redl = max_t(u32, divider / 4, 1);
+
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_DEL,
+                          (fedl << BCM2835_I2C_FEDL_SHIFT) |
+                          (redl << BCM2835_I2C_REDL_SHIFT));
+
+       /*
+        * Set the clock stretch timeout.
+        */
+       if (rate > 0xffff * 1000 / BCM2835_I2C_CLKT_TOUT_MS)
+               clk_tout = 0xffff;
+       else
+               clk_tout = BCM2835_I2C_CLKT_TOUT_MS * rate / 1000;
+
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_CLKT, clk_tout);
+
+       return 0;
+}
+
+static void bcm2835_i2c_start_transfer(struct bcm2835_i2c_priv *i2c_dev)
+{
+       u32 c = BCM2835_I2C_C_ST | BCM2835_I2C_C_I2CEN;
+       struct i2c_msg *msg = i2c_dev->curr_msg;
+       bool last_msg = (i2c_dev->num_msgs == 1);
+
+       if (!i2c_dev->num_msgs)
+               return;
+
+       i2c_dev->num_msgs--;
+       i2c_dev->msg_buf = msg->buf;
+       i2c_dev->msg_buf_remaining = msg->len;
+
+       if (msg->flags & I2C_M_RD)
+               c |= BCM2835_I2C_C_READ;
+       else
+               c |= BCM2835_I2C_C_INTT;
+
+       if (last_msg)
+               c |= BCM2835_I2C_C_INTD;
+
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_A, msg->addr);
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_DLEN, msg->len);
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_C, c);
+}
+
+static void bcm2835_i2c_finish_transfer(struct bcm2835_i2c_priv *i2c_dev)
+{
+       i2c_dev->curr_msg = NULL;
+       i2c_dev->num_msgs = 0;
+
+       i2c_dev->msg_buf = NULL;
+       i2c_dev->msg_buf_remaining = 0;
+}
+
+static int bcm2835_i2c_xfer(struct udevice *dev, struct i2c_msg *msgs, int 
count)
+{
+       struct bcm2835_i2c_priv *i2c_dev = dev_get_priv(dev);
+       bool ignore_nak = false;
+       bool done = false;
+       ulong start;
+       int i;
+       u32 val, err;
+       int ret = 0;
+
+       if (!msgs || count <= 0)
+               return -EINVAL;
+
+       for (i = 0; i < count; i++) {
+               if (msgs[i].flags & I2C_M_TEN)
+                       return -EOPNOTSUPP;
+               if (msgs[i].flags & I2C_M_RD) {
+                       if (i < count - 1) {
+                               dev_err(dev, "read message must be last\n");
+                               return -EOPNOTSUPP;
+                       }
+               }
+               if (msgs[i].len > 0xffff)
+                       return -EINVAL;
+               if (msgs[i].flags & I2C_M_IGNORE_NAK)
+                       ignore_nak = true;
+       }
+
+       /* Clear stale status and FIFO state before starting a new transaction. 
*/
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_C, BCM2835_I2C_C_CLEAR);
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_S,
+                          BCM2835_I2C_S_CLKT | BCM2835_I2C_S_ERR |
+                          BCM2835_I2C_S_DONE);
+
+       i2c_dev->curr_msg = msgs;
+       i2c_dev->num_msgs = count;
+       i2c_dev->msg_err = 0;
+
+       bcm2835_i2c_start_transfer(i2c_dev);
+       start = get_timer(0);
+       while (get_timer(start) < BCM2835_I2C_XFER_TIMEOUT_MS) {
+               val = bcm2835_i2c_readl(i2c_dev, BCM2835_I2C_S);
+
+               err = val & (BCM2835_I2C_S_CLKT | BCM2835_I2C_S_ERR);
+               if (err && !(val & BCM2835_I2C_S_TA))
+                       i2c_dev->msg_err = err;
+
+               if (val & BCM2835_I2C_S_DONE) {
+                       if (!i2c_dev->curr_msg) {
+                               dev_err(dev, "unexpected completion status\n");
+                       } else if (i2c_dev->curr_msg->flags & I2C_M_RD) {
+                               bcm2835_drain_rxfifo(i2c_dev);
+                               val = bcm2835_i2c_readl(i2c_dev, BCM2835_I2C_S);
+                       }
+
+                       if ((val & BCM2835_I2C_S_RXD) ||
+                           i2c_dev->msg_buf_remaining)
+                               i2c_dev->msg_err = BCM2835_I2C_S_LEN;
+                       done = true;
+                       break;
+               }
+
+               if (val & BCM2835_I2C_S_TXW) {
+                       if (!i2c_dev->msg_buf_remaining) {
+                               i2c_dev->msg_err = val | BCM2835_I2C_S_LEN;
+                               break;
+                       }
+
+                       bcm2835_fill_txfifo(i2c_dev);
+
+                       if (i2c_dev->num_msgs > 0 && 
!i2c_dev->msg_buf_remaining) {
+                               i2c_dev->curr_msg++;
+                               bcm2835_i2c_start_transfer(i2c_dev);
+                       }
+               }
+
+               if (val & BCM2835_I2C_S_RXR) {
+                       if (!i2c_dev->msg_buf_remaining) {
+                               i2c_dev->msg_err = val | BCM2835_I2C_S_LEN;
+                               break;
+                       }
+
+                       bcm2835_drain_rxfifo(i2c_dev);
+               }
+               udelay(1);
+       }
+
+       if (!done) {
+               val = bcm2835_i2c_readl(i2c_dev, BCM2835_I2C_S);
+               dev_err(dev, "transfer timeout: status=%08x remaining=%zu\n",
+                       val, i2c_dev->msg_buf_remaining);
+               ret = -ETIMEDOUT;
+       }
+
+       /* Always abort/reset the controller and clear W1C status bits. */
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_C, BCM2835_I2C_C_CLEAR);
+       bcm2835_i2c_writel(i2c_dev, BCM2835_I2C_S, BCM2835_I2C_S_CLKT |
+                          BCM2835_I2C_S_ERR | BCM2835_I2C_S_DONE);
+
+       bcm2835_i2c_finish_transfer(i2c_dev);
+       if (ret)
+               return ret;
+
+       if (ignore_nak)
+               i2c_dev->msg_err &= ~BCM2835_I2C_S_ERR;
+
+       if (!i2c_dev->msg_err)
+               return 0;
+
+       if (i2c_dev->msg_err & BCM2835_I2C_S_ERR)
+               return -EREMOTEIO;
+
+       return -EIO;
+}
+
+static const struct dm_i2c_ops bcm2835_i2c_ops = {
+       .xfer          = bcm2835_i2c_xfer,
+       .set_bus_speed = bcm2835_i2c_set_rate,
+};
+
+static const struct udevice_id bcm2835_i2c_ids[] = {
+       {.compatible = "brcm,bcm2711-i2c"},
+       {.compatible = "brcm,bcm2835-i2c"},
+       {}
+};
+
+static int bcm2835_i2c_probe(struct udevice *bus)
+{
+       struct bcm2835_i2c_priv *priv = dev_get_priv(bus);
+       unsigned int bus_clk_rate;
+       int ret;
+
+       priv->regs = dev_read_addr_ptr(bus);
+       if (!priv->regs) {
+               printf("bcm2835_i2c: cannot get base address\n");
+               return -EINVAL;
+       }
+
+       ret = bcm2835_i2c_get_input_rate(&priv->input_rate);
+       if (ret)
+               return ret;
+
+       bus_clk_rate = dev_read_u32_default(bus, "clock-frequency",
+                                          I2C_SPEED_STANDARD_RATE);
+       ret = bcm2835_i2c_set_rate(bus, bus_clk_rate);
+       if (ret)
+               return ret;
+
+       /* clear FIFO + status */
+       bcm2835_i2c_writel(priv,
+                          BCM2835_I2C_C,
+                          BCM2835_I2C_C_CLEAR);
+
+       bcm2835_i2c_writel(priv,
+                          BCM2835_I2C_S,
+                          BCM2835_I2C_S_CLKT |
+                          BCM2835_I2C_S_ERR |
+                          BCM2835_I2C_S_DONE);
+
+       return 0;
+}
+
+U_BOOT_DRIVER(bcm2835_i2c) = {
+       .name       = "bcm2835_i2c",
+       .id         = UCLASS_I2C,
+       .of_match   = bcm2835_i2c_ids,
+       .probe      = bcm2835_i2c_probe,
+       .priv_auto  = sizeof(struct bcm2835_i2c_priv),
+       .ops        = &bcm2835_i2c_ops,
+};
-- 
2.34.1

Reply via email to