On 28-06-22, 18:13, Sean Anderson wrote: > This adds support for the Lynx 10G "SerDes" devices found on various NXP > QorIQ SoCs. There may be up to four SerDes devices on each SoC, each > supporting up to eight lanes. Protocol support for each SerDes is highly > heterogeneous, with each SoC typically having a totally different > selection of supported protocols for each lane. Additionally, the SerDes > devices on each SoC also have differing support. One SerDes will > typically support Ethernet on most lanes, while the other will typically > support PCIe on most lanes. > > There is wide hardware support for this SerDes. I have not done extensive > digging, but it seems to be used on almost every QorIQ device, including > the AMP and Layerscape series. Because each SoC typically has specific > instructions and exceptions for its SerDes, I have limited the initial > scope of this module to just the LS1046A. Additionally, I have only added > support for Ethernet protocols. There is not a great need for dynamic > reconfiguration for other protocols (SATA and PCIe handle rate changes in > hardware), so support for them may never be added. > > Nevertheless, I have tried to provide an obvious path for adding support > for other SoCs as well as other protocols. SATA just needs support for > configuring LNmSSCR0. PCIe may need to configure the equalization > registers. It also uses multiple lanes. I have tried to write the driver > with multi-lane support in mind, so there should not need to be any large > changes. Although there are 6 protocols supported, I have only tested SGMII > and XFI. The rest have been implemented as described in the datasheet. > > The PLLs are modeled as clocks proper. This lets us take advantage of the > existing clock infrastructure. I have not given the same treatment to the > lane "clocks" (dividers) because they need to be programmed in-concert with > the rest of the lane settings. One tricky thing is that the VCO (pll) rate > exceeds 2^32 (maxing out at around 5GHz). This will be a problem on 32-bit > platforms, since clock rates are stored as unsigned longs. To work around > this, the pll clock rate is generally treated in units of kHz. > > The PLLs are configured rather interestingly. Instead of the usual direct > programming of the appropriate divisors, the input and output clock rates > are selected directly. Generally, the only restriction is that the input > and output must be integer multiples of each other. This suggests some kind > of internal look-up table. The datasheets generally list out the supported > combinations explicitly, and not all input/output combinations are > documented. I'm not sure if this is due to lack of support, or due to an > oversight. If this becomes an issue, then some combinations can be > blacklisted (or whitelisted). This may also be necessary for other SoCs > which have more stringent clock requirements. > > The general API call list for this PHY is documented under the driver-api > docs. I think this is rather standard, except that most driverts configure > the mode (protocol) at xlate-time. Unlike some other phys where e.g. PCIe > x4 will use 4 separate phys all configured for PCIe, this driver uses one > phy configured to use 4 lanes. This is because while the individual lanes > may be configured individually, the protocol selection acts on all lanes at > once. Additionally, the order which lanes should be configured in is > specified by the datasheet. To coordinate this, lanes are reserved in > phy_init, and released in phy_exit. > > When getting a phy, if a phy already exists for those lanes, it is reused. > This is to make things like QSGMII work. Four MACs will all want to ensure > that the lane is configured properly, and we need to ensure they can all > call phy_init, etc. There is refcounting for phy_init and phy_power_on, so > the phy will only be powered on once. However, there is no refcounting for > phy_set_mode. A "rogue" MAC could set the mode to something non-QSGMII and > break the other MACs. Perhaps there is an opportunity for future > enhancement here. > > This driver was written with reference to the LS1046A reference manual. > However, it was informed by reference manuals for all processors with > mEMACs, especially the T4240 (which appears to have a "maxed-out" > configuration). > > Signed-off-by: Sean Anderson <sean.anderson@xxxxxxxx> > --- > As noted later on in this series (in the phylink conversion patch), this > driver does not quite function properly. When the bootloader is > instructed to not configure the SerDes, only one lane comes up. > > Changes in v2: > - Clear SGMIIaCR1_PCS_EN during probe > - Fix not clearing group->pll after disabling it > - Handle 1000Base-KX in lynx_proto_mode_prep > - Power off lanes during probe > - Rename LYNX_PROTO_UNKNOWN to LYNX_PROTO_NONE > - Rename driver to Lynx 10G (etc.) > - Support 1 and 2 phy-cells > > Documentation/driver-api/phy/index.rst | 1 + > Documentation/driver-api/phy/qoriq.rst | 93 ++ > MAINTAINERS | 6 + > drivers/phy/freescale/Kconfig | 19 + > drivers/phy/freescale/Makefile | 1 + > drivers/phy/freescale/phy-fsl-lynx-10g.c | 1483 ++++++++++++++++++++++ > 6 files changed, 1603 insertions(+) > create mode 100644 Documentation/driver-api/phy/qoriq.rst > create mode 100644 drivers/phy/freescale/phy-fsl-lynx-10g.c > > diff --git a/Documentation/driver-api/phy/index.rst b/Documentation/driver-api/phy/index.rst > index 69ba1216de72..cc7ded8b969c 100644 > --- a/Documentation/driver-api/phy/index.rst > +++ b/Documentation/driver-api/phy/index.rst > @@ -7,6 +7,7 @@ Generic PHY Framework > .. toctree:: > > phy > + qoriq > samsung-usb2 > > .. only:: subproject and html > diff --git a/Documentation/driver-api/phy/qoriq.rst b/Documentation/driver-api/phy/qoriq.rst > new file mode 100644 > index 000000000000..cbc2ac9ca4aa > --- /dev/null > +++ b/Documentation/driver-api/phy/qoriq.rst > @@ -0,0 +1,93 @@ > +.. SPDX-License-Identifier: GPL-2.0 > + > +======================= > +QorIQ SerDes (Lynx 10G) > +======================= > + > +Using this phy > +-------------- > + > +The general order of calls should be:: > + > + [devm_][of_]phy_get() > + phy_init() > + phy_power_on() > + phy_set_mode[_ext]() > + ... > + phy_power_off() > + phy_exit() > + [[of_]phy_put()] Why is this required here? This should conform to generic phy sequences as documented in Documentation/driver-api/phy/phy.rst > + > +:c:func:`phy_get` just gets (or creates) a new :c:type:`phy` with the lanes > +described in the phandle. :c:func:`phy_init` is what actually reserves the > +lanes for use. Unlike some other drivers, when the phy is created, there is no > +default protocol. :c:func:`phy_set_mode <phy_set_mode_ext>` must be called in > +order to set the protocol. > + > +Supporting SoCs > +--------------- > + > +Each new SoC needs a :c:type:`struct lynx_conf <lynx_conf>` for each SerDes. > +The most important member is `modes`, which is an array of :c:type:`struct > +lynx_mode <lynx_mode>`. Each "mode" represents a configuration which can be > +programmed into a protocol control register. Modes can support multiple lanes > +(such for PCIe x2 or x4), as well as multiple protocols (such as SGMII and > +1000Base-KX). There are several helper macros to make configuring each mode > +easier. It is important that the list of modes is complete, even if not all > +protocols are supported. This lets the driver know which lanes are available, > +and which have been configured by the RCW. > + > +If a protocol is missing, add it to :c:type:`enum lynx_protocol > +<lynx_protocol>`, and to ``UNSUPPORTED_PROTOS``. If the PCCR shifts/masks for > +your protocol are missing, you will need to add them to > +:c:func:`lynx_proto_mode_mask` and :c:func:`lynx_proto_mode_shift`. > + > +For example, the configuration for SerDes1 of the LS1046A is:: > + > + static const struct lynx_mode ls1046a_modes1[] = { > + CONF_SINGLE(1, PCIE, 0x0, 1, 0b001), > + CONF_1000BASEKX(0, 0x8, 0, 0b001), > + CONF_SGMII25KX(1, 0x8, 1, 0b001), > + CONF_SGMII25KX(2, 0x8, 2, 0b001), > + CONF_SGMII25KX(3, 0x8, 3, 0b001), > + CONF_SINGLE(1, QSGMII, 0x9, 2, 0b001), > + CONF_XFI(2, 0xB, 0, 0b010), > + CONF_XFI(3, 0xB, 1, 0b001), > + }; > + > + static const struct lynx_conf ls1046a_conf1 = { > + .modes = ls1046a_modes1, > + .mode_count = ARRAY_SIZE(ls1046a_modes1), > + .lanes = 4, > + .endian = REGMAP_ENDIAN_BIG, > + }; > + > +There is an additional set of configuration for SerDes2, which supports a > +different set of modes. Both configurations should be added to the match > +table:: > + > + { .compatible = "fsl,ls1046-serdes-1", .data = &ls1046a_conf1 }, > + { .compatible = "fsl,ls1046-serdes-2", .data = &ls1046a_conf2 }, > + > +Supporting Protocols > +-------------------- > + > +Each protocol is a combination of values which must be programmed into the lane > +registers. To add a new protocol, first add it to :c:type:`enum lynx_protocol > +<lynx_protocol>`. If it is in ``UNSUPPORTED_PROTOS``, remove it. Add a new > +entry to `lynx_proto_params`, and populate the appropriate fields. You may need > +to add some new members to support new fields. Modify `lynx_lookup_proto` to > +map the :c:type:`enum phy_mode <phy_mode>` to :c:type:`enum lynx_protocol > +<lynx_protocol>`. Ensure that :c:func:`lynx_proto_mode_mask` and > +:c:func:`lynx_proto_mode_shift` have been updated with support for your > +protocol. > + > +You may need to modify :c:func:`lynx_set_mode` in order to support your > +procotol. This can happen when you have added members to :c:type:`struct > +lynx_proto_params <lynx_proto_params>`. It can also happen if you have specific > +clocking requirements, or protocol-specific registers to program. > + > +Internal API Reference > +---------------------- > + > +.. kernel-doc:: drivers/phy/freescale/phy-fsl-lynx-10g.c > diff --git a/MAINTAINERS b/MAINTAINERS > index ca95b1833b97..ef65e2acdb48 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -7977,6 +7977,12 @@ F: drivers/ptp/ptp_qoriq.c > F: drivers/ptp/ptp_qoriq_debugfs.c > F: include/linux/fsl/ptp_qoriq.h > > +FREESCALE QORIQ SERDES DRIVER > +M: Sean Anderson <sean.anderson@xxxxxxxx> > +S: Maintained > +F: Documentation/driver-api/phy/qoriq.rst > +F: drivers/phy/freescale/phy-qoriq.c > + > FREESCALE QUAD SPI DRIVER > M: Han Xu <han.xu@xxxxxxx> > L: linux-spi@xxxxxxxxxxxxxxx > diff --git a/drivers/phy/freescale/Kconfig b/drivers/phy/freescale/Kconfig > index f9c54cd02036..857b4d123515 100644 > --- a/drivers/phy/freescale/Kconfig > +++ b/drivers/phy/freescale/Kconfig > @@ -38,3 +38,22 @@ config PHY_FSL_LYNX_28G > found on NXP's Layerscape platforms such as LX2160A. > Used to change the protocol running on SerDes lanes at runtime. > Only useful for a restricted set of Ethernet protocols. > + > +config PHY_FSL_LYNX_10G > + tristate "Freescale Layerscale Lynx 10G SerDes support" > + select GENERIC_PHY > + select REGMAP_MMIO > + help > + This adds support for the Lynx "SerDes" devices found on various QorIQ > + SoCs. There may be up to four SerDes devices on each SoC, and each > + device supports up to eight lanes. The SerDes is configured by default > + by the RCW, but this module is necessary in order to support dynamic > + reconfiguration (such as to support 1G and 10G ethernet on the same > + interface). The hardware supports a variety of protocols, including > + Ethernet, SATA, PCIe, and more exotic links such as Interlaken and > + Aurora. This driver only supports Ethernet, but it will try not to > + touch lanes configured for other protocols. > + > + If you have a QorIQ processor and want to dynamically reconfigure your > + SerDes, say Y. If this driver is compiled as a module, it will be > + named phy-qoriq. > diff --git a/drivers/phy/freescale/Makefile b/drivers/phy/freescale/Makefile > index 3518d5dbe8a7..aa4374ed217c 100644 > --- a/drivers/phy/freescale/Makefile > +++ b/drivers/phy/freescale/Makefile > @@ -2,4 +2,5 @@ > obj-$(CONFIG_PHY_FSL_IMX8MQ_USB) += phy-fsl-imx8mq-usb.o > obj-$(CONFIG_PHY_MIXEL_MIPI_DPHY) += phy-fsl-imx8-mipi-dphy.o > obj-$(CONFIG_PHY_FSL_IMX8M_PCIE) += phy-fsl-imx8m-pcie.o > +obj-$(CONFIG_PHY_FSL_LYNX_10G) += phy-fsl-lynx-10g.o > obj-$(CONFIG_PHY_FSL_LYNX_28G) += phy-fsl-lynx-28g.o > diff --git a/drivers/phy/freescale/phy-fsl-lynx-10g.c b/drivers/phy/freescale/phy-fsl-lynx-10g.c > new file mode 100644 > index 000000000000..480bd493fbc2 > --- /dev/null > +++ b/drivers/phy/freescale/phy-fsl-lynx-10g.c > @@ -0,0 +1,1483 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * Copyright (C) 2022 Sean Anderson <sean.anderson@xxxxxxxx> > + */ > + > +#include <linux/clk.h> > +#include <linux/clk-provider.h> > +#include <linux/math64.h> > +#include <linux/platform_device.h> > +#include <linux/phy.h> > +#include <linux/phy/phy.h> > +#include <linux/regmap.h> > +#include <linux/units.h> > + > +#define PLL_STRIDE 0x20 > +#define PLLa(a, off) ((a) * PLL_STRIDE + (off)) > +#define PLLaRSTCTL(a) PLLa(a, 0x00) > +#define PLLaCR0(a) PLLa(a, 0x04) > + > +#define PLLaRSTCTL_RSTREQ BIT(31) > +#define PLLaRSTCTL_RST_DONE BIT(30) > +#define PLLaRSTCTL_RST_ERR BIT(29) > +#define PLLaRSTCTL_PLLRST_B BIT(7) > +#define PLLaRSTCTL_SDRST_B BIT(6) > +#define PLLaRSTCTL_SDEN BIT(5) > + > +#define PLLaCR0_POFF BIT(31) > +#define PLLaCR0_RFCLK_SEL GENMASK(30, 28) > +#define PLLaCR0_PLL_LCK BIT(23) > +#define PLLaCR0_FRATE_SEL GENMASK(19, 16) > +#define PLLaCR0_DLYDIV_SEL GENMASK(1, 0) > + > +#define PCCR_BASE 0x200 > +#define PCCR_STRIDE 0x4 > +#define PCCRn(n) (PCCR_BASE + n * PCCR_STRIDE) > + > +#define PCCR0_PEXa_MASK GENMASK(2, 0) > +#define PCCR0_PEXa_SHIFT(a) (28 - (a) * 4) use FIELD_GET/PREP instead of defining shifts? That would use MASK to extract/set the field > + > +#define PCCR2_SATAa_MASK GENMASK(2, 0) > +#define PCCR2_SATAa_SHIFT(a) (28 - (a) * 4) > + > +#define PCCR8_SGMIIa_KX BIT(3) > +#define PCCR8_SGMIIa_MASK GENMASK(3, 0) > +#define PCCR8_SGMIIa_SHIFT(a) (28 - (a) * 4) > + > +#define PCCR9_QSGMIIa_MASK GENMASK(2, 0) > +#define PCCR9_QSGMIIa_SHIFT(a) (28 - (a) * 4) > + > +#define PCCRB_XFIa_MASK GENMASK(2, 0) > +#define PCCRB_XFIa_SHIFT(a) (28 - (a) * 4) > + > +#define LANE_BASE 0x800 > +#define LANE_STRIDE 0x40 > +#define LNm(m, off) (LANE_BASE + (m) * LANE_STRIDE + (off)) > +#define LNmGCR0(m) LNm(m, 0x00) > +#define LNmGCR1(m) LNm(m, 0x04) > +#define LNmSSCR0(m) LNm(m, 0x0C) > +#define LNmRECR0(m) LNm(m, 0x10) > +#define LNmRECR1(m) LNm(m, 0x14) > +#define LNmTECR0(m) LNm(m, 0x18) > +#define LNmSSCR1(m) LNm(m, 0x1C) > +#define LNmTTLCR0(m) LNm(m, 0x20) > + > +#define LNmGCR0_RPLL_LES BIT(31) > +#define LNmGCR0_RRAT_SEL GENMASK(29, 28) > +#define LNmGCR0_TPLL_LES BIT(27) > +#define LNmGCR0_TRAT_SEL GENMASK(25, 24) > +#define LNmGCR0_RRST_B BIT(22) > +#define LNmGCR0_TRST_B BIT(21) > +#define LNmGCR0_RX_PD BIT(20) > +#define LNmGCR0_TX_PD BIT(19) > +#define LNmGCR0_IF20BIT_EN BIT(18) > +#define LNmGCR0_FIRST_LANE BIT(16) > +#define LNmGCR0_TTRM_VM_SEL GENMASK(13, 12) > +#define LNmGCR0_PROTS GENMASK(11, 7) > + > +#define LNmGCR0_RAT_SEL_SAME 0b00 > +#define LNmGCR0_RAT_SEL_HALF 0b01 > +#define LNmGCR0_RAT_SEL_QUARTER 0b10 > +#define LNmGCR0_RAT_SEL_DOUBLE 0b11 > + > +#define LNmGCR0_PROTS_PCIE 0b00000 > +#define LNmGCR0_PROTS_SGMII 0b00001 > +#define LNmGCR0_PROTS_SATA 0b00010 > +#define LNmGCR0_PROTS_XFI 0b01010 > + > +#define LNmGCR1_RDAT_INV BIT(31) > +#define LNmGCR1_TDAT_INV BIT(30) > +#define LNmGCR1_OPAD_CTL BIT(26) > +#define LNmGCR1_REIDL_TH GENMASK(22, 20) > +#define LNmGCR1_REIDL_EX_SEL GENMASK(19, 18) > +#define LNmGCR1_REIDL_ET_SEL GENMASK(17, 16) > +#define LNmGCR1_REIDL_EX_MSB BIT(15) > +#define LNmGCR1_REIDL_ET_MSB BIT(14) > +#define LNmGCR1_REQ_CTL_SNP BIT(13) > +#define LNmGCR1_REQ_CDR_SNP BIT(12) > +#define LNmGCR1_TRSTDIR BIT(7) > +#define LNmGCR1_REQ_BIN_SNP BIT(6) > +#define LNmGCR1_ISLEW_RCTL GENMASK(5, 4) > +#define LNmGCR1_OSLEW_RCTL GENMASK(1, 0) > + > +#define LNmRECR0_GK2OVD GENMASK(27, 24) > +#define LNmRECR0_GK3OVD GENMASK(19, 16) > +#define LNmRECR0_GK2OVD_EN BIT(15) > +#define LNmRECR0_GK3OVD_EN BIT(16) > +#define LNmRECR0_BASE_WAND GENMASK(11, 10) > +#define LNmRECR0_OSETOVD GENMASK(5, 0) > + > +#define LNmRECR0_BASE_WAND_OFF 0b00 > +#define LNmRECR0_BASE_WAND_DEFAULT 0b01 > +#define LNmRECR0_BASE_WAND_ALTERNATE 0b10 > +#define LNmRECR0_BASE_WAND_OSETOVD 0b11 > + > +#define LNmTECR0_TEQ_TYPE GENMASK(29, 28) > +#define LNmTECR0_SGN_PREQ BIT(26) > +#define LNmTECR0_RATIO_PREQ GENMASK(25, 22) > +#define LNmTECR0_SGN_POST1Q BIT(21) > +#define LNmTECR0_RATIO_PST1Q GENMASK(20, 16) > +#define LNmTECR0_ADPT_EQ GENMASK(13, 8) > +#define LNmTECR0_AMP_RED GENMASK(5, 0) > + > +#define LNmTECR0_TEQ_TYPE_NONE 0b00 > +#define LNmTECR0_TEQ_TYPE_PRE 0b01 > +#define LNmTECR0_TEQ_TYPE_BOTH 0b10 > + > +#define LNmTTLCR0_FLT_SEL GENMASK(29, 24) > + > +#define PCS_STRIDE 0x10 > +#define CR_STRIDE 0x4 > +#define PCSa(a, base, cr) (base + (a) * PCS_STRIDE + (cr) * CR_STRIDE) > + > +#define PCSaCR1_MDEV_PORT GENMASK(31, 27) > + > +#define SGMII_BASE 0x1800 > +#define SGMIIaCR1(a) PCSa(a, SGMII_BASE, 1) > + > +#define SGMIIaCR1_SGPCS_EN BIT(11) > + > +#define QSGMII_OFFSET 0x1880 > +#define QSGMIIaCR1(a) PCSa(a, QSGMII_BASE, 1) > + > +#define XFI_OFFSET 0x1980 > +#define XFIaCR1(a) PCSa(a, XFI_BASE, 1) > + > +/* The maximum number of lanes in a single serdes */ > +#define MAX_LANES 8 > + > +enum lynx_protocol { > + LYNX_PROTO_NONE = 0, > + LYNX_PROTO_SGMII, > + LYNX_PROTO_SGMII25, > + LYNX_PROTO_1000BASEKX, > + LYNX_PROTO_QSGMII, > + LYNX_PROTO_XFI, > + LYNX_PROTO_10GKR, > + LYNX_PROTO_PCIE, /* Not implemented */ > + LYNX_PROTO_SATA, /* Not implemented */ lets skip that and add when it is implemented > + LYNX_PROTO_LAST, > +}; > + > +static const char lynx_proto_str[][16] = { > + [LYNX_PROTO_NONE] = "unknown", > + [LYNX_PROTO_SGMII] = "SGMII", > + [LYNX_PROTO_SGMII25] = "2.5G SGMII", > + [LYNX_PROTO_1000BASEKX] = "1000Base-KX", > + [LYNX_PROTO_QSGMII] = "QSGMII", > + [LYNX_PROTO_XFI] = "XFI", > + [LYNX_PROTO_10GKR] = "10GBase-KR", > + [LYNX_PROTO_PCIE] = "PCIe", > + [LYNX_PROTO_SATA] = "SATA", > +}; > + > +#define PROTO_MASK(proto) BIT(LYNX_PROTO_##proto) > +#define UNSUPPORTED_PROTOS (PROTO_MASK(SATA) | PROTO_MASK(PCIE)) > + > +/** > + * struct lynx_proto_params - Parameters for configuring a protocol > + * @frate_khz: The PLL rate, in kHz > + * @rat_sel: The divider to get the line rate > + * @if20bit: Whether the proto is 20 bits or 10 bits > + * @prots: Lane protocol select > + * @reidl_th: Receiver electrical idle detection threshold > + * @reidl_ex: Exit electrical idle filter > + * @reidl_et: Enter idle filter > + * @slew: Slew control > + * @baseline_wander: Enable baseline wander correction > + * @gain: Adaptive equalization gain override > + * @offset_override: Adaptive equalization offset override > + * @teq: Transmit equalization type (none, precursor, or precursor and > + * postcursor). The next few values are only used for appropriate > + * equalization types. > + * @preq_ratio: Ratio of full swing transition bit to pre-cursor > + * @postq_ratio: Ratio of full swing transition bit to first post-cursor. > + * @adpt_eq: Transmitter Adjustments for 8G/10G > + * @amp_red: Overall TX Amplitude Reduction > + * @flt_sel: TTL configuration selector > + */ > +struct lynx_proto_params { > + u32 frate_khz; > + u8 rat_sel; > + u8 prots; > + u8 reidl_th; > + u8 reidl_ex; > + u8 reidl_et; > + u8 slew; > + u8 gain; > + u8 baseline_wander; > + u8 offset_override; > + u8 teq; > + u8 preq_ratio; > + u8 postq_ratio; > + u8 adpt_eq; > + u8 amp_red; > + u8 flt_sel; > + bool if20bit; > +}; > + > +static const struct lynx_proto_params lynx_proto_params[] = { > + [LYNX_PROTO_SGMII] = { > + .frate_khz = 5000000, > + .rat_sel = LNmGCR0_RAT_SEL_QUARTER, > + .if20bit = false, > + .prots = LNmGCR0_PROTS_SGMII, > + .reidl_th = 0b001, > + .reidl_ex = 0b011, > + .reidl_et = 0b100, > + .slew = 0b01, > + .gain = 0b1111, > + .offset_override = 0b0011111, > + .teq = LNmTECR0_TEQ_TYPE_NONE, > + .adpt_eq = 0b110000, > + .amp_red = 0b000110, > + .flt_sel = 0b111001, > + }, > + [LYNX_PROTO_1000BASEKX] = { > + .frate_khz = 5000000, > + .rat_sel = LNmGCR0_RAT_SEL_QUARTER, > + .if20bit = false, > + .prots = LNmGCR0_PROTS_SGMII, > + .slew = 0b01, > + .gain = 0b1111, > + .offset_override = 0b0011111, > + .teq = LNmTECR0_TEQ_TYPE_NONE, > + .adpt_eq = 0b110000, > + .flt_sel = 0b111001, > + }, > + [LYNX_PROTO_SGMII25] = { > + .frate_khz = 3125000, > + .rat_sel = LNmGCR0_RAT_SEL_SAME, > + .if20bit = false, > + .prots = LNmGCR0_PROTS_SGMII, > + .slew = 0b10, > + .offset_override = 0b0011111, > + .teq = LNmTECR0_TEQ_TYPE_PRE, > + .postq_ratio = 0b00110, > + .adpt_eq = 0b110000, > + }, > + [LYNX_PROTO_QSGMII] = { > + .frate_khz = 5000000, > + .rat_sel = LNmGCR0_RAT_SEL_SAME, > + .if20bit = true, > + .prots = LNmGCR0_PROTS_SGMII, > + .slew = 0b01, > + .offset_override = 0b0011111, > + .teq = LNmTECR0_TEQ_TYPE_PRE, > + .postq_ratio = 0b00110, > + .adpt_eq = 0b110000, > + .amp_red = 0b000010, > + }, > + [LYNX_PROTO_XFI] = { > + .frate_khz = 5156250, > + .rat_sel = LNmGCR0_RAT_SEL_DOUBLE, > + .if20bit = true, > + .prots = LNmGCR0_PROTS_XFI, > + .slew = 0b01, > + .baseline_wander = LNmRECR0_BASE_WAND_DEFAULT, > + .offset_override = 0b1011111, > + .teq = LNmTECR0_TEQ_TYPE_PRE, > + .postq_ratio = 0b00011, > + .adpt_eq = 0b110000, > + .amp_red = 0b000111, > + }, > + [LYNX_PROTO_10GKR] = { > + .frate_khz = 5156250, > + .rat_sel = LNmGCR0_RAT_SEL_DOUBLE, > + .prots = LNmGCR0_PROTS_XFI, > + .slew = 0b01, > + .baseline_wander = LNmRECR0_BASE_WAND_DEFAULT, > + .offset_override = 0b1011111, > + .teq = LNmTECR0_TEQ_TYPE_BOTH, > + .preq_ratio = 0b0011, > + .postq_ratio = 0b01100, > + .adpt_eq = 0b110000, > + }, > +}; > + > +/** > + * struct lynx_mode - A single configuration of a protocol controller > + * @protos: A bitmask of the &enum lynx_protocol this mode supports > + * @lanes: A bitmask of the lanes which will be used when this config is > + * selected > + * @pccr: The number of the PCCR which contains this mode > + * @idx: The index of the protocol controller. For example, SGMIIB would have > + * index 1. > + * @cfg: The value to program into the controller to select this mode > + * > + * The serdes has multiple protocol controllers which can be each be selected > + * independently. Depending on their configuration, they may use multiple lanes > + * at once (e.g. AUI or PCIe x4). Additionally, multiple protocols may be > + * supported by a single mode (XFI and 10GKR differ only in their protocol > + * parameters). > + */ > +struct lynx_mode { > + u16 protos; > + u8 lanes; > + u8 pccr; > + u8 idx; > + u8 cfg; > +}; > + > +static_assert(LYNX_PROTO_LAST - 1 <= > + sizeof_field(struct lynx_mode, protos) * BITS_PER_BYTE); > +static_assert(MAX_LANES <= > + sizeof_field(struct lynx_mode, lanes) * BITS_PER_BYTE); > + > +#define CONF(_lanes, _protos, _pccr, _idx, _cfg) { \ > + .lanes = _lanes, \ > + .protos = _protos, \ > + .pccr = _pccr, \ > + .idx = _idx, \ > + .cfg = _cfg, \ > +} > + > +#define CONF_SINGLE(lane, proto, pccr, idx, cfg) \ > + CONF(BIT(lane), PROTO_MASK(proto), pccr, idx, cfg) > + > +#define CONF_1000BASEKX(lane, pccr, idx, cfg) \ > + CONF(BIT(lane), PROTO_MASK(SGMII) | PROTO_MASK(1000BASEKX), \ > + pccr, idx, cfg) > + > +#define CONF_SGMII25(lane, pccr, idx, cfg) \ > + CONF(BIT(lane), PROTO_MASK(SGMII) | PROTO_MASK(SGMII25), \ > + pccr, idx, cfg) > + > +#define CONF_SGMII25KX(lane, pccr, idx, cfg) \ > + CONF(BIT(lane), \ > + PROTO_MASK(SGMII) | PROTO_MASK(1000BASEKX) | PROTO_MASK(SGMII25), \ > + pccr, idx, cfg) > + > +#define CONF_XFI(lane, pccr, idx, cfg) \ > + CONF(BIT(lane), PROTO_MASK(XFI) | PROTO_MASK(10GKR), pccr, idx, cfg) > + > +/** > + * struct lynx_conf - Configuration for a particular serdes > + * @modes: Valid protocol controller configurations > + * @mode_count: Number of modes in @modes > + * @lanes: Number of lanes > + * @endian: Endianness of the registers > + */ > +struct lynx_conf { > + const struct lynx_mode *modes; > + size_t mode_count; > + unsigned int lanes; > + enum regmap_endian endian; > +}; > + > +struct lynx_priv; > + > +/** > + * struct lynx_clk - Driver data for the PLLs > + * @hw: The clock hardware > + * @serdes: The parent serdes > + * @idx: Which PLL this clock is for > + */ > +struct lynx_clk { > + struct clk_hw hw; > + struct lynx_priv *serdes; > + unsigned int idx; > +}; > + > +static struct lynx_clk *lynx_clk_hw_to_priv(struct clk_hw *hw) > +{ > + return container_of(hw, struct lynx_clk, hw); > +} > + > +/** > + * struct lynx_priv - Driver data for the serdes > + * @lock: A lock protecting "common" registers in @regmap, as well as the > + * members of this struct. Lane-specific registers are protected by the > + * phy's lock. PLL registers are protected by the clock's lock. > + * @pll: The PLL clocks > + * @ref: The reference clocks for the PLLs > + * @dev: The serdes device > + * @regmap: The backing regmap > + * @conf: The configuration for this serdes > + * @used_lanes: Bitmap of the lanes currently used by phys > + * @groups: List of the created groups > + */ > +struct lynx_priv { > + struct mutex lock; > + struct lynx_clk pll[2]; > + struct clk *ref[2]; > + struct device *dev; > + struct regmap *regmap; > + const struct lynx_conf *conf; > + unsigned int used_lanes; > + struct list_head groups; > +}; > + > +/** > + * struct lynx_group - Driver data for a group of lanes > + * @groups: List of other groups; protected by @serdes->lock. > + * @phy: The associated phy > + * @serdes: The parent serdes > + * @pll: The currently-used pll > + * @first_lane: The first lane in the group > + * @last_lane: The last lane in the group > + * @proto: The currently-configured protocol > + * @users: Number of current users; protected by @serdes->lock. > + */ > +struct lynx_group { > + struct list_head groups; > + struct phy *phy; > + struct lynx_priv *serdes; > + struct clk *pll; > + unsigned int first_lane; > + unsigned int last_lane; > + enum lynx_protocol proto; > + unsigned int users; > +}; > + > +static u32 lynx_read(struct lynx_priv *serdes, u32 reg) > +{ > + unsigned int ret = 0; > + > + WARN_ON_ONCE(regmap_read(serdes->regmap, reg, &ret)); > + return ret; > +} > + > +static void lynx_write(struct lynx_priv *serdes, u32 val, u32 reg) > +{ > + WARN_ON_ONCE(regmap_write(serdes->regmap, reg, val)); > +} > + > +/* XXX: The output rate is in kHz to avoid overflow on 32-bit arches */ > + > +static void lynx_pll_disable(struct clk_hw *hw) > +{ > + struct lynx_clk *clk = lynx_clk_hw_to_priv(hw); > + struct lynx_priv *serdes = clk->serdes; > + u32 rstctl = lynx_read(serdes, PLLaRSTCTL(clk->idx)); > + > + dev_dbg(clk->serdes->dev, "%s(pll%d)\n", __func__, clk->idx); no need for __func__ in dev_dbg pls > + > + rstctl &= ~PLLaRSTCTL_SDRST_B; > + lynx_write(serdes, rstctl, PLLaRSTCTL(clk->idx)); > + ndelay(50); > + rstctl &= ~(PLLaRSTCTL_SDEN | PLLaRSTCTL_PLLRST_B); > + lynx_write(serdes, rstctl, PLLaRSTCTL(clk->idx)); > + ndelay(100); > +} > + > +static int lynx_pll_enable(struct clk_hw *hw) > +{ > + struct lynx_clk *clk = lynx_clk_hw_to_priv(hw); > + struct lynx_priv *serdes = clk->serdes; > + u32 rstctl = lynx_read(serdes, PLLaRSTCTL(clk->idx)); > + > + dev_dbg(clk->serdes->dev, "%s(pll%d)\n", __func__, clk->idx); > + > + rstctl |= PLLaRSTCTL_RSTREQ; > + lynx_write(serdes, rstctl, PLLaRSTCTL(clk->idx)); > + > + rstctl &= ~PLLaRSTCTL_RSTREQ; > + rstctl |= PLLaRSTCTL_SDEN | PLLaRSTCTL_PLLRST_B | PLLaRSTCTL_SDRST_B; > + lynx_write(serdes, rstctl, PLLaRSTCTL(clk->idx)); > + > + /* TODO: wait for the PLL to lock */ when will this be added? > + > + return 0; > +} > + > +static int lynx_pll_is_enabled(struct clk_hw *hw) > +{ > + struct lynx_clk *clk = lynx_clk_hw_to_priv(hw); > + struct lynx_priv *serdes = clk->serdes; > + u32 rstctl = lynx_read(serdes, PLLaRSTCTL(clk->idx)); > + > + dev_dbg(clk->serdes->dev, "%s(pll%d)\n", __func__, clk->idx); > + > + return rstctl & PLLaRSTCTL_RST_DONE && !(rstctl & PLLaRSTCTL_RST_ERR); > +} > + > +static const u32 rfclk_sel_map[8] = { > + [0b000] = 100000000, > + [0b001] = 125000000, > + [0b010] = 156250000, > + [0b011] = 150000000, > +}; > + > +/** > + * lynx_rfclk_to_sel() - Convert a reference clock rate to a selector > + * @rate: The reference clock rate > + * > + * To allow for some variation in the reference clock rate, up to 100ppm of > + * error is allowed. > + * > + * Return: An appropriate selector for @rate, or -%EINVAL. > + */ > +static int lynx_rfclk_to_sel(u32 rate) > +{ > + int ret; > + > + for (ret = 0; ret < ARRAY_SIZE(rfclk_sel_map); ret++) { > + u32 rfclk_rate = rfclk_sel_map[ret]; > + /* Allow an error of 100ppm */ > + u32 error = rfclk_rate / 10000; > + > + if (rate > rfclk_rate - error && rate < rfclk_rate + error) > + return ret; > + } > + > + return -EINVAL; > +} > + > +static const u32 frate_sel_map[16] = { > + [0b0000] = 5000000, > + [0b0101] = 3750000, > + [0b0110] = 5156250, > + [0b0111] = 4000000, > + [0b1001] = 3125000, > + [0b1010] = 3000000, > +}; > + > +/** > + * lynx_frate_to_sel() - Convert a VCO clock rate to a selector > + * @rate_khz: The VCO frequency, in kHz > + * > + * Return: An appropriate selector for @rate_khz, or -%EINVAL. > + */ > +static int lynx_frate_to_sel(u32 rate_khz) > +{ > + int ret; > + > + for (ret = 0; ret < ARRAY_SIZE(frate_sel_map); ret++) > + if (frate_sel_map[ret] == rate_khz) > + return ret; > + > + return -EINVAL; > +} > + > +static u32 lynx_pll_ratio(u32 frate_sel, u32 rfclk_sel) > +{ > + u64 frate; > + u32 rfclk, error, ratio; > + > + frate = frate_sel_map[frate_sel] * (u64)HZ_PER_KHZ; > + rfclk = rfclk_sel_map[rfclk_sel]; > + > + if (!frate || !rfclk) > + return 0; > + > + ratio = div_u64_rem(frate, rfclk, &error); > + if (!error) > + return ratio; > + return 0; > +} > + > +static unsigned long lynx_pll_recalc_rate(struct clk_hw *hw, > + unsigned long parent_rate) > +{ > + struct lynx_clk *clk = lynx_clk_hw_to_priv(hw); > + struct lynx_priv *serdes = clk->serdes; > + u32 cr0 = lynx_read(serdes, PLLaCR0(clk->idx)); > + u32 frate_sel = FIELD_GET(PLLaCR0_FRATE_SEL, cr0); > + u32 rfclk_sel = FIELD_GET(PLLaCR0_RFCLK_SEL, cr0); > + unsigned long ret; > + > + dev_dbg(clk->serdes->dev, "%s(pll%d, %lu)\n", __func__, > + clk->idx, parent_rate); > + > + ret = mult_frac(parent_rate, lynx_pll_ratio(frate_sel, rfclk_sel), > + HZ_PER_KHZ); > + return ret; > +} > + > +static long lynx_pll_round_rate(struct clk_hw *hw, unsigned long rate_khz, > + unsigned long *parent_rate) > +{ > + int frate_sel, rfclk_sel; > + struct lynx_clk *clk = lynx_clk_hw_to_priv(hw); > + u32 ratio; > + > + dev_dbg(clk->serdes->dev, "%s(pll%d, %lu, %lu)\n", __func__, > + clk->idx, rate_khz, *parent_rate); > + > + frate_sel = lynx_frate_to_sel(rate_khz); > + if (frate_sel < 0) > + return frate_sel; > + > + rfclk_sel = lynx_rfclk_to_sel(*parent_rate); > + if (rfclk_sel >= 0) { > + ratio = lynx_pll_ratio(frate_sel, rfclk_sel); > + if (ratio) > + return mult_frac(*parent_rate, ratio, HZ_PER_KHZ); > + } > + > + for (rfclk_sel = 0; > + rfclk_sel < ARRAY_SIZE(rfclk_sel_map); > + rfclk_sel++) { > + ratio = lynx_pll_ratio(frate_sel, rfclk_sel); > + if (ratio) { > + *parent_rate = rfclk_sel_map[rfclk_sel]; > + return mult_frac(*parent_rate, ratio, HZ_PER_KHZ); > + } > + } > + > + return -EINVAL; > +} > + > +static int lynx_pll_set_rate(struct clk_hw *hw, unsigned long rate_khz, > + unsigned long parent_rate) This really sounds like a clk driver, why is this in phy driver. Ideally this should a clock driver. Please move it to one.. > +{ > + int frate_sel, rfclk_sel, ret; > + struct lynx_clk *clk = lynx_clk_hw_to_priv(hw); > + struct lynx_priv *serdes = clk->serdes; > + u32 ratio, cr0 = lynx_read(serdes, PLLaCR0(clk->idx)); > + > + dev_dbg(clk->serdes->dev, "%s(pll%d, %lu, %lu)\n", __func__, > + clk->idx, rate_khz, parent_rate); > + > + frate_sel = lynx_frate_to_sel(rate_khz); > + if (frate_sel < 0) > + return frate_sel; > + > + /* First try the existing rate */ > + rfclk_sel = lynx_rfclk_to_sel(parent_rate); > + if (rfclk_sel >= 0) { > + ratio = lynx_pll_ratio(frate_sel, rfclk_sel); > + if (ratio) > + goto got_rfclk; > + } > + > + for (rfclk_sel = 0; > + rfclk_sel < ARRAY_SIZE(rfclk_sel_map); > + rfclk_sel++) { > + ratio = lynx_pll_ratio(frate_sel, rfclk_sel); > + if (ratio) { > + ret = clk_set_rate(serdes->ref[clk->idx], > + rfclk_sel_map[rfclk_sel]); > + if (!ret) > + goto got_rfclk; > + } > + } > + > + return ret; > + > +got_rfclk: > + cr0 &= ~(PLLaCR0_RFCLK_SEL | PLLaCR0_FRATE_SEL); > + cr0 |= FIELD_PREP(PLLaCR0_RFCLK_SEL, rfclk_sel); > + cr0 |= FIELD_PREP(PLLaCR0_FRATE_SEL, frate_sel); > + lynx_write(serdes, cr0, PLLaCR0(clk->idx)); > + return 0; > +} > + > +static const struct clk_ops lynx_pll_clk_ops = { > + .enable = lynx_pll_enable, > + .disable = lynx_pll_disable, > + .is_enabled = lynx_pll_is_enabled, > + .recalc_rate = lynx_pll_recalc_rate, > + .round_rate = lynx_pll_round_rate, > + .set_rate = lynx_pll_set_rate, > +}; right, this should be a clk driver > + > +static struct clk_hw *lynx_clk_get(struct of_phandle_args *clkspec, void *data) > +{ > + struct lynx_priv *serdes = data; > + > + if (clkspec->args_count != 1) > + return ERR_PTR(-EINVAL); > + > + if (clkspec->args[0] > 1) > + return ERR_PTR(-EINVAL); > + > + return &serdes->pll[clkspec->args[0]].hw; > +} > + > +/** > + * lynx_lane_bitmap() - Get a bitmap for a group of lanes > + * @group: The group of lanes > + * > + * Return: A mask containing all bits between @group->first and @group->last > + */ > +static unsigned int lynx_lane_bitmap(struct lynx_group *group) > +{ > + if (group->first_lane > group->last_lane) > + return GENMASK(group->first_lane, group->last_lane); > + else > + return GENMASK(group->last_lane, group->first_lane); > +} > + > +static int lynx_init(struct phy *phy) > +{ > + int ret = 0; > + struct lynx_group *group = phy_get_drvdata(phy); > + struct lynx_priv *serdes = group->serdes; > + unsigned int lane_mask = lynx_lane_bitmap(group); > + > + mutex_lock(&serdes->lock); > + if (serdes->used_lanes & lane_mask) > + ret = -EBUSY; > + else > + serdes->used_lanes |= lane_mask; > + mutex_unlock(&serdes->lock); > + return ret; > +} > + > +static int lynx_exit(struct phy *phy) > +{ > + struct lynx_group *group = phy_get_drvdata(phy); > + struct lynx_priv *serdes = group->serdes; > + > + clk_disable_unprepare(group->pll); > + clk_rate_exclusive_put(group->pll); > + group->pll = NULL; > + > + mutex_lock(&serdes->lock); > + serdes->used_lanes &= ~lynx_lane_bitmap(group); > + mutex_unlock(&serdes->lock); > + return 0; > +} > + > +/* > + * This is tricky. If first_lane=1 and last_lane=0, the condition will see 2, > + * 1, 0. But the loop body will see 1, 0. We do this to avoid underflow. We > + * can't pull the same trick when incrementing, because then we might have to > + * start at -1 if (e.g.) first_lane = 0. > + */ > +#define for_range(val, start, end) \ > + for (val = start < end ? start : start + 1; \ > + start < end ? val <= end : val-- > end; \ > + start < end ? val++ : 0) > +#define for_each_lane(lane, group) \ > + for_range(lane, group->first_lane, group->last_lane) > +#define for_each_lane_reverse(lane, group) \ > + for_range(lane, group->last_lane, group->first_lane) > + > +static int lynx_power_on(struct phy *phy) > +{ > + int i; > + struct lynx_group *group = phy_get_drvdata(phy); > + u32 gcr0; > + > + for_each_lane(i, group) { > + gcr0 = lynx_read(group->serdes, LNmGCR0(i)); > + gcr0 &= ~(LNmGCR0_RX_PD | LNmGCR0_TX_PD); > + lynx_write(group->serdes, gcr0, LNmGCR0(i)); > + > + usleep_range(15, 30); > + gcr0 |= LNmGCR0_RRST_B | LNmGCR0_TRST_B; > + lynx_write(group->serdes, gcr0, LNmGCR0(i)); > + } > + > + return 0; > +} > + > +static void lynx_power_off_lane(struct lynx_priv *serdes, unsigned int lane) > +{ > + u32 gcr0 = lynx_read(serdes, LNmGCR0(lane)); > + > + gcr0 |= LNmGCR0_RX_PD | LNmGCR0_TX_PD; > + gcr0 &= ~(LNmGCR0_RRST_B | LNmGCR0_TRST_B); > + lynx_write(serdes, gcr0, LNmGCR0(lane)); > +} > + > +static int lynx_power_off(struct phy *phy) > +{ > + unsigned int i; > + struct lynx_group *group = phy_get_drvdata(phy); > + > + for_each_lane_reverse(i, group) > + lynx_power_off_lane(group->serdes, i); > + > + return 0; > +} > + > +/** > + * lynx_lookup_proto() - Convert a phy-subsystem mode to a protocol > + * @mode: The mode to convert > + * @submode: The submode of @mode > + * > + * Return: A corresponding serdes-specific mode > + */ > +static enum lynx_protocol lynx_lookup_proto(enum phy_mode mode, int submode) > +{ > + switch (mode) { > + case PHY_MODE_ETHERNET: > + switch (submode) { > + case PHY_INTERFACE_MODE_SGMII: > + case PHY_INTERFACE_MODE_1000BASEX: > + return LYNX_PROTO_SGMII; > + case PHY_INTERFACE_MODE_2500BASEX: > + return LYNX_PROTO_SGMII25; > + case PHY_INTERFACE_MODE_QSGMII: > + return LYNX_PROTO_QSGMII; > + case PHY_INTERFACE_MODE_XGMII: > + case PHY_INTERFACE_MODE_10GBASER: > + return LYNX_PROTO_XFI; > + case PHY_INTERFACE_MODE_10GKR: > + return LYNX_PROTO_10GKR; > + default: > + return LYNX_PROTO_NONE; > + } > + /* Not implemented (yet) */ > + case PHY_MODE_PCIE: > + case PHY_MODE_SATA: > + default: > + return LYNX_PROTO_NONE; > + } > +} > + > +/** > + * lynx_lookup_mode() - Get the mode for a group/protocol combination > + * @group: The group of lanes to use > + * @proto: The protocol to use > + * > + * Return: An appropriate mode to use, or %NULL if none match. > + */ > +static const struct lynx_mode *lynx_lookup_mode(struct lynx_group *group, > + enum lynx_protocol proto) > +{ > + int i; > + const struct lynx_conf *conf = group->serdes->conf; > + > + for (i = 0; i < conf->mode_count; i++) { > + const struct lynx_mode *mode = &conf->modes[i]; > + > + if (BIT(proto) & mode->protos && > + lynx_lane_bitmap(group) == mode->lanes) > + return mode; > + } > + > + return NULL; > +} > + > +static int lynx_validate(struct phy *phy, enum phy_mode phy_mode, int submode, > + union phy_configure_opts *opts) > +{ > + enum lynx_protocol proto; > + struct lynx_group *group = phy_get_drvdata(phy); > + const struct lynx_mode *mode; > + > + proto = lynx_lookup_proto(phy_mode, submode); > + if (proto == LYNX_PROTO_NONE) > + return -EINVAL; > + > + /* Nothing to do */ > + if (proto == group->proto) > + return 0; > + > + mode = lynx_lookup_mode(group, proto); > + if (!mode) > + return -EINVAL; > + > + return 0; > +} > + > +/** > + * lynx_proto_mode_mask() - Get the mask for a PCCR config > + * @mode: The mode to use > + * > + * Return: The mask, shifted down to the lsb. > + */ > +static u32 lynx_proto_mode_mask(const struct lynx_mode *mode) > +{ > + switch (mode->pccr) { > + case 0x0: > + if (mode->protos & PROTO_MASK(PCIE)) > + return PCCR0_PEXa_MASK; > + break; > + case 0x2: > + if (mode->protos & PROTO_MASK(SATA)) > + return PCCR2_SATAa_MASK; > + break; > + case 0x8: > + if (mode->protos & PROTO_MASK(SGMII)) > + return PCCR8_SGMIIa_MASK; > + break; > + case 0x9: > + if (mode->protos & PROTO_MASK(QSGMII)) > + return PCCR9_QSGMIIa_MASK; > + break; > + case 0xB: > + if (mode->protos & PROTO_MASK(XFI)) > + return PCCRB_XFIa_MASK; > + break; > + } > + pr_err("unknown mode PCCR%X %s%c\n", mode->pccr, > + lynx_proto_str[mode->protos], 'A' + mode->idx); > + return 0; > +} > + > +/** > + * lynx_proto_mode_shift() - Get the shift for a PCCR config > + * @mode: The mode to use > + * > + * Return: The amount of bits to shift the mask. > + */ > +static u32 lynx_proto_mode_shift(const struct lynx_mode *mode) > +{ > + switch (mode->pccr) { > + case 0x0: > + if (mode->protos & PROTO_MASK(PCIE)) > + return PCCR0_PEXa_SHIFT(mode->idx); > + break; > + case 0x2: > + if (mode->protos & PROTO_MASK(SATA)) > + return PCCR2_SATAa_SHIFT(mode->idx); > + break; > + case 0x8: > + if (mode->protos & PROTO_MASK(SGMII)) > + return PCCR8_SGMIIa_SHIFT(mode->idx); > + break; > + case 0x9: > + if (mode->protos & PROTO_MASK(QSGMII)) > + return PCCR9_QSGMIIa_SHIFT(mode->idx); > + break; > + case 0xB: > + if (mode->protos & PROTO_MASK(XFI)) > + return PCCRB_XFIa_SHIFT(mode->idx); > + break; > + } > + pr_err("unknown mode PCCR%X %s%c\n", mode->pccr, > + lynx_proto_str[mode->protos], 'A' + mode->idx); > + return 0; > +} > + > +/** > + * lynx_proto_mode_get() - Get the current config for a PCCR mode > + * @mode: The mode to use > + * @pccr: The current value of the PCCR > + * > + * Return: The current value of the PCCR config for this mode > + */ > +static u32 lynx_proto_mode_get(const struct lynx_mode *mode, u32 pccr) > +{ > + return (pccr >> lynx_proto_mode_shift(mode)) & > + lynx_proto_mode_mask(mode); > +} > + > +/** > + * lynx_proto_mode_prep() - Configure a PCCR for a protocol > + * @mode: The mode to use > + * @pccr: The current value of the PCCR > + * @proto: The protocol to configure > + * > + * This configures a PCCR for a mode and protocol. To disable a mode, pass > + * %LYNX_PROTO_NONE as @proto. If @proto is 1000Base-KX, then the KX bit > + * will be set. > + * > + * Return: The new value for the PCCR > + */ > +static u32 lynx_proto_mode_prep(const struct lynx_mode *mode, u32 pccr, > + enum lynx_protocol proto) > +{ > + u32 shift = lynx_proto_mode_shift(mode); > + > + pccr &= ~(lynx_proto_mode_mask(mode) << shift); > + if (proto != LYNX_PROTO_NONE) > + pccr |= mode->cfg << shift; > + > + if (proto == LYNX_PROTO_1000BASEKX) { > + if (mode->pccr == 8) > + pccr |= PCCR8_SGMIIa_KX << shift; > + else > + pr_err("PCCR%X doesn't have a KX bit\n", mode->pccr); > + } > + > + return pccr; > +} > + > +#define abs_diff(a, b) ({ \ > + typeof(a) _a = (a); \ > + typeof(b) _b = (b); \ > + _a > _b ? _a - _b : _b - _a; \ > +}) > + > +static int lynx_set_mode(struct phy *phy, enum phy_mode phy_mode, int submode) > +{ > + enum lynx_protocol proto; > + const struct lynx_proto_params *params; > + const struct lynx_mode *old_mode = NULL, *new_mode; > + int i, pll, ret; > + struct lynx_group *group = phy_get_drvdata(phy); > + struct lynx_priv *serdes = group->serdes; > + u32 tmp; > + u32 gcr0 = 0, gcr1 = 0, recr0 = 0, tecr0 = 0; > + u32 gcr0_mask = 0, gcr1_mask = 0, recr0_mask = 0, tecr0_mask = 0; > + > + proto = lynx_lookup_proto(phy_mode, submode); > + if (proto == LYNX_PROTO_NONE) { > + dev_dbg(&phy->dev, "unknown mode/submode %d/%d\n", > + phy_mode, submode); > + return -EINVAL; > + } > + > + /* Nothing to do */ > + if (proto == group->proto) > + return 0; > + > + new_mode = lynx_lookup_mode(group, proto); > + if (!new_mode) { > + dev_dbg(&phy->dev, "could not find mode for %s on lanes %u to %u\n", > + lynx_proto_str[proto], group->first_lane, > + group->last_lane); > + return -EINVAL; > + } > + > + if (group->proto != LYNX_PROTO_NONE) { > + old_mode = lynx_lookup_mode(group, group->proto); > + if (!old_mode) { > + dev_err(&phy->dev, "could not find mode for %s\n", > + lynx_proto_str[group->proto]); > + return -EBUSY; > + } > + } > + > + clk_disable_unprepare(group->pll); > + clk_rate_exclusive_put(group->pll); > + group->pll = NULL; > + > + /* First, try to use a PLL which already has the correct rate */ > + params = &lynx_proto_params[proto]; > + for (pll = 0; pll < ARRAY_SIZE(serdes->pll); pll++) { > + struct clk *clk = serdes->pll[pll].hw.clk; > + unsigned long rate = clk_get_rate(clk); > + unsigned long error = abs_diff(rate, params->frate_khz); > + > + dev_dbg(&phy->dev, "pll%d has rate %lu\n", pll, rate); > + /* Accept up to 100ppm deviation */ > + if ((!error || params->frate_khz / error > 10000) && > + !clk_set_rate_exclusive(clk, rate)) > + goto got_pll; > + /* Someone else got a different rate first */ > + } > + > + /* If neither PLL has the right rate, try setting it */ > + for (pll = 0; pll < 2; pll++) { > + ret = clk_set_rate_exclusive(serdes->pll[pll].hw.clk, > + params->frate_khz); > + if (!ret) > + goto got_pll; > + } > + > + dev_dbg(&phy->dev, "could not get a pll at %ukHz\n", > + params->frate_khz); > + return ret; > + > +got_pll: > + group->pll = serdes->pll[pll].hw.clk; > + clk_prepare_enable(group->pll); > + > + gcr0_mask |= LNmGCR0_RRAT_SEL | LNmGCR0_TRAT_SEL; > + gcr0_mask |= LNmGCR0_RPLL_LES | LNmGCR0_TPLL_LES; > + gcr0_mask |= LNmGCR0_RRST_B | LNmGCR0_TRST_B; > + gcr0_mask |= LNmGCR0_RX_PD | LNmGCR0_TX_PD; > + gcr0_mask |= LNmGCR0_IF20BIT_EN | LNmGCR0_PROTS; > + gcr0 |= FIELD_PREP(LNmGCR0_RPLL_LES, !pll); > + gcr0 |= FIELD_PREP(LNmGCR0_TPLL_LES, !pll); > + gcr0 |= FIELD_PREP(LNmGCR0_RRAT_SEL, params->rat_sel); > + gcr0 |= FIELD_PREP(LNmGCR0_TRAT_SEL, params->rat_sel); > + gcr0 |= FIELD_PREP(LNmGCR0_IF20BIT_EN, params->if20bit); > + gcr0 |= FIELD_PREP(LNmGCR0_PROTS, params->prots); > + > + gcr1_mask |= LNmGCR1_RDAT_INV | LNmGCR1_TDAT_INV; > + gcr1_mask |= LNmGCR1_OPAD_CTL | LNmGCR1_REIDL_TH; > + gcr1_mask |= LNmGCR1_REIDL_EX_SEL | LNmGCR1_REIDL_ET_SEL; > + gcr1_mask |= LNmGCR1_REIDL_EX_MSB | LNmGCR1_REIDL_ET_MSB; > + gcr1_mask |= LNmGCR1_REQ_CTL_SNP | LNmGCR1_REQ_CDR_SNP; > + gcr1_mask |= LNmGCR1_TRSTDIR | LNmGCR1_REQ_BIN_SNP; > + gcr1_mask |= LNmGCR1_ISLEW_RCTL | LNmGCR1_OSLEW_RCTL; > + gcr1 |= FIELD_PREP(LNmGCR1_REIDL_TH, params->reidl_th); > + gcr1 |= FIELD_PREP(LNmGCR1_REIDL_EX_SEL, params->reidl_ex & 3); > + gcr1 |= FIELD_PREP(LNmGCR1_REIDL_ET_SEL, params->reidl_et & 3); > + gcr1 |= FIELD_PREP(LNmGCR1_REIDL_EX_MSB, params->reidl_ex >> 2); > + gcr1 |= FIELD_PREP(LNmGCR1_REIDL_ET_MSB, params->reidl_et >> 2); > + gcr1 |= FIELD_PREP(LNmGCR1_TRSTDIR, > + group->first_lane > group->last_lane); > + gcr1 |= FIELD_PREP(LNmGCR1_ISLEW_RCTL, params->slew); > + gcr1 |= FIELD_PREP(LNmGCR1_OSLEW_RCTL, params->slew); > + > + recr0_mask |= LNmRECR0_GK2OVD | LNmRECR0_GK3OVD; > + recr0_mask |= LNmRECR0_GK2OVD_EN | LNmRECR0_GK3OVD_EN; > + recr0_mask |= LNmRECR0_BASE_WAND | LNmRECR0_OSETOVD; > + if (params->gain) { > + recr0 |= FIELD_PREP(LNmRECR0_GK2OVD, params->gain); > + recr0 |= FIELD_PREP(LNmRECR0_GK3OVD, params->gain); > + recr0 |= LNmRECR0_GK2OVD_EN | LNmRECR0_GK3OVD_EN; > + } > + recr0 |= FIELD_PREP(LNmRECR0_BASE_WAND, params->baseline_wander); > + recr0 |= FIELD_PREP(LNmRECR0_OSETOVD, params->offset_override); > + > + tecr0_mask |= LNmTECR0_TEQ_TYPE; > + tecr0_mask |= LNmTECR0_SGN_PREQ | LNmTECR0_RATIO_PREQ; > + tecr0_mask |= LNmTECR0_SGN_POST1Q | LNmTECR0_RATIO_PST1Q; > + tecr0_mask |= LNmTECR0_ADPT_EQ | LNmTECR0_AMP_RED; > + tecr0 |= FIELD_PREP(LNmTECR0_TEQ_TYPE, params->teq); > + if (params->preq_ratio) { > + tecr0 |= FIELD_PREP(LNmTECR0_SGN_PREQ, 1); > + tecr0 |= FIELD_PREP(LNmTECR0_RATIO_PREQ, params->preq_ratio); > + } > + if (params->postq_ratio) { > + tecr0 |= FIELD_PREP(LNmTECR0_SGN_POST1Q, 1); > + tecr0 |= FIELD_PREP(LNmTECR0_RATIO_PST1Q, params->postq_ratio); > + } > + tecr0 |= FIELD_PREP(LNmTECR0_ADPT_EQ, params->adpt_eq); > + tecr0 |= FIELD_PREP(LNmTECR0_AMP_RED, params->amp_red); > + > + mutex_lock(&serdes->lock); > + > + /* Disable the old controller */ > + if (old_mode) { > + tmp = lynx_read(serdes, PCCRn(old_mode->pccr)); > + tmp = lynx_proto_mode_prep(old_mode, tmp, LYNX_PROTO_NONE); > + lynx_write(serdes, tmp, PCCRn(old_mode->pccr)); > + > + if (old_mode->protos & PROTO_MASK(SGMII)) { > + tmp = lynx_read(serdes, SGMIIaCR1(old_mode->idx)); > + tmp &= SGMIIaCR1_SGPCS_EN; > + lynx_write(serdes, tmp, SGMIIaCR1(old_mode->idx)); > + } > + } > + > + for_each_lane_reverse(i, group) { > + tmp = lynx_read(serdes, LNmGCR0(i)); > + tmp &= ~(LNmGCR0_RRST_B | LNmGCR0_TRST_B); > + lynx_write(serdes, tmp, LNmGCR0(i)); > + ndelay(50); > + > + tmp &= ~gcr0_mask; > + tmp |= gcr0; > + tmp |= FIELD_PREP(LNmGCR0_FIRST_LANE, i == group->first_lane); > + lynx_write(serdes, tmp, LNmGCR0(i)); > + > + tmp = lynx_read(serdes, LNmGCR1(i)); > + tmp &= ~gcr1_mask; > + tmp |= gcr1; > + lynx_write(serdes, tmp, LNmGCR1(i)); > + > + tmp = lynx_read(serdes, LNmRECR0(i)); > + tmp &= ~recr0_mask; > + tmp |= recr0; > + lynx_write(serdes, tmp, LNmRECR0(i)); > + > + tmp = lynx_read(serdes, LNmTECR0(i)); > + tmp &= ~tecr0_mask; > + tmp |= tecr0; > + lynx_write(serdes, tmp, LNmTECR0(i)); > + > + tmp = lynx_read(serdes, LNmTTLCR0(i)); > + tmp &= ~LNmTTLCR0_FLT_SEL; > + tmp |= FIELD_PREP(LNmTTLCR0_FLT_SEL, params->flt_sel); > + lynx_write(serdes, tmp, LNmTTLCR0(i)); > + > + ndelay(120); > + tmp = lynx_read(serdes, LNmGCR0(i)); > + tmp |= LNmGCR0_RRST_B | LNmGCR0_TRST_B; > + lynx_write(serdes, tmp, LNmGCR0(i)); > + } > + > + if (proto == LYNX_PROTO_1000BASEKX) { > + /* FIXME: this races with clock updates */ > + tmp = lynx_read(serdes, PLLaCR0(pll)); > + tmp &= ~PLLaCR0_DLYDIV_SEL; > + tmp |= FIELD_PREP(PLLaCR0_DLYDIV_SEL, 1); > + lynx_write(serdes, tmp, PLLaCR0(pll)); > + } > + > + /* Enable the new controller */ > + tmp = lynx_read(serdes, PCCRn(new_mode->pccr)); > + tmp = lynx_proto_mode_prep(new_mode, tmp, proto); > + lynx_write(serdes, tmp, PCCRn(new_mode->pccr)); > + > + if (new_mode->protos & PROTO_MASK(SGMII)) { > + tmp = lynx_read(serdes, SGMIIaCR1(new_mode->idx)); > + tmp |= SGMIIaCR1_SGPCS_EN; > + lynx_write(serdes, tmp, SGMIIaCR1(new_mode->idx)); > + } > + > + mutex_unlock(&serdes->lock); > + > + group->proto = proto; > + dev_dbg(&phy->dev, "set mode to %s on lanes %u to %u\n", > + lynx_proto_str[proto], group->first_lane, group->last_lane); > + return 0; > +} > + > +static void lynx_release(struct phy *phy) > +{ > + struct lynx_group *group = phy_get_drvdata(phy); > + struct lynx_priv *serdes = group->serdes; > + > + mutex_lock(&serdes->lock); > + if (--group->users) { > + mutex_unlock(&serdes->lock); > + return; > + } > + list_del(&group->groups); > + mutex_unlock(&serdes->lock); > + > + phy_destroy(phy); > + kfree(group); > +} > + > +static const struct phy_ops lynx_phy_ops = { > + .init = lynx_init, > + .exit = lynx_exit, > + .power_on = lynx_power_on, > + .power_off = lynx_power_off, > + .set_mode = lynx_set_mode, > + .validate = lynx_validate, > + .release = lynx_release, > + .owner = THIS_MODULE, > +}; > + > +static struct phy *lynx_xlate(struct device *dev, struct of_phandle_args *args) > +{ > + struct phy *phy; > + struct list_head *head; > + struct lynx_group *group; > + struct lynx_priv *serdes = dev_get_drvdata(dev); > + unsigned int last_lane; > + > + if (args->args_count == 1) > + last_lane = args->args[0]; > + else if (args->args_count == 2) > + last_lane = args->args[1]; > + else > + return ERR_PTR(-EINVAL); > + > + mutex_lock(&serdes->lock); > + > + /* Look for an existing group */ > + list_for_each(head, &serdes->groups) { > + group = container_of(head, struct lynx_group, groups); > + if (group->first_lane == args->args[0] && > + group->last_lane == last_lane) { > + group->users++; > + return group->phy; > + } > + } > + > + /* None found, create our own */ > + group = kzalloc(sizeof(*group), GFP_KERNEL); > + if (!group) { > + mutex_unlock(&serdes->lock); > + return ERR_PTR(-ENOMEM); > + } > + > + group->serdes = serdes; > + group->first_lane = args->args[0]; > + group->last_lane = last_lane; > + group->users = 1; > + phy = phy_create(dev, NULL, &lynx_phy_ops); > + if (IS_ERR(phy)) { > + kfree(group); > + } else { > + group->phy = phy; > + phy_set_drvdata(phy, group); > + list_add(&group->groups, &serdes->groups); > + } > + > + mutex_unlock(&serdes->lock); > + return phy; > +} > + > +static int lynx_probe(struct platform_device *pdev) > +{ > + bool grabbed_clocks = false; > + int i, ret; > + struct device *dev = &pdev->dev; > + struct lynx_priv *serdes; > + struct regmap_config regmap_config = {}; > + const struct lynx_conf *conf; > + struct resource *res; > + void __iomem *base; > + > + serdes = devm_kzalloc(dev, sizeof(*serdes), GFP_KERNEL); > + if (!serdes) > + return -ENOMEM; > + platform_set_drvdata(pdev, serdes); > + mutex_init(&serdes->lock); > + INIT_LIST_HEAD(&serdes->groups); > + serdes->dev = dev; > + > + base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); > + if (IS_ERR(base)) { > + ret = PTR_ERR(base); > + dev_err_probe(dev, ret, "could not get/map registers\n"); > + return ret; > + } > + > + conf = device_get_match_data(dev); > + serdes->conf = conf; > + regmap_config.reg_bits = 32; > + regmap_config.reg_stride = 4; > + regmap_config.val_bits = 32; > + regmap_config.val_format_endian = conf->endian; > + regmap_config.max_register = res->end - res->start; > + regmap_config.disable_locking = true; > + serdes->regmap = devm_regmap_init_mmio(dev, base, ®map_config); > + if (IS_ERR(serdes->regmap)) { > + ret = PTR_ERR(serdes->regmap); > + dev_err_probe(dev, ret, "could not create regmap\n"); > + return ret; > + } > + > + for (i = 0; i < ARRAY_SIZE(serdes->ref); i++) { > + static const char fmt[] = "ref%d"; > + char name[sizeof(fmt)]; > + > + snprintf(name, sizeof(name), fmt, i); > + serdes->ref[i] = devm_clk_get(dev, name); > + if (IS_ERR(serdes->ref[i])) { > + ret = PTR_ERR(serdes->ref[i]); > + dev_err_probe(dev, ret, "could not get %s\n", name); > + return ret; > + } > + } > + > + for (i = 0; i < ARRAY_SIZE(serdes->pll); i++) { > + static const char fmt[] = "%s.pll%d"; > + char *name; > + const struct clk_hw *ref_hw[] = { > + __clk_get_hw(serdes->ref[i]), > + }; > + size_t len; > + struct clk_init_data init = {}; > + > + len = snprintf(NULL, 0, fmt, pdev->name, i); > + name = devm_kzalloc(dev, len + 1, GFP_KERNEL); > + if (!name) > + return -ENOMEM; > + > + snprintf(name, len + 1, fmt, pdev->name, i); > + init.name = name; > + init.ops = &lynx_pll_clk_ops; > + init.parent_hws = ref_hw; > + init.num_parents = 1; > + init.flags = CLK_SET_RATE_GATE | CLK_GET_RATE_NOCACHE; > + init.flags |= CLK_SET_RATE_PARENT | CLK_OPS_PARENT_ENABLE; > + > + serdes->pll[i].hw.init = &init; > + serdes->pll[i].serdes = serdes; > + serdes->pll[i].idx = i; > + ret = devm_clk_hw_register(dev, &serdes->pll[i].hw); > + if (ret) { > + dev_err_probe(dev, ret, "could not register %s\n", > + name); > + return ret; > + } > + } > + > + ret = devm_of_clk_add_hw_provider(dev, lynx_clk_get, serdes); > + if (ret) { > + dev_err_probe(dev, ret, "could not register clock provider\n"); > + return ret; > + } > + > + /* Deselect anything configured by the RCW/bootloader */ > + for (i = 0; i < conf->mode_count; i++) { > + const struct lynx_mode *mode = &conf->modes[i]; > + u32 pccr = lynx_read(serdes, PCCRn(mode->pccr)); > + > + if (lynx_proto_mode_get(mode, pccr) == mode->cfg) { > + if (mode->protos & UNSUPPORTED_PROTOS) { > + /* Don't mess with modes we don't support */ > + serdes->used_lanes |= mode->lanes; > + if (grabbed_clocks) > + continue; > + > + grabbed_clocks = true; > + clk_prepare_enable(serdes->pll[0].hw.clk); > + clk_prepare_enable(serdes->pll[1].hw.clk); > + clk_rate_exclusive_get(serdes->pll[0].hw.clk); > + clk_rate_exclusive_get(serdes->pll[1].hw.clk); > + } else { > + /* Otherwise, clear out the existing config */ > + pccr = lynx_proto_mode_prep(mode, pccr, > + LYNX_PROTO_NONE); > + lynx_write(serdes, pccr, PCCRn(mode->pccr)); > + } > + > + /* Disable the SGMII PCS until we're ready for it */ > + if (mode->protos & LYNX_PROTO_SGMII) { > + u32 cr1; > + > + cr1 = lynx_read(serdes, SGMIIaCR1(mode->idx)); > + cr1 &= ~SGMIIaCR1_SGPCS_EN; > + lynx_write(serdes, cr1, SGMIIaCR1(mode->idx)); > + } > + } > + } > + > + /* Power off all lanes; used ones will be powered on later */ > + for (i = 0; i < conf->lanes; i++) > + lynx_power_off_lane(serdes, i); > + > + ret = PTR_ERR_OR_ZERO(devm_of_phy_provider_register(dev, lynx_xlate)); > + if (ret) > + dev_err_probe(dev, ret, "could not register phy provider\n"); > + else > + dev_info(dev, "probed with %d lanes\n", conf->lanes); > + return ret; > +} > + > +/* > + * XXX: For SerDes1, lane A uses pins SD1_RX3_P/N! That is, the lane numbers > + * and pin numbers are _reversed_. In addition, the PCCR documentation is > + * _inconsistent_ in its usage of these terms! > + * > + * PCCR "Lane 0" refers to... > + * ==== ===================== > + * 0 Lane A > + * 2 Lane A > + * 8 Lane A > + * 9 Lane A > + * B Lane D! > + */ > +static const struct lynx_mode ls1046a_modes1[] = { > + CONF_SINGLE(1, PCIE, 0x0, 1, 0b001), /* PCIe.1 x1 */ > + CONF_1000BASEKX(0, 0x8, 0, 0b001), /* SGMII.6 */ > + CONF_SGMII25KX(1, 0x8, 1, 0b001), /* SGMII.5 */ > + CONF_SGMII25KX(2, 0x8, 2, 0b001), /* SGMII.10 */ > + CONF_SGMII25KX(3, 0x8, 3, 0b001), /* SGMII.9 */ > + CONF_SINGLE(1, QSGMII, 0x9, 2, 0b001), /* QSGMII.6,5,10,1 */ > + CONF_XFI(2, 0xB, 0, 0b010), /* XFI.10 */ > + CONF_XFI(3, 0xB, 1, 0b001), /* XFI.9 */ > +}; > + > +static const struct lynx_conf ls1046a_conf1 = { > + .modes = ls1046a_modes1, > + .mode_count = ARRAY_SIZE(ls1046a_modes1), > + .lanes = 4, > + .endian = REGMAP_ENDIAN_BIG, > +}; > + > +static const struct lynx_mode ls1046a_modes2[] = { > + CONF_SINGLE(0, PCIE, 0x0, 0, 0b001), /* PCIe.1 x1 */ > + CONF(GENMASK(3, 0), PROTO_MASK(PCIE), 0x0, 0, 0b011), /* PCIe.1 x4 */ > + CONF_SINGLE(2, PCIE, 0x0, 2, 0b001), /* PCIe.2 x1 */ > + CONF(GENMASK(3, 2), PROTO_MASK(PCIE), 0x0, 2, 0b010), /* PCIe.3 x2 */ > + CONF_SINGLE(3, PCIE, 0x0, 2, 0b011), /* PCIe.3 x1 */ > + CONF_SINGLE(3, SATA, 0x2, 0, 0b001), /* SATA */ > + CONF_1000BASEKX(1, 0x8, 1, 0b001), /* SGMII.2 */ > +}; > + > +static const struct lynx_conf ls1046a_conf2 = { > + .modes = ls1046a_modes2, > + .mode_count = ARRAY_SIZE(ls1046a_modes2), > + .lanes = 4, > + .endian = REGMAP_ENDIAN_BIG, > +}; > + > +static const struct of_device_id lynx_of_match[] = { > + { .compatible = "fsl,ls1046a-serdes-1", .data = &ls1046a_conf1 }, > + { .compatible = "fsl,ls1046a-serdes-2", .data = &ls1046a_conf2 }, > +}; > +MODULE_DEVICE_TABLE(of, lynx_of_match); > + > +static struct platform_driver lynx_driver = { > + .probe = lynx_probe, > + .driver = { > + .name = "qoriq_serdes", > + .of_match_table = lynx_of_match, > + }, > +}; > +module_platform_driver(lynx_driver); > + > +MODULE_AUTHOR("Sean Anderson <sean.anderson@xxxxxxxx>"); > +MODULE_DESCRIPTION("Lynx 10G SerDes driver"); > +MODULE_LICENSE("GPL"); > -- > 2.35.1.1320.gc452695387.dirty -- ~Vinod