Linux 内核定时器与 DHT11

Linux 内核定时器与 DHT11:从内核机制到裸机驱动的完整解析

本文整理了中断处理、进程线程、文件描述符、内核定时器,以及 DHT11 单总线传感器驱动的核心知识点,适合嵌入式与 Linux 驱动方向的读者系统复习。


一、中断处理函数

1.1 什么是 ISR

中断处理函数是操作系统和嵌入式系统中响应硬件事件的核心机制。当外设产生中断请求时,CPU 暂停当前程序,保存现场,跳转到预先注册的中断处理函数执行,处理完成后再恢复现场并继续执行。

复制代码
正常执行 ──► 中断发生 ──► 保存现场 ──► 执行 ISR ──► 恢复现场 ──► 继续执行

在 Linux 内核中,ISR 运行在中断上下文,不能睡眠、不能阻塞,也不能执行可能引发调度的操作。

1.2 编写要点

  • 尽量短小:ISR 执行期间通常会屏蔽同级或更低优先级中断。
  • 不可阻塞:不能睡眠,不能调用可能引起调度的函数。
  • 注意并发:多核环境下同一中断可能在不同 CPU 上并发执行。
  • 清除中断标志:否则硬件会反复触发中断。
  • 返回值语义 :Linux 中通常返回 IRQ_HANDLED 或 IRQ_NONE。

1.3 通用模板

复制代码
void XXX_IRQHandler(void)
{
    uint32_t status = DEV->INT_STATUS;   // 1. 取状态
    if (!(status & DEV_INT_FLAG))        // 2. 判断是否本设备
        return;
    DEV->INT_CLR = status;               // 3. 清标志
    dev->counter++;                      // 4. 快速处理
    schedule_bottom_half(dev);           // 5. 触发下半部
}

核心原则:ISR 只做"确认中断 + 清标志 + 搬数据 + 发通知",其余交给下半部。


二、进程与线程

2.1 核心区别

维度 进程 线程
定义 资源分配的基本单位 CPU 调度的基本单位
地址空间 独立 共享所属进程
切换开销 大 小
通信方式 IPC 直接读写共享变量
隔离性 强 弱

2.2 内存布局

复制代码
进程地址空间
├── 代码段 / 数据段 / 堆     ← 线程共享
├── 文件描述符表             ← 线程共享
├── 线程1:栈 + 寄存器       ← 线程独有
├── 线程2:栈 + 寄存器       ← 线程独有

2.3 选型建议

场景 推荐
强隔离、稳定性优先 进程
频繁通信、数据共享多 线程
崩溃不能互相影响 进程

一句话:进程 = 资源 + 隔离;线程 = 执行 + 共享。


三、文件描述符

3.1 本质

文件描述符是一个非负整数 ,是进程访问内核资源的句柄。它本质上是进程 fd 表中的索引,指向内核的打开文件表项,最终再指向 inode。

复制代码
fd(整数)
  └── 属于 → 进程的 fd 表
        └── 指向 → 内核打开文件表项
              └── 指向 → inode

3.2 三个默认 fd

fd 名称 宏
0 标准输入 STDIN_FILENO
1 标准输出 STDOUT_FILENO
2 标准错误 STDERR_FILENO

3.3 分配规则

内核总是分配当前最小的可用整数 。这个特性常被用来做重定向:先 close(1),再 open(...),新文件就会占用 fd 1。

3.4 "一切皆文件"

fd 可以代表普通文件、目录、设备、管道、socket、eventfd、epoll 实例等。Unix 的统一 I/O 接口就体现在这里。


四、内核定时器

4.1 本质

内核定时器建立在硬件定时器之上:硬件产生 tick,内核用 tick 驱动软件定时器子系统,管理大量"在某个时刻执行某个函数"的请求。

复制代码
硬件定时器 ──► tick 中断 ──► 检查到期定时器 ──► 执行回调

关键点:timer_list 的回调运行在软中断上下文,不能睡眠。

4.2 时间基准

复制代码
unsigned long timeout = jiffies + msecs_to_jiffies(500); // 500ms 后
unsigned long timeout2 = jiffies + HZ;                    // 1 秒后
  • HZ:每秒 tick 数。
  • jiffies:自启动以来的 tick 计数。
  • jiffies 会回绕,比较必须使用 time_after() 等宏。

4.3 老代码的典型问题

下面这段代码很常见,但存在多处问题:

复制代码
static void timeout(unsigned long data)
{
    pr_info("timeout ok!\n");
}

static DEFINE_TIMER(mtimer, timeout, 0, 0);

static int __init kernel_timer_init(void)
{
    init_timer(&mtimer);       // 多余
    add_timer(&mtimer);        // 错误:expires=0 会立即触发
    mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));
    return 0;
}
问题 说明 修正
init_timer 多余 DEFINE_TIMER 已初始化 删掉
add_timer 时机错误 expires=0 会立即触发 删掉,只用 mod_timer
del_timer 不安全 不等待回调结束 改 del_timer_sync
回调签名过时 unsigned long data 是老接口 用 struct timer_list *

4.4 修正版

适配 Linux 4.15 之后以及 5.x/6.x 内核的写法如下:

复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/timer.h>
#include <linux/jiffies.h>

static struct timer_list mtimer;

static void timeout(struct timer_list *t)
{
    pr_info("timeout ok!\n");
}

static int __init kernel_timer_init(void)
{
    timer_setup(&mtimer, timeout, 0);
    mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));
    pr_info("add timer success\n");
    return 0;
}

static void __exit kernel_timer_exit(void)
{
    del_timer_sync(&mtimer);
    pr_info("del timer success\n");
}

module_init(kernel_timer_init);
module_exit(kernel_timer_exit);
MODULE_LICENSE("GPL");

4.5 老接口 vs 新接口

老接口 新接口
init_timer(&t) timer_setup(&t, fn, flags)
t.function = fn 在 timer_setup 里指定
t.data = x from_timer() 反推
void fn(unsigned long data) void fn(struct timer_list *t)

4.6 周期性定时器

复制代码
static void timeout(struct timer_list *t)
{
    pr_info("timeout ok!\n");
    mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));
}

卸载时必须使用 del_timer_sync(),否则回调可能又把自己重新加入定时器队列。


五、static 的两种作用

代码里大量出现 static,初学者容易疑惑:为什么只在本文件可见的函数还能被内核调用?

作用 修饰对象 含义
链接属性 全局变量/函数 只在本文件可见
存储期 局部变量 静态存储,函数退出不销毁

**关键在于:static 限制的是名字能否被其他文件引用,不限制函数能否被调用。**内核通过函数指针、宏注册拿到地址后,照样可以调用。

复制代码
module_init(kernel_timer_init);  // 把地址注册给内核
DEFINE_TIMER(mtimer, timeout, 0, 0); // 把 timeout 指针存进结构体

一句话:static 挡的是"名字",不是"指针"。


六、DHT11 温湿度传感器

6.1 基本参数

项目 参数
测量范围 温度 050°C,湿度 2090%RH
精度 温度 ±2°C,湿度 ±5%RH
分辨率 整数
供电 3.3V ~ 5.5V
通信 单总线
采样周期 ≥1 秒

6.2 通信原理

DHT11 使用单根数据线进行双向通信,主机通过拉低总线发起通信,DHT11 通过响应脉冲确认,再发送 40 位数据。

复制代码
主机:拉低 >18ms  →  释放  →  等待响应
DHT :拉低 80us   →  拉高 80us  →  传 40 位数据
每位:50us 低 + 高电平长短决定 0/1
      高 26~28us → 0
      高 70us    → 1

6.3 40 位数据格式

复制代码
[湿度整数 8位][湿度小数 8位][温度整数 8位][温度小数 8位][校验和 8位]

校验和等于前 4 个字节相加后的低 8 位。

6.4 硬件连接

复制代码
DHT11          MCU
VCC    ──────  3.3V/5V
DATA   ──────  GPIO(需 4.7k~10k 上拉电阻到 VCC)
GND    ──────  GND

上拉电阻必须加。模块版通常自带,裸传感器需要自己加。

6.5 核心读取代码

复制代码
uint8_t dht11_read(uint8_t *temp, uint8_t *humi)
{
    uint8_t buf[5] = {0};

    // 1. 主机发送起始信号
    pin_output();
    write_pin(0);
    delay_us(18000);        // 拉低 >18ms
    write_pin(1);
    delay_us(30);

    // 2. 切换输入,等待响应
    pin_input();
    if (wait_level(0, 100)) return 1;
    if (wait_level(1, 100)) return 2;
    if (wait_level(0, 100)) return 3;

    // 3. 读 40 位
    for (int i = 0; i < 5; i++) {
        for (int j = 0; j < 8; j++) {
            if (wait_level(1, 100)) return 4;
            delay_us(40);           // 延时采样
            buf[i] <<= 1;
            if (read_pin()) {
                buf[i] |= 1;
                wait_level(0, 100);
            }
        }
    }

    // 4. 校验
    if ((uint8_t)(buf[0]+buf[1]+buf[2]+buf[3]) != buf[4]) return 5;

    *humi = buf[0];
    *temp = buf[2];
    return 0;
}

6.6 常见问题

现象 原因 解决
一直读失败 没加上拉电阻 DATA 接 4.7k~10k 到 VCC
偶发失败 采样太快 间隔 ≥1s
数据全 0 时序不准 检查 delay_us
校验错 干扰/线太长 缩短线、加滤波电容
第一次失败 上电不稳定 上电后延时 1s 再读

6.7 注意事项

  1. 必须加外部上拉电阻。
  2. 两次读取间隔至少 1 秒。
  3. 读期间对时序敏感,裸机实现时通常要关中断或保证延时准确。
  4. delay_us 必须准确,建议使用 SysTick、硬件定时器或内核提供的精确延时函数。
  5. DHT11 只有整数输出,需要小数精度可选 DHT22。

七、总结

主题 核心要点
中断处理函数 短、不阻塞、清标志、下半部
进程 vs 线程 资源隔离 vs 执行共享
文件描述符 进程访问内核资源的整数句柄
内核定时器 tick + 软件管理;回调在软中断上下文
static 限制名字可见性,不限制调用
DHT11 单总线,靠高低电平时长编码

这些内容贯穿了从操作系统内核机制到裸机驱动开发的完整链路。理解中断、定时器、fd 这些抽象,再回头看 DHT11 这种时序敏感的传感器驱动,会发现底层逻辑是相通的:中断负责响应,定时器负责调度,fd 负责抽象资源,而驱动负责在时序约束下完成数据交互。


参考资料

  • Linux 内核文档:Documentation/timers/
  • DHT11 数据手册
  • 《Linux 设备驱动程序》

如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题可以在评论区交流,后续也可以继续整理 Linux 驱动、设备树、GPIO 子系统、定时器与中断相关的实战笔记。


相关推荐
KIDULT°9 小时前
MySQL系列 (1):数据库原理
运维·数据库·mysql·云计算
别动我齐刘海10 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
wewowilling12 小时前
认识微服务
java·运维·微服务·springcloud
玖石书12 小时前
Ubuntu 24.04 UFW 全命令攻略:从入门到端口开放实战
linux·运维·ubuntu
xiaoqiMikko12 小时前
Linux 实战(七):useradd、sudoers、umask、SGID、软链 —— 三台真机实测
linux·centos
weifont14 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
拂拉氏14 小时前
【知识讲解】 Linux之动静态库的了解
linux·运维·服务器·动静态库
Fcy64814 小时前
Linux下 五种IO模型的认识与⾮阻塞IO的实现
linux·服务器·io
Mikko715 小时前
Linux 重启后日志没了?进程被 OOM 杀了去哪看?logrotate 之后日志还往旧文件写?三台真机实测日志与性能排查
linux·运维·centos