简介:该资源为基于 Linux DSA 框架的 YT9215 switch 驱动源码包,面向嵌入式网络开发与交换机调试人员。驱动加载后会生成 lan* 网卡用于获取各网口状态,实际数据通信则依赖 eth* 网卡,并支持通过 VLAN 划分实现每个接口独立管理,具体划分方式可参考包内 patch 说明。压缩包为 rar 格式,共 3 个文件,约 12KB,包含 1 个 patch 补丁、1 个头文件与 1 个 C 源文件,分别用于设备树配置、寄存器定义与驱动主体实现,结构精简便于快速移植与二次开发。目前已有 455 人学习下载,适合需要理解 DSA 驱动框架、调试多网口交换机或进行 VLAN 隔离配置的开发者参考,可帮助快速掌握网口状态获取与数据转发分离的设计思路。
1. yt9215 switch 驱动:从一颗车机主控到可维护的以太网交换驱动
如果你手上有块 yt9215 车机主板,或者正在给基于这颗 SoC 的设备做网络功能,大概率会遇到同一个问题:板载的交换芯片怎么在 Linux 下跑起来。yt9215 本身是车机领域常见的多媒体主控,很多方案会把一颗以太网交换芯片挂在它的某个 MAC 接口上,用来扩展出多路网口。所谓 yt9215 switch 驱动,本质就是让内核认识这颗交换芯片、把每个物理口映射成独立的网络设备、并且能正确转发和配置 VLAN。它解决的不是「能不能上网」,而是「多路网口能不能被系统当成独立接口管理」。适合谁看:做车机、工控网关、多网口终端的嵌入式工程师,尤其是拿到 SDK 却不知道怎么把 switch 接进内核网络栈的人。下面按「先搞清硬件连接 → 再写驱动骨架 → 再调参数 → 再避坑」的顺序讲透。
2. 先搞清 yt9215 与交换芯片的硬件连接方式
2.1 三种常见挂载方式与选型判断
yt9215 这类主控通常带一到两个以太网 MAC 控制器,交换芯片和它之间的连接方式决定了驱动怎么写。常见做法有三种:第一种是 MAC 到 MAC 的 RGMII/RMII 直连,交换芯片的一个口作为 CPU 口,主控的 MAC 直接对接,这种最省事,驱动里只需要把 CPU 口当成普通 PHY 处理;第二种是主控通过 SPI 或 I2C 去配置交换芯片的寄存器,数据面仍然走 RGMII,这种需要两套驱动配合,控制面和数据面分离;第三种是交换芯片自带 PCIe 接口,主控通过 PCIe 枚举,这种在车机上少见但工控板上有。
判断你手上是哪种,最直接的办法是看原理图:找交换芯片的 RGMII/TXD 引脚有没有连到 yt9215 的 MAC 引脚,如果有,就是数据面直连;再看交换芯片的 SCL/SDA 或 SPI_CLK 有没有连到主控的 I2C/SPI 控制器,如果有,说明配置走独立总线。我一般会先用万用表确认 CPU 口的 TX_CLK 是否有 25MHz 或 50MHz 输出,没有时钟基本可以断定数据面没接对。
选型上,如果只是要扩展两三个百兆口,RGMII 直连加内核自带的 DSA 框架最稳;如果要配 VLAN 和端口镜像,SPI 配置型更灵活,但驱动工作量翻倍。别一上来就选最复杂的方案,先确认交换芯片型号是否在内核 DSA 支持列表里,不在的话就得自己写 switchdev 或纯 PHY 驱动。
2.2 用设备树描述 CPU 口与 PHY 的对应关系
不管哪种连接,设备树都是第一步。以 RGMII 直连为例,需要在 yt9215 的 MAC 节点下描述 CPU 口,并声明交换芯片的 mdio 总线。下面是一个最小可用的设备树片段,基于常见做法整理,具体寄存器地址要按你手上的芯片手册改。
/* yt9215 MAC0 对接交换芯片 CPU 口 */ &mac0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <&sw_phy0>; mdio { #address-cells = <1>; #size-cells = <0>; sw_phy0: ethernet-phy@0 { reg = <0>; /* CPU 口对应的 PHY 地址,查手册确认 */ }; }; }; /* 交换芯片配置总线,假设走 I2C1 */ &i2c1 { status = "okay"; switch@5f { compatible = "vendor,sw-chip"; reg = <0x5f>; /* 交换芯片复位脚 */ reset-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>; }; };逻辑说明:phy-mode = "rgmii-id"表示 MAC 内部做延迟,这是 RGMII 直连最常用的模式,如果时钟采样有问题可以改成rgmii或rgmii-txid。phy-handle指向 CPU 口对应的 PHY 节点,交换芯片内部通常把 CPU 口映射成一个固定 PHY 地址,这个地址必须查芯片手册,写错会导致eth0起不来。reset-gpios很关键,很多板子交换芯片复位没做好,内核跑起来后读寄存器全是 0xFFFF,就是复位时序不对。
参数怎么改:reg地址由硬件原理图上的片选或地址引脚决定,I2C 地址一般是 7 位,注意内核里写的是不含读写位的值。phy-mode如果写错,现象是ifconfig能看到接口但 ping 不通,或者大量 CRC 错误。改完设备树后,用dmesg | grep -i eth看 MAC 是否 probe 成功,再用mdio-tool读一下 PHY 的 ID 寄存器,能读到非 0xFFFF 的值说明 MDIO 通了。
3. 写一个能跑的最小 switch 驱动骨架
3.1 注册 mdio 总线与 PHY 设备
如果交换芯片不在内核 DSA 支持列表里,最省事的做法是先把它当成一组 PHY 来驱动,每个物理口注册一个phy_device,数据面靠 CPU 口透传。下面是一个基于mdio_device的骨架,展示怎么在 I2C 配置总线上注册 PHY。
#include <linux/module.h> #include <linux/i2c.h> #include <linux/phy.h> #include <linux/gpio/consumer.h> struct sw_chip { struct i2c_client *client; struct gpio_desc *reset_gpio; struct mii_bus *mdio_bus; }; static int sw_chip_mdio_read(struct mii_bus *bus, int addr, int reg) { struct sw_chip *chip = bus->priv; u8 buf[2]; int ret; /* 交换芯片内部寄存器通过 I2C 间接访问 */ ret = i2c_smbus_read_i2c_block_data(chip->client, reg & 0xff, 2, buf); if (ret < 0) return ret; return (buf[0] << 8) | buf[1]; } static int sw_chip_mdio_write(struct mii_bus *bus, int addr, int reg, u16 val) { struct sw_chip *chip = bus->priv; u8 buf[2] = { val >> 8, val & 0xff }; return i2c_smbus_write_i2c_block_data(chip->client, reg & 0xff, 2, buf); } static int sw_chip_probe(struct i2c_client *client) { struct sw_chip *chip; int ret; chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); if (!chip) return -ENOMEM; chip->client = client; chip->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", GPIOD_OUT_LOW); if (chip->reset_gpio) { gpiod_set_value_cansleep(chip->reset_gpio, 1); msleep(50); /* 复位保持 50ms,查手册确认 */ gpiod_set_value_cansleep(chip->reset_gpio, 0); msleep(100); } chip->mdio_bus = devm_mdiobus_alloc(&client->dev); if (!chip->mdio_bus) return -ENOMEM; chip->mdio_bus->name = "sw_chip_mdio"; chip->mdio_bus->read = sw_chip_mdio_read; chip->mdio_bus->write = sw_chip_mdio_write; chip->mdio_bus->priv = chip; chip->mdio_bus->parent = &client->dev; ret = devm_mdiobus_register(&client->dev, chip->mdio_bus); if (ret) return ret; i2c_set_clientdata(client, chip); dev_info(&client->dev, "switch chip probed\n"); return 0; }逻辑说明:sw_chip_mdio_read/write把标准 MDIO 读写转成 I2C 块读写,这是 SPI/I2C 配置型交换芯片最常见的做法。devm_mdiobus_alloc分配一个虚拟 MDIO 总线,注册后内核就能通过它扫描 PHY。复位时序里msleep(50)和msleep(100)是经验值,不同芯片要求不同,复位时间不够会导致 PHY ID 读出来是 0xFFFF。
参数说明:reg & 0xff是因为很多交换芯片的寄存器地址只有 8 位,高位用于区分页,如果你的芯片有分页机制,这里要改成先写页寄存器再读数据。GPIOD_OUT_LOW表示复位脚低有效,如果你的板子是高有效,改成GPIOD_OUT_HIGH并反转gpiod_set_value的参数。probe 成功后,/sys/class/mdio_bus/下会出现sw_chip_mdio,用ls能看到挂在上面的 PHY 地址。
3.2 把每个物理口映射成独立 net_device
光有 PHY 还不够,用户要的是eth1、eth2这样的独立接口。常见做法是用 DSA 框架的dsa_switch_ops,如果芯片不支持 DSA,就自己实现一个轻量级 net_device 包装。下面展示怎么在 DSA 框架下声明端口映射,这是最推荐的方式,因为内核已经帮你处理了桥接和 VLAN。
#include <net/dsa.h> static const struct dsa_port ports[] = { { .index = 0, .name = "cpu", .phy_mode = PHY_INTERFACE_MODE_RGMII }, { .index = 1, .name = "lan1", .phy_mode = PHY_INTERFACE_MODE_INTERNAL }, { .index = 2, .name = "lan2", .phy_mode = PHY_INTERFACE_MODE_INTERNAL }, { .index = 3, .name = "lan3", .phy_mode = PHY_INTERFACE_MODE_INTERNAL }, }; static int sw_dsa_setup(struct dsa_switch *ds) { int i; for (i = 0; i < ARRAY_SIZE(ports); i++) { if (ports[i].phy_mode == PHY_INTERFACE_MODE_INTERNAL) continue; /* 配置 CPU 口和外部口的 MAC 模式 */ dsa_port_setup(ds, ports[i].index); } return 0; } static const struct dsa_switch_ops sw_dsa_ops = { .setup = sw_dsa_setup, .phy_read = sw_chip_mdio_read, .phy_write = sw_chip_mdio_write, };逻辑说明:dsa_port数组里index对应交换芯片的物理端口号,cpu口是连到 yt9215 MAC 的那个口,lan1到lan3是面板上的网口。PHY_INTERFACE_MODE_INTERNAL表示这些口是芯片内部 PHY,不需要外部 MAC 配置。dsa_switch_ops里的phy_read/write直接复用前面实现的 MDIO 函数。
参数说明:index必须和硬件端口号一致,写错会导致lan1实际对应到别的物理口。phy_mode对 CPU 口要写实际连接方式,对内部口写INTERNAL。注册成功后,ip link会看到lan1、lan2等接口,默认都是 down 状态,需要ip link set lan1 up才能用。如果接口出不来,先检查dsa_switch_ops是否注册成功,看dmesg | grep dsa。
4. 交换芯片寄存器配置与 VLAN 参数怎么调
4.1 必调的四个寄存器组
交换芯片能跑起来只是第一步,要让它按预期转发,至少得配四组寄存器:端口使能、VLAN 表、转发规则、CPU 口 tagging。下面用表格列出常见寄存器偏移和推荐值,具体地址以你手上的手册为准,这里给的是同类芯片的典型布局。
| 寄存器组 | 典型偏移 | 作用 | 推荐值 |
|---|---|---|---|
| PORT_EN | 0x00 | 端口使能 | 0x1F(使能 0-4 口) |
| VLAN_MAP | 0x10 | 端口默认 VLAN | 1(所有口默认 VLAN 1) |
| FWD_CTRL | 0x20 | 转发规则 | 0x03(允许未知单播泛洪) |
| CPU_TAG | 0x30 | CPU 口 tagging | 0x01(启用 802.1Q tag) |
配置顺序很重要:先使能端口,再配 VLAN,最后开 CPU tagging。顺序反了会出现 CPU 口收到包但转发不出去。我一般会在驱动 probe 的最后一步统一写这些寄存器,写完后读回校验,读回值不一致说明 I2C 时序有问题。
参数怎么改:PORT_EN的位对应物理口,比如只用了 0-2 口就写 0x07。VLAN_MAP如果要做端口隔离,每个口写不同的 VLAN ID,然后在 Linux 侧用bridge vlan命令配合。CPU_TAG启用后,CPU 口收到的包会带 VLAN tag,内核需要能解析,否则tcpdump看到的是带 tag 的帧但协议栈不认。
4.2 用 bridge 和 vlan 命令验证转发
驱动跑起来后,验证转发最直接的办法是用 Linux 的 bridge 工具。假设lan1和lan2要互通,lan3隔离,可以这样配:
# 创建桥,把 lan1 lan2 加进去 ip link add br0 type bridge ip link set lan1 master br0 ip link set lan2 master br0 ip link set br0 up ip link set lan1 up ip link set lan2 up # lan3 单独配 VLAN 100 ip link add link lan3 name lan3.100 type vlan id 100 ip addr add 192.168.100.1/24 dev lan3.100 ip link set lan3.100 up # 查看 VLAN 表 bridge vlan show逻辑说明:ip link add br0 type bridge创建软件桥,交换芯片的硬件转发和软件桥可以共存,但要注意如果芯片已经做了硬件转发,软件桥可能看到重复包。lan3.100是在lan3上创建 VLAN 子接口,前提是交换芯片的 VLAN 表里lan3的 PVID 设成了 100。
参数说明:bridge vlan show会列出每个端口的 VLAN 配置,如果显示不全,说明交换芯片的 VLAN 表没写对。ip addr add的地址段要和你的网络规划一致。验证时用ping从lan1侧设备 pinglan2侧设备,通的话说明硬件转发正常;如果不通但tcpdump -i lan1能看到包,说明转发规则没配对。
5. yt9215 switch 驱动避坑:五个血泪翻车记录
5.1 复位时序不对导致 PHY ID 读成 0xFFFF
现象:驱动 probe 成功,但mdiobus下扫不到任何 PHY,或者读出来的 ID 全是 0xFFFF。原因:交换芯片复位脚保持时间不够,或者复位释放后没有等待内部 PLL 锁定。解决:把复位低电平时间从 10ms 加到 50ms 以上,释放后加 100ms 延时,再读 PHY ID。如果还不行,用示波器看复位脚波形,确认没有毛刺。
5.2 RGMII 延迟模式写错导致大量 CRC 错误
现象:接口能 up,ping小包通,大包丢,ifconfig看到 RX errors 和 CRC 计数飞涨。原因:phy-mode写成了rgmii但硬件实际需要rgmii-id,或者反过来。解决:依次试rgmii、rgmii-id、rgmii-txid、rgmii-rxid四种模式,每次改完重新编译设备树,用ethtool -S lan1看 CRC 错误是否归零。我一般会先试rgmii-id,因为大部分车机板子 MAC 侧不做延迟。
5.3 I2C 地址冲突导致配置寄存器写不进去
现象:驱动加载时i2c_smbus_write返回-EREMOTEIO,或者写进去读回来不变。原因:交换芯片的 I2C 地址和板上其他器件冲突,或者上拉电阻阻值不对。解决:用i2cdetect -y 1扫描总线,确认地址没被占用。如果地址冲突,改硬件片选或换总线。上拉电阻一般 4.7k,太小会导致波形上升沿过冲,太大导致上升沿变缓,I2C 时钟跑不到 400kHz。
5.4 CPU 口 tagging 没开导致 VLAN 包被丢弃
现象:配了 VLAN 子接口,tcpdump能看到带 tag 的包,但内核协议栈不处理,ping不通。原因:交换芯片 CPU 口的 tagging 没启用,或者内核的 VLAN 模块没加载。解决:确认CPU_TAG寄存器写的是启用状态,然后modprobe 8021q加载内核 VLAN 模块,再用ip link add link创建子接口。如果还不行,检查dmesg有没有unknown VLAN相关报错。
5.5 端口映射顺序和面板丝印不一致
现象:lan1实际对应面板上的第三个口,用户插错口找不到设备。原因:设备树里dsa_port的index按芯片端口号写,但面板丝印是按物理位置标的,两者顺序可能相反。解决:拿一根网线逐个口插,用ethtool lan1看 link 状态,确认哪个lanX对应哪个物理口,然后调整设备树里的index或改 udev 规则重命名。这个坑不致命但很烦,建议在驱动 probe 时打印每个端口的 PHY 地址和物理口对应关系。
6. 用 ethtool 和寄存器回读做驱动自检
驱动写完不是终点,得有一套自检手段。我习惯在 probe 成功后加一段回读逻辑:把关键寄存器写一遍再读回来,不一致就打印警告。下面是一个自检函数,放在sw_chip_probe最后调用。
static int sw_chip_self_test(struct sw_chip *chip) { u16 val; int ret; /* 回读 PORT_EN,确认端口使能生效 */ ret = sw_chip_mdio_read(chip->mdio_bus, 0, 0x00); if (ret < 0) { dev_err(&chip->client->dev, "read PORT_EN failed\n"); return ret; } val = ret; if ((val & 0x1F) != 0x1F) { dev_warn(&chip->client->dev, "PORT_EN mismatch: 0x%04x\n", val); } /* 回读 CPU_TAG,确认 tagging 开启 */ ret = sw_chip_mdio_read(chip->mdio_bus, 0, 0x30); if (ret >= 0 && !(ret & 0x01)) dev_warn(&chip->client->dev, "CPU tagging not enabled\n"); return 0; }逻辑说明:sw_chip_mdio_read复用前面的 MDIO 读函数,地址 0 是交换芯片内部 PHY 地址,寄存器 0x00 和 0x30 对应 PORT_EN 和 CPU_TAG。回读不一致时只警告不返回错误,因为有些芯片上电默认值就是对的,写不进去也不影响功能。
参数说明:0x1F是五个端口都使能的掩码,如果你的板子只用了三个口,改成0x07。0x30寄存器的 bit0 是 tagging 使能位,不同芯片可能在不同 bit,查手册确认。自检函数跑完后,用dmesg | grep sw_chip看有没有警告,有警告就按第 5 章的避坑条目排查。
进阶用法上,如果要做端口镜像,可以在dsa_switch_ops里实现port_mirror_add回调,把某个口的流量复制到 CPU 口,然后用tcpdump -i lan1抓包分析。验证方法很简单:配好镜像后,从lan2侧发广播包,在lan1上能抓到就说明镜像生效。我踩过的最大教训是别在驱动里硬编码寄存器地址,一定用宏定义并加注释,否则换一颗芯片就得重读一遍代码。希望帮到你。
本文还有配套的精品资源,点击获取