zoukankan      html  css  js  c++  java
  • 从点一个灯开始学写Linux字符设备驱动

    关注、星标嵌入式客栈,精彩及时送达

    [导读] 前一篇文章,介绍了如何将一个hello word模块编译进内核或者编译为动态加载内核模块,本篇来介绍一下如何利用Linux驱动模型来完成一个LED灯设备驱动。点一个灯有什么好谈呢?况且Linux下有专门的leds驱动子系统。

    点灯有啥好聊呢?

    在很多嵌入式系统里,有可能需要实现数字开关量输出,比如:

    • LED状态显示

    • 阀门/继电器控制

    • 蜂鸣器

    • ......

    嵌入式Linux一般需求千变万化,也不可能这些需求都有现成设备驱动代码可供使用,所以如何学会完成一个开关量输出设备的驱动,一方面点个灯可以比较快了解如何具体写一个字符类设备驱动,另一方面实际项目中对于开关量输出设备就可以这样干,所以是具有较强的实用价值的。

    要完成这样一个开关量输出GPIO的驱动程序,需要梳理梳理下面这些概念:

    • 设备编号

    • 设备挂载

    • 关键数据结构

    设备编号

    字符设备是通过文件系统内的设备名称进行访问的,其本质是设备文件系统树的节点。故Linux下设备也是一个文件,Linux下字符设备在/dev目录下。可以在开发板的控制台或者编译的主Linux系统中利用ls -l /dev查看,如下图:

    对于ls -l列出的属性,做一个比较细的解析:

    细心的朋友或许会发现设备号属性,在有的文件夹下列出来不是这样,这就对了!普通文件夹下是这样:

    差别在于一个是文件大小,一个是设备号。

    再细心一点的朋友或许还会问,这些/dev下的文件时间属性为神马都相差无几?这是因为/dev设备树节点是在内核启动挂载设备驱动动态生成的,所以时间就是系统开机后按次序生成的,你如不信,不妨重启一下系统在查看一下。

    常见文件类型:

    • d: directory 文件夹

    • l: link  符号链接

    • p: FIFO pipe 管道文件,可以用mkfifo命令生成创建

    • s: socket 套接字文件

    • c: char 字符型设备文件

    • b: block 块设备文件

    • -:常规文件

    回到设备号,设备号是一个32位无符号整型数,其中:

    • 12位用来表示主设备号,用于标识设备对应的驱动程序。

    • 20位用来表示次设备号,用于正确确定设备文件所指的设备。

    这怎么理解呢,看下串口类设备就比较清楚了:

    主设备号一样证明这些设备共用了一个驱动程序,而次设备号不一样,则对应了不同的串口设备。那么怎么得到设备号呢?

    /*下列定义位于./include/linux/types.h */
    typedef u32 __kernel_dev_t;
    typedef __kernel_dev_t dev_t;
    
    /* 下面宏用于生成主设备号,次设备号       */
    /* 下列定义位于./include/linux/Kdev_t.h */
    #define MINORBITS 20
    #define MINORMASK ((1U << MINORBITS) - 1)
    
    #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
    #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
    #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
    

    使用举例:

    /* 主设备号 */
    MAJOR(dev_t dev); 
    /* 次设备号 */
    MINOR(dev_t dev);
    

    设备挂载

    为简化问题,本文描述一下动态加载设备驱动模块,暂不考虑设备树。参考<<Linux设备驱动程序>>一书。可参照前文将驱动编译成模块,然后利用下面脚步动态加载模块。由前面描述,知道设备最终需要在/dev目录下生成一个设备文件,那么这个设备文件节点是怎么生成呢,看看下面的脚本:

    #!/bin/sh
    #-----------------------------------------------------------------------
    module="led"
    device="led"
    mode="664"
    group="staff"
    
    # 利用insmod命令加载设备模块
    insmod -f $module.ko $* || exit 1
    # 获取系统分配的主设备号 
    major=`cat /proc/devices | awk "\$2=="$module" {print \$1}"`
    
    # 删除旧节点
    rm -f /dev/${device} 
    
    #创建设备文件节点
    mknod /dev/${device} c $major 0
    #设置设备文件节点属性
    chgrp $group /dev/${device}
    chmod $mode  /dev/${device}
    

    这里要提一下/proc/devices,这是一个文件记录了字符和块设备的主设备号,以及分配到这些设备号的设备名称。比如使用cat命令来列出这个文件内容:

    关键数据结构

    字符设备由什么关键数据结构进行抽象的呢,来看看:

    • file_operations定义在./include/linux/fs.h

    • cdev定义在./include/linux/cdev.h


    cdev中与字符设备驱动编程相关两个数据域:

    • const struct file_operations *ops;

    • dev_t dev;设备编号

    文件操作符是一个庞大的数据结构,常规字符设备驱动一般需要实现下面一些函数指针:

    • read:用来实现从设备中读取数据

    • write:用于实现写入数据到设备

    • ioctl:实现执行设备特定命令的方法

    • open:用实现打开一个设备文件

    • release:当file结构被释放时,将调用这个接口函数

    点灯设备

    先上代码(可左右滑动显示):

    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/errno.h>
    #include <linux/kernel.h>  /* printk() */
    #include <linux/major.h>
    #include <linux/cdev.h>
    #include <linux/fs.h>      /* everything... */
    #include <linux/gpio.h>
    #include <asm/uaccess.h>   /* copy_*_user */
    
    /*这里具体参考不同开发板的电路 GPIOC24 */
    #define LED_CTRL   (2*32+24)  
    
    static const unsigned int led_pad_cfg = LED_CTRL;
    
    struct t_led_dev{
     struct cdev cdev;
     unsigned char value; 
    };
    
    struct t_led_dev  led_dev;
    static dev_t led_major;
    static dev_t led_minor=0; 
    
    static int led_open(struct inode * inode,struct file * filp)
    { 
     filp->private_data = &led_dev; 
     
     printk ("led is opened!
    ");          
     return 0;
    }
    
    static int led_release(struct inode * inode,
                           struct file * filp)
    {
     return 0;
    }
    
    static ssize_t led_read(struct file * file, 
                            char __user * buf,
                            size_t count, 
                            loff_t *ppos)
    {
     ssize_t ret=1;   
     if(copy_to_user(&(led_dev.value),buf,1))
      return -EFAULT;
     printk ("led is read!
    ");
     return ret;
    }
    
    static ssize_t led_write(struct file * filp, 
                             const char __user *buf,
                             size_t count,loff_t *ppos)
    {
     unsigned char value; 
        ssize_t retval = 0;
     if(copy_from_user(&value,buf,1))
      return -EFAULT;
     
     if(value&0x01)
      gpio_set_value(led_pad_cfg, 1);
     else
      gpio_set_value(led_pad_cfg, 0);
    
        printk ("led is written!
    ");
     return retval;
    }
    
    static const struct file_operations led_fops = {
     .owner = THIS_MODULE,
     .read  = led_read,
     .write = led_write,
     .open  = led_open,
     .release = led_release,
    };
    
    static void led_setup_cdev(struct t_led_dev * dev, int index)
    { 
        /* 初始化字符设备驱动数据域 */
     int err,devno = MKDEV(led_major,led_minor+index);
     cdev_init(&(dev->cdev),&led_fops);
     dev->cdev.owner = THIS_MODULE;
     dev->cdev.ops = &led_fops;
        /* 字符设备注册 */
     err = cdev_add(&(dev->cdev),devno,1);
     if(err)
      printk(KERN_NOTICE "Error %d adding led %d",err,index); 
    }
    
    static int led_gpio_init(void)
    {
     if (gpio_request(LED_CTRL, "led") < 0) {
      printk("Led request gpio failed
    ");
      return -1;
     }
     
     printk("Led gpio requested ok
    ");
     
     gpio_direction_output(LED_CTRL, 1); 
     gpio_set_value(LED_CTRL, 1);
     
     return 0;
    }
    /* 注销设备 */
    void led_cleanup(void)
    { 
     dev_t devno = MKDEV(led_major, led_minor); 
     gpio_set_value(LED_CTRL, 0); 
     gpio_free(LED_CTRL);
    
     cdev_del(&led_dev.cdev); 
     unregister_chrdev_region(devno, 1);    //注销设备号  
    }
    
    /* 注册设备 */
    static int led_init(void)
    { 
     int result;
     dev_t dev = MKDEV( led_major, 0 );
     /* 动态分配设备号 */
     result = alloc_chrdev_region(&dev, 0, 1, "led");      
     if(result<0)
      return result; 
     
     led_major = MAJOR(dev);
     
     memset(&led_dev,0,sizeof(struct t_led_dev));
     led_setup_cdev(&led_dev,0);
    
     led_gpio_init();
     printk ("led device initialised!
    "); 
     
     return result;
    }
    
    module_init(led_init);
    module_exit(led_cleanup);
    
    MODULE_DESCRIPTION("Led device demo");
    MODULE_AUTHOR("embinn");
    MODULE_LICENSE("GPL");
    

    来总结一下要点:

    • init函数,需要用module_init宏包起来,本例中即为led_init,module_init宏的作用就是选编译为模块或进内核的底层实现,建议刚开始不必深究。一般而言主要实现:

      • 申请分配主设备号alloc_chrdev_region

      • 为特定设备相关数据结构分配内存

      • 将入口函数(open read write等)与字符设备驱动的cdev抽象数据结构关联

      • 将主设备与驱动程序cdev相关联

      • 申请硬件资源,初始化硬件

      • 调用cdev_add注册设备

    • exit函数,一样需要用module_exit包起来,主要负责:

      • 释放硬件资源

      • 调用cdev_del删除设备

      • 调用unregister_chrdev_region注销设备号

    • 用户空间与驱动数据交换

      • copy_to_user,如其名一样,将内核空间数据信息传递到用户空间

      • copy_from_user,如其名一样,从用户空间拷贝数据进内核空间

    • 善用printk进行驱动调试,这是内核打印函数。

    • gpio相关操作函数,这里就不一一列举其作用了,比较容易理解。

    测试驱动

    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define READ_SIZE 10
    
    int main(int argc, char **argv){
     int fd,count;
     float value;
     unsigned char buf[READ_SIZE+1];
     printf( "Cmd argv[0]:%s,argv[1]:%s,argv[2]:%s
    ",argv[0],argv[1],argv[2] );
     
     if( argc<2 ){
      printf( "[Usage: test device_name ]
    " );
      exit(0);
     }
        if(strlen(argv[2]!=1)
            printf( "Invalid parameter
    " );
        
     if(( fd = open(argv[1],O_WRONLY  ))<0){
      printf( "Error:can not open the device: %s
    ",argv[1] );
      exit(1);
     }
        
        if(argv[2][0] == '1')
            buf[0] = 1;
        else if(argv[2][0] == '0')
            buf[0] = 0;
        else
            printf( "Invalid parameter
    " );
    
     printf("write: %d
    ",buf[0]);
     if( (count = write( fd, buf ,1 ))<0 ){
      perror("write error.
    ");
      exit(1);  
     }
     
     close(fd);
     printf("close device %s
    ",argv[1] );
     return 0;
    }
    

    编译成可执行文件,调用前面的脚本加载设备后,在/dev下就可以看到led设备了。比如测试代码编译成ledTest执行文件,则使用下面命令运行测试程序就可以看到led控制效果了:

    /*打开led 具体取决电路是高有效还是低有效*/
    ./ledTest /dev/led 1
    ./ledTest /dev/led 0
    

    这样就实现了用户空间驱动底层设备了,实际应用代码就可以这样去访问底层的字符型设备。

    总结一下

    本文总结了简单字符设备的驱动开发的一些要点,以及如何动态加载,在设备文件系统树上创建设备节点,并演示了驱动以及驱动使用的基本要点。

    本文辛苦原创,如喜欢请点赞/在看/分享支持,不胜感激!

    END

    往期精彩推荐,点击即可阅读

    ▲Linux内核中I2C总线及设备长啥样? 

    ▲学Linux驱动:应先了解总线驱动模型

    ▲看思维导图:一文带你学Verilog HDL语言

  • 相关阅读:
    Android Studio中图片的格式转换
    VS2013关于C++ Primer5 的3.42题报错
    VS2013 注释多行与取消多行注释快捷键
    【Ubuntu】安装tar.gz文件
    vs下程序运行结果框闪退的解决方案
    深度学习相关链接
    问题解决:Failed to get convolution algorithm. This is probably because cuDNN failed to initialize
    【验证码识别】Pillow、tesseract-ocr与pytesseract模块的安装以及错误解决
    霍夫变换原理(看完就懂)
    python 字节数组和字符串的互转
  • 原文地址:https://www.cnblogs.com/embInn/p/14038111.html
Copyright © 2011-2022 走看看