Linux 内核定时器实验-基于正点原子IMX6ULL开发板

1 Linux 时间管理和内核定时器简介

1.1 内核时间管理简介

Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱 动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置,设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)( 有的资料也叫系统频率 ) ,比如 1000Hz 100Hz 等等说的就是 系统节拍率 。系统节拍率是可以设置的,单位是 Hz ,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率。
 
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的 系统节拍数 系统启动的时候会将 jiffies 初始化为 0 jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;
HZ 表示每秒的节拍数,jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。

1.2 内核定时器简介

定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。 Linux 内核定时器 采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。 Linux 内核定时器使 用很简单,只需要提供超时时间( 相当于定时值 ) 和定时处理函数即可,当超时时间到了以后设 置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要 做一大堆的寄存器初始化工作。在 使用内核定时器的时候要注意一点,内核定时器并不是周期 性运行的,超时以后就会自动关闭 ,因此如果想要实现周期性定时,那么就需要在定时处理函 数中重新开启定时器。
Linux 内核使用 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;
};
要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器。
 
expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时
器,那么这个定时器的超时时间就是 jiffies+(2*HZ) ,因此 expires=jiffies+(2*HZ)。
function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定 时处理函数。

定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器,这些函数如下:
void init_timer(struct timer_list *timer)
void add_timer(struct timer_list *timer)
int del_timer(struct timer_list * timer)
int del_timer_sync(struct timer_list *timer)
int mod_timer(struct timer_list *timer, unsigned long expires)

内核定时器使用方法演示
 
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.3 Linux 内核短延时函数
void ndelay(unsigned long nsecs)
void udelay(unsigned long usecs)
void mdelay(unsigned long mseces)

2 硬件原理图分析

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

Linux 内核定时器实验-基于正点原子IMX6ULL开发板_第1张图片


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

3.1
修改设备树文件


同《pinctrl 和 gpio 子系统实验-基于正点原子IMX6ULL开发板》4.1

3.2 定时器驱动程序编写
新建工程,在
timer.c 里面输入如下内容:

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

#define TIMER_CNT 1
#define TIMER_NAME "timer"

#define CLOSE_CMD (_IO(0xEF, 1))           //关闭命令
#define OPEN_CMD (_IO(0xEF, 2))            //打开命令
#define SETPERIOD_CMD (_IOW(0xEF, 3, int)) //设置周期

/*TIMER 设备结构体*/
struct timer_dev
{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    struct timer_list timer; /* 定义一个定时器 */
    int timeperiod;          /* 定时周期,单位为 ms */
    int led_gpio;
};

struct timer_dev timerdev; /*TIMER */

static int timer_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &timerdev;
    return 0;
}

static int timer_release(struct inode *inode, struct file *filp)
{

    return 0;
}

static long timer_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret = 0;
    int value = 0;
    struct timer_dev *dev = filp->private_data;

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

    case OPEN_CMD: /* 打开定时器 */
        mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
        break;

    case SETPERIOD_CMD: /* 设置定时器周期 */
        ret = copy_from_user(&value, (int *)arg, sizeof(int));
        if (ret < 0)
        {
            return -EFAULT;
        }
        dev->timeperiod = value;
        mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
        break;
    default:
        break;
    }
    return ret;
}

/*操作集*/
static const struct file_operations timerdev_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = timer_ioctl,
    .open = timer_open,
    .release = timer_release,
};

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

    sta = !sta;
    gpio_set_value(dev->led_gpio, sta);
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
}

/*初始化LED灯*/
int led_init(struct timer_dev *dev)
{

    int ret = 0;
    dev->nd = of_find_node_by_path("/gpioled");
    if (dev->nd == NULL)
    {
        ret = -EINVAL;
        goto fail_fd;
    }

    dev->led_gpio = of_get_named_gpio(dev->nd, "led-gpios", 0);
    if (dev->led_gpio < 0)
    {
        ret = -EINVAL;
        goto fail_gpio;
    }

    /* 初始化 led 所使用的 IO */
    ret = gpio_request(dev->led_gpio, "led");
    if (ret)
    {
        ret = -EBUSY;
        printk("IO %d can't request!\r\n", dev->led_gpio);
        goto fail_request;
    }

    ret = gpio_direction_output(dev->led_gpio, 1); /*设置输出,默认关灯*/
    if (ret < 0)
    {

        ret = -EINVAL;
        goto fail_gpioset;
    }
    return 0;

fail_gpioset:
    gpio_free(dev->led_gpio);
fail_request:

fail_gpio:

fail_fd:

    return ret;
}

/*驱动入口函数*/
static int __init timer_init(void)
{
    int ret = 0;
    /*1,注册字符设备驱动*/
    timerdev.major = 0;
    if (timerdev.major)
    { /*给定主设备号*/
        timerdev.devid = MKDEV(timerdev.major, 0);
        ret = register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);
    }
    else
    {
        ret = alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME);
        timerdev.major = MAJOR(timerdev.devid);
        timerdev.minor = MINOR(timerdev.devid);
    }

    if (ret < 0)
    {
        goto fail_devid;
    }

    printk("timerdev major =%d, minor =%d \r\n", timerdev.major, timerdev.minor);

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

    /*3,添加cdev*/
    ret = cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);
    if (ret)
    {
        goto fail_cdevadd;
    }
    /*4,创建类*/
    timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
    if (IS_ERR(timerdev.class))
    {
        ret = PTR_ERR(timerdev.class);
        goto fail_class;
    }
    /*5,创建设备*/
    timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);
    if (IS_ERR(timerdev.device))
    {
        ret = PTR_ERR(timerdev.device);
        goto fail_device;
    }

    /*6,初始化LED灯*/
    ret = led_init(&timerdev);
    if (ret < 0)
    {
        goto fail_ledinit;
    }

    /*7,初始化定时器*/
    init_timer(&timerdev.timer);

    timerdev.timeperiod = 500;
    timerdev.timer.function = timer_func;
    timerdev.timer.expires = jiffies + msecs_to_jiffies(timerdev.timeperiod);
    timerdev.timer.data = (unsigned long)&timerdev;
    add_timer(&timerdev.timer); /*添加到系统*/

    return 0;

fail_ledinit:

fail_device:
    class_destroy(timerdev.class);
fail_class:
    cdev_del(&timerdev.cdev);
fail_cdevadd:
    unregister_chrdev_region(timerdev.devid, TIMER_CNT);
fail_devid:

    return ret;
}

/*驱动出口函数*/
static void __exit timer_exit(void)
{
    /*关灯*/
    gpio_set_value(timerdev.led_gpio, 1);

    /*删除定时器*/
    del_timer(&timerdev.timer);

    /*注销字符设备驱动*/
    cdev_del(&timerdev.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("supersmart");
3.3 编写测试 APP

 测试 APP 我们要实现的内容如下:

①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。新建名为 timerAPP.c 的文件,然后输入如下所示内容:
 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


/*
 *argc:应用程序参数个数
 * argv[]:具体的参数内容,字符串形式
 * ./timerAPP  <0:1> 0 关灯,1 开灯
 * ./timerAPP /dev/timer
 */

#define CLOSE_CMD _IO(0xEF, 1)           //关闭命令
#define OPEN_CMD _IO(0xEF, 2)            //打开命令
#define SETPERIOD_CMD _IOW(0xEF, 3, int) //设置周期

int main(int argc, char *argv[])
{

    int fd, ret;
    char *filename;
    unsigned char cnt;
    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("file %s open failed!\r\n", filename);
        return -1;
    }

    /*循环读取*/
    while (1)
    {
        printf("Input CMD:");
        ret = scanf("%d", &cmd);
        if (ret != 1)
        {
            gets(str); /*防止卡死*/
        }

        if (cmd == 1)   /*关闭*/
        {
            ioctl(fd,CLOSE_CMD,&arg);
        }
        else if (cmd == 2)/*打开*/
        {
            ioctl(fd,OPEN_CMD,&arg);
        }
        else if (cmd == 3)/*设置周期*/
        {
            printf("Input Timer period:");
            ret = scanf("%d",&arg);
            if(ret !=1){
                 gets(str); /*防止卡死*/
            }
            ioctl(fd,SETPERIOD_CMD,&arg);
        }
    }
    close(fd);
    return 0;
}
4 运行测试

4.1 编译驱动程序和测试 APP
1、编译驱动程序
2
、编译测试 APP

4.2 运行测试

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


 
驱动加载成功以后如下命令来测试:
Linux 内核定时器实验-基于正点原子IMX6ULL开发板_第2张图片

led灯按照设置的定时器周期进行闪烁。
卸载驱动




详细内容参考正点原子《I.MX6U 嵌入式 Linux 驱动开发指南 V1.6》第50章。

 

补充:ioctl 用户与驱动之间的协议
 ioctl 方法第二个参数 cmd 为用户与驱动的 “协议”,理论上可以为任意 int 型数据,可以为 0、1、2、3……,但是为了确保该 “协议” 的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图所示:
Linux 内核定时器实验-基于正点原子IMX6ULL开发板_第3张图片
 

  • dir(direction),ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据。
  • type(device type),是个0-0xff的数或者一个字符,占8bit。这个数是用来区分不同的驱动的,像设备号一样,内核有一个文档(Documentation/ioctl/ioctl-number.txt)给出一些推荐的或者已经被使用的幻数。
  • nr(number),命令编号/序数,8 bit,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增。
  • size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,只需要填参数的类型,如int,函数就会帮你检测类型的正确性然后赋值sizeof(int)。

为了方便我们会使用宏 _IOC() 衍生的接口来直接定义 ioctl 命令,函数原型如下所示:

#define _IO(type,nr)    _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
_IO:       定义不带参数的 ioctl 命令
_IOW:      定义带写参数的 ioctl 命令(copy_from_user)
_IOR:      定义带读参数的ioctl命令(copy_to_user)
_IOWR:     定义带读写参数的 ioctl 命令

参考内容:
linux驱动开发学习笔记十九:认识一下ioctl函数_耐心的小黑的博客-CSDN博客

你可能感兴趣的:(linux,arm,c++,vscode,嵌入式硬件)