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


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

相关推荐
fei_sun7 小时前
网卡、网关、网桥、交换机、路由器
linux·服务器·网络
肖永威7 小时前
麒麟 V10 编译 Python 3.11 依赖包缺失与版本冲突问题解决实录
运维·python·麒麟操作系统
johnny2337 小时前
规范驱动开发SDD:Spec Kit、OpenSpec、BMAD-METHOD、Kiro Spec
驱动开发
志栋智能7 小时前
超自动化运维的治理框架与最佳实践
运维·自动化
2601_961593427 小时前
Mac 虚拟机跑 Linux 选哪个?CentOS 8 稳定版适配方案
linux·macos·centos
ljs6482739517 小时前
智慧商城(Smart Mall):基于 Spring Boot + Vue 3 的现代化电商平台实战
linux·vue.js·spring boot·后端
C137的本贾尼8 小时前
Agent 与 LLM 的交互协议:从 ReAct 到工具调用
人工智能·交互
run21professional8 小时前
系统包管理器和语言级包管理器的区别,及其源配置
linux
阿成学长_Cain8 小时前
Linux grpck命令超全详解|校验组文件完整性、修复/etc/group错误实战
linux·运维·服务器·网络
进击切图仔8 小时前
基于千问的白盒蒸馏 SOP
运维·服务器·人工智能·python·效率