四、【字符设备】基本字符设备驱动模型

一. 设计驱动程序的步骤:

 

1、定义一个字符设备结构体struct cdev,用来描述某个字符设备

struct cdev {
	struct kobject kobj;          //内核管理字符设备时使用该程序,驱动程序设计不需要初始化它
	struct module *owner;          //当前字符设备属于哪个内核模块,一般设置THIS_MODULE
	const struct file_operations *ops;  //文件操作集合
	struct list_head list;         //用于管理cdev的内核链表的节点
	dev_t dev;                 //用来存放设备号
	unsigned int count;            //此设备(号)的个数
};

2、初始化字符设备结构体,其中有一个非常重要的成员,文件操作集合成员

        struct file_operations-------需要事先定义文件操作集合结构体

void cdev_init(struct cdev *cdev, const struct file_operations *fops)   //初始化字符设备结构体

参数:

  • cdev:字符设备
  • fops:文件操作集合 
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
};

3、申请设备号(主设备号,次设备号)

(1)设备号介绍
  • 设备号是一个32bit的无符号整数。
  • 高12bit是主设备号——表示某一类设备,主设备号相同,使用同一个驱动程序
  • 低20bit是次设备号——表示这一类设备中的具体某个设备。
  • 完整的设备号=(major<<20|(minor<<0)。

如下图,都使用mmc的驱动程序,不同的设备次设备号不一样,以此来区分不同的设备。  

  

      

  • 内核中提供大的设备号相关的宏
#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))  //从完整设备号中获取主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))  //从完整设备号中获取次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))       //用主次设备号合成完整设备号
(2)设备号的动态申请
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)

参数:

  • dev:指向的内存用来存申请到的设备号的第一个。
  • baseminor:次设备号从几开始。
  • count:申请几个。
  • name:设备名。注意:设备名和设备文件名不是一样的。用同一个驱动的设备设备名一样,但是设备文件名不一样,应用层是通过设备文件名去访问驱动程序的。

返回值:

  • 申请成功返回0,失败返回负的错误码。
(3)设备号的静态申请
int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数:

  • from:指定设备号的第一个。
  • 申请几个
  • 自定义的设备名

返回值:

  • 申请成功返回0;失败返回负的错误码。
(4)释放设备号
void unregister_chrdev_region(dev_t from, unsigned count) 
  • from:从哪个设备号开始释放。
  • count:释放几个。  

4、字符设备的注册和注销

(1)注册
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

参数:

  • p:指向待注册的字符设备结构体
  • dev:设备号
  • count:注册几个。

返回值:

  • 注册成功返回0,失败则返回错误码。  
(2)注销
void cdev_del(struct cdev *p)

5、创建跟字符设备对应的设备文件-----应用程序操作设备文件

(1)手动创建----mknod命令
  • 如果驱动程序中没有自动创建设备文件,先将驱动文件first_cdev_drv.ko进行安装 
insmod firt_cdev_drv.ko 
  • 查看驱动安装是否成功
cat /proc/devices 
  • 查看驱动程序中申请的主设备号和次设备号,然后手动创建设备文件
mknod  /dev/first_cdev_node  c  244  0 
  • 创建成功后,在/dev/目录下会看到一个first_cdev_node的设备文件。

注意:

  手动创建的设备文件,在驱动模块卸载时,不会自动删除,如果需要删除,使用rm命令删除即可。

rm /dev/first_dev_node
(2)自动创建

  基本原理:驱动程序提供信息(设备文件名、设备号),根文件系统中的应用程序mdev根据驱动提供的信息,在/dev目录下自动创建出设备文件

  • 创建class(创建目录)
struct class * class_create (struct module *owner, const char *name)    

参数:
  owner : 一般设置为THIS_MODULE.
  name : 自定义的class名字
  • 销毁class
void class_destroy(struct class *cls)
  • 在class下创建device
struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
参数:
 
 class :在哪个class创建device。

  parent:待创建设备的父设备是哪个。
  devt: 设备号。
  drvdata: 设备驱动的私有数据,一般设置为NULL.
  fmt:自定义的device的名字(设备文件名)。

返回值:
 
 成功返回strct device*.

  失败返回ERR_PTR()
  •  销毁设备
void device_destroy(struct class *class, dev_t devt)

6、申请IO内存(为了资源的互斥访问)和释放IO内存

(1)申请IO内存
request_mem_region(start,n,name)
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
struct resource * __request_region(struct resource *parent, resource_size_t start, resource_size_t n, const char *name, int flags)

参数:

  • Start:要申请使用的IO内存(物理内存)的起始地址
  • n  :要申请的IO内存的大小(字节为单位)        
  • name:自定义的IO内存的名字

返回值:

  • 成功返回struct resource *。
  • 失败返回NULL。
(2)释放IO内存
#define release_mem_region(start,n)	__release_region(&iomem_resource, (start), (n))
void __release_region(struct resource *parent, resource_size_t start,resource_size_t n)

7、把IO内存映射成虚拟内存,得到虚拟地址

(1)映射虚拟内存
void __iomem *ioremap(unsigned long phys_addr, size_t size)

参数:

  • phys_addr:待映射为虚拟内存的物理内存的起始地址。
  • size: 大小,字节为单位。

返回值:

         成功返回与物理内存对应的虚拟内存的起始地址。

(2)取消映射  
void iounmap(volatile void __iomem *io_addr) 

8、驱动程序与应用程序之间的数据交互

(1)写操作

应用层

write(fd, buf, size);  

驱动层:

static ssize_t xxx_write (struct file *filp, const char __user *user, size_t size, loff_t *oft) 
{
  copy_from_user
}

驱动层主要将用户空间的数据拷贝到驱动层的函数:

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

函数类型:

  • __must_check :必须检查该函数的返回值,否则编译时报警告。
  • __user : from指针指向的是用户空间某处。

参数:

  • to:指向内核空间的某片内存,这片内存用于存储从用户空间拷贝过来的数据
  • from: from指针指向的是用户空间某处,从用户空间的此处拷贝数据
  •  n: 拷贝数据的大小,单位是字节

返回值:

  • 返回的是剩余的未拷贝的字节数。

(2)读操作

应用层:

read(fd,  buf  ,10) 

驱动层:

ssize_t xxx_read (struct file *, char __user *, size_t, loff_t *)
{
	copy_to_user
}  
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

二、驱动程序编写

功能:《6818开发板流水灯实现》

1、驱动程序解析

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/device.h>
#include <linux/ioport.h>
#include <linux/io.h>
#include <linux/uaccess.h>
static struct cdev led_cdev;//定义字符设备
static dev_t dev;
struct class * led_class=NULL;
struct device * led_device = NULL;
struct resource *led_res = NULL;

static void __iomem * GPIOCBASE = NULL;
static void __iomem * GPIOCOUT = NULL;
static void __iomem * GPIOCOUTENB = NULL;
static void __iomem * GPIOCALTFN0 = NULL;
static void __iomem * GPIOCALTFN1 = NULL;



ssize_t led_write (struct file * filp, const char __user *buf, size_t size, loff_t *oft)
{
	char kbuf[2]={0};
	int ret;
	if(size !=2 )
	{
		printk(KERN_ALERT"size error\n");
		return -EINVAL;
	}
	//获取用于空间的数据
	ret = copy_from_user(kbuf, buf, size);
	if(ret !=0 )
	{
		printk(KERN_ALERT"copy_from_user error\n");
		return (size-ret);
	}	
	
	//printk(KERN_ALERT"ret = %d,kbuf[0]=%d,kbuf[1]=%d\n",ret,kbuf[0],kbuf[1]);
	
	//根据用户空间的数据,操作不同的LED
	switch(kbuf[0])//哪一个LED
	{
		case 8 :
			if(kbuf[1]==1)
				*(unsigned int *)GPIOCOUT &= ~(1<<17);//D8---GPIOC17
			else if(kbuf[1]==0)
				*(unsigned int *)GPIOCOUT |= (1<<17);
			else
				return -EINVAL;  //代表无效的参数,可以在内核源码中找到EINVAL是一个宏,代表一个错误码
		break;

		case 9 :
			if(kbuf[1]==1)
				*(unsigned int *)GPIOCOUT &= ~(1<<8);//D9---GPIOC8
			else if(kbuf[1]==0)
				*(unsigned int *)GPIOCOUT |= (1<<8);
			else
				return -EINVAL;
		break;

		case 10 :
			if(kbuf[1]==1)
				*(unsigned int *)GPIOCOUT &= ~(1<<7);//D10---GPIOC7
			else if(kbuf[1]==0)
				*(unsigned int *)GPIOCOUT |= (1<<7);
			else
				return -EINVAL;
		break;

		default:
			return -EINVAL;

	}

	return size;

}

//定义文件操作集合结构体
static struct file_operations  fops = 
{
	//.open = led_open,//操作集合中没有提供.open和.release
	//.release = led_close,//使用系统默认的,其它操作函数没有默认
	.write 	= led_write,
};

//入口函数
static int  __init led_init (void)
{
	int ret;
	printk("<1>""led_init\n");
	
	//申请设备号
	ret = alloc_chrdev_region(&dev, 0, 1, "led_chrdev");
	if(ret != 0)
	{
		printk(KERN_ALERT"alloc_chrdev_region fail\n");
		goto alloc_chrdev_region_err;
	}
	
	//初始化
	led_cdev.owner = THIS_MODULE;
	cdev_init(&led_cdev,&fops);

	//注册字符设备
	ret = cdev_add(&led_cdev, dev, 1);
	if(ret != 0)
	{
		printk(KERN_ALERT"cdev_add fail\n");
		goto cdev_add_err;
	}


	//创建class(创建目录)
	led_class = class_create(THIS_MODULE, "led_class");
	if (IS_ERR(led_class)) 
	{
		printk(KERN_ALERT"class_create fail\n");
		ret = -EBUSY;
		goto class_create_err;
	}


	//在class下创建device
	led_device = device_create(led_class, NULL, dev, NULL, "led_node");
	if (IS_ERR(led_device)) 
	{
		printk(KERN_ALERT"class_create fail\n");
		ret = -ENOMEM;
		goto device_create_err;
	}

	
	//申请物理内存(IO内存)
	led_res = request_mem_region(0xc001c000, 0x44,"led_iomem");
	if(led_res == NULL)
	{
		printk(KERN_ALERT"request_mem_region fail\n");
		ret = -EBUSY;
		goto request_mem_region_err;
	}
	
	//io内存的动态映射
	GPIOCBASE = ioremap(0xc001c000,0x44);
	if(GPIOCBASE == NULL)
	{
		printk(KERN_ALERT"ioremap fail\n");
		ret = -EBUSY;
		goto ioremap_err;
	}


	GPIOCOUT = GPIOCBASE;
	GPIOCOUTENB = GPIOCBASE + 0x4;
	GPIOCALTFN0 = GPIOCBASE + 0x20;
	GPIOCALTFN1 = GPIOCBASE + 0x24;

	//printk(KERN_ALERT"GPIOCBASE = %p\n",GPIOCBASE);
	
	//通过映射得到的虚拟地址操作寄存器
	//选择与LED相连引脚为复用功能1---GPIOC7/8/17
	//*(unsigned int *)GPIOCALTFN0 &= ~(0xf<<14);
	writel(  (readl(GPIOCALTFN0) & (~(0xf<<14))),GPIOCALTFN0  );
	
	*(unsigned int *)GPIOCALTFN0 |= (1<<14) |(1<<16);
	
	*(unsigned int *)GPIOCALTFN1 &= ~(0x3<<2);
	*(unsigned int *)GPIOCALTFN1 |= (1<<2) ;

	//设置GPIOC7/8/17为output
	*(unsigned int *)GPIOCOUTENB  |= (1<<7) |(1<<8)|(1<<17);

	//默认都灭
	*(unsigned int *)GPIOCOUT |= (1<<7) |(1<<8)|(1<<17);
	
	return 0;

//各个资源创建申请失败要跳转进行销毁,注意:如果goto跳转到ioremap_err,则会继续执行request_mem_region_err、device_create_err、class_create_err...一直到return。不是只执行一个标签下的语句(这是跳转语句特点)
ioremap_err:
	//释放IO内存
	release_mem_region(0xc001c000,0x44);

request_mem_region_err:
	//销毁device
	device_destroy(led_class, dev);	

device_create_err:
	//销毁前面创建成功的class
	class_destroy(led_class);
	
class_create_err:
	//注销前面注册成的字符设备
	cdev_del(&led_cdev);

cdev_add_err:
	//释放前面申请成功的设备号
	unregister_chrdev_region(dev, 1);

alloc_chrdev_region_err:
	return ret;


}

//出口函数  :卸载驱动模块的时候会执行exit函数,此函数是按照init的反向顺序去销毁和释放资源的。
static void __exit led_exit(void)
{

	printk(KERN_ALERT"led_exit\n");
	//取消映射
	iounmap(GPIOCBASE);
	
	//释放IO内存
	release_mem_region(0xc001c000,0x44);
	
	//销毁device
	device_destroy(led_class, dev);

	//销毁class
	class_destroy(led_class);
	
	//注销字符设备
	cdev_del(&led_cdev);
	
	//释放设备号
	unregister_chrdev_region(dev, 1);
	
}


module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XLG");
MODULE_DESCRIPTION("first chardev drv");
 

3、应用层程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
int main(void)
{
	char buf[2] = {0};//buf[0]存入LED编号,buf[1]存入LED的亮灭状态
	int ret;
	int fd = open("/dev/led_node",O_RDWR);
	if(fd < 0)
	{
		perror("open failed");
		return -1;
	}

	while(1)
	{

		buf[0] = 10;buf[1]=0;
		ret = write(fd,buf,2);
		if(ret < 0)
		{
			perror("write failed");

		}
		buf[0] = 8;buf[1]=1;
		write(fd,buf,2);
		sleep(1);

		buf[0] = 8;buf[1]=0;
		write(fd,buf,2);
		buf[0] = 9;buf[1]=1;
		write(fd,buf,2);
		sleep(1);

		buf[0] = 9;buf[1]=0;
		write(fd,buf,2);
		buf[0] = 10;buf[1]=1;
		write(fd,buf,2);
		sleep(1);

	}

	close(fd);
	
	return 0;

}

  

2、Makefile

obj-m += led_drv.o
CROSS_DIR=/home/gec/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-				
KERN_DIR=/home/gec/6818GEC/kernel			
all:
	make ARCH=arm CROSS_COMPILE=$(CROSS_DIR) -C $(KERN_DIR) M=`pwd` modules
     #编译应用层程序  cp *.ko /opt/nfs arm-linux-gcc test.c -o test cp test /opt/nfs clean: make ARCH=arm CROSS_COMPILE=$(CROSS_DIR) -C $(KERN_DIR) M=`pwd` modules clean rm test -f
原文地址:https://www.cnblogs.com/yuanqiangfei/p/15626736.html