이더넷 PHY (Physical Layer)
이더넷 PHY는 MAC이 만든 프레임을 실제 전기·광 신호로 바꾸고, 반대로 선로에서 들어온 신호를 디지털 비트스트림으로 복원하는 계층입니다. 이 문서는 PHY의 역할과 계층 경계, MDIO 레지스터(Register) 접근, Clause 22/45와 MMD, phylib/phylink 수명주기, 분리형 PCS와 phylink_pcs, SerDes/SFP, EEE·downshift·EDPD·FEC 같은 기능 튜닝, Device Tree와 보드 bring-up, 신호 무결성(Integrity), 링크 장애 디버깅(Debugging) 절차를 커널 드라이버와 하드웨어 설계 관점에서 최대한 상세히 정리합니다.
핵심 요약
- PHY — MAC과 실제 매체 사이에서 부호화, 클럭 복구, 신호 송수신, 링크 감지를 담당하는 계층입니다.
- MDIO — MAC이나 SoC가 PHY 레지스터를 읽고 쓰는 관리 버스(Bus)입니다. Clause 22와 Clause 45 두 주소 모델이 있습니다.
- phylib — 커널의 공통 PHY 프레임워크로,
phy_device와phy_driver를 통해 링크 상태 머신을 운영합니다. - phylink — MAC, PHY, PCS, SFP, fixed-link를 하나의 정책 계층으로 묶어 주는 상위 프레임워크입니다.
- phy-mode — RGMII, SGMII, 1000BASE-X 같은 MAC-PHY 인터페이스 모드를 뜻하며, 이 설정이 틀리면 링크는 올라와도 CRC 오류와 속도 강등이 생길 수 있습니다.
단계별 이해
- PHY가 어디까지 담당하는지 구분하기
MAC은 프레임을 만들고, PHY는 그 프레임을 선로에 맞는 신호로 바꿉니다. PCS/PMA/PMD 경계를 먼저 잡아야 로그와 보드 문제를 분리할 수 있습니다. - MDIO로 상태를 읽는 법 익히기
링크 업 여부, PHY ID, 광고 능력, RGMII 지연 같은 핵심 정보는 대부분 MDIO 레지스터에서 확인합니다. PHY 디버깅의 기본은 레지스터 관찰입니다. - 커널 수명주기 이해하기
probe에서 attach하고, open에서phy_start()혹은phylink_start()를 호출하며, 링크 변화 시 MAC 재설정 콜백(Callback)이 들어오는 흐름을 따라가야 합니다. - 보드와 링크 장애를 함께 점검하기
phy-mode, reset GPIO, 클럭, 전원, 케이블, 상대편 광고 능력을 같이 봐야 합니다. PHY 문제는 드라이버 버그와 보드 타이밍 문제가 자주 섞여 나타납니다.
개요
이더넷 PHY의 핵심 임무는 세 가지입니다. 첫째, MAC이 제공한 병렬 또는 직렬 디지털 데이터를 선로 규격에 맞는 전기·광 신호로 변환합니다. 둘째, 상대편 장치와 링크 속도, 듀플렉스, pause, FEC 같은 능력을 협상합니다. 셋째, 온도·전압·신호 품질 변화에 따라 CDR(Clock and Data Recovery), equalization, amplitude 설정을 조정해 실제 배선 위에서 동작하도록 만듭니다.
즉 PHY는 단순한 "포트"가 아니라, 아날로그 회로와 디지털 상태 머신이 결합된 독립 장치입니다. 링크가 안 올라오거나 CRC가 증가할 때는 소프트웨어만 볼 일이 아니라, PHY 내부 지연 보상, reference clock, magnetics, 케이블 품질, 상대편 포트의 협상 정책까지 같이 봐야 합니다.
Link detected: yes가 찍힌다고 PHY 문제가 끝난 것이 아닙니다.
링크 업 이후에도 CRC 증가, carrier error, EEE 복귀 지연, 잘못된 speed/duplex 합의, RGMII skew 오류가 남을 수 있으므로 "업 여부"와 "링크 품질"을 반드시 분리해서 봐야 합니다.
MAC과 PHY의 경계
문제를 빨리 좁히려면 "어디까지가 MAC이고 어디부터 PHY인가"를 먼저 구분해야 합니다. MAC은 프레임 헤더, 주소 필터, pause 처리, DMA와 큐를 담당하고, PHY는 부호화/직렬화(Serialization)/클럭 복구/매체 송수신을 맡습니다. 1G 이하 임베디드 보드에서는 MAC과 외부 PHY가 RGMII로 연결되는 경우가 많고, 서버 NIC에서는 MAC/PCS가 SoC나 NIC 내부에 있고 외부 모듈은 PMD 역할만 하는 경우도 흔합니다.
| 구성 요소 | 주요 책임 | 대표 증상 | 점검 포인트 |
|---|---|---|---|
| MAC | 프레임 조립, 주소 필터, DMA, pause, 통계 | queue hang, TX timeout, 잘못된 offload | net_device, DMA 링, IRQ, NAPI |
| MAC-PHY 인터페이스 | RGMII/SGMII 등 디지털 링크 | 속도 강등, CRC, 불안정한 링크 | phy-mode, skew, clock, in-band status |
| PHY PCS/PMA | 부호화, AN, 직렬화, CDR | 링크 미협상, symbol error | MDIO 레지스터, 광고 능력, 벤더 확장 레지스터 |
| PMD/매체 | 전기·광 신호, magnetics, 커넥터 | 거리 민감, 케이블 교체 시 개선 | 케이블, 모듈, 커넥터, 온도, 전원 품질 |
- MDIO는 제어면입니다. 데이터 패킷(Packet)은 RGMII/SGMII 같은 MAC-PHY 인터페이스를 지나고, 설정과 상태 조회만 MDIO를 통합니다.
- PCS와 PMA는 종종 MAC 안에도 존재합니다. 특히 10G 이상 SerDes 기반 장비에서는 PHY가 외부 칩이 아니라 모듈이나 PMD 수준으로 축소될 수 있습니다.
- 링크 품질 문제는 경계에 걸쳐 나타납니다. 잘못된
phy-mode는 MAC 로그로 보이고, 원인은 PHY 내부 지연 혹은 PCB 스큐일 수 있습니다.
PHY 내부 하위 계층
이더넷 PHY를 깊게 이해하려면 PHY를 단일 칩으로 보지 말고 PCS, PMA, PMD, MDI로 나눠 생각해야 합니다. 1000BASE-T 같은 구리 PHY는 PCS에서 부호화와 auto-negotiation control word 처리를 하고, PMA/PMD에서 echo cancellation, NEXT/FEXT 제거, adaptive equalization, timing recovery 같은 DSP 처리를 수행합니다. 반면 광 링크는 PMD가 레이저 드라이버, CDR, 광 수신기와 더 강하게 결합됩니다.
즉 "링크가 안 붙는다"는 하나의 증상도 PCS 문제인지, PMA의 CDR lock 실패인지, PMD의 광 세기 부족인지, MDI 쪽 배선 문제인지에 따라 전혀 다른 디버깅 절차가 필요합니다. 초기에 이 구분을 해 두면 드라이버와 보드 설계 문제를 훨씬 빠르게 분리할 수 있습니다.
| 하위 계층 | 주요 역할 | 구리 PHY 구현 예 | 광/SerDes 구현 예 | 문제 징후 |
|---|---|---|---|---|
| PCS | 부호화, 스크램블링, AN control word 처리 | 1000BASE-T 4D-PAM5 심볼 처리 준비 | 8B/10B, 64B/66B, block lock | 협상 실패, block lock 불가 |
| PMA | 직렬화, 클럭 복구, lane 정렬 | ADC/DAC와 DSP 사이 timing recovery | SerDes, CDR, lane deskew | 링크 flap, symbol error |
| PMD | 실제 매체 송수신 | magnetics, hybrid, echo cancellation | laser/TIA, 광 세기, DAC/AOC | 거리 민감, 온도 민감, CRC 증가 |
| MDI | 커넥터와 매체 접점 | RJ45, 트위스트 페어 | LC 커넥터, backplane, DAC | 케이블 교체 시 증상 변화 |
MDIO와 표준 레지스터
MDIO(Management Data Input/Output)는 PHY를 설정하고 상태를 읽는 가장 기본적인 관리 경로입니다. 10/100/1000 PHY에서는 Clause 22가, 고속 PHY와 복잡한 서브블록을 가진 장치에서는 Clause 45가 많이 쓰입니다. Clause 22는 5비트 PHY 주소와 5비트 레지스터 번호만 다루지만, Clause 45는 MMD(devad) 개념을 도입해 PMA/PCS/AN 등 기능 블록별 레지스터 공간을 분리합니다.
MDIO 물리 인터페이스
MDIO 버스는 두 개의 신호선으로 구성됩니다. MDC(Management Data Clock)는 MAC(또는 MDIO 컨트롤러)이 생성하는 단방향 클럭이고, MDIO(Management Data I/O)는 양방향 데이터 선입니다. 전기적으로 MDIO 선은 open-drain 방식이며 외부 pull-up 저항(1.5 kΩ ~ 10 kΩ)이 필요합니다. MDC의 최대 주파수는 IEEE 802.3에서 2.5 MHz로 규정하지만, 많은 PHY가 12.5 MHz 이상도 허용합니다. 보드 설계 시 pull-up 저항 값과 배선 길이가 잘못되면 특정 PHY만 간헐적으로 읽히지 않는 문제가 발생합니다.
| 신호 | 방향 | 특성 | 설계 포인트 |
|---|---|---|---|
| MDC | MAC → PHY (단방향) | 클럭, 최대 2.5 MHz (IEEE 표준) | rise/fall time 10 ns 이내 권장, 다중 PHY 시 fan-out 고려 |
| MDIO | 양방향 (open-drain) | 데이터, 클럭 상승 엣지에 샘플링 | pull-up 1.5 kΩ~10 kΩ, 배선 길이 짧게 유지 |
MDIO 프레임 포맷
하나의 MDIO 트랜잭션은 직렬(serial)로 전송되는 프레임으로 구성됩니다. Clause 22 프레임은 총 64비트(프리앰블(Preamble) 32비트 + 프레임 32비트)이며, Clause 45는 주소 프레임과 데이터 프레임을 두 번에 나눠 보내 16비트 레지스터 주소 공간을 확보합니다.
리눅스 커널 MDIO API
리눅스 커널에서 MDIO 버스는 struct mii_bus로 추상화됩니다. MAC 드라이버나 전용 MDIO 컨트롤러 드라이버가 이 구조체를 등록하고, read와 write 콜백을 통해 실제 하드웨어 접근을 수행합니다. PHY 드라이버는 phy_read() / phy_write()를 통해 간접적으로 이 버스를 사용하며, Clause 45 접근은 phy_read_mmd() / phy_write_mmd()로 분리되어 있습니다.
/* MDIO 버스 등록 — MAC 또는 MDIO 컨트롤러 드라이버 */
struct mii_bus *bus = mdiobus_alloc();
bus->name = "my-mdio";
bus->read = my_mdio_read; /* Clause 22 read 콜백 */
bus->write = my_mdio_write; /* Clause 22 write 콜백 */
bus->read_c45 = my_mdio_c45_read; /* Clause 45 read 콜백 (v5.18+) */
bus->write_c45 = my_mdio_c45_write; /* Clause 45 write 콜백 (v5.18+) */
bus->parent = &pdev->dev;
bus->phy_mask = 0; /* 0이면 모든 주소 스캔, 비트마스크로 특정 주소 제외 */
mdiobus_register(bus);
/* PHY 드라이버에서 레지스터 접근 */
int val = phy_read(phydev, MII_BMSR); /* Clause 22 */
int pcs = phy_read_mmd(phydev, MDIO_MMD_PCS, 0x0001); /* Clause 45 */
phy_write_mmd(phydev, MDIO_MMD_VEND1, 0x8000, 0x0001); /* Clause 45 쓰기 */
/* Clause 22만 지원하는 PHY에서 간접 Clause 45 접근 (MII_MMD_CTRL/DATA) */
phy_write(phydev, MII_MMD_CTRL, devad);
phy_write(phydev, MII_MMD_DATA, reg);
phy_write(phydev, MII_MMD_CTRL, devad | MII_MMD_CTRL_NOINCR);
val = phy_read(phydev, MII_MMD_DATA);
read_c45/write_c45 콜백이 없었고, Clause 45 접근도 read/write 콜백에 특수 인코딩(MII_ADDR_C45 플래그)을 섞어 전달했습니다.
레거시 드라이버를 다룰 때는 이 차이를 인지해야 합니다.
mii_bus 수명주기와 등록 API
struct mii_bus는 MDIO 컨트롤러를 커널에 등록하는 핵심 구조체입니다. 할당부터 해제까지의 전체 수명주기와 각 필드의 역할을 이해해야 PHY 탐색, 주소 마스킹, reset 타이밍 같은 실무 문제를 올바르게 처리할 수 있습니다.
/* mii_bus 주요 필드 (include/linux/mdio.h, include/linux/phy.h) */
struct mii_bus {
const char *name; /* 버스 이름 (sysfs에 표시) */
char id[MII_BUS_ID_SIZE]; /* 고유 ID (Device Tree에서 자동 생성) */
struct device *parent; /* 부모 디바이스 (MAC 또는 MDIO 컨트롤러) */
void *priv; /* 드라이버 전용 데이터 */
/* Clause 22 콜백: 반드시 구현해야 합니다 */
int (*read)(struct mii_bus *bus, int addr, int regnum);
int (*write)(struct mii_bus *bus, int addr, int regnum, u16 val);
/* Clause 45 콜백 (v5.18+): NULL이면 Clause 45 미지원 */
int (*read_c45)(struct mii_bus *bus, int addr, int devad, int regnum);
int (*write_c45)(struct mii_bus *bus, int addr, int devad, int regnum, u16 val);
/* PHY reset 콜백: 버스 등록 시 한 번 호출됩니다 */
int (*reset)(struct mii_bus *bus);
u32 phy_mask; /* 비트가 1인 주소는 스캔에서 제외됩니다 */
u32 phy_ignore; /* (v6.1+) phy_mask와 유사, 별도 용도 */
struct mdio_device *mdio_map[PHY_MAX_ADDR]; /* 주소별 장치 맵 */
struct mutex mdio_lock; /* 버스 접근 직렬화 */
};
/* 완전한 mii_bus 등록/해제 수명주기 */
static int my_mdio_read(struct mii_bus *bus, int addr, int regnum)
{
struct my_priv *priv = bus->priv;
u32 cmd;
/* MDIO 컨트롤러 레지스터에 읽기 명령 구성 */
cmd = MDIO_CMD_READ | (addr << 5) | regnum;
writel(cmd, priv->regs + MDIO_CMD_REG);
/* 완료 대기 — 보통 수 us, 최대 수백 us */
if (readl_poll_timeout(priv->regs + MDIO_STATUS_REG,
cmd, cmd & MDIO_DONE, 10, 10000))
return -ETIMEDOUT;
return readl(priv->regs + MDIO_DATA_REG) & 0xffff;
}
static int my_mdio_write(struct mii_bus *bus, int addr, int regnum, u16 val)
{
struct my_priv *priv = bus->priv;
u32 cmd;
cmd = MDIO_CMD_WRITE | (addr << 5) | regnum;
writel(val, priv->regs + MDIO_DATA_REG);
writel(cmd, priv->regs + MDIO_CMD_REG);
return readl_poll_timeout(priv->regs + MDIO_STATUS_REG,
cmd, cmd & MDIO_DONE, 10, 10000)
? -ETIMEDOUT : 0;
}
static int my_probe(struct platform_device *pdev)
{
struct mii_bus *bus;
struct my_priv *priv;
/* mdiobus_alloc_size(): priv 크기를 인자로 받아 한 번에 할당합니다 */
bus = mdiobus_alloc_size(sizeof(*priv));
if (!bus)
return -ENOMEM;
priv = bus->priv; /* mdiobus_alloc_size가 priv 포인터를 설정합니다 */
priv->regs = devm_ioremap_resource(&pdev->dev, ...);
bus->name = "my-soc-mdio";
bus->parent = &pdev->dev;
bus->read = my_mdio_read;
bus->write = my_mdio_write;
bus->phy_mask = 0; /* 모든 주소 스캔 */
/* Device Tree 기반이면 of_mdiobus_register()로 DT 노드에서 PHY를 탐색합니다 */
snprintf(bus->id, MII_BUS_ID_SIZE, "%s", pdev_name(pdev));
return of_mdiobus_register(bus, pdev->dev.of_node);
}
static void my_remove(struct platform_device *pdev)
{
struct mii_bus *bus = platform_get_drvdata(pdev);
mdiobus_unregister(bus); /* PHY 장치들을 모두 해제합니다 */
mdiobus_free(bus); /* mii_bus 메모리를 해제합니다 */
}
| 함수 | 역할 | 사용 시점 |
|---|---|---|
mdiobus_alloc() | mii_bus 구조체 할당 (priv 없음) | priv가 불필요할 때 |
mdiobus_alloc_size(sz) | mii_bus + sz 바이트 priv 할당 | 드라이버별 데이터가 필요할 때 |
mdiobus_register(bus) | 버스 등록 + 전체 주소 스캔 | 비 DT 환경에서 사용 |
of_mdiobus_register(bus, np) | DT 기반 등록 (노드에서 PHY 정보 파싱) | Device Tree 기반 시스템 |
devm_mdiobus_register(dev, bus) | devres 관리 등록 (remove에서 자동 해제) | 드라이버 remove 단순화 |
mdiobus_unregister(bus) | 등록 해제, 연결된 PHY 제거 | 드라이버 remove 시 |
mdiobus_free(bus) | 메모리 해제 | unregister 후 호출 |
devm_mdiobus_register()를 사용하면 remove 콜백에서 mdiobus_unregister()를 직접 호출하지 않아도 됩니다.
다만 mdiobus_alloc()은 devres가 아니므로, devm_mdiobus_alloc_size()(v6.3+)와 함께 쓰는 것이 가장 깔끔합니다.
MDIO 버스 락킹과 직접 접근 API
MDIO 버스는 mdio_lock 뮤텍스로 보호됩니다. PHY 드라이버가 사용하는 phy_read()/phy_write()는 내부에서 이 락을 잡고 풀지만, 여러 레지스터를 원자적으로 읽거나 써야 하는 경우에는 락을 직접 관리해야 합니다.
/* 버스 레벨 접근 — 락 자동 관리 (일반적인 경우) */
int val = mdiobus_read(bus, phy_addr, regnum); /* 락 잡기 → read → 풀기 */
mdiobus_write(bus, phy_addr, regnum, val); /* 락 잡기 → write → 풀기 */
/* 버스 레벨 접근 — 락 없음 (호출자가 이미 락을 잡은 상태) */
int val = __mdiobus_read(bus, phy_addr, regnum);
__mdiobus_write(bus, phy_addr, regnum, val);
/* PHY 레벨 접근 — 락 자동 관리 */
int val = phy_read(phydev, regnum); /* 내부에서 mdio_lock 획득 */
phy_write(phydev, regnum, val);
/* 여러 레지스터를 원자적으로 접근해야 할 때 — 직접 락 관리 */
mutex_lock(&phydev->mdio.bus->mdio_lock);
/* 락 보유 중에는 __mdiobus_read/write 사용 */
int page = __mdiobus_read(phydev->mdio.bus, phydev->mdio.addr, 0x1f);
__mdiobus_write(phydev->mdio.bus, phydev->mdio.addr, 0x1f, 0x0a43);
val = __mdiobus_read(phydev->mdio.bus, phydev->mdio.addr, 0x19);
__mdiobus_write(phydev->mdio.bus, phydev->mdio.addr, 0x1f, page); /* 원래 페이지 복원 */
mutex_unlock(&phydev->mdio.bus->mdio_lock);
phy_read()와 __mdiobus_read()를 혼용하면 데드락이 발생합니다.
phy_read()는 내부에서 mdio_lock을 잡으므로, 이미 락을 잡은 상태에서 호출하면 같은 뮤텍스를 두 번 잡게 됩니다.
락 보유 중에는 반드시 __mdiobus_read()/__mdiobus_write()를 사용하세요.
레지스터 수정 헬퍼 API
PHY 레지스터의 특정 비트만 변경하는 작업은 매우 빈번합니다. 매번 read-modify-write를 직접 구현하면 코드가 반복되고 실수가 생기므로, 커널은 phy_modify() 계열의 헬퍼를 제공합니다. 이 함수들은 내부에서 락을 잡고, 읽기-수정-쓰기를 원자적으로 수행합니다.
/* phy_modify(phydev, regnum, mask, set)
* 동작: val = phy_read(reg); val = (val & ~mask) | set; phy_write(reg, val);
* 반환값: 변경 전 원래 값 (음수면 에러) */
/* 예: BMCR에서 AN enable 비트를 끄고 강제 100M full duplex 설정 */
phy_modify(phydev, MII_BMCR,
BMCR_ANENABLE | BMCR_SPEED100 | BMCR_FULLDPLX, /* 클리어할 비트 마스크 */
BMCR_SPEED100 | BMCR_FULLDPLX); /* 세팅할 비트 */
/* phy_set_bits(): 특정 비트만 세팅 (mask = set = bits) */
phy_set_bits(phydev, MII_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
/* phy_clear_bits(): 특정 비트만 클리어 (mask = bits, set = 0) */
phy_clear_bits(phydev, MII_BMCR, BMCR_PDOWN);
/* Clause 45 MMD 버전도 동일 패턴입니다 */
phy_modify_mmd(phydev, MDIO_MMD_PCS, 0x0001, mask, set);
phy_set_bits_mmd(phydev, MDIO_MMD_VEND1, 0x8000, BIT(3));
phy_clear_bits_mmd(phydev, MDIO_MMD_AN, 0x0010, BIT(0));
| 함수 | 동작 | 용도 |
|---|---|---|
phy_modify(dev, reg, mask, set) | (read & ~mask) | set → write | 복수 비트를 한 번에 변경 |
phy_set_bits(dev, reg, bits) | read | bits → write | 특정 비트 켜기 |
phy_clear_bits(dev, reg, bits) | read & ~bits → write | 특정 비트 끄기 |
phy_modify_mmd(dev, devad, reg, mask, set) | Clause 45 read-modify-write | MMD 레지스터 수정 |
phy_set_bits_mmd(dev, devad, reg, bits) | Clause 45 비트 세팅 | MMD 비트 켜기 |
phy_clear_bits_mmd(dev, devad, reg, bits) | Clause 45 비트 클리어 | MMD 비트 끄기 |
phy_modify()는 반환값이 변경 전 원래 레지스터 값입니다.
이를 활용하면 "이전 상태를 확인하면서 동시에 변경"하는 패턴을 한 번의 호출로 처리할 수 있습니다.
에러 시에는 음수가 반환되므로 반드시 ret < 0 검사를 해야 합니다.
벤더 페이지 전환 API
Clause 22만 지원하는 PHY에서도 벤더 확장 레지스터에 접근해야 하는 경우가 흔합니다. 대부분의 벤더 PHY(Realtek, Marvell, Microchip 등)는 "페이지 전환 레지스터"(보통 reg 0x1f 또는 reg 0x16)를 통해 레지스터 공간을 확장합니다. 커널은 이를 안전하게 처리하기 위해 phy_read_paged() 계열 API를 제공합니다.
/* PHY 드라이버가 페이지 전환 방식을 선언합니다 (probe 시) */
static int my_phy_read_page(struct phy_device *phydev)
{
return __phy_read(phydev, 0x1f); /* 현재 페이지 번호 반환 */
}
static int my_phy_write_page(struct phy_device *phydev, int page)
{
return __phy_write(phydev, 0x1f, page); /* 페이지 전환 */
}
/* phy_driver 구조체에서 콜백 등록 */
static struct phy_driver my_phy_drivers[] = {{
.phy_id = 0x001cc840,
.phy_id_mask = 0xffffff00,
.name = "My Vendor PHY",
.read_page = my_phy_read_page,
.write_page = my_phy_write_page,
/* ... 기타 콜백 ... */
}};
/* 페이지 전환 API 사용 — 자동으로 페이지 전환 → 접근 → 원래 페이지 복원 */
int val = phy_read_paged(phydev, 0x0a43, 0x19); /* 페이지 0x0a43, 레지스터 0x19 읽기 */
phy_write_paged(phydev, 0x0d08, 0x15, 0x1234); /* 페이지 0x0d08, 레지스터 0x15 쓰기 */
phy_modify_paged(phydev, 0x0a46, 0x14, BIT(5), 0); /* 페이지 0x0a46에서 bit 5 클리어 */
/* 여러 레지스터를 같은 페이지에서 접근할 때 — 수동 페이지 선택 */
int old_page = phy_select_page(phydev, 0x0a43); /* 락 획득 + 페이지 전환 */
if (old_page >= 0) {
val = __phy_read(phydev, 0x19); /* 락 보유 중이므로 __ 접두사 사용 */
__phy_write(phydev, 0x1a, 0x00ff);
}
phy_restore_page(phydev, old_page, val); /* 원래 페이지 복원 + 락 해제 */
| 함수 | 동작 | 락 관리 |
|---|---|---|
phy_read_paged(dev, page, reg) | 페이지 전환 → read → 복원 | 내부에서 자동 |
phy_write_paged(dev, page, reg, val) | 페이지 전환 → write → 복원 | 내부에서 자동 |
phy_modify_paged(dev, page, reg, mask, set) | 페이지 전환 → modify → 복원 | 내부에서 자동 |
phy_select_page(dev, page) | 락 획득 + 페이지 전환 | 호출자가 restore 필수 |
phy_restore_page(dev, old_page, ret) | 페이지 복원 + 락 해제 | select_page와 쌍 |
phy_select_page()와 phy_restore_page()는 반드시 쌍으로 사용해야 합니다.
phy_select_page()는 mdio_lock을 잡은 채로 반환하므로, 반드시 phy_restore_page()로 락을 풀어야 합니다.
이 구간에서 phy_read()(락을 잡는 함수)를 호출하면 데드락이 발생합니다 — __phy_read()를 사용하세요.
MDIO 디바이스 모델과 mdio_driver
모든 PHY(phy_device)는 mdio_device를 내장하고 있으며, mdio_device는 MDIO 버스 위의 일반 장치입니다. PHY가 아닌 MDIO 장치(예: 이더넷 스위치, SFP 모듈 관리 칩, retimer)를 다룰 때는 mdio_driver를 사용합니다.
/* mdio_device: MDIO 버스 위 모든 장치의 기본 구조체 */
struct mdio_device {
struct device dev;
struct mii_bus *bus; /* 소속 MDIO 버스 */
int addr; /* MDIO 주소 (0..31) */
int flags; /* MDIO_DEVICE_FLAG_* */
struct gpio_desc *reset_gpio; /* reset GPIO (DT에서 파싱) */
unsigned int reset_assert_delay; /* reset assert 유지 시간 (us) */
unsigned int reset_deassert_delay; /* reset deassert 후 대기 시간 (us) */
};
/* phy_device는 mdio_device를 내장합니다 */
struct phy_device {
struct mdio_device mdio; /* 첫 번째 멤버 — 구조체 캐스팅 가능 */
struct phy_driver *drv;
u32 phy_id;
int speed, duplex;
int link;
/* ... 수십 개의 상태 필드 ... */
};
/* PHY가 아닌 MDIO 장치를 위한 드라이버 등록 예시 */
static int my_switch_probe(struct mdio_device *mdiodev)
{
/* mdiodev->bus, mdiodev->addr를 사용해 레지스터 접근 */
int id = mdiobus_read(mdiodev->bus, mdiodev->addr, 0x00);
dev_info(&mdiodev->dev, "switch ID: 0x%04x\n", id);
return 0;
}
static const struct of_device_id my_switch_of_match[] = {
{ .compatible = "my,switch-chip" },
{ }
};
static struct mdio_driver my_switch_driver = {
.mdiodrv.driver = {
.name = "my-switch",
.of_match_table = my_switch_of_match,
},
.probe = my_switch_probe,
};
mdio_module_driver(my_switch_driver);
MDIO API 실전 패턴
PHY 드라이버와 MAC 드라이버에서 가장 자주 사용되는 MDIO 접근 패턴들을 정리합니다. 각 패턴은 실제 커널 드라이버에서 반복적으로 나타나는 코드를 단순화한 것입니다.
/* ━━━━━━ 패턴 1: PHY ID 읽기와 검증 ━━━━━━ */
static u32 read_phy_id(struct phy_device *phydev)
{
int id1 = phy_read(phydev, MII_PHYSID1); /* reg 0x02 */
int id2 = phy_read(phydev, MII_PHYSID2); /* reg 0x03 */
if (id1 < 0 || id2 < 0)
return 0xffffffff; /* 읽기 실패 — 버스 미연결 가능 */
return (id1 << 16) | id2; /* 예: RTL8211F = 0x001cc916 */
}
/* ━━━━━━ 패턴 2: 소프트 리셋과 완료 대기 ━━━━━━ */
static int my_phy_soft_reset(struct phy_device *phydev)
{
int ret;
phy_set_bits(phydev, MII_BMCR, BMCR_RESET);
/* BMCR_RESET 비트가 자동으로 0이 될 때까지 대기 (최대 500ms) */
ret = phy_read_poll_timeout(phydev, MII_BMCR, ret,
!(ret & BMCR_RESET),
1000, 500000, false);
if (ret)
phydev_err(phydev, "soft reset timeout\n");
return ret;
}
/* ━━━━━━ 패턴 3: 링크 상태 확인 (래치 비트 고려) ━━━━━━ */
static bool check_link_status(struct phy_device *phydev)
{
int bmsr;
/* 첫 번째 읽기: 래치된 값을 소모합니다 */
bmsr = phy_read(phydev, MII_BMSR);
if (bmsr < 0)
return false;
/* 두 번째 읽기: 현재 링크 상태가 반영됩니다 */
bmsr = phy_read(phydev, MII_BMSR);
return bmsr >= 0 && (bmsr & BMSR_LSTATUS);
}
/* ━━━━━━ 패턴 4: AN 재시작과 완료 대기 ━━━━━━ */
static int restart_and_wait_an(struct phy_device *phydev)
{
int ret;
/* AN enable + restart AN 비트를 동시에 세팅합니다 */
phy_set_bits(phydev, MII_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
/* AN complete 비트 대기 — 케이블 길이와 상대편 PHY에 따라 수 초 */
ret = phy_read_poll_timeout(phydev, MII_BMSR, ret,
ret & BMSR_ANEGCOMPLETE,
50000, 5000000, false);
return ret;
}
/* ━━━━━━ 패턴 5: loopback 설정 (PHY 내부 루프백) ━━━━━━ */
static int set_phy_loopback(struct phy_device *phydev, bool enable)
{
if (enable)
return phy_set_bits(phydev, MII_BMCR, BMCR_LOOPBACK);
else
return phy_clear_bits(phydev, MII_BMCR, BMCR_LOOPBACK);
}
/* ━━━━━━ 패턴 6: 벤더 확장 레지스터로 RGMII 지연 설정 ━━━━━━ */
/* Realtek RTL8211F 기준 예시 */
static int rtl8211f_set_rgmii_delay(struct phy_device *phydev, bool tx, bool rx)
{
u16 val = 0;
if (tx) val |= BIT(1); /* TX delay enable */
if (rx) val |= BIT(0); /* RX delay enable */
return phy_modify_paged(phydev, 0x0d08, 0x11,
BIT(1) | BIT(0), val);
}
/* ━━━━━━ 패턴 7: Clause 45 EEE 광고 능력 설정 ━━━━━━ */
static int set_eee_advertisement(struct phy_device *phydev, bool enable_1g)
{
u16 adv = 0;
if (enable_1g)
adv = MDIO_EEE_1000T;
/* MMD 7 (AN), 레지스터 0x003c: EEE Advertisement */
return phy_write_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV, adv);
}
phy_read() 등 상위 API를 사용하고, 이 API들은 내부에서 mdio_lock을 거쳐 하드웨어 콜백에 도달합니다.MDIO 에러 처리와 디버깅 패턴
MDIO 접근은 실패할 수 있습니다. 버스 타임아웃, PHY 미응답(0xffff), reset 미완료 등이 흔한 원인입니다. 커널 MDIO API는 에러 시 음수를 반환하고, 일부 PHY는 미연결 주소에서 0xffff를 돌려줍니다. 올바른 에러 처리 패턴을 따르지 않으면 잘못된 레지스터 값으로 PHY를 설정해 버리는 사고가 생깁니다.
/* 올바른 에러 처리 패턴 */
static int my_phy_config(struct phy_device *phydev)
{
int ret;
/* phy_read()는 에러 시 음수, 성공 시 0..0xffff */
ret = phy_read(phydev, MII_BMSR);
if (ret < 0) {
phydev_err(phydev, "BMSR read failed: %d\n", ret);
return ret;
}
/* 0xffff는 PHY 미연결 또는 버스 문제를 의미할 수 있습니다 */
if (ret == 0xffff) {
phydev_warn(phydev, "BMSR all-ones — PHY not responding\n");
return -ENODEV;
}
/* phy_modify()는 에러 시 음수, 성공 시 변경 전 원래 값 */
ret = phy_modify(phydev, MII_BMCR, BMCR_PDOWN, 0);
if (ret < 0)
return ret;
/* phy_write()는 에러 시 음수, 성공 시 0 */
ret = phy_write(phydev, MII_ADVERTISE, ADVERTISE_ALL | ADVERTISE_CSMA);
return ret;
}
/* 디버깅 시 레지스터 덤프 패턴 */
static void dump_phy_regs(struct phy_device *phydev)
{
int i, val;
phydev_info(phydev, "=== PHY register dump (Clause 22) ===\n");
for (i = 0; i < 32; i++) {
val = phy_read(phydev, i);
if (val < 0)
phydev_info(phydev, " reg[0x%02x] = ERROR (%d)\n", i, val);
else
phydev_info(phydev, " reg[0x%02x] = 0x%04x\n", i, val);
}
}
| 증상 | 가능한 원인 | 확인 방법 |
|---|---|---|
| 모든 레지스터가 0xffff | PHY 전원 미인가, reset 미해제, 주소 불일치 | 전원/reset GPIO 확인, DT reg 속성과 PHY 스트랩 핀 대조 |
| 모든 레지스터가 0x0000 | MDIO 데이터 라인 단선(pull-down 상태) | 오실로스코프로 MDIO 파형 확인 |
| 읽기 타임아웃 (-ETIMEDOUT) | MDC 클럭 미공급, 컨트롤러 클럭 게이팅 | SoC 클럭 트리 확인, MDC 핀 오실로스코프 측정 |
| 특정 주소만 응답 없음 | PHY 스트랩 핀 설정 오류, mux 선택 누락 | 다른 주소 스캔, 스트랩 핀 설계도 확인 |
| 읽기 값이 매번 다름 | 버스 신호 무결성 문제, pull-up 부족 | MDC/MDIO 파형 품질 확인, 2.2kΩ pull-up 유무 |
mdio_bus 드라이버가 PHY를 스캔하는 로그는 dmesg | grep -i mdio로 확인할 수 있습니다.
PHY가 아예 보이지 않으면 MDIO 컨트롤러 드라이버 로드 여부(lsmod | grep mdio)부터 확인하고,
ls /sys/bus/mdio_bus/devices/로 커널이 인식한 MDIO 장치 목록을 조회하세요.
| 모드 | 주소 구조 | 적용 범위 | 특징 |
|---|---|---|---|
| Clause 22 | PHY 주소 5비트 + 레지스터 5비트 | 주로 1G 이하 PHY | 구조가 단순하고 기본 상태 조회에 적합 |
| Clause 45 | PHY 주소 + MMD(devad) + 16비트 레지스터 | 10G 이상, 복합 PHY | PMA/PCS/AN/FEC를 블록별로 분리해 접근 |
| 레지스터 | Clause 22 주소 | 의미 | 실무 활용 |
|---|---|---|---|
| BMCR | 0x00 | reset, speed select, duplex, autoneg enable | 강제 속도/듀플렉스 설정 여부 확인 |
| BMSR | 0x01 | link, autoneg capable, autoneg complete | 링크 업과 협상 완료 여부 점검 |
| PHYID1 | 0x02 | 벤더 OUI 상위 비트 | PHY 모델 식별 |
| PHYID2 | 0x03 | OUI 하위 비트 + 모델/리비전 | 드라이버 매칭 검증 |
| ANAR / ANLPAR | 0x04 / 0x05 | 광고 능력 / 상대편 응답 | 속도, pause, duplex 불일치 원인 파악 |
| ANER | 0x06 | AN expansion (parallel detect, page received) | 상대편이 AN을 지원하는지 확인 |
| GBCR / GBSR | 0x09 / 0x0A | 1000BASE-T control/status | master/slave 역할, 1G 광고/결과 |
| GBESR | 0x0F | extended status (1000BASE-T/X 지원 여부) | PHY가 1G 이상을 지원하는지 확인 |
BMCR과 BMSR 비트 맵
BMCR(Basic Mode Control Register, 0x00)과 BMSR(Basic Mode Status Register, 0x01)은 모든 Clause 22 PHY가 반드시 구현해야 하는 핵심 레지스터입니다. BMCR은 PHY 동작을 제어하고, BMSR은 현재 상태와 PHY의 능력을 보고합니다. 특히 BMSR의 링크 상태 비트(bit 2)는 래치(Latch) 동작을 하는 PHY가 많아서, 링크가 한 번이라도 끊어지면 0으로 래치되고 다시 읽어야 현재 상태가 반영됩니다.
# 네트워크 인터페이스가 어떤 PHY에 연결되었는지 확인
readlink -f /sys/class/net/eth0/phydev
# MDIO 버스에 보이는 PHY 목록과 ID 확인
grep . /sys/bus/mdio_bus/devices/*/phy_id
# 기본 링크 정보와 광고 능력 확인
ethtool eth0
# 드라이버가 찍는 PHY 로그 확인
dmesg | grep -i -E "phy|mdio|link"
ethtool 출력과 함께 비교하는 습관이 필요합니다.
Clause 45와 MMD 주소 공간
Clause 45의 핵심은 PHY를 하나의 납작한 레지스터 맵으로 보지 않고, 기능 블록별 MMD(Manageable Device)로 나누는 데 있습니다. 10G 이상 PHY나 복합 SerDes는 PMA/PMD, PCS, Auto-Negotiation, vendor-specific 블록이 분리되어 있어야 디버깅과 기능 확장이 가능합니다. 리눅스 커널도 이를 mdiobus_c45_read() 같은 API와 MDIO_MMD_* 매크로(Macro)로 반영합니다.
| devad | MMD 이름 | 대표 내용 | 실무 활용 |
|---|---|---|---|
| 1 | PMA/PMD | signal detect, PMD control, optical/copper 특성 | 광 세기, lane signal, 전기 특성 확인 |
| 2 | WIS | WAN 인터페이스 하위 계층 | 특수 WAN PHY에서 사용 |
| 3 | PCS | block lock, fault, PCS status | SerDes lock, alignment, fault 분리 |
| 4 | PHY XS | PHY 확장 SerDes 계층 | 고속 lane 상태 확인 |
| 5 | DTE XS | DTE 쪽 확장 SerDes | 호스트 측 lane 진단 |
| 6 | TC | transmission convergence | 특정 고속 PHY 구현에서 사용 |
| 7 | Auto-Negotiation | AN control/status, next page | 고속 AN 실패 원인 분석 |
| 30/31 | Vendor 1/2 | 벤더 확장 기능 | RGMII delay, downshift, 온도 센서, patch RAM |
/* 개념 예시: Clause 45 MMD 직접 읽기 */
struct mii_bus *bus = phydev->mdio.bus;
int phy_addr = phydev->mdio.addr;
/* PCS status 1 */
int pcs_stat = mdiobus_c45_read(bus, phy_addr, MDIO_MMD_PCS, 0x0001);
/* AN status 1 */
int an_stat = mdiobus_c45_read(bus, phy_addr, MDIO_MMD_AN, 0x0001);
/* 벤더 확장 레지스터 */
int vend = mdiobus_c45_read(bus, phy_addr, MDIO_MMD_VEND1, 0x8050);
MDIO 버스 토폴로지(Topology)와 다중 PHY
실제 보드에서는 PHY가 하나만 있는 경우보다, 하나의 MDIO 버스에 여러 PHY와 스위치 내부 포트 PHY, retimer, 별도 PCS, SFP 관리 장치가 함께 매달리는 경우가 더 많습니다. MDIO는 I2C처럼 주소 기반 공유 버스이므로, 주소 충돌과 버스 길이, pull-up, reset 순서를 동시에 관리해야 합니다. 보드 bring-up에서 PHY 하나는 보이고 나머지는 안 보인다면 드라이버보다 먼저 버스 토폴로지를 확인해야 합니다.
특히 스위치 칩은 외부 포트의 copper PHY와 별개로 내부 포트용 PHY 또는 PCS를 또 가지고 있을 수 있습니다. 이 경우 운영체제는 "인터페이스 하나당 PHY 하나"라고 단순화해서 볼 수 없고, MDIO 버스 아래에 여러 개의 mdio_device와 phy_device가 계층적으로 배치됩니다. 주소가 겹치거나 reset 순서가 꼬이면 전혀 다른 장치에 레지스터를 쓰는 사고가 발생할 수 있습니다.
| 토폴로지 | 구성 예 | 장점 | 흔한 문제 |
|---|---|---|---|
| 단일 버스 + 단일 PHY | SoC MAC ↔ 외부 RJ45 PHY | 가장 단순, bring-up 쉬움 | 주소 실수, reset 타이밍 |
| 단일 버스 + 다중 PHY | 듀얼 포트 보드, 4포트 PHY | 배선 단순화 | 주소 충돌, 전원 순서, 버스 부하 |
| 버스 뒤에 스위치/DSA | CPU MAC ↔ 스위치 ↔ 포트 PHY들 | 포트 확장 용이 | 내부 MDIO 주소 맵 이해 필요 |
| MDIO mux/게이트 | 백플레인 슬롯별 PHY 선택 | 주소 재사용 가능 | mux 선택 상태와 reset 순서 꼬임 |
| MAC PCS + 외부 PHY 혼합 | host PCS는 MAC 내부, copper PHY는 외부 | SerDes와 copper 분리 가능 | PCS와 PHY 상태를 따로 해석해야 함 |
/* 개념 예시: 하나의 MDIO 버스에 다중 PHY가 매달린 경우 */
&mdio0 {
#address-cells = <1>;
#size-cells = <0>;
phy0: ethernet-phy@1 {
reg = <1>;
};
phy1: ethernet-phy@2 {
reg = <2>;
};
switch0: ethernet-switch@10 {
reg = <0x10>;
};
};
- 주소는 0..31 범위만 있으므로, 슬롯 구조에서는 물리적으로 같은 주소를 재사용하고 mux로 분리하는 설계가 흔합니다.
- 0xffff / 0x0000 읽힘이 한 포트에서만 발생하면 전체 드라이버보다 해당 포트의 reset, pull-up, mux 선택 상태를 먼저 의심하세요.
- DSA 스위치가 개입되면 CPU 포트용 PCS, 사용자 포트용 PHY, 내부 MDIO 주소 맵을 각각 분리해서 이해해야 합니다.
MDIO mux 프레임워크
MDIO 주소 공간이 5비트(0~31)로 제한되기 때문에, 대규모 장비에서는 MDIO mux(멀티플렉서(Multiplexer))로 버스를 분할합니다. 리눅스 커널은 mdio-mux 프레임워크를 제공하며, GPIO mux(mdio-mux-gpio), MMIO mux(mdio-mux-mmio), I2C mux 등 다양한 구현이 있습니다. mux 뒤의 각 세그먼트(Segment)는 독립적인 MDIO 버스로 취급되므로, 같은 PHY 주소를 세그먼트마다 재사용할 수 있습니다.
/* Device Tree 예시: GPIO 기반 MDIO mux */
mdio-mux {
compatible = "mdio-mux-gpio";
gpios = <&gpio0 3 0>, <&gpio0 4 0>; /* 2비트 선택선 → 4개 세그먼트 */
mdio-parent-bus = <&mdio0>;
#address-cells = <1>;
#size-cells = <0>;
mdio_seg0: mdio@0 {
reg = <0>;
#address-cells = <1>;
#size-cells = <0>;
phy_slot0: ethernet-phy@1 { reg = <1>; };
};
mdio_seg1: mdio@1 {
reg = <1>;
#address-cells = <1>;
#size-cells = <0>;
phy_slot1: ethernet-phy@1 { reg = <1>; }; /* 같은 주소 재사용 */
};
};
select() 콜백 자체도 MDIO 트랜잭션이 필요할 수 있습니다(예: I2C mux).
이 경우 mux 선택과 실제 PHY 접근 사이에 락(lock) 순서가 꼬이면 데드락(deadlock)이 발생합니다.
커널의 mdio_mux_init()는 이를 고려해 부모 버스 락을 잡은 채 select를 호출하므로, select 내부에서 같은 부모 버스의 MDIO 접근을 시도하면 안 됩니다.
PHY 탐지와 링크 수명주기
커널 관점에서 PHY 수명주기는 "발견, attach, 광고 설정, 상태 머신 시작, 링크 변화 통지, stop"의 반복입니다. probe 단계에서는 PHY 노드를 찾고 드라이버를 매칭합니다. 인터페이스가 올라갈 때는 phy_start() 또는 phylink_start()가 상태 머신을 돌리고, 링크가 변하면 드라이버는 speed, duplex, pause, interface mode를 MAC에 다시 적용해야 합니다.
- 발견 —
phy-handle또는 firmware node를 통해 PHY를 찾고 ID를 읽어 드라이버를 매칭합니다. - attach —
of_phy_connect()또는 phylink 연결 함수로 MAC과 PHY를 연결합니다. - 정책 설정 — 지원 속도, EEE, pause, advertisement, in-band status 정책을 설정합니다.
- 상태 머신 시작 — 인터페이스 up 시 PHY state machine이 주기적으로 링크를 확인합니다.
- 링크 변화 통지 —
adjust_link혹은mac_link_up()경로에서 MAC 레지스터가 재설정됩니다. - 정지 — 인터페이스 down 시 queue 정지 후 PHY polling 또는 IRQ를 멈춥니다.
/* 개념 예시: phylib attach와 링크 수명주기 */
static void my_adjust_link(struct net_device *ndev)
{
struct phy_device *phydev = ndev->phydev;
if (!phydev->link) {
my_mac_link_down(ndev);
return;
}
my_mac_link_up(ndev, phydev->speed, phydev->duplex);
}
static int my_probe(struct platform_device *pdev)
{
struct net_device *ndev = platform_get_drvdata(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_connect(ndev, phy_np, &my_adjust_link, 0,
PHY_INTERFACE_MODE_RGMII_ID);
of_node_put(phy_np);
if (!phydev)
return -ENODEV;
phy_set_max_speed(phydev, SPEED_1000);
phy_attached_info(phydev);
return 0;
}
static int my_open(struct net_device *ndev)
{
phy_start(ndev->phydev);
netif_start_queue(ndev);
return 0;
}
static int my_stop(struct net_device *ndev)
{
netif_stop_queue(ndev);
phy_stop(ndev->phydev);
return 0;
}
PHY 상태 머신 내부 상태와 전이 규칙
커널 phylib는 phy_state 열거형(enum)으로 PHY의 수명 주기를 관리합니다. 각 상태는 phy_state_machine() 폴링 루프와 phy_start()/phy_stop() 등의 제어 호출에 의해 전이됩니다. 상태를 정확히 이해하면 드라이버 개발 시 발생하는 링크 불안정, 무한 재시도, 장치 미해제 같은 문제를 빠르게 진단할 수 있습니다.
phy_state 열거형 정의
/* include/linux/phy.h (v6.x 기준, 일부 커널 버전에서 이름이 다를 수 있음) */
enum phy_state {
PHY_DOWN = 0, /* PHY가 비활성화됨 — 드라이버 미연결 또는 phy_detach() 완료 */
PHY_READY = 1, /* phy_attach() 완료, phy_start() 대기 중 */
PHY_HALTED = 2, /* phy_stop() 호출됨 — 폴링 중단, MAC 링크 다운 알림 완료 */
PHY_ERROR = 3, /* config_init / read_status 등에서 복구 불가 오류 발생 */
PHY_UP = 4, /* phy_start() 호출됨 — config_init 수행 후 협상 대기 */
PHY_RUNNING = 5, /* 링크 UP — MAC이 속도/듀플렉스로 설정 완료 */
PHY_NOLINK = 6, /* 협상 완료 후 링크 신호 없음 — 재협상 또는 케이블 대기 */
PHY_CABLETEST = 7, /* ethtool cable test 진행 중 — 완료 후 PHY_UP 또는 PHY_HALTED로 복귀 */
};
각 상태의 상세 설명
다음은 각 상태가 실제로 의미하는 바와 전이 조건입니다.
- PHY_DOWN:
phy_device가 시스템에 등록되었지만 MAC 드라이버와 아직 연결(attach)되지 않았거나,phy_detach()가 호출된 직후의 상태입니다. 이 상태에서는 폴링 타이머가 동작하지 않으며 MDIO 접근도 수행되지 않습니다. - PHY_READY:
phy_attach_direct()완료 후 진입합니다.config_init()은 아직 호출되지 않았으므로 PHY 레지스터 초기화가 완료되지 않은 상태입니다. MAC 드라이버가phy_start()를 호출할 때까지 대기합니다. - PHY_HALTED:
phy_stop()호출에 의해 진입합니다. 폴링이 중단되고 MAC에는 링크 다운이 통보됩니다. 인터페이스가ifconfig eth0 down또는 드라이버 제거(rmmod) 시에 나타납니다. - PHY_ERROR:
config_init(),config_aneg(),read_status()등에서 복구 불가능한 오류가 발생한 경우에 진입합니다. 드라이버는 이 상태를 감지해 재초기화를 시도하거나 에러 로그를 남겨야 합니다. - PHY_UP:
phy_start()가 호출된 후phy_state_machine()이config_init()과config_aneg()를 실행하는 단계입니다. 아직 링크가 확립되지 않은 상태이므로 MAC에 링크 업 통보는 없습니다. - PHY_RUNNING:
read_status()가 링크 UP을 확인하고 MAC 드라이버의adjust_link()를 통해 속도와 듀플렉스 설정이 완료된 상태입니다. 실제 패킷 송수신이 이루어집니다. - PHY_NOLINK: 협상은 완료되었으나 물리적 신호(링크 비트)가 감지되지 않는 상태입니다. 케이블 미연결, SFP 모듈 문제, 원격 장치 다운 등이 원인이 됩니다. 폴링 주기마다
read_status()를 반복 호출합니다. - PHY_CABLETEST:
ethtool --cable-test명령 또는phy_start_cable_test()호출 시 진입합니다. 케이블 진단이 완료되면 결과에 따라PHY_UP또는PHY_HALTED로 전이됩니다.
상태 전이 다이어그램
phy_state_machine() 핵심 폴링 루프
phy_state_machine()은 PHY_POLL 모드에서 주기적으로 호출되며(기본 PHY_STATE_TIME = 1 Hz), 인터럽트 모드에서는 인터럽트 핸들러가 대신 트리거합니다. 현재 상태를 읽고 다음 상태를 결정하는 구조입니다.
/* drivers/net/phy/phy.c — 핵심 로직 요약 (실제 코드 단순화) */
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;
enum phy_state old_state;
int err = 0;
mutex_lock(&phydev->lock);
old_state = phydev->state;
switch (phydev->state) {
case PHY_DOWN:
case PHY_READY:
break; /* 폴링 비활성 — 아무 동작 없음 */
case PHY_UP:
needs_aneg = true; /* config_aneg() 호출 예약 */
break;
case PHY_NOLINK:
case PHY_RUNNING:
err = phy_check_link_status(phydev); /* read_status() → adjust_link() */
break;
case PHY_HALTED:
do_suspend = true;
break;
case PHY_ERROR:
phy_error(phydev); /* 오류 상태 유지 또는 리셋 시도 */
break;
default:
break;
}
mutex_unlock(&phydev->lock);
if (needs_aneg)
err = phy_start_aneg(phydev); /* config_init → config_aneg 순서로 호출 */
if (do_suspend)
phy_suspend(phydev);
if (err == -ENODEV)
phy_error(phydev);
/* 다음 폴링 스케줄 (인터럽트 모드가 아닐 때만) */
if (phy_polling_mode(phydev) && phy_is_started(phydev))
phy_queue_state_machine(phydev, PHY_STATE_TIME);
}
phy_start() / phy_stop() 흐름
phy_start()와 phy_stop()은 MAC 드라이버가 인터페이스를 올리거나 내릴 때 직접 호출하는 진입점입니다. 두 함수는 뮤텍스(mutex)로 보호된 상태에서 phydev->state를 변경하고 phy_state_machine()을 즉시 스케줄합니다.
/* drivers/net/phy/phy.c */
void phy_start(struct phy_device *phydev)
{
mutex_lock(&phydev->lock);
if (phydev->state != PHY_READY &&
phydev->state != PHY_HALTED) {
WARN_ONCE(true, "phy_start() called in unexpected state %d\n",
phydev->state);
mutex_unlock(&phydev->lock);
return;
}
if (phydev->state == PHY_HALTED) {
phydev->state = PHY_UP;
phy_resume(phydev); /* 드라이버 resume 훅 호출 */
} else {
phydev->state = PHY_UP;
}
phy_queue_state_machine(phydev, 0); /* 즉시 state_machine 트리거 */
mutex_unlock(&phydev->lock);
}
EXPORT_SYMBOL(phy_start);
void phy_stop(struct phy_device *phydev)
{
mutex_lock(&phydev->lock);
if (!phy_is_started(phydev) && phydev->state != PHY_DOWN) {
WARN_ONCE(true, "phy_stop() called in unexpected state\n");
mutex_unlock(&phydev->lock);
return;
}
phydev->state = PHY_HALTED;
phy_process_queued_link_change(phydev); /* 펜딩 링크 이벤트 처리 */
mutex_unlock(&phydev->lock);
phy_stop_machine(phydev); /* 폴링 워크큐 취소 */
phy_disable_interrupts(phydev);
phy_link_down(phydev); /* MAC adjust_link(link=0) 호출 */
}
EXPORT_SYMBOL(phy_stop);
상태 전이 조건 요약 테이블
| 전이 (From → To) | 트리거 | 호출 컨텍스트 | 주의 사항 |
|---|---|---|---|
DOWN → READY |
phy_attach_direct() |
MAC 드라이버 probe 또는 open() |
config_init()은 아직 미호출 상태 |
READY → UP |
phy_start() |
net_device.ndo_open() 내부 |
phy_start() 전에 phy_connect()가 완료되어야 합니다. |
UP → RUNNING |
read_status()에서 link=1 감지 |
phy_state_machine() 폴링 루프 |
adjust_link() 콜백으로 MAC에 통보됩니다. |
RUNNING → NOLINK |
read_status()에서 link=0 감지 |
phy_state_machine() 폴링 루프 |
케이블 단선, 원격 장치 다운, 신호 손실 등이 원인입니다. |
NOLINK → RUNNING |
read_status()에서 link=1 재감지 |
phy_state_machine() 폴링 루프 |
Auto-Negotiation 재협상이 필요할 수 있습니다. |
RUNNING/UP → HALTED |
phy_stop() |
net_device.ndo_stop() 내부 |
폴링이 즉시 중단되고 MAC에 링크 다운이 통보됩니다. |
HALTED → UP |
phy_start() |
net_device.ndo_open() |
phy_resume()가 내부에서 호출됩니다. |
HALTED → DOWN |
phy_detach() |
MAC 드라이버 remove | 반드시 HALTED 상태에서만 detach해야 합니다. |
UP/RUNNING → ERROR |
config_init() 또는 read_status() 오류 |
phy_state_machine() |
드라이버는 phy_error()를 호출해 이 상태로 진입시킵니다. |
RUNNING → CABLETEST |
phy_start_cable_test() |
ethtool 명령 또는 직접 호출 |
진단 완료 후 phy_start_cable_test_tdr_notify()로 결과 전달됩니다. |
- phy_start() 전 config_init() 미완료:
phy_start()는 내부적으로phy_state_machine()을 통해config_init()을 호출합니다. MAC 드라이버가 직접config_init()을 호출하면 이중 초기화가 발생할 수 있습니다. - PHY_READY 상태에서 phy_stop() 호출:
phy_stop()은PHY_READY상태에서는 동작하지 않습니다. 반드시phy_start()이후, 즉PHY_UP이상 상태에서 호출해야 합니다. - phy_detach() 타이밍:
phy_detach()는 반드시phy_stop()이후PHY_HALTED또는PHY_DOWN상태에서만 안전합니다. 링크가 살아있는 상태에서detach하면 use-after-free 버그가 발생할 수 있습니다. - PHY_ERROR 상태 무시:
phy_error()가 호출되면 폴링 루프는 계속 실행되지만 상태를 복구하지 않습니다. MAC 드라이버는adjust_link()콜백에서phydev->state == PHY_ERROR를 확인하고 상위 계층에 알려야 합니다. - 인터럽트 모드와 폴링 모드 혼용:
PHY_POLL플래그 없이phy_attach()하면 인터럽트 구동 방식이 기본값입니다. 인터럽트 핀이 올바르게 연결되지 않은 하드웨어에서는 링크 이벤트가 전달되지 않아PHY_NOLINK상태에 무한히 머무를 수 있습니다. 이 경우phy_attach()시MDIO_BUS_PHY_IRQ_POLL을 명시적으로 설정해야 합니다. - PHY_CABLETEST 중 링크 이벤트 수신: 케이블 진단 중에는
phy_state_machine()이 링크 상태를 확인하지 않으므로, 진단이 완료될 때까지 링크 변경 이벤트가 MAC으로 전달되지 않습니다.
리눅스 커널 객체 모델
리눅스에서 PHY 관련 코드를 읽을 때는 mii_bus, mdio_device, phy_device, phy_driver, phylink, net_device가 어떤 관계인지 먼저 잡아야 합니다. mii_bus는 실제 MDIO 컨트롤러이고, phy_device는 그 버스 위에 매달린 PHY 인스턴스, phy_driver는 특정 PHY ID에 매칭되는 동작 구현, phylink는 MAC과 PHY 사이 정책 오케스트레이터입니다.
| 객체 | 대표 소유자 | 핵심 역할 | 어디서 자주 보나 |
|---|---|---|---|
mii_bus | MAC/SoC 드라이버 | MDIO read/write/reset 제공 | MDIO 컨트롤러 probe |
mdio_device | MDIO 코어 | 버스 위 디바이스 공통 래퍼 | PHY 이외 MDIO 장치 |
phy_device | phylib | 링크 상태, 광고 능력, 인터페이스 모드 보유 | phydev->link, speed, duplex |
phy_driver | 벤더 PHY 드라이버 | config_init, config_aneg, read_status 구현 | drivers/net/phy/ |
phylink | MAC 드라이버 | MAC/PHY/PCS/SFP 정책 통합 | SFP, fixed-link, in-band status |
net_device | MAC 드라이버 | 유저스페이스와 커널 네트워크 스택(Network Stack) 진입점(Entry Point) | eth0 링크 관리 |
/* 개념 예시: PHY 관련 커널 객체의 대표 필드 */
struct mii_bus {
char id[MII_BUS_ID_SIZE];
int (*read)(...);
int (*write)(...);
int (*reset)(...);
struct device *parent;
};
struct phy_device {
struct mdio_device mdio;
phy_interface_t interface;
int link;
int speed;
int duplex;
int pause;
int asym_pause;
int irq;
linkmode_t supported;
linkmode_t advertising;
linkmode_t lp_advertising;
};
struct phy_driver {
u32 phy_id;
u32 phy_id_mask;
const char *name;
int (*config_init)(...);
int (*config_aneg)(...);
int (*read_status)(...);
irqreturn_t (*handle_interrupt)(...);
};
PHY 드라이버 콜백을 어디까지 구현해야 하나
많은 PHY는 genphy_config_aneg(), genphy_read_status() 같은 공통 헬퍼만으로도 동작합니다. 하지만 RGMII 지연, downshift, EEE, LED, strap override, 온도 센서, FEC 정책처럼 벤더 확장 기능이 필요한 경우에는 전용 콜백 구현이 필요합니다.
| 콜백 | 언제 필요한가 | 대표 작업 |
|---|---|---|
config_init | probe 직후 초기 설정 | strap 보정, 지연 모드, 기본 LED 정책 |
config_aneg | 광고 능력 커스터마이즈 | EEE/FEC/downshift/속도 제한 |
read_status | 링크 상태 해석이 벤더별로 다름 | speed/duplex/pause, master/slave 반영 |
handle_interrupt | IRQ 기반 PHY | 링크 변화 인터럽트(Interrupt) 처리 |
suspend/resume | 저전력 운용 필요 | WoL, EEE, analog power gating |
/* 개념 예시: 벤더 페이지 전환 레지스터 접근 패턴 */
static int myphy_set_rgmii_delay(struct phy_device *phydev)
{
int oldpage, ret;
oldpage = phy_select_page(phydev, 0xd08);
if (oldpage < 0)
return oldpage;
ret = phy_modify(phydev, 0x11, 0x0300, 0x0200);
return phy_restore_page(phydev, oldpage, ret);
}
genphy 헬퍼 함수 상세 분석
대부분의 PHY 드라이버는 벤더 전용 레지스터 접근이 필요한 일부 콜백만 직접 구현하고, 나머지는 커널이 제공하는 genphy_* 계열 함수에 위임합니다. 이 함수들은 IEEE 802.3 표준 레지스터(BMCR, BMSR, ANAR, ANLPAR, GBCR, GBSR)만을 사용해 동작하므로, 표준 Clause 22를 준수하는 PHY라면 드라이버 없이도 링크를 올릴 수 있습니다. 이 절에서는 각 함수가 어떤 레지스터를 어떤 순서로 다루는지 실제 커널 로직에 근거해 상세히 살펴봅니다.
genphy_config_aneg() — Auto-Negotiation 광고 설정
genphy_config_aneg()는 phy_device의 advertising 링크모드(Linkmode) 비트맵을 읽어 ANAR(Auto-Negotiation Advertisement Register, 레지스터 4번)와 GBCR(1000BASE-T Control Register, 레지스터 9번)에 기록한 뒤, BMCR(Basic Mode Control Register, 레지스터 0번)에서 재시작 비트를 세트합니다. PHY가 AN을 지원하지 않는 경우에는 강제 속도/듀플렉스(duplex) 경로로 폴백합니다.
/* drivers/net/phy/phy_device.c 참조 — 개념 재현 */
int genphy_config_aneg(struct phy_device *phydev)
{
int err, changed = 0;
/* (1) 1000BASE-T 마스터/슬레이브(Master/Slave) 광고 — 레지스터 9 */
if (genphy_config_eee_advert(phydev) &&
linkmode_test_bit(ETHTOOL_LINK_MODE_Autoneg_BIT,
phydev->advertising)) {
err = genphy_config_advert(phydev);
if (err < 0)
return err;
changed |= err;
}
if (linkmode_test_bit(ETHTOOL_LINK_MODE_Autoneg_BIT,
phydev->advertising)) {
/* (2) BMCR 에 AN_ENABLE + RESTART_AN 세트 */
if (changed) {
err = phy_modify(phydev, MII_BMCR,
BMCR_ANRESTART | BMCR_ANENABLE,
BMCR_ANENABLE | BMCR_ANRESTART);
if (err < 0)
return err;
}
} else {
/* (3) AN 비활성: 강제 속도/듀플렉스 적용 */
err = genphy_setup_forced(phydev);
if (err < 0)
return err;
}
return 0;
}
/* genphy_config_advert: ANAR(reg4)와 GBCR(reg9)를 advertising 비트맵으로 갱신 */
static int genphy_config_advert(struct phy_device *phydev)
{
int err, adv, changed = 0;
/* ANAR: 10/100 half/full + pause 비트 */
adv = ethtool_adv_to_mii_adv_t(
linkmode_adv_to_lcl_adv_t(phydev->advertising));
err = phy_modify_changed(phydev, MII_ADVERTISE,
ADVERTISE_ALL | ADVERTISE_100BASE4 |
ADVERTISE_PAUSE_CAP | ADVERTISE_PAUSE_ASYM,
adv);
if (err < 0)
return err;
changed |= err;
/* GBCR(reg9): 1000BASE-T half/full 광고 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT,
phydev->advertising) ||
linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Half_BIT,
phydev->advertising)) {
adv = ethtool_adv_to_mii_ctrl1000_t(
linkmode_adv_to_lcl_adv_t(phydev->advertising));
err = phy_modify_changed(phydev, MII_CTRL1000,
CTL1000_ENABLE_MASTER |
CTL1000_AS_MASTER |
ADVERTISE_1000FULL |
ADVERTISE_1000HALF, adv);
if (err < 0)
return err;
changed |= err;
}
return changed;
}
주목할 점은 phy_modify_changed()가 기존 레지스터 값과 비교해 실제로 비트가 달라졌을 때만 쓰기를 수행하고 1을 반환한다는 것입니다. 광고 내용이 변하지 않았다면 불필요한 AN 재시작을 생략해 링크 순간 끊김을 방지합니다.
genphy_read_status() — 링크 상태 판독
genphy_read_status()는 링크가 올라온 후 커널 폴러(Poller) 또는 인터럽트 핸들러에서 주기적으로 호출되어, PHY 레지스터를 읽고 phydev의 link, speed, duplex, pause 필드를 갱신합니다. MAC 드라이버는 이 필드를 보고 MAC 속도를 재설정합니다.
int genphy_read_status(struct phy_device *phydev)
{
int err;
/* (1) BMSR 읽기 — sticky 비트이므로 두 번 읽어 래치 해제 */
err = genphy_update_link(phydev);
if (err)
return err;
/* 링크가 없으면 속도/듀플렉스 정보는 의미 없음 */
phydev->speed = SPEED_UNKNOWN;
phydev->duplex = DUPLEX_UNKNOWN;
phydev->pause = 0;
phydev->asym_pause = 0;
if (phydev->autoneg == AUTONEG_ENABLE && phydev->link) {
/* (2) ANLPAR(reg5): 파트너 광고 읽기 */
err = genphy_read_lpa(phydev);
if (err < 0)
return err;
/* (3) GBSR(reg10): 1000BASE-T 마스터/슬레이브 결과 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT,
phydev->supported) ||
linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Half_BIT,
phydev->supported)) {
err = genphy_read_1000t_status(phydev);
if (err < 0)
return err;
}
/* (4) 협상 결과로 speed/duplex/pause 결정 */
phy_resolve_aneg_linkmode(phydev);
} else if (phydev->autoneg == AUTONEG_DISABLE) {
/* (5) 강제 모드: BMCR 비트로 speed/duplex 직접 판독 */
err = genphy_read_status_fixed(phydev);
if (err < 0)
return err;
}
return 0;
}
genphy_update_link()는 BMSR(Basic Mode Status Register, 레지스터 1번)의 비트 2(Link Status)를 확인합니다. 이 비트는 스티키(sticky) 비트여서 링크 다운 이벤트를 기록하고 나면 읽어야 클리어되므로, 래치 해제를 위해 BMSR을 두 번 연속으로 읽습니다. phy_resolve_aneg_linkmode()는 자신의 advertising과 파트너의 lp_advertising 비트맵을 AND 연산해 최대 공통 모드를 선택합니다.
genphy_suspend() / genphy_resume() — 전원 관리
genphy_suspend()는 BMCR의 비트 11(Power Down)을 세트해 PHY 아날로그 회로를 저전력 상태로 전환합니다. genphy_resume()는 해당 비트를 클리어해 정상 동작 상태로 복귀시킵니다. 드라이버가 이 콜백을 오버라이드하는 일반적인 이유는 WoL(Wake-on-LAN) 활성화 여부를 확인하거나, Power Down 시에도 유지해야 하는 벤더 레지스터 상태가 있을 때입니다.
int genphy_suspend(struct phy_device *phydev)
{
return phy_set_bits(phydev, MII_BMCR, BMCR_PDOWN);
}
EXPORT_SYMBOL(genphy_suspend);
int genphy_resume(struct phy_device *phydev)
{
return phy_clear_bits(phydev, MII_BMCR, BMCR_PDOWN);
}
EXPORT_SYMBOL(genphy_resume);
/* 오버라이드 예시: WoL이 활성화된 경우 Power Down 생략 */
static int myphy_suspend(struct phy_device *phydev)
{
if (phy_interrupt_is_valid(phydev) && phydev->wol_enabled)
return 0; /* WoL 활성: Power Down 금지 */
return genphy_suspend(phydev);
}
genphy_soft_reset() — PHY 소프트 리셋
genphy_soft_reset()은 BMCR의 비트 15(Reset)를 세트하고, PHY 내부가 리셋을 완료해 해당 비트를 자동으로 클리어할 때까지 폴링합니다. 최대 대기 시간은 500ms이며, 이 기간을 초과하면 -ETIMEDOUT을 반환합니다. 리셋 직후에는 PHY가 EEPROM 스트랩(strap) 또는 내부 기본값으로 레지스터를 복원하므로, 이후에 벤더 초기화 코드를 반드시 재실행해야 합니다.
int genphy_soft_reset(struct phy_device *phydev)
{
u16 res = BMCR_RESET;
int err;
if (phydev->autoneg == AUTONEG_ENABLE)
res |= BMCR_ANRESTART;
/* BMCR bit15 = 1 → PHY 리셋 개시 */
err = phy_modify(phydev, MII_BMCR, BMCR_ISOLATE, res);
if (err < 0)
return err;
/* PHY가 리셋을 완료하면 bit15를 스스로 클리어함 */
err = phy_poll_reset(phydev);
if (err)
return err;
/* 리셋 후 일부 PHY는 추가 안정화 시간이 필요 */
msleep(1);
return 0;
}
EXPORT_SYMBOL(genphy_soft_reset);
phy_poll_reset() 내부는 최대 100ms 간격으로 BMCR bit15를 읽으면서 클리어를 기다립니다. 재시도 횟수 초과 시 -ETIMEDOUT을 반환합니다. 하드웨어 리셋 핀(reset-gpios)이 붙어 있는 경우에는 mdio_device_reset()이 GPIO를 토글하고, 그 다음 단계로 genphy_soft_reset()이 호출됩니다.
genphy_read_abilities() / genphy_c45_read_abilities() — PHY 능력 탐지
genphy_read_abilities()는 PHY 탐지 직후 phy_driver.get_features 콜백 위치에서 호출되어 phydev->supported 링크모드 비트맵을 채웁니다. BMSR의 능력 비트(10BASE-T, 100BASE-TX, 1000BASE-T, AN 지원)를 읽어 표준 비트맵으로 변환하고, GBSR(레지스터 10번)도 추가로 확인합니다.
int genphy_read_abilities(struct phy_device *phydev)
{
int val;
linkmode_set_bit_array(phy_basic_ports_array,
ARRAY_SIZE(phy_basic_ports_array),
phydev->supported);
/* BMSR(reg1): 기본 속도/듀플렉스 능력 비트 */
val = phy_read(phydev, MII_BMSR);
if (val < 0)
return val;
linkmode_mod_bit(ETHTOOL_LINK_MODE_Autoneg_BIT,
phydev->supported, val & BMSR_ANEGCAPABLE);
linkmode_mod_bit(ETHTOOL_LINK_MODE_100baseT_Full_BIT,
phydev->supported, val & BMSR_100FULL);
linkmode_mod_bit(ETHTOOL_LINK_MODE_100baseT_Half_BIT,
phydev->supported, val & BMSR_100HALF);
linkmode_mod_bit(ETHTOOL_LINK_MODE_10baseT_Full_BIT,
phydev->supported, val & BMSR_10FULL);
linkmode_mod_bit(ETHTOOL_LINK_MODE_10baseT_Half_BIT,
phydev->supported, val & BMSR_10HALF);
/* ESTATUS(reg15): 기가비트(Gigabit) 능력 확인 */
if (val & BMSR_ESTATEN) {
val = phy_read(phydev, MII_ESTATUS);
if (val < 0)
return val;
linkmode_mod_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT,
phydev->supported, val & ESTATUS_1000_TFULL);
linkmode_mod_bit(ETHTOOL_LINK_MODE_1000baseT_Half_BIT,
phydev->supported, val & ESTATUS_1000_THALF);
linkmode_mod_bit(ETHTOOL_LINK_MODE_1000baseX_Full_BIT,
phydev->supported, val & ESTATUS_1000_XFULL);
}
return 0;
}
EXPORT_SYMBOL(genphy_read_abilities);
Clause 45 PHY를 위한 genphy_c45_read_abilities()는 PMA/PMD MMD(레지스터 1.4, 1.7, 1.8 등)를 읽어 10GBASE-T, 25GBASE-KR, 40GBASE-CR4 같은 상위 속도 비트까지 채웁니다. PHY 드라이버가 get_features를 오버라이드하지 않으면 phylib이 Clause 22/45 여부를 자동으로 판단해 둘 중 하나를 기본으로 호출합니다.
/* Clause 45 PHY 능력 탐지 — MMD 1(PMA/PMD), MMD 3(PCS) 참조 */
int genphy_c45_read_abilities(struct phy_device *phydev)
{
int val;
/* PMA/PMD Ctrl2(1.7): 10G 능력 비트 */
val = phy_read_mmd(phydev, MDIO_MMD_PMAPMD, MDIO_PMA_CTRL2);
if (val < 0)
return val;
linkmode_mod_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT,
phydev->supported,
(val & MDIO_PMA_CTRL2_10GBTR));
/* PCS Status2(3.8): PCS 능력 확인 */
val = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_PCS_STAT2);
if (val < 0)
return val;
linkmode_mod_bit(ETHTOOL_LINK_MODE_10000baseR_FEC_BIT,
phydev->supported,
(val & MDIO_PCS_STAT2_10GBR));
return 0;
}
genphy 함수 참조 테이블
| 함수 | 역할 요약 | 주요 레지스터 | 호출 시점 | 오버라이드 필요 조건 |
|---|---|---|---|---|
genphy_config_aneg() | AN 광고 비트맵 → ANAR/GBCR 기록, AN 재시작 | BMCR(0), ANAR(4), GBCR(9) | config_aneg 콜백 | EEE/FEC/downshift 광고, 벤더 AN 페이지 필요 시 |
genphy_read_status() | BMSR 링크 확인, ANLPAR/GBSR 읽기, speed/duplex 결정 | BMSR(1), ANLPAR(5), GBSR(10) | read_status 콜백, 폴러/인터럽트 | 벤더 확장 속도(2.5G 등), EEE, master/slave 재해석 시 |
genphy_suspend() | BMCR PDOWN 세트 → PHY 저전력 | BMCR(0) bit11 | 시스템 suspend, ethtool --set-priv-flags | WoL 활성 시 Power Down 생략, 벤더 전용 저전력 모드 |
genphy_resume() | BMCR PDOWN 클리어 → 정상 복귀 | BMCR(0) bit11 | 시스템 resume | resume 후 벤더 레지스터 재초기화 필요 시 |
genphy_soft_reset() | BMCR Reset 비트 세트 후 클리어 폴링 | BMCR(0) bit15 | 드라이버 probe, config_init 전 | 리셋 후 즉시 벤더 레지스터 복원이 필요한 경우 |
genphy_read_abilities() | BMSR/ESTATUS 읽어 supported 비트맵 채움 | BMSR(1), ESTATUS(15) | get_features 콜백, PHY 탐지 직후 | 벤더 전용 능력 레지스터, 2.5G/5G 확장 속도 지원 시 |
genphy_c45_read_abilities() | MMD PMA/PCS 레지스터로 10G+ 능력 탐지 | MMD1.4, MMD1.7, MMD3.8 | Clause 45 PHY의 get_features | 25G/40G 이상, 벤더 MMD 확장 속도 필요 시 |
genphy_config_init() | supported → advertising 복사 (기본 초기화) | — | config_init 콜백 (폴백) | 거의 항상 오버라이드: strap 보정, RGMII 지연, LED 등 |
ethtool 명령에서 MDIO 레지스터까지의 호출 체인
- genphy만으로 충분한 경우: 표준 Clause 22를 완전히 준수하고, 특수 기능(RGMII 지연 조정, downshift, EEE 강제, 온도 알람, LED 매핑)이 없는 단순 PHY입니다. 이때는
.config_aneg = genphy_config_aneg,.read_status = genphy_read_status로 지정하거나phy_driver구조체에서 해당 필드를NULL로 두면 phylib이 자동으로 genphy를 폴백으로 사용합니다. - 반드시 커스텀 구현이 필요한 경우: RGMII/SGMII 인터페이스 지연을 PHY 레지스터로 설정해야 할 때, 2.5G/5G/10G 같이 표준 BMSR로는 표현되지 않는 속도를 지원할 때, 하드웨어 EEE 협상 알고리즘이 표준과 다를 때, 벤더 페이지 전환이 필요한 레지스터에 접근해야 할 때입니다.
- 혼합 패턴 (가장 흔한 형태):
config_init와config_aneg는 벤더 구현,read_status는genphy_read_status()직접 호출 후 벤더 필드만 덧붙이는 방식입니다. 이렇게 하면 표준 협상 로직을 재사용하면서 추가 필드만 보강할 수 있습니다. - 디버깅 팁: 링크는 올라오지만 속도가
SPEED_UNKNOWN으로 남아 있다면,read_status가 genphy를 사용하고 있고 PHY가 비표준 속도 레지스터를 쓰는 상황입니다.ethtool -d eth0으로 레지스터 덤프(Dump)를 확인하고 BMSR/ANLPAR 비트를 직접 점검하세요.
Auto-Negotiation
Auto-Negotiation은 "링크 업" 자체보다 "어떤 모드로 링크를 올릴지"를 결정하는 규칙입니다. 구리 PHY에서는 FLP(Fast Link Pulse) 기반 Clause 28이 기본이고, SerDes 기반 인터페이스에서는 Clause 37이나 Clause 73의 in-band status가 많이 쓰입니다. 현대 장비는 속도뿐 아니라 pause, FEC, EEE, master/slave 역할까지 협상에 포함할 수 있습니다.
| 규격 | 주요 대상 | 교환 방식 | 실무 포인트 |
|---|---|---|---|
| Clause 28 | 10/100BASE-T | FLP burst | 기본 페이지로 speed/duplex/pause 광고 |
| Clause 37 | 1000BASE-X, SGMII | 8B/10B in-band | SerDes 링크와 MAC 상태 동기화 |
| Clause 73 | 백플레인, 10G 이상 | base page + next page | FEC, lane 특성, 고속 backplane와 결합 |
Clause 28: FLP 기반 Auto-Negotiation 상세
Clause 28은 10/100/1000BASE-T 구리 이더넷에서 사용하는 Auto-Negotiation 규격입니다. 양단의 PHY가 FLP(Fast Link Pulse) 버스트를 교환하여 지원하는 속도, duplex, pause 능력을 알리고, 공통적으로 가능한 최고 모드를 선택합니다. FLP 버스트는 33개의 펄스로 구성되며, 그중 17개의 클럭 펄스 사이에 끼워진 16개의 데이터 펄스가 하나의 Link Code Word(16비트)를 운반합니다.
ethtool -s eth0 master-slave forced-master로 디버깅할 수 있습니다.
NLP에서 FLP로: Auto-Negotiation의 탄생 배경
Auto-Negotiation이 등장하기 전, 10BASE-T PHY는 링크 유지를 위해 NLP(Normal Link Pulse)를 사용했습니다. NLP는 16ms 간격으로 전송되는 단일 전압 펄스(100ns 폭)로, "나는 살아 있다"는 단순 신호에 불과하며 어떤 정보도 운반하지 않습니다. 10BASE-T 시대에는 속도가 하나뿐이었으므로 이것만으로도 충분했습니다.
100BASE-TX가 등장하면서 문제가 생겼습니다. 10M 포트와 100M 포트가 같은 RJ45 커넥터로 연결될 수 있게 되자, 케이블을 꽂는 순간 양단이 어떤 속도로 통신할지 자동으로 합의하는 메커니즘이 필요해졌습니다. IEEE 802.3u(1995)는 기존 NLP 타이밍 슬롯 사이에 데이터 펄스를 끼워 넣어 16비트 정보를 운반하는 FLP(Fast Link Pulse) 버스트를 정의했고, 이것이 Clause 28 Auto-Negotiation의 물리적 기반입니다.
| 구분 | NLP | FLP |
|---|---|---|
| 목적 | 링크 존재 확인(Link Integrity) | 능력 광고 + 링크 존재 확인 |
| 펄스 구조 | 단일 펄스, 16ms 간격 | 17 클럭 + 16 데이터 = 33 펄스 버스트 |
| 정보량 | 0비트 (유무만 의미) | 16비트 (Link Code Word) |
| 버스트 간격 | 16ms ± 8ms | 16ms ± 8ms (버스트 단위) |
| 하위 호환 | — | NLP만 인식하는 10M PHY는 FLP를 NLP로 해석 → 10M 링크 성립 |
FLP 물리 신호의 상세 구조
FLP 버스트 하나는 정확히 2ms 안에 33개의 펄스를 담고 있습니다. 홀수 번째 펄스(1, 3, 5, …, 33번)가 클럭 펄스이고, 짝수 번째 위치(2, 4, 6, …, 32번)가 데이터 펄스 슬롯입니다. 데이터 슬롯에 펄스가 있으면 비트 1, 없으면 비트 0으로 해석합니다.
| 파라미터 | IEEE 규격값 | 설명 |
|---|---|---|
| 클럭 펄스 폭 | 100ns ± 20ns | NLP와 동일한 폭 |
| 클럭-데이터 간격 | 62.5µs ± 7µs | 클럭 직후 데이터 슬롯까지의 간격 |
| 클럭-클럭 간격 | 125µs ± 14µs | 인접 클럭 펄스 사이 간격 |
| 버스트 길이 | ≈ 2ms | 33 펄스 × 62.5µs |
| 버스트 간격 | 16ms ± 8ms | 연속 FLP 버스트 사이 |
| 펄스 진폭 | CAT-5에서 ≈ 1V 피크 | 차동 신호 기준 |
이 타이밍은 선로 위의 감쇠와 반사를 고려하여 정의되었습니다. 클럭-데이터 간격이 62.5µs인 이유는, 이보다 좁으면 매체 위의 반사 신호와 실제 데이터 펄스를 구분하기 어렵고, 이보다 넓으면 버스트 전체 시간이 길어져 협상 지연이 증가하기 때문입니다.
Clause 28 Auto-Negotiation 상태 머신
IEEE 802.3 Clause 28은 Auto-Negotiation의 동작을 7개 주요 상태로 정의합니다. 이 상태 머신은 PHY 하드웨어 내부에서 실행되며, 커널이 BMCR의 AN Enable/Restart 비트를 세트하면 PHY가 자율적으로 상태를 전이합니다. 소프트웨어는 BMSR의 AN Complete 비트를 폴링하거나 인터럽트를 기다려 완료를 감지합니다.
| 상태 | 진입 조건 | 동작 | 탈출 조건 |
|---|---|---|---|
| AN_ENABLE | BMCR.AN_ENABLE = 1 | PHY 내부 AN 회로 활성화 | 즉시 ABILITY_DETECT로 전이 |
| TRANSMIT_DISABLE | AN 미지원 또는 링크 실패 | 송신 정지, 수신만 감시 | FLP/NLP 감지 시 ABILITY_DETECT |
| ABILITY_DETECT | AN 시작 | 자신의 Base Page를 FLP로 반복 송신 | 상대의 Base Page 수신(3회 일관) |
| ACK_DETECT | 상대 Base Page 수신 | Ack 비트를 세트한 Base Page 송신 | 상대의 Ack 비트 수신 |
| COMPLETE_ACK | 양단 Ack 교환 | 일관성 검사(3회 동일 수신) | NP 비트 확인 → NEXT_PAGE_WAIT 또는 AN_GOOD_CHECK |
| NEXT_PAGE_WAIT | NP 비트 = 1 | Next Page 교환 (EEE, OUI 등) | NP 교환 완료 → AN_GOOD_CHECK |
| AN_GOOD_CHECK | 모든 페이지 교환 완료 | link_good 타이머 가동 | 타이머 만료 → AN_GOOD |
| AN_GOOD | link_good 확인 | BMSR.AN_COMPLETE = 1, 링크 업 보고 | 링크 실패 또는 AN 재시작 |
Base Page 비트 필드 완전 해부
Clause 28 Base Page(ANAR, 레지스터 0x04)는 16비트이며, FLP 버스트 하나가 이 16비트를 통째로 운반합니다. 상대편이 보내는 Base Page는 ANLPAR(레지스터 0x05)에서 읽을 수 있습니다. 각 비트 필드의 역할을 정확히 이해해야 협상 결과를 해석할 수 있습니다.
| 비트 | 필드 | 설명 | 기본값 |
|---|---|---|---|
| 4:0 | Selector Field | 프로토콜 식별자. IEEE 802.3은 00001, IEEE 802.9(ISLAN 16T)은 00010. 사실상 00001만 사용됩니다. | 00001 |
| 5 | 10BASE-T | 10Mbps half-duplex 지원 | 1 |
| 6 | 10BASE-T FD | 10Mbps full-duplex 지원 | 1 |
| 7 | 100BASE-TX | 100Mbps half-duplex 지원 | 1 |
| 8 | 100BASE-TX FD | 100Mbps full-duplex 지원 | 1 |
| 9 | 100BASE-T4 | 100Mbps 4쌍(pair) 모드 (구식, 거의 사용하지 않음) | 0 |
| 10 | PAUSE | 대칭(symmetric) pause 프레임 지원 | PHY별 |
| 11 | ASM_DIR (Asymmetric Pause) | 비대칭 pause 지원 방향 | PHY별 |
| 12 | Reserved | 예약 비트, 항상 0 | 0 |
| 13 | Remote Fault | 상대편에게 "내 쪽에 오류가 있다"는 신호 | 0 |
| 14 | Acknowledge | 상대의 Base Page를 정상 수신했다는 확인 | 상태 머신 제어 |
| 15 | Next Page | 추가 페이지(Next Page)가 뒤따른다는 표시 | 0 (NP 미사용 시) |
00001(IEEE 802.3)일 때만 비트 5~9의 기술(Technology) 필드가 위 표의 의미를 가집니다.
만약 Selector가 다른 값이면 기술 필드의 해석도 달라집니다. 실무에서는 00001 외의 값을 볼 일이 거의 없지만, ethtool 결과가 이상할 때 ANLPAR을 직접 읽어 Selector를 확인하면 원인을 좁힐 수 있습니다.
# Base Page (ANAR, reg 4) 직접 읽기
phytool read eth0/0/4
# 예: 0x05e1 = 0000 0101 1110 0001
# Selector=00001(802.3), 10T=1, 10T-FD=1, 100TX=1, 100TX-FD=1, Pause=0
# 상대편 Base Page (ANLPAR, reg 5) 읽기
phytool read eth0/0/5
# 양단의 교집합에서 최고 우선순위가 결과 모드
# ethtool로도 확인 가능
ethtool eth0 | grep -A20 "Advertised link modes"
우선순위 결정 알고리즘 (Priority Resolution)
양단이 Base Page를 교환한 후, 공통으로 지원하는 모드의 교집합에서 IEEE 802.3이 정의한 고정 우선순위 표에 따라 최고 모드가 선택됩니다. 이 우선순위는 PHY 하드웨어에 내장되어 있으며 소프트웨어로 변경할 수 없습니다.
| 우선순위 | 기술 | 속도 | Duplex | 비고 |
|---|---|---|---|---|
| 1 (최고) | 1000BASE-T FD | 1000M | Full | Clause 40 확장 레지스터(GBCR 0x09)로 광고 |
| 2 | 1000BASE-T HD | 1000M | Half | 사실상 미사용 (대부분 PHY가 미지원) |
| 3 | 100BASE-T2 FD | 100M | Full | 2쌍 100M, 규격만 존재 |
| 4 | 100BASE-TX FD | 100M | Full | 가장 흔한 100M 모드 |
| 5 | 100BASE-T2 HD | 100M | Half | 규격만 존재 |
| 6 | 100BASE-T4 | 100M | Half | 4쌍 CAT-3, 레거시(Legacy) |
| 7 | 100BASE-TX HD | 100M | Half | half-duplex 100M |
| 8 | 10BASE-T FD | 10M | Full | — |
| 9 (최저) | 10BASE-T HD | 10M | Half | 최소 공통 분모(Fallback) |
우선순위 결정은 커널 코드에서는 genphy_read_status()가 협상 완료 후 결과를 읽어오는 방식으로 반영됩니다. PHY 하드웨어가 이미 최고 공통 모드를 선택하여 동작 중이므로, 소프트웨어는 그 결과를 BMSR, ANLPAR, GBSR 레지스터에서 읽어 phydev->speed, phydev->duplex에 기록할 뿐입니다.
Next Page 프로토콜
Base Page 16비트만으로는 모든 능력을 표현할 수 없습니다. EEE(Energy Efficient Ethernet), 추가적인 기술 광고, OUI(Organizationally Unique Identifier) 기반 벤더 확장 정보를 전달하려면 Next Page 메커니즘이 필요합니다. Base Page의 비트 15(NP)를 1로 세트하면, 상태 머신은 COMPLETE_ACK 이후 NEXT_PAGE_WAIT 상태로 진입하여 추가 페이지를 교환합니다.
| 비트 | 필드 | 설명 |
|---|---|---|
| 10:0 | Message/Unformatted Code | Message Page일 때는 메시지 코드, Unformatted Page일 때는 자유 데이터 |
| 11 | Toggle | 연속 페이지 동기화용 토글 비트 (매 페이지마다 반전) |
| 12 | Ack2 | 이전 페이지의 메시지를 수신·처리 완료했다는 확인 |
| 13 | Message Page | 1 = Message Page (코드 해석), 0 = Unformatted Page (원시 데이터) |
| 14 | Ack | 상대 Next Page 수신 확인 |
| 15 | Next Page | 1 = 추가 페이지 있음, 0 = 마지막 페이지 |
Next Page 교환은 양단의 NP 비트가 모두 0이 될 때까지 반복됩니다. 한 쪽에 더 보낼 페이지가 없어도 상대가 NP=1을 보내면 Null Page(모두 0)로 응답해야 합니다. 이 프로토콜은 양방향이므로, 순서가 어긋나면 AN이 재시작됩니다.
ethtool --show-eee eth0에서 "Advertised EEE link modes"가 비어 있다면, PHY가 Next Page를 보내지 않거나 상대가 NP를 지원하지 않는 것일 수 있습니다.
커널에서는 genphy_config_eee_advert()가 EEE 광고를 MDIO MMD 레지스터(Clause 45 접근)에 설정하며, 이 정보가 Next Page 교환에 반영됩니다.
# Next Page 교환 레지스터 확인 (Clause 22)
# ANNPTR (reg 7) — 로컬 Next Page 송신
phytool read eth0/0/7
# ANNPRR (reg 8) — 상대편 Next Page 수신
phytool read eth0/0/8
# EEE 광고 레지스터 (Clause 45, MMD 7 reg 60)
phytool read eth0/7/60
# bit 1: 100BASE-TX EEE, bit 2: 1000BASE-T EEE
# 상대편 EEE 능력 (MMD 7 reg 61)
phytool read eth0/7/61
Pause 프레임 해석 알고리즘 (Annex 28B)
Auto-Negotiation에서 pause 능력은 Base Page의 비트 10(PAUSE)과 비트 11(ASM_DIR)으로 교환됩니다. 양단의 이 두 비트 조합으로 실제 흐름 제어(Flow Control) 방향이 결정되는데, 이 해석 규칙은 IEEE 802.3 Annex 28B에 정의되어 있으며 직관적이지 않으므로 주의가 필요합니다.
| 로컬 PAUSE | 로컬 ASM_DIR | 상대 PAUSE | 상대 ASM_DIR | 로컬→상대 Pause | 상대→로컬 Pause |
|---|---|---|---|---|---|
| 0 | 0 | X | X | 불가 | 불가 |
| 0 | 1 | 1 | 1 | 불가 | 가능 |
| 1 | 0 | 1 | X | 가능 | 가능 (대칭) |
| 1 | 1 | 0 | 1 | 가능 | 불가 |
| 1 | 1 | 1 | 0 | 가능 | 가능 (대칭) |
| 1 | 1 | 1 | 1 | 가능 | 가능 (대칭) |
핵심 포인트는 다음과 같습니다:
- PAUSE=1, ASM_DIR=0은 "대칭 pause만 지원"을 의미합니다 — 양방향 모두 가능하거나 모두 불가능합니다.
- PAUSE=0, ASM_DIR=1은 "나는 pause를 보내지 않지만, 상대가 보내면 처리할 수 있다"는 뜻입니다.
- PAUSE=1, ASM_DIR=1은 "대칭과 비대칭 모두 지원"으로, 가장 유연한 설정입니다.
ethtool -A eth0 rx off tx off로 명시적으로 꺼야 안전합니다.
/* 커널 내부 pause 해석 — drivers/net/phy/phy_device.c 참조 */
void phy_resolve_aneg_pause(struct phy_device *phydev)
{
/* ANAR(reg 4)의 pause 비트와 ANLPAR(reg 5)의 pause 비트를 비교 */
if (phydev->duplex == DUPLEX_FULL) {
bool lcl_pause = linkmode_test_bit(ETHTOOL_LINK_MODE_Pause_BIT,
phydev->advertising);
bool lcl_asym = linkmode_test_bit(ETHTOOL_LINK_MODE_Asym_Pause_BIT,
phydev->advertising);
bool rmt_pause = linkmode_test_bit(ETHTOOL_LINK_MODE_Pause_BIT,
phydev->lp_advertising);
bool rmt_asym = linkmode_test_bit(ETHTOOL_LINK_MODE_Asym_Pause_BIT,
phydev->lp_advertising);
/* IEEE 802.3 Annex 28B 테이블 구현 */
phydev->pause = lcl_pause && rmt_pause;
phydev->asym_pause = lcl_pause ^ rmt_pause;
}
}
1000BASE-T Master/Slave 결정 메커니즘 상세 (Clause 40)
1000BASE-T는 4쌍 모두를 동시에 사용하는 양방향 통신이므로, 한쪽이 master(타이밍 기준 송신), 다른 쪽이 slave(타이밍 추종 수신) 역할을 맡아야 합니다. 이 역할은 신호의 부호화(PAM-5)에서 클럭 복구 기준점을 결정하므로, 양쪽이 같은 역할을 선택하면 통신이 불가능합니다.
Master/slave 결정은 GBCR(Gigabit Control Register, 레지스터 0x09)의 비트 12:11로 설정하며, 4가지 모드가 있습니다:
| bit 12 (MS Manual) | bit 11 (MS Value) | 의미 | 사용 시나리오 |
|---|---|---|---|
| 0 | 0 | 자동, multiport(slave 선호) | 스위치 포트 (기본값) |
| 0 | 1 | 자동, single-port(master 선호) | NIC (기본값) |
| 1 | 0 | 강제 slave | 디버깅용 |
| 1 | 1 | 강제 master | 디버깅용 |
자동 모드에서의 결정 과정은 다음과 같습니다:
- 양단이 GBCR의 bit 11(MS Value)을 교환합니다. 하나가 1(master 선호)이고 다른 하나가 0(slave 선호)이면 즉시 역할이 결정됩니다.
- 양쪽 모두 같은 값(둘 다 master 선호 또는 둘 다 slave 선호)이면, 각 PHY가 생성한 10비트 난수(seed)를 비교하여 큰 쪽이 master가 됩니다.
- 난수까지 같으면(확률 1/1024) AN이 재시작됩니다.
ethtool eth0에서 master-slave cfg: preferred-master / master-slave status:로 현재 상태를 확인하세요.
# master/slave 상태 확인
ethtool eth0 | grep -i "master-slave"
# master-slave cfg: preferred-master
# master-slave status: master
# 강제 slave로 변경 (디버깅)
ethtool -s eth0 master-slave forced-slave
# GBCR(reg 0x09) 직접 확인
phytool read eth0/0/9
# bit 12:11 확인, bit 9: 1000FD 광고, bit 8: 1000HD 광고
# GBSR(reg 0x0A) — 협상 결과
phytool read eth0/0/10
# bit 14: master/slave config fault (양쪽 강제 동일 시 1)
# bit 13: local=master(1)/slave(0)
Auto-Negotiation 타이밍 파라미터
AN 완료까지 걸리는 시간은 고정값이 아니라, 매체 조건, PHY 구현, 교환할 페이지 수에 따라 달라집니다. 아래는 IEEE 802.3이 정의한 타이밍과 실측치입니다.
| 파라미터 | 규격값 | 실측 범위 | 설명 |
|---|---|---|---|
| break_link_timer | ≥ 75ms | 75~100ms | AN 시작 시 기존 링크를 끊고 대기하는 시간 |
| autoneg_wait_timer | 1000ms (최소) | 1~3초 | AN 미완료 시 parallel detect로 전환하기 전 대기 |
| link_fail_inhibit_timer | 750~1000ms | PHY 구현별 | AN 완료 후 실제 링크 업 전까지 안정화 대기 |
| FLP 교환 최소 시간 | ≈ 100ms | 50~200ms | Base Page 교환 + Ack (3회 일관) |
| Next Page 1쌍 교환 | ≈ 50ms | 48~100ms | 3회 일관 × 16ms |
| 1000BASE-T 링크 훈련 | — | 500ms~2초 | master/slave + equalizer 수렴 |
| AN 전체 (10/100M) | — | 1~3초 | 케이블 삽입부터 링크 업까지 |
| AN 전체 (1000BASE-T) | — | 2~5초 | equalizer 훈련 포함 |
| AN 전체 (2.5G/5GBASE-T) | — | 3~8초 | 고속일수록 훈련 시간 증가 |
dmesg에서 "Link is Up" 메시지의 타임스탬프를 보면 보드별 AN 소요 시간을 측정할 수 있습니다.
커널의 Auto-Negotiation 구현 흐름
리눅스 커널에서 AN은 크게 세 단계로 나뉩니다: 광고 설정, AN 시작, 결과 판독. 이 흐름은 phylib 상태 머신과 ethtool 인터페이스를 통해 제어됩니다.
/* AN 전체 흐름 요약 — 관련 커널 함수 */
/* 1단계: 광고 설정 + AN 시작 */
phy_ethtool_set_link_ksettings()
→ phydev->autoneg = AUTONEG_ENABLE;
→ linkmode_copy(phydev->advertising, ...);
→ phy_start_aneg(phydev);
→ phydev->drv->config_aneg(phydev); /* 대부분 genphy_config_aneg */
→ genphy_config_advert(phydev); /* ANAR, GBCR 기록 */
→ phy_modify(phydev, MII_BMCR, ..., BMCR_ANRESTART);
/* 2단계: 상태 머신 폴링 (1초 주기) */
phy_state_machine()
→ phydev->drv->read_status(phydev); /* 대부분 genphy_read_status */
→ genphy_update_link(phydev); /* BMSR.LINK_STATUS 확인 */
→ genphy_read_lpa(phydev); /* ANLPAR, GBSR 판독 */
→ genphy_read_master_slave(phydev); /* GBSR bit 14:13 판독 */
→ phydev->speed, phydev->duplex 갱신
/* 3단계: MAC 통보 */
phy_adjust_link(phydev->attached_dev);
→ MAC 드라이버의 adjust_link() 콜백
→ 속도 변경, duplex 변경, pause 설정 반영
genphy_update_link()는 첫 번째 읽기 결과가 0이면 한 번 더 읽어서 현재 실제 링크 상태를 확인합니다. 이 동작을 모르면 "링크가 살아 있는데 커널이 down으로 보고한다"는 오해가 생길 수 있습니다.
광고 비트 제어와 속도 제한 전략
AN을 유지하면서 특정 속도만 광고하는 것은, AN을 완전히 끄는 것보다 안전한 속도 제한 방법입니다. AN을 끄면 parallel detect 위험이 생기지만, 광고 비트만 조절하면 AN 프로토콜은 정상 동작하면서 원하는 속도로 합의할 수 있습니다.
# 현재 광고 확인
ethtool eth0 | grep -A5 "Advertised link modes"
# 100M Full만 광고 (AN은 유지)
ethtool -s eth0 autoneg on advertise 0x008
# 1000M Full만 광고
ethtool -s eth0 autoneg on advertise 0x020
# 100M + 1000M Full 광고 (10M 제외)
ethtool -s eth0 autoneg on advertise 0x028
# 모든 지원 속도 광고 복원
ethtool -s eth0 autoneg on advertise 0x03f
# advertise 비트맵 의미 (ethtool 비트)
# 0x001 = 10baseT Half
# 0x002 = 10baseT Full
# 0x004 = 100baseT Half
# 0x008 = 100baseT Full
# 0x010 = 1000baseT Half
# 0x020 = 1000baseT Full
# 0x1000 = 2500baseT Full (NBASE-T)
# 0x2000 = 5000baseT Full (NBASE-T)
advertise 0x008(100M Full만)로 설정하면 AN 프레임워크는 그대로 유지되면서 100M Full-duplex로 안전하게 합의합니다. Pause 협상도 정상 동작합니다.
Auto-Negotiation 실패 패턴과 체계적 디버깅
AN 관련 문제는 크게 네 가지 범주로 나뉩니다. 각 범주별 증상과 원인, 점검 순서를 정리합니다.
범주 1: AN 자체가 완료되지 않음
| 증상 | 가능한 원인 | 점검 방법 |
|---|---|---|
| BMSR.AN_COMPLETE가 영구적으로 0 | 상대편 PHY 비활성 또는 AN 미지원 | phytool read eth0/0/1 (BMSR), 상대편 전원/설정 확인 |
| AN 무한 재시작 (링크 flap) | master/slave 충돌, 심한 노이즈 | ethtool eth0 | grep master, GBSR bit 14(config fault) 확인 |
| AN 시간 초과 후 parallel detect | 상대편 AN off, FLP 미송신 | 상대편 autoneg 설정 확인 |
범주 2: AN 완료되나 기대와 다른 속도
| 증상 | 가능한 원인 | 점검 방법 |
|---|---|---|
| 1G 가능한데 100M로 링크 | 상대편이 1G 미광고, 케이블 4쌍 미결선 | ANLPAR(reg5) 읽기, 케이블 테스터로 쌍 확인 |
| 2.5G 포트가 1G로만 링크 | 상대편 NBASE-T 미지원, CAT-5 케이블 | ethtool eth0 | grep "Link partner" |
| 100M만 가능 (10M로도 안 내림) | 한쪽 광고 마스크가 100M만 허용 | 양단 advertise 비트맵 비교 |
범주 3: AN 완료되나 duplex mismatch
| 증상 | 가능한 원인 | 점검 방법 |
|---|---|---|
| late collision 폭증, 처리량 1/10 이하 | 한쪽 autoneg off → 상대 parallel detect → half duplex | 양단 duplex 비교, ethtool -S eth0 | grep collision |
| 간헐적 패킷 손실 (큰 프레임에서) | duplex mismatch + TCP 재전송으로 숨겨진 상태 | netstat -s | grep retrans, ethtool eth0 | grep Duplex |
범주 4: AN 완료되나 pause 관련 문제
| 증상 | 가능한 원인 | 점검 방법 |
|---|---|---|
| 대역폭 충분한데 간헐적 지연 | 불필요한 pause 프레임 수신 | ethtool -S eth0 | grep pause |
| pause 협상 실패 | 한쪽이 PAUSE=0/ASM_DIR=0 광고 | ethtool -a eth0, ANAR/ANLPAR 비트 10:11 비교 |
# AN 문제 체계적 디버깅 순서
# 1. 로컬 PHY 상태 확인
ethtool eth0
# Speed, Duplex, Autoneg, Link detected, master-slave status 확인
# 2. 광고/상대편 능력 비교
ethtool eth0 | grep -E "Supported|Advertised|Link partner"
# 3. 핵심 레지스터 직접 판독
phytool read eth0/0/0 # BMCR: AN enable? speed? duplex?
phytool read eth0/0/1 # BMSR: AN complete? link status?
phytool read eth0/0/4 # ANAR: 로컬 광고
phytool read eth0/0/5 # ANLPAR: 상대 광고
phytool read eth0/0/6 # ANER: AN 확장 상태
phytool read eth0/0/9 # GBCR: 1G 광고 + master/slave
phytool read eth0/0/10 # GBSR: 1G 결과 + config fault
# 4. 카운터로 증상 수치화
ethtool -S eth0 | grep -E "crc|collision|error|pause|drop"
# 5. 링크 flap 감시
ip -ts monitor link dev eth0
# 6. dmesg에서 PHY 이벤트 확인
dmesg | grep -i "link\|phy\|eth0"
NBASE-T (2.5G/5G/10GBASE-T) Auto-Negotiation 고려사항
NBASE-T(IEEE 802.3bz)는 기존 CAT-5e/6 케이블에서 1G 이상의 속도를 제공하기 위해 개발되었습니다. AN 관점에서 NBASE-T는 기존 Clause 28/40 프레임워크를 확장하여 2.5G와 5G 능력을 광고합니다.
| 속도 | 케이블 요구 | 광고 방법 | Master/Slave |
|---|---|---|---|
| 2.5GBASE-T | CAT-5e (100m) | Clause 45 MMD 7, reg 32 bit 7 | Clause 40 확장 (동일 메커니즘) |
| 5GBASE-T | CAT-5e (100m, 일부 CAT-6 권장) | Clause 45 MMD 7, reg 32 bit 8 | Clause 40 확장 |
| 10GBASE-T | CAT-6a (100m) / CAT-6 (55m) | Clause 45 MMD 7, reg 32 bit 12 | Clause 40 확장 |
NBASE-T AN의 핵심 차이점:
- 다단계 폴백(Downshift): 10G → 5G → 2.5G → 1G → 100M 순서로 자동 강등됩니다. downshift 횟수와 대상 속도는 PHY 벤더별 레지스터로 설정합니다.
- Clause 45 접근 필수: 2.5G/5G 능력 광고 레지스터는 Clause 22로는 접근할 수 없고, Clause 45 MMD(MDIO Manageable Device) 7번을 통해야 합니다. 커널에서는
genphy_c45_an_config_aneg()를 사용합니다. - 훈련 시간 증가: 고속일수록 echo cancellation과 crosstalk 제거가 복잡해져 링크 훈련 시간이 길어집니다. 10GBASE-T는 최대 5~10초까지 걸릴 수 있습니다.
# 2.5G/5G 광고 레지스터 확인 (Clause 45, MMD 7 reg 32)
phytool read eth0/7/32
# bit 7: 2.5GBASE-T 광고, bit 8: 5GBASE-T 광고
# 상대편 NBASE-T 능력 (MMD 7 reg 33)
phytool read eth0/7/33
# ethtool로 확인
ethtool eth0 | grep "2500\|5000\|10000"
# downshift 설정 확인 (PHY 벤더별)
# Realtek: phytool read eth0/0/20 (page 0xa43)
# Marvell: phytool read eth0/7/0x8001
EEE와 Auto-Negotiation의 상호작용
EEE(Energy Efficient Ethernet, IEEE 802.3az)는 AN의 Next Page 메커니즘을 통해 협상되지만, 실제 EEE 능력 레지스터는 Clause 45 MMD 7에 별도로 존재합니다. AN 과정에서 EEE가 어떻게 처리되는지 이해하면 EEE 관련 링크 불안정 문제를 풀 수 있습니다.
- AN 시작 전: 커널은
genphy_config_eee_advert()를 호출하여 MMD 7 레지스터 60(EEE Advertisement)에 지원 모드를 기록합니다. - AN 진행 중: PHY 하드웨어가 Base Page 교환 후 Next Page를 통해 EEE 능력을 교환합니다. 이 과정은 PHY 내부에서 자동으로 처리됩니다.
- AN 완료 후: MMD 7 레지스터 61(EEE LP Ability)에서 상대편 EEE 능력을 확인할 수 있습니다. 양단 모두 지원하는 속도에서만 EEE가 활성화됩니다.
- LPI(Low Power Idle) 진입: 링크 업 이후 트래픽이 없으면 PHY가 LPI 모드로 진입하여 전력을 절감합니다. 복귀 시간(wake time)은 100BASE-TX에서 30µs, 1000BASE-T에서 16.5µs입니다.
ethtool --set-eee eth0 eee off로 EEE를 끄고 증상이 개선되는지 확인하세요. 임베디드 장비에서는 EEE를 기본적으로 끄는 것이 일반적입니다.
Auto-Negotiation 관련 레지스터 종합 참조표
AN 디버깅에 필요한 모든 레지스터를 한 곳에 모았습니다. Clause 22(기본)와 Clause 45(확장)로 구분합니다.
| 레지스터 | 번호 | 이름 | 핵심 비트 | 용도 |
|---|---|---|---|---|
| — Clause 22 기본 레지스터 — | ||||
| BMCR | 0x00 | Basic Mode Control | bit 12: AN Enable, bit 9: AN Restart | AN 시작/정지 제어 |
| BMSR | 0x01 | Basic Mode Status | bit 5: AN Complete, bit 3: AN Ability, bit 2: Link Status | AN 완료 여부, 링크 상태 |
| ANAR | 0x04 | AN Advertisement | bit 10:11: Pause, bit 5~9: Technology | 로컬 광고 설정 |
| ANLPAR | 0x05 | AN Link Partner Ability | (ANAR과 동일 구조) | 상대편 광고 판독 |
| ANER | 0x06 | AN Expansion | bit 4: Parallel Detect Fault, bit 1: Page Received | AN 확장 상태 |
| ANNPTR | 0x07 | AN Next Page Transmit | bit 15: NP, bit 13: MP, bit 10:0: Code | Next Page 송신 |
| ANNPRR | 0x08 | AN Next Page Receive | (ANNPTR과 동일 구조) | Next Page 수신 |
| GBCR | 0x09 | 1000BASE-T Control | bit 12:11: MS 설정, bit 9: 1G FD, bit 8: 1G HD | 1G 광고 + master/slave |
| GBSR | 0x0A | 1000BASE-T Status | bit 14: MS Config Fault, bit 13: Master/Slave | 1G 협상 결과 |
| — Clause 45 확장 레지스터 (MMD 7: AN) — | ||||
| AN Control | 7.0 | AN Control | bit 12: AN Enable, bit 9: AN Restart | Clause 45 AN 제어 |
| AN Status | 7.1 | AN Status | bit 5: AN Complete, bit 3: AN Ability | Clause 45 AN 상태 |
| EEE Adv | 7.60 | EEE Advertisement | bit 1: 100TX EEE, bit 2: 1G EEE, bit 3: 10G EEE | EEE 광고 |
| EEE LP | 7.61 | EEE LP Ability | (EEE Adv와 동일 구조) | 상대편 EEE 능력 |
| NBASE-T Adv | 7.32 | 10G/NBASE-T AN Control | bit 7: 2.5G, bit 8: 5G, bit 12: 10G | NBASE-T 광고 |
| NBASE-T LP | 7.33 | 10G/NBASE-T AN Status | (7.32와 동일 구조) | 상대편 NBASE-T 능력 |
autoneg off로 고정하면 상대편이 평행 감지(parallel detect)로 링크를 올리면서 duplex를 잘못 해석할 수 있습니다.
이런 경우 링크는 올라오지만 late collision, carrier error, 심한 처리량(Throughput) 저하가 발생합니다. 양쪽 정책을 반드시 짝으로 맞추세요.
# 현재 링크 협상 결과
ethtool eth0
# 자동 협상 끄고 강제 설정
ethtool -s eth0 speed 1000 duplex full autoneg off
# EEE 상태 확인
ethtool --show-eee eth0
# 링크 업/다운 이벤트 실시간 감시
ip monitor link
협상 디버깅에서 중요한 것은 "로컬이 무엇을 광고했고 상대편이 무엇을 응답했는가"입니다. 단순히 속도 결과만 보면 중간 원인을 놓칩니다. 예를 들어 2.5G 포트가 1G로만 붙는다면 케이블 품질 문제일 수도 있지만, 상대편의 advertisement mask가 1G까지만 켜져 있는 정책 문제일 수도 있습니다.
Parallel Detect와 강제 모드의 위험
Auto-Negotiation이 실패하거나 상대편이 AN을 지원하지 않을 때, PHY는 Parallel Detect로 폴백(Fallback)합니다. 이 모드는 상대편이 보내는 신호의 패턴만으로 속도를 감지하되, duplex 정보는 교환하지 않습니다. 결과적으로 한쪽은 full duplex, 다른 쪽은 half duplex로 동작하는 duplex mismatch가 발생할 수 있습니다.
| 로컬 설정 | 상대편 설정 | 결과 | 증상 |
|---|---|---|---|
| autoneg on | autoneg on | 정상 협상 | 없음 |
| autoneg on | autoneg off (1G full) | parallel detect → 1G half | late collision, 처리량 저하 |
| autoneg off (1G full) | autoneg off (1G full) | 강제 1G full | 동작하지만 pause 미협상 |
| autoneg off (100M full) | autoneg on | parallel detect → 100M half | duplex mismatch |
# 양단 광고 능력 비교 — 로컬
ethtool eth0 | grep -E "Supported|Advertised|Link partner"
# 특정 속도만 광고하도록 제한 (AN 유지)
ethtool -s eth0 autoneg on advertise 0x020 # 1000baseT Full만
# master/slave 역할 확인 (1000BASE-T 이상)
ethtool eth0 | grep -i "master"
Auto-Negotiation Off: 강제 모드 상세 동작과 조건
Auto-Negotiation을 비활성화하면 PHY는 BMCR(Basic Mode Control Register)의 비트 12(AN Enable)를 0으로 클리어하고, 비트 13(Speed Selection MSB), 비트 6(Speed Selection LSB), 비트 8(Duplex Mode)의 조합으로 속도와 듀플렉스를 직접 지정합니다. FLP(Fast Link Pulse) 교환이 중단되므로 상대편과 어떤 능력 정보도 주고받지 않습니다.
BMCR 강제 모드 비트 구성
AN을 끄면 BMCR의 속도/듀플렉스 비트가 직접 PHY 동작을 결정합니다. AN Enable(비트 12)이 1일 때는 이 비트들이 무시되지만, 0이 되면 즉시 유효해집니다.
| BMCR 비트 | 이름 | 값 | 의미 |
|---|---|---|---|
| bit 13 + bit 6 | Speed Selection | 00 | 10 Mbps |
| bit 13 + bit 6 | Speed Selection | 01 | 100 Mbps |
| bit 13 + bit 6 | Speed Selection | 10 | 1000 Mbps |
| bit 8 | Duplex Mode | 0 | Half Duplex |
| bit 8 | Duplex Mode | 1 | Full Duplex |
| bit 12 | AN Enable | 0 | 강제 모드 활성 (위 비트 유효) |
| bit 9 | Restart AN | 무시됨 | AN이 꺼져 있으면 효과 없음 |
커널 구현: genphy_setup_forced()
사용자가 ethtool -s eth0 autoneg off speed 1000 duplex full을 실행하면, 커널은 phy_ethtool_sset() → phy_start_aneg() → config_aneg() 경로를 타고, phydev->autoneg == AUTONEG_DISABLE이면 genphy_setup_forced()를 호출합니다. 이 함수는 BMCR에서 AN Enable 비트를 클리어하고 speed/duplex 비트를 직접 세트합니다.
/* drivers/net/phy/phy_device.c — 강제 모드 레지스터 설정 */
int genphy_setup_forced(struct phy_device *phydev)
{
u16 ctl = 0;
phydev->pause = 0;
phydev->asym_pause = 0;
/* speed 비트 설정 */
if (phydev->speed == SPEED_1000)
ctl |= BMCR_SPEED1000;
else if (phydev->speed == SPEED_100)
ctl |= BMCR_SPEED100;
/* SPEED_10이면 두 비트 모두 0 */
/* duplex 비트 설정 */
if (phydev->duplex == DUPLEX_FULL)
ctl |= BMCR_FULLDPLX;
/* BMCR 쓰기: AN Enable 클리어, speed/duplex 적용 */
return phy_modify(phydev, MII_BMCR,
BMCR_ANENABLE | BMCR_SPEED1000 |
BMCR_SPEED100 | BMCR_FULLDPLX, ctl);
}
phy_sanitize_settings()는 genphy_setup_forced() 호출 전에 실행되어, 요청된 speed/duplex 조합이 PHY의 supported 비트맵에 존재하는지 검증합니다. 지원하지 않는 조합이면 가능한 최고 속도로 자동 하향 조정합니다.
/* drivers/net/phy/phy.c — 강제 모드 설정값 검증 */
void phy_sanitize_settings(struct phy_device *phydev)
{
const struct phy_setting *setting;
/* speed/duplex 조합이 supported에 있는지 확인 */
setting = phy_find_valid(phydev->speed, phydev->duplex,
phydev->supported);
if (setting) {
phydev->speed = setting->speed;
phydev->duplex = setting->duplex;
} else {
/* 유효한 조합이 없으면 최저 속도로 폴백 */
phydev->speed = SPEED_UNKNOWN;
phydev->duplex = DUPLEX_UNKNOWN;
}
}
속도별 강제 모드 제약 조건
모든 이더넷(Ethernet) 속도가 동일하게 강제 모드를 지원하는 것은 아닙니다. 물리 계층의 특성에 따라 속도별로 제약 사항이 크게 다릅니다.
| 속도 | 강제 모드 지원 | 제약 및 주의사항 |
|---|---|---|
| 10BASE-T | 완전 지원 | NLP(Normal Link Pulse)만 사용하므로 AN 없이도 안정적으로 동작합니다. Manchester 인코딩은 클럭 복원이 간단하여 양단 강제 설정이 가장 문제없는 속도입니다. |
| 100BASE-TX | 완전 지원 | MLT-3 인코딩과 4B5B 코딩으로 별도의 훈련 과정 없이 동작합니다. 양단 강제 설정 시 가장 많이 사용되는 속도입니다. |
| 1000BASE-T | 제한적 지원 | Master/Slave 역할 결정이 필요합니다. 정상적인 AN에서는 Clause 28의 확장으로 master/slave를 자동 결정하지만, AN을 끄면 양단 모두 PHY가 기본값(보통 slave)으로 고정됩니다. 양쪽 모두 같은 역할이면 링크가 올라오지 않습니다. 한쪽은 반드시 master, 다른 쪽은 slave로 수동 설정해야 합니다. |
| 2.5GBASE-T / 5GBASE-T | PHY 벤더 의존 | NBASE-T Alliance(현 MGBASE-T) 표준은 AN을 전제로 설계되었습니다. 일부 PHY는 강제 모드를 벤더 특정 레지스터로 지원하지만, 표준 BMCR 비트만으로는 2.5G/5G 속도를 지정할 수 없습니다(BMCR speed 비트 조합에 해당 값이 없음). |
| 10GBASE-T | 실질적 불가 | DSP 기반 128-DSQ 변조와 LDPC 코딩으로 반드시 링크 트레이닝(Link Training)이 필요합니다. AN 없이는 equalizer 계수를 교환할 수 없어 물리적으로 링크를 올릴 수 없습니다. Clause 55는 AN을 필수(mandatory)로 규정합니다. |
| 25G/40G/100G+ | 불가 | Clause 73 AN과 Clause 72 링크 트레이닝이 필수입니다. SerDes 기반 인터페이스에서 equalizer 튜닝 없이는 BER(Bit Error Rate)이 허용 범위를 초과합니다. |
# 1000BASE-T 강제 모드 — 양단 설정 예시
# 장비 A (master로 설정)
ethtool -s eth0 speed 1000 duplex full autoneg off
phytool write eth0/0/9 0x1800 # GBCR: manual M/S enable + master
# 장비 B (slave로 설정)
ethtool -s eth0 speed 1000 duplex full autoneg off
phytool write eth0/0/9 0x1000 # GBCR: manual M/S enable + slave
# master/slave 상태 확인
ethtool eth0 | grep -i "master"
강제 모드에서 상실되는 기능
AN을 끄면 FLP 교환이 중단되므로, AN의 Base Page와 Next Page를 통해 협상되던 여러 기능이 자동으로 비활성화됩니다. 이 영향은 단순한 속도/듀플렉스 고정보다 훨씬 광범위합니다.
| 기능 | AN 활성 시 | AN 비활성(강제 모드) 시 | 영향 |
|---|---|---|---|
| Pause (흐름 제어) | Base Page bit 10/11로 협상 | 협상 불가, 양단 모두 비활성 | 수신 버퍼 오버플로 시 프레임 드롭 증가. 특히 버스트 트래픽이 많은 환경에서 패킷 손실이 악화됩니다. |
| Asymmetric Pause | ASM_DIR 비트로 방향 협상 | 협상 불가 | 단방향 흐름 제어가 불가능합니다. DCB(Data Center Bridging) 환경에서는 PFC도 영향을 받습니다. |
| EEE (Energy Efficient Ethernet) | Next Page 또는 LLDP로 협상 | 협상 불가, EEE 비활성 | PHY가 LPI(Low Power Idle) 상태에 진입하지 않아 유휴 시 전력 소모가 증가합니다. 서버/데이터센터에서는 의도적으로 EEE를 끄는 경우가 많으므로 이것이 오히려 장점일 수 있습니다. |
| Master/Slave 역할 | preference + 난수로 자동 결정 | 수동 설정 필수 (1G 이상) | 수동 설정을 빠뜨리면 링크 자체가 올라오지 않습니다. |
| Downshift | PHY가 자동으로 낮은 속도 재시도 | 비활성 (고정 속도) | 케이블 품질이 나빠도 지정 속도에서만 재시도하므로 링크 실패가 지속됩니다. |
| Remote Fault 통지 | Base Page bit 13으로 전달 | 전달 불가 | 상대편 PHY 오류를 감지할 수 없어 장애 원인 추적이 어려워집니다. |
| Next Page 확장 능력 | FEC, 추가 속도, OUI 메시지 | 전달 불가 | 고속 환경에서 FEC 협상이 안 되면 BER이 증가합니다. |
강제 모드를 사용해야 하는 정당한 사유
대부분의 환경에서 AN on이 권장되지만, AN을 끄는 것이 올바른 선택인 구체적인 시나리오가 있습니다.
| 시나리오 | 사유 | 설정 시 주의사항 |
|---|---|---|
| 관리형 스위치의 정책 강제 | 네트워크 관리자가 포트별 속도/듀플렉스를 중앙에서 통제할 때. 협상 결과의 불확실성을 제거합니다. | 반드시 양단을 같은 값으로 설정합니다. 스위치 포트와 서버 NIC 양쪽 모두 변경해야 합니다. |
| 구형 장비 호환 | AN을 지원하지 않거나 구현이 불완전한 레거시(Legacy) PHY와 연결할 때. 일부 초기 100BASE-TX PHY는 AN 상태 머신에 버그가 있습니다. | 100M full duplex 이하로 강제합니다. 1G 이상은 구형 장비에서 지원하지 않을 가능성이 높습니다. |
| 미디어 컨버터 경유 | Copper-to-Fiber 미디어 컨버터가 AN을 중계하지 못하는 경우. 컨버터가 AN을 종단(terminate)하여 양단이 독립적으로 협상합니다. | 컨버터 양쪽 세그먼트를 각각 독립적으로 설정합니다. Copper 측과 Fiber 측의 AN 정책이 달라야 할 수 있습니다. |
| 산업용/임베디드 전용 링크 | 점대점(Point-to-Point) 고정 연결에서 링크 업 시간을 최소화할 때. AN 과정(수백 ms~수 초)을 생략하면 링크 복구 시간이 단축됩니다. | 10/100M에서만 권장합니다. 1G에서는 master/slave 설정이 추가로 필요합니다. |
| AN 디버깅 격리 | AN 관련 문제를 배제하고 물리 계층 문제만 분리 진단할 때. 강제 모드로 링크가 올라오면 AN 구현 문제, 올라오지 않으면 케이블/PHY 하드웨어 문제입니다. | 진단 후 반드시 AN on으로 원복합니다. 강제 모드를 운영 환경에 그대로 두지 않습니다. |
| PXE/IPMI 부트 환경 | 일부 BMC(Baseboard Management Controller)의 네트워크 스택이 AN을 제대로 처리하지 못해 BIOS/UEFI 단계에서 강제 설정이 필요한 경우가 있습니다. | OS 부팅 후에는 드라이버가 AN on으로 재설정하는지 확인합니다. |
강제 모드 적용 전 체크리스트
- 양단 동일 설정: speed, duplex, autoneg off가 양쪽 모두 일치하는지 확인합니다. 한쪽이라도 autoneg on이면 parallel detect로 duplex mismatch가 발생합니다.
- 1G 이상이면 master/slave: 1000BASE-T 강제 모드에서는 한쪽을 master, 다른 쪽을 slave로 수동 설정합니다. 미설정 시 링크 자체가 올라오지 않습니다.
- pause 대체 수단: 흐름 제어가 필요한 환경이라면 AN 없이는 pause를 협상할 수 없으므로, 양단에서
ethtool -A eth0 rx on tx on으로 수동 활성화하거나 충분한 버퍼를 확보합니다. - EEE 영향 검토: AN을 끄면 EEE도 자동 비활성화됩니다. 전력 절감이 중요한 환경에서는 영향을 평가합니다.
- 2.5G 이상 불가: NBASE-T(2.5G/5G) 이상 속도에서는 AN이 필수입니다. BMCR 비트로 해당 속도를 지정할 수 없으며, 링크 트레이닝 없이는 물리적으로 통신이 불가능합니다.
- 원복 계획: 강제 모드는 진단이나 특수 상황을 위한 임시 설정입니다. 변경 이유와 원복 절차를 문서화합니다.
강제 모드 설정과 검증 절차
# === 1단계: 현재 상태 기록 (원복용) ===
ethtool eth0 | tee /tmp/eth0-before.txt
# === 2단계: 강제 모드 적용 (양단 동시) ===
# 100M full duplex 강제 (가장 안전한 강제 설정)
ethtool -s eth0 speed 100 duplex full autoneg off
# === 3단계: 링크 상태 확인 ===
ethtool eth0 | grep -E "Speed|Duplex|Auto-negotiation|Link detected"
# 기대 출력:
# Speed: 100Mb/s
# Duplex: Full
# Auto-negotiation: off
# Link detected: yes
# === 4단계: duplex mismatch 징후 점검 ===
ethtool -S eth0 | grep -iE "collision|error|drop|carrier"
# late_collision이 증가하면 상대편 duplex 설정이 다른 것임
# === 5단계: pause 상태 확인 ===
ethtool -a eth0
# 강제 모드에서는 Autonegotiate: off, RX/TX: off로 표시됨
# 필요하면 수동 활성화:
ethtool -A eth0 rx on tx on
# === 원복 ===
ethtool -s eth0 autoneg on
# AN이 재시작되며 수 초 내에 최적 속도로 링크가 올라옴
커널의 강제 모드 상태 판독 경로
강제 모드에서는 AN이 꺼져 있으므로 ANLPAR(Link Partner Advertisement Register)을 읽을 수 없습니다. 대신 genphy_read_status_fixed()가 BMCR을 직접 읽어 현재 speed/duplex를 결정합니다.
/* drivers/net/phy/phy_device.c — 강제 모드 상태 판독 */
int genphy_read_status_fixed(struct phy_device *phydev)
{
int bmcr = phy_read(phydev, MII_BMCR);
if (bmcr < 0)
return bmcr;
/* BMCR speed 비트 해석 */
if (bmcr & BMCR_SPEED1000)
phydev->speed = SPEED_1000;
else if (bmcr & BMCR_SPEED100)
phydev->speed = SPEED_100;
else
phydev->speed = SPEED_10;
/* BMCR duplex 비트 해석 */
phydev->duplex = (bmcr & BMCR_FULLDPLX) ?
DUPLEX_FULL : DUPLEX_HALF;
/* pause는 AN 결과가 아니므로 0으로 유지 */
phydev->pause = 0;
phydev->asym_pause = 0;
return 0;
}
이 경로에서 pause와 asym_pause가 항상 0으로 설정되는 것을 주목하세요. 강제 모드에서는 AN이 수행되지 않았으므로 pause 능력 교환이 없고, 커널은 흐름 제어를 비활성으로 보고합니다. 이것이 ethtool -a에서 "Autonegotiate: off"로 표시되는 이유입니다.
# 1G full만 광고 — AN 유지, 속도 사실상 고정
ethtool -s eth0 autoneg on advertise 0x020
# 100M full만 광고
ethtool -s eth0 autoneg on advertise 0x008
비대칭 AN 설정: 한쪽 On / 한쪽 Off의 동작 메커니즘
실무에서 가장 빈번하고 진단이 어려운 문제는 한쪽은 AN on, 다른 쪽은 AN off인 비대칭 설정입니다. 이 조합에서 링크가 올라오기는 하지만 duplex mismatch가 거의 확실하게 발생하며, 겉으로는 정상처럼 보이기 때문에 문제를 인지하기까지 수주가 걸리는 경우도 있습니다. IEEE 802.3 Clause 28.2.3.1에 정의된 Parallel Detection 메커니즘이 이 상황의 핵심입니다.
Parallel Detection의 정확한 동작 원리
AN이 활성화된 PHY(이하 "A측")는 FLP(Fast Link Pulse) 버스트를 지속적으로 송신하면서 상대편의 FLP 응답을 기다립니다. 그런데 AN이 꺼진 PHY(이하 "B측")는 FLP를 송신하지 않고, 속도에 따라 다른 종류의 신호만 보냅니다.
| B측 강제 속도 | B측이 보내는 신호 | A측 PHY의 감지 방식 | A측의 해석 결과 |
|---|---|---|---|
| 10BASE-T | NLP (16ms 간격 단일 펄스) | FLP 대신 NLP만 수신됨을 감지 | 10BASE-T로 판단, duplex는 half로 가정 |
| 100BASE-TX | Idle 심볼 (4B5B + MLT-3) | 100MHz 대역 에너지 감지 | 100BASE-TX로 판단, duplex는 half로 가정 |
| 1000BASE-T | 없음 (AN 없이는 훈련 불가) | 감지 불가 또는 비표준 동작 | 링크 실패 또는 PHY 벤더 특정 동작 |
핵심은 Parallel Detection이 속도만 감지하고 duplex 정보는 전달받지 못한다는 점입니다. IEEE 802.3은 Parallel Detection 시 duplex를 half duplex로 기본 설정하도록 규정합니다. 그런데 B측은 자신이 full duplex로 강제 설정되어 있으므로, 양단의 duplex가 불일치하게 됩니다.
속도별 비대칭 AN 결과 상세
비대칭 설정의 결과는 B측(AN off)의 강제 속도에 따라 크게 달라집니다. 아래 표는 A측이 항상 AN on(전체 속도 광고)일 때의 각 시나리오를 정리합니다.
| B측 강제 설정 | A측 Parallel Detection 결과 | 실제 양단 상태 | 링크 가능 여부 | 세부 증상 |
|---|---|---|---|---|
| 10M full | 10M half | A: 10M half / B: 10M full | 링크 성공 | 저속이라 증상이 경미합니다. 간헐적 collision이 발생하지만 10M 환경에서는 재전송으로 흡수됩니다. 부하가 낮으면 문제를 인지하기 어렵습니다. |
| 10M half | 10M half | A: 10M half / B: 10M half | 링크 성공 | 유일하게 정상 동작하는 비대칭 조합입니다. Parallel Detection이 half로 가정하고 B측도 half이므로 일치합니다. 단, pause 협상은 여전히 불가합니다. |
| 100M full | 100M half | A: 100M half / B: 100M full | 링크 성공 | 가장 흔한 duplex mismatch 시나리오입니다. A측이 half duplex로 동작하므로 B측이 동시에 송신하면 A측에서 late collision이 발생합니다. 부하가 낮을 때는 정상처럼 보이다가 트래픽이 증가하면 급격한 처리량 저하가 나타납니다. |
| 100M half | 100M half | A: 100M half / B: 100M half | 링크 성공 | duplex는 일치하지만, half duplex 자체의 한계(CSMA/CD 오버헤드)로 실효 처리량이 full duplex의 50~60% 수준입니다. pause 협상도 불가합니다. |
| 1000M full | 감지 실패 또는 비표준 | 불확정 | 대부분 링크 실패 | IEEE 802.3은 1000BASE-T에 대한 Parallel Detection을 표준으로 정의하지 않습니다. 일부 PHY 벤더는 자체 구현으로 감지를 시도하지만, master/slave 역할이 결정되지 않아 링크가 올라오지 않습니다. 링크가 올라와도 극도로 불안정합니다. |
| 1000M half | 해당 없음 | 해당 없음 | 불가 | IEEE 802.3ab는 1000BASE-T half duplex를 정의하지 않습니다. 대부분의 PHY가 이 조합을 거부합니다. |
Duplex Mismatch의 구체적 메커니즘
duplex mismatch가 왜 late collision을 유발하는지 이해하려면 half duplex와 full duplex의 근본적인 차이를 알아야 합니다.
| 동작 | Half Duplex (A측) | Full Duplex (B측) | 충돌 시나리오 |
|---|---|---|---|
| 송신 전 | 캐리어 감지(Carrier Sense) 수행 — 매체가 유휴일 때만 송신 시작 | 캐리어 감지 없음 — 언제든 송신 가능 | A측이 매체를 감시하는 동안 B측은 무관하게 송신합니다. |
| 동시 송수신 | 불가 — 하나의 방향만 사용 | 가능 — 송수신 독립 경로 사용 | B측이 A측 송신 중에 동시에 보내면, A측은 이를 충돌로 인식합니다. |
| 충돌 감지 | 활성 — 충돌 시 jam 신호 후 백오프(backoff) | 비활성 — 충돌 개념 자체가 없음 | A측이 jam 신호를 보내고 재전송을 기다리지만, B측은 jam을 무시하고 계속 송신합니다. |
| 프레임 크기 제한 | slot time(512비트, 100M 기준 51.2μs) 이내 충돌은 normal collision | 제한 없음 | slot time 이후의 충돌은 late collision으로 분류되며, 이는 반드시 비정상입니다. |
ethtool -S eth0 | grep -i late에서 카운터가 증가하면 즉시 양단 duplex 설정을 비교해야 합니다. Late collision으로 드롭된 프레임은 MAC 계층에서 재전송하지 않으므로 TCP 재전송에 의존하게 되어 지연이 크게 증가합니다.
비대칭 AN의 처리량 영향 분석
duplex mismatch 상태에서의 처리량 저하는 트래픽 패턴에 따라 크게 달라집니다.
| 트래픽 패턴 | 정상 AN (100M full) | duplex mismatch (A: half / B: full) | 저하 비율 | 원인 |
|---|---|---|---|---|
| 단방향 소량 (1 Mbps) | ~1 Mbps | ~1 Mbps | 거의 없음 | half duplex에서도 충돌 확률이 낮아 정상처럼 동작합니다. 이것이 문제를 늦게 발견하는 원인입니다. |
| 단방향 대량 (A→B, 90 Mbps) | ~94 Mbps | ~60~70 Mbps | 25~35% | A측(half)이 송신 중 B측의 동시 송신을 충돌로 인식하여 백오프가 발생합니다. |
| 양방향 대칭 (50 Mbps 씩) | ~94 Mbps (합산) | ~15~30 Mbps (합산) | 70~85% | A측(half)이 한 방향만 사용할 수 있어 양방향 동시 전송 시 충돌이 극심합니다. late collision으로 프레임 드롭이 빈발합니다. |
| 양방향 버스트 (간헐적 대량) | ~94 Mbps | ~10~25 Mbps | 75~90% | 최악의 시나리오입니다. B측의 버스트 송신이 A측의 송신 윈도와 지속적으로 겹쳐 late collision이 폭증합니다. |
비대칭 AN 상태의 진단 방법
# === 1단계: 양단 AN 상태 비교 ===
# A측 (의심되는 장비)
ethtool eth0 | grep -E "Speed|Duplex|Auto-negotiation|Link partner"
# 출력 예시:
# Speed: 100Mb/s
# Duplex: Half ← half이면 의심
# Auto-negotiation: on
# Link partner advertised link modes: Not reported ← 상대가 AN off인 증거
# === 2단계: 카운터 확인 (결정적 증거) ===
ethtool -S eth0 | grep -iE "late.?coll|coll|fcs|align|carrier"
# late_collision 카운터가 증가 중이면 duplex mismatch 확정
# 10초 간격으로 두 번 읽어 증가율 확인:
ethtool -S eth0 | grep late; sleep 10; ethtool -S eth0 | grep late
# === 3단계: 상대편 설정 확인 (접근 가능할 때) ===
# B측 (상대편 장비)
ethtool eth0 | grep -E "Speed|Duplex|Auto-negotiation"
# 출력 예시:
# Speed: 100Mb/s
# Duplex: Full ← A측과 불일치!
# Auto-negotiation: off ← 원인
# === 4단계: 스위치 포트 확인 (상대가 스위치일 때) ===
# Cisco IOS
# show interface GigabitEthernet0/1 | include duplex|collision
# Juniper JunOS
# show interfaces ge-0/0/1 extensive | match "duplex|collision"
비대칭 AN의 해결 방법 (우선순위 순)
| 우선순위 | 방법 | 조치 내용 | 장점 | 단점 |
|---|---|---|---|---|
| 1 (권장) | 양단 모두 AN on | B측에서 ethtool -s eth0 autoneg on | pause, master/slave, EEE 모두 정상 협상됩니다. 가장 안전한 해결책입니다. | B측 장비에 접근 권한이 필요합니다. |
| 2 | AN on + advertisement 제한 | A측: ethtool -s eth0 autoneg on advertise 0x008 (100M full만 광고) | AN을 유지하면서 속도를 사실상 고정합니다. B측이 AN on이면 해당 속도로 정상 협상됩니다. | B측이 AN off이면 여전히 Parallel Detection으로 빠집니다(duplex mismatch 미해결). |
| 3 | 양단 모두 AN off | 양쪽: ethtool -s eth0 speed 100 duplex full autoneg off | duplex mismatch를 확실히 해소합니다. | pause, EEE 협상 불가. 1G 이상이면 master/slave 수동 설정 필수입니다. |
| 4 | A측도 AN off로 맞춤 | A측을 B측과 동일하게 강제 설정 | B측을 변경할 수 없을 때 유일한 방법입니다. | A측의 모든 AN 기능이 상실됩니다. 반드시 B측과 speed/duplex를 정확히 일치시켜야 합니다. |
speed 100 duplex full로 강제하면 해당 포트의 AN이 꺼집니다. 이때 상대편 서버 NIC가 AN on(기본값)이면 Parallel Detection → duplex mismatch가 발생합니다. 스위치 포트를 강제하면 상대편 NIC도 반드시 같은 값으로 강제하거나, 스위치 포트도 AN on 상태에서 advertisement만 제한하는 것이 안전합니다. 대부분의 스위치는 speed auto 100 같은 advertisement 제한 명령을 지원합니다.
커널의 Parallel Detection 처리 경로
커널의 AN 상태 머신이 Parallel Detection을 어떻게 처리하는지, 코드 경로를 추적합니다. A측(AN on)에서 상대편의 FLP 응답이 없을 때의 흐름입니다.
/* genphy_read_status() — AN 완료 후 파트너 정보 판독 */
/* Parallel Detection으로 링크가 올라온 경우의 동작 */
if (phydev->autoneg == AUTONEG_ENABLE && phydev->link) {
err = genphy_read_lpa(phydev);
/*
* genphy_read_lpa()는 ANLPAR(레지스터 5)을 읽음.
* Parallel Detection 시 상대편이 FLP를 보내지 않았으므로
* ANLPAR = 0x0000 (파트너 광고 없음).
*
* 이 경우 lp_advertising 비트맵이 비어 있고,
* phy_resolve_aneg_linkmode()가 AND 연산 시
* 공통 모드를 찾지 못함.
*
* PHY 하드웨어가 Parallel Detection으로 이미 결정한
* 속도를 BMCR/BMSR에서 반영하되, duplex는 half로 설정됨.
*/
phy_resolve_aneg_linkmode(phydev);
/*
* ANLPAR이 비어 있으면:
* - speed: PHY 하드웨어가 감지한 값 (Parallel Detection 결과)
* - duplex: HALF (파트너 duplex 정보 없음 → 보수적 기본값)
* - pause: 0 (파트너 pause 능력 알 수 없음)
*/
}
ethtool의 출력에서 "Link partner advertised link modes"가 "Not reported"이면 상대편이 AN에 참여하지 않았다는 뜻이며, Parallel Detection으로 링크가 올라온 것입니다. 이 상태에서 duplex가 "Half"이고 상대편이 "Full"로 강제되어 있다면 duplex mismatch가 확정됩니다. 커널 로그(dmesg | grep -i duplex)에서도 mismatch 경고를 확인할 수 있으며, 일부 NIC 드라이버는 netdev_warn()으로 이 상황을 명시적으로 경고합니다.
비대칭 AN에서 흔히 간과되는 부차적 문제
| 문제 | 원인 | 영향 | 진단 방법 |
|---|---|---|---|
| TCP 성능 비대칭 | 한 방향(A→B)은 half duplex 제약을 받고 반대 방향(B→A)은 받지 않습니다. | 방향에 따라 처리량이 2~5배 차이납니다. 업로드/다운로드 속도 불균형으로 나타납니다. | iperf3 -c <peer>와 iperf3 -c <peer> -R로 양방향 비교 |
| ARP 타임아웃 간헐 발생 | ARP 요청/응답이 late collision으로 드롭되면 IP 통신 자체가 간헐적으로 실패합니다. | ping이 간헐적으로 실패하고, SSH 연결이 끊어집니다. 물리 계층 문제를 네트워크 계층 문제로 오진하기 쉽습니다. | ip -s neigh show에서 STALE/FAILED 빈도 확인 |
| VLAN 태그 충돌 증폭 | 802.1Q 태그로 프레임이 4바이트 커지면 slot time 경계에서의 충돌 확률이 높아집니다. | VLAN 환경에서 더 심한 성능 저하가 나타납니다. | VLAN 인터페이스와 물리 인터페이스의 error 카운터 비교 |
| Jumbo Frame 완전 실패 | half duplex에서는 slot time 대비 프레임이 너무 커서 late collision이 거의 모든 프레임에서 발생합니다. | MTU 9000에서 실효 처리량이 거의 0에 가깝습니다. | ping -s 8000 -M do <peer>로 대형 패킷 전달 가능 여부 확인 |
비대칭 AN 발생 원인별 현실적 시나리오
비대칭 AN은 실무에서 주로 다음과 같은 상황에서 의도치 않게 발생합니다.
| 시나리오 | 발생 경위 | 진단이 늦어지는 이유 | 예방 조치 |
|---|---|---|---|
| 스위치 교체 후 | 기존 스위치는 AN on이었는데, 새 스위치가 포트별 기본 설정이 AN off인 경우. 일부 산업용/관리형 스위치의 기본값이 100M full 강제입니다. | 기존 링크가 정상이었으므로 NIC 측 설정은 확인하지 않습니다. 링크가 올라오니 "정상 교체"로 판단합니다. | 스위치 교체 후 ethtool eth0에서 duplex와 "Link partner advertised"를 반드시 확인합니다. |
| NIC 드라이버 업데이트 | 드라이버 업데이트 시 기본 AN 설정이 변경되거나, 이전에 적용된 /etc/network/ 설정이 누락되는 경우. | 드라이버 업데이트는 "네트워크 설정 변경"으로 인식되지 않아 AN 상태를 확인하지 않습니다. | 드라이버 업데이트 후 ethtool eth0 결과를 업데이트 전과 비교합니다. |
| VM 마이그레이션 | 물리 서버 간 마이그레이션 시 목적지 서버의 물리 NIC AN 설정이 출발지와 다른 경우. | VM 내부에서는 가상 NIC만 보이므로 물리 계층 문제를 인지하기 어렵습니다. | 하이퍼바이저 수준에서 물리 NIC의 ethtool 출력을 모니터링합니다. |
| 미디어 컨버터 삽입 | Copper-Fiber 컨버터가 AN을 종단하여, 컨버터 양쪽이 독립적인 AN 도메인이 됩니다. 컨버터의 Copper 측이 AN off로 기본 설정된 경우. | 컨버터는 "투명한 장비"로 인식되어 AN 정책을 확인하지 않습니다. | 컨버터 삽입 시 양쪽 세그먼트에서 각각 ethtool을 확인합니다. |
| BIOS/UEFI PXE 부팅 후 | PXE 부팅 중 BIOS가 NIC를 100M full 강제로 설정하고, OS 드라이버가 로드되기 전 스위치와 Parallel Detection으로 링크가 올라옵니다. OS 드라이버가 AN on으로 재설정하지 못하는 경우. | 부팅은 성공하므로 "정상"으로 판단합니다. 느린 성능을 소프트웨어 문제로 오진합니다. | OS 부팅 후 ethtool eth0에서 AN 상태를 확인하는 스크립트를 시작 시 실행합니다. |
# 1. late_collision 카운터 증가율 감시 (0이어야 정상)
ethtool -S eth0 | awk '/late.?coll/{print $2}'
# 2. duplex가 half인 인터페이스 탐지
ethtool eth0 | grep "Duplex: Half" && echo "WARNING: half duplex detected on eth0"
# 3. AN on인데 Link partner advertised가 비어 있으면 Parallel Detection
ethtool eth0 | grep -A1 "Auto-negotiation: on" | grep "Not reported"
# 4. 전체 인터페이스 일괄 점검 스크립트
for iface in $(ls /sys/class/net/ | grep -v lo); do
duplex=$(ethtool "$iface" 2>/dev/null | awk '/Duplex:/{print $2}')
an=$(ethtool "$iface" 2>/dev/null | awk '/Auto-negotiation:/{print $2}')
[ "$duplex" = "Half" ] && [ "$an" = "on" ] && \
echo "ALERT: $iface — AN on but half duplex (possible parallel detect)"
done
AN 트리거 조건과 재시작 메커니즘
Auto-Negotiation은 PHY가 특정 이벤트를 감지했을 때 자동으로 시작되거나, 소프트웨어가 명시적으로 재시작할 수 있습니다. AN이 언제, 왜 시작되는지를 정확히 이해하면 링크 flap의 원인 분석이 훨씬 수월해집니다.
| 트리거 | 메커니즘 | AN 재시작 여부 | 설명 |
|---|---|---|---|
| 케이블 삽입 | PHY 하드웨어 자동 | 예 (전체 AN) | 링크 파트너 전압 감지 → FLP 송신 시작. PHY의 전원이 켜진 상태에서 케이블이 연결되면 수백 ms 이내에 AN이 시작됩니다. |
| PHY 리셋 | BMCR bit 15 (Reset) | 예 (전체 AN) | 모든 레지스터가 기본값으로 복원되고, AN Enable이 기본 1이므로 자동으로 AN이 시작됩니다. |
| AN Restart | BMCR bit 9 (Restart_AN) | 예 (재협상) | 기존 링크를 끊고 break_link_timer(≥75ms) 후 새로 FLP를 교환합니다. 광고 레지스터 변경 후 반드시 이 비트를 세트해야 합니다. |
| Power-Down 복귀 | BMCR bit 11 (Power_Down) 해제 | 예 (전체 AN) | PHY가 저전력 모드에서 깨어나면 리셋과 유사하게 AN이 재시작됩니다. |
| 링크 실패 감지 | PHY 하드웨어 자동 | PHY 구현별 | signal loss, excessive CRC 등 감지 시 일부 PHY가 자동 재협상합니다. Downshift 기능이 활성화되면 낮은 속도로 재시도합니다. |
| ethtool -s 명령 | 커널 → config_aneg() | 변경 시에만 | phy_modify_changed()로 광고 비트가 실제로 달라졌을 때만 AN을 재시작하여 불필요한 링크 끊김을 방지합니다. |
| phy_start() 호출 | MAC 드라이버 | 예 | 네트워크 인터페이스 up 시 MAC 드라이버가 호출합니다. phy_start_aneg()를 통해 AN을 시작합니다. |
| suspend/resume | 시스템 전원 관리 | 예 (resume 시) | resume 후 phy_init_hw() → config_init() → config_aneg() 순서로 PHY를 재초기화합니다. |
/* AN 재시작의 핵심: phy_start_aneg() — drivers/net/phy/phy.c */
int phy_start_aneg(struct phy_device *phydev)
{
int err;
if (!phydev->drv)
return -EIO;
mutex_lock(&phydev->lock);
if (!__phy_is_started(phydev)) {
WARN(1, "called from state %s\n",
phy_state_to_str(phydev->state));
err = -EBUSY;
goto out;
}
/* AN 미지원이면 강제 모드로 전환 */
if (AUTONEG_DISABLE == phydev->autoneg)
phy_sanitize_settings(phydev);
/* PHY 드라이버의 config_aneg 콜백 호출 */
err = phydev->drv->config_aneg(phydev);
if (err < 0)
goto out;
/* 상태를 AN으로 전환 — 상태 머신이 완료를 감시 */
if (phydev->autoneg == AUTONEG_ENABLE) {
err = phy_check_link_status(phydev);
}
out:
mutex_unlock(&phydev->lock);
return err;
}
phy_modify_changed()는 이 문제를 방지하기 위해 레지스터 값이 실제로 달라졌을 때만 쓰기를 수행합니다.
ethtool eth0 | grep -i "downshift"로 현재 설정을 확인하고, ethtool --set-phy-tunable eth0 downshift on count 3으로 재시도 횟수를 설정할 수 있습니다.
linkmode 비트맵과 MDIO 레지스터 매핑
커널의 phydev->advertising은 unsigned long 배열로 구현된 linkmode 비트맵이며, 이것이 실제 PHY 레지스터(ANAR, GBCR 등)로 변환되는 과정을 이해하면 AN 동작의 전체 그림이 보입니다. 변환은 양방향으로 이루어집니다: 설정 시에는 linkmode → 레지스터, 판독 시에는 레지스터 → linkmode입니다.
/* 변환 함수 체인 — include/linux/mii.h 기반 개념 재현 */
/* 1. linkmode → ANAR (설정 경로) */
static inline u32 linkmode_adv_to_lcl_adv_t(
const unsigned long *advertising)
{
u32 lcl_adv = 0;
if (linkmode_test_bit(ETHTOOL_LINK_MODE_10baseT_Half_BIT, advertising))
lcl_adv |= ADVERTISED_10baseT_Half;
if (linkmode_test_bit(ETHTOOL_LINK_MODE_10baseT_Full_BIT, advertising))
lcl_adv |= ADVERTISED_10baseT_Full;
if (linkmode_test_bit(ETHTOOL_LINK_MODE_100baseTX_Half_BIT, advertising))
lcl_adv |= ADVERTISED_100baseT_Half;
if (linkmode_test_bit(ETHTOOL_LINK_MODE_100baseTX_Full_BIT, advertising))
lcl_adv |= ADVERTISED_100baseT_Full;
/* Pause 비트도 동일 패턴 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_Pause_BIT, advertising))
lcl_adv |= ADVERTISED_Pause;
if (linkmode_test_bit(ETHTOOL_LINK_MODE_Asym_Pause_BIT, advertising))
lcl_adv |= ADVERTISED_Asym_Pause;
return lcl_adv;
}
static inline u32 ethtool_adv_to_mii_adv_t(u32 ethadv)
{
u32 result = 0;
/* ethtool 비트 → MII 레지스터 비트로 위치 변환 */
if (ethadv & ADVERTISED_10baseT_Half)
result |= ADVERTISE_10HALF; /* ANAR bit 5 */
if (ethadv & ADVERTISED_10baseT_Full)
result |= ADVERTISE_10FULL; /* ANAR bit 6 */
if (ethadv & ADVERTISED_100baseT_Half)
result |= ADVERTISE_100HALF; /* ANAR bit 7 */
if (ethadv & ADVERTISED_100baseT_Full)
result |= ADVERTISE_100FULL; /* ANAR bit 8 */
if (ethadv & ADVERTISED_Pause)
result |= ADVERTISE_PAUSE_CAP; /* ANAR bit 10 */
if (ethadv & ADVERTISED_Asym_Pause)
result |= ADVERTISE_PAUSE_ASYM; /* ANAR bit 11 */
return result;
}
/* 2. ANLPAR → linkmode (판독 경로) — genphy_read_lpa() 내부 */
static inline u32 mii_lpa_to_ethtool_lpa_t(u32 lpa)
{
u32 result = 0;
/* MII 레지스터 비트 → ethtool 비트로 역변환 */
if (lpa & LPA_10HALF) result |= ADVERTISED_10baseT_Half;
if (lpa & LPA_10FULL) result |= ADVERTISED_10baseT_Full;
if (lpa & LPA_100HALF) result |= ADVERTISED_100baseT_Half;
if (lpa & LPA_100FULL) result |= ADVERTISED_100baseT_Full;
if (lpa & LPA_PAUSE_CAP) result |= ADVERTISED_Pause;
if (lpa & LPA_PAUSE_ASYM) result |= ADVERTISED_Asym_Pause;
return result;
}
/* 3. 결과 해석 — phy_resolve_aneg_linkmode() */
void phy_resolve_aneg_linkmode(struct phy_device *phydev)
{
__ETHTOOL_DECLARE_LINK_MODE_MASK(common);
/* 교집합: 로컬 advertising ∩ 상대편 lp_advertising */
linkmode_and(common, phydev->advertising, phydev->lp_advertising);
/* 우선순위에 따라 speed/duplex 결정 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT, common)) {
phydev->speed = SPEED_1000;
phydev->duplex = DUPLEX_FULL;
} else if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Half_BIT, common)) {
phydev->speed = SPEED_1000;
phydev->duplex = DUPLEX_HALF;
} else if (linkmode_test_bit(ETHTOOL_LINK_MODE_100baseTX_Full_BIT, common)) {
phydev->speed = SPEED_100;
phydev->duplex = DUPLEX_FULL;
} else if (linkmode_test_bit(ETHTOOL_LINK_MODE_100baseTX_Half_BIT, common)) {
phydev->speed = SPEED_100;
phydev->duplex = DUPLEX_HALF;
} else {
phydev->speed = SPEED_10;
phydev->duplex = linkmode_test_bit(
ETHTOOL_LINK_MODE_10baseT_Full_BIT, common)
? DUPLEX_FULL : DUPLEX_HALF;
}
/* Pause 해석 — Annex 28B 알고리즘 */
phy_resolve_aneg_pause(phydev);
}
u32 기반의 ethtool_link_ksettings 비트맵을 사용했지만, 2.5G/5G/25G/100G 등 새로운 속도가 추가되면서 32비트로는 부족해졌습니다.
이를 해결하기 위해 __ETHTOOL_LINK_MODE_MASK_NBITS까지 확장 가능한 unsigned long 배열 기반의 linkmode 비트맵이 도입되었습니다.
현재 커널에서는 linkmode_* 계열 함수가 표준이며, 구형 u32 인터페이스는 호환 래퍼를 통해 유지됩니다.
1000BASE-T PHY Training(링크 훈련) 상세
10/100BASE-T에서는 AN이 완료되면 곧바로 데이터 전송이 가능하지만, 1000BASE-T는 AN 완료 후 추가적인 PHY Training 단계가 필요합니다. 이 훈련 과정은 4쌍 양방향 PAM-5 부호화의 복잡성에서 비롯됩니다.
| 속도 | 사용 쌍 수 | 부호화 | 통신 방향 | 훈련 필요 |
|---|---|---|---|---|
| 10BASE-T | 2쌍 (TX 1, RX 1) | 맨체스터(Manchester) | 단방향 (쌍별) | 불필요 |
| 100BASE-TX | 2쌍 (TX 1, RX 1) | MLT-3 + NRZI | 단방향 (쌍별) | 불필요 |
| 1000BASE-T | 4쌍 (모두 양방향) | 4D-PAM5 | 양방향 동시 (쌍별) | 필수 |
| 2.5GBASE-T | 4쌍 (모두 양방향) | PAM-16 (DSQ128) | 양방향 동시 | 필수 (더 복잡) |
| 5GBASE-T | 4쌍 (모두 양방향) | PAM-16 (DSQ128) | 양방향 동시 | 필수 |
| 10GBASE-T | 4쌍 (모두 양방향) | PAM-16 (DSQ128) | 양방향 동시 | 필수 (가장 복잡) |
1000BASE-T의 핵심 문제는 같은 쌍(pair)으로 동시에 송신과 수신을 한다는 것입니다. 이를 위해 PHY 내부의 DSP(Digital Signal Processor)가 여러 간섭 성분을 제거해야 합니다:
| 간섭 유형 | 원인 | 제거 기법 | 필터 계수 | 수렴 시간 |
|---|---|---|---|---|
| Echo | 동일 쌍에서 자기 TX 신호가 RX에 반사 | Hybrid(하이브리드) 회로 + DSP Echo Canceller | ~128 탭 FIR | 50~200ms |
| NEXT | 인접 3쌍의 로컬 TX가 RX에 혼입 | 3채널 NEXT Canceller (로컬 TX 신호 참조) | 3 × ~48 탭 FIR | 100~300ms |
| FEXT | 인접 3쌍의 원격 TX가 케이블 내에서 혼입 | 3채널 FEXT Canceller (수신 심볼 기반 추정) | 3 × ~16 탭 FIR | 200~500ms |
| ISI | 케이블 주파수 응답으로 심볼 간 간섭 | FFE(Feed-Forward EQ) + DFE(Decision Feedback EQ) | FFE ~24탭 + DFE ~8탭 | 100~300ms |
ethtool eth0에서 Speed가 기대보다 낮으면, dmesg | grep -i "downshift\|link"로 강등 이력을 확인하세요.
# PHY Training 관련 진단
# 1. SNR(Signal-to-Noise Ratio) 확인 — 일부 PHY만 지원
# Marvell PHY: 페이지 5, 레지스터 21~24 (쌍별 SNR)
phytool write eth0/0/22 0x0005 # 페이지 5 선택
phytool read eth0/0/21 # Pair 0 SNR (dB 단위)
phytool read eth0/0/22 # Pair 1 SNR
phytool read eth0/0/23 # Pair 2 SNR
phytool read eth0/0/24 # Pair 3 SNR
# SNR > 20dB: 양호 | 15~20dB: 경계 | < 15dB: 불안정
# 2. 케이블 길이 추정 — TDR(Time Domain Reflectometry)
ethtool --cable-test eth0
# 또는 PHY 벤더별 TDR 레지스터 직접 접근
# 3. 심볼 에러 카운터
ethtool -S eth0 | grep -i "symbol\|phy_err\|frame"
# 심볼 에러가 지속 증가하면 Training 수렴이 불완전
# 4. Downshift 발생 여부 확인
ethtool eth0 | grep -i "downshift"
dmesg | grep -i "downshift"
# 5. PHY Training 시간 측정
# AN restart부터 link up까지의 dmesg 타임스탬프 차이
ethtool -r eth0 # AN 재시작
dmesg -w | grep "Link is"
# 1G: 보통 1~3초, 100M: 보통 0.5~1.5초
Clause 37 In-Band Auto-Negotiation 구현 상세
Clause 37은 1000BASE-X(광섬유) 및 SGMII(구리, MAC-PHY 간) 환경에서 사용되는 SerDes 기반 In-Band AN입니다. Clause 28이 별도의 FLP 펄스를 사용하는 것과 달리, Clause 37은 데이터 경로 자체(8B/10B 인코딩의 /C/ ordered set)를 통해 설정 정보를 교환합니다.
| 구분 | Clause 28 (구리) | Clause 37 (SerDes) |
|---|---|---|
| 물리적 매체 | 트위스트 페어(UTP) | 광섬유 또는 SerDes 백보드 |
| 신호 방식 | FLP 전압 펄스 | 8B/10B /C/ ordered set |
| 정보량 | 16비트 Base Page | 16비트 Config Reg |
| 속도 협상 | 10/100/1000M 선택 | 고정 (1000M 또는 SGMII 확장) |
| 협상 대상 | speed, duplex, pause | duplex, pause (speed 고정) |
| 상태 머신 | 7개 상태 | 5개 상태 (AN_ENABLE, ABILITY, ACK, IDLE, LINK_OK) |
| 하드웨어 위치 | PHY 아날로그단 | PCS(Physical Coding Sublayer) |
Clause 37의 Config Register(16비트)는 /C1/ 또는 /C2/ ordered set에 실려 전송됩니다. 이 ordered set는 8B/10B 인코딩에서 데이터와 구분되는 특수 심볼(/K28.5/)로 시작하므로, 데이터 전송 중에도 링크 상태를 교환할 수 있습니다.
/* Clause 37 AN 구현 — drivers/net/pcs/pcs-lynx.c 참조 (개념 재현) */
/* PCS에서 Clause 37 AN 설정 */
static int lynx_pcs_config_cl37(struct phylink_pcs *pcs,
const unsigned long *advertising)
{
u16 config_reg = 0;
/* 1000BASE-X: speed 협상 없음, duplex + pause만 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseX_Full_BIT, advertising))
config_reg |= ADVERTISE_1000XFULL; /* bit 5 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_Pause_BIT, advertising))
config_reg |= ADVERTISE_1000XPAUSE; /* bit 7 */
if (linkmode_test_bit(ETHTOOL_LINK_MODE_Asym_Pause_BIT, advertising))
config_reg |= ADVERTISE_1000XPSE_ASYM; /* bit 8 */
/* PCS AN Advertisement 레지스터에 기록 (MII_ADVERTISE, reg 4) */
mdiodev_modify(pcs->mdio, MII_ADVERTISE,
ADVERTISE_1000XFULL | ADVERTISE_1000XHALF |
ADVERTISE_1000XPAUSE | ADVERTISE_1000XPSE_ASYM,
config_reg);
/* BMCR에서 AN 활성화 + 재시작 */
mdiodev_modify(pcs->mdio, MII_BMCR,
BMCR_ANENABLE | BMCR_ANRESTART,
BMCR_ANENABLE | BMCR_ANRESTART);
return 0;
}
/* SGMII AN 결과 판독 — PHY→MAC 방향 Control Word 해석 */
static void lynx_pcs_get_sgmii_state(struct phylink_pcs *pcs,
struct phylink_link_state *state)
{
int lpa = mdiodev_read(pcs->mdio, MII_LPA); /* ANLPAR */
/* SGMII Control Word 해석 */
state->link = !!(lpa & LPA_SGMII_LINK); /* bit 15 */
state->duplex = (lpa & LPA_SGMII_DPX_SPD_1000)
? DUPLEX_FULL : DUPLEX_HALF; /* bit 12 */
/* Speed: bit 11:10 */
switch (lpa & LPA_SGMII_SPEED) {
case LPA_SGMII_10:
state->speed = SPEED_10;
break;
case LPA_SGMII_100:
state->speed = SPEED_100;
break;
case LPA_SGMII_1000:
state->speed = SPEED_1000;
break;
}
}
phylink_pcs 드라이버의 pcs_config()와 pcs_get_state() 콜백을 통해 제어됩니다.
pcs_config()은 AN 광고를 설정하고, pcs_get_state()는 AN 결과를 읽어 phylink_link_state에 채웁니다.
phylink은 이 정보를 MAC 드라이버의 mac_config()/mac_link_up() 콜백에 전달하여 MAC 속도를 맞춥니다.
이 구조 덕분에 PHY 드라이버(copper AN)와 PCS 드라이버(SerDes AN)가 동시에 동작하는 복잡한 토폴로지도 처리할 수 있습니다.
phy-mode Device Tree 속성이 정확해야 하는 이유입니다.
AN 시퀀스 타임라인: 실제 교환 시나리오
Auto-Negotiation의 전체 과정을 시간 순서대로 추적하면, 각 단계에서 어떤 신호가 오가고 어떤 조건으로 다음 단계로 진행되는지 명확해집니다. 아래는 1000BASE-T 환경에서 NIC와 스위치가 연결되는 전형적인 시나리오입니다.
| 단계 | 소요 시간 | 하드웨어/소프트웨어 | 실패 시 결과 |
|---|---|---|---|
| 전압 감지 + AN 시작 | 0~16ms | PHY 하드웨어 | 케이블 미연결 → BMSR.LINK=0 |
| FLP Base Page 교환 | 16~80ms | PHY 하드웨어 | FLP 미수신 → Parallel Detect 대기 |
| Ack + Consistency Check | 80~130ms | PHY 하드웨어 | 노이즈로 불일치 → AN 재시작 |
| Next Page (GBCR, EEE) | 130~200ms | PHY 하드웨어 | NP 미지원 → 1G 미광고 → 100M 폴백 |
| Master/Slave 결정 | AN 완료 직후 | PHY 하드웨어 | 양쪽 force-master → config fault |
| Echo/NEXT Canceller 수렴 | 200~500ms | PHY DSP | 수렴 실패 → AN 재시작 (downshift) |
| FEXT/ISI Equalizer 수렴 | 500ms~1.5s | PHY DSP | BER 초과 → 링크 불안정 |
| link_fail_inhibit_timer | ~750ms | PHY 하드웨어 | (안정화 대기, 실패 아님) |
| 커널 폴링 → MAC 설정 | ~1s (폴링 주기) | 커널 소프트웨어 | IRQ 모드면 즉시 감지 |
# AN 시퀀스 추적 체크리스트
# 1단계: AN 시작 확인 — BMCR 읽기
phytool read eth0/0/0
# bit 12(AN_ENABLE)=1, bit 9(AN_RESTART) 확인
# AN 시작 직후에는 bit 9가 1이었다가 PHY가 자동으로 0으로 클리어
# 2단계: Base Page 교환 확인 — BMSR, ANAR, ANLPAR 읽기
phytool read eth0/0/1 # BMSR: bit 5(AN_COMPLETE) 확인
phytool read eth0/0/4 # ANAR: 내가 무엇을 광고했는지
phytool read eth0/0/5 # ANLPAR: 상대가 무엇을 광고했는지
# ANLPAR이 0x0000이면 상대가 응답하지 않은 것
# 3단계: 1G 확장 확인 — GBCR, GBSR 읽기
phytool read eth0/0/9 # GBCR: bit 9(1G-FD 광고), bit 12:11(MS 설정)
phytool read eth0/0/10 # GBSR: bit 14(MS config fault), bit 13(master/slave)
# bit 14=1이면 양쪽 모두 같은 역할을 강제한 것
# 4단계: 결과 확인
ethtool eth0
# Speed, Duplex, Autoneg, Link detected, master-slave status 전체 확인
# 5단계: Training 성공 확인 — 에러 카운터
ethtool -S eth0 | grep -E "crc|symbol|error"
# Training 직후 symbol error가 잠깐 나올 수 있으나, 지속되면 문제
# 6단계: 타이밍 측정
ethtool -r eth0 && dmesg -w | grep "Link is"
# AN restart부터 Link Up까지 시간 확인
# 10/100M: 1~2초 | 1000M: 2~4초 | 5초 이상: 케이블/PHY 문제
AN 완료 인터럽트와 폴링 전환 메커니즘
PHY가 AN 완료를 커널에 알리는 방법은 인터럽트(IRQ)와 폴링(Polling) 두 가지입니다. 어떤 방식이 사용되는지는 하드웨어 설계와 PHY 드라이버에 따라 달라지며, 두 방식의 차이는 링크 감지 지연 시간에 직접 영향을 줍니다.
| 방식 | 지연 | 조건 | 장점 | 단점 |
|---|---|---|---|---|
| 인터럽트 (IRQ) | 수 µs | PHY IRQ 핀이 SoC GPIO에 연결, DT에 interrupts 속성 존재 | AN 완료 즉시 감지, CPU 부하 없음 | 하드웨어 배선 필요, shared IRQ 시 복잡 |
| 폴링 (Timer) | 최대 1초 | IRQ 미연결 또는 PHY 드라이버가 PHY_POLL 사용 | 하드웨어 의존성 없음, 항상 동작 | 1초 간격으로 MDIO 읽기, 감지 지연 |
/* PHY 인터럽트 vs 폴링 — drivers/net/phy/phy.c 참조 */
/* 인터럽트 모드: PHY가 링크 변화를 IRQ로 통보 */
static irqreturn_t phy_interrupt(int irq, void *phy_dat)
{
struct phy_device *phydev = phy_dat;
/* PHY 드라이버의 handle_interrupt 콜백 호출 */
if (phydev->drv->handle_interrupt)
return phydev->drv->handle_interrupt(phydev);
/* 레거시: did_interrupt + ack_interrupt */
if (phydev->drv->did_interrupt &&
!phydev->drv->did_interrupt(phydev))
return IRQ_NONE;
/* threaded IRQ에서 상태 갱신 예약 */
phy_trigger_machine(phydev);
return IRQ_HANDLED;
}
/* 폴링 모드: 1초 주기로 상태 확인 */
static void phy_state_machine(struct work_struct *work)
{
struct phy_device *phydev = ...;
mutex_lock(&phydev->lock);
/* BMSR 읽기 → link/speed/duplex 갱신 */
phy_check_link_status(phydev);
mutex_unlock(&phydev->lock);
/* 다음 폴링 예약 (PHY_STATE_TIME = 1초) */
if (phy_polling_mode(phydev))
phy_queue_state_machine(phydev, PHY_STATE_TIME);
}
/* phy_polling_mode() 판단 기준 */
static inline bool phy_polling_mode(struct phy_device *phydev)
{
/* IRQ가 PHY_POLL이면 폴링 모드 */
if (phydev->irq == PHY_POLL)
return true;
/* MAC이 in-band status를 사용하면 PHY 폴링 불필요 */
if (phydev->drv->flags & PHY_IS_INTERNAL)
return false;
return false;
}
(1)
config_intr(): PHY의 인터럽트 마스크 레지스터에서 "link change" 이벤트를 활성화합니다.
(2)
handle_interrupt(): 인터럽트 발생 시 PHY의 인터럽트 상태 레지스터를 읽어 원인을 확인하고, link change이면 phy_trigger_machine()을 호출합니다.
이 콜백이 없으면 phydev->irq = PHY_POLL로 설정되어 1초 폴링으로 폴백합니다.
/* Realtek RTL8211F의 인터럽트 처리 예시 */
static int rtl8211f_config_intr(struct phy_device *phydev)
{
int val;
if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
/* Link change, AN complete, speed change 인터럽트 활성화 */
val = phy_read(phydev, RTL8211F_INER);
val |= RTL8211F_INER_LINK_STATUS; /* link change */
return phy_write(phydev, RTL8211F_INER, val);
} else {
/* 모든 인터럽트 비활성화 */
return phy_write(phydev, RTL8211F_INER, 0);
}
}
static irqreturn_t rtl8211f_handle_interrupt(struct phy_device *phydev)
{
int val;
/* 인터럽트 상태 레지스터 읽기 (읽으면 자동 클리어) */
val = phy_read(phydev, RTL8211F_INSR);
if (val < 0)
return IRQ_NONE;
/* link change 비트가 세트되었으면 상태 머신 트리거 */
if (val & RTL8211F_INSR_LINK_STATUS) {
phy_trigger_machine(phydev);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
interrupts 또는 interrupt-parent 속성이 있으면 커널이 자동으로 IRQ 모드를 사용합니다.
이 속성이 없으면 PHY_POLL로 폴백됩니다. 인터럽트 배선이 되어 있는데도 폴링 모드로 동작한다면, DT 속성 누락이 원인일 가능성이 높습니다.
/* Device Tree: PHY 인터럽트 설정 예시 */
&mdio {
phy0: ethernet-phy@0 {
reg = <0>;
/* IRQ 모드 — AN 완료를 즉시 감지 */
interrupt-parent = <&gpio1>;
interrupts = <25 IRQ_TYPE_LEVEL_LOW>;
};
};
/* 위 속성이 없으면 → PHY_POLL → 1초 간격 폴링 */
MAC-PHY 인터페이스 모드
phy-mode는 매체 종류가 아니라 MAC과 PHY 사이의 디지털 인터페이스 계약을 뜻합니다. 이 값이 틀리면 링크가 아예 안 올라오거나, 올라와도 CRC와 symbol error가 폭증합니다. 특히 RGMII 계열은 지연 보상 위치를 명확히 해야 하고, SGMII/1000BASE-X/2500BASE-X는 in-band status 처리 여부까지 맞아야 합니다.
| 모드 | 형태 | 주요 속도 | 핵심 주의점 |
|---|---|---|---|
| MII / RMII | 병렬 | 10/100M | 저속 임베디드, 핀 수와 기준 클럭 확인 |
| GMII / RGMII | 병렬 | 1G | RGMII는 2ns 지연을 한 곳에서만 추가 |
| SGMII / QSGMII | 직렬 SerDes | 1G, 4x1G | in-band status와 PCS 설정 동기화 |
| 1000BASE-X / 2500BASE-X | 직렬 SerDes | 1G, 2.5G | 광 모듈, switch port, PCS 모드 일치 필요 |
| USXGMII | 직렬 SerDes | 10/5/2.5/1G | 멀티기가비트 통합, MAC과 PHY 모두 지원 필요 |
rgmii는 내부 지연 없음, rgmii-id는 TX/RX 둘 다 PHY가 지연, rgmii-txid와 rgmii-rxid는 한 방향만 PHY가 지연한다는 뜻입니다.
PCB 지연을 이미 넣은 설계에서 rgmii-id를 추가로 쓰면 이중 지연이 발생합니다.
phy-mode 선택 가이드
phy-mode는 보드 설계에 이미 결정되어 있는 물리적 연결을 소프트웨어에 알려 주는 설정이므로, "무엇이 더 좋은가"가 아니라 "회로도와 일치하는가"가 유일한 기준입니다. 회로도 확인이 어려운 경우에는 다음 순서로 추론할 수 있습니다.
- SoC 데이터시트에서 해당 포트의 지원 인터페이스 목록을 확인합니다.
- PHY 데이터시트에서 해당 SoC 포트와 연결된 핀이 RGMII인지 SGMII인지 확인합니다.
- 클럭 연결을 봅니다. RGMII는 125MHz 참조 클럭이 별도로 있고, SGMII/SerDes는 차동 클럭 또는 CDR 내장입니다.
- 기존 벤더 BSP의 Device Tree 설정을 참고하되, 벤더 실수가 있을 수 있으므로 실물 회로도와 교차 확인합니다.
RGMII DDR 타이밍과 지연 보상 상세
RGMII(Reduced Gigabit Media Independent Interface)는 125MHz 클럭을 사용하여 상승 에지와 하강 에지 모두에서 데이터를 전송하는 DDR(Double Data Rate) 방식입니다. 클럭 주기는 8ns이며, 상승/하강 에지 간격은 4ns입니다. IEEE 802.3 표준과 RGMII 사양은 데이터 유효 윈도우를 클럭 에지 기준으로 setup 시간 약 1.5ns, hold 시간 약 1.5ns로 규정합니다.
문제는 MAC이 클럭과 데이터를 동시에 출력한다는 점입니다. 클럭 에지가 데이터 천이(transition) 순간과 정확히 일치하면 수신 측 PHY는 데이터가 변하는 순간을 샘플링하게 되어 신뢰할 수 없는 결과를 읽습니다. 이를 해결하기 위해 클럭을 2ns 지연시켜 에지가 데이터 유효 구간의 중앙에 오도록 보정합니다.
RGMII 서브모드 상세
Device Tree에서 phy-mode와 rx-internal-delay-ps, tx-internal-delay-ps 속성을 조합하거나, 레거시 방식으로 아래 4가지 서브모드 문자열을 직접 지정합니다. 각 서브모드가 내부 지연을 어느 방향에 적용하는지 정확히 이해해야 이중 적용을 방지할 수 있습니다.
| phy-mode 값 | TX 지연 | RX 지연 | 의미 | 적합한 상황 |
|---|---|---|---|---|
rgmii | 없음 | 없음 | PHY 내부 지연 없음 | MAC 또는 PCB가 양방향 지연을 모두 담당하는 경우 |
rgmii-id | PHY 내부 | PHY 내부 | PHY가 TX·RX 양방향 모두 2ns 지연 삽입 | MAC도 PCB도 지연을 제공하지 않는 경우 |
rgmii-txid | PHY 내부 | 없음 | PHY가 TX 방향만 2ns 지연 삽입 | RX는 MAC 또는 PCB가 담당하는 경우 |
rgmii-rxid | 없음 | PHY 내부 | PHY가 RX 방향만 2ns 지연 삽입 | TX는 MAC 또는 PCB가 담당하는 경우 |
커널의 RGMII 지연 결정 흐름
커널은 Device Tree 속성을 읽어 PHY 드라이버에 요청할 지연 값을 결정합니다. phy_get_internal_delay() 함수는 rx-internal-delay-ps 또는 tx-internal-delay-ps 속성을 피코초 단위로 읽고, PHY 드라이버가 제공하는 지원 지연 테이블과 가장 가까운 값을 선택합니다.
/* drivers/net/phy/phy.c (개념 흐름 — 실제 함수 시그니처 요약) */
/* Device Tree 속성에서 피코초 단위 지연을 읽는다 */
int phy_get_internal_delay(struct phy_device *phydev,
struct device *dev,
const int *delay_values, /* PHY 지원 테이블 */
int size,
bool is_rx)
{
/* "rx-internal-delay-ps" 또는 "tx-internal-delay-ps" 읽기 */
u32 delay_ps;
if (device_property_read_u32(dev,
is_rx ? "rx-internal-delay-ps"
: "tx-internal-delay-ps",
&delay_ps))
return -EINVAL; /* 속성 없음 → 레거시 phy-mode 문자열로 결정 */
/* PHY 지원 테이블에서 가장 가까운 값 선택 */
for (int i = 0; i < size - 1; i++) {
if (delay_ps <= (delay_values[i] + delay_values[i + 1]) / 2)
return i;
}
return size - 1;
}
/* PHY 드라이버 probe 시 호출 예시 */
static int my_phy_config_init(struct phy_device *phydev)
{
/* rgmii-id / rgmii-txid → TX 내부 지연 활성화 */
bool tx_delay = phy_interface_is_rgmii(phydev->interface) &&
(phydev->interface != PHY_INTERFACE_MODE_RGMII) &&
(phydev->interface != PHY_INTERFACE_MODE_RGMII_RXID);
/* rgmii-id / rgmii-rxid → RX 내부 지연 활성화 */
bool rx_delay = phy_interface_is_rgmii(phydev->interface) &&
(phydev->interface != PHY_INTERFACE_MODE_RGMII) &&
(phydev->interface != PHY_INTERFACE_MODE_RGMII_TXID);
my_phy_set_delay(phydev, tx_delay, rx_delay);
return 0;
}
지연 삽입 위치와 책임 분리
TX와 RX 각 방향에 대해 2ns 지연을 삽입할 수 있는 위치는 세 군데입니다. 세 위치 중 정확히 한 곳만 지연을 담당해야 합니다. 아래 다이어그램은 TX 방향을 기준으로 각 위치의 역할을 보여 줍니다.
흔한 실수 유형
| 실수 유형 | 총 지연 | 증상 | 원인 |
|---|---|---|---|
| 이중 적용 (double delay) | 4ns | 1G에서 CRC 오류 폭증, 링크 불안정 | PHY를 rgmii-id로 설정하면서 MAC도 내부 지연 활성화, 또는 PCB가 지연 트레이스를 보유 |
| 지연 없음 (no delay) | 0ns | 1G에서 CRC 오류, 100M는 정상 | rgmii로 설정했으나 MAC·PCB 모두 지연 미제공 |
| TX/RX 방향 혼동 | 한쪽 4ns, 한쪽 0ns | TX 또는 RX 중 한 방향만 오류 발생 | rgmii-txid와 rgmii-rxid를 혼동하거나 PCB 지연 방향 오파악 |
| PCB 지연 미인지 | 2+2=4ns | 이중 적용과 동일 증상 | PCB 설계자가 클럭 트레이스 길이 조정으로 이미 지연을 추가했으나 소프트웨어 담당자가 모르고 rgmii-id 사용 |
PHY 드라이버의 지연 레지스터 설정 예시
Realtek 계열 PHY를 포함한 많은 Gigabit PHY는 내부 지연을 레지스터로 제어합니다. 아래는 개념적인 드라이버 패턴으로, 실제 비트 위치와 레지스터 번호는 각 PHY 데이터시트를 확인해야 합니다.
/* 개념적 Realtek 스타일 RGMII 지연 설정 */
#define PHY_PAGE_ADDR_REG 31
#define PHY_EXT_PAGE 7
#define PHY_RGMII_DELAY_REG 28 /* 확장 레지스터 (페이지 7) */
#define TX_DELAY_BIT BIT(1)
#define RX_DELAY_BIT BIT(0)
static int rtl_phy_set_rgmii_delay(struct phy_device *phydev,
bool tx_delay, bool rx_delay)
{
int ret, val;
/* 확장 레지스터 페이지 진입 */
ret = phy_write(phydev, PHY_PAGE_ADDR_REG, PHY_EXT_PAGE);
if (ret)
return ret;
val = phy_read(phydev, PHY_RGMII_DELAY_REG);
if (val < 0)
goto restore_page;
/* TX/RX 지연 비트 설정 */
if (tx_delay)
val |= TX_DELAY_BIT;
else
val &= ~TX_DELAY_BIT;
if (rx_delay)
val |= RX_DELAY_BIT;
else
val &= ~RX_DELAY_BIT;
ret = phy_write(phydev, PHY_RGMII_DELAY_REG, val);
restore_page:
/* 일반 레지스터 페이지 복귀 */
phy_write(phydev, PHY_PAGE_ADDR_REG, 0);
return ret;
}
static int rtl_phy_config_init(struct phy_device *phydev)
{
bool tx = (phydev->interface == PHY_INTERFACE_MODE_RGMII_ID ||
phydev->interface == PHY_INTERFACE_MODE_RGMII_TXID);
bool rx = (phydev->interface == PHY_INTERFACE_MODE_RGMII_ID ||
phydev->interface == PHY_INTERFACE_MODE_RGMII_RXID);
return rtl_phy_set_rgmii_delay(phydev, tx, rx);
}
rgmii(지연 없음)를 사용해야 합니다. 이 정보가 보드 설계 문서에 명확히 기재되지 않으면 rgmii-id를 잘못 설정하여 4ns 이중 지연이 발생합니다. 보드 수신 후에는 항상 회로도의 클럭 트레이스 길이와 SoC의 내부 지연 레지스터 기본값을 함께 확인하십시오.
SerDes, PCS, SFP 모듈
구리 RJ45 PHY만 생각하면 PHY를 외부 칩 하나로 이해하기 쉽지만, 실제 장비에서는 host PCS, SerDes lane, SFP cage, 모듈 내부 PHY 또는 광 PMD가 나뉘어 있을 수 있습니다. 특히 1000BASE-X와 SGMII는 비슷한 1.25Gbaud 직렬 링크처럼 보여도 control word 의미와 in-band status 해석이 달라, 모드를 잘못 맞추면 링크 LED는 켜져도 실제 프레임 송수신이 안 될 수 있습니다.
| 모드 | 주요 대상 | baud / lane | 협상/상태 전달 | 주의점 |
|---|---|---|---|---|
| SGMII | MAC ↔ 외부 PHY | 1.25Gbaud, 1 lane | in-band status, speed code | 속도 표시는 control word에 실림 |
| 1000BASE-X | 광 모듈, 스위치 업링크 | 1.25Gbaud, 1 lane | Clause 37 AN | SGMII와 전기적 속도는 비슷해도 의미가 다름 |
| 2500BASE-X | 2.5G SerDes | 3.125Gbaud, 1 lane | 장치별 운용 차이 존재 | 양단 PCS 설정 일치 필요 |
| QSGMII | 4포트 집적 PHY | 5Gbaud, 1 lane | 4개 포트 상태 다중화(Multiplexing) | 포트 분리 해석 필요 |
| USXGMII | 멀티기가비트 | 10.3125Gbaud, 1 lane | 10/5/2.5/1G 다중화 | MAC/PHY 둘 다 지원해야 의미 있음 |
- 광 모듈은 주로 PMD 성격이 강하고, host 쪽 PCS와 SerDes 설정이 맞아야 링크가 정상입니다.
- RJ45 copper SFP는 안에 별도 copper PHY가 들어 있어 발열, 전력, latency 특성이 일반 광 모듈과 다릅니다.
- LOS/TX_FAULT/MOD_ABS는 MDIO가 아니라 모듈 sideband 신호인 경우가 많습니다. PHY 레지스터만 봐서는 원인을 못 찾을 수 있습니다.
SFP sideband 신호를 같이 봐야 하는 이유
광 모듈 문제는 MDIO만으로 보이지 않는 경우가 많습니다. 모듈이 스스로 올리는 LOS, TX_FAULT, MOD_ABS, TX_DISABLE 같은 sideband는 host PCS와 phylink 정책에 직접 영향을 줍니다. 즉 광량과 DOM은 정상인데도 sideband 한 줄이 반대로 연결되어 전체 링크가 막힐 수 있습니다.
| 신호 | 의미 | 보통 누가 해석하나 | 문제 시 증상 |
|---|---|---|---|
MOD_ABS | 모듈 삽입 여부 | SFP 코어 / phylink | 모듈이 꽂혀도 미삽입으로 보임 |
LOS | 수신 광 신호 손실 | host 드라이버 / 모듈 상태기 | 광량은 있는데 링크 down 반복 |
TX_FAULT | 송신 레이저 fault | host 드라이버 | 송신만 불가, fault latched |
TX_DISABLE | 송신 레이저 비활성화 | host 제어 | 모듈은 인식되지만 원격 링크 없음 |
RATE_SELECT | 속도/이퀄라이저 선택 | host와 모듈 조합 | 특정 속도에서만 불안정 |
phylib과 phylink
phylib은 "PHY 칩을 커널이 공통 방식으로 다루기 위한 프레임워크"이고, phylink는 "MAC, PHY, PCS, fixed-link, SFP를 하나의 정책으로 묶는 상위 계층"입니다. 외부 PHY 하나를 단순히 연결하는 임베디드 보드라면 phylib만으로 충분할 수 있지만, SFP 모듈 핫플러그(Hotplug)나 in-band status, 고속 SerDes가 얽히면 phylink가 사실상 표준 선택입니다.
| 항목 | phylib | phylink |
|---|---|---|
| 주요 대상 | 외부 PHY 직접 연결 | MAC + PHY + PCS + SFP 조합 |
| 핵심 구조체(Struct) | phy_device, phy_driver | phylink, phylink_config, phylink_mac_ops |
| 장점 | 단순하고 익숙함 | fixed-link, SFP, in-band status 통합 |
| 주의점 | 복합 토폴로지 대응이 약함 | MAC 콜백 계약을 정확히 구현해야 함 |
/* 개념 예시: PHY 드라이버가 구현하는 핵심 콜백 */
static int myphy_config_init(struct phy_device *phydev);
static int myphy_config_aneg(struct phy_device *phydev);
static int myphy_read_status(struct phy_device *phydev);
static struct phy_driver myphy_driver = {
.phy_id = 0x001cc916,
.phy_id_mask = 0xfffffff0,
.name = "Example PHY",
.config_init = myphy_config_init,
.config_aneg = myphy_config_aneg,
.read_status = myphy_read_status,
};
/* 개념 예시: phylink 초기화와 firmware 기반 PHY 연결 */
static const struct phylink_mac_ops my_phylink_ops = {
.mac_config = my_mac_config,
.mac_link_up = my_mac_link_up,
.mac_link_down = my_mac_link_down,
};
static int my_phylink_init(struct my_priv *priv)
{
struct phylink_config *cfg = &priv->phylink_config;
struct fwnode_handle *fwnode = dev_fwnode(priv->dev);
phy_interface_t iface = priv->phy_mode;
priv->phylink = phylink_create(cfg, fwnode, iface, &my_phylink_ops);
if (IS_ERR(priv->phylink))
return PTR_ERR(priv->phylink);
if (is_of_node(fwnode))
return phylink_of_phy_connect(priv->phylink, to_of_node(fwnode), 0);
return phylink_fwnode_phy_connect(priv->phylink, fwnode, 0);
}
phylink를 도입하면 링크 업/다운 알림, SFP 모듈 감지, PCS 선택, fixed-link 해석을 한 계층에서 정리할 수 있습니다. 반대로 mac_config(), mac_link_up(), mac_link_down() 구현이 느슨하면 링크는 오르는데 실제 MAC 레지스터가 이전 속도를 유지하는 문제가 생길 수 있습니다.
phylib vs phylink 선택 기준
새 MAC 드라이버를 작성할 때 어느 쪽을 선택할지는 보드의 PHY 토폴로지에 따라 결정됩니다. 아래 조건 중 하나라도 해당하면 phylink을 사용하는 것이 맞습니다.
| 조건 | phylib | phylink |
|---|---|---|
| SFP 슬롯(핫플러그) 지원 | 어려움 | 필수 선택 |
| in-band status (SGMII/1000BASE-X) | 직접 구현 필요 | 프레임워크 지원 |
| PCS가 MAC과 분리된 하드웨어 | 미지원 | phylink_pcs로 통합 |
| fixed-link (PHY 없는 직결) | 별도 처리 | of_phy_is_fixed_link 자동 감지 |
| 외부 PHY 1개, RGMII 고정 | 충분 | 가능하지만 불필요한 복잡도 |
mac_link_up()에서 MAC 레지스터의 speed, duplex, flow control을 반드시 갱신해야 합니다.
이 콜백을 빈 함수로 두면 PHY가 1G→100M으로 재협상해도 MAC은 1G 설정을 유지하여 프레임이 깨집니다. 이 오류는 "링크 업 상태에서 CRC 폭증"으로 나타나 RGMII delay 문제와 혼동하기 쉽습니다.
분리형 PCS와 In-Band Status
고속 SerDes 기반 설계에서는 PHY가 모든 계층을 혼자 담당하지 않습니다. host MAC 안에 별도 PCS가 있고, 외부에는 PMA/PMD 성격이 강한 장치만 있거나, 반대로 외부 PHY는 있지만 링크 상태 결정은 host PCS가 수행하는 경우가 있습니다. 이때 커널은 단순한 phy_device만으로는 부족하고, phylink_pcs를 통해 "MAC 옆의 PCS"를 별도 객체로 다뤄야 합니다.
실무적으로는 SGMII, 1000BASE-X, 2500BASE-X, 10GBASE-R에서 이 구분이 중요합니다. 외부 copper PHY가 속도 협상을 끝냈더라도, host PCS가 그 결과를 in-band status로 받아 MAC 속도를 다시 맞추지 않으면 실제 데이터 경로가 잘못된 속도로 남을 수 있습니다.
| 구성 | 누가 상태를 결정하나 | phylink 역할 | 실수하기 쉬운 점 |
|---|---|---|---|
| RGMII + 외부 PHY | 외부 PHY | PHY 결과를 MAC에 전달 | phy-mode와 지연 설정 불일치 |
| SGMII + 외부 PHY + host PCS | 외부 PHY와 host PCS 둘 다 | PCS와 PHY 결과 동기화 | in-band status 미적용 |
| 1000BASE-X + SFP | host PCS와 모듈 상태 | 모듈 삽입, LOS, Clause 37 AN 조정 | SGMII와 의미 혼동 |
| fixed-link + host PCS | firmware 고정값 | MAC 설정만 적용 | 고정 링크인데도 AN 기대 |
| backplane + Clause 73 | host PCS/AN 블록 | FEC와 lane 정책 조정 | FEC 강제/자동 혼선 |
/* 개념 예시: 분리형 PCS를 phylink에 등록 */
static void my_pcs_get_state(struct phylink_pcs *pcs,
struct phylink_link_state *state)
{
/* host PCS의 block lock, speed code, duplex 상태 해석 */
}
static const struct phylink_pcs_ops my_pcs_ops = {
.pcs_get_state = my_pcs_get_state,
.pcs_config = my_pcs_config,
.pcs_an_restart = my_pcs_restart_an,
.pcs_link_up = my_pcs_link_up,
};
static int my_mac_select_pcs(struct phylink_config *config,
phy_interface_t interface)
{
return interface == PHY_INTERFACE_MODE_SGMII ? 0 : 0;
}
/* SFP/SerDes 경로에서 host PCS가 in-band status를 읽는 예시 */
ð0 {
phy-mode = "sgmii";
managed = "in-band-status";
sfp = <&sfp0>;
status = "okay";
};
fixed-link, managed = "in-band-status", 외부 PHY attach는 같은 포트에서 역할이 다릅니다.
고정 링크인데도 in-band status를 기대하거나, host PCS가 속도를 결정하는데 외부 PHY 결과만 믿으면 MAC 속도가 이전 값에 남아 실제 송수신이 어긋날 수 있습니다.
In-Band vs Out-of-Band 링크 상태 결정
PHY 링크 상태를 MAC에 전달하는 경로는 크게 두 가지입니다. Out-of-Band는 데이터 경로와 물리적으로 분리된 관리 채널을 통해 상태를 읽는 방식이고, In-Band는 데이터가 흐르는 직렬 링크 자체에 상태 정보를 실어 보내는 방식입니다. 두 방식은 배타적이지 않으며, 같은 포트에서 외부 PHY의 MDIO(out-of-band)와 host PCS의 in-band status가 동시에 존재할 수 있습니다. 이 경우 phylink이 양쪽 결과를 통합하여 MAC 속도를 결정합니다.
| 항목 | Out-of-Band | In-Band |
|---|---|---|
| 물리 경로 | 데이터 경로와 별도 (MDIO 2선, SFP I2C) | 데이터 경로와 동일 (SerDes 직렬 링크) |
| 대표 예 | MDIO Clause 22/45 BMSR 폴링, SFP MOD_ABS/LOS | Clause 37 /C/ ordered set, SGMII control word, Clause 73 DME page |
| 속도/duplex 결정 | 소프트웨어가 PHY 레지스터를 읽어 해석 | PCS 하드웨어가 수신 스트림에서 자동 추출 |
| Device Tree 설정 | phy-handle = <&phy> | managed = "in-band-status" |
| 커널 콜백 | phy_read_status() → phylib | pcs_get_state() → phylink_pcs |
| 대표 인터페이스 | RGMII, MII, RMII | SGMII, 1000BASE-X, 2500BASE-X, 10GBASE-R |
| 대표 실패 증상 | PHY ID 0xffff, MDIO timeout | 링크 LED 켜지지만 speed code 불일치로 데이터 미전송 |
SGMII In-Band Control Word 형식
SGMII는 Cisco가 정의한 인터페이스로, Clause 37과 같은 1.25Gbaud 8B/10B 직렬 링크를 사용하지만 control word의 의미가 완전히 다릅니다. PHY→MAC 방향에서는 링크 상태, 속도, duplex 정보를 담은 16비트 word를 보내고, MAC→PHY 방향에서는 acknowledge만 돌려보냅니다. 이 비대칭 구조가 Clause 37(양쪽 대칭)과의 핵심 차이입니다.
| 비트 | PHY→MAC 방향 | MAC→PHY 방향 | 설명 |
|---|---|---|---|
| 15 | Link | Link (echo) | 1 = link up, 0 = link down |
| 14 | Acknowledge | Acknowledge | 상대편 word 수신 확인 |
| 13:12 | [12] Duplex | Reserved | 1 = full duplex, 0 = half duplex |
| 11:10 | Speed | Reserved | 00=10M, 01=100M, 10=1000M, 11=reserved |
| 9:1 | Reserved | Reserved | 향후 확장용 |
| 0 | 1 (고정) | 1 (고정) | SGMII 식별자 — Clause 37과 구분하는 핵심 비트 |
phy-mode로 명시적으로 지정합니다.
/* 개념 예시: SGMII control word 해석 */
static void sgmii_decode_word(u16 word, struct phylink_link_state *state)
{
state->link = !!(word & BIT(15));
state->duplex = (word & BIT(12)) ? DUPLEX_FULL : DUPLEX_HALF;
switch ((word >> 10) & 3) {
case 0: state->speed = SPEED_10; break;
case 1: state->speed = SPEED_100; break;
case 2: state->speed = SPEED_1000; break;
default: state->link = 0; break;
}
}
/* 참조: drivers/net/pcs/pcs-xpcs.c — xpcs_get_state_sgmii()
* drivers/net/pcs/pcs-lynx.c — lynx_pcs_get_state_sgmii() */
1000BASE-X Clause 37 Base Page 형식
Clause 37은 IEEE 802.3에서 정의한 1000BASE-X 전용 Auto-Negotiation입니다. 8B/10B 스트림 안에서 /C/ ordered set을 교환하여 16비트 base page를 주고받습니다. SGMII와 달리 양쪽이 대칭이며, 속도 정보가 없습니다(항상 1000Mbps). 협상의 주된 목적은 pause(흐름 제어) 능력과 duplex를 결정하는 것입니다.
| 비트 | 이름 | 의미 |
|---|---|---|
| 15 | Next Page | 추가 next page 존재 여부 |
| 14 | Acknowledge | 상대편 base page 수신 확인 |
| 13:12 | Remote Fault | 00=정상, 01=link failure, 10=offline, 11=AN error |
| 11:9 | Reserved | — |
| 8:7 | Pause | 01=symmetric, 10=asymmetric, 11=both |
| 6 | Half Duplex | half duplex 지원 |
| 5 | Full Duplex | full duplex 지원 |
| 4:0 | Reserved | — |
/* 개념 예시: Clause 37 base page 해석 */
static void clause37_decode_page(u16 lpa, struct phylink_link_state *state)
{
state->speed = SPEED_1000; /* 항상 1000Mbps */
state->duplex = (lpa & BIT(5)) ? DUPLEX_FULL : DUPLEX_HALF;
state->pause = MLO_PAUSE_NONE;
if (lpa & BIT(7))
state->pause |= MLO_PAUSE_SYM;
if (lpa & BIT(8))
state->pause |= MLO_PAUSE_ASYM;
}
/* 참조: include/uapi/linux/mii.h — ADVERTISE_1000XFULL, ADVERTISE_1000XPAUSE */
Clause 73 백플레인 In-Band AN
10GBASE-KR, 25GBASE-KR, 40GBASE-KR4 등 고속 백플레인(Backplane) 링크에서는 Clause 73이 사용됩니다. Clause 37과 달리 multi-page 구조(base page 48비트 + next page)를 가지며, FEC 능력 광고와 AN 후 link training 시퀀스가 추가됩니다. AN이 완료된 뒤 양단이 SerDes equalizer를 최적화하는 훈련 과정을 거쳐야 비로소 데이터 경로가 열립니다.
| base page 필드 | 비트 범위 | 용도 |
|---|---|---|
| Selector Field | A[4:0] | 기술 유형 (IEEE 802.3 = 1) |
| Echoed Nonce | A[8:5] | 상대편 nonce 반향 — 자기 자신과 협상 중이 아님을 확인 |
| Pause Ability | A[12:10] | symmetric/asymmetric pause 광고 |
| Nonce | A[17:13] | 로컬 난수 |
| Technology Ability | A[24:21] | 10GBase-KR, 40GBase-KR4, 25GBase-KR 등 속도 능력 |
| FEC Ability | A[47:44] | BASE-R FEC(Clause 74), RS-FEC(Clause 91/108) 지원 여부 |
phylink_pcs_ops.pcs_config()에서 FEC 정책을 PCS에 반영합니다.
phylink의 링크 상태 결정 경로
phylink은 초기화 시점에 Device Tree 설정과 MAC 드라이버 능력에 따라 링크 상태 결정 모드를 선택합니다. 세 가지 모드 — MLO_AN_PHY, MLO_AN_INBAND, MLO_AN_FIXED — 는 각각 다른 콜백 체인을 활성화하며, 이 선택이 링크 수명주기 전체를 결정합니다.
| 항목 | MLO_AN_PHY | MLO_AN_INBAND | MLO_AN_FIXED |
|---|---|---|---|
| 상태 소스 | 외부 PHY (MDIO 레지스터) | Host PCS (in-band signaling) | Device Tree 고정값 |
| DT 설정 | phy-handle = <&phy> | managed = "in-band-status" | fixed-link { speed = 1000; ... } |
| 대표 인터페이스 | RGMII, MII, RMII | SGMII, 1000BASE-X, 2500BASE-X | chip-to-chip 직결, 일부 스위치 |
| MAC 속도 결정 주체 | PHY가 알려줌 | PCS가 in-band word에서 추출 | DT에 명시된 고정값 |
| 핵심 콜백 | phy_read_status() | pcs_get_state() | 초기 설정만 |
| 대표 실패 패턴 | PHY ID 못 읽음, MDIO timeout | speed code 불일치, AN 무한 반복 | 속도 변경 불가, 상대편 변화 감지 불가 |
/* 개념 예시: phylink_resolve() 단순화 흐름
* 참조: drivers/net/phy/phylink.c — phylink_resolve() */
static void phylink_resolve(struct work_struct *w)
{
struct phylink *pl = container_of(w, struct phylink, resolve);
struct phylink_link_state link_state;
switch (pl->cur_link_an_mode) {
case MLO_AN_PHY:
/* 외부 PHY가 이미 읽어둔 상태를 복사 */
link_state = pl->phy_state;
break;
case MLO_AN_INBAND:
/* Host PCS에서 in-band 상태를 직접 읽음 */
pl->pcs->ops->pcs_get_state(pl->pcs, &link_state);
break;
case MLO_AN_FIXED:
/* DT에서 가져온 고정값 사용 */
link_state = pl->link_config;
link_state.link = 1;
break;
}
/* 상태가 변경되었으면 MAC에 반영 */
if (link_state.link && !pl->old_link_state)
pl->mac_ops->mac_link_up(pl->config, pl->cur_link_an_mode,
link_state.interface, ...);
}
/* 개념 예시: PCS 드라이버가 SGMII in-band 상태를 보고하는 콜백
* 참조: drivers/net/pcs/pcs-xpcs.c, drivers/net/pcs/pcs-lynx.c */
static void my_pcs_get_state(struct phylink_pcs *pcs,
struct phylink_link_state *state)
{
u32 status = readl(priv->pcs_base + PCS_STATUS);
u16 lpa = readl(priv->pcs_base + PCS_LP_ABILITY);
state->an_complete = !!(status & PCS_AN_DONE);
state->link = !!(status & PCS_LINK_UP);
if (state->link) {
if (state->interface == PHY_INTERFACE_MODE_SGMII) {
/* SGMII: control word에서 speed/duplex 추출 */
sgmii_decode_word(lpa, state);
} else {
/* 1000BASE-X: Clause 37 base page에서 pause/duplex 추출 */
clause37_decode_page(lpa, state);
}
}
}
MLO_AN_PHY와 MLO_AN_INBAND가 동시에 작동할 수 있습니다.
외부 PHY가 구리선에서 협상한 속도를 SGMII control word로 host PCS에 전달하면, phylink은 PCS 상태와 PHY 상태를 모두 확인한 뒤 MAC 속도를 결정합니다.
이 경우 PCS의 pcs_get_state()가 SGMII word를 올바르게 해석하지 못하면, PHY는 1G를 보고하는데 MAC은 100M으로 남는 증상이 발생합니다.
PHY 기능과 튜닝 포인트
PHY는 단순히 링크를 올리는 장치가 아니라, 운영 중 성능과 안정성을 좌우하는 여러 정책을 가집니다. 저전력 중심이면 EEE가 중요하고, 긴 케이블과 구형 배선이면 downshift와 cable diagnostics가 유용하며, 고속 광링크에서는 FEC 선택이 BER과 latency를 좌우합니다. 튜닝은 "항상 켠다/끈다"가 아니라 증상과 환경에 따라 선택해야 합니다.
| 기능 | 무엇을 하나 | 언제 점검하나 | 대표 명령 | 주의점 |
|---|---|---|---|---|
| EEE | 유휴 시 LPI로 전력 절감 | idle 후 첫 패킷 지연, link flap | ethtool --show-eee eth0 | 저지연 환경에서 불리할 수 있음 |
| Downshift | 고속 협상 실패 시 낮은 속도로 폴백 | 2.5G/5G가 자주 1G로 떨어짐 | ethtool --set-phy-tunable eth0 downshift on count 3 | 배선 문제를 가릴 수 있음 |
| Master/Slave | 1000BASE-T 기준 클럭 소스 결정 | 특정 스위치와 협상 불안정 | ethtool eth0 | 한쪽만 강제하면 오히려 문제 |
| Cable Test | TDR 기반 배선 진단 | 구리선 품질 의심 | ethtool --cable-test eth0 | PHY 지원 필요 |
| FEC | 고속 링크 비트 오류 정정 | 25G+/광 링크 BER 높음 | ethtool --show-fec eth0 | 약간의 지연 증가 가능 |
| Loopback | MAC/PHY 내부 경로 단독 검사 | 상대편 없이 자체 진단 | 벤더 레지스터 또는 self-test | 트래픽 경로 일부만 검증 |
# EEE 상태 조회와 비활성화
ethtool --show-eee eth0
ethtool --set-eee eth0 eee off
# Downshift 정책 설정 (지원 PHY만)
ethtool --set-phy-tunable eth0 downshift on count 3
# 케이블 테스트 (지원 PHY만)
ethtool --cable-test eth0
# FEC 상태 조회
ethtool --show-fec eth0
Master/Slave, downshift, EDPD를 함께 읽기
1000BASE-T 이상 구리 PHY에서는 링크 양단이 기준 클럭 제공자를 정해야 합니다. 이것이 master/slave 결정입니다. 동시에 배선 품질이 좋지 않으면 고속 협상에 반복 실패하고, 그 결과 downshift가 작동해 낮은 속도로만 링크가 붙습니다. 여기서 EDPD(Energy Detect Power Down)가 섞이면 idle 상태에서 PHY가 더 깊게 잠들었다가 복귀하면서 협상이 늦어지는 증상이 나타날 수 있습니다.
| 항목 | 좋은 경우 | 나쁜 경우 | 주로 보이는 증상 |
|---|---|---|---|
| Master/Slave | 자동 결정 또는 양단 일관 설정 | 한쪽만 강제 | 간헐적 협상 실패, 재협상 반복 |
| Downshift | 불량 배선에서 폴백 안전장치 | 원인 숨김 | 2.5G 장비가 계속 1G로만 링크 업 |
| EDPD | 유휴 전력 절감 | 상대편과 복귀 정책 불일치 | idle 후 첫 링크 업 지연 |
| EEE | 지속 유휴 구간에서 전력 절감 | 낮은 지연 요구와 충돌 | burst 트래픽에서 변동 지연 |
# PHY 튜너블 조회
ethtool --show-phy-tunable eth0
# 에너지 감지 파워다운 설정 (지원 PHY만)
ethtool --set-phy-tunable eth0 energy-detect-power-down on
Loopback과 자가 진단을 계층별로 분리하기
루프백은 "상대편 없이 내 경로만 검증"하는 가장 빠른 방법이지만, 어느 계층에서 되돌리는지에 따라 검증 범위가 달라집니다. MAC loopback은 MAC과 DMA까지만 확인하고, PCS loopback은 host PCS와 SerDes 일부까지, PHY loopback은 PMA/PMD까지 포함할 수 있습니다. 반대로 외부 케이블과 원격 포트는 빠지므로, 루프백이 통과했다고 실제 배선 문제가 사라지는 것은 아닙니다.
- MAC loopback
DMA, descriptor, netdev 경로를 빠르게 검증합니다. - PCS loopback
SerDes encoding과 block lock 경로를 좁혀 봅니다. - PHY 내부 loopback
외부 케이블 없이 PHY 내부 DSP 경로를 확인합니다. - 원격 loopback
상대편 장비가 지원할 때 전체 선로까지 포함한 검증이 가능합니다.
PHY 하드웨어 타임스탬프와 시간 민감 네트워크
일부 PHY는 MAC보다 더 선로 가까운 위치에서 송수신 타임스탬프를 찍을 수 있습니다. 이런 PHY는 PTP(IEEE 1588)나 TSN 환경에서 유리할 수 있는데, 패킷이 실제 wire를 통과한 시점에 더 가까운 시각을 얻을 수 있기 때문입니다. 다만 시스템 전체에서는 MAC 쪽 PHC와 PHY 쪽 타임스탬프 경로를 혼동하지 않아야 하며, 사용자 공간(User Space)에서는 결국 SO_TIMESTAMPING과 ethtool -T로 노출된 능력으로 보게 됩니다.
| 위치 | 장점 | 주의점 | 대표 점검 |
|---|---|---|---|
| MAC 타임스탬프 | 드라이버 통합이 단순 | 선로 기준과 약간 거리 있음 | ethtool -T eth0 |
| PHY 타임스탬프 | wire-side 기준에 더 가까움 | PHY와 MAC의 clock domain 정렬 필요 | driver timestamp capability |
| 소프트웨어 타임스탬프 | 지원 폭 넓음 | 지연/지터 큼 | SO_TIMESTAMPING fallback |
# 인터페이스의 HW timestamping / PHC 지원 확인
ethtool -T eth0
PHY 인터럽트, 폴링(Polling), 전원 관리(Power Management)
PHY 상태 머신은 크게 폴링과 인터럽트 두 방식으로 링크 변화를 감시합니다. 폴링은 단순하고 안정적이지만 변화 감지가 느리고, 인터럽트는 빠르지만 마스크/ack 처리와 공유 IRQ 문제를 정확히 구현해야 합니다. 모바일·임베디드 장치에서는 여기에 suspend/resume, Wake-on-LAN, EEE, EDPD가 더해져 "전원이 꺼져 있을 때 PHY를 어디까지 살릴 것인가"가 추가 과제가 됩니다.
| 주제 | 핵심 API/개념 | 장점 | 위험 요소 |
|---|---|---|---|
| 폴링 | phy_start(), 상태 머신 주기 점검 | 구현 단순, IRQ 불필요 | 감지 지연, 불필요한 MDIO 트래픽 |
| 인터럽트 | handle_interrupt, phy_trigger_machine() | 링크 변화 즉시 반응 | ack 누락, IRQ storm, 공유 선 문제 |
| suspend/resume | suspend, resume | 전력 절감 | refclk/strap 재초기화 누락 |
| Wake-on-LAN | magic packet, PHY 유지 전원 | 저전력 대기에서 원격 기동 | analog rail 완전 차단 시 실패 |
| EDPD/EEE | LPI, energy detect | 유휴 전력 절감 | 복귀 지연, flap 오인 |
/* 개념 예시: IRQ 기반 PHY 링크 감지 */
static irqreturn_t myphy_handle_interrupt(struct phy_device *phydev)
{
int irq_stat;
irq_stat = phy_read(phydev, 0x13);
if (irq_stat < 0)
return IRQ_NONE;
/* 벤더 IRQ status를 읽어 clear 한 뒤 상태 머신 재실행 */
phy_trigger_machine(phydev);
return IRQ_HANDLED;
}
static int myphy_suspend(struct phy_device *phydev)
{
/* WoL, EEE, EDPD 정책에 따라 analog block 일부만 유지 */
return genphy_suspend(phydev);
}
static int myphy_resume(struct phy_device *phydev)
{
int ret = genphy_resume(phydev);
if (ret)
return ret;
/* resume 뒤 strap override와 delay bit를 다시 써야 하는 PHY도 있다 */
return myphy_set_rgmii_delay(phydev);
}
- IRQ가 있다고 항상 좋은 것은 아닙니다. 링크 변화가 잦은 환경에서는 잘못된 인터럽트 마스크가 오히려 CPU를 더 흔듭니다.
- resume 뒤 재설정이 필요한 PHY가 있습니다. strap 결과를 runtime에서 override한 경우 특히 resume에서 다시 써야 합니다.
- WoL을 켜려면 PHY analog rail과 magic packet 검출 경로를 살려야 하므로, 단순 regulator off와 충돌할 수 있습니다.
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 = <®_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 주소 | 다른 주소로 부팅 | 기대 주소에서 0xffff | pull 저항과 reset 타이밍 재검토 |
| RGMII delay 기본값 | 내부 지연 on/off 반전 | 1G CRC 폭증 | strap 상태와 runtime override 둘 다 확인 |
| fiber / copper 선택 | 잘못된 매체 모드 | 링크 LED 동작이 이상함 | strap 조합과 보드 옵션 확인 |
| LED 모드 | LED 핀 동작 혼선 | 링크는 되는데 LED만 오작동 | strap 겸용 핀 배치 재검토 |
| 항목 | 왜 중요한가 | 실패 시 증상 |
|---|---|---|
phy-handle / PHY 주소 | 올바른 PHY 노드와 연결 | probe 실패, 잘못된 PHY 드라이버 매칭 |
phy-mode | MAC-PHY 인터페이스 계약 일치 | 링크 불안정, CRC, 속도 강등 |
| reset 타이밍 | strap pin, clock 안정화 보장 | PHY ID가 0xffff로 읽힘 |
| 전원 / regulator | 아날로그 블록 안정성 확보 | 온도 민감, 링크 flapping |
| reference clock | PMA/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/strap | reset 폭, 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가 없는 경우: fixed-link
스위치 CPU 포트나 백플레인처럼 외부 PHY가 없이 PCS/SerDes만 존재하는 경우에는 phy-handle 대신 fixed-link를 사용하기도 합니다. 이 경우 MDIO로 읽을 PHY가 없으므로, 링크 정책과 상태 전달을 MAC/phylink 계층에서 처리해야 합니다.
/* 외부 PHY 없이 고정 링크를 선언하는 예시 */
ð0 {
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 | 아날로그 프런트엔드 안정화 | 온도 의존 링크 flap | analog/digital rail 분리와 근접 캐패시터 |
| SFP cage 접지 | EMI와 모듈 안정성 | 모듈별 편차, hot-plug 불안정 | cage 접지, 전력 예산, sideband pull-up |
대표 장애 패턴과 원인 분리
PHY 문제는 반복적으로 비슷한 모양으로 나타납니다. 아래 패턴을 외워 두면 "드라이버 버그인지, 보드 타이밍인지, 상대편 정책인지"를 빨리 구분할 수 있습니다.
패턴 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를 전혀 찾지 못함 | 잘못된 PHY 주소, reset 타이밍 부족, 전원 미인가 | dmesg, /sys/bus/mdio_bus/devices | PHY 주소와 reset GPIO, regulator 순서 점검 |
| 링크 업 안 됨 | 상대편 포트 정책, 케이블, advertisement 불일치 | ethtool eth0 | 양쪽 autoneg 정책과 케이블 등급 확인 |
| 링크 업 후 CRC 증가 | RGMII skew, 신호 품질, magnetics 문제 | ethtool -S eth0 | phy-mode와 보드 지연 위치 재검토 |
| 2.5G/5G가 1G로만 붙음 | 케이블 품질, 상대편 광고 제한 | supported/advertised link modes | Cat5e/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, 케이블 테스트 | 가장 먼저 |
dmesg | probe, reset, link flap, SFP 이벤트 | 부팅 직후 실패 |
| dynamic debug | phylib 상태 머신과 벤더 드라이버 상세 로그 | 간헐 재현, resume 이슈 |
ethtool -m | 광 모듈 DOM, 온도, RX/TX power | 광 링크 품질 문제 |
ethtool -d | 드라이버 제공 레지스터 덤프(Dump) | 벤더 드라이버 비교 |
실전 점검 순서
- PHY가 존재하는지
/sys/bus/mdio_bus/devices와 PHY ID부터 봅니다. - 링크 정책이 맞는지
ethtool eth0에서 speed, duplex, advertisement, pause를 확인합니다. - 오류가 물리 계층인지
ethtool -S에서 CRC, align, symbol, carrier 카운터를 봅니다. - 보드 타이밍 문제인지
phy-mode, RGMII delay, reset timing, clock 안정성을 점검합니다. - 상대편 의존 문제인지
스위치 포트, 모듈, 케이블, 강제 속도 설정을 바꿔 증상이 이동하는지 확인합니다.
2.5GBASE-T / 5GBASE-T / 10GBASE-T PHY
NBASE-T(IEEE 802.3bz)는 기존 Cat5e/Cat6 배선 위에서 1G 이상 속도를 달성하기 위해 만들어진 규격입니다. 2.5GBASE-T와 5GBASE-T는 각각 Cat5e와 Cat6 케이블에서 동작하도록 설계되었으며, 10GBASE-T(802.3an)는 Cat6a 이상을 요구합니다. 이 PHY들은 기존 1000BASE-T PHY보다 훨씬 복잡한 DSP를 탑재하고 있어 전력 소비, 발열, 협상 시간이 모두 증가합니다.
| 규격 | IEEE | 속도 | 최소 케이블 | 최대 거리 | 부호화 | 전력 특성 |
|---|---|---|---|---|---|---|
| 2.5GBASE-T | 802.3bz | 2.5 Gbps | Cat5e | 100m | PAM-16 / DSQ128 | ~2.5W (PHY 단독) |
| 5GBASE-T | 802.3bz | 5 Gbps | Cat6 | 100m | PAM-16 / DSQ128 | ~3.5W |
| 10GBASE-T | 802.3an | 10 Gbps | Cat6a | 100m | PAM-16 + LDPC | ~4~7W |
| 1000BASE-T | 802.3ab | 1 Gbps | Cat5e | 100m | PAM-5 | ~0.5W |
NBASE-T PHY 내부 구조
2.5G/5G/10G 구리 PHY는 1000BASE-T와 마찬가지로 4쌍 동시 전송을 사용하지만, 심볼 레이트와 부호화 복잡도가 크게 다릅니다. 10GBASE-T는 LDPC(Low-Density Parity-Check) FEC를 필수로 적용하고, echo cancellation과 NEXT/FEXT 제거를 위한 DSP 복잡도가 수십 배 증가합니다. 이 때문에 초기 협상에 최대 수 초가 걸릴 수 있으며, PHY 칩의 발열이 상당합니다.
NBASE-T와 downshift 관계
2.5G/5G/10G PHY에서 downshift는 특히 중요합니다. 케이블 품질이 부족하면 10G에서 5G, 5G에서 2.5G, 2.5G에서 1G로 순차 폴백이 일어날 수 있습니다. 문제는 downshift가 자동으로 일어나면 사용자가 "왜 10G 포트인데 1G로만 연결되지?"라는 혼란을 겪는다는 점입니다. downshift 횟수와 현재 협상된 속도를 같이 확인해야 원인을 분리할 수 있습니다.
# NBASE-T 포트의 지원 속도와 현재 속도 확인
ethtool eth0 | grep -E "Speed|Supported|Advertised"
# 2.5G/5G/10G 속도 광고 제한
ethtool -s eth0 advertise 0x1000000000000 # 2.5GBASE-T만 광고
# 케이블 등급과 품질이 NBASE-T에 미치는 영향 확인
ethtool --cable-test eth0
ethtool --cable-test-tdr eth0
| 속도 | 케이블 요구 | 실패 시 downshift 대상 | 주의 사항 |
|---|---|---|---|
| 10GBASE-T | Cat6a / Cat7 (100m) | 5G, 2.5G, 1G | Cat6은 55m까지만 공식 지원, 짧은 거리에서만 동작 가능 |
| 5GBASE-T | Cat6 (100m) | 2.5G, 1G | Cat5e는 일부 환경에서만 동작, 보장 없음 |
| 2.5GBASE-T | Cat5e (100m) | 1G | 오래된 Cat5(비 e)에서는 실패 가능 |
Clause 45 레지스터 디버깅 실전
10G 이상 PHY를 디버깅할 때는 Clause 22의 32개 레지스터로는 부족하고, Clause 45의 MMD별 레지스터를 직접 읽어야 합니다. 리눅스에서는 mdio-tools 패키지나 devlink, ethtool의 레지스터 덤프 기능을 사용할 수 있습니다. 실전에서는 PCS block lock 상태, PMA signal detect, AN 상태를 체계적으로 읽어 문제를 계층별로 분리하는 것이 핵심입니다.
mdio-tools를 이용한 직접 레지스터 접근
mdio-tools는 사용자 공간에서 MDIO 버스를 통해 PHY 레지스터를 직접 읽고 쓸 수 있는 도구입니다. Clause 22와 Clause 45 모두 지원하며, 벤더 확장 레지스터 접근이나 드라이버가 노출하지 않는 상태 비트를 확인하는 데 유용합니다.
# mdio-tools 설치 (배포판에 따라 다름)
apt install mdio-tools # Debian/Ubuntu
dnf install mdio-tools # Fedora/RHEL
# Clause 22: PHY 주소 1의 BMCR(0x00)과 BMSR(0x01) 읽기
mdio eth0 0x00
mdio eth0 0x01
# Clause 45: PCS(MMD 3)의 status 1 레지스터 읽기
mdio eth0 mmd 3 0x0001
# Clause 45: PMA/PMD(MMD 1)의 status 1 읽기
mdio eth0 mmd 1 0x0001
# Clause 45: AN(MMD 7)의 status 읽기
mdio eth0 mmd 7 0x0001
# Clause 45: 벤더 확장(MMD 30/31) 레지스터 접근
mdio eth0 mmd 30 0x8000
# 레지스터 쓰기 (주의: 잘못 쓰면 링크 장애 발생)
mdio eth0 mmd 3 0x0000 0x2040
Clause 45 디버깅 체계적 접근
10G 이상 PHY 장애를 체계적으로 분석하려면 PMA, PCS, AN 순서로 상태를 확인합니다. 각 MMD의 핵심 비트를 읽어 어느 계층에서 문제가 시작되는지 좁히는 것이 목표입니다.
| 단계 | MMD | 레지스터 | 확인 비트 | 의미 |
|---|---|---|---|---|
| 1. PMA 링크 | MMD 1 (PMA/PMD) | 1.1 Status 1 | bit 2: Receive Link Status | PMA 수준에서 신호 감지 여부 |
| 2. PCS lock | MMD 3 (PCS) | 3.1 Status 1 | bit 2: PCS Receive Link | block lock과 alignment 달성 여부 |
| 3. PCS 상세 | MMD 3 (PCS) | 3.32-33 | BER counter, errored blocks | FEC 전후 오류율 |
| 4. AN 상태 | MMD 7 (AN) | 7.1 Status | bit 5: AN Complete | 협상 완료 여부와 결과 |
| 5. 벤더 확장 | MMD 30/31 | 벤더별 | 온도, SNR, cable length | PHY 내부 진단 정보 |
devlink를 이용한 PHY 진단
최신 커널과 드라이버에서는 devlink를 통해 PHY 진단 정보를 구조화된 형태로 얻을 수 있습니다. devlink health reporter, port parameters, 드라이버별 진단 메시지를 통해 드라이버가 해석한 PHY 상태를 확인할 수 있습니다.
# devlink 포트 목록 확인
devlink port show
# devlink health reporter 목록
devlink health show
# devlink health 덤프 (드라이버 지원 필요)
devlink health dump show pci/0000:01:00.0 reporter fw
# devlink port 파라미터 조회
devlink port param show pci/0000:01:00.0/0
# ethtool 레지스터 덤프와 함께 사용
ethtool -d eth0
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");
주요 콜백 상세 설명
| 콜백 | 호출 시점 | 기본 구현 | 커스텀 필요 상황 | 주의점 |
|---|---|---|---|---|
probe | PHY 장치 발견 시 | phylib 코어 | 특수 초기화 (firmware 로드) | 슬립(Sleep) 가능 컨텍스트 |
config_init | attach 직후, resume 시 | genphy_config_init | strap override, delay bit, LED 모드 | resume에서 재호출 보장 필요 |
config_aneg | 광고 능력 변경 시 | genphy_config_aneg | 벤더 고유 AN 확장 (EEE, FEC) | restart AN 포함 여부 주의 |
read_status | 상태 머신 주기 또는 IRQ 후 | genphy_read_status | 벤더 레지스터에서 추가 상태 해석 | phydev 필드에 결과 반영 |
handle_interrupt | IRQ 발생 시 | 없음 | IRQ 지원 PHY | IRQ status read-to-clear 순서 |
suspend/resume | 시스템 PM | genphy_suspend/resume | WoL, EDPD, strap 재적용 | analog rail 상태에 따라 분기 |
get_features | PHY 능력 감지 | genphy_read_abilities | 표준 레지스터에 없는 능력 추가 | linkmode 비트맵(Bitmap) 정확히 설정 |
set_tunable | ethtool --set-phy-tunable | 없음 | downshift, EDPD 정책 | 범위 검증 필수 |
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_status는 genphy_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_select_page()와 phy_restore_page()는 벤더별 .select_page 콜백을 통해 이 차이를 추상화합니다.EEE와 PHY 상호작용
EEE(Energy Efficient Ethernet, IEEE 802.3az)는 유휴 시 PHY를 LPI(Low Power Idle) 모드로 전환해 전력을 절감합니다. 그러나 EEE는 단순한 "켜기/끄기"가 아니라, LPI 진입 타이밍, wake 타이밍, MAC과 PHY 간의 LPI 신호 동기화가 정확히 맞아야 합니다. 이 동기화가 깨지면 유휴 후 첫 패킷의 지연이 크게 늘어나거나, 심한 경우 링크 flap이 발생합니다.
| EEE 파라미터 | 의미 | 기본값 범위 | 영향 |
|---|---|---|---|
| tx-lpi | MAC이 LPI 신호를 보내는지 여부 | on/off | off이면 EEE 사실상 비활성화 |
| tx-timer | 유휴 후 LPI 진입까지 대기 시간(Latency) | 60~255 us | 짧으면 절전 효과 크나 지연 증가 |
| Tw_sys_tx | LPI 해제 후 송신 가능까지 시간 | 규격별 다름 | 이 시간 동안 MAC은 전송 불가 |
| Tw_sys_rx | LPI 해제 후 수신 가능까지 시간 | 규격별 다름 | PHY가 완전 깨어나야 수신 시작 |
# EEE 상세 상태 확인
ethtool --show-eee eth0
# EEE 비활성화 (저지연 환경)
ethtool --set-eee eth0 eee off
# EEE 활성화 + tx-timer 조정
ethtool --set-eee eth0 eee on tx-timer 200
# LPI 카운터 확인 (드라이버 지원 필요)
ethtool -S eth0 | grep -i "lpi"
PTP와 PHY 타임스탬프 지원
PTP(IEEE 1588) 정밀 시간 동기화에서 PHY 수준 타임스탬프는 MAC 타임스탬프보다 매체에 더 가까운 위치에서 시각을 찍을 수 있어 정밀도가 높습니다. 일부 PHY(예: Marvell Alaska, Microchip LAN8814, TI DP83869)는 내부에 PHC(PTP Hardware Clock)를 탑재하고 있어, 패킷이 PHY를 통과하는 시점에 타임스탬프를 기록합니다.
| 타임스탬프 위치 | 정밀도 | 장점 | 단점 | 대표 PHY |
|---|---|---|---|---|
| 소프트웨어 | ~us 수준 | 모든 인터페이스에서 사용 가능 | 커널 스케줄링 지터 포함 | 해당 없음 |
| MAC 하드웨어 | ~10ns | 드라이버 통합이 비교적 단순 | MAC-PHY 사이 지연 포함 | Intel i210/i225 |
| PHY 하드웨어 | ~ns 수준 | 매체에 가장 가까운 시점 | PHY와 MAC 시계 동기화 필요 | LAN8814, DP83869 |
# 인터페이스의 타임스탬프 능력 확인
ethtool -T eth0
# PHC 장치 목록 확인
ls /dev/ptp*
# PHC 정보 확인
ethtool -T eth0 | grep -i "PTP"
# hwstamp_config 설정 (PTP 데몬이 자동으로 설정하기도 함)
hwstamp_ctl -i eth0 -r 1 -t 1
/* PHY 드라이버에서 PTP 지원 등록 (개념 예시) */
static const struct ptp_clock_info myphy_ptp_caps = {
.owner = THIS_MODULE,
.name = "myphy_ptp",
.max_adj = 50000000,
.n_alarm = 0,
.n_ext_ts = 0,
.n_per_out = 0,
.adjfine = myphy_ptp_adjfine,
.adjtime = myphy_ptp_adjtime,
.gettime64 = myphy_ptp_gettime,
.settime64 = myphy_ptp_settime,
};
/* mii_timestamper 등록으로 PHY 타임스탬프를 네트워크 스택에 연결 */
static const struct mii_timestamper myphy_mii_ts = {
.rxtstamp = myphy_rxtstamp,
.txtstamp = myphy_txtstamp,
.hwtstamp = myphy_hwtstamp,
};
Temperature 센서와 PHY 진단 (TDR)
현대 PHY 칩은 내부에 온도 센서를 탑재하고 있어, 런타임에 PHY 다이 온도를 모니터링할 수 있습니다. 커널 6.x부터는 hwmon 서브시스템과 통합되어 sensors 명령이나 sysfs를 통해 PHY 온도를 읽을 수 있는 드라이버가 늘고 있습니다. 또한 TDR(Time Domain Reflectometry) 기반 케이블 테스트는 PHY 내부 DSP를 이용해 케이블 길이, 단선/단락 위치, 쌍별 상태를 진단합니다.
| 진단 기능 | PHY 지원 필요 | 커널 API | 사용자 공간 도구 | 주요 용도 |
|---|---|---|---|---|
| 온도 센서 | 벤더 레지스터 | hwmon 또는 phy_driver 직접 | sensors, sysfs | 열 보호, 환경 모니터링 |
| Cable Test (기본) | TDR 하드웨어 | cable_test ethtool op | ethtool --cable-test | open/short/ok 상태 |
| Cable Test TDR | TDR DSP | cable_test_tdr ethtool op | ethtool --cable-test-tdr | 거리별 반사 파형, 정밀 진단 |
| SNR | 벤더 레지스터 | 벤더 드라이버 | 벤더 도구 또는 직접 레지스터 | 신호 품질 정량 측정 |
# PHY 온도 확인 (hwmon 통합 드라이버, 예: Marvell Alaska)
sensors | grep -A2 "phy"
cat /sys/class/hwmon/hwmon*/temp1_input # 밀리도 단위
# 케이블 테스트 (기본) - 각 쌍의 open/short/ok 상태
ethtool --cable-test eth0
# TDR 케이블 테스트 - 거리별 반사 파형
ethtool --cable-test-tdr eth0
# 벤더 레지스터에서 온도 직접 읽기 (예: Realtek RTL8211F)
mdio eth0 mmd 31 0xa43 # 페이지 전환 후
mdio eth0 0x1a # 온도 레지스터
/* PHY 드라이버에서 cable test 지원 (개념 예시) */
static int myphy_cable_test_start(struct phy_device *phydev)
{
/* PHY에게 TDR 테스트 시작 명령 */
return phy_write(phydev, MYPHY_REG_CABLE_TEST, 0x8000);
}
static int myphy_cable_test_get_status(struct phy_device *phydev,
bool *finished)
{
int val = phy_read(phydev, MYPHY_REG_CABLE_RESULT);
if (val < 0)
return val;
*finished = !!(val & 0x8000);
if (*finished) {
/* 각 쌍 결과를 ethnl_cable_test_result()로 보고 */
ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_A,
(val & 0x03) == 0 ?
ETHTOOL_A_CABLE_RESULT_CODE_OK :
ETHTOOL_A_CABLE_RESULT_CODE_OPEN);
}
return 0;
}
static struct phy_driver myphy_drivers[] = {
{
/* ... 기존 콜백 ... */
.cable_test_start = myphy_cable_test_start,
.cable_test_get_status = myphy_cable_test_get_status,
},
};
Copper/Fiber 전환 미디어 컨버터 처리
일부 PHY는 copper(RJ45)와 fiber(SFP) 인터페이스를 모두 지원하는 combo(dual-media) 포트를 제공합니다. 이런 PHY는 strap pin이나 소프트웨어 레지스터로 활성 매체를 선택하며, 일부는 자동 매체 감지(Auto Media Detect)를 지원해 케이블이 꽂힌 쪽을 자동으로 선택합니다.
| 방식 | 동작 | 장점 | 주의점 |
|---|---|---|---|
| 수동 선택 | strap pin 또는 레지스터로 고정 | 예측 가능, 단순 | 양쪽 동시 연결 시 혼란 |
| Auto Media Detect | PHY가 copper/fiber 중 활성 매체 자동 선택 | 운영 편의 | 전환 지연, 우선순위(Priority) 정책 필요 |
| 외부 미디어 컨버터 | 별도 장치가 copper와 fiber 사이를 변환 | 기존 장비 재사용 | 추가 지연, 전원, 장애점 증가 |
/* Combo PHY: copper와 fiber를 함께 지원하는 Device Tree 예시 */
&mdio {
combo_phy: ethernet-phy@1 {
reg = <1>;
/* 벤더 고유 속성으로 매체 선택 */
marvell,preferred-medium = "copper";
sfp = <&sfp0>;
};
};
/* 개념 예시: combo PHY 드라이버에서 매체 전환 처리 */
static int combo_phy_config_init(struct phy_device *phydev)
{
int val;
bool prefer_fiber = of_property_read_bool(
phydev->mdio.dev.of_node, "prefer-fiber");
/* 매체 선택 레지스터 설정 */
val = prefer_fiber ? 0x0002 : 0x0001;
return phy_modify_paged(phydev, 0x12, 0x14, 0x0003, val);
}
LED 제어와 PHY LED 트리거
PHY의 LED 핀은 링크 상태, 활동(activity), 속도, 전이중(full-duplex) 같은 네트워크 상태를 표시합니다. 전통적으로는 PHY 내부 레지스터로 LED 동작을 설정했지만, 커널 6.x부터는 netdev LED 트리거 프레임워크가 도입되어 PHY LED를 커널 LED 서브시스템에 통합할 수 있습니다. 이를 통해 사용자 공간에서 /sys/class/leds/를 통해 PHY LED 동작을 제어할 수 있습니다.
| LED 모드 | 의미 | 전통적 설정 | 커널 LED 트리거 |
|---|---|---|---|
| Link | 링크 상태 표시 | PHY 레지스터 direct | netdev trigger: link |
| Activity | TX/RX 트래픽 표시 | PHY 레지스터 direct | netdev trigger: tx rx |
| Speed | 속도별 다른 색상/깜빡임 | PHY 레지스터 직접 설정 | 커널 6.5+ 속도별 트리거 |
| Force On/Off | 강제 켜기/끄기 | PHY 레지스터 override | sysfs brightness |
# PHY LED를 커널 LED 서브시스템으로 확인 (드라이버 지원 시)
ls /sys/class/leds/ | grep "phy"
# LED 트리거 목록 확인
cat /sys/class/leds/eth0-led0/trigger
# netdev LED 트리거 설정: 링크 + 활동
echo "netdev" > /sys/class/leds/eth0-led0/trigger
echo "1" > /sys/class/leds/eth0-led0/link
echo "1" > /sys/class/leds/eth0-led0/tx
echo "1" > /sys/class/leds/eth0-led0/rx
# LED 강제 켜기/끄기
echo "255" > /sys/class/leds/eth0-led0/brightness
echo "0" > /sys/class/leds/eth0-led0/brightness
/* PHY 드라이버에서 LED 하드웨어 제어 콜백 등록 (커널 6.x) */
static int myphy_led_hw_is_supported(struct phy_device *phydev,
u8 index, unsigned long rules)
{
/* LED index와 규칙(link, activity, speed 등)이 HW에서 지원 가능한지 */
if (index > 1)
return -EINVAL;
return 0;
}
static int myphy_led_hw_control_set(struct phy_device *phydev,
u8 index, unsigned long rules)
{
u16 val = 0;
if (rules & BIT(TRIGGER_NETDEV_LINK))
val |= 0x01;
if (rules & BIT(TRIGGER_NETDEV_TX))
val |= 0x02;
if (rules & BIT(TRIGGER_NETDEV_RX))
val |= 0x04;
return phy_modify_paged(phydev, 0xd04,
MYPHY_REG_LED_CTRL + index,
0x07, val);
}
static struct phy_driver myphy_drivers[] = {
{
/* ... 기존 콜백 ... */
.led_hw_is_supported = myphy_led_hw_is_supported,
.led_hw_control_set = myphy_led_hw_control_set,
.led_hw_control_get = myphy_led_hw_control_get,
.led_brightness_set = myphy_led_brightness_set,
},
};
커널 버전별 phylink/phylib 변경 이력
phylib과 phylink는 커널 버전이 올라갈 때마다 기능이 확장되고 API가 변경됩니다. 드라이버를 작성하거나 기존 드라이버를 다른 커널 버전으로 포팅할 때는 이 변경 이력을 참고해야 합니다.
| 커널 버전 | phylib/phylink 주요 변경 | 영향 |
|---|---|---|
| 4.20 | phylink 프레임워크 병합 | SFP, in-band status, fixed-link 통합 관리 시작 |
| 5.2 | phy_device에 is_gigabit_capable 추가 | 1G 이상 PHY 감지 간소화 |
| 5.5 | phylink_pcs 인터페이스 도입 | host PCS를 별도 객체로 분리, MAC 드라이버 구조 변경 |
| 5.9 | phy_modify_mmd(), C45 헬퍼 강화 | Clause 45 레지스터 접근 편의성 향상 |
| 5.14 | EEE 설정 API 재구조화 | phy_init_eee() 폐지 방향, phylink EEE 통합 |
| 5.17 | cable_test / cable_test_tdr netlink 인터페이스 | 사용자 공간 케이블 진단 표준화 |
| 5.19 | SFP quirks 프레임워크 | 비표준 SFP 모듈 대응 개선 |
| 6.1 | PHY LED 프레임워크 (led_hw_control_* 콜백) | PHY LED를 커널 LED 서브시스템에 통합 |
| 6.3 | phylink_pcs_neg_mode() 도입 | PCS AN 모드 선택 명확화 |
| 6.5 | linkmode 2.5GBASE-T, 5GBASE-T 비트 추가 | NBASE-T PHY 속도 광고 표준화 |
| 6.6 | EEE 리워크 (struct eee_config) | EEE 설정이 phylink/ethtool에서 통합 관리 |
| 6.8 | phy_package 인프라 강화 | 공유 PHY 패키지 관리 개선 |
| 6.9 | Rate Matching 지원 (phy_rate_matching()) | MAC과 PHY 사이 속도 불일치 처리 |
| 6.11 | PHY LED netdev 속도별 트리거 | 1G/100M/10M 속도별 LED 색상/패턴 분리 |
phy_driver 구조체의 콜백 시그니처 변경에 특히 주의하세요. 예를 들어 read_status와 config_aneg의 반환값 의미, phylink_mac_ops의 콜백 목록, pcs_ops의 멤버 변경이 빈번합니다. 커널 drivers/net/phy/ 디렉터리의 Kconfig와 기존 벤더 드라이버 변경 이력을 함께 참고하세요.
PHY 패키지와 공유 PHY (Shared MDIO Bus)
다수의 PHY 벤더는 4포트, 8포트 PHY를 하나의 패키지(single IC)로 제공합니다. 이런 PHY 패키지는 MDIO 버스를 공유하고, 일부 레지스터(global configuration, LED 정책, 온도 센서)는 패키지 전체에 공통으로 적용됩니다. 리눅스 커널은 phy_package 인프라를 통해 이러한 공유 PHY를 관리합니다.
| 항목 | 개별 PHY 기준 | PHY 패키지 기준 | 주의점 |
|---|---|---|---|
| 초기화 | 각 PHY 개별 config_init | 패키지 글로벌 레지스터는 한 번만 | init 순서와 경합(Contention) 방지 |
| 온도 센서 | 각 PHY마다 별도 | 패키지 공통 1개 (또는 가장 뜨거운 다이) | hwmon 등록 중복 방지 |
| LED 정책 | 포트별 독립 | 글로벌 LED 모드 + 포트별 override | 글로벌 설정이 포트별 설정을 덮어쓸 수 있음 |
| reset | 포트별 가능 | 패키지 reset은 모든 포트에 영향 | 하나의 포트만 재설정하려면 soft reset 사용 |
| MDIO 주소 | 개별 주소 | 연속 주소 (base + offset) | base 주소는 strap pin으로 결정 |
/* 개념 예시: PHY 패키지 공유 레지스터 접근 */
static int myphy_package_config_init(struct phy_device *phydev)
{
int ret;
/* 패키지 내 첫 번째 PHY에서만 글로벌 초기화 수행 */
ret = phy_package_init_once(phydev);
if (ret == 0) {
/* 이 PHY가 첫 번째: 글로벌 레지스터 설정 */
phy_package_write(phydev, MYPHY_GLOBAL_LED_REG, 0x1234);
phy_package_write(phydev, MYPHY_GLOBAL_TEMP_REG, 0x0001);
}
/* 포트별 개별 설정 */
return myphy_port_config_init(phydev);
}
/* 패키지 내 PHY 사이에서 공유 잠금 사용 */
static int myphy_shared_reg_read(struct phy_device *phydev,
u16 reg)
{
int val;
phy_lock_mdio_bus(phydev);
val = __phy_package_read(phydev, reg);
phy_unlock_mdio_bus(phydev);
return val;
}
/* 4포트 PHY 패키지의 Device Tree 예시 */
&mdio {
ethernet-phy-package@8 {
#address-cells = <1>;
#size-cells = <0>;
reg = <8>;
phy0: ethernet-phy@8 { reg = <8>; };
phy1: ethernet-phy@9 { reg = <9>; };
phy2: ethernet-phy@a { reg = <0xa>; };
phy3: ethernet-phy@b { reg = <0xb>; };
};
};
Rate Matching과 MAC-PHY 속도 불일치 처리
Rate Matching(속도 정합)은 PHY가 원격 링크 파트너와 협상한 속도와 MAC-PHY 사이 SerDes 링크 속도가 다를 때 그 차이를 흡수하는 메커니즘입니다. 예를 들어 SGMII나 QSGMII 인터페이스에서 SerDes는 항상 1.25 Gbps(1000BASE-X 기준)로 고정 동작하는데, PHY가 원격 파트너와 100 Mbps Auto-Negotiation에 성공하면 속도 차이가 4배에 달합니다. 이 때 Rate Matching 없이 그냥 데이터를 보내면 MAC과 PHY 사이의 흐름 제어가 무너집니다.
Rate Matching이 필요한 대표적인 인터페이스는 SGMII, QSGMII, USXGMII partial 등입니다. 이 인터페이스들은 SerDes 속도를 링크 협상 결과에 따라 바꿀 수 없거나, 바꾸려면 재초기화가 필요하기 때문에 PHY나 중간 PCS 계층에서 속도 차이를 메워야 합니다. 반면 RGMII 같은 병렬 인터페이스는 클럭 자체가 속도에 따라 달라지므로 Rate Matching 개념이 적용되지 않습니다.
Rate Matching 세 가지 모드
리눅스 커널은 enum phy_rate_matching으로 세 가지 Rate Matching 모드를 정의합니다.
| 모드 | 상수 | 동작 방식 | 장점 | 단점 |
|---|---|---|---|---|
| Pause 기반 | PHY_RATE_MATCH_PAUSE |
PHY가 SerDes와 실제 링크 속도 차이만큼 PAUSE 프레임을 주기적으로 생성해 MAC쪽 흐름을 제어 | 구현 단순, 추가 버퍼 불필요 | PAUSE 프레임이 대역폭을 소비, 레이턴시 증가 가능 |
| CRS 기반 | PHY_RATE_MATCH_CRS |
PHY가 Carrier Sense 신호를 반 듀플렉스(half-duplex) 방식으로 조작해 MAC 송신을 자연스럽게 억제 | PAUSE 프레임 오버헤드 없음 | 반 듀플렉스 환경에서만 적용, 전이중(full-duplex)에는 부적합 |
| MAC측 적응 | PHY_RATE_MATCH_NONE(+MAC 지원) |
MAC 드라이버가 PHY 링크 속도에 따라 SerDes 클럭을 동적으로 변경 | 가장 깔끔한 해결책 | MAC 하드웨어가 SerDes 재설정을 지원해야 함 |
커널 API
PHY 드라이버는 phy_driver의 .get_rate_matching 콜백을 구현해 자신이 지원하는 Rate Matching 모드를 알려 줍니다. phylink는 링크 협상 결과와 SerDes 모드를 보고 적절한 모드를 선택합니다.
/* include/linux/phy.h */
enum phy_rate_matching {
PHY_RATE_MATCH_NONE = 0, /* Rate Matching 미지원 또는 불필요 */
PHY_RATE_MATCH_PAUSE = 1, /* PAUSE 프레임으로 속도 불일치 흡수 */
PHY_RATE_MATCH_CRS = 2, /* CRS 조작으로 반이중 억제 */
};
/* PHY 드라이버가 구현하는 콜백 */
int (*get_rate_matching)(struct phy_device *phydev,
phy_interface_t iface);
phylink 내부에서는 phylink_get_link_timer_ns() 흐름과 연동되어, Rate Matching이 필요한 구간에서 자동으로 PAUSE 협상 활성화나 CRS 모드 전환이 이루어집니다.
PHY 드라이버 구현 예시
/* PHY 드라이버가 get_rate_matching 콜백을 구현하는 예시 */
static int example_get_rate_matching(struct phy_device *phydev,
phy_interface_t iface)
{
/* SGMII 또는 QSGMII 인터페이스에서만 Rate Matching 지원 */
if (iface == PHY_INTERFACE_MODE_SGMII ||
iface == PHY_INTERFACE_MODE_QSGMII) {
/*
* 이 PHY는 PAUSE 기반 Rate Matching을 지원합니다.
* phylink는 이 정보를 바탕으로 PAUSE 협상을 활성화합니다.
*/
return PHY_RATE_MATCH_PAUSE;
}
/* 그 외 인터페이스(RGMII 등)는 Rate Matching 불필요 */
return PHY_RATE_MATCH_NONE;
}
static struct phy_driver example_phy_driver = {
.phy_id = 0x001CC916,
.phy_id_mask = 0x001FFFFF,
.name = "Example PHY",
.features = PHY_GBIT_FEATURES,
.get_rate_matching = example_get_rate_matching,
/* ... */
};
- SGMII/QSGMII SerDes를 사용하는 SoC에서 10/100 Mbps 장비와 연결할 때
- SerDes 재초기화 없이 속도 변경이 불가능한 하드웨어 구조일 때
- RGMII처럼 클럭 속도가 링크 속도에 따라 변하는 인터페이스
- MAC이 SerDes 클럭을 동적으로 재설정 가능한 경우 (PHY_RATE_MATCH_NONE + MAC 측 처리)
- Gigabit 전용 링크여서 속도 불일치가 발생하지 않는 환경
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 업데이트 필요 |
펌웨어 로딩 문제 해결
펌웨어 관련 문제는 크게 세 가지 유형으로 나뉩니다.
- 파일 없음:
/lib/firmware/에 펌웨어 파일이 없습니다.linux-firmware패키지를 설치하거나 linux-firmware 저장소에서 직접 파일을 복사하세요. initramfs에 펌웨어를 포함시켜야 부팅 초기에 로드할 수 있습니다. - 버전 불일치: PHY 하드웨어 리비전(Revision)과 펌웨어 버전이 맞지 않으면 PHY가 reset 루프에 빠지거나 링크를 올리지 못합니다.
ethtool -m ethX로 PHY가 보고하는 펌웨어 버전을 확인하세요. - 부팅 속도 저하: 10G PHY 펌웨어는 수백 KB에서 수 MB에 달하며 MDIO(최대 2.5 Mbps)로 전송하면 수 초가 걸릴 수 있습니다.
request_firmware_nowait()로 비동기 로딩을 사용하면 네트워크가 필요한 시점까지 부팅을 지연시키지 않을 수 있습니다.
linux-firmware 저장소(git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git)를 통해 별도 배포됩니다. 배포판(Distro)에서는 linux-firmware 패키지로 제공되며, 임베디드 시스템에서는 rootfs 또는 initramfs에 펌웨어 파일을 직접 포함시켜야 합니다. 펌웨어 파일의 재배포 가능 여부는 각 칩 제조사의 EULA를 반드시 확인하시기 바랍니다.
Wake-on-LAN PHY 수준 상세 구현
Wake-on-LAN(WoL)은 시스템이 S3(Suspend-to-RAM) 또는 S5(Soft-off) 상태일 때 네트워크에서 오는 특정 패킷을 PHY가 감지해 시스템을 깨우는 기능입니다. PHY 수준에서는 완전히 전원을 차단하는 대신 아날로그 수신(Rx) 회로만 켜 두고, 디지털 처리 부분과 송신(Tx) 회로는 최소 전력 상태로 유지합니다. Magic Packet을 감지하면 PME(Power Management Event) 핀 또는 인터럽트를 통해 SoC나 PCH에 웨이크업 신호를 보냅니다.
Magic Packet 구조
Magic Packet은 동기화 스트림(6바이트의 0xFF)과 그 뒤에 오는 대상 MAC 주소의 16회 반복으로 구성됩니다. 총 102바이트(6+6×16=102)의 페이로드가 UDP 또는 Raw Ethernet 프레임에 담겨 전송됩니다.
/* Magic Packet 구조 (총 102 bytes payload) */
/* 1단계: 동기화 스트림 (6 bytes) */
FF FF FF FF FF FF
/* 2단계: 대상 MAC 주소 16회 반복 (6 bytes x 16 = 96 bytes) */
/* 대상 MAC이 AA:BB:CC:DD:EE:FF 라면: */
AA BB CC DD EE FF /* 1번째 */
AA BB CC DD EE FF /* 2번째 */
...
AA BB CC DD EE FF /* 16번째 */
/* 선택적: 4~6 bytes 패스워드 (SecureOn) */
/* PHY 하드웨어가 패스워드 검증을 지원하는 경우에만 사용 */
WoL 중 PHY 전원 도메인
WoL 동작 중 PHY 내부의 전원 도메인은 다음과 같이 분리 관리됩니다.
| 전원 도메인 | WoL 대기 상태 | 이유 |
|---|---|---|
| 아날로그 Rx (PMA) | 활성(Active) | 케이블에서 들어오는 신호를 계속 수신해야 함 |
| 클럭 복구(CDR) | 활성 | 수신 데이터의 비트 동기를 유지해야 함 |
| MAC 패턴 매처 | 활성 | Magic Packet 패턴 비교 엔진 동작 |
| 아날로그 Tx (PMA) | 비활성(Off) | WoL 대기 중 송신 불필요, 전력 절약 |
| 디지털 MAC 인터페이스 | 비활성 | MAC(SoC)이 절전 상태이므로 RGMII/SGMII 클럭 없음 |
| MDIO 슬레이브 | 최소 활성 | 일부 칩은 WoL 상태 확인을 위해 MDIO 응답 유지 |
PHY 드라이버 WoL 콜백 구현
#include <linux/ethtool.h>
#include <linux/phy.h>
/* WoL 활성화/비활성화 콜백 */
static int example_set_wol(struct phy_device *phydev,
struct ethtool_wolinfo *wol)
{
int ret;
if (wol->wolopts & ~(WAKE_MAGIC | WAKE_PHY)) {
/* 이 PHY가 지원하지 않는 WoL 옵션 */
return -EOPNOTSUPP;
}
if (wol->wolopts & WAKE_MAGIC) {
/*
* 1단계: PHY에 자신의 MAC 주소를 프로그래밍합니다.
* PHY는 이 주소를 기준으로 Magic Packet을 필터링합니다.
*/
ret = phy_write_mmd(phydev, MDIO_MMD_VEND1,
EXAMPLE_WOL_MAC_HI,
(wol->sopass[0] << 8) | wol->sopass[1]);
if (ret)
return ret;
ret = phy_write_mmd(phydev, MDIO_MMD_VEND1,
EXAMPLE_WOL_MAC_MID,
(wol->sopass[2] << 8) | wol->sopass[3]);
if (ret)
return ret;
ret = phy_write_mmd(phydev, MDIO_MMD_VEND1,
EXAMPLE_WOL_MAC_LO,
(wol->sopass[4] << 8) | wol->sopass[5]);
if (ret)
return ret;
/* 2단계: WoL 기능 활성화 */
ret = phy_set_bits_mmd(phydev, MDIO_MMD_VEND1,
EXAMPLE_WOL_CTRL,
EXAMPLE_WOL_MAGIC_EN | EXAMPLE_WOL_PME_EN);
if (ret)
return ret;
/* 3단계: PHY를 WoL 모드로 설정 (저전력 + Rx 유지) */
ret = phy_set_bits(phydev, MII_BMCR, BMCR_PDOWN);
/* 주의: Rx 회로는 유지하는 칩별 설정이 필요합니다 */
} else {
/* WoL 비활성화: 정상 모드 복귀 */
ret = phy_clear_bits_mmd(phydev, MDIO_MMD_VEND1,
EXAMPLE_WOL_CTRL,
EXAMPLE_WOL_MAGIC_EN | EXAMPLE_WOL_PME_EN);
}
return ret;
}
/* 현재 WoL 설정 조회 콜백 */
static void example_get_wol(struct phy_device *phydev,
struct ethtool_wolinfo *wol)
{
int val;
/* 이 PHY가 지원하는 WoL 옵션 목록 */
wol->supported = WAKE_MAGIC | WAKE_PHY;
wol->wolopts = 0;
val = phy_read_mmd(phydev, MDIO_MMD_VEND1, EXAMPLE_WOL_CTRL);
if (val < 0)
return;
if (val & EXAMPLE_WOL_MAGIC_EN)
wol->wolopts |= WAKE_MAGIC;
}
static struct phy_driver example_phy_driver = {
/* ... */
.set_wol = example_set_wol,
.get_wol = example_get_wol,
};
커널 ethtool WoL API
phylib는 상위 ethtool 인터페이스와 PHY 드라이버의 set_wol/get_wol 콜백을 연결하는 래퍼(Wrapper) 함수를 제공합니다.
/* include/linux/phy.h */
/* ethtool_ops.get_wol에서 호출 */
int phy_ethtool_get_wol(struct phy_device *phydev,
struct ethtool_wolinfo *wol);
/* ethtool_ops.set_wol에서 호출 */
int phy_ethtool_set_wol(struct phy_device *phydev,
struct ethtool_wolinfo *wol);
/* MAC 드라이버의 ethtool_ops 연결 예시 */
static const struct ethtool_ops example_mac_ethtool_ops = {
.get_wol = phy_ethtool_get_wol,
.set_wol = phy_ethtool_set_wol,
/* ... */
};
Device Tree wakeup-source 속성
&mdio0 {
phy0: ethernet-phy@0 {
compatible = "ethernet-phy-id001c.c916";
reg = <0>;
/*
* 이 노드가 시스템 wakeup source임을 선언합니다.
* ACPI가 없는 임베디드 시스템에서 사용합니다.
* device_init_wakeup()이 자동으로 호출됩니다.
*/
wakeup-source;
/*
* WoL을 위한 인터럽트 핀 연결.
* PME# 핀이 GPIO에 연결된 경우 필요합니다.
*/
interrupts-extended = <&gpio1 5 IRQ_TYPE_LEVEL_LOW>;
};
};
WoL 모드 비교
| 모드 | ethtool 상수 | 동작 | PHY 하드웨어 지원 |
|---|---|---|---|
| Magic Packet | WAKE_MAGIC |
FF x6 + MAC x16 패턴 감지 | 대부분 지원 (가장 일반적) |
| PHY 링크 변화 | WAKE_PHY |
링크 업/다운 이벤트 자체로 웨이크업 | 대부분 지원 |
| 유니캐스트 | WAKE_UCAST |
자신의 MAC 주소로 오는 임의 프레임 | 일부 고급 PHY |
| 멀티캐스트 | WAKE_MCAST |
멀티캐스트 주소 수신 시 웨이크업 | 드물게 지원 |
| 브로드캐스트 | WAKE_BCAST |
브로드캐스트 프레임 수신 시 웨이크업 | 일부 PHY |
| ARP | WAKE_ARP |
ARP 요청 수신 시 웨이크업 | 드물게 지원, MAC에서 처리하는 경우도 있음 |
| Magic Packet + 패스워드 | WAKE_MAGICSECURE |
Magic Packet에 SecureOn 패스워드 추가 검증 | 일부 PHY (sopass 필드 활용) |
일반적인 WoL 문제와 해결책
- PHY 전원 레일 차단: suspend 중 보드 설계가 PHY 전원(3.3V 또는 1.0V 코어)을 완전히 차단하면 WoL이 동작하지 않습니다. 보드 회로도에서 PHY의 VDDIO와 VDD_CORE가 절전 모드에서도 유지되는지 확인하세요. Device Tree의
regulator설정 중regulator-always-on또는regulator-boot-on이 필요할 수 있습니다. - MAC 주소 미프로그래밍: PHY가 자신의 MAC 주소를 모르면 Magic Packet을 필터링할 수 없습니다.
set_wol콜백이 호출될 때phydev->attached_dev->dev_addr를 읽어 PHY 레지스터에 써 주는 코드가 필요합니다. 일부 드라이버는net_device의 MAC 주소가 바뀔 때마다 PHY를 다시 프로그래밍해야 합니다. - PME# 핀 연결 누락: PHY의 PME# 출력이 SoC의 웨이크업 입력 핀에 연결되지 않으면 신호가 전달되지 않습니다. 보드 설계 단계에서 반드시 확인하고, Device Tree에
interrupts-extended로 연결을 명시해야 합니다. - 커널 wakeup source 미등록: Device Tree에
wakeup-source속성이 없거나, 드라이버에서device_init_wakeup()을 호출하지 않으면 ACPI/PM 서브시스템이 이 장치를 웨이크업 소스로 인식하지 못합니다.
- PHY 전원 유지: suspend 중 PHY에 최소 전원이 공급되어야 합니다. 보드 설계 시 절전 상태에서도 PHY 전원 레일이 유지되도록 PowerSEQ나 PMIC 설정을 확인하세요.
- PME# 라우팅: PHY PME# 신호선이 SoC 웨이크업 핀 또는 PCIe PME# 핀에 보드 레벨에서 물리적으로 연결되어 있어야 합니다. 이 연결이 없으면 소프트웨어로는 해결할 수 없습니다.
- 케이블 연결 유지: WoL은 링크가 맺힌 상태에서만 동작합니다. 케이블이 뽑혀 있으면 Magic Packet 자체가 도달하지 않습니다. 일부 PHY는
WAKE_PHY모드로 링크 복구 후 웨이크업을 지원하기도 합니다. - 스위치/라우터 지원: 네트워크 스위치나 라우터가 Magic Packet을 올바른 포트로 포워딩해야 합니다. VLAN 환경에서는 Tagged/Untagged 설정도 고려해야 합니다.
ethtool 통계와 PHY 카운터 해석 실전 가이드
ethtool -S <interface> 명령은 드라이버가 노출하는 하드웨어 통계 카운터를 출력합니다. 이 카운터들은 NIC(Network Interface Card) MAC 계층과 PHY 계층에서 독립적으로 집계되므로, 어떤 카운터가 증가하는지 조합해서 보면 문제의 위치를 MAC인지 PHY인지, 더 나아가 케이블인지 상대방 장비인지까지 좁혀 낼 수 있습니다.
카운터 분류 개요
ethtool -S 출력은 드라이버마다 이름이 다르지만, 기능상으로 다음 세 범주로 분류할 수 있습니다.
- MAC Rx/Tx 카운터 — MAC이 직접 집계합니다. 프레임이 MAC에 도달한 이후의 오류(FCS, 길이, 정렬)를 반영합니다.
- PHY Rx/Tx 카운터 — PHY 내부 레지스터(RMON, Clause 45 PCS 등)에서 읽어옵니다. 케이블과 아날로그 경계에 가까운 오류를 반영합니다.
- 큐·흐름 제어 카운터 — DMA FIFO 언더런/오버런, TX 타임아웃, 링 버퍼 drop 등 드라이버 레벨 문제를 반영합니다.
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만 증가할 때: 프레임은 완성됐으나 FCS가 틀렸다는 뜻이므로, 케이블이나 RGMII 타이밍이 원인일 가능성이 높습니다. MAC 내부 버그보다 PHY 또는 보드 배선 문제를 먼저 의심하세요.
- rx_frame_errors + rx_crc_errors 동시 증가: PHY의 비트 동기화 문제이거나, 케이블이 손상돼 프리앰블이 잘린 것입니다.
- rx_dropped + rx_missed_errors 증가, 오류 카운터 정상: 프레임 자체는 올바르지만 시스템이 소화하지 못하는 것이므로, MAC Rx FIFO나 CPU/DMA 처리 속도 문제입니다.
- collisions 증가 (Full-duplex 환경): Auto-negotiation이 실패해 한쪽은 Full-duplex, 다른 쪽은 Half-duplex로 동작하는 Duplex mismatch 상태입니다.
ethtool eth0으로 양단 모드를 즉시 확인하세요. - symbol_errors 증가: PCS/PMA 수준의 물리적 오류입니다. 1GbE 구리에서는 케이블 감쇠나 크로스토크(Crosstalk), SFP 광 링크에서는 수신 광 출력 부족(
ethtool -m eth0의 RX power 확인)을 점검하세요.
카운터 진단 흐름도
지속 모니터링 스크립트
일회성 조회 대신 카운터 변화량을 지속적으로 관찰하면 간헐적 오류도 포착할 수 있습니다.
# 방법 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_stats와 ethtool_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
Device Tree 실전 예제 모음
Device Tree(장치 트리)에서 이더넷 PHY를 올바르게 기술하는 것은 보드 bring-up의 핵심입니다. 이 절은 다양한 토폴로지별 완성된 예제를 제공하며, 각 속성의 용도와 자주 발생하는 실수를 함께 설명합니다.
RGMII + 외부 PHY (STM32MP1 + RTL8211F)
STM32MP1 SoC의 GMAC과 Realtek RTL8211F PHY를 RGMII 모드로 연결하는 가장 일반적인 예제입니다. RGMII 인터페이스는 1 Gbps에서 클럭 스큐가 매우 민감하므로 지연(delay) 속성을 정확히 기술해야 합니다.
ðernet0 {
status = "okay";
pinctrl-0 = <ðernet0_rgmii_pins_a>;
pinctrl-1 = <ðernet0_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>;
};
};
};
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를 감지할 수 없으므로 권장합니다.
Fixed-link (PHY 없는 직결)
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-link와 phy-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 참조 */
ð0 {
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를 개별 참조 */
ð0 { phy-handle = <&phy_pkg0>; phy-mode = "rgmii-id"; }
ð1 { phy-handle = <&phy_pkg1>; phy-mode = "rgmii-id"; }
ð2 { phy-handle = <&phy_pkg2>; phy-mode = "rgmii-id"; }
ð3 { 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 드라이버 문서를 참고하세요.
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);
}
}
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/MII | SFP, 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);
}
adjust_link / mac_link_up 올바르게 구현하기
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를 { }로 두면 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;
}
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);
}
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-id | PHY가 TX/RX 모두 지연 추가 | PHY 내부 | PCB에서 이미 보상했는데 이중 지연 |
rgmii-txid | PHY가 TX만 지연 추가 | PHY TX + PCB RX | TX/RX를 혼동하여 반대로 설정 |
rgmii-rxid | PHY가 RX만 지연 추가 | PHY RX + PCB TX | TX/RX를 혼동하여 반대로 설정 |
sgmii | SerDes 기반 1.25 Gbps | 해당 없음 | 1000BASE-X와 혼동, control word 해석 차이 |
mii / rmii | 10/100M 전용 | PHY/MAC/PCB 조합 | ref_clk 방향(입력/출력) 미설정 |
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 콜백이 config_init을 다시 호출하지 않는 드라이버가 간혹 있습니다. 이 경우 RGMII delay bit, LED 모드, downshift 정책이 strap 기본값으로 복귀하여 부팅 직후와 다른 동작을 보입니다. PHY 드라이버의 resume 구현을 확인하고, 필요하면 MAC 드라이버에서 PHY soft reset 후 재연결을 시도하세요.
초보 NIC 드라이버 개발자의 흔한 실수 TOP 10
아래는 커널 메일링 리스트와 실무에서 반복적으로 보고되는 PHY 관련 실수를 정리한 것입니다. 새 드라이버를 작성하거나 기존 드라이버를 수정할 때 체크리스트로 활용하세요.
| # | 실수 | 증상 | 올바른 처리 |
|---|---|---|---|
| 1 | adjust_link/mac_link_up을 빈 함수로 둠 | 속도 재협상 후 CRC 폭증, "링크 업인데 통신 불가" | speed, duplex, pause를 MAC 레지스터에 반드시 반영 |
| 2 | phy-mode 불일치 | 1G에서만 CRC 에러, 100M에서는 정상 | 보드 회로도 확인, PHY 데이터시트의 지연 보상 위치 맞추기 |
| 3 | MDIO bus 등록 전에 PHY 연결 시도 | probe 실패, NULL 포인터 | of_mdiobus_register() → of_phy_connect() 순서 보장 |
| 4 | deferred probe 미처리 | 부팅 순서에 따라 간헐적 PHY 연결 실패 | -EPROBE_DEFER 반환으로 재시도 허용 |
| 5 | phy_disconnect() 전에 MDIO bus 해제 | 커널 oops, use-after-free | 반드시 PHY disconnect → MDIO unregister → MDIO free 순서 |
| 6 | ethtool PHY ops 미연결 | ethtool eth0에서 속도/듀플렉스 정보 없음 | phy_ethtool_* 또는 phylink_ethtool_* 헬퍼 연결 |
| 7 | phy_start() 호출 누락 | 인터페이스 up 해도 링크 상태 머신 미가동, 항상 링크 다운 | ndo_open에서 phy_start() 반드시 호출 |
| 8 | resume에서 PHY 재초기화 누락 | suspend/resume 후 링크 불안정 또는 속도 강등 | resume 경로에서 phy_start() 재호출, PHY 드라이버 resume 콜백 확인 |
| 9 | phy_set_max_speed() 미호출 | MAC이 100M만 지원하는데 PHY가 1G 광고 → 상대편과 1G 협상 → 프레임 손실 | MAC 하드웨어 능력에 맞게 최대 속도 제한 |
| 10 | fixed-link인데 phy-handle 기술 | 존재하지 않는 PHY를 탐색하느라 probe 지연 또는 실패 | PHY 없는 직결 구성은 fixed-link만 사용, 둘을 동시에 기술하지 않기 |
첫 보드 Bring-up 시 최소 동작 검증 절차
새 MAC 드라이버를 작성한 후, 또는 새 보드에 기존 드라이버를 포팅한 후 PHY 동작을 확인하는 최소 절차입니다.
- 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) - 링크 업 확인 — 케이블 연결 후
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: 링크 다운 - 속도/듀플렉스 확인 —
ethtool eth0으로 협상된 속도와 듀플렉스가 기대값인지 확인합니다.ethtool eth0 # Speed: 1000Mb/s, Duplex: Full, Auto-negotiation: on - ping 테스트 — 작은 패킷과 큰 패킷 모두 테스트합니다.
ping -c 10 192.168.1.1 ping -c 10 -s 1472 192.168.1.1 # MTU 근처 크기 - 에러 카운터 확인 —
ethtool -S eth0으로 CRC, carrier, frame 에러가 증가하지 않는지 확인합니다.ethtool -S eth0 | grep -iE 'err|crc|drop|miss|carrier' # 모든 에러 카운터가 0 또는 증가하지 않아야 정상 - 속도 강제 변경 테스트 — 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 # 원복 - 링크 플랩 테스트 — 케이블 분리/재연결을 3~5회 반복하면서
dmesg에 링크 up/down 로그가 정상 출력되는지, 복구 후 통신이 되는지 확인합니다. - 대역폭 측정 —
iperf3로 실제 throughput이 링크 속도의 90% 이상인지 확인합니다. 현저히 낮으면 DMA, NAPI, 또는 PHY 설정 문제일 수 있습니다.# 서버 측 iperf3 -s # 클라이언트 측 iperf3 -c 192.168.1.1 -t 30 # 1G 링크에서 ~940 Mbps 이상이면 정상
참고자료
- 이더넷 (Ethernet)
- Network Device 드라이버 (net_device)
- ethtool
- Device Tree
- DSA Tagging
- Linux kernel networking PHY documentation
- 이더넷 (Ethernet) — PHY가 전체 이더넷 설계 안에서 어디에 위치하는지 다시 연결
- Network Device 드라이버 — MAC 쪽 링크 콜백과 큐 제어 계약 이해
- ethtool — 운영 환경에서 링크 정책과 통계를 읽는 방법