Linux字符设备驱动开发(九):内核定时器——实现LED周期性闪烁与轮询驱动原理

前言

上一篇文章中,我们学会了将耗时工作从中断顶半部推迟到底半部处理,并利用 tasklet 和工作队列实现了软件消抖。这些机制都是"一次性"延迟执行:一次中断触发一次处理。然而,很多场景需要周期性地执行某个动作,例如让 LED 以固定频率闪烁、定期轮询传感器状态、内核心跳检测等。

Linux 内核提供了内核定时器(timer_list)来完成这类周期性任务。本文将以 GPIO LED 为例,编写一个可以设置闪烁频率的字符设备驱动,并借此介绍定时器的核心 API、使用方法以及注意事项。同时,我们会简要说明轮询驱动的概念------无需外部中断,仅靠定时器反复查询硬件状态即可实现设备监控。

读完本文你将掌握:

  • timer_list 的初始化、启动与删除
  • mod_timer 实现周期触发的技巧
  • 自旋锁保护共享数据(定时器回调上下文与进程上下文并发)
  • 通过设备节点动态调整定时周期
  • 高精度定时器 hrtimer 简介

一、内核定时器简介

1.1 内核定时器(timer_list)

struct timer_list 是 Linux 内核提供的一种低精度定时器,基于jiffies(内核节拍数)工作,典型分辨率为 1~10 毫秒(取决于 HZ 配置)。它的回调函数在软中断上下文中执行,因此不能睡眠,也不能调用可能导致阻塞的函数。

核心 API

函数 作用
timer_setup(timer, callback, flags) 初始化定时器并绑定回调函数(新内核推荐)
mod_timer(timer, expires) 修改定时器的到期时间,并激活定时器。常用于实现周期触发
add_timer(timer) 向内核添加一个已经设置了 expires 的定时器
del_timer(timer) 删除定时器,若已失效则返回 0,否则返回 1
del_timer_sync(timer) 等待定时器处理函数执行完毕后删除(用于卸载模块)

旧版内核使用 init_timer()timer->datatimer->function,新内核已废弃,推荐使用 timer_setup

1.2 实现周期触发的常用方法

在定时器回调函数中再次调用 mod_timer,即可形成"一次性触发→处理→重新设定下次触发"的循环,实现周期性任务。退出循环时,只需不再调用 mod_timer 或调用 del_timer 即可。


二、设计思路

本文沿用第四篇文章中定义的 gpioled 设备树节点,使用 i.MX6ULL 的 GPIO5_IO03 控制 LED(低电平有效)。驱动将以字符设备框架呈现:

  • 设备节点 /dev/ledblink,支持读写。
  • 默认闪烁周期为 500ms(即 1Hz),加载后 LED 开始闪烁。
  • write 操作 :用户可写入新的周期值(毫秒),例如 echo 200 > /dev/ledblink 将闪烁周期设为 200ms。
  • read 操作:返回当前设置的周期值。
  • 定时器回调函数在软中断中运行,翻转 GPIO 电平并重新设定定时器。
  • 使用自旋锁保护共享变量(周期值和 LED 状态),因为回调在软中断上下文,write 在进程上下文,需防止竞态。

三、设备树修改

沿用之前的 LED 设备树节点(无需改动)。确保板级设备树中存在以下内容:

dts 复制代码
/ {
    gpioled {
        compatible = "yourname,gpioled";
        gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

如果你的 LED 连接在其他 GPIO,请相应修改。


四、驱动代码实现

新建文件 led_blink_timer.c,完整代码如下:

c 复制代码
/*
 * led_blink_timer.c
 * 基于内核定时器的 LED 闪烁驱动。
 * 使用 platform_driver 获取 GPIO,timer_list 实现周期性翻转。
 * 设备节点 /dev/ledblink:支持读写,写入毫秒值改变闪烁周期。
 * 作者:[你的ID]
 * 适配内核:Linux 5.x (4.x 亦可)
 * 参考开发板:i.MX6ULL
 */

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/timer.h>
#include <linux/spinlock.h>
#include <linux/jiffies.h>
#include <linux/of.h>

#define DEVICE_NAME "ledblink"
#define CLASS_NAME  "ledblink_class"
#define DEFAULT_PERIOD_MS  500    /* 默认闪烁周期 500ms */

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;

static struct gpio_desc *led_gpio;      /* LED GPIO 描述符 */
static struct timer_list blink_timer;   /* 内核定时器 */

static int led_state;                   /* 当前LED逻辑状态(0/1) */
static int period_ms = DEFAULT_PERIOD_MS;  /* 闪烁周期(毫秒) */

/* 自旋锁保护 led_state 和 period_ms */
static DEFINE_SPINLOCK(lock);

/* 定时器回调函数(软中断上下文) */
static void blink_timer_callback(struct timer_list *t)
{
    unsigned long flags;

    spin_lock_irqsave(&lock, flags);

    /* 翻转 LED 逻辑状态 */
    led_state = !led_state;
    gpiod_set_value(led_gpio, led_state);

    /* 重新设定定时器,实现周期触发 */
    mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));

    spin_unlock_irqrestore(&lock, flags);

    pr_info("ledblink: LED %s (period=%dms)\n",
            led_state ? "ON" : "OFF", period_ms);
}

/* 打开设备 */
static int ledblink_open(struct inode *inode, struct file *file)
{
    pr_info("ledblink: device opened\n");
    return 0;
}

/* 关闭设备 */
static int ledblink_release(struct inode *inode, struct file *file)
{
    pr_info("ledblink: device closed\n");
    return 0;
}

/* 读取当前闪烁周期(返回字符串,单位毫秒) */
static ssize_t ledblink_read(struct file *file, char __user *buf,
                             size_t count, loff_t *f_pos)
{
    char kbuf[16];
    int len;
    unsigned long flags;
    int current_period;

    spin_lock_irqsave(&lock, flags);
    current_period = period_ms;
    spin_unlock_irqrestore(&lock, flags);

    len = snprintf(kbuf, sizeof(kbuf), "%d\n", current_period);
    if (*f_pos >= len)
        return 0;

    if (copy_to_user(buf, kbuf, len))
        return -EFAULT;

    *f_pos += len;
    return len;
}

/* 写入新的闪烁周期(单位毫秒) */
static ssize_t ledblink_write(struct file *file, const char __user *buf,
                              size_t count, loff_t *f_pos)
{
    char kbuf[16] = {0};
    unsigned long new_period;
    int ret;
    unsigned long flags;

    if (count > 15)
        count = 15;

    if (copy_from_user(kbuf, buf, count))
        return -EFAULT;

    ret = kstrtoul(kbuf, 0, &new_period);
    if (ret < 0)
        return -EINVAL;

    if (new_period == 0)
        return -EINVAL;   /* 不允许周期为0,否则定时器频繁触发 */

    spin_lock_irqsave(&lock, flags);
    period_ms = new_period;
    /* 更新定时器,使新周期立即生效 */
    mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
    spin_unlock_irqrestore(&lock, flags);

    pr_info("ledblink: period set to %ld ms\n", new_period);
    return count;
}

static struct file_operations ledblink_fops = {
    .owner   = THIS_MODULE,
    .open    = ledblink_open,
    .release = ledblink_release,
    .read    = ledblink_read,
    .write   = ledblink_write,
};

/* ---------------- platform_driver 部分 ---------------- */

static int ledblink_probe(struct platform_device *pdev)
{
    int ret;
    struct device *dev = &pdev->dev;

    pr_info("ledblink: probe called\n");

    /* 1. 获取 GPIO 描述符,初始化为低电平(LED灭) */
    led_gpio = gpiod_get(dev, NULL, GPIOD_OUT_LOW);
    if (IS_ERR(led_gpio)) {
        pr_err("ledblink: failed to get gpio\n");
        return PTR_ERR(led_gpio);
    }

    /* 2. 分配设备号 */
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("ledblink: alloc_chrdev_region failed\n");
        goto err_alloc;
    }

    /* 3. 初始化 cdev */
    cdev_init(&my_cdev, &ledblink_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret) {
        pr_err("ledblink: cdev_add failed\n");
        goto err_cdev_add;
    }

    /* 4. 创建 class */
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        pr_err("ledblink: class_create failed\n");
        ret = PTR_ERR(my_class);
        goto err_class_create;
    }

    /* 5. 创建设备节点 */
    my_device = device_create(my_class, dev, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        pr_err("ledblink: device_create failed\n");
        ret = PTR_ERR(my_device);
        goto err_device_create;
    }

    /* 6. 初始化内核定时器 */
    timer_setup(&blink_timer, blink_timer_callback, 0);
    /* 启动定时器:第一次触发在 period_ms 毫秒后 */
    mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));

    pr_info("ledblink: /dev/%s created, default period=%d ms\n",
            DEVICE_NAME, period_ms);
    return 0;

err_device_create:
    class_destroy(my_class);
err_class_create:
    cdev_del(&my_cdev);
err_cdev_add:
    unregister_chrdev_region(dev_num, 1);
err_alloc:
    gpiod_put(led_gpio);
    return ret;
}

static int ledblink_remove(struct platform_device *pdev)
{
    del_timer_sync(&blink_timer);    /* 安全删除定时器 */

    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);

    /* 关闭 LED */
    gpiod_set_value(led_gpio, 0);
    gpiod_put(led_gpio);

    pr_info("ledblink: module unloaded\n");
    return 0;
}

/* 设备树匹配表 */
static const struct of_device_id ledblink_of_match[] = {
    { .compatible = "yourname,gpioled" },
    { }
};
MODULE_DEVICE_TABLE(of, ledblink_of_match);

static struct platform_driver ledblink_driver = {
    .probe  = ledblink_probe,
    .remove = ledblink_remove,
    .driver = {
        .name           = "ledblink",
        .owner          = THIS_MODULE,
        .of_match_table = ledblink_of_match,
    },
};

module_platform_driver(ledblink_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("LED blink driver using kernel timer");
MODULE_VERSION("1.0");

代码核心说明

  • timer_setup :将 blink_timer 与回调函数绑定,第三个参数 flags 通常为 0。
  • mod_timer :重新设定定时器的 expires 并激活。在回调中再次调用 mod_timer,实现周期性。
  • 自旋锁 :保护 led_stateperiod_ms,因为定时器回调(软中断)与 write(进程上下文)可能并发访问。
  • led_state 翻转 :在回调中用 led_state = !led_state 并调用 gpiod_set_value,实现亮灭交替。
  • 默认周期DEFAULT_PERIOD_MS 为 500ms,用户可通过设备节点修改。
  • 错误处理 :probe 中采用 goto 链式回滚;remove 中调用 del_timer_sync 确保定时器不再运行。

五、Makefile

makefile 复制代码
# Makefile for led_blink_timer

KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

obj-m := led_blink_timer.o

all:
	make -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	make -C $(KERNEL_DIR) M=$(PWD) clean

交叉编译时设置 ARCHCROSS_COMPILE


六、测试与验证

6.1 确认设备树生效

开发板启动后,检查 GPIO LED 节点(/proc/device-tree/gpioled)是否存在。

6.2 加载驱动

bash 复制代码
insmod led_blink_timer.ko
dmesg | tail
# ledblink: probe called
# ledblink: /dev/ledblink created, default period=500 ms

此时 LED 应开始以 1Hz 频率闪烁(亮 500ms,灭 500ms)。

6.3 查看设备节点并赋权

bash 复制代码
ls -l /dev/ledblink
chmod 666 /dev/ledblink

6.4 读取当前周期

bash 复制代码
cat /dev/ledblink
# 输出 500

6.5 动态修改闪烁周期

bash 复制代码
echo 200 > /dev/ledblink   # 改为 200ms,闪烁明显加快
echo 1000 > /dev/ledblink  # 改为 1s

观察 LED 闪烁速度变化。

6.6 关闭 LED 并停止闪烁

写入 0 是无效的(驱动返回 -EINVAL),因为周期为 0 没有意义。若需停止闪烁,可直接卸载模块:

bash 复制代码
rmmod led_blink_timer

LED 会熄灭,设备节点消失。


七、高精度定时器(hrtimer)简介

timer_list 基于 jiffies,精度受 HZ 限制(通常为 1~10ms)。对于需要微秒甚至纳秒级精度的场景(如音频、精密控制),内核提供了高精度定时器(hrtimer)

hrtimer 基于 ktime_t 时间值,提供了比 jiffies 更精确的定时服务。其基本用法与 timer_list 类似,但能实现纳秒级的定时触发。当内核配置了 CONFIG_HIGH_RES_TIMERS 时,hrtimer 会自动使用高精度时钟源。对于普通的 LED 闪烁,timer_list 已经足够,但若需要精确定时(如产生特定 PWM 波形),可以考虑 hrtimer。其常用 API 包括:

  • hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL)
  • hrtimer_start(&timer, ktime_set(0, 500000000), HRTIMER_MODE_REL) (500ms)
  • hrtimer_cancel(&timer)

读者可自行查阅内核源码中的 samples/timers/ 示例。


八、轮询驱动的概念

本文实现的 LED 闪烁是由定时器主动翻转 GPIO,这属于典型的定时器驱动输出 。而另一类常见用法是轮询驱动输入:在没有硬件中断(或中断不可用)的情况下,使用定时器周期性地读取 GPIO 或其他寄存器的值,从而实现按键检测、传感器数据采集等。其优点是不依赖中断控制器,实现简单;缺点是占用 CPU 资源,功耗较高。在实际嵌入式系统中,如果能使用中断,应优先选择中断方式(如上一篇的输入子系统按键驱动),轮询通常用于中断资源紧张的极低成本场景。


九、总结与下篇预告

本文利用内核定时器 timer_list 实现了一个 LED 闪烁驱动,并可通过设备节点动态调整闪烁频率。我们再次实践了平台驱动、GPIO 控制和自旋锁保护共享数据的并发编程技巧。

下篇预告 :至此,我们已经掌握了字符设备驱动开发的核心技术。下一篇文章将是本系列的收官之作------我们将把这些知识点整合起来,实现一个综合性实例(例如结合 I2C 传感器和 LED 的多功能驱动),并探讨 Linux 内核驱动开发的学习路径与进阶方向。敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!

相关推荐
A小辣椒2 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒6 小时前
TShark:基础知识
linux
AlfredZhao8 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式