i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、
【公众号】迅为电子
【粉丝群】258811263(加群获取驱动文档+例程)
第 四十五 章 注册字符类设备
本章导读
在整个Linux设备驱动的学习中,字符设备驱动较为基础。本章将讲解注册字符类设备的编程方法。
45.1章节讲解了注册字符设备的基本步骤
45.2章节编写注册字符设备的驱动程序
45.3章节编写应用层测试程序
45.4章节编译驱动程序为驱动模块
本章内容对应视频讲解链接(在线观看):
注册字符类设备 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=16
程序源码在网盘资料"iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\07-注册字符类设备"路径下。
45 .1 注册字符类设备 简介
在Linux内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如下:
cpp
struct cdev { //描述字符设备的一个结构体
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号。使用下列宏可以从dev_t获得主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成dev_t
MKDEV(int major, int minor)
cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux内核提供了一组函数以用于操作cdev结构体:
cpp
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。
|-------|------------------------------------------------------------------|
| 函数 | void cdev_init(struct cdev *, const struct file_operations *); |
| 第一个参数 | 要初始化的cdev |
| 第二个参数 | 文件操作集 cdev->ops = fops; //实际就是把文件操作集写给ops |
| 功能 | cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的连接。 |
|-------|------------------------------------------------------------------|
| 函数 | void cdev_init(struct cdev *, const struct file_operations *); |
| 第一个参数 | 要初始化的cdev |
| 第二个参数 | 文件操作集 cdev->ops = fops; //实际就是把文件操作集写给ops |
| 功能 | cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的连接。 |
|-------|--------------------------------|
| 函数 | void cdev_del(struct cdev *); |
| 第一个参数 | cdev的结构体指针 |
生成设备节点
字符设备注册完以后不会自动生成设备节点。我们需要使用mknod命令创建一个设备节点
格式:mknod 名称 类型 主设备号 次设备号
举例:
mknod /dev/test c 247 0
45 .2 编写字符设备驱动
通过45.1章节注册字符类设备的学习,我们已经把基本概念搞懂了。我们在ubuntu的/home/topeet/imx8mm/07目录下新建chrdev.c文件。我们可以在上次实验编写的chrdev.c基础上进行编辑。
完整代码如下所示:
cpp
/*
* @Descripttion: 注册字符设备
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/fs.h> //包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations
#include <linux/kdev_t.h>
#include <linux/cdev.h> // 对字符设备结构cdev以及一系列的操作函数的定义。包含了cdev 结构及相关函数的定义。
#define DEVICE_NUMBER 1 //定义次设备号的个数
#define DEVICE_SNAME "schrdev" //定义静态注册设备的名称
#define DEVICE_ANAME "achrdev" //定义动态注册设备的名称
#define DEVICE_MINOR_NUMBER 0 //定义次设备号的起始地址
static int major_num, minor_num; //定义主设备号和次设备号
struct cdev cdev; //定义一个cdev结构体
module_param(major_num, int, S_IRUSR); //驱动模块传入普通参数major_num
module_param(minor_num, int, S_IRUSR); //驱动模块传入普通参数minor_num
int chrdev_open(struct inode *inode, struct file *file)
{
printk("chrdev_open\n");
return 0;
}
// file_operations chrdev_ops
struct file_operations chrdev_ops = {
.owner = THIS_MODULE,
.open = chrdev_open};
static int hello_init(void)
{
dev_t dev_num;
int ret; //函数返回值
if (major_num)
{
/*静态注册设备号*/
printk("major_num = %d\n", major_num); //打印传入进来的主设备号
printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
dev_num = MKDEV(major_num, minor_num); //MKDEV将主设备号和次设备号合并为一个设备号
ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); //注册设备号
if (ret < 0)
{
printk("register_chrdev_region error\n");
}
printk("register_chrdev_region ok\n"); //静态注册设备号成功
}
else
{
/*动态注册设备号*/
ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME);
if (ret < 0)
{
printk("alloc_chrdev_region error\n");
}
printk("alloc_chrdev_region ok\n"); //动态注册设备号成功
major_num = MAJOR(dev_num); //将主设备号取出来
minor_num = MINOR(dev_num); //将次设备号取出来
printk("major_num = %d\n", major_num); //打印传入进来的主设备号
printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
}
cdev.owner = THIS_MODULE;
//cdev_init函数初始化cdev结构体成员变量
cdev_init(&cdev, &chrdev_ops);
//完成字符设备注册到内核
cdev_add(&cdev, dev_num, DEVICE_NUMBER);
return 0;
}
static void hello_exit(void)
{
unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); //注销设备号
cdev_del(&cdev);
printk("gooodbye! \n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
45 . 3 编写应用程序
编写应用程序如下所示:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int fd;
char buf[64] = {0};
fd = open("/dev/test",O_RDWR); //打开设备节点
if(fd < 0)
{
perror("open error \n");
return fd;
}
//read(fd,buf,sizeof(buf)); //从文件中读取数据放入缓冲区中
close(fd);
return 0;
}
我们将app.c文件拷贝到Ubuntu的/home/topeet/imx8mm/07目录下,输入以下命令编译app.c。生成的app。
45.4 开发板实验
这里我们以iTOP-iMX8MM开发板为例,将刚刚45.2章节编写的驱动代码编译成模块。将上次编译chrdev.c的Makefile文件和build.sh文件拷贝到chrdev.c同级目录下,修改Makefile为:
cpp
obj-m += chrdev.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
make -C $(KDIR) M=$(PWD) clean
文件如下图所示:
驱动编译成功如下图所示:
我们通过nfs将编译好的驱动程序加载模块,进入共享目录,加载驱动模块如下图所示:
insmod chrdev.ko
从上图可知,动态申请好了字符设备号,主设备号是511,次设备号是0。但是我们想要验证我们的字符设备是否注册成功,需要运行应用程序。我们输入"mknod /dev/test c 511 0"创建设备节点,然后再运行45.3章节编译好的APP应用程序,如下图所示:
如上图所示,应用程序APP成功地打开了设备节点,说明字符设备注册成功。