驱动相关基础
一、驱动分类与区别
字符设备驱动
一个字节一个字节进行读写操作的设备,以字符流的形式进行数据传输(如鼠标、键盘、串口)。
块设备驱动
以块为单位进行读写操作的设备,块的大小通常为 512 字节、1024 字节。
块设备驱动主要用于管理存储设备,支持随机访问和高效的数据传输(磁盘、硬盘、SD 卡)
网络设备驱动
负责将网络数据包从物理层传输到网络层,并将网络层的数据包封装成物理层可以传输的形式。
网络设备驱动需要支持多种网络协议和接口标准(以太网网卡驱动)。
二、驱动的编译流程
(1)内核源码目录内
在 /drivers/char 目录下编写驱动,在 Makefile 中添加
obj -m $(XXXX) += xxx.o
在 Kconfig 中添加对应驱动的相关选项信息,在顶层目录中通过 make menuconfig 找到添加好的驱动选项并选为 M/Y (动态编译/静态编译),会将驱动的编译方式写入到 .config 文件,输入
make / make modules 即可被静态编译进内核或者编译成 .ko 模块。
(2)内核源码目录外
编译好驱动后,通过 Makefile 指定源码路径,当前目录路径,驱动模块名等,make 编译即可在当前目录下生成驱动模块。
三、静态编译和动态编译的区别
静态编译:
将驱动直接编译进内核,后续如果要从内核中删除该驱动,需要通过 make menuconfig 将该驱动选项选为 N ,重新编译内核,开发调试较为复杂。
动态编译:
将驱动编译成 .ko 模块,后续如果更改了驱动直接重新编译生成新的 .ko 模块即可,方便开发、调试。
四、字符型设备驱动的流程
(1)构建 cdev 结构体,用来描述字符型设备相关信息。
(2)构建 file_operation 结构体,用来描述驱动层操作函数。
(3)完成 module_init()和 module_exit()宏的填充,指定驱动模块的初始化和注销函数,
MODULE_LICENSE 指定 GNU 组织的 GPL 协定。
(4)实现驱动的初始化 init 函数,调用
aloc_chrdev_region / register_chrdev_region
cdev_init
cdev_add
class_create
device_create
完成对字符型设备的注册、cdev 结构体的初始化、cdev 结构体增加到系统,自动生成设备节点,此后完成相应硬件初始化。
(5)实现驱动的注销 exit 函数,调用
unregister_chrdev_region
cdev_del
class_destroy
device_destroy
完成对字符型设备的注销、cdev 的注销、节点删除,再调用相关函数完成硬件的注销。
(6)完善 file_operation 结构体中的操作函数,如 open、read、write、release 等函数。
五、静态注册字符型设备和动态注册的区别
静态注册:
事先定义好设备号,将定义好的设备号注册到系统。
动态注册:
让内核自动分配一个未被占用的设备号,可避免和内核中的设备号冲突。
六、如何手动创建设备节点
mknod /dev/xxx c 250
/dev/xxx :设备节点名
c :设备类型,字符型设备
250:主设备号
0 :次设备号
七、如何加载驱动模块
insmod / modprobe
八、inmsod 和 modprobe 加载驱动模块的区别
如果要加载多个驱动模块时,多个驱动模块之间可能存在依赖关系(比如 A.ko 依赖 B.ko,此时如果 insmod 直接加载 A.ko 会导致失败,因为 insmod 不会分析驱动模块间的依赖关系)
modprobe:能够分析驱动模块间的依赖关系,确保在加载一个驱动模块时,其依赖的其他驱动模块也会被加载。
insmod:不会分析驱动模块间的依赖关系。
九、主设备号和次设备号的区别
主设备号:确定是哪一类设备。
次设备号:该类设备下的具体哪个设备。
十、设备号的大小
Linux 中,用 32 位的 unsigned int 表示设备号,高 12 位是主设备号,低 20 位是次设备号。
十一、如何构建设备号
通过 MKDEV 宏传入主次设备号构建一个新的设备号。
十二、__init 修饰 init 函数的作用
__init 用来标记驱动模块的初始化函数,当执行 insmod 时,该初始化函数会被调用将标记的代码放在特定的内存(.init.text)在加载驱动模块后,这些初始化代码所占用的内存会被自动释放,节省内核空间。
十三、__exit 修饰 init 函数的作用
__exit 用来标记驱动模块的注销函数,当执行 rmmod 时,该注销函数会被调用,释放相应资源,__exit 宏会将标记的代码段存放在特定的内存区域(.exit.text)
十四、杂项设备驱动的主设备号是多少
10。
十五、杂项设备驱动和字符型设备驱动的区别
字符型设备驱动不会自动生成设备节点,需要手动用 mknod 生成或者调用 class_create 和
device_create 自动创建设备节点,主设备号不固定。
杂项设备驱动可以自动生成设备节点,主设备号固定是 10,代表设备类型。
十六、platform 总线思想
platform ,虚拟总线,通过 device-driver-bus 实现设备和驱动分离的思想,将 device 和 driver 注册到 platform 总线,通过 device name 和 driver name 匹配,匹配成功后执行 driver 中的 probe 函数,注销执行 remove 函数。
十七、platform 中的 device 和 driver 如何匹配
platform_match 函数通过比较总线上的 device 和 driver 的名字来实现设备和驱动匹配。
十八、platform 驱动实现的流程
(1)构建 device.c 和 driver.c
(2)device.c 中完善 platform_device 结构体描述设备信息,通过调用 init 函数中的 platform_device_register 函数将设备注册到 bus 总线,调用 exit 函数中的 platform_device_unregister 函数将设备从 bus 总线注销。
(3)driver.c 中构建 platform_driver 结构体,填充 probe、remove 等成员;
在 init 函数中调用 platform_driver_register 函数将驱动注册到 bus 总线;
exit 函数中调用 platform_driver_unregister 函数将驱动从 bus 总线注销;
probe 函数中实现字符型设备驱动的相关初始化以及硬件初始化;
remove 函数中实现字符型设备驱动的相关注销以及硬件注销。
(4)当 device 和 driver 在 bus 总线上通过 platform_match 函数匹配成功后执行驱动中的 probe 函数。
十九、platform_match 函数匹配原理
strcmp 比较 device 和 driver 的 .name 字段 ,匹配成功函数返回 1,失败返回 0。
二十、I2C 的驱动框架介绍(I2C子系统)
I2C_hardware 层:具体硬件层(I2C传感器)。
I2C_adapter:I2C 适配器层,驱动厂家实现。
I2C_core:I2C 核心层,提供 I2C 设备和驱动的注册、匹配及通信方法。
I2C_client / I2C_driver :遵循 platform 设备驱动分离思想,匹配成功后执行 probe 函数。
I2C 传感器挂载在对应的 I2C 总线上,I2C 适配器层驱动由芯片厂家实现,I2C 核心层用来管理
I2C 设备和驱动的注册、匹配以及 I2C 通信方法,设备驱动层通过将 I2C 总线上的设备和驱动匹配,执行 probe 函数。应用层调动驱动层接口实现 I2C 的读取写入。
二十一、I2C 子系统中,device 和 driver 匹配的过程
1.注册 I2C 适配器
在 device 文件中,定义设备,携带控制器寄存器地址、中断等信息。
static struct resource s3c_i2c_resource[] = {[0] = { .start = S3C2440_PA_I2C, .end = S3C2440_PA_I2C + 0x3f, .flags = IORESOURCE_MEM },[1] = { .start = IRQ_I2C, .end = IRQ_I2C, .flags = IORESOURCE_IRQ },
};static struct platform_device s3c_device_i2c = {.name = "s3c2440-i2c",.id = 0,.num_resources = ARRAY_SIZE(s3c_i2c_resource),.resource = s3c_i2c_resource,
};
在 driver 文件中,在 probe 函数中初始化 I2C 适配器;
static int s3c2440_i2c_probe(struct platform_device *pdev) {struct s3c2440_i2c *i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);// 映射寄存器、申请中断、设置时钟频率等i2c->adapter.name = "s3c2440-i2c";i2c->adapter.algo = &s3c2440_i2c_algo; // 通信算法(如 xfer 函数)int ret = i2c_add_adapter(&i2c->adapter); // 注册适配器到 I2C 总线return ret;
}
i2c_add_adapter 函数会触发 I2C 总线扫描,尝试与已注册的设备匹配。
2.注册 I2C 设备
在 device 文件在中,定义设备信息,如从机地址、设备名称,适配器初始化时自动创建设备。
static struct i2c_board_info smdk2440_i2c_devs[] __initdata = {{I2C_BOARD_INFO("24c02", 0x50), // 设备名称为 "24c02",地址 0x50.platform_data = &eeprom_platform_data, // 可选平台数据},
};static void __init smdk2440_i2c_init(void) {i2c_register_board_info(0, smdk2440_i2c_devs, ARRAY_SIZE(smdk2440_i2c_devs));
}
i2c_register_board_info 会在适配器注册时(通过 i2c_add_adapter)自动调用 i2c_new_device 来创建 i2c_device。
3.注册 I2C 驱动
通过 i2c_driver 结构体注册到 I2C 总线:
static struct i2c_device_id eeprom_ids[] = {{ "24c02", 0 }, // 匹配设备名称 "24c02"{ }
};
MODULE_DEVICE_TABLE(i2c, eeprom_ids);static struct i2c_driver eeprom_driver = {.driver = {.name = "eeprom", // 驱动名称},.probe = eeprom_probe,.remove = eeprom_remove,.id_table = eeprom_ids, // 匹配表
};static int __init eeprom_driver_init(void) {return i2c_add_driver(&eeprom_driver); // 注册驱动到 I2C 总线
}
module_init(eeprom_driver_init);
匹配逻辑:
当设备或驱动注册到 I2C 总线时,触发匹配流程(i2c_bus_type.match 函数)
1.设备注册时:
新设备(i2c_device)注册到总线后,总线遍历所有已注册的驱动。
对每个驱动,检查以下条件:
(1)ID 表匹配:遍历驱动的 id_table,检查设备名称(name)或从机地址是否匹配。比如,设备名称为 “myi2c”,驱动 id_table 包含 “myi2c”,则匹配成功。
(2)若驱动没有提供 id_table,直接比较设备名称与驱动名称。
匹配成功后,调用驱动的 probe 函数,并传入设备结构体。
2.驱动注册时:
总线遍历所有已注册好的设备,也执行上述匹配逻辑。
驱动探测 probe 函数与绑定:
匹配成功后,内核调用驱动的 probe 函数,完成设备初始化。
static int eeprom_probe(struct i2c_client *client, const struct i2c_device_id *id) {struct eeprom_device *eeprom = devm_kzalloc(&client->dev, sizeof(*eeprom), GFP_KERNEL);eeprom->client = client;// 读取设备数据、创建文件系统节点等return 0;
}
struct i2c_client 是 i2c_device 的封装,包含设备地址、适配器、名称等信息。
驱动通过 i2c_transfer 等函数与设备通信。