(本节笔记的实验代码,在这里)
一. 使用字符设备驱动程序
1. 编译和安装驱动程序模块
1.1 将驱动模块源码memdev.c复制到Linux系统中,编写Makefile,编译成内核模块memdev.ko并拷贝到开发板的NFS的rootfs中。
2.2 把memdev.ko加载(insmod)到开发板的Linux系统内核中。
2. 创建设备文件
通过字符设备文件,应用程序可以使用相应的字符设备驱动程序来控制字符设备。创建字符设备文件的方法有两种:1)使用mknod命令,如:【mknod /dev/文件名 c 主设备号 次设备号】;2)使用函数在驱动程序中创建。
//可cat/proc/devices 查看已挂载的设备驱动的主设备号
//而次设备号一般取非负切小于255的整数(0~254)
//memdev.ko模块的功能是模拟一个硬件,通过往设备写入数据,模拟向硬件设备中写入数据
创建设备文件:mknod/dev/memdev0 c 253 0
3. 应用程序通过设备文件访问设备
touch write_mem.c
『
#include <stdio.h>
#include <linux/types.h>
#include <linux/stat.h>
#include <fcntl.h>
int main()
{
int src = 2015;
int fd = 0;
fd = open("/dev/memdev0",O_RDWR);
write(fd, &src, sizeof(int));
close(fd);
return 0;
}
』
对write_mem.c进行交叉编译:arm-linux-gccwrite_men.c -o write_men
把write_men拷贝到rootfs中,在开发板上运行write_men,若出现以下提示:~/bin/sh:./write_mem:notfound,证明开发板Linux系统中并不包含write_mem所需要的库(可通过arm-linux-readelf -d write_mem来检查该应用程序所需要的动态链接库),因此需要加入-static选项进行静态编译。
touch read_mem.c
『
#include <stdio.h>
#include <linux/types.h>
#include <linux/stat.h>
#include <fcntl.h>
int main()
{
int dst = 0;
int fd = 0;
fd = open("/dev/memdev0",0_RDWR);
read(fd, &dst, sizeof(int));
printf("dst is %d\n",dst);
close(fd);
return 0;
}
』
//通过read_mem把写入到设备中的数据读出来。
二. 字符设备驱动模型
在Linux系统中,设备的类型繁多,如:字符设备、块设备、网络接口设备、USB设备、PCI设备、平台设备和混杂设备等,设备类型的不同意味着对应的驱动程序模型的不同。
0. Linux设备驱动模型
0.1 驱动程序初始化
1)分配设备描述符
2)初始化设备描述结构
3)注册设备描述结构
4)硬件初始化
0.2 实现设备操作
0.3 驱动注销
1. 驱动程序初始化
1.0 基本概念
1.0.1 设备描述结构cdev //#include <linux/cdev.h>
任何一种驱动模型中,设备都会用内核中的一种结构来描述,而字符设备在内核中使用struct cdev来进行描述,具体内容如下:
struct cdev{
struct kobject kobj;
struct module *cwner;
※ const struct file_operations *ops; //设备操作集
structlist_head list;
※ dev_t dev; //设备号
※ unsigned int count; //设备数
}
1.0.2 设备号
主设备号——在/dev目录下的设备文件是通过主设备号,与内核设备驱动程序建立联系。
次设备号——同一个设备驱动要支持多个同样的物理设备,就需要次设备号来进行区分。
设备号的操作——Linux内核中使用dev_t类型来定义设备号,dev_t实质是32位unsigned int型数据,其中高12位为主设备号,低20位为次设备号。dev_t和主、次设备号间的转换函数为:
dev_t dev = MKDEV(主设备号,次设备号)
主设备号(int) = MAJOR(dev_t dev)
次设备号(int) = MINOR(dev_t dev)
设备号的分配——分为静态申请方式和动态申请方式两种:
静态申请:用户选择一个数字作为主设备号,然后通过函数register_chrdev_region向内核申请使用,缺点是,如果申请使用的设备号已被内核中其他驱动占用,则申请失败。
动态分配:使用alloc_chrdev_region函数,由内核分配一个可用的主设备号,其优点是内核知道哪些号可被调用,因此不会导致申请的设备号被占用的情况发生。※(一般采用这种方式进行主设备号的申请)函数原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:自动分配好的设备号所存放的dev_t类型数据地址。
baseminor:指定次设备号起始号码。
count:指定次设备号的个数。
name:出现在/proc/devivers中的设备名。
设备号的注销——驱动退出时,应使用unregister_chrdev_region函数释放设备号,避免系统资源的浪费。函数原型如下:
void unregister_chrdev_region(dev_t from, unsigned count);
from:分配的dev_t类型的起始设备号;
count:需要注销设备的数量,一般为次设备号的数量。
1.1 设备描述结构cdev的分配方式
静态分配方式——struct cdev mdev;
动态分配方式——struct cdev *pdev = cdev_alloc();
1.2 初始化cdev
struct cdev的初始化使用cdev_init函数来完成。原型如下:
cdev_init(struct cdev *cdev, const struct file_operations *fops)
//关于struct file_operations操作函数集,参看下一小节。
cdev:待初始化的cdev结构,先分配,后传进该函数。
fops:设备操作函数集,先定义struct file_operations,把指针传进来。
1.3 注册cdev
struct cdev的注册使用cdev_add函数来完成。
cdev_add(struct cdev *p, dev_t dev, unsigned count);
p:待添加到内核的字符设备结构,也就是上面已初始化的cdev指针。
dev:该设备驱动的设备号,先分配,后传进来。
count:使用该设备驱动程序的设备个数。
1.4 硬件初始化
对硬件寄存器进行初始化。
2. 实现设备操作
2.0 基本概念
2.0.1 文件操作集struct file_operations // #include <linux/fs.h>
函数原型为:
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 *, 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);
};
struct file_operations是一个函数指针的集合,定义了能在设备上进行的操作。结构中的函数指针指向驱动程序中的函数,这些函数实现了一个针对设备的操作,对于不支持的操作则设置函数指针为NULL。例如:
struct file_operations dev_fops = {
.llseek = NULL,
.read = dev_read,
.write = dev_write,
.ioctl = dev_ioctl,
.open = dev_open,
.release = dev_release, };
2.0.2 文件描述结构struct file
在Linux系统中,每一个打开的文件,在内核都会关联一个文件描述结构struct file,它由内核在打开文件时创建,在文件关闭后释放。
重要成员有:
loff_t f_pos /* 文件读写指针 */
struct file_operations *f_op /* 该文件所对应的操作 */
2.0.3 文件信息结构struct inode
每一个存在于文件系统里面的文件都会关联一个inode结构,该结构主要用来记录文件物理上的信息。因此,它和代表打开文件的struct file结构是不同的。一个文件没有被打开时不会关联struct file结构,但却会关联一个struct inode结构。
仲要成员有:、
dev_t i_rdev /* 设备号 */
2.1 open
int (*release)(struct inode *, struct file *);
open设备方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open方法完成如下工作有:表明次设备号,以及启动设备。
具体操作步骤是:
1)获取次设备号:
int num = MINOR(inode->i_rdev);
2)把寄存器地址安装不同的次设备号传给struct file里面的private_data成员进行保存。
if(num==0)
filp->private_data = dev1_registers;
else if(num ==1)
filp->private_data = dev2_registers;
else
return -ENODEV; //无效的次设备号
2.2 close
int (*release) (struct inode *, struct file *);
release方法的作用正好与open相反,这个设备方法又是被称为close,主要工作为关闭设备。
2.3 read
read设备方法通常完成2件事情:1.从设备中读取数据(属于硬件访问累操作);2.将读到的数据返回给应用程序
函数原型为:ssize_t (*read)(struct file *filp, char _user *buff, size_t count, loff_t *offp);
filp:与字符设备文件关联的struct file结构指针,由内核创建。
buff:从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。而该参数来源于用户空间的指针,这类指针无法被内核代码直接应用,因此不许用以下参数进行数据传递://#include <asm/uaccess.h>
int copy_from_user(void *to, const void _user *from, int n);
int copy_to_user(void _user *to, const void *from, int n);
count:请求传输的数据量,由read系统调用提供该参数。
offp:文件的读写位置,由内核从struct file结构中取出后,传递进来。
实现步骤:
1)获取设备寄存器基地址
int *register_addr = filp->private_data;
2)判断读取位置是否有效
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
3)读取数据到用户空间
if (copy_to_user(buf, register_addr + p, count))
{
ret = -EFAULT;
}
else
{
*ppos += count; //成功读取后,将影响文件读写指针;
ret = count;
}
2.4 write
write设备方法通常完成2件事:1.从应用程序提供的地址中取出数据;2.将数据写入设备(属于硬件访问类操作)。
函数原型:ssize_t (*write)(struct file *filp, char _user *buff, size_t count, loff_t *offp);
filp:与字符设备文件关联的struct file结构指针,由内核创建。
buff:从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。而该参数来源于用户空间的指针,这类指针无法被内核代码直接应用,因此不许用以下参数进行数据传递:
int copy_from_user(void *to, const void _user *from, int n);
int copy_to_user(void _user *to, const void *from, int n);
count:请求传输的数据量,由read系统调用提供该参数。
offp:文件的读写位置,由内核从struct file结构中取出后,传递进来。
2.5 llseek
ssize_t (*read)(struct file *, char _user *, size_t, loff_t *);
重定位文件读写指针,响应lseek系统调用。
3.驱动注销
当需要从内核中卸载驱动程序时,需要使用cdev_del函数来完成字符设备的注销。
具体步骤:
1)注销设备
cdev_del(&cdev);
2)释放设备号
unregister_chrdev_region(devno,2);
三. 字符型设备驱动的设计步骤
Linux设备驱动模型(步骤)
1. 驱动程序初始化
1)分配设备描述符——静态分配
2)初始化设备描述结构——cdev_init
3)注册设备描述结构——cdev_add
4)硬件初始化
2. 实现设备操作
2.1打开设备——open
2.2读设备数据read——copy_to_user
2.3写数据到设备write——copy_from_user
2.4读写位置重定位——lseek
2.5关闭设备——close
3. 驱动注销
3.1 注销设备——cdev_del
3.2 释放设备号——unregister_chrdev_region
touch memdev.c
『
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
int dev1_registers[5];
int dev2_registers[5];
struct cdev cdev;
dev_t devno;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num==0)
filp->private_data = dev1_registers;
else if(num == 1)
filp->private_data = dev2_registers;
else
return -ENODEV; //无效的次设备号
return 0;
}
/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/
/*判断读位置是否有效*/
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
/*读数据到用户空间*/
if (copy_to_user(buf, register_addr+p, count))
{
ret = -EFAULT;
}
else
{
*ppos += count;
ret = count;
}
return ret;
}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
int *register_addr = filp->private_data; /*获取设备的寄存器地址*/
/*分析和获取有效的写长度*/
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
/*从用户空间写入数据*/
if (copy_from_user(register_addr + p, buf, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
}
return ret;
}
/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case SEEK_SET:
newpos = offset;
break;
case SEEK_CUR:
newpos = filp->f_pos + offset;
break;
case SEEK_END:
newpos = 5*sizeof(int)-1 + offset;
break;
default:
return -EINVAL;
}
if ((newpos<0) || (newpos>5*sizeof(int)))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
/*设备驱动模块加载函数*/
static int memdev_init(void)
{
/*初始化cdev结构*/
cdev_init(&cdev, &mem_fops);
/* 注册字符设备 */
alloc_chrdev_region(&devno, 0, 2, "memdev");
cdev_add(&cdev, devno, 2);
}
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
unregister_chrdev_region(devno, 2); /*释放设备号*/
}
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit); 』
四. 字符驱动的访问原理
应用程序调用read系统调用,通过内核,把read系统调用转化为xx_read。
1. 对之前的read_mem应用程序进行反编译(arm-linux-objdump -D -S read_mem >dump)
把read系统调用的编号(0X3)传进R7,调用SVC(系统调用指令)从用户态切换到内核态,进入位于/arch/arm/kernel/entry-common.S中的入口函数
再从R7中取出编号,通过编号搜寻system call number
通过搜索,得知表位于call.S中
根据R7中的0X3,为sys_read
而sys_read的实现代码在位于/fs/Read_write.c中的
SYSCALL_DEFINE3(read,unsignedint,fd,char __user *,buf,size_t,count)函数,该函数是一个宏,拆开后转化为一般看到的形式。
而该函数中包含以下代码:
通过搜索其实现函数为:
通过file->f_op->read(file,buf,count,pos)就能调用对模块中所对应的read的方法调用了。