blob: 1cf6d621c828af6fba00e75cea4d7ef1edc6f27d [file] [log] [blame]
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) 2024 sensry.io
*/
#define DT_DRV_COMPAT microchip_vsc8541
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(microchip_vsc8541, CONFIG_PHY_LOG_LEVEL);
#include <zephyr/kernel.h>
#include <zephyr/net/phy.h>
#include <zephyr/net/mii.h>
#include <zephyr/drivers/mdio.h>
#include <string.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/drivers/gpio.h>
#include "phy_mii.h"
/* phy page selectors */
#define PHY_PAGE_0 0x00 /* main registers space active */
#define PHY_PAGE_1 0x01 /* reg 16 - 30 will be redirected to ext. register space 1 */
#define PHY_PAGE_2 0x02 /* reg 16 - 30 will be redirected to ext. register space 2 */
#define PHY_PAGE_3 0x03 /* reg 0 - 30 will be redirected to gpio register space */
/* virtual register, treat higher byte as page selector and lower byte is register */
#define PHY_REG(page, reg) ((page << 8) | (reg << 0))
/* Generic Register */
#define PHY_REG_PAGE0_STAT100 PHY_REG(PHY_PAGE_0, 0x10)
#define PHY_REG_PAGE0_STAT1000_EXT2 PHY_REG(PHY_PAGE_0, 0x11)
#define PHY_REG_AUX_CTRL 0x12
#define PHY_REG_PAGE0_ERROR_COUNTER_1 PHY_REG(0, 0x13)
#define PHY_REG_PAGE0_ERROR_COUNTER_2 PHY_REG(0, 0x14)
#define PHY_REG_PAGE0_EXT_CTRL_STAT PHY_REG(PHY_PAGE_0, 0x16)
#define PHY_REG_PAGE0_EXT_CONTROL_1 PHY_REG(PHY_PAGE_0, 0x17)
#define PHY_REG_PAGE0_EXT_DEV_AUX PHY_REG(PHY_PAGE_0, 0x1C)
#define PHY_REG_LED_MODE 0x1d
#define PHY_REG_PAGE_SELECTOR 0x1F
/* Extended Register */
#define PHY_REG_PAGE1_EXT_MODE_CTRL PHY_REG(PHY_PAGE_1, 0x13)
#define PHY_REG_PAGE2_RGMII_CONTROL PHY_REG(PHY_PAGE_2, 0x14)
#define PHY_REG_PAGE2_MAC_IF_CONTROL PHY_REG(PHY_PAGE_2, 0x1b)
#define PHY_REG_PAGE0_EXT_DEV_AUX_DUPLEX BIT(5)
enum vsc8541_interface {
VSC8541_MII,
VSC8541_RMII,
VSC8541_GMII,
VSC8541_RGMII,
};
/* Thread stack size */
#define STACK_SIZE 512
/* Thread priority */
#define THREAD_PRIORITY 7
struct mc_vsc8541_config {
uint8_t addr;
const struct device *mdio_dev;
enum vsc8541_interface microchip_interface_type;
enum phy_link_speed default_speeds;
uint8_t rgmii_rx_clk_delay;
uint8_t rgmii_tx_clk_delay;
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(reset_gpios)
const struct gpio_dt_spec reset_gpio;
#endif
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(int_gpios)
const struct gpio_dt_spec interrupt_gpio;
#endif
};
struct mc_vsc8541_data {
const struct device *dev;
struct phy_link_state state;
int active_page;
struct k_mutex mutex;
phy_callback_t cb;
void *cb_data;
struct k_thread link_monitor_thread;
uint8_t link_monitor_thread_stack[STACK_SIZE];
};
static int phy_mc_vsc8541_read(const struct device *dev, uint16_t reg_addr, uint16_t *data);
static int phy_mc_vsc8541_write(const struct device *dev, uint16_t reg_addr, uint16_t data);
static void phy_mc_vsc8541_link_monitor(void *arg1, void *arg2, void *arg3);
#if CONFIG_PHY_VERIFY_DEVICE_IDENTIFICATION
/**
* @brief Reads the phy manufacture id and compares to designed, known model version
*/
static int phy_mc_vsc8541_verify_phy_id(const struct device *dev)
{
uint16_t phy_id_1;
uint16_t phy_id_2;
if (phy_mc_vsc8541_read(dev, MII_PHYID1R, &phy_id_1) < 0) {
return -EINVAL;
}
if (phy_mc_vsc8541_read(dev, MII_PHYID2R, &phy_id_2) < 0) {
return -EINVAL;
}
if (phy_id_1 == 0x0007) {
if (phy_id_2 == 0x0771) {
LOG_INF("model vsc8541-01 rev b");
return 0;
}
if (phy_id_2 == 0x0772) {
LOG_INF("model vsc8541-02/-05 rev c");
return 0;
}
}
LOG_INF("phy id is %#.4x - %#.4x", phy_id_1, phy_id_2);
return -EINVAL;
}
#endif
/**
* @brief Low level reset procedure that toggles the reset gpio
*/
static int phy_mc_vsc8541_reset(const struct device *dev)
{
const struct mc_vsc8541_config *cfg = dev->config;
int ret;
uint16_t reg = 0U;
int count = 0;
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(reset_gpios)
if (!cfg->reset_gpio.port) {
LOG_WRN("missing reset port definition");
return -EINVAL;
}
/* configure the reset pin */
ret = gpio_pin_configure_dt(&cfg->reset_gpio, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
for (uint32_t i = 0; i < 2; i++) {
/* Start reset */
ret = gpio_pin_set_dt(&cfg->reset_gpio, 1);
if (ret < 0) {
LOG_WRN("failed to set reset gpio");
return -EINVAL;
}
/* Wait as specified by datasheet */
k_sleep(K_MSEC(200));
/* Reset over */
gpio_pin_set_dt(&cfg->reset_gpio, 0);
/* After de-asserting reset, must wait before using the config interface */
k_sleep(K_MSEC(200));
}
#endif /* DT_ANY_INST_HAS_PROP_STATUS_OKAY(reset_gpios) */
/* According to IEEE 802.3, Section 2, Subsection 22.2.4.1.1,
* a PHY reset may take up to 0.5 s.
*/
k_sleep(K_MSEC(500));
#if CONFIG_PHY_VERIFY_DEVICE_IDENTIFICATION
/* confirm phy organizationally unique identifier, if enabled */
if (phy_mc_vsc8541_verify_phy_id(dev) < 0) {
LOG_ERR("failed to verify phy id");
return -EINVAL;
}
#endif
/* set RGMII mode (must be executed BEFORE software reset -- see datasheet) */
if (cfg->microchip_interface_type == VSC8541_RGMII) {
ret = phy_mc_vsc8541_write(dev, PHY_REG_PAGE0_EXT_CONTROL_1,
(0x0 << 13) | (0x2 << 11));
if (ret < 0) {
return ret;
}
}
/* software reset */
ret = phy_mc_vsc8541_write(dev, MII_BMCR, MII_BMCR_RESET);
if (ret < 0) {
return ret;
}
/* wait for phy finished software reset */
do {
ret = phy_mc_vsc8541_read(dev, MII_BMCR, &reg);
if (ret < 0) {
return ret;
}
if (count++ > 1000) {
LOG_ERR("phy reset timed out");
return -ETIMEDOUT;
}
} while ((reg & MII_BMCR_RESET) != 0U);
/* configure the RGMII clk delay */
reg = 0x0;
/* RX_CLK delay */
reg |= (cfg->rgmii_rx_clk_delay << 4);
/* TX_CLK delay */
reg |= (cfg->rgmii_tx_clk_delay << 0);
return phy_mc_vsc8541_write(dev, PHY_REG_PAGE2_RGMII_CONTROL, reg);
}
/**
* @brief Update the phy state with current speed given by readings of the phy
*
* @param dev device structure of the phy
* @param state being updated (we only update speed here)
*/
static int phy_mc_vsc8541_get_speed(const struct device *dev, struct phy_link_state *state)
{
int ret;
uint16_t aux_status;
uint16_t link10_status;
uint16_t link100_status;
uint16_t link1000_status;
bool is_duplex;
ret = phy_mc_vsc8541_read(dev, PHY_REG_PAGE0_EXT_DEV_AUX, &aux_status);
if (ret < 0) {
return ret;
}
is_duplex = (aux_status & PHY_REG_PAGE0_EXT_DEV_AUX_DUPLEX) != 0U;
ret = phy_mc_vsc8541_read(dev, PHY_REG_PAGE0_STAT1000_EXT2, &link1000_status);
if (ret < 0) {
return ret;
}
if ((link1000_status & BIT(12)) != 0U) {
state->speed = is_duplex ? LINK_FULL_1000BASE : LINK_HALF_1000BASE;
return 0; /* no need to check lower speeds */
}
ret = phy_mc_vsc8541_read(dev, PHY_REG_PAGE0_STAT100, &link100_status);
if (ret < 0) {
return ret;
}
if ((link100_status & BIT(12)) != 0U) {
state->speed = is_duplex ? LINK_FULL_100BASE : LINK_HALF_100BASE;
return 0; /* no need to check lower speeds */
}
ret = phy_mc_vsc8541_read(dev, PHY_REG_PAGE0_EXT_CTRL_STAT, &link10_status);
if (ret < 0) {
return ret;
}
if ((link10_status & BIT(6)) != 0U) {
state->speed = is_duplex ? LINK_FULL_10BASE : LINK_HALF_10BASE;
} else {
state->speed = 0; /* no link */
}
return 0;
}
static int phy_mc_vsc8541_cfg_link(const struct device *dev, enum phy_link_speed adv_speeds,
enum phy_cfg_link_flag flags)
{
int ret;
if ((flags & PHY_FLAG_AUTO_NEGOTIATION_DISABLED) != 0U) {
ret = phy_mii_set_bmcr_reg_autoneg_disabled(dev, adv_speeds);
} else {
ret = phy_mii_cfg_link_autoneg(dev, adv_speeds, true);
}
return ret;
}
/**
* @brief Initializes the phy and starts the link monitor
*
*/
static int phy_mc_vsc8541_init(const struct device *dev)
{
struct mc_vsc8541_data *data = dev->data;
const struct mc_vsc8541_config *cfg = dev->config;
int ret;
data->active_page = -1;
k_mutex_init(&data->mutex);
/* Reset PHY */
ret = phy_mc_vsc8541_reset(dev);
if (ret < 0) {
LOG_ERR("initialize failed");
return ret;
}
/* setup thread to watch link state */
k_thread_create(&data->link_monitor_thread,
(k_thread_stack_t *)data->link_monitor_thread_stack, STACK_SIZE,
phy_mc_vsc8541_link_monitor, (void *)dev, NULL, NULL, THREAD_PRIORITY, 0,
K_NO_WAIT);
k_thread_name_set(&data->link_monitor_thread, "phy-link-mon");
phy_mc_vsc8541_cfg_link(dev, cfg->default_speeds, 0);
return 0;
}
/**
* @brief Update the link status with readings from phy
*
* @param dev device structure to phy
* @param state which is being updated
*/
static int phy_mc_vsc8541_get_link(const struct device *dev, struct phy_link_state *state)
{
int ret;
uint16_t reg_sr;
uint16_t reg_cr;
bool hasLink;
bool auto_negotiation_finished = true;
ret = phy_mc_vsc8541_read(dev, MII_BMSR, &reg_sr);
if (ret < 0) {
return ret;
}
ret = phy_mc_vsc8541_read(dev, MII_BMCR, &reg_cr);
if (ret < 0) {
return ret;
}
hasLink = (reg_sr & MII_BMSR_LINK_STATUS) != 0;
if ((reg_cr & MII_BMCR_AUTONEG_ENABLE) != 0U) {
/* auto negotiation active; update status */
auto_negotiation_finished = (reg_sr & MII_BMSR_AUTONEG_COMPLETE) != 0;
}
if (hasLink && auto_negotiation_finished) {
state->is_up = true;
ret = phy_mc_vsc8541_get_speed(dev, state);
if (ret < 0) {
return ret;
}
} else {
state->is_up = false;
state->speed = 0;
}
return 0;
}
/**
* @brief Set callback which is used to announce link status changes
*
* @param dev device structure to phy device
* @param cb
* @param user_data
*/
static int phy_mc_vsc8541_link_cb_set(const struct device *dev, phy_callback_t cb, void *user_data)
{
struct mc_vsc8541_data *data = dev->data;
data->cb = cb;
data->cb_data = user_data;
data->cb(dev, &data->state, data->cb_data);
return 0;
}
/**
* @brief Monitor thread to check the link state and announce if changed
*
* @param arg1 provides a pointer to device structure
* @param arg2 not used
* @param arg3 not used
*/
void phy_mc_vsc8541_link_monitor(void *arg1, void *arg2, void *arg3)
{
const struct device *dev = arg1;
struct mc_vsc8541_data *data = dev->data;
struct phy_link_state new_state;
while (1) {
k_sleep(K_MSEC(CONFIG_PHY_MONITOR_PERIOD));
phy_mc_vsc8541_get_link(dev, &new_state);
if ((new_state.is_up != data->state.is_up) ||
(new_state.speed != data->state.speed)) {
/* state changed */
data->state.is_up = new_state.is_up;
data->state.speed = new_state.speed;
if (data->cb) {
/* announce new state */
data->cb(dev, &data->state, data->cb_data);
}
}
}
}
/**
* @brief Reading of phy register content at given address via mdio interface
*
* - high byte of register address defines page
* - low byte of register address defines the address within selected page
* - to speed up, we store the last used page and only swap page if needed
*
*/
static int phy_mc_vsc8541_read(const struct device *dev, uint16_t reg_addr, uint16_t *data)
{
const struct mc_vsc8541_config *cfg = dev->config;
struct mc_vsc8541_data *dev_data = dev->data;
int ret;
*data = 0U;
/* decode page */
uint16_t page = reg_addr >> 8;
/* mask out lower byte */
reg_addr &= 0x00ff;
k_mutex_lock(&dev_data->mutex, K_FOREVER);
mdio_bus_enable(cfg->mdio_dev);
/* select page, given by register upper byte */
if (dev_data->active_page != page) {
ret = mdio_write(cfg->mdio_dev, cfg->addr, PHY_REG_PAGE_SELECTOR, page);
if (ret < 0) {
goto read_end;
}
dev_data->active_page = (int)page;
}
/* select register, given by register lower byte */
ret = mdio_read(cfg->mdio_dev, cfg->addr, reg_addr, data);
read_end:
mdio_bus_disable(cfg->mdio_dev);
k_mutex_unlock(&dev_data->mutex);
return ret;
}
/**
* @brief Reading of phy register content at given address via mdio interface
* For external API using uint32_t as data type
*/
static int phy_mc_vsc8541_read_ext(const struct device *dev, uint16_t reg_addr, uint32_t *data)
{
return phy_mc_vsc8541_read(dev, reg_addr, (uint16_t *)data);
}
/**
* @brief Writing of new value to phy register at given address via mdio interface
*
* - high byte of register address defines page
* - low byte of register address defines the address within selected page
* - to speed up, we store the last used page and only swap page if needed
*
*/
static int phy_mc_vsc8541_write(const struct device *dev, uint16_t reg_addr, uint16_t data)
{
const struct mc_vsc8541_config *cfg = dev->config;
struct mc_vsc8541_data *dev_data = dev->data;
int ret;
/* decode page */
uint16_t page = reg_addr >> 8;
/* mask out lower byte */
reg_addr &= 0x00ff;
k_mutex_lock(&dev_data->mutex, K_FOREVER);
mdio_bus_enable(cfg->mdio_dev);
/* select page, given by register upper byte */
if (dev_data->active_page != page) {
ret = mdio_write(cfg->mdio_dev, cfg->addr, PHY_REG_PAGE_SELECTOR, (uint16_t)page);
if (ret < 0) {
goto write_end;
}
dev_data->active_page = (int)page;
}
/* write register, given by lower byte */
ret = mdio_write(cfg->mdio_dev, cfg->addr, reg_addr, data);
write_end:
mdio_bus_disable(cfg->mdio_dev);
k_mutex_unlock(&dev_data->mutex);
return ret;
}
/**
* @brief Writing of new value to phy register at given address via mdio interface
* For external API using uint32_t as data type
*/
static int phy_mc_vsc8541_write_ext(const struct device *dev, uint16_t reg_addr, uint32_t data)
{
return phy_mc_vsc8541_write(dev, reg_addr, (uint16_t)data);
}
static DEVICE_API(ethphy, mc_vsc8541_phy_api) = {
.get_link = phy_mc_vsc8541_get_link,
.cfg_link = phy_mc_vsc8541_cfg_link,
.link_cb_set = phy_mc_vsc8541_link_cb_set,
.read = phy_mc_vsc8541_read_ext,
.write = phy_mc_vsc8541_write_ext,
};
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(reset_gpios)
#define RESET_GPIO(n) .reset_gpio = GPIO_DT_SPEC_INST_GET_OR(n, reset_gpios, {0}),
#else
#define RESET_GPIO(n)
#endif /* reset gpio */
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(int_gpios)
#define INTERRUPT_GPIO(n) .interrupt_gpio = GPIO_DT_SPEC_INST_GET_OR(n, int_gpios, {0}),
#else
#define INTERRUPT_GPIO(n)
#endif /* interrupt gpio */
#define MICROCHIP_VSC8541_INIT(n) \
static const struct mc_vsc8541_config mc_vsc8541_##n##_config = { \
.addr = DT_INST_REG_ADDR(n), \
.mdio_dev = DEVICE_DT_GET(DT_INST_PARENT(n)), \
.microchip_interface_type = DT_INST_ENUM_IDX(n, microchip_interface_type), \
.rgmii_rx_clk_delay = DT_INST_PROP(n, microchip_rgmii_rx_clk_delay), \
.rgmii_tx_clk_delay = DT_INST_PROP(n, microchip_rgmii_tx_clk_delay), \
.default_speeds = PHY_INST_GENERATE_DEFAULT_SPEEDS(n), \
RESET_GPIO(n) INTERRUPT_GPIO(n)}; \
\
static struct mc_vsc8541_data mc_vsc8541_##n##_data; \
\
DEVICE_DT_INST_DEFINE(n, &phy_mc_vsc8541_init, NULL, &mc_vsc8541_##n##_data, \
&mc_vsc8541_##n##_config, POST_KERNEL, CONFIG_PHY_INIT_PRIORITY, \
&mc_vsc8541_phy_api);
DT_INST_FOREACH_STATUS_OKAY(MICROCHIP_VSC8541_INIT)