提示:字符设备是指在 I/O 传输过程中以字符为
单位进行传输的设备, 可以使用与普通文件相同的文件操作命令(打开、 关闭、 读、 写等) 对
字符设备进行操作, 是 Linux 驱动中最基本的一类设备驱动, 例如最常见的 LED、 按键、 IIC、
SPI, LCD 等都属于字符设备的范畴。 要想对字符设备进行操作, 需要通过设备号来对相应的设
备进行查找
文章目录
目的
-
掌握静态指定设备号的register_chrdev_region方法
-
掌握动态分配设备号的alloc_chrdev_region方法
-
理解两种方法的区别及适用场景
备注
字符设备内容包括了:申请-注册-创建节点-杂项设备-内核空间用户空间数据交换-文件私有数据-驱动兼容不同设备-错误处理-点亮LED灯 等系统模块,所以这篇文章内容是开篇,方便后续理解,打基础。
设备号申请
在 Linux 系统中每一个设备都有相应的设备号, 通过该设备号查找对应的设备, 从而进行
之后的文件操作。 设备号有主设备号与次设备号之分, 主设备号用来表示一个特定的驱动, 次
设备号用来管理下面的设备。
在 Linux 驱动中可以使用以下两种方法进行设备号的申请:
- 通过 register_chrdev_region(dev_t from, unsigned count, const char *name)函数进行静态
申请设备号。 - 通 过 alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char
*name)函数进行动态申请设备号。
两个函数在“内核源码/include/linux/fs.h”文件中引用
设备号基础概念
设备号由两部分组成:
-
主设备号:标识设备类型(如 240 代表某个特定驱动)
-
次设备号:标识同类设备中的具体实例
设备号用 dev_t 类型表示(32位整数,高12位为主设备号,低20位为次设备号)
设备号申请方法
静态申请(指定设备号)
#include <linux/fs.h>
// 指定主设备号和设备名称
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
参数说明:
- first:要分配的起始设备号(用 MKDEV 宏生成)
- count:连续设备号的数量
- name:设备名称(出现在 /proc/devices)
示例申请代码
#define MAJOR_NUM 240 // 选择未被使用的主设备号
#define DEVICE_NAME "my_static_dev"
dev_t dev = MKDEV(MAJOR_NUM, 0); // 主设备号240,次设备号0
if (register_chrdev_region(dev, 1, DEVICE_NAME) < 0) {
printk(KERN_ERR "Failed to register device number\n");
return -EBUSY; // 设备号可能已被占用
}
这里再次逐字分析:
- MAJOR_NUM:定义主设备号
- DEVICE_NAME: 定义字符驱动名称
- dev_t: 类型
- MKDEV:宏定义,构造设备号
- register_chrdev_region :静态申请字符设备
动态申请(自动分配设备号)
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, const char *name);
参数说明:
- dev:输出参数,保存分配到的设备号
- firstminor:请求的起始次设备号(通常为0)
- count:需要的设备号数量
- name:设备名称
动态示例申请代码
dev_t dev;
int major;
if (alloc_chrdev_region(&dev, 0, 1, "my_dynamic_dev") < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return -EFAULT;
}
major = MAJOR(dev); // 提取主设备号
printk(KERN_INFO "Allocated major number: %d\n", major);
这里再次逐字分析:
- dev_t: 类型,输出参数,保存分配到的设备号
- major:定义一个变量,最后动态申请得到的主设备号
- alloc_chrdev_region: 动态申请设备号方法
- MAJOR: 通过方法,提取设备号,参数是dev_t
实验
源码程序
根据上面的 设备号申请 相关介绍,这里给出测试程序
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h> //申请设备号需要用到的头文件
#include <linux/kdev_t.h>
static int major;//定义静态加载方式时的主设备号参数major
static int minor;//定义静态加载方式时的次设备号参数major
module_param(major, int, S_IRUGO);//传递int 类型变量name
module_param(minor, int, S_IRUGO);//传递int 类型变量name
static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num
static int __init dev_t_init(void)//驱动入口函数
{
int ret;//定义int类型的变量ret,用来判断函数返回值
/*以主设备号进行条件判断,即如果通过驱动传入了major参数则条件成立,进入以下分支*/
if(major){
dev_num=MKDEV(major,minor); //通过MKDEV函数将驱动传参的主设备号和次设备号转换成dev_t类型的设备号
printk("major received %d \n",major);
printk("major received %d \n",minor);
ret=register_chrdev_region(dev_num,1,"chardev_name");
if(ret<0){
printk("register_chrdev_region is error \n");
}
//printk("register_chrdev_region is ok %d \n" , ret);
}else{
/*如果没有通过驱动传入major参数,则条件成立,进入以下分支*/
ret=alloc_chrdev_region(&dev_num,0,1,"chardev_num"); //通过动态方式进行设备号注册
if(ret < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_chrdev_region is ok\n");
major=MAJOR(dev_num);//通过MAJOR()函数进行主设备号获取
minor=MINOR(dev_num);//通过MINOR()函数进行次设备号获取
printk("major is %d\n",major);
printk("minor is %d\n",minor);
}
return 0;
}
static void __exit dev_t_exit(void)//驱动出口函数
{
unregister_chrdev_region(dev_num,1);释放字符驱动设备号
printk(KERN_EMERG "parameter_exit\n");
}
module_init(dev_t_init);//注册入口函数
module_exit(dev_t_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("wang fang chen "); //作者信息
源程序代码分析
再次对上面需要注意到的点,逐字分析说明:
- major 、minor : 主设备号 测设备号定义变量,int 类型。
- module_param : 前面的基础知识,驱动传参,这里用到了。
- dev_num :dev_t的类型变量,就是设备号的定义。
- MKDEV:将主设备号和此设备号传递的值,转换为 内核认识的dev_t 类型的设备号
- register_chrdev_region :设备号静态申请方法
- alloc_chrdev_region :设备号动态申请方法
- &dev_num : 动态申请设备号的 输出参数,保存分配到的设备号。 这里为什么用 指针类型,这里不多讲了,基础内容,需要注意下。
- MAJOR :动态申请到设备号后,可以通过这个api进行获取主设备号
- MINOR :动态申请到设备号后,可以通过这个api进行获取次设备号
MakeFile 编译文件
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += dev_t.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
make 进行编译
测试
动传参,传递参数实验-静态申请
insmod dev_t.ko major=200 minor=0
获取内核打印信息
dmesg | tail
查看系统字符设备驱动
可以看到传入的主设备号和次设备号都被打印了出来, “register_chrdev_region is ok” 也
被成功打印了证明设备注册成功了, 然后使用以下命令进行注册设备号的查看
cat /proc/devices
我们看到了我们静态申请的设备号了
动传参,传递参数实验-动态申请
先卸载驱动一次
rmmod dev_t.ko
实际结果如下
进行动态传参实验->动态申请设备号
insmod dev_t.ko
获取内核打印信息
dmesg | tail
查看系统字符设备驱动
cat /proc/devices
动态申请和静态申请区别-场景-对比
静态申请 | 动态申请 | |
---|---|---|
类型 | register_chrdev_region | alloc_chrdev_region |
设备号指定方式 | 开发者静态指定 | 内核动态分配 |
主要参数 | 明确的dev_t设备号 | 输出参数获取分配的设备号 |
优点 | 设备号固定,便于管理 | 避免冲突,更安全 |
缺点 | 可能发生设备号冲突 | 每次加载可能获得不同设备号 |
适用场景 | 需要固定设备号的传统设备 | 大多数现代驱动开发 |
错误处理 | 需检查设备号是否被占用 | 只需检查分配是否成功 |
总结
- 之前的动态传参实际应用
- 设备号的理解,动态和静态申请设备号熟悉理解,后续必须用到的基础知识
- 字符设备号在系统位置的熟悉