blob: c7b22b0f658eeb7941babecb4a5e02006932da34 [file] [log] [blame]
/*
* 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)