【嵌入式开发学习】第 52 天:嵌入式 Linux 核心基础 —— 字符设备驱动开发
本文深入解析Linux字符设备驱动开发,重点介绍GPIO输出(LED)和输入(按键中断)两种典型场景的实现方法。文章从驱动开发的核心思维转变开始,强调内核态编程规范,详细讲解字符设备驱动的五大核心组件:设备号管理、驱动注册、file_operations接口、设备文件创建和硬件操作函数。针对嵌入式开发特点,提供完整的LED驱动和按键驱动代码实现,包括GPIO子系统API使用、中断处理架构(顶半部+
核心目标:从Linux 内核与硬件的交互本质出发,全方位拆解字符设备驱动的核心架构、开发流程、内核 API 使用规范,通过LED(GPIO 输出) 和按键(GPIO 输入 + 中断) 两个经典实战案例,实现从驱动代码编写→交叉编译→加载测试的端到端落地,掌握设备号管理、驱动注册、GPIO 操作、中断处理四大核心技能,解决 “硬件无法控制、驱动加载失败、中断丢失、设备文件异常” 等嵌入式开发核心痛点,为后续 I2C/SPI/UART 等总线驱动开发奠定坚实基础。
一、本质追问:嵌入式 Linux 驱动的核心使命与开发准则
嵌入式系统的核心是 **“软件控制硬件”,而 Linux 内核作为系统的核心,无法直接识别物理硬件(如 GPIO、传感器、电机)——设备驱动是连接 Linux 内核与硬件的唯一桥梁 **,也是嵌入式开发从 “应用层” 走向 “底层” 的核心门槛。
1.1 裸机硬件操作的致命痛点
在无操作系统的裸机开发中,开发者通过直接读写物理寄存器控制硬件(如 GPIO 口、串口),这种方式在简单系统中可行,但在复杂的 Linux 系统中存在无法解决的问题:
- 软硬件强耦合:代码与硬件寄存器地址、引脚编号硬绑定,更换硬件(如从 STM32MP157 换到 RK3328)需重写所有硬件操作代码,可移植性为 0;
- 无资源管理:多个程序操作同一硬件时会发生资源冲突(如两个程序同时控制一个 GPIO),内核无法介入调度;
- 无标准化接口:每个硬件的操作方式都不同,应用程序需针对不同硬件编写专属代码,无法实现 “一次开发,多硬件适配”;
- 无法处理异步事件:对于按键、传感器等异步触发的硬件,只能通过轮询检测状态,大幅占用 CPU 资源,实时性差。
1.2 Linux 设备驱动的核心使命
Linux 内核为设备驱动设计了一套标准化、模块化、可扩展的架构,其核心使命可概括为 **“硬件抽象 + 标准化接口 + 资源管理”**,彻底解决裸机开发的痛点:
- 硬件抽象:将硬件的物理操作(读写寄存器、操作引脚、处理中断)封装为内核标准接口,屏蔽硬件底层差异 —— 应用程序无需关心寄存器地址、引脚编号,只需操作统一的接口;
- 标准化用户态接口:通过VFS(虚拟文件系统) 将驱动封装为设备文件(/dev/xxx),应用程序可通过
open/read/write/close等 POSIX 标准函数控制硬件,实现 “硬件无关的应用开发”; - 系统级资源管理:内核统一管理所有硬件资源(设备号、GPIO 引脚、中断号、DMA 通道),通过 **“申请 - 使用 - 释放”** 机制避免资源冲突,确保硬件操作的唯一性和安全性;
- 异步事件处理:内核提供完善的中断管理框架,支持硬件异步事件的高效处理,替代裸机的轮询方式,大幅提升 CPU 利用率;
- 模块化动态加载:驱动以内核模块(.ko 文件) 形式存在,可在系统运行时动态加载 / 卸载,无需重新编译内核、重启系统,大幅提升开发和调试效率。
1.3 嵌入式 Linux 驱动的三大分类与学习优先级
Linux 内核根据硬件的数据传输特性、工作方式,将设备驱动分为三大类,嵌入式开发中90% 以上的驱动属于字符设备驱动,也是入门的核心(块设备、网络设备驱动均基于字符设备驱动的基础扩展)。
| 驱动类型 | 核心特性 | 数据传输方式 | 嵌入式常见硬件 | 开发难度 | 学习优先级 |
|---|---|---|---|---|---|
| 字符设备驱动 | 按字符流顺序读写,无缓存,实时性高 | 字节 / 字符级,逐个传输 | GPIO、LED、按键、UART、I2C、SPI、ADC、PWM | 低(入门首选) | ★★★★★ |
| 块设备驱动 | 按数据块随机读写,有内核缓存 | 块级(512B/4KB),批量传输 | Flash、eMMC、SD 卡、硬盘 | 中 | ★★★ |
| 网络设备驱动 | 按数据包传输,面向网络协议 | 数据包级,基于 TCP/IP/UDP | 以太网、WiFi、4G/5G 模块 | 高 | ★★ |
核心结论:掌握字符设备驱动是嵌入式 Linux 驱动开发的入门关键—— 字符设备驱动的核心架构、内核 API、开发流程是所有其他驱动的基础,学会后可轻松扩展到 I2C/SPI/UART 等总线驱动。
1.4 嵌入式字符设备驱动的特殊性
相比于桌面 Linux 驱动,嵌入式字符设备驱动受限于硬件资源和场景需求,有三个核心特殊性,也是开发中的重点关注方向:
- 资源极度受限:内核内存、CPU 算力有限,驱动代码必须轻量化、无冗余,避免大量循环、复杂计算,中断处理、GPIO 操作的延迟需控制在微秒级;
- 与硬件强绑定:驱动开发必须深入理解硬件手册(寄存器地址、引脚定义、中断触发方式、电气特性),无硬件手册则无法开发驱动;
- 实时性要求高:工业控制、机器人、物联网等场景中,驱动需快速响应硬件事件(如按键按下、传感器数据采集),不允许出现中断丢失、GPIO 控制延迟等问题;
- 模块化要求严格:嵌入式系统通常无硬盘,驱动需以轻量 ko 模块存在,不能占用过多内核空间,且支持动态加载 / 卸载。
1.5 驱动开发的核心思维:从 “应用思维” 到 “内核思维”
从应用层开发转向驱动层开发,最核心的不是 API 的变化,而是思维方式的转变—— 驱动运行在内核态,内核的运行规则决定了驱动的开发准则,必须摒弃应用层的思维定式,建立 **“内核思维”**,遵循以下 6 条核心准则(缺一不可,否则会导致内核崩溃):
- 内核态与用户态严格隔离:驱动不能使用任何用户态库函数(如
printf/scanf/malloc/free/strcpy),必须使用内核专属 API(如printk/kmalloc/kfree/strncpy); - 绝对禁止阻塞 / 睡眠(关键路径):中断处理函数、自旋锁保护的代码段不能调用任何可能导致阻塞 / 睡眠的函数(如
kmalloc(GFP_KERNEL)、msleep、copy_from_user),否则会导致内核死锁; - 所有操作必须做错误处理:驱动中所有内核 API 调用(如申请设备号、注册驱动、申请 GPIO、申请中断)都必须检查返回值,且实现错误回滚—— 一步失败,释放已申请的所有资源,避免内核资源泄漏;
- 内存管理极致严格:内核内存是稀缺资源,驱动中分配的内存(
kmalloc/kzalloc/vmalloc)必须在驱动卸载 / 硬件反初始化时释放,且禁止内存越界、野指针; - 必须处理并发与同步:多进程 / 多线程同时操作同一硬件时,需通过自旋锁、互斥体实现同步,避免硬件操作冲突(如一个进程写 GPIO,另一个进程同时读 GPIO);
- 严格遵循内核编码规范:驱动代码需符合 Linux 内核的命名、注释、返回值规范(如错误返回负的 errno 码,
read/write成功返回实际传输字节数),否则会出现内核警告甚至运行异常。
二、内核基础:字符设备驱动的核心架构与必备 API
字符设备驱动是 Linux 内核中最简单、最基础的驱动架构,其核心是将硬件操作封装为标准化的内核接口,再通过 VFS 暴露为用户态可访问的设备文件。本节将拆解字符设备驱动的五大核心组件和嵌入式开发必备的内核 API,所有 API 均附带函数原型、核心参数、使用示例、嵌入式注意事项,可直接作为开发手册使用。
2.1 字符设备驱动的五大核心组件
字符设备驱动的实现围绕五大核心组件展开,这五个组件构成了驱动的完整框架,缺一不可,也是后续实战开发的核心脉络:
plaintext
设备号(唯一标识)→ 驱动注册/注销(内核识别)→ file_operations(接口封装)→ 设备文件(用户态入口)→ 硬件操作函数(物理控制)
- 设备号:驱动的唯一数字标识,由主设备号(major) 和次设备号(minor) 组成,内核通过主设备号区分不同类型的驱动,通过次设备号区分同一驱动下的不同硬件;
- 驱动注册 / 注销:将驱动的核心信息(设备号、操作接口)注册到 Linux 内核的字符设备子系统,让内核识别并管理该驱动;注销则是将驱动从内核中移除,释放相关资源;
- file_operations 结构体:驱动的核心接口集,封装了硬件的
open/close/read/write/ioctl等操作,是驱动与 VFS 的桥梁 ——VFS 将用户态的系统调用直接映射为该结构体中的对应函数; - 设备文件:驱动暴露给用户态的操作入口,位于
/dev目录下(如/dev/led、/dev/key),应用程序通过操作该文件实现对硬件的控制,无需关心底层硬件细节; - 硬件操作函数:驱动的核心业务逻辑,实现对硬件的物理控制(如 GPIO 引脚的高低电平设置、中断处理、寄存器读写),是驱动与硬件的直接交互层。
2.2 字符设备驱动必备内核 API(分类整理,直接可用)
驱动运行在内核态,必须使用内核专属 API,以下是嵌入式字符设备驱动开发中高频使用、必须掌握的内核 API,按功能分类整理,贴合嵌入式实战场景。
2.2.1 日志打印 API:printk(替代 printf)
内核中无printf函数,使用printk实现日志打印,支持日志级别,内核会根据级别决定是否输出日志,嵌入式开发中通过日志排查问题是核心手段。
c
// 函数原型
printk(const char *fmt, ...);
// 核心日志级别(从高到低,级别越高越优先输出,嵌入式常用前4种)
#define KERN_EMERG "<0>" // 紧急错误,系统崩溃前输出
#define KERN_ERR "<3>" // 普通错误,驱动执行失败(如GPIO申请失败)
#define KERN_WARNING "<4>" // 警告,不影响运行但需注意(如参数异常)
#define KERN_INFO "<6>" // 信息,普通日志(如驱动初始化成功、硬件状态变化)
#define KERN_DEBUG "<7>" // 调试日志,详细信息(嵌入式内核通常关闭)
使用示例:
c
printk(KERN_INFO "led driver init success\n"); // 普通信息
printk(KERN_ERR "gpio request failed: %ld\n", PTR_ERR(gpio_desc)); // 错误日志
嵌入式注意:
- 可通过
dmesg命令查看内核日志,dmesg | grep led过滤指定驱动日志; - 嵌入式内核通常会关闭
KERN_DEBUG级别日志,减少内存占用和日志输出量; - 日志内容需简洁,避免频繁打印日志(如中断处理函数中禁止打印日志),防止占用 CPU 资源。
2.2.2 内存管理 API:替代 malloc/free
内核提供多种内存分配 API,根据内存大小、使用场景选择,核心准则:分配必检查,释放必及时,嵌入式开发中优先使用kzalloc(自动初始化 0,避免野指针)。
| 函数原型 | 核心特性 | 适用场景 | 嵌入式注意事项 |
|---|---|---|---|
void *kzalloc(size_t size, gfp_t flags); |
分配连续物理内存,自动初始化为 0,速度快 | 驱动中小内存块(缓冲区、结构体、GPIO 描述符) | 最常用,flags 选GFP_KERNEL(普通场景)/GFP_ATOMIC(中断上下文) |
void kfree(const void *objp); |
释放kmalloc/kzalloc分配的内存 |
驱动卸载、硬件反初始化 | 不能重复释放,释放后将指针置 NULL |
void *vmalloc(unsigned long size); |
分配非连续物理内存,可分配大内存 | 驱动中大内存块(如帧缓冲区、数据缓存) | 速度慢,中断上下文不能使用 |
void vfree(const void *addr); |
释放vmalloc分配的内存 |
对应vmalloc的释放 |
- |
使用示例:
c
// 分配128字节内存,普通场景(可睡眠)
char *buf = kzalloc(128, GFP_KERNEL);
if (!buf) { // 必须检查返回值,NULL表示分配失败
printk(KERN_ERR "kzalloc failed\n");
return -ENOMEM; // 返回内核错误码
}
// 释放内存
kfree(buf);
buf = NULL; // 置空,避免野指针
gfp_t 标志嵌入式选型:
GFP_KERNEL:普通场景首选,允许睡眠,分配成功率最高;GFP_ATOMIC:中断上下文 / 自旋锁保护段必须使用,不允许睡眠,分配成功率较低;- 禁止在中断处理函数中使用
GFP_KERNEL,否则会导致内核死锁。
2.2.3 设备号管理 API:驱动的唯一标识
设备号是32 位无符号整数,格式为dev_t = (major << 20) | minor(32 位内核),其中主设备号占 12 位(0~4095),次设备号占 20 位(0~1048575)。内核中设备号是稀缺资源,必须先申请,后使用。
1. 设备号基础宏(合成 / 分解)
c
#define MKDEV(major, minor) // 主、次设备号合成dev_t
#define MAJOR(dev) // 从dev_t分解主设备号
#define MINOR(dev) // 从dev_t分解次设备号
2. 设备号申请 / 释放 API(嵌入式优先动态申请)
| 函数原型 | 特性 | 适用场景 |
|---|---|---|
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name); |
动态申请,内核自动分配未使用的主设备号 | 嵌入式开发首选,避免设备号冲突 |
int register_chrdev_region(dev_t dev, unsigned int count, const char *name); |
静态申请,指定固定主设备号 | 需固定设备号的场景(如工业设备) |
void unregister_chrdev_region(dev_t dev, unsigned int count); |
释放设备号 | 动态 / 静态申请的设备号都需释放 |
参数说明:
dev:传入传出参数,申请成功后存储分配的设备号;firstminor:次设备号起始值,通常为 0;count:要分配的设备号数量,通常为 1(一个驱动对应一个硬件);name:驱动名称,可通过cat /proc/devices查看。
使用示例:
c
dev_t dev_num; // 设备号
// 动态申请:次设备号从0开始,分配1个,驱动名称"led_drv"
int ret = alloc_chrdev_region(&dev_num, 0, 1, "led_drv");
if (ret < 0) {
printk(KERN_ERR "alloc chrdev region failed\n");
return ret;
}
// 分解主、次设备号
int major = MAJOR(dev_num);
int minor = MINOR(dev_num);
printk(KERN_INFO "major=%d, minor=%d\n", major, minor);
// 释放设备号(驱动卸载时执行)
unregister_chrdev_region(dev_num, 1);
2.2.4 字符设备注册 / 注销 API:内核识别驱动
申请设备号后,需将字符设备结构体(struct cdev) 注册到内核的字符设备子系统,让内核识别驱动的file_operations接口。
1. 核心结构体:struct cdev
c
struct cdev {
struct kobject kobj; // 内核对象(无需手动操作)
struct module *owner; // 驱动所属模块,固定为THIS_MODULE
const struct file_operations *ops; // 驱动的操作接口集(核心)
struct list_head list; // 内核字符设备链表(无需手动操作)
dev_t dev; // 驱动的设备号
unsigned int count; // 设备数量,通常为1
};
2. 核心操作 API
c
void cdev_init(struct cdev *cdev, const struct file_operations *fops); // 初始化cdev
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count); // 注册到内核
void cdev_del(struct cdev *cdev); // 从内核注销
使用示例:
c
struct cdev led_cdev; // 定义字符设备结构体
// 初始化cdev:绑定file_operations接口
cdev_init(&led_cdev, &led_fops);
led_cdev.owner = THIS_MODULE; // 固定为THIS_MODULE
// 注册到内核:设备号dev_num,设备数量1
int ret = cdev_add(&led_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "cdev add failed\n");
unregister_chrdev_region(dev_num, 1); // 错误回滚:释放设备号
return ret;
}
// 注销驱动(驱动卸载时执行)
cdev_del(&led_cdev);
核心注意:cdev_add失败后必须执行错误回滚,释放已申请的设备号,避免内核资源泄漏。
2.2.5 GPIO 子系统 API:硬件引脚控制(嵌入式核心)
嵌入式开发中,80% 的字符设备驱动都是GPIO 驱动(LED、按键、蜂鸣器、继电器等),Linux 内核提供GPIO 子系统 API(gpiolib),无需直接读写寄存器,大幅提升驱动的可移植性,嵌入式开发优先使用描述符式 API(gpiod_),替代老旧的编号式 API(gpio_)。
| 函数原型 | 功能 | 嵌入式使用场景 |
|---|---|---|
struct gpio_desc *gpiod_get(NULL, const char *name, enum gpiod_flags flags); |
申请 GPIO 引脚 | 初始化硬件,获取 GPIO 的操作句柄 |
void gpiod_put(struct gpio_desc *desc); |
释放 GPIO 引脚 | 反初始化硬件,释放资源 |
void gpiod_set_value(struct gpio_desc *desc, int value); |
设置 GPIO 输出值 | GPIO 输出(如 LED 亮灭:1 = 高电平,0 = 低电平) |
int gpiod_get_value(const struct gpio_desc *desc); |
获取 GPIO 输入值 | GPIO 输入(如按键状态:1 = 高电平,0 = 低电平) |
int gpiod_to_irq(const struct gpio_desc *desc); |
将 GPIO 转为中断号 | GPIO 中断(如按键按下触发中断) |
关键参数说明:
name:GPIO 引脚名(如"gpio100"、"key-gpio"),由设备树(DTS)定义,不同板卡只需修改该名称,驱动无需改动;flags:GPIO 属性,嵌入式常用:GPIOD_OUT_LOW:设为输出,默认低电平;GPIOD_OUT_HIGH:设为输出,默认高电平;GPIOD_IN:设为输入,可配合上拉 / 下拉(设备树中定义);
value:GPIO 值,1 = 高电平,0 = 低电平(与硬件连接方式匹配)。
使用示例(GPIO 输出:LED):
c
struct gpio_desc *led_gpio; // GPIO描述符(句柄)
// 申请GPIO:引脚名"led-gpio",输出,默认低电平(LED灭)
led_gpio = gpiod_get(NULL, "led-gpio", GPIOD_OUT_LOW);
if (IS_ERR(led_gpio)) { // 检查申请结果,IS_ERR判断是否为错误指针
printk(KERN_ERR "led gpio get failed: %ld\n", PTR_ERR(led_gpio));
return PTR_ERR(led_gpio);
}
// 控制LED亮:设置GPIO为高电平
gpiod_set_value(led_gpio, 1);
// 控制LED灭:设置GPIO为低电平
gpiod_set_value(led_gpio, 0);
// 释放GPIO(驱动卸载时)
gpiod_put(led_gpio);
嵌入式注意:
- GPIO 引脚名由设备树(DTS) 定义,需确保驱动中的
name与设备树一致; - 输入 GPIO 需在设备树中配置上拉 / 下拉电阻,避免引脚悬空导致的误触发(如按键 GPIO 配置上拉);
IS_ERR()是判断内核指针是否为错误指针的标准宏,PTR_ERR()可获取错误码。
2.2.6 file_operations 结构体:驱动与 VFS 的桥梁
file_operations是字符设备驱动的核心结构体,封装了硬件的所有操作接口,VFS 将用户态的open/read/write/close等系统调用,直接映射为该结构体中的对应函数,应用程序无需关心底层硬件细节。
1. 嵌入式常用的结构体成员(精简版)
c
struct file_operations {
struct module *owner; // 驱动所属模块,固定为THIS_MODULE(核心)
int (*open) (struct inode *inode, struct file *file); // 打开设备文件
int (*release) (struct inode *inode, struct file *file); // 关闭设备文件
ssize_t (*read) (struct file *file, char __user *buf, size_t count, loff_t *pos); // 读取硬件数据
ssize_t (*write) (struct file *file, const char __user *buf, size_t count, loff_t *pos); // 写入硬件数据
long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg); // 硬件复杂控制
};
2. 核心注意点(嵌入式开发必守)
owner:必须设置为 THIS_MODULE,内核会根据该字段维护驱动的引用计数—— 应用打开设备时引用计数 + 1,关闭时 - 1,引用计数 > 0 时驱动无法被卸载,避免驱动被卸载时应用仍在操作硬件;__user修饰符:表示该指针是用户态指针,内核不能直接读写(会导致内存访问异常),必须使用copy_from_user/copy_to_user实现内核态与用户态的数据拷贝;- 返回值规范:严格遵循内核返回值规则,如
read/write成功返回实际传输的字节数,失败返回负的 errno 码(如-EINVAL表示参数无效,-EFAULT表示内存访问异常)。
2.2.7 内核态 - 用户态数据拷贝 API
内核不能直接读写用户态指针,必须使用copy_from_user/copy_to_user实现数据拷贝,这两个函数会检查用户态指针的有效性,避免内核访问非法内存导致崩溃。
c
// 将用户态buf的数据拷贝到内核态to,拷贝count字节
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
// 将内核态from的数据拷贝到用户态buf,拷贝count字节
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
返回值:成功返回 0,失败返回未拷贝的字节数;使用示例:
c
char kernel_buf[1]; // 内核态缓冲区(存储LED的亮灭指令)
const char __user *user_buf = buf; // 用户态指针(来自write函数)
// 从用户态拷贝1个字节到内核态
if (copy_from_user(kernel_buf, user_buf, 1)) {
printk(KERN_ERR "copy from user failed\n");
return -EFAULT; // 返回内核错误码
}
2.2.8 设备文件自动创建 API:class/device
申请设备号、注册字符设备后,内核已识别驱动,但用户态还无对应的设备文件(/dev/xxx),传统方式需手动执行mknod命令创建,嵌入式开发中通过内核设备模型实现设备文件自动创建 / 删除,核心是class_create和device_create。
c
// 创建设备类:位于/sys/class/目录下,类名自定义
struct class *class_create(struct module *owner, const char *name);
// 销毁设备类
void class_destroy(struct class *cls);
// 创建设备:在class下创建设备,自动在/dev/目录下生成设备文件
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
// 销毁设备:自动删除/dev/目录下的设备文件
void device_destroy(struct class *cls, dev_t devt);
使用示例:
c
struct class *led_class;
struct device *led_device;
// 创建设备类:类名"led_class",位于/sys/class/led_class/
led_class = class_create(THIS_MODULE, "led_class");
if (IS_ERR(led_class)) { // 检查创建结果
printk(KERN_ERR "class create failed\n");
cdev_del(&led_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(led_class);
}
// 创建设备:自动生成/dev/led,设备号dev_num
led_device = device_create(led_class, NULL, dev_num, NULL, "led");
if (IS_ERR(led_device)) {
printk(KERN_ERR "device create failed\n");
class_destroy(led_class);
cdev_del(&led_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(led_device);
}
// 销毁设备和类(驱动卸载时)
device_destroy(led_class, dev_num);
class_destroy(led_class);
嵌入式注意:
- 设备类创建后会在
/sys/class/目录下生成对应文件夹,可通过ls /sys/class/查看; - 设备文件的权限默认是
root:root 600,可通过udev/mdev配置权限(如666,允许所有用户访问)。
2.2.9 内核模块加载 / 卸载 API
嵌入式字符设备驱动以内核模块(.ko) 形式存在,需实现模块的加载和卸载函数,内核通过以下宏指定模块的入口(加载) 和出口(卸载),且必须设置模块许可证(否则内核加载时会警告)。
c
module_init(init_func); // 指定模块加载函数(驱动入口)
module_exit(exit_func); // 指定模块卸载函数(驱动出口)
MODULE_LICENSE("GPL"); // 模块许可证,必须为GPL(核心)
MODULE_DESCRIPTION("Embedded Linux LED Driver"); // 模块描述(可选)
MODULE_AUTHOR("Embedded Developer"); // 模块作者(可选)
MODULE_VERSION("V1.0"); // 模块版本(可选)
核心要求:
- 加载函数(init_func):实现设备号申请、cdev 初始化与注册、类 / 设备创建、硬件初始化,必须做错误回滚,一步失败释放所有已申请资源;
- 卸载函数(exit_func):实现硬件反初始化、设备 / 类销毁、cdev 注销、设备号释放,资源释放顺序与加载顺序完全相反;
MODULE_LICENSE("GPL"):必须设置,Linux 内核是 GPL 协议,非 GPL 协议的模块会导致内核失去 GPL 保护,且部分内核 API 无法使用。
2.3 中断处理核心 API(GPIO 输入必备)
嵌入式中 GPIO 输入设备(如按键、触摸传感器)均通过中断实现异步触发,避免轮询占用 CPU 资源。Linux 内核的中断处理遵循 **“顶半部 + 底半部”** 架构,顶半部快速处理中断(清标志、调度底半部),底半部处理耗时操作(如数据处理、通知应用),嵌入式中底半部优先使用tasklet(轻量级,适用于小任务)。
2.3.1 中断核心概念
- 顶半部(Top Half):即中断处理函数,要求执行速度极快、不可阻塞、不可睡眠,核心工作:清除硬件中断标志、调度底半部、立即返回;
- 底半部(Bottom Half):处理中断的耗时操作,可阻塞、可睡眠,由内核在中断空闲时调度执行,嵌入式常用
tasklet实现; - 中断触发方式:嵌入式常用边沿触发(下降沿 / 上升沿 / 双边沿),避免电平触发的重复中断(如按键按下时电平保持高 / 低,会持续触发中断)。
2.3.2 中断处理必备 API
c
// 1. 将GPIO描述符转为中断号(GPIO中断核心)
int gpiod_to_irq(const struct gpio_desc *desc);
// 2. 注册中断处理函数(Linux5.0+推荐)
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);
// 3. 注销中断处理函数
void free_irq(unsigned int irq, void *dev_id);
// 4. tasklet底半部初始化
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
// 5. 调度tasklet(顶半部中调用)
void tasklet_schedule(struct tasklet_struct *t);
// 6. 销毁tasklet
void tasklet_kill(struct tasklet_struct *t);
关键参数说明:
irq:中断号(由gpiod_to_irq从 GPIO 描述符获取);handler:中断处理函数(顶半部),原型:irqreturn_t (*irq_handler_t)(int irq, void *dev_id),返回IRQ_HANDLED(处理成功)/IRQ_NONE(非本设备中断);flags:中断触发方式,嵌入式常用:IRQF_TRIGGER_FALLING:下降沿触发(如按键按下,GPIO 从高→低);IRQF_TRIGGER_RISING:上升沿触发(如按键松开,GPIO 从低→高);IRQF_TRIGGER_BOTH:双边沿触发(按下 / 松开都触发);
name:中断名称,可通过cat /proc/interrupts查看;dev_id:传递给中断处理函数的私有数据,中断共享时必须唯一,通常为驱动的全局结构体指针;tasklet_struct:tasklet 底半部结构体,func为底半部处理函数,data为传递给 func 的参数。
使用示例(按键中断):
c
struct gpio_desc *key_gpio;
int key_irq; // 中断号
struct tasklet_struct key_tasklet; // tasklet底半部
// 底半部处理函数(按键去抖、状态更新)
void key_tasklet_func(unsigned long data) {
int key_val = gpiod_get_value(key_gpio);
printk(KERN_INFO "key state: %d\n", key_val);
}
// 顶半部:中断处理函数
irqreturn_t key_irq_handler(int irq, void *dev_id) {
// 调度底半部,立即返回
tasklet_schedule(&key_tasklet);
return IRQ_HANDLED;
}
// 初始化中断
key_gpio = gpiod_get(NULL, "key-gpio", GPIOD_IN); // 申请GPIO输入
key_irq = gpiod_to_irq(key_gpio); // GPIO转中断号
tasklet_init(&key_tasklet, key_tasklet_func, 0); // 初始化tasklet
// 注册中断:下降沿触发,中断名称"key-irq"
int ret = request_irq(key_irq, key_irq_handler, IRQF_TRIGGER_FALLING, "key-irq", NULL);
if (ret < 0) {
printk(KERN_ERR "request irq failed\n");
return ret;
}
// 注销中断(驱动卸载时)
free_irq(key_irq, NULL);
tasklet_kill(&key_tasklet);
gpiod_put(key_gpio);
三、实战开发 1:LED 字符设备驱动(GPIO 输出)
理论的核心是落地,本节以嵌入式最经典的 LED 驱动为例,实现一套可直接运行、可移植、符合内核规范的字符设备驱动,涵盖代码编写→Makefile 编写→交叉编译→加载卸载→应用测试全流程,所有代码带详细注释,适配所有支持 Linux GPIO 子系统的嵌入式板卡(如 RK3328、IMX6ULL、STM32MP157)。
3.1 硬件分析(通用型,适配所有板卡)
LED 驱动的硬件原理极其简单,核心是GPIO 引脚控制 LED 的亮灭,硬件连接和控制逻辑为通用设计,不同板卡只需修改GPIO 引脚名,驱动代码无需任何改动。
- 硬件连接:LED 的阳极通过限流电阻(220Ω) 连接到嵌入式板卡的 GPIO 引脚,阴极接地(GND);
- 控制逻辑:
- GPIO 引脚输出高电平:GPIO 与 GND 之间产生电压差,电流流过 LED,LED亮;
- GPIO 引脚输出低电平:GPIO 与 GND 之间无电压差,无电流流过 LED,LED灭;
- 硬件操作本质:通过内核 GPIO 子系统 API 设置 GPIO 引脚的高低电平,无需直接读写寄存器。
3.2 开发需求分析
实现一个 LED 字符设备驱动,满足嵌入式开发的核心需求,兼顾易用性、可移植性、稳定性:
- 驱动以内核模块(.ko) 形式存在,支持动态加载 / 卸载;
- 动态申请设备号,自动创建 **/dev/led** 设备文件,无需手动执行
mknod; - 应用程序通过
write系统调用控制 LED 亮灭(写入 '1' 亮,写入 '0' 灭); - 完善的错误处理和资源释放,无内存泄漏、无资源残留;
- 高可移植性:不同板卡只需修改 GPIO 引脚名,驱动代码无需改动;
- 符合 Linux 内核编码规范,无内核警告,支持
dmesg日志调试。
3.3 完整 LED 驱动代码编写(带详细注释,直接可用)
驱动代码分为头文件引入→宏定义→全局变量→硬件操作函数→file_operations 接口→模块加载 / 卸载函数→模块信息7 个部分,代码遵循内核编码规范,有完善的错误回滚,可直接复制到嵌入式开发环境中使用。
3.3.1 驱动代码:led_drv.c
c
/*************************************************************************
* 嵌入式Linux LED字符设备驱动
* 功能:应用通过write(/dev/led, '1'/'0', 1)控制LED亮灭
* 适配:所有支持Linux GPIO子系统的嵌入式板卡(RK3328/IMX6ULL/STM32MP157)
* 核心:GPIO子系统(gpiod_*)+ 字符设备驱动框架
*************************************************************************/
#include <linux/init.h> // 模块加载/卸载宏
#include <linux/module.h> // 内核模块核心头文件
#include <linux/cdev.h> // 字符设备结构体头文件
#include <linux/fs.h> // file_operations头文件
#include <linux/uaccess.h> // 内核-用户态数据拷贝
#include <linux/gpio/consumer.h>// GPIO子系统API
#include <linux/err.h> // IS_ERR/PTR_ERR错误指针判断
/************************** 1. 宏定义:硬件与驱动配置(仅需修改此处适配不同板卡) **************************/
#define LED_DEV_NAME "led" // 设备文件名,自动创建/dev/led
#define LED_GPIO_NAME "led-gpio" // GPIO引脚名(与设备树DTS一致,核心适配点)
#define LED_ON '1' // LED亮:应用写入'1'
#define LED_OFF '0' // LED灭:应用写入'0'
#define DEV_CNT 1 // 设备数量,通常为1
/************************** 2. 全局变量:驱动核心数据 **************************/
static dev_t led_dev_num; // 设备号(主+次)
static struct cdev led_cdev; // 字符设备结构体
static struct class *led_class; // 设备类,自动创建/sys/class/led_class
static struct gpio_desc *led_gpio; // GPIO描述符(LED的操作句柄)
/************************** 3. 硬件操作函数:GPIO控制LED(与硬件直接交互) **************************/
/**
* @brief LED硬件初始化:申请GPIO,设置为输出
* @return 成功返回0,失败返回-errno
*/
static int led_hw_init(void)
{
// 申请GPIO引脚:与设备树名一致,输出,默认低电平(LED灭)
led_gpio = gpiod_get(NULL, LED_GPIO_NAME, GPIOD_OUT_LOW);
if (IS_ERR(led_gpio)) {
printk(KERN_ERR "LED GPIO apply failed: %ld\n", PTR_ERR(led_gpio));
return PTR_ERR(led_gpio);
}
printk(KERN_INFO "LED GPIO init success: %s\n", LED_GPIO_NAME);
return 0;
}
/**
* @brief LED硬件反初始化:释放GPIO
*/
static void led_hw_deinit(void)
{
if (!IS_ERR(led_gpio)) {
gpiod_put(led_gpio); // 释放GPIO引脚
led_gpio = NULL;
printk(KERN_INFO "LED GPIO deinit success\n");
}
}
/**
* @brief LED亮灭控制
* @param state: LED_ON('1')亮,LED_OFF('0')灭
* @return 成功返回0,失败返回-errno
*/
static int led_ctrl(char state)
{
if (IS_ERR(led_gpio)) {
return -ENODEV; // 设备未初始化
}
// 根据应用传入的状态控制GPIO
switch (state) {
case LED_ON:
gpiod_set_value(led_gpio, 1); // GPIO高电平,LED亮
printk(KERN_INFO "LED on\n");
break;
case LED_OFF:
gpiod_set_value(led_gpio, 0); // GPIO低电平,LED灭
printk(KERN_INFO "LED off\n");
break;
default:
printk(KERN_ERR "Invalid LED state: %c\n", state);
return -EINVAL; // 无效参数
}
return 0;
}
/************************** 4. file_operations接口实现:对接VFS,封装硬件操作 **************************/
/**
* @brief 打开设备文件:触发LED硬件初始化
*/
static int led_open(struct inode *inode, struct file *file)
{
int ret = led_hw_init();
if (ret < 0) {
return ret;
}
file->private_data = led_gpio; // 私有数据存储GPIO句柄(可选)
printk(KERN_INFO "LED device opened\n");
return 0;
}
/**
* @brief 关闭设备文件:触发LED硬件反初始化
*/
static int led_release(struct inode *inode, struct file *file)
{
led_hw_deinit();
printk(KERN_INFO "LED device closed\n");
return 0;
}
/**
* @brief 写入设备文件:应用控制LED亮灭的核心接口
* @param buf: 用户态缓冲区,存储'1'/'0'
* @param count: 写入字节数,必须为1
* @return 成功返回1,失败返回-errno
*/
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
char kernel_buf; // 内核态缓冲区(存储1个字符)
// 校验写入字节数:必须为1,否则返回无效参数
if (count != 1) {
printk(KERN_ERR "Invalid write count: %ld, must be 1\n", count);
return -EINVAL;
}
// 从用户态拷贝1个字符到内核态(用户态指针不能直接读写)
if (copy_from_user(&kernel_buf, buf, 1)) {
printk(KERN_ERR "Copy from user failed\n");
return -EFAULT; // 内存访问异常
}
// 控制LED亮灭
int ret = led_ctrl(kernel_buf);
if (ret < 0) {
return ret;
}
return 1; // 成功返回实际写入的字节数(内核规范)
}
/************************** 5. file_operations结构体初始化:绑定接口函数 **************************/
static const struct file_operations led_fops = {
.owner = THIS_MODULE, // 固定为THIS_MODULE,维护引用计数
.open = led_open, // 打开设备
.release = led_release, // 关闭设备
.write = led_write, // 写入设备(核心控制接口)
};
/************************** 6. 模块加载/卸载函数:资源申请与释放(核心) **************************/
/**
* @brief 模块加载函数:驱动入口,按顺序申请所有资源
* @return 成功返回0,失败返回-errno
*/
static int __init led_drv_init(void)
{
int ret;
/********** 步骤1:动态申请设备号 **********/
ret = alloc_chrdev_region(&led_dev_num, 0, DEV_CNT, LED_DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "Alloc chrdev region failed: %d\n", ret);
goto err_alloc; // 错误回滚:直接返回
}
printk(KERN_INFO "Alloc chrdev success: major=%d, minor=%d\n",
MAJOR(led_dev_num), MINOR(led_dev_num));
/********** 步骤2:初始化并注册字符设备 **********/
cdev_init(&led_cdev, &led_fops); // 绑定file_operations
led_cdev.owner = THIS_MODULE;
ret = cdev_add(&led_cdev, led_dev_num, DEV_CNT); // 注册到内核
if (ret < 0) {
printk(KERN_ERR "Cdev add failed: %d\n", ret);
goto err_cdev; // 错误回滚:释放设备号
}
printk(KERN_INFO "Cdev add success\n");
/********** 步骤3:创建设备类(为自动创建设备文件做准备) **********/
led_class = class_create(THIS_MODULE, LED_DEV_NAME "_class");
if (IS_ERR(led_class)) {
printk(KERN_ERR "Class create failed: %ld\n", PTR_ERR(led_class));
ret = PTR_ERR(led_class);
goto err_class; // 错误回滚:注销字符设备+释放设备号
}
printk(KERN_INFO "Class create success: /sys/class/%s_class\n", LED_DEV_NAME);
/********** 步骤4:创建设备,自动生成/dev/led **********/
ret = IS_ERR(device_create(led_class, NULL, led_dev_num, NULL, LED_DEV_NAME));
if (ret) {
printk(KERN_ERR "Device create failed: %d\n", ret);
goto err_device; // 错误回滚:销毁设备类+注销字符设备+释放设备号
}
printk(KERN_INFO "Device create success: /dev/%s\n", LED_DEV_NAME);
printk(KERN_INFO "LED driver init success\n");
return 0;
/********** 错误回滚:按资源申请逆序释放 **********/
err_device:
class_destroy(led_class); // 销毁设备类
err_class:
cdev_del(&led_cdev); // 注销字符设备
err_cdev:
unregister_chrdev_region(led_dev_num, DEV_CNT); // 释放设备号
err_alloc:
return ret;
}
/**
* @brief 模块卸载函数:驱动出口,按逆序释放所有资源
*/
static void __exit led_drv_exit(void)
{
/********** 资源释放顺序:与申请顺序完全相反 **********/
device_destroy(led_class, led_dev_num); // 销毁设备,删除/dev/led
class_destroy(led_class); // 销毁设备类
cdev_del(&led_cdev); // 注销字符设备
unregister_chrdev_region(led_dev_num, DEV_CNT); // 释放设备号
led_hw_deinit(); // 硬件反初始化,释放GPIO
printk(KERN_INFO "LED driver exit success\n");
}
/************************** 7. 模块信息:内核识别与描述 **************************/
module_init(led_drv_init); // 指定模块加载入口
module_exit(led_drv_exit); // 指定模块卸载入口
MODULE_LICENSE("GPL"); // 必须为GPL,否则内核警告
MODULE_DESCRIPTION("Embedded Linux LED Character Device Driver");
MODULE_AUTHOR("Embedded Developer");
MODULE_VERSION("V1.0");
MODULE_ALIAS("led:gpio-drv"); // 驱动别名(可选)
3.3.2 代码核心亮点
- 高可移植性:唯一的适配点是
LED_GPIO_NAME(GPIO 引脚名),与设备树一致即可,无需修改其他代码; - 完善的错误处理:每一步资源申请都有检查,错误时按逆序回滚,释放所有已申请资源,无内核资源泄漏;
- 符合内核规范:严格遵循内核的返回值、命名、注释规范,
read/write返回值符合内核要求,无内核警告; - 自动创建设备文件:通过
class_create/device_create实现/dev/led自动创建 / 删除,无需手动mknod; - 清晰的分层设计:硬件操作函数(
led_hw_*)与驱动接口函数(file_operations)完全分离,代码可读性、可维护性高; - 安全的资源管理:所有分配的资源(设备号、GPIO、cdev、class)都在卸载函数中释放,且释放顺序与申请顺序相反。
3.4 Makefile 编写(适配交叉编译,直接可用)
嵌入式驱动需要交叉编译(在 x86/x64 主机上编译出 ARM/ARM64 架构的.ko 文件),以下 Makefile 为通用型,只需修改内核源码路径和交叉编译工具链,即可适配所有嵌入式板卡。
3.4.1 Makefile 文件:Makefile
makefile
# 驱动模块名:与驱动代码中的cdev名称一致,obj-m += 模块名.o
obj-m += led_drv.o
# 设备数量:若多个文件编译,写obj-m += led_drv.o file1.o file2.o
# ************************** 需根据实际情况修改的配置 **************************
# 1. 嵌入式板卡的Linux内核源码目录(必须与板卡内核版本一致,已配置+编译)
KERNELDIR ?= /home/developer/linux-5.10.61
# 2. 交叉编译工具链前缀(根据板卡CPU架构选择,如ARM/ARM64/MIPS)
CROSS_COMPILE ?= arm-linux-gnueabihf-
# 3. CPU架构(ARM=arm,ARM64=arm64,x86=x86,MIPS=mips)
ARCH ?= arm
# *****************************************************************************
# 主机当前目录(无需修改)
PWD := $(shell pwd)
# 编译目标:生成.ko内核模块
all:
@echo "Compiling LED driver for $(ARCH)..."
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
# 清理目标:删除编译生成的所有文件
clean:
@echo "Cleaning LED driver..."
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
rm -rf .*.cmd *.mod.c *.o *.ko .tmp_versions modules.order Module.symvers
# 安装目标:将.ko文件拷贝到嵌入式板卡(需配置板卡IP,可选)
install:
@echo "Copying led_drv.ko to board..."
scp led_drv.ko root@192.168.1.100:/opt/drv/
3.4.2 关键配置说明(必看)
- KERNELDIR:嵌入式板卡的 Linux 内核源码目录,必须满足两个条件:
- 与板卡的内核版本完全一致(如板卡内核是 5.10.61,源码也必须是 5.10.61);
- 已完成内核配置(make defconfig/menuconfig) 和内核编译(make zImage),否则驱动编译会失败;
- CROSS_COMPILE:交叉编译工具链前缀,根据板卡 CPU 架构选择:
- ARM32:
arm-linux-gnueabihf-(最常用); - ARM64:
aarch64-linux-gnu-; - MIPS:
mips-linux-gnu-;
- ARM32:
- ARCH:CPU 架构,与板卡匹配:ARM32=arm,ARM64=arm64,x86=x86;
- 交叉编译工具链需提前安装在主机上,且添加到系统 PATH 中(可通过
arm-linux-gnueabihf-gcc -v验证是否安装成功)。
3.5 驱动交叉编译(生成.ko 文件)
在主机端(x86/x64 Ubuntu/CentOS)执行以下步骤,编译生成 ARM 架构的led_drv.ko驱动模块:
- 将
led_drv.c和Makefile放在同一目录下(如/home/developer/drv/led/); - 打开终端,进入该目录;
- 执行编译命令:
bash
运行
make - 编译成功标志:目录下生成
led_drv.ko、led_drv.o等文件,无编译错误和警告; - (可选)将驱动拷贝到嵌入式板卡:
bash
运行
(需确保主机与板卡在同一局域网,板卡开启 SSH 服务)。make install
编译失败排查:
- 内核源码未配置 / 编译:执行
make defconfig && make zImage在源码目录; - 交叉编译工具链未安装:安装对应架构的工具链(如 ARM32:
sudo apt install gcc-arm-linux-gnueabihf); - 内核版本不匹配:更换与板卡内核版本一致的源码。
3.6 驱动加载 / 卸载与测试(嵌入式板卡端)
将编译好的led_drv.ko拷贝到嵌入式板卡后,在板卡端(root 用户) 执行以下操作,完成驱动的加载、测试、卸载,所有操作均通过shell 命令实现,无需编写应用程序。
3.6.1 驱动加载
bash
# 加载LED驱动模块(核心命令)
insmod led_drv.ko
# 查看驱动是否加载成功
lsmod | grep led_drv
# 查看设备号是否申请成功(可看到led_drv对应的主设备号)
cat /proc/devices | grep led
# 查看设备文件是否自动创建(/dev/led)
ls -l /dev/led
# 查看内核日志,确认驱动初始化成功(核心调试命令)
dmesg | grep LED
加载成功标志:
lsmod能看到led_drv模块,显示占用内存大小;cat /proc/devices能看到led对应的主设备号(如 240);ls /dev/led能看到设备文件,格式为crw-r--r-- 1 root root 240, 0 Jan 1 00:00 led;dmesg | grep LED能看到LED driver init success、/dev/led create success等日志。
3.6.2 驱动测试:控制 LED 亮灭
无需编写 C 语言应用程序,直接通过echo 命令实现write系统调用,向/dev/led写入 '1'/'0' 控制 LED 亮灭:
bash
# 控制LED亮:向/dev/led写入'1'
echo 1 > /dev/led
# 查看内核日志,确认LED亮
dmesg | tail -5
# 控制LED灭:向/dev/led写入'0'
echo 0 > /dev/led
# 查看内核日志,确认LED灭
dmesg | tail -5
测试成功标志:
- 执行
echo 1 > /dev/led,物理 LED点亮,内核日志显示LED on; - 执行
echo 0 > /dev/led,物理 LED熄灭,内核日志显示LED off。
3.6.3 驱动卸载
bash
# 卸载LED驱动模块(核心命令)
rmmod led_drv
# 查看驱动是否卸载成功(无输出表示卸载成功)
lsmod | grep led_drv
# 查看设备文件是否自动删除(无输出表示删除成功)
ls -l /dev/led
# 查看内核日志,确认驱动卸载成功
dmesg | grep LED
卸载成功标志:
lsmod无led_drv模块输出;ls /dev/led无设备文件输出;dmesg | grep LED能看到LED driver exit success、LED GPIO deinit success等日志。
四、实战开发 2:按键字符设备驱动(GPIO 输入 + 中断)
完成 LED 驱动(GPIO 输出)后,本节实现按键驱动(GPIO 输入 + 中断),这是嵌入式字符设备驱动的进阶实战,核心掌握GPIO 输入、中断处理、顶半部 + 底半部架构、tasklet 使用,实现按键按下的异步触发,避免轮询占用 CPU 资源,应用程序可通过read系统调用读取按键状态。
4.1 硬件分析(通用型)
按键驱动的硬件原理为通用设计,与 LED 驱动互补,实现 GPIO 输入的核心场景:
- 硬件连接:按键的一端连接到嵌入式板卡的 GPIO 引脚,另一端接地(GND);
- GPIO 配置:GPIO 引脚设为输入模式,并配置上拉电阻(设备树中定义)—— 无按键按下时,GPIO 为高电平;按键按下时,GPIO 为低电平;
- 中断触发方式:采用下降沿触发—— 按键按下时,GPIO 从高电平→低电平,触发中断,实现异步检测。
4.2 开发需求分析
实现一个按键字符设备驱动,满足嵌入式 GPIO 输入的核心需求:
- 驱动以内核模块形式存在,支持动态加载 / 卸载,自动创建
/dev/key设备文件; - 采用中断 + tasklet架构,实现按键按下的异步触发,无轮询,CPU 占用率为 0;
- 实现按键去抖(软件去抖),避免机械抖动导致的重复中断;
- 应用程序通过
read系统调用读取按键状态(0 = 未按下,1 = 按下); - 完善的错误处理和资源释放,高可移植性,不同板卡只需修改 GPIO 引脚名。
4.3 完整按键驱动代码编写(带详细注释)
按键驱动基于 LED 驱动的字符设备框架,新增中断处理、tasklet 底半部、按键去抖、read 接口,代码与 LED 驱动风格一致,可直接运行。
4.3.1 驱动代码:key_drv.c
c
/*************************************************************************
* 嵌入式Linux 按键字符设备驱动(GPIO输入+中断+tasklet)
* 功能:按键按下触发中断,应用通过read(/dev/key)读取按键状态
* 适配:所有支持Linux GPIO子系统的嵌入式板卡
* 核心:GPIO输入 + 中断顶半部 + tasklet底半部 + 软件去抖
*************************************************************************/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/gpio/consumer.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/tasklet.h>
/************************** 1. 宏定义:硬件与驱动配置(仅需修改此处) **************************/
#define KEY_DEV_NAME "key" // 设备文件名,/dev/key
#define KEY_GPIO_NAME "key-gpio" // GPIO引脚名(与设备树一致)
#define DEV_CNT 1 // 设备数量
#define KEY_DELAY_MS 20 // 按键去抖延时(20ms)
#define KEY_STATE_UP 0 // 按键未按下
#define KEY_STATE_DOWN 1 // 按键按下
/************************** 2. 全局变量:驱动核心数据 **************************/
static dev_t key_dev_num; // 设备号
static struct cdev key_cdev; // 字符设备结构体
static struct class *key_class; // 设备类
static struct gpio_desc *key_gpio; // GPIO描述符
static int key_irq; // 中断号(GPIO转中断)
static int key_state = KEY_STATE_UP;// 按键状态,默认未按下
static struct tasklet_struct key_tasklet; // tasklet底半部
/************************** 3. 硬件操作与中断处理:按键核心逻辑 **************************/
/**
* @brief 按键软件去抖+状态更新(底半部:耗时操作)
*/
static void key_tasklet_func(unsigned long data)
{
int val;
// 软件去抖:延时20ms,再次读取GPIO状态
msleep(KEY_DELAY_MS);
val = gpiod_get_value(key_gpio);
// 按键按下:GPIO低电平(硬件连接为接地)
if (val == 0) {
key_state = KEY_STATE_DOWN;
printk(KERN_INFO "Key pressed, state: %d\n", key_state);
} else {
key_state = KEY_STATE_UP;
printk(KERN_INFO "Key released, state: %d\n", key_state);
}
}
/**
* @brief 按键中断处理函数(顶半部:快速处理)
*/
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
// 调度底半部,立即返回(顶半部核心要求:快)
tasklet_schedule(&key_tasklet);
return IRQ_HANDLED; // 表示中断处理成功
}
/**
* @brief 按键硬件初始化:申请GPIO+中断
*/
static int key_hw_init(void)
{
int ret;
// 申请GPIO:输入模式(设备树配置上拉)
key_gpio = gpiod_get(NULL, KEY_GPIO_NAME, GPIOD_IN);
if (IS_ERR(key_gpio)) {
printk(KERN_ERR "Key GPIO apply failed: %ld\n", PTR_ERR(key_gpio));
return PTR_ERR(key_gpio);
}
// GPIO转为中断号
key_irq = gpiod_to_irq(key_gpio);
if (key_irq < 0) {
printk(KERN_ERR "GPIO to IRQ failed: %d\n", key_irq);
ret = key_irq;
goto err_irq;
}
// 初始化tasklet底半部
tasklet_init(&key_tasklet, key_tasklet_func, 0);
// 注册中断:下降沿触发(按键按下,GPIO高→低)
ret = request_irq(key_irq, key_irq_handler, IRQF_TRIGGER_FALLING, KEY_DEV_NAME, NULL);
if (ret < 0) {
printk(KERN_ERR "Request IRQ failed: %d\n", ret);
goto err_req;
}
printk(KERN_INFO "Key GPIO+IRQ init success: %s, irq=%d\n", KEY_GPIO_NAME, key_irq);
return 0;
err_req:
tasklet_kill(&key_tasklet);
err_irq:
gpiod_put(key_gpio);
return ret;
}
/**
* @brief 按键硬件反初始化:释放GPIO+中断
*/
static void key_hw_deinit(void)
{
if (!IS_ERR(key_gpio)) {
free_irq(key_irq, NULL); // 注销中断
tasklet_kill(&key_tasklet); // 销毁tasklet
gpiod_put(key_gpio); // 释放GPIO
key_gpio = NULL;
printk(KERN_INFO "Key GPIO+IRQ deinit success\n");
}
}
/************************** 4. file_operations接口实现:应用读取按键状态 **************************/
static int key_open(struct inode *inode, struct file *file)
{
int ret = key_hw_init();
if (ret < 0) {
return ret;
}
printk(KERN_INFO "Key device opened\n");
return 0;
}
static int key_release(struct inode *inode, struct file *file)
{
key_hw_deinit();
printk(KERN_INFO "Key device closed\n");
return 0;
}
/**
* @brief 读取按键状态:应用通过read获取
*/
static ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
// 校验读取字节数:必须为1
if (count != 1) {
return -EINVAL;
}
// 将内核态的按键状态拷贝到用户态
if (copy_to_user(buf, &key_state, 1)) {
printk(KERN_ERR "Copy to user failed\n");
return -EFAULT;
}
// 读取后重置状态为未按下(可选,根据需求调整)
key_state = KEY_STATE_UP;
return 1; // 成功返回实际读取的字节数
}
/************************** 5. file_operations结构体初始化 **************************/
static const struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.release = key_release,
.read = key_read, // 核心接口:读取按键状态
};
/************************** 6. 模块加载/卸载函数 **************************/
static int __init key_drv_init(void)
{
int ret;
// 1. 动态申请设备号
ret = alloc_chrdev_region(&key_dev_num, 0, DEV_CNT, KEY_DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "Alloc chrdev failed: %d\n", ret);
return ret;
}
// 2. 初始化并注册字符设备
cdev_init(&key_cdev, &key_fops);
key_cdev.owner = THIS_MODULE;
ret = cdev_add(&key_cdev, key_dev_num, DEV_CNT);
if (ret < 0) {
printk(KERN_ERR "Cdev add failed: %d\n", ret);
goto err_cdev;
}
// 3. 创建设备类
key_class = class_create(THIS_MODULE, KEY_DEV_NAME "_class");
if (IS_ERR(key_class)) {
ret = PTR_ERR(key_class);
printk(KERN_ERR "Class create failed: %ld\n", ret);
goto err_class;
}
// 4. 创建设备文件
ret = IS_ERR(device_create(key_class, NULL, key_dev_num, NULL, KEY_DEV_NAME));
if (ret) {
printk(KERN_ERR "Device create failed: %d\n", ret);
goto err_device;
}
printk(KERN_INFO "Key driver init success\n");
return 0;
err_device:
class_destroy(key_class);
err_class:
cdev_del(&key_cdev);
err_cdev:
unregister_chrdev_region(key_dev_num, DEV_CNT);
return ret;
}
static void __exit key_drv_exit(void)
{
device_destroy(key_class, key_dev_num);
class_destroy(key_class);
cdev_del(&key_cdev);
unregister_chrdev_region(key_dev_num, DEV_CNT);
key_hw_deinit();
printk(KERN_INFO "Key driver exit success\n");
}
/************************** 7. 模块信息 **************************/
module_init(key_drv_init);
module_exit(key更多推荐



所有评论(0)