本博客移往腾讯QQ空间: http://user.qzone.qq.com/308337370/blog/1308149414
-
近期文章
近期评论
mayli发表在《移植、裁减及配置Linux内核到s3c2440开发板》 scyangzhu发表在《ARM编程进阶之一 —— ARM汇编伪指令》 笑寒发表在《ARM编程进阶之一 —— ARM汇编伪指令》 Shaohua发表在《关于中国教育之我见——重点大学篇》 tao发表在《关于中国教育之我见——重点大学篇》 归档
分类
功能
本博客移往腾讯QQ空间: http://user.qzone.qq.com/308337370/blog/1308149414
我们在LED灯的驱动中,学习了如何在驱动中实现ioctl功能函数,但这种简化的实现存在一些潜在的隐患。例如,有2个设备,一个是led灯,另一个是硬盘。这2个设备的驱动由不同的程序员编写。led灯驱动定义命令0表示亮灯,而硬盘驱动定义命令0表示格式化硬盘。这样一来就存在潜在的威胁,例如,如果应用程序员本想通过ioctl(fd, 0)点亮led灯,但在调用open的时候,误将硬盘的设备文件当成了led灯的设备文件来打开,那么当应用程序执行ioctl(fd, 0)的时候就会导致硬盘被格式化。这个问题如何解决呢?最好的解决方案就是想办法让不同设备的命令的编号落在不同的区间上,例如led的命令编号为0-10,硬盘的命令编号为20-100,以此类推,这样即使应用程序误将硬盘当作led灯来打开,但由于其执行的是ioctl(fd, 0)或者ioctl(fd, 1),命令0和1均不是硬盘对应的命令,这样只会使得ioctl执行失败,而不会导致硬盘被格式化。
正是基于这样的考虑,内核编写者给出了一个规范,使得不同设备的命令的编号处于不同的区间,所以我们在编写驱动程序时应该遵循该规范。下面就来介绍该规范。
int scull_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
ioctl的第3个参数(cmd)是命令编号,第4个参数(arg)是命令的参数。
一、规范ioctl的第3个参数
1、规范将cmd的32bit从高位到低位分为4个部分:
2、系统头文件中定义的几个规范的宏
这样一来#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3) 使得命令编号SCULL_IOCTQUANTUM为二进制的 00 00000000000000 字符k的ASCII码 00000011。由于字符k是专门分给scull驱动所使用(其它驱动不会使用k),所以只要scull驱动自己在定义自己的命令编号(NR)时保证不冲突,则这个编号在所有驱动中都不会冲突。
二、规范化的ioctl的第3、4个参数的应用(_IOC_TYPE、_IOC_NR等都是内核定义的宏,猜出它们的涵义应该不是难事吧!)
1、if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
可以通过比较cmd的TYPE段与分配给本驱动的TYPE段是否相等来确定应用程序传入的命令是否是本驱动支持的命令
2、if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;
可以通过比较cmd的NR段是否大于了本驱动支持的命令的总数量来确定应用程序传入的命令是否是本驱动支持的命令(当然前提是命令的编号从1开始顺次编号,并且宏SCULL_IOC_MAXNR是命令的总数量)
3、if (_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
可以通过查看cmd的DIR段来得知ioctl是否用来从内核读、写数据到用户空间,进而调用access_ok来测试该用户缓冲区是否安全可写(或可读)
注:内核API access_ok 用于测试用户缓冲区(arg指向的内存区,大小为_IOC_SIZE(cmd ))是否安全可写(或可读)
4、__get_user利用ioctl的第4个参数
switch(cmd)
case SCULL_IOCSQUANTUM:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
retval = __get_user(scull_quantum, (int __user *)arg);
注:__get_usr用于从arg所指向的用户内存区拷贝sizeof(*arg)大小的数据到scull_quantum,用于替换copy_from_user;__put_usr反之 ;capable (CAP_SYS_ADMIN)表示进程有没有管理员权限
三、scull驱动对ioctl规范化实现的代码分析
这就留给不太懒的你吧!
请查看scull驱动中的scull_ioctl函数的实现来体会ioctl的规范化实现;
请查看scull驱动中scull.h头文件以了解对ioctl各个命令的编号的规范化定义:
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
前面的讲解中我们看到了,main(或相当于main的xmain)函数并不是整个程序首先运行的部分,它会被一个称为启动例程的代码所调用。如果是用gcc编译运行在Linux操作系统上的应用程序,则启动例程是由gcc在编译时放在程序员所写代码之前的;现在我们是用ads编写裸机程序,所以需要程序员自行编写启动例程并设法将其放在整个程序的开头。
为什么需要启动例程呢?启动例程都干些什么事情?为什么启动例程总是用汇编语言写就?如何才能保证程序运行时首先运行的是启动例程?
好,先看一个
本文的素材以及源代码(稍有改动)均来源于《Linux Device Driver》,因此本文可视为该书块设备驱动相关章节的阅读理解。
一、体验块设备驱动(单击下载驱动源码)
本驱动模拟了一个硬盘。想想你去中关村(不过我更喜欢去成都@世界)买了一个硬盘,迫不及待地安装在你的Linux机器上,你怎么样才能使用这个新硬盘呢?当然要先把它驱动起来。
1、make生成sbull.ko后加载驱动:sudo insmod sbull.ko
2、对硬盘分区
3、输入 sudo mkfs.ext2 /dev/sbulla1 在硬盘分区上格式化ext2文件系统
4、挂载新硬盘上的分区
之后,你就可以通过testdir目录来访问新硬盘分区了。
二、块设备驱动框架介绍
1、驱动接口简介
注1: 借助minor number辨别partition编号(也可能是整个磁盘),再借助分区表决定分区的起始sector号
注2: #define KERNEL_SECTOR_SHIFT 9 #define KERNEL_SECTOR_SIZE (1 << KERNEL_SECTOR_SHIFT)
2、块设备结构体
块设备结构体 struct gendisk 是块设备驱动中最重要的数据结构,它在操作系统和驱动中代表一个物理磁盘。其主要字段有:
3、块设备结构体注册
4、块设备结构体注销
5、块设备的简单读写-原理
6、读写队列结构体-注册与注销
7、块设备的简单读写-实现
108 static void sbull_request(request_queue_t *q)
109 {
110 struct request *req;
112 while ((req = elv_next_request(q)) != NULL) { //从request queue中取出一个request,循环直到request queue中的所有request被传送完
//因为读写队列与块设备结构体已关联,所以block layer层在将读写请求链入request queue时,能将rq_disk字段指向块设备结构体
113 struct sbull_dev *dev = req->rq_disk->private_data;
114 if (! blk_fs_request(req)) {
116 end_request(req, 0);
117 continue;
118 }
123 sbull_transfer(dev, req->sector, req->current_nr_sectors, //根据request中指定的方向(rq_data_dir)、数据在内存中的位置( req->buffer )、
124 req->buffer, rq_data_dir(req)); //在设备上的位置( req->sector )、传输数据量的大小( req->current_nr_sectors ),完成物理读写
125 end_request(req, 1); //通知block layer层;将request从request queue中摘下;释放request结构的内存,唤醒等待该request完成的所有进程
126 }
127 }
void end_request(struct request *req, int uptodate)
{
//驱动通知block layer层实际完成的读写量,block layer层据此更新其内部各个数据结构;并告知驱动是否整个request已经处理完成
if (!end_that_request_first(req, uptodate, req->hard_cur_sectors)) {
add_disk_randomness(req->rq_disk);
blkdev_dequeue_request(req); //若是,驱动则负责将request从request queue中摘下
end_that_request_last(req); //释放request结构的内存,唤醒等待该request完成的所有进程
}
}
8、简单读写的不足
9、请求队列
10、块设备的高效读写-原理与实现
一个request结构体中包含多个在内存中离散,但在物理设备上却连续的数据块,由bio和bio_vec结构体来表示
337 static void setup_device(struct sbull_dev *dev, int which)
362 switch (request_mode) {
370 case RM_FULL:
371 dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
374 break;
385 }
387 dev->queue->queuedata = dev;
409 }
175 static void sbull_full_request(request_queue_t *q)
176 {
177 struct request *req;
178 int sectors_xferred;
179 struct sbull_dev *dev = q->queuedata;
181 while ((req = elv_next_request(q)) != NULL) { //每次取出读写队列中的一个request
187 sectors_xferred = sbull_xfer_request(dev, req);
188 if (! end_that_request_first(req, 1, sectors_xferred)) {
189 blkdev_dequeue_request(req);
191 end_that_request_last(req, 1);
192 }
193 }
194 }
#define rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
157 static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
158 {
159 struct bio *bio;
160 int nsect = 0;
162 rq_for_each_bio(bio, req) { //while循环,每次取出req中的一个bio
163 sbull_xfer_bio(dev, bio);
164 nsect += bio->bi_size/KERNEL_SECTOR_SIZE; //bi_size记录一个bio中数据的总字节数
165 }
167 return nsect;
168 }
#define bio_for_each_segment(bvl, bio, i) \
__bio_for_each_segment(bvl, bio, i, (bio)->bi_idx)
#define __bio_for_each_segment(bvl, bio, i, start_idx) \
for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx); \
i < (bio)->bi_vcnt; \
bvl++, i++)
133 static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
134 {
135 int i;
136 struct bio_vec *bvec;
137 sector_t sector = bio->bi_sector; //bi_sector记录bio中首字节应位于硬件的哪个扇区。bio中的所有segment在硬件上的sector位置全部连续
139 /* Do each segment independently. */
140 bio_for_each_segment(bvec, bio, i) { //while循环,每次取出bio中的一个bio_vec来传输
141 char *buffer = __bio_kmap_atomic(bio, i, KM_USER0); //获取bv_page的内核virtual address
143 sbull_transfer(dev, sector, bio_cur_sectors(bio), //bio_cur_sectors(bio)求出bio当前segment的大小(sector数目)
144 buffer, bio_data_dir(bio) == WRITE);
145 sector += bio_cur_sectors(bio); //累进数据在硬件上的位置(sector)
147 __bio_kunmap_atomic(bio, KM_USER0);
149 }
151 return 0; /* Always "succeed" */
152 }
11、块设备的其它操作接口fops(open、release、media_change、revalidate_disk、ioctl)
1) open与release(本驱动可以模拟光盘从光驱中更换)
216 static int sbull_open(struct inode *inode, struct file *filp)
217 {
218 struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data; //inode->i_bdev->bd_disk指向关联的gendisk structure
223 if (! dev->users)
224 check_disk_change(inode->i_bdev); //check_disk_change将导致对media_change的调用,若介质已改变,将导致调用revalidate_disk
225 dev->users++;
228 }
int check_disk_change(struct block_device *bdev)
{
struct gendisk *disk = bdev->bd_disk;
struct block_device_operations * bdops = disk->fops;
if (!bdops->media_changed) //media_changed为NULL
return 0;
if (!bdops->media_changed(bdev->bd_disk)) //media_changed不为NULL时,则会被内核API check_disk_change所调用
return 0;
if (__invalidate_device(bdev))
printk("VFS: busy inodes on changed media.\n");
if (bdops->revalidate_disk) // 在media_changed返回true的情况下,revalidate_disk不为NULL时,则会被内核API check_disk_change所调用
bdops->revalidate_disk(bdev->bd_disk);
if (bdev->bd_disk->minors > 1)
bdev->bd_invalidated = 1;
return 1;
}
230 static int sbull_release(struct inode *inode, struct file *filp)
231 {
232 struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
235 dev->users–;
244 }
12、media_change与revalidate_disk
249 int sbull_media_changed(struct gendisk *gd)
250 {
251 struct sbull_dev *dev = gd->private_data;
253 return dev->media_change;
254 }
260 int sbull_revalidate(struct gendisk *gd)
261 {
262 struct sbull_dev *dev = gd->private_data;
264 if (dev->media_change) {
265 dev->media_change = 0;
266 // memset (dev->data, 0, dev->size);
267 }
269 }
13、ioctl
291 int sbull_ioctl (struct inode *inode, struct file *filp,
292 unsigned int cmd, unsigned long arg)
293 {
294 long size;
295 struct hd_geometry geo;
296 struct sbull_dev *dev = filp->private_data;
298 switch(cmd) {
299 case HDIO_GETGEO:
301 /*
302 * Get geometry: since we are a virtual device, we have to make
303 * up something plausible. So we claim 16 sectors, four heads,
304 * and calculate the corresponding number of cylinders. We set the
305 * start of data at sector four.
306 */
307 // size = dev->size*(hardsect_size/KERNEL_SECTOR_SIZE);
308 size = dev->size / KERNEL_SECTOR_SIZE;
309 geo.cylinders = (size & ~0x3f) >> 6;
310 geo.heads = 4;
311 geo.sectors = 16;
312 geo.start = 4;
313 if (copy_to_user((void __user *) arg, &geo, sizeof(geo)))
314 return -EFAULT;
315 return 0;
316 }
318 return -ENOTTY; /* unknown command */
319 }
本文将以真实的网卡cs8900为实例,介绍网卡驱动程序的编写。(单击下载cs8900驱动源码)
通过“网络设备驱动基础”一文,我们学习了网卡驱动编写的大致框架。对于真实网卡驱动,其驱动程序架构也是类似的,最大区别在于:
一、驱动整体框架分析
1、驱动接口简介
2、网络设备结构体struct net_device在操作系统内部代表一张网卡,含有很多重要字段
3、数据包结构体,代表要发送或发送的一个数据包。含有很多重要字段:
4、网络设备注册(cs8900_probe (struct platform_device *pdev) )
1)操作系统接口
2)硬件接口
5、网络设备注销(cs8900_drv_remove(struct platform_device *pdev))
6、关于平台设备
1)平台设备及其驱动的数据结构定义
static struct resource cs8900_resource[] = {
[0] = {
.start = 0x19000300,
.end = 0x19000310,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_EINT9,
.end = IRQ_EINT9,
.flags = IORESOURCE_IRQ,
}
};
struct platform_device net_device_cs8900 = {
.name = "cs8900",
.id = -1,
.num_resources =ARRAY_SIZE(cs8900_resource),
.resource = cs8900_resource,
};
static struct platform_device *network_devices[] __initdata = {
&net_device_cs8900,
};
static struct platform_driver cs8900_driver = {
.driver = {
.name = "cs8900",
.owner = THIS_MODULE,
},
.probe = cs8900_probe,
.remove = __devexit_p(cs8900_drv_remove)
}
2)平台设备及其驱动注册
platform_add_devices(network_devices, ARRAY_SIZE(network_devices));
platform_driver_register(&cs8900_driver)
{
将cs8900_driver插入平台驱动链表;
扫描平台驱动链表以及平台设备链表,用2者的name字段进行匹配;
if (匹配成功)
回调平台驱动中定义的probe函数
}
在注册成功后,如果发生平台设备的热插拔,操作系统会自动回调匹配的平台驱动的probe和remove函数。platform_driver_unregister的调用也会引起平台驱动的remove函数被执行。这样就可以在probe函数中实现网卡设备的注册,在remove函数中实现网卡设备的注销
在平台设备注册成功后,就可以在任何时候(通常在平台驱动的probe函数中)方便的获得平台设备的信息(资源)
cs8900_probe (struct platform_device *pdev)
{
priv->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //priv->addr_res 将会等于cs8900_resource[0],其中有I/O基址0x19000300
priv->irq_res= platform_get_resource(pdev, IORESOURCE_IRQ, 0); // priv->irq_res将会等于cs8900_resource[1],其中有中断号IRQ_EINT9
}
7、打开网卡设备cs8900_open(struct net_device *ndev)
8、关闭网卡设备cs8900_close(struct net_device *ndev)
9、数据发送cs8900_start_xmit (struct sk_buff *skb, struct net_device *ndev)
netif_stop_queue (ndev);//通知协议栈不要再调用自己发送数据
//通知硬件准备发送以及要发送的数据的长度后,向硬件提交数据 4.10.8
cs8900_write (ndev,PP_TxCMD,TxStart (After5));
cs8900_write (ndev,PP_TxLength,skb->len);
status = cs8900_read (ndev,PP_BusST);
if ((status & TxBidErr)) return 1;//数据包超长,返回1告知协议栈传输错误
if (!(status & Rdy4TxNOW)) return 1;//硬件未能成功分配容纳数据包的buffer
cs8900_frame_write (ndev,skb);//向硬件提交数据
ndev->trans_start = jiffies;//记录发送时间,供协议栈检测超时使用
dev_kfree_skb (skb);//已发送成功,故kfree skb
priv->txlen = skb->len;//暂存发送数据的长度,供将来硬件发送数据成功后,中断服务程序更新统计信息
return 0;//通知协议栈发送成功,协议栈将会将skb从系统链表中摘除,不再重传该skb
10、数据发送超时处理cs8900_tx_timeout (struct net_device *ndev)
struct cs8900_priv *priv = netdev_priv(ndev);
//更新统计记录
priv->stats.tx_errors++;
priv->stats.tx_heartbeat_errors++;
//通知协议栈可重新提交发送数据
netif_wake_queue (ndev);
11、中断处理irqreturn_t cs8900_interrupt (int irq,void *id)
读取硬件的中断类型指示寄存器,判定中断类型。若是发送成功中断则更新统计信息后netif_wakeup_queue;若是接收成功中断则调用接收函数cs8900_receive 。
while ((status = cs8900_read (ndev, PP_ISQ))) {
switch (RegNum (status)) {
case TxEvent:
if (RegContent (status) & TxOK) {
priv->stats.tx_packets++;
priv->stats.tx_bytes += priv->txlen;
netif_wake_queue (ndev);
break;
}
case RxEvent:
cs8900_receive (ndev);
break;
}
}
12、数据接收处理cs8900_receive (struct net_device *ndev)
status = cs8900_read (ndev,PP_RxStatus);
length = cs8900_read (ndev,PP_RxLength);//从硬件获知数据长度
if (!(status & RxOK)) {
priv->stats.rx_errors++;
return;
}
skb = dev_alloc_skb (length + 4);//kmalloc skb
skb->dev = ndev;
skb_reserve (skb,2);//因mac头长度为14,+2可保证ip头16字节对齐,方便协议栈处理skb
cs8900_frame_read (ndev,skb,length);//从硬件读取数据,填充skb
skb->protocol = eth_type_trans (skb,ndev);//设置正确的skb->mac\ pkt_type\protocol,去掉mac头
netif_rx (skb);//向协议栈提交skb
13、改变接收模式cs8900_set_receive_mode (struct net_device *ndev)
if ((ndev->flags & IFF_PROMISC))
cs8900_set (ndev,PP_RxCTL,PromiscuousA);
else
cs8900_clear (ndev,PP_RxCTL,PromiscuousA);
if ((ndev->flags & IFF_ALLMULTI) && ndev->mc_list)
cs8900_set (ndev,PP_RxCTL,MulticastA);
else
cs8900_clear (ndev,PP_RxCTL,MulticastA);
14、获取统计信息cs8900_get_stats(struct net_device *ndev)
struct cs8900_priv *priv = netdev_priv(ndev);
struct net_device_stats *stats = &priv->stats;
return stats;
15、改变mac地址int (*set_mac_address)(struct net_device *dev, void *addr)
二、驱动中关于cs8900硬件操作的探讨
一中的内容大多数与“网络设备驱动基础”一文中的内容一致,都比较容易理解。但其中有关硬件的操作,则是与cs8900网卡芯片紧密相关的,要理解与硬件相关的代码,必须了解如何访问和操纵硬件,这要求全面了解网卡芯片的操控机制及各个硬件寄存器的地址与访问方式(这需要阅读网卡芯片数据手册),此外还需要看懂网卡芯片与CPU的硬件连线图。下面对这个问题,予以简要说明。
(画图)cs8900提供2类寄存器:可以用I/O内存地址直接访问的外部寄存器8个;不能用I/O内存地址直接访问,而必须通过外部寄存器间接访问的内部寄存器若干。
1、网卡寄存器的I/O内存地址范围为何是0x19000300 — 0x19000310?
查看cs8900芯片与CPU的硬件连线图可知1、2、3,再加上4,可得网卡寄存器的I/O内存地址的首地址是0x19000300,再由5可知网卡寄存器的I/O内存地址的末地址是0x19000310
2、如何访问内部寄存器
每个内部寄存器都有一个编号。要想访问某个内部寄存器,先将内部寄存器编号写入外部寄存器PP_Address(地址:0x0a),再读取(或写入)外部寄存器PP_Data(地址:0x0c)即可。例如:
120 static inline u16 cs8900_read (struct net_device *ndev,u16 reg)
121 {
122 outw (reg,ndev->base_addr + PP_Address);
123 return (inw (ndev->base_addr + PP_Data));
124 }
125
126 static inline void cs8900_write (struct net_device *ndev,u16 reg,u16 value)
127 {
128 outw (reg,ndev->base_addr + PP_Address);
129 outw (value,ndev->base_addr + PP_Data);
130 }
131
132 static inline void cs8900_set (struct net_device *ndev,u16 reg,u16 value)
133 {
134 cs8900_write (ndev,reg,cs8900_read (ndev,reg) | value);
135 }
136
137 static inline void cs8900_clear (struct net_device *ndev,u16 reg,u16 value)
138 {
139 cs8900_write (ndev,reg,cs8900_read (ndev,reg) & ~value);
140 }
3、驱动的各个功能函数中操控硬件的代码解析
1)cs8900_probe中的硬件操控代码
547 if ((value = cs8900_read (ndev,PP_ProductID)) != EISA_REG_CODE) {
548 printk (KERN_ERR "%s: incorrect signature 0x%.4x\n",ndev->name,value);
549 return -ENXIO;
550 }
554 if (VERSION (value) != CS8900A) {
555 printk (KERN_ERR "%s: unknown chip version 0x%.8x\n",ndev->name,VERSION (value));
556 return -ENXIO;
557 }
559 cs8900_write (ndev,PP_IntNum,0);
565 for (i = 0; i < ETH_ALEN; i += 2)
566 cs8900_write (ndev,PP_IA + i,ndev->dev_addr[i] | (ndev->dev_addr[i + 1] << 8));
2)、打开网卡设备cs8900_open(struct net_device *ndev) 中的硬件操控代码
如果以上针对硬件的描述你理解不了的话,哈哈,你有空的时候就应该去了解一下ISO的OSI模型的数据链路层的相关知识了。
391 static int cs8900_open(struct net_device *ndev)
392 {
395 /* in case of ifconfig modify mac address */
397 for (i = 0; i < ETH_ALEN; i += 2)
398 cs8900_write (ndev,PP_IA + i,ndev->dev_addr[i] | (ndev->dev_addr[i + 1] << 8));
400 /* enable the ethernet controller */
401 cs8900_set (ndev,PP_RxCFG,RxOKiE | BufferCRC | CRCerroriE | RuntiE | ExtradataiE);
402 cs8900_set (ndev,PP_RxCTL,RxOKA | IndividualA | BroadcastA);
403 cs8900_set (ndev,PP_TxCFG,TxOKiE | Out_of_windowiE | JabberiE);
404 cs8900_set (ndev,PP_BufCFG,Rdy4TxiE | RxMissiE | TxUnderruniE | TxColOvfiE | MissOvfloiE);
405 cs8900_set (ndev,PP_LineCTL,SerRxON | SerTxON);
406 cs8900_set (ndev,PP_BusCTL,EnableRQ);
412 udelay(200);
423 }
验证用户可以用ifconfig改变网卡的运行时MAC地址:
开发板 | Linux机器 |
# ifconfig # ping 192.168.2.11 |
dennis@dennis-desktop:/$ arp -a |
# ifconfig eth0 down # ping 192.168.2.11 |
dennis@dennis-desktop:/$ arp -a |
3)、关闭网卡设备cs8900_close(struct net_device *ndev) 中的硬件操控代码
425 static int cs8900_close(struct net_device *ndev)
426 {
428 /* disable ethernet controller */
429 cs8900_write (ndev,PP_BusCTL,0); //禁止产生中断
430 cs8900_write (ndev,PP_TestCTL,0);
431 cs8900_write (ndev,PP_SelfCTL,0);
432 cs8900_write (ndev,PP_LineCTL,0); //禁止发送和接收
433 cs8900_write (ndev,PP_BufCFG,0);
434 cs8900_write (ndev,PP_TxCFG,0);
435 cs8900_write (ndev,PP_RxCTL,0);
436 cs8900_write (ndev,PP_RxCFG,0);
443 }
4)、发送数据函数中的硬件操控代码
264 static int cs8900_start_xmit (struct sk_buff *skb,struct net_device *ndev)
265 {
273 cs8900_write (ndev,PP_TxCMD,TxStart (After5));
274 cs8900_write (ndev,PP_TxLength,skb->len);
276 status = cs8900_read (ndev,PP_BusST);
278 if ((status & TxBidErr)) {
281 priv->stats.tx_errors++;
282 priv->stats.tx_aborted_errors++;
283 priv->txlen = 0;
284 return 1;
285 }
287 if (!(status & Rdy4TxNOW)) {
290 priv->stats.tx_errors++;
291 priv->txlen = 0;
292 /* FIXME: store skb and send it in interrupt handler */
293 return 1;
294 }
296 cs8900_frame_write (ndev,skb);
310 }
假设要发送的数据长度为125字节,则下面的函数cs8900_frame_write通过向0x19000300这个I/O内存地址连续写入63个半字来完成将要发送的数据(125个有效的字节+1个无效的字节)提交给硬件芯片。哈哈,到此为止,我驱动的任务就完成了。硬件小子,下面就看你的了!
147 static inline void cs8900_frame_write (struct net_device *ndev,struct sk_buff *skb)
148 {
149 outsw (ndev->base_addr,skb->data,(skb->len + 1) / 2);
150 }
5)中断处理irqreturn_t cs8900_interrupt (int irq,void *id)中的硬件操控代码
312 static irqreturn_t cs8900_interrupt (int irq,void *id)
313 {
328 while ((status = cs8900_read (ndev, PP_ISQ))) {
331 switch (RegNum (status)) {
332 case RxEvent:
333 cs8900_receive (ndev);
334 break;
336 case TxEvent:
337 priv->stats.collisions += ColCount (cs8900_read (ndev,PP_TxCOL));
338 if (!(RegContent (status) & TxOK)) {
339 priv->stats.tx_errors++;
340 if ((RegContent (status) & Out_of_window)) priv->stats.tx_window_errors++;
341 if ((RegContent (status) & Jabber)) priv->stats.tx_aborted_errors++;
342 break;
343 } else if (priv->txlen) {
344 priv->stats.tx_packets++;
345 priv->stats.tx_bytes += priv->txlen;
346 }
347 priv->txlen = 0;
348 netif_wake_queue (ndev);
349 break;
351 case BufEvent:
352 if ((RegContent (status) & RxMiss)) {
353 u16 missed = MissCount (cs8900_read (ndev,PP_RxMISS));
354 priv->stats.rx_errors += missed;
355 priv->stats.rx_missed_errors += missed;
356 }
357 if ((RegContent (status) & TxUnderrun)) {
358 priv->stats.tx_errors++;
359 priv->stats.tx_fifo_errors++;
361 priv->txlen = 0;
362 netif_wake_queue (ndev);
363 }
364 /* FIXME: if Rdy4Tx, transmit last sent packet (if any) */
365 break;
367 case TxCOL:
368 priv->stats.collisions += ColCount (cs8900_read (ndev,PP_TxCOL));
369 break;
371 case RxMISS:
372 status = MissCount (cs8900_read (ndev,PP_RxMISS));
373 priv->stats.rx_errors += status;
374 priv->stats.rx_missed_errors += status;
375 break;
376 }
377 }
379 }
6)数据接收处理cs8900_receive (struct net_device *ndev) 中的硬件操控代码
224 static void cs8900_receive (struct net_device *ndev)
225 {
231 status = cs8900_read (ndev,PP_RxStatus);
232 length = cs8900_read (ndev,PP_RxLength);
234 if (!(status & RxOK)) {
235 priv->stats.rx_errors++;
236 if ((status & (Runt | Extradata))) priv->stats.rx_length_errors++;
237 if ((status & CRCerror)) priv->stats.rx_crc_errors++;
238 return;
239 }
249 cs8900_frame_read (ndev,skb,length);
262 }
假设硬件芯片接收到的数据长度为125字节,则下面的函数cs8900_frame_read通过从0x19000300这个I/O内存地址连续读取63个半字来完成将接收到的数据(125个有效的字节+1个无效的字节)copy到skb。可见地址0x19000300这个寄存器实际上是硬件芯片内部帧缓存的外部读写窗口。
142 static inline void cs8900_frame_read (struct net_device *ndev,struct sk_buff *skb,u16 length)
143 {
144 insw (ndev->base_addr,skb_put (skb,length),(length + 1) / 2);
145 }
7)改变接收模式cs8900_set_receive_mode (struct net_device *ndev) 中的硬件操控代码
453 static void cs8900_set_receive_mode (struct net_device *ndev)
454 {
455 if ((ndev->flags & IFF_PROMISC))
456 cs8900_set (ndev,PP_RxCTL,PromiscuousA);
457 else
458 cs8900_clear (ndev,PP_RxCTL,PromiscuousA);
460 if ((ndev->flags & IFF_ALLMULTI) && ndev->mc_list)
461 cs8900_set (ndev,PP_RxCTL,MulticastA);
462 else
463 cs8900_clear (ndev,PP_RxCTL,MulticastA);
464 }
验证用户可以用ifconfig使网卡处于混杂模式:
去掉255、256行(位于函数cs8900_receive中)的注释,以显示接收到的数据包的类型。
#define PACKET_HOST 0 /* To us */
#define PACKET_BROADCAST 1 /* To all */
#define PACKET_MULTICAST 2 /* To group */
#define PACKET_OTHERHOST 3 /* To someone else */
255 if (((skb->pkt_type) == PACKET_HOST) || ((skb->pkt_type) == PACKET_OTHERHOST))
256 printk("received a frame which package type is %d\n", skb->pkt_type);
开发板 | Linux机器 |
# ifconfig eth0 192.168.2.17 |
|
dennis@dennis-desktop:/$ ping -c 1 192.168.2.17 |
|
# received a frame which package type is 0 | |
dennis@dennis-desktop:/$ sudo arp -s 192.168.2.18 08:00:3E:26:0A:59
dennis@dennis-desktop:/$ ping -c 1 192.168.2.18 — 192.168.2.18 ping statistics — |
|
received a frame which package type is 3 | |
# ifconfig eth0 -promisc |
|
dennis@dennis-desktop:/$ ping -c 1 192.168.2.17 |
|
# received a frame which package type is 0 | |
dennis@dennis-desktop:/$ ping -c 1 192.168.2.18 — 192.168.2.18 ping statistics — |
|
没有任何显示 |
4、中断服务程序为何是while循环?
5、中断为何是EINT9?为什么是上升沿触发?
76 .start = IRQ_EINT9,
414 set_irq_type(ndev->irq, IRQ_TYPE_EDGE_RISING);
5、为什么要设置内存控制器?
500 __raw_writel(0x2211d110,S3C2410_BWSCON);//!!
501 __raw_writel(0x1f7c,S3C2410_BANKCON3);//!!
答:cs8900的I/O内存地址占用的是BANK3的地址空间
网络设备驱动框架总结:
一、体验网卡驱动
1、下载虚拟网卡驱动源码(单击下载)后,执行make得到snull.ko,加载驱动 sudo insmod snull.ko
2、分别配置2张网卡(sn0和sn1)的ip地址
dennis@dennis-desktop:/work/studydriver/snull$ sudo ifconfig sn0 192.168.140.1
dennis@dennis-desktop:/work/studydriver/snull$ sudo ifconfig sn1 192.168.141.2
3、测试2张网卡之间的通信
dennis@dennis-desktop:/work/studydriver/snull$ ping 192.168.140.2
PING 192.168.140.2 (192.168.140.2) 56(84) bytes of data.
64 bytes from 192.168.140.2: icmp_seq=1 ttl=64 time=0.167 ms
64 bytes from 192.168.140.2: icmp_seq=2 ttl=64 time=0.112 ms
64 bytes from 192.168.140.2: icmp_seq=3 ttl=64 time=0.111 ms
64 bytes from 192.168.140.2: icmp_seq=4 ttl=64 time=0.139 ms
— 192.168.140.2 ping statistics —
4 packets transmitted, 4 received, 0% packet loss, time 3000ms
rtt min/avg/max/mdev = 0.111/0.132/0.167/0.024 ms
注:也许你感觉2中ip地址的分配和3中的ping的目标有些奇怪,甚至有些难以理解。这是由于我们要测试网卡sn0是否能ping通网卡sn1,而sn0和sn1是本机的2张网卡,如果将它们的ip地址配在同一网段,则测试时数据包根本不会外送。为解决测试的问题,scull驱动对外发数据包作了一些额外处理,将目标ip的第3段加1(即:将目标ip从192.168.140.2改为192.168.141.2),将源ip的第3段加1(即:将源ip从192.168.140.1改为192.168.141.1)。详情请参见《Linux Device Driver》17.1节。
二、网卡驱动的基本知识——2个结构体和5个函数
1、结构体 struct net_device
网络设备的注册方式与字符和块设备不同的。网络设备没有主次设备号,驱动为每个刚刚探测到的网络设备(或称为接口)在一个全局的网络设备链表里插入一个节点,该节点就代表1个网络设备,它是struct net_device类型的结构体。
1)、net_device结构体的内容(字段)很多,主要用于操作系统和驱动程序操控和查询网卡
2) 、net_device结构体的生成与初始化
struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *))
3)、net_device结构体的注册(插入全局的网络设备链表)
在对net_device结构体完成初始化后,传递这个结构给 int register_netdev(struct net_device *dev),以完成注册
4)、将net_device结构体从内核中注销
void unregister_netdev(struct net_device *dev)
5)、net_device结构体的销毁
void free_netdev(struct net_device *dev) 归还 net_device 结构给内核
2、 网络接口的打开与关闭
int snull_open(struct net_device *dev) {
memcpy(dev->dev_addr, "SNUL0", ETH_ALEN);
netif_start_queue(dev);
}
int snull_release(struct net_device *dev) {
netif_stop_queue(dev);
}
3、数据包发送
4、中断处理
5、 数据包接收
数据包接收有2种模式:中断驱动和轮询。这里只介绍第中断驱动,在这种模式下,数据包接收函数是由中断处理程序来调用的。接收函数完成功能的流程如下:
1)分配一个缓存区来保存数据包.
2)一旦有一个有效的 skb 指针, 通过调用 memcpy, 报文数据被拷贝到缓存区
3)skb的dev、protocol、pkt_type 成员必须在缓存向上传递前赋值,使协议栈知道数据包的一些信息。以太网支持代码输出一个辅助函数 eth_type_trans(skb, dev) 来完成这个工作
4)指出IP校验和要如何进行,即:对skb->ip_summed赋值。其取值有3种:
5)驱动更新它的统计计数(存放于私有数据区)来记录收到一个报文。统计结构由几个成员组成,最重要的是:
6)调用int netif_rx(struct sk_buff *skb)执行最后的接受数据包工作, 它递交 socket 缓存给上层。netif_rx 返回一个整数:
6、结构体 struct sk_buff及相关内核API
1)结构体 struct sk_buff的组成
可用缓存空间是 skb->end – skb->head, 有效数据(即:数据包)的空间是 skb->tail – skb->data
truesize:表示缓存区的整体长度,置为sizeof(struct sk_buff)加上传入alloc_skb()函数的长度(或dev_alloc_skb分配的数据缓存区长度)
2)分配Socket 缓存的API(对应的释放API用kfree替换alloc即可)
分配一个缓存区. alloc_skb 函数分配一个缓存并且将 skb->data 和 skb->tail 都初始化成 skb->head.
dev_alloc_skb 函数是使用 GFP_ATOMIC 优先级调用 alloc_skb 的快捷方法, 并且在 skb->head 和 skb->data 之间保留了一些空间. 这个数据空间用在网络层之间的优化, 驱动不要动它
3)操纵Socket 缓存的API
更新 sk_buff 结构中的 tail 和 len 成员; 它们用来增加数据到缓存的结尾, 每个函数的返回值是 skb->tail 的前一个值(换句话说, 它指向刚刚创建的数据空间). 两个函数的区别在于 skb_put 检查以确认数据适合缓存, 而 __skb_put 省略这个检查.
递减 skb->data 和递增 skb->len 的函数. 它们与 skb_put 相似, 除了数据是添加到报文的开始而不是结尾. 返回值指向刚刚创建的数据空间. 这些函数用来在发送报文之前添加一个硬件头部. 又一次, __skb_push 不同在它不检查空间是否足够
将data 和 tail的值加上len.
三、snull网卡驱动代码分析
1、设备生成与注册
688 int snull_init_module(void)
689 {
694 /* Allocate the devices */
695 snull_devs[0] = alloc_netdev(sizeof(struct snull_priv), "sn%d",
696 snull_init);
697 snull_devs[1] = alloc_netdev(sizeof(struct snull_priv), "sn%d",
698 snull_init);
703 for (i = 0; i < 2; i++)
704 if ((result = register_netdev(snull_devs[i])))
705 printk("snull: error %i registering device \"%s\"\n",
706 result, snull_devs[i]->name);
707 else
708 ret = 0;
714 }
在模块初始化函数中,695、697行初始化net_device结构体。struct snull_priv结构体用来存放一些私有数据,例如:统计数据。函数snull_init则设置net_device的各个重要字段。704行并将代表网卡的net_device结构体注册进操作系统。至此,网卡就存在于系统中,可以被使用了。
模块的卸载函数完成相反的功能——将net_device结构体从内核中注销,以及销毁net_device结构体
617 void snull_init(struct net_device *dev)
618 {
634 dev->open = snull_open;
635 dev->stop = snull_release;
636 dev->set_config = snull_config;
637 dev->hard_start_xmit = snull_tx;
638 dev->do_ioctl = snull_ioctl;
639 dev->get_stats = snull_stats;
640 dev->change_mtu = snull_change_mtu;
641 dev->rebuild_header = snull_rebuild_header;
642 dev->hard_header = snull_header;
643 dev->tx_timeout = snull_tx_timeout;
644 dev->watchdog_timeo = timeout; //timeout为5个jiffies
663 }
snull_init函数将net_device结构体的重要字段初始化为了驱动中实现的各个功能函数,因此
下面就来分析这几个功能函数。
2、设备打开、关闭与查询
scull_open完成:填写struct net_device结构体的dev_addr字段(mac地址);启动传输队列netif_start_queue(dev)
scull_release完成:停止传输队列netif_stop_queue(dev)
scull_stats完成:将网卡的统计数据返回给操作系统。之后操作系统将该信息返回给应用程序(例如:ifconfig)
78 struct snull_priv {
79 struct net_device_stats stats;
86 struct sk_buff *skb;
87 spinlock_t lock;
88 };
557 struct net_device_stats *snull_stats(struct net_device *dev)
558 {
559 struct snull_priv *priv = netdev_priv(dev);
560 return &priv->stats;
561 }
3、数据的发送
需要发送数据时,协议栈会调用dev->hard_start_xmit。传入的第1个参数是socket缓存结构体(通过它可以找到需要发送的数据内容、长度等相关信息)的指针;第2个参数是对应于该网卡的net_device结构体。
501 int snull_tx(struct sk_buff *skb, struct net_device *dev)
502 {
503 int len;
504 char *data, shortpkt[ETH_ZLEN];
505 struct snull_priv *priv = netdev_priv(dev);
509 data = skb->data;
510 len = skb->len;
511 if (len < ETH_ZLEN) {
512 memset(shortpkt, 0, ETH_ZLEN);
513 memcpy(shortpkt, skb->data, skb->len);
514 len = ETH_ZLEN;
515 data = shortpkt;
516 }
517 dev->trans_start = jiffies; /* save the timestamp */
519 /* Remember the skb, so we can free it at interrupt time */
520 priv->skb = skb;
522 /* actual deliver of data is device-specific, and not shown here */
523 snull_hw_tx(data, len, dev);
525 return 0; /* Our simple device can not fail */
526 }
如果协议栈检测到发送超时(如果协议栈被netif_stop_queue(dev)通知暂停发送后,超过dev->watchdog_timeo个jiffies仍然没有被netif_wake_queue(dev)通知恢复传送,则协议栈认为在网卡dev上发生了发送超时),将会调用dev->tx_timeout。
531 void snull_tx_timeout (struct net_device *dev)
532 {
533 struct snull_priv *priv = netdev_priv(dev);
540 priv->stats.tx_errors++;
541 netif_wake_queue(dev);
543 }
4、中断处理程序
327 static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
328 {
329 int statusword;
330 struct snull_priv *priv;
331 struct snull_packet *pkt = NULL;
332 /*
333 * As usual, check the "device" pointer to be sure it is
334 * really interrupting.
335 * Then assign "struct device *dev"
336 */
337 struct net_device *dev = (struct net_device *)dev_id;
348 /* retrieve statusword: real netdevices use I/O instructions */
349 statusword = priv->status;
350 priv->status = 0;
351 if (statusword & SNULL_RX_INTR) {
352 /* send it to snull_rx for handling */
353 pkt = priv->rx_queue;
354 if (pkt) {
355 priv->rx_queue = pkt->next;
356 snull_rx(dev, pkt);
357 }
358 }
359 if (statusword & SNULL_TX_INTR) {
360 /* a transmission is over: free the skb */
361 priv->stats.tx_packets++;
362 priv->stats.tx_bytes += priv->tx_packetlen;
363 dev_kfree_skb(priv->skb);
364 }
368 if (pkt) snull_release_buffer(pkt); /* Do this outside the lock! */
370 }
5、数据的接收
当硬件成功接收数据后会产生中断,中断处理程序在判明是接收中断后会调用数据包接收程序,并将struct net_device结构体的指针作为参数传递
249 void snull_rx(struct net_device *dev, struct snull_packet *pkt)
250 {
251 struct sk_buff *skb;
252 struct snull_priv *priv = netdev_priv(dev);
254 /*
255 * The packet has been retrieved from the transmission
256 * medium. Build an skb around it, so upper layers can handle it
257 */
258 skb = dev_alloc_skb(pkt->datalen + 2);
259 if (!skb) {
262 priv->stats.rx_dropped++;
263 goto out;
264 }
265 skb_reserve(skb, 2); /* align IP on 16B boundary */
266 memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
268 /* Write metadata, and then pass to the receive level */
269 skb->dev = dev;
270 skb->protocol = eth_type_trans(skb, dev);
271 skb->ip_summed = CHECKSUM_UNNECESSARY; /* don’t check it */
272 priv->stats.rx_packets++;
273 priv->stats.rx_bytes += pkt->datalen;
274 netif_rx(skb);
275 out:
276 return;
277 }
总结:网卡驱动的编写,主要内容是
一、裸机程序中的中断编程与有操作系统下的中断编程的区别
一、Linux中断处理系统的架构(详情参阅《嵌入式linux应用开发完全手册》P401-P403)
二、关于共享中断的说明
三、共享中断实例
dummyisr.c如下:
11 static irqreturn_t dummy_isr(int irq, void *dev_id)
12 {
13 static int haveint = 0;
14 if (haveint == 0) {
15 printk(KERN_NOTICE "dummy_isr will return IRQ_NONE");
16 haveint = 1;
17 return IRQ_RETVAL(IRQ_NONE);
18 } else {
19 printk(KERN_NOTICE "dummy_isr will return IRQ_HANDLED");
20 haveint = 0;
21 return IRQ_RETVAL(IRQ_HANDLED);
22 }
23 }
24
25 struct cdev dummy_cdev;
26 static int __init my_init(void)
27 {
28 int err;
29 err = request_irq(IRQ_EINT19, dummy_isr, IRQF_TRIGGER_FALLING | IRQF_SHARED, "dummy_isr", (void *)&dummy_cdev);
30 if (err) {
31 printk(KERN_WARNING "request_irq IRQ_EINT19 for dummy_isr failed, error number is %d\n", err);
32 } else {
33 printk(KERN_NOTICE "register dummy_isr succeed\n");
34 }
35 return 0;
36 }
37 static void __exit my_fini(void)
38 {
39 free_irq(IRQ_EINT19, (void *)&dummy_cdev);
40 printk(KERN_NOTICE "unregister dummy_isr succeed\n");
41 }
s3c24xx_buttons_v2.5.c如下:
63 {IRQ_EINT19, IRQF_TRIGGER_FALLING | IRQF_SHARED, "KEY1"}, /* K1 */
175 for (i = 0; i < BUTTON_NUM; i++) {
177 err = request_irq(buttons_dev.button_irqs[i].irqno, buttons_interrupt,
178 buttons_dev.button_irqs[i].flags, buttons_dev.button_irqs[i].name,
179 (void *)&buttons_dev);
s3c24xx_buttons_v2.5.c与dummyisr.c注册了共享中断IRQ_EINT19,且均遵循了二中的1、2两点。(请务必注意在dummyisr中request_irq时除了要使用IRQF_SHARED标志外,还要使用IRQF_TRIGGER_FALLING标志,因为在button驱动中采用了该标志,二者必须一致,否则在注册第2个中断时总是不能成功,会出现如下错误。
# insmod s3c24xx_buttons.ko
buttons initialized
# ./button_test &
# open success
输入回车
# insmod dummyisr.ko
request_irq IRQ_EINT19 for dummy_isr failed, error number is -16
一、区分和使用中断顶半部与底半部的原因
二、tasklet机制
1、原理
2、tasklet使用的内核API
三、Workqueue机制
1、原理
# ps
PID Uid VSZ Stat Command
1 root 3092 S init
2 root SW< [kthreadd]
3 root SWN [ksoftirqd/0] //N表示低优先级, <表示高优先级
4 root SW< [events/0]
5 root SW< [khelper]
41 root SW< [kblockd/0]
42 root SW< [ksuspend_usbd]
45 root SW< [khubd]
47 root SW< [kseriod]
59 root SW [pdflush]
60 root SW [pdflush]
61 root SW< [kswapd0]
62 root SW< [aio/0]
177 root SW< [mtdblockd]
226 root SWN [jffs2_gcd_mtd2]
248 root 1952 S /usr/sbin/vsftpd
249 root 3096 S -sh
250 root 3092 S /usr/sbin/telnetd
253 www 3092 S /usr/sbin/httpd -h /www -u www
256 root 3096 R ps
2、Tasklet与Workqueue的区别
Tasklet |
Workqueue |
处于atomic context,不能sleep |
不处于atomic context,可以sleep |
运行在调度它们的同一个 CPU 上 |
默认运行在调度它们的同一个 CPU 上 |
不能指定确定时间进行调度 |
不能指定确定时间进行调度或指定至少延迟一个确定的时间后调度 |
只能提交给ksoftirqd/0 |
可以提交给events/0,也可以提交给自定义的workqueue |
tasklet函数带参数 |
work函数不带参数 |
3、Workqueue API
四、tasklet实例(s3c24xx_buttons_v3.5.c)
1、实例功能描述
按下K1(K2、K3、K4)键时,LED1(LED2、LED3、LED4)闪烁1次。由于LED灯闪烁这个操作比较耗时(需3秒),因此不能在中断顶半部中执行,所以由顶半部调度它在中断底半部(本实例在tasklet)中运行。
2、主要实现代码分析与说明
314 for (i = 0; i < BUTTON_NUM; i++) /* intialize tasklets */
315 tasklet_init(&(buttons_dev.button_tasklets[i]), button_do_tasklet, i);
模块初始化时,完成对tasklet的初始化
150 tasklet_schedule(&(buttons_dev.button_tasklets[butno-1])); /* schedule tasklet */
在中断处理程序(顶半部)中,调度tasklet(底半部)在稍后安全的时间(非顶半部时间)运行
82 static void button_do_tasklet(unsigned long data)
83 {
84 PDEBUG("enter tasklet, current pid is %d, button number is %li\n", current->pid, data+1);
85 switch (data) {
86 case 0:
87 ledon(0);
88 mdelay(1000);
89 ledoff(0);
90 mdelay(1000);
91 ledon(0);
92 break;
93 case 1:
116 }
117 PDEBUG("exit tasklet, current pid is %d, button number is %li\n", current->pid, data+1);
118 }
在tasklet中调用mdelay完成了毫秒级的延时,由于mdelay是忙等待,而不是睡眠,所以可以在tasklet(中断上下文)中使用。
在tasklet中,调用ledon和ledoff完成亮灯和灭灯的操作。ledon和ledoff并非是在本程序中定义的,而是在其它驱动内核模块中定义的,所以在编译本模块时会有警告,告知这2个函数在本模块中尚未定义。
dennis@dennis-desktop:/work/studydriver/buttons$ make
make -C /work/system/linux-2.6.22.6/ M=/work/studydriver/buttons LDDINC=/work/studydriver/buttons/../include modules
make[1]: Entering directory `/work/system/linux-2.6.22.6′
CC [M] /work/studydriver/buttons/s3c24xx_buttons.o
Building modules, stage 2.
MODPOST 1 modules
WARNING: "ledoff" [/work/studydriver/buttons/s3c24xx_buttons.ko] undefined!
WARNING: "ledon" [/work/studydriver/buttons/s3c24xx_buttons.ko] undefined!
CC /work/studydriver/buttons/s3c24xx_buttons.mod.o
LD [M] /work/studydriver/buttons/s3c24xx_buttons.ko
make[1]: Leaving directory `/work/system/linux-2.6.22.6′
也正是由于这个原因,必须在加载本模块之前,先加载ledon和ledoff被定义的模块
# insmod s3c24xx_buttons.ko
s3c24xx_buttons: Unknown symbol ledoff
s3c24xx_buttons: Unknown symbol ledon
insmod: cannot insert ‘s3c24xx_buttons.ko’: Unknown symbol in module (-1): No such file or directory
# insmod leds
leds initialized
# insmod s3c24xx_buttons.ko
buttons initialized
而且在ledon和ledoff被定义的模块(ledsv2.c)中,还必须用内核定义的EXPORT_SYMBOL宏将这2个函数导出到内核全局符号表,以供别的模块使用。
93 EXPORT_SYMBOL(ledon);
100 EXPORT_SYMBOL(ledoff);
由于这2个函数是被导出到内核全局符号表的,因此不能与内核全局符号表中的其它函数(内核API)重名,否则就造成了内核名字空间污染。
3、运行结果分析
1 # ./button_test
2 open success
3 buttons: who(63) enter interrupt
4 buttons: quit interrupt
5 buttons: who(63) enter interrupt
6 buttons: enter tasklet, current pid is 0, button number is 1
7 buttons: who(63) enter interrupt
8 buttons: who(63) enter interrupt
9 buttons: who(63) enter interrupt
10 buttons: who(63) enter interrupt
11 buttons: who(63) enter interrupt
延迟时长约2秒
12 buttons: exit tasklet, current pid is 0, button number is 1
13 read buttons successfully, begin print the result:
14 K1 has been pressed 1 times!
五、workqueue实例(s3c24xx_buttons_v4.5.c)
1、实例功能描述
按下K1(K2、K3、K4)键时,LED1(LED2、LED3、LED4)闪烁1次。由于LED灯闪烁这个操作比较耗时(需3秒),因此不能在中断顶半部中执行,所以由顶半部调度它在中断底半部(本实例在workqueue)中运行。
2、主要实现代码分析与说明
397 INIT_WORK(&(buttons_dev.buttons_workqueue), button_do_workqueue); /* initialize workqueue */
模块初始化时,完成对workqueue的初始化
229 buttons_dev.dkeynum = butno – 1;
230 schedule_work(&(buttons_dev.buttons_workqueue)); /* schedule workqueue */
在中断处理程序(顶半部)中,调度workqueue(底半部)在稍后安全的时间(非顶半部时间)运行
88 static void button_do_workqueue(struct work_struct * wq)
89 {
90 PDEBUG("enter workqueue, current pid is %d, button number is %d\n", current->pid, buttons_dev.dkeynum + 1);
101 switch (buttons_dev.dkeynum) {
102 case 0:
103 ledon(0);
104 ssleep(3);
105 ledoff(0);
106 ssleep(3);
107 ledon(0);
108 break;
123 case 3:
124 ledon(3);
125 mdelay(3000);
126 ledoff(3);
127 mdelay(3000);
128 ledon(3);
129 break;
132 }
133 PDEBUG("exit workqueue, current pid is %d, button number is %d\n", current->pid, buttons_dev.dkeynum + 1);
134 }
在workqueue与tasklet一个重大区别就是可以睡眠,所以它可以调用mdelay进行忙等待,也可以安全地调用ssleep睡眠。
3、运行结果分析
1 # ./button_test
2 open success
3 按下K1键
4 buttons: who(63) enter interrupt, current pid is 0
5 buttons: in interrupt, CPSR = 60000093, SPSR = 80000013
6 buttons: who(63) quit interrupt, current pid is 0, button number is 1
7 buttons: enter workqueue, current pid is 4, button number is 1
8 buttons: in workqueue, CPSR = 60000013, SPSR = 60000013
9 read buttons successfully, begin print the result:
10 K1 has been pressed 1 times!
11 buttons: who(63) enter interrupt, current pid is 0, button number is 0
12 buttons: who(63) enter interrupt, current pid is 0, button number is 0
13 buttons: who(63) enter interrupt, current pid is 0, button number is 0
14 延迟时长约6秒
15 buttons: exit workqueue, current pid is 4, button number is 1
16 按下K4键
17 buttons: who(16) enter interrupt, current pid is 0
18 buttons: in interrupt, CPSR = 60000013, SPSR = 60000013
19 buttons: who(16) quit interrupt, current pid is 0, button number is 4
20 buttons: who(16) enter interrupt, current pid is 0
21 buttons: who(16) enter interrupt, current pid is 0
22 buttons: enter workqueue, current pid is 4, button number is 4
23 buttons: in workqueue, CPSR = 60000013, SPSR = 60000013
24 buttons: who(16) enter interrupt, current pid is 4
25 buttons: who(16) enter interrupt, current pid is 4
26 延迟时长约6秒
27 buttons: exit workqueue, current pid is 4, button number is 4
28 read buttons successfully, begin print the result:
29 K4 has been pressed 1 times!
75 .button_irqs = {
76 {IRQ_EINT19, IRQF_TRIGGER_FALLING|IRQF_DISABLED, "KEY1"}, /* K1 */
77 {IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /* K2 */
78 {IRQ_EINT2, IRQF_TRIGGER_FALLING, "KEY3"}, /* K3 */
79 {IRQ_EINT0, IRQF_TRIGGER_FALLING, "KEY4"} /* K4 */
80 },
不过为何处于SVC模式而不是IRQ模式有待阅读内核源码,可能是内核处理中断时为了能够中断嵌套进行了模式切换
4、验证在tasklet中不能睡眠
将第54行注释掉,从而使用tasklet。
54 //#define USEWORKQUEUE
136 static void button_do_tasklet(unsigned long data)
137 {
149 switch (data) {
150 case 0:
151 ledon(0);
152 mdelay(1000);
153 ledoff(0);
154 mdelay(1000);
155 ledon(0);
156 break;
171 case 3:
172 ledon(3);
173 mdelay(1000);
174 ledoff(3);
175 ssleep(1);
176 //mdelay(1000);
177 ledon(3);
178 break;
181 }
183 }
运行的结果如下,验证了在tasklet的确不能睡眠(有时间的话,就好好看看OOP的输出吧,也许你能发现一些内核的小秘密!):
# ./button_test
open success
按下K1键
buttons: who(63) enter interrupt, current pid is 0
buttons: in interrupt, CPSR = 60000093, SPSR = 93
buttons: who(63) quit interrupt, current pid is 0, button number is 1
buttons: who(63) enter interrupt, current pid is 0
buttons: enter tasklet, current pid is 0, button number is 1
buttons: in tasklet, CPSR = 60000013, SPSR = 60000013
buttons: who(63) enter interrupt, current pid is 0
buttons: who(63) enter interrupt, current pid is 0
buttons: who(63) enter interrupt, current pid is 0
buttons: who(63) enter interrupt, current pid is 0
buttons: who(63) enter interrupt, current pid is 0
buttons: exit tasklet, current pid is 0, button number is 1
read buttons successfully, begin print the result:
K1 has been pressed 1 times!
按下K4键
buttons: who(16) enter interrupt, current pid is 0
buttons: in interrupt, CPSR = 60000013, SPSR = 80000013
buttons: who(16) quit interrupt, current pid is 0, button number is 4
buttons: enter tasklet, current pid is 0, button number is 4
buttons: in tasklet, CPSR = 60000013, SPSR = 60000013
BUG: scheduling while atomic: swapper/0x00000100/0
[<c0028c90>] (dump_stack+0x0/0x14) from [<c0200cf0>] (schedule+0x50/0x750)
[<c0200ca0>] (schedule+0x0/0x750) from [<c0201dec>] (schedule_timeout+0x8c/0xbc)
[<c0201d60>] (schedule_timeout+0x0/0xbc) from [<c0201e68>] (schedule_timeout_uninterruptible+0x24/0x28)
r8:3001f48c r7:c02ce280 r6:00000004 r5:c0282000 r4:ffffffff
[<c0201e44>] (schedule_timeout_uninterruptible+0x0/0x28) from [<c00442f4>] (msleep+0x1c/0x28)
[<c00442d8>] (msleep+0x0/0x28) from [<bf00208c>] (button_do_tasklet+0x8c/0x1c8 [s3c24xx_buttons])
[<bf002000>] (button_do_tasklet+0x0/0x1c8 [s3c24xx_buttons]) from [<c00404c0>] (tasklet_action+0x88/0xdc)
r6:0000000a r5:c02ce2a4 r4:00000000
[<c0040438>] (tasklet_action+0x0/0xdc) from [<c0040000>] (__do_softirq+0x5c/0xc8)
r5:c02ce2e4 r4:00000001
[<c003ffa4>] (__do_softirq+0x0/0xc8) from [<c0040224>] (irq_exit+0x44/0x4c)
r7:00000000 r6:c02d2edc r5:c028b0a8 r4:00000010
[<c00401e0>] (irq_exit+0x0/0x4c) from [<c002404c>] (asm_do_IRQ+0x4c/0x60)
[<c0024000>] (asm_do_IRQ+0x0/0x60) from [<c0024a24>] (__irq_svc+0x24/0xa0)
Exception stack(0xc0283f54 to 0xc0283f9c)
3f40: 00000000 ffffffff f020000c
3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff
r7:c02dfb58 r6:00000001 r5:f0000000 r4:ffffffff
[<c0025974>] (default_idle+0x0/0x78) from [<c0025a34>] (cpu_idle+0x48/0x64)
[<c00259ec>] (cpu_idle+0x0/0x64) from [<c02006c4>] (rest_init+0x48/0x58)
r5:c02bc328 r4:c02d12d4
[<c020067c>] (rest_init+0x0/0x58) from [<c0008938>] (start_kernel+0x27c/0x2e4)
[<c00086bc>] (start_kernel+0x0/0x2e4) from [<30008030>] (0x30008030)
bad: scheduling from the idle thread!
[<c0028c90>] (dump_stack+0x0/0x14) from [<c0200d3c>] (schedule+0x9c/0x750)
[<c0200ca0>] (schedule+0x0/0x750) from [<c0201dec>] (schedule_timeout+0x8c/0xbc)
[<c0201d60>] (schedule_timeout+0x0/0xbc) from [<c0201e68>] (schedule_timeout_uninterruptible+0x24/0x28)
r8:3001f48c r7:c02ce280 r6:00000004 r5:c0282000 r4:ffffffff
[<c0201e44>] (schedule_timeout_uninterruptible+0x0/0x28) from [<c00442f4>] (msleep+0x1c/0x28)
[<c00442d8>] (msleep+0x0/0x28) from [<bf00208c>] (button_do_tasklet+0x8c/0x1c8 [s3c24xx_buttons])
[<bf002000>] (button_do_tasklet+0x0/0x1c8 [s3c24xx_buttons]) from [<c00404c0>] (tasklet_action+0x88/0xdc)
r6:0000000a r5:c02ce2a4 r4:00000000
[<c0040438>] (tasklet_action+0x0/0xdc) from [<c0040000>] (__do_softirq+0x5c/0xc8)
r5:c02ce2e4 r4:00000001
[<c003ffa4>] (__do_softirq+0x0/0xc8) from [<c0040224>] (irq_exit+0x44/0x4c)
r7:00000000 r6:c02d2edc r5:c028b0a8 r4:00000010
[<c00401e0>] (irq_exit+0x0/0x4c) from [<c002404c>] (asm_do_IRQ+0x4c/0x60)
[<c0024000>] (asm_do_IRQ+0x0/0x60) from [<c0024a24>] (__irq_svc+0x24/0xa0)
Exception stack(0xc0283f54 to 0xc0283f9c)
3f40: 00000000 ffffffff f020000c
3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff
r7:c02dfb58 r6:00000001 r5:f0000000 r4:ffffffff
[<c0025974>] (default_idle+0x0/0x78) from [<c0025a34>] (cpu_idle+0x48/0x64)
[<c00259ec>] (cpu_idle+0x0/0x64) from [<c02006c4>] (rest_init+0x48/0x58)
r5:c02bc328 r4:c02d12d4
[<c020067c>] (rest_init+0x0/0x58) from [<c0008938>] (start_kernel+0x27c/0x2e4)
[<c00086bc>] (start_kernel+0x0/0x2e4) from [<30008030>] (0x30008030)
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c0004000
[00000000] *pgd=00000000
Internal error: Oops: 17 [#1]
Modules linked in: s3c24xx_buttons leds
CPU: 0 Not tainted (2.6.22.6 #22)
PC is at dequeue_task+0xc/0x84
LR is at deactivate_task+0x34/0x40
pc : [<c0036b44>] lr : [<c0036ec8>] psr: 60000093
sp : c0283e14 ip : c0283e28 fp : c0283e24
r10: 004c4b18 r9 : 894f8c40 r8 : c0284ea0
r7 : c0283e74 r6 : 000000c9 r5 : c0282000 r4 : c0284ea0
r3 : 00000080 r2 : 00000080 r1 : 00000000 r0 : c0284ea0
Flags: nZCv IRQs off FIQs on Mode SVC_32 Segment kernel
Control: c000717f Table: 338c8000 DAC: 00000017
Process swapper (pid: 0, stack limit = 0xc0282258)
Stack: (0xc0283e14 to 0xc0284000)
3e00: c0284ea0 c0283e38 c0283e28
3e20: c0036ec8 c0036b48 004c4b18 c0283e70 c0283e3c c0200e50 c0036ea4 c0284edc
3e40: c0284fac 3b9aca00 ffff7c22 c0282000 000000c9 c0283e74 c028a4d0 41129200
3e60: 3001f458 c0283eac c0283e74 c0201dec c0200cb0 c02cee88 c02cee88 ffff7c22
3e80: c00440a8 c0284ea0 c02ce4a0 ffffffff c0282000 00000004 c02ce280 3001f48c
3ea0: c0283ebc c0283eb0 c0201e68 c0201d70 c0283ecc c0283ec0 c00442f4 c0201e54
3ec0: c0283ee8 c0283ed0 bf00208c c00442e8 00000000 c02ce2a4 0000000a c0283f00
3ee0: c0283eec c00404c0 bf002010 00000001 c02ce2e4 c0283f20 c0283f04 c0040000
3f00: c0040448 00000010 c028b0a8 c02d2edc 00000000 c0283f30 c0283f24 c0040224
3f20: c003ffb4 c0283f50 c0283f34 c002404c c00401f0 ffffffff f0000000 00000001
3f40: c02dfb58 c0283fa8 c0283f54 c0024a24 c0024010 00000000 ffffffff f020000c
3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff c0283fc0
3fa0: c0283fac c0025a34 c0025984 c02d12d4 c02bc328 c0283fd0 c0283fc4 c02006c4
3fc0: c00259fc c0283ff4 c0283fd4 c0008938 c020068c c0008324 c0020f28 c0007175
3fe0: c02bc7e4 c0285c9c 00000000 c0283ff8 30008030 c00086cc 00000000 00000000
Backtrace:
[<c0036b38>] (dequeue_task+0x0/0x84) from [<c0036ec8>] (deactivate_task+0x34/0x40)
r4:c0284ea0
[<c0036e94>] (deactivate_task+0x0/0x40) from [<c0200e50>] (schedule+0x1b0/0x750)
r4:004c4b18
[<c0200ca0>] (schedule+0x0/0x750) from [<c0201dec>] (schedule_timeout+0x8c/0xbc)
[<c0201d60>] (schedule_timeout+0x0/0xbc) from [<c0201e68>] (schedule_timeout_uninterruptible+0x24/0x28)
r8:3001f48c r7:c02ce280 r6:00000004 r5:c0282000 r4:ffffffff
[<c0201e44>] (schedule_timeout_uninterruptible+0x0/0x28) from [<c00442f4>] (msleep+0x1c/0x28)
[<c00442d8>] (msleep+0x0/0x28) from [<bf00208c>] (button_do_tasklet+0x8c/0x1c8 [s3c24xx_buttons])
[<bf002000>] (button_do_tasklet+0x0/0x1c8 [s3c24xx_buttons]) from [<c00404c0>] (tasklet_action+0x88/0xdc)
r6:0000000a r5:c02ce2a4 r4:00000000
[<c0040438>] (tasklet_action+0x0/0xdc) from [<c0040000>] (__do_softirq+0x5c/0xc8)
r5:c02ce2e4 r4:00000001
[<c003ffa4>] (__do_softirq+0x0/0xc8) from [<c0040224>] (irq_exit+0x44/0x4c)
r7:00000000 r6:c02d2edc r5:c028b0a8 r4:00000010
[<c00401e0>] (irq_exit+0x0/0x4c) from [<c002404c>] (asm_do_IRQ+0x4c/0x60)
[<c0024000>] (asm_do_IRQ+0x0/0x60) from [<c0024a24>] (__irq_svc+0x24/0xa0)
Exception stack(0xc0283f54 to 0xc0283f9c)
3f40: 00000000 ffffffff f020000c
3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff
r7:c02dfb58 r6:00000001 r5:f0000000 r4:ffffffff
[<c0025974>] (default_idle+0x0/0x78) from [<c0025a34>] (cpu_idle+0x48/0x64)
[<c00259ec>] (cpu_idle+0x0/0x64) from [<c02006c4>] (rest_init+0x48/0x58)
r5:c02bc328 r4:c02d12d4
[<c020067c>] (rest_init+0x0/0x58) from [<c0008938>] (start_kernel+0x27c/0x2e4)
[<c00086bc>] (start_kernel+0x0/0x2e4) from [<30008030>] (0x30008030)
Code: c02bcffc e1a0c00d e92dd810 e24cb004 (e5913000)
Kernel panic – not syncing: Fatal exception in interrupt
一、内核中如何记录时间
任何程序都需要时间控制,其主要目的是:
为达到这个目的,应用程序使用日历时间(年月日时分秒)或者自1970年1月1日零时零分零秒到当前的秒数来度量时间的流逝,但内核中需要更加有精度的时间度量,因此内核使用时钟嘀嗒来记录时间。时钟中断发生后内核内部时间计数器增加1(即:增加1个时钟嘀嗒),系统引导时为0,当前值为自上次系统引导以来的时钟滴答数,程序可通过内核定义的全局变量jiffies_64或jiffies来访问。真实硬件上每秒的嘀嗒数从 50 到 1200 不等 ,x86上默认为1000,而s3c2440上默认为200,出于统一编程接口的考虑,内核定义了一个宏HZ,它表示1秒钟的嘀嗒数,可供程序使用。
注意:32-位 平台上当 HZ 是 1000 时, 计数器只是每 50 天溢出一次, 必要时你的代码应当准备处理这个事件
它们分别在时间a在时间b之后、之前、之后或相等、之前或相等的时候为真,反之为假
二、内核定时器
1、概述
2、定时器 API
三、内核定时器与内核时间的应用案例——按键消抖
在“驱动程序中的中断处理”一文的实验中,如果将编译s3c2440_buttons_v2.5.c改为编译s3c2440_buttons_v1.c(rm s3c2440_buttons.c; ln –s s3c2440_buttons_v1.c s3c2440_buttons.c; make),你可能注意到了,当仅按1次按键的时候,结果显示有多次按键,这跟我们理想的状态有些差异,出现这种情况是由于存在按键抖动。如下的显示,是由于一次人工按键共产生了4次中断:第1次中断唤醒了测试进程,并且测试进程在第2次中断产生前成功读取了按键次数;第2次中断唤醒了测试进程,但在测试进程尚未成功读取按键次数前,第3次中断迅速产生,因此当测试进程赶在第4次中断产生前读取按键次数时,该值已经变为了2;第4次中断唤醒了测试进程,同时由于按键已经平稳,不再产生新的中断,从而测试进程成功读取了按键次数。
# ./button_test
open success
read buttons successfully, begin print the result:
K1 has been pressed 1 times!
read buttons successfully, begin print the result:
K1 has been pressed 2 times!
read buttons successfully, begin print the result:
K1 has been pressed 1 times!
按键的物理特性决定它肯定会存在抖动。因此要消除抖动,只能采用软件消抖的方法。请编译s3c24xx_buttons_v2.5.c,可得到消除了按键抖动的驱动版本。
# ./button_test
open success
read buttons successfully, begin print the result:
K1 has been pressed 1 times!
s3c24xx_buttons_v2.5.c是怎么做到的呢?其消抖方案是:只在第1次中断产生时才记录按键次数;在第1次中断产生后,多次延时0.1秒直到检测到按键已被放开,最后再做1次延迟0.1秒的操作,然后允许中断可以再次记录按键次数。这样一来,第1次延迟0.1秒消除了按下键时的抖动,最后1次延迟0.1秒消除了放开键时的抖动。这个方案用到的技术主要就是内核定时器和内核时间。
下面对主要实现代码进行说明:
58 static struct buttons_dev_t buttons_dev =
59 {
60 .ev_press = 0,
61 .press_cnt = {0, 0, 0, 0},
62 .button_irqs = {
63 {IRQ_EINT19, IRQF_TRIGGER_FALLING, "KEY1"}, /* K1 */
64 {IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /* K2 */
65 {IRQ_EINT2, IRQF_TRIGGER_FALLING, "KEY3"}, /* K3 */
66 {IRQ_EINT0, IRQF_TRIGGER_FALLING, "KEY4"} /* K4 */
67 },
68 .firstint = 0
69 };
79 if (buttons_dev.firstint == 1)
80 return IRQ_RETVAL(IRQ_NONE);
82 buttons_dev.firstint = 1;
中断处理函数如果发现不是第1次中断,就不记录按键次数。
277 for (i = 0; i < BUTTON_NUM; i++) /* setup button delay timers without set expires. default 4 timers */
278 setup_timer(&(buttons_dev.button_timers[i]), buttons_timer_handler, i);
这是在模块的初始化函数中对4个定时器(分别对应4个按键)进行初始化。
105 buttons_dev.button_timers[butno-1].expires = jiffies + HZ/10; /* delay 0.1s */
106 add_timer(&(buttons_dev.button_timers[butno-1]));
中断处理函数做第1次0.1秒的延时。
152 static void buttons_timer_handler(unsigned long data)
153 {
154 static int shouldfinish = 0;
157 if (shouldfinish) {
158 shouldfinish = 0;
159 buttons_dev.firstint = 0;
160 return;
161 }
162 if (!keydown(data))
163 shouldfinish = 1;
164 mod_timer(&(buttons_dev.button_timers[data]), jiffies + HZ/10); /* delay 0.1s */
165 }
定时器处理函数进行多次0.1秒的延时(164行),并在判定出按键已被放开的情况下作最后一次0.1秒延迟(162-164行),之后允许中断处理函数可以再次记录按键次数(159行)。
41 #define GPGDAT 0x56000064
296 if (!(button12virtaddr = ioremap(GPGDAT, 0x4))) {
297 printk(KERN_NOTICE "ioremap failed\n");
298 result = -ENOMEM;
299 goto fail_ioremap_GPGDAT;
300 }
112 static int keydown(unsigned long data)
113 {
114 int result;
115 switch (data) {
116 case 0:
117 PDEBUGG("K1\n");
118 if ((ioread32(button12virtaddr) & (1<<11)) == 0)
119 result = 1;
120 else
121 result = 0;
122 break;
123 case 1:
147 }
149 return result;
150 }
keydown函数用于判定某个按键是否按下。按下返回真,放开返回假。关于获得按键是否被按下的机制,请参阅“ARM体系结构与编程”中的相关文章。
三、如何在内核中实现延时
设备驱动常常需要延后一段时间执行一个特定片段的代码, 以便允许硬件完成某个任务。延时一般区分为短延时和长延时。
1、短延时
当一个设备驱动需要等待硬件的反应时间, 涉及到的延时常常是最多几个毫秒 。此种延时就是短延时,一般采用忙等待。相关函数如下:
2、长延时
如果需要延后较长时间,就可以采用长延时。长延时可分为忙等待和让出CPU两种方式。
1)、忙等待
unsigned long j1 = jiffies + 2*HZ;
while (time_before(jiffies, j1))
cpu_relax();
cpu_relex 的调用使用了一个特定于体系的方式,你此时没有用处理器做事情。
2)、让出处理器
unsigned long j1 = jiffies + 3600*HZ;
while (time_before(jiffies, j))
schedule();
忙等待强加了一个重负载给系统总体,通过释放CPU改变这种状况。
3)、此外,如果你的驱动使用一个等待队列来等待某些其他事件,但是你也想确保它在一个确定时间段内运行能够运行,而不是永久等待,那么可以使用超时