diff --git a/CODEOWNERS b/CODEOWNERS index 4a71903a42a..b032ff60b19 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -222,6 +222,7 @@ /drivers/ethernet/*stm32* @Nukersson @lochej /drivers/ethernet/*w5500* @parthitce /drivers/ethernet/*xlnx_gem* @ibirnbaum +/drivers/ethernet/phy/ @rlubos @tbursztyka @arvinf /drivers/mdio/ @rlubos @tbursztyka @arvinf /drivers/flash/ @nashif @nvlsianpu /drivers/flash/*b91* @yurvyn diff --git a/drivers/ethernet/CMakeLists.txt b/drivers/ethernet/CMakeLists.txt index dd569345529..f37e041ba1e 100644 --- a/drivers/ethernet/CMakeLists.txt +++ b/drivers/ethernet/CMakeLists.txt @@ -40,3 +40,5 @@ if(CONFIG_ETH_NATIVE_POSIX) eth_native_posix_adapt.c ) endif() + +add_subdirectory(phy) diff --git a/drivers/ethernet/Kconfig b/drivers/ethernet/Kconfig index 05478d644f9..6dd2405b5fd 100644 --- a/drivers/ethernet/Kconfig +++ b/drivers/ethernet/Kconfig @@ -59,4 +59,6 @@ source "drivers/ethernet/Kconfig.w5500" source "drivers/ethernet/Kconfig.dsa" source "drivers/ethernet/Kconfig.xlnx_gem" +source "drivers/ethernet/phy/Kconfig" + endmenu # "Ethernet Drivers" diff --git a/drivers/ethernet/phy/CMakeLists.txt b/drivers/ethernet/phy/CMakeLists.txt new file mode 100644 index 00000000000..57f312a8efa --- /dev/null +++ b/drivers/ethernet/phy/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_sources_ifdef(CONFIG_PHY_GENERIC_MII phy_mii.c) diff --git a/drivers/ethernet/phy/Kconfig b/drivers/ethernet/phy/Kconfig new file mode 100644 index 00000000000..070bd8960ca --- /dev/null +++ b/drivers/ethernet/phy/Kconfig @@ -0,0 +1,47 @@ +# Ethernet PHY drivers configuration options + +# Copyright (c) 2021 IP-Logix Inc. +# SPDX-License-Identifier: Apache-2.0 + +menu "Ethernet PHY Drivers" + depends on NET_L2_ETHERNET + +module = PHY +module-dep = LOG +module-str = Log level for Ethernet PHY driver +module-help = Sets log level for Ethernet PHY Device Drivers. +source "subsys/net/Kconfig.template.log_config.net" + +config PHY_INIT_PRIORITY + int "Ethernet PHY driver init priority" + default 70 + help + Ethernet PHY device driver initialization priority. + Do not mess with it unless you know what you are doing. + Note that the priority needs to be lower than the net stack + so that it can start before the networking sub-system. + +config PHY_GENERIC_MII + bool "Generic MII PHY Driver" + default y + depends on MDIO + help + This is a generic MII PHY interface that communicates with the + PHY using the MDIO bus. + +config PHY_AUTONEG_TIMEOUT_MS + int "Auto-negotiation timeout value in milliseconds" + default 4000 + help + Maximum duration of auto-negotiation sequence in milliseconds + before the process fails + +config PHY_MONITOR_PERIOD + int "Monitor task execution period" + default 500 + help + Monitor task execution period in milliseconds. The monitor task is + periodically executed to detect and report any changes in the + PHY link status to the operating system. + +endmenu # "Ethernet PHY Drivers" diff --git a/drivers/ethernet/phy/phy_mii.c b/drivers/ethernet/phy/phy_mii.c new file mode 100644 index 00000000000..998183f4307 --- /dev/null +++ b/drivers/ethernet/phy/phy_mii.c @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2021 IP-Logix Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT ethernet_phy + +#include +#include +#include +#include +#include +#include +#include + +#include +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; +}; + +#define DEV_NAME(dev) ((dev)->name) +#define DEV_DATA(dev) ((struct phy_mii_dev_data *const)(dev)->data) +#define DEV_CFG(dev) \ + ((const struct phy_mii_dev_config *const)(dev)->config) + +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_CFG(dev); + + 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_CFG(dev); + + 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_CFG(dev); + struct phy_mii_dev_data *const data = DEV_DATA(dev); + 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(dev); + 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 phy_mii_dev_data *const data = + CONTAINER_OF(work, 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(dev); + + 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(dev); + + 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_CFG(dev); + struct phy_mii_dev_data *const data = DEV_DATA(dev); + 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_NODE_HAS_PROP(DT_DRV_INST(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_PROP(DT_DRV_INST(n), address), \ + .fixed = IS_FIXED_LINK(n), \ + .fixed_speed = DT_ENUM_IDX_OR(DT_DRV_INST(n), fixed_link, 0), \ + .mdio = UTIL_AND(UTIL_NOT(IS_FIXED_LINK(n)), \ + DEVICE_DT_GET(DT_PHANDLE(DT_DRV_INST(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) diff --git a/dts/bindings/ethernet/ethernet-phy.yaml b/dts/bindings/ethernet/ethernet-phy.yaml new file mode 100644 index 00000000000..5ba01e7b06c --- /dev/null +++ b/dts/bindings/ethernet/ethernet-phy.yaml @@ -0,0 +1,33 @@ +# Copyright (c) 2021 IP-Logix Inc. +# SPDX-License-Identifier: Apache-2.0 + +# Common fields for MIIPHY devices + +description: Generic MII PHY + +compatible: "ethernet-phy" + +include: phy.yaml + +properties: + address: + type: int + required: true + description: PHY address + mdio: + type: phandle + required: true + description: MDIO driver node + no-reset: + type: boolean + required: false + description: Do not reset the PHY during initialization + fixed-link: + type: string + required: false + description: This link is fixed and does not require PHY configuration + enum: + - "10BASE-T Half-Duplex" + - "10BASE-T Full-Duplex" + - "100BASE-T Half-Duplex" + - "100BASE-T Full-Duplex" diff --git a/dts/bindings/ethernet/phy.yaml b/dts/bindings/ethernet/phy.yaml new file mode 100644 index 00000000000..8a66919cf29 --- /dev/null +++ b/dts/bindings/ethernet/phy.yaml @@ -0,0 +1,6 @@ +# Copyright (c) 2021 IP-Logix Inc. +# SPDX-License-Identifier: Apache-2.0 + +# Common fields for PHY devices + +include: base.yaml diff --git a/include/net/phy.h b/include/net/phy.h new file mode 100644 index 00000000000..1e64d7b4ab7 --- /dev/null +++ b/include/net/phy.h @@ -0,0 +1,231 @@ +/** + * @file + * + * @brief Public APIs for Ethernet PHY drivers. + */ + +/* + * Copyright (c) 2021 IP-Logix Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef ZEPHYR_INCLUDE_DRIVERS_PHY_H_ +#define ZEPHYR_INCLUDE_DRIVERS_PHY_H_ + +/** + * @brief Ethernet PHY Interface + * @defgroup ethernet_phy Ethernet PHY Interface + * @ingroup networking + * @{ + */ +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Ethernet link speeds. */ +enum phy_link_speed { + /** 10Base-T Half-Duplex */ + LINK_HALF_10BASE_T = BIT(0), + /** 10Base-T Full-Duplex */ + LINK_FULL_10BASE_T = BIT(1), + /** 100Base-T Half-Duplex */ + LINK_HALF_100BASE_T = BIT(2), + /** 100Base-T Full-Duplex */ + LINK_FULL_100BASE_T = BIT(3), +}; + +#define PHY_LINK_IS_FULL_DUPLEX(x) (x & (BIT(1) | BIT(3))) +#define PHY_LINK_IS_SPEED_100M(x) (x & (BIT(2) | BIT(3))) + +/** @brief Link state */ +struct phy_link_state { + /** Link speed */ + enum phy_link_speed speed; + /** When true the link is active and connected */ + bool is_up; +}; + +/** + * @typedef phy_callback_t + * @brief Define the callback function signature for + * `phy_link_callback_set()` function. + * + * @param dev PHY device structure + * @param state Pointer to link_state structure. + * @param user_data Pointer to data specified by user + */ +typedef void (*phy_callback_t)(const struct device *dev, + struct phy_link_state *state, + void *user_data); + +/** + * @cond INTERNAL_HIDDEN + * + * These are for internal use only, so skip these in + * public documentation. + */ +__subsystem struct ethphy_driver_api { + /** Get link state */ + int (*get_link)(const struct device *dev, + struct phy_link_state *state); + + /** Configure link */ + int (*cfg_link)(const struct device *dev, + enum phy_link_speed adv_speeds); + + /** Set callback to be invoked when link state changes. */ + int (*link_cb_set)(const struct device *dev, phy_callback_t cb, + void *user_data); + + /** Read PHY register */ + int (*read)(const struct device *dev, uint16_t reg_addr, + uint32_t *data); + + /** Write PHY register */ + int (*write)(const struct device *dev, uint16_t reg_addr, + uint32_t data); +}; +/** + * @endcond + */ + +/** + * @brief Configure PHY link + * + * This route configures the advertised link speeds. + * + * @param[in] dev PHY device structure + * @param speeds OR'd link speeds to be advertised by the PHY + * + * @retval 0 If successful. + * @retval -EIO If communication with PHY failed. + * @retval -ENOTSUP If not supported. + */ +__syscall int phy_configure_link(const struct device *dev, + enum phy_link_speed speeds); + +static inline int z_impl_phy_configure_link(const struct device *dev, + enum phy_link_speed speeds) +{ + const struct ethphy_driver_api *api = + (const struct ethphy_driver_api *)dev->api; + + return api->cfg_link(dev, speeds); +} + +/** + * @brief Get PHY link state + * + * Returns the current state of the PHY link. This can be used by + * to determine when a link is up and the negotiated link speed. + * + * + * @param[in] dev PHY device structure + * @param state Pointer to receive PHY state + * + * @retval 0 If successful. + * @retval -EIO If communication with PHY failed. + */ +__syscall int phy_get_link_state(const struct device *dev, + struct phy_link_state *state); + +static inline int z_impl_phy_get_link_state(const struct device *dev, + struct phy_link_state *state) +{ + const struct ethphy_driver_api *api = + (const struct ethphy_driver_api *)dev->api; + + return api->get_link(dev, state); +} + +/** + * @brief Set link state change callback + * + * Sets a callback that is invoked when link state changes. This is the + * preferred method for ethernet drivers to be notified of the PHY link + * state change. + * + * @param[in] dev PHY device structure + * @param callback Callback handler + * @param user_data Pointer to data specified by user. + * + * @retval 0 If successful. + * @retval -ENOTSUP If not supported. + */ +__syscall int phy_link_callback_set(const struct device *dev, + phy_callback_t callback, + void *user_data); + +static inline int z_impl_phy_link_callback_set(const struct device *dev, + phy_callback_t callback, + void *user_data) +{ + const struct ethphy_driver_api *api = + (const struct ethphy_driver_api *)dev->api; + + return api->link_cb_set(dev, callback, user_data); +} + +/** + * @brief Read PHY registers + * + * This routine provides a generic interface to read from a PHY register. + * + * @param[in] dev PHY device structure + * @param[in] reg_addr Register address + * @param value Pointer to receive read value + * + * @retval 0 If successful. + * @retval -EIO If communication with PHY failed. + */ +__syscall int phy_read(const struct device *dev, uint16_t reg_addr, + uint32_t *value); + +static inline int z_impl_phy_read(const struct device *dev, uint16_t reg_addr, + uint32_t *value) +{ + const struct ethphy_driver_api *api = + (const struct ethphy_driver_api *)dev->api; + + return api->read(dev, reg_addr, value); +} + +/** + * @brief Write PHY register + * + * This routine provides a generic interface to write to a PHY register. + * + * @param[in] dev PHY device structure + * @param[in] reg_addr Register address + * @param[in] value Value to write + * + * @retval 0 If successful. + * @retval -EIO If communication with PHY failed. + */ +__syscall int phy_write(const struct device *dev, uint16_t reg_addr, + uint32_t value); + +static inline int z_impl_phy_write(const struct device *dev, uint16_t reg_addr, + uint32_t value) +{ + const struct ethphy_driver_api *api = + (const struct ethphy_driver_api *)dev->api; + + return api->write(dev, reg_addr, value); +} + + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#include + +#endif /* ZEPHYR_INCLUDE_DRIVERS_PHY_H_ */