字符设备驱动

字符设备驱动

驱动分类:字符设备,块设备,网络接口设备。
什么是字符设备:按字节来访问的设备,驱动通常实现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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值