U-Boot:以太网驱动简介
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 以太网设备的初始化
下面以 U-Boot 2016.05 代码下 ARMv7 架构代码为例,来说明 U-Boot 以太网设备的初始化过程。
/* arch/arm/lib/vector.S */
/* ARM 中断向量表 */
.globl _start /* U-Boot 入口 (由 arch/arm/cpu/u-boot.lds 链接脚本 指定) */
.section ".vectors", "ax"
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
...
/* arch/arm/cpu/armv7/start.S */
reset: /* CPU 复位中断入口 */
...
bl _main
/* arch/arm/lib/crt0.S */
ENTRY(_main)
...
mov r0, #0
bl board_init_f
...
ldr pc, =board_init_r
...
ENDPROC(_main)
前面一部分,是 U-Boot 启动汇编部分,做了一些基础的初始化工作,如 C 运行时环境构建 等工作。下面从 board_init_r() 开始,分析 以太网驱动 初始化过程。首先是 网络接口(RMII, RGMII等) 和 MDIO 总线 的 PIN 脚配置:
/* 以 AM335x 的 U-Boot 为例,假定配置的是 RGMII 千兆网口 */
board_init_f()
initcall_run_list(init_sequence_f)
board_early_init_f()
set_mux_conf_regs()
/* board/xxx/xxx/mux.c */
static struct module_pin_mux rgmii1_pin_mux[] = {
{OFFSET(mii1_crs), MODE(7)}, /* RESET */
//{OFFSET(rmii1_refclk), MODE(0) | RXACTIVE}, /* RGMII1_REFCLK */
/*
* RGMII PIN 配置 (12 PINs):
* RGMII1_TCTL, RGMII1_RCTL, RGMII1_TCLK, RGMII1_RCLK, RGMII1_TD0~3, RGMII1_RD0~3
*/
{OFFSET(mii1_txen), MODE(2)}, /* RGMII1_TCTL */
{OFFSET(mii1_rxdv), MODE(2) | RXACTIVE}, /* RGMII1_RCTL */
{OFFSET(mii1_txd3), MODE(2)}, /* RGMII1_TD3 */
{OFFSET(mii1_txd2), MODE(2)}, /* RGMII1_TD2 */
{OFFSET(mii1_txd1), MODE(2)}, /* RGMII1_TD1 */
{OFFSET(mii1_txd0), MODE(2)}, /* RGMII1_TD0 */
{OFFSET(mii1_txclk), MODE(2)}, /* RGMII1_TCLK */
{OFFSET(mii1_rxclk), MODE(2) | RXACTIVE}, /* RGMII1_RCLK */
{OFFSET(mii1_rxd3), MODE(2) | RXACTIVE}, /* RGMII1_RD3 */
{OFFSET(mii1_rxd2), MODE(2) | RXACTIVE}, /* RGMII1_RD2 */
{OFFSET(mii1_rxd1), MODE(2) | RXACTIVE}, /* RGMII1_RD1 */
{OFFSET(mii1_rxd0), MODE(2) | RXACTIVE}, /* RGMII1_RD0 */
/* MDIO 总线 PIN 配置: MDIO DATA,CLK */
{OFFSET(mdio_data), MODE(0) | RXACTIVE | PULLUP_EN},/* MDIO_DATA */
{OFFSET(mdio_clk), MODE(0) | PULLUP_EN}, /* MDIO_CLK */
{-1},
};
void set_mux_conf_regs(void)
{
...
configure_module_pin_mux(rgmii1_pin_mux);
...
}
接着是 PHY 和 以太网 MAC 芯片 的初始化:
/* common/board_r.c */
init_fnc_t init_sequence_r[] = {
...
#ifdef CONFIG_CMD_NET
...
initr_net,
#endif
...
};
board_init_r()
...
if (initcall_run_list(init_sequence_r))
hang();
...
/* lib/initcall.c */
initcall_run_list()
...
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
...
ret = (*init_fnc_ptr)(); /* initr_net() */
...
}
/* common/board_r.c */
initr_net()
puts("Net: ");
eth_initialize(); /* 以 legacy 驱动模式 为例 */
...
/* net/eth_legacy.c */
eth_initialize()
...
eth_devices = NULL;
eth_current = NULL;
/* 1. MII 和 PHY 芯片初始化 */
eth_common_init();
...
#if defined(CONFIG_MII) || defined(CONFIG_CMD_MII) || defined(CONFIG_PHYLIB)
miiphy_init();
INIT_LIST_HEAD(&mii_devs);
current_mii = NULL;
#endif
#ifdef CONFIG_PHYLIB
phy_init(); /* 注册 PHY 芯片驱动 */
#endif
/* 2. 每个板型特定的 以太网 MAC 芯片 相关的初始化 */
if (board_eth_init != __def_eth_init) {
if (board_eth_init(gd->bd) < 0) /* 这里以 TI 的 AM335X 板型为例加以说明 */
printf("Board Net Initialization Failed\n");
} else if (cpu_eth_init != __def_eth_init) { /* CPU 架构类型 特定的 以太网初始化 */
...
} else {
printf("Net Initialization Skipped\n");
}
if (!eth_devices) { /* 没有找到 或 没有成功初始化 以太网芯片 */
puts("No ethernet found.\n");
...
} else { /* 成功初始化 以太网芯片 */
...
}
...
下面分别看一下 PHY 芯片 和 以太网 MAC 芯片 的 初始化过程。先看 PHY 芯片驱动 的注册过程:
/* drivers/net/phy/phy.c */
phy_init();
...
#ifdef CONFIG_PHY_ATHEROS
phy_atheros_init();
#endif
...
/* drivers/net/phy/aetheros.c */
static struct phy_driver AR8021_driver = {
.name = "AR8021",
.uid = 0x4dd040,
.mask = 0x4ffff0,
.features = PHY_GBIT_FEATURES,
.config = ar8021_config,
.startup = genphy_startup,
.shutdown = genphy_shutdown,
};
static struct phy_driver AR8031_driver = {
.name = "AR8031/AR8033",
.uid = 0x4dd074,
.mask = 0xffffffef,
.features = PHY_GBIT_FEATURES,
.config = ar8031_config,
.startup = genphy_startup,
.shutdown = genphy_shutdown,
};
static struct phy_driver AR8035_driver = {
.name = "AR8035",
.uid = 0x4dd072,
.mask = 0xffffffef,
.features = PHY_GBIT_FEATURES,
.config = ar8035_config,
.startup = genphy_startup,
.shutdown = genphy_shutdown,
};
/* 向系统注册了3个类型 PHY 芯片的驱动 */
phy_atheros_init()
phy_register(&AR8021_driver);
phy_register(&AR8031_driver);
phy_register(&AR8035_driver);
...
/* drivers/net/phy/phy.c */
phy_register()
INIT_LIST_HEAD(&drv->list);
list_add_tail(&drv->list, &phy_drivers); /* 添加到 PHY 驱动列表 */
再看特定板型的 以太网芯片 初始化过程:
/* board/myirtech/myd_c335x/myd_c335x.c */
board_eth_init()
...
/* 设置为 RMII 接口模式 & 使能时钟 */
writel(RMII_MODE_ENABLE | RMII_CHIPCKL_ENABLE, &cdev->miisel);
/* 复位 PHY 芯片 */
board_phy_init();
/* 注册 以太网设备 到系统 */
rv = cpsw_register(&cpsw_data);
struct cpsw_priv *priv; /* 以太网设备 对象 私有数据 */
struct eth_device *dev; /* 以太网设备 对象 */
/* 创建 以太网设备 对象 */
dev = calloc(sizeof(*dev), 1);
...
/* 创建 以太网设备 对象 私有数据 */
priv = calloc(sizeof(*priv), 1);
priv->dev = dev;
priv->data = *data;
/* 设置 以太网设备 对象 名称 和 接口 */
strcpy(dev->name, "cpsw");
dev->iobase = 0;
dev->init = cpsw_init;
dev->halt = cpsw_halt;
dev->send = cpsw_send;
dev->recv = cpsw_recv;
dev->priv = priv;
/* 注册 以太网设备 到系统 */
eth_register(dev);
...
if (!eth_devices) {
eth_devices = dev;
eth_current = dev;
...
} else {
...
}
dev->state = ETH_STATE_INIT;
dev->next = eth_devices;
dev->index = index++;
...
/* 注册 MDIO 总线, 连接 以太网芯片 和 PHY 芯片 */
ret = _cpsw_register(priv);
...
/* 以太网芯片 上连接的 slave (PHY 芯片) */
priv->slaves = malloc(sizeof(struct cpsw_slave) * data->slaves);
...
/* 注册 MDIO 总线 到系统 */
cpsw_mdio_init(priv->dev->name, data->mdio_base, data->mdio_div);
struct mii_dev *bus = mdio_alloc();
...
bus->read = cpsw_mdio_read;
bus->write = cpsw_mdio_write;
strcpy(bus->name, name);
mdio_register(bus);
...
/* add it to the list */
list_add_tail(&bus->link, &mii_devs);
if (!current_mii)
current_mii = bus;
...
...
/* 以太网芯片 上连接的 slave (PHY 芯片) 的 初始化 和 连接 */
for_each_slave(slave, priv)
cpsw_phy_init(priv, slave);
...
/* 关联 以太网芯片 和 PHY 芯片 */
phydev = phy_connect(priv->bus, slave->data->phy_addr,
priv->dev, slave->data->phy_if);
...
priv->phydev = phydev; /* 关联 PHY 和 以太网芯片 MAC */
/* 初始配置 PHY 芯片 */
phy_config(phydev);
...
...
来仔细看下 以太网芯片 和 PHY 芯片 的连接过程 phy_connect()。期间扫描 MDIO 总线上的 PHY 设备,读取 PHY 芯片 ID,为扫描到的 PHY 创建设备对象,加载匹配的 PHY 驱动,并调用 PHY 驱动 .startup 回调接口进行 PHY 的初始化。
phy_connect() /* drivers/net/phy/phy.c */
struct phy_device *phydev;
phydev = phy_find_by_mask(bus, 1 << addr, interface); /* 创建 phy 设备对象(struct phy_device) */
if (phydev)
phy_connect_dev(phydev, dev); /* 关联 以太网芯片设备 和 PHY 设备 */
else
printf("Could not get PHY for %s: addr %d\n", bus->name, addr);
return phydev;
phy_find_by_mask()
/* Reset the bus */
if (bus->reset) {
bus->reset(bus);
/* Wait 15ms to make sure the PHY has come out of hard reset */
udelay(15000);
}
return get_phy_device_by_mask(bus, phy_mask, interface);
get_phy_device_by_mask()
int i;
struct phy_device *phydev;
...
/* Try Standard (ie Clause 22) access */
/* Otherwise we have to try Clause 45 */
for (i = 0; i < 5; i++) {
phydev = create_phy_by_mask(bus, phy_mask,
i ? i : MDIO_DEVAD_NONE, interface);
...
if (phydev)
return phydev;
}
...
create_phy_by_mask()
u32 phy_id = 0xffffffff;
while (phy_mask) {
int addr = ffs(phy_mask) - 1;
int r = get_phy_id(bus, addr, devad, &phy_id); /* 读取 PHY 芯片 ID, 用于匹配 PHY 驱动 */
/* If the PHY ID is mostly f's, we didn't find anything */
if (r == 0 && (phy_id & 0x1fffffff) != 0x1fffffff)
return phy_device_create(bus, addr, phy_id, interface);
phy_mask &= ~(1 << addr);
}
return NULL;
phy_device_create()
struct phy_device *dev;
dev = malloc(sizeof(*dev));
...
memset(dev, 0, sizeof(*dev));
dev->duplex = -1;
dev->link = 0;
dev->interface = interface;
dev->autoneg = AUTONEG_ENABLE;
dev->addr = addr;
dev->phy_id = phy_id;
dev->bus = bus;
dev->drv = get_phy_driver(dev, interface);
phy_probe(dev); /* PHY 驱动 probe */
phydev->advertising = phydev->supported = phydev->drv->features;
phydev->mmds = phydev->drv->mmds;
if (phydev->drv->probe)
err = phydev->drv->probe(phydev);
bus->phymap[addr] = dev;
return dev;
get_phy_driver()
...
/* 找到匹配的 PHY 驱动 */
list_for_each(entry, &phy_drivers) { /* 这里正是前面 eth_initialize() -> phy_init() 注册的 PHY 驱动 */
drv = list_entry(entry, struct phy_driver, list);
if ((drv->uid & drv->mask) == (phy_id & drv->mask))
return drv;
}
...
phy_connect() 触发 PHY 驱动的 .startup 回调进行 PHY 的初始化,phy_config() 触发 PHY 驱动的 .config 回调对 PHY 进行进一步配置:
phy_config(phydev)
board_phy_config(phydev);
if (phydev->drv->config) {
return phydev->drv->config(phydev);
}
到此,以太网芯片 和 PHY 芯片 初始配置工作已经完成,可以用来收发数据了。
3. 以太网设备的数据收发过程
以 U-Boot 的 ping 命令为例,来说明 以太网设备的数据收发过程。
/* cmd/net.c */
do_ping()
...
net_ping_ip = string_to_ip(argv[1]); /* ping 的 IP 地址 */
...
if (net_loop(PING) < 0) {
/* ping 失败了 */
printf("ping failed; host %s is not alive\n", argv[1]);
return CMD_RET_FAILURE;
}
printf("host %s is alive\n", argv[1]);
...
/* net/net.c */
net_loop()
...
/* 网络系统初始化 */
net_init();
static int first_call = 1;
if (first_call) {
net_tx_packet = &net_pkt_buf[0] + (PKTALIGN - 1);
net_tx_packet -= (ulong)net_tx_packet % PKTALIGN;
for (i = 0; i < PKTBUFSRX; i++) {
net_rx_packets[i] = net_tx_packet + (i + 1) * PKTSIZE_ALIGN;
}
arp_init();
arp_wait_packet_ethaddr = NULL;
net_arp_wait_packet_ip.s_addr = 0;
net_arp_wait_reply_ip.s_addr = 0;
arp_wait_tx_packet_size = 0;
arp_tx_packet = &arp_tx_packet_buf[0] + (PKTALIGN - 1);
arp_tx_packet -= (ulong)arp_tx_packet % PKTALIGN;
net_clear_handlers();
net_set_udp_handler(NULL);
net_set_arp_handler(NULL);
net_set_timeout_handler(0, NULL);
/* Only need to setup buffer pointers once. */
first_call = 0;
}
/* 读取 以太网 MAC 到 @net_ethaddr */
net_init_loop();
if (eth_get_dev()) // @eth_current
memcpy(net_ethaddr, eth_get_ethaddr(), 6);
...
...
/*
* eth_is_on_demand_init() 返回非0值, 表示 以太网芯片
* 在需要真正进行通信的时候, 才进行具体芯片硬件相关的
* 初始化, 也就是推迟芯片硬件相关的初始化工作.
*/
if (eth_is_on_demand_init() || protocol != NETCONS) {
eth_halt();
eth_set_current();
ret = eth_init(); /* 以太网芯片初始化 */
...
eth_current->init(eth_current, gd->bd) = cpsw_init()
_cpsw_init(priv, dev->enetaddr);
/* 各种 以太网芯片的 寄存器配置 */
...
/* 连接的各个 PHY 相关初始化 */
for_active_slave(slave, priv)
cpsw_slave_init(slave, priv);
/* 连接的各个 PHY link 状态相关初始化:启动 PHY 等工作 */
cpsw_update_link(priv);
for_active_slave(slave, priv)
cpsw_slave_update_link(slave, priv, &link);
...
phy_startup(phy); /* 启动 PHY */
/* 调用 PHY 驱动的 .startup 回调 */
if (phydev->drv->startup)
return phydev->drv->startup(phydev);
*link = phy->link;
/* 接口模式: RGMII, RMII, MII */
if (*link) { /* link up */
mac_control = priv->data.mac_control;
if (phy->speed == 1000)
mac_control |= GIGABITEN;
if (phy->duplex == DUPLEX_FULL)
mac_control |= FULLDUPLEXEN;
if (phy->speed == 100)
mac_control |= MIIEN;
}
...
/*
* 我们熟悉的日志来了,如:
* link up on port 0, speed 100, full duplex
*/
if (mac_control) {
printf("link up on port %d, speed %d, %s duplex\n",
slave->slave_num, phy->speed,
(phy->duplex == DUPLEX_FULL) ? "full" : "half");
} else {
printf("link down on port %d\n", slave->slave_num);
}
...
...
...
...
}
...
switch (net_check_prereq(protocol)) { /* 做一些合法性检查 */
...
case 0: /* 检查结果 ok, 执行协议相关的 初始化(如 ICMP ping) */
net_dev_exists = 1;
net_boot_file_size = 0;
switch (protocol) {
...
#if defined(CONFIG_CMD_PING)
case PING:
ping_start();
printf("Using %s device\n", eth_get_name()); /* 熟悉吧: Using cpsw device */
net_set_timeout_handler(10000UL, ping_timeout_handler); /* 设置 ping 超时处理接口 */
time_handler = f;
time_start = get_timer(0);
time_delta = iv * CONFIG_SYS_HZ / 1000;
ping_send(); /* 构建 ICMP 包 + 发送 ARP 请求 */
uchar *pkt;
int eth_hdr_size;
...
net_arp_wait_packet_ip = net_ping_ip;
eth_hdr_size = net_set_ether(net_tx_packet, net_null_ethaddr, PROT_IP);
pkt = (uchar *)net_tx_packet + eth_hdr_size;
set_icmp_header(pkt, net_ping_ip); /* 构建 ICMP 包头部 */
/* size of the waiting packet */
arp_wait_tx_packet_size = eth_hdr_size + IP_ICMP_HDR_SIZE;
/* and do the ARP request */
arp_wait_try = 1;
arp_wait_timer_start = get_timer(0);
arp_request(); /* 发送 ARP 请求把对方的 MAC 地址要过来 */
...
arp_raw_request(net_ip, net_null_ethaddr, net_arp_wait_reply_ip);
uchar *pkt;
struct arp_hdr *arp;
int eth_hdr_size;
...
pkt = arp_tx_packet;
eth_hdr_size = net_set_ether(pkt, net_bcast_ethaddr, PROT_ARP);
pkt += eth_hdr_size;
arp = (struct arp_hdr *)pkt;
/* 构建 ARP 请求头部 */
arp->ar_hrd = htons(ARP_ETHER);
arp->ar_pro = htons(PROT_IP);
arp->ar_hln = ARP_HLEN;
arp->ar_pln = ARP_PLEN;
arp->ar_op = htons(ARPOP_REQUEST);
/* 构建 ARP 请求 数据报 内容 */
memcpy(&arp->ar_sha, net_ethaddr, ARP_HLEN); /* source ET addr */
net_write_ip(&arp->ar_spa, source_ip); /* source IP addr */
memcpy(&arp->ar_tha, target_ethaddr, ARP_HLEN); /* target ET addr */
net_write_ip(&arp->ar_tpa, target_ip); /* target IP addr */
/* 通过 以太网芯片 将 ARP 请求包发送出去 */
net_send_packet(arp_tx_packet, eth_hdr_size + ARP_HDR_SIZE);
eth_send(pkt, len);
eth_current->send(eth_current, packet, length);
cpsw_send()
return 1; /* waiting */
break;
#endif
...
}
...
}
...
/* 网络包 收发循环 */
for (;;) {
...
if (arp_timeout_check() > 0) /* ARP 请求 响应 超时检测 */
time_start = get_timer(0);
/*
* 收取网络包。
* 如发 ping 命令时,
* 第一次收取 ARP 请求的响应数据;
* 第二次收取 ICMP 包的 echo 包。
*/
eth_rx();
eth_current->recv(eth_current);
cpsw_recv() /* 以太网芯片的接收数据 */
...
len = _cpsw_recv(priv, &pkt); /* 从以太网芯片读取接收的数据 */
/* 网络协议栈 处理 进入的数据报 */
if (len > 0) {
net_process_received_packet(pkt, len); /* net/net.c */
...
net_rx_packet = in_packet; /* 接收的数据缓冲 */
net_rx_packet_len = len;
et = (struct ethernet_hdr *)in_packet;
...
eth_proto = ntohs(et->et_protlen); /* 提取 数据报 的 协议类型 */
...
switch (eth_proto) {
case PROT_ARP: /* 处理 ARP 请求 的 回应数据报 */
arp_receive(et, ip, len);
struct arp_hdr *arp;
struct in_addr reply_ip_addr;
...
arp = (struct arp_hdr *)ip; /* ARP 请求响应头部 */
...
switch (ntohs(arp->ar_op)) {
...
case ARPOP_REPLY: /* arp reply */
...
reply_ip_addr = net_read_ip(&arp->ar_spa); /* ARP 请求的目标 IP (这里是 ping 的 IP) */
/* matched waiting packet's address */
if (reply_ip_addr.s_addr == net_arp_wait_reply_ip.s_addr) { /* 匹配目标地址 */
/* set the mac address in the waiting packet's header
and transmit it */
/* 设置 ping 目标 IP 的 MAC 地址 */
memcpy(((struct ethernet_hdr *)net_tx_packet)->et_dest,
&arp->ar_sha, ARP_HLEN);
/* 好了,现在有了目标 IP 和 其 MAC,可以向其发送 ICMP 包了 */
net_send_packet(net_tx_packet, arp_wait_tx_packet_size);
/* no arp request pending now */
net_arp_wait_packet_ip.s_addr = 0;
arp_wait_tx_packet_size = 0;
arp_wait_packet_ethaddr = NULL;
}
return;
...
}
break;
...
case PROT_IP:
...
if (ip->ip_p == IPPROTO_ICMP) { /* 处理 ICMP 包的 echo */
receive_icmp(ip, len, src_ip, et);
...
switch (icmph->type) {
...
default:
#if defined(CONFIG_CMD_PING)
ping_receive(et, ip, len);
...
switch (icmph->type) {
case ICMP_ECHO_REPLY:
src_ip = net_read_ip((void *)&ip->ip_src);
if (src_ip.s_addr == net_ping_ip.s_addr)
/* 好的,成功收到对端对 ICMP 请求的 echo */
net_set_state(NETLOOP_SUCCESS);
return;
...
}
#endif
}
return;
} else if (ip->ip_p != IPPROTO_UDP) { /* Only UDP packets */
...
}
...
...
}
...
}
return len;
...
/* 协议包超时处理(如 ICMP 回应超时) */
if (time_handler &&
((get_timer(0) - time_start) > time_delta)) {
thand_f *x;
...
x = time_handler;
time_handler = (thand_f *)0;
(*x)(); /* ping_timeout_handler() */
}
/* 本次失败之后,可能再来一次之前(是否重来,取决于配置),重新进行以太网初始化 */
if (net_state == NETLOOP_FAIL)
ret = net_start_again(); /* 如果配置了重试(如 "netretry"="yes"),返回 NETLOOP_RESTART */
switch (net_state) {
case NETLOOP_RESTART: /* 重试 */
net_restarted = 1;
goto restart;
case NETLOOP_SUCCESS: /* 成功,做网络设置清理工作,然后退出循环 */
net_cleanup_loop();
...
goto done;
case NETLOOP_FAIL: /* 失败,并且没有配置重试,做网络设置清理工作,然后退出循环 */
net_cleanup_loop();
...
goto done;
}
}
到此,以太网芯片的整个数据的收发过程已经分析完毕。
值得一提的是,其中有1个错误报告信息是开发中常见的:在向目标机器发送 ARP 请求获取其 MAC 地址,可能出现的 ARP 请求超时:
arp_timeout_check()
ulong t;
...
t = get_timer(0);
/* check for arp timeout */
if ((t - arp_wait_timer_start) > ARP_TIMEOUT) {
arp_wait_try++;
if (arp_wait_try >= ARP_TIMEOUT_COUNT) { /* 重发了 ARP_TIMEOUT_COUNT 次,仍然没收到回应 */
puts("\nARP Retry count exceeded; starting again\n");
arp_wait_try = 0;
net_set_state(NETLOOP_FAIL);
} else {
arp_wait_timer_start = t;
arp_request(); /* 再次发送 ARP 请求 */
}
}
这时的错误报告信息内容如下:
ARP Retry count exceeded; starting again
遇到这种情况,可以尝试调大 ARP 请求超时时间 ARP_TIMEOUT 和 次数 ARP_TIMEOUT_COUNT,如果仍不成功,就可能硬件引发的问题,譬如 PHY 没有工作等等。
笔者本人遇到的一个实际案例,是硬件的 RMII 接口 和 PHY 复位脚 改变了,软件没有做相应修改,导致 ARP 广播没有办法从 PHY 发送出去,实际抓包,目标机器也没有收到对应的 ARP 广播包。

浙公网安备 33010602011771号