Linux 驱动开发入门:从最简单的 hello 驱动到硬件交互

作者:4. 嵌入式铲屎官日期:2026/4/17

Linux 驱动开发入门:从最简单的 hello 驱动到硬件交互

🎉 写给未来的自己和领导:本文是 Linux 驱动开发的 入门级保姆教程,从零开始搭建驱动框架,逐行解释代码,记录每一个踩过的坑。无论你是刚接触内核编程,还是想快速上手 GPIO 中断,都能在这里找到清晰的思路和可复现的步骤。


📚 目录

  1. 引言:驱动是什么?
  2. 驱动的基本框架 —— 一切皆文件
  3. 实战:第一个 hello 驱动
    • 3.1 完整的驱动源码(带详细注释)
    • 3.2 编译驱动 —— Makefile 解析
    • 3.3 上机测试 —— 从 insmod 到读写 /dev/hello
    • 3.4 常见错误与解决方法
  4. 驱动与 APP 的数据传输 —— copy_to/from_user
  5. 驱动提供能力,不提供策略 —— 四种访问方式
  6. GPIO 子系统 —— 用编号控制引脚
    • 6.1 确定 GPIO 编号的方法
    • 6.2 基于 sysfs 操作 GPIO(用户态验证)
    • 6.3 GPIO 子系统的内核函数
  7. 中断处理 —— 让驱动响应硬件事件
    • 7.1 中断申请流程
    • 7.2 按键驱动框架(含定时器防抖)
  8. 总结与后续学习建议

1. 引言:驱动是什么?

在这里插入图片描述

一句话白话:驱动就是 内核中的“翻译官”。APP 说“我要读数据”,驱动把它翻译成硬件能懂的指令(拉高拉低 GPIO、读写寄存器),然后把硬件返回的结果再翻译回 APP 能理解的数据。

生活化类比 🏢:

  • APP = 公司老板,只会说“我要营业额”。
  • 驱动 = 财务经理,知道怎么查数据库、算报表,最后交给老板一个数字。
  • 硬件 = 服务器,只接受底层指令。

在 Linux 中,驱动最终以 .ko(kernel object)文件存在,可以动态加载和卸载。


2. 驱动的基本框架 —— 一切皆文件

Linux 的设计哲学是 “一切皆文件”。硬件设备也被抽象成文件(比如 /dev/hello),APP 使用标准的 open / read / write / ioctl / close 来访问。

2.1 核心结构体 file_operations

这个结构体是一张 函数跳转表,告诉内核:当 APP 对设备文件调用某个系统调用时,应该执行驱动的哪个函数。

c

1static const struct file_operations hello_drv = {
2    .owner   = THIS_MODULE,
3    .open    = hello_open,
4    .read    = hello_read,
5    .write   = hello_write,
6    .release = hello_release,
7};
8

2.2 驱动编写四步曲

  1. 构造 file_operations:填充分发函数。
  2. 注册字符设备register_chrdev 告诉内核这个驱动的主设备号。
  3. 入口函数:模块加载时执行,完成注册和自动创建设备节点。
  4. 出口函数:模块卸载时执行,清理资源。

2.3 自动创建设备节点 —— classdevice

传统方式需要手动 mknod 创建设备节点,太麻烦。现代驱动会这样做:

c

1hello_class = class_create(THIS_MODULE, "hello_class");
2device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
3
  • class_create/sys/class 下创建一个类。
  • device_create 会在 /dev 下自动生成 /dev/hello 节点。

3. 实战:第一个 hello 驱动

我们的目标是:写一个驱动,提供一个 /dev/hello 设备,APP 可以向它写入字符串,再读出来。

3.1 步骤

在这里插入图片描述
添加02.1_hello_transfer
在这里插入图片描述
如果你的vi compile_commands.json内容很少的话
那应该将"cc"改为"arm-buildroot-linux-gnueabihf-gcc"在这里插入图片描述

3.1 完整的驱动源码(带详细注释)

文件:hello_drv.c

c

1#include <linux/module.h>
2#include <linux/fs.h>          // file_operations, register_chrdev
3#include <linux/uaccess.h>     // copy_to_user, copy_from_user
4#include <linux/device.h>      // class_create, device_create
5
6static int major;                    // 主设备号,由内核自动分配
7static unsigned char hello_buf[100]; // 存储 APP 写入的数据
8
9//  APP 调用 open("/dev/hello") 时,这个函数会被执行
10static int hello_open(struct inode *node, struct file *filp)
11{
12    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
13    return 0;
14}
15
16//  APP 调用 read() 时执行
17static ssize_t hello_read(struct file *filp, char __user *buf,
18                          size_t size, loff_t *offset)
19{
20    unsigned long len = size > 100 ? 100 : size;
21    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
22    // 将内核空间的数据拷贝到用户空间
23    if (copy_to_user(buf, hello_buf, len)) {
24        return -EFAULT;
25    }
26    return len;
27}
28
29//  APP 调用 write() 时执行
30static ssize_t hello_write(struct file *filp, const char __user *buf,
31                           size_t size, loff_t *offset)
32{
33    unsigned long len = size > 100 ? 100 : size;
34    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
35    // 将用户空间的数据安全拷贝到内核空间
36    if (copy_from_user(hello_buf, buf, len)) {
37        return -EFAULT;
38    }
39    return len;
40}
41
42//  APP 调用 close() 时执行
43static int hello_release(struct inode *node, struct file *filp)
44{
45    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
46    return 0;
47}
48
49// 定义 file_operations 结构体,并初始化各个成员
50static const struct file_operations hello_drv = {
51    .owner   = THIS_MODULE,
52    .open    = hello_open,
53    .read    = hello_read,
54    .write   = hello_write,
55    .release = hello_release,
56};
57
58// 模块加载时执行的入口函数
59static int __init hello_init(void)
60{
61    // 注册字符设备,动态分配主设备号
62    major = register_chrdev(0, "100ask_hello", &hello_drv);
63    if (major < 0) {
64        printk("register_chrdev failed\n");
65        return major;
66    }
67
68    // 创建一个类,用于自动生成设备节点
69    hello_class = class_create(THIS_MODULE, "hello_class");
70    if (IS_ERR(hello_class)) {
71        printk("class_create failed\n");
72        unregister_chrdev(major, "100ask_hello");
73        return PTR_ERR(hello_class);
74    }
75
76    //  /dev 下创建设备节点 /dev/hello
77    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
78    printk("hello driver loaded, major=%d\n", major);
79    return 0;
80}
81
82// 模块卸载时执行的出口函数
83static void __exit hello_exit(void)
84{
85    device_destroy(hello_class, MKDEV(major, 0));
86    class_destroy(hello_class);
87    unregister_chrdev(major, "100ask_hello");
88    printk("hello driver unloaded\n");
89}
90
91module_init(hello_init);
92module_exit(hello_exit);
93MODULE_LICENSE("GPL");
94

代码解释(为什么要这么做):

  • __init__exit:告诉内核这些函数只在加载/卸载时使用,执行完后可以释放内存。
  • register_chrdev(0, "name", &fops):第一个参数 0 表示让内核自动分配主设备号。返回的主设备号保存在 major 中,用于后续创建设备节点。
  • copy_to_user / copy_from_user:绝不能直接使用 memcpy 拷贝用户空间的数据,因为用户空间可能非法或不在当前进程地址空间。这些函数会做安全检查。
  • IS_ERR 判断class_create 失败时返回的不是 NULL,而是一个错误码指针,需要用 IS_ERR 判断。

3.2 编译驱动 —— Makefile 解析

在同一目录下创建 Makefile

makefile

1# 指定内核源码路径(根据你的开发板修改)
2KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
3
4all:
5    make -C $(KERN_DIR) M=$(PWD) modules
6    $(CROSS_COMPILE)gcc -o hello_test hello_test.c
7
8clean:
9    make -C $(KERN_DIR) M=$(PWD) modules clean
10    rm -rf hello_test
11
12obj-m += hello_drv.o
13

解释

  • -C $(KERN_DIR):切换到内核源码目录,读取它的顶层 Makefile。
  • M=$(PWD):告诉内核回到当前目录编译模块。
  • obj-m += hello_drv.o:表示将 hello_drv.c 编译成 hello_drv.ko 模块。
  • 最后一行编译测试程序 hello_test.c,使用交叉编译工具链(环境变量已提前设置)。

执行 make 后,会生成 hello_drv.kohello_test

3.3 上机测试 —— 从 insmod 到读写 /dev/hello

步骤 1:将文件推送到开发板

bash

1adb push hello_drv.ko /root
2adb push hello_test /root
3

步骤 2:加载驱动

bash

1adb shell
2cd /root
3insmod hello_drv.ko
4

加载成功后,内核会打印 hello driver loaded, major=...。此时可以查看设备节点:

bash

1ls -l /dev/hello   # 应该存在
2cat /proc/devices | grep hello   # 查看主设备号
3

步骤 3:运行测试程序

测试程序 hello_test.c 源码:

c

1#include <sys/types.h>
2#include <sys/stat.h>
3#include <fcntl.h>
4#include <unistd.h>
5#include <stdio.h>
6#include <string.h>
7
8int main(int argc, char **argv)
9{
10    int fd;
11    int len;
12    char buf[100];
13
14    if (argc < 2) {
15        printf("Usage: %s <dev> [string]\n", argv[0]);
16        return -1;
17    }
18
19    fd = open(argv[1], O_RDWR);
20    if (fd < 0) {
21        printf("can not open file %s\n", argv[1]);
22        return -1;
23    }
24
25    if (argc == 3) {
26        // 写操作:将命令行参数写入驱动
27        len = write(fd, argv[2], strlen(argv[2]) + 1);  // +1 包含 '\0'
28        printf("write ret = %d\n", len);
29    } else {
30        // 读操作:从驱动读取之前写入的字符串
31        len = read(fd, buf, 100);
32        buf[99] = '\0';
33        printf("read str : %s\n", buf);
34    }
35
36    close(fd);
37    return 0;
38}
39

执行写操作:

bash

1./hello_test /dev/hello 100ask
2# 输出:write ret = 7
3

执行读操作:

bash

1./hello_test /dev/hello
2# 输出:read str : 100ask
3

步骤 4:查看内核打印信息

在另一个终端(或串口)执行 dmesg | tail,可以看到 hello_open, hello_write, hello_read, hello_release 的打印。

步骤 5:卸载驱动

bash

1rmmod hello_drv
2ls /dev/hello   # 应该已经消失
3

3.4 常见错误与解决方法

错误现象可能原因解决方法
insmod: ERROR: could not insert module hello_drv.ko: Device or resource busy主设备号冲突或已有同名驱动检查 cat /proc/devices,换一个名字或用动态分配
can not open file /dev/hello设备节点未自动创建检查 class_create 和 device_create 是否执行成功;手动 mknod /dev/hello c 245 0 临时测试
write ret = -1驱动中的 copy_from_user 失败检查用户空间指针是否有效,确认 len 不超过缓冲区
编译时 warning: ignoring return value of ‘copy_from_user’未检查返回值应该处理返回值,但初学可忽略

⚠️ 特别提醒:如果 register_chrdev 忘记写或参数错误,会导致 major 为 0,device_create 失败,最终 /dev/hello 不会出现。上面的源码中已经修正。


4. 驱动与 APP 的数据传输 —— copy_to/from_user

为什么不能直接 memcpy?

因为用户空间和内核空间是 隔离的。用户进程的虚拟地址在内核中可能没有映射,直接访问会导致 缺页异常 甚至内核崩溃。copy_to_usercopy_from_user 会检查地址有效性,并且处理缺页。

使用格式

c

1unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
2unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
3
  • 返回值:未能拷贝的字节数。0 表示全部成功,非 0 表示出错。
  • 因此正确用法应该检查返回值:

c

1if (copy_to_user(buf, hello_buf, len)) {
2    return -EFAULT;   // 返回错误码
3}
4

5. 驱动提供能力,不提供策略 —— 四种访问方式

在这里插入图片描述

驱动只负责 能不能读写,而 什么时候读写 由 APP 决定。常见的四种访问方式(以读取按键为例):

方式生活化类比驱动实现APP 行为
非阻塞(查询)妈妈时不时进房间看孩子醒了没read 函数立即返回数据或 -EAGAIN循环调用 read,每次不等待
阻塞(休眠-唤醒)妈妈陪孩子睡,醒了才醒没有数据时让进程休眠,中断中唤醒read 会一直等待直到有数据
poll(定闹钟)妈妈陪睡一会儿,设个闹钟实现 .poll 函数,支持超时调用 poll/select 设定等待时间
异步通知(信号)孩子醒了主动跑出房间喊妈妈在中断中发送 SIGIO 信号注册信号处理函数,无需主动读

理解“不提供策略”:驱动不应该规定 APP 必须用哪种方式,而是提供所有可能(非阻塞、阻塞、poll、异步通知),让 APP 根据自己的需求选择。


6. GPIO 子系统 —— 用编号控制引脚

驱动最终要操作硬件引脚。Linux 内核提供了 GPIO 子系统,统一管理所有 GPIO。

6.1 确定 GPIO 编号的方法

方法一:通过 /sys/kernel/debug/gpio 查看

bash

1cat /sys/kernel/debug/gpio
2

输出示例:

text

1gpiochip0: GPIOs 0-31, parent: platform/209c000.gpio, 209c000.gpio:
2 gpio-5   (                    |goodix_ts_int       ) in  hi IRQ
3 gpio-19  (                    |cd                  ) in  hi IRQ
4...
5

方法二:在 /sys/class/gpio 下查看每个 gpiochip 的 label

bash

1ls /sys/class/gpio/gpiochip* -d
2cat /sys/class/gpio/gpiochip0/label   # 得到 "209c000.gpio" 
3

对于 IMX6ULL,GPIO 编号公式:(bank-1)*32 + pin。例如 GPIO5_3 → (5-1)*32+3 = 131。

6.2 基于 sysfs 操作 GPIO(用户态验证)

不需要写驱动,就可以在命令行操作 GPIO(前提是该引脚没有被占用)。

bash

1# 导出引脚
2echo 131 > /sys/class/gpio/export
3# 设为输出
4echo out > /sys/class/gpio/gpio131/direction
5# 输出高电平
6echo 1 > /sys/class/gpio/gpio131/value
7# 解除导出
8echo 131 > /sys/class/gpio/unexport
9

如果出现 write error: Device or resource busy,说明该引脚已被某个驱动占用。

6.3 GPIO 子系统的内核函数

内核推荐使用 descriptor-based 的新接口(以 gpiod_ 开头):

功能新接口旧接口
获取 GPIOgpiod_get()gpio_request()
设置方向gpiod_direction_input()gpio_direction_input()
输出值gpiod_set_value()gpio_set_value()
输入值gpiod_get_value()gpio_get_value()
释放gpiod_put()gpio_free()

通常还需要配合设备树或平台数据来获取 GPIO 描述符。简单的测试可以直接使用旧接口。


7. 中断处理 —— 让驱动响应硬件事件

以按键为例,我们希望按下按键时,驱动程序能立即通知 APP。

7.1 中断申请流程

  1. 获得中断号gpio_to_irq(gpio_num)
  2. 注册中断处理函数request_irq(irq, handler, flags, name, dev)
  3. 在中断处理函数中
    • 分辨中断(如果有多个中断源)
    • 处理数据(如读取按键值,唤醒等待队列)
    • 清除中断(硬件相关)
  4. 卸载时释放中断free_irq(irq, dev)

7.2 按键驱动框架(含定时器防抖)

为什么需要定时器? 机械按键在按下和释放时会产生多个抖动,导致多次中断。用定时器延迟一小段时间,再读取稳定状态。

驱动骨架示例

c

1#include <linux/interrupt.h>
2#include <linux/gpio.h>
3
4static int gpio_irq;
5static struct timer_list key_timer;
6
7// 定时器回调函数:用于防抖
8static void key_timer_func(struct timer_list *t)
9{
10    int val = gpio_get_value(KEY_GPIO);
11    if (val == 0) { // 按下(假设低电平有效)
12        // 通知 APP(唤醒等待队列,或发送信号)
13    }
14}
15
16// 中断处理函数
17static irqreturn_t key_isr(int irq, void *dev_id)
18{
19    // 修改定时器,延迟 20ms 后执行防抖
20    mod_timer(&key_timer, jiffies + msecs_to_jiffies(20));
21    return IRQ_HANDLED;
22}
23
24static int __init key_init(void)
25{
26    // 申请 GPIO
27    gpio_request(KEY_GPIO, "my_key");
28    gpio_direction_input(KEY_GPIO);
29    // 获得中断号并注册
30    gpio_irq = gpio_to_irq(KEY_GPIO);
31    request_irq(gpio_irq, key_isr, IRQF_TRIGGER_FALLING, "my_key", NULL);
32    // 初始化定时器
33    timer_setup(&key_timer, key_timer_func, 0);
34    return 0;
35}
36
37static void __exit key_exit(void)
38{
39    free_irq(gpio_irq, NULL);
40    gpio_free(KEY_GPIO);
41    del_timer(&key_timer);
42}
43
  • jiffies 是内核的全局时间戳,msecs_to_jiffies(20) 将 20 毫秒转换成节拍数。
  • mod_timer 会修改定时器的超时时间,如果定时器还未触发,就重新计时。

8. 总结与后续学习建议

通过本文,你已经掌握了:

  • ✅ 驱动的基本框架(file_operations, 注册/注销, 自动创建设备节点)
  • ✅ 内核与用户空间的数据传输(copy_to/from_user
  • ✅ 四种访问方式的概念
  • ✅ GPIO 编号的确定和 sysfs 操作
  • ✅ 中断申请与定时器防抖

下一步可以学习

  • 设备树:如何描述 GPIO 和中断资源,让驱动更通用。
  • platform 驱动模型:将驱动和设备分离。
  • input 子系统:按键、触摸屏等输入设备的统一框架。
  • 内核调试技巧printk 的级别,/sys/kernel/debugftrace

推荐实验

  1. 修改 hello 驱动,增加 ioctl 方法,实现清空缓冲区功能。
  2. 写一个完整的按键驱动,支持阻塞和非阻塞读,并用 poll 测试。
  3. 将按键驱动和 LED 驱动结合,实现“按一下开关灯,再按一下关灯”。

🎉 恭喜你完成了 Linux 驱动开发的入门!记住:驱动就是提供能力,不提供策略。多写代码,多读内核源码,你会越来越强大。


Linux 驱动开发入门:从最简单的 hello 驱动到硬件交互》 是转载文章,点击查看原文


相关推荐


深入剖析 Redis 经典面试题
Thomas.Sir2026/4/9

1、什么是Redis?它主要用来什么的? Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、


260331-OpenWebUI统计所有Chat的对话字符个数
GuokLiu2026/4/1

1 OWUI启动脚本 # Open-WebUI Settings export DATA_DIR='data0331' export ENABLE_SIGNUP=True export DEFAULT_USER_ROLE='admin' export DEFAULT_GROUP_ID='xai' export OFFLINE_MODE=false export HF_HUB_OFFLINE=1 # OpenAI API 配置 export ENABLE_OLLAMA_API=false ex


[LangChain智能体本质论]中间件是如何参与Agent、Model和Tool三者交互的?
JaydenAI2026/3/23

LangChain的中间件(Middleware)是围绕Agent执行流程构建的“可插拔钩子系统”。它允许开发者在不修改核心逻辑的情况下,在执行的关键节点(如输入处理、模型调用前后、输出解析等)对数据流进行拦截、修改或验证。中间件类型以AgentMiddleware为基类。 1. AgentMiddleware AgentMiddleware是一个泛型类型,两个泛型参数分别代表状态和静态上下文的类型,我们可以利用state_schema字段得到状态类型。它的name属性返回中间件的名称,默认返回


haproxy案例项目(haproxy+dns+nginx+nfs+keepalived)
爱莉希雅&&&2026/3/15

HAProxy+Nginx+NFS+DNS 部署笔记 一、环境规划 主机名IP 地址安装软件角色说明haproxy192.168.72.100/24haproxy负载均衡器nginx1192.168.72.10/24nginx、nfs-utilsWeb 节点 1(挂载 NFS 共享)nginx2192.168.72.20/24nginx、nfs-utilsWeb 节点 2(挂载 NFS 共享)nfs192.168.72.30/24nfs-utilsNFS 文件共享服务器dns192.168.


Spring Cloud+AI :实现分布式智能推荐系统
我不是呆头2026/3/7

欢迎文末添加好友交流,共同进步! “ 俺はモンキー・D・ルフィ。海贼王になる男だ!” 引言 在当今数字化时代,推荐系统已成为电商平台、内容分发平台、社交网络等互联网产品的核心竞争力之一。从淘宝的"猜你喜欢"、抖音的精准内容推送,到 Netflix 的影视推荐,优秀的推荐系统不仅能显著提升用户留存率和转化率,更能为企业带来可观的商业价值。据统计,亚马逊约 35% 的销售额来自推荐系统,Netflix 则通过推荐算法为用户节省了每年约 10 亿美元的搜索成本。 然而,随着业


一文搞懂激活函数!
aicoting2026/2/27

推荐直接网站在线阅读:aicoting.cn 在深度学习中,激活函数(Activation Function)是神经网络的灵魂。它不仅赋予网络非线性能力,还决定了训练的稳定性和模型性能。那么,激活函数到底是什么?为什么我们非用不可?有哪些经典函数?又该如何选择? 所有相关源码示例、流程图、模型配置与知识库构建技巧,我也将持续更新在Github:AIHub,欢迎关注收藏! 1. 什么是激活函数,为什么需要激活函数 激活函数的核心作用就是为神经网络引入非线性。 为什么需要非线性? 想象一下,如果


【Python练习五】Python 正则与网络爬虫实战:专项练习(2道经典练习带你巩固基础——看完包会)
纯.Pure_Jin(g)2026/2/18

第一题 题目: 使用正则完成下列内容的匹配 匹配陕西省区号 029-12345匹配邮政编码 745100匹配邮箱 lijian@xianoupeng.com匹配身份证号 62282519960504337X 代码: import re # 1. 匹配陕西省区号 029-12345 pattern_area = r'^029-\d{5}$' # 精确匹配 029- 开头,后接5位数字 test_area = '029-12345' print("区号匹配:", re.match(pattern_


Claude Code Agent Teams:3个AI同时写代码,底层原理和主流框架对比
易安说AI2026/2/10

大家好,我是易安,AI超级个体,大厂程序员二孩奶爸。 Claude Opus 4.6 带来了 Agent Teams 功能,可以让多个 Claude Code 实例并行工作。我用它做了个小项目,踩了一些坑,也顺便把底层原理和市面上几个主流多 Agent 框架做了个对比。这篇文章干货比较多,建议收藏。 Agent Teams 到底是什么 简单说就是一个 Lead Agent 可以 spawn 出多个 Teammate Agent,每个 Teammate 是一个完全独立的 Claude Code 会


abigen使用教程 - go版本
Warson_L2026/2/1

在 Web3 后端开发中,abigen 是一个至关重要的工具。它能根据 Solidity 合约生成的 ABI(应用二进制接口)自动生成 Go 语言代码,让你像调用普通 Go 函数一样调用智能合约。 以下是详细的 abigen 使用教程。 第一步:安装 abigen 工具 abigen 是 go-ethereum 项目的一部分。你可以通过以下命令安装: # 安装最新版 abigen go install github.com/ethereum/go-ethereum/cmd/abigen@lat


Verifier-state pruning in BPF
mounter6252026/1/22

The BPF verifier works, on a theoretical level, by considering every possible path that a BPF program could take. As a practical matter, however, it needs to do that in a reasonable amount of time. At the 2025 Linux Plumbers Conference, Mahé Tardy an

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客