MDIO介绍
MDIO

我们平时看到网口映入眼帘的就是RJ45网口底座,也很少追究网口底座之下还有什么。最近在搞MDIO的驱动,对MAC、PHY之间的关系很陌生。而这两者也是组成网口的物理硬件,所以趁着这次机会弄清楚这两者之间的关系。
上图是组成网口的基本单元,一般SOC芯片内部会集成MAC,然后外接PHY芯片,PHY芯片最后连接RJ45。可以看到MAC与PHY之间有两组连线,其中MDIO是用于MAC与PHY之间的控制线;而GMII或者RGMII是MAC与PHY之间的数据传输线,网口的数据走的就是这组线。
1 MDIO
MDIO 全称是 Management Data Input/Output,直译过来就是管理数据输入输出接口,是一个简单的两线串行接口,一根 MDIO 数据线,一根 MDC 时钟线。
驱动程序通过MDIO和MDC这两根线就可以访问PHY芯片的任意一个寄存器。和i2c总线一样,同一根MDIO总线下的器件地址是唯一的。同一时刻只能对一个PHY进行操作。
MDIO线是双向的数据线,在写PHY寄存器的时候,由MAC驱动MDIO向PHY写入数据;在读PHY寄存器的时候,会从PHY读取寄存器的值。
MAC通过Clause 22和Clause 45两种数据帧读写PHY寄存器。
1.2 Clause 22
Clause 22用于百兆或者千兆以太网设备。最多只能接入32个PHY芯片。Clause 22只有5bit的寄存器地址空间,因此最多只能访问32个寄存器。

Clause 22的数据帧格式如上图。Clause 22的数据帧分为读和写。
PRE:帧前导码,为32个连续“1”比特。OP:操作码,CL22中比特“10”表示此帧为一读操作帧,比特“01”表示此帧为一写操作帧MMD的物理地址,5个比特,每个MMD都把自己的地址与这5个比特进行比较,若匹配则响应后面的操作,若不匹配,则忽略掉后面的操作。REGAD:用来选MMD的32个寄存器中的某个寄存器的地址。DATA:帧的寄存器的数据域,16比特,若为读操作,则为MMD送到STA的数据,若为写操作,则为STA送到MMD数据。TA:用于地址传输和数据传输之间的空闲时间。由于读操作和写操作的控制方不同,设置2位的TA是为了防止MDIO总线上产生竞争。在读操作时,TA的第1位表示PHY和MAC都释放总线控制,输出高阻态。TA的第2位表示总线由PHY控制,输出低电平(0)。因此,读操作时TA为2'bZ0(由于总线上有上拉电阻存在,从总线上看到的依然是2'b10)。在写操作时,TA由MAC输出2'b10。
clause22里面的PHYAD 通常由PHY设备的硬件配置去决定的,值大小为0-31.注意PHYAD 与 PHY ID不要弄混了。 其中phy_addr可以通过ethtool eth0获取PHYAD值。
1.3 Clause 45
由于Clause 22访问的寄存器数量有限,因此在Clause 22的基础上推出了Clause 45,有16bit地址空间,最多能访问65536个寄存器。

PRE:帧前导码,为32个连续“1”比特。ST:帧开始标志,Clause22的开始标志为比特“01”。OP:操作码,CL22中比特“10”表示此帧为一读操作帧,比特“01”表示此帧为一写操作帧。PHYAD:MMD的物理地址,5个比特,每个MMD都把自己的地址与这5个比特进行比较,若匹配则响应后面的操作,若不匹配,则忽略掉后面的操作。REGAD:用来选MMD的32个寄存器中的某个寄存器的地址。TA:状态转换域,若为读操作,则第一比特时MDIO为高阻态,第二比特时由MMD使MDIO置“0”。若为写操作,则MDIO仍由STA控制,连续输出“10”两个比特。DATA:帧的寄存器的数据域,16比特,若为读操作,则为MMD送到STA的数据,若为写操作,则为STA送到MMD数据。IDLE:帧结束后的空闲状态,此时MDIO无源驱动,处高阻状态。
1.4 Clause 22扩展
Clause 22访问超过5bit地址空间的寄存器,可以使用PHY的13和14号寄存器。

把13号寄存器的Bit14:15写入Clause 45的OP字段,Bit0:4写入Clause45的DEVAD字段,然后14号寄存器作为Clause 45的数据寄存器。举个例子,github上有个开源的MDIO工具。
这个工具就是通过上述这种方式读写超过5bit地址空间的寄存器的。
Clause 22扩展读:
/* Register Control Register */
#define MII_REGCR 0x0d
/* Data register */
#define MII_ADDAR 0x0e
#define MII_ADDAR_DATA_NOINC 0xC000
static int mdio_read_extended_reg(int skfd, int mmd, int location)
{
// 13号寄存器Bit14:15=00,Bit0:4=mmd,表示需要读取的device
mdio_write(skfd, MII_REGCR, mmd);
// 需要把需要读取的寄存器地址发送PHY芯片
mdio_write(skfd, MII_ADDAR, location);
/* 13号寄存器Bit14:15=11,Bit0:4=mmd,表示要读取location寄存器数据
需要注意的是Bit14:15=11,表示在读写寄存器后,PHY芯片的地址指针会加一。
如果不想地址指针移动,可以根据上图填入合适的值。
*/
mdio_write(skfd, MII_REGCR, mmd | MII_ADDAR_DATA_NOINC);
// 从14号寄存器读取数据
return mdio_read(skfd, MII_ADDAR);
}
Clause 22扩展写:
static void mdio_write_extended_reg(int skfd, int mmd, int location, int value)
{
// 13号寄存器Bit14:15=00,Bit0:4=mmd,表示需要读取的device
mdio_write(skfd, MII_REGCR, mmd);
// 需要把需要读取的寄存器地址发送PHY芯片
mdio_write(skfd, MII_ADDAR, location);
/* 13号寄存器Bit14:15=11,Bit0:4=mmd,表示要读取location寄存器数据
需要注意的是Bit14:15=11,表示在读写寄存器后,PHY芯片的地址指针会加一。
如果不想地址指针移动,可以根据上图填入合适的值。
*/
mdio_write(skfd, MII_REGCR, mmd | MII_ADDAR_DATA_NOINC);
// 写入寄存器的数据
mdio_write(skfd, MII_ADDAR, value);
}
2 Aspeed 2600 MDIO驱动
Aspeed 2600芯片的驱动实际上很简单,就只需要读写MDC/MDIO Bus的控制寄存器和数据寄存器即可。

可以看到控制寄存器实际上就是按照Clause 22和Clause 45协议填入对应的字段。

Clause 45读:
static int aspeed_mdio_op(struct mii_bus *bus, u8 st, u8 op, u8 phyad, u8 regad,
u16 data)
{
struct aspeed_mdio *ctx = bus->priv;
u32 ctrl;
dev_dbg(&bus->dev, "%s: st: %u op: %u, phyad: %u, regad: %u, data: %u\n",
__func__, st, op, phyad, regad, data);
/*
把phyad、regad、st、op和data写入控制寄存器对应的比特位上;
如果是读操作,那data对应的bit位全部填0即可。
*/
ctrl = ASPEED_MDIO_CTRL_FIRE
| FIELD_PREP(ASPEED_MDIO_CTRL_ST, st)
| FIELD_PREP(ASPEED_MDIO_CTRL_OP, op)
| FIELD_PREP(ASPEED_MDIO_CTRL_PHYAD, phyad)
| FIELD_PREP(ASPEED_MDIO_CTRL_REGAD, regad)
| FIELD_PREP(ASPEED_MDIO_DATA_MIIRDATA, data);
/*
把组装好的控制寄存器的值写入寄存器
*/
iowrite32(ctrl, ctx->base + ASPEED_MDIO_CTRL);
/* Add dummy read to ensure triggering mdio controller */
(void)ioread32(ctx->base + ASPEED_MDIO_CTRL);
/*
循环读取bit31的值等于0为止,否则超时。读到为0,表示数据发送完成。
*/
return readl_poll_timeout(ctx->base + ASPEED_MDIO_CTRL, ctrl,
!(ctrl & ASPEED_MDIO_CTRL_FIRE),
ASPEED_MDIO_INTERVAL_US,
ASPEED_MDIO_TIMEOUT_US);
}
static int aspeed_mdio_read_c45(struct mii_bus *bus, int addr, int devad,
int regnum)
{
int rc;
/*
Clause 45读取寄存器的第一步就是把devad和regad地址发送给对应的PHY芯片
*/
rc = aspeed_mdio_op(bus, ASPEED_MDIO_CTRL_ST_C45, MDIO_C45_OP_ADDR,
addr, devad, regnum);
if (rc < 0)
return rc;
/*
Clause 45读取寄存器的第二步就是发送一条读命令给PHY芯片
*/
rc = aspeed_mdio_op(bus, ASPEED_MDIO_CTRL_ST_C45, MDIO_C45_OP_READ,
addr, devad, 0);
if (rc < 0)
return rc;
/*
从MDIO的数据寄存器读取PHY芯片返回的数据
*/
return aspeed_mdio_get_data(bus);
}
Clause 45写:
static int aspeed_mdio_op(struct mii_bus *bus, u8 st, u8 op, u8 phyad, u8 regad,
u16 data)
{
struct aspeed_mdio *ctx = bus->priv;
u32 ctrl;
dev_dbg(&bus->dev, "%s: st: %u op: %u, phyad: %u, regad: %u, data: %u\n",
__func__, st, op, phyad, regad, data);
/*
把phyad、regad、st、op和data写入控制寄存器对应的比特位上;
如果是读操作,那data对应的bit位全部填0即可。
*/
ctrl = ASPEED_MDIO_CTRL_FIRE
| FIELD_PREP(ASPEED_MDIO_CTRL_ST, st)
| FIELD_PREP(ASPEED_MDIO_CTRL_OP, op)
| FIELD_PREP(ASPEED_MDIO_CTRL_PHYAD, phyad)
| FIELD_PREP(ASPEED_MDIO_CTRL_REGAD, regad)
| FIELD_PREP(ASPEED_MDIO_DATA_MIIRDATA, data);
/*
把组装好的控制寄存器的值写入寄存器
*/
iowrite32(ctrl, ctx->base + ASPEED_MDIO_CTRL);
/* Add dummy read to ensure triggering mdio controller */
(void)ioread32(ctx->base + ASPEED_MDIO_CTRL);
/*
循环读取bit31的值等于0为止,否则超时。读到为0,表示数据发送完成。
*/
return readl_poll_timeout(ctx->base + ASPEED_MDIO_CTRL, ctrl,
!(ctrl & ASPEED_MDIO_CTRL_FIRE),
ASPEED_MDIO_INTERVAL_US,
ASPEED_MDIO_TIMEOUT_US);
}
static int aspeed_mdio_write_c45(struct mii_bus *bus, int addr, int devad,
int regnum, u16 val)
{
int rc;
/*
把devad和regad地址发送给对应的PHY芯片
*/
rc = aspeed_mdio_op(bus, ASPEED_MDIO_CTRL_ST_C45, MDIO_C45_OP_ADDR,
addr, devad, regnum);
if (rc < 0)
return rc;
/*
把需要发送的数据写入控制寄存器,把OP Code设置为MDIO_C45_OP_WRITE
*/
return aspeed_mdio_op(bus, ASPEED_MDIO_CTRL_ST_C45, MDIO_C45_OP_WRITE,
addr, devad, val);
}

浙公网安备 33010602011771号