Linux DM9000网卡驱动程序完全分析

版权声明: 可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息。

说明1:本文分析基于内核源码版本为linux-2.6.31
说明2:本文在理解了linux中总线、设备和驱动模型的基础上加以分析代码

 

虽然Linux驱动程序应该是和具体的硬件平台分离的,但是为了更好的理解DM9000的驱动程序,这里还是结合一下Mini2440开发板,这样也可以更好的体会如何实现驱动和平台分离。

本文分成以下几个部分:
一、Mini2440开发板上DM9000的电气连接和Mach-mini2440.c文件的关系。
二、两个重要的结构体介绍:sk_buff和net_device
三、具体代码分析

 

 

一、Mini2440开发板上DM9000的电气连接和Mach-mini2440.c文件的关系
Mini2440开发板上DM9000与S3C2440的连接关系如下:  
 
其中片选信号AEN使用了nGCS4,所以网卡的内存区域在BANK4,也就是从地址0x20000000开始。DM9000的TXD[2:0]作为strap pin在电路图中是空接的,所以IO base是300H。中断使用了EINT7。这些内容在Mach文件中有如下体现:

另外在Mach文件中还定义了DM9000平台设备,设备名称为“dm9000”,设备资源就是上面定义的IO和中断资源。代码清单如下:

这个DM9000平台设备作为众多平台设备中的一个在扳子初始化的时候就被添加到了总线上。代码清单如下:

  

  

 

二、两个重要的结构体简单介绍:sk_buff和net_device

   *sk_buff

    如果把网络传输看成是运送货物的话,那么sk_buff就是这个“货物”了,所有经手这个货物的人都要干点什么事儿,要么加个包装,要么印个戳儿等等。收货的时候就要拆掉这些包装,得到我们需要的货物(payload data)。没有货物你还运输什么呢?由此可见sk_buff的重要性了。关于sk_buff的详细介绍和几个操作它的函数,参考本博客转载的一篇文章:“linux内核sk_buff的结构分析”,写得非常明白了。赞一个~

  *net_device

    又是一个庞大的结构体。好吧,我承认我从来就没有看全过这个结构体。它在内核中就是指代了一个网络设备。驱动程序需要在探测的时候分配并初始化这个结构体,然后使用register_netdev来注册它,这样就可以把操作硬件的函数与内核挂接在一起。

   

三、具体代码的分析
   在顺序分析之前先看三个结构体变量和一个自定义的结构体。

   * dm9000_driver变量。是platform_driver结构体变量,其中包含了重要的:驱动的名字(用来match)和几个重要操作函数。

  

  * dm9000_netdev_ops变量。是net_device_ops结构体变量, 其中定义了操作net_device的重要函数,我们在驱动程序中根据需要的操作要填充这些函数。代码清单如下:

 

   * dm9000_ethtool_ops变量。是ethtool_ops结构体变量,为了支持ethtool,其中的函数主要是用于查询和设置网卡参数(当然也有的驱动程序可能不支持ethtool)。代码清单如下:

 

   * board_info结构体。用来保存芯片相关的一些私有信息。具体在代码中分析。下面是这个结构体的清单。

 

 

下面看一下具体代码。

分析代码还是从init顺序开始。

     1. 注册平台驱动。

    主要完成的任务是:将驱动添加到总线上,完成驱动和设备的match,并执行驱动的probe函数。代码清单如下:

 

    2. probe函数。

   主要完成的任务是:探测设备获得并保存资源信息,根据这些信息申请内存和中断,最后调用register_netdev注册这个网络设备。以下是代码清单,可以分成几个部分来看:

   1) 首先定义了几个局部变量:

         struct dm9000_plat_data *pdata = pdev->dev.platform_data;
         struct board_info *db; /* Point a board information structure */
         struct net_device *ndev;

   2) 初始化一个网络设备。关键系统函数:alloc_etherdev()

   3) 获得资源信息并将其保存在board_info变量db中。关键系统函数:netdev_priv(),  platform_get_resource()

   4) 根据资源信息分配内存,申请中断等等, 并将申请后的资源信息也保存到db中,并且填充ndev中的参数。 关键系统函数:request_mem_region(),  ioremap()。 自定义函数:dm9000_set_io()

   5) 完成了第4步以后,回顾一下db和ndev中都有了什么:

       struct board_info *db:

                 addr_res -- 地址资源

                 data_res -- 数据资源

                 irq_res    -- 中断资源

                 addr_req -- 分配的地址内存资源

                 io_addr   -- 寄存器I/O基地址

                 data_req -- 分配的数据内存资源

                 io_data   -- 数据I/O基地址

                 dumpblk  -- IO模式

                 outblk     -- IO模式

                 inblk        -- IO模式

                 lock         -- 自旋锁(已经被初始化)

                 addr_lock -- 互斥锁(已经被初始化)

        struct net_device *ndev:

                 base_addr  -- 设备IO地址

                 irq              -- 设备IRQ号

     6) 设备复位。硬件操作函数dm9000_reset()

     7) 读一下生产商和制造商的ID,应该是0x9000 0A46。 关键函数:ior()

     8) 读一下芯片类型。

     ========以上步骤结束后我们可以认为已经找到了DM9000========

     9) 借助ether_setup()函数来部分初始化ndev。因为对以太网设备来讲,很多操作与属性是固定的,内核可以帮助完成。

    10) 手动初始化ndev的ops和db的mii部分。

    11) (如果有的话)从EEPROM中读取节点地址。这里可以看到mini2440这个板子上没有为DM9000外挂EEPROM,所以读取出来的全部是0xff。见函数dm9000_read_eeprom。 关于外挂EEPROM,可以参考datasheet上的7.EEPROM Format一节。

    12)  很显然ndev是我们在probe函数中定义的局部变量,如果我想在其他地方使用它怎么办呢? 这就需要把它保存起来。内核提供了这个方法,使用函数platform_set_drvdata()可以将ndev保存成平台总线设备的私有数据。以后再要使用它时只需调用platform_get_drvdata()就可以了。

    13) 使用register_netdev()注册ndev。

下面是代码清单:

 

    3. platform_driver的remove, suspend和resume的实现

        remove函数的功能是把设备从内核中移除,释放内存区域。该函数在卸载模块时被调用。代码清单如下:

 

        suspend函数并不真正把设备从内核中移除,而只是标志设备为removed状态,并设置挂起标志位,最后关闭设备。代码清单如下:

      resume函数将挂起的设备复位并初始化,软后将设备标志为attached状态,并设置挂起标志位。代码清单如下:

 

     4. 下面看一下用于填充net_device中netdev_ops和ethtool_ops的一些函数。

    代码在上面已经写出来了,为了看着方便在下面再写一遍,可以看出虽然mini2440的板子上没有为DM9000挂EEPROM,但这里还是定义了操作EEPROM的函数。就是说写驱动的时候是不考虑具体的板子的,你板子用不用是你的事,但是我们的驱动应该所有的功能都考虑进去。这也体现了驱动和平台分离的设计思想。

  

    *dm9000_open()

    进行的工作有 向内核注册中断,复位并初始化dm9000,检查MII接口,使能传输等。代码清单如下:

   

    *dm9000_stop()

     做的工作基本上和open相反。代码清单如下:

 

    *dm9000_start_xmit()

    重要的发送数据包函数。从上层发送sk_buff包。在看代码之前先来看一下DM9000是如何发送数据包的。

   

如上图所示,在DM9000内部SRAM中,地址0x0000~0x0BFF是TX Buffer, 地址0x0C00~0x3FFF是RX Buffer。在发送一个包之前,包中的有效数据必须先被存储到TX Buffer中并且使用输出端口命令来选择MWCMD寄存器。包的长度定义在TXPLL和TXPLH中。最后设置TXCR寄存器的bit[0] TXREQ来自动发送包。如果设置了IMR寄存器的PTM位,则DM9000会产生一个中断触发在ISR寄存器的bit[1]=PTS=1, 同时设置一个完成标志在NSR寄存器的bit[2]=TX1END或者 bit[3]=TX2END,表示包已经发送完了。发送一个包的具体步骤如下:

Step 1: 检查存储数据宽度。通过读取中断状态寄存器(ISR)的bit[7:6]来确定是8bit,16bit还是32bit。

Step 2: 写数据到TX SRAM中。

Step 3: 写传输长度到TXPLL和TXPLH寄存器中。

Step 4: 设置TXCR寄存器的bit[0]TXREQ来开始发送一个包。

 

代码清单如下,让我们看看在获得自旋锁这段期间都干了些什么:

 

    *dm9000_timeout()

    当watchdog超时时调用该函数。主要的功能是保存寄存器地址,停止队列,重启并初始化DM9000,唤醒队列,恢复寄存器地址。

    代码清单如下:

 

    *dm9000_hash_table()

    该函数用来设置DM9000的组播地址。代码清单如下:

 

    *dm9000_ioctl()

    从源码可以看出,dm9000的ioctl实际上是使用了mii的ioctl。代码清单如下:

 

    *dm9000_poll_controller()

    当内核配置Netconsole时该函数生效。代码清单如下:

 

    *dm9000_get_drvinfo()

    该函数去的设备的基本信息(设备名,版本,总线名)传给ethtool_drvinfo结构体变量。代码清单如下:

 

    *dm9000_get_settings()

    该函数得到由参数cmd指定的设置信息。

    *dm9000_set_settings()

    该函数设置由参数cmd指定的信息。

    *dm9000_get_msglevel()

    *dm9000_set_msglevel()

    这两个函数设置和取得message level,实际是设置和取得board_info中的msg_enable信息。

    *dm9000_nway_reset()

    重启mii的自动协商

    *dm9000_get_link()

    该函数的到link状态。如果带外部PHY,则返回mii链接状态。 否则返回DM9000 NSR寄存器数值。

    *dm9000_get_eeprom_len()
      dm9000_get_eeprom()
      dm9000_set_eeprom()

    这三个函数用来读写eeprom。

 

   5. 与数据传输有关的函数。

     上面已经分析了一个与数据传输有关的函数,那就是发送数据的函数dm9000_start_xmit()。这里再来分析数据的接收。再看具体代码之前还是来看看DM9000的数据接收的过程。

      接收的数据存储在RX SRAM中,地址是0C00h~3FFFh。存储在RX_SRAM中的每个包都有4个字节的信息头。可以使用MRCMDX和MRCMD寄存器来得到这些信息。第一个字节用来检查数据包是否接收到了RX_SRAM中,如果这个字节是"01",意味着一个包已经接收。如果是"00",则还没有数据包被接收到RX_SRAM中。第二个字节保存接收到的数据包的信息,格式和RSR寄存器一样。根据这个格式,接收到的包能被校验是正确的还是错误的包。第三和第四字节保存了接收的数据包的长度。这四个字节以外的其他字节就是接收包的数据。看下图可以更好的理解这种格式。

根据包的结构可以知道接收一个包应该按照下面的步骤来进行:

第一步:判断包是否已经接收过来了。需要用到MRCMDX寄存器。MRCMDX寄存器是存储数据读命令寄存器(地址不增加)。 这个寄存器只是用来读接收包标志位"01"。下面这段代码是一个例子,用来判断RX ready:

 

 第二步:检查包的状态和长度。需要用到MRCMD寄存器(存储数据读命令,读指针自动增加)。下面这段例子代码用来读RX状态和长度。

第三步:读包的数据。也需要MRCMD寄存器。例子代码如下:

 

 

下面的dm9000_rx()函数实际上是按照上面这三个步骤来实现的,具体实现并不一定是要参照例子代码。 注意这里按照DM9000接收包的格式定义了一个结构体dm9000_rxhdr用来表示头部的四个字节。代码清单如下:

接收函数代码如下:

 

   6. 中断处理相关函数

   DM9000的驱动程序采用了中断方式而非轮询方式。触发中断的时机发生在:1)DM9000接收到一个包以后。2)DM9000发送完了一个包以后。

   中断处理函数在open的时候被注册进内核。代码清单如下:

   *dm9000_tx_done()

   注:dm9000可以发送两个数据包,当发送一个数据包产生中断后,要确认一下队列中有没有第2个包需要发送。

    (1)读取dm9000寄存器NSR(Network Status Register)获取发送的状态,存在变量tx_status中;

    (2)如果发送状态为NSR_TX2END(第2个包发送完毕)或者NSR_TX1END(第1个包发送完毕),则将待发送的数据包数量(db->tx_pkt_cnt)减1,已发送的数据包数量(dev->stats.tx_packets)加1

    (3)检查变量db->tx_pkt_cnt(待发送的数据包)是否大于0(表明还有数据包要发送),则调用函数dm9000_send_packet发送队列中的数据包;

    (4)调用函数netif_wake_queue(dev)通知内核可以将待发送的数据包进入发送队列。

 

 

   7. 一些操作硬件细节的函数。

   在看函数之前还是先来看一下DM9000 CMD Pin 和Processor并行总线的连接关系。CMD管脚用来设置命令类型。当CMD管脚拉高时,这个命令周期访问DATA_PORT。 如果拉低, 则这个命令周期访问ADDR_PORT。见下图:

当然,内存映射的I/O空间读写还是采用最基本的readb(), readw(), readl(), writeb(), writew(), writel() , readsb(), readsw(), readsl(), writesb(), writesw(), writesl() 。

在DM9000的驱动中还自定义了几个函数,方便操作。

     * ior()

        从IO端口读一个字节。代码清单如下:

 

    * iow()

        向IO端口写一个字节。代码清单如下:

 

此外还有dm9000_outblk_8bit(), dm9000_outblk_16bit(), dm9000_outblk_32bit(), dm9000_inblk_8bit(), dm9000_inblk_16bit(), dm9000_inblk_32bit()等等。不一一解释。

 

 

======================THE END======================

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页