字符设备是Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。在详细的学习字符设备驱动架构之前,我们先来简单的了解一下Linux 下的应用程序是如何调用驱动程序的,Linux 应用程序对驱动程序的调用如下图 所示:
其中关于 C库以及如何通过系统调用 “陷入 到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open这个函数,那么在驱动程序中也得有一个名为 open的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux内核文件 include/linux/fs.h中有个叫做 file_operations的结构体,此结构体就是 Linux内核驱动操作函数集合,内容如下所示:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init函数用来向 Linux内核注册一个模块加载函数,参数 xxx_init就是需要注册的具体函数,当使用“ insmod”命令加载驱动的时候 xxx_init这个函数就会被调 用。 module_exit()函数用来向 Linux内核注册一个模块卸载函数,参数 xxx_exit就是需要注册的具体函数,当使用“ rmmod”命令卸载具体驱动的时候 xxx_exit函数就会被调用。所以一般在xxx_init函数里进行一些驱动的初始化工作,在xxx_exit里面就需要对驱动程序的卸载做一些回收工作。
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_operation结构体,对file_operations结构体成员进行实例化。
假设实现一个chttest驱动,打开open和关闭close是最基本的要求,几乎所有设备都得提供打开和关闭的功能,因此我们要实现file_operations结构体中的open和release两个函数。
假设chrtest这个设备控制着一段缓冲区,应用程序需要哦通过read和write这两个函数来对缓冲区的进行读写操作,那么我们也得实现file_operations结构体中的read和write两个函数。
假设基本需求就这些,那么驱动框架如下:
/*1、实现file_operations结构体中的open、release、read、write函数*/
static int chrtest_open(struct inode *inode,struct file *filp)
{
//具体内容根据具体需求实现
return 0;
}
//从设备读取数据
static ssize_t chrtest_read(struct file *filp,char __user *buf,size_t cnt,loff_t * offt)
{
//具体内容根据具体需求实现
return 0;
}
//向设备写入数据
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 *indoe,struct file *filp)
{
//具体内容根据具体需求实现
return 0;
}
//初始化file_operations结构体
static struct file_operations test_fops=(void)
{
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
//驱动入口函数
static int __init xxx_init(void)
{
int ret=0;
//
ret = register_chrtest(200,"chrtest",&test_fops);
if(ret<0)
{
//出错处理
}
return 0;
}
//驱动出口函数
static void __exit xxx_exit(void)
{
//注销字符设备驱动
unregister_chrdev(200,"chrtest");
}
//指定驱动出入口函数
module_init(xxx_init);
module_exit(xxx_exit);
//添加LICENSE和作者信息
MODULE_LICENSE() //添加模块 LICENSE信息
MODULE_AUTHOR() //添加模块作者信息
为了方便管理, Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux提供了一个名为 dev_t的数据类型表示设备号, dev_t定义在文件 include/linux/types.h里面,定义如下:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
可以看出 dev_t是 __u32类型的,而 __u32定义在文件 include/uapi/asm-generic/int-ll64.h里面,定义如下:
typedef unsigned int __u32;
综上所述, dev_t其实就是 unsigned int类型,是一个 32位的数据类型。这 32位的数据构成了主设备号和次设备号两部分,其中高12位为主设备号, 低 20位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。
静态分配号,可以由开发者自己确定设备号,但是会有可能指定到一个正在使用的设备号,这是静态分配设备号的缺点。在确定设备号前,可以先查看设备号是否被使用,可以用cat /proc/devices查看。
动态分配设备号有系统分配一个未被使用的设备号,这样也就不会造成冲突了,但是缺点就是分配之后的设备号到底是多少,是不确定的。
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
dev: 保存申请到的设备号
baseminor:此设备号的起始地址,alloc_chrdev_region可以申请到一端连续的多个设备号,这些设备号的主设备号都一样,但是次设备号不同,次设备号以baseminor为起始地址开始递增。一般baseminor为0,所以说此设备号从0开始。
count:要申请的设备号数量。
name:设备名字
void unregister_chrdev_region(dev_t from,unsigned count)
from:要释放的设备号
count:表示从from开始,要释放的设备号数量
实现对设备缓冲区数据的读取和写入
#include
#include
#include
#include
#include
/*test file*/
#define CHRDEVBASE_MAJOR 200 /*Main equipment num*/
#define CHRDEVBASE_NAME "chrdevbase" /*Secondary equipment num*/
static char readbuf[100]; /*read buffer*/
static char writebuf[100]; /*write buffer*/
static char kerneldata[] = {"kernel data!"};
static int chrdevbase_open(struct inode *inode,struct file *filp)
{
return 0;
}
static ssize_t chrdevbase_read (struct file *filp,char __user *buf,
size_t cnt,loff_t *offt)
{
int rv=0;
memcpy(readbuf,kerneldata,sizeof(kerneldata));
rv = copy_to_user(buf,readbuf,cnt);
if(0==rv)
{
printk("kernel senddata ok !\r\n");
}
else
{
printk("kernel senddata failure!\r\n");
}
return 0;
}
static ssize_t chrdevbase_write(struct file *filp,
const char __user *buf,size_t cnt,loff_t *offt)
{
int rv=0;
rv = copy_from_user(writebuf,buf,cnt);
if(0==rv)
{
printk("kernel recevdata:%s\r\n",writebuf);
}
else
{
printk("kernel recevdata failure\r\n");
}
return 0;
}
static int chrdevbase_release(struct inode *inode,struct file *filp)
{
return 0;
}
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
static int __init chrdevbase_init(void)
{
int rv=0;
rv = register_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME,
&chrdevbase_fops);
if(rv<0)
{
printk("chrdevbase driver register failure\r\n");
}
printk("chrdevbase_init()\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME);
printk("chrdevbase_exit()\r\n");
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dairongan");
Makefile
#编译环境,提供一些内核需要的API等
KERNELDIR := /home/dra/imx6ull_file/bsp/kernel/linux-imx
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
#-C表示将当前的工作目录切换到指定的目录中
#M表示模块源码目录
应用层测试程序
/*************************************
*文件名 :chrdevbaseApp.c
*作者 :dra
*版本 :V1.0
*描述 :chrdevbase驱动测试APP
*其他 :使用方法:./chrdevbaseApp /dev/chrdevbase <1>|<2>
argv[2] 1:读文件
argv[2] 2:写文件
**************************************/
#include
#include
#include
#include
#include
#include
#include
static char usrdata[]={"usr data!"};
int main(int argc,char **argv)
{
int fd,rv;
char *filename;
char readbuf[100],writebuf[100];
if(argc != 3)
{
printf("Error Usage!\r\n");
printf("exemple:./chrdevbaseApp /dev/chrdevbase <1>|<2>\r\n");
return -1;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if(fd<0)
{
printf("Can't open file %s\r\n",filename);
return -1;
}
if(atoi(argv[2]) == 1)
{
rv = read(fd,readbuf,50);
if(rv<0)
{
printf("read file %s failure\r\n",filename);
}
else
{
printf("read data:%s\r\n",readbuf);
}
}
if(atoi(argv[2]) == 2)
{
memcpy(writebuf,usrdata,sizeof(usrdata));
rv = write(fd,writebuf,50);
if(rv<0)
{
printf("write file %s failure\r\n",filename);
}
else
{
printf("write data:%s\r\n",writebuf);
}
}
rv = close(fd);
if(rv<0)
{
printf("Can't close file %s\r\n",filename);
return -1;
}
return 0;
}
编译生成ko文件后,将其放到开发板合适的位置上后:
1、insmod chrdevbase.ko加载驱动
2、lsmod 检查驱动是否加载成功
3、cat /proc/devices查看设备号
4、mknod /dev/chrdevbase c 200 0创建设备节点 c表示字符设备,200为主设备号,0为次设备号。
5、运行测试文件