博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【嵌入式Linux驱动开发】十四、了解Linux内核定时器使用流程,实现LED闪烁
阅读量:2028 次
发布时间:2019-04-28

本文共 12154 字,大约阅读时间需要 40 分钟。

   致敬英雄!

文章目录


一、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 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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!

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

转载地址:http://ibnaf.baihongyu.com/

你可能感兴趣的文章
【C/C++】Linux下使用system()函数一定要谨慎
查看>>
setsid()函数的作用
查看>>
守护进程的创建方法和步骤
查看>>
ioctl用法详解
查看>>
每天进步一点点——Linux中的线程局部存储(二)
查看>>
【C++】explicit关键字
查看>>
八大排序算法
查看>>
C++ 11
查看>>
Spring @Configuration 和 @Component 区别
查看>>
JVM内存模型
查看>>
springbootTestDao
查看>>
一张表里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把mysql重启,再insert一条记录,这条记录的ID是18还是15 ?
查看>>
Mysql:查询卡死的sql
查看>>
分库分表技术演进&最佳实践-修订篇
查看>>
arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别
查看>>
syslog日志记录
查看>>
Linux GCC与GDB调试
查看>>
Linux下的动态库.so
查看>>
无线通信技术协议-6LoWPAN
查看>>
C/C++ 解析ini文件
查看>>