资讯
HOME
资讯
正文内容
linux nand 驱动分析 一文搞懂 Linux 网络 Phy 驱动
发布时间 : 2025-01-19
作者 : 小编
访问数量 : 23
扫码分享至微信

一文搞懂 Linux 网络 Phy 驱动

概述

数据结构

phy_device

phy_driver

mii_bus

net_device

phy 设备的注册

phy 驱动的注册

genphy_driver 通用驱动

NXP TJA 专有驱动

网卡 fec 和 Phy 的协作

自协商配置

link 状态读取

link 状态通知

Linux BSP实战课(网络篇):数据包的发送过程

Linux BSP实战课(网络篇):数据包的接收过程

概述

上图来自 瑞昱半导体 (RealTek) 的 RTL8201F 系列网卡 PHY 芯片手册。按OSI 7层网络模型划分,网卡PHY 芯片(图中的RTL8201F)位于物理层,对应的软件层就是本文讨论的 PHY 驱动层;而 MAC 位于 数据链路层,也是通常软件上所说的网卡驱动层,它不是本文的重点,不做展开。另外,可通过 MDIO 接口对 PHY 芯片进行配置(如PHY芯片寄存器读写),而 PHY 和 MAC 通过 MII/RMII 进行数据传输。

PHY芯片通过MII/GMII/RMII/SGMII/XGMII等多种媒体独立接口(介质无关接口)与数据链路层的MAC芯片相连,并通过MDIO接口实现对PHY 状态的监控、配置和管理。

PHY与MAC整体的连接框图:

数据结构

每个 phy 芯片会创建一个 struct phy_device 类型的设备,对应的有 struct phy_driver 类型的驱动,这两者实际上是挂载在 mdio_bus_type 总线上的,mac 会被注册成 struct net_device。

phy_device

struct phy_device {struct phy_driver *drv; // PHY设备驱动 struct mii_bus *bus; // 对应的MII总线 struct device dev; // 设备文件 u32 phy_id; // PHY ID struct phy_c45_device_ids c45_ids; bool is_c45;bool is_internal;bool has_fixups;bool suspended;enum phy_state state; // PHY状态 u32 dev_flags;phy_interface_t interface; // PHY接口 int addr; // PHY 总线地址(0~31) int speed; // 速度 int duplex; // 双工模式 int pause; // 停止 int asym_pause;int link; u32 interrupts; // 中断使能标志 u32 supported; u32 advertising; u32 lp_advertising;int autoneg;int link_timeout;int irq; // 中断号 void *priv; // 私有数据 struct work_struct phy_queue; // PHY工作队列 struct delayed_work state_queue; // PHY延时工作队列 atomic_t irq_disable;struct mutex lock;struct net_device *attached_dev; // 网络设备 void (*adjust_link)(struct net_device *dev);};

phy_driver

struct phy_driver {struct mdio_driver_common mdiodrv; u32 phy_id;char *name; u32 phy_id_mask; u32 features; u32 flags;const void *driver_data;int (*soft_reset)(struct phy_device *phydev);int (*config_init)(struct phy_device *phydev);int (*probe)(struct phy_device *phydev);int (*suspend)(struct phy_device *phydev);int (*resume)(struct phy_device *phydev);int (*config_aneg)(struct phy_device *phydev);int (*aneg_done)(struct phy_device *phydev);int (*read_status)(struct phy_device *phydev);int (*ack_interrupt)(struct phy_device *phydev);int (*config_intr)(struct phy_device *phydev);int (*did_interrupt)(struct phy_device *phydev);void (*remove)(struct phy_device *phydev);int (*match_phy_device)(struct phy_device *phydev);int (*ts_info)(struct phy_device *phydev, struct ethtool_ts_info *ti);int (*hwtstamp)(struct phy_device *phydev, struct ifreq *ifr);bool (*rxtstamp)(struct phy_device *dev, struct sk_buff *skb, int type);void (*txtstamp)(struct phy_device *dev, struct sk_buff *skb, int type);int (*set_wol)(struct phy_device *dev, struct ethtool_wolinfo *wol);void (*get_wol)(struct phy_device *dev, struct ethtool_wolinfo *wol);void (*link_change_notify)(struct phy_device *dev);int (*read_mmd)(struct phy_device *dev, int devnum, u16 regnum);int (*write_mmd)(struct phy_device *dev, int devnum, u16 regnum, u16 val);int (*read_page)(struct phy_device *dev);int (*write_page)(struct phy_device *dev, int page)int (*module_info)(struct phy_device *dev, struct ethtool_modinfo *modinfo);int (*module_eeprom)(struct phy_device *dev, struct ethtool_eeprom *ee, u8 *data);int (*get_sset_count)(struct phy_device *dev);void (*get_strings)(struct phy_device *dev, u8 *data);void (*get_stats)(struct phy_device *dev, struct ethtool_stats *stats, u64 *data);int (*get_tunable)(struct phy_device *dev, struct ethtool_tunable *tuna, void *data);int (*set_tunable)(struct phy_device *dev, struct ethtool_tunable *tuna,const void *data);int (*set_loopback)(struct phy_device *dev, bool enable); ANDROID_KABI_RESERVE(1); ANDROID_KABI_RESERVE(2);};

mii_bus

struct mii_bus {const char *name; // 总线名字char id[MII_BUS_ID_SIZE]; // ID MII_BUS_ID_SIZE=61void *priv; // 私有数据int (*read)(struct mii_bus *bus, int phy_id, int regnum); // 读方式int (*write)(struct mii_bus *bus, int phy_id, int regnum, u16 val); // 写方式int (*reset)(struct mii_bus *bus); // 复位struct mutex mdio_lock;struct device *parent; // 父设备enum { MDIOBUS_ALLOCATED = 1, MDIOBUS_REGISTERED, MDIOBUS_UNREGISTERED, MDIOBUS_RELEASED, } state; // 总线状态struct device dev; // 设备文件struct phy_device *phy_map[PHY_MAX_ADDR]; // PHY设备数组 u32 phy_mask;int *irq; // 中断};

net_device

struct net_device {char name[IFNAMSIZ]; /* 用于存放网络设备的设备名称 */char *ifalias; /* 网络设备的别名 */int ifindex; /* 网络设备的接口索引值,独一无二的网络设备标识符 */struct hlist_node name_hlist; /* 这个字段用于构建网络设备名的哈希散列表,而struct net中的name_hlist就指向每个哈希散列表的链表头 */struct hlist_node index_hlist; /* 用于构建网络设备的接口索引值哈希散列表,在struct net中的index_hlist用于指向接口索引值哈希散列表的链表头 */struct list_head dev_list; /* 用于将每一个网络设备加入到一个网络命名空间中的网络设备双链表中 */unsigned int flags; /* 网络设备接口的标识符 */unsigned int priv_flags; /* 网络设备接口的标识符,但对用户空间不可见;*/unsigned short type; /* 接口硬件类型 */unsigned int mtu; /* 网络设备接口的最大传输单元 */unsigned short hard_header_len; /* 硬件接口头长度 */unsigned char *dev_addr; /* 网络设备接口的MAC地址 */bool uc_promisc; /* 网络设备接口的单播模式 */unsigned int promiscuity; /* 网络设备接口的混杂模式 */unsigned int allmulti; /* 网络设备接口的全组播模式 */struct netdev_hw_addr_list uc; /* 辅助单播MAC地址列表 */struct netdev_hw_addr_list mc; /* 主mac地址列表 */struct netdev_hw_addr_list dev_addrs; /* hw设备地址列表 */unsigned char broadcast[MAX_ADDR_LEN]; /* hw广播地址 */struct netdev_rx_queue *_rx; /* 网络设备接口的数据包接收队列 */struct netdev_queue *_tx /* 网络设备接口的数据包发送队列 */unsigned int num_tx_queues; /* TX队列数 */unsigned int real_num_tx_queues; /* 当前设备活动的TX队列数 */unsigned long tx_queue_len; /* 每个队列允许的最大帧 */unsigned long state; /* 网络设备接口的状态 */struct net_device_stats stats; /* 网络设备接口的统计情况 */possible_net_t nd_net; /* 用于执行网络设备所在的命名空间 */};

phy 设备的注册

以网卡 Fec 为例,网卡驱动在初始化 fec_probe() 时遍历 dts 的定义,创建相应 struct phy_device 类型的设备,主要步骤为:

注册网络设备 net_device申请队列和 DMA申请 MDIO 总线创建并注册 Phy 设备

fec_probe(struct platform_device *pdev) -> struct device_node *np = pdev->dev.of_node, *phy_node; // 获取设备树节点句柄,并创建一个phy的设备树节点句柄 -> fec_enet_get_queue_num(pdev, &num_tx_qs, &num_rx_qs); // 从设备树获取fsl,num-tx-queues和fsl,num-rx-queues的属性值 -> ndev = alloc_etherdev_mqs // 申请net_device -> netdev_priv(ndev) // 获取私有数据空间首地址-------------------------------------------------------------------------------------------------------------------------- -> of_parse_phandle(np, "phy-handle", 0) // 从mac的设备树节点中获取phy子节点 -> of_get_phy_mode(pdev->dev.of_node) // 从设备树节点中获取phy模式,phy-mode = "rmii"; -> fec_reset_phy(pdev); // 复位phy -> fec_enet_init(ndev) // 申请队列和DMA,设置MAC地址 -> of_property_read_u32(np, "fsl,wakeup_irq", &irq) // 唤醒中断 -> fec_enet_mii_init(pdev); // 注册MDIO总线、注册phy_device -> fep->mii_bus = mdiobus_alloc() //申请MDIO总线 -> fep->mii_bus->name = "fec_enet_mii_bus"; // 总线名字 -> fep->mii_bus->read = fec_enet_mdio_read; // 总线的读函数 -> fep->mii_bus->write = fec_enet_mdio_write; // 总线的写函数 -> snprintf(fep->mii_bus->id, MII_BUS_ID_SIZE, "%s-%x",pdev->name, fep->dev_id + 1); // 总线id -> of_get_child_by_name(pdev->dev.of_node, "mdio"); // 获取phy节点句柄 -> of_mdiobus_register // 注册mii_bus设备,并通过设备树子节点创建PHY设备 drivers/of/of_mdio.c of_mdiobus_register(struct mii_bus *mdio, struct device_node *np) -> mdio->phy_mask = ~0; // 屏蔽所有PHY,防止自动探测。相反,设备树中列出的phy将在总线注册后填充 -> mdio->dev.of_node = np; -> mdio->reset_delay_us = DEFAULT_GPIO_RESET_DELAY; -> mdiobus_register(mdio) // 注册MDIO总线设备 -> bus->dev.parent = bus->parent; -> bus->dev.class = &mdio_bus_class; // 总线设备类“/sys/bus/mdio_bus”/*----------------------------------------- static struct class mdio_bus_class = { .name = "mdio_bus", .dev_release = mdiobus_release, }; -------------------------------------------*/ -> bus->dev.groups = ; -> dev_set_name(&bus->dev, "%s", bus->id); //设置总线设备的名称 -> device_register(&bus->dev); // 注册总线设备 -> if (bus->reset) bus->reset(bus); // 总线复位---------------------------------------另一条分支解析(可忽略)-------------------------------------------------------- -> phydev = mdiobus_scan(bus, i); // 扫描phy设备 -> phydev = get_phy_device(bus, addr); //获取创建phy设备 ->err = phy_device_register(phydev); //注册phy设备-------------------------------------------------------------------------------------------------------------------- -> for_each_available_child_of_node(np, child) { // 遍历这个平台设备的子节点并为每个phy注册一个phy_device -> addr = of_mdio_parse_addr(&mdio->dev, child) // 从子节点的"reg"属性中获得PHY设备的地址 -> of_property_read_u32(np, "reg", &addr) -> if (addr < 0) scanphys = true; continue; // 如果未获得子节点的"reg"属性,则在后面再启用扫描可能存在的PHY的,然后注册 -> of_mdiobus_register_phy(mdio, child, addr) } // 创建并注册PHY设备 -> is_c45 = of_device_is_compatible(child,"ethernet-phy-ieee802.3-c45") //判断设备树中的PHY的属性是否指定45号条款 -> if (!is_c45 && !of_get_phy_id(child, &phy_id)) //如果设备树中的PHY的属性未指定45号条款 且未通过"ethernet-phy-id%4x.%4x"属性指定PHY的ID -> phy = phy_device_create(mdio, addr, phy_id, 0, ); -> else phy = get_phy_device(mdio, addr, is_c45); //用这个分支 -> get_phy_id(bus, addr, &phy_id, is_c45, &c45_ids);//通过mdio得到PHY的ID -> mdiobus_read(bus, addr, MII_PHYSID1) -> __mdiobus_read(bus, addr, regnum); -> bus->read(bus, addr, regnum) -> mdiobus_read(bus, addr, MII_PHYSID2) -> phy_device_create(bus, addr, phy_id, is_c45, &c45_ids) // 创建PHY设备 -> struct phy_device *dev; -> dev = kzalloc(sizeof(*dev), GFP_KERNEL); dev->dev.release = phy_device_release; dev->speed = 0; dev->duplex = -1; dev->pause = 0; dev->asym_pause = 0; dev->link = 1; dev->interface = PHY_INTERFACE_MODE_GMII; dev->autoneg = AUTONEG_ENABLE; // 默认支持自协商(自动使能) dev->is_c45 = is_c45; dev->addr = addr; dev->phy_id = phy_id;if (c45_ids) dev->c45_ids = *c45_ids; dev->bus = bus; dev->dev.parent = bus->parent; dev->dev.bus = &mdio_bus_type; //PHY设备和驱动都会挂在mdio_bus下,匹配时会调用对应的match函数 -- /*---------------------------------------------------------------------------------------------------- struct bus_type mdio_bus_type = { .name = "mdio_bus", //总线名称 .match = mdio_bus_match, //用来匹配总线上设备和驱动的函数 .pm = MDIO_BUS_PM_OPS, .dev_groups = mdio_dev_groups, }; ----------------------------------------------------------------------------------------------------*/ dev->irq = bus->irq != ? bus->irq[addr] : PHY_POLL; dev_set_name(&dev->dev, PHY_ID_FMT, bus->id, addr); dev->state = PHY_DOWN; //指示PHY设备和驱动程序尚未准备就绪,在PHY驱动的probe函数中会更改为READY -> INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine); //PHY的状态机(核心WORK)后续解析 -> INIT_WORK(&dev->phy_queue, phy_change); // 由phy_interrupt / timer调度以处理PHY状态的更改 -> request_module(MDIO_MODULE_PREFIX MDIO_ID_FMT, MDIO_ID_ARGS(phy_id)); // 加载内核模块 -> device_initialize(&dev->dev); //设备模型中的一些设备,主要是kset、kobject、ktype的设置 -> irq_of_parse_and_map(child, 0); //将中断解析并映射到linux virq空间( -> of_node_get(child); //将OF节点与设备结构相关联 -> phy->dev.of_node = child; -> phy_device_register(phy) // 注册phy设备 -> if (phydev->bus->phy_map[phydev->addr]) //判断PHY是否已经注册了 -> phydev->bus->phy_map[phydev->addr] = phydev; //添加PHY到总线的phy_map里 -> phy_scan_fixups(phydev); //执行匹配的fixups -> device_add(&phydev->dev); // 注册到linux设备模型框架中 -> if (!scanphys) return 0; // 如果从子节点的"reg"属性中获得PHY设备的地址,scanphys=false,这里就直接返回了,因为不需要再扫描了------------一般来说只要设备树种指定了PHY设备的"reg"属性,后面的流程可以自动忽略 ------------ -> register_netdev(ndev) // 向内核注册net_device

phy 驱动的注册

genphy_driver 通用驱动

内核中有 genphy_driver 通用驱动,以及专有驱动(这里以 NXP TJA 驱动为例),分别如下:

static struct phy_driver genphy_driver = { .phy_id = 0xffffffff, .phy_id_mask = 0xffffffff, .name = "Generic PHY", .get_features = genphy_read_abilities, .suspend = genphy_suspend, .resume = genphy_resume, .set_loopback = genphy_loopback,};

static struct phy_driver tja11xx_driver[] = { { PHY_ID_MATCH_MODEL(PHY_ID_TJA1100), .name = "NXP TJA1100", .features = PHY_BASIC_T1_FEATURES, .probe = tja11xx_probe, .soft_reset = tja11xx_soft_reset, .config_aneg = tja11xx_config_aneg, .config_init = tja11xx_config_init, .read_status = tja11xx_read_status, .get_sqi = tja11xx_get_sqi, .get_sqi_max = tja11xx_get_sqi_max, .suspend = genphy_suspend, .resume = genphy_resume, .set_loopback = genphy_loopback,/* Statistics */ .get_sset_count = tja11xx_get_sset_count, .get_strings = tja11xx_get_strings, .get_stats = tja11xx_get_stats, }};module_phy_driver(tja11xx_driver);

genphy_driver 的 struct phy_driver 的注册过程如下:

phy_init phy_driver_register() driver_register(&new_driver->mdiodrv.driver) bus_add_driver(drv) driver_attach(drv) bus_for_each_dev(drv->bus, , drv, __driver_attach)while ((dev = next_device(&i)) && !error) /* 循环到注册的 PHY 设备时 */ fn(dev, data) = __driver_attach() /* 匹配设备和驱动 */ driver_match_device(drv, dev) mdio_bus_match(dev, drv) phy_bus_match(dev, drv) /* 按 phy_id & phy_id_mask 匹配 */return (phydrv->phy_id & phydrv->phy_id_mask) == (phydev->phy_id & phydrv->phy_id_mask); /* 匹配到设备和驱动,加载驱动 */ driver_probe_device(drv, dev) really_probe(dev, drv) dev->driver = drv; /* 绑定设备的驱动 */ drv->probe(dev) = phy_probe

其中一个关键点是 mdio driver 的 probe 函数是一个通用函数 phy_probe:

static int phy_probe(struct device *dev){struct phy_device *phydev = to_phy_device(dev); // 获取PHY设备struct device_driver *drv = phydev->mdio.dev.driver;struct phy_driver *phydrv = to_phy_driver(drv); // 获取PHY驱动int err = 0; phydev->drv = phydrv; /* 绑定 phy_device 和 phy_driver *//* PHY 中断模式最终配置 */if (!phy_drv_supports_irq(phydrv) && phy_interrupt_is_valid(phydev)) // 设置中断方式 phydev->irq = PHY_POLL;if (phydrv->flags & PHY_IS_INTERNAL) phydev->is_internal = true;/* Deassert the reset signal */ phy_device_reset(phydev, 0);if (phydev->drv->probe) { // 判断驱动有probe方式 err = phydev->drv->probe(phydev); /* PHY 驱动的 probe */if (err)goto out; } ......if (phydrv->features) { linkmode_copy(phydev->supported, phydrv->features); }else if (phydrv->get_features) err = phydrv->get_features(phydev);else if (phydev->is_c45) err = genphy_c45_pma_read_abilities(phydev);else err = genphy_read_abilities(phydev); //读取状态寄存器来确定 phy 芯片的能力if (err)goto out;if (!linkmode_test_bit(ETHTOOL_LINK_MODE_Autoneg_BIT, phydev->supported)) phydev->autoneg = 0;if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Half_BIT, phydev->supported)) phydev->is_gigabit_capable = 1;if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT, phydev->supported)) phydev->is_gigabit_capable = 1;/* PHY 功能特性配置 */ of_set_phy_supported(phydev); phy_advertise_supported(phydev); ...... of_set_phy_eee_broken(phydev); ......if (!test_bit(ETHTOOL_LINK_MODE_Pause_BIT, phydev->supported) && !test_bit(ETHTOOL_LINK_MODE_Asym_Pause_BIT, phydev->supported)) { linkmode_set_bit(ETHTOOL_LINK_MODE_Pause_BIT, phydev->supported); linkmode_set_bit(ETHTOOL_LINK_MODE_Asym_Pause_BIT, phydev->supported); }/* Set the state to READY by default */ phydev->state = PHY_READY; /* 标记 PHY 设备已经就绪 */out:/* Re-assert the reset signal on error */if (err) phy_device_reset(phydev, 1);return err;}

其中通用 phy 驱动会调用函数 genphy_read_abilities 来读取状态寄存器来确定 phy 芯片的能力:

genphy_read_abilities() `-| { | val = phy_read(phydev, MII_BMSR); // 读取 mdio 0x01 寄存器来确定 phy 的 10/100M 能力 | 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); | if (val & BMSR_ESTATEN) { | val = phy_read(phydev, MII_ESTATUS); // 读取 mdio 0x0f 寄存器来确定 phy 的 1000M 能力 | 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); | } | }

NXP TJA 专有驱动

NXP TJA 驱动的 struct phy_driver 的注册过程如下:

#define phy_module_driver(__phy_drivers, __count) \static int __init phy_module_init(void) \{ \ return phy_drivers_register(__phy_drivers, __count, THIS_MODULE); \} \module_init(phy_module_init); \static void __exit phy_module_exit(void) \{ \ phy_drivers_unregister(__phy_drivers, __count); \} \module_exit(phy_module_exit)#define module_phy_driver(__phy_drivers) \ phy_module_driver(__phy_drivers, ARRAY_SIZE(__phy_drivers))

int phy_drivers_register(struct phy_driver *new_driver, int n, struct module *owner){int i, ret = 0;for (i = 0; i < n; i++) { ret = phy_driver_register(new_driver + i, owner); // 注册数组中所有的phy驱动if (ret) {while (i-- > 0) phy_driver_unregister(new_driver + i);break; } }return ret;}EXPORT_SYMBOL(phy_drivers_register);

根据上面的分析,由于存在 phydev->drv->probe,所以会调用其注册的函数 tja11xx_probe。

网卡 fec 和 Phy 的协作

在 linux 内核中,以太网 mac 会被注册成 struct net_device,phy 芯片会被注册成 struct phy_device。 phy_device 的状态怎么传递给 net_device,让其在 link 状态变化时做出对应的配置改变,这个任务就落在上述的 struct phylink 中介身上。

下面就以 fec 网口驱动为例,展示一下网卡 fec 和 phy 的协作过程。整个 phy 驱动的主要调用流程如下图所示:

一个 phy 驱动的原理其实是非常简单的,一般流程如下:

用轮询/中断的方式通过 mdio 总线读取 phy 芯片的状态。在 phy link 状态变化的情况下,正确配置 mac 的状态。(例如:根据 phy 自协商的速率 10/100/1000M 把 mac 配置成对应速率)

phy 芯片状态在 phy 设备注册的时候已经体现,这里详细讲下如何在 phy link 状态变化的情况下,正确配置 mac 的状态。

void phy_state_machine(struct work_struct *work){ old_state = phydev->state;/* (1) 状态机主体 */switch (phydev->state) {/* (1.1) 在 PHY_DOWN/PHY_READY 状态下不动作 */case PHY_DOWN:case PHY_READY:break;/* (1.2) 在 PHY_UP 状态下,表明网口被 up 起来,需要启动自协商并且查询自协商后的 link 状态 如果自协商结果是 link up,进入 PHY_RUNNING 状态 如果自协商结果是 link down,进入 PHY_NOLINK 状态 */case PHY_UP: needs_aneg = true;break;/* (1.3) 在运行的过程中定时轮询 link 状态 如果 link up,进入 PHY_RUNNING 状态 如果 link down,进入 PHY_NOLINK 状态 */case PHY_NOLINK:case PHY_RUNNING: err = phy_check_link_status(phydev);break; }/* (2) 如果需要,启动自协商配置 */if (needs_aneg) err = phy_start_aneg(phydev);/* (3) 如果 phy link 状态有变化,通知给对应网口 netdev */if (old_state != phydev->state) { phydev_dbg(phydev, "PHY state change %s -> %s\n", phy_state_to_str(old_state), phy_state_to_str(phydev->state));if (phydev->drv && phydev->drv->link_change_notify) phydev->drv->link_change_notify(phydev); }/* (4) 重新启动 work,周期为 1s */if (phy_polling_mode(phydev) && phy_is_started(phydev)) phy_queue_state_machine(phydev, PHY_STATE_TIME);}

自协商配置

具体启动 phy 自协商的代码流程如下:

phy_state_machine()`-| phy_start_aneg() `-| phy_config_aneg() `-| genphy_config_aneg() `-| __genphy_config_aneg() `-| genphy_setup_master_slave() // (1) 如果是千兆网口,配置其 master/slave `-| { | phy_modify_changed(phydev, MII_CTRL1000, // 配置 mdio 0x09 寄存器 | (CTL1000_ENABLE_MASTER | CTL1000_AS_MASTER | CTL1000_PREFER_MASTER), ctl); | } | genphy_config_advert() // (2) 设置本端的 advert 能力 `-| { | linkmode_and(phydev->advertising, phydev->advertising, phydev->supported); | adv = linkmode_adv_to_mii_adv_t(phydev->advertising); | phy_modify_changed(phydev, MII_ADVERTISE, // 10M/100M 能力配置到 mdio 0x04 寄存器 | ADVERTISE_ALL | ADVERTISE_100BASE4 | | ADVERTISE_PAUSE_CAP | ADVERTISE_PAUSE_ASYM, adv); | if (!(bmsr & BMSR_ESTATEN)) return changed; | adv = linkmode_adv_to_mii_ctrl1000_t(phydev->advertising); | phy_modify_changed(phydev, MII_CTRL1000, // 1000M 能力配置到 mdio 0x09 寄存器 | ADVERTISE_1000FULL | ADVERTISE_1000HALF, adv); | } | genphy_check_and_restart_aneg() `-| genphy_restart_aneg() // (3) 启动 phy 自协商 `-| { | phy_modify(phydev, MII_BMCR, BMCR_ISOLATE, // 配置 mdio 0x00 寄存器 | BMCR_ANENABLE | BMCR_ANRESTART); | }

link 状态读取

phy link 状态读取的代码流程如下:

phy_state_machine()`-| phy_check_link_status() `-| phy_read_status() // (1) 读取 link 状态 `-| genphy_read_status() `-| { | genphy_update_link(phydev); // (1.1) 更新 link 状态 | if (phydev->autoneg == AUTONEG_ENABLE && old_link && phydev->link) return 0; | genphy_read_master_slave(phydev); // (1.2) 如果是千兆网口,更新本端和对端的 master/slave | genphy_read_lpa(phydev); // (1.3) 更新对端(link partner) 声明的能力 | if (phydev->autoneg == AUTONEG_ENABLE && phydev->autoneg_complete) { | phy_resolve_aneg_linkmode(phydev); // (1.4.1) 自协商模式,解析 link 结果 | } else if (phydev->autoneg == AUTONEG_DISABLE) { | genphy_read_status_fixed(phydev); // (1.4.2) 固定模式,解析 link 结果 | } | } | if (phydev->link && phydev->state != PHY_RUNNING) { // (2) link 状态 change 事件:变成 link up | phydev->state = PHY_RUNNING; | phy_link_up(phydev); // link up 事件,通知给 phylink | } else if (!phydev->link && phydev->state != PHY_NOLINK) { // (3) link 状态 change 事件:变成 link down | phydev->state = PHY_NOLINK; | phy_link_down(phydev); // link down 事件,通知给 phylink | }

link 状态通知

phy 的 link 状态变化怎么通知给 netdev,并且让 mac 做出相应的配置改变,这个是通过一个中介 phylink 来实现的。

phy_link_up()/phy_link_down()`-| phydev->phy_link_change(phydev, true/false); `-| phylink_phy_change() `-| { | pl->phy_state.speed = phydev->speed; // (1) 把 `phy_device` 状态更新给 `phylink` | pl->phy_state.duplex = phydev->duplex; | pl->phy_state.interface = phydev->interface; | pl->phy_state.link = up; | phylink_run_resolve(pl); // (2) 通知 `phylink` 的轮询任务启动 | }

Linux 的启动流程

本篇的重点是讲解设备和驱动的启动流程,设备和驱动的流程是整个内核启动的核心,也是工作中最常面对的问题。出于知识点的系统性考虑,在进入主题之前我们先看下整个 Linux 在 ARM 中的启动流程如何。

Uboot 的启动流程

ARM Linux 的启动流程大致为:Uboot → Kernel → Root filesystem。Uboot 在上电的时候就拿到 CPU 的控制权,实现了硬件的初始化。具体是怎么实现的呢?一起来看一下,CPU 的内部集成了小容量的 Sram,而 PC 指针一上电就指向 Sram 的起始地址 0x00000000,所以一上电 Uboot 代码就得到了运行。

Uboot 拿到 CPU 使用权就开始做初始化工作,比如关闭看门狗、设置 CPU 运行模式、设置堆栈、初始化内存、网卡、nand flash 等,最后把 Linux 内核加载到内存中。

初始化 RAM

因为内核要在 RAM 中运行,所以在调用内核之前必须初始化和设置 RAM,为调用内核做好准备。

初始化串口

内核在启动过程中可以将信息通过串口输出,这样就可以清楚的知道内核启动信息。虽然串口不是 Uboot 必须要完成的工作,但是通过串口可以方便调试 Uboot 和内核的各种信息。

检测处理器类型

Uboot 在调用内核前需要检测系统的处理器类型,并将其保存在某个变量中提供给内核,内核在启动过程中会根据该处理器的类型调用相应的初始化程序。

设置内核启动参数

内核在启动过程中会根据该启动参数进行相应的初始化工作。

调用内核镜像

值得注意的是存储 Uboot 的存储器不同,Uboot 的执行过程也并不相同,一般来讲 Flash 分为 nor Flash 和 nand Flash 两种:nor Flash 支持芯片内执行(XIP,eXecute In Place),这样代码可以在 Flash 上直接执行而不必复制到 RAM 中去执行。

但是 nand Flash 并不支持 XIP,所以要想执行 nand Flash 上的代码,必须先将其复制到 RAM 中去,然后跳到 RAM 中去执行。如果内核存放在 nor Flash 中,那么可直接跳转到内核中去执行。但通常由于在 nor Flash 中执行代码会有种种限制,而且速度也远不及 RAM 快,所以一般的嵌入式系统都是将内核复制到 RAM 中,然后跳转到 RAM 中去执行。不论哪种情况,在跳到内核执行之前 CPU 的寄存器必须满足以下条件:r0 = 0,r1 = 处理器类型,r2 = 标记列表在 RAM 中的地址。

Linux 内核的启动流程(设备和驱动的加载)

关于 Uboot 的启动本课程不做详细介绍,因为本课程的主要内容是内核。在讲述内核启动之前让我们先了解下内核的组成结构:

其中,

(1)vmlinusx 是 ELF 格式的 Object 文件,这种文件只是各个源代码经过连接以后得到的文件,并不能在 ARM 平台上运行。

(2)经过 objcopy 这个工具转换以后,得到了二进制格式文件 Image,Image 文件相比于 vmlinusx 文件,除了格式不同以外,还被去除了许多注释和调试的信息。

(3)Image 文件经过压缩以后得到了 piggy.gz,这个文件仅仅是 Image 的压缩版,并无其他不同。

(4)接着编译生成另外几个模块文件 misc.o、big_endian.o、head.o、head-xscale.o,这几个文件组成一个叫 Bootstrap Loader 的组件,又叫引导程序,编译生成 piggy.o 文件。

(5)最后 piggy.o 文件和 Bootstrap Loader 组成一个 Bootable Kernel Image 文件(可启动文件)。

经过上面的分析不难知道 piggy.o 就是内核镜像,而剩下的几个文件就组成了引导程序。知道了内核的组成结构,Uboot 就是按照内核的组成结构一层一层剥开然后引导内核的:

可以说 start_kernel之前的所有工作都是为了将环境准备好,满足start_kernel的要求,然后由start_kernel开始进行内核的加载:

关于 start_kernl函数的内容太多,可以通过红色回调函数看出,start_kernel函数基本是在回调很多对应的注册函数。为了本系列课程的结构性这里就不展开所有知识点讲解,本篇内容接着前一篇设备树的内容重点讲解下设备和驱动的匹配过程。

还记得上一篇讲到的设备树三大作用吗?

平台标识;

运行时配置;

设备信息集合。

接下来我们就看看内核在启动的时候是如何寻找设备,驱动又如何和设备绑定的。

首先在平台目录下可以看到有很多平台描述的文件,如图:

有那么多的平台,我们到底要执行哪个平台是首先要考虑的事情。这也是设备三大功能的第一个功能——平台标识。

设备树里有对设备根节点的 Compatible 描述,平台文件里有对 __initconst的描述,如果两个字段一致则找到了对应的板级文件,这样就通过设备树把要用的设备平台与其他平台区分开来了,如图:

找到平台后就可以根据回调函数的指针调用该平台的注册函数。这里以飞思卡尔 imx.6dl 平台为例,回调的时候会调用 imx6q_init_machine函数,如下:

这里补充一个知识点,细心的读者也许发现了在 Compatible 字段里用逗号分隔了两个字符串。板级匹配的时候用的是哪个字符串,另外一个字符串又是做什么用?首先后面的字段 "fsl,imx6dl" 是抽象共用平台描述符,前面的字段 "fsl,imx6dl-sabresd" 是通用平台下的具体平台描述符,可以理解为母板和子板的区别。在具体的子板文件中我们可以通过前面的字段进行设备信息的获取,如图:

接着是运行时配置,让内核在启动的时候根据参数设置进行不同的处理。有经验的读者清楚在 Uboot 里也有对 Bootargs 的配置,这里为什么多此一举呢,是为了在 Uboot 中更灵活的对内核启动进行配置。

最后的作用就是设备信息集合,这是设备和驱动匹配的核心,也是工作中面对最多的情况。出于这一作用的内容是工作中经常遇到的重点也是难点,我们专门用一篇内容来详细讲解各级设备是如何展开的,并且手把手教你如何定制一套自己的开发板全新案例。

相关问答

linux 怎么看amd显卡 驱动 ?

在Linux系统中查看AMD显卡驱动的信息,可以通过以下几种方式:1.使用命令行工具:打开终端,输入以下命令来查看已安装的AMD显卡驱动信息:```lspc...在Linux...

如何自学 linux驱动 开发,做驱动开发需要哪些方面的知识?

做嵌入式应用的话一般的编程就可以了。那么嵌入式驱动开发与内核开发的话就需要学习多个方面的知识。我就把这方面的要求给你交流一下:(一家之言啊,自己多年从...

linux驱动 程序如何调用?

Linux驱动程序可以通过特定的接口被调用,在Linux内核中,所有的设备驱动都是一个个独立的模块。这些模块可以被加载或卸载,也可以被使用者调用。要调用Linux驱...

linux 系统无法安装打印机 驱动 ?

别找了没有的。我也找了N年了。linux下的驱动基本没有。试试三星的产品吧,大多数都有LINUX驱动。别找了没有的。我也找了N年了。linux下的驱动基本没有。试...

linux 的设备 驱动 程序的接口是怎样的?设备有哪些类型?

在Linux中,设备驱动程序的接口是通过设备文件和系统调用实现的。设备文件:每个设备在Linux中都表示为一个特殊的文件,称为设备文件。设备文件位于`/dev`...

linux驱动 程序例子?

以下是一个简单的Linux驱动程序的示例:```c#include以下是一个简单的Linux驱动程序的示例:```c#include

富士施乐 linux驱动 ?

1、网上下载富士施乐打印机驱动,然后先解压,解压之后我们找到并运行“Dpinst.exe”进行安装。2、到这里开始安装驱动,点击下一步。3、驱动会自动检测打印机...

Linux驱动 工程师需要掌握哪些驱动子系统?

作为一名Linux驱动工程师,你需要掌握以下几个重要的驱动子系统:1.内核基础知识:了解Linux内核的基本概念、架构和组织结构,包括进程管理、内存管理、文件系...

Linux 驱动 开发 有意义吗?

Linux驱动开发非常有意义。首先,Linux是一种开源操作系统,其内核源代码可以公开查看和修改,因此开发Linux驱动可以加深人们对操作系统的理解和掌握。其次...

Linux 操作系统下的USB硬件设备 驱动 是怎样的? - 185****6447 ...

USB是英文"UniversalSerialBus"的缩写,意为"通用串行总线"。是由Compaq(康柏)、DEC、IBM、Intel、NEC、微软以及Nort...

 宾波  草榴区 
王经理: 180-0000-0000(微信同号)
10086@qq.com
北京海淀区西三旗街道国际大厦08A座
©2025  上海羊羽卓进出口贸易有限公司  版权所有.All Rights Reserved.  |  程序由Z-BlogPHP强力驱动
网站首页
电话咨询
微信号

QQ

在线咨询真诚为您提供专业解答服务

热线

188-0000-0000
专属服务热线

微信

二维码扫一扫微信交流
顶部