驱动篇:中断与时钟(二)

驱动篇:中断与时钟(二)

1.实时钟中断
S3C2410 处理器内部集成了实时钟(RTC)模块,该模块能够在系统断电的情况下由后备电池供电继续工作,其主要功能相对于一个时钟,记录年、月、日、时、分、秒等。S3C2410 的 RTC 可产生两种中断:周期节拍(tick)中断和报警(alarm)中断,前者相当于一个周期性的定时器,后者相当于一个“闹钟” ,它在预先设定的时间到来时产生中断

实时钟设备驱动的 open()函数中,会申请它将要使用的中断

 static int s3c2410_rtc_open(void)
 {
int ret;
/*申请 alarm 中断*/
ret = request_irq(s3c2410_rtc_alarmno, s3c2410_rtc_alarmirq,
SA_INTERRUPT, "s3c2410-rtc alarm", NULL);
/*中断号被占用*/
if (ret)
printk(KERN_ERR "IRQ%d already in use\n", s3c2410_rtc_alarmno);
 /*申请 tick 中断*/
ret = request_irq(s3c2410_rtc_tickno, s3c2410_rtc_tickirq,
SA_INTERRUPT,
"s3c2410-rtc tick", NULL);
 /*中断号被占用*/
 if (ret)
 {
printk(KERN_ERR "IRQ%d already in use\n", s3c2410_rtc_tickno);
goto tick_err;
 }
 return ret;
 tick_err: free_irq(s3c2410_rtc_alarmno, NULL); /*释放 alarm 中断*/
 return ret;
 }

release()函数中,会释放它将要使用的中断

 static void s3c2410_rtc_release(void)
 {
s3c2410_rtc_setpie(0);
/* 释放中断 */
free_irq(s3c2410_rtc_alarmno, NULL);
free_irq(s3c2410_rtc_tickno, NULL);
 }

中断处理比较简单,不需要分为上下两个半部,而只存在顶半部

 /*中断处理*/
 static irqreturn_t s3c2410_rtc_alarmirq(int irq, void *id, struct pt_regs *r)
 {
 rtc_update(1, RTC_AF | RTC_IRQF);
 return IRQ_HANDLED;
 }
 static irqreturn_t s3c2410_rtc_tickirq(int irq, void *id, struct pt_regs *r)
 {
 rtc_update(1, RTC_PF | RTC_IRQF);
 return IRQ_HANDLED;
 }

rtc_upda te () 函 数 定 义 于linux-2.6.16\arch\arm\common\Rtctime.c 文件中,被各种实时钟共享

 void rtc_update(unsigned long num, unsigned long events)
{
spin_lock(&rtc_lock);
rtc_irq_data = (rtc_irq_data + (num << 8)) | events;
spin_unlock(&rtc_lock);
wake_up_interruptible(&rtc_wait);/*唤醒等待队列*/
kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);/*释放信号*/
}
EXPORT_SYMBOL(rtc_update);

上述中断处理程序并没有底半部(或者说没有严格意义上的 tasklet、工作队列或软中断底半部)
,实际上,它只是唤醒一个等待队列 rtc_wait,而这个等待队列的唤醒也将导致一个阻塞的进程被执行(这个阻塞的进程可看做底半部) 。现在我们看到,等待队列可以作为中断处理程序顶半部和进程同步的一种良好机制。但是,任何情况下,都不能在顶半部等待一个等待队列,而只能唤醒。
2.中断共享
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,PCI 设备即是如此。Linux 2.6 支持这种中断共享。下面是中断共享的使用方法。

l 共享中断的多个设备在申请中断时都应该使用 SA_SHIRQ 标志,而且一个设备以 SA_SHIRQ 申请某中断成功的前提是之前申请该中断的所有设备也都以 SA_SHIRQ 标志申请该中断。
l 尽 管 内核 模 块 可 访问 的 全 局 地 址 都 可 以作为request_irq (…, void *dev_id) 的最后一个参数 dev_id,但是设备结构体指针是可传入的最佳参数。
l 在中断到来时,所有共享此中断的中断处理程序都会被执行,在中断处理程序顶半部中,应迅速地根据硬件寄存器中的信息比照传入的 dev_id 参数判断是否是本设备的中断,若不是,应迅速返回

共享中断编程的模板

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs
{
...
int status = read_int_status();/*获知中断源*/
if(!is_myint(dev_id,status))/*判断是否是本设备中断*/
{
return IRQ_NONE; /*立即返回*/
}
...
return IRQ_HANDLED;
}
/*设备驱动模块加载函数*/
int xxx_init(void)
{
...
/*申请共享中断*/
result = request_irq(sh_irq, xxx_interrupt,
SA_SHIRQ, "xxx", xxx_dev);
...
}
/*设备驱动模块卸载函数*/
void xxx_exit(void)
{
...
/*释放中断*/
free_irq(xxx_irq, xxx_interrupt);
...
}

3.内核定时器编程
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序执行 update_process_timers()函数, 该函数调用 run_local_timers()函数,这个函数处理 TIMER_SOFTIRQ 软中断,运行当前处理器上到期的所有定时器。在 Linux 设备驱动编程中,可以利用 Linux 内核中提供的一组函数和数据结构来完成定时触发工作或者完成某周期性的事务。这组函数和数据结构使得驱动工程师多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。Linux 内核所提供的用于操作定时器的数据结构和函数如下
1.timer_list
timer_list 结构体的一个实例对应一个定时器

struct timer_list {
struct list_head entry; //定时器列表
unsigned long expires; //定时器到期时间
void (*function)(unsigned long); //定时器处理函数
unsigned long data; //作为参数被传入定时器处理函数
struct timer_base_s *base;
};

当定时器期满后, 其中 function()成员将被执行, data 成员则
是传入其中的参数, expires 则是定时器到期的时间(jiffies)

2.初始化定时器

void init_timer(struct timer_list * timer);

上述 init_timer()函数初始化 timer_list 的 entry 的 next 为 NULL,并给 base 指针赋值。

TIMER_INITIALIZER(_function, _expires, _data)宏用于赋值定时器结构体的function、expires、data 和 base 成员
在这里插入图片描述
DEFINE_TIMER(_name , _ function, _expires, _ data )宏是定义并初始化定时器成员的“快捷方式”
在这里插入图片描述
此外,setup_timer()也可用于初始化定时器并赋值其成员

static inline void setup_timer(struct timer_list * timer,
void (*function)(unsigned long),
unsigned long data)
{
timer->function = function;
timer->data = data;
init_timer(timer);
}

3.增加定时器

void add_timer(struct timer_list * timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。

4.删除定时器

int del_timer(struct timer_list * timer);

上述函数用于删除定时器。
del_timer_sync()del_timer()的同步版,主要在多处理器系统中使用,如果编译
内核时不支持 SMP,del_timer_sync()del_timer()等价。

5.修改定时器的 expire

int mod_timer(struct timer_list *timer, unsigned long expires);

上述函数用于修改定时器的到期时间,在新的被传入的 expires 到来后才会执行定时器函数。

内核定时器使用模板

/*定时器使用模板*/
/*xxx 设备结构体*/
 struct xxx_dev
 {
 struct cdev cdev;
 ...
 timer_list xxx_timer;/*设备要使用的定时器*/
 };
 /*xxx 驱动中的某函数*/
 xxx_func1(...)
 {
 struct xxx_dev *dev = filp->private_data;
 ...
 /*初始化定时器*/
 init_timer(&dev->xxx_timer);
 dev->xxx_timer.function = &xxx_do_timer;
 dev->xxx_timer.data = (unsigned long)dev;
/*设备结构体指针作为定时器处理函数参数*/
 dev->xxx_timer.expires = jiffies + delay;
 /*添加(注册)定时器*/
 add_timer(&dev->xxx_timer);
 ...
 }
 /*xxx 驱动中的某函数*/
 xxx_func2(...)
 {
 ...
 /*删除定时器*/
 del_timer (&dev->xxx_timer);
 ...
 }
 /*定时器处理函数*/
 static void xxx_do_timer(unsigned long arg)
  {
 struct xxx_device *dev = (struct xxx_device *)(arg);
 ...
 /*调度定时器再执行*/
 dev->xxx_timer.expires = jiffies + delay;
 add_timer(&dev->xxx_timer);
 ...
 }

定时器的到期时间往往是在目前 jiffies 的基础上添加一个时延,若为 Hz,则表示延迟 1s。在定时器处理函数中,在做完相应的工作后, 往往会延后 expires 并将定时器再次添加到内核定时器链表,以便定时器能再次被触发

4.实例:秒字符设备
秒字符设备在被打开的时候初始化一个定时器并将其添加到内核定时器链表,每秒输出一次当前的 jiffies(为此,定时器处理函数中每次都要修改新的 expires)

#include ...
#define SECOND_MAJOR 252
/*预设的 second 的主设备号*/
static int second_major = SECOND_MAJOR;

/*second 设备结构体*/
 struct second_dev
 {
 struct cdev cdev; /*cdev 结构体*/
 atomic_t counter;/* 一共经历了多少秒?*/
 struct timer_list s_timer; /*设备要使用的定时器*/
 };



struct second_dev *second_devp; /*设备结构体指针*/



 /*定时器处理函数*/
 static void second_timer_handle(unsigned long arg)
 {
 mod_timer(&second_devp->s_timer,jiffies + HZ);
 atomic_inc(&second_devp->counter);
 printk(KERN_NOTICE "current jiffies is %ld\n", jiffies);
 }






 /*文件打开函数*/
 int second_open(struct inode *inode, struct file *filp)
 {
 /*初始化定时器*/
 init_timer(&second_devp->s_timer);
 second_devp->s_timer.function = &second_timer_handle;
 second_devp->s_timer.expires = jiffies + HZ;
add_timer(&second_devp->s_timer); /*添加(注册)定时器*/
atomic_set(&second_devp->counter,0); //计数清零
return 0;
 }



 /*文件释放函数*/
 int second_release(struct inode *inode, struct file *filp)
 {
del_timer(&second_devp->s_timer);
return 0;
 }





 /*globalfifo 读函数*/
static ssize_t second_read(struct file *filp, char _ _user *buf,size_t count,loff_t *ppos)
{
int counter;
 counter = atomic_read(&second_devp->counter);
 if(put_user(counter, (int*)buf))
return - EFAULT;
else
return sizeof(unsigned int);
 }



 /*文件操作结构体*/
static const struct file_operations second_fops =
 {
 .owner = THIS_MODULE,
 .open = second_open,
 .release = second_release,
 .read = second_read,
 };


 /*初始化并注册 cdev*/
 static void second_setup_cdev(struct second_dev *dev, int index)
{
int err, devno = MKDEV(second_major, index);
 cdev_init(&dev->cdev, &second_fops);
 dev->cdev.owner = THIS_MODULE;
 dev->cdev.ops = &second_fops;
 err = cdev_add(&dev->cdev, devno, 1);
 if (err)
printk(KERN_NOTICE "Error %d adding LED%d", err, index);
 }



 /*设备驱动模块加载函数*/
 int second_init(void)
 {
 int ret;
 dev_t devno = MKDEV(second_major, 0);
 /* 申请设备号*/
 if (second_major)
ret = register_chrdev_region(devno, 1, "second");
 else /* 动态申请设备号 */
 {
 ret = alloc_chrdev_region(&devno, 0, 1, "second");
 second_major = MAJOR(devno);
 }
 if (ret < 0)
return ret;
/* 动态申请设备结构体的内存成功返回
   分配的目标内存空间的首地址;失败返回NULL.  */
 second_devp = kmalloc(sizeof(struct second_dev), GFP_KERNEL);
 if (!second_devp)
/*申请失败*/
   {
ret = - ENOMEM;
 goto fail_malloc;
}
 //Memset 用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为‘ ’或‘/0’;
memset(second_devp, 0, sizeof(struct second_dev));
second_setup_cdev(second_devp, 0);
return 0;
fail_malloc: unregister_chrdev_region(devno, 1);
 }




 /*模块卸载函数*/
 void second_exit(void)
 {
 cdev_del(&second_devp->cdev);/*注销 cdev*/
 kfree(second_devp);/*释放设备结构体内存*/
 unregister_chrdev_region(MKDEV(second_major, 0), 1); /*释放设备号*/
 }




 MODULE_AUTHOR("xxx");
 MODULE_LICENSE("Dual BSD/GPL");
 module_param(second_major, int, S_IRUGO);
 module_init(second_init);
 module_exit(second_exit);

在 second 的 open()函数中,将启动定时器,此后每 1s 会再次运行定时器处理函数,在 second 的 release()函数中,定时器被删除。second_dev 结构体中的原子变量 counter 用于秒计数,每次在定时器处理函数中将被 atomic_inc()调用原子的增 1,second 的 read()函数会将这个值返回给用户空间

second 设备用户空间测试程序

#include ...
main()
{
int fd;
int counter = 0;
int old_counter = 0;
/*打开/dev/second 设备文件*/
fd = open("/dev/second", O_RDONLY);
if (fd != - 1)
{
while (1)
{
read(fd,&counter, sizeof(unsigned int));//读目前经历的秒数
if(counter!=old_counter)
{
printf("seconds after open /dev/second :%d\n",counter);
old_counter = counter;
}
}
}
else
{
printf("Device open failure\n");
}
}

5.内核延时
短延迟

void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

3 个函数分别进行纳秒、微秒和毫秒延迟

上述延迟的实现原理本质上是忙等待, 它根据 CPU 频率进行一定次数的循环。 有时候,可以在软件中进行这样的延迟:
void delay(unsigned int time)
{
while (time–);
}
ndelay()、udelay()和 mdelay()函数的实现方式机理与此类似。毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用mdelay()函数,这将无谓地耗费 CPU 资源,对于毫秒级以上时延,内核提供了下述函数

void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);

上述函数将使得调用它的进程睡眠参数指定的时间,msleep()ssleep()不能被打
断,msleep_interruptible()则可以被打断。

长延迟
内核中进行延迟的一个很直观的方法是比较当前的 jiffies 和目标 jiffies ( 设置为当前 jiffies 加上时间间隔的 jiffies),直到未来的 jiffies 达到目标 jiffies。代码清单 10.13给出了使用忙等待先延迟 100 个 jiffies 再延迟 2s 的实例。
忙等待时延实例

/*延迟 100 个 jiffies*/
unsigned long delay = jiffies + 100;
while (time_before(jiffies, delay));
/*再延迟 2s*/
unsigned long delay = jiffies + 2*HZ;
while (time_before(jiffies, delay));

与 time_before()对应的还有一个 time_after(),它们在内核中定义为(实际上只是将传入的未来时间 jiffies 和被调用时的 jiffies 进行一个简单的比较):

#define time_after(a,b)  \
(typecheck(unsigned long, a) &&  \
typecheck(unsigned long, b) &&  \
((long)(b) - (long)(a) < 0))

#define time_before(a,b) time_after(b,a)

为了防止 time_before()time_after()的比较过程中编译器对 jiffies 的优化,内核
将其定义为 volatile 变量,这将保证它每次都被重新读取。

关键字volatile和const是完全相反的。它表明变量可能会通过某种方式发生改变,而这种方式是你通过分析正常的程序流程完全预测不出来的每次对变量内容的引用会重新从内存中加载而不是从变量在寄存器里面的拷贝加载
6.睡着延迟
睡着延迟无疑是比忙等待更好的方式,随着延迟在等待的时间到来之间进程处于睡眠状态,CPU 资源被其他进程使用。schedule_timeout()可以使当前任务睡眠指定的jiffies 之后重新被调度执行, msleep()和 msleep_interruptible()在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()实现的

void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
unsigned long msleep_interruptible(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout && !signal_pending(current))
timeout = schedule_timeout_interruptible(timeout);
return jiffies_to_msecs(timeout);
}

实际上,schedule_timeout()的实现原理是向系统添加一个定时器,在定时器处理
函数中唤醒参数对应的进程

另外,下面两个函数可以将当前进程添加到等待队列中, 从而在等待队列上睡眠。当超时发生时,进程将被唤醒(后者可以在超时前被打断),如下所示:

sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q,unsigned long timeout);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值