Linux嵌入式驱动初体验(五)--- LED驱动解析

时间:2022-06-20 23:38:31

      在编写驱动程序的时候,入门的应用应该就是LED的驱动了,它的地位就像是Hello World之于C语言。其实LED灯是一种原子设备(我记得我们一个硬件老师说了这么一个名字,如果我说错了,就怨我没记住吧),意思就是只有0和1两种结果,就是只有亮和不亮两种结果,要是非和我抬杠说有半亮不亮的状态,那我也没话说了。所以对于一个原子设备来说,它的驱动还是比较容易弄懂的,而且对于一个简单的LED驱动来说,可以不用考虑一个最让人头疼的部分,就是---中断,所以还是比较好上手的。

      根据上一篇文章,我们可以知道,对于一个简单的设备来说,写驱动首先就是写操作它的函数,下面我们可以把LED灯看成是字符设备,我们只需要写open/release/read/write/ioctl,而对于LED灯这种设备,我们其实已经没有必要去进行read操作了,如果我们再依照platform来写驱动的话,那么我们还需要probe和remove,就是注册和卸载,然后我们再通过file_operation、miscdevice、platform相关的结构体,使各个函数串联起来。好的,结构就是这样的,那么我们首先就可以定下一些数据结构和其中的元素了:


好了,有了一个整体的架构之后,下面的工作就是填充我们定义的这些结构体了,首先我们实现open和release:


仔细一看,这两个函数只有三点不同:1、函数名不同;2、open中是try_module_get,release中是module_get;3、printk中的内容不同。第一点和第三点可以忽略,真正可以区分两个函数的是try_module_get和module_get,前者用于增加模块使用计数,后者用于减少模块使用计数,可以理解为添加模块和删除模块,实现open和release的功能。下面我们再看看相同的部分,当然,return 0我们就不做过多研究了,重点放在 __raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE)) 这句话上,首先还得介绍一下gpio的控制,就是对于开发板上的引脚,它们相当于都有自己的宿舍楼,然后又有自己的房间,在这个例子中,我们要控制的是LED2,而它可以和GPO5短接,所以我们只要控制GPO5就能控制LED2的亮灭了,而GPO5“住”在“3号楼”的第5间房子里(注意是以0开始的,它是5号,也就是第6位),就是P3的第5位,所以只要控制P3的第5位,给它赋值0或1(0对于低电平,1对应高电平),就可以控制它的亮灭了。再看__raw_writel中的两个参数,它们都是宏定义的,__BIT(5)展开就是1<<5,就是把1向左移动5位,正好对应到GPO5的“住址”,然后把1<<5写到P3的SET寄存器中,就是给GPO5一个高电平,使得LED2先灭掉了。综上所述,open中就是实现加载模块,release中就是卸载模块。

      下一步我们来实现write:


顾名思义,就是要进行写操作,通过上面的分析,大概知道,要想让LED2亮灭,就要控制GPO5,给它写1或0。在Linux中设备都是当文件来看的,所以参数第一个是文件名,第二个是要写入的数据,第三个是计数器,第四个是这次对文件进行操作的起始位置(这个参数的解释是上网找的,不是很理解它的作用,因为在函数体重没有用到它)。函数体前面的都是检测部分,看是否运行了多次设备,看共享资源是否被使用而不能进行中断处理,然后就是把要写入的数据进行一定的处理(这应该和硬件有关,这个只是针对SmartArm3250来说的),判断想要干什么,如果是0,说明要使LED2亮,就给P3的CLR寄存器的第5位置1,给GPO5一个低电平,让LED2点亮;如果是1,就是要使LED2灭,就把P3的SET寄存器的对应位置1,给它一个高电平。最后就是释放信号量,返回计数值。

      然后我们再看一下ioctl的实现:


前面依旧是检测部分,对于ioctl来说,作用其实和write差不多,只是ioctl是接受命令(其实就是一个宏定义的东西),write是直接写具体的值进去。本着这个原则,看函数体中的switch部分,如果是SET_LED_ON的话,就要让LED2亮起来,就是用__raw_writel函数写1到相应位置,如果是SET_LED_OFF或是其他,则要让LED2灭掉。原理和上面的write中的实现是一样的,就不多说了。

完成上面的4个函数之后,我们就可以填充led_fops这个结构体了,下面就是为了应和platform结构体系,所以我们还要编写另外的两个函数probe和remove,实现如下:


很像前面的open和release函数,其实目的也是一样的,只是它会用到两个系统中已经定义好的函数:misc_register和misc_deregister,就是注册和撤销。

驱动都是以模块的形式来实现的,所以就要有模块的入口和出口,就是init和exit。我们先看init:


前两句话忽略,然后第三句话,很眼熟,只是第二个参数没有见过,它是一个宏定义的函数,再拿前面的例子,如果把GPO5看成是住址,它是住在P3号楼,这里的目的就是让你先找到P3号楼,就是找到基址,用稍微专业一点的话来说,就是使能P3端口。下面的platform_device_register_simple函数也是系统里有的,就是起注册的作用。driver_register也是其注册作用,前面的是注册platform_device的结构,而这个是注册driver结构。当然还有一些出错处理,然后还会有一个信号量的初始化。

下面是exit,就是用来退出模块,内容比较“传统”:


就是两个单纯的撤销函数driver_unregister和platform_device_unregister,还有一些客套的输出。

    下面我再把整个程序的剩余代码贴出来:

 

前面是大量的调用头文件,就不多说明了,对于那两个宏定义,重点说一下第二个,io_p2v,这也是系统中有定义的一个宏定义,作用就是p(物理地址)2(to)v(虚地址),这就是定义了p3的基址,在后面的__raw_writel时使用。后面就是更客套的模块编程的东西了,是模块基本都这么写,不过不同的人MODULE_AUTHOR中的内容应该是不同的。。。

    为了保持程序的完整性,我再把Makefile贴出来:

 

不同的人不同的电脑,Makefile可能就不一样了,但是原理是一样的,就是进行一些简单的配置然后进行编译。不过别人的东西终究是别人的,只有有了自己的东西,才叫真正的提高,所以还是建议大家自己动手写一个自己的驱动程序,过程可能是艰辛的,但是收获一定是大大的。


好了,至此为止,一个简单的platform结构的LED驱动就写好了,不过大家可能注意到了,有好多函数的参数其实并没有在函数体中出现,但是这样的语法是正确的,其实这只是在应和系统中的格式定义,因为我们都是在填充结构中的元素,所以要和它们的形式保持一致。不过对于驱动来说,简单理解,它只是编写了许多的函数,这些函数可以控制对应的设备,但是如果没有调用这些函数,那么LED也不会亮起来的,所以还会有一个测试函数,来调用这些编写好的函数,这个测试函数后面我会写出来。

以上就是LED驱动解析的全部内容,前些日子有些“忙”,所以一直没有写完,期间也一直在研究这个驱动,也是在进步和成长中,现在写的和我刚开始的理解已经有了很大的不同了,所以对于学习,应该是慢慢体会的,一口吃不了一个大胖子,成长中一定是会有障碍的,还是那句话错误中成长,改错中进步。现在只迈出了一小步,大家共同进步!