Linux PHY幾個狀態的跟蹤 本文轉自: http://www.latelee.org/programming-under-linux/linux-phy-state.html
前面文章零零星星地分析了PHY,本來想完整地,系統地做分析,發現工程量太大了,而自己又一知半解,所以只好各個擊破,一點一點來分析。本文主要分析了裝置上電、撥出網線、插上網線、自動協商等過程的PHY狀態。 MAC驅動和PHY驅動
PHY一般和具體的MAC控制驅動聯絡一起,這裡以TI的MAC驅動為例,由它切入到PHY驅動。Linux核心通過mdio匯流排訪問、控制PHY,源碼實現在driver/net/phy/mdio_bus.c中。下面是mdio掃描、找到並註冊phy的過程:
davinci_mdio_probe ->mdiobus_register -> device_register -> mdiobus_scan -> get_phy_device -> get_phy_id // 讀寄存器 -> phy_device_create -> INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine); // !!!!!!初始化狀態機器函數 -> phy_device_register
在phy_device_create中做了大量的初始化工作,比如預設就是使能自動協商,另外調用INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine)建立phy的狀態機器,——實際上它是一個延時工作隊列。
cpsw驅動在net_device_ops的ndo_open函數,亦即cpsw_ndo_open中調用cpsw_slave_open,通過phy_connect與phy串連,同時將cpsw_adjust_link賦值給phy的狀態調整函數指標adjust_link。在些過程將將PHY狀態機器開啟。
這個過程主要的函數如下:
cpsw_ndo_open -> cpsw_slave_open -> phy_connect (傳遞cpsw_adjust_link) -> phy_connect_direct (PHY_READY) -> phy_prepare_link (賦值cpsw_adjust_link為adjust_link) -> phy_start_machine -> phy_start (PHY_READY變成PHY_UP)
當系統啟動時,經過上述的步驟,一切已經準備妥當。就等著迎接PHY的狀態變更了。在這裡,需要提及的函數是cpsw_adjust_link,它調用了_cpsw_adjust_link,之後通知核心其它網路模組當前的狀態。這個函數將在phy狀態機器函數中時時被調用,所以要關注一下。代碼如下:
static void cpsw_adjust_link(struct net_device *ndev) { struct cpsw_priv *priv = netdev_priv(ndev); bool link = false; for_each_slave(priv, _cpsw_adjust_link, priv, &link); if (link) { netif_carrier_on(ndev); // 通知核心子系統網路,當前連結是OK的 if (netif_running(ndev)) netif_wake_queue(ndev); } else { netif_carrier_off(ndev); // 通知核心子系統網路,當前連結斷開了 netif_stop_queue(ndev); } }
真正幹活(設定)的是這個函數:
static void _cpsw_adjust_link(struct cpsw_slave *slave, struct cpsw_priv *priv, bool *link) { struct phy_device *phy = slave->phy; u32 mac_control = 0; u32 slave_port; if (!phy) return; slave_port = cpsw_get_slave_port(priv, slave->slave_num); if (phy->link) { mac_control = priv->data.mac_control; /* enable forwarding */ cpsw_ale_control_set(priv->ale, slave_port, ALE_PORT_STATE, ALE_PORT_STATE_FORWARD); if (phy->speed == 1000) // 千兆 mac_control |= BIT(7); /* GIGABITEN */ if (phy->duplex) mac_control |= BIT(0); /* FULLDUPLEXEN */ /* set speed_in input in case RMII mode is used in 100Mbps */ if (phy->speed == 100) // 百兆 mac_control |= BIT(15); else if (phy->speed == 10) // 十兆 mac_control |= BIT(18); /* In Band mode */ *link = true; } else { mac_control = 0; /* disable forwarding */ cpsw_ale_control_set(priv->ale, slave_port, ALE_PORT_STATE, ALE_PORT_STATE_DISABLE); } if (mac_control != slave->mac_control) { phy_print_status(phy); // 當狀態不同時,需要寫寄存器時,才列印網路狀態 __raw_writel(mac_control, &slave->sliver->mac_control); } slave->mac_control = mac_control; }
它實際上寫mac_control寄存器,這個寄存器控制著速率(千兆、百兆、十兆)和雙工。之前不太理解,問了高手,才知道不單單要設定PHY寄存器,還要設定mac控制模組的寄存器。phy_print_status是phy驅動的通用函數,用以列印網路狀態(初步查了下,像Intel的網路驅動,不調用此函數,等有空再研究研究)。
void phy_print_status(struct phy_device *phydev) { if (phydev->link) { netdev_info(phydev->attached_dev, "Link is Up - %s/%s - flow control %s\n", phy_speed_to_str(phydev->speed), DUPLEX_FULL == phydev->duplex ? "Full" : "Half", phydev->pause ? "rx/tx" : "off"); } else { netdev_info(phydev->attached_dev, "Link is Down\n"); } }
其中的phy_speed_to_str函數是將網速轉化成字串,在核心的舊版本上是沒有的。
當網路連接時,會列印如下資訊:
PHY: 2:50 - Link is Up - 100Mbps/Full - flow control off
當網路斷開時,會列印:
PHY: 2:50 - Link is Down
PHY狀態機器
先看看PHY有的狀態定義:
enum phy_state { PHY_DOWN = 0, // PHY晶片和驅動沒準備好,一般情況下少發生 PHY_STARTING, // PHY晶片OK了,但驅動還沒有準備好 PHY_READY, // 準備好了,在probe中賦值,接下來會切到PHY_UP PHY_PENDING, PHY_UP, // phy啟動了,可以工作了,接下來會到PHY_AN PHY_AN, // 自動協商 PHY_RUNNING, // 正在運行中,在網路連接(插上網線)時會到這個狀態 PHY_NOLINK, // 斷網了 PHY_FORCING, // 強制,當自動協商不使能時,就會進行此狀態(實際上會讀PHY寄存器進行設定速率、雙工,等) PHY_CHANGELINK, // 變化,這個狀態很重要,當串連時,會換到PHY_RUNNING,當斷網時,會切到PHY_NOLINK PHY_HALTED, PHY_RESUMING };
phy狀態變化主要在phy_state_machine函數,該函數一直在運行(每隔一秒檢測一次網路狀態),該函數判斷不同的網路狀態作出不同的動作。其中CHANGELINK是會根據網路連、斷來判斷是RUNNING還是NOLINK。這樣,就知道網路是串連上還是斷開。當串連上網路後(註:不斷開情況),狀態為RUNNING時,之後重新賦值CHANGELINK,到了CHANGELINK又賦值RUNNING,這兩種狀態之間不斷切換。完整代碼如下:
/** * phy_state_machine - Handle the state machine * @work: work_struct that describes the work to be done */ 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, do_resume = false; int err = 0; mutex_lock(&phydev->lock); 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: needs_aneg = true; phydev->link_timeout = PHY_AN_TIMEOUT; // 逾時,自動協商不成功時,則會在逾時後強制設定速率等參數 break; case PHY_AN: err = phy_read_status(phydev); // 讀phy狀態,包括link,速率、雙工,等等 if (err < 0) break; /* If the link is down, give up on negotiation for now */ if (!phydev->link) { phydev->state = PHY_NOLINK; // 沒有串連,則狀態變成PHY_NOLINK netif_carrier_off(phydev->attached_dev); // 通知核心其它網路模組(phy是最底一層)斷網了。 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; // 完成後,變成PHY_RUNNING狀態 netif_carrier_on(phydev->attached_dev); // 發通知,串連OK phydev->adjust_link(phydev->attached_dev); // 列印、調用參數 } else if (0 == phydev->link_timeout--) needs_aneg = true; break; case PHY_NOLINK: err = phy_read_status(phydev); // 讀phy狀態,包括link,速率、雙工,等等 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: err = genphy_update_link(phydev); // 先更新狀態 if (err) break; if (phydev->link) { phydev->state = PHY_RUNNING; // 運行。。。 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: /* Only register a CHANGE if we are * polling or ignoring interrupts */ if (!phy_interrupt_is_valid(phydev)) phydev->state = PHY_CHANGELINK; // 如果是RUNNING,則改變為CHANGELINK。 break; case PHY_CHANGELINK: err = phy_read_status(phydev); // 讀phy狀態,包括link,速率、雙工,等等 if (err) break; if (phydev->link) { phydev->state = PHY_RUNNING; // 串連網路時,則變成RUNNING netif_carrier_on(phydev->attached_dev); } else { phydev->state = PHY_NOLINK; // 不連網時,變成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: 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: err = phy_clear_interrupt(phydev); if (err) break; err = phy_config_interrupt(phydev, PHY_INTERRUPT_ENABLED); if (err) break; 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); // 讀phy狀態,包括link,速率、雙工,等等 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); } do_resume = true; break; } mutex_unlock(&phydev->lock); if (needs_aneg) err = phy_start_aneg(phydev); else if (do_suspend) phy_suspend(phydev); else if (do_resume) phy_resume(phydev); if (err < 0) phy_error(phydev); queue_delayed_work(system_power_efficient_wq, &phydev->state_queue, PHY_STATE_TIME * HZ); }
經過一大段的分析研究後,當網路發生變化時,就十分清晰了。 PHY狀態
上電時狀態變化:
PHY_READY -> PHY_UP -> PHY_AN -> PHY_RUNNING
撥出網線時狀態變化:
PHY_RUNNING ->PHY_NOLINK
插上網線時狀態變化:
PHY_NOLINK -> PHY_RUNNING
自動協商過程:
cpsw_ndo_open->cpsw_slave_open -> PHY_UP -> phy_start_aneg -> genphy_config_aneg -> genphy_config_advert -> genphy_restart_aneg -> PHY_AN -> PHY_NOLINK(串口列印Down) -> phy_aneg_done -> PHY_RUNNING(串口列印Up)
註:在AN後出現NOLINK狀態,我猜是因為自動協商需要時間,此時間大於1秒,然後執行到狀態機器判斷成NOLINK,然後判斷是否完成自動協商,然後再到RUNNING狀態。
本文源碼分析基於3.17版本核心,地址:http://lxr.oss.org.cn/source/drivers/net/ethernet/ti/?v=3.17
本文分析基於一定的實踐經驗,限於能力,個中錯誤難免,將會擇機更正。
2015年4月6日,李遲,於清明假期
本文固定連結: http://www.latelee.org/programming-under-linux/linux-phy-state.html