• 嵌入式Linux应用开发-基础知识-第十九章驱动程序基石③


    第十九章 驱动程序基石③

    在这里插入图片描述

    19.5 定时器

    使用 GIT命令载后,本节源码位于这个目录下:

    01_all_series_quickstart\ 
    05_嵌入式 Linux驱动开发基础知识\source\ 
    06_gpio_irq\ 
        07_read_key_irq_poll_fasync_block_timer   
    
    • 1
    • 2
    • 3
    • 4

    19.5.1 内核函数

    所谓定时器,就是闹钟,时间到后你就要做某些事。有 2个要素:时间、做事,换成程序员的话就是:超时时间、函数。
    在内核中使用定时器很简单,涉及这些函数(参考内核源码 include\linux\timer.h):
    ① setup_timer(timer, fn, data):
    设置定时器,主要是初始化 timer_list结构体,设置其中的函数、参数。
    ② void add_timer(struct timer_list *timer):
    向内核添加定时器。timer->expires表示超时时间。
    当超时时间到达,内核就会调用这个函数:timer->function(timer->data)。
    ③ int mod_timer(struct timer_list *timer, unsigned long expires):
    修改定时器的超时时间,
    它等同于:del_timer(timer); timer->expires = expires; add_timer(timer);
    但是更加高效。
    ④ int del_timer(struct timer_list *timer):
    删除定时器。

    19.5.2 定时器时间单位

    编译内核时,可以在内核源码根目录下用“ls -a”看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:

    CONFIG_HZ=100 
    
    • 1

    这表示内核每秒中会发生 100次系统滴答中断(tick),这就像人类的心跳一样,这是 Linux系统的心跳。每发生一次 tick中断,全局变量 jiffies就会累加 1。
    CONFIG_HZ=100表示每个滴答是 10ms。
    定时器的时间就是基于 jiffies的,我们修改超时时间时,一般使用这 2种方法:
    ① 在 add_timer之前,直接修改:

    timer.expires = jiffies + xxx;   // xxx表示多少个滴答后超时,也就是 xxx*10ms 
    timer.expires = jiffies + 2*HZ;  // HZ等于 CONFIG_HZ,2*HZ就相当于 2秒 
    
    • 1
    • 2

    ② 在 add_timer之后,使用 mod_timer修改:

    mod_timer(&timer, jiffies + xxx);   // xxx表示多少个滴答后超时,也就是 xxx*10ms 
    mod_timer(&timer, jiffies + 2*HZ);  // HZ等于 CONFIG_HZ,2*HZ就相当于 2秒 
    
    • 1
    • 2

    19.5.3 使用定时器处理按键抖动

    在实际的按键操作中,可能会有机械抖动:
    在这里插入图片描述
    按下或松开一个按键,它的 GPIO电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。 如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。
    怎么处理?
    ① 在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报
    ② 使用定时器
    显然第 1种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。
    怎么使用定时器?看下图:
    在这里插入图片描述

    核心在于:在 GPIO中断中并不立刻记录按键值,而是修改定时器超时时间,10ms后再处理。 如果 10ms内又发生了 GPIO中断,那就认为是抖动,这时再次修改超时时间为 10ms。
    只有 10ms之内再无 GPIO中断发生,那么定时器的函数才会被调用。
    在定时器函数中记录按键值。

    19.5.4 现场编程、上机

    19.5.5 深入研究:定时器的内部机制

    初学者会用定时器就行,本节不用看。
    怎么实现定时器,逻辑上很简单:每发生一次硬件中断时,硬件中断处理完后就会看看有没有软件中断要处理。
    定时器就是通过软件中断来实现的,它属于 TIMER_SOFTIRQ软中断。
    对于 TIMER_SOFTIRQ软中断,初始化代码如下:

    void __init init_timers(void) 
    { 
     init_timer_cpus(); 
     init_timer_stats(); 
     open_softirq(TIMER_SOFTIRQ, run_timer_softirq); 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。对于 TIMER_SOFTIRQ,会调用 run_timer_softirq,它的函数如下:

    run_timer_softirq 
    __run_timers(base); 
        while (time_after_eq(jiffies, base->clk)) { 
            …… 
    expire_timers(base, heads + levels);     fn = timer->function; 
        data = timer->data; 
        call_timer_fn(timer, fn, data);         fn(data); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    简单地说,add_timer函数会把 timer放入内核里某个链表;
    在 TIMER_SOFTIRQ的处理函数中,会从链表中把这些超时的 timer取出来,执行其中的函数。 怎么判断是否超时?jiffies大于或等于 timer->expires时,timer就超时。
    内核中有很多 timer,如果高效地找到超时的 timer?这是比较复杂的,
    我们以后如果要深入讲解 timer的话,会用视频来讲解。

    19.5.6 深入研究:找到系统滴答

    这只是一些笔记,初学者不用看。
    在开发板执行以下命令,可以看到 CPU0下有一个数值变化特别快,它就是滴答中断:

    # cat /proc/interrupts 
               CPU0 
     16:       2532       GPC  55 Level     i.MX Timer Tick 
     19:         22       GPC  33 Level     2010000.ecspi 
     20:        384       GPC  26 Level     2020000.serial 
     21:          0       GPC  98 Level     sai 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以 xxxxxx_IMX6ULL为做,滴答中断名字就是“i.MX Timer Tick”。
    在 Linux内核源码目录下执行以下命令:

    $ grep "i.MX Timer Tick" * -nr 
    drivers/clocksource/timer-imx-gpt.c:319:        act->name = "i.MX Timer Tick"; 
    
    • 1
    • 2

    打开 timer-imx-gpt.c 319行左右,可得如下源码:

     act->name = "i.MX Timer Tick"; 
     act->flags = IRQF_TIMER | IRQF_IRQPOLL; 
     act->handler = mxc_timer_interrupt; 
     act->dev_id = ced; 
    return setup_irq(imxtm->irq, act); 
    mxc_timer_interrupt应该就是滴答中断的处理函数,代码如下: static irqreturn_t mxc_timer_interrupt(int irq, void *dev_id) { 
     struct clock_event_device *ced = dev_id; 
     struct imx_timer *imxtm = to_imx_timer(ced); 
     uint32_t tstat; 
    tstat = readl_relaxed(imxtm->base + imxtm->gpt->reg_tstat); imxtm->gpt->gpt_irq_acknowledge(imxtm); 
    ced->event_handler(ced); 
    return IRQ_HANDLED; 
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在上述代码中没看到对 jiffies的累加操作啊,应该是在 ced->event_handler(ced)中进行。
    ced->event_handler(ced)是哪一个函数?不太好找,我使用QEMU来调试内核,在mxc_timer_interrupt中打断点跟踪代码(以后的课程会讲怎么用 QEMU调试内核),发现它对应 tick_handle_periodic。
    tick_handle_periodic位于 kernel\time\tick-common.c中,它里面的调用关系如下:

    tick_handle_periodic 
    tick_periodic(cpu); 
        do_timer(1); 
            jiffies_64 += ticks;  // jiffies就是 jiffies_64 
    
    • 1
    • 2
    • 3
    • 4

    你为何说 jiffies就是 jiffies_64?在 arch\arm\kernel\vmlinux.lds.S有如下代码:

    #ifndef __ARMEB__ 
    jiffies = jiffies_64; 
    #else 
    jiffies = jiffies_64 + 4; 
    #endif 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上述代码说明了,对于大字节序的 CPU,jiffies指向 jiffies_64的高 4字节;对于小字节序的 CPU,jiffies指向 jiffies_64的低 4字节。
    对 jiffies_64的累加操作,就是对 jiffies的累加操作。

    19.6 中断下半部tasklet

    使用 GIT命令载后,本节源码位于这个目录下:

    01_all_series_quickstart\ 
    05_嵌入式 Linux驱动开发基础知识\source\ 
    06_gpio_irq\ 
        08_read_key_irq_poll_fasync_block_timer_tasklet 
    
    • 1
    • 2
    • 3
    • 4

    在前面我们介绍过中断上半部、下半部。中断的处理有几个原则:
    ① 不能嵌套;
    ② 越快越好。
    在处理当前中断时,即使发生了其他中断,其他中断也不会得到处理,所以中断的处理要越快越好。但是某些中断要做的事情稍微耗时,这时可以把中断拆分为上半部、下半部。
    在上半部处理紧急的事情,在上半部的处理过程中,中断是被禁止的;
    在下半部处理耗时的事情,在下半部的处理过程中,中断是使能的。
    中断上半部、下半部的关系机制,请回顾第 18.2.5节。

    19.6.1 内核函数

    19.6.1.1 定义 tasklet

    中断下半部使用结构体 tasklet_struct来表示,它在内核源码 include\linux\interrupt.h中定义: struct tasklet_struct
    {
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
    };
    其中的 state有 2位:
    ① bit0表示 TASKLET_STATE_SCHED
    等于 1时表示已经执行了 tasklet_schedule把该 tasklet放入队列了;tasklet_schedule会判断该位,如果已经等于 1那么它就不会再次把 tasklet放入队列。
    ② bit1表示 TASKLET_STATE_RUN
    等于 1时,表示正在运行 tasklet中的 func函数;函数执行完后内核会把该位清 0。
    其中的 count表示该 tasklet是否使能:等于 0表示使能了,非 0表示被禁止了。对于 count非 0的tasklet,里面的 func函数不会被执行。
    使用中断下半部之前,要先实现一个 tasklet_struct结构体,这可以用这 2个宏来定义结构体:

    #define DECLARE_TASKLET(name, func, data) \ 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } 
    #define DECLARE_TASKLET_DISABLED(name, func, data) \ 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } 
    
    • 1
    • 2
    • 3
    • 4

    使用 DECLARE_TASKLET定义的 tasklet结构体,它是使能的;
    使用 DECLARE_TASKLET_DISABLED定义的 tasklet结构体,它是禁止的;使用之前要先调用tasklet_enable使能它。
    也可以使用函数来初始化 tasklet结构体:

    extern void tasklet_init(struct tasklet_struct *t, 
        void (*func)(unsigned long), unsigned long data); 
    
    • 1
    • 2
    19.6.1.2 使能/禁止 tasklet
    static inline void tasklet_enable(struct tasklet_struct *t); 
    static inline void tasklet_disable(struct tasklet_struct *t); 
    
    • 1
    • 2

    tasklet_enable把 count增加 1;tasklet_disable把 count减 1。

    19.6.1.3 调度 tasklet
    static inline void tasklet_schedule(struct tasklet_struct *t); 
    
    • 1

    把 tasklet放入链表,并且设置它的 TASKLET_STATE_SCHED状态为 1。

    19.6.1.4
    kill tasklet 
    extern void tasklet_kill(struct tasklet_struct *t); 
    
    • 1
    • 2

    如果一个 tasklet未被调度,tasklet_kill会把它的 TASKLET_STATE_SCHED状态清 0;
    如果一个 tasklet已被调度,tasklet_kill会等待它执行完华,再把它的 TASKLET_STATE_SCHED状态清 0。
    通常在卸载驱动程序时调用 tasklet_kill。

    19.6.2

    tasklet使用方法
    先定义 tasklet,需要使用时调用 tasklet_schedule,驱动卸载前调用 tasklet_kill。
    tasklet_schedule只是把 tasklet放入内核队列,它的 func函数会在软件中断的执行过程中被调用。

    19.6.3

    tasklet内部机制
    作为初学者,可以不看本节。
    tasklet属于 TASKLET_SOFTIRQ软件中断,入口函数为 tasklet_action,这在内核 kernel\softirq.c中设置:
    在这里插入图片描述
    当驱动程序调用 tasklet_schedule时,会设置 tasklet的 state为 TASKLET_STATE_SCHED,并把它放入某个链表:
    在这里插入图片描述

    当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于 TASKLET_SOFTIRQ软件中断,会调用 tasklet_action函数。
    执行过程还是挺简单的:从队列中找到 tasklet,进行状态判断后执行 func函数,从队列中删除 tasklet。
    从这里可以看出:
    ① tasklet_schedule调度 tasklet时,其中的函数并不会立刻执行,而只是把 tasklet放入队列;
    ② 调用一次 tasklet_schedule,只会导致 tasklnet的函数被执行一次;
    ③ 如果 tasklet的函数尚未执行,多次调用 tasklet_schedule也是无效的,只会放入队列一次。
    tasklet_action函数解析如下:
    在这里插入图片描述

  • 相关阅读:
    【2023最新版】Spring Cloud面试题总结(35道题含答案解析)
    MySQL事务
    化妆品展示网页设计作业 静态HTML化妆品网站 DW美妆网站模板下载 大学生简单网页作品代码 个人网页制作 学生个人网页设计作业
    需求管理手册-对交付物的要求(12)
    mysql安装与配置及四大引擎和数据类型、建表以及约束、增删改查、常用函数、聚合函数以及合并
    vue 创建vue项目
    JPA概述
    黄菊华老师,Python毕业设计毕设辅导教程(2):Python开发准备,Window 平台安装 Python
    【mybatis】缓存
    Git在已有的项目中引入Submodule子模块管理:添加、更新、删除(实战示例代码)
  • 原文地址:https://blog.csdn.net/kingpower2018/article/details/133445467