1.总线设备驱动模型
设备定义资源,platform_device结构体
驱动定义platform_driver结构体,实现probe, file_operations
总线驱动模型优点:
驱动只是一套控制驱动框架,基本不用修改,和单板硬件相关的都在设备代码里面,硬件修改只需要修改设备资源相关的代码,不用关心具体的函数实现和寄存器控制。
1.1总线/平台设备/平台驱动结构体
1.2 platform_device的注册过程
(1)系统初始化时,调用platform_add_devices函数,把所有放置在板级platform_device数组中的platform_device注册到系统中去。
- 此函数循环调用platform_device_register函数,来注册每个platform_device。
- 而platform_device_register中会调用platform_device_add函数。
(2)platform_device全部注册到系统之后,便可以通过platform的操作接口,来获取platform_device中的resource资源。
- 比如地址、中断号等,以进行request_memregion、ioremap(将resource分配的物理地址映射到kernel的虚拟空间来)和request_irq操作。
- platform的操作接口包括platform_get_irq、platform_get_irq_byname、platform_get_resource、platform_get_resource_byname等。
1.3 platform_driver的注册过程
(1)kernel中的调用关系
当insmod设备驱动的时候会透过module_init调用, 过程如下:
module_init-》platform_driver_register()-》driver_register()-》driver_attach()—》bus_for_each_dev()函数对每个挂在虚拟的platform bus的设备作__driver_attach()—》driver_probe_device()—》drv->bus—》match()==platform_match(); 比较strncmp(pdev->name, drv->name, BUS_ID_SIZE),如果相符就调用platform_drv_probe()->driver->probe(),如果probe成功则绑定该设备到该驱动。
当match成功后,就会调用platform driver的probe函数。
(2)probe函数中:
- 驱动代码可以通过platform_get_resource_byname等函数来获取对应platform_device的resource资源,配置IO资源,配置IO内存,配置gpio等等。
- probe函数可以通过get_resource来获取寄存器物理基地址,然后ioremap到kernel的虚拟空间来,这样驱动就可以正式操纵和修改设备的寄存器。
- 进行cdev的初始化及cdev_add的操作。
大致总结过程如下:
1.4 driver与device的匹配
一个驱动是可以匹配多个设备的,平台总线中的驱动要具有三种匹配信息的能力,基于这种需求,platform_driver中使用不同的成员来进行相应的匹配。系统为platform总线定义了一个bus_type 的实例platform_bus_type, 会不断循环呼叫platform_match函数去遍历所有设备和驱动:
① of_match_table
Of_match_table就是从dts中对应node的compatible属性去匹配设备和驱动。
compatible属性也叫做“兼容性”属性,是一个字符串列表, 格式如下所示
“manufacturer,model”
manufacturrer表示厂商,model表示驱动名字,该属性用于将设备和驱动绑定起来。
platform_device.dev.of_node 和 platform_driver.driver.of_match_table。由设备树节点转换得来的 platform_device 中,含有一个结构体:of_node。 它的类型如下:
platform_driver.driver.of_match_table 是一个数组,类型如下:
一般驱动程序都会有一个of_match_table匹配表,此of_match_table匹配表保存着一些compatible值,如果dts中的compatible属性值和of_match_table匹配表中的有一个值相等,那么就表示设备可以使用这个驱动。
如下图dts定义了一个cif节点。
驱动程序中的定义如下:
那么这里驱动程序中的.of_match_table和dts能够匹配,那么就说明match成功,匹配成功后调用platform driver的probe函数。一般在驱动程序module int的时候,也就是insmod的时候,会用platform_driver_register来进行match过程。
② ID table
下面这个例子就是用一个驱动来匹配两个分别叫"demo0"和"demo1"的设备,注意,数组最后的{}是一定要的,这个是内核判断数组已经结束的标志。
static struct platform_device_id tbl[] = { {"demo0"}, {"demo1"}, {}, };
MODULE_DEVICE_TABLE(platform, tbl);
用MODULE_DEVICE_TABLE来声明id table这种匹配方式用的相对较少。
③ name
如果platform_driver和C语言编码的platform_device是一一匹配的,我们还可以使用device_driver中的.name来进行匹配。
匹配小结
细心的读者可能会发现,这么多方式都写在一个对象中,那如果我同时注册了三种匹配结构内核该用哪种呢?此时就需要我们搬出平台总线的匹配方式:
//drivers/base/platform.c 748 static int platform_match(struct device *dev, struct device_driver *drv) 749 { 750 struct platform_device *pdev = to_platform_device(dev); 751 struct platform_driver *pdrv = to_platform_driver(drv); 752 753 /* Attempt an OF style match first */ 754 if (of_driver_match_device(dev, drv)) 755 return 1; 756 757 /* Then try ACPI style match */ 758 if (acpi_driver_match_device(dev, drv)) 759 return 1; 760 761 /* Then try to match against the id table */ 762 if (pdrv->id_table) 763 return platform_match_id(pdrv->id_table, pdev) != NULL; 764 765 /* fall-back to driver name match */ 766 return (strcmp(pdev->name, drv->name) == 0); 767 }
从中不难看出,这几中形式的匹配是有优先级的:of_match_table>id_table>name
1.5 注册/反注册
platform_device_register/ platform_device_unregister
platform_driver_register/ platform_driver_unregister
platform_add_devices // 注册多个 device
1.6 Platform_device资源获取函数
1.6.1 IO resource
struct resource *platform_get_resource(struct platform_device *, unsigned int, unsigned int);
kernel\include\linux\ioport.h中有resource结构。用来描述hw设备的资源信息。
flags一般有以下几种:比如中断资源, IO端口资源, IO内存资源, DMA资源
- IORESOURCE_IO:表示IO资源,cpu需要用特殊指令才能访问或需要用特殊访问方式才能访问,不能直接用指针来寻址
- IORESOURCE_MEM:表示IO内存,可以直接用指针引用来直接寻址操作
这里举个例子
1.dts中的节点:
2.resource init code:
打印如下:那这里的pdev对应dts中的mipi_rx节点。platform_get_resource可以从dts node中找到io内存资源。
reg // 用来指定IO内存的地址、大小.
那这里循环获取4次,地址范围和上面的dts节点一致。
1.6.2 IRQ
- IORESOURCE_IRQ: 中断irq资源
#define IRQ_TYPE_EDGE_RISING 1 上升沿触发 #define IRQ_TYPE_EDGE_FALLING 2 下降沿触发 #define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING) 双边沿触发 #define IRQ_TYPE_LEVEL_HIGH 4 电平触发-高电平
Dts之GIC:(Generic Interrupt Controller)是ARM公司提供的一个通用的中断控制器。
GIC3要素:
- 中断类型
- 中断号
- 中断触发方式这三个要素
GIC的外设中断(除去SGI)类型有两类:
- SPI,共享外设中断(由GIC内部的distributor来分发到相关CPU),中断号:32~1019
- PPI,私有外设中断(指定CPU接收),中断号:16~31
外设中断号的分配规则如下:
- 32~1019给SPI
- 16~31给PPI
所有外设中断都支持四种触发方式:
- 上升沿触发
- 下降沿触发
- 高电平触发
- 低电平触发
所以DTS中接在GIC的device node的interrupts属性也是用这三个要素来描述一个具体的中断。
格式如:interrupts = <interruptType interruptNumber triggerType>
Interrrupt Types |
Interrrupt Number |
Trigger Type |
0 = SPI 1 = PPI |
32 … … 1019 |
1 = low to high 2 = high to low 4 = high level 8 = low level |
Sample code如下:
打印结果如下:
那么最后dts解析的结果为:
out_irq->np = interrupt-parent = gic node
out_irq->args[0] = GIC_SPI;
out_irq->args[1] = 硬件中断号 = 155
out_irq->args[2] = 中断触发类型 = IRQ_TYPE_LEVEL_HIGH
out_irq->np = interrupt-parent = gic node
out_irq->args[0] = GIC_SPI;
out_irq->args[1] = 硬件中断号 = 156
out_irq->args[2] = 中断触发类型 = IRQ_TYPE_LEVEL_HIGH
platform_get_irq返回一个虚拟中断号,这里对应的是27, 28。
devm_request_irq用来申请中断,分配isr中断处理函数。该函数可以在驱动卸载时不用主动调用free_irq显示释放中断请求。
可以看到两次call devm_request_irq却是用的同一个中断服务程序cif_isr,这也是允许的,我们看一下函数原型:
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) typedef irqreturn_t (*irq_handler_t)(int, void *);
devm_request_irq会建立中断号irq_num和中断服务程序isr的绑定,最后一个参数会传给中断服务程序isr.
中断服务程序isr能够根据中断号irq_num和传进的参数进行区分中断源。
1.6.3 GPIO
of_get_named_gpio_flags获取dts中gpio 编号,并且会找到device_node,找到of_gpio_flags
gpio_request申请gpio
gpio_direction_output设置成output且set gpio val
#define GPIO_ACTIVE_HIGH 0 #define GPIO_ACTIVE_LOW 1
这里的gpio 编号=411 = GPIO_D + offset = 404 + 7 =411(也就是dts中配置的portd 7),这里由于是of_gpio_flags 是OF_GPIO_ACTIVE_LOW =0x01,所以snsr_rst_pol = 1.
2.示例代码和分析
①分配/设置/注册 platform_device 结构体 在里面定义所用资源,指定设备名字。
②分配/设置/注册 platform_driver 结构体 在其中的 probe 函数里,分配/设置/注册 file_operations 结构体, 并从 platform_device 中确实所用硬件资源。 指定 platform_driver 的名字。
#ifndef _LED_OPR_H #define _LED_OPR_H struct led_operations { int (*init) (int which); /* 初始化LED, which-哪个LED */ int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */ }; struct led_operations *get_board_led_opr(void); #endif
2.1 通用字符驱动框架
#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" /* 1. 确定主设备号 */ static int major = 0; static struct class *led_class; struct led_operations *p_led_opr; #define MIN(a, b) (a < b ? a : b) void led_class_create_device(int minor) { device_create(led_class, NULL, MKDEV(major, minor), NULL, "100ask_led%d", minor); /* /dev/100ask_led0,1,... */ } void led_class_destroy_device(int minor) { device_destroy(led_class, MKDEV(major, minor)); } 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); /* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */ 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; } /* write(fd, &val, 1); */ static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { int err; char status; 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); /* 根据次设备号和status控制LED */ p_led_opr->ctl(minor, status); return 1; } 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; } static int led_drv_close (struct inode *node, struct file *file) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } /* 2. 定义自己的file_operations结构体 */ 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, }; /* 4. 把file_operations结构体告诉内核:注册驱动程序 */ /* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */ static int __init led_init(void) { int err; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); major = register_chrdev(0, "100ask_led", &led_drv); /* /dev/led */ led_class = class_create(THIS_MODULE, "100ask_led_class"); err = PTR_ERR(led_class); if (IS_ERR(led_class)) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); unregister_chrdev(major, "led"); return -1; } return 0; } /* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */ static void __exit led_exit(void) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); class_destroy(led_class); unregister_chrdev(major, "100ask_led"); } /* 7. 其他完善:提供设备信息,自动创建设备节点 */ module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
实现一个led字符设备通用型驱动框架,实现open/read/write/ioctl,调用具体的led驱动。先注册字符设备驱动,确定好class和主设备号。
因为暂时还不知道具体led驱动是啥,因此需要外部去注册具体的led驱动,交给platform_driver去建立。
暂时先不建立设备节点,设备节点交给platform_device去建立,因为暂时不知道设备的led资源信息。
所以先EXPORT_SYMBOL导出led_class_create_device/led_class_destroy_device/register_led_operations,当用户调用open函数时,调用具体的led初始化,进行pinmux,gpio配置。当用户调用write函数,调用具体的led操作函数。
2.2 具体单板资源描述驱动(platform_device)
#ifndef _LED_RESOURCE_H #define _LED_RESOURCE_H /* GPIO3_0 */ /* bit[31:16] = group */ /* bit[15:0] = which pin */ #define GROUP(x) (x>>16) #define PIN(x) (x&0xFFFF) #define GROUP_PIN(g,p) ((g<<16) | (p)) #endif
Board_A_led.c这里实现了单板的资源定义,这里是gpio3_1,gpio5_8。
#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 <linux/platform_device.h> #include "led_resource.h" static void led_dev_release(struct device *dev) { } static struct resource resources[] = { { .start = GROUP_PIN(3,1), .flags = IORESOURCE_IRQ, .name = "100ask_led_pin", }, { .start = GROUP_PIN(5,8), .flags = IORESOURCE_IRQ, .name = "100ask_led_pin", }, }; static struct platform_device board_A_led_dev = { .name = "100ask_led", .num_resources = ARRAY_SIZE(resources), .resource = resources, .dev = { .release = led_dev_release, }, }; static int __init led_dev_init(void) { int err; err = platform_device_register(&board_A_led_dev); return 0; } static void __exit led_dev_exit(void) { platform_device_unregister(&board_A_led_dev); } module_init(led_dev_init); module_exit(led_dev_exit); MODULE_LICENSE("GPL");
注意:
如果 platform_device 中不提供 release 函数,如下图所示不提供红框部分的函数
则在调用 platform_device_unregister 时会出现警告,如下图所示, 因此我们可以将release实现为空。
2.3 具体芯片驱动(platform_driver)
#ifndef _LED_OPR_H #define _LED_OPR_H struct led_operations { int (*init) (int which); /* 初始化LED, which-哪个LED */ int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */ }; struct led_operations *get_board_led_opr(void); #endif #ifndef _LEDDRV_H #define _LEDDRV_H #include "led_opr.h" void led_class_create_device(int minor); void led_class_destroy_device(int minor); void register_led_operations(struct led_operations *opr); #endif /* _LEDDRV_H */
Chip_demo_gpio.c
#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 <linux/platform_device.h> #include "led_opr.h" #include "leddrv.h" #include "led_resource.h" static int g_ledpins[100]; static int g_ledcnt = 0; static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */ { //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which); printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which])); 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; } static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */ { //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off"); printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which])); 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; } static struct led_operations board_demo_led_opr = { .init = board_demo_led_init, .ctl = board_demo_led_ctl, }; static int chip_demo_gpio_probe(struct platform_device *pdev) { struct resource *res; int i = 0; 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; } static int chip_demo_gpio_remove(struct platform_device *pdev) { struct resource *res; int i = 0; 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; 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");
当platform_device和platform_driver都注册后,总线设备驱动模型会进行match匹配,匹配成功调用probe函数,init时注册led_operations,得到具体的led操作方法(这里实现暂时为空)。当用户调用open/write时便可操作具体chip的led驱动方法。probe函数里面获取单板定义的资源信息,依次创建设备节点。
Makefile
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR # 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量: # 2.1 ARCH, 比如: export ARCH=arm64 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu- # 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin # 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同, # 请参考各开发板的高级用户使用手册 KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4 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
编译出3个ko,依次insmod leddrv.ko chip_demo_gpio.ko board_A_led.ko
测试:
#include <sys/types.h> #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; }
dts中,通常使用memory-region将设备和reserved memory 关联起,cvifb 通过 memory-region 关联到 fb_reserved 这块 reserved memory 上面.
通过cvifb节点的memory-region属性找到reserved-memory,of_reserved_mem_lookup根据关联的reserved-momory节点找到预留内存地址
IS_ERR()函数
内核中的函数常常返回指针,如果出错,也希望能够通过返回的指针体现出来。那么有三种情况:合法指针,NULL指针和非法指针。
1.合法指针:内核函数返回的指针一般是4K对齐,即 ptr & 0xfff == 0,也就是0x1000的倍数。
2.非法指针:一般内核函数地址不会落在(0xfffff000,0xffffffff)之间,而一般内核的出错代码也是一个小负数,在-4095到0之间,转变成unsigned long,正好在(0xfffff000,0xffffffff)之间。因此可以用 (unsigned long)ptr > (unsigned long)-1000L
-1000L=0xfffff000
3.linux内核中有一个宏MAX_ERRNO = 4095,include/asm-generic/errno-base.h
可以看到如果是合法指针,IS_ERR函数返回0,否则返回非0,PTR_ERR函数是将返回的非法指针转成long型,返回给用户。