0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

i.MX6ULL驱动开发2—新字符设备开发模板

码农爱学习 来源:码农爱学习 作者:码农爱学习 2022-03-17 09:11 次阅读

上篇文章介绍了字符设备的开发模板,但那是一种旧版本的驱动开发模式,设备驱动需要手动分配设备号再使用 register_chrdev进行注册,加载成功以后还需要手动使用mknod命令创建设备节点,比较麻烦

目前Linux内核推荐的新字符设备驱动API函数,使得驱动的使用更加自动化,本篇就来一起研究下。

1 旧字符设备驱动的弊端

使用register_chrdev函数注册字符设备,需要指定一个设备号,这就造成:

需要事先确定好哪些主设备号没有使用

会将一个主设备号下的所有次设备号都使用掉,比如主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被占用了

回顾上一篇的操作,先是加载驱动:

pYYBAGIyAAmAFyp0AAB4iFrzgsc420.png

加载完,还有手动使用mknod指令来手动创建该设备节点,并且指定驱动程序中写死的设备号:

pYYBAGIyAA-AX6J5AAAy_IcL8LE995.png

本篇,就要使用一种新的字符驱动编写方式,实现设备号的自动分配,省去mknod指令操作

2 新字符设备驱动原理

2.1 分配和释放设备号

使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。

使用如下函数来申请设备号(该函数在上篇提到过):

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

如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:

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

注销字符设备之后要释放设备号,不管是通过alloc_chrdev_region函数的动态分配还是register_chrdev_region函数手动指定的设备号,统一使用(和上篇使用的一样)的释放函数:

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

新字符设备驱动下,设备号分配示例代码如下:

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);       /*获取分配号的次设备号*/ 
}

2.2 字符设备注册

2.2.1 cdev字符设备结构

在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; 
};

2.2.2 cdev_init 函数

定义好cdev变量以后就要使用cdev_init函数对其进行初始化:

/*
* cdev:要初始化的cdev结构体变量
* fops:字符设备文件操作函数集合
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops) 

该函数的使用示例如下:

/*要初始化的cdev结构体*/
struct cdev testcdev; 

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

testcdev.owner = THIS_MODULE;

/* 初始化cdev*/ 
cdev_init(&testcdev, &test_fops); 

2.2.3 cdev_add函数

该函数用于向Linux系统添加字符设备,即cdev结构体变量:

/*
* cdev:要初始化的cdev结构体变量
* dev:字符设备所使用的设备号
* count:要添加的设备数量
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count) 

2.2.4 cdev_del函数

卸载驱动的时候要使用cdev_del函数从Linux内核中删除字符设备:

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

2.3 自动创建设备节点

上篇的Linux驱动实验中,在使用modprobe加载驱动程序以后还需要使用“mknod”命令手动创建设备节点,比较麻烦,这里就来研究一下如何实现自动创建设备节点。

2.3.1 mdev机制

在Linux下通过udev来实现设备文件的自动创建与删除。使用busybox构建根文件系统的时候,busybox会创建一个udev的简化版本mdev

所以,在嵌入式开发中使用mdev来实现设备节点文件的自动创建与删除。Linux系统中的热插拔事件也由mdev 管理,在/etc/init.d/rcS 文件中如下语句:

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

2.3.2 创建和删除类

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

首先要创建一个class类,其实是个结构体,定义在include/linux/device.h里面。class_create是类创建函数(宏定义):

#define class_create(owner, name) \ 
({ \ 
    static struct lock_class_key __key; \ 
    __class_create(owner, name, &__key); \ 
}) 

struct class *__class_create(struct module *owner, 
                             const char *name, 
                             struct lock_class_key *key) 

卸载驱动程序的时候需要使用函数为class_destroy删除掉类

/*
* cls:要删除的类
*/
void class_destroy(struct class *cls); 

2.3.3 创建设备

创建好类以后还不能实现自动创建设备节点,还需要在这个类下创建一个设备。使用device_create函数创建设备:

/*
* class:设备要创建哪个类下面
* parent:父设备, 一般为 NULL
* devt:设备号
* drvdata:设备可能会使用的一些数据,一般为 NULL
* fmt:设备名字
*/
struct device *device_create(struct clas *class,  
                             struct device *parent,  
                             dev_t devt,  
                             void *drvdata,  
                             const char *fmt, ...) 

参数最后的...表示这在是一个可变参数的函数。

2.4 设置文件私有数据

每个硬件设备都有一些属性, 比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式:

 dev_t         devid;     /*设备号*/ 
struct cdev   cdev;      /*cdev*/ 
struct class  *class;    /*类*/ 
struct device *device;   /*设备*/ 
int           major;     /*主设备号*/ 
int           minor;     /*次设备号*/ 

可以将所有属性信封装到结构体中, 并在编写驱动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; 
} 

3 驱动程序编写

在上篇的基础上进行修改,因为只是更换的驱动程序的编写方式,与应用程序无关,因此只修改驱动程序即可。

3.1 添加一些定义

因为上篇文章的代码中使用的是chrdevbase这个名称,为了减少修改量,这里仅把结构体类型定义为带有new标志的newchr_dev,变量名仍使用chrdevbase这个名称。

#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; /* 自定义字符设备 */

3.2 修改open函数

在上篇程序的基础上增加了一条“设置私有数据”

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

3.3 修改init函数

这个修改比较大,因为要在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=%d\r\n",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!\r\n");
	return 0;
}

3.4 修改exit函数

因为init修改较大,对应的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!\r\n");
}

至此,修改完毕,其它的与之前的一样。

3.5 新旧驱动方式对比

通过一张图来对比新旧两种驱动编写方式的区别

旧方式编写驱动的流程

pYYBAGIyAdOAKvkdAACBY8npGD4752.png

新方式编写驱动的流程

pYYBAGIyAdiAMiETAAEM8HQykVI076.png

可以看出主要区别在驱动的加载和卸载。

4 编译驱动

和上次编译驱动的方式一样,使用makefile,因为驱动的c文件名由chrdevbase.c改为了newchrdevbase.c,因此makefile文件中也要把名字改掉。

编译完之后,将编译出的ko文件先复制到ubuntu虚拟机的tftpboot目录中,为后面的测序做准备。

pYYBAGIyAeCARtBwAACum9V3bvM294.png

复制后,看一下tftpboot目录:

poYBAGIyAeWAcC6OAABRgY3MQbY132.png

5 程序测试

5.1 文件发送到板子

和上篇一样,使用tftp传输,将ubuntu虚拟机编译出的ko文件发送到linux板子中

再来看下tftp传输的硬件环境示意图:

poYBAGIx__mAJAMxAAFqlflsgR8688.png

然后是传输指令以及传输结果,可以看到newchrdevbase.ko已经从ubuntu虚拟机的tftpboot目录传输到了linux板子的/lib/modules/4.1.15目录中了。

pYYBAGIyAfuAE3rpAACDeMdklK0816.png

5.2 测试

输入如下两条指令加载 newchrdevbase.ko 驱动模块:

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

驱动加载成功后,可以看到自动申请到的主设备号和次设备号,如下图,主设备号为249。

再输入ls /dev/chrdevbase -l指令验证/dev/chrdevbase 这个设备节点文件是否存在,如下图,可以看到设备存在,注意和上篇旧驱动方式操作上的不同之处,旧的驱动方式需要额外使用mknod指令来手动创建该设备节点

poYBAGIyAgGAbiVoAABRaJSZ9J0244.png

驱动已经加载成功,再来测试APP程序,理论上和上篇的效果一样,实测也是:

pYYBAGIyAgaAb-B4AAB-1c076Y0255.png

OK,测试完毕,测试完使用rmmod指令卸载驱动。

6 总结

此篇文章针对上篇文章使用旧字符驱动编写方式存在的不足,介绍了一种新的字符驱动编写方式,对比两种方式编写的主要区别,在上篇驱动代码的基础上进行修改,并测试通过,和上篇实现一样的效果,但驱动的加载更加方便,不再需要人为指定设备号。

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 设备
    +关注

    关注

    2

    文章

    4172

    浏览量

    69226
  • 模板
    +关注

    关注

    0

    文章

    106

    浏览量

    20478
  • 函数
    +关注

    关注

    3

    文章

    3866

    浏览量

    61308
收藏 人收藏

    评论

    相关推荐

    i.MX6ULL驱动开发1—字符设备开发模板

    本篇介绍了嵌入式Linux驱动开发中的基础驱动——字符驱动开发的基本模式,使用了一个虚拟的
    的头像 发表于 03-17 09:13 3017次阅读
    <b class='flag-5'>i.MX6ULL</b><b class='flag-5'>驱动</b><b class='flag-5'>开发</b>1—<b class='flag-5'>字符</b><b class='flag-5'>设备</b><b class='flag-5'>开发</b><b class='flag-5'>模板</b>

    i.MX6UL/i.MX6ULL开发常见问题】单独编译内核,uboot生成很多文件,具体用哪一个?

    i.MX6UL/i.MX6ULL开发常见问题》基于米尔电子 i.MX6UL/i.MX6ULL产品(V.10)2.3单独编译内核,uboot
    发表于 07-01 17:50

    I.MX6ULL终结者开发板支持JTAG调试

    的硬件环境1、i.MX6ULL终结者开发板一块 2、JLNK V9下载器一个 3、JLINK V9转换板一个(2.54mm转2.0mm) 1.2 搭建开发环境1.2.1 安装JLINK
    发表于 04-26 15:11

    迅为I.MX6ULL终结者开发板支持JTAG调试

    的硬件环境1、迅为-i.MX6ULL终结者开发板一块2、JLNK V9下载器一个3、JLINK V9转换板一个(2.54mm转2.0mm)1.2 搭建开发环境1.2.1 安装JLINK
    发表于 05-06 14:09

    移植5.4内核到迅为I.MX6ULL开发

    ),选择5.4.3内核编译生成的对应镜像和设备树文件(关于i.MX6ULL终结者开发板镜像的烧写,大家可以参考开发板使用手册的:第六章 I.MX6U
    发表于 06-29 10:13

    I.MX6ULL终结者开发板裸机仿真jlink调试

    I.MX6ULL‘终结者’开发板预留了JTAG仿真接口,并给出了开发文档,可以实现在JLINK仿真器条件下的单步跟踪、断点调试等功能,使得开发研究i
    发表于 07-07 10:56

    评测丨再次进阶,更高性价比——飞凌i.MX6ULL开发

    。FETMX6ULL-S将CPU更换为NXP的i.MX6ULL,主频提升到800MHz;成为FETMX6UL-C核心板的互补产品,而且在功能和FETMX6UL-C完全兼容。
    发表于 10-21 15:28

    i.MX6ULL开发板硬件资源

    迅为i.MX6ULL 终结者开发板硬件资源非常丰富,几乎将 i.MX6ULL 芯片的所有资源都扩展引出到底板上了,底板提供了丰富的外设接口,开发板的尺寸是 190mm*125mm,充分
    发表于 12-29 06:18

    介绍UART在i.mx6ull上的开发步骤

    的HAL库开发在之前文章已经提及,本篇文章将介绍UART在i.mx6ull上的开发步骤。由于此芯片的裸机开发在Linux下并使用ARM-Linux交叉编译生成可执行文件下载至板中进行运
    发表于 12-16 06:34

    初识 i.MX6ULL 寄存器

    裸机开发_L1_汇编LED实验0. 本节目标1. 硬件层电路2. 初识 i.MX6ULL 寄存器2.1 i.MX6ULL 时钟控制寄存器2.2 i.
    发表于 12-20 07:13

    I.MX6ULL无法枚举USB2514是为什么?

    你好目前,I.MX6ULL开发存在一些问题。其中之一是OTG USB2无法正常挂载USB2514,无法正确枚举下游设备,只显示设备id。us
    发表于 04-03 06:55

    I.MX6ULL UART传输问题求解

    I.MX6ULL UART传输问题
    发表于 04-21 08:09

    如何在i.MX6ULL上为PF1510配置i2c?

    我们计划将 PF1510 与 i.MX6ULL 处理器一起使用。我看到这个设备树示例: https://github.com/Freescale/linux-fslc/blob
    发表于 05-17 14:02

    i.MX6ULL驱动开发4——点亮LED(寄存器版)

    本篇主要介绍了如何通过操作寄存器来点亮i.MX6ULL开发板上的led,通过编写LED对应的驱动程序和应用程序,实现程序设计的分层。
    的头像 发表于 05-21 21:26 2647次阅读
    【<b class='flag-5'>i.MX6ULL</b>】<b class='flag-5'>驱动</b><b class='flag-5'>开发</b>4——点亮LED(寄存器版)

    Linux新字符设备驱动开发方式

    Linux字符设备驱动开发模板中介绍了旧版本的驱动开发
    的头像 发表于 04-14 12:02 644次阅读
    Linux<b class='flag-5'>新字符</b><b class='flag-5'>设备</b><b class='flag-5'>驱动</b><b class='flag-5'>开发</b>方式