Re: [PATCH v3 4/5] iommu: Add Broadcom BCM2712 IOMMU driver

From: Florian Fainelli

Date: Wed Aug 26 2026 - 14:34:27 EST


On 8/25/26 13:56, Daniel Drake wrote:
This IOMMU translates memory access requests for the VC6 display
pipeline and various multimedia devices in the Broadcom BCM2712 SoC used
on Raspberry Pi 5.

The driver uses the generic_pt framework to manage the 2-level page
tables. It also provides support for the centralized L2 TLB (IOMMUC),
which is shared across the SoC's IOMMU instances and must be flushed
when modifying or unmapping page table entries.

Adapted from Raspberry Pi's downstream bcm2712-iommu driver
(original author Nick Hollinghurst).

Signed-off-by: Daniel Drake <dan@xxxxxxxxxxxxxxx>
---
drivers/iommu/Kconfig | 15 +
drivers/iommu/Makefile | 1 +
drivers/iommu/bcm2712-iommu-cache.c | 83 ++++++
drivers/iommu/bcm2712-iommu-cache.h | 9 +
drivers/iommu/bcm2712-iommu.c | 556 ++++++++++++++++++++++++++++++++++++
5 files changed, 664 insertions(+)

diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig
index 6e07bd69467a..f2c8788158b0 100644
--- a/drivers/iommu/Kconfig
+++ b/drivers/iommu/Kconfig
@@ -386,6 +386,21 @@ config VSI_IOMMU
Say Y here if you want to use this IOMMU in front of these
hardware blocks.
+config BCM2712_IOMMU
+ bool "BCM2712 IOMMU driver"
+ depends on (ARCH_BCM && ARM64) || COMPILE_TEST
+ select IOMMU_API
+ select GENERIC_PT
+ select IOMMU_PT
+ select IOMMU_PT_BCM2712
+ help
+ Support for IOMMU on BCM2712 SoC. This IOMMU can be used by the
+ display controller and various multimedia devices to perform
+ efficient memory management.
+
+ Say Y here if you want to use this IOMMU in front of these
+ hardware blocks.
+
config IOMMU_DEBUG_PAGEALLOC
bool "Debug IOMMU mappings against page allocations"
depends on DEBUG_PAGEALLOC && IOMMU_API && PAGE_EXTENSION
diff --git a/drivers/iommu/Makefile b/drivers/iommu/Makefile
index 2f05725eaab1..29a26d2a3af1 100644
--- a/drivers/iommu/Makefile
+++ b/drivers/iommu/Makefile
@@ -37,4 +37,5 @@ obj-$(CONFIG_IOMMU_IOPF) += io-pgfault.o
obj-$(CONFIG_SPRD_IOMMU) += sprd-iommu.o
obj-$(CONFIG_APPLE_DART) += apple-dart.o
obj-$(CONFIG_VSI_IOMMU) += vsi-iommu.o
+obj-$(CONFIG_BCM2712_IOMMU) += bcm2712-iommu.o bcm2712-iommu-cache.o
obj-$(CONFIG_IOMMU_DEBUG_PAGEALLOC) += iommu-debug-pagealloc.o
diff --git a/drivers/iommu/bcm2712-iommu-cache.c b/drivers/iommu/bcm2712-iommu-cache.c
new file mode 100644
index 000000000000..040cfc4ccf9f
--- /dev/null
+++ b/drivers/iommu/bcm2712-iommu-cache.c
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * IOMMU driver for BCM2712 TLB cache
+ *
+ * Copyright (c) 2023 Raspberry Pi Ltd.
+ * Copyright (c) 2026 Daniel Drake
+ *
+ * The BCM2712 IOMMUC is a centralized TLB which accelerates address translation
+ * across the SoC's IOMMU devices. If an address mapping is not found in the
+ * IOMMU's local TLB cache, then this IOMMUC is consulted. The IOMMUC must be
+ * explicitly invalidated when modifying or unmapping IOMMU page tables.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/err.h>
+#include <linux/of_platform.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <linux/iopoll.h>
+
+#include "bcm2712-iommu-cache.h"
+
+struct bcm2712_iommu_cache {
+ struct device *dev;
+ spinlock_t hw_lock;
+ void __iomem *reg_base;
+};
+
+#define MMUC_CONTROL_ENABLE 1
+#define MMUC_CONTROL_FLUSH 2
+#define MMUC_CONTROL_FLUSHING 4

Those should be BIT(0), BIT(1) and BIT(2) respectively.

+
+void bcm2712_iommu_cache_flush(struct bcm2712_iommu_cache *cache)
+{
+ u32 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &cache->hw_lock) {
+ writel(MMUC_CONTROL_ENABLE | MMUC_CONTROL_FLUSH,
+ cache->reg_base);
+
+ ret = readl_poll_timeout_atomic(cache->reg_base, val,
+ !(val & MMUC_CONTROL_FLUSHING),
+ 0, 50);
+ }
+
+ if (ret)
+ dev_err_ratelimited(cache->dev, "cache flush timed out\n");
+}
+
+static int bcm2712_iommu_cache_probe(struct platform_device *pdev)
+{
+ struct bcm2712_iommu_cache *cache;
+
+ cache = devm_kzalloc(&pdev->dev, sizeof(*cache), GFP_KERNEL);
+ if (!cache)
+ return -ENOMEM;
+
+ cache->dev = &pdev->dev;
+ spin_lock_init(&cache->hw_lock);
+
+ cache->reg_base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(cache->reg_base))
+ return PTR_ERR(cache->reg_base);
+
+ platform_set_drvdata(pdev, cache);
+ return 0;
+}
+
+static const struct of_device_id bcm2712_iommu_cache_of_match[] = {
+ { .compatible = "brcm,bcm2712-iommuc" },
+ { /* sentinel */ },
+};
+
+static struct platform_driver bcm2712_iommu_cache_driver = {
+ .probe = bcm2712_iommu_cache_probe,
+ .driver = {
+ .name = "bcm2712-iommu-cache",
+ .of_match_table = bcm2712_iommu_cache_of_match,
+ .suppress_bind_attrs = true,
+ },
+};
+builtin_platform_driver(bcm2712_iommu_cache_driver);
diff --git a/drivers/iommu/bcm2712-iommu-cache.h b/drivers/iommu/bcm2712-iommu-cache.h
new file mode 100644
index 000000000000..d2f7851831cc
--- /dev/null
+++ b/drivers/iommu/bcm2712-iommu-cache.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _BCM2712_IOMMU_CACHE_H
+#define _BCM2712_IOMMU_CACHE_H
+
+struct bcm2712_iommu_cache;
+
+void bcm2712_iommu_cache_flush(struct bcm2712_iommu_cache *cache);
+
+#endif
diff --git a/drivers/iommu/bcm2712-iommu.c b/drivers/iommu/bcm2712-iommu.c
new file mode 100644
index 000000000000..1bb456a83df5
--- /dev/null
+++ b/drivers/iommu/bcm2712-iommu.c
@@ -0,0 +1,556 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * IOMMU driver for Broadcom BCM2712
+ *
+ * Copyright (c) 2023-2025 Raspberry Pi Ltd.
+ * Copyright (c) 2026 Daniel Drake
+ *
+ * Each BCM2712 IOMMU has multiple devices hardwired into it, whose
+ * DMA transactions all route through the IOMMU. There is no stream ID tagging
+ * or any other kind of segmentation to differentiate between requests from
+ * different devices. It is also not possible to toggle a specific device
+ * between iommu-mapped and bypass modes.
+ *
+ * The hardware supports the simultaneous configuration of a bypass window
+ * (where memory accesses operate in identity mode) and a translation aperture
+ * (supporting page mapping). While combining these two would allow for
+ * GART-like operation, this driver's paging domain implementation configures
+ * only the translation aperture (starting at IOVA 0) offering a degree of
+ * memory protection.
+ *
+ * The page table format is a two-level format handled by generic_pt/bcm2712.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/dma-mapping.h>
+#include <linux/err.h>
+#include <linux/iommu.h>
+#include <linux/iopoll.h>
+#include <linux/of_platform.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <linux/sizes.h>
+#include <linux/generic_pt/iommu.h>
+
+#include "bcm2712-iommu-cache.h"
+#include "dma-iommu.h"
+#include "iommu-pages.h"
+
+/* BCM2712 IOMMU is organized around 4Kbyte pages */
+#define IOMMU_PAGE_SHIFT 12
+#define IOMMU_PAGE_SIZE (1ul << IOMMU_PAGE_SHIFT)
+/* A PTE is 4 bytes */
+#define PTE_SIZE_SHIFT 2
+/* L1/L2 table sizing (IOMMU hardware pages): 1024 entries per page */
+#define PTES_PER_IOPG_SHIFT (IOMMU_PAGE_SHIFT - PTE_SIZE_SHIFT)
+
+#define MMMU_CTRL_OFFSET 0x00
+#define MMMU_CTRL_CAP_EXCEEDED BIT(27)
+#define MMMU_CTRL_CAP_EXCEEDED_ABORT_EN BIT(26)
+#define MMMU_CTRL_CAP_EXCEEDED_INT_EN BIT(25)
+#define MMMU_CTRL_CAP_EXCEEDED_EXCEPTION_EN BIT(24)
+#define MMMU_CTRL_PT_INVALID BIT(20)
+#define MMMU_CTRL_PT_INVALID_ABORT_EN BIT(19)
+#define MMMU_CTRL_PT_INVALID_INT_EN BIT(18)
+#define MMMU_CTRL_PT_INVALID_EXCEPTION_EN BIT(17)
+#define MMMU_CTRL_PT_INVALID_EN BIT(16)
+#define MMMU_CTRL_WRITE_VIOLATION BIT(12)
+#define MMMU_CTRL_WRITE_VIOLATION_ABORT_EN BIT(11)
+#define MMMU_CTRL_WRITE_VIOLATION_INT_EN BIT(10)
+#define MMMU_CTRL_WRITE_VIOLATION_EXCEPTION_EN BIT(9)
+#define MMMU_CTRL_BYPASS BIT(8)
+#define MMMU_CTRL_TLB_CLEARING BIT(7)
+#define MMMU_CTRL_STATS_CLEAR BIT(3)
+#define MMMU_CTRL_TLB_CLEAR BIT(2)
+#define MMMU_CTRL_STATS_ENABLE BIT(1)
+#define MMMU_CTRL_ENABLE BIT(0)
+
+#define MMMU_CTRL_OPERATING_FLAGS (\
+ MMMU_CTRL_CAP_EXCEEDED_ABORT_EN | \
+ MMMU_CTRL_PT_INVALID_ABORT_EN | \
+ MMMU_CTRL_PT_INVALID_EN | \
+ MMMU_CTRL_WRITE_VIOLATION_ABORT_EN | \
+ MMMU_CTRL_STATS_ENABLE | \
+ MMMU_CTRL_ENABLE)
+
+#define MMMU_PT_PA_BASE_OFFSET 0x04
+
+#define MMMU_ADDR_CAP_OFFSET 0x14
+#define MMMU_ADDR_CAP_ENABLE BIT(31)
+#define ADDR_CAP_SHIFT ilog2(SZ_256M)
+
+#define MMMU_SHOOT_DOWN_OFFSET 0x18
+#define MMMU_SHOOT_DOWN_SHOOTING BIT(31)
+#define MMMU_SHOOT_DOWN_SHOOT BIT(30)
+
+#define MMMU_BYPASS_START_OFFSET 0x1c
+#define MMMU_BYPASS_START_ENABLE BIT(31)
+
+#define MMMU_BYPASS_END_OFFSET 0x20
+#define MMMU_BYPASS_END_ENABLE BIT(31)
+
+#define MMMU_MISC_OFFSET 0x24
+#define MMMU_MISC_SINGLE_TABLE BIT(31)
+
+#define MMMU_ILLEGAL_ADR_OFFSET 0x30
+#define MMMU_ILLEGAL_ADR_ENABLE BIT(31)
+
+#define MMMU_DEBUG_INFO_OFFSET 0x38
+#define MMMU_DEBUG_INFO_VERSION_MASK 0x0000000Fu
+#define MMMU_DEBUG_INFO_VA_WIDTH_MASK 0x000000F0u
+#define MMMU_DEBUG_INFO_PA_WIDTH_MASK 0x00000F00u
+#define MMMU_DEBUG_INFO_BIGPAGE_WIDTH_MASK 0x000FF000u
+#define MMMU_DEBUG_INFO_SUPERPAGE_WIDTH_MASK 0x0FF00000u
+#define MMMU_DEBUG_INFO_BYPASS_4M BIT(28)
+#define MMMU_DEBUG_INFO_BYPASS BIT(29)
+
+struct bcm2712_iommu {
+ struct device *dev;
+ struct iommu_device iommu;
+ struct bcm2712_iommu_domain *domain;
+ struct bcm2712_iommu_cache *cache;
+ void __iomem *reg_base;
+ spinlock_t hw_lock;
+ size_t bigpage_size;
+ size_t superpage_size;
+};
+
+struct bcm2712_iommu_domain {
+ union {
+ struct iommu_domain base;
+ struct pt_iommu_bcm2712 pt;
+ };
+ struct bcm2712_iommu *mmu;
+ void *default_page;
+};
+
+#define MMU_WR(off, val) writel(val, mmu->reg_base + (off))
+#define MMU_RD(off) readl(mmu->reg_base + (off))

I would prefer using static inline wrappers here because it gets super easy to just instrument those with debug prints by adding a couple of lines, it also gives you type validation at the same time.

[snip]

+static struct iommu_domain bcm2712_blocking_domain = {
+ .type = IOMMU_DOMAIN_BLOCKED,
+ .ops = &(const struct iommu_domain_ops) {
+ .attach_dev = bcm2712_iommu_blocking_attach,
+ },
+};
+
+static int bcm2712_iommu_enable_and_clear_tlb(struct bcm2712_iommu *mmu)

Maybe add __must_hold() here to indicate when this is called?

The rest looks good to me, but I defer to Jim for the functional review.
--
Florian