網絡卡驅動之02驅動原始碼分析
0 環境
核心:經過xilinx基於zynq平臺定製的4.4.0系核心;
硬體:zynq晶片,其中mac contorller是使用Cadence的IP核,phy晶片使用提marvell的1116R晶片;
裝置樹:如下表所示,定義了phy晶片與mac controller;
compatible = "cdns,zynq-gem","cdns,gem"; reg = <0xe000b000 0x1000>; status = "okay"; interrupts = <0x0 0x16 0x4>; clocks = <0x1 0x1e 0x1 0x1e 0x1 0xd>; clock-names = "pclk", "hclk", "tx_clk"; #address-cells = <0x1>; #size-cells = <0x0>; phy-mode = "rgmii-id"; phy-handle = <0x7>; pinctrl-names = "default"; pinctrl-0 = <0x8>; reg = <0x17>; linux,phandle = <0x7>; phandle = <0x7>; }; }; |
1 MDIO匯流排裝置模型
事實上,我認為網絡卡驅動程式可以分為兩層,一層專注於與資料的收發,另外一層則是MDIO,對PHY晶片的控制資訊。對於MDIO相信閱讀完《網絡卡驅動之01硬體及協議介紹》已經不陌生。本文主要分析的是MDIO匯流排驅動。
MDIO驅動模型,套用了常規的匯流排驅動裝置模型。此處的“匯流排”即指MDIO匯流排,“驅動
MDIO匯流排的定義在~/drivers/net/phy/mdio_bus.c
struct bus_type mdio_bus_type = { .name = "mdio_bus", .match = mdio_bus_match, .pm = MDIO_BUS_PM_OPS, .dev_groups = mdio_dev_groups, }; |
驅動端的註冊在呼叫int phy_driver_register(struct phy_driver *new_driver)函式時觸發,該函式定義在~/drivers/net/phy/phy_device.c中。
裝置端的註冊則在int phy_device_register(struct phy_device *phydev)函式完成,該函式定義在~/drviers/net/phy_device.c中。
2 驅動程式核心檔案結構分佈
根據上述分析其實也不難分析到對核心檔案分佈特點,事實上分析驅動原始碼的組織結構對我們處理其它的晶片驅動非常有益。大致總結如下。
首先,~/drivers/net/phy 目錄下核心提供了phy晶片訪問通用的程式碼,包括了phy_device.c、phy.c和marvell.c通用程式碼,其中MDIO專注於維護匯流排,phy_device.c則是專注於為mac 和 phy晶片提供介面,而phy.c則是提供以USB為介質的介面函式。
其次,關於mdio_bus.c,如果MACcontroller中有MDIO介面那麼使用的是該檔案,但如果沒有MDIO介面,那麼可能需要使用GPIO模擬,那麼使用的是mdio-gpio.c。還有在net/目錄下的mdio.c暫時沒有發現什麼用處。
然後,對於phy晶片毫無疑問,是儲存在~/drivrs/net/phy目錄下,例如我們使用的是marvell phy晶片,所以編譯進核心的是~/drivrs/net/phy/marvell.c。
最後,mac controller驅動,因為使用的是乙太網協議,所有有關此協議的mac驅動都存放在~/drivers/net/ethernet/下,在net目錄下還有一些其它網路協議,如PPP,則相關mac驅動儲存在~/drivers/net/ppp目錄下。另外,由於我們使用的是cadence,所以對應的驅動程式碼都在~/drivers/net/ethernet/cadence中的macb.c檔案。
3 原始碼追蹤
先放棄核心完成的部分(比如MDIO匯流排驅動),而從硬體模組角度出發。顯然,有兩個硬體MacController和PHY晶片。
如上所述遵循的是匯流排-裝置-驅動模型,那麼隨意從裝置或者驅動分析都是可以的(因為模型中,任何一端無論是裝置還是驅動載入時,都會去另外一端尋找匹配的支援)。下面就先以Mac Controller端分析。
MacController晶片端的驅動是Cadence,是~/drivers/net/ethernet/cadence中的macb.c檔案。分析驅動肯定從入口函式分析。
1)裝置樹中.compatible屬性觸發了macb_probe的呼叫。
static struct platform_driver macb_driver = { .probe = macb_probe, .remove = macb_remove, .driver = { .name = "macb", .of_match_table = of_match_ptr(macb_dt_ids), .pm = &macb_pm_ops, }, }; |
static const struct of_device_id macb_dt_ids[] = { { .compatible = "cdns,at32ap7000-macb" }, { .compatible = "cdns,at91sam9260-macb" }, { .compatible = "cdns,macb" }, { .compatible = "cdns,pc302-gem", .data = &pc302gem_config }, { .compatible = "cdns,gem", .data = &pc302gem_config }, { .compatible = "atmel,sama5d3-gem", .data = &sama5d3_config }, { .compatible = "atmel,sama5d4-gem", .data = &sama5d4_config }, { .compatible = "cdns,zynqmp-gem", .data = &zynqmp_config}, { /* sentinel */ } }; |
compatible = "cdns,zynq-gem","cdns,gem"; |
2)mac_probe函式,正如之前所述maccontroller的功能除了“向下”與phy晶片建立MDIO的控制資料的交換,還需要“向上”完成網絡卡的本職工作,即資料流的收發。因此mac_probe的程式碼也可以劃分為兩部分,在此只關注與PHY晶片的互動,MII協議(MDIO可以算是MII的一個組成部分),因為在該函式中並無mdio類似的關鍵詞,但有mii的關鍵詞,所以相信會在mii相關處理函式中完成了驅動掛接。
static int macb_probe(struct platform_device *pdev) { //設定mac controller相關的程式碼僅針對mac controller 不涉及與PHY晶片的通訊 //準確地說上述程式碼是與NET DEVICE 相關,不是MAC相關 err = register_netdev(dev); err = macb_mii_init(bp); } |
3)macb_mii_init函式,如上所述mdio是屬於mii的一個組成部分,所以macb_mii_init函式中也分為兩部分,mii相關的工作和MDIO相關的工作。顯然需要繼續追蹤的是of_mdiobus_register函式,該函式應該會完成MDIO匯流排的註冊。(由此可知,匯流排雖然定義了(mdio_bus.c),但是沒有例項化,是在此提出註冊完成初始化)
int macb_mii_init(struct macb *bp) { //mii 相關 …… //mdio 相關 np = bp->pdev->dev.of_node; if (np) { //因為裝置樹中已經有相關節點了,所以進入該分支 /* try dt phy registration */ err = of_mdiobus_register(bp->mii_bus, np); /* fallback to standard phy registration if no phy were found during dt phy registration */ if (!err && !phy_find_first(bp->mii_bus)) { for (i = 0; i < PHY_MAX_ADDR; i++) { struct phy_device *phydev; phydev = mdiobus_scan(bp->mii_bus, i); if (IS_ERR(phydev)) { err = PTR_ERR(phydev); break; } } } } err = macb_mii_probe(bp->dev); } |
4)of_mdiobus_register函式,這裡需要注意mii_bus中的phy_mask屬性,該屬性對應位如果置1,那麼會遮蔽掉對該PHY地址的掃描工作。而且這裡有兩個需要繼續追蹤,一個是mdiobus_register函式,還有一個是of_mdiobus_register_phy函式。
nt of_mdiobus_register(struct mii_bus *mdio, struct device_node *np) { /* Mask out all PHYs from auto probing. Instead the PHYs listed in * the device tree are populated after the bus has been registered */ mdio->phy_mask = ~0;//之所以將這點單獨列出是因為phy_mask為1表示不掃描該地址掛接的PHY晶片 //否則,在mdiobus_register中會掃描所有地址,這樣我們的裝置樹中節點就無意義了 /* Register the MDIO bus */ rc = mdiobus_register(mdio); //此處並不會註冊PHY晶片 /* Loop over the child nodes and register a phy_device for each one */ //這裡會根據子節點即dts中的ethernet-phy中進行註冊 for_each_available_child_of_node(np, child) { addr = of_mdio_parse_addr(&mdio->dev, child); if (addr < 0) { scanphys = true; continue; } rc = of_mdiobus_register_phy(mdio, child, addr); if (rc) continue; } //顯然scanphys仍然為false,返回從而避免了auto scan if (!scanphys) return 0; //auto scan 相關程式碼 return 0; } |
5.1)mdiobus_register函式,完成MDIO匯流排的初始化工作,同時將dev掛接到MDIO匯流排的裝置端,此時dev並沒有初始化。因為mac conroller會作為mdio匯流排的裝置端,所以載入了驅動當然也需要掛接,只不過因為沒有初始化所以不會執行初始化工作。
另外在註冊完MDIO匯流排時,會掃描總線上是否掛有phy裝置,但是由於在4)中將mask全為1,所以會遮蔽掉掃描的操作。
5.2)of_mdiobus_register_phy,根據裝置樹中相關的定義,ethernet節點下還掛接了phy節點,所以在4)中的掃描子節點時會發現0x17的phy節點,0x17即為phy addr。從而進入of_mdiobus_register_phy 函式。詳細分析如下注釋
static int of_mdiobus_register_phy(struct mii_bus *mdio, struct device_node *child, u32 addr) { is_c45 = of_device_is_compatible(child, "ethernet-phy-ieee802.3-c45"); //裝置樹中無ethernet-phy-ieee802.3-c45屬性且通過of_get_phy_id返回錯誤碼負值 //進入else分支 if (!is_c45 && !of_get_phy_id(child, &phy_id)) phy = phy_device_create(mdio, addr, phy_id, 0, NULL); else //同時如果跟蹤進去會發現在get_phy_device中建立了phy_device(phy_device_create) //phy_device_create中最關鍵的是使用的時侯確定該裝置需要掛接的匯流排是MDIO_BUS_TYPE //但並沒有真正的掛接 phy = get_phy_device(mdio, addr, is_c45); /* Associate the OF node with the device structure so it * can be looked up later */ //phy->dev.of_node指向[email protected]節點 of_node_get(child); phy->dev.of_node = child; /* All data is now stored in the phy struct; * register it */ //向剛才已經註冊的MDIO總線上註冊PHY裝置 //顯然待會兒肯定會註冊PHY驅動 rc = phy_device_register(phy); //至此完成了MDIO匯流排,以及從裝置樹中掃描到的PHY節點的作為裝置端註冊至MDIO總線上 } |
6)get_phy_device 初始化並且構造phy結構體,將作為mdio匯流排的裝置端加入到匯流排中。下面貼出了初始化程式碼,非常容易理解,在此就不多解釋。需要注意的是,從此phy_device特性是phy_addr是0x17(因為在5)中是解析到了子節點且將reg引數作為addr去掃描的,而硬體上設定了phy晶片的addr就是0x17,所以是有迴應的);phy_id這是ieee定義的必須在每個phy晶片中的addr為2和3的暫存器中定義phy_id標識phy晶片,所以在建立裝置後的初始化中也會讀取該ID(注意區分phy_id和addr區別後者是由硬體設計決定的);bus_type為剛初始化的&mdio_bus_type。
struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45) { r = get_phy_id(bus, addr, &phy_id, is_c45, &c45_ids); return phy_device_create(bus, addr, phy_id, is_c45, &c45_ids); } struct phy_device *phy_device_create(struct mii_bus *bus, int addr, int phy_id, bool is_c45, struct phy_c45_device_ids *c45_ids) { dev->dev.bus = &mdio_bus_type; //MDIO_BUS_TYPE 待會兒在驅動端你也會發現,bustype是mdio_bus_type dev_set_name(&dev->dev, PHY_ID_FMT, bus->id, addr); device_initialize(&dev->dev); return dev; } |
7)phy_device_register,6)中完成的是phy晶片的初始化工作,而此處則是掛接到mdio總線上。
至此,完成了MDIO匯流排和裝置端的初始化和註冊工作。
接下來分析phy晶片的註冊掛接以及匹配的過程。根據硬體和2中所述,phy晶片完成MDIO匯流排的驅動端註冊,且是在marvell.c(~/drivers/net/phy/marvell.c)中定義。但是沒有module_init的初始化函式,而是定義了一堆如下的結構體。
最後仔細分析,發現在於module_phy_driver巨集定義。跟蹤進去即可發現它完成了驅動端的註冊,相對簡單在此就不贅述。
static struct phy_driver marvell_drivers[] = { { .phy_id = MARVELL_PHY_ID_88E1101, .phy_id_mask = MARVELL_PHY_ID_MASK, .name = "Marvell 88E1101", .features = PHY_GBIT_FEATURES, .flags = PHY_HAS_INTERRUPT, .config_aneg = &marvell_config_aneg, .read_status = &genphy_read_status, .ack_interrupt = &marvell_ack_interrupt, .config_intr = &marvell_config_intr, .resume = &genphy_resume, .suspend = &genphy_suspend, .driver = { .owner = THIS_MODULE }, }, ... }; module_phy_driver(marvell_drivers); |
#define module_phy_driver(__phy_drivers) \ phy_module_driver(__phy_drivers, ARRAY_SIZE(__phy_drivers)) #define phy_module_driver(__phy_drivers, __count) \ static int __init phy_module_init(void) \ { \ return phy_drivers_register(__phy_drivers, __count); \ } int phy_drivers_register(struct phy_driver *new_driver, int n) { ret = phy_driver_register(new_driver + i); } int phy_driver_register(struct phy_driver *new_driver) { new_driver->driver.name = new_driver->name; new_driver->driver.bus = &mdio_bus_type; new_driver->driver.probe = phy_probe; new_driver->driver.remove = phy_remove; retval = driver_register(&new_driver->driver); } |
可以很清楚的發現,這是一個高度抽象化的框架,首先,核心定義了一系列需要用到的操作函式作為phy_driver的結構體成員,這樣各phy晶片廠商可以根據自身晶片的特點去實現各成員函式。然後,phy_device.c負責呼叫各個介面函式的操作。
換句話說,核心實現了ieee定義的mdio標準,完成MDIO的通訊任務,而各大phy晶片廠商必然滿足該標準,所以核心的程式碼可以複用所有的phy晶片,但這是流程化的操作,而具體訪問的地址是多少,則由phy晶片廠商定義,所以核心定義的某些成員函式由晶片廠商作為驅動程式去例項化。這樣,大大減輕了phy晶片驅動開發的工作量。
最後,簡單說下匹配的問題。匹配函式在mdio_bus.c中定義,顯然我們定義的裝置樹只會進入最後的分支。從上述分析中可以知道在初始化phy_device中,已經從晶片中讀取到了ID值是0x01410e40(#define MARVELL_PHY_ID_88E1116R 0x01410e40),所以和驅動(marvell.c)中定義phy_driver結構體匹配上,完成匹配工作。
//至此,匯流排MDIO,裝置mac-controller ,驅動marvell.c phy.c //由於都是mdio_bus_type 所以任何一次載入都會互相掃描 //檢視match函式至此完成了互相關聯 static int mdio_bus_match(struct device *dev, struct device_driver *drv) { struct phy_device *phydev = to_phy_device(dev); struct phy_driver *phydrv = to_phy_driver(drv); if (of_driver_match_device(dev, drv)) return 1; if (phydrv->match_phy_device) return phydrv->match_phy_device(phydev); //進入的是最後一個分支 return (phydrv->phy_id & phydrv->phy_id_mask) == (phydev->phy_id & phydrv->phy_id_mask); } |
總結
通過上述分析,相信即使對於其它核心版本也是有相當的幫助的。其實核心驅動都遵循一套:分配à設定à註冊à呼叫的套路。