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') 

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