Linux驱动开发——定时器

简介: Linux驱动开发——定时器

文章目录

Linux内核定时器

硬件定时器

硬件定时器特点

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

Linux内核跟时间相关的概念

软件定时器

Linux内核软件定时器的特点

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

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

总结


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


总结

20191230214344220.png

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


相关文章
|
11月前
|
Ubuntu 搜索推荐 Linux
详解Ubuntu的strings与grep命令:Linux开发的实用工具。
这就是Ubuntu中的strings和grep命令,透明且强大。我希望你喜欢这个神奇的世界,并能在你的Linux开发旅程上,通过它们找到你的方向。记住,你的电脑是你的舞台,在上面你可以做任何你想做的事,只要你敢于尝试。
486 32
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
3702 77
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
768 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
307 6
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
440 5
|
编解码 安全 Linux
基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究
这段内容讲述了国产操作系统背景下,大牛直播SDK针对国产操作系统与Linux平台发布的RTMP/RTSP直播播放SDK。此SDK支持arm64架构,基于X协议输出视频,采用PulseAudio和Alsa Lib处理音频,具备实时静音、快照、缓冲时间设定等功能,并支持H.265编码格式。此外,提供了示例代码展示如何实现多实例播放器的创建与管理,包括窗口布局调整、事件监听、视频分辨率变化和实时快照回调等关键功能。这一技术实现有助于提高直播服务的稳定性和响应速度,适应国产操作系统在各行业中的应用需求。
498 3
|
Ubuntu Linux Docker
Java演进问题之Alpine Linux创建更小的Docker镜像如何解决
Java演进问题之Alpine Linux创建更小的Docker镜像如何解决
244 0
|
弹性计算 运维 自然语言处理
阿里云OS Copilot测评:重塑Linux运维与开发体验的智能革命
阿里云OS Copilot巧妙地将大语言模型的自然语言处理能力与操作系统团队的深厚经验相结合,支持自然语言问答、辅助命令执行等功能,为Linux用户带来了前所未有的智能运维与开发体验。
|
Web App开发 缓存 Linux
FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流
《FFmpeg开发实战》书中第10章提及轻量级流媒体服务器MediaMTX,适合测试RTSP/RTMP协议,但不适合生产环境。推荐使用SRS或ZLMediaKit,其中SRS是国产开源实时视频服务器,支持多种流媒体协议。本文简述在华为欧拉系统上编译安装SRS和FFmpeg的步骤,包括安装依赖、下载源码、配置、编译以及启动SRS服务。此外,还展示了如何通过FFmpeg进行RTMP推流,并使用VLC播放器测试拉流。更多FFmpeg开发内容可参考相关书籍。
1252 2
FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流