嵌入式Linux驱动——3 总线设备驱动模型
目录
1.总线设备驱动模型
1.1 总线设备驱动模型
1.2 设备树
1.3 platform_device 和 platform_driver 的匹配规则
1.3.1 最先比较
1.3.2 然后比较
1.3.3 最后比较
2.LED 模板驱动程序的改造:总线设备驱动模型
1.总线设备驱动模型
在前面的 led 驱动程序中,我们使用分离的思想驱动程序分为了 led_resource.c 和 led_drv.c
分离思想为不同种类的硬件资源定义了不同的 resource 结构体,但是在设备有各种资源,难道每种资源都用一个结构体表示?
这并不现实,因此在分离思想中扩展出总线设备驱动模型
ps:其实并没有解决这个问题,这也是一种分离的思想方法
1.1 总线设备驱动模型
引入 platform_device 和 platform_driver,将“资源”与“驱动”分离开来:
可以看到,一种硬件资源通过一个 platform_device 结构体表示,并通过总线 bus 与其相对的驱动 platform_driver 结构体相连接
(ps:好像并没有解决一种资源用一个结构体表示的问题,不过韦老师在视频是这样介绍的...不懂了)
答:因为这也是一种分离的思想,确实是没有解决这个问题
特点:
- 代码稍微复杂,但是易于扩展。跟“分离思想”类似,可随时修改硬件资源,不像传统写法,引脚的使用和操作都写死在代码中
- 冗余代码太多,修改引脚时设备端的代码需要重新编译
1.2 设备树
因每个板子的硬件资源都不同,因此会存在大量的硬件资源 .c 文件存在于Linux内核中,这就会导致内核十分庞大臃肿
由此引出设备树
- 对于每个单板都会有其对应的 dts 文件包含它的所有硬件资源
- 需要使用时,将该单板的 dts 文件编译成 dtb 文件并传入内核
- 内核会解析dtb文件并构造出一系列的 platform_device
因 dts 文件放在内核之外,这样就保持了 Linux 内核的干净
并不是说有了设备树文件就不用再编写驱动:设备树仅用于描述硬件资源,具体操作还得编写驱动代码
1.3 platform_device 和 platform_driver 的匹配规则
1.3.1 最先比较
若 platform_device 中定义了 driver_override
则最先比较 platform_device.driver_override 和 platform_driver.driver.name
可以通过设置 platform_device 的 driver_override,强制选择某个 platform_driver (非某人不嫁)
1.3.2 然后比较
platform_device. name 和 platform_driver.id_table[i].name
Platform_driver.id_table 是 “platform_device_id” 指针,表示该 drv 支持若干个 device,它里面列出了各个 device 的{.name, .driver_data},其中的“name”表示该 drv 支持的设备的名字,driver_data 是些提供给该 device 的私有数据。
通过 name 进行匹配
1.3.3 最后比较
platform_device.name 和 platform_driver.driver.name
platform_driver.id_table 可能为空, 这时可以根据 platform_driver.driver.name 来寻找同名的 platform_device。
2.LED 模板驱动程序的改造:总线设备驱动模型
board_A_led.c:
- 定义 platform_device 结构体中的内容:name、num_resources、resource等成员
- 在入口函数和出口函数中分别注册和注销 platform_device 结构体
demo 示例代码:
#include "led_resource.h" static struct led_resource board_A_led = { .pin = GROUP_PIN(3,1), }; /*定义resource资源*/ /*flag随便拿一个IORESOURCE_IRQ用着先*/ static struct resource resource[] = { { .start = GROUP_PIN(3,1), .flags = IORESOURCE_IRQ, }, { .start = GROUP_PIN(5,8), .flags = IORESOURCE_IRQ, }, } /*定义platform_device结构体*/ static struct platform_device board_A_led_dev ={ .name = "100ask_led", .num_resources = ARRAY_SIZE(resource), .resource = resource, }; /*入口函数*/ static int led_dev_init(void) { int err; /*注册*/ err = platform_device_register(&board_A_led_dev); return 0; } /*出口函数*/ static void led_dev_exit(void) { int err; /*注销*/ err = platform_device_unregister(&board_A_led_dev); return 0; } module_init(led_dev_init); module_exit(led_dev_exit); MODULE_LICENSE("GPL");
chip_demo_gpio.c:
- 老规矩,配置 led_operations 结构体,定义实现结构体成员的函数 board_demo_led_init 和 board_demo_led_ctl(硬件操作)
- 配置平台驱动 platform_driver 结构体,实现成员函数:
chip_demo_gpio_probe:用于初始化设备,只要有 platform_device 与 platform_driver 配对,就会执行这个函数,可以看出,把之前在 leddrv.c 中的创建设备节点的命令放到了这里
chip_demo_gpio_remove:用于清除设备,就是和上面相反- 最后也是老规矩,实现该驱动的入口函数和出口函数
注意:入口函数中的 register_led_operations 操作,这是给 leddrv.c 传入led_operations 结构体用的,为什么不在 leddrv.c 中调用定义好的 get_board_led_opr 函数??
这涉及一个相互依赖问题,因为前面 chip_demo_gpio_probe 中创建和清除设备节点的函数 led_class_create_device 依赖于底层的 leddrv.c ,如果此时又在底层的 leddrv.c 中调用上层的 get_board_led_opr ,会造成的交叉依赖的问题,因此只能通过指针在上层传给leddrv.c
demo程序代码:(只打印信息,不做具体操作)#include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h> #include <linux/platform_device.h> #include "led_opr.h" #include "leddrv.h" #include "led_resource.h" // 全局LED引脚数组和计数器 static int g_ledpins[100]; static int g_ledcnt = 0; // LED初始化函数:配置指定LED的GPIO引脚 static int board_demo_led_init(int which) { printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which])); // 根据GPIO组号执行不同的初始化操作 switch(GROUP(g_ledpins[which])) { case 0: printk("init pin of group 0 ...\n"); break; case 1: printk("init pin of group 1 ...\n"); break; case 2: printk("init pin of group 2 ...\n"); break; case 3: printk("init pin of group 3 ...\n"); break; } return 0; } // LED控制函数:设置指定LED的开关状态 static int board_demo_led_ctl(int which, char status) { printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which])); // 根据GPIO组号执行不同的控制操作 switch(GROUP(g_ledpins[which])) { case 0: printk("set pin of group 0 ...\n"); break; case 1: printk("set pin of group 1 ...\n"); break; case 2: printk("set pin of group 2 ...\n"); break; case 3: printk("set pin of group 3 ...\n"); break; } return 0; } // LED操作接口结构体 static struct led_operations board_demo_led_opr = { .init = board_demo_led_init, .ctl = board_demo_led_ctl, }; // 获取LED操作接口 struct led_operations *get_board_led_opr(void) { return &board_demo_led_opr; } // 平台设备探测函数:初始化LED设备 static int chip_demo_gpio_probe(struct platform_device *pdev) { struct resource *res; int i = 0; // 遍历并注册所有LED资源 while (1) { res = platform_get_resource(pdev, IORESOURCE_IRQ, i++); if (!res) break; g_ledpins[g_ledcnt] = res->start; led_class_create_device(g_ledcnt); g_ledcnt++; } return 0; } // 平台设备移除函数:清理LED设备 static int chip_demo_gpio_remove(struct platform_device *pdev) { struct resource *res; int i = 0; // 注销所有LED设备 while (1) { res = platform_get_resource(pdev, IORESOURCE_IRQ, i); if (!res) break; led_class_destroy_device(i); i++; g_ledcnt--; } return 0; } // 平台驱动定义 static struct platform_driver chip_demo_gpio_driver = { .probe = chip_demo_gpio_probe, .remove = chip_demo_gpio_remove, .driver = { .name = "100ask_led", // 驱动名称 }, }; // 驱动初始化 static int __init chip_demo_gpio_drv_init(void) { int err; // 注册平台驱动和LED操作 err = platform_driver_register(&chip_demo_gpio_driver); register_led_operations(&board_demo_led_opr); return 0; } // 驱动退出 static void __exit lchip_demo_gpio_drv_exit(void) { platform_driver_unregister(&chip_demo_gpio_driver); } module_init(chip_demo_gpio_drv_init); module_exit(lchip_demo_gpio_drv_exit); MODULE_LICENSE("GPL");
leddrv.c:
- 使用 EXPORT_SYMBOL() 函数将创建、删除设备号和读取 led_operations 结构体函数导出给 chip_demo_gpio.c 使用
EXPORT_SYMBOL():使其对所有内核代码可见,从而可以在其他内核模块中直接调用。若只包含在头文件中,则只有在该模块中可以使用- 其他就是正常的 led 驱动操作,配置 file_operations 结构体,实现结构体成员的各种函数
上面已经解释了为什么不能通过get_board_led_opr函数获取led_operations结构体,这里不再重复
demo程序代码:(只打印信息,不做具体操作)#include <linux/module.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/miscdevice.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h> #include "led_opr.h" // 自定义LED操作接口头文件 /* 主设备号:0表示由内核动态分配 */ static int major = 0; static struct class *led_class; // 设备类指针 struct led_operations *p_led_opr; // LED操作函数集指针 /* 工具宏:取最小值 */ #define MIN(a, b) (a < b ? a : b) /**************************** 设备管理接口 ****************************/ /** * @brief 创建设备节点 * @param minor 次设备号 */ void led_class_create_device(int minor) { // 在/dev目录下创建设备节点,命名为100ask_led0, 100ask_led1等 device_create(led_class, NULL, MKDEV(major, minor), NULL, "100ask_led%d", minor); } /** * @brief 销毁设备节点 * @param minor 次设备号 */ void led_class_destroy_device(int minor) { device_destroy(led_class, MKDEV(major, minor)); } /** * @brief 注册LED操作函数集 * @param opr 包含init/ctl等操作函数的指针结构体 */ void register_led_operations(struct led_operations *opr) { p_led_opr = opr; // 保存外部传入的操作函数集 } /* 导出符号供其他模块使用 */ EXPORT_SYMBOL(led_class_create_device); EXPORT_SYMBOL(led_class_destroy_device); EXPORT_SYMBOL(register_led_operations); /**************************** 文件操作接口 ****************************/ /* read操作(未实现实际功能) */ static ssize_t led_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } /** * @brief write操作:控制LED状态 * @param file 文件结构体 * @param buf 用户空间数据缓冲区(包含控制命令) * @param size 数据大小 * @param offset 文件偏移 * @return 成功写入的字节数 */ static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { int err; char status; // 存储LED状态(0/1) struct inode *inode = file_inode(file); int minor = iminor(inode); // 获取次设备号 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); // 从用户空间拷贝控制数据 err = copy_from_user(&status, buf, 1); /* 调用注册的LED控制函数 */ p_led_opr->ctl(minor, status); return 1; // 返回已处理的字节数 } /** * @brief open操作:初始化LED * @param node inode结构体 * @param file 文件结构体 * @return 成功返回0 */ static int led_drv_open(struct inode *node, struct file *file) { int minor = iminor(node); // 获取次设备号 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); /* 调用注册的LED初始化函数 */ p_led_opr->init(minor); return 0; } /* release操作(未实现实际功能) */ static int led_drv_close(struct inode *node, struct file *file) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } /* 文件操作结构体定义 */ static struct file_operations led_drv = { .owner = THIS_MODULE, // 模块所有者 .open = led_drv_open, .read = led_drv_read, .write = led_drv_write, .release = led_drv_close, }; /**************************** 模块初始化/退出 ****************************/ /** * @brief 模块初始化函数 * @return 成功返回0,失败返回错误码 */ static int __init led_init(void) { int err; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); /* 1. 注册字符设备(动态分配主设备号) */ major = register_chrdev(0, "100ask_led", &led_drv); /* 2. 创建设备类 */ led_class = class_create(THIS_MODULE, "100ask_led_class"); if (IS_ERR(led_class)) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); unregister_chrdev(major, "led"); return -1; } return 0; } /** * @brief 模块退出函数 */ static void __exit led_exit(void) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); /* 1. 销毁设备类 */ class_destroy(led_class); /* 2. 注销字符设备 */ unregister_chrdev(major, "100ask_led"); } /* 指定模块的初始化和退出函数 */ module_init(led_init); module_exit(led_exit); /* 模块许可证声明(必需) */ MODULE_LICENSE("GPL");
ledtest.c:
正常的测试函数,跟之前的都一样
#include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> /* * ./ledtest /dev/100ask_led0 on * ./ledtest /dev/100ask_led0 off */ int main(int argc, char **argv) { int fd; char status; /* 1. 判断参数 */ if (argc != 3) { printf("Usage: %s <dev> <on | off>\n", argv[0]); return -1; } /* 2. 打开文件 */ fd = open(argv[1], O_RDWR); if (fd == -1) { printf("can not open file %s\n", argv[1]); return -1; } /* 3. 写文件 */ if (0 == strcmp(argv[2], "on")) { status = 1; write(fd, &status, 1); } else { status = 0; write(fd, &status, 1); } close(fd); return 0; }
Makefile:
跟以往不同,这里需要把不同的驱动.c文件各自编译成单独的ko文件
1.必须拆分为独立
.ko
的情况:
设备信息来自硬件(如DTB或ACPI)
同一总线支持多种设备(如USB键盘/鼠标共用一个USB总线驱动)
2.可合并的情况一个.ko的情况:
纯软件模拟的总线(如虚拟平台设备)
设备固定且无需动态匹配
KERN_DIR = all: make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o ledtest ledtest.c clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order rm -f ledtest # 参考内核源码drivers/char/ipmi/Makefile # 要想把a.c, b.c编译成ab.ko, 可以这样指定: # ab-y := a.o b.o # obj-m += ab.o obj-m += leddrv.o chip_demo_gpio.o board_A_led.o