| /* |
| * Copyright (c) 2021 IP-Logix Inc. |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT ethernet_phy |
| |
| #include <errno.h> |
| #include <zephyr/device.h> |
| #include <zephyr/init.h> |
| #include <soc.h> |
| #include <zephyr/drivers/mdio.h> |
| #include <zephyr/net/phy.h> |
| #include <zephyr/net/mii.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(phy_mii, CONFIG_PHY_LOG_LEVEL); |
| |
| struct phy_mii_dev_config { |
| uint8_t phy_addr; |
| bool no_reset; |
| bool fixed; |
| int fixed_speed; |
| const struct device * const mdio; |
| }; |
| |
| struct phy_mii_dev_data { |
| const struct device *dev; |
| phy_callback_t cb; |
| void *cb_data; |
| struct k_work_delayable monitor_work; |
| struct phy_link_state state; |
| struct k_sem sem; |
| }; |
| |
| static int phy_mii_get_link_state(const struct device *dev, |
| struct phy_link_state *state); |
| |
| static inline int reg_read(const struct device *dev, uint16_t reg_addr, |
| uint16_t *value) |
| { |
| const struct phy_mii_dev_config *const cfg = dev->config; |
| |
| return mdio_read(cfg->mdio, cfg->phy_addr, reg_addr, value); |
| } |
| |
| static inline int reg_write(const struct device *dev, uint16_t reg_addr, |
| uint16_t value) |
| { |
| const struct phy_mii_dev_config *const cfg = dev->config; |
| |
| return mdio_write(cfg->mdio, cfg->phy_addr, reg_addr, value); |
| } |
| |
| static int reset(const struct device *dev) |
| { |
| uint32_t timeout = 12U; |
| uint16_t value; |
| |
| /* Issue a soft reset */ |
| if (reg_write(dev, MII_BMCR, MII_BMCR_RESET) < 0) { |
| return -EIO; |
| } |
| |
| /* Wait up to 0.6s for the reset sequence to finish. According to |
| * IEEE 802.3, Section 2, Subsection 22.2.4.1.1 a PHY reset may take |
| * up to 0.5 s. |
| */ |
| do { |
| if (timeout-- == 0U) { |
| return -ETIMEDOUT; |
| } |
| |
| k_sleep(K_MSEC(50)); |
| |
| if (reg_read(dev, MII_BMCR, &value) < 0) { |
| return -EIO; |
| } |
| } while (value & MII_BMCR_RESET); |
| |
| return 0; |
| } |
| |
| static int get_id(const struct device *dev, uint32_t *phy_id) |
| { |
| uint16_t value; |
| |
| if (reg_read(dev, MII_PHYID1R, &value) < 0) { |
| return -EIO; |
| } |
| |
| *phy_id = (value & 0xFFFF) << 16; |
| |
| if (reg_read(dev, MII_PHYID2R, &value) < 0) { |
| return -EIO; |
| } |
| |
| *phy_id |= (value & 0xFFFF); |
| |
| return 0; |
| } |
| |
| static int update_link_state(const struct device *dev) |
| { |
| const struct phy_mii_dev_config *const cfg = dev->config; |
| struct phy_mii_dev_data *const data = dev->data; |
| bool link_up; |
| |
| uint16_t anar_reg = 0; |
| uint16_t bmcr_reg = 0; |
| uint16_t bmsr_reg = 0; |
| uint16_t anlpar_reg = 0; |
| uint32_t timeout = CONFIG_PHY_AUTONEG_TIMEOUT_MS / 100; |
| |
| if (reg_read(dev, MII_BMSR, &bmsr_reg) < 0) { |
| return -EIO; |
| } |
| |
| link_up = bmsr_reg & MII_BMSR_LINK_STATUS; |
| |
| /* If there is no change in link state don't proceed. */ |
| if (link_up == data->state.is_up) { |
| return -EAGAIN; |
| } |
| |
| data->state.is_up = link_up; |
| |
| /* If link is down, there is nothing more to be done */ |
| if (data->state.is_up == false) { |
| return 0; |
| } |
| |
| /** |
| * Perform auto-negotiation sequence. |
| */ |
| LOG_DBG("PHY (%d) Starting MII PHY auto-negotiate sequence", |
| cfg->phy_addr); |
| |
| /* Read PHY default advertising parameters */ |
| if (reg_read(dev, MII_ANAR, &anar_reg) < 0) { |
| return -EIO; |
| } |
| |
| /* Configure and start auto-negotiation process */ |
| if (reg_read(dev, MII_BMCR, &bmcr_reg) < 0) { |
| return -EIO; |
| } |
| |
| bmcr_reg |= MII_BMCR_AUTONEG_ENABLE | MII_BMCR_AUTONEG_RESTART; |
| bmcr_reg &= ~MII_BMCR_ISOLATE; /* Don't isolate the PHY */ |
| |
| if (reg_write(dev, MII_BMCR, bmcr_reg) < 0) { |
| return -EIO; |
| } |
| |
| /* Wait for the auto-negotiation process to complete */ |
| do { |
| if (timeout-- == 0U) { |
| LOG_DBG("PHY (%d) auto-negotiate timedout", |
| cfg->phy_addr); |
| return -ETIMEDOUT; |
| } |
| |
| k_sleep(K_MSEC(100)); |
| |
| if (reg_read(dev, MII_BMSR, &bmsr_reg) < 0) { |
| return -EIO; |
| } |
| } while (!(bmsr_reg & MII_BMSR_AUTONEG_COMPLETE)); |
| |
| LOG_DBG("PHY (%d) auto-negotiate sequence completed", |
| cfg->phy_addr); |
| |
| /** Read peer device capability */ |
| if (reg_read(dev, MII_ANLPAR, &anlpar_reg) < 0) { |
| return -EIO; |
| } |
| |
| /* Determine best link speed */ |
| if ((anar_reg & anlpar_reg) & MII_ADVERTISE_100_FULL) { |
| data->state.speed = LINK_FULL_100BASE_T; |
| } else if ((anar_reg & anlpar_reg) & MII_ADVERTISE_100_HALF) { |
| data->state.speed = LINK_HALF_100BASE_T; |
| } else if ((anar_reg & anlpar_reg) & MII_ADVERTISE_10_FULL) { |
| data->state.speed = LINK_FULL_10BASE_T; |
| } else { |
| data->state.speed = LINK_HALF_10BASE_T; |
| } |
| |
| LOG_INF("PHY (%d) Link speed %s Mb, %s duplex", |
| cfg->phy_addr, |
| PHY_LINK_IS_SPEED_100M(data->state.speed) ? "100" : "10", |
| PHY_LINK_IS_FULL_DUPLEX(data->state.speed) ? "full" : "half"); |
| |
| return 0; |
| } |
| |
| static void invoke_link_cb(const struct device *dev) |
| { |
| struct phy_mii_dev_data *const data = dev->data; |
| struct phy_link_state state; |
| |
| if (data->cb == NULL) { |
| return; |
| } |
| |
| phy_mii_get_link_state(dev, &state); |
| |
| data->cb(data->dev, &state, data->cb_data); |
| } |
| |
| static void monitor_work_handler(struct k_work *work) |
| { |
| struct k_work_delayable *dwork = k_work_delayable_from_work(work); |
| struct phy_mii_dev_data *const data = |
| CONTAINER_OF(dwork, struct phy_mii_dev_data, monitor_work); |
| const struct device *dev = data->dev; |
| int rc; |
| |
| k_sem_take(&data->sem, K_FOREVER); |
| |
| rc = update_link_state(dev); |
| |
| k_sem_give(&data->sem); |
| |
| /* If link state has changed and a callback is set, invoke callback */ |
| if (rc == 0) { |
| invoke_link_cb(dev); |
| } |
| |
| /* Submit delayed work */ |
| k_work_reschedule(&data->monitor_work, |
| K_MSEC(CONFIG_PHY_MONITOR_PERIOD)); |
| } |
| |
| static int phy_mii_read(const struct device *dev, uint16_t reg_addr, |
| uint32_t *data) |
| { |
| return reg_read(dev, reg_addr, (uint16_t *)data); |
| } |
| |
| static int phy_mii_write(const struct device *dev, uint16_t reg_addr, |
| uint32_t data) |
| { |
| return reg_write(dev, reg_addr, (uint16_t)data); |
| } |
| |
| static int phy_mii_cfg_link(const struct device *dev, |
| enum phy_link_speed adv_speeds) |
| { |
| uint16_t anar_reg; |
| uint16_t bmcr_reg; |
| |
| if (reg_read(dev, MII_ANAR, &anar_reg) < 0) { |
| return -EIO; |
| } |
| |
| if (reg_read(dev, MII_BMCR, &bmcr_reg) < 0) { |
| return -EIO; |
| } |
| |
| if (adv_speeds & LINK_FULL_10BASE_T) |
| anar_reg |= MII_ADVERTISE_10_FULL; |
| else |
| anar_reg &= ~MII_ADVERTISE_10_FULL; |
| |
| if (adv_speeds & LINK_HALF_10BASE_T) |
| anar_reg |= MII_ADVERTISE_10_HALF; |
| else |
| anar_reg &= ~MII_ADVERTISE_10_HALF; |
| |
| if (adv_speeds & LINK_FULL_100BASE_T) |
| anar_reg |= MII_ADVERTISE_100_FULL; |
| else |
| anar_reg &= ~MII_ADVERTISE_100_FULL; |
| |
| if (adv_speeds & LINK_HALF_100BASE_T) |
| anar_reg |= MII_ADVERTISE_100_HALF; |
| else |
| anar_reg &= ~MII_ADVERTISE_100_HALF; |
| |
| bmcr_reg |= MII_BMCR_AUTONEG_ENABLE; |
| |
| if (reg_write(dev, MII_ANAR, anar_reg) < 0) { |
| return -EIO; |
| } |
| |
| if (reg_write(dev, MII_BMCR, bmcr_reg) < 0) { |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static int phy_mii_get_link_state(const struct device *dev, |
| struct phy_link_state *state) |
| { |
| struct phy_mii_dev_data *const data = dev->data; |
| |
| k_sem_take(&data->sem, K_FOREVER); |
| |
| memcpy(state, &data->state, sizeof(struct phy_link_state)); |
| |
| k_sem_give(&data->sem); |
| |
| return 0; |
| } |
| |
| static int phy_mii_link_cb_set(const struct device *dev, phy_callback_t cb, |
| void *user_data) |
| { |
| struct phy_mii_dev_data *const data = dev->data; |
| |
| data->cb = cb; |
| data->cb_data = user_data; |
| |
| /** |
| * Immediately invoke the callback to notify the caller of the |
| * current link status. |
| */ |
| invoke_link_cb(dev); |
| |
| return 0; |
| } |
| |
| static int phy_mii_initialize(const struct device *dev) |
| { |
| const struct phy_mii_dev_config *const cfg = dev->config; |
| struct phy_mii_dev_data *const data = dev->data; |
| uint32_t phy_id; |
| |
| k_sem_init(&data->sem, 1, 1); |
| |
| data->dev = dev; |
| data->cb = NULL; |
| |
| /** |
| * If this is a *fixed* link then we don't need to communicate |
| * with a PHY. We set the link parameters as configured |
| * and set link state to up. |
| */ |
| if (cfg->fixed) { |
| const static int speed_to_phy_link_speed[] = { |
| LINK_HALF_10BASE_T, |
| LINK_FULL_10BASE_T, |
| LINK_HALF_100BASE_T, |
| LINK_FULL_100BASE_T, |
| }; |
| |
| data->state.speed = speed_to_phy_link_speed[cfg->fixed_speed]; |
| data->state.is_up = true; |
| } else { |
| data->state.is_up = false; |
| |
| mdio_bus_enable(cfg->mdio); |
| |
| if (cfg->no_reset == false) { |
| reset(dev); |
| } |
| |
| if (get_id(dev, &phy_id) == 0) { |
| if (phy_id == 0xFFFFFF) { |
| LOG_ERR("No PHY found at address %d", |
| cfg->phy_addr); |
| |
| return -EINVAL; |
| } |
| |
| LOG_INF("PHY (%d) ID %X", cfg->phy_addr, phy_id); |
| } |
| |
| /* Advertise all speeds */ |
| phy_mii_cfg_link(dev, LINK_HALF_10BASE_T | |
| LINK_FULL_10BASE_T | |
| LINK_HALF_100BASE_T | |
| LINK_FULL_100BASE_T); |
| |
| k_work_init_delayable(&data->monitor_work, |
| monitor_work_handler); |
| |
| monitor_work_handler(&data->monitor_work.work); |
| } |
| |
| return 0; |
| } |
| |
| #define IS_FIXED_LINK(n) DT_INST_NODE_HAS_PROP(n, fixed_link) |
| |
| static const struct ethphy_driver_api phy_mii_driver_api = { |
| .get_link = phy_mii_get_link_state, |
| .cfg_link = phy_mii_cfg_link, |
| .link_cb_set = phy_mii_link_cb_set, |
| .read = phy_mii_read, |
| .write = phy_mii_write, |
| }; |
| |
| #define PHY_MII_CONFIG(n) \ |
| static const struct phy_mii_dev_config phy_mii_dev_config_##n = { \ |
| .phy_addr = DT_INST_PROP(n, address), \ |
| .fixed = IS_FIXED_LINK(n), \ |
| .fixed_speed = DT_INST_ENUM_IDX_OR(n, fixed_link, 0), \ |
| .mdio = UTIL_AND(UTIL_NOT(IS_FIXED_LINK(n)), \ |
| DEVICE_DT_GET(DT_INST_PHANDLE(n, mdio))) \ |
| }; |
| |
| #define PHY_MII_DEVICE(n) \ |
| PHY_MII_CONFIG(n); \ |
| static struct phy_mii_dev_data phy_mii_dev_data_##n; \ |
| DEVICE_DT_INST_DEFINE(n, \ |
| &phy_mii_initialize, \ |
| NULL, \ |
| &phy_mii_dev_data_##n, \ |
| &phy_mii_dev_config_##n, POST_KERNEL, \ |
| CONFIG_PHY_INIT_PRIORITY, \ |
| &phy_mii_driver_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(PHY_MII_DEVICE) |