嵌入式Linux驱动开发的最终目的
从裸机开发经历来看
从C51到STM32开发的经历来看,最终都是操作寄存器以达到某些目的。
正点原子的嵌入式Linux教程也是从裸机开发开始的
汇编点灯
.global _start
_start:
ldr r0, =0X020C4068
ldr r1, =0XFFFFFFFF
str r1, [r0]
ldr r0, =0X020E0068
ldr r1, =0X5
str r1,[r0]
ldr r0, =0X020E02F4
ldr r1, =0X10B0
str r1,[r0]
ldr r0, =0X0209C004
ldr r1, =0X0000008
str r1,[r0]
ldr r0, =0X0209C000
ldr r1, =0
str r1,[r0]
loop:
b loop
嵌入式Linux驱动的目的
从上面的裸机开发可以知道嵌入式Linux驱动开发的目的还是操作物理寄存器,只不过为什么叫驱动开发呢?我的理解有两个:
- 以前像C51和STM32之类单片机的开发,驱动和应用其实是没有分开的(没有驱动这个概念)。即一个工程甚至是一个文件里面即包括驱动(操作物理寄存器),也包括应用(实现实际的功能)。
- 而对于用Linux这样优秀的操作系统来做开发,应该做到分离和分层。这样可以很好的、很快的、很轻易的做到快速开发,以及分工合作。驱动就只管做好操作物理寄存器,并提供相应的API给应用程序(具体是给内核,内核再给用户),这就是驱动工程师该做的事情。应用程序就只管应用需求的开发,不用在意底层的实现,只管调用底层提供的API即可,这就是应用开发工程师该做的事情。
韦老师的教学路线
上面我们说了,嵌入式Linux开发分为两类:应用开发和驱动开发。
实现最基本的需求
主要分为应用程序部分和驱动部分。整体框架如下:
应用程序部分
直接调用open、read、write、close等函数。
驱动程序部分
实现chr_open、chr_read、chr_write、chr_close等函数供应用程序调用。而在这些函数中要实现其自己的功能,比如配置寄存器、操作寄存器等。
面向对象的思想
在驱动程序中的相应函数中直接操作寄存器会使程序移植、维护变得困难,可以将操作物理寄存器和驱动分开。即整体框架如下:
对于开发板上的led灯,可以抽象出一个led_opr结构体,其中包括对led的初始化和配置函数,并且将led_opr结构体提供给上层。
struct led_operations {
int num;
int (*init) (int which);
int (*ctl) (int which, char status);
};
struct led_operations *get_board_led_opr(void);
主要有3个c文件,app.c, driver.c, led_opr.c 。调用关系为: app.c 调用相关函数,进入到driver.c 驱动程序中,而在驱动程序中的open函数中调用led_opr.c 提供的led_opr结构体中的init成员初始化led灯,在驱动程序中的write函数中调用led_opr.c 提供的led_opr结构体中的ctrl成员控制led灯。
以后只需改变led_opr结构体中相应函数即可,不必理会driver.c文件。
面向对象更进一步
上面说了,如果led变了,那么就需要改变led_opr结构体中相应函数的具体实现。但是对于某些开发板来说,虽然它们板上的led所用的GPIO管脚不一样,但是用的芯片是一样的,而对于同一款芯片来说,GPIO的操作都是一样的。
所以我们可以把开发板和芯片分离,即针对某一款芯片编写一个c文件来初始化GPIO;对于开发板编写一个c文件来实现控制哪一个led(GPIO),即在这一个文件中说明led用的是哪一个GPIO资源。
主要有4个文件,app.c, driver.c, led_resource.c, chip_gpio.c 。调用关系为:app.c调用相关函数,进入到driver.c驱动程序中,而在驱动程序中的open函数中调用chip_gpio.c 提供的init函数(同上面的led_opr),在init函数中初始化相应的led管脚,具体是哪一个管脚呢?由led_resource.c 指定。也可一这么说,在init函数中调用led_resource.c 提供的led_pin;在驱动程序中的write函数中调用chip_gpio.c 提供的ctrl函数(同上面的led_opr)操作相应的led管脚(led对应的gpio已经由init函数获得)。
更进一步/总线驱动
前面一直在说对led的操作,那么对于其它的呢?比如蜂鸣器、spi等。那么Linux内核抽象出了platform_device和platform_driver,一个定义资源,一个实现操作。
更具体的图:
和上面比,其实platform_device=led_resource.c,platform_driver=chip_gpio.c 。
chip_demo_gpio.c (chip_gpio.c )相当于最开始的中根据platform_device指定的资源进行初始化和操作,该文件中有platform_driver结构体,其中有probe成员。
board_A_led.c(相当于最开始的led_resource.c)中根据platform_device指定需要操作对象,上图中的resource成员指定了2个gpio管脚。
文件有点多,说明一下它们之间的关系:
platform_device这个文件加载时,platform_driver结构体中的probe成员会自动执行(如果两者匹配成功的话),并且platform_device作为probe的参数,所以就可以在probe中获得platform_device指定的pin管脚资源。而platform_driver这个文件是操作物理寄存器的,所以它加载到内核时,首先把自己操作物理寄存器的init和ctrl方法告诉(写一个函数返回封装的led_opr)给它的上层驱动程序leddrv.c ,而又由于platform_driver已经获得了具体的资源,所以init和ctrl都有了具体的操作对象。
设备树的引入
设备树的引入是为了解决platform_device的,因为每个设备都要写一个.c文件,这样的话会让Linux源码中充满着垃圾(.c文件),所以现在就用设备树代替.c文件中的platform_device。
即用设备树来描述开发板的情况,设备树中的每个结点都会被Linux内核解析成一个device_node结构体,然后某些device_node结构体会被解析成platform_device。
部分
设备树中的结点
device_node
platform_device
最终总结
应用+驱动
在应用中使用相应的函数,在驱动中实现相应的函数。并且在相应的函数中(如open、write等)直接操作物理寄存器。
所含文件:app.c、driver.c
特点:如果更换led灯的话,driver.c 要重写。
应用+驱动框架+led_opr
在驱动相应的函数中不去操作物理寄存器,而是使用下层提供的led_opr结构体,其中包含init和write等成员,在下层中实现init、write等函数(操作寄存器)。
所含文件:app.c、driver.c、led_opr.c
特点:led灯更换后只需修改led_opr.c 文件。
应用+驱动框架+led_resource+chip_gpio
正如上面所说,led灯更换后,led_opr需要修改。所以我们可以继续封装,led_resource.c 中指明用到了那些设备/资源,chip_gpio.c 中是对某款芯片所有的gpio的操作。这样我们在led_resource中指明所用的设备(led),在driver.c 相应的函数中(open、write等)从led_resource中获取指定管脚,然后再调用chip_gpio提供的一些函数(如init、ctrl等)来操作指定管脚。
所含文件:app.c、driver.c、led_resource.c、chip_gpio.c
特点:led更换后只需修改led_resource.c 文件。即led_resource指明led灯的管脚,chip_gpio 实现某款芯片所有gpio 的操作(init和ctrl),driver.c 根据led_resource 指明的引脚告诉chip_gpio 操作那个引脚。
应用+驱动框架+platform_device+platform_driver
和上面一个一样的,只不过采用Linux内核提供的总线驱动框架来编写驱动程序。platform_device 声明设备/led 所用的资源,platform_driver 实现对platform_device 声明的设备进行操作(platform_driver 可以写入到驱动框架中去)。
所含文件:app.c、driver.c、led_resource.c(platform_device)、chip_gpio.c(platform_driver)
应用+驱动框架+设备树+platform_driver
和上面一样,只不过platform_device 写入到设备树中去了(前面说过,设备树中的结点会被内核解析成device_node ,某些device_node 被解析成platform_device )。然后platform_driver 可以和驱动框架写一块。最终的文件只含:
所含文件:app.c、driver.c(platform_driver) 、设备树.dts
|