本文基于 U-Boot v2025.10 中 DWC Ethernet QOS 驱动相关代码。
Driver-specific operation set
下面开始介绍 DWC Ethernet QOS 驱动中的操作集,上层应用对以太网上数据收发等相关操作便是通过这些接口调用的。
static const struct eth_ops eqos_ops = {
.start = eqos_start,
.stop = eqos_stop,
.send = eqos_send,
.recv = eqos_recv,
.free_pkt = eqos_free_pkt,
.write_hwaddr = eqos_write_hwaddr,
.read_rom_hwaddr = eqos_read_rom_hwaddr,
};
eqos_start & eqos_stop
在 eqos_start()
中做了一系列的初始化工作以备后续的数据收发,主要是进行了复位 MAC、配置 PHY、配置 MAC、准备描述符这些操作,下面进一步的详细介绍如下:
ret = eqos->config->ops->eqos_start_resets(dev);
if (ret < 0) {
pr_err("eqos_start_resets() failed: %d\n", ret);
goto err;
}
udelay(10);
eqos->reg_access_ok = true;
/*
* Assert the SWR first, the actually reset the MAC and to latch in
* e.g. i.MX8M Plus GPR[1] content, which selects interface mode.
*/
setbits_le32(&eqos->dma_regs->mode, EQOS_DMA_MODE_SWR);
ret = wait_for_bit_le32(&eqos->dma_regs->mode,
EQOS_DMA_MODE_SWR, false,
eqos->config->swr_wait, false);
if (ret) {
pr_err("EQOS_DMA_MODE_SWR stuck\n");
goto err_stop_resets;
}
ret = eqos->config->ops->eqos_calibrate_pads(dev);
if (ret < 0) {
pr_err("eqos_calibrate_pads() failed: %d\n", ret);
goto err_stop_resets;
}
if (eqos->config->ops->eqos_get_tick_clk_rate) {
rate = eqos->config->ops->eqos_get_tick_clk_rate(dev);
val = (rate / 1000000) - 1;
writel(val, &eqos->mac_regs->us_tic_counter);
}
上面首先进行 PHY 的硬复位操作(如果 eqos_start_resets()
实现),然后尝试 MAC 的软复位,当 DMA_Mode
中的 SWR
置位后,MAC 和 DMA 控制器会复位 DMA、MTL 和 MAC 的逻辑和所有内部寄存器,并在所有复位完成后自动清除该位。
eqos_calibrate_pads()
的作用是在驱动初始化时触发一次以太网 PHY 接口 PAD 的电阻/电压自动调节,保证信号完整性。然后,如果 eqos_get_tick_clk_rate()
函数被定义,则获取时钟频率以计算 MAC_1US_Tic_Counter
的值,为 EQOS 以太网控制器生成精确的 1 微秒内部节拍信号,以便驱动 EEE(Energy Efficient Ethernet)等需要 1 µs 时间基准的硬件事件和统计计数器。
/*
* if PHY was already connected and configured,
* don't need to reconnect/reconfigure again
*/
if (!eqos->phy) {
int addr = -1;
ofnode fixed_node;
if (IS_ENABLED(CONFIG_PHY_FIXED)) {
fixed_node = ofnode_find_subnode(dev_ofnode(dev),
"fixed-link");
if (ofnode_valid(fixed_node))
eqos->phy = fixed_phy_create(dev_ofnode(dev));
}
if (!eqos->phy) {
addr = eqos_get_phy_addr(eqos, dev);
eqos->phy = phy_connect(eqos->mii, addr, dev,
eqos->config->interface(dev));
}
if (!eqos->phy) {
pr_err("phy_connect() failed\n");
ret = -ENODEV;
goto err_stop_resets;
}
if (eqos->max_speed) {
ret = phy_set_supported(eqos->phy, eqos->max_speed);
if (ret) {
pr_err("phy_set_supported() failed: %d\n", ret);
goto err_shutdown_phy;
}
}
eqos->phy->node = eqos->phy_of_node;
ret = phy_config(eqos->phy);
if (ret < 0) {
pr_err("phy_config() failed: %d\n", ret);
goto err_shutdown_phy;
}
}
ret = phy_startup(eqos->phy);
if (ret < 0) {
pr_err("phy_startup() failed: %d\n", ret);
goto err_shutdown_phy;
}
if (!eqos->phy->link) {
pr_err("No link\n");
ret = -EAGAIN;
goto err_shutdown_phy;
}
ret = eqos_adjust_link(dev);
if (ret < 0) {
pr_err("eqos_adjust_link() failed: %d\n", ret);
goto err_shutdown_phy;
}
这部分代码则是对 PHY 驱动进行配置与初始化。首先,如果开启了 CONFIG_PHY_FIXED,则尝试创建虚拟 PHY 设备,并跳过下面对物理 PHY 的初始化。
对于物理 PHY 的配置,在 eqos_get_phy_addr()
获取设备树中 phy-handle
节点 reg
所指定的 PHY 的地址,这个地址一般由 PHY 地址引脚的高低电平决定。获取到 PHY 地址后,由 phy_connect()
查找或创建 PHY 设备 (struct phy_device
)。
struct phy_device *phy_connect(struct mii_dev *bus, int addr,
struct udevice *dev,
phy_interface_t interface)
{
struct phy_device *phydev = NULL;
uint mask = (addr >= 0) ? (1 << addr) : 0xffffffff;
#ifdef CONFIG_PHY_FIXED
phydev = phy_connect_fixed(bus, dev);
#endif
#ifdef CONFIG_PHY_NCSI
if (!phydev && interface == PHY_INTERFACE_MODE_NCSI)
phydev = phy_device_create(bus, 0, PHY_NCSI_ID, false);
#endif
#ifdef CONFIG_PHY_ETHERNET_ID
if (!phydev)
phydev = phy_connect_phy_id(bus, dev, addr);
#endif
#ifdef CONFIG_PHY_XILINX_GMII2RGMII
if (!phydev)
phydev = phy_connect_gmii2rgmii(bus, dev);
#endif
if (!phydev)
phydev = phy_find_by_mask(bus, mask);
if (phydev)
phy_connect_dev(phydev, dev, interface);
else
printf("Could not get PHY for %s: addr %d\n", bus->name, addr);
return phydev;
}
这里在不考虑额外的 CONFIG_PHY_*
的情况下简单介绍 phy_connect()
的调用流程。
phy_connect()
-> phy_find_by_mask()
-> get_phy_device_by_mask()
-> search_for_existing_phy()
-> create_phy_by_mask()
-> get_phy_id()
-> phy_device_create()
-> get_phy_driver()
-> phy_connect_dev()
在调用 phy_connect()
时传入的 addr
(即 eqos_get_phy_addr()
获得的 PHY 地址)会首先通过计算 (addr >= 0) ? (1 << addr) : 0xffffffff
得到 mask
。然后在 get_phy_device_by_mask()
中,首先调用 search_for_existing_phy()
尝试在 bus->phymap[]
中查找已经注册的 PHY 设备,如果没有查找到已注册的 PHY 设备,则通过 create_phy_by_mask()
创建。
static struct phy_device *create_phy_by_mask(struct mii_dev *bus,
uint phy_mask, int devad)
{
u32 phy_id = 0xffffffff;
bool is_c45;
while (phy_mask) {
int addr = ffs(phy_mask) - 1;
int r = get_phy_id(bus, addr, devad, &phy_id);
/*
* If the PHY ID is flat 0 we ignore it. There are C45 PHYs
* that return all 0s for C22 reads (like Aquantia AQR112) and
* there are C22 PHYs that return all 0s for C45 reads (like
* Atheros AR8035).
*/
if (r == 0 && phy_id == 0)
goto next;
/* If the PHY ID is mostly f's, we didn't find anything */
if (r == 0 && (phy_id & 0x1fffffff) != 0x1fffffff) {
is_c45 = (devad == MDIO_DEVAD_NONE) ? false : true;
return phy_device_create(bus, addr, phy_id, is_c45);
}
next:
phy_mask &= ~(1 << addr);
}
return NULL;
}
create_phy_by_mask()
会由右至左遍历 phy_mask
(也就是之前由 addr
计算得到的 mask
)的每个 bit,在 get_phy_id()
中通过 MDIO 读取 PHY 的 PHY Identification Register1 (0x02)
与 PHY Identification Register2 (0x03)
。
获取到 PHY ID 后会调用 phy_device_create()
创建 PHY 设备并与 PHY 驱动匹配,如果从寄存器中读取到的 PHY ID 与 PHY 驱动中定义的 PHY ID 匹配,就会调用 phy_probe()
初始化 PHY 驱动。其中 PHY 驱动的定义如下,uid
与 mask
便是用于与从寄存器中读取到的 PHY ID 进行匹配的,有些类似于普通设备的设备树中的 compatible
字段。
U_BOOT_PHY_DRIVER(genphy) = {
.uid = 0xffffffff,
.mask = 0xffffffff,
.name = "Generic PHY",
.features = PHY_GBIT_FEATURES | SUPPORTED_MII |
SUPPORTED_AUI | SUPPORTED_FIBRE |
SUPPORTED_BNC,
.config = genphy_config,
.startup = genphy_startup,
.shutdown = genphy_shutdown,
};
通用 PHY 驱动的定义如上,如有特殊的需要可以另行实现如上形式的自定义 PHY 驱动,其中uid
与 mask
便是用于与从寄存器中读取到的 PHY ID 进行匹配的,类似于普通设备的设备树中的 compatible
字段。
phy_device_create()
会创建 PHY 设备并通过 get_phy_driver()
与 PHY 驱动匹配,如果从寄存器中读取到的 PHY ID 与驱动中定义的 uid & mask
匹配就会返回对应的驱动与设备进行绑定,然后调用 phy_probe()
初始化 PHY 驱动,最后将创建的 PHY 设备添加到 bus->phymap[]
中以便后续可以直接查找到设备。
如果 PHY 设备成功创建,则调用 phy_connect_dev()
将 PHY 设备与网卡设备进行绑定。此后,网卡驱动中便可以找到 PHY 设备并对 PHY 进行操作。
接下来是 phy_config()
与 phy_startup()
,它们会最终会分别调用到 PHY 驱动中的 .config
与 .startup
函数。
例如,
genphy
中的genphy_config
会通过读取 PHY 的Basic Status Register (0x01)
与Extended status register (0x0F)
,获得phydev->supported
与phydev->advertising
并调用genphy_config_aneg()
配置自协商。
phy_startup()
会首先在genphy_update_link()
中读取Basic Status Register (0x01)
获取 link 状态,如果开启了自协商,则会判断自协商完成状态;然后会在genphy_parse_link()
中读取Basic Control Register (0x00)
获取 PHY 支持的工作模式,如果开启了自协商则会读取Auto-Negotiation Advertisement (0x04)
、Auto-Negotiation Link Partner Ability (0x05)
与Basic Control Register (0x00)
获取链路两端自动协商最高的共同工作模式。
eqos_adjust_link()
会根据前面从 PHY 中获取到的工作模式来配置 MAC 的寄存器来配置全双工/半双工、link 速度等能力。
之后则是进行 DWC Ethernet QOS 的寄存器配置,依次配置了 MTL、MAC、DMA 相关的寄存器,因为这些寄存器过于繁杂,这里不一一进行介绍。
/* Set up descriptors */
memset(eqos->tx_descs, 0, eqos->desc_size * EQOS_DESCRIPTORS_TX);
memset(eqos->rx_descs, 0, eqos->desc_size * EQOS_DESCRIPTORS_RX);
for (i = 0; i < EQOS_DESCRIPTORS_TX; i++) {
struct eqos_desc *tx_desc = eqos_get_desc(eqos, i, false);
eqos->config->ops->eqos_flush_desc(tx_desc);
}
for (i = 0; i < EQOS_DESCRIPTORS_RX; i++) {
struct eqos_desc *rx_desc = eqos_get_desc(eqos, i, true);
addr64 = (ulong)(eqos->rx_dma_buf + (i * EQOS_MAX_PACKET_SIZE));
rx_desc->des0 = lower_32_bits(addr64);
rx_desc->des1 = upper_32_bits(addr64);
rx_desc->des3 = EQOS_DESC3_OWN | EQOS_DESC3_BUF1V;
mb();
eqos->config->ops->eqos_flush_desc(rx_desc);
eqos->config->ops->eqos_inval_buffer((void *)addr64, EQOS_MAX_PACKET_SIZE);
}
addr64 = (ulong)eqos_get_desc(eqos, 0, false);
writel(upper_32_bits(addr64), &eqos->dma_regs->ch0_txdesc_list_haddress);
writel(lower_32_bits(addr64), &eqos->dma_regs->ch0_txdesc_list_address);
writel(EQOS_DESCRIPTORS_TX - 1,
&eqos->dma_regs->ch0_txdesc_ring_length);
addr64 = (ulong)eqos_get_desc(eqos, 0, true);
writel(upper_32_bits(addr64), &eqos->dma_regs->ch0_rxdesc_list_haddress);
writel(lower_32_bits(addr64), &eqos->dma_regs->ch0_rxdesc_list_address);
writel(EQOS_DESCRIPTORS_RX - 1,
&eqos->dma_regs->ch0_rxdesc_ring_length);
/* Enable everything */
setbits_le32(&eqos->dma_regs->ch0_tx_control,
EQOS_DMA_CH0_TX_CONTROL_ST);
setbits_le32(&eqos->dma_regs->ch0_rx_control,
EQOS_DMA_CH0_RX_CONTROL_SR);
setbits_le32(&eqos->mac_regs->configuration,
EQOS_MAC_CONFIGURATION_TE | EQOS_MAC_CONFIGURATION_RE);
/* TX tail pointer not written until we need to TX a packet */
/*
* Point RX tail pointer at last descriptor. Ideally, we'd point at the
* first descriptor, implying all descriptors were available. However,
* that's not distinguishable from none of the descriptors being
* available.
*/
last_rx_desc = (ulong)eqos_get_desc(eqos, EQOS_DESCRIPTORS_RX - 1, true);
writel(last_rx_desc, &eqos->dma_regs->ch0_rxdesc_tail_pointer);
最后便是描述符相关的准备,Tx 描述符的准备比较简单,只是将全 0 数据同步 Cache 到内存中;而 Rx 描述符除同步 Cache 以外,还将 RX buffer (接收数据缓冲区)以 EQOS_MAX_PACKET_SIZE
分块的地址填入描述符中,并设置 Buffer 1 Address Valid
与 OWN
表示 RDES0 中的地址有效以及该描述符为 DMA 所有。
随后,将 Tx 与 Rx 第一个描述符的地址分别配置到 DMA_CH0_TxDesc_List_HAddress
、DMA_CH0_TxDesc_List_Address
与DMA_CH0_RxDesc_List_HAddress
、DMA_CH0_RxDesc_List_Address
中以及分别配置描述符的长度到 DMA_CH0_TxDesc_Ring_Length
与 DMA_CH0_RxDesc_Ring_Length
中。
最后的最后,配置 DMA_CH0_Tx_Control
与 DMA_CH0_Rx_Control
使能 Tx 与 Rx DMA,配置 MAC_Configuration
使能数据包的发送与接收。至此,DWC Ethernet QOS 数据包的接收与发送已全部准备完毕,而在获取 Rx 的最后一个描述符的地址并配置到 DMA_CH0_RxDesc_Tail_Pointer
后,数据包的接收便已经开始。
eqos_stop()
中,值得注意的是接收发送能力关闭的顺序,通过读取 MTL_TxQ0_Debug
与 MTL_RxQ0_Debug
以确保接收发送数据包传输完成,最后调用 phy_shutdown()
与 eqos_stop_resets()
完成最后的清理工作。