基于335X平台Linux交换芯片驱动开发

2022-11-17,,,,

基于335X平台Linux交换芯片驱动开发

 

一、软硬件平台资料

1、开发板:创龙AM3359核心板,网口采用RMII形式。

2、Kernel版本:4.4.12,采用FDT

3、交换芯片MARVELL的88E6321.

二、移植准备工作

1、熟悉88E6321的datasheet及Functional_Specification_Rev.0.05

2、熟悉设备树相关理论和用法

3、熟悉Linux网络驱动MDIO、PHY部分的软件流程

三、DTS文件修改

本工程的DTS文件以am335x-icev2.dts为基础进行修改

1、修改网络接口MODE

我们板子上使用的是335X的RMII接口与SW相连,使用SW的RMII接口提供的50M时钟信号。根据硬件连接关系,335X的RMII接口与SW的PORT5相连,根据SW使用单芯片模式,根据SW的访问机制,这里设置phy_id为0x15.所以对设备树相关接口模式部分做如下修改

-  &cpsw_emac0 {

-   phy_id = <&davinci_mdio>, <0>;

-   phy-mode = "rgmii";

-  };

+ &cpsw_emac0 {

+  phy_id = <&davinci_mdio>, <0x15>;

+  phy-mode = "rmii";

+ };

2、修改网络接口pinmux

将原有cpsw_default、cpsw_sleep部分的pinmux改为RMII模式相对应的pinmux配置,如下:

cpsw_default: cpsw_default {

pinctrl-single,pins = <

/* Slave 1, RMII mode */

AM33XX_IOPAD(0x914, PIN_OUTPUT_PULLDOWN | MUX_MODE1) /* mii1_txen.rmii1_txen */

AM33XX_IOPAD(0x924, PIN_OUTPUT_PULLDOWN | MUX_MODE1) /* mii1_txd1.rmii1_td1 */

AM33XX_IOPAD(0x928, PIN_OUTPUT_PULLDOWN | MUX_MODE1) /* mii1_txd0.rmii1_td0 */

AM33XX_IOPAD(0x93c, PIN_INPUT_PULLDOWN | MUX_MODE1)  /* mii1_rxd1.rmii1_rd1 */

AM33XX_IOPAD(0x940, PIN_INPUT_PULLDOWN | MUX_MODE1)  /* mii1_rxd0.rmii1_rd0 */

AM33XX_IOPAD(0x944, PIN_INPUT_PULLDOWN | MUX_MODE0)  /* RMII1_REF_CLK.rmii1_refclk*/

AM33XX_IOPAD(0x910, PIN_INPUT_PULLDOWN | MUX_MODE1)  /* MII1_RX_ER. rmii1_rxerr*/

AM33XX_IOPAD(0x90c, PIN_INPUT_PULLDOWN | MUX_MODE1)  /* MII1_CRS. rmii1_crs_dv*/

>;

};

cpsw_sleep: cpsw_sleep {

pinctrl-single,pins = <

/* Slave 1 reset value */

AM33XX_IOPAD(0x914, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x924, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x928, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x93c, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x940, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x944, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x910, PIN_INPUT_PULLDOWN | MUX_MODE7)

AM33XX_IOPAD(0x90c, PIN_INPUT_PULLDOWN | MUX_MODE7)

>;

};

四、网口驱动架构及流程分析

网口驱动架构见文档:基于335X的Linux网口驱动分析

4.1.网口底层驱动启动及加载过程大致如下:

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23

--> kernel相关启动及初始化
  --> phy_init
  --> mdio_bus_init
  --> phy_driver_register
  --> davinci_mdio_probe
  --> cpsw_probe
  --> cpsw_ndo_open
  --> cpsw_slave_open
  --> phy_probe
  --> PHY_READY->PHY_UP
  --> phy_state_machine状态机函数
  --> phy_start_aneg
      --> genphy_config_aneg
          --> genphy_config_advert
          --> genphy_restart_aneg
      --> PHY_AN
   
  --> PHY_AN->HY_RUNNING
   
  --> _cpsw_adjust_link
      --> phy_print_status
          --> "Link is Up -   xxxx"
  --> PHY_RUNNING->PHY_CHANGELINK->PHY_RUNNING(循环)

4.2.网口底层驱动关键过程分析

4.2.1.phy_driver初始化

在phy_device.c中,函数phy_init进行相关phy设备驱动的加载注册,具体过程为:

phy_init()

->mdio_bus_init

->phy_drivers_register

phy_init函数主要完成了mdio_bus的初始化工作,注册了mdio_bus_class,注册了mdio_bus_type,其中mdio_bus_type中的mdio_bus_match成员函数规定了phy_device和phy_driver的匹配方式。

4.2.2. davinci_mdio设备及驱动

在davinci_mdio.c文件中会进行davinci_mdio_driver的注册,davinci_mdio_driver中的davinci_mdio_of_mtable有compatible字符。

在系统启动时,会进行设备树文件的解析,将设备树中的设备节点转换为设备结构体,会转换出davinci_mdio相关的设备。davinci_mdio设备及驱动都属于platform_bus。通过比较设备树文件中davinci_mdio节点的compatible属性以及davinci_mdio_driver中的davinci_mdio_of_mtable包含的compatible内容。如果匹配,就会执行davinci_mdio_probe函数。

4.2.3 davinci_mdio_probe函数分析

最主要的工作是进行mdio_bus的相关对象内容的填充及mdio_bus_class总线类型的设置。主要是完成MDIO读、写、复位函数的关联。之后通过__mdiobus_register函数中的bus->reset(bus)对CPSW的alive寄存器进行读取,其值标识这MDIO总线检测到“活着”的phy设备。再通过mdiobus_scan对检测到的phy设备进行通过phydev->bus->phy_map[phydev->addr]对其进行管理。并对其分别注册。

在注册PHY设备时,就会进行与4.2.1小节中进行注册的phy_driver进行匹配,若匹配成功,则执行phy_probe函数。注意,这里可能会检测到多个PHY,也可能会有多个PHY和多个phy_driver的匹配,最终具体使用哪个PHY及phy_driver。在cpsw_probe中连接PHY设备时才会选择,这里只是对“活着”的PHY设备进行驱动的匹配。

4.2.4 cpsw设备及驱动的匹配

与davinci_mdio设备类似,cpsw设备及驱动也是属于platform_bus,系统启动时将设备树中的设备节点转换为设备结构体,会转换出cpsw相关的设备。也是使用compatible属性和cpsw_driver进行匹配,匹配成功后就会执行cpsw_probe函数。

4.2.5 cpsw_probe函数分析

与网口底层设置及关键的调用如下:

代码路径:drivers\net\ethernet\ti\cpsw.c
  cpsw_probe
  --> CPSW平台数据、资源、DMA初始化、赋值netdev_ops、ethtool_ops
  --> 注册网络设备:register_netdev
  --> cpsw_ndo_open (此处根据实际经验得到,目前还没有从源代码级别解答出调用过程)
  --> cpsw_slave_open
      --> phy_connect   (传递cpsw_adjust_link)
      --> bus_find_device_by_name (从mdio总线上查找device)
      --> to_phy_device (从device结构体中找到phy_device)
      --> phy_connect_direct (传递handler,初始化 ***)
          --> phy_attach_direct   ()
              -->   phydev->attached_dev = dev; (有函数使用到attached_dev指针)
              --> phydev->state =   PHY_READY; (将PHY状态标记为PHY_READY)
              --> phy_init_hw (硬件级的初始化,最后会调用genphy_config_init)
          --> phy_prepare_link   (赋值adjust_link为cpsw_adjust_link)
          --> phy_start_machine   (启动PHY状态机 ***)
          --> phy_start_interrupts (经测了,但好像没调用到这里)
  --> phy_start(PHY_READY变成PHY_UP)
  --> 其它的CPSW的初始化

phy_connect函数传入的slave->data->phy_id对应设备树文件中的phy_id,slave->data->phy_if对应于设备树文件中的phy-mode = "rmii"。

这里就会在MDIO总线上“活着”的PHY中找到对应的phydev。phy_attach_direct函数中进行phydev相关成员的初始化,phy_init_hw会调用genphy_config_init进行PHY的初始化配置。phy_prepare_link函数赋值adjust_link为cpsw_adjust_link。最终_cpsw_adjust_link函数会根据link状态及phy的速率及模式等对mac_control寄存器进行赋值。这也是CPSW层重要的配置步骤。

之后phy_start_machine开启phy_state_machine。phy_start函数将PHY_READY变成PHY_UP,之后进行phy_state_machine的状态转换。有关phy_state_machine的相关状态变化可以参考相关代码及文档。主要就是根据现有状态及phy_driver的相关config_init、config_aneg、aneg_done、read_status函数进行状态的读取及变更。

phy_state_machine常规的初始状态为PHY_UP

在PHY_UP中会将需要自动协商的标志是能,进一步通过phy_start_aneg函数调用phydev->drv->config_aneg(phydev);最终根据自动协商使能与否,转换到状态PHY_AN或PHY_FORCING。

PHY_AN状态通过read_status函数及aneg_done后的情况转换到状态PHY_RUNNING并且调用adjust_link或PHY_NOLINK

PHY_FORCING则通过genphy_update_link后的状态决定是否转换状态到PHY_RUNNING。最后,还是会执行adjust_link,进行mac_control的配置。

在PHY_AN及PHY_FORCING中如果要转换状态到PHY_RUNNING,都会有一步netif_carrier_on(phydev->attached_dev)操作。这里应该理解为打开网络传输的上层操作吧。这个尤为重要。是最终网络传输开始的开关。

PHY_RUNNING状态下,会根据read_status后的结果是否改变决定是否改变状态为PHY_CHANGELINK,或是维持在PHY_RUNNING状态。

PHY_CHANGELINK就是会根据据read_status后的结果来判断转换到PHY_RUNNING并且netif_carrier_on或PHY_NOLINK并且netif_carrier_off,

所以,最重要的是是状态机转换到PHY_RUNNING并且netif_carrier_on,病保持在这一状态。

五、switch芯片的驱动构建

这里使用类 PHY设备的形式进行SW驱动构建,由于并不是PHY设备,所以在phy_device中的genphy_driver中添加一项VIRPHY_DRV_SW。利用88E6321的PORT5相关寄存器来进行相关状态的读取,并且在这里禁止自动协商。最终的PHY状态及状态变化为PHY_UP->PHY_FORCING->PHY_RUNNING.

这里需要改变、加打印、及添加的函数为

Phy.c  //加打印参数

int phy_start_aneg(struct phy_device *phydev)

{

int err;

printk("phy_start_aneg now\n");//test

mutex_lock(&phydev->lock);

if (AUTONEG_DISABLE == phydev->autoneg)

phy_sanitize_settings(phydev);

//*******************************************//

printk(" phydev->speed: 0x%08X\n ", phydev->speed);         //for test

printk(" phydev->duplex: 0x%08X\n ", phydev->duplex);      //

printk("phydev->supported: 0x%08X\n ",phydev->supported);    //

//*******************************************//

/* Invalidate LP advertising flags */

phydev->lp_advertising = 0;

err = phydev->drv->config_aneg(phydev);

if (err < 0)

goto out_unlock;

if (phydev->state != PHY_HALTED) {

if (AUTONEG_ENABLE == phydev->autoneg) {

printk("phydev->state = PHY_AN\n");//test

phydev->state = PHY_AN;

phydev->link_timeout = PHY_AN_TIMEOUT;

} else {

printk("phydev->state = PHY_FORCING\n");//test

phydev->state = PHY_FORCING;

phydev->link_timeout = PHY_FORCE_TIMEOUT;

}

}

out_unlock:

mutex_unlock(&phydev->lock);

return err;

}

Phy_device.c //添加

//////////////////////////////////////////////////////////

#define MV88E6321_Port_Status_REG             0x0

#define MV88E6321_Physical_Control_REG            0x1

#define MV88E6321_Jamming_Control_REG           0x2

#define MV88E6321_Product_Identifier_REG          0x3

#define MV88E6321_Port_Control_REG           0x4

#define MV88E6321_Port_Control_1_REG        0x5

#define MV88E6321_Port_Based_VLAN_REG          0x6

#define MV88E6321_Port_VLAN_ID_REG         0x7

#define MV88E6321_LED_Control_REG  0x16

#define MV88E6321_internal_phy3_addr         0x03

#define MV88E6321_internal_phy4_addr        0x04

#define MV88E6321_internal_serdes0_addr     0x0c

#define MV88E6321_internal_serdes1_addr    0x0d

#define MV88E6321_internal_phyid1_REG      0x02

#define MV88E6321_internal_phyid2_REG      0x03

/* Basic mode PortStatus REG. */

#define mv88e6321_LINK_STA      0x0800

#define mv88e6321_FULL_DPX  0x0400

#define mv88e6321_SPEED_100M    0x0100

#define mv88e6321_C_Mode_RMII    0x0004

////////////////////////////////////////////////////////////

添加一个驱动项

////////////////////////////////////////////

enum genphy_driver {

GENPHY_DRV_1G,

GENPHY_DRV_10G,

VIRPHY_DRV_SW,

GENPHY_DRV_MAX

};

//************** add SW driver  **************//

static int vir_swphy_soft_reset(struct phy_device *phydev)

{

/* Do nothing for now */

printk("vir_swphy_soft_reset now\n");

return 0;

}

//************** add SW driver **************//

static int vir_swphy_soft_reset(struct phy_device *phydev)

{

/* Do nothing for now */

printk("vir_swphy_soft_reset now\n");

return 0;

}

I

int vir_swphy_config_init(struct phy_device *phydev)

{

int val;

u32 features;

printk("vir_swphy_config_init now\n");

features = (SUPPORTED_TP | SUPPORTED_MII

| SUPPORTED_AUI | SUPPORTED_FIBRE |

SUPPORTED_BNC);

/* Do we support autonegotiation? */

val = phy_read(phydev, MV88E6321_Port_Status_REG);

printk(" MV88E6321_Port_Status_REG: 0x%04X\n ", val);//

if (val < 0)

return val;

if( (val & mv88e6321_FULL_DPX)&&(val & mv88e6321_SPEED_100M))

{

features |= SUPPORTED_100baseT_Full;

printk(" features is SUPPORTED_100baseT_Full\n ");//

}

else

{

features |= SUPPORTED_10baseT_Full;

printk(" features is SUPPORTED_10baseT_Full\n ");//

}

printk(" features: 0x%08X\n ", features);//

phydev->supported &= features;

phydev->advertising &= features;

phydev->autoneg == AUTONEG_DISABLE;    //不需要自动协商

return 0;

}

int vir_swphy_config_aneg(struct phy_device *phydev)

{

printk("vir_swphy_config_aneg\n");

if (AUTONEG_ENABLE != phydev->autoneg)

{

phydev->pause = 0;

phydev->asym_pause = 0;

phydev->speed=SPEED_100;

phydev->duplex=DUPLEX_FULL;

}

else

{

phydev->autoneg=AUTONEG_DISABLE;

}

return 0;

}

int vir_swphy_read_status(struct phy_device *phydev)

{

int adv;

int err;

int lpa;

int lpagb = 0;

int common_adv;

int common_adv_gb = 0;

printk("vir_swphy_read_status\n");

/* Update the link, but return if there was an error */

err = genphy_update_link(phydev);

if (err)

return err;

phydev->lp_advertising = 0;

if (AUTONEG_ENABLE == phydev->autoneg)

{

phydev->autoneg=AUTONEG_DISABLE;

}

else

{

int psr = phy_read(phydev, MV88E6321_Port_Status_REG);

printk(" MV88E6321_Port_Status_REG: 0x%08X\n ", psr);//

if (psr < 0)

return psr;

if (psr & mv88e6321_FULL_DPX)

{

phydev->duplex = DUPLEX_FULL;

printk("phydev->duplex = DUPLEX_FULL\n");

}

else

phydev->duplex = DUPLEX_HALF;

if (psr & mv88e6321_SPEED_100M)

{

phydev->speed = SPEED_100;

printk(" phydev->speed = SPEED_100\n");

}

else

phydev->speed = SPEED_10;

phydev->pause = 0;

phydev->asym_pause = 0;

}

return 0;

}

///////////////////////////////////////////////////////////

修改genphy_update_link函数

/**

* genphy_update_link - update link status in @phydev

* @phydev: target phy_device struct

*

* Description: Update the value in phydev->link to reflect the

*   current link value.  In order to do this, we need to read

*   the status register twice, keeping the second value.

*/

/*

int genphy_update_link(struct phy_device *phydev)

{

int status;

status = phy_read(phydev, MII_BMSR);// Do a fake read

if (status < 0)

return status;

status = phy_read(phydev, MII_BMSR);// Read link and autonegotiation status

if (status < 0)

return status;

if ((status & BMSR_LSTATUS) == 0)

phydev->link = 0;

else

phydev->link = 1;

return 0;

}

*/

int genphy_update_link(struct phy_device *phydev)

{

int status;

/* Do a fake read */

status = phy_read(phydev, MV88E6321_Port_Status_REG);

if (status < 0)

return status;

/* Read link and autonegotiation status */

status = phy_read(phydev, MV88E6321_Port_Status_REG);

if (status < 0)

return status;

if ((status &mv88e6321_LINK_STA)&&(status &mv88e6321_C_Mode_RMII)) // linked&&RMII

phydev->link = 1;

else

phydev->link = 0;

return 0;

}

最终在genphy_driver增加一个SW PHY驱动

static struct phy_driver genphy_driver[] = {

{

.phy_id             = 0xffffffff,

.phy_id_mask    = 0xffffffff,

.name        = "Generic PHY",

.soft_reset  = genphy_soft_reset,

.config_init = genphy_config_init,

.features    = PHY_GBIT_FEATURES | SUPPORTED_MII |

SUPPORTED_AUI | SUPPORTED_FIBRE |

SUPPORTED_BNC,

.config_aneg     = genphy_config_aneg,

.aneg_done      = genphy_aneg_done,

.read_status      = genphy_read_status,

.suspend    = genphy_suspend,

.resume            = genphy_resume,

.driver        = { .owner = THIS_MODULE, },

}, {

.phy_id         = 0xffffffff,

.phy_id_mask    = 0xffffffff,

.name           = "Generic 10G PHY",

.soft_reset  = gen10g_soft_reset,

.config_init    = gen10g_config_init,

.features       = 0,

.config_aneg    = gen10g_config_aneg,

.read_status    = gen10g_read_status,

.suspend        = gen10g_suspend,

.resume         = gen10g_resume,

.driver         = {.owner = THIS_MODULE, },

}, {

.phy_id             = 0x00003102,

.phy_id_mask    = 0x0000ffff,

.name        = "virtual SW PHY",

.soft_reset  = vir_swphy_soft_reset,

.config_init = vir_swphy_config_init,

.features    = PHY_GBIT_FEATURES | SUPPORTED_MII |

SUPPORTED_AUI | SUPPORTED_FIBRE |

SUPPORTED_BNC,

.config_aneg     = vir_swphy_config_aneg,

.read_status      = vir_swphy_read_status,

.suspend    = vir_swphy_suspend,

.resume            = vir_swphy_resume,

.driver        = { .owner = THIS_MODULE, },

}};

Phy.c    //加打印,为了查看状态变化情况

void phy_state_machine(struct work_struct *work)

{

struct delayed_work *dwork = to_delayed_work(work);

struct phy_device *phydev =

container_of(dwork, struct phy_device, state_queue);

bool needs_aneg = false, do_suspend = false;

enum phy_state old_state;

int err = 0;

int old_link;

mutex_lock(&phydev->lock);

old_state = phydev->state;

if (phydev->drv->link_change_notify)

phydev->drv->link_change_notify(phydev);

switch (phydev->state) {

case PHY_DOWN:

case PHY_STARTING:

case PHY_READY:

case PHY_PENDING:

break;

case PHY_UP:

printk("PHY_UP now\n");///////test

needs_aneg = true;

phydev->link_timeout = PHY_AN_TIMEOUT;

break;

case PHY_AN:

printk("PHY_AN now\n");///////test

err = phy_read_status(phydev);

if (err < 0)

break;

/* If the link is down, give up on negotiation for now */

if (!phydev->link) {

phydev->state = PHY_NOLINK;

netif_carrier_off(phydev->attached_dev);

phydev->adjust_link(phydev->attached_dev);

break;

}

/* Check if negotiation is done.  Break if there's an error */

err = phy_aneg_done(phydev);

if (err < 0)

break;

/* If AN is done, we're running */

if (err > 0) {

phydev->state = PHY_RUNNING;

netif_carrier_on(phydev->attached_dev);

phydev->adjust_link(phydev->attached_dev);

} else if (0 == phydev->link_timeout--)

needs_aneg = true;

break;

case PHY_NOLINK:

printk("PHY_NOLINK now\n");///////test

if (phy_interrupt_is_valid(phydev))

break;

err = phy_read_status(phydev);

if (err)

break;

if (phydev->link) {

if (AUTONEG_ENABLE == phydev->autoneg) {

err = phy_aneg_done(phydev);

if (err < 0)

break;

if (!err) {

phydev->state = PHY_AN;

phydev->link_timeout = PHY_AN_TIMEOUT;

break;

}

}

phydev->state = PHY_RUNNING;

netif_carrier_on(phydev->attached_dev);

phydev->adjust_link(phydev->attached_dev);

}

break;

case PHY_FORCING:

printk("PHY_FORCING now\n");///////test

err = genphy_update_link(phydev);

if (err)

break;

if (phydev->link) {

phydev->state = PHY_RUNNING;

printk("GO TO PHY_RUNNING\n");///////test

netif_carrier_on(phydev->attached_dev);

} else {

if (0 == phydev->link_timeout--)

needs_aneg = true;

}

phydev->adjust_link(phydev->attached_dev);

break;

case PHY_RUNNING:

//           printk("PHY_RUNNING now\n");///////test

/* Only register a CHANGE if we are polling or ignoring

* interrupts and link changed since latest checking.

*/

if (!phy_interrupt_is_valid(phydev)) {

old_link = phydev->link;

err = phy_read_status(phydev);

if (err)

break;

if (old_link != phydev->link)

phydev->state = PHY_CHANGELINK;

}

break;

case PHY_CHANGELINK:

printk("PHY_CHANGELINK now\n");///////test

err = phy_read_status(phydev);

if (err)

break;

if (phydev->link) {

phydev->state = PHY_RUNNING;

netif_carrier_on(phydev->attached_dev);

} else {

phydev->state = PHY_NOLINK;

netif_carrier_off(phydev->attached_dev);

}

phydev->adjust_link(phydev->attached_dev);

if (phy_interrupt_is_valid(phydev))

err = phy_config_interrupt(phydev,

PHY_INTERRUPT_ENABLED);

break;

case PHY_HALTED:

printk("PHY_HALTED now\n");///////test

if (phydev->link) {

phydev->link = 0;

netif_carrier_off(phydev->attached_dev);

phydev->adjust_link(phydev->attached_dev);

do_suspend = true;

}

break;

case PHY_RESUMING:

printk("PHY_RESUMING now\n");///////test

if (AUTONEG_ENABLE == phydev->autoneg) {

err = phy_aneg_done(phydev);

if (err < 0)

break;

/* err > 0 if AN is done.

* Otherwise, it's 0, and we're  still waiting for AN

*/

if (err > 0) {

err = phy_read_status(phydev);

if (err)

break;

if (phydev->link) {

phydev->state = PHY_RUNNING;

netif_carrier_on(phydev->attached_dev);

} else      {

phydev->state = PHY_NOLINK;

}

phydev->adjust_link(phydev->attached_dev);

} else {

phydev->state = PHY_AN;

phydev->link_timeout = PHY_AN_TIMEOUT;

}

} else {

err = phy_read_status(phydev);

if (err)

break;

if (phydev->link) {

phydev->state = PHY_RUNNING;

netif_carrier_on(phydev->attached_dev);

} else      {

phydev->state = PHY_NOLINK;

}

phydev->adjust_link(phydev->attached_dev);

}

break;

}

mutex_unlock(&phydev->lock);

if (needs_aneg)

err = phy_start_aneg(phydev);

else if (do_suspend)

phy_suspend(phydev);

if (err < 0)

phy_error(phydev);

dev_dbg(&phydev->dev, "PHY state change %s -> %s\n",

phy_state_to_str(old_state), phy_state_to_str(phydev->state));

queue_delayed_work(system_power_efficient_wq, &phydev->state_queue,

PHY_STATE_TIME * HZ);

}

基于335X平台Linux交换芯片驱动开发的相关教程结束。

《基于335X平台Linux交换芯片驱动开发.doc》

下载本文的Word格式文档,以方便收藏与打印。