加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • 1. 新字符设备驱动原理
    • 2. 新字符设备驱动开发实验
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

Linux 新字符设备驱动开发模板

2022/06/13
1392
阅读需 22 分钟
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

公众号:嵌入式攻城狮(ID:andyxi_linux)

作者:安迪西

Linux字符设备驱动开发模板中介绍了旧版本的驱动开发模板,其需要手动分配设备号后,再进行注册,驱动加载成功后还需要手动创建设备节点,比较麻烦。目前Linux内核推荐的新字符设备驱动API函数,可以自动分配设备号、创建设备节点,使得驱动的使用更加方便

1. 新字符设备驱动原理

 

1.1 分配和释放设备号

旧字符设备驱动开发中使用register_chrdev函数注册字符设备时,需要事先确定好主设备号,并且注册成功后,会将该设备号下的所有次设备号都使用掉而新字符设备驱动API函数很好的解决了这个问题,使用设备号时再向内核申请,需要几个就申请几个,由内核分配设备可以使用的设备号

⏩ 设备号申请函数:没有指定设备号

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 
//dev:保存申请到的设备号
//baseminor:次设备号起始地址,一般为0
//count:要申请的设备号数量
//name:设备名字

⏩ 设备号申请函数:指定了主次设备号

int register_chrdev_region(dev_t from, unsigned count, const char *name) 
//from:要申请的起始设备号
//count:要申请的设备号数量
//name:设备名字

⏩ 设备号释放函数:统一使用下面函数释放

void unregister_chrdev_region(dev_t from, unsigned count) 
//from:要释放的设备号
//count:表示从from开始,要释放的设备号数量

因此新字符设备驱动中,设备号分配代码通常按如下示例编写:

int major;     /* 主设备号 */
int minor;     /* 次设备号 */
dev_t devid;   /* 设备号 */

if (major) {   /* 定义了主设备号 */
    devid = MKDEV(major, 0);    /*大部分驱动次设备号都选择0*/
    register_chrdev_region(devid, 1, "test");
} else {      /* 没有定义设备号 */
    alloc_chrdev_region(&devid, 0, 1, "test"); /*申请设备号*/
    major = MAJOR(devid);       /* 获取分配号的主设备号 */
    minor = MINOR(devid);       /* 获取分配号的次设备号 */
}

1.2 注册字符设备

Linux中使用cdev表示字符设备,在include/linux/cdev.h中定义:

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops; //文件操作函数集合
    struct list_head list;
    dev_t dev;  //设备号
    unsigned int count;
};
//编写字符设备驱动之前需要定义一个cdev结构体变量

⏩ cdev_init函数:定义好cdev变量后,用该函数进行初始化

⏩ cdev_add函数:向系统添加字符设备(cdev结构体变量)

struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    /* 其他具体的初始项 */
};

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops);  /* 初始化cdev结构体变量 */
cdev_add(&testcdev, devid, 1);     /* 添加字符设备 */

⏩ cdev_del函数:卸载驱动时从内核中删除相应的字符设备

void cdev_del(struct cdev *p)
//p:要删除的字符设备

1.3 自动创建设备节点

旧字符设备驱动开发中,驱动程序加载成功后还需要使用mknod命令手动创建设备节点,十分麻烦。而新字符设备驱动开发中,Linux通过udev用户程序来实现设备文件的自动创建与删除。udev会检测系统中硬件设备状态,并根据硬件设备状态来创建或者删除设备文件。

使用busybox构建根文件系统时,busybox会创建一个udev的简化版本mdev。因此,在嵌入式开发中使用mdev来实现设备节点文件的自动创建与删除。Linux系统中的热插拔事件也由mdev管理,在/etc/init.d/rcS文件中有如下语句:

echo /sbin/mdev > /proc/sys/kernel/hotplug 

⏩ 创建类:自动创建设备节点的工作是在驱动程序入口函数中完成的,一般在cdev_add之后添加相关代码

struct class *class_create (struct module *owner, const char *name)
//owner 一般为 THIS_MODULE
//name 是类名字
//返回值是个指向结构体class的指针,也就是创建的类

删除类:卸载驱动程序时需要删除类

void class_destroy(struct class *cls)
//cls 就是要删除的类

⏩ 创建设备:类创建好后还不能实现自动创建设备节点,还需要在该类下创建一个设备

struct device *device_create(struct class *class, 
                             struct device *parent, 
                             dev_t devt,   
                             void *drvdata,   
                             const char *fmt, ...) 
//class 设备创建在哪个类下
//parent 父设备,一般为NULL
//devt 设备号
//drvdata 设备可能会使用的数据,一般NULL
//fmt 设备名字
//返回值是创建好的设备

删除设备:卸载驱动时需要删除创建的设备

void device_destroy(struct class *class, dev_t devt)
//class 是要删除的设备所处的类
//devt 是要删除的设备号

1.4 设置文件私有数据

每个硬件设备都有一些属性,比如主设备号、类、设备、开关状态等等,在编写驱动时可将这些属性封装成一个结构体。并在编写驱动open函数时将设备结构体作为私有数据添加到设备文件中:

/*设备结构体*/ 
struct test_dev{ 
    dev_t         devid;     /*设备号*/ 
    struct cdev   cdev;      /*cdev*/ 
    struct class  *class;    /*类*/ 
    struct device *device;   /*设备*/ 
    int           major;     /*主设备号*/ 
    int           minor;     /*次设备号*/ 
}; 

struct test_dev testdev; 
/*open函数*/ 
static int test_open(struct inode *inode, struct file *filp) 
{ 
    filp->private_data = &testdev; /*设置私有数据*/ 
    return 0; 
} 

综上所述,新字符设备驱动开发流程如下图所示:

2. 新字符设备驱动开发实验

 

新字符设备驱动开发实验是在Linux字符设备驱动开发模板一文的基础上进行修改,只更改了驱动的编写方式,与应用程序无关,因此只修改驱动程序即可

2.1 驱动程序编写

⏩ 添加定义:宏及字符设备定义

#define CHRDEVBASE_CNT   1            //设备号个数
#define CHRDEVBASE_NAME  "chrdevbase" //名字
/*newchr设备结构体 */
struct newchr_dev{
    dev_t devid;             //设备号
    struct cdev cdev;        //cdev 
    struct class *class;     //类   
    struct device *device;   //设备 
    int major;               //主设备号
    int minor;               //次设备号
};

struct newchr_dev chrdevbase; //自定义字符设备

⏩ 修改open函数:设置私有数据

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    printk("chrdevbase open!rn");
    filp->private_data = &chrdevbase; //设置私有数据
    return 0;
}

⏩ 修改init函数

static int __init chrdevbase_init(void)
{
    /* 注册字符设备驱动 */
    //1、创建设备号
    if (chrdevbase.major) /* 定义了设备号 */
    {
        chrdevbase.devid = MKDEV(chrdevbase.major, 0);
        register_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT, CHRDEVBASE_NAME);
    } 
    else /* 没有定义设备号 */
    {
        alloc_chrdev_region(&chrdevbase.devid, 0, CHRDEVBASE_CNT, CHRDEVBASE_NAME); /* 申请设备号 */
        chrdevbase.major = MAJOR(chrdevbase.devid); /* 获取分配号的主设备号 */
        chrdevbase.minor = MINOR(chrdevbase.devid); /* 获取分配号的次设备号 */
    }
    printk("chrdevbase major=%d,minor=%drn",chrdevbase.major, chrdevbase.minor);  
    //2、初始化cdev
    chrdevbase.cdev.owner = THIS_MODULE;
    cdev_init(&chrdevbase.cdev, &chrdevbase_fops); 
    //3、添加一个cdev
    cdev_add(&chrdevbase.cdev, chrdevbase.devid, CHRDEVBASE_CNT);
    //4、创建类
    chrdevbase.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
    if (IS_ERR(chrdevbase.class)) 
    {
     return PTR_ERR(chrdevbase.class);
     }
    //5、创建设备
    chrdevbase.device = device_create(chrdevbase.class, NULL, chrdevbase.devid, NULL, CHRDEVBASE_NAME);
    if (IS_ERR(chrdevbase.device)) 
    {
        return PTR_ERR(chrdevbase.device);
    }
    
    printk("chrdevbase init done!rn");
    return 0;
}

⏩ 修改exit函数

static void __exit chrdevbase_exit(void)
{
    /* 注销字符设备驱动 */
    cdev_del(&chrdevbase.cdev);/*  删除cdev */
    unregister_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT); /* 注销设备号 */

    device_destroy(chrdevbase.class, chrdevbase.devid);
    class_destroy(chrdevbase.class);
    
    printk("chrdevbase exit done!rn");
}

2.2 程序编译

程序编译包括驱动程序和应用程序编译两个部分:

⏩ 驱动程序编译:创建Makefile文件,使用make命令,编译驱动程序

KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
CURRENT_PATH := $(shell pwd)
obj-m := newchrdev.o

build: kernel_modules

kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

⏩ 应用程序编译:无需内核参与,直接编译即可

arm-linux-gnueabihf-gcc newchrdevApp.c -o newchrdevApp

2.3 运行测试

为了方便,选择通过TFTP从网络启动,并使用NFS挂载网络根文件系统。确保开发板能正常启动,在Ubuntu中将驱动和测试文件复制到modules/4.1.15目录中

⏩ 在开发板中输入如下指令加载驱动模块

depmod                    //第一次加载驱动的时候需要运行此命令
modprobe newchrdev.ko     //加载驱动

⏩ 驱动加载成功后,可以看到自动申请到的主设备号和次设备号

 

⏩ 使用ls /dev/chrdevbase -l命令验证该设备节点文件是否存在,而旧驱动方式需要额外使用mknod指令来手动创建该设备节点文件

 

⏩ 驱动加载成功后,测试APP程序,如下

 

⏩ 测试完使用rmmod指令卸载驱动

以上可见Linux新字符设备驱动开发方式可以自动分配设备号、创建设备节点,使得驱动的使用更加方便、便捷。

相关推荐

电子产业图谱

公众号:嵌入式攻城狮;专注于分享和记录嵌入式开发技术,主要包含C语言、STM32、STM32CubeMX、lwIP、FreeRTOS、Linux、Zigbee、WIFI、BLE、LoRa、NB-loT、PCB电路设计、QT等等。