驅動開發(fā)實戰(zhàn):從MAC/PHY硬件到設備樹與內(nèi)核調(diào)優(yōu))
1. 嵌入式以太網(wǎng)驅動開發(fā)到底在搞什么1.1 從一個真實場景說起先聊一個我親身經(jīng)歷的事。幾年前接手一個工業(yè)網(wǎng)關項目主控用的是Zynq系列芯片PS端跑LinuxPL端掛了個自定義IP核需要通過以太網(wǎng)把采集到的傳感器數(shù)據(jù)實時傳到上位機。硬件同事把板子焊好、網(wǎng)口燈也亮了我這邊驅動一加載ifconfig能看到eth0但就是ping不通ethtool顯示link up但收發(fā)包計數(shù)全是零。折騰了整整兩天最后發(fā)現(xiàn)是PHY芯片的地址在設備樹里配錯了——MDIO總線上掛了兩個PHY一個地址是0x01另一個是0x03我照著參考板抄成了0x00結果驅動一直在跟一個不存在的PHY通信。這件事讓我意識到嵌入式以太網(wǎng)驅動開發(fā)遠不是“寫個初始化函數(shù)就完事”的活兒。它橫跨硬件原理、總線協(xié)議、內(nèi)核網(wǎng)絡子系統(tǒng)、設備樹配置、性能調(diào)優(yōu)等多個層面任何一個環(huán)節(jié)出問題表現(xiàn)都是“網(wǎng)不通”三個字但根因可能千差萬別。這一期我們就圍繞嵌入式驅動開發(fā)中的Ethernet以太網(wǎng)這個主題把從硬件接口到內(nèi)核驅動、從設備樹配置到性能調(diào)優(yōu)的完整鏈路拆開來講。不管你是剛接觸嵌入式Linux驅動的新手還是已經(jīng)做過幾個項目但遇到瓶頸的老手都能從中找到可以直接復用的經(jīng)驗和方法。1.2 以太網(wǎng)驅動在嵌入式系統(tǒng)里的位置要理解以太網(wǎng)驅動開發(fā)先得搞清楚它在整個系統(tǒng)里的位置。一個典型的嵌入式Linux系統(tǒng)網(wǎng)絡數(shù)據(jù)從應用層到物理網(wǎng)線大致經(jīng)過這么幾層應用層socket接口TCP/UDP數(shù)據(jù)內(nèi)核網(wǎng)絡協(xié)議棧IP層、ARP、路由等網(wǎng)絡設備層net_device結構體NAPI機制以太網(wǎng)驅動MAC控制器驅動收發(fā)描述符管理PHY驅動通過MDIO總線管理PHY芯片硬件層MAC控制器、PHY芯片、變壓器、RJ45接口以太網(wǎng)驅動開發(fā)的核心工作就是實現(xiàn)中間那兩層——MAC控制器驅動和PHY驅動讓內(nèi)核網(wǎng)絡協(xié)議棧能夠通過硬件收發(fā)以太網(wǎng)幀。聽起來簡單但實際做起來每一層都有大量的細節(jié)需要處理。1.3 為什么嵌入式以太網(wǎng)驅動值得單獨拿出來講有人可能會問PC上的網(wǎng)卡驅動不也是以太網(wǎng)驅動嗎有什么特別的區(qū)別大了。PC上的網(wǎng)卡通常是PCIe接口硬件自動枚舉驅動模型成熟廠商提供完整的驅動源碼。而嵌入式場景下MAC控制器可能是SoC內(nèi)置的寄存器手冊幾百頁PHY芯片通過MDIO/MDC兩根線管理地址需要手動配置接口類型多樣MII、RMII、GMII、RGMII、SGMII每種時序不同時鐘樹復雜參考時鐘來源和頻率需要仔細配置設備樹需要自己寫每個板子都不一樣功耗、實時性、吞吐量要求各不相同這些差異決定了嵌入式以太網(wǎng)驅動開發(fā)需要一套獨立的知識體系。下面我就按照實際開發(fā)的流程從硬件接口講到驅動實現(xiàn)再講到調(diào)試和優(yōu)化。2. 硬件接口與協(xié)議基礎搞懂MAC、PHY和接口類型2.1 MAC和PHY的分工以太網(wǎng)通信的硬件核心是兩顆芯片MAC和PHY。MAC全稱Media Access Control負責以太網(wǎng)幀的封裝和解封裝、CRC校驗、地址過濾等PHY全稱Physical Layer負責把MAC送來的數(shù)字信號轉換成適合在網(wǎng)線上傳輸?shù)哪M信號。打個比方MAC就像公司的收發(fā)室負責把文件裝進標準信封、寫上收發(fā)地址PHY就像郵遞員負責把信封實際送到對方手里。兩者之間通過標準接口連接最常見的是MII系列接口。在嵌入式SoC里MAC通常是內(nèi)置的比如STM32的ETH外設、Zynq的GEM、i.MX系列的FEC。PHY則是外掛的獨立芯片比如常見的KSZ8081、DP83848、RTL8211等。MAC和PHY之間還需要一個MDIO接口用來讀寫PHY的內(nèi)部寄存器配置速率、雙工模式、自協(xié)商等參數(shù)。2.2 MII、RMII、GMII、RGMII到底怎么選這是新手最容易迷糊的地方。MII是Media Independent Interface的縮寫意思是“介質(zhì)無關接口”不管你是雙絞線還是光纖MAC和PHY之間的接口是統(tǒng)一的。但隨著速率提升MII的引腳數(shù)太多于是衍生出一堆變種接口類型速率數(shù)據(jù)位寬時鐘頻率引腳數(shù)典型場景MII10/100M4位25MHz/2.5MHz16早期設計RMII10/100M2位50MHz8引腳受限場景GMII1000M8位125MHz24千兆設計RGMII1000M4位125MHz DDR12主流千兆方案SGMII1000M串行625MHz4背板、光模塊選型邏輯很直接引腳夠用、速率要求不高就選MII引腳緊張選RMII千兆優(yōu)先選RGMII因為引腳少、成本低需要走背板或連接光模塊就選SGMII。RGMII有個坑需要注意它用DDR方式在125MHz時鐘的上下沿都傳數(shù)據(jù)所以對時序要求很嚴。PCB走線長度不匹配、時鐘偏移沒調(diào)好就會出現(xiàn)丟包甚至完全不通。我在一個項目里遇到過RGMII的RX時鐘延遲配置不對導致接收方向大量CRC錯誤后來在設備樹里調(diào)整了rx-internal-delay-ps參數(shù)才解決。2.3 MDIO總線和PHY地址MDIO是Management Data Input/Output的縮寫只有兩根線MDC時鐘和MDIO數(shù)據(jù)。MAC通過這兩根線讀寫PHY的寄存器標準寄存器有32個前16個是IEEE定義的后16個廠商自定義。每個PHY在MDIO總線上有一個5位的地址范圍0到31。一個MDIO總線可以掛最多32個PHY。設備樹里需要正確配置PHY地址否則驅動找不到PHY。前面提到的我那個踩坑經(jīng)歷就是PHY地址配錯了。提示如果不確定PHY地址可以在uboot里用mii info命令掃描MDIO總線或者用mii dump addr逐個地址讀取寄存器0如果能讀出0x1140或0x1040之類的值說明這個地址上有PHY。2.4 自協(xié)商與強制模式PHY上電后默認會進行自協(xié)商和対端交換能力信息協(xié)商出雙方都支持的最高速率和雙工模式。大部分場景下自協(xié)商是沒問題的但在某些工業(yè)環(huán)境下対端設備可能不支持自協(xié)商或者自協(xié)商不穩(wěn)定這時候就需要強制指定速率和雙工模式。強制模式在設備樹里通過phy-mode和fixed-link來配置。比如fec1 { phy-mode rmii; fixed-link { speed 100; full-duplex; }; };這段配置的意思是不使用自協(xié)商強制100M全雙工。注意強制模式下兩端必須配置一致否則會出現(xiàn)半雙工/全雙工不匹配表現(xiàn)為能ping通但丟包嚴重、吞吐量極低。3. 設備樹配置以太網(wǎng)驅動的第一道關卡3.1 設備樹里以太網(wǎng)節(jié)點的關鍵屬性設備樹是嵌入式Linux驅動的“硬件說明書”以太網(wǎng)驅動能不能正常工作很大程度上取決于設備樹寫得對不對。一個典型的以太網(wǎng)節(jié)點包含這些關鍵屬性mac { pinctrl-names default; pinctrl-0 mac_pins; phy-mode rgmii-id; phy-handle phy0; status okay; mdio { #address-cells 1; #size-cells 0; phy0: ethernet-phy1 { reg 1; reset-gpios gpio1 15 GPIO_ACTIVE_LOW; reset-assert-us 10000; reset-deassert-us 50000; }; }; };逐個解釋phy-mode指定MAC和PHY之間的接口類型常見值有mii、rmii、gmii、rgmii、rgmii-id、sgmii。rgmii-id表示RGMII接口且內(nèi)部延遲MAC和PHY各自處理自己的時鐘延遲。phy-handle指向MDIO總線下的PHY節(jié)點。regPHY在MDIO總線上的地址。reset-gpiosPHY的復位引腳有些PHY需要硬件復位才能正常工作。reset-assert-us和reset-deassert-us復位拉低和拉高的持續(xù)時間單位微秒。3.2 phy-mode的坑rgmii、rgmii-id、rgmii-txid、rgmii-rxid這四個值看起來差不多但含義完全不同rgmiiMAC和PHY都不加內(nèi)部延遲需要PCB走線來保證時序rgmii-idMAC和PHY都加內(nèi)部延遲rgmii-txid只在發(fā)送方向加內(nèi)部延遲rgmii-rxid只在接收方向加內(nèi)部延遲選哪個取決于硬件設計。如果PCB走線等長做得很好可以用rgmii如果走線有偏差就需要用內(nèi)部延遲來補償。我一般先用rgmii-id試不通再換其他模式。實測下來大部分國產(chǎn)PHY配合國產(chǎn)SoCrgmii-id的成功率最高。3.3 時鐘配置容易被忽略的關鍵點以太網(wǎng)需要時鐘而且不止一個。以RGMII為例MAC需要125MHz的發(fā)送時鐘和接收時鐘PHY需要25MHz或50MHz的參考時鐘MDIO需要MDC時鐘通常由MAC分頻產(chǎn)生參考時鐘的來源有兩種外部晶振直接提供給PHY或者由SoC的時鐘控制器輸出。設備樹里需要配置時鐘樹確保每個時鐘都正確使能。mac { clocks clks IMX6QDL_CLK_ENET, clks IMX6QDL_CLK_ENET_REF; clock-names ipg, ptp; assigned-clocks clks IMX6QDL_CLK_ENET_REF; assigned-clock-rates 125000000; };這段配置的意思是MAC的IPG時鐘和PTP時鐘來自時鐘控制器并且把ENET_REF時鐘設置為125MHz。如果時鐘頻率不對PHY可能無法正常建立鏈路或者鏈路能建立但數(shù)據(jù)錯誤率極高。3.4 引腳復用配置SoC的引腳通常是多功能的以太網(wǎng)引腳需要配置為正確的復用功能。這部分在設備樹的pinctrl節(jié)點里完成iomuxc { mac_pins: macgrp { fsl,pins MX6QDL_PAD_ENET_MDIO__ENET_MDIO 0x1b0b0 MX6QDL_PAD_ENET_MDC__ENET_MDC 0x1b0b0 MX6QDL_PAD_RGMII_TXC__RGMII_TXC 0x1b0b0 MX6QDL_PAD_RGMII_TD0__RGMII_TD0 0x1b0b0 /* ... 其他引腳 ... */ ; }; };每個引腳的配置值包含驅動能力、上下拉、速率等參數(shù)。這些值通常參考SoC手冊和參考板設計不要隨意改動。我曾經(jīng)因為把某個引腳的驅動能力從0x1b0b0改成了0x1b0b1導致RGMII發(fā)送時鐘信號質(zhì)量下降千兆模式下丟包率飆升。4. 驅動代碼實現(xiàn)從probe到收發(fā)4.1 驅動框架概覽Linux內(nèi)核的以太網(wǎng)驅動遵循標準的網(wǎng)絡設備驅動框架。核心結構體是struct net_device驅動需要實現(xiàn)的主要回調(diào)函數(shù)包括ndo_open打開設備初始化硬件啟動收發(fā)ndo_stop關閉設備停止收發(fā)ndo_start_xmit發(fā)送數(shù)據(jù)包ndo_set_mac_address設置MAC地址ndo_get_stats獲取統(tǒng)計信息ndo_do_ioctlioctl命令處理ndo_tx_timeout發(fā)送超時處理驅動的初始化流程通常是平臺驅動probe - 分配net_device - 初始化硬件 - 注冊net_device - 等待用戶ifconfig up。4.2 probe函數(shù)里做了什么probe函數(shù)是驅動的入口主要工作包括獲取設備樹信息讀取寄存器基地址、中斷號、時鐘、PHY句柄等映射寄存器devm_ioremap_resource把物理地址映射到虛擬地址使能時鐘clk_prepare_enable使能MAC和PHY的時鐘復位MAC寫MAC控制寄存器軟復位配置MAC設置MAC地址、接口模式、速率等連接PHY通過of_phy_connect或phy_connect連接PHY設備注冊中斷申請收發(fā)中斷分配描述符分配DMA描述符環(huán)注冊net_deviceregister_netdev注冊網(wǎng)絡設備每一步都可能出問題。比如時鐘沒使能讀寫寄存器會返回全0或全FPHY連接失敗phy_connect會返回NULL中斷申請失敗收發(fā)中斷無法響應。4.3 收發(fā)描述符與DMA現(xiàn)代以太網(wǎng)MAC都支持DMA驅動需要管理描述符環(huán)。以發(fā)送為例驅動分配一組發(fā)送描述符通常256或512個每個描述符指向一個數(shù)據(jù)緩沖區(qū)驅動把要發(fā)送的數(shù)據(jù)填入緩沖區(qū)更新描述符狀態(tài)MAC讀取描述符通過DMA把數(shù)據(jù)發(fā)送出去發(fā)送完成后MAC更新描述符狀態(tài)觸發(fā)中斷驅動在中斷處理中回收緩沖區(qū)接收方向類似只是方向相反。描述符的管理是驅動性能的關鍵描述符數(shù)量太少會導致丟包太多會浪費內(nèi)存。/* 發(fā)送描述符結構體示例 */ struct tx_desc { __le32 status; __le32 length; __le32 buf_addr; __le32 next; };描述符的status字段包含OWN位表示描述符歸屬MAC還是CPU、錯誤標志、校驗和狀態(tài)等。驅動和MAC通過OWN位來同步描述符的歸屬。4.4 NAPI與中斷處理Linux內(nèi)核從2.6版本開始引入NAPINew API機制用于在高負載下減少中斷開銷。NAPI的核心思想是中斷觸發(fā)后關閉接收中斷改用輪詢方式處理數(shù)據(jù)包直到?jīng)]有數(shù)據(jù)包或達到預算上限再重新打開中斷。以太網(wǎng)驅動的中斷處理函數(shù)通常這樣寫static irqreturn_t mac_interrupt(int irq, void *dev_id) { struct net_device *ndev dev_id; struct mac_priv *priv netdev_priv(ndev); u32 status; status readl(priv-base MAC_ISR); writel(status, priv-base MAC_ISR); if (status MAC_ISR_RX) { if (napi_schedule_prep(priv-napi)) { writel(0, priv-base MAC_IER); __napi_schedule(priv-napi); } } if (status MAC_ISR_TX) { mac_tx_clean(priv); } return IRQ_HANDLED; }NAPI的poll函數(shù)負責實際的數(shù)據(jù)包接收static int mac_poll(struct napi_struct *napi, int budget) { struct mac_priv *priv container_of(napi, struct mac_priv, napi); int work_done 0; while (work_done budget) { struct sk_buff *skb mac_rx(priv); if (!skb) break; napi_gro_receive(napi, skb); work_done; } if (work_done budget) { napi_complete(napi); writel(MAC_IER_RX, priv-base MAC_IER); } return work_done; }注意NAPI的budget參數(shù)通常設為64或128表示一次poll最多處理多少個數(shù)據(jù)包。設太小會導致中斷頻繁設太大會增加延遲。實測下來64到128之間是比較平衡的值。4.5 PHY驅動與自協(xié)商流程PHY驅動通常不需要自己寫內(nèi)核已經(jīng)包含了大部分常見PHY的驅動。驅動開發(fā)者需要做的是在設備樹里正確描述PHY然后調(diào)用phy_connect連接。PHY連接后內(nèi)核的PHY狀態(tài)機會自動處理自協(xié)商。自協(xié)商完成后PHY會觸發(fā)一個中斷或通過輪詢通知MACMAC再調(diào)整自己的速率和雙工模式。如果PHY驅動不支持你的PHY芯片就需要自己寫一個。PHY驅動的主要工作是實現(xiàn)config_init回調(diào)配置PHY的初始狀態(tài)實現(xiàn)read_status回調(diào)讀取鏈路狀態(tài)實現(xiàn)config_aneg回調(diào)配置自協(xié)商參數(shù)注冊到phy_driver鏈表static struct phy_driver my_phy_driver { .phy_id 0x12345678, .name My PHY, .phy_id_mask 0xffffffff, .features PHY_GBIT_FEATURES, .config_init my_phy_config_init, .config_aneg my_phy_config_aneg, .read_status my_phy_read_status, };5. 調(diào)試與問題排查網(wǎng)不通到底卡在哪5.1 調(diào)試工具和方法以太網(wǎng)驅動調(diào)試有一套成熟的工具鏈ifconfig/ip link查看接口狀態(tài)、MAC地址、統(tǒng)計計數(shù)ethtool查看鏈路狀態(tài)、速率、雙工、自協(xié)商參數(shù)ethtool -S查看詳細的收發(fā)統(tǒng)計包括錯誤計數(shù)mii-tool查看和設置PHY狀態(tài)mii dump在uboot里讀寫PHY寄存器tcpdump抓包分析ping/iperf連通性和性能測試調(diào)試的基本思路是自底向上先確認PHY鏈路是否建立再確認MAC是否正常收發(fā)最后確認協(xié)議棧是否正常。5.2 常見問題速查表現(xiàn)象可能原因排查方法網(wǎng)口燈不亮供電、復位、時鐘、PHY地址量電壓、查時鐘、掃MDIOlink up但ping不通MAC地址、IP配置、防火墻ip addr、tcpdump丟包嚴重雙工不匹配、RGMII延遲、描述符不足ethtool -S、調(diào)整延遲吞吐量低中斷開銷、描述符數(shù)量、CPU占用iperf、top、調(diào)整NAPI千兆協(xié)商失敗時鐘質(zhì)量、PCB走線、PHY配置降速測試、查信號完整性熱插拔后不通PHY狀態(tài)機、中斷丟失重新ifconfig up、查中斷計數(shù)5.3 一個典型的排查案例回到開頭我提到的那個Zynq項目。現(xiàn)象是ifconfig能看到eth0ethtool顯示link up但ping不通ethtool -S顯示rx_packets和tx_packets都是0。排查步驟確認PHY地址在uboot里用mii info掃描發(fā)現(xiàn)地址0x01和0x03上有PHY0x00上沒有。設備樹里配的是0x00改成0x01。重新加載驅動ethtool顯示link up但ethtool -S仍然全是0。檢查中斷cat /proc/interrupts發(fā)現(xiàn)以太網(wǎng)中斷計數(shù)為0。說明中斷沒有觸發(fā)。檢查中斷配置設備樹里中斷號寫的是0 29 4但Zynq的GEM中斷號應該是0 57 4。改過來。重新加載中斷計數(shù)開始增長ethtool -S顯示收發(fā)包正常ping通。這個案例說明以太網(wǎng)驅動的問題往往不是單一原因而是多個配置錯誤疊加。排查時要一層一層剝先解決最底層的硬件問題再往上查。5.4 性能調(diào)優(yōu)的幾個方向網(wǎng)通了之后下一步就是調(diào)優(yōu)。嵌入式以太網(wǎng)的性能瓶頸通常在這幾個地方描述符數(shù)量發(fā)送和接收描述符各增加到512或1024可以減少丟包NAPI預算從64增加到128或256減少中斷次數(shù)中斷合并如果MAC支持開啟中斷合并減少CPU占用CPU親和性把以太網(wǎng)中斷綁定到特定CPU核心避免緩存顛簸TCP窗口調(diào)整內(nèi)核的TCP窗口參數(shù)提升大流量吞吐# 把eth0的中斷綁定到CPU1 echo 2 /proc/irq/57/smp_affinity # 調(diào)整TCP緩沖區(qū) sysctl -w net.core.rmem_max16777216 sysctl -w net.core.wmem_max16777216實測下來在Zynq平臺上經(jīng)過這些調(diào)優(yōu)千兆以太網(wǎng)的TCP吞吐量可以從600Mbps提升到900Mbps以上。6. 幾個容易踩坑的細節(jié)和經(jīng)驗6.1 PHY復位時序不能省很多PHY芯片需要在上電后拉低復位引腳至少10ms然后拉高再等待至少50ms才能訪問寄存器。如果復位時間不夠PHY可能處于不確定狀態(tài)表現(xiàn)為MDIO讀寫失敗或鏈路不穩(wěn)定。設備樹里的reset-assert-us和reset-deassert-us就是干這個的不要圖省事刪掉。6.2 MAC地址的讀取和設置嵌入式設備的MAC地址通常存在EEPROM、OTP或Flash的某個分區(qū)里。驅動需要在probe時讀取MAC地址并寫入MAC寄存器。如果讀不到內(nèi)核會隨機生成一個但隨機MAC在局域網(wǎng)里可能沖突。建議在uboot里就把MAC地址設置好傳給內(nèi)核。6.3 中斷和輪詢的取舍NAPI是中斷和輪詢的混合體但不是所有場景都適合。在低負載場景下純中斷模式延遲更低在高負載場景下NAPI能顯著降低CPU占用。有些MAC支持自適應中斷合并可以根據(jù)負載動態(tài)調(diào)整這是比較理想的方案。6.4 別忘了看SoC手冊的勘誤表SoC的以太網(wǎng)MAC可能有硬件bug比如某個版本的芯片在特定條件下會丟包、DMA會卡死等。這些通常在勘誤表Errata里有說明驅動里需要加workaround。我遇到過一個SoC的MAC在千兆模式下發(fā)送描述符超過256個時會卡死后來在驅動里限制發(fā)送隊列長度為128才解決。6.5 測試要覆蓋異常場景不要只測正常ping通就完事。要測網(wǎng)線熱插拔、對端設備重啟、強制雙工不匹配、大流量持續(xù)傳輸、CPU高負載下的收發(fā)、休眠喚醒后的恢復。這些異常場景才是真正暴露問題的地方。嵌入式以太網(wǎng)驅動開發(fā)是一個需要耐心和細心的活兒硬件、設備樹、驅動、協(xié)議棧每一層都可能出問題。但只要掌握了正確的調(diào)試方法和排查思路大部分問題都能在可控時間內(nèi)解決。希望這一期的內(nèi)容能幫你在下一個以太網(wǎng)項目里少走一些彎路。