From: Parthiban Veerasooran <parthiban.veerasooran@microchip.com>
To: <andrew+netdev@lunn.ch>, <davem@davemloft.net>,
<edumazet@google.com>, <kuba@kernel.org>, <pabeni@redhat.com>,
<hkallweit1@gmail.com>, <linux@armlinux.org.uk>
Cc: <netdev@vger.kernel.org>, <linux-kernel@vger.kernel.org>,
<UNGLinuxDriver@microchip.com>,
<Parthiban.Veerasooran@microchip.com>,
Parthiban Veerasooran <parthiban.veerasooran@microchip.com>
Subject: [PATCH net-next v3 2/4] net: ethernet: oa_tc6: deliver the PHY interrupt to phylib
Date: Fri, 18 Sep 2026 20:03:00 +0530 [thread overview]
Message-ID: <20260918143302.76909-3-parthiban.veerasooran@microchip.com> (raw)
In-Reply-To: <20260918143302.76909-1-parthiban.veerasooran@microchip.com>
The OA TC6 standard defines PHY interrupt delivery via the SPI status
register as optional. The PHY has no dedicated interrupt line in that
case; its interrupt is signalled through the MAC-PHY SPI interface, but
phy_mac_interrupt() can't make the PHY driver read and acknowledge its
own status registers. Expose it to phylib instead as a nested virtual
IRQ, gated by a new OA_TC6_PHY_INT quirk flag for drivers that route
PHY interrupts in-band.
When set, a nested virtual IRQ is created in oa_tc6_mdiobus_register()
before mdiobus_register(), and all mii_bus->irq[] entries are populated
with it so phy_device_create() picks it up regardless of MDIO address.
Teardown is integrated into oa_tc6_mdiobus_unregister().
A custom irq_chip (oa_tc6_phy_irq_chip) implements mask/unmask via
irq_bus_lock/irq_bus_sync_unlock, writing the mask bit to hardware over
SPI. The interrupt starts masked (hardware reset default) and is only
unmasked when phylib requests it, so disabling the nested IRQ actually
masks the hardware source too, preventing interrupt storms.
Dispatch is deferred to a workqueue rather than run synchronously from
the threaded IRQ: phy_interrupt() takes phydev->lock and PHY
handle_interrupt() issues synchronous SPI transfers, either of which
would otherwise stall the single thread pumping every TX/RX data chunk.
PHYINT is level triggered and stays asserted until acked, so a no-op
reschedule on an already-pending work item can't lose or duplicate an
event.
Select IRQ_DOMAIN in Kconfig for the irq_domain APIs used here.
Prerequisite for "net: phy: microchip_t1s: fix collision detection on
PLCA status change" (Fixes: 78341049fbcd) to fully cover the LAN865X
internal PHY.
Signed-off-by: Parthiban Veerasooran <parthiban.veerasooran@microchip.com>
---
drivers/net/ethernet/Kconfig | 1 +
drivers/net/ethernet/oa_tc6.c | 145 ++++++++++++++++++++++++++++++++++
include/linux/oa_tc6.h | 3 +
3 files changed, 149 insertions(+)
diff --git a/drivers/net/ethernet/Kconfig b/drivers/net/ethernet/Kconfig
index 8581ccba1505..ea2161232a17 100644
--- a/drivers/net/ethernet/Kconfig
+++ b/drivers/net/ethernet/Kconfig
@@ -150,6 +150,7 @@ config OA_TC6
tristate "OPEN Alliance TC6 10BASE-T1x MAC-PHY support" if COMPILE_TEST
depends on SPI
select PHYLIB
+ select IRQ_DOMAIN
help
This library implements OPEN Alliance TC6 10BASE-T1x MAC-PHY
Serial Interface protocol for supporting 10BASE-T1x MAC-PHYs.
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 6fcc5f561d56..b4a95155e924 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -8,6 +8,8 @@
#include <linux/bitfield.h>
#include <linux/iopoll.h>
#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
#include <linux/mdio.h>
#include <linux/phy.h>
#include <linux/oa_tc6.h>
@@ -70,6 +72,11 @@ struct oa_tc6 {
struct phy_device *phydev;
struct mii_bus *mdiobus;
struct spi_device *spi;
+ struct mutex phy_irq_lock; /* Serialises irq_bus_lock/sync_unlock */
+ bool phy_irq_masked; /* Shadow of OA_TC6_INT_MASK0_PHY_INT_MASK */
+ struct irq_domain *phy_irq_domain;
+ int phy_virq;
+ struct work_struct phy_irq_work;
struct mutex spi_ctrl_lock; /* Protects spi control transfer */
spinlock_t tx_skb_lock; /* Protects tx skb handling */
void *spi_ctrl_tx_buf;
@@ -528,6 +535,114 @@ int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
}
EXPORT_SYMBOL_GPL(oa_tc6_mdiobus_write_c45);
+static void oa_tc6_phy_irq_work(struct work_struct *work)
+{
+ struct oa_tc6 *tc6 = container_of(work, struct oa_tc6, phy_irq_work);
+
+ /* Dispatched off the SPI chunk-processing thread so that
+ * phy_interrupt() taking phydev->lock and issuing synchronous SPI
+ * control transfers from PHY handle_interrupt() cannot stall the single
+ * thread pumping TX/RX data chunks.
+ */
+ handle_nested_irq(tc6->phy_virq);
+}
+
+static void oa_tc6_phy_irq_mask(struct irq_data *irqd)
+{
+ struct oa_tc6 *tc6 = irq_data_get_irq_chip_data(irqd);
+
+ tc6->phy_irq_masked = true;
+}
+
+static void oa_tc6_phy_irq_unmask(struct irq_data *irqd)
+{
+ struct oa_tc6 *tc6 = irq_data_get_irq_chip_data(irqd);
+
+ tc6->phy_irq_masked = false;
+}
+
+static void oa_tc6_phy_irq_bus_lock(struct irq_data *irqd)
+{
+ struct oa_tc6 *tc6 = irq_data_get_irq_chip_data(irqd);
+
+ mutex_lock(&tc6->phy_irq_lock);
+}
+
+static void oa_tc6_phy_irq_bus_sync_unlock(struct irq_data *irqd)
+{
+ struct oa_tc6 *tc6 = irq_data_get_irq_chip_data(irqd);
+ u32 regval;
+ int ret;
+
+ ret = oa_tc6_read_register(tc6, OA_TC6_REG_INT_MASK0, ®val);
+ if (ret) {
+ dev_err(&tc6->spi->dev, "Failed to read INT_MASK0: %d\n", ret);
+ goto unlock;
+ }
+
+ if (tc6->phy_irq_masked)
+ regval |= OA_TC6_INT_MASK0_PHY_INT_MASK;
+ else
+ regval &= ~OA_TC6_INT_MASK0_PHY_INT_MASK;
+
+ ret = oa_tc6_write_register(tc6, OA_TC6_REG_INT_MASK0, regval);
+ if (ret)
+ dev_err(&tc6->spi->dev, "Failed to write INT_MASK0: %d\n", ret);
+
+unlock:
+ mutex_unlock(&tc6->phy_irq_lock);
+}
+
+static struct irq_chip oa_tc6_phy_irq_chip = {
+ .name = "oa_tc6_phy",
+ .irq_mask = oa_tc6_phy_irq_mask,
+ .irq_unmask = oa_tc6_phy_irq_unmask,
+ .irq_bus_lock = oa_tc6_phy_irq_bus_lock,
+ .irq_bus_sync_unlock = oa_tc6_phy_irq_bus_sync_unlock,
+};
+
+static int oa_tc6_phy_irq_map(struct irq_domain *domain, unsigned int irq,
+ irq_hw_number_t hwirq)
+{
+ irq_set_chip_data(irq, domain->host_data);
+ irq_set_chip_and_handler(irq, &oa_tc6_phy_irq_chip, handle_simple_irq);
+ irq_set_nested_thread(irq, true);
+ irq_set_noprobe(irq);
+
+ return 0;
+}
+
+static const struct irq_domain_ops oa_tc6_phy_irq_domain_ops = {
+ .map = oa_tc6_phy_irq_map,
+};
+
+static int oa_tc6_phy_irq_setup(struct oa_tc6 *tc6)
+{
+ INIT_WORK(&tc6->phy_irq_work, oa_tc6_phy_irq_work);
+
+ tc6->phy_irq_domain =
+ irq_domain_create_linear(NULL, 1,
+ &oa_tc6_phy_irq_domain_ops, tc6);
+ if (!tc6->phy_irq_domain)
+ return -ENOMEM;
+
+ tc6->phy_virq = irq_create_mapping(tc6->phy_irq_domain, 0);
+ tc6->phy_irq_masked = true;
+ if (!tc6->phy_virq) {
+ irq_domain_remove(tc6->phy_irq_domain);
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static void oa_tc6_phy_irq_teardown(struct oa_tc6 *tc6)
+{
+ cancel_work_sync(&tc6->phy_irq_work);
+ irq_dispose_mapping(tc6->phy_virq);
+ irq_domain_remove(tc6->phy_irq_domain);
+}
+
static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6)
{
int ret;
@@ -559,9 +674,25 @@ static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6)
snprintf(tc6->mdiobus->id, ARRAY_SIZE(tc6->mdiobus->id), "%s",
dev_name(&tc6->spi->dev));
+ if (tc6->quirk_flags & OA_TC6_PHY_INT) {
+ ret = oa_tc6_phy_irq_setup(tc6);
+ if (ret) {
+ mdiobus_free(tc6->mdiobus);
+ return ret;
+ }
+ /* Populate all irq[] entries before registration so
+ * phy_device_create() picks up the virtual IRQ regardless of
+ * the PHY's MDIO address.
+ */
+ for (int i = 0; i < PHY_MAX_ADDR; i++)
+ tc6->mdiobus->irq[i] = tc6->phy_virq;
+ }
+
ret = mdiobus_register(tc6->mdiobus);
if (ret) {
netdev_err(tc6->netdev, "Could not register MDIO bus\n");
+ if (tc6->quirk_flags & OA_TC6_PHY_INT)
+ oa_tc6_phy_irq_teardown(tc6);
mdiobus_free(tc6->mdiobus);
return ret;
}
@@ -572,6 +703,8 @@ static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6)
static void oa_tc6_mdiobus_unregister(struct oa_tc6 *tc6)
{
mdiobus_unregister(tc6->mdiobus);
+ if (tc6->quirk_flags & OA_TC6_PHY_INT)
+ oa_tc6_phy_irq_teardown(tc6);
mdiobus_free(tc6->mdiobus);
}
@@ -810,6 +943,17 @@ static int oa_tc6_process_extended_status(struct oa_tc6 *tc6)
return ret;
}
+ /* Dispatch the PHY interrupt to phylib via the nested virtual IRQ so
+ * the PHY driver reads and acknowledges its status. This is deferred
+ * to a workqueue rather than dispatched synchronously here, since
+ * phy_interrupt() takes phydev->lock and PHY handle_interrupt() issues
+ * synchronous SPI control transfers, which would otherwise block this
+ * thread.
+ */
+ if ((tc6->quirk_flags & OA_TC6_PHY_INT) &&
+ FIELD_GET(OA_TC6_STATUS0_PHY_INT, value))
+ schedule_work(&tc6->phy_irq_work);
+
if (FIELD_GET(OA_TC6_STATUS0_RX_BUFFER_OVERFLOW_ERROR, value)) {
oa_tc6_look_for_new_frame(tc6);
net_err_ratelimited("%s: Receive buffer overflow error\n",
@@ -1468,6 +1612,7 @@ struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev,
tc6->spi = spi;
tc6->netdev = netdev;
SET_NETDEV_DEV(netdev, &spi->dev);
+ mutex_init(&tc6->phy_irq_lock);
mutex_init(&tc6->spi_ctrl_lock);
spin_lock_init(&tc6->tx_skb_lock);
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index 27f652d4920b..1c8cc263c5e5 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -30,6 +30,7 @@
/* Status Register #0 */
#define OA_TC6_REG_STATUS0 0x0008
+#define OA_TC6_STATUS0_PHY_INT BIT(7)
#define OA_TC6_STATUS0_RESETC BIT(6) /* Reset Complete */
#define OA_TC6_STATUS0_HEADER_ERROR BIT(5)
#define OA_TC6_STATUS0_LOSS_OF_FRAME_ERROR BIT(4)
@@ -43,6 +44,7 @@
/* Interrupt Mask Register #0 */
#define OA_TC6_REG_INT_MASK0 0x000C
+#define OA_TC6_INT_MASK0_PHY_INT_MASK BIT(7)
#define OA_TC6_INT_MASK0_HEADER_ERR_MASK BIT(5)
#define OA_TC6_INT_MASK0_LOSS_OF_FRAME_ERR_MASK BIT(4)
#define OA_TC6_INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK BIT(3)
@@ -68,6 +70,7 @@ struct oa_tc6;
enum oa_tc6_quirk_flag {
OA_TC6_BROKEN_PHY = BIT(0),
+ OA_TC6_PHY_INT = BIT(1),
};
struct oa_tc6_quirks {
--
2.43.0
next prev parent reply other threads:[~2026-09-18 14:33 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-18 14:32 [PATCH net-next v3 0/4] net: microchip_t1s: fix collision detection on PLCA status change Parthiban Veerasooran
2026-09-18 14:32 ` [PATCH net-next v3 1/4] net: phy: " Parthiban Veerasooran
2026-09-22 15:08 ` netdev-bot+sashiko
2026-09-18 14:33 ` Parthiban Veerasooran [this message]
2026-09-22 15:08 ` [PATCH net-next v3 2/4] net: ethernet: oa_tc6: deliver the PHY interrupt to phylib netdev-bot+sashiko
2026-09-18 14:33 ` [PATCH net-next v3 3/4] microchip: lan865x: enable PHY interrupt via virtual IRQ Parthiban Veerasooran
2026-09-22 15:08 ` netdev-bot+sashiko
2026-09-18 14:33 ` [PATCH net-next v3 4/4] net: phy: microchip_t1s: fix collision detection for LAN867X Rev.D0 Parthiban Veerasooran
2026-09-22 15:08 ` netdev-bot+sashiko
2026-09-24 15:33 ` [PATCH net-next v3 0/4] net: microchip_t1s: fix collision detection on PLCA status change Jakub Kicinski
2026-09-25 10:57 ` Parthiban Veerasooran
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260918143302.76909-3-parthiban.veerasooran@microchip.com \
--to=parthiban.veerasooran@microchip.com \
--cc=UNGLinuxDriver@microchip.com \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=hkallweit1@gmail.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux@armlinux.org.uk \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox