* Re: [PATCH 3/3] usb: typec: Use %pe to print error pointers
From: Badhri Jagan Sridharan @ 2026-07-20 11:38 UTC (permalink / raw)
To: Subasri S
Cc: Peter Chen, Greg Kroah-Hartman, Frank Li, Sascha Hauer,
Pengutronix Kernel Team, Fabio Estevam, Duncan Sands,
Chas Williams, Minas Harutyunyan, Hans de Goede, Heikki Krogerus,
linux-usb, imx, linux-arm-kernel, linux-kernel, linux-atm-general,
netdev
In-Reply-To: <20260719-usb-ptr_err_patchset-v1-3-85f7f2e4fefb@gmail.com>
On Sun, Jul 19, 2026 at 5:56 AM Subasri S <subasris1210@gmail.com> wrote:
>
> Use the %pe format specifier instead of %ld with PTR_ERR() for printing
> error pointers in various typec drivers. This prints symbolic
> error names (e.g.-ENOMEM) instead of errno numbers (e.g. -12),
> making error logs more readable.
>
> This patch fixes coccinelle reported warnings:
> ./typec/tcpm/tcpm.c:4799:60-67: WARNING: Consider using %pe to print PTR_ERR()
> ./typec/mux/pi3usb30532.c:143:3-10: WARNING: Consider using %pe to print PTR_ERR()
> ./typec/mux/pi3usb30532.c:155:3-10: WARNING: Consider using %pe to print PTR_ERR()
> ./typec/wusb3801.c:281:5-12: WARNING: Consider using %pe to print PTR_ERR()
>
> Compile tested only.
>
> Signed-off-by: Subasri S <subasris1210@gmail.com>
Reviewed-by: Badhri Jagan Sridharan <badhri@google.com>
> ---
> drivers/usb/typec/mux/pi3usb30532.c | 8 ++++----
> drivers/usb/typec/tcpm/tcpm.c | 2 +-
> drivers/usb/typec/wusb3801.c | 4 ++--
> 3 files changed, 7 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/usb/typec/mux/pi3usb30532.c b/drivers/usb/typec/mux/pi3usb30532.c
> index 985683fe49e9..a2357a28ecf4 100644
> --- a/drivers/usb/typec/mux/pi3usb30532.c
> +++ b/drivers/usb/typec/mux/pi3usb30532.c
> @@ -139,8 +139,8 @@ static int pi3usb30532_probe(struct i2c_client *client)
>
> pi->sw = typec_switch_register(dev, &sw_desc);
> if (IS_ERR(pi->sw)) {
> - dev_err(dev, "Error registering typec switch: %ld\n",
> - PTR_ERR(pi->sw));
> + dev_err(dev, "Error registering typec switch: %pe\n",
> + pi->sw);
> return PTR_ERR(pi->sw);
> }
>
> @@ -151,8 +151,8 @@ static int pi3usb30532_probe(struct i2c_client *client)
> pi->mux = typec_mux_register(dev, &mux_desc);
> if (IS_ERR(pi->mux)) {
> typec_switch_unregister(pi->sw);
> - dev_err(dev, "Error registering typec mux: %ld\n",
> - PTR_ERR(pi->mux));
> + dev_err(dev, "Error registering typec mux: %pe\n",
> + pi->mux);
> return PTR_ERR(pi->mux);
> }
>
> diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c
> index 89eec20a2064..ad10470ea73b 100644
> --- a/drivers/usb/typec/tcpm/tcpm.c
> +++ b/drivers/usb/typec/tcpm/tcpm.c
> @@ -4796,7 +4796,7 @@ static void tcpm_typec_connect(struct tcpm_port *port)
> port->partner_desc.accessory = TYPEC_ACCESSORY_NONE;
> partner = typec_register_partner(port->typec_port, &port->partner_desc);
> if (IS_ERR(partner)) {
> - dev_err(port->dev, "Failed to register partner (%ld)\n", PTR_ERR(partner));
> + dev_err(port->dev, "Failed to register partner (%pe)\n", partner);
> return;
> }
>
> diff --git a/drivers/usb/typec/wusb3801.c b/drivers/usb/typec/wusb3801.c
> index 6062875fb04a..86d13ab41670 100644
> --- a/drivers/usb/typec/wusb3801.c
> +++ b/drivers/usb/typec/wusb3801.c
> @@ -277,8 +277,8 @@ static void wusb3801_hw_update(struct wusb3801 *wusb3801)
> if (partner_type != WUSB3801_STAT_PARTNER_STANDBY) {
> wusb3801->partner = typec_register_partner(port, &desc);
> if (IS_ERR(wusb3801->partner))
> - dev_err(dev, "Failed to register partner: %ld\n",
> - PTR_ERR(wusb3801->partner));
> + dev_err(dev, "Failed to register partner: %pe\n",
> + wusb3801->partner);
> }
>
> data_role = pwr_role == TYPEC_SOURCE ? TYPEC_HOST : TYPEC_DEVICE;
>
> --
> 2.43.0
>
^ permalink raw reply
* Re: (subset) [PATCH] phy: rockchip: naneng-combphy: Always configure SSC spread direction
From: Vinod Koul @ 2026-07-20 11:36 UTC (permalink / raw)
To: Neil Armstrong, Heiko Stuebner, Shawn Lin, Alexey Charkov
Cc: linux-phy, linux-arm-kernel, linux-rockchip, linux-kernel, stable
In-Reply-To: <20260714-naneng-ssc-fix-v1-1-1c40a58061ae@flipper.net>
On Tue, 14 Jul 2026 19:41:20 +0400, Alexey Charkov wrote:
> Commit 0b31f297557f ("phy: rockchip: naneng-combphy: Consolidate SSC
> configuration") moved the SSC spread spectrum direction setup into the
> new rk_combphy_common_cfg_ssc() helper. That helper returns early when
> the 'rockchip,enable-ssc' property is absent, whereas the equivalent
> RK3568_PHYREG32 direction writes previously ran unconditionally in the
> per-type switch statements, independent of whether SSC modulation was
> actually enabled.
>
> [...]
Applied, thanks!
[1/1] phy: rockchip: naneng-combphy: Always configure SSC spread direction
commit: be2b5b17b7053fee142939076746d26b2d6c9702
Best regards,
--
~Vinod
^ permalink raw reply
* Re: [PATCH] net: stmmac: dwmac4: mask interrupts before stopping DMA in suspend
From: luis.la @ 2026-07-20 11:24 UTC (permalink / raw)
To: Maxime Chevallier, netdev
Cc: Andrew Lunn, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Maxime Coquelin, Alexandre Torgue,
Russell King (Oracle), Ovidiu Panait, Oleksij Rempel,
Rohan G Thomas, moderated list:ARM/STM32 ARCHITECTURE,
moderated list:ARM/STM32 ARCHITECTURE, open list
In-Reply-To: <5cc0d788-c78d-40f2-998f-235d40f567f2@bootlin.com>
Hi Maxime,
> This is almost perfect for a new submission, only 2 things to improve :
>
> - you're missing the tree name in the patch subject. As this is a fix, it should be :
>
> [PATCH net] net: stmmac: dwmac4: mask interrupts before stopping DMA in suspend
>
> you can find instructions for this in the doc Andrew linked :
>
> https://www.kernel.org/doc/html/latest/process/maintainer-netdev.html
>
> - When submitting a patch, do so in a dedicated thread, and not in reply to
> other threads.
>
> You should send a V2 addressing the tree name in the patch subject.
Thanks for the feedback, I addressed these changes and sent a v2 patch.
> Sashiko noted something on the patch :
>
> https://sashiko.dev/#/patchset/20260718152802.83553-1-luis.la%40mail.de
>
> It looks to me that sashiko is just wrong there though, I can't make sense
> of what it is saying.
This seems to be because strictly speaking the title of the commit was wrong,
the interrupts don't get deactivated before stopping dma.
I changed it, but only read after that it is discouraged to change the Subject, sorry...
I hope the patch now meets the requirements,
Luis
^ permalink raw reply
* [PATCH net v2] net: airoha: fix ETS channel derivation in airoha_tc_setup_qdisc_ets()
From: Lorenzo Bianconi @ 2026-07-20 11:22 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, Simon Horman
Derive the hardware QoS channel from opt->parent instead of opt->handle
in airoha_tc_setup_qdisc_ets(). The ETS qdisc handle is either
user-specified or auto-allocated by qdisc_alloc_handle() and bears no
relation to the HTB leaf classid that identifies the hardware channel.
HTB derives the channel from TC_H_MIN(opt->classid), and ETS is always
attached as a child of an HTB leaf, so its opt->parent matches that
classid. Using opt->handle instead can cause two ETS qdiscs on different
HTB leaves to collide on the same hardware channel, corrupting scheduler
configuration and stats.
Fixes: 20bf7d07c956 ("net: airoha: Add sched ETS offload support")
Reviewed-by: Simon Horman <horms@kernel.org>
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
Changes in v2:
- Rebase on top of net main branch
- Link to v1: https://lore.kernel.org/r/20260704-airoha-ets-handle-fix-v1-1-42516b3549a1@kernel.org
---
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 59001fd4b6f7..fac2aaefffff 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -2504,8 +2504,7 @@ static int airoha_tc_setup_qdisc_ets(struct net_device *dev,
if (opt->parent == TC_H_ROOT)
return -EINVAL;
- channel = TC_H_MAJ(opt->handle) >> 16;
- channel = channel % AIROHA_NUM_QOS_CHANNELS;
+ channel = TC_H_MIN(opt->parent) % AIROHA_NUM_QOS_CHANNELS;
switch (opt->command) {
case TC_ETS_REPLACE:
---
base-commit: e13caf1c26587434f0b768193100440939c0fb91
change-id: 20260704-airoha-ets-handle-fix-6c166de3d396
Best regards,
--
Lorenzo Bianconi <lorenzo@kernel.org>
^ permalink raw reply related
* [PATCH net v2] net: stmmac: dwmac4: mask interrupts when stopping DMA in suspend
From: Luis Lang @ 2026-07-20 11:15 UTC (permalink / raw)
To: netdev
Cc: Luis Lang, Andrew Lunn, Maxime Chevallier, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Maxime Coquelin, Alexandre Torgue, Russell King (Oracle),
Oleksij Rempel, Ovidiu Panait, Rohan G Thomas,
moderated list:ARM/STM32 ARCHITECTURE,
moderated list:ARM/STM32 ARCHITECTURE, open list
Since commit 1b9707e6f1a9 ("net: stmmac: enable RPS and RBU
interrupts"), suspending causes an interrupt storm from the RPS
interrupt.
Fix this by adding a deinit_chan() op to stmmac_dma_ops, which
masks all default dma channel interrupts. This is called from
stmmac_stop_all_dma(), so interrupts don't trigger while suspending.
Fixes: 1b9707e6f1a9 ("net: stmmac: enable RPS and RBU interrupts")
Suggested-by: Andrew Lunn <andrew@lunn.ch>
Suggested-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
Signed-off-by: Luis Lang <luis.la@mail.de>
---
.../net/ethernet/stmicro/stmmac/dwmac4_dma.c | 24 +++++++++++++++++++
drivers/net/ethernet/stmicro/stmmac/hwif.h | 4 ++++
.../net/ethernet/stmicro/stmmac/stmmac_main.c | 4 ++++
3 files changed, 32 insertions(+)
diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac4_dma.c b/drivers/net/ethernet/stmicro/stmmac/dwmac4_dma.c
index 829a23bdad01..23ffe1adcd0d 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac4_dma.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac4_dma.c
@@ -106,6 +106,17 @@ static void dwmac4_dma_init_channel(struct stmmac_priv *priv,
ioaddr + DMA_CHAN_INTR_ENA(dwmac4_addrs, chan));
}
+static void dwmac4_dma_deinit_channel(struct stmmac_priv *priv,
+ void __iomem *ioaddr, u32 chan)
+{
+ const struct dwmac4_addrs *dwmac4_addrs = priv->plat->dwmac4_addrs;
+ u32 value;
+
+ value = readl(ioaddr + DMA_CHAN_INTR_ENA(dwmac4_addrs, chan));
+ value &= ~DMA_CHAN_INTR_DEFAULT_MASK;
+ writel(value, ioaddr + DMA_CHAN_INTR_ENA(dwmac4_addrs, chan));
+}
+
static void dwmac410_dma_init_channel(struct stmmac_priv *priv,
void __iomem *ioaddr,
struct stmmac_dma_cfg *dma_cfg, u32 chan)
@@ -125,6 +136,17 @@ static void dwmac410_dma_init_channel(struct stmmac_priv *priv,
ioaddr + DMA_CHAN_INTR_ENA(dwmac4_addrs, chan));
}
+static void dwmac410_dma_deinit_channel(struct stmmac_priv *priv,
+ void __iomem *ioaddr, u32 chan)
+{
+ const struct dwmac4_addrs *dwmac4_addrs = priv->plat->dwmac4_addrs;
+ u32 value;
+
+ value = readl(ioaddr + DMA_CHAN_INTR_ENA(dwmac4_addrs, chan));
+ value &= ~DMA_CHAN_INTR_DEFAULT_MASK_4_10;
+ writel(value, ioaddr + DMA_CHAN_INTR_ENA(dwmac4_addrs, chan));
+}
+
static void dwmac4_dma_init(void __iomem *ioaddr,
struct stmmac_dma_cfg *dma_cfg)
{
@@ -548,6 +570,7 @@ const struct stmmac_dma_ops dwmac4_dma_ops = {
.reset = dwmac4_dma_reset,
.init = dwmac4_dma_init,
.init_chan = dwmac4_dma_init_channel,
+ .deinit_chan = dwmac4_dma_deinit_channel,
.init_rx_chan = dwmac4_dma_init_rx_chan,
.init_tx_chan = dwmac4_dma_init_tx_chan,
.axi = dwmac4_dma_axi,
@@ -577,6 +600,7 @@ const struct stmmac_dma_ops dwmac410_dma_ops = {
.reset = dwmac4_dma_reset,
.init = dwmac4_dma_init,
.init_chan = dwmac410_dma_init_channel,
+ .deinit_chan = dwmac410_dma_deinit_channel,
.init_rx_chan = dwmac4_dma_init_rx_chan,
.init_tx_chan = dwmac4_dma_init_tx_chan,
.axi = dwmac4_dma_axi,
diff --git a/drivers/net/ethernet/stmicro/stmmac/hwif.h b/drivers/net/ethernet/stmicro/stmmac/hwif.h
index e6317b94fff7..04dafec021b4 100644
--- a/drivers/net/ethernet/stmicro/stmmac/hwif.h
+++ b/drivers/net/ethernet/stmicro/stmmac/hwif.h
@@ -170,6 +170,8 @@ struct stmmac_dma_ops {
void (*init)(void __iomem *ioaddr, struct stmmac_dma_cfg *dma_cfg);
void (*init_chan)(struct stmmac_priv *priv, void __iomem *ioaddr,
struct stmmac_dma_cfg *dma_cfg, u32 chan);
+ void (*deinit_chan)(struct stmmac_priv *priv, void __iomem *ioaddr,
+ u32 chan);
void (*init_rx_chan)(struct stmmac_priv *priv, void __iomem *ioaddr,
struct stmmac_dma_cfg *dma_cfg,
dma_addr_t phy, u32 chan);
@@ -235,6 +237,8 @@ struct stmmac_dma_ops {
stmmac_do_void_callback(__priv, dma, init, __args)
#define stmmac_init_chan(__priv, __args...) \
stmmac_do_void_callback(__priv, dma, init_chan, __priv, __args)
+#define stmmac_deinit_chan(__priv, __args...) \
+ stmmac_do_void_callback(__priv, dma, deinit_chan, __priv, __args)
#define stmmac_init_rx_chan(__priv, __args...) \
stmmac_do_void_callback(__priv, dma, init_rx_chan, __priv, __args)
#define stmmac_init_tx_chan(__priv, __args...) \
diff --git a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
index 2a0d7eff88d3..af29a50ddb89 100644
--- a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
+++ b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
@@ -2560,6 +2560,7 @@ static void stmmac_stop_all_dma(struct stmmac_priv *priv)
{
u8 rx_channels_count = priv->plat->rx_queues_to_use;
u8 tx_channels_count = priv->plat->tx_queues_to_use;
+ u8 dma_csr_ch = max(rx_channels_count, tx_channels_count);
u8 chan;
for (chan = 0; chan < rx_channels_count; chan++)
@@ -2567,6 +2568,9 @@ static void stmmac_stop_all_dma(struct stmmac_priv *priv)
for (chan = 0; chan < tx_channels_count; chan++)
stmmac_stop_tx_dma(priv, chan);
+
+ for (chan = 0; chan < dma_csr_ch; chan++)
+ stmmac_deinit_chan(priv, priv->ioaddr, chan);
}
/**
--
2.55.0
^ permalink raw reply related
* Re: [PATCH v15 1/7] spi: pxa2xx: introduce clock enable and disable helper functions
From: Andy Shevchenko @ 2026-07-20 11:12 UTC (permalink / raw)
To: Shih-Yuan Lee
Cc: Mark Brown, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel
In-Reply-To: <20260720104029.13740-2-fourdollars@debian.org>
On Mon, Jul 20, 2026 at 06:40:23PM +0800, Shih-Yuan Lee wrote:
> The driver disables the clock during PM runtime suspend, PM system
> suspend, and device unbinding (remove). It also disables the clock
> on various error unwinding paths in pxa2xx_spi_probe().
You forgot to add cover letter (or forgot to Cc me). On top of that you haven't
waited for at least 24h (and for this series even more) as stated in the
Documentation. Last but not least, I only today answered with the people who
will be interested (in my opinion) to glance on this.
So, thanks for the effort.
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply
* Re: [PATCH] KVM: arm64: ptdump: Flush the last region
From: Wei-Lin Chang @ 2026-07-20 11:08 UTC (permalink / raw)
To: Mark Rutland
Cc: linux-arm-kernel, kvmarm, linux-kernel, Marc Zyngier,
Oliver Upton, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Dev Jain, Ryan Roberts, Sebastian Ene, Vincent Donnefort
In-Reply-To: <n5yni7vnama2ok2uzmhahrghrioqjbpcqui44v367rqc57qzqu@wfnio5stdmp3>
On Mon, Jul 20, 2026 at 11:58:44AM +0100, Wei-Lin Chang wrote:
> On Mon, Jul 20, 2026 at 09:35:01AM +0100, Mark Rutland wrote:
> > On Sat, Jul 18, 2026 at 12:12:33AM +0100, Wei-Lin Chang wrote:
> > > Currently the stage-2 ptdump calls note_page() at each leaf entry visit.
> > > This simply misses the output of the last region, because note_page()
> > > only dumps output when it detects a change in level/prot, or when the
> > > walk enters a next marker section. The last region in the guest IPA
> > > space with the same level/prot is not dumped since there is no change
> > > after it.
> > >
> > > To dump the final region, manually issue a note_page() call with address
> > > BIT(ia_bits) (end of guest IPA space) and level == -1 to trigger a level
> > > change after the last region. Additionally treat level == -1 as a last
> > > flushing note_page() call and avoid dumping the name of the next marker
> > > for this case.
> >
> > I think you can use note_page_flush() rather than doing that manually.
> >
> > > Fixes: 7c4f73548ed1 ("KVM: arm64: Register ptdump with debugfs on guest creation")
> > > Reported-by: Sashiko AI <sashiko-bot@kernel.org>
> > > Closes: https://lore.kernel.org/kvmarm/20260630122758.891011F00A3A@smtp.kernel.org/
> > > Signed-off-by: Wei-Lin Chang <weilin.chang@arm.com>
> > > ---
> > > arch/arm64/kvm/ptdump.c | 8 +++++---
> > > arch/arm64/mm/ptdump.c | 5 +++--
> > > 2 files changed, 8 insertions(+), 5 deletions(-)
> > >
> > > diff --git a/arch/arm64/kvm/ptdump.c b/arch/arm64/kvm/ptdump.c
> > > index c9140e22abcf..4096e4a92fae 100644
> > > --- a/arch/arm64/kvm/ptdump.c
> > > +++ b/arch/arm64/kvm/ptdump.c
> > > @@ -155,11 +155,13 @@ static int kvm_ptdump_guest_show(struct seq_file *m, void *unused)
> > > .seq = m,
> > > };
> > >
> > > - write_lock(&kvm->mmu_lock);
> > > + guard(write_lock)(&kvm->mmu_lock);
> > > ret = kvm_pgtable_walk(mmu->pgt, 0, BIT(mmu->pgt->ia_bits), &walker);
> > > - write_unlock(&kvm->mmu_lock);
> > > + if (ret)
> > > + return ret;
> > > + note_page(&st->parser_state.ptdump, BIT(mmu->pgt->ia_bits), -1, 0);
> >
> > This can be:
> >
> > note_page_flush(&st->parser_state.ptdump);
> >
> > The level change alone should trigger the dump, so the address doesn't
> > need to be at the end of the guest IPA space.
> >
> > Importantly, note_page_flush() will pass 0 as the address, which won't
> > trigger the checks you try to suppress below.
> >
> > Please use note_page_flush() here, and drop the changes to
> > arch/arm64/mm/ptdump.c.
>
> Sorry, I shouldn't have omitted this information, but I did try
> note_page_flush(). And it gives something like this in the last row:
>
> 0x00000000ffc00000-0x0000000000000000 17592186040324M 2 R W px ux AF BLK
>
> The astronomical size is from (addr - st->start_address). As IA bits for
> stage-2 are not close to 64, we'll have large sizes for the last row.
> That's one of the reasons I chose to call note_page() with
> BIT(pgtable->ia_bits) as addr, to end the ptdump at the end of the guest
> IPA space.
>
> Additionally, the last row is combining two ranges:
>
> 1. 0x00000000ffc00000-0x0000000100000000 4M 2 R W px ux AF BLK
> 2. 0x0000000100000000-0x0000000000000000 BIG_SIZE - (empty prot)
>
> The attributes are wrong for the large range after
> BIT(pgtable->ia_bits). This is because before dumping the last row, the
> ptdump code is waiting to be notified of the end of the final region
> with all those {R, W, px, ux, AF, BLK} attributes. Using
> note_page_flush() essentially tells it the valid range ends at 1 << 64.
> So actually using note_page() with BIT(pgtable->ia_bits) is required for
> correctness.
>
> The kernel ptdump is not affected by this (at least from my quick test):
>
> 0xffffffffff6fe000-0xffffffffff800000 1032K PTE
> 0xffffffffff800000-0x0000000000000000 8M PMD
>
> Because it uses addresses close to the end of the address space, and its
> last leaf PTE entry is an invalid one.
Hmm no. It's because the kernel page table is capable of translating the
full range 0xffff000000000000-0x0000000000000000. This isn't true for
stage-2 page tables.
Thanks,
Wei-Lin Chang
>
> Thanks,
> Wei-Lin Chang
>
> >
> > >
> > > - return ret;
> > > + return 0;
> > > }
> > >
> > > static int kvm_ptdump_guest_open(struct inode *m, struct file *file)
> > > diff --git a/arch/arm64/mm/ptdump.c b/arch/arm64/mm/ptdump.c
> > > index ab9899ca1e5f..fed4e4407e0e 100644
> > > --- a/arch/arm64/mm/ptdump.c
> > > +++ b/arch/arm64/mm/ptdump.c
> > > @@ -194,6 +194,7 @@ void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
> > > struct ptdump_pg_state *st = container_of(pt_st, struct ptdump_pg_state, ptdump);
> > > struct ptdump_pg_level *pg_level = st->pg_level;
> > > static const char units[] = "KMGTPE";
> > > + bool flush = level == -1;
> > > ptdesc_t prot = 0;
> > >
> > > /* check if the current level has been folded dynamically */
> > > @@ -234,7 +235,7 @@ void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
> > > pg_level[st->level].num);
> > > pt_dump_seq_puts(st->seq, "\n");
> > >
> > > - if (addr >= st->marker[1].start_address) {
> > > + if (addr >= st->marker[1].start_address && !flush) {
> > > st->marker++;
> > > pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
> > > }
> >
> > If you use note_page_flush(), addr will be 0. As the final marker is at
> > BIT(pgtable->ia_bits), this condition cannot fire.
> >
> > > @@ -244,7 +245,7 @@ void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
> > > st->level = level;
> > > }
> > >
> > > - if (addr >= st->marker[1].start_address) {
> > > + if (addr >= st->marker[1].start_address && !flush) {
> > > st->marker++;
> > > pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
> > > }
> >
> > Likewise here.
> >
> > Mark.
^ permalink raw reply
* Re: [PATCH] KVM: arm64: ptdump: Flush the last region
From: Wei-Lin Chang @ 2026-07-20 10:58 UTC (permalink / raw)
To: Mark Rutland
Cc: linux-arm-kernel, kvmarm, linux-kernel, Marc Zyngier,
Oliver Upton, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Dev Jain, Ryan Roberts, Sebastian Ene, Vincent Donnefort
In-Reply-To: <al3dtUVPxBXY0AJP@J2N7QTR9R3.cambridge.arm.com>
On Mon, Jul 20, 2026 at 09:35:01AM +0100, Mark Rutland wrote:
> On Sat, Jul 18, 2026 at 12:12:33AM +0100, Wei-Lin Chang wrote:
> > Currently the stage-2 ptdump calls note_page() at each leaf entry visit.
> > This simply misses the output of the last region, because note_page()
> > only dumps output when it detects a change in level/prot, or when the
> > walk enters a next marker section. The last region in the guest IPA
> > space with the same level/prot is not dumped since there is no change
> > after it.
> >
> > To dump the final region, manually issue a note_page() call with address
> > BIT(ia_bits) (end of guest IPA space) and level == -1 to trigger a level
> > change after the last region. Additionally treat level == -1 as a last
> > flushing note_page() call and avoid dumping the name of the next marker
> > for this case.
>
> I think you can use note_page_flush() rather than doing that manually.
>
> > Fixes: 7c4f73548ed1 ("KVM: arm64: Register ptdump with debugfs on guest creation")
> > Reported-by: Sashiko AI <sashiko-bot@kernel.org>
> > Closes: https://lore.kernel.org/kvmarm/20260630122758.891011F00A3A@smtp.kernel.org/
> > Signed-off-by: Wei-Lin Chang <weilin.chang@arm.com>
> > ---
> > arch/arm64/kvm/ptdump.c | 8 +++++---
> > arch/arm64/mm/ptdump.c | 5 +++--
> > 2 files changed, 8 insertions(+), 5 deletions(-)
> >
> > diff --git a/arch/arm64/kvm/ptdump.c b/arch/arm64/kvm/ptdump.c
> > index c9140e22abcf..4096e4a92fae 100644
> > --- a/arch/arm64/kvm/ptdump.c
> > +++ b/arch/arm64/kvm/ptdump.c
> > @@ -155,11 +155,13 @@ static int kvm_ptdump_guest_show(struct seq_file *m, void *unused)
> > .seq = m,
> > };
> >
> > - write_lock(&kvm->mmu_lock);
> > + guard(write_lock)(&kvm->mmu_lock);
> > ret = kvm_pgtable_walk(mmu->pgt, 0, BIT(mmu->pgt->ia_bits), &walker);
> > - write_unlock(&kvm->mmu_lock);
> > + if (ret)
> > + return ret;
> > + note_page(&st->parser_state.ptdump, BIT(mmu->pgt->ia_bits), -1, 0);
>
> This can be:
>
> note_page_flush(&st->parser_state.ptdump);
>
> The level change alone should trigger the dump, so the address doesn't
> need to be at the end of the guest IPA space.
>
> Importantly, note_page_flush() will pass 0 as the address, which won't
> trigger the checks you try to suppress below.
>
> Please use note_page_flush() here, and drop the changes to
> arch/arm64/mm/ptdump.c.
Sorry, I shouldn't have omitted this information, but I did try
note_page_flush(). And it gives something like this in the last row:
0x00000000ffc00000-0x0000000000000000 17592186040324M 2 R W px ux AF BLK
The astronomical size is from (addr - st->start_address). As IA bits for
stage-2 are not close to 64, we'll have large sizes for the last row.
That's one of the reasons I chose to call note_page() with
BIT(pgtable->ia_bits) as addr, to end the ptdump at the end of the guest
IPA space.
Additionally, the last row is combining two ranges:
1. 0x00000000ffc00000-0x0000000100000000 4M 2 R W px ux AF BLK
2. 0x0000000100000000-0x0000000000000000 BIG_SIZE - (empty prot)
The attributes are wrong for the large range after
BIT(pgtable->ia_bits). This is because before dumping the last row, the
ptdump code is waiting to be notified of the end of the final region
with all those {R, W, px, ux, AF, BLK} attributes. Using
note_page_flush() essentially tells it the valid range ends at 1 << 64.
So actually using note_page() with BIT(pgtable->ia_bits) is required for
correctness.
The kernel ptdump is not affected by this (at least from my quick test):
0xffffffffff6fe000-0xffffffffff800000 1032K PTE
0xffffffffff800000-0x0000000000000000 8M PMD
Because it uses addresses close to the end of the address space, and its
last leaf PTE entry is an invalid one.
Thanks,
Wei-Lin Chang
>
> >
> > - return ret;
> > + return 0;
> > }
> >
> > static int kvm_ptdump_guest_open(struct inode *m, struct file *file)
> > diff --git a/arch/arm64/mm/ptdump.c b/arch/arm64/mm/ptdump.c
> > index ab9899ca1e5f..fed4e4407e0e 100644
> > --- a/arch/arm64/mm/ptdump.c
> > +++ b/arch/arm64/mm/ptdump.c
> > @@ -194,6 +194,7 @@ void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
> > struct ptdump_pg_state *st = container_of(pt_st, struct ptdump_pg_state, ptdump);
> > struct ptdump_pg_level *pg_level = st->pg_level;
> > static const char units[] = "KMGTPE";
> > + bool flush = level == -1;
> > ptdesc_t prot = 0;
> >
> > /* check if the current level has been folded dynamically */
> > @@ -234,7 +235,7 @@ void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
> > pg_level[st->level].num);
> > pt_dump_seq_puts(st->seq, "\n");
> >
> > - if (addr >= st->marker[1].start_address) {
> > + if (addr >= st->marker[1].start_address && !flush) {
> > st->marker++;
> > pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
> > }
>
> If you use note_page_flush(), addr will be 0. As the final marker is at
> BIT(pgtable->ia_bits), this condition cannot fire.
>
> > @@ -244,7 +245,7 @@ void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
> > st->level = level;
> > }
> >
> > - if (addr >= st->marker[1].start_address) {
> > + if (addr >= st->marker[1].start_address && !flush) {
> > st->marker++;
> > pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
> > }
>
> Likewise here.
>
> Mark.
^ permalink raw reply
* [PATCH v15 5/7] spi: pxa2xx: disable DMA for Apple MacBook8,1
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
On MacBook8,1 (early 2015 12" MacBook), the LPSS SPI controller at
00:15.4 suffers from hardware DMA handshake failures and interrupt
routing bugs, causing keyboard/touchpad transactions to fail when
DMA is enabled.
Move the forced PIO mode DMI quirk to spi-pxa2xx-pci.c (the LPSS host
controller PCI glue driver) to avoid layering violations in client
drivers (such as applespi).
Add an explicit DMI match table for MacBook8,1 and a module parameter
spi_pxa2xx_force_pio to allow forcing PIO mode on demand.
Link: https://bugzilla.kernel.org/show_bug.cgi?id=108331
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx-pci.c | 35 +++++++++++++++++++++++++++++++++--
1 file changed, 33 insertions(+), 2 deletions(-)
diff --git a/drivers/spi/spi-pxa2xx-pci.c b/drivers/spi/spi-pxa2xx-pci.c
index cae77ac18520..31bdaa096d9e 100644
--- a/drivers/spi/spi-pxa2xx-pci.c
+++ b/drivers/spi/spi-pxa2xx-pci.c
@@ -18,9 +18,14 @@
#include <linux/dmaengine.h>
#include <linux/platform_data/dma-dw.h>
+#include <linux/dmi.h>
#include "spi-pxa2xx.h"
+static bool spi_pxa2xx_force_pio;
+module_param_named(force_pio, spi_pxa2xx_force_pio, bool, 0444);
+MODULE_PARM_DESC(force_pio, "Force PIO mode (disables DMA) for SPI transfers. ([0] = disabled, 1 = enabled)");
+
#define PCI_DEVICE_ID_INTEL_QUARK_X1000 0x0935
#define PCI_DEVICE_ID_INTEL_BYT 0x0f0e
#define PCI_DEVICE_ID_INTEL_MRFLD 0x1194
@@ -93,6 +98,32 @@ static void lpss_dma_put_device(void *dma_dev)
pci_dev_put(dma_dev);
}
+static const struct dmi_system_id pxa2xx_spi_pci_dmi_table[] = {
+ {
+ .ident = "Apple MacBook8,1",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "Apple Inc."),
+ DMI_MATCH(DMI_PRODUCT_NAME, "MacBook8,1"),
+ },
+ },
+ { }
+};
+
+static bool pxa2xx_spi_pci_can_dma(struct pci_dev *dev)
+{
+ if (spi_pxa2xx_force_pio) {
+ pci_info(dev, "Forcing PIO mode (disabling DMA)\n");
+ return false;
+ }
+
+ if (dmi_check_system(pxa2xx_spi_pci_dmi_table)) {
+ pci_info(dev, "MacBook8,1 detected: disabling DMA to force PIO mode\n");
+ return false;
+ }
+
+ return true;
+}
+
static int lpss_spi_setup(struct pci_dev *dev, struct pxa2xx_spi_controller *c)
{
struct ssp_device *ssp = &c->ssp;
@@ -166,7 +197,7 @@ static int lpss_spi_setup(struct pci_dev *dev, struct pxa2xx_spi_controller *c)
c->dma_filter = lpss_dma_filter;
c->dma_burst_size = 1;
- c->enable_dma = 1;
+ c->enable_dma = pxa2xx_spi_pci_can_dma(dev);
return 0;
}
@@ -238,7 +269,7 @@ static int mrfld_spi_setup(struct pci_dev *dev, struct pxa2xx_spi_controller *c)
c->dma_filter = lpss_dma_filter;
c->dma_burst_size = 8;
- c->enable_dma = 1;
+ c->enable_dma = pxa2xx_spi_pci_can_dma(dev);
return 0;
}
--
2.39.5
^ permalink raw reply related
* [PATCH v15 6/7] spi: pxa2xx: restore LPSS private register state on S3 resume
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
Intel LPSS SPI controllers lose all private register state across S3
suspend because the LPSS power domain is fully removed. On resume the
driver only re-enables the SSP clock, leaving the LPSS private registers
in their power-on-reset state, which causes two problems:
1. LPSS_PRIV_RESETS (offset 0x04 within the LPSS private space) stays
zero, keeping the functional block in reset. Any MMIO access while
the block is held in reset causes a PCIe Completion Timeout and a
watchdog-triggered system reset. LPSS_PRIV_RESETS_FUNC and
LPSS_PRIV_RESETS_IDMA must be de-asserted before any other register
access on resume.
2. The LPSS software chip-select control register must not be blindly
restored from its suspend-time snapshot: if CS was asserted at the
moment of suspend, restoring that state corrupts the first
post-resume SPI transaction. Instead, call lpss_ssp_setup() which
unconditionally writes SW_MODE | CS_HIGH (idle/deasserted), matching
the state established at probe time.
To resolve these issues safely:
- Wrap S3 suspend/resume with pm_runtime_resume_and_get() and
pm_runtime_put_autosuspend() to guarantee active clocks during MMIO
access and preserve PM reference counting.
- Restrict LPSS private register save/restore to LPT, BYT, and BSW
platforms via pxa2xx_spi_need_lpss_restore() (newer platforms are
handled by intel-lpss.c).
- Save only the first 6 LPSS private registers (offsets 0x00..0x14) in
drv_data during suspend, avoiding reserved offsets beyond 0x14.
- On resume, de-assert resets first, restore saved registers, call
lpss_ssp_setup(), and clear drv_data->suspended to prevent unclocked
IRQ access.
- Add error recovery paths for spi_controller_suspend/resume failures.
Link: https://bugzilla.kernel.org/show_bug.cgi?id=108331
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx.c | 95 ++++++++++++++++++++++++++++++++++++++--
drivers/spi/spi-pxa2xx.h | 1 +
2 files changed, 92 insertions(+), 4 deletions(-)
diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index 88ed28096b07..56e434f6e794 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -72,6 +72,11 @@ struct chip_data {
#define LPSS_CAPS_CS_EN_SHIFT 9
#define LPSS_CAPS_CS_EN_MASK (0xf << LPSS_CAPS_CS_EN_SHIFT)
+/* Offsets from drv_data->lpss_base */
+#define LPSS_PRIV_RESETS 0x04
+#define LPSS_PRIV_RESETS_IDMA BIT(2)
+#define LPSS_PRIV_RESETS_FUNC 0x3
+
#define LPSS_PRIV_CLOCK_GATE 0x38
#define LPSS_PRIV_CLOCK_GATE_CLK_CTL_MASK 0x3
#define LPSS_PRIV_CLOCK_GATE_CLK_CTL_FORCE_ON 0x3
@@ -189,6 +194,18 @@ static bool is_lpss_ssp(const struct driver_data *drv_data)
}
}
+static bool pxa2xx_spi_need_lpss_restore(const struct driver_data *drv_data)
+{
+ switch (drv_data->ssp_type) {
+ case LPSS_LPT_SSP:
+ case LPSS_BYT_SSP:
+ case LPSS_BSW_SSP:
+ return true;
+ default:
+ return false;
+ }
+}
+
static bool is_quark_x1000_ssp(const struct driver_data *drv_data)
{
return drv_data->ssp_type == QUARK_X1000_SSP;
@@ -1527,17 +1544,45 @@ static int pxa2xx_spi_suspend(struct device *dev)
struct ssp_device *ssp = drv_data->ssp;
int status;
+ status = pm_runtime_resume_and_get(dev);
+ if (status < 0)
+ return status;
+
status = spi_controller_suspend(drv_data->controller);
if (status)
- return status;
+ goto out_put;
+ /* Disable SSP interrupt generation on hardware level while clock is active */
pxa2xx_spi_off(drv_data);
+
+ /* Mark as suspended and synchronize IRQ before disabling clock */
drv_data->suspended = true;
synchronize_irq(ssp->irq);
+ if (pxa2xx_spi_need_lpss_restore(drv_data)) {
+ unsigned int i;
+
+ /*
+ * Save the first 6 LPSS private registers (offsets 0x00 to 0x14)
+ * while the clock is still enabled. They are lost when the LPSS
+ * power domain is removed across S3 and must be restored on resume.
+ * Use drv_data->lpss_base so the correct per-platform offset
+ * is applied regardless of LPSS IP revision.
+ * Registers beyond 0x14 (except CS control at 0x18) are reserved
+ * or unimplemented on LPT, and accessing them triggers a PCIe
+ * Completion Timeout causing a system halt.
+ */
+ for (i = 0; i < 6; i++)
+ drv_data->lpss_priv_ctx[i] = readl(drv_data->lpss_base + i * 4);
+ }
+
pxa2xx_spi_clk_disable(drv_data);
return 0;
+
+out_put:
+ pm_runtime_put_noidle(dev);
+ return status;
}
static int pxa2xx_spi_resume(struct device *dev)
@@ -1550,9 +1595,47 @@ static int pxa2xx_spi_resume(struct device *dev)
if (!pm_runtime_suspended(dev)) {
status = pxa2xx_spi_clk_enable(drv_data);
if (status)
- return status;
+ goto out_put;
}
+ if (pxa2xx_spi_need_lpss_restore(drv_data)) {
+ unsigned int i;
+
+ /*
+ * The LPSS power domain is removed across S3, taking
+ * all private registers with it. De-assert the
+ * functional block and IDMA resets first; any MMIO
+ * access while the block is held in reset causes a
+ * PCIe Completion Timeout and a watchdog-triggered
+ * system reset.
+ */
+ writel(LPSS_PRIV_RESETS_FUNC | LPSS_PRIV_RESETS_IDMA,
+ drv_data->lpss_base + LPSS_PRIV_RESETS);
+
+ /* Restore the other 5 saved private registers */
+ for (i = 0; i < 6; i++) {
+ if (i == LPSS_PRIV_RESETS / 4)
+ continue;
+ writel(drv_data->lpss_priv_ctx[i],
+ drv_data->lpss_base + i * 4);
+ }
+ }
+
+ if (is_lpss_ssp(drv_data)) {
+ /*
+ * Re-initialise the SW chip-select control register so
+ * CS starts deasserted (SW_MODE | CS_HIGH), regardless
+ * of the state it was in at suspend time. A stale
+ * asserted CS on the first post-resume transaction
+ * corrupts the write-status response from the device.
+ */
+ lpss_ssp_setup(drv_data);
+ }
+
+ /*
+ * Now that resets are de-asserted and registers are restored,
+ * it is safe to handle interrupts.
+ */
drv_data->suspended = false;
/* Start the queue running */
@@ -1561,10 +1644,14 @@ static int pxa2xx_spi_resume(struct device *dev)
drv_data->suspended = true;
synchronize_irq(ssp->irq);
pxa2xx_spi_clk_disable(drv_data);
- return status;
+ goto out_put;
}
- return 0;
+out_put:
+ if (!pm_runtime_suspended(dev))
+ pm_runtime_put_noidle(dev);
+
+ return status;
}
static int pxa2xx_spi_runtime_suspend(struct device *dev)
diff --git a/drivers/spi/spi-pxa2xx.h b/drivers/spi/spi-pxa2xx.h
index 44f37bf9c519..48169494f74e 100644
--- a/drivers/spi/spi-pxa2xx.h
+++ b/drivers/spi/spi-pxa2xx.h
@@ -71,6 +71,7 @@ struct driver_data {
irqreturn_t (*transfer_handler)(struct driver_data *drv_data);
void __iomem *lpss_base;
+ u32 lpss_priv_ctx[6];
bool suspended;
bool clk_enabled;
--
2.39.5
^ permalink raw reply related
* [PATCH v15 7/7] spi: pxa2xx: rename local status variable to ret
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
Rename the return value variable name from 'status' to 'ret' in the
pxa2xx_spi_probe(), pxa2xx_spi_suspend(), pxa2xx_spi_resume(), and
pxa2xx_spi_runtime_resume() functions to conform to standard Linux kernel
coding conventions.
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx.c | 62 ++++++++++++++++++++--------------------
1 file changed, 31 insertions(+), 31 deletions(-)
diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index 56e434f6e794..612361ffe6dc 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -1313,7 +1313,7 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
struct spi_controller *controller;
struct driver_data *drv_data;
const struct lpss_config *config;
- int status;
+ int ret;
u32 tmp;
if (platform_info->is_target)
@@ -1372,8 +1372,8 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
/* Setup DMA if requested */
if (platform_info->enable_dma) {
- status = pxa2xx_spi_dma_setup(drv_data);
- if (status) {
+ ret = pxa2xx_spi_dma_setup(drv_data);
+ if (ret) {
dev_warn(dev, "no DMA channels available, using PIO\n");
platform_info->enable_dma = false;
} else {
@@ -1387,16 +1387,16 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
}
/* Enable SOC clock */
- status = pxa2xx_spi_clk_enable(drv_data);
- if (status)
+ ret = pxa2xx_spi_clk_enable(drv_data);
+ if (ret)
goto out_error_dma_alloc;
drv_data->suspended = false;
- status = request_irq(ssp->irq, ssp_int, IRQF_SHARED, dev_name(dev),
+ ret = request_irq(ssp->irq, ssp_int, IRQF_SHARED, dev_name(dev),
drv_data);
- if (status < 0) {
- status = dev_err_probe(dev, status, "cannot get IRQ %d\n", ssp->irq);
+ if (ret < 0) {
+ ret = dev_err_probe(dev, ret, "cannot get IRQ %d\n", ssp->irq);
goto out_error_clock_enabled;
}
@@ -1477,23 +1477,23 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
drv_data->gpiod_ready = devm_gpiod_get_optional(dev,
"ready", GPIOD_OUT_LOW);
if (IS_ERR(drv_data->gpiod_ready)) {
- status = PTR_ERR(drv_data->gpiod_ready);
+ ret = PTR_ERR(drv_data->gpiod_ready);
goto out_error_irq_alloc;
}
}
/* Register with the SPI framework */
dev_set_drvdata(dev, drv_data);
- status = spi_register_controller(controller);
- if (status) {
- dev_err_probe(dev, status, "problem registering SPI controller\n");
+ ret = spi_register_controller(controller);
+ if (ret) {
+ dev_err_probe(dev, ret, "problem registering SPI controller\n");
goto out_error_irq_alloc;
}
if (is_lpss_ssp(drv_data) && !platform_info->enable_dma)
pm_runtime_get_noresume(dev);
- return status;
+ return ret;
out_error_irq_alloc:
free_irq(ssp->irq, drv_data);
@@ -1504,7 +1504,7 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
out_error_dma_alloc:
pxa2xx_spi_dma_release(drv_data);
- return status;
+ return ret;
}
EXPORT_SYMBOL_NS_GPL(pxa2xx_spi_probe, "SPI_PXA2xx");
@@ -1542,14 +1542,14 @@ static int pxa2xx_spi_suspend(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
struct ssp_device *ssp = drv_data->ssp;
- int status;
+ int ret;
- status = pm_runtime_resume_and_get(dev);
- if (status < 0)
- return status;
+ ret = pm_runtime_resume_and_get(dev);
+ if (ret < 0)
+ return ret;
- status = spi_controller_suspend(drv_data->controller);
- if (status)
+ ret = spi_controller_suspend(drv_data->controller);
+ if (ret)
goto out_put;
/* Disable SSP interrupt generation on hardware level while clock is active */
@@ -1582,19 +1582,19 @@ static int pxa2xx_spi_suspend(struct device *dev)
out_put:
pm_runtime_put_noidle(dev);
- return status;
+ return ret;
}
static int pxa2xx_spi_resume(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
struct ssp_device *ssp = drv_data->ssp;
- int status;
+ int ret;
/* Enable the SSP clock */
if (!pm_runtime_suspended(dev)) {
- status = pxa2xx_spi_clk_enable(drv_data);
- if (status)
+ ret = pxa2xx_spi_clk_enable(drv_data);
+ if (ret)
goto out_put;
}
@@ -1639,8 +1639,8 @@ static int pxa2xx_spi_resume(struct device *dev)
drv_data->suspended = false;
/* Start the queue running */
- status = spi_controller_resume(drv_data->controller);
- if (status) {
+ ret = spi_controller_resume(drv_data->controller);
+ if (ret) {
drv_data->suspended = true;
synchronize_irq(ssp->irq);
pxa2xx_spi_clk_disable(drv_data);
@@ -1651,7 +1651,7 @@ static int pxa2xx_spi_resume(struct device *dev)
if (!pm_runtime_suspended(dev))
pm_runtime_put_noidle(dev);
- return status;
+ return ret;
}
static int pxa2xx_spi_runtime_suspend(struct device *dev)
@@ -1668,11 +1668,11 @@ static int pxa2xx_spi_runtime_suspend(struct device *dev)
static int pxa2xx_spi_runtime_resume(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
- int status;
+ int ret;
- status = pxa2xx_spi_clk_enable(drv_data);
- if (status)
- return status;
+ ret = pxa2xx_spi_clk_enable(drv_data);
+ if (ret)
+ return ret;
drv_data->suspended = false;
return 0;
--
2.39.5
^ permalink raw reply related
* [PATCH v15 4/7] spi: pxa2xx: lock out runtime autosuspend for Intel LPSS SPI in PIO mode
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
When operating in PIO mode on Intel LPSS SPI controllers, runtime PM
autosuspend clock-gates the hardware block when idle. On subsequent
transfers, MMIO accesses to trigger resume are performed before the runtime
PM state machine can wake the controller, causing PCIe Completion Timeouts.
This issue does not affect DMA mode, where the DMA engine holds the required
resources, nor does it affect non-LPSS controllers.
Lock out runtime PM autosuspend for LPSS SPI controllers specifically
when operating in PIO mode.
Acquire a runtime PM reference via pm_runtime_get_noresume() in
pxa2xx_spi_probe() after registration, and release it via
pm_runtime_put_sync() in pxa2xx_spi_remove() to ensure the runtime PM state
machine transitions to RPM_SUSPENDED upon driver unbind, preventing active
child count leaks on the parent LPSS power domain.
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index 64b5ec744756..88ed28096b07 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -1473,6 +1473,9 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
goto out_error_irq_alloc;
}
+ if (is_lpss_ssp(drv_data) && !platform_info->enable_dma)
+ pm_runtime_get_noresume(dev);
+
return status;
out_error_irq_alloc:
@@ -1513,6 +1516,8 @@ void pxa2xx_spi_remove(struct device *dev)
/* Release DMA */
if (drv_data->controller_info->enable_dma)
pxa2xx_spi_dma_release(drv_data);
+ else if (is_lpss_ssp(drv_data))
+ pm_runtime_put_sync(dev);
}
EXPORT_SYMBOL_NS_GPL(pxa2xx_spi_remove, "SPI_PXA2xx");
--
2.39.5
^ permalink raw reply related
* [PATCH v15 3/7] spi: pxa2xx: overhaul teardown and suspend sequence using pxa2xx_spi_off
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
When removing the driver or suspending the device, the clock must not
be disabled while shared interrupts are still active. Gating the clock
before waiting for in-flight interrupt handlers to complete results
in race conditions where the handler performs unclocked MMIO accesses,
causing PCIe Completion Timeouts.
Overhaul the remove, suspend, and runtime_suspend paths to use a strict
synchronized teardown order:
1. Disable hardware interrupt generation at the controller level.
2. Mark the device state as suspended (suspended = true) to prevent
subsequent interrupt handlers from attempting MMIO reads.
3. Call synchronize_irq() to wait for any active interrupt handlers
to drain completely.
4. Gate the clock via pxa2xx_spi_clk_disable().
Additionally, commit 29d7e05c5f75 ("spi: pxa2xx: Avoid touching
SSCR0_SSE on MMP2") documented that disabling the hardware block via SSE on
MMP2 SoC platforms corrupts the RX/TX FIFO. Instead of calling
pxa_ssp_disable() directly, use the helper function pxa2xx_spi_off(),
which respects the MMP2 platform quirk by bypassing SSE register writes.
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx.c | 49 ++++++++++++++++++++++++++++++++--------
1 file changed, 39 insertions(+), 10 deletions(-)
diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index d4a9a48c2624..64b5ec744756 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -1495,16 +1495,24 @@ void pxa2xx_spi_remove(struct device *dev)
spi_unregister_controller(drv_data->controller);
- /* Disable the SSP at the peripheral and SOC level */
- pxa_ssp_disable(ssp);
+ /* Disable SSP interrupt generation on hardware level while clock is active */
+ pxa2xx_spi_off(drv_data);
+
+ /* Mark as suspended to prevent further IRQ handling */
+ drv_data->suspended = true;
+
+ /* Wait for any pending interrupt handlers to complete */
+ synchronize_irq(ssp->irq);
+
+ /* Release IRQ */
+ free_irq(ssp->irq, drv_data);
+
+ /* Safe to disable the SSP clock now */
pxa2xx_spi_clk_disable(drv_data);
/* Release DMA */
if (drv_data->controller_info->enable_dma)
pxa2xx_spi_dma_release(drv_data);
-
- /* Release IRQ */
- free_irq(ssp->irq, drv_data);
}
EXPORT_SYMBOL_NS_GPL(pxa2xx_spi_remove, "SPI_PXA2xx");
@@ -1518,10 +1526,11 @@ static int pxa2xx_spi_suspend(struct device *dev)
if (status)
return status;
- pxa_ssp_disable(ssp);
+ pxa2xx_spi_off(drv_data);
+ drv_data->suspended = true;
+ synchronize_irq(ssp->irq);
- if (!pm_runtime_suspended(dev))
- pxa2xx_spi_clk_disable(drv_data);
+ pxa2xx_spi_clk_disable(drv_data);
return 0;
}
@@ -1529,6 +1538,7 @@ static int pxa2xx_spi_suspend(struct device *dev)
static int pxa2xx_spi_resume(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
+ struct ssp_device *ssp = drv_data->ssp;
int status;
/* Enable the SSP clock */
@@ -1538,14 +1548,27 @@ static int pxa2xx_spi_resume(struct device *dev)
return status;
}
+ drv_data->suspended = false;
+
/* Start the queue running */
- return spi_controller_resume(drv_data->controller);
+ status = spi_controller_resume(drv_data->controller);
+ if (status) {
+ drv_data->suspended = true;
+ synchronize_irq(ssp->irq);
+ pxa2xx_spi_clk_disable(drv_data);
+ return status;
+ }
+
+ return 0;
}
static int pxa2xx_spi_runtime_suspend(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
+ pxa2xx_spi_off(drv_data);
+ drv_data->suspended = true;
+ synchronize_irq(drv_data->ssp->irq);
pxa2xx_spi_clk_disable(drv_data);
return 0;
}
@@ -1553,8 +1576,14 @@ static int pxa2xx_spi_runtime_suspend(struct device *dev)
static int pxa2xx_spi_runtime_resume(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
+ int status;
+
+ status = pxa2xx_spi_clk_enable(drv_data);
+ if (status)
+ return status;
- return pxa2xx_spi_clk_enable(drv_data);
+ drv_data->suspended = false;
+ return 0;
}
EXPORT_NS_GPL_DEV_PM_OPS(pxa2xx_spi_pm_ops, SPI_PXA2xx) = {
--
2.39.5
^ permalink raw reply related
* [PATCH v15 2/7] spi: pxa2xx: introduce suspended flag for interrupt synchronization
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
When a shared interrupt line is used, the interrupt handler ssp_int()
can be triggered by other devices sharing the line. The handler must
ensure it does not access the SSP controller registers via MMIO when
the device is powered down or when its clock is gated; otherwise, it
will cause PCIe Completion Timeouts and system hangs.
Currently, ssp_int() guards MMIO access using:
if (pm_runtime_suspended(drv_data->ssp->dev)) return IRQ_NONE;
However, during PM transitions (such as system suspend or runtime PM
autosuspend), device callbacks execute to disable the hardware and gate the
clock, but the PM state machine does not mark the device as RPM_SUSPENDED
until after the suspend callback returns. During this transitional state
(RPM_SUSPENDING), pm_runtime_suspended() returns false. If a shared
interrupt fires after the clock has been gated but before the PM state has
transitioned, ssp_int() will execute, attempt MMIO reads on the unclocked
register space, and hang the system.
Formal verification using Spin/PROMELA confirms that a scheduling window
exists where the interrupt thread accesses MMIO when clk_enabled is false,
violating safety properties.
Introduce a custom 'suspended' boolean flag in struct driver_data to
track the device's suspended state across all PM transitions. Check
both 'drv_data->suspended' and '!drv_data->clk_enabled' in ssp_int()
to return IRQ_NONE immediately before any MMIO access is attempted.
This closes the state transition race condition and mathematically guarantees
deadlock-free, safe shared interrupt handling during power transitions.
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx.c | 37 ++++++++++++++++++++++---------------
1 file changed, 22 insertions(+), 15 deletions(-)
diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index d50152aad348..d4a9a48c2624 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -743,12 +743,12 @@ static irqreturn_t ssp_int(int irq, void *dev_id)
u32 status;
/*
- * The IRQ might be shared with other peripherals so we must first
- * check that are we RPM suspended or not. If we are we assume that
- * the IRQ was not for us (we shouldn't be RPM suspended when the
- * interrupt is enabled).
+ * The IRQ might be shared with other peripherals or trigger during
+ * power state transitions. First check if device is suspended or if
+ * clock is disabled; if so, return IRQ_NONE immediately to avoid
+ * unclocked MMIO reads.
*/
- if (pm_runtime_suspended(drv_data->ssp->dev))
+ if (drv_data->suspended || !drv_data->clk_enabled)
return IRQ_NONE;
/*
@@ -1310,6 +1310,7 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
drv_data->controller = controller;
drv_data->controller_info = platform_info;
drv_data->ssp = ssp;
+ drv_data->suspended = true;
/* The spi->mode bits understood by this driver: */
controller->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH | SPI_LOOP;
@@ -1352,11 +1353,6 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
| SSSR_ROR | SSSR_TUR;
}
- status = request_irq(ssp->irq, ssp_int, IRQF_SHARED, dev_name(dev),
- drv_data);
- if (status < 0)
- return dev_err_probe(dev, status, "cannot get IRQ %d\n", ssp->irq);
-
/* Setup DMA if requested */
if (platform_info->enable_dma) {
status = pxa2xx_spi_dma_setup(drv_data);
@@ -1376,7 +1372,16 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
/* Enable SOC clock */
status = pxa2xx_spi_clk_enable(drv_data);
if (status)
- goto out_error_dma_irq_alloc;
+ goto out_error_dma_alloc;
+
+ drv_data->suspended = false;
+
+ status = request_irq(ssp->irq, ssp_int, IRQF_SHARED, dev_name(dev),
+ drv_data);
+ if (status < 0) {
+ status = dev_err_probe(dev, status, "cannot get IRQ %d\n", ssp->irq);
+ goto out_error_clock_enabled;
+ }
controller->max_speed_hz = clk_get_rate(ssp->clk);
/*
@@ -1456,7 +1461,7 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
"ready", GPIOD_OUT_LOW);
if (IS_ERR(drv_data->gpiod_ready)) {
status = PTR_ERR(drv_data->gpiod_ready);
- goto out_error_clock_enabled;
+ goto out_error_irq_alloc;
}
}
@@ -1465,17 +1470,19 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
status = spi_register_controller(controller);
if (status) {
dev_err_probe(dev, status, "problem registering SPI controller\n");
- goto out_error_clock_enabled;
+ goto out_error_irq_alloc;
}
return status;
+out_error_irq_alloc:
+ free_irq(ssp->irq, drv_data);
+
out_error_clock_enabled:
pxa2xx_spi_clk_disable(drv_data);
-out_error_dma_irq_alloc:
+out_error_dma_alloc:
pxa2xx_spi_dma_release(drv_data);
- free_irq(ssp->irq, drv_data);
return status;
}
--
2.39.5
^ permalink raw reply related
* [PATCH v15 1/7] spi: pxa2xx: introduce clock enable and disable helper functions
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
In-Reply-To: <20260720104029.13740-1-fourdollars@debian.org>
The driver disables the clock during PM runtime suspend, PM system
suspend, and device unbinding (remove). It also disables the clock
on various error unwinding paths in pxa2xx_spi_probe().
However, if the clock is already disabled (for example, if the device is
already runtime-suspended during driver unbinding), calling the common
clock framework's clk_disable_unprepare() again leads to clock prepare/enable
count underflows, generating kernel warnings. Furthermore, in the probe error
unwinding paths, failing to request the IRQ does not disable the clock, leaving
a clock enable leak.
Introduce pxa2xx_spi_clk_enable() and pxa2xx_spi_clk_disable() helper
functions that track the clock enable state using a new 'clk_enabled'
boolean flag in struct driver_data.
This ensures clk_disable_unprepare() is called only when the clock is
active, preventing clock underflows during suspend transitions and unbind. It
also allows the probe function to safely unwind resource allocations without
leaking the clock or triggering clock underflows.
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
drivers/spi/spi-pxa2xx.c | 37 +++++++++++++++++++++++++++++--------
drivers/spi/spi-pxa2xx.h | 3 +++
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index 6291d7c2e06f..d50152aad348 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -713,6 +713,28 @@ static void handle_bad_msg(struct driver_data *drv_data)
dev_err(drv_data->ssp->dev, "bad message state in interrupt handler\n");
}
+static int pxa2xx_spi_clk_enable(struct driver_data *drv_data)
+{
+ int ret;
+
+ if (drv_data->clk_enabled)
+ return 0;
+
+ ret = clk_prepare_enable(drv_data->ssp->clk);
+ if (ret == 0)
+ drv_data->clk_enabled = true;
+
+ return ret;
+}
+
+static void pxa2xx_spi_clk_disable(struct driver_data *drv_data)
+{
+ if (drv_data->clk_enabled) {
+ clk_disable_unprepare(drv_data->ssp->clk);
+ drv_data->clk_enabled = false;
+ }
+}
+
static irqreturn_t ssp_int(int irq, void *dev_id)
{
struct driver_data *drv_data = dev_id;
@@ -1352,7 +1374,7 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
}
/* Enable SOC clock */
- status = clk_prepare_enable(ssp->clk);
+ status = pxa2xx_spi_clk_enable(drv_data);
if (status)
goto out_error_dma_irq_alloc;
@@ -1449,7 +1471,7 @@ int pxa2xx_spi_probe(struct device *dev, struct ssp_device *ssp,
return status;
out_error_clock_enabled:
- clk_disable_unprepare(ssp->clk);
+ pxa2xx_spi_clk_disable(drv_data);
out_error_dma_irq_alloc:
pxa2xx_spi_dma_release(drv_data);
@@ -1468,7 +1490,7 @@ void pxa2xx_spi_remove(struct device *dev)
/* Disable the SSP at the peripheral and SOC level */
pxa_ssp_disable(ssp);
- clk_disable_unprepare(ssp->clk);
+ pxa2xx_spi_clk_disable(drv_data);
/* Release DMA */
if (drv_data->controller_info->enable_dma)
@@ -1492,7 +1514,7 @@ static int pxa2xx_spi_suspend(struct device *dev)
pxa_ssp_disable(ssp);
if (!pm_runtime_suspended(dev))
- clk_disable_unprepare(ssp->clk);
+ pxa2xx_spi_clk_disable(drv_data);
return 0;
}
@@ -1500,12 +1522,11 @@ static int pxa2xx_spi_suspend(struct device *dev)
static int pxa2xx_spi_resume(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
- struct ssp_device *ssp = drv_data->ssp;
int status;
/* Enable the SSP clock */
if (!pm_runtime_suspended(dev)) {
- status = clk_prepare_enable(ssp->clk);
+ status = pxa2xx_spi_clk_enable(drv_data);
if (status)
return status;
}
@@ -1518,7 +1539,7 @@ static int pxa2xx_spi_runtime_suspend(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
- clk_disable_unprepare(drv_data->ssp->clk);
+ pxa2xx_spi_clk_disable(drv_data);
return 0;
}
@@ -1526,7 +1547,7 @@ static int pxa2xx_spi_runtime_resume(struct device *dev)
{
struct driver_data *drv_data = dev_get_drvdata(dev);
- return clk_prepare_enable(drv_data->ssp->clk);
+ return pxa2xx_spi_clk_enable(drv_data);
}
EXPORT_NS_GPL_DEV_PM_OPS(pxa2xx_spi_pm_ops, SPI_PXA2xx) = {
diff --git a/drivers/spi/spi-pxa2xx.h b/drivers/spi/spi-pxa2xx.h
index 447be0369384..44f37bf9c519 100644
--- a/drivers/spi/spi-pxa2xx.h
+++ b/drivers/spi/spi-pxa2xx.h
@@ -72,6 +72,9 @@ struct driver_data {
void __iomem *lpss_base;
+ bool suspended;
+ bool clk_enabled;
+
/* Optional slave FIFO ready signal */
struct gpio_desc *gpiod_ready;
};
--
2.39.5
^ permalink raw reply related
* [PATCH v15 0/7] spi: pxa2xx: Fix PM and interrupt issues on Intel LPSS SPI
From: Shih-Yuan Lee @ 2026-07-20 10:40 UTC (permalink / raw)
To: Mark Brown
Cc: Andy Shevchenko, Daniel Mack, Haojian Zhuang, Robert Jarzmik,
linux-arm-kernel, linux-spi, linux-kernel, Shih-Yuan Lee
Hi Mark,
This 7-patch series resolves issues in the spi-pxa2xx host controller driver
related to Intel LPSS SPI controllers and the Apple MacBook8,1.
Changes in v15:
- Split the large PIO mode PM and interrupt patch (v14 Patch 2) into 4 distinct,
single-purpose commits (clock helpers, suspended flag, teardown overhaul,
and autosuspend lockout).
- Reordered the commits so that PIO PM and interrupt bug fixes are applied
before disabling DMA on MacBook8,1, satisfying kernel bisection safety.
- Replaced raw pxa_ssp_disable() calls in PM suspend/remove with
pxa2xx_spi_off() to respect the MMP2 platform's SSE-disable quirk.
- Renamed return variables from 'status' to 'ret' in clock helper functions,
and added a cleanup commit to rename 'status' to 'ret' in the PM and probe
callbacks to conform to standard coding conventions.
Changes in v14:
- Addressed feedback from Sashiko review on the v13 patchset for Patch 2:
- Updated commit message text for Patch 2 to accurately reflect that ssp_int()
uses drv_data->suspended and drv_data->clk_enabled instead of pm_runtime_get_if_active().
- Clarified in commit message that active SPI transfers hold a PM reference via
spi_controller.auto_runtime_pm (pm_runtime_get_sync()), preventing autosuspend
from racing while an IRQ handler is reading/writing FIFO registers.
- Clarified teardown ordering: pxa_ssp_disable() disables hardware interrupt generation
first, followed by setting drv_data->suspended = true and calling synchronize_irq(),
ensuring in-flight IRQ handlers drain completely before clock gating.
Changes in v13:
- Addressed feedback from Sashiko review on the v12 patchset for Patch 2:
- Removed pm_runtime_get_if_active() check from ssp_int(). During PM state
transitions (such as RPM_SUSPENDING or RPM_RESUMING), pm_runtime_get_if_active()
returns 0 because the state is not RPM_ACTIVE. Returning IRQ_NONE during transition
without clearing a level-triggered interrupt would cause the interrupt controller
to endlessly re-invoke the handler in a loop.
- Rely on drv_data->suspended and drv_data->clk_enabled in ssp_int() instead.
If the clock is enabled (drv_data->clk_enabled == true), MMIO reads are 100% safe
and will not cause PCIe Completion Timeouts. If the clock is disabled or the device
is suspended, ssp_int() immediately returns IRQ_NONE to prevent unclocked access.
Changes in v12:
- Addressed feedback from Sashiko review on the v11 patchset for Patch 2:
- Preserved the if (!pm_runtime_suspended(dev)) check prior to enabling/disabling
the clock in pxa2xx_spi_suspend() and pxa2xx_spi_resume(). On non-LPSS platforms,
if a device was runtime suspended prior to system sleep, unconditionally enabling
its hardware clock during system resume forced the clock ON while the PM core
retained RPM_SUSPENDED. Restoring this check prevents PM state desynchronization,
avoiding power leaks on non-LPSS platforms.
Changes in v11:
- Addressed feedback from Sashiko review on the v10 patchset for Patch 2:
- Replaced pm_runtime_put_noidle() with pm_runtime_put_sync() in pxa2xx_spi_remove().
Using pm_runtime_put_noidle() dropped usage count without executing the runtime_suspend
callback or updating the device runtime status, leaving it stuck in RPM_ACTIVE. This
permanently leaked an active child count on the parent LPSS power domain, preventing
the parent from entering low-power runtime suspend. Switching to pm_runtime_put_sync()
ensures the runtime PM state machine properly transitions to RPM_SUSPENDED upon driver
unbind.
Changes in v10:
- Addressed feedback from Sashiko review on the v9 patchset:
- Fixed ssp_int() by explicitly returning IRQ_NONE when pm_runtime_get_if_active()
returns 0 (device inactive or suspending). This prevents reading SSSR and MMIO
registers when the hardware is powered down or in power transition.
- Resolved PM usage counter leak and double-increment for PIO mode devices. Removed
the redundant/unconditional pm_runtime_get_noresume() in pxa2xx_spi_probe() and
moved the single pm_runtime_get_noresume() call to occur strictly after
spi_register_controller() succeeds.
Changes in v9:
- Separated the original "disable DMA and fix runtime PM" patch into two distinct commits:
1. spi: pxa2xx: disable DMA for Apple MacBook8,1 (PCI glue DMI quirk)
2. spi: pxa2xx: fix runtime PM and interrupt handling in PIO mode (core driver PM/IRQ fixes)
- Condensed commit messages for Patches 2 and 3 for clarity and brevity.
- Fixed checkpatch.pl warning regarding unnecessary braces in pxa2xx_spi_remove().
- Addressed feedback from Sashiko review on the v8 patchset:
- Scoped pm_runtime_get_noresume() in pxa2xx_spi_probe() and pm_runtime_put_noidle()
in remove/error paths using is_lpss_ssp(drv_data). This locks out autosuspend
for LPSS controllers operating in PIO mode without causing power regressions on
non-LPSS platforms (e.g. PXA25x, Intel Quark, CE4100) operating in PIO mode.
- Guarded MMIO accesses in pxa2xx_spi_runtime_suspend() with a drv_data->clk_enabled
check. If the clock has already been turned off (e.g. in pxa2xx_spi_remove() or
resume error path), runtime suspend skips hardware writes, avoiding unclocked
MMIO accesses and PCIe Completion Timeouts if runtime PM triggers after teardown.
- Cleaned up pxa2xx_spi_pci_can_dma() signature in spi-pxa2xx-pci.c by removing the
redundant `bool verbose` parameter.
Changes in v8:
- Addressed feedback from Sashiko review on the v7 patchset:
- Fixed runtime PM resume interrupt storm in ssp_int() by checking drv_data->clk_enabled
instead of pm_runtime_get_if_active() == 0.
- Fixed PM disabled configuration (CONFIG_PM=n) support in ssp_int() by avoiding
active <= 0 early returns.
- Reordered suspend and remove sequences to invoke pxa_ssp_disable() before setting
drv_data->suspended = true and synchronizing the IRQ, closing a race window
where level-triggered interrupts could cause a storm.
- Masked hardware interrupt generation in pxa2xx_spi_runtime_suspend() via
pxa_ssp_disable() to prevent unexpected interrupts during clock enable.
- Avoided PCIe Completion Timeout system hangs on newer LPSS platforms (SPT, BXT, CNL)
by introducing pxa2xx_spi_need_lpss_restore() to restrict MMIO save/restore
loops to LPT, BYT, and BSW platforms (which lack an MFD parent).
- Fixed compiler error in pxa2xx_spi_probe() due to unused label.
Changes in v7:
- Addressed feedback from Sashiko review on the v6 patchset:
- Fixed a race condition during probe by moving the request_irq() call after
the clock is enabled and the suspended flag is cleared. This prevents an early
shared interrupt from asserting and triggering an interrupt storm before the clock
is active to allow clearing it.
- Rectified the teardown sequence in pxa2xx_spi_remove(): first set drv_data->suspended
to true and disable SSP hardware-level interrupt generation (pxa_ssp_disable()), then call
synchronize_irq() to wait for in-flight interrupt handlers to complete, free the
IRQ, and only then disable the clocks. This eliminates both post-clock-disable MMIO
accesses and unhandled shared hardware interrupt storms.
- Clarified that the unconditional MMIO register access in pxa2xx_spi_suspend()
is safe because pm_runtime_resume_and_get() is invoked at the very beginning of the
suspend callback, guaranteeing the LPSS device is active and clocked during register
disabling/saving.
- Added spi_controller_resume() recovery to the error path of spi_controller_suspend()
in pxa2xx_spi_suspend() to prevent the controller from remaining permanently disabled
in the event system suspend is aborted.
Changes in v6:
- Addressed feedback from Sashiko review on the v5 patchset:
- Added a synchronize_irq() call to the spi_controller_resume() error path in
pxa2xx_spi_resume(). This ensures any concurrent shared interrupt handlers
(which might execute because drv_data->suspended = false was set earlier)
finish executing before we disable the clock, preventing PCIe timeouts.
- Checked and confirmed that active == 0 is the correct check in ssp_int(). If
Runtime PM is disabled, pm_runtime_get_if_active() returns a negative error
code (like -EINVAL). Changing this check to active <= 0 would cause a
regression on non-PM configurations because the handler would always return
IRQ_NONE. Under disabled Runtime PM, the hardware clock is kept constantly
active, so it is safe to proceed and read registers when active < 0.
Changes in v5:
- Reverted runtime PM configuration in spi-pxa2xx-pci.c to be unconditional.
This prevents the usage count from leaking by +1 on unbind for PIO mode,
while letting pxa2xx_spi_probe()'s pm_runtime_get_noresume() and
pxa2xx_spi_remove()'s pm_runtime_put_noidle() handle the permanent
autosuspend lockout for PIO devices symmetrically.
- Removed the pm_runtime_forbid() call from pxa2xx_spi_remove(). This resolves
reference leaks for non-PCI platform devices where pm_runtime_allow() was
never called during probe.
- Delayed clearing the drv_data->suspended flag on resume until after LPSS reset
deassertion and private register restoration are complete. This prevents a
shared interrupt from firing during resume and attempting to read the SSSR
register while the LPSS block is still held in reset.
- Set drv_data->suspended = true on spi_controller_resume() failure path in
pxa2xx_spi_resume(). This ensures that subsequent shared interrupts do not
attempt register access once the clock is disabled.
Changes in v4:
- Track clock state using drv_data->clk_enabled via pxa2xx_spi_clk_enable() and
pxa2xx_spi_clk_disable() helper functions. This guarantees clock enable/disable
symmetry, preventing clock disable count underflows and framework warnings on S3
resume or runtime autosuspend error paths.
- Introduce drv_data->suspended flag to protect MMIO access in ssp_int() during
system suspend and runtime suspend transition windows.
- Initialize drv_data->suspended = true early in probe(), clearing it only after
the clock is successfully enabled. This completely prevents shared interrupt
handler races during device probe when the clock is still off.
- Call synchronize_irq() after setting drv_data->suspended = true in suspend and
runtime_suspend. This ensures any running shared interrupt handlers finish
executing before the clock is physically turned off.
Changes in v3:
- Avoid PM reference leaks on probe bind/unbind cycle by keeping probe PM
configuration symmetric.
- Prevent userspace (PowerTOP, udev) from overriding runtime PM settings when
DMA is disabled by holding a PM reference via pm_runtime_get_noresume()
in pxa2xx_spi_probe() and dropping it in remove/error paths.
- Check device status in the shared interrupt handler ssp_int() using
pm_runtime_get_if_active() instead of pm_runtime_suspended(). If the device is
suspending (RPM_SUSPENDING) or suspended, ssp_int() immediately returns
IRQ_NONE to avoid reading unclocked MMIO registers during power transition.
- Adjust the driver teardown order in pxa2xx_spi_remove() and probe error paths:
always call free_irq() to unregister the handler before calling
clk_disable_unprepare() to turn off the clock, preventing concurrent
interrupts from reading registers while the clock is disabled.
- Avoid duplicate can-DMA pci_info() logging by checking the pre-computed
enable_dma status in probe and passing a verbose flag to can_dma().
Changes in v2:
- Addressed feedback from Mark Brown on the original v1 series.
- Used drv_data->lpss_base together with relative offsets rather than
hardcoding absolute MMIO offsets that vary between LPSS IP revisions.
- Moved the register save block in suspend to after the controller is quiesced
(after spi_controller_suspend() and pxa_ssp_disable()).
- Store the context array lpss_priv_ctx[6] inside struct driver_data instead of
struct pxa2xx_spi_controller. This keeps the changes entirely local to the
core driver, preventing symbol version mismatches (disagrees about version
of symbol) for other subsystem components (e.g., spi-pxa2xx-platform.ko).
- Restrict the save/restore loop to the first 6 LPSS private registers
(offsets 0x00 to 0x14). Offsets beyond 0x14 (except CS control at 0x18, which is
re-initialised by lpss_ssp_setup()) are reserved/unimplemented on LPT
platforms (such as MacBook8,1), and writing to them triggers a PCIe
Completion Timeout causing a system freeze.
- Added named constants for LPSS_PRIV_RESETS and the de-assert value.
- Wrapped S3 suspend/resume with pm_runtime_resume_and_get() and
pm_runtime_put_autosuspend() respectively.
Shih-Yuan Lee (7):
spi: pxa2xx: introduce clock enable and disable helper functions
spi: pxa2xx: introduce suspended flag for interrupt synchronization
spi: pxa2xx: overhaul teardown and suspend sequence using
pxa2xx_spi_off
spi: pxa2xx: lock out runtime autosuspend for Intel LPSS SPI in PIO
mode
spi: pxa2xx: disable DMA for Apple MacBook8,1
spi: pxa2xx: restore LPSS private register state on S3 resume
spi: pxa2xx: rename local status variable to ret
drivers/spi/spi-pxa2xx-pci.c | 35 +++++-
drivers/spi/spi-pxa2xx.c | 225 +++++++++++++++++++++++++++++------
drivers/spi/spi-pxa2xx.h | 4 +
3 files changed, 224 insertions(+), 40 deletions(-)
--
2.39.5
^ permalink raw reply
* Re: [PATCH 2/3] sched: Convert paravirt_steal to new static key APIs
From: Jürgen Groß @ 2026-07-20 10:35 UTC (permalink / raw)
To: Hongyan Xia, mingo@redhat.com, peterz@infradead.org,
juri.lelli@redhat.com, vincent.guittot@linaro.org,
dietmar.eggemann@arm.com, rostedt@goodmis.org, bsegall@google.com,
mgorman@suse.de, vschneid@redhat.com, kprateek.nayak@amd.com,
Ajay Kaher, Alexey Makhalov, Broadcom internal kernel review list,
Catalin Marinas, Will Deacon, Huacai Chen, WANG Xuerui,
Madhavan Srinivasan, Michael Ellerman, Nicholas Piggin,
Christophe Leroy (CS GROUP), Paul Walmsley, Palmer Dabbelt,
Albert Ou, Alexandre Ghiti, Thomas Gleixner, Borislav Petkov,
Dave Hansen, x86@kernel.org, H. Peter Anvin, Paolo Bonzini,
Vitaly Kuznetsov, Stefano Stabellini, Oleksandr Tyshchenko
Cc: Jiazi Li, linux-kernel@vger.kernel.org,
virtualization@lists.linux.dev,
linux-arm-kernel@lists.infradead.org, loongarch@lists.linux.dev,
linuxppc-dev@lists.ozlabs.org, linux-riscv@lists.infradead.org,
kvm@vger.kernel.org, xen-devel@lists.xenproject.org
In-Reply-To: <773bbd8fcd5ef2ebfe1c64303c7a71a56368de69.1784538478.git.hongyan.xia@transsion.com>
[-- Attachment #1.1.1: Type: text/plain, Size: 375 bytes --]
On 20.07.26 11:38, Hongyan Xia wrote:
> From: Hongyan Xia <hongyan.xia@transsion.com>
>
> paravirt_steal_rq_enabled and paravirt_steal_enabled use raw static_key
> APIs which are now deprecated. Use the new API instead.
>
> No functional change.
>
> Signed-off-by: Hongyan Xia <hongyan.xia@transsion.com>
Acked-by: Juergen Gross <jgross@suse.com>
Juergen
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 3743 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 495 bytes --]
^ permalink raw reply
* Re: [PATCH v6 00/16] firmware: arm_scmi: Fix SCMI core cleanup paths
From: Sudeep Holla @ 2026-07-20 10:33 UTC (permalink / raw)
To: arm-scmi, linux-arm-kernel, Sudeep Holla; +Cc: Cristian Marussi
In-Reply-To: <20260714-scmi_core_fixes-v6-0-3afe499d46e3@kernel.org>
On Tue, 14 Jul 2026 13:56:19 +0100, Sudeep Holla wrote:
> Most of these issues were found by Sashiko[1][2] while reviewing the
> ACPI SCMI PCC work[3]. They are posted separately because the problems
> are independent SCMI core, mailbox and SMC transport cleanup bugs, and do
> not depend on the ACPI PCC series.
>
> The series fixes SCMI core and transport lifetime issues around channel
> setup, probe failure, notification teardown, protocol-device creation,
> driver binding and malformed firmware descriptions.
>
> [...]
Applied to sudeep.holla/linux (for-next/scmi/updates), thanks!
[01/16] firmware: arm_scmi: Publish channel state before callbacks
https://git.kernel.org/sudeep.holla/c/0314900dcdde
[02/16] firmware: arm_scmi: Unregister device notifier before IDR teardown
https://git.kernel.org/sudeep.holla/c/66a0bbf30cc1
[03/16] firmware: arm_scmi: Quiesce notifications before teardown
https://git.kernel.org/sudeep.holla/c/8e49055d0d49
[04/16] firmware: arm_scmi: Clean up channels on setup failure
https://git.kernel.org/sudeep.holla/c/687d67be3d87
[05/16] firmware: arm_scmi: Free transport channel on IDR failure
https://git.kernel.org/sudeep.holla/c/d72e7e5f2468
[06/16] firmware: arm_scmi: Avoid IDR updates while cleaning channels
https://git.kernel.org/sudeep.holla/c/c38b1e19485a
[07/16] firmware: arm_scmi: Reject out of range DT protocol IDs
https://git.kernel.org/sudeep.holla/c/59407ccb5213
[08/16] firmware: arm_scmi: Use channel ID for transport teardown
https://git.kernel.org/sudeep.holla/c/a71a3d4d8a6e
[09/16] firmware: arm_scmi: Protect device request lookup with RCU
https://git.kernel.org/sudeep.holla/c/e6a0e7a49d83
[10/16] firmware: arm_scmi: Drop handle on protocol bind failures
https://git.kernel.org/sudeep.holla/c/e3a5c30d233c
[11/16] firmware: arm_scmi: Clear SystemPower flag on create failure
https://git.kernel.org/sudeep.holla/c/e4c16ae24ca0
[12/16] firmware: arm_scmi: Fix OF node reference handling
https://git.kernel.org/sudeep.holla/c/659705f5eb37
[13/16] firmware: arm_scmi: Unwind TX receiver mailbox setup failure
https://git.kernel.org/sudeep.holla/c/6f7c06744d53
[14/16] firmware: arm_scmi: Unwind P2A receiver mailbox setup failure
https://git.kernel.org/sudeep.holla/c/f3e3773c4e5e
[15/16] firmware: arm_scmi: Fix SCMI device destroy lifetimes
https://git.kernel.org/sudeep.holla/c/6abe8fe36b29
[16/16] firmware: arm_scmi: Fix transport device teardown lookup
https://git.kernel.org/sudeep.holla/c/a14dd8fe0a95
--
Regards,
Sudeep
^ permalink raw reply
* Re: [PATCHv2] firmware: arm_scmi: simplify some allocations
From: Sudeep Holla @ 2026-07-20 10:31 UTC (permalink / raw)
To: arm-scmi, Rosen Penev
Cc: Sudeep Holla, Cristian Marussi, Kees Cook, Gustavo A. R. Silva,
linux-arm-kernel
In-Reply-To: <20260630204719.334211-1-rosenp@gmail.com>
On Tue, 30 Jun 2026 13:47:19 -0700, Rosen Penev wrote:
> Use flexible array members to combine allocations and remove kcalloc
> usage.
>
> Add __counted_by where appropriate for extra runtime analysis. Move
> counting variable assignment after allocation before any potential array
> access.
>
> [...]
Applied to sudeep.holla/linux (for-next/scmi/updates), thanks!
[1/1] firmware: arm_scmi: simplify some allocations
https://git.kernel.org/sudeep.holla/c/9c62bb72a5d0
--
Regards,
Sudeep
^ permalink raw reply
* [PATCH v4 3/3] phy: nuvoton: phy-ma35d1-usb2: extend to dual-port with OTG support
From: Joey Lu @ 2026-07-20 10:22 UTC (permalink / raw)
To: Vinod Koul, Neil Armstrong
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Arnd Bergmann,
Catalin Marinas, Jacky Huang, Shan-Chun Hung, Hui-Ping Chen,
Joey Lu, linux-phy, devicetree, linux-arm-kernel, linux-kernel,
Joey Lu
In-Reply-To: <20260720102222.251179-1-a0987203069@gmail.com>
PHY0 and PHY1 use the same power-on/reset sequence in USBPMISCR, with
PHY1 control bits shifted 16 positions relative to PHY0. A separate
driver for PHY1 would duplicate this logic, so the existing driver is
extended to manage both ports.
The original driver polled only DEVCKSTB after releasing PHY0 from
reset. When USB0 operates in host mode (USB ID pin floating or tied
high) only HSTCKSTB and CK12MSTB assert; DEVCKSTB never sets. Polling
exclusively for DEVCKSTB in host mode causes a 1 ms timeout on every
phy_init() call from the EHCI driver. The init callback is changed to
accept either host-mode or device-mode clock stability, whichever
asserts first.
The power_on and power_off callbacks are replaced by a single init
callback that handles PHY reset and clock-stable polling, because
there is no PHY-specific clock gate on MA35D1; the PHY analog block
derives its reference from the HXT crystal.
A read-only USB role switch is registered for PHY0 to expose the USB0
role to userspace via the standard role-switch sysfs interface. The
.set callback returns -EOPNOTSUPP because the hardware mux is fully
automatic with no software override path. When CONFIG_USB_ROLE_SWITCH
is not enabled, the registration returns -ENODEV and the driver skips
the role switch gracefully without failing probe.
Two new optional device-tree properties are implemented:
- nuvoton,rcalcode: writes per-port 4-bit resistor calibration trim
codes to the RCALCODE field in USBPMISCR.
- nuvoton,oc-active-high: sets the UHOVRCURH bit in MISCFCR0 to treat
the over-current detect input as active-high.
Signed-off-by: Joey Lu <a0987203069@gmail.com>
---
drivers/phy/nuvoton/phy-ma35d1-usb2.c | 284 +++++++++++++++++++-------
1 file changed, 210 insertions(+), 74 deletions(-)
diff --git a/drivers/phy/nuvoton/phy-ma35d1-usb2.c b/drivers/phy/nuvoton/phy-ma35d1-usb2.c
index 9a459b700ed4..2c60be0ee50f 100644
--- a/drivers/phy/nuvoton/phy-ma35d1-usb2.c
+++ b/drivers/phy/nuvoton/phy-ma35d1-usb2.c
@@ -1,11 +1,16 @@
// SPDX-License-Identifier: GPL-2.0
/*
- * Copyright (C) 2024 Nuvoton Technology Corp.
+ * Nuvoton MA35D1 USB 2.0 PHY driver
+ *
+ * Supports PHY0 (USB0 OTG port, shared between DWC2 gadget and EHCI0/OHCI0)
+ * and PHY1 (USB1 host-only port, used by EHCI1/OHCI1). The hardware mux on
+ * PHY0 switches automatically via the USB ID pin.
+ *
+ * Copyright (C) 2026 Nuvoton Technology Corp.
*/
#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/delay.h>
-#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
@@ -13,131 +18,262 @@
#include <linux/phy/phy.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
+#include <linux/usb/role.h>
-/* USB PHY Miscellaneous Control Register */
-#define MA35_SYS_REG_USBPMISCR 0x60
-#define PHY0POR BIT(0) /* PHY Power-On Reset Control Bit */
-#define PHY0SUSPEND BIT(1) /* PHY Suspend; 0: suspend, 1: operaion */
-#define PHY0COMN BIT(2) /* PHY Common Block Power-Down Control */
-#define PHY0DEVCKSTB BIT(10) /* PHY 60 MHz UTMI clock stable bit */
+#define MA35_SYS_PWRONOTP 0x04
+#define PWRONOTP_USBP0ID BIT(16)
+
+#define MA35_SYS_USBPMISCR 0x60
+#define USBPMISCR_PHY_POR(n) BIT(0 + (n) * 16)
+#define USBPMISCR_PHY_SUSPEND(n) BIT(1 + (n) * 16)
+#define USBPMISCR_PHY_COMN(n) BIT(2 + (n) * 16)
+#define USBPMISCR_PHY_HSTCKSTB(n) BIT(8 + (n) * 16)
+#define USBPMISCR_PHY_CK12MSTB(n) BIT(9 + (n) * 16)
+#define USBPMISCR_PHY_DEVCKSTB(n) BIT(10 + (n) * 16)
+/* Mask for control bits (POR, SUSPEND, COMN) */
+#define USBPMISCR_PHY_CTL_MASK(n) (0x7u << ((n) * 16))
+/* Host-mode ready */
+#define USBPMISCR_PHY_HOST_READY(n) (USBPMISCR_PHY_SUSPEND(n) | \
+ USBPMISCR_PHY_HSTCKSTB(n) | \
+ USBPMISCR_PHY_CK12MSTB(n))
+/* Device-mode ready */
+#define USBPMISCR_PHY_DEV_READY(n) (USBPMISCR_PHY_SUSPEND(n) | \
+ USBPMISCR_PHY_DEVCKSTB(n))
+/* RCALCODE: 4-bit resistor trim */
+#define USBPMISCR_RCAL_SHIFT(n) (12 + (n) * 16)
+#define USBPMISCR_RCAL_MASK(n) GENMASK(USBPMISCR_RCAL_SHIFT(n) + 3, \
+ USBPMISCR_RCAL_SHIFT(n))
+
+#define MA35_SYS_MISCFCR0 0x70
+/* USB host over-current detect polarity (shared, both ports) */
+#define MISCFCR0_UHOVRCURH BIT(12)
+
+#define MA35_PHY_NUM 2
+
+struct ma35_phy_port {
+ struct phy *phy;
+ unsigned int idx;
+};
struct ma35_usb_phy {
- struct clk *clk;
struct device *dev;
struct regmap *sysreg;
+ struct ma35_phy_port port[MA35_PHY_NUM];
+ struct usb_role_switch *role_sw;
};
-static int ma35_usb_phy_power_on(struct phy *phy)
+static int ma35_usb_phy_init(struct phy *phy)
{
- struct ma35_usb_phy *p_phy = phy_get_drvdata(phy);
+ struct ma35_phy_port *port = phy_get_drvdata(phy);
+ struct ma35_usb_phy *p = container_of(port - port->idx,
+ struct ma35_usb_phy, port[0]);
+ unsigned int n = port->idx;
unsigned int val;
int ret;
- ret = clk_prepare_enable(p_phy->clk);
- if (ret < 0) {
- dev_err(p_phy->dev, "Failed to enable PHY clock: %d\n", ret);
- return ret;
- }
+ regmap_read(p->sysreg, MA35_SYS_USBPMISCR, &val);
- regmap_read(p_phy->sysreg, MA35_SYS_REG_USBPMISCR, &val);
- if (val & PHY0SUSPEND) {
- /*
- * USB PHY0 is in operation mode already
- * make sure USB PHY 60 MHz UTMI Interface Clock ready
- */
- ret = regmap_read_poll_timeout(p_phy->sysreg, MA35_SYS_REG_USBPMISCR, val,
- val & PHY0DEVCKSTB, 10, 1000);
- if (ret == 0)
- return 0;
- }
+ if (val & USBPMISCR_PHY_SUSPEND(n))
+ return 0;
- /*
- * reset USB PHY0.
- * wait until USB PHY0 60 MHz UTMI Interface Clock ready
- */
- regmap_update_bits(p_phy->sysreg, MA35_SYS_REG_USBPMISCR, 0x7, (PHY0POR | PHY0SUSPEND));
+ regmap_update_bits(p->sysreg, MA35_SYS_USBPMISCR,
+ USBPMISCR_PHY_CTL_MASK(n),
+ USBPMISCR_PHY_POR(n) | USBPMISCR_PHY_SUSPEND(n));
udelay(20);
- /* make USB PHY0 enter operation mode */
- regmap_update_bits(p_phy->sysreg, MA35_SYS_REG_USBPMISCR, 0x7, PHY0SUSPEND);
+ regmap_update_bits(p->sysreg, MA35_SYS_USBPMISCR,
+ USBPMISCR_PHY_CTL_MASK(n),
+ USBPMISCR_PHY_SUSPEND(n));
- /* make sure USB PHY 60 MHz UTMI Interface Clock ready */
- ret = regmap_read_poll_timeout(p_phy->sysreg, MA35_SYS_REG_USBPMISCR, val,
- val & PHY0DEVCKSTB, 10, 1000);
- if (ret == -ETIMEDOUT) {
- dev_err(p_phy->dev, "Check PHY clock, Timeout: %d\n", ret);
- clk_disable_unprepare(p_phy->clk);
+ if (n == 0) {
+ ret = regmap_read_poll_timeout(p->sysreg, MA35_SYS_USBPMISCR,
+ val,
+ ((val & USBPMISCR_PHY_HOST_READY(0)) ==
+ USBPMISCR_PHY_HOST_READY(0)) ||
+ ((val & USBPMISCR_PHY_DEV_READY(0)) ==
+ USBPMISCR_PHY_DEV_READY(0)),
+ 10, 1000);
+ } else {
+ ret = regmap_read_poll_timeout(p->sysreg, MA35_SYS_USBPMISCR,
+ val,
+ (val & USBPMISCR_PHY_HOST_READY(n)) ==
+ USBPMISCR_PHY_HOST_READY(n),
+ 10, 1000);
+ }
+
+ if (ret) {
+ dev_err(p->dev, "USB PHY%u clock not stable (USBPMISCR=0x%08x)\n",
+ n, val);
return ret;
}
return 0;
}
-static int ma35_usb_phy_power_off(struct phy *phy)
+static const struct phy_ops ma35_usb_phy_ops = {
+ .init = ma35_usb_phy_init,
+ .owner = THIS_MODULE,
+};
+
+static int ma35_role_sw_set(struct usb_role_switch *sw, enum usb_role role)
+{
+ return -EOPNOTSUPP;
+}
+
+static enum usb_role ma35_role_sw_get(struct usb_role_switch *sw)
+{
+ struct ma35_usb_phy *p = usb_role_switch_get_drvdata(sw);
+ u32 val;
+
+ regmap_read(p->sysreg, MA35_SYS_PWRONOTP, &val);
+
+ return (val & PWRONOTP_USBP0ID) ? USB_ROLE_HOST : USB_ROLE_DEVICE;
+}
+
+static int ma35_role_switch_init(struct platform_device *pdev,
+ struct ma35_usb_phy *p)
{
- struct ma35_usb_phy *p_phy = phy_get_drvdata(phy);
+ struct usb_role_switch_desc sw_desc = {0};
+
+ sw_desc.set = ma35_role_sw_set;
+ sw_desc.get = ma35_role_sw_get;
+ sw_desc.allow_userspace_control = true;
+ sw_desc.driver_data = p;
+ sw_desc.fwnode = dev_fwnode(&pdev->dev);
+
+ p->role_sw = usb_role_switch_register(&pdev->dev, &sw_desc);
+ if (IS_ERR(p->role_sw)) {
+ if (PTR_ERR(p->role_sw) == -ENODEV) {
+ p->role_sw = NULL;
+ return 0;
+ }
+ return dev_err_probe(&pdev->dev, PTR_ERR(p->role_sw),
+ "failed to register role switch\n");
+ }
- clk_disable_unprepare(p_phy->clk);
return 0;
}
-static const struct phy_ops ma35_usb_phy_ops = {
- .power_on = ma35_usb_phy_power_on,
- .power_off = ma35_usb_phy_power_off,
- .owner = THIS_MODULE,
-};
+static void ma35_role_switch_exit(struct ma35_usb_phy *p)
+{
+ if (p->role_sw) {
+ usb_role_switch_unregister(p->role_sw);
+ p->role_sw = NULL;
+ }
+}
+
+static struct phy *ma35_usb_phy_xlate(struct device *dev,
+ const struct of_phandle_args *args)
+{
+ struct ma35_usb_phy *p = dev_get_drvdata(dev);
+ unsigned int idx;
+
+ if (args->args_count == 0)
+ idx = 0;
+ else
+ idx = args->args[0];
+
+ if (idx >= MA35_PHY_NUM)
+ return ERR_PTR(-EINVAL);
+
+ return p->port[idx].phy;
+}
static int ma35_usb_phy_probe(struct platform_device *pdev)
{
struct phy_provider *provider;
- struct ma35_usb_phy *p_phy;
- struct phy *phy;
+ struct ma35_usb_phy *p;
+ struct clk *clk;
+ int n, ret;
+ u32 code;
- p_phy = devm_kzalloc(&pdev->dev, sizeof(*p_phy), GFP_KERNEL);
- if (!p_phy)
+ p = devm_kzalloc(&pdev->dev, sizeof(*p), GFP_KERNEL);
+ if (!p)
return -ENOMEM;
- p_phy->dev = &pdev->dev;
- platform_set_drvdata(pdev, p_phy);
+ p->dev = &pdev->dev;
+ platform_set_drvdata(pdev, p);
+
+ p->sysreg = syscon_regmap_lookup_by_phandle(pdev->dev.of_node,
+ "nuvoton,sys");
+ if (IS_ERR(p->sysreg))
+ return dev_err_probe(&pdev->dev, PTR_ERR(p->sysreg),
+ "failed to get SYS regmap\n");
- p_phy->sysreg = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "nuvoton,sys");
- if (IS_ERR(p_phy->sysreg))
- return dev_err_probe(&pdev->dev, PTR_ERR(p_phy->sysreg),
- "Failed to get SYS registers\n");
+ clk = devm_clk_get_enabled(&pdev->dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(&pdev->dev, PTR_ERR(clk),
+ "failed to get clock\n");
- p_phy->clk = of_clk_get(pdev->dev.of_node, 0);
- if (IS_ERR(p_phy->clk))
- return dev_err_probe(&pdev->dev, PTR_ERR(p_phy->clk),
- "failed to find usb_phy clock\n");
+ for (n = 0; n < MA35_PHY_NUM; n++) {
+ if (of_property_read_u32_index(pdev->dev.of_node,
+ "nuvoton,rcalcode", n, &code))
+ continue;
- phy = devm_phy_create(&pdev->dev, NULL, &ma35_usb_phy_ops);
- if (IS_ERR(phy))
- return dev_err_probe(&pdev->dev, PTR_ERR(phy), "Failed to create PHY\n");
+ if (code > 15)
+ return dev_err_probe(&pdev->dev, -EINVAL,
+ "rcalcode[%d] %u out of range (0-15)\n",
+ n, code);
- phy_set_drvdata(phy, p_phy);
+ regmap_update_bits(p->sysreg, MA35_SYS_USBPMISCR,
+ USBPMISCR_RCAL_MASK(n),
+ code << USBPMISCR_RCAL_SHIFT(n));
+ }
+
+ if (of_property_read_bool(pdev->dev.of_node, "nuvoton,oc-active-high"))
+ regmap_update_bits(p->sysreg, MA35_SYS_MISCFCR0,
+ MISCFCR0_UHOVRCURH, MISCFCR0_UHOVRCURH);
+
+ for (n = 0; n < MA35_PHY_NUM; n++) {
+ p->port[n].idx = n;
+
+ p->port[n].phy = devm_phy_create(&pdev->dev, pdev->dev.of_node,
+ &ma35_usb_phy_ops);
+ if (IS_ERR(p->port[n].phy))
+ return dev_err_probe(&pdev->dev, PTR_ERR(p->port[n].phy),
+ "failed to create PHY%d\n", n);
+
+ phy_set_drvdata(p->port[n].phy, &p->port[n]);
+ }
+
+ ret = ma35_role_switch_init(pdev, p);
+ if (ret)
+ return ret;
- provider = devm_of_phy_provider_register(&pdev->dev, of_phy_simple_xlate);
- if (IS_ERR(provider))
+ provider = devm_of_phy_provider_register(&pdev->dev, ma35_usb_phy_xlate);
+ if (IS_ERR(provider)) {
+ ma35_role_switch_exit(p);
return dev_err_probe(&pdev->dev, PTR_ERR(provider),
- "Failed to register PHY provider\n");
+ "failed to register PHY provider\n");
+ }
+
return 0;
}
+static void ma35_usb_phy_remove(struct platform_device *pdev)
+{
+ struct ma35_usb_phy *p = platform_get_drvdata(pdev);
+
+ ma35_role_switch_exit(p);
+}
+
static const struct of_device_id ma35_usb_phy_of_match[] = {
- { .compatible = "nuvoton,ma35d1-usb2-phy", },
- { },
+ { .compatible = "nuvoton,ma35d1-usb2-phy" },
+ { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, ma35_usb_phy_of_match);
static struct platform_driver ma35_usb_phy_driver = {
.probe = ma35_usb_phy_probe,
- .driver = {
- .name = "ma35d1-usb2-phy",
- .of_match_table = ma35_usb_phy_of_match,
+ .remove = ma35_usb_phy_remove,
+ .driver = {
+ .name = "ma35d1-usb2-phy",
+ .of_match_table = ma35_usb_phy_of_match,
},
};
module_platform_driver(ma35_usb_phy_driver);
MODULE_DESCRIPTION("Nuvoton ma35d1 USB2.0 PHY driver");
MODULE_AUTHOR("Hui-Ping Chen <hpchen0nvt@gmail.com>");
+MODULE_AUTHOR("Joey Lu <a0987203069@gmail.com>");
MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
* [PATCH v4 2/3] arm64: dts: nuvoton: ma35d1: add USB controllers and dual-port PHY node
From: Joey Lu @ 2026-07-20 10:22 UTC (permalink / raw)
To: Vinod Koul, Neil Armstrong
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Arnd Bergmann,
Catalin Marinas, Jacky Huang, Shan-Chun Hung, Hui-Ping Chen,
Joey Lu, linux-phy, devicetree, linux-arm-kernel, linux-kernel,
Joey Lu
In-Reply-To: <20260720102222.251179-1-a0987203069@gmail.com>
Add the USB subsystem nodes for the MA35D1 SoC and boards:
- usb_phy: standalone PHY node with #phy-cells = <1>, covering both
the OTG port (PHY0, index 0) and the host-only port (PHY1, index 1).
Uses nuvoton,sys phandle and HUSBH0_GATE clock per the binding.
- usb (DWC2 gadget, USB0): phys = <&usb_phy 0>, dr_mode = peripheral.
- ehci0/ohci0 (USB0 host): phys = <&usb_phy 0>, companion pairing.
- ehci1/ohci1 (USB1 host): phys = <&usb_phy 1>.
Board files (ma35d1-som-256m.dts, ma35d1-iot-512m.dts) add pinctrl for
VBUSVLD, PWREN and OVC signals and enable all USB nodes.
Signed-off-by: Joey Lu <a0987203069@gmail.com>
---
.../boot/dts/nuvoton/ma35d1-iot-512m.dts | 36 ++++++++++
.../boot/dts/nuvoton/ma35d1-som-256m.dts | 36 ++++++++++
arch/arm64/boot/dts/nuvoton/ma35d1.dtsi | 65 +++++++++++++++++++
3 files changed, 137 insertions(+)
diff --git a/arch/arm64/boot/dts/nuvoton/ma35d1-iot-512m.dts b/arch/arm64/boot/dts/nuvoton/ma35d1-iot-512m.dts
index 78534d613486..0a068abda6e0 100644
--- a/arch/arm64/boot/dts/nuvoton/ma35d1-iot-512m.dts
+++ b/arch/arm64/boot/dts/nuvoton/ma35d1-iot-512m.dts
@@ -97,6 +97,16 @@ pinctrl_uart14: uart14-pins {
power-source = <1>;
};
};
+
+ hsusb {
+ pinctrl_hsusb: hsusb-pins {
+ nuvoton,pins = <5 15 1>, /* VBUSVLD */
+ <11 12 9>, /* PWREN */
+ <11 13 9>; /* OVC */
+ bias-disable;
+ power-source = <1>;
+ };
+ };
};
&uart0 {
@@ -151,3 +161,29 @@ eth_phy1: ethernet-phy@1 {
reg = <1>;
};
};
+
+&usb_phy {
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_hsusb>;
+ status = "okay";
+};
+
+&usb {
+ status = "okay";
+};
+
+&ehci0 {
+ status = "okay";
+};
+
+&ehci1 {
+ status = "okay";
+};
+
+&ohci0 {
+ status = "okay";
+};
+
+&ohci1 {
+ status = "okay";
+};
diff --git a/arch/arm64/boot/dts/nuvoton/ma35d1-som-256m.dts b/arch/arm64/boot/dts/nuvoton/ma35d1-som-256m.dts
index a029b660e8dc..7f1e6d2a1fc2 100644
--- a/arch/arm64/boot/dts/nuvoton/ma35d1-som-256m.dts
+++ b/arch/arm64/boot/dts/nuvoton/ma35d1-som-256m.dts
@@ -100,6 +100,16 @@ pinctrl_uart16: uart16-pins {
power-source = <1>;
};
};
+
+ hsusb {
+ pinctrl_hsusb: hsusb-pins {
+ nuvoton,pins = <5 15 1>, /* VBUSVLD */
+ <11 12 9>, /* PWREN */
+ <11 13 9>; /* OVC */
+ bias-disable;
+ power-source = <1>;
+ };
+ };
};
&uart0 {
@@ -153,3 +163,29 @@ eth_phy1: ethernet-phy@1 {
reg = <1>;
};
};
+
+&usb_phy {
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_hsusb>;
+ status = "okay";
+};
+
+&usb {
+ status = "okay";
+};
+
+&ehci0 {
+ status = "okay";
+};
+
+&ehci1 {
+ status = "okay";
+};
+
+&ohci0 {
+ status = "okay";
+};
+
+&ohci1 {
+ status = "okay";
+};
diff --git a/arch/arm64/boot/dts/nuvoton/ma35d1.dtsi b/arch/arm64/boot/dts/nuvoton/ma35d1.dtsi
index 7228ad4735b5..5a7a39b1b514 100644
--- a/arch/arm64/boot/dts/nuvoton/ma35d1.dtsi
+++ b/arch/arm64/boot/dts/nuvoton/ma35d1.dtsi
@@ -88,6 +88,14 @@ sys: system-management@40460000 {
#reset-cells = <1>;
};
+ usb_phy: usb-phy {
+ compatible = "nuvoton,ma35d1-usb2-phy";
+ clocks = <&clk HUSBH0_GATE>;
+ nuvoton,sys = <&sys>;
+ #phy-cells = <1>;
+ status = "disabled";
+ };
+
clk: clock-controller@40460200 {
compatible = "nuvoton,ma35d1-clk";
reg = <0x00000000 0x40460200 0x0 0x100>;
@@ -431,5 +439,62 @@ mdio1: mdio {
#size-cells = <0>;
};
};
+
+ usb: usb@40200000 {
+ compatible = "snps,dwc2";
+ reg = <0x0 0x40200000 0x0 0x1000>;
+ interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk USBD_GATE>;
+ clock-names = "otg";
+ phys = <&usb_phy 0>;
+ phy-names = "usb2-phy";
+ dr_mode = "peripheral";
+ g-np-tx-fifo-size = <16>;
+ g-rx-fifo-size = <0x100>;
+ g-tx-fifo-size = <256 256 64 64 64 32 32 32>;
+ status = "disabled";
+ };
+
+ ehci0: usb@40140000 {
+ compatible = "generic-ehci";
+ reg = <0x0 0x40140000 0x0 0x1000>;
+ interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk HUSBH0_GATE>;
+ phys = <&usb_phy 0>;
+ phy-names = "usb";
+ companion = <&ohci0>;
+ status = "disabled";
+ };
+
+ ehci1: usb@401c0000 {
+ compatible = "generic-ehci";
+ reg = <0x0 0x401c0000 0x0 0x1000>;
+ interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk HUSBH1_GATE>;
+ phys = <&usb_phy 1>;
+ phy-names = "usb";
+ companion = <&ohci1>;
+ status = "disabled";
+ };
+
+ ohci0: usb@40150000 {
+ compatible = "generic-ohci";
+ reg = <0x0 0x40150000 0x0 0x1000>;
+ interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk HUSBH0_GATE>;
+ phys = <&usb_phy 0>;
+ phy-names = "usb";
+ status = "disabled";
+ };
+
+ ohci1: usb@401d0000 {
+ compatible = "generic-ohci";
+ reg = <0x0 0x401d0000 0x0 0x1000>;
+ interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk HUSBH1_GATE>;
+ phys = <&usb_phy 1>;
+ phy-names = "usb";
+ status = "disabled";
+ };
};
};
--
2.43.0
^ permalink raw reply related
* [PATCH v4 1/3] dt-bindings: phy: nuvoton,ma35d1-usb2-phy: extend for dual-port and OTG
From: Joey Lu @ 2026-07-20 10:22 UTC (permalink / raw)
To: Vinod Koul, Neil Armstrong
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Arnd Bergmann,
Catalin Marinas, Jacky Huang, Shan-Chun Hung, Hui-Ping Chen,
Joey Lu, linux-phy, devicetree, linux-arm-kernel, linux-kernel,
Joey Lu, Krzysztof Kozlowski
In-Reply-To: <20260720102222.251179-1-a0987203069@gmail.com>
The MA35D1 exposes two USB PHY ports (PHY0 and PHY1) managed by the
same hardware block (USBPMISCR register in the system-management syscon).
PHY0 is shared between the DWC2 gadget controller and EHCI0/OHCI0
through an automatic hardware mux that follows the USB ID pin; PHY1 is
dedicated to EHCI1/OHCI1. Because both ports share the same register
topology a single binding and driver should cover both.
Changing '#phy-cells' from const: 0 to enum: [0, 1] lets consumers name
the port they need while preserving backward compatibility: boards already
using '#phy-cells = <0>' continue to validate and function unchanged.
The two new optional properties:
- nuvoton,rcalcode: the resistor calibration trim code is determined at
board design time to match the PCB trace impedance.
- nuvoton,oc-active-high: the over-current detect polarity depends on
the choice of VBUS power switch.
Signed-off-by: Joey Lu <a0987203069@gmail.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
---
.../bindings/phy/nuvoton,ma35d1-usb2-phy.yaml | 44 ++++++++++++++++++-
1 file changed, 42 insertions(+), 2 deletions(-)
diff --git a/Documentation/devicetree/bindings/phy/nuvoton,ma35d1-usb2-phy.yaml b/Documentation/devicetree/bindings/phy/nuvoton,ma35d1-usb2-phy.yaml
index fff858c909a0..1bd22dd0e5fc 100644
--- a/Documentation/devicetree/bindings/phy/nuvoton,ma35d1-usb2-phy.yaml
+++ b/Documentation/devicetree/bindings/phy/nuvoton,ma35d1-usb2-phy.yaml
@@ -8,6 +8,18 @@ title: Nuvoton MA35D1 USB2 phy
maintainers:
- Hui-Ping Chen <hpchen0nvt@gmail.com>
+ - Joey Lu <yclu4@nuvoton.com>
+
+description:
+ USB 2.0 PHY for the Nuvoton MA35D1 SoC. The PHY node is a standalone
+ platform device that accesses the USB PHY control registers inside the
+ system-management syscon block via the nuvoton,sys phandle.
+
+ PHY0 is the OTG port whose signals are routed to either the DWC2 gadget
+ controller or the EHCI0/OHCI0 host controller by a hardware mux that
+ follows the USB ID pin automatically.
+
+ PHY1 is a dedicated host-only port used by EHCI1/OHCI1.
properties:
compatible:
@@ -15,7 +27,12 @@ properties:
- nuvoton,ma35d1-usb2-phy
"#phy-cells":
- const: 0
+ enum: [0, 1]
+ description:
+ When 0, the node exposes PHY0 only and consumers reference it
+ with no cell argument. When 1, the single cell selects the port,
+ with 0 for the OTG port (USB0, shared with DWC2 gadget controller)
+ and 1 for the host-only port (USB1).
clocks:
maxItems: 1
@@ -23,7 +40,29 @@ properties:
nuvoton,sys:
$ref: /schemas/types.yaml#/definitions/phandle
description:
- phandle to syscon for checking the PHY clock status.
+ Phandle to the system-management syscon node providing access to the
+ USB PHY control registers.
+
+ nuvoton,rcalcode:
+ $ref: /schemas/types.yaml#/definitions/uint32-array
+ minItems: 2
+ maxItems: 2
+ items:
+ minimum: 0
+ maximum: 15
+ description:
+ Resistor calibration trim codes for PHY0 and PHY1 respectively.
+ Each 4-bit value is written to the RCALCODE field in USBPMISCR and
+ adjusts the PHY's internal termination resistance. Both entries must
+ be supplied when this property is present; when absent the hardware
+ reset default is used for each port.
+
+ nuvoton,oc-active-high:
+ type: boolean
+ description:
+ When present, the over-current detect input from the VBUS power switch
+ is treated as active-high. The default (property absent) is active-low.
+ This setting is shared by both USB host ports.
required:
- compatible
@@ -42,4 +81,5 @@ examples:
clocks = <&clk USBD_GATE>;
nuvoton,sys = <&sys>;
#phy-cells = <0>;
+ nuvoton,rcalcode = <7 7>;
};
--
2.43.0
^ permalink raw reply related
* [PATCH v4 0/3] phy: nuvoton: extend MA35D1 USB2 PHY driver for dual-port OTG support
From: Joey Lu @ 2026-07-20 10:22 UTC (permalink / raw)
To: Vinod Koul, Neil Armstrong
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Arnd Bergmann,
Catalin Marinas, Jacky Huang, Shan-Chun Hung, Hui-Ping Chen,
Joey Lu, linux-phy, devicetree, linux-arm-kernel, linux-kernel,
Joey Lu
The MA35D1 SoC has two USB PHY ports managed by a shared hardware block:
- PHY0 (USB0): OTG port shared between the DWC2 gadget controller and
the EHCI0/OHCI0 host controllers. A hardware mux automatically routes
USB0 signals to the correct controller based on the USB ID pin state.
- PHY1 (USB1): dedicated host-only port for EHCI1/OHCI1.
This series extends the existing phy-ma35d1-usb2.c driver and its binding
to cover both ports and add OTG role-switch support, while keeping full
backward compatibility with existing device trees.
Changes since v3:
- dt-bindings: add nuvoton,rcalcode = <7 7> example to illustrate the
optional resistor calibration property; revert redundant clock example
change (back to USBD_GATE as in the original binding).
- driver: handle -ENODEV from usb_role_switch_register() gracefully so
that probe succeeds when CONFIG_USB_ROLE_SWITCH is not enabled.
Joey Lu (3):
dt-bindings: phy: nuvoton,ma35d1-usb2-phy: extend for dual-port and
OTG
arm64: dts: nuvoton: ma35d1: add USB controllers and dual-port PHY
node
phy: nuvoton: phy-ma35d1-usb2: extend to dual-port with OTG support
.../bindings/phy/nuvoton,ma35d1-usb2-phy.yaml | 44 ++-
.../boot/dts/nuvoton/ma35d1-iot-512m.dts | 36 +++
.../boot/dts/nuvoton/ma35d1-som-256m.dts | 36 +++
arch/arm64/boot/dts/nuvoton/ma35d1.dtsi | 65 ++++
drivers/phy/nuvoton/phy-ma35d1-usb2.c | 284 +++++++++++++-----
5 files changed, 389 insertions(+), 76 deletions(-)
base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
--
2.43.0
^ permalink raw reply
* Re: [PATCH v6 08/14] dt-bindings: media: mediatek: vcodec: add decoder dt-bindings for mt8196
From: Krzysztof Kozlowski @ 2026-07-20 10:19 UTC (permalink / raw)
To: Kyrie Wu (吴晗)
Cc: matthias.bgg@gmail.com, fshao@chromium.org,
Yunfei Dong (董云飞), conor+dt@kernel.org,
fanwu01@zju.edu.cn, jacopo.mondi@ideasonboard.com,
haoxiang_li2024@163.com, tfiga@chromium.org,
AngeloGioacchino Del Regno,
Andrew-CT Chen (陳智迪),
linux-media@vger.kernel.org, ribalda@chromium.org,
wenst@chromium.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, kees@kernel.org,
rongqianfeng@vivo.com, Irui Wang (王瑞),
krzk+dt@kernel.org, linux-mediatek@lists.infradead.org,
Tiffany Lin (林慧珊),
sakari.ailus@linux.intel.com, benjamin.gaignard@collabora.com,
p.zabel@pengutronix.de, mchehab@kernel.org,
sebastian.fricke@collabora.com, robh@kernel.org,
linux-arm-kernel@lists.infradead.org,
nicolas.dufresne@collabora.com, hverkuil+cisco@kernel.org,
laurent.pinchart+renesas@ideasonboard.com
In-Reply-To: <a9123dce5ae7ef184f5640b12e3e9427084ec542.camel@mediatek.com>
On 20/07/2026 08:26, Kyrie Wu (吴晗) wrote:
> On Mon, 2026-07-20 at 07:49 +0200, Krzysztof Kozlowski wrote:
>> On 20/07/2026 07:46, Krzysztof Kozlowski wrote:
>>> On Mon, Jul 20, 2026 at 09:20:50AM +0800, Kyrie Wu wrote:
>>>> Add the MT8196 compatible string to the MediaTek vcodec subdev
>>>> decoder binding.
>>>>
>>>> Compared to previous ICs, the MT8196 supports a 10-bit decoder
>>>> and has a decoding capability of 4K@120fps. It also supports
>>>> 36-bit DRAM IOVA address and Video Power Control to optimize
>>>> bandwidth and voltage usage.
>>>
>>> Please wrap commit message according to Linux coding style /
>>> submission
>>> process (neither too early nor over the limit):
>>> https://urldefense.com/v3/__https://elixir.bootlin.com/linux/v6.4-rc1/source/Documentation/process/submitting-patches.rst*L597__;Iw!!CTRNKA9wMg0ARbw!gDjbq0EZNkhLTzuJM_w5PopnDFej1mG4qnkRFgyoiU7Dvhm7tj3MeG5eZYNGA0L0G_61j-cmdIxORA$
>>>
>>>
>>>>
>>>> Signed-off-by: Kyrie Wu <kyrie.wu@mediatek.com>
>>>> Acked-by: Nicolas Dufresne <nicolas.dufresne@collabora.com>
>>>> ---
>>>> .../bindings/media/mediatek,vcodec-subdev-decoder.yaml
>>>> | 1 +
>>>> 1 file changed, 1 insertion(+)
>>>
>>> Acked-by: Krzysztof Kozlowski
>>> <krzysztof.kozlowski@oss.qualcomm.com>
>>
>> Actually no. I checked previous comments and you received feedback
>> which
>> you basically ignored, so explain why this model has flexible number
>> of
>> clocks
>>
>> Best regards,
>> Krzysztof
>
> Dear Krzysztof,
>
> As mentioned in the previous version of the communication, the
> information regarding clk was incorrect, and I have corrected it in
> this version.
>
> Thank you for your attention and reminder.
Read my message carefully instead of responding whatever necessary to
make me go away.
NAK
Where did you explain that device has flexible number of clocks?
Best regards,
Krzysztof
^ permalink raw reply
* Re: [PATCH v13 3/3] media: nxp: Add i.MX95 CSI pixel formatter v4l2 driver
From: Loic Poulain @ 2026-07-20 10:15 UTC (permalink / raw)
To: guoniu.zhou
Cc: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Shawn Guo, Sascha Hauer, Pengutronix Kernel Team,
Fabio Estevam, Laurent Pinchart, Frank Li, Bryan O'Donoghue,
Abel Vesa, Peng Fan, Michael Turquette, Stephen Boyd, imx,
linux-media, devicetree, linux-arm-kernel, linux-kernel,
linux-clk, Guoniu Zhou
In-Reply-To: <20260720-csi_formatter-v13-3-4dc9a80e4cfd@oss.nxp.com>
On Mon, Jul 20, 2026 at 11:56 AM <guoniu.zhou@oss.nxp.com> wrote:
>
> From: Guoniu Zhou <guoniu.zhou@nxp.com>
>
> The CSI pixel formatter is a module found on i.MX95 used to reformat
> packet info, pixel and non-pixel data from CSI-2 host controller to
> match Pixel Link(PL) definition.
>
> Add data formatting support.
>
> Signed-off-by: Guoniu Zhou <guoniu.zhou@nxp.com>
> Reviewed-by: Frank Li <Frank.Li@nxp.com>
Looks good to me but if you're planning another version, I'd recommend
using devm_pm_clk_create(dev) together with of_pm_clk_add_clks(dev)
and relying on the generic PM clock infrastructure instead of handling
the clocks explicitly in the driver.
Reviewed-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
> ---
> Changes in v13:
> - Replace pr_warn_once() with dev_warn() in csi_formatter_get_index_by_dt()
> to provide device context and warn on every occurrence (Loic)
> - Add WARN_ON() check for csi_formatter_find_format() return value in
> start/stop_stream functions to catch unexpected NULL (Loic)
> - Use regmap_set_bits()/regmap_clear_bits() instead of read-modify-write
> pattern and remove unused csi_formatter_read/write helper functions (Loic)
> - Add Reviewed-by tag from Frank Li
>
> Changes in v12:
> - Fix stream ID handling: iterate routing table instead of assuming
> stream ID equals loop index (0-7)
> - Remove stream_to_vc[] array: derive VC from routing table and frame
> descriptor on each start/stop operation
> - Remove V4L2_SUBDEV_FL_HAS_EVENTS flag since driver does not generate events
> - Support stream IDs 0-63 by using BIT_ULL() for stream masks
> - Add get_frame_desc call in stop_stream with proper error handling
> - Add csi_formatter_read() helper function for register reads
> - Use read-modify-write for CSI_VC_PIXEL_DATA_TYPE register to support
> multiplexed streams sharing the same virtual channel
> - Use route->sink_pad instead of hardcoded CSI_FORMATTER_PAD_SINK
> - Write back coerced format in set_fmt before propagating to source stream
> - Drop Frank's Reviewed-by tag due to significant changes, requesting re-review
>
> Changes in v10:
> - Use u8 for vc in csi_formatter_get_vc() and drop vc < 0 check
> - Add MFD_SYSCON dependency to Kconfig
> - Fix stream/VC mapping potential mismatch in start/stop_stream functions
>
> Changes in v8:
> - Remove fmt field and look up format from subdev state instead
> - Unify function and structure naming to use csi_formatter_ prefix
> - Remove misleading alignment comment from set_fmt function
> - Optimize get_frame_desc to call once per start_stream
> - Replace V4L2_FRAME_DESC_ENTRY_MAX with CSI_FORMATTER_VC_NUM in loops
> - Remove redundant debug message in enable_streams
> - Use MEDIA_PAD_FL_MUST_CONNECT flag instead of manual link check
> - Fix typo: Formater -> Formatter in Kconfig help text
> - Improve grammar in data type index mapping comment
>
> Changes in v7:
> - Update references from imx9 to imx95 for consistency with dt-bindings
> - Enable PM runtime before async registration
>
> Changes in v6:
> - Remove unused header includes
> - Unify macro naming: VCx/VCX -> VC and parameter x -> vc
> - Remove unused format field from csi_formatter struct
> - Use compact initialization for formats array
> - Make find_csi_format() return NULL instead of default format
> - Use unsigned int for array index in find_csi_format()
> - Add err_ prefix to error handling labels
> - Add v4l2_subdev_cleanup() and reorder cleanup sequence
> - Update enable_streams debug output format
> - Rename VC_MAX to VC_NUM and fix boundary check
> - Update CSI formatter Kconfig description
> - Use v4l2_subdev_get_frame_desc_passthrough() helper
> - Fix error paths in async registration and probe
> - Add mutex to protect enabled_streams
> - Switch to devm_pm_runtime_enable()
> - Remove redundant num_routes check in set_routing
> - Optimize get_index_by_dt() and add warning for unsupported type
> - csi_formatter_start/stop_stream: Process all streams in mask
> ---
> MAINTAINERS | 8 +
> drivers/media/platform/nxp/Kconfig | 15 +
> drivers/media/platform/nxp/Makefile | 1 +
> drivers/media/platform/nxp/imx95-csi-formatter.c | 808 +++++++++++++++++++++++
> 4 files changed, 832 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index efbf808063e5..05009228b162 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -19275,6 +19275,14 @@ S: Maintained
> F: Documentation/devicetree/bindings/media/nxp,imx8-jpeg.yaml
> F: drivers/media/platform/nxp/imx-jpeg
>
> +NXP i.MX 95 CSI PIXEL FORMATTER V4L2 DRIVER
> +M: Guoniu Zhou <guoniu.zhou@nxp.com>
> +L: imx@lists.linux.dev
> +L: linux-media@vger.kernel.org
> +S: Maintained
> +F: Documentation/devicetree/bindings/media/fsl,imx95-csi-formatter.yaml
> +F: drivers/media/platform/nxp/imx95-csi-formatter.c
> +
> NXP i.MX CLOCK DRIVERS
> M: Abel Vesa <abelvesa@kernel.org>
> R: Peng Fan <peng.fan@nxp.com>
> diff --git a/drivers/media/platform/nxp/Kconfig b/drivers/media/platform/nxp/Kconfig
> index 40e3436669e2..8f49908b0022 100644
> --- a/drivers/media/platform/nxp/Kconfig
> +++ b/drivers/media/platform/nxp/Kconfig
> @@ -28,6 +28,21 @@ config VIDEO_IMX8MQ_MIPI_CSI2
> Video4Linux2 driver for the MIPI CSI-2 receiver found on the i.MX8MQ
> SoC.
>
> +config VIDEO_IMX95_CSI_FORMATTER
> + tristate "NXP i.MX95 CSI Pixel Formatter driver"
> + depends on ARCH_MXC || COMPILE_TEST
> + depends on MFD_SYSCON
> + depends on VIDEO_DEV
> + select MEDIA_CONTROLLER
> + select V4L2_FWNODE
> + select VIDEO_V4L2_SUBDEV_API
> + help
> + This driver provides support for the CSI Pixel Formatter found on
> + i.MX95 series SoCs. This module unpacks the pixels received from the
> + CSI-2 interface and reformats them to meet pixel link requirements.
> +
> + Say Y here to enable CSI Pixel Formatter module for i.MX95 SoC.
> +
> config VIDEO_IMX_MIPI_CSIS
> tristate "NXP MIPI CSI-2 CSIS receiver found on i.MX7 and i.MX8 models"
> depends on ARCH_MXC || COMPILE_TEST
> diff --git a/drivers/media/platform/nxp/Makefile b/drivers/media/platform/nxp/Makefile
> index 4d90eb713652..6410115d870e 100644
> --- a/drivers/media/platform/nxp/Makefile
> +++ b/drivers/media/platform/nxp/Makefile
> @@ -6,6 +6,7 @@ obj-y += imx8-isi/
>
> obj-$(CONFIG_VIDEO_IMX7_CSI) += imx7-media-csi.o
> obj-$(CONFIG_VIDEO_IMX8MQ_MIPI_CSI2) += imx8mq-mipi-csi2.o
> +obj-$(CONFIG_VIDEO_IMX95_CSI_FORMATTER) += imx95-csi-formatter.o
> obj-$(CONFIG_VIDEO_IMX_MIPI_CSIS) += imx-mipi-csis.o
> obj-$(CONFIG_VIDEO_IMX_PXP) += imx-pxp.o
> obj-$(CONFIG_VIDEO_MX2_EMMAPRP) += mx2_emmaprp.o
> diff --git a/drivers/media/platform/nxp/imx95-csi-formatter.c b/drivers/media/platform/nxp/imx95-csi-formatter.c
> new file mode 100644
> index 000000000000..b0e8e753e94a
> --- /dev/null
> +++ b/drivers/media/platform/nxp/imx95-csi-formatter.c
> @@ -0,0 +1,808 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright 2025 NXP
> + */
> +
> +#include <linux/bits.h>
> +#include <linux/clk.h>
> +#include <linux/mfd/syscon.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/regmap.h>
> +
> +#include <media/mipi-csi2.h>
> +#include <media/v4l2-ctrls.h>
> +#include <media/v4l2-event.h>
> +#include <media/v4l2-fwnode.h>
> +#include <media/v4l2-mc.h>
> +#include <media/v4l2-subdev.h>
> +
> +/* CSI Pixel Formatter registers map */
> +
> +#define CSI_VC_INTERLACED_LINE_CNT(vc) (0x00 + (vc) * 0x04)
> +#define INTERLACED_ODD_LINE_CNT_SET(x) FIELD_PREP(GENMASK(13, 0), (x))
> +#define INTERLACED_EVEN_LINE_CNT_SET(x) FIELD_PREP(GENMASK(29, 16), (x))
> +
> +#define CSI_VC_INTERLACED_CTRL 0x20
> +
> +#define CSI_VC_INTERLACED_ERR 0x24
> +#define CSI_VC_ERR_MASK GENMASK(7, 0)
> +#define CSI_VC_ERR(vc) BIT((vc))
> +
> +#define CSI_VC_YUV420_FIRST_LINE_EVEN 0x28
> +#define YUV420_FIRST_LINE_EVEN(vc) BIT((vc))
> +
> +#define CSI_RAW32_CTRL 0x30
> +#define CSI_VC_RAW32_MODE(vc) BIT((vc))
> +#define CSI_VC_RAW32_SWAP_MODE(vc) BIT((vc) + 8)
> +
> +#define CSI_STREAM_FENCING_CTRL 0x34
> +#define CSI_VC_STREAM_FENCING(vc) BIT((vc))
> +#define CSI_VC_STREAM_FENCING_RST(vc) BIT((vc) + 8)
> +
> +#define CSI_STREAM_FENCING_STS 0x38
> +#define CSI_STREAM_FENCING_STS_MASK GENMASK(7, 0)
> +
> +#define CSI_VC_NON_PIXEL_DATA_TYPE(vc) (0x40 + (vc) * 0x04)
> +
> +#define CSI_VC_PIXEL_DATA_CTRL(vc) (0x60 + (vc) * 0x04)
> +#define NEW_VC(vc) FIELD_PREP(GENMASK(3, 1), vc)
> +#define REROUTE_VC_ENABLE BIT(0)
> +
> +#define CSI_VC_ROUTE_PIXEL_DATA_TYPE(vc) (0x80 + (vc) * 0x04)
> +
> +#define CSI_VC_NON_PIXEL_DATA_CTRL(vc) (0xa0 + (vc) * 0x04)
> +
> +#define CSI_VC_PIXEL_DATA_TYPE(vc) (0xc0 + (vc) * 0x04)
> +
> +#define CSI_VC_PIXEL_DATA_TYPE_ERR(vc) (0xe0 + (vc) * 0x04)
> +
> +#define CSI_FORMATTER_PAD_SINK 0
> +#define CSI_FORMATTER_PAD_SOURCE 1
> +#define CSI_FORMATTER_PAD_NUM 2
> +
> +#define CSI_FORMATTER_VC_NUM 8 /* Number of virtual channels */
> +
> +struct csi_formatter_pix_format {
> + u32 code;
> + u32 data_type;
> +};
> +
> +struct csi_formatter {
> + struct device *dev;
> + struct regmap *regs;
> + struct clk *clk;
> +
> + struct v4l2_subdev sd;
> + struct v4l2_subdev *csi_sd;
> + struct v4l2_async_notifier notifier;
> + struct media_pad pads[CSI_FORMATTER_PAD_NUM];
> +
> + u32 remote_pad;
> + u32 reg_offset;
> +
> + /* Protects enabled_streams */
> + struct mutex lock;
> + u64 enabled_streams;
> +};
> +
> +struct csi_formatter_dt_index {
> + u8 dtype;
> + u8 index;
> +};
> +
> +/*
> + * The index corresponds to the bit index in the register that enables
> + * the data type of pixel data transported by the Formatter.
> + */
> +static const struct csi_formatter_dt_index formatter_dt_to_index_map[] = {
> + { .dtype = MIPI_CSI2_DT_YUV420_8B, .index = 0 },
> + { .dtype = MIPI_CSI2_DT_YUV420_8B_LEGACY, .index = 2 },
> + { .dtype = MIPI_CSI2_DT_YUV422_8B, .index = 6 },
> + { .dtype = MIPI_CSI2_DT_RGB444, .index = 8 },
> + { .dtype = MIPI_CSI2_DT_RGB555, .index = 9 },
> + { .dtype = MIPI_CSI2_DT_RGB565, .index = 10 },
> + { .dtype = MIPI_CSI2_DT_RGB666, .index = 11 },
> + { .dtype = MIPI_CSI2_DT_RGB888, .index = 12 },
> + { .dtype = MIPI_CSI2_DT_RAW6, .index = 16 },
> + { .dtype = MIPI_CSI2_DT_RAW7, .index = 17 },
> + { .dtype = MIPI_CSI2_DT_RAW8, .index = 18 },
> + { .dtype = MIPI_CSI2_DT_RAW10, .index = 19 },
> + { .dtype = MIPI_CSI2_DT_RAW12, .index = 20 },
> + { .dtype = MIPI_CSI2_DT_RAW14, .index = 21 },
> + { .dtype = MIPI_CSI2_DT_RAW16, .index = 22 },
> +};
> +
> +static const struct csi_formatter_pix_format formats[] = {
> + /* YUV formats */
> + { MEDIA_BUS_FMT_UYVY8_1X16, MIPI_CSI2_DT_YUV422_8B },
> + /* RGB formats */
> + { MEDIA_BUS_FMT_RGB565_1X16, MIPI_CSI2_DT_RGB565 },
> + { MEDIA_BUS_FMT_RGB888_1X24, MIPI_CSI2_DT_RGB888 },
> + /* RAW (Bayer and greyscale) formats */
> + { MEDIA_BUS_FMT_SBGGR8_1X8, MIPI_CSI2_DT_RAW8 },
> + { MEDIA_BUS_FMT_SGBRG8_1X8, MIPI_CSI2_DT_RAW8 },
> + { MEDIA_BUS_FMT_SGRBG8_1X8, MIPI_CSI2_DT_RAW8 },
> + { MEDIA_BUS_FMT_SRGGB8_1X8, MIPI_CSI2_DT_RAW8 },
> + { MEDIA_BUS_FMT_Y8_1X8, MIPI_CSI2_DT_RAW8 },
> + { MEDIA_BUS_FMT_SBGGR10_1X10, MIPI_CSI2_DT_RAW10 },
> + { MEDIA_BUS_FMT_SGBRG10_1X10, MIPI_CSI2_DT_RAW10 },
> + { MEDIA_BUS_FMT_SGRBG10_1X10, MIPI_CSI2_DT_RAW10 },
> + { MEDIA_BUS_FMT_SRGGB10_1X10, MIPI_CSI2_DT_RAW10 },
> + { MEDIA_BUS_FMT_Y10_1X10, MIPI_CSI2_DT_RAW10 },
> + { MEDIA_BUS_FMT_SBGGR12_1X12, MIPI_CSI2_DT_RAW12 },
> + { MEDIA_BUS_FMT_SGBRG12_1X12, MIPI_CSI2_DT_RAW12 },
> + { MEDIA_BUS_FMT_SGRBG12_1X12, MIPI_CSI2_DT_RAW12 },
> + { MEDIA_BUS_FMT_SRGGB12_1X12, MIPI_CSI2_DT_RAW12 },
> + { MEDIA_BUS_FMT_Y12_1X12, MIPI_CSI2_DT_RAW12 },
> + { MEDIA_BUS_FMT_SBGGR14_1X14, MIPI_CSI2_DT_RAW14 },
> + { MEDIA_BUS_FMT_SGBRG14_1X14, MIPI_CSI2_DT_RAW14 },
> + { MEDIA_BUS_FMT_SGRBG14_1X14, MIPI_CSI2_DT_RAW14 },
> + { MEDIA_BUS_FMT_SRGGB14_1X14, MIPI_CSI2_DT_RAW14 },
> + { MEDIA_BUS_FMT_SBGGR16_1X16, MIPI_CSI2_DT_RAW16 },
> + { MEDIA_BUS_FMT_SGBRG16_1X16, MIPI_CSI2_DT_RAW16 },
> + { MEDIA_BUS_FMT_SGRBG16_1X16, MIPI_CSI2_DT_RAW16 },
> + { MEDIA_BUS_FMT_SRGGB16_1X16, MIPI_CSI2_DT_RAW16 },
> +};
> +
> +static const struct v4l2_mbus_framefmt formatter_default_fmt = {
> + .code = MEDIA_BUS_FMT_UYVY8_1X16,
> + .width = 1920U,
> + .height = 1080U,
> + .field = V4L2_FIELD_NONE,
> + .colorspace = V4L2_COLORSPACE_SMPTE170M,
> + .xfer_func = V4L2_MAP_XFER_FUNC_DEFAULT(V4L2_COLORSPACE_SMPTE170M),
> + .ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(V4L2_COLORSPACE_SMPTE170M),
> + .quantization = V4L2_QUANTIZATION_LIM_RANGE,
> +};
> +
> +static const struct csi_formatter_pix_format *csi_formatter_find_format(u32 code)
> +{
> + unsigned int i;
> +
> + for (i = 0; i < ARRAY_SIZE(formats); i++)
> + if (code == formats[i].code)
> + return &formats[i];
> +
> + return NULL;
> +}
> +
> +/* -----------------------------------------------------------------------------
> + * V4L2 subdev operations
> + */
> +
> +static inline struct csi_formatter *sd_to_formatter(struct v4l2_subdev *sdev)
> +{
> + return container_of(sdev, struct csi_formatter, sd);
> +}
> +
> +static int __csi_formatter_subdev_set_routing(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *state,
> + struct v4l2_subdev_krouting *routing)
> +{
> + int ret;
> +
> + ret = v4l2_subdev_routing_validate(sd, routing,
> + V4L2_SUBDEV_ROUTING_ONLY_1_TO_1);
> + if (ret)
> + return ret;
> +
> + return v4l2_subdev_set_routing_with_fmt(sd, state, routing,
> + &formatter_default_fmt);
> +}
> +
> +static int csi_formatter_subdev_init_state(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *sd_state)
> +{
> + struct v4l2_subdev_route routes[] = {
> + {
> + .sink_pad = CSI_FORMATTER_PAD_SINK,
> + .sink_stream = 0,
> + .source_pad = CSI_FORMATTER_PAD_SOURCE,
> + .source_stream = 0,
> + .flags = V4L2_SUBDEV_ROUTE_FL_ACTIVE,
> + },
> + };
> +
> + struct v4l2_subdev_krouting routing = {
> + .num_routes = ARRAY_SIZE(routes),
> + .routes = routes,
> + };
> +
> + return __csi_formatter_subdev_set_routing(sd, sd_state, &routing);
> +}
> +
> +static int csi_formatter_subdev_enum_mbus_code(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *sd_state,
> + struct v4l2_subdev_mbus_code_enum *code)
> +{
> + if (code->pad == CSI_FORMATTER_PAD_SOURCE) {
> + struct v4l2_mbus_framefmt *fmt;
> +
> + if (code->index > 0)
> + return -EINVAL;
> +
> + fmt = v4l2_subdev_state_get_format(sd_state, code->pad,
> + code->stream);
> + code->code = fmt->code;
> + return 0;
> + }
> +
> + if (code->index >= ARRAY_SIZE(formats))
> + return -EINVAL;
> +
> + code->code = formats[code->index].code;
> +
> + return 0;
> +}
> +
> +static int csi_formatter_subdev_set_fmt(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *sd_state,
> + struct v4l2_subdev_format *sdformat)
> +{
> + struct csi_formatter_pix_format const *format;
> + struct v4l2_mbus_framefmt *fmt;
> +
> + if (sdformat->pad == CSI_FORMATTER_PAD_SOURCE)
> + return v4l2_subdev_get_fmt(sd, sd_state, sdformat);
> +
> + format = csi_formatter_find_format(sdformat->format.code);
> + if (!format)
> + format = &formats[0];
> +
> + v4l_bound_align_image(&sdformat->format.width, 1, 0xffff, 2,
> + &sdformat->format.height, 1, 0xffff, 0, 0);
> +
> + fmt = v4l2_subdev_state_get_format(sd_state, sdformat->pad,
> + sdformat->stream);
> + *fmt = sdformat->format;
> +
> + /* Set default code if user set an invalid value */
> + fmt->code = format->code;
> + sdformat->format = *fmt;
> +
> + /* Propagate the format from sink stream to source stream */
> + fmt = v4l2_subdev_state_get_opposite_stream_format(sd_state, sdformat->pad,
> + sdformat->stream);
> + if (!fmt)
> + return -EINVAL;
> +
> + *fmt = sdformat->format;
> +
> + return 0;
> +}
> +
> +static int csi_formatter_subdev_set_routing(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *state,
> + enum v4l2_subdev_format_whence which,
> + struct v4l2_subdev_krouting *routing)
> +{
> + if (which == V4L2_SUBDEV_FORMAT_ACTIVE &&
> + media_entity_is_streaming(&sd->entity))
> + return -EBUSY;
> +
> + return __csi_formatter_subdev_set_routing(sd, state, routing);
> +}
> +
> +static u8 csi_formatter_get_index_by_dt(struct csi_formatter *formatter,
> + u8 data_type)
> +{
> + unsigned int i;
> +
> + for (i = 0; i < ARRAY_SIZE(formatter_dt_to_index_map); ++i) {
> + const struct csi_formatter_dt_index *entry =
> + &formatter_dt_to_index_map[i];
> +
> + if (data_type == entry->dtype)
> + return entry->index;
> + }
> +
> + dev_warn(formatter->dev, "Unsupported data type 0x%x, using default\n",
> + data_type);
> +
> + return formatter_dt_to_index_map[0].index;
> +}
> +
> +static int csi_formatter_get_vc(struct csi_formatter *formatter,
> + struct v4l2_mbus_frame_desc *fd,
> + unsigned int stream)
> +{
> + struct v4l2_mbus_frame_desc_entry *entry = NULL;
> + unsigned int i;
> + u8 vc;
> +
> + for (i = 0; i < fd->num_entries; ++i) {
> + if (fd->entry[i].stream == stream) {
> + entry = &fd->entry[i];
> + break;
> + }
> + }
> +
> + if (!entry) {
> + dev_err(formatter->dev,
> + "No frame desc entry for stream %u\n", stream);
> + return -EPIPE;
> + }
> +
> + vc = entry->bus.csi2.vc;
> +
> + if (vc >= CSI_FORMATTER_VC_NUM) {
> + dev_err(formatter->dev, "Invalid virtual channel %u\n", vc);
> + return -EINVAL;
> + }
> +
> + return vc;
> +}
> +
> +static void csi_formatter_stop_stream(struct csi_formatter *formatter,
> + struct v4l2_subdev_state *state,
> + u64 stream_mask)
> +{
> + const struct csi_formatter_pix_format *pix_fmt;
> + struct v4l2_mbus_frame_desc fd = {};
> + struct v4l2_subdev_route *route;
> + struct v4l2_mbus_framefmt *fmt;
> + unsigned int reg;
> + unsigned int mask;
> + int vc;
> + int ret;
> +
> + ret = v4l2_subdev_call(formatter->csi_sd, pad, get_frame_desc,
> + formatter->remote_pad, &fd);
> + if (ret < 0 && ret != -ENOIOCTLCMD) {
> + dev_err(formatter->dev, "Failed to get frame desc: %d\n", ret);
> + return;
> + }
> +
> + for_each_active_route(&state->routing, route) {
> + if (route->source_pad != CSI_FORMATTER_PAD_SOURCE)
> + continue;
> +
> + if (!(stream_mask & BIT_ULL(route->source_stream)))
> + continue;
> +
> + if (ret == -ENOIOCTLCMD) {
> + /*
> + * Source doesn't implement get_frame_desc, use
> + * default VC 0
> + */
> + vc = 0;
> + } else {
> + vc = csi_formatter_get_vc(formatter, &fd,
> + route->sink_stream);
> + if (vc < 0)
> + continue;
> + }
> +
> + fmt = v4l2_subdev_state_get_format(state, route->sink_pad,
> + route->sink_stream);
> +
> + pix_fmt = csi_formatter_find_format(fmt->code);
> + if (WARN_ON(!pix_fmt))
> + continue;
> +
> + reg = CSI_VC_PIXEL_DATA_TYPE(vc) + formatter->reg_offset;
> + mask = BIT(csi_formatter_get_index_by_dt(formatter,
> + pix_fmt->data_type));
> +
> + /* Clear the data type bit to disable this VC */
> + regmap_clear_bits(formatter->regs, reg, mask);
> + }
> +}
> +
> +static int csi_formatter_start_stream(struct csi_formatter *formatter,
> + struct v4l2_subdev_state *state,
> + u64 stream_mask)
> +{
> + const struct csi_formatter_pix_format *pix_fmt;
> + struct v4l2_subdev_route *route;
> + struct v4l2_mbus_framefmt *fmt;
> + struct v4l2_mbus_frame_desc fd = {};
> + u64 configured_streams = 0;
> + unsigned int reg;
> + unsigned int mask;
> + int vc;
> + int ret;
> +
> + ret = v4l2_subdev_call(formatter->csi_sd, pad, get_frame_desc,
> + formatter->remote_pad, &fd);
> + if (ret < 0 && ret != -ENOIOCTLCMD) {
> + dev_err(formatter->dev, "Failed to get frame desc: %d\n", ret);
> + return ret;
> + }
> +
> + for_each_active_route(&state->routing, route) {
> + if (route->source_pad != CSI_FORMATTER_PAD_SOURCE)
> + continue;
> +
> + if (!(stream_mask & BIT_ULL(route->source_stream)))
> + continue;
> +
> + if (ret == -ENOIOCTLCMD) {
> + /*
> + * Source doesn't implement get_frame_desc, use
> + * default VC 0
> + */
> + vc = 0;
> + } else {
> + vc = csi_formatter_get_vc(formatter, &fd,
> + route->sink_stream);
> + if (vc < 0) {
> + ret = vc;
> + goto err_cleanup;
> + }
> + }
> +
> + fmt = v4l2_subdev_state_get_format(state, route->sink_pad,
> + route->sink_stream);
> +
> + pix_fmt = csi_formatter_find_format(fmt->code);
> + if (WARN_ON(!pix_fmt)) {
> + ret = -EINVAL;
> + goto err_cleanup;
> + }
> +
> + reg = CSI_VC_PIXEL_DATA_TYPE(vc) + formatter->reg_offset;
> + mask = BIT(csi_formatter_get_index_by_dt(formatter,
> + pix_fmt->data_type));
> +
> + /* Set the data type bit to enable this VC */
> + regmap_set_bits(formatter->regs, reg, mask);
> +
> + configured_streams |= BIT_ULL(route->source_stream);
> + }
> +
> + return 0;
> +
> +err_cleanup:
> + csi_formatter_stop_stream(formatter, state, configured_streams);
> + return ret;
> +}
> +
> +static int csi_formatter_subdev_enable_streams(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *state,
> + u32 pad, u64 streams_mask)
> +{
> + struct csi_formatter *formatter = sd_to_formatter(sd);
> + struct device *dev = formatter->dev;
> + u64 sink_streams;
> + int ret;
> +
> + sink_streams = v4l2_subdev_state_xlate_streams(state,
> + CSI_FORMATTER_PAD_SOURCE,
> + CSI_FORMATTER_PAD_SINK,
> + &streams_mask);
> + if (!sink_streams || !streams_mask)
> + return -EINVAL;
> +
> + guard(mutex)(&formatter->lock);
> +
> + if (!formatter->enabled_streams) {
> + ret = pm_runtime_resume_and_get(formatter->dev);
> + if (ret < 0) {
> + dev_err(dev, "Failed to resume runtime PM: %d\n", ret);
> + return ret;
> + }
> + }
> +
> + ret = csi_formatter_start_stream(formatter, state, streams_mask);
> + if (ret)
> + goto err_runtime_put;
> +
> + ret = v4l2_subdev_enable_streams(formatter->csi_sd,
> + formatter->remote_pad,
> + sink_streams);
> + if (ret)
> + goto err_stop_stream;
> +
> + formatter->enabled_streams |= streams_mask;
> +
> + return 0;
> +
> +err_stop_stream:
> + csi_formatter_stop_stream(formatter, state, streams_mask);
> +err_runtime_put:
> + if (!formatter->enabled_streams)
> + pm_runtime_put(formatter->dev);
> + return ret;
> +}
> +
> +static int csi_formatter_subdev_disable_streams(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *state,
> + u32 pad, u64 streams_mask)
> +{
> + struct csi_formatter *formatter = sd_to_formatter(sd);
> + u64 sink_streams;
> + int ret;
> +
> + sink_streams = v4l2_subdev_state_xlate_streams(state,
> + CSI_FORMATTER_PAD_SOURCE,
> + CSI_FORMATTER_PAD_SINK,
> + &streams_mask);
> + if (!sink_streams || !streams_mask)
> + return -EINVAL;
> +
> + guard(mutex)(&formatter->lock);
> +
> + ret = v4l2_subdev_disable_streams(formatter->csi_sd, formatter->remote_pad,
> + sink_streams);
> + if (ret)
> + dev_err(formatter->dev, "Failed to disable streams: %d\n", ret);
> +
> + csi_formatter_stop_stream(formatter, state, streams_mask);
> +
> + formatter->enabled_streams &= ~streams_mask;
> +
> + if (!formatter->enabled_streams)
> + pm_runtime_put(formatter->dev);
> +
> + return ret;
> +}
> +
> +static const struct v4l2_subdev_pad_ops formatter_subdev_pad_ops = {
> + .enum_mbus_code = csi_formatter_subdev_enum_mbus_code,
> + .get_fmt = v4l2_subdev_get_fmt,
> + .set_fmt = csi_formatter_subdev_set_fmt,
> + .get_frame_desc = v4l2_subdev_get_frame_desc_passthrough,
> + .set_routing = csi_formatter_subdev_set_routing,
> + .enable_streams = csi_formatter_subdev_enable_streams,
> + .disable_streams = csi_formatter_subdev_disable_streams,
> +};
> +
> +static const struct v4l2_subdev_ops formatter_subdev_ops = {
> + .pad = &formatter_subdev_pad_ops,
> +};
> +
> +static const struct v4l2_subdev_internal_ops formatter_internal_ops = {
> + .init_state = csi_formatter_subdev_init_state,
> +};
> +
> +/* -----------------------------------------------------------------------------
> + * Media entity operations
> + */
> +
> +static const struct media_entity_operations formatter_entity_ops = {
> + .link_validate = v4l2_subdev_link_validate,
> + .get_fwnode_pad = v4l2_subdev_get_fwnode_pad_1_to_1,
> +};
> +
> +static int csi_formatter_subdev_init(struct csi_formatter *formatter)
> +{
> + struct v4l2_subdev *sd = &formatter->sd;
> + int ret;
> +
> + v4l2_subdev_init(sd, &formatter_subdev_ops);
> +
> + snprintf(sd->name, sizeof(sd->name), "%s", dev_name(formatter->dev));
> + sd->internal_ops = &formatter_internal_ops;
> +
> + sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE |
> + V4L2_SUBDEV_FL_STREAMS;
> + sd->entity.function = MEDIA_ENT_F_PROC_VIDEO_PIXEL_FORMATTER;
> + sd->entity.ops = &formatter_entity_ops;
> + sd->dev = formatter->dev;
> +
> + formatter->pads[CSI_FORMATTER_PAD_SINK].flags = MEDIA_PAD_FL_SINK
> + | MEDIA_PAD_FL_MUST_CONNECT;
> + formatter->pads[CSI_FORMATTER_PAD_SOURCE].flags = MEDIA_PAD_FL_SOURCE;
> +
> + ret = media_entity_pads_init(&sd->entity, CSI_FORMATTER_PAD_NUM,
> + formatter->pads);
> + if (ret) {
> + dev_err(formatter->dev, "Failed to init pads\n");
> + return ret;
> + }
> +
> + ret = v4l2_subdev_init_finalize(sd);
> + if (ret)
> + media_entity_cleanup(&sd->entity);
> +
> + return ret;
> +}
> +
> +static inline struct csi_formatter *
> +notifier_to_csi_formatter(struct v4l2_async_notifier *n)
> +{
> + return container_of(n, struct csi_formatter, notifier);
> +}
> +
> +static int csi_formatter_notify_bound(struct v4l2_async_notifier *notifier,
> + struct v4l2_subdev *sd,
> + struct v4l2_async_connection *asc)
> +{
> + const unsigned int link_flags = MEDIA_LNK_FL_IMMUTABLE
> + | MEDIA_LNK_FL_ENABLED;
> + struct csi_formatter *formatter = notifier_to_csi_formatter(notifier);
> + struct v4l2_subdev *sdev = &formatter->sd;
> + struct media_pad *sink = &sdev->entity.pads[CSI_FORMATTER_PAD_SINK];
> + struct media_pad *remote_pad;
> + int ret;
> +
> + formatter->csi_sd = sd;
> +
> + dev_dbg(formatter->dev, "Bound subdev: %s pad\n", sd->name);
> +
> + ret = v4l2_create_fwnode_links_to_pad(sd, sink, link_flags);
> + if (ret < 0)
> + return ret;
> +
> + remote_pad = media_pad_remote_pad_first(sink);
> + if (!remote_pad) {
> + dev_err(formatter->dev, "Pipe not setup correctly\n");
> + return -EPIPE;
> + }
> + formatter->remote_pad = remote_pad->index;
> +
> + return 0;
> +}
> +
> +static const struct v4l2_async_notifier_operations formatter_notify_ops = {
> + .bound = csi_formatter_notify_bound,
> +};
> +
> +static int csi_formatter_async_register(struct csi_formatter *formatter)
> +{
> + struct device *dev = formatter->dev;
> + struct v4l2_async_connection *asc;
> + int ret;
> +
> + struct fwnode_handle *ep __free(fwnode_handle) =
> + fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0,
> + FWNODE_GRAPH_ENDPOINT_NEXT);
> + if (!ep)
> + return -ENOTCONN;
> +
> + v4l2_async_subdev_nf_init(&formatter->notifier, &formatter->sd);
> +
> + asc = v4l2_async_nf_add_fwnode_remote(&formatter->notifier, ep,
> + struct v4l2_async_connection);
> + if (IS_ERR(asc)) {
> + ret = PTR_ERR(asc);
> + goto err_cleanup_notifier;
> + }
> +
> + formatter->notifier.ops = &formatter_notify_ops;
> +
> + ret = v4l2_async_nf_register(&formatter->notifier);
> + if (ret)
> + goto err_cleanup_notifier;
> +
> + ret = v4l2_async_register_subdev(&formatter->sd);
> + if (ret)
> + goto err_unregister_notifier;
> +
> + return 0;
> +
> +err_unregister_notifier:
> + v4l2_async_nf_unregister(&formatter->notifier);
> +err_cleanup_notifier:
> + v4l2_async_nf_cleanup(&formatter->notifier);
> + return ret;
> +}
> +
> +static void csi_formatter_async_unregister(struct csi_formatter *formatter)
> +{
> + v4l2_async_unregister_subdev(&formatter->sd);
> + v4l2_async_nf_unregister(&formatter->notifier);
> + v4l2_async_nf_cleanup(&formatter->notifier);
> +}
> +
> +/* -----------------------------------------------------------------------------
> + * Suspend/resume
> + */
> +
> +static int csi_formatter_runtime_suspend(struct device *dev)
> +{
> + struct v4l2_subdev *sd = dev_get_drvdata(dev);
> + struct csi_formatter *formatter = sd_to_formatter(sd);
> +
> + clk_disable_unprepare(formatter->clk);
> +
> + return 0;
> +}
> +
> +static int csi_formatter_runtime_resume(struct device *dev)
> +{
> + struct v4l2_subdev *sd = dev_get_drvdata(dev);
> + struct csi_formatter *formatter = sd_to_formatter(sd);
> +
> + return clk_prepare_enable(formatter->clk);
> +}
> +
> +static DEFINE_RUNTIME_DEV_PM_OPS(csi_formatter_pm_ops,
> + csi_formatter_runtime_suspend,
> + csi_formatter_runtime_resume, NULL);
> +
> +static int csi_formatter_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct csi_formatter *formatter;
> + u32 val;
> + int ret;
> +
> + formatter = devm_kzalloc(dev, sizeof(*formatter), GFP_KERNEL);
> + if (!formatter)
> + return -ENOMEM;
> +
> + formatter->dev = dev;
> +
> + ret = devm_mutex_init(dev, &formatter->lock);
> + if (ret)
> + return ret;
> +
> + formatter->regs = syscon_node_to_regmap(dev->parent->of_node);
> + if (IS_ERR(formatter->regs))
> + return dev_err_probe(dev, PTR_ERR(formatter->regs),
> + "Failed to get csi formatter regmap\n");
> +
> + ret = of_property_read_u32(dev->of_node, "reg", &val);
> + if (ret < 0)
> + return dev_err_probe(dev, ret,
> + "Failed to get csi formatter reg property\n");
> +
> + formatter->reg_offset = val;
> +
> + formatter->clk = devm_clk_get(dev, NULL);
> + if (IS_ERR(formatter->clk))
> + return dev_err_probe(dev, PTR_ERR(formatter->clk),
> + "Failed to get pixel clock\n");
> +
> + ret = csi_formatter_subdev_init(formatter);
> + if (ret < 0)
> + return dev_err_probe(dev, ret, "Failed to initialize formatter subdev\n");
> +
> + platform_set_drvdata(pdev, &formatter->sd);
> +
> + /* Enable runtime PM. */
> + ret = devm_pm_runtime_enable(dev);
> + if (ret)
> + goto err_cleanup_subdev;
> +
> + ret = csi_formatter_async_register(formatter);
> + if (ret < 0) {
> + dev_err_probe(dev, ret, "Failed to register async subdevice\n");
> + goto err_cleanup_subdev;
> + }
> +
> + return 0;
> +
> +err_cleanup_subdev:
> + v4l2_subdev_cleanup(&formatter->sd);
> + media_entity_cleanup(&formatter->sd.entity);
> + return ret;
> +}
> +
> +static void csi_formatter_remove(struct platform_device *pdev)
> +{
> + struct v4l2_subdev *sd = platform_get_drvdata(pdev);
> + struct csi_formatter *formatter = sd_to_formatter(sd);
> +
> + csi_formatter_async_unregister(formatter);
> +
> + v4l2_subdev_cleanup(&formatter->sd);
> + media_entity_cleanup(&formatter->sd.entity);
> +}
> +
> +static const struct of_device_id csi_formatter_of_match[] = {
> + { .compatible = "fsl,imx95-csi-formatter" },
> + { /* sentinel */ },
> +};
> +MODULE_DEVICE_TABLE(of, csi_formatter_of_match);
> +
> +static struct platform_driver csi_formatter_device_driver = {
> + .driver = {
> + .name = "csi-pixel-formatter",
> + .of_match_table = csi_formatter_of_match,
> + .pm = pm_ptr(&csi_formatter_pm_ops),
> + },
> + .probe = csi_formatter_probe,
> + .remove = csi_formatter_remove,
> +};
> +
> +module_platform_driver(csi_formatter_device_driver);
> +
> +MODULE_AUTHOR("NXP Semiconductor, Inc.");
> +MODULE_DESCRIPTION("NXP i.MX95 CSI Pixel Formatter driver");
> +MODULE_LICENSE("GPL");
>
> --
> 2.34.1
>
^ permalink raw reply
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