前两天对helloworld驱动进行了学习,现在正式进入到FL2440驱动的学习。依然按照程序进阶的step,现在对led驱动做一个分析总结。Led设备属于字符设备,字符设备驱动程序适合于大多数简单的硬件设备,因此作为驱动入门学习起来更容易理解和掌握。
分析一个驱动代码,首先应该找到驱动初始化函数,在helloworld驱动中即hello_init(void)函数,在本例程中即s3c_led_init()函数,因为在内核加载时,首先执行的就是该函数。
static int __init s3c_led_init(void)//led初始化函数
{
int result;
dev_t devno;
/*如果硬件初始化失败,打印如下信息并返回-ENODEV*/
if( 0 != s3c_hw_init() )
{
printk(KERN_ERR "s3c2440 LED hardware initialize failure.\n");
return -ENODEV;
}
/* 如果硬件初始化成功,分配主次设备号 */
if (0 != dev_major) /* 如果已经有了设备号 静态获取主次设备号 */
{
devno = MKDEV(dev_major, 0);
result = register_chrdev_region (devno, dev_count, DEV_NAME);
}
else//动态获取主次设备号
{
result = alloc_chrdev_region(&devno, dev_minor, dev_count, DEV_NAME);
dev_major = MAJOR(devno);
}
/* 如果设备号申请失败,打印如下信息并返回-ENODEV */
if (result < 0)
{
printk(KERN_ERR "S3C %s driver can't use major %d\n", DEV_NAME, dev_major);
return -ENODEV;
}
printk(KERN_DEBUG "S3C %s driver use major %d\n", DEV_NAME, dev_major);
if(NULL == (led_cdev=cdev_alloc()) )//分配cdev结构体,如果分配失败,打印如下信息
{
printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
unregister_chrdev_region(devno, dev_count);
return -ENOMEM;
}
led_cdev->owner = THIS_MODULE;//如果分配成功,将cdev结构体添加进内核
cdev_init(led_cdev, &led_fops);//连接led_cdev和led_fops
/*只有当cdev_add()函数执行之后,才能在 /dev/目录下看到设备节点*/
result = cdev_add(led_cdev, devno, dev_count);//将其添加进内核,字符设备注册的最后一步
if (0 != result)
{
printk(KERN_INFO "S3C %s driver can't reigster cdev: result=%d\n", DEV_NAME, result);
goto ERROR;
}
printk(KERN_ERR "S3C %s driver[major=%d] version %d.%d.%d installed successfully!\n",
DEV_NAME, dev_major, DRV_MAJOR_VER, DRV_MINOR_VER,DRV_REVER_VER);
return 0;
ERROR:
printk(KERN_ERR "S3C %s driver installed failure.\n", DEV_NAME);
cdev_del(led_cdev);
unregister_chrdev_region(devno, dev_count);
return result;
}
由该函数不难看出驱动初始化的过程分为5个阶段,即硬件初始化->主次设备号获取->分配cdev结构体->初始化cdev结构体->激活驱动
这里使用s3c_led_init()函数对 led 的相关寄存器进行配置,稍后会分析到。
设备号可以有动态获取和静态获取两种方式,为了与现有设备号冲突,我们一般使用动态获取的方式,上面用到的函数为:alloc_chrdev_region(&devno, dev_minor,dev_count, DEV_NAME);
Cdev被称之为内核与设备的接口,内核中每一个设备都对应一个cdev结构体变量,cdev有静态初始化和动态初始化两种初始化方式,
静态初始化:
struct cdev led_cdev;
cdev_init(&led_cdev, &fops);
led_cdev.owner = THIS_MODULE;
动态初始化:
struct cdev *led_cdev = cdev_alloc()
led_cdev->ops = &fops;
led_cdev->owner = THIS_MODULE;
4. 申请cdev,与fops建立链接
5.使用cdev_add()函数激活驱动
只要调用cdev_add函数并且返回值不为0,我们的设备就『激活』了。它的操作就可以被内核调用。因此,cdev_add()函数一定是驱动初始化的最后执行的函数。
2.s3c_led_exit(void)函数
s3c_led_exit()执行功能与s3c_led_init()函数的功能完全相反。该函数主要的功能为卸载驱动,释放内存,释放设备节点和设备号。
static void __exit s3c_led_exit(void)//led卸载函数
{
dev_t devno = MKDEV(dev_major, dev_minor);
s3c_hw_term();//释放。。。
cdev_del(led_cdev);//删掉cdev
unregister_chrdev_region(devno, dev_count);//释放设备节点号
printk(KERN_ERR "S3C %s driver version %d.%d.%d removed!\n",
DEV_NAME, DRV_MAJOR_VER, DRV_MINOR_VER,DRV_REVER_VER);
return ;
}
介绍该函数之前,先来分析一下什么叫做系统调用。
在Linux系统进程中,分为内核空间和用户空间,当一个任务(进程)执行系统调用而进入内核代码中执行时,我们就称进程处于内核运行态(内核态)。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。用户态不能访问内核空间,包括代码和数据。如果内核要访问用户空间的设备或者文件,必须通过系统调用open去打开该设备,通知内核新建一个代表该文件的结构,并且返回该文件的描述符(一个整数),该描述符在进程内唯一。
下面看看led_open()函数的真面目
static int led_open(struct inode *inode, struct file *file)//"打开led"函数
{
int minor = iminor(inode);//获取次设备号
file->private_data = (void *)minor;//在file结构体中,private_data是一个空类型指针
printk(KERN_DEBUG "/dev/led%d opened.\n", minor);
return 0;
}
该函数在这实现的功能比较简单,仅仅是获取设备的次设备号。
大部分驱动除了需要具备读写设备的能力之外,还需要具备对硬件控制的能力。在用户空间,使用ioctl系统调用来控制设备。Ioctl函数通过传入命令告诉驱动需要实现的功能,led_ioctl就是通过用户空间系统调用函数ioct操作实现对led灯亮灭的控制。
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)//参数为用户程序空间传过来的参数
{
int which = (int)file->private_data;//获取次设备号
/*判断并执行相应的命令,如果传来的命令为LED_ON,则打开LED灯,如果传来的命令为LED_OFF,则关闭LED灯*/
switch (cmd)
{
case LED_ON:
turn_led(which, LED_ON);
break;
case LED_OFF:
turn_led(which, LED_OFF);
break;
default:
printk(KERN_ERR "%s driver don't support ioctl command=%d\n", DEV_NAME, cmd);
print_help();
break;
}
return 0;
}
在分配了主次设备号之后,使用file_operations结构体来建立驱动程序操作和分配的设备号之间的链接。该结构体定义在linux/fs.h头文件中。该结构体中每一个字段都必须指向驱动程序中实现特定操作的函数。
static struct file_operations led_fops = //定义fop结构体,针对该驱动提供的系统调用和操作
{
.owner = THIS_MODULE,//.owener的值一般为THIS_MODULE
.open = led_open,//open为指向led_open()的函数指针,调用open()函数时会用到
.release = led_release,
.unlocked_ioctl = led_ioctl,
};
硬件初始化函数比较容易理解,相信任何有单片机基础的朋友对照着datasheet应该都能看懂。这里需要强调的是linux系统对于寄存器的操作和普通单片机操有所不同,linux系统有MMU即内存管理单元,所以操作的是虚拟地址。因此在操作寄存器的时候我们尤其要注意虚拟地址到物理地址的映射问题,普通单片机不存在MMU,直接操作物理地址就可以。
static int s3c_hw_init(void)//硬件初始化,设置相应GPIO口为output模式
{
int i;
volatile unsigned long gpb_con, gpb_dat, gpb_up;
/*为s3c2440_led申请一片物理地址,其起始地址为S3C_GPB_BASE,大小为s3c_GPB_LEN;如果申请失败,返回-EBUSY */
if(!request_mem_region(S3C_GPB_BASE, S3C_GPB_LEN, "s3c2440 led"))
{
return -EBUSY;
}
/*如果物理地址申请成功,将其映射到相应的虚拟地址。以后操作寄存器一律操作虚拟地址即s3c_gpb_membase的地址*/
if( !(s3c_gpb_membase=ioremap(S3C_GPB_BASE, S3C_GPB_LEN)) )//ioremap函数作用为将物理地址映射到虚拟地址
{
release_mem_region(S3C_GPB_BASE, S3C_GPB_LEN);//如果映射失败,一定要将物理地址释放
return -ENOMEM;//申请失败返回错误信息-ENOMEM
}
for(i=0; i
为了方便控制led的亮灭,我们用turn_led(int witch,unsigned int cmd)函数实现。在单片机中也经常用这种函数控制led亮灭,是不是比直接操作寄存器简单易懂多了。
static void turn_led(int which, unsigned int cmd)//操作led
{
volatile unsigned long gpb_dat;
gpb_dat = s3c_gpio_read(GPBDAT_OFFSET);
if(LED_ON == cmd)
{
gpb_dat &= ~(0x1<
为了节约资源,我们在使用了LED驱动之后应该释放之前分配的物理内存并且取消物理地址与虚拟地址的映射关系。
static void s3c_hw_term(void)//清除led操作
{
int i;
volatile unsigned long gpb_dat;
for(i=0; i
到这里led驱动的主要函数都分析完了。