(一)概念
-
字符设备
Linux内核中将设备按照硬件操作特性分为三类:
1)字符设备 :读写时以字节为单位 读写顺序固定
例如 键盘 鼠标 触摸屏 …2)块设备: 读写时以扇区(多字节)为单位 读写顺序不固定
例如 硬盘 flash …3)网络设备: 读写时以帧(多字节)为单位 读写顺序固定
在linux系统中实现一个字符设备硬件驱动:例如 键盘,实则就是在内核中实例化一个struct cdev类型的对象struct cdev { //设备号 dev_t dev; const struct file_operations *ops; ... }
-
设备号
设备号的数据类型: dev_t 32位系统中就是unsigned int
设备号 = 主设备号(高12bit) + 次设备号(低20bit)
主设备号, 用于区分不同类型的设备
次设备号, 用于区分同一类设备中的不同个体
设备号的注册方式:
1)静态注册
选一个未被使用的主设备:0~255
如何找到哪些主设备号已经被占用:
cat /proc/devices从中选择一个未被使用的主设备号为我所用
注册设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
from,要注册的起始设备号
count, 要连续注册的个数
name, 名称,注册成功后可cat /proc/devices看到对应主设备号的名称
注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count)
2)动态注册
由内核分配一个未被使用的主设备号为我所用
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
dev,传出参数 用于返回被注册成功的第一个设备号
baseminor,起始次设备号
count, 连续注册的设备号个数
注销:unregister_chrdev_region
(二)API
-
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
//给struct cdev.ops 操作函数集合赋值
cdev->ops = fops;
} -
//注册cdev
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
//给struct cdev.dev赋值设备号
p->dev = dev;
…
} -
void cdev_del(struct cdev *p)//注销cdev
-
MKDEV//宏,将主设备号和次设备号组合, 如dev = MKDEV(major, minor)<---->dev = major << 20 | minor
-
MAJOR和MINOR//宏,从设备号中提取主设备号/次设备号
(三)代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
MODULE_LICENSE("GPL");
unsigned int major = 0;
unsigned int minor = 10;
//设备号
dev_t dev = 0;
/*定义一个cdev变量*/
struct cdev led_cdev;
int led_open(struct inode *inode, struct file *filp)
{
printk("<1>" "enter %s\n", __func__);
return 0;
}
int led_release(struct inode *inode, struct file *filp)
{
printk("<1>" "enter %s\n", __func__);
return 0;
}
struct file_operations led_fops=
{
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
};
static int __init led_drv_init(void)
{
if(major)
{
//dev = major<<20 | minor;
dev = MKDEV(major, minor);
register_chrdev_region(dev, 1, "myleds");
}
else
{
alloc_chrdev_region(&dev, minor, 1, "myleds");
major = MAJOR(dev);
minor = MINOR(dev);
printk("<1>" "major=%d minor=%d\n",major, minor);
}
/*初始化cdev*/
cdev_init(&led_cdev, &led_fops);
/*注册cdev*/
/*
*led_cdev和申请得到的设备号发生关联
* */
cdev_add(&led_cdev, dev, 1);
return 0;
}
static void __exit led_drv_exit(void)
{
/*注销 cdev*/
cdev_del(&led_cdev);
unregister_chrdev_region(dev, 1);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
测试代码
test.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd = 0;
fd = open("/dev/myleds", O_RDONLY);
if(fd < 0)
{
perror("open failed!");
return -1;
}
printf("open successed.....\n");
sleep(10);
printf("is closing the device ....\n");
close(fd);
return 0;
}
执行效果:
- 安装模块时显示主设备号和从设备号
- 安装完模块之后,手动创建设备文件
mknod /dev/xxx 主设备号 次设备号
(其中,xxx任意,相当于给设备起名字,但是测试程序中打开的设备,一定要是这个设备,测试程序中写的myleds,所以这里xxx就是myleds) - 执行测试程序,会发现内核中的led_open函数被调用,然后10秒后led_release被调用。
总结:如linux驱动开发LDD(三)中所记录,系统调用最终会调用到该类设备或者该类文件中对应的函数。在本例中,用户空间调用open函数,最终会在内核态执行led_open,在用户空间执行close,会在内核态执行led_release。只要把相关的硬件驱动代码放入相关的函数中,就可以实现硬件的驱动了。
(四) 设备文件的自动创建
必备条件:
-
根文件系统中存在可以执行成mdev
-
挂载了 procfs sysfs两个文件系统
-
配置了热插拔事件
echo /sbin/mdev > /proc/sys/kernel/hotplug
热插拔:/sys目录下文件的变化 (变多 或者变少) -
编程 安装 卸载模块时产生热插拔事件,四个函数:
1)class_create,宏
owner,THIS_MODULE
name, 在/sys/class下创建一个新的类别#define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ })
2)struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, …)
class,创建的devic属于哪个类
parent,父设备,可以为NULL
devt, 设备号
drvdata,创建设备时传递的私有数据,可以为NULL
fmt, …: 可变参数,用来控制设备名。类似printf的fmt,…3)void device_destroy(struct class *class, dev_t devt)
4)void class_destroy(struct class *cls)
关于自动创建设备文件的代码,见下一章节