首页 > 其他分享 >定时器-内核定时器

定时器-内核定时器

时间:2024-06-10 15:55:17浏览次数:14  
标签:__ 定时器 int 内核 key gpio include

在Linux内核中,定时器通过软件中断实现,而这个软件中断其实依赖于实际的物理定时器中断。概括来说,物理定时器会每隔一段时间发送一次中断,然后有一个全局变量jiffies就会加1,当到达某个阈值时,就会触发定时器软件中断。软件中断是在每次发生了硬件(物理)中断并处理中断后由内核去检查是否有需要执行的软件中断,软件中断处理流程可概括如下:
发生硬件中断 => 内核执行负责获取中断号等一系列操作,然后关中断,执行我们写的中断处理函数,开中断 => 检查有没有需要执行的软件中断,有就执行

在Linux内核中,通常10ms发生一次硬件定时器中断,因此对于软中断,至少10ms就有可能被执行。

1 Linux内核定时器的使用
在Linux内核中,硬件定时器中断通常10ms发生一次,每发生一次全局变量jiffies加1,因此使用内核中的定时器就是修改timer结构体中的expires,当jiffies超过设定值时会触发定时器软件中断。

在内核中使用定时器中断涉及的函数如下:

在驱动中,使用定时器的步骤如下:
(1)设置定时器处理函数,函数原型为 void (*timer_expire)(unsigned long data)
虽然函数参数是整数,但是指针本身也是一个整数,因此可以传入一个结构体指针然后在函数内部再强制类型转换,这样可以一次性传入一组有关的数据。
(2)设置定时器setup_timer(timer, fn, data)
(3)设置timer结构体中的expire变量确定定时时间
(4)使用add_timer向内核添加定时器

从上面的定时器相关函数以及使用流程,可以看到每一个软件定时器其实都通过一个struct timer_list的结构体相关联,这个结构体会被放进一个timer链表中,内核在处理软件中断时会检查这条链表上是否有超时的timer_list结构体,如果有就取出其中对应的函数执行。

总体使用还是很简单的,接下来可以使用定时器进行按键消抖
添加了定时器消抖的按键中断异步通知代码如下:

点击查看代码
/*
基于总线设备驱动模型进行驱动开发,platform_device由设备树产生,并且使用GPIO子系统和pinctr子系统
在该驱动程序中需要自己构造platform_driver结构体并且要和设备树生成的platform_device结构体匹配
1、使用循环缓冲区记录按键值
2、采用异步通知机制,当发生按键中断时通过向进程发信号,然后信号处理函数会被调用,在其中读取按键值
*/

#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm-generic/siginfo.h"
#include "asm/signal.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/fcntl.h>
#include <linux/timer.h>

static int major;   //主设备号
static struct class *key_class;
static int count;   //引脚树,也即按键数量

/*
设置按键中断时,会使用到引脚的引脚编号(老版本),引脚描述结构体,引脚中断号,引脚标志位
因此这些信息可以使用一个结构体来保存,一个引脚按键就对应一个,如果有多个按键就可以用结构体数组
按键中断本质是引脚输入电平变化引起的中断,因此如果有外设在完成工作后发出电平变化信号的话,可以使用这种中断方式
比如AD7606在完成转换后BUSY引脚会变成持续一段时间的高电平,那么可以使用这个高电平产生引脚中断然后去读取AD7606的AD转换值
*/
struct gpio_interrupt{
    int gpio_num;   //GPIO引脚编号,老版本
    struct gpio_desc *desc; //gpio引进描述结构体
    int irq;    //中断号
    int flag;
    struct timer_list key_timer;   //定时器结构体

};

struct fasync_struct *key_fasync_struct;
struct gpio_interrupt *key_gpio;    //定义一个结构体指针,在probe函数中再根据设备树中定义的引脚数量动态分配内存
#define BUF_LEN 128 //缓冲区大小
static int key_buf[BUF_LEN];
static int r, w;    //定义缓冲区写指针,r=w时表示缓冲区空,(w+1) % BUF_LEN = r时表示缓冲区满
//实际上BUF_LEN个缓存单元只能用BUF_LEN-1个,因为要判断是否满/空

#define next_position(x) (((x) + 1) % BUF_LEN)

static bool is_buf_empty(void)
{
    // printk("%s %s line %d, w = %d, r = %d\n", __FILE__, __FUNCTION__, __LINE__, w, r);
    return (r == w);
}

static bool is_buf_full(void)
{
    // printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return (r == next_position(w));
}

static void put_key_value(int key)
{
    // printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    if(!is_buf_full())
    {
        key_buf[w] = key;
        w = next_position(w);   //更新写指针
    }
    
}

static int get_key_value(void)
{
    int key;
    if(!is_buf_empty())
    {
        key = key_buf[r];
        r = next_position(r);   //更新指针
    }
    else 
    {
        key = -1;
        printk("%s %s line %d, buf is empty! failed to read\n", __FILE__, __FUNCTION__, __LINE__);    
    }
    return key;
}

int key_open(struct inode *node, struct file *file)
{
    /*打开文件时会执行,在这里可以做一些初始化操作
        设置GPIO为输入
    */
    int i;
    r = 0;
    w = 0;
    for(i = 0; i < count; i++)
    {
        gpiod_direction_input(key_gpio[i].desc);
    }
    return 0;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
    /*用户程序使用read系统调用时会调用,返回值最终会返回给用户程序中的read
    读取按键值,如果循环缓冲区有内容,那么直接读取缓冲区内容,否则休眠
    因此可以使用缓冲区是否为空作为休眠的信号
    */
    int err;
    int key;
    printk("%s %s line %d, w = %d, r = %d\n", __FILE__, __FUNCTION__, __LINE__, w, r);

    //如果buf空,is_buf_empty()返回1, !is_buf_empty()是0,那么陷入睡眠
    wait_event_interruptible(gpio_key_wait, !is_buf_empty());
    //如果没有休眠或者从休眠唤醒,说明缓冲区有内容了,读缓冲区
    key = get_key_value();
    //把按键值写入用户空间
    err = copy_to_user(buf, &key, sizeof(key));
    if(err)
    {
        printk("failed copy data to user\n");
        return -EFAULT;
    }
    return sizeof(key);
}

int key_fasync(int fd, struct file *file, int on)
{

    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);   
    if(fasync_helper(fd, file, on, &key_fasync_struct) >= 0) //在file结构体中的flag的FASYNC位发生变化时调用
    {
        return 0;
    }
    else 
    {
        return -EIO; 
    }
}

int key_close(struct inode *node, struct file *file)
{
    /*用户程序调用close时会执行*/
    int i;
    for(i = 0; i < count; i++)
    {
        gpiod_direction_output(key_gpio[i].desc, 0);
    }
    return 0;
}

struct file_operations key_opr = {
    .owner = THIS_MODULE,
    .open = key_open,
    .read = key_read,
    .fasync = key_fasync,
    .release = key_close,
};


static void key_timer_expire(unsigned long data)
{
    /*按键定时器消抖
        (1)读取按键状态并放入缓存
        (2)唤醒进程
        (3)发送信号
    */
    struct gpio_interrupt *key = (struct gpio_interrupt *)data;    //强制类型转换
    int val;
    int key_val;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    mod_timer(&(key->key_timer), ~0);   //重新设置定时器为无限长,如果没有的话,即使按键没有按下,也会每个几十ms触发一次定时器中断.实验了一下,好像不影响
    val = gpiod_get_value(key->desc);
    val ^= (1);
    key_val = (key->gpio_num << 8) | val;
    put_key_value(key_val); //把按键状态放入缓冲区
    wake_up_interruptible(&gpio_key_wait);  //唤醒进程
    kill_fasync(&key_fasync_struct, SIGIO, POLL_IN);    //发送信号
}

static irqreturn_t gpio_isr(int irq, void *dev_id)
{
    /*中断处理函数上半部
    发生中断后,会执行相应的中断处理函数,在内核中已经提供了相应的中断处理函数
    在里面会判断中断号,关中断,我们所写的中断处理函数被注册进内核后就作为内核中断
    处理函数的回调函数被调用,在里面我们可以写上我们想执行的操作
    在这里不执行读取按键值的操作,只是修改重置定时器定时时间,进行消抖
    在定时器函数中再执行按键读取操作
    */
    struct gpio_interrupt *key = dev_id;   //获取引脚信息
    mod_timer(&key->key_timer, jiffies + HZ / 20);  //定时50ms
    return IRQ_HANDLED; //返回IRQ_HANDLED表示已经处理中断,否则内核还会继续执行action链表中其他函数
}

int key_probe(struct platform_device *pdev)
{
    /*
    驱动和设备节点匹配时执行
    在这里会做一些初始化操作:获取设备树信息,设置中断,注册驱动file_operation结构体
    */
    struct device_node *node = pdev->dev.of_node;   //获取设备树中引脚对应的device_node结构体
    int err;
    int i;
    enum of_gpio_flags flag;

    count = of_gpio_named_count(node, "key-gpios"); //获取设备树节点中定义了多少个GPIO
    if(count <= 0)
    {
        printk("%s %s line %d, No available gpio, count = %d\n", __FILE__, __FUNCTION__, __LINE__, count);
        return -1;
    }
    key_gpio = kzalloc(sizeof(struct gpio_interrupt) * count, GFP_KERNEL);  //为自定义的引脚结构体分配内存
    //获取设备树中定义的节点信息
    for(i = 0; i < count; i++)
    {
        //获取引脚编号
        // key_gpio[i].gpio_num = of_get_gpio_flags(node, i, &flag);    //以后尽量使用命名式的函数
        key_gpio[i].gpio_num = of_get_named_gpio_flags(node, "key-gpios", 0, &flag);
        if(key_gpio[i].gpio_num < 0)
        {
            printk("%s %s line %d, of_get_gpio_flags failed\n", __FILE__, __FUNCTION__, __LINE__);
            return -1;
        }
        //获取引脚描述结构体
        key_gpio[i].desc = gpiod_get_index(&pdev->dev, "key", i);   //这里使用get,那么在remove函数里就需要使用put释放引脚
        key_gpio[i].flag = flag & OF_GPIO_ACTIVE_LOW;   
        key_gpio[i].irq = gpiod_to_irq(key_gpio[i].desc);   //获取对应的中断号

        /*添加定时器*/
        setup_timer(&(key_gpio[i].key_timer), key_timer_expire, (int)&key_gpio[i]);
        key_gpio[i].key_timer.expires = ~0;   //设置定时时间无限长
        add_timer(&key_gpio[i].key_timer);
    }

    //注册中断
    for(i = 0; i < count; i++)
    {
        err = request_irq(key_gpio[i].irq, gpio_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "gpio_key", &key_gpio[i]);
    }

    /*注册file_operaton结构体,创建类和设备*/
    major = register_chrdev(0, "key_interrupt", &key_opr);
    key_class = class_create(THIS_MODULE, "key_class");
    if(IS_ERR(key_class))
    {
        printk("%s %s line %d, class create failed\n", __FILE__, __FUNCTION__, __LINE__);
        unregister_chrdev(major, "key_interrupt");
        return PTR_ERR(key_class);
    }
    //创建设备节点,不需要为每个引脚都创建设备节点,所有引脚(按键)共用一个设备节点
    device_create(key_class, NULL, MKDEV(major, 0), NULL, "key");

    return 0;
}

int key_remove(struct platform_device *pdev)
{
    int i;
    /*卸载驱动时执行,在这里要撤销中断,注销驱动、类和设备节点,释放动态分配的内存*/
    //删除设备节点
    device_destroy(key_class, MKDEV(major, 0));
    //删除类
    class_destroy(key_class);
    //撤销驱动
    unregister_chrdev(major, "key_interrupt");
    //撤销中断,释放GPIO
    for(i = 0; i < count; i++)
    {
        free_irq(key_gpio[i].irq, &key_gpio[i]);
        gpiod_put(key_gpio[i].desc);
        del_timer(&key_gpio[i].key_timer);
    }
    kfree(key_gpio);   //释放内存
    return 0;
}

static const struct of_device_id keys_match_table[] = {
    {
        .compatible = "mykey_driver_interrupt"
    }
};

struct platform_driver key_driver = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "key_interrupt",
        .of_match_table = keys_match_table,
    },
};


static int __init key_drv_init(void)
{
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    err = platform_driver_register(&key_driver);
    return err;
}

static void __exit key_drv_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    platform_driver_unregister(&key_driver);
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");

使用定时器消抖后,按键按下读取按键,明显看到几乎不受抖动影响了。
2 Linux内核中定时器的机制
未完待续,后面再更

标签:__,定时器,int,内核,key,gpio,include
From: https://www.cnblogs.com/starstxg/p/18239326

相关文章

  • 【驱动】Linux内核调试之使用模块参数
    环境:处理器架构:arm64内核源码:linux-6.6.29ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope本文主要介绍内核开发中常用的模块传参手段,通过模块参数传递可以通过用户态来获取内核的一些信息,也可以通过用户态写入一些值来控制内核相关行为。一般内核开发者很喜欢使用模块传参......
  • KPTI——可以缓解“熔断” (Meltdown) 漏洞的内核新特性
    Linux内核修复办法:内核页表隔离KPTl(kernelpagetableisolation)每个进程一张页表变成两张:运行在内核态和运行在用户态时分别使用各自分离的页表Kernel页表包含了进程用户空间地址的映射和Kernel使用的内存映射用户页表仅仅包含了用户空间的内存映射以及内核跳板的......
  • Multisim555定时器电路设计仿真
       最近在完成数电课设,需要用到555定时器产生一个秒脉冲,虽然网上有很多关于555的资料,但是按照他们的进行仿真波形一直不对。自己设计的占空比有不太准确,但是其实Multisim有提供555定时器电路设计向导,填好参数会自动生成电路。下面是仿真步骤:电路的R1和R2有参数要求,需要......
  • 定时器更新界面,线程报错
    项目场景:在javafx框架下使用线程更新UI的时候,出现无法正常更新UI。问题代码如下:packageclock;importjava.util.Calendar;importjava.util.GregorianCalendar;importjava.util.Timer;importjava.util.TimerTask;importjavafx.application.Application;importj......
  • Centos7系统禁用Nouveau内核驱动程序【笔记】
    在CentOS系统中,Nouveau是开源的NVIDIA显卡驱动程序,但它与NVIDIA的官方驱动程序NVIDIAProprietaryDriver存在兼容性问题。如果你想要禁用Nouveau并使用NVIDIA官方驱动,可以按照以下步骤操作:1、创建一个黑名单文件以禁用Nouveau驱动。echo'blacklistnouveau'|sudote......
  • Linux内核链表源代码
    /*SPDX-License-Identifier:GPL-2.0*/#ifndef_LINUX_LIST_H#define_LINUX_LIST_H#include<linux/types.h>#include<linux/stddef.h>#include<linux/poison.h>#include<linux/const.h>#include<linux/kernel.h>/**Simple......
  • 基于修改iOS内核绕过iOS 基于svc 0x80的ptrace反调试
    yuzhouheike62天 看到一个帖子:[原创]绕过iOS基于svc0x80的ptrace反调试24.跟着操作了下.这篇文章的核心思想来源于[原创]iOS内核修改之过某音等PT_DENY_ATTACH反动态ptrace调试我的设备是:iphone7iOS14.1,DarwinKernelVersion20.0.0:WedSep3003:24:41......
  • C51学习归纳5 --- 定时器
        个人觉得定时器是一个十分重要的内容,这东西忘简单说其实就是一个计数器,从设定的初始值开始加一操作,每当计数器满了,引发一次计数器中断,芯片的CPU过来进行中断处理,然后一直重复这样的过程。替代长时间的Delay,提高CPU的运行效率和处理速度。    首先,我们来考......
  • 内核线程被调度执行的时候需要一个地址空间,这个地址空间是从哪里来的
    内核线程被调度执行时确实需要一个地址空间,但这个地址空间并不是为每个内核线程独立创建的。内核线程运行在操作系统的内核空间中,而不是在用户空间。以下是内核线程执行时地址空间的来源和管理方式:地址空间来源共享内核地址空间:所有内核线程共享内核地址空间,这包括内核代码......
  • 内核线程为什么没有地址空间
    内核线程没有独立的地址空间,这是因为内核线程是在操作系统内核空间中运行的,内核空间本身是所有进程共享的。以下是一些更详细的解释:内核与用户态的区别:操作系统通常将内存分为用户空间和内核空间。用户空间是为用户进程提供的,它们有各自的虚拟地址空间,相互之间隔离,不能直接访问......