分离分层原则:
驱动 总线 设备
.c platform 设备树
字符设备框架 I2C pinctrl
块设备框架 SPI等
网络设备框架
对于驱动的编写是在写内核程序,本质还是调操作系统的api
我们需要会的就是写设备树
知道不同框架下调用的api
在驱动程序中无论去调用哪一种api,都要记得有注册就有销毁
驱动部分
驱动模块的加载和卸载
将linux驱动添加到内核中有两种方式,(1)与linux内核代码一起编译运行
(2) 编译成模块(扩展名.ko) (这就是我想找的动态加载)
运行命令:
加载:
insmod #不能解决模块的依赖关系 (自己按顺序分析加载)
---
depmod #扫描 `/lib/modules/<kernel-version>/`目录,分析所有 `.ko`模块文件之间的依赖关系,并生成 `modules.dep`、`modules.alias`等索引文件
modprobe #会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中(depmod生成的)
卸载:
rmmod xx.ko
或
modprobe -r xx.ko
depmod
modprobe xx.ko
rmmod xx.ko
模块的加载和卸载注册函数(写到.c下):
| 宏/声明 | 是否必需 | 基本含义 |
|---|---|---|
module_init(xx_init) |
是 | 指定驱动模块的加载入口函数 |
module_exit(xx_exit) |
是 | 指定驱动模块的卸载出口函数 |
MODULE_LICENSE("GPL") |
是 | 声明模块的开源许可证 |
MODULE_AUTHOR("MARK") |
否 | 声明模块的作者信息 |
MODULE_INFO(intree, "Y") |
否 | 提供模块的额外自定义信息 |
(新)字符设备框架
// (1) 分配和释放设备号
// 这里面如果有定义设备号就使用,没有就去申请
#define NEWCHRLED_CNT 1 /*设备号个数 */
int major; /* 计划使用的或内核分配的主设备号 */
int minor; /* 计划使用的或内核分配的次设备号 */
dev_t devid; /* 用于存储完整设备号的变量 */
if (major) {
/* 情况1:静态指定 - 当我们已经知道一个可用的主设备号时使用 */
/* 将主设备号(major)和次设备号(0)组合成一个完整的设备号 */
devid = MKDEV(major, 0);
/* 向内核注册(申请)这个指定的设备号范围 */
register_chrdev_region(devid, NEWCHRLED_CNT, "test");
} else {
/* 情况2:动态申请 - 让内核自动为我们分配一个可用的主设备号 */
/* 申请一个设备号,其信息(主、次设备号)将存回devid变量中 */
alloc_chrdev_region(&devid, 0, NEWCHRLED_CNT, "test");
/* 从内核分配得到的完整设备号(devid)中,提取出主设备号 */
major = MAJOR(devid);
/* 从内核分配得到的完整设备号(devid)中,提取出次设备号 */
minor = MINOR(devid);
}
/*
MKDEV的头文件是 linux/kdev_t.h
其是一个宏
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MINORBITS通常是20
这也是我们常说的老字符设备驱动的问题:
使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为
200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
*/
//无论是上面哪一种(静态指定/动态申请)
//对应的注销设备号的代码
unregister_chrdev_region(devid,NEWCHRLED_CNT);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
| 参数名 | 类型 | 说明 | 典型值 |
|---|---|---|---|
baseminor |
unsigned | 期望分配的起始次设备号。通常设为0,表示从这类设备的第一个次设备号开始分配。 | 0 |
count |
unsigned | 请求分配的连续设备编号的数量(即次设备号的个数)。 | 1 |
字符设备注册方法
//(1) cdev结构体
//include/linux/cdev.h
struct cdev
{
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
| 字段名 | 类型 | 说明 |
|---|---|---|
kobj |
struct kobject |
内嵌的内核对象。这是 Linux 设备模型的基础。它负责: 1. 引用计数:跟踪有多少个地方正在使用这个 cdev,确保在使用时模块不会被卸载。 2. 在 sysfs 中创建条目:例如在 /sys/下生成对应的设备信息。 3. 提供层次结构:实现对象之间的父子关系。 |
owner |
struct module * |
指向拥有这个字符设备的内核模块的指针。通常直接初始化为 THIS_MODULE。它的作用是防止内核模块在设备仍被使用(文件已打开)时被意外卸载。如果模块被卸载而设备还在用,会导致内核崩溃。 |
ops |
const struct file_operations * |
指向文件操作集合的指针。这是最关键的字段!它指向一个包含了驱动所实现的各个函数指针(如 open, read, write, ioctl, release等)的结构体。用户空间的应用程序调用如 read(fd, ...),最终就是通过这里找到对应的驱动函数来执行的。
|
list |
struct list_head |
链表头。内核维护了一个全局链表来管理所有的字符设备(cdev)。这个字段用于将当前 cdev对象链接到那个全局链表中,方便内核进行遍历和管理。驱动开发者通常不直接操作它。 |
dev |
dev_t |
设备的起始设备号。它标识了这个 cdev所对应的设备。如果这个 cdev代表一个设备(如 count=1),那么 dev就是这个设备的完整设备号。 |
count |
unsigned int |
这个 cdev所管理的连续设备编号的数量。它通常和你调用 alloc_chrdev_region或 register_chrdev_region时传入的 count参数一致。例如,一个驱动管理了3个从设备(次设备号0,1,2),那么 count就应该是3。 |
使用cdev_init函数进行初始化
函数原型
// char_dev.c
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
/* 定义一个字符设备控制结构体,内核通过它来管理字符设备 */
struct cdev testcdev;
/* 定义并初始化文件操作函数集结构体
* 这个结构体包含了驱动实现的所有设备操作函数指针(如open、read、write等)
* 当用户空间对设备文件进行操作时,VFS将通过这里找到对应的驱动函数
file_operations是绑定操作函数(使用的是函数指针)
THIS_MODULE代表的是当前正在运行的“内核模块”
*/
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = xx_open,
.read = xx_read,
.write = xx_write,
.release = xx_release,
};
testcdev.owner=THIS_MODULE;
/* 初始化cdev结构体:将testcdev与test_fops绑定
* 这个函数会:
* 1. 清空testcdev(memset清零)
* 2. 将test_fops的地址赋值给testcdev->ops
* 3. 初始化testcdev内部的链表等字段
*/
cdev_init(&testcdev, &test_fops);
使用cdev_add函数向linux系统添加字符设备
在使用cdev_init函数对cdev结构体变量进行初始化后,使用cdev_add函数向linux系统添加(cdev结构体变量)这个字符设备
函数原型:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
在上面cdev_init的代码基础上
cdev_add(&testcdev,devid,1); //添加字符设备
有添加就要有删除
(这个函数是要在卸载驱动的函数中)
使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
void cdev_del(struct cdev *p)
cdev_del(&testcdev);
设备节点文件
手动创建
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件
用户态下的应用程序就是通过操作这个设备节点文件来完成对具体设备的操作
字符设备创建设备节点文件
(testchrdevbase是我们命令)
mknod /dev/testchrdevbase c 200 0
“mknod”是创建节点命令,“/dev/testchrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。
创建完成以后就会存在/dev/testchrdevbase 这个文件,可以使用“ls /dev/testchrdevbase -l”命令查看
自动创建设备节点
代码需要放到驱动程序的入口函数(一般写在cdev_add函数后)
(1)创建"类"
struct class *m_class;
m_class=class_create(THIS_MODULE, NEWCHRLED_NAME);
___
//原型
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
对应的卸载
void class_destroy(struct class *cls);
(2)创建设备
函数原型
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父
设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
//使用举例
device = device_create(dtsled.class, NULL, devid, NULL, NEWCHRLED_NAME);
对应的卸载
void device_destroy(struct class *class, dev_t devt)