Linux驱动开发——定时器


Linux内核定时器

硬件定时器

硬件定时器特点

硬件定时器能够按照一定的频率周期性的有规律的给CPU发送中断信号,发送中断的频率(周期)可以通过软件编程来设置,硬件定时器产生的中断信号可以称之为时钟中断。

硬件定时器对应的中断处理函数

硬件定时器产生的中断同样也会有中断处理函数,只是这个中断处理函数由内核已经帮你写好,此中断处理函数同样被内核按照一定的频率周期性的有规律的被内核调用。
硬件定时器对应的中断处理函数会做如下内容:

  • 更新系统的运行时间。
  • 更新实际时间(wall time)。
  • 检查当前进程的时间片是否用尽,是否决定进行调度。
  • 检查是否有超时的内核软件定时器,如果有超时的软件定时器,内核调度超时的软件定时器对应的超时处理函数。
  • 统计系统的资源,数据信息(top命令查看CPU使用率等数据的更新)

Linux内核跟时间相关的概念

HZ: 是内核的一个全局常量(不同CPU架构中这个宏定义不同),例如:

  • ARM架构:HZ=100(HZ=100表示硬件定时器一秒钟会给CPU发送100次硬件定时器中断,每发生一次硬件定时器中断的时间间隔为10ms)
  • X86架构:HZ=1000

jiffies_64: 是内核的一个全局变量,64位(unsigned long long),记录自开机以来,硬件定时器给CPU发送的硬件定时器中断的次数,即每发生一次硬件定时器中断,jiffies_64自动加一(由硬件定时器的中断处理函数进行执行),也就是时间上加10ms。

jiffies: 也是内核的一个全局变量,32位(unsigned long),它的值取的是jiffies_64的低32位,也就是每发生一次硬件定时器中断,jiffies_64和jiffies都会加一,但是由于jiffies的存储范围的原因,通常用来记录时间间隔(记录流失的时间),切记:通常内核中使用jiffies一般是用来获取当前时刻,用来设置超时时间的。

  • 超时处理常见使用方式:
unsigned long timeout = jiffies + 5*HZ;
//说明:jiffies:表示代码执行到这条语句对应的当前时刻的时间				 
//5*HZ=5*100=500:表示500次硬件定时器中断,一次为10ms
//所以5*HZ表示5秒钟
//timeout:5秒以后的时间
	   
unsigned long timeout = jiffies + 2;
//说明:jiffies:表示代码执行到这条语句对应的当前时刻的时间				 
//2次硬件定时器中断,共20ms
//timeout:20ms以后的时间
  • 通常超时处理使用的方式一般为:
 unsigned long timeout = jiffies + 5*HZ;
//一堆代码
	   ...
	   ...
	   ...
//以上代码执行完毕,判断是否发生了超时现象
if(jiffies > timeout)
	//超时
else
	//没超时

使用以上方法粗看上去没有问题,但是细想其实忽略了jiffies溢出的情况,如果开始取timeout的时候取得的值是jiffies溢出后的值,那么按照以上方式判断就会有很大问题了。Linux内核中引入了一个考虑到会有溢出情况的超时判断函数(time_after()),所以实际使用时应该如下使用:

	 unsigned long timeout = jiffies + 5*HZ;
   //一堆代码
   ...
   ...
   ...
   //以上代码执行完毕,判断是否发生了超时现象
   if(time_after(jiffies, timeout))
			超时
	 else
	 	  没超时

软件定时器

Linux内核软件定时器的特点

可以指定一个超时时间,一旦超时时间到期,内核会自动调用定时器的超时处理函数。切记:Linux内核软件定时器是基于软中断实现,所以其超时处理函数不能进行休眠操作。

Linux内核软件定时器的相关数据结构和配套函数

数据结构:

struct timer_list {
	unsigned long expires;
	void (*function)(unsigned long data);
	unsigned long data;
	...
};

成员:

  • expires:定时器的超时时间,例如:5秒以后超时,expires=jiffies+5*HZ;
  • function:定时器的超时处理函数,基于软中断实现,所以不能进行休眠操作,形参data:保存传递的参数信息
  • data:就超时处理函数传递的参数。

配套函数:

 init_timer(&定时器对象);//初始化定时器对象
  • 注意:还需要额外自己初始化超时时刻的时间、超时处理函数和传递的参数信息(如果有必要)
    定时器对象.expires = jiffies+5*HZ; //指定超时时间
    定时器对象.function = xxxx_function;//指定超时处理函数
    定时器对象.data = (unsigned long)&xxx; //给超时处理函数传递参数
add_timer(&定时器对象);//向内核注册添加一个定时器,
  • 一旦添加完毕,定时器就开始倒计时, 一旦时间到期,内核就会调用其超时处理函数并且将定时器从内核中删除, 所以内核定时器的超时处理函数只执行一次。
del_timer(&定时器对象);//从内核中删除定时器
mod_timer(&定时器对象,新的超时时刻的时间);//修改定时器的超时时刻的时间
//注意:mod_timer=先del_timer,然后expires=jiffies+20*HZ,最后add_timer

示例(定时操作GPIO亮灭LED灯)

设置定时亮灭GPIO, 在这里加入btn操作超时时间,key_up增加1s延迟,key_down降低1s延迟,延迟范围(0~10)。

  • mytimer_demo.c(初版,尝试了一下软件定时器的具体使用)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/gpio.h>
#include <mach/platform.h>

//声明描述LED的硬件信息的数据结构
//定义初始化4个LED硬件信息对象

//定义定时器对象
static struct timer_list mytimer;

//定时器的超时处理函数
//定时器超时时间到期,内核执行此函数
//切记:千万不能进行休眠操作
//data = (unsigned long)&g_data
static void mytimer_function(unsigned long data)
{
    //1.如果是开灯,那就关灯;如果是关灯,那就开灯
    //注意:不允许使用if ... else判断
    printk("%s: data = %#x\n", __func__, *(int *)data);
    
    //2.重新向内核添加定时器对象
    /*以下两条语句相当之危险,因为他们访问了一个全局变量mytimer,如果将来有高优先级的硬件中断和高优先级的软中断来打断
     * 这两条语句的执行,势必造成定时器的错误,需要考虑互斥问题
    mytimer.expires = jiffies + 2*HZ; //重新添加新的超时时间
    add_timer(&mytimer);
    */
    //del_timer+expire=...+add_timer
    //此函数非常安全,因为里面做了互斥访问机制
    mod_timer(&mytimer, jiffies+2*HZ);
}
/*
static irqreturn_t button_isr(int irq, void *dev)
{
    //此代码会篡改定时器的超时时间
    mytimer.expires = jiffies + 50*HZ;
    return IRQ_HANDLED; 
}
*/
static int g_data = 0x5555;

static int mytimer_init(void)
{
    //1.申请GPIO资源,配置GPIO为输出,输出1
    //2.初始化定时器对象
    init_timer(&mytimer);
    //3.额外指定定时器的超时时间
    mytimer.expires = jiffies + 2*HZ;
    //4.额外指定定时器的超时处理函数
    mytimer.function = mytimer_function;
    //5.额外指定给超时处理函数传递的参数
    mytimer.data = (unsigned long)&g_data;
    //6.向内核添加定时器对象,开始倒计时
    //一旦时间到期,内核调用超时处理函数并且删除定时器
    add_timer(&mytimer);
    return 0;
}

static void mytimer_exit(void)
{
    //1.释放GPIO资源
    //2.删除定时器对象
    del_timer(&mytimer);
}
module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");

  • mytimer_drv.c(正式版,用到了前面提过的tasklet和工作队列)
/*************************************************************************
	> File Name: btn_drv.c
	> Author: 
	> Mail: 
	> Created Time: 2019年12月29日 星期日 09时08分30秒
 ************************************************************************/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h>

struct btn_resource{
    int gpio; //按键对应的GPIO
    char *name;//gpio Name
    int code;//按键值
};

struct led_resource{
    int gpio;
    char *name;
};

//定义吃时候按键对应的硬件信息
static struct btn_resource btn_info[] = {
    {
        .gpio = PAD_GPIO_A + 28,
        .name = "KEY_UP",
        .code = KEY_UP
    },
    {
        .gpio = PAD_GPIO_B + 9,
        .name = "KEY_DOWN",
        .code = KEY_DOWN
    }
};

static struct led_resource led_info[] ={
    {
        .gpio = PAD_GPIO_C + 12,
        .name = "LED1"
    },
    {
        .gpio = PAD_GPIO_C + 7,
        .name = "LED2"
    },
    {
        .gpio = PAD_GPIO_C + 11,
        .name = "LED3"
    },
    {
        .gpio = PAD_GPIO_B + 26,
        .name = "LED4"
    }
};

//记录按键的硬件信息
static struct btn_resource *pdata;

//定义定时器对象
static struct timer_list mytimer;

//定义定时器时间
static int mytimer_value = 5;
static int led_cmd_flag = 0;

//tasklet process mytimer_function led opts

static void timer_tasklet_function(unsigned long data)
{
    //open led or close led
    int i = 0;
    int *p_data = data;
    printk("%s : current timer value = %d, state = %d\n", __func__, mytimer_value, *p_data);
    if(*p_data)
    {
        *p_data = 0;
        for(i = 0; i < ARRAY_SIZE(led_info); i++)
        {
            gpio_set_value(led_info[i].gpio, 0);
        }
    }
    else
    {
        *p_data = 1;
        for(i = 0; i < ARRAY_SIZE(led_info); i++)
        {
            gpio_set_value(led_info[i].gpio, 1);
        }
    }
    printk("tasklet process : %s \n", __func__);

}

//定义初始化一个tasklet对象
static DECLARE_TASKLET(mytimer_tasklet, timer_tasklet_function, (unsigned long)&led_cmd_flag);

//定时器处理函数
//data = (unsigned long)&g_data
static void mytimer_function(unsigned long data)
{
    //登记底半部tasklet处理函数
    tasklet_schedule(&mytimer_tasklet);

    //add_timer(&mytimer);
    mod_timer(&mytimer, jiffies + mytimer_value * HZ);
    printk("schedule timer top process : %s \n", __func__);
}

//工作队列方式处理定时器延长或缩短
//work = &btn_work
static void btn_work_function(struct work_struct *work)
{
    switch(pdata->code)
    {
        case KEY_UP:
            //add expir timeout
            if(mytimer_value <= 10)
                mytimer_value++;
            break;
        case KEY_DOWN:
            //submit expir timeout
            if(mytimer_value > 0)
                mytimer_value--;
            break;
        default:
            printk("dev->code is not impire.\n");
    }
    printk("btn work function : %s \n", __func__);

}

//定义工作队列
static struct work_struct btn_work;

//中断处理函数
//不同的按键触发irq不同,KEY_UP对应irq = gpio_to_irq(GPIOA28)
//响应的参数不同,dev = &btn_info[0]或者dev = &btn_info[1]
static irqreturn_t button_isr(int irq, void *dev)
{
    //获取当前硬件信息
    pdata = (struct btn_resource *)dev;

    //登记工作队列
    schedule_work(&btn_work);
    
    //验证顶半部先执行
    printk("top func : %s mytimer_value = %d\n", __func__, mytimer_value);

    return IRQ_HANDLED;//返回执行成功
}

static int mytimer_init(void)
{
    int i;

    //吃时候定时器对象
    init_timer(&mytimer);
    //指定定时器超时时间
    mytimer.expires = jiffies + 5 * HZ;
    mytimer.function = mytimer_function;

    //指定参数
    mytimer.data = (unsigned long)&led_cmd_flag;
    add_timer(&mytimer);

    for(i = 0; i < ARRAY_SIZE(btn_info); i++)
    {
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_request(btn_info[i].gpio, btn_info[i].name);
        //IRQF_TRIGGER_FALLING
        //IRQF_TRIGGER_RISING
        request_irq(irq, button_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, btn_info[i].name, &btn_info[i]);
    }

    for(i = 0; i < ARRAY_SIZE(led_info); i++)
    {
        gpio_request(led_info[i].gpio, led_info[i].name);
        gpio_direction_output(led_info[i].gpio, 0);
    }

    printk("led btn timer init...\n");

    //初始化工作队列
    INIT_WORK(&btn_work, btn_work_function);

    return 0;
}

static void mytimer_exit(void)
{
    int i;
    del_timer(&mytimer);

    for(i =0; i < ARRAY_SIZE(btn_info); i++)
    {
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_free(btn_info[i].gpio);
        free_irq(irq, &btn_info[i]);
    }
    for(i = 0; i < ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, 1);
        gpio_free(led_info[i].gpio);
    }

    printk("led btn timer exit...\n");
}

module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");


  • Makefile
obj-m += mytimer_drv.o
all:
	make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
	make -C /opt/kernel SUBDIRS=$(PWD) clean

总结

在这里插入图片描述

  • 从图中可以看出,当我们按下按键调整大小时发现每次都是触发两次按键中断处理,这可能是因为双边沿触发的问题,但是最重要的是我们在处理函数中已经判断过延时间断的数值范围了,实际使用时发现还是会超出范围,这个现象就是因为我们用的变量是一个全局变量,在这里产生了并发和竞争的现象,要解决这个问题就需要加锁、信号量等等一些方式了,后面马上就需要针对这个问题进行改进了。
发布了78 篇原创文章 · 获赞 25 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_37596943/article/details/103760395