核心目标:从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 内核为设备驱动设计了一套标准化、模块化、可扩展的架构,其核心使命可概括为 **“硬件抽象 + 标准化接口 + 资源管理”**,彻底解决裸机开发的痛点:

  1. 硬件抽象:将硬件的物理操作(读写寄存器、操作引脚、处理中断)封装为内核标准接口,屏蔽硬件底层差异 —— 应用程序无需关心寄存器地址、引脚编号,只需操作统一的接口;
  2. 标准化用户态接口:通过VFS(虚拟文件系统) 将驱动封装为设备文件(/dev/xxx),应用程序可通过open/read/write/close等 POSIX 标准函数控制硬件,实现 “硬件无关的应用开发”;
  3. 系统级资源管理:内核统一管理所有硬件资源(设备号、GPIO 引脚、中断号、DMA 通道),通过 **“申请 - 使用 - 释放”** 机制避免资源冲突,确保硬件操作的唯一性和安全性;
  4. 异步事件处理:内核提供完善的中断管理框架,支持硬件异步事件的高效处理,替代裸机的轮询方式,大幅提升 CPU 利用率;
  5. 模块化动态加载:驱动以内核模块(.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 驱动,嵌入式字符设备驱动受限于硬件资源和场景需求,有三个核心特殊性,也是开发中的重点关注方向:

  1. 资源极度受限:内核内存、CPU 算力有限,驱动代码必须轻量化、无冗余,避免大量循环、复杂计算,中断处理、GPIO 操作的延迟需控制在微秒级
  2. 与硬件强绑定:驱动开发必须深入理解硬件手册(寄存器地址、引脚定义、中断触发方式、电气特性),无硬件手册则无法开发驱动;
  3. 实时性要求高:工业控制、机器人、物联网等场景中,驱动需快速响应硬件事件(如按键按下、传感器数据采集),不允许出现中断丢失、GPIO 控制延迟等问题;
  4. 模块化要求严格:嵌入式系统通常无硬盘,驱动需以轻量 ko 模块存在,不能占用过多内核空间,且支持动态加载 / 卸载。

1.5 驱动开发的核心思维:从 “应用思维” 到 “内核思维”

应用层开发转向驱动层开发,最核心的不是 API 的变化,而是思维方式的转变—— 驱动运行在内核态,内核的运行规则决定了驱动的开发准则,必须摒弃应用层的思维定式,建立 **“内核思维”**,遵循以下 6 条核心准则(缺一不可,否则会导致内核崩溃):

  1. 内核态与用户态严格隔离:驱动不能使用任何用户态库函数(如printf/scanf/malloc/free/strcpy),必须使用内核专属 API(如printk/kmalloc/kfree/strncpy);
  2. 绝对禁止阻塞 / 睡眠(关键路径):中断处理函数、自旋锁保护的代码段不能调用任何可能导致阻塞 / 睡眠的函数(如kmalloc(GFP_KERNEL)msleepcopy_from_user),否则会导致内核死锁;
  3. 所有操作必须做错误处理:驱动中所有内核 API 调用(如申请设备号、注册驱动、申请 GPIO、申请中断)都必须检查返回值,且实现错误回滚—— 一步失败,释放已申请的所有资源,避免内核资源泄漏;
  4. 内存管理极致严格:内核内存是稀缺资源,驱动中分配的内存(kmalloc/kzalloc/vmalloc)必须在驱动卸载 / 硬件反初始化时释放,且禁止内存越界、野指针;
  5. 必须处理并发与同步:多进程 / 多线程同时操作同一硬件时,需通过自旋锁、互斥体实现同步,避免硬件操作冲突(如一个进程写 GPIO,另一个进程同时读 GPIO);
  6. 严格遵循内核编码规范:驱动代码需符合 Linux 内核的命名、注释、返回值规范(如错误返回负的 errno 码read/write成功返回实际传输字节数),否则会出现内核警告甚至运行异常。

二、内核基础:字符设备驱动的核心架构与必备 API

字符设备驱动是 Linux 内核中最简单、最基础的驱动架构,其核心是将硬件操作封装为标准化的内核接口,再通过 VFS 暴露为用户态可访问的设备文件。本节将拆解字符设备驱动的五大核心组件嵌入式开发必备的内核 API,所有 API 均附带函数原型、核心参数、使用示例、嵌入式注意事项,可直接作为开发手册使用。

2.1 字符设备驱动的五大核心组件

字符设备驱动的实现围绕五大核心组件展开,这五个组件构成了驱动的完整框架,缺一不可,也是后续实战开发的核心脉络:

plaintext

设备号(唯一标识)→ 驱动注册/注销(内核识别)→ file_operations(接口封装)→ 设备文件(用户态入口)→ 硬件操作函数(物理控制)
  1. 设备号:驱动的唯一数字标识,由主设备号(major)次设备号(minor) 组成,内核通过主设备号区分不同类型的驱动,通过次设备号区分同一驱动下的不同硬件;
  2. 驱动注册 / 注销:将驱动的核心信息(设备号、操作接口)注册到 Linux 内核的字符设备子系统,让内核识别并管理该驱动;注销则是将驱动从内核中移除,释放相关资源;
  3. file_operations 结构体:驱动的核心接口集,封装了硬件的open/close/read/write/ioctl等操作,是驱动与 VFS 的桥梁 ——VFS 将用户态的系统调用直接映射为该结构体中的对应函数;
  4. 设备文件:驱动暴露给用户态的操作入口,位于/dev目录下(如/dev/led/dev/key),应用程序通过操作该文件实现对硬件的控制,无需关心底层硬件细节;
  5. 硬件操作函数:驱动的核心业务逻辑,实现对硬件的物理控制(如 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_createdevice_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 中断核心概念
  1. 顶半部(Top Half):即中断处理函数,要求执行速度极快、不可阻塞、不可睡眠,核心工作:清除硬件中断标志、调度底半部、立即返回;
  2. 底半部(Bottom Half):处理中断的耗时操作,可阻塞、可睡眠,由内核在中断空闲时调度执行,嵌入式常用tasklet实现;
  3. 中断触发方式:嵌入式常用边沿触发(下降沿 / 上升沿 / 双边沿),避免电平触发的重复中断(如按键按下时电平保持高 / 低,会持续触发中断)。
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 引脚名,驱动代码无需任何改动。

  1. 硬件连接:LED 的阳极通过限流电阻(220Ω) 连接到嵌入式板卡的 GPIO 引脚,阴极接地(GND);
  2. 控制逻辑
    • GPIO 引脚输出高电平:GPIO 与 GND 之间产生电压差,电流流过 LED,LED
    • GPIO 引脚输出低电平:GPIO 与 GND 之间无电压差,无电流流过 LED,LED
  3. 硬件操作本质:通过内核 GPIO 子系统 API 设置 GPIO 引脚的高低电平,无需直接读写寄存器。

3.2 开发需求分析

实现一个 LED 字符设备驱动,满足嵌入式开发的核心需求,兼顾易用性、可移植性、稳定性

  1. 驱动以内核模块(.ko) 形式存在,支持动态加载 / 卸载;
  2. 动态申请设备号,自动创建 **/dev/led** 设备文件,无需手动执行mknod
  3. 应用程序通过write系统调用控制 LED 亮灭(写入 '1' 亮,写入 '0' 灭);
  4. 完善的错误处理和资源释放,无内存泄漏、无资源残留;
  5. 高可移植性:不同板卡只需修改 GPIO 引脚名,驱动代码无需改动;
  6. 符合 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 代码核心亮点
  1. 高可移植性:唯一的适配点是LED_GPIO_NAME(GPIO 引脚名),与设备树一致即可,无需修改其他代码;
  2. 完善的错误处理:每一步资源申请都有检查,错误时按逆序回滚,释放所有已申请资源,无内核资源泄漏;
  3. 符合内核规范:严格遵循内核的返回值、命名、注释规范,read/write返回值符合内核要求,无内核警告;
  4. 自动创建设备文件:通过class_create/device_create实现/dev/led自动创建 / 删除,无需手动mknod
  5. 清晰的分层设计:硬件操作函数(led_hw_*)与驱动接口函数(file_operations)完全分离,代码可读性、可维护性高;
  6. 安全的资源管理:所有分配的资源(设备号、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 关键配置说明(必看)
  1. KERNELDIR嵌入式板卡的 Linux 内核源码目录,必须满足两个条件:
    • 与板卡的内核版本完全一致(如板卡内核是 5.10.61,源码也必须是 5.10.61);
    • 已完成内核配置(make defconfig/menuconfig)内核编译(make zImage),否则驱动编译会失败;
  2. CROSS_COMPILE:交叉编译工具链前缀,根据板卡 CPU 架构选择:
    • ARM32:arm-linux-gnueabihf-(最常用);
    • ARM64:aarch64-linux-gnu-
    • MIPS:mips-linux-gnu-
  3. ARCH:CPU 架构,与板卡匹配:ARM32=arm,ARM64=arm64,x86=x86;
  4. 交叉编译工具链需提前安装在主机上,且添加到系统 PATH 中(可通过arm-linux-gnueabihf-gcc -v验证是否安装成功)。

3.5 驱动交叉编译(生成.ko 文件)

主机端(x86/x64 Ubuntu/CentOS)执行以下步骤,编译生成 ARM 架构的led_drv.ko驱动模块:

  1. led_drv.cMakefile放在同一目录下(如/home/developer/drv/led/);
  2. 打开终端,进入该目录;
  3. 执行编译命令:

    bash

    运行

    make
    
  4. 编译成功标志:目录下生成led_drv.koled_drv.o等文件,无编译错误警告
  5. (可选)将驱动拷贝到嵌入式板卡:

    bash

    运行

    make install
    
    (需确保主机与板卡在同一局域网,板卡开启 SSH 服务)。

编译失败排查

  • 内核源码未配置 / 编译:执行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

卸载成功标志

  • lsmodled_drv模块输出;
  • ls /dev/led无设备文件输出;
  • dmesg | grep LED能看到LED driver exit successLED GPIO deinit success等日志。

四、实战开发 2:按键字符设备驱动(GPIO 输入 + 中断)

完成 LED 驱动(GPIO 输出)后,本节实现按键驱动(GPIO 输入 + 中断),这是嵌入式字符设备驱动的进阶实战,核心掌握GPIO 输入、中断处理、顶半部 + 底半部架构、tasklet 使用,实现按键按下的异步触发,避免轮询占用 CPU 资源,应用程序可通过read系统调用读取按键状态。

4.1 硬件分析(通用型)

按键驱动的硬件原理为通用设计,与 LED 驱动互补,实现 GPIO 输入的核心场景:

  1. 硬件连接:按键的一端连接到嵌入式板卡的 GPIO 引脚,另一端接地(GND);
  2. GPIO 配置:GPIO 引脚设为输入模式,并配置上拉电阻(设备树中定义)—— 无按键按下时,GPIO 为高电平;按键按下时,GPIO 为低电平;
  3. 中断触发方式:采用下降沿触发—— 按键按下时,GPIO 从高电平→低电平,触发中断,实现异步检测。

4.2 开发需求分析

实现一个按键字符设备驱动,满足嵌入式 GPIO 输入的核心需求:

  1. 驱动以内核模块形式存在,支持动态加载 / 卸载,自动创建/dev/key设备文件;
  2. 采用中断 + tasklet架构,实现按键按下的异步触发,无轮询,CPU 占用率为 0;
  3. 实现按键去抖(软件去抖),避免机械抖动导致的重复中断;
  4. 应用程序通过read系统调用读取按键状态(0 = 未按下,1 = 按下);
  5. 完善的错误处理和资源释放,高可移植性,不同板卡只需修改 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
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐