imx6ull-驱动开发篇2——字符设备驱动开发步骤

目录

前言

开发步骤

驱动模块的加载/卸载

模块的加/卸载

模块加载命令

模块卸载命令

操作示例

字符设备注册与注销

注册函数

注销函数

操作示例

实现设备的具体操作函数

[添加 LICENSE 和作者信息](#添加 LICENSE 和作者信息)

[Linux 设备号](#Linux 设备号)

设备号的组成

设备号的分配

静态分配设备号

动态分配设备号


前言

在上一讲内容里,字符设备驱动简介,我们介绍了linux驱动开发的3种类型、Linux 应用程序对驱动程序的调用流程、用户空间和内核空间、file_operations 的结构体(Linux 内核驱动操作函数集合)。

本讲实验,就是学习字符设备驱动的开发步骤,包括模块加载/卸载机制、设备号管理、操作函数实现等

开发步骤

驱动模块的加载/卸载

Linux 驱动有两种运行方式:

  1. 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
  2. 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用"insmod"命令加载驱动模块。

在调试驱动的时候一般都选择将其编译为模块,好处有两点:

  • 编译:只编译修改好的驱动代码,不需要编译整个 Linux 代码。
  • 调试:只需要加载或者卸载驱动模块,不需要重启整个系统。

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数。

模块的加/卸载

模块的加载和卸载注册函数如下:

cpp 复制代码
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

**module_init 函数:**用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用"insmod"命令加载驱动的时候, xxx_init 这个函数就会被调用。

**module_exit()函数:**用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

字符设备驱动模块加载和卸载模板如下所示:

cpp 复制代码
/**
 * 驱动初始化函数 - 内核模块加载时自动执行
 * 返回值:0 表示成功,负数表示失败
 */
static int __init xxx_init(void)
{
    /* 初始化操作通常包括:
     * 1. 注册字符设备/块设备/平台设备
     * 2. 申请硬件资源(IO端口、内存、中断等)
     * 3. 初始化设备默认状态
     * 4. 创建sysfs/debugfs调试接口
     */
    printk(KERN_INFO "Driver initialized\n");
    return 0;  // 返回0表示初始化成功
}

/**
 * 驱动退出函数 - 内核模块卸载时自动执行
 */
static void __exit xxx_exit(void)
{
    /* 清理操作通常包括:
     * 1. 释放设备号与注销设备
     * 2. 释放硬件资源
     * 3. 删除调试接口
     * 4. 重置硬件状态
     */
    printk(KERN_INFO "Driver removed\n");
}

/* 宏声明指定入口/出口函数 */
module_init(xxx_init);  // 将xxx_init注册为模块加载函数
module_exit(xxx_exit);  // 将xxx_exit注册为模块卸载函数

模块加载命令

​命令​ ​功能​ ​使用示例​ ​特点​
insmod 直接加载指定 .ko文件 insmod drv.ko - 需手动处理依赖 - 必须指定完整路径
modprobe 自动加载模块及其依赖项 modprobe drv - 自动解析依赖关系 - 默认搜索 /lib/modules/<kernel-version>目录

关键区别​​:

  • insmod是基础命令,不处理依赖;modprobe是智能工具,需依赖 depmod生成的模块依赖列表。
  • 使用 modprobe前需确保模块已复制到标准路径(如 /lib/modules/4.1.15/)并运行 depmod -a生成依赖关系。

模块卸载命令

​命令​ ​功能​ ​使用示例​ ​特点​
rmmod 卸载指定模块 rmmod drv - 直接卸载,不检查依赖
modprobe -r 卸载模块及其未使用的依赖 modprobe -r drv - 自动卸载孤立依赖项 - 若依赖被其他模块占用则失败

推荐场景​​:

  • 卸载单个模块:优先用 rmmod(简单直接)
  • 彻底清理依赖:用 modprobe -r(需确保无冲突)

操作示例

安装模块到标准路径​

cpp 复制代码
sudo cp drv.ko /lib/modules/$(uname -r)/kernel/drivers/
sudo depmod -a  # 生成模块依赖关系

加载模块(推荐modprobe)​

cpp 复制代码
sudo modprobe drv  # 自动加载依赖

卸载模块​

cpp 复制代码
sudo rmmod drv     # 快速卸载
# 或
sudo modprobe -r drv  # 尝试卸载依赖(谨慎使用)

modprobe 命令相比 insmod 要智能一些,提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。

使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。

所以对于模块的卸载,还是推荐使用 rmmod 命令。

字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。

注册函数

字符设备的注册函数原型如下所示:

cpp 复制代码
/**
 * register_chrdev - 注册字符设备驱动(传统方式)
 * @major: 主设备号(传入0表示自动分配)
 * @name: 设备名称(显示在/proc/devices)
 * @fops: 文件操作结构体指针
 *
 * 此函数会注册一个主设备号及对应的256个次设备号范围。
 * 新驱动建议使用cdev接口代替。
 *
 * 返回值:
 *  成功 - 返回分配的主设备号(≥0)
 *  失败 - 返回负的错误码(如-ENOMEM)
 */
static inline int register_chrdev(unsigned int major, const char *name,
                                const struct file_operations *fops);
  • major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。
  • name:设备名字,指向一串字符串。
  • fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。

举例说明:

cpp 复制代码
#include <linux/fs.h>

static int major; // 保存分配的主设备号
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
};

static int __init my_init(void)
{
    major = register_chrdev(0, "mydev", &fops); // 自动分配主设备号
    if (major < 0) {
        printk(KERN_ERR "Failed to register: %d\n", major);
        return major;
    }
    printk(KERN_INFO "Registered with major=%d\n", major);
    return 0;
}

注销函数

字符设备的注销函数原型如下所示:

cpp 复制代码
/**
 * unregister_chrdev - 注销字符设备驱动
 * @major: 要释放的主设备号(必须与注册时一致)
 * @name: 设备名称(需与注册时完全匹配)
 *
 * 注意事项:
 * 1. 必须在模块退出函数中调用
 * 2. 调用前需确保所有设备节点已关闭
 * 3. 不会自动释放次设备号资源
 */
static inline void unregister_chrdev(unsigned int major, const char *name);
  • major: 要注销的设备对应的主设备号。
  • name: 要注销的设备对应的设备名。

举例说明:

cpp 复制代码
static void __exit my_exit(void)
{
    unregister_chrdev(major, "mydev");
    printk(KERN_INFO "Unregistered\n");
}

module_init(my_init);
module_exit(my_exit);

操作示例

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,

字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。

cpp 复制代码
#include <linux/fs.h>

/* 文件操作结构体实现 */
static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = mydev_open,
    .release = mydev_release,
    .read = mydev_read,
    .write = mydev_write,
};

static int __init my_init(void)
{
    int ret;
    
    /* 注册设备(自动分配主设备号) */
    ret = register_chrdev(0, "mydev", &my_fops);
    if (ret < 0) {
        pr_err("Registration failed: %d\n", ret);
        return ret;
    }
    pr_info("Registered with major=%d\n", ret);
    return 0;
}

static void __exit my_exit(void)
{
    /* 注销设备 */
    unregister_chrdev(major_num, "mydev");
    pr_info("Driver unregistered\n");
}

module_init(my_init);
module_exit(my_exit);

其中需要注意的一点就是,注册设备时要选择没有被使用的主设备号。

输入命令"cat /proc/devices"可以查看当前已经被使用掉的设备号,如图

可以列出当前系统中所有的字符设备和块设备,其中第 1 列就是设备对应的主设备号。

实现设备的具体操作函数

file_operations 结构体是设备的具体操作函数。

现在想注册一个字符设备,主设备号为 200,设备名字为"chrtest",那么我们需要初始化哪些函数?

1、能够对 chrtest 进行打开和关闭操作

  • 设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。

2、对 chrtest 进行读写操作

  • 假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。

示例代码如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define CHRDEV_NAME   "chrtest"  // 设备名称
#define CHRDEV_MAJOR  200        // 主设备号(需确保未被占用)

/* 设备打开函数 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 设备读取函数 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
                          size_t count, loff_t *f_pos)
{
     /* 用户实现具体功能 */
    return 0;  
}

/* 设备写入函数 */
static ssize_t chrtest_write(struct file *filp,
                           const char __user *buf,
                           size_t count, loff_t *f_pos)
{
    /* 用户实现具体功能 */
    return 0;  0
}

/* 设备关闭/释放函数 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 文件操作结构体 */
static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* 驱动入口函数 */
static int __init chrdev_init(void)
{
    int ret;
    
    // 注册字符设备(固定主设备号200)
    ret = register_chrdev(CHRDEV_MAJOR, CHRDEV_NAME, &test_fops);
    if (ret < 0) {
        printk(KERN_ERR "Failed to register chrdev: %d\n", ret);
        return ret;
    }
    
    printk(KERN_INFO "Registered chrdev: major=%d, name=%s\n", 
           CHRDEV_MAJOR, CHRDEV_NAME);
    return 0;
}

/* 驱动出口函数 */
static void __exit chrdev_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(CHRDEV_MAJOR, CHRDEV_NAME);
    printk(KERN_INFO "Unregistered chrdev\n");
}

/* 模块声明 */
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HUAX");

我们编写了四个函数:

  • chrtest_open
  • chrtest_read
  • chrtest_write
  • chrtest_release

这四个函数就是 chrtest 设备的 open、 read、 write 和 release 操作函数。

然后初始化 test_fops 的 open、 read、 write 和 release 这四个成员变量。

添加 LICENSE 和作者信息

我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。

LICENSE 和作者信息的添加使用如下两个函数:

cpp 复制代码
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

所以上面的示例代码最后有这一段:

cpp 复制代码
/* 模块声明 */
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HUAX");

LICENSE :采用 GPL 协议。

字符设备驱动开发的完整步骤就是上面所说的4步,我们也编写好了一个完整的字符设备驱动模板,以后字符设备驱动开发都可以在此模板上进行。

Linux 设备号

设备号的组成

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成:

  • 主设备号表示某一个具体的驱动,
  • 次设备号表示使用这个驱动的各个设备。

Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,定义如下:

cpp 复制代码
typedef __u32 __kernel_dev_t;

typedef __kernel_dev_t dev_t;

typedef unsigned int __u32;

可以看出,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。其中高 12 位为主设备号, 低 20 位为次设备号。

因此 Linux系统中主设备号范围为 0~4095

在文件 include/linux/kdev_t.h 中有关于设备号的宏,如下所示:

cpp 复制代码
#define MINORBITS   20  // 次设备号占用的位数(20位)
#define MINORMASK   ((1U << MINORBITS) - 1)  // 次设备号掩码(低20位为1)

#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))  // 从dev_t提取主设备号
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))  // 从dev_t提取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))         // 组合主次设备号为dev_t
​宏​ ​输入​ ​输出​ ​典型用途​
MAJOR() dev_t设备号 主设备号(12位) 识别设备类型
MINOR() dev_t设备号 次设备号(20位) 识别设备实例
MKDEV() 主设备号+次设备号 完整 dev_t 设备号注册前的组合

设备号的分配

设备号分配主要是主设备号的分配。

静态分配设备号

使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号。

驱动开发者可以静态地指定一个设备号,比如选择 200 这个主设备号。前提是这个设备号,硬件平台运行过程中没有使用。

有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。

动态分配设备号

Linux 社区推荐使用动态分配设备号:在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。

设备号的申请函数如下:

cpp 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
参数​ ​类型​ ​作用​
dev dev_t * 输出参数,保存分配到的​​起始设备号​​(主设备号 + 起始次设备号)
baseminor unsigned ​起始次设备号​​(通常设为0)
count unsigned 需要分配的​​连续设备号数量​ ​(主设备号相同,次设备号从baseminor递增)
name const char * 设备名称(出现在/proc/devices和内核日志中)

返回值​

  • ​成功​ ​:返回 0,并通过 dev参数返回分配的设备号

  • ​失败​ ​:返回负的错误码(如 -EBUSY-ENOMEM

举例:单个设备注册

cpp 复制代码
dev_t dev;
int ret;

ret = alloc_chrdev_region(&dev, 0, 1, "mydev");
if (ret < 0) {
    printk(KERN_ERR "Failed to allocate device number\n");
    return ret;
}

printk(KERN_INFO "Allocated major=%u, minor=%u\n", MAJOR(dev), MINOR(dev));

注销字符设备之后要释放掉设备号,设备号释放函数如下:

cpp 复制代码
void unregister_chrdev_region(dev_t from, unsigned count);
​参数​ ​类型​ ​作用​
from dev_t ​要释放的起始设备号​​(包含主设备号和次设备号)
count unsigned ​连续释放的设备号数量​ ​(从 from开始递增次设备号)

举例:释放单个设备号

cpp 复制代码
dev_t dev;

// 分配设备号(动态主设备号,次设备号0)
alloc_chrdev_region(&dev, 0, 1, "mydev");

// 使用设备号...

// 释放设备号(需与分配时的参数对应)
unregister_chrdev_region(dev, 1);
相关推荐
UU_Yang16 分钟前
Linux跑后台服务
linux·运维·服务器
kfepiza1 小时前
vim的`:q!` 与 `ZQ` 笔记250729
linux·笔记·编辑器·vim
jack-hui61 小时前
docker配置gpu运行环境:linux离线安装nvidia-container,避免网络问题
linux·docker·容器
渡我白衣1 小时前
Linux网络编程:UDP 的DictServer
linux·网络·网络协议·udp
小立爱学习2 小时前
Linux 内存管理之 Rmap 反向映射
linux·c语言
Young_Zn_Cu2 小时前
Windows安装虚拟机遇到内容解码失败
linux·windows·ubuntu
monkey_lqd2 小时前
arm ramdump调试
linux
ihui数学建模2 小时前
【Mac版】Linux 入门命令行快捷键+联想记忆
linux·运维·macos
*wj2 小时前
【linux驱动开发】编译linux驱动程序报错:ERROR: Kernel configuration is invalid.
linux·运维·驱动开发