字符设备驱动

字符设备驱动

字符设备驱动_第1张图片

文章目录

  • 字符设备驱动
  • Linux字符设备驱动结构
    • cdev结构体
    • 分配和释放设备号
    • file_operations结构体
    • Linux字符设备驱动的组成
      • 字符设备驱动模块加载与卸载函数
      • 字符设备驱动的file_operations结构体中的成员函数
  • globalmem虚拟设备实例描述
  • globalmem设备驱动

Linux字符设备驱动结构

cdev结构体

在Linux内核中,使用一个cdev结构体描述一个字符设备

cdev结构体的定义:

struct cdev {
 struct kobject kobj;
 struct module *owner;
 const struct file_operations *ops;
 struct list_head list;
 dev_t dev;
 unsigned int count;
};

其中dev_t成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号;file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数

使用以下宏可以从dev_t 获得主设备号和次设备号

MAJOR(dev_t dev)

MINOR(dev_t dev)

使用以下宏可以通过主设备号和次设备号生成dev_t

MKDEV(int major, int minor)

Linux内核提供了一组函数以用于操作cdev结构体

/*用于初始化cdev成员,并建立cdev和file_operations之间的连接*/
void cdev_init(struct cdev *dev, struct file_operations *fops);
{
	memset(cdev, 0, sizeof(cdev));
	INIT_LIST_HEAD(&head->list); //初始化双向链表的宏,并且会将双向链表头部的指针指向自己
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops //建立连接
}

/*cdev_alloc函数用于动态的申请一个cdev内存*/
struct cdev *cdev_alloc(void);
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if(p){
	INIT_LIST_HEAD(&p->list);
	kobject_init(&p->kobj, &ktype_cdev_dynamic);
	return p;
}
}
void cdev_put(struct cdev *p);

/*分别向系统添加和删除一个cdev,完成字符设备的注册和注销,分别出现在模块加载函数和模块卸载函数*/
int cdev_add(struct cdev *, dev_t, unsigned); 
void cdev_del(struct cdev *);

分配和释放设备号

在使用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region() 或者是 alloc_chrdev_region() 向系统申请设备号。

其中register表示已知起始设备的设备号的情况,而alloc表示设备号未知的情况向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把设备号放入第一个参数dev中。

相反,在使用 cdev_del() 函数时,应该先将设备号主动释放使用 unregister_chrdev_region() 应该被调用以释放原先申请的设备号。

file_operations结构体

file_operation结构体中的成员函数是字符设备驱动设计的主体内容,这次函数会在应用程序对文件操作open()/close()/write()/read()时最终被调用,结构体被存放在 include/linux/fs.h

Linux字符设备驱动的组成

字符设备驱动模块加载与卸载函数

在字符设备驱动模块加载函数中应该实现对设备号的申请和cdev的注册,在模块卸载函数中应该实现对设备号的释放和对cdev的注销。

#define test DEV_NAME
#define 1 MAJOR 

/*cdev 设备结构体
structe test_dev {
	struct cdev cdev;
	...
} test_dev;
*/

//设备模块加载函数
static int __init dev_init(void)
{
	//初始化cdev
	cdev_init(&test_dev.cdev, test_fops);
	test_dev.cdev.owner = THIS_MODULE;
	//注册设备号
	//获取字符设备号
	if(test_major){
	register_chrdev_region(test_dev_no, 1, DEV_NAME);
}
	else{
	alloc_chrdev_region(&test_dev_no, 0, 1, DEV_NAME);
}
	//注册设备
	cdev_add(&test_dev.cdev, test_dev_no, 1);
}

/*模块卸载函数*/
static void __exit test_exit(void)
{
	//释放设备号
	unregister_chrdev_region(test_dev_no, 1);
	//注销设备
	cdev_del(&test_dev_cdev);
}

字符设备驱动的file_operations结构体中的成员函数

字符设备驱动读、写、I/O控制函数模板:

  • write和read

驱动程序的write()方法包括从用户空间读取到内核空间,然后在内核中处理这些数据使用

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

read()方法则相反,从内核空间读取到用户空间使用

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)

其中带__user的参数的都表示用户空间内存

虽然内核空间可以访问用户空间的缓存区,但是在访问前需要检查其的合法性通过 access_ok(type, add, size) 进行判断,以确定传入的缓存区属于用户空间。

如果要复制单个简单变量如 char、int这样,内核会提供专用的宏:

x表示复制到的值,ptr表示目标空间的地址,成功时返回0,失败时返回-EFAULT

put_user(x, ptr)

get_user(x, ptr)

还有__put_user()这种与上面不一样的是不会进行access_ok()的检查

file_operations的write函数:

ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos);

返回值是写入的字节数(长度)

*buf是来自用户的数据缓存区

count是请求传输的数据长度

*pos表示数据在文件中应该写入的起始位置

file_operations的read函数:

#define 文件内存大小 filesize

ssize_t eeprom_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
	//检查来自用户空间的错误请求
	//只有在设备提供内存(电可擦编程只读存储器、I/O内存等)时才有意义
	if(*pos >= filesize)
	return -EINVAL;

	//针对剩余字节数调整count,以便不超过文件大小
	if(*pos + count > filesize)
	count = filesize - *pos;

	//找到能够开始写入的位置
	void *from = pos_to_address(*pos)

	//从用户空间拷贝内容到内核
	if(copy_from_user(dev->buff, buff, count) != 0){
	retval = -EFAULT;
	goto out;
}

	//再从内核空间写入到设备中去
	write_error = write_device(dev->buff, count);
	if(write_error){
	return -EFAULT;
}

	//根据写入的字节数设置文件光标的位置,返回复制的字节数
	*pos += count;
	return count;

}

ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *pos);

返回值是读取的数据量

count请求传输数据的大小

#define 文件内存大小 filesize
ssize_t eeprom_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	//防止读操作超出文件大小,并返回文件结束
	if(*pos >= filesize){
	return 0; //0 means EOF
}
	
	//读取的字节数不能超过文件大小
	if(*pos + count > filesize){
	count = filesize - *pos;
}

	//找到读取的起始位置
	void *from = pos_to_address(*pos);

	//读取数据
	if(copy_to_user(buff, from, count)){
	return -EFAULT;
}
	
	//移动光标的位置
	*pos += count;
	return count;
}
  • open

open是每次打开设备文件时都会调用,通常在open中执行设备和数据结构的初始化

第一个参数struct inode结构内的i_cdev字段通常指向在__init函数中分配出来的cdev

int (*open)(struct inode *inode, struct file *filp);

struct pcf2127 {
struct cdev cdev;
unsigned char *sram_data;
struct i2c_client *client;
int sram_size;
...
};

static unsigned int sram_major = 0;
static struct class *sram_class = NULL;

static int sram_open(struct inode *inode, struct file *filp)
{
	//获取主次设备号
	unsigned int maj = imajor(inode);
	unsigned int min = iminor(inode);

	if(maj != sram_major || min <0){
	pr_err("device not found\n"); //这个宏会将指定的错误信息输出到系统日志中,以便开发人员进行故障排除。
	return -ENODEV;
}
	//获取特定成员的结构体的指针
	struct pcf2127 *pcf = NULL;
	pcf = container_of(inode->i_cdev, struct pcf2127, cdev); //container_of用的好
	pcf->sram_size = SRAM_SIZE;

	//如果是第一次打开,准备缓冲
	if(pcf->sram_data == NULL){
	pcf->data = kzalloc(pcf->sram_size, GFP_KERNEL);
	if(pcf->sram_data == NULL){
	pr_err("memory allocation failed\n");
	return -ENOMEM;
}
}
	~~~~filp->private_data = pcf;//文件私有数据
	return 0;
}

文件私有数据

  • release方法

static int sram_release(struct inode *inode, struct file *filp);

与open相反,release需要在设备关闭的时候调用,必须撤销在open任务中已经执行的所有操作。

释放在open阶段所有的私有内存

关闭设备

static int sram_release(struct inode *inode, struct file *filp)
{
	struct pcf2127 *pcf = NULL;
	pcf = container_of(inode->i_cdev, struct pcf2127, cdev);
	
	mutex_lock(&device_list_lock);
	filp->private_data = NULL;
	//关闭设备
	pcf->users--;
	if(!pcf->users--){
	kfree(tx_buffer);
	kfree(rx_buffer);
	tx_buffer = NULL;
	rx_buffer = NULL;

	if(any_global_struct)
	kfree(any_global_struct);
}
	mutex_unlock(&device_list_lock);
	return 0;
}
  • llseek

在文件中移动光标位置时会调用llseek函数

loff_t(*llseek) (structfile *filp, loff_t offset, int whence);

返回值是文件中的新位置

offset是相对与当前位置的文件的偏移量,也就是位置将改变多少

whence定义从哪里开始找{SEEK_SET SEEK_CUR SEEK_END}

switch(whence){
	case SEEK_SET:
		newpos += offset;
		break;
	case SEEK_CUR:
		newpos = file->f_pos + offset;
		break;
	case SEEK_END:
		newpos = filesize + offset;
		break;
	default:
		return -EINVAL;
}

//检查newpos是否有效
if(newpos < 0){
	return -EINVAL;
}

//将文件光标重置
file->f_pos = newpos;
//返回新文件指针的位置
return newpos;
  • ioctl

long ioctl(struct file *f, unsigned int cmd, unsigned long arg);

有时候设备需要进行未被定义的系统调用的命令,特别是文件和设备文件相关的命令,用ioctl可以向设备发送特殊命令(重置,关机,配置等)如果驱动程序没有定义这个方法,则内核向ioctl系统调用返回-ENOTTY错误。

Linux提供了四个帮助宏来创建ioctl标识符,选用取决于是否有数据传输和传输的方向:

_IO (MAGIC, SEQ_NO):ioctl不需要传输数据

_IOW (MAGIC, SEQ_NO, type):ioctl需要写入参数

_IOR (MAGIC, SEQ_NO, type):ioctl需要读取参数

_IOWR (MAGIC, SEQ_NO, type):ioctl需要写入和读取参数

参数的含义(8为魔数[0~255],8为序列号或命令ID,数据类型)

/*生成ioctl命令*/
//定义一个eep_ioctl.h文件,在用户和内核空间中都要调用
"#include "
#ifndef PACKT_IOCTL_H
#define PACKT_IOCTL_H

//需要为驱动选择一个数字,以及每个驱动选择一个序列号
#define EEP_MAGIC 'E'
//擦除
#define ERASE_SEQ_NO 0x01 
//重命名
#define RENAME_SEQ_NO 0x02
//清除字节
#define CLEAN_BYTE_SEQ_NO 0x03
//获取大小
#define GET_SIZE_SEQ_NO 0x04

//定义最大分区大小
#define MAX_PART 32

//定义ioctl命令
#define EEP_ERASE          _IO(EEP_MAGIC, ERASE_SEQ_NO)
#define EEP_RENAME_PART    _IOW(EEP_MAGIC, RENAME_SEQ_NO, unsigned long)
#define EEP_GET_SIZE       _IOR(EEP_MAGIC, GET_SIZE_SEQ_NO, int *)

#endif
//内核空间代码
#include "epp_ioctl.h"

static long ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    int part;
    char *buf = NULL;
    int size = 1300;
    switch (cmd){
    case EEP_ERASE:
        erase_eeprom();
        break;
    case EEP_RENAME_PART:
        buf = kzalloc(MAX_PART, GFP_KERNEL);
        copy_from_user(buf, (char *)arg, MAX_PART);
        rename_part(buf);
        break;
    case EEP_GET_SIZE:
        copy_to_user((int *)arg, &size, sizeof(int));//最后的长度为指针的大小
        break;
    default:
        break;
    }
    return 0;
}
//用户空间程序代码 my_main()
#include "eep_ioctl.h"

int main()
{
    int size = 0;
    int fd;
    char *newname = "new_part";//小于MAX_PART
    fd = open("/dev/eep_mem1", O_RDWR);
    if(fd<0){
        printf("Eorror while opening the eeprom\n");
        return -1;
    }

    ioctl(fd, EEP_ERASE);
    ioctl(fd, EEP_RENAME_PART, newname);
    ioctl(fd, EEP_GET_SIZE, &size);

    close(fd);
    return 0;
}

字符设备驱动_第2张图片

globalmem虚拟设备实例描述

globalmem为一块虚拟的存储空间,大小为4KB内存空间,并在驱动中提供针对该片内存的读写,控制,和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容

globalmem设备驱动

/*
 * a simple char device driver : globalmem without mutex
 * Licensed under GPLv2 or later
 * 按照上面的模块写的,头文件我就不一一列举出来了
 */

#define MEM_SIZE 0x1000 //4KB
//打开设备文件
//释放设备文件
//控制设备
//读取设备
//写入设备
//设备文件定位
//回调file_operations
//设置设备
//加载模块
//卸载模块
//模块信息

假如我要加载 N 个globalmem的驱动代码

首先结构框架:
#define MAX_MEM 0x1000
#define GLOBAL_MAJOR 230
#define MEM_CLEAN 0x1
#define GLOBAL_NUM N

static int globalmem_major = GLOBAL_MAJOR
module_param(globalmem_major, int, S_IRUGO);

//设备文件
static struct globalmem{
    unsigned char mem[MAX_MEM];
    struct cdev cdev;
};

struct globalmem *dev_p;

//设置设备
static void globalmem_setup_dev(struct globalmem *dev, int index) //其中dev指代的就是我的设备
{
    int error;
    dev_t devno = MKDEV(globalmem_major, index);
    //初始化设备
    cdev_init(&dev->cdev, &globalmem_fops);
    //添加设备
    error = cdev_add(&dev->cdev, devno, 1);
    if(error){
        printk("add device failed\n");
        return error;
    }
}

//打开文件
static int globalmem_open(struct inode *inode, struct file *filp)
{
    //可以使用 container_of 先通过结构体某一成员的访问找到这个结构体的首地址,
		//inode中包含一个i_cdev
    //然后就可以访问其它成员变量了
		struct globalmem *dev = container_of(inode->i_cdev, struct globalmem, cdev)
		//第一个参数为结构体指针,第二个参数为结构体类型,第三个参数是要访问的结构体成员
    filp->private_data = dev_p;
    return 0;
}

//释放文件
static void globalmem_release(struct inode *inode, struct file *filp)
{
    return 0;
}

//读取文件
static int globalmem_read(struct file *filp, char __buf *buf, size_t size, loff_t *pos)
{
    int ret;
    unsigned int count = size;
    unsigned long p = *pos;
    struct globalmem *dev = file->private_data;
    //检查是否光标已经到了末尾
    if(p >= MAX_MEM){
        printk("the location of pos is exceed\n");
        return -EINVAL;
    }
    //检查读取的数量是否不够文件大小
    if(p + count > MAX_MEM){
        count = MAX_MEM - p;
    }
    //读取
    ret = copy_to_user(buf, dev->mem+p, count);
    if(ret < 0){
        printk("read failed\n");
        return -EFAULT;
    }
    else{
        *pos += count;//光标开始移动
        ret = count;
        printk("has been read %u bytes dara\n", count);
    }
    return ret;
}

//写入文件
static int globalmem_write(struct file *filp, char __buf *buf, size_t size, loff_t *pos)
{
    int ret;
    unsigned int count = size;
    unsigned long p = *pos;
    struct globalmem *dev = filp->private_data;
    //检查是否已经写到文件末尾
    if(p >= MAX_MEM){
        return 0;
    }
    //检查一次写入的量是否超出了文件大小
    if(p + count > MAX_MEM){
        count = MAX_MEM - p;
    }
    //写入数据
    ret = copy_from_user(dev->mem + p, buf, count);
    if(ret < 0){
        printk("write failed\n");
        return -EFAULT;
    }
    else{
        *pos += count;
        ret = count;
        printk("has been writen %u bytes data\n", count);
    }
    return ret;
}

//文件的控制
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct globalmem *dev = filp->private_data;
    switch(cmd){
        case MEM_CLEAN:
        memset(dev->mem, 0 , MAX_MEM);
        printk("KERN INFO:the globalmem is set to zero\n");
        break;

        default:
        return -EINVAL;
    }
}

//文件偏移
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int whence)
{
    loff_t ret;
    switch(whence){
        case 0://f_pos在文件开头
        //如果文件指针偏移大于文件大小
        if((unsigned int)offset > MAX_MEM){
            return -EINVAL;
            break;
        }
        //如果文件指针偏移为负数
        if(offset < 0){
            return -EINVAL;
            break;
        }
        filp->f_pos += (unsigned int)offset;
        ret = file->f_pos;
        return ret;
        break;

        case 1://文件指针f_pos不在文件开头
        //如果文件指针移动大于文件大小
        if(file->f_pos + (unsigned int)offset > MAX_MEM){
            return -EINVAL;
            break;
        }
        //如果文件指针移动小于0
        if(file->f_pos + (unsigned int)offset < 0){
            return -EINVAL;
            break;
        }
        file->f_pos += (unsigned int)offset;
        ret = file->f_pos;
        return ret;
        break;
    }
}

//文件操作函数重载,用在初始化设备当中
const struct file_operations globalmem_fops{
    .open    = globalmem_open,
    .release = globalmem_release,
    .write   = globalmem_write,
    .read    = globalmem_read,
    .ioctl   = globalmem_ioctl,
    .llseek  = globalmem_llseek,
		.owner   = THIS_MODULE,
};

//添加模块
static int __init globalmem_init(void)
{
    int ret;
    //获取设备号
    dev_t devno = MKDEV(globalmem_major, 0);
    //注册设备
    if(globalmem_major){
    ret = register_chrdev_region(devno, GLOBAL_NUM, "globalmem");
    if(ret){
        printk("register failed\n");
        return ret;
    }
    }
    else{
        alloc_chrdev_region(devno, 0, GLOBAL_NUM, "globalmem");
				globalmem_major = MAJOR(devno);
    }
    //设置设备
    dev_p = kzalloc(sizeof(struct globalmem) * GLOBAL_NUM, GFP_KERNEL);
    if(!dev_p){
        return -ENOMEM;
        goto fail_malloc;
    }
		for(int i = 0, i<GLOBAL_NUM, i++)
    globalmem_setup_dev(dev_p+i, i);
    return 0;

    fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}

//卸载模块
static void __exit globalmem_exit(void)
{
		int i;
		for(i=0, i<GLOBAL_NUM, i++){
    //删除设备
    cdev_del(&(dev_p+i)->cdev);
    //释放空间
    kfree(dev_p)
    dev_p = NULL;
		}
    //注销设备
    unregister_chrdev_region(MKDEV(globalmem_major, 0), GLOBAL_NUM);
}

//模块信息
module_init(globalmem_init);
module_exit(globalmem_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("GuduMemories")

你可能感兴趣的:(Linux设备驱动开发,linux,Linux驱动开发,驱动开发,字符设备)