19 字符设备驱动基础

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ForFuture_/article/details/79406691

字符设备驱动基础


设备驱动通常是给用户进程来调用的

最常用的是设备驱动里实现字符设备驱动,实现后在”/dev/”目录里提供一个设备文件,然后用户进程就可以通过操作该设备文件来调用驱动


如pc上的uart设备文件:

crw-rw- 1 root dialout 4, 64 Jun 8 09:20 /dev/ttyS0 
crw-rw- 1 root dialout 4, 65 Jun 8 09:20 /dev/ttyS1 
crw-rw- 1 root dialout 4, 66 Jun 8 09:20 /dev/ttyS2 
crw-rw- 1 root dialout 4, 67 Jun 8 09:20 /dev/ttyS3

第一个字符'c'表示字符设备文件,也就是此设备文件对应着一个字符设备驱动
设备文件名字不重要,不管改成什么名字,功能还是可以用的

设备文件的设备号才是最重要的,设备号由主设备号和次设备号组成 
    如:ttyS0的主设备号是4,次设备号是64
主设备号通常表示一个字符设备驱动,上面4个uart设备它们驱动方法应是一样的,可以共用一个驱动 
在驱动里可通过次设备号来区分不同的硬件接口

设备驱动里在初始化时也需指定使用哪些设备号
当用户进程操作设备文件时,内核会根据设备文件的设备号找到对应的设备驱动,从而让用户进程通过内核与设备驱动建立联系,实现调用驱动里实现的功能

设备号很重要,在系统里是不可以重用的资源,设备号不能重用

在”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)) //由主设备号和次设备号生成一个设备号

设备号使用前需要向内核申请,使用完后需返还内核

设备号操作方法:

#include <linux/fs.h>    

//静态申请指定的设备号,from指设备号(需已指定主设备和次设备号),count指使用该驱动有多少个设备(次设备号),name为设备名(用于查看用,长度不能超过64字节)
int register_chrdev_region(dev_t from, unsigned count, const char *name);

//动态申请设备号,由内核分配没有使用的主设备号,分配好的设备号存在dev(不需要初始化),baseminor指次设备号从多少开始,count指设备个数,name为设备名
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

//释放设备号,from指设备号,count指设备数
void unregister_chrdev_region(dev_t from, unsigned count);

简单的事例代码1(申请设备号)(xxx.c):

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>

#define MYMA  1234
#define MYMI  3344
#define COUNT    3

dev_t devid;//用于存放设备号 

static int __init test_init(void)
{
    int ret;

    devid = MKDEV(MYMA, MYMI);//生成一个设备号
    ret = register_chrdev_region(devid, COUNT, "mydev"); //申请设备号
    if (ret < 0)
        goto err0;

    //执行到这里,则有三个设备号(1234, 3344), (1234, 3345), (1234, 3346)
    return 0;
err0:
    return ret;
}

static void __exit test_exit(void)
{

    unregister_chrdev_region(devid, COUNT);//使用完后需回收设备号
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");

Makefile文件:

obj-m += xxx.o

KSRC := /目录路径/orangepi_sdk/source/linux-3.4.112/
export ARCH := arm
export CROSS_COMPILE := arm-linux-gnueabihf-

all : 
    make -C $(KSRC) modules M=`pwd`

.PHONY : clean
clean : 
    make -C $(KSRC) modules clean M=`pwd`

编译后,加载模块后,可通过”cat /proc/devices”查看系统里主设备号的使用状况


在linux内核里使用”struct cdev”结构体类型的一个对象来描述一个字符设备驱动

include <linux/cdev.h>

struct cdev {
    struct kobject kobj;//内核用于管理字符设备驱动,kobject就是内核里最底层的类(内核里会自动管理此成员)
    struct module *owner;//通常设为THIS_MODULE,用于防止驱动在使用中时卸载驱动模块
    const struct file_operations *ops;//怎样操作(vfs),也就是实现当用户进程进行的open/read/write等操作时,驱动里对应的操作
    struct list_head list;//内核链表节点(内核里自动管理此成员)
    dev_t dev;//设备号
    unsigned int count;//设备数
};

同时内核里也提供对cdev对象操作的函数:

#include <linux/cdev.h>

void cdev_init(struct cdev *, const struct file_operations *);//初始化cdev对象

struct cdev *cdev_alloc(void);//动态分配一个cdev对象

int cdev_add(struct cdev *, dev_t, unsigned);//设置cdev对象使用设备号及设备个数,再把cdev对象增加到内核里,让它工作起来

void cdev_del(struct cdev *);//从内核里移除cdev对象

字符设备驱动实现的基本流程:

1.申请设备号:
    register_chrdev_region(...);

2.声明一个全局的cdev对象:
    struct cdev mycdev;

3.声明一个全局的file_operations的文件操作对象:
    struct file_operations fops = {
        .owner = THIS_MODULE,
        .read = 读函数地址,
        ....,
    };

4.初始化cdev对象,并把fops对象与cdev对象关联起来:
    cdev_init(&mycdev, &fops);//mycdev.ops = &fops;
    mycdev.owner = THIS_MODULE; 

5.把cdev对象加入内核里cdev_map(字符设备驱动的哈希表),并指定该驱动对象的设备号:
    cdev_add(&mycdev, 设备号, 次设备号的个数);

6.卸载模块时,要把设备驱动对象从内核里移除,并把设备号反注册:
    unregister_chrdev_region(..);
    cdev_del(&mycdev);

事例代码2(xxx.c):

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define MYMA  1234
#define MYMI  3344
#define COUNT    3

dev_t devid;//用于存放设备号
struct cdev mycdev; 

int myopen(struct inode *ind, struct file *fl)
{
    printk("in %s\n", __func__);
    return 0;
}

ssize_t myread(struct file *fl, char *__user buf, size_t len, loff_t *off)
{
    printk("in %s\n", __func__);
    return len;
}

ssize_t mywrite(struct file *fl, const char __user *buf, size_t len, loff_t *off)
{
    printk("in %s\n", __func__);
    return len;
}

struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = myopen,
    .read = myread,
    .write = mywrite,
};

static int __init test_init(void)
{
    int ret;

    devid = MKDEV(MYMA, MYMI);//生成一个设备号
    ret = register_chrdev_region(devid, COUNT, "mydev");
    if (ret < 0)
        goto err0;

    cdev_init(&mycdev, &fops);
    mycdev.owner = THIS_MODULE;
    ret = cdev_add(&mycdev, devid, COUNT);//将cdev加入内核
    if (ret < 0)
        goto err1;  

    return 0;

err1:
    unregister_chrdev_region(devid, COUNT);
err0:
    return ret;
}

static void __exit test_exit(void)
{ 
    unregister_chrdev_region(devid, COUNT);//使用完后需回收设备号
    cdev_del(&mycdev);
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");

编译加载驱动模块后,需要用”mknod /dev/设备文件名 c 主设备号 次设备号”来创建设备文件与设备号相对应:
(以后操作设备文件时,就可以触发相应的设备号所对应的设备驱动)

mknod /dev/mydev0 c 1234 3344
mknod /dev/mydev1 c 1234 3345
mknod /dev/mydev2 c 1234 3346

然后可以写应用程序来操作设备文件,也可以用命令来简单地测试:

cat /dev/mydev0  //会触发驱动里的openread函数
echo "kkk" > /dev/mydev0   //会触发驱动里的openwrite函数

猜你喜欢

转载自blog.csdn.net/ForFuture_/article/details/79406691
19