1.第一个Linux驱动
-
字符设备:只能一个字节一个字节的读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序进行。字符设备是面向流的设备,常见的字符设备如鼠标、键盘、串口、控制台、LED等。
-
块设备:是指可以从设备的任意位置读取一定长度的数据设备。块设备如硬盘、磁盘、U盘和SD卡等存储设备。
-
网络设备:网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。
-
对于字符设备和块设备来说,在/dev目录下都有对应的设备文件。linux用户程序通过设备文件或叫做设备节点来使用驱动程序操作字符设备和块设备。
1.1字符设备驱动框架
/* 引入Linux内核模块开发必需的头文件 */
#include <linux/module.h> // 包含内核模块相关函数和宏(如module_init/module_exit)
/*
* 模块初始化函数 - 在模块加载时执行
* __init宏表示该函数会被放在.init.text内存段,初始化完成后可释放这段内存
* 返回值:成功返回0,失败返回错误码
*/
static int __init chrdevbase_init(void)
{
// 此处一般包含:
// 1. 设备号申请(register_chrdev_region/alloccdev_register)
// 2. 字符设备注册(cdev_init/cdev_add)
// 3. 设备节点创建(class_create/device_create)
return 0; // 初始化成功返回0
}
/*
* 模块退出函数 - 在模块卸载时执行
* __exit宏表示该函数会被放在.exit.text内存段
*/
static void __exit chrdevbase_exit(void)
{
// 此处一般包含:
// 1. 设备号释放(unregister_chrdev_region)
// 2. 字符设备注销(cdev_del)
// 3. 设备节点删除(device_destroy/class_destroy)
}
/* 指定模块的初始化和清理函数 */
module_init(chrdevbase_init); // 注册模块加载时调用的初始化函数
module_exit(chrdevbase_exit); // 注册模块卸载时调用的退出函数
/* 模块元信息(可选但建议添加) */
MODULE_LICENSE("GPL"); // 必须声明模块许可证(避免内核污染警告)
MODULE_AUTHOR("YourName"); // 模块作者信息
MODULE_DESCRIPTION("Simple Character Device Driver Template");// 模块功能描述
MODULE_VERSION("V1.0"); // 模块版本信息
在字符型设备中可以加入printk函数进行
日志输出用于调试
1.2字符设备驱动加载与卸载
在开发板中使用命令完成驱动的加载与卸载
1.2.1在上述代码中使用printk打印日志信息
static int __init chrdevbase_init(void){
printk("chrdevbase_init!!!\r\n");//打印注册成功时的提示信息
return 0;
}
static void __exit chrdevbase_exit(void){
printk("chrdevbase_exit!!!\r\n");//打印卸载成功时的提示信息
}
1.2.2顶层Makefile编写
# 内核源码绝对路径(需替换为实际路径)
KERNELDIR := /home/<用户名>/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_ga
# 获取当前模块代码路径(自动获取无需修改)
CURRENT_PATH := $(shell pwd)
# 指定要编译的内核模块对象
# obj-m 表示编译为可加载模块,obj-y 表示编译进内核
obj-m := chrdevbase.o
# 默认构建目标(依赖内核模块编译)
build: kernel_modules
# 内核模块编译目标
kernel_modules:
# 调用内核构建系统:
# -C 指定内核源码目录
# M= 指定模块源码目录
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
# 清理编译产物
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
1.2.3驱动源码编译
进入Makefile所在路径下执行make命令即可
注:如果make命令报错请确认在自己的linux内核源码中是否指定了交叉编译器,如果没有请手动指定自己的交叉编译器(请勿照抄)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
1.2.4驱动文件的挂载和卸载
注:首先应该确保自己的开发板可以和Ubuntu虚拟机ping通,并且成功挂载nfs服务!
1.编译成功后应该可以看到如下文件
2.将chrdevbase.ko文件拷贝至如下目录(缺少部分个人手动创建)
sudo cp chrdevbase.ko /home/xxxx/linux/nfs/rootfs/lib/modules/4.1.15/ -f
3.将开发板上电并连接串口打印信息
此时已经可以看到我们导入的文件
4.驱动设备加载命令(modprobe/insmod)
此时执行加载驱动命令
modprobe chrdevbase.ko
若提示以下错误信息请执行命令depmod
此时再次执行加载驱动命令则不再报错,printk函数成功打印信息,使用lsmod命令可以查看当前存在的驱动,这里应可以看到我们编写的chrdevbase驱动
5.驱动设备加载命令(rmmod)
同加载驱动一样,执行卸载驱动命令rmmod <驱动名>,此时串口打印卸载驱动的信息,再次查看当前驱动时,chrdevbase已经成功卸载
1.3字符设备驱动代码编写
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下: major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下: major: 要注销的设备对应的主设备号。
name: 要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
1.3.1Linux设备号
为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 其实设备号就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据组成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。
这里我们在开发板下使用命令cat /proc/devices查看当前已经使用的设备号
可以发现200未被使用,所以此处我们选择200作为主设备号
1.3.2字符驱动代码编写
这里已经编写好了驱动代码并标有详细注释
#include <linux/module.h> // 内核模块基础头文件
#include <linux/kernel.h> // 内核打印函数printk()
#include <linux/init.h> // 模块初始化和退出宏
#include <linux/fs.h> // 文件系统相关操作结构体
/* 设备号定义 */
#define CHARDEVBASE_MAJOR 200 // 主设备号(确保未被占用)
#define CHARDEVBASE_NAME "chrdevbase"// 设备名称(出现在/proc/devices)
/**
* @brief 打开设备操作
* @param inode: 设备文件的inode结构指针
* @param filp: 文件结构指针
* @retval 0表示成功,负数表示失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp){
printk("chrdevbase open!\r\n");
return 0;
}
/**
* @brief 关闭设备操作
* @param inode: 设备文件的inode结构指针
* @param filp: 文件结构指针
* @retval 0表示成功,负数表示失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp){
printk("chrdevbase release!\r\n");
return 0;
}
/**
* @brief 设备读操作
* @param filp: 文件结构指针
* @param buf: 用户空间缓冲区指针(需要copy_to_user)
* @param count: 请求读取的字节数
* @param ppos: 文件偏移量指针
* @retval 成功返回读取字节数,失败返回负数
*/
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count,loff_t *ppos){
printk("chrdevbase read!\r\n");
return 0;
}
/**
* @brief 设备写操作
* @param filp: 文件结构指针
* @param buf: 用户空间缓冲区指针(需要copy_from_user)
* @param count: 请求写入的字节数
* @param ppos: 文件偏移量指针
* @retval 成功返回写入字节数,失败返回负数
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos){
printk("chrdevbase write!\r\n");
return 0;
}
/* 字符设备操作结构体定义 */
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE, // 防止模块被卸载时设备正在使用
.open = chrdevbase_open, // 绑定打开操作
.release = chrdevbase_release, // 绑定关闭操作
.read = chrdevbase_read, // 绑定读操作
.write = chrdevbase_write, // 绑定写操作
};
/**
* @brief 模块初始化函数
* @note 通过module_init宏注册为模块入口
* @retval 0表示成功,负数表示失败
*/
static int __init chrdevbase_init(void){
int major=0;
printk("chrdevbase_init!!!\r\n");
/* 注册字符设备驱动 */
major = register_chrdev(CHARDEVBASE_MAJOR, CHARDEVBASE_NAME, &chrdevbase_fops);
if(major < 0){
printk("chrdevbase register failed!!!\r\n");
return major; // 返回错误码
}
return 0;
}
/**
* @brief 模块退出函数
* @note 通过module_exit宏注册为模块退出处理
*/
static void __exit chrdevbase_exit(void){
printk("chrdevbase_exit!!!\r\n");
/* 注销字符设备驱动 */
unregister_chrdev(CHARDEVBASE_MAJOR, CHARDEVBASE_NAME);
}
/* 模块入口/出口注册 */
module_init(chrdevbase_init); // 指定模块加载时执行的函数
module_exit(chrdevbase_exit); // 指定模块卸载时执行的函数
/* 模块元信息 */
MODULE_LICENSE("GPL"); // 必须的GPL协议声明
MODULE_AUTHOR("wanghaipeng <2057566154@qq.com>"); // 开发者信息
MODULE_DESCRIPTION("Basic Character Device Driver Framework"); // 模块功能描述
编写好驱动代码以后,我们还需要编写一个应用程序来测试驱动中的open,close(release),read,write的相关功能是否正常
1.3.3测试文件编写
在目录下新建文件chrdevbaseAPP.c,并编写以下测试代码
/*--------------------------------头文件--------------------------------*/
#include <sys/types.h> // 定义文件类型和属性(如dev_t)
#include <sys/stat.h> // 文件状态操作(如文件模式定义)
#include <fcntl.h> // 文件控制选项(如O_RDWR)
#include <unistd.h> // POSIX系统调用(read/write/close)
#include <stdio.h> // 标准输入输出函数(printf)
/*--------------------------------主函数--------------------------------*/
/**
* @brief 文件读写测试程序
* @note 执行流程:打开文件 → 读取前50字节 → 写入相同内容 → 关闭文件
*/
int main(int argc, char *argv[])
{
int ret = 0; // 存储系统调用返回值
int fd = 0; // 文件描述符(文件操作句柄)
char *filename; // 文件名指针
char buffer; // 读写缓冲区(实际使用前50字节)
/* 从命令行参数获取文件名(建议添加参数校验)*/
filename = argv;
/*-- 打开文件 ------------------------------------------------------*/
// O_RDWR: 读写模式打开,文件不存在时不自动创建
fd = open(filename, O_RDWR);
if(fd < 0) { // 文件描述符为负数表示失败
printf("Can't open file %s\r\n", filename);
return -1; // 返回错误码
}
/*-- 读取文件 ------------------------------------------------------*/
// 从当前文件偏移量读取50字节到buffer
ret = read(fd, buffer, 50);
if(ret < 0) { // 返回负数表示读取失败
printf("Read file %s falied!!\r\n", filename);
return -1; // 注意:此处未关闭文件描述符(潜在资源泄漏)
} else {
// 成功时ret为实际读取字节数(可能小于50)
}
/*-- 写入文件 ------------------------------------------------------*/
// 将buffer内容写入当前文件偏移量(接在读取内容之后)
ret = write(fd, buffer, 50);
if(ret < 0) { // 返回负数表示写入失败
printf("Write file %s falied!!\r\n", filename);
return -1; // 注意:此处未关闭文件描述符
} else {
// 成功时ret为实际写入字节数(可能小于50)
}
/*-- 关闭文件 ------------------------------------------------------*/
ret = close(fd); // 释放文件描述符资源
if(ret < 0) { // 返回非0表示关闭失败
printf("Close file %s falied!!\r\n", filename);
return -1;
}
return 0; // 所有操作成功完成
}
1.3.4字符设备驱动文件测试
1.驱动文件的编译
可参考1.2.3小节驱动源码编译,并将编译好的chrdevbase.ko文件拷贝到个人的nfs/rootfs/lib/modules/4.1.15目录下
2.测试应用的编译
使用以下命令编译chrdevbaseAPP.c文件
arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP
注:因为编译后的代码是在arm架构下运行,因此需要指定gcc编译器为arm-linux-gnueabihf-gcc
编译完成后可以看到我们的目录下新增了一个名为chrdevbaseAPP的可执行文件,如法炮制拷贝到个人的nfs/rootfs/lib/modules/4.1.15目录下
3.驱动文件的测试
首先按照1.2.4小节的方法挂载 chrdevbase.ko驱动,使用命令cat /proc/devices查看当前是否成功挂载驱动,挂载成功如图所示
4.创建设备节点文件
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:
mknod /dev/chrdevbase c 200 0
其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“ 200”是设备的主设备号,“ 0”是设备的次设备号。创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看,结果如图所示:
4.chrdevbase 设备操作测试
如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。前面一直说 Linux 下一切皆文件,包括设备也是文件。
一切准备就绪,使用 chrdevbaseApp 软件操作 chrdevbase 这个设备,看看读写是否正常,首先进行读操作,输入如下命令:
./chrdevbaseApp /dev/chrdevbase
如果看到以下现象,那么说明我们的驱动文件测试成功了,可以进入下一步
2.操作寄存器点亮LED
首先确定LED的寄存器物理地址映射
/* CCM(Clock Controller Module)时钟控制模块寄存器 */
#define CCM_CCGR1_BASE (0X020C406C) // CCGR1控制寄存器,用于GPIO1时钟使能
/*
* [31:0] 各外设时钟使能位,每2bit控制一个模块:
* bit26-27: GPIO1_CLK_ENABLE (0b11表示时钟始终开启)
*/
/* IOMUXC复用控制寄存器 */
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) // GPIO1_IO03引脚复用控制寄存器
/*
* [5:0] 复用模式选择:
* 0x5 = ALT5模式(GPIO1_IO03功能)
* 其他模式详见手册Table 32-5
*/
/* IOMUXC电气属性寄存器 */
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) // GPIO1_IO03电气属性配置寄存器
/*
* [11:0] 关键配置位:
* bit0-2: 压摆率(SRE)0-慢,1-快
* bit3-5: 驱动能力(DSE)0b110=8x驱动强度
* bit6: 开漏输出(ODE)0-关闭
* bit7: 保持使能(HYS)0-关闭
* 典型值:0x10B0(需根据实际硬件验证)
*/
/* GPIO1寄存器组 */
#define GPIO1_DR_BASE (0X0209C000) // GPIO1数据寄存器
/*
* [31:0] 每个bit对应引脚电平状态:
* 写1输出高电平,写0输出低电平
*/
#define GPIO1_GDIR_BASE (0X0209C004) // GPIO1方向寄存器
/*
* [31:0] 每个bit对应引脚方向:
* 1=输出模式,0=输入模式
*/
2.1Linux中的虚拟地址
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址
,因此如果要操作物理地址需要拿到其物理地址的映射,这里使用__iomem,使用方法如下
static void __iomem *<寄存器命名>;
//例如
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
得到虚拟地址后就可以进行寄存器的读写了
2.1.1使用ioremap函数进行地址重映射
ioremap函数的作用是将物理地址(如寄存器地址)映射为内核可操作的虚拟地址,解决MMU启用后无法直接访问物理地址的问题,使用方法如下
IMX6U_CCM_CCGR1=ioremap(CCM_CCGR1_BASE,4);
SW_MUX_GPIO1_IO03=ioremap(SW_MUX_GPIO1_IO03_BASE,4);
SW_PAD_GPIO1_IO03=ioremap(SW_PAD_GPIO1_IO03_BASE,4);
GPIO1_DR=ioremap(GPIO1_DR_BASE,4);
GPIO1_GDIR=ioremap(GPIO1_GDIR_BASE,4);
1.ioremap函数原型
void __iomem *ioremap(resource_size_t phys_addr, size_t size);
参数说明
参数名 | 类型 | 作用 | 注意事项 |
---|---|---|---|
phys_addr | resource_size_t | 要映射的物理起始地址(如寄存器物理地址) | - 必须对齐到页边界(4KB对齐),但未对齐时内核会自动按页取整(如 0x10002 会映射为 0x10000 ) |
size | size_t | 需要映射的内存区域大小(单位:字节) | - 实际映射大小会按页对齐(向上取整到4096的倍数)例如 size=4 会映射整个页(4096字节) |
2.readl
和 writel函数
readl
和 writel
是 Linux 内核中用于访问 I/O 内存的函数。它们主要用于与硬件设备进行交互,例如读取或写入硬件寄存器。下面详细介绍这两个函数的使用方法和参数列表:
readl
函数功能
从指定的 I/O 内存地址读取一个 32 位无符号整数(u32
)。常用于读取硬件寄存器的值。
参数列表
u32 readl(const volatile void __iomem *addr);
addr
:指向 I/O 内存地址的指针(类型为volatile void __iomem *
)。volatile
:告诉编译器不要对该地址的访问进行优化,确保每次都真正读取硬件。__iomem
:内核标记,表明该地址是用于 I/O 内存映射的,帮助静态分析工具检查错误。
返回值
- 返回从指定地址读取的 32 位无符号整数值(
u32
)。
writel
函数功能
向指定的 I/O 内存地址写入一个 32 位无符号整数(u32
)。常用于配置硬件寄存器。
参数列表
void writel(u32 value, volatile void __iomem *addr);
value
:要写入的 32 位无符号整数值(u32
)。addr
:指向 I/O 内存地址的指针(类型为volatile void __iomem *
)。
返回值
- 无返回值(
void
)。
3.向寄存器写数据完成初始化LED
/*使能GPIO时钟*/
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3<<26); //清除bit26,27的配置
val |= 3<<26; //将bit26,27置1
writel(val,IMX6U_CCM_CCGR1); //将设置后的值写入地址
writel(0X5,SW_MUX_GPIO1_IO03); //设置GPIO复用
writel(0X10B0,SW_PAD_GPIO1_IO03);//设置电气属性
val = readl(GPIO1_GDIR);
val |= 1<<3; //将GPIO3置1
writel(val,GPIO1_GDIR); //将设置后的值写入地址
val = readl(GPIO1_DR);
val |= (1<<3); //将GPIO3置0
writel(val,GPIO1_DR); //将设置后的值写入地址
2.2使用register_chrdev_region函数注册字符设备
register_chrdev()
会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走,会大大浪费次设备号
register_chrdev_region()
-
功能:静态分配字符设备的设备号(主设备号 + 次设备号)。
-
特点:
-
需要手动指定主设备号(或让内核自动分配)。
-
分离设备号分配和 cdev 注册的步骤,更灵活。
-
推荐用于需要精确控制设备号的场景(如驱动需要固定主设备号)。
-
register_chrdev()
-
功能:一次性完成字符设备号分配和注册(合并了
register_chrdev_region()
和cdev_add()
的功能)。 -
特点:
-
简化了注册流程,但灵活性较低,因为。
-
若主设备号为 0,内核会自动分配;否则使用指定的主设备号。
-
已逐渐被视为 “遗留接口”,新驱动推荐使用
register_chrdev_region()
+cdev
结构。
-
2.2.1分配设备号
首先将驱动的所有特性封装进结构体中,这样方便新增和删除某些特性
/*LED设备结构体*/
struct newchrled_dev{
struct cdev cdev;
dev_t devid; //设备号
int major; //主设备号
struct class *class; //类
struct device *device; //设备
int minor; //次设备号
};
struct newchrled_dev newchrled; //led设备
1.自动申请设备号和给定设备号
如果给定设备号则生成设备号,否则自动申请
if(newchrled.major){ //给定设备号
newchrled.devid=MKDEV(newchrled.major,0);
ret = register_chrdev_region(newchrled.devid,NEWCHRLED_COUNT,NEWCHRLED_NAME);
}else{ //未给定设备号
ret = alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_COUNT,NEWCHRLED_NAME);
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
/*字符设备注册失败*/
if(ret < 0){
printk("newchrdev chrdev_region error....\r\n");
return -1;
}
2.MKDEV宏
设备号的组成
在 Linux 系统中,每个设备都有一个唯一的设备号(dev_t
),它由两部分组成:
- 主设备号(Major Number):标识设备驱动程序,通常对应一类设备(如硬盘、串口)
- 次设备号(Minor Number):标识具体设备实例,由驱动程序自行解释(如硬盘的分区)
功能概述
- 将主设备号和次设备号合并为一个
dev_t
类型的值 - 与
MAJOR
、MINOR
宏配合使用(用于从dev_t
中提取主 / 次设备号)
函数原型
#include <linux/kdev_t.h>
#define MKDEV(major, minor) (((major) << MINORBITS) | (minor))
-
参数:
major
:主设备号(无符号整数)。minor
:次设备号(无符号整数)。
-
返回值:
- 返回类型为
dev_t
的设备号。
- 返回类型为
3. register_chrdev_region函数
功能概述
在 Linux 系统中,每个字符设备都需要一个唯一的设备号(dev_t
),它由主设备号(Major)和次设备号(Minor)组成。register_chrdev_region()
的主要功能是:
函数原型
#include <linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name);
-
参数:
from
:起始设备号(通过MKDEV(major, minor)
生成)。count
:需要分配的连续设备号数量。name
:设备名(出现在/proc/devices
中,用于标识设备类型)。
-
返回值:
- 成功:返回 0。
- 失败:返回负错误码(如
-EBUSY
表示设备号已被占用)。
4. alloc_chrdev_region函数
功能概述
- 自动选择主设备号:内核会从可用的主设备号池中分配一个未被使用的号码。
- 指定次设备号起始值:用户只需指定起始的次设备号和数量,无需关心主设备号的具体值。
应用场景
- 开发通用驱动(无需固定主设备号)。
- 避免与现有驱动的主设备号冲突。
- 简化驱动注册流程(无需手动管理主设备号)。
函数原型
#include <linux/fs.h>
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
-
参数:
dev
:输出参数,存储分配的第一个设备号(通过MAJOR()
和MINOR()
提取主 / 次设备号)。baseminor
:起始次设备号(例如 0)。count
:需要分配的连续设备号数量。name
:设备名(出现在/proc/devices
中,用于标识设备类型)。
-
返回值:
- 成功:返回 0。
- 失败:返回负错误码(如
-ENFILE
表示设备号用尽)。
5. MAJOR和MINOR宏
功能概述
在 Linux 内核中,MAJOR
和 MINOR
是用于处理设备号(dev_t
)的两个核心宏,分别用于从设备号中提取主设备号(Major Number)和次设备号(Minor Number)
宏的作用
MAJOR(dev_t)
:从dev_t
类型的设备号中提取主设备号。MINOR(dev_t)
:从dev_t
类型的设备号中提取次设备号。
宏定义与实现原理
#include <linux/kdev_t.h>
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
/* 常量定义 */
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
至此,我们就完成了设备号的申请
2.2.2注册字符设备
因为采取了设备号和注册字符设备分离的方式,因此需要单独进行字符设备注册的操作
/*注册字符设备*/
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev,&newchrdev_fops);
ret = cdev_add(&newchrled.cdev,newchrled.devid,NEWCHRLED_COUNT);
if(ret < 0){
printk("Chrdev register filaed!\r\n");
return -1;
}
1.首先要将字符设备与当前模块绑定
将字符设备 newchrled.cdev
的所有权归属到当前内核模块,确保模块卸载时,内核知道哪些设备属于该模块,避免资源泄漏。在设备操作(如 open
、read
)时,内核能正确引用模块的符号表
2.cdev_init函数
功能概述
绑定操作函数:将用户定义的文件操作函数集(struct file_operations
)与内核的 struct cdev
关联。
设置默认值:初始化 struct cdev
的内部字段,为后续注册做准备。
应用场景
字符设备驱动开发中,在注册设备前必须调用 cdev_init()
。
与 cdev_add()
配合使用,完成字符设备的完整注册流程。
函数原型
#include <linux/cdev.h>
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
-
参数:
cdev
:指向待初始化的struct cdev
结构体的指针。fops
:指向文件操作函数集(struct file_operations
)的常量指针。
-
返回值:
- 无返回值(
void
)。
- 无返回值(
3.cdev_add函数
功能概述
将设备添加到内核:使内核能够将用户空间的系统调用(如 open()
、read()
)映射到驱动的 操作函数。
关联设备号:将 struct cdev
与之前分配的设备号(dev_t
)绑定。
应用场景
字符设备驱动开发中,在完成设备号分配(alloc_chrdev_region()
)和设备初始化 (cdev_init()
)后调用。
动态创建设备(如按需创建多个子设备)。
函数原型
#include <linux/cdev.h>
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
-
参数:
p
:指向已初始化的struct cdev
结构体的指针。dev
:设备号(通过MKDEV(major, minor)
或动态分配获得)。count
:连续设备号的数量(通常为 1,表示单个设备)。
-
返回值:
- 成功:返回 0。
- 失败:返回负错误码(如
-EBUSY
表示设备号已被占用)。