s3c2440- led驱动分析及其测试程序


***********************************************************************************************************************************

#操作系统:CentOS6.7

#开发板  :fl2440

#cpu        :s3c2440(arm920t)

#编译器 :arm-linux-gcc(buildroot-2012.08)

#linux内核版本:linux-3.0

#开发模块:LED

************************************************************************************************************************************


1.vim 编写LED驱动模块

[zoulei@CentOS ~]$ mkdir LED

[zoulei@CentOS LED]$ vim s3c_led.c

/*********************************************************************************
 *      Copyright:  (C) 2017 zoulei  
 *                  All rights reserved.
 *
 *       Filename:  s3c_led.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(03/25/2017)
 *         Author:  zoulei 
 *      ChangeLog:  1, Release initial version on "03/25/2017 21:03:40 PM"
 *                 
 ********************************************************************************/

#include    //linux内核模块必须包含的头文件
#include      //linux内核模块必须包含的头文件
#include    //printk函数定义在这个头文件中
#include        //struct fops结构体定义
#include     //错误编码(-EDODEV,-ENOMEM)
#include      //struct cdev定义
#include          /* ioremap()  */  
#include     /* request_mem_region() */  

#include       /* Linux kernel space head file for macro _IO() to generate ioctl command  */  
#ifndef __KERNEL__
#include       /* User space head file for macro _IO() to generate ioctl command */  
#endif


#define DRV_AUTHOR                "zoulei"
#define DRV_DESC                  "S3C24XX LED driver"

#define DEV_NAME                  "led"//定义驱动名
#define LED_NUM                   4  //定义led设备的个数


#ifndef LED_MAJOR
#define LED_MAJOR                 0
#endif

#define DRV_MAJOR_VER             1
#define DRV_MINOR_VER             0
#define DRV_REVER_VER             0

#define DISABLE                   0 //禁用某个特性的宏 
#define ENABLE                    1 //使能某个特性的宏 

#define GPIO_INPUT                0x00 //宏定义 GPIO输入模式用0代替  
#define GPIO_OUTPUT               0x01 //宏定义 GPIO输出模式用0代替  


#define PLATDRV_MAGIC             0x60 //宏定义 魔数
#define LED_OFF                   _IO (PLATDRV_MAGIC, 0x18) //
#define LED_ON                    _IO (PLATDRV_MAGIC, 0x19)

#define S3C_GPB_BASE              0x56000010 //查用户手册可知这是s3c2440的GPBCON寄存器基址
#define GPBCON_OFFSET             0         //定义GPBCON寄存器偏移地址  
#define GPBDAT_OFFSET             4         //定义GPBDAT偏移地址
#define GPBUP_OFFSET              8          //定义GPBUP偏移地址
#define S3C_GPB_LEN               0x10        //GPB寄存器的总长度(0x56000010~0x56000020)

int led[LED_NUM] = {5,6,8,10};   //4个led使用GPB5,GPB6,GPB8,GPB10端口

static void __iomem *s3c_gpb_membase;//定义s3c_gpb_membase为void __iomem*类型指针 ,在后面用来保存映射后的虚拟基地址
              /*__iomem是linux2.6.9内核中加入的特性。 
                 这个是用来表示这个指针是指向一个I/O的内存空间。 
                 主要是为了驱动程序的通用性考虑。 
                 由于不同的CPU体系结构对I/O空间的表示可能不同。 
                 当使用__iomem时,编译器会忽略对变量的检查(因为用的是void __iomem)。 
                 若要对它进行检查,当__iomem的指针和正常的指针混用时,就会发出一些警告。 */                

#define s3c_gpio_write(val, reg) __raw_writel((val), (reg)+s3c_gpb_membase)
#define s3c_gpio_read(reg)       __raw_readl((reg)+s3c_gpb_membase)
/*Linux对I/O的操作都定义在asm/io.h中,相应的在arm平台下,就在asm-arm/io.h中。
#define __raw_readl(a)   (__chk_io_ptr(a), *(volatile unsigned int __force   *)(a))
#define __raw_writel(v,a) (__chk_io_ptr(a), *(volatile unsigned int __force   *)(a) = (v))*/

int dev_count = ARRAY_SIZE(led); //宏ARRAY_SIZE,是求设备结构体中设备的个数
                                 /*#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof((arr)[0]) + __must_be_array(arr))
                                   sizeof(arr)/sizeof((arr)[0])是求出设备的个数,
                                      __must_be_array(arr)是防止被吴用,比如用指针而不是数组上。*/

int dev_major = LED_MAJOR; //主设备号定义赋值给dev_major 
int dev_minor = 0;          //次设备定义号赋值0  
int debug = DISABLE;        //出错定义赋值 

static struct cdev      *led_cdev;//定义一个cdev类型的结构体指针,cdev是内核中表示字符设备的一个结构体

static int s3c_hw_init(void)//硬件初始化函数
{
    int          i;
    volatile unsigned long  gpb_con, gpb_dat, gpb_up;

    if(!request_mem_region(S3C_GPB_BASE, S3C_GPB_LEN, "s3c2440 led"))//request_mem_region这个宏是为申请I/O内存的
    {       //#define request_mem_region(start,n,name)   __request_region(&iomem_resource, (start), (n), (name))  
                       //其中,参数start是I/O内存资源的起始物理地址(是CPU在RAM物理地址空间中的物理地址),  
                       //参数n指定I/O内存资源的大小。  
                     //在请求IO内存资源成功后,开始用ioremap进行映射操作.  
        return -EBUSY;
    }//判断是否申请失败,如果硬件资源被占据着则返回-EBUSY.成功则往下执行

    if( !(s3c_gpb_membase=ioremap(S3C_GPB_BASE, S3C_GPB_LEN)) )//ioremap用来将IO资源的物理地址映射到内核虚拟地址空间,
	                                                            //获得寄存器虚拟地址
    {
        release_mem_region(S3C_GPB_BASE, S3C_GPB_LEN);//若映射失败,则释放指定的I/O内存资源。 
        return -ENOMEM;
    }
     //内存申请成功且地址映射成功则进行对各个寄存器的操作
    for(i=0; iprivate_data = (void *)minor;
	/*private_data用于在系统调用期间保存各种状态信息,它是一个void*类型的指针*/  

    printk(KERN_DEBUG "/dev/led%d opened.\n", minor);
    return 0;
}

static int led_release(struct inode *inode, struct file *file)//led灯灭后打印此灯已经灭的函数  
{
    printk(KERN_DEBUG "/dev/led%d closed.\n", iminor(inode));

    return 0;
}

static void print_help(void)//打印提示信息函数  
{
    printk("Follow is the ioctl() commands for %s driver:\n", DEV_NAME);
   
    printk("Turn LED on command  : %u\n", LED_ON);
    printk("Turn LED off command : %u\n", LED_OFF);

    return;
}

static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)//led 中断控制函数
{
    int which = (int)file->private_data;

    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;
}


static struct file_operations led_fops = /*结构体file_operations在头文件 linux/fs.h中定义,
                                         用来存储驱动内核模块提供的对设备进行各种操作的函数指针.  */
                                         /*在这里你可以将你对自己设备要进行的操作赋值给这个结构体中的相关函数指针,
										 则系统将会完成这些动作。*/
{
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
    .unlocked_ioctl = led_ioctl,
};

static int __init s3c_led_init(void) //在内核中初始化led驱动,底层函数.
{
    int                    result;
    dev_t                  devno;

    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); //将主设备号和次设备号转换成dev_t类型  
        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);
    }

    
    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_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_init(led_cdev, &led_fops);

    result = cdev_add(led_cdev, devno, dev_count);//cdev_add为注册设备函数,通常发生在驱动模块的加载函数中,
	                                               //并将返回值传递给result,为0则成功.  
    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;
}

static void __exit s3c_led_exit(void)//内核中注销led驱动的底层函数  
{
    dev_t devno = MKDEV(dev_major, dev_minor);

    s3c_hw_term();

    cdev_del(led_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 ;
}




module_init(s3c_led_init);//用来指定s3c_led模块初始化函数的宏
module_exit(s3c_led_exit);//用来指定s3c_led模块清除函数的宏

module_param(debug, int, S_IRUGO); //声明模块参数debug的宏,指定模块参数,用于在加载模块时或者模块加载以后传递参数给模块
module_param(dev_major, int, S_IRUGO);

MODULE_AUTHOR(DRV_AUTHOR);
MODULE_DESCRIPTION(DRV_DESC);
MODULE_LICENSE("GPL");//
/*小结:

           学习led驱动3,4天了,整体上已经把驱动的每段代码都注释了一遍,然后对led驱动有了宏观的了解,知道每个宏,函数,结构体的意思,不过还是有点知其然,不知其所以然,可能是因为以前没怎么接触过,所以理解起来感觉有点抽象!不过学了这么久还是来分析一下这个驱动的流程吧!!

           其实注册设备驱动的流程无非是1.初始化硬件设备2.注册主次设备号3.定义file_operations 4.在内核中定义cdev,5.在内核中加入定义好的cdev.这五个流程而已!其中先是调用module_init函数,在调用s3c_led_init函数,这个函数会调用 s3c_hw_init进行初始化;接着s3c_led_init函数会进行分配主次设备号,按照传入的参数判断是动态分配或者是静态分配;再者是分配结构体,如果链接owner成功就可接着连接fops;然后是LED_ON 和LED_OFF的实现功能,使用ioctl将装入卸载次设备号驱动以及模块信息和fops与主次设备号结构体进行链接,ioctl通过传参来实现相应的功能;最后进行撤销

          最重要的3个结构体file_operations , file  , inode;  file_operations其实就是连接设备和驱动之间的桥梁,file代表打开的文件描述符,在执行file_operation中的open操作时被创建,当然操作的时候你能同时操作多个文件那么就会存在多个file文件结构,inode中文翻译是“索引节点”,用来存储文件信息的,内核用inode结构来表示文件inode结构中包含了大量的文件信息。

         还有一个比较重要的结构体:struct cdev,linux内核内部使用cdev结构来表示字符设备,在这个驱动模块中,获取了一个独立的led_cdev结构,于是要用cdev_init函数来初始化,值得注意的是,led_cdev结构中有一个所有者字段(owner)也需要初始化,和file_operations结构类似,设置为THIS_MODULE

          了解一下两个宏:__raw_readl(a),__raw_writel

__raw_readl(a)展开是:((void)0, *(volatile unsigned int _force *)(a))。在定义了__CHECKER__的时候先调用__chk_io_ptr检查该地址,否则__chk_io_ptr什么也不做,* (volatile unsigned int _force *)(a)就是返回地址为a处的值。(void)xx的做法有时候是有用的,例如编译器打开了检查未使用的参数的时候需要将没有用到的参数这么弄一下才能 编译通过
         CPU对I/O的物理地址的编程方式有两种:一种是I/O映射,一种是内存映射。 __raw_readl和__raw_writel等是原始的操作I/O的方法,由此派生出来的操作方法有:inb、outb、 _memcpy_fromio、readb、writeb、ioread8、iowrite8等。而linux内核cpu只能访问内存的虚拟地址,因此程序需将内存的物理地址映射成为虚拟地址,这也就是传说中的内存映射!                                                               */                    

2.编写Makefile
[zoulei@CentOS LED]$ vim Makefile
C=/opt/buildroot-2012.08/arm920t/usr/bin/arm-linux-gcc  
KDIR?=~/fl2440/kernel/linux-3.0  
obj-m:=s3c_led.o  
   
default:  
	@$(MAKE) -C $(KDIR) M=`pwd` modules  
	@make clean  
   
clean:
	rm -f *.o *mod.c *.order *.symvers  
*************************************************************************
说明:make会生   .ko  文件 也就是我们所需要加载的驱动模块。
*************************************************************************
[zoulei@CentOS LED]$ ls
Makefile  s3c_led.c  s3c_led.ko

3.编写led测试程序
[zoulei@CentOS ~]$ mkdir LED_test
[zoulei@CentOS LED_test]$ vim  led_test.c
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
   
 
#define DEVNAME_LEN     10  
   
   
   
#define PLATDRV_MAGIC             0x60  
#define LED_OFF                   _IO (PLATDRV_MAGIC, 0x18)  
#define LED_ON                    _IO (PLATDRV_MAGIC, 0x19)  

int main(int argc, char **argv)  
{  
        int fd;  
        int     i=1;  
        
        char devname[DEVNAME_LEN] = {0};  
   
        
          
        snprintf(devname, sizeof(devname), "/dev/led%i",i);  
        fd= open(devname, O_RDWR,755);  
        if(fd< 0)  
          {  
               printf("Can not open %s: %s", devname, strerror(errno));           
               close(fd);  
               return -1;  
          }  
          
        ioctl(fd, LED_ON);  
        sleep(5);
        ioctl(fd,LED_OFF);
        close(fd);    
        return 0;  
}  

************************************************************************************************************************************************************
说明:1.sprintf,snprintf的区别 sprintf函数返回的是实际输出到字符串缓冲中的字符个数,包括null结束符。而snprintf函数返回的是应该输出到字符串缓冲的字符个数,所以snprintf的返回值可能大于给定的可用缓冲大小以及最终得到的字符串长度,因为sprintf可能导致缓冲区溢出问题而不被推荐使用,所以我们通常使用snprintf。更多关于sprintf与snprintf的区别我觉得这个人分析的还不错,可以参考一下:https://my.oschina.net/shelllife/blog/177279
 而printf是打印输出到屏幕上!
            2.代码中我们定义了一个魔数字的宏,#define  PLATDRV_MAGIC  0x60 ,其实那个魔数字是在linux内核中寻找一个没有冲突的数字,然后__IO_(PLATDRV_MAGIC 0x18)这个宏就是将魔数字与0x18作某种逻辑运算,生成一个独一无二的的数字LED_OFF,这么做的目的是防止ioctl命令字与别的驱动冲突,所以说ioctl()函数中的第二个参数不是随便给定的
*****************************************************************************************************************************************************************************************

[zoulei@CentOS LED_test]$  /opt/buildroot-2012.08/arm920t/usr/bin/arm-linux-gcc led_test.c

[zoulei@CentOS LED_test]$ ls
a.out  led_test.c
***************************************************************************************************************************************************

说明:应用程序使用交叉编译器编译得到a.out。在我们的开发板上将  .ko文件和 a.out 文件 tftp 过去。这是板子上会有我们这两个文件,注意这两个文件的权限需要改成可执行。insmod 文件名.ko  就可以看到我们的主设备号。  如major=251  

************************************************************************************************************************************************

4.下载文件到开发板上执行

>: tftp -gr a.out 192.168.1.155
a.out                100% |*******************************|  5645   0:00:00 ETA

>: tftp -gr s3c_led.ko 192.168.155

s3c_led.ko           100% |*******************************|  6210   0:00:00 ETA

>: insmod s3c_led.ko  //加载模块
S3C led driver[major=251] version 1.0.0 installed successfully!

>:  mknod /dev/led0 c 251 0//手动创建节点
>: ls -l dev/led*

crw-r--r--    1 root     root      251,   0 Dec 31 17:13 dev/led0
>: mknod /dev/led1 c 251 1
>: mknod /dev/led2 c 251 2
>: mknod /dev/led3 c 251 3
>: ls -l dev/led*

crw-r--r--    1 root     root      251,   0 Dec 31 17:13 dev/led0
crw-r--r--    1 root     root      251,   1 Dec 31 17:13 dev/led1
crw-r--r--    1 root     root      251,   2 Dec 31 17:14 dev/led2
crw-r--r--    1 root     root      251,   3 Dec 31 17:14 dev/led3

>: ll a.out
-rwxrwxrwx    1 root     root          5743 Dec 31 17:38 a.out

>: chmod 777 a.out

>: ./a.out

s3c2440- led驱动分析及其测试程序_第1张图片

s3c2440- led驱动分析及其测试程序_第2张图片


加载s3c_led.ko模块后,开发板的四个led灯默认是全灭的,运行a.out之后,开发板上的的led1灯亮了5秒之后又灭了,当然我的程序是这么设计的!


遇到的问题


原因是权限不够!

解决方法:

                 >: chmod 777 a.out

                 >: ll a.out
               -rwxrwxrwx    1 root     root          5743 Dec 31 17:38 a.out







你可能感兴趣的:(linux驱动学习)