字符设备驱动
驱动分类:字符设备,块设备,网络接口设备。
什么是字符设备:按字节来访问的设备,驱动通常实现open,read,write等系统调用。
字符设备驱动模型
字符设备程序设计
设备号
(1)设备号是什么:字符设备通过字符设备文件来存取。输入 ls -l输出的第一列是‘c’,这就是字符设备文件的标识。还有逗号分隔的两个数,他们分别是字符设备文件的主次设备号。
主设备号:用来标识与设备文件相连的驱动程序,用来反应设备是什么类型。
次设备号:被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
描述:dev_t:实质是unsigned int 32位整数,其中高12位是主设备号,低20位是次设备号。
主设备号:MAJOR(dev_t dev)
次设备号:MINOR(dev_t dev)
(2)分配设备号:静态申请和动态分配。
1. 静态申请:使用函数register_chrdev_region注册设备号,优点是简单,缺点是一旦驱动被广泛使用,这个随机的主设备号可能会导致设备号冲突,导致驱动程序无法注册。
函数原型:int register_chrdev_region(dev_t from, unsigned count, const char* name);
功能:申请使用从from开始的count个设备号(主设备号不变,次设备号增加)
参数解析:
from:希望申请使用的设备号
count:希望申请使用的设备号数目
name:设备名(体现在/proc/device)
2. 动态分配:使用函数alloc_chrdev_region分配设备号,优点是简单,易于驱动推广,缺点是无法在安装驱动前创建设备文件(因为在安装前还没有分配到设备号)
函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。
参数解析:
dev:分配到的设备号
baseminor:起始的次设备号
count:需要分配的设备号数目
name:设备名
(3)注销设备号:不论哪种方法分配设备号,不用的时候都应该释放设备号。
函数原型:void unregister_chrdev_region(dev_t from, unsigned count);
功能:释放从from开始的count个设备号。
创建设备文件:命令mknod手动创建
使用:mkmod filename type major minor
设备注册
(1)描述:struct cdev
(2)注册:3个步骤
1. 分配cdev
struct cdev的分配可使用cdev_alloc函数完成
使用:struct cdev* cdev_alloc(void)
2. 初始化cdev
struct cdev的初始化使用cdev_init函数完成
使用:void cdev_init(struct cdev* cdev, const struct file_operations* fops)
参数解析:
cdev:待初始化的cdev结构
fops:设备对应的操作函数
3. 添加cdev
struct cdev的注册使用cdev_add函数完成
使用:int cdev_add(struct cdev* p, dev_t dev, unsigned count)
参数解析:
p:待添加到内核的字符设备驱动结构
dev:设备号
count:添加的设备个数
3种重要数据结构
struct file, struct inode, struct file_operations
(1)struct file:代表打开的文件。Linux系统中每一个打开的文件在内核空间都有一个关联的struct file。他由内核在打开文件时创建,在文件关闭后释放。
重要成员:loff_t f_pos 文件读写位置
(2)struct file_operations* f_op:一个函数指针的集合,定义可以在设备进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL
(3)struct inode:用来记录文件的物理上的信息。和file结构不同,一个文件可以对应多个file结构,但是只有一个inode结构。
重要成员:dev_t i_rdev 设备号
设备操作
open:设备文件的第一个操作
函数原型:int (open)(struct inode, struct file*)
并不要求驱动程序一定要实现这个操作。如果该项是NULL,设备的打开操作永远成功的。
在大部分驱动程序中,open完成的工作是:初始化设备,标明次设备号。
release:设备文件关闭时调用这个操作
函数原型:void (release)(struct inode, struct file*)
与open类似,release也可以没有。作用与open相反:关闭设备。
read:从设备中读数据到用户空间
函数原型:ssize_t(read)(struct file, char __user*, size_t, loff_t*);
write:向设备发数据,将数据传递给驱动程序
函数原型:ssize_t(write)(struct file, char __user*, size_t, loff_t*);
读写类似:ssize_t xxx_read(struct file* filp, char __user* buff, size_t count, loff_t* offp);
ssize_t xxx_write(struct file* filp, char __user* buff, size_t count, loff_t* offp);
参数解析:
1. filp:文件指针
2. count:请求传输的数据量
3. buff:用户空间指针,指向数据缓存。不能被内核代码直接调用,因为用户空间指针在内核空间可能根本是无效的,没有地址映射。内核提供了专门的函数用于访问用户空间指针:
int copy_from_user(void* to, const void __user* from, int n);
int copy_to_user(void __user* to, const void* from, int n);
4. offp:文件当前的访问位置
poll:对应select系统调用
函数原型:unsigned int(poll)(struct file, struct poll_table_struct*)
ioctl:控制设备
函数原型:int (ioctl)(struct inode, struct file*, unsigned int, unsigned long)
mmap:将设备映射到进程虚拟地址空间中
函数原型:int (mmap)(struct file, struct vm_area_struct*)
llseek:修改文件当前位置,并将新位置作为返回值
函数原型:off_t (llseek)(struct file, loff_t, int)
cdev_del:字符设备注销
函数原型:int cdev_del(struct cdev* p)
参数解析:p是要注销的字符设备
范例设计:简单字符驱动程序
开发一个基本的字符设备:建立一个名为GlobalChar的虚拟设备,设备内部只有一个全局变量供用户操作。
设备提供功能:
1. 读函数读取设备内部全局变量的值返回给用户
2. 写函数把用户设定的值写入全局变量
步骤如下:
编写代码:GlobalCharDev.c
#include<linux/module.h>
#include<linux/init.h>
#include<linux/kernel.h>
#include<cdev.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include<asm/uaccess.h>
#include<linux/device.h>
#define DEV_NAME "GlobalChar"
static ssize_t GlobalRead(struct file *, char *, size_t, loff_t *);
static ssize_t GlobalWrite(struct file *, const char *, size_t, loff_t *);
static int char_major = 0;
static int GlobalData = 0;
static const struct file_operations globalchar_fops =
{
.read = GlobalRead,
.write = GlobalWrite
};
static int __init GlobalChar_init(void)
{
int ret;
ret = register_chrdev(char_major, DEV_NAME, &globalchar_fops);
if(ret < 0)
{
printk(KERN_ALERT "GlobalChar register failed!\n");
}
else
{
printk(KERN_ALERT "GlobalChar register sucess!\n");
char_major = ret;
printk(KERN_ALERT "major = %d\n", char_major);
}
return ret;
}
static void __exit GlobalChar_exit(void)
{
unregister_chrdev(char_major, DEV_NAME);
return;
}
static ssize_t GlobalRead(struct file *filp, char *buf, size_t len, loff_t *off)
{
if(copy_to_user(buf, &GlobalData, sizeof(int)))
{
return -EFAULT;
}
return sizeof(int);
}
static ssize_t GlobalWrite(struct file *filp, const char *buf, size_t len, loff_t *off)
{
if(copy_from_user(&GlobalData, buf, sizeof(int)))
{
return -EFAULT;
}
return sizeof(int);
}
module_init(GlobalChar _init);
module_exit(GlobalChar _exit);
编写Makefile
obj-m := GlobalCharDev.o
KDIR := /lib/modules/$(shell uname -r)/bulid
SRCPWD := $(shell pwd)
all:
make -C $(KDIR) M=$(SRCPWD) modules
编译并加载内核模块
查看内核分配的主设备号:dmesg | tail -n 10和cat /proc/devices | grep GlobalChar
使用mknod命令创建一个设备文件:sudo mknod -m 666 /dev/GlobalChar c 249 0
到此为止,已经成功添加了一个字符设备到内核,下面是测试驱动程序能否正常工作。
测试代码:GlobalCharTest.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define DEV_NAME "/dev/GlobalChar"
int main()
{
int fd, num;
fd = open(DEV_NAME, O_RDWR, S_IRUSR | S_IWUSR);
if(fd < 0)
{
printf("open device fail!\n");
return -1;
}
read(fd, &num, sizeof(int));
printf("the GlobalChar is %d\n", num);
printf("please input a number written to GlobalChar: ");
scanf("%d", &num);
write(fd, &num, sizeof(int));
read(fd, &num, sizeof(int));
printf("the GlobalChar is %d\n", num);
close(fd);
return 0;
}