字符设备驱动一

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

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;">

原文地址:https://www.cnblogs.com/luosir520/p/11446813.html

时间: 2024-07-31 19:06:54

字符设备驱动一的相关文章

register_chrdev_region/alloc_chrdev_region和cdev注册字符设备驱动

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region().alloc_chrdev_region() 和 register_chrdev(). (1)register_chrdev  比较老的内核注册的形式   早期的驱动(2)register_chrdev_region/alloc_chrdev_region + cdev  新的驱动形式 (3)register_chrdev()函数是老版本里面的设备号注册函数,可以实现静态和动态注册两种方法

linux字符设备驱动

一.字符设备.字符设备驱动与用户空间访问该设备的程序三者之间的关系. 如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主.次设备号)以确定字符设备的唯一性.通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open().read().write()等. 在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获

linux 字符设备驱动开发详解

一.设备的分类及特点 1.字符设备 字符设备是面向数据流的设备,没有请求缓冲区,对设备的存取只能按顺序按字节的存取而不能随机访问.    Linux下的大多设备都是字符设备.应用程序是通过字符设备节点来访问字符设备的.通常至少需要实现 open, close, read, 和 write 等系统调用.    设备节点一般都由mknod命令都创建在/dev目录下,包含了设备的类型.主/次设备号以及设备的访问权限控制等,如:crw-rw----  1 root  root 4, 64 Feb 18

linux设备驱动第三篇:写一个简单的字符设备驱动

在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动.本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存. 下面就开始学习如何写一个简单的字符设备驱动.首先我们来分解一下字符设备驱动都有那些结构或者方法组成,也就是说实现一个可以使用的字符设备驱动我们必须做些什么工作. 1.主设备号和次设备号 对于字符设备的访问是通过文件系统中的设备名称进行的.他们通常位于/dev目录下.如下: [plain] vie

13、字符设备驱动的使用

编译和安装驱动 下面是通过一个例子来学会使用驱动程序: 1---驱动程序: Memdev.c #include <linux/module.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/cdev.h> #include <asm/uaccess.h> int dev1_registers[5]; int dev2_registers[5]; struct cdev

linux设备驱动之字符设备驱动模型(2)

在上一篇中我们已经了解了字符设备驱动的原理,也了解了应用层调用内核函数的机制,但是我们每次操作设备,都必须首先通过mknod命令创建一个设备文件名,比如说我们要打开u盘,硬盘等这些设备,难道我们还要自己创建,就如同刘老师常说的一句话,这也太山寨了吧,所以我们今天我们来点比较专业的,让函数帮我们自动创建: 在Linux 下,设备和驱动通常都需要挂接在一种总线上,总线有PCI.USB.I2C.SPI 等等,总线是处理器和设备之间的通道,在设备模型中,所有的设备都通过总线相连,一总线来管理设备和驱动函

字符设备驱动模型

1.设备描述结构cdev驱动模型种类繁多,这就需要我从众多的模型中提取出他们的一些共性:a.驱动初始化a.1 分配设备描述结构a.2 初始化设备描述结构a.3 注册设备描述结构a.4 硬件初始化b.实现设备操作c.驱动注销 ------------------------------------------------------------------ 设备描述结构:在任何一种驱动模型中,设备都会用的内核中的一种结构来描述,我们的字符设备在内核中使用struct cdev 来来描述.struc

Linux字符设备驱动剖析

一.先看看设备应用程序 1.很简单,open设备文件,read.write.ioctl,最后close退出.如下: intmain(int argc ,char *argv[]){ unsigned char val[1] = 1; int fd =open("/dev/LED",O_RDWR);//打开设备 write(fd,val,1);//写入设备,这里代表LED全亮 close(fd);//关闭设备 return 0; } 二./dev目录与文件系统 2./dev是根文件系统下

字符设备驱动框架

scull from <Linux设备驱动程序> memdev.c /* * memdev.c * create at 2015/01/07 * 字符设备驱动程序框架 */ #include <linux/module.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/mm.h> #include <

字符设备驱动

在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主.次设备号)以确定字符设备的唯一性.通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open().read().write()等. 在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获取设备号,通过cdev_init( )建立cdev与file_operati