遇顺境,处之淡然;遇逆境,处之泰然
分类: LINUX
2013-03-20 10:59:24
和所有其他模块一样,函数init_module是cs8900的入口:
module_init (cs8900_init);
module_exit (cs8900_cleanup);
cs8900_init函数填充net_device结构体。net_device是网络驱动中最为重要的一个结构,需要认真阅读,其原型在 include/linux/netdevice.h中。为了填充如此复杂的结构,内核提供了ether_setup函数作为辅助。关于ether_setup函数,你只要知道它的功能就可以了。当然,ether_setup不是万能的,你还要手工完成类似如下一些内容:
ndev->open = cs8900_start;
ndev->stop = cs8900_stop;
ndev->hard_start_xmit = cs8900_send_start;
ndev->get_stats = cs8900_get_stats;
ndev->set_multicast_list = cs8900_set_receive_mode;
ndev->tx_timeout = cs8900_transmit_timeout;
而下面语句的含义也非常明确:
cs8900_dev->init = cs8900_probe
cs8900_probe是初始化函数(Driver initialization routines),主要完成一些初始化操作,我们在下一章中介绍。cs8900_init中使用了另外一个函数还没有说-->alloc_etherdev。alloc_etherdev是alloc_netdev的封装函数,它负责在内核空间为 net_device结构体分配内存(kmalloc)。alloc_etherdev的原型在include/linux/etherdevice.h中。
在函数最后,通过register_netdev完成设备注册。register_netdev函数原型在net/core/dev.c中,但是如果你就是为了写驱动,你可以不往下看了。
一句话总结:cs8900_init在使用各种手段填充了net_device结构体后,通过register_netdev向系统注册了一个网络设备。
cs8900_probe函数本身并不难,但是你必须清楚probe函数的重要职能。因为你很有可能要为属于你的设备编写一个probe函数。
先来看一行最易懂的代码: printk(VERSION_STRING"/n"); 显然,内核启动时显示的相关信息就是这句话打印出来的。
net_device中的dev_addr就是网卡的MAC地址,你应该提供:
#if defined(CONFIG_ARCH_SMDK2410) dev->dev_addr[0] = 0x00; dev->dev_addr[1] = 0x00; dev->dev_addr[2] = 0x3e; dev->dev_addr[3] = 0x26; dev->dev_addr[4] = 0x0a; dev->dev_addr[5] = 0x00; #endif
使用类似的代码,为我们的网络芯片提供 dev->if_port = IF_PORT_10BASET; (支持的多种接口) dev->priv = (void *) &priv; (设备私有数据结构)
哦,还要多说一句,IF_PORT_10BASET在include/linux/netdevice.h中定义,你通过这个枚举类型,可以了解更多的接口信息:
enum { IF_PORT_UNKNOWN = 0, IF_PORT_10BASE2, IF_PORT_10BASET, IF_PORT_AUI, IF_PORT_100BASET, IF_PORT_100BASETX, IF_PORT_100BASEFX };
下面的代码对于移植是非常关键的。vSMDK2410_ETH_IO是网卡的虚拟地址,IRQ_EINT9是硬件中断。这两个值和另外一个地址(网卡的物理地址)必须在头文件中定义,而且要正确的定义。
#if defined(CONFIG_ARCH_SMDK2410) dev->base_addr = vSMDK2410_ETH_IO + 0x300; dev->irq = IRQ_EINT9; #endif /* #if defined(CONFIG_ARCH_SMDK2410) */
接下来分配I/O端口资源。使用的函数是check_mem_region和request_mem_region。LDD中的理论结合这里的实践,相信你很快就掌握驱动编程中的这个API了。
下面的代码实现了硬件的读写。使用的函数是cs8900_read和cs8900_write。虽然只需很短的篇幅就可以解释这对读写函数,但是为了不分散你对cs8900_probe函数的注意力,我还是留到下一节中说,不要着急:) 哦,差点忘了,这里使用了内核的自旋锁(spin_lock_init),不过关于自旋锁的机制已经超出本文的范围了。你现在只需要知道自旋锁用于并发控制,以及如何动态初始化自旋锁就可以了。如果你还想继续了解,请自行学习与自旋锁相关的API,它们是:
spinlock_t spin; //定义自旋锁 spin_lock(lock); //获得自旋锁 spin_trylock(lock); //尝试获得自旋锁 spin_unlock(lock); //释放自旋锁
一句话总结:cs8900_probe函数继续为设备进行初始化,并申请各种资源。
上回留下了cs8900_read和cs8900_write没有讲。本节会专门讲解这两个函数,以及相关联的内容。其实就函数本身并不难,说白了就是向硬件进行读写,完成硬件的控制。但是这个过程是整个驱动程序设计中最关键的。换句话说,你要写驱动,很重要的一部分工作就是设置你的网络芯片,使它能乖乖的为你工作。
好,先看函数原型,它们都在cs8900.c文件中定义:
static inline u16 cs8900_read (struct net_device *dev,u16 reg)
{
outw (reg,dev->base_addr + PP_Address);
return (inw (dev->base_addr + PP_Data));
}
static inline void cs8900_write (struct net_device *dev,u16 reg,u16 value)
{
outw (reg,dev->base_addr + PP_Address);
outw (value,dev->base_addr + PP_Data);
}
看起来真的很简单。reg表示寄存器(不知道寄存器?赶快google去),value表示要赋的值。
/*******
几乎每一种外设都是通过读写设备上的寄存器来进行的。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址。CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。
********/
打开cs8900.h和硬件芯片的datasheet,你就会明白了:
#define PP_Address 0x0a /* PacketPage Pointer Port (Section 4.10.10) */
#define PP_Data 0x0c /* PacketPage Data Port (Section 4.10.10) */
曾经有一个学生和我争论,说他的Linux驱动中使用了就是类似0x0a之类的实地址,而不是我讲的映射后的虚拟地址。其实是他忘记了代码中是使用dev->base_addr+偏移的形式。所以不要再犯同样的错误哦。
剩下的一点知识就是Linux提供的I/O端口访问方法。除了本例使用的outw/inw外,还有下面几个(linux/include/asm-arm/io.h)。它们的区别我记得在LDD中有了解释。
#ifdef __io
#define outb(v,p) __raw_writeb(v,__io(p))
#define outw(v,p) __raw_writew((__force __u16) /
cpu_to_le16(v),__io(p))
#define outl(v,p) __raw_writel((__force __u32) /
cpu_to_le32(v),__io(p))
#define inb(p) ({ __u8 __v = __raw_readb(__io(p)); __v; })
#define inw(p) ({ __u16 __v = le16_to_cpu((__force __le16) /
__raw_readw(__io(p))); __v; })
#define inl(p) ({ __u32 __v = le32_to_cpu((__force __le32) /
__raw_readl(__io(p))); __v; })
#define outsb(p,d,l) __raw_writesb(__io(p),d,l)
#define outsw(p,d,l) __raw_writesw(__io(p),d,l)
#define outsl(p,d,l) __raw_writesl(__io(p),d,l)
#define insb(p,d,l) __raw_readsb(__io(p),d,l)
#define insw(p,d,l) __raw_readsw(__io(p),d,l)
#define insl(p,d,l) __raw_readsl(__io(p),d,l)
#endif
一句话总结:cs8900_read和cs8900_write函数用来读写设备。
我们最初分析的函数是cs8900_init,并由此展开,跟踪到了cs8900_probe、cs8900_read、cs8900_write等,现在收回来,还是回到cs8900_init中。
在cs8900_init里找到如下一行代码:
ndev->open = cs8900_start;
如果你之前了解过字符设备,你很容易联想到file_operation中的open。其实它们是一样的。open函数在网络设备被激活时(ifconfig)调用。因此,我们在编写网卡驱动时,要考虑网卡激活时,需要完成哪些事情。通常要进行中断的申请、资源的申请等。在cs8900的驱动中,主要完成两件事情:激活网卡和申请中断。激活网卡的代码如下:
cs8900_set (dev,PP_RxCFG,RxOKiE | BufferCRC | CRCerroriE | RuntiE | ExtradataiE);
cs8900_set (dev,PP_RxCTL,RxOKA | IndividualA | BroadcastA);
cs8900_set (dev,PP_TxCFG,TxOKiE | Out_of_windowiE | JabberiE);
cs8900_set (dev,PP_BufCFG,Rdy4TxiE | RxMissiE | TxUnderruniE | TxColOvfiE | MissOvfloiE);
cs8900_set (dev,PP_LineCTL,SerRxON | SerTxON);
cs8900_set (dev,PP_BusCTL,EnableRQ);
申请中断的方法和以前一样:
request_irq (dev->irq, &cs8900_interrupt, 0, dev->name, dev))
显然,我们下一个任务就是完成中断处理函数了(cs8900_interrupt),我们后面再说。
当资源准备就绪后,需要调用netif_start_queue函数开启网络接口的接收和发送数据队列。这个函数原型在netdevivce.h中。与它类似的还有一个函数,叫netif_wake_queue。有人更喜欢使用netif_wake_queue函数,因为它可以通知网络系统可再次开始传输数据包。
static inline void netif_start_queue(struct net_device *dev)
{
clear_bit(__LINK_STATE_XOFF, &dev->state);
}
static inline void netif_wake_queue(struct net_device *dev)
{
#ifdef CONFIG_NETPOLL_TRAP
if (netpoll_trap()) {
clear_bit(__LINK_STATE_XOFF, &dev->state);
return;
}
#endif
if (test_and_clear_bit(__LINK_STATE_XOFF, &dev->state))
__netif_schedule(dev);
}
一句话总结:我们需要实现一个open函数,用来完成资源申请和开启网络接口的数据队列任务。
上次说到的cs8900_start函数中,完成了中断申请任务。什么是中断?中断是一种电信号,由硬件设备产生,并直接送入中断控制器的输入引脚上,然后再由中断控制器向处理器发送相应的信号。
其中中断处理函数cs8900_interrupt没有讲。中断处理函数是中断发生时系统要执行的函数。本文将主要介绍中断处理函数的功能。我们不分析中断实现机制,而是关心中断发生时,要做哪些事情。要知道,中断处理函数设计的好坏,会直接关心到性能乃至稳定问题。
虽然我不会讲中断的实现,但是你必须清楚中断的两种类型:
1. 轮询(polling) 让内核定期对设备的状态进行查询,然后做出相应的处理;
2. 中断(interrupt) 让硬件在需要的时候向内核发出信号(变内核主动为硬件主动)。
如果你对中断的实现感兴趣,你应该去了解hw_interrupt_type、irq_desc_t 和 irqaction这些数据结构。
我们的中断处理函数通过switch/case语句判断中断类型,并进行相应处理。具体过程是:首先读出ISQ寄存器的值,然后根据ISQ的值分别处理各种情况。当中断发生时,这些中断实际反映在相应的寄存器中,ISQ寄存器用低6位记录了当前寄存器的编号,高10位记录了当前寄存器的实际内容。代码如下:
while ((status = cs8900_read (dev, PP_ISQ))) {
handled = 1;
switch (RegNum (status)) {
case RxEvent: //接收到了数据包
cs8900_receive (dev);
break;
case TxEvent: //根据open中的设置,分别处理各种类型的发送事件。
priv->stats.collisions += ColCount (cs8900_read (dev,PP_TxCOL));
if (!(RegContent (status) & TxOK)) {
priv->stats.tx_errors++;
if ((RegContent (status) & Out_of_window)) priv->stats.tx_window_errors++;
if ((RegContent (status) & Jabber)) priv->stats.tx_aborted_errors++;
break;
} else if (priv->txlen) {
priv->stats.tx_packets++;
priv->stats.tx_bytes += priv->txlen;
}
priv->txlen = 0;
netif_wake_queue (dev);
break;
case BufEvent: //两种情况,分别进行处理。
//当RxMiss置位,表示传输过程中丢帧,于是读寄存器获取丢失的包的数目。
if ((RegContent (status) & RxMiss)) {
u16 missed = MissCount (cs8900_read (dev,PP_RxMISS));
priv->stats.rx_errors += missed;
priv->stats.rx_missed_errors += missed;
}
//当TxUnderrun置位,表示在帧结束前网卡运行已过时。改变网络状态结构体中对应元素的值,接着通知上层可往下发送包数据。
if ((RegContent (status) & TxUnderrun)) {
priv->stats.tx_errors++;
priv->stats.tx_fifo_errors++;
priv->txlen = 0;
netif_wake_queue (dev);
}
/* FIXME: if Rdy4Tx, transmit last sent packet (if any) */
break;
case TxCOL: //当传输出现冲突错误时,通过读寄存器值得到当前冲突的个数,加到统计结构体中的对应元素值上。
priv->stats.collisions += ColCount (cs8900_read (dev,PP_TxCOL));
break;
case RxMISS: //读寄存器获取丢失帧的个数。
status = MissCount (cs8900_read (dev,PP_RxMISS));
priv->stats.rx_errors += status;
priv->stats.rx_missed_errors += status;
break;
}
其中的细节我们下次再说。
一句话总结:中断处理函数是中断产生时执行的函数,它根据中断种类进行处理。
是时候讲解数据收发的时候了。用来接收数据的函数是cs8900_receive,它在上文中出现过。那么数据如何处理的?简单的说,1)把数据放到skb中,2)执行netif_rx 函数。
skb又是什么?它是网络设备驱动中,和net_device一样重要的数据结构。skb就是sk_buff结构,也就是套接字缓冲区。它在skbuff.h中定义。有多重要呢?不描述了,干脆把代码列出来,虽然有点长,但毕竟也不是书,大家就忍了吧。反正这个结构是必须清楚的。
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
struct sock *sk;
ktime_t tstamp;
struct net_device *dev;
struct dst_entry *dst;
struct sec_path *sp;
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48];
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
__u32 priority;
__u8 local_df:1,
cloned:1,
ip_summed:2,
nohdr:1,
nfctinfo:3;
__u8 pkt_type:3,
fclone:2,
ipvs_property:1,
peeked:1,
nf_trace:1;
__be16 protocol;
void (*destructor)(struct sk_buff *skb);
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct nf_conntrack *nfct;
struct sk_buff *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
int iif;
#ifdef CONFIG_NETDEVICES_MULTIQUEUE
__u16 queue_mapping;
#endif
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
/* 2 byte hole */
#ifdef CONFIG_NET_DMA
dma_cookie_t dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
__u32 mark;
sk_buff_data_t transport_header;
sk_buff_data_t network_header;
sk_buff_data_t mac_header;
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
atomic_t users;
};
也许你看到这个庞大的结构(实际上它并不是很庞大)有些晕!但是更令人晕的是内核把sk_buff组织成一个双向链表。而且这个链表的结构要比常见的双向链表的结构还要复杂。sk_buff中用两个指针(next和prev)表示前后一个节点。这个链表还有另一个需求:每个sk_buff结构都必须能够很快找到链表头节点。为了满足这个需求,在第一个节点前面会插入另一个结构sk_buff_head,这是一个辅助节点。在Linux内核世界中,图示永远比代码好理解,所以你要善于看图说话。
本来我很想多介绍一下skb,但是又怕那将令大家再一次陷入内核的迷雾,所以还是见好就收吧。你现在知道,网卡传输数据离不开skb,因此我们需要填充这个skb。当skb搞定后,只要下面一句就可以:
netif_rx (skb);
它把skb扔到了上层。netif_rx函数原型在net/core/dev.c中,它有两个返回值:
NET_RX_SUCCESS (no congestion)
NET_RX_DROP (packet was dropped)
一句话总结:网卡的数据接收是通过netif_rx完成的,而传递给netif_rx的是一个包含数据的socket buffer。