1. 程式人生 > >Linux PHY幾個狀態的跟蹤

Linux PHY幾個狀態的跟蹤

前面文章零零星星地分析了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狀態。

本文分析基於一定的實踐經驗,限於能力,箇中錯誤難免,將會擇機更正。

2015年4月6日,李遲,於清明假期