Linux 4.33 字符设备驱动分析

 Linux的外设可以分为三类:字符设备(characterdevice)、块设备(block device)和网络接口(networkinterface)。在寒假期间,我着重研究了一下字符设备驱动。

    字符设备是能够像字节流(比如文件)一样被访问的设备,对它的访问是以字节为单位的。例如利用串口收发数据时就是一个一个字节进行的,在编写驱动程序时,我们可以在驱动程序中设置缓冲区来存放数据以提高效率,但是串口本身并无此要求。字符设备驱动程序中运用到了open 、close、read、write等系统调用,应用程序可以通过设备文件来访问字符设备。

    在寒假期间,我着重看了一下简单的LED字符设备驱动,以三星的开发板S3c24xx为例的LED驱动。首先是模块的初始化和卸载函数:

 static int __init leds_init(void)  

{           

leds_class = class_create(THIS_MODULE, "leds");   

    if (IS_ERR(leds_class))           

    return PTR_ERR(leds_class);         

leds_class->suspend = led_suspend;         

   leds_class->resume = led_resume;        

   leds_class->dev_attrs = led_class_attrs;     

    return 0;  

 }    

  static void __exit leds_exit(void)   

{        

class_destroy(leds_class);   

}     

subsys_initcall(leds_init);  

module_exit(leds_exit);  

在初始化函数主要做了一件事情,那就是调用class_create函数注册了一个class。

然后是led_classdev_register():

/**

  *led_classdev_register - register a new object of led_classdev class.

  * @parent: Thedevice to register.

  * @led_cdev: theled_classdev structure for this device.

  */

 intled_classdev_register(struct device *parent, struct led_classdev *led_cdev)

 {

        led_cdev->dev = device_create(leds_class, parent, 0, led_cdev,

                                      "%s", led_cdev->name);

         if(IS_ERR(led_cdev->dev))

                 return PTR_ERR(led_cdev->dev);

 

#ifdef CONFIG_LEDS_TRIGGERS

        init_rwsem(&led_cdev->trigger_lock);

 #endif

         /* add tothe list of leds */

        down_write(&leds_list_lock);

        list_add_tail(&led_cdev->node, &leds_list);

        up_write(&leds_list_lock);

 

         if(!led_cdev->max_brightness)

                led_cdev->max_brightness = LED_FULL;

 

        led_update_brightness(led_cdev);

 

        init_timer(&led_cdev->blink_timer);

        led_cdev->blink_timer.function = led_timer_function;

        led_cdev->blink_timer.data = (unsigned long)led_cdev;

 

 #ifdefCONFIG_LEDS_TRIGGERS

        led_trigger_set_default(led_cdev);

#endif

 

        printk(KERN_DEBUG "Registered led device: %s\n",

                         led_cdev->name);

 

         return 0;

}

EXPORT_SYMBOL_GPL(led_classdev_register);

在led_classdev_register函数中主要做了以下一些事情:
1.在2.6版本内核中是自动在/dev目录下创建设备节点,而在4.33中最后调用了device_register函数向系统注册了一个struct device(可以在/sys/class/leds/目录下找到这个设备,比如lcd背光驱动的lcd-backlight)。
2. 加入led_cdev加入到链表leds_list中。
3. 调用led_update_brightness函数更新led亮度值。
4. 调用init_timer初始化一个定时器。

 

/**

  *led_classdev_unregister - unregisters a object of led_properties class.

  *@led_cdev: the led device to unregister

  *

  *Unregisters a previously registered via led_classdev_register object.

 

  */

void led_classdev_unregister(structled_classdev *led_cdev)

{

#ifdef CONFIG_LEDS_TRIGGERS

        down_write(&led_cdev->trigger_lock);

        if (led_cdev->trigger)

                 led_trigger_set(led_cdev,NULL);

        up_write(&led_cdev->trigger_lock);

#endif

 

       /* Stop blinking */

        led_brightness_set(led_cdev, LED_OFF);

 

        device_unregister(led_cdev->dev);

 

        down_write(&leds_list_lock);

        list_del(&led_cdev->node);

        up_write(&leds_list_lock);

}

EXPORT_SYMBOL_GPL(led_classdev_unregister);

 

 

led_classdev_unregister()函数的主要作用是注销led的这个class。 down_write(&led_cdev->trigger_lock)用于关闭led。然后调用device_unregister注销设备。list_del(&led_cdev->node)来从链表leds_list中删除节点。

 

 

 

在Linux2.66中,模块初始化和卸载函数如下:

/*

 * 执行insmod命令时就会调用这个函数

 */

static int __init s3c24xx_leds_init(void)

//static int __initinit_module(void)

 

{

    int ret;

    int minor = 0;

 

    gpio_va = ioremap(0x56000000, 0x100000);

    if (!gpio_va) {

        return -EIO;

    }

 

    /* 注册字符设备

     * 参数为主设备号、设备名字、file_operations结构;

     * 这样,主设备号就和具体的file_operations结构联系起来了,

     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数

     * LED_MAJOR可以设为0,表示由内核自动分配主设备号

     */

    ret = register_chrdev(LED_MAJOR,DEVICE_NAME, &s3c24xx_leds_fops);

    if (ret < 0) {

      printk(DEVICE_NAME " can't registermajor number\n");

      return ret;

    }

  printk(DEVICE_NAME "initialized\n");

    return 0;

}

/*

 * 执行rmmod命令时就会调用这个函数

 */

static void __exit s3c24xx_leds_exit(void)

{

    int minor;

    /* 卸载驱动程序 */

    unregister_chrdev(LED_MAJOR, DEVICE_NAME);

 

    for (minor = 0; minor < 4; minor++)

    {

        class_device_unregister(leds_class_devs[minor]);

    }

    class_destroy(leds_class);

    iounmap(gpio_va);

}

 

/* 这两行指定驱动程序的初始化函数和卸载函数 */

module_init(s3c24xx_leds_init);

module_exit(s3c24xx_leds_exit);

 

执行“insmod s3c24xx_led.ko”命令时就会调用s3c24xx_leds_init函数。它调用register_chrdev函数向内核注册驱动程序:将主设备号LED_MAJOR与file_operation结构s3c24xx_leds_fops联系起来。以后应用程序操作主设备号为LED_MAJOR与file_operation结构s3c24xx_leds_fops联系起来。当应用程序操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数,比如open、read、write、ioctl,对应于s3c24xx_leds_fops中有其对应的函数,就会调用这些函数。

而执行“remmod s3c24xx_led.ko”时,就会调用s3c24xx_leds_exit函数,它进而调用unregister_chrdev函数卸载驱动程序,功能与register_chrdev函数相反。

s3c24xx_leds_fops的组成为:

/* 这个结构是字符设备驱动程序的核心

 * 当应用程序操作设备文件时所调用的open、read、write等函数,

 * 最终会调用这个结构中指定的对应函数

 */

static struct file_operationss3c24xx_leds_fops = {

    .owner =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */

    .open  =   s3c24xx_leds_open,    

    .read   =   s3c24xx_leds_read,    

    .write  =   s3c24xx_leds_write,   

};

 

 

file_operation类型的s3c24xx_leds_fops结构是驱动中最重要的数据结构,字符设备驱动就是要填充其中各个成员。如open、ioctl成员被设为s3c24xx_leds_open和s3c24xx_leds_ioctl函数。s3c24xx_leds_open用于 初始化LED所用的GPIO引脚,s3c24xx_leds_ioctl用来根据用户传入的参数设置GPIO输出电平。s3c24xx_leds_open函数代码如下:

 

static ints3c24xx_leds_open(struct inode *inode, struct file *file)

{

    int i;

    for (i=0;i<4;i++){

    //设置GPIO引脚的功能,为输出功能

    s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);

}

return 0;

}

 

        s3c2410_gpio_cfgpin函数是内核里实现的,它用来选择引脚的功能。其实就是设置GPIO的控制寄存器。

   

S3c24xx_leds_ioctl代码如下:

 

/* 应用程序对设备文件/dev/leds执行ioctl(...)时,

 * 就会调用s3c24xx_leds_ioctl函数

 */

static int s3c2440_leds_ioctl(structinode *inode,

                                structfile *file,

                                unsignedint cmd,

                                unsignedlong arg)

{

   if(arg > 4){

        return–EINVAL;

}

 

switch(cmd){            //通过命令设置LED引脚电平状态

 

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;

}

}

 

应用程序执行系统调用ioctl(fd,cmd,arg)时,s3c2440_leds_ioctl函数被调用,cmd,arg参数调用s3c2410_gpio_setpinhans,来设置引脚的输出电平,输出0时点亮LED,输出1时熄灭LED。

 

 

    在Linux4.33的led_s3c24xx驱动中还有两个主要的设备属性文件brightness和max_brightness。
brightness有操作函数led_brightness_show和led_birghtness_shore,led_brightness_show用于显示led的亮度值,而led_birghtness_shore用于设置led的亮度值,所以可以通过echo 120 > brightness的方式设置lcd的背光亮度为120。
而max_brightness只有一个操作函数为led_max_brightness_show用于显示led的最大亮度值。

 

 

还有led_timer_function函数

static void led_timer_function(unsigned longdata)

  {

         struct led_classdev *led_cdev = (void *)data;

         unsigned long brightness;

         unsigned long delay;

 

         if (!led_cdev->blink_delay_on || !led_cdev->blink_delay_off) {

                  led_set_brightness(led_cdev,LED_OFF);

                  return;

         }

 

        brightness = led_get_brightness(led_cdev);

        if (!brightness) {

                /* Time to switch the LED on.*/

               brightness =led_cdev->blink_brightness;

                delay =led_cdev->blink_delay_on;

        } else {

                 /* Store the currentbrightness value to be able

                  * to restore it when thedelay_off period is over.

                  */

                 led_cdev->blink_brightness= brightness;

                 brightness = LED_OFF;

                 delay =led_cdev->blink_delay_off;

       }

 

        led_set_brightness(led_cdev, brightness);

       

        mod_timer(&led_cdev->blink_timer, jiffies +msecs_to_jiffies(delay));

}

此函数运用定时器来控制led灯。首先判断led_cdev中的blink_delay_on和blink_delay_off,如果这两个值为0,那么直接设置led亮度值为0,并返回。
再读取brightness,这里有两个分支,如果brightness值为0,即led是关闭的,那么设置brightness为led_cdev->blink_brightness,然后设置led的亮度值,修改定时器,是它延长led_cdev->blink_delay_on再执行led_timer_function这个函数,从这里可以看到blink_delay_on为led灯亮的时间。如果brightness不为0,也就是led是亮着的,先将之前的led亮度值保存在变量led_cdev->blink_brightness中,然后关闭led,延长blink_delay_off这么长时间,所以blink_delay_off是led关闭的时间。
从这里我们可以看出,blink_delay_on和blink_delay_off是用于调节led亮和灭的时间长度,单位是毫秒,如果它们都是500毫秒,那么就会出现led500毫秒一亮一灭,这样设计的目的是为了节约用电,比如控制lcd的背光(注意:PWM控制的LCD背光不需要blink_delay_on和blink_delay_off这两个参数,因为PWM本身就具有这个功能)。

注意:每个调用led_classdev_register注册到内核的struct led_classdev都有个定时器,而且他们的定时器操作函数都是相同的,他们各自管理自己的delay_on、dealy_off。初始情况下led_timer_function是不会被执行的,除非有外界条件去触发它,例如调用了mod_timer。

led_set_brightness用来设置led亮度,在led.h中:

static inline void led_set_brightness(struct led_classdev *led_cdev,

                                         enum led_brightness value)

  {

        if (value >led_cdev->max_brightness)

                  value =led_cdev->max_brightness;

         led_cdev->brightness =value;

         if (!(led_cdev->flags& LED_SUSPENDED))

               led_cdev->brightness_set(led_cdev, value);

}

此函数中调用led_classdev中的brightness_set,在led_core.c中,有led_brightness_set()、led_blink_set两个接口函数。

void led_brightness_set(struct led_classdev *led_cdev,

                         enumled_brightness brightness)

{

        led_stop_software_blink(led_cdev);

        led_cdev->brightness_set(led_cdev, brightness);

}

EXPORT_SYMBOL(led_brightness_set);

void led_blink_set(struct led_classdev *led_cdev,

                   unsigned long*delay_on,

                   unsigned long*delay_off)

{

         del_timer_sync(&led_cdev->blink_timer);

        if (led_cdev->blink_set&&

           !led_cdev->blink_set(led_cdev, delay_on, delay_off))

               return;

       /* blink with 1 Hz as defaultif nothing specified */

        if (!*delay_on &&!*delay_off)

                *delay_on = *delay_off = 500;

       led_set_software_blink(led_cdev, *delay_on, *delay_off);

}

EXPORT_SYMBOL(led_blink_set);

 

Linux2.66中创建一个file_operation结构体。填充这个结构体,指定操作设备文件时的函数是什么,例如open、read、write等。而在Linux4.33中,如果去写一个led驱动,首先得定义一个struct led_classdev,它得有两个成员,brightness_set和blink_set,最后调用led_classdev_register函数去完成注册。以leds-s3c24xx为例:

平台驱动部分为:

staticstruct platform_driver s3c24xx_led_driver = {

        .probe          = s3c24xx_led_probe,

       .remove         = s3c24xx_led_remove,

         .driver         = {

                .name           = "s3c24xx_led",

                .owner          = THIS_MODULE,

};

 

module_platform_driver(s3c24xx_led_driver);

staticint s3c24xx_led_probe(struct platform_device *dev)

{

         struct s3c24xx_led_platdata *pdata =dev->dev.platform_data;

        struct s3c24xx_gpio_led *led;

        int ret;

        led = kzalloc(sizeof(structs3c24xx_gpio_led), GFP_KERNEL);

       if (led == NULL) {

                 dev_err(&dev->dev,"No memory for device\n");

                return -ENOMEM;

         }

        platform_set_drvdata(dev, led);

        led->cdev.brightness_set =s3c24xx_led_set;

        led->cdev.default_trigger =pdata->def_trigger;

        led->cdev.name = pdata->name;

         led->cdev.flags |=LED_CORE_SUSPENDRESUME;

 

         led->pdata = pdata;

        /* no point in having a pull-up if weare always driving */

        if (pdata->flags &S3C24XX_LEDF_TRISTATE) {

                s3c2410_gpio_setpin(pdata->gpio,0);

                s3c2410_gpio_cfgpin(pdata->gpio, S3C2410_GPIO_INPUT);

        } else {

               s3c2410_gpio_pullup(pdata->gpio, 0);

                s3c2410_gpio_setpin(pdata->gpio, 0);

                s3c2410_gpio_cfgpin(pdata->gpio, S3C2410_GPIO_OUTPUT);

         }

 

         /* register our new led device */

        ret =led_classdev_register(&dev->dev, &led->cdev);

        if (ret < 0) {

                dev_err(&dev->dev,"led_classdev_register failed\n");

                kfree(led);

                 return ret;

        }

        return 0;

}

在probe函数中首先申请内存,其中就包括struct led_classdev,然后是对它的一些赋值操作,比如brightness_set为s3c24xx_led_set。再然后是初始化gpio口,最后是调用led_classdev_register函数完成注册。

 

下面编写一个测试程序测试它:

 

#include<sys/types.h>

#include<sys/stat.h>

#include<fcntl.h>

#include<stdio.h>

 

/*

  * ledtest <dev> <on|off>

  */

 

voidprint_usage(char *file)

{

    printf("Usage:\n");

    printf("%s <dev><on|off>\n",file);

    printf("eg. \n");

    printf("%s /dev/leds on\n",file);

    printf("%s /dev/leds off\n",file);

    printf("%s /dev/led1 on\n",file);

    printf("%s /dev/led1 off\n",file);

}

 

intmain(int argc, char **argv)

{

    int fd;

    char* filename;

    char val;

 

    if (argc != 3)

    {

        print_usage(argv[0]);

        return 0;

    }

 

    filename = argv[1];

 

    fd = open(filename, O_RDWR);

    if (fd < 0)

    {

        printf("error, can't open%s\n", filename);

        return 0;

    }

 

    if (!strcmp("on", argv[2]))

    {

        // 亮灯

        val = 0;

        write(fd, &val, 1);

    }

    else if (!strcmp("off", argv[2]))

    {

        // 灭灯

        val = 1;

        write(fd, &val, 1);

    }

    else

    {

        print_usage(argv[0]);

        return 0;

    }

   

   

    return 0;

}

 

 

运行 :./ledtest  /dev/led1  on

可用。

 

 

 

 

你可能感兴趣的:(linux,C语言,驱动,led,字符设备)