第九章 理一理驱动程序的编写

    很想把这些天跟着老师学习的内容再重新整理出来,并不按照讲课的顺序,而是按照逐次使用的顺序写出来,如上图!在此期间问了自己14个问题:

1什么是pinctrl子系统?

2什么是gpio子系统?

3gpio和pinctrl子系统怎么编写?

4gpio和pinctrl子系统怎么转化到内核中?

5platform_driver_register是怎么进行驱动注册的?

6platform_driver如何匹配到platform_device?

7_probe函数的形参是谁给的?

8gpiod_get函数是怎么找到设备树中的led节点?

9register_chrdev函数是怎么把100ask_led注册到内核中的?

10为什么要创建设备类class?class_create是怎么在内核中创建的class?

11用register_chrdev要创建主设备,device_create创建次设备,怎么创建的?

12gpiod_direction_output是怎么实现gpio初始化的?

13gpiod_set_value是怎么实现设置的gpio的?

14假如有2个led1/2呢?怎么去区分led0和led1的?

    问题看着还蛮多的,其实在前面的blog中都有描写,但是感觉不满意,逻辑不够清晰,知识点太肤浅,渴望可以再深挖一点,也是受到别人写出blog的感染,想写出自己满意的blog。


9.1.驱动程序进化之路

    总结起来老师用了3种方法来写led驱动,传统法、基于platform法和设备树法,下图是进化之路,它们的核心都是分配、设置、注册 file_operations结构体,以及对应的open、read、write和close函数,根据方法的不同函数的内容也有所不同,主要是open和write的不同。

    3种方法的主要区别是硬件资源的获取和配置,小结如下表,但是感觉太简单了。

     方法

        共同点

                              不同点

              驱动层

       硬件层

传统法

分配、设置、注册 file_operations结构体,编写对应的open、read、write和close函数。

         不分离,即有驱动层又有硬件操作

Platform法

         platform_driver

platform_device

设备树法

         platform_driver

         .dts

1.1传统方法

    传统方法,简单粗暴,不分层级结构,直接在驱动程序中的init和ctl函数中操作寄存器,缺点也很明显,更换硬件类型,需重新编写,例如同样是使能某个gpio的时钟,imx6ull和rk3399是不一样的。

/* imx6ull:enable GPIO5 
* CG15, b[31:30] = 0b11
*/
    *CCM_CCGR1 |= (3<<30);
*******************************************************
/* rk3399 GPIO2_D3 */
/* a. 使能GPIO2
* set CRU to enable GPIO2
* CRU_CLKGATE_CON31 0xFF760000 + 0x037c
* (1<<(3+16)) | (0<<3)
*/
    *CRU_CLKGATE_CON31 = (1<<(3+16)) | (0<<3);

    看下传统方法的主要架构:

    为此大牛们想出了分层和分离的思想,具体是哪些大牛就不知道了,分层的意思是将驱动程序分成2个层级,上层用来完成注册file_operations等工作,不涉及硬件操作;下层实现硬件操作,不涉及内核注册等工作。分离呢,就将下层根据每种单板实现分别控制(即多个.c文件)。另外,上下层之间需要一个中间文件来连接,承上启下,chip_demo_gpio.c,向上接收驱动函数的调用指令,向下操作硬件。多个单板,用一个连接文件调用,就必须统一接口,也就用到了面向对象的思想,特意去搜了下面向对象的定义,引用:

    “面向过程——步骤化,面向过程就是分析出实现需求所需要的步骤,通过函数一步一步实现这些步骤,接着依次调用即可。

    面向对象——行为化,面向对象是把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象,创建了对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为”

    有点抽象,总之就是把各个单板的和led相关的gpio资源抽象成一个led_resource结构体,例如:

static struct led_resource board_A_led = {
	.pin = GROUP_PIN(3,1),
};

    然后在chip_demo_gpio.c中获取这个资源。

    好像也没省多少代码,只是把各个单板独立开来。

1.2 platform法

    这里参考了正点原子的《I.MX6U嵌入式Linux驱动开发指南V1.2》第54章内容,讲到多个SOC平台都有MPU6050 这个 I2C 接口的六轴传感器,那就可以只写一个驱动程序,多个SOC平台可以共同使用这个驱动程序,如下图,这样就大大简化了内核的代码量。

    像I2C、SPI和USB等都有自己的总线,有些外设是没有总线,led应该没有吧,但还是想用这套模型该怎么办?Linux提出了platform这个虚拟总线,对应的设备为platform_device,对应的驱动为platform_driver,共同构成Linux 总线、驱动和设备模式,如下图:

    “当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。”

    回到led驱动程序,前面一小节的哪些部分是platform_device?哪些是platform_driver?led的操作应该是driver,led的相关gpio资源应该是device,看下代码。

/* 定义led引脚资源 */
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",
        },
};
/* 定义名为100ask_led的platform_device */
static struct platform_device board_A_led_dev = {
        .name = "100ask_led",
        .num_resources = ARRAY_SIZE(resources),
        .resource = resources,
        .dev = {
                .release = led_dev_release,
         },
};

    代码中定义了platform_device结构体board_A_led_dev,其中有name、num_resources、resource和dev.release成员。再看下platform_driver的代码,定义了platform_driver结构体chip_demo_gpio_driver,成员如下,这里通过“100ask_led”这个name字符串来匹配device和driver,具体的匹配过程以及driver中probe()的执行过程后续再说。

/* 定义名为100ask_led的platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "100ask_led",
    },
};

各个.c和.h之间的关系如下图,图片压缩之后有点看不清了。

    其实发现好像没有简化多少代码,还是蛮复杂的,设备越多,驱动越多,.c和.h就越多,Linux 之父 linus不愿意了,Linux开始引入设备树!

1.3设备树法

    还是参考了正点原子的开发指南,第43章,设备树,Device Tree,描述文件,DTS,Device Tree Source,示意图:

    DTS 是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。

    还是回到led驱动,也就是将各个单板的gpio资源写到.dts文件中,而不是用.c和.h,看下100ask_led.dts:

#define GROUP_PIN(g,p) ((g<<16) | (p))

/ {
	100ask_led@0 {
		compatible = "100as,leddrv";
		pin = <GROUP_PIN(3, 1)>;
	};

	100ask_led@1 {
		compatible = "100as,leddrv";
		pin = <GROUP_PIN(5, 8)>;
	};
};

    使用设备树的整个文件框架,如下图,关于driver是怎么来调用的设备树信息,后面再说。

    有种大功告成的感觉,但是再去一看发现init()和ctl()还是存在啊,还是得去配置寄存器操作led啊,和传统方法没啥区别啊,折腾半天就是把led相关的gpio资源抽象出来由.c定义变成了设备树定义。有什么办法可以不去操作寄存器?有的,Linux的pinctrl和gpio子系统。

    还是参考正点原子的开发指南,比较详细,韦老师的讲课文档属于浓缩,总结下,pinctrl子系统主要用来设置pin的复用功能以及电气特性,gpio子系统为pinctrl和driver提供“交流”的接口函数。驱动程序中不用专门去操作寄存器了,大大简化了代码,简化的框架如下图:

    到这里的时候自己问了自己14个问题,趁着清明假期把韦老师18年将的设备树专题视频过来一遍,很多东西又清晰了,还是忍不住想写写,不想放弃,但是会花很多时间。

    下面就针对前面提出的问题写出自己的理解吧。

9.2.什么是pinctrl子系统?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值