字符设备驱动

简介

字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备。应用可以通过调用open、close、wirte、read等函数对已经编译好的驱动文件进行读写操作,驱动文件中就需要实现open、close、wirte、read等函数的具体功能。

Linux下的字符设备驱动的框架

在这里插入图片描述

Linux中一切皆为文件,编写好的所有驱动都会在/dev目录下以文件的形式存放。当需要控制外设时,就需要通过读写驱动文件的方式来控制外设的状态。
应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行在内核空间。当用户空间要实现对内核的操作,比如调用open函数打开/dev/SPI驱动,由于用户不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。
应用程序使用的函数在驱动中都有与之对应的函数,比如应用程序调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。在Linux内核文件include/linux/fs.h中有个叫做file_operations的结构体,此结构体就是Linux内核驱动操作函数集合。下面为在此结构体中常用的函数:

函数名描述
owner拥有file_operations结构体模块的指针,一般设置为THIS_MODULE
llseek用于修改文件当前的读写位置
read用于读取设备文件
write用于设备文件写入(发送)数据
poll轮询函数,用于查询设备是否可以进行非阻塞的读写
unlocked_ioctl提供对于设备的控制功能,与应用程序中的ioctl函数对应
compat_ioctl与unlock_ioctl函数功能一样,区别在于该函数用于64位系统,而32位系统用unlock_ioctl函数
mmap用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应
open用于打开设备文件
release用于释放(关闭)设备文件,与
fasync用于刷新待处理的数据,用于将缓存区中的数据刷新到磁盘中
aio_fasync与fasync函数的功能类似,只是此函数用于异步刷新等待处理的数据

Linux驱动架构

在Linux驱动开发中肯定也是要初始化相应的外设寄存器,只是在Linux驱动开中需要按照规定的架构来编写驱动。

Linux驱动运行的两种方式

Linux驱动运行有两种方式:
1)将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。
2)将驱动编译成模块(Linux下模块扩展名位.ko),在Linux内核启动以后使用“insmod”命令加载驱动模块。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

module_init(XXX_Init)	//注册模块加载函数
module_exit(XXX_exit)	//注册模块卸载函数

举例:

/* 驱动入口函数 */
 static int __init xxx_init(void) 
{  
    /* 入口函数具体内容 */
	return 0; 
}
/* 驱动出口函数 */
 static void __exit xxx_exit(void)
 {
 	/* 出口函数具体内容 */
 }
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块;insmod和modprobe,insmod是最简单的模块加载命令,此命令用于加载指定的.ko模块,比如加载drv.ko这个驱动模块,命令如下:

insmod drv.ko

insmod加载模块不能解决依赖关系,而modprobe可以分析依赖关系,然后将所有的依赖模块都加载到内核中。
驱动模块的卸载命令:

rmmod drv.ko
modprobe -r drv.ko //会卸载驱动所依赖的其他模块,建议卸载模块不要使用该命令

字符设备注册与注销

对于字符设备驱动以模块的形式加载成功以后就需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

static inline int register_chrdev(unsigned int major, const char *name,
									const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。

实现设备具体操作函数

驱动的主要目的就是实现file_operations结构体中的成员变量,也就是初始化open、release、read、write。

添加LICENSE和作者信息

LICENSE是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE和作者信息的添加使用如下两个函数:

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

字符驱动开发框架

创建创建chrdevbase.c文件

static struct file_operations test_fops;
/*打开设备*/
 static int chrtest_open(struct inode *inode, struct file *filp){
     /* 用户实现具体功能 */
     return 0;
 }
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
    /*用户实现具体功能*/
    return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}
/* 关闭/释放设备 */
 static int chrtest_release(struct inode *inode, struct file *filp)
 {
     /* 用户实现具体功能 */
		return 0;
 }
static struct file_operations test_fops = {
 .owner = THIS_MODULE, 
 .open = chrtest_open,
 .read = chrtest_read,
 .write = chrtest_write,
 .release = chrtest_release,
 };

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
	int retvalue = 0;
	/* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
	if(retvalue < 0){
    	/* 字符设备注册失败,自行处理 */
    }
    return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */

module_init(xxx_init);
module_exit(xxx_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("heihei");

Linux设备号

设备号的组成

Linux中每个设备都有一个设备号,设备号有主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备 。设备号是由主设备号和次设备号两部分,其中高12位为主设备号(主设备号范围为0-4095),低20位为次设备号。
设备号分配方法:
1)静态分配号
静态分配就是在注册字符设备的时候需要给设备指定一个设备号,比如200这个主设备号。
2)动态分配设备号
静态分配设备号会导致设备号冲突,Linux社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,设备号申请函数如下:

/*申请设备号函数*/
/*
*@param dev:保存申请到的设备号。
*		baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设*		备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
*		count:要申请的设备号数量。
*		name:设备名字。
*
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
/*注销设备号函数*/
/*
*@param from:要释放的设备号。
*		count:表示从 from 开始,要释放的设备号数量。
*
*/
void unregister_chrdev_region(dev_t from, unsigned count)

编写测试APP

C库文件操作基本函数

编写测试APP就是编写Linux应用,需要用C库里面和文件操作有关的一些函数,比如open、read、write、close这四个函数。

open函数

函数原型如下

/*
*@param-pathname:要打开的设备或者文件名
*@param-flags:文件打开模式,以下三种模式必选其一:
			O_RDONLY	只读模式
			O_WRONLY	只写模式
			O_RDWR		读写模式
			O_APPEND 	每次写操作都写入文件的末尾
			O_CREAT 	如果指定文件不存在,则创建这个文件
			O_EXCL 		如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
			O_TRUNC 	如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
			O_NOCTTY 	如果路径名指向终端设备,不要把这个设备用作控制终端
			O_NONBLOCK 	如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞DSYNC 等待物理 I/O 结束								后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新
			O_RSYNC 	read 等待所有写入同一区域的写操作完成后再进行
			O_SYNC 		等待物理 I/O 结束后再 write,包括更新文件属性的 I/O
*@return 如果文件打开成功的话返回文件的文件描述符
*/
int open(const char *pathname, int flags)
read函数

函数原型如下

/*
*@param-fd:要打开的设备或者文件名
*@param-buf:数据读取到此buf中
*@param-count:要读取的数据长度,也就是字节数
*@return 读取成功的话返回读取到的字节数;如果返回0表示读取到了文件末尾;如果返回负值,表示读取失败。
*/
ssize_t read(int fd, void *buf, size_t count)
write 函数

函数原型如下

/*
*fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符
*buf:要写入的数据
*count:要写入的数据长度,也就是字节数
*/
ssize_t write(int fd, const void *buf, size_t count);
close函数
/*
*fd:要关闭的文件描述符
*/
int close(int fd);

编写驱动测试APP

创建chrdevbaseApp.c文件

 #include "stdio.h"
 #include "unistd.h"
 #include "sys/types.h"
 #include "sys/stat.h"
 #include "fcntl.h"
 #include "stdlib.h"
 #include "string.h"
 static char usrdata[] = {"usr data!"}; //app向设备中写入的数据

 /*
 * @description : main 主程序
 * @param - argc : argv 数组元素个数
 * @param - argv : 具体参数
 * @return : 0 成功;其他 失败
 */
 int main(int argc, char *argv[])
 {
 	int fd, retvalue;
 	char *filename;
 	char readbuf[100], writebuf[100];

 	if(argc != 3){//参数如果不为3的话表示测试APP用法错误 (详细看注释①)
 		printf("Error Usage!\r\n");
 		return -1;
 	}

 	filename = argv[1]; //获取要打开的设备文件名字,argv[1]保存着文件名字。

 	/* 打开驱动文件 */
 	fd = open(filename, O_RDWR); //调用 C 库中的 open 函数打开设备文件:/dev/chrdevbase
 	if(fd < 0){
 		printf("Can't open file %s\r\n", filename);
 		return -1;
 	}
	/*判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换		为真实的数字。*/
 	if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
 	retvalue = read(fd, readbuf, 50);/*当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的数										据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据。*/

 	if(retvalue < 0){
 		printf("read file %s failed!\r\n", filename);
 	}else{
 		/* 读取成功,打印出读取成功的数据 */
 		printf("read data:%s\r\n",readbuf);
 	}
 }
	/*当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据*/
	if(atoi(argv[2]) == 2){
	/* 向设备驱动写数据 */
		memcpy(writebuf, usrdata, sizeof(usrdata));
		retvalue = write(fd, writebuf, 50);
	if(retvalue < 0){
 		printf("write file %s failed!\r\n", filename);
	}
}

/* 关闭设备 */
retvalue = close(fd);/*对 chrdevbase 设备操作完成以后就关闭设备*/
if(retvalue < 0){
 	printf("Can't close file %s\r\n", filename);
 	return -1;
 }

 return 0;
}

main 函数的 argc 参数表示 参数数量,argv[]保存着具体的参数。
注释
①比如, 现在要从 chrdevbase 设备中读取数据,需要输入如下命令:

./chrdevbaseApp /dev/chrdevbase 1

上述命令一共有三个参数“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,这三个参数分别 对应 argv[0]、argv[1]和 argv[2]。第一个参数表示运行 chrdevbaseAPP 这个软件,第二个参数表 示测试APP要打开/dev/chrdevbase这个设备。第三个参数就是要执行的操作,1表示从设备中读取数据,2 表示向 设备中写数据。

编译驱动和APP文件

编译驱动文件

创建Makefile文件,并填入以下内容:

KERNELDIR := /home/heihei/MX6U/kenerl/mx6ull_kenerl
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

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

KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根 据自己的实际情况填写即可。

CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。

obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块

编译APP文件

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

编译完成后可以用 file chrdevbaseApp 查看程序文件的信息

测试

将编译完成的chrdevbase.ko和chrdevbaseApp 拷贝到nfs/rootfs/lib/mouble/4.1.15/

加载驱动
depmod //自动生成动生成 modules.alias、modules.symbols 和 modules.dep
insmod chrdevbase.ko
modprobe chrdevbase.ko
查看已经挂载的驱动
lsmod 				//查看当前系统中存在的模块
cat /proc/devices 	//查看当前系统中的所有设备
创建设备节点文件

驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。

mknod /dev/chrdevbase c 200 0

mknod是创建节点命令,“/dev/chrdevbase”是创建的节点文件,c 表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在“/dev/chrdevbase”这个文件是chrdevbase设备在用户空间中的实现。

操作设备
./chrdevbaseApp /dev/chrdevbase 1 //
./chrdevbaseApp /dev/chrdevbase 2 //
卸载驱动模块
rmmod chrdevbase.ko

总结

1)字符驱动设备其实就是对file_operations结构体成员变量的具体功能的实现,编写完驱动后还需要编写对应的测试APP。
2)编写字符驱动要对芯片对应外设的寄存器很熟悉,了解如何驱动该外设的方法。

参考

《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嘉鑫的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值