参考:
https://blog.csdn.net/zqixiao_09/article/details/50839042
static struct cdev my_cdev; //设备属性结构体
static dev_t dev_from; //设备号
static struct class *test_class = NULL;
test_class = class_create(THIS_MODULE, "ljj_class"); //创建设备类
device_create(test_class, NULL, dev_from, NULL, "test"); //创建设备文件/dev/test
1.注册字符设备驱动新接口1
1.1、新接口与老接口
(1)老接口:register_chrdev
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
register_chrdev = MKDEV(MYMAJOR, 0) + register_chrdev_region + cdev_init + cdev_add
(2)新接口:
register_chrdev_region (dev_t from, unsigned count, const char *name)
函数用于已知起始设备的设备号的情况
- #define MYMAJOR 200
- mydev = MKDEV(MYMAJOR, 0); //生成设备号
- retval = register_chrdev_region(mydev, MYCNT, MYNAME);
alloc_chrdev_region (dev_t *dev, unsigned baseminor, unsigned count,const char *name);
用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中
unregister_chrdev_region (dev_t from, unsigned count)
释放原先申请的设备号
1.2、cdev介绍 在cdev.h文件定义
(1)结构体
/*
*内核源码位置
*linux2.6.38/include/linux/cdev.h
*/
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针,一般初始化为:THIS_MODULE
const struct file_operations *ops; //字符设备用到的一个重要的结构体file_operations,cdev初始化时与之绑定
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //主设备号24位 与次设备号8位,dev_t为32位整形,每个系统给定义不一样
unsigned int count; //使用该字符设备驱动的个数.
};
(2)相关函数:
cdev_alloc:用于动态申请一个cdev内存,减少栈的压力
cdev_init:用于初始化cdev的成员,并建立cdev和file_operations之间的连接
cdev_add:注册,它的调用通常发生在字符设备驱动模块加载函数中
cdev_del:注销,它的函数的调用则通常发生在字符设备驱动模块卸载函数中
- cdev = cdev_alloc();
- cdev->owner = fops->owner;
- cdev->ops = fops;
这几步等于:cdev_init(),因为cdev_alloc()里面做一些cdev_init()的工作
1.3、设备号
(1)主设备号和次设备号
(2)dev_t类型
dev设备号的主设备号占几位,在各个系统给定义不一样,所以用下面的宏来操作
(3)MKDEV、MAJOR、MINOR三个宏
1) -- 从设备号中提取major和minor
MAJOR(dev_t dev);
MINOR(dev_t dev);
2) -- 通过major和minor构建设备号
MKDEV(int major,int minor);
注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;
//宏定义:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) // = 1111 1111 1111 1111 1111
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))</span>
实例代码:
#define MYMAJOR 251
#define MYNAME "testchar"
#define MYCOUNT 0x1
static struct cdev my_cdev;
static dev_t dev_from;
//第1步:注册/分配主次设备号
dev_from = MKDEV(MYMAJOR, 0);
retval = register_chrdev_region(dev_from, MYCOUNT, MYNAME);
if (retval){
printk(KERN_INFO "register_chrdev_region error\n");
return -EINVAL;
}else{
printk(KERN_INFO "register number sucess\n");
}
//第2步:注册字符设备驱动
cdev_init(&my_cdev, &test_fops);
if (cdev_add(&my_cdev, dev_from, MYCOUNT)) {
printk(KERN_INFO "cdev_add error\n");
return -EINVAL;
}else{
printk(KERN_INFO "register driver sucess\n");
}
3.注册字符设备驱动新接口3
2.1、使用alloc_chrdev_region自动分配设备号
(1)register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。
(2)更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。
(3)自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去mknod创建他对应的设备文件。
#define MYMAJOR 251
#define MYNAME "testchar"
#define MYCOUNT 0x1
static struct cdev *p_my_cdev;
static dev_t dev_from;
//第1步:注册/分配主次设备号
retval = alloc_chrdev_region(&dev_from, 0, MYCOUNT, MYNAME); //自动分配设备号
if (retval){
printk(KERN_INFO "register_chrdev_region error\n");
goto err;
}else{
printk(KERN_INFO "主设备:= %d ,次设备 = %d\n", MAJOR(dev_from), MINOR(dev_from));
printk(KERN_INFO "register number sucess\n");
}
//第2步:注册字符设备驱动
p_my_cdev = cdev_alloc(); //自动分配空间
cdev_init(p_my_cdev, &test_fops);
cdev_add(p_my_cdev, dev_from, MYCOUNT)
2.2、得到分配的主设备号和次设备号
(1)使用MAJOR宏和MINOR宏从dev_t得到major和minor
(2)反过来使用MKDEV宏从major和minor得到dev_t。
(3)使用这些宏的代码具有可移植性
4.cdev 结构和inode 结构体
cdev结构体:
struct cdev {
struct kobject kobj; //内嵌的kobject结构,用于内核设备驱动模型的管理
struct module *owner; //指向包含该结构的模块的指针,用于引用计数
const struct file_operations *ops; //指向字符识别操作的指针
struct list_head list; //该结构将使用该驱动的字符设备的链接成一个链表
dev_t dev;
unsigned int count;//使用该设备驱动的数量
};
一个cdev结构相当于一个字符设备
inode结构体:
inode和cdev的关系
每一个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode结点。这样可以通过inode结点的i_cdev字段找到cdev字符结构体。通过cdev的ops指针,就能找到设备A的操作函数。
内核使用inode结构在内部表示文件。inode一般作为file_operations结构中函数的参数传递过来。例如, open()函数将传递一个inode指针进来,表示目前打开的文件结点。需要注意的是, inode的成员已经被系统赋予了合适的值,驱动程序只需要使用该结点中的信息,而不用更改。Oepn()函数为:
int (*open) (struct inode *, struct file *);
除了从dev_t得到主设备号和次设备号外,这里还可以使用imajor()和iminor)函数从inode->irdev中得到主设备号和次设备号。
static inline unsigned iminor(const struct inode *inode)
{
return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
return MAJOR(inode->i_rdev);
}
cdev_alloc
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
cdev_init
一般的赋值步骤:
- cdev = cdev_alloc();
- cdev->owner = fops->owner;
- cdev->init(cdev, fops);
- cdev_add(&dev->cdev, 设备号, 1);
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
6.自动创建字符设备驱动的设备文件
1.使用mknod创建设备文件的缺点
麻烦,还要手动创建,可移植性差
2、解决方案:udev(嵌入式中用的是mdev)
(1)什么是udev?应用层的一个应用程序
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3)应用层启用udev,内核驱动中使用相应接口
(4)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
3、内核驱动设备类相关函数
(1)class_create// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
// 给udev,让udev自动创建和删除设备文件
(2)device_create// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
// 所以我们这里要的文件名是/dev/test
4、编程实践
class_create(owner, name) //创建一个设备类
owner:THIS_MODULE
name : 名字//创建后会产生/sys/class/ljj_class,/sys/devices/virtual/ljj_class
//和 ls /sys/class/ljj_class/test/
-r--r--r-- 1 root 0 4096 Jan 1 12:01 dev
drwxr-xr-x 2 root 0 0 Jan 1 12:03 power
lrwxrwxrwx 1 root 0 0 Jan 1 12:03 subsystem -> ../../../../class/ljj_class
-rw-r--r-- 1 root 0 4096 Jan 1 12:03 ueventvoid class_destroy(struct class *cls) //销毁一个设备类
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...) //创建一个字符设备文件
struct class *class :类
struct device *parent:NULL
dev_t devt :设备号
void *drvdata :NULL
const char *fmt :名字device_destroy(struct class *class, dev_t devt); //销毁一个字符设备文件
//创建设备文件
test_class = class_create(THIS_MODULE, "ljj_class"); //创建设备类/sys/class/ljj_class
if (IS_ERR(test_class))
goto error;
device_create(test_class, NULL, dev_from, NULL, "test"); //创建设备文件/dev/test
//销毁设备文件/dev/test
device_destroy(test_class, dev_from);
class_destroy(test_class);
7.设备类相关代码分析1
1、sys文件系统简介
(1)sys文件系统的设计思想
(2)设备类的概念
(3)/sys/class/xxx/中的文件的作用
2、
(1)
class_create
__class_create
__class_register
kset_register
kobject_uevent
(2)
device_create
device_create_vargs
kobject_set_name_vargs
device_register
device_add
kobject_add
device_create_file
device_create_sys_dev_entry
devtmpfs_create_node
device_add_class_symlinks
device_add_attrs
device_pm_add
kobject_uevent
8.静态映射表建立过程分析
1、建立映射表的三个关键部分
(1)映射表具体物理地址和虚拟地址的值相关的宏定义
(2)映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。
在kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数
smdkc110_map_io
s5p_init_io
iotable_init
结论:经过分析,真正的内核移植时给定的静态映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。
(3)开机时调用映射表建立函数
问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?
start_kernel
setup_arch
paging_init
devicemaps_init
if (mdesc->map_io)
mdesc->map_io();
动态建立映射:
//动态分配虚拟空间
request_mem_region(FB_GPJ0_BASE + FB_GPJ0_CON, 4, "GPJ0CON") //申请空间
request_mem_region(FB_GPJ0_BASE + FB_GPJ0_DAT, 4, "GPJ0DAT")
pGPJ0CON = ioremap(FB_GPJ0_BASE + FB_GPJ0_CON, 4); //把申请的空间的起始地址赋值
pGPJ0DAT = ioremap(FB_GPJ0_BASE + FB_GPJ0_DAT, 4);
*(pGPJ0CON) = 0x11111111; //写值
*(pGPJ0DAT) = ((0<<3) | (0<<4) | (0<<5));
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(FB_GPJ0_BASE + FB_GPJ0_DAT, 4); //释放空间
release_mem_region(FB_GPJ0_BASE + FB_GPJ0_CON, 4);
9.内核提供的读写寄存器接口
1、前面访问寄存器的方式
优点:方便快捷
缺点:可以移植性差,在不同的CPU架构中,它的直接寻址会不一样,采用writel就会避免这样的情况。
2、内核提供的寄存器读写接口
(1)writel和readl
writel(v, c) :v为要写的值,c为要写的地址
(2)iowrite32和ioread32
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
实例代码:
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/ioport.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h> //copy_from_user
#include <asm/string.h>
#include <asm/io.h>
#include <mach/gpio-bank.h>
#include <mach/regs-gpio.h>
#define FB_GPJ0_BASE 0xe0200240
#define FB_GPJ0_SIZE 8
#define FB_GPJ0_CON (unsigned int)0x0
#define FB_GPJ0_DAT (unsigned int)0x4
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
//注册
#define MYMAJOR 251
#define MYNAME "testchar"
#define MYCOUNT 0x1
//static struct cdev my_cdev;
static struct cdev *p_my_cdev;
static dev_t dev_from;
//设备文件
static struct class *test_class = NULL;
//GPIO
unsigned int *gpj0_base = NULL;
unsigned int *pGPJ0CON = NULL;
unsigned int *pGPJ0DAT = NULL;
char kernel_buf[100] = {0};
int mymajor = -1;//device number
//open function
static int test_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test open function\n");
++p_my_cdev->count; //使用该设备驱动的个数加 1
rGPJ0CON = 0x11111111;
return 0;
}
static int test_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test release function\n");
--p_my_cdev->count; //使用该设备驱动的个数减 1
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
return 0;
}
//read file
static ssize_t test_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
printk(KERN_INFO "test read function\n");
if (copy_to_user(buf, kernel_buf, strlen(kernel_buf)))
return -EINVAL;
printk(KERN_INFO "user-->kernel sucess \n");
return 0;
}
//write file
static ssize_t test_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos)
{
printk(KERN_INFO "test write function\n");
memset(kernel_buf, 0, sizeof(kernel_buf));
if (copy_from_user(kernel_buf, user_buf, strlen(user_buf)))
return -EINVAL;
switch(kernel_buf[0]){
case '1':
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
break;
case '0':
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
break;
}
printk(KERN_INFO "kernel-->user sucess \n");
return 0;
}
//file_operation结构体
static const struct file_operations test_fops = {
.open = test_open,
.write = test_write,
.read = test_read,
//.llseek = seq_lseek,
.release = test_release,
.owner = THIS_MODULE,
};
// 模块安装函数
static int __init chrdev_init(void)
{
int retval = -1;
printk(KERN_INFO "chrdev_init helloworld init\n");
//第1步:注册/分配主次设备号
retval = alloc_chrdev_region(&dev_from, 0, MYCOUNT, MYNAME);
if (retval){
printk(KERN_INFO "register_chrdev_region error\n");
goto err;
}else{
printk(KERN_INFO "主设备:= %d ,次设备 = %d\n", MAJOR(dev_from), MINOR(dev_from));
printk(KERN_INFO "register number sucess\n");
}
//第2步:注册字符设备驱动
p_my_cdev = cdev_alloc();
cdev_init(p_my_cdev, &test_fops);
if (cdev_add(p_my_cdev, dev_from, MYCOUNT)) {
printk(KERN_INFO "cdev_add error\n");
goto err_cdev_init;
}else{
printk(KERN_INFO "register driver sucess\n");
}
//创建设备文件
test_class = class_create(THIS_MODULE, "ljj_class");//创建设备类
if (IS_ERR(test_class))
goto err_mem_region_con;
device_create(test_class, NULL, dev_from, NULL, "test");//创建设备文件
//动态分配虚拟空间
if (!request_mem_region(FB_GPJ0_BASE + FB_GPJ0_CON, 4, "GPJ0CON"))
{
printk(KERN_ERR "request_mem_region error\n");
goto err_mem_region_con;
}
if (!request_mem_region(FB_GPJ0_BASE + FB_GPJ0_DAT, 4, "GPJ0DAT"))
{
printk(KERN_ERR "request_mem_region error\n");
goto err_mem_region_dat;
}
pGPJ0CON = ioremap(FB_GPJ0_BASE + FB_GPJ0_CON, 4);
pGPJ0DAT = ioremap(FB_GPJ0_BASE + FB_GPJ0_DAT, 4);
*(pGPJ0CON) = 0x11111111;
*(pGPJ0DAT) = ((0<<3) | (0<<4) | (0<<5));
printk(KERN_INFO "register_chrdev success...\n");
return 0;
err_mem_region_dat:
release_mem_region(FB_GPJ0_BASE + FB_GPJ0_DAT, 4);
release_mem_region(FB_GPJ0_BASE + FB_GPJ0_CON, 4);
err_mem_region_con:
cdev_del(p_my_cdev);
err_cdev_init:
unregister_chrdev_region(dev_from, MYCOUNT);
err:
return -EINVAL;
}
// 模块下载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
*(pGPJ0DAT) = ((1<<3) | (1<<4) | (1<<5));
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(FB_GPJ0_BASE + FB_GPJ0_CON, 4);
release_mem_region(FB_GPJ0_BASE + FB_GPJ0_DAT, 4);
//销毁设备文件/dev/test
device_destroy(test_class, dev_from);
class_destroy(test_class);
// 第一步真正注销字符设备驱动用cdev_del
cdev_del(p_my_cdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(dev_from, MYCOUNT);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息