基于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 |
--> kernel相关启动及初始化 |
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 |
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);
}