10_linux内核定时器实验

一、linux时间管理和内核定时器简介

1、内核时间管理简介

1)宏HZ

​ 硬件定时器产生的周期性中断,中断频率就是系统频率(拍率)。系统拍率可以设置,单位是HZ,可在编译内核时通过图形化界面设置,设置路径如下:Kernel Features -> Timer frequency([=y])

​ 配置完以后,可在内核源码根目录下的 .config 文件找到 CONFIG_HZ 的值为所设置的系统频率。而文件 include/asm-generic/param.h 中的宏:

#define HZ 		CONFIG_HZ

​ 在编译驱动时经常使用 HZ 宏来设置定时器的参数。

2)jiffies

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

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

​ 其中 jiffies_64 和 jiffies 是同一个东西,jiffies_64 用于 64 位系统,jiffies 用于 32 位系统。jiffies 是 jiffies_64 的的低32位。jiffies 会有溢出的风险,当 jiffies 溢出后就会从 0 开始极数(绕回)。假如 HZ 为 1000,则 49.7 天就会发生绕回,linux 内核提供了 API 来处理绕回:

函数 描述(unkown 通常为 jiffies,known 通常是需要对比的值。)
time_after(unkown, known) 如果 unkown 超过 known,返回真,否则返回假
time_before(unkown, known) 如果 unkown 没超过 known,返回真,否则返回假
time_after_eq(unkown, known) 和 time_after 一样,多了判断等于
time_before_eq(unkown, known) 和 time_before 一样,多了判断等于
	判断一段代码执行时间有没有超时:
unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点2S */

/*************************************
具体的代码
************************************/

 /* 判断有没有超时 */
if(time_before(jiffies, timeout)) {
/* 超时发生 */
} else {
/* 超时未发生 */
}

​ linux 内核提供了 jiffies 和 ms、us、ns 之间的转换函数

函数 描述
int jiffies_to_msecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的毫秒
int jiffies_to_usecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的微秒
u64 jiffies_to_nsecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的纳秒
long msecs_to_jiffies(const unsigned int m) 将毫秒转换为 jiffies 类型
long usecs_to_jiffies(const unsigned int u) 将微秒转换为 jiffies 类型
unsigned long nsecs_to_jiffies(u64 n) 将纳秒转换为 jiffies 类型

2、内核定时器简介

1)timer_list 结构体

​ 使用内核定时器,不需要设置寄存器,内核提供了定时器结构体和 API 函数。内核使用 timer_list 结构体来表示内核定时器,timer_list 定义在 include/linux/timer.h 中:

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

2)定时器API函数

① init_timer函数

​ init_timer 函数用于初始化 timer_list 类型变量,函数原型:

void init_timer(struct timer_list *timer)

timer:要初始化的定时器。

② add_timer函数

​ add_timer 函数用于向 Linux内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:

void add_timer(struct timer_list *timer)

timer:要注册的定时器。

注意:一般重复启动定时器推荐使用 mod_timer

③ del_timer函数

​ del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。函数原型如下:

int del_timer(struct timer_list * timer)

timer:要删除的定时器。

返回值:0,定时器还没被激活; 1,定时器已经激活。

④ del_timer_sync函数

​ del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。函数原型如下所示:

int del_timer_sync(struct timer_list *timer)

timer:要删除的定时器。

返回值:0,定时器还没被激活; 1,定时器已经激活。

⑤ mod_timer函数

​ mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器。函数原型如下:

int mod_timer(struct timer_list *timer, unsigned long expires)

timer:要删除的定时器。

expires:超时时间。

返回值:0,调用 mod_timer函数前定时器未被激活; 1,调用 mod_timer函数前定时器已激活。

3、linux 内核短延时函数

​ Linux内核提供了毫秒、微秒和纳秒延时函数,这三个函数如表:

函数 描述
void ndelay(unsigned long nsecs) 纳秒延时函数
void udelay(unsigned long usecs) 微秒延时函数
void mdelay(unsigned long mseces) 毫秒延时函数

二、内核定时器实验

1、添加设备树节点

1)添加设备节点

​ 实验使用定时器控制 led 亮灭,在根节点下创建设备节点:

gpioled {
		compatible = "gpioled_test";
		status = "okay";
		pinctrl-names = "default";
		pinctrl-0 = <&gpioled>;
		gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
	};

2)添加pinctrl节点

​ 在 iomuxc 节点下的子节点 imx6ul-evk 中添加 pinctrl 节点:

gpioled: ledgrp {
			fsl,pins = <
				MX6UL_PAD_GPIO1_IO03__GPIO1_IO03   0x10b0	
			>;
		};

2、添加设备结构体

/* 设备结构体 */
struct timer_dev {
	dev_t devid;	//设备号
	int major;		//主设备号
	int minor;		//次设备号
	struct cdev cdev;	//字符设备
	struct class *class;	//类
	struct device *device;	//设备
	struct spinlock	lock;	//自旋锁
	int gpioled;		//led gpio编号
	struct device_node *led_nd;	//led节点
	struct timer_list timer;	//定时器
	unsigned long expires;	//定时时间
};

struct timer_dev timer;

3、编写加载和卸载注册函数

​ 加载和卸载注册函数如下:

module_init(timer_init);
module_exit(timer_exit);

入口函数:

/* 入口函数 */
static int __init timer_init(void)
{
	int ret = 0;
	spin_lock_init(&timer.lock);

	/* 设备号处理 */
	timer.major = 0;
	if(timer.major){	//指定了主设备号
		timer.devid = MKDEV(timer.major, 0);
		ret = register_chrdev_region(timer.devid, DEVICE_CNT, DEVICE_NAME);
		if(ret < 0){
			return -EINVAL;
		}
	}else{		//没有指定设备号
		ret = alloc_chrdev_region(&timer.devid, 0, DEVICE_CNT, DEVICE_NAME);
		if(ret < 0){
			return -EINVAL;
		}
		timer.major = MAJOR(timer.devid);
		timer.minor = MINOR(timer.devid);
		printk("major is: %d\r\nmainr is: %d\r\n",timer.major, timer.minor);
	}

	/* 注册字符设备 */
	timer.cdev.owner = THIS_MODULE;
	cdev_init(&timer.cdev, &timer_fops);
	ret = cdev_add(&timer.cdev, timer.devid, DEVICE_CNT);
	if(ret < 0){
		ret = -EINVAL;
		goto fail_cdev;
	}

	/* 自动创建节点 */
	timer.class = NULL;
	timer.device = NULL;
	timer.class = class_create(THIS_MODULE, DEVICE_NAME);
	if(timer.class == NULL){
		ret = -EINVAL;
		goto fail_create_class;
	}
	timer.device = device_create(timer.class, NULL, timer.devid, NULL, DEVICE_NAME);
	if(timer.device == NULL){
		ret = -EINVAL;
		goto fail_create_device;
	}

	return 0;

fail_cdev:
	unregister_chrdev_region(timer.devid, DEVICE_CNT);
fail_create_class:
	cdev_del(&timer.cdev);
	unregister_chrdev_region(timer.devid, DEVICE_CNT);
fail_create_device:
	class_destroy(timer.class);
	cdev_del(&timer.cdev);
	unregister_chrdev_region(timer.devid, DEVICE_CNT);

	return ret;
}

出口函数:

​ 如果激活了定时器,在卸载模块时,一定要先删除定时器,否则将无法卸载模块。

/* 出口函数 */
static void __exit timer_exit(void)
{
	led_switch(LED_OFF);
	gpio_free(timer.gpioled);			//注销led gpio
	del_timer_sync(&timer.timer);		//删除定时器
	device_destroy(timer.class, timer.devid);	//摧毁设备
	class_destroy(timer.class);			//摧毁类
	cdev_del(&timer.cdev);				//删除字符设备
	unregister_chrdev_region(timer.devid, DEVICE_CNT);	//注销设备号
}

4、编写led gpio初始化函数

​ 将 led gpio 函数放在 open 函数里。

/* gpio初始化函数 */
static int gpio_init(void)
{
	/* 获取设备树节点 */
	timer.led_nd = of_find_node_by_name(NULL, "gpioled");
	if(timer.led_nd == NULL){
		printk("fail to find led_nd\r\n");
		return -EINVAL;
	}
	/* 获取gpio编号 */
	timer.gpioled = of_get_named_gpio(timer.led_nd, "gpios", 0);
	if(timer.gpioled < 0){
		printk("fail to find led_nd\r\n");
		return -EINVAL;
	}
	printk("gpio num: %d\r\n",timer.gpioled);
	/* 设置gpio */
	gpio_request(timer.gpioled,"led");
	gpio_direction_output(timer.gpioled, 0);
	printk("led init\r\n");

	return 0;
}

5、编写led状态切换函数

/* LED状态切换函数 */
void led_switch(int led_state)
{
	if(led_state == LED_ON){
		gpio_set_value(gpioled.led_gpio, 0);	//使用gpio子系统的API函数
	}else{
		gpio_set_value(gpioled.led_gpio, 1);
	}
}

6、编写定时器初始化函数

/* 定时器初始化函数 */
void Timer_Init(void)
{
    unsigned long flags;
    
	init_timer(&timer.timer);					//初始化定时器
	timer.timer.function = timer_function;		 //注册定时回调函数
	timer.timer.data = (unsigned long)&timer;	 //将设备结构体作为回调函数的传入参数
    spin_lock_irqsave(&timer.lock, flags);		 //上锁
	timer.timer.expires = jiffies + 2 * HZ;		 //设置超时时间 2S,给add_timer用
    timer.expires = timer.timer.expires;		 //回调函数中下周期的定时时间
    spin_unlock_irqrestore(&timer.lock, flags);	  //解锁
}

7、编写定时回调函数

​ linux 内核的定时器启动后只会运行一次,如果要连续定时,需要在回调函数中重新启动定时器。

/* 定时器回调函数 */
void timer_function(unsigned long arg)
{
	unsigned long flags;
	static int led_state = 1;
	struct timer_dev *dev = (struct timer_dev *)arg;
	int time_period = 0;

	led_switch(led_state = !led_state);		//切换led状态
	printk("\r\nled state : %d\r\n", led_state);
	/* 重启定时器 */
	spin_lock_irqsave(&dev->lock, flags);
	time_period = dev->expires;
	spin_unlock_irqrestore(&dev->lock, flags);
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(time_period));	//设置定时时间,启动定时器
}

8、编写设备的具体操作函数

/* open函数 */
static int timer_open(struct inode *inode, struct file *filp)
{
	int ret = 0;
	filp->private_data = &timer;	//设置私有化数据

	ret = gpio_init();	 //初始化 led gpio
	Timer_Init();		//初始化定时器

	return ret;
}

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


	printk("cmd = %x   arg = %ld\r\n",cmd, arg);
	switch(cmd){
		case TIMER_OPEN:
			add_timer(&dev->timer);		//注册并启动定时器
		break;
		case TIMER_CLOSE:
			del_timer_sync(&dev->timer);		//删除定时器
		break;
		case TIMER_SET:
			spin_lock_irqsave(&dev->lock, flags);		//上锁
			dev->expires = arg;
			spin_unlock_irqrestore(&dev->lock, flags);		//解锁
			mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));	//设置定时时间,启动定时器
		break;
		default:
		break;
	}
	return 0;
}

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

4、添加头文件

​ 参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
​ man 命令数字含义:1:标准命令 2:系统调用 3:库函数
​ 添加以下头文件:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEVICE_CNT 1
#define DEVICE_NAME "timer"
#define LED_ON 1
#define LED_OFF 0
#define TIMER_OPEN 	0X0F
#define TIMER_CLOSE 0X1F
#define TIMER_SET   0XFF

5、添加 License 和作者信息

​ 驱动的 License 是必须的,缺少的话会报错,在文件最末端添加以下代码:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzk");

三、编写测试应用程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "linux/ioctl.h"

#define TIMER_OPEN 	0X0F
#define TIMER_CLOSE 0X1F
#define TIMER_SET   0XFF

int main(int argc, char *argv[])
{
    int fd = 0;
    int ret = 0;
    u_int32_t cmd = 0;
    u_int32_t arg = 0;
    char *filename;
    unsigned char str[100];
    
    if(argc != 2){
        printf("missing parameter!\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd < 0){
        printf("open file %s failed\r\n", filename);
        return -1;
    }

    while(1){
        printf("Input CMD:");
        scanf("%d", &cmd);

        if(cmd == 1)			//命令1,打开定时器,按照初始设定闪灯
            cmd = TIMER_OPEN;
        if(cmd == 2)			//命令2,关闭定时器
            cmd = TIMER_CLOSE;
        if(cmd == 3){			//命令3,设置定时时间
            cmd = TIMER_SET;
            printf("input time:");
            scanf("%d", &arg);
        }
        ioctl(fd,cmd,arg);		//将命令传给内核空间
    }


    ret = close(fd);
    if(ret < 0) //返回值小于0关闭文件失败
    {
        printf("close file %s failed\r\n", filename);
        return -1;
    }

    return 0;
}

你可能感兴趣的:(linux驱动开发)