怎么写一个LED驱动程序?
- 看原理图,确定怎么去操作;
- 写驱动程序;
- 写测试程序;
假设2440的一个GPIO引脚,引脚上连接一个小灯,一个限流电阻,还有3.3V电源,那么要怎么让小灯亮。
从上面的描述我们知道,让引脚输出低电平就可以点亮小灯,要获得这个信息就要看原理图。
对于点灯来说,看原理图就是:
- 确定引脚;
- 看芯片手册确定怎么操作这个引脚;
之后就要写驱动程序了。
写驱动程序的人需要看原理图看芯片手册,但是对于写应用程序的人,他们不知道也不关心怎么看原理图和芯片手册,他们只需要怎么去写应用程序,怎么去实现业务需求。
驱动程序就是将应用和底层分离开,起一个封装作用。让写应用程序的人可以专心负责上层需求,写驱动程序的人则负责底层实现。
在应用程序中,接口基本都是固定的,想要操作一个设备首先要open,然后有写的read函数,读的write函数,i/o操作的ioctl函数。
同样,在驱动程序中,也有与之一一对应的驱动函数。这些驱动函数将应用程序和底层硬件分离开,应用程序通过固定的接口调用底层驱动,而不是直接操作硬件。
那要怎么实现他们的分离呢?
- 分配一个结构体file_operations;
- 设置该结构体:.open = led_open,
.write = led_write,
这样调用open就相当于调用led_open函数; - 注册(告诉内核);
- 入口函数:调用register_chrdev;
- 出口函数:调用unregister_chrdev;
所谓注册,就是告诉内核这个驱动程序的相关信息,把这个结构体放入一个数组里面。
内核中有许多数组,这个数组里面存放着许多驱动程序,register_chrdev需要带一个参数叫主设备号,这个主设备号就是存放在数组的哪个元素,如果为0则表示由系统自动选一个空闲的位置存放。
register_chrdev会传入三个参数,分别是主设备号,结构体,设备名字,其中设备名字不重要。
通常,在led_open中,把LED引脚配置为输出引脚(设置功能);
在led_write中,根据APP传入的值设置引脚状态(设置状态);
问:在驱动中如何指定引脚?看原理图可以知道引脚是哪个,但是怎么告诉驱动程序呢?
答:有三种方法:
- 传统方法:在代码中写死;
- 总线设备驱动模型,把驱动一分为2:
a.led_drv.c:实现分配和注册结构体;
b.led_dev.c:指定引脚; - 使用设备树指明引脚:
a.led_drv.c:同样是实现分配和注册结构体;
b.jz2440.dts:指定引脚;
这三种方法都有一个相同点,就是配置结构体,不同的是怎么去指定引脚。也就是,驱动写法:核心不变,差别在于:如何指定硬件资源。
问:那么这三种方法有什么优缺点呢?
答:假设使用同一款芯片做了两款产品,一款是TV,另一款是Camera。
这两款产品都有状态灯,不同的是TV使用的是pin1,Camera使用的是pin2,他们的连接电路相同,都是低电平点亮,高电平熄灭,不同的只是控制的引脚。
现在要写程序:
1.传统方法
首先写TV的,要写一个led_drv.c,然后:
- 分配一个file_operations结构体;
- 设置结构体,.open = led_open;(配置pin1为输出引脚,把引脚写死了)
.write = led_write;(根据APP传入的值设置pin1状态) - 注册;
- 入口;
- 出口;
写完TV之后要写camera的,可以将上面的代码复制一遍,然后在此基础上进行修改,只要把原来的pin1改成pin2就可以了;
优点:简单;
缺点:不易扩展,每次换一个板子都要重新写代码重新编译,即使改动很小也要重做一个版本,工作量较大;
2.总线设备驱动模型
这两款产品都是使用相同的芯片做的,也就是说pin1和pin2的操作是类似的,可以将驱动分为两部分:led_dev.c和led_drv.c,它们都挂在platform总线上。
led_dev.c:
指定资源,分配/设置/注册 platform_device,platform_device里面有各种资源,其中.resource 指明引脚,还有一个.name 指明名字。
创建两个类似的platform_device,TV的platform_device中.resource 为pin1,TV的platform_device中.resource 为pin2,在led_drv.c中决定使用哪个platform_device。
led_drv.c:
分配/设置/注册 platform_driver 结构体,这个结构体里面有.probe,.driver(.driver里面包含一个成员.name)。
当内核中发现有一个platform_device和一个platform_driver 的.name相同时,.proce函数就会被调用,该函数会做:
- 分配一个file_operations结构体;
- 设置结构体,.open = led_open;(配置对应引脚为输出引脚,来自平台设备,不是写死的了)
.write = led_write;(根据APP传入的值设置引脚状态) - 注册;
- 入口;
- 出口;
就和第一种方法类似,但是引脚不是写死的了,而是来自平台设备。
优点:易扩展(对于TV和Camera,led_drv.c保持不变,唯一要修改的是led_dev.c中的资源);
缺点:稍复杂;容易产生冗余代码(TV和Camera需要两个platform_device结构体,如果有更多的改动就要有更多的platform_device,而且这些结构体都是以.c文件的形式出现的,会占用多余的空间);每换一个引脚都需要重新编译led_dev.c,即每次改动都要重新编译一个版本,增加了工作量;
由于这些缺点,linus说这种方法是垃圾。。。
3.设备树
总线设备模型的方法在于,通过c文件来指定使用的资源,这样每次改动都需要编译。
那么有没有什么其他的方法来指定使用的资源呢?可以使用设备树的方法来指定资源。
使用设备树来写程序时,同样需要两部分。
led_drv.c:
分配/设置/注册 platform_driver,这部分同总线设备模型是完全一样的。
.dts文件:
内核根据该文件来分配/设置/注册platform_device。当你需要更改设置时,只需要修改.dts文件即可。
该文件最终会被编译为.dtb文件。
使用设备树,在修改配置时就不需要重新编译内核,重新编译C文件了,只要给它一个.dtb文件就可以。
优点:易扩展;无冗余代码;不需要重新编译内核/驱动,只需要提供不一样的设备树文件即可;
缺点:稍复杂;