* [PATCH AUTOSEL 7.0-6.12] net: ethernet: mtk_eth_soc: avoid writing to ESW registers on MT7628
From: Sasha Levin @ 2026-04-20 13:20 UTC (permalink / raw)
To: patches, stable
Cc: Joris Vaisvila, Daniel Golle, Stefan Roese, Jakub Kicinski,
Sasha Levin, nbd, lorenzo, andrew+netdev, davem, edumazet, pabeni,
matthias.bgg, angelogioacchino.delregno, netdev, linux-kernel,
linux-arm-kernel, linux-mediatek
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Joris Vaisvila <joey@tinyisr.com>
[ Upstream commit 9a04d3b2f0708a9e5e1f731bafb69b040bb934a0 ]
The MT7628 has a fixed-link PHY and does not expose MAC control
registers. Writes to these registers only corrupt the ESW VLAN
configuration.
This patch explicitly registers no-op phylink_mac_ops for MT7628, as
after removing the invalid register accesses, the existing
phylink_mac_ops effectively become no-ops.
This code was introduced by commit 296c9120752b
("net: ethernet: mediatek: Add MT7628/88 SoC support")
Signed-off-by: Joris Vaisvila <joey@tinyisr.com>
Reviewed-by: Daniel Golle <daniel@makrotpia.org>
Reviewed-by: Stefan Roese <stefan.roese@mailbox.org>
Link: https://patch.msgid.link/20260226154547.68553-1-joey@tinyisr.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Now I have all the information needed for a thorough analysis. Let me
compile my findings.
---
## PHASE 1: COMMIT MESSAGE FORENSICS
**Step 1.1: Subject Line**
- Subsystem: `net: ethernet: mtk_eth_soc`
- Action verb: "avoid" — strongly implies a bug fix (preventing harmful
behavior)
- Summary: Avoid writing to ESW (Embedded Switch) registers on MT7628 to
prevent corruption
**Step 1.2: Tags**
- No `Fixes:` tag, but commit body references `296c9120752b` ("net:
ethernet: mediatek: Add MT7628/88 SoC support") from August 2019 as
the introducing commit
- `Signed-off-by: Joris Vaisvila <joey@tinyisr.com>` — author
- `Reviewed-by: Daniel Golle <daniel@makrotpia.org>` — MediaTek ethernet
maintainer/expert
- `Reviewed-by: Stefan Roese <stefan.roese@mailbox.org>` — original
author of the MT7628 support commit
- `Link:` to patch.msgid.link (standard netdev submission)
- `Signed-off-by: Jakub Kicinski <kuba@kernel.org>` — netdev maintainer
applied it
Record: Two reviewer tags from highly relevant people (original MT7628
author + subsystem expert). No syzbot. No explicit Cc: stable.
**Step 1.3: Commit Body**
- Bug: MT7628 has a fixed-link PHY and does not expose MAC control
registers. Writes to `MTK_MAC_MCR(x)` (offset 0x10100) on MT7628 hit
the ESW VLAN configuration instead of non-existent MAC control
registers.
- Symptom: VLAN configuration corruption on MT7628
- Root cause: The phylink_mac_ops callbacks (`link_down`, `link_up`,
`mac_finish`) write to `MTK_MAC_MCR` registers without checking for
MT7628
**Step 1.4: Hidden Bug Fix Detection**
This is clearly a data corruption fix. The word "avoid" means preventing
invalid register writes that corrupt VLAN config.
## PHASE 2: DIFF ANALYSIS
**Step 2.1: Inventory**
- Single file: `drivers/net/ethernet/mediatek/mtk_eth_soc.c`
- Approximate: +27 lines added, -5 lines removed
- Functions modified: `mtk_mac_config` (guard removed), `mtk_add_mac`
(ops selection added)
- Functions added: `rt5350_mac_config`, `rt5350_mac_link_down`,
`rt5350_mac_link_up` (all no-ops), `rt5350_phylink_ops` (new ops
struct)
**Step 2.2: Code Flow Change**
1. In `mtk_mac_config`: The `!MTK_HAS_CAPS(eth->soc->caps,
MTK_SOC_MT7628)` guard was removed. Safe because MT7628 now uses
entirely different (no-op) ops, so this function is never called for
MT7628.
2. In `mtk_add_mac`: Added conditional to select `rt5350_phylink_ops`
for MT7628 instead of `mtk_phylink_ops`.
3. New no-op functions: `rt5350_mac_config`, `rt5350_mac_link_down`,
`rt5350_mac_link_up` — all empty.
**Step 2.3: Bug Mechanism**
Category: **Hardware workaround / data corruption fix**
The bug: On MT7628, register offset 0x10100 is part of the ESW VLAN
configuration, not a MAC control register. The existing
`mtk_mac_link_down()`, `mtk_mac_link_up()`, and `mtk_mac_finish()` all
write to `MTK_MAC_MCR(mac->id)` (= 0x10100) without MT7628 checks. Only
`mtk_mac_config()` had a guard. Every link state change event corrupts
the VLAN configuration.
**Step 2.4: Fix Quality**
- Obviously correct: The fix prevents ALL register writes by
substituting no-op callbacks
- Minimal regression risk: Empty callbacks for a fixed-link PHY that
never needed MAC configuration
- Self-contained in one file
- Reviewed by the original MT7628 author (Stefan Roese) and MediaTek
network expert (Daniel Golle)
## PHASE 3: GIT HISTORY
**Step 3.1: Blame**
- The buggy code in `mtk_mac_link_down`/`mtk_mac_link_up` was introduced
by `b8fc9f30821ec0` (René van Dorst, 2019-08-25) during the phylink
conversion
- The `mtk_mac_config` guard was already in `b8fc9f30821ec0` but was
never added to `link_down`/`link_up`/`finish`
**Step 3.2: Original commit**
- `296c9120752b` ("Add MT7628/88 SoC support") was merged in v5.3-rc6
(August 2019)
- This commit is present in all stable trees from v5.3 onwards
(confirmed in p-5.10, p-5.15 tags)
**Step 3.3/3.4: Author & File History**
- Joris Vaisvila is not a frequent kernel contributor (only 1-2 commits
found)
- However, both reviewers are well-known in this subsystem
- File has 231 commits since 296c9120752b; 32 since v6.12
**Step 3.5: Dependencies**
- The patch is self-contained. The no-op ops pattern doesn't depend on
any other patches.
- In v6.6, the `mtk_mac_finish` function also writes to `MTK_MAC_MCR`
without MT7628 guard — same bug. The no-op ops approach fixes all
callbacks at once.
## PHASE 4: MAILING LIST
Lore/b4 dig returned results but couldn't access full discussions due to
Anubis protection. The patch was submitted as
`20260226154547.68553-1-joey@tinyisr.com` and accepted by Jakub Kicinski
(netdev maintainer).
## PHASE 5: CODE SEMANTIC ANALYSIS
**Step 5.1-5.4: Impact Surface**
- `mtk_mac_link_down` is called by phylink whenever the link goes down —
every cable disconnect, PHY negotiation change
- `mtk_mac_link_up` is called on every link up event
- `mtk_mac_finish` is called during PHY configuration
- On MT7628, these are called regularly during normal operation
- `mtk_set_mcr_max_rx` at line 3886 already has its own `MTK_SOC_MT7628`
guard, confirming the developers know these registers don't exist on
MT7628
## PHASE 6: STABLE TREE ANALYSIS
**Step 6.1:** The buggy code exists in ALL stable trees from v5.3+,
including v5.15, v6.1, v6.6, and 6.12.
- In v6.6: `mtk_mac_link_down` at line 689 unconditionally writes to
`MTK_MAC_MCR` — confirmed the same bug
- In v6.6: `mtk_mac_link_up` at line 769 also unconditionally writes to
`MTK_MAC_MCR` — confirmed
- In v6.6: `mtk_mac_finish` at line 660 also writes to `MTK_MAC_MCR` —
confirmed
**Step 6.2: Backport Difficulty**
For v7.0: Should apply cleanly or with minor fuzz.
For v6.6 and older: Will need rework. The `mtk_mac_link_down`/`link_up`
implementations differ significantly (v7.0 has xgmii handling added by
`51cf06ddafc91e`). However, the *concept* of the fix (separate no-op
ops) is portable.
## PHASE 7: SUBSYSTEM CONTEXT
- Subsystem: Network driver (embedded Ethernet), IMPORTANT criticality
for MT7628 users
- MT7628/MT7688 is a widely-used MIPS SoC found in popular embedded
platforms (Omega2, VoCore2, many OpenWrt routers)
## PHASE 8: IMPACT AND RISK ASSESSMENT
**Step 8.1: Affected Users**
- All MT7628/MT7688 users (embedded routers running Linux with VLANs)
**Step 8.2: Trigger Conditions**
- Triggered on every link state change (boot, cable plug/unplug, PHY
state change)
- Extremely common — happens during normal boot
**Step 8.3: Failure Mode**
- **ESW VLAN configuration corruption** — MEDIUM-HIGH severity
- VLAN configuration is silently corrupted, leading to incorrect network
behavior
- Not a crash but a data corruption issue affecting network
configuration
**Step 8.4: Risk-Benefit**
- Benefit: HIGH — prevents VLAN corruption on every MT7628 system
- Risk: LOW — the fix adds empty callback functions and selects them
conditionally; the no-op approach is obviously correct for a fixed-
link PHY with no MAC control registers
## PHASE 9: FINAL SYNTHESIS
**Evidence FOR backporting:**
1. Fixes real data corruption (VLAN config) on real hardware
(MT7628/MT7688)
2. Bug present since v5.3 (2019) — affects all stable trees
3. Reviewed by original MT7628 author and subsystem expert
4. Accepted by netdev maintainer (Jakub Kicinski)
5. Fix is obviously correct (no-op callbacks for hardware without MAC
registers)
6. Single file change, well-contained
7. Other code in the same file already has MT7628 guards for the same
registers (`mtk_set_mcr_max_rx` at line 3886)
**Evidence AGAINST backporting:**
1. ~30 lines of new code (not trivially small, but straightforward)
2. May need rework for older stable trees (6.6, 6.1, 5.15) due to
function refactoring
3. No explicit Cc: stable (expected for commits under review)
**Stable Rules Checklist:**
1. Obviously correct and tested? YES — reviewed by 2 experts, one the
original author
2. Fixes a real bug? YES — VLAN config corruption on MT7628
3. Important issue? YES — data corruption, affects all MT7628 users
4. Small and contained? YES — single file, ~30 lines
5. No new features? CORRECT — only prevents invalid register writes
6. Can apply to stable? YES for 7.0; needs rework for older trees
**Verification:**
- [Phase 1] Parsed tags: Reviewed-by from Daniel Golle and Stefan Roese,
Link to netdev submission
- [Phase 2] Diff analysis: Adds no-op phylink_mac_ops for MT7628,
selects them in `mtk_add_mac()`, removes now-unreachable guard in
`mtk_mac_config()`
- [Phase 3] git blame: buggy code from `b8fc9f30821ec0` (2019), phylink
conversion missing MT7628 guards in link_down/link_up
- [Phase 3] git show 296c9120752b: confirmed original MT7628 support
commit from v5.3 era (2019-08-16)
- [Phase 3] git tag --contains: original commit present in p-5.10,
p-5.15 tags (all active stable trees)
- [Phase 5] Verified `mtk_mac_link_down` writes to
`MTK_MAC_MCR(mac->id)` without MT7628 check (line 731 in current code)
- [Phase 5] Verified `mtk_mac_link_up`→`mtk_gdm_mac_link_up` writes to
`MTK_MAC_MCR(mac->id)` (line 846) without MT7628 check
- [Phase 5] Verified `mtk_mac_finish` writes to `MTK_MAC_MCR(mac->id)`
(line 709/716) without MT7628 check
- [Phase 5] Confirmed `mtk_set_mcr_max_rx` (line 3886) already guards
against MT7628, proving developers know these registers don't exist on
MT7628
- [Phase 6] Verified v6.6 stable has the same bug: `mtk_mac_link_down`
(line 689) and `mtk_mac_link_up` (line 769) unconditionally write to
`MTK_MAC_MCR`
- [Phase 6] `MTK_MAC_MCR(x)` = 0x10100 + x*0x100, confirmed in header
file (line 453)
- [Phase 8] VLAN corruption confirmed by commit message: "Writes to
these registers only corrupt the ESW VLAN configuration"
- UNVERIFIED: Could not access full lore.kernel.org discussion due to
Anubis protection; relied on tags in the commit message
**YES**
drivers/net/ethernet/mediatek/mtk_eth_soc.c | 34 ++++++++++++++++++---
1 file changed, 30 insertions(+), 4 deletions(-)
diff --git a/drivers/net/ethernet/mediatek/mtk_eth_soc.c b/drivers/net/ethernet/mediatek/mtk_eth_soc.c
index ddc321a02fdae..bb8ced22ca3be 100644
--- a/drivers/net/ethernet/mediatek/mtk_eth_soc.c
+++ b/drivers/net/ethernet/mediatek/mtk_eth_soc.c
@@ -562,9 +562,7 @@ static void mtk_mac_config(struct phylink_config *config, unsigned int mode,
int val, ge_mode, err = 0;
u32 i;
- /* MT76x8 has no hardware settings between for the MAC */
- if (!MTK_HAS_CAPS(eth->soc->caps, MTK_SOC_MT7628) &&
- mac->interface != state->interface) {
+ if (mac->interface != state->interface) {
/* Setup soc pin functions */
switch (state->interface) {
case PHY_INTERFACE_MODE_TRGMII:
@@ -956,6 +954,30 @@ static const struct phylink_mac_ops mtk_phylink_ops = {
.mac_enable_tx_lpi = mtk_mac_enable_tx_lpi,
};
+static void rt5350_mac_config(struct phylink_config *config, unsigned int mode,
+ const struct phylink_link_state *state)
+{
+}
+
+static void rt5350_mac_link_down(struct phylink_config *config, unsigned int mode,
+ phy_interface_t interface)
+{
+}
+
+static void rt5350_mac_link_up(struct phylink_config *config,
+ struct phy_device *phy,
+ unsigned int mode, phy_interface_t interface,
+ int speed, int duplex, bool tx_pause, bool rx_pause)
+{
+}
+
+/* MT76x8 (rt5350-eth) does not expose any MAC control registers */
+static const struct phylink_mac_ops rt5350_phylink_ops = {
+ .mac_config = rt5350_mac_config,
+ .mac_link_down = rt5350_mac_link_down,
+ .mac_link_up = rt5350_mac_link_up,
+};
+
static void mtk_mdio_config(struct mtk_eth *eth)
{
u32 val;
@@ -4780,6 +4802,7 @@ static const struct net_device_ops mtk_netdev_ops = {
static int mtk_add_mac(struct mtk_eth *eth, struct device_node *np)
{
+ const struct phylink_mac_ops *mac_ops = &mtk_phylink_ops;
const __be32 *_id = of_get_property(np, "reg", NULL);
phy_interface_t phy_mode;
struct phylink *phylink;
@@ -4914,9 +4937,12 @@ static int mtk_add_mac(struct mtk_eth *eth, struct device_node *np)
mac->phylink_config.supported_interfaces);
}
+ if (MTK_HAS_CAPS(eth->soc->caps, MTK_SOC_MT7628))
+ mac_ops = &rt5350_phylink_ops;
+
phylink = phylink_create(&mac->phylink_config,
of_fwnode_handle(mac->of_node),
- phy_mode, &mtk_phylink_ops);
+ phy_mode, mac_ops);
if (IS_ERR(phylink)) {
err = PTR_ERR(phylink);
goto free_netdev;
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 6.18] media: rkvdec: reduce stack usage in rkvdec_init_v4l2_vp9_count_tbl()
From: Sasha Levin @ 2026-04-20 13:20 UTC (permalink / raw)
To: patches, stable
Cc: Arnd Bergmann, Nicolas Dufresne, Mauro Carvalho Chehab,
Sasha Levin, detlev.casanova, ezequiel, mchehab, heiko,
linux-media, linux-rockchip, linux-arm-kernel, linux-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Arnd Bergmann <arnd@arndb.de>
[ Upstream commit c03b7dec3c4ddc97872fa12bfca75bae9cb46510 ]
The deeply nested loop in rkvdec_init_v4l2_vp9_count_tbl() needs a lot
of registers, so when the clang register allocator runs out, it ends up
spilling countless temporaries to the stack:
drivers/media/platform/rockchip/rkvdec/rkvdec-vp9.c:966:12: error: stack frame size (1472) exceeds limit (1280) in 'rkvdec_vp9_start' [-Werror,-Wframe-larger-than]
Marking this function as noinline_for_stack keeps it out of
rkvdec_vp9_start(), giving the compiler more room for optimization.
The resulting code is good enough that both the total stack usage
and the loop get enough better to stay under the warning limit,
though it's still slow, and would need a larger rework if this
function ends up being called in a fast path.
Signed-off-by: Arnd Bergmann <arnd@arndb.de>
Reviewed-by: Nicolas Dufresne <nicolas.dufresne@collabora.com>
Signed-off-by: Nicolas Dufresne <nicolas.dufresne@collabora.com>
Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Error: Failed to generate final synthesis
drivers/media/platform/rockchip/rkvdec/rkvdec-vp9.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/media/platform/rockchip/rkvdec/rkvdec-vp9.c b/drivers/media/platform/rockchip/rkvdec/rkvdec-vp9.c
index 0e7e16f20eeb0..bc74d2d824ef2 100644
--- a/drivers/media/platform/rockchip/rkvdec/rkvdec-vp9.c
+++ b/drivers/media/platform/rockchip/rkvdec/rkvdec-vp9.c
@@ -923,7 +923,8 @@ static void rkvdec_vp9_done(struct rkvdec_ctx *ctx,
update_ctx_last_info(vp9_ctx);
}
-static void rkvdec_init_v4l2_vp9_count_tbl(struct rkvdec_ctx *ctx)
+static noinline_for_stack void
+rkvdec_init_v4l2_vp9_count_tbl(struct rkvdec_ctx *ctx)
{
struct rkvdec_vp9_ctx *vp9_ctx = ctx->priv;
struct rkvdec_vp9_intra_frame_symbol_counts *intra_cnts = vp9_ctx->count_tbl.cpu;
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 6.18] soc: aspeed: socinfo: Mask table entries for accurate SoC ID matching
From: Sasha Levin @ 2026-04-20 13:20 UTC (permalink / raw)
To: patches, stable
Cc: Potin Lai, Andrew Jeffery, Sasha Levin, joel, linux-arm-kernel,
linux-aspeed, linux-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Potin Lai <potin.lai.pt@gmail.com>
[ Upstream commit 7ec1bd3d9be671d04325b9e06149b8813f6a4836 ]
The siliconid_to_name() function currently masks the input silicon ID
with 0xff00ffff, but compares it against unmasked table entries. This
causes matching to fail if the table entries contain non-zero values in
the bits covered by the mask (bits 16-23).
Update the logic to apply the 0xff00ffff mask to the table entries
during comparison. This ensures that only the relevant model and
revision bits are considered, providing a consistent match across
different manufacturing batches.
[arj: Add Fixes: tag, fix 'soninfo' typo, clarify function reference]
Fixes: e0218dca5787 ("soc: aspeed: Add soc info driver")
Signed-off-by: Potin Lai <potin.lai.pt@gmail.com>
Link: https://patch.msgid.link/20260122-soc_aspeed_name_fix-v1-1-33a847f2581c@gmail.com
Signed-off-by: Andrew Jeffery <andrew@codeconstruct.com.au>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Error: Failed to generate final synthesis
drivers/soc/aspeed/aspeed-socinfo.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/soc/aspeed/aspeed-socinfo.c b/drivers/soc/aspeed/aspeed-socinfo.c
index 67e9ac3d08ecc..a90b100f4d101 100644
--- a/drivers/soc/aspeed/aspeed-socinfo.c
+++ b/drivers/soc/aspeed/aspeed-socinfo.c
@@ -39,7 +39,7 @@ static const char *siliconid_to_name(u32 siliconid)
unsigned int i;
for (i = 0 ; i < ARRAY_SIZE(rev_table) ; ++i) {
- if (rev_table[i].id == id)
+ if ((rev_table[i].id & 0xff00ffff) == id)
return rev_table[i].name;
}
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 6.18] arm64: dts: imx93-9x9-qsb: change usdhc tuning step for eMMC and SD
From: Sasha Levin @ 2026-04-20 13:20 UTC (permalink / raw)
To: patches, stable
Cc: Luke Wang, Frank Li, Sasha Levin, robh, krzk+dt, conor+dt,
shawnguo, peng.fan, devicetree, imx, linux-arm-kernel,
linux-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Luke Wang <ziniu.wang_1@nxp.com>
[ Upstream commit 08903184553def7ba1ad6ba4fa8afe1ba2ee0a21 ]
During system resume, the following errors occurred:
[ 430.638625] mmc1: error -84 writing Cache Enable bit
[ 430.643618] mmc1: error -84 doing runtime resume
For eMMC and SD, there are two tuning pass windows and the gap between
those two windows may only have one cell. If tuning step > 1, the gap may
just be skipped and host assumes those two windows as a continuous
windows. This will cause a wrong delay cell near the gap to be selected.
Set the tuning step to 1 to avoid selecting the wrong delay cell.
For SDIO, the gap is sufficiently large, so the default tuning step does
not cause this issue.
Fixes: 0565d20cd8c2 ("arm64: dts: freescale: Support i.MX93 9x9 Quick Start Board")
Signed-off-by: Luke Wang <ziniu.wang_1@nxp.com>
Reviewed-by: Frank Li <Frank.Li@nxp.com>
Signed-off-by: Frank Li <Frank.Li@nxp.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Error: Failed to generate final synthesis
arch/arm64/boot/dts/freescale/imx93-9x9-qsb.dts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/arch/arm64/boot/dts/freescale/imx93-9x9-qsb.dts b/arch/arm64/boot/dts/freescale/imx93-9x9-qsb.dts
index 0852067eab2cb..197c8f8b7f669 100644
--- a/arch/arm64/boot/dts/freescale/imx93-9x9-qsb.dts
+++ b/arch/arm64/boot/dts/freescale/imx93-9x9-qsb.dts
@@ -507,6 +507,7 @@ &usdhc1 {
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
bus-width = <8>;
non-removable;
+ fsl,tuning-step = <1>;
status = "okay";
};
@@ -519,6 +520,7 @@ &usdhc2 {
vmmc-supply = <®_usdhc2_vmmc>;
bus-width = <4>;
no-mmc;
+ fsl,tuning-step = <1>;
status = "okay";
};
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 7.0-6.6] wifi: mt76: mt7996: reset device after MCU message timeout
From: Sasha Levin @ 2026-04-20 13:20 UTC (permalink / raw)
To: patches, stable
Cc: Chad Monroe, Felix Fietkau, Sasha Levin, lorenzo, ryder.lee,
matthias.bgg, angelogioacchino.delregno, linux-wireless,
linux-kernel, linux-arm-kernel, linux-mediatek
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Chad Monroe <chad@monroe.io>
[ Upstream commit d2b860454ea2df8f336e9b859da7ffb27f43444d ]
Trigger a full reset after MCU message timeout.
Signed-off-by: Chad Monroe <chad@monroe.io>
Link: https://patch.msgid.link/6e05ed063f3763ad3457633c56b60a728a49a6f0.1765203753.git.chad@monroe.io
Signed-off-by: Felix Fietkau <nbd@nbd.name>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Now I have all the information needed for the complete analysis. Let me
compile the findings.
---
## PHASE 1: COMMIT MESSAGE FORENSICS
**Step 1.1: Subject Line**
- Subsystem: `wifi: mt76: mt7996`
- Action verb: "reset" (implies recovery from a failure state)
- Summary: Trigger a device reset when MCU message timeouts occur on
mt7996
- Record: [wifi/mt76/mt7996] [reset] [Trigger full device recovery after
MCU message timeout]
**Step 1.2: Tags**
- `Signed-off-by: Chad Monroe <chad@monroe.io>` - patch author
- `Link: https://patch.msgid.link/...` - reference to lore
- `Signed-off-by: Felix Fietkau <nbd@nbd.name>` - maintainer/committer
of mt76
- No Fixes: tag (expected for manual review candidates)
- No Cc: stable (expected)
- No Reported-by tag
- Record: Author is Chad Monroe; applied by Felix Fietkau (the mt76
subsystem maintainer)
**Step 1.3: Commit Body**
- Body is very brief: "Trigger a full reset after MCU message timeout."
- No stack traces or reproduction steps given
- The mt7915 equivalent (commit 10f73bb3938f7c5) provides more context:
"MCU hangs do not trigger watchdog interrupts, so they can only be
detected through MCU message timeouts. Ensure that the hardware gets
restarted when that happens in order to prevent a permanent stuck
state."
- Record: Bug = MCU hang leaves device permanently stuck. Symptom = WiFi
device becomes non-functional, requires reboot. Root cause = MCU hang
without watchdog interrupt, only detectable via message timeout, no
recovery triggered.
**Step 1.4: Hidden Bug Fix Detection**
- "reset device after MCU message timeout" - this is clearly a fix for a
missing recovery path. Without it, a firmware hang results in a
permanent stuck state.
- Record: This IS a bug fix. The device becomes permanently stuck
without it.
---
## PHASE 2: DIFF ANALYSIS
**Step 2.1: Inventory**
- `mcu.c`: +9 lines (in `mt7996_mcu_parse_response`)
- `mac.c`: +5 lines (in `mt7996_reset`)
- Total: ~14 lines added, 0 removed
- Scope: Single-driver, surgical fix in two closely-related functions
- Record: 2 files, +14 lines, functions: mt7996_mcu_parse_response,
mt7996_reset
**Step 2.2: Code Flow Changes**
Hunk 1 (mcu.c): In `mt7996_mcu_parse_response()`, when `skb == NULL`
(MCU timeout):
- **Before**: Log error, return -ETIMEDOUT. No recovery action.
- **After**: Log error, atomically set `MT76_MCU_RESET` bit (via
`test_and_set_bit` to prevent duplicates), set `recovery.restart =
true`, wake up MCU wait queue, queue `reset_work`, wake up
`reset_wait`, then return -ETIMEDOUT.
Hunk 2 (mac.c): In `mt7996_reset()`, before the existing `queue_work`:
- **Before**: Always queue reset_work and wake reset_wait
unconditionally.
- **After**: If `MT_MCU_CMD_STOP_DMA` is set, additionally set
`MT76_MCU_RESET` bit and wake up MCU wait queue, aborting pending MCU
operations before reset.
**Step 2.3: Bug Mechanism**
- Category: Missing error recovery / permanent hardware stuck state
- The MCU can hang in a way that doesn't trigger a hardware watchdog
interrupt. The only indication is MCU message timeouts. Without this
patch, timeouts just return an error code but never trigger device
recovery. The device becomes permanently non-functional.
- Record: Missing recovery mechanism. MCU hang → timeout → error return
→ no recovery → permanent stuck state.
**Step 2.4: Fix Quality**
- Obviously correct: Mirrors the exact same pattern used in mt7915
(commit 10f73bb3938f7c5) and mt7915's STOP_DMA handling (commit
b13cd593ef2402).
- Minimal/surgical: Only adds recovery trigger code at the exact points
needed.
- `test_and_set_bit` prevents duplicate resets.
- Regression risk: Very low. The reset_work handler already handles
`recovery.restart = true` properly. The STOP_DMA path already exists
for other triggers.
- Record: High quality fix, obviously correct, mirrors established
patterns.
---
## PHASE 3: GIT HISTORY INVESTIGATION
**Step 3.1: Blame**
- `mt7996_mcu_parse_response()`: Unchanged since original driver
addition by Shayne Chen (commit 98686cd21624c7, November 2022, v6.2).
- `mt7996_reset()`: Added by Bo Jiao (commit 27015b6fbcca83, April 2023,
v6.4) as "enable full system reset support".
- The buggy code (missing recovery trigger) has been present since the
driver was first created.
- Record: Bug present since v6.2 (mcu.c) and v6.4 (mac.c had
mt7996_reset without STOP_DMA handling).
**Step 3.2: No Fixes: Tag**
- N/A - no Fixes: tag present (expected).
**Step 3.3: File History**
- The mt7996 reset infrastructure was significantly improved in v6.18
(ace5d3b6b49e8 "improve hardware restart reliability"). However, the
basic recovery mechanism has been in place since v6.4.
- The commit `beb01caa570c52` in v6.18 decreased MCU timeouts to allow
faster recovery - this patch's logic works with either timeout value.
- Record: This commit is standalone; no prerequisites needed beyond the
v6.4 reset infrastructure.
**Step 3.4: Author**
- Chad Monroe is a contributor to mt76 (5 commits found in the driver).
- Felix Fietkau (nbd@nbd.name) is THE mt76 subsystem maintainer - he
applied the patch.
- Felix also authored the identical fix for mt7915 (10f73bb3938f7c5).
- Record: Applied by subsystem maintainer. Author is a regular
contributor.
**Step 3.5: Dependencies**
- All structures/flags used already exist: `MT76_MCU_RESET`,
`recovery.restart`, `mcu.wait`, `reset_work`, `reset_wait`,
`MT_MCU_CMD_STOP_DMA`.
- No new functions or data structures introduced.
- Record: Fully self-contained, no dependencies on other uncommitted
patches.
---
## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH
**Step 4.1: Original Discussion**
- lore.kernel.org returned anti-bot protection; direct web access was
blocked.
- b4 dig of the mt7915 equivalent found it was part of a 24-patch series
(v2) by Felix Fietkau from August 2024.
- The mt7996 version is by Chad Monroe and was ported from the mt7915
fix.
- Record: Could not access lore directly due to anti-bot protection. b4
confirmed the mt7915 version was part of Felix Fietkau's cleanup
series.
**Step 4.2: Reviewer**
- Applied by Felix Fietkau, the mt76 subsystem maintainer.
- Record: Subsystem maintainer applied the patch directly.
**Step 4.3-4.5**: Blocked by lore anti-bot protection. No additional
information could be gathered.
---
## PHASE 5: CODE SEMANTIC ANALYSIS
**Step 5.1: Functions Modified**
- `mt7996_mcu_parse_response()` - MCU response parser (callback)
- `mt7996_reset()` - device reset entry point
**Step 5.2: Callers of `mt7996_mcu_parse_response`**
- Registered as `.mcu_parse_response` in `mt7996_mcu_ops` (mcu.c line
3363).
- Called from `mt76_mcu_skb_send_and_get_msg()` in `mcu.c` (core mt76
code, line 122).
- This is the universal MCU message response handler - called for EVERY
MCU command the driver issues.
- Record: Called for every MCU message. Critical, high-frequency path.
**Step 5.3: Callers of `mt7996_reset`**
- Called from interrupt context and error recovery paths.
- Used by `mt7996_irq_tasklet()` when MCU command interrupts occur.
- Record: Called from interrupt handler / tasklet context.
**Step 5.4: Call Chain**
- Any WiFi operation → MCU command → `mt76_mcu_skb_send_and_get_msg()` →
wait for response → `mt7996_mcu_parse_response()` → if timeout →
trigger reset
- This path is reachable from normal WiFi operations (scan, associate,
channel switch, etc.)
- Record: Fully reachable from normal user operations.
**Step 5.5: Similar Patterns**
- mt7915 has identical recovery logic (10f73bb3938f7c5 +
b13cd593ef2402).
- mt7921/mt7925 have similar reset mechanisms.
- Record: Well-established pattern across the mt76 driver family.
---
## PHASE 6: STABLE TREE ANALYSIS
**Step 6.1: Buggy Code in Stable Trees**
- mt7996 driver added in v6.2.
- `mt7996_reset()` added in v6.4.
- `mt7996_mcu_parse_response()` unchanged since v6.2.
- The mcu.c part of the fix applies to 6.2+. The mac.c part applies to
6.4+.
- Affected stable trees: 6.6.y, 6.12.y, and any other active LTS that
includes mt7996.
- Record: Bug exists in 6.6.y and all later stable trees.
**Step 6.2: Backport Complications**
- The code being modified is unchanged since original introduction.
- Should apply cleanly to 6.6.y.
- Record: Expected clean apply.
**Step 6.3: No Related Fixes Already in Stable**
- No similar fix found in stable trees.
- Record: No existing fix for this issue in stable.
---
## PHASE 7: SUBSYSTEM CONTEXT
**Step 7.1: Subsystem**
- WiFi driver (drivers/net/wireless/mediatek/mt76/mt7996)
- MT7996 is MediaTek's Wi-Fi 7 (802.11be) chipset - used in routers and
access points.
- Criticality: IMPORTANT - WiFi is critical infrastructure for many
users.
- Record: [WiFi driver] [IMPORTANT - affects mt7996 hardware users]
**Step 7.2: Activity**
- Very actively developed - dozens of commits in recent releases.
- Active MLO/Wi-Fi 7 development ongoing.
- Record: Highly active subsystem.
---
## PHASE 8: IMPACT AND RISK ASSESSMENT
**Step 8.1: Affected Users**
- All users of mt7996/mt7992 WiFi hardware (routers, access points, PCIe
WiFi cards).
- Record: Hardware-specific, but MT7996 is a current-generation popular
WiFi chipset.
**Step 8.2: Trigger Conditions**
- Triggers when MCU firmware hangs without issuing a watchdog interrupt.
- Can happen during normal operation (firmware bugs, hardware glitches).
- Not user-triggered in the security sense, but can happen during
routine WiFi operation.
- Record: Firmware hang during normal operation. Not predictable but
happens in practice (same fix was needed for mt7915).
**Step 8.3: Failure Mode**
- Without the fix: WiFi device becomes permanently non-functional until
reboot.
- This is a system hang from the WiFi perspective.
- Severity: HIGH (permanent loss of WiFi connectivity, requires reboot)
- Record: Permanent device stuck state. Severity: HIGH.
**Step 8.4: Risk-Benefit Ratio**
- Benefit: HIGH - prevents permanent WiFi device failure, enables
automatic recovery.
- Risk: VERY LOW - 14 lines, uses `test_and_set_bit` for safety, mirrors
proven mt7915 pattern, no changes to public APIs or data structures.
- Record: Benefit HIGH, Risk VERY LOW. Excellent ratio.
---
## PHASE 9: FINAL SYNTHESIS
**Step 9.1: Evidence Summary**
FOR backporting:
- Fixes permanent device stuck state (WiFi becomes completely non-
functional)
- Small and surgical fix (14 lines, 2 files in same driver)
- Mirrors identical fix already applied to sister chip mt7915 (proven
pattern)
- Applied by mt76 subsystem maintainer (Felix Fietkau)
- No dependencies on other patches
- Buggy code present since driver introduction (v6.2/v6.4)
- `test_and_set_bit` prevents duplicate resets (safe concurrent
behavior)
- Should apply cleanly to stable trees
AGAINST backporting:
- Brief commit message (but supplemented by identical mt7915 fix
explanation)
- No explicit stable nomination or Reported-by (but this is expected for
manual review candidates)
**Step 9.2: Stable Rules Checklist**
1. Obviously correct and tested? **YES** - mirrors proven mt7915
pattern, applied by maintainer
2. Fixes a real bug? **YES** - permanent device stuck state
3. Important issue? **YES** - device becomes permanently non-functional
4. Small and contained? **YES** - 14 lines in single driver
5. No new features or APIs? **YES** - only adds error recovery
6. Can apply to stable? **YES** - code unchanged since introduction
**Step 9.3: Exception Categories**
- Not an exception category - this is a standard bug fix.
**Step 9.4: Decision**
This is a clear bug fix that prevents a permanent device stuck state.
It's small, obviously correct, follows established patterns, and was
applied by the subsystem maintainer. The benefit-to-risk ratio is
excellent.
---
## Verification
- [Phase 1] Parsed tags: Link to patch.msgid, signed by author (Chad
Monroe) and maintainer (Felix Fietkau)
- [Phase 2] Diff analysis: +9 lines in mcu.c (reset trigger on timeout),
+5 lines in mac.c (STOP_DMA handling)
- [Phase 3] git blame: mt7996_mcu_parse_response unchanged since
98686cd21624c7 (v6.2); mt7996_reset from 27015b6fbcca83 (v6.4)
- [Phase 3] git show 10f73bb3938f7c5: confirmed identical mt7915 fix
exists with clear explanation of the permanent stuck state bug
- [Phase 3] git show b13cd593ef2402: confirmed identical mt7915 STOP_DMA
handling pattern
- [Phase 3] git log --author: Chad Monroe has 5 commits in mt76, is a
regular contributor
- [Phase 4] b4 dig: found mt7915 version in Felix Fietkau's series at
lore
- [Phase 4] lore direct access blocked by anti-bot protection
- [Phase 5] Grep MT76_MCU_RESET: confirmed flag is used across 15+ files
in mt76 for reset coordination
- [Phase 5] Grep mt7996_mcu_parse_response: confirmed it's registered as
mcu_ops callback (line 3363)
- [Phase 5] Grep reset_work: confirmed reset_work/reset_wait
infrastructure exists since v6.4
- [Phase 6] git tag --contains: mt7996_reset present since v6.4; driver
since v6.2
- [Phase 6] Current 7.0 code confirmed: mt7996_reset lacks STOP_DMA
handling, parse_response lacks reset trigger
- [Phase 8] Failure mode: permanent WiFi device stuck state requiring
reboot - severity HIGH
- UNVERIFIED: Exact lore discussion content could not be accessed due to
anti-bot protection
**YES**
drivers/net/wireless/mediatek/mt76/mt7996/mac.c | 5 +++++
drivers/net/wireless/mediatek/mt76/mt7996/mcu.c | 9 +++++++++
2 files changed, 14 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
index d4f3ee943b472..b7aa51481ce82 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
@@ -2737,6 +2737,11 @@ void mt7996_reset(struct mt7996_dev *dev)
return;
}
+ if (READ_ONCE(dev->recovery.state) & MT_MCU_CMD_STOP_DMA) {
+ set_bit(MT76_MCU_RESET, &dev->mphy.state);
+ wake_up(&dev->mt76.mcu.wait);
+ }
+
queue_work(dev->mt76.wq, &dev->reset_work);
wake_up(&dev->reset_wait);
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index c0c042de477b8..54776f0703876 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -209,6 +209,7 @@ static int
mt7996_mcu_parse_response(struct mt76_dev *mdev, int cmd,
struct sk_buff *skb, int seq)
{
+ struct mt7996_dev *dev = container_of(mdev, struct mt7996_dev, mt76);
struct mt7996_mcu_rxd *rxd;
struct mt7996_mcu_uni_event *event;
int mcu_cmd = FIELD_GET(__MCU_CMD_FIELD_ID, cmd);
@@ -217,6 +218,14 @@ mt7996_mcu_parse_response(struct mt76_dev *mdev, int cmd,
if (!skb) {
dev_err(mdev->dev, "Message %08x (seq %d) timeout\n",
cmd, seq);
+
+ if (!test_and_set_bit(MT76_MCU_RESET, &dev->mphy.state)) {
+ dev->recovery.restart = true;
+ wake_up(&dev->mt76.mcu.wait);
+ queue_work(dev->mt76.wq, &dev->reset_work);
+ wake_up(&dev->reset_wait);
+ }
+
return -ETIMEDOUT;
}
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 7.0] spi: stm32: fix rx DMA request error handling
From: Sasha Levin @ 2026-04-20 13:20 UTC (permalink / raw)
To: patches, stable
Cc: Alain Volmat, Dan Carpenter, Mark Brown, Sasha Levin,
mcoquelin.stm32, alexandre.torgue, linux-spi, linux-stm32,
linux-arm-kernel, linux-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Alain Volmat <alain.volmat@foss.st.com>
[ Upstream commit 837f6691d5f39ea6453e4489dded40fb17755c60 ]
Avoid trying to release the RX DMA channel when an error occurs
during RX dma_request_chan() call. Instead, jump directly to
release the TX channel, if applicable.
Reported-by: Dan Carpenter <dan.carpenter@linaro.org>
Closes: https://lore.kernel.org/linux-spi/aYXvY6NH7OlZ-OAF@stanley.mountain/T/#u
Signed-off-by: Alain Volmat <alain.volmat@foss.st.com>
Link: https://patch.msgid.link/20260212-spi-stm32-fix-dma-rx-release-v1-1-53a37c31626b@foss.st.com
Signed-off-by: Mark Brown <broonie@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Now I have all the information needed to complete the analysis. Let me
compile the full assessment.
---
## PHASE 1: COMMIT MESSAGE FORENSICS
### Step 1.1: Subject Line
- **Subsystem:** `spi: stm32`
- **Action verb:** "fix"
- **Summary:** Fix incorrect error handling when RX DMA channel request
fails during probe.
- Record: [spi: stm32] [fix] [rx DMA request error handling in probe
path]
### Step 1.2: Tags
- **Reported-by:** Dan Carpenter `<dan.carpenter@linaro.org>` — well-
known static analysis expert (Smatch maintainer)
- **Closes:** `https://lore.kernel.org/linux-spi/aYXvY6NH7OlZ-
OAF@stanley.mountain/T/#u` — Dan's bug report
- **Signed-off-by:** Alain Volmat (author, STM32 SPI subsystem
maintainer)
- **Link:** patch.msgid.link URL for the fix patch
- **Signed-off-by:** Mark Brown (SPI subsystem maintainer)
- Record: Reported by Dan Carpenter (Smatch static analysis). Author is
the STM32 SPI maintainer. Merged by SPI subsystem maintainer Mark
Brown.
### Step 1.3: Commit Body
The commit message states: avoid trying to release the RX DMA channel
when an error occurs during `dma_request_chan()` for RX. Instead, jump
directly to release the TX channel. This is clearly describing a bug
where on RX DMA request failure, the cleanup code incorrectly tries to
release a never-acquired RX DMA channel.
Record: Bug is calling `dma_release_channel()` on an ERR_PTR pointer
when RX DMA request fails. Symptom: crash/undefined behavior during
driver probe failure path.
### Step 1.4: Hidden Bug Fix Detection
Not hidden — clearly labeled as "fix" in subject line.
## PHASE 2: DIFF ANALYSIS
### Step 2.1: Inventory
- **Files changed:** `drivers/spi/spi-stm32.c` only
- **Lines changed:** ~6 added, ~6 removed (label and goto target
changes)
- **Functions modified:** `stm32_spi_probe()` — the probe error-handling
section
- **Scope:** Single-file, surgical fix in error path cleanup labels
### Step 2.2: Code Flow Change
**Before the fix:** When `dma_request_chan(spi->dev, "rx")` fails with
an error other than `-ENODEV`, the code does `goto err_dma_release`. At
that label, `spi->dma_rx` still holds an `ERR_PTR` value (non-NULL), so
`if (spi->dma_rx)` evaluates to true and
`dma_release_channel(spi->dma_rx)` is called with an invalid pointer.
**After the fix:** The goto target is changed to `err_dma_tx_release`
(new label), which skips the RX DMA release and only releases the TX
channel. The cleanup labels are split: RX release first (only reached
when RX was successfully acquired), then `err_dma_tx_release` for TX-
only cleanup.
### Step 2.3: Bug Mechanism
**Category:** Error path / resource handling bug leading to invalid
pointer dereference.
The root cause: commit `c266d19b7d4e5` ("spi: stm32: properly fail on
dma_request_chan error") moved the `spi->dma_rx = NULL` assignment
inside the `-ENODEV` branch but kept the `goto err_dma_release` in the
else branch. Before that commit, `spi->dma_rx` was always set to NULL
before any goto, making the cleanup safe.
### Step 2.4: Fix Quality
- Obviously correct: the fix ensures we skip releasing a channel that
was never acquired.
- Minimal/surgical: only changes a goto label and reorganizes 6 lines of
cleanup.
- Regression risk: extremely low — only affects error paths, and the
reordering correctly reverses the acquisition order (TX before RX, so
cleanup is RX then TX).
## PHASE 3: GIT HISTORY INVESTIGATION
### Step 3.1: Blame
The buggy `goto err_dma_release` at line 2508 is attributed to the
original Peter Ujfalusi commit `0a454258febb73` (2019), but the actual
bug was introduced by `c266d19b7d4e5` (Alain Volmat, 2025-12-18) which
restructured the error handling and removed the safety `spi->dma_rx =
NULL` before the goto.
Record: Bug introduced by c266d19b7d4e5 merged in v7.0-rc1 (v7.0 merge
window, after v6.14).
### Step 3.2: Fixes Tag
No explicit Fixes: tag in the fix commit, but from analysis, the bug was
introduced by c266d19b7d4e5. This commit exists only in v7.0-rc1+
(confirmed via `git tag --contains`).
### Step 3.3: File History
Dan Carpenter previously reported another probe error path bug in the
same file (`f4d8438e6a402` — sram pool free). This pattern of error path
bugs in probe is consistent.
### Step 3.4: Author
Alain Volmat is the STM32 SPI subsystem maintainer — 15+ commits to this
file. He both introduced the bug (c266d19b7d4e5) and wrote the fix. High
confidence in fix quality.
### Step 3.5: Dependencies
The fix depends on commit c266d19b7d4e5 being present. Since that commit
is the one that introduced the bug, the fix is only relevant to trees
containing c266d19b7d4e5, which is v7.0-rc1+.
## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH
### Step 4.1: Patch Discussion
The fix patch message ID `20260212-spi-stm32-fix-dma-rx-
release-v1-1-53a37c31626b@foss.st.com` indicates a v1 patch (single
revision). It was merged by Mark Brown (SPI maintainer). The "Closes:"
link references Dan Carpenter's original report.
### Step 4.2: Reviewers
Merged by Mark Brown (SPI subsystem maintainer). Author is Alain Volmat
(STM32 SPI maintainer). Dan Carpenter (reporter) is the Smatch static
analysis maintainer.
### Step 4.3: Bug Report
The report from Dan Carpenter at Linaro is a static analysis finding
(Smatch/Smatch-based). Dan is extremely reputable — his static analysis
findings are almost always real bugs.
### Step 4.5: Stable Discussion
No explicit stable nomination found, which is expected for the commits
under review.
## PHASE 5: CODE SEMANTIC ANALYSIS
### Step 5.1: Functions Modified
Only `stm32_spi_probe()` — the driver probe function, specifically its
cleanup/error path labels.
### Step 5.2: Callers
`stm32_spi_probe` is called by the platform driver framework during
device enumeration. It is the `.probe` callback for the
`stm32_spi_driver`. This is a standard driver entry point called when
device-tree matching finds an STM32 SPI controller.
### Step 5.4: Reachability
The bug is reachable during normal device probe when the RX DMA channel
request fails for reasons other than `-ENODEV` (e.g., `-EBUSY`,
`-ENOMEM`, `-EPROBE_DEFER` deferred probe). This is a realistic scenario
on STM32 embedded platforms.
## PHASE 6: STABLE TREE ANALYSIS
### Step 6.1: Buggy Code in Stable Trees
The buggy commit `c266d19b7d4e5` was merged in v7.0-rc1. It is NOT in
v6.14 or any earlier release. Therefore, this fix is only relevant to
the **7.0.y stable tree**.
### Step 6.2: Backport Complications
The fix should apply cleanly to 7.0.y since the buggy code c266d19b7d4e5
exists there unchanged.
## PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT
### Step 7.1: Subsystem
- **Path:** `drivers/spi/spi-stm32.c`
- **Subsystem:** SPI driver for STM32 (ARM embedded platform)
- **Criticality:** PERIPHERAL — affects STM32 embedded/IoT users
specifically, but STM32 is a very widely used embedded platform.
## PHASE 8: IMPACT AND RISK ASSESSMENT
### Step 8.1: Who Is Affected
STM32 SPI users on v7.0.y where the RX DMA channel request fails during
probe.
### Step 8.2: Trigger Conditions
Triggered when `dma_request_chan()` for the RX channel returns an error
other than `-ENODEV` during `stm32_spi_probe()`. This can happen with
DMA controller misconfiguration, resource contention, or deferred probe
scenarios.
### Step 8.3: Failure Mode
Calling `dma_release_channel()` with an ERR_PTR value causes a **kernel
crash** (invalid pointer dereference inside `dma_release_channel()`).
Severity: **HIGH** (kernel crash during probe).
### Step 8.4: Risk-Benefit Ratio
- **BENEFIT:** Prevents kernel crash on probe failure path — HIGH
- **RISK:** Very low — 6 lines changed, only in error cleanup labels,
obviously correct
- **Ratio:** Strongly favorable for backporting
## PHASE 9: FINAL SYNTHESIS
### Step 9.1: Evidence
**FOR backporting:**
- Fixes a real crash: `dma_release_channel()` called with ERR_PTR value
- Found by Dan Carpenter (highly reputable static analysis expert)
- Author is the STM32 SPI maintainer; merged by SPI subsystem maintainer
- Tiny, surgical fix (6 lines, single file, only error path labels)
- Obviously correct — only skips releasing a never-acquired channel
- Bug is in v7.0.y stable tree (introduced by c266d19b7d4e5)
**AGAINST backporting:**
- Only affects STM32 platforms (not universal)
- Only affects v7.0.y (bug was introduced in v7.0 merge window)
- Only triggers on probe failure path (not normal operation)
### Step 9.2: Stable Rules Checklist
1. Obviously correct and tested? **YES** — trivially verifiable goto
label change
2. Fixes a real bug? **YES** — crash from invalid pointer in cleanup
path
3. Important issue? **YES** — kernel crash on probe failure
4. Small and contained? **YES** — 6 lines in one file
5. No new features/APIs? **YES** — pure bugfix
6. Can apply to stable? **YES** — should apply cleanly to 7.0.y
### Step 9.3: Exception Categories
Not applicable — this is a straightforward bugfix.
### Step 9.4: Decision
This is a clear, small, correct bugfix for a crash in a driver probe
error path. It meets all stable kernel criteria.
## Verification
- [Phase 1] Parsed tags: Reported-by Dan Carpenter (Smatch/Linaro),
Closes link to lore report, Signed-off-by Mark Brown (SPI maintainer)
- [Phase 2] Diff analysis: ~6 lines changed in `stm32_spi_probe()`
cleanup labels — goto target changed from `err_dma_release` to
`err_dma_tx_release`, cleanup split into separate RX/TX labels
- [Phase 2] Verified bug mechanism: `spi->dma_rx` holds ERR_PTR when
goto fires; `if (spi->dma_rx)` is true for ERR_PTR;
`dma_release_channel(ERR_PTR)` causes crash
- [Phase 3] git blame: buggy `goto err_dma_release` at line 2508 traces
back through c266d19b7d4e5 (2025-12-18)
- [Phase 3] git show c266d19b7d4e5: confirmed this commit removed the
safety `spi->dma_rx = NULL` before the goto, introducing the bug
- [Phase 3] `git tag --contains c266d19b7d4e5`: first in v7.0-rc1, not
in v6.14 or earlier
- [Phase 3] Author Alain Volmat has 15+ commits to spi-stm32.c, is the
STM32 SPI maintainer
- [Phase 3] b4 dig -a c266d19b7d4e5: part of "spi: stm32: stability &
performance enhancements" series (v2)
- [Phase 5] `stm32_spi_probe` is the platform driver .probe callback
(line 2689), called during device enumeration
- [Phase 6] Buggy code only exists in v7.0-rc1+ (confirmed via git log
v6.14..v7.0-rc1)
- [Phase 6] Fix should apply cleanly — file unchanged between fix commit
and current 7.0 tree (bug still present in current tree at line 2508)
- [Phase 8] Failure mode: kernel crash from dma_release_channel() on
ERR_PTR — severity HIGH
**YES**
drivers/spi/spi-stm32.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/spi/spi-stm32.c b/drivers/spi/spi-stm32.c
index 33f211e159ef1..6d5eefa41f717 100644
--- a/drivers/spi/spi-stm32.c
+++ b/drivers/spi/spi-stm32.c
@@ -2505,7 +2505,7 @@ static int stm32_spi_probe(struct platform_device *pdev)
spi->dma_rx = NULL;
} else {
dev_err_probe(&pdev->dev, ret, "failed to request rx dma channel\n");
- goto err_dma_release;
+ goto err_dma_tx_release;
}
} else {
ctrl->dma_rx = spi->dma_rx;
@@ -2574,11 +2574,11 @@ static int stm32_spi_probe(struct platform_device *pdev)
if (spi->sram_pool)
gen_pool_free(spi->sram_pool, (unsigned long)spi->sram_rx_buf,
spi->sram_rx_buf_size);
-err_dma_release:
- if (spi->dma_tx)
- dma_release_channel(spi->dma_tx);
if (spi->dma_rx)
dma_release_channel(spi->dma_rx);
+err_dma_tx_release:
+ if (spi->dma_tx)
+ dma_release_channel(spi->dma_tx);
err_clk_disable:
clk_disable_unprepare(spi->clk);
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 7.0-5.10] drm/mediatek: mtk_dsi: enable hs clock during pre-enable
From: Sasha Levin @ 2026-04-20 13:21 UTC (permalink / raw)
To: patches, stable
Cc: Gary Bisson, CK Hu, Chen-Yu Tsai, AngeloGioacchino Del Regno,
Chun-Kuang Hu, Sasha Levin, p.zabel, airlied, simona,
matthias.bgg, dri-devel, linux-mediatek, linux-kernel,
linux-arm-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Gary Bisson <bisson.gary@gmail.com>
[ Upstream commit 76255024cadbe8c40462953f8193d2d78cd3b0ac ]
Some bridges, such as the TI SN65DSI83, require the HS clock to be
running in order to lock its PLL during its own pre-enable function.
Without this change, the bridge gives the following error:
sn65dsi83 14-002c: failed to lock PLL, ret=-110
sn65dsi83 14-002c: Unexpected link status 0x01
sn65dsi83 14-002c: reset the pipe
Move the necessary functions from enable to pre-enable.
Signed-off-by: Gary Bisson <bisson.gary@gmail.com>
Reviewed-by: CK Hu <ck.hu@mediatek.com>
Tested-by: Chen-Yu Tsai <wenst@chromium.org> # Chromebooks
Tested-by: AngeloGioacchino Del Regno <angelogioacchino.delregno@collabora.com>
Reviewed-by: AngeloGioacchino Del Regno <angelogioacchino.delregno@collabora.com>
Link: https://patchwork.kernel.org/project/dri-devel/patch/20260120-mtkdsi-v1-1-b0f4094f3ac3@gmail.com/
Signed-off-by: Chun-Kuang Hu <chunkuang.hu@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Good. Now I have all the information needed for a comprehensive
analysis.
---
## Complete Analysis
### PHASE 1: COMMIT MESSAGE FORENSICS
**Step 1.1: Subject Line**
- Subsystem: `drm/mediatek: mtk_dsi`
- Action: "enable" (HS clock during pre-enable)
- Summary: Move HS clock activation from the bridge enable phase to pre-
enable so downstream bridges can use it.
Record: [drm/mediatek] [enable] [Move HS clock setup earlier to fix
bridge PLL locking]
**Step 1.2: Tags**
- Signed-off-by: Gary Bisson (author)
- Reviewed-by: CK Hu (MediaTek DSI maintainer)
- Tested-by: Chen-Yu Tsai (Chromium developer) on 5 Chromebook variants
- Tested-by: AngeloGioacchino Del Regno (Collabora, regular MTK
contributor)
- Reviewed-by: AngeloGioacchino Del Regno
- Link: to patchwork entry
- Signed-off-by: Chun-Kuang Hu (subsystem maintainer, merged it)
Record: Strong review and testing from 3 independent testers across 7+
hardware platforms. Maintainer reviewed and approved.
**Step 1.3: Commit Body**
- Bug: SN65DSI83 bridge chip fails to lock its PLL during pre-enable,
resulting in errors: "failed to lock PLL, ret=-110" / "Unexpected link
status 0x01" / "reset the pipe"
- Root cause: HS clock not running during the pre-enable phase
- Fix: Move `mtk_dsi_lane_ready()` and `mtk_dsi_clk_hs_mode(dsi, 1)`
from enable to pre-enable (poweron)
Record: Clear bug description with error messages. Display completely
fails without fix.
**Step 1.4: Hidden Bug Fix?**
This is NOT hidden - it's explicitly a fix for display not working with
certain DSI bridges.
### PHASE 2: DIFF ANALYSIS
**Step 2.1: Inventory**
- 1 file: `drivers/gpu/drm/mediatek/mtk_dsi.c`
- +17 / -18 lines (net -1 line)
- Functions modified: `mtk_dsi_lane_ready()` (moved earlier),
`mtk_dsi_poweron()` (added 2 calls), `mtk_output_dsi_enable()`
(removed 3 lines)
Record: Single-file surgical fix, minimal scope.
**Step 2.2: Code Flow Change**
- `mtk_dsi_lane_ready()` function definition moved earlier (before
`mtk_dsi_poweron`) - this is purely for forward declaration ordering
- In `mtk_dsi_poweron()` (called during bridge pre_enable): added
`mtk_dsi_lane_ready(dsi)` and `mtk_dsi_clk_hs_mode(dsi, 1)` at end
- In `mtk_output_dsi_enable()` (called during bridge enable): removed
`mtk_dsi_lane_ready(dsi)` and `mtk_dsi_clk_hs_mode(dsi, 1)`, kept
`mtk_dsi_set_mode(dsi)` and `mtk_dsi_start(dsi)`
Before: Lane ready + HS clock in enable phase
After: Lane ready + HS clock in pre-enable phase
**Step 2.3: Bug Mechanism**
Category: Hardware interoperability / timing issue. The SN65DSI83 bridge
requires HS clock from the DSI host during its pre_enable to lock its
PLL. Without HS clock, the bridge fails completely.
**Step 2.4: Fix Quality**
- Obviously correct: just moves existing function calls earlier in the
init sequence
- Minimal: no new logic, no new code paths
- Regression risk is LOW: extensively tested on 7+ platforms with
different bridges/panels, all confirmed no regressions
### PHASE 3: GIT HISTORY
**Step 3.1: Blame**
- `mtk_dsi_lane_ready()` introduced by commit `39e8d062b03c3d` (Jitao
Shi, 2022-05-20) - present since ~v5.19
- `mtk_dsi_clk_hs_mode(dsi, 1)` in enable path introduced by
`80a5cfd60d2a94` (yt.shen@mediatek.com, 2017-03-31) - present since
v4.x
- The buggy ordering has existed since 2022 when lane_ready was moved to
enable
Record: Bug present in all active stable trees (v5.19+)
**Step 3.2: No Fixes: tag** (expected for autosel candidate)
**Step 3.3: File History**
- Recent changes to mtk_dsi.c include bridge API updates
(devm_drm_bridge_alloc, encoder parameter), HS mode support, pre-
enable order fix/revert
- The pre-enable order fix/revert (f5b1819193667 / 33e8150bd32d7) is
related but independent - it was about `pre_enable_prev_first` flag
management
**Step 3.4: Author**
- Gary Bisson is a regular contributor to MediaTek platforms (Tungsten
boards), actively maintains DT and driver support
**Step 3.5: Dependencies**
- No dependencies. The commit 8b00951402f74 (HS mode in cmdq) is
completely independent
- The SN65DSI83 driver already sets `pre_enable_prev_first = true`,
ensuring correct bridge ordering
### PHASE 4: MAILING LIST DISCUSSION
**Step 4.1: Original Discussion**
- b4 mbox retrieved 5 messages in the thread
- CK Hu (MediaTek DSI maintainer) noted "this changes the flow for all
SoC and panel, so I would wait for more SoC and more panel test" -
then gave Reviewed-by after testing completed
- AngeloGioacchino Del Regno tested on MT6795 + MT8395, gave both
Tested-by and Reviewed-by
- Chen-Yu Tsai tested on 5 Chromebook models (MT8173, MT8183x2,
MT8186x2) - "No regressions observed"
- Chun-Kuang Hu applied it with message "Applied to mediatek-drm-next"
**Step 4.2: Reviewers**
All appropriate MediaTek subsystem maintainers were CC'd and reviewed.
CK Hu explicitly asked for extensive testing, which was provided.
### PHASE 5: CODE SEMANTIC ANALYSIS
**Step 5.1: Functions Modified**
- `mtk_dsi_poweron()`: called from `mtk_dsi_bridge_atomic_pre_enable()`
and `mtk_dsi_ddp_start()`
- `mtk_output_dsi_enable()`: called from
`mtk_dsi_bridge_atomic_enable()`
- `mtk_dsi_lane_ready()`: also called from `mtk_dsi_host_transfer()`
(for DSI command transfers)
**Step 5.2: Impact on mtk_dsi_host_transfer**
After the patch, `mtk_dsi_lane_ready()` call in
`mtk_dsi_host_transfer()` becomes a no-op during normal operation (lanes
already ready from poweron). This is safe because DSI must be powered on
before any host transfers.
**Step 5.3: Bridge ordering confirmed**
The SN65DSI83 bridge driver sets `ctx->bridge.pre_enable_prev_first =
true` (line 1041 of `ti-sn65dsi83.c`), which causes
`drm_atomic_bridge_chain_pre_enable()` to call the MTK DSI pre_enable
BEFORE the SN65DSI83's pre_enable. This confirms the fix works
correctly.
### PHASE 6: STABLE TREE ANALYSIS
**Step 6.1: Code exists in stable trees**
The buggy code (`mtk_dsi_lane_ready()` being called from enable instead
of pre_enable) has been present since v5.19, so it affects all active
stable trees from 6.1 onwards.
**Step 6.2: Backport difficulty**
The patch should apply cleanly - the context in `mtk_dsi_poweron()` and
`mtk_output_dsi_enable()` is unchanged in the 7.0 tree.
### PHASE 7: SUBSYSTEM CONTEXT
**Step 7.1: Subsystem**
- drm/mediatek (DRM display driver) - IMPORTANT category
- MediaTek SoCs are used extensively in Chromebooks, Android devices,
and embedded systems
### PHASE 8: IMPACT AND RISK
**Step 8.1: Affected users**
Users of MediaTek SoCs with DSI bridges that need HS clock during
initialization (specifically SN65DSI83, potentially others).
**Step 8.2: Trigger conditions**
Every display initialization when using SN65DSI83 with MediaTek DSI. The
display simply doesn't work.
**Step 8.3: Failure mode**
Without fix: Display completely fails to initialize (PLL lock fails,
bridge errors, no display output). Severity: HIGH - affects usability.
**Step 8.4: Risk-Benefit**
- Benefit: HIGH - enables display to work for specific bridge
combinations, no regressions observed on 7+ platforms
- Risk: LOW - moves existing function calls earlier, extensively tested,
well-reviewed, single file
### PHASE 9: SYNTHESIS
**Evidence FOR backporting:**
1. Fixes a real hardware bug: display doesn't work with SN65DSI83 bridge
2. Small, surgical change (17 insertions, 18 deletions, net -1 line,
single file)
3. Extensive testing: 7+ platforms (Chromebooks, MediaTek boards) - zero
regressions
4. Strong review: MediaTek DSI maintainer, Collabora engineer, Chromium
developer
5. Code is well-understood: simply moves existing calls from enable to
pre-enable
6. Affects popular hardware (Chromebooks use MediaTek SoCs extensively)
**Evidence AGAINST backporting:**
1. CK Hu noted it "changes the flow for all SoC and panel" - but this
concern was addressed by extensive testing
2. No explicit Cc: stable tag - but this is expected for autosel
candidates
**Stable rules checklist:**
1. Obviously correct and tested? YES - extensively tested on 7+
platforms
2. Fixes a real bug? YES - display completely fails with SN65DSI83
bridge
3. Important issue? YES - hardware doesn't work (no display output)
4. Small and contained? YES - single file, 35 lines total changed
5. No new features? YES - just moves existing code to different phase
6. Can apply to stable? YES - context appears unchanged
---
### Verification
- [Phase 1] Parsed tags: Reviewed-by (CK Hu, AngeloGioacchino), Tested-
by (Chen-Yu Tsai on 5 Chromebooks, AngeloGioacchino on 2 platforms)
- [Phase 2] Diff analysis: mtk_dsi_lane_ready + clk_hs_mode moved from
enable to poweron (pre_enable), 17 add / 18 del
- [Phase 3] git blame: lane_ready introduced in commit 39e8d062b03c3d
(2022, v5.19), clk_hs_mode in enable since 80a5cfd60d2a94 (2017)
- [Phase 3] git show 33e8150bd32d7: revert of pre_enable_prev_first in
MediaTek DSI is independent
- [Phase 3] git show 8b00951402f74: HS mode support in cmdq is
independent, no dependency
- [Phase 4] b4 mbox retrieved thread: CK Hu reviewed, requested more
testing, got it; patch applied to mediatek-drm-next
- [Phase 4] Chen-Yu Tsai tested on MT8173, MT8183, MT8186 Chromebooks -
no regressions
- [Phase 4] AngeloGioacchino tested on MT6795, MT8395 - no regressions
- [Phase 5] Confirmed SN65DSI83 sets pre_enable_prev_first=true (line
1041), so MTK DSI pre_enable runs before SN65DSI83 pre_enable
- [Phase 5] mtk_dsi_host_transfer's call to lane_ready() becomes no-op
(safe)
- [Phase 6] Buggy code exists in all stable trees since v5.19
- [Phase 8] Failure mode: display doesn't work at all (PLL lock fails),
severity HIGH
The fix is small, surgical, well-tested across multiple platforms, and
fixes a real hardware bug where display completely fails with SN65DSI83
bridges on MediaTek DSI.
**YES**
drivers/gpu/drm/mediatek/mtk_dsi.c | 35 +++++++++++++++---------------
1 file changed, 17 insertions(+), 18 deletions(-)
diff --git a/drivers/gpu/drm/mediatek/mtk_dsi.c b/drivers/gpu/drm/mediatek/mtk_dsi.c
index aaf6c9ebd319f..5aa71fcdcfab7 100644
--- a/drivers/gpu/drm/mediatek/mtk_dsi.c
+++ b/drivers/gpu/drm/mediatek/mtk_dsi.c
@@ -673,6 +673,21 @@ static s32 mtk_dsi_switch_to_cmd_mode(struct mtk_dsi *dsi, u8 irq_flag, u32 t)
}
}
+static void mtk_dsi_lane_ready(struct mtk_dsi *dsi)
+{
+ if (!dsi->lanes_ready) {
+ dsi->lanes_ready = true;
+ mtk_dsi_rxtx_control(dsi);
+ usleep_range(30, 100);
+ mtk_dsi_reset_dphy(dsi);
+ mtk_dsi_clk_ulp_mode_leave(dsi);
+ mtk_dsi_lane0_ulp_mode_leave(dsi);
+ mtk_dsi_clk_hs_mode(dsi, 0);
+ usleep_range(1000, 3000);
+ /* The reaction time after pulling up the mipi signal for dsi_rx */
+ }
+}
+
static int mtk_dsi_poweron(struct mtk_dsi *dsi)
{
struct device *dev = dsi->host.dev;
@@ -725,6 +740,8 @@ static int mtk_dsi_poweron(struct mtk_dsi *dsi)
mtk_dsi_set_vm_cmd(dsi);
mtk_dsi_config_vdo_timing(dsi);
mtk_dsi_set_interrupt_enable(dsi);
+ mtk_dsi_lane_ready(dsi);
+ mtk_dsi_clk_hs_mode(dsi, 1);
return 0;
err_disable_engine_clk:
@@ -770,30 +787,12 @@ static void mtk_dsi_poweroff(struct mtk_dsi *dsi)
dsi->lanes_ready = false;
}
-static void mtk_dsi_lane_ready(struct mtk_dsi *dsi)
-{
- if (!dsi->lanes_ready) {
- dsi->lanes_ready = true;
- mtk_dsi_rxtx_control(dsi);
- usleep_range(30, 100);
- mtk_dsi_reset_dphy(dsi);
- mtk_dsi_clk_ulp_mode_leave(dsi);
- mtk_dsi_lane0_ulp_mode_leave(dsi);
- mtk_dsi_clk_hs_mode(dsi, 0);
- usleep_range(1000, 3000);
- /* The reaction time after pulling up the mipi signal for dsi_rx */
- }
-}
-
static void mtk_output_dsi_enable(struct mtk_dsi *dsi)
{
if (dsi->enabled)
return;
- mtk_dsi_lane_ready(dsi);
mtk_dsi_set_mode(dsi);
- mtk_dsi_clk_hs_mode(dsi, 1);
-
mtk_dsi_start(dsi);
dsi->enabled = true;
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 7.0-6.18] wifi: mt76: mt7996: fix frequency separation for station STR mode
From: Sasha Levin @ 2026-04-20 13:21 UTC (permalink / raw)
To: patches, stable
Cc: Peter Chiu, Lorenzo Bianconi, Felix Fietkau, Sasha Levin,
ryder.lee, matthias.bgg, angelogioacchino.delregno,
linux-wireless, linux-kernel, linux-arm-kernel, linux-mediatek
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Peter Chiu <chui-hao.chiu@mediatek.com>
[ Upstream commit 59a295335021f6973a34566554b2b9371f1c6f7d ]
Fix frequency separation field for STR in MLD capabilities to get the
correct chip capability.
Signed-off-by: Peter Chiu <chui-hao.chiu@mediatek.com>
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
Link: https://patch.msgid.link/20260316-mt7996-sta-str-v1-1-666814e6ab2d@kernel.org
Signed-off-by: Felix Fietkau <nbd@nbd.name>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Now I have enough information to complete the full analysis.
---
## PHASE 1: COMMIT MESSAGE FORENSICS
### Step 1.1: Subject Line
- **Subsystem**: `wifi: mt76: mt7996:` — MediaTek mt7996 WiFi 7 wireless
driver
- **Action verb**: "fix" — explicitly a bug fix
- **Summary**: Corrects frequency separation field for station STR mode
in MLD capabilities
### Step 1.2: Tags
- `Signed-off-by: Peter Chiu <chui-hao.chiu@mediatek.com>` — author from
MediaTek (the chip vendor)
- `Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>` — co-
author/maintainer of mt76
- `Link:` to patch.msgid.link — mailing list submission link
- `Signed-off-by: Felix Fietkau <nbd@nbd.name>` — mt76 maintainer
applied the patch
- No Fixes: tag, no Cc: stable, no Reported-by — expected for candidate
review
### Step 1.3: Commit Body
The message says: "Fix frequency separation field for STR in MLD
capabilities to get the correct chip capability." The bug is that the
station's MLD capabilities struct was missing the `FREQ_SEP_TYPE_IND`
field, causing the driver to advertise incorrect STR capabilities during
(Re)Association Request frames.
### Step 1.4: Hidden Bug Fix?
This is an explicit fix — the word "fix" is in the subject. The missing
capability field causes incorrect WiFi frame content to be advertised to
the AP during MLD association.
Record: Genuine bug fix — incorrect WiFi capability advertisement.
---
## PHASE 2: DIFF ANALYSIS
### Step 2.1: Inventory
- **1 file** changed: `drivers/net/wireless/mediatek/mt76/mt7996/init.c`
- **1 line added**:
`FIELD_PREP_CONST(IEEE80211_MLD_CAP_OP_FREQ_SEP_TYPE_IND, 1) |`
- Scope: Single-file, static initializer change
### Step 2.2: Code Flow
- **Before**: Station iftype entry in `iftypes_ext_capa[]` only sets
`IEEE80211_MLD_CAP_OP_MAX_SIMUL_LINKS`
- **After**: Station entry additionally sets
`IEEE80211_MLD_CAP_OP_FREQ_SEP_TYPE_IND` to value 1
- The `mld_capa_and_ops` field is consumed by mac80211 in `mlme.c`
(lines 2069 and 10612-10613) and included directly in MLD capability
elements of association frames
### Step 2.3: Bug Mechanism
This is a **logic/correctness fix** — a missing capability field in a
static const initializer. `IEEE80211_MLD_CAP_OP_FREQ_SEP_TYPE_IND` (mask
`0x0f80`, bits 7-11) was not set, meaning the station reported frequency
separation type = 0 to the AP, which does not reflect the actual mt7996
hardware STR capability.
### Step 2.4: Fix Quality
- Trivially correct — adding a missing field to a bitfield OR expression
- Cannot introduce a regression — it's a static const initializer
- Minimal and surgical
- No red flags
Record: 1 line added, static initializer, zero regression risk.
---
## PHASE 3: GIT HISTORY INVESTIGATION
### Step 3.1: Blame
The station entry was introduced by commit `f5160304d57c55` ("wifi:
mt76: mt7996: Enable MLO support for client interfaces") by Lorenzo
Bianconi on 2025-09-01. The base `iftypes_ext_capa[]` array was
introduced by `a9eae65d97f3cb` ("Export MLO AP capabilities to
mac80211") on 2025-08-27.
### Step 3.2: Fixes Target
No explicit Fixes: tag, but the implicit target is `f5160304d57c55`
which first introduced the station MLD capabilities. That commit was
first present in **v6.18** (confirmed via `git merge-base --is-
ancestor`). NOT in v6.17 or earlier.
### Step 3.3: File History
The file has 20 recent commits, many related to MLO/MLD enablement. This
fix appears standalone — no series dependencies.
### Step 3.4: Author
- Peter Chiu is from MediaTek (the silicon vendor) — strong authority on
hardware capabilities
- Lorenzo Bianconi is the mt76 co-maintainer who also signed off
- Felix Fietkau (nbd) is the mt76 maintainer who applied the patch
Record: Author is from the chip vendor. Both mt76 maintainers signed
off. Bug introduced in v6.18.
### Step 3.5: Dependencies
The fix only adds a line to an existing OR expression in a static
initializer. No functional dependencies. The prerequisite code
(`iftypes_ext_capa` with the station entry) exists in v6.18 and v7.0.
---
## PHASE 4: MAILING LIST
### Step 4.1-4.5
Lore/WebFetch was blocked by Anubis protection. `b4 dig` for the
message-id didn't find a match by commit hash (the commit hasn't been
applied to the tree I'm on). The link
`https://patch.msgid.link/20260316-mt7996-sta-
str-v1-1-666814e6ab2d@kernel.org` indicates this was a v1 single-patch
submission from Lorenzo Bianconi.
Record: Could not fetch discussion. Patch submitted as v1, single patch.
---
## PHASE 5: CODE SEMANTIC ANALYSIS
### Step 5.1-5.4
The `iftypes_ext_capa[]` array is assigned to `wiphy->iftype_ext_capab`
at line 497 of `init.c`. This is consumed by mac80211's `mlme.c` via
`cfg80211_get_iftype_ext_capa()` — the `mld_capa_and_ops` field is
directly encoded into (Re)Association Request frames and MLD
reconfiguration frames. This is a hot path for any MLD station
association.
### Step 5.5: Similar Patterns
The AP section of the same array does NOT include `FREQ_SEP_TYPE_IND`
either, but only the station section is fixed here (STR is a station-
side mode). The mt7925 driver also sets `mld_capa_and_ops` but
dynamically.
Record: Capability is directly embedded in WiFi management frames during
association.
---
## PHASE 6: STABLE TREE ANALYSIS
### Step 6.1: Does the buggy code exist in stable trees?
- The buggy commit `f5160304d57c55` is in **v6.18 and v7.0 only**
- NOT in v6.17 or earlier — so this fix is irrelevant for all current
LTS trees (6.12.y, 6.6.y, 6.1.y, 5.15.y)
- Only relevant for **7.0.y** stable and potentially 6.18.y if that is
still maintained
### Step 6.2: Backport Complications
- 1 line addition to a static initializer — will apply cleanly to 7.0.y
- No conflicting changes expected
### Step 6.3: No related fixes already in stable
---
## PHASE 7: SUBSYSTEM CONTEXT
### Step 7.1
- **Subsystem**: WiFi drivers / MediaTek mt76 / mt7996
- **Criticality**: PERIPHERAL (specific WiFi hardware) but WiFi is
important for many users
### Step 7.2
- Very active subsystem — 188 mt7996 commits since v6.12
- mt7996 is a WiFi 7 chip with active MLO development
---
## PHASE 8: IMPACT AND RISK ASSESSMENT
### Step 8.1: Affected Users
- Mt7996 WiFi 7 hardware users attempting MLD (Multi-Link Device)
operation in station mode
### Step 8.2: Trigger Conditions
- Triggered whenever a mt7996 station performs MLD association (common
for WiFi 7 users)
- The wrong capability is always advertised
### Step 8.3: Failure Mode
- Incorrect WiFi capability in association frames → potentially
incorrect STR mode negotiation, possible performance degradation or
failed MLD operation
- Severity: **MEDIUM** — functional correctness issue, not a crash or
security vulnerability
### Step 8.4: Risk-Benefit
- **Benefit**: Medium — corrects WiFi MLD capability for mt7996 users
- **Risk**: Very low — 1-line static initializer change, cannot regress
- **Ratio**: Favorable
---
## PHASE 9: FINAL SYNTHESIS
### Step 9.1: Evidence
**FOR backporting:**
- Genuine correctness fix (missing capability field)
- Trivially small (1 line) and obviously correct
- Zero regression risk (static const initializer)
- Written by chip vendor (MediaTek), signed by both mt76 maintainers
- Fixes real functional issue for WiFi 7 MLD users
- Buggy code exists in 7.0
**AGAINST backporting:**
- Not a crash, security, or data corruption fix
- Only affects mt7996 MLD station mode users (relatively narrow)
- Relatively new code (v6.18+)
### Step 9.2: Stable Rules Checklist
1. Obviously correct and tested? **Yes** — trivially correct 1-line
addition
2. Fixes a real bug? **Yes** — incorrect WiFi capability advertisement
3. Important issue? **Medium** — functional correctness for WiFi MLD
4. Small and contained? **Yes** — 1 line in 1 file
5. No new features? **Correct** — fixes existing code
6. Can apply to stable? **Yes** — clean apply expected
### Step 9.3: Exception Categories
Not an exception category; this is a standard driver bug fix.
### Step 9.4: Decision
The fix is a genuine 1-line correctness fix that corrects incorrect WiFi
MLD capability advertisement for mt7996 hardware. It's written by the
chip vendor, reviewed by both subsystem maintainers, and has zero
regression risk. While it's not a crash or security fix, it corrects
real WiFi functionality for mt7996 users in MLD/STR mode. The extremely
small scope and zero risk make this appropriate for stable.
---
## Verification
- [Phase 1] Parsed tags: Author from MediaTek, co-signed by mt76 co-
maintainer and maintainer
- [Phase 2] Diff analysis: 1 line added to static const initializer,
adds missing `IEEE80211_MLD_CAP_OP_FREQ_SEP_TYPE_IND` field
- [Phase 3] git blame: Station MLD caps introduced by `f5160304d57c55`
(v6.18), confirmed via `git merge-base`
- [Phase 3] git merge-base: Buggy commit NOT in v6.17 or earlier, IS in
v6.18 and v7.0
- [Phase 5] grep callers: `mld_capa_and_ops` consumed in
`net/mac80211/mlme.c` lines 2069 and 10612-10613 for association
frames
- [Phase 5] grep definition: `IEEE80211_MLD_CAP_OP_FREQ_SEP_TYPE_IND` =
0x0f80 in `include/linux/ieee80211-eht.h`
- [Phase 6] Buggy code only in v6.18+ and v7.0; irrelevant for older LTS
trees
- [Phase 7] Peter Chiu confirmed as MediaTek contributor; Lorenzo
Bianconi and Felix Fietkau are mt76 maintainers
- UNVERIFIED: Could not access lore.kernel.org discussion due to Anubis
protection
**YES**
drivers/net/wireless/mediatek/mt76/mt7996/init.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
index 00a8286bd1368..d940ca84bd0f1 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
@@ -85,6 +85,7 @@ static const struct wiphy_iftype_ext_capab iftypes_ext_capa[] = {
.extended_capabilities_mask = if_types_ext_capa_ap,
.extended_capabilities_len = sizeof(if_types_ext_capa_ap),
.mld_capa_and_ops =
+ FIELD_PREP_CONST(IEEE80211_MLD_CAP_OP_FREQ_SEP_TYPE_IND, 1) |
FIELD_PREP_CONST(IEEE80211_MLD_CAP_OP_MAX_SIMUL_LINKS,
MT7996_MAX_RADIOS - 1),
},
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 6.18] arm64: dts: imx8mq: Set the correct gpu_ahb clock frequency
From: Sasha Levin @ 2026-04-20 13:21 UTC (permalink / raw)
To: patches, stable
Cc: Sebastian Krzyszkowiak, Frank Li, Peng Fan, Fabio Estevam,
Sasha Levin, robh, krzk+dt, conor+dt, shawnguo, l.stach, agx,
devicetree, imx, linux-arm-kernel, linux-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Sebastian Krzyszkowiak <sebastian.krzyszkowiak@puri.sm>
[ Upstream commit 1f99b5d93d99ca17d50b386a674d0ce1f20932d8 ]
According to i.MX 8M Quad Reference Manual, GPU_AHB_CLK_ROOT's maximum
frequency is 400MHz.
Fixes: 45d2c84eb3a2 ("arm64: dts: imx8mq: add GPU node")
Reviewed-by: Frank Li <Frank.Li@nxp.com>
Signed-off-by: Sebastian Krzyszkowiak <sebastian.krzyszkowiak@puri.sm>
Reviewed-by: Peng Fan <peng.fan@nxp.com>
Reviewed-by: Fabio Estevam <festevam@gmail.com>
Signed-off-by: Frank Li <Frank.Li@nxp.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Error: Failed to generate final synthesis
arch/arm64/boot/dts/freescale/imx8mq.dtsi | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/arch/arm64/boot/dts/freescale/imx8mq.dtsi b/arch/arm64/boot/dts/freescale/imx8mq.dtsi
index 607962f807beb..6a25e219832ce 100644
--- a/arch/arm64/boot/dts/freescale/imx8mq.dtsi
+++ b/arch/arm64/boot/dts/freescale/imx8mq.dtsi
@@ -1632,7 +1632,7 @@ gpu: gpu@38000000 {
<&clk IMX8MQ_GPU_PLL_OUT>,
<&clk IMX8MQ_GPU_PLL>;
assigned-clock-rates = <800000000>, <800000000>,
- <800000000>, <800000000>, <0>;
+ <800000000>, <400000000>, <0>;
power-domains = <&pgc_gpu>;
};
--
2.53.0
^ permalink raw reply related
* [PATCH AUTOSEL 6.18] net: airoha: Fix memory leak in airoha_qdma_rx_process()
From: Sasha Levin @ 2026-04-20 13:22 UTC (permalink / raw)
To: patches, stable
Cc: Lorenzo Bianconi, Simon Horman, Jakub Kicinski, Sasha Levin,
andrew+netdev, davem, edumazet, pabeni, linux-arm-kernel,
linux-mediatek, netdev, linux-kernel
In-Reply-To: <20260420132314.1023554-1-sashal@kernel.org>
From: Lorenzo Bianconi <lorenzo@kernel.org>
[ Upstream commit 285fa6b1e03cff78ead0383e1b259c44b95faf90 ]
If an error occurs on the subsequents buffers belonging to the
non-linear part of the skb (e.g. due to an error in the payload length
reported by the NIC or if we consumed all the available fragments for
the skb), the page_pool fragment will not be linked to the skb so it will
not return to the pool in the airoha_qdma_rx_process() error path. Fix the
memory leak partially reverting commit 'd6d2b0e1538d ("net: airoha: Fix
page recycling in airoha_qdma_rx_process()")' and always running
page_pool_put_full_page routine in the airoha_qdma_rx_process() error
path.
Fixes: d6d2b0e1538d ("net: airoha: Fix page recycling in airoha_qdma_rx_process()")
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/20260402-airoha_qdma_rx_process-mem-leak-fix-v1-1-b5706f402d3c@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
LLM Generated explanations, may be completely bogus:
Error: Failed to generate final synthesis
drivers/net/ethernet/airoha/airoha_eth.c | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
index 4fc6bd282b465..bdf600fea9508 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -709,9 +709,8 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
if (q->skb) {
dev_kfree_skb(q->skb);
q->skb = NULL;
- } else {
- page_pool_put_full_page(q->page_pool, page, true);
}
+ page_pool_put_full_page(q->page_pool, page, true);
}
airoha_qdma_fill_rx_queue(q);
--
2.53.0
^ permalink raw reply related
* Re: [PATCH 2/2] arm64: dts: rockchip: Replace deprecated snps,* props for NanoPi R5S
From: Diederik de Haas @ 2026-04-20 13:35 UTC (permalink / raw)
To: Tianling Shen, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Heiko Stuebner
Cc: Arnd Bergmann, devicetree, linux-arm-kernel, linux-rockchip,
linux-kernel, Quentin Schulz, Jonas Karlman
In-Reply-To: <2d2b1e17-388f-431a-be86-a0f26b5be6cf@gmail.com>
On Mon Apr 20, 2026 at 8:58 AM CEST, Tianling Shen wrote:
> On 2026/4/15 22:23, Diederik de Haas wrote:
>> On Wed Apr 1, 2026 at 3:11 PM CEST, Diederik de Haas wrote:
>>> The various snps,reset-* properties are deprecated, so convert them into
>>> their replacements.
>>>
>>> Signed-off-by: Diederik de Haas <diederik@cknow-tech.com>
>>> ---
>>> arch/arm64/boot/dts/rockchip/rk3568-nanopi-r5s.dts | 7 +++----
>>> 1 file changed, 3 insertions(+), 4 deletions(-)
>>>
>>> diff --git a/arch/arm64/boot/dts/rockchip/rk3568-nanopi-r5s.dts b/arch/arm64/boot/dts/rockchip/rk3568-nanopi-r5s.dts
>>> index 90ce6f0e1dcf..92d044ec696b 100644
>>> --- a/arch/arm64/boot/dts/rockchip/rk3568-nanopi-r5s.dts
>>> +++ b/arch/arm64/boot/dts/rockchip/rk3568-nanopi-r5s.dts
>>> @@ -85,10 +85,6 @@ &gmac0_tx_bus2
>>> &gmac0_rx_bus2
>>> &gmac0_rgmii_clk
>>> &gmac0_rgmii_bus>;
>>> - snps,reset-gpio = <&gpio0 RK_PC5 GPIO_ACTIVE_LOW>;
>>> - snps,reset-active-low;
>>> - /* Reset time is 15ms, 50ms for rtl8211f */
>>> - snps,reset-delays-us = <0 15000 50000>;
>>> tx_delay = <0x3c>;
>>> rx_delay = <0x2f>;
>>> status = "okay";
>>> @@ -100,6 +96,9 @@ rgmii_phy0: ethernet-phy@1 {
>>> reg = <1>;
>>> pinctrl-0 = <&gmac0_rstn_gpio0_c5_pin>;
>>> pinctrl-names = "default";
>>> + reset-assert-us = <15000>;
>>> + reset-deassert-us = <50000>;
>>> + reset-gpios = <&gpio0 RK_PC5 GPIO_ACTIVE_LOW>;
>>> };
>>> };
>>>
>>
>> Please disregard/drop this patch.
>>
>> I was recently made aware of 'sashiko.dev' and checked whether it had
>> also checked my patch, which it did:
>> https://sashiko.dev/#/patchset/20260401131551.734456-1-diederik%40cknow-tech.com
>>
>> And it turns out that the concern raised is valid (thanks Quentin!), so
>> this patch could introduce a regression.
>> So it looks like staying with the deprecated properties is actually
>> better (in this case?).
>
> Well actually we more or less rely on U-Boot to reset the PHY first now.
This change would introduce such a dependency where it was not there
before, so this could introduce a regression.
> Many rockchip boards in tree require a reset before the PHY can be
> recognized, but we just use the generic "ethernet-phy-ieee802.3-c22"
> compatible.
I've identified ~40 Rockchip based boards where there is a dependency on
the bootloader due to using that generic compatible. Some from the start
and some got it added with a similar conversion as I proposed above.
I haven't seen massive bug reports, so it looks like it's currently ok.
I don't like having such a dependency and certainly not adding one where
it previously was not the case.
In other cases, the generic compatible was replaced with a specific one
for the PHY being used, which 'circumvents' the raised concern:
https://lore.kernel.org/linux-rockchip/20260202-px30-eth-phy-v1-0-ef365be64922@cherry.de/
According to the FriendlyELEC schematics I checked, they seem to use the
RTL8211F a LOT. On the NanoPi R6* they use a/the specific compatible:
https://elixir.bootlin.com/linux/v7.0/source/arch/arm64/boot/dts/rockchip/rk3588s-nanopi-r6.dtsi#L348
I've sent FriendlyELEC an email to ask whether they ONLY used that PHY
in the R5S (LTS) in which case it is safe to replace the generic
compatible with the specific one. I haven't received a response yet.
> Another option is to move the reset props to mdio node instead of PHY
> node, though.
I prefer that there's first an agreed upon 'strategy' on how to deal
with the above mentioned raised concern so that it can be implemented
consistently.
Cheers,
Diederik
^ permalink raw reply
* [PATCH v4 0/5] Add Rockchip RK3576 PWM Support Through MFPWM
From: Nicolas Frattaroli @ 2026-04-20 13:35 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio, Conor Dooley
This series introduces support for some of the functions of the new PWM
silicon found on Rockchip's RK3576 SoC. Due to the wide range of
functionalities offered by it, including many parts which this series'
first iteration does not attempt to implement for now. The drivers are
modelled as an MFD, with no leakage of the MFD-ness into the binding, as
it's a Linux implementation detail.
Here's some of the features of the hardware:
- Continuous PWM output (implemented in this series)
- One-shot/Finite repetition PWM output
- PWM capture by counting high/low cycles (implemented in this series)
- Sending IR transmissions in several TV remote protocols
- Generating an interrupt based on the input being one of 16
user-specified values ("Power key capture")
- Biphasic counter support
- Using the hardware to measure a clock signal's frequency
- Using the hardware to count a clock signal's pulses
- Generating PWM output waveforms through a user-specified lookup table
As you can tell, there's a lot. I've focused on continuous PWM output
for now as the most important one for things like controlling fans. The
PWM capture driver is an added bonus, because I needed at least two
drivers to test things. Anyone doing consumer electronic devices like
TVs based on the RK3576 may need to do the power key stuff at some
stage, as it can be used to wake up the SoC with an IR remote. The IR
transmission stuff in general may be a funny weekend project for someone
at some point; I assume it's there so TV boxes can turn on and off TVs
without needing the HDMI control stuff.
At first, I considered simply integrating support for this new IP into
the old pwm-rockchip driver, as the downstream vendor kernel did.
However, the IP is significantly different from previous iterations.
Especially if the goal is to support some of the additional
functionality that the new silicon brings, doing it all in a single pwm
driver would be untenable. Especially one that already supports other
hardware with a way different set of registers.
Hence, the mfpwm pattern: each device functionality is its own driver,
and they all get registered as MFD cells by the parent mfpwm MFD driver,
which is the one that binds to the DT compatible. Each device function
driver then has to _acquire and _release the hardware when it needs
control of it. If some other device function is using the device
already, -EBUSY is returned, which the device function driver can then
forward to the user and everyone is happy.
The PWM output driver, pwm-rockchip-v4, uses the new waveform APIs. I
thought while writing a new driver that I might as well use the new
APIs.
The PWM capture driver, implemented as a counter driver, is somewhat
primitive, in that it doesn't make use of things like the biphasic
counter support or clock measuring, but it serves as a good way to
showcase and test the mutual exclusion that the mfpwm framework tries to
achieve. It directly exposes the HPC/LPC counts as counters. Shoutouts
to the counter subsystem's documentation by the way, it is some of the
best subsystem documentation I've come across so far, and was a great
help.
All instances of the PWM controller have three clocks that they can pick
and choose to derive the PWM signal from. One is the default PLL from
the CRU, one is the 24 MHz crystal oscillator (gated by the CRU), and
one is an RC oscillator (also gated by the CRU). Each PWM channel can
switch between these with a clock selection register in the PWM register
range, hence this is implemented as a clock mux.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
Changes in v4:
- Fix MAINTAINERS entry for mfpwm
- Make mfpwm core driver depend on ARCH_ROCKCHIP || COMPILE_TEST
- Remove redundant Kconfig deps from pwm output and counter
- mfpwm core: Introduce mfpwm_get_mode
- mfpwm core: Rename pwm out to rockchip-pwm-v4
- mfpwm core: Remove leftover commented out code
- pwm output: Rename to rockchip-pwm-v4
- pwm output: Rework round_wf_tohw:
- Pass wf/wfhw into round_params
- If wfhw->period is 0, don't do the offset clamping calculation to
avoid underflow
- Return -ERANGE in a theoretical future where the clock is that high
- Change debug print
- pwm output: Change fromhw debug print to conform to other PWM drivers
- pwm output: Adjust comments at the start of the file
- pwm output: Store rate in wfhw struct
- pwm output: Get rid of unnecessary initialization of locals
- pwm output: Round up in fromhw
- pwm output: Use common is_enabled helper in read_wf
- pwm output: put exclusive rate and clk_disable on unlikely error path
- pwm output: Set of_node_reused on this device, rather than the parent,
and set its device node to the parent node
- pwm output: Make failure to acquire PWM in probe an error rather than
a warning
- pwm output: Re-do error handling in probe function to drop clock and
mfpwm on failure
- counter: Get rid of enable_lock and is_enabled, read this from hw regs
- counter: Request IRQ after setting up the counter device
- counter: Acquire mfpwm if counter hardware is enabled at module probe
time
- counter: Rework signals, synapses and counts
- Add patch to describe the Radxa ROCK 4D's PWM-controlled fan in DT
- Link to v3: https://lore.kernel.org/r/20251027-rk3576-pwm-v3-0-654a5cb1e3f8@collabora.com
Changes in v3:
- Move drivers to using MFD; MFPWM now lives in the mfd tree as
requested by Lee Jones
- Use the new FIELD_PREP_WM16 macros, and rebase onto next-20251027
- Get rid of some unused hardware version accessor inline functions
- pwm-rockchip-v4 pwm output: use devm_pwmchip_add and get rid of the
driver remove callback that's no longer needed
- pwm-rockchip-v4 pwm output: use the parent MFD device's OF node, so
that referencing the pwm node in DT works correctly (ty Heiko)
- pwm-rockchip-v4 pwm output: add link to public TRM for the hardware in
comment at the start of the file
- pwm-rockchip-v4 pwm output: Capitalise first letter in kernel messages
- pwm-rockchip-v4 pwm output: get rid of unnecessary mul_u64_u64_div_u64
calls where the operands cannot produce an overflow, turning it into a
regular u64 division
- pwm-rockchip-v4 pwm output: simplify round_rate functions
- pwm-rockchip-v4 pwm output: remove redundant duty <= period check
- pwm-rockchip-v4 pwm output: print input parameters in tohw/fromhw in
debug statement
- pwm-rockchip-v4 pwm output: clarify the offset < (period - duty) thing
being dictated by hardware with a comment in the limitations list and
near where the check is
- pwm-rockchip-v4 pwm output: remove pointless mfpwm_acquire/release
calls in the fromhw/tohw functions, as they don't actually protect
against anything
- pwm-rockchip-capture counter: expose HPC and LPC directly, and fire a
change-of-state event on the appropriate channel on interrupt
- pwm-rockchip-capture counter: remove all the captures_left and delayed
worker cruft
- pwm-rockchip-capture counter: use MFD parent's OF node
- pwm-rockchip-capture counter: change intsts ^ clr to != and add a
comment explaining why there's no mask here
- Link to v2: https://lore.kernel.org/r/20250602-rk3576-pwm-v2-0-a6434b0ce60c@collabora.com
Changes in v2:
- bindings: make osc required (as it's present in all instances of the
hardware I'm aware of) and add the rc clock as well. I thought it
wasn't present on some instances of the PWM IP due to the vendor SoC
dtsi, but checking the CRU made me realise those clocks do exist for
all instances. Did not include Conor's R-b as this constitutes a
substantial enough change to necessitate a re-review
- move bitfield write-enable mask macros into bitfield.h by replacing
the original rockchip-specific utils header patch with a bitfield.h
patch.
- mfpwm: change all instances of WARN to be dev_warn instead, as we have
a device pointer.
- mfpwm: replace the ad-hoc clock mux implementation that used a sysfs
interface with a generic clk-mux.
- mfpwm: add the rc clock
- mfpwm: rename all the pwmv4_ prefixed functions to have the
rockchip_pwm_v4_ prefix instead
- mfpwm: remove the pwmclk indirection, hand chosen_clk to pwmf
- mfpwm: move to use the new bitfield macros for the WE mask
- mfpwm: mark reg access inline functions as static to fix build errors
- pwm-rockchip-v4 pwm output: replace mult_frac with mul_u64_u64_div_u64
- pwm-rockchip-v4 pwm output: don't return error if parameters are out
of range, just set them to the maximum
- pwm-rockchip-v4 pwm output: add rate to debug message
- pwm-rockchip-v4 pwm output: if rate is 0 and pwm is disabled, set
waveform parameters to 0. The clock is expected to not have a rate in
this case.
- pwm-rockchip-v4 pwm output: add pwmchip_remove in remove callback,
which also necessitated using chip as the platdata instead of the
driver private struct
- pwm-rockchip-v4 pwm output: rework PWMV4_CTRL_UPDATE_EN since it never
needs to be set to 0 by the driver
- pwm-rockchip-v4 pwm output: add a limitations list
- pwm-rockchip-v4 pwm output: handle initial hardware state during
probe, enabling the pwm clock if the PWM is on and in continuous mode
- pwm-rockchip-v4 pwm output: rename pwmv4_is_enabled to use the
rockchip_pwm_v4_ prefix instead
- pwm-rockchip-v4 pwm output: remove pwmclk indirection, use clk API
directly
- pwm-rockchip-v4 pwm output: no longer claim the chip as being atomic,
as the clk_rate_exclusive_get calls may sleep.
- rockchip-pwm-capture counter: remove pwmclk indirection, use clk API
directly
- rockchip-pwm-capture counter: replace mult_frac with
mul_u64_u64_div_u64
- rockchip-pwm-capture counter: don't output periods/duty cycles if the
period is longer than the chosen timeout; this works around the
hardware cycle counter seemingly being impossible to clear
- dts: added osc and rc to every pwm node
- dts: reordered properties in pwm0 to be sorted
- Link to v1: https://lore.kernel.org/r/20250408-rk3576-pwm-v1-0-a49286c2ca8e@collabora.com
To: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
To: Uwe Kleine-König <ukleinek@kernel.org>
To: Rob Herring <robh@kernel.org>
To: Krzysztof Kozlowski <krzk+dt@kernel.org>
To: Conor Dooley <conor+dt@kernel.org>
To: Heiko Stuebner <heiko@sntech.de>
To: Lee Jones <lee@kernel.org>
To: William Breathitt Gray <wbg@kernel.org>
To: Damon Ding <damon.ding@rock-chips.com>
Cc: kernel@collabora.com
Cc: Jonas Karlman <jonas@kwiboo.se>
Cc: Alexey Charkov <alchark@gmail.com>
Cc: linux-rockchip@lists.infradead.org
Cc: linux-pwm@vger.kernel.org
Cc: devicetree@vger.kernel.org
Cc: linux-arm-kernel@lists.infradead.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-iio@vger.kernel.org
---
Nicolas Frattaroli (5):
dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
mfd: Add Rockchip mfpwm driver
pwm: Add rockchip PWMv4 driver
arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi
arm64: dts: rockchip: Add cooling fan to ROCK 4D
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 ++++
MAINTAINERS | 11 +
arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts | 50 +++
arch/arm64/boot/dts/rockchip/rk3576.dtsi | 208 +++++++++
drivers/counter/Kconfig | 11 +
drivers/counter/Makefile | 1 +
drivers/counter/rockchip-pwm-capture.c | 307 ++++++++++++++
drivers/mfd/Kconfig | 16 +
drivers/mfd/Makefile | 1 +
drivers/mfd/rockchip-mfpwm.c | 357 ++++++++++++++++
drivers/pwm/Kconfig | 11 +
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-rockchip-v4.c | 383 +++++++++++++++++
include/linux/mfd/rockchip-mfpwm.h | 470 +++++++++++++++++++++
14 files changed, 1904 insertions(+)
---
base-commit: 77a9bb0193d790fb71c0edfc567bddc1b56fb3ff
change-id: 20250407-rk3576-pwm-46761bd0deaa
Best regards,
--
Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
^ permalink raw reply
* [PATCH v4 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
From: Nicolas Frattaroli @ 2026-04-20 13:35 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio, Conor Dooley
In-Reply-To: <20260420-rk3576-pwm-v4-0-421738c7bf28@collabora.com>
The Rockchip RK3576 SoC has a newer PWM controller IP revision than
previous Rockchip SoCs. This IP, called "PWMv4" by Rockchip, introduces
several new features, and consequently differs in its bindings.
Instead of expanding the ever-growing rockchip-pwm binding that already
has an if-condition, add an entirely new binding to handle this.
There are two additional clocks, "osc" and "rc". These are available for
every PWM instance, and the PWM hardware can switch between the "pwm",
"osc" and "rc" clock at runtime.
The PWM controller also comes with an interrupt now. This interrupt is
used to signal various conditions.
Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 ++++++++++++++++++++++
MAINTAINERS | 7 ++
2 files changed, 84 insertions(+)
diff --git a/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
new file mode 100644
index 000000000000..48d5055c8b06
--- /dev/null
+++ b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
@@ -0,0 +1,77 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pwm/rockchip,rk3576-pwm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Rockchip PWMv4 controller
+
+maintainers:
+ - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+
+description: |
+ The Rockchip PWMv4 controller is a PWM controller found on several Rockchip
+ SoCs, such as the RK3576.
+
+ It supports both generating and capturing PWM signals.
+
+allOf:
+ - $ref: pwm.yaml#
+
+properties:
+ compatible:
+ items:
+ - const: rockchip,rk3576-pwm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: Used to derive the PWM signal.
+ - description: Used as the APB bus clock.
+ - description: Used as an alternative to derive the PWM signal.
+ - description: Used as another alternative to derive the PWM signal.
+
+ clock-names:
+ items:
+ - const: pwm
+ - const: pclk
+ - const: osc
+ - const: rc
+
+ interrupts:
+ maxItems: 1
+
+ "#pwm-cells":
+ const: 3
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - interrupts
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/rockchip,rk3576-cru.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ soc {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ pwm@2add0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>,
+ <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
+ #pwm-cells = <3>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 891fb83ba7a9..86f20cb563c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23172,6 +23172,13 @@ F: Documentation/userspace-api/media/v4l/metafmt-rkisp1.rst
F: drivers/media/platform/rockchip/rkisp1
F: include/uapi/linux/rkisp1-config.h
+ROCKCHIP MFPWM
+M: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+L: linux-rockchip@lists.infradead.org
+L: linux-pwm@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
M: Daniel Golle <daniel@makrotopia.org>
M: Aurelien Jarno <aurelien@aurel32.net>
--
2.53.0
^ permalink raw reply related
* [PATCH v4 2/5] mfd: Add Rockchip mfpwm driver
From: Nicolas Frattaroli @ 2026-04-20 13:35 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio
In-Reply-To: <20260420-rk3576-pwm-v4-0-421738c7bf28@collabora.com>
With the Rockchip RK3576, the PWM IP used by Rockchip has changed
substantially. Looking at both the downstream pwm-rockchip driver as
well as the mainline pwm-rockchip driver made it clear that with all its
additional features and its differences from previous IP revisions, it
is best supported in a new driver.
This brings us to the question as to what such a new driver should be.
To me, it soon became clear that it should actually be several new
drivers, most prominently when Uwe Kleine-König let me know that I
should not implement the pwm subsystem's capture callback, but instead
write a counter driver for this functionality.
Combined with the other as-of-yet unimplemented functionality of this
new IP, it became apparent that it needs to be spread across several
subsystems.
For this reason, we add a new MFD core driver, called mfpwm (short for
"Multi-function PWM"). This "parent" driver makes sure that only one
device function driver is using the device at a time, and is in charge
of registering the MFD cell devices for the individual device functions
offered by the device.
An acquire/release pattern is used to guarantee that device function
drivers don't step on each other's toes.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
MAINTAINERS | 2 +
drivers/mfd/Kconfig | 16 ++
drivers/mfd/Makefile | 1 +
drivers/mfd/rockchip-mfpwm.c | 357 ++++++++++++++++++++++++++++
include/linux/mfd/rockchip-mfpwm.h | 470 +++++++++++++++++++++++++++++++++++++
5 files changed, 846 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 86f20cb563c6..d52731242a33 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23178,6 +23178,8 @@ L: linux-rockchip@lists.infradead.org
L: linux-pwm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F: drivers/mfd/rockchip-mfpwm.c
+F: include/linux/mfd/rockchip-mfpwm.h
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
M: Daniel Golle <daniel@makrotopia.org>
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 7192c9d1d268..80b4e82c4937 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -1378,6 +1378,22 @@ config MFD_RC5T583
Additional drivers must be enabled in order to use the
different functionality of the device.
+config MFD_ROCKCHIP_MFPWM
+ tristate "Rockchip multi-function PWM controller"
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
+ depends on OF
+ depends on HAS_IOMEM
+ depends on COMMON_CLK
+ select MFD_CORE
+ help
+ Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
+ several different functions, such as generating PWM waveforms but also
+ counting waveforms.
+
+ This driver manages the overall device, and selects between different
+ functionalities at runtime as needed. Drivers for them are implemented
+ in their respective subsystems.
+
config MFD_RK8XX
tristate
select MFD_CORE
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..ebadbaea9e4a 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -231,6 +231,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o
obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o
obj-$(CONFIG_MFD_NTXEC) += ntxec.o
obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o
+obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o
obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o
obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o
obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o
diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c
new file mode 100644
index 000000000000..72d04982b961
--- /dev/null
+++ b/drivers/mfd/rockchip-mfpwm.c
@@ -0,0 +1,357 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A driver to manage all the different functionalities exposed by Rockchip's
+ * PWMv4 hardware.
+ *
+ * This driver is chiefly focused on guaranteeing non-concurrent operation
+ * between the different device functions, as well as setting the clocks.
+ * It registers the device function platform devices, e.g. PWM output or
+ * PWM capture.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/overflow.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+
+/**
+ * struct rockchip_mfpwm - private mfpwm driver instance state struct
+ * @pdev: pointer to this instance's &struct platform_device
+ * @base: pointer to the memory mapped registers of this device
+ * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
+ * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
+ * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from
+ * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk
+ * @pclk: pointer to the APB bus clock needed for mmio register access
+ * @active_func: pointer to the currently active device function, or %NULL if no
+ * device function is currently actively using any of the shared
+ * resources. May only be checked/modified with @state_lock held.
+ * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
+ * it. Must only be checked or modified while holding @state_lock.
+ * @state_lock: this lock is held while either the active device function, the
+ * enable register, or the chosen clock is being changed.
+ * @irq: the IRQ number of this device
+ */
+struct rockchip_mfpwm {
+ struct platform_device *pdev;
+ void __iomem *base;
+ struct clk *pwm_clk;
+ struct clk *osc_clk;
+ struct clk *rc_clk;
+ struct clk *chosen_clk;
+ struct clk *pclk;
+ struct rockchip_mfpwm_func *active_func;
+ unsigned int acquire_cnt;
+ spinlock_t state_lock;
+ int irq;
+};
+
+static atomic_t subdev_id = ATOMIC_INIT(0);
+
+static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
+{
+ return platform_get_drvdata(pdev);
+}
+
+static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
+ const char *fname)
+{
+ struct device *dev = &pwmf->parent->pdev->dev;
+
+ if (IS_ERR_OR_NULL(pwmf)) {
+ dev_warn(dev, "called %s with an erroneous handle, no effect\n",
+ fname);
+ return -EINVAL;
+ }
+
+ if (IS_ERR_OR_NULL(pwmf->parent)) {
+ dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
+ fname);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+__attribute__((nonnull))
+static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
+ unsigned int cnt;
+
+ if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
+ return -EBUSY;
+
+ if (!mfpwm->active_func)
+ mfpwm->active_func = pwmf;
+
+ if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
+ mfpwm->acquire_cnt = cnt;
+ } else {
+ dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
+ __func__);
+ return -EOVERFLOW;
+ }
+
+ dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
+ pwmf->id, mfpwm->acquire_cnt);
+
+ return clk_enable(mfpwm->pclk);
+}
+
+int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ unsigned long flags;
+ int ret = 0;
+
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
+ if (ret)
+ return ret;
+
+ mfpwm = pwmf->parent;
+ dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
+
+ if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
+ return -EBUSY;
+
+ ret = mfpwm_do_acquire(pwmf);
+
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM");
+
+__attribute__((nonnull))
+static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
+
+ if (!mfpwm->active_func)
+ return;
+
+ if (mfpwm->active_func->id != pwmf->id)
+ return;
+
+ /*
+ * No need to check_sub_overflow here, !mfpwm->active_func above catches
+ * this type of problem already.
+ */
+ mfpwm->acquire_cnt--;
+
+ if (!mfpwm->acquire_cnt)
+ mfpwm->active_func = NULL;
+
+ clk_disable(mfpwm->pclk);
+}
+
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ unsigned long flags;
+
+ if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
+ return;
+
+ mfpwm = pwmf->parent;
+
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
+ mfpwm_do_release(pwmf);
+ dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
+ pwmf->id, mfpwm->acquire_cnt);
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM");
+
+int mfpwm_get_mode(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ int ret;
+
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
+ if (ret)
+ return ret;
+
+ mfpwm = pwmf->parent;
+
+ guard(spinlock_irqsave)(&mfpwm->state_lock);
+
+ if (!rockchip_pwm_v4_is_enabled(mfpwm_reg_read(mfpwm->base, PWMV4_REG_ENABLE)))
+ return -1;
+
+ return mfpwm_reg_read(mfpwm->base, PWMV4_REG_CTRL) & PWMV4_MODE_MASK;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_get_mode, "ROCKCHIP_MFPWM");
+
+/**
+ * mfpwm_register_subdev - register a single mfpwm_func
+ * @mfpwm: pointer to the parent &struct rockchip_mfpwm
+ * @name: sub-device name string
+ *
+ * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
+ * and register a new mfd cell.
+ *
+ * Returns: 0 on success, negative errno on error
+ */
+static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
+ const char *name)
+{
+ struct rockchip_mfpwm_func *func;
+ struct mfd_cell cell = {};
+
+ func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
+ if (IS_ERR(func))
+ return PTR_ERR(func);
+ func->irq = mfpwm->irq;
+ func->parent = mfpwm;
+ func->id = atomic_inc_return(&subdev_id);
+ func->base = mfpwm->base;
+ func->core = mfpwm->chosen_clk;
+ cell.name = name;
+ cell.platform_data = func;
+ cell.pdata_size = sizeof(*func);
+
+ return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL,
+ 0, NULL);
+}
+
+static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
+{
+ int ret;
+
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-v4");
+ if (ret)
+ return ret;
+
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int rockchip_mfpwm_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct rockchip_mfpwm *mfpwm;
+ char *clk_mux_name;
+ const char *mux_p_names[3];
+ int ret = 0;
+
+ mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL);
+ if (IS_ERR(mfpwm))
+ return PTR_ERR(mfpwm);
+
+ mfpwm->pdev = pdev;
+
+ spin_lock_init(&mfpwm->state_lock);
+
+ mfpwm->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(mfpwm->base))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->base),
+ "failed to ioremap address\n");
+
+ mfpwm->pclk = devm_clk_get_prepared(dev, "pclk");
+ if (IS_ERR(mfpwm->pclk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pclk),
+ "couldn't get and prepare 'pclk' clock\n");
+
+ mfpwm->irq = platform_get_irq(pdev, 0);
+ if (mfpwm->irq < 0)
+ return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n");
+
+ mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm");
+ if (IS_ERR(mfpwm->pwm_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
+ "couldn't get and prepare 'pwm' clock\n");
+
+ mfpwm->osc_clk = devm_clk_get_prepared(dev, "osc");
+ if (IS_ERR(mfpwm->osc_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk),
+ "couldn't get and prepare 'osc' clock\n");
+
+ mfpwm->rc_clk = devm_clk_get_prepared(dev, "rc");
+ if (IS_ERR(mfpwm->rc_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->rc_clk),
+ "couldn't get and prepare 'rc' clock\n");
+
+ clk_mux_name = devm_kasprintf(dev, GFP_KERNEL, "%s_chosen", dev_name(dev));
+ if (!clk_mux_name)
+ return -ENOMEM;
+
+ mux_p_names[0] = __clk_get_name(mfpwm->pwm_clk);
+ mux_p_names[1] = __clk_get_name(mfpwm->osc_clk);
+ mux_p_names[2] = __clk_get_name(mfpwm->rc_clk);
+ mfpwm->chosen_clk = clk_register_mux(dev, clk_mux_name, mux_p_names,
+ ARRAY_SIZE(mux_p_names),
+ CLK_SET_RATE_PARENT,
+ mfpwm->base + PWMV4_REG_CLK_CTRL,
+ PWMV4_CLK_SRC_SHIFT, PWMV4_CLK_SRC_WIDTH,
+ CLK_MUX_HIWORD_MASK, NULL);
+ ret = clk_prepare(mfpwm->chosen_clk);
+ if (ret) {
+ dev_err(dev, "failed to prepare PWM clock mux: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ platform_set_drvdata(pdev, mfpwm);
+
+ ret = mfpwm_register_subdevs(mfpwm);
+ if (ret) {
+ dev_err(dev, "failed to register sub-devices: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ return ret;
+}
+
+static void rockchip_mfpwm_remove(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
+
+ if (mfpwm->chosen_clk) {
+ clk_unprepare(mfpwm->chosen_clk);
+ clk_unregister_mux(mfpwm->chosen_clk);
+ }
+
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+
+static const struct of_device_id rockchip_mfpwm_of_match[] = {
+ {
+ .compatible = "rockchip,rk3576-pwm",
+ },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match);
+
+static struct platform_driver rockchip_mfpwm_driver = {
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .of_match_table = rockchip_mfpwm_of_match,
+ },
+ .probe = rockchip_mfpwm_probe,
+ .remove = rockchip_mfpwm_remove,
+};
+module_platform_driver(rockchip_mfpwm_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip MFPWM Driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/rockchip-mfpwm.h b/include/linux/mfd/rockchip-mfpwm.h
new file mode 100644
index 000000000000..dbf1588a4382
--- /dev/null
+++ b/include/linux/mfd/rockchip-mfpwm.h
@@ -0,0 +1,470 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * Common header file for all the Rockchip Multi-function PWM controller
+ * drivers that are spread across subsystems.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#ifndef __SOC_ROCKCHIP_MFPWM_H__
+#define __SOC_ROCKCHIP_MFPWM_H__
+
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/hw_bitfield.h>
+#include <linux/io.h>
+#include <linux/spinlock.h>
+
+struct rockchip_mfpwm;
+
+/**
+ * struct rockchip_mfpwm_func - struct representing a single function driver
+ *
+ * @id: unique id for this function driver instance
+ * @base: pointer to start of MMIO registers
+ * @parent: a pointer to the parent mfpwm struct
+ * @irq: the shared IRQ gotten from the parent mfpwm device
+ * @core: a pointer to the clk mux that drives this channel's PWM
+ */
+struct rockchip_mfpwm_func {
+ int id;
+ void __iomem *base;
+ struct rockchip_mfpwm *parent;
+ int irq;
+ struct clk *core;
+};
+
+/*
+ * PWMV4 Register Definitions
+ * --------------------------
+ *
+ * Attributes:
+ * RW - Read-Write
+ * RO - Read-Only
+ * WO - Write-Only
+ * W1T - Write high, Self-clearing
+ * W1C - Write high to clear interrupt
+ *
+ * Bit ranges to be understood with Verilog-like semantics,
+ * e.g. [03:00] is 4 bits: 0, 1, 2 and 3.
+ *
+ * All registers must be accessed with 32-bit width accesses only
+ */
+
+#define PWMV4_REG_VERSION 0x000
+/*
+ * VERSION Register Description
+ * [31:24] RO | Hardware Major Version
+ * [23:16] RO | Hardware Minor Version
+ * [15:15] RO | Reserved
+ * [14:14] RO | Hardware supports biphasic counters
+ * [13:13] RO | Hardware supports filters
+ * [12:12] RO | Hardware supports waveform generation
+ * [11:11] RO | Hardware supports counter
+ * [10:10] RO | Hardware supports frequency metering
+ * [09:09] RO | Hardware supports power key functionality
+ * [08:08] RO | Hardware supports infrared transmissions
+ * [07:04] RO | Channel index of this instance
+ * [03:00] RO | Number of channels the base instance supports
+ */
+
+#define PWMV4_REG_ENABLE 0x004
+/*
+ * ENABLE Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:06] RO | Reserved
+ * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled
+ */
+#define PWMV4_CHN_CNT_RD_EN(v) FIELD_PREP_WM16(BIT(5), (v))
+/*
+ * [04:04] W1T | PWM Globally Joined Control Enable
+ * 1 = this PWM channel will be enabled by a global pwm enable
+ * bit instead of the PWM Enable bit.
+ */
+#define PWMV4_GLOBAL_CTRL_EN(v) FIELD_PREP_WM16(BIT(4), (v))
+/*
+ * [03:03] RW | Force Clock Enable
+ * 0 = disabled, if the PWM channel is inactive then so is the
+ * clock prescale module
+ */
+#define PWMV4_FORCE_CLK_EN(v) FIELD_PREP_WM16(BIT(3), (v))
+/*
+ * [02:02] W1T | PWM Control Update Enable
+ * 1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and
+ * _OFFSET registers once 1 is written to it
+ */
+#define PWMV4_CTRL_UPDATE_EN FIELD_PREP_WM16_CONST(BIT(2), 1)
+/*
+ * [01:01] RW | PWM Enable, 1 = enabled
+ * If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_EN_MASK BIT(1)
+#define PWMV4_EN(v) FIELD_PREP_WM16(PWMV4_EN_MASK, \
+ ((v) ? 1 : 0))
+/*
+ * [00:00] RW | PWM Clock Enable, 1 = enabled
+ * If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_CLK_EN_MASK BIT(0)
+#define PWMV4_CLK_EN(v) FIELD_PREP_WM16(PWMV4_CLK_EN_MASK, \
+ ((v) ? 1 : 0))
+#define PWMV4_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK)
+static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val)
+{
+ return (val & PWMV4_EN_BOTH_MASK);
+}
+
+#define PWMV4_REG_CLK_CTRL 0x008
+/*
+ * CLK_CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:15] RW | Clock Global Selection
+ * 0 = current channel scale clock
+ * 1 = global channel scale clock
+ */
+#define PWMV4_CLK_GLOBAL(v) FIELD_PREP_WM16(BIT(15), (v))
+/*
+ * [14:13] RW | Clock Source Selection
+ * 0 = Clock from PLL, frequency can be configured
+ * 1 = Clock from crystal oscillator, frequency is fixed
+ * 2 = Clock from RC oscillator, frequency is fixed
+ * 3 = Reserved
+ * NOTE: The purpose for this clock-mux-outside-CRU construct is
+ * to let the SoC go into a sleep state with the PWM
+ * hardware still having a clock signal for IR input, which
+ * can then wake up the SoC.
+ */
+#define PWMV4_CLK_SRC_PLL 0x0U
+#define PWMV4_CLK_SRC_CRYSTAL 0x1U
+#define PWMV4_CLK_SRC_RC 0x2U
+#define PWMV4_CLK_SRC_SHIFT 13
+#define PWMV4_CLK_SRC_WIDTH 2
+/*
+ * [12:04] RW | Scale Factor to apply to pre-scaled clock
+ * 1 <= v <= 256, v means clock divided by 2*v
+ */
+#define PWMV4_CLK_SCALE_F(v) FIELD_PREP_WM16(GENMASK(12, 4), (v))
+/*
+ * [03:03] RO | Reserved
+ * [02:00] RW | Prescale Factor
+ * v here means the input clock is divided by pow(2, v)
+ */
+#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v))
+
+#define PWMV4_REG_CTRL 0x00C
+/*
+ * CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:09] RO | Reserved
+ * [08:06] RW | PWM Input Channel Selection
+ * By default, the channel selects its own input, but writing v
+ * here selects PWM input from channel v instead.
+ */
+#define PWMV4_CTRL_IN_SEL(v) FIELD_PREP_WM16(GENMASK(8, 6), (v))
+/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */
+#define PWMV4_CTRL_UNALIGNED(v) FIELD_PREP_WM16(BIT(5), (v))
+/* [04:04] RW | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */
+#define PWMV4_LEFT_ALIGNED 0x0U
+#define PWMV4_CENTRE_ALIGNED 0x1U
+#define PWMV4_CTRL_OUT_MODE(v) FIELD_PREP_WM16(BIT(4), (v))
+/*
+ * [03:03] RW | Inactive Polarity for when the channel is either disabled or
+ * has completed outputting the entire waveform in one-shot mode.
+ * 0 = Negative, 1 = Positive
+ */
+#define PWMV4_POLARITY_N 0x0U
+#define PWMV4_POLARITY_P 0x1U
+#define PWMV4_INACTIVE_POL(v) FIELD_PREP_WM16(BIT(3), (v))
+/*
+ * [02:02] RW | Duty Cycle Polarity to use at the start of the waveform.
+ * 0 = Negative, 1 = Positive
+ */
+#define PWMV4_DUTY_POL_SHIFT 2
+#define PWMV4_DUTY_POL_MASK BIT(PWMV4_DUTY_POL_SHIFT)
+#define PWMV4_DUTY_POL(v) FIELD_PREP_WM16(PWMV4_DUTY_POL_MASK, \
+ (v))
+/*
+ * [01:00] RW | PWM Mode
+ * 0 = One-shot mode, PWM generates waveform RPT times
+ * 1 = Continuous mode
+ * 2 = Capture mode, PWM measures cycles of input waveform
+ * 3 = Reserved
+ */
+#define PWMV4_MODE_ONESHOT 0x0U
+#define PWMV4_MODE_CONT 0x1U
+#define PWMV4_MODE_CAPTURE 0x2U
+#define PWMV4_MODE_MASK GENMASK(1, 0)
+#define PWMV4_MODE(v) FIELD_PREP_WM16(PWMV4_MODE_MASK, (v))
+#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \
+ PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \
+ PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \
+ PWMV4_CTRL_UNALIGNED(true))
+#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \
+ PWMV4_CTRL_COM_FLAGS)
+#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \
+ PWMV4_CTRL_COM_FLAGS)
+
+#define PWMV4_REG_PERIOD 0x010
+/*
+ * PERIOD Register Description
+ * [31:00] RW | Period of the output waveform
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_DUTY 0x014
+/*
+ * DUTY Register Description
+ * [31:00] RW | Duty cycle of the output waveform
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_OFFSET 0x018
+/*
+ * OFFSET Register Description
+ * [31:00] RW | Offset of the output waveform, based on the PWM clock
+ * Constraints: 0 <= v <= (PERIOD - DUTY)
+ */
+
+#define PWMV4_REG_RPT 0x01C
+/*
+ * RPT Register Description
+ * [31:16] RW | Second dimensional of the effective number of waveform
+ * repetitions. Increases by one every first dimensional times.
+ * Value `n` means `n + 1` repetitions. The final number of
+ * repetitions of the waveform in one-shot mode is:
+ * `(first_dimensional + 1) * (second_dimensional + 1)`
+ * [15:00] RW | First dimensional of the effective number of waveform
+ * repetitions. Value `n` means `n + 1` repetitions.
+ */
+
+#define PWMV4_REG_FILTER_CTRL 0x020
+/*
+ * FILTER_CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:04] RW | Filter window number
+ * [03:01] RO | Reserved
+ * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled
+ */
+
+#define PWMV4_REG_CNT 0x024
+/*
+ * CNT Register Description
+ * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles,
+ * 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_ENABLE_DELAY 0x028
+/*
+ * ENABLE_DELAY Register Description
+ * [31:16] RO | Reserved
+ * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles
+ */
+
+#define PWMV4_REG_HPC 0x02C
+/*
+ * HPC Register Description
+ * [31:00] RW | Number of effective high polarity cycles of the input waveform
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_LPC 0x030
+/*
+ * LPC Register Description
+ * [31:00] RW | Number of effective low polarity cycles of the input waveform
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040
+/*
+ * BIPHASIC_CNT_CTRL0 Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0,
+ * 0 = rising edge (posedge), 1 = falling edge (negedge)
+ * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable
+ * [07:07] W1T | Synchronous Enable
+ * [06:06] W1T | Mode Switch
+ * 0 = Normal Mode, 1 = Switch timer clock and measured clock
+ * Constraints: "Biphasic Counter Mode" must be 0 if this is 1
+ * [05:03] RW | Biphasic Counter Mode
+ * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3,
+ * 0x4 = Mode 4, 0x5 = Reserved
+ * [02:02] RW | Biphasic Counter Clock Selection
+ * 0 = clock is from PLL and frequency can be configured
+ * 1 = clock is from crystal oscillator and frequency is fixed
+ * [01:01] RW | Biphasic Counter Continuous Mode
+ * [00:00] W1T | Biphasic Counter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044
+/*
+ * BIPHASIC_CNT_CTRL1 Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:11] RO | Reserved
+ * [10:04] RW | Biphasic Counter Filter Window Number
+ * [03:01] RO | Reserved
+ * [00:00] RW | Biphasic Counter Filter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048
+/*
+ * BIPHASIC_CNT_TIMER Register Description
+ * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter
+ * timer clock cycles
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C
+/*
+ * BIPHASIC_CNT_RES Register Description
+ * [31:00] RO | Biphasic Counter Result Value
+ * Constraints: Can only be read after INTSTS[9] is asserted
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050
+/*
+ * BIPHASIC_CNT_RES_S Register Description
+ * [31:00] RO | Biphasic Counter Result Value with synchronised processing
+ * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1
+ */
+
+#define PWMV4_REG_INTSTS 0x070
+/*
+ * INTSTS Register Description
+ * [31:10] RO | Reserved
+ * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted
+ * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted
+ * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted
+ * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted
+ * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted
+ * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted
+ * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted
+ * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted
+ * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted
+ * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted
+ */
+#define PWMV4_INT_LPC BIT(0)
+#define PWMV4_INT_HPC BIT(1)
+#define PWMV4_INT_LPC_W(v) FIELD_PREP_WM16(PWMV4_INT_LPC, \
+ ((v) ? 1 : 0))
+#define PWMV4_INT_HPC_W(v) FIELD_PREP_WM16(PWMV4_INT_HPC, \
+ ((v) ? 1 : 0))
+
+#define PWMV4_REG_INT_EN 0x074
+/*
+ * INT_EN Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled
+ * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled
+ * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled
+ * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled
+ * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled
+ * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled
+ * [03:03] W1C | Reload Interrupt Enable, 1 = enabled
+ * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled
+ * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled
+ * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled
+ */
+
+#define PWMV4_REG_INT_MASK 0x078
+/*
+ * INT_MASK Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked
+ * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked
+ * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked
+ * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked
+ * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked
+ * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked
+ * [03:03] W1C | Reload Interrupt Masked, 1 = masked
+ * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked
+ * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked
+ * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked
+ */
+
+static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg)
+{
+ return readl(base + reg);
+}
+
+static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val)
+{
+ writel(val, base + reg);
+}
+
+/**
+ * mfpwm_acquire - try becoming the active mfpwm function device
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * mfpwm device "function" drivers must call this function before doing anything
+ * that either modifies or relies on the parent device's state, such as clocks,
+ * enabling/disabling outputs, modifying shared regs etc.
+ *
+ * The return statues should always be checked.
+ *
+ * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release()
+ * calls once the device is no longer making changes that affect other devices,
+ * or stops producing user-visible effects that depend on the current device
+ * state being kept as-is. (e.g. after the PWM output signal is stopped)
+ *
+ * The same device function may mfpwm_acquire() multiple times while it already
+ * is active, i.e. it is re-entrant, though it needs to balance this with the
+ * same number of mfpwm_release() calls.
+ *
+ * Context: This function does not sleep.
+ *
+ * Return:
+ * * %0 - success
+ * * %-EBUSY - a different device function is active
+ * * %-EOVERFLOW - the acquire counter is at its maximum
+ */
+extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_release - drop usage of active mfpwm device function by 1
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * This is the balancing call to mfpwm_acquire(). If no users of the device
+ * function remain, set the mfpwm device to have no active device function,
+ * allowing other device functions to claim it.
+ */
+extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_get_mode - get the current mode the hardware is in
+ * @pwmf: pointer to a &struct rockchip_mfpwm_func
+ *
+ * Check the hardware registers of the PWM hardware to determine which mode it
+ * is currently operating in, if any.
+ *
+ * Returns:
+ * - %-EINVAL if @pwmf is %NULL or an error pointer
+ * - %-1 if the PWM hardware is off, regardless of operating mode
+ * - %PWMV4_MODE_ONESHOT if PWM hardware is in one-shot output mode
+ * - %PWMV4_MODE_CONT if PWM hardware is in continuous output mode
+ * - %PWMV4_MODE_CAPTURE if PWM hardware is in capture mode
+ */
+extern int mfpwm_get_mode(const struct rockchip_mfpwm_func *pwmf);
+
+#endif /* __SOC_ROCKCHIP_MFPWM_H__ */
--
2.53.0
^ permalink raw reply related
* [PATCH v4 4/5] arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi
From: Nicolas Frattaroli @ 2026-04-20 13:35 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio
In-Reply-To: <20260420-rk3576-pwm-v4-0-421738c7bf28@collabora.com>
The RK3576 SoC features three distinct PWM controllers, with variable
numbers of channels. Add each channel as a separate node to the SoC's
device tree, as they don't really overlap in register ranges.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
arch/arm64/boot/dts/rockchip/rk3576.dtsi | 208 +++++++++++++++++++++++++++++++
1 file changed, 208 insertions(+)
diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
index e12a2a0cfb89..55d6b103c329 100644
--- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi
+++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
@@ -1032,6 +1032,32 @@ uart1: serial@27310000 {
status = "disabled";
};
+ pwm0_2ch_0: pwm@27330000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x27330000 0x0 0x1000>;
+ clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>,
+ <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm0m0_ch0>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm0_2ch_1: pwm@27331000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x27331000 0x0 0x1000>;
+ clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>,
+ <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm0m0_ch1>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
pmu: power-management@27380000 {
compatible = "rockchip,rk3576-pmu", "syscon", "simple-mfd";
reg = <0x0 0x27380000 0x0 0x800>;
@@ -2630,6 +2656,188 @@ uart9: serial@2adc0000 {
status = "disabled";
};
+ pwm1_6ch_0: pwm@2add0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch0>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_1: pwm@2add1000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add1000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch1>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_2: pwm@2add2000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add2000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 104 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch2>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_3: pwm@2add3000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add3000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 105 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch3>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_4: pwm@2add4000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add4000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch4>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_5: pwm@2add5000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add5000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 107 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch5>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_0: pwm@2ade0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 108 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch0>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_1: pwm@2ade1000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade1000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 109 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch1>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_2: pwm@2ade2000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade2000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 110 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch2>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_3: pwm@2ade3000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade3000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 111 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch3>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_4: pwm@2ade4000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade4000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 112 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch4>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_5: pwm@2ade5000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade5000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 113 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch5>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_6: pwm@2ade6000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade6000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch6>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_7: pwm@2ade7000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade7000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch7>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
saradc: adc@2ae00000 {
compatible = "rockchip,rk3576-saradc", "rockchip,rk3588-saradc";
reg = <0x0 0x2ae00000 0x0 0x10000>;
--
2.53.0
^ permalink raw reply related
* [PATCH v4 3/5] pwm: Add rockchip PWMv4 driver
From: Nicolas Frattaroli @ 2026-04-20 13:35 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio
In-Reply-To: <20260420-rk3576-pwm-v4-0-421738c7bf28@collabora.com>
The Rockchip RK3576 brings with it a new PWM IP, in downstream code
referred to as "v4". This new IP is different enough from the previous
Rockchip IP that I felt it necessary to add a new driver for it, instead
of shoehorning it in the old one.
Add this new driver, based on the PWM core's waveform APIs. Its platform
device is registered by the parent mfpwm driver, from which it also
receives a little platform data struct, so that mfpwm can guarantee that
all the platform device drivers spread across different subsystems for
this specific hardware IP do not interfere with each other.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
MAINTAINERS | 2 +
drivers/counter/Kconfig | 11 +
drivers/counter/Makefile | 1 +
drivers/counter/rockchip-pwm-capture.c | 307 ++++++++++++++++++++++++++
drivers/pwm/Kconfig | 11 +
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-rockchip-v4.c | 383 +++++++++++++++++++++++++++++++++
7 files changed, 716 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index d52731242a33..3f72784dd5bc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23178,7 +23178,9 @@ L: linux-rockchip@lists.infradead.org
L: linux-pwm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F: drivers/counter/rockchip-pwm-capture.c
F: drivers/mfd/rockchip-mfpwm.c
+F: drivers/pwm/pwm-rockchip-v4.c
F: include/linux/mfd/rockchip-mfpwm.h
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig
index d30d22dfe577..85adeb41aeed 100644
--- a/drivers/counter/Kconfig
+++ b/drivers/counter/Kconfig
@@ -90,6 +90,17 @@ config MICROCHIP_TCB_CAPTURE
To compile this driver as a module, choose M here: the
module will be called microchip-tcb-capture.
+config ROCKCHIP_PWM_CAPTURE
+ tristate "Rockchip PWM Counter Capture driver"
+ depends on MFD_ROCKCHIP_MFPWM
+ help
+ Generic counter framework driver for the multi-function PWM on
+ Rockchip SoCs such as the RK3576.
+
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
+ to guarantee exclusive operation with other functions of the same
+ device implemented by drivers in other subsystems.
+
config RZ_MTU3_CNT
tristate "Renesas RZ/G2L MTU3a counter driver"
depends on RZ_MTU3
diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile
index fa3c1d08f706..2bfcfc2c584b 100644
--- a/drivers/counter/Makefile
+++ b/drivers/counter/Makefile
@@ -17,3 +17,4 @@ obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec.o
obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o
obj-$(CONFIG_INTEL_QEP) += intel-qep.o
obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o
+obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o
diff --git a/drivers/counter/rockchip-pwm-capture.c b/drivers/counter/rockchip-pwm-capture.c
new file mode 100644
index 000000000000..09a92f2bc409
--- /dev/null
+++ b/drivers/counter/rockchip-pwm-capture.c
@@ -0,0 +1,307 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A counter driver for the Pulse-Width-Modulation (PWM) hardware found on
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". It
+ * allows for measuring the high cycles and low cycles of a PWM signal through
+ * the generic counter framework, while guaranteeing exclusive use over the
+ * MFPWM device while the counter is enabled.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#include <linux/cleanup.h>
+#include <linux/counter.h>
+#include <linux/devm-helpers.h>
+#include <linux/interrupt.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/mod_devicetable.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+
+#define RKPWMC_INT_MASK (PWMV4_INT_LPC | PWMV4_INT_HPC)
+
+struct rockchip_pwm_capture {
+ struct rockchip_mfpwm_func *pwmf;
+ struct counter_device *counter;
+};
+
+static struct counter_signal rkpwmc_signals[] = {
+ {
+ .id = 0,
+ .name = "PWM Clock"
+ },
+};
+
+static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = {
+ COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
+ COUNTER_SYNAPSE_ACTION_NONE,
+};
+
+static struct counter_synapse rkpwmc_pwm_synapses[] = {
+ {
+ .actions_list = rkpwmc_hpc_lpc_actions,
+ .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
+ .signal = &rkpwmc_signals[0]
+ },
+};
+
+static const enum counter_function rkpwmc_functions[] = {
+ COUNTER_FUNCTION_INCREASE,
+};
+
+static inline bool rkpwmc_is_enabled(struct rockchip_mfpwm_func *pwmf)
+{
+ return mfpwm_get_mode(pwmf) == PWMV4_MODE_CAPTURE;
+}
+
+static bool rkpwmc_acquire_if_enabled(struct rockchip_pwm_capture *pc)
+{
+ int ret;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret < 0)
+ return false;
+
+ if (rkpwmc_is_enabled(pc->pwmf))
+ return true;
+
+ mfpwm_release(pc->pwmf);
+
+ return false;
+}
+
+static int rkpwmc_enable_read(struct counter_device *counter,
+ struct counter_count *count,
+ u8 *enable)
+{
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
+
+ *enable = rkpwmc_is_enabled(pc->pwmf);
+
+ return 0;
+}
+
+static int rkpwmc_enable_write(struct counter_device *counter,
+ struct counter_count *count,
+ u8 enable)
+{
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
+ int ret;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ return ret;
+
+ if (!!enable != rkpwmc_is_enabled(pc->pwmf)) {
+ if (enable) {
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_EN(false));
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL,
+ PWMV4_CTRL_CAP_FLAGS);
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
+ PWMV4_INT_LPC_W(true) |
+ PWMV4_INT_HPC_W(true));
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_EN(true) | PWMV4_CLK_EN(true));
+
+ ret = clk_enable(pc->pwmf->core);
+ if (ret)
+ goto err_release;
+
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
+ if (ret)
+ goto err_disable_pwm_clk;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ goto err_unprotect_pwm_clk;
+ } else {
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
+ PWMV4_INT_LPC_W(false) |
+ PWMV4_INT_HPC_W(false));
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_EN(false) | PWMV4_CLK_EN(false));
+ clk_rate_exclusive_put(pc->pwmf->core);
+ clk_disable(pc->pwmf->core);
+ mfpwm_release(pc->pwmf);
+ }
+ }
+
+ mfpwm_release(pc->pwmf);
+
+ return 0;
+
+err_unprotect_pwm_clk:
+ clk_rate_exclusive_put(pc->pwmf->core);
+err_disable_pwm_clk:
+ clk_disable(pc->pwmf->core);
+err_release:
+ mfpwm_release(pc->pwmf);
+
+ return ret;
+}
+
+static struct counter_comp rkpwmc_ext[] = {
+ COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write),
+};
+
+enum rkpwmc_count_id {
+ COUNT_LPC = 0,
+ COUNT_HPC = 1,
+};
+
+static struct counter_count rkpwmc_counts[] = {
+ {
+ .id = COUNT_LPC,
+ .name = "Low Polarity Capture",
+ .functions_list = rkpwmc_functions,
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
+ .synapses = rkpwmc_pwm_synapses,
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
+ .ext = rkpwmc_ext,
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
+ },
+ {
+ .id = COUNT_HPC,
+ .name = "High Polarity Capture",
+ .functions_list = rkpwmc_functions,
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
+ .synapses = rkpwmc_pwm_synapses,
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
+ .ext = rkpwmc_ext,
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
+ },
+};
+
+static int rkpwmc_count_read(struct counter_device *counter,
+ struct counter_count *count, u64 *value)
+{
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
+
+ switch (count->id) {
+ case COUNT_LPC:
+ if (rkpwmc_acquire_if_enabled(pc)) {
+ *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_LPC);
+ mfpwm_release(pc->pwmf);
+ } else {
+ *value = 0;
+ }
+ return 0;
+ case COUNT_HPC:
+ if (rkpwmc_acquire_if_enabled(pc)) {
+ *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_HPC);
+ mfpwm_release(pc->pwmf);
+ } else {
+ *value = 0;
+ }
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct counter_ops rkpwmc_ops = {
+ .count_read = rkpwmc_count_read,
+};
+
+static irqreturn_t rkpwmc_irq_handler(int irq, void *data)
+{
+ struct rockchip_pwm_capture *pc = data;
+ u32 intsts;
+ u32 clr = 0;
+
+ intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS);
+
+ if (!(intsts & RKPWMC_INT_MASK))
+ return IRQ_NONE;
+
+ if (intsts & PWMV4_INT_LPC) {
+ clr |= PWMV4_INT_LPC;
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
+ }
+
+ if (intsts & PWMV4_INT_HPC) {
+ clr |= PWMV4_INT_HPC;
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 1);
+ }
+
+ if (clr)
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr);
+
+ /* If other interrupt status bits are set, they're not for this driver */
+ if (intsts != clr)
+ return IRQ_NONE;
+
+ return IRQ_HANDLED;
+}
+
+static int rockchip_pwm_capture_probe(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
+ struct rockchip_pwm_capture *pc;
+ struct counter_device *counter;
+ int ret;
+
+ /* Set our (still unset) OF node to the parent MFD device's OF node */
+ pdev->dev.parent->of_node_reused = true;
+ device_set_node(&pdev->dev,
+ of_fwnode_handle(no_free_ptr(pdev->dev.parent->of_node)));
+
+ counter = devm_counter_alloc(&pdev->dev, sizeof(*pc));
+ if (IS_ERR(counter))
+ return PTR_ERR(counter);
+
+ pc = counter_priv(counter);
+ pc->pwmf = pwmf;
+
+ platform_set_drvdata(pdev, pc);
+
+ /* If the counter is on at module probe, acquire it */
+ rkpwmc_acquire_if_enabled(pc);
+
+ counter->name = pdev->name;
+ counter->signals = rkpwmc_signals;
+ counter->num_signals = ARRAY_SIZE(rkpwmc_signals);
+ counter->ops = &rkpwmc_ops;
+ counter->counts = rkpwmc_counts;
+ counter->num_counts = ARRAY_SIZE(rkpwmc_counts);
+
+ pc->counter = counter;
+
+ ret = devm_counter_add(&pdev->dev, counter);
+ if (ret < 0)
+ return dev_err_probe(&pdev->dev, ret, "Failed to add counter\n");
+
+ ret = devm_request_irq(&pdev->dev, pwmf->irq, rkpwmc_irq_handler,
+ IRQF_SHARED, pdev->name, pc);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n");
+
+ return 0;
+}
+
+static const struct platform_device_id rockchip_pwm_capture_id_table[] = {
+ { .name = "rockchip-pwm-capture", },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_capture_id_table);
+
+static struct platform_driver rockchip_pwm_capture_driver = {
+ .probe = rockchip_pwm_capture_probe,
+ .id_table = rockchip_pwm_capture_id_table,
+ .driver = {
+ .name = "rockchip-pwm-capture",
+ },
+};
+module_platform_driver(rockchip_pwm_capture_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
+MODULE_IMPORT_NS("COUNTER");
+MODULE_ALIAS("platform:rockchip-pwm-capture");
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 6f3147518376..3fe7993bf12b 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -625,6 +625,17 @@ config PWM_ROCKCHIP
Generic PWM framework driver for the PWM controller found on
Rockchip SoCs.
+config PWM_ROCKCHIP_V4
+ tristate "Rockchip PWM v4 support"
+ depends on MFD_ROCKCHIP_MFPWM
+ help
+ Generic PWM framework driver for the PWM controller found on
+ later Rockchip SoCs such as the RK3576.
+
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
+ to guarantee fearlessly concurrent operation with other functions of
+ the same device implemented by drivers in other subsystems.
+
config PWM_SAMSUNG
tristate "Samsung PWM support"
depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 0dc0d2b69025..a234027dbbc6 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -56,6 +56,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o
obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o
obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
+obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
new file mode 100644
index 000000000000..b7de72c433c5
--- /dev/null
+++ b/drivers/pwm/pwm-rockchip-v4.c
@@ -0,0 +1,383 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
+ * the MFPWM infrastructure to guarantee exclusive use over the device without
+ * other functions of the device from different drivers interfering with its
+ * operation while it's active.
+ *
+ * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which
+ * uses the same PWM hardware and has a publicly available TRM.
+ * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ *
+ * Limitations:
+ * - The hardware supports both completing the currently running period
+ * on disable (by switching to oneshot mode with a single repetition and
+ * only disable when the complete irq fires), and abrupt disable (freeze).
+ * Only the latter is implemented in the driver.
+ * - When the output is disabled, the pin will remain driven to whatever state
+ * it last had.
+ * - Adjustments to the duty cycle will only take effect during the next period.
+ * - Adjustments to the period length will only take effect during the next
+ * period.
+ * - The hardware only supports offsets in [0, period - duty_cycle]
+ */
+
+#include <linux/math64.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+
+struct rockchip_pwm_v4 {
+ struct rockchip_mfpwm_func *pwmf;
+ struct pwm_chip chip;
+};
+
+struct __packed rockchip_pwm_v4_wf {
+ u32 period;
+ u32 duty;
+ u32 offset;
+ unsigned long rate;
+};
+
+static inline struct rockchip_pwm_v4 *to_rockchip_pwm_v4(struct pwm_chip *chip)
+{
+ return pwmchip_get_drvdata(chip);
+}
+
+/**
+ * rockchip_pwm_v4_round_single - convert a PWM parameter to hardware
+ * @rate: clock rate of the PWM clock, as per clk_get_rate
+ * Assumed to be <= 1GHz for overflow considerations
+ * @in_val: parameter in nanoseconds to convert
+ *
+ * Returns the rounded value, saturating at U32_MAX if too large
+ */
+static u32 rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val)
+{
+ u64 tmp;
+
+ tmp = mul_u64_u64_div_u64(rate, in_val, NSEC_PER_SEC);
+ if (tmp > U32_MAX)
+ tmp = U32_MAX;
+
+ return tmp;
+}
+
+/**
+ * rockchip_pwm_v4_round_params - convert PWM parameters to hardware
+ * @rate: PWM clock rate to do the calculations at
+ * @wf: pointer to the generic &struct pwm_waveform input parameters
+ * @wfhw: pointer to the hardware-specific &struct rockchip_pwm_v4_wf output
+ * parameters that the results will be stored in
+ *
+ * Convert nanosecond-based duty/period/offset parameters to the PWM hardware's
+ * native rounded representation in number of cycles at clock rate @rate. Should
+ * any of the input parameters be out of range for the hardware, the
+ * corresponding output parameter is the maximum permissible value for said
+ * parameter with considerations to the others.
+ */
+static void rockchip_pwm_v4_round_params(unsigned long rate,
+ const struct pwm_waveform *wf,
+ struct rockchip_pwm_v4_wf *wfhw)
+{
+ wfhw->period = rockchip_pwm_v4_round_single(rate, wf->period_length_ns);
+
+ wfhw->duty = rockchip_pwm_v4_round_single(rate, wf->duty_length_ns);
+
+ /* As per TRM, PWM_OFFSET: "The value ranges from 0 to (period-duty)" */
+ wfhw->offset = rockchip_pwm_v4_round_single(rate, wf->duty_offset_ns);
+ if (!wfhw->period) /* Don't underflow when pwm disabled */
+ wfhw->offset = 0;
+ else if (wfhw->offset > wfhw->period - wfhw->duty)
+ wfhw->offset = wfhw->period - wfhw->duty;
+}
+
+static int rockchip_pwm_v4_round_wf_tohw(struct pwm_chip *chip,
+ struct pwm_device *pwm,
+ const struct pwm_waveform *wf,
+ void *_wfhw)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ unsigned long rate;
+
+ rate = clk_get_rate(pc->pwmf->core);
+
+ /*
+ * It's unlikely this code path is ever taken, as current hardware does
+ * not expose a clock that comes anywhere close to 1GHz. However, in
+ * order to avoid even a theoretical overflow in parameter rounding,
+ * error out if this ever happens to be the case.
+ */
+ if (rate > NSEC_PER_SEC)
+ return -ERANGE;
+
+ rockchip_pwm_v4_round_params(rate, wf, wfhw);
+
+ if (wf->period_length_ns > 0)
+ wfhw->rate = rate;
+ else
+ wfhw->rate = 0;
+
+ dev_dbg(&chip->dev,
+ "tohw: pwm#%u: %lld/%lld [+%lld] @%lu -> DUTY: %08x, PERIOD: %08x, OFFSET: %08x\n",
+ pwm->hwpwm, wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
+ rate, wfhw->duty, wfhw->period, wfhw->offset);
+
+ return 0;
+}
+
+static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip,
+ struct pwm_device *pwm,
+ const void *_wfhw,
+ struct pwm_waveform *wf)
+{
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ unsigned long rate = wfhw->rate;
+
+ if (rate) {
+ wf->period_length_ns = DIV_ROUND_UP((u64)wfhw->period * NSEC_PER_SEC, rate);
+ wf->duty_length_ns = DIV_ROUND_UP((u64)wfhw->duty * NSEC_PER_SEC, rate);
+ wf->duty_offset_ns = DIV_ROUND_UP((u64)wfhw->offset * NSEC_PER_SEC, rate);
+ } else {
+ wf->period_length_ns = 0;
+ wf->duty_length_ns = 0;
+ wf->duty_offset_ns = 0;
+ }
+
+ dev_dbg(&chip->dev,
+ "fromhw: pwm#%u: DUTY: %08x, PERIOD: %08x, OFFSET: %08x @%lu -> %lld/%lld [+%lld]\n",
+ pwm->hwpwm, wfhw->duty, wfhw->period, wfhw->offset, rate,
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns);
+
+ return 0;
+}
+
+static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
+ void *_wfhw)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ unsigned long rate;
+ int ret;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ return ret;
+
+ rate = clk_get_rate(pc->pwmf->core);
+
+ wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
+ wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
+ wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
+ if (rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE)))
+ wfhw->rate = rate;
+ else
+ wfhw->rate = 0;
+
+ mfpwm_release(pc->pwmf);
+
+ return 0;
+}
+
+static int rockchip_pwm_v4_write_wf(struct pwm_chip *chip, struct pwm_device *pwm,
+ const void *_wfhw)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ bool was_enabled;
+ int ret;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ return ret;
+
+ was_enabled = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
+ PWMV4_REG_ENABLE));
+
+ /*
+ * "But Nicolas", you ask with valid concerns, "why would you enable the
+ * PWM before setting all the parameter registers?"
+ *
+ * Excellent question, Mr. Reader M. Strawman! The RK3576 TRM Part 1
+ * Section 34.6.3 specifies that this is the intended order of writes.
+ * Doing the PWM_EN and PWM_CLK_EN writes after the params but before
+ * the CTRL_UPDATE_EN, or even after the CTRL_UPDATE_EN, results in
+ * erratic behaviour where repeated turning on and off of the PWM may
+ * not turn it off under all circumstances. This is also why we don't
+ * use relaxed writes; it's not worth the footgun.
+ */
+ if (wfhw->rate)
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK,
+ PWMV4_EN_BOTH_MASK));
+ else
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK, 0));
+
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_PERIOD, wfhw->period);
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_DUTY, wfhw->duty);
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_OFFSET, wfhw->offset);
+
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, PWMV4_CTRL_CONT_FLAGS);
+
+ /* Commit new configuration to hardware output. */
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_CTRL_UPDATE_EN);
+
+ if (wfhw->rate) {
+ if (!was_enabled) {
+ dev_dbg(&chip->dev, "Enabling PWM output\n");
+ ret = clk_enable(pc->pwmf->core);
+ if (ret)
+ goto err_mfpwm_release;
+ ret = clk_set_rate_exclusive(pc->pwmf->core, wfhw->rate);
+ if (ret) {
+ clk_disable(pc->pwmf->core);
+ goto err_mfpwm_release;
+ }
+
+ /*
+ * Output should be on now, acquire device to guarantee
+ * exclusion with other device functions while it's on.
+ *
+ * It's highly unlikely that this fails, as mfpwm has
+ * already been acquired before, and this is just a
+ * usage counter increase. Not worth the added
+ * complexity of clearing the PWMV4_REG_ENABLE again,
+ * especially considering the CTRL_UPDATE_EN behaviour.
+ */
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret) {
+ clk_rate_exclusive_put(pc->pwmf->core);
+ clk_disable(pc->pwmf->core);
+ goto err_mfpwm_release;
+ }
+ }
+ } else if (was_enabled) {
+ dev_dbg(&chip->dev, "Disabling PWM output\n");
+ clk_rate_exclusive_put(pc->pwmf->core);
+ clk_disable(pc->pwmf->core);
+ /* Output is off now, extra release to balance extra acquire */
+ mfpwm_release(pc->pwmf);
+ }
+
+err_mfpwm_release:
+ mfpwm_release(pc->pwmf);
+
+ return ret;
+}
+
+static const struct pwm_ops rockchip_pwm_v4_ops = {
+ .sizeof_wfhw = sizeof(struct rockchip_pwm_v4_wf),
+ .round_waveform_tohw = rockchip_pwm_v4_round_wf_tohw,
+ .round_waveform_fromhw = rockchip_pwm_v4_round_wf_fromhw,
+ .read_waveform = rockchip_pwm_v4_read_wf,
+ .write_waveform = rockchip_pwm_v4_write_wf,
+};
+
+static bool rockchip_pwm_v4_on_and_continuous(struct rockchip_pwm_v4 *pc)
+{
+ bool en;
+ u32 val;
+
+ en = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
+ PWMV4_REG_ENABLE));
+ val = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_CTRL);
+
+ return en && ((val & PWMV4_MODE_MASK) == PWMV4_MODE_CONT);
+}
+
+static int rockchip_pwm_v4_probe(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
+ struct rockchip_pwm_v4 *pc;
+ struct pwm_chip *chip;
+ struct device *dev = &pdev->dev;
+ int ret;
+
+ /*
+ * For referencing the PWM in the DT to work, we need the parent MFD
+ * device's OF node.
+ */
+ dev->of_node_reused = true;
+ device_set_node(dev, of_fwnode_handle(dev->parent->of_node));
+
+ chip = devm_pwmchip_alloc(dev, 1, sizeof(*pc));
+ if (IS_ERR(chip))
+ return PTR_ERR(chip);
+
+ pc = to_rockchip_pwm_v4(chip);
+ pc->pwmf = pwmf;
+
+ ret = mfpwm_acquire(pwmf);
+ if (ret)
+ return dev_err_probe(dev, ret, "Couldn't acquire mfpwm in probe\n");
+
+ if (!rockchip_pwm_v4_on_and_continuous(pc))
+ mfpwm_release(pwmf);
+ else {
+ dev_dbg(dev, "PWM was already on at probe time\n");
+ ret = clk_enable(pwmf->core);
+ if (ret) {
+ dev_err_probe(dev, ret, "Enabling pwm clock failed\n");
+ goto err_mfpwm_release;
+ }
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
+ if (ret) {
+ dev_err_probe(dev, ret, "Protecting pwm clock failed\n");
+ goto err_clk_disable;
+ }
+ }
+
+ platform_set_drvdata(pdev, chip);
+
+ chip->ops = &rockchip_pwm_v4_ops;
+
+ ret = devm_pwmchip_add(dev, chip);
+ if (ret) {
+ dev_err_probe(dev, ret, "Failed to add PWM chip\n");
+ if (rockchip_pwm_v4_on_and_continuous(pc))
+ goto err_rate_put;
+
+ return ret;
+ }
+
+ return 0;
+
+err_rate_put:
+ clk_rate_exclusive_put(pwmf->core);
+err_clk_disable:
+ clk_disable(pwmf->core);
+err_mfpwm_release:
+ mfpwm_release(pwmf);
+
+ return ret;
+}
+
+static const struct platform_device_id rockchip_pwm_v4_ids[] = {
+ { .name = "rockchip-pwm-v4", },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_v4_ids);
+
+static struct platform_driver rockchip_pwm_v4_driver = {
+ .probe = rockchip_pwm_v4_probe,
+ .driver = {
+ .name = "rockchip-pwm-v4",
+ },
+ .id_table = rockchip_pwm_v4_ids,
+};
+module_platform_driver(rockchip_pwm_v4_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
+MODULE_ALIAS("platform:pwm-rockchip-v4");
--
2.53.0
^ permalink raw reply related
* [PATCH v4 5/5] arm64: dts: rockchip: Add cooling fan to ROCK 4D
From: Nicolas Frattaroli @ 2026-04-20 13:35 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio
In-Reply-To: <20260420-rk3576-pwm-v4-0-421738c7bf28@collabora.com>
The ROCK 4D has a header to connect a small cooling fan. This fan is
driven by one of the SoC's PWM outputs driving a transistor, that in
turn controls the fan's power.
With the introduction of PWM support, add a description of this cooling
fan, as well as the additional trips and cooling-maps for it.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts | 50 +++++++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts
index 899a84b1fbf9..2d5ede010ad0 100644
--- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts
@@ -45,6 +45,14 @@ rfkill {
shutdown-gpios = <&gpio2 RK_PD1 GPIO_ACTIVE_HIGH>;
};
+ fan: pwm-fan {
+ compatible = "pwm-fan";
+ cooling-levels = <0 180 205 230 255>;
+ fan-supply = <&vcc_5v0_sys>;
+ pwms = <&pwm2_8ch_5 0 60000 0>;
+ #cooling-cells = <2>;
+ };
+
leds: leds {
compatible = "gpio-leds";
pinctrl-names = "default";
@@ -711,6 +719,36 @@ rgmii_phy0: ethernet-phy@1 {
};
};
+&package_thermal {
+ polling-delay = <100>;
+
+ trips {
+ package_fan0: package-fan0 {
+ temperature = <50000>;
+ hysteresis = <2000>;
+ type = "active";
+ };
+
+ package_fan1: package-fan1 {
+ temperature = <60000>;
+ hysteresis = <2000>;
+ type = "active";
+ };
+ };
+
+ cooling-maps {
+ map1 {
+ trip = <&package_fan0>;
+ cooling-device = <&fan THERMAL_NO_LIMIT 1>;
+ };
+
+ map2 {
+ trip = <&package_fan1>;
+ cooling-device = <&fan 2 THERMAL_NO_LIMIT>;
+ };
+ };
+};
+
&pcie0 {
pinctrl-names = "default";
pinctrl-0 = <&pcie_reset>;
@@ -720,6 +758,13 @@ &pcie0 {
};
&pinctrl {
+ fan {
+ fan_pwm: fan-pwm {
+ rockchip,pins =
+ <4 RK_PC5 14 &pcfg_pull_down_drv_level_5>;
+ };
+ };
+
hym8563 {
hym8563_int: hym8563-int {
rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>;
@@ -770,6 +815,11 @@ wifi_en_h: wifi-en-h {
};
};
+&pwm2_8ch_5 {
+ pinctrl-0 = <&fan_pwm>;
+ status = "okay";
+};
+
&sai6 {
status = "okay";
};
--
2.53.0
^ permalink raw reply related
* Re: [PATCH] arm64: dts: ti: k3-j722s: use ti,j7200-padconf compatible
From: Thomas Richard @ 2026-04-20 13:38 UTC (permalink / raw)
To: Richard Genoud (TI), Nishanth Menon, Vignesh Raghavendra,
Tero Kristo, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Thomas Petazzoni, Gregory CLEMENT, Udit Kumar, Abhash Kumar,
linux-arm-kernel, devicetree, linux-kernel
In-Reply-To: <20260420131735.3833993-1-richard.genoud@bootlin.com>
Hello Richard,
On 4/20/26 3:17 PM, Richard Genoud (TI) wrote:
> From: Abhash Kumar Jha <a-kumar2@ti.com>
>
> The pinctrl contexts for j722s should be saved and restored during
> suspend-to-ram, just like it is done for j7200 and j784s4 SoCs.
>
> Use ti,j7200-padconf compatible to save and restore pinctrl contexts during
> suspend-to-ram.
>
> Signed-off-by: Abhash Kumar Jha <a-kumar2@ti.com>
> Signed-off-by: Richard Genoud (TI) <richard.genoud@bootlin.com>
> ---
> arch/arm64/boot/dts/ti/k3-j722s-evm.dts | 2 ++
> 1 file changed, 2 insertions(+)
>
> diff --git a/arch/arm64/boot/dts/ti/k3-j722s-evm.dts b/arch/arm64/boot/dts/ti/k3-j722s-evm.dts
> index e66330c71593..757eb6abcd72 100644
> --- a/arch/arm64/boot/dts/ti/k3-j722s-evm.dts
> +++ b/arch/arm64/boot/dts/ti/k3-j722s-evm.dts
> @@ -242,6 +242,7 @@ &phy_gmii_sel {
> };
>
> &main_pmx0 {
> + compatible = "ti,j7200-padconf", "pinctrl-single";
>
> main_mcan0_pins_default: main-mcan0-default-pins {
> pinctrl-single,pins = <
> @@ -418,6 +419,7 @@ &main_uart5 {
> };
>
> &mcu_pmx0 {
> + compatible = "ti,j7200-padconf", "pinctrl-single";
>
> mcu_i2c0_pins_default: mcu-i2c0-default-pins {
> pinctrl-single,pins = <
It should be done at SoC level, not just for the EVM board.
You should modify k3-j722s-main.dtsi and create k3-j722s-mcu.dtsi.
Best Regards,
Thomas
^ permalink raw reply
* Re: [PATCH v4 3/5] pwm: Add rockchip PWMv4 driver
From: Nicolas Frattaroli @ 2026-04-20 13:44 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio
In-Reply-To: <20260420-rk3576-pwm-v4-3-421738c7bf28@collabora.com>
On Monday, 20 April 2026 15:35:21 Central European Summer Time Nicolas Frattaroli wrote:
> The Rockchip RK3576 brings with it a new PWM IP, in downstream code
> referred to as "v4". This new IP is different enough from the previous
> Rockchip IP that I felt it necessary to add a new driver for it, instead
> of shoehorning it in the old one.
>
> Add this new driver, based on the PWM core's waveform APIs. Its platform
> device is registered by the parent mfpwm driver, from which it also
> receives a little platform data struct, so that mfpwm can guarantee that
> all the platform device drivers spread across different subsystems for
> this specific hardware IP do not interfere with each other.
>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> ---
> MAINTAINERS | 2 +
> drivers/counter/Kconfig | 11 +
> drivers/counter/Makefile | 1 +
> drivers/counter/rockchip-pwm-capture.c | 307 ++++++++++++++++++++++++++
> drivers/pwm/Kconfig | 11 +
> drivers/pwm/Makefile | 1 +
> drivers/pwm/pwm-rockchip-v4.c | 383 +++++++++++++++++++++++++++++++++
> 7 files changed, 716 insertions(+)
>
Ah jeez, I accidentally squashed the counter driver into this commit.
time to send out a v5, please ignore this.
^ permalink raw reply
* Re: [PATCH 0/3] mm: split the file's i_mmap tree for NUMA
From: Pedro Falcato @ 2026-04-20 13:48 UTC (permalink / raw)
To: Huang Shijie
Cc: Mateusz Guzik, akpm, viro, brauner, linux-mm, linux-kernel,
linux-arm-kernel, linux-fsdevel, muchun.song, osalvador,
linux-trace-kernel, linux-perf-users, linux-parisc, nvdimm,
zhongyuan, fangbaoshun, yingzhiwei
In-Reply-To: <aeWLCxru6cLWsxvQ@SH-HV00110.Hygon.cn>
BTW you're missing _a lot_ of CC's here, including the whole of mm/rmap.c
maintainership.
On Mon, Apr 20, 2026 at 10:10:19AM +0800, Huang Shijie wrote:
> On Mon, Apr 13, 2026 at 05:33:21PM +0200, Mateusz Guzik wrote:
> > On Mon, Apr 13, 2026 at 02:20:39PM +0800, Huang Shijie wrote:
> > > In NUMA, there are maybe many NUMA nodes and many CPUs.
> > > For example, a Hygon's server has 12 NUMA nodes, and 384 CPUs.
> > > In the UnixBench tests, there is a test "execl" which tests
> > > the execve system call.
> > >
> > > When we test our server with "./Run -c 384 execl",
> > > the test result is not good enough. The i_mmap locks contended heavily on
> > > "libc.so" and "ld.so". For example, the i_mmap tree for "libc.so" can have
> > > over 6000 VMAs, all the VMAs can be in different NUMA mode.
> > > The insert/remove operations do not run quickly enough.
> > >
> > > patch 1 & patch 2 are try to hide the direct access of i_mmap.
> > > patch 3 splits the i_mmap into sibling trees, and we can get better
> > > performance with this patch set:
> > > we can get 77% performance improvement(10 times average)
> > >
> >
> > To my reading you kept the lock as-is and only distributed the protected
> > state.
> >
> > While I don't doubt the improvement, I'm confident should you take a
> > look at the profile you are going to find this still does not scale with
> > rwsem being one of the problems (there are other global locks, some of
> > which have experimental patches for).
> >
> > Apart from that this does nothing to help high core systems which are
> > all one node, which imo puts another question mark on this specific
> > proposal.
> >
> > Of course one may question whether a RB tree is the right choice here,
> > it may be the lock-protected cost can go way down with merely a better
> > data structure.
> >
> > Regardless of that, for actual scalability, there will be no way around
> > decentralazing locking around this and partitioning per some core count
> > (not just by numa awareness).
> >
> > Decentralizing locking is definitely possible, but I have not looked
> > into specifics of how problematic it is. Best case scenario it will
> > merely with separate locks. Worst case scenario something needs a fully
> > stabilized state for traversal, in that case another rw lock can be
> > slapped around this, creating locking order read lock -> per-subset
> > write lock -- this will suffer scalability due to the read locking, but
> > it will still scale drastically better as apart from that there will be
> > no serialization. In this setting the problematic consumer will write
> > lock the new thing to stabilize the state.
> >
> I thought over again.
> I can change this patch set to support the non-NUMA case by:
> 1.) Still use one rw lock.
No. This doesn't help anything.
> 2.) For NUMA, keep the patch set as it is.
Please no. No NUMA vs non-NUMA case.
> 3.) For non-NUMA case, split the i_mmap tree to several subtrees.
> For example, if a machine has 192 CPUs, split the 32 CPUs as a tree.
If lock contention is the problem, I don't see how splitting the tree helps,
unless it helps reduce lock hold time in a way that randomly helps your workload.
But that's entirely random.
>
> So extend the patch set to support both the NUMA and non-NUMA machines.
FYI I've discussed some concrete ideas for reworking file rmap with Mateusz.
I'll be giving them a shot. Note that this needs to be done _carefully_,
particularly as there are some hidden assumptions wrt forking that aren't
very clear as to how they work[1].
[1] https://lore.kernel.org/all/bnukmnuxxuhdfeasjz33miemgr7w35c4aa6pqdmgupx7oxmeeb@gozgc3yxhcdd/
--
Pedro
^ permalink raw reply
* Re: [PATCH] arm64: dts: ti: k3-j722s: use ti,j7200-padconf compatible
From: Richard GENOUD @ 2026-04-20 13:51 UTC (permalink / raw)
To: Thomas Richard, Nishanth Menon, Vignesh Raghavendra, Tero Kristo,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Thomas Petazzoni, Gregory CLEMENT, Udit Kumar, Abhash Kumar,
linux-arm-kernel, devicetree, linux-kernel
In-Reply-To: <c36821db-2fcb-438c-b54c-a4f2d5b33fa5@bootlin.com>
Hi Thomas,
Le 20/04/2026 à 15:38, Thomas Richard a écrit :
> Hello Richard,
>
> On 4/20/26 3:17 PM, Richard Genoud (TI) wrote:
>> From: Abhash Kumar Jha <a-kumar2@ti.com>
>>
>> The pinctrl contexts for j722s should be saved and restored during
>> suspend-to-ram, just like it is done for j7200 and j784s4 SoCs.
>>
>> Use ti,j7200-padconf compatible to save and restore pinctrl contexts during
>> suspend-to-ram.
>>
>> Signed-off-by: Abhash Kumar Jha <a-kumar2@ti.com>
>> Signed-off-by: Richard Genoud (TI) <richard.genoud@bootlin.com>
>> ---
>> arch/arm64/boot/dts/ti/k3-j722s-evm.dts | 2 ++
>> 1 file changed, 2 insertions(+)
>>
>> diff --git a/arch/arm64/boot/dts/ti/k3-j722s-evm.dts b/arch/arm64/boot/dts/ti/k3-j722s-evm.dts
>> index e66330c71593..757eb6abcd72 100644
>> --- a/arch/arm64/boot/dts/ti/k3-j722s-evm.dts
>> +++ b/arch/arm64/boot/dts/ti/k3-j722s-evm.dts
>> @@ -242,6 +242,7 @@ &phy_gmii_sel {
>> };
>>
>> &main_pmx0 {
>> + compatible = "ti,j7200-padconf", "pinctrl-single";
>>
>> main_mcan0_pins_default: main-mcan0-default-pins {
>> pinctrl-single,pins = <
>> @@ -418,6 +419,7 @@ &main_uart5 {
>> };
>>
>> &mcu_pmx0 {
>> + compatible = "ti,j7200-padconf", "pinctrl-single";
>>
>> mcu_i2c0_pins_default: mcu-i2c0-default-pins {
>> pinctrl-single,pins = <
>
> It should be done at SoC level, not just for the EVM board.
> You should modify k3-j722s-main.dtsi and create k3-j722s-mcu.dtsi.
Indeed.
Thanks!
>
> Best Regards,
> Thomas
^ permalink raw reply
* Re: [PATCH v7 3/4] KVM: arm64: PMU: Introduce FIXED_COUNTERS_ONLY
From: Marc Zyngier @ 2026-04-20 13:53 UTC (permalink / raw)
To: Akihiko Odaki
Cc: Oliver Upton, Joey Gouly, Suzuki K Poulose, Zenghui Yu,
Catalin Marinas, Will Deacon, Kees Cook, Gustavo A. R. Silva,
Paolo Bonzini, Jonathan Corbet, Shuah Khan, linux-arm-kernel,
kvmarm, linux-kernel, linux-hardening, devel, kvm, linux-doc,
linux-kselftest
In-Reply-To: <ad44c69e-2f99-4f31-81b4-faae52eea080@rsg.ci.i.u-tokyo.ac.jp>
On Mon, 20 Apr 2026 13:07:15 +0100,
Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp> wrote:
>
> On 2026/04/20 18:51, Marc Zyngier wrote:
> > On Mon, 20 Apr 2026 09:36:16 +0100,
> > Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp> wrote:
> >>
> >> On 2026/04/20 2:19, Marc Zyngier wrote:
> >>> On Sat, 18 Apr 2026 09:14:25 +0100,
> >>> Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp> wrote:
> >>>>
> >>>> On a heterogeneous arm64 system, KVM's PMU emulation is based on the
> >>>> features of a single host PMU instance. When a vCPU is migrated to a
> >>>> pCPU with an incompatible PMU, counters such as PMCCNTR_EL0 stop
> >>>> incrementing.
> >>>>
> >>>> Although this behavior is permitted by the architecture, Windows does
> >>>> not handle it gracefully and may crash with a division-by-zero error.
> >>>>
> >>>> The current workaround requires VMMs to pin vCPUs to a set of pCPUs
> >>>> that share a compatible PMU. This is difficult to implement correctly in
> >>>> QEMU/libvirt, where pinning occurs after vCPU initialization, and it
> >>>> also restricts the guest to a subset of available pCPUs.
> >>>>
> >>>> Introduce the KVM_ARM_VCPU_PMU_V3_FIXED_COUNTERS_ONLY attribute to
> >>>> create a "fixed-counters-only" PMU. When set, KVM exposes a PMU that is
> >>>> compatible with all pCPUs but that does not support programmable
> >>>> event counters which may have different feature sets on different PMUs.
> >>>>
> >>>> This allows Windows guests to run reliably on heterogeneous systems
> >>>> without crashing, even without vCPU pinning, and enables VMMs to
> >>>> schedule vCPUs across all available pCPUs, making full use of the host
> >>>> hardware.
> >>>>
> >>>> Much like KVM_ARM_VCPU_PMU_V3_IRQ and other read-write attributes, this
> >>>> attribute provides a getter that facilitates kernel and userspace
> >>>> debugging/testing.
> >>>
> >>> OK, so that's the sales pitch. But how is it implemented? I would like
> >>> to be able to read a high-level description of the implementation
> >>> trade-offs.
> >>
> >> Implementation-wise it is very trivial. Essentially the following
> >> addition in kvm_arm_pmu_v3_get_attr() is the entire implementation:
> >> + case KVM_ARM_VCPU_PMU_V3_FIXED_COUNTERS_ONLY:
> >> + if (test_bit(KVM_ARCH_FLAG_PMU_V3_FIXED_COUNTERS_ONLY,
> >> &vcpu->kvm->arch.flags))
> >> + return 0;
> >>
> >> Both its functionality and code complexity is trivial. So we can argue that:
> >> - the functionality is too trivial to be useful or
> >> - the interface/implementation complexity is so trivial that it does not
> >> incur maintenance burden
> >>
> >> In this case the selftest uses the getter so I was more inclined to
> >> have it, but adding one just for the selftest sounds too ad-hoc, so
> >> here I looked into other attributes to ensure that it was not
> >> introducing inconsistency with existing interfaces.
> >>
> >> As the result, I found there are other read-write attributes; in fact
> >> there are more read-write attributes than write-only ones.
> >
> > You're completely missing the point. I'm referring to the whole of the
> > commit message, which is more of a marketing slide than a technical
> > description.
>
> In terms of implementation, the obvious tradeoff is that it adds more
> code to implement the feature. One thing to note is that
> kvm_vcpu_load_pmu() is added and is called each time a vCPU migrates
> across pCPUs. The heavy part, making the KVM_REQ_RELOAD_PMU request,
> only happens when the feature is enabled.
Well, that's what I want to see. The repeated blurb about Windows
being broken is cover letter material, but not fir for a commit
message.
[...]
> >>> "for the first time" gives the impression that it will work if you try
> >>> again. I'd rather we say that "This feature is incompatible with the
> >>> existence of a PMU event filter".
> >>
> >> The following sequence will work:
> >> 1. Set KVM_ARM_VCPU_PMU_V3_FIXED_COUNTERS_ONLY
> >> 2. Set KVM_ARM_VCPU_PMU_V3_FILTER
> >> 3. Set KVM_ARM_VCPU_PMU_V3_FIXED_COUNTERS_ONLY
> >>
> >> This is to make the behavior conistent with KVM_ARM_VCPU_PMU_V3_SET_PMU.
> >
> > I don't think this is correct. Filtering is completely at odds with
> > this patch, and I don't want to have to reason about the combination.
>
> kvm_arm_pmu_v3_set_pmu() has the following condition:
>
> if (kvm_vm_has_ran_once(kvm) ||
> (kvm->arch.pmu_filter && kvm->arch.arm_pmu != arm_pmu)) {
> ret = -EBUSY;
> break;
> }
>
> kvm_arm_pmu_v3_set_pmu_fixed_counters_only() has the corresponding
> condition for consistency:
>
> if (kvm_vm_has_ran_once(kvm) ||
> (kvm->arch.pmu_filter &&
> !test_bit(KVM_ARCH_FLAG_PMU_V3_FIXED_COUNTERS_ONLY,
> &kvm->arch.flags)))
> return -EBUSY;
>
> We can of course kill the PMU event filter for
> FIXED_COUNTERS_ONLY. The filter is effectively no-op with
> FIXED_COUNTERS_ONLY and I don't think that consistency matters much.
We shouldn't allow weird combinations in the UAPI. Since it makes no
sense to have both fixed-function *and* filters, we should make them
mutually exclusive.
[...]
> >> I expect migration will be handled with the conventional register
> >> getters and setters, but please share if you have a concern.
> >
> > At the very least I want to see some documentation explaining that.
>
> What kind of documentation do you expect?
A description of what counters are exposed by this feature, and what
architectural features they are dependent on.
> If we change kvm_vcpu_load_pmu() to avoid for_each_set_bit(), there
> would be a good chance to forget updating it when mechanically
> updating existing for_each_set_bit() instances, so it is a candidate
> for documentation. But I don't have a good idea where to place it
> either.
The moment we introduce FEAT_PMUv3_ICNTR, the whole PMUv3 emulation
will catch fire anyway, as we already limit ourselves to 32 bits for
reset and nesting switch.
So this will be a major redesign anyway. If you are really worried,
leave a comment in there.
M.
--
Without deviation from the norm, progress is not possible.
^ permalink raw reply
* [PATCH v5 0/6] Add Rockchip RK3576 PWM Support Through MFPWM
From: Nicolas Frattaroli @ 2026-04-20 13:52 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio, Conor Dooley
This series introduces support for some of the functions of the new PWM
silicon found on Rockchip's RK3576 SoC. Due to the wide range of
functionalities offered by it, including many parts which this series'
first iteration does not attempt to implement for now. The drivers are
modelled as an MFD, with no leakage of the MFD-ness into the binding, as
it's a Linux implementation detail.
Here's some of the features of the hardware:
- Continuous PWM output (implemented in this series)
- One-shot/Finite repetition PWM output
- PWM capture by counting high/low cycles (implemented in this series)
- Sending IR transmissions in several TV remote protocols
- Generating an interrupt based on the input being one of 16
user-specified values ("Power key capture")
- Biphasic counter support
- Using the hardware to measure a clock signal's frequency
- Using the hardware to count a clock signal's pulses
- Generating PWM output waveforms through a user-specified lookup table
As you can tell, there's a lot. I've focused on continuous PWM output
for now as the most important one for things like controlling fans. The
PWM capture driver is an added bonus, because I needed at least two
drivers to test things. Anyone doing consumer electronic devices like
TVs based on the RK3576 may need to do the power key stuff at some
stage, as it can be used to wake up the SoC with an IR remote. The IR
transmission stuff in general may be a funny weekend project for someone
at some point; I assume it's there so TV boxes can turn on and off TVs
without needing the HDMI control stuff.
At first, I considered simply integrating support for this new IP into
the old pwm-rockchip driver, as the downstream vendor kernel did.
However, the IP is significantly different from previous iterations.
Especially if the goal is to support some of the additional
functionality that the new silicon brings, doing it all in a single pwm
driver would be untenable. Especially one that already supports other
hardware with a way different set of registers.
Hence, the mfpwm pattern: each device functionality is its own driver,
and they all get registered as MFD cells by the parent mfpwm MFD driver,
which is the one that binds to the DT compatible. Each device function
driver then has to _acquire and _release the hardware when it needs
control of it. If some other device function is using the device
already, -EBUSY is returned, which the device function driver can then
forward to the user and everyone is happy.
The PWM output driver, pwm-rockchip-v4, uses the new waveform APIs. I
thought while writing a new driver that I might as well use the new
APIs.
The PWM capture driver, implemented as a counter driver, is somewhat
primitive, in that it doesn't make use of things like the biphasic
counter support or clock measuring, but it serves as a good way to
showcase and test the mutual exclusion that the mfpwm framework tries to
achieve. It directly exposes the HPC/LPC counts as counters. Shoutouts
to the counter subsystem's documentation by the way, it is some of the
best subsystem documentation I've come across so far, and was a great
help.
All instances of the PWM controller have three clocks that they can pick
and choose to derive the PWM signal from. One is the default PLL from
the CRU, one is the 24 MHz crystal oscillator (gated by the CRU), and
one is an RC oscillator (also gated by the CRU). Each PWM channel can
switch between these with a clock selection register in the PWM register
range, hence this is implemented as a clock mux.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
Changes in v5:
- Fix the accidentally squashed counter driver patch, please refer to
"Changes in v4"
- Link to v4: https://patch.msgid.link/20260420-rk3576-pwm-v4-0-421738c7bf28@collabora.com
Changes in v4:
- Fix MAINTAINERS entry for mfpwm
- Make mfpwm core driver depend on ARCH_ROCKCHIP || COMPILE_TEST
- Remove redundant Kconfig deps from pwm output and counter
- mfpwm core: Introduce mfpwm_get_mode
- mfpwm core: Rename pwm out to rockchip-pwm-v4
- mfpwm core: Remove leftover commented out code
- pwm output: Rename to rockchip-pwm-v4
- pwm output: Rework round_wf_tohw:
- Pass wf/wfhw into round_params
- If wfhw->period is 0, don't do the offset clamping calculation to
avoid underflow
- Return -ERANGE in a theoretical future where the clock is that high
- Change debug print
- pwm output: Change fromhw debug print to conform to other PWM drivers
- pwm output: Adjust comments at the start of the file
- pwm output: Store rate in wfhw struct
- pwm output: Get rid of unnecessary initialization of locals
- pwm output: Round up in fromhw
- pwm output: Use common is_enabled helper in read_wf
- pwm output: put exclusive rate and clk_disable on unlikely error path
- pwm output: Set of_node_reused on this device, rather than the parent,
and set its device node to the parent node
- pwm output: Make failure to acquire PWM in probe an error rather than
a warning
- pwm output: Re-do error handling in probe function to drop clock and
mfpwm on failure
- counter: Get rid of enable_lock and is_enabled, read this from hw regs
- counter: Request IRQ after setting up the counter device
- counter: Acquire mfpwm if counter hardware is enabled at module probe
time
- counter: Rework signals, synapses and counts
- Add patch to describe the Radxa ROCK 4D's PWM-controlled fan in DT
- Link to v3: https://lore.kernel.org/r/20251027-rk3576-pwm-v3-0-654a5cb1e3f8@collabora.com
Changes in v3:
- Move drivers to using MFD; MFPWM now lives in the mfd tree as
requested by Lee Jones
- Use the new FIELD_PREP_WM16 macros, and rebase onto next-20251027
- Get rid of some unused hardware version accessor inline functions
- pwm-rockchip-v4 pwm output: use devm_pwmchip_add and get rid of the
driver remove callback that's no longer needed
- pwm-rockchip-v4 pwm output: use the parent MFD device's OF node, so
that referencing the pwm node in DT works correctly (ty Heiko)
- pwm-rockchip-v4 pwm output: add link to public TRM for the hardware in
comment at the start of the file
- pwm-rockchip-v4 pwm output: Capitalise first letter in kernel messages
- pwm-rockchip-v4 pwm output: get rid of unnecessary mul_u64_u64_div_u64
calls where the operands cannot produce an overflow, turning it into a
regular u64 division
- pwm-rockchip-v4 pwm output: simplify round_rate functions
- pwm-rockchip-v4 pwm output: remove redundant duty <= period check
- pwm-rockchip-v4 pwm output: print input parameters in tohw/fromhw in
debug statement
- pwm-rockchip-v4 pwm output: clarify the offset < (period - duty) thing
being dictated by hardware with a comment in the limitations list and
near where the check is
- pwm-rockchip-v4 pwm output: remove pointless mfpwm_acquire/release
calls in the fromhw/tohw functions, as they don't actually protect
against anything
- pwm-rockchip-capture counter: expose HPC and LPC directly, and fire a
change-of-state event on the appropriate channel on interrupt
- pwm-rockchip-capture counter: remove all the captures_left and delayed
worker cruft
- pwm-rockchip-capture counter: use MFD parent's OF node
- pwm-rockchip-capture counter: change intsts ^ clr to != and add a
comment explaining why there's no mask here
- Link to v2: https://lore.kernel.org/r/20250602-rk3576-pwm-v2-0-a6434b0ce60c@collabora.com
Changes in v2:
- bindings: make osc required (as it's present in all instances of the
hardware I'm aware of) and add the rc clock as well. I thought it
wasn't present on some instances of the PWM IP due to the vendor SoC
dtsi, but checking the CRU made me realise those clocks do exist for
all instances. Did not include Conor's R-b as this constitutes a
substantial enough change to necessitate a re-review
- move bitfield write-enable mask macros into bitfield.h by replacing
the original rockchip-specific utils header patch with a bitfield.h
patch.
- mfpwm: change all instances of WARN to be dev_warn instead, as we have
a device pointer.
- mfpwm: replace the ad-hoc clock mux implementation that used a sysfs
interface with a generic clk-mux.
- mfpwm: add the rc clock
- mfpwm: rename all the pwmv4_ prefixed functions to have the
rockchip_pwm_v4_ prefix instead
- mfpwm: remove the pwmclk indirection, hand chosen_clk to pwmf
- mfpwm: move to use the new bitfield macros for the WE mask
- mfpwm: mark reg access inline functions as static to fix build errors
- pwm-rockchip-v4 pwm output: replace mult_frac with mul_u64_u64_div_u64
- pwm-rockchip-v4 pwm output: don't return error if parameters are out
of range, just set them to the maximum
- pwm-rockchip-v4 pwm output: add rate to debug message
- pwm-rockchip-v4 pwm output: if rate is 0 and pwm is disabled, set
waveform parameters to 0. The clock is expected to not have a rate in
this case.
- pwm-rockchip-v4 pwm output: add pwmchip_remove in remove callback,
which also necessitated using chip as the platdata instead of the
driver private struct
- pwm-rockchip-v4 pwm output: rework PWMV4_CTRL_UPDATE_EN since it never
needs to be set to 0 by the driver
- pwm-rockchip-v4 pwm output: add a limitations list
- pwm-rockchip-v4 pwm output: handle initial hardware state during
probe, enabling the pwm clock if the PWM is on and in continuous mode
- pwm-rockchip-v4 pwm output: rename pwmv4_is_enabled to use the
rockchip_pwm_v4_ prefix instead
- pwm-rockchip-v4 pwm output: remove pwmclk indirection, use clk API
directly
- pwm-rockchip-v4 pwm output: no longer claim the chip as being atomic,
as the clk_rate_exclusive_get calls may sleep.
- rockchip-pwm-capture counter: remove pwmclk indirection, use clk API
directly
- rockchip-pwm-capture counter: replace mult_frac with
mul_u64_u64_div_u64
- rockchip-pwm-capture counter: don't output periods/duty cycles if the
period is longer than the chosen timeout; this works around the
hardware cycle counter seemingly being impossible to clear
- dts: added osc and rc to every pwm node
- dts: reordered properties in pwm0 to be sorted
- Link to v1: https://lore.kernel.org/r/20250408-rk3576-pwm-v1-0-a49286c2ca8e@collabora.com
To: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
To: Uwe Kleine-König <ukleinek@kernel.org>
To: Rob Herring <robh@kernel.org>
To: Krzysztof Kozlowski <krzk+dt@kernel.org>
To: Conor Dooley <conor+dt@kernel.org>
To: Heiko Stuebner <heiko@sntech.de>
To: Lee Jones <lee@kernel.org>
To: William Breathitt Gray <wbg@kernel.org>
To: Damon Ding <damon.ding@rock-chips.com>
Cc: kernel@collabora.com
Cc: Jonas Karlman <jonas@kwiboo.se>
Cc: Alexey Charkov <alchark@gmail.com>
Cc: linux-rockchip@lists.infradead.org
Cc: linux-pwm@vger.kernel.org
Cc: devicetree@vger.kernel.org
Cc: linux-arm-kernel@lists.infradead.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-iio@vger.kernel.org
---
Nicolas Frattaroli (6):
dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
mfd: Add Rockchip mfpwm driver
pwm: Add rockchip PWMv4 driver
counter: Add rockchip-pwm-capture driver
arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi
arm64: dts: rockchip: Add cooling fan to ROCK 4D
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 ++++
MAINTAINERS | 11 +
arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts | 50 +++
arch/arm64/boot/dts/rockchip/rk3576.dtsi | 208 +++++++++
drivers/counter/Kconfig | 11 +
drivers/counter/Makefile | 1 +
drivers/counter/rockchip-pwm-capture.c | 307 ++++++++++++++
drivers/mfd/Kconfig | 16 +
drivers/mfd/Makefile | 1 +
drivers/mfd/rockchip-mfpwm.c | 357 ++++++++++++++++
drivers/pwm/Kconfig | 11 +
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-rockchip-v4.c | 383 +++++++++++++++++
include/linux/mfd/rockchip-mfpwm.h | 470 +++++++++++++++++++++
14 files changed, 1904 insertions(+)
---
base-commit: 77a9bb0193d790fb71c0edfc567bddc1b56fb3ff
change-id: 20250407-rk3576-pwm-46761bd0deaa
Best regards,
--
Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
^ permalink raw reply
* [PATCH v5 1/6] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
From: Nicolas Frattaroli @ 2026-04-20 13:52 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio, Conor Dooley
In-Reply-To: <20260420-rk3576-pwm-v5-0-ae7cfbbe5427@collabora.com>
The Rockchip RK3576 SoC has a newer PWM controller IP revision than
previous Rockchip SoCs. This IP, called "PWMv4" by Rockchip, introduces
several new features, and consequently differs in its bindings.
Instead of expanding the ever-growing rockchip-pwm binding that already
has an if-condition, add an entirely new binding to handle this.
There are two additional clocks, "osc" and "rc". These are available for
every PWM instance, and the PWM hardware can switch between the "pwm",
"osc" and "rc" clock at runtime.
The PWM controller also comes with an interrupt now. This interrupt is
used to signal various conditions.
Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 ++++++++++++++++++++++
MAINTAINERS | 7 ++
2 files changed, 84 insertions(+)
diff --git a/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
new file mode 100644
index 000000000000..48d5055c8b06
--- /dev/null
+++ b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
@@ -0,0 +1,77 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pwm/rockchip,rk3576-pwm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Rockchip PWMv4 controller
+
+maintainers:
+ - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+
+description: |
+ The Rockchip PWMv4 controller is a PWM controller found on several Rockchip
+ SoCs, such as the RK3576.
+
+ It supports both generating and capturing PWM signals.
+
+allOf:
+ - $ref: pwm.yaml#
+
+properties:
+ compatible:
+ items:
+ - const: rockchip,rk3576-pwm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: Used to derive the PWM signal.
+ - description: Used as the APB bus clock.
+ - description: Used as an alternative to derive the PWM signal.
+ - description: Used as another alternative to derive the PWM signal.
+
+ clock-names:
+ items:
+ - const: pwm
+ - const: pclk
+ - const: osc
+ - const: rc
+
+ interrupts:
+ maxItems: 1
+
+ "#pwm-cells":
+ const: 3
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - interrupts
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/rockchip,rk3576-cru.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ soc {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ pwm@2add0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>,
+ <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
+ #pwm-cells = <3>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 891fb83ba7a9..86f20cb563c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23172,6 +23172,13 @@ F: Documentation/userspace-api/media/v4l/metafmt-rkisp1.rst
F: drivers/media/platform/rockchip/rkisp1
F: include/uapi/linux/rkisp1-config.h
+ROCKCHIP MFPWM
+M: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+L: linux-rockchip@lists.infradead.org
+L: linux-pwm@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
M: Daniel Golle <daniel@makrotopia.org>
M: Aurelien Jarno <aurelien@aurel32.net>
--
2.53.0
^ permalink raw reply related
* [PATCH v5 2/6] mfd: Add Rockchip mfpwm driver
From: Nicolas Frattaroli @ 2026-04-20 13:52 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Damon Ding
Cc: Nicolas Frattaroli, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio
In-Reply-To: <20260420-rk3576-pwm-v5-0-ae7cfbbe5427@collabora.com>
With the Rockchip RK3576, the PWM IP used by Rockchip has changed
substantially. Looking at both the downstream pwm-rockchip driver as
well as the mainline pwm-rockchip driver made it clear that with all its
additional features and its differences from previous IP revisions, it
is best supported in a new driver.
This brings us to the question as to what such a new driver should be.
To me, it soon became clear that it should actually be several new
drivers, most prominently when Uwe Kleine-König let me know that I
should not implement the pwm subsystem's capture callback, but instead
write a counter driver for this functionality.
Combined with the other as-of-yet unimplemented functionality of this
new IP, it became apparent that it needs to be spread across several
subsystems.
For this reason, we add a new MFD core driver, called mfpwm (short for
"Multi-function PWM"). This "parent" driver makes sure that only one
device function driver is using the device at a time, and is in charge
of registering the MFD cell devices for the individual device functions
offered by the device.
An acquire/release pattern is used to guarantee that device function
drivers don't step on each other's toes.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
MAINTAINERS | 2 +
drivers/mfd/Kconfig | 16 ++
drivers/mfd/Makefile | 1 +
drivers/mfd/rockchip-mfpwm.c | 357 ++++++++++++++++++++++++++++
include/linux/mfd/rockchip-mfpwm.h | 470 +++++++++++++++++++++++++++++++++++++
5 files changed, 846 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 86f20cb563c6..d52731242a33 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23178,6 +23178,8 @@ L: linux-rockchip@lists.infradead.org
L: linux-pwm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F: drivers/mfd/rockchip-mfpwm.c
+F: include/linux/mfd/rockchip-mfpwm.h
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
M: Daniel Golle <daniel@makrotopia.org>
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 7192c9d1d268..80b4e82c4937 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -1378,6 +1378,22 @@ config MFD_RC5T583
Additional drivers must be enabled in order to use the
different functionality of the device.
+config MFD_ROCKCHIP_MFPWM
+ tristate "Rockchip multi-function PWM controller"
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
+ depends on OF
+ depends on HAS_IOMEM
+ depends on COMMON_CLK
+ select MFD_CORE
+ help
+ Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
+ several different functions, such as generating PWM waveforms but also
+ counting waveforms.
+
+ This driver manages the overall device, and selects between different
+ functionalities at runtime as needed. Drivers for them are implemented
+ in their respective subsystems.
+
config MFD_RK8XX
tristate
select MFD_CORE
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..ebadbaea9e4a 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -231,6 +231,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o
obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o
obj-$(CONFIG_MFD_NTXEC) += ntxec.o
obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o
+obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o
obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o
obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o
obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o
diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c
new file mode 100644
index 000000000000..72d04982b961
--- /dev/null
+++ b/drivers/mfd/rockchip-mfpwm.c
@@ -0,0 +1,357 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A driver to manage all the different functionalities exposed by Rockchip's
+ * PWMv4 hardware.
+ *
+ * This driver is chiefly focused on guaranteeing non-concurrent operation
+ * between the different device functions, as well as setting the clocks.
+ * It registers the device function platform devices, e.g. PWM output or
+ * PWM capture.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/overflow.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+
+/**
+ * struct rockchip_mfpwm - private mfpwm driver instance state struct
+ * @pdev: pointer to this instance's &struct platform_device
+ * @base: pointer to the memory mapped registers of this device
+ * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
+ * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
+ * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from
+ * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk
+ * @pclk: pointer to the APB bus clock needed for mmio register access
+ * @active_func: pointer to the currently active device function, or %NULL if no
+ * device function is currently actively using any of the shared
+ * resources. May only be checked/modified with @state_lock held.
+ * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
+ * it. Must only be checked or modified while holding @state_lock.
+ * @state_lock: this lock is held while either the active device function, the
+ * enable register, or the chosen clock is being changed.
+ * @irq: the IRQ number of this device
+ */
+struct rockchip_mfpwm {
+ struct platform_device *pdev;
+ void __iomem *base;
+ struct clk *pwm_clk;
+ struct clk *osc_clk;
+ struct clk *rc_clk;
+ struct clk *chosen_clk;
+ struct clk *pclk;
+ struct rockchip_mfpwm_func *active_func;
+ unsigned int acquire_cnt;
+ spinlock_t state_lock;
+ int irq;
+};
+
+static atomic_t subdev_id = ATOMIC_INIT(0);
+
+static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
+{
+ return platform_get_drvdata(pdev);
+}
+
+static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
+ const char *fname)
+{
+ struct device *dev = &pwmf->parent->pdev->dev;
+
+ if (IS_ERR_OR_NULL(pwmf)) {
+ dev_warn(dev, "called %s with an erroneous handle, no effect\n",
+ fname);
+ return -EINVAL;
+ }
+
+ if (IS_ERR_OR_NULL(pwmf->parent)) {
+ dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
+ fname);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+__attribute__((nonnull))
+static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
+ unsigned int cnt;
+
+ if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
+ return -EBUSY;
+
+ if (!mfpwm->active_func)
+ mfpwm->active_func = pwmf;
+
+ if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
+ mfpwm->acquire_cnt = cnt;
+ } else {
+ dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
+ __func__);
+ return -EOVERFLOW;
+ }
+
+ dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
+ pwmf->id, mfpwm->acquire_cnt);
+
+ return clk_enable(mfpwm->pclk);
+}
+
+int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ unsigned long flags;
+ int ret = 0;
+
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
+ if (ret)
+ return ret;
+
+ mfpwm = pwmf->parent;
+ dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
+
+ if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
+ return -EBUSY;
+
+ ret = mfpwm_do_acquire(pwmf);
+
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM");
+
+__attribute__((nonnull))
+static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
+
+ if (!mfpwm->active_func)
+ return;
+
+ if (mfpwm->active_func->id != pwmf->id)
+ return;
+
+ /*
+ * No need to check_sub_overflow here, !mfpwm->active_func above catches
+ * this type of problem already.
+ */
+ mfpwm->acquire_cnt--;
+
+ if (!mfpwm->acquire_cnt)
+ mfpwm->active_func = NULL;
+
+ clk_disable(mfpwm->pclk);
+}
+
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ unsigned long flags;
+
+ if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
+ return;
+
+ mfpwm = pwmf->parent;
+
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
+ mfpwm_do_release(pwmf);
+ dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
+ pwmf->id, mfpwm->acquire_cnt);
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM");
+
+int mfpwm_get_mode(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ int ret;
+
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
+ if (ret)
+ return ret;
+
+ mfpwm = pwmf->parent;
+
+ guard(spinlock_irqsave)(&mfpwm->state_lock);
+
+ if (!rockchip_pwm_v4_is_enabled(mfpwm_reg_read(mfpwm->base, PWMV4_REG_ENABLE)))
+ return -1;
+
+ return mfpwm_reg_read(mfpwm->base, PWMV4_REG_CTRL) & PWMV4_MODE_MASK;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_get_mode, "ROCKCHIP_MFPWM");
+
+/**
+ * mfpwm_register_subdev - register a single mfpwm_func
+ * @mfpwm: pointer to the parent &struct rockchip_mfpwm
+ * @name: sub-device name string
+ *
+ * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
+ * and register a new mfd cell.
+ *
+ * Returns: 0 on success, negative errno on error
+ */
+static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
+ const char *name)
+{
+ struct rockchip_mfpwm_func *func;
+ struct mfd_cell cell = {};
+
+ func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
+ if (IS_ERR(func))
+ return PTR_ERR(func);
+ func->irq = mfpwm->irq;
+ func->parent = mfpwm;
+ func->id = atomic_inc_return(&subdev_id);
+ func->base = mfpwm->base;
+ func->core = mfpwm->chosen_clk;
+ cell.name = name;
+ cell.platform_data = func;
+ cell.pdata_size = sizeof(*func);
+
+ return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL,
+ 0, NULL);
+}
+
+static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
+{
+ int ret;
+
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-v4");
+ if (ret)
+ return ret;
+
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int rockchip_mfpwm_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct rockchip_mfpwm *mfpwm;
+ char *clk_mux_name;
+ const char *mux_p_names[3];
+ int ret = 0;
+
+ mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL);
+ if (IS_ERR(mfpwm))
+ return PTR_ERR(mfpwm);
+
+ mfpwm->pdev = pdev;
+
+ spin_lock_init(&mfpwm->state_lock);
+
+ mfpwm->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(mfpwm->base))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->base),
+ "failed to ioremap address\n");
+
+ mfpwm->pclk = devm_clk_get_prepared(dev, "pclk");
+ if (IS_ERR(mfpwm->pclk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pclk),
+ "couldn't get and prepare 'pclk' clock\n");
+
+ mfpwm->irq = platform_get_irq(pdev, 0);
+ if (mfpwm->irq < 0)
+ return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n");
+
+ mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm");
+ if (IS_ERR(mfpwm->pwm_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
+ "couldn't get and prepare 'pwm' clock\n");
+
+ mfpwm->osc_clk = devm_clk_get_prepared(dev, "osc");
+ if (IS_ERR(mfpwm->osc_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk),
+ "couldn't get and prepare 'osc' clock\n");
+
+ mfpwm->rc_clk = devm_clk_get_prepared(dev, "rc");
+ if (IS_ERR(mfpwm->rc_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->rc_clk),
+ "couldn't get and prepare 'rc' clock\n");
+
+ clk_mux_name = devm_kasprintf(dev, GFP_KERNEL, "%s_chosen", dev_name(dev));
+ if (!clk_mux_name)
+ return -ENOMEM;
+
+ mux_p_names[0] = __clk_get_name(mfpwm->pwm_clk);
+ mux_p_names[1] = __clk_get_name(mfpwm->osc_clk);
+ mux_p_names[2] = __clk_get_name(mfpwm->rc_clk);
+ mfpwm->chosen_clk = clk_register_mux(dev, clk_mux_name, mux_p_names,
+ ARRAY_SIZE(mux_p_names),
+ CLK_SET_RATE_PARENT,
+ mfpwm->base + PWMV4_REG_CLK_CTRL,
+ PWMV4_CLK_SRC_SHIFT, PWMV4_CLK_SRC_WIDTH,
+ CLK_MUX_HIWORD_MASK, NULL);
+ ret = clk_prepare(mfpwm->chosen_clk);
+ if (ret) {
+ dev_err(dev, "failed to prepare PWM clock mux: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ platform_set_drvdata(pdev, mfpwm);
+
+ ret = mfpwm_register_subdevs(mfpwm);
+ if (ret) {
+ dev_err(dev, "failed to register sub-devices: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ return ret;
+}
+
+static void rockchip_mfpwm_remove(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
+
+ if (mfpwm->chosen_clk) {
+ clk_unprepare(mfpwm->chosen_clk);
+ clk_unregister_mux(mfpwm->chosen_clk);
+ }
+
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+
+static const struct of_device_id rockchip_mfpwm_of_match[] = {
+ {
+ .compatible = "rockchip,rk3576-pwm",
+ },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match);
+
+static struct platform_driver rockchip_mfpwm_driver = {
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .of_match_table = rockchip_mfpwm_of_match,
+ },
+ .probe = rockchip_mfpwm_probe,
+ .remove = rockchip_mfpwm_remove,
+};
+module_platform_driver(rockchip_mfpwm_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip MFPWM Driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/rockchip-mfpwm.h b/include/linux/mfd/rockchip-mfpwm.h
new file mode 100644
index 000000000000..dbf1588a4382
--- /dev/null
+++ b/include/linux/mfd/rockchip-mfpwm.h
@@ -0,0 +1,470 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * Common header file for all the Rockchip Multi-function PWM controller
+ * drivers that are spread across subsystems.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#ifndef __SOC_ROCKCHIP_MFPWM_H__
+#define __SOC_ROCKCHIP_MFPWM_H__
+
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/hw_bitfield.h>
+#include <linux/io.h>
+#include <linux/spinlock.h>
+
+struct rockchip_mfpwm;
+
+/**
+ * struct rockchip_mfpwm_func - struct representing a single function driver
+ *
+ * @id: unique id for this function driver instance
+ * @base: pointer to start of MMIO registers
+ * @parent: a pointer to the parent mfpwm struct
+ * @irq: the shared IRQ gotten from the parent mfpwm device
+ * @core: a pointer to the clk mux that drives this channel's PWM
+ */
+struct rockchip_mfpwm_func {
+ int id;
+ void __iomem *base;
+ struct rockchip_mfpwm *parent;
+ int irq;
+ struct clk *core;
+};
+
+/*
+ * PWMV4 Register Definitions
+ * --------------------------
+ *
+ * Attributes:
+ * RW - Read-Write
+ * RO - Read-Only
+ * WO - Write-Only
+ * W1T - Write high, Self-clearing
+ * W1C - Write high to clear interrupt
+ *
+ * Bit ranges to be understood with Verilog-like semantics,
+ * e.g. [03:00] is 4 bits: 0, 1, 2 and 3.
+ *
+ * All registers must be accessed with 32-bit width accesses only
+ */
+
+#define PWMV4_REG_VERSION 0x000
+/*
+ * VERSION Register Description
+ * [31:24] RO | Hardware Major Version
+ * [23:16] RO | Hardware Minor Version
+ * [15:15] RO | Reserved
+ * [14:14] RO | Hardware supports biphasic counters
+ * [13:13] RO | Hardware supports filters
+ * [12:12] RO | Hardware supports waveform generation
+ * [11:11] RO | Hardware supports counter
+ * [10:10] RO | Hardware supports frequency metering
+ * [09:09] RO | Hardware supports power key functionality
+ * [08:08] RO | Hardware supports infrared transmissions
+ * [07:04] RO | Channel index of this instance
+ * [03:00] RO | Number of channels the base instance supports
+ */
+
+#define PWMV4_REG_ENABLE 0x004
+/*
+ * ENABLE Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:06] RO | Reserved
+ * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled
+ */
+#define PWMV4_CHN_CNT_RD_EN(v) FIELD_PREP_WM16(BIT(5), (v))
+/*
+ * [04:04] W1T | PWM Globally Joined Control Enable
+ * 1 = this PWM channel will be enabled by a global pwm enable
+ * bit instead of the PWM Enable bit.
+ */
+#define PWMV4_GLOBAL_CTRL_EN(v) FIELD_PREP_WM16(BIT(4), (v))
+/*
+ * [03:03] RW | Force Clock Enable
+ * 0 = disabled, if the PWM channel is inactive then so is the
+ * clock prescale module
+ */
+#define PWMV4_FORCE_CLK_EN(v) FIELD_PREP_WM16(BIT(3), (v))
+/*
+ * [02:02] W1T | PWM Control Update Enable
+ * 1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and
+ * _OFFSET registers once 1 is written to it
+ */
+#define PWMV4_CTRL_UPDATE_EN FIELD_PREP_WM16_CONST(BIT(2), 1)
+/*
+ * [01:01] RW | PWM Enable, 1 = enabled
+ * If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_EN_MASK BIT(1)
+#define PWMV4_EN(v) FIELD_PREP_WM16(PWMV4_EN_MASK, \
+ ((v) ? 1 : 0))
+/*
+ * [00:00] RW | PWM Clock Enable, 1 = enabled
+ * If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_CLK_EN_MASK BIT(0)
+#define PWMV4_CLK_EN(v) FIELD_PREP_WM16(PWMV4_CLK_EN_MASK, \
+ ((v) ? 1 : 0))
+#define PWMV4_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK)
+static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val)
+{
+ return (val & PWMV4_EN_BOTH_MASK);
+}
+
+#define PWMV4_REG_CLK_CTRL 0x008
+/*
+ * CLK_CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:15] RW | Clock Global Selection
+ * 0 = current channel scale clock
+ * 1 = global channel scale clock
+ */
+#define PWMV4_CLK_GLOBAL(v) FIELD_PREP_WM16(BIT(15), (v))
+/*
+ * [14:13] RW | Clock Source Selection
+ * 0 = Clock from PLL, frequency can be configured
+ * 1 = Clock from crystal oscillator, frequency is fixed
+ * 2 = Clock from RC oscillator, frequency is fixed
+ * 3 = Reserved
+ * NOTE: The purpose for this clock-mux-outside-CRU construct is
+ * to let the SoC go into a sleep state with the PWM
+ * hardware still having a clock signal for IR input, which
+ * can then wake up the SoC.
+ */
+#define PWMV4_CLK_SRC_PLL 0x0U
+#define PWMV4_CLK_SRC_CRYSTAL 0x1U
+#define PWMV4_CLK_SRC_RC 0x2U
+#define PWMV4_CLK_SRC_SHIFT 13
+#define PWMV4_CLK_SRC_WIDTH 2
+/*
+ * [12:04] RW | Scale Factor to apply to pre-scaled clock
+ * 1 <= v <= 256, v means clock divided by 2*v
+ */
+#define PWMV4_CLK_SCALE_F(v) FIELD_PREP_WM16(GENMASK(12, 4), (v))
+/*
+ * [03:03] RO | Reserved
+ * [02:00] RW | Prescale Factor
+ * v here means the input clock is divided by pow(2, v)
+ */
+#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v))
+
+#define PWMV4_REG_CTRL 0x00C
+/*
+ * CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:09] RO | Reserved
+ * [08:06] RW | PWM Input Channel Selection
+ * By default, the channel selects its own input, but writing v
+ * here selects PWM input from channel v instead.
+ */
+#define PWMV4_CTRL_IN_SEL(v) FIELD_PREP_WM16(GENMASK(8, 6), (v))
+/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */
+#define PWMV4_CTRL_UNALIGNED(v) FIELD_PREP_WM16(BIT(5), (v))
+/* [04:04] RW | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */
+#define PWMV4_LEFT_ALIGNED 0x0U
+#define PWMV4_CENTRE_ALIGNED 0x1U
+#define PWMV4_CTRL_OUT_MODE(v) FIELD_PREP_WM16(BIT(4), (v))
+/*
+ * [03:03] RW | Inactive Polarity for when the channel is either disabled or
+ * has completed outputting the entire waveform in one-shot mode.
+ * 0 = Negative, 1 = Positive
+ */
+#define PWMV4_POLARITY_N 0x0U
+#define PWMV4_POLARITY_P 0x1U
+#define PWMV4_INACTIVE_POL(v) FIELD_PREP_WM16(BIT(3), (v))
+/*
+ * [02:02] RW | Duty Cycle Polarity to use at the start of the waveform.
+ * 0 = Negative, 1 = Positive
+ */
+#define PWMV4_DUTY_POL_SHIFT 2
+#define PWMV4_DUTY_POL_MASK BIT(PWMV4_DUTY_POL_SHIFT)
+#define PWMV4_DUTY_POL(v) FIELD_PREP_WM16(PWMV4_DUTY_POL_MASK, \
+ (v))
+/*
+ * [01:00] RW | PWM Mode
+ * 0 = One-shot mode, PWM generates waveform RPT times
+ * 1 = Continuous mode
+ * 2 = Capture mode, PWM measures cycles of input waveform
+ * 3 = Reserved
+ */
+#define PWMV4_MODE_ONESHOT 0x0U
+#define PWMV4_MODE_CONT 0x1U
+#define PWMV4_MODE_CAPTURE 0x2U
+#define PWMV4_MODE_MASK GENMASK(1, 0)
+#define PWMV4_MODE(v) FIELD_PREP_WM16(PWMV4_MODE_MASK, (v))
+#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \
+ PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \
+ PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \
+ PWMV4_CTRL_UNALIGNED(true))
+#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \
+ PWMV4_CTRL_COM_FLAGS)
+#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \
+ PWMV4_CTRL_COM_FLAGS)
+
+#define PWMV4_REG_PERIOD 0x010
+/*
+ * PERIOD Register Description
+ * [31:00] RW | Period of the output waveform
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_DUTY 0x014
+/*
+ * DUTY Register Description
+ * [31:00] RW | Duty cycle of the output waveform
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_OFFSET 0x018
+/*
+ * OFFSET Register Description
+ * [31:00] RW | Offset of the output waveform, based on the PWM clock
+ * Constraints: 0 <= v <= (PERIOD - DUTY)
+ */
+
+#define PWMV4_REG_RPT 0x01C
+/*
+ * RPT Register Description
+ * [31:16] RW | Second dimensional of the effective number of waveform
+ * repetitions. Increases by one every first dimensional times.
+ * Value `n` means `n + 1` repetitions. The final number of
+ * repetitions of the waveform in one-shot mode is:
+ * `(first_dimensional + 1) * (second_dimensional + 1)`
+ * [15:00] RW | First dimensional of the effective number of waveform
+ * repetitions. Value `n` means `n + 1` repetitions.
+ */
+
+#define PWMV4_REG_FILTER_CTRL 0x020
+/*
+ * FILTER_CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:04] RW | Filter window number
+ * [03:01] RO | Reserved
+ * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled
+ */
+
+#define PWMV4_REG_CNT 0x024
+/*
+ * CNT Register Description
+ * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles,
+ * 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_ENABLE_DELAY 0x028
+/*
+ * ENABLE_DELAY Register Description
+ * [31:16] RO | Reserved
+ * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles
+ */
+
+#define PWMV4_REG_HPC 0x02C
+/*
+ * HPC Register Description
+ * [31:00] RW | Number of effective high polarity cycles of the input waveform
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_LPC 0x030
+/*
+ * LPC Register Description
+ * [31:00] RW | Number of effective low polarity cycles of the input waveform
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040
+/*
+ * BIPHASIC_CNT_CTRL0 Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0,
+ * 0 = rising edge (posedge), 1 = falling edge (negedge)
+ * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable
+ * [07:07] W1T | Synchronous Enable
+ * [06:06] W1T | Mode Switch
+ * 0 = Normal Mode, 1 = Switch timer clock and measured clock
+ * Constraints: "Biphasic Counter Mode" must be 0 if this is 1
+ * [05:03] RW | Biphasic Counter Mode
+ * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3,
+ * 0x4 = Mode 4, 0x5 = Reserved
+ * [02:02] RW | Biphasic Counter Clock Selection
+ * 0 = clock is from PLL and frequency can be configured
+ * 1 = clock is from crystal oscillator and frequency is fixed
+ * [01:01] RW | Biphasic Counter Continuous Mode
+ * [00:00] W1T | Biphasic Counter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044
+/*
+ * BIPHASIC_CNT_CTRL1 Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:11] RO | Reserved
+ * [10:04] RW | Biphasic Counter Filter Window Number
+ * [03:01] RO | Reserved
+ * [00:00] RW | Biphasic Counter Filter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048
+/*
+ * BIPHASIC_CNT_TIMER Register Description
+ * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter
+ * timer clock cycles
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C
+/*
+ * BIPHASIC_CNT_RES Register Description
+ * [31:00] RO | Biphasic Counter Result Value
+ * Constraints: Can only be read after INTSTS[9] is asserted
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050
+/*
+ * BIPHASIC_CNT_RES_S Register Description
+ * [31:00] RO | Biphasic Counter Result Value with synchronised processing
+ * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1
+ */
+
+#define PWMV4_REG_INTSTS 0x070
+/*
+ * INTSTS Register Description
+ * [31:10] RO | Reserved
+ * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted
+ * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted
+ * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted
+ * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted
+ * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted
+ * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted
+ * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted
+ * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted
+ * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted
+ * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted
+ */
+#define PWMV4_INT_LPC BIT(0)
+#define PWMV4_INT_HPC BIT(1)
+#define PWMV4_INT_LPC_W(v) FIELD_PREP_WM16(PWMV4_INT_LPC, \
+ ((v) ? 1 : 0))
+#define PWMV4_INT_HPC_W(v) FIELD_PREP_WM16(PWMV4_INT_HPC, \
+ ((v) ? 1 : 0))
+
+#define PWMV4_REG_INT_EN 0x074
+/*
+ * INT_EN Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled
+ * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled
+ * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled
+ * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled
+ * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled
+ * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled
+ * [03:03] W1C | Reload Interrupt Enable, 1 = enabled
+ * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled
+ * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled
+ * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled
+ */
+
+#define PWMV4_REG_INT_MASK 0x078
+/*
+ * INT_MASK Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked
+ * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked
+ * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked
+ * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked
+ * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked
+ * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked
+ * [03:03] W1C | Reload Interrupt Masked, 1 = masked
+ * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked
+ * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked
+ * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked
+ */
+
+static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg)
+{
+ return readl(base + reg);
+}
+
+static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val)
+{
+ writel(val, base + reg);
+}
+
+/**
+ * mfpwm_acquire - try becoming the active mfpwm function device
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * mfpwm device "function" drivers must call this function before doing anything
+ * that either modifies or relies on the parent device's state, such as clocks,
+ * enabling/disabling outputs, modifying shared regs etc.
+ *
+ * The return statues should always be checked.
+ *
+ * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release()
+ * calls once the device is no longer making changes that affect other devices,
+ * or stops producing user-visible effects that depend on the current device
+ * state being kept as-is. (e.g. after the PWM output signal is stopped)
+ *
+ * The same device function may mfpwm_acquire() multiple times while it already
+ * is active, i.e. it is re-entrant, though it needs to balance this with the
+ * same number of mfpwm_release() calls.
+ *
+ * Context: This function does not sleep.
+ *
+ * Return:
+ * * %0 - success
+ * * %-EBUSY - a different device function is active
+ * * %-EOVERFLOW - the acquire counter is at its maximum
+ */
+extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_release - drop usage of active mfpwm device function by 1
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * This is the balancing call to mfpwm_acquire(). If no users of the device
+ * function remain, set the mfpwm device to have no active device function,
+ * allowing other device functions to claim it.
+ */
+extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_get_mode - get the current mode the hardware is in
+ * @pwmf: pointer to a &struct rockchip_mfpwm_func
+ *
+ * Check the hardware registers of the PWM hardware to determine which mode it
+ * is currently operating in, if any.
+ *
+ * Returns:
+ * - %-EINVAL if @pwmf is %NULL or an error pointer
+ * - %-1 if the PWM hardware is off, regardless of operating mode
+ * - %PWMV4_MODE_ONESHOT if PWM hardware is in one-shot output mode
+ * - %PWMV4_MODE_CONT if PWM hardware is in continuous output mode
+ * - %PWMV4_MODE_CAPTURE if PWM hardware is in capture mode
+ */
+extern int mfpwm_get_mode(const struct rockchip_mfpwm_func *pwmf);
+
+#endif /* __SOC_ROCKCHIP_MFPWM_H__ */
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox