| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * OPEN Alliance 10BASE‑T1x MAC‑PHY Serial Interface framework | |
| * | |
| * Author: Parthiban Veerasooran <parthiban.veerasooran@microchip.com> | |
| */ | |
| #include <linux/bitfield.h> | |
| #include <linux/iopoll.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/mdio.h> | |
| #include <linux/phy.h> | |
| #include <linux/oa_tc6.h> | |
| /* Control command header */ | |
| #define OA_TC6_CTRL_HEADER_DATA_NOT_CTRL BIT(31) | |
| #define OA_TC6_CTRL_HEADER_WRITE_NOT_READ BIT(29) | |
| #define OA_TC6_CTRL_HEADER_MEM_MAP_SELECTOR GENMASK(27, 24) | |
| #define OA_TC6_CTRL_HEADER_ADDR GENMASK(23, 8) | |
| #define OA_TC6_CTRL_HEADER_LENGTH GENMASK(7, 1) | |
| #define OA_TC6_CTRL_HEADER_PARITY BIT(0) | |
| /* Data header */ | |
| #define OA_TC6_DATA_HEADER_DATA_NOT_CTRL BIT(31) | |
| #define OA_TC6_DATA_HEADER_DATA_VALID BIT(21) | |
| #define OA_TC6_DATA_HEADER_START_VALID BIT(20) | |
| #define OA_TC6_DATA_HEADER_START_WORD_OFFSET GENMASK(19, 16) | |
| #define OA_TC6_DATA_HEADER_END_VALID BIT(14) | |
| #define OA_TC6_DATA_HEADER_END_BYTE_OFFSET GENMASK(13, 8) | |
| #define OA_TC6_DATA_HEADER_PARITY BIT(0) | |
| /* Data footer */ | |
| #define OA_TC6_DATA_FOOTER_EXTENDED_STS BIT(31) | |
| #define OA_TC6_DATA_FOOTER_RXD_HEADER_BAD BIT(30) | |
| #define OA_TC6_DATA_FOOTER_CONFIG_SYNC BIT(29) | |
| #define OA_TC6_DATA_FOOTER_RX_CHUNKS GENMASK(28, 24) | |
| #define OA_TC6_DATA_FOOTER_DATA_VALID BIT(21) | |
| #define OA_TC6_DATA_FOOTER_START_VALID BIT(20) | |
| #define OA_TC6_DATA_FOOTER_START_WORD_OFFSET GENMASK(19, 16) | |
| #define OA_TC6_DATA_FOOTER_END_VALID BIT(14) | |
| #define OA_TC6_DATA_FOOTER_END_BYTE_OFFSET GENMASK(13, 8) | |
| #define OA_TC6_DATA_FOOTER_TX_CREDITS GENMASK(5, 1) | |
| #define OA_TC6_CTRL_PROT_REPLY_SIZE 4 | |
| #define OA_TC6_CTRL_HEADER_SIZE 4 | |
| #define OA_TC6_CTRL_REG_VALUE_SIZE 4 | |
| #define OA_TC6_CTRL_IGNORED_SIZE 4 | |
| #define OA_TC6_CTRL_MAX_REGISTERS 128 | |
| #define OA_TC6_CTRL_SPI_BUF_SIZE (OA_TC6_CTRL_HEADER_SIZE +\ | |
| (OA_TC6_CTRL_MAX_REGISTERS *\ | |
| (OA_TC6_CTRL_REG_VALUE_SIZE +\ | |
| OA_TC6_CTRL_PROT_REPLY_SIZE)) +\ | |
| OA_TC6_CTRL_IGNORED_SIZE) | |
| #define OA_TC6_CHUNK_PAYLOAD_SIZE 64 | |
| #define OA_TC6_DATA_HEADER_SIZE 4 | |
| #define OA_TC6_CHUNK_SIZE (OA_TC6_DATA_HEADER_SIZE +\ | |
| OA_TC6_CHUNK_PAYLOAD_SIZE) | |
| #define OA_TC6_MAX_TX_CHUNKS 48 | |
| #define OA_TC6_SPI_DATA_BUF_SIZE (OA_TC6_MAX_TX_CHUNKS *\ | |
| OA_TC6_CHUNK_SIZE) | |
| #define STATUS0_RESETC_POLL_DELAY 1000 | |
| #define STATUS0_RESETC_POLL_TIMEOUT 1000000 | |
| #define OA_TC6_REG_MMS_MASK GENMASK(19, 16) | |
| /* Internal structure for MAC-PHY drivers */ | |
| struct oa_tc6 { | |
| struct net_device *netdev; | |
| struct phy_device *phydev; | |
| struct mii_bus *mdiobus; | |
| struct spi_device *spi; | |
| struct mutex spi_ctrl_lock; /* Protects spi control transfer */ | |
| spinlock_t tx_skb_lock; /* Protects tx skb handling */ | |
| void *spi_ctrl_tx_buf; | |
| void *spi_ctrl_rx_buf; | |
| void *spi_data_tx_buf; | |
| void *spi_data_rx_buf; | |
| struct sk_buff *ongoing_tx_skb; | |
| struct sk_buff *waiting_tx_skb; | |
| struct sk_buff *rx_skb; | |
| u16 tx_skb_offset; | |
| u16 spi_data_tx_buf_offset; | |
| u16 tx_credits; | |
| u8 rx_chunks_available; | |
| bool wait_until_start_valid; | |
| bool int_flag; | |
| bool disable_traffic; | |
| bool prot_ctrl; | |
| enum oa_tc6_quirk_flag quirk_flags; | |
| }; | |
| enum oa_tc6_header_type { | |
| OA_TC6_CTRL_HEADER, | |
| OA_TC6_DATA_HEADER, | |
| }; | |
| enum oa_tc6_register_op { | |
| OA_TC6_CTRL_REG_READ = 0, | |
| OA_TC6_CTRL_REG_WRITE = 1, | |
| }; | |
| enum oa_tc6_data_valid_info { | |
| OA_TC6_DATA_INVALID, | |
| OA_TC6_DATA_VALID, | |
| }; | |
| enum oa_tc6_data_start_valid_info { | |
| OA_TC6_DATA_START_INVALID, | |
| OA_TC6_DATA_START_VALID, | |
| }; | |
| enum oa_tc6_data_end_valid_info { | |
| OA_TC6_DATA_END_INVALID, | |
| OA_TC6_DATA_END_VALID, | |
| }; | |
| static int oa_tc6_spi_transfer(struct oa_tc6 *tc6, | |
| enum oa_tc6_header_type header_type, u16 length) | |
| { | |
| struct spi_transfer xfer = { 0 }; | |
| struct spi_message msg; | |
| if (header_type == OA_TC6_DATA_HEADER) { | |
| xfer.tx_buf = tc6->spi_data_tx_buf; | |
| xfer.rx_buf = tc6->spi_data_rx_buf; | |
| } else { | |
| xfer.tx_buf = tc6->spi_ctrl_tx_buf; | |
| xfer.rx_buf = tc6->spi_ctrl_rx_buf; | |
| } | |
| xfer.len = length; | |
| spi_message_init(&msg); | |
| spi_message_add_tail(&xfer, &msg); | |
| return spi_sync(tc6->spi, &msg); | |
| } | |
| static int oa_tc6_get_parity(u32 p) | |
| { | |
| /* Public domain code snippet, lifted from | |
| * http://www-graphics.stanford.edu/~seander/bithacks.html | |
| */ | |
| p ^= p >> 1; | |
| p ^= p >> 2; | |
| p = (p & 0x11111111U) * 0x11111111U; | |
| /* Odd parity is used here */ | |
| return !((p >> 28) & 1); | |
| } | |
| static __be32 oa_tc6_prepare_ctrl_header(u32 addr, u8 length, | |
| enum oa_tc6_register_op reg_op) | |
| { | |
| u32 header; | |
| header = FIELD_PREP(OA_TC6_CTRL_HEADER_DATA_NOT_CTRL, | |
| OA_TC6_CTRL_HEADER) | | |
| FIELD_PREP(OA_TC6_CTRL_HEADER_WRITE_NOT_READ, reg_op) | | |
| FIELD_PREP(OA_TC6_CTRL_HEADER_MEM_MAP_SELECTOR, addr >> 16) | | |
| FIELD_PREP(OA_TC6_CTRL_HEADER_ADDR, addr) | | |
| FIELD_PREP(OA_TC6_CTRL_HEADER_LENGTH, length - 1); | |
| header |= FIELD_PREP(OA_TC6_CTRL_HEADER_PARITY, | |
| oa_tc6_get_parity(header)); | |
| return cpu_to_be32(header); | |
| } | |
| static void oa_tc6_update_ctrl_write_data(struct oa_tc6 *tc6, u32 value[], | |
| u8 length) | |
| { | |
| __be32 *tx_buf = tc6->spi_ctrl_tx_buf + OA_TC6_CTRL_HEADER_SIZE; | |
| for (int i = 0; i < length; i++) { | |
| *tx_buf++ = cpu_to_be32(value[i]); | |
| if (tc6->prot_ctrl) | |
| *tx_buf++ = cpu_to_be32(~value[i]); | |
| } | |
| } | |
| static u16 oa_tc6_calculate_ctrl_buf_size(u8 length, bool ctrl_prot) | |
| { | |
| u32 reply_size = OA_TC6_CTRL_REG_VALUE_SIZE; | |
| if (ctrl_prot) | |
| reply_size += OA_TC6_CTRL_PROT_REPLY_SIZE; | |
| /* Control command consists 4 bytes header + 4 bytes register value for | |
| * each register (+ 4 bytes for the register value complement in case | |
| * protected mode is used) + 4 bytes ignored value. | |
| */ | |
| return OA_TC6_CTRL_HEADER_SIZE + reply_size * length + | |
| OA_TC6_CTRL_IGNORED_SIZE; | |
| } | |
| static void oa_tc6_prepare_ctrl_spi_buf(struct oa_tc6 *tc6, u32 address, | |
| u32 value[], u8 length, | |
| enum oa_tc6_register_op reg_op, | |
| u16 buf_size) | |
| { | |
| __be32 *tx_buf = tc6->spi_ctrl_tx_buf; | |
| memset(tx_buf, 0, buf_size); | |
| *tx_buf = oa_tc6_prepare_ctrl_header(address, length, reg_op); | |
| if (reg_op == OA_TC6_CTRL_REG_WRITE) | |
| oa_tc6_update_ctrl_write_data(tc6, value, length); | |
| } | |
| static int oa_tc6_check_ctrl_write_reply(struct oa_tc6 *tc6, u8 size) | |
| { | |
| u8 *tx_buf = tc6->spi_ctrl_tx_buf; | |
| u8 *rx_buf = tc6->spi_ctrl_rx_buf; | |
| rx_buf += OA_TC6_CTRL_IGNORED_SIZE; | |
| /* The echoed control write must match with the one that was | |
| * transmitted. | |
| */ | |
| if (memcmp(tx_buf, rx_buf, size - OA_TC6_CTRL_IGNORED_SIZE)) | |
| return -EPROTO; | |
| return 0; | |
| } | |
| static int oa_tc6_check_ctrl_read_reply(struct oa_tc6 *tc6, u8 length) | |
| { | |
| __be32 *rx_buf = tc6->spi_ctrl_rx_buf + OA_TC6_CTRL_IGNORED_SIZE; | |
| __be32 *tx_buf = tc6->spi_ctrl_tx_buf; | |
| u32 complement; | |
| u32 reply; | |
| /* The echoed control read header must match with the one that was | |
| * transmitted. | |
| */ | |
| if (*tx_buf != *rx_buf) | |
| return -EPROTO; | |
| if (tc6->prot_ctrl) { | |
| /* Skip past the echoed header to the value/complement pairs */ | |
| rx_buf += 1; | |
| for (int i = 0; i < length; i++) { | |
| reply = be32_to_cpu(rx_buf[0]); | |
| complement = be32_to_cpu(rx_buf[1]); | |
| if (complement != ~reply) | |
| return -EPROTO; | |
| rx_buf += 2; | |
| } | |
| } | |
| return 0; | |
| } | |
| static void oa_tc6_copy_ctrl_read_data(struct oa_tc6 *tc6, u32 value[], | |
| u8 length) | |
| { | |
| __be32 *rx_buf = tc6->spi_ctrl_rx_buf + OA_TC6_CTRL_IGNORED_SIZE + | |
| OA_TC6_CTRL_HEADER_SIZE; | |
| for (int i = 0; i < length; i++) { | |
| value[i] = be32_to_cpu(*rx_buf++); | |
| /* skip complement word */ | |
| if (tc6->prot_ctrl) | |
| rx_buf++; | |
| } | |
| } | |
| static int oa_tc6_perform_ctrl(struct oa_tc6 *tc6, u32 address, u32 value[], | |
| u8 length, enum oa_tc6_register_op reg_op) | |
| { | |
| u16 size; | |
| int ret; | |
| size = oa_tc6_calculate_ctrl_buf_size(length, tc6->prot_ctrl); | |
| /* Prepare control command and copy to SPI control buffer */ | |
| oa_tc6_prepare_ctrl_spi_buf(tc6, address, value, length, reg_op, size); | |
| /* Perform SPI transfer */ | |
| ret = oa_tc6_spi_transfer(tc6, OA_TC6_CTRL_HEADER, size); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, "SPI transfer failed for control: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| /* Check echoed/received control write command reply for errors */ | |
| if (reg_op == OA_TC6_CTRL_REG_WRITE) | |
| return oa_tc6_check_ctrl_write_reply(tc6, size); | |
| /* Check echoed/received control read command reply for errors */ | |
| ret = oa_tc6_check_ctrl_read_reply(tc6, length); | |
| if (ret) | |
| return ret; | |
| oa_tc6_copy_ctrl_read_data(tc6, value, length); | |
| return 0; | |
| } | |
| /** | |
| * oa_tc6_read_registers - function for reading multiple consecutive registers. | |
| * @tc6: oa_tc6 struct. | |
| * @address: address of the first register to be read in the MAC-PHY. | |
| * @value: values to be read from the starting register address @address. | |
| * @length: number of consecutive registers to be read from @address. | |
| * | |
| * Maximum of 128 consecutive registers can be read starting at @address. | |
| * | |
| * Return: 0 on success otherwise failed. | |
| */ | |
| int oa_tc6_read_registers(struct oa_tc6 *tc6, u32 address, u32 value[], | |
| u8 length) | |
| { | |
| int ret; | |
| if (!length || length > OA_TC6_CTRL_MAX_REGISTERS) { | |
| dev_err(&tc6->spi->dev, "Invalid register length parameter\n"); | |
| return -EINVAL; | |
| } | |
| mutex_lock(&tc6->spi_ctrl_lock); | |
| ret = oa_tc6_perform_ctrl(tc6, address, value, length, | |
| OA_TC6_CTRL_REG_READ); | |
| mutex_unlock(&tc6->spi_ctrl_lock); | |
| return ret; | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_read_registers); | |
| /** | |
| * oa_tc6_read_register - function for reading a MAC-PHY register. | |
| * @tc6: oa_tc6 struct. | |
| * @address: register address of the MAC-PHY to be read. | |
| * @value: value read from the @address register address of the MAC-PHY. | |
| * | |
| * Return: 0 on success otherwise failed. | |
| */ | |
| int oa_tc6_read_register(struct oa_tc6 *tc6, u32 address, u32 *value) | |
| { | |
| return oa_tc6_read_registers(tc6, address, value, 1); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_read_register); | |
| /** | |
| * oa_tc6_read_register_mms - function for reading a MAC-PHY register in a | |
| * specified memory map. | |
| * @tc6: oa_tc6 struct. | |
| * @mms: Memory map selector for the register. | |
| * @address: register address of the MAC-PHY to be read. | |
| * @value: value read from the @address register address of the MAC-PHY. | |
| * | |
| * Return: 0 on success or a negative error code on failure. | |
| */ | |
| int oa_tc6_read_register_mms(struct oa_tc6 *tc6, u8 mms, u16 address, | |
| u32 *value) | |
| { | |
| u32 mms_reg; | |
| mms_reg = FIELD_PREP(OA_TC6_REG_MMS_MASK, mms) | address; | |
| return oa_tc6_read_registers(tc6, mms_reg, value, 1); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_read_register_mms); | |
| /** | |
| * oa_tc6_write_registers - function for writing multiple consecutive registers. | |
| * @tc6: oa_tc6 struct. | |
| * @address: address of the first register to be written in the MAC-PHY. | |
| * @value: values to be written from the starting register address @address. | |
| * @length: number of consecutive registers to be written from @address. | |
| * | |
| * Maximum of 128 consecutive registers can be written starting at @address. | |
| * | |
| * Return: 0 on success otherwise failed. | |
| */ | |
| int oa_tc6_write_registers(struct oa_tc6 *tc6, u32 address, u32 value[], | |
| u8 length) | |
| { | |
| int ret; | |
| if (!length || length > OA_TC6_CTRL_MAX_REGISTERS) { | |
| dev_err(&tc6->spi->dev, "Invalid register length parameter\n"); | |
| return -EINVAL; | |
| } | |
| mutex_lock(&tc6->spi_ctrl_lock); | |
| ret = oa_tc6_perform_ctrl(tc6, address, value, length, | |
| OA_TC6_CTRL_REG_WRITE); | |
| mutex_unlock(&tc6->spi_ctrl_lock); | |
| return ret; | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_write_registers); | |
| /** | |
| * oa_tc6_write_register - function for writing a MAC-PHY register. | |
| * @tc6: oa_tc6 struct. | |
| * @address: register address of the MAC-PHY to be written. | |
| * @value: value to be written in the @address register address of the MAC-PHY. | |
| * | |
| * Return: 0 on success otherwise failed. | |
| */ | |
| int oa_tc6_write_register(struct oa_tc6 *tc6, u32 address, u32 value) | |
| { | |
| return oa_tc6_write_registers(tc6, address, &value, 1); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_write_register); | |
| /** | |
| * oa_tc6_write_register_mms - function for writing a MAC-PHY register in a | |
| * specified memory map. | |
| * @tc6: oa_tc6 struct. | |
| * @mms: Memory map selector for the register. | |
| * @address: register address of the MAC-PHY to be written. | |
| * @value: value to be written in the @address register address of the MAC-PHY. | |
| * | |
| * Return: 0 on success or a negative error code on failure. | |
| */ | |
| int oa_tc6_write_register_mms(struct oa_tc6 *tc6, u8 mms, u16 address, | |
| u32 value) | |
| { | |
| u32 mms_reg; | |
| mms_reg = FIELD_PREP(OA_TC6_REG_MMS_MASK, mms) | address; | |
| return oa_tc6_write_registers(tc6, mms_reg, &value, 1); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_write_register_mms); | |
| static int oa_tc6_check_phy_reg_direct_access_capability(struct oa_tc6 *tc6) | |
| { | |
| u32 regval; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_STDCAP, ®val); | |
| if (ret) | |
| return ret; | |
| if (!(regval & OA_TC6_STDCAP_DIRECT_PHY_REG_ACCESS)) | |
| return -ENODEV; | |
| return 0; | |
| } | |
| static void oa_tc6_handle_link_change(struct net_device *netdev) | |
| { | |
| phy_print_status(netdev->phydev); | |
| } | |
| static int oa_tc6_mdiobus_read(struct mii_bus *bus, int addr, int regnum) | |
| { | |
| struct oa_tc6 *tc6 = bus->priv; | |
| u32 regval; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_PHY_STD_REG_ADDR_BASE | | |
| (regnum & OA_TC6_PHY_STD_REG_ADDR_MASK), | |
| ®val); | |
| if (ret) | |
| return ret; | |
| return regval; | |
| } | |
| static int oa_tc6_mdiobus_write(struct mii_bus *bus, int addr, int regnum, | |
| u16 val) | |
| { | |
| struct oa_tc6 *tc6 = bus->priv; | |
| return oa_tc6_write_register(tc6, OA_TC6_PHY_STD_REG_ADDR_BASE | | |
| (regnum & OA_TC6_PHY_STD_REG_ADDR_MASK), | |
| val); | |
| } | |
| static int oa_tc6_get_phy_c45_mms(int devnum) | |
| { | |
| switch (devnum) { | |
| case MDIO_MMD_PCS: | |
| return OA_TC6_PHY_C45_PCS_MMS2; | |
| case MDIO_MMD_PMAPMD: | |
| return OA_TC6_PHY_C45_PMA_PMD_MMS3; | |
| case MDIO_MMD_VEND2: | |
| return OA_TC6_PHY_C45_VS_PLCA_MMS4; | |
| case MDIO_MMD_AN: | |
| return OA_TC6_PHY_C45_AUTO_NEG_MMS5; | |
| case MDIO_MMD_POWER_UNIT: | |
| return OA_TC6_PHY_C45_POWER_UNIT_MMS6; | |
| default: | |
| return -EOPNOTSUPP; | |
| } | |
| } | |
| int oa_tc6_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum, | |
| int regnum) | |
| { | |
| struct oa_tc6 *tc6 = bus->priv; | |
| u32 regval; | |
| int mms; | |
| int ret; | |
| mms = oa_tc6_get_phy_c45_mms(devnum); | |
| if (mms < 0) | |
| return mms; | |
| ret = oa_tc6_read_register_mms(tc6, mms, regnum, ®val); | |
| if (ret) | |
| return ret; | |
| return regval; | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_mdiobus_read_c45); | |
| int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum, | |
| int regnum, u16 val) | |
| { | |
| struct oa_tc6 *tc6 = bus->priv; | |
| int mms; | |
| mms = oa_tc6_get_phy_c45_mms(devnum); | |
| if (mms < 0) | |
| return mms; | |
| return oa_tc6_write_register_mms(tc6, mms, regnum, val); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_mdiobus_write_c45); | |
| static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6) | |
| { | |
| int ret; | |
| tc6->mdiobus = mdiobus_alloc(); | |
| if (!tc6->mdiobus) { | |
| netdev_err(tc6->netdev, "MDIO bus alloc failed\n"); | |
| return -ENOMEM; | |
| } | |
| tc6->mdiobus->priv = tc6; | |
| tc6->mdiobus->read = oa_tc6_mdiobus_read; | |
| tc6->mdiobus->write = oa_tc6_mdiobus_write; | |
| /* OPEN Alliance 10BASE-T1x compliance MAC-PHYs will have both C22 and | |
| * C45 registers space. If the PHY is discovered via C22 bus protocol it | |
| * assumes it uses C22 protocol and always uses C22 registers indirect | |
| * access to access C45 registers. This is because, we don't have a | |
| * clean separation between C22/C45 register space and C22/C45 MDIO bus | |
| * protocols. Resulting, PHY C45 registers direct access can't be used | |
| * which can save multiple SPI bus access. To support this feature, PHY | |
| * drivers can set .read_mmd/.write_mmd in the PHY driver to call | |
| * .read_c45/.write_c45. Ex: drivers/net/phy/microchip_t1s.c | |
| */ | |
| tc6->mdiobus->read_c45 = oa_tc6_mdiobus_read_c45; | |
| tc6->mdiobus->write_c45 = oa_tc6_mdiobus_write_c45; | |
| tc6->mdiobus->name = "oa-tc6-mdiobus"; | |
| tc6->mdiobus->parent = &tc6->spi->dev; | |
| snprintf(tc6->mdiobus->id, ARRAY_SIZE(tc6->mdiobus->id), "%s", | |
| dev_name(&tc6->spi->dev)); | |
| ret = mdiobus_register(tc6->mdiobus); | |
| if (ret) { | |
| netdev_err(tc6->netdev, "Could not register MDIO bus\n"); | |
| mdiobus_free(tc6->mdiobus); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static void oa_tc6_mdiobus_unregister(struct oa_tc6 *tc6) | |
| { | |
| mdiobus_unregister(tc6->mdiobus); | |
| mdiobus_free(tc6->mdiobus); | |
| } | |
| static int oa_tc6_phy_init(struct oa_tc6 *tc6) | |
| { | |
| int ret; | |
| if (tc6->quirk_flags & OA_TC6_BROKEN_PHY) | |
| return 0; | |
| ret = oa_tc6_check_phy_reg_direct_access_capability(tc6); | |
| if (ret) { | |
| netdev_err(tc6->netdev, | |
| "Direct PHY register access is not supported by the MAC-PHY\n"); | |
| return ret; | |
| } | |
| ret = oa_tc6_mdiobus_register(tc6); | |
| if (ret) | |
| return ret; | |
| tc6->phydev = phy_find_first(tc6->mdiobus); | |
| if (!tc6->phydev) { | |
| netdev_err(tc6->netdev, "No PHY found\n"); | |
| oa_tc6_mdiobus_unregister(tc6); | |
| return -ENODEV; | |
| } | |
| tc6->phydev->is_internal = true; | |
| ret = phy_connect_direct(tc6->netdev, tc6->phydev, | |
| &oa_tc6_handle_link_change, | |
| PHY_INTERFACE_MODE_INTERNAL); | |
| if (ret) { | |
| netdev_err(tc6->netdev, "Can't attach PHY to %s\n", | |
| tc6->mdiobus->id); | |
| oa_tc6_mdiobus_unregister(tc6); | |
| return ret; | |
| } | |
| phy_attached_info(tc6->netdev->phydev); | |
| return 0; | |
| } | |
| static void oa_tc6_phy_exit(struct oa_tc6 *tc6) | |
| { | |
| if (tc6->quirk_flags & OA_TC6_BROKEN_PHY) | |
| return; | |
| phy_disconnect(tc6->phydev); | |
| oa_tc6_mdiobus_unregister(tc6); | |
| } | |
| static int oa_tc6_read_status0(struct oa_tc6 *tc6) | |
| { | |
| u32 regval; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_STATUS0, ®val); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, "STATUS0 register read failed: %d\n", | |
| ret); | |
| return 0; | |
| } | |
| return regval; | |
| } | |
| static int oa_tc6_sw_reset_macphy(struct oa_tc6 *tc6) | |
| { | |
| u32 regval = OA_TC6_RESET_SWRESET; | |
| int ret; | |
| ret = oa_tc6_write_register(tc6, OA_TC6_REG_RESET, regval); | |
| if (ret) | |
| return ret; | |
| /* Poll for soft reset complete for every 1ms until 1s timeout */ | |
| ret = readx_poll_timeout(oa_tc6_read_status0, tc6, regval, | |
| regval & OA_TC6_STATUS0_RESETC, | |
| STATUS0_RESETC_POLL_DELAY, | |
| STATUS0_RESETC_POLL_TIMEOUT); | |
| if (ret) | |
| return -ENODEV; | |
| /* Clear the reset complete status */ | |
| return oa_tc6_write_register(tc6, OA_TC6_REG_STATUS0, regval); | |
| } | |
| static int oa_tc6_unmask_macphy_error_interrupts(struct oa_tc6 *tc6) | |
| { | |
| u32 regval; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_INT_MASK0, ®val); | |
| if (ret) | |
| return ret; | |
| regval &= ~(OA_TC6_INT_MASK0_TX_PROTOCOL_ERR_MASK | | |
| OA_TC6_INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK | | |
| OA_TC6_INT_MASK0_LOSS_OF_FRAME_ERR_MASK | | |
| OA_TC6_INT_MASK0_HEADER_ERR_MASK); | |
| return oa_tc6_write_register(tc6, OA_TC6_REG_INT_MASK0, regval); | |
| } | |
| static int oa_tc6_enable_data_transfer(struct oa_tc6 *tc6) | |
| { | |
| u32 value; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_CONFIG0, &value); | |
| if (ret) | |
| return ret; | |
| /* Enable configuration synchronization for data transfer */ | |
| value |= OA_TC6_CONFIG0_SYNC; | |
| return oa_tc6_write_register(tc6, OA_TC6_REG_CONFIG0, value); | |
| } | |
| /* Called when a frame that is meant to be transmitted, is dropped. */ | |
| static void oa_tc6_drop_tx_skb(struct oa_tc6 *tc6, struct sk_buff *skb) | |
| { | |
| if (skb) { | |
| tc6->netdev->stats.tx_dropped++; | |
| dev_kfree_skb_any(skb); | |
| } | |
| } | |
| static struct sk_buff *oa_tc6_detach_waiting_tx_skb(struct oa_tc6 *tc6) | |
| { | |
| struct sk_buff *skb; | |
| lockdep_assert_held(&tc6->tx_skb_lock); | |
| skb = tc6->waiting_tx_skb; | |
| tc6->waiting_tx_skb = NULL; | |
| return skb; | |
| } | |
| static void oa_tc6_cleanup_ongoing_rx_skb(struct oa_tc6 *tc6) | |
| { | |
| if (tc6->rx_skb) { | |
| tc6->netdev->stats.rx_dropped++; | |
| kfree_skb(tc6->rx_skb); | |
| tc6->rx_skb = NULL; | |
| } | |
| } | |
| static void oa_tc6_cleanup_ongoing_tx_skb(struct oa_tc6 *tc6) | |
| { | |
| oa_tc6_drop_tx_skb(tc6, tc6->ongoing_tx_skb); | |
| tc6->ongoing_tx_skb = NULL; | |
| } | |
| static void oa_tc6_cleanup_waiting_tx_skb(struct oa_tc6 *tc6) | |
| { | |
| struct sk_buff *skb; | |
| spin_lock_bh(&tc6->tx_skb_lock); | |
| skb = oa_tc6_detach_waiting_tx_skb(tc6); | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| oa_tc6_drop_tx_skb(tc6, skb); | |
| } | |
| static void oa_tc6_free_ongoing_skbs(struct oa_tc6 *tc6) | |
| { | |
| oa_tc6_cleanup_ongoing_tx_skb(tc6); | |
| oa_tc6_cleanup_ongoing_rx_skb(tc6); | |
| } | |
| static void oa_tc6_free_pending_skbs(struct oa_tc6 *tc6) | |
| { | |
| oa_tc6_free_ongoing_skbs(tc6); | |
| oa_tc6_cleanup_waiting_tx_skb(tc6); | |
| } | |
| static void oa_tc6_look_for_new_frame(struct oa_tc6 *tc6) | |
| { | |
| tc6->wait_until_start_valid = true; | |
| oa_tc6_cleanup_ongoing_rx_skb(tc6); | |
| } | |
| /* If the failure is at SPI interface level, masking and clearing | |
| * the interrupt of the device won't work. Since SPI interrupt is | |
| * disabled, it should stop the repeated interrupts. | |
| */ | |
| static void oa_tc6_disable_traffic(struct oa_tc6 *tc6) | |
| { | |
| u32 regval = OA_TC6_INT_MASK0_ALL_INTERRUPTS; | |
| struct sk_buff *skb; | |
| spin_lock_bh(&tc6->tx_skb_lock); | |
| tc6->disable_traffic = true; | |
| skb = oa_tc6_detach_waiting_tx_skb(tc6); | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| /* disable_traffic, when set, is a point of no return to | |
| * working state. Keeping the TX queues disabled. | |
| */ | |
| netif_tx_disable(tc6->netdev); | |
| oa_tc6_drop_tx_skb(tc6, skb); | |
| oa_tc6_free_ongoing_skbs(tc6); | |
| oa_tc6_write_register(tc6, OA_TC6_REG_INT_MASK0, regval); | |
| oa_tc6_read_register(tc6, OA_TC6_REG_STATUS0, ®val); | |
| oa_tc6_write_register(tc6, OA_TC6_REG_STATUS0, regval); | |
| dev_err(&tc6->spi->dev, "Device interrupt disabled to avoid interrupt storm"); | |
| } | |
| static int oa_tc6_process_extended_status(struct oa_tc6 *tc6) | |
| { | |
| u32 value; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_STATUS0, &value); | |
| if (ret) { | |
| netdev_err(tc6->netdev, "STATUS0 register read failed: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| /* This function is called for each chunk received in a given SPI | |
| * transaction. In case, extended status bit is set in more than | |
| * one chunk, skip the write, if status0 is already cleared. | |
| */ | |
| if (!value) | |
| return 0; | |
| /* Clear the error interrupts status */ | |
| ret = oa_tc6_write_register(tc6, OA_TC6_REG_STATUS0, value); | |
| if (ret) { | |
| netdev_err(tc6->netdev, "STATUS0 register write failed: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| if (FIELD_GET(OA_TC6_STATUS0_RX_BUFFER_OVERFLOW_ERROR, value)) { | |
| oa_tc6_look_for_new_frame(tc6); | |
| net_err_ratelimited("%s: Receive buffer overflow error\n", | |
| tc6->netdev->name); | |
| return -EAGAIN; | |
| } | |
| if (FIELD_GET(OA_TC6_STATUS0_TX_PROTOCOL_ERROR, value)) { | |
| netdev_err(tc6->netdev, "Transmit protocol error\n"); | |
| return -ENODEV; | |
| } | |
| /* TODO: Currently loss of frame and header errors are treated as | |
| * non-recoverable errors. They will be handled in the next version. | |
| */ | |
| if (FIELD_GET(OA_TC6_STATUS0_LOSS_OF_FRAME_ERROR, value)) { | |
| netdev_err(tc6->netdev, "Loss of frame error\n"); | |
| return -ENODEV; | |
| } | |
| if (FIELD_GET(OA_TC6_STATUS0_HEADER_ERROR, value)) { | |
| netdev_err(tc6->netdev, "Header error\n"); | |
| return -ENODEV; | |
| } | |
| return 0; | |
| } | |
| static int oa_tc6_process_rx_chunk_footer(struct oa_tc6 *tc6, u32 footer) | |
| { | |
| int ret = 0; | |
| /* Process rx chunk footer for the following, | |
| * 1. tx credits | |
| * 2. errors if any from MAC-PHY | |
| * 3. receive chunks available | |
| */ | |
| tc6->tx_credits = FIELD_GET(OA_TC6_DATA_FOOTER_TX_CREDITS, footer); | |
| tc6->rx_chunks_available = FIELD_GET(OA_TC6_DATA_FOOTER_RX_CHUNKS, | |
| footer); | |
| if (FIELD_GET(OA_TC6_DATA_FOOTER_EXTENDED_STS, footer)) { | |
| ret = oa_tc6_process_extended_status(tc6); | |
| /* EAGAIN error is recoverable. Move on to check | |
| * HEADER and SYNC errors before returning. | |
| */ | |
| if (ret && ret != -EAGAIN) | |
| return ret; | |
| } | |
| /* TODO: Currently received header bad and configuration unsync errors | |
| * are treated as non-recoverable errors. They will be handled in the | |
| * next version. | |
| */ | |
| if (FIELD_GET(OA_TC6_DATA_FOOTER_RXD_HEADER_BAD, footer)) { | |
| netdev_err(tc6->netdev, "Rxd header bad error\n"); | |
| return -ENODEV; | |
| } | |
| if (!FIELD_GET(OA_TC6_DATA_FOOTER_CONFIG_SYNC, footer)) { | |
| netdev_err(tc6->netdev, "Config unsync error\n"); | |
| return -ENODEV; | |
| } | |
| return ret; | |
| } | |
| static void oa_tc6_submit_rx_skb(struct oa_tc6 *tc6) | |
| { | |
| /* MAC-PHY delivers each frame with its Ethernet FCS attached. | |
| * Strip it before handing over to the stack, unless the user | |
| * has asked to keep it via NETIF_F_RXFCS. Keeping the FCS | |
| * in the frame is harmless for IP traffic, but is parsed as | |
| * a (malformed) suffix TLV by PTP, which makes ptp4l reject | |
| * every message with "bad message" error. | |
| */ | |
| if (!(tc6->netdev->features & NETIF_F_RXFCS) && | |
| tc6->rx_skb->len > ETH_FCS_LEN) | |
| skb_trim(tc6->rx_skb, tc6->rx_skb->len - ETH_FCS_LEN); | |
| tc6->rx_skb->protocol = eth_type_trans(tc6->rx_skb, tc6->netdev); | |
| tc6->netdev->stats.rx_packets++; | |
| tc6->netdev->stats.rx_bytes += tc6->rx_skb->len; | |
| netif_rx(tc6->rx_skb); | |
| tc6->rx_skb = NULL; | |
| } | |
| /* On oversubscribed traffic condition, particularly with overwhelming rx | |
| * buffer overflow errors, there could be data chunk loss. If tail + length | |
| * goes beyond end pointer, that is an indication that the data chunk with | |
| * end_valid bit is lost. Time to look for a data chunk with start_valid bit. | |
| * | |
| * If rx_skb is NULL, it is time to start looking for data chunk with | |
| * start_bit. | |
| */ | |
| static int oa_tc6_update_rx_skb(struct oa_tc6 *tc6, u8 *payload, u8 length) | |
| { | |
| if (!tc6->rx_skb || | |
| skb_tailroom(tc6->rx_skb) < length) { | |
| oa_tc6_look_for_new_frame(tc6); | |
| return -EAGAIN; | |
| } | |
| memcpy(skb_put(tc6->rx_skb, length), payload, length); | |
| return 0; | |
| } | |
| /* On overwhelming rx buffer overflow errors, due to data chunk loss, it is | |
| * possible that we get two data chunks with start_valid bit set, without | |
| * end_valid bit set in between. In this case, rx_skb would have a valid | |
| * buffer pointer. We should release, if a valid pointer is found before | |
| * allocating a new one. | |
| */ | |
| static int oa_tc6_allocate_rx_skb(struct oa_tc6 *tc6) | |
| { | |
| oa_tc6_cleanup_ongoing_rx_skb(tc6); | |
| tc6->rx_skb = netdev_alloc_skb_ip_align(tc6->netdev, tc6->netdev->mtu + | |
| ETH_HLEN + ETH_FCS_LEN); | |
| if (!tc6->rx_skb) { | |
| tc6->netdev->stats.rx_dropped++; | |
| return -ENOMEM; | |
| } | |
| return 0; | |
| } | |
| static int oa_tc6_prcs_complete_rx_frame(struct oa_tc6 *tc6, u8 *payload, | |
| u16 size) | |
| { | |
| int ret; | |
| ret = oa_tc6_allocate_rx_skb(tc6); | |
| if (ret) | |
| return ret; | |
| ret = oa_tc6_update_rx_skb(tc6, payload, size); | |
| if (ret) | |
| return ret; | |
| oa_tc6_submit_rx_skb(tc6); | |
| return 0; | |
| } | |
| static int oa_tc6_prcs_rx_frame_start(struct oa_tc6 *tc6, u8 *payload, u16 size) | |
| { | |
| int ret; | |
| ret = oa_tc6_allocate_rx_skb(tc6); | |
| if (ret) | |
| return ret; | |
| return oa_tc6_update_rx_skb(tc6, payload, size); | |
| } | |
| static int oa_tc6_prcs_rx_frame_end(struct oa_tc6 *tc6, u8 *payload, u16 size) | |
| { | |
| int ret; | |
| ret = oa_tc6_update_rx_skb(tc6, payload, size); | |
| if (!ret) | |
| oa_tc6_submit_rx_skb(tc6); | |
| return ret; | |
| } | |
| static int oa_tc6_prcs_ongoing_rx_frame(struct oa_tc6 *tc6, u8 *payload, | |
| u32 footer) | |
| { | |
| return oa_tc6_update_rx_skb(tc6, payload, | |
| OA_TC6_CHUNK_PAYLOAD_SIZE); | |
| } | |
| static int oa_tc6_prcs_rx_chunk_payload(struct oa_tc6 *tc6, u8 *data, | |
| u32 footer) | |
| { | |
| u8 start_byte_offset = FIELD_GET(OA_TC6_DATA_FOOTER_START_WORD_OFFSET, | |
| footer) * sizeof(u32); | |
| u8 end_byte_offset = FIELD_GET(OA_TC6_DATA_FOOTER_END_BYTE_OFFSET, | |
| footer); | |
| bool start_valid = FIELD_GET(OA_TC6_DATA_FOOTER_START_VALID, footer); | |
| bool end_valid = FIELD_GET(OA_TC6_DATA_FOOTER_END_VALID, footer); | |
| u16 size; | |
| /* Restart the new rx frame after receiving rx buffer overflow error */ | |
| if (start_valid && tc6->wait_until_start_valid) | |
| tc6->wait_until_start_valid = false; | |
| if (tc6->wait_until_start_valid) | |
| return 0; | |
| /* Process the chunk with complete rx frame */ | |
| if (start_valid && end_valid && start_byte_offset < end_byte_offset) { | |
| size = end_byte_offset + 1 - start_byte_offset; | |
| return oa_tc6_prcs_complete_rx_frame(tc6, | |
| &data[start_byte_offset], | |
| size); | |
| } | |
| /* Process the chunk with only rx frame start */ | |
| if (start_valid && !end_valid) { | |
| size = OA_TC6_CHUNK_PAYLOAD_SIZE - start_byte_offset; | |
| return oa_tc6_prcs_rx_frame_start(tc6, | |
| &data[start_byte_offset], | |
| size); | |
| } | |
| /* Process the chunk with only rx frame end */ | |
| if (end_valid && !start_valid) { | |
| size = end_byte_offset + 1; | |
| return oa_tc6_prcs_rx_frame_end(tc6, data, size); | |
| } | |
| /* Process the chunk with previous rx frame end and next rx frame | |
| * start. | |
| */ | |
| if (start_valid && end_valid && start_byte_offset > end_byte_offset) { | |
| /* After rx buffer overflow error received, there might be a | |
| * possibility of getting an end valid of a previously | |
| * incomplete rx frame along with the new rx frame start valid. | |
| */ | |
| if (tc6->rx_skb) { | |
| size = end_byte_offset + 1; | |
| oa_tc6_prcs_rx_frame_end(tc6, data, size); | |
| /* Return value from oa_tc6_prcs_rx_frame_end is not | |
| * checked. If it returned an error, it is to make | |
| * the code to look for new frame. At this stage, | |
| * code below is going to process a new frame. So, | |
| * error condition is set to false, in case it is | |
| * set before proceeding. | |
| */ | |
| tc6->wait_until_start_valid = false; | |
| } | |
| size = OA_TC6_CHUNK_PAYLOAD_SIZE - start_byte_offset; | |
| return oa_tc6_prcs_rx_frame_start(tc6, | |
| &data[start_byte_offset], | |
| size); | |
| } | |
| /* Process the chunk with ongoing rx frame data */ | |
| return oa_tc6_prcs_ongoing_rx_frame(tc6, data, footer); | |
| } | |
| static u32 oa_tc6_get_rx_chunk_footer(struct oa_tc6 *tc6, u16 footer_offset) | |
| { | |
| u8 *rx_buf = tc6->spi_data_rx_buf; | |
| __be32 footer; | |
| footer = *((__be32 *)&rx_buf[footer_offset]); | |
| return be32_to_cpu(footer); | |
| } | |
| static int oa_tc6_process_spi_data_rx_buf(struct oa_tc6 *tc6, u16 length) | |
| { | |
| u16 no_of_rx_chunks = length / OA_TC6_CHUNK_SIZE; | |
| bool retry = false; | |
| int ret = 0; | |
| u32 footer; | |
| /* All the rx chunks in the receive SPI data buffer are examined here */ | |
| for (int i = 0; i < no_of_rx_chunks; i++) { | |
| /* Last 4 bytes in each received chunk consist footer info */ | |
| footer = oa_tc6_get_rx_chunk_footer(tc6, i * OA_TC6_CHUNK_SIZE + | |
| OA_TC6_CHUNK_PAYLOAD_SIZE); | |
| ret = oa_tc6_process_rx_chunk_footer(tc6, footer); | |
| if (ret) { | |
| if (ret != -EAGAIN) | |
| return ret; | |
| retry = true; | |
| } | |
| /* If there is a data valid chunks then process it for the | |
| * information needed to determine the validity and the location | |
| * of the receive frame data. | |
| */ | |
| if (FIELD_GET(OA_TC6_DATA_FOOTER_DATA_VALID, footer)) { | |
| u8 *payload = tc6->spi_data_rx_buf + i * | |
| OA_TC6_CHUNK_SIZE; | |
| ret = oa_tc6_prcs_rx_chunk_payload(tc6, payload, | |
| footer); | |
| if (ret) { | |
| if (ret != -ENOMEM && ret != -EAGAIN) | |
| return ret; | |
| retry = true; | |
| } | |
| } | |
| } | |
| /* Not bailing out on recoverable error codes, -EAGAIN and | |
| * -ENOMEM. If subsequent loop iterations, if any, succeeds, | |
| * error code would be overwritten. retry flag helps to | |
| * make the caller to continue and retry. Since recovery | |
| * action for -ENOMEM and -EAGAIN are same, we are returning | |
| * one of the error codes, that is -EAGAIN. | |
| * | |
| * Successful recovery depends on how small the frames are, | |
| * how many chunks, among the received chunks triggered the | |
| * error, whether data is intact even with error conditions. | |
| * As a result, there is no single, best method to recover | |
| * most data when error conditions hit. We do our best by | |
| * processing all the chunks with good "footer header" and | |
| * "data valid" bit set. | |
| */ | |
| if (retry) { | |
| ret = -EAGAIN; | |
| oa_tc6_look_for_new_frame(tc6); | |
| } | |
| return ret; | |
| } | |
| static __be32 oa_tc6_prepare_data_header(bool data_valid, bool start_valid, | |
| bool end_valid, u8 end_byte_offset) | |
| { | |
| u32 header = FIELD_PREP(OA_TC6_DATA_HEADER_DATA_NOT_CTRL, | |
| OA_TC6_DATA_HEADER) | | |
| FIELD_PREP(OA_TC6_DATA_HEADER_DATA_VALID, data_valid) | | |
| FIELD_PREP(OA_TC6_DATA_HEADER_START_VALID, start_valid) | | |
| FIELD_PREP(OA_TC6_DATA_HEADER_END_VALID, end_valid) | | |
| FIELD_PREP(OA_TC6_DATA_HEADER_END_BYTE_OFFSET, | |
| end_byte_offset); | |
| header |= FIELD_PREP(OA_TC6_DATA_HEADER_PARITY, | |
| oa_tc6_get_parity(header)); | |
| return cpu_to_be32(header); | |
| } | |
| static void oa_tc6_add_tx_skb_to_spi_buf(struct oa_tc6 *tc6) | |
| { | |
| enum oa_tc6_data_end_valid_info end_valid = OA_TC6_DATA_END_INVALID; | |
| __be32 *tx_buf = tc6->spi_data_tx_buf + tc6->spi_data_tx_buf_offset; | |
| u16 remaining_len = tc6->ongoing_tx_skb->len - tc6->tx_skb_offset; | |
| u8 *tx_skb_data = tc6->ongoing_tx_skb->data + tc6->tx_skb_offset; | |
| enum oa_tc6_data_start_valid_info start_valid; | |
| u8 end_byte_offset = 0; | |
| u16 length_to_copy; | |
| /* Initial value is assigned here to avoid more than 80 characters in | |
| * the declaration place. | |
| */ | |
| start_valid = OA_TC6_DATA_START_INVALID; | |
| /* Set start valid if the current tx chunk contains the start of the tx | |
| * ethernet frame. | |
| */ | |
| if (!tc6->tx_skb_offset) | |
| start_valid = OA_TC6_DATA_START_VALID; | |
| /* If the remaining tx skb length is more than the chunk payload size of | |
| * 64 bytes then copy only 64 bytes and leave the ongoing tx skb for | |
| * next tx chunk. | |
| */ | |
| length_to_copy = min_t(u16, remaining_len, OA_TC6_CHUNK_PAYLOAD_SIZE); | |
| /* Copy the tx skb data to the tx chunk payload buffer */ | |
| memcpy(tx_buf + 1, tx_skb_data, length_to_copy); | |
| tc6->tx_skb_offset += length_to_copy; | |
| /* Set end valid if the current tx chunk contains the end of the tx | |
| * ethernet frame. | |
| */ | |
| if (tc6->ongoing_tx_skb->len == tc6->tx_skb_offset) { | |
| end_valid = OA_TC6_DATA_END_VALID; | |
| end_byte_offset = length_to_copy - 1; | |
| tc6->tx_skb_offset = 0; | |
| tc6->netdev->stats.tx_bytes += tc6->ongoing_tx_skb->len; | |
| tc6->netdev->stats.tx_packets++; | |
| kfree_skb(tc6->ongoing_tx_skb); | |
| tc6->ongoing_tx_skb = NULL; | |
| } | |
| *tx_buf = oa_tc6_prepare_data_header(OA_TC6_DATA_VALID, start_valid, | |
| end_valid, end_byte_offset); | |
| tc6->spi_data_tx_buf_offset += OA_TC6_CHUNK_SIZE; | |
| } | |
| static u16 oa_tc6_prepare_spi_tx_buf_for_tx_skbs(struct oa_tc6 *tc6) | |
| { | |
| u16 used_tx_credits; | |
| /* Get tx skbs and convert them into tx chunks based on the tx credits | |
| * available. | |
| */ | |
| for (used_tx_credits = 0; used_tx_credits < tc6->tx_credits; | |
| used_tx_credits++) { | |
| if (!tc6->ongoing_tx_skb) { | |
| spin_lock_bh(&tc6->tx_skb_lock); | |
| tc6->ongoing_tx_skb = tc6->waiting_tx_skb; | |
| tc6->waiting_tx_skb = NULL; | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| } | |
| if (!tc6->ongoing_tx_skb) | |
| break; | |
| oa_tc6_add_tx_skb_to_spi_buf(tc6); | |
| } | |
| return used_tx_credits * OA_TC6_CHUNK_SIZE; | |
| } | |
| static void oa_tc6_add_empty_chunks_to_spi_buf(struct oa_tc6 *tc6, | |
| u16 needed_empty_chunks) | |
| { | |
| __be32 header; | |
| header = oa_tc6_prepare_data_header(OA_TC6_DATA_INVALID, | |
| OA_TC6_DATA_START_INVALID, | |
| OA_TC6_DATA_END_INVALID, 0); | |
| while (needed_empty_chunks--) { | |
| __be32 *tx_buf = tc6->spi_data_tx_buf + | |
| tc6->spi_data_tx_buf_offset; | |
| *tx_buf = header; | |
| tc6->spi_data_tx_buf_offset += OA_TC6_CHUNK_SIZE; | |
| } | |
| } | |
| static u16 oa_tc6_prepare_spi_tx_buf_for_rx_chunks(struct oa_tc6 *tc6, u16 len) | |
| { | |
| u16 tx_chunks = len / OA_TC6_CHUNK_SIZE; | |
| u16 needed_empty_chunks; | |
| /* If there are more chunks to receive than to transmit, we need to add | |
| * enough empty tx chunks to allow the reception of the excess rx | |
| * chunks. | |
| */ | |
| if (tx_chunks >= tc6->rx_chunks_available) | |
| return len; | |
| needed_empty_chunks = tc6->rx_chunks_available - tx_chunks; | |
| oa_tc6_add_empty_chunks_to_spi_buf(tc6, needed_empty_chunks); | |
| return needed_empty_chunks * OA_TC6_CHUNK_SIZE + len; | |
| } | |
| static int oa_tc6_try_spi_transfer(struct oa_tc6 *tc6) | |
| { | |
| int ret; | |
| while (true) { | |
| u16 spi_len = 0; | |
| tc6->spi_data_tx_buf_offset = 0; | |
| if (tc6->ongoing_tx_skb || tc6->waiting_tx_skb) | |
| spi_len = oa_tc6_prepare_spi_tx_buf_for_tx_skbs(tc6); | |
| spi_len = oa_tc6_prepare_spi_tx_buf_for_rx_chunks(tc6, spi_len); | |
| if (tc6->int_flag) { | |
| tc6->int_flag = false; | |
| if (spi_len == 0) { | |
| oa_tc6_add_empty_chunks_to_spi_buf(tc6, 1); | |
| spi_len = OA_TC6_CHUNK_SIZE; | |
| } | |
| } | |
| if (spi_len == 0) | |
| break; | |
| ret = oa_tc6_spi_transfer(tc6, OA_TC6_DATA_HEADER, spi_len); | |
| if (ret) { | |
| netdev_err(tc6->netdev, "SPI data transfer failed: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| ret = oa_tc6_process_spi_data_rx_buf(tc6, spi_len); | |
| if (ret && ret != -EAGAIN) { | |
| oa_tc6_free_ongoing_skbs(tc6); | |
| netdev_err(tc6->netdev, "Device error: %d\n", ret); | |
| return ret; | |
| } | |
| if (!tc6->waiting_tx_skb && netif_queue_stopped(tc6->netdev)) | |
| netif_wake_queue(tc6->netdev); | |
| } | |
| return 0; | |
| } | |
| static irqreturn_t oa_tc6_macphy_threaded_irq(int irq, void *data) | |
| { | |
| struct oa_tc6 *tc6 = data; | |
| int ret = 0; | |
| /* It is possible that interrupt woke the thread before it is | |
| * disabled. Until we come up with good recovery mechanism, | |
| * no need to attempt spi transfer, once it fails. Pending skbs | |
| * are already freed. | |
| */ | |
| spin_lock_bh(&tc6->tx_skb_lock); | |
| if (tc6->disable_traffic) { | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| return IRQ_HANDLED; | |
| } | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| while (tc6->int_flag || | |
| (tc6->waiting_tx_skb && tc6->tx_credits)) { | |
| ret = oa_tc6_try_spi_transfer(tc6); | |
| if (ret) { | |
| disable_irq_nosync(tc6->spi->irq); | |
| oa_tc6_disable_traffic(tc6); | |
| break; | |
| } | |
| } | |
| return IRQ_HANDLED; | |
| } | |
| static int oa_tc6_update_buffer_status_from_register(struct oa_tc6 *tc6) | |
| { | |
| u32 value; | |
| int ret; | |
| /* Initially tx credits and rx chunks available to be updated from the | |
| * register as there is no data transfer performed yet. Later they will | |
| * be updated from the rx footer. | |
| */ | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_BUFFER_STATUS, &value); | |
| if (ret) | |
| return ret; | |
| tc6->tx_credits = FIELD_GET(OA_TC6_BUFFER_STATUS_TX_CREDITS_AVAILABLE, | |
| value); | |
| tc6->rx_chunks_available = | |
| FIELD_GET(OA_TC6_BUFFER_STATUS_RX_CHUNKS_AVAILABLE, value); | |
| return 0; | |
| } | |
| static irqreturn_t oa_tc6_macphy_isr(int irq, void *data) | |
| { | |
| struct oa_tc6 *tc6 = data; | |
| /* MAC-PHY interrupt can occur for the following reasons. | |
| * - availability of tx credits if it was 0 before and not reported in | |
| * the previous rx footer. | |
| * - availability of rx chunks if it was 0 before and not reported in | |
| * the previous rx footer. | |
| * - extended status event not reported in the previous rx footer. | |
| */ | |
| if (tc6->disable_traffic) | |
| disable_irq_nosync(tc6->spi->irq); | |
| else | |
| tc6->int_flag = true; | |
| /* Wake IRQ thread to perform spi transfer . In case | |
| * disable_traffic is set, threaded irq may run again | |
| * one more time. | |
| */ | |
| return IRQ_WAKE_THREAD; | |
| } | |
| /** | |
| * oa_tc6_zero_align_receive_frame_enable - function to enable zero align | |
| * receive frame feature. | |
| * @tc6: oa_tc6 struct. | |
| * | |
| * Return: 0 on success otherwise failed. | |
| */ | |
| int oa_tc6_zero_align_receive_frame_enable(struct oa_tc6 *tc6) | |
| { | |
| u32 regval; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_CONFIG0, ®val); | |
| if (ret) | |
| return ret; | |
| /* Set Zero-Align Receive Frame Enable */ | |
| regval |= OA_TC6_CONFIG0_ZARFE_ENABLE; | |
| return oa_tc6_write_register(tc6, OA_TC6_REG_CONFIG0, regval); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_zero_align_receive_frame_enable); | |
| /** | |
| * oa_tc6_start_xmit - function for sending the tx skb which consists ethernet | |
| * frame. | |
| * @tc6: oa_tc6 struct. | |
| * @skb: socket buffer in which the ethernet frame is stored. | |
| * | |
| * Return: NETDEV_TX_OK either on successful queueing of the packet for | |
| * transmission, or on packet getting dropped. Packet can be dropped due to | |
| * failure in linearizing the buffer or disable_traffic is set due to | |
| * earlier fatal error. Returns NETDEV_TX_BUSY when there is no room | |
| * to queue the packet. | |
| */ | |
| netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, struct sk_buff *skb) | |
| { | |
| if (skb_linearize(skb)) { | |
| oa_tc6_drop_tx_skb(tc6, skb); | |
| return NETDEV_TX_OK; | |
| } | |
| spin_lock_bh(&tc6->tx_skb_lock); | |
| if (tc6->waiting_tx_skb) { | |
| netif_stop_queue(tc6->netdev); | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| return NETDEV_TX_BUSY; | |
| } | |
| if (tc6->disable_traffic) { | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| oa_tc6_drop_tx_skb(tc6, skb); | |
| return NETDEV_TX_OK; | |
| } | |
| tc6->waiting_tx_skb = skb; | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| /* Wake the threaded IRQ to perform spi transfer. */ | |
| irq_wake_thread(tc6->spi->irq, tc6); | |
| return NETDEV_TX_OK; | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_start_xmit); | |
| static int oa_tc6_check_ctrl_protection(struct oa_tc6 *tc6) | |
| { | |
| u32 regval; | |
| int ret; | |
| ret = oa_tc6_read_register(tc6, OA_TC6_REG_CONFIG0, ®val); | |
| if (ret) | |
| return ret; | |
| tc6->prot_ctrl = FIELD_GET(OA_TC6_CONFIG0_PROTE, regval); | |
| return 0; | |
| } | |
| /** | |
| * oa_tc6_init - allocates and initializes oa_tc6 structure. | |
| * @spi: device with which data will be exchanged. | |
| * @netdev: network device interface structure. | |
| * @quirks: device specific modifiers for the OA TC6 protocol. | |
| * | |
| * Return: pointer reference to the oa_tc6 structure if the MAC-PHY | |
| * initialization is successful otherwise NULL. | |
| */ | |
| struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev, | |
| struct oa_tc6_quirks *quirks) | |
| { | |
| struct oa_tc6 *tc6; | |
| int ret; | |
| tc6 = devm_kzalloc(&spi->dev, sizeof(*tc6), GFP_KERNEL); | |
| if (!tc6) | |
| return NULL; | |
| tc6->spi = spi; | |
| tc6->netdev = netdev; | |
| SET_NETDEV_DEV(netdev, &spi->dev); | |
| mutex_init(&tc6->spi_ctrl_lock); | |
| spin_lock_init(&tc6->tx_skb_lock); | |
| if (quirks) | |
| tc6->quirk_flags = quirks->quirk_flags; | |
| /* Set the SPI controller to pump at realtime priority */ | |
| tc6->spi->rt = true; | |
| if (spi_setup(tc6->spi) < 0) | |
| return NULL; | |
| tc6->spi_ctrl_tx_buf = devm_kzalloc(&tc6->spi->dev, | |
| OA_TC6_CTRL_SPI_BUF_SIZE, | |
| GFP_KERNEL); | |
| if (!tc6->spi_ctrl_tx_buf) | |
| return NULL; | |
| tc6->spi_ctrl_rx_buf = devm_kzalloc(&tc6->spi->dev, | |
| OA_TC6_CTRL_SPI_BUF_SIZE, | |
| GFP_KERNEL); | |
| if (!tc6->spi_ctrl_rx_buf) | |
| return NULL; | |
| tc6->spi_data_tx_buf = devm_kzalloc(&tc6->spi->dev, | |
| OA_TC6_SPI_DATA_BUF_SIZE, | |
| GFP_KERNEL); | |
| if (!tc6->spi_data_tx_buf) | |
| return NULL; | |
| tc6->spi_data_rx_buf = devm_kzalloc(&tc6->spi->dev, | |
| OA_TC6_SPI_DATA_BUF_SIZE, | |
| GFP_KERNEL); | |
| if (!tc6->spi_data_rx_buf) | |
| return NULL; | |
| /* Check the PROTE bit status so that we can reset the device */ | |
| ret = oa_tc6_check_ctrl_protection(tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, | |
| "Failed to check the protection mode: %d\n", ret); | |
| return NULL; | |
| } | |
| ret = oa_tc6_sw_reset_macphy(tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, | |
| "MAC-PHY software reset failed: %d\n", ret); | |
| return NULL; | |
| } | |
| ret = oa_tc6_unmask_macphy_error_interrupts(tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, | |
| "MAC-PHY error interrupts unmask failed: %d\n", ret); | |
| return NULL; | |
| } | |
| ret = oa_tc6_phy_init(tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, | |
| "MAC internal PHY initialization failed: %d\n", ret); | |
| return NULL; | |
| } | |
| ret = oa_tc6_enable_data_transfer(tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, "Failed to enable data transfer: %d\n", | |
| ret); | |
| goto phy_exit; | |
| } | |
| ret = oa_tc6_update_buffer_status_from_register(tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, | |
| "Failed to update buffer status: %d\n", ret); | |
| goto phy_exit; | |
| } | |
| ret = devm_request_threaded_irq(&tc6->spi->dev, tc6->spi->irq, | |
| oa_tc6_macphy_isr, | |
| oa_tc6_macphy_threaded_irq, | |
| IRQF_TRIGGER_LOW | IRQF_ONESHOT, | |
| dev_name(&tc6->spi->dev), tc6); | |
| if (ret) { | |
| dev_err(&tc6->spi->dev, "Failed to request macphy isr %d\n", | |
| ret); | |
| goto phy_exit; | |
| } | |
| /* oa_tc6_sw_reset_macphy() function resets and clears the MAC-PHY reset | |
| * complete status. IRQ is also asserted on reset completion and it is | |
| * remain asserted until MAC-PHY receives a data chunk. So performing an | |
| * empty data chunk transmission will deassert the IRQ. Refer section | |
| * 7.7 and 9.2.8.8 in the OPEN Alliance specification for more details. | |
| */ | |
| tc6->int_flag = true; | |
| irq_wake_thread(tc6->spi->irq, tc6); | |
| return tc6; | |
| phy_exit: | |
| oa_tc6_phy_exit(tc6); | |
| return NULL; | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_init); | |
| /** | |
| * oa_tc6_exit - exit function. | |
| * @tc6: oa_tc6 struct. | |
| */ | |
| void oa_tc6_exit(struct oa_tc6 *tc6) | |
| { | |
| disable_irq(tc6->spi->irq); | |
| spin_lock_bh(&tc6->tx_skb_lock); | |
| tc6->disable_traffic = true; | |
| spin_unlock_bh(&tc6->tx_skb_lock); | |
| oa_tc6_phy_exit(tc6); | |
| oa_tc6_free_pending_skbs(tc6); | |
| } | |
| EXPORT_SYMBOL_GPL(oa_tc6_exit); | |
| MODULE_DESCRIPTION("OPEN Alliance 10BASE‑T1x MAC‑PHY Serial Interface Lib"); | |
| MODULE_AUTHOR("Parthiban Veerasooran <parthiban.veerasooran@microchip.com>"); | |
| MODULE_LICENSE("GPL"); |