linux驱动篇-Platformbus

作者: 84f22431fa2a | 来源:发表于2019-03-17 10:42 被阅读1次

    Platformbus

    前言

    在嵌入式行业,有很多从业者。我们工作的主旋律是拿开源代码,拿厂家代码,完成产品的功能,提升产品的性能,进而解决各种各样的问题。或者是维护一个模块或方向,一搞就是好几年。

    时间长了,笔者发现我们对从零开始编写驱动、应用、算法、系统、协议、文件系统等缺乏经验。没有该有的广度和深度。笔者也是这样,工作了很多年,都是针对某个问题点修修补补或者某个模块的局部删删改改。很少有机会去独自从零开始编写一整套完整的代码。

    当然,这种现状对于企业来说是比较正常的,可以降低风险。但是对于员工本身,如果缺乏必要的规划,很容易工作多年却还是停留在单点的层面,而丧失了提升到较高层面的机会。随着时间的增长很容易丧失竞争力。

    另外,根据笔者的经验,绝大多数公司对于0-5年经验从业者的定位主要是积极的问题解决者。而对于5-10经验从业者的定位主要是积极的系统规划者和引领者。在这种行业规则下,笔者认为,每个从业者都应该问自己一句,“5年后,我是否具备系统化把控软件的能力呢?”。

    当前的这种行业现状,如果我们不做出一点改变,是没有办法突破的。有些东西,仅仅知道是不够的,还需要深思熟虑的思考和必要的训练,简单来说就是要知行合一。

    也许有读者会有疑惑?这不就是重复造轮子么?我们确实是在重复造轮子,因为别人会造轮子那是别人的能力,我们自己会造轮子是我们自己的能力。在行业中,有太多的定制化需求是因为轮子本身有原生性缺陷,我们无法直接使用,或者需要对其进行改进,或者需要抽取开源代码的主体思想和框架,根据公司的需要定制自己的各项功能。设想,如果我们具备这种能力,必然会促使我们在行业中脱颖而出,而不是工作很多年一直在底层搬砖。底层搬砖没什么不好,问题是当有更廉价更激情的劳动力涌进来的时候,我们这些老的搬砖民工也就失去了价值。我们不会天天重复造轮子,我们需要通过造几个轮子使得自己具备造轮子的能力,从而更好的适应这个环境,适应这个世界。

    针对当前行业现状,笔者经过深思熟虑,想为大家做点实实在在的事情,希望能够帮助大家在巩固基础的同时提升系统化把控软件的能力。当然,笔者的水平也有限,有些观点也只是一家之谈,希望大家独立思考,谨慎采用,如果写的有错误或者不对的地方还请读者们批评斧正,我们一起共同进步。

    在这里简单介绍下笔者,笔者现在就职于一家大型国际化公司,工作经验6年,硕士毕业。曾经担任过组内的项目主管,项目经理,也曾经组建过新团队,带领大家冲锋陷阵。在工作中,有做的不错的地方,也有失误的地方,有激情的时刻,也有失落的时刻。现在偏安一隅,专心搞技术,目前个人规划的技术方向是嵌入式和AI基础设施建设,以及嵌入式和AI的融合发展。

    最后,说了这么多,笔者希望,在未来的日子里和未知的领域里,你我同行,为我们的美好生活而努力奋斗。

    总体目标

    本篇文章的目标是介绍如何从自顶向下从零编写linux下的platformbus驱动。着力从总体思路,需求端,分析端,实现端,详尽描述一个完整需求的开发流程,是笔者多年经验的提炼,希望读者能够有所收获。最后的实战目标,请读者尽量完成,这样读者才能形成自己的思路。

    本示例采用arm920架构,天祥电子生产的tx2440a开发板,核心为三星的s3c2440。Linux版本为2.6.31,是已经移植好的版本。编译器为arm920t-eabi-4.1.2.tar。

    总体思路

    总体思路是严格遵循需求的开发流程来,不遗漏任何思考环节。读者在阅读时请先跟笔者的思路走一遍,然后再抛弃笔者的思路,按照自己的思路走一遍,如果遇到困难请先自己思考,实在不会再来参考笔者的思路和实现。

    笔者在写代码的的总体思路如下:
    需求描述—能够详细完整的描述一个需求。
    需求分析—根据需求描述,提取可供实现的功能,需要有定量或者定性的指标。(从宏观上确定需要什么功能)。
    需求分解—根据需求分析,考虑要实现需求所需要做的工作(根据宏观确定的功能,拆分成小的可单独实现的功能)。
    编写思路—根据需求分解从总体上描述应该如何编写代码,(解决怎么在宏观上实现)。
    详细步骤—根据编写思路,落实具体步骤,(解决怎么在微观上实现)。
    编写框架—根据编写思路,实现总体框架(实现编写思路里主体框架,细节内容留在具体代码里编写)。
    具体代码—根据编写框架,编写每一个函数里所需要实现的小功能,主要是实现驱动代码,测试代码
    Makefile—用来编译驱动代码
    目录结构—用来说明当完成编码后的结果
    测试步骤—说明如何对驱动进行测试,主要是加载驱动模块,执行测试代码
    执行结果—观察执行结果是否符合预期
    结果总结—回顾本节的思路,知识点,api,结构体
    实战目标—说明如何根据本文档训练

    需求描述

    使用platformbus提供的接口来点亮和熄灭led灯3,写1时点亮,写0时熄灭。(总共4个led灯,0,1,2,3)

    需求分析

    使用platformbus总线来管理led设备和led驱动,需要具备以下功能:
    1注册和卸载led设备(包括led相关资源和处理函数)
    2注册和卸载led驱动
    3提供可供操作的设备节点
    4当输入为参数为1时点亮led,当输入参数为0时熄灭led灯

    需求分解

    分析完需求后,可以确定编写代码的目标。
    1编写platform_dev.c以注册和卸载led设备
    2编写platform_drv.c以注册和卸载led驱动
    3编写platform_test.c以测试led设备和led驱动是否正常工作

    编写思路

    本示例需要将设备和驱动分开编写,设备文件(platform_dev.c)主要用来提供资源和注册资源;驱动文件(platform_drv.c)主要用来使用资源,控制硬件,向用户返回数据。思路如下:
    1注册和卸载platform_device类型的led设备(在platform_dev.c里)
    1.1拷贝头文件,编写和修饰入口函数和出口函数,声明许可证
    1.2在入口函数中注册platform_device类型的led设备
    1.2.1定义led设备
    1.2.2定义led设备资源
    1.2.3定义led设备需要的release函数
    1.3在出口函数中卸载platform_device类型的led设备

    2注册和卸载platform_driver类型的led驱动(在platform_drv.c里)
    2.1拷贝头文件,编写和修饰入口函数和出口函数,声明许可证
    2.2在入口函数中注册platform_driver类型的led驱动
    2.2.1定义platform_driver类型的led驱动
    2.2.2定义probe函数和remove函数
    2.2.3 编写probe函数
    2.2.3.1获取资源
    2.2.3.2注册字符设备及操作函数
    2.2.3.3提供可操作的设备节点
    2.2.4编写remove函数(卸载字符设备,释放资源)
    2.3在出口函数中卸载platform_driver类型的led驱动

    详细步骤

    根据需求分析的结果来考虑如何实施,主要是考虑在哪一步做什么具体的工作
    1创建两个文件用来生成两个模块,platform_drv.c(存放led操作函数),platform_dev.c(存放led设备及资源);创建Makefile文件来编译模块;创建platform_test.c存放测试代码;创建platform_dev_skeleton.c和platform_drv_skeleton.c只是用来搭建框架和理顺思路。

    2编写platform_dev.c。
    2.1编写代码框架:头文件,入口函数,出口函数,声明LICENSE为GPL
    2.2在入口函数中注册led设备,类型为struct platform_device
    2.2.1定义并led设备,类型为struct platform_device
    2.2.2填充led设备,放入设备名字,设备id,设备资源,资源个数,release函数
    2.2.3定义设备资源数组,类型为struct resource,放入内存资源和中断资源
    2.3在出口函数中卸载platform_device类型的led设备

    3编写platform_drv.c
    3.1编写代码框架:头文件,入口函数,出口函数,声明LICENSE为GPL
    3.2在入口函数中注册platform_driver类型的led驱动结构体
    3.2.1定义struct platform_driver类型的led驱动
    放入名字,probe函数,remove函数等
    3.2.2定义probe函数
    获取内存资源和引脚号资源;映射寄存器
    注册字符设备
    创建设备类;创建设备节点
    3.2.3定义remove函数
    卸载字符设备
    卸载设备节点;销毁类
    释放资源
    3.2.4定义struct file_operations结构体,编写对应的函数
    3.2.5定义open函数
    根据引脚号配置gpio管脚为输出模式
    3.2.6定义write函数
    获取用户空间数据,控制寄存器来控制gpio管脚的低和高
    如果用户写1,点亮led,写0,熄灭led
    3.3在出口函数中卸载platform_driver类型的led驱动结构体

    编写框架

    首先编写总体框架代码,先将总体的结构搭建起来,然后再在框架代码里编写比较具体的代码,platform_dev_skeleton.c和platform_dev_skeleton.c只是用来参考,主要说明一般写代码时是如何先搭建框架再写细节代码的。

    Platform_dev_skeleton.c
    /* 本文件是依照platformbus驱动<编写思路>章节编写,本文件
      * 的目的是编写代码框架,不做具体细节的编写
      */
    /* 本头文件是linux2.6.31内核所提供的,其他版本按需调整 */
    /* 1.1拷贝头文件,编写和修饰入口函数和出口函数,声明许可证 */
    #include <linux/module.h>
    #include <linux/version.h>
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/types.h>
    #include <linux/interrupt.h>
    #include <linux/list.h>
    #include <linux/timer.h>
    #include <linux/init.h>
    #include <linux/serial_core.h>
    #include <linux/platform_device.h>
    
    /*所要用的内存资源和gpio引脚号资源,按需添加*/
    /* 1.2.2定义led设备资源 */
    static struct resource plat_led_resource[] = {
           [0] = {
           },
           [1] = {
           },    
    };
    /*按需添加*/
    /* 1.2.3定义led设备需要的release函数 */
    static void plat_led_dev_release(struct device *dev)
    {
    }
     
    /*核心数据结构,资源结构体*/
    /* 1.2.1定义led设备 */
    static struct platform_device plat_led_dev = {
           .name = "plat_led_dev",
           .id = -1,
           .resource = &plat_led_resource,
           .num_resources = ARRAY_SIZE(plat_led_resource),
           .dev = {
                  .release = plat_led_dev_release,
           }
    };
     
    /*入口函数*/
    /* 1.2在入口函数中注册platform_device类型的led设备 */
    static int led_dev_init(void)
    {
           platform_device_register(&plat_led_dev);
           return 0;
    }
    
    /*出口函数*/
    /* 1.3在出口函数中卸载platform_device类型的led设备 */
    static void led_dev_exit(void)
    {
           platform_device_unregister(&plat_led_dev);
    }
     
    /* 1.1拷贝头文件,编写和修饰入口函数和出口函数,声明许可证 */
    module_init(led_dev_init);
    module_exit(led_dev_exit);
     
    MODULE_LICENSE("GPL");
    
    platform_drv_skeleton.c
    /* 本文件是依照platformbus驱动<编写思路>章节编写,本文件
      * 的目的是编写代码框架,不做具体细节的编写
      */
    /* 本头文件是linux2.6.31内核所提供的,其他版本按需调整 */
    /* 2.1拷贝头文件,编写和修饰入口函数和出口函数,声明许可证 */
    #include <linux/module.h>
    #include <linux/ioport.h>
    #include <linux/io.h>
    #include <linux/platform_device.h>
    #include <linux/init.h>
    #include <linux/serial_core.h>
    #include <linux/serial.h>
    #include <linux/irq.h>
    #include <asm/irq.h>
    #include <asm/io.h>
    #include <asm/uaccess.h>
    #include <mach/hardware.h>
    #include <mach/regs-gpio.h>
    #include <mach/gpio-fns.h>
    #include <plat/regs-serial.h>
    #include <linux/input.h>
     
    /* 加载驱动时并且和已有的dev匹配时执行probe函数 */
    /* 2.2.2定义probe函数和remove函数 */
    /* 2.2.3 编写probe函数*/
    static int plat_led_drv_probe(struct platform_device *dev)
    {
        /* 
         * 2.2.3.1获取资源
         * 2.2.3.2注册字符设备及操作函数
         * 2.2.3.3提供可操作的设备节点
         */
        return 0;
     
    }
    /*断开连接时执行,和probe动作相反*/
    /* 2.2.4编写remove函数(卸载字符设备,释放资源) */
    static int plat_led_drv_remove(struct platform_device *dev)
    {
    
    }
    
    /* 核心结构体*/
    /* 2.2.1定义platform_driver类型的led驱动 */
    static struct platform_driver plat_led_drv = {
        .driver = {
            .name = "plat_led_dev",
        },
        .probe = plat_led_drv_probe,
        .remove = plat_led_drv_remove,
    };
    
    /* 2.2在入口函数中注册platform_driver类型的led驱动 */
    static int plat_led_drv_init(void)
    {
        platform_driver_register(&plat_led_drv);
        return 0;
    }
    
    /* 2.3在出口函数中卸载platform_driver类型的led驱动 */
    static void plat_led_drv_exit(void)
    {
        platform_driver_unregister(&plat_led_drv);
    }
    
    /* 2.1拷贝头文件,编写和修饰入口函数和出口函数,声明许可证 */
    module_init(plat_led_drv_init);
    module_exit(plat_led_drv_exit);
    MODULE_LICENSE("GPL");
    

    驱动代码

    Platform_dev.c和platform_drv.c是驱动的核心代码,也是需要生成的.ko文件。测试代码在操作led时就需要这两个文件。

    Platform_dev.c
    /* 本文件是依照platform驱动<详细步骤>章节编写,本文件
     * 的目的是编写和介绍具体代码,不介绍框架
     */
     
    /* 本头文件是linux2.6.31内核所提供的,其他版本按需调整 */
    
    /* 2.1编写代码框架:头文件,入口函数,出口函数,声明LICENSE为GPL */
    #include <linux/module.h>
    #include <linux/version.h>
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/types.h>
    #include <linux/interrupt.h>
    #include <linux/list.h>
    #include <linux/timer.h>
    #include <linux/init.h>
    #include <linux/serial_core.h>
    #include <linux/platform_device.h>
    
    /* 2.2.3定义设备资源数组,类型为struct resource,放入内存资源和中断资源 */
    static struct resource plat_led_resource[] = {
        [0] = {
              .start = 0x56000050,
              .end   = 0x56000050 + 8 - 1,
              .flags = IORESOURCE_MEM,
        },
        [1] = {
              .start = 3,
              .end   = 3,
              .flags = IORESOURCE_IRQ,
        },    
    };
    static void plat_led_dev_release(struct device *dev)
    {
        printk("welcome to platform bus world of led\n");
    }
     
    /* 2.2.1定义并led设备,类型为struct platform_device */
    /* 2.2.2填充led设备,放入设备名字,设备id,设备资源,资源个数,release函数 */
    static struct platform_device plat_led_dev = {
        .name = "plat_led_dev",
        .id = -1,
        .resource = &plat_led_resource,
        .num_resources = ARRAY_SIZE(plat_led_resource),
        .dev = {
            .release = plat_led_dev_release,
        }
    };
    /* 2.2在入口函数中注册led设备,类型为struct platform_device */
    static int led_dev_init(void)
    {
        platform_device_register(&plat_led_dev);
        return 0;
    }
    
    /* 2.3在出口函数中卸载platform_device类型的led设备 */
    static void led_dev_exit(void)
    {
        platform_device_unregister(&plat_led_dev);
    }
    
    /* 2.1编写代码框架:头文件,入口函数,出口函数,声明LICENSE为GPL */
    module_init(led_dev_init);
    module_exit(led_dev_exit);
     
    MODULE_LICENSE("GPL");
    
    Platform_drv.c
    /* 本文件是依照platform驱动<详细步骤>章节编写,本文件
     * 的目的是编写和介绍具体代码,不介绍框架
     */
     
    /* 本头文件是linux2.6.31内核所提供的,其他版本按需调整 */
    
    /* 3.1编写代码框架:头文件,入口函数,出口函数,声明LICENSE为GPL */
    #include <linux/module.h>
    #include <linux/ioport.h>
    #include <linux/io.h>
    #include <linux/platform_device.h>
    #include <linux/init.h>
    #include <linux/serial_core.h>
    #include <linux/serial.h>
    #include <linux/irq.h>
    #include <asm/irq.h>
    #include <asm/io.h>
    #include <asm/uaccess.h>
    #include <mach/hardware.h>
    #include <mach/regs-gpio.h>
    #include <mach/gpio-fns.h>
    #include <plat/regs-serial.h>
    #include <linux/input.h>
     
     
    static struct class * plat_leddrv_cls;
    static struct class_device      * plat_leddrvcls_device;
    volatile unsigned long *gpfcon = NULL;
    volatile unsigned long *gpfdat = NULL;
    int major = 0;
    int pin = 0;
    
    /* 3.2.5定义open函数,根据引脚号配置gpio管脚为输出模式 */ 
    static int plat_led_drv_open(struct inode *inode, struct file *file)
    {
        /* 配置GPF3为输出 */
        *gpfcon &= ~(0x3<<(pin*2));
        *gpfcon |= (0x1<<(pin*2));
        return 0;
    }
    
    /* 根据引脚号配置gpio管脚为输出模式 */
    static ssize_t plat_led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
    {
        int val;
        /* 获取用户空间数据,控制寄存器来控制gpio管脚的低和高 */
        copy_from_user(&val, buf, count); //  copy_to_user();
    
        /* 如果用户写1,点亮led,写0,熄灭led */
        if (val == 1)
        {
            // 点灯
            *gpfdat &= ~((1<<pin));
        }
        else
        {
            // 灭灯
            *gpfdat |= (1<<pin);
        }
        return 0;
    }
     
    /* 3.2.4定义struct file_operations结构体,编写对应的函数 */
    static struct file_operations plat_led_drv_fops = {
        .owner  = THIS_MODULE,    
        .open   = plat_led_drv_open,    
        .write  = plat_led_drv_write,   
    };
    
    /* 3.2.2定义probe函数 */
    static int plat_led_drv_probe(struct platform_device *dev)
    {
        /* 1 获取内存资源和中断资源(这里是把gpio管脚号当做了中断资源) */
        struct resource * res;
        res = platform_get_resource(dev,IORESOURCE_MEM,0);
        /* 2 映射gpio管脚f组的控制寄存器和数据寄存器 */
        gpfcon = (volatile unsigned long *)ioremap(res->start, res->end - res->start + 1);
        gpfdat = gpfcon + 1;
        res = platform_get_resource(dev,IORESOURCE_IRQ,0);
        pin = res->start;
    
        /* 3 注册字符设备 */
        major = register_chrdev(0, "plat_led_drv", &plat_led_drv_fops); // 注册, 告诉内核
        /* 4 创建设备类;创建设备节点 */
        plat_leddrv_cls = class_create(THIS_MODULE, "plat_led_drv");
        plat_leddrvcls_device = device_create(plat_leddrv_cls, NULL, MKDEV(major, 0), NULL, "plat_led"); /* /dev/xyz */
    
        return 0;
    
    }
    
    /* 3.2.3定义remove函数 */
    static int plat_led_drv_remove(struct platform_device *dev)
    {
        /* 主要是和出口函数的反向操作
        * 1 卸载字符设备
        * 2 卸载class下的设备
        * 3 销毁这个类
        * 4 取消地址映射
        */
        unregister_chrdev(major, "plat_led_drv"); // 卸载
        /* 2.6.31 上只有device_create 其他版本内核可能有class_device_create */
        device_unregister(plat_leddrvcls_device);
        class_destroy(plat_leddrv_cls);
        iounmap(gpfcon);
    }
     
    /* 3.2.1定义struct platform_driver类型的led驱动 */ 
    static struct platform_driver plat_led_drv = {
        .driver = {
              .name = "plat_led_dev",
        },
        .probe = plat_led_drv_probe,
        .remove = plat_led_drv_remove,
    };
    /*  */
    /* 3.2在入口函数中注册platform_driver类型的led驱动结构体 */ 
    static int plat_led_drv_init(void)
    {
        platform_driver_register(&plat_led_drv);
        return 0;
    }
    
    /* 3.3在出口函数中卸载platform_driver类型的led驱动结构体 */
    static void plat_led_drv_exit(void)
    {
        platform_driver_unregister(&plat_led_drv);
    }
    
    /* 3.1编写代码框架:头文件,入口函数,出口函数,声明LICENSE为GPL */
    module_init(plat_led_drv_init);
    module_exit(plat_led_drv_exit);
    MODULE_LICENSE("GPL");
    

    测试代码

    Platform_test.c
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    int main(int argc,char *argv[])
    {
        int fd;
        int val = 1;
        fd = open("/dev/plat_led", O_RDWR);
        if (fd < 0)
        {
            printf("can't open!\n");
        }
        if (argc != 2)
        {
            printf("Usage :\n");
            printf("%s <on|off>\n", argv[0]);
            return 0;
        }
        if (strcmp(argv[1], "on") == 0)
        {
            val  = 1; //点灯
        }
        else
        {
            val = 0;   //灭灯
        }
    
        write(fd, &val, 4);
        return 0;
    }
    

    Makefile

    KERN_DIR =  /home/linux/tools/linux-2.6.31_TX2440A
     
    all:
           make -C $(KERN_DIR) M=`pwd` modules
    clean:
           make -C $(KERN_DIR) M=`pwd` modules clean
           rm -rf modules.order
    obj-m   += platform_drv.o
    obj-m   += platform_dev.o
    

    目录结构

    xxx@xxx:~/nfs/440/201903-train/09platformbus$ tree
    .
    ├── Makefile
    ├── platform
    ├── platform_dev.c
    ├── platform_dev_skeleton.c
    ├── platform_drv.c
    ├── platform_drv_skeleton.c
    └── platform_test.c
    

    测试步骤

    0 在linux下的makefile +180行处配置好arch为arm,cross_compile为arm-linux-(arm-angstrom-linux-gnueabi-)
    1 在menuconfig中配置好内核源码的目标系统为s3c2440
    2 在pc上将驱动程序编译生成.ko,命令:make
    3 在pc上将测试程序编译生成elf可执行文件,命令:arm-linux-gcc -o -o platform platform_test.c
    4 挂载nfs,这样就可以在开发板上看到pc端的.ko文件和测试文件
    mount -t nfs -o nolock,vers=2 192.168.0.105:/home/linux/nfs_root /mnt/nfs

    执行结果

    [root@TX2440A 09platformbus]# ./platform on
    执行后,LED3被点亮
    [root@TX2440A 09platformbus]# ./platform off
    执行后,LED3被熄灭
    Platformbus的驱动和不带platform的驱动有异曲同工的效果。

    结果总结

    在本篇文章中,笔者跟读者分享了platformbus驱动的编写思路和方法,其中贯穿始终的有几个函数和关键数据结构,它们分别是:
    platform_device_register
    platform_device_unregister
    platform_driver_register
    platform_driver_unregister
    platform_get_resource

    struct platform_device
    struct resource
    struct platform_driver
    请读者尽力去了解这些函数的作用,入参,返回值。

    实战目标

    1请读者根据《需求描述》章节,独立编写需求分析和需求分解。
    2请读者根据需求分析和需求分解,独立编写编写思路和详细步骤。
    3请读者根据编写思路,独立写出编写框架。
    4请读者根据详细步骤,独立编写驱动代码和测试代码。
    5请读者根据《Makefile》章节,独立编写Makefile。
    6请读者根据《测试步骤》章节,独立进行测试。
    7请读者抛开上述练习,自顶向下从零开始再编写一遍驱动代码,测试代码,makefile
    8如果无法独立写出7,请重复练习1-6,直到能独立写出7。

    参考资料

    《TX2440开发手册及代码》
    《linux设备驱动开发详解》

    致谢

    感谢在嵌入式领域深耕多年的前辈,感谢笔者的家人,感谢读者。没有前辈们的开拓,我辈也不能站在巨人的肩膀上看世界;没有家人的鼎力支持,我们也不能集中精力完成自己的工作;没有读者的关注,我们也没有充足的动力来编写和完善文章。希望读者学到的不仅仅是如何编写代码,更进一步能够学到一种思路和一种方法。

    笔者后续有计划按照本模板编写linux下的常见驱动,敬请读者关注。

    联系方式

    微信订阅号:自顶向下学嵌入式
    公众号微信:EmbeddedAIOT
    CSDN博客:chichi123137
    CSDN博客网址:https://blog.csdn.net/chichi123137?utm_source=blog_pc_recommand
    QQ邮箱:834759803@qq.com
    QQ群:766756075
    更多原创文章请关注微信公众号

    自顶向下学嵌入式公众号.jpg 赞赏码2.png

    相关文章

      网友评论

        本文标题:linux驱动篇-Platformbus

        本文链接:https://www.haomeiwen.com/subject/ucwymqtx.html