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 内核驱动开发的学习路径与进阶方向。敬请期待!


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

相关推荐
艾莉丝努力练剑1 小时前
【Linux网络】Linux 网络编程:传输层TCP(二)
linux·运维·服务器·网络·tcp/ip·计算机网络
都在酒里2 小时前
Linux字符设备驱动开发(十):综合实例——I2C传感器 + LED智能控制与进阶指南
linux·运维·服务器·驱动开发·交互
2301_8090511410 小时前
Linux 网络编程 学习笔记
linux·网络·学习
wanhengidc10 小时前
服务器租用有何优点
运维·服务器·安全·web安全
hai31524754310 小时前
RISC-V核E203核前向旁路的架构性顽疾
驱动开发·架构·硬件架构·硬件工程·risc-v
hai31524754310 小时前
RISC-V CVA6 AXI适配器+DMA桥蜂鸟E203处理器的总线接口单元(BIU)仲裁器
驱动开发·fpga开发·硬件架构·硬件工程·精益工程
ZGi.ai10 小时前
人工审查节点:让自动化工作流多一步人工把关
运维·人工智能·自动化·人机协同·智能体工作流·人工审查
坤昱10 小时前
cfs调度类深入解刨——最新内核细节分析2
linux·服务器·cfs·cfs调度·eevdf调度·eevdf·kernel 7.1
艾莉丝努力练剑10 小时前
【Linux:文件】Ext系列文件系统进阶
linux·运维·服务器·c++·文件系统·文件io·ext