/* * Copyright (c) 2017-2018 ARM Limited * Copyright (c) 2018 Linaro Limited * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT smsc_lan9220 /* SMSC911x/SMSC9220 driver. Partly based on mbedOS driver. */ #define LOG_MODULE_NAME eth_smsc911x #define LOG_LEVEL CONFIG_ETHERNET_LOG_LEVEL #include LOG_MODULE_REGISTER(LOG_MODULE_NAME); #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ethernet/eth_stats.h" #ifdef CONFIG_SHARED_IRQ #include #endif #include "eth_smsc911x_priv.h" #define RESET_TIMEOUT 10 #define PHY_RESET_TIMEOUT K_MSEC(100) #define REG_WRITE_TIMEOUT 50 /* Controller has only one PHY with address 1 */ #define PHY_ADDR 1 struct eth_context { struct net_if *iface; uint8_t mac[6]; #if defined(CONFIG_NET_STATISTICS_ETHERNET) struct net_stats_eth stats; #endif }; /* SMSC911x helper functions */ static int smsc_mac_regread(uint8_t reg, uint32_t *val) { uint32_t cmd = MAC_CSR_CMD_BUSY | MAC_CSR_CMD_READ | reg; SMSC9220->MAC_CSR_CMD = cmd; while ((SMSC9220->MAC_CSR_CMD & MAC_CSR_CMD_BUSY) != 0) { } *val = SMSC9220->MAC_CSR_DATA; return 0; } static int smsc_mac_regwrite(uint8_t reg, uint32_t val) { uint32_t cmd = MAC_CSR_CMD_BUSY | MAC_CSR_CMD_WRITE | reg; SMSC9220->MAC_CSR_DATA = val; SMSC9220->MAC_CSR_CMD = cmd; while ((SMSC9220->MAC_CSR_CMD & MAC_CSR_CMD_BUSY) != 0) { } return 0; } int smsc_phy_regread(uint8_t regoffset, uint32_t *data) { uint32_t val = 0U; uint32_t phycmd = 0U; unsigned int time_out = REG_WRITE_TIMEOUT; if (smsc_mac_regread(SMSC9220_MAC_MII_ACC, &val) < 0) { return -1; } if (val & MAC_MII_ACC_MIIBZY) { *data = 0U; return -EBUSY; } phycmd = 0U; phycmd |= PHY_ADDR << 11; phycmd |= (regoffset & 0x1F) << 6; phycmd |= MAC_MII_ACC_READ; phycmd |= MAC_MII_ACC_MIIBZY; /* Operation start */ if (smsc_mac_regwrite(SMSC9220_MAC_MII_ACC, phycmd)) { return -1; } val = 0U; do { k_sleep(K_MSEC(1)); time_out--; if (smsc_mac_regread(SMSC9220_MAC_MII_ACC, &val)) { return -1; } } while (time_out != 0U && (val & MAC_MII_ACC_MIIBZY)); if (time_out == 0U) { return -ETIMEDOUT; } if (smsc_mac_regread(SMSC9220_MAC_MII_DATA, data) < 0) { return -1; } return 0; } int smsc_phy_regwrite(uint8_t regoffset, uint32_t data) { uint32_t val = 0U; uint32_t phycmd = 0U; unsigned int time_out = REG_WRITE_TIMEOUT; if (smsc_mac_regread(SMSC9220_MAC_MII_ACC, &val) < 0) { return -1; } if (val & MAC_MII_ACC_MIIBZY) { return -EBUSY; } if (smsc_mac_regwrite(SMSC9220_MAC_MII_DATA, data & 0xFFFF) < 0) { return -1; } phycmd |= PHY_ADDR << 11; phycmd |= (regoffset & 0x1F) << 6; phycmd |= MAC_MII_ACC_WRITE; phycmd |= MAC_MII_ACC_MIIBZY; /* Operation start */ if (smsc_mac_regwrite(SMSC9220_MAC_MII_ACC, phycmd) < 0) { return -1; } do { k_sleep(K_MSEC(1)); time_out--; if (smsc_mac_regread(SMSC9220_MAC_MII_ACC, &phycmd)) { return -1; } } while (time_out != 0U && (phycmd & MAC_MII_ACC_MIIBZY)); if (time_out == 0U) { return -ETIMEDOUT; } return 0; } static int smsc_read_mac_address(uint8_t *mac) { uint32_t tmp; int res; res = smsc_mac_regread(SMSC9220_MAC_ADDRL, &tmp); if (res < 0) { return res; } mac[0] = (uint8_t)(tmp >> 0); mac[1] = (uint8_t)(tmp >> 8); mac[2] = (uint8_t)(tmp >> 16); mac[3] = (uint8_t)(tmp >> 24); res = smsc_mac_regread(SMSC9220_MAC_ADDRH, &tmp); if (res < 0) { return res; } mac[4] = (uint8_t)(tmp >> 0); mac[5] = (uint8_t)(tmp >> 8); return 0; } static int smsc_check_id(void) { uint32_t id = SMSC9220->ID_REV; /* If bottom and top halves of the word are the same, * the hardware is (likely) not present. */ if (((id >> 16) & 0xFFFF) == (id & 0xFFFF)) { return -1; } switch (((id >> 16) & 0xFFFF)) { case 0x9220: /* SMSC9220 on MPS2 */ case 0x0118: /* SMS9118 as emulated by QEMU */ break; default: return -1; } return 0; } static int smsc_soft_reset(void) { unsigned int time_out = RESET_TIMEOUT; SMSC9220->HW_CFG |= HW_CFG_SRST; do { k_sleep(K_MSEC(1)); time_out--; } while (time_out != 0U && (SMSC9220->HW_CFG & HW_CFG_SRST)); if (time_out == 0U) { return -1; } return 0; } void smsc_set_txfifo(unsigned int val) { /* 2kb minimum, 14kb maximum */ if (val >= 2U && val <= 14U) { SMSC9220->HW_CFG = val << 16; } } void smsc_init_irqs(void) { SMSC9220->INT_EN = 0; /* Clear all interrupts */ SMSC9220->INT_STS = 0xFFFFFFFF; /* Polarity config which works with QEMU */ /* IRQ deassertion at 220 usecs and master IRQ enable */ SMSC9220->IRQ_CFG = 0x22000111; } static int smsc_check_phy(void) { uint32_t phyid1, phyid2; if (smsc_phy_regread(SMSC9220_PHY_ID1, &phyid1)) { return -1; } if (smsc_phy_regread(SMSC9220_PHY_ID2, &phyid2)) { return -1; } return ((phyid1 == 0xFFFF && phyid2 == 0xFFFF) || (phyid1 == 0x0 && phyid2 == 0x0)); } int smsc_reset_phy(void) { uint32_t val; if (smsc_phy_regread(SMSC9220_PHY_BCONTROL, &val)) { return -1; } val |= 1 << 15; if (smsc_phy_regwrite(SMSC9220_PHY_BCONTROL, val)) { return -1; } return 0; } /** * Advertise all speeds and pause capabilities */ void smsc_advertise_caps(void) { uint32_t aneg_adv = 0U; smsc_phy_regread(SMSC9220_PHY_ANEG_ADV, &aneg_adv); aneg_adv |= 0xDE0; smsc_phy_regwrite(SMSC9220_PHY_ANEG_ADV, aneg_adv); smsc_phy_regread(SMSC9220_PHY_ANEG_ADV, &aneg_adv); } void smsc_establish_link(void) { uint32_t bcr = 0U; uint32_t hw_cfg = 0U; smsc_phy_regread(SMSC9220_PHY_BCONTROL, &bcr); bcr |= (1 << 12) | (1 << 9); smsc_phy_regwrite(SMSC9220_PHY_BCONTROL, bcr); smsc_phy_regread(SMSC9220_PHY_BCONTROL, &bcr); hw_cfg = SMSC9220->HW_CFG; hw_cfg &= 0xF0000; hw_cfg |= (1 << 20); SMSC9220->HW_CFG = hw_cfg; } static inline void smsc_enable_xmit(void) { SMSC9220->TX_CFG = 0x2 /*TX_CFG_TX_ON*/; } void smsc_enable_mac_xmit(void) { uint32_t mac_cr = 0U; smsc_mac_regread(SMSC9220_MAC_CR, &mac_cr); mac_cr |= (1 << 3); /* xmit enable */ mac_cr |= (1 << 28); /* Heartbeat disable */ smsc_mac_regwrite(SMSC9220_MAC_CR, mac_cr); } void smsc_enable_mac_recv(void) { uint32_t mac_cr = 0U; smsc_mac_regread(SMSC9220_MAC_CR, &mac_cr); mac_cr |= (1 << 2); /* Recv enable */ smsc_mac_regwrite(SMSC9220_MAC_CR, mac_cr); } int smsc_init(void) { unsigned int phyreset = 0U; if (smsc_check_id() < 0) { return -1; } if (smsc_soft_reset() < 0) { return -1; } smsc_set_txfifo(5); /* Sets automatic flow control thresholds, and backpressure */ /* threshold to defaults specified. */ SMSC9220->AFC_CFG = 0x006E3740; /* May need to initialize EEPROM/read MAC from it on real HW. */ /* Configure GPIOs as LED outputs. */ SMSC9220->GPIO_CFG = 0x70070000; smsc_init_irqs(); /* Configure MAC addresses here if needed. */ if (smsc_check_phy() < 0) { return -1; } if (smsc_reset_phy() < 0) { return -1; } k_sleep(PHY_RESET_TIMEOUT); /* Checking whether phy reset completed successfully.*/ if (smsc_phy_regread(SMSC9220_PHY_BCONTROL, &phyreset)) { return 1; } if (phyreset & (1 << 15)) { return 1; } smsc_advertise_caps(); /* bit [12] of BCONTROL seems self-clearing. */ /* Although it's not so in the manual. */ smsc_establish_link(); /* Interrupt threshold */ SMSC9220->FIFO_INT = 0xFF000000; smsc_enable_mac_xmit(); smsc_enable_xmit(); SMSC9220->RX_CFG = 0; smsc_enable_mac_recv(); /* Rx status FIFO level irq threshold */ SMSC9220->FIFO_INT &= ~(0xFF); /* Clear 2 bottom nibbles */ /* This sleep is compulsory otherwise txmit/receive will fail. */ k_sleep(K_MSEC(2000)); return 0; } /* Driver functions */ static enum ethernet_hw_caps eth_smsc911x_get_capabilities(struct device *dev) { ARG_UNUSED(dev); return ETHERNET_LINK_10BASE_T | ETHERNET_LINK_100BASE_T; } #if defined(CONFIG_NET_STATISTICS_ETHERNET) static struct net_stats_eth *get_stats(struct device *dev) { struct eth_context *context = dev->driver_data; return &context->stats; } #endif static void eth_initialize(struct net_if *iface) { struct device *dev = net_if_get_device(iface); struct eth_context *context = dev->driver_data; LOG_DBG("eth_initialize"); smsc_read_mac_address(context->mac); SMSC9220->INT_EN |= BIT(SMSC9220_INTERRUPT_RXSTATUS_FIFO_LEVEL); net_if_set_link_addr(iface, context->mac, sizeof(context->mac), NET_LINK_ETHERNET); context->iface = iface; ethernet_init(iface); } static int smsc_write_tx_fifo(const uint8_t *buf, uint32_t len, bool is_last) { uint32_t *buf32; __ASSERT_NO_MSG(((uintptr_t)buf & 3) == 0); if (is_last) { /* Last fragment may be not full */ len = (len + 3) & ~3; } if ((len & 3) != 0U || len == 0U) { LOG_ERR("Chunk size not aligned: %u", len); return -1; } buf32 = (uint32_t *)buf; len /= 4U; do { SMSC9220->TX_DATA_PORT = *buf32++; } while (--len); return 0; } static int eth_tx(struct device *dev, struct net_pkt *pkt) { uint16_t total_len = net_pkt_get_len(pkt); static uint8_t tx_buf[NET_ETH_MAX_FRAME_SIZE] __aligned(4); uint32_t txcmd_a, txcmd_b; uint32_t tx_stat; int res; txcmd_a = (1/*is_first_segment*/ << 13) | (1/*is_last_segment*/ << 12) | total_len; /* Use len as a tag */ txcmd_b = total_len << 16 | total_len; SMSC9220->TX_DATA_PORT = txcmd_a; SMSC9220->TX_DATA_PORT = txcmd_b; if (net_pkt_read(pkt, tx_buf, total_len)) { goto error; } res = smsc_write_tx_fifo(tx_buf, total_len, true); if (res < 0) { goto error; } tx_stat = SMSC9220->TX_STAT_PORT; LOG_DBG("TX_STAT: %x", tx_stat); return 0; error: LOG_ERR("Writing pkt to FIFO failed"); return -1; } static const struct ethernet_api api_funcs = { .iface_api.init = eth_initialize, .get_capabilities = eth_smsc911x_get_capabilities, .send = eth_tx, #if defined(CONFIG_NET_STATISTICS_ETHERNET) .get_stats = get_stats, #endif }; static void smsc_discard_pkt(void) { /* TODO: */ /* Datasheet p.43: */ /* When performing a fast-forward, there must be at least 4 DWORDs * of data in the RX data FIFO for the packet being discarded. For * less than 4 DWORDs do not use RX_FFWD. In this case data must be * read from the RX data FIFO and discarded using standard PIO read * operations. */ SMSC9220->RX_DP_CTRL = RX_DP_CTRL_RX_FFWD; } static inline void smsc_wait_discard_pkt(void) { while ((SMSC9220->RX_DP_CTRL & RX_DP_CTRL_RX_FFWD) != 0) { } } static int smsc_read_rx_fifo(struct net_pkt *pkt, uint32_t len) { uint32_t buf32; __ASSERT_NO_MSG((len & 3) == 0U && len >= 4U); len /= 4U; do { buf32 = SMSC9220->RX_DATA_PORT; if (net_pkt_write(pkt, &buf32, sizeof(uint32_t))) { return -1; } } while (--len); return 0; } static struct net_pkt *smsc_recv_pkt(struct device *dev, uint32_t pkt_size) { struct eth_context *context = dev->driver_data; struct net_pkt *pkt; uint32_t rem_size; /* Round up to next DWORD size */ rem_size = (pkt_size + 3) & ~3; /* Don't account for FCS when filling net pkt */ rem_size -= 4U; pkt = net_pkt_rx_alloc_with_buffer(context->iface, rem_size, AF_UNSPEC, 0, K_NO_WAIT); if (!pkt) { LOG_ERR("Failed to obtain RX buffer"); smsc_discard_pkt(); return NULL; } if (smsc_read_rx_fifo(pkt, rem_size) < 0) { smsc_discard_pkt(); net_pkt_unref(pkt); return NULL; } /* Discard FCS */ { uint32_t __unused dummy = SMSC9220->RX_DATA_PORT; } /* Adjust len of the last buf down for DWORD alignment */ if (pkt_size & 3) { net_pkt_update_length(pkt, net_pkt_get_len(pkt) - (4 - (pkt_size & 3))); } return pkt; } static void eth_smsc911x_isr(struct device *dev) { uint32_t int_status = SMSC9220->INT_STS; struct eth_context *context = dev->driver_data; LOG_DBG("%s: INT_STS=%x INT_EN=%x", __func__, int_status, SMSC9220->INT_EN); if (int_status & BIT(SMSC9220_INTERRUPT_RXSTATUS_FIFO_LEVEL)) { struct net_pkt *pkt; uint32_t pkt_size, val; uint32_t rx_stat; val = SMSC9220->RX_FIFO_INF; uint32_t pkt_pending = BFIELD(val, RX_FIFO_INF_RXSUSED); LOG_DBG("in RX FIFO: pkts: %u, bytes: %u", pkt_pending, BFIELD(val, RX_FIFO_INF_RXDUSED)); /* Ack rxstatus_fifo_level only when no packets pending. The * idea is to serve 1 packet per interrupt (e.g. to allow * higher priority interrupts to fire) by keeping interrupt * pending for as long as there're packets in FIFO. And when * there's none, finally acknowledge it. */ if (pkt_pending == 0U) { goto done; } int_status &= ~BIT(SMSC9220_INTERRUPT_RXSTATUS_FIFO_LEVEL); /* Make sure that any previously started discard op is * finished. */ smsc_wait_discard_pkt(); rx_stat = SMSC9220->RX_STAT_PORT; pkt_size = BFIELD(rx_stat, RX_STAT_PORT_PKT_LEN); LOG_DBG("pkt sz: %u", pkt_size); pkt = smsc_recv_pkt(dev, pkt_size); LOG_DBG("out RX FIFO: pkts: %u, bytes: %u", SMSC9220_BFIELD(RX_FIFO_INF, RXSUSED), SMSC9220_BFIELD(RX_FIFO_INF, RXDUSED)); if (pkt != NULL) { int res = net_recv_data(context->iface, pkt); if (res < 0) { LOG_ERR("net_recv_data: %d", res); net_pkt_unref(pkt); } } } done: /* Ack pending interrupts */ SMSC9220->INT_STS = int_status; } /* Bindings to the platform */ DEVICE_DECLARE(eth_smsc911x_0); int eth_init(struct device *dev) { IRQ_CONNECT(DT_INST_IRQN(0), DT_INST_IRQ(0, priority), eth_smsc911x_isr, DEVICE_GET(eth_smsc911x_0), 0); int ret = smsc_init(); if (ret != 0) { LOG_ERR("smsc911x failed to initialize"); return -ENODEV; } irq_enable(DT_INST_IRQN(0)); return ret; } static struct eth_context eth_0_context; ETH_NET_DEVICE_INIT(eth_smsc911x_0, "smsc911x_0", eth_init, device_pm_control_nop, ð_0_context, NULL /*ð_config_0*/, CONFIG_ETH_INIT_PRIORITY, &api_funcs, NET_ETH_MTU /*MTU*/);