这是我的第一个驱动程序,控制LED灯的亮灭
先介绍一下我的开发环境是,一个mini2440开发板,使用的是Linux2.6.32的内核。本开发板上使用引脚GPB5--GPB8外接4个LED。要点亮一个LED灯,首先保证(3)引脚功能设置为输出(2)要点亮LED,令引脚输出0(3)要熄灭LED,令引脚输出1。
首先这是一个字符设备的驱动程序,字符设备驱动程序中最重要的一个结构体是下面的这个:在Linux内核的include/linux/fs.h中。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,
unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,
unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
|
当应用程序调用open函数打开某个设备时,设备驱动程序的file_operations结构中的相应的open成员就会被调用;其它相应的read,write,ioctl等函数读写、控制设备时,驱动的程序中的相应的read,write,ioctl函数会被调用。从这个角度来说,编写字符设备驱动程序就是为具体硬件的file_operations结构编写各个函数。
那么,当应用程序通过open,read,write等系统调用访问某个设备文件时,Linux系统怎么知道去调用哪个驱动程序的file_operations结构中的open,read,write等成员呢?
(1) 设备文件中有主/次设备号
设备文件分为字符设备、块设备,比如PC机上的串口设备属于字符设备,硬盘属于块设备。
#:/dev$ ls /dev/ttyS0 /dev/sda -l
brw-rw---- 1 root disk 8, 0 2010-06-08 08:40 /dev/sda
crw-rw---- 1 root dialout 4, 64 2010-06-08 11:34 /dev/ttyS0
|
"brw-rw----" 中的"b" 表示/dev/sda 是个块设备,它的主设备号为8,次设备为0;"crw-rw---- "中的"C"表示/dev/ttyS0 是个字符设备,它的主设备号为4,次设备号为64.
(2)模块初始化时,将主设备号与file_operations结构一起向内核注册。
驱动程序有一个初始化函数,在安装驱动程序时会调用它。在初始化函数中,会将驱动程序的
file_operations结构连同其主设备号一起向内核进行注册。对于设备使用如下的函数注册
int register_chrdev(unsigned int major,const char * name,struct file_operations *fops);
这样,应用程序操作设备文件时,Linux系统就会根据设备文件的类型(是字符设备还是块设备)、主设备号找到在内核中注册的file_operations结构,次设备号供驱动程序驱动自身来分辨它是同类设备中的第几个。
编写字符驱动程序的过程大概如下。
(1) 编写驱动程序初始化函数。
进行必要的初始化,包括硬件初始化,向内核注册驱动程序等
(2)构造file_operations结构中要用到的各个成员函数
在实际的驱动程序中当然比这两个步骤要复杂,还有其它的高级的技术,比如中断,select机制、fasync异步通知机制。
下面介绍一下主要的函数:
static int __init s3c2440_leds_init(void)
{
int ret;
/* 注册字符设备驱动程序
* 参数为主设备号、设备名字、file_operations结构;
* 这样,主设备号就和具体的file_operations结构联系起来了,
* 操作主设备为LED_MAJOR的设备文件时,就会调用s3c2440_leds_fops中的相关成员函数
* LED_MAJOR可以设为0,表示由内核自动分配主设备号
*/
ret = register_chrdev(LED_MAJOR,DEVICE_NAME,&s3c2440_leds_fops);
if(ret<0)
{
printk(DEVICE_NAME "can't register major number\n");
return ret;
}
printk(DEVICE_NAME "initialized\n");
return 0;
}
/*
* 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数
*/
static void __exit s3c2440_leds_exit(void)
{
/* 卸载驱动程序 */
unregister_chrdev(LED_MAJOR,DEVICE_NAME);
}
/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c2440_leds_init);
module_exit(s3c2440_leds_exit);
/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("Frankzfz"); // 驱动程序的作者
MODULE_DESCRIPTION("S3C2440 LED Driver");// 一些描述信息
MODULE_LICENSE("GPL");
|
执行“insmod s3c2440_leds.ko”命令时就会调用这个函数register_chrdev 函数向内核注册驱动程序:将设备号LED_MAJOR与file_operations结构s3c2440_leds_fops联系起来,以后应用程序操作主设备号为LED_MAJOR的设备文件时,比如open,read,write、ioctl,s3c2440_leds_fops中的相应成员函数就会被调用。但是,在s3c2440_leds_fops中并不会全部实现这些函数s3c2440_leds_init s3c2440_leds_exit函数前的"__init"、"__exit"只有在将驱动程序静态链接进内核时才有意义。前者表示s3c2440_leds_init函数的代码被放在".init.text"段中,这个段在使用一次后被释放;后者表示s3c2440_leds_exit函数的代码被放在".exit.data"段中。
下面这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
static struct file_operations s3c2440_leds_fops =
{
.owner = THIS_MODULE,/* 这是一个宏,推向编译模块时自动创建的__this_module变量 在文件
include/linux/module.h*/
.open = s3c2440_leds_open,//用来初始化LED所用的GPIO引脚
.ioctl = s3c2440_leds_ioctl,//用来根据用户传入的参数设置用户传入的参数设置GPIO的输出电平
};
|
/* 应用程序对设备文件/dev/leds执行open(...)时,
* 就会调用s3c2440_leds_open函数
*/
static int s3c2440_leds_open(struct inode *inode,struct file *file)
{
int i;
for(i=0;i<4;i++)
{
// 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能
s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
}
return 0;
}
|
不在模块的初始化函数中进行这些设置的原因是:虽然加载了模块,但是这个模块却不一定会被用到,就是说这些引脚不一定用于这些用途,它们可能在其他模块中另作他用。所以,在使用时才设置它,我们把对引脚的初始化放在open操作中。s3c2410_gpio_cfgpin函数是内核里实现的,它被用来选择引脚的功能,其实现原理就是设置GPIO的控制寄存器。
static int s3c2440_leds_ioctl(struct inode *inode,struct file *file,unsigned int
cmd,unsigned long arg)
{
if(arg>4)
{
return -EINVAL;
}
switch(cmd)
{
case IOCTL_LED_ON:
// 设置指定引脚的输出电平为0
s3c2410_gpio_setpin(led_table[arg],0);
return 0;
case IOCTL_LED_OFF:
// 设置指定引脚的输出电平为1
s3c2410_gpio_setpin(led_table[arg],1);
return 0;
default:
return -EINVAL;
}
}
|
应用程序执行系统调用ioclt(fd,cmd,arg)时,s3c2440_leds_ioctl函数将被调用。根据传入的cmd,arg参数调用s3c2410_gpio_setpin函数,来设置引脚的输出电平:输出0时点高LED,输出1时熄灭LED。s3c2410_gpio_setpin也是内核中实现的,它通过GPIO的数据寄存器来设置输出电平。应用程序执行open、ioctl等系统调用,它们的参数和驱动程序中相应函数的参数不是一一对应的,其中经过了内核文件系统层的转换。
int open(const char *pathname,int flags)
int ioctl(int d,int request,....)
ssize_t read(int fd,void *buf,size_t count)
ssize_t write(int fd,const void *buf,size_t count);
|
file_operations结构中的成员如下:ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
|
可以看到,这些参数有很大一部分是相似的
(1) 系统调用 open传入的参数已经被内核文件系统层处理了,在驱动程序中看不出原来的参数了
(2)系统调用ioclt的参数个数可变,一般最多传入3个:后面两个参数与file_operations结构中ioctl成员的后面两个参数对应
(3) 系统调用read传入的buf、count参数,对应file_operation结构中read成员的buf、count参数.而参数offp表示用户在文件中进行存取操作的位置,当执行完写操作后由驱动程序设置
(4)系统调用write与file_operations结构中write成员的参数关系。
驱动程序的编译
把s3c2440_leds.c文件放入内核的drivers/char子目录下,在drivers/char/Makefile中增加下面一行:
obj-m +=s3c2440_leds.o
然后在内核的根目录下执行"make modules",就可以生成模块drivers/char/s3c2440_leds.ko.把它放到目标板根文件系统的/lib/modules/2.6.32目录下,就可以使用如下命令加载了
[root@Frankzfz 2.6.32.2]$insmod ./2440_leds.ko
ledsinitialized
|
驱动程序的测试程序#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define IOCTL_LED_ON 0
#define IOCTL_LED_OFF 1
void usage(char * exename)
{
printf("Usage:\n");
printf("%s \n",exename);
printf("led_no = 1 ,2,3 or 4\n");
}
int main(int argc,char ** argv)
{
unsigned int led_no;
int fd = -1;
if(argc !=3)
goto err;
fd = open("/dev/leds",0);//打开设备
if(fd <0)
{
printf("Can't open /dev/leds \n");
return -1;
}
led_no = strtoul (argv[1],0,0) -1; //操作哪个LED灯
if(led_no > 3)
goto err;
if(!strcmp(argv[2],"on"))
{
ioctl(fd,IOCTL_LED_ON,led_no); //点亮它
}
else if(!strcmp(argv[2],"off"))
{
ioctl(fd,IOCTL_LED_OFF,led_no); //熄灭它
}
else
{
goto err;
}
close(fd);
return 0;
err:
if(fd>0)
close(fd);
usage(argv[0]);
return -1;
}
|
把编译生成的可执行程序led_test放入目标板根文件系统/usr/bin目录下后。然后,在目标板根文件系统中如下建立设备文件
现在就可以参照led_test的使用说明操作LED了,以下两条命令点亮熄灭LEDled_test 1 on
led_test 1 off
|
下面是完整的LED驱动的程序:
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/list.h>
#include <linux/pci.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <asm/unistd.h>
#define DEVICE_NAME "leds" // 加载模式后,执行”cat /proc/devices”命令看到的设备名称 也是在mknod /dev/leds 用到的名字
#define LED_MAJOR 231 //主设备号
/* 应用程序执行ioctl(fd, cmd, arg)时的第2个参数 */
#define IOCTL_LED_ON 0
#define IOCTL_LED_OFF 1
/* 用来指定LED所用的GPIO引脚 arch/arm/mach-s3c2410/include/mach/gpio-nrs.h
#define S3C2410_GPB(_nr) (S3C2410_GPIO_A_START + (_nr))*/
static unsigned long led_table [] =
{
S3C2410_GPB(5),
S3C2410_GPB(6),
S3C2410_GPB(7),
S3C2410_GPB(8),
};
/* 用来指定GPIO引脚的功能:输出 arch/arm/mach-s3c2410/include/mach/gpio-nrs.h
#define S3C2410_GPIO_OUTPUT (0xFFFFFFF1)*/
static unsigned int led_cfg_table [] =
{
S3C2410_GPIO_OUTPUT,
S3C2410_GPIO_OUTPUT,
S3C2410_GPIO_OUTPUT,
S3C2410_GPIO_OUTPUT,
};
/* 应用程序对设备文件/dev/leds执行open(...)时,
* 就会调用s3c2440_leds_open函数
*/
static int s3c2440_leds_open(struct inode *inode,struct file *file)
{
int i;
for(i=0;i<4;i++)
{
// 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能
s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
}
return 0;
}
/* 应用程序对设备文件/dev/leds执行ioclt(...)时,
* 就会调用s3c2440_leds_ioctl函数
*/
static int s3c2440_leds_ioctl(struct inode *inode,struct file *file,unsigned int
cmd,unsigned long arg)
{
if(arg>4)
{
return -EINVAL;
}
switch(cmd)
{
case IOCTL_LED_ON:
// 设置指定引脚的输出电平为0
s3c2410_gpio_setpin(led_table[arg],0);
return 0;
case IOCTL_LED_OFF:
// 设置指定引脚的输出电平为1
s3c2410_gpio_setpin(led_table[arg],1);
return 0;
default:
return -EINVAL;
}
}
/* 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
*/
static struct file_operations s3c2440_leds_fops =
{
.owner = THIS_MODULE,/* 这是一个宏,推向编译模块时自动创建的__this_module变量 在文件include/linux/module.h*/
.open = s3c2440_leds_open,//用来初始化LED所用的GPIO引脚
.ioctl = s3c2440_leds_ioctl,//用来根据用户传入的参数设置用户传入的参数设置GPIO的输出电平
};
/*
* 执行“insmod s3c2440_leds.ko”命令时就会调用这个函数
*/
static int __init s3c2440_leds_init(void)
{
int ret;
/* 注册字符设备驱动程序
* 参数为主设备号、设备名字、file_operations结构;
* 这样,主设备号就和具体的file_operations结构联系起来了,
* 操作主设备为LED_MAJOR的设备文件时,就会调用s3c2440_leds_fops中的相关成员函数
* LED_MAJOR可以设为0,表示由内核自动分配主设备号
*/
ret = register_chrdev(LED_MAJOR,DEVICE_NAME,&s3c2440_leds_fops);
if(ret<0)
{
printk(DEVICE_NAME "can't register major number\n");
return ret;
}
printk(DEVICE_NAME "initialized\n");
return 0;
}
/*
* 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数
*/
static void __exit s3c2440_leds_exit(void)
{
/* 卸载驱动程序 */
unregister_chrdev(LED_MAJOR,DEVICE_NAME);
}
/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c2440_leds_init);
module_exit(s3c2440_leds_exit);
/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("Frankzfz"); // 驱动程序的作者
MODULE_DESCRIPTION("S3C2440 LED Driver");// 一些描述信息
MODULE_LICENSE("GPL");
|
阅读(7835) | 评论(0) | 转发(4) |