免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1202 | 回复: 0
打印 上一主题 下一主题

linux 设备驱动编程(二)(转) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2007-05-16 10:15 |只看该作者 |倒序浏览

Sis 900
SIS 900 是一个可以用来实作 10/100 网络卡的控制芯片。它提供了对 PCI mastermode , MII, 802.3x 流量控制等各种标准的支援。这篇文章将告诉大家,如何写一个 Linux 的网络驱动程序,它将比大家想像中简单很多。这篇文章将以 Linux 2.4 版为对象, 2.2 版提供的界面略有不同,但差别并不太大,读完本文后再读 2.2 版的程序码应该不会有太大困难才是。 本文所参考的驱动程序是在 2.4.3 版中 drivers/net/sis900.c 这个档案。你可以在 http://xxx.xxx.xxx.xxx/linux-2.4.3/drivers/net/sis900.c 找到它。如果你能有一份硬件的 databook 在手边,读起驱动程序的码可能会更简单。 SIS900的 databook 可以直接在http://www.sis.com.tw/ftp/Databook/900/sis900.exe下载。
PCI 驱动程序
对一个 PCI 驱动程序而言, Linux 提供了很完整的支援,大部份的 PCI 资讯都由内建的程序读出。对个别的驱动程序而言直接使用就可以了。所以在这个部份,唯一要做的事只是告知 PCI 子系统一个新的驱动程序己经被加入系统之中了。在档案的最末端,你会看到下面的程序,
static struct pci_driver sis900_pci_driver = {
        name:           SIS900_MODULE_NAME,
        id_table:       sis900_pci_tbl,
        probe:          sis900_probe,
        remove:         sis900_remove,
};
static int __init sis900_init_module(void)
{
        printk(KERN_INFO "%s", version);
        return pci_module_init(&sis900_pci_driver);
}
static void __exit sis900_cleanup_module(void)
{
        pci_unregister_driver(&sis900_pci_driver);
}
pci_module_init 是用来向 PCI 子系统注册一个 PCI 驱动程序。根据 id_table 中所提供的资料, PCI 子系统会在发现符合驱动程序要求的装置时使用它。那 PCI 子系统如何做到这件事呢 ? 我们先看一下 id_table 的内容就很清楚了。
static struct pci_device_id sis900_pci_tbl [] __devinitdata = {
        {PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_900,
         PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_900},
        {PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_7016,
         PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_7016},
        {0,}
};
MODULE_DEVICE_TABLE (pci, sis900_pci_tbl);
看懂了吗 ? 嗯,我想你懂了。不过我还是解释一下。前面四个分别是
vendor id : PCI_VENDOR_ID_SI
device id : PCI_DEVICE_ID_SI_900
sub vendor id : PCI_ANY_ID
sub device id : PCI_ANY_ID
意思是说这个驱动程序支援 SIS 出的 SIS900 系列所有的硬件,我们不介意 subvendor id 和 sub device id 。你可以加入任何你想要的项目。对于不同的网络卡制造商,它们可能会有不同的 sub vendor id 和 sub device id 。但只要它们用SIS900 这个芯片,那这个驱动程序就可能适用。我们可以说这是一个『公版』的驱动程序。初始化好了,那其它的部份呢 ? 还记意 sis900_pci_driver 中其它的二个项目 probe 和remove 吗 ? 它们是用来初始化和移除一个驱动程序的呼叫。你可以把它们想成驱动程序物件的 constructor 和 destructor 。在 probe 中,你应该由硬件中把一些将来可能会用到的资讯准备好。由于这是一个 PCI 驱动程序,你不必特意去检查装置是否真的存在。但如果你的驱动程序只支援某些特定的硬件,或是你想要检查系统中是否有一些特别的硬件存在,你可以在这里做。例如在这个驱动程序中,对不同版本的硬件,我们用不
同的方法去读它的 MAC 位址。
         pci_read_config_byte(pci_dev, PCI_CLASS_REVISION, &revision);
        if (revision == SIS630E_900_REV || revision == SIS630EA1_900_REV)
                ret = sis630e_get_mac_addr(pci_dev, net_dev);
        else if (revision == SIS630S_900_REV)
                ret = sis630e_get_mac_addr(pci_dev, net_dev);
        else
                ret = sis900_get_mac_addr(pci_dev, net_dev);
对于 SIS630E SIS630EA1 和 SIS630S 这些整合式芯片而言,其 MAC 位址被储存在 APC CMOS RAM 之中。但对其它独立的芯片而言则是存在网络卡的 EEPROM 之上。为了不要让这篇文章像流水帐一般,我不仔细的说明 probe 的过程。大家自己揣摸一下吧 !
在 probe 中还有一段比较和后文有关的程序码
         net_dev->open = &sis900_open;
        net_dev->hard_start_xmit = &sis900_start_xmit;
        net_dev->stop = &sis900_close;
        net_dev->get_stats = &sis900_get_stats;
        net_dev->set_config = &sis900_set_config;
        net_dev->set_multicast_list = &set_rx_mode;
        net_dev->do_ioctl = &mii_ioctl;
        net_dev->tx_timeout = sis900_tx_timeout;
        net_dev->watchdog_timeo = TX_TIMEOUT;
我想这很清楚,我们透过 net_dev 这个结构告诉 Linux 网络子系统如何来操作这个装置。当你使用 ifconfig 这个 R 令时,系统会使用 sis900_open 打开这个驱动程序,并使用 set_config 来说定装置的参数,如 IP address 。当有资料需要被传送时, sis900_start_xmit 被用来将资料送入装置之中。接下来,我们就一一的检视这些函数。
初始化装置
sis900_open(struct net_device *net_dev);
这个函数会在我们使用 ifconfig 将一网络装置激活时被呼叫。当驱动程序被插入系统之后,通常并不会马上开始接收或传送封包。一般来说,在 probe 的阶段,我们只是单纯的判断装置是否存在。实际激活硬件的动作在这里才会被实际执行。以 SIS900 为例,在其硬件中只有一个大约 2K 的缓冲区。也就是说在装置上只有一个
封包的缓冲区。当一个封包被传送后,装置必须产生一个中断要求操作系统将下一个封包传入。如果由中断到中断驱动程序被执行需要 5ms 的时间,那一秒至多我们可以送出 200 个封包。也就是说网络传送是不可能大于 400K/s ,这对于一般的情况下是不太可能接受的事。SIS900 虽然在装置上只有很小的缓冲区,但它可以透过 PCI master 模式直接控制主机板上的记忆体。事实上,它使用下面的方式来传送资料。你必须在记忆体中分配一组串接成环状串列的缓冲区,然后将 TXDP 指向缓冲区的第一个位址。 SIS900 会在第一个缓冲区传送完后自动的由第二个缓冲区取资料,并更新记忆中的资料将己传送完缓冲区的 OWN 位元清除。当 CPU 将缓冲区串列设定完成后,这个动作可以在完全没有 CPU 的介入下完成。所以硬件不必等待作业系统将新的资料送入,而可以连续的送出多个封包。操作系统只要能来的及让环状串列不会进入空的状态就可以了。
同样的,我们也需要一个接收缓冲区,使用进来的封包不至因操作系统来不及处理而遗失。在 sis900_open 中, sis900_init_rx_ring 和 sis900_init_tx_ring 就是用来负处初始化这二个串列。
在初始化串列之后,我们便可以要求 SIS900 开始接收封包。下面二行程序码便是用来做这件事。
  outl((RxSOVR|RxORN|RxERR|RxOK|TxURN|TxERR|TxIDLE), ioaddr + imr);
  outl(RxENA, ioaddr + cr);
  outl(IE, ioaddr + ier);
第一行设定硬件在下列情况发出一个系统中断,
接收失败时
接收成功 时
传送失败时
所有缓冲区中的资料都传送完时
第二行则告诉硬件操作系统己经准备好要接收资料了。第三行则时硬件实际开始送出中断。
在这个函数的最后,我们安装一个每秒执行五次的 timer 。在它的处理函数 sis900_timer 中,我们会检查目前的连结状态,这包括了连结的种类 (10/100)和连接的状态 ( 网络卡是否直的被接到网络上去 ) 。
如果各位用过 Window 2000 ,另人印象最深刻的是当你将网络线拔出时, GUI 会自动警言网络己经中断。其实 Linux 也可以做到这件事,只是你需要一个比较好的图形界面就是了。
传送一个封包的 descriptor 给网络卡
sis900_start_xmit(struct sk_buff *skb, struct net_device *net_dev);
这个函数是用来将一个由 skb 描述的网络资料缓冲区送进传送缓冲区中准备传送。其中最重要的程序码为
    sis_priv->tx_ring[entry].bufptr = virt_to_bus(skb->data);
    sis_priv->tx_ring[entry].cmdsts = (OWN | skb->len);
    outl(TxENA, ioaddr + cr);
SIS900 会使用 DMA 由缓冲区中取得封包的资料。由于缓冲区的数目有限,我们必须在缓冲区用完的时后告诉上层的网络协定不要再往下送资料了。在这里我们用下面的程序来做这件事。
     if (++sis_priv->cur_tx - sis_priv->dirty_tx tx_full = 1;
        netif_stop_queue(net_dev);
    }
netif_start_queue 用来告诉上层网络协定这个驱动程序还有空的缓冲区可用,请把下一个封包送进来。 netif_stop_queue 则是用来告诉上层网络协定所有的封包都用完了,请不要再送。
接收一个或多个封包
int sis900_rx(struct net_device *net_dev);
这个函式在会在有封包进入系统时被呼叫,因为可能有多于一个的封包在缓冲区之中。这个函数会逐一检查所有的缓冲区,直到遇到一个空的缓冲区为止。当我们发现一个有资料的缓冲区时,我们需要做二件事。首先是告知上层网络协定有一个新的封包进入系统,这件事由下面的程序完成
               skb = sis_priv->rx_skbuff[entry];
               skb_put(skb, rx_size);
               skb->protocol = eth_type_trans(skb, net_dev);
               netif_rx(skb);
前三行根据封包的内容更新 skbuff 中的档头。最后一行则是正式通知上层处理封包。
请注意 Linux 为了增加处理效能,在 netif_rx 并不会真的做完整接收封包的动作,而只是将这个封包记下来。真实的动作是在 bottom half 中才去处理。因为如此,原先储存封包的缓冲区暂时不能再被使用,我们必须重新分配一个新的缓冲区供下一个封包使用。下面的程序码是用来取得一个新的缓冲区。
      if ((skb = dev_alloc_skb(RX_BUF_SIZE)) == NULL) {
          sis_priv->rx_skbuff[entry] = NULL;
          sis_priv->rx_ring[entry].cmdsts = 0;
          sis_priv->rx_ring[entry].bufptr = 0;
          sis_priv->stats.rx_dropped++;
          break;
     }
     skb->dev = net_dev;
     sis_priv->rx_skbuff[entry] = skb;
     sis_priv->rx_ring[entry].cmdsts = RX_BUF_SIZE;
     sis_priv->rx_ring[entry].bufptr = virt_to_bus(skb->tail);
     sis_priv->dirty_rx++;
这个函数其馀的部份其实只是用来记录一些统计资料而己。
传送下一个封包
void sis900_finish_xmit (struct net_device *net_dev);
这个函数用来处理传送中断。在收到一个 TX 中断,表示有一个或多数缓冲区中的资料己经传送完成。我们可以把原先的缓冲区释出来供其它的封包使用,并且用下面的程序告诉上层协定可以送新的封包下来了。
     if (sis_priv->tx_full && netif_queue_stopped(net_dev) &&
        sis_priv->cur_tx - sis_priv->dirty_tx tx_full = 0;
        netif_wake_queue (net_dev);
    }
netif_wake_queue() 会使得上层协定开始传送新的资料下来。
改变装置的设定
int sis900_set_config(struct net_device *dev, struct ifmap *map);
处理由 ifconfig 送来的命令,在驱动程序中我们通常只处理 media type的改变。这个函数会根据 ifconfig 送来的值改变 MII 控制器的 media tyep ,你可以使用
     # ifconfig eth0 media 10basT
将目前的输出入界面强迫改到 10basT 。对于某些自动媒体检测做的有问题的switch 而言这可能是必要的设定,但一般而言默认的 auto 是最好的设定。硬件会自动决定要使用那一个界面,使用者完全不必担心,当实体层的设定改变 ( 例如将网络线插到不同的地方 ) ,硬件会自动侦测并改变设定。
void set_rx_mode(struct net_device *net_dev);
改变目前封包过滤器的模式。当你使用
      # ifconfig eth0 promisc
      # ifconfig eth0 multicast
等命令时会被呼叫。一般而言,驱动程序的默认值是只接受目的位址和网络卡的 MAC address 相同的封包。你可以透过 ifconfig 命令控制驱动程序接受其它种类的封包。结语好了 ! 我己经解析完整个网络卡的驱动程序了。当你了解这个驱动程序后,再去了解其它的驱动程序变成一件很简单的事情。大部份网络驱动程序的架构其实都很类似。事实上, Linux 早期的网络卡驱动程序几乎是由同一个人完成的。而后来的驱动程序也几乎
都以这些驱动程序为蓝本,所以看起来都很类似。你要不要也试著再去读另一个网络驱动程序的源代码呢 ? 也许 你会开始抱怨怎么写驱动程序这么神秘的东西怎么变得如此简单了 !
多馀的一节
这一节多馀的,你不想看就算了 :-) 为了证明网络驱动程序之间有多类似我再简略的trace Intel eepro100 的驱程程序给大家看。不罗唆,马上开始。
初始化
static struct pci_device_id eepro100_pci_tbl[] __devinitdata = {
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82557,
                PCI_ANY_ID, PCI_ANY_ID, },
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82559ER,
                PCI_ANY_ID, PCI_ANY_ID, },
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1029,
                PCI_ANY_ID, PCI_ANY_ID, },
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1030,
                PCI_ANY_ID, PCI_ANY_ID, },
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82820FW_4,
                PCI_ANY_ID, PCI_ANY_ID, },
        { 0,}
};
MODULE_DEVICE_TABLE(pci, eepro100_pci_tbl);
tatic struct pci_driver eepro100_driver = {
        name:           "eepro100",
        id_table:       eepro100_pci_tbl,
        probe:          eepro100_init_one,
        remove:         eepro100_remove_one,
#ifdef CONFIG_EEPRO100_PM
        suspend:        eepro100_suspend,
        resume:         eepro100_resume,
#endif
};
return pci_module_init(&eepro100_driver);
嗯 ! 一切都不出意类之外,是吧 !
初始化装置
eepro100_init_one()
这个看起来比 SIS900 的复杂多了。不过几个关鉴的函数还是一样,只是它的程序码看起比较乱。 BSD 的人喜欢说 Linux 的程序码太乱 ! 嗯,好像不承认不行 :-) 不过我说它乱的很可爱,行了吧 !
传送封包
speedo_start_xmit(struct sk_buff *skb, struct net_device *dev)
这个函数相似到我不必做任何讲解,也不必有任何文件你就可以知道它在做些什么事了 ! 程序码几乎到了一行对一行的程度 ( 夸张了一点 ! 不过很接近事实。我信相 SIS900 的 driver 是很整个程序 copy 过去再修改的 )
中断处理
void speedo_interrupt(int irq, void *dev_instance, struct pt_regs *regs);
这个函数,我再喜欢 Linux 也不得不抱怨一下了。 Donald Becker 先生,能麻烦程序写的好看一点好吗 ?
基本上,它把 sis900_rx 的内容直接放在中断处理函数之中。不过我想分开还是会清楚一些。
speedo_tx_buffer_gc 基本上就是 sis900_finish_xmit 。下面的程序是不是很眼熟呢 ?
     dirty_tx = sp->dirty_tx;
     while ((int)(sp->cur_tx - dirty_tx) > 0) {
        int entry = dirty_tx % TX_RING_SIZE;
        int status = le32_to_cpu(sp->tx_ring[entry].status);
        }
连变数名字都很像呢 !
不过 eepro100 的驱动程序没有实作 set_config 的界面,所以你不能用ifconfig 来改变 media type 。不过 eepro100 提供了由模块命令列选项改变的功 能,当然它是不及 set_config 来的方便就是了。
还要再来一个吗 ? 你自己去做吧 !

[目录]
ISA总线DMA的实现
Linux对ISA总线DMA的实现
  (By 詹荣开,NUDT dep3)
  Copyright ? 2002 by 詹荣开
  E-mail:zhanrk@sohu.com
  Linux-2.4.0 Version 1.0.0,2002-10-16
  关键词:Linux、I/O、ISA总线、设备驱动程序
  申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
  你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA
  欢迎各位指出文档中的错误与疑问。
----------------------------------------------------
  DMA是一种无需CPU的参与就可以让外设与系统RAM之间进行双向(to device 或 from device)数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率(throughput)。
  由于DMA是一种硬件机制,因此它通常与硬件体系结构是相关的,尤其是依赖于外设的总线技术。比如:ISA卡的DMA机制就与PCI卡的DMA机制有区别。本站主要讨论ISA总线的DMA技术。
1.DMA概述
  DMA是外设与主存之间的一种数据传输机制。一般来说,外设与主存之间存在两种数据传输方法:(1)Pragrammed I/O(PIO)方法,也即由CPU通过内存读写指令或I/O指令来持续地读写外设的内存单元(8位、16位或32位),直到整个数据传输过程完成。(2)DMA,即由DMA控制器(DMA Controller,简称DMAC)来完成整个数据传输过程。在此期间,CPU可以并发地执行其他任务,当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的ISR进行后处理。
  DMA技术产生时正是ISA总线在PC中流行的时侯。因此,ISA卡的DMA数据传输是通过ISA总线控制芯片组中的两个级联8237 DMAC来实现的。这种DMA机制也称为“标准DMA”(standard DMA)。标准DMA有时也称为“第三方DMA”(third-party DMA),这是因为:系统DMAC完成实际的传输过程,所以它相对于传输过程的“前两方”(传输的发送者和接收者)来说是“第三方”。
  标准DMA技术主要有两个缺点:(1)8237 DMAC的数据传输速度太慢,不能与更高速的总线(如PCI)配合使用。(2)两个8237 DMAC一起只提供了8个DMA通道,这也成为了限制系统I/O吞吐率提升的瓶颈。
  鉴于上述两个原因,PCI总线体系结构设计一种成为“第一方DMA”(first-party DMA)的DMA机制,也称为“Bus Mastering”(总线主控)。在这种情况下,进行传输的PCI卡必须取得系统总线的主控权后才能进行数据传输。实际的传输也不借助慢速的ISA DMAC来进行,而是由内嵌在PCI卡中的DMA电路(比传统的ISA DMAC要快)来完成。Bus Mastering方式的DMA可以让PCI外设得到它们想要的传输带宽,因此它比标准DMA功能满足现代高性能外设的要求。
  随着计算机外设技术的不断发展,现代能提供更快传输速率的Ultra DMA(UDMA)也已经被广泛使用了。本为随后的篇幅只讨论ISA总线的标准DMA技术在Linux中的实现。记住:ISA卡几乎不使用Bus Mastering模式的DMA;而PCI卡只使用Bus Mastering模式的DMA,它从不使用标准DMA。
2.Intel 8237 DMAC
  最初的IBM PC/XT中只有一个8237 DMAC,它提供了4个8位的DMA通道(DMA channel 0-3)。从IBM AT开始,又增加了一个8237 DMAC(提供4个16位的DMA通道,DMA channel 4-7)。两个8237 DMAC一起为系统提供8个DMA通道。与中断控制器8259的级联方式相反,第一个DMAC被级联到第二个DMAC上,通道4被用于DMAC级联,因此它对外设来说是不可用的。第一个DMAC也称为“slave DAMC”,第二个DMAC也称为“Master DMAC”。
  下面我们来详细叙述一下Intel 8237这个DMAC的结构。
  每个8237 DMAC都提供4个DMA通道,每个DMA通道都有各自的寄存器,而8237本身也有一组控制寄存器,用以控制它所提供的所有DMA通道。
  2.1 DMA通道的寄存器
  8237 DMAC中的每个DMA通道都有5个寄存器,分别是:当前地址寄存器、当前计数寄存器、地址寄存器(也称为偏移寄存器)、计数寄存器和页寄存器。其中,前两个是8237的内部寄存器,对外部是不可见的。
  (1)当前地址寄存器(Current Address Register):每个DMA通道都有一个16位的当前地址寄存器,表示一个DMA传输事务(Transfer Transaction)期间当前DMA传输操作的DMA物理内存地址。在每个DMA传输开始前,8237都会自动地用该通道的Address Register中的值来初始化这个寄存器;在传输事务期间的每次DMA传输操作之后该寄存器的值都会被自动地增加或减小。
  (2)当前计数寄存器(Current Count Register):每个每个DMA通道都有一个16位的当前计数寄存器,表示当前DMA传输事务还剩下多少未传输的数据。在每个DMA传输事务开始之前,8237都会自动地用该通道的Count Register中的值来初始化这个寄存器。在传输事务期间的每次DMA传输操作之后该寄存器的值都会被自动地增加或减小(步长为1)。
  (3)地址寄存器(Address Register)或偏移寄存器(Offset Register):每个DMA通道都有一个16位的地址寄存器,表示系统RAM中的DMA缓冲区的起始位置在页内的偏移。
  (4)计数寄存器(Count Register):每个DMA通道都有一个16位的计数寄存器,表示DMA缓冲区的大小。
  (5)页寄存器(Page Register):该寄存器定义了DMA缓冲区的起始位置所在物理页的基地址,即页号。页寄存器有点类似于PC中的段基址寄存器。
  2.2 8237 DAMC的控制寄存器
  (1)命令寄存器(Command Register)
  这个8位的寄存器用来控制8237芯片的操作。其各位的定义如下图所示:
  (2)模式寄存器(Mode Register)
  用于控制各DMA通道的传输模式,如下所示:
  (3)请求寄存器(Request Register)
  用于向各DMA通道发出DMA请求。各位的定义如下:
  (4)屏蔽寄存器(Mask Register)
  用来屏蔽某个DMA通道。当一个DMA通道被屏蔽后,它就不能在服务于DMA请求,直到通道的屏蔽码被清除。各位的定义如下:
  上述屏蔽寄存器也称为“单通道屏蔽寄存器”(Single Channel Mask Register),因为它一次只能屏蔽一个通道。此外含有一个屏蔽寄存器,可以实现一次屏蔽所有4个DMA通道,如下:
  (5)状态寄存器(Status Register)
  一个只读的8位寄存器,表示各DMA通道的当前状态。比如:DMA通道是否正服务于一个DMA请求,或者某个DMA通道上的DMA传输事务已经完成。各位的定义如下:
  2.3 8237 DMAC的I/O端口地址
  主、从8237 DMAC的各个寄存器都是编址在I/O端口空间的。而且其中有些I/O端口地址对于I/O读、写操作有不同的表示含义。如下表示所示:
Slave DMAC’s I/O port        Master DMAC’sI/O port        read        write
0x000        0x0c0        Channel 0/4 的Address Register
0x001        0x0c1        Channel 0/4的Count Register
0x002        0x0c2        Channel 1/5 的Address Register
0x003        0x0c3        Channel 1/5的Count Register
0x004        0x0c4        Channel 2/6的Address Register
0x005        0x0c5        Channel 2/6的Count Register
0x006        0x0c6        Channel 3/7的Address Register
0x007        0x0c7        Channel 3/7的Count Register
0x008        0x0d0        Status Register        Command Register
0x009        0x0d2                Request Register
0x00a        0x0d4                Single Channel Mask Register
0x00b        0x0d6                Mode Register
0x00c        0x0d8                Clear Flip-Flop Register
0x00d        0x0da        Temporary Register        Reset DMA controller
0x00e        0x0dc                Reset all channel masks
0x00f        0x0de                all-channels Mask Register

  各DMA通道的Page Register在I/O端口空间中的地址如下:
DMA channel        Page Register’sI/O port address
0        0x087
1        0x083
2        0x081
3        0x082
4        0x08f
5        0x08b
6        0x089
7        0x08a

  注意两点:
  1. 各DMA通道的Address Register是一个16位的寄存器,但其对应的I/O端口是8位宽,因此对这个寄存器的读写就需要两次连续的I/O端口读写操作,低8位首先被发送,然后紧接着发送高8位。
  2. 各DMA通道的Count Register:这也是一个16位宽的寄存器(无论对于8位DMA还是16位DMA),但相对应的I/O端口也是8位宽,因此读写这个寄存器同样需要两次连续的I/O端口读写操作,而且同样是先发送低8位,再发送高8位。往这个寄存器中写入的值应该是实际要传输的数据长度减1后的值。在DMA传输事务期间,这个寄存器中的值在每次DMA传输操作后都会被减1,因此读取这个寄存器所得到的值将是当前DMA事务所剩余的未传输数据长度减1后的值。当DMA传输事务结束时,该寄存器中的值应该被置为0。
  2.4 DMA通道的典型使用
  在一个典型的PC机中,某些DMA通道通常被固定地用于一些PC机中的标准外设,如下所示:
Channel        Size        Usage
0        8-bit        Memory Refresh
1        8-bit        Free
2        8-bit        Floppy Disk Controller
3        8-bit        Free
4        16-bit        Cascading
5        16-bit        Free
6        16-bit        Free
7        16-bit        Free

  2.5 启动一个DMA传输事务的步骤
  要启动一个DMA传输事务必须对8237进行编程,其典型步骤如下:
  1.通过CLI指令关闭中断。
  2.Disable那个将被用于此次DMA传输事务的DMA通道。
  3.向Flip-Flop寄存器中写入0值,以重置它。
  4.设置Mode Register。
  5.设置Page Register。
  6.设置Address Register。
  7.设置Count Register。
  8.Enable那个将被用于此次DMA传输事务的DMA通道。
  9.用STI指令开中断。
3 Linux对读写操作8237 DMAC的实现
  由于DMAC的各寄存器是在I/O端口空间中编址的,因此读写8237 DMAC是平台相关的。对于x86平台来说,Linux在include/asm-i386/Dma.h头文件中实现了对两个8237 DMAC的读写操作。
  3.1 端口地址和寄存器值的宏定义
  Linux用宏MAX_DMA_CHANNELS来表示系统当前的DMA通道个数,如下:
  #define MAX_DMA_CHANNELS        8

  然后,用宏IO_DMA1_BASE和IO_DMA2_BASE来分别表示两个DMAC在I/O端口空间的端口基地址:
  #define IO_DMA1_BASE        0x00
    /* 8 bit slave DMA, channels 0..3 */
  #define IO_DMA2_BASE        0xC0
    /* 16 bit master DMA, ch 4(=slave input)..7 */

  接下来,Linux定义了DMAC各控制寄存器的端口地址。其中,slave SMAC的各控制寄存器的端口地址定义如下:
#define DMA1_CMD_REG                0x08        /* command register (w) */
#define DMA1_STAT_REG                0x08        /* status register (r) */
#define DMA1_REQ_REG            0x09    /* request register (w) */
#define DMA1_MASK_REG                0x0A        /* single-channel mask (w) */
#define DMA1_MODE_REG                0x0B        /* mode register (w) */
#define DMA1_CLEAR_FF_REG        0x0C        /* clear pointer flip-flop (w) */
#define DMA1_TEMP_REG           0x0D    /* Temporary Register (r) */
#define DMA1_RESET_REG                0x0D        /* Master Clear (w) */
#define DMA1_CLR_MASK_REG       0x0E    /* Clear Mask */
#define DMA1_MASK_ALL_REG       0x0F    /* all-channels mask (w) */

  Master DMAC的各控制寄存器的端口地址定义如下:
#define DMA2_CMD_REG                0xD0        /* command register (w) */
#define DMA2_STAT_REG                0xD0        /* status register (r) */
#define DMA2_REQ_REG            0xD2    /* request register (w) */
#define DMA2_MASK_REG                0xD4        /* single-channel mask (w) */
#define DMA2_MODE_REG                0xD6        /* mode register (w) */
#define DMA2_CLEAR_FF_REG        0xD8        /* clear pointer flip-flop (w) */
#define DMA2_TEMP_REG           0xDA    /* Temporary Register (r) */
#define DMA2_RESET_REG                0xDA        /* Master Clear (w) */
#define DMA2_CLR_MASK_REG       0xDC    /* Clear Mask */
#define DMA2_MASK_ALL_REG       0xDE    /* all-channels mask (w) */

  8个DMA通道的Address Register的端口地址定义如下:
#define DMA_ADDR_0              0x00    /* DMA address registers */
#define DMA_ADDR_1              0x02
#define DMA_ADDR_2              0x04
#define DMA_ADDR_3              0x06
#define DMA_ADDR_4              0xC0
#define DMA_ADDR_5              0xC4
#define DMA_ADDR_6              0xC8
#define DMA_ADDR_7              0xCC

  8个DMA通道的Count Register的端口地址定义如下:
#define DMA_CNT_0               0x01    /* DMA count registers */
#define DMA_CNT_1               0x03
#define DMA_CNT_2               0x05
#define DMA_CNT_3               0x07
#define DMA_CNT_4               0xC2
#define DMA_CNT_5               0xC6
#define DMA_CNT_6               0xCA
#define DMA_CNT_7               0xCE

  8个DMA通道的Page Register的端口地址定义如下:
#define DMA_PAGE_0              0x87    /* DMA page registers */
#define DMA_PAGE_1              0x83
#define DMA_PAGE_2              0x81
#define DMA_PAGE_3              0x82
#define DMA_PAGE_5              0x8B
#define DMA_PAGE_6              0x89
#define DMA_PAGE_7              0x8A

  Mode Register的几个常用值的定义如下:
  #define DMA_MODE_READ        0x44
  /* I/O to memory, no autoinit, increment, single mode */
  #define DMA_MODE_WRITE        0x48
  /* memory to I/O, no autoinit, increment, single mode */
  #define DMA_MODE_CASCADE 0xC0
   /* pass thru DREQ->HRQ, DACK>16);
        if (dmanr >8) & 0xff, ((dmanr&3)>1) & 0xff, ((dmanr&3)>9) & 0xff, ((dmanr&3)>8) & 0xff, ((dmanr&3)>1) & 0xff, ((dmanr&3)>9) & 0xff, ((dmanr&3)= MAX_DMA_CHANNELS)
                return -EINVAL;
        if (xchg(&dma_chan_busy[dmanr].lock, 1) != 0)
                return -EBUSY;
        dma_chan_busy[dmanr].device_id = device_id;
        /* old flag was 0, now contains 1 to indicate busy */
        return 0;
}

  上述函数的核心实现就是用原子操作xchg()让成员变量dma_chan_busy[dmanr].lock和值1进行交换操作,xchg()将返回lock成员在交换操作之前的值。因此:如果xchg()返回非0值,这说明dmanr所指定的DMA通道已被其他设备所占用,所以request_dma()函数返回错误值-EBUSY表示指定DMA通道正忙;否则,如果xchg()返回0值,说明dmanr所指定的DMA通道正处于free状态,于是xchg()将其lock成员设置为1,取得资源的使用权。
  4.3 释放DMA通道资源
  DMA传输事务完成后,设备驱动程序一定要记得释放所占用的DMA通道资源。否则别的外设将一直无法使用该DMA通道。
  函数free_dma()释放指定的DMA通道资源。如下:
void free_dma(unsigned int dmanr)
{
        if (dmanr >= MAX_DMA_CHANNELS) {
                printk("Trying to free DMA%d
", dmanr);
                return;
        }
        if (xchg(&dma_chan_busy[dmanr].lock, 0) == 0) {
                printk("Trying to free free DMA%d
", dmanr);
                return;
        }
} /* free_dma */

  显然,上述函数的核心实现就是用原子操作xchg()将lock成员清零。
  4.4 对/proc/dma文件的实现
  文件/proc/dma将列出当前8个DMA通道的使用状况。Linux在kernel/Dma.c文件中实现了函数个get_dma_list()函数来至此/proc/dma文件的实现。函数get_dma_list()的实现比较简单。主要就是遍历数组dma_chan_busy[],并将那些lock成员为非零值的数组元素输出到列表中即可。如下:
int get_dma_list(char *buf)
{
        int i, len = 0;
        for (i = 0 ; i

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/17928/showart_302241.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP