基于linux的led驱动程序实现一. 博创开发平台硬件LED的实现博创开发平台设置了3个GPIO控制的LED和一个可直接产生外部硬件中断的按键,LED分别使用了S3C2410的GPC5,GPC6,GPC7三个GPIO,按键接到INT5中断。
下面对S3C2410 GPIO的各个寄存器作出说明,用GPIO控制的LED就是通过操作GPIO的各个寄存器进行配置和操作的。
S3C2410包含GPA 、GPB 、……、GPH 八个I/O端口。
它们的寄存器是相似的:GPxCON 用于设置端口功能(00 表示输入、01表示输出、10 表示特殊功能、11 保留不用),GPxDAT 用于读/写数据,GPxUP 用于决定是否使用内部上拉电阻(某位为0 时,相应引脚无内部上拉;为1时,相应引脚使用内部上拉)。
这里要稍微注意一点的地方是PORTA和其他几组端口的使用不太一样,这里不讨论A口,B到H组口的使用完全相同。
以下是S3C2410手册上的数据[13]:图1.1 S3C2410端口GPC口有16个IO口,查datasheet《S3C2410》所用的地址为:图1.2 C组GPIO的地址即GPCCON 地址为0x56000020,GPCDAT地址为0x56000024,各位的设置具体见下图,则对应的GPCCON寄存器的位为:图1.3 GPCCON寄存器相应的位这里用到了5,6,7三个口,CON寄存器要完成对对应口的设置工作,将相应的口设置为输出状态,其他的口不用考虑,设置为输出的话就是0x15<<10,这样3个IO口就设置为了输出。
下面就可以通过向DATA口写入低电平来点亮LED,GPCDAT的各位分布如下,每一个bit对应一个口。
图1.4 GPCDAT的位分布GPCDAT有16位,我们这里要用到的就是5,6,7三位即将这3位设置为低电平点亮LED。
具体使用情况见驱动的实现。
这三个LED的硬件原理图如下:图1.5 GPIO控制的LED硬件原理图二.通过GPIO控制的LED驱动程序本驱动中没有用到内核提供的write_gpio宏,对硬件地址的操作完全自己实现,可分为以下几部分:①模块的初始化和退出:int led_init(void){int ret;ret=register_chrdev(MAJOR_LED,NAME,&leds_fops);port_addr= (unsigned long )ioremap(0x56000020,0x8);if(ret<0)goto fail;printk(KERN_INFO NAME"initialized!!\n");return 0;fail:printk(NAME"Can not register major number %d!!\n",MAJOR_LED);unregister_chrdev(MAJOR_LED,NAME);return ret;}void led_exit(void){iounmap(port_addr);printk(KERN_INFO NAME"quit!!\n");unregister_chrdev(MAJOR_LED,NAME);}module_init(led_init);module_exit(led_exit);module_init和module_exit为内核提供的接口,以模块方式插入到内核中时内核首先要找的就是这两个宏,找到对应的初始函数这里为led_init初始化模块,和卸载函数这里为led_exit当模块撤出内核时调用。
这两个函数名称可以自己定义,但是module_init这个两个宏的名字不能改变,并且led_init的返回值类型必须为int型,led_exit的返回类型必须为空。
这两个函数只是告诉内核驱动模块在内核中了,但并不一定在使用它,而open和release是当设备被打开和关闭的时候才回被调用,模块不会退出内核。
初始化函数led_init中主要完成的工作为:注册设备号和文件操作结构;映射内存地址空间;做出一定的错误处理。
设备注册的工作由register_chrdev来完成,如果返回值是负值表示错误,0或者返回值为正值表示操作成功,其中MAJOR_LED为静态申请的主设备号定义为#define MAJOR_LED 237,NAME 为设备的名称定义为#define NAME "leds",leds_fops为file_operations类结构体定义如下:static struct file_operations leds_fops={owner:THIS_MODULE,open: led_open,release:led_close,ioctl: led_ioctl,};可以看出,此设备驱动要完成的工作只是简单的打开(open)、关闭(release)、通过应用程序传参数来控制LED(ioctl)。
各函数的具体实现下面讲解。
port_addr= (unsigned long )ioremap(0x56000020,0x8);完成物理地址到虚拟地址的映射,前面已经提到,linux系统只认虚拟地址而不人物理地址,所以利用这个内核API来完成映射,0x56000020是GPCCON寄存器的物理地址,这个可以通过图4.3知道,0x8表示从上面那个物理地址开始的8个字节的地址空间要映射到内核虚拟地址空间,这个空间中包括了GPCDAT寄存器,可以通过返回的地址加4得到,这个返回的虚地址存放在port_addr中,以后对硬件的操作都是通过对这个地址的操作实现的。
地址定义格式如下:#define GPC_CON (*(volatile unsigned long *)port_addr)#define GPC_DAT (*(volatile unsigned long *)(port_addr+0x4))这里将地址定义为long类型,是因为ARM为4字节对齐方式,并且GPCCON 寄存器为32位,所以下面GPCDAT寄存器地址直接加4就可以访问到了,并且ARM寄存不支持直接对字中字节进行直接访问,这需要使用专门的访问函数。
Led_init中还做了简单的错误处理,如果注册失败则解除注册并且返回。
模块卸载函数只是做了解除工作,即释放主设备号,并且解除地址映射。
②接口函数的实现:static int led_open(struct inode *inode,struct file *filp){GPC_CON=GPC5_OUT|GPC6_OUT|GPC7_OUT;printk("major number %d\n",inode->i_rdev);printk(NAME"open success!!\n");return 0;}static int led_close(struct inode *inode,struct file *filp){printk(NAME"release!!\n");return 0;}static int led_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned longarg){switch(cmd){case 0:if(arg>3){return -EINV AL;}GPC_DAT=LED1_OFF|LED2_OFF|LED3_OFF;break;case 1:if(arg>3){return -EINV AL;}GPC_DAT=LED1_ON&LED2_ON&LED3_ON;break;default:return -EINV AL;}return 0;}以上函数的接口集合在file_operations结构中,实现了系统提供给用户程序的接口。
Open函数在file_operations结构中的原型为int (* open)(struct inode *,struct file *);这是设备的第一个操作,但是并不是要求驱动程序必须去实现这个方法,如果这个入口为NULL,那么设备的打开操作将永远成功,一般驱动程序中open 要完成的工作有:增加使用计数;检查设备特定的错误;如果设备是首次打开,则对其进行初始化;识别次设备号,并且如果必要,更新f_op指针;分配并填写被置于filp->private_data里的数据结构。
我的理解是,open函数就是要完成设备驱动和文件系统的关联,上面已经讲过file和inode两个结构的关系,这里参数中的两个结构正是系统在/dev创建设备节点后提供给驱动的文件结构。
本驱动中的open实现只是完成了对C组GPIO的GPC_CON寄存器进行初始化,将3个LED对应的3个口设置为输出模式,定义格式如下:GPC_CON=GPC5_OUT|GPC6_OUT|GPC7_OUT;其中GPCX_OUT的定义为:#define GPC5_OUT (1<<(5*2))#define GPC6_OUT (1<<(6*2))#define GPC7_OUT (1<<(7*2))具体的位模式可以查看图1.3,这样要设置后的寄存器内容为010101,这样就将3个口设置为输出模式了。
Open剩下的工作就是打印设备号。
Release函数即驱动中的close函数要完成的工作就是:释放由open分配的、保存在filp->private_data中的所有内容;在最后一次关闭操作时关闭设备;使用计数器减1。
这里和上面的open函数都提到了一个模块计数,意思就是内核要统计这个模块被打开的次数,这样才不会在还有使用的情况下卸载模块,在早期的linux版本中,模块计数的工作要由驱动程序自己完成,用到类似于MOD_INC_USE_COUNT的宏来实现,现在的内核版本是内核自动维护这个计数,不用在驱动中实现,所以本驱动中的release函数并没有实现具体的操作。
Ioctl在接口结构中的原型为:int (* ioctl)(struct inode *,struct file *,unsigned int, unsigned long);为用户程序的ioctl系统调用提供了一种执行设备特定命令的方法(即读写之外的操作),并且,内核还能识别一部分ioctl命令,而不必调用fops 表中的ioctl。
如果设备不提供ioctl入口点,则对于任何内核未定义的请求,ioctl 系统调用将返回错误,如果该设备方法返回一个非负值,那么相同的值会被调用返回给调用程序以表示调用成功。