imx6ull-驱动开发篇2——字符设备驱动开发步骤
目录
前言
开发步骤
驱动模块的加载/卸载
模块的加/卸载
模块加载命令
模块卸载命令
操作示例
字符设备注册与注销
注册函数
注销函数
操作示例
实现设备的具体操作函数
添加 LICENSE 和作者信息
Linux 设备号
设备号的组成
设备号的分配
静态分配设备号
动态分配设备号
前言
在上一讲内容里,字符设备驱动简介,我们介绍了linux驱动开发的3种类型、Linux 应用程序对驱动程序的调用流程、用户空间和内核空间、file_operations 的结构体(Linux 内核驱动操作函数集合)。
本讲实验,就是学习字符设备驱动的开发步骤,包括模块加载/卸载机制、设备号管理、操作函数实现等。
开发步骤
驱动模块的加载/卸载
Linux 驱动有两种运行方式:
- 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
- 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,好处有两点:
- 编译:只编译修改好的驱动代码,不需要编译整个 Linux 代码。
- 调试:只需要加载或者卸载驱动模块,不需要重启整个系统。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数。
模块的加/卸载
模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init 函数:用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。
module_exit()函数:用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
字符设备驱动模块加载和卸载模板如下所示:
/*** 驱动初始化函数 - 内核模块加载时自动执行* 返回值: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是基础命令,不处理依赖;modprobe是智能工具,需依赖 depmod生成的模块依赖列表。
- 使用 modprobe前需确保模块已复制到标准路径(如 /lib/modules/4.1.15/)并运行 depmod -a生成依赖关系。
模块卸载命令
命令 | 功能 | 使用示例 | 特点 |
---|---|---|---|
| 卸载指定模块 |
| - 直接卸载,不检查依赖 |
| 卸载模块及其未使用的依赖 |
| - 自动卸载孤立依赖项 |
推荐场景:
- 卸载单个模块:优先用 rmmod(简单直接)
- 彻底清理依赖:用 modprobe -r(需确保无冲突)
操作示例
安装模块到标准路径
sudo cp drv.ko /lib/modules/$(uname -r)/kernel/drivers/
sudo depmod -a # 生成模块依赖关系
加载模块(推荐modprobe)
sudo modprobe drv # 自动加载依赖
卸载模块
sudo rmmod drv # 快速卸载
# 或
sudo modprobe -r drv # 尝试卸载依赖(谨慎使用)
modprobe 命令相比 insmod 要智能一些,提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。
使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。
所以对于模块的卸载,还是推荐使用 rmmod 命令。
字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。
注册函数
字符设备的注册函数原型如下所示:
/*** 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 类型指针,指向设备的操作函数集合变量。
举例说明:
#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;
}
注销函数
字符设备的注销函数原型如下所示:
/*** unregister_chrdev - 注销字符设备驱动* @major: 要释放的主设备号(必须与注册时一致)* @name: 设备名称(需与注册时完全匹配)** 注意事项:* 1. 必须在模块退出函数中调用* 2. 调用前需确保所有设备节点已关闭* 3. 不会自动释放次设备号资源*/
static inline void unregister_chrdev(unsigned int major, const char *name);
- major: 要注销的设备对应的主设备号。
- name: 要注销的设备对应的设备名。
举例说明:
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 中进行。
#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 这两个函数。
示例代码如下:
#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 和作者信息的添加使用如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
所以上面的示例代码最后有这一段:
/* 模块声明 */
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 里面,定义如下:
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 中有关于设备号的宏,如下所示:
#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
宏 | 输入 | 输出 | 典型用途 |
---|---|---|---|
|
| 主设备号(12位) | 识别设备类型 |
|
| 次设备号(20位) | 识别设备实例 |
| 主设备号+次设备号 | 完整 | 设备号注册前的组合 |
设备号的分配
设备号分配主要是主设备号的分配。
静态分配设备号
使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。
驱动开发者可以静态地指定一个设备号,比如选择 200 这个主设备号。前提是这个设备号,硬件平台运行过程中没有使用。
有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。
动态分配设备号
Linux 社区推荐使用动态分配设备号:在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。
设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
参数 | 类型 | 作用 |
---|---|---|
|
| 输出参数,保存分配到的起始设备号(主设备号 + 起始次设备号) |
|
| 起始次设备号(通常设为0) |
|
| 需要分配的连续设备号数量(主设备号相同,次设备号从 |
|
| 设备名称(出现在 |
返回值
-
成功:返回
0
,并通过dev
参数返回分配的设备号 -
失败:返回负的错误码(如
-EBUSY
、-ENOMEM
)
举例:单个设备注册
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));
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count);
参数 | 类型 | 作用 |
---|---|---|
|
| 要释放的起始设备号(包含主设备号和次设备号) |
|
| 连续释放的设备号数量(从 |
举例:释放单个设备号
dev_t dev;// 分配设备号(动态主设备号,次设备号0)
alloc_chrdev_region(&dev, 0, 1, "mydev");// 使用设备号...// 释放设备号(需与分配时的参数对应)
unregister_chrdev_region(dev, 1);