[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <ZQf1QwNzK5jjOWk9@lopingdog.com>
Date: Mon, 18 Sep 2023 01:59:15 -0500
From: Jay Monkman <jtm@...ingdog.com>
To: devicetree@...r.kernel.org, netdev@...r.kernel.org,
linux-kernel@...r.kernel.org,
Piergiorgio Beruto <piergiorgio.beruto@...il.com>,
Arndt Schuebel <Arndt.Schuebel@...emi.com>
Subject: [PATCH 4/4] net/onsemi: Add NCN26010 driver
This adds a driver for the onsemi NCN26010 ethernet controller. The
NCN26010 is combined MAC/PHY that uses SPI to interface to a
microcontroller.
Signed-off-by: Jay Monkman <jtm@...ingdog.com>
---
drivers/net/ethernet/Kconfig | 1 +
drivers/net/ethernet/Makefile | 1 +
drivers/net/ethernet/onsemi/Kconfig | 27 +
drivers/net/ethernet/onsemi/Makefile | 9 +
drivers/net/ethernet/onsemi/onsemi_macphy.h | 298 +++
.../net/ethernet/onsemi/onsemi_macphy_core.c | 2149 +++++++++++++++++
.../ethernet/onsemi/onsemi_macphy_ethtool.c | 179 ++
7 files changed, 2664 insertions(+)
create mode 100644 drivers/net/ethernet/onsemi/Kconfig
create mode 100644 drivers/net/ethernet/onsemi/Makefile
create mode 100644 drivers/net/ethernet/onsemi/onsemi_macphy.h
create mode 100644 drivers/net/ethernet/onsemi/onsemi_macphy_core.c
create mode 100644 drivers/net/ethernet/onsemi/onsemi_macphy_ethtool.c
diff --git a/drivers/net/ethernet/Kconfig b/drivers/net/ethernet/Kconfig
index 5a274b99f299..5ae26396ccfa 100644
--- a/drivers/net/ethernet/Kconfig
+++ b/drivers/net/ethernet/Kconfig
@@ -145,6 +145,7 @@ source "drivers/net/ethernet/ni/Kconfig"
source "drivers/net/ethernet/natsemi/Kconfig"
source "drivers/net/ethernet/neterion/Kconfig"
source "drivers/net/ethernet/netronome/Kconfig"
+source "drivers/net/ethernet/onsemi/Kconfig"
source "drivers/net/ethernet/8390/Kconfig"
source "drivers/net/ethernet/nvidia/Kconfig"
source "drivers/net/ethernet/nxp/Kconfig"
diff --git a/drivers/net/ethernet/Makefile b/drivers/net/ethernet/Makefile
index 0d872d4efcd1..62900d4fcd7a 100644
--- a/drivers/net/ethernet/Makefile
+++ b/drivers/net/ethernet/Makefile
@@ -73,6 +73,7 @@ obj-$(CONFIG_NET_VENDOR_NVIDIA) += nvidia/
obj-$(CONFIG_LPC_ENET) += nxp/
obj-$(CONFIG_NET_VENDOR_OKI) += oki-semi/
obj-$(CONFIG_ETHOC) += ethoc.o
+obj-$(CONFIG_NET_VENDOR_ONSEMI) += onsemi/
obj-$(CONFIG_NET_VENDOR_PACKET_ENGINES) += packetengines/
obj-$(CONFIG_NET_VENDOR_PASEMI) += pasemi/
obj-$(CONFIG_NET_VENDOR_QLOGIC) += qlogic/
diff --git a/drivers/net/ethernet/onsemi/Kconfig b/drivers/net/ethernet/onsemi/Kconfig
new file mode 100644
index 000000000000..a5b029e53a6d
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/Kconfig
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause
+#
+# onsemi device configuration
+#
+
+config NET_VENDOR_ONSEMI
+ bool "onsemi devices"
+ default y
+ depends on SPI
+ help
+ If you have a network (Ethernet) card belonging to this class, say Y.
+
+ Note that the answer to this question doesn't directly affect the
+ kernel: saying N will just cause the configurator to skip all
+ the questions about ONSEMI devices. If you say Y, you will be asked
+ for your specific card in the following questions.
+
+if NET_VENDOR_ONSEMI
+
+config ONSEMI_MACPHY
+ tristate "onsemi NCN260xx SPI MAC-PHY devices"
+ depends on SPI
+ select NCN26000_PHY
+ help
+ Say yes here to build support for onsemi MAC-PHY devices
+
+endif # NET_VENDOR_ONSEMI
diff --git a/drivers/net/ethernet/onsemi/Makefile b/drivers/net/ethernet/onsemi/Makefile
new file mode 100644
index 000000000000..50ef2e4930a1
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause
+#
+# Makefile for the onsemi network device drivers.
+#
+
+obj-$(CONFIG_ONSEMI_MACPHY) += onsemi_macphy.o
+onsemi_macphy-y := \
+ onsemi_macphy_core.o \
+ onsemi_macphy_ethtool.o
diff --git a/drivers/net/ethernet/onsemi/onsemi_macphy.h b/drivers/net/ethernet/onsemi/onsemi_macphy.h
new file mode 100644
index 000000000000..0e84d5f06ca8
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/onsemi_macphy.h
@@ -0,0 +1,298 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later OR BSD-2-Clause */
+
+/* onsemi MACPHY ethernet driver
+ * Copyright (c) 2023 onsemi
+ */
+#include <linux/hrtimer.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/phylink.h>
+#include <linux/spi/spi.h>
+
+// macros ------------------------------------------------------------------- //
+
+#define USEC_TO_KTIME(x) (ns_to_ktime((x) * NSEC_PER_USEC))
+
+// constants ---------------------------------------------------------------- //
+
+#define DRV_NAME "onmph"
+#define DRV_VERSION "1.0.3.0"
+
+#define ONMPH_MAX_FRAME_SIZE 1522 // 1500 + MAC HDR + 2 x 802.1Q VLAN
+#define ONMPH_N_MCAST_FILTERS 3
+
+#define ONMPH_MODEL_NCN26010 0x00
+
+// number of stat counters
+#define ONMPH_STATS_NUM 33
+
+// max number of frames we can store in the TX queue
+#define ONMPH_MAX_PENDING_TX_FRAMES 32
+
+// list of device capabilities
+#define ONMPH_CAP_MACADDR BIT(0) // MAC address stored in ROM
+
+// maximum protocol payload size
+#define MPH_MAX_CPS 64
+
+// maximum TX/RX credits reported in the footer
+#define MPH_MAX_CRED 32
+
+// ONMPH registers ----------------------------------------------------------- //
+
+// List of available MMS
+#define ONMPH_MMS_SPI_MII 0
+#define ONMPH_MMS_MAC 1
+#define ONMPH_MMS_PCS 2
+#define ONMPH_MMS_PMA 3
+#define ONMPH_MMS_PLCA_VEND2 4
+#define ONMPH_MMS_VS 12
+
+// SPI protocol ID & version register
+#define ONMPH_REG_SPI_IDVER 0x00
+
+// SPI OID and model register
+#define ONMPH_REG_SPI_PHYID 0x01
+
+#define ONMPH_SPI_PHYID_OUI_SHIFT 10
+#define ONMPH_SPI_PHYID_OUI_MASK GENMASK(31, ONMPH_SPI_PHYID_OUI_SHIFT)
+#define ONMPH_SPI_PHYID_MODEL_SHIFT 4
+#define ONMPH_SPI_PHYID_MODEL_MASK GENMASK(9, ONMPH_SPI_PHYID_MODEL_SHIFT)
+#define ONMPH_SPI_PHYID_REV_SHIFT 0
+#define ONMPH_SPI_PHYID_REV_MASK GENMASK(3, ONMPH_SPI_PHYID_REV_SHIFT)
+
+// SPI reset register
+#define ONMPH_REG_SPI_RST 0x03
+
+#define ONMPH_RST_SRST_BIT BIT(0)
+
+// SPI configuration register #0
+#define ONMPH_REG_SPI_CFG0 0x04
+
+#define ONMPH_SPI_CFG0_SYNC_BIT BIT(15)
+#define ONMPH_SPI_CFG0_TXFCSVE_BIT BIT(14)
+#define ONMPH_SPI_CFG0_CSARFE_BIT BIT(13)
+#define ONMPH_SPI_CFG0_ZARFE_BIT BIT(12)
+#define ONMPH_SPI_CFG0_TXCTHRESH_SHIFT 10
+#define ONMPH_SPI_CFG0_TXCTHRESH_MASK GENMASK(11, ONMPH_SPI_CFG0_TXCTHRESH_SHIFT)
+#define ONMPH_SPI_CFG0_TXCTE_BIT BIT(9)
+#define ONMPH_SPI_CFG0_RXCTE_BIT BIT(8)
+#define ONMPH_SPI_CFG0_FTSE_BIT BIT(7)
+#define ONMPH_SPI_CFG0_FTSS_BIT BIT(6)
+#define ONMPH_SPI_CFG0_PROTE_BIT BIT(5)
+#define ONMPH_SPI_CFG0_SEQE_BIT BIT(4)
+#define ONMPH_SPI_CFG0_CPS_SHIFT 0
+#define ONMPH_SPI_CFG0_CPS_MASK GENMASK(2, ONMPH_SPI_CFG0_CPS_SHIFT)
+
+#define ONMPH_TXCTHRESH_1 0x0
+#define ONMPH_TXCTHRESH_4 0x1
+#define ONMPH_TXCTHRESH_8 0x2
+#define ONMPH_TXCTHRESH_16 0x3
+
+#define ONMPH_CPS_8 0x3
+#define ONMPH_CPS_16 0x4
+#define ONMPH_CPS_32 0x5
+#define ONMPH_CPS_64 0x6
+
+// SPI status register #0
+#define ONMPH_REG_SPI_ST0 0x08
+
+#define ONMPH_SPI_ST0_CDPE_BIT BIT(12)
+#define ONMPH_SPI_ST0_TXFCSE_BIT BIT(11)
+#define ONMPH_SPI_ST0_TTSCAC_BIT BIT(10)
+#define ONMPH_SPI_ST0_TTSCAB_BIT BIT(9)
+#define ONMPH_SPI_ST0_TTSCAA_BIT BIT(8)
+#define ONMPH_SPI_ST0_PHYINT_BIT BIT(7)
+#define ONMPH_SPI_ST0_RESETC_BIT BIT(6)
+#define ONMPH_SPI_ST0_HDRE_BIT BIT(5)
+#define ONMPH_SPI_ST0_LOFE_BIT BIT(4)
+#define ONMPH_SPI_ST0_RXBOE_BIT BIT(3)
+#define ONMPH_SPI_ST0_TXBUE_BIT BIT(2)
+#define ONMPH_SPI_ST0_TXBOE_BIT BIT(1)
+#define ONMPH_SPI_ST0_TXPE_BIT BIT(0)
+
+// SPI IRQ enable register #0 (use the ONMPH_SPI_ST0_*_BIT constants)
+#define ONMPH_REG_SPI_IRQM0 0x0c
+
+// SPI buffer status register
+#define ONMPH_REG_SPI_BUFST 0x0b
+
+#define ONMPH_SPI_BUFST_TXC_SHIFT 8
+#define ONMPH_SPI_BUFST_TXC_MASK GENMASK(15, ONMPH_SPI_BUFST_TXC_SHIFT)
+#define ONMPH_SPI_BUFST_RCA_SHIFT 0
+#define ONMPH_SPI_BUFST_RCA_MASK GENMASK(7, ONMPH_SPI_BUFST_RCA_SHIFT)
+
+// MAC control register
+#define ONMPH_REG_MAC_CONTROL 0
+
+#define ONMPH_MAC_CONTROL_NFCSF_BIT BIT(19)
+#define ONMPH_MAC_CONTROL_MCSF_BIT BIT(18)
+#define ONMPH_MAC_CONTROL_ADRF_BIT BIT(16)
+#define ONMPH_MAC_CONTROL_FCSA_BIT BIT(8)
+#define ONMPH_MAC_CONTROL_TXEN_BIT BIT(1)
+#define ONMPH_MAC_CONTROL_RXEN_BIT BIT(0)
+
+// MAC address filter registers
+#define ONMPH_REG_MAC_ADDRFILTL(n) (16 + 2 * (n))
+#define ONMPH_REG_MAC_ADDRFILTH(n) (17 + 2 * (n))
+#define ONMPH_REG_MAC_ADDRMASKL(n) (32 + 2 * (n))
+#define ONMPH_REG_MAC_ADDRMASKH(n) (33 + 2 * (n))
+
+#define ONMPH_MAC_ADDRFILT_EN_BIT BIT(31)
+
+// MAC statistic registers
+#define ONMPH_REG_MAC_STOCTECTSTXL 48
+#define ONMPH_REG_MAC_STOCTECTSTXH 49
+#define ONMPH_REG_MAC_STFRAMESTXOK 50
+#define ONMPH_REG_MAC_STBCASTTXOK 51
+#define ONMPH_REG_MAC_STMCASTTXOK 52
+#define ONMPH_REG_MAC_STFRAMESTX64 53
+#define ONMPH_REG_MAC_STFRAMESTX65 54
+#define ONMPH_REG_MAC_STFRAMESTX128 55
+#define ONMPH_REG_MAC_STFRAMESTX256 56
+#define ONMPH_REG_MAC_STFRAMESTX512 57
+#define ONMPH_REG_MAC_STFRAMESTX1024 58
+#define ONMPH_REG_MAC_STTXUNDEFLOW 59
+#define ONMPH_REG_MAC_STSINGLECOL 60
+#define ONMPH_REG_MAC_STMULTICOL 61
+#define ONMPH_REG_MAC_STEXCESSCOL 62
+#define ONMPH_REG_MAC_STDEFERREDTX 63
+#define ONMPH_REG_MAC_STCRSERR 64
+#define ONMPH_REG_MAC_STOCTECTSRXL 65
+#define ONMPH_REG_MAC_STOCTECTSRXH 66
+#define ONMPH_REG_MAC_STFRAMESRXOK 67
+#define ONMPH_REG_MAC_STBCASTRXOK 68
+#define ONMPH_REG_MAC_STMCASTRXOK 69
+#define ONMPH_REG_MAC_STFRAMESRX64 60
+#define ONMPH_REG_MAC_STFRAMESRX65 71
+#define ONMPH_REG_MAC_STFRAMESRX128 72
+#define ONMPH_REG_MAC_STFRAMESRX256 73
+#define ONMPH_REG_MAC_STFRAMESRX512 74
+#define ONMPH_REG_MAC_STFRAMESRX1024 75
+#define ONMPH_REG_MAC_STRUNTSERR 76
+#define ONMPH_REG_MAC_STRXTOOLONG 77
+#define ONMPH_REG_MAC_STFCSERRS 78
+#define ONMPH_REG_MAC_STSYMBOLERRS 79
+#define ONMPH_REG_MAC_STALIGNERRS 80
+#define ONMPH_REG_MAC_STRXOVERFLOW 81
+#define ONMPH_REG_MAC_STRXDROPPED 82
+
+// first/last statistic register for sequential access
+#define ONMPH_REG_MAC_FIRST_STAT ONMPH_REG_MAC_STOCTECTSTXL
+#define ONMPH_REG_MAC_LAST_STAT ONMPH_REG_MAC_STRXDROPPED
+
+#define ONMPH_NUMBER_OF_STAT_REGS \
+ (ONMPH_REG_MAC_LAST_STAT - ONMPH_REG_MAC_FIRST_STAT + 1)
+
+// permanent MAC address register
+#define ONMPH_REG_VS_MACID0 0x1002
+#define ONMPH_REG_VS_MACID1 0x1003
+
+#define ONMPH_MACID1_UID_SHIFT 0
+#define ONMPH_MACID1_UID_MASK GENMASK(7, ONMPH_MACID1_UID_SHIFT)
+
+// chip identification register
+#define ONMPH_REG_VS_CHIPID 0x1000
+
+#define ONMPH_CHIPID_MODEL_SHIFT 8
+#define ONMPH_CHIPID_MODEL_MASK GENMASK(15, ONMPH_CHIPID_MODEL_SHIFT)
+#define ONMPH_CHIPID_REVISION_SHIFT 0
+#define ONMPH_CHIPID_REVISION_MASK GENMASK(7, ONMPH_CHIPID_REVISION_SHIFT)
+
+// prototypes / forward declarations ---------------------------------------- //
+
+extern const struct ethtool_ops onmph_ethtool_ops;
+
+// type definitions --------------------------------------------------------- //
+
+struct onmph_info {
+ struct device *dev;
+ struct net_device *ndev;
+
+ // model information
+ u32 model;
+ u32 version;
+ unsigned int capabilities;
+
+ // tasks and synchronization variables
+ spinlock_t lock;
+ struct task_struct *thread;
+
+ // global state variables
+ bool event_pending; // related to the EXST footer flag
+ unsigned int rx_flags;
+ bool rx_flags_upd;
+
+ // global configuration
+ unsigned int mtu; // maximum frame size for RX
+ bool ctrl_protected; // reg read/write protected enabled
+
+ bool tx_fcs_calc; // driver calculates TX FCS
+ bool rx_fcs_check; // driver checks RX FCS
+
+ // polling time in jiffies, or MAX_SCHEDULE_TIMEOUT (disable)
+ signed long poll_jiff;
+
+ // SPI
+ struct spi_device *spi;
+ bool irq_pending;
+ unsigned int cps; // current chunk payload size
+ unsigned int csz; // total chunk size
+
+ // PHY IRQ emulation
+ struct irq_domain *irqdm;
+ bool phy_irq_pending;
+ bool phy_irq_en;
+ int phy_virq;
+
+ // PHY MDIO emulation
+ struct mii_bus *mdio_bus;
+ char mdio_bus_name[MII_BUS_ID_SIZE];
+ u16 mii_c45_ctrl;
+ u16 mii_c45_addr[32];
+
+ // PHY config/status
+ struct phy_device *phy;
+ struct phylink *phylink;
+ struct phylink_config phylink_config;
+
+ // TX state variables
+ struct list_head tx_skb_q; // queue of skb ready for TX
+ unsigned int tx_skb_q_sz; // number of TX skbs in tx_skb_q
+
+ struct sk_buff *tx_skb; // skb currently being transmitted
+ unsigned int tx_txc; // TX chunks available
+ void *tx_mem;
+
+ // RX state variables
+ struct sk_buff *rx_skb; // skb currently being received
+ unsigned int rx_rca; // RX chunks available
+ bool rx_skip; // skip chunks until new SV
+ void *rx_mem;
+
+ // statistic counters variables
+ u64 stats_data[ONMPH_STATS_NUM];
+
+ struct irq_chip irq_chip;
+};
+
+// helpers ------------------------------------------------------------------ //
+
+int onmph_wregs(struct onmph_info *priv, int mms, int addr, int len,
+ const u32 *data);
+
+int onmph_rregs(struct onmph_info *priv, int mms, int addr, int len, u32 *data);
+
+static inline int onmph_wreg(struct onmph_info *priv, int mms, int addr, u32 data)
+{
+ return onmph_wregs(priv, mms, addr, 1, &data);
+}
+
+static inline int onmph_rreg(struct onmph_info *priv, int mms, int addr, u32 *data)
+{
+ return onmph_rregs(priv, mms, addr, 1, data);
+}
+
+// -------------------------------------------------------------------------- //
diff --git a/drivers/net/ethernet/onsemi/onsemi_macphy_core.c b/drivers/net/ethernet/onsemi/onsemi_macphy_core.c
new file mode 100644
index 000000000000..121f38b3f895
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/onsemi_macphy_core.c
@@ -0,0 +1,2149 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR BSD-2-Clause
+
+/* onsemi MACPHY ethernet driver
+ * Copyright (c) 2022 onsemi
+ */
+
+#include "onsemi_macphy.h"
+
+#include <linux/etherdevice.h>
+#include <linux/if_ether.h>
+#include <linux/irqchip.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/phy.h>
+
+// ONMPH functions & definitions--------------------------------------------- //
+
+// struct to hold chunk header flags
+struct mph_dhdr_info {
+ unsigned int sv; // start-valid flag
+ unsigned int ev; // end-valid flag
+ unsigned int swo; // start word (u32) offset
+ unsigned int ebo; // end byte offset
+};
+
+// converts a MACPHY ID to a device name
+static inline const char *onmph_id_to_name(u32 id)
+{
+ switch (id) {
+ case ONMPH_MODEL_NCN26010:
+ return "NCN26010";
+ }
+
+ return "unknown";
+}
+
+// calculates the header parity according to the MACPHY protocol specifications
+static inline int parity32(u32 data)
+{
+ u32 tmp = data ^ (data >> 16);
+
+ tmp ^= tmp >> 8;
+ tmp ^= tmp >> 4;
+ tmp ^= tmp >> 2;
+ tmp ^= tmp >> 1;
+
+ return !(tmp & 1);
+}
+
+// make generic data header
+#define MPH_MKDATAHDR(DV, SV, EV, SWO, EBO, NRX) (\
+ (0x80000000) | \
+ ((NRX) << 29) | \
+ ((DV) << 21) | \
+ ((SV) << 20) | \
+ ((SWO) << 16) | \
+ ((EV) << 14) | \
+ ((EBO) << 8))
+
+// make data header for TX/RX
+#define MPH_MKDATAHDR_TXRX(SV, EV, SWO, EBO) \
+ MPH_MKDATAHDR(1, SV, EV, SWO, EBO, 0)
+
+// make data header for TX only
+#define MPH_MKDATAHDR_TX(SV, EV, SWO, EBO) MPH_MKDATAHDR(1, SV, EV, SWO, EBO, 1)
+
+// make data header for RX only
+#define MPH_MKDATAHDR_RX MPH_MKDATAHDR(0, 0, 0, 0, 0, 0)
+
+// make a data header for TX and RX in network byte order
+static inline u32 onmph_mkdatahdr_txrx(struct mph_dhdr_info *info)
+{
+ u32 ret = MPH_MKDATAHDR_TXRX(info->sv, info->ev, info->swo, info->ebo);
+
+ ret = cpu_to_be32(ret | parity32(ret));
+
+ return ret;
+}
+
+// make a data header for RX only in network byte order
+static inline u32 onmph_mkdatahdr_rx(void)
+{
+ // return a constant which parity is 0 (optimization)
+ return cpu_to_be32(MPH_MKDATAHDR_RX);
+}
+
+// make generic control header
+#define MPH_MKCTLHDR(MMS, ADR, LEN, WNR) (\
+ ((WNR) << 29) | \
+ ((MMS) << 24) | \
+ ((ADR) << 8) | \
+ (((LEN) - 1) << 1))
+
+// make single register (R)ead/(W)rite
+#define MPH_MKCTLHDR_SR(MMS, ADR) MPH_MKCTLHDR(MMS, ADR, 1, 0)
+#define MPH_MKCTLHDR_SW(MMS, ADR) MPH_MKCTLHDR(MMS, ADR, 1, 1)
+
+// make multiple register (R)ead/(W)rite
+#define MPH_MKCTLHDR_MR(MMS, ADR, LEN) MPH_MKCTLHDR(MMS, ADR, LEN, 0)
+#define MPH_MKCTLHDR_MW(MMS, ADR, LEN) MPH_MKCTLHDR(MMS, ADR, LEN, 1)
+
+/* Make a control header a single register read
+ *
+ * The header includes parity and is in network byte order
+ * Parameters
+ * mms - memory mode selection group
+ * addr - register address
+ */
+static inline u32 onmph_mkctlhdr_sr(int mms, int addr)
+{
+ u32 ret = MPH_MKCTLHDR_SR(mms, addr);
+
+ ret = cpu_to_be32(ret | parity32(ret));
+
+ return ret;
+}
+
+/* Make a control header for a multiple register read
+ *
+ * The header includes parity and is in network byte order
+ * Parameters
+ * mms - memory mode selection group
+ * addr - base register address
+ * len - number of registers
+ */
+static inline u32 onmph_mkctlhdr_mr(int mms, int addr, int len)
+{
+ u32 ret = MPH_MKCTLHDR_MR(mms, addr, len);
+
+ ret = cpu_to_be32(ret | parity32(ret));
+
+ return ret;
+}
+
+/* Make a control header a single register write
+ *
+ * The header includes parity and is in network byte order
+ * Parameters
+ * mms - memory mode selection group
+ * addr - register address
+ */
+static inline u32 onmph_mkctlhdr_sw(int mms, int addr)
+{
+ u32 ret = MPH_MKCTLHDR_SW(mms, addr);
+
+ ret = cpu_to_be32(ret | parity32(ret));
+
+ return ret;
+}
+
+/* Make a control header for a multiple register write
+ *
+ * The header includes parity and is in network byte order
+ * Parameters
+ * mms - memory mode selection group
+ * addr - base register address
+ * len - number of registers
+ */
+static inline u32 onmph_mkctlhdr_mw(int mms, int addr, int len)
+{
+ u32 ret = MPH_MKCTLHDR_MW(mms, addr, len);
+
+ ret = cpu_to_be32(ret | parity32(ret));
+
+ return ret;
+}
+
+// struct to hold chunk footer flags
+struct mph_dftr_info {
+ bool exst;
+ bool hdrb;
+ bool sync;
+ bool dv;
+ bool sv;
+ bool fd;
+ bool ev;
+
+ unsigned int rca;
+ unsigned int txc;
+ unsigned int swo;
+ unsigned int ebo;
+ unsigned int sbo;
+};
+
+#define MPH_FOOTER_EXTS BIT(31)
+#define MPH_FOOTER_HDRB BIT(30)
+#define MPH_FOOTER_SYNC BIT(29)
+#define MPH_FOOTER_DV BIT(21)
+#define MPH_FOOTER_SV BIT(20)
+#define MPH_FOOTER_FD BIT(15)
+#define MPH_FOOTER_EV BIT(14)
+
+#define MPH_FOOTER_RCA_SHIFT 24
+#define MPH_FOOTER_RCA_MASK GENMASK(28, MPH_FOOTER_RCA_SHIFT)
+#define MPH_FOOTER_SWO_SHIFT 16
+#define MPH_FOOTER_SWO_MASK GENMASK(19, MPH_FOOTER_SWO_SHIFT)
+#define MPH_FOOTER_EBO_SHIFT 8
+#define MPH_FOOTER_EBO_MASK GENMASK(13, MPH_FOOTER_EBO_SHIFT)
+#define MPH_FOOTER_TXC_SHIFT 1
+#define MPH_FOOTER_TXC_MASK GENMASK(5, MPH_FOOTER_TXC_SHIFT)
+
+static int onmph_parse_footer(u32 ftr, struct mph_dftr_info *info)
+{
+ ftr = be32_to_cpu(ftr);
+
+ if (parity32(ftr))
+ return -EPROTO;
+
+ info->exst = (ftr & MPH_FOOTER_EXTS) != 0;
+ info->hdrb = (ftr & MPH_FOOTER_HDRB) != 0;
+ info->sync = (ftr & MPH_FOOTER_SYNC) != 0;
+ info->dv = (ftr & MPH_FOOTER_DV) != 0;
+ info->sv = (ftr & MPH_FOOTER_SV) != 0;
+ info->fd = (ftr & MPH_FOOTER_FD) != 0;
+ info->ev = (ftr & MPH_FOOTER_EV) != 0;
+
+ info->rca = (ftr & MPH_FOOTER_RCA_MASK) >> MPH_FOOTER_RCA_SHIFT;
+ info->txc = (ftr & MPH_FOOTER_TXC_MASK) >> MPH_FOOTER_TXC_SHIFT;
+ info->swo = (ftr & MPH_FOOTER_SWO_MASK) >> MPH_FOOTER_SWO_SHIFT;
+ info->ebo = (ftr & MPH_FOOTER_EBO_MASK) >> MPH_FOOTER_EBO_SHIFT;
+ info->sbo = info->swo << 2;
+
+ return 0;
+}
+
+// Helper to prepare a control SPI transfer.
+static int onmph_prepare_ctrl_transfer(struct onmph_info *priv, int len,
+ struct spi_transfer *st)
+{
+ int ws;
+
+ // protocol only allows from 1 to 128 registers access in a row
+ if (unlikely(len > 128 || len < 1))
+ return -EINVAL;
+
+ /* Allocate a single buffer for both TX & RX (twice the transfer length)
+ * including 2 words for the header and the dummy tail.
+ * When protected mode is enabled, we further need to double the
+ * data buffer length to accommodate the redundancy.
+ */
+ ws = (priv->ctrl_protected ? 2 * len : len) + 2;
+ st->len = ws << 2; // 4 bytes per register / header
+
+ st->tx_buf = kzalloc(2 * st->len, GFP_ATOMIC);
+ if (!st->tx_buf)
+ return -ENOMEM;
+
+ // set rx_buf half-way into the allocated buffer
+ st->rx_buf = (void *)(st->tx_buf + st->len);
+ return 0;
+}
+
+static inline void onmph_finalize_ctrl_transfer(const struct spi_transfer *st)
+{
+ kfree(st->tx_buf);
+}
+
+// Write multiple MACPHY registers
+int onmph_wregs(struct onmph_info *priv, int mms, int addr, int len,
+ const u32 *data)
+{
+ struct spi_transfer st = { 0 };
+ u32 *wptr;
+ int ret;
+
+ ret = onmph_prepare_ctrl_transfer(priv, len, &st);
+ if (ret)
+ return ret;
+
+ wptr = (u32 *)st.tx_buf;
+ *(wptr++) = onmph_mkctlhdr_mw(mms, addr, len);
+
+ // copy the data in network byte order
+ do {
+ const u32 val = cpu_to_be32(*data);
+ *wptr = val;
+
+ // in protected mode, duplicate the value (complemented)
+ if (priv->ctrl_protected)
+ *(++wptr) = ~val;
+
+ if (--len == 0)
+ break;
+
+ wptr++;
+ data++;
+ } while (true);
+
+ ret = spi_sync_transfer(priv->spi, &st, 1);
+ if (!priv->ctrl_protected || ret)
+ goto out;
+
+ // check header & data loopback skipping the initial dummy RX word
+ if (memcmp(st.tx_buf, st.rx_buf + 4, st.len - 4))
+ ret = -EIO;
+
+out:
+ onmph_finalize_ctrl_transfer(&st);
+ return ret;
+}
+
+// Read multiple MACPHY registers
+int onmph_rregs(struct onmph_info *priv, int mms, int addr, int len, u32 *data)
+{
+ struct spi_transfer st = { 0 };
+ const u32 *rptr;
+ u32 *wptr;
+ int ret;
+
+ ret = onmph_prepare_ctrl_transfer(priv, len, &st);
+ if (ret)
+ return ret;
+
+ wptr = (u32 *)st.tx_buf;
+ *wptr = onmph_mkctlhdr_mr(mms, addr, len);
+
+ ret = spi_sync_transfer(priv->spi, &st, 1);
+ if (ret)
+ goto out;
+
+ rptr = st.rx_buf + 4; // looped-back header (skip dummy)
+ if (*(rptr++) != *wptr) {
+ ret = -EIO;
+ goto out;
+ }
+
+ do {
+ *data = be32_to_cpu(*rptr);
+
+ if (priv->ctrl_protected) {
+ if (~(*data) != be32_to_cpu(*(++rptr))) {
+ ret = -EIO;
+ goto out;
+ }
+ }
+
+ if (--len == 0)
+ break;
+
+ rptr++;
+ data++;
+ } while (true);
+out:
+ onmph_finalize_ctrl_transfer(&st);
+ return ret;
+}
+
+// initializes the net device MAC address by reading the UID stored into the
+// device internal non-volatile memory
+static int onmph_read_mac_from_nvmem(struct onmph_info *priv)
+{
+ u8 addr[ETH_ALEN];
+ int i, j;
+ u32 val;
+ int ret;
+
+ ret = onmph_rreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_PHYID, &val);
+ if (ret)
+ goto out;
+
+ val = (val & ONMPH_SPI_PHYID_OUI_MASK) >> ONMPH_SPI_PHYID_OUI_SHIFT;
+
+ // convert the OID in host bit / byte order
+ for (i = 2; i >= 0; --i) {
+ addr[i] = 0;
+ for (j = 0; j < 8; ++j) {
+ addr[i] |= (val & 1) << (7 - j);
+ val >>= 1;
+ }
+ }
+
+ ret = onmph_rreg(priv, ONMPH_MMS_VS, ONMPH_REG_VS_MACID1, &val);
+ if (ret)
+ goto out;
+
+ addr[3] = val & 0xff;
+
+ ret = onmph_rreg(priv, ONMPH_MMS_VS, ONMPH_REG_VS_MACID0, &val);
+ if (ret)
+ goto out;
+
+ addr[4] = (val >> 8) & 0xff;
+ addr[5] = val & 0xff;
+
+ __dev_addr_set(priv->ndev, addr, ETH_ALEN);
+ priv->ndev->addr_assign_type = NET_ADDR_PERM;
+
+out:
+ return ret;
+}
+
+// Writes MAC address to macphy registers
+static int onmph_set_mac_filter(struct net_device *ndev, const u8 *mac)
+{
+ struct onmph_info *priv = netdev_priv(ndev);
+ u32 val;
+ int ret;
+
+ // set unicast address filter
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_ADDRMASKL(0),
+ 0xffffffff);
+ if (ret)
+ return ret;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_ADDRMASKH(0),
+ 0x0000ffff);
+ if (ret)
+ return ret;
+
+ val = ((u32)mac[2] << 24) |
+ ((u32)mac[3] << 16) |
+ ((u32)mac[4] << 8) |
+ ((u32)mac[5]);
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_ADDRFILTL(0), val);
+ if (ret)
+ return ret;
+
+ val = ONMPH_MAC_ADDRFILT_EN_BIT | ((u32)mac[0] << 8) | ((u32)mac[1]);
+
+ return onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_ADDRFILTH(0), val);
+}
+
+/* Reset the NCN26010 and configure control data protection */
+static int onmph_reset(struct onmph_info *priv)
+{
+ int i = 0;
+ u32 val;
+ int ret;
+
+ // reset the MACPHY
+ ret = onmph_wreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_RST,
+ ONMPH_RST_SRST_BIT);
+ if (ret)
+ return ret;
+
+ // the reset cleared PROTE bit --> switch to normal accesses
+ priv->ctrl_protected = false;
+
+ do {
+ // wait for reset to complete
+ usleep_range(1000, 2000);
+ ret = onmph_rreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_ST0,
+ &val);
+
+ if (ret)
+ return ret;
+ } while (!(val & ONMPH_SPI_ST0_RESETC_BIT) && (i++ < 3));
+
+ if (i >= 3)
+ return -ENODEV;
+
+ if (device_property_read_bool(priv->dev, "ctrl-protected")) {
+ // protected control access is configured --> enable it
+ ret = onmph_rreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_CFG0,
+ &val);
+ if (ret) {
+ dev_err(priv->dev, "failed to read CFG0 reg\n");
+ return ret;
+ }
+
+ val |= ONMPH_SPI_CFG0_PROTE_BIT;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_CFG0,
+ val);
+ if (ret) {
+ dev_err(priv->dev, "failed to write CFG0 reg\n");
+ return ret;
+ }
+
+ priv->ctrl_protected = true;
+
+ dev_info(priv->dev, "control data protection enabled\n");
+ } else {
+ // nothing to do!
+ dev_info(priv->dev, "control data protection disabled\n");
+ }
+
+ return 0;
+}
+
+static int onmph_init(struct onmph_info *priv)
+{
+ u32 val;
+ int ret;
+
+ ret = onmph_reset(priv);
+ if (ret) {
+ dev_err(priv->dev, "device reset failed\n");
+ goto out;
+ }
+
+ // acknowledge all IRQ status bits
+ ret = onmph_wreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_ST0, val);
+ if (ret)
+ goto out;
+
+ /* TODO: investigate why enabling ONMPH_SPI_CFG0_RXCTE_BIT
+ * (RX Cut-Through mode) makes the device produce RX frames larger than
+ * the MTU in some corner cases (e.g, SPI too slow). Potential HW bug??
+ * For now, let's stick to Store & Forward
+ */
+
+ // configure the SPI protocol
+ val = (ONMPH_SPI_CFG0_SYNC_BIT) |
+ (ONMPH_TXCTHRESH_8 << ONMPH_SPI_CFG0_TXCTHRESH_SHIFT) |
+ (ONMPH_CPS_64 << ONMPH_SPI_CFG0_CPS_SHIFT);
+
+ if (priv->ctrl_protected)
+ val |= ONMPH_SPI_CFG0_PROTE_BIT;
+
+ if (priv->tx_fcs_calc)
+ val |= ONMPH_SPI_CFG0_TXFCSVE_BIT;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_CFG0, val);
+ if (ret)
+ goto out;
+
+ // unmask relevant interrupts
+ val = (u32) ~(ONMPH_SPI_ST0_PHYINT_BIT | ONMPH_SPI_ST0_RESETC_BIT |
+ ONMPH_SPI_ST0_HDRE_BIT | ONMPH_SPI_ST0_LOFE_BIT |
+ ONMPH_SPI_ST0_RXBOE_BIT | ONMPH_SPI_ST0_TXBOE_BIT |
+ ONMPH_SPI_ST0_TXPE_BIT);
+
+ ret = onmph_wreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_IRQM0, val);
+ if (ret)
+ goto out;
+
+ // read the initial value of TX credits
+ ret = onmph_rreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_BUFST, &val);
+ if (ret)
+ goto out;
+
+ priv->tx_txc = (val & ONMPH_SPI_BUFST_TXC_MASK) >> ONMPH_SPI_BUFST_TXC_SHIFT;
+
+ if (priv->tx_txc > MPH_MAX_CRED)
+ priv->tx_txc = MPH_MAX_CRED;
+
+ // RX credits and errors are reset too
+ priv->rx_rca = 0;
+ priv->rx_skip = false;
+
+ // program the source MAC address into the device
+ ret = onmph_set_mac_filter(priv->ndev, priv->ndev->dev_addr);
+
+ // enable the MAC w/ default config
+ val = ONMPH_MAC_CONTROL_ADRF_BIT | ONMPH_MAC_CONTROL_TXEN_BIT |
+ ONMPH_MAC_CONTROL_RXEN_BIT;
+
+ if (!priv->tx_fcs_calc)
+ val |= ONMPH_MAC_CONTROL_FCSA_BIT;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, val);
+ if (ret)
+ goto out;
+
+ // allow TX frames to be delivered
+ netif_start_queue(priv->ndev);
+out:
+ return ret;
+}
+
+static void onmph_shutdown(struct onmph_info *priv)
+{
+ struct net_device *ndev = priv->ndev;
+ u32 reg;
+
+ // stop the TX queue
+ netif_stop_queue(ndev);
+ disable_irq(ndev->irq);
+
+ // stop the MAC
+ onmph_rreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, ®);
+ reg &= ~(ONMPH_MAC_CONTROL_TXEN_BIT | ONMPH_MAC_CONTROL_RXEN_BIT);
+ onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, reg);
+}
+
+// Configures macphy for promiscuous mode
+static int onmph_set_promiscuous_mode(struct onmph_info *priv,
+ unsigned int rx_flags)
+{
+ int ret = 0;
+ u32 val;
+ u32 ctrl0;
+
+ // read the current status of the control register
+ ret = onmph_rreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, &ctrl0);
+ if (ret)
+ return ret;
+
+ if (rx_flags & IFF_PROMISC) {
+ if (!(ctrl0 & ONMPH_MAC_CONTROL_ADRF_BIT))
+ return 0;
+
+ // set promiscuous mode by clearing ADRF
+ val = ctrl0 & ~ONMPH_MAC_CONTROL_ADRF_BIT;
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL,
+ val);
+ } else if (!(ctrl0 & ONMPH_MAC_CONTROL_ADRF_BIT)) {
+ // disable promiscuous mode
+ val = ctrl0 | ONMPH_MAC_CONTROL_ADRF_BIT;
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL,
+ val);
+ }
+
+ return ret;
+}
+
+// Configure macphy for multicast filtering
+static int onmph_set_multicast_mode(struct onmph_info *priv,
+ unsigned int rx_flags)
+{
+ int ret = 0;
+ u32 ctrl0;
+ u32 val;
+
+ if ((rx_flags & IFF_ALLMULTI) ||
+ (netdev_mc_count(priv->ndev) > ONMPH_N_MCAST_FILTERS)) {
+ // Disable multicast filter
+ ret = onmph_rreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, &ctrl0);
+ if (ret)
+ return ret;
+
+ ctrl0 &= ~ONMPH_MAC_CONTROL_MCSF_BIT;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, ctrl0);
+ if (ret)
+ return ret;
+
+ // accept all multicasts (any address with the LSB = 1 in the first byte)
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRMASKL(1), 0x00000000);
+ if (ret)
+ return ret;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRMASKH(1), 0x00000100);
+ if (ret)
+ return ret;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRFILTL(1), 0x00000000);
+ if (ret)
+ return ret;
+
+ val = ONMPH_MAC_ADDRFILT_EN_BIT | 0x00000100;
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRFILTH(1), val);
+
+ return ret;
+
+ } else if (netdev_mc_count(priv->ndev) == 0) {
+ int i;
+
+ // Enable multicast filter
+ ret = onmph_rreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, &ctrl0);
+ if (ret)
+ return ret;
+
+ ctrl0 |= ONMPH_MAC_CONTROL_MCSF_BIT;
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, ctrl0);
+ if (ret)
+ return ret;
+
+ // disable filters
+ for (i = 1; i <= 3; i++) {
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRFILTH(i), 0x00000000);
+ if (ret)
+ return ret;
+ }
+
+ return ret;
+ } else {
+ struct netdev_hw_addr *ha;
+ u32 addrh;
+ u32 addrl;
+ int i;
+
+ // Disable multicast filter
+ ret = onmph_rreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, &ctrl0);
+ if (ret)
+ return ret;
+
+ ctrl0 &= ~ONMPH_MAC_CONTROL_MCSF_BIT;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_CONTROL, ctrl0);
+ if (ret)
+ return ret;
+
+ // disable filters
+ for (i = 1; i <= 3; i++) {
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRFILTH(i), 0x00000000);
+ if (ret)
+ return ret;
+ }
+
+ i = 1;
+ netdev_for_each_mc_addr(ha, priv->ndev) {
+ if (i > 3)
+ break;
+
+ addrh = ((ha->addr[0] << 8) | ha->addr[1] |
+ ONMPH_MAC_ADDRFILT_EN_BIT);
+ addrl = ((ha->addr[2] << 24) | (ha->addr[3] << 16) |
+ (ha->addr[4] << 8) | (ha->addr[5]));
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRFILTH(i), addrh);
+ if (ret)
+ return ret;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRFILTL(i), addrl);
+ if (ret)
+ return ret;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRMASKL(i), 0xffffffff);
+ if (ret)
+ return ret;
+
+ ret = onmph_wreg(priv, ONMPH_MMS_MAC,
+ ONMPH_REG_MAC_ADDRMASKH(i), 0x0000ffff);
+ if (ret)
+ return ret;
+ i++;
+ }
+
+ return ret;
+ }
+}
+
+// deferred function for applying RX mode flags in non-atomic context
+static int onmph_rx_mode_update(struct onmph_info *priv)
+{
+ unsigned int rx_flags;
+ unsigned long flags;
+ int ret;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ // apply the configuration changes
+ rx_flags = priv->rx_flags;
+ priv->rx_flags_upd = false;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ ret = onmph_set_promiscuous_mode(priv, rx_flags);
+ if (ret)
+ goto out;
+
+ ret = onmph_set_multicast_mode(priv, rx_flags);
+ if (ret)
+ goto out;
+
+out:
+ return ret;
+}
+
+static struct sk_buff *onmph_pop_tx_frame(struct onmph_info *priv,
+ unsigned int *npkt)
+{
+ struct sk_buff *ret = NULL;
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ if (priv->tx_skb_q_sz) {
+ ret = list_first_entry(&priv->tx_skb_q, struct sk_buff, list);
+
+ __list_del_entry(&ret->list);
+ *npkt = --priv->tx_skb_q_sz;
+
+ if (netif_queue_stopped(priv->ndev) &&
+ priv->tx_skb_q_sz < ONMPH_MAX_PENDING_TX_FRAMES / 2)
+ netif_wake_queue(priv->ndev);
+ } else {
+ // queue is empty
+ *npkt = 0;
+ }
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return ret;
+}
+
+/* Breaks up parts of one or more skbs into chunks for transmission.
+ * This will create as many chunks as there is space available.
+ */
+static int onmph_prepare_tx_chunks(struct onmph_info *priv, bool *more)
+{
+ struct mph_dhdr_info pflags = { 0 };
+ struct sk_buff *skb = priv->tx_skb;
+ u32 *chdr = priv->tx_mem;
+ void *cdata = chdr + 1;
+ unsigned int nchk = 0;
+ unsigned int nwr = 0;
+ unsigned int npkt;
+ unsigned int n;
+
+ /* if we are congested, send a number of empty TX chunks for receiving,
+ * or just one in case no RX frames are available.
+ */
+ if (unlikely(!priv->tx_txc))
+ goto add_rx_chunks;
+
+ // load as many TX chunks as possible, up to the available credits
+ while (true) {
+ if (!skb) {
+ // get next SKB in the queue
+ skb = onmph_pop_tx_frame(priv, &npkt);
+
+ // no skb --> no party
+ if (!skb)
+ break;
+
+ pflags.swo = nwr >> 2;
+ pflags.sv = 1;
+ if (priv->tx_fcs_calc) {
+ int to_add;
+ u32 crc;
+
+ to_add = ETH_FCS_LEN +
+ (skb->len >= ETH_ZLEN ? 0 : ETH_ZLEN - skb->len);
+
+ if (skb_tailroom(skb) < to_add) {
+ struct sk_buff *new_skb;
+
+ new_skb = skb_copy_expand(skb, 0,
+ to_add - skb_tailroom(skb),
+ GFP_KERNEL);
+ if (!new_skb) {
+ // Couldn't grow the skb, drop it and
+ // continue
+ dev_err(priv->dev,
+ "Unable to grow skb\n");
+ consume_skb(skb);
+ priv->ndev->stats.rx_dropped++;
+ continue;
+ }
+
+ consume_skb(skb);
+ skb = new_skb;
+ }
+
+ if (skb->len < ETH_ZLEN)
+ skb_put_zero(skb, ETH_ZLEN - skb->len);
+
+ crc = ~ether_crc_le(skb->len, skb->data);
+ skb_put_data(skb, &crc, sizeof(crc));
+ }
+ } else {
+ /* we don't need locking here, we're just accessing a
+ * basic integer, and we don't care if we read the
+ * old or the new value
+ */
+ npkt = READ_ONCE(priv->tx_skb_q_sz);
+ }
+
+ // copy as much data as possible from the skb to the chunk
+ n = min(skb->len, priv->cps - nwr);
+ memcpy(cdata + nwr, skb->data, n);
+ priv->ndev->stats.tx_bytes += n;
+ nwr += n;
+
+ /* if there is no more data in the socket buffer, record the end
+ * of frame within the chunk and prepare for loading the next
+ * socket buffer
+ */
+ if (skb->len == n) {
+ consume_skb(skb);
+ skb = NULL;
+
+ pflags.ev = 1;
+ pflags.ebo = nwr - 1;
+ nwr = round_up(nwr, 4);
+ priv->ndev->stats.tx_packets++;
+
+ /* if there is still room in the current chunk, and
+ * there are more packets available, concatenate two
+ * frames within the same chunk. That, unless the
+ * current frame fits entirely in one chunk.
+ */
+ if (npkt && nwr < priv->cps && !pflags.sv)
+ continue;
+ } else {
+ // consume data from the beginning of the socket buffer
+ skb_pull(skb, n);
+ }
+
+ // write the header
+ *chdr = onmph_mkdatahdr_txrx(&pflags);
+ chdr = cdata + priv->cps;
+
+ /* stop if we exhausted the available number of chunks, or we
+ * don't have any further data to transmit
+ */
+ if (++nchk == priv->tx_txc || (!npkt && !skb))
+ break;
+
+ // move to the next chunk
+ cdata = chdr + 1;
+
+ // reset the protocol flags & variables
+ memset(&pflags, 0, sizeof(pflags));
+ nwr = 0;
+ }
+
+ // save the current socket buffer being transmitted for the next round
+ priv->tx_skb = skb;
+
+ /* if at the end of the cycle we still have frames pending, schedule
+ * another round immediately after this one
+ */
+ if (skb || npkt)
+ *more = true;
+
+add_rx_chunks:
+
+ /* add as many empty chunks up to the maximum buffer size and up to
+ * the receive chunks available to optimize RX
+ */
+ for (n = max(priv->rx_rca, 1U); nchk < n; nchk++) {
+ *chdr = onmph_mkdatahdr_rx();
+ chdr = (void *)chdr + priv->csz;
+ }
+
+ return nchk;
+}
+
+// Initiates SPI transfer for tx/rx chunks
+static int onmph_do_txrx(struct onmph_info *priv, int nchk)
+{
+ struct spi_transfer st = { 0 };
+
+ // the TX chunks have been prepared in priv->tx_mem
+ st.tx_buf = priv->tx_mem;
+ st.rx_buf = priv->rx_mem;
+ st.len = priv->csz * nchk;
+
+ return spi_sync_transfer(priv->spi, &st, 1);
+}
+
+// Final processing of receive SKB before sending up network stack
+static int onmph_deliver_rx_skb(struct onmph_info *priv, int err)
+{
+ const int rxall = priv->ndev->features & NETIF_F_RXALL;
+ struct sk_buff *skb = priv->rx_skb;
+ unsigned int len;
+
+ if (err && !((err == -EIO) && rxall))
+ goto out_drop;
+
+ if (priv->rx_fcs_check) {
+ u32 *rx_fcs;
+ u32 crc;
+
+ crc = ~ether_crc_le(skb->len - 4, skb->data);
+ rx_fcs = (u32 *)&skb->data[skb->len - 4];
+
+ if (unlikely(crc != *rx_fcs) && !rxall) {
+ err = NET_RX_DROP;
+ goto out_drop;
+ }
+ }
+
+ if (likely(!(priv->ndev->features & NETIF_F_RXFCS)))
+ skb_trim(skb, skb->len - 4);
+
+ // set protocol from EtherType field
+ skb->protocol = eth_type_trans(skb, priv->ndev);
+
+ len = skb->len;
+ err = netif_rx(skb);
+ if (err == NET_RX_SUCCESS) {
+ priv->ndev->stats.rx_bytes += len;
+
+ priv->ndev->stats.rx_packets++;
+ }
+
+out:
+ priv->rx_skb = NULL;
+ return err;
+
+out_drop:
+ kfree_skb_reason(skb, SKB_DROP_REASON_SKB_CSUM);
+ priv->ndev->stats.rx_dropped++;
+ goto out;
+}
+
+// process received chunks, assemble in skbs
+static int onmph_process_rx_chunks(struct onmph_info *priv, bool *more, int nchk)
+{
+ void *cdata = priv->rx_mem;
+ struct mph_dftr_info ftr;
+ unsigned int nrd;
+ int err = 0;
+ int n = 0;
+ u32 *cftr;
+
+ cftr = cdata + priv->cps;
+
+ while (true) {
+ err = onmph_parse_footer(*cftr, &ftr);
+
+ if (unlikely(err)) {
+ netdev_warn(priv->ndev, "footer parity error!\n");
+ priv->ndev->stats.rx_fifo_errors++;
+ // frame drop: skip everything until new SV without FD
+ priv->rx_skip = true;
+ if (priv->rx_skb)
+ onmph_deliver_rx_skb(priv, -EIO);
+
+ goto next_chunk;
+ }
+
+ if (unlikely(!ftr.sync)) {
+ // the macphy restarted unexpectedly, we need to
+ // stop the interface and let userspace handle the
+ // problem.
+ netdev_err(priv->ndev, "spurious reset detected!\n");
+
+ if (priv->rx_skb)
+ onmph_deliver_rx_skb(priv, -EIO);
+ return -EIO;
+ }
+
+ if (unlikely(ftr.hdrb)) {
+ priv->ndev->stats.rx_fifo_errors++;
+ netdev_warn(priv->ndev, "header bad detected!\n");
+ }
+
+ // skip empty chunks
+ if (!ftr.dv)
+ goto next_chunk;
+
+ if (ftr.fd) {
+ // frame drop: skip everything until new SV without FD
+ if (priv->rx_skb)
+ onmph_deliver_rx_skb(priv, -EIO);
+
+ priv->rx_skip = true;
+ goto next_chunk;
+ }
+
+ // if dropping the frame, skip chunks until a new SV
+ if (priv->rx_skip) {
+ if (!ftr.sv) {
+ priv->rx_skip = true;
+ goto next_chunk;
+ }
+
+ priv->rx_skip = false;
+ }
+
+ // handle data
+ if (priv->rx_skb) {
+ /* this chunk shall contain the continuation or the end
+ * of a packet
+ */
+ if (ftr.sv && (!ftr.ev || ftr.ebo >= ftr.sbo)) {
+ netdev_warn(priv->ndev,
+ "SPI protocol error (repeated SV)");
+
+ priv->rx_skip = true;
+
+ // discard frame
+ onmph_deliver_rx_skb(priv, -EPROTO);
+ goto next_chunk;
+ }
+
+ // calculate the amount of valid data in the chunk
+ nrd = ftr.ev ? ftr.ebo + 1 : priv->cps;
+
+ // check that there is enough room left in the buffer
+ if (unlikely(priv->rx_skb->len + nrd > priv->mtu)) {
+ netdev_err(priv->ndev,
+ "received frame exceeds the MTU!");
+
+ priv->rx_skip = true;
+
+ // discard frame
+ onmph_deliver_rx_skb(priv, -EPROTO);
+ goto next_chunk;
+ }
+
+ // copy data to the socket buffer
+ skb_put_data(priv->rx_skb, cdata, nrd);
+
+ if (!ftr.ev) {
+ // intermediate chunk --> keep going
+ goto next_chunk;
+ }
+
+ // the socket buffer is ready to be delivered
+ onmph_deliver_rx_skb(priv, err);
+
+ if (!ftr.sv) {
+ // end of frame w/o concatenation
+ goto next_chunk;
+ }
+
+ // handle concatenation
+ ftr.ev = 0;
+ }
+
+ // this chunk shall contain the start of a packet
+ if (!ftr.sv) {
+ netdev_warn(priv->ndev,
+ "SPI protocol error (missing SV)");
+
+ priv->rx_skip = true;
+ goto next_chunk;
+ }
+
+ priv->rx_skb = netdev_alloc_skb_ip_align(priv->ndev, priv->mtu);
+ if (unlikely(!priv->rx_skb)) {
+ netdev_err(priv->ndev, "cannot allocate RX skb!\n");
+ // No memory available, skip the chunk
+ break;
+ }
+
+ // adjust the offset of the data within the chunk
+ cdata += ftr.sbo;
+
+ // calculate the number of valid bytes within the chunk
+ nrd = priv->cps - ftr.sbo;
+
+ // copy data to the socket
+ skb_put_data(priv->rx_skb, cdata, nrd);
+
+ if (ftr.ev)
+ onmph_deliver_rx_skb(priv, err);
+
+next_chunk:
+ // stop if we processed all the chunks
+ if (++n == nchk)
+ break;
+
+ // move to next footer
+ cdata = cftr + 1;
+ cftr = cdata + priv->cps;
+ }
+
+ // use the last footer to retrieve status information
+ if (ftr.exst)
+ priv->event_pending = true;
+
+ priv->tx_txc = ftr.txc;
+ priv->rx_rca = ftr.rca;
+
+ if (ftr.rca > 0)
+ *more = true;
+
+ return 0;
+}
+
+// Check for and handle any PHY events
+static int onmph_process_events(struct onmph_info *priv)
+{
+ unsigned long flags;
+ u32 val;
+ int ret;
+
+ if (!priv->event_pending)
+ return 0;
+
+ priv->event_pending = false;
+
+ ret = onmph_rreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_ST0, &val);
+ if (ret) {
+ dev_err(priv->dev, "Error reading ST0 register: %d\n", ret);
+ return ret;
+ }
+
+ if ((val & ONMPH_SPI_ST0_PHYINT_BIT) && priv->phy_virq != PHY_POLL) {
+ spin_lock_irqsave(&priv->lock, flags);
+ if (priv->phy_irq_en) {
+ spin_unlock_irqrestore(&priv->lock, flags);
+ generic_handle_irq_safe(priv->phy_virq);
+ } else {
+ priv->phy_irq_pending = true;
+ spin_unlock_irqrestore(&priv->lock, flags);
+ }
+ }
+
+ // acknowledge all handled events
+ ret = onmph_wreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_ST0, val);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+// kernel thread for MACPHY protocol handling
+static int onmph_thread_fun(void *data)
+{
+ struct onmph_info *priv = data;
+ signed long tout = priv->poll_jiff;
+ bool update_rx_mode = false;
+ struct net_device *ndev;
+ unsigned long flags;
+ bool more;
+ int ret;
+
+ ndev = priv->ndev;
+ // IRQ was created disabled to allow the task to start first
+ enable_irq(ndev->irq);
+
+ do {
+ if (update_rx_mode) {
+ // check RX mode flags
+ ret = onmph_rx_mode_update(priv);
+ if (unlikely(ret)) {
+ netdev_err(ndev, "failed to set new RX mode\n");
+ break;
+ }
+ }
+
+ if (tout == 0) {
+ // reload polling timer
+ tout = priv->poll_jiff;
+
+ // force checking the status register
+ priv->event_pending = true;
+ }
+
+ do {
+ int chunks;
+
+ more = false;
+
+ // process pending TX frames, if any
+ chunks = onmph_prepare_tx_chunks(priv, &more);
+
+ // process "ret" chunks
+ if (onmph_do_txrx(priv, chunks) != 0) {
+ // if we get here, there was an SPI error
+ // and we can't recover here
+ ret = -EIO;
+ break;
+ }
+
+ // analyze received footer(s) and handle RX
+ ret = onmph_process_rx_chunks(priv, &more, chunks);
+ if (ret)
+ break;
+
+ // process any interrupt / event from the device
+ onmph_process_events(priv);
+ } while (more);
+
+ // Unrecoverable error - disable irq, exit thread
+ if (unlikely(ret))
+ goto err_unrecoverable;
+
+ // handle new IRQs or sleep
+
+ spin_lock_irqsave(&priv->lock, flags);
+ __set_current_state(TASK_INTERRUPTIBLE);
+
+ update_rx_mode = priv->rx_flags_upd;
+ ret = priv->irq_pending || priv->tx_skb_q_sz || update_rx_mode;
+
+ priv->irq_pending = false;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ if (!ret)
+ tout = schedule_timeout(tout);
+ else
+ set_current_state(TASK_RUNNING);
+ } while (!kthread_should_stop());
+
+ return 0;
+
+err_unrecoverable:
+ disable_irq(ndev->irq);
+ return ret;
+}
+
+// MACPHY ISR - wakeup the handler thread
+static irqreturn_t onmph_interrupt(int irq, void *dev)
+{
+ struct onmph_info *priv = netdev_priv(dev);
+
+ priv->irq_pending = true;
+
+ // wakeup the kthread
+ return wake_up_process(priv->thread) ? IRQ_HANDLED : IRQ_NONE;
+}
+
+// phylink mac ops ---------------------------------------------------------- //
+// The phylink callbacks are required, but we don't need to do anything in them
+static void onmph_phylink_mac_link_up(struct phylink_config *config,
+ struct phy_device *phy, unsigned int mode,
+ phy_interface_t interface, int speed,
+ int duplex, bool tx_pause, bool rx_pause)
+{
+}
+
+static void onmph_phylink_mac_link_down(struct phylink_config *config,
+ unsigned int mode,
+ phy_interface_t interface)
+{
+}
+
+static void onmph_phylink_mac_config(struct phylink_config *config,
+ unsigned int mode,
+ const struct phylink_link_state *state)
+{
+}
+
+static const struct phylink_mac_ops onmph_phylink_mac_ops = {
+ .validate = phylink_generic_validate,
+ .mac_config = onmph_phylink_mac_config,
+ .mac_link_down = onmph_phylink_mac_link_down,
+ .mac_link_up = onmph_phylink_mac_link_up,
+};
+
+// netdev ops --------------------------------------------------------------- //
+
+static int onmph_ioctl(struct net_device *ndev, struct ifreq *rq, int cmd)
+{
+ struct onmph_info *priv = netdev_priv(ndev);
+ int ret = -EOPNOTSUPP;
+
+ if (!netif_running(ndev))
+ return -EINVAL;
+
+ switch (cmd) {
+ case SIOCGMIIPHY:
+ case SIOCGMIIREG:
+ case SIOCSMIIREG:
+ ret = phylink_mii_ioctl(priv->phylink, rq, cmd);
+ break;
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+// Updates address list filtering
+static void onmph_set_rx_mode(struct net_device *ndev)
+{
+ struct onmph_info *priv = netdev_priv(ndev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ priv->rx_flags_upd = true;
+ priv->rx_flags = ndev->flags;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ wake_up_process(priv->thread);
+}
+
+// Sets MAC address
+static int onmph_set_mac_address(struct net_device *ndev, void *p)
+{
+ struct sockaddr *addr = p;
+
+ if (!is_valid_ether_addr(addr->sa_data))
+ return -EADDRNOTAVAIL;
+
+ eth_hw_addr_set(ndev, addr->sa_data);
+ return onmph_set_mac_filter(ndev, addr->sa_data);
+}
+
+// Add TX skb to queue of outgoing SKBs and wake up handler thread
+static netdev_tx_t onmph_start_xmit(struct sk_buff *skb, struct net_device *ndev)
+{
+ struct onmph_info *priv = netdev_priv(ndev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ // add the socket to the coaleshing list
+ list_add_tail(&skb->list, &priv->tx_skb_q);
+
+ // apply TX flow control
+ if (++priv->tx_skb_q_sz >= ONMPH_MAX_PENDING_TX_FRAMES) {
+ // suspend TX queue until we flushed out some frames
+ netif_stop_queue(ndev);
+ }
+
+ // wakeup the kthread
+ wake_up_process(priv->thread);
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return NETDEV_TX_OK;
+}
+
+#ifdef CONFIG_NET_POLL_CONTROLLER
+
+static void onmph_poll_controller(struct net_device *ndev)
+{
+ disable_irq(ndev->irq);
+ onmph_interrupt(ndev->irq, ndev);
+ enable_irq(ndev->irq);
+}
+
+#endif
+
+// Brings up network interface
+static int onmph_open(struct net_device *ndev)
+{
+ struct onmph_info *priv = netdev_priv(ndev);
+ int ret;
+
+ // request the IRQ
+ priv->irq_pending = false;
+ if (request_irq(ndev->irq, &onmph_interrupt, IRQF_NO_AUTOEN, ndev->name, ndev)) {
+ ret = -EAGAIN;
+ goto out;
+ }
+
+ ret = onmph_init(priv);
+ if (unlikely(ret)) {
+ netdev_err(ndev, "failed to initialize the device");
+ goto err_phy_connect;
+ }
+
+ // connect to the PHY
+ ret = phylink_fwnode_phy_connect(priv->phylink, priv->dev->fwnode, 0);
+
+ if (ret) {
+ netdev_err(priv->ndev,
+ "could not connect to the PHY, err = %d\n", ret);
+
+ ret = -ENODEV;
+ goto err_phy_connect;
+ }
+
+ // start the PHY
+ phylink_start(priv->phylink);
+
+ // create and start the main thread
+ priv->thread = kthread_run(onmph_thread_fun, priv, DRV_NAME "/%s:%d",
+ dev_name(&priv->spi->dev),
+ priv->spi->chip_select);
+
+ if (IS_ERR(priv->thread)) {
+ ret = PTR_ERR(priv->thread);
+ goto err_create_thread;
+ }
+
+ return 0;
+
+err_create_thread:
+ phylink_disconnect_phy(priv->phylink);
+
+err_phy_connect:
+ free_irq(ndev->irq, ndev);
+
+out:
+ return ret;
+}
+
+// Brings down network interface
+static int onmph_stop(struct net_device *ndev)
+{
+ struct onmph_info *priv = netdev_priv(ndev);
+
+ // stop the MAC
+ onmph_shutdown(priv);
+
+ // stop the thread and wait
+ kthread_stop(priv->thread);
+ priv->thread = NULL;
+
+ // stop the PHY
+ phylink_stop(priv->phylink);
+
+ // disconnect from the PHY
+ phylink_disconnect_phy(priv->phylink);
+
+ // release IRQs
+ free_irq(ndev->irq, ndev);
+
+ // free the rx skb
+ if (priv->rx_skb)
+ kfree_skb(priv->rx_skb);
+
+ return 0;
+}
+
+// mask the interrupt source
+static void onmph_irq_mask(struct irq_data *d)
+{
+ struct onmph_info *priv = irq_data_get_irq_chip_data(d);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ priv->phy_irq_en = false;
+ spin_unlock_irqrestore(&priv->lock, flags);
+}
+
+// unmask the interrupt source
+static void onmph_irq_unmask(struct irq_data *d)
+{
+ struct onmph_info *priv = irq_data_get_irq_chip_data(d);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ priv->phy_irq_en = true;
+
+ if (priv->phy_irq_pending) {
+ priv->phy_irq_pending = false;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ generic_handle_irq_safe(priv->phy_virq);
+ } else {
+ spin_unlock_irqrestore(&priv->lock, flags);
+ }
+}
+
+// Create mapping between virtual and hardware irq numbers
+static int onmph_irqdomain_map(struct irq_domain *d, unsigned int irq,
+ irq_hw_number_t hwirq)
+{
+ struct onmph_info *priv = d->host_data;
+
+ irq_set_chip_and_handler(irq, &priv->irq_chip, handle_simple_irq);
+ irq_set_chip_data(irq, priv);
+ irq_set_noprobe(irq);
+
+ return 0;
+}
+
+// Remove mapping between virtual and hardware irq numbers
+static void onmph_irqdomain_unmap(struct irq_domain *d, unsigned int irq)
+{
+ irq_set_chip_and_handler(irq, NULL, NULL);
+ irq_set_chip_data(irq, NULL);
+}
+
+// callbacks for irq_domain objects
+static const struct irq_domain_ops onmphirqd_ops = {
+ .map = onmph_irqdomain_map,
+ .unmap = onmph_irqdomain_unmap,
+ .xlate = irq_domain_xlate_onecell,
+};
+
+static const struct net_device_ops onmph_netdev_ops = {
+ .ndo_open = onmph_open,
+ .ndo_stop = onmph_stop,
+ .ndo_start_xmit = onmph_start_xmit,
+ .ndo_set_mac_address = onmph_set_mac_address,
+ .ndo_eth_ioctl = onmph_ioctl,
+ .ndo_set_rx_mode = onmph_set_rx_mode,
+#ifdef CONFIG_NET_POLL_CONTROLLER
+ .ndo_poll_controller = onmph_poll_controller,
+#endif
+};
+
+// convert the PHY's MMDs into MACPHY's MMSs
+static int mmd2mms(int mmd)
+{
+ int ret;
+
+ switch (mmd) {
+ case MDIO_MMD_PMAPMD:
+ ret = ONMPH_MMS_PMA;
+ break;
+
+ case MDIO_MMD_PCS:
+ ret = ONMPH_MMS_PCS;
+ break;
+
+ case MDIO_MMD_VEND1:
+ ret = ONMPH_MMS_VS;
+ break;
+
+ case MDIO_MMD_VEND2:
+ ret = ONMPH_MMS_PLCA_VEND2;
+ break;
+
+ default:
+ ret = -ENXIO;
+ break;
+ }
+
+ return ret;
+}
+
+// Read MDIO clause 45 register
+static int onmph_mdio_read_c45(struct mii_bus *bus, int addr, int devnum,
+ int regnum)
+{
+ struct onmph_info *priv = bus->priv;
+ int mms;
+ u32 val;
+ int ret;
+
+ // we support only PHY #0 at this time
+ if (addr != 0)
+ return -ENODEV;
+
+ if (regnum < 0 || regnum > 0xffff)
+ return -ENXIO;
+
+ mms = mmd2mms(devnum);
+ if (mms < 0)
+ return mms;
+
+ ret = onmph_rreg(priv, mms, regnum, &val);
+ return ret < 0 ? ret : (int)val;
+}
+
+// Read clause MDIO register
+static int onmph_mdio_read(struct mii_bus *bus, int addr, int regnum)
+{
+ struct onmph_info *priv = bus->priv;
+ int mmd;
+ int mms;
+ int cmd;
+ u32 val;
+ int ret;
+
+ // we support only PHY #0 at this time
+ if (addr != 0)
+ return -ENODEV;
+
+ if (regnum < 0 || regnum > 31)
+ return -ENXIO;
+
+ if (regnum == MII_MMD_CTRL) {
+ // emulate c22 to c45 control access
+ return priv->mii_c45_ctrl;
+ }
+
+ if (regnum == MII_MMD_DATA) {
+ // emulate c22 to c45 data access
+ cmd = priv->mii_c45_ctrl & ~MII_MMD_CTRL_DEVAD_MASK;
+ mmd = priv->mii_c45_ctrl & MII_MMD_CTRL_DEVAD_MASK;
+
+ if (cmd == MII_MMD_CTRL_ADDR) {
+ // just return the indirect address for this mmd
+ return priv->mii_c45_addr[mmd];
+ }
+
+ // do read clause 45 register
+ ret = onmph_mdio_read_c45(bus, addr, mmd,
+ priv->mii_c45_addr[mmd]);
+ if (ret < 0)
+ return ret;
+
+ switch (cmd) {
+ case MII_MMD_CTRL_INCR_ON_WT:
+ fallthrough;
+ case MII_MMD_CTRL_NOINCR:
+ // no increment --> done!
+ break;
+
+ case MII_MMD_CTRL_INCR_RDWT:
+ // increment address
+ priv->mii_c45_addr[mmd]++;
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ return ret;
+ }
+
+ if (priv->version == 0x01) {
+ // WA: due to an HW bug the PHYID does get updated
+ // when there is a new OUI assigned to onsemi
+ // Therefore, we force overwriting the PHYID on read
+ // to the 60:C0:BF OUI
+ if (regnum == MII_PHYSID1) {
+ // overwrite PHYID reg 2
+ return 0x180f;
+ } else if (regnum == MII_PHYSID2) {
+ // overrite PHYID reg 3
+ return 0xf5a1;
+ }
+ }
+
+ if (regnum < 16) {
+ // standard register, map to MMS SPI w/ offset
+ mms = ONMPH_MMS_SPI_MII;
+ regnum |= 0xff00;
+ } else {
+ // vendor specific register, map to specific MMS
+ mms = ONMPH_MMS_VS;
+ }
+
+ ret = onmph_rreg(priv, mms, regnum, &val);
+ return ret ? ret : (int)val;
+}
+
+// Write MDIO clause 45 register
+static int onmph_mdio_write_c45(struct mii_bus *bus, int addr, int devnum,
+ int regnum, u16 val)
+{
+ struct onmph_info *priv = bus->priv;
+ int mms;
+
+ // we support only PHY #0 at this time
+ if (addr != 0)
+ return -ENODEV;
+
+ if (regnum < 0 || regnum > 0xffff)
+ return -ENXIO;
+
+ mms = mmd2mms(devnum);
+ if (mms < 0)
+ return mms;
+
+ return onmph_wreg(priv, mms, regnum, val);
+}
+
+// Write MDIO register
+static int onmph_mdio_write(struct mii_bus *bus, int addr, int regnum, u16 val)
+{
+ struct onmph_info *priv = bus->priv;
+ int mmd;
+ int mms;
+ int cmd;
+ int ret;
+
+ // we support only PHY #0 at this time
+ if (addr != 0)
+ return -ENODEV;
+
+ if (regnum < 0 || regnum > 31)
+ return -ENXIO;
+
+ if (regnum == MII_MMD_CTRL) {
+ // emulate c22 to c45 control access
+ priv->mii_c45_ctrl = val;
+ return 0;
+ }
+
+ if (regnum == MII_MMD_DATA) {
+ // emulate c22 to c45 data access
+ cmd = priv->mii_c45_ctrl & ~MII_MMD_CTRL_DEVAD_MASK;
+ mmd = priv->mii_c45_ctrl & MII_MMD_CTRL_DEVAD_MASK;
+
+ if (cmd == MII_MMD_CTRL_ADDR) {
+ // just record the indirect address for this mmd
+ priv->mii_c45_addr[mmd] = val;
+ return 0;
+ }
+
+ // do write clause 45 register
+ ret = onmph_mdio_write_c45(bus, addr, mmd,
+ priv->mii_c45_addr[mmd], val);
+ if (ret < 0)
+ return ret;
+
+ switch (cmd) {
+ case MII_MMD_CTRL_NOINCR:
+ // no increment --> done!
+ break;
+
+ case MII_MMD_CTRL_INCR_RDWT:
+ fallthrough;
+ case MII_MMD_CTRL_INCR_ON_WT:
+ // increment address
+ priv->mii_c45_addr[mmd]++;
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+ }
+
+ // WA: prevent the PHY from being reset from the control register. The
+ // NCN26010 triggers a global soft-reset when resetting the PHY.
+ if (regnum == MII_BMCR && (val & BMCR_RESET))
+ val &= ~BMCR_RESET;
+
+ if (regnum < 16) {
+ // standard register, map to MMS SPI w/ offset
+ mms = ONMPH_MMS_SPI_MII;
+ regnum |= 0xff00;
+ } else {
+ // vendor specific register, map to specific MMS
+ mms = ONMPH_MMS_VS;
+ }
+
+ return onmph_wreg(priv, mms, regnum, val);
+}
+
+// Create the pseudo MDIO bus for accessing PHY regs
+static int onmph_mdiobus_create(struct onmph_info *priv)
+{
+ struct mii_bus *mdio_bus;
+ int ret;
+
+ mdio_bus = devm_mdiobus_alloc(priv->dev);
+ if (!mdio_bus) {
+ ret = -ENOMEM;
+ goto err_alloc;
+ }
+
+ snprintf(priv->mdio_bus_name, MII_BUS_ID_SIZE, "%s:%s:%u", "NCN26010",
+ dev_name(&priv->spi->dev), priv->spi->chip_select);
+
+ mdio_bus->priv = priv;
+ mdio_bus->parent = priv->dev;
+
+ // These two are required for phylink to work properly.
+ device_set_of_node_from_dev(&mdio_bus->dev, priv->dev);
+ device_set_node(&mdio_bus->dev, priv->dev->fwnode);
+
+ mdio_bus->name = priv->mdio_bus_name;
+ mdio_bus->read = onmph_mdio_read;
+ mdio_bus->write = onmph_mdio_write;
+ mdio_bus->read_c45 = onmph_mdio_read_c45;
+ mdio_bus->write_c45 = onmph_mdio_write_c45;
+
+ mdio_bus->phy_mask = ~((u32)GENMASK(0, 0));
+
+ snprintf(mdio_bus->id, MII_BUS_ID_SIZE, "%s", dev_name(priv->dev));
+
+ ret = mdiobus_register(mdio_bus);
+ if (ret)
+ goto err_register;
+
+ priv->mdio_bus = mdio_bus;
+ return 0;
+
+err_register:
+ mdiobus_free(mdio_bus);
+
+err_alloc:
+ return ret;
+}
+
+// Create phy interface for accessing phy part of MACPHY
+static int onmph_phy_create(struct onmph_info *priv)
+{
+ struct fwnode_handle *mac_fwn = priv->dev->fwnode;
+ struct net_device *ndev = priv->ndev;
+ struct fwnode_handle *phy_fwn;
+ phy_interface_t phy_mode;
+
+ phy_mode = fwnode_get_phy_mode(mac_fwn);
+ if (phy_mode < 0) {
+ netdev_err(ndev, "incorrect phy-mode\n");
+ return phy_mode;
+ }
+
+ phy_fwn = fwnode_get_phy_node(mac_fwn);
+ if (IS_ERR(phy_fwn)) {
+ netdev_err(ndev, "cannot find PHY device node\n");
+ return PTR_ERR(phy_fwn);
+ }
+
+ priv->phy = fwnode_phy_find_device(phy_fwn);
+ if (IS_ERR(priv->phy)) {
+ netdev_err(ndev, "cannot get PHY from device node\n");
+ return PTR_ERR(priv->phy);
+ }
+
+ priv->phy->irq = priv->phy_virq;
+
+ priv->phylink_config.dev = &ndev->dev;
+ priv->phylink_config.type = PHYLINK_NETDEV;
+
+ priv->phylink_config.mac_capabilities = MAC_10;
+
+ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+ priv->phylink_config.supported_interfaces);
+
+ priv->phylink = phylink_create(&priv->phylink_config, mac_fwn, phy_mode,
+ &onmph_phylink_mac_ops);
+
+ if (IS_ERR(priv->phylink))
+ return PTR_ERR(priv->phylink);
+
+ return 0;
+}
+
+// Bind driver to SPI device, begin initialization
+static int onmph_probe(struct spi_device *spi)
+{
+ const struct spi_device_id *devid = spi_get_device_id(spi);
+ struct device *dev = &spi->dev;
+ struct fwnode_handle *fwn;
+ struct net_device *ndev;
+ struct onmph_info *priv;
+ u32 val;
+ u32 model;
+ int ret;
+
+ fwn = dev->fwnode;
+
+ // allocate net device and related private data
+ ndev = devm_alloc_etherdev(dev, sizeof(struct onmph_info));
+ if (!ndev) {
+ ret = -ENOMEM;
+ goto err_netdev_alloc;
+ }
+
+ priv = netdev_priv(ndev);
+ SET_NETDEV_DEV(ndev, dev);
+
+ // initialize private data
+ spin_lock_init(&priv->lock);
+ INIT_LIST_HEAD(&priv->tx_skb_q);
+
+ priv->dev = dev;
+ priv->spi = spi;
+ priv->ndev = ndev;
+
+ priv->cps = MPH_MAX_CPS;
+ priv->csz = priv->cps + 4; // + 4 for header or footer
+
+ priv->mtu = ONMPH_MAX_FRAME_SIZE;
+
+ priv->irq_chip.name = dev->driver->name;
+ priv->irq_chip.irq_mask = onmph_irq_mask;
+ priv->irq_chip.irq_unmask = onmph_irq_unmask;
+
+ priv->tx_mem = devm_kmalloc(dev, MPH_MAX_CRED * priv->csz, GFP_KERNEL);
+ priv->rx_mem = devm_kmalloc(dev, MPH_MAX_CRED * priv->csz, GFP_KERNEL);
+
+ if (!priv->tx_mem || !priv->rx_mem) {
+ ret = -ENOMEM;
+ goto err_netdev_alloc;
+ }
+
+ // initialize SPI parameters
+ spi->dev.platform_data = priv;
+
+ // configure SPI for OATC14 MACPHY protocol
+ spi->mode = SPI_MODE_0; // CPOL=0, CPHA=0, MSB first
+ spi->bits_per_word = 8;
+
+ // initialize netdev
+ ndev->netdev_ops = &onmph_netdev_ops;
+ ndev->ethtool_ops = &onmph_ethtool_ops;
+ ndev->priv_flags |= IFF_UNICAST_FLT;
+
+ // set supported changeable features
+ // NETIF_F_RXFCS --> append FCS to skb (default off)
+ // NETIF_F_RXALL --> receive errored frames (default off)
+ ndev->hw_features = NETIF_F_RXFCS | NETIF_F_RXALL;
+
+ // get model & version
+ priv->model = devid->driver_data;
+
+ /* Start with non-protected control accesses for single register reads.
+ * NOTE: the device starts in this mode after reset, but it is possible
+ * that the PROTE bit was set by a previous module load/unload.
+ * In this case, non-protected register writes won't work, but -single-
+ * unprotected register reads will. Therefore, we can safely probe the
+ * device using regular control accesses and switch to protected mode
+ * later, when resetting the device.
+ */
+ ret = onmph_rreg(priv, ONMPH_MMS_VS, ONMPH_REG_VS_CHIPID, &val);
+ if (ret)
+ goto err_model;
+
+ model = (val & ~ONMPH_CHIPID_MODEL_MASK) >> ONMPH_CHIPID_MODEL_SHIFT;
+ if (priv->model != model) {
+ dev_err(priv->dev, "unrecognized macphy model 0x%08x\n", model);
+
+ ret = -ENODEV;
+ goto err_model;
+ }
+
+ priv->version = (val & ONMPH_CHIPID_REVISION_MASK) >> ONMPH_CHIPID_REVISION_SHIFT;
+
+ ret = onmph_rreg(priv, ONMPH_MMS_SPI_MII, ONMPH_REG_SPI_CFG0, &val);
+ if (ret) {
+ dev_err(priv->dev, "failed to read CFG0 register\n");
+ goto err_model;
+ }
+
+ // If the device is already configured for protected register accesses,
+ // switch to protected mode now, otherwise we would not even be able to
+ // reset it.
+ if (val & ONMPH_SPI_CFG0_PROTE_BIT)
+ priv->ctrl_protected = true;
+
+ if (val & ONMPH_SPI_CFG0_SYNC_BIT) {
+ // The device is already configured --> reset it now
+ ret = onmph_reset(priv);
+ if (ret) {
+ dev_err(priv->dev, "device reset failed\n");
+ goto err_reset;
+ }
+ }
+
+ if (device_property_read_u32(priv->dev, "poll-interval", &val))
+ val = 5;
+
+ priv->poll_jiff = HZ * val;
+
+ // model-specific initializations/fixups
+ switch (priv->model) {
+ case ONMPH_MODEL_NCN26010:
+ priv->capabilities = ONMPH_CAP_MACADDR;
+
+ if (priv->version == 0x01) {
+ // WA: due to an HW bug the PHY irq may not be fired.
+ // Therefore, we force polling for this device.
+ priv->phy_virq = PHY_POLL;
+ }
+ break;
+ }
+
+ priv->tx_fcs_calc = device_property_read_bool(priv->dev,
+ "tx-fcs-calc");
+ if (priv->tx_fcs_calc)
+ dev_info(priv->dev, "TX frame SW FCS calculation enabled\n");
+
+ priv->rx_fcs_check =
+ device_property_read_bool(priv->dev, "rx-fcs-check");
+ if (priv->rx_fcs_check)
+ dev_info(priv->dev, "RX frame SW FCS check enabled\n");
+
+ // get the MAC address from DT
+ ret = device_get_ethdev_address(dev, ndev);
+
+ // if failed, try reading from NVM
+ if (ret && (priv->capabilities & ONMPH_CAP_MACADDR))
+ ret = onmph_read_mac_from_nvmem(priv);
+
+ // use a random MAC as a last resort
+ if (ret) {
+ eth_hw_addr_random(ndev);
+ dev_warn(dev, "using random MAC address %pM\n", ndev->dev_addr);
+ }
+
+ dev_info(priv->dev, "onsemi macphy model %s, version %u\n",
+ onmph_id_to_name(priv->model), priv->version);
+
+ // netdev structure initialization
+ ndev->irq = fwnode_irq_get(fwn, 0);
+ if (ndev->irq == -ENXIO) {
+ dev_err(priv->dev, "failed to map onmph IRQ\n");
+ ret = ndev->irq;
+
+ goto err_irq;
+ }
+
+ // initialize PHY IRQ emulation
+ if (priv->phy_virq != PHY_POLL) {
+ priv->irqdm =
+ irq_domain_create_linear(fwn, 1, &onmphirqd_ops, priv);
+ if (IS_ERR(priv->irqdm))
+ goto err_irqdomain;
+
+ priv->phy_virq = irq_create_mapping(priv->irqdm, 0);
+ }
+
+ // initialize the virtual MDIO bus
+ ret = onmph_mdiobus_create(priv);
+ if (ret)
+ goto err_mdiobus;
+
+ // configures priv->phylink
+ ret = onmph_phy_create(priv);
+ if (ret)
+ goto err_no_phy;
+
+ ret = register_netdev(ndev);
+ if (ret) {
+ dev_err(dev, "failed to register the ONMPH device\n");
+ ret = -ENODEV;
+
+ goto err_netdev_register;
+ }
+
+ // success
+ return 0;
+
+err_netdev_register:
+ phylink_destroy(priv->phylink);
+
+err_no_phy:
+err_mdiobus:
+ if (priv->phy_virq != PHY_POLL) {
+ irq_dispose_mapping(priv->phy_virq);
+ irq_domain_remove(priv->irqdm);
+ }
+
+err_irqdomain:
+ irq_dispose_mapping(ndev->irq);
+
+err_irq:
+err_reset:
+err_model:
+err_netdev_alloc:
+ dev_err(priv->dev, "could not initialize macphy, err = %d\n", ret);
+ return ret;
+}
+
+static void onmph_remove(struct spi_device *spi)
+{
+ struct onmph_info *priv = spi->dev.platform_data;
+ struct net_device *ndev = priv->ndev;
+
+ unregister_netdev(ndev);
+ phylink_destroy(priv->phylink);
+
+ mdiobus_unregister(priv->mdio_bus);
+ of_node_put(priv->mdio_bus->dev.of_node);
+
+ if (priv->phy_virq != PHY_POLL) {
+ irq_dispose_mapping(priv->phy_virq);
+ irq_domain_remove(priv->irqdm);
+ }
+
+ irq_dispose_mapping(ndev->irq);
+}
+
+static const struct of_device_id onmph_of_match[] = {
+ { .compatible = "onnn,ncn26010" },
+ {}
+};
+
+static const struct spi_device_id onmph_ids[] = {
+ { "ncn26010", ONMPH_MODEL_NCN26010 },
+ {}
+};
+
+MODULE_DEVICE_TABLE(spi, onmph_ids);
+
+static struct spi_driver onsemi_onmph_driver = {
+ .driver = {
+ .name = DRV_NAME,
+ .of_match_table = onmph_of_match,
+ },
+ .probe = onmph_probe,
+ .remove = onmph_remove,
+ .id_table = onmph_ids,
+};
+
+module_spi_driver(onsemi_onmph_driver);
+
+MODULE_AUTHOR("Piergiorgio Beruto <piergiorgio.beruto@...il.com>");
+MODULE_AUTHOR("Jay Monkman <jtm@...ingdog.com>");
+MODULE_DESCRIPTION("onsemi MACPHY ethernet driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/ethernet/onsemi/onsemi_macphy_ethtool.c b/drivers/net/ethernet/onsemi/onsemi_macphy_ethtool.c
new file mode 100644
index 000000000000..131b5112db38
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/onsemi_macphy_ethtool.c
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR BSD-2-Clause
+/*
+ * onsemi MACPHY ethernet driver
+ * Copyright (c) 2022 onsemi
+ */
+
+#include "onsemi_macphy.h"
+#include <linux/ethtool.h>
+
+// constants ---------------------------------------------------------------- //
+
+static const char onmph_stat_strings[][ETH_GSTRING_LEN] = {
+ "tx-bytes-ok",
+ "tx-frames",
+ "tx-broadcast-frames",
+ "tx-multicast-frames",
+ "tx-64-frames",
+ "tx-65-127-frames",
+ "tx-128-255-frames",
+ "tx-256-511-frames",
+ "tx-512-1023-frames",
+ "tx-1024-1518-frames",
+ "tx-underrun-errors",
+ "tx-single-collision",
+ "tx-multiple-collision",
+ "tx-excessive-collision",
+ "tx-deferred-frames",
+ "tx-carrier-sense-errors",
+ "rx-bytes-ok",
+ "rx-frames",
+ "rx-broadcast-frames",
+ "rx-multicast-frames",
+ "rx-64-frames",
+ "rx-65-127-frames",
+ "rx-128-255-frames",
+ "rx-256-511-frames",
+ "rx-512-1023-frames",
+ "rx-1024-1518-frames",
+ "rx-runt",
+ "rx-too-long-frames",
+ "rx-crc-errors",
+ "rx-symbol-errors",
+ "rx-alignment-errors",
+ "rx-busy-drop-frames",
+ "rx-mismatch-drop-frames",
+};
+
+#define ONMPH_STATS_LEN ARRAY_SIZE(onmph_stat_strings)
+static_assert(ONMPH_STATS_LEN == ONMPH_STATS_NUM);
+
+// helper functions --------------------------------------------------------- //
+
+#define STAT_REG_OFFSET(x) ((ONMPH_REG_MAC_ST##x) - ONMPH_REG_MAC_FIRST_STAT)
+
+#define STAT_OFF_TX_BYTES_OK 0
+#define STAT_OFF_TX_FRAMES 1
+
+static void onmph_update_mac_stats(struct onmph_info *priv)
+{
+ u64 *data = priv->stats_data;
+ u64 tx_frames_before;
+ u32 *regs, *rptr;
+ int ret;
+
+ // read all registers at once
+ regs = kmalloc_array(ONMPH_STATS_LEN, sizeof(u32), GFP_ATOMIC);
+ ret = onmph_rregs(priv, ONMPH_MMS_MAC, ONMPH_REG_MAC_STOCTECTSTXL,
+ ONMPH_NUMBER_OF_STAT_REGS, regs);
+
+ if (ret)
+ goto out;
+
+ rptr = regs;
+
+ // WA for NCN26010 version 0x01
+ tx_frames_before = priv->stats_data[STAT_OFF_TX_FRAMES];
+
+ // TX bytes is a 64-bit register that spans over two 32-bit regs
+ // note: HW does auto-freeze when reading LSB and un-freeze on MSB
+ *(data++) += ((u64)*rptr) | (((u64)*(rptr + 1)) << 32);
+
+ // run until the next 64-bit register
+ for (rptr += 2; (rptr - regs) < STAT_REG_OFFSET(OCTECTSRXL); ++rptr)
+ *(data++) += *rptr;
+
+ // RX bytes is a 64-bit register that spans over two 32-bit regs
+ // note: HW does auto-freeze when reading LSB and un-freeze on MSB
+ *(data++) += ((u64)*rptr) | (((u64)*(rptr + 1)) << 32);
+
+ // copy everything else
+ for (rptr += 2; (rptr - regs) < ONMPH_NUMBER_OF_STAT_REGS; ++rptr)
+ *(data++) += *rptr;
+
+ // model-specific fixes
+ switch (priv->model) {
+ case ONMPH_MODEL_NCN26010:
+ if (priv->version == 0x01) {
+ // add 4 to transmitted bytes for each transmitted packet
+ // because the HW is not counting the FCS
+ priv->stats_data[STAT_OFF_TX_BYTES_OK] +=
+ 4 * (priv->stats_data[STAT_OFF_TX_FRAMES] -
+ tx_frames_before);
+ }
+ break;
+ }
+out:
+ kfree(regs);
+}
+
+// ethtool ops -------------------------------------------------------------- //
+
+static void onmph_get_drvinfo(struct net_device *dev,
+ struct ethtool_drvinfo *info)
+{
+ strscpy(info->driver, DRV_NAME, sizeof(info->driver));
+ strscpy(info->bus_info, dev_name(&dev->dev), sizeof(info->bus_info));
+ strscpy(info->version, DRV_VERSION, sizeof(info->version));
+}
+
+static int onmph_ethtool_set_link_ksettings(struct net_device *dev,
+ const struct ethtool_link_ksettings *cmd)
+{
+ struct onmph_info *priv = netdev_priv(dev);
+
+ return phylink_ethtool_ksettings_set(priv->phylink, cmd);
+}
+
+static int onmph_ethtool_get_link_ksettings(struct net_device *dev,
+ struct ethtool_link_ksettings *cmd)
+{
+ struct onmph_info *priv = netdev_priv(dev);
+
+ return phylink_ethtool_ksettings_get(priv->phylink, cmd);
+}
+
+static int onmph_get_sset_count(struct net_device *dev, int sset)
+{
+ int ret;
+
+ switch (sset) {
+ case ETH_SS_STATS:
+ ret = ONMPH_STATS_LEN;
+ break;
+
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+
+ return ret;
+}
+
+static void onmph_get_strings(struct net_device *dev, u32 stringset, u8 *buf)
+{
+ memcpy(buf, onmph_stat_strings, ONMPH_STATS_LEN * ETH_GSTRING_LEN);
+}
+
+static void onmph_get_ethtool_stats(struct net_device *dev,
+ struct ethtool_stats *stats, u64 *data)
+{
+ struct onmph_info *priv = netdev_priv(dev);
+
+ onmph_update_mac_stats(priv);
+ memcpy(data, priv->stats_data, sizeof(priv->stats_data));
+}
+
+// -------------------------------------------------------------------------- //
+
+const struct ethtool_ops onmph_ethtool_ops = {
+ .get_drvinfo = onmph_get_drvinfo,
+ .get_link = ethtool_op_get_link,
+ .get_link_ksettings = onmph_ethtool_get_link_ksettings,
+ .set_link_ksettings = onmph_ethtool_set_link_ksettings,
+ .get_sset_count = onmph_get_sset_count,
+ .get_strings = onmph_get_strings,
+ .get_ethtool_stats = onmph_get_ethtool_stats,
+};
+
+// -------------------------------------------------------------------------- //
--
2.40.1
Powered by blists - more mailing lists