字符设备驱动一

Posted luosir520

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字符设备驱动一相关的知识,希望对你有一定的参考价值。

一、 字符设备驱动之概念介绍

1、 应用程序、库、内核、驱动程序的关系

如下图,一个软件系统可以分为:应用程序、库、操作系统(内核)、驱动程序。
技术图片
以点亮LED为例:

    1)应用程序使用库提供的 open 函数打开代表LED的设备文件
    2)库根据 open 函数传入的参数执行 "swi" 指令,这条指令会引起CPU异常,进入内核
    3)内核的异常处理函数根据这些参数找到相应的驱动程序,返回一个文件句柄给库,进而返回给应用程序
    4)应用程序得到文件句柄后,使用库提供的 write 或 ioclt 函数发出控制指令
    5)库根据 write 或 ioctl 传入的参数执行 "swi" 指令,这条指令会引起CPU异常,进入内核
    6)内核的异常处理函数根据这些参数调用驱动程序的相关函数,点亮LED

实际上,内核和驱动程序之间并没有界限,因为驱动程序最终是要编进内核去的。

2、 Linux 驱动程序的分类和开发步骤

A、 Linux 驱动程序的分类

Linux的外设可以分为3类:字符设备(character device)、块设备(block device)和网络接口(network interface)。

    字节设备是能够像字节流(文件)一样被访问的设备,就是说对它的读写是以字节为单位的。字符设备的驱动程序中实现了 open、close、read、write等系统调用。
    块设备的数据以块的形式存放,比如 NAND Flash 上的数据就是以页为单位存放的。应用程序也可以通过相应的设备文件(比如/dev/mtdblock0等)来调用open、close、read、write等系统调用。
    网络接口同时具有字符设备、块设备的部分特点。它的输入/输出是有结构的、成块的,它的块又不是固定的大小。

B、 Linux 驱动程序开发步骤

Linux 内核就是由各种驱动程序组成的,内核源码中有大约 85% 是各种驱动程序的代码。编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。

    一般来说,编写 Linux 设备驱动程序流程如下:
    1)查看原理图、数据手册,了解设备的操作方法。
    2)在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始。
    3)实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序。
    4)设计所要实现的操作,比如:open、close、read、write等函数。
    5)实现中断服务(中断并不是每个设备驱动所必须的)。
    6)编译该驱动程序到内核中,或者用 insmod 命令加载。
    7)测试驱动程序。

二、 字符设备驱动程序之LED驱动程序(第二到四节课)

技术图片
应用程序通过C库的 open 函数打开设备文件,打开文件后可获得属性(比如为(c)字符设备,主设备号为111)。应用程序通过C库进入到内核,内核最后会调用到驱动程序。
VFS(虚拟文件系统)怎么根据打开的设备找到驱动呢?

    字符设备就根据主设备号111在内核里面定义的字符设备数组里面找到 file_operation 这个结构体,这个结构的成员在我们的驱动里面实现。
    驱动程序里实现步骤:
    1、实现 led_open, led_read, led_write 函数。
    2、问:怎么告诉内核呢?答:a.定义一个 file_operation 结构,让这个的结构体里的成员函数(.open 和 .write)分别指向我们自己实现的 led_open, led_write 函数;b.在驱动的入口函数(比如:int first_chrdev_init(void))里面调用 register_chrdev(主设备号, 主设备名, &file_operation) 注册函数把这个结构体放到内核里面的字符设备数组里。
    3、问:内核怎么知道是(int first_chrdev_init(void))这个入口函数?答:需要用一个宏 (module_init(first_chrdev_init))来修饰一下,这宏是一个结构体,结构体里面有一个函数指针指向我们传入的入口函数,当我们去加载一个驱动程序(insmod)的时候,内核就会自动的找到这个结构体,然后调用里面的函数指针。
    注意:驱动程序和应用程序就是通过【设备类型和主设备号】联系起来的,与设备名称无关。

第一个驱动程序(first_chrdev.c)

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>


static int first_chrdev_open(struct inode *inode, struct file *file)

    printk("first_chrdev_open\\n\\r");
    return 0;



static ssize_t first_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

    printk("first_chrdev_write\\n\\r");
    return 0;



static struct file_operations first_chrdev_fops = 
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_chrdev_open,
    .write    =    first_chrdev_write,
;


int major;
static int first_chrdev_init(void)

    /* 0表示让系统自动为我们分配一个从1到255的主设备号*/
    major = register_chrdev(0, "first_chrdev", &first_chrdev_fops);  //注册
    return 0;


static void first_chrdev_exit(void)

    unregister_chrdev(major, "first_chrdev");                    //卸载



module_init(first_chrdev_init);
module_exit(first_chrdev_exit);

MODULE_LICENSE("GPL");

第一个测试程序(first_chrdev_test.c)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char **argv)

    int fd;
    int val = 1;
    fd = open("/dev/xyz",O_RDWR);
    if(fd < 0)
    
        printf("can‘t open!\\n");
    
    write(fd, &val, 4);

    return 0;

first_chrdev的Makefile
技术图片
编译驱动程序时会依赖与内核,

    -C表示 指定进入指定的目录即KERN_DIR,是内核源代码目录,调用该目录顶层下的Makefile,目标为modules。
    M=$(shell pwd) | `pwd`选项让该Makefile在构造modules目标之前返回到模块源代码目录并在当前目录生成obj-m指定的xxx.o目标模块。
    clean这个目标表示将模块清理掉。
    obj-m += xxx.o即指定当前目录要生成的目标模块,然后modules目标指向obj-m变量中设定的模块。

编译模块、拷贝文件

    在make和编译(arm-linux-gcc -o first_chrdev_test fist first_chrdev_test.c)之后将其(first_chrdev.ko 和 first_chrdev_test)拷贝到挂接的文件系统下。

测试:

    cat /proc/devices:表示内核目前所支持的设备,第一列表示主设备号,第二列表示主设备名

技术图片

    insmod ./first_chrdev.ko:加载驱动,也就意味着会调用moudle_init函数

技术图片

    lsmod:用于查看所加载的驱动

技术图片


    rmmod ./first_chrdev.ko:卸载驱动,也就意味着会调用moudle_exit函数

注意:此时运行测试程序会出错
技术图片
原因:没有设备结点,也就是没有(/dev/xyz)这个文件

解决办法:手动创建一个设备结点

    mknod /dev/xyz c 252 0 :手动创建一个字符类型,主设备号为252,次设备号为0的设备结点

技术图片

再次执行测试程序
技术图片

问:每次驱动程序自动分配主设备号后我们都要使用(cat /proc/devices)命令来查看主设备号后再手工创建设备结点吗?

解决办法:使用mdev根据系统信息创建设备结点
定义下面两个变量

    static struct class *firstdrv_class;   定义一个类
    static struct class_device    *firstdrv_class_devs;   定义一个设备

自动创建设备的驱动程序(led_chrdev.c)

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *led_chrdev_class;    //定义一个类
static struct class_device *led_chrdev_class_dev;    //定义一个设备

volatile unsigned int *gpfcon = NULL;
volatile unsigned int *gpfdat = NULL;

static int led_chrdev_open(struct inode *inode, struct file *file)

    printk("led_chrdev_open\\n\\r");
    *gpfcon &= ~((3<<(4*2)) | (3<<(5*2)) | (3<<(6*2)));
    *gpfcon |=  ((1<<(4*2)) | (1<<(5*2)) | (1<<(6*2)));
    return 0;



static ssize_t led_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

    int val;
    printk("led_chrdev_write\\n\\r");
    copy_from_user(&val, buf, count); //从用户空间到内核空间,buf:应用程序传入的值
    if(val == 1)
    
        //open led
        *gpfdat  &= ~((1<<4) | (1<<5) | (1<<6));
    
    else
    
        //close led
        *gpfdat |=  ((1<<4) | (1<<5) | (1<<6));
    
    return 0;



static struct file_operations led_chrdev_fops = 
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   led_chrdev_open,
    .write    =    led_chrdev_write,
;


int major;
static int led_chrdev_init(void)

    major = register_chrdev(0, "led_chrdev", &led_chrdev_fops);  //在内核的字符设备数组中注册一个file_operation结构
    led_chrdev_class = class_create(THIS_MODULE, "ledchrdev");        //创建一个类:会自动的在(/sys/class)目录下自动创建一个(ledchrdev)这个类。
    //自动创建一个设备:会自动的在(/sys/class/ledchrdev)这个目录里面创建一个(xyz)文件夹,这个文件夹内有一个(dev)文件,它的内容是(252:0)主设备号和此设备号。
    led_chrdev_class_dev = class_device_create(led_chrdev_class, NULL, MKDEV(major, 0), NULL, "xyz");
    gpfcon = (volatile unsigned int *)ioremap(0x56000050, 16);
    gpfdat = gpfcon + 1;
    return 0;


static void led_chrdev_exit(void)

    unregister_chrdev(major, "led_chrdev");                    //卸载:从内核的字符设备数组中一主设备号找到这一项把它卸载
    class_device_unregister(led_chrdev_class_dev);            //删除自动创建的设备
    class_destroy(led_chrdev_class);                        //摧毁自动创建的类
    iounmap(gpfcon);


module_init(led_chrdev_init);
module_exit(led_chrdev_exit);

MODULE_LICENSE("GPL");

测试程序(led_dev_test.c)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char **argv)

    int fd;
    int val = 1;
    fd = open("/dev/xyz",O_RDWR);
    if(fd < 0)
    
        printf("can‘t open!\\n");
    
    if(argc != 2)
    
        printf("Usage :\\n\\r");
        printf("%s <on/off>\\n\\r", argv[0]);
        return 0;
    

    if(strcmp(argv[1], "on") ==0)
    
        val = 1;
    
    else
    
        val = 0;
    
    write(fd, &val, 4);
    return 0;

测试

  1. 修改Makefile
  2. 编译并拷贝到(first_fs)目录
  3. 加载模块(insmod ./led_chrdev.ko)
  4. 运行测试程序(./led_dev_test)

注意1:使用虚拟地址的好处:读写内存更安全,由于系统和 mmu 的限制,使得这个过程无法操作到其它进程的数据。
注意2:此时可以直接运行测试程序,因为系统已经帮我们自动创建了设备结点。
技术图片
问:这个设备结点在单板上是怎么被创建出来的呢?
答:led_chrdev_class=class_create(THIS_MODULE, "ledchrdev")这个函数会自动的在(/sys/class)目录下创建一个(ledchrdev)类;led_chrdev_class_dev=class_device_create(led_chrdev_class, NULL, MKDEV(major, 0), NULL, "xyz")这个函数会自动的在(/sys/class/ledchrdev)目录下创建一个(xyz)设备,并且该文件夹下有一个(dev)文件保存有该设备的主设备号和次设备号
技术图片
技术图片
问:为什么(/sys)目录下的信息已更改,mdev就能自动去生成呢?
答:因为在我们脚本文件(/etc/init.d/rcS)中使用了mdev机制,mdev:mdev是udev的一个简化版,在(/sys)目录根据系统信息自动的创建设备结点
技术图片

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

以上是关于字符设备驱动一的主要内容,如果未能解决你的问题,请参考以下文章

Linux驱动开发:字符设备驱动开发

Linux驱动开发:字符设备驱动开发

Linux驱动字符设备驱动

Linux设备驱动之字符设备驱动

嵌入式开发(七):linux字符型设备驱动初步

深入理解Linux字符设备驱动