当前位置: 首页 > news >正文

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

目录

前言

开发步骤

驱动模块的加载/卸载

模块的加/卸载

模块加载命令

模块卸载命令

操作示例

字符设备注册与注销

注册函数

注销函数

操作示例

实现设备的具体操作函数

添加 LICENSE 和作者信息

Linux 设备号

设备号的组成

设备号的分配

静态分配设备号

动态分配设备号


前言

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

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

开发步骤

驱动模块的加载/卸载

Linux 驱动有两种运行方式:

  1. 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
  2. 将驱动编译成模块(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

直接加载指定 .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(需确保无冲突)

操作示例

安装模块到标准路径​

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

​宏​

​输入​

​输出​

​典型用途​

MAJOR()

dev_t设备号

主设备号(12位)

识别设备类型

MINOR()

dev_t设备号

次设备号(20位)

识别设备实例

MKDEV()

主设备号+次设备号

完整 dev_t

设备号注册前的组合

设备号的分配

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

静态分配设备号

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

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

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

动态分配设备号

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

设备号的申请函数如下:

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

举例:单个设备注册

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);

​参数​

​类型​

​作用​

from

dev_t

​要释放的起始设备号​​(包含主设备号和次设备号)

count

unsigned

​连续释放的设备号数量​​(从 from开始递增次设备号)

举例:释放单个设备号

dev_t dev;// 分配设备号(动态主设备号,次设备号0)
alloc_chrdev_region(&dev, 0, 1, "mydev");// 使用设备号...// 释放设备号(需与分配时的参数对应)
unregister_chrdev_region(dev, 1);

http://www.lryc.cn/news/603855.html

相关文章:

  • 网络通信基础(一)
  • Redis 跨主机连接超时分析:从网络波动到架构优化
  • 使用鼠标在Canvas上绘制矩形
  • 【C++算法】80.BFS解决FloodFill算法_岛屿数量
  • 《Java 程序设计》第 9 章 - 内部类、枚举和注解
  • 实在智能Agent智能体荣登全球“Go_Global_AI_100”百强榜,中国AI走向世界!
  • STM32——HAL库
  • 什么是EasyVR shield 3?如何设置EasyVR shield 3
  • 大模型应用开发模拟面试
  • 用动态的观点看加锁
  • TCMalloc 内存分配原理简析
  • 2-verilog-基础语法
  • Coze Studio概览(三)--智能体管理
  • sqli-labs通关笔记-第24关 SQL二次注入(单引号闭合)
  • 硬件学习笔记--73 电能表新旧精度等级对应关系
  • debug redis里面的lua脚本
  • Spring Boot 防重放攻击全面指南:原理、方案与最佳实践
  • ElasticSearch 的3种数据迁移方案
  • 在Word和WPS文字中把全角数字全部改为半角
  • Vue2学习-MVVM模型
  • Spring Boot 简单接口角色授权检查实现
  • C++入门知识学习(上)
  • 嵌入式学习日志(十一)
  • css3之三维变换详说
  • SQL Server中的分页查询
  • leetcode热题——螺旋矩阵
  • 第十一天:不定方程求解
  • 镁金属接骨螺钉注册检测:骨科植入安全的科学基石
  • Rust基础-part8-模式匹配、常见集合
  • 亚马逊 Vine 计划:评论生态重构与合规运营策略