本文基于正点原子 i.MX6ULL 开发板,遵循 Linux 驱动课程中字符设备驱动框架,从零拆解一个完整的 LED 字符设备驱动。从硬件寄存器操作、内核驱动框架实现,到用户空间应用程序交互,,最终形成一套可复用的字符设备驱动开发模板。
一、Linux 字符设备驱动框架
Linux 秉承 **"一切皆是文件"** 的设计思想,字符设备作为三大设备类型(字符、块、网络)中最常用的一类,被抽象为文件系统中的设备文件。用户空间通过open/read/write/close等标准文件操作接口,即可访问底层硬件设备,而连接用户空间系统调用与内核硬件操作的桥梁,就是字符设备驱动。
字符设备驱动的核心组成如下:
- 设备号(dev_t) :32 位无符号整数,高 12 位为主设备号 (标识设备类型),低 20 位为次设备号 (标识同类型下的具体设备)。内核提供
alloc_chrdev_region(动态分配,推荐)和register_chrdev_region(静态注册)两套接口管理设备号。 - 字符设备结构体(cdev) :内核中描述字符设备的核心载体,通过
cdev_alloc申请、cdev_init绑定操作集、cdev_add注册到内核、cdev_del注销,完成字符设备的全生命周期管理。 - 文件操作集(file_operations):驱动的核心功能载体,结构体中包含大量函数指针,将用户空间的文件系统调用与内核驱动的硬件操作函数一一绑定,是用户与内核交互的核心入口。
- 用户 / 内核空间数据交互 :Linux 系统做了严格的权限隔离,用户空间无法直接访问内核空间内存,必须通过
copy_from_user(用户→内核)和copy_to_user(内核→用户)完成安全的数据拷贝。 - 设备节点自动创建 :通过
class_create创建设备类、device_create创建设备节点,让 udev/mdev 自动在/dev目录下生成设备文件,无需手动mknod。 - 并发与竞态保护 :针对多核 CPU、多进程同时访问设备的场景,通过互斥体
mutex保护临界资源(硬件寄存器、设备状态变量),避免数据错乱和硬件异常。 - sysfs 文件系统 :通过
device_create_file在/sys目录下创建设备属性节点,支持用户通过cat/echo命令直接调试硬件,无需编写专用应用程序,提升驱动的可调试性。
二、驱动头文件led_drv.h解析
头文件集中定义了硬件寄存器、设备状态宏、私有数据结构体和依赖的内核头文件,实现了代码的高内聚低耦合。
c
运行
#ifndef __LED_DRV_H__
#define __LED_DRV_H__
// 硬件寄存器物理地址定义(对应i.MX6ULL GPIO1_IO03引脚,开发板LED)
#define IOMUX_MUX_REGADDR 0x020E0068 // 引脚复用寄存器
#define IOMUX_PAD_REGADDR 0x020E02F4 // 引脚电气属性寄存器
#define GPIO_DIR_REGADDR 0x0209C004 // GPIO方向寄存器
#define GPIO_DAT_REGADDR 0x0209C000 // GPIO数据寄存器
// LED状态宏定义,与应用层保持统一
#define LED_ON 1
#define LED_OFF 0
// 内核依赖头文件
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include <linux/mutex.h>
// 驱动私有数据结构体:封装所有驱动相关资源,避免全局变量泛滥
typedef struct led_drv {
struct cdev *pcdev; // 字符设备核心结构体
struct device *pdev; // 设备节点结构体
struct class *pclass; // 设备类结构体
dev_t devno; // 设备号(主+次)
int curledstat; // LED当前状态
struct mutex lock; // 互斥锁,保护临界资源
// 寄存器映射后的内核虚拟地址
void __iomem *piomux_mux_regaddr;
void __iomem *piomux_pad_regaddr;
void __iomem *pgpio_dir_regaddr;
void __iomem *pgpio_dat_regaddr;
}led_drv_t;
#endif
代码细节分析
- 头文件保护宏 :
#ifndef __LED_DRV_H__是 C 语言标准的头文件保护机制,防止头文件被多次包含导致的重定义编译错误。 - 硬件寄存器地址:4 个寄存器地址均来自 i.MX6ULL 官方数据手册,分别对应引脚复用、电气属性、GPIO 方向、GPIO 数据功能,是驱动操作硬件的基础。
- 私有数据结构体
led_drv_t:将驱动所有资源(字符设备、设备号、锁、寄存器地址、设备状态)封装在一个结构体中,避免了全局变量的使用,既保证了代码的可维护性,也为后续多设备扩展(多 LED)提供了基础。 __iomem修饰符:内核专用修饰符,标识该指针指向的是 IO 映射后的虚拟地址,而非普通内存地址,既可以做代码语义提示,也能让静态检查工具发现非法的内存操作。
三、驱动核心代码led_drv.c分析
驱动代码是完全遵循 Linux 字符设备驱动的开发流程,我们将按功能模块拆解,逐行分析实现逻辑与设计思路。
1. 基础定义与全局变量
c
运行
#include "led_drv.h"
// 驱动私有数据指针,静态变量限制作用域仅在本文件内
static led_drv_t *pledcfg;
这里定义了一个静态的结构体指针,所有资源都在入口函数中动态申请,而非定义全局结构体变量。
2. sysfs 属性节点实现
sysfs 节点为驱动提供了无需应用程序的调试能力。
c
运行
// sysfs节点读操作:cat /sys/class/myled/myled0/attr 时触发
static ssize_t led_show(struct device *dev, struct device_attribute *attr, char *buf)
{
mutex_lock(&pledcfg->lock);
// 打印当前LED状态到内核日志
if (LED_ON == pledcfg->curledstat) {
pr_info("LED_ON\n");
}
else if (LED_OFF == pledcfg->curledstat) {
pr_info("LED_OFF\n");
}
mutex_unlock(&pledcfg->lock);
return 0;
}
// sysfs节点写操作:echo LED_ON > /sys/class/myled/myled0/attr 时触发
static ssize_t led_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
char tmpbuff[32] = {0};
// 从用户空间获取输入的字符串
sscanf(buf, "%s", tmpbuff);
mutex_lock(&pledcfg->lock);
// 匹配指令,操作硬件寄存器
if (0 == strcmp(tmpbuff, "LED_ON")) {
// GPIO1_IO03输出低电平,点亮LED
writel(readl(pledcfg->pgpio_dat_regaddr) & ~(0x1 << 3), pledcfg->pgpio_dat_regaddr);
pledcfg->curledstat = LED_ON;
} else if (0 == strcmp(tmpbuff, "LED_OFF")) {
// GPIO1_IO03输出高电平,熄灭LED
writel(readl(pledcfg->pgpio_dat_regaddr) | (0x1 << 3), pledcfg->pgpio_dat_regaddr);
pledcfg->curledstat = LED_OFF;
}
mutex_unlock(&pledcfg->lock);
return count;
}
// 定义sysfs属性结构体,绑定读写函数,设置文件权限0664
static struct device_attribute led_attr = {
.attr = {
.name = "attr",
.mode = 0664,
},
.show = led_show,
.store = led_store,
};
代码细节分析
- 函数参数规范 :
show和store函数的入参是内核固定格式,dev为对应设备结构体,buf为用户空间的数据缓冲区,count为数据长度。 - 临界资源保护 :所有对 LED 状态和硬件寄存器的操作,都被
mutex_lock/mutex_unlock包裹,确保同一时间只有一个线程操作临界资源,避免竞态导致的状态错乱。 - 寄存器操作函数 :
readl/writel是内核提供的 32 位寄存器读写函数,专门用于 IO 内存操作。LED 点亮时通过& ~(0x1 << 3)将 bit3 清 0,熄灭时通过| (0x1 << 3)将 bit3 置 1,完全匹配 i.MX6ULL 开发板 LED 的硬件设计(阴极接 GPIO,阳极接电源,低电平点亮)。 - 返回值规范 :
store函数必须返回成功写入的字节数count,否则用户空间会认为写入失败,触发重试逻辑;show函数应返回写入buf的字节数,当前代码返回 0,仅做内核日志打印,可优化为将状态写入用户缓冲区。 - 文件权限 :
mode = 0664表示所有者、同组用户可读可写,其他用户只读,适配 sysfs 节点的权限设计规范,避免开放过高权限带来的安全问题。
3. 文件操作集接口实现
这部分实现了file_operations中的常用函数,对应用户空间的open/close/read/write系统调用。
c
运行
// 对应应用层open():打开设备文件时触发
static int led_open(struct inode *node, struct file *fp)
{
pr_info("Kernel:led open success\n");
return 0;
}
// 对应应用层close():关闭设备文件时触发
static int led_close(struct inode *node, struct file *fp)
{
pr_info("Kernel:led close success\n");
return 0;
}
// 对应应用层read():读取LED当前状态
static ssize_t led_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
unsigned long nret = 0;
// 内核空间→用户空间数据拷贝
nret = copy_to_user(puser, &pledcfg->curledstat, 4);
if (nret != 0) {
pr_info("copy_to_user failed\n");
return -EFAULT;
}
pr_info("Kernel:led read success\n");
return 4; // 修复:返回成功拷贝的字节数,符合read函数规范
}
// 对应应用层write():设置LED亮灭状态
static ssize_t led_write(struct file *fp, const char __user *puser, size_t n, loff_t *off)
{
int setstat = 0;
unsigned long nret = 0;
// 用户空间→内核空间数据拷贝
nret = copy_from_user(&setstat, puser, 4);
if (nret != 0) {
pr_info("copy_from_user failed\n");
return -EFAULT;
}
mutex_lock(&pledcfg->lock);
// 根据用户指令设置LED状态
if (LED_ON == setstat) {
writel(readl(pledcfg->pgpio_dat_regaddr) & ~(0x1 << 3), pledcfg->pgpio_dat_regaddr);
pledcfg->curledstat = LED_ON;
}
else if (LED_OFF == setstat) {
writel(readl(pledcfg->pgpio_dat_regaddr) | (0x1 << 3), pledcfg->pgpio_dat_regaddr);
pledcfg->curledstat = LED_OFF;
}
mutex_unlock(&pledcfg->lock);
pr_info("Kernel:led write success\n");
return 4; // 修复:返回成功写入的字节数,符合write函数规范
}
// 绑定文件操作集,建立用户系统调用与内核函数的映射
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_close,
.read = led_read,
.write = led_write,
};
代码细节分析
__user修饰符:内核专用修饰符,标识该指针指向用户空间地址,提醒开发者不能直接解引用,必须通过专用拷贝函数操作,同时可被静态检查工具识别非法操作。copy_to_user/copy_from_user:用户与内核空间数据交互的唯一安全接口,会先检查用户空间地址的合法性,再完成数据拷贝。返回值为 0 表示拷贝成功,非 0 表示失败,此时应返回-EFAULT错误码,而非原代码的 - 1,符合 Linux 内核错误码规范。- 返回值修复说明 :原代码中
read/write函数返回 0,这不符合 POSIX 标准。read函数应返回成功读取的字节数,write函数应返回成功写入的字节数,否则应用层的 C 库会认为 IO 操作失败,触发重试逻辑,导致功能异常。 owner = THIS_MODULE:必须设置的成员,将该操作集绑定到当前内核模块,防止模块在被使用时被意外卸载,导致内核崩溃。release函数 :对应应用层的close,而非close函数名,这是内核file_operations的固定命名规范。
4. 驱动入口函数led_drv_init
驱动入口函数是模块加载时执行的第一行代码,负责所有资源的申请、初始化和注册。
c
运行
static int __init led_drv_init(void)
{
int ret = 0;
// 1. 申请驱动私有数据结构体内存
pledcfg = kmalloc(sizeof(*pledcfg), GFP_KERNEL);
if (NULL == pledcfg) {
pr_info("kmalloc pledcfg failed\n");
return -ENOMEM;
}
// 2. 申请字符设备cdev结构体
pledcfg->pcdev = cdev_alloc();
if (NULL == pledcfg->pcdev) {
pr_info("cdev_alloc failed\n");
ret = -ENOMEM;
goto err_kmalloc;
}
// 3. 动态分配设备号(次设备号从0开始,申请1个设备)
ret = alloc_chrdev_region(&pledcfg->devno, 0, 1, "myled");
if (-1 == ret) {
pr_info("alloc_chrdev_region failed\n");
goto err_cdev_alloc;
}
// 4. 绑定字符设备与文件操作集
pledcfg->pcdev->ops = &fops;
// 5. 将字符设备添加到内核链表,完成注册
ret = cdev_add(pledcfg->pcdev, pledcfg->devno, 1);
if (-1 == ret) {
pr_info("cdev_add failed\n");
goto err_unregister_chrdev;
}
// 6. 初始化互斥锁
mutex_init(&pledcfg->lock);
// 7. 物理寄存器→内核虚拟地址映射
pledcfg->piomux_mux_regaddr = ioremap(IOMUX_MUX_REGADDR, 4);
pledcfg->piomux_pad_regaddr = ioremap(IOMUX_PAD_REGADDR, 4);
pledcfg->pgpio_dat_regaddr = ioremap(GPIO_DAT_REGADDR, 4);
pledcfg->pgpio_dir_regaddr = ioremap(GPIO_DIR_REGADDR, 4);
// 8. 硬件寄存器初始化
writel(0x5, pledcfg->piomux_mux_regaddr); // 引脚复用为GPIO功能
writel(0x10B0, pledcfg->piomux_pad_regaddr); // 设置引脚电气属性
// 配置GPIO1_IO03为输出模式
writel(readl(pledcfg->pgpio_dir_regaddr) | (0x1 << 3), pledcfg->pgpio_dir_regaddr);
// 初始状态:输出高电平,LED熄灭
writel(readl(pledcfg->pgpio_dat_regaddr) | (0x1 << 3), pledcfg->pgpio_dat_regaddr);
pledcfg->curledstat = LED_OFF;
// 9. 创建设备类,在/sys/class下生成myled目录
pledcfg->pclass = class_create(THIS_MODULE, "myled");
if (NULL == pledcfg->pclass) {
pr_info("class_create failed\n");
ret = -EINVAL;
goto err_iounmap;
}
// 10. 创建设备节点,自动在/dev下生成myled0设备文件
pledcfg->pdev = device_create(pledcfg->pclass, NULL, pledcfg->devno, NULL, "myled%d", 0);
if (NULL == pledcfg->pdev) {
pr_info("device_create failed\n");
ret = -EINVAL;
goto err_class_destroy;
}
// 11. 创建sysfs属性节点
ret = device_create_file(pledcfg->pdev, &led_attr);
if (ret != 0) {
pr_info("device_create_file failed\n");
goto err_device_destroy;
}
// 打印初始化成功信息,输出主次设备号
pr_info("major:%d, minor:%d\n", MAJOR(pledcfg->devno), MINOR(pledcfg->devno));
pr_info("led_drv_init success!\n");
return 0;
// 错误处理:逆向释放已申请的资源,避免内存泄漏
err_device_destroy:
device_destroy(pledcfg->pclass, pledcfg->devno);
err_class_destroy:
class_destroy(pledcfg->pclass);
err_iounmap:
iounmap(pledcfg->piomux_mux_regaddr);
iounmap(pledcfg->piomux_pad_regaddr);
iounmap(pledcfg->pgpio_dat_regaddr);
iounmap(pledcfg->pgpio_dir_regaddr);
mutex_destroy(&pledcfg->lock);
cdev_del(pledcfg->pcdev);
err_unregister_chrdev:
unregister_chrdev_region(pledcfg->devno, 1);
err_cdev_alloc:
kfree(pledcfg->pcdev);
err_kmalloc:
kfree(pledcfg);
return ret;
}
代码细节分析
__init修饰符:内核专用修饰符,标识该函数仅在模块初始化时执行,执行完成后会释放该函数占用的内存,提升内核内存利用率。- 内存申请
kmalloc:GFP_KERNEL是内核常规内存分配标志,允许内核在内存不足时睡眠等待可用内存,适用于非中断上下文的内存申请;申请后必须检查返回值是否为 NULL,避免空指针访问导致内核崩溃。 - goto 错误处理机制 :这是 Linux 内核编程的经典规范。驱动初始化过程中,任何一步失败,都通过 goto 跳转到对应的错误处理标签,逆向释放已申请的资源,确保半初始化状态下不会出现资源泄漏。
- 设备号管理 :使用
alloc_chrdev_region动态分配设备号,内核会自动分配一个未被使用的主设备号,彻底避免了静态注册主设备号冲突的问题,推荐这样做。MAJOR/MINOR宏用于从dev_t中提取主次设备号。 - 寄存器映射
ioremap:Linux 内核运行在虚拟地址空间,无法直接访问物理寄存器地址,必须通过ioremap将 4 字节的物理寄存器地址映射为内核虚拟地址,之后才能通过readl/writel操作。映射长度与寄存器位宽一致,避免地址越界。 - 硬件初始化顺序:先配置引脚复用和电气属性,再设置 GPIO 方向,最后设置默认输出电平,完全符合 i.MX6ULL GPIO 的配置流程,避免引脚电平出现不确定的毛刺。
- 设备节点自动创建 :
class_create+device_create组合,让 udev/mdev 自动生成/dev/myled0设备文件,无需用户手动执行mknod命令。
5. 驱动出口函数led_drv_exit
驱动出口函数负责模块卸载时的资源释放,要与入口函数的操作逆序,确保所有申请的资源都被正确释放,无内存泄漏。
c
运行
static void __exit led_drv_exit(void)
{
// 与入口函数逆序释放资源
device_remove_file(pledcfg->pdev, &led_attr);
device_destroy(pledcfg->pclass, pledcfg->devno);
class_destroy(pledcfg->pclass);
// 解除寄存器地址映射
iounmap(pledcfg->piomux_mux_regaddr);
iounmap(pledcfg->piomux_pad_regaddr);
iounmap(pledcfg->pgpio_dat_regaddr);
iounmap(pledcfg->pgpio_dir_regaddr);
mutex_destroy(&pledcfg->lock);
cdev_del(pledcfg->pcdev);
unregister_chrdev_region(pledcfg->devno, 1);
kfree(pledcfg);
pr_info("led_drv_exit success!\n");
return;
}
代码细节分析
__exit修饰符:内核专用修饰符,标识该函数仅在模块卸载时执行,若驱动被编译进内核(非模块形式),该函数会被自动丢弃,不占用内核内存。- 资源释放顺序 :严格遵循先申请的后释放,后申请的先释放原则。例如 sysfs 节点是最后创建的,因此最先释放;私有数据内存是最先申请的,因此最后释放,避免释放正在使用的资源导致内核崩溃。
- 必须释放的资源 :地址映射必须通过
iounmap解除,设备号必须释放,cdev 必须删除,内核内存必须通过kfree释放,否则会导致内核地址空间和内存泄漏,长期运行会造成系统资源耗尽。
6. 模块声明与许可证
c
运行
module_init(led_drv_init); // 指定驱动入口函数
module_exit(led_drv_exit); // 指定驱动出口函数
MODULE_LICENSE("GPL"); // 声明模块许可证,必须为GPL协议
MODULE_AUTHOR("pute"); // 声明模块作者
关键说明
MODULE_LICENSE("GPL")是必须的,Linux 内核中大量核心函数仅对 GPL 协议的模块开放导出。如果不声明 GPL 协议,模块不仅会被内核标记为 "污染",还无法使用mutex、class_create等大量内核接口,导致编译失败或运行异常。
四、应用层测试程序led_app.c
驱动最终要被用户空间的应用程序调用,测试程序通过标准文件操作接口,实现了 LED 的循环亮灭控制,同时读取并打印当前 LED 状态。
c
运行
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
// 与驱动层保持一致的状态宏
#define LED_ON 1
#define LED_OFF 0
int main()
{
int stat; // 要写入的LED控制状态
int readstat; // 从驱动读取的LED当前状态
// 打开LED设备文件,读写模式
int fd = open("/dev/myled0", O_RDWR);
if (fd < 0) {
perror("open failed");
return -1;
}
// 死循环:每秒切换一次LED亮灭
while(1)
{
// 点亮LED
stat = LED_ON;
write(fd, &stat, sizeof(LED_ON));
read(fd, &readstat, sizeof(stat));
printf("CURRENT LED STATE = %s\n", readstat == LED_ON ? "LED ON" : "LED OFF");
sleep(1);
// 熄灭LED
stat = LED_OFF;
write(fd, &stat, sizeof(LED_OFF));
read(fd, &readstat, sizeof(stat));
printf("CURRENT LED STATE = %s\n", readstat == LED_ON ? "LED ON" : "LED OFF");
sleep(1);
}
// 关闭设备文件(循环中不会执行,仅做规范保留)
close(fd);
return 0;
}
代码细节分析
- 设备文件打开 :
open("/dev/myled0", O_RDWR)打开驱动创建的字符设备文件,返回文件描述符fd,后续所有读写操作都基于该 fd。perror会打印系统调用失败的具体原因(如设备文件不存在、权限不足),是 Linux 应用开发调试的关键手段。 - write 操作 :向驱动写入
int类型的状态值,sizeof(LED_ON)确保写入长度为 4 字节,与驱动中copy_from_user的拷贝长度完全匹配,避免数据截断。 - read 操作 :从驱动中读取当前 LED 状态,存入
readstat变量,通过三目运算符判断状态并打印,直观展示 LED 当前工作状态。 - sleep 函数 :秒级休眠,让 LED 保持亮 / 灭状态 1 秒,实现周期性闪烁效果。注意应用层的
sleep是秒级,内核态的延时函数为msleep/udelay,二者不能混用。
五、驱动编译与板级测试流程
1. 驱动模块 Makefile 编写
makefile
# 内核源码路径,需替换为自己的i.MX6ULL内核源码目录
KERNELDIR := /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
# 交叉编译工具链,与课程保持一致
CROSS_COMPILE := arm-linux-gnueabihf-
# 目标模块名
obj-m := led_drv.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean
执行make命令,即可编译生成led_drv.ko内核模块文件。
2. 应用程序交叉编译
执行以下命令,生成 ARM 平台可执行文件:
bash
运行
arm-linux-gnueabihf-gcc led_app.c -o led_app
3. 开发板测试步骤
- 将
led_drv.ko和led_app通过 nfs 共享目录或 tftp 下载到 i.MX6ULL 开发板中; - 加载驱动模块:
insmod led_drv.ko; - 查看内核日志,确认驱动加载成功:
dmesg | grep "led_drv_init",可看到分配的主次设备号; - 确认设备节点生成:
ls /dev/myled0,若文件存在则设备创建成功; - 方式一:应用程序测试 :执行
./led_app,可看到开发板 LED 每秒亮灭切换,串口同步打印状态信息; - 方式二:sysfs 节点调试 :
- 点亮 LED:
echo LED_ON > /sys/class/myled/myled0/attr - 熄灭 LED:
echo LED_OFF > /sys/class/myled/myled0/attr - 查看状态:
cat /sys/class/myled/myled0/attr,内核日志会打印当前状态;
- 点亮 LED:
- 卸载驱动模块:
rmmod led_drv,查看内核日志确认卸载成功,/dev/myled0节点会自动删除。
六、开发中常见问题与排查方案
在实际开发中,即使代码逻辑正确,也会遇到各种环境和硬件相关的问题,这里总结了最常见的问题与务实的排查方案。
-
应用层 open 失败,提示 "No such file or directory"
- 排查点:驱动是否加载成功?
insmod是否有报错?ls /dev/myled0是否存在? - 解决方案:检查驱动中
device_create是否执行成功,查看内核日志是否有报错;确认开发板的 mdev/udev 服务正常运行;临时测试可手动创建设备节点:mknod /dev/myled0 c 主设备号 0。
- 排查点:驱动是否加载成功?
-
write/read 执行后 LED 无反应,内核无打印
- 排查点:
copy_from_user/copy_to_user是否拷贝成功?应用层与驱动层的数据长度是否匹配?函数返回值是否正确? - 解决方案:将
read/write函数的返回值修改为实际读写的字节数;确认应用层写入的是int类型 4 字节数据,与驱动拷贝长度一致;在内核函数中增加日志打印,确认函数是否被调用。
- 排查点:
-
LED 完全不亮或亮灭逻辑相反
- 排查点:硬件原理图中 LED 的点亮电平是高还是低?寄存器地址和引脚号是否与硬件匹配?
- 解决方案:正点原子 i.MX6ULL 开发板 LED 为低电平点亮,确认
LED_ON时是清 0 操作,LED_OFF时是置 1 操作;用万用表测量 GPIO 引脚电平,确认寄存器操作是否生效;检查引脚复用配置是否正确,是否被其他外设占用。
-
多进程同时访问驱动时,LED 状态错乱
- 排查点:竞态问题,多个进程同时修改寄存器和状态变量,导致数据不一致。
- 解决方案:确认互斥锁的保护范围覆盖了整个寄存器操作和状态修改;可在
open函数中增加原子变量,限制设备同时只能被一个进程打开,避免多进程并发访问。
-
驱动卸载时内核崩溃(Oops)
- 排查点:资源释放顺序错误,或释放了未申请的资源,导致空指针访问。
- 解决方案:严格按照入口函数的逆序释放资源;确保每一步资源申请都有对应的错误处理,半初始化状态下不会释放未申请的资源。
七、总结
本文拆解的 LED 驱动,虽然功能简单,却完整覆盖了 Linux 字符设备驱动开发的大部分知识点:从设备号管理、cdev 字符设备注册,到用户 / 内核空间数据交互、硬件寄存器映射、并发竞态保护、sysfs 调试节点、设备节点自动创建,遵循 Linux 内核驱动的开发规范和字符设备驱动框架。
依据这个 LED 驱动的开发思路,后续无论是按键、串口、传感器、显示屏等字符设备驱动,都可以基于这个框架进行扩展 ------ 只需修改硬件相关的寄存器操作,保留字符设备驱动的核心框架,即可快速完成新设备的驱动开发。