嵌入式Linux(11):Liunx内核定时器

1、内核时间管理

UCOS 或 FreeRTOS 是需要一个硬件定时器提供系统时钟,一般使用 Systick 作为系统时钟源。同理, Linux 要运行,也是需要一个系统时钟的。

硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate),比如 1000Hz, 100Hz 说的就是系统节拍率(HZ 表示一秒的节拍数)。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率。

-> Kernel Features
	-> Timer frequency (<choice> [=y])
  • 高节拍率和低节拍率的优缺点:
    ①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
    ②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。

Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0, jiffies 定义在文件 include/linux/jiffies.h 中

76 extern u64 __jiffy_data jiffies_64;
77 extern unsigned long volatile __jiffy_data jiffies;

jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统, jiffies 其实就是 jiffies_64 的低 32 位。

  • HZ 表示每秒的节拍数, jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。溢出风险,溢出以后会重新从 0 开始计数,API 函数来处理绕回。在这里插入图片描述
    如果 unkown 超过 known 的话, time_after 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。 time_after_eq 函数和 time_after 函数类似。
使用 jiffies 判断超时
1 unsigned long timeout;
2 timeout = jiffies + (2 * HZ); /* 超时的时间点 */
3 
4/*************************************
5 具体的代码
6 ************************************/
7
8/* 判断有没有超时 */
9 if(time_before(jiffies, timeout)) {
10 		/* 超时未发生 */
11 } else {
12		 /* 超时发生 */
13 }

timeout 就是超时时间点,比如我们要判断代码执行时间是不是超过了 2 秒,那么超时时间点就是 jiffies+(2*HZ),如果 jiffies 大于 timeout 那就表示超时了,否则就是没有超时。
在这里插入图片描述

2、内核定时器

Linux 内核定时器只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,不需要做一大堆的寄存器初始化工作。软件定时器

在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。 Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件include/linux/timer.h 中

                            timer_list 结构体
struct timer_list {
	struct list_head entry;
	unsigned long expires; /* 定时器超时时间,单位是节拍数 */
	struct tvec_base *base;
	void (*function)(unsigned long); /* 定时处理函数 */
	unsigned long data; /* 要传递给 function 函数的参数 */
	int slack;
};

API函数

定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器

1、 init_timer 函数

  • init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化。 init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
	函数参数和返回值含义如下:
	timer:要初始化定时器。
	返回值: 没有返回值。

2、 add_timer 函数

  • add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
	函数参数和返回值含义如下:
	timer:要注册的定时器。
	返回值: 没有返回值。

3、 del_timer 函数

  • del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。 del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
	函数参数和返回值含义如下:
	timer:要删除的定时器。
	返回值: 0,定时器还没被激活; 1,定时器已经激活。

4、 del_timer_sync 函数

  • del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。 del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
	函数参数和返回值含义如下:
	timer:要删除的定时器。
	返回值: 0,定时器还没被激活; 1,定时器已经激活。

5、 mod_timer 函数

  • mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
	函数参数和返回值含义如下:
	timer:要修改超时时间(定时值)的定时器。
	expires:修改后的超时时间。
	返回值: 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活。
1 struct timer_list timer; /* 定义定时器 */
2 
3/* 定时器回调函数 */
4 void function(unsigned long arg)
5 {
6 /*
7 * 定时器处理代码
8 */
9
10 /* 如果需要定时器周期性运行的话就使用 mod_timer
11 * 函数重新设置超时值并且启动定时器。
12 */
13 		mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
14 }
15
16 /* 初始化函数 */
17 void init(void)
18 {
19  	init_timer(&timer); /* 初始化定时器 */
20
21 		timer.function = function; /* 设置定时处理函数 */
22 		timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
23 		timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
24
25 		add_timer(&timer); /* 启动定时器 */
26 }
27
28 /* 退出函数 */
29 void exit(void)
30 {
31 		del_timer(&timer); /* 删除定时器 */
32		 /* 或者使用 */
33 		del_timer_sync(&timer);
34 }

在这里插入图片描述

3、实现

通过设置一个定时器来实现周期性的闪烁 LED 灯:

  • 使用内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周期由内核定时器来设置,测试应用程序可以控制内核定时器周期。

驱动程序编写

新建名为“12_timer”的文件夹,然后在 12_timer 文件夹里面创建 vscode 工程,工作区命名为“timer”。工程创建好以后新建 timer.c 文件。
在这里插入图片描述
驱动代码段:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/irq.h>

#define TIMER_CNT           1          /*设备号个数*/
#define TIMER_NAME       "timer"       /*设备名字*/
#define CLOSE_CMD     (_IO(0XEF,0x1))  /*关闭定时器*/
#define OPEN_CMD      (_IO(0XEF,0x2))  /*打开定时器*/
#define SETPERIOD_CMD (_IO(0XEF,0x3))  /*设置定时器周期命令*/
#define LEDOFF           0             /*关灯*/
#define LEDON            1             /*开灯*/

/*timer设备结构体*/
struct timer_dev{
    dev_t devid;            /*设备号*/
    struct cdev cdev;       /*cdev*/
    struct class *class;    /*lei*/
    struct device *device;  /*设备*/
    int major;              /*主设备号*/
    int minor;              /*次设备号*/
    struct device_node * nd; /*设备节点*/
    int led_gpio;            /*led所使用的gpio编号*/
    int timerperiod;          /*定时周期,单位为ms*/
    struct timer_list timer; /*定义一个定时器*/
    spinlock_t lock;           /*定义自旋锁*/
};

struct timer_dev timerdev;    /*定义timer设备*/

/*初始化 LED 灯 IO, open 函数打开驱动的时候初始化 LED 灯所使用的 GPIO 引脚。*/
static int led_init(void)
{
    int ret=0;

    /*1、获取设备节点:gpioled*/
    timerdev.nd=of_find_node_by_path("/gpioled");
    if(timerdev.nd == NULL){
        printk("gpioled node can not found!\r\n");
        return  -EFAULT;
    }else{
        printk("gpioled node has been found!\r\n");
    }

    /*2、获取设备树中的gpio属性,得到LED所使用的LED编号*/
    timerdev.led_gpio=of_get_named_gpio(timerdev.nd,"led-gpio",0);
    if(timerdev.led_gpio<0){
        printk("can't get led-gpio");
        return -EFAULT;
    }
    printk("led-gpio num= %d\r\n",timerdev.led_gpio);

    /*3、申请IO*/
    ret = gpio_request(timerdev.led_gpio,"led");
    if(ret){
        printk("Failed to request the led\r\n");
        return -EFAULT;
    }

    /* 4、设置GPIO_IO03为输出,并且输出高电平,默认关闭LED */
    ret = gpio_direction_output(timerdev.led_gpio,1);
    if(ret < 0) {
        printk("can't set gpio!\r\n");
    }
    return 0;
}

/*打开设备*/
static int timer_open(struct inode *inode, struct file *filp)
{
    int ret = 0;
    filp->private_data = &timerdev;           /* 设置私有数据 */

    timerdev.timerperiod = 1000;               /*默认周期1s*/ 
    ret=led_init();                             /*初始化LED IO*/
    if(ret<0){
        return ret;
    }
    return 0;
}

/*ioctl 函数,filp : 要打开的设备文件(文件描述符),
cmd : 应用程序发送过来的命令
arg : 参数
return : 0 成功;其他 失败*/
static long timer_unlocked_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
    struct timer_dev *dev=(struct timer_dev*)filp->private_data;
    int timerperiod;
    unsigned long flags;

    switch(cmd){
        case CLOSE_CMD:   /*关闭定时器*/
            del_timer_sync(&dev->timer);
            break;

        case OPEN_CMD:      /*打开定时器*/
            spin_lock_irqsave(&dev->lock,flags);
            timerperiod=dev->timerperiod;
            spin_unlock_irqrestore(&dev->lock,flags);
            mod_timer(&dev->timer,jiffies+msecs_to_jiffies(timerperiod));
            break;

        case SETPERIOD_CMD:       /*设置定时器周期*/
            spin_lock_irqsave(&dev->lock,flags);
            dev->timerperiod=arg;
            spin_unlock_irqrestore(&dev->lock,flags);
            mod_timer(&dev->timer,jiffies+msecs_to_jiffies(arg));
            break;
        default:
            break;
    }
    return 0;
}

/* 设备操作函数 */
static struct file_operations timer_fops={
    .owner=THIS_MODULE,
    .open=timer_open,
    .unlocked_ioctl=timer_unlocked_ioctl,
};

/*定时器回调处理函数*/
void timer_function(unsigned long arg)
{
    struct timer_dev *dev = (struct timer_dev *)arg;
    static int sta=1;
    int  timerperiod;
    unsigned long flags;

    sta = !sta; /* 每次都取反,实现 LED 灯反转 */
    gpio_set_value(dev->led_gpio,sta);

    /*重启定时器*/
    spin_lock_irqsave(&dev->lock,flags);
    timerperiod=dev->timerperiod;
    spin_unlock_irqrestore(&dev->lock,flags);
    mod_timer(&dev->timer,jiffies+msecs_to_jiffies(dev->timerperiod));
}

/*模块入口函数*/
static int __init timer_init(void)
{
    /*初始化自旋锁*/
    spin_lock_init(&timerdev.lock);
 
     /*注册字符设备驱动*/
    /*1、注册设备号*/
    if(timerdev.major){            /*定义设备号*/
    timerdev.devid=MKDEV(timerdev.major,0);
    register_chrdev_region(timerdev.devid,TIMER_CNT,TIMER_NAME);
    }else{                         /*没有定义设备*/
        alloc_chrdev_region(&timerdev.devid,0,TIMER_CNT,TIMER_NAME);  /*申请设备号*/
        timerdev.major=MAJOR(timerdev.devid);                            /* 获取分配号的主设备号 */
        timerdev.minor=MINOR(timerdev.devid);
    }
    printk("timerdev major=%d, minor=%d\r\n",timerdev.major,timerdev.minor);

    /*2、初始化cdev*/
    timerdev.cdev.owner=THIS_MODULE;
    cdev_init(&timerdev.cdev,&timer_fops);

    /*3、添加一个cdev*/
    cdev_add(&timerdev.cdev,timerdev.devid,TIMER_CNT);

    /*4、创建类*/
    timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
    if (IS_ERR(timerdev.class)) {
        return PTR_ERR(timerdev.class);
    }

    /*5、创建设备*/
    timerdev.device = device_create(timerdev.class, NULL, timerdev.devid,NULL, TIMER_NAME);
    if (IS_ERR(timerdev.device)) {
        return PTR_ERR(timerdev.device);
    }
    /*6、初始化timer,设置定时器处理函数,还未设置周期,所以不会激活定时器*/
    init_timer(&timerdev.timer);
    timerdev.timer.function=timer_function;
    timerdev.timer.data=(unsigned long)&timerdev;
    return 0;
}


/*驱动出口*/
static void __exit timer_exit(void)
{
     gpio_set_value(timerdev.led_gpio,1);  /* 卸载驱动的时候关闭 LED */
     del_timer_sync(&timerdev.timer);        /*删除timer*/
    
    /* 注销字符设备驱动 */
    cdev_del(&timerdev.cdev);                             /* 删除 cdev */
    unregister_chrdev_region(timerdev.devid, TIMER_CNT); /*注销设备号*/

    device_destroy(timerdev.class, timerdev.devid);
    class_destroy(timerdev.class);

    /*释放IO*/
    gpio_free(timerdev.led_gpio);

}

/*注册模块入口和出口*/
module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bruce");
  • 第 31~43行,定时器设备结构体,在 41 行定义了一个定时器成员变量 timer。
  • 第 48~82行, LED 灯初始化函数,从设备树中获取 LED 灯信息,然后初始化相应的 IO。
  • 第 85~96 行,函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开/dev/timer 驱动文件的时候此函数就会执行。此函数设置文件私有数据为 timerdev,并且初始化定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。
  • 第 103~131 行,函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数: filp, cmd 和 arg,其中 filp是对应的设备文件, cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数,在例程中 arg 参数表示定时周期。
    一共有三种命令 CLOSE_CMD, OPEN_CMD 和 SETPERIOD_CMD,这三个命令分别为关闭定时器、打开定时器、设置定时周期。这三个命令的左右如下:
    • CLOSE_CMD: 关闭定时器命令, 调用 del_timer_sync 函数关闭定时器。
    • OPEN_CMD:打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的timeperiod 成员变量,定时周期默认是 1 秒。
    • SETPERIOD_CMD:设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的timeperiod 成员变量为 arg 所表示定时周期指。并且使用 mod_timer 重新打开定时器,使定时器以新的周期运行。
  • 第 134~138 行,定时器驱动操作函数集 timer_fops。
  • 第 141~157行,函数 timer_function,定时器服务函数,此函有一个参数 arg,在本例程中arg 参数就是 timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。当定时周期到了以后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。因为内核定时器不是循环的定时器,执行一次以后就结束了,因此在 156行又调用了mod_timer 函数重新开启定时器。
  • 第 160~ 200行,函数 timer_init,驱动入口函数。在第197行初始化定时器,设置定时器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev的地址。在此函数中并没有调用 timer_add 函数来开启定时器,因此定时器默认是关闭的,除非应用程序发送打开命令。
  • 第 204~219 行,驱动出口函数,在 206 行关闭 LED,也就是卸载驱动以后 LED 处于熄灭状态。第207 行调用 del_timer_sync 函数删除定时器,也可以使用 del_timer 函数。

测试App

实现的内容如下:

  • ①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
  • ②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"

/* 使用方法 : ./timertest /dev/timer 打开测试 */

/*命令值*/
#define CLOSE_CMD              (_IO(0xEF,0x1))   /*关闭定时器*/
#define OPEN_CMD               (_IO(0xEF,0x2))   /*打开定时器*/
#define SETPERIOD_CMD          (_IO(0xEF,0x3))   /*设置定时器周期命令*/

int main(int argc, char** argv[])
{
    int fd,ret;
    char * filename;
    unsigned int cmd;
    unsigned int arg;
    unsigned char str[100];

    if(argc!=2){
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /*打开led驱动*/
    fd=open(filename,O_RDWR);
    if(fd<0){
        printf("file %s open failed!\r\n",argv[1]);
        return -1;
    }

    /*向/dev/led文件写入数据*/
    while(1){
        printf("Input CMD:");
        ret=scanf("%d",&cmd);
        if (ret!=1){
            gets(str);  /*参数输入错误、防止卡死*/
        }
        
        if(cmd==1)
            cmd=CLOSE_CMD;
        else if (cmd==2)
            cmd=OPEN_CMD;
        else if(cmd==3){
            cmd=SETPERIOD_CMD;
            printf("Input Timer Period: ");
            ret=scanf("%d",&arg);
            if (ret!=1){
                gets(str);
            }
        }
        ioctl(fd,cmd,arg);  /*控制定时器的打开和关闭*/
    }
    close(fd);/*关闭文件*/
}
  • while(1)循环,让用户输入要测试的命令,然后通过第59行的 ioctl 函数发送给驱动程序。如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用户输入的周期值。

运行测试

编译驱动程序和测试 APP

  • 将Makefile文件的 obj-m 变量的值改为 timer.o
  • make命令编译成功后会生成一个名为“ timer.ko”的驱动模块文件
  • 输入arm-linux-gnueabihf-gcc timerApp.c -o timerApp命令编译测试 timerApp.c 程序,生成 timerApp

运行测试

  • 将编译出来的 timer.ko 和 timerApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录 lib/modules/4.1.15 中,输入如下命令加载 timer.ko 驱动模
    块:insmod timer.ko

  • 输入./timerApp /dev/timer命令终端提示输入命令在这里插入图片描述

  • 输入“2”,打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。在输入“3”来设置定时周期,根据提示输入要设置的周期值在这里插入图片描述

  • 输入“500”,表示设置定时器周期值为 500ms,设置好以后 LED 灯就会以 500ms 为间隔,开始闪烁。最后可以通过输入“1”来关闭定时器

  • 如果要卸载驱动的话输入如下命令即可:rmmod timer.ko

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值