* [PATCH v3 00/11] Cortina Systems CS75xx: interrupt controller, GPIO, I2C and TWD watchdog
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
This is the second step of the Cortina Systems CS75xx ("Goldengate G2")
/ Securifi Almond+ support. It applies on top of the platform series
"ARM: Add support for Cortina Systems CS75xx and Securifi Almond+"
v6 [1] and adds:
- a driver for the per-CPU private watchdog of the ARM11 MPCore,
Cortex-A5 and Cortex-A9 ("arm,cortex-a9-twd-wdt"),
- the peripheral interrupt controller that combines the interrupts of
the low speed peripherals into one GIC interrupt,
- the GPIO banks,
- the I2C controller,
- the device tree nodes, including the reset button of the Almond+,
and the multi_v7_defconfig and MAINTAINERS updates.
As in the first series there is no public documentation; register
layouts come from the GPL vendor kernels (2.6.36 for the Almond+, 4.9
for the Verizon FiOS G1100 with the same SoC) and were checked on the
board with the out-of-tree OpenWrt port these drivers are based on.
The patches go to different trees and only depend on each other through
the device tree: the irqchip (1-2), GPIO (3-4), I2C (5-6) and watchdog
(7) patches can be picked up independently, the dts, defconfig and
MAINTAINERS patches (8-11) need the platform series.
TWD watchdog:
The binding "arm,cortex-a9-twd-wdt" (watchdog/arm,twd-wdt.yaml) exists
and several dtsi files use it, but there has been no driver since
mpcore_wdt was deleted in v3.11 ("watchdog: delete mpcore_wdt driver").
The reasons given then were that the watchdog is banked per CPU while
the driver only programmed the CPU that happened to run the ioctl, that
it used request_irq() for a per-CPU interrupt, and that it had no users.
This driver does not use the interrupt at all (the watchdog runs in
reset mode) and programs, pings and stops the watchdogs of all online
CPUs with on_each_cpu() under cpus_read_lock(). A CPU hotplug state
arms the watchdog of a CPU coming online while the device is active and
stops it on a CPU going offline, since an offline CPU cannot ping it.
A watchdog found running at probe time on any CPU is reported as
WDOG_HW_RUNNING and reprogrammed with the driver's timeout without
being stopped; the reset flag sets WDIOF_CARDRESET.
On the Almond+ the stock U-Boot arms the watchdog and the board resets
after ~300 s unless it is serviced. With this driver the watchdog core
pings it until userspace takes over.
The driver also registers a restart handler: the CPU that restarts the
system lets its own watchdog expire right away. The platform series has
no other way to restart the CS75xx, so without it "reboot" hangs.
Interrupt controller:
A status register shows the level of 15 sources and an enable register
masks them; the sources are acknowledged in the peripherals. The driver
uses a generic chip on a linear domain and a chained handler. There is
a second status/enable pair right behind it whose purpose I do not know;
it is not described.
GPIO and pin muxing:
Each pin is either a GPIO line or belongs to a peripheral (NAND,
Ethernet, UART, I2C, PCIe reset, ...). The choice is made per pin in one
GPIO_MUX register per bank, and these registers live in the global
control block at 0xf0000000, which also holds the boot straps, the
clock and PLL settings, peripheral resets and the other pin mux
registers.
In this version the GPIO node gets its bank's GPIO_MUX register as a
second reg entry. A line is switched to GPIO when it is requested as a
GPIO or as an interrupt and switched back to the setting found at probe
time when it is released. This keeps the driver self-contained, but it
means that requesting a line that is in use by a peripheral silently
takes the pin away from it; on this board exporting all lines through
sysfs hangs it (NAND and Ethernet pins lose their function). Boards can
list such lines in gpio-reserved-ranges.
I think the proper model is a pin controller for the global control
block: the block becomes a syscon (it is also needed by the clock driver
that will replace the fixed-clock), a pinctrl driver on top of it
describes the pins, the GPIO function and the peripheral groups, the
GPIO nodes get gpio-ranges and the GPIO driver uses
gpiochip_generic_request()/gpiochip_generic_free(). Pinmux ownership
then refuses GPIO requests for pins claimed by a peripheral's pinctrl
state. The remaining pin mux registers of the block are only partially
understood, which is why I did not start with that. Would you accept
the GPIO driver in this form first, with the pinctrl conversion (and a
binding change to gpio-ranges) later, or should the pin controller come
first?
I2C:
The controller transfers one byte per command with an OpenCores-like
command register. The driver supports plain I2C and emulated SMBus
without quick commands (every message carries at least one data byte)
and without 10-bit addresses.
Testing:
Tested on a Securifi Almond+ with next-20260929 plus series 1 v4 and
this series, both CPUs online. The watchdog found the TWD armed by the boot
loader on both CPUs and took it over ("running on CPU0-1, taking
over"); pinged for 60 s from userspace and then left alone, it reset
the board when timeleft reached zero. The I2C and GPIO interrupts are
delivered through the new peripheral interrupt controller; the reset
button (GPIO 37) is claimed by gpio-keys as an active-low input and
reports KEY_RESTART; an I2C read of the touch controller at 0x48 is
ACKed and an empty address returns -ENXIO without timeouts. reboot(2)
issued from CPU1 restarted the board through the restart handler.
The same drivers also run in an OpenWrt image on Linux 6.18 booted from
NAND through the stock U-Boot, where the SX8650 touchscreen uses a
level-triggered GPIO interrupt.
Build tested with multi_v7_defconfig (GCC 14 and Clang 21, W=1),
dt_binding_check and dtbs_check. Not tested: CPU hotplug of the
watchdog (the platform has no cpu_die yet), repeated START between
messages on I2C (only single-message transfers were run), GPIO
interrupts with real edges.
Open questions:
1. GPIO muxing, see above.
2. The TWD watchdog binding marks clocks as optional, the driver
requires them (the rate is needed to compute the timeout). Should
the binding be tightened, or should the driver fall back to
something when there is no clock?
3. Is a separate MAINTAINERS entry for the TWD watchdog driver the
right thing, or should it be listed under the watchdog subsystem?
[1] https://lore.kernel.org/all/20261013-b4-cs75xx-v6-0-37e8f3969b7c@gmail.com/
---
Changes in v3:
- rebase on v6 of the platform series
- watchdog: follow rate changes of the clock, which on many MPCore
systems scales with the CPU clock, and restore the watchdog of the
boot CPU on resume (sashiko-bot)
- i2c: reset the controller with its interrupt disabled, check the
prescaler against the field width exactly, wait for a bus held by
another master up to the adapter timeout (sashiko-bot)
- Link to v2: https://patch.msgid.link/20261013-b4-cs75xx-s2-v2-0-94f8c0a90cda@gmail.com
Changes in v2:
- rebase on v5 of the platform series
- gpio, i2c, watchdog: do not include <linux/mod_devicetable.h> (Uwe)
- watchdog: the prescaler cannot be changed in watchdog mode (verified
on the board), so a watchdog left running by the bootloader with a
different prescaler is stopped before it is reprogrammed (sashiko-bot)
- i2c: complete a failed transfer only once its stop condition is done,
so that the stop interrupt cannot race with the next transfer; take
the lock over the whole interrupt handler; report a NACK of the last
byte of a write; enable the interrupt again after a reset on timeout;
fail with -EBUSY when the bus stays busy (sashiko-bot)
- gpio: update the both-edges mask only for a valid trigger type and
test it under the lock in the interrupt handler; refuse more than 32
lines (sashiko-bot)
- Link to v1: https://patch.msgid.link/20261012-b4-cs75xx-s2-v1-0-8a7641b7b79c@gmail.com
---
Fil Dunsky (11):
dt-bindings: interrupt-controller: Add Cortina Systems CS7542 peripheral interrupt controller
irqchip: Add Cortina Systems CS75xx peripheral interrupt controller
dt-bindings: gpio: Add Cortina Systems CS7542 GPIO controller
gpio: Add Cortina Systems CS75xx GPIO driver
dt-bindings: i2c: Add Cortina Systems CS7542 I2C controller
i2c: Add Cortina Systems CS75xx I2C controller driver
watchdog: Add ARM TWD (MPCore private) watchdog driver
ARM: dts: cortina: cs7542: Add interrupt controller, GPIO and I2C
ARM: dts: cortina: almondplus: Add reset button and enable I2C
ARM: multi_v7_defconfig: Enable Cortina Systems CS75xx GPIO, I2C and TWD watchdog
MAINTAINERS: Add Cortina Systems CS75xx interrupt controller, GPIO and I2C
.../bindings/gpio/cortina,cs7542-gpio.yaml | 68 +++
.../bindings/i2c/cortina,cs7542-i2c.yaml | 56 +++
.../interrupt-controller/cortina,cs7542-intc.yaml | 57 +++
MAINTAINERS | 12 +
.../dts/cortina/cs7542-securifi-almondplus.dts | 19 +
arch/arm/boot/dts/cortina/cs7542.dtsi | 75 ++++
arch/arm/configs/multi_v7_defconfig | 3 +
drivers/gpio/Kconfig | 12 +
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-cs75xx.c | 336 +++++++++++++++
drivers/i2c/busses/Kconfig | 11 +
drivers/i2c/busses/Makefile | 1 +
drivers/i2c/busses/i2c-cs75xx.c | 392 +++++++++++++++++
drivers/irqchip/Kconfig | 12 +
drivers/irqchip/Makefile | 1 +
drivers/irqchip/irq-cs75xx.c | 103 +++++
drivers/watchdog/Kconfig | 15 +
drivers/watchdog/Makefile | 1 +
drivers/watchdog/arm_twd_wdt.c | 472 +++++++++++++++++++++
19 files changed, 1647 insertions(+)
---
base-commit: 6474fa070f2b8013b4b87350b775b8c3be6e8aac
change-id: 20261012-b4-cs75xx-s2-5f548492f51e
prerequisite-message-id: <20261013-b4-cs75xx-v6-0-37e8f3969b7c@gmail.com>
prerequisite-patch-id: 104c6fb668f30ac8c2c5b3b6c524c18caa8808c4
prerequisite-patch-id: e7d503348cbec7558cd084162133f8e34df1c1bc
prerequisite-patch-id: 1d5770db02b983d012f6c58806fbf3f5291a06c9
prerequisite-patch-id: 8515988ae3509d4289a8d845803353f5ed412982
prerequisite-patch-id: 6c0137d52f201d8ec085ee6cc7b2d49af15583d8
prerequisite-patch-id: 24e110f3c8c008deffdda079bdc27db729b5014d
prerequisite-patch-id: 74958574be7df70bb792f0f6e767d5cae13e1086
prerequisite-patch-id: 15c11a668a1a281649fa0713371e7f07ee44917e
prerequisite-patch-id: 9b4a468b7b74861017ef533ba0a5574bc14b212e
prerequisite-patch-id: 1f232c94dd3fc0d862203b7c0c1449e6ba9a6859
Best regards,
--
Fil Dunsky <filipp.dunsky@gmail.com>
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH v3 01/11] dt-bindings: interrupt-controller: Add Cortina Systems CS7542 peripheral interrupt controller
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
The low speed peripherals of the Cortina Systems CS75xx SoCs share one
GIC interrupt. A small controller in the peripheral register block
reports which of its 15 sources are asserted and allows masking them.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../interrupt-controller/cortina,cs7542-intc.yaml | 57 ++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml b/Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml
new file mode 100644
index 000000000..2963acaf7
--- /dev/null
+++ b/Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml
@@ -0,0 +1,57 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/interrupt-controller/cortina,cs7542-intc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cortina Systems CS7542 peripheral interrupt controller
+
+maintainers:
+ - Fil Dunsky <filipp.dunsky@gmail.com>
+
+description:
+ The low speed peripherals of the Cortina Systems CS75xx (Goldengate G2)
+ SoCs (two software interrupts, two timers, the AXI bridge, SPI, the five
+ GPIO banks, I2C, MDIO, SSP and the random number generator) share one GIC
+ interrupt. This controller has a status register showing the level of the
+ 15 sources and an enable register to mask them. The sources are cleared in
+ the peripherals themselves.
+
+properties:
+ compatible:
+ const: cortina,cs7542-intc
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ interrupt-controller: true
+
+ "#interrupt-cells":
+ const: 1
+ description:
+ The number of the source, 0 to 14. All sources are active high level
+ interrupts.
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - interrupt-controller
+ - "#interrupt-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ interrupt-controller@f0070004 {
+ compatible = "cortina,cs7542-intc";
+ reg = <0xf0070004 0x8>;
+ interrupts = <GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-controller;
+ #interrupt-cells = <1>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 01/11] dt-bindings: interrupt-controller: Add Cortina Systems CS7542 peripheral interrupt controller
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
The low speed peripherals of the Cortina Systems CS75xx SoCs share one
GIC interrupt. A small controller in the peripheral register block
reports which of its 15 sources are asserted and allows masking them.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../interrupt-controller/cortina,cs7542-intc.yaml | 57 ++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml b/Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml
new file mode 100644
index 000000000..2963acaf7
--- /dev/null
+++ b/Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml
@@ -0,0 +1,57 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/interrupt-controller/cortina,cs7542-intc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cortina Systems CS7542 peripheral interrupt controller
+
+maintainers:
+ - Fil Dunsky <filipp.dunsky@gmail.com>
+
+description:
+ The low speed peripherals of the Cortina Systems CS75xx (Goldengate G2)
+ SoCs (two software interrupts, two timers, the AXI bridge, SPI, the five
+ GPIO banks, I2C, MDIO, SSP and the random number generator) share one GIC
+ interrupt. This controller has a status register showing the level of the
+ 15 sources and an enable register to mask them. The sources are cleared in
+ the peripherals themselves.
+
+properties:
+ compatible:
+ const: cortina,cs7542-intc
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ interrupt-controller: true
+
+ "#interrupt-cells":
+ const: 1
+ description:
+ The number of the source, 0 to 14. All sources are active high level
+ interrupts.
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - interrupt-controller
+ - "#interrupt-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ interrupt-controller@f0070004 {
+ compatible = "cortina,cs7542-intc";
+ reg = <0xf0070004 0x8>;
+ interrupts = <GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-controller;
+ #interrupt-cells = <1>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 02/11] irqchip: Add Cortina Systems CS75xx peripheral interrupt controller
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
The timers, GPIO banks, I2C, MDIO, SPI and a few other low speed
peripherals of the Cortina Systems CS75xx SoCs share a single GIC
interrupt. A status register shows the level of the 15 sources and an
enable register masks them; there is nothing to acknowledge, each source
is cleared in its peripheral.
Add a chained driver using a generic chip on a linear domain.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
drivers/irqchip/Kconfig | 12 +++++
drivers/irqchip/Makefile | 1 +
drivers/irqchip/irq-cs75xx.c | 103 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 116 insertions(+)
diff --git a/drivers/irqchip/Kconfig b/drivers/irqchip/Kconfig
index a8f8c423b..89afb4580 100644
--- a/drivers/irqchip/Kconfig
+++ b/drivers/irqchip/Kconfig
@@ -187,6 +187,18 @@ config BRCMSTB_L2_IRQ
select GENERIC_IRQ_CHIP
select IRQ_DOMAIN
+config CS75XX_INTC
+ bool "Cortina Systems CS75xx peripheral interrupt controller"
+ depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
+ default ARCH_CORTINA_ACCESS
+ select GENERIC_IRQ_CHIP
+ select IRQ_DOMAIN
+ help
+ Support for the interrupt controller of the Cortina Systems
+ CS75xx SoCs that combines the interrupts of the low speed
+ peripherals (timers, GPIO banks, I2C, MDIO, SPI) into a single
+ GIC interrupt. Say Y if you have a board with such an SoC.
+
config DAVINCI_CP_INTC
bool
select GENERIC_IRQ_CHIP
diff --git a/drivers/irqchip/Makefile b/drivers/irqchip/Makefile
index ab33cccd8..38c94bfb3 100644
--- a/drivers/irqchip/Makefile
+++ b/drivers/irqchip/Makefile
@@ -135,3 +135,4 @@ obj-$(CONFIG_APPLE_AIC) += irq-apple-aic.o
obj-$(CONFIG_MCHP_EIC) += irq-mchp-eic.o
obj-$(CONFIG_SOPHGO_SG2042_MSI) += irq-sg2042-msi.o
obj-$(CONFIG_SUNPLUS_SP7021_INTC) += irq-sp7021-intc.o
+obj-$(CONFIG_CS75XX_INTC) += irq-cs75xx.o
diff --git a/drivers/irqchip/irq-cs75xx.c b/drivers/irqchip/irq-cs75xx.c
new file mode 100644
index 000000000..4d458039a
--- /dev/null
+++ b/drivers/irqchip/irq-cs75xx.c
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Cortina Systems CS75xx peripheral interrupt controller
+ *
+ * The low speed peripherals on the register bus (timers, GPIO banks, I2C,
+ * MDIO, SPI, ...) share a single GIC interrupt. A status register shows
+ * the level of the 15 sources and an enable register masks them; there is
+ * nothing to acknowledge, the sources are cleared in the peripherals.
+ */
+
+#include <linux/bitops.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/irqchip.h>
+#include <linux/irqchip/chained_irq.h>
+#include <linux/irqdomain.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/of_irq.h>
+
+#define CS75XX_INTC_STATUS 0x00
+#define CS75XX_INTC_ENABLE 0x04
+
+#define CS75XX_INTC_NR_IRQS 15
+
+static void cs75xx_intc_handler(struct irq_desc *desc)
+{
+ struct irq_domain *domain = irq_desc_get_handler_data(desc);
+ struct irq_chip_generic *gc = irq_get_domain_generic_chip(domain, 0);
+ struct irq_chip *chip = irq_desc_get_chip(desc);
+ unsigned long pending;
+ unsigned int hwirq;
+
+ chained_irq_enter(chip, desc);
+
+ pending = readl(gc->reg_base + CS75XX_INTC_STATUS) & gc->mask_cache;
+ for_each_set_bit(hwirq, &pending, CS75XX_INTC_NR_IRQS)
+ generic_handle_domain_irq(domain, hwirq);
+
+ chained_irq_exit(chip, desc);
+}
+
+static int __init cs75xx_intc_init(struct device_node *np,
+ struct device_node *parent)
+{
+ struct irq_chip_generic *gc;
+ struct irq_domain *domain;
+ void __iomem *base;
+ int parent_irq;
+ int ret;
+
+ base = of_io_request_and_map(np, 0, of_node_full_name(np));
+ if (IS_ERR(base)) {
+ pr_err("%pOF: unable to map registers\n", np);
+ return PTR_ERR(base);
+ }
+
+ /* Mask everything before the chained handler is installed */
+ writel(0, base + CS75XX_INTC_ENABLE);
+
+ parent_irq = irq_of_parse_and_map(np, 0);
+ if (!parent_irq) {
+ pr_err("%pOF: unable to map parent interrupt\n", np);
+ ret = -EINVAL;
+ goto err_unmap;
+ }
+
+ domain = irq_domain_create_linear(of_fwnode_handle(np),
+ CS75XX_INTC_NR_IRQS,
+ &irq_generic_chip_ops, NULL);
+ if (!domain) {
+ ret = -ENOMEM;
+ goto err_dispose;
+ }
+
+ ret = irq_alloc_domain_generic_chips(domain, CS75XX_INTC_NR_IRQS, 1,
+ "cs75xx-intc", handle_level_irq,
+ IRQ_NOREQUEST | IRQ_NOPROBE, 0,
+ IRQ_GC_INIT_MASK_CACHE);
+ if (ret)
+ goto err_domain;
+
+ gc = irq_get_domain_generic_chip(domain, 0);
+ gc->reg_base = base;
+ gc->chip_types[0].regs.mask = CS75XX_INTC_ENABLE;
+ gc->chip_types[0].chip.irq_mask = irq_gc_mask_clr_bit;
+ gc->chip_types[0].chip.irq_unmask = irq_gc_mask_set_bit;
+
+ irq_set_chained_handler_and_data(parent_irq, cs75xx_intc_handler,
+ domain);
+
+ return 0;
+
+err_domain:
+ irq_domain_remove(domain);
+err_dispose:
+ irq_dispose_mapping(parent_irq);
+err_unmap:
+ iounmap(base);
+ return ret;
+}
+
+IRQCHIP_DECLARE(cs7542_intc, "cortina,cs7542-intc", cs75xx_intc_init);
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 02/11] irqchip: Add Cortina Systems CS75xx peripheral interrupt controller
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
The timers, GPIO banks, I2C, MDIO, SPI and a few other low speed
peripherals of the Cortina Systems CS75xx SoCs share a single GIC
interrupt. A status register shows the level of the 15 sources and an
enable register masks them; there is nothing to acknowledge, each source
is cleared in its peripheral.
Add a chained driver using a generic chip on a linear domain.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
drivers/irqchip/Kconfig | 12 +++++
drivers/irqchip/Makefile | 1 +
drivers/irqchip/irq-cs75xx.c | 103 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 116 insertions(+)
diff --git a/drivers/irqchip/Kconfig b/drivers/irqchip/Kconfig
index a8f8c423b..89afb4580 100644
--- a/drivers/irqchip/Kconfig
+++ b/drivers/irqchip/Kconfig
@@ -187,6 +187,18 @@ config BRCMSTB_L2_IRQ
select GENERIC_IRQ_CHIP
select IRQ_DOMAIN
+config CS75XX_INTC
+ bool "Cortina Systems CS75xx peripheral interrupt controller"
+ depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
+ default ARCH_CORTINA_ACCESS
+ select GENERIC_IRQ_CHIP
+ select IRQ_DOMAIN
+ help
+ Support for the interrupt controller of the Cortina Systems
+ CS75xx SoCs that combines the interrupts of the low speed
+ peripherals (timers, GPIO banks, I2C, MDIO, SPI) into a single
+ GIC interrupt. Say Y if you have a board with such an SoC.
+
config DAVINCI_CP_INTC
bool
select GENERIC_IRQ_CHIP
diff --git a/drivers/irqchip/Makefile b/drivers/irqchip/Makefile
index ab33cccd8..38c94bfb3 100644
--- a/drivers/irqchip/Makefile
+++ b/drivers/irqchip/Makefile
@@ -135,3 +135,4 @@ obj-$(CONFIG_APPLE_AIC) += irq-apple-aic.o
obj-$(CONFIG_MCHP_EIC) += irq-mchp-eic.o
obj-$(CONFIG_SOPHGO_SG2042_MSI) += irq-sg2042-msi.o
obj-$(CONFIG_SUNPLUS_SP7021_INTC) += irq-sp7021-intc.o
+obj-$(CONFIG_CS75XX_INTC) += irq-cs75xx.o
diff --git a/drivers/irqchip/irq-cs75xx.c b/drivers/irqchip/irq-cs75xx.c
new file mode 100644
index 000000000..4d458039a
--- /dev/null
+++ b/drivers/irqchip/irq-cs75xx.c
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Cortina Systems CS75xx peripheral interrupt controller
+ *
+ * The low speed peripherals on the register bus (timers, GPIO banks, I2C,
+ * MDIO, SPI, ...) share a single GIC interrupt. A status register shows
+ * the level of the 15 sources and an enable register masks them; there is
+ * nothing to acknowledge, the sources are cleared in the peripherals.
+ */
+
+#include <linux/bitops.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/irqchip.h>
+#include <linux/irqchip/chained_irq.h>
+#include <linux/irqdomain.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/of_irq.h>
+
+#define CS75XX_INTC_STATUS 0x00
+#define CS75XX_INTC_ENABLE 0x04
+
+#define CS75XX_INTC_NR_IRQS 15
+
+static void cs75xx_intc_handler(struct irq_desc *desc)
+{
+ struct irq_domain *domain = irq_desc_get_handler_data(desc);
+ struct irq_chip_generic *gc = irq_get_domain_generic_chip(domain, 0);
+ struct irq_chip *chip = irq_desc_get_chip(desc);
+ unsigned long pending;
+ unsigned int hwirq;
+
+ chained_irq_enter(chip, desc);
+
+ pending = readl(gc->reg_base + CS75XX_INTC_STATUS) & gc->mask_cache;
+ for_each_set_bit(hwirq, &pending, CS75XX_INTC_NR_IRQS)
+ generic_handle_domain_irq(domain, hwirq);
+
+ chained_irq_exit(chip, desc);
+}
+
+static int __init cs75xx_intc_init(struct device_node *np,
+ struct device_node *parent)
+{
+ struct irq_chip_generic *gc;
+ struct irq_domain *domain;
+ void __iomem *base;
+ int parent_irq;
+ int ret;
+
+ base = of_io_request_and_map(np, 0, of_node_full_name(np));
+ if (IS_ERR(base)) {
+ pr_err("%pOF: unable to map registers\n", np);
+ return PTR_ERR(base);
+ }
+
+ /* Mask everything before the chained handler is installed */
+ writel(0, base + CS75XX_INTC_ENABLE);
+
+ parent_irq = irq_of_parse_and_map(np, 0);
+ if (!parent_irq) {
+ pr_err("%pOF: unable to map parent interrupt\n", np);
+ ret = -EINVAL;
+ goto err_unmap;
+ }
+
+ domain = irq_domain_create_linear(of_fwnode_handle(np),
+ CS75XX_INTC_NR_IRQS,
+ &irq_generic_chip_ops, NULL);
+ if (!domain) {
+ ret = -ENOMEM;
+ goto err_dispose;
+ }
+
+ ret = irq_alloc_domain_generic_chips(domain, CS75XX_INTC_NR_IRQS, 1,
+ "cs75xx-intc", handle_level_irq,
+ IRQ_NOREQUEST | IRQ_NOPROBE, 0,
+ IRQ_GC_INIT_MASK_CACHE);
+ if (ret)
+ goto err_domain;
+
+ gc = irq_get_domain_generic_chip(domain, 0);
+ gc->reg_base = base;
+ gc->chip_types[0].regs.mask = CS75XX_INTC_ENABLE;
+ gc->chip_types[0].chip.irq_mask = irq_gc_mask_clr_bit;
+ gc->chip_types[0].chip.irq_unmask = irq_gc_mask_set_bit;
+
+ irq_set_chained_handler_and_data(parent_irq, cs75xx_intc_handler,
+ domain);
+
+ return 0;
+
+err_domain:
+ irq_domain_remove(domain);
+err_dispose:
+ irq_dispose_mapping(parent_irq);
+err_unmap:
+ iounmap(base);
+ return ret;
+}
+
+IRQCHIP_DECLARE(cs7542_intc, "cortina,cs7542-intc", cs75xx_intc_init);
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 03/11] dt-bindings: gpio: Add Cortina Systems CS7542 GPIO controller
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
The Cortina Systems CS75xx SoCs have five banks of 32 GPIO lines. Each
pin is shared between a GPIO line and a peripheral function, selected by
the bank's GPIO_MUX register in the global control block, which is
described as the second register region of the bank.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../bindings/gpio/cortina,cs7542-gpio.yaml | 68 ++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml b/Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml
new file mode 100644
index 000000000..b54421d65
--- /dev/null
+++ b/Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml
@@ -0,0 +1,68 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/gpio/cortina,cs7542-gpio.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cortina Systems CS7542 GPIO controller
+
+maintainers:
+ - Fil Dunsky <filipp.dunsky@gmail.com>
+
+description:
+ The Cortina Systems CS75xx (Goldengate G2) SoCs have five banks of 32
+ GPIO lines, each with its own interrupt. Every pin is shared between a
+ GPIO line and a peripheral function; the choice is made per pin in the
+ GPIO_MUX register of the bank, which is part of the global control
+ block.
+
+properties:
+ compatible:
+ const: cortina,cs7542-gpio
+
+ reg:
+ items:
+ - description: GPIO bank registers
+ - description: GPIO_MUX register of the bank
+
+ interrupts:
+ maxItems: 1
+
+ gpio-controller: true
+
+ "#gpio-cells":
+ const: 2
+
+ gpio-line-names:
+ maxItems: 32
+
+ gpio-reserved-ranges: true
+
+ interrupt-controller: true
+
+ "#interrupt-cells":
+ const: 2
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - gpio-controller
+ - "#gpio-cells"
+ - interrupt-controller
+ - "#interrupt-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ gpio@f00701f0 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf00701f0 0x20>, <0xf0000020 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <7>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 03/11] dt-bindings: gpio: Add Cortina Systems CS7542 GPIO controller
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
The Cortina Systems CS75xx SoCs have five banks of 32 GPIO lines. Each
pin is shared between a GPIO line and a peripheral function, selected by
the bank's GPIO_MUX register in the global control block, which is
described as the second register region of the bank.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../bindings/gpio/cortina,cs7542-gpio.yaml | 68 ++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml b/Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml
new file mode 100644
index 000000000..b54421d65
--- /dev/null
+++ b/Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml
@@ -0,0 +1,68 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/gpio/cortina,cs7542-gpio.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cortina Systems CS7542 GPIO controller
+
+maintainers:
+ - Fil Dunsky <filipp.dunsky@gmail.com>
+
+description:
+ The Cortina Systems CS75xx (Goldengate G2) SoCs have five banks of 32
+ GPIO lines, each with its own interrupt. Every pin is shared between a
+ GPIO line and a peripheral function; the choice is made per pin in the
+ GPIO_MUX register of the bank, which is part of the global control
+ block.
+
+properties:
+ compatible:
+ const: cortina,cs7542-gpio
+
+ reg:
+ items:
+ - description: GPIO bank registers
+ - description: GPIO_MUX register of the bank
+
+ interrupts:
+ maxItems: 1
+
+ gpio-controller: true
+
+ "#gpio-cells":
+ const: 2
+
+ gpio-line-names:
+ maxItems: 32
+
+ gpio-reserved-ranges: true
+
+ interrupt-controller: true
+
+ "#interrupt-cells":
+ const: 2
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - gpio-controller
+ - "#gpio-cells"
+ - interrupt-controller
+ - "#interrupt-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ gpio@f00701f0 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf00701f0 0x20>, <0xf0000020 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <7>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 04/11] gpio: Add Cortina Systems CS75xx GPIO driver
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
Add a driver for the GPIO banks of the Cortina Systems CS75xx SoCs. The
data and direction registers are handled by gpio-mmio; each bank has its
own interrupt with level and edge triggers, both edges are emulated by
flipping the polarity.
A pin only works as a GPIO when its bit in the bank's GPIO_MUX register
is set. The driver sets it when a line is requested as a GPIO or as an
interrupt and restores the value found at probe time when the line is
released.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
drivers/gpio/Kconfig | 12 ++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-cs75xx.c | 336 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 349 insertions(+)
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index dfdc9ee1a..6b9524f25 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -300,6 +300,18 @@ config GPIO_CLPS711X
help
Say yes here to support GPIO on CLPS711X SoCs.
+config GPIO_CS75XX
+ tristate "Cortina Systems CS75xx GPIO support"
+ depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
+ select GPIO_GENERIC
+ select GPIOLIB_IRQCHIP
+ help
+ Say yes here to support the GPIO banks of the Cortina Systems
+ CS75xx SoCs, including their interrupts.
+
+ Requesting a line switches its pin from the peripheral function
+ to GPIO until the line is released.
+
config GPIO_DAVINCI
tristate "TI Davinci/Keystone GPIO support"
default y if ARCH_DAVINCI
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index 88f748846..a38ad4331 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -59,6 +59,7 @@ obj-$(CONFIG_GPIO_CLPS711X) += gpio-clps711x.o
obj-$(CONFIG_GPIO_SNPS_CREG) += gpio-creg-snps.o
obj-$(CONFIG_GPIO_CROS_EC) += gpio-cros-ec.o
obj-$(CONFIG_GPIO_CRYSTAL_COVE) += gpio-crystalcove.o
+obj-$(CONFIG_GPIO_CS75XX) += gpio-cs75xx.o
obj-$(CONFIG_GPIO_CS5535) += gpio-cs5535.o
obj-$(CONFIG_GPIO_DA9052) += gpio-da9052.o
obj-$(CONFIG_GPIO_DA9055) += gpio-da9055.o
diff --git a/drivers/gpio/gpio-cs75xx.c b/drivers/gpio/gpio-cs75xx.c
new file mode 100644
index 000000000..d3e8d3a99
--- /dev/null
+++ b/drivers/gpio/gpio-cs75xx.c
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Cortina Systems CS75xx GPIO controller
+ *
+ * Each bank has 32 lines with direction, data and interrupt registers.
+ * Whether a pin is driven by the GPIO bank or by its peripheral function
+ * (NAND, Ethernet, UART, ...) is selected by the bank's GPIO_MUX register
+ * in the global control block. A line is switched to GPIO when it is
+ * requested as a GPIO or as an interrupt and switched back to its initial
+ * setting when it is released.
+ */
+
+#include <linux/bitops.h>
+#include <linux/gpio/driver.h>
+#include <linux/gpio/generic.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+#define CS75XX_GPIO_CFG 0x00 /* 1: input, 0: output */
+#define CS75XX_GPIO_OUT 0x04
+#define CS75XX_GPIO_IN 0x08
+#define CS75XX_GPIO_LVL 0x0c /* 1: high level or rising edge */
+#define CS75XX_GPIO_EDGE 0x10 /* 1: edge, 0: level */
+#define CS75XX_GPIO_IE 0x14
+#define CS75XX_GPIO_INT 0x18 /* write 1 to clear */
+
+struct cs75xx_gpio {
+ struct gpio_generic_chip chip;
+ void __iomem *base;
+ void __iomem *mux;
+ u32 mux_init;
+ u32 gpio_used;
+ u32 irq_used;
+ u32 both_edges;
+};
+
+/* Called with the chip lock held */
+static void cs75xx_gpio_update_mux(struct cs75xx_gpio *g, unsigned int offset)
+{
+ u32 mask = BIT(offset);
+ u32 val = readl(g->mux);
+
+ if ((g->gpio_used | g->irq_used) & mask)
+ val |= mask;
+ else
+ val = (val & ~mask) | (g->mux_init & mask);
+
+ writel(val, g->mux);
+}
+
+static int cs75xx_gpio_request(struct gpio_chip *gc, unsigned int offset)
+{
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ g->gpio_used |= BIT(offset);
+ cs75xx_gpio_update_mux(g, offset);
+
+ return 0;
+}
+
+static void cs75xx_gpio_free(struct gpio_chip *gc, unsigned int offset)
+{
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ g->gpio_used &= ~BIT(offset);
+ cs75xx_gpio_update_mux(g, offset);
+}
+
+static void cs75xx_gpio_irq_ack(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+
+ writel(BIT(irqd_to_hwirq(d)), g->base + CS75XX_GPIO_INT);
+}
+
+static void cs75xx_gpio_irq_mask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+
+ scoped_guard(gpio_generic_lock_irqsave, &g->chip) {
+ u32 ie = readl(g->base + CS75XX_GPIO_IE);
+
+ writel(ie & ~BIT(hwirq), g->base + CS75XX_GPIO_IE);
+ }
+
+ gpiochip_disable_irq(gc, hwirq);
+}
+
+static void cs75xx_gpio_irq_unmask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+ u32 ie;
+
+ gpiochip_enable_irq(gc, hwirq);
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ /*
+ * The status of a level interrupt is latched while the line is
+ * asserted, also after the ack, so it may be stale by now. Clear it,
+ * it is set again right away if the line is still asserted.
+ */
+ if (irqd_is_level_type(d))
+ writel(BIT(hwirq), g->base + CS75XX_GPIO_INT);
+ ie = readl(g->base + CS75XX_GPIO_IE);
+ writel(ie | BIT(hwirq), g->base + CS75XX_GPIO_IE);
+}
+
+/* Both edges are emulated by flipping the polarity on every interrupt */
+static void cs75xx_gpio_set_polarity(struct cs75xx_gpio *g, u32 mask)
+{
+ u32 lvl = readl(g->base + CS75XX_GPIO_LVL);
+
+ if (readl(g->base + CS75XX_GPIO_IN) & mask)
+ lvl &= ~mask;
+ else
+ lvl |= mask;
+
+ writel(lvl, g->base + CS75XX_GPIO_LVL);
+}
+
+static int cs75xx_gpio_irq_set_type(struct irq_data *d, unsigned int type)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ u32 mask = BIT(irqd_to_hwirq(d));
+ u32 lvl, edge, both_edges;
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+
+ lvl = readl(g->base + CS75XX_GPIO_LVL);
+ edge = readl(g->base + CS75XX_GPIO_EDGE);
+ both_edges = g->both_edges & ~mask;
+
+ switch (type & IRQ_TYPE_SENSE_MASK) {
+ case IRQ_TYPE_LEVEL_LOW:
+ lvl &= ~mask;
+ edge &= ~mask;
+ break;
+ case IRQ_TYPE_LEVEL_HIGH:
+ lvl |= mask;
+ edge &= ~mask;
+ break;
+ case IRQ_TYPE_EDGE_FALLING:
+ lvl &= ~mask;
+ edge |= mask;
+ break;
+ case IRQ_TYPE_EDGE_RISING:
+ lvl |= mask;
+ edge |= mask;
+ break;
+ case IRQ_TYPE_EDGE_BOTH:
+ both_edges |= mask;
+ edge |= mask;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ g->both_edges = both_edges;
+ writel(lvl, g->base + CS75XX_GPIO_LVL);
+ writel(edge, g->base + CS75XX_GPIO_EDGE);
+ if (g->both_edges & mask)
+ cs75xx_gpio_set_polarity(g, mask);
+
+ if (type & IRQ_TYPE_LEVEL_MASK)
+ irq_set_handler_locked(d, handle_level_irq);
+ else
+ irq_set_handler_locked(d, handle_edge_irq);
+
+ return 0;
+}
+
+static int cs75xx_gpio_irq_reqres(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+ int ret;
+
+ /* The boot loader may leave the line configured as an output */
+ ret = gc->direction_input(gc, hwirq);
+ if (ret)
+ return ret;
+
+ ret = gpiochip_reqres_irq(gc, hwirq);
+ if (ret)
+ return ret;
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ g->irq_used |= BIT(hwirq);
+ cs75xx_gpio_update_mux(g, hwirq);
+
+ return 0;
+}
+
+static void cs75xx_gpio_irq_relres(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+
+ scoped_guard(gpio_generic_lock_irqsave, &g->chip) {
+ g->irq_used &= ~BIT(hwirq);
+ cs75xx_gpio_update_mux(g, hwirq);
+ }
+
+ gpiochip_relres_irq(gc, hwirq);
+}
+
+static const struct irq_chip cs75xx_gpio_irq_chip = {
+ .name = "cs75xx-gpio",
+ .irq_ack = cs75xx_gpio_irq_ack,
+ .irq_mask = cs75xx_gpio_irq_mask,
+ .irq_unmask = cs75xx_gpio_irq_unmask,
+ .irq_set_type = cs75xx_gpio_irq_set_type,
+ .irq_request_resources = cs75xx_gpio_irq_reqres,
+ .irq_release_resources = cs75xx_gpio_irq_relres,
+ .flags = IRQCHIP_IMMUTABLE | IRQCHIP_MASK_ON_SUSPEND,
+};
+
+static void cs75xx_gpio_irq_handler(struct irq_desc *desc)
+{
+ struct gpio_chip *gc = irq_desc_get_handler_data(desc);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ struct irq_chip *chip = irq_desc_get_chip(desc);
+ unsigned long pending;
+ unsigned int offset;
+
+ chained_irq_enter(chip, desc);
+
+ pending = readl(g->base + CS75XX_GPIO_INT) &
+ readl(g->base + CS75XX_GPIO_IE);
+
+ for_each_set_bit(offset, &pending, gc->ngpio) {
+ scoped_guard(gpio_generic_lock, &g->chip) {
+ if (g->both_edges & BIT(offset))
+ cs75xx_gpio_set_polarity(g, BIT(offset));
+ }
+ generic_handle_domain_irq(gc->irq.domain, offset);
+ }
+
+ chained_irq_exit(chip, desc);
+}
+
+static int cs75xx_gpio_probe(struct platform_device *pdev)
+{
+ struct gpio_generic_chip_config config;
+ struct device *dev = &pdev->dev;
+ struct gpio_irq_chip *girq;
+ struct cs75xx_gpio *g;
+ int irq, ret;
+
+ g = devm_kzalloc(dev, sizeof(*g), GFP_KERNEL);
+ if (!g)
+ return -ENOMEM;
+
+ g->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(g->base))
+ return PTR_ERR(g->base);
+
+ g->mux = devm_platform_ioremap_resource(pdev, 1);
+ if (IS_ERR(g->mux))
+ return PTR_ERR(g->mux);
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ return irq;
+
+ g->mux_init = readl(g->mux);
+
+ config = (struct gpio_generic_chip_config) {
+ .dev = dev,
+ .sz = 4,
+ .dat = g->base + CS75XX_GPIO_IN,
+ .set = g->base + CS75XX_GPIO_OUT,
+ .dirin = g->base + CS75XX_GPIO_CFG,
+ };
+
+ ret = gpio_generic_chip_init(&g->chip, &config);
+ if (ret)
+ return dev_err_probe(dev, ret, "unable to init generic GPIO\n");
+
+ if (g->chip.gc.ngpio > 32)
+ return dev_err_probe(dev, -EINVAL, "too many lines: %u\n",
+ g->chip.gc.ngpio);
+
+ g->chip.gc.request = cs75xx_gpio_request;
+ g->chip.gc.free = cs75xx_gpio_free;
+ g->chip.gc.owner = THIS_MODULE;
+
+ /* Mask and clear all interrupts before the parent is set up */
+ writel(0, g->base + CS75XX_GPIO_IE);
+ writel(~0, g->base + CS75XX_GPIO_INT);
+
+ girq = &g->chip.gc.irq;
+ gpio_irq_chip_set_chip(girq, &cs75xx_gpio_irq_chip);
+ girq->parent_handler = cs75xx_gpio_irq_handler;
+ girq->num_parents = 1;
+ girq->parents = devm_kcalloc(dev, 1, sizeof(*girq->parents),
+ GFP_KERNEL);
+ if (!girq->parents)
+ return -ENOMEM;
+ girq->parents[0] = irq;
+ girq->default_type = IRQ_TYPE_NONE;
+ girq->handler = handle_bad_irq;
+
+ return devm_gpiochip_add_data(dev, &g->chip.gc, g);
+}
+
+static const struct of_device_id cs75xx_gpio_of_match[] = {
+ { .compatible = "cortina,cs7542-gpio" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, cs75xx_gpio_of_match);
+
+static struct platform_driver cs75xx_gpio_driver = {
+ .probe = cs75xx_gpio_probe,
+ .driver = {
+ .name = "cs75xx-gpio",
+ .of_match_table = cs75xx_gpio_of_match,
+ },
+};
+module_platform_driver(cs75xx_gpio_driver);
+
+MODULE_DESCRIPTION("Cortina Systems CS75xx GPIO driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 04/11] gpio: Add Cortina Systems CS75xx GPIO driver
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
Add a driver for the GPIO banks of the Cortina Systems CS75xx SoCs. The
data and direction registers are handled by gpio-mmio; each bank has its
own interrupt with level and edge triggers, both edges are emulated by
flipping the polarity.
A pin only works as a GPIO when its bit in the bank's GPIO_MUX register
is set. The driver sets it when a line is requested as a GPIO or as an
interrupt and restores the value found at probe time when the line is
released.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
drivers/gpio/Kconfig | 12 ++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-cs75xx.c | 336 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 349 insertions(+)
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index dfdc9ee1a..6b9524f25 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -300,6 +300,18 @@ config GPIO_CLPS711X
help
Say yes here to support GPIO on CLPS711X SoCs.
+config GPIO_CS75XX
+ tristate "Cortina Systems CS75xx GPIO support"
+ depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
+ select GPIO_GENERIC
+ select GPIOLIB_IRQCHIP
+ help
+ Say yes here to support the GPIO banks of the Cortina Systems
+ CS75xx SoCs, including their interrupts.
+
+ Requesting a line switches its pin from the peripheral function
+ to GPIO until the line is released.
+
config GPIO_DAVINCI
tristate "TI Davinci/Keystone GPIO support"
default y if ARCH_DAVINCI
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index 88f748846..a38ad4331 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -59,6 +59,7 @@ obj-$(CONFIG_GPIO_CLPS711X) += gpio-clps711x.o
obj-$(CONFIG_GPIO_SNPS_CREG) += gpio-creg-snps.o
obj-$(CONFIG_GPIO_CROS_EC) += gpio-cros-ec.o
obj-$(CONFIG_GPIO_CRYSTAL_COVE) += gpio-crystalcove.o
+obj-$(CONFIG_GPIO_CS75XX) += gpio-cs75xx.o
obj-$(CONFIG_GPIO_CS5535) += gpio-cs5535.o
obj-$(CONFIG_GPIO_DA9052) += gpio-da9052.o
obj-$(CONFIG_GPIO_DA9055) += gpio-da9055.o
diff --git a/drivers/gpio/gpio-cs75xx.c b/drivers/gpio/gpio-cs75xx.c
new file mode 100644
index 000000000..d3e8d3a99
--- /dev/null
+++ b/drivers/gpio/gpio-cs75xx.c
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Cortina Systems CS75xx GPIO controller
+ *
+ * Each bank has 32 lines with direction, data and interrupt registers.
+ * Whether a pin is driven by the GPIO bank or by its peripheral function
+ * (NAND, Ethernet, UART, ...) is selected by the bank's GPIO_MUX register
+ * in the global control block. A line is switched to GPIO when it is
+ * requested as a GPIO or as an interrupt and switched back to its initial
+ * setting when it is released.
+ */
+
+#include <linux/bitops.h>
+#include <linux/gpio/driver.h>
+#include <linux/gpio/generic.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+#define CS75XX_GPIO_CFG 0x00 /* 1: input, 0: output */
+#define CS75XX_GPIO_OUT 0x04
+#define CS75XX_GPIO_IN 0x08
+#define CS75XX_GPIO_LVL 0x0c /* 1: high level or rising edge */
+#define CS75XX_GPIO_EDGE 0x10 /* 1: edge, 0: level */
+#define CS75XX_GPIO_IE 0x14
+#define CS75XX_GPIO_INT 0x18 /* write 1 to clear */
+
+struct cs75xx_gpio {
+ struct gpio_generic_chip chip;
+ void __iomem *base;
+ void __iomem *mux;
+ u32 mux_init;
+ u32 gpio_used;
+ u32 irq_used;
+ u32 both_edges;
+};
+
+/* Called with the chip lock held */
+static void cs75xx_gpio_update_mux(struct cs75xx_gpio *g, unsigned int offset)
+{
+ u32 mask = BIT(offset);
+ u32 val = readl(g->mux);
+
+ if ((g->gpio_used | g->irq_used) & mask)
+ val |= mask;
+ else
+ val = (val & ~mask) | (g->mux_init & mask);
+
+ writel(val, g->mux);
+}
+
+static int cs75xx_gpio_request(struct gpio_chip *gc, unsigned int offset)
+{
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ g->gpio_used |= BIT(offset);
+ cs75xx_gpio_update_mux(g, offset);
+
+ return 0;
+}
+
+static void cs75xx_gpio_free(struct gpio_chip *gc, unsigned int offset)
+{
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ g->gpio_used &= ~BIT(offset);
+ cs75xx_gpio_update_mux(g, offset);
+}
+
+static void cs75xx_gpio_irq_ack(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+
+ writel(BIT(irqd_to_hwirq(d)), g->base + CS75XX_GPIO_INT);
+}
+
+static void cs75xx_gpio_irq_mask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+
+ scoped_guard(gpio_generic_lock_irqsave, &g->chip) {
+ u32 ie = readl(g->base + CS75XX_GPIO_IE);
+
+ writel(ie & ~BIT(hwirq), g->base + CS75XX_GPIO_IE);
+ }
+
+ gpiochip_disable_irq(gc, hwirq);
+}
+
+static void cs75xx_gpio_irq_unmask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+ u32 ie;
+
+ gpiochip_enable_irq(gc, hwirq);
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ /*
+ * The status of a level interrupt is latched while the line is
+ * asserted, also after the ack, so it may be stale by now. Clear it,
+ * it is set again right away if the line is still asserted.
+ */
+ if (irqd_is_level_type(d))
+ writel(BIT(hwirq), g->base + CS75XX_GPIO_INT);
+ ie = readl(g->base + CS75XX_GPIO_IE);
+ writel(ie | BIT(hwirq), g->base + CS75XX_GPIO_IE);
+}
+
+/* Both edges are emulated by flipping the polarity on every interrupt */
+static void cs75xx_gpio_set_polarity(struct cs75xx_gpio *g, u32 mask)
+{
+ u32 lvl = readl(g->base + CS75XX_GPIO_LVL);
+
+ if (readl(g->base + CS75XX_GPIO_IN) & mask)
+ lvl &= ~mask;
+ else
+ lvl |= mask;
+
+ writel(lvl, g->base + CS75XX_GPIO_LVL);
+}
+
+static int cs75xx_gpio_irq_set_type(struct irq_data *d, unsigned int type)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ u32 mask = BIT(irqd_to_hwirq(d));
+ u32 lvl, edge, both_edges;
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+
+ lvl = readl(g->base + CS75XX_GPIO_LVL);
+ edge = readl(g->base + CS75XX_GPIO_EDGE);
+ both_edges = g->both_edges & ~mask;
+
+ switch (type & IRQ_TYPE_SENSE_MASK) {
+ case IRQ_TYPE_LEVEL_LOW:
+ lvl &= ~mask;
+ edge &= ~mask;
+ break;
+ case IRQ_TYPE_LEVEL_HIGH:
+ lvl |= mask;
+ edge &= ~mask;
+ break;
+ case IRQ_TYPE_EDGE_FALLING:
+ lvl &= ~mask;
+ edge |= mask;
+ break;
+ case IRQ_TYPE_EDGE_RISING:
+ lvl |= mask;
+ edge |= mask;
+ break;
+ case IRQ_TYPE_EDGE_BOTH:
+ both_edges |= mask;
+ edge |= mask;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ g->both_edges = both_edges;
+ writel(lvl, g->base + CS75XX_GPIO_LVL);
+ writel(edge, g->base + CS75XX_GPIO_EDGE);
+ if (g->both_edges & mask)
+ cs75xx_gpio_set_polarity(g, mask);
+
+ if (type & IRQ_TYPE_LEVEL_MASK)
+ irq_set_handler_locked(d, handle_level_irq);
+ else
+ irq_set_handler_locked(d, handle_edge_irq);
+
+ return 0;
+}
+
+static int cs75xx_gpio_irq_reqres(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+ int ret;
+
+ /* The boot loader may leave the line configured as an output */
+ ret = gc->direction_input(gc, hwirq);
+ if (ret)
+ return ret;
+
+ ret = gpiochip_reqres_irq(gc, hwirq);
+ if (ret)
+ return ret;
+
+ guard(gpio_generic_lock_irqsave)(&g->chip);
+ g->irq_used |= BIT(hwirq);
+ cs75xx_gpio_update_mux(g, hwirq);
+
+ return 0;
+}
+
+static void cs75xx_gpio_irq_relres(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+
+ scoped_guard(gpio_generic_lock_irqsave, &g->chip) {
+ g->irq_used &= ~BIT(hwirq);
+ cs75xx_gpio_update_mux(g, hwirq);
+ }
+
+ gpiochip_relres_irq(gc, hwirq);
+}
+
+static const struct irq_chip cs75xx_gpio_irq_chip = {
+ .name = "cs75xx-gpio",
+ .irq_ack = cs75xx_gpio_irq_ack,
+ .irq_mask = cs75xx_gpio_irq_mask,
+ .irq_unmask = cs75xx_gpio_irq_unmask,
+ .irq_set_type = cs75xx_gpio_irq_set_type,
+ .irq_request_resources = cs75xx_gpio_irq_reqres,
+ .irq_release_resources = cs75xx_gpio_irq_relres,
+ .flags = IRQCHIP_IMMUTABLE | IRQCHIP_MASK_ON_SUSPEND,
+};
+
+static void cs75xx_gpio_irq_handler(struct irq_desc *desc)
+{
+ struct gpio_chip *gc = irq_desc_get_handler_data(desc);
+ struct cs75xx_gpio *g = gpiochip_get_data(gc);
+ struct irq_chip *chip = irq_desc_get_chip(desc);
+ unsigned long pending;
+ unsigned int offset;
+
+ chained_irq_enter(chip, desc);
+
+ pending = readl(g->base + CS75XX_GPIO_INT) &
+ readl(g->base + CS75XX_GPIO_IE);
+
+ for_each_set_bit(offset, &pending, gc->ngpio) {
+ scoped_guard(gpio_generic_lock, &g->chip) {
+ if (g->both_edges & BIT(offset))
+ cs75xx_gpio_set_polarity(g, BIT(offset));
+ }
+ generic_handle_domain_irq(gc->irq.domain, offset);
+ }
+
+ chained_irq_exit(chip, desc);
+}
+
+static int cs75xx_gpio_probe(struct platform_device *pdev)
+{
+ struct gpio_generic_chip_config config;
+ struct device *dev = &pdev->dev;
+ struct gpio_irq_chip *girq;
+ struct cs75xx_gpio *g;
+ int irq, ret;
+
+ g = devm_kzalloc(dev, sizeof(*g), GFP_KERNEL);
+ if (!g)
+ return -ENOMEM;
+
+ g->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(g->base))
+ return PTR_ERR(g->base);
+
+ g->mux = devm_platform_ioremap_resource(pdev, 1);
+ if (IS_ERR(g->mux))
+ return PTR_ERR(g->mux);
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ return irq;
+
+ g->mux_init = readl(g->mux);
+
+ config = (struct gpio_generic_chip_config) {
+ .dev = dev,
+ .sz = 4,
+ .dat = g->base + CS75XX_GPIO_IN,
+ .set = g->base + CS75XX_GPIO_OUT,
+ .dirin = g->base + CS75XX_GPIO_CFG,
+ };
+
+ ret = gpio_generic_chip_init(&g->chip, &config);
+ if (ret)
+ return dev_err_probe(dev, ret, "unable to init generic GPIO\n");
+
+ if (g->chip.gc.ngpio > 32)
+ return dev_err_probe(dev, -EINVAL, "too many lines: %u\n",
+ g->chip.gc.ngpio);
+
+ g->chip.gc.request = cs75xx_gpio_request;
+ g->chip.gc.free = cs75xx_gpio_free;
+ g->chip.gc.owner = THIS_MODULE;
+
+ /* Mask and clear all interrupts before the parent is set up */
+ writel(0, g->base + CS75XX_GPIO_IE);
+ writel(~0, g->base + CS75XX_GPIO_INT);
+
+ girq = &g->chip.gc.irq;
+ gpio_irq_chip_set_chip(girq, &cs75xx_gpio_irq_chip);
+ girq->parent_handler = cs75xx_gpio_irq_handler;
+ girq->num_parents = 1;
+ girq->parents = devm_kcalloc(dev, 1, sizeof(*girq->parents),
+ GFP_KERNEL);
+ if (!girq->parents)
+ return -ENOMEM;
+ girq->parents[0] = irq;
+ girq->default_type = IRQ_TYPE_NONE;
+ girq->handler = handle_bad_irq;
+
+ return devm_gpiochip_add_data(dev, &g->chip.gc, g);
+}
+
+static const struct of_device_id cs75xx_gpio_of_match[] = {
+ { .compatible = "cortina,cs7542-gpio" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, cs75xx_gpio_of_match);
+
+static struct platform_driver cs75xx_gpio_driver = {
+ .probe = cs75xx_gpio_probe,
+ .driver = {
+ .name = "cs75xx-gpio",
+ .of_match_table = cs75xx_gpio_of_match,
+ },
+};
+module_platform_driver(cs75xx_gpio_driver);
+
+MODULE_DESCRIPTION("Cortina Systems CS75xx GPIO driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH v3 04/11] gpio: Add Cortina Systems CS75xx GPIO driver
2026-10-13 6:07 ` Fil Dunsky
(?)
@ 2026-10-13 6:44 ` sashiko-bot
-1 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-10-13 6:44 UTC (permalink / raw)
To: Fil Dunsky; +Cc: devicetree, linux-gpio, linux-watchdog, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] gpio: hardware direction mutated before acquiring IRQ ownership
- [Medium] gpio: cs75xx_gpio_irq_handler drops latched edges due to early polarity flip
- [Low] kconfig: unmet dependency warning for GPIO_GENERIC without HAS_IOMEM
--
commit 4137a122e8274d6f113967f6692bbb652886d35a
Author: Fil Dunsky <filipp.dunsky@gmail.com>
gpio: Add Cortina Systems CS75xx GPIO driver
Add a driver for the GPIO banks of the Cortina Systems CS75xx SoCs.
The data and direction registers are handled by gpio-mmio; each bank
has its own interrupt with level and edge triggers.
> diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
> --- a/drivers/gpio/Kconfig
> +++ b/drivers/gpio/Kconfig
[ ... ]
> @@ -300,6 +300,18 @@ config GPIO_CLPS711X
> help
> Say yes here to support GPIO on CLPS711X SoCs.
>
> +config GPIO_CS75XX
> + tristate "Cortina Systems CS75xx GPIO support"
> + depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
> + select GPIO_GENERIC
[Severity: Low]
Does this Kconfig setup trigger an unmet dependency warning when HAS_IOMEM is
not set?
By enabling COMPILE_TEST, this configuration allows the driver to be built on
architectures like User-Mode Linux which lack HAS_IOMEM. However, it uses
select GPIO_GENERIC without inheriting its dependency on HAS_IOMEM. This
could lead to unmet direct dependency warnings and potential build failures.
Consider adding depends on HAS_IOMEM here to safely propagate the requirement.
> diff --git a/drivers/gpio/gpio-cs75xx.c b/drivers/gpio/gpio-cs75xx.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/gpio/gpio-cs75xx.c
[ ... ]
> +static void cs75xx_gpio_irq_ack(struct irq_data *d)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct cs75xx_gpio *g = gpiochip_get_data(gc);
> +
> + writel(BIT(irqd_to_hwirq(d)), g->base + CS75XX_GPIO_INT);
> +}
[ ... ]
> +static int cs75xx_gpio_irq_reqres(struct irq_data *d)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct cs75xx_gpio *g = gpiochip_get_data(gc);
> + irq_hw_number_t hwirq = irqd_to_hwirq(d);
> + int ret;
> +
> + /* The boot loader may leave the line configured as an output */
> + ret = gc->direction_input(gc, hwirq);
> + if (ret)
> + return ret;
> +
> + ret = gpiochip_reqres_irq(gc, hwirq);
[Severity: Medium]
Is it safe to mutate the hardware direction before successfully acquiring
the gpiolib IRQ ownership?
If the GPIO line is concurrently requested and actively driven as an output
by another subsystem, changing its hardware direction abruptly could break
the output signal and corrupt the hardware state. While gpiochip_reqres_irq()
checks the in-use state and will ultimately reject the request if it is an
output, the hardware has already been improperly modified here. The core
gpiolib ownership check should likely happen before the hardware state is
changed.
[ ... ]
> +static void cs75xx_gpio_irq_handler(struct irq_desc *desc)
> +{
> + struct gpio_chip *gc = irq_desc_get_handler_data(desc);
> + struct cs75xx_gpio *g = gpiochip_get_data(gc);
> + struct irq_chip *chip = irq_desc_get_chip(desc);
> + unsigned long pending;
> + unsigned int offset;
> +
> + chained_irq_enter(chip, desc);
> +
> + pending = readl(g->base + CS75XX_GPIO_INT) &
> + readl(g->base + CS75XX_GPIO_IE);
> +
> + for_each_set_bit(offset, &pending, gc->ngpio) {
> + scoped_guard(gpio_generic_lock, &g->chip) {
> + if (g->both_edges & BIT(offset))
> + cs75xx_gpio_set_polarity(g, BIT(offset));
> + }
> + generic_handle_domain_irq(gc->irq.domain, offset);
[Severity: Medium]
Could this drop latched edge interrupts due to an early polarity flip?
In this handler, the edge polarity is toggled to arm the hardware for the
opposite edge, followed by generic_handle_domain_irq(), which uses
handle_edge_irq(). The generic edge handler invokes cs75xx_gpio_irq_ack()
which explicitly clears the interrupt status bit unconditionally.
If the pin state flips again in the small window between
cs75xx_gpio_set_polarity() and cs75xx_gpio_irq_ack(), the hardware latches
the new edge, but the subsequent ack clears it. This could result in edge
interrupts being permanently lost, potentially causing peripheral drivers
waiting for GPIO events to hang.
> + }
> +
> + chained_irq_exit(chip, desc);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261013-b4-cs75xx-s2-v3-0-181bb0d23f4c@gmail.com?part=4
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v3 05/11] dt-bindings: i2c: Add Cortina Systems CS7542 I2C controller
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
Add a binding for the I2C ("Bi-Wire") controller of the Cortina Systems
CS75xx SoCs.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../bindings/i2c/cortina,cs7542-i2c.yaml | 56 ++++++++++++++++++++++
1 file changed, 56 insertions(+)
diff --git a/Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml b/Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml
new file mode 100644
index 000000000..f70984bd7
--- /dev/null
+++ b/Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i2c/cortina,cs7542-i2c.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cortina Systems CS7542 I2C controller
+
+maintainers:
+ - Fil Dunsky <filipp.dunsky@gmail.com>
+
+description:
+ The I2C ("Bi-Wire") controller of the Cortina Systems CS75xx (Goldengate
+ G2) SoCs. It transfers one byte per command and runs from the peripheral
+ (APB) clock through a 16-bit prescaler.
+
+allOf:
+ - $ref: /schemas/i2c/i2c-controller.yaml#
+
+properties:
+ compatible:
+ const: cortina,cs7542-i2c
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 1
+
+ clock-frequency:
+ enum: [100000, 400000]
+ default: 100000
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ i2c@f00700e8 {
+ compatible = "cortina,cs7542-i2c";
+ reg = <0xf00700e8 0x28>;
+ interrupt-parent = <&intc>;
+ interrupts = <11>;
+ clocks = <&apb_clk>;
+ clock-frequency = <400000>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 05/11] dt-bindings: i2c: Add Cortina Systems CS7542 I2C controller
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
Add a binding for the I2C ("Bi-Wire") controller of the Cortina Systems
CS75xx SoCs.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../bindings/i2c/cortina,cs7542-i2c.yaml | 56 ++++++++++++++++++++++
1 file changed, 56 insertions(+)
diff --git a/Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml b/Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml
new file mode 100644
index 000000000..f70984bd7
--- /dev/null
+++ b/Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i2c/cortina,cs7542-i2c.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cortina Systems CS7542 I2C controller
+
+maintainers:
+ - Fil Dunsky <filipp.dunsky@gmail.com>
+
+description:
+ The I2C ("Bi-Wire") controller of the Cortina Systems CS75xx (Goldengate
+ G2) SoCs. It transfers one byte per command and runs from the peripheral
+ (APB) clock through a 16-bit prescaler.
+
+allOf:
+ - $ref: /schemas/i2c/i2c-controller.yaml#
+
+properties:
+ compatible:
+ const: cortina,cs7542-i2c
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 1
+
+ clock-frequency:
+ enum: [100000, 400000]
+ default: 100000
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ i2c@f00700e8 {
+ compatible = "cortina,cs7542-i2c";
+ reg = <0xf00700e8 0x28>;
+ interrupt-parent = <&intc>;
+ interrupts = <11>;
+ clocks = <&apb_clk>;
+ clock-frequency = <400000>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 06/11] i2c: Add Cortina Systems CS75xx I2C controller driver
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
The I2C ("Bi-Wire") controller of the Cortina Systems CS75xx SoCs
transfers one byte per command, with a command register much like the
OpenCores one (start, stop, read, write, acknowledge), and raises an
interrupt when a command has completed.
Messages are sent with a repeated start between them and a stop after
the last one. Every message needs at least one data byte, so SMBus quick
commands are not supported. Arbitration loss is reported as -EAGAIN and
retried by the core.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
drivers/i2c/busses/Kconfig | 11 ++
drivers/i2c/busses/Makefile | 1 +
drivers/i2c/busses/i2c-cs75xx.c | 392 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 404 insertions(+)
diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig
index d7b895083..a65e951a5 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -520,6 +520,17 @@ config I2C_CADENCE
Say yes here to select Cadence I2C Host Controller. This controller is
e.g. used by Xilinx Zynq.
+config I2C_CS75XX
+ tristate "Cortina Systems CS75xx I2C controller"
+ depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
+ depends on HAS_IOMEM
+ help
+ Say yes here to support the I2C ("Bi-Wire") controller of the
+ Cortina Systems CS75xx SoCs.
+
+ This driver can also be built as a module. If so, the module
+ will be called i2c-cs75xx.
+
config I2C_CBUS_GPIO
tristate "CBUS I2C driver"
depends on GPIOLIB || COMPILE_TEST
diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile
index 3755c54b3..aab1beeb2 100644
--- a/drivers/i2c/busses/Makefile
+++ b/drivers/i2c/busses/Makefile
@@ -47,6 +47,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_CS75XX) += i2c-cs75xx.o
obj-$(CONFIG_I2C_CGBC) += i2c-cgbc.o
obj-$(CONFIG_I2C_CPM) += i2c-cpm.o
obj-$(CONFIG_I2C_DAVINCI) += i2c-davinci.o
diff --git a/drivers/i2c/busses/i2c-cs75xx.c b/drivers/i2c/busses/i2c-cs75xx.c
new file mode 100644
index 000000000..d39c8e75a
--- /dev/null
+++ b/drivers/i2c/busses/i2c-cs75xx.c
@@ -0,0 +1,392 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Cortina Systems CS75xx I2C ("Bi-Wire") controller
+ *
+ * The controller works one byte at a time: software writes the byte to
+ * transmit and a command (start, stop, read, write, acknowledge) and gets
+ * an interrupt when the command has completed.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/clk.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/spinlock.h>
+
+#define CS75XX_I2C_CFG 0x00
+#define CS75XX_I2C_CTRL 0x04
+#define CS75XX_I2C_TXR 0x08
+#define CS75XX_I2C_RXR 0x0c
+#define CS75XX_I2C_ACK 0x10
+#define CS75XX_I2C_IE 0x14
+#define CS75XX_I2C_INT 0x18
+
+#define CS75XX_I2C_CFG_EN BIT(0)
+#define CS75XX_I2C_CFG_PRER GENMASK(31, 16)
+
+#define CS75XX_I2C_CTRL_DONE BIT(0)
+#define CS75XX_I2C_CTRL_NACK BIT(3)
+#define CS75XX_I2C_CTRL_WRITE BIT(4)
+#define CS75XX_I2C_CTRL_READ BIT(5)
+#define CS75XX_I2C_CTRL_STOP BIT(6)
+#define CS75XX_I2C_CTRL_START BIT(7)
+
+#define CS75XX_I2C_ACK_AL BIT(0)
+#define CS75XX_I2C_ACK_BUSY BIT(1)
+#define CS75XX_I2C_ACK_NACK BIT(2)
+
+#define CS75XX_I2C_INT_DONE BIT(0)
+
+enum cs75xx_i2c_state {
+ CS75XX_I2C_IDLE,
+ CS75XX_I2C_ADDR,
+ CS75XX_I2C_WRITE,
+ CS75XX_I2C_READ,
+ CS75XX_I2C_STOP,
+};
+
+struct cs75xx_i2c {
+ struct i2c_adapter adap;
+ struct device *dev;
+ void __iomem *base;
+ int irq;
+ u32 prer;
+
+ spinlock_t lock; /* protects the transfer state */
+ struct completion done;
+ enum cs75xx_i2c_state state;
+ struct i2c_msg *msg;
+ unsigned int pos;
+ bool last;
+ int err;
+};
+
+static void cs75xx_i2c_hw_init(struct cs75xx_i2c *i2c)
+{
+ if (readl(i2c->base + CS75XX_I2C_CFG) & CS75XX_I2C_CFG_EN) {
+ writel(0, i2c->base + CS75XX_I2C_CFG);
+ usleep_range(5000, 10000);
+ }
+
+ writel(FIELD_PREP(CS75XX_I2C_CFG_PRER, i2c->prer) | CS75XX_I2C_CFG_EN,
+ i2c->base + CS75XX_I2C_CFG);
+ usleep_range(5000, 10000);
+}
+
+/*
+ * Reset the controller and enable its interrupt again, with the handler
+ * kept away from the registers meanwhile
+ */
+static void cs75xx_i2c_reset(struct cs75xx_i2c *i2c)
+{
+ disable_irq(i2c->irq);
+ cs75xx_i2c_hw_init(i2c);
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_INT);
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_IE);
+ enable_irq(i2c->irq);
+}
+
+static void cs75xx_i2c_finish(struct cs75xx_i2c *i2c, int err)
+{
+ i2c->err = err;
+ i2c->state = CS75XX_I2C_IDLE;
+ complete(&i2c->done);
+}
+
+/*
+ * End the transfer with a stop condition and complete it only once the
+ * stop is done, so that its interrupt cannot be taken for the next one
+ */
+static void cs75xx_i2c_abort(struct cs75xx_i2c *i2c, int err)
+{
+ i2c->err = err;
+ i2c->state = CS75XX_I2C_STOP;
+ writel(CS75XX_I2C_CTRL_STOP, i2c->base + CS75XX_I2C_CTRL);
+}
+
+/* Start the transfer of the next data byte of the message */
+static void cs75xx_i2c_next_byte(struct cs75xx_i2c *i2c)
+{
+ struct i2c_msg *msg = i2c->msg;
+ bool last_byte = i2c->pos == msg->len - 1;
+ u32 cmd;
+
+ if (msg->flags & I2C_M_RD) {
+ i2c->state = CS75XX_I2C_READ;
+ cmd = CS75XX_I2C_CTRL_READ;
+ /* Do not acknowledge the last byte of a read */
+ if (last_byte)
+ cmd |= CS75XX_I2C_CTRL_NACK;
+ } else {
+ i2c->state = CS75XX_I2C_WRITE;
+ writel(msg->buf[i2c->pos++], i2c->base + CS75XX_I2C_TXR);
+ cmd = CS75XX_I2C_CTRL_WRITE;
+ }
+
+ if (last_byte && i2c->last)
+ cmd |= CS75XX_I2C_CTRL_STOP;
+
+ writel(cmd, i2c->base + CS75XX_I2C_CTRL);
+}
+
+static void cs75xx_i2c_process(struct cs75xx_i2c *i2c, u32 ack)
+{
+ struct i2c_msg *msg = i2c->msg;
+
+ if (i2c->state == CS75XX_I2C_STOP) {
+ cs75xx_i2c_finish(i2c, i2c->err);
+ return;
+ }
+
+ if (ack & CS75XX_I2C_ACK_AL) {
+ cs75xx_i2c_abort(i2c, -EAGAIN);
+ return;
+ }
+
+ switch (i2c->state) {
+ case CS75XX_I2C_ADDR:
+ if (ack & CS75XX_I2C_ACK_NACK) {
+ cs75xx_i2c_abort(i2c, -ENXIO);
+ return;
+ }
+ cs75xx_i2c_next_byte(i2c);
+ break;
+
+ case CS75XX_I2C_WRITE:
+ if (ack & CS75XX_I2C_ACK_NACK) {
+ /* The last byte of the last message carried the stop */
+ if (i2c->pos == msg->len && i2c->last)
+ cs75xx_i2c_finish(i2c, -EIO);
+ else
+ cs75xx_i2c_abort(i2c, -EIO);
+ return;
+ }
+ if (i2c->pos == msg->len) {
+ cs75xx_i2c_finish(i2c, 0);
+ return;
+ }
+ cs75xx_i2c_next_byte(i2c);
+ break;
+
+ case CS75XX_I2C_READ:
+ msg->buf[i2c->pos++] = readl(i2c->base + CS75XX_I2C_RXR);
+ if (i2c->pos == msg->len) {
+ cs75xx_i2c_finish(i2c, 0);
+ return;
+ }
+ cs75xx_i2c_next_byte(i2c);
+ break;
+
+ case CS75XX_I2C_IDLE:
+ case CS75XX_I2C_STOP:
+ /* Late interrupt of a transfer that timed out */
+ break;
+ }
+}
+
+static irqreturn_t cs75xx_i2c_isr(int irq, void *dev_id)
+{
+ struct cs75xx_i2c *i2c = dev_id;
+ u32 ctrl, ack;
+
+ guard(spinlock)(&i2c->lock);
+
+ if (!(readl(i2c->base + CS75XX_I2C_INT) & CS75XX_I2C_INT_DONE))
+ return IRQ_NONE;
+
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_INT);
+
+ /* The done flag is cleared by writing it back */
+ ctrl = readl(i2c->base + CS75XX_I2C_CTRL);
+ writel(ctrl, i2c->base + CS75XX_I2C_CTRL);
+
+ ack = readl(i2c->base + CS75XX_I2C_ACK);
+ cs75xx_i2c_process(i2c, ack);
+
+ return IRQ_HANDLED;
+}
+
+static int cs75xx_i2c_xfer_msg(struct cs75xx_i2c *i2c, struct i2c_msg *msg,
+ bool last)
+{
+ unsigned long time_left;
+
+ spin_lock_irq(&i2c->lock);
+ reinit_completion(&i2c->done);
+ i2c->msg = msg;
+ i2c->pos = 0;
+ i2c->last = last;
+ i2c->err = 0;
+ i2c->state = CS75XX_I2C_ADDR;
+ writel(i2c_8bit_addr_from_msg(msg), i2c->base + CS75XX_I2C_TXR);
+ writel(CS75XX_I2C_CTRL_START | CS75XX_I2C_CTRL_WRITE,
+ i2c->base + CS75XX_I2C_CTRL);
+ spin_unlock_irq(&i2c->lock);
+
+ time_left = wait_for_completion_timeout(&i2c->done, i2c->adap.timeout);
+ if (!time_left) {
+ spin_lock_irq(&i2c->lock);
+ i2c->state = CS75XX_I2C_IDLE;
+ spin_unlock_irq(&i2c->lock);
+ dev_err(i2c->dev, "transfer timed out, resetting the controller\n");
+ cs75xx_i2c_reset(i2c);
+ return -ETIMEDOUT;
+ }
+
+ return i2c->err;
+}
+
+static int cs75xx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
+ int num)
+{
+ struct cs75xx_i2c *i2c = i2c_get_adapdata(adap);
+ u32 val;
+ int i, ret;
+
+ /* Another master may hold the bus for a whole transfer */
+ if (readl_poll_timeout(i2c->base + CS75XX_I2C_ACK, val,
+ !(val & CS75XX_I2C_ACK_BUSY), 100,
+ jiffies_to_usecs(adap->timeout))) {
+ cs75xx_i2c_reset(i2c);
+ if (readl(i2c->base + CS75XX_I2C_ACK) & CS75XX_I2C_ACK_BUSY)
+ return -EBUSY;
+ }
+
+ for (i = 0; i < num; i++) {
+ ret = cs75xx_i2c_xfer_msg(i2c, &msgs[i], i == num - 1);
+ if (ret)
+ return ret;
+ }
+
+ return num;
+}
+
+static u32 cs75xx_i2c_func(struct i2c_adapter *adap)
+{
+ return I2C_FUNC_I2C | (I2C_FUNC_SMBUS_EMUL & ~I2C_FUNC_SMBUS_QUICK);
+}
+
+static const struct i2c_algorithm cs75xx_i2c_algo = {
+ .xfer = cs75xx_i2c_xfer,
+ .functionality = cs75xx_i2c_func,
+};
+
+/* A message always carries at least one data byte after the address */
+static const struct i2c_adapter_quirks cs75xx_i2c_quirks = {
+ .flags = I2C_AQ_NO_ZERO_LEN,
+};
+
+static void cs75xx_i2c_disable(void *data)
+{
+ struct cs75xx_i2c *i2c = data;
+
+ writel(0, i2c->base + CS75XX_I2C_IE);
+ writel(0, i2c->base + CS75XX_I2C_CFG);
+}
+
+static int cs75xx_i2c_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct cs75xx_i2c *i2c;
+ unsigned long ref_rate;
+ struct clk *clk;
+ u32 bus_rate;
+ u64 div;
+ int ret;
+
+ i2c = devm_kzalloc(dev, sizeof(*i2c), GFP_KERNEL);
+ if (!i2c)
+ return -ENOMEM;
+
+ i2c->dev = dev;
+ spin_lock_init(&i2c->lock);
+ init_completion(&i2c->done);
+
+ i2c->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(i2c->base))
+ return PTR_ERR(i2c->base);
+
+ i2c->irq = platform_get_irq(pdev, 0);
+ if (i2c->irq < 0)
+ return i2c->irq;
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk), "failed to get clock\n");
+
+ ref_rate = clk_get_rate(clk);
+ if (device_property_read_u32(dev, "clock-frequency", &bus_rate))
+ bus_rate = I2C_MAX_STANDARD_MODE_FREQ;
+ if (!bus_rate || bus_rate > I2C_MAX_FAST_MODE_FREQ)
+ return dev_err_probe(dev, -EINVAL,
+ "unsupported bus frequency %u Hz\n",
+ bus_rate);
+
+ div = DIV_ROUND_UP_ULL(ref_rate, 5ULL * bus_rate);
+ if (!div || div - 1 > FIELD_MAX(CS75XX_I2C_CFG_PRER))
+ return dev_err_probe(dev, -EINVAL,
+ "cannot derive %u Hz from %lu Hz\n",
+ bus_rate, ref_rate);
+ i2c->prer = div - 1;
+
+ cs75xx_i2c_hw_init(i2c);
+
+ writel(0, i2c->base + CS75XX_I2C_IE);
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_INT);
+
+ ret = devm_add_action_or_reset(dev, cs75xx_i2c_disable, i2c);
+ if (ret)
+ return ret;
+
+ ret = devm_request_irq(dev, i2c->irq, cs75xx_i2c_isr, 0, dev_name(dev),
+ i2c);
+ if (ret)
+ return ret;
+
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_IE);
+
+ i2c->adap.owner = THIS_MODULE;
+ i2c->adap.algo = &cs75xx_i2c_algo;
+ i2c->adap.quirks = &cs75xx_i2c_quirks;
+ i2c->adap.retries = 3;
+ i2c->adap.dev.parent = dev;
+ i2c->adap.dev.of_node = dev->of_node;
+ strscpy(i2c->adap.name, dev_name(dev), sizeof(i2c->adap.name));
+ i2c_set_adapdata(&i2c->adap, i2c);
+
+ ret = devm_i2c_add_adapter(dev, &i2c->adap);
+ if (ret)
+ return ret;
+
+ dev_dbg(dev, "bus %u Hz, reference %lu Hz\n", bus_rate,
+ ref_rate);
+
+ return 0;
+}
+
+static const struct of_device_id cs75xx_i2c_of_match[] = {
+ { .compatible = "cortina,cs7542-i2c" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, cs75xx_i2c_of_match);
+
+static struct platform_driver cs75xx_i2c_driver = {
+ .probe = cs75xx_i2c_probe,
+ .driver = {
+ .name = "cs75xx-i2c",
+ .of_match_table = cs75xx_i2c_of_match,
+ },
+};
+module_platform_driver(cs75xx_i2c_driver);
+
+MODULE_DESCRIPTION("Cortina Systems CS75xx I2C controller driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 06/11] i2c: Add Cortina Systems CS75xx I2C controller driver
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
The I2C ("Bi-Wire") controller of the Cortina Systems CS75xx SoCs
transfers one byte per command, with a command register much like the
OpenCores one (start, stop, read, write, acknowledge), and raises an
interrupt when a command has completed.
Messages are sent with a repeated start between them and a stop after
the last one. Every message needs at least one data byte, so SMBus quick
commands are not supported. Arbitration loss is reported as -EAGAIN and
retried by the core.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
drivers/i2c/busses/Kconfig | 11 ++
drivers/i2c/busses/Makefile | 1 +
drivers/i2c/busses/i2c-cs75xx.c | 392 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 404 insertions(+)
diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig
index d7b895083..a65e951a5 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -520,6 +520,17 @@ config I2C_CADENCE
Say yes here to select Cadence I2C Host Controller. This controller is
e.g. used by Xilinx Zynq.
+config I2C_CS75XX
+ tristate "Cortina Systems CS75xx I2C controller"
+ depends on ARCH_CORTINA_ACCESS || COMPILE_TEST
+ depends on HAS_IOMEM
+ help
+ Say yes here to support the I2C ("Bi-Wire") controller of the
+ Cortina Systems CS75xx SoCs.
+
+ This driver can also be built as a module. If so, the module
+ will be called i2c-cs75xx.
+
config I2C_CBUS_GPIO
tristate "CBUS I2C driver"
depends on GPIOLIB || COMPILE_TEST
diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile
index 3755c54b3..aab1beeb2 100644
--- a/drivers/i2c/busses/Makefile
+++ b/drivers/i2c/busses/Makefile
@@ -47,6 +47,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_CS75XX) += i2c-cs75xx.o
obj-$(CONFIG_I2C_CGBC) += i2c-cgbc.o
obj-$(CONFIG_I2C_CPM) += i2c-cpm.o
obj-$(CONFIG_I2C_DAVINCI) += i2c-davinci.o
diff --git a/drivers/i2c/busses/i2c-cs75xx.c b/drivers/i2c/busses/i2c-cs75xx.c
new file mode 100644
index 000000000..d39c8e75a
--- /dev/null
+++ b/drivers/i2c/busses/i2c-cs75xx.c
@@ -0,0 +1,392 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Cortina Systems CS75xx I2C ("Bi-Wire") controller
+ *
+ * The controller works one byte at a time: software writes the byte to
+ * transmit and a command (start, stop, read, write, acknowledge) and gets
+ * an interrupt when the command has completed.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/clk.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/spinlock.h>
+
+#define CS75XX_I2C_CFG 0x00
+#define CS75XX_I2C_CTRL 0x04
+#define CS75XX_I2C_TXR 0x08
+#define CS75XX_I2C_RXR 0x0c
+#define CS75XX_I2C_ACK 0x10
+#define CS75XX_I2C_IE 0x14
+#define CS75XX_I2C_INT 0x18
+
+#define CS75XX_I2C_CFG_EN BIT(0)
+#define CS75XX_I2C_CFG_PRER GENMASK(31, 16)
+
+#define CS75XX_I2C_CTRL_DONE BIT(0)
+#define CS75XX_I2C_CTRL_NACK BIT(3)
+#define CS75XX_I2C_CTRL_WRITE BIT(4)
+#define CS75XX_I2C_CTRL_READ BIT(5)
+#define CS75XX_I2C_CTRL_STOP BIT(6)
+#define CS75XX_I2C_CTRL_START BIT(7)
+
+#define CS75XX_I2C_ACK_AL BIT(0)
+#define CS75XX_I2C_ACK_BUSY BIT(1)
+#define CS75XX_I2C_ACK_NACK BIT(2)
+
+#define CS75XX_I2C_INT_DONE BIT(0)
+
+enum cs75xx_i2c_state {
+ CS75XX_I2C_IDLE,
+ CS75XX_I2C_ADDR,
+ CS75XX_I2C_WRITE,
+ CS75XX_I2C_READ,
+ CS75XX_I2C_STOP,
+};
+
+struct cs75xx_i2c {
+ struct i2c_adapter adap;
+ struct device *dev;
+ void __iomem *base;
+ int irq;
+ u32 prer;
+
+ spinlock_t lock; /* protects the transfer state */
+ struct completion done;
+ enum cs75xx_i2c_state state;
+ struct i2c_msg *msg;
+ unsigned int pos;
+ bool last;
+ int err;
+};
+
+static void cs75xx_i2c_hw_init(struct cs75xx_i2c *i2c)
+{
+ if (readl(i2c->base + CS75XX_I2C_CFG) & CS75XX_I2C_CFG_EN) {
+ writel(0, i2c->base + CS75XX_I2C_CFG);
+ usleep_range(5000, 10000);
+ }
+
+ writel(FIELD_PREP(CS75XX_I2C_CFG_PRER, i2c->prer) | CS75XX_I2C_CFG_EN,
+ i2c->base + CS75XX_I2C_CFG);
+ usleep_range(5000, 10000);
+}
+
+/*
+ * Reset the controller and enable its interrupt again, with the handler
+ * kept away from the registers meanwhile
+ */
+static void cs75xx_i2c_reset(struct cs75xx_i2c *i2c)
+{
+ disable_irq(i2c->irq);
+ cs75xx_i2c_hw_init(i2c);
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_INT);
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_IE);
+ enable_irq(i2c->irq);
+}
+
+static void cs75xx_i2c_finish(struct cs75xx_i2c *i2c, int err)
+{
+ i2c->err = err;
+ i2c->state = CS75XX_I2C_IDLE;
+ complete(&i2c->done);
+}
+
+/*
+ * End the transfer with a stop condition and complete it only once the
+ * stop is done, so that its interrupt cannot be taken for the next one
+ */
+static void cs75xx_i2c_abort(struct cs75xx_i2c *i2c, int err)
+{
+ i2c->err = err;
+ i2c->state = CS75XX_I2C_STOP;
+ writel(CS75XX_I2C_CTRL_STOP, i2c->base + CS75XX_I2C_CTRL);
+}
+
+/* Start the transfer of the next data byte of the message */
+static void cs75xx_i2c_next_byte(struct cs75xx_i2c *i2c)
+{
+ struct i2c_msg *msg = i2c->msg;
+ bool last_byte = i2c->pos == msg->len - 1;
+ u32 cmd;
+
+ if (msg->flags & I2C_M_RD) {
+ i2c->state = CS75XX_I2C_READ;
+ cmd = CS75XX_I2C_CTRL_READ;
+ /* Do not acknowledge the last byte of a read */
+ if (last_byte)
+ cmd |= CS75XX_I2C_CTRL_NACK;
+ } else {
+ i2c->state = CS75XX_I2C_WRITE;
+ writel(msg->buf[i2c->pos++], i2c->base + CS75XX_I2C_TXR);
+ cmd = CS75XX_I2C_CTRL_WRITE;
+ }
+
+ if (last_byte && i2c->last)
+ cmd |= CS75XX_I2C_CTRL_STOP;
+
+ writel(cmd, i2c->base + CS75XX_I2C_CTRL);
+}
+
+static void cs75xx_i2c_process(struct cs75xx_i2c *i2c, u32 ack)
+{
+ struct i2c_msg *msg = i2c->msg;
+
+ if (i2c->state == CS75XX_I2C_STOP) {
+ cs75xx_i2c_finish(i2c, i2c->err);
+ return;
+ }
+
+ if (ack & CS75XX_I2C_ACK_AL) {
+ cs75xx_i2c_abort(i2c, -EAGAIN);
+ return;
+ }
+
+ switch (i2c->state) {
+ case CS75XX_I2C_ADDR:
+ if (ack & CS75XX_I2C_ACK_NACK) {
+ cs75xx_i2c_abort(i2c, -ENXIO);
+ return;
+ }
+ cs75xx_i2c_next_byte(i2c);
+ break;
+
+ case CS75XX_I2C_WRITE:
+ if (ack & CS75XX_I2C_ACK_NACK) {
+ /* The last byte of the last message carried the stop */
+ if (i2c->pos == msg->len && i2c->last)
+ cs75xx_i2c_finish(i2c, -EIO);
+ else
+ cs75xx_i2c_abort(i2c, -EIO);
+ return;
+ }
+ if (i2c->pos == msg->len) {
+ cs75xx_i2c_finish(i2c, 0);
+ return;
+ }
+ cs75xx_i2c_next_byte(i2c);
+ break;
+
+ case CS75XX_I2C_READ:
+ msg->buf[i2c->pos++] = readl(i2c->base + CS75XX_I2C_RXR);
+ if (i2c->pos == msg->len) {
+ cs75xx_i2c_finish(i2c, 0);
+ return;
+ }
+ cs75xx_i2c_next_byte(i2c);
+ break;
+
+ case CS75XX_I2C_IDLE:
+ case CS75XX_I2C_STOP:
+ /* Late interrupt of a transfer that timed out */
+ break;
+ }
+}
+
+static irqreturn_t cs75xx_i2c_isr(int irq, void *dev_id)
+{
+ struct cs75xx_i2c *i2c = dev_id;
+ u32 ctrl, ack;
+
+ guard(spinlock)(&i2c->lock);
+
+ if (!(readl(i2c->base + CS75XX_I2C_INT) & CS75XX_I2C_INT_DONE))
+ return IRQ_NONE;
+
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_INT);
+
+ /* The done flag is cleared by writing it back */
+ ctrl = readl(i2c->base + CS75XX_I2C_CTRL);
+ writel(ctrl, i2c->base + CS75XX_I2C_CTRL);
+
+ ack = readl(i2c->base + CS75XX_I2C_ACK);
+ cs75xx_i2c_process(i2c, ack);
+
+ return IRQ_HANDLED;
+}
+
+static int cs75xx_i2c_xfer_msg(struct cs75xx_i2c *i2c, struct i2c_msg *msg,
+ bool last)
+{
+ unsigned long time_left;
+
+ spin_lock_irq(&i2c->lock);
+ reinit_completion(&i2c->done);
+ i2c->msg = msg;
+ i2c->pos = 0;
+ i2c->last = last;
+ i2c->err = 0;
+ i2c->state = CS75XX_I2C_ADDR;
+ writel(i2c_8bit_addr_from_msg(msg), i2c->base + CS75XX_I2C_TXR);
+ writel(CS75XX_I2C_CTRL_START | CS75XX_I2C_CTRL_WRITE,
+ i2c->base + CS75XX_I2C_CTRL);
+ spin_unlock_irq(&i2c->lock);
+
+ time_left = wait_for_completion_timeout(&i2c->done, i2c->adap.timeout);
+ if (!time_left) {
+ spin_lock_irq(&i2c->lock);
+ i2c->state = CS75XX_I2C_IDLE;
+ spin_unlock_irq(&i2c->lock);
+ dev_err(i2c->dev, "transfer timed out, resetting the controller\n");
+ cs75xx_i2c_reset(i2c);
+ return -ETIMEDOUT;
+ }
+
+ return i2c->err;
+}
+
+static int cs75xx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
+ int num)
+{
+ struct cs75xx_i2c *i2c = i2c_get_adapdata(adap);
+ u32 val;
+ int i, ret;
+
+ /* Another master may hold the bus for a whole transfer */
+ if (readl_poll_timeout(i2c->base + CS75XX_I2C_ACK, val,
+ !(val & CS75XX_I2C_ACK_BUSY), 100,
+ jiffies_to_usecs(adap->timeout))) {
+ cs75xx_i2c_reset(i2c);
+ if (readl(i2c->base + CS75XX_I2C_ACK) & CS75XX_I2C_ACK_BUSY)
+ return -EBUSY;
+ }
+
+ for (i = 0; i < num; i++) {
+ ret = cs75xx_i2c_xfer_msg(i2c, &msgs[i], i == num - 1);
+ if (ret)
+ return ret;
+ }
+
+ return num;
+}
+
+static u32 cs75xx_i2c_func(struct i2c_adapter *adap)
+{
+ return I2C_FUNC_I2C | (I2C_FUNC_SMBUS_EMUL & ~I2C_FUNC_SMBUS_QUICK);
+}
+
+static const struct i2c_algorithm cs75xx_i2c_algo = {
+ .xfer = cs75xx_i2c_xfer,
+ .functionality = cs75xx_i2c_func,
+};
+
+/* A message always carries at least one data byte after the address */
+static const struct i2c_adapter_quirks cs75xx_i2c_quirks = {
+ .flags = I2C_AQ_NO_ZERO_LEN,
+};
+
+static void cs75xx_i2c_disable(void *data)
+{
+ struct cs75xx_i2c *i2c = data;
+
+ writel(0, i2c->base + CS75XX_I2C_IE);
+ writel(0, i2c->base + CS75XX_I2C_CFG);
+}
+
+static int cs75xx_i2c_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct cs75xx_i2c *i2c;
+ unsigned long ref_rate;
+ struct clk *clk;
+ u32 bus_rate;
+ u64 div;
+ int ret;
+
+ i2c = devm_kzalloc(dev, sizeof(*i2c), GFP_KERNEL);
+ if (!i2c)
+ return -ENOMEM;
+
+ i2c->dev = dev;
+ spin_lock_init(&i2c->lock);
+ init_completion(&i2c->done);
+
+ i2c->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(i2c->base))
+ return PTR_ERR(i2c->base);
+
+ i2c->irq = platform_get_irq(pdev, 0);
+ if (i2c->irq < 0)
+ return i2c->irq;
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk), "failed to get clock\n");
+
+ ref_rate = clk_get_rate(clk);
+ if (device_property_read_u32(dev, "clock-frequency", &bus_rate))
+ bus_rate = I2C_MAX_STANDARD_MODE_FREQ;
+ if (!bus_rate || bus_rate > I2C_MAX_FAST_MODE_FREQ)
+ return dev_err_probe(dev, -EINVAL,
+ "unsupported bus frequency %u Hz\n",
+ bus_rate);
+
+ div = DIV_ROUND_UP_ULL(ref_rate, 5ULL * bus_rate);
+ if (!div || div - 1 > FIELD_MAX(CS75XX_I2C_CFG_PRER))
+ return dev_err_probe(dev, -EINVAL,
+ "cannot derive %u Hz from %lu Hz\n",
+ bus_rate, ref_rate);
+ i2c->prer = div - 1;
+
+ cs75xx_i2c_hw_init(i2c);
+
+ writel(0, i2c->base + CS75XX_I2C_IE);
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_INT);
+
+ ret = devm_add_action_or_reset(dev, cs75xx_i2c_disable, i2c);
+ if (ret)
+ return ret;
+
+ ret = devm_request_irq(dev, i2c->irq, cs75xx_i2c_isr, 0, dev_name(dev),
+ i2c);
+ if (ret)
+ return ret;
+
+ writel(CS75XX_I2C_INT_DONE, i2c->base + CS75XX_I2C_IE);
+
+ i2c->adap.owner = THIS_MODULE;
+ i2c->adap.algo = &cs75xx_i2c_algo;
+ i2c->adap.quirks = &cs75xx_i2c_quirks;
+ i2c->adap.retries = 3;
+ i2c->adap.dev.parent = dev;
+ i2c->adap.dev.of_node = dev->of_node;
+ strscpy(i2c->adap.name, dev_name(dev), sizeof(i2c->adap.name));
+ i2c_set_adapdata(&i2c->adap, i2c);
+
+ ret = devm_i2c_add_adapter(dev, &i2c->adap);
+ if (ret)
+ return ret;
+
+ dev_dbg(dev, "bus %u Hz, reference %lu Hz\n", bus_rate,
+ ref_rate);
+
+ return 0;
+}
+
+static const struct of_device_id cs75xx_i2c_of_match[] = {
+ { .compatible = "cortina,cs7542-i2c" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, cs75xx_i2c_of_match);
+
+static struct platform_driver cs75xx_i2c_driver = {
+ .probe = cs75xx_i2c_probe,
+ .driver = {
+ .name = "cs75xx-i2c",
+ .of_match_table = cs75xx_i2c_of_match,
+ },
+};
+module_platform_driver(cs75xx_i2c_driver);
+
+MODULE_DESCRIPTION("Cortina Systems CS75xx I2C controller driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 07/11] watchdog: Add ARM TWD (MPCore private) watchdog driver
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
ARM11 MPCore, Cortex-A5 and Cortex-A9 MPCore systems have a watchdog
next to the local timer of every CPU, banked at the same address. The
binding "arm,cortex-a9-twd-wdt" exists, but there has been no driver
since mpcore_wdt was removed in v3.11, among other reasons because it
only ever programmed the CPU that ran the ioctl.
Program, ping and stop the watchdogs of all online CPUs together with
on_each_cpu(), and use a CPU hotplug state to arm the watchdog of a CPU
coming online while the device is active and to stop it on a CPU going
offline, which could not ping it any more. The watchdog is used in reset
mode only, its interrupt is not needed.
Bootloaders such as the one of the Securifi Almond+ start the watchdog
before booting the kernel. A watchdog found running on any CPU is marked
WDOG_HW_RUNNING and taken over with the driver's timeout without being
stopped, unless it runs with a different prescaler: the prescaler cannot
be changed in watchdog mode. Once the watchdog mode is set it can only
be left through the disable register sequence, which is what stop does.
The watchdog is also registered as a restart handler: the CPU that
restarts the system, the only one left running, lets its own watchdog
expire right away.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
MAINTAINERS | 6 +
drivers/watchdog/Kconfig | 15 ++
drivers/watchdog/Makefile | 1 +
drivers/watchdog/arm_twd_wdt.c | 472 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 494 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 176046436..53f6fa1c5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2508,6 +2508,12 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/soc/soc.git
F: arch/arm/mach-*/
F: arch/arm/plat-*/
+ARM TWD WATCHDOG DRIVER
+M: Fil Dunsky <filipp.dunsky@gmail.com>
+L: linux-watchdog@vger.kernel.org
+S: Maintained
+F: drivers/watchdog/arm_twd_wdt.c
+
ARM/ACTIONS SEMI ARCHITECTURE
M: Andreas Färber <afaerber@suse.de>
M: Manivannan Sadhasivam <mani@kernel.org>
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index f7d0a0c2c..907e861ec 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -465,6 +465,21 @@ config ARM_SP805_WATCHDOG
ARM Primecell SP805 Watchdog timer. This will reboot your system when
the timeout is reached.
+config ARM_TWD_WATCHDOG
+ tristate "ARM TWD (MPCore private) Watchdog"
+ depends on HAVE_ARM_TWD || COMPILE_TEST
+ depends on HAS_IOMEM
+ select WATCHDOG_CORE
+ help
+ Say Y here to include support for the private watchdog found next
+ to the local (TWD) timer of every CPU in ARM11 MPCore, Cortex-A5
+ and Cortex-A9 MPCore systems. The watchdogs of all online CPUs are
+ driven together. A watchdog left running by the bootloader is
+ taken over.
+
+ To compile this driver as a module, choose M here: the
+ module will be called arm_twd_wdt.
+
config ARM_SBSA_WATCHDOG
tristate "ARM SBSA Generic Watchdog"
depends on ARM64
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index 9ac406677..4ff0bced5 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -40,6 +40,7 @@ obj-$(CONFIG_USBPCWATCHDOG) += pcwd_usb.o
# ARM Architecture
obj-$(CONFIG_ARM_SP805_WATCHDOG) += sp805_wdt.o
obj-$(CONFIG_ARM_SBSA_WATCHDOG) += sbsa_gwdt.o
+obj-$(CONFIG_ARM_TWD_WATCHDOG) += arm_twd_wdt.o
obj-$(CONFIG_ARMADA_37XX_WATCHDOG) += armada_37xx_wdt.o
obj-$(CONFIG_AIROHA_WATCHDOG) += airoha_wdt.o
obj-$(CONFIG_ASM9260_WATCHDOG) += asm9260_wdt.o
diff --git a/drivers/watchdog/arm_twd_wdt.c b/drivers/watchdog/arm_twd_wdt.c
new file mode 100644
index 000000000..18875ae24
--- /dev/null
+++ b/drivers/watchdog/arm_twd_wdt.c
@@ -0,0 +1,472 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Watchdog driver for the ARM Cortex-A9/A5 and ARM11 MPCore private
+ * (TWD) watchdog
+ *
+ * Every CPU has its own watchdog, banked at the same address, and any of
+ * them resets the system when it expires. The watchdog device therefore
+ * programs, pings and stops the watchdog of every online CPU, and a CPU
+ * that is brought online or taken offline has its watchdog set up or
+ * stopped on the way. Once the watchdog mode bit is set it can only be
+ * cleared by writing the disable sequence to the disable register.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/cpu.h>
+#include <linux/cpuhotplug.h>
+#include <linux/cpumask.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/smp.h>
+#include <linux/watchdog.h>
+
+#define TWD_WDT_LOAD 0x00
+#define TWD_WDT_COUNTER 0x04
+#define TWD_WDT_CONTROL 0x08
+#define TWD_WDT_RESET_STATUS 0x10
+#define TWD_WDT_DISABLE 0x14
+
+#define TWD_WDT_CTRL_ENABLE BIT(0)
+#define TWD_WDT_CTRL_WD_MODE BIT(3)
+#define TWD_WDT_CTRL_PRESCALER GENMASK(15, 8)
+
+#define TWD_WDT_RESET_FLAG BIT(0)
+
+#define TWD_WDT_DISABLE_KEY1 0x12345678
+#define TWD_WDT_DISABLE_KEY2 0x87654321
+
+/* Largest prescaler, the counter clock is the input clock divided by 256 */
+#define TWD_WDT_PRESCALE 255
+#define TWD_WDT_DEFAULT_TIMEOUT 30
+
+static bool nowayout = WATCHDOG_NOWAYOUT;
+module_param(nowayout, bool, 0);
+MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
+ __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
+
+static unsigned int timeout;
+module_param(timeout, uint, 0);
+MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds");
+
+static enum cpuhp_state twd_wdt_hp_state;
+
+struct twd_wdt {
+ struct watchdog_device wdd;
+ struct hlist_node node;
+ void __iomem *base;
+ struct notifier_block clk_nb;
+ /* Changed under cpus_read_lock() once the hotplug instance exists */
+ unsigned long rate;
+ bool armed;
+ bool resume_armed;
+ cpumask_t running;
+ cpumask_t reset;
+ u32 counter_min;
+};
+
+static u32 twd_wdt_ticks(struct twd_wdt *wdt, unsigned int secs)
+{
+ u64 ticks = div_u64((u64)wdt->rate * secs, TWD_WDT_PRESCALE + 1);
+
+ return min_t(u64, ticks, U32_MAX);
+}
+
+/* The following run on the CPU whose watchdog they access */
+static void twd_wdt_cpu_stop(void *data)
+{
+ struct twd_wdt *wdt = data;
+
+ writel(TWD_WDT_DISABLE_KEY1, wdt->base + TWD_WDT_DISABLE);
+ writel(TWD_WDT_DISABLE_KEY2, wdt->base + TWD_WDT_DISABLE);
+ writel(0, wdt->base + TWD_WDT_CONTROL);
+}
+
+static void twd_wdt_cpu_start(void *data)
+{
+ struct twd_wdt *wdt = data;
+ u32 ctrl = readl(wdt->base + TWD_WDT_CONTROL);
+
+ /*
+ * In watchdog mode the prescaler cannot be changed, so a watchdog
+ * started by the bootloader with a different one is stopped first.
+ */
+ if ((ctrl & TWD_WDT_CTRL_WD_MODE) &&
+ FIELD_GET(TWD_WDT_CTRL_PRESCALER, ctrl) != TWD_WDT_PRESCALE)
+ twd_wdt_cpu_stop(wdt);
+
+ /*
+ * Load the counter before enabling, it may still be zero. A watchdog
+ * that is already running is reloaded and keeps running, the mode bit
+ * cannot be cleared by writing the control register.
+ */
+ writel(twd_wdt_ticks(wdt, wdt->wdd.timeout), wdt->base + TWD_WDT_LOAD);
+ writel(FIELD_PREP(TWD_WDT_CTRL_PRESCALER, TWD_WDT_PRESCALE) |
+ TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE,
+ wdt->base + TWD_WDT_CONTROL);
+}
+
+static void twd_wdt_cpu_ping(void *data)
+{
+ struct twd_wdt *wdt = data;
+
+ /* In watchdog mode a write to the load register reloads the counter */
+ writel(twd_wdt_ticks(wdt, wdt->wdd.timeout), wdt->base + TWD_WDT_LOAD);
+}
+
+static void twd_wdt_cpu_counter(void *data)
+{
+ struct twd_wdt *wdt = data;
+ u32 val = readl(wdt->base + TWD_WDT_COUNTER);
+ u32 old = READ_ONCE(wdt->counter_min);
+
+ /* Called on all CPUs at once, keep the smallest value */
+ while (val < old) {
+ u32 prev = cmpxchg(&wdt->counter_min, old, val);
+
+ if (prev == old)
+ break;
+ old = prev;
+ }
+}
+
+static void twd_wdt_cpu_probe(void *data)
+{
+ struct twd_wdt *wdt = data;
+ unsigned int cpu = smp_processor_id();
+ u32 ctrl = readl(wdt->base + TWD_WDT_CONTROL);
+
+ if ((ctrl & (TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE)) ==
+ (TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE))
+ cpumask_set_cpu(cpu, &wdt->running);
+
+ if (readl(wdt->base + TWD_WDT_RESET_STATUS) & TWD_WDT_RESET_FLAG) {
+ cpumask_set_cpu(cpu, &wdt->reset);
+ /* The flag is cleared by writing one */
+ writel(TWD_WDT_RESET_FLAG, wdt->base + TWD_WDT_RESET_STATUS);
+ }
+}
+
+static void twd_wdt_on_each_cpu(struct twd_wdt *wdt, smp_call_func_t func)
+{
+ cpus_read_lock();
+ on_each_cpu(func, wdt, 1);
+ cpus_read_unlock();
+}
+
+static int twd_wdt_start(struct watchdog_device *wdd)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+
+ cpus_read_lock();
+ on_each_cpu(twd_wdt_cpu_start, wdt, 1);
+ wdt->armed = true;
+ cpus_read_unlock();
+
+ return 0;
+}
+
+static int twd_wdt_stop(struct watchdog_device *wdd)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+
+ cpus_read_lock();
+ wdt->armed = false;
+ on_each_cpu(twd_wdt_cpu_stop, wdt, 1);
+ cpus_read_unlock();
+
+ return 0;
+}
+
+static int twd_wdt_ping(struct watchdog_device *wdd)
+{
+ twd_wdt_on_each_cpu(watchdog_get_drvdata(wdd), twd_wdt_cpu_ping);
+
+ return 0;
+}
+
+static int twd_wdt_set_timeout(struct watchdog_device *wdd, unsigned int t)
+{
+ wdd->timeout = t;
+ if (watchdog_active(wdd) || watchdog_hw_running(wdd))
+ twd_wdt_ping(wdd);
+
+ return 0;
+}
+
+static unsigned int twd_wdt_get_timeleft(struct watchdog_device *wdd)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+ u64 ticks;
+
+ cpus_read_lock();
+ wdt->counter_min = U32_MAX;
+ on_each_cpu(twd_wdt_cpu_counter, wdt, 1);
+ ticks = (u64)wdt->counter_min * (TWD_WDT_PRESCALE + 1);
+ cpus_read_unlock();
+
+ return div_u64(ticks, wdt->rate);
+}
+
+/*
+ * The TWD runs from the peripheral clock, which follows the CPU clock. While
+ * the rate goes up, count with the new rate already: the watchdog then just
+ * fires later than asked until the change is done.
+ */
+static int twd_wdt_clk_notify(struct notifier_block *nb, unsigned long event,
+ void *data)
+{
+ struct twd_wdt *wdt = container_of(nb, struct twd_wdt, clk_nb);
+ struct clk_notifier_data *cnd = data;
+ unsigned long rate;
+
+ switch (event) {
+ case PRE_RATE_CHANGE:
+ if (cnd->new_rate <= cnd->old_rate)
+ return NOTIFY_OK;
+ rate = cnd->new_rate;
+ break;
+ case POST_RATE_CHANGE:
+ rate = cnd->new_rate;
+ break;
+ case ABORT_RATE_CHANGE:
+ rate = cnd->old_rate;
+ break;
+ default:
+ return NOTIFY_DONE;
+ }
+
+ if (!rate)
+ return NOTIFY_OK;
+
+ cpus_read_lock();
+ wdt->rate = rate;
+ if (wdt->armed)
+ on_each_cpu(twd_wdt_cpu_ping, wdt, 1);
+ cpus_read_unlock();
+
+ return NOTIFY_OK;
+}
+
+static int twd_wdt_restart(struct watchdog_device *wdd, unsigned long action,
+ void *data)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+
+ /* Only this CPU is still running, let its watchdog expire right away */
+ writel(1, wdt->base + TWD_WDT_LOAD);
+ writel(TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE,
+ wdt->base + TWD_WDT_CONTROL);
+ mdelay(500);
+
+ return 0;
+}
+
+static const struct watchdog_ops twd_wdt_ops = {
+ .owner = THIS_MODULE,
+ .start = twd_wdt_start,
+ .stop = twd_wdt_stop,
+ .ping = twd_wdt_ping,
+ .set_timeout = twd_wdt_set_timeout,
+ .get_timeleft = twd_wdt_get_timeleft,
+ .restart = twd_wdt_restart,
+};
+
+static const struct watchdog_info twd_wdt_info = {
+ .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
+ WDIOF_MAGICCLOSE | WDIOF_CARDRESET,
+ .identity = "ARM TWD watchdog",
+};
+
+static int twd_wdt_cpu_online(unsigned int cpu, struct hlist_node *node)
+{
+ struct twd_wdt *wdt = hlist_entry(node, struct twd_wdt, node);
+
+ /*
+ * Also called for every online CPU at probe time. A watchdog that is
+ * running while the device is not active would not be pinged.
+ */
+ if (wdt->armed)
+ twd_wdt_cpu_start(wdt);
+ else
+ twd_wdt_cpu_stop(wdt);
+
+ return 0;
+}
+
+static int twd_wdt_cpu_offline(unsigned int cpu, struct hlist_node *node)
+{
+ struct twd_wdt *wdt = hlist_entry(node, struct twd_wdt, node);
+
+ /* Nobody pings the watchdog of an offline CPU */
+ twd_wdt_cpu_stop(wdt);
+
+ return 0;
+}
+
+static void twd_wdt_remove_instance(void *data)
+{
+ struct twd_wdt *wdt = data;
+
+ cpuhp_state_remove_instance_nocalls(twd_wdt_hp_state, &wdt->node);
+}
+
+static int twd_wdt_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct twd_wdt *wdt;
+ struct clk *clk;
+ int ret;
+
+ wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL);
+ if (!wdt)
+ return -ENOMEM;
+
+ wdt->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(wdt->base))
+ return PTR_ERR(wdt->base);
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk), "failed to get clock\n");
+
+ wdt->rate = clk_get_rate(clk);
+ if (!wdt->rate)
+ return dev_err_probe(dev, -EINVAL, "clock rate is zero\n");
+
+ wdt->clk_nb.notifier_call = twd_wdt_clk_notify;
+ ret = devm_clk_notifier_register(dev, clk, &wdt->clk_nb);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to register clock notifier\n");
+ platform_set_drvdata(pdev, wdt);
+
+ wdt->wdd.info = &twd_wdt_info;
+ wdt->wdd.ops = &twd_wdt_ops;
+ wdt->wdd.parent = dev;
+ wdt->wdd.min_timeout = 1;
+ wdt->wdd.max_timeout = div_u64((u64)U32_MAX * (TWD_WDT_PRESCALE + 1),
+ wdt->rate);
+ wdt->wdd.timeout = min_t(unsigned int, TWD_WDT_DEFAULT_TIMEOUT,
+ wdt->wdd.max_timeout);
+ watchdog_init_timeout(&wdt->wdd, timeout, dev);
+ watchdog_set_nowayout(&wdt->wdd, nowayout);
+ watchdog_set_restart_priority(&wdt->wdd, 128);
+ watchdog_stop_on_reboot(&wdt->wdd);
+ watchdog_stop_on_unregister(&wdt->wdd);
+ watchdog_set_drvdata(&wdt->wdd, wdt);
+
+ twd_wdt_on_each_cpu(wdt, twd_wdt_cpu_probe);
+ if (!cpumask_empty(&wdt->running)) {
+ /*
+ * Left running by the bootloader: the online callback takes it
+ * over with our settings on every CPU without stopping it.
+ */
+ wdt->armed = true;
+ set_bit(WDOG_HW_RUNNING, &wdt->wdd.status);
+ }
+
+ ret = cpuhp_state_add_instance(twd_wdt_hp_state, &wdt->node);
+ if (ret)
+ return ret;
+
+ ret = devm_add_action_or_reset(dev, twd_wdt_remove_instance, wdt);
+ if (ret)
+ return ret;
+
+ if (!cpumask_empty(&wdt->reset)) {
+ wdt->wdd.bootstatus = WDIOF_CARDRESET;
+ dev_info(dev, "last reset caused by the watchdog of CPU%*pbl\n",
+ cpumask_pr_args(&wdt->reset));
+ }
+
+ if (watchdog_hw_running(&wdt->wdd))
+ dev_info(dev, "running on CPU%*pbl, taking over\n",
+ cpumask_pr_args(&wdt->running));
+
+ ret = devm_watchdog_register_device(dev, &wdt->wdd);
+ if (ret)
+ return ret;
+
+ dev_info(dev, "timeout %u s (max %u s), clock %lu Hz\n",
+ wdt->wdd.timeout, wdt->wdd.max_timeout, wdt->rate);
+
+ return 0;
+}
+
+static const struct of_device_id twd_wdt_of_match[] = {
+ { .compatible = "arm,arm11mp-twd-wdt" },
+ { .compatible = "arm,cortex-a5-twd-wdt" },
+ { .compatible = "arm,cortex-a9-twd-wdt" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, twd_wdt_of_match);
+
+/*
+ * The watchdogs lose their state in suspend. The secondary CPUs get theirs
+ * back through the hotplug state, the boot CPU through resume.
+ */
+static int twd_wdt_suspend(struct device *dev)
+{
+ struct twd_wdt *wdt = dev_get_drvdata(dev);
+
+ wdt->resume_armed = wdt->armed;
+ if (wdt->resume_armed)
+ twd_wdt_stop(&wdt->wdd);
+
+ return 0;
+}
+
+static int twd_wdt_resume(struct device *dev)
+{
+ struct twd_wdt *wdt = dev_get_drvdata(dev);
+
+ if (wdt->resume_armed)
+ twd_wdt_start(&wdt->wdd);
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(twd_wdt_pm_ops, twd_wdt_suspend, twd_wdt_resume);
+
+static struct platform_driver twd_wdt_driver = {
+ .probe = twd_wdt_probe,
+ .driver = {
+ .name = "arm-twd-wdt",
+ .of_match_table = twd_wdt_of_match,
+ .pm = pm_sleep_ptr(&twd_wdt_pm_ops),
+ },
+};
+
+static int __init twd_wdt_init(void)
+{
+ int ret;
+
+ ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, "watchdog/arm_twd:online",
+ twd_wdt_cpu_online, twd_wdt_cpu_offline);
+ if (ret < 0)
+ return ret;
+ twd_wdt_hp_state = ret;
+
+ ret = platform_driver_register(&twd_wdt_driver);
+ if (ret)
+ cpuhp_remove_multi_state(twd_wdt_hp_state);
+
+ return ret;
+}
+module_init(twd_wdt_init);
+
+static void __exit twd_wdt_exit(void)
+{
+ platform_driver_unregister(&twd_wdt_driver);
+ cpuhp_remove_multi_state(twd_wdt_hp_state);
+}
+module_exit(twd_wdt_exit);
+
+MODULE_DESCRIPTION("ARM TWD (MPCore private) watchdog driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 07/11] watchdog: Add ARM TWD (MPCore private) watchdog driver
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
ARM11 MPCore, Cortex-A5 and Cortex-A9 MPCore systems have a watchdog
next to the local timer of every CPU, banked at the same address. The
binding "arm,cortex-a9-twd-wdt" exists, but there has been no driver
since mpcore_wdt was removed in v3.11, among other reasons because it
only ever programmed the CPU that ran the ioctl.
Program, ping and stop the watchdogs of all online CPUs together with
on_each_cpu(), and use a CPU hotplug state to arm the watchdog of a CPU
coming online while the device is active and to stop it on a CPU going
offline, which could not ping it any more. The watchdog is used in reset
mode only, its interrupt is not needed.
Bootloaders such as the one of the Securifi Almond+ start the watchdog
before booting the kernel. A watchdog found running on any CPU is marked
WDOG_HW_RUNNING and taken over with the driver's timeout without being
stopped, unless it runs with a different prescaler: the prescaler cannot
be changed in watchdog mode. Once the watchdog mode is set it can only
be left through the disable register sequence, which is what stop does.
The watchdog is also registered as a restart handler: the CPU that
restarts the system, the only one left running, lets its own watchdog
expire right away.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
MAINTAINERS | 6 +
drivers/watchdog/Kconfig | 15 ++
drivers/watchdog/Makefile | 1 +
drivers/watchdog/arm_twd_wdt.c | 472 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 494 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 176046436..53f6fa1c5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2508,6 +2508,12 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/soc/soc.git
F: arch/arm/mach-*/
F: arch/arm/plat-*/
+ARM TWD WATCHDOG DRIVER
+M: Fil Dunsky <filipp.dunsky@gmail.com>
+L: linux-watchdog@vger.kernel.org
+S: Maintained
+F: drivers/watchdog/arm_twd_wdt.c
+
ARM/ACTIONS SEMI ARCHITECTURE
M: Andreas Färber <afaerber@suse.de>
M: Manivannan Sadhasivam <mani@kernel.org>
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index f7d0a0c2c..907e861ec 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -465,6 +465,21 @@ config ARM_SP805_WATCHDOG
ARM Primecell SP805 Watchdog timer. This will reboot your system when
the timeout is reached.
+config ARM_TWD_WATCHDOG
+ tristate "ARM TWD (MPCore private) Watchdog"
+ depends on HAVE_ARM_TWD || COMPILE_TEST
+ depends on HAS_IOMEM
+ select WATCHDOG_CORE
+ help
+ Say Y here to include support for the private watchdog found next
+ to the local (TWD) timer of every CPU in ARM11 MPCore, Cortex-A5
+ and Cortex-A9 MPCore systems. The watchdogs of all online CPUs are
+ driven together. A watchdog left running by the bootloader is
+ taken over.
+
+ To compile this driver as a module, choose M here: the
+ module will be called arm_twd_wdt.
+
config ARM_SBSA_WATCHDOG
tristate "ARM SBSA Generic Watchdog"
depends on ARM64
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index 9ac406677..4ff0bced5 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -40,6 +40,7 @@ obj-$(CONFIG_USBPCWATCHDOG) += pcwd_usb.o
# ARM Architecture
obj-$(CONFIG_ARM_SP805_WATCHDOG) += sp805_wdt.o
obj-$(CONFIG_ARM_SBSA_WATCHDOG) += sbsa_gwdt.o
+obj-$(CONFIG_ARM_TWD_WATCHDOG) += arm_twd_wdt.o
obj-$(CONFIG_ARMADA_37XX_WATCHDOG) += armada_37xx_wdt.o
obj-$(CONFIG_AIROHA_WATCHDOG) += airoha_wdt.o
obj-$(CONFIG_ASM9260_WATCHDOG) += asm9260_wdt.o
diff --git a/drivers/watchdog/arm_twd_wdt.c b/drivers/watchdog/arm_twd_wdt.c
new file mode 100644
index 000000000..18875ae24
--- /dev/null
+++ b/drivers/watchdog/arm_twd_wdt.c
@@ -0,0 +1,472 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Watchdog driver for the ARM Cortex-A9/A5 and ARM11 MPCore private
+ * (TWD) watchdog
+ *
+ * Every CPU has its own watchdog, banked at the same address, and any of
+ * them resets the system when it expires. The watchdog device therefore
+ * programs, pings and stops the watchdog of every online CPU, and a CPU
+ * that is brought online or taken offline has its watchdog set up or
+ * stopped on the way. Once the watchdog mode bit is set it can only be
+ * cleared by writing the disable sequence to the disable register.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/cpu.h>
+#include <linux/cpuhotplug.h>
+#include <linux/cpumask.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/smp.h>
+#include <linux/watchdog.h>
+
+#define TWD_WDT_LOAD 0x00
+#define TWD_WDT_COUNTER 0x04
+#define TWD_WDT_CONTROL 0x08
+#define TWD_WDT_RESET_STATUS 0x10
+#define TWD_WDT_DISABLE 0x14
+
+#define TWD_WDT_CTRL_ENABLE BIT(0)
+#define TWD_WDT_CTRL_WD_MODE BIT(3)
+#define TWD_WDT_CTRL_PRESCALER GENMASK(15, 8)
+
+#define TWD_WDT_RESET_FLAG BIT(0)
+
+#define TWD_WDT_DISABLE_KEY1 0x12345678
+#define TWD_WDT_DISABLE_KEY2 0x87654321
+
+/* Largest prescaler, the counter clock is the input clock divided by 256 */
+#define TWD_WDT_PRESCALE 255
+#define TWD_WDT_DEFAULT_TIMEOUT 30
+
+static bool nowayout = WATCHDOG_NOWAYOUT;
+module_param(nowayout, bool, 0);
+MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
+ __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
+
+static unsigned int timeout;
+module_param(timeout, uint, 0);
+MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds");
+
+static enum cpuhp_state twd_wdt_hp_state;
+
+struct twd_wdt {
+ struct watchdog_device wdd;
+ struct hlist_node node;
+ void __iomem *base;
+ struct notifier_block clk_nb;
+ /* Changed under cpus_read_lock() once the hotplug instance exists */
+ unsigned long rate;
+ bool armed;
+ bool resume_armed;
+ cpumask_t running;
+ cpumask_t reset;
+ u32 counter_min;
+};
+
+static u32 twd_wdt_ticks(struct twd_wdt *wdt, unsigned int secs)
+{
+ u64 ticks = div_u64((u64)wdt->rate * secs, TWD_WDT_PRESCALE + 1);
+
+ return min_t(u64, ticks, U32_MAX);
+}
+
+/* The following run on the CPU whose watchdog they access */
+static void twd_wdt_cpu_stop(void *data)
+{
+ struct twd_wdt *wdt = data;
+
+ writel(TWD_WDT_DISABLE_KEY1, wdt->base + TWD_WDT_DISABLE);
+ writel(TWD_WDT_DISABLE_KEY2, wdt->base + TWD_WDT_DISABLE);
+ writel(0, wdt->base + TWD_WDT_CONTROL);
+}
+
+static void twd_wdt_cpu_start(void *data)
+{
+ struct twd_wdt *wdt = data;
+ u32 ctrl = readl(wdt->base + TWD_WDT_CONTROL);
+
+ /*
+ * In watchdog mode the prescaler cannot be changed, so a watchdog
+ * started by the bootloader with a different one is stopped first.
+ */
+ if ((ctrl & TWD_WDT_CTRL_WD_MODE) &&
+ FIELD_GET(TWD_WDT_CTRL_PRESCALER, ctrl) != TWD_WDT_PRESCALE)
+ twd_wdt_cpu_stop(wdt);
+
+ /*
+ * Load the counter before enabling, it may still be zero. A watchdog
+ * that is already running is reloaded and keeps running, the mode bit
+ * cannot be cleared by writing the control register.
+ */
+ writel(twd_wdt_ticks(wdt, wdt->wdd.timeout), wdt->base + TWD_WDT_LOAD);
+ writel(FIELD_PREP(TWD_WDT_CTRL_PRESCALER, TWD_WDT_PRESCALE) |
+ TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE,
+ wdt->base + TWD_WDT_CONTROL);
+}
+
+static void twd_wdt_cpu_ping(void *data)
+{
+ struct twd_wdt *wdt = data;
+
+ /* In watchdog mode a write to the load register reloads the counter */
+ writel(twd_wdt_ticks(wdt, wdt->wdd.timeout), wdt->base + TWD_WDT_LOAD);
+}
+
+static void twd_wdt_cpu_counter(void *data)
+{
+ struct twd_wdt *wdt = data;
+ u32 val = readl(wdt->base + TWD_WDT_COUNTER);
+ u32 old = READ_ONCE(wdt->counter_min);
+
+ /* Called on all CPUs at once, keep the smallest value */
+ while (val < old) {
+ u32 prev = cmpxchg(&wdt->counter_min, old, val);
+
+ if (prev == old)
+ break;
+ old = prev;
+ }
+}
+
+static void twd_wdt_cpu_probe(void *data)
+{
+ struct twd_wdt *wdt = data;
+ unsigned int cpu = smp_processor_id();
+ u32 ctrl = readl(wdt->base + TWD_WDT_CONTROL);
+
+ if ((ctrl & (TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE)) ==
+ (TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE))
+ cpumask_set_cpu(cpu, &wdt->running);
+
+ if (readl(wdt->base + TWD_WDT_RESET_STATUS) & TWD_WDT_RESET_FLAG) {
+ cpumask_set_cpu(cpu, &wdt->reset);
+ /* The flag is cleared by writing one */
+ writel(TWD_WDT_RESET_FLAG, wdt->base + TWD_WDT_RESET_STATUS);
+ }
+}
+
+static void twd_wdt_on_each_cpu(struct twd_wdt *wdt, smp_call_func_t func)
+{
+ cpus_read_lock();
+ on_each_cpu(func, wdt, 1);
+ cpus_read_unlock();
+}
+
+static int twd_wdt_start(struct watchdog_device *wdd)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+
+ cpus_read_lock();
+ on_each_cpu(twd_wdt_cpu_start, wdt, 1);
+ wdt->armed = true;
+ cpus_read_unlock();
+
+ return 0;
+}
+
+static int twd_wdt_stop(struct watchdog_device *wdd)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+
+ cpus_read_lock();
+ wdt->armed = false;
+ on_each_cpu(twd_wdt_cpu_stop, wdt, 1);
+ cpus_read_unlock();
+
+ return 0;
+}
+
+static int twd_wdt_ping(struct watchdog_device *wdd)
+{
+ twd_wdt_on_each_cpu(watchdog_get_drvdata(wdd), twd_wdt_cpu_ping);
+
+ return 0;
+}
+
+static int twd_wdt_set_timeout(struct watchdog_device *wdd, unsigned int t)
+{
+ wdd->timeout = t;
+ if (watchdog_active(wdd) || watchdog_hw_running(wdd))
+ twd_wdt_ping(wdd);
+
+ return 0;
+}
+
+static unsigned int twd_wdt_get_timeleft(struct watchdog_device *wdd)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+ u64 ticks;
+
+ cpus_read_lock();
+ wdt->counter_min = U32_MAX;
+ on_each_cpu(twd_wdt_cpu_counter, wdt, 1);
+ ticks = (u64)wdt->counter_min * (TWD_WDT_PRESCALE + 1);
+ cpus_read_unlock();
+
+ return div_u64(ticks, wdt->rate);
+}
+
+/*
+ * The TWD runs from the peripheral clock, which follows the CPU clock. While
+ * the rate goes up, count with the new rate already: the watchdog then just
+ * fires later than asked until the change is done.
+ */
+static int twd_wdt_clk_notify(struct notifier_block *nb, unsigned long event,
+ void *data)
+{
+ struct twd_wdt *wdt = container_of(nb, struct twd_wdt, clk_nb);
+ struct clk_notifier_data *cnd = data;
+ unsigned long rate;
+
+ switch (event) {
+ case PRE_RATE_CHANGE:
+ if (cnd->new_rate <= cnd->old_rate)
+ return NOTIFY_OK;
+ rate = cnd->new_rate;
+ break;
+ case POST_RATE_CHANGE:
+ rate = cnd->new_rate;
+ break;
+ case ABORT_RATE_CHANGE:
+ rate = cnd->old_rate;
+ break;
+ default:
+ return NOTIFY_DONE;
+ }
+
+ if (!rate)
+ return NOTIFY_OK;
+
+ cpus_read_lock();
+ wdt->rate = rate;
+ if (wdt->armed)
+ on_each_cpu(twd_wdt_cpu_ping, wdt, 1);
+ cpus_read_unlock();
+
+ return NOTIFY_OK;
+}
+
+static int twd_wdt_restart(struct watchdog_device *wdd, unsigned long action,
+ void *data)
+{
+ struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
+
+ /* Only this CPU is still running, let its watchdog expire right away */
+ writel(1, wdt->base + TWD_WDT_LOAD);
+ writel(TWD_WDT_CTRL_WD_MODE | TWD_WDT_CTRL_ENABLE,
+ wdt->base + TWD_WDT_CONTROL);
+ mdelay(500);
+
+ return 0;
+}
+
+static const struct watchdog_ops twd_wdt_ops = {
+ .owner = THIS_MODULE,
+ .start = twd_wdt_start,
+ .stop = twd_wdt_stop,
+ .ping = twd_wdt_ping,
+ .set_timeout = twd_wdt_set_timeout,
+ .get_timeleft = twd_wdt_get_timeleft,
+ .restart = twd_wdt_restart,
+};
+
+static const struct watchdog_info twd_wdt_info = {
+ .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
+ WDIOF_MAGICCLOSE | WDIOF_CARDRESET,
+ .identity = "ARM TWD watchdog",
+};
+
+static int twd_wdt_cpu_online(unsigned int cpu, struct hlist_node *node)
+{
+ struct twd_wdt *wdt = hlist_entry(node, struct twd_wdt, node);
+
+ /*
+ * Also called for every online CPU at probe time. A watchdog that is
+ * running while the device is not active would not be pinged.
+ */
+ if (wdt->armed)
+ twd_wdt_cpu_start(wdt);
+ else
+ twd_wdt_cpu_stop(wdt);
+
+ return 0;
+}
+
+static int twd_wdt_cpu_offline(unsigned int cpu, struct hlist_node *node)
+{
+ struct twd_wdt *wdt = hlist_entry(node, struct twd_wdt, node);
+
+ /* Nobody pings the watchdog of an offline CPU */
+ twd_wdt_cpu_stop(wdt);
+
+ return 0;
+}
+
+static void twd_wdt_remove_instance(void *data)
+{
+ struct twd_wdt *wdt = data;
+
+ cpuhp_state_remove_instance_nocalls(twd_wdt_hp_state, &wdt->node);
+}
+
+static int twd_wdt_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct twd_wdt *wdt;
+ struct clk *clk;
+ int ret;
+
+ wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL);
+ if (!wdt)
+ return -ENOMEM;
+
+ wdt->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(wdt->base))
+ return PTR_ERR(wdt->base);
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk), "failed to get clock\n");
+
+ wdt->rate = clk_get_rate(clk);
+ if (!wdt->rate)
+ return dev_err_probe(dev, -EINVAL, "clock rate is zero\n");
+
+ wdt->clk_nb.notifier_call = twd_wdt_clk_notify;
+ ret = devm_clk_notifier_register(dev, clk, &wdt->clk_nb);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to register clock notifier\n");
+ platform_set_drvdata(pdev, wdt);
+
+ wdt->wdd.info = &twd_wdt_info;
+ wdt->wdd.ops = &twd_wdt_ops;
+ wdt->wdd.parent = dev;
+ wdt->wdd.min_timeout = 1;
+ wdt->wdd.max_timeout = div_u64((u64)U32_MAX * (TWD_WDT_PRESCALE + 1),
+ wdt->rate);
+ wdt->wdd.timeout = min_t(unsigned int, TWD_WDT_DEFAULT_TIMEOUT,
+ wdt->wdd.max_timeout);
+ watchdog_init_timeout(&wdt->wdd, timeout, dev);
+ watchdog_set_nowayout(&wdt->wdd, nowayout);
+ watchdog_set_restart_priority(&wdt->wdd, 128);
+ watchdog_stop_on_reboot(&wdt->wdd);
+ watchdog_stop_on_unregister(&wdt->wdd);
+ watchdog_set_drvdata(&wdt->wdd, wdt);
+
+ twd_wdt_on_each_cpu(wdt, twd_wdt_cpu_probe);
+ if (!cpumask_empty(&wdt->running)) {
+ /*
+ * Left running by the bootloader: the online callback takes it
+ * over with our settings on every CPU without stopping it.
+ */
+ wdt->armed = true;
+ set_bit(WDOG_HW_RUNNING, &wdt->wdd.status);
+ }
+
+ ret = cpuhp_state_add_instance(twd_wdt_hp_state, &wdt->node);
+ if (ret)
+ return ret;
+
+ ret = devm_add_action_or_reset(dev, twd_wdt_remove_instance, wdt);
+ if (ret)
+ return ret;
+
+ if (!cpumask_empty(&wdt->reset)) {
+ wdt->wdd.bootstatus = WDIOF_CARDRESET;
+ dev_info(dev, "last reset caused by the watchdog of CPU%*pbl\n",
+ cpumask_pr_args(&wdt->reset));
+ }
+
+ if (watchdog_hw_running(&wdt->wdd))
+ dev_info(dev, "running on CPU%*pbl, taking over\n",
+ cpumask_pr_args(&wdt->running));
+
+ ret = devm_watchdog_register_device(dev, &wdt->wdd);
+ if (ret)
+ return ret;
+
+ dev_info(dev, "timeout %u s (max %u s), clock %lu Hz\n",
+ wdt->wdd.timeout, wdt->wdd.max_timeout, wdt->rate);
+
+ return 0;
+}
+
+static const struct of_device_id twd_wdt_of_match[] = {
+ { .compatible = "arm,arm11mp-twd-wdt" },
+ { .compatible = "arm,cortex-a5-twd-wdt" },
+ { .compatible = "arm,cortex-a9-twd-wdt" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, twd_wdt_of_match);
+
+/*
+ * The watchdogs lose their state in suspend. The secondary CPUs get theirs
+ * back through the hotplug state, the boot CPU through resume.
+ */
+static int twd_wdt_suspend(struct device *dev)
+{
+ struct twd_wdt *wdt = dev_get_drvdata(dev);
+
+ wdt->resume_armed = wdt->armed;
+ if (wdt->resume_armed)
+ twd_wdt_stop(&wdt->wdd);
+
+ return 0;
+}
+
+static int twd_wdt_resume(struct device *dev)
+{
+ struct twd_wdt *wdt = dev_get_drvdata(dev);
+
+ if (wdt->resume_armed)
+ twd_wdt_start(&wdt->wdd);
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(twd_wdt_pm_ops, twd_wdt_suspend, twd_wdt_resume);
+
+static struct platform_driver twd_wdt_driver = {
+ .probe = twd_wdt_probe,
+ .driver = {
+ .name = "arm-twd-wdt",
+ .of_match_table = twd_wdt_of_match,
+ .pm = pm_sleep_ptr(&twd_wdt_pm_ops),
+ },
+};
+
+static int __init twd_wdt_init(void)
+{
+ int ret;
+
+ ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, "watchdog/arm_twd:online",
+ twd_wdt_cpu_online, twd_wdt_cpu_offline);
+ if (ret < 0)
+ return ret;
+ twd_wdt_hp_state = ret;
+
+ ret = platform_driver_register(&twd_wdt_driver);
+ if (ret)
+ cpuhp_remove_multi_state(twd_wdt_hp_state);
+
+ return ret;
+}
+module_init(twd_wdt_init);
+
+static void __exit twd_wdt_exit(void)
+{
+ platform_driver_unregister(&twd_wdt_driver);
+ cpuhp_remove_multi_state(twd_wdt_hp_state);
+}
+module_exit(twd_wdt_exit);
+
+MODULE_DESCRIPTION("ARM TWD (MPCore private) watchdog driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH v3 07/11] watchdog: Add ARM TWD (MPCore private) watchdog driver
2026-10-13 6:07 ` Fil Dunsky
(?)
@ 2026-10-13 7:03 ` sashiko-bot
-1 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-10-13 7:03 UTC (permalink / raw)
To: Fil Dunsky; +Cc: devicetree, linux-gpio, linux-watchdog, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] watchdog: acquiring cpus_read_lock() in clock notifier causes deadlock
- [High] watchdog: arm_twd_wdt: watchdog silently disabled across cpuidle deep sleep
- [High] watchdog: arm_twd_wdt: missed ping on concurrent clock rate change
--
commit 3df85baea3ae83b30f30fe157567af6f425f2a20
Author: Fil Dunsky <filipp.dunsky@gmail.com>
watchdog: Add ARM TWD (MPCore private) watchdog driver
This patch adds a watchdog driver for the ARM Cortex-A9/A5 and ARM11
MPCore private watchdog. It programs, pings and stops the watchdog on
all online CPUs and registers a hotplug state to handle CPUs coming
online and going offline.
> diff --git a/drivers/watchdog/arm_twd_wdt.c b/drivers/watchdog/arm_twd_wdt.c
> new file mode 100644
> index 0000000000000..18875ae24a243
> --- /dev/null
> +++ b/drivers/watchdog/arm_twd_wdt.c
[ ... ]
> +static void twd_wdt_cpu_ping(void *data)
> +{
> + struct twd_wdt *wdt = data;
> +
> + /* In watchdog mode a write to the load register reloads the counter */
> + writel(twd_wdt_ticks(wdt, wdt->wdd.timeout), wdt->base + TWD_WDT_LOAD);
> +}
[Severity: High]
Does this ping routine silently fail to re-enable the watchdog if the CPU
previously entered a deep cpuidle sleep state?
If cpuidle cuts power to the CPU's power domain, TWD_WDT_CONTROL might be
lost. Writing to TWD_WDT_LOAD alone without restoring TWD_WDT_CONTROL could
leave the watchdog permanently disabled upon wakeup.
[ ... ]
> +static int twd_wdt_start(struct watchdog_device *wdd)
> +{
> + struct twd_wdt *wdt = watchdog_get_drvdata(wdd);
> +
> + cpus_read_lock();
> + on_each_cpu(twd_wdt_cpu_start, wdt, 1);
> + wdt->armed = true;
> + cpus_read_unlock();
> +
> + return 0;
> +}
[Severity: High]
Could setting wdt->armed = true after calling on_each_cpu() cause a race
condition with a concurrent clock rate change?
Because this function uses a shared reader lock (cpus_read_lock()), it can
run concurrently with twd_wdt_clk_notify(). If the clock rate increases just
as the watchdog is started, twd_wdt_clk_notify() might see wdt->armed == false
and skip resynchronizing the ping, resulting in the hardware running with a
timeout calculated for the old, slower clock rate.
[ ... ]
> +static int twd_wdt_clk_notify(struct notifier_block *nb, unsigned long event,
> + void *data)
> +{
> + struct twd_wdt *wdt = container_of(nb, struct twd_wdt, clk_nb);
> + struct clk_notifier_data *cnd = data;
> + unsigned long rate;
[ ... ]
> + cpus_read_lock();
> + wdt->rate = rate;
> + if (wdt->armed)
> + on_each_cpu(twd_wdt_cpu_ping, wdt, 1);
> + cpus_read_unlock();
> +
> + return NOTIFY_OK;
> +}
[Severity: High]
Can acquiring cpus_read_lock() here cause an AB-BA deadlock with CPU
hotplug?
The clock framework's prepare_lock is already held when this notifier is
called. The CPU hotplug path acquires the global hotplug lock for writing
via cpus_write_lock() and invokes callbacks, including cpuhp_cpufreq_online().
Cpufreq drivers update CPU frequencies during hotplug by calling
clk_set_rate(), which acquires the clock framework's prepare_lock mutex,
establishing a locking order of cpu_hotplug_lock -> prepare_lock.
Acquiring cpus_read_lock() here creates an inverted lock order of
prepare_lock -> cpu_hotplug_lock.
[ ... ]
> +static int twd_wdt_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct twd_wdt *wdt;
> + struct clk *clk;
> + int ret;
[ ... ]
> + ret = cpuhp_state_add_instance(twd_wdt_hp_state, &wdt->node);
> + if (ret)
> + return ret;
[Severity: High]
Is a call to cpu_pm_register_notifier() missing here to ensure the watchdog
context is saved and restored across deep CPU idle states?
Without this, the watchdog could remain disabled if TWD hardware loses its
state during deep sleep.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261013-b4-cs75xx-s2-v3-0-181bb0d23f4c@gmail.com?part=7
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH v3 08/11] ARM: dts: cortina: cs7542: Add interrupt controller, GPIO and I2C
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
Describe the peripheral interrupt controller, the five GPIO banks and
the I2C controller of the CS7542.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
arch/arm/boot/dts/cortina/cs7542.dtsi | 75 +++++++++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/arch/arm/boot/dts/cortina/cs7542.dtsi b/arch/arm/boot/dts/cortina/cs7542.dtsi
index 29b0b7da8..c88449c54 100644
--- a/arch/arm/boot/dts/cortina/cs7542.dtsi
+++ b/arch/arm/boot/dts/cortina/cs7542.dtsi
@@ -44,6 +44,26 @@ soc {
#address-cells = <1>;
#size-cells = <1>;
+ intc: interrupt-controller@f0070004 {
+ compatible = "cortina,cs7542-intc";
+ reg = <0xf0070004 0x8>;
+ interrupts = <GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-controller;
+ #interrupt-cells = <1>;
+ };
+
+ i2c: i2c@f00700e8 {
+ compatible = "cortina,cs7542-i2c";
+ reg = <0xf00700e8 0x28>;
+ interrupt-parent = <&intc>;
+ interrupts = <11>;
+ clocks = <&apb_clk>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ status = "disabled";
+ };
+
uart0: serial@f0070110 {
compatible = "cortina,cs7542-uart";
reg = <0xf0070110 0x30>;
@@ -80,6 +100,61 @@ uart3: serial@f00701a0 {
status = "disabled";
};
+ gpio0: gpio@f00701d0 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf00701d0 0x20>, <0xf000001c 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <6>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio1: gpio@f00701f0 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf00701f0 0x20>, <0xf0000020 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <7>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio2: gpio@f0070210 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf0070210 0x20>, <0xf0000024 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <8>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio3: gpio@f0070230 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf0070230 0x20>, <0xf0000028 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <9>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio4: gpio@f0070250 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf0070250 0x20>, <0xf000002c 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <10>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
l2: cache-controller@f5010000 {
compatible = "arm,pl310-cache";
reg = <0xf5010000 0x1000>;
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 08/11] ARM: dts: cortina: cs7542: Add interrupt controller, GPIO and I2C
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
Describe the peripheral interrupt controller, the five GPIO banks and
the I2C controller of the CS7542.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
arch/arm/boot/dts/cortina/cs7542.dtsi | 75 +++++++++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/arch/arm/boot/dts/cortina/cs7542.dtsi b/arch/arm/boot/dts/cortina/cs7542.dtsi
index 29b0b7da8..c88449c54 100644
--- a/arch/arm/boot/dts/cortina/cs7542.dtsi
+++ b/arch/arm/boot/dts/cortina/cs7542.dtsi
@@ -44,6 +44,26 @@ soc {
#address-cells = <1>;
#size-cells = <1>;
+ intc: interrupt-controller@f0070004 {
+ compatible = "cortina,cs7542-intc";
+ reg = <0xf0070004 0x8>;
+ interrupts = <GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-controller;
+ #interrupt-cells = <1>;
+ };
+
+ i2c: i2c@f00700e8 {
+ compatible = "cortina,cs7542-i2c";
+ reg = <0xf00700e8 0x28>;
+ interrupt-parent = <&intc>;
+ interrupts = <11>;
+ clocks = <&apb_clk>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ status = "disabled";
+ };
+
uart0: serial@f0070110 {
compatible = "cortina,cs7542-uart";
reg = <0xf0070110 0x30>;
@@ -80,6 +100,61 @@ uart3: serial@f00701a0 {
status = "disabled";
};
+ gpio0: gpio@f00701d0 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf00701d0 0x20>, <0xf000001c 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <6>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio1: gpio@f00701f0 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf00701f0 0x20>, <0xf0000020 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <7>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio2: gpio@f0070210 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf0070210 0x20>, <0xf0000024 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <8>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio3: gpio@f0070230 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf0070230 0x20>, <0xf0000028 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <9>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
+ gpio4: gpio@f0070250 {
+ compatible = "cortina,cs7542-gpio";
+ reg = <0xf0070250 0x20>, <0xf000002c 0x4>;
+ interrupt-parent = <&intc>;
+ interrupts = <10>;
+ gpio-controller;
+ #gpio-cells = <2>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+ };
+
l2: cache-controller@f5010000 {
compatible = "arm,pl310-cache";
reg = <0xf5010000 0x1000>;
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 09/11] ARM: dts: cortina: almondplus: Add reset button and enable I2C
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
The pinhole button on the back of the Almond+ is connected to GPIO 37
(bank 1, line 5), active low. The stock firmware uses it for restart
and factory reset.
Enable the I2C bus, which has the touchscreen controller on it; the
touchscreen itself will be added together with its driver changes.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../boot/dts/cortina/cs7542-securifi-almondplus.dts | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts b/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts
index e2988c687..0619bd754 100644
--- a/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts
+++ b/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts
@@ -5,6 +5,9 @@
/dts-v1/;
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/input.h>
+
#include "cs7542.dtsi"
/ {
@@ -19,6 +22,16 @@ chosen {
stdout-path = "serial0:115200n8";
};
+ keys {
+ compatible = "gpio-keys";
+
+ button-reset {
+ label = "reset";
+ gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_RESTART>;
+ };
+ };
+
memory@0 {
device_type = "memory";
reg = <0x00000000 0x20000000>;
@@ -50,6 +63,12 @@ &apb_clk {
clock-frequency = <150000000>;
};
+&i2c {
+ clock-frequency = <400000>;
+
+ status = "okay";
+};
+
&uart0 {
status = "okay";
};
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH v3 09/11] ARM: dts: cortina: almondplus: Add reset button and enable I2C
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
The pinhole button on the back of the Almond+ is connected to GPIO 37
(bank 1, line 5), active low. The stock firmware uses it for restart
and factory reset.
Enable the I2C bus, which has the touchscreen controller on it; the
touchscreen itself will be added together with its driver changes.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
.../boot/dts/cortina/cs7542-securifi-almondplus.dts | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts b/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts
index e2988c687..0619bd754 100644
--- a/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts
+++ b/arch/arm/boot/dts/cortina/cs7542-securifi-almondplus.dts
@@ -5,6 +5,9 @@
/dts-v1/;
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/input.h>
+
#include "cs7542.dtsi"
/ {
@@ -19,6 +22,16 @@ chosen {
stdout-path = "serial0:115200n8";
};
+ keys {
+ compatible = "gpio-keys";
+
+ button-reset {
+ label = "reset";
+ gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_RESTART>;
+ };
+ };
+
memory@0 {
device_type = "memory";
reg = <0x00000000 0x20000000>;
@@ -50,6 +63,12 @@ &apb_clk {
clock-frequency = <150000000>;
};
+&i2c {
+ clock-frequency = <400000>;
+
+ status = "okay";
+};
+
&uart0 {
status = "okay";
};
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 10/11] ARM: multi_v7_defconfig: Enable Cortina Systems CS75xx GPIO, I2C and TWD watchdog
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
Enable the GPIO and I2C drivers of the Cortina Systems CS75xx SoCs and
the ARM TWD watchdog driver, which is needed on the Securifi Almond+
because its bootloader starts the watchdog.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
arch/arm/configs/multi_v7_defconfig | 3 +++
1 file changed, 3 insertions(+)
diff --git a/arch/arm/configs/multi_v7_defconfig b/arch/arm/configs/multi_v7_defconfig
index 23f144359..9a161f8bd 100644
--- a/arch/arm/configs/multi_v7_defconfig
+++ b/arch/arm/configs/multi_v7_defconfig
@@ -423,6 +423,7 @@ CONFIG_I2C_ASPEED=m
CONFIG_I2C_AT91=m
CONFIG_I2C_BCM2835=y
CONFIG_I2C_CADENCE=y
+CONFIG_I2C_CS75XX=y
CONFIG_I2C_DAVINCI=y
CONFIG_I2C_DESIGNWARE_CORE=y
CONFIG_I2C_DIGICOLOR=m
@@ -492,6 +493,7 @@ CONFIG_PINCTRL_QCOM_SSBI_PMIC=y
CONFIG_PINCTRL_RZA2=y
CONFIG_PINCTRL_RZN1=y
CONFIG_GPIO_ASPEED_SGPIO=y
+CONFIG_GPIO_CS75XX=y
CONFIG_GPIO_DAVINCI=y
CONFIG_GPIO_DWAPB=y
CONFIG_GPIO_EM=y
@@ -568,6 +570,7 @@ CONFIG_UNIPHIER_THERMAL=y
CONFIG_DA9063_WATCHDOG=m
CONFIG_XILINX_WATCHDOG=y
CONFIG_ARM_SP805_WATCHDOG=y
+CONFIG_ARM_TWD_WATCHDOG=y
CONFIG_AT91SAM9X_WATCHDOG=y
CONFIG_SAMA5D4_WATCHDOG=y
CONFIG_S3C2410_WATCHDOG=m
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 10/11] ARM: multi_v7_defconfig: Enable Cortina Systems CS75xx GPIO, I2C and TWD watchdog
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
Enable the GPIO and I2C drivers of the Cortina Systems CS75xx SoCs and
the ARM TWD watchdog driver, which is needed on the Securifi Almond+
because its bootloader starts the watchdog.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
arch/arm/configs/multi_v7_defconfig | 3 +++
1 file changed, 3 insertions(+)
diff --git a/arch/arm/configs/multi_v7_defconfig b/arch/arm/configs/multi_v7_defconfig
index 23f144359..9a161f8bd 100644
--- a/arch/arm/configs/multi_v7_defconfig
+++ b/arch/arm/configs/multi_v7_defconfig
@@ -423,6 +423,7 @@ CONFIG_I2C_ASPEED=m
CONFIG_I2C_AT91=m
CONFIG_I2C_BCM2835=y
CONFIG_I2C_CADENCE=y
+CONFIG_I2C_CS75XX=y
CONFIG_I2C_DAVINCI=y
CONFIG_I2C_DESIGNWARE_CORE=y
CONFIG_I2C_DIGICOLOR=m
@@ -492,6 +493,7 @@ CONFIG_PINCTRL_QCOM_SSBI_PMIC=y
CONFIG_PINCTRL_RZA2=y
CONFIG_PINCTRL_RZN1=y
CONFIG_GPIO_ASPEED_SGPIO=y
+CONFIG_GPIO_CS75XX=y
CONFIG_GPIO_DAVINCI=y
CONFIG_GPIO_DWAPB=y
CONFIG_GPIO_EM=y
@@ -568,6 +570,7 @@ CONFIG_UNIPHIER_THERMAL=y
CONFIG_DA9063_WATCHDOG=m
CONFIG_XILINX_WATCHDOG=y
CONFIG_ARM_SP805_WATCHDOG=y
+CONFIG_ARM_TWD_WATCHDOG=y
CONFIG_AT91SAM9X_WATCHDOG=y
CONFIG_SAMA5D4_WATCHDOG=y
CONFIG_S3C2410_WATCHDOG=m
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 11/11] MAINTAINERS: Add Cortina Systems CS75xx interrupt controller, GPIO and I2C
2026-10-13 6:07 ` Fil Dunsky
@ 2026-10-13 6:07 ` Fil Dunsky
-1 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky via B4 Relay @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
From: Fil Dunsky <filipp.dunsky@gmail.com>
List the new Cortina Systems CS75xx interrupt controller, GPIO and I2C
drivers and their bindings in the CS75xx entry.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
MAINTAINERS | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 53f6fa1c5..604993705 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2925,10 +2925,16 @@ M: Fil Dunsky <filipp.dunsky@gmail.com>
L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Maintained
F: Documentation/devicetree/bindings/arm/cortina.yaml
+F: Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml
+F: Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml
+F: Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml
F: Documentation/devicetree/bindings/serial/cortina,cs7542-uart.yaml
F: arch/arm/boot/dts/cortina/
F: arch/arm/include/debug/cs75xx.S
F: arch/arm/mach-goldengate/
+F: drivers/gpio/gpio-cs75xx.c
+F: drivers/i2c/busses/i2c-cs75xx.c
+F: drivers/irqchip/irq-cs75xx.c
F: drivers/tty/serial/cortina-uart.c
ARM/CORTINA SYSTEMS GEMINI ARM ARCHITECTURE
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* [PATCH v3 11/11] MAINTAINERS: Add Cortina Systems CS75xx interrupt controller, GPIO and I2C
@ 2026-10-13 6:07 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:07 UTC (permalink / raw)
To: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
Krzysztof Kozlowski
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog, Fil Dunsky
List the new Cortina Systems CS75xx interrupt controller, GPIO and I2C
drivers and their bindings in the CS75xx entry.
Signed-off-by: Fil Dunsky <filipp.dunsky@gmail.com>
---
MAINTAINERS | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 53f6fa1c5..604993705 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2925,10 +2925,16 @@ M: Fil Dunsky <filipp.dunsky@gmail.com>
L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
S: Maintained
F: Documentation/devicetree/bindings/arm/cortina.yaml
+F: Documentation/devicetree/bindings/gpio/cortina,cs7542-gpio.yaml
+F: Documentation/devicetree/bindings/i2c/cortina,cs7542-i2c.yaml
+F: Documentation/devicetree/bindings/interrupt-controller/cortina,cs7542-intc.yaml
F: Documentation/devicetree/bindings/serial/cortina,cs7542-uart.yaml
F: arch/arm/boot/dts/cortina/
F: arch/arm/include/debug/cs75xx.S
F: arch/arm/mach-goldengate/
+F: drivers/gpio/gpio-cs75xx.c
+F: drivers/i2c/busses/i2c-cs75xx.c
+F: drivers/irqchip/irq-cs75xx.c
F: drivers/tty/serial/cortina-uart.c
ARM/CORTINA SYSTEMS GEMINI ARM ARCHITECTURE
--
2.53.0
^ permalink raw reply related [flat|nested] 28+ messages in thread
* Re: [PATCH v3 00/11] Cortina Systems CS75xx: interrupt controller, GPIO, I2C and TWD watchdog
2026-10-13 6:07 ` Fil Dunsky
` (11 preceding siblings ...)
(?)
@ 2026-10-13 6:18 ` Krzysztof Kozlowski
2026-10-13 6:19 ` Fil Dunsky
-1 siblings, 1 reply; 28+ messages in thread
From: Krzysztof Kozlowski @ 2026-10-13 6:18 UTC (permalink / raw)
To: filipp.dunsky, Thomas Gleixner, Radu Rendec, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Linus Walleij,
Bartosz Golaszewski, Andi Shyti, Wim Van Sebroeck, Guenter Roeck,
Russell King, Arnd Bergmann
Cc: linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog
On 13/10/2026 08:07, Fil Dunsky via B4 Relay wrote:
> This is the second step of the Cortina Systems CS75xx ("Goldengate G2")
> / Securifi Almond+ support. It applies on top of the platform series
> "ARM: Add support for Cortina Systems CS75xx and Securifi Almond+"
> v6 [1] and adds:
>
> - a driver for the per-CPU private watchdog of the ARM11 MPCore,
> Cortex-A5 and Cortex-A9 ("arm,cortex-a9-twd-wdt"),
> - the peripheral interrupt controller that combines the interrupts of
> the low speed peripherals into one GIC interrupt,
> - the GPIO banks,
> - the I2C controller,
> - the device tree nodes, including the reset button of the Almond+,
> and the multi_v7_defconfig and MAINTAINERS updates.
>
Slow down. One small patchset per 24h. If you send some big work, then
once per 48h.
Implement previous review.
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 28+ messages in thread* Re: [PATCH v3 00/11] Cortina Systems CS75xx: interrupt controller, GPIO, I2C and TWD watchdog
2026-10-13 6:18 ` [PATCH v3 00/11] Cortina Systems CS75xx: interrupt controller, GPIO, I2C and TWD watchdog Krzysztof Kozlowski
@ 2026-10-13 6:19 ` Fil Dunsky
0 siblings, 0 replies; 28+ messages in thread
From: Fil Dunsky @ 2026-10-13 6:19 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: Thomas Gleixner, Radu Rendec, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Linus Walleij, Bartosz Golaszewski, Andi Shyti,
Wim Van Sebroeck, Guenter Roeck, Russell King, Arnd Bergmann,
linux-arm-kernel, linux-kernel, devicetree, linux-gpio, linux-i2c,
linux-watchdog
On 13/10/2026 08:18, Krzysztof Kozlowski wrote:
> Slow down. One small patchset per 24h. If you send some big work, then
> once per 48h.
>
> Implement previous review.
Sorry, understood. Your comments on v2 arrived after v3 had gone out;
they are done in my tree (I2C and GPIO as modules in multi_v7_defconfig)
and will be in v4, which I will not send before 48 hours have passed,
and the platform series likewise.
Thanks,
Fil
^ permalink raw reply [flat|nested] 28+ messages in thread