1 驱动的分层思想

1.1 主机驱动与外设驱动分离

Linux中的SPI、I2C、USB等子系统都利用了典型的把主机驱动和外设驱动分离的想法, 让主机端只负责产生总线上的传输波形, 而外设端只是通过标准的API来让主机端以适当的波形访问自身,涉及了4个软件模块:

  • 主机端驱动
  • 连接主机和外设的纽带
  • 外设端驱动
  • 板级逻辑

分离后的驱动框架如下所示:
image.png
在实际的驱动开发中,一般 I2C 主机控制器驱动已经由半导体厂家编写好了,而设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可,比如 I2C 设备的话提供设备连接到了哪个 I2C 接口上, I2C 的速度是多少等等。
相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型 。总线就负责设备和驱动之间的匹配和联系。

1.2 驱动核心层

核心层肩负的3大职责:

  1. 对上提供接口:file_operations的读、 写、 ioctl都被中间层搞定, 各种I/O模型也被处理掉了。
  2. 中间层实现通用逻辑:可以被底层各种实例共享的代码都被中间层搞定, 避免底层重复实现。
  3. 对下定义框架:底层的驱动不再需要关心Linux内核VFS的接口和各种可能的I/O模型, 而只需处理与具体硬件相关的访问。

这种分层有时候还不是两层, 可以有更多层, 在软件上呈现为面向对象里类继承和多态的状态。

2 platform总线架构

像上面分层思想讲的,在Linux中,设备和驱动是分离的,通过总线将设备和驱动绑定。

  • 在注册设备到系统时,会在左侧会寻找与之匹配的驱动;
  • 在注册驱动到设备时,也会在右侧寻找匹配的设备;

这个过程是由总线完成的由于上面这个优势,一个驱动可以供同类的几个设备使用
image.png

2.1 platform总线

Linux发明了一种虚拟的总线,叫platform总线,对应的结构如下:
image.png
系统为platfrom总线定义了一个bus_type结构体的实例,用于定义platform设备和驱动的匹配关系(不需要我们编写)

  1. //platform平台总线
  2. struct bus_type platform_bus_type = {
  3. .name = "platform",
  4. .dev_groups = platform_dev_groups,
  5. .match = platform_match, //匹配关系查找函数
  6. .uevent = platform_uevent,
  7. .dma_configure = platform_dma_configure,
  8. .pm = &platform_dev_pm_ops,
  9. };
  10. //匹配关系有4种可能性, 所以依次检查
  11. //1.基于设备树风格的匹配
  12. //2.基于ACPI风格的匹配
  13. //3.匹配ID table
  14. //4.匹配设备名和驱动名,用的最多
  15. static int platform_match(struct device *dev, struct device_driver *drv)
  16. {
  17. struct platform_device *pdev = to_platform_device(dev);
  18. struct platform_driver *pdrv = to_platform_driver(drv);
  19. /* When driver_override is set, only bind to the matching driver */
  20. if (pdev->driver_override)
  21. return !strcmp(pdev->driver_override, drv->name);
  22. /* Attempt an OF style match first */
  23. if (of_driver_match_device(dev, drv))
  24. return 1;
  25. /* Then try ACPI style match */
  26. if (acpi_driver_match_device(dev, drv))
  27. return 1;
  28. /* Then try to match against the id table */
  29. if (pdrv->id_table)
  30. return platform_match_id(pdrv->id_table, pdev) != NULL;
  31. /* fall-back to driver name match */
  32. return (strcmp(pdev->name, drv->name) == 0);
  33. }

2.2 platform驱动

platform_driver结构体用于表示platform驱动:

  1. //include/linux/platform_device.h
  2. struct platform_driver {
  3. int (*probe)(struct platform_device *);//设备和驱动匹配成功时自动执行
  4. int (*remove)(struct platform_device *);
  5. void (*shutdown)(struct platform_device *);
  6. int (*suspend)(struct platform_device *, pm_message_t state);
  7. int (*resume)(struct platform_device *);
  8. struct device_driver driver; //相当于基类,提供最基础的驱动框架
  9. const struct platform_device_id *id_table; //匹配方式的一种,id表
  10. bool prevent_deferred_probe;
  11. };
  12. //include/linux/device.h
  13. struct device_driver {
  14. const char *name;
  15. struct bus_type *bus;//指定platform总线实例,用里面的match函数
  16. //...
  17. const struct of_device_id *of_match_table;//匹配方式的一种,设备树
  18. const struct acpi_device_id *acpi_match_table;//匹配方式的一种,acpi
  19. //...
  20. };

在编写 platform 驱动的时候:

  1. 首先定义一个 platform_driver 结构体变量,然后实现结构体中的各个成员变量,重点是实现匹配方法以及 probe 函数。当驱动和设备匹配成功以后 probe函数就会执行,具体的驱动程序在probe函数里面编写,比如字符设备驱动等等。
  2. 当我们定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用platform_driver_register 函数向 Linux 内核注册一个platform驱动。
  3. 在驱动卸载函数中通过 platform_driver_unregister 函数卸载 platform 驱动 ```c int platform_driver_register (struct platform_driver driver);//成功返回0 void platform_driver_unregister(struct platform_driver );

//module_platform_driver宏可以同时完成2、3步操作

define module_platform_driver(__platform_driver) \

  1. module_driver(__platform_driver, platform_driver_register, \
  2. platform_driver_unregister)
  1. <a name="FiCHo"></a>
  2. ## 2.3 platform设备
  3. `platform_device`这个结构体表示 platform 设备。
  4. > 这里我们要注意,如果内核支持设备树的话就不要再使用 platform_device 来描述设备了,因为改用设备树去描述了
  5. ```c
  6. //include/linux/platform_device.h
  7. struct platform_device {
  8. const char *name;
  9. int id;
  10. bool id_auto;
  11. struct device dev;
  12. u64 platform_dma_mask;
  13. struct device_dma_parameters dma_parms;
  14. u32 num_resources;//资源数量
  15. struct resource *resource;//资源,设备信息(内存地址,类型等)
  16. const struct platform_device_id *id_entry;
  17. char *driver_override; /* Driver name to force a match */
  18. /* MFD cell pointer */
  19. struct mfd_cell *mfd_cell;
  20. /* arch specific additions */
  21. struct pdev_archdata archdata;
  22. };

同样的,platform设备也需要注册和注销函数:

  1. int platform_device_register(struct platform_device *);
  2. void platform_device_unregister(struct platform_device *);

如果使用了设备树,Linux 内核启动的时候会从设备树中读取设备信息,然后将其组织成 platform_device 形式,不再需要手动注册注销设备了

2.4 工作原理

platform总线的工作原理如下:
image.png
image.png

3 驱动中引入platform的好处

  1. 使得设备被挂接在一个总线上,符合Linux 2.6以后内核的设备模型。其结果是使配套的sysfs节点、设备电源管理都成为可能
  2. 隔离BSP和驱动。在BSP中定义platform设备和设备使用的资源、设备的具体配置信息,而在驱动中,只需要通过通用API去获取资源和数据,做到了板相关代码和驱动代码分离,使得驱动具有更好的可扩展性和跨平台性
  3. 让一个驱动支持多个设备实例。譬如DM9000的驱动只有一份,但是我们可以在板级添加多份DM9000的platform_device,它们都可以与唯一的驱动匹配

    4 设备树+platform驱动的点灯示例

    单纯的设备树点灯示例参考:13 设备树
    本示例将基于设备树自动添加platfrom device,然后编写platform driver:

  4. 设备树文件和上面链接一致,不做修改

  5. 驱动程序修改如下: ```c

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

define DTSLED_CNT 1 / 设备号个数 /

define DTSLED_NAME “dtsplatled” / 名字 /

define LEDOFF 0 / 关灯 /

define LEDON 1 / 开灯 /

/ 映射后的寄存器虚拟地址指针 / static void iomem *IMX6U_CCM_CCGR1; static void iomem SW_MUX_GPIO1_IO03; static void __iomem SW_PAD_GPIO1_IO03; static void iomem *GPIO1_DR; static void iomem *GPIO1_GDIR;

/ dtsled设备结构体 / struct dtsled_dev{ dev_t devid; / 设备号 / struct cdev cdev; / cdev / struct class class; // struct device device; / 设备 / int major; / 主设备号 / int minor; / 次设备号 / struct device_node nd; / 设备节点 */ };

struct dtsled_dev dtsled; / led设备 /

/*

  • @description : LED打开/关闭
  • @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
  • @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) {
    1. val = readl(GPIO1_DR);
    2. val &= ~(1 << 3);
    3. writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
    1. val = readl(GPIO1_DR);
    2. val|= (1 << 3);
    3. writel(val, GPIO1_DR);
    }
    }

/*

  • @description : 打开设备
  • @param - inode : 传递给驱动的inode
  • @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
  • 一般在open的时候将private_data指向设备结构体。
  • @return : 0 成功;其他 失败 / static int led_open(struct inode inode, struct file filp) { filp->private_data = &dtsled; / 设置私有数据 */ return 0; }

/*

  • @description : 从设备读取数据
  • @param - filp : 要打开的设备文件(文件描述符)
  • @param - buf : 返回给用户空间的数据缓冲区
  • @param - cnt : 要读取的数据长度
  • @param - offt : 相对于文件首地址的偏移
  • @return : 读取的字节数,如果为负值,表示读取失败 / static ssize_t led_read(struct file filp, char __user buf, size_t cnt, loff_t offt) { return 0; }

/*

  • @description : 向设备写数据
  • @param - filp : 设备文件,表示打开的文件描述符
  • @param - buf : 要写给设备写入的数据
  • @param - cnt : 要写入的数据长度
  • @param - offt : 相对于文件首地址的偏移
  • @return : 写入的字节数,如果为负值,表示写入失败 / static ssize_t led_write(struct file filp, const char __user buf, size_t cnt, loff_t offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt); if(retvalue < 0) {

    1. printk("kernel write failed!\r\n");
    2. return -EFAULT;

    }

    ledstat = databuf[0]; / 获取状态值 /

    if(ledstat == LEDON) {

    1. led_switch(LEDON); /* 打开LED灯 */

    } else if(ledstat == LEDOFF) {

    1. led_switch(LEDOFF); /* 关闭LED灯 */

    } return 0; }

/*

  • @description : 关闭/释放设备
  • @param - filp : 要关闭的设备文件(文件描述符)
  • @return : 0 成功;其他 失败 / static int led_release(struct inode inode, struct file *filp) { return 0; }

/ 设备操作函数 / static struct file_operations dtsled_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, };

/*

  • @description : flatform 驱动的 probe 函数,当驱动与设备匹配以后此函数就会执行
  • @param - dev : platform 设备
  • @return : 0,成功;其他负值,失败 / static int led_probe(struct platform_device dev) { / 初始化LED / IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);

    1. SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);

    GPIO1_DR = of_iomap(dtsled.nd, 3); GPIO1_GDIR = of_iomap(dtsled.nd, 4);

    / 2、使能GPIO1时钟 / val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); / 清楚以前的设置 / val |= (3 << 26); / 设置新值 / writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置GPIO1_IO03的复用功能,将其复用为

    • GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03);

      /寄存器SW_PAD_GPIO1_IO03设置IO属性 bit 16:0 HYS关闭 bit [15:14]: 00 默认下拉 bit [13]: 0 kepper功能 bit [12]: 1 pull/keeper使能 bit [11]: 0 关闭开路输出 bit [7:6]: 10 速度100Mhz bit [5:3]: 110 R0/6驱动能力 bit [0]: 0 低转换率 / writel(0x10B0, SW_PAD_GPIO1_IO03);

      / 4、设置GPIO1_IO03为输出功能 / val = readl(GPIO1_GDIR); val &= ~(1 << 3); / 清除以前的设置 / val |= (1 << 3); / 设置为输出 / writel(val, GPIO1_GDIR);

      / 5、默认关闭LED / val = readl(GPIO1_DR); val |= (1 << 3);
      writel(val, GPIO1_DR);

      / 注册字符设备驱动 / / 1、创建设备号 / if (dtsled.major) { / 定义了设备号 / dtsled.devid = MKDEV(dtsled.major, 0); register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME); } else { / 没有定义设备号 / alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); / 申请设备号 / dtsled.major = MAJOR(dtsled.devid); / 获取分配号的主设备号 / dtsled.minor = MINOR(dtsled.devid); / 获取分配号的次设备号 / } printk(“dtsled major=%d,minor=%d\r\n”,dtsled.major, dtsled.minor);

      / 2、初始化cdev / dtsled.cdev.owner = THIS_MODULE; cdev_init(&dtsled.cdev, &dtsled_fops);

      / 3、添加一个cdev / cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

      / 4、创建类 / dtsled.class = class_create(THIS_MODULE, DTSLED_NAME); if (IS_ERR(dtsled.class)) { return PTR_ERR(dtsled.class); }

      / 5、创建设备 / dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME); if (IS_ERR(dtsled.device)) { return PTR_ERR(dtsled.device); }

      return 0; }

/*

  • @description : remove 函数,移除 platform 驱动的时候此函数会执行
  • @param - dev : platform 设备
  • @return : 0,成功;其他负值,失败 / static int led_remove(struct platform_device dev) { / 取消映射 / iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR);

    / 注销字符设备驱动 / cdev_del(&dtsled.cdev);/ 删除cdev / unregister_chrdev_region(dtsled.devid, DTSLED_CNT); / 注销设备号 /

    device_destroy(dtsled.class, dtsled.devid); class_destroy(dtsled.class); }

/ 匹配列表 / static const struct of_device_id led_of_match[] = { { .compatible = “atkalpha-led” }, { / Sentinel / } };

/ platform驱动结构体 / static struct platform_driver led_driver = { .driver = { .name = “barretled”, / 驱动名字,用于和设备匹配 / .of_match_table = led_of_match, / 设备树匹配表 / }, .probe = led_probe, //指定驱动初始化函数 .remove = led_remove,//指定驱动卸载函数 };

/*

  • @description : 驱动模块加载函数
  • @param : 无
  • @return : 无 */ static int __init leddriver_init(void) { return platform_driver_register(&led_driver); }

/*

  • @description : 驱动模块卸载函数
  • @param : 无
  • @return : 无 */ static void __exit leddriver_exit(void) { platform_driver_unregister(&led_driver); }

module_init(leddriver_init); module_exit(leddriver_exit); MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“barretren”); ```