이더넷 PHY 드라이버 개발

이 문서는 리눅스 커널에서 이더넷 PHY 드라이버를 작성하고 보드를 bring-up하는 과정을 다룹니다. PHY 드라이버 골격 코드와 주요 콜백, Realtek/Marvell/Microchip 같은 실제 벤더 드라이버 소스 분석, Device Tree 설정과 보드 설계 포인트, 신호 무결성, 대표 장애 패턴과 트러블슈팅, PHY 펌웨어 로딩, ethtool 통계 해석, NIC(MAC) 드라이버와 PHY 통합까지 실전 중심으로 정리합니다. PHY의 하드웨어 동작 원리와 커널 프레임워크(phylib/phylink)는 이더넷 PHY (Physical Layer) 문서를 참고하세요.

관련 문서: PHY 하드웨어 계층 구조, MDIO 레지스터, phylib/phylink 프레임워크, Auto-Negotiation, SerDes/SFP 등은 이더넷 PHY (Physical Layer) 문서에서 다룹니다. MAC 드라이버 계약은 Network Device 드라이버, Device Tree 기초는 Device Tree, 운영 제어는 ethtool 문서를 참고하세요.

PHY 드라이버 작성 가이드

새로운 PHY 칩을 지원하는 커널 드라이버를 작성하려면 drivers/net/phy/ 디렉터리에 벤더별 드라이버를 추가해야 합니다. 대부분의 구리 PHY는 genphy_* 헬퍼 함수만으로도 기본 동작이 가능하지만, RGMII 지연 설정, LED 제어, 온도 센서, downshift, EEE 정책 같은 벤더 고유 기능은 전용 콜백을 구현해야 합니다.

기본 골격 코드

PHY 드라이버의 최소 구조는 phy_driver 구조체 정의, module_phy_driver() 매크로, PHY ID 매칭 테이블로 구성됩니다. config_init()에서 하드웨어 초기 설정을 하고, read_status()에서 링크 상태를 해석하며, config_aneg()에서 광고 능력을 설정합니다.

#include <linux/module.h>
#include <linux/phy.h>
#include <linux/ethtool.h>

#define MYPHY_PHY_ID           0x001cc916
#define MYPHY_PHY_ID_MASK      0xfffffff0

/* 벤더 확장 레지스터 오프셋 */
#define MYPHY_REG_PAGE_SELECT  0x1f
#define MYPHY_REG_RGMII_DELAY  0x11
#define MYPHY_REG_LED_CTRL     0x18
#define MYPHY_REG_DOWNSHIFT    0x14
#define MYPHY_REG_TEMP         0x1a

/**
 * config_init - PHY 하드웨어 초기 설정
 *
 * probe 직후와 resume 시 호출됩니다.
 * RGMII 지연, LED 모드, downshift 정책 등 벤더 고유 설정을 적용합니다.
 */
static int myphy_config_init(struct phy_device *phydev)
{
    int ret;

    /* RGMII 지연 설정 (phy-mode에 따라) */
    if (phydev->interface == PHY_INTERFACE_MODE_RGMII_ID) {
        ret = phy_modify_paged(phydev, 0xd08, MYPHY_REG_RGMII_DELAY,
                               0x0300, 0x0300);
        if (ret < 0)
            return ret;
    }

    /* LED0: 링크 상태, LED1: 활동 표시 */
    ret = phy_write(phydev, MYPHY_REG_LED_CTRL, 0x61);
    if (ret < 0)
        return ret;

    /* downshift: 3회 실패 후 한 단계 낮은 속도로 폴백 */
    ret = phy_modify(phydev, MYPHY_REG_DOWNSHIFT, 0x001f, 0x0013);
    if (ret < 0)
        return ret;

    return genphy_config_init(phydev);
}

/**
 * read_status - 링크 상태 해석
 *
 * PHY 상태 머신이 주기적으로 호출합니다.
 * 공통 genphy_read_status() 호출 후 벤더 고유 정보를 추가합니다.
 */
static int myphy_read_status(struct phy_device *phydev)
{
    int ret;

    ret = genphy_read_status(phydev);
    if (ret)
        return ret;

    /* 벤더 레지스터에서 master/slave 결과 확인 (필요 시) */
    /* 벤더 레지스터에서 downshift 발생 여부 확인 (필요 시) */

    return 0;
}

/**
 * config_aneg - 자동 협상 설정
 *
 * ethtool이 광고 능력을 변경하거나, 드라이버가 autoneg을 재시작할 때 호출됩니다.
 */
static int myphy_config_aneg(struct phy_device *phydev)
{
    /* 벤더 고유 AN 설정이 필요하면 여기서 처리 */
    return genphy_config_aneg(phydev);
}

/**
 * handle_interrupt - PHY 인터럽트 처리
 *
 * PHY가 IRQ 라인을 지원하는 경우, 링크 변화를 인터럽트로 감지합니다.
 */
static irqreturn_t myphy_handle_interrupt(struct phy_device *phydev)
{
    int irq_status;

    irq_status = phy_read(phydev, 0x13); /* IRQ status, read clears */
    if (irq_status < 0 || !(irq_status & 0x0001))
        return IRQ_NONE;

    phy_trigger_machine(phydev);
    return IRQ_HANDLED;
}

/* PHY 드라이버 구조체 */
static struct phy_driver myphy_drivers[] = {
    {
        PHY_ID_MATCH_EXACT(MYPHY_PHY_ID),
        .name           = "My Company Example PHY",
        .config_init    = myphy_config_init,
        .config_aneg    = myphy_config_aneg,
        .read_status    = myphy_read_status,
        .handle_interrupt = myphy_handle_interrupt,
        .suspend        = genphy_suspend,
        .resume         = myphy_config_init, /* resume = config_init 재적용 */
    },
};

module_phy_driver(myphy_drivers);

static const struct mdio_device_id __maybe_unused myphy_tbl[] = {
    { MYPHY_PHY_ID, MYPHY_PHY_ID_MASK },
    { }
};
MODULE_DEVICE_TABLE(mdio, myphy_tbl);

MODULE_DESCRIPTION("My Company Example PHY driver");
MODULE_AUTHOR("Author Name");
MODULE_LICENSE("GPL");

주요 콜백 상세 설명

콜백호출 시점기본 구현커스텀 필요 상황주의점
probePHY 장치 발견 시phylib 코어특수 초기화 (firmware 로드)슬립(Sleep) 가능 컨텍스트
config_initattach 직후, resume 시genphy_config_initstrap override, delay bit, LED 모드resume에서 재호출 보장 필요
config_aneg광고 능력 변경 시genphy_config_aneg벤더 고유 AN 확장 (EEE, FEC)restart AN 포함 여부 주의
read_status상태 머신 주기 또는 IRQ 후genphy_read_status벤더 레지스터에서 추가 상태 해석phydev 필드에 결과 반영
handle_interruptIRQ 발생 시없음IRQ 지원 PHYIRQ status read-to-clear 순서
suspend/resume시스템 PMgenphy_suspend/resumeWoL, EDPD, strap 재적용analog rail 상태에 따라 분기
get_featuresPHY 능력 감지genphy_read_abilities표준 레지스터에 없는 능력 추가linkmode 비트맵(Bitmap) 정확히 설정
set_tunableethtool --set-phy-tunable없음downshift, EDPD 정책범위 검증 필수
PHY 드라이버 콜백 호출 흐름 PHY 드라이버 콜백 호출 흐름 MDIO 발견 PHY ID 매칭 probe() FW 로드 등 config_init() HW 초기 설정 config_aneg() 광고 설정 phy_start() 상태 머신 시작 read_status() 주기적 호출 또는 IRQ handle_interrupt() 링크 변화 인터럽트 suspend() / resume() 시스템 PM 진입/복귀
PHY 드라이버 콜백은 발견-초기화-협상-상태 감시-전원 관리 순서로 호출되며, resume 시 config_init 재적용이 중요합니다.

Kconfig와 Makefile 추가

# drivers/net/phy/Kconfig에 추가
config MYPHY_PHY
    tristate "My Company PHY driver"
    help
      Support for My Company Example PHY.
      This driver provides support for the MYPHY Gigabit Ethernet PHY.
# drivers/net/phy/Makefile에 추가
obj-$(CONFIG_MYPHY_PHY)     += myphy.o

실제 벤더 PHY 드라이버 소스 분석

커널 트리에는 수십 종의 벤더 PHY 드라이버가 포함되어 있으며, 각각 벤더 고유의 페이지 전환 방식, 레지스터 배치, LED 제어 방식을 사용합니다. 이 절에서는 임베디드 보드와 서버 환경에서 가장 자주 사용되는 세 드라이버를 실제 커널 소스 기준으로 분석합니다. 코드 흐름을 따라가면 이전 절의 콜백 구조가 실제로 어떻게 구현되는지 이해할 수 있습니다.

Realtek RTL8211F 드라이버 분석

RTL8211F는 10/100/1000BASE-T 기가비트 PHY로, 라즈베리파이(Raspberry Pi), 로크칩(Rockchip) 기반 SBC, NXP i.MX 보드 등 수많은 임베디드 플랫폼에 탑재되어 있습니다. 소스 파일은 drivers/net/phy/realtek.c이며, 동일 파일에서 RTL8211B, RTL8211E, RTL8211FD, RTL822x 계열까지 함께 관리합니다.

PHY ID와 매칭

RTL8211F의 PHY ID는 0x001cc916이며, 하위 4비트는 하드웨어 리비전(Revision)에 따라 달라지므로 마스크(Mask)를 0xfffffff0으로 설정하여 리비전과 무관하게 매칭합니다. 커널 4.17 이후에는 PHY_ID_MATCH_MODEL() 매크로가 이 마스크 패턴을 표준화합니다.

config_init: RGMII 지연과 LED 설정

RTL8211F의 핵심 초기화는 페이지 0xd08(확장 레지스터 페이지)의 레지스터 0x11에서 RGMII TX/RX 지연 비트를 제어하는 것입니다. 레지스터 0x1f에 페이지 번호를 쓰면 현재 페이지가 전환되며, 작업 후 반드시 페이지 0으로 복원해야 합니다. 커널은 이 패턴을 phy_modify_paged() 헬퍼로 추상화하여 원자적으로 처리합니다.

LED 설정은 페이지 0xd04의 레지스터 0x10에서 수행하며, LED0(링크/속도 표시)와 LED1(활동 표시)의 점등 조건을 비트필드로 지정합니다. 일부 보드는 LED 극성이 반전되어 있어 active-low Device Tree 속성을 사용해야 합니다. RTL8211F-CG 이후 변종은 클럭 출력(CLK_OUT) 핀을 소프트웨어로 끌 수 있어, 불필요한 EMI 방출을 줄이기 위해 config_init에서 기본 비활성화합니다.

/* drivers/net/phy/realtek.c (커널 6.x 기준, 개념 요약) */

#define RTL8211F_PHY_ID            0x001cc916
#define RTL8211F_PAGE_SELECT       0x1f

/* 확장 레지스터 페이지 번호 */
#define RTL8211F_PAGE_RGMII_DELAY  0xd08
#define RTL8211F_REG_RGMII_DELAY   0x11
#define RTL8211F_TX_DELAY_BIT      BIT(8)
#define RTL8211F_RX_DELAY_BIT      BIT(3)

#define RTL8211F_PAGE_LED          0xd04
#define RTL8211F_REG_LED_CTRL      0x10
#define RTL8211F_LED0_LINK_1000    BIT(3)
#define RTL8211F_LED0_LINK_100     BIT(1)
#define RTL8211F_LED1_ACTIVE       BIT(8)

/* 페이지 전환 헬퍼 (phy_modify_paged 내부 동작 요약) */
static int rtl8211f_page_write(struct phy_device *phydev, int page)
{
    return phy_write(phydev, RTL8211F_PAGE_SELECT, page);
}

static int rtl8211f_config_init(struct phy_device *phydev)
{
    int ret;
    u16 delay_mask = 0, delay_val = 0;

    /* phy-mode에 따라 TX/RX 지연 비트 결정 */
    switch (phydev->interface) {
    case PHY_INTERFACE_MODE_RGMII_ID:
        delay_mask = RTL8211F_TX_DELAY_BIT | RTL8211F_RX_DELAY_BIT;
        delay_val  = RTL8211F_TX_DELAY_BIT | RTL8211F_RX_DELAY_BIT;
        break;
    case PHY_INTERFACE_MODE_RGMII_TXID:
        delay_mask = RTL8211F_TX_DELAY_BIT;
        delay_val  = RTL8211F_TX_DELAY_BIT;
        break;
    case PHY_INTERFACE_MODE_RGMII_RXID:
        delay_mask = RTL8211F_RX_DELAY_BIT;
        delay_val  = RTL8211F_RX_DELAY_BIT;
        break;
    default:
        delay_mask = RTL8211F_TX_DELAY_BIT | RTL8211F_RX_DELAY_BIT;
        delay_val  = 0; /* 지연 없음 */
        break;
    }

    /* phy_modify_paged: 페이지 전환 → 수정 → 페이지 0 복원을 원자적으로 수행 */
    ret = phy_modify_paged(phydev, RTL8211F_PAGE_RGMII_DELAY,
                            RTL8211F_REG_RGMII_DELAY,
                            delay_mask, delay_val);
    if (ret < 0)
        return ret;

    /* LED0 = 1G/100M 링크, LED1 = TX/RX 활동 */
    ret = phy_modify_paged(phydev, RTL8211F_PAGE_LED,
                            RTL8211F_REG_LED_CTRL,
                            0x03ff,
                            RTL8211F_LED0_LINK_1000 | RTL8211F_LED0_LINK_100 |
                            RTL8211F_LED1_ACTIVE);
    if (ret < 0)
        return ret;

    return genphy_config_init(phydev);
}

/* read_status는 genphy_read_status를 그대로 사용 */
static struct phy_driver rtl8211f_driver = {
    PHY_ID_MATCH_MODEL(RTL8211F_PHY_ID),
    .name           = "RTL8211F Gigabit Ethernet",
    .config_init    = rtl8211f_config_init,
    .read_status    = genphy_read_status,
    .suspend        = genphy_suspend,
    .resume         = rtl8211f_config_init,
};

read_status와 페이지 전환 메커니즘

RTL8211F는 표준 레지스터(MII_BMSR, MII_LPA 등)로 링크 상태를 충분히 읽을 수 있어 read_statusgenphy_read_status를 그대로 사용합니다. 페이지 전환은 레지스터 0x1f에 원하는 페이지 번호를 기록하는 방식이며, 커널 phy_select_page() / phy_restore_page() API가 이를 표준화합니다. RTL8211F 드라이버는 .select_page 콜백을 구현하여 이 API와 연동합니다.

Marvell Alaska 88E1510 드라이버 분석

88E1510은 구리(Copper)/광(Fiber) 이중 미디어를 지원하는 기가비트 PHY로, 서버 NIC, 게이트웨이, 산업용 이더넷 장비에 광범위하게 사용됩니다. 소스 파일은 drivers/net/phy/marvell.c이며, 마블(Marvell) 드라이버는 커널 PHY 드라이버 중 가장 복잡한 구현 중 하나로 꼽힙니다. 페이지 전환에 레지스터 22(0x16)를 사용하는 점이 Realtek과 다릅니다.

PHY ID와 매칭

88E1510의 PHY ID는 0x01410dd0이며, 88E1518, 88E1540, 88E1543 같은 패밀리 멤버들은 PHY ID 하위 비트만 다르므로 드라이버는 마스크 0xfffffff0을 사용합니다. 광 전용 모드인 88E1112도 별도 엔트리로 등록되어 있으나 코어 초기화 함수를 공유합니다.

config_init: Fiber/Copper 감지와 RGMII 지연

88E1510의 config_init는 먼저 페이지 0으로 복귀한 뒤, 페이지 18(MISC 제어 페이지)에서 미디어 타입을 확인합니다. Fiber 모드로 감지되면 1000BASE-X 협상을 활성화하고, Copper 모드이면 표준 자동 협상 흐름을 유지합니다. RGMII 지연은 페이지 2(MAC 제어 페이지)의 레지스터 21(0x15)에서 설정하며, RX와 TX 지연이 독립 비트로 분리되어 있습니다. downshift는 페이지 0의 레지스터 16(0x10)에서 재시도 횟수를 설정합니다.

/* drivers/net/phy/marvell.c (커널 6.x 기준, 개념 요약) */

#define MII_88E1510_PHY_ID      0x01410dd0
#define MII_MARVELL_PHY_PAGE    22   /* 0x16 — Marvell 표준 페이지 선택 레지스터 */

/* 주요 페이지 번호 */
#define MII_MARVELL_COPPER_PAGE 0
#define MII_MARVELL_FIBER_PAGE  1
#define MII_MARVELL_MAC_PAGE    2
#define MII_MARVELL_MISC_PAGE   18

/* 페이지 2, 레지스터 21: RGMII 지연 설정 */
#define MII_88E1121_PHY_MAC_CTRL             21
#define MII_88E1121_PHY_MAC_RGMII_TX_DELAY  BIT(4)
#define MII_88E1121_PHY_MAC_RGMII_RX_DELAY  BIT(5)

/* 페이지 0, 레지스터 16: Copper 기능 제어 (downshift 등) */
#define MII_88E1011_PHY_SCR                  16
#define MII_88E1011_PHY_SCR_DOWNSHIFT_EN     BIT(11)

static int m88e1510_config_init(struct phy_device *phydev)
{
    int err;

    /* 항상 Copper 페이지(0)에서 시작 */
    err = marvell_set_page(phydev, MII_MARVELL_COPPER_PAGE);
    if (err < 0)
        return err;

    /* RGMII TX/RX 지연: MAC 페이지(2) 레지스터 21에서 설정 */
    err = marvell_config_rgmii_delays(phydev);
    if (err < 0)
        return err;

    /* LED 제어: 페이지 3, 레지스터 16 — 링크/활동 극성 */
    err = marvell_config_led(phydev);
    if (err < 0)
        return err;

    /* downshift: 4회 시도 후 폴백, 페이지 0 레지스터 16 */
    err = phy_modify_paged(phydev, MII_MARVELL_COPPER_PAGE,
                            MII_88E1011_PHY_SCR,
                            0x7800,
                            MII_88E1011_PHY_SCR_DOWNSHIFT_EN | (4 - 1) << 12);
    if (err < 0)
        return err;

    return marvell_config_init(phydev);
}

/**
 * config_aneg: 다중 페이지 설정 (Copper + Fiber 각각)
 * Fiber 페이지(1)에서 1000BASE-X AN 광고, Copper 페이지(0)에서 일반 AN 광고
 */
static int m88e1510_config_aneg(struct phy_device *phydev)
{
    int err;

    if (phydev->interface == PHY_INTERFACE_MODE_SGMII) {
        /* SGMII: 페이지 1에서 Fiber AN 설정 */
        err = marvell_set_page(phydev, MII_MARVELL_FIBER_PAGE);
        if (err < 0)
            return err;
        err = genphy_config_aneg(phydev);
        if (err < 0)
            return err;
    }

    /* Copper 페이지(0) 복귀 후 표준 AN */
    err = marvell_set_page(phydev, MII_MARVELL_COPPER_PAGE);
    if (err < 0)
        return err;

    return genphy_config_aneg(phydev);
}

/**
 * read_status: Copper 또는 Fiber 상태를 인터페이스에 따라 분리 읽기
 */
static int m88e1510_read_status(struct phy_device *phydev)
{
    int err;

    if (phydev->interface == PHY_INTERFACE_MODE_SGMII)
        err = marvell_read_status_page(phydev, MII_MARVELL_FIBER_PAGE);
    else
        err = marvell_read_status_page(phydev, MII_MARVELL_COPPER_PAGE);

    return err;
}

/* 인터럽트: 페이지 0 레지스터 18(IEVENT) 읽기로 IRQ 원인 확인 후 클리어 */
static irqreturn_t marvell_handle_interrupt(struct phy_device *phydev)
{
    int irq_status;

    irq_status = phy_read(phydev, MII_M1011_IEVENT);
    if (irq_status < 0 || irq_status == 0)
        return IRQ_NONE;

    phy_trigger_machine(phydev);
    return IRQ_HANDLED;
}

인터럽트 처리와 WoL

88E1510은 전용 IRQ 핀을 지원하며, 레지스터 19(0x13, Interrupt Enable)에서 링크 변화·속도 변화·downshift 발생 등 다양한 이벤트에 대해 인터럽트를 개별 활성화할 수 있습니다. 레지스터 18(0x12, Interrupt Status)을 읽으면 IRQ가 클리어됩니다. WoL(Wake-on-LAN) 기능은 페이지 17(Sleep 제어 페이지)에서 설정하며, set_wol / get_wol 콜백을 통해 ethtool과 연동합니다.

Microchip LAN8720A / LAN8742A 드라이버 분석

LAN8720A와 LAN8742A는 10/100BASE-T 전용 소형 PHY로, STM32, NXP LPC, Cortex-M MCU 기반 시스템과 저전력 IoT 장치에 주로 탑재됩니다. 소스 파일은 drivers/net/phy/smsc.c이며, 드라이버 규모가 작고 구조가 단순하여 PHY 드라이버 학습의 출발점으로 적합합니다.

PHY ID와 매칭

LAN8720A의 PHY ID는 0x0007c0f0, LAN8742A는 0x0007c130입니다. 두 칩 모두 SMSC(현 Microchip) 제품군이며 기본 동작은 동일합니다. 마스크 0xfffffff0을 사용해 리비전을 무시하고 매칭합니다.

config_init: EDPD와 RMII 클럭

LAN8720A는 기가비트 기능이 없으므로 config_init이 매우 단순합니다. 주요 설정은 두 가지입니다. 첫째, EDPD(Energy Detect Power Down)는 케이블이 연결되지 않았을 때 PHY를 저전력 감지 모드로 전환하는 기능입니다. 레지스터 17(EDPD 제어)의 EDPD 비트를 설정하면 유휴 전류를 수 mA에서 수십 μA로 낮출 수 있습니다. 둘째, RMII 기준 클럭(50 MHz)을 PHY가 공급할지 외부에서 입력받을지 레지스터 16(특수 제어)의 XTAL_EN 비트로 지정합니다. MCU가 클럭 소스를 가지지 않는 경우 PHY의 클럭 출력 핀을 RMII REF_CLK로 연결하고 이 비트를 설정해야 합니다.

/* drivers/net/phy/smsc.c (커널 6.x 기준, 개념 요약) */

#define LAN8720A_PHY_ID     0x0007c0f0
#define LAN8742A_PHY_ID     0x0007c130

/* 레지스터 17: EDPD (Energy Detect Power Down) */
#define LAN8720_REG_EDPD_CTRL      17
#define LAN8720_EDPD_EN            BIT(13)
#define LAN8720_EDPD_TX_NLP_EN    BIT(12)   /* NLP 전송으로 원격 wake 지원 */

/* 레지스터 16: 특수 모드 제어 */
#define LAN8720_REG_MODE_CTRL      16
#define LAN8720_MODE_XTAL_EN       BIT(15)   /* PHY가 50 MHz 클럭 출력 */

static int lan8720_config_init(struct phy_device *phydev)
{
    int ret;

    /* EDPD 활성화: 케이블 미연결 시 저전력 감지 모드 */
    ret = phy_set_bits(phydev, LAN8720_REG_EDPD_CTRL,
                       LAN8720_EDPD_EN | LAN8720_EDPD_TX_NLP_EN);
    if (ret < 0)
        return ret;

    /* RMII 기준 클럭: Device Tree 속성이 있으면 PHY에서 클럭 출력 */
    if (of_property_read_bool(phydev->mdio.dev.of_node,
                               "microchip,rmii-refclk-output")) {
        ret = phy_set_bits(phydev, LAN8720_REG_MODE_CTRL,
                           LAN8720_MODE_XTAL_EN);
        if (ret < 0)
            return ret;
    }

    /* 기가비트 기능 없음: 표준 genphy_config_init으로 충분 */
    return genphy_config_init(phydev);
}

static struct phy_driver smsc_phy_drivers[] = {
    {
        PHY_ID_MATCH_MODEL(LAN8720A_PHY_ID),
        .name           = "SMSC LAN8720A",
        .config_init    = lan8720_config_init,
        .read_status    = genphy_read_status,
        .suspend        = genphy_suspend,
        .resume         = lan8720_config_init,
    },
    {
        PHY_ID_MATCH_MODEL(LAN8742A_PHY_ID),
        .name           = "Microchip LAN8742A",
        .config_init    = lan8720_config_init,
        .config_aneg    = genphy_config_aneg,
        .read_status    = genphy_read_status,
        .suspend        = genphy_suspend,
        .resume         = lan8720_config_init,
    },
};

LAN8742A와 Wake-on-LAN 고려사항

LAN8742A는 LAN8720A에 비해 EDPD 타이머 설정과 WoL Magic Packet 감지 기능이 추가되었습니다. 단, WoL은 MAC 측에서 필터를 설정해야 하며, PHY 레벨에서는 원격 당사자에게 NLP(Normal Link Pulse)를 보내 PHY가 아직 살아있음을 알리는 역할만 담당합니다. LAN8742A는 별도 페이지 전환 메커니즘 없이 단일 주소 공간(페이지 0만 사용)으로 동작하므로, phy_modify_paged() 호출이 필요 없습니다.

벤더 PHY 드라이버 기능 비교

기능 Realtek RTL8211F Marvell 88E1510 Microchip LAN8720A/42A
지원 속도 10/100/1000M 10/100/1000M (Fiber 포함) 10/100M 전용
인터페이스 모드 RGMII, RGMII-ID, RGMII-TXID, RGMII-RXID RGMII, SGMII, 1000BASE-X RMII, MII
페이지 전환 레지스터 레지스터 0x1f (31번) 레지스터 22 (0x16) 없음 (단일 주소 공간)
RGMII 지연 설정 페이지 0xd08, 레지스터 0x11 페이지 2, 레지스터 21 해당 없음 (RMII 전용)
LED 제어 페이지 0xd04, 레지스터 0x10 페이지 3, 레지스터 16 특수 제어 레지스터 (레지스터 17)
온도 센서 일부 변종(RTL8211F-VD) 지원 88E1540/88E1543 이상 지원 미지원
케이블 진단 (TDR) 지원 (VCT via 페이지 전환) 지원 (ethtool cable-test) 미지원
인터럽트(IRQ) 지원 지원 (레지스터 0x12) 지원 (레지스터 18, 19) 지원 (제한적, 레지스터 30)
EEE (802.3az) 지원 지원 미지원 (10/100 전용)
EDPD 지원 (벤더 레지스터) 지원 (페이지 0 레지스터 16) 지원 (레지스터 17, 핵심 기능)
Fiber/Copper 이중 미디어 미지원 지원 (88E1510 핵심 특성) 미지원
소스 파일 drivers/net/phy/realtek.c drivers/net/phy/marvell.c drivers/net/phy/smsc.c
벤더 PHY 레지스터 페이지 아키텍처와 커널 API 벤더 PHY 레지스터 페이지 아키텍처와 커널 API 커널 공통 API phy_select_page() / phy_restore_page() / phy_modify_paged() Realtek RTL8211F 페이지 선택: reg 0x1f (31번) Marvell 88E1510 페이지 선택: reg 22 (0x16) Microchip LAN8720A 페이지 없음 (단일 주소 공간) Page 0 (기본) IEEE 표준 레지스터 Page 0xd08 RGMII TX/RX 지연 Page 0xd04 LED 제어 Page 0xd40 EEE 제어 Page 0 (Copper) AN, 링크 상태, downshift Page 1 (Fiber) 1000BASE-X / SGMII Page 2 (MAC) RGMII 지연, MAC IF Page 18 (MISC) Copper/Fiber 선택 단일 레지스터 공간 reg 16: 특수 모드 (XTAL_EN) reg 17: EDPD 제어 reg 30: 인터럽트 소스 phy_write(phydev, 0x1f, page_no) 페이지 전환, 작업 후 0으로 복원 phy_write(phydev, 22, page_no) Marvell 표준, Copper 페이지(0)로 복원 커널 phy_select_page / phy_restore_page 래핑 구조 1. phy_select_page(phydev, page) -> .select_page 콜백 호출 (벤더별 구현) 2. 레지스터 R/W 수행 -> 3. phy_restore_page()로 이전 페이지 복원
Realtek은 레지스터 0x1f, Marvell은 레지스터 22로 페이지를 선택하며, Microchip LAN8720A는 페이지 전환 없이 단일 레지스터 공간을 사용합니다. 커널의 phy_select_page()phy_restore_page()는 벤더별 .select_page 콜백을 통해 이 차이를 추상화합니다.

Device Tree와 보드 설계 포인트

보드 bring-up에서는 커널 코드보다 Device Tree와 하드웨어 배선이 더 자주 문제를 일으킵니다. PHY 주소가 틀리거나, reset GPIO hold 시간이 부족하거나, 25MHz reference clock이 안정되기 전에 MDIO 접근이 시작되면 드라이버는 정상이어도 PHY를 못 찾습니다. 특히 RGMII는 내부 지연과 PCB trace delay를 어느 한 곳에서만 넣어야 합니다.

/* 보드 .dts 예시: 외부 RGMII PHY 연결 */
&emac {
    pinctrl-names = "default";
    pinctrl-0 = <&ext_rgmii_pins>;
    phy-mode = "rgmii-id";
    phy-handle = <&ext_rgmii_phy>;
    phy-supply = <&reg_gmac_3v3>;
    status = "okay";
};

&mdio {
    ext_rgmii_phy: ethernet-phy@1 {
        compatible = "ethernet-phy-ieee802.3-c22";
        reg = <1>;
        reset-gpios = <&pio 3 14 GPIO_ACTIVE_LOW>;
        reset-assert-us = <15000>;
        reset-deassert-us = <40000>;
    };
};

strap pin과 reset 시퀀싱을 따로 점검하기

많은 PHY는 reset 해제 순간 strap pin을 샘플링해 주소, RGMII delay 기본값, LED 모드, SGMII/Fiber 선택 같은 초기 동작을 결정합니다. 이때 LED 핀과 strap 핀이 겸용인 설계가 흔해서, pull-up/pull-down 세기와 reset 폭이 기대와 조금만 어긋나도 PHY는 전혀 다른 모드로 부팅할 수 있습니다. 소프트웨어가 나중에 override할 수 있어도, 부팅 초기에 이미 잘못된 주소와 모드로 올라오면 MDIO 탐지부터 실패할 수 있습니다.

strap 항목잘못 샘플링되면흔한 단서대응
PHY 주소다른 주소로 부팅기대 주소에서 0xffffpull 저항과 reset 타이밍 재검토
RGMII delay 기본값내부 지연 on/off 반전1G CRC 폭증strap 상태와 runtime override 둘 다 확인
fiber / copper 선택잘못된 매체 모드링크 LED 동작이 이상함strap 조합과 보드 옵션 확인
LED 모드LED 핀 동작 혼선링크는 되는데 LED만 오작동strap 겸용 핀 배치 재검토
RGMII 지연 보상 위치 정상 예시: 지연 1곳만 사용 MAC PHY PCB 또는 PHY 한 곳만 +2ns 예: phy-mode = "rgmii-id" PCB 추가 지연은 넣지 않음 문제 예시: 이중 지연 MAC PHY PCB +2ns PHY 내부 +2ns 결과: CRC, 속도 강등, 간헐적 링크 다운 rgmii-id와 보드 지연을 동시에 쓰면 안 됨
RGMII는 TX/RX 클럭에 필요한 2ns 지연을 정확히 한 곳에서만 넣어야 합니다.
항목왜 중요한가실패 시 증상
phy-handle / PHY 주소올바른 PHY 노드와 연결probe 실패, 잘못된 PHY 드라이버 매칭
phy-modeMAC-PHY 인터페이스 계약 일치링크 불안정, CRC, 속도 강등
reset 타이밍strap pin, clock 안정화 보장PHY ID가 0xffff로 읽힘
전원 / regulator아날로그 블록 안정성 확보온도 민감, 링크 flapping
reference clockPMA/CDR 기준 클럭autoneg 실패, symbol error

보드 Bring-up 체크리스트

외부 PHY가 달린 보드를 처음 올릴 때는 "드라이버가 로드됐는가"보다 "PHY가 물리적으로 살아 있는가"부터 확인해야 합니다. bring-up 초반에는 MDIO 읽기가 성공하는지만으로도 절반은 해결된 상태입니다. 이후에는 reset 타이밍, strap pin, 지연 보상, 상대편 링크 정책, 실제 트래픽 품질 순서로 범위를 좁히는 것이 좋습니다.

단계무엇을 확인하나성공 기준실패 시 다음 행동
1. 전원PHY rail, analog rail, module power전압 안정, brown-out 없음regulator 순서와 power-good 재검토
2. 기준 클럭25MHz/50MHz/refclk 존재오실로스코프에서 안정적 파형clock source, pinmux 확인
3. reset/strapreset 폭, strap 샘플링 타이밍예상 PHY 주소와 모드로 부팅reset-assert/deassert 시간 조정
4. MDIO 스캔PHY ID 읽기, 버스 접근정상 OUI/모델 읽힘PHY 주소, MDIO pull-up, bus mux 확인
5. 링크 정책phy-mode, advertisement, fixed-link예상 속도와 duplex 협상양단 설정 비교
6. 트래픽CRC, drops, EEE/LPI오류 카운터 안정RGMII 지연, 케이블, EEE 검토
7. 스트레스열, idle/wake, 장시간 안정성link flap 없음전원 margin, 온도, 모듈 교체 테스트
PHY 보드 Bring-up 흐름 전원 rail reference clock reset / strap MDIO 스캔 PHY ID / 드라이버 링크 정책 트래픽 / CRC / drops iperf + ethtool -S idle / wake / EEE link flap 여부 열 / 장시간 안정성 온도와 장시간 burn-in
Bring-up은 전원과 클럭부터 시작해, MDIO 스캔과 링크 정책 검증을 거쳐 트래픽/열 안정성으로 올라가는 순서가 효율적입니다.

스위치 CPU 포트나 백플레인처럼 외부 PHY가 없이 PCS/SerDes만 존재하는 경우에는 phy-handle 대신 fixed-link를 사용하기도 합니다. 이 경우 MDIO로 읽을 PHY가 없으므로, 링크 정책과 상태 전달을 MAC/phylink 계층에서 처리해야 합니다.

/* 외부 PHY 없이 고정 링크를 선언하는 예시 */
&eth0 {
    phy-mode = "sgmii";
    fixed-link {
        speed = <1000>;
        full-duplex;
        pause;
    };
};

신호 무결성과 보드 레이아웃

PHY 문서가 소프트웨어 쪽으로만 흘러가기 쉽지만, 실제로는 보드 레이아웃이 링크 품질을 절반 이상 좌우합니다. 특히 RGMII 병렬 버스는 각 신호의 도착 시간 차이가 중요하고, SerDes는 임피던스와 return path, via stub, refclk 품질이 더 중요합니다. 같은 드라이버가 어떤 보드에서는 완벽하고 다른 보드에서는 CRC를 뿜는 이유가 여기에 있습니다.

중요한 점은 "숫자 하나"를 외우는 것이 아니라, PHY 데이터시트와 PCB stack-up, 커넥터 배치, 전원 분리 전략을 함께 보는 것입니다. RGMII는 지연 위치가 핵심이고, SGMII/USXGMII는 차동쌍 품질과 refclk 지터, 광 모듈 경로는 cage 접지와 전력 예산이 중요합니다.

항목왜 중요한가나빠질 때 보이는 현상실무 점검 포인트
RGMII 길이 매칭125MHz DDR 타이밍 여유 확보1G에서만 CRC, 100M은 정상지연을 PCB와 PHY 중 한 곳만 부여
refclk 품질PMA/CDR 기준 안정성협상 불안정, symbol error클럭 소스, 전원 노이즈, jitter budget
SerDes 차동쌍임피던스와 손실 관리block lock 불안정, BER 증가차동 간격, via 수, return path 연속성
magnetics 배치구리 포트 절연과 잡음 억제거리 민감, ESD 이후 링크 저하RJ45 근접 배치, center tap 전원/접지 검토
전원 decoupling아날로그 프런트엔드 안정화온도 의존 링크 flapanalog/digital rail 분리와 근접 캐패시터
SFP cage 접지EMI와 모듈 안정성모듈별 편차, hot-plug 불안정cage 접지, 전력 예산, sideband pull-up
보드 레이아웃과 신호 무결성 MAC SoC 내부 PHY refclk, analog rail Magnetics 절연, common-mode RJ45 / SFP cage 외부 매체 접점 RGMII skew / SerDes pair 짧고 연속적인 return path 잡음과 ESD 경계 refclk / decoupling PHY 핀 가까이 배치 return path / cage 접지 고속 차동쌍 단절 방지
PHY 품질 문제는 드라이버 설정뿐 아니라 refclk, 차동쌍, magnetics, cage 접지처럼 보드 물리 배치와 직접 연결됩니다.
숫자에 집착하지 말 것: trace 길이와 skew 허용치는 PHY 모델, PCB 적층, 기준 클럭 구조에 따라 달라집니다. 데이터시트 권장 길이 규칙과 레퍼런스 레이아웃을 우선 기준으로 삼고, 일반론으로 얻은 숫자를 그대로 복사해 적용하지 마세요.

대표 장애 패턴과 원인 분리

PHY 문제는 반복적으로 비슷한 모양으로 나타납니다. 아래 패턴을 외워 두면 "드라이버 버그인지, 보드 타이밍인지, 상대편 정책인지"를 빨리 구분할 수 있습니다.

PHY 장애 증상에서 원인 분리 진단 흐름도 PHY 장애 진단 흐름도 PHY 문제 발생 링크가 올라오는가? No PHY ID 읽히나? No MDIO / 하드웨어 문제 PHY 주소, reset GPIO, refclk, 전원 Yes 협상 / 상대편 문제 AN 정책, advertisement, 케이블, 상대편 포트 Yes CRC/오류 증가? Yes 속도 의존적인가? 1G에서만 RGMII delay phy-mode 재확인 모든 속도 신호 / 매체 문제 케이블, magnetics, 온도, 전원 No Flapping? Yes EEE / 전원 / 열 EEE off 테스트, DOM, 온도 빠른 원인 분리 참고표 RGMII delay 1G CRC, 100M 정상 AN / 정책 링크 안 오름, ID는 정상 EEE / 열 idle 후 flap, 온도 상관 PCS 모드 LED만 반응, 데이터 없음 MDIO / HW ID 0xffff, probe 실패
장애 증상을 "링크 업 여부 → 오류 유형 → 속도 의존성" 순서로 분기하면 원인 계층을 빠르게 좁힐 수 있습니다.

패턴 1: 100M은 되는데 1G에서만 CRC가 폭증

가장 흔한 원인은 RGMII 지연 위치 오류입니다. 100M에서는 타이밍 margin이 커서 버티지만, 1G에서는 125MHz DDR 경계가 매우 빡빡해져 CRC와 carrier error가 늘어납니다. 이 경우 케이블을 바꿔도 개선이 거의 없고, phy-mode를 바꾸거나 PHY 내부 delay bit를 조정하면 즉시 달라집니다.

패턴 2: 수 분 간격으로 링크가 내려갔습니다 올라옵니다

EEE, 전원 품질, 열, 상대편 스위치 호환성 중 하나일 가능성이 큽니다. idle 상태에서만 재현되면 EEE를 먼저 끄고, 부하를 걸면 사라지면 전원 rail 또는 module thermal 문제일 가능성이 있습니다. dmesg에 일정 간격으로 link down/up이 반복되면 소프트웨어보다는 물리 계층이나 협상 정책 쪽을 더 의심하는 편이 맞습니다.

패턴 3: PHY ID가 0xffff 또는 0x0000으로 읽힌다

이 경우는 대부분 MDIO 자체가 실패하거나 PHY가 reset에서 아직 나오지 않았다는 뜻입니다. 잘못된 PHY 주소, MDC/MDIO pull-up 불량, reset deassert 지연 부족, refclk 부재를 우선 확인하세요. 드라이버를 아무리 바꿔도 PHY ID가 안 읽히면 소프트웨어 문제일 확률은 낮습니다.

패턴 4: SGMII와 1000BASE-X를 바꾸면 링크 LED만 반응하고 데이터는 안 흐릅니다

전기적 속도만 같다고 프로토콜 의미까지 같은 것은 아닙니다. SGMII는 speed code와 duplex 상태를 control word에 담고, 1000BASE-X는 Clause 37 의미를 따릅니다. 양단의 PCS 의미 체계가 다르면 block lock은 되더라도 유효 프레임 처리가 안 될 수 있습니다.

패턴 5: RJ45 SFP 모듈만 유난히 뜨겁고 링크 품질이 나쁘다

RJ45 SFP는 작은 폼팩터 안에 copper PHY와 magnetics 상당 부분을 넣기 때문에 열 여유가 매우 작습니다. 일부 스위치나 NIC는 전력 예산 때문에 copper SFP를 제한하기도 합니다. 광 모듈에서는 멀쩡한 포트가 copper SFP만 꽂으면 불안정해진다면 모듈 발열과 host cage 전원 budget을 의심하세요.

패턴 6: 부팅 직후는 정상인데 suspend/resume 뒤 링크가 나빠진다

resume 경로에서 PHY strap override, RGMII delay bit, host PCS mode, 모듈 sideband 초기화가 다시 적용되지 않는 경우가 많습니다. 특히 부팅 때만 실행한 config_init에 핵심 설정을 몰아 넣으면, resume 뒤에는 기본 strap 값으로 돌아가 CRC나 속도 강등이 다시 나타날 수 있습니다.

패턴 7: 같은 보드인데 슬롯이나 포트 하나만 간헐적으로 안 보입니다

이 경우는 PHY 드라이버보다 MDIO mux, shared reset, 주소 재사용 구조를 먼저 봐야 합니다. 특정 슬롯에서만 PHY ID가 0xffff로 튄다면 해당 슬롯의 mux 선택선, pull-up, hot-plug reset 타이밍이 가장 유력합니다. 동일한 드라이버가 다른 슬롯에서는 멀쩡하다는 사실이 중요한 단서입니다.

패턴 8: fixed-link로는 되는데 in-band status로 바꾸면 링크가 흐트러진다

보통은 host PCS가 speed code와 duplex 상태를 제대로 읽지 못하거나, 반대로 양단이 AN 의미를 서로 다르게 해석하고 있는 뜻입니다. fixed-link는 문제가 가려진 상태일 수 있으므로, 실제 목표 토폴로지가 in-band status라면 PCS와 SerDes 설정을 끝까지 맞춰야 합니다.

원인 추적 참고: managed = "in-band-status"는 커널 내부에서 MLO_AN_INBAND 모드를 활성화하며, 이 모드에서는 PCS 드라이버의 pcs_get_state()가 실제로 SGMII control word 또는 Clause 37 base page를 해석해야 합니다. PCS 드라이버가 이 콜백을 제대로 구현하지 않으면 fixed-link에서는 문제없던 포트가 in-band status로 전환하자마자 실패합니다. 세부 동작 원리는 In-Band vs Out-of-Band 링크 상태 결정phylink의 링크 상태 결정 경로 절을 참고하세요.

패턴 9: 광량은 정상인데 LOS가 계속 올라와 링크가 안 오른다

SFP 계열에서는 PHY 레지스터보다 cage sideband가 더 중요할 때가 있습니다. LOS, TX_FAULT, MOD_ABS pull-up 전압이 맞지 않거나 극성이 뒤집히면 광 모듈 자체는 정상이어도 host는 계속 fault 상태로 판단합니다. 이 경우 ethtool -m으로 DOM이 정상인데 링크만 안 오른다는 모순된 모습이 나옵니다.

트러블슈팅

PHY 장애는 "링크가 안 오른다", "링크는 오르는데 품질이 나쁘다", "부팅 직후만 실패합니다" 세 부류로 나누면 정리가 쉽습니다. 첫 번째는 Device Tree, reset, PHY 주소, 케이블, 상대편 광고 능력부터 확인합니다. 두 번째는 phy-mode, RGMII 지연, EEE, FEC, 온도, 모듈 품질로 좁힙니다. 세 번째는 부팅 순서와 전원 안정화, strap pin, clock ready 타이밍 문제일 가능성이 큽니다.

PHY 트러블슈팅 실전 점검 순서 실전 점검 순서와 도구 매핑 1. PHY 존재 확인 mdio_bus/devices PHY ID 읽기 2. 링크 정책 확인 ethtool eth0 speed/duplex/AN 3. 물리 오류 확인 ethtool -S eth0 CRC/align/symbol 4. 보드 타이밍 phy-mode, delay reset, clock 5. 상대편 포트/케이블 교체 테스트 단계별 사용 도구와 확인 대상 dmesg probe 로그 확인 PHY 드라이버 바인딩 sysfs /sys/bus/mdio_bus/ devices 목록 readlink /sys/class/net/ eth0/phydev ethtool speed, duplex 확인 supported/advertised ethtool --show-eee EEE 상태 확인 ip monitor link 링크 이벤트 실시간 ethtool -S CRC, align, carrier symbol error 카운터 ethtool -m SFP DOM 광량 TX/RX power, 온도 --cable-test TDR 배선 진단 Device Tree 확인 phy-mode 값 reset-gpios 타이밍 dynamic debug drivers/net/phy/* +p ethtool -d MAC/PHY 레지스터 dump 비교 교체 테스트 케이블 교체 스위치 포트 변경 강제 100M 테스트로 속도 분리
트러블슈팅은 PHY 존재 확인부터 시작해 링크 정책, 물리 오류, 보드 타이밍, 상대편 순서로 좁혀 나갑니다.
증상가능한 원인즉시 확인할 것다음 조치
PHY를 전혀 찾지 못함잘못된 PHY 주소, reset 타이밍 부족, 전원 미인가dmesg, /sys/bus/mdio_bus/devicesPHY 주소와 reset GPIO, regulator 순서 점검
링크 업 안 됨상대편 포트 정책, 케이블, advertisement 불일치ethtool eth0양쪽 autoneg 정책과 케이블 등급 확인
링크 업 후 CRC 증가RGMII skew, 신호 품질, magnetics 문제ethtool -S eth0phy-mode와 보드 지연 위치 재검토
2.5G/5G가 1G로만 붙음케이블 품질, 상대편 광고 제한supported/advertised link modesCat5e/Cat6 등급과 상대편 설정 확인
온도 올라가면 링크 flapping전원 품질, 모듈 열화, 아날로그 margin 부족온도 로그, 모듈 DOM전원 rail, 방열, PHY 벤더 권장값 적용
# 1. 기본 링크 요약
ethtool eth0

# 2. NIC/드라이버 통계에서 물리 계층 오류 확인
ethtool -S eth0 | grep -E "crc|align|carrier|symbol|an|eee"

# 3. 현재 netdev가 어떤 PHY에 연결되어 있는지 추적
readlink -f /sys/class/net/eth0/phydev

# 4. 커널 로그에서 MDIO / PHY 이벤트 추적
dmesg | grep -i -E "phy|mdio|link|eee"

# 5. Device Tree의 phy-mode 확인
grep -R . /sys/firmware/devicetree/base | grep phy-mode

# 6. PHY 튜너블과 EDPD/downshift 확인
ethtool --show-phy-tunable eth0

# 7. 지원 시 케이블 TDR 진단
ethtool --cable-test-tdr eth0

# 8. SFP DOM / 광량 확인
ethtool -m eth0

커널 디버그 훅 활용

PHY 문제를 깊게 보려면 ethtool 출력만으로는 부족할 때가 있습니다. 이때는 동적 디버그, 드라이버별 register dump, 모듈 DOM, 장시간 링크 이벤트 로그를 조합해 "협상 실패인지, 품질 문제인지, 전원 복귀 문제인지"를 분리합니다. 특히 phylib는 공통 상태 머신을 쓰므로, 공통 로그와 벤더 드라이버 로그를 같이 보는 것이 좋습니다.

# 동적 디버그가 켜진 커널에서 PHY 공통 로그 활성화
echo 'file drivers/net/phy/* +p' > /sys/kernel/debug/dynamic_debug/control

# 특정 MAC 드라이버 로그도 함께 활성화
echo 'file drivers/net/ethernet/*/* +p' > /sys/kernel/debug/dynamic_debug/control

# 드라이버가 지원하면 MAC/PHY 레지스터 덤프
ethtool -d eth0

# 인터페이스 링크 이벤트 감시
ip monitor link
도구무엇을 보기 좋나언제 쓰나
ethtool링크 정책, 통계, EEE, FEC, 케이블 테스트가장 먼저
dmesgprobe, reset, link flap, SFP 이벤트부팅 직후 실패
dynamic debugphylib 상태 머신과 벤더 드라이버 상세 로그간헐 재현, resume 이슈
ethtool -m광 모듈 DOM, 온도, RX/TX power광 링크 품질 문제
ethtool -d드라이버 제공 레지스터 덤프(Dump)벤더 드라이버 비교

실전 점검 순서

  1. PHY가 존재하는지
    /sys/bus/mdio_bus/devices와 PHY ID부터 봅니다.
  2. 링크 정책이 맞는지
    ethtool eth0에서 speed, duplex, advertisement, pause를 확인합니다.
  3. 오류가 물리 계층인지
    ethtool -S에서 CRC, align, symbol, carrier 카운터를 봅니다.
  4. 보드 타이밍 문제인지
    phy-mode, RGMII delay, reset timing, clock 안정성을 점검합니다.
  5. 상대편 의존 문제인지
    스위치 포트, 모듈, 케이블, 강제 속도 설정을 바꿔 증상이 이동하는지 확인합니다.
오판하기 쉬운 경우: 커널 로그에 드라이버 이름이 보인다고 해서 PHY 설정이 맞다는 뜻은 아닙니다. PHY 드라이버가 바인딩되더라도 RGMII delay, strap pin, EEE, SerDes mode, 모듈 전력 예산까지 모두 맞아야 실제 링크 품질이 확보됩니다.
현장 팁: 증상이 애매하면 먼저 상대편 장비를 바꾸세요. PHY 문제는 로컬 드라이버만 봐서는 절반만 보입니다. 케이블 교체, 스위치 포트 변경, 강제 1G/100M 테스트는 원인을 빠르게 반으로 줄이는 가장 싸고 빠른 방법입니다.

PHY 펌웨어(Firmware) 로딩

일부 고성능 PHY는 리셋 후 부팅 과정에서 외부 펌웨어(Firmware)를 MDIO 버스를 통해 업로드받아야만 정상 동작합니다. 대표적으로 Aquantia(현 Marvell) AQR 시리즈 10G PHY와 일부 Marvell 88X 시리즈가 해당합니다. 이런 PHY는 내부에 DSP(Digital Signal Processor) 코어를 내장하고 있으며, DSP가 실행할 마이크로코드를 부팅 시 MDIO를 통해 기록해야만 10GBASE-T 등의 복잡한 선로 등화(Equalization) 알고리즘을 수행할 수 있습니다.

펌웨어 없이 PHY가 올라오면 드라이버 probe는 성공하더라도 링크 협상 자체가 시작되지 않습니다. 커널 로그에서 phy firmware not found 또는 request_firmware failed 메시지가 보인다면 이 단계가 실패한 것입니다.

커널 펌웨어 로딩 API

커널의 표준 펌웨어 로딩 인터페이스를 사용합니다. 펌웨어 파일은 /lib/firmware/ 하위에 위치하며, linux-firmware 저장소로 배포됩니다.

#include <linux/firmware.h>

/* 동기식 펌웨어 요청 (probe 시 가장 일반적으로 사용) */
int request_firmware(const struct firmware **fw,
                     const char *name,
                     struct device *device);

/* 비동기식 요청 (부트 타임 지연을 줄이고 싶을 때) */
int request_firmware_nowait(struct module *module,
                            bool uevent,
                            const char *name,
                            struct device *device,
                            gfp_t gfp,
                            void *context,
                            void (*cont)(const struct firmware *fw,
                                         void *context));

/* 사용 후 반드시 해제 */
void release_firmware(const struct firmware *fw);

펌웨어 파일 경로 규칙: /lib/firmware/marvell/<chip>.bin 또는 /lib/firmware/aquantia/<chip>.cld 형식이 관례입니다. Device Tree에서 firmware-name 속성으로 경로를 지정하면 드라이버가 이 값을 우선적으로 사용합니다.

펌웨어 로딩 순서

/* Aquantia 스타일 PHY 펌웨어 로딩 개념 예시 */
static int aqr_firmware_load(struct phy_device *phydev)
{
    const struct firmware *fw;
    const char *fw_name;
    int ret;

    /* 1단계: Device Tree에서 firmware-name 읽기,
     *         없으면 칩 기본 이름 사용 */
    if (of_property_read_string(phydev->mdio.dev.of_node,
                                "firmware-name", &fw_name))
        fw_name = "aquantia/aqr107.cld";

    /* 2단계: 커널 펌웨어 서브시스템에서 파일 요청 */
    ret = request_firmware(&fw, fw_name, &phydev->mdio.dev);
    if (ret) {
        phydev_err(phydev, "펌웨어 로드 실패: %s (%d)\n",
                   fw_name, ret);
        return ret;
    }

    /* 3단계: PHY를 MDIO 부트로더 모드로 전환
     *         (칩별로 magic 레지스터 시퀀스가 다름) */
    phy_write_mmd(phydev, MDIO_MMD_VEND1,
                  AQR_GLOBAL_BOOT_CTRL, AQR_BOOT_MODE_FLASH);

    /* 4단계: 펌웨어 바이너리를 MDIO 쓰기로 업로드
     *         (일반적으로 Mailbox 레지스터 또는 특수 MMD 블록 사용) */
    ret = aqr_upload_firmware(phydev, fw->data, fw->size);
    if (ret) {
        phydev_err(phydev, "펌웨어 업로드 실패 (%d)\n", ret);
        goto out;
    }

    /* 5단계: PHY 재부팅(soft reset) 후 펌웨어 실행 대기 */
    phy_write_mmd(phydev, MDIO_MMD_VEND1,
                  AQR_GLOBAL_SOFT_RESET, AQR_SOFT_RESET_BIT);
    msleep(2000); /* 펌웨어 초기화 완료 대기 */

    phydev_info(phydev, "펌웨어 로드 완료: %s (%zu bytes)\n",
                fw_name, fw->size);
out:
    release_firmware(fw);
    return ret;
}

static int aqr_probe(struct phy_device *phydev)
{
    int ret;

    /* probe 단계에서 펌웨어 로드 */
    ret = aqr_firmware_load(phydev);
    if (ret)
        return ret;

    /* 이후 일반 초기화 진행 */
    return 0;
}

Device Tree firmware-name 속성

/* Device Tree 예시 */
&mdio0 {
    phy0: ethernet-phy@0 {
        compatible = "ethernet-phy-id03a1.b4b3";
        reg = <0>;
        /* 기본 펌웨어 경로를 재정의할 때 사용 */
        firmware-name = "aquantia/aqr107_custom.cld";
    };
};

펌웨어가 필요한 주요 PHY

제조사칩 시리즈속도펌웨어 경로비고
Aquantia / Marvell AQR107, AQR108, AQR405, AQR411, AQR412 2.5G / 5G / 10G aquantia/aqr107.cld CLD 포맷, 수백 KB
Marvell 88X3310, 88X3340 10G marvell/88X3310.bin SBUS 펌웨어 포함
Broadcom BCM84891 10G broadcom/bcm84891.bin SPI NOR 선행 로딩 가능
Intel / Maxlinear GPY211, GPY215 2.5G 내장 ROM (펌웨어 불필요) 일부 보드는 NVM 업데이트 필요

펌웨어 로딩 문제 해결

펌웨어 관련 문제는 크게 세 가지 유형으로 나뉩니다.

펌웨어 라이선스와 linux-firmware: PHY 펌웨어 파일은 대부분 독점(Proprietary) 라이선스를 가집니다. 커널 트리에 직접 포함할 수 없으며, linux-firmware 저장소(git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git)를 통해 별도 배포됩니다. 배포판(Distro)에서는 linux-firmware 패키지로 제공되며, 임베디드 시스템에서는 rootfs 또는 initramfs에 펌웨어 파일을 직접 포함시켜야 합니다. 펌웨어 파일의 재배포 가능 여부는 각 칩 제조사의 EULA를 반드시 확인하시기 바랍니다.

ethtool 통계와 PHY 카운터 해석 실전 가이드

ethtool -S <interface> 명령은 드라이버가 노출하는 하드웨어 통계 카운터를 출력합니다. 이 카운터들은 NIC(Network Interface Card) MAC 계층과 PHY 계층에서 독립적으로 집계되므로, 어떤 카운터가 증가하는지 조합해서 보면 문제의 위치를 MAC인지 PHY인지, 더 나아가 케이블인지 상대방 장비인지까지 좁혀 낼 수 있습니다.

카운터 분류 개요

ethtool -S 출력은 드라이버마다 이름이 다르지만, 기능상으로 다음 세 범주로 분류할 수 있습니다.

PHY 관련 주요 카운터 상세

카운터 이름 의미 증가 원인 의심 위치
rx_crc_errors FCS(Frame Check Sequence) 불일치 프레임 수 RGMII 스큐(skew) 오설정, 케이블 손상, EMI(Electromagnetic Interference) 간섭, SFP 모듈 불량 PHY 아날로그 / RGMII 타이밍
rx_frame_errors 정렬(alignment) 오류 — 길이가 8비트의 배수가 아닌 프레임 이더넷 프리앰블 손상, 링크 파트너의 타이밍 문제, 물리 계층 잡음 PHY PCS / 케이블
rx_length_errors 최소(64 B) 미만이거나 최대(1518/9000 B) 초과 프레임 프레임 잘림(Rx FIFO 오버런), Jumbo Frame 미설정, 링크 파트너 MTU 불일치 MAC Rx FIFO / MTU 설정
carrier_errors / link_loss_count 캐리어 감지 실패 횟수 — 링크 up/down 반복 케이블 접촉 불량, 전원 불안정(EEE 진입/복귀 실패), SGMII 협상 실패 PHY 링크 상태 머신 / 케이블
symbol_errors PCS(Physical Coding Sublayer)가 복호화하지 못한 심볼 수 PMA(Physical Medium Attachment) 손실 마진 부족, SerDes 이퀄라이저 미조정, 광 수신 출력 부족 PHY PCS/PMA / 광 링크 예산
collisions 충돌 감지 횟수 Half-duplex로 동작 중인 경우 정상이나, Full-duplex에서 증가하면 Duplex 불일치(Duplex mismatch) 신호 Auto-negotiation 실패 / Duplex 강제 설정 오류

ethtool -S 출력 예시와 주석

# Realtek RTL8211F PHY가 달린 인터페이스 예시
$ ethtool -S eth0

NIC statistics:
     tx_packets: 4821093          # 정상 송신 프레임
     rx_packets: 3927451          # 정상 수신 프레임
     tx_errors: 0
     rx_errors: 17                # ← 0이 아니면 추가 조사 필요
     tx_dropped: 0
     rx_dropped: 3                # FIFO 오버런 또는 소프트웨어 버퍼 부족
     multicast: 1024

     rx_crc_errors: 14            # ← RGMII 스큐 문제 의심, 타이밍 보정 확인
     rx_frame_errors: 2           # ← PHY PCS 레벨 잡음
     rx_length_errors: 1          # ← MTU 불일치 또는 FIFO 오버런
     rx_missed_errors: 0
     tx_aborted_errors: 0
     tx_carrier_errors: 0
     tx_fifo_errors: 0
     tx_heartbeat_errors: 0
     tx_window_errors: 0          # 0이 아니면 Half-duplex 충돌 창 문제

     # PHY vendor-specific (드라이버마다 이름 다름)
     phy_symbol_errors: 0
     phy_rx_aligns: 0
     phy_remote_rx_status: 1      # 원격 PHY Rx 정상
     phy_local_rx_status: 1       # 로컬 PHY Rx 정상

MAC 문제와 PHY 문제 구별 방법

카운터만으로 MAC과 PHY 층을 완전히 분리하기는 어렵지만, 다음 원칙이 실용적입니다.

카운터 진단 흐름도

어떤 카운터가 증가하는가? rx_crc_errors collisions (Full-duplex) symbol_errors rx_dropped / missed RGMII 타이밍 점검 rgmii-rxid / txid 설정 확인 rx-internal-delay-ps 케이블·커넥터 교체 시도 Duplex mismatch ethtool eth0 으로 양단 duplex 확인 강제 설정 또는 AN 재협상 PCS/PMA 물리 계층 SFP: ethtool -m 광 출력 확인 구리: 케이블 길이·품질 SerDes 이퀄라이저 튜닝 소프트웨어 처리 속도 Rx 링 버퍼 크기 확인 ethtool -G 링 확대 IRQ affinity / NAPI 조정 장기 추이 확인 → watch -n 1 ethtool -S eth0 | grep -v ": 0" 근본 원인 식별 후 수정
카운터 종류별 진단 흐름. 점선은 모든 경우에 장기 추이 모니터링이 필요함을 나타냅니다.

지속 모니터링 스크립트

일회성 조회 대신 카운터 변화량을 지속적으로 관찰하면 간헐적 오류도 포착할 수 있습니다.

# 방법 1: watch로 0이 아닌 카운터만 1초 간격 갱신
watch -n 1 'ethtool -S eth0 | grep -v ": 0$"'

# 방법 2: 변화량(delta) 추출 스크립트
#!/bin/bash
IFACE=${1:-eth0}
INTERVAL=${2:-5}

declare -A PREV
while IFS=': ' read -r key val; do
    PREV["$key"]=$val
done < <(ethtool -S "$IFACE" | tail -n +2)

while true; do
    sleep "$INTERVAL"
    declare -A CURR
    while IFS=': ' read -r key val; do
        CURR["$key"]=$val
    done < <(ethtool -S "$IFACE" | tail -n +2)

    echo "=== $(date '+%T') delta (${INTERVAL}s) ==="
    for key in "${!CURR[@]}"; do
        delta=$(( CURR[$key] - ${PREV[$key]:-0} ))
        if (( delta != 0 )); then
            printf "  %-40s %+d\n" "$key" "$delta"
        fi
    done

    for key in "${!CURR[@]}"; do
        PREV[$key]=${CURR[$key]}
    done
done

# 방법 3: 파일에 기록 (후속 분석용)
while true; do
    echo "$(date '+%F %T')" >> /tmp/eth0_stats.log
    ethtool -S eth0 | grep -v ": 0$" >> /tmp/eth0_stats.log
    sleep 60
done

증상 패턴과 근본 원인 매핑

증가하는 카운터 조합 의심 근본 원인 즉시 확인 명령
rx_crc_errors 단독 증가, 링크 안정 RGMII tx/rx delay 설정 오류 cat /sys/bus/mdio_bus/devices/*/phy_id, DT rgmii-rxid 확인
rx_crc_errors + rx_frame_errors 케이블 손상 또는 EMI 간섭 케이블 교체 후 비교, STP 차폐 케이블 사용 여부 확인
collisions Full-duplex 환경에서 증가 Duplex mismatch — 한쪽 강제 Half-duplex ethtool eth0으로 Speed/Duplex 양단 확인
carrier_errors 지속 증가 케이블 접촉 불량 또는 EEE 진입/복귀 타임아웃 ethtool --show-eee eth0, dmesg | grep eth0
symbol_errors 증가, SFP 광 링크 수신 광 출력 부족 또는 광섬유 굽힘 ethtool -m eth0 (Rx power dBm 확인)
rx_dropped + rx_missed_errors 증가 CPU 처리 지연 또는 Rx 링 버퍼 부족 ethtool -g eth0, ethtool -G eth0 rx 4096
rx_crc_errors 급증 후 링크 down/up 반복 SFP 모듈 불량 또는 광 커넥터 오염 SFP 모듈 교체, APC 클리너로 커넥터 청소
모든 오류 카운터 0, 처리량만 낮음 자동 협상 결과 속도 강등(예: 1G→100M) ethtool eth0 | grep Speed

PHY 전용 통계 (--phy-statistics)

Linux 4.16부터 ethtool --phy-statistics <interface> 옵션이 도입되어 PHY 드라이버가 구현한 경우 MAC 통계와 별도로 PHY 레지스터 기반 카운터를 조회할 수 있습니다.

# PHY 전용 통계 조회 (드라이버 지원 시)
$ ethtool --phy-statistics eth0

PHY statistics for eth0:
     phy_receive_errors: 3        # PHY 내부 Rx 오류 (CRC, 심볼 등 PHY 자체 집계)
     phy_duplex_mismatch: 0       # Duplex mismatch 감지 횟수 (일부 PHY만)
     phy_fast_retrain_count: 2    # 2.5G BASE-T fast retrain 횟수

PHY 전용 통계는 드라이버가 ethtool_ops.get_phy_statsethtool_ops.get_phy_sset_count를 구현해야 노출됩니다. Realtek RTL8211F, Marvell Alaska, Micrel KSZ 시리즈 등 일부 드라이버만 지원하며, 지원하지 않는 경우 "Operation not supported" 오류가 반환됩니다.

Clause 45 PHY라면 MMD 3(PCS)의 RMON 카운터 레지스터(3.0x1010~0x1019)를 직접 읽을 수도 있습니다.

# Clause 45 PHY RMON 카운터 직접 읽기 (phytool 필요)
# MMD 3, Register 0x1014 = Rx FCS Error Frame Counter
phytool read eth0/1/3:0x1014
래치(Latched) 카운터와 런닝(Running) 카운터의 차이: 일부 PHY(특히 구형 Clause 22 PHY)의 오류 카운터는 래치 방식으로 동작합니다. 레지스터를 읽는 순간 값이 초기화되므로, 연속 두 번 읽으면 첫 번째 읽기에서만 값이 나옵니다. 반면 MAC의 RMON 카운터와 최신 Clause 45 PHY 카운터는 런닝 방식으로 누적됩니다. 드라이버가 폴링 주기마다 읽어 누적하는 경우도 있으므로, 짧은 폴링 간격에서 카운터가 0으로 보이더라도 오류가 없다고 단정하지 마세요. PHY 데이터시트(Datasheet)의 레지스터 설명에서 "clear on read" 여부를 반드시 확인하세요.

Device Tree 실전 예제 모음

Device Tree(장치 트리)에서 이더넷 PHY를 올바르게 기술하는 것은 보드 bring-up의 핵심입니다. 이 절은 다양한 토폴로지별 완성된 예제를 제공하며, 각 속성의 용도와 자주 발생하는 실수를 함께 설명합니다.

RGMII + 외부 PHY (STM32MP1 + RTL8211F)

STM32MP1 SoC의 GMAC과 Realtek RTL8211F PHY를 RGMII 모드로 연결하는 가장 일반적인 예제입니다. RGMII 인터페이스는 1 Gbps에서 클럭 스큐가 매우 민감하므로 지연(delay) 속성을 정확히 기술해야 합니다.

&ethernet0 {
    status = "okay";
    pinctrl-0 = <&ethernet0_rgmii_pins_a>;
    pinctrl-1 = <&ethernet0_rgmii_sleep_pins_a>;
    pinctrl-names = "default", "sleep";

    phy-mode = "rgmii-id";          /* 내부 Tx+Rx 지연 모두 MAC 측에서 삽입 */
    /* 대안: PHY 측 지연 사용 시 "rgmii-rxid" 또는 "rgmii-txid" */

    max-speed = <1000>;
    phy-handle = <&phy0>;          /* mdio 노드 안의 phy 노드 참조 */

    clocks = <&rcc ETH_MAC>, <&rcc ETH_TX>, <&rcc ETH_RX>;
    clock-names = "stmmaceth", "mac-clk-tx", "mac-clk-rx";

    mdio0: mdio {
        #address-cells = <1>;
        #size-cells = <0>;
        compatible = "snps,dwmac-mdio";

        phy0: ethernet-phy@1 {
            compatible = "ethernet-phy-id001c.c916"; /* RTL8211F PHY ID */
            reg = <1>;                               /* MDIO 주소 */

            /* 하드웨어 reset GPIO */
            reset-gpios = <&gpioa 12 GPIO_ACTIVE_LOW>;
            reset-assert-us   = <10000>;  /* reset 인가 시간 10 ms */
            reset-deassert-us = <30000>;  /* reset 해제 후 안정화 30 ms */

            /* EEE(Energy-Efficient Ethernet) 비활성화 예시 */
            eee-broken-100tx;

            /* 자동 협상 광고 속도 제한 */
            max-speed = <1000>;
        };
    };
};
rgmii-id vs rgmii-rxid vs rgmii: phy-mode에서 id 접미사는 어느 쪽에서 클럭 지연을 삽입하는지를 나타냅니다. rgmii-id는 MAC이 Tx와 Rx 모두 지연을 처리하므로 PHY의 내부 지연을 반드시 비활성화해야 합니다. rgmii-rxid는 PHY가 Rx 지연을 삽입하므로 MAC 쪽 Rx 지연을 끄고, rgmii-txid는 PHY가 Tx 지연을 삽입합니다. 두 곳이 모두 지연을 삽입하면 타이밍이 두 배가 돼 CRC 오류가 대량 발생합니다.

SGMII + SFP 케이지 (NXP LS1028A)

NXP LS1028A의 ENETC MAC을 SGMII 모드로 SFP 케이지에 연결하는 예제입니다. SFP 케이지는 별도의 sfp 노드로 기술하고, 모듈 감지·신호·전원 제어 GPIO를 각각 지정해야 합니다.

/* SFP 케이지 노드 (보드 최상위 레벨) */
sfp_eth0: sfp-eth0 {
    compatible = "sff,sfp";

    i2c-bus = <&i2c1>;                              /* SFP EEPROM I2C 버스 */

    mod-def0-gpios   = <&gpio3 5  GPIO_ACTIVE_LOW>; /* 모듈 삽입 감지 (MOD_ABS) */
    los-gpios        = <&gpio3 6  GPIO_ACTIVE_HIGH>;/* 수신 신호 손실 (LOS) */
    tx-fault-gpios   = <&gpio3 7  GPIO_ACTIVE_HIGH>;/* 송신 장애 (TX_FAULT) */
    tx-disable-gpios = <&gpio3 8  GPIO_ACTIVE_HIGH>;/* 송신 레이저 끄기 (TX_DIS) */

    /* 선택적: SFP 최대 전력 등급 */
    maximum-power-milliwatt = <1000>;
};

/* ENETC MAC 노드 */
&enetc_port0 {
    status = "okay";
    phy-mode = "sgmii";
    sfp = <&sfp_eth0>;    /* SFP 케이지 연결 */
    managed = "in-band-status"; /* SGMII in-band auto-negotiation 사용 */
    /* phy-handle 없음: SFP 내 PHY는 자동 감지 */
};

SFP 케이지에 구리 SFP-T 모듈이 삽입되면 phylink가 자동으로 PHY 드라이버를 바인딩합니다. 광 모듈의 경우 managed = "in-band-status"가 SGMII 협상을 처리합니다. mod-def0-gpios가 없으면 모듈 hot-plug를 감지할 수 없으므로 권장합니다.

MAC과 스위치 칩, 또는 두 MAC을 직접 연결할 때는 Auto-negotiation이 없으므로 속도와 Duplex를 fixed-link 서브노드에 명시해야 합니다.

/* MAC ↔ 스위치 내부 포트 직결 예 */
&fec1 {
    status = "okay";
    phy-mode = "rgmii";           /* 지연 없는 순수 RGMII */
    pinctrl-0 = <&pinctrl_fec1>;
    pinctrl-names = "default";

    fixed-link {
        speed      = <1000>;     /* 링크 속도 Mbps */
        full-duplex;              /* Half-duplex이면 이 줄 제거 */
        /* pause;                    선택적: PAUSE 프레임 지원 표시 */
        /* asym-pause;               선택적: 비대칭 PAUSE 지원 */
    };
};

/* 참고: MDIO가 필요 없으므로 mdio 서브노드 불필요 */
/* 스위치 칩은 별도의 SPI/I2C/MDIO 연결로 제어 */
fixed-link 사용 시 주의: fixed-linkphy-handle을 동시에 기술하면 충돌합니다. 또한 phy-mode에서 rgmii-id처럼 지연이 포함된 변형을 사용하면 MAC 드라이버가 하드웨어 지연을 삽입하려 시도하므로, 직결 구성에서는 rgmii(순수)를 사용하거나 보드 회로도에 지연 소자가 있는지 확인하세요.

MDIO 멀티플렉서 토폴로지

하나의 MDIO 버스에 물리적 멀티플렉서(Mux)를 달아 여러 버스를 파생하는 구성입니다. 멀티플렉서 선택 신호는 GPIO 또는 I2C 제어 레지스터로 구현할 수 있습니다.

/* GPIO 기반 MDIO 멀티플렉서 */
&mdio1 {
    #address-cells = <1>;
    #size-cells = <0>;

    mdio_mux: mdio-mux@0 {
        compatible = "mdio-mux-gpio";

        mux-gpios = <&gpio1 20 GPIO_ACTIVE_HIGH   /* 선택 비트 0 */
                     &gpio1 21 GPIO_ACTIVE_HIGH>;  /* 선택 비트 1 */

        #address-cells = <1>;
        #size-cells = <0>;
        mdio-parent-bus = <&mdio1>;

        /* 채널 0: GPIO = 0b00 */
        mdio_mux_0: mdio@0 {
            reg = <0>;
            #address-cells = <1>;
            #size-cells = <0>;

            phy_mux0: ethernet-phy@4 {
                reg = <4>;
            };
        };

        /* 채널 1: GPIO = 0b01 */
        mdio_mux_1: mdio@1 {
            reg = <1>;
            #address-cells = <1>;
            #size-cells = <0>;

            phy_mux1: ethernet-phy@4 {
                reg = <4>;     /* 다른 버스이므로 주소 충돌 없음 */
            };
        };

        /* 채널 2: GPIO = 0b10 */
        mdio_mux_2: mdio@2 {
            reg = <2>;
            #address-cells = <1>;
            #size-cells = <0>;

            phy_mux2: ethernet-phy@4 {
                reg = <4>;
            };
        };
    };
};

/* MAC에서 멀티플렉서 뒤쪽 PHY 참조 */
&eth0 {
    phy-handle = <&phy_mux0>;
    phy-mode = "rgmii-id";
};

듀얼 MAC과 공유 MDIO 버스

두 개의 MAC이 하나의 MDIO 버스를 공유하고 각각 다른 PHY를 제어하는 구성입니다. MDIO 버스는 한 MAC의 서브노드로 선언하고, 다른 MAC은 phy-handle로 동일 노드를 참조합니다.

/* MAC 0: MDIO 버스 소유 */
&gmac0 {
    status = "okay";
    phy-mode = "rgmii-id";
    phy-handle = <&phy0>;

    mdio_shared: mdio {
        #address-cells = <1>;
        #size-cells = <0>;
        compatible = "snps,dwmac-mdio";

        phy0: ethernet-phy@1 {
            reg = <1>;                          /* MDIO 주소 1: gmac0용 PHY */
            reset-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
            reset-assert-us   = <10000>;
            reset-deassert-us = <30000>;
        };

        phy1: ethernet-phy@2 {
            reg = <2>;                          /* MDIO 주소 2: gmac1용 PHY */
            reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;
            reset-assert-us   = <10000>;
            reset-deassert-us = <30000>;
        };
    };
};

/* MAC 1: 공유 MDIO 버스의 PHY를 참조 */
&gmac1 {
    status = "okay";
    phy-mode = "rgmii-id";
    phy-handle = <&phy1>;
    /* 자체 mdio 서브노드 없음 — gmac0의 mdio 버스 공유 */
};

PHY 패키지 (4포트 통합 PHY)

Marvell 88E1512 4포트나 Realtek RTL8367RB 같이 단일 칩에 여러 PHY가 내장된 경우 ethernet-phy-package 노드로 공유 자원을 묶어야 합니다.

/* 4포트 PHY 패키지 예 (Marvell 88E1510 quad) */
&mdio {
    #address-cells = <1>;
    #size-cells = <0>;

    phy_package: ethernet-phy-package@0 {
        #address-cells = <1>;
        #size-cells = <0>;
        reg = <0>;                  /* 패키지 기준 MDIO 주소 */

        /* 패키지 전체 하드웨어 reset: 모든 포트에 동시 적용 */
        reset-gpios = <&gpio2 10 GPIO_ACTIVE_LOW>;
        reset-assert-us   = <10000>;
        reset-deassert-us = <50000>;

        /* 포트 0: MDIO 주소 0 */
        phy_pkg0: ethernet-phy@0 {
            reg = <0>;
        };

        /* 포트 1: MDIO 주소 1 */
        phy_pkg1: ethernet-phy@1 {
            reg = <1>;
        };

        /* 포트 2: MDIO 주소 2 */
        phy_pkg2: ethernet-phy@2 {
            reg = <2>;
        };

        /* 포트 3: MDIO 주소 3 */
        phy_pkg3: ethernet-phy@3 {
            reg = <3>;
        };
    };
};

/* 각 MAC이 패키지 내 PHY를 개별 참조 */
&eth0 { phy-handle = <&phy_pkg0>; phy-mode = "rgmii-id"; }
&eth1 { phy-handle = <&phy_pkg1>; phy-mode = "rgmii-id"; }
&eth2 { phy-handle = <&phy_pkg2>; phy-mode = "rgmii-id"; }
&eth3 { phy-handle = <&phy_pkg3>; phy-mode = "rgmii-id"; }

커널의 phy_package_join() / phy_package_leave() API는 패키지 내 여러 PHY 드라이버가 글로벌 초기화를 한 번만 수행하도록 조율합니다. PHY 드라이버는 phy_package_init_once()를 사용해 공유 레지스터 초기화가 중복 실행되지 않도록 해야 합니다.

자주 혼동하는 DT 속성 비교표

속성 쌍 속성 A 속성 B 차이 및 올바른 사용
phy-mode vs phy-connection-type phy-mode = "rgmii-id"
현재 표준 속성명
phy-connection-type = "rgmii-id"
구형 속성명 (deprecated)
두 이름은 동일한 의미입니다. 신규 DTS에서는 반드시 phy-mode를 사용하세요. 구형 BSP를 포팅할 때 phy-connection-type을 만나면 phy-mode로 교체하세요.
phy-handle vs MDIO 자식 노드 직접 기술 phy-handle = <&phy0>;
별도 선언된 PHY 노드를 phandle로 참조
MAC 노드 아래 mdio 서브노드 안에 ethernet-phy@N 직접 선언 두 방법 모두 유효합니다. phy-handle은 MDIO를 공유하거나 PHY 노드를 여러 곳에서 참조할 때 필요합니다. 단독 구성에서는 직접 선언도 가능하지만, phy-handle 방식이 더 명확합니다.
managed vs in-band-status managed = "in-band-status"
속성 이름이 managed, 값이 "in-band-status"
in-band-status;
구형 불리언 속성 (일부 BSP에서 사용)
managed = "in-band-status"가 현재 표준입니다. SGMII/1000BASE-X 링크에서 PHY 없이 in-band(대역 내) 협상을 사용할 때 기술합니다. 이 속성이 있으면 phylink가 외부 PHY 없이도 링크 상태를 추적합니다.
fixed-link vs phy-handle fixed-link { speed = <1000>; full-duplex; }; phy-handle = <&phyN>; fixed-link는 Auto-negotiation이 없는 직결 구성에서만 사용합니다. 실제 PHY가 붙어있으면 반드시 phy-handle을 사용하세요. 둘을 동시에 기술하면 동작이 정의되지 않습니다.
reset-gpios 위치 PHY 노드(ethernet-phy@N) 안에 기술 MAC 노드나 보드 최상위에 기술 PHY reset GPIO는 반드시 ethernet-phy@N 노드 안에 기술해야 phylib가 자동으로 reset 시퀀스를 제어합니다. MAC 노드나 최상위에 기술하면 커널이 인식하지 못합니다.
rx-internal-delay-ps vs rxc-skew-ps rx-internal-delay-ps = <2000>;
PHY 내부 Rx 지연을 피코초 단위로 설정 (신규 방식)
rxc-skew-ps = <1860>;
구형 Micrel/KSZ 방식 속성
신규 보드에서는 rx-internal-delay-ps / tx-internal-delay-ps를 사용하세요. 두 속성이 공존하면 rx-internal-delay-ps가 우선합니다. 지원 범위는 PHY 드라이버 코드에서 확인하세요.

NIC(MAC) 드라이버 개발자를 위한 PHY 통합 실전 가이드

PHY 드라이버를 직접 작성하는 것과, MAC(NIC) 드라이버에서 PHY를 올바르게 연결하고 운용하는 것은 별개의 과제입니다. 이 절은 첫 MAC 드라이버를 작성하거나 기존 드라이버에 PHY 지원을 추가하는 초급 개발자가 반드시 알아야 할 사항을 다룹니다. PHY 드라이버 자체는 PHY 드라이버 작성 가이드, MAC 드라이버 구조는 Network Device 드라이버 문서를 참고하세요.

이 절의 전제: 여기서 "NIC 드라이버"란 MAC 하드웨어를 제어하는 드라이버(net_device_ops 구현체)를 뜻합니다. PHY 칩 자체의 벤더 드라이버가 아니라, MAC 쪽에서 PHY를 찾고 연결하고 링크 변화에 대응하는 코드를 작성하는 관점입니다.

MDIO 버스 등록: MAC 드라이버의 첫 번째 책임

MAC 드라이버가 PHY를 사용하려면 먼저 mii_bus를 할당하고 등록해야 합니다. 이 과정이 없으면 커널은 PHY를 찾을 수 없습니다. MDIO 버스 등록은 MAC의 probe() 안에서 수행하며, remove()에서 반드시 해제해야 합니다.

/* MAC 드라이버의 MDIO 버스 등록 전체 흐름 */
static int my_mac_mdio_init(struct my_priv *priv)
{
    struct mii_bus *bus;
    struct device_node *mdio_np;
    int ret;

    bus = mdiobus_alloc();
    if (!bus)
        return -ENOMEM;

    bus->name = "my-mac-mdio";
    snprintf(bus->id, MII_BUS_ID_SIZE, "%s",
             dev_name(priv->dev));
    bus->read  = my_mdio_read;   /* 하드웨어 MDIO read 구현 */
    bus->write = my_mdio_write;  /* 하드웨어 MDIO write 구현 */
    bus->parent = priv->dev;
    bus->priv = priv;

    /* Device Tree에서 mdio 서브노드 찾기 */
    mdio_np = of_get_child_by_name(priv->dev->of_node, "mdio");

    if (mdio_np) {
        /* DT 기반 등록: PHY 주소를 DT에서 자동 해석 */
        ret = of_mdiobus_register(bus, mdio_np);
        of_node_put(mdio_np);
    } else {
        /* DT 없이 전체 주소 스캔 (비추천, 레거시용) */
        ret = mdiobus_register(bus);
    }

    if (ret) {
        mdiobus_free(bus);
        return ret;
    }

    priv->mii_bus = bus;
    return 0;
}

/* remove에서 반드시 해제 */
static void my_mac_mdio_fini(struct my_priv *priv)
{
    if (priv->mii_bus) {
        mdiobus_unregister(priv->mii_bus);
        mdiobus_free(priv->mii_bus);
    }
}
mdio_read/write 구현 시 주의: MDIO read/write 콜백은 슬립이 가능한 컨텍스트에서 호출됩니다. 하드웨어 레지스터 접근 시 폴링 타임아웃을 반드시 설정하고, 무한 루프를 방지해야 합니다. 또한 버스 잠금(locking)은 mii_bus->mdio_lock이 자동으로 관리하므로 콜백 안에서 별도 락을 잡지 마세요.

PHY 연결 패턴: phylib 직접 연결 vs phylink

MAC 드라이버에서 PHY를 연결하는 방법은 크게 두 가지입니다. 어떤 패턴을 선택하느냐에 따라 구현해야 하는 콜백과 링크 변화 처리 방식이 완전히 달라집니다.

항목phylib 직접 연결phylink 사용
연결 함수of_phy_connect() / phy_connect_direct()phylink_create() + phylink_of_phy_connect()
링크 변화 콜백adjust_link(net_device *)mac_link_up() / mac_link_down() / mac_config()
시작/정지phy_start() / phy_stop()phylink_start() / phylink_stop()
ethtool 연결phy_ethtool_get_link_ksettings() 등 수동 연결phylink_ethtool_ksettings_get() 등 일괄 제공
해제phy_disconnect()phylink_disconnect_phy() + phylink_destroy()
적합한 경우외부 PHY 1개, 단순 RGMII/MIISFP, in-band, PCS 분리, fixed-link

패턴 A: phylib 직접 연결 (단순 구성)

/* probe에서 PHY 연결 */
static int my_mac_probe(struct platform_device *pdev)
{
    struct my_priv *priv = ...;
    struct device_node *phy_np;
    int ret;

    /* 1단계: MDIO 버스 등록 */
    ret = my_mac_mdio_init(priv);
    if (ret)
        return ret;

    /* 2단계: DT에서 phy-handle 찾기 */
    phy_np = of_parse_phandle(pdev->dev.of_node, "phy-handle", 0);
    if (!phy_np) {
        dev_err(&pdev->dev, "phy-handle not found in DT\n");
        ret = -ENODEV;
        goto err_mdio;
    }

    /* 3단계: phy-mode 파싱 */
    ret = of_get_phy_mode(pdev->dev.of_node, &priv->phy_mode);
    if (ret) {
        priv->phy_mode = PHY_INTERFACE_MODE_RGMII; /* 폴백 기본값 */
    }

    /* 4단계: PHY 연결 — adjust_link 콜백 등록 */
    priv->phydev = of_phy_connect(priv->ndev, phy_np,
                                   &my_adjust_link, 0,
                                   priv->phy_mode);
    of_node_put(phy_np);

    if (!priv->phydev) {
        dev_err(&pdev->dev, "PHY connect failed\n");
        ret = -ENODEV;
        goto err_mdio;
    }

    /* 5단계: 지원 속도 제한 (하드웨어 능력에 맞게) */
    phy_set_max_speed(priv->phydev, SPEED_1000);
    phy_remove_link_mode(priv->phydev,
                         ETHTOOL_LINK_MODE_1000baseT_Half_BIT);

    phy_attached_info(priv->phydev);
    return 0;

err_mdio:
    my_mac_mdio_fini(priv);
    return ret;
}

패턴 B: phylink 사용 (권장, 확장 가능)

/* phylink MAC 콜백 구현 */
static void my_mac_config(struct phylink_config *config,
                          unsigned int mode,
                          const struct phylink_link_state *state)
{
    struct my_priv *priv = container_of(config, struct my_priv,
                                          phylink_config);

    /* 인터페이스 모드 변경 시 MAC 클럭 소스/PCS 설정 조정 */
    if (state->interface != priv->current_interface) {
        my_mac_set_interface_mode(priv, state->interface);
        priv->current_interface = state->interface;
    }
}

static void my_mac_link_up(struct phylink_config *config,
                           struct phy_device *phy,
                           unsigned int mode,
                           phy_interface_t interface,
                           int speed, int duplex,
                           bool tx_pause, bool rx_pause)
{
    struct my_priv *priv = container_of(config, struct my_priv,
                                          phylink_config);

    /* ▼ 반드시 구현해야 할 핵심 작업 */
    my_mac_set_speed(priv, speed);        /* MAC 클럭 분주 변경 */
    my_mac_set_duplex(priv, duplex);      /* full/half 모드 설정 */
    my_mac_set_pause(priv, tx_pause, rx_pause); /* flow control */
    my_mac_enable_tx_rx(priv);            /* TX/RX 활성화 */
}

static void my_mac_link_down(struct phylink_config *config,
                             unsigned int mode,
                             phy_interface_t interface)
{
    struct my_priv *priv = container_of(config, struct my_priv,
                                          phylink_config);

    my_mac_disable_tx_rx(priv);           /* TX/RX 비활성화 */
}

static const struct phylink_mac_ops my_phylink_mac_ops = {
    .mac_config    = my_mac_config,
    .mac_link_up   = my_mac_link_up,
    .mac_link_down = my_mac_link_down,
};

/* probe에서 phylink 초기화 */
static int my_mac_probe_phylink(struct my_priv *priv)
{
    struct phylink_config *plcfg = &priv->phylink_config;

    plcfg->dev = &priv->ndev->dev;
    plcfg->type = PHYLINK_NETDEV;

    /* MAC이 지원하는 인터페이스 모드 선언 */
    __set_bit(PHY_INTERFACE_MODE_RGMII_ID,
              plcfg->supported_interfaces);
    __set_bit(PHY_INTERFACE_MODE_SGMII,
              plcfg->supported_interfaces);

    /* MAC 능력 선언 */
    phy_interface_set_rgmii(plcfg->supported_interfaces);
    plcfg->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
                              MAC_10 | MAC_100 | MAC_1000FD;

    priv->phylink = phylink_create(plcfg, dev_fwnode(priv->dev),
                                    priv->phy_mode,
                                    &my_phylink_mac_ops);
    if (IS_ERR(priv->phylink))
        return PTR_ERR(priv->phylink);

    return phylink_of_phy_connect(priv->phylink,
                                   priv->dev->of_node, 0);
}

NIC 드라이버 초보자가 가장 자주 실수하는 부분이 링크 변화 콜백입니다. PHY가 속도/듀플렉스/pause를 재협상하면 MAC 하드웨어 레지스터를 반드시 갱신해야 합니다. 이 콜백을 빈 함수로 두거나, speed만 바꾸고 duplex나 flow control을 빠뜨리면 실제 운영에서 미묘한 장애가 발생합니다.

/* phylib adjust_link 콜백: 반드시 구현할 4가지 작업 */
static void my_adjust_link(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct phy_device *phydev = ndev->phydev;
    bool changed = false;

    /* ① 링크 다운 처리 */
    if (!phydev->link) {
        if (priv->link_up) {
            priv->link_up = false;
            my_mac_disable_tx_rx(priv);
            netif_carrier_off(ndev);
            netdev_info(ndev, "Link down\n");
        }
        return;
    }

    /* ② 속도 변경 반영 */
    if (priv->speed != phydev->speed) {
        my_mac_set_speed(priv, phydev->speed);
        priv->speed = phydev->speed;
        changed = true;
    }

    /* ③ 듀플렉스 변경 반영 */
    if (priv->duplex != phydev->duplex) {
        my_mac_set_duplex(priv, phydev->duplex);
        priv->duplex = phydev->duplex;
        changed = true;
    }

    /* ④ Flow control 반영 */
    if (priv->pause != phydev->pause ||
        priv->asym_pause != phydev->asym_pause) {
        my_mac_set_pause(priv, phydev->pause, phydev->asym_pause);
        priv->pause = phydev->pause;
        priv->asym_pause = phydev->asym_pause;
        changed = true;
    }

    /* 링크 업 전환 */
    if (!priv->link_up) {
        priv->link_up = true;
        my_mac_enable_tx_rx(priv);
        netif_carrier_on(ndev);
        changed = true;
    }

    if (changed)
        phy_print_status(phydev);
}
빈 adjust_link 금지: adjust_link{ }로 두면 PHY가 1G→100M으로 재협상해도 MAC은 여전히 1G 클럭을 사용합니다. 결과적으로 "링크 업인데 CRC가 폭증하는" 증상이 나타나며, RGMII delay 문제와 쉽게 혼동됩니다. mac_link_up()도 마찬가지입니다.

ethtool PHY 관련 콜백 연결

MAC 드라이버는 ethtool에서 PHY 관련 명령이 들어오면 phylib 또는 phylink의 헬퍼로 위임해야 합니다. 이 연결을 빠뜨리면 ethtool eth0이 정보를 보여주지 못하거나 ethtool -s eth0 speed 100 같은 명령이 동작하지 않습니다.

ethtool 기능phylib 사용 시phylink 사용 시
속도/듀플렉스 조회phy_ethtool_get_link_ksettings()phylink_ethtool_ksettings_get()
속도/듀플렉스 설정phy_ethtool_set_link_ksettings()phylink_ethtool_ksettings_set()
AN 재시작phy_ethtool_nway_reset()phylink_ethtool_nway_reset()
pause 조회/설정직접 구현phylink_ethtool_get/set_pauseparam()
EEE 조회/설정phy_ethtool_get/set_eee()phylink_ethtool_get/set_eee() (커널 6.x+)
/* phylib 사용 시 ethtool ops 연결 예시 */
static const struct ethtool_ops my_ethtool_ops = {
    .get_link_ksettings = phy_ethtool_get_link_ksettings,
    .set_link_ksettings = phy_ethtool_set_link_ksettings,
    .nway_reset         = phy_ethtool_nway_reset,
    .get_link           = ethtool_op_get_link,
    /* ... MAC 고유 ops ... */
};

/* phylink 사용 시 ethtool ops 연결 예시 */
static const struct ethtool_ops my_ethtool_ops_phylink = {
    .get_link_ksettings = phylink_ethtool_ksettings_get,
    .set_link_ksettings = phylink_ethtool_ksettings_set,
    .nway_reset         = phylink_ethtool_nway_reset,
    .get_pauseparam     = phylink_ethtool_get_pauseparam,
    .set_pauseparam     = phylink_ethtool_set_pauseparam,
    .get_link           = ethtool_op_get_link,
    /* ... MAC 고유 ops ... */
};

open/close에서의 PHY 시작·정지 순서

인터페이스가 올라갈 때(ndo_open)와 내려갈 때(ndo_stop)의 PHY 관련 호출 순서는 미묘하지만 중요합니다. 순서가 뒤바뀌면 링크 변화 콜백이 이미 해제된 리소스에 접근하거나, 큐가 활성화된 상태에서 MAC 설정이 바뀌는 문제가 생깁니다.

/* ndo_open: 올바른 시작 순서 */
static int my_ndo_open(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    int ret;

    /* ① MAC 하드웨어 초기화 (DMA, 링 버퍼) */
    ret = my_mac_hw_init(priv);
    if (ret)
        return ret;

    /* ② IRQ 등록 */
    ret = my_mac_request_irqs(priv);
    if (ret)
        goto err_hw;

    /* ③ NAPI 활성화 */
    napi_enable(&priv->napi);

    /* ④ PHY 상태 머신 시작 — 여기서부터 adjust_link가 호출될 수 있음 */
    phy_start(ndev->phydev);
    /* phylink 사용 시: phylink_start(priv->phylink); */

    /* ⑤ 네트워크 큐 시작 */
    netif_tx_start_all_queues(ndev);
    return 0;

err_hw:
    my_mac_hw_fini(priv);
    return ret;
}

/* ndo_stop: 올바른 정지 순서 (open의 역순) */
static int my_ndo_stop(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* ① 네트워크 큐 정지 */
    netif_tx_stop_all_queues(ndev);
    netif_carrier_off(ndev);

    /* ② PHY 정지 — 더 이상 adjust_link가 호출되지 않음 */
    phy_stop(ndev->phydev);
    /* phylink 사용 시: phylink_stop(priv->phylink); */

    /* ③ NAPI 비활성화 */
    napi_disable(&priv->napi);

    /* ④ IRQ 해제 */
    my_mac_free_irqs(priv);

    /* ⑤ MAC 하드웨어 정지 */
    my_mac_hw_fini(priv);
    return 0;
}
ndo_open / ndo_stop 호출 순서 ndo_open (시작) ① MAC HW 초기화 (DMA, 링 버퍼) ② IRQ 등록 ③ napi_enable() ④ phy_start() / phylink_start() ⑤ netif_tx_start_all_queues() ndo_stop (정지, 역순) ① netif_tx_stop + carrier_off ② phy_stop() / phylink_stop() ③ napi_disable() ④ IRQ 해제 ⑤ MAC HW 정지 역순 phy_stop() 이후에는 adjust_link가 호출되지 않으므로, 그 이전에 큐 정지가 완료되어야 합니다
ndo_open과 ndo_stop은 서로 역순으로 리소스를 초기화/해제합니다. PHY 시작·정지 위치가 잘못되면 경합 조건이 발생합니다.

remove에서의 정리 순서

MAC 드라이버의 remove()(또는 disconnect())에서는 PHY 관련 자원을 올바른 순서로 해제해야 합니다. 특히 unregister_netdev()는 내부적으로 ndo_stop()을 호출할 수 있으므로, PHY disconnect와 MDIO bus 해제는 그 이후에 수행해야 합니다.

/* remove에서의 정리 순서 */
static void my_mac_remove(struct platform_device *pdev)
{
    struct my_priv *priv = platform_get_drvdata(pdev);

    /* ① netdev 해제 — 내부에서 ndo_stop() 호출 가능 */
    unregister_netdev(priv->ndev);

    /* ② PHY 연결 해제 */
    if (priv->ndev->phydev)
        phy_disconnect(priv->ndev->phydev);
    /* phylink 사용 시:
     * phylink_disconnect_phy(priv->phylink);
     * phylink_destroy(priv->phylink);
     */

    /* ③ MDIO 버스 해제 — PHY disconnect 이후에! */
    my_mac_mdio_fini(priv);

    /* ④ 기타 하드웨어 자원 해제 */
    free_netdev(priv->ndev);
}
해제 순서 위반 시 증상: MDIO bus를 PHY disconnect 전에 해제하면, PHY 상태 머신이 이미 해제된 버스에 read/write를 시도하여 커널 oops가 발생합니다. phylink 사용 시에도 phylink_destroy()phylink_disconnect_phy() 이후에 호출해야 합니다.

Deferred Probe 처리

Device Tree 기반 시스템에서는 PHY가 아직 probe되지 않았을 때 MAC 드라이버의 of_phy_connect()가 실패할 수 있습니다. 이 경우 -EPROBE_DEFER를 반환하면 커널이 나중에 다시 probe를 시도합니다. 이 처리를 빠뜨리면 부팅 순서에 따라 PHY가 연결되지 않는 간헐적 문제가 발생합니다.

/* deferred probe 처리 패턴 */
static int my_mac_probe(struct platform_device *pdev)
{
    struct device_node *phy_np;
    struct phy_device *phydev;

    phy_np = of_parse_phandle(pdev->dev.of_node, "phy-handle", 0);
    if (!phy_np)
        return -ENODEV;

    phydev = of_phy_find_device(phy_np);
    of_node_put(phy_np);

    if (!phydev) {
        /* PHY가 아직 등록되지 않음 — 나중에 재시도 요청 */
        return -EPROBE_DEFER;
    }

    /* phydev를 사용한 이후에는 참조를 해제 */
    put_device(&phydev->mdio.dev);
    /* 이후 of_phy_connect()로 정식 연결 진행 */
    ...
}

phy-mode 선택과 흔한 실수

MAC 드라이버 초보자가 가장 많이 겪는 문제가 phy-mode 설정 오류입니다. 올바른 모드를 선택하지 않으면 링크는 올라와도 데이터가 깨지거나 속도가 제한될 수 있습니다.

phy-mode의미지연 보상 위치흔한 실수
rgmii지연 없음 (PCB에서 보상)PCB 트레이스 길이PHY도 MAC도 지연을 넣지 않아 CRC 폭증
rgmii-idPHY가 TX/RX 모두 지연 추가PHY 내부PCB에서 이미 보상했는데 이중 지연
rgmii-txidPHY가 TX만 지연 추가PHY TX + PCB RXTX/RX를 혼동하여 반대로 설정
rgmii-rxidPHY가 RX만 지연 추가PHY RX + PCB TXTX/RX를 혼동하여 반대로 설정
sgmiiSerDes 기반 1.25 Gbps해당 없음1000BASE-X와 혼동, control word 해석 차이
mii / rmii10/100M 전용PHY/MAC/PCB 조합ref_clk 방향(입력/출력) 미설정
RGMII 지연 디버깅 팁: 100M에서는 정상이고 1G에서만 CRC가 증가하면 거의 확실히 RGMII 지연 문제입니다. 보드 회로도에서 PHY와 MAC 사이의 트레이스 지연 보상 여부를 확인하고, 해당 조합에 맞는 phy-mode를 선택하세요. 확인이 안 되면 rgmii, rgmii-id, rgmii-txid, rgmii-rxid 네 가지를 순서대로 시도하면서 ethtool -S로 CRC 에러 카운터를 비교하는 것이 가장 빠른 방법입니다.

suspend/resume에서 PHY 재초기화

시스템 suspend/resume 시 PHY는 전원이 차단되거나 리셋될 수 있습니다. MAC 드라이버의 resume 경로에서 PHY가 정상 상태로 돌아왔는지 확인하고, 필요시 재초기화해야 합니다.

/* suspend/resume에서 PHY 처리 */
static int my_mac_suspend(struct device *dev)
{
    struct my_priv *priv = dev_get_drvdata(dev);

    if (netif_running(priv->ndev)) {
        netif_device_detach(priv->ndev);
        phy_stop(priv->ndev->phydev);
        /* phylink 사용 시: phylink_stop(priv->phylink); */
        my_mac_hw_fini(priv);
    }
    return 0;
}

static int my_mac_resume(struct device *dev)
{
    struct my_priv *priv = dev_get_drvdata(dev);

    if (netif_running(priv->ndev)) {
        my_mac_hw_init(priv);

        /* PHY가 resume 콜백에서 config_init을 재실행함
         * → strap, delay, LED 등 벤더 설정이 복원됨
         * MAC 드라이버는 phy_start()만 호출하면 됨 */
        phy_start(priv->ndev->phydev);
        /* phylink 사용 시: phylink_start(priv->phylink); */

        netif_device_attach(priv->ndev);
    }
    return 0;
}
resume 후 링크 품질 저하: PHY의 resume 콜백이 config_init을 다시 호출하지 않는 드라이버가 간혹 있습니다. 이 경우 RGMII delay bit, LED 모드, downshift 정책이 strap 기본값으로 복귀하여 부팅 직후와 다른 동작을 보입니다. PHY 드라이버의 resume 구현을 확인하고, 필요하면 MAC 드라이버에서 PHY soft reset 후 재연결을 시도하세요.

초보 NIC 드라이버 개발자의 흔한 실수 TOP 10

아래는 커널 메일링 리스트와 실무에서 반복적으로 보고되는 PHY 관련 실수를 정리한 것입니다. 새 드라이버를 작성하거나 기존 드라이버를 수정할 때 체크리스트로 활용하세요.

#실수증상올바른 처리
1adjust_link/mac_link_up을 빈 함수로 둠속도 재협상 후 CRC 폭증, "링크 업인데 통신 불가"speed, duplex, pause를 MAC 레지스터에 반드시 반영
2phy-mode 불일치1G에서만 CRC 에러, 100M에서는 정상보드 회로도 확인, PHY 데이터시트의 지연 보상 위치 맞추기
3MDIO bus 등록 전에 PHY 연결 시도probe 실패, NULL 포인터of_mdiobus_register()of_phy_connect() 순서 보장
4deferred probe 미처리부팅 순서에 따라 간헐적 PHY 연결 실패-EPROBE_DEFER 반환으로 재시도 허용
5phy_disconnect() 전에 MDIO bus 해제커널 oops, use-after-free반드시 PHY disconnect → MDIO unregister → MDIO free 순서
6ethtool PHY ops 미연결ethtool eth0에서 속도/듀플렉스 정보 없음phy_ethtool_* 또는 phylink_ethtool_* 헬퍼 연결
7phy_start() 호출 누락인터페이스 up 해도 링크 상태 머신 미가동, 항상 링크 다운ndo_open에서 phy_start() 반드시 호출
8resume에서 PHY 재초기화 누락suspend/resume 후 링크 불안정 또는 속도 강등resume 경로에서 phy_start() 재호출, PHY 드라이버 resume 콜백 확인
9phy_set_max_speed() 미호출MAC이 100M만 지원하는데 PHY가 1G 광고 → 상대편과 1G 협상 → 프레임 손실MAC 하드웨어 능력에 맞게 최대 속도 제한
10fixed-link인데 phy-handle 기술존재하지 않는 PHY를 탐색하느라 probe 지연 또는 실패PHY 없는 직결 구성은 fixed-link만 사용, 둘을 동시에 기술하지 않기

첫 보드 Bring-up 시 최소 동작 검증 절차

새 MAC 드라이버를 작성한 후, 또는 새 보드에 기존 드라이버를 포팅한 후 PHY 동작을 확인하는 최소 절차입니다.

  1. PHY ID 확인dmesg | grep -i phy로 PHY가 정상 탐지되었는지 확인합니다. ID가 0xffff 또는 0x0000이면 MDIO 버스 자체의 문제입니다.
    # 정상 출력 예시
    my-mac-mdio: probed
    my-mac 1c30000.ethernet eth0: PHY [my-mac-mdio:01] driver [RTL8211F] (irq=POLL)
    my-mac 1c30000.ethernet eth0: attached PHY driver (mii_bus:phy_addr=my-mac-mdio:01, irq=POLL)
  2. 링크 업 확인 — 케이블 연결 후 ip link show eth0에서 LOWER_UP 표시와 carrier 상태를 확인합니다.
    ip link show eth0
    # 정상: <BROADCAST,MULTICAST,UP,LOWER_UP>
    # 비정상: <BROADCAST,MULTICAST,UP> (LOWER_UP 없음 = 링크 없음)
    
    cat /sys/class/net/eth0/carrier
    # 1: 링크 업, 0: 링크 다운
  3. 속도/듀플렉스 확인ethtool eth0으로 협상된 속도와 듀플렉스가 기대값인지 확인합니다.
    ethtool eth0
    # Speed: 1000Mb/s, Duplex: Full, Auto-negotiation: on
  4. ping 테스트 — 작은 패킷과 큰 패킷 모두 테스트합니다.
    ping -c 10 192.168.1.1
    ping -c 10 -s 1472 192.168.1.1  # MTU 근처 크기
  5. 에러 카운터 확인ethtool -S eth0으로 CRC, carrier, frame 에러가 증가하지 않는지 확인합니다.
    ethtool -S eth0 | grep -iE 'err|crc|drop|miss|carrier'
    # 모든 에러 카운터가 0 또는 증가하지 않아야 정상
  6. 속도 강제 변경 테스트 — 100M 강제 설정 후 복귀하여 adjust_link/mac_link_up 콜백이 올바르게 동작하는지 확인합니다.
    ethtool -s eth0 speed 100 duplex full autoneg off
    ping -c 5 192.168.1.1
    ethtool -S eth0 | grep -iE 'err|crc'
    ethtool -s eth0 autoneg on # 원복
  7. 링크 플랩 테스트 — 케이블 분리/재연결을 3~5회 반복하면서 dmesg에 링크 up/down 로그가 정상 출력되는지, 복구 후 통신이 되는지 확인합니다.
  8. 대역폭 측정iperf3로 실제 throughput이 링크 속도의 90% 이상인지 확인합니다. 현저히 낮으면 DMA, NAPI, 또는 PHY 설정 문제일 수 있습니다.
    # 서버 측
    iperf3 -s
    
    # 클라이언트 측
    iperf3 -c 192.168.1.1 -t 30
    # 1G 링크에서 ~940 Mbps 이상이면 정상
디버깅 순서 요약: PHY ID 확인 → 링크 업 여부 → 속도/듀플렉스 → CRC 카운터 → ping → iperf 순서로 점검하세요. 각 단계에서 문제가 발견되면 이후 단계를 건너뛰고 해당 계층을 먼저 해결하는 것이 효율적입니다. PHY ID가 안 읽히면 드라이버를 아무리 바꿔도 소용없고, CRC가 증가하면 iperf 결과도 의미가 없습니다.

참고자료

다음 학습: