arm9+linux s3c2440 led 驱动s3c_led.c 解析及运行过程

一、led的驱动代码:

#include <linux/module.h>   /* Every Linux kernel module must include this head */
#include <linux/init.h>     /* Every Linux kernel module must include this head */
#include <linux/kernel.h>   /* printk() */
#include <linux/fs.h>       /* struct fops */
#include <linux/errno.h>    /* error codes */
#include <linux/cdev.h>     /* cdev_alloc()  */
#include <asm/io.h>         /* ioremap()  */
#include <linux/ioport.h>   /* request_mem_region() */
#include <asm/ioctl.h>      /* Linux kernel space head file for macro _IO() to generate ioctl command  */
#ifndef __KERNEL__
#include <sys/ioctl.h>      /* User space head file for macro _IO() to generate ioctl command */
#endif
//#include <linux/printk.h> /* Define log level KERN_DEBUG, no need include here */
#define DRV_AUTHOR                "shaocongshuai" 
#define DRV_DESC                  "S3C24XX LED driver"
#define DEV_NAME                  "led"
#define LED_NUM                   4

/* Set the LED dev major number */
//#define LED_MAJOR                 79
#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
#define GPIO_OUTPUT               0x01
//关于iotrl cmd __IO http://blog.csdn.net/ghostyu/article/details/8085693
#define PLATDRV_MAGIC             0x60
#define LED_OFF                   _IO (PLATDRV_MAGIC, 0x18) 
#define LED_ON                    _IO (PLATDRV_MAGIC, 0x19)

#define S3C_GPB_BASE              0x56000010
#define GPBCON_OFFSET             0
#define GPBDAT_OFFSET             4
#define GPBUP_OFFSET              8
#define S3C_GPB_LEN               0x10        /* 0x56000010~0x56000020  */

int led[LED_NUM] = {5,6,8,10};  /* Four LEDs use GPB5,GPB6,GPB8,GPB10 */

static void __iomem *s3c_gpb_membase;
      __iomem是Linux2.6.9内核中加入的特性。是用来个表示指针指向一个I/O的内存空间。主要是为了驱动程序的通用性考虑。由于不同的CPU体系结构对I/O空间的表示可能不同。当使用__iomem时,编译器会忽略对变量的检查(因为使用的是void __iomem).但sparse会对它进行检查,当__iomem的指针和正常的指针混用时,就会发出一些warnings.

#define s3c_gpio_write(val, reg) __raw_writel((val), (reg)+s3c_gpb_membase)
#define s3c_gpio_read(reg)       __raw_readl((reg)+s3c_gpb_membase)
       __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_writel 和__raw_readl等是原始的操作I/O的方法,由此派生出来的操作方法有很多。

int dev_count = ARRAY_SIZE(led);
/* 宏ARRAY_SIZE,是求设备结构体中设备的个数,定义在include/linux/kernel.h中  #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;
int dev_minor = 0;
int debug = DISABLE;

static struct cdev      *led_cdev;

static int s3c_hw_init(void)
{
    int          i;
    volatile unsigned long  gpb_con, gpb_dat, gpb_up;
    举例说明:
        volatile int i=10; 
    int j = i; 
    ... 
        int k = i; 

    volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的可执行码会重新从i的地址读取数据放在k中。 

        volatile 影响编译器编译的结果,指出,volatile 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错,(VC++ 在产生release版可执行码时会进行编译优化,加volatile关键字的变量有关的运算,将不进行编译优化。)。

        其中编译器编译优化是:
        由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在k中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问,不会出错。

    if(!request_mem_region(S3C_GPB_BASE, S3C_GPB_LEN, "s3c2440 led")) 
    {
        用于申请I/O内存用的。申请之后,还需要使用ioremap或者ioremap_nocache函数来映射.三个参数start,n,name表示你想使用从start开始的size为N的I/Oport资源,name就是你的名字。
        return -EBUSY; 这是一种标准的错误值,表示“设备正忙或资源忙”错误。
    }

    if( !(s3c_gpb_membase=ioremap(S3C_GPB_BASE, S3C_GPB_LEN)) )
    {
         1.要映射的I/O空间的起始地址.2.要映射空间的大小.功能:将一个I/O地址空间映射到内核的虚拟地址空间去,便于访问;
        release_mem_region(S3C_GPB_BASE, S3C_GPB_LEN);
        return -ENOMEM;
    }

    for(i=0; i<dev_count; i++)
    {
        /*Set GPBCON register, set correspond GPIO port as input or output mode */ 
        gpb_con = s3c_gpio_read(GPBCON_OFFSET);
        gpb_con &= ~(0x3<<(2*led[i]));   /* Clear the currespond LED GPIO configure register */
        gpb_con |= GPIO_OUTPUT<<(2*led[i]); /* Set the currespond LED GPIO as output mode */
        s3c_gpio_write(gpb_con, GPBCON_OFFSET);

        /* Set GPBUP register, set correspond GPIO port pull up resister as enable or disable  */
        gpb_up = s3c_gpio_read(GPBUP_OFFSET);
        //gpb_up &= ~(0x1<<led[i]); /* Enable pull up resister */
        gpb_up |= (0x1<<led[i]);  /* Disable pull up resister */
        s3c_gpio_write(gpb_up, GPBUP_OFFSET);

        /* Set GPBDAT register, set correspond GPIO port power level as high level or low level */
        gpb_dat = s3c_gpio_read(GPBDAT_OFFSET);
        //gpb_dat &= ~(0x1<<led[i]); /* This port set to low level, then turn LED on */
        gpb_dat |= (0x1<<led[i]);  /* This port set to high level, then turn LED off */
        s3c_gpio_write(gpb_dat, GPBDAT_OFFSET);
    }

    return 0;
}

static void turn_led(int which, unsigned int cmd)
{
    volatile unsigned long  gpb_dat;

    gpb_dat = s3c_gpio_read(GPBDAT_OFFSET);

    if(LED_ON == cmd)
    {
        gpb_dat &= ~(0x1<<led[which]); /*  Turn LED On */
    }
    else if(LED_OFF == cmd)
    {
        gpb_dat |= (0x1<<led[which]);  /*  Turn LED off */
    }

    s3c_gpio_write(gpb_dat, GPBDAT_OFFSET);
}

static void s3c_hw_term(void)
{
    int                     i;
    volatile unsigned long  gpb_dat;

    for(i=0; i<dev_count; i++)
    {
        gpb_dat = s3c_gpio_read(GPBDAT_OFFSET);
        gpb_dat |= (0x1<<led[i]);  /* Turn LED off */
        s3c_gpio_write(gpb_dat, GPBDAT_OFFSET);
    }

    release_mem_region(S3C_GPB_BASE, S3C_GPB_LEN);
    iounmap(s3c_gpb_membase);
}

static int led_open(struct inode *inode, struct file *file)
{
    int minor = iminor(inode);

    file->private_data = (void *)minor;

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

static int led_release(struct inode *inode, struct file *file)
{
    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("Enable Driver debug command: %u\n", SET_DRV_DEBUG);
    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)
{
    int which = (int)file->private_data;
     定义在 include/linux/fs.h文件中。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。
   void*private_data; open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它;你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在release 方法中释放那个内存. 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在头文件include/linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。驱动内核模块是不需要实现每个函数的。你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。指向结构体struct file_operations的指针通常命名为fops。
    .owner = THIS_MODULE,
     它是指向拥有这个结构模块的指针,这个成员用来在它的操作还在被使用时阻止模块被加载。几乎所用的时间中,它被简单初始化为THIS_MODULE,定义在include/linux/module.h中
    .open = led_open,
     尽管这常常是对设备进行的第一个操作,不要求驱动声明一个对应的方法。如果这个项是NULL,设备一直打开成功,但是你的驱动不会得到通知。
    .release = led_release,
     在文件结构释放时引用这个操作,如同open,release可以为NULL。
    .unlocked_ioctl = led_ioctl,
     ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读也不是写).另外,几个ioctl命令被内核识别而不必引用fops表,如果设备不提供Ioctl方法,对于任何事先定义的要求(-ENOTTY,”设备无这样的ioctl"),系统调用返回一个错误
};

static int __init s3c_led_init(void) 
     __init的属性标志,是要把这种属性的代码放入到目标文件的.init.text节,这一过程是通过编译内核时为为相关目标平台提供了xxx.lds的连接脚本来着指导ld完成的.对编译成module的代码和数据来说,当module加载时,__init的属性的函数被执行.在初始化完成后,用这些关键字标识的函数或数据所占的内存和数据会被释放掉。所有标识为__init的函数在链接时都被放在.init.text这个区段内,在这个区段内,函数的摆放顺序和链接的顺序有关,是不确定。所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化内核或通过这些函数指针来调用这些__init函数指针,并在整个初始化完成后释放整个init区段(包括.init.text,.initcall.init等),注意这些函数的在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,和上段中所描述的这些函数本身在.init.text区段的顺序无关。
{
    int                    result;
    dev_t                  devno; 
    unsigned int 类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号. 你在/dev目录下,用命令ll就可以看到那些设备文件的主次设备号.在程序中用宏MAJOR(dev_t dev)可以解析出主设备号,用宏MINOR(dev_t dev)可以解析出次设备号

    if( 0 != s3c_hw_init() )
    {
        printk(KERN_ERR "s3c2440 LED hardware initialize failure.\n");
    内核通过 printk() 输出的信息具有日志级别,日志级别是通过在 printk() 输出的字符串前加一个带尖括号的整数来控制的,如 printk("<6>Hello, world!/n");。内核中共提供了八种不同的日志级别,在 /include/linux/printk.h 中有相应的宏对应。

#define KERN_EMERG    "<0>"      system is unusable 
#define KERN_ALERT    "<1>"      action must be taken immediately 
#define KERN_CRIT     "<2>"      critical conditions 
#define KERN_ERR      "<3>"      error conditions 
#define KERN_WARNING  "<4>"      warning conditions 
#define KERN_NOTICE   "<5>"      normal but significant
#define KERN_INFO     "<6>"      informational 
#define KERN_DEBUG    "<7>"      debug-level messages 
        
    所以 printk() 可以这样用:printk(KERN_INFO "Hello, world!/n");。
    未指定日志级别的 printk() 采用的默认级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在 kernel/printk.c 中被定义为整数 4,即对应KERN_WARNING。 在 /kernel/printk 会显示4个数值(可由 echo 修改),分别表示当前控制台日志级别、未明确指定日志级别的默认消息日志级别、最小(最高)允许设置的控制台日志级别、引导时默认的日志级别。当 printk() 中的消息日志级别小于当前控制台日志级别时,printk 的信息(要有/n符)就会在控制台上显示。但无论当前控制台日志级别是何值,通过 使用dmesg总能查看。另外如果配置好并运行了 syslogd 或 klogd,没有在控制台上显示的 printk 的信息也会追加到 /var/log/messages 中。

       return -ENODEV;

     linux驱动中的ENODEV是默认尚未分配到具体设备的意思。如果程序有一个打开的设备句柄,在当前结构里,我们只要把它赋值为空,就像它已经消失了。对于每一次设备读写等其它函数操作,我们都要检查结构是否存在。
 如果不存在,就表明设备已经消失,并返回一个-ENODEV错误给用户程序。ENODEV 应该是默认尚未分配到具体设备的意思。
    }

    /*  Alloc the device for driver */
    if (0 != dev_major) /*  Static */
    {
        devno = MKDEV(dev_major, 0); //通过主次设备号来生成dev_t
        result = register_chrdev_region (devno, dev_count, DEV_NAME);
        注册一组字符设备编号。1,要分配的设备编号范围的初始值(次设备号常设为0)2,连续编号范围, 3,编号相关联的设备名称。
    }
    else
    {
        result = alloc_chrdev_region(&devno, dev_minor, dev_count, DEV_NAME);
        动态的申请设备编号范围,这个函数好像并没有检查范围过大的情况,不过动态分配总是找个空的散列桶,所以问题也不大。通过指针参数返回实际获得的起始设备编号。 
        dev_major = MAJOR(devno);
    }

    /*  Alloc for device major failure */
    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()) )
    {
        printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
        unregister_chrdev_region(devno, dev_count);
        return -ENOMEM;
    }

    linux 内核中每个字符设备都对应一个cdev的结构变量,在include/linux/cdev.h中。一个cdev一般它有两种定义初始化方式:静态的和动态的。
        
    led_cdev->owner = THIS_MODULE;
    cdev_init(led_cdev, &led_fops);

    result = cdev_add(led_cdev, devno, dev_count);

    初始化cdev后,需要把它添加到系统中去。为此可以调用cdev_add()函数。传入cdev结构的指针,起始设备编号,以及设备编号范围。

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

/* These two functions defined in <linux/init.h> */
module_init(s3c_led_init);
module_exit(s3c_led_exit);

module_param(debug, int, S_IRUGO);
     module_param为内核模块传递参数。使用了3个参数:变量名,它的类型,以及一个权限掩码。这个宏定义应当放在任何函数之外,典型的是出现在源文件的前面.在include/linux/stat.h 中定义  #define S_IRUGO     (S_IRUSR|S_IRGRP|S_IROTH)
module_param(dev_major, int, S_IRUGO);
MODULE_AUTHOR(DRV_AUTHOR);
MODULE_DESCRIPTION(DRV_DESC);

MODULE_LICENSE("GPL");


二、编写应用程序和加载驱动:

编译驱动:

[shaocongshuai@localhost led]$ make
make -C /home/shaocongshuai/fl2440/kernel/linux-3.0.2 M=/home/shaocongshuai/led modules
make[1]: Entering directory `/home/shaocongshuai/fl2440/kernel/linux-3.0.2'
make[2]: Warning: File `/home/shaocongshuai/led/s3c_led.c' has modification time 2.3e+06 s in the future
  CC [M]  /home/shaocongshuai/led/s3c_led.o
make[2]: warning:  Clock skew detected.  Your build may be incomplete.
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/shaocongshuai/led/s3c_led.mod.o
  LD [M]  /home/shaocongshuai/led/s3c_led.ko
make[1]: Leaving directory `/home/shaocongshuai/fl2440/kernel/linux-3.0.2'
make clean
make[1]: Entering directory `/home/shaocongshuai/led'
rm -f *.o *.order *.symvers *.mod.c
make[1]: Leaving directory `/home/shaocongshuai/led'

编译驱动的Makefile:

  1 obj-m := s3c_led.o
  2 
  3 LINUX_SRC ?= /home/shaocongshuai/fl2440/kernel/linux-3.0.2
  4 CROSS_COMPILE = /opt/buildroot-2012.08/arm920t/usr/bin/arm-linux-
  5 PWD := $(shell pwd)
  6 
  7 modules:
  8     make -C $(LINUX_SRC) M=$(PWD) modules
  9     make clean
 10 
 11 clean:
 12     rm -f *.o *.order *.symvers *.mod.c

编写测试文件led.c

  1 /*********************************************************************************
  2  *      Copyright:  (C) 2016 lingyun
  3  *                  All rights reserved.
  4  *
  5  *       Filename:  led.c
  6  *    Description:  This file 
  7  *        Version:  1.0.0(05/11/2016)
  8  *         Author:  shaocongshuai <[email protected]>
  9  *      ChangeLog:  1, Release initial version on "05/11/2016 02:54:33 AM"
 10  *                 
 11  ********************************************************************************/
 12 
 13 #include <stdio.h>
 14 #include <sys/ioctl.h>
 15 #include <sys/types.h>
 16 #include <sys/stat.h>
 17 #include <fcntl.h>
 18 #include <unistd.h>
 19 
 20 #define LED_NUM 4       
 21 #define DEVNAME_LEN 10          
 22 
 23 #define PLATDRV_MAGIC   0x60            
 24 #define LED_OFF                   _IO (PLATDRV_MAGIC, 0x18)
 25 #define LED_ON                    _IO (PLATDRV_MAGIC, 0x19)
 26 /********************************************************************************
 27  *  Description:
 28  *   Input Args:
 29  *  Output Args:
 30  * Return Value:
 31  ********************************************************************************/
 32 int main (int argc, char **argv)
 33 {
 34     int     i = 0;
 35     int     fd[LED_NUM];
 36     char    devname[DEVNAME_LEN];

 37 
 38 
 39     for ( i=0; i<LED_NUM; i++)
 40     {
 41         snprintf(devname, sizeof(devname), "/dev/led%d", i);
 42         fd[i] = open(devname, O_RDWR);
 43         if ( fd[i] < 0 )
 44         {
 45             goto err;
 46         }
 47     }
 48 
 49     while(1)
 50     {
 51         for ( i=0; i<LED_NUM; i++)
 52         {
 53             ioctl(fd[i], LED_ON);
 54             sleep(1);
 55             ioctl(fd[i], LED_OFF);
 56             sleep(1);
 57         }
 58    }
 59 
 60     for ( i=0; i<LED_NUM; i++)
 61     {
 62         close(fd[i]);
 63     }
 64     return 0;
 65 
 66 err:
 67     for ( i=0; i<LED_NUM; i++)
 68     {
 69         close(fd[i]);
 70     }
 71     return -1;
 72 } /* ----- End of main() ----- */

[shaocongshuai@localhost led]$ /opt/buildroot-2012.08/arm920t/usr/bin/arm-linux-gcc led.c -o led

[shaocongshuai@localhost led]$ ls
led  led.c  Makefile  s3c_led.c  s3c_led_driver.sh  s3c_led.ko

然后用自己的交叉编译器arm-linux-gcc编译该c文件会得到一个led的led文件,使用tftp服务器把它送到开发板,然后使用chmod 777 led(解权限) 
在开发板下执行./led即可.

由于在该驱动文件中使用的是动态获取主设备号:

动态获取主设备号 result=alloc_chrdev_region(&devno, dev_minor, dev_count, DEV_NAME); dev_major = MAJOR(devno);

所以在安装驱动之前并不知道内核给我们分配哪一个主设备号,故只有在安装该驱动之后才能在/dev/目录下创建设备节点。因此,为了加载一个使用动态获取主设备号的设备驱动程序,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件。 
该s3c_led_driver.sh脚本文件如下:

  1 #!/bin/sh
  2 
  3 tftp -gr s3c_led.ko 192.168.1.93
  4 tftp -gr led 192.168.1.93
  5 
  6 insmod s3c_led.ko
  7 major=`cat /proc/devices | grep led | cut -d ' ' -f 1`
  8 mknod -m 755 /dev/led0 c $major 0
  9 mknod -m 755 /dev/led1 c $major 1
 10 mknod -m 755 /dev/led2 c $major 2
 11 mknod -m 755 /dev/led3 c $major 3
 12 
 13 chmod 777 led
 14 ./led

选项 

-m mode, --mode=mode 

为新建立的文档设定模式,就象应用命令chmod相同,以后仍然使用缺省模式建立新目录。 缺省地,所产生的文档模式为0666('a+rw') 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章