物理层定义数据发送和接收所需要电与光信号,线路状态,时钟基准,编码和电路等,并向数据链路设备提供标准接口,物理层芯片称为PHY。
数据链路层则提供寻址机构,数据帧的构建,数据差错检查,传送控制向网络层提供标准的数据接口功能。
IEEE802.3描述物理层和数据链路层的MAC子层的实现方法。
以太网接口的实质是MAC通过MII总线控制PHY的过程。
MII(媒体独立接口),媒体独立表明在不对mac硬件重新设计或替换的情况下,任何类型的PHY设备都可以正常工作。
MAC通过SMI总线不断读取PHY的状态寄存器以得知目前PHY的状态,比如连接速度,双工的能力等。
PHY是物理接口收发器,它实现物理层,包括MII/GMII子层,PCS(物理编码子层),PMA(物理介质附加子层),PMD(物理介质相关子层),MDI子层。比较重要的是实现CSMA/CD部分功能。
PCI总线接MAC总线,MAC接PHY,PHY接网线。
PHY整合了大量的模拟硬件,MAC是典型的全数字器件。
每次内核加载,他知道从/etc/fstab开始mount文件系统,每次系统启动会根据该文件自动挂载。
在linux网络分为两层,分别是网络堆栈协议层以及接收和发送网络协议的设备驱动程序层。网络堆栈式硬件独立出来部分,主要用来TCP/IP等多种协议,而网络设备驱动层时连接网络堆栈协议层和网络硬件的中间层。
net_device结构体存储一个网络接口重要信息,它是系统中网络设备代表。
sk_buff是socket buffer在网络传输过程中起重要作用,内核封装成socket buffer向网络硬件发送,当网络硬件接收到数据包时,再把数据包封装成socket buffer向内核发送。
write()->sk_buff缓冲区->hard_start_xmit()->DMA->硬件接口
硬件一般都会在上层数据发送之前加上自己的硬件帧头,比如以太网就有14字节的帧头。
所有套接字的输入输出缓冲区是sk_buff形成的链表。
大多数实际的物理连接的网络技术提供载波状态信息,载波存在意味着硬件功能是正常的。
在init函数中这次有两个platform_driver_register();一个是有关stmphy,一个是有关stmmac,再 stmphy_dvr_probe()仅仅是打印信息。
在stmmac_dvr_probe函数中,一开始获取linux中的硬件资源,分配一个网络设备的内存空间alloc_etherdev(本质还是调用alloc_netdev_mqs()).,接下来比较重要的是stmmac_mac_device_setup()函数。
struct mac_device_info { const struct stmmac_ops *mac; const struct stmmac_desc_ops *desc; const struct stmmac_dma_ops *dma; struct mii_regs mii; /* MII register Addresses */ struct mac_link link; };
里面将mac_device_info中的三个operations注册了回调函数,以便让接下来的函数stmmac_probe()进行调用。在该函数中比较重要的是ether_setup():
/** * ether_setup - setup Ethernet network device * @dev: network device * Fill in the fields of the device structure with Ethernet-generic values. */ void ether_setup(struct net_device *dev) { dev->header_ops = ð_header_ops; dev->type = ARPHRD_ETHER; dev->hard_header_len = ETH_HLEN; dev->mtu = ETH_DATA_LEN; dev->addr_len = ETH_ALEN; dev->tx_queue_len = 1000; /* Ethernet wants good queues */ dev->flags = IFF_BROADCAST|IFF_MULTICAST; dev->priv_flags = IFF_TX_SKB_SHARING; memset(dev->broadcast, 0xFF, ETH_ALEN); } EXPORT_SYMBOL(ether_setup);
默认赋值很多以太网设备的参数。
接下来就是注册最最重要的net_device_ops中的一系列回调函数
static const struct net_device_ops stmmac_netdev_ops = { .ndo_open = stmmac_open, .ndo_start_xmit = stmmac_xmit, .ndo_stop = stmmac_release, .ndo_change_mtu = stmmac_change_mtu, .ndo_fix_features = stmmac_fix_features, .ndo_set_multicast_list = stmmac_multicast_list, .ndo_tx_timeout = stmmac_tx_timeout, .ndo_do_ioctl = stmmac_ioctl, .ndo_set_config = stmmac_config, #ifdef STMMAC_VLAN_TAG_USED .ndo_vlan_rx_register = stmmac_vlan_rx_register, #endif #ifdef CONFIG_NET_POLL_CONTROLLER .ndo_poll_controller = stmmac_poll_controller, #endif .ndo_set_mac_address = eth_mac_addr, };
在stmmac_open函数中进行物理层(phy)设备的初始化,主要是执行phy_connect()这个函数来链接以太网设备和物理层的设备。接下来主要是DMA Descriptor的初始化和进行DMA分配和映射。(dma_alloc_coherent,dma_map_single).也为每一个接收的dma分配一个skb(网络通信中数据存储的核心数据结构)netdev_alloc_skb_ip_align().
在open函数中需要申请中断request_irq(),来判断是否有数据要接受或者发送的数据已经完成。中断处理函数为stmmac_interrupt(),然后调用stmmac_dma_interrupt(),最终调用了一个很关键的函数napi_sechedule().(napi是指new api,在数据传输量不大时采用中断的方式,但是在数据传输很频繁时会采用poll轮询的方式)。
/** * napi_schedule - schedule NAPI poll * @n: napi context * * Schedule NAPI poll routine to be called if it is not already * running. */ static inline void napi_schedule(struct napi_struct *n) { if (napi_schedule_prep(n)) __napi_schedule(n); }napi添加的方法在之后会调用netif_napi_add()。传入的函数参数为stmmc_poll()里面调用了真正的数据接收的函数stmmac_rx().
phy_start()开启phy设备。实际上就是设置一下phy状态。
/** * phy_start - start or restart a PHY device * @phydev: target phy_device struct * * Description: Indicates the attached device's readiness to * handle PHY-related work. Used during startup to start the * PHY, and after a call to phy_stop() to resume operation. * Also used to indicate the MDIO bus has cleared an error * condition. */ void phy_start(struct phy_device *phydev) { mutex_lock(&phydev->lock); switch (phydev->state) { case PHY_STARTING: phydev->state = PHY_PENDING; break; case PHY_READY: phydev->state = PHY_UP; break; case PHY_HALTED: phydev->state = PHY_RESUMING; default: break; } mutex_unlock(&phydev->lock); } EXPORT_SYMBOL(phy_start);napi_enable().使能NAPI调度
netif_start_queue().允许数据传输
/** * netif_start_queue - allow transmit * @dev: network device * * Allow upper layers to call the device hard_start_xmit routine. */ static inline void netif_start_queue(struct net_device *dev) { netif_tx_start_queue(netdev_get_tx_queue(dev, 0)); }
stmmac_xmit();是驱动程序发送的入口点。里面有一个特别点是skb_shinfo();来查找所需要的分散数据片段(scatter/gather)最终调用dma_map_page来发送数据,注意参数里的一个结构体skb_frag_struct;
struct skb_frag_struct { struct page *page; #if (BITS_PER_LONG > 32) || (PAGE_SIZE >= 65536) __u32 page_offset; __u32 size; #else __u16 page_offset; __u16 size; #endif };
接下来设置ethtool方法,ethtool是为系统管理员提供的用语控制网络接口的工具,控制速度,介质类型,双工操作,DMA设置,硬件校验,LAN唤醒等操作。
SET_ETHTOOL_OPS(netdev, &stmmac_ethtool_ops);
net_device中的feature设置也是比较重要的它相当于是一些操作的开关,比如说只有在device结构的feature设置了NETIF_F_SG标志位,内核才能将分散的数据包传递给stmmac_xmit函数。
最后就是一个将net_device注册到网络子系统中去.register_netdevice().里面调用了register_netdevice().
为创建网络设备创建sysfs入口等操作。
之后注册MDIO总线。因为phy设备和mac控制器之间是需要总线进行通信的,介质无关接口(MII)是一个IEEE802.3标准,描述了以太网收发器是如何与网络控制器链接的。
在stmmac_mdio_register中先分配mii_bus内存空间,然后设置相对应的参数(名字,读写等回调函数)
<span style="white-space:pre"> </span> new_bus->name = "STMMAC MII Bus"; new_bus->read = &stmmac_mdio_read; new_bus->write = &stmmac_mdio_write; new_bus->reset = &stmmac_mdio_reset; snprintf(new_bus->id, MII_BUS_ID_SIZE, "%x", priv->plat->bus_id); new_bus->priv = ndev; new_bus->irq = irqlist; new_bus->phy_mask = priv->phy_mask; new_bus->parent = priv->device; err = mdiobus_register(new_bus); if (err != 0) { pr_err("%s: Cannot register as MDIO bus\n", new_bus->name); goto bus_register_fail; }
最后进行mdiobus_register()注册,先注册设备device_register(),,d然后调用mdiobus_scan(),扫描总线上地址获取phy_device(实际上是根据地址获得phy_id,然后用phy_id创建phy设备,最后注册phy设备).
struct phy_device *mdiobus_scan(struct mii_bus *bus, int addr) { struct phy_device *phydev; int err; phydev = get_phy_device(bus, addr); if (IS_ERR(phydev) || phydev == NULL) return phydev; err = phy_device_register(phydev); if (err) { phy_device_free(phydev); return NULL; } return phydev; } EXPORT_SYMBOL(mdiobus_scan);
mdio的读写最后还是通过phy_mii_ioctl(),而这个函数被封装在net_device_ops中的ndo_do_ioctl()的回调函数中。
网卡期望在内存中建有一个循环缓冲区,并与处理器共享;每个输入的数据包都放入缓冲器环中的下一个可用缓冲器中,然后引发中断。
用过向内核传递“mem=参数”的办法保留顶部的RAM,为缓冲区分配内存。
使用DMA的设备驱动程序将于连接到总线接口上的硬件通信,硬件使用的是物理地址,而程序代码使用的是虚拟地址(基于DMA硬件使用总线地址,而不是硬件地址)。
DMA操作最终会分配缓冲区,并将总线地址传递给设备。
如果设备支持常见的32位DMA操作,没有必要调用dma_set_mask();(该函数设置寻址范围)
一个DMA映射是要分配的DMA缓冲区与该缓冲区生成的,设备可访问地址的组合。
当驱动程序要试图在外围设备不可访问的地址上执行DMA时,将创建回弹缓冲区。//是内存中的独立区域,它可被设备访问。
如果设备改变了主内存中的区域,则任何覆盖该区域的处理器缓存都将无效,否则处理器将使用不正确的主内存映射,从而产生不正确的数据。
dma_addr_t来表示总线地址,该类型的变量对驱动程序是不透明的。
一致性映射的缓冲区必须可同时被CPU和外围设备访问,一致性映射必须保存在一致性缓存中。
dma_alloc_coherent(struct device *dev, size_t size, dam_addr_t *dma_handle, int flag);
前面两个参数是device结构和所需缓冲区大小,函数返回值是缓冲区的虚拟内核地址,可以被驱动程序使用,而与其相关的总线地址返回时保存在dma_handle中。
DMA池是一个生成小型,一致性DMA映射的机制。
流式DMA映射:dma_map_single();一旦缓冲区被映射,它将属于设备,而不是处理器。知道缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。
与其说是一个网络驱动,倒不如网络子系统,在linux系统起来时,就会调用/driver/core/dev.c中的subsys_init(net_dev_init);
子系统的事情以后再慢慢研究,驱动的问题就研究到这里了。