• Linux学习第21天:Linux内核定时器驱动开发: 流淌的时间长河


    Linux版本号4.1.15   芯片I.MX6ULL                                    大叔学Linux    品人间百味  思文短情长 


            在人类的发展进化中,时间是一个非常重要神秘的物质量。任何事物都是在时间的长河中流淌发生、发展、变化。我们进行驱动开发中对时间的定义和使用也是必须要掌握的重要知识点。

            本节笔记主要学习Linux内核定时器的驱动开发,主要包括内核时间管理和定时器、硬件原理图分析【LED】、驱动开发和测试。最重要的内容为定时器驱动开发部分。

            本笔记的脑图如下:

    一、Linux时间管理和内核定时器

    1.内核时间管理

            作为一个应用者,不需要研究太深的具体实现。会用相应的API函数即可。

            图形化配置界面可以设置系统节拍率。具体路径为:

            Kernel Features

                    Timer frequency([=y])

            使用CONFIG_HZ来设置自己的系统时钟。

            HZ表示一秒的节拍数,也就是频率。

            使用jiffies来记录系统从启动以来的系统节拍数。

            jiffies/HZ就是系统运行时间,单位为秒。

            处理32位jiffies的绕回,用到的几个函数。如下:

    函数        描述
    time_after(unknown,known)unkonwn超过known时为真
    time_before(unknown,known)unkonwn没有超过known时为真
    time_after_eq(unknown,known)unkonwn超过或等于known时为真
    time_before_eq(unknown,known)unkonwn没有超过或等于known时为真

    2.内核定时器

            需要周期性处理的工作都要用到定时器。Linux定时器采用系统时钟来实现,只需要提供超时时间和定时处理函数即可。

            内核定时器超时会自动关闭。

            使用timer_list结构体表示内核定时器。该结构体的定义如下:

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

    定义好定时器以后,需要以下函数对其进行初始化:

    函数说明
    init_timer初始化
    add_timer注册
    del_timer删除
    del_timer_sync等待其他处理器使用完定时器后删除
    mod_timer修改定时值

            内核定时器的使用流程如下:

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

    3.Linux内核短延时函数

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


    二、硬件原理图分析

            LED0 接到了 GPIO_3 上, GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03的输出电平,输出 0 就亮,输出 1 就灭

    三、驱动程序开发

    1.修改设备树文件

            使用已有的即可。

    2.定时器驱动程序开发

    1. 38 struct timer_dev{
    2. 39 dev_t devid; /* 设备号 */
    3. 40 struct cdev cdev; /* cdev */
    4. 41 struct class *class; /* 类 */
    5. 42 struct device *device; /* 设备 */
    6. 43 int major; /* 主设备号 */
    7. 44 int minor; /* 次设备号 */
    8. 45 struct device_node *nd; /* 设备节点 */
    9. 46 int led_gpio; /* key 所使用的 GPIO 编号 */
    10. 47 int timeperiod; /* 定时周期,单位为 ms */
    11. 48 struct timer_list timer; /* 定义一个定时器 */
    12. 49 spinlock_t lock; /* 定义自旋锁 */
    13. 50 };

            定时器设备结构体,在 48 行定义了一个定时器成员变量 timer。

    1. 60 static int led_init(void)
    2. 61 {
    3. 62 int ret = 0;
    4. 63
    5. 64 timerdev.nd = of_find_node_by_path("/gpioled");
    6. 65 if (timerdev.nd== NULL) {
    7. 66 return -EINVAL;
    8. 67 }
    9. 68
    10. 69 timerdev.led_gpio = of_get_named_gpio(timerdev.nd ,"led-gpio",
    11. 0);
    12. 70 if (timerdev.led_gpio < 0) {
    13. 71 printk("can't get led\r\n");
    14. 72 return -EINVAL;
    15. 73 }
    16. 74
    17. 75 /* 初始化 led 所使用的 IO */
    18. 76 gpio_request(timerdev.led_gpio, "led"); /* 请求 IO */
    19. 77 ret = gpio_direction_output(timerdev.led_gpio, 1);
    20. 78 if(ret < 0) {
    21. 79 printk("can't set gpio!\r\n");
    22. 80 }
    23. 81 return 0;
    24. 82 }

            LED 灯初始化函数,从设备树中获取 LED 灯信息,然后初始化相应的 IO。

    1. 91 static int timer_open(struct inode *inode, struct file *filp)
    2. 92 {
    3. 93 int ret = 0;
    4. 94 filp->private_data = &timerdev; /* 设置私有数据 */
    5. 95
    6. 96 timerdev.timeperiod = 1000; /* 默认周期为 1s */
    7. 97 ret = led_init(); /* 初始化 LED IO */
    8. 98 if (ret < 0) {
    9. 99 return ret;
    10. 100 }
    11. 101 return 0;
    12. 102 }

            函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开/dev/timer 驱动文件的时候此函数就会执行。此函数设置文件私有数据为 timerdev,并且初始化定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。

    1. 111 static long timer_unlocked_ioctl(struct file *filp,
    2. unsigned int cmd, unsigned long arg)
    3. 112 {
    4. 原子哥在线教学:www.yuanzige.com 论坛:www.openedv.com
    5. 1228
    6. I.MX6U 嵌入式 Linux 驱动开发指南
    7. 113 struct timer_dev *dev = (struct timer_dev *)filp->private_data;
    8. 114 int timerperiod;
    9. 115 unsigned long flags;
    10. 116
    11. 117 switch (cmd) {
    12. 118 case CLOSE_CMD: /* 关闭定时器 */
    13. 119 del_timer_sync(&dev->timer);
    14. 120 break;
    15. 121 case OPEN_CMD: /* 打开定时器 */
    16. 122 spin_lock_irqsave(&dev->lock, flags);
    17. 123 timerperiod = dev->timeperiod;
    18. 124 spin_unlock_irqrestore(&dev->lock, flags);
    19. 125 mod_timer(&dev->timer, jiffies +
    20. msecs_to_jiffies(timerperiod));
    21. 126 break;
    22. 127 case SETPERIOD_CMD: /* 设置定时器周期 */
    23. 128 spin_lock_irqsave(&dev->lock, flags);
    24. 129 dev->timeperiod = arg;
    25. 130 spin_unlock_irqrestore(&dev->lock, flags);
    26. 131 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
    27. 132 break;
    28. 133 default:
    29. 134 break;
    30. 135 }
    31. 136 return 0;
    32. 137 }

            函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数: filp, cmd 和 arg,其中 filp是对应的设备文件, cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数,在本章例程中 arg 参数表示定时周期。

    CLOSE_CMD: 关闭定时器命令, 调用 del_timer_sync 函数关闭定时器。
    OPEN_CMD:打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的
    timeperiod 成员变量,定时周期默认是 1 秒。
    SETPERIOD_CMD:设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的
    timeperiod 成员变量为 arg 所表示定时周期指。并且使用 mod_timer 重新打开定时器,使定时器
    以新的周期运行。
     

    1. 139 /* 设备操作函数 */
    2. 140 static struct file_operations timer_fops = {
    3. 141 .owner = THIS_MODULE,
    4. 142 .open = timer_open,
    5. 143 .unlocked_ioctl = timer_unlocked_ioctl,
    6. 144 };

    定时器驱动操作函数集 timer_fops。
     

    1. 146 /* 定时器回调函数 */
    2. 147 void timer_function(unsigned long arg)
    3. 148 {
    4. 149 struct timer_dev *dev = (struct timer_dev *)arg;
    5. 150 static int sta = 1;
    6. 151 int timerperiod;
    7. 152 unsigned long flags;
    8. 153
    9. 154 sta = !sta; /* 每次都取反,实现 LED 灯反转 */
    10. 155 gpio_set_value(dev->led_gpio, sta);
    11. 156
    12. 157 /* 重启定时器 */
    13. 158 spin_lock_irqsave(&dev->lock, flags);
    14. 159 timerperiod = dev->timeperiod;
    15. 160 spin_unlock_irqrestore(&dev->lock, flags);
    16. 161 mod_timer(&dev->timer, jiffies +
    17. msecs_to_jiffies(dev->timeperiod));
    18. 162 }

    函数 timer_function,定时器服务函数,此函有一个参数 arg,在本例程中arg 参数就是 timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。当定时周期到了以后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。因为内核定时器不是循环的定时器,执行一次以后就结束了,因此在 161 行又调用了 mod_timer 函数重新开启定时器。

    1. 169 static int __init timer_init(void)
    2. 170 {
    3. 171 /* 初始化自旋锁 */
    4. 172 spin_lock_init(&timerdev.lock);
    5. 173
    6. 174 /* 注册字符设备驱动 */
    7. 175 /* 1、创建设备号 */
    8. 176 if (timerdev.major) { /* 定义了设备号 */
    9. 177 timerdev.devid = MKDEV(timerdev.major, 0);
    10. 178 register_chrdev_region(timerdev.devid, TIMER_CNT,
    11. TIMER_NAME);
    12. 179 } else { /* 没有定义设备号 */
    13. 180 alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT,
    14. TIMER_NAME);
    15. 181 timerdev.major = MAJOR(timerdev.devid); /* 获取主设备号 */
    16. 182 timerdev.minor = MINOR(timerdev.devid); /* 获取次设备号 */
    17. 183 }
    18. 184
    19. 185 /* 2、初始化 cdev */
    20. 186 timerdev.cdev.owner = THIS_MODULE;
    21. 187 cdev_init(&timerdev.cdev, &timer_fops);
    22. 188
    23. 189 /* 3、添加一个 cdev */
    24. 190 cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);
    25. 191
    26. 192 /* 4、创建类 */
    27. 193 timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
    28. 194 if (IS_ERR(timerdev.class)) {
    29. 195 return PTR_ERR(timerdev.class);
    30. 196 }
    31. 197
    32. 198 /* 5、创建设备 */
    33. 199 timerdev.device = device_create(timerdev.class, NULL,
    34. timerdev.devid, NULL, TIMER_NAME);
    35. 200 if (IS_ERR(timerdev.device)) {
    36. 201 return PTR_ERR(timerdev.device);
    37. 202 }
    38. 203
    39. 204 /* 6、初始化 timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
    40. 205 init_timer(&timerdev.timer);
    41. 206 timerdev.timer.function = timer_function;
    42. 207 timerdev.timer.data = (unsigned long)&timerdev;
    43. 208 return 0;
    44. 209 }

            第 169~209 行,函数 timer_init,驱动入口函数。在第 205~207 行初始化定时器,设置定时器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev的地址。在此函数中并没有调用 timer_add 函数来开启定时器,因此定时器默认是关闭的,除非应用程序发送打开命令。

    1. 216 static void __exit timer_exit(void)
    2. 217 {
    3. 218
    4. 219 gpio_set_value(timerdev.led_gpio, 1); /* 卸载驱动的时候关闭 LED */
    5. 220 del_timer_sync(&timerdev.timer); /* 删除 timer */
    6. 221 #if 0
    7. 222 del_timer(&timerdev.tiemr);
    8. 223 #endif
    9. 224
    10. 225 /* 注销字符设备驱动 */
    11. 226 cdev_del(&timerdev.cdev); /* 删除 cdev */
    12. 227 unregister_chrdev_region(timerdev.devid, TIMER_CNT);
    13. 228
    14. 229 device_destroy(timerdev.class, timerdev.devid);
    15. 230 class_destroy(timerdev.class);
    16. 231 }

    驱动出口函数,在 219 行关闭 LED,也就是卸载驱动以后 LED 处于熄灭状态。第 220 行调用 del_timer_sync 函数删除定时器,也可以使用 del_timer 函数。
     

    3.编写测试APP

    1. 21 /* 命令值 */
    2. 22 #define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
    3. 23 #define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
    4. 24 #define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
    5. 25
    6. 26 /*
    7. 27 * @description : main 主程序
    8. 28 * @param - argc : argv 数组元素个数
    9. 29 * @param - argv : 具体参数
    10. 30 * @return : 0 成功;其他 失败
    11. 31 */
    12. 32 int main(int argc, char *argv[])
    13. 33 {
    14. 34 int fd, ret;
    15. 35 char *filename;
    16. 36 unsigned int cmd;
    17. 37 unsigned int arg;
    18. 38 unsigned char str[100];
    19. 39
    20. 40 if (argc != 2) {
    21. 41 printf("Error Usage!\r\n");
    22. 42 return -1;
    23. 43 }
    24. 44
    25. 45 filename = argv[1];
    26. 46
    27. 47 fd = open(filename, O_RDWR);
    28. 48 if (fd < 0) {
    29. 49 printf("Can't open file %s\r\n", filename);
    30. 50 return -1;
    31. 51 }
    32. 52
    33. 53 while (1) {
    34. 54 printf("Input CMD:");
    35. 55 ret = scanf("%d", &cmd);
    36. 56 if (ret != 1) { /* 参数输入错误 */
    37. 57 gets(str); /* 防止卡死 */
    38. 58 }
    39. 59
    40. 60 if(cmd == 1) /* 关闭 LED 灯 */
    41. 61 cmd = CLOSE_CMD;
    42. 62 else if(cmd == 2) /* 打开 LED 灯 */
    43. 63 cmd = OPEN_CMD;
    44. 64 else if(cmd == 3) {
    45. 65 cmd = SETPERIOD_CMD; /* 设置周期值 */
    46. 66 printf("Input Timer Period:");
    47. 67 ret = scanf("%d", &arg);
    48. 68 if (ret != 1) { /* 参数输入错误 */
    49. 69 gets(str); /* 防止卡死 */
    50. 70 }
    51. 71 }
    52. 72 ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
    53. 73 }
    54. 74 close(fd);
    55. 75 }

    ①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
    ②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。

    第 22~24 行,命令值。
    第 53~73 行, while(1)循环,让用户输入要测试的命令,然后通过第 72 行的 ioctl 函数发送给驱动程序。如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用户输入的周期值。


    四、运行测试

    1.编译驱动程序和测试AP

    编译成功以后就会生成一个名为“ timer.ko”的驱动模块文件。

    编译成功以后就会生成 timerApp 这个应用程序。


    2.运行测试

    根据提示输入指令及周期,观察LED的交替亮变化即可。

    五、总结

            本节笔记主要学习Linux内核定时器的驱动开发,主要包括内核时间管理和定时器、硬件原理图分析【LED】、驱动开发和测试。最重要的内容为定时器驱动开发部分。


    本文为参考正点原子开发板配套教程整理而得,仅用于学习交流使用,不得用于商业用途。

  • 相关阅读:
    编曲软件FL studio20.8中文版功能和作用
    详解机器学习最优化算法
    Linux第一个小程序进度条
    JMeter 测试脚本编写技巧
    网络协议层次模型
    PoeAI:免费使用GPT-4的AI聊天机器人平台
    Jmeter之数据参数化方法汇总
    echarts饼图label自定义样式
    自动驾驶专题介绍 —— 停车位相关介绍
    网页按钮点击动画
  • 原文地址:https://blog.csdn.net/jiage987450/article/details/133362615