一、综述
本章开始,我们就要学会写一个最基本的字符设备驱动了,麻雀虽小五脏俱全。这是本章的重点,首先要弄明白一些基本的数据结构等相关知识,在去尝试编写驱动。
知识点
1.主设备号和次设备号
a.
一个主设备 对应 一个驱动程序
b.
次设备号 确定 具体的设备
c.
dev_t(在linux/types.h中)保存设备编号,
前12位 - 主设备号
后20位 - 次设备号
MAJOR(dev_t dev):获取主设备号
MANOR(dev_t dev) : 获得次设备号
MKDEV(int major,int minor) : 主设备号和次设备号转换成dev_t
d.
分配和释放设备编号
静态分配: int register_chrdev_region(dev_t first,unsigned int count ,char *name);
first:起始编号,通常为0
count: 连续请求的设备编号个数
name:设备名称,将会出现在 /proc/devices和sysfs中
成功:返回0
失败:返回负数
动态分配
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name)
dev:仅用于输出的参数,在成功完成调用后将班车已分配范围的第一个编号
firstminor:第一个次设备号,通常为0
count和那么跟上面的一样
e.释放函数
释放函数:void unregister_chrdev_region(dev_t first,unsigned int count)
2.字符设备的注册
方式一:注册独立的cdev结构
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_ops;
方式2:将cdev内嵌到自己的设备结构中
调用以下函数来初始化
void cdev_init(strcuct cdev *cdev,struct file_operations *fops);
指定cdev的所有者和操作函数
cdev.ower = THIS_MODULE;
cdev.ops = &my_ops;
把字符设备添加到内核
int cdev_add(struct cdev *dev,dev_t num,unsigned int count);
在适当的地方删除
void cdev_del(struct cdev *dev);
3.三个重要的数据结构
struct file_operations:保存操作字符驱动程序的方法
strcut file:表示打开一个文件
strucr inode:表示一个磁盘上的文件
大多数设备驱动程序都会用到这三个数据结构
其他知识点
#include <linux/kernel.h>
container_of(pointer,type,field)
一个方便使用的宏,可用于从包含在某个数据结构中的指针获得结构体本身的指针。
#include <linux/uaccess.h>
该头文件声明了内核代码和用户空间之间移动数据的函数
unsigned long copy(void *to,const void *from,unsigned long count);
unsigned long copy(void *to,const void *from,unsigned long count);
用户空间和内核空间拷贝数据
实践环节--在安卓系统中编写字符驱动
编写一个名字为hello的字符驱动设备(参照书本和罗升阳博客),自己跟着一遍遍敲打出来,还是会遇到很多编译错误,也学习到很多东西,自己动手,丰衣足食,理解也更深!
学习的过程中,大家别跟着代码从头敲到尾,要按照思路来写,比如我先定义一些需要的变量和方法,然后先写一些空方法,把框架搭好,module_init(),module_exit()等相关方法
再去传统操作设备的方法,接着再去写devfs文件系统的方法
有思路的去写!!!
有些东西已经抛弃了
init_MUTEX(),在内核2.6版本就抛弃了,取而代之的是sema_init();
具体实现
kernel-3.18/drivers/misc/mediatek/创建hello目录
kernel-3.18/drivers/misc/mediatek/hello/
--hello.h
--hello.c
--Kconfig
--Makefile
hello.h
#include <linux/cdev.h>
#include <linux/semaphore.h>
//节点名称
#define HELLO_DEVICE_NODE_NAME "hello"
//定义hello_dev结构体
struct hello_dev {
int val;//变量--模拟寄存器
struct semaphore sem;//信号量 互斥访问 防止竞态
struct cdev dev;//字符设备
};
hello.c
引入头文件,定义相关的方法
#include <linux/init.h>
#include <linux/module.h>
#include <asm/uaccess.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/semaphore.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include "hello.h"
/*主设备号 和 从设备号 变量*/
static int hello_major = 0;
static int hello_minor = 0;
/*设备类别和设备变量*/
static struct class *hello_class = NULL;
static struct hello_dev* hello_dev = NULL;
/*传统的设备文件操作方法*/
static int hello_open(struct inode *inode,struct file *filp);
static int hello_release(struct inode *inode,struct file *filp);
static ssize_t hello_read(struct file *filp,char __user *buf,
size_t count,loff_t *f_ops);
static ssize_t hello_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_ops);
/*设备文件操作方法表*/
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.write = hello_write,
};
/*访问设置属性方法*/
static ssize_t hello_val_show(struct device *dev,struct device_attribute *attr,char *buf);
static ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count);
/*定义设备属性*/
static DEVICE_ATTR(val,S_IRUGO | S_IWUSR,hello_val_show,hello_val_store);
传统的设备文件操作方法
定义传统的设备文件访问方法,主要是定义hello_open、hello_release、hello_read和hello_write这四个打开、释放、读和写设备文件的方法:
/*打开设备方法*/
static int hello_open(struct inode *inode,struct file *filp) {
struct hello_dev *dev;
dev = container_of(inode->i_cdev,struct hello_dev,dev);
filp->private_data = dev;
return 0;
}
/*是否设备方法 这里空实现*/
static int hello_release(struct inode *inode,struct file * filp) {
return 0;
}
/*读取寄存器设备 val的值*/
static ssize_t hello_read(struct file *filp,char __user *buf,
size_t count,loff_t *f_ops) {
ssize_t err = 0;
struct hello_dev *dev = filp->private_data;
/*同步访问*/
if(down_interruptible(&(dev->sem)));
return -ERESTARTSYS;
if(count < sizeof(dev->val)){
goto out;
}
/*将寄存器val的值拷贝到用户提供的缓存区*/
if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){
err = -EFAULT;
goto out;
}
out:
up(&(dev->sem));
return err;
}
/*写设备寄存器val的值*/
return 0;
}
/*读取寄存器设备 val的值*/
static ssize_t hello_read(struct file *filp,char __user *buf,
size_t count,loff_t *f_ops) {
ssize_t err = 0;
struct hello_dev *dev = filp->private_data;
/*同步访问*/
if(down_interruptible(&(dev->sem)));
return -ERESTARTSYS;
if(count < sizeof(dev->val)){
goto out;
}
/*将寄存器val的值拷贝到用户提供的缓存区*/
if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){
err = -EFAULT;
goto out;
}
out:
up(&(dev->sem));
return err;
}
/*写设备寄存器val的值*/
static ssize_t hello_write(struct file *filp,const char __user *buf,
size_t count,loff_t * f_ops){
ssize_t err = 0;
struct hello_dev *dev = filp->private_data;
/*同步访问*/
if(down_interruptible(&(dev->sem))){
return -ERESTARTSYS;
}
if(count != sizeof(dev->val)) {
goto out;
}
/*将用户提供的缓存区的值写到设备寄存器中去*/
if(copy_from_user(&(dev->val),buf,count)){
err = EFAULT;
goto out;
}
err = sizeof(dev->val);//成功写入的数据大小
out:
up(&(dev->sem));
return err;
}
/*
字符设备注册和初始化的方式
方式2:将cdev内嵌到自己的设备结构中
调用以下函数来初始化
void cdev_init(strcuct cdev *cdev,struct file_operations *fops);
指定cdev的所有者和操作函数
cdev.ower = THIS_MODULE;
cdev.ops = &my_ops;
把字符设备添加到内核
}
/*写设备寄存器val的值*/
static ssize_t hello_write(struct file *filp,const char __user *buf,
size_t count,loff_t * f_ops){
ssize_t err = 0;
struct hello_dev *dev = filp->private_data;
/*同步访问*/
if(down_interruptible(&(dev->sem))){
return -ERESTARTSYS;
}
if(count != sizeof(dev->val)) {
goto out;
}
/*将用户提供的缓存区的值写到设备寄存器中去*/
if(copy_from_user(&(dev->val),buf,count)){
err = EFAULT;
goto out;
}
err = sizeof(dev->val);//成功写入的数据大小
out:
up(&(dev->sem));
return err;
}
devfs文件系统访问方法
定义通过devfs文件系统访问方法,这里把设备的寄存器val看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现hello_val_show和hello_val_store两个方法,同时定义了两个内部使用的访问val值的方法__hello_get_val和__hello_set_val
//---------------------------方式2----------------------------
/*读取寄存器val的值到缓存区buf中 内部使用*/
static ssize_t __hello_get_val(struct hello_dev *dev,char *buf) {
int val = 0;
/*同步访问*/
if(down_interruptible(&(dev->sem))){
return -ERESTARTSYS;
}
val = dev->val;
up(&(dev->sem));
return snprintf(buf,PAGE_SIZE,"%d\n",val);
}
/*把缓冲区buf的值写到设备寄存器val中去 内部使用*/
static ssize_t __hello_set_val(struct hello_dev *dev,const char *buf,size_t count){
int val = 0;
//把字符串转换成数字
val = simple_strtol(buf,NULL,10);
//同步访问
if(down_interruptible(&(dev->sem))){
return -ERESTARTSYS;
}
dev->val = val;
up(&(dev->sem));
return count;
}
/*读取设备属性 val*/
static ssize_t hello_val_show(struct device *dev,struct device_attribute *attr,char *buf){
struct hello_dev *hdev = (struct hello_dev*)dev_get_drvdata(dev);
return __hello_get_val(hdev,buf);
}
/*写设备属性val*/
static ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count){
struct hello_dev *hdev = (struct hello_dev*)dev_get_drvdata(dev);
return __hello_set_val(hdev,buf,count);
}
定义模块加载和卸载方法,这里只要是执行设备注册和初始化操作
/*初始化设备*/
static int hello_setup_dev(struct hello_dev *dev){
int err;
dev_t devno = MKDEV(hello_major,hello_minor);
//先清空dev,书上没有调用
memset(dev,0,sizeof(struct hello_dev));
//调用cdev_init初始化,接着绑定操作设备的方法
cdev_init(&(dev->dev),&hello_fops);
dev->dev.owner = THIS_MODULE;
dev->dev.ops = &hello_fops;
//注册字符设备
err = cdev_add(&(dev->dev),devno,1);
if(err)
return err;
//初始化信号量和寄存器的值
sema_init(&(dev->sem),1);
dev->val = 0;
return 0;
}
/*模块加载方法*/
static int hello_init(void){
int err = -1;
dev_t dev = 0;
struct device* temp = NULL;
printk("[%s] is init\n",__func__);
//动态分配主设备号和从设备好
err = alloc_chrdev_region(&dev,0,1,HELLO_DEVICE_NODE_NAME);
if(err < 0){
printk("faile to alloc_chrdev_region()\n");
goto fail;
}
hello_major = MAJOR(dev);
hello_minor = MINOR(dev);
//分配内存
hello_dev = kmalloc(sizeof(struct hello_dev),GFP_KERNEL);
if(!hello_dev){
err = -ENOMEM;
printk("faile to alooc hello_dev\n");
goto unregister;
}
//初始化设备
err = hello_setup_dev(hello_dev);
if(err){
printk("failed to set up hello device\n");
goto cleanup;
}
//在 /sys/class目录下创建设备类别目录hello
hello_class = class_create(THIS_MODULE,"hello");
if(IS_ERR(hello_class)){
err = -1;
printk(KERN_ALERT"Failed to create hello class.\n");
goto destroy_cdev;
}
//在dev目录 和 sys/class/hello目录下分别创建设备文件hello
temp = device_create(hello_class,NULL,dev,NULL,"hello");
if(IS_ERR(temp)){
err = -2;
printk(KERN_ALERT"Failed to create hello device.");
goto destroy_class;
}
//在 sys/class/hello/hello目录下创建属性文件val
err = device_create_file(temp,&dev_attr_val);
if(err < 0){
printk(KERN_ALERT"Failed to create attribute val.");
goto destroy_device;
}
dev_set_drvdata(temp,hello_dev);
printk("success to init hello device\n");
return 0;
destroy_device:
device_destroy(hello_class, dev);
destroy_class:
class_destroy(hello_class);
destroy_cdev:
cdev_del(&(hello_dev->dev));
cleanup:
kfree(hello_dev);
unregister:
unregister_chrdev_region(MKDEV(hello_major,hello_minor),1);
fail:
return err;
}
/*模块卸载方法*/
static void hello_exit(void){
dev_t devno = MKDEV(hello_major,hello_minor);
printk("destroy hello device \n");
//销毁设备类别和设备
if(hello_class){
device_destroy(hello_class,MKDEV(hello_major,hello_minor));
class_destroy(hello_class);
}
//删除字符设备和释放设备内存
if(hello_dev){
cdev_del(&(hello_dev->dev));
kfree(hello_dev);
}
//释放设备号
unregister_chrdev_region(devno,1);
}
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("First Andorid Driver");
module_init(hello_init);
module_exit(hello_exit);
Kconfig
config HELLO
tristate "First Android Driver"
default n
help
This is my first android driver.
Makefile
obj-y += hello.o
接着
kernel-3.18/drivers/misc/mediatek/Kconfig添加一句
source "drivers/misc/mediatek/hello/Kconfig"
kernel-3.18/drivers/misc/mediatek/Makefile添加一句
obj-y += hello/
最后 使用编译命令 ./mk bootimage
把生成的bootimage刷机即可
结果
进入到dev目录,可以看到hello设备文件:
进入到sys/class目录,可以看到hello目录,hello文件,
进入到下一层hello目录,可以看到val文件:
重启问题
static int hello_init(void){
int err = -1;
dev_t dev = 0;
printk("[%s] is init\n",__func__);
//动态分配主设备号和从设备好
err = alloc_chrdev_region(dev,0,1,HELLO_DEVICE_NODE_NAME);
//省略代码
}
问题出在alloc_chrdev_region()这里,原来发现第一个参数dev需要的是地址,导致了重启
修改:alloc_chrdev_region(&dev,0,1,HELLO_DEVICE_NODE_NAME);