【嵌入式Linux驱动开发】十四、了解Linux内核定时器使用流程,实现LED闪烁

   致敬英雄!


一、Linux内核定时器初探

1.1、图形界面配置系统节拍率

  中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate),单位是 Hz。系统节拍率是可以设置的,在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率。

  • 进入Linux内核源码目录,终端输入make menuconfig,依次选择Kernel Features -> Timer frequency,切换到100Hz,按下空格,进行选中!

在这里插入图片描述

  • 设置好之后,保存退出。在内核源码根目录,查看.cofig文件内容,可以看到有如下宏定义!

在这里插入图片描述

1.2、重要全局变量jiffies

  在上一步,我们采用了 100Hz 的节拍率,这样时间精度就是 10ms。不管是 32 位的系统还是 64 位系统,都可以使用 jiffies来记录系统从启动以来的系统节拍数。(初始化默认为0)

   100HZ 表示1秒有100个节拍, jiffies 表示系统运行的总节拍数。那么后者除以前者,即可得到系统的运行时间。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此该现象称之为绕回现象。处理 jiffies 的绕回显得尤为重要,Linux 内核提供了如下表所示的几个 API 函数来处理绕回。

函数 描述
time_after(unkown, known) unkown > kown,返回真
time_before(unkown, known) unkown < kown,返回真
time_after_eq(unkown, known) unkown ≥ kown,返回真
time_before_eq(unkown, known) unkown ≤ kown,返回真

注:表中的unkown 通常为 jiffies, known 通常是需要对比的值。

为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数,如下表

函数 描述
int jiffies_to_msecs(const unsigned long j) jiffies转化为对应的ms
int jiffies_to_usecs(const unsigned long j) jiffies转化为对应的us
u64 jiffies_to_nsecs(const unsigned long j) jiffies转化为对应的ns
long msecs_to_jiffies(const unsigned int m) ms转化为对应的jiffies
long usecs_to_jiffies(const unsigned int u) us转化为对应的jiffies
unsigned long nsecs_to_jiffies(u64 n) ns转化为对应的jiffies

这里再补充一下Linux 内核短延时函数

函数 描述
void ndelay(unsigned long nsecs) ns延时
void udelay(unsigned long usecs) us延时
void mdelay(unsigned long mseces) ms延时
1.3、内核定时器中断

   Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行。要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器

  Linux 内核使用 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;
};

比如我们要定义一个周期为2s的定时器,那么expires = jiffies + msecs_to_jiffies(timerperiod)

定时器相关API函数

函数 描述
init_timer 初始化 timer_list 类型变量
add_timer 向 Linux 内核注册定时器
del_timer 删除一个定时器(不管有没有激活,立即删除)(不常用)
del_timer_sync 使用完定时器再删除,不能使用在中断上下文
mod_timer 修改定时值(会激活定时器,一般放到中断函数尾,用于周期定时)

使用流程

struct timer_list timer; /* 定义定时器 */

/* 定时器回调函数 */
void function(unsigned long arg)
{
/*
 * 定时器处理代码
 */
 
/* 如果需要定时器周期性运行的话就使用 mod_timer
 * 函数重新设置超时值并且启动定时器。
 */
	mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
}

/* 初始化函数 */
void init(void)
{
	init_timer(&timer); /* 初始化定时器 */

	timer.function = function; /* 设置定时处理函数 */
	timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
	timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */

	add_timer(&timer); /* 启动定时器 */
}

/* 退出函数 */
void exit(void)
{
	del_timer(&timer); /* 删除定时器 */
	/* 或者使用 */
	del_timer_sync(&timer);
}
1.4、ioctl 简单介绍

  ioctl 系统调用主要用于增加系统调用的硬件控制能力,它可以构建自己的命令,也能接受参数。通过 ioctl 控制硬件 I/O,必须在驱动中为 ioctl()系统调用设计一些控制命令,通过不同的命令实现不同的硬件控制。更加深入研究,可参考<这里>。

1.4.1 应用程序 ioctl 函数

  用户空间的 ioctl 函数原型如下所示,

int ioctl (int fd, unsigned long cmd, ...)
  • fd 是被打开的设备文件, cmd 是操作设备的命令,“ …”代表可变数目的参数表,通常用 char *argp 来定义,如果 cmd 命令不需要参数,则传入 NULL 即可。
1.4.2 驱动程序 ioctl 函数

  内核空间 iotcl 函数原型如下所示,定义的 ioctl 命令通过 cmd 传递,数据通过 arg 传递。驱动得到 cmd 命令和 arg 参数后,须首先用解析 ioctl 命令的宏定义对命令和参数进行解析判断,没有问题再进行后续处理。

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
  • filp 表示文件描述符,cmd表示命令,arg表示与命令相关的参数,至于参数具体表达什么含义,完全由驱动编写者来定义。
1.4.3 ioctl 命令构成

  ioctl 操作与硬件平台相关,使用 ioctl 的驱动需要包含<linux/ioctl.h>文件。每个 ioctl 命令cmd实际上都是一个 32 位整型数,各字段和含义如下表所示。

在这里插入图片描述
  例如,0x82187201,它的二进制如下表所示。所以含义为:读:_IOR;参数长度536;幻数114,ASCII为r,功能号1.

字段 31~30 29~16 15~8 7~0
二进制 10 00 0010 0001 1000 0111 0010 0000 0001

  实际上这个命令是<linux/msdos_fs.h>中的 VFAT_IOCTL_READDIR_BOTH 命令:#define VFAT_IOCTL_READDIR_BOTH _IOR('r', 1, struct __fat_dirent[2])

1.4.4 构造ioctl命令

  为驱动构造 ioctl 命令,首先要为驱动选择一个可用的幻数作为驱动的特征码,以区分不同驱动的命令。内核已经使用了很多幻数,为了防止冲突,最好不要再使用这些系统已经占用的幻数来作为驱动的特征码。已经被使用的幻数列表详见内核源码目录Documentation/ioctl/ioctl-number.txt文件。
  在不同平台上,幻数所使用情况都不同,为防止冲突,可以选择其它平台使用的幻数来用。选定幻数后,可以这样来进行定义:

#define LED_IOC_MAGIC 'Z'

  ioctl 命令字段的 bit[31:30]表示命令的方向,分别表示使用_IO、 _IOW、 _IOR 和_IOWR
这几个宏定义,分别用于构造不同的命令,具体见下表:

命令 描述
_IO(type,nr) 构造无参数的命令编号
_IOW(type,nr,size) 构造往驱动写入数据的命令编号
_IOR(type,nr,size) 构造从驱动中读取数据的命令编号
_IOWR(type,nr,size) 构造双向传输的命令编号

  其中, type 是幻数, nr 是功能号, size 是数据大小。

  例如,为 LED 驱动构造 ioctl 命令,由于控制 LED 无需数据传输,可以这样定义:

#define SET_LED_ON _IO(LED_IOC_MAGIC, 0)
#define SET_LED_OFF _IO(LED_IOC_MAGIC, 1)

  例如,如果想在 ioctl 中往驱动写入一个 int 型的数据,可以这样定义:

#define CHAR_WRITE_DATA _IOW(CHAR_IOC_MAGIC, 2, int)

  例如,要从驱动中读取 int 型的数据,则定义为:

#define CHAR_READ_DATA _IOR(CHAR_IOC_MAGIC, 3, int)

注意:同一份驱动的 ioctl 命令定义,无论有无数据传输以及数据传输方向是否相同,各命令的序号都不能相同

  定义完全部所需命令后,还需定义一个命令的最大的编号,防止传入参数超过编号范围。

1.4.5 解析 ioctl 命令

  驱动程序必须对传入的命令进行解析,包括传输方向、命令类型、命令编号以及参数大
小,分别可以通过下表的宏定义完成:

宏定义 描述
_IOC_DIR(nr) 解析命令的传输方向
_IOC_TYPE(nr) 解析命令类型
_IOC_NR(nr) 解析命令序号
_IOC_SIZE(nr) 解析参数大小

  如果解析发现命令出错,可以返回-ENOTTY,如:

if (_IOC_TYPE(cmd) != LED_IOC_MAGIC) {
	return -ENOTTY;
}
if (_IOC_NR(cmd) >= LED_IOC_MAXNR) {
	return -ENOTTY;
}

二、编写代码

2.1 修改、编译、覆盖设备树文件

参考第九节内容。

2.2 驱动程序编写

这一次将驱动程序的框架又完善了一下,认真体会!

leddrv.c

#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/timer.h>


#define DEV_CNT			1		/* 设备个数 	*/
#define DEV_NAME		"led"	/* 设备名字 	*/

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


/* 定义led_dev设备结构体 */
struct led_dev{
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	int major;					/* 主设备号	  */
	int minor;					/* 次设备号   */
	/*GPIO子系统*/
	struct gpio_desc *led_gpio;	/* GPIO子系统接口   */
	/*定时器*/
	int timeperiod; 			/* 定时周期,单位为ms */
	struct timer_list timer;	/* 定义一个定时器*/
	/*自旋锁*/
	spinlock_t lock;			/* 定义自旋锁 */
};

struct led_dev leddev;	/* led设备 */


static int led_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	file->private_data = &leddev;	/* 设置私有数据 */
	leddev.timeperiod = 1000;		/* 默认周期为1s */

	gpiod_direction_output(leddev.led_gpio, 1);	/* 初始化LED - on */

	return 0;
}

static long led_drv_unlocked_ioctl (struct file *file, unsigned int cmd, unsigned long arg)
{
	struct led_dev *dev =  (struct led_dev *)file->private_data;
	int timerperiod;
	unsigned long flags;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	switch (cmd) {
		case CLOSE_CMD:		/* 关闭定时器 */
			del_timer_sync(&dev->timer);
			break;
		case OPEN_CMD:		/* 打开定时器 */
			spin_lock_irqsave(&dev->lock, flags);
			timerperiod = dev->timeperiod;
			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->timeperiod = arg;
			spin_unlock_irqrestore(&dev->lock, flags);
			mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
			break;
		default:
			break;
	}

	return 0;
}

/* 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {
	.owner	 = THIS_MODULE,
	.open    = led_drv_open,
	.unlocked_ioctl = led_drv_unlocked_ioctl,
};

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

	sta = !sta;		/* 每次都取反,实现LED灯反转 */
	gpiod_set_value(dev->led_gpio, sta);/* 用的时候需要强制转化为struct led_dev*,并且只能用->运算符 */

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

/*   从platform_device获得GPIO
 *   把file_operations结构体告诉内核:注册驱动程序
 */
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	/* 1、从设备树中获取资源。设备树中定义有: led-gpios=<...>;	*/
    leddev.led_gpio = gpiod_get(&pdev->dev, "led", 0);
	if (IS_ERR(leddev.led_gpio)) {
		dev_err(&pdev->dev, "Failed to get GPIO for led\n");
		return PTR_ERR(leddev.led_gpio);
	}

	/* 2、注册字符设备驱动 */
	/* ①、创建设备号 */
	if (leddev.major) {		/*  定义了设备号 */
		leddev.devid = MKDEV(leddev.major, 0);
		register_chrdev_region(leddev.devid, DEV_CNT, DEV_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&leddev.devid, 0, DEV_CNT, DEV_NAME);	/* 申请设备号 */
		leddev.major = MAJOR(leddev.devid);	/* 获取分配号的主设备号 */
		leddev.minor = MINOR(leddev.devid);	/* 获取分配号的次设备号 */
	}
	
	/* ②、初始化cdev */
	leddev.cdev.owner = THIS_MODULE;
	cdev_init(&leddev.cdev, &led_drv);
	
	/* ③、添加一个cdev */
	cdev_add(&leddev.cdev, leddev.devid, DEV_CNT);

	/* ④、创建类 */
	leddev.class = class_create(THIS_MODULE, DEV_NAME);
	if (IS_ERR(leddev.class)) {
		return PTR_ERR(leddev.class);
	}

	/* ⑤、创建设备 */
	leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, DEV_NAME);
	if (IS_ERR(leddev.device)) {
		return PTR_ERR(leddev.device);
	}
	
	/* 初始化自旋锁 */
	spin_lock_init(&leddev.lock);
	/* 初始化timer,设置定时器处理函数,还未设置周期,所以不会激活定时器 */
	init_timer(&leddev.timer);
	leddev.timer.function = timer_function;
	/* 注意leddev类型是结构体led_dev,这里取地址然后强制转化为unsigned long,用的时候需要强制转化为struct led_dev* */
	leddev.timer.data = (unsigned long)&leddev;
	
	return 0;
}

static int chip_demo_gpio_remove(struct platform_device *pdev)
{
	gpiod_set_value(leddev.led_gpio, 0);/* 卸载驱动的时候关闭LED */

	gpiod_put(leddev.led_gpio);
	del_timer_sync(&leddev.timer);		/* 删除timer */

	/* 注销字符设备驱动 */
	cdev_del(&leddev.cdev);/*  删除cdev */
	unregister_chrdev_region(leddev.devid, DEV_CNT); /* 注销设备号 */
	device_destroy(leddev.class, leddev.devid);
	class_destroy(leddev.class);	
    
    return 0;
}


static const struct of_device_id ask100_leds[] = {
    { .compatible = "100ask,leddrv" },
    { },
};

/* 1. 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "100ask_led",
        .of_match_table = ask100_leds,
    },
};

/* 2. 在入口函数注册platform_driver */
static int __init led_init(void)
{
    int err;

	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    err = platform_driver_register(&chip_demo_gpio_driver);
	
	return err;
}

/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 *     卸载platform_driver
 */
static void __exit led_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&chip_demo_gpio_driver);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

需要说明的:

  • ①、灯的状态

    • 设备树中设置低电平有效,打开-红色-写1 关闭-白色-写0
  • ②、使用自旋锁

    • 取出定时周期值的时候,timerperiod = dev->timeperiod;
    • 设置定时周期的时候,dev->timeperiod = arg;
  • ③、ioctl

    • 幻数0xEF,对应十进制239,对应ASCII为符号'∩'!(不是小写字母n,数学符号交集)
    • 驱动程序中,为了方便并没有做解析,而是直接switch-case选择!
  • ④、私有数据

    • 一般在open的时候将file结构体中的private_data指向设备结构体,即设置私有数据!
  • ⑤、修改定时器

    • 修改定时器会激活定时器
    • msecs_to_jiffies转换系统节拍的时候,借助的是第三方变量,而没有直接操作dev->timeperiod
  • ⑥、强制转化

    • 程序中涉及到结构体和unsigned long的转化,注意体会思想,也可参考<这篇>文章里的强制转化进行理解!
  • 所有LED相关的放到了一个设备结构体里,然后引入私有数据的思想,值得认真体会!

2.2 应用程序编写

ledtest.c

#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"

/* 命令值 */
#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];

	fd = open(filename, O_RDWR);
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		printf("Input CMD:");
		ret = scanf("%d", &cmd);
		if (ret != 1) {				/* 参数输入错误 */
			gets(str);				/* 防止卡死 */
		}

		if(cmd == 1)				/* 关闭LED灯 */
			cmd = CLOSE_CMD;
		else if(cmd == 2)			/* 打开LED灯 */
			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);
}

需要说明的

  • gets的加入可能会让程序编译的时候有警告,忽略即可!
  • 实现功能
    • 输入 1 表示关闭定时器
    • 输入 2 表示打开定时器
    • 输入 3 设置定时器周期
      • 选择设置定时器周期的话,接着需要输入设置的周期值,单位为毫秒

三、运行程序

编译程序没有问题后,运行qemu虚拟开发板,并做好准备工作!将

  • 拷贝led.ko和ledtest到NFS中
cp *.ko ledtest ~/linux/qemu/NFS/
  • 在qemu终端,加载led.ko文件
insmod leddrv.ko

在qemu中加载最后一个模块时,会出现下面的提示信息,但是ctrl+c之后,似乎测试还是可以用的,不知道是怎么回事。知道的朋友,可以在下面留言一起探讨!
在这里插入图片描述

  • 在qemu终端,运行应用程序
./ledtest /dev/led

同时,可以看到,qemu模拟板的第一个小灯,又白色变成红色表示打开。同时终端会提示让继续输入命令,我们尝试输入2,打开定时器,观察小灯闪烁!【无法录屏,这里就不放图了】

接着输入1,关闭定时器,取消LED闪烁!

最后输入3,自定义LED闪烁时间为2000ms!

大功告成,还是很完美的!

发布了716 篇原创文章 · 获赞 1197 · 访问量 85万+

猜你喜欢

转载自blog.csdn.net/ReCclay/article/details/105316031